Первая часть Первая статья еще не успела остыть, но я решил не держать вас в напряжении и написать продолжение.
Итак, в предыдущей статье мы говорили о компоновке, загрузке файла ядра и первичной инициализации.
Я дал несколько полезных ссылок, рассказал, как загружаемое ядро размещается в памяти, как соотносятся виртуальные и физические адреса при загрузке, а также как включить поддержку страничного механизма.
Наконец, управление перешло к функции kmain моего ядра, написанной на Rust. Пришло время двигаться дальше и узнать, насколько глубока кроличья нора! В этой части заметок я Кратко опишу свою конфигурацию Rust, в общих чертах расскажу о выводе информации в VGA, а подробно о настройке сегментов и прерываний.
.
Приглашаю всех желающих следить за разрезом, и мы начнем.
Настройка Руста В целом, ничего особо сложного в этой процедуре нет; для подробностей вы можете связаться Блог Филиппа .
Однако на некоторых моментах я все же остановлюсь.
Некоторые функции, необходимые для низкоуровневой разработки, до сих пор не поддерживаются стабильной версией Rust, поэтому, чтобы отключить стандартную библиотеку и начать сборку на Bare Bones, нам нужен Rust каждую ночь.
Будьте осторожны, однажды после обновления до последней я получил совершенно нерабочий компилятор и пришлось откатиться на ближайший стабильный.
Если вы уверены, что вчера ваш компилятор работал, но обновился и не работает - запустите команду, подставив нужную вам дату
За подробностями механизма вы можете обратиться здесь .rustup override add nightly-YYYY-MM-DD
Далее мы настроим целевую платформу, для которой будем собирать.
Я взял за основу блог Филипа Оппермана, поэтому многие вещи в этом разделе взяты у него, разобраны и адаптированы под мои нужды.
Филипп в своем блоге разрабатывает для x64, но я изначально выбрал x32, поэтому мой target.json будет немного другой.
цитирую полностью {
"llvm-target": "i686-unknown-none",
"data-layout": "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128",
"arch": "x86",
"target-endian": "little",
"target-pointer-width": "32",
"target-c-int-width": "32",
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}
Самое сложное здесь — это расположение данных В документации LLVM нам сказано, что это параметры размещения данных, разделенные знаком «-».
Самый первый символ «e» отвечает за индийскость — в нашем случае это прямой порядок байтов, как того требует платформа.
Второй символ — m , «искажение».
Отвечает за имена символов при верстке.
Поскольку наш выходной формат будет ELF (см.
скрипт верстки), мы выбираем значение «m:e».
Третий символ — это размер указателя в битах и ABI (.
Бинарный интерфейс приложения).
Здесь все просто, у нас 32 бита, поэтому смело ставьте «p:32:32».
Далее идут числа с плавающей запятой.
Сообщаем, что мы поддерживаем 64-битные числа ABI 32 с выравниванием 64. «f64:32:64», а также 80-битные числа с выравниванием по умолчанию «f80:32».
Следующий элемент — целые числа.
Начинаем с 8 бит и переходим к платформе максимум 32 бита — «n8:16:32».
Последний — выравнивание стека.
Мне нужны даже 128-битные целые числа, поэтому пусть это будет S128. В любом случае LLVM может спокойно игнорировать этот параметр, это наше предпочтение.
По остальным параметрам можно уточнить у Филиппа, он все хорошо объясняет. Нам также нужен Cargo-xbuild, инструмент, который позволяет выполнять кросс-компиляцию Rust-ядра при сборке для незнакомой целевой платформы.
Устанавливаем.
cargo install cargo-xbuild
Соберем его так.
cargo xbuild -Z unstable-options --manifest-path=kernel/Cargo.toml --target kernel/targets/$(ARCH).
json --out-dir=build/lib
Мне нужно было указать манифест, чтобы Make работал корректно, так как он запускается из корневого каталога, а ядро находится в каталоге ядра.
Из особенностей манифеста могу лишь выделить тип ящика = ["staticlib"] , который создает на выходе связанный файл.
В будущем мы скормим его LLD. kmain и первоначальная настройка Согласно соглашениям Rust, если мы создаем статическую библиотеку (или «плоский» двоичный файл), в корне крейта должен находиться файл lib.rs, который является точкой входа.
В нем с помощью атрибутов настраиваются возможности языка, а также находится заветный kmain. Итак, на первом этапе нам нужно будет отключить библиотеку 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 {}
}
Что тут происходит? Как я уже сказал, мы отключаем стандартную библиотеку.
Также мы анонсируем два очень важных модуля — debug (в котором мы будем писать на экран) и Arch (в котором будет жить вся платформо-зависимая магия).
Я использую функцию конфигурации Rust, чтобы объявлять одни и те же интерфейсы в разных архитектурных реализациях и использовать их в полной мере.
Здесь я акцентирую внимание только на x86 и дальше мы будем говорить только о нем.
Я объявил совершенно примитивный обработчик паники, который требуется Rust. Потом можно будет его доработать.
kmain принимает три аргумента и также экспортируется в нотацию C без изменения имени, чтобы компоновщик мог правильно связать функцию с вызовом из _loader, который я описал в предыдущей статье.
Первый аргумент — адрес таблицы страниц PD, второй — физический адрес структуры GRUB, откуда мы возьмем карту памяти, третий — магическое число.
В будущем я хотел бы реализовать как поддержку Multiboot 2, так и собственный загрузчик, поэтому я использую магическое число для определения метода загрузки.
Самый первый вызов kmain — это инициализация, зависящая от платформы.
Давай зайдем внутрь.
Функция Arch_init находится в файле Arch/i686/mod.rs, является общедоступной, специфичной для 32-битной платформы x86 и выглядит следующим образом: pub fn arch_init(pd: usize) {
unsafe {
vga::VGA_WRITER.lock().
init();
gdt::setup_gdt();
idt::init_idt();
paging::setup_pd(pd);
}
}
Как видите, для x86 вывод, сегментация, прерывания и подкачка памяти инициализируются по порядку.
Начнем с VGA. Инициализация VGA Каждый туториал считает своим долгом распечатать Hello World, поэтому вы везде найдете способы работы с VGA. По этой причине я пройдусь по нему максимально кратко, остановившись только на фишках, которые сделал сам.
Я отсылаю вас к блогу Филиппа об использовании lazy_static и не буду объяснять это подробно.
const fn еще не выпущен, поэтому статическую инициализацию пока невозможно выполнить красиво.
Еще добавим спин-лок, чтобы не получился полный бардак.
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) }
});
}
Как известно, экранный буфер расположен по физическому адресу 0xB8000 и имеет размер 80x25x2 байта (ширина и высота экрана, по одному байту на символ и атрибуты: цвета, мерцание).
Поскольку мы уже включили виртуальную память, доступ к этому адресу приведет к сбою, поэтому добавляем 3 ГБ.
Мы также разыменовываем необработанный указатель, что небезопасно, но мы знаем, что делаем.
Пожалуй, единственное интересное в этом файле — реализация структуры Writer, которая позволяет не только отображать символы подряд, но и прокручивать, переходить в любое место экрана и другие приятные мелочи.
VGA-писатель 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
Поскольку стек начинается в конце пространства и уменьшает указатель стека при вызове функции для получения параметров, точки возврата и т. д., размер аргумента, выровненный по выравниванию стека, должен быть добавлен в регистр 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 и схватился за голову.
Мне потребовалось несколько месяцев, чтобы прочитать достаточно материала, переварить его и осмыслить.
Возможно, я стал жертвой учебника Питера Абеля «Ассемблер.
Язык и программирование для IBM PC» (отличная книга!), где описывается сегментация для Intel 8086. В те хорошие времена мы загружали в сегментный регистр старшие 16 бит двадцатибитного адреса, и это был именно адрес в памяти.
Сильным разочарованием оказалось то, что начиная с i286 в защищенном режиме все совсем по-другому.
Итак, голая теория гласит, что x86 поддерживает сегментную модель памяти, поскольку только так старые программы могли вырваться за пределы 640 КБ, а затем и 1 МБ памяти.
Программистам приходилось думать о том, как разместить исполняемый код, как разместить данные и как обеспечить их безопасность.
С приходом страничной организации организация сегментов стала ненужной, но она осталась в целях совместимости и безопасности (разделение привилегий на ядро-пространство и пользовательское пространство), так что без этого просто никуда.
Некоторые инструкции процессора запрещены при уровнях привилегий ниже 0, а доступ между сегментами программы и ядра приведет к ошибке сегментации.
Поговорим еще раз о трансляции адресов (надеюсь, в последний раз) Линейный адрес [0x08:0xFFFFFFFF] -> Проверка прав сегмента 0x08 -> Виртуальный адрес [0xFFFFFFFF] -> Таблица страниц + TLB -> Физический адрес [0xAAAAFFFF] Сегмент используется только внутри процессора, хранится в специальном сегментном регистре (CS, SS, DS, ES, FS, GS) и используется исключительно для проверки прав исполнения кода и передачи управления.
Вот почему вы не можете просто вызвать функцию ядра из пользовательского пространства.
Сегмент с дескриптором 0x18 (у меня один, у вас другой) имеет права 3 уровня, а сегмент с дескриптором 0x08 — права 0 уровня.
Согласно соглашению x86, для защиты от несанкционированного доступа сегмент с более низкими правами доступа не может напрямую обращаться к сегменту с более высокими.
права через jmp 0x08:[EAX], но должны использовать другие механизмы, такие как ловушки, гейты, прерывания.
Сегменты и их типы (код, данные, ловушки, гейты) должны быть описаны в глобальной таблице дескрипторов GDT, виртуальный адрес и размер которого загружаются в регистр GDTR. При переходе между сегментами (для простоты буду считать, что возможен прямой переход) нужно вызвать инструкцию jmp 0x08:[EAX], где 0x08 — смещение первого допустимого дескриптора в байтах от начала таблицы , а EAX — это регистр, содержащий адрес перехода.
Смещение (селектор) будет загружено в регистр CS, а соответствующий дескриптор — в теневой регистр процессора.
Каждый дескриптор представляет собой 8-байтовую структуру.
Он хорошо документирован и его описание можно найти как на OSDev, так и в документации Intel (см.
первую статью).
Позвольте мне подвести итог.
Когда мы инициализируем GDT и выполняем переход jmp 0x08:[EAX], состояние процессора будет следующим:
- GDPR содержит виртуальный адрес GDT
- CS содержит значение 0x08
- Дескриптор по адресу [GDTR+0x08] был скопирован из памяти в теневой регистр CS.
- Регистр 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 осуществляется через отдельную структуру, содержащую всего два поля.
Адрес этой структуры передается команде lgdt. Мы загружаем следующий дескриптор в регистры сегмента данных по смещению 0x10. 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 было добавлено соглашение о вызове прерываний x86, которое позволяет писать обработчики прерываний, выполняющие iret. Однако я сознательно решил не идти по этому пути, так как решил разделить ассемблер и Rust на разные файлы, а значит и функции.
Да, я неразумно использую стековую память, я это понимаю, но это всё равно дело вкуса.
Мои обработчики прерываний написаны на ассемблере и делают ровно одно: вызывают почти одноименные обработчики прерываний, написанные на Rust. Пожалуйста, примите этот факт и будьте снисходительны.
В целом процесс инициализации прерываний аналогичен инициализации GDT, но его легче понять.
С другой стороны, вам нужно много монотонного кода.
Разработчики Redox OS сделали красивое решение, используя все прелести языка, но я пошел напролом и решил разрешить дублирование кода.
Согласно соглашению x86, у нас есть прерывания и есть исключения.
В этом плане настройки для нас практически ничем не отличаются.
Единственное отличие состоит в том, что при возникновении исключения стек может содержать дополнительную информацию.
Например, я использую его для обработки пропущенных страниц при работе с кучей (но всему свое время).
И прерывания, и исключения обрабатываются из одной таблицы, которую нам с вами необходимо заполнить.
Также необходимо запрограммировать PIC (программируемый контроллер прерываний).
Ещё есть APIC, но я пока не разобрался.
Много комментариев по работе с PIC давать не буду, так как в интернете много примеров по работе с ним.
Начну с обработчиков в ассемблере.
Они все совершенно однотипные, поэтому код помещу под спойлер.
прерывание 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» — для различия и удобства.
Обработка исключений абсолютно аналогична.
Префикс «e» выбран для ассемблерных функций, а «k» — для Rust. Обработчик Page Fault отличается, но подробнее о нем см.
в примечаниях по управлению памятью.
Исключения 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();
}
Мы определяем обработчики Rust, которые вызываем выше.
Обратите внимание, что для прерывания работы клавиатуры я просто вывожу результирующий код, который получаю с порта 0x60 — так работает клавиатура в самом простом режиме.
В будущем это трансформируется в полноценный драйвер, я надеюсь.
После каждого прерывания нужно выводить на контроллер сигнал окончания обработки 0х20, это важно! В противном случае вы больше не получите прерываний.
#[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);
}
.
Инициализация IDT и PIC. Я нашел большое количество туториалов по PIC и его перепрошивке разной степени детализации, от OSDev до любительских сайтов.
Поскольку процедура программирования оперирует постоянной последовательностью операций и постоянными командами, я приведу этот код без дополнительных пояснений.
Просто обратите внимание, что обработчики аппаратных прерываний занимают в таблице диапазон индексов 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);
}
Таблицу прерываний мы загружаем в регистр IDTR точно так же, как и GDTR — через дополнительную структуру с адресом и размером.
С помощью инструкции STI разрешаем прерывания и можем попробовать нажать на клавиатуру — в месте положения курсора на экране будут отображаться сухари — это сканкоды, напрямую преобразованные в символы, без ASCII-перехода и обработки скан-кодов.
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 .
Спасибо за внимание! УПД: Часть 3 Теги: #Процессоры #операционные системы #Rust #Системное программирование #x86 #Ассемблер
-
Язык – Кто Может Читать Ваш Веб-Сайт?
19 Oct, 24 -
Русские Программисты В Канаде
19 Oct, 24 -
Организация Работы С Ит-Подрядчиками
19 Oct, 24 -
Сервис Rambler-Ads Официально Запустился
19 Oct, 24