При закрытии подписчики были переданы в рассылку "Для бухгалтера: программы, новости, советы" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
| Информационный Канал 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 |
Отписаться
Убрать рекламу |
| В избранное | ||