Данная статья служит одной простой цели: помочь человеку, который вдруг решил разработать свою операционную систему (в частности, ядро) для архитектуры x86, выйти на тот этап, где он сможет просто добавлять свой функционал, не беспокоясь о сборке, запуске и прочих слабо относящихся к самой разработке деталей. В интернете и на хабре в частности уже есть материалы по данной теме, но довольно трудно написать хотя бы “Hello world”-ядро, не открывая десятков вкладок, что я и попытаюсь исправить. Примеры кода будут по большей части на языке C, но многие другие языки тоже можно адаптировать для OSDev. Давно желавшим и только что осознавшим желание разработать свою операционную систему с нуля — добро пожаловать под кат.
$ export TARGET=i686-elf
$ export PREFIX=<путь к кросс-компилятору>
$TARGET
— система, под которую будет собирать полученный компилятор. Обычно она называется наподобие i686-linux-gnu
, но здесь результат запускается без ОС, поэтому указывается просто формат исполняемого файла. Почему i686, а не i386? Просто архитектуре 80386 уже, кхм, много лет, и с тех пор многое изменилось; в частности, появились кэши, многоядерные и многопроцессорные системы, встроенные FPU, “большие” атомарные инструкции вроде CMPXCHG
, так что, собирая под i386, можно сильно потерять в быстродействии и немного приобрести в поддержке старых компьютеров.$PREFIX
— то, куда будут установлены инструменты. Обычно используются пути вроде /usr/i686-elf
, /usr/local/i686-elf
и подобные, но можно установить и в произвольную папку. Этот каталог также называется sysroot, поскольку он будет представлять собой корневой каталог для кросс-компилятора и утилит. Говоря точнее, это не полноправный путь, а именно префикс к пути; таким образом, для установки в корень $PREFIX будет представлять из себя пустую строку, а не /
. На время сборки GCC потребуется добавить в PATH
путь $PREFIX/bin
.$ ../binutils-2.29/configure --target=$TARGET --prefix="$PREFIX" --with-sysroot --disable-nls --disable-werror
--with-sysroot
— использовать sysroot;--disable-nls
— выключить поддержку родного языка. OSDev-сообщество не так велико, чтобы на какую-нибудь непонятную ошибку сборки обязательно нашёлся человек, говорящий на языке того, у кого она возникла;--disable-werror
— компилятор при сборке Binutils выдает предупреждения, а с -Werror это приводит к остановке сборки.$ make
$ make install
contrib/download_prerequisites
, который их скачает и использует при сборке. Конфигурацию выполняем так:$ ../gcc-7.2.0/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++ --without-headers
--without-nls
— то же самое, что и для Binutils;--without-headers
— не предполагать, что на целевой системе будет стандартная библиотека (этим, собственно, и отличается необходимый нам компилятор от стандартного);--enable-languages=c,c++
— собрать компиляторы только для выбранных языков. Опционально, но существенно ускоряет сборку.make && make install
не подойдет, поскольку некоторые компоненты GCC ориентируются на готовую операционную систему, поэтому собираем и устанавливаем только необходимое:$ make all-gcc all-target-libgcc
$ make install-gcc install-target-libgcc
$ git clone https://github.com/vertis/objconv.git
$ cd objconv
$ g++ -o objconv -O2 src/*.cpp
$ cd ../grub
$ ./autogen.sh
$ mkdir ../build-grub
$ cd ../build-grub
$ ../grub-2.02/configure --disable-werror TARGET_CC=$TARGET-gcc TARGET_OBJCOPY=$TARGET-objcopy TARGET_STRIP=$TARGET-strip TARGET_NM=$TARGET-nm TARGET_RANLIB=$TARGET-ranlib --target=$TARGET
$ make
$ make install
$ mkdir build-gdb
$ cd build-gdb
$ ../gdb-8.0.1/configure --target=$TARGET --prefix="$PREFIX"
$ make
$ make install
$ dd if=/dev/zero of=disk.img bs=1048576 count=<размер в МБ>
$ fdisk disk.img
Welcome to fdisk (util-linux 2.27.1).
Changes will remain in memory only, until you decide to write them
Be careful before using the write command.
Device does not contain a recognized partition table.
Created a new DOS disklabel with disk identifier 0x########.
Command (m for help): n
Partition type
p primary (0 primary, 0 extended, 4 free)
e extended (container for logical partitions)
Select (default p): <Enter>
Using default response p.
Partition number (1-4, default 1): <Enter>
First sector (2048-N, default 2048): <Enter>
Last sector, +sectors or +size{K,M,G,T,P} (2048-N, default N): <Enter>
Created a new partition 1 of type 'Linux' and of size N MiB.
Command (m for help): t
Selected partition 1
Partition type (type L to list all types): 0B
Changed type of partition 'Linux' to 'W95 FAT32'.
Command (m for help): a
Selected partition 1
The bootable flag on partition 1 is enabled now.
Command (m for help): w
The partition table has been altered.
Syncing disks.
$ losetup disk.img --show -f -o 1048576 # выведет <устройство>
$ mkfs.fat -F 32 <устройство>
$ mount <device> <точка монтирования>
$ mount -o loop,offset=1048576 disk.img <точка монтирования>
$ grub-install --modules="part_msdos biosdisk fat multiboot configfile" --root-directory="<точка монтирования>" ./disk.img
$ sync
$ dd if=/dev/zero of=disk.img bs=1048576 count=<размер в МБ>
$ fdisk -e disk.img
Would you like to initialize the partition table? [y] y
fdisk:*1> edit 1
Partition id ('0' to disable) [0 - FF]: [0] (? for help) 0B
Do you wish to edit in CHS mode? [n] n
Partition offset [0 - n]: [63] 2047
Partition size [1 - n]: [n] <Enter>
fdisk:*1> write
fdisk: 1> quit
$ dd if=disk.img of=mbr.img bs=512 count=2047
$ dd if=disk.img of=fs.img bs=512 skip=2047
$ hdiutil attach -nomount fs.img # выведет <устройство>
$ newfs_msdos -F 32 <устройство>
$ hdiutil detach <устройство>
$ cat mbr.img fs.img > disk.img
$ hdiutil attach disk.img
$ /usr/local/sbin/grub-install --modules="part_msdos biosdisk fat multiboot configfile" --root-directory="<точка монтирования>" ./disk.img
set default=0
set timeout=0
menuentry "BetterThanLinux" {
multiboot /путь/к/ядру/ядро.elf
boot
}
$ nasm -f elf -o file.o file.s
$ i686-elf-gcc -c -ffreestanding -o file.o file.c
$ i686-elf-gcc -T linker.ld -o file.elf -ffreestanding -nostdlib file1.o file2.o -lgcc
-ffreestanding
— генерировать freestanding-код;-nostdlib
— не включать стандартную библиотеку, поскольку ее реализация является hosted-кодом и будет совершенно бесполезна;-lgcc
— подключаем описанную выше libgcc. Ее подключение всегда идет после остальных объектных файлов, иначе компоновщик будет жаловаться на неразрешенные ссылки;-T
— поскольку нужно где-то разместить заголовок Multiboot, обычная раскладка ELF-файла не подойдёт. Ее можно изменить при помощи скрипта компоновщика, который и задает этот флаг. Вот готовый его вариант:/* Исполнение начнется с этой функции */
ENTRY(_start)
/* Как расположить секции в файле */
SECTIONS
{
/* Ядра обычно загружаются по смещению 1Мб. Можно указать любое значение */
. = 1M;
/* Сначала заголовок Multiboot, чтобы его нашел загрузчик, а также исполняемый код */
.text BLOCK(4K) : ALIGN(4K)
{
*(.multiboot)
*(.text)
}
/* Данные (только чтение) */
.rodata BLOCK(4K) : ALIGN(4K)
{
*(.rodata)
}
/* Данные (чтение и запись, проинициализированные) */
.data BLOCK(4K) : ALIGN(4K)
{
*(.data)
}
/* Неинициализированная область (данные для чтения и записи, стек) */
.bss BLOCK(4K) : ALIGN(4K)
{
*(COMMON)
*(.bss)
}
/* Сюда можно добавлять все, что только можно */
}
FLAGS equ 0 ; пока никакие флаги не нужны
MAGIC equ 0x1BADB002 ; 'magic number' lets bootloader find the header
CHECKSUM equ -(MAGIC + FLAGS) ; checksum of above, to prove we are multiboot
; Собственно заголовок
section .multiboot
align 4
dd MAGIC
dd FLAGS
dd CHECKSUM
section .bss
align 16
stack_bottom:
resb 16384 ; 16 KiB
stack_top:
section .text
global _start:function (_start.end - _start)
_start:
mov esp, stack_top ; настраиваем стек
push ebx ; указатель на данные от загрузчика
extern kernel_main
call kernel_main
cli ; если почему-то вышли из ядра, отключить прерывания (то, что может внезапно вернуть управление в ядро)
.hang: hlt ; зависнуть
jmp .hang ; если процессор пробудился, обратно зависнуть
.end:
#include <stddef.h>
void kernel_main(void* multiboot_structure) {
const char str[] = "H\x0F""e\x0Fl\x0Fl\x0Fo\x0F \x0Fw\x0Fo\x0Fr\x0Fl\x0F""d\x0F";
char* buf = (char*) 0xB8000;
char c;
for(size_t i = 0; c = str[i]; i++) {
buf[i] = str[i];
}
while(1);
}
-s -S
. QEMU будет дожидаться отладчика и включит сетевую отладку. Также стоит заметить, что отладка не будет работать при использовании ускорителя, так что флаг --enable-kvm
придется убрать, если он используется.--enable-gdb-stub
, а в конфиг включить строку наподобие gdbstub: enabled=1, port=1234, text_base=0, data_base=0, bss_base=0
.(gdb) file kernel.elf
(gdb) target remote localhost:1234
(gdb) c
К сожалению, не доступен сервер mySQL