Давай напишем ядро! Создаем простейшее рабочее ядро операционной системы — «Хакер»

Найти:

- Взлом
- Приватность
- Трюки
- Кодинг
- Админ
- Geek
- Обучение
- Магазин

- Windows
- Linux
- Android
- Железо
- Мессенджеры
- Утечки
- Коронавирус
- Все выпуски «Хакера»

### Содержание статьи

- Как загружаются x86-машины
- Что нам понадобится
- Входная точка на ассемблере
- Ядро на C
- Компоновка
- GRUB и мультизагрузка
- Собираем ядро
- Настраиваем GRUB и запускаем ядро
- GRUB 2
- Пишем ядро с поддержкой клавиатуры и экрана
- Работа с портами: чтение и вывод
- Прерывания
- Задаем IDT
- Функция — обработчик прерывания клавиатуры

Разработка ядра по праву считается задачей не из легких, но написать простейшее ядро может каждый. Чтобы прикоснуться к магии кернел-хакинга, нужно лишь соблюсти некоторые условности и совладать с ассемблером. В этой статье мы на пальцах разберем, как это сделать.
Другие статьи в выпуске:

### Xakep #231. Мессенджеры

- Содержание выпуска
- Подписка на «Хакер»

## INFO

Это перевод двух статей Арджуна Сридхарана:

- Kernel 101 — Let’s write a Kernel и
- Kernel 201 — Let’s write a Kernel with keyboard and screen support.

Публикуется с разрешения автора. Перевел Андрей Письменный.

Привет, мир!

Давай напишем ядро, которое будет загружаться через GRUB на системах, совместимых с x86. Наше первое ядро будет показывать сообщение на экране и на этом останавливаться.

## Как загружаются x86-машины

Прежде чем думать о том, как писать ядро, давай посмотрим, как компьютер загружается и передает управление ядру. Большинство регистров процессора x86 имеют определенные значения после загрузки. Регистр — указатель на инструкцию (EIP) содержит адрес инструкции, которая будет исполнена процессором. Его захардкоженное значение — это 0xFFFFFFF0. То есть x86-й процессор всегда будет начинать исполнение с физического адреса 0xFFFFFFF0. Это последние 16 байт 32-разрядного адресного пространства. Этот адрес называется «вектор сброса» (reset vector).

В карте памяти, которая содержится в чипсете, прописано, что адрес 0xFFFFFFF0 ссылается на определенную часть BIOS, а не на оперативную память. Однако BIOS копирует себя в оперативку для более быстрого доступа — этот процесс называется «шедоуинг» (shadowing), создание теневой копии. Так что адрес 0xFFFFFFF0 будет содержать только инструкцию перехода к тому месту в памяти, куда BIOS скопировала себя.

Итак, BIOS начинает исполняться. Сначала она ищет устройства, с которых можно загружаться в том порядке, который задан в настройках. Она проверяет носители на наличие «волшебного числа», которое отличает загрузочные диски от обычных: если байты 511 и 512 в первом секторе равны 0xAA55, значит, диск загрузочный.

Как только BIOS найдет загрузочное устройство, она скопирует содержимое первого сектора в оперативную память, начиная с адреса 0x7C00, а затем переведет исполнение на этот адрес и начнет исполнение того кода, который только что загрузила. Вот этот код и называется загрузчиком (bootloader).

Загрузчик загружает ядро по физическому адресу 0x100000. Именно он и используется большинством популярных ядер для x86.

Все процессоры, совместимые с x86, начинают свою работу в примитивном 16-разрядном режиме, которые называют «реальным режимом» (real mode). Загрузчик GRUB переключает процессор в 32-разрядный защищенный режим (protected mode), переводя нижний бит регистра CR0 в единицу. Поэтому ядро начинает загружаться уже в 32-битном защищенном режиме.

Заметь, что GRUB в случае с ядрами Linux выбирает соответствующий протокол загрузки и загружает ядро в реальном режиме. Ядра Linux сами переключаются в защищенный режим.

