Перейти к содержимому

Как написать ядро операционной системы

  • автор:

Написание собственной работоспособной ОС за полгода

image

Здравствуйте! Всех категорически приветствую, сегодня хотел бы рассказать Вам о своём опыте написание работоспособной ОС под архитектуру x86.

Как-то весенней ночью у меня родилась гениальная идея — попробовать себя в написании собственной ОС, которая может позволить запускать программы, работать с устройствами, да и в общем выжимать всю мощь из Intel’овской архитектуры в своих нуждах: к примеру, для своей фабрики или чего-либо иного. Моей целью было и есть написание такой ОС, которая могла бы позволить максимальную производительность для каких-то конкретных задач, не тратя процессорное время на всяческие излишества. В основном я преследую лишь спортивный интерес, получение опыта для себя в системном программировании и написания драйверов для устройств, которые используются повсеместно. Что из этого вышло — решать вам, сразу говорю, что не надо писать комментарии про создание собственного дистрибутива линукса, и преследовал интерес написать всё «From scratch» — с нуля, дабы хорошо погрузиться в тему ОСдева. Сразу хочу выразить огромную благодарность Бенджамину Лунту и форуму OSDev, так же как их Вики. Бен помог мне разобраться с EHCI, что несомненно внесло огромный вклад в мою ОС — USB устройства, они везде! Так же передо мной стояла задача создать собственную архитектуру, удобную мне, не исключая использование стандартов ELF-файлов. Что же, давайте перейдем к сути.
UPD: всю инфу можно найти в группе тык, так же там есть пост с доками и образом(старым, сейчас дописываю доки для stable-версии)

Что сделано?

Сейчас моя ОС умеет работать с USB-флешками, мышками, клавиатурами, AHCI дисками, PCI IDE контроллером, APIC’ом и ACPI, реализована вытесняющая многозадачность, реализован запуск программ, поточная работа с файлами, SVGA драйвер, который работает на 0x118 режиме VBE, работают DNS, DHCP, TCP, UPD, IPv4, HTTP, полный драйвер FAT32, драйвер RTL8139(69) и Intel Gigabit Ethernet.

Оконная система вместе с моей реализацией SVGA позволяет выдать аж 120 FPS при полной перерисовке экрана. Давайте перейдем к тому, как это всё реализовано и вообще может работать.

Как это работает?

Для начала, я написал бутлоадер, который с FAT32 читает вторичный загрузчик с ядром. Второй загрузчик переходит в защищенный режим и прыгает в ядро, там я загружаю и настраиваю IDT, после чего инициализирую PCI-устройства, запускаю ядра и запускаю CMD.

Кто-то спросит, как же ты добился такого перфоманса с SVGA? Ответ прост: ассемблер, ассемблер и еще раз ассемблер. Не обошлось без SSE инструкций, которые очень ускоряют копирование памяти. К примеру, вот код для копирования видеобуфера в LFB(Linear Frame Buffer):

.byte 0x60#Save registers in stack mov %2,%%ecx #Repeat count to ecx mov %0,%%edi #Video memory start to edi mov %1,%%esi #Video buffer start to esi ww1sse2: movaps (%%esi),%%xmm0 #Copy 16 bytes to xmm0 from buffer movaps %%xmm0,(%%edi) #Copy from xmm0 to video memory movaps 16(%%esi),%%xmm0 #16 again, but + 16 from current movaps %%xmm0,16(%%edi) #16 again, but + 16 from current movaps 32(%%esi),%%xmm0 #16 again, but + 32 from current movaps %%xmm0,32(%%edi) #16 again, but + 32 from current movaps 48(%%esi),%%xmm0 #16 again, but + 48 from current movaps %%xmm0,48(%%edi) #16 again, but + 48 from current add $64,%%edi #Add 64 bytes to edi add $64,%%esi #Add 64 bytes to esi dec%%ecx#Decrement count #test %%ecx,%%ecx #Compare ecx with zero jnz ww1sse2 #If not zero, repeat again .byte 0x61 #Restore registers from stack 

