Справжній
"Hello World" h2>
Станіслав
Иевлев p>
З чого
починається вивчення нової мови (або середовища) програмування? З написання
простенькій програми, що виводить на екран коротке привітання типу "Hello
World! ". Наприклад, для C це буде виглядати приблизно так: p>
main () ( p>
printf ( "Hello
World! N "); p>
) p>
Показово,
але зовсім нецікаво. Програма, звичайно, працює, привітання своє
пише; але ж для цього потрібна ціла операційна система! А що якщо
хочеться написати програмку, для якої нічого не треба? Вставляємо дискетки в
комп'ютер, завантажується з неї і ... "Hello World"! Можна навіть
прокричати це вітання із захищеного режиму ... Сказано - зроблено. З чого
б почати? .. Набратися знань, звичайно. Для цього дуже добре полазити в
исходники Linux і Thix. Перша система всім добре знайома, другий менш
відома, але не менш корисна. p>
Підучившись?
Тепер займемося. Зрозуміло, що насамперед треба написати завантажувальний сектор для
нашої міні-операційки (а це ж буде саме міні-операційка!). Оскільки
процесор вантажиться в 16-розрядному режимі, то для створення завантажувального сектора
використовується асемблер і лінковщік з пакету bin86. Можна, звичайно, пошукати
ще що-небудь, але обидва наших прикладу використовують саме його, і ми теж підемо
стопах вчителів. Синтаксис цього асемблера трохи дивакуватий, що суміщає
риси, характерні і для Intel і для AT & T, але після кількох тижнів мук
можна звикнути. p>
Завантажувальний
сектор (boot.S) 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 МБ це кількість дорівнює 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>
Тепер
необхідно налаштувати як слід сегменти для даних (es, ds) і для стека.
Неприємно, звичайно, що все доводиться робити вручну, але що поробиш - адже
крім нас і BIOS в пам'яті комп'ютера нікого немає. p>
go: p>
mov ax, # 0xF0 p>
mov ss, ax p>
mov sp, ax p>
; Стек розмістимо
як 0xF0: 0xF0 = 0xFF0 p>
mov ax, # 0x60 p>
; Сегменти для
даних 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>
; щоб не
виводити повідомлення де попало. p>
xor bh, bh p>
int 0x10 p>
pop cx p>
mov bx, # 0x0007 p>
; Параметри
символів, що виводяться: p>
; відеосторінок
0, атрибут 7 (сірий на чорному) p>
mov ax, # 0x1301 p>
; Виводимо рядок
і Зрушуємо курсор 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. Ядро 32-розрядне. Воно буде у нас розміщуватися окремо від
завантажувального сектора і збиратися вже за допомогою gcc і gas. Синтаксис асемблера
gas відповідає вимогам AT & T, так що тут все буде простіше. Але для
Спершу нам потрібно прочитати ядро. Знову скористаємося готової функцією 0x2
переривання 0x13. p>
recalibrate: p>
mov ah, # 0 p>
mov dl, # FLOPPY_ID p>
int 0x13 p>
; проведемо
Реініціалізація дисководу. p>
jc recalibrate p>
call read_track p>
; виклик функції
читання ядра p>
jnc next_work p>
; якщо під час
читання не відбулося p>
; нічого
поганого, то працюємо далі p>
bad_read: p>
; якщо читання
відбулося невдало - p>
; виводимо
повідомлення про помилку p>
mov bp, # error_read_msg p>
mov cx, 7 p>
call write_message p>
inf1: jmp inf1 p>
; і йдемо в
нескінченний цикл. Тепер p>
; нас врятує
тільки ручна перезавантаження p>
Сама функція
читання гранично проста: довго і нудно заповнюємо параметри, а потім одним
махом зчитуємо ядро. Труднощі почнуться, коли ядро перестане міститися в 17
секторах (тобто 8.5КБ); але це поки що в майбутньому, а зараз цілком достатньо
такого блискавичного читання p>
read_track: p>
pusha p>
push es p>
push ds p>
mov di, # SYSSEG p>
; Визначаємо p>
mov es, di p>
; адреса буфера
для даних p>
xor bx, bx p>
mov ch, # START_TRACK p>
; доріжка 0 p>
mov cl, # START_SECTOR p>
; починаючи з
сектора 2 p>
mov dl, # FLOPPY_ID p>
mov dh, # START_HEAD p>
mov ah, # 2 p>
mov al, # SYSSIZE p>
; вважати 10
секторів p>
int 0x13 p>
pop ds p>
pop es p>
popa p>
ret p>
; Ось і все.
Ядро успішно прочитано, p>
; і можна
вивести ще одне радісне p>
; повідомлення на
екран. p>
next_work: p>
call kill_motor p>
; зупиняємо
привід дисководу p>
mov
bp, # load_msg p>
; виводимо
сполучення 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" і лампочка приводу
флоппі-дисків погашена. Всі затихли і готові до смертельного номеру - стрибка в
захищений режим. Для початку треба включити адресну лінію A20. Це в точності
означає, що ми будемо використовувати 32-розрядну адресацію до даних. p>
mov al, # 0xD1 p>
; команда запису
для 8042 p>
out # 0x64, al p>
mov al, # 0xDF p>
; включити 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>
Поки у нас ще
живий BIOS, запам'ятаємо позицію курсору і збережемо її у відомому місці (0000:0 x8000
). Ядро пізніше забере всі дані і буде їх використовувати для виведення на екран
переможного повідомлення. p>
save_cursor: p>
mov ah, # 0x03 p>
; читаємо поточну
позицію курсору p>
xor bh, bh p>
int 0x10 p>
seg cs p>
mov [0x8000], dx p>
; зберігаємо в
спеціальному тайнику p>
Тепер
увагу, забороняємо переривання (нема чого відволікатися під час такої роботи) і
завантажуємо таблицю дескрипторів p>
cli p>
lgdt GDT_DESCRIPTOR p>
; завантажуємо
описувач таблиці дескрипторів. 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 p>
; Номер 0:
порожній дескриптор p>
. word 0xFFFF, 0
p>
; Номер 8:
дескриптор коду p>
. byte 0, CODE_ARB, 0xC0, 0 p>
. word 0xFFFF, 0 p>
; Номер 0x10:
дескриптор даних p>
. byte 0, DATA_ARB, 0xCF, 0 p>
Перехід в
захищений режим може відбуватися мінімум двома способами, але обидві ОС,
обрані нами для прикладу (Linux і Thix) використовують для сумісності з 286
процесором команду lmsw. Ми будемо діяти тим же способом p>
mov ax, # 1 p>
lmsw ax p>
; прощай
реальний режим. Ми тепер p>
; знаходимося в
захищеному режимі. p>
jmpi 0x1000, 8 p>
; Затяжний
стрибок на 32-розрядне ядро. p>
Ось і вся
робота завантажувального сектора - не мало, але й не багато. Тепер з ним ми
попрощаємося і попрямуємо до ядра. Наприкінці асемблерні файлу корисно додати
наступну інструкцію. p>
org 511 p>
end_boot:. byte 0 p>
У результаті
скомпільований код буде займати рівно 512 байт, що дуже зручно для
підготовки образу завантажувального диска. p>
Перші зітхання
ядра (head.S) 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>
викличемо
довгоочікувану функцію, вже написану на С: call SYMBOL_NAME (start_my_kernel). І
більше нам тут робити нічого. p>
Поговоримо на
мові високого рівня (start.c) p>
Ось тепер ми
повернулися до того, з чого починали розповідь. Майже повернулися, тому що 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>
Робота ведеться
через введення/виведення в відеопортів */ 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>
використовуючи
пряму запис у відеопам'ять */ 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 + +)! = '