В предыдущей статье мы с вами реализовали менеджер динамической памяти.
Сегодня мы рассмотрим основы работы в защищенном режиме процессора Intel i386.
А именно: глобальную таблицу дескрипторов и таблицу векторов прерываний.
Система сборки (make, gcc, gas). Первоначальная загрузка (multiboot). Запуск (qemu). Библиотека C (strcpy, memcpy, strext).
Библиотека C (sprintf, strcpy, strcmp, strtok, va_list ...). Сборка библиотеки в режиме ядра и в режиме пользовательского приложения.
Системный журнал ядра. Видеопамять. Вывод на терминал (kprintf, kpanic, kassert).
Динамическая память, куча (kmalloc, kfree).
Организация памяти и обработка прерываний (GDT, IDT, PIC, syscall). Исключения.
Виртуальная память (каталог страниц и таблица страниц).
Процесс. Планировщик. Многозадачность. Системные вызовы (kill, exit, ps).
Файловая система ядра (initrd), elf и его внутренности. Системные вызовы (exec).
Драйверы символьных устройств. Системные вызовы (ioctl, fopen, fread, fwrite). Библиотека C (fopen, fclose, fprintf, fscanf).
Оболочка как полноценная программа для ядра.
Пользовательский режим защиты (ring3). Сегмент состояния задачи (tss).
В процессорах Intel есть 2 основных режима работы: Protected Mode x32 и IA-32e x64.
Вообще Зубков об этом очень хорошо и понятно пишет, рекомендую почитать, хотя в принципе и Intel Manual можно, он не сложный, но избыточный и большой.
У них есть отдельный том для системного программирования, его рекомендую и читать.
Русскоязычной информации по первому гораздо больше, поэтому рассмотрим в кратце основные моменты.
Есть два типа адресации линейная и страничная. Линейная означает что все физическое пространство описывается непрерывно и совпадает с физческим, поскольку как правило базы сегметных дескрипторов равны нулю, ибо так проще.
При этом для режима ядра нужно создать три дескриптора описывающих память: для кода, стека и данных. Их различают некоторые аппаратные средства защиты.
Каждый такой сегмент имеет базу нуль и лимит адресуемый максимальным размером машинного слова. Стек растет в обратном направлении, и для этого тоже есть флаг в дескрипторе.
Так, тремя записями такого формата мы адресуем все что нам нужно:
Каждому сегментному регистру (cs, ds, ss) соответствует свой дескриптор в GDT, поэтому когда мы что-то пишем в секцию кода, мы получим ошибку, ибо там в дескрипторе прописана защита от записи.
Чтобы это все начало работать нам нужно загрузить в регистр GDTR структуру такого формата:
Лимит — конец таблицы GDT минус 1, база — ее начало в памяти.
GDT загружается в регистр так:
И сразу после этого загружаем во все сегментные регистры селекторы данных ядра казывающе на дескриптор данных (нулевое кольцо защиты).
После этого все готово для включения страничной адресации, но о ней позже.
Кстати загрузчики по спецификации multiboot рекомендуют сразу настраивать свою GDT, хотя и делают это сами, мол так надежнее.
Как все это сделать технически правильно смотри в видеоуроке.
По аналогии с GDT для таблицы прерываний есть свой регистр IDTR, в который тоже нужно загрузить похожий указатель но уже на IDT.
Сама же таблица прерываний описывается такими записями:
В качестве типа обычно выступает шлюз прерывания, так как мы хотим обработать именно прерывания. Ловушки и шлюза вызова мы пока не рассматриваем, так как это ближе к TSS и кольцам защиты.
Давай с тобой создадим интерфейс для работы с этими таблицами. Их нужно просто один раз настроить и забыть.
А вот теперь обьявим сами обработчики прерываний, занесенные в записи IDT.
Сначала напишем обработчики аппаратных ошибок:
Затем абработчик прерывания клавиатуры:
Пришло время инициализировать таблицу IDT.
Выглядит это примерно так:
Тут мы зарегистрировали три обработчика аппаратных ошибок и один прерывания.
Чтобы это все начало работать нам нужно загрузить в регистр IDTR специальный указатель с базой и лимитом:
Лимиты нужны чтобы понять сколько записей в таблице.
Пора написать обработчик прерывания клавиатуры:
Замечание: далее и везде по коду «нижние половины» эквивалентны «верхним» в Linux. А «верхнии» соответственно наоборот. Приношу извинения, в голове отложилось наоборот :D
Собственно он будет порсто передавать код высокоуровневому обработчику.
Тот в свою очередь будет вызывать обработчик нижних половин у соответствующего драйвера, зарегистрировавшего запрос на обработку этого прерывания.
В нашем случае это будет драйвер символьного устройства.
Нижние половины нужны чтобы быстро обработать прерывания, не затормаживая другие, а потом, когда будет время обработчик верхних половин постепенно выполнит дополнительную работу, ведь такой обработчик уже может быть вытесняем (прерываться).
Теперь при нажатии на клавишу клавиатуры мы каждый раз в системном журнале ядра будем видеть соответствующую запись.
А теперь, открывай видеоурок к этой статье
И смотри параллельно git репозиторий (тебе нужна ветка lesson5)
1. James Molloy. Roll your own toy UNIX-clone OS.
2. Зубков. Ассемблер для DOS, Windows, Unix
3. Калашников. Ассемблер — это просто!
4. Таненбаум. Операционные системы. Реализация и разработка.
5. Роберт Лав. Ядро Linux. Описание процесса разработки.
Сегодня мы рассмотрим основы работы в защищенном режиме процессора Intel i386.
А именно: глобальную таблицу дескрипторов и таблицу векторов прерываний.
Оглавление
Система сборки (make, gcc, gas). Первоначальная загрузка (multiboot). Запуск (qemu). Библиотека C (strcpy, memcpy, strext).
Библиотека C (sprintf, strcpy, strcmp, strtok, va_list ...). Сборка библиотеки в режиме ядра и в режиме пользовательского приложения.
Системный журнал ядра. Видеопамять. Вывод на терминал (kprintf, kpanic, kassert).
Динамическая память, куча (kmalloc, kfree).
Организация памяти и обработка прерываний (GDT, IDT, PIC, syscall). Исключения.
Виртуальная память (каталог страниц и таблица страниц).
Процесс. Планировщик. Многозадачность. Системные вызовы (kill, exit, ps).
Файловая система ядра (initrd), elf и его внутренности. Системные вызовы (exec).
Драйверы символьных устройств. Системные вызовы (ioctl, fopen, fread, fwrite). Библиотека C (fopen, fclose, fprintf, fscanf).
Оболочка как полноценная программа для ядра.
Пользовательский режим защиты (ring3). Сегмент состояния задачи (tss).
Линейная адресация
В процессорах Intel есть 2 основных режима работы: Protected Mode x32 и IA-32e x64.
Вообще Зубков об этом очень хорошо и понятно пишет, рекомендую почитать, хотя в принципе и Intel Manual можно, он не сложный, но избыточный и большой.
У них есть отдельный том для системного программирования, его рекомендую и читать.
Русскоязычной информации по первому гораздо больше, поэтому рассмотрим в кратце основные моменты.
Есть два типа адресации линейная и страничная. Линейная означает что все физическое пространство описывается непрерывно и совпадает с физческим, поскольку как правило базы сегметных дескрипторов равны нулю, ибо так проще.
При этом для режима ядра нужно создать три дескриптора описывающих память: для кода, стека и данных. Их различают некоторые аппаратные средства защиты.
Каждый такой сегмент имеет базу нуль и лимит адресуемый максимальным размером машинного слова. Стек растет в обратном направлении, и для этого тоже есть флаг в дескрипторе.
Так, тремя записями такого формата мы адресуем все что нам нужно:
/*
* Global descriptor table entry
*/
struct GDT_entry_t {
u16 limit_low: 16;
u16 base_low: 16;
u8 base_middle: 8;
u8 type: 4; /* whether code (0b1010), data (0b0010), stack (0b0110) or tss (0b1001) */
u8 s: 1; /* whether system descriptor */
u8 dpl: 2; /* privilege level */
u8 p: 1; /* whether segment prensent */
u8 limit_high: 4;
u8 a: 1; /* reserved for operation system */
u8 zero: 1; /* zero */
u8 db: 1; /* whether 16 or 32 segment */
u8 g: 1; /* granularity */
u8 base_high: 8;
} attribute(packed);
Каждому сегментному регистру (cs, ds, ss) соответствует свой дескриптор в GDT, поэтому когда мы что-то пишем в секцию кода, мы получим ошибку, ибо там в дескрипторе прописана защита от записи.
Чтобы это все начало работать нам нужно загрузить в регистр GDTR структуру такого формата:
/*
* Global descriptor table pointer
*/
struct GDT_pointer_t {
u16 limit;
u32 base;
} attribute(packed);
Лимит — конец таблицы GDT минус 1, база — ее начало в памяти.
GDT загружается в регистр так:
/*
* Load global descriptor table
* void asm_gdt_load(void *gdt_ptr)
*/
asm_gdt_load:
mov 4(%esp),%eax # eax = gdt_ptr
lgdt (%eax)
mov $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
mov %ax,%ss
jmp $0x08,$asm_gdt_load_exit
asm_gdt_load_exit:
ret
И сразу после этого загружаем во все сегментные регистры селекторы данных ядра казывающе на дескриптор данных (нулевое кольцо защиты).
После этого все готово для включения страничной адресации, но о ней позже.
Кстати загрузчики по спецификации multiboot рекомендуют сразу настраивать свою GDT, хотя и делают это сами, мол так надежнее.
Как все это сделать технически правильно смотри в видеоуроке.
Обработка прерываний
По аналогии с GDT для таблицы прерываний есть свой регистр IDTR, в который тоже нужно загрузить похожий указатель но уже на IDT.
Сама же таблица прерываний описывается такими записями:
/*
* Interrupt table entry
*/
struct IDT_entry_t {
u16 offset_lowerbits;
u16 selector;
u8 zero;
u8 type_attr;
u16 offset_higherbits;
};
В качестве типа обычно выступает шлюз прерывания, так как мы хотим обработать именно прерывания. Ловушки и шлюза вызова мы пока не рассматриваем, так как это ближе к TSS и кольцам защиты.
Давай с тобой создадим интерфейс для работы с этими таблицами. Их нужно просто один раз настроить и забыть.
/*
* Api
*/
extern void gdt_init();
extern void idt_init();
А вот теперь обьявим сами обработчики прерываний, занесенные в записи IDT.
Сначала напишем обработчики аппаратных ошибок:
/*
* Api - IDT
*/
extern void ih_double_fault();
extern void ih_general_protect();
extern void ih_page_fault();
extern void ih_alignment_check();
extern void asm_ih_double_fault();
extern void asm_ih_general_protect();
extern void asm_ih_page_fault();
extern void asm_ih_alignment_check();
Затем абработчик прерывания клавиатуры:
/*
* Api - IRQ
*/
extern void ih_keyboard();
extern void asm_ih_keyboard();
Пришло время инициализировать таблицу IDT.
Выглядит это примерно так:
extern void idt_init()
{
size_t idt_address;
size_t idt_ptr[2];
pic_init();
/* fill idt */
idt_fill_entry(INT_DOUBLE_FAULT, (size_t)asm_ih_double_fault);
idt_fill_entry(INT_GENERAL_PROTECT, (size_t)asm_ih_general_protect);
idt_fill_entry(INT_ALIGNMENT_CHECK, (size_t)asm_ih_alignment_check);
idt_fill_entry(INT_KEYBOARD, (size_t)asm_ih_keyboard);
/* load idt */
idt_address = (size_t)IDT;
idt_ptr[0] = (LOW_WORD(idt_address) << 16) + (sizeof(struct IDT_entry_t) * IDT_SIZE);
idt_ptr[1] = idt_address >> 16;
asm_idt_load(idt_ptr);
}
Тут мы зарегистрировали три обработчика аппаратных ошибок и один прерывания.
Чтобы это все начало работать нам нужно загрузить в регистр IDTR специальный указатель с базой и лимитом:
/*
* Load interrupt table
* void asm_idt_load(unsigned long *addr)
*/
asm_idt_load:
push %edx
mov 8(%esp), %edx
lidt (%edx)
pop %edx
ret
Лимиты нужны чтобы понять сколько записей в таблице.
Пора написать обработчик прерывания клавиатуры:
/*
* Handle IRQ1
* void asm_ih_keyboard(unsigned int)
*/
asm_ih_keyboard:
pushal
call ih_keyboard
popal
iretl
Замечание: далее и везде по коду «нижние половины» эквивалентны «верхним» в Linux. А «верхнии» соответственно наоборот. Приношу извинения, в голове отложилось наоборот :D
Собственно он будет порсто передавать код высокоуровневому обработчику.
Тот в свою очередь будет вызывать обработчик нижних половин у соответствующего драйвера, зарегистрировавшего запрос на обработку этого прерывания.
В нашем случае это будет драйвер символьного устройства.
Нижние половины нужны чтобы быстро обработать прерывания, не затормаживая другие, а потом, когда будет время обработчик верхних половин постепенно выполнит дополнительную работу, ведь такой обработчик уже может быть вытесняем (прерываться).
/*
* Api - Keyboard interrupt handler
*/
extern void ih_keyboard()
{
printf("[IH]: irq %u\n", 1);
u_char status = asm_read_port(KEYBOARD_STATUS_PORT);
if (status & 0x01) {
char keycode = asm_read_port(KEYBOARD_DATA_PORT);
if (keycode < 1) {
goto end;
}
/* call low half (bottom) interrupt handler */
}
end:
asm_write_port(PIC1_CMD_PORT, 0x20); /* end of interrupt */
}
Теперь при нажатии на клавишу клавиатуры мы каждый раз в системном журнале ядра будем видеть соответствующую запись.
Ссылки
А теперь, открывай видеоурок к этой статье
И смотри параллельно git репозиторий (тебе нужна ветка lesson5)
Список литературы
1. James Molloy. Roll your own toy UNIX-clone OS.
2. Зубков. Ассемблер для DOS, Windows, Unix
3. Калашников. Ассемблер — это просто!
4. Таненбаум. Операционные системы. Реализация и разработка.
5. Роберт Лав. Ядро Linux. Описание процесса разработки.
MooNDeaR
Ваши статьи незаслуженно мало комментируют, но пожалуйста, продолжайте их писать :)