## Что нам понадобится

- Компьютер, совместимый с x86 (очевидно),
- Linux,
- ассемблер NASM,
- GCC,
- ld (GNU Linker),
- GRUB.

## WWW

Ты можешь найти исходный код того, что у нас должно получиться, в репозитории автора на GitHub.

## Входная точка на ассемблере

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

Как сделать так, чтобы ассемблерный код стал стартовой точкой для нашего ядра? Мы используем скрипт для компоновщика (linker), который линкует объектные файлы и создает финальный исполняемый файл ядра (подробнее объясню чуть ниже). В этом скрипте мы напрямую укажем, что хотим, чтобы наш бинарный файл загружался по адресу 0x100000. Это адрес, как я уже писал, по которому загрузчик ожидает увидеть входную точку в ядро.

Вот код на ассемблере.

##### kernel.asm

    bits 32
    section .text

    global start
    extern kmain

    start:
      cli
      mov esp, stack_space
      call kmain
      hlt

    section .bss
    resb 8192
    stack_space:

Первая инструкция `bits 32` — это не ассемблер x86, а директива NASM, сообщающая, что нужно генерировать код для процессора, который будет работать в 32-разрядном режиме. Для нашего примера это не обязательно, но указывать это явно — хорошая практика.

Вторая строка начинает текстовую секцию, также известную как секция кода. Сюда пойдет весь наш код.

`global` — это еще одна директива NASM, она объявляет символы из нашего кода глобальными. Это позволит компоновщику найти символ `start`, который и служит нашей точкой входа.

`kmain` — это функция, которая будет определена в нашем файле `kernel.c`. `extern` объявляет, что функция декларирована где-то еще.

Далее идет функция `start`, которая вызывает `kmain` и останавливает процессор инструкцией `hlt`. Прерывания могут будить процессор после `hlt`, так что сначала мы отключаем прерывания инструкцией `cli` (clear interrupts).

В идеале мы должны выделить какое-то количество памяти под стек и направить на нее указатель стека (esp). GRUB, кажется, это и так делает за нас, и на этот момент указатель стека уже задан. Однако на всякий случай выделим немного памяти в секции BSS и направим указатель стека на ее начало. Мы используем инструкцию `resb` — она резервирует память, заданную в байтах. Затем оставляется метка, указывающая на край зарезервированного куска памяти. Прямо перед вызовом `kmain` указатель стека (esp) направляется на эту область инструкцией `mov`.

## Ядро на C

В файле `kernel.asm` мы вызвали функцию `kmain()`. Так что в коде на C исполнение начнется с нее.

##### kernel.c

    void kmain(void)
    {
        const char *str = "my first kernel";
        char *vidptr = (char*)0xb8000;
        unsigned int i = 0;
        unsigned int j = 0;

        while(j < 80 * 25 * 2) {
            vidptr[j] = ' ';
            vidptr[j+1] = 0x07;
            j = j + 2;
        }

        j = 0;

        while(str[j] != '\0') {
            vidptr[i] = str[j];
            vidptr[i+1] = 0x07;
            ++j;
            i = i + 2;
        }
        return;
    }

Все, что будет делать наше ядро, — очищать экран и выводить строку my first kernel.

Первым делом мы создаем указатель vidptr, который указывает на адрес 0xb8000. В защищенном режиме это начало видеопамяти. Текстовая экранная память — это просто часть адресного пространства. Под экранный ввод-вывод выделен участок памяти, который начинается с адреса 0xb8000, — в него помещается 25 строк по 80 символов ASCII.

Каждый символ в текстовой памяти представлен 16 битами (2 байта), а не 8 битами (1 байтом), к которым мы привыкли. Первый байт — это код символа в ASCII, а второй байт — это `attribute-byte`. Это определение формата символа, в том числе — его цвет.

Чтобы вывести символ `s` зеленым по черному, нам нужно поместить `s` в первый байт видеопамяти, а значение 0x02 — во второй байт. 0 здесь означает черный фон, а 2 — зеленый цвет. Мы будем использовать светло-серый цвет, его код — 0x07.