Оконная система построена на ООП и, думаю, не нуждается в комментариях, всё почти как в шинде.

Менеджер памяти простейший — «Watermark Allocator». Распределение ресурсов осуществляется за счет того, что в ядре нет функций, которые могли бы нагадить друг другу, всё запросы сделаны через очереди и т.п.

Пока что нет никаких потоков ввода-вывода, но в ближайшее время они будут реализованы.
Система логических дисков аналогична MS-DOSу: одна буква — один диск. Поддерживаются как MBR разделы, так и GPT разделы.

Разработка драйверов устройств

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

Если честно, то я сторонник того, что главное в программе — функционал, но в тоже время считаю, что за графику можно и функционалом немного пожертвовать: к примеру, VIM.

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

Отладка

Отладка ОС занимает огромную кучу времени: скомпилить сорсы, скопировать на виртуальный диск, сохранить образ диска, проверить на эмуляторе, после чего скопировать файлы на флешку и проверить уже на реальном железе. А главное — никаких отладчиков, выводи в текстовый режим, либо в окошечки — повисло — ставь return’ы.

Итоги

Да, ОС всё еще в стадии разработки, хочется под новый год получить стабильную бета-версию, а может в будущем даже продолжить разработку на более серьезном уровне.

Что хочу сказать напоследок — если тема разработки ОС кого-то заинтересовала, или кто-то хочет узнать аспекты работы с какими-либо устройствами и интерфейсами, хитрости реализации, или просто помочь проекту(не деньгами, а кодом) — можете отписать об этом в комментариях, все пожелания будут учтены, и в деталях и красках будут описаны интересующее вас аспекты.

Ассемблер и ядро Операционной Системы

Ядро в данном случае будет загружаться по адресу со смещением в 1M относительно начала памяти компьютера. Описываются переменные code, data, bss, end. Задается выравнивание всех сегментов на границу страницы памяти (4096 байт). В качестве стартовой берётся функция с именем start. Функция взята из примера: http://www.osdever.net/bkerndev/Docs/basickernel.htm

; This is the kernel's entry point. We could either call main here, ; or we can use this to setup the stack or other nice stuff, like ; perhaps setting up the GDT and segments. Please note that interrupts ; are disabled at this point: More on interrupts later! [BITS 32] global start start: mov esp, _sys_stack ; This points the stack to our new stack area jmp stublet ; This part MUST be 4byte aligned, so we solve that issue using 'ALIGN 4' ALIGN 4 mboot: ; Multiboot macros to make a few lines later more readable MULTIBOOT_PAGE_ALIGN equ 1

