Настоящий Hello World
Показательно, но совершенно неинтересно. Программа, конечно, работает, приветствие свое пишет; но ведь для этого требуется целая операционная система! А что если хочется написать программку, для которой ничего не надо? Вставляем дискетку в компьютер, загружаемся с нее и … «Hello World » ! Можно даже прокричать это приветствие из защищенного режима… Сказано — сделано. С чего бы начать?.. Набраться… Читать ещё >
Настоящий Hello World (реферат, курсовая, диплом, контрольная)
Настоящий «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:0×600, перейдем туда и спокойно продолжим работу. Собственно вся наша работа будет состоять из загрузки ядра (сектора 2 — 12 первой дорожки дискеты) по адресу 0×100:0000, переходу в защищенный режим и скачку на первые строки ядра. В связи с этим еще несколько констант:
BOOTSEG = 0x7c00 — Сюда поместит загрузочный сектор BIOS.
INITSEG = 0×600 — Сюда его переместим мы.
SYSSEG = 0×100 — А здесь приятно расположится наше ядро.
DATA_ARB = 0×92 — Определитель сегмента данных для дескриптора.
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, #0×100.
repnz.
movsw.
jmpi go, #0.
Теперь необходимо настроить как следует сегменты для данных (es, ds) и для стека. Неприятно, конечно, что все приходится делать вручную, но что поделаешь — ведь кроме нас и BIOS в памяти компьютера никого нет.
go:
mov ax, #0xF0.
mov ss, ax.
mov sp, ax.
;Стек разместим как 0xF0:0xF0 = 0xFF0.
mov ax, #0×60.
;Сегменты для данных ES и DS зададим в 0×60.
mov ds, ax.
mov es, ax.
Наконец, можно вывести победное приветствие. Пусть мир узнает, что мы смогли загрузиться! Поскольку у нас есть все-таки целый BIOS, воспользуемся готовой функцией 0×13 прерывания 0×10. Можно, конечно, его презреть и написать напрямую в видеопамять, но у нас каждый байт команды на счету, а байт таких всего 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,#0×03.
;прочитаем текущее положение курсора,.
;дабы не выводить сообщения где попало.
xor bh, bh.
int 0×10.
pop cx.
mov bx,#0×0007.
;Параметры выводимых символов:
;видеостраница 0, атрибут 7 (серый на черном).
mov ax,#0×1301.
;Выводим строку и сдвигаем курсор
int 0×10.
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, так что тут все будет попроще. Но для начала нам нужно прочитать ядро. Опять воспользуемся готовой функцией 0×2 прерывания 0×13.
recalibrate:
mov ah, #0.
mov dl, #FLOPPY_ID.
int 0×13.
;проведем реинициализацию дисковода.
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 0×13.
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 #0×64, al.
mov al, #0xDF.
;включить A20.
out #0×60, 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:0×8000). Ядро позже заберет все данные и будет их использовать для вывода на экран победного сообщения.
save_cursor:
mov ah,#0×03.
;читаем текущую позицию курсора.
xor bh, bh.
int 0×10.
seg cs.
mov [0×8000], dx.
;сохраняем в специальном тайнике Теперь внимание, запрещаем прерывания (нечего отвлекаться во время такой работы) и загружаем таблицу дескрипторов.
cli.
lgdt GDT_DESCRIPTOR.
;загружаем описатель таблицы дескрипторов.
У нас таблица дескрипторов состоит из трех описателей: нулевой (всегда должен присутствовать), сегмента кода и сегмента данных.
align 4.
.word 0.
GDT_DESCRIPTOR: .word 3 * 8 — 1 ;
;размер таблицы дескрипторов.
.long 0×600 + GDT.
;местоположение таблицы дескрипторов.
.align 2.
GDT:
.long 0, 0.
;Номер 0: пустой дескриптор
.word 0xFFFF, 0.
;Номер 8: дескриптор кода.
.byte 0, CODE_ARB, 0xC0, 0.
.word 0xFFFF, 0.
;Номер 0×10: дескриптор данных.
.byte 0, DATA_ARB, 0xCF, 0.
Переход в защищенный режим может происходить минимум двумя способами, но обе ОС, выбранные нами для примера (Linux и Thix) используют для совместимости с 286 процессором команду lmsw. Мы будем действовать тем же способом.
mov ax, #1.
lmsw ax.
;прощай реальный режим. Мы теперь.
;находимся в защищенном режиме.
jmpi 0×1000, 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, 0×0.
cmpl %eax, 0×100 000.
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++) ≠ «.