В первом цикле `while` программа заполняет пустыми символами с атрибутом 0x07 все 25 строк по 80 символов. Это очистит экран.

Во втором цикле `while` символы строки my first kernel, оканчивающейся нулевым символом, записываются в видеопамять и каждый символ получает attribute-byte, равный 0x07. Это должно привести к выводу строки.

## Компоновка

Теперь мы должны собрать kernel.asm в объектный файл с помощью NASM, а затем при помощи GCC скомпилировать kernel.c в другой объектный файл. Наша задача — слинковать эти объекты в исполняемое ядро, пригодное к загрузке. Для этого потребуется написать для компоновщика (ld) скрипт, который мы будем передавать в качестве аргумента.

##### link.ld

    OUTPUT_FORMAT(elf32-i386)
    ENTRY(start)
    SECTIONS
     {
       . = 0x100000;
       .text : { *(.text) }
       .data : { *(.data) }
       .bss  : { *(.bss)  }
     }

Здесь мы сначала задаем формат (`OUTPUT_FORMAT`) нашего исполняемого файла как 32-битный ELF (Executable and Linkable Format), стандартный бинарный формат для Unix-образных систем для архитектуры x86.

`ENTRY` принимает один аргумент. Он задает название символа, который будет служить входной точкой исполняемого файла.

`SECTIONS` — это самая важная для нас часть. Здесь мы определяем раскладку нашего исполняемого файла. Мы можем определить, как разные секции будут объединены и куда каждая из них будет помещена.

В фигурных скобках, которые идут за выражением `SECTIONS`, точка означает счетчик позиции (location counter). Он автоматически инициализируется значением 0x0 в начале блока `SECTIONS`, но его можно менять, назначая новое значение.

Ранее я уже писал, что код ядра должен начинаться по адресу 0x100000. Именно поэтому мы и присваиваем счетчику позиции значение 0x100000.

Взгляни на строку `.text : { *(.text) }`. Звездочкой здесь задается маска, под которую подходит любое название файла. Соответственно, выражение `*(.text)` означает все входные секции `.text` во всех входных файлах.

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

После того как компоновщик выдаст текстовую секцию, значение счетчика позиции будет 0x100000 плюс размер текстовой секции. Точно так же секции `data` и `bss` будут слиты и помещены по адресу, который задан счетчиком позиции.

## GRUB и мультизагрузка

Теперь все наши файлы готовы к сборке ядра. Но поскольку мы будем загружать ядро при помощи GRUB, остается еще один шаг.

Существует стандарт для загрузки разных ядер x86 с помощью бутлоадера. Это называется «спецификация мультибута». GRUB будет загружать только те ядра, которые ей соответствуют.

В соответствии с этой спецификацией ядро может содержать заголовок (Multiboot header) в первых 8 килобайтах. В этом заголовке должно быть прописано три поля:

- magic — содержит «волшебное» число 0x1BADB002, по которому идентифицируется заголовок;
- flags — это поле для нас не важно, можно оставить ноль;
- checksum — контрольная сумма, должна дать ноль, если прибавить ее к полям `magic` и `flags`.

Наш файл kernel.asm теперь будет выглядеть следующим образом.

###### kernel.asm

    bits 32
    section .text
            ;multiboot spec
            align 4
            dd 0x1BADB002            ;magic
            dd 0x00                  ;flags
            dd - (0x1BADB002 + 0x00) ;checksum

    global start
    extern kmain

    start:
      cli
      mov esp, stack_space
      call kmain
      hlt

    section .bss
    resb 8192
    stack_space:

Инструкция dd задает двойное слово размером 4 байта.

## Собираем ядро

Итак, все готово для того, чтобы создать объектный файл из kernel.asm и kernel.c и слинковать их с применением нашего скрипта. Пишем в консоли:

    $ nasm -f elf32 kernel.asm -o kasm.o

По этой команде ассемблер создаст файл kasm.o в формате ELF-32 bit. Теперь настал черед GCC:

    $ gcc -m32 -c kernel.c -o kc.o