Приведенный пример удивляет адресом загрузки. Как в real mode можно загрузить ядро по физическому адресу за пределами первого мегабайта? Ответ: в real mode никак. Пример этот рассчитан на использование GRUB, работающего в защищенном режиме. Подведу итоги про ld. Существует множество версий gcc и binutils. Они поддерживают самые разные форматы объектных файлов. В том числе есть версии binutils скомпилированные под Windows. Это позволяет разрабатывать ядро Операционной системы с использованием любого подходящего для разработчика компилятора. Главное, чтобы в пару к компилятору была соответствующая версия binutils, поддерживающая нужную версию объектных и выполнимых (тоже объектных) файлов. Дополнительные примеры линкерного скрипта можно посмотртеть в статье: Настройка виртуальных адресов внутри ядра UEFI P.S. Есть ещё один аргумент против ассемблера… Как известно, загрузкой операционной системы после включения компьютера занимается BIOS. BIOS — Basic Input Output System. А в целом, API для работы с аппаратурой, представляет из себя набор прерываний, с передачей данные через регистры, а также набор портов ввода-вывода и memory mapped областей памяти. Появилась такая система в момент разработки первых компьютеров IBM/PC еще в 80-е годы прошлого века. Архитектура IBM PC Compatible систем медленно эволюционировала. Появлялись новые системные шины и стандарты: ISA, EISA, PCI, и т.д. При этом BIOS оставался частной собственностью нескольких компаний. BIOS предоставляет интерфейсы программам только в реальном режиме, интерфейсы BIOS устарели, несли бремя совместимости и ограничены в возможностях. Такая ситуация стала тормозить развитие фантазии разработчиков аппаратных средств. При разработке платформы Itanium, Intel выступил с инициативой EFI (Extendible Firmware Interface). Идея EFI заключалась в замене BIOS новой разработкой. EFI, в отличии от BIOS больше не базируется на прерываниях. EFI больше не написан на ассемблере, и предоставляет для использования таблицы функций разного назначения написанных на Cи. Естественно процесс продвижения новой технологии долгий. Прошло несколько лет и после версии EFI 1.10 Intel передал права на дальнейшее развитие нового стандарта ново созданной инициативной группе UEFI, включающей в себя представителей Intel, IBM, Apple, Microsoft и других. Теперь эта технология получила приставку Unified, означающую, что единые переносимые firmware получат как компьютеры на базе Intel PC, так и планшетники на базе ARM. UEFI, по сравнению с BIOS несомненно прогрессивен. Он предоставляет графические драйвера, сетевые драйвера и т.д. Компьютер без операционной системы будет способен осуществлять в том числе сетевые подключения… Однако есть у этого всего и минусы. Одной из технологий, поддерживаемой UEFI является Safe boot. Запрет загрузки не подписанного софта. Это означает, что возможно создание железа, на котором будет загружаться лишь одна разрешенная (подписанная) операционная система. Самым недавним ударом по BIOS стало требование Microsoft во всех системах, сертифицированных для работы с Windows 8 наличие UEFI safe boot. Что из вышесказанного следует? Из вышесказанного следует, что платформе Intel PC в том её виде, в котором она существует сегодня, жить осталось недолго. Выход Windows 8 не за горами. Второй вывод — ассемблер перестает быть инструментом, даже при написании операционных систем. С появлением новых интерфейсов, необходимость в ассемблере, судя по всему уменьшится. Третий вывод — загрузка операционной системы, да и архитектура драйверов в ближайшее время переживет революционные изменения. Продолжение читайте здесь…

Все операционные системы написаны на С. Не пора ли переписать их на Rust?

Системный программист рассказывает, почему операционную систему нельзя написать на Python или Java, но можно на Rust.

Кадр: сериал «Мистер Робот»

Марина Демидова

Марина Демидова

Программист, консультант, специалист по документированию. Легко и доступно рассказывает о сложных вещах в программировании и дизайне.

Брайан Кантрилл

об авторе

Системный программист с двадцатилетним стажем, сооснователь и технический директор Oxide Computer Company. С 1996 по 2006 год разрабатывал ядро ОС Solaris в Sun Microsystems, а с 2010 по 2019 год — SmartOS в компании Joyent.

В докладе на конференции QCon Брайан Кантрилл объясняет, почему многим системным программистам нравится Rust и почему на этом языке будут писать современные операционные системы.

В статье мы пересказали основные идеи доклада.

Если я спрошу, не пора ли переписать все операционные системы на Rust, по закону заголовков Беттериджа вы можете сказать «нет». Но я приведу много доводов, почему стоит ответить «да». Ещё я расскажу, зачем ставлю в названии статьи вопросительный, а не восклицательный знак.

Что такое операционная система

На первый взгляд это простой вопрос, но начнём с определения программного обеспечения. Считается ли им, например, алгоритм Евклида ? Нет, потому что он появился раньше компьютеров.

То же и с операционной системой. ОС — это то, что работает поверх железа. Но не только физического, ведь её можно запустить в облаке на виртуальном оборудовании. Получается, что и здесь всё сложно.

