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:0 x7c00 і передасть туди управління.
Ми його отримаємо і для початку перемістити себе нижче за адресою 0000:0 x600,
перейдемо туди і спокійно продовжимо роботу. Власне вся наша робота буде
складатися з завантаження ядра (сектори 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:0 x8000). Ядро пізніше забере всі дані і буде їх використовувати для виведення
на екран переможного повідомлення.
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 + +)! = '