Параметр `-c` указывает на то, что файл после компиляции не нужно линковать. Мы это сделаем сами:

    $ ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o

Эта команда запустит компоновщик с нашим скриптом и сгенерирует исполняемый файл под названием `kernel`.

## WARNING

Хакингом ядра лучше всего заниматься в виртуалке. Чтобы запустить ядро в QEMU вместо GRUB, используй команду `qemu-system-i386 -kernel kernel`.

## Настраиваем GRUB и запускаем ядро

GRUB требует, чтобы название файла с ядром следовало конвенции `kernel-<версия>`. Так что переименовываем файл — я назову свой `kernel-701`.

Теперь кладем ядро в каталог `/boot`. На это понадобятся привилегии суперпользователя.

В конфигурационный файл GRUB grub.cfg нужно будет добавить что-то в таком роде:

    title myKernel
        root (hd0,0)
        kernel /boot/kernel-701 ro

Не забудь убрать директиву hiddenmenu, если она прописана.

### GRUB 2

Чтобы запустить созданное нами ядро в GRUB 2, который по умолчанию поставляется в новых дистрибутивах, твой конфиг должен выглядеть следующим образом:

    menuentry 'kernel 701' {
        set root='hd0,msdos1'
        multiboot /boot/kernel-701 ro
    }

Благодарю Рубена Лагуану за это дополнение.

Перезагружай компьютер, и ты должен будешь увидеть свое ядро в списке! А выбрав его, ты увидишь ту самую строку.

Это и есть твое ядро!

## WWW

- wiki.osdev.org
- osdever.net
- Multiboot spec

## Пишем ядро с поддержкой клавиатуры и экрана

Мы закончили работу над минимальным ядром, которое загружается через GRUB, работает в защищенном режиме и выводит на экран одну строку. Настала пора расширить его и добавить драйвер клавиатуры, который будет читать символы с клавиатуры и выводить их на экран.

## WWW

Полный исходный код ты можешь найти в репозитории автора на GitHub.

Мы будем общаться с устройствами ввода-вывода через порты ввода-вывода. По сути, они просто адреса на шине ввода-вывода. Для операций чтения и записи в них существуют специальные процессорные инструкции.

## Работа с портами: чтение и вывод

    read_port:
        mov edx, [esp + 4]
        in al, dx
        ret

    write_port:
        mov   edx, [esp + 4]
        mov   al, [esp + 4 + 4]
        out   dx, al
        ret

Доступ к портам ввода-вывода осуществляется при помощи инструкций `in` и `out`, входящих в набор x86.

В `read_port` номер порта передается в качестве аргумента. Когда компилятор вызывает функцию, он кладет все аргументы в стек. Аргумент копируется в регистр `edx` при помощи указателя на стек. Регистр `dx` — это нижние 16 бит регистра `edx`. Инструкция `in` здесь читает порт, номер которого задан в `dx`, и кладет результат в `al`. Регистр `al` — это нижние 8 бит регистра `eax`. Возможно, ты помнишь из институтского курса, что значения, возвращаемые функциями, передаются через регистр `eax`. Таким образом, `read_port` позволяет нам читать из портов ввода-вывода.

Функция `write_port` работает схожим образом. Мы принимаем два аргумента: номер порта и данные, которые будут записаны. Инструкция `out` пишет данные в порт.

## Прерывания

Теперь, прежде чем мы вернемся к написанию драйвера, нам нужно понять, как процессор узнает, что какое-то из устройств выполнило операцию.

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

За прерывания в архитектуре x86 отвечает чип под названием Programmable Interrupt Controller (PIC). Он обрабатывает хардверные прерывания и направляет и превращает их в соответствующие системные прерывания.

Когда пользователь что-то делает с устройством, чипу PIC отправляется импульс, называемый запросом на прерывание (Interrupt Request, IRQ). PIC переводит полученное прерывание в системное прерывание и отправляет процессору сообщение о том, что пора остановить то, что он делает. Дальнейшая обработка прерываний — это задача ядра.

