Главная » Хабрахабр » OS1: примитивное ядро на Rust для x86. Часть 2. VGA, GDT, IDT

OS1: примитивное ядро на Rust для x86. Часть 2. VGA, GDT, IDT

Первая часть

Я дал несколько полезных ссылок, рассказал, как размещается загруженное ядро в памяти, как соотносятся виртуальные и физические адреса при загрузке, а так же как включить поддержку механизма страниц. Первая статья еще не успела остыть, а я решил не держать вас в интриге и написать продолжение.
Итак, в предыдущей статье мы поговорили о линковке, загрузке файла ядра и первичной инициализации. Пришло время двигаться дальше и узнать, насколько глубока кроличья нора! В последнюю очередь управление перешло в функцию kmain моего ядра, написанного на Rust.

Всех заинтересованных прошу под кат, и мы начинаем. В этой части заметок я кратко опишу свою конфигурацию Rust, в общих чертах расскажу про вывод информации в VGA, и детально о настройке сегментов и прерываний.

Однако, на некоторых моментах я все же остановлюсь. В целом, ничего особо сложного в этой процедуре нет, за подробностями можно обратиться в блог Филлиппа.

Будьте внимательны, как-то раз после обновления до latest я получил полностью нерабочий компилятор и пришлось откатываться до ближайшей стабильной. Некоторые фичи, необходимые для низкоуровневой разработки, стабильный Rust все еще не поддерживает, поэтому, чтобы отключить стандартную библиотеку и собираться на Bare Bones, нам необходим Rust nightly. Если вы уверены, что вчера ваш компилятор работал, а обновился и не работает — выполните команду, подставив нужную вам дату

rustup override add nightly-YYYY-MM-DD

Я основывался на блоге Филиппа Оппермана, поэтому многие вещи в этом разделе взяты у него, разобраны по косточкам и адаптированы под мои нужды. За деталями механизма можно обратиться сюда.
Далее настроим целевой платформу, под которую будем собираться. Привожу его полностью Филипп в своем блоге разрабатывает для x64, я же изначально выбрал x32, поэтому мой target.json будет несколько отличаться.

Документация LLVM говорит нам, что это параметры раскладки данных, разделенные “-”. Самое сложное здесь — параметр “data-layout”. Второй символ — m, “искажение”. Самый первый символ “e” отвечает за индианность — в нашем случае это little-endian, как того требует платформа. Так как наш выходной формат будет ELF (смотри скрипт компоновки), мы выбираем значение “m:e”. Отвечает за имена символов при компоновке. Тут все просто, у нас 32 бита, так что смело ставим “p:32:32”. Третий символ — размер указателя в битах и ABI (Application binary interface). Мы сообщаем, что поддерживаем 64-разрядные числа по ABI 32 с выравниванием 64 — “f64:32:64”, а также 80-ти разрядные числа с выравниванием по умолчанию — “f80:32”. Далее — числа с плавающей точкой. Начинаем с 8 бит и двигаемся к максимуму платформы в 32 бита — “n8:16:32”. Следующий элемент — целые числа. Мне нужны даже 128 разрядные целые, так что пусть будет S128. Последний — выравнивание стека. В любом случае, LLVM этот параметр может смело проигнорировать, это наше предпочтение.

По поводу остальных параметров можно подсмотреть у Филиппа, он хорошо все объясняет.

Еще нам понадобится cargo-xbuild — инструмент, который позволяет делать кросс-компиляцию rust-core при сборке под незнакомую платформу target.
Устанавливаем.

cargo install cargo-xbuild

Собирать будем вот так.

cargo xbuild -Z unstable-options --manifest-path=kernel/Cargo.toml --target kernel/targets/$(ARCH).json --out-dir=build/lib

Указание манифеста мне понадобилось для корректной работы Make, так как он запускается из корневого каталога, а ядро лежит в каталоге kernel.

Его мы в дальнейшем скормим в LLD. Из особенностей манифеста могу выделить только crate-type = ["staticlib"], который дает на выходе линкуемый файл.

В нем с помощью атрибутов настраиваются фичи языка, а также располагается заветная kmain. Согласно соглашениям Rust, если мы создаем статическую библиотеку (или “плоский” бинарный файл), в корне крэйта должен находиться файл lib.rs, который является точкой входа.

