Главная
 Сайт Андрея Зайчикова
Пятница, 1 Февраля 2008г. 
Карта сайта Поиск по сайту Написать письмо  
 .:Навигатор 
Новости
Библиотека
Статьи
Олимпиады
FAQ (ЧаВо)
Гостевая книга 
Ссылки
 .:Информация 


Настоящий "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++ ) != '\0' ) {
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;i<__n;i++) d[i] = s[i];
}

/*функция, издающая долгий и протяжный звук.
Использует только ввод/вывод в порты поэтому
очень полезна для отладки */
make_sound()
{
__asm__("
movb $0xB6, %al\n\t
outb %al, $0x43\n\t
movb $0x0D, %al\n\t
outb %al, $0x42\n\t
movb $0x11, %al\n\t
outb %al, $0x42\n\t
inb $0x61, %al\n\t
orb $3, %al\n\t
outb %al, $0x61\n\t
");
}

/*А вот и основная функция*/
int start_my_kernel()
{

/*задаются основные параметры */
vidmem = (char *) 0xb8000;
vidport = 0x3d4;
lines = 25;
cols = 80;

/*считываются предусмотрительно сохраненные
координаты курсора*/
curr_x=*(unsigned char *)(0x8000);
curr_y=*(unsigned char *)(0x8001);

/*выводится строка*/
puts("done\n");

/*уходим в бесконечный цикл*/
while(1);
}

Вот и вывели мы этот "Hello World" на экран. Сколько проделано работы, а на экране только две строчки:

Booting data ...done
Go to proteсted mode ...done

А что – плохо?! Закричала новая операционная система. Мир с радостью воспринял ее. Кто знает, может быть - это новый Linux ?...

Подготовка загрузочного образа(floppy.img)
Теперь подготовим загрузочный образ нашей системки.Для начала соберем загрузочный сектор.

as86 -0 -a -o boot.o boot.S
ld86 -0 -s -o boot.img boot.o

Обрежем 32-битный заголовок и получим таким образом чистый двоичный код.

dd if=boot.img of=boot.bin bs=32 skip=1

Соберем ядро

gcc -traditional -c head.S -o head.o
gcc -O2 -DSTDC_HEADERS -c start.c

При компоновке НЕ ЗАБУДЬТЕ параметр "-T"! Он указывает, относительно какого смещения вести расчеты; в нашем случае, поскольку ядро грузится по адресy 0x1000, смещение соответствующее:

ld -m elf_i386 -Ttext 0x1000 -e startup_32 head.o start.o -o head.img

Отделим зерна от плевел, то есть чистый двоичный код от всяческих служебных заголовков и комментариев:

objcopy -O binary -R .note -R .comment -S head.img head.bin

И соединим воедино загрузочный сектор и ядро

cat boot.bin head.bin >floppy.img

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

cat floppy.img >/dev/fd0

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

 
 © Андрей Зайчиков