Без PIC нам бы пришлось опрашивать все устройства, присутствующие в системе, чтобы посмотреть, не произошло ли событие с участием какого-то из них.

Давай разберем, как это работает в случае с клавиатурой. Клавиатура висит на портах 0x60 и 0x64. Порт 0x60 отдает данные (когда нажата какая-то кнопка), а порт 0x64 передает статус. Однако нам нужно знать, когда конкретно читать эти порты.

Прерывания здесь приходятся как нельзя более кстати. Когда кнопка нажата, клавиатура отправляет PIC сигнал по линии прерываний IRQ1. PIС хранит значение `offset`, сохраненное во время его инициализации. Он добавляет номер входной линии к этому отступу, чтобы сформировать вектор прерывания. Затем процессор ищет структуру данных, называемую «таблица векторов прерываний» (Interrupt Descriptor Table, IDT), чтобы дать функции — обработчику прерывания адрес, соответствующий его номеру.

Затем код по этому адресу исполняется и обрабатывает прерывание.

## Задаем IDT

    struct IDT_entry{
        unsigned short int offset_lowerbits;
        unsigned short int selector;
        unsigned char zero;
        unsigned char type_attr;
        unsigned short int offset_higherbits;
    };

    struct IDT_entry IDT[IDT_SIZE];

    void idt_init(void)
    {
        unsigned long keyboard_address;
        unsigned long idt_address;
        unsigned long idt_ptr[2];

        keyboard_address = (unsigned long)keyboard_handler;
        IDT[0x21].offset_lowerbits = keyboard_address & 0xffff;
        IDT[0x21].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */
        IDT[0x21].zero = 0;
        IDT[0x21].type_attr = 0x8e; /* INTERRUPT_GATE */
        IDT[0x21].offset_higherbits = (keyboard_address & 0xffff0000) >> 16;

        write_port(0x20 , 0x11);
        write_port(0xA0 , 0x11);

        write_port(0x21 , 0x20);
        write_port(0xA1 , 0x28);

        write_port(0x21 , 0x00);
        write_port(0xA1 , 0x00);

        write_port(0x21 , 0x01);
        write_port(0xA1 , 0x01);

        write_port(0x21 , 0xff);
        write_port(0xA1 , 0xff);

        idt_address = (unsigned long)IDT ;
        idt_ptr[0] = (sizeof (struct IDT_entry) * IDT_SIZE) + ((idt_address & 0xffff) << 16);
        idt_ptr[1] = idt_address >> 16 ;

        load_idt(idt_ptr);
    }

IDT — это массив, объединяющий структуры IDT_entry. Мы еще обсудим привязку клавиатурного прерывания к обработчику, а сейчас посмотрим, как работает PIC.

Современные системы x86 имеют два чипа PIC, у каждого восемь входных линий. Будем называть их PIC1 и PIC2. PIC1 получает от IRQ0 до IRQ7, а PIC2 — от IRQ8 до IRQ15. PIC1 использует порт 0x20 для команд и 0x21 для данных, а PIC2 — порт 0xA0 для команд и 0xA1 для данных.

Оба PIC инициализируются восьмибитными словами, которые называются «командные слова инициализации» (Initialization command words, ICW).

В защищенном режиме обоим PIC первым делом нужно отдать команду инициализации ICW1 (0x11). Она сообщает PIC, что нужно ждать еще трех инициализационных слов, которые придут на порт данных.

Эти команды передадут PIC:

- вектор отступа (ICW2),
- какие между PIC отношения master/slave (ICW3),
- дополнительную информацию об окружении (ICW4).

Вторая команда инициализации (ICW2) тоже шлется на вход каждого PIC. Она назначает `offset`, то есть значение, к которому мы добавляем номер линии, чтобы получить номер прерывания.

PIC разрешают каскадное перенаправление их выводов на вводы друг друга. Это делается при помощи ICW3, и каждый бит представляет каскадный статус для соответствующего IRQ. Сейчас мы не будем использовать каскадное перенаправление и выставим нули.

