Настоящий «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 < lines * cols * 2; i += 2 )

vidmem[i] = ‘ ‘;

}

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

static void puts(const char *s)

{

int x,y;

char c;

x = curr_x;

y = curr_y;

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

if ( c == ‘n’ ) {

x = 0;

if ( ++y >= lines ) {

scroll();

y—;

}

} else {

vidmem [ ( x + cols * y ) * 2 ] = c;

if ( ++x >= cols ) {

x = 0;

if ( ++y >= lines ) {

scroll();

y—;

}

}

}

}

gotoxy(x,y);

}

/*функция копирования из одной области памяти

в другую. Заменитель стандартной функции glibc */

void* memcpy(void* __dest, __const void* __src,

unsigned int __n)

{

int i;

char *d = (char *)__dest, *s = (char *)__src;

for (i=0;ifloppy.img

Образ готов. Записываем на дискетку (заготовьте несколько для экспериментов, я прикончил три штуки), перезагружаем компьютер и наслаждаемся…

cat floppy.img >/dev/fd0

Ё-мое, что ж я сделал… :-[ ]

Здорово, правда? Приятно почувствовать себя будущим Торвальдсом, или кем-то еще. Первая тропка протоптана, можно смело идти вперед — дописывать и переписывать систему!… Описанная процедура пока что едина для множества операционных систем, будь то UNIX или Windows. Что напишете вы? … не знает никто. Ведь это будет уже ваша система…

Список литературы

Для подготовки данной работы были использованы материалы с сайта http://andrey.nnov.ru/