Это делается макросом Итак, на первом шаге нам понадобится отключить std-библиотеку.

#![no_std]

Более того, мы даже лишаем себя макроса println!, так что реализовать его придется самостоятельно. Таким нехитрым шагом мы сразу забываем про многопоточность, динамическую память и прочие прелести стандартной библиотеки. Как это сделать расскажу в следующий раз.

Мы пойдем другим путем. Многие туториалы где-то на этом месте и заканчиваются, выводя “Hello World” и не объясняя как же жить дальше. В первую очередь, нам нужно задать сегменты кода и данных для защищенного режима, настроить VGA, настроить прерывания, чем мы и займемся.

#![no_std] #[macro_use] pub mod debug; #[cfg(target_arch = "x86")] #[path = "arch/i686/mod.rs"] pub mod arch; #[no_mangle] extern "C" fn kmain(pd: usize, mb_pointer: usize, mb_magic: usize) { arch::arch_init(pd);
......
} #[panic_handler] fn panic(_info: &PanicInfo) -> ! { println!("{}", _info); loop {}
}

Как я уже сказал, мы отключаем стандартную библиотеку. Что здесь происходит? Я использую фичу Rust с конфигурациями, чтобы в разных архитектурных реализациях объявить одинаковые интерфейсы и использовать их на полную катушку. Еще мы объявлем два очень важных модуля — debug (в котором будем писать на экран) и arch (в котором будет жить вся платформозависимая магия). Здесь я останавливаюсь только на x86 и дальше говорим только о нем.

Потом можно будет его дорабатывать. Я объявил совершенно примитивный panic handler, наличия которого требует Rust.

Первый аргумент — адрес таблицы страниц PD, второй — физический адрес структуры GRUB, откуда мы будем доставать карту памяти, третий — магическое число. kmain принимает три аргумента, а также экспортируется в нотации Си без искажения имени, чтобы линкер смог корректно связать функцию с вызовом из _loader, который я описывал в предыдущей статье. В будущем я бы хотел реализовать как поддержку Multiboot 2, так и собственный загрузчик, поэтому использую магическое число для идентификации способа загрузки.

Идем внутрь. Первый же вызов kmain — платформозависимая инициализация. Функция arch_init располагается в файле arch/i686/mod.rs, публична, специфична для платформы x86 в 32 бит, и выглядит так:

pub fn arch_init(pd: usize) { unsafe { vga::VGA_WRITER.lock().init(); gdt::setup_gdt(); idt::init_idt(); paging::setup_pd(pd); }
}

Начнем с VGA. Как можно увидеть, для x86 по порядку инициализируется вывод, сегментация, прерывания и страничная организация памяти.

По этой причине пройдусь максимально кратко, остановлюсь только на фишках, которые сделал сам. Каждый туториал считает своим долгом напечатать Hello World, поэтому как работать с VGA вы найдете везде. const fn еще не в релизе, поэтому красиво статические инициализации сделать пока нельзя. По использованию lazy_static отправлю вас в блог Филиппа и не буду детально разъяснять. А еще добавим спин-блокировку, дабы не получилась полная каша.

use lazy_static::lazy_static;
use spin::Mutex; lazy_static! { pub static ref VGA_WRITER : Mutex<Writer> = Mutex::new(Writer { cursor_position: 0, vga_color: ColorCode::new(Color::LightGray, Color::Black), buffer: unsafe { &mut *(0xC00B8000 as *mut VgaBuffer) } });
}

Так как мы уже включили виртуальную память, обращение по этому адресу вызовет крах, поэтому добавляем 3 ГиБ. Как известно, буфер экрана находится по физическому адресу 0xB8000 и имеет размер 80*25 байт (ширина и высота экрана). Также мы разыменовываем сырой указатель, что небезопасно — но мы ведь знаем, что делаем.
Из интересного в этом файле пожалуй только реализация структуры Writer, которая позволяет не только выводить символы подряд, но и делать скроллинг, переход в любое место экрана и прочую приятную мелочь.

VGA Writer

