Сайт Андрея Зайчикова
|
|
Настоящий "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, переходу в защищенный режим и скачку на первые строки ядра. В связи с этим еще несколько констант:
Первым делом произведем перемещение самих себя в более приемлемое место. 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. Что напишете вы? ... не знает никто. Ведь это будет уже ваша система...
|