Если говорить проще, операционная система — программа, которая позволяет запускать на железе стороннее ПО. Если ОС перестаёт работать, все остальные процессы завершаются вместе с ней.

У операционной системы есть ядро — часть с полным доступом к ресурсам процессора, памяти и другого оборудования. Но кроме ядра в ОС ещё есть библиотеки, команды и демоны .

Как развивались операционные системы

Подобия ОС писали на ассемблере уже после Второй мировой. Но полноценная операционная система появилась в 1961 году — это была MCP , написанная на языке ESPOL . Её создали для компьютера Burroughs B5000, который опередил своё время, но был коммерчески бесполезен.

В 1964 году в MIT попытались развить идею и начали разрабатывать CTSS — мультизадачную операционную систему, которая умела распределять аппаратные ресурсы между разными программами и пользователями. Её назвали Multics.

Создатели Multics хотели сделать всё правильно, но случилось то, что я называю синдромом второй системы. Они решили переписать с нуля почти все компоненты, поэтому проект получился огромный, а сроки сдачи переносились всё дальше.

ОС писали на языке PL/I , а его компилятор отдали разрабатывать аутсорсинговой компании, но у той ничего не вышло. Чтобы спасти положение, команда учёных создала язык EPL и продолжила работу на нём.

В 1969 году Bell Labs, одна из участниц программы, посчитала проект бесперспективным и вышла из него. А сотрудник Bell Labs Кен Томпсон сделал на его базе UNIX — операционную систему для мини-компьютера PDP-7.

UNIX и языки высокого уровня

Некоторые думают, что UNIX написали на С, но это не так — её полностью разработали на ассемблере. В отличие от авторов Multics, у разработчиков UNIX не было синдрома второй системы. Её создатели сначала делали работающий компонент, а потом писали к нему документацию.

В том же 1969 году в Bell Labs создали язык B. На нём написали некоторые утилиты UNIX, причём и тут не обошлось без ассемблера.

Но разработчиков UNIX не устраивала производительность языка B. Да, они считали, что операционную систему можно сделать на языке высокого уровня , но B для этого не годился — в нём не хватало байтовой адресации .

Тогда придумали язык С. В первой версии в нём не было структур — они появились только к 1973 году (понадобились для ядра UNIX). По той же причине добавили и битовые поля .

Язык дорабатывали постепенно — как раз в этом и его сила, и его слабость. Меня бесит, что в нём нет логического XOR — один из создателей языка объяснял это тем, что XOR не подходит для вычислений по короткой схеме .

Но С всё равно развивался. В нём появились макросы — а это одна из вещей, которые мне очень нравятся и в Rust. То же и с препроцессорами — многие не любят их использовать, но они важны для DTrace , ZFS и других систем.

Операционные системы в 1980–1990-е годы

В 1980-е годы С доминировал. На нём были написаны почти все операционные системы. Но в 1990-е стало популярным объектно-ориентированное программирование, и ОС на C объявили устаревшими. Считали, что скоро их заменят ОС на Java и C++. Но эти попытки провалились, и одна из причин — синдром второй системы.

Я помню 1990-е годы, когда гендиректором Apple был Джил Амелио. Компания громко рекламировала Mac OS Copland, но никак не могла её выпустить. Остальные же компании из Кремниевой долины ждали, пока Apple разорится, чтобы по дешёвке выкупить её имущество.

А Sun Microsystems разрабатывала операционную систему Spring — на C++. Ещё в студенчестве мне в руки попал CD-диск со Spring: я попробовал её установить, но компьютер не запустился. Оказалось, что у меня только 32 МБ оперативной памяти, а система требовала минимум 128 МБ. В наше время это всё равно что 2 ТБ RAM.

Не получилось сделать и ОС на Java. В этом языке нет беззнакового типа данных , без которого системе трудно взаимодействовать с железом.

Операционные системы в 2000-х

