The Real Hello World h2>
В
цій статті ми напишемо ... власну міні-ОС. Так так, створимо свою власну
операційну систему. Правда система буде вантажитися з дискети і виводити
знайоме Hello World, але погодьтеся, це справить враження і на вас, і на
ваших друзів. Адже саме Ви створите СВОЮ p>
міні-ОС. p>
1. Ідея (hello.c) h2>
Вивчення
нової мови програмування починається, як правило, з написання простенькій
програми, що виводить на екран коротке привітання типу "Hello
World! ". Наприклад, для C це буде виглядати приблизно так. P>
main () p>
( p>
printf ( "Hello
World! N "); p>
) p>
Показово,
але зовсім не цікаво. Програма, звичайно працює, захищений режим, але
адже для її функціонування потрібно ЦІЛА операційна система. А що якщо
написати такий "Hello World", для якого нічого не треба. Вставляємо
дискетки в комп'ютер, завантажується з неї і ... "Hello World". Можна
навіть прокричати це вітання із захищеного режиму. p>
Сказано
- Зроблено. З чого почати? .. Набратися знань, звичайно. Для цього дуже
добре полазити в исходники Linux і Thix. Перша система всім добре знайома,
друга менш відома, але не менш корисна. p>
Підучившись?
... Зрозуміло, що спочатку треба написати завантажувальний сектор для нашої міні-опрераціонкі
(адже це саме міні-операційка). Оскільки процесор вантажиться в
16-розрядному режимі, то для созджанія завантажувального сектора використовується
асемблер і лінковщік з пакету bin86. Можна, звичайно, пошукати ще що-небудь,
але обидва наших прикладу використовують саме його і ми теж піде по стопах вчителів.
Синтаксис цього асемблера немколько дивакуватий, що поєднує риси,
характерні і для Intel і для AT & T (за подробицями прямуйте до
Linux-Assembly-HOWTO), але після кількох тижнів мук можна звикнути. P>
2. Завантажувальний сектор (boot.S) h2>
Свідомо
не буду приводити лістингів програм. Так стануть зрозуміліше основні ідеї, та й
вам буде набагато приємніше, якщо все напишіть своїми руками. p>
Для
Спершу визначимося з основними константами. p>
START_HEAD
= 0 - Головка приводу, яку будемо використовувати. P>
START_TRACK
= 0 - Доріжка, звідки почнемо читання. P>
START_SECTOR
= 2 - Сектор, починаючи з якого будемо зчитувати наше ядерце. P>
SYSSIZE
= 10 - Розмір ядра в секторах (кожен сектор містить 512 байт) p>
FLOPPY_ID
= 0 - Ідентифікатор приводу. 0 - для першого, 1 - для друга p>
HEADS
= 2 - Кількість головок приводу. P>
SECTORS
= 18 - Кількість доріжок на дискеті. Для формату 1.44 Mb це кількість дорівнює
18. P>
В
процесі завантаження буде відбуватися наступне. Завантажувач BIOS вважає перший
сектор дискети, покладе його за адресою 0000:0 x7c00 і передасть туди управління.
Ми його отримаємо і для початку перемістити себе нижче за адресою 0000:0 x600,
перейдемо туди і спокійно продовжимо роботу. Власне вся наша робота буде
складатися з завантаження ядра (сектори 2 - 12 перших доріжки дискети) за адресою
0x100: 0000, переходу в захищений режим і стрибка на перші рядки ядра. У зв'язку
з цим ще декілька констант: p>
BOOTSEG
= 0x7c00 - Сюди помістить завантажувальний сектор BIOS. P>
INITSEG
= 0x600 - Сюди його перемістив ми. P>
SYSSEG
= 0x100 - А тут приємно розташується наше ядро. P>
DATA_ARB
= 0x92 - Визначник сегмента даних для дескриптора p>
CODE_ARB
= 0x9A - Визначник сегменту коду для дескриптора. P>
Першим
справою зробимо переміщення самих себе в більш прийнятне місце. p>
cli p>
xor ax, ax p>
mov ss, ax p>
mov sp, # BOOTSEG p>
mov si, sp p>
mov ds, ax p>
mov es, ax p>
sti p>
cld p>
mov di, # INITSEG p>
mov cx, # 0x100 p>
repnz p>
movsw p>
jmpi
go, # 0; стрибок у нове місце розташування p>
завантажувального
сектору на мітку go p>
Тепер
необхідно налаштувати як слід сегменти для даних (es, ds) і для стека. Це
звичайно неприємно, що все доводиться робити вручну, але що робити. Адже немає
нікого в пам'яті комп'ютера, крім нас і BIOS. p>
go: p>
mov ax, # 0xF0 p>
mov ss, ax p>
mov
sp, ax; Стек розмістимо як 0xF0: 0xF0 = 0xFF0 p>
mov
ax, # 0x60; Сегменти для даних ES і DS поставимо в 0x60 p>
mov ds, ax p>
mov es, ax p>
Нарешті
можна вивести переможний привітання. Нехай світ дізнається, що ми змогли
завантажитися. Оскільки у нас є все-таки ще BIOS, скористаємося готової
функцією 0x13 переривання 0x10. Можна звичайно знехтувати його і написати прямо в
відеопам'ять, але у нас кожен байт команди на рахунку, а байт таких всього 512.
Витратимо їх краще на щось більш корисне. P>
mov
cx, # 18 p>
mov bp, # boot_msg p>
call write_message p>
Функція
write_message вигдядіт наступним чином p>
write_message: p>
push bx p>
push ax p>
push cx p>
push dx p>
push cx p>
mov
ah, # 0x03; прочитаємо поточне положення курсору, p>
щоб
не виводити повідомлення де попало. p>
xor bh, bh p>
int 0x10 p>
pop cx p>
mov
bx, # 0x0007; Параметри символів, що виводяться: p>
відеосторінок
0, атрибут 7 (сірий на чорному) p>
mov
ax, # 0x1301; Виводимо рядок і Зрушуємо курсор. p>
int 0x10 p>
pop dx p>
pop cx p>
pop
ax p>
pop
bx p>
ret p>
А
повідомлення так p>
boot_msg: p>
. byte 13,10 p>
. ascii "Booting data ..." p>
. byte
0 p>
До
цього часу на дисплеї комп'ютера з'явиться скромне "Booting data
... ". Це в принципі вже" Hello World ", але давайте доб'ємося
трішки більшого. Перейдемо в захищений режим і виведемо цей "Hello"
вже з програми написаної на C. p>
Ядро
32-розрядне. Воно буде у нас розміщуватися окремо від завантажувального сектора і
збиратися вже gcc і gas. Синтаксис асемблера gas відповідає вимогам
AT & T, так що тут уже все простіше. Але для початку нам потрібно прочитати ядро.
Знову скористаємося готової функцією 0x2 переривання 0x13. P>
recalibrate: p>
mov ah, # 0 p>
mov dl, # FLOPPY_ID p>
int
0x13; виробляємо переініціалізацію дисководу. P>
jc recalibrate p>
call read_track; виклик функції читання ядра p>
jnc
next_work; якщо під час читання не відбулося нічого p>
поганого
то працюємо далі p>
bad_read: p>
;
якщо читання відбулося невдало то виводимо повідомлення про помилку p>
mov bp, # error_read_msg p>
mov cx, 7 p>
call
write_message p>
inf1:
jmp inf1; і йдемо в нескінченний цикл. p>
Тепер
нас врятує тільки ручна перезавантаження p>
Сама
функція читання гранично проста: довго і нудно заповнюємо параметри, а потім
одним махом зчитуємо ядро. Ускладнення розпочнуться, коли ядро перестане
поміщатися в 17 секторах (тобто 8.5 kb), але це поки що тільки в майбутньому, а
поки цілком достатньо такого блискавичного читання. p>
read_track: p>
pusha p>
push es p>
push ds p>
mov
di, # SYSSEG; Визначаємо p>
mov
es, di; адреса буфера для даних p>
xor bx, bx p>
mov ch, # START_TRACK; доріжка 0 p>
mov
cl, # START_SECTOR; починаючи з сектора 2 p>
mov dl, # FLOPPY_ID p>
mov dh, # START_HEAD p>
mov
ah, # 2 p>
mov
al, # SYSSIZE; вважати 10 секторів p>
int 0x13 p>
pop ds p>
pop es p>
popa p>
ret p>
Ось
і все. Ядро успішно прочитано і можна вивести ще одне радісне повідомлення на
екран. p>
next_work: p>
call
kill_motor; зупиняємо привід дисководу p>
mov
bp, # load_msg; виводимо повідомлення p>
mov
cx, # 4 p>
call
write_message p>
Ось
вміст повідомлення p>
load_msg: p>
. ascii "done" p>
. byte 0 p>
А
от функція зупинки двигуна приводу. p>
kill_motor: p>
push dx p>
push ax p>
mov dx, # 0x3f2 p>
xor al, al p>
out dx, al p>
pop ax p>
pop dx p>
ret p>
На
даний момент на екрані виведено "Booting data ... done" і лампочка
привода флоппі-дисків погашена. Всі затихли і готові до смертельного номеру --
стрибка в захищений режим. p>
Для
Спершу треба включити адресну лінію A20. Це в точності означає, що ми будемо
використовувати 32-розрядну адресацію до даних. p>
mov
al, # 0xD1; команда запису для 8042 p>
out # 0x64, al p>
mov al, # 0xDF; включити A20 p>
out
# 0x60, al p>
Виведемо
попередження, про те, що переходимо в захищений режим. Нехай все
знають, які ми важливі. p>
protected_mode: p>
mov
bp, # loadp_msg p>
mov cx, # 25 p>
call write_message p>
(Повідомлення: p>
loadp_msg: p>
. byte 13,10 p>
. ascii "Go to protected
mode ..." p>
. byte
0 p>
) p>
Поки
ще у нас живий BIOS, запам'ятаємо позицію курсору і збережемо її у відомому місці (
0000:0 x8000). Ядро пізніше забере всі дані і буде їх використовувати для виведення
на екран переможного повідомлення. p>
save_cursor: p>
mov
ah, # 0x03; читаємо поточну позицію курсору p>
xor bh, bh p>
int 0x10 p>
seg cs p>
mov
[0x8000], dx; зберігаємо в спеціальному тайнику p>
Тепер
увагу, забороняємо переривання (нема чого відволікатися під час такої роботи) і
завантажуємо таблицю дескрипторів p>
cli p>
lgdt
GDT_DESCRIPTOR; завантажуємо описувач таблиці дескрипторів. P>
У
нас таблиця дескрипторів складається з трьох описувачів: Нульовий (завжди повинен
бути присутнім), сегменту коду і сегменту даних p>
. align 4 p>
. word 0 p>
GDT_DESCRIPTOR:. word 3 * 8 - 1; розмір таблиці p>
дескрипторів p>
. long
0x600 + GDT; місце розташування p>
таблиці
дескрипторів p>
. align
2 p>
GDT: p>
. long
0, 0; Номер 0: порожній p>
дескриптор p>
. word
0xFFFF, 0; Номер 8: p>
дескриптор
коду p>
. byte
0, CODE_ARB, 0xC0, 0 p>
. word
0xFFFF, 0; Номер 0x10: p>
дескриптор
даних p>
. byte
0, DATA_ARB, 0xCF, 0 p>
Перехід
в захищений режим може відбуватися мінімум двома способами, але обидві ОС,
обрані нами для прикладу (Linux і Thix) використовують для сумісності з 286
процесором команду lmsw. Ми будемо діяти тим же способом p>
mov
ax, # 1 p>
lmsw
ax; прощай реальний режим. Ми тепер p>
знаходимося
в захищеному режимі. p>
jmpi
0x1000, 8; Затяжний стрибок на 32-розрядне ядро. P>
Ось
і вся робота завантажувального сектора - чимало, але й небагато. Тепер ми попрощаємося
з ним і попрямуємо до ядра. p>
В
Наприкінці асемблерні файлу корисно додати наступну інструкцію. p>
. org
511 p>
end_boot:
. byte 0 p>
В
результаті скомпільований код буде займати рівно 512 байт, що дуже зручно
для підготовки образу завантажувального диска. p>
3.
Перші подихи ядра (head.S) p>
Ядро
на жаль знову почнеться з асемблерні коду. Але тепер його буде зовсім
небагато. p>
Ми
власне задамо правильні значення сегментів для даних (ES, DS, FS, GS).
Записав туди значення відповідного дескриптора даних. P>
cld p>
cli p>
movl
$ (__KERNEL_DS),% Eax p>
movl% ax,% ds p>
movl% ax,% es p>
movl% ax,% fs p>
movl% ax,% gs p>
Перевіримо,
чи нормально включилася адресна лінія A20 простим тестом запису. Обнулив для
чистоти експерименту регістр прапорів. p>
xorl
% eax,% eax p>
1:
incl% eax p>
movl% eax, 0x000000 p>
cmpl% eax, 0x100000 p>
je
1b p>
pushl
$ 0 p>
popfl p>
викличемо
довгоочікувану функцію, уже написану на С. p>
call SYMBOL_NAME (start_my_kernel) p>
І
більше нам тут робити нічого. p>
inf:
jmp inf p>
4. Поговоримо на мові високого рівня (start.c) h2>
Ось
тепер ми повернулися до того з чого починали розповідь. Майже повернулися, тому що
printf () тепер треба робити вручну. оскільки готових переривань вже немає, то
будемо використовувати прямі запис у відеопам'ять. Для цікавих - майже весь код
цій частині, з незначними змінами, повзаімствован з частини ядра Linux,
здійснює розпакування (/ arch/i386/boot/compressed/*). Для збирання вам
буде потрібно додатково визначити такі макроси як inb (), outb (), inb_p (),
outb_p (). Готові визначення найпростіше позичити з будь-якої версії Linux. P>
Тепер,
щоб не плутатися з вбудованими в glibc функціями, скасуємо їх визначення p>
# undef memcpy p>
Задамо кілька своїх p>
static void puts (const char *); p>
static char * vidmem = (char
*) 0xb8000;/* адреса відеопаматі */ p>
static int vidport;/* відеопортів */ p>
static int lines, cols;/* кількість ліній і рядків на
екран */ p>
static
int curr_x, curr_y;/* поточне положення курсору */ p>
І
почнемо, нарешті, писати код на мові високого рівня ... правда з невеликими
асемблерні вставками. p>
/* функція
перекладу курсору в положення (x, y). Робота ведеться через введення/виведення в
відеопортів */ p>
void gotoxy (int x, int y) p>
( p>
int pos; p>
pos = (x + cols * y) * 2; p>
outb_p (14, vidport); p>
outb_p (0xff & (pos>> 9),
vidport +1); p>
outb_p (15, vidport); p>
outb_p (0xff & (pos>> 1),
vidport +1); p>
) p>
/* функція
прокручування екрану. Працює, використовуючи пряму запис у відеопам'ять */ p>
static void scroll () p>
( p>
int i; p>
memcpy (vidmem, vidmem + cols * 2,
(Lines - 1) * cols * 2); p>
for (i = (lines - 1) * cols * 2;
i
vidmem [i]
= ''; P>
) p>
/* функція
виводу рядка на екран */ p>
static void puts (const char * s) p>
( p>
int x, y; p>
char c; p>
x = curr_x; p>
y = curr_y; p>
while ((c = * s + +)! = '