The Real Hello World

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

vidmem[i]
= ‘ ‘;

}

/*функция
вывода строки на экран*/

static void puts(const char *s)

{

int x,y;

char c;

x = curr_x;

y = curr_y;

while ( ( c = *s++ ) != ‘