ICW4 задает дополнительные параметры окружения. Нам нужно определить только нижний бит, чтобы PIC знали, что мы работаем в режиме 80x86.

Та-дам! Теперь PIC проинициализированы.

У каждого PIC есть внутренний восьмибитный регистр, который называется «регистр масок прерываний» (Interrupt Mask Register, IMR). В нем хранится битовая карта линий IRQ, которые идут в PIC. Если бит задан, PIC игнорирует запрос. Это значит, что мы можем включить или выключить определенную линию IRQ, выставив соответствующее значение в 0 или 1.

Чтение из порта данных возвращает значение в регистре IMR, а запись — меняет регистр. В нашем коде после инициализации PIC мы выставляем все биты в единицу, чем деактивируем все линии IRQ. Позднее мы активируем линии, которые соответствуют клавиатурным прерываниям. Но для начала все же выключим!

Если линии IRQ работают, наши PIC могут получать сигналы по IRQ и преобразовывать их в номер прерывания, добавляя офсет. Нам же нужно заполнить IDT таким образом, чтобы номер прерывания, пришедшего с клавиатуры, соответствовал адресу функции-обработчика, которую мы напишем.

На какой номер прерывания нам нужно завязать в IDT обработчик клавиатуры?

Клавиатура использует IRQ1. Это входная линия 1, ее обрабатывает PIC1. Мы проинициализировали PIC1 с офсетом 0x20 (см. ICW2). Чтобы получить номер прерывания, нужно сложить 1 и 0x20, получится 0x21. Значит, адрес обработчика клавиатуры будет завязан в IDT на прерывание 0x21.

Задача сводится к тому, чтобы заполнить IDT для прерывания 0x21. Мы замапим это прерывание на функцию `keyboard_handler`, которую напишем в ассемблерном файле.

Каждая запись в IDT состоит из 64 бит. В записи, соответствующей прерыванию, мы не сохраняем адрес функции-обработчика целиком. Вместо этого мы разбиваем его на две части по 16 бит. Нижние биты сохраняются в первых 16 битах записи в IDT, а старшие 16 бит — в последних 16 битах записи. Все это сделано для совместимости с 286-ми процессорами. Как видишь, Intel выделывает такие номера на регулярной основе и во многих-многих местах!

В записи IDT нам осталось прописать тип, обозначив таким образом, что все это делается, чтобы отловить прерывание. Еще нам нужно задать офсет сегмента кода ядра. GRUB задает GDT за нас. Каждая запись GDT имеет длину 8 байт, где дескриптор кода ядра — это второй сегмент, так что его офсет составит 0x08 (подробности не влезут в эту статью). Гейт прерывания представлен как 0x8e. Оставшиеся в середине 8 бит заполняем нулями. Таким образом, мы заполним запись IDT, которая соответствует клавиатурному прерыванию.

Когда с маппингом IDT будет покончено, нам надо будет сообщить процессору, где находится IDT. Для этого существует ассемблерная инструкция lidt, она принимает один операнд. Им служит указатель на дескриптор структуры, которая описывает IDT.

С дескриптором никаких сложностей. Он содержит размер IDT в байтах и его адрес. Я использовал массив, чтобы вышло компактнее. Точно так же можно заполнить дескриптор при помощи структуры.

В переменной `idr_ptr` у нас есть указатель, который мы передаем инструкции `lidt` в функции `load_idt()`.

    load_idt:
        mov edx, [esp + 4]
        lidt [edx]
        sti
        ret

Дополнительно функция `load_idt()` возвращает прерывание при использовании инструкции `sti`.

Заполнив и загрузив IDT, мы можем обратиться к IRQ клавиатуры, используя маску прерывания, о которой мы говорили ранее.

    void kb_init(void)
    {
        write_port(0x21 , 0xFD);
    }

`0xFD` — это `11111101` — включаем только IRQ1 (клавиатуру).

## Функция — обработчик прерывания клавиатуры