pub struct Writer { cursor_position: usize, vga_color: ColorCode, buffer: &'static mut VgaBuffer,
} impl Writer { pub fn init(&mut self) { let vga_color = self.vga_color; for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code: vga_color, } } } self.set_cursor_abs(0); } fn set_cursor_abs(&mut self, position: usize) { unsafe { outb(0x3D4, 0x0F); outb(0x3D5, (position & 0xFF) as u8); outb(0x3D4, 0x0E); outb(0x3D4, ((position >> 8) & 0xFF) as u8); } self.cursor_position = position; } pub fn set_cursor(&mut self, x: usize, y: usize) { self.set_cursor_abs(y * VGA_WIDTH + x); } pub fn move_cursor(&mut self, offset: usize) { self.cursor_position = self.cursor_position + offset; self.set_cursor_abs(self.cursor_position); } pub fn get_x(&mut self) -> u8 { (self.cursor_position % VGA_WIDTH) as u8 } pub fn get_y(&mut self) -> u8 { (self.cursor_position / VGA_WIDTH) as u8 } pub fn scroll(&mut self) { for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = self.buffer.chars[(y + 1) * VGA_WIDTH + x] } } for x in 0..VGA_WIDTH { let color_code = self.vga_color; self.buffer.chars[(VGA_HEIGHT - 1) * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code } } } pub fn ln(&mut self) { let next_line = self.get_y() as usize + 1; if next_line >= VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } else { self.set_cursor(0, next_line) } } pub fn write_byte_at_xy(&mut self, byte: u8, color: ColorCode, x: usize, y: usize) { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte_at_pos(&mut self, byte: u8, color: ColorCode, position: usize) { self.buffer.chars[position] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte(&mut self, byte: u8) { if self.cursor_position >= VGA_WIDTH * VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } self.write_byte_at_pos(byte, self.vga_color, self.cursor_position); self.move_cursor(1); } pub fn write_string(&mut self, s: &str) { for byte in s.bytes() { match byte { 0x20...0xFF => self.write_byte(byte), b'\n' => self.ln(), _ => self.write_byte(0xfe), } } }
}

Немного более интересны вызовы outb — никакими способами, кроме работы с портами ввода-вывода невозможно переместить курсор. При перемотке всего лишь происходит копирование участков памяти размером в ширину экрана назад, с заполнением пробелами новой строки (так я делаю очистку). Под спойлером ниже будет ассемблерный код. Впрочем, ввод-вывод через порты нам еще понадобится, поэтому они были вынесены в отдельный пакет и завернуты в безопасные обертки. Пока достаточно знать, что:

  • Выводится абсолютное смещение курсора, а не координаты
  • Выводить в контроллер можно по одному байту за раз
  • Вывод одного байта происходит в две команды — сначала пишем команду контроллеру, потом данные.
  • Порт для команд — 0x3D4, порт для данных — 0x3D5
  • Сначала выводим нижний байт положения командой 0x0F, затем верхний командой 0x0E

out.asm

Так как стек начинается с конца пространства и уменьшает указатель стека при вызове функции, чтобы получить параметры, точку возврата и прочее, к регистру ESP необходимо добавлять размер аргумента, выровненный на выравнивание стека — в нашем случае 4 байта. Обратите внимание на работу с переданными переменными в стеке.

global writeb
global writew
global writed
section .text writeb: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 byte value aligned 4 bytes out dx, al ;write byte by port number an dx - value in al mov esp, ebp pop ebp ret writew: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 word value aligned 4 bytes out dx, ax ;write word by port number an dx - value in ax mov esp, ebp pop ebp ret writed: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 double word value aligned 4 bytes out dx, eax ;write double word by port number an dx - value in eax mov esp, ebp pop ebp ret

Как я уже говорил в предыдущей статье, в моей голове смешались страничная и сегментная организация памяти, я загружал адрес таблицы страниц в GDTR и хватался за голову. Мы подобрались к самой головоломной, но в то же время самой простой теме. Возможно, я пал жертвой учебника Питера Абеля “Ассемблер. Мне потребовалось несколько месяцев, чтобы вдоволь начитаться материала, переварить его и суметь осознать. В те приятные времена мы загружали в сегментный регистр верхние 16 бит двадцатиразрядного адреса, и это был именно адрес в памяти. Язык и программирование для IBM PC” (великолепная книга!), в которой описана сегментация для Intel 8086. Жестоким разочарованием оказалось, что начиная с i286 в защищенном режиме все совсем не так.

