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

Поговорим с вами о том как:
Запуск ядра в Long mode
Работа с Multiboot таблицами
Реализация графического режима
Реализация драйвера ATA (IDE)
Реализация драйвера PCI
Реализация переключателя Kernel mode / User mode
Если вы ещё не читали первую часть статьи, где мы начинали создание собственного ядра на C - настоятельно рекомендую начать с неё:👉 Создание своего ядра на C
Запуск ядра в 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
Спасибо за прочтение статьи!
Надеюсь, она была интересна для вас, и вы узнали что-то новое.
