В этой статье мы продолжим наш путь создания простого, но функционального ядра операционной системы на языке C.

Поговорим с вами о том как:
Запуск ядра в Long mode
Работа с Multiboot таблицами
Реализация графического режима
Реализация драйвера ATA (IDE)
Реализация драйвера PCI
Реализация переключателя Kernel mode / User mode
Запуск ядра в Long mode
В предыдущей статье мы рассматривали запуск ядра в Protected Mode, где работа велась с 32-битными регистрами семейства EXX (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP, EFLAGS).
После перехода в Long Mode активируется поддержка нового набора 64-битных регистров семейства RXX (RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8-R15, RIP, RFLAGS). Это позволяет оперировать данными типа uint64_t, увеличивая объём обрабатываемой информации и расширяя адресное пространство.
Для активации данного режима необходимо на этапе ассемблерной инициализации правильно настроить процессор, обеспечив корректный переход в 64-битную среду выполнения.
; kernel.asm
[BITS 32]
start:
cli
; --- Включаем PAE (CR4.PAE = 1) ---
mov eax, cr4
bts eax, 5
mov cr4, eax
; --- Устанавливаем CR3 = адрес pml4_table ---
mov eax, pml4_table
mov cr3, eax
; --- Включаем LME через MSR IA32_EFER (0xC0000080) ---
mov ecx, 0xC0000080
rdmsr
bts eax, 8
wrmsr
; --- Включаем paging (CR0.PG = 1) ---
mov eax, cr0
bts eax, 31
mov cr0, eax
; --- Far jump в 64-bit код ---
jmp 0x08:long_mode_entry
; -----------------------------------------------------------------------
; 64-bit entry
; -----------------------------------------------------------------------
[BITS 64]
long_mode_entry:
mov ax, 0x10
mov ds, ax
mov es, ax
mov ss, ax
; настроим стек
lea rsp, [rel stack64_top]
and rsp, -16
; вызов 64-битного kmain (собранного с -m64)
mov rdi, rbx
call kmain
.hang64:
hlt
jmp .hang64
; -----------------------------------------------------------------------
; место под стек 64-bit
; -----------------------------------------------------------------------
section .bss
align 16
stack64_bottom:
resb 65536 ; резервируем 64 KiB под стек
stack64_top:
; -----------------------------------------------------------------------
; Конец
; -----------------------------------------------------------------------
section .note.GNU-stack
; empty
Данная настройка активирует в ядре поддержку 64-битного режима.
Скрытый текст
P.S.
В предыдущей статье в файле link.ld мы использовали отдельную кучу для пользовательских приложений.
Это решение оказалось нецелесообразным, поэтому в новой версии все данные размещаются в общей куче.
OUTPUT_FORMAT(elf64-x86-64)
ENTRY(start)
PHDRS
{
text PT_LOAD FLAGS(5); /* PF_R | PF_X */
data PT_LOAD FLAGS(6); /* PF_R | PF_W */
}
SECTIONS
{
. = 0x00100000;
.multiboot ALIGN(8) : { KEEP(*(.multiboot)) } :text
.text : {
*(.text)
*(.text*)
*(.rodata)
*(.rodata*)
} :text
.data : {
*(.data)
*(.data*)
} :data
.bss : {
*(.bss)
*(.bss*)
*(COMMON)
} :data
.heap (NOLOAD) : {
. = ALIGN(8);
_heap_start = .;
. = . + 512 * 1024 * 1024;
_heap_end = .;
} :data
}
P.P.S.
После перехода в Long Mode и использования новых регистров примеры кода обработки прерываний из прошлой статьи в новом ядре работать не будут.
Смотрите новый код здесь.
Работа с Multiboot таблицами
Поскольку ядро для своей загрузки использует загрузчик GRUB, который выполняет первоначальную настройку системы и запускает ядро, мы можем использовать Multiboot-таблицы.
С их помощью можно как настраивать GRUB, так и получать от него полезную информацию о системе. Полную спецификацию структуры Multiboot-таблиц можно найти в официальной документации.
В текущей реализации мы будем использовать Multiboot-таблицы для двух целей:
Обеспечения поддержки запуска ядра в режиме UEFI (через Multiboot v1).
Реализации графического видеорежима (с помощью Multiboot v2).
Для этого, перед запуском ядра, на этапе начальной загрузки, необходимо в секции .text прописать ключевые настройки, которые сообщат GRUB о наших требованиях.
; kernel.asm
[BITS 32]
section .text
align 8
; --------------------
; Multiboot v1 (минимальный)
; --------------------
align 4
mb1_start:
dd 0x1BADB002 ; magic
dd 0x00000000 ; flags = 0 (минимальный)
dd -(0x1BADB002 + 0x00000000) ; checksum
; --------------------
; Multiboot2 (минимальный)
; --------------------
align 8
mb2_start:
dd 0xE85250D6 ; magic (multiboot2)
dd 0x00000000 ; architecture (0 = i386)
dd mb2_end - mb2_start ; header length (в байтах)
; checksum = -(magic + arch + length) (выражение ниже даёт 32-bit)
dd 0x100000000 - (0xE85250D6 + 0x00000000 + (mb2_end - mb2_start))
; --- Framebuffer (header) tag: request preferred mode ---
align 8
dw 5 ; tag type = 5 (framebuffer request in header)
dw 0 ; flags (0 = required; if you want optional set bit0)
dd 20 ; size of this tag (header + payload: 2+2+4 + 3*4 = 20)
dd 1920 ; width (px) <-- поменяйте на нужную
dd 1080 ; height (px) <-- поменяйте на нужную
dd 32 ; bpp (bits per pixel) <-- 32 обычно
; --- End tag ---
align 8
dw 0
dw 0
dd 8
mb2_end:
Данная настройка обеспечивает загрузку ядра через UEFI (если система его поддерживает) и инициализацию framebuffer'а с заданным разрешением. GRUB также передаст в регистре EBX указатель на структуру Multiboot.
В дальнейшем этот указатель мы передаём в ядро через регистр RAX, после чего парсим полученную структуру для получения необходимой информации о системе.
Реализация графического режима
После настройки таблиц и получения в регистре EBX указателя на структуру, можно приступать к реализации графического режима.
Первым шагом является передача этого указателя в код на C. Для этого необходимо переместить значение из регистра RBX в регистр RAX.
Скрытый текст
Почему RBX, если был EBX?
Когда CPU находится в 64-битном режиме (Long Mode), любой доступ к 32-битному регистру EAX/EBX/ECX/… автоматически выполняет zero-extend в соответствующий 64-битный регистр RAX/RBX/RCX/...
mov ebx, value
в Long mode запишет низшие 32 бита RBX, а старшие 32 бита RBX будут обнулены.
; kernel.asm
; вызов 64-битного kmain (собранного с -m64)
mov rdi, rbx
call kmain
// kernel.c
/*-------------------------------------------------------------
Основная функция ядра
-------------------------------------------------------------*/
void kmain(uint64_t mb2_addr)
{
}
После получения адреса структуры Multiboot 2 в переменной mb2_addr мы можем приступить к её обработке. Для этого необходимо:
Описать в коде структуры данных в соответствии со спецификацией Multiboot 2.
Реализовать парсер для обхода и извлечения информации из тегов, расположенных по этому адресу.
// graphics/mb2/mb2.c
/* Глобальная структура с информацией о фреймбуфере */
static framebuffer_info_t fb_info;
/* Вспомогательная функция - выравнивает значение вверх до 8 байт */
static inline size_t align_up8(size_t x)
{
return (x + (MB2_TAG_ALIGN - 1)) & ~(MB2_TAG_ALIGN - 1);
}
/* Безопасное чтение 32-битного значения из памяти
(используется memcpy, чтобы избежать проблем с невыравненными адресами) */
static inline uint32_t read_u32(const void *p)
{
uint32_t v;
memcpy(&v, p, sizeof(v));
return v;
}
/* Безопасное чтение 64-битного значения */
static inline uint64_t read_u64(const void *p)
{
uint64_t v;
memcpy(&v, p, sizeof(v));
return v;
}
static void process_framebuffer_tag(uint8_t *payload, size_t payload_len)
{
/* Современный формат: 64-битный адрес + pitch + width + height + bpp + тип */
if (payload_len >= MB2_FB_PAYLOAD_MODERN)
{
uint64_t addr64 = read_u64(payload + 0);
uint32_t pitch = read_u32(payload + 8);
uint32_t width = read_u32(payload + 12);
uint32_t height = read_u32(payload + 16);
uint8_t bpp = 0;
uint8_t fbtype = 0;
memcpy(&bpp, payload + 20, 1);
memcpy(&fbtype, payload + 21, 1);
fb_info.addr = addr64;
fb_info.pitch = pitch;
fb_info.width = width;
fb_info.height = height;
fb_info.bpp = bpp;
fb_info.fb_type = fbtype;
}
/* Старый формат: 32-битный адрес + pitch + width + height */
else if (payload_len >= MB2_FB_PAYLOAD_LEGACY)
{
uint32_t addr32 = read_u32(payload + 0);
uint32_t pitch = read_u32(payload + 4);
uint32_t width = read_u32(payload + 8);
uint32_t height = read_u32(payload + 12);
fb_info.addr = (uint64_t)addr32;
fb_info.pitch = pitch;
fb_info.width = width;
fb_info.height = height;
fb_info.bpp = 0;
fb_info.fb_type = 0;
}
else if (payload_len >= MB2_FB_PAYLOAD_MINIMAL)
{
uint64_t addr64 = read_u64(payload + 0);
fb_info.addr = addr64;
fb_info.pitch = 0;
fb_info.width = 0;
fb_info.height = 0;
fb_info.bpp = 0;
fb_info.fb_type = 0;
}
/* Иначе данных недостаточно - игнорируем */
}
/* Функция вычисляет примерный размер фреймбуфера в байтах.
Если высота неизвестна - возвращает минимум 2 МиБ. */
uint64_t fb_calc_size(const framebuffer_info_t *fb)
{
if (!fb)
return DEFAULT_FB_SIZE;
/* Если неизвестен pitch (байт на строку) или высота - возвращаем 2 МиБ */
if (fb->pitch == 0 || fb->height == 0)
return DEFAULT_FB_SIZE;
if (fb->height > UINT64_MAX / fb->pitch)
return DEFAULT_FB_SIZE;
/* Общий объём памяти, занимаемой изображением */
uint64_t bytes = (uint64_t)fb->pitch * (uint64_t)fb->height;
/* Округляем вверх до ближайшего кратного 2 МиБ */
uint64_t rounded = (bytes + 0x1FFFFF) & ~((uint64_t)0x1FFFFF);
if (rounded == 0)
rounded = DEFAULT_FB_SIZE;
return rounded;
}
/* Основная функция разбора структуры Multiboot2 */
void mb2_parse(uint64_t mb2_addr)
{
if (mb2_addr == 0)
return;
/* Обнуляем структуру с информацией о фреймбуфере */
memset(&fb_info, 0, sizeof(fb_info));
/* Указатель на начало Multiboot2-заголовка */
uint8_t *base = (uint8_t *)(uintptr_t)mb2_addr;
/* Первые 8 байт: общий размер и резерв */
uint32_t total_size = read_u32(base + 0);
/* Проверяем корректность размера */
if (total_size < MB2_TAG_HDR_SIZE)
return;
uint8_t *end = base + total_size; /* конец всей структуры */
uint8_t *ptr = base + MB2_TAG_HDR_SIZE; /* первый тег идёт сразу после заголовка */
/* Проходим по всем тегам */
while (ptr + MB2_TAG_HDR_SIZE <= end)
{
/* Читаем заголовок тега */
uint32_t tag_type = read_u32(ptr);
uint32_t tag_size = read_u32(ptr + 4);
/* Проверяем корректность размера тега */
if (tag_size < MB2_TAG_HDR_SIZE)
break;
/* Вычисляем смещение к следующему тегу, с выравниванием */
size_t aligned_size = align_up8((size_t)tag_size);
uint8_t *next = ptr + aligned_size;
if (next > end || next <= ptr)
break; /* повреждённая структура или переполнение - выходим */
/* Полезная нагрузка идёт сразу после 8-байтного заголовка */
uint8_t *payload = ptr + MB2_TAG_HDR_SIZE;
size_t payload_len = (size_t)tag_size - MB2_TAG_HDR_SIZE;
/* Обрабатываем тег по типу */
switch (tag_type)
{
/* Тег конца списка - выходим */
case MB2_TAG_TYPE_END:
return;
/* Тег с информацией о framebuffer */
case MB2_TAG_TYPE_FRAMEBUFFER:
process_framebuffer_tag(payload, payload_len);
break;
/* Все прочие теги игнорируем */
default:
break;
}
/* Переходим к следующему тегу */
ptr = next;
}
}
/* Возвращает указатель на заполненную структуру framebuffer_info */
framebuffer_info_t *get_framebuffer_info(void)
{
return &fb_info;
}
Скрытый текст
Это отрывок кода парсера, полный код здесь.
Данный код получает все необходимые данные о Framebuffer (адрес буфера, разрешение экрана и т.д.), сохраняет их в структуру и возвращает для дальнейшего использования.
Формат работы с буфером:
Пиксели располагаются строка за строкой, сверху вниз:
addr + 0*pitch … первая строка
addr + 1*pitch … вторая строка
addr + y*pitch … строка y
Внутри строки пиксели идут слева направо:
pixel(x) = base + x * (bpp / 8)
Адрес конкретного пикселя вычисляется по формуле:
addr + y*pitch + x*(bpp/8)
Мы получили физический адрес буфера, по которому расположена видеопамять. Однако в операционных системах с включенной виртуальной памятью прямое обращение к физическим адресам невозможно - все программы работают с виртуальными адресами.
Почему запись не отображается на экране?
Если попытаться записать данные по полученному физическому адресу, изменений на экране не произойдет, так как процессор использует механизм виртуальной памяти и интерпретирует все адреса как виртуальные. Для доступа к оборудованию необходимо настроить отображение виртуальных адресов на физические.
Решение: Identity Mapping
В данном случае реализован частный случай отображения памяти - Identity Map (тождественное отображение). Вся физическая память в диапазоне от 0 до 4 ГБ напрямую отображается на идентичные виртуальные адреса.
; kernel.asm
; -----------------------------------------------------------------------
; Identity-map 0..4GiB (2MiB pages)
; -----------------------------------------------------------------------
section .data
align 4096
pml4_table:
dq pdpt_table + 0x007 ; PML4[0] -> PDPT (flags in low bits)
align 4096
pdpt_table:
dq pd_table0 + 0x007 ; PDPT[0] -> PD0 (0..1GiB)
dq pd_table1 + 0x007 ; PDPT[1] -> PD1 (1..2GiB)
dq pd_table2 + 0x007 ; PDPT[2] -> PD2 (2..3GiB)
dq pd_table3 + 0x007 ; PDPT[3] -> PD3 (3..4GiB)
; остальные записи нулевые (по умолчанию)
; PD0: maps 0x0000_0000 .. 0x3FF_FFFF (1 GiB)
align 4096
pd_table0:
%assign j 0
%rep 512
dq j * 0x200000 + 0x087
%assign j j + 1
%endrep
; PD1: maps 0x4000_0000 .. 0x7FF_FFFF
align 4096
pd_table1:
%assign j 512
%rep 512
dq j * 0x200000 + 0x087
%assign j j + 1
%endrep
; PD2: maps 0x8000_0000 .. 0xBFF_FFFF
align 4096
pd_table2:
%assign j 1024
%rep 512
dq j * 0x200000 + 0x087
%assign j j + 1
%endrep
; PD3: maps 0xC000_0000 .. 0xFFF_FFFF (сюда попадает 0xFD000000)
align 4096
pd_table3:
%assign j 1536
%rep 512
dq j * 0x200000 + 0x087
%assign j j + 1
%endrep
После активации данной таблицы страниц (Paging) создается прямое соответствие: каждый физический адрес в диапазоне 0-4 ГБ доступен по идентичному виртуальному адресу. Это означает, что физический адрес видеобуфера 0xFD000000 автоматически становится доступным по тому же виртуальному адресу 0xFD000000.
Итог: Благодаря identity mapping запись данных по виртуальному адресу, равному физическому адресу видеобуфера, теперь напрямую изменяет изображение на экране.
Реализация драйвера ATA (IDE)
ATA/IDE — это протокол, по которому ОС взаимодействует с жёсткими дисками, SSD (старые), CD/DVD-ROM и другими устройствами хранения.
Структура системы
CPU → I/O порты → IDE контроллер → Устройство (HDD/CD-ROM)
Драйвер поддерживает архитектуру из двух каналов: основного (Primary) и дополнительного (Secondary). Каждый из этих каналов, в свою очередь, обеспечивает управление парой устройств - ведущим (Master) и ведомым (Slave).
┌─────────────────────────────────────────────────────┐
│ КОНТРОЛЛЕР IDE │
├─────────────────────────────────────────────────────┤
│ PRIMARY CHANNEL (0x1F0-0x1F7, 0x3F6) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ MASTER │ │ SLAVE │ │
│ │ (hda) │ │ (hdb) │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ SECONDARY CHANNEL (0x170-0x177, 0x376) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ MASTER │ │ SLAVE │ │
│ │ (hdc) │ │ (hdd) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────┘
Регистры
0x00: DATA - Чтение/запись 16-битных слов
0x01: ERROR - Код ошибки (чтение)
0x02: NSECT - Количество секторов
0x03: SECTOR - LBA0 / номер сектора CHS
0x04: LCYL - LBA1 / младший цилиндр CHS
0x05: HCYL - LBA2 / старший цилиндр CHS
0x06: SELECT - Выбор устройства (бит 4: 0=master,1=slave)
0x07: STATUS - Статус операции (чтение)
0x07: COMMAND - Команда контроллеру (запись)
Биты STATUS (0x07):
BSY(7)=1 - устройство занято
DRDY(6)=1 - устройство готово
DRQ(3)=1 - данные готовы к передаче
ERR(0)=1 - произошла ошибка
// drivers/ide.c
#include "ide.h"
#include "../portio/portio.h"
/* Небольшая задержка: четыре чтения ALTSTATUS (~100ns) */
static inline void io_delay(uint16_t ctrl_port)
{
(void)inb(ctrl_port + IDE_ALTSTATUS);
(void)inb(ctrl_port + IDE_ALTSTATUS);
(void)inb(ctrl_port + IDE_ALTSTATUS);
(void)inb(ctrl_port + IDE_ALTSTATUS);
}
/* Ждём BSY=0 */
static int wait_bsy_clear(uint16_t base_port, uint16_t ctrl_port, uint32_t timeout)
{
for (uint32_t i = 0; i < timeout; ++i)
{
uint8_t s = inb(base_port + IDE_STATUS);
if (!(s & IDE_STATUS_BSY))
return IDE_OK;
if ((i & 0xFF) == 0)
io_delay(ctrl_port);
}
return IDE_ERR_TIMEOUT;
}
/* Ждём DRQ=1 и BSY=0; если ERR — возвращаем ошибку */
static int wait_drq_or_err(uint16_t base_port, uint16_t ctrl_port, uint32_t timeout)
{
for (uint32_t i = 0; i < timeout; ++i)
{
uint8_t s = inb(base_port + IDE_STATUS);
if (s & IDE_STATUS_ERR)
return IDE_ERR_DEVICE;
if (!(s & IDE_STATUS_BSY) && (s & IDE_STATUS_DRQ))
return IDE_OK;
if ((i & 0xFF) == 0)
io_delay(ctrl_port);
}
return IDE_ERR_TIMEOUT;
}
/* Проверить ERR и прочитать регистр ошибок */
static int check_err_and_clear(uint16_t base_port)
{
uint8_t s = inb(base_port + IDE_STATUS);
if (s & IDE_STATUS_ERR)
{
(void)inb(base_port + IDE_ERROR);
return IDE_ERR_DEVICE;
}
return IDE_OK;
}
/* Выбрать устройство (LBA/CHS) и задержка */
static void select_device_and_delay(uint16_t base, uint16_t ctrl, uint8_t drive, int lba_flag, uint8_t head_high4)
{
uint8_t value = (lba_flag ? 0xE0 : 0xA0) | ((drive & 1) << 4) | (head_high4 & 0x0F);
outb(base + IDE_SELECT, value);
io_delay(ctrl);
}
/* Собрать 4 слова идентификатора в uint64_t (малый порядок слов) */
static uint64_t ident_words_to_u64(const uint16_t ident[256], int w)
{
uint64_t v = 0;
v |= (uint64_t)ident[w + 0];
v |= (uint64_t)ident[w + 1] << 16;
v |= (uint64_t)ident[w + 2] << 32;
v |= (uint64_t)ident[w + 3] << 48;
return v;
}
/* Прочитать один сектор (256 слов) в aligned_words */
static void read_sector_words_to(uint16_t base, uint16_t aligned_words[256])
{
for (int i = 0; i < 256; ++i)
aligned_words[i] = inw(base + IDE_DATA);
}
/* Записать один сектор (256 слов) из aligned_words */
static void write_sector_words_from(uint16_t base, const uint16_t aligned_words[256])
{
for (int i = 0; i < 256; ++i)
outw(base + IDE_DATA, aligned_words[i]);
}
/* IDENTIFY: заполняет ident_buffer и поля disk */
int ide_identify(ide_disk_t *disk, uint16_t ident_buffer[256])
{
if (!disk || !ident_buffer)
return IDE_ERR_INVALID;
disk->type = IDE_TYPE_NONE;
disk->supports_lba48 = 0;
disk->sector_size = 512;
disk->total_sectors = 0;
/* Выбор устройства (CHS) */
select_device_and_delay(disk->base_port, disk->ctrl_port, disk->drive, 0, 0);
/* Отправка IDENTIFY */
outb(disk->base_port + IDE_COMMAND, IDE_CMD_IDENTIFY);
io_delay(disk->ctrl_port);
uint8_t status = inb(disk->base_port + IDE_STATUS);
if (status == 0)
return IDE_ERR_DEVICE;
/* Если ERR — возможно ATAPI */
if (status & IDE_STATUS_ERR)
{
uint8_t cl = inb(disk->base_port + IDE_LCYL);
uint8_t ch = inb(disk->base_port + IDE_HCYL);
if ((cl == 0x14 && ch == 0xEB) || (cl == 0x69 && ch == 0x96))
{
outb(disk->base_port + IDE_COMMAND, IDE_CMD_IDENTIFY_PACKET);
if (wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS) != IDE_OK)
return IDE_ERR_TIMEOUT;
if (check_err_and_clear(disk->base_port) != IDE_OK)
return IDE_ERR_DEVICE;
if (wait_drq_or_err(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS) != IDE_OK)
return IDE_ERR_TIMEOUT;
for (int i = 0; i < 256; ++i)
ident_buffer[i] = inw(disk->base_port + IDE_DATA);
disk->type = IDE_TYPE_ATAPI;
disk->sector_size = 2048;
disk->total_sectors = 0;
return IDE_OK;
}
else
{
return IDE_ERR_DEVICE;
}
}
if (wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS) != IDE_OK)
return IDE_ERR_TIMEOUT;
int rc = wait_drq_or_err(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS);
if (rc != IDE_OK)
return rc;
for (int i = 0; i < 256; ++i)
ident_buffer[i] = inw(disk->base_port + IDE_DATA);
if (ident_buffer[0] == 0)
return IDE_ERR_DEVICE;
disk->type = IDE_TYPE_ATA;
/* LBA28 (words 60-61) */
uint32_t lba28 = ((uint32_t)ident_buffer[61] << 16) | ident_buffer[60];
disk->total_sectors = lba28;
/* Проверка LBA48 (word 83 bit 10) */
if (ident_buffer[83] & (1u << 10))
{
disk->supports_lba48 = 1;
uint64_t lba48 = ident_words_to_u64(ident_buffer, 100);
if (lba48 != 0)
disk->total_sectors = lba48;
}
else
{
disk->supports_lba48 = 0;
}
disk->sector_size = 512;
return IDE_OK;
}
/* Инициализация структуры и вызов IDENTIFY */
int ide_init(ide_disk_t *disk, ide_channel_t channel, uint8_t drive)
{
if (!disk || drive > 1)
return IDE_ERR_INVALID;
if (channel == IDE_CHANNEL_PRIMARY)
{
disk->base_port = IDE_BASE_PRIMARY;
disk->ctrl_port = IDE_CTRL_PRIMARY;
}
else
{
disk->base_port = IDE_BASE_SECONDARY;
disk->ctrl_port = IDE_CTRL_SECONDARY;
}
disk->drive = drive & 1;
disk->channel = channel;
disk->type = IDE_TYPE_NONE;
disk->sector_size = 512;
disk->supports_lba48 = 0;
disk->total_sectors = 0;
uint16_t ident[256];
int rc = ide_identify(disk, ident);
if (rc != IDE_OK)
return rc;
return IDE_OK;
}
/* Настройка регистров для LBA28 */
static void setup_lba28_regs(uint16_t base, uint16_t ctrl, uint32_t lba, uint8_t count, uint8_t drive)
{
select_device_and_delay(base, ctrl, drive, 1, (uint8_t)((lba >> 24) & 0x0F));
outb(base + IDE_NSECT, count);
outb(base + IDE_SECTOR, (uint8_t)(lba & 0xFF));
outb(base + IDE_LCYL, (uint8_t)((lba >> 8) & 0xFF));
outb(base + IDE_HCYL, (uint8_t)((lba >> 16) & 0xFF));
}
/* Настройка регистров для LBA48 */
static void setup_lba48_regs(uint16_t base, uint16_t ctrl, uint64_t lba, uint16_t count, uint8_t drive)
{
select_device_and_delay(base, ctrl, drive, 1, (uint8_t)((lba >> 24) & 0x0F));
io_delay(ctrl);
outb(base + IDE_NSECT, (uint8_t)((count >> 8) & 0xFF)); /* SECCOUNT1 */
outb(base + IDE_SECTOR, (uint8_t)((lba >> 24) & 0xFF)); /* LBA3 */
outb(base + IDE_LCYL, (uint8_t)((lba >> 32) & 0xFF)); /* LBA4 */
outb(base + IDE_HCYL, (uint8_t)((lba >> 40) & 0xFF)); /* LBA5 */
outb(base + IDE_NSECT, (uint8_t)(count & 0xFF)); /* SECCOUNT0 */
outb(base + IDE_SECTOR, (uint8_t)(lba & 0xFF)); /* LBA0 */
outb(base + IDE_LCYL, (uint8_t)((lba >> 8) & 0xFF)); /* LBA1 */
outb(base + IDE_HCYL, (uint8_t)((lba >> 16) & 0xFF)); /* LBA2 */
}
/* Проверка выравнивания по 2 байтам */
static inline int is_aligned_2(const void *ptr)
{
return (((uintptr_t)ptr) & 1u) == 0;
}
/* Чтение секторов */
int ide_read_sectors(ide_disk_t *disk, uint64_t lba, uint32_t count, void *buffer)
{
if (!disk || !buffer || count == 0)
return IDE_ERR_INVALID;
if (disk->total_sectors && lba > disk->total_sectors - (uint64_t)count)
return IDE_ERR_INVALID;
const uint32_t max_per_op = 256;
uint8_t *user_buf = (uint8_t *)buffer;
uint32_t remaining = count;
uint16_t tmp_sector_words[256];
while (remaining)
{
uint32_t chunk = remaining > max_per_op ? max_per_op : remaining;
for (uint32_t s = 0; s < chunk; ++s)
{
uint64_t cur_lba = lba + (count - remaining) + s;
if (disk->supports_lba48 && (cur_lba > 0x0FFFFFFF))
{
setup_lba48_regs(disk->base_port, disk->ctrl_port, cur_lba, 1, disk->drive);
outb(disk->base_port + IDE_COMMAND, IDE_CMD_READ_SECTORS_EXT);
}
else
{
uint32_t cur_lba32 = (uint32_t)cur_lba;
setup_lba28_regs(disk->base_port, disk->ctrl_port, cur_lba32, 1, disk->drive);
outb(disk->base_port + IDE_COMMAND, IDE_CMD_READ_SECTORS);
}
int rc = wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS);
if (rc != IDE_OK)
return rc;
rc = wait_drq_or_err(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS);
if (rc != IDE_OK)
return rc;
int bytes_per_sector = disk->sector_size;
int words_per_sector = bytes_per_sector / 2;
uint8_t *dest = user_buf + ((count - remaining) + s) * (size_t)bytes_per_sector;
if (is_aligned_2(dest) && bytes_per_sector == 512)
{
uint16_t *wptr = (uint16_t *)dest;
for (int i = 0; i < words_per_sector; ++i)
wptr[i] = inw(disk->base_port + IDE_DATA);
}
else
{
if (bytes_per_sector == 512)
{
read_sector_words_to(disk->base_port, tmp_sector_words);
memcpy(dest, tmp_sector_words, 512);
}
else
{
for (int i = 0; i < words_per_sector; ++i)
{
uint16_t w = inw(disk->base_port + IDE_DATA);
dest[2 * i + 0] = (uint8_t)(w & 0xFF);
dest[2 * i + 1] = (uint8_t)((w >> 8) & 0xFF);
}
}
}
}
user_buf += (size_t)chunk * disk->sector_size;
remaining -= chunk;
lba += chunk;
}
return IDE_OK;
}
/* Запись секторов */
int ide_write_sectors(ide_disk_t *disk, uint64_t lba, uint32_t count, const void *buffer)
{
if (!disk || !buffer || count == 0)
return IDE_ERR_INVALID;
if (disk->total_sectors && lba > disk->total_sectors - (uint64_t)count)
return IDE_ERR_INVALID;
const uint32_t max_per_op = 256;
const uint8_t *user_buf = (const uint8_t *)buffer;
uint32_t remaining = count;
int used_lba48 = 0;
uint16_t tmp_sector_words[256];
while (remaining)
{
uint32_t chunk = remaining > max_per_op ? max_per_op : remaining;
for (uint32_t s = 0; s < chunk; ++s)
{
uint64_t cur_lba = lba + (count - remaining) + s;
int use_lba48 = disk->supports_lba48 && (cur_lba > 0x0FFFFFFF);
if (use_lba48)
{
setup_lba48_regs(disk->base_port, disk->ctrl_port, cur_lba, 1, disk->drive);
outb(disk->base_port + IDE_COMMAND, IDE_CMD_WRITE_SECTORS_EXT);
used_lba48 = 1;
}
else
{
uint32_t cur_lba32 = (uint32_t)cur_lba;
setup_lba28_regs(disk->base_port, disk->ctrl_port, cur_lba32, 1, disk->drive);
outb(disk->base_port + IDE_COMMAND, IDE_CMD_WRITE_SECTORS);
}
int rc = wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS);
if (rc != IDE_OK)
return rc;
rc = wait_drq_or_err(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS);
if (rc != IDE_OK)
return rc;
int bytes_per_sector = disk->sector_size;
int words_per_sector = bytes_per_sector / 2;
const uint8_t *src = user_buf + ((count - remaining) + s) * (size_t)bytes_per_sector;
if (is_aligned_2(src) && bytes_per_sector == 512)
{
const uint16_t *wptr = (const uint16_t *)src;
for (int i = 0; i < words_per_sector; ++i)
outw(disk->base_port + IDE_DATA, wptr[i]);
}
else
{
if (bytes_per_sector == 512)
{
memcpy(tmp_sector_words, src, 512);
for (int i = 0; i < words_per_sector; ++i)
outw(disk->base_port + IDE_DATA, tmp_sector_words[i]);
}
else
{
for (int i = 0; i < words_per_sector; ++i)
{
uint16_t w = (uint16_t)src[2 * i] | ((uint16_t)src[2 * i + 1] << 8);
outw(disk->base_port + IDE_DATA, w);
}
}
}
if (wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS) != IDE_OK)
return IDE_ERR_TIMEOUT;
if (check_err_and_clear(disk->base_port) != IDE_OK)
return IDE_ERR_DEVICE;
}
user_buf += (size_t)chunk * disk->sector_size;
remaining -= chunk;
lba += chunk;
}
/* Flush cache */
if (used_lba48)
outb(disk->base_port + IDE_COMMAND, IDE_CMD_CACHE_FLUSH_EXT);
else
outb(disk->base_port + IDE_COMMAND, IDE_CMD_CACHE_FLUSH);
if (wait_bsy_clear(disk->base_port, disk->ctrl_port, IDE_TIMEOUT_LOOPS) != IDE_OK)
return IDE_ERR_TIMEOUT;
if (check_err_and_clear(disk->base_port) != IDE_OK)
return IDE_ERR_DEVICE;
return IDE_OK;
}
Данный код реализует полный набор инструментов для работы с дисками и включает все необходимые проверки для обеспечения безопасной работы.
Реализация драйвера PCI
PCI драйвер - это программное обеспечение, которое обеспечивает взаимодействие между операционной системой и устройствами, подключенными к шине PCI (Peripheral Component Interconnect).
Примеры устройств, требующих PCI драйверов:
Видеокарты (GPU)
Сетевые карты (NIC)
Звуковые карты
USB контроллеры
RAID контроллеры
К��к работает доступ к конфигурации?
В x86 системах используются два портовых адреса для доступа к PCI конфигурационному пространству:
Порт
0xCF8- адресный порт. В этот порт записывается адрес того устройства и регистра, который нужно прочитать или записать.Порт
0xCFC- портовый адрес данных. Из этого порта читаются или в этот порт записываются данные.
Принцип простой: сначала указываем адрес (outl(0xCF8, адрес)), потом читаем/пишем данные (inl(0xCFC) или outl(0xCFC, данные)).
Формат адреса конфигурации
Адрес - это 32-битное число, в котором закодирована информация о том, какое устройство и регистр нам нужны:
Бит 31 → 1 (включить доступ)
Биты 23-16 → номер шины (Bus)
Биты 15-11 → номер слота/устройства (Device)
Биты 10-8 → номер функции (Function)
Биты 7-2 → смещение в конфиг. пространстве (Offset)
Функция pci_conf_addr() формирует такой адрес из компонентов.
Основной алгоритм сканирования
Драйвер работает по схеме:
Инициализация → Программа вызывает
pci_init(). Она проверяет, мультифункциональное ли устройство0:0:0(первое устройство на первой шине). Если да, то в системе несколько контроллеров, и нужно сканировать шины 0-7. Если нет, то сканируется только шина 0.Сканирование шины →
pci_scan_bus()перебирает все 32 возможных устройства на шине (номера 0-31).Проверка устройства →
pci_check_device()проверяет, есть ли на этом слоте устройство. Если есть, проверяет, поддерживает ли оно несколько функций. Устройство может иметь до 8 функций (0-7). Обычно одна функция, но некоторые сложные устройства имеют несколько.-
Проверка функции →
pci_check_function()читает параметры функции из конфигурационного пространства:Vendor ID (смещение
0x00) - производительDevice ID (смещение
0x02) - идентификатор устройстваClass Code (смещение
0x08) - класс устройства (видеокарта, сетевая карта и т.д.)Subclass (смещение
0x08) - подклассHeader Type (смещение
0x0C) - тип заголовка конфигурации
Зондирование BAR →
pci_probe_bars()определяет адреса и размеры памяти/портов, которые использует устройство. BAR расшифровывается как Base Address Register - регистры, где хранятся адреса ресурсов устройства. Их может быть до 6 на устройство.Регистрация → Найденное устройство добавляется в глобальный массив
g_pci_devices[].Рекурсия → Если найденное устройство является PCI-to-PCI bridge (класс
0x06, подкласс0x04), это значит, что за ним стоит ещё одна PCI шина (secondary bus). Драйвер читает номер этой шины из смещения0x19и рекурсивно её сканирует.
Код драйвера:
// drivers/pci.c
#include "pci.h"
#include "../portio/portio.h"
#include "../malloc/malloc.h"
#include "../graphics/exception_handler/kprint.h"
#include <string.h>
#include <stdint.h>
#define CONFIG_ADDRESS_PORT 0xCF8
#define CONFIG_DATA_PORT 0xCFC
static pci_device_t *g_pci_devices = NULL;
static int g_pci_dev_count = 0;
static int g_pci_dev_capacity = 0;
static void ensure_capacity(void)
{
if (!g_pci_devices)
{
g_pci_dev_capacity = 32;
g_pci_devices = (pci_device_t *)malloc(sizeof(pci_device_t) * g_pci_dev_capacity);
g_pci_dev_count = 0;
}
else if (g_pci_dev_count >= g_pci_dev_capacity)
{
int nc = g_pci_dev_capacity * 2;
g_pci_devices = (pci_device_t *)realloc(g_pci_devices, sizeof(pci_device_t) * nc);
g_pci_dev_capacity = nc;
}
}
/* Формирование 32-битного адреса конфигурации (Mechanism #1) */
static uint32_t pci_conf_addr(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset)
{
return (uint32_t)((1u << 31) |
((uint32_t)bus << 16) |
((uint32_t)device << 11) |
((uint32_t)function << 8) |
(offset & 0xFC));
}
/* Чтение / запись конфигурационного пространства */
static uint32_t pci_config_read32(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset)
{
uint32_t addr = pci_conf_addr(bus, device, function, offset);
outl(CONFIG_ADDRESS_PORT, addr);
return inl(CONFIG_DATA_PORT);
}
static void pci_config_write32(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint32_t value)
{
uint32_t addr = pci_conf_addr(bus, device, function, offset);
outl(CONFIG_ADDRESS_PORT, addr);
outl(CONFIG_DATA_PORT, value);
}
static uint16_t pci_config_read16(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset)
{
uint32_t v = pci_config_read32(bus, device, function, offset & 0xFC);
int shift = (offset & 2) * 8;
return (uint16_t)((v >> shift) & 0xFFFF);
}
static uint8_t pci_config_read8(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset)
{
uint32_t v = pci_config_read32(bus, device, function, offset & 0xFC);
int shift = (offset & 3) * 8;
return (uint8_t)((v >> shift) & 0xFF);
}
/* Добавление устройства в список */
static void pci_register_device(const pci_device_t *dev)
{
ensure_capacity();
g_pci_devices[g_pci_dev_count++] = *dev;
}
/* Пробирование BAR: возвращает base и size (size==0 => не выделен) */
static void pci_probe_bars(pci_device_t *dev)
{
/* Сохраним Command Register и временно выключим I/O и Memory responses */
uint16_t cmd = pci_config_read16(dev->bus, dev->device, dev->function, 0x04);
uint16_t cmd_saved = cmd;
cmd &= ~(1u << 0); /* I/O Space */
cmd &= ~(1u << 1); /* Memory Space */
pci_config_write32(dev->bus, dev->device, dev->function, 0x04, (uint32_t)cmd);
for (int i = 0; i < 6; ++i)
{
dev->bar_addr[i] = 0;
dev->bar_size[i] = 0;
dev->bar_is_io[i] = 0;
uint8_t off = 0x10 + i * 4;
uint32_t orig = pci_config_read32(dev->bus, dev->device, dev->function, off);
if (orig == 0)
{
continue;
}
/* Определить тип */
if (orig & 0x1)
{
/* I/O Space */
dev->bar_is_io[i] = 1;
pci_config_write32(dev->bus, dev->device, dev->function, off, 0xFFFFFFFF);
uint32_t val = pci_config_read32(dev->bus, dev->device, dev->function, off);
uint32_t mask = val & 0xFFFFFFFC;
uint32_t size = (~mask) + 1;
if (size == 0)
{
size = 0;
}
dev->bar_size[i] = (uint64_t)size;
dev->bar_addr[i] = (uint64_t)(orig & 0xFFFFFFFC);
pci_config_write32(dev->bus, dev->device, dev->function, off, orig);
}
else
{
/* Memory Space */
uint32_t type = (orig >> 1) & 0x3;
if (type == 0x2)
{
/* 64-bit BAR: consumes two entries */
uint32_t orig_hi = pci_config_read32(dev->bus, dev->device, dev->function, off + 4);
uint64_t full_orig = ((uint64_t)orig_hi << 32) | (orig & 0xFFFFFFF0);
/* write all ones to both */
pci_config_write32(dev->bus, dev->device, dev->function, off, 0xFFFFFFFF);
pci_config_write32(dev->bus, dev->device, dev->function, off + 4, 0xFFFFFFFF);
uint32_t val_lo = pci_config_read32(dev->bus, dev->device, dev->function, off) & 0xFFFFFFF0;
uint32_t val_hi = pci_config_read32(dev->bus, dev->device, dev->function, off + 4);
uint64_t mask = ((uint64_t)val_hi << 32) | val_lo;
uint64_t size = (~mask) + 1;
dev->bar_size[i] = size;
dev->bar_addr[i] = full_orig;
/* restore original low/high */
pci_config_write32(dev->bus, dev->device, dev->function, off, (uint32_t)(full_orig & 0xFFFFFFFF));
pci_config_write32(dev->bus, dev->device, dev->function, off + 4, (uint32_t)(full_orig >> 32));
/* отметим следующий BAR как пропущенный (занят 64-bit) */
++i;
}
else
{
/* 32-bit memory BAR */
pci_config_write32(dev->bus, dev->device, dev->function, off, 0xFFFFFFFF);
uint32_t val = pci_config_read32(dev->bus, dev->device, dev->function, off);
uint32_t mask = val & 0xFFFFFFF0;
uint32_t size = (~mask) + 1;
dev->bar_size[i] = (uint64_t)size;
dev->bar_addr[i] = (uint64_t)(orig & 0xFFFFFFF0);
/* restore */
pci_config_write32(dev->bus, dev->device, dev->function, off, orig);
}
}
}
/* восстановим Command Register */
pci_config_write32(dev->bus, dev->device, dev->function, 0x04, (uint32_t)cmd_saved);
}
/* Обрабатываем одну функцию устройства */
static void pci_check_function(uint8_t bus, uint8_t device, uint8_t function)
{
uint16_t vendor = pci_config_read16(bus, device, function, 0x00);
if (vendor == 0xFFFF)
return;
pci_device_t dev;
memset(&dev, 0, sizeof(dev));
dev.bus = bus;
dev.device = device;
dev.function = function;
dev.vendor_id = vendor;
dev.device_id = pci_config_read16(bus, device, function, 0x02);
uint32_t reg = pci_config_read32(bus, device, function, 0x08);
dev.class_code = (reg >> 24) & 0xFF;
dev.subclass = (reg >> 16) & 0xFF;
dev.prog_if = (reg >> 8) & 0xFF;
dev.header_type = pci_config_read8(bus, device, function, 0x0C);
/* Probe BARs */
pci_probe_bars(&dev);
/* Зарегистрируем устройство */
pci_register_device(&dev);
/* Если это PCI-to-PCI bridge (base class 6, subclass 4) - рекурсивно сканируем secondary bus */
if (dev.class_code == 0x06 && dev.subclass == 0x04)
{
uint8_t secondary = pci_config_read8(bus, device, function, 0x19);
if (secondary != 0)
{
/* рекурсивный проход */
/* простая защита от глубокой рекурсии: если secondary == bus - пропускаем */
if (secondary != bus)
{
extern void pci_scan_bus(uint8_t bus);
pci_scan_bus(secondary);
}
}
}
}
/* Обрабатываем устройство (включая мульти-Функции) */
static void pci_check_device(uint8_t bus, uint8_t device)
{
uint16_t vendor = pci_config_read16(bus, device, 0, 0x00);
if (vendor == 0xFFFF)
return;
/* Проверим header type для мультифункции */
uint8_t header = pci_config_read8(bus, device, 0, 0x0C);
pci_check_function(bus, device, 0);
if (header & 0x80)
{
for (uint8_t f = 1; f < 8; ++f)
{
if (pci_config_read16(bus, device, f, 0x00) != 0xFFFF)
{
pci_check_function(bus, device, f);
}
}
}
}
/* Сканирование одной шины: 32 устройства */
void pci_scan_bus(uint8_t bus)
{
for (uint8_t dev = 0; dev < 32; ++dev)
{
pci_check_device(bus, dev);
}
}
void pci_init(void)
{
/* инициализация контейнера */
if (!g_pci_devices)
{
ensure_capacity();
}
/* Проверка: если устройство 0:0:0 мультифункциональное, возможно несколько host controllers */
uint8_t header = pci_config_read8(0, 0, 0, 0x0C);
if ((header & 0x80) == 0)
{
/* single host controller */
pci_scan_bus(0);
}
else
{
/* multi host controllers: функция i управляет шинами i */
for (uint8_t f = 0; f < 8; ++f)
{
if (pci_config_read16(0, 0, f, 0x00) == 0xFFFF)
break;
pci_scan_bus(f);
}
}
}
int pci_get_device_count(void)
{
return g_pci_dev_count;
}
pci_device_t *pci_get_device(int idx)
{
if (idx < 0 || idx >= g_pci_dev_count)
return NULL;
return &g_pci_devices[idx];
}
Таким образом, ядро получает поддержку внешних PCI-устройств, с которыми оно может взаимодействовать.
Реализация переключателя Kernel mode / User mode
Переключатель Kernel Mode / User Mode - это аппаратный механизм процессора, позволяющий операционной системе переводить процессор между привилегированным режимом ядра и непривилегированным пользовательским режимом для защиты системных ресурсов от несанкционированного доступа приложений.
Kernel Mode - это привилегированный режим работы процессора, в котором исполняется код операционной системы (ядро). В этом режиме:
Полный доступ ко всем ресурсам компьютера (память, диски, порты)
Возможность выполнять привилегированные инструкции процессора
Прямая работа с железом (драйверы, прерывания)
Ошибка в коде ядра может привести к краху всей системы
User Mode - это непривилегированный режим, в котором исполняются приложения пользователей. В этом режиме:
Нет прямого доступа к системным ресурсам
Нельзя выполнять привилегированные инструкции
Изолированы друг от друга - ошибка в приложении не влияет на систему
Вызовы ядра выполняются через системные вызовы (syscalls)
Kernel Mode (DPL=0, Code ring 0x08) → User Mode (DPL=3, Code ring 0x1B) переключаются через GDT дескрипторы и стек в памяти.
Создание User-задачи
utask_create() вызвана
↓
malloc(task_t) + malloc(kstack)
↓
prepare_initial_stack(..., user_mode=1)
↓
На стеке ядра готовится фрейм:
┌─────────────────┐
│ SS (USER) │ ← 0x23 (Ring 3)
│ RSP (user_mem) │
│ RFLAGS (IF=1) │
│ CS (USER) │ ← 0x1B (Ring 3)
│ RIP (entry) │
│ ... regs ... │
│ int_no = 32 │
└─────────────────┘
↓
add_to_ring() → задача в очередь READY
Что происходит:
Когда позже выполнится
IRET, CPU видит Ring 3 в CS/SSCPU автоматически переключается в user mode (более низкий привилегия)
Стек переходит с kernel stack на user stack
Переход User → Kernel (при прерывании)
User-задача выполняется (ring 3)
↓
Прерывание: Timer / Syscall / Exception
↓
CPU (автоматически!) проверяет GDT текущего CS:
"Прерывание в ring 3 → нужно switch в ring 0"
↓
CPU берёт RSP0 из TSS → это kernel stack
↓
CPU сохраняет на kernel stack:
┌──────────────┐
│ SS (0x23) │ ← старый user stack selector
│ RSP (user) │ ← старый user stack pointer
│ RFLAGS │
│ CS (0x1B) │ ← откуда пришли
│ RIP (адрес) │
│ [ERR_CODE] │ ← опционально
└──────────────┘
↓
Переходим в kernel mode (ring 0, SS=0x10, RSP=kernel_stack)
↓
ISR обработчик выполняется в kernel
Возврат Kernel → User (IRET)
Syscall / Exception обработана в kernel
↓
schedule_from_isr() выбрал user задачу
↓
ISR выполняет: IRET (pop из kernel stack)
↓
IRET pop:
┌────────────────┐
│ RIP (entry) │ → register
│ CS (0x1B) │ → register ← RING 3!
│ RFLAGS │ → register
│ RSP (user) │ → register
│ SS (0x23) │ → register
└────────────────┘
↓
CPU видит CS=0x1B (ring 3)
↓
CPU переключает режим: ring 0 → ring 3
↓
CPU переключает stack: kernel → user_mem
↓
Выполняется user-код с ring 3 привилегиями
Ключевым компонентом при реализации пользовательского режима (User Mode) является таблица дескрипторов.
; kernel.asm
; -----------------------------------------------------------------------
; GDT с поддержкой Ring 3 и TSS
; -----------------------------------------------------------------------
align 8
gdt:
dq 0x0000000000000000 ; Null descriptor
dq 0x00AF9A000000FFFF ; 0x08: Kernel Code (L=1, DPL=0)
dq 0x00AF92000000FFFF ; 0x10: Kernel Data (L=1, DPL=0)
dq 0x00AFFA000000FFFF ; 0x18: User Code (L=1, DPL=3)
dq 0x00AFF2000000FFFF ; 0x20: User Data (L=1, DPL=3)
; TSS descriptor (64-bit требует 2 записи)
dq 0x0000000000000000 ; 0x28: TSS lower 8 bytes (будет заполнен далее)
dq 0x0000000000000000 ; 0x30: TSS upper 8 bytes
gdt_end:
gdt_desc:
dw gdt_end - gdt - 1
dq gdt
Фрагменты кода реализации поддержки переключения режимов.
// multitask/multitask.c
#define USER_CS ((uint64_t)0x18 | 3) /* 0x1B */
#define USER_SS ((uint64_t)0x20 | 3) /* 0x23 */
static uint64_t *prepare_initial_stack(void (*entry)(void),
void *kstack_top,
void *user_stack_top,
int argc,
uintptr_t argv_ptr,
int user_mode)
{
const int FRAME_QWORDS = 22;
uint64_t *sp = (uint64_t *)kstack_top;
sp = (uint64_t *)(((uintptr_t)sp) & ~0xFULL); /* align down 16 */
sp -= FRAME_QWORDS;
sp[0] = 32; /* int_no (dummy) */
sp[1] = 0; /* err_code */
sp[2] = 0; /* r15 */
sp[3] = 0; /* r14 */
sp[4] = 0; /* r13 */
sp[5] = 0; /* r12 */
sp[6] = 0; /* r11 */
sp[7] = 0; /* r10 */
sp[8] = 0; /* r9 */
sp[9] = 0; /* r8 */
sp[10] = (uint64_t)argc; /* rdi */
sp[11] = (uint64_t)argv_ptr; /* rsi */
sp[12] = 0; /* rbp */
sp[13] = 0; /* rbx */
sp[14] = 0; /* rdx */
sp[15] = 0; /* rcx */
sp[16] = 0; /* rax */
sp[17] = (uint64_t)entry; /* rip */
sp[19] = 0x202; /* rflags (IF=1) */
if (user_mode)
{
sp[18] = USER_CS;
sp[20] = (uint64_t)user_stack_top;
sp[21] = USER_SS;
}
else
{
sp[18] = 0x08;
sp[20] = (uint64_t)kstack_top;
sp[21] = 0x10;
}
return sp;
}
// tss/tss.c
extern tss_t tss_buffer;
extern uint64_t gdt[];
extern uint64_t stack64_top;
static tss_t *tss = NULL;
void tss_init(void)
{
tss = &tss_buffer;
/* Очищаем TSS */
for (int i = 0; i < sizeof(tss_t); i++)
((uint8_t *)tss)[i] = 0;
/* Инициализируем rsp0 с начального kernel stack */
tss->rsp0 = (uint64_t)&stack64_top;
tss->iopb_offset = sizeof(tss_t);
/* Формируем 64-bit TSS descriptor согласно Intel manual */
uint64_t tss_addr = (uint64_t)tss;
uint16_t tss_limit = sizeof(tss_t) - 1;
/* Lower 8 bytes: limit(16) | base_low(24) | access(8) | flags(4) | base_mid(16) */
uint64_t lower = 0;
lower |= ((uint64_t)tss_limit & 0xFFFF); /* bits 0-15: limit */
lower |= (((tss_addr) & 0xFFFFFF) << 16); /* bits 16-39: base_low */
lower |= (0x89ULL << 40); /* bits 40-47: access (P=1, DPL=0, Type=9) */
lower |= ((((tss_addr) >> 24) & 0xFFFF) << 48); /* bits 48-63: base_mid */
/* Upper 8 bytes: base_high(32) | reserved(32) */
uint64_t upper = (tss_addr >> 40) & 0xFFFFFFFF;
gdt[5] = lower;
gdt[6] = upper;
/* Загружаем TSS */
asm volatile("mov $0x28, %%ax; ltr %%ax" ::: "ax");
}
void tss_update_rsp0(uint64_t rsp0)
{
if (tss)
tss->rsp0 = rsp0;
}
Таким образом ядро становится безопаснее, так как отделяет опасный функционал требующийся для работы ядра, от приложений для которых этот функционал лишний.
Итог:
В этой статье мы успешно продолжили разработку ядра операционной системы на C, собрав ключевые компоненты в работающий фундамент. Мы реализовали:
Запуск в Long Mode для работы в современном 64-битном пространстве.
Поддержку Multiboot для стандартной загрузки и получения информации о системе.
Графический режим, обеспечив базовый вывод на экран.
Драйвер ATA (IDE) для доступа к дисковым накопителям.
Драйвер PCI для обнаружения и конфигурации оборудования.
Переключатель привилегий (Kernel/User Mode), заложив основу безопасности и изоляции.
Эти компоненты образуют минимальную, но функциональную основу, способную управлять памятью, железом и выполняемым кодом.
Полный исходный код проекта, а также инструкция по сборке и запуску доступны здесь: GitHub
Спасибо за прочтение статьи!
Надеюсь, она была интересна для вас, и вы узнали что-то новое.