Программистам приходилось думать, как размещать исполняемый код, как размещать данные, как соблюдать их безопасность. Итак, голая теория гласит, что x86 поддерживает сегментную модель память, так как старые программы только так могли вырваться за пределы 640 КиБ, а потом и 1 МиБ памяти. Некоторые инструкции процессора запрещены при уровне привилегий слабее 0, а доступ между сегментами программ и ядра вызовет ошибку сегментации. Приход страничной организации сделал сегментную организацию ненужной, но она осталась с целью совместимости и защиты (разделения привилегий на kernel-space и user-space), так что без нее просто никуда.

Давайте еще раз (надеюсь в последний) о преобразовании адресов
Линейный адрес [0x08:0xFFFFFFFF] -> Проверка прав сегмента 0x08 -> Виртуальный адрес [0xFFFFFFFF] -> Таблица страниц + TLB -> Физический адрес [0xAAAAFFFF]

Именно поэтому нельзя просто так взять и вызвать функцию ядра из пространства пользователя. Сегмент используется только внутри процессора, хранится в специальном сегментном регистре (CS, SS, DS, ES, FS, GS) и используется исключительно для проверки прав выполнения кода и передачи управления. Согласно конвенции x86, для защиты от несанкционированного доступа, сегмент с меньшими правами доступа не может напрямую вызвать сегмент с большими правами через jmp 0x08:[EAX], а обязан использовать другие механизмы, такие как трапы, гейты, прерывания. Сегмент с дескриптором 0x18 (у меня такой, у вас другой) имеет права уровня 3, а сегмент с дескриптором 0x08 имеет права уровня 0.