Когда появился Linux, написание систем на языке C стало мейнстримом. Много кто пробовал создать ядро ОС на C++, но из этих попыток ничего не вышло. Хотя есть исключения — та же Haiku. С одной оговоркой — всё-таки это был любительский проект, ничего серьёзного.

В то же время, в начале 2000-х, главными языками были Java и C++. А потом заметными стали Ruby, Python и JavaScript. Но все они фокусируются на увеличении скорости разработки. Никто в здравом уме не пробовал создать ОС на Ruby или Python.

JavaScript сначала был не слишком популярным. Но им стали активно пользоваться в 2006 году, когда все поняли, что в браузерах появился полноценный язык, на котором можно создавать веб-приложения. Для меня JavaScript — это замаскированный Lisp в браузере, но без многочисленных скобок.

К 2010-м годам системные программисты хотели получить язык, который был бы таким же производительным, как C, но в то же время обладал бы преимуществами других языков — например, методом split () и регулярными выражениями .

Из этого вышло много интересного. Например, появились мощные среды выполнения JavaScript, в частности движок V8, который можно было запускать на сервере. Потом появился Node.js. Мне очень нравится JS, но в мире системного программного обеспечения с ним было бы чересчур много проблем.

Некоторые сотрудники Bell Labs перешли в Google и разработали там язык Go. Он лишён многих недостатков JavaScript, но у него есть некоторые особенности.

В своём выступлении разработчик из Google Адин Сканнелл рассказал, что на Go можно создать ядро операционной системы, но у Go и JavaScript есть общая проблема — сборщики мусора . Они только замедляют системные программы, а потому не подходят для создания операционных систем.

Что особенного в языке Rust

Я написал на C++ сто тысяч строк кода и больше никогда не хочу к нему возвращаться. Меня всё сильнее и сильнее интересует Rust, потому что мне нравятся его ценности — это быстрый и безопасный язык для системного ПО.

Одна из самых интересных особенностей Rust — понятие владения. Язык статически определяет, кому что принадлежит. Компилятор выделяет вам память, а затем освобождает её, когда вы закончите с ней работать. А если он не знает, кому принадлежит память, то он её одалживает. Это называется заимствованием. Такой процесс исключает утечку ресурсов и обеспечивает безопасность — одна программа не может перезаписать память другой. И это действительно важно.

В Rust есть и другие функции, которые мне нравятся:

Алгебраические типы. Позволяют превосходно обрабатывать ошибки, работают примерно как объединение в C.

Гигиенические макросы. У меня их не было в C, и я буквально не знал, как пишется слово «гигиенический». Но такой макрос, который можно получить в выводе абстрактного синтаксического дерева вместе с другим программным кодом, — это удивительно.

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

Ключевое слово unsafe. Его используют, чтобы выключить безопасный доступ к памяти и выполнять операции, которые компилятор иначе бы не разрешил.

Можно ли написать операционную систему на Rust

Я думаю, что на Rust можно смело писать операционные системы. Подобный язык мы ждали последние 30 лет.

Первая попытка, о которой я знаю, — система Weenix. Её создал студент Захари Эспириту для своей дипломной работы. Он подробно описывает проблемы, с которыми столкнулся в процессе.

С 2015 года небольшие операционные системы на Rust появлялись одна за другой: вышли Redox, Tock, IntermezzOS, BlogOS, QuiltOS, Rux и другие. IntermezzOS и BlogOS — учебные проекты, а Tock создана для интернета вещей.

К сожалению, у этих ОС нет бинарной совместимости с Linux, а значит, на них не запускается большая часть линуксовых программ. Поэтому я думаю, что гибридные подходы — самый правильный способ написать ОС на Rust:

