Создание kernel.bin
Для создания мультизагрузочного диска нужно прописать заголовок образа. То есть, в первые 8 Кб нужно запихнуть три 32-битные машинные слова – это «магическое число», мультизагрузочные флаги и контрольная сумма.
Для начала нужно создать программу начальной загрузки. По сколько код программы прокомментирован, нет смысла объяснять, что там происходит.
/* boot.S - bootstrap the kernel */ /* first we give some definitions, to make our code more readable later */ #define ASM 1 #define MULTIBOOT_HEADER_MAGIC 0x1BADB002 /* magic no. for multiboot header */ #define MULTIBOOT_HEADER_FLAGS 0x00000003 /* flags for multiboot header */ #define STACK_SIZE 0x4000 /* size of our stack (16KB) */ /* On some systems we have to jump from assembly to C by referring to the C name prefixed by an underscore. This is all defined during configuration. We do not worry about it here and make sure we can handle either (HAVE_ASM_USCORE is defined by configure). */ #ifdef HAVE_ASM_USCORE # define EXT_C(sym) _ ## sym #else # define EXT_C(sym) sym #endif /* The text segment will contain code, the data segment data and the bss segment zero-filled static variables */ .text .globl start /* start is the global entry point into kernel */ start: /* As we need the multiboot magic values in the beginning of our file, we will add them presently. The real code continues right after those 3 long words, so jump over them */ jmp multiboot_entry /* Align the multiboot header at a 32 bits boundary. */ .align 4 multiboot_header: /* Now comes the multiboot header. */ .long MULTIBOOT_HEADER_MAGIC .long MULTIBOOT_HEADER_FLAGS .long -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS) multiboot_entry: /* We do not like assembly. All we want is to create a a stack and start executing C code */ /* Initialize the stack pointer (definition of stack follows) */ movl $(stack + STACK_SIZE), %esp /* Now enter the C main function (EXT_C will handle the underscore, if needed) */ call EXT_C(cmain) /* Halt. When we return from C, we end up here. */ loop: hlt jmp loop .section ".bss" /* We define our stack area. The .comm pseudo op declares a common symbol (common means that if a symbol with the same name is defined in another object file, it can be they can be merged (after all, we only need one stack). If the loader (ld) does not find an existing definition, it will allocate STACK_SIZE bytes of uninitialised memory. */ .comm stack, STACK_SIZE
Код состоит из трех сегментов: текстовый, данных и bss. И теперь нужно указать место в оперативной памяти, куда мы будем помещать данные сегменты, т.е. создать указатель (линкер), который будет объединять файл загрузки с главной программой (Hello world). Код линкера link.ld.
ENTRY(start) phys = 0x00100000; SECTIONS { .text phys : AT(phys) { code = .; *(.text) *(.rodata) . = ALIGN(4096); } .data : AT(phys + (data - code)) { data = .; *(.data) . = ALIGN(4096); } .bss : AT(phys + (bss - code)) { bss = .; *(.bss) . = ALIGN(4096); } end = .; }
Чтобы получить что-то на мониторе, точнее вывести, нужно задействовать систему ввода вывода (I/O).
Для начала нужно создать манипулятор памятью mem.c, так как у нас нет созданных libc-файлов, которые бы занимались распределением, все нужно делать вручную.
unsigned char *memcpy(unsigned char *dest, const unsigned char *src, int count) { int i; for (i=0; ireturn dest; } unsigned char *memset(unsigned char *dest, unsigned char val, int count) { int i; for (i=0; ireturn dest; } Теперь нам нужно научиться получать сигнал от клавиатуры и выводить на монитор информацию (basicio.c). /* We use inbyte() to read from the I/O ports (e.g., to get data from * the keyboard). */ unsigned char inbyte (unsigned short _port) { unsigned char rv; __asm__ __volatile__ ("inb %1, %0" : "=a" (rv) : "dN" (_port)); return rv; } /* We use outbyte() to write to the I/O ports (e.g., the screen). */ void outbyte (unsigned short _port, unsigned char _data) { __asm__ __volatile__ ("outb %1, %0" : : "dN" (_port), "a" (_data)); }
Ну и теперь беремся за сам вывод на монитор, подключая видео карту.
#include "types.h" #include "mem.h" #include "basicio.h" #define COLOURS 0xF0 #define COLS 80 #define ROWS 25 #define VGA_START 0xB8000 #define PRINTABLE(c) (c>=' ') uint16_t *Scrn; // screen area int Curx, Cury = 0; // current cursor coordinates uint16_t EmptySpace = COLOURS << 8 | 0x20; /* 0x20 is ascii value of space */ /* These define our textpointer, our background and foreground * colors (attributes), and x and y cursor coordinates */ unsigned short *textmemptr; int attrib = 0x0F; int csr_x = 0, csr_y = 0; void scroll(void) { int dist = Cury - ROWS + 1; if (dist > 0) { uint8_t *newstart = ((uint8_t*) Scrn) + dist * COLS *2; int bytesToCopy = (ROWS-dist)*COLS*2; uint16_t *newblankstart = Scrn + (ROWS-dist) * COLS; int bytesToBlank = dist*COLS*2; memcpy ((uint8_t*)Scrn, newstart, bytesToCopy); memset ((uint8_t*) newblankstart, EmptySpace, bytesToBlank); } } // Print a character on the screen void putchar (uint8_t c) { uint16_t *addr; // first handle a few special characters // tab -> move cursor in steps of 4 if(c == 't') Curx = ((Curx + 4)/4)*4; // carriage return -> reset x pos else if(c == 'r') Curx = 0; // newline: reset x pos and go to newline else if(c == 'n') { Curx = 0; Cury++; } // backspace -> cursor moves left else if(c == 0x08 && Curx != 0) Curx--; // finally, if a normal character, print it else if(PRINTABLE(c)) { addr = Scrn + (Cury * COLS + Curx); *addr = (COLOURS<<8) | c; Curx++; } // if we have reached the end of the line, move to the next if (Curx >= COLS) { Curx = 0; Cury++; } // also scroll if needed scroll(); } // print a longer string void puts(unsigned char *str) { while (*str) { putchar(*str); str++;} } void itoa (char *buf, int base, int d) { char *p = buf; char *p1, *p2; unsigned long ud = d; int divisor = 10; /* If %d is specified and D is minus, put `-' in the head. */ if (base == 'd' && d < 0) { *p++ = '-'; buf++; ud = -d; } else if (base == 'x') divisor = 16; /* Divide UD by DIVISOR until UD == 0. */ do { int remainder = ud % divisor; *p++ = (remainder < 10) ? remainder + '0' : remainder + 'a' - 10; } while (ud /= divisor); /* Terminate BUF. */ *p = 0; /* Reverse BUF. */ p1 = buf; p2 = p - 1; while (p1 < p2) { char tmp = *p1; *p1 = *p2; *p2 = tmp; p1++; p2--; } } // Format a string and print it on the screen, just like the libc // function printf. void printf (const char *format, ...) { char **arg = (char **) &format; int c; char buf[20]; arg++; while ((c = *format++) != 0) { if (c != '%') putchar (c); else { char *p; c = *format++; switch (c) { case 'd': case 'u': case 'x': itoa (buf, c, *((int *) arg++)); p = buf; goto string; break; case 's': p = *arg++; if (p == NULL) p = "(null)"; string: while (*p) putchar (*p++); break; default: putchar (*((int *) arg++)); break; } } } } // Clear the screen void clear() { int i; for(i = 0; i < ROWS*COLS; i++) putchar (' '); Curx = Cury = 0; //Scrn[i] = EmptySpace; } void vga_init(void) { Scrn = (unsigned short *)VGA_START; clear(); }
Далее нужно создать файл с нашим Hello world (main.c).
#include "types.h" #include "scrn.h" /* The main() function. It prints a message and then sits in an * infinite loop (yes, this will later become the 'idle' loop). */ void cmain (unsigned long magic, unsigned long addr) { vga_init(); puts ((uint8_t*)"hello world!"); /* start the idle loop (note: even if this loop is removed, there * is an extra infinite loop in the assembly that called into * this main function */ for (;;); }
Все приготовления сделаны. Теперь нужно создать файл для автосборки ядра (Makefile).
CFLAGS := -fno-stack-protector -fno-builtin -nostdinc -O -g -Wall -I. all: kernel.bin kernel.bin: boot.o main.o mem.o basicio.o scrn.o ld -T link.ld -o kernel.bin boot.o main.o mem.o basicio.o scrn.o @echo Done! clean: rm -f *.o *.bin
В результате чего, будет создан бинарник ядра – kernel.bin.
Настройка и запуск ядра
Первым делом устанавливаем нужные пакеты:
$ sudo apt-get install qemu gcc make binutils grub mtools
Для создания своего ядра нам нужно создать загружаемый образ, на котором будет храниться ядро и базовый загрузчик.
Создаем загружаемый образ.
$ dd if=/dev/zero of=core.img count=088704 bs=512
Теперь создаем файловую систему на этом образе и инициализируем таблицу разделов.
$ echo "drive c: file="`pwd`/core.img" partition=1" > ~/.mtoolsrc $ mpartition -I c:
Теперь нужно создать новый раздел, который должен соответствовать количеству секторов, которые мы задавали при создании загружаемого образа.
$ mpartition -c -t 88 -h 16 -s 63 c:
Далее форматируем раздел и добавляем директории.
$ mformat c: $ mmd c:/boot $ mmd c:/boot/grub
Так, как у меня установлен Grub-загрузчик первой версии, можно скопировать уровни загрузки в наши директории.
$ mcopy /boot/grub/stage1 c:/boot/grub/ $ mcopy /boot/grub/stage2 c:/boot/grub/ $ mcopy /boot/grub/fat_stage1_5 c:/boot/grub/
Теперь нужно создать «карту жестких дисков» по который наш Grub-загрузчик будет ориентироваться и указать, что наш образ выступает в роли первого загрузочного диска.
$ echo "(hd0) core.img" > bmap $ printf "geometry (hd0) 88 16 63 n root (hd0,0) n setup (hd0)n" | /usr/sbin/grub --device-map=bmap --batch
Теперь у нас есть все, что нужно для загрузки ядра. Копируем файл kernel.bin в папку c:/boot/grub.
$ mcopy kernel.bin c:/boot/grub/
Создаем файл menu.lst для Grub-загрузчика со следующим содержанием.
serial --unit=0 --stop=1 --speed=115200 --parity=no --word=8 terminal --timeout=0 serial console default 0 timeout = 0 title = mykernel kernel=/boot/grub/kernel.bin
Копируем созданный файл меню в наш образ.
$ mcopy menu.lst c:/boot/grub/
Теперь запускаем виртуальную машину и смотрим на результат
$ qemu -hda ../core.img