При переходе между сегментами (для упрощения, я допущу, что прямой переход возможен) необходимо вызвать инструкцию jmp 0x08:[EAX], где 0x08 — смещение первого валидного дескриптора в байтах от начала таблицы, а EAX — регистр, содержащий адрес перехода. Сегменты и их типы (код, данные, трапы, гейты) должны быть описаны в глобальной дескрипторной таблице GDT, виртуальный адрес и размер которой загружается в регистр GDTR. Каждый дескриптор — структура размером 8 байт. Смещение (селектор) будет загружен в регистр CS, а соответствующий ему дескриптор — в теневой регистр процессора. первую статью). Она хорошо задокументирована и ее описание можно найти как на OSDev, так и в документации Intel (см.

Когда мы инициализируем GDT и выполним переход jmp 0x08:[EAX], состояние процессора будет следующим: Резюмирую.

  • GDTR содержит виртуальный адрес GDT
  • CS содержит значение 0x08
  • В теневой регистр CS из памяти скопирован дескриптор по адресу [GDTR + 0x08]
  • Регистр EIP содержит адрес из регистра EAX

На дескрипторе TSS и его значении я остановлюсь подробнее когда будем обсуждать многопоточность. Нулевой дескриптор всегда должен быть неинициализирован и обращение по нему запрещено. Сейчас моя таблица GDT выглядит следующим образом:

extern { fn load_gdt(base: *const GdtEntry, limit: u16);
} pub unsafe fn setup_gdt() { GDT[5].set_offset((&super::tss::TSS) as *const _ as u32); GDT[5].set_limit(core::mem::size_of::<super::tss::Tss>() as u32); let gdt_ptr: *const GdtEntry = GDT.as_ptr(); let limit = (GDT.len() * core::mem::size_of::<GdtEntry>() - 1) as u16; load_gdt(gdt_ptr, limit);
} static mut GDT: [GdtEntry; 7] = [ //null descriptor - cannot access GdtEntry::new(0, 0, 0, 0), //kernel code GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //kernel data GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //user code GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //user data GdtEntry::new(0, 0xFFFFFFFF, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PAGE_SIZE | GDT_F_PROTECTED_MODE), //TSS - for interrupt handling in multithreading GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_TSS_AVAIL, 0), GdtEntry::new(0, 0, 0, 0),
];

Загрузка адреса и размера GDT выполняется через отдельную структуру, которая содержит всего два поля. А вот так выглядит инициализация, о которой я столько рассказывал выше. В регистры сегментов данных загружаем следующий дескриптор со смещением 0x10. В команду lgdt передается именно адрес этой структуры.

global load_gdt
section .text gdtr dw 0 ; For limit storage dd 0 ; For base storage load_gdt: mov eax, [esp + 4] mov [gdtr + 2], eax mov ax, [esp + 8] mov [gdtr], ax lgdt [gdtr] jmp 0x08:.reload_CS
.reload_CS: mov ax, 0x10 ; 0x10 points at the new data selector mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov ax, 0x28 ltr ax ret

Дальше все будет немного легче, но не менее интересно.

Для этого необходимо инициализировать контроллер прерываний. Собственно настало время дать нам возможность взаимодействовать с нашим ядром (по крайней мере, увидеть что мы нажимаем на клавиатуре).

Однако я осознанно решил не идти этим путем, так как я решил разделять ассемблер и Rust по разным файлам, а значит и функциям. Лирическое отступление о стиле кода.
Благодаря усилиям сообщества и конкретно Филиппа Оппермана, в Rust была добавлена конвенция вызовов x86-interrupt, которая позволяет писать обработчики прерываний, выполняющие iret. Мои обработчики прерываний написаны на ассемблере и делают ровно одну вещь: вызывают почти одноименные обработчики прерываний, написанные на Rust. Да, я неразумно использую стековую память, осознаю это, но это все еще вкусовщина. Пожалуйста, примите этот факт и отнеситесь снисходительно.

С другой стороны, нужно много однообразного кода. В целом, процесс инициализации прерываний похож на инициализацию GDT, но проще для понимания. Разработчики Redox OS делают красивое решение, используя все прелести языка, однако я пошел “в лоб” и решил допустить дублирование кода.

В данном контексте настройки они для нас практически не отличаются. Согласно конвенции x86, у нас есть прерывания, а есть исключительные ситуации. Например, я использую ее для обработки отсутствия страницы при работе с кучей (но всему свое время). Единственное отличие состоит в том, что при срабатывании исключительной ситуации, в стеке может содержаться дополнительная информация. Также необходимо запрограммировать PIC (Programmable Interrupt Controller). И прерывания, и исключения обрабатываются из одной таблицы, которую нам с вами и нужно заполнить. Есть еще APIC, но с ним я пока не разобрался.

Начну с обработчиков в ассемблере. По работе с PIC я не буду давать много комментариев, так как в сети много примеров по работе с ним. Они все совершенно однотипны, поэтому я уберу код под спойлер.

IRQ

global irq0
global irq1
......
global irq14
global irq15 extern kirq0
extern kirq1
......
extern kirq14
extern kirq15 section .text irq0: pusha call kirq0 popa iret irq1: pusha call kirq1 popa iret ...... irq14: pusha call kirq14 popa iret irq15: pusha call kirq15 popa iret

Обработка исключений абсолютно аналогична. Как можно заметить, все вызовы Rust функций начинаются с префикса “k” — для различия и удобства. Отличается обработчик Page Fault, но о нем — в заметках по управлению памятью. Для ассемблерных функций выбран префикс “e”, для Rust — “k”.

Исключения

global e0_zero_divide
global e1_debug
......
global eE_page_fault
......
global e14_virtualization
global e1E_security extern k0_zero_divide
extern k1_debug
......
extern kE_page_fault
......
extern k14_virtualization
extern k1E_security section .text e0_zero_divide: pushad call k0_zero_divide popad iret e1_debug: pushad call k1_debug popad iret ...... eE_page_fault: pushad mov eax, [esp + 32] push eax mov eax, cr2 push eax call kE_page_fault pop eax pop eax popad add esp, 4 iret ...... e14_virtualization: pushad call k14_virtualization popad iret e1E_security: pushad call k1E_security popad iret

Объявляем ассемблерные обработчики:

extern { fn load_idt(base: *const IdtEntry, limit: u16); fn e0_zero_divide(); fn e1_debug();
...... fn e14_virtualization(); fn e1E_security(); fn irq0(); fn irq1();
...... fn irq14(); fn irq15();
}

Обратите внимание, что для прерывания клавиатуры я просто вывожу полученный код, который получаю с порта 0x60 — так работает клавиатура в простейшем режиме. Определяем Rust обработчики, которые вызываем выше. После каждого прерывания нужно вывести в контроллер сигнал конца обработки 0x20, это важно! В дальнейшем это трансформируется в полноценный драйвер, надеюсь. Иначе больше прерываний вы не получите.

#[no_mangle] pub unsafe extern fn kirq0() { // println!("IRQ 0"); outb(0x20, 0x20);
} #[no_mangle] pub unsafe extern fn kirq1() { let ch: char = inb(0x60) as char; crate::arch::vga::VGA_WRITER.force_unlock(); println!("IRQ 1 {}", ch); outb(0x20, 0x20);
} #[no_mangle] pub unsafe extern fn kirq2() { println!("IRQ 2"); outb(0x20, 0x20);
} ...