ПодходВ чём его смысл
Добавлять в ядра на С новые компоненты на RustМне нравится, что С и Rust умеют взаимодействовать друг с другом. Можно сохранить ядро, написанное на C или ассемблере, а на Rust разрабатывать драйверы, файловые системы и прошивки
Писать на Rust компоненты операционной системыВ ОС входит не только ядро, но и утилиты, демоны, диспетчер служб, диспетчер устройств, управление сбоями и отладчики. Их можно с успехом переписать на Rust
Переписать прошивку с DOS на RustМного критически важного системного ПО работает под управлением DOS. Большинство проблем этой прошивки можно решить, если переписать её на Rust. Тот, кто это сделает, окажет человечеству огромную услугу

Почему я жду появления гибридных операционных систем на С и Rust

Я очень оптимистично отношусь к Rust. Думаю, мы можем использовать его в операционных системах: в прошивке, в расширениях для ядра, при создании компонентов ОС, хотя на практике наверняка будет много трудностей.

Rust совместим с системами на C, и в этом его красота. Такой язык открывает новые возможности.

Читайте также:

  • Facebook* заржавел: зачем корпорации поддерживать язык Rust
  • Задача про трёх тестировщиков на собеседовании
  • Must read. 7 свежих англоязычных статей о FreeBSD, NetBSD и OpenBSD

Закон заголовков Беттериджа говорит, что на любой заголовок с вопросительным знаком можно ответить «нет».

Алгоритм Евклида помогает находить наибольший целый делитель двух целых чисел.

Демон (daemon) — фоновая программа в UNIX-системе. Она умеет выполнять только одну задачу. В Windows аналог демонов — службы.

MCP (Master Control Program) — операционная система, которую разработала компания Burroughs Corporation.

ESPOL (Executive Systems Problem Oriented Language) — язык, производный от ALGOL 60.

PL/I (Programming Language I) — язык программирования, который создали в 1964 году для научных, инженерных и бизнес-вычислений.

EPL стал упрощённым диалектом PL/I.

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

При байтовой адресации компьютер может обращаться и изменять в памяти отдельные байты, а не только их группы (слова).

Структура — объединение нескольких объектов под одним именем. Объектами могут быть переменные, массивы, указатели и так далее.

Битовые поля (bit fields) позволяют хранить целые числа, используя меньший объём памяти.

XOR — исключающее «или».

При вычислениях по короткой схеме второй логический оператор не вычисляется, если сработал первый. Например, если левый операнд AND выдал значение false, общее значение выражения также будет false. Второй оператор проверяться не будет.

DTrace — фреймворк компании Sun Microsystems, который используют для отладки приложений.

ZFS (Zettabyte File System) — файловая система, которую создали в Sun Microsystems.

Беззнаковые числа (тип unsigned) всегда положительны и не могут принимать значение меньше нуля.

Метод split в разных языках делит строку на несколько строк по заданному разделителю.

Регулярные выражения позволяют проводить массовые операции со строками в тексте.

В языках вроде Go, JavaScript, C# и Java есть сборщики мусора — специальные процессы, которые сканируют память и удаляют те ячейки, которые уже не нужны приложению.

Системный программист разрабатывает операционные системы, утилиты, драйверы и другие похожие программы.

Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро

Обложка поста Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро

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

Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро 1

Как загружается x86-система?

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

В большей части регистров процессора при запуске уже находятся определённые значения. Регистр, указывающий на адрес инструкций (Instruction Pointer, EIP), хранит в себе адрес памяти, по которому лежит исполняемая процессором инструкция. EIP по умолчанию равен 0xFFFFFFF0. Таким образом, x86-процессоры на аппаратном уровне начинают работу с адреса 0xFFFFFFF0. На самом деле это — последние 16 байт 32-битного адресного пространства. Этот адрес называется вектором перезагрузки (reset vector).

Теперь карта памяти чипсета гарантирует, что 0xFFFFFFF0 принадлежит определённой части BIOS, не RAM. В это время BIOS копирует себя в RAM для более быстрого доступа. Адрес 0xFFFFFFF0 будет содержать лишь инструкцию перехода на адрес в памяти, где хранится копия BIOS.

