The Real Hello World
В этой статье мы напишем... собственную мини-ОС. Да да, создадим свою собственную операционную систему. Правда система будет грузиться с дискеты и выводить знакомое Hello World, но согласитесь, это произведет впечатление и на вас, и на ваших друзей. Ведь именно Вы создадите СВОЮ
1. Идея (hello. c)
"Hello World!". Например, для C это будет выглядить приблизительно так.
main()
{
printf("Hello World!\n");
}
Показательно, но совершенно не интересно. Программа, конечно работает, режим защищенный, но ведь для ее функционирования требуется ЦЕЛАЯ операционная система. А что если написать такой "Hello World", для которого ничего не надо. Вставляем дискетку в компьютер, загружаемся с нее и..."Hello World". Можно даже прокричать это приветствие из защищенного режима.
Сказано - сделано. С чего бы начать?.. Набраться знаний, конечно. Для этого очень хорошо полазить в исходниках Linux и Thix. Первая система всем хорошо знакома, вторая менее известна, но не менее полезна.
Подучились? ... Понятно, что сперва надо написать загрузочный сектор для нашей мини-опрерационки (а ведь это именно мини-операционка). Поскольку процессор грузится в 16-разрядном режиме, то для созджания загрузочного сектора используется ассемблер и линковщик из пакета bin86. Можно, конечно, поискать еще что-нибудь, но оба наших примера используют именно его и мы тоже пойдет по стопам учителей. Синтаксис этого ассемблера немколько странноватый, совмещающий черты, характерные и для Intel и для AT&T (за подробностями направляйтесь в Linux-Assembly-HOWTO), но после пары недель мучений можно привыкнуть.
2. Загрузочный сектор (boot. S)
Для начала определимся с основными константами.
STARTHEAD = 0 - Головка привода, которою будем использовать.
STARTTRACK = 0 - Дорожка, откуда начнем чтение.
STARTSECTOR = 2 - Сектор, начиная с которого будем считывать наше ядрышко.
SYSSIZE = 10 - Размер ядра в секторах (каждый сектор содержит 512 байт)
FLOPPYID = 0 - Идентификатор привода. 0 - для первого, 1 - для второго
HEADS = 2 - Количество головок привода.
SECTORS = 18 - Количество дорожек на дискете. Для формата 1. 44 Mb это количество равно 18.
В процессе загрузки будет происходить следующее. Загрузчик BIOS считает первый сектор дискеты, положит его по адресу 0000:0x7c00 и передаст туда управление. Мы его получим и для начала переместим себя пониже по адресу 0000:0x600, перейдем туда и спокойно продолжим работу. Собственно вся наша работа будет состоять из загрузки ядра (сектора 2 - 12 первой дорожки дискеты) по адресу 0x100:0000, переходу в защищенный режим и скачку на первые строки ядра. В связи с этим еще несколько констант:
BOOTSEG = 0x7c00 - Сюда поместит загрузочный сектор BIOS.
SYSSEG = 0x100 - А здесь приятно расположится наше ядро.
DATAARB = 0x92 - Определитель сегмента данных для дескриптора
CODEARB = 0x9A - Определитель сегмента кода для дескриптора.
Первым делом произведем перемещение самих себя в более приемлемое место.
cli
xor ax, ax
mov ss, ax
mov sp, #BOOTSEG
mov si, sp
mov es, ax
sti
cld
mov di, #INITSEG
repnz
movsw
jmpi go, #0 ; прыжок в новое местоположение
загрузочного сектора на метку go
Теперь необходимо настроить как следует сегменты для данных (es, ds) и для стека. Это конечно неприятно, что все приходится делать вручную, но что делать. Ведь нет никого в памяти компьютера, кроме нас и BIOS.
go:
mov ax, #0xF0
mov ss, ax
mov sp, ax ; Стек разместим как 0xF0:0xF0 = 0xFF0
mov ax, #0x60 ; Сегменты для данных ES и DS зададим в 0x60
mov es, ax
Наконец можно вывести победное приветствие. Пусть мир узнает, что мы смогли загрузиться. Поскольку у нас есть все-таки еще BIOS, воспользуемся готовой функцией 0x13 прерывания 0x10. Можно конечно презреть его и написать напрямую в видеопамять, но у нас каждый байт команды на счету, а байт таких всего 512. Потратим их лучше на что-нибудь более полезное.
mov bp,#bootmsg
call writemessage
Функция writemessage выгдядит следующим образом
writemessage:
push ax
push dx
push cx
mov ah,#0x03 ; прочитаем текущее положение курсора,
дабы не выводить сообщения где попало.
xor bh,bh
int 0x10
pop cx
mov bx,#0x0007 ; Параметры выводимых символов :
видеостраница 0, аттрибут 7 (серый на черном)
mov ax,#0x1301 ; Выводим строку и сдвигаем курсор.
int 0x10
pop cx
pop ax
pop bx
ret
А сообщение так
bootmsg:
. byte 13,10
. ascii "Booting data..."
. byte 0
К этому времени на дисплее компьютера появится скромное "Booting data..." . Это в принципе уже "Hello World", но давайте добьемся чуточку большего. Перейдем в защищенный режим и выведем этот "Hello" уже из программы написаной на C.
Ядро 32-разрядное. Оно будет у нас размещаться отдельно от загрузочного сектора и собираться уже gcc и gas. Синтаксис ассемблера gas соответсвует требованиям AT&T, так что тут уже все проще. Но для начала нам нужно прочитать ядро. Опять воспользуемся готовой функцией 0x2 прерывания 0x13.
recalibrate:
mov ah, #0
mov dl, #FLOPPYID
int 0x13 ; производим переинициализацию дисковода.
jc recalibrate
call readtrack ; вызов функции чтения ядра
jnc nextwork ; если во время чтения не произошло ничего
плохого то работаем дальше
badread:
; если чтение произошло неудачно то выводим сообщение об ошибке
mov bp,#errorreadmsg
mov cx,7
call writemessage
inf1: jmp inf1 ; и уходим в бесконечный цикл.
Сама функция чтения предельно простая: долго и нудно заполняем параметры, а затем одним махом считываем ядро. Усложнения начнуться, когда ядро перестанет помещаться в 17 секторах ( то есть 8. 5 kb), но это пока только в будущем, а пока вполне достаточно такого молниеносного чтения.
readtrack:
pusha
push es
push ds
mov di, #SYSSEG ; Определяем
mov es, di ; адрес буфера для данных
xor bx, bx
mov ch, #STARTTRACK ;дорожка 0
mov cl, #STARTSECTOR ;начиная с сектора 2
mov dl, #FLOPPYID
mov dh, #STARTHEAD
mov al, #SYSSIZE ;считать 10 секторов
int 0x13
pop ds
popa
ret
mov cx,#4
call writemessage
Вот содержимое сообщения
. ascii "done"
. byte 0
А вот функция остановки двигателя привода.
killmotor:
push dx
push ax
xor al,al
out dx,al
pop ax
ret
На данный момент на экране выведено "Booting data... done" и лампочка привода флоппи-дисков погашена. Все затихли и готовы к смертельному номеру - прыжку в защищенный режим.
Для начала надо включить адресную линию A20. Это в точности означает, что мы будем использовать 32-разрядную адресацию к данным.
mov al, #0xD1 ; команда записи для 8042
out #0x64, al
out #0x60, al
protectedmode:
mov bp,#loadpmsg
mov cx,#25
call writemessage
(Сообщение:
loadpmsg:
. byte 13,10
. ascii "Go to protected mode..."
)
Пока еще у нас жив BIOS, запомним позицию курсора и сохраним ее в известном месте ( 0000:0x8000 ). Ядро позже заберет все данные и будет их использовать для вывода на экран победного сообщения.
savecursor:
mov ah,#0x03 ; читаем текущую позицию курсора
xor bh,bh
int 0x10
seg cs
cli
У нас таблица дескрипторов состоит из трех описателей: Нулевой (всегда должен присутствовать), сегмента кода и сегмента данных
. align 4
. word 0
дескрипторов
таблицы дескрипторов
GDT:
. long 0, 0 ; Номер 0: пустой
дескриптор
. word 0xFFFF, 0 ; Номер 8:
. byte 0, CODEARB, 0xC0, 0
. word 0xFFFF, 0 ; Номер 0x10:
дескриптор данных
. byte 0, DATAARB, 0xCF, 0
тем же способом
mov ax, #1
lmsw ax ; прощай реальный режим. Мы теперь
находимся в защищенном режиме.
jmpi 0x1000, 8 ; Затяжной прыжок на 32-разрядное ядро.
Вот и вся работа загрузочного сектора - немало, но и немного. Теперь мы попрощаемся с ним и направимся к ядру.
В конце ассемблерного файла полезно добавить следующую инструкцию.
endboot: . byte 0
В результате скомпилированный код будет занимать ровно 512 байт, что очень удобно для подготовки образа загрузочного диска.
3. Первые вздохи ядра (head. S)
Ядро к сожалению опять начнется с ассемблерного кода. Но теперь его будет совсем немного.
Мы собственно зададим правильные значения сегментов для данных (ES, DS, FS, GS). Записав туда значение соответствующего дескриптора данных.
cld
cli
movl $(__KERNELDS),%eax
movl %ax,%ds
movl %ax,%es
movl %ax,%fs
movl %ax,%gs
xorl %eax,%eax
1: incl %eax
movl %eax,0x000000
cmpl %eax,0x100000
je 1b
pushl $0
popfl
call SYMBOLNAME(startmykernel)
И больше нам тут делать нечего.
Для любопытных - почти весь код этой части , с незначительными изменениями, повзаимствован из части ядра Linux, осуществляющей распаковку (/arch/i386/boot/compressed/*). Для сборки вам потребуется дополнительно определить такие макросы как inb(), outb(), inbp(), outbp(). Готовые определения проще всего одолжить из любой версии Linux.
Зададим несколько своих
static void puts(const char *);
static char *vidmem = (char *)0xb8000; /*адрес видеопамати*/
static int vidport; /*видеопорт*/
static int lines, cols; /*количество линий и строк на экран*/
static int currx,curry; /*текущее положение курсора */
И начнем, наконец, писать код на языке высокого уровня... правда с небольшими ассемблерными вставками.
/*функция перевода курсора в положение (x,y). Работа ведется через ввод/вывод в видеопорт*/
void gotoxy(int x, int y)
{
int pos;
pos = (x + cols * y) * 2;
outbp(14, vidport);
& (pos >> 9), vidport+1);
outbp(0xff & (pos >> 1), vidport+1);
}
/*функция прокручивания экрана. Работает, используя прямую запись в видеопамять*/
static void scroll()
{
int i;
memcpy ( vidmem, vidmem + cols * 2, ( lines - 1 ) * cols * 2 );
for ( i = ( lines - 1 ) * cols * 2; i < lines * cols * 2; i += 2 )
vidmem[i] = ' ';
}
/*функция вывода строки на экран*/
static void puts(const char *s)
{
int x,y;
char c;
x = currx;
while ( ( c = *s++ ) != '\0' ) {
if ( c == '\n' ) {
x = 0;
if ( ++y >= lines ) {
y--;
}
} else {
if ( ++x >= cols ) {
x = 0;
if ( ++y >= lines ) {
y--;
}
}
}
}
gotoxy(x,y);
}
/*функция копирования из одной области памяти в другую. Заместитель стандартной функции glibc */
unsigned int __n)
{
int i;
for (i=0;i<__n;i++) d[i] = s[i];
}
/*функция издающая долгий и протяжных звук. Использует только ввод/вывод в порты поэтому очень полезна для отладки*/
makesound()
{
__asm__("
movb $0xB6, %al\n\t
outb %al, $0x43\n\t
movb $0x0D, %al\n\t
movb $0x11, %al\n\t
outb %al, $0x42\n\t
inb $0x61, %al\n\t
orb $3, %al\n\t
outb %al, $0x61\n\t
");
}
/*А вот и основная функция*/
int startmykernel()
{
/*задаются основные параметры */
vidport = 0x3d4;
lines = 25;
cols = 80;
/*считывается предусмотрительно сохраненные координаты курсора*/
currx=*(unsigned char *)(0x8000);
/*выводится строка*/
"done\n");
while(1);
}
Вот и вывели мы этот "Hello World" на экран. Сколько проделано работы, а на экране только две строчки
Booting data... done
Go to proteсted mode... done
5. Подготовка загрузочного образа (floppy. img)
Итак, подготовим загрузочный образ нашей системки.
as86 -0 -a -o boot. o boot. S
ld86 -0 -s -o boot. img boot. o
dd if=boot. img of=boot. bin bs=32 skip=1
Соберем ядро
gcc -traditional -c head. S -o head. o
gcc -O2 -DSTDCHEADERS -c start. c
При компоновке НЕ ЗАБУДБЬТЕ параметр "-T" он указывает относительно которого смещения вести расчеты, в нашем случае поскольку ядро грузится по адресy 0x1000, то и смещение соотетствующее
ld -m elfi386 -Ttext 0x1000 -e startup32 head. o start. o -o head. img
objcopy -O binary -R. note -R. comment -S head. img head. bin
И соединяем воедино загрузочный сектор и ядро
cat boot. bin head. bin >floppy. img
Образ готов. Записываем на дискетку (заготовьте несколько для экспериментов, я прикончил три штуки) перезагружаем компьютер и наслаждаемся.
>/dev/fd0
6. Е-мое, что ж я сделал (...)
Здорово, правда? Приятно почувствовать себя будущим Торвальдсом или кем-то еще. Красная линия намечена, можно смело идти вперед, дописывать и переписывать систему. Описанная процедура пока что едина для множества операционных систем, будь то UNIX или Windows. Что напишете Вы? ... не знает не кто. Ведь это будет Ваша система.
|