При закрытии подписчики были переданы в рассылку "Для бухгалтера: программы, новости, советы" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
Информационный Канал Subscribe.Ru |
Сегодня в номере:
Сегодня мы впервые используем этот замечательный язык в разработке нашей системы. Как вы помните, написанный в предыдущем выпуске загрузчик обладает возможностью загружать и выполнять код, находящийся в файле kernel.bin
и скомпонованный по адресу 0x200000
(очевидно, что эти два параметра элементарным образом можно изменить, но, в дальнейшем, я буду предполагать, что они равны первоначальным значениям). Запуск файла производится копированием его по адресу 0x200000
и передачей управления на начало кода. Отсюда следует, что ни один из форматов исполняемых файлов (за исключением "сырого" бинарного) нам не подходит - наш загрузчик не умеет распознавать заголовок файла.
Но нам и не обязательно использовать формат вроде ELF
или PE
- компоновщик ld
, который я буду использовать, поддерживает создание сырых бинарников с помощью опции --oformat binary
. Второй важный параметр, который нам понадобится - указание адреса, по которому будет произведена компоновка. Фактически все наше ядро располагается в сегменте кода, поэтому необходимо указать только его адрес: -Ttext 0x200000
(так уж повелось, что секция (сегмент) кода называется 'text').
И еще один момент: при создании объектного кода, который в дальнейшем будет компоновкой приведен к бинарному, мы указываем GCC
опцию -ffreestanding
. Теперь он не будет делать глупых предположений по поводу функций стандартной библиотеки (например не будет предполагать, что printf
- это именно printf (const char *fmt, ...)
)
А теперь пришло время рассказать о первой ловушке, которая будет нас подстерегать. Представим например, что наше ядро выглядит вот так:
void main() { printf ("Hello World!\n"); }
Что сделает GCC
при компиляции этого файла? Он вынесет константу "Hello World!\n" в начало файла. Получится, что в начале kernel.bin
(а ведь туда мы и передаем управление!) у нас будет не код, а непонятно что. Работать оно конечно не будет
Избавиться от этого просто - достаточно при компоновке кода первым указывать компоновщику файл, который наверняка скомпилировался должным образом (например, полученный из ассемблерного исходника). Выглядеть этот "переходник" может достаточно просто:
[BITS 32] [EXTERN kernel_main] [GLOBAL _start] _start: mov esp, 0x200000-4 call kernel_main
Перед тем, как отдать управление функции kernel_main
мы к тому же и устанавливаем стек (конечно, не обязательно делать это вот здесь - в самый последний момент, можно и в загрузчике).
Вторая трудность, которая нас ожидает - отсутствие функций стандартной библиотеки. Конечно, как только мы получили возможность использовать Си, так и чешутся руки вывести на экран какой-нибудь хелловорлд, но... нечем. Обычно, функция printf
бралась из библиотеки Си операционной системы, ну а сейчас ей взяться неоткуда. Будем писать ее сами, ну а заодно и несколько других функций, без которых жизнь наша будет подобна ночному кошмару. Та часть функций, которые мы напишем и будем использовать сегодня, предназначена для функционирования телетайпного устройства (tty). Еще раз напомню, что в нем:
1) можно использовать контрольные символы (например '\n') для перемещения курсора
2) автоматически производится контроль за экраном (например при выходе курсора за границы экрана происходит сдвиг экрана вверх).
Создать tty-устройство на базе видеопамяти несложно и ниже приведена его реализация:
#define VIDEO_WIDTH 80 //ширина экрана #define VIDEO_HEIGHT 25 //высота экрана #define VIDEO_RAM 0xb8000 //адрес видеопамяти int tty_cursor; //положение курсора int tty_attribute; //текущий аттрибут символа //Инициализация tty void init_tty() { tty_cursor = 0; tty_attribute = 7; } //Смена текущего аттрибута символа void textcolor(char c) { tty_attribute = c; } //Очистка экрана void clear() { char *video = VIDEO_RAM; int i; for (i = 0; i < VIDEO_HEIGHT*VIDEO_WIDTH; i++) { *(video + i*2) = ' '; } tty_cursor = 0; } //Вывод одного символа в режиме телетайпа void putchar(char c) { char *video = VIDEO_RAM; int i; switch (c) { case '\n': //Если это символ новой строки tty_cursor+=VIDEO_WIDTH; tty_cursor-=tty_cursor%VIDEO_WIDTH; break; default: *(video + tty_cursor*2) = c; *(video + tty_cursor*2+1) = tty_attribute; tty_cursor++; break; } //Если курсор вышел за границу экрана, сдвинем экран вверх на одну строку if(tty_cursor>VIDEO_WIDTH*VIDEO_HEIGHT){ for(i=VIDEO_WIDTH*2;i<=VIDEO_WIDTH*VIDEO_HEIGHT*2+VIDEO_WIDTH*2;i++){ *(video+i-VIDEO_WIDTH*2)=*(video+i); } tty_cursor-=VIDEO_WIDTH; } } //Вывод строки, заканчивающейся нуль-символом void puts(const char *s) { while(*s) { putchar(*s); s++; } }
А теперь о том, как заставить это все работать. Предположим, что tty у вас находится в файле ktty.c
, "переходник" для си-кода - в startup.asm
, а главная функция ядра такого содержания:
void kernel_main() { init_tty(); clear(); puts("We use C, isn't this great?\n"); for(;;); }находится в файле
kernel.c
Сборка ядра производится таким образом:
gcc -ffreestanding -c -o ktty.o ktty.c
gcc -ffreestanding -c -o kernel.o kernel.c
nasm -felf -o startup.o startup.asm
ld --oformat binary -Ttext 0x200000 -o kernel.bin startup.o ktty.o kernel.o
Не правда ли, набивать все это в консоли достаточно утомительно? О том как процесс сборки упростить - читайте в следующем абзаце.
Сохраните вышеприведенный текст в файле 'Makefile' в той же директории, где находятся остальные файлы системы. Теперь для сборки достаточно запустить программу Как видите, В теоретической части этого выпуска мы поговорим об исключениях, присутствующих в процессорах IA-32. Ниже приведена полная их таблица для процессора Pentium 4 (примечание: все исключения обратно-совместимы).Программа 'Make'
Программа 'make' может управлять компиляцией основываясь на зависимостях (например: объектный файл kernel.o
зависит от исходного файла kernel.c
). Лучше всего принцип ее действия рассмотреть на конкретном Makefile
(он будет предназначен для компиляции кода рассмотренного выше).
#Определим переменные OBJECTS и CFLAGS
#(к ним можно будет обращаться как $(OBJECTS) и $(CFLAGS)
OBJECTS=startup.o kernel.o ktty.o
CFLAGS=-ffreestanding -c
#цель 'all' - "глобальная" цель всех наших пертурбаций -
#получение образа дискеты image.bin из файла bootsect.asm
#(я предполагаю, что файл sb.bin был вставлен в конце bootsect.asm
#директивой incbin 'sb.bin')
#для успешного выполнения 'all' должны присутствовать файлы
#bootsect.asm и sb.bin (они указываются после двоеточия)
#(это и есть зависимости)
#действие для выполнения (nasm -fbin -o image.bin bootsect.asm)
#указано строчкой ниже
all: bootsect.asm sb.bin
nasm -fbin -o image.bin bootsect.asm
#для создания sb.bin нам нужны sb.asm и kernel.bin
sb.bin: sb.asm kernel.bin
nasm -fbin -o sb.bin sb.asm
#для создания kernel.bin нам нужны файлы, указанные в переменной OBJECTS
kernel.bin: $(OBJECTS)
ld --oformat binary -Ttext 0x200000 -o kernel.bin $(OBJECTS)
#для создания startup.o нам нужен только startup.asm
startup.o: startup.asm
nasm -felf -o startup.o startup.asm
#для создания kernel.o - нужен kernel.c
kernel.o: kernel.c
gcc $(CFLAGS) -o kernel.o kernel.c
#ну а для ktty.o - ktty.c
ktty.o: ktty.c
gcc $(CFLAGS) -o ktty.o ktty.c
make
находясь в этой же директории. Вот как будет выглядеть результат запуска:
nasm -felf -o startup.o startup.asm
gcc -ffreestanding -c -o kernel.o kernel.c
gcc -ffreestanding -c -o ktty.o ktty.c
ktty.c: In function clear':
ktty.c:18: warning: initialization makes pointer from integer without a cast
ktty.c: In function putchar':
ktty.c:31: warning: initialization makes pointer from integer without a cast
ld --oformat binary -Ttext 0x200000 -o kernel.bin startup.o kernel.o ktty.o
nasm -fbin -o sb.bin sb.asm
sb.asm:151: warning: uninitialised space declared in .text section: zeroing
nasm -fbin -o image.bin bootsect.asm
make
поочередно провела все этапы компиляции, причем именно в том порядке, какой был необходим (т.к. она руководствовалась указанными в Makefile зависимостями) Исключения IA-32
Номер исключения | Описание |
0 |
Деление на нуль (Divide Error Exception или #DE ) Тип исключения: fault |
1 |
Отладочное прерывание (Debug Exception или #DB) Тип исключения: trap или fault - смотря как вызвано |
2 | NMI - немаскируемое прерывание |
3 |
Точка останова (Breakpoint Exception или #BP) Тип: trap Примечание: используется отладчиками, т.к. инструкция INT3 занимает один байт (0xCC )
|
4 |
Переполнение (Overflow Exception или #OF) Тип: trap Примечание: вызвается если при выполнении инструкции INTO (Interrupt on overflow) флаг переполнения OF установлен
|
5 |
Выход за допустимые границы при BOUND (Bound Range Exceeded Exception или #BR) Тип: fault Примечание: вызвается если операнд инструкции BOUND выходит за границы массива
|
6 |
Неправильная инструкция (Invalid Opcode Exception или #UD) Тип: fault Примечание: вызывается при попытке выполнить несуществующую инструкцию или инструкцию с недопустимыми операндами |
7 |
Математический сопроцессор не доступен (No Math или #NM). Тип: fault Примечание: вызывается при попытке выполнить инструкцию FPU, если его использование запрещено (проверяются несколько флагов CR0) |
8 |
Двойная ошибка (Double Fault Exception или #DF) Тип: abort Примечание: вызывается, если при вызове обработчика для исключения случилось еще одно исключение. Если при вызове #DF опять случится исключение, то процессор получит сигнал RESET# (обычно это приводит к перезагрузке системы). |
9 |
Зарезервировано Примечание: на 386 это было Coprocessor Segment Overrun |
10 (0xA) | Ошибочный TSS (Invalid TSS или #TS) |
11 (0xB) |
Несуществующий сегмент (Segment Not Present или #NP) Тип: fault Примечание: вызывается при обращении к сегменту (или дескриптору какого-либо шлюза), бит P которого установлен в 0. |
12 (0xC) |
Ошибка стека (Stack Fault Exception или #SS) Тип: fault Примечание: вызывается при превышении лимита сегмента стека или загрузке несуществующего (P=0) дескриптора в SS |
13 (0xD) |
Общее исключение защиты (General Protection Exception или #GP) Тип: fault Примечание: основное исключение защищенного режима. Не перечесть всех случаев в которых оно вызывается :) |
14 (0xE) |
Ошибка страничной адресации (Page Fault Exception или #PF) Тип: fault Примечание: в регистре CR2 находится адрес, обращение к которому вызвало ошибку |
15 (0xF) | Зарезервировано |
16 (0x10) |
Ошибка сопроцессора (FPU Error или #MF) Тип: fault |
17 (0x11) |
Ошибка выравнивания (Alignment Check Exception или #AC) Тип: fault Примечание: вызывается при невыровненном обращении к памяти непривилегированным (CPL=3) кодом если установлены флаги AC в EFLAGS и AM в CR0
|
18 (0x12) |
Машинно-зависимая ошибка (Machine Check Exception или #MC) Тип: abort |
19 (0x13) |
Ошибка SSE/SSE2 (SIMD Floating Point Exception или #XF) Тип: fault |
20 - 31 (0x14 - 0x1F) | Зарезервированы |
На сегодня все, уважаемые подписчики.
Как всегда, мой почтовый ящик открыт для вас: lonesome@lowlevel.ru
Также вы можете задавать интересующие вас вопросы в форуме lowlevel.ru
Предыдущие выпуски рассылки вы можете найти по этому адресу:
http://subscribe.ru/archive/comp.soft.prog.osdev
Всего наилучшего!
Lonesome
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||