В предыдущей статье мы с вами реализовали менеджер динамической памяти.
Сегодня мы рассмотрим основы работы в защищенном режиме процессора 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. Описание процесса разработки.

Комментарии (3)


  1. MooNDeaR
    13.09.2019 12:33

    Ваши статьи незаслуженно мало комментируют, но пожалуйста, продолжайте их писать :)


  1. MaxVetrov
    13.09.2019 23:53

    Можно добавить еще один ответ в опрос — любая.


  1. Foror
    15.09.2019 18:26

    RISC-V