Настоящий
"Hello World"
Станислав
Иевлев
С чего
начинается изучение нового языка (или среды) программирования? С написания
простенькой программы, выводящей на экран краткое приветствие типа "Hello
World!". Например, для C это будет выглядеть приблизительно так:
main() {
printf("Hello
World!n");
}
Показательно,
но совершенно неинтересно. Программа, конечно, работает, приветствие свое
пишет; но ведь для этого требуется целая операционная система! А что если
хочется написать программку, для которой ничего не надо? Вставляем дискетку в
компьютер, загружаемся с нее и ..."Hello World"! Можно даже
прокричать это приветствие из защищенного режима... Сказано - сделано. С чего
бы начать?.. Набраться знаний, конечно. Для этого очень хорошо полазить в
исходниках Linux и Thix. Первая система всем хорошо знакома, вторая менее
известна, но не менее полезна.
Подучились?
Теперь займемся. Понятно, что первым делом надо написать загрузочный сектор для
нашей мини-операционки (а ведь это будет именно мини-операционка!). Поскольку
процессор грузится в 16-разрядном режиме, то для создания загрузочного сектора
используется ассемблер и линковщик из пакета bin86. Можно, конечно, поискать
еще что-нибудь, но оба наших примера используют именно его; и мы тоже пойдем по
стопам учителей. Синтаксис этого ассемблера немного странноватый, совмещающий
черты, характерные и для Intel и для AT&T, но после пары недель мучений
можно привыкнуть.
Загрузочный
сектор (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 МБ это количество равно 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
Теперь
необходимо настроить как следует сегменты для данных (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КБ); но это пока в будущем, а сейчас вполне достаточно
такого молниеносного чтения
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 байт, что очень удобно для
подготовки образа загрузочного диска.
Первые вздохи
ядра (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). И
больше нам тут делать нечего.
Поговорим на
языке высокого уровня (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++ ) != '