Так начинается исполнение кода BIOS. Сперва BIOS ищет устройство, с которого можно загрузиться, в предустановленном порядке. Ищется магическое число, определяющее, является ли устройство загрузочным (511-ый и 512-ый байты первого сектора должны равняться 0xAA55).

Когда BIOS находит загрузочное устройство, она копирует содержимое первого сектора устройства в RAM, начиная с физического адреса 0x7c00; затем переходит на адрес и исполняет загруженный код. Этот код называется бутлоадером.

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

Все x86-процессоры начинают работу в простом 16-битном режиме, называющимся реальным режимом. Бутлоадер GRUB переключает режим в 32-битный защищённый режим, устанавливая нижний бит регистра CR0 в 1. Таким образом, ядро загружается в 32-битном защищённом режиме.

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

Что нам нужно?

  • x86-компьютер;
  • Linux;
  • ассемблер NASM;
  • gcc;
  • ld (GNU Linker);
  • grub;

Исходники можно найти на GitHub.

Задаём точку входа на ассемблере

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

Как же нам сделать так, чтобы этот код обязательно был именно исходной точкой?

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

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

;;kernel.asm bits 32 ;nasm directive - 32 bit section .text global start extern kmain ;kmain is defined in the c file start: cli ;block interrupts mov esp, stack_space ;set stack pointer call kmain hlt ;halt the CPU section .bss resb 8192 ;8KB for stack stack_space: 

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

Со второй строки начинается секция с кодом.

global — это ещё одна директива NASM, делающая символы исходного кода глобальными. Таким образом, линковщик знает, где находится символ start — наша точка входа.

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

Затем идёт функция start , вызывающая функцию kmain и останавливающая процессор инструкцией hlt . Именно поэтому мы заранее отключаем прерывания инструкцией cli .

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

Ядро на Си

В kernel.asm мы совершили вызов функции kmain() . Таким образом, наш “сишный” код должен начать исполнение с kmain() :

/* * kernel.c */ void kmain(void) < const char *str = "my first kernel"; char *vidptr = (char*)0xb8000; //video mem begins here. unsigned int i = 0; unsigned int j = 0; /* this loops clears the screen * there are 25 lines each of 80 columns; each element takes 2 bytes */ while(j < 80 * 25 * 2) < /* blank character */ vidptr[j] = ' '; /* attribute-byte - light grey on black screen */ vidptr[j+1] = 0x07; j = j + 2; >j = 0; /* this loop writes the string to video memory */ while(str[j] != '\0') < /* the character's ascii */ vidptr[i] = str[j]; /* attribute-byte: give character black bg and light grey fg */ vidptr[i+1] = 0x07; ++j; i = i + 2; >return; > 

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

Сперва мы создаём указатель vidptr , который указывает на адрес 0xb8000. С этого адреса в защищённом режиме начинается “видеопамять”. Для вывода текста на экран мы резервируем 25 строк по 80 ASCII-символов, начиная с 0xb8000.

Каждый символ отображается не привычными 8 битами, а 16. В первом байте хранится сам символ, а во втором — attribute-byte . Он описывает форматирование символа, например, его цвет.

Для вывода символа s зелёного цвета на чёрном фоне мы запишем этот символ в первый байт и значение 0x02 во второй. 0 означает чёрный фон, 2 — зелёный цвет текста.

Вот таблица цветов:

0 - Black, 1 - Blue, 2 - Green, 3 - Cyan, 4 - Red, 5 - Magenta, 6 - Brown, 7 - Light Grey, 8 - Dark Grey, 9 - Light Blue, 10/a - Light Green, 11/b - Light Cyan, 12/c - Light Red, 13/d - Light Magenta, 14/e - Light Brown, 15/f – White. 

В нашем ядре мы будем использовать светло-серый текст на чёрном фоне, поэтому наш байт-атрибут будет иметь значение 0x07.