Про PIC и его ремаппинг я нашел большое количество туториалов разной степени подробности, начиная с OSDev и заканчивая любительскими сайтами. Инициализация IDT и PIC. Обратите внимание только на то, что обработчики аппаратных прерываний занимает диапазон индексов 0x20-0x2F в таблице, и в функцию настройки передаются аргументы 0x20 и 0x28, которые как раз покрывают 16 прерываний в диапазоне IDT. Так как процедура программирования оперирует константной последовательностью операций и константными командами, приведу этот код без дальнейших пояснений.

unsafe fn setup_pic(pic1: u8, pic2: u8) { // Start initialization outb(PIC1, 0x11); outb(PIC2, 0x11); // Set offsets outb(PIC1 + 1, pic1); /* remap */ outb(PIC2 + 1, pic2); /* pics */ // Set up cascade outb(PIC1 + 1, 4); /* IRQ2 -> connection to slave */ outb(PIC2 + 1, 2); // Set up interrupt mode (1 is 8086/88 mode, 2 is auto EOI) outb(PIC1 + 1, 1); outb(PIC2 + 1, 1); // Unmask interrupts outb(PIC1 + 1, 0); outb(PIC2 + 1, 0); // Ack waiting outb(PIC1, 0x20); outb(PIC2, 0x20);
} pub unsafe fn init_idt() { IDT[0x0].set_func(e0_zero_divide); IDT[0x1].set_func(e1_debug);
...... IDT[0x14].set_func(e14_virtualization); IDT[0x1E].set_func(e1E_security); IDT[0x20].set_func(irq0); IDT[0x21].set_func(irq1);
...... IDT[0x2E].set_func(irq14); IDT[0x2F].set_func(irq15); setup_pic(0x20, 0x28); let idt_ptr: *const IdtEntry = IDT.as_ptr(); let limit = (IDT.len() * core::mem::size_of::<IdtEntry>() - 1) as u16; load_idt(idt_ptr, limit);
}

Инструкцией STI разрешаем прерывания и можем пробовать нажимать клавиатуру — на экран будут выводиться кракозябры в позиции курсора — это сканкоды, напрямую преобразованные в символы, без ASCII-перехода и обработки скан-кодов. Таблицу прерываний загружаем в регистр IDTR совершенно аналогично GDTR — через дополнительную структуру с адресом и размером.

global load_idt
section .text idtr dw 0 ; For limit storage dd 0 ; For base storage load_idt: mov eax, [esp + 4] mov [idtr + 2], eax mov ax, [esp + 8] mov [idtr], ax lidt [idtr] sti ret

Я краем кода зацепил функцию setup_pd, но рассказ про ее назначение и устройство оставлю на следующий заход. Что ж, эта статья получилась весьма объемной, поэтому про инициализацию памяти и управление ей я расскажу в следующий раз. Пожалуйста, не стесняйтесь писать, что можно улучшить в содержании, в коде.

Исходный код по-прежнему доступен на GitLab.

Спасибо за внимание!


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Слушаем SID-музыку через OPL3 на современных ПК

Кто-то может подумает, что это будет что-то ужасное, а оказывается если сделать простой маппер, то можно получить весьма хорошее звучание, как это сделали несколько разработчиков в программе LLSID ещё в далеком 2007 году. Наверное не все любители чиптюн музыки знают, ...

Пользователь в Docker

В новой статье он рассказывает, как создать пользователей в Docker. Андрей Копылов, наш технический директор, любит, активно использует и пропагандирует Docker. Правильная работа с ними, почему пользователей нельзя оставлять с root правами и, как решить задачу несовпадения идентификаторов в Dockerfile. Это кажется очень удобно, ведь ...