Итак, мы успешно привязали прерывания клавиатуры к функции `keyboard_handler`, создав запись IDT для прерывания 0x21. Эта функция будет вызываться каждый раз, когда ты нажимаешь на какую-нибудь кнопку.

    keyboard_handler:
        call    keyboard_handler_main
        iretd

Эта функция вызывает другую функцию, написанную на C, и возвращает управление при помощи инструкций класса iret. Мы могли бы тут написать весь наш обработчик, но на C кодить значительно легче, так что перекатываемся туда. Инструкции `iret/iretd` нужно использовать вместо `ret`, когда управление возвращается из функции, обрабатывающей прерывание, в программу, выполнение которой было им прервано. Этот класс инструкций поднимает флаговый регистр, который попадает в стек при вызове прерывания.

    void keyboard_handler_main(void) {
        unsigned char status;
        char keycode;

        /* Пишем EOI */
        write_port(0x20, 0x20);

        status = read_port(KEYBOARD_STATUS_PORT);
        /* Нижний бит статуса будет выставлен, если буфер не пуст */
        if (status & 0x01) {
            keycode = read_port(KEYBOARD_DATA_PORT);
            if(keycode < 0)
                return;
            vidptr[current_loc++] = keyboard_map[keycode];
            vidptr[current_loc++] = 0x07;
        }
    }

Здесь мы сначала даем сигнал EOI (End Of Interrupt, окончание обработки прерывания), записав его в командный порт PIC. Только после этого PIC разрешит дальнейшие запросы на прерывание. Нам нужно читать два порта: порт данных 0x60 и порт команд (он же status port) 0x64.

Первым делом читаем порт 0x64, чтобы получить статус. Если нижний бит статуса — это ноль, значит, буфер пуст и данных для чтения нет. В других случаях мы можем читать порт данных 0x60. Он будет выдавать нам код нажатой клавиши. Каждый код соответствует одной кнопке. Мы используем простой массив символов, заданный в файле `keyboard_map.h`, чтобы привязать коды к соответствующим символам. Затем символ выводится на экран при помощи той же техники, что мы применяли в первой версии ядра.

Чтобы не усложнять код, я здесь обрабатываю только строчные буквы от a до z и цифры от 0 до 9. Ты с легкостью можешь добавить спецсимволы, Alt, Shift и Caps Lock. Узнать, что клавиша была нажата или отпущена, можно из вывода командного порта и выполнять соответствующее действие. Точно так же можешь привязать любые сочетания клавиш к специальным функциям вроде выключения.

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

И начинай печатать!

#### Оцени статью:

###### Что тебе понравилось больше всего?

Статья легкая и понятная Статья глубокая и подробная Интересуюсь этой темой Данные новые и актуальные Приятный стиль изложения

###### Что тебе не понравилось больше всего?

Статья поверхностная Статья слишком сложная Тема мне не интересна Данные устарели Неприятный стиль изложения
← Ранее Генеральный директор «Ростелеком-Solar» Игорь Ляпунов об особенностях российского рынка ИБ
Далее → Хакеры взломали аккаунт разработчиков криптовалюты Syscoin на GitHub и подменили официальный клиент

- Далее по этой теме
- Ранее по этой теме

- ### Эксперты рассказали о трояне Calisto для macOS, «прародителе» семейства малвари Proton

  Специалисты «Лаборатории Касперского» рассказали об обнаружении бэкдор для macOS, который …
  24.07.2018
  5 мин на чтение

- ### Пентест UEFI. Оцениваем защищенность прошивки UEFI с помощью CHIPSEC

  В этой статье мы разберем схему функционирования и устройство прошивки UEFI на материнских…
  09.07.2018
  14 мин на чтение

- ### Искусство распаковки. Потрошим защиту хитрого банкера GootKit

  Распаковка исполняемых файлов — одна из задач, с которыми приходится сталкиваться при реве…
  13.08.2018
  14 мин на чтение