В первом цикле программа выводит пустой символ по всей зоне 80×25. Это очистит экран. В следующем цикле в “видеопамять” записываются символы из нуль-терминированной строки “my first kernel” с байтом-атрибутом, равным 0x07. Это выведет строку на экран.

Связующая часть

Мы должны собрать kernel.asm в объектный файл, используя NASM; затем при помощи GCC скомпилировать kernel.c в ещё один объектный файл. Затем их нужно присоединить к исполняемому загрузочному ядру.

Для этого мы будем использовать связывающий скрипт, который передаётся ld в качестве аргумента.

/* * link.ld */ OUTPUT_FORMAT(elf32-i386) ENTRY(start) SECTIONS < . = 0x100000; .text : < *(.text) >.data : < *(.data) >.bss : < *(.bss) >> 

Сперва мы зададим формат вывода как 32-битный Executable and Linkable Format (ELF). ELF — это стандарный формат бинарных файлов Unix-систем архитектуры x86. ENTRY принимает один аргумент, определяющий имя символа, являющегося точкой входа. SECTIONS — это самая важная часть. В ней определяется разметка нашего исполняемого файла. Мы определяем, как должны соединяться разные секции и где их разместить.

В скобках после SECTIONS точка (.) отображает счётчик положения, по умолчанию равный 0x0. Его можно изменить, что мы и делаем.

Смотрим на следующую строку: .text : < *(.text) >. Звёздочка (*) — это специальный символ, совпадающий с любым именем файла. Выражение *(.text) означает все секции .text из всех входных файлов.

Таким образом, линковщик соединяет все секции кода объектных файлов в одну секцию исполняемого файла по адресу в счётчике положения (0x100000). После этого значение счётчика станет равным 0x100000 + размер полученной секции.

Аналогично всё происходит и с другим секциями.

Grub и Multiboot

Теперь все файлы готовы к созданию ядра. Но остался ещё один шаг.

Существует стандарт загрузки x86-ядер с использованием бутлоадера, называющийся Multiboot specification. GRUB загрузит наше ядро, только если оно удовлетворяет этим спецификациям.

Следуя им, ядро должно содержать заголовок в своих первых 8 килобайтах. Кроме того, этот заголовок должен содержать 3 поля, являющихся 4 байтами:

  • магическое поле: содержит магическое число 0x1BADB002 для идентификации ядра.
  • поле flags: нам оно не нужно, установим в ноль.
  • поле checksum: если сложить его с предыдущими двумя, должен получиться ноль.

Наш kernel.asm станет таким:

;;kernel.asm ;nasm directive - 32 bit bits 32 section .text ;multiboot spec align 4 dd 0x1BADB002 ;magic dd 0x00 ;flags dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero global start extern kmain ;kmain is defined in the c file start: cli ;block interrupts mov esp, stack_space ;set stack pointer call kmain hlt ;halt the CPU section .bss resb 8192 ;8KB for stack stack_space: 

Строим ядро

Теперь мы создадим объектные файлы из kernel.asm и kernel.c и свяжем их, используя наш скрипт.

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

Эта строка запустит ассемблер для создания объектного файла kasm.o в формате ELF-32.

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

Опция “-c” гарантирует, что после компиляции не произойдёт скрытого линкования.

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

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

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

GRUB требует, чтобы имя ядра удовлетворяло шаблону kernel- . Поэтому переименуйте ядро. Своё я назвал kernel-701.

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

В конфигурационном файле GRUB grub.cfg добавьте следующее:

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

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

Перезагрузите компьютер, и вы увидите список ядер с вашим в том числе. Выберите его, и вы увидите:

Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро 2

Это ваше ядро! В следующей части добавим систему ввода / вывода.

P.S.

  • Для любых фокусов с ядром лучше использовать виртуальную машину.
  • Для запуска ядра в grub2 конфиг должен выглядеть так:menuentry 'kernel 7001'
  • если вы хотите использовать эмулятор qemu , используйте:qemu-system-i386 -kernel kernel

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *