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)


Сознательно не буду приводить листингов программ. Так станут понятней основные идеи, да и вам будет намного приятней, если все напишите своими руками.


Для начала определимся с основными константами.


START_HEAD = 0 - Головка привода, которою будем использовать.


START_TRACK = 0 - Дорожка, откуда начнем чтение.


START_SECTOR = 2 - Сектор, начиная с которого будем считывать наше ядрышко.


SYSSIZE = 10 - Размер ядра в секторах (каждый сектор содержит 512 байт)


FLOPPY_ID = 0 - Идентификатор привода. 0 - для первого, 1 - для второго


HEADS = 2 - Количество головок привода.


SECTORS = 18 - Количество дорожек на дискете. Для формата 1.44 Mb это количество равно 18.


В процессе загрузки будет происходить следующее. Загрузчик BIOS считает первый сектор дискеты, положит его по адресу 0000:0x7c00 и передаст туда управление. Мы его получим и для начала переместим себя пониже по адресу 0000:0x600, перейдем туда и спокойно продолжим работу. Собственно вся наша работа будет состоять из загрузки ядра (сектора 2 - 12 первой дорожки дискеты) по адресу 0x100:0000, переходу в защищенный режим и скачку на первые строки ядра. В связи с этим еще несколько констант:


BOOTSEG = 0x7c00 - Сюда поместит загрузочный сектор BIOS.


INITSEG = 0x600 - Сюда его переместим мы.


SYSSEG = 0x100 - А здесь приятно расположится наше ядро.


DATA_ARB = 0x92 - Определитель сегмента данных для дескриптора


CODE_ARB = 0x9A - Определитель сегмента кода для дескриптора.


Первым делом произведем перемещение самих себя в более приемлемое место.


cli


xor ax, ax


mov ss, ax


mov sp, #BOOTSEG


mov si, sp


mov ds, ax


mov es, ax


sti


cld


mov di, #INITSEG


mov cx, #0x100


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 ds, ax


mov es, ax


Наконец можно вывести победное приветствие. Пусть мир узнает, что мы смогли загрузиться. Поскольку у нас есть все-таки еще BIOS, воспользуемся готовой функцией 0x13 прерывания 0x10. Можно конечно презреть его и написать напрямую в видеопамять, но у нас каждый байт команды на счету, а байт таких всего 512. Потратим их лучше на что-нибудь более полезное.


mov cx,#18


mov bp,#boot_msg


call write_message


Функция write_message выгдядит следующим образом


write_message:


push bx


push ax


push cx


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 dx


pop cx


pop ax


pop bx


ret


А сообщение так


boot_msg:


.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, #FLOPPY_ID


int 0x13 ; производим переинициализацию дисковода.


jc recalibrate


call read_track ; вызов функции чтения ядра


jnc next_work ; если во время чтения не произошло ничего


плохого то работаем дальше


bad_read:


; если чтение произошло неудачно то выводим сообщение об ошибке


mov bp,#error_read_msg


mov cx,7


call write_message


inf1: jmp inf1 ; и уходим в бесконечный цикл.


Теперь нас спасет только ручная перезагрузка


Сама функция чтения предельно простая: долго и нудно заполняем параметры, а затем одним махом считываем ядро. Усложнения начнуться, когда ядро перестанет помещаться в 17 секторах ( то есть 8.5 kb), но это пока только в будущем, а пока вполне достаточно такого молниеносного чтения.


read_track:


pusha


push es


push ds


mov di, #SYSSEG ; Определяем


mov es, di ; адрес буфера для данных


xor bx, bx


mov ch, #START_TRACK ;дорожка 0


mov cl, #START_SECTOR ;начиная с сектора 2


mov dl, #FLOPPY_ID


mov dh, #START_HEAD


mov ah, #2


mov al, #SYSSIZE ;считать 10 секторов


int 0x13


pop ds


pop es


popa


ret


Вот и все. Ядро успешно прочитано и можно вывести еще одно радостное сообщение на экран.


next_work:


call kill_motor ; останавливаем привод дисковода


mov bp,#load_msg ; выводим сообщение


mov cx,#4


call write_message


Вот содержимое сообщения


load_msg:


.ascii "done"


.byte 0


А вот функция остановки двигателя привода.


kill_motor:


push dx


push ax


mov dx,#0x3f2


xor al,al


out dx,al


pop ax


pop dx


ret


На данный момент на экране выведено "Booting data ...done" и лампочка привода флоппи-дисков погашена. Все затихли и готовы к смертельному номеру - прыжку в защищенный режим.


Для начала надо включить адресную линию A20. Это в точности означает, что мы будем использовать 32-разрядную адресацию к данным.


mov al, #0xD1 ; команда записи для 8042


out #0x64, al


mov al, #0xDF ; включить A20


out #0x60, al


Выведем предупреждающее сообщение, о том, что переходим в защищенный режим. Пусть все знают, какие мы важные.


protected_mode:


mov bp,#loadp_msg


mov cx,#25


call write_message


(Сообщение:


loadp_msg:


.byte 13,10


.ascii "Go to protected mode..."


.byte 0


)


Пока еще у нас жив BIOS, запомним позицию курсора и сохраним ее в известном месте ( 0000:0x8000 ). Ядро позже заберет все данные и будет их использовать для вывода на экран победного сообщения.


save_cursor:


mov ah,#0x03 ; читаем текущую позицию курсора


xor bh,bh


int 0x10


seg cs


mov [0x8000],dx ;сохраняем в специальном тайнике


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


cli


lgdt GDT_DESCRIPTOR ; загружаем описатель таблицы дескрипторов.


У нас таблица дескрипторов состоит из трех описателей: Нулевой (всегда должен присутствовать), сегмента кода и сегмента данных


.align 4


.word 0


GDT_DESCRIPTOR: .word 3 * 8 - 1 ; размер таблицы


дескрипторов


.long 0x600 + GDT ; местоположение


таблицы дескрипторов


.align 2


GDT:


.long 0, 0 ; Номер 0: пустой


дескриптор


.word 0xFFFF, 0 ; Номер 8:


дескриптор кода


.byte 0, CODE_ARB, 0xC0, 0


.word 0xFFFF, 0 ; Номер 0x10:


дескриптор данных


.byte 0, DATA_ARB, 0xCF, 0


Переход в защищенный режим может происходить минимум двумя способами, но обе ОС , выбранные нами для примера (Linux и Thix) используют для совместимости с 286 процессором команду lmsw. Мы будем действовать тем же способом


mov ax, #1


lmsw ax ; прощай реальный режим. Мы теперь


находимся в защищенном режиме.


jmpi 0x1000, 8 ; Затяжной прыжок на 32-разрядное ядро.


Вот и вся работа загрузочного сектора - немало, но и немного. Теперь мы попрощаемся с ним и направимся к ядру.


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


.org 511


end_boot: .byte 0


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


3. Первые вздохи ядра (head.S)


Ядро к сожалению опять начнется с ассемблерного кода. Но теперь его будет совсем немного.


Мы собственно зададим правильные значения сегментов для данных (ES, DS, FS, GS). Записав туда значение соответствующего дескриптора данных.


cld


cli


movl $(__KERNEL_DS),%eax


movl %ax,%ds


movl %ax,%es


movl %ax,%fs


movl %ax,%gs


Проверим, нормально ли включилась адресная линия A20 простым тестом записи. Обнулим для чистоты эксперимента регистр флагов.


xorl %eax,%eax


1: incl %eax


movl %eax,0x000000


cmpl %eax,0x100000


je 1b


pushl $0


popfl


Вызовем долгожданную функцию, уже написанную на С.


call SYMBOL_NAME(start_my_kernel)


И больше нам тут делать нечего.


inf: jmp inf


4. Поговорим на языке высокого уровня (start.c)


Вот теперь мы вернулись к тому с чего начинали рассказ. Почти вернулись, потому что printf() теперь надо делать вручную. поскольку готовых прерываний уже нет, то будем использовать прямую запись в видеопамять. Для любопытных - почти весь код этой части , с незначительными изменениями, повзаимствован из части ядра Linux, осуществляющей распаковку (/arch/i386/boot/compressed/*). Для сборки вам потребуется дополнительно определить такие макросы как inb(), outb(), inb_p(), outb_p(). Готовые определения проще всего одолжить из любой версии Linux.


Теперь, дабы не путаться со встроенными в glibc функциями, отменим их определение


#undef memcpy


Зададим несколько своих


static void puts(const char *);


static char *vidmem = (char *)0xb8000; /*адрес видеопамати*/


static int vidport; /*видеопорт*/


static int lines, cols; /*количество линий и строк на экран*/


static int curr_x,curr_y; /*текущее положение курсора */


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


/*функция перевода курсора в положение (x,y). Работа ведется через ввод/вывод в видеопорт*/


void gotoxy(int x, int y)


{


int pos;


pos = (x + cols * y) * 2;


outb_p(14, vidport);


outb_p(0xff & (pos >> 9), vidport+1);


outb_p(15, vidport);


outb_p(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 = curr_x;


y = curr_y;


while ( ( c = *s++ ) != '0' ) {


if ( c == 'n' ) {


x = 0;


if ( ++y >= lines ) {


scroll();


y--;


}


} else {


vidmem [ ( x + cols * y ) * 2 ] = c;


if ( ++x >= cols ) {


x = 0;


if ( ++y >= lines ) {


scroll();


y--;


}


}


}


}


gotoxy(x,y);


}


/*функция копирования из одной области памяти в другую. Заместитель стандартной функции glibc */


void* memcpy(void* __dest, __const void* __src,


unsigned int __n)


{


int i;


char *d = (char *)__dest, *s = (char *)__src;


for (i=0;i<__n;i++) d[i] = s[i];


}


/*функция издающая долгий и протяжных звук. Использует только ввод/вывод в порты поэтому очень полезна для отладки*/


make_sound()


{


__asm__("


movb $0xB6, %alnt


outb %al, $0x43nt


movb $0x0D, %alnt


outb %al, $0x42nt


movb $0x11, %alnt


outb %al, $0x42nt


inb $0x61, %alnt


orb $3, %alnt


outb %al, $0x61nt


");


}


/*А вот и основная функция*/


int start_my_kernel()


{


/*задаются основные параметры */


vidmem = (char *) 0xb8000;


vidport = 0x3d4;


lines = 25;


cols = 80;


/*считывается предусмотрительно сохраненные координаты курсора*/


curr_x=*(unsigned char *)(0x8000);


curr_y=*(unsigned char *)(0x8001);


/*выводится строка*/


puts("donen");


/*уходим в бесконечный цикл*/


while(1);


}


Вот и вывели мы этот "Hello World" на экран. Сколько проделано работы, а на экране только две строчки


Booting data ...done


Go to proteсted mode ...done


Немного, но и немало. Закричала новая операционная система. Мир с радостью воспринял ее. Кто знает, может быть это новый Linux ...


5. Подготовка загрузочного образа (floppy.img)


Итак, подготовим загрузочный образ нашей системки.


Для начала соберем загрузочный сектор.


as86 -0 -a -o boot.o boot.S


ld86 -0 -s -o boot.img boot.o


Обрежем 32 битный заголовок и получим таким образом чистый двоичный код.


dd if=boot.img of=boot.bin bs=32 skip=1


Соберем ядро


gcc -traditional -c head.S -o head.o


gcc -O2 -DSTDC_HEADERS -c start.c


При компоновке НЕ ЗАБУДБЬТЕ параметр "-T" он указывает относительно которого смещения вести расчеты, в нашем случае поскольку ядро грузится по адресy 0x1000, то и смещение соотетствующее


ld -m elf_i386 -Ttext 0x1000 -e startup_32 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


Образ готов. Записываем на дискетку (заготовьте несколько для экспериментов, я прикончил три штуки) перезагружаем компьютер и наслаждаемся.


cat floppy.img >/dev/fd0


6. Е-мое, что ж я сделал (...)


Здорово, правда? Приятно почувствовать себя будущим Торвальдсом или кем-то еще. Красная линия намечена, можно смело идти вперед, дописывать и переписывать систему. Описанная процедура пока что едина для множества операционных систем, будь то UNIX или Windows. Что напишете Вы? ... не знает не кто. Ведь это будет Ваша система.

Сохранить в соц. сетях:
Обсуждение:
comments powered by Disqus

Название реферата: The Real Hello World

Слов:2237
Символов:18880
Размер:36.88 Кб.