- ### Пишем майнер на Java. Кодим добытчик криптовалюты Electroneum

  В наше время каждая бабушка слышала о криптовалютах, курсы майнинга проводят даже серьезны…
  30.07.2018
  12 мин на чтение

- ### APT из Поднебесной. Китайские хакерские группы и техники их целенаправленных атак

  Следы большинства таргетированных атак в последние годы приводят в азиатский регион, где я…
  09.08.2018
  13 мин на чтение

- ### Обгоняя флагманы. Большой гайд по оптимизации Android

  Тебе не хватает заряда аккумулятора? Надоели тормоза в приложениях? Не отчаивайся и не тор…
  20.06.2018
  13 мин на чтение

- ### Волшебные хуки. Как перехватывать управление любой программой через WinAPI

  Технология перехвата вызовов функций WinAPI известна уже давно, она часто используется как…
  26.01.2018
  11 мин на чтение

- ### Ручная распаковка. Вскрываем кастомный пакер на примере вымогателя GlobeImposter 2.0

  При реверсе вирусов зачастую обнаруживается, что малварь накрыта какой-нибудь «навесной» з…
  23.05.2018
  11 мин на чтение

- ### В гостях у чертёнка. FreeBSD глазами линуксоида

  Порог вхождения новичка в мир Linux за последние десять-пятнадцать лет ощутимо снизился. О…
  22.11.2017
  13 мин на чтение

- ### Android: инструменты для мобильного пентестера, история развития мобильной рансомвари и большое исследование безопасности загрузчиков

  Сегодня в выпуске: эволюция ransomware от простейших локеров до продвинутых криптошифровал…
  05.09.2017
  16 мин на чтение

- ### Взлом Wi-Fi со смартфона. Выполняем аудит точек доступа с Kali NetHunter и внешним Wi-Fi адаптером

  Вардрайвингом всерьёз можно заниматься и со смартфона, но для этого мало встроенного радио…
  13.07.2017
  16 мин на чтение

- ### Android: взлом приложений для Android и iOS и инструменты для мобильного пентестера

  В этом выпуске: разбор принципов работы Face ID и Touch ID, руководство по анализу и взлом…
  02.10.2017
  18 мин на чтение

9 комментариев к записи Давай напишем ядро! Создаем простейшее рабочее ядро операционной системы

#### Последние взломы

### Краденые миллиарды и плохой рэп. Как арестовали супругов, которые отмывали деньги Bitfinex и снимали тиктоки

### HTB EarlyAccess. Эксплуатируем веб-уязвимости и мисконфиги Docker

### HTB Horizontall. Захватываем веб-сервер через уязвимости в Strapi и Laravel

### Препарируем P-Code. Как реверсить старый софт на Visual Basic

#### Компьютерные трюки

### Терминальное счастье. Колонка главреда

### Анатомия терминала. Осваиваем текстуальную графику

### Змей-стражник. Сигнализация для велосипеда из старого смартфона на Android

### M1 для хакера. Тестируем Homebrew, виртуалки, Docker и Node на новой архитектуре Apple

#### Свежие новости

- 2 дня назад

  ### Для критической уязвимости в Magento появился эксплоит, а Adobe исправила второй аналогичный баг

- 2 дня назад

  ### Вредоносные письма могут вывести Cisco Email Security Appliance из строя

- 3 дня назад

  ### WordPress-плагин с 3 000 000 установок позволял всем желающим скачивать бэкапы сайтов

- 3 дня назад

  ### Француз случайно оставил без интернета целый город, пытаясь отключить интернет своим детям

- 3 дня назад

  ### Google представила «легкую» Chrome OS Flex, подходящую для старых ПК

Вопросы по материалам и подписке: support@glc.ru
Отдел рекламы и спецпроектов: yakovleva.a@glc.ru
Контент 18+
Сайт защищен Qrator —
самой забойной защитой от DDoS в мире
Подписка для физлиц
Подписка для юрлиц
Реклама на «Хакере»
Контакты
Posting....
wpDiscuz
FooBox
…
FooBox
…
FooBox
…