В самой первой части нашей серии «Как запустить программу без операционной системы» мы остановились на том, что загрузили ядро с помощью GRUB’а и распечатали на экран классический “Hello World!”. Теперь мы покажем, как можно использовать прерывания BIOS’а в ядре ОС. А для начала — рассмотрим, что же такое прерывания BIOS’а, для чего они используются, и почему возникают проблемы с их вызовом.
Немного о прерываниях
Прерывание – это сигнал, сообщающий процессору о возникновении какого-либо события. Прерывания можно разбить на 2 группы:
• внешние прерывания – генерируются устройствами и другими процессорами;
• внутренние прерывания – генерируются процессором при возникновении каких-либо исключительных ситуаций (например, деление на 0 или обращение к недопустимым адресам) или инструкцией int.
В общем случае, после возникновения прерывания процессор должен передать управление обработчику прерывания, из таблицы, на которую указывает специальный регистр IDTR. В зависимости от режима работы процессора формат таблицы может отличаться и при вызове могут происходить дополнительные проверки на корректность прерывания.
После включения питания процессор начинает свою работу в режиме, очень похожим на Real Mode. Одним из этапов инициализации процессора является передача управления BIOS. BIOS настраивает некоторые регистры CPU, инициализирует оперативную память, выполняет проверку устройств POST, инициализацию базового оборудования, копирование загрузчика в память и передачу ему управления. Одним из шагов BIOS является настройка таблицы обработчиков прерываний, адрес которой хранится в IDTR. Обычно это 0x0. В реальном режиме работы процессора (Real Mode) запись в таблице прерываний состоит из пары sel:offset, которая содержит адрес обработчика прерывания. BIOS устанавливает собственные обработчики прерываний, что бы избавить операционную систему от низкоуровневой работы с аппаратурой, которая может отличаться от машины к машине.
Прерывания BIOS выступают в роли интерфейса для работы с оборудованием. К примеру, прерывание 0x13 используется для чтения секторов с дисков, прерывание 0х10 — для настройки видеорежимов. Для вызова прерываний BIOS’а, программа, работающая в Real Mode, может использовать ассемблерную инструкцию int. Например, чтобы прочитать сектор с диска, нужно использовать инструкцию int 0x13, с параметрами в регистрах общего назначения.
В реальном режиме работы процессор не может обращаться к памяти выше мегабайта, и у него отсутствуют механизмы для изоляции выполняющихся задач (кроме сегментации). Поэтому современные операционные системы для платформы x86 выполняются в других режимах работы процессора, таких как защищенный режим (Protected Mode) и длинный режим (Long Mode).
В защищенном режиме (Protected Mode) таблица прерываний выглядит иначе. На нее по-прежнему указывает регистр IDTR, но записью в этой таблице является Gate Descriptor одного из 3х возможных типов (читаем пункт 6.11 мануала Intel). Таблица прерываний должна настраиваться операционной системой, а не BIOS-ом, следовательно, в защищенном режиме нет возможности использовать прерывания BIOS’а. Вся работа с устройствами (HDD, CD-ROM, видеокарта…) ложится на плечи операционной системы, которая использует для этого драйвера. В длинном режиме (Long Mode) ситуация точно такая же, с точностью до размера Gate Descriptor’а.
Способы вызова BIOS'а из защищенного режима
Ну а что делать, если ядру ОС, работающему в защищенном режиме, все же нужно прочитать что-то с диска (например, драйвер жесткого диска), а драйвер еще не загружен? Сделать это можно двумя способами.
1. Настроить VirtualMode86 и выполнять обращения к BIOS’у в Protected Mode.
2. Перейти в RealMode, обратиться к BIOS’у, вернуться в Protected Mode.
Virtual Mode 86(VM86) – это еще один режим работы процессора, в котором сегментная адресация аналогична реальному режиму, но при этом работает страничная адресация (paging) защищенного режима. Мы будем использовать второй способ, так как, применяя подобную технику, можно обращаться к BIOS’у и из 64х битного кода (который выполняется в LongMode, не поддерживающем переход в VM86). Оставим работу с диском на потом, а сейчас определим размер оперативной памяти с помощью BIOS.
Строго говоря, есть еще третий способ вызова BIOS’a из защищенного 32-го режима, и именно его мы применили в третьей части нашей серии для использования VBE. Это выполнение 16-ти битного кода BIOS’a в 32-х битном эмуляторе. Этот способ плох тем, что будет трудно доставлять в эмулятор прерывания, генерируемые внешними устройствами. При определении размера оперативной памяти внешние устройства не используются, так как BIOS уже все определил на этапе своей загрузки, и нам бы подошел этот метод для выполнения поставленной задачи, но все же воспользуемся 2-м способом, так как этот код еще пригодится нам в последующих статьях.
Отдельно нужно отметить, что выбранный нами способ является очень медленным по сравнению с работой драйвера, так как тратит много времени на переключение между режимами CPU и не использует такие технологии, как MMIO и DMA. Помимо этого, векторы прерываний у всех устройств должны быть настроены именно так, как этого ожидает BIOS, что может не выполняться, если в системе для отдельных устройств уже работают драйвера. Уже запущенные драйвера, при подобной передаче управления BIOS’у, будут терять прерывания, что может привести к проблемам. Все это значит, что действовать описанным образом можно только в начале работы ОС.
Разбираемся со вторым способом
Итак, наша цель: определить размер оперативной памяти по таблицам e820, которые мы получим с помощью прерывания BIOS’а int 0x15. Таблицы e820 являются картой физической памяти, которая описывает, какие диапазоны физической памяти доступны для использования операционной системой. В недоступных регионах физической памяти хранится основной BIOS и BIOS видеокарты, таблицы ACPI, некоторые диапазоны физической памяти замаплены в память устройств и их регистров. Если в Windows 7 запустить монитор ресурсов, и открыть вкладку «память», то «зарезервированное оборудование» включает в себя объем физической памяти, перекрытый зарезервированными диапазонами.
Для получения записи из таблицы E820 нужно записать в регистры следующие значения: в EBX значение 0, в EAX значение 0xE820, в ECX – размер буфера (не меньше 20-ти байт), в EDX значение 0x534d4150, в ES:DI нужно записать указатель на буфер. После вызова прерывания int0x15 в буфер будет записана одна запись из таблицы e820, а значение в EBX увеличится на 1. Прерывания нужно повторять, пока EBX снова не станет равным 0, что означает конец таблицы. После этого в буфере окажутся все значения из таблицы. При успешном завершении Carry Flag в регистре Eflags будет сброшен, а в EAX будет записано значение ‘SMAP’. Запись таблицы имеет следующий формат:
Тип равен “1”, если физическую память может использовать менеджер памяти, “2” – если память зарезервирована для устройств или BIOS-а, “3” или “4” – если память используется ACPI. Остальные значения зарезервированы, диапазоны не пересекаются. Пример карты памяти:
Сразу оговоримся, что GRUB, который мы используем в качестве загрузчика, может предоставить уже готовую карту физической памяти, но цель данной статьи – продемонстрировать обращение к BIOS из защищенного режима, а не использование возможностей GRUB.
После запуска компьютера CPU работает в режиме RealMode, в котором и происходит передача управления BIOS’у. BIOS волен работать, как ему заблагорассудится, и может перейти в защищенный режим, как это делает, к примеру, Coreboot. Выполнив свою работу, BIOS загружает в память и передает управление первому сектору наиболее приоритетного устройства в соответствии с порядком загрузки (по крайней мере, если это hdd или устройство эмулируется как hdd). BIOS передает управление, будучи в RM. В нашем случае загрузка происходит с hdd и первым сектором оказывается MBR, передающий управление GRUB’у. GRUB в основном работает в PM, но при необходимости обратиться к оборудованию (например, прочитать сектор с диска) переходит в RM и использует прерывания BIOS’а. GRUB передает управление нашему ядру в PM. На рисунке ниже изображена описанная последовательность переходов CPU между режимами работы в процессе загрузки.
Для продолжения разговора и перехода непосредственно к коду нужно сделать небольшой теоретический отступ в сторону защищенного режима. Тема обширная и хорошо освещена, так что здесь будет описано только то, что чуть позже мы увидим в коде. Основное отличие между RM и PM заключается в механизмах сегментной адресации памяти. А что вообще такое сегментная адресация памяти? В RM для обращения к памяти используются адреса длиной в 20 бит, но длина доступных регистров ограничена 16-ю битами (из-за этого код и называется 16-ти битным). Поэтому для адресации используются пары регистров, один из которых содержит базу сегмента, а второй смещение. Линейный адрес получается путем сложения смещения и базы сегмента, сдвинутой влево на 4. Сегментная адресация в RM продемонстрирована на рисунке ниже.
Смещение может храниться в любом регистре общего назначения. База сегмента хранится в одном из следующих регистров: CS, DS, SS, ES, FS, GS. Эти регистры называются селекторами. Для всех команд определены селекторы по умолчанию. Для команд PUSH, POP это SS (сегмент стека), для JUMP, LOOP это CS (сегмент кода), для MOV это DS (сегмент данных).
В PM тоже используется сегментная адресация памяти, но механизм сильно изменился. Теперь селектор не сам хранит базу сегмента, а ссылается на дескриптор, который хранится в одной из таблиц дескрипторов. Структура селектора показана на рисунке ниже (и взята из мануалов Intel).
• RPL (Requested Privilege Level) –используется для разделения уровня привилегий в сегментном механизме защиты.
• TI – указывает на тип таблицы дескрипторов, в которой находится искомый дескриптор. 1 – LDT (Local Descriptor Table), 0 – GDT (Global Descriptor Table).
• Index – индекс дескриптора в таблице дескрипторов.
Существует 2 типа таблиц дескриптора – GDT и LDT. Таблица GDT одна на всю систему, таблиц LDT может быть много (к примеру, своя на каждый процесс). Мы будем использовать GDT, так как для использования LDT в любом случае пришлось бы настраивать GDT. Дескрипторы можно разбить на 2 группы: системные и пользовательские. Пользовательские дескрипторы отвечают за сегменты. Системные дескрипторы описывают переходы процессора между уровнями привилегий. В нашем коде не будет системных дескрипторов, так что о них говорить не будем. Структура пользовательского дескриптора, представленная ниже (также взята из мануалов Intel), наводит на мысль о том, как давно появилась архитектура x86, и сколько доработок ей пришлось перетерпеть.
Дескриптор сегмента определяет тип сегмента, размер, уровень привилегий, необходимый для доступа к нему, права на чтение, запись и исполнение, базу сегмента. Разберем структуру дескриптора.
• Base Address – это 32-х битный адрес первого байта сегмента, поле разбито на 3 части base_0_15, base_16_23, base_24_31.
• Segment Limit – размер сегмента в байтах, если флаг G = 0, или в блоках по 4Кб, если флаг G = 1.
• G (granularity) – если флаг выставлен, то Segment Limit измеряется в блоках по 4Кб, иначе в байтах.
• S (descriptor type) – если флаг выставлен, то дескриптор пользовательский, иначе системный. В нашем коде у всех дескрипторов этот флаг установлен.
• Type – интерпретация этого поля зависит от флага S. Для пользовательского сегмента возможны 2 основных варианта: сегмент кода и сегмент данных, это определяется старшим битом поля. В таблице ниже представлены все возможные комбинации.
Для сегмента данных определены следующие биты: E (expansion-direction), (W) write-enable, (A) accessed. Бит (W) позволяет писать в сегмент, (E) используется для динамического расширения сегмента стека, (A) – общий бит для сегментов данных и кода, выставляется в 1 при обращении к сегменту, будь то чтение, запись или исполнение. В случае сегмента кода бит (E) интерпретируется как ©, а (W) как ®. Бит © conforming, отменяет часть проверок безопасности при вызове кода этого сегмента из другого сегмента. Бит ® read enable разрешает чтение из сегмента кода. Писать в сегмента кода в защищенном режиме нельзя.
• L (64-bit code segment) – выставляется, если сегмент содержит 64х-битный код. Флаг может быть выставлен в 1 только для сегментов кода.
• AVL (Available and reserved bits) – не используется процессором, может быть использован ОС.
• D/B (default operation size) – определяет разрядность пользовательских сегментов кода и данных. 16 бит, если флаг выставлен в 0 и 32, если в 1 (да, да, 16ти битный код в защищенном режиме тоже бывает).
• DPL (descriptor privilege level) – определяет уровень привилегий сегмента. Может принимать значения от 0 до 3, где 0 – самое привилегированное. Используется для ограничения доступа к сегменту.
Подробнее о структуре дескриптора можно прочитать в Intel System Programming Guide Part 1, в разделе 3.4.5. Там же можно найти описание того, как устроено разделение доступа к сегментам в соответствии с их уровнем привилегий. На Хабре есть хороший перевод на эту тему.
Вспомним, для чего мы все это начали – нам нужно вызывать прерывания BIOS из кода на C. Т.е. понадобится из кода на С перейти к коду в RM на ASM и затем обратно. Код на С выполняется в 32х-битном PM. План перехода будет выглядеть следующим образом:
Помимо прочего нужно передавать аргументы из кода на С коду в RM и результаты из кода в RM коду на С.
! ВАЖНО! Все дальнейшие действия могут успешно осуществляться только после успешного прохождения всех 6-ти шагов из первой части статьи “Как запустить программу без операционной системы”!
Итак, наш план:
1. Настроить собственную таблицу GDT, взамен той, что настроил GRUB.
2. Написать обертку для обращения к BIOS на С.
3. Добавить несколько общих функций.
4. Слепить все вместе и запустить.
Приступим!
Шаг 1. Инициализируем GDT
1. Добавим в папку include файл bitvisor-1.2\core\desc.h, взятый из проекта BitVisor. Код можно скачать тут. В файле содержится объявление структуры пользовательского дескриптора.
2. Добавим файл descriptor.c со следующим содержимым:
#include "types.h"
#include "desc.h"
#include "string.h"
static void SetSegDesc(struct segdesc *d, u32 limit, u32 base, enum segdesc_type type,
enum segdesc_s s, unsigned int dpl, unsigned int p,
unsigned int avl, enum segdesc_l l, enum segdesc_d_b d_b)
{
d->base_15_0 = base;
d->base_23_16 = base >> 16;
d->type = type;
d->s = s;
d->dpl = dpl;
d->p = p;
d->avl = avl;
d->l = l;
d->d_b = d_b;
d->base_31_24 = base >> 24;
if (limit <= 0xFFFFF)
{
d->g = 0;
d->limit_15_0 = limit >> 0;
d->limit_19_16 = limit >> 16;
}
else
{
d->g = 1;
d->limit_15_0 = limit >> 12;
d->limit_19_16 = limit >> 28;
}
}
void SetupDescTables(struct segdesc *GDT_base)
{
// SEG_SEL_NULL
memset(&GDT_base[0], 0, sizeof(GDT_base[0])); // нулевой сегмент. всегда 0
// SEG_SEL_CODE32
SetSegDesc(&GDT_base[1], 0xFFFFFFFF, 0x00000000, // 32х битный сегмент кода
SEGDESC_TYPE_EXECREAD_CODE, // уровень привилегий 0
SEGDESC_S_CODE_OR_DATA_SEGMENT, 0, 1, // база 0 лимит 4G
0, SEGDESC_L_16_OR_32, SEGDESC_D_B_32);
// SEG_SEL_DATA32
SetSegDesc(&GDT_base[2], 0xFFFFFFFF, 0x00000000, // 32х битный сегмент данных
SEGDESC_TYPE_RDWR_DATA, // уровень привилегий 0
SEGDESC_S_CODE_OR_DATA_SEGMENT, 0, 1, // база 0 лимит 4G
0, SEGDESC_L_16_OR_32, SEGDESC_D_B_32);
// SEG_SEL_CODE16
SetSegDesc(&GDT_base[3], 0x0000FFFF, 0x00000000, // 16ти битный сегмент кода
SEGDESC_TYPE_EXECREAD_CODE, // уровень привилегий 0
SEGDESC_S_CODE_OR_DATA_SEGMENT, 0, 1, // база 0 лимит 4G
0, SEGDESC_L_16_OR_32, SEGDESC_D_B_16);
// SEG_SEL_DATA16
SetSegDesc(&GDT_base[4], 0x0000FFFF, 0x00000000, // 16ти битный сегмент данных
SEGDESC_TYPE_RDWR_DATA, // уровень привилегий 0
SEGDESC_S_CODE_OR_DATA_SEGMENT, 0, 1, // база 0 лимит 4G
0, SEGDESC_L_16_OR_32, SEGDESC_D_B_16);
struct descreg gdtr;
gdtr.base = (ulong)GDT_base; // указатель на сформированную таблицу
gdtr.limit = 5 * sizeof(*GDT_base) - 1; // размер таблицы в байтах - 1
__asm__ volatile ("lgdt %0" // GCC-Inline-Assembly
:
: "m" (gdtr));
}
Для работы кода на С достаточно 2х пользовательских сегментов: 32х битный сегмент кода и 32х битный сегмент данных. Для перехода в 16ти битный код нам понадобятся два дополнительных сегмента: 16ти битные сегменты кода и данных. Функция SetupDescTables формирует по адресу *GDT_base таблицу GDT с пятью дескрипторами, первый из которых является нулевым, а оставшиеся 4 соответствуют описанным выше сегментам. Все сегменты имеют базу 0 и лимит 4G. Первый дескриптор в GDT всегда должен быть нулевым. Регистр GDTR, который указывает на GDT, инициализируется инструкцией lgdt. Для вызова инструкции применяется ассемблерная вставка со специфичным GCC синтаксисом. Ассемблерные вставки имеют структуру следующего вида:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
Использованная asm-вставка превращается в следующий код:
Строго говоря, для того, чтобы таблица GDT начала использоваться, нужно загрузить в регистры CS, SS, DS значения соответствующих селекторов. Но на данном этапе это не так критично.
3. Добавим в kernel.c вызов SetupDescTables и несколько объявлений. В результате получается следующее:
#include "printf.h"
#include "screen.h"
#include "types.h"
#include "desc.h"
#include "callrealmode.h"
struct segdesc g_GDT[5];
void SetupDescTables(struct segdesc *GDT_base);
void kmain(void)
{
clear_screen();
printf(" -- Kernel started! -- \n");
SetupDescTables(g_GDT);
u64 ram_size = GetAvalibleRAMSize ();
printf("ram_size = %llu(%lluMb)\n", ram_size, ram_size / 0x100000);
}
Вызов GetAvalibleRAMSize () возвращает размер оперативной памяти в байтах.
Шаг 2. Добавим несколько общих функций
1. Добавим в папку common файл bitvisor-1.2\core\string.s, в папку include файлы bitvisor-1.2\core\longmode.h и bitvisor-1.2\include\core\string.h из проекта BitVisor. В этих файлах находится реализация нескольких функций общего назначения, таких как memcpy и memset. Содержимое include\types.h заменить на следующее:
#ifndef _TYPES_H
#define _TYPES_H
#define NULL 0
typedef unsigned long size_t;
typedef unsigned long ulong;
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef unsigned long long u64;
#endif
Шаг 3. Обращение к BIOS
1. Добавим в include файл segment.h, содержащий значения селекторов для определенных в функции SetupDescTables дескрипторов.
#ifndef _SEGMENT_H
#define _SEGMENT_H
#define SEG_SEL_NULL 0
#define SEG_SEL_CODE32 (1 * 8) // Index = 1, TI = 0, RPL = 0
#define SEG_SEL_DATA32 (2 * 8) // Index = 2, TI = 0, RPL = 0
#define SEG_SEL_CODE16 (3 * 8) // Index = 3, TI = 0, RPL = 0
#define SEG_SEL_DATA16 (4 * 8) // Index = 4, TI = 0, RPL = 0
#endif
и файл callrealmode.h, с прототипом функции GetRamsize.
#ifndef _CALLREALMODE_H
#define _CALLREALMODE_H
#include "types.h"
u64 GetAvalibleRAMSize();
#endif
2. Добавим в корень нашего проекта файл callrealmode.c со следующим содержимым:
#include "printf.h"
#include "types.h"
#include "string.h"
#include "segment.h"
#include "callrealmode_asm.h"
// interrupts and paging must be disabled
static void callrealmode_Call(struct callrealmode_Data *p_param)
{
u16 sp16;
u32 sp32;
// copy 16 bit code and stack
// первый вызов memcpy копирует 16ти битный код между
// метками callrealmode_start и callrealmode_end
// по адресу CALLREALMODE_OFFSET < 1Mb. Это делается
// из за того что, код в RM не может обращаться к
// памяти выше 1Mb.
memcpy ((u8*)CALLREALMODE_OFFSET, &callrealmode_start,
&callrealmode_end - &callrealmode_start);
// перед кодом располагаем стек, который не будет затирать код, так как растет вниз.
// параметры для вызова в RM располагаются на стеке, и новое значение регистра SP
// вычисляется с учетом этого
sp16 = CALLREALMODE_OFFSET - sizeof(*p_param);
// второй вызов memcpy копирует параметры на стек
memcpy ((void*)(u32)sp16, p_param, sizeof(*p_param));
__asm__ volatile (
"mov %%esp,%0\n" // Сохраняем ресистр ESP в переменную sp32
"mov %1,%%ds \n" // Загружаем 16ти битный селектор данных в
"mov %1,%%es \n" // регистры DS, ES, FS, GS, SS
"mov %1,%%fs \n" //
"mov %1,%%gs \n" //
"mov %1,%%ss \n" //
"mov %2,%%esp\n" // Переходим на 16ти битный стек по адресу sp16
"pusha \n" // Сохраняем регистры общего назначения
"lcall %3,%4 \n" // Загружаем 16ти битный селектор кода в CS
// и переходим на адрес CALLREALMODE_OFFSET.
// На стеке сохраняется текущий CS и EIP,
// которые будут использованны инструкцией
// lretl для возврата в 32х битный код
"popa \n" // востанавливаем сохраненные регистры общего назначения
"mov %5,%%ds \n" // Загружаем 32х битный селектор данных в
"mov %5,%%es \n" // регистры DS, ES, FS, GS, SS
"mov %5,%%fs \n" //
"mov %5,%%gs \n" //
"mov %5,%%ss \n" //
"mov %0,%%esp\n" // Переходим на 32х битный стек, адрес
// которого сохранен в переменной sp32
: "=&a" (sp32) // %0 – Input
: "b" ((u32)SEG_SEL_DATA16) // %1 - Output
, "c" ((u32)sp16) // %2 - Output
, "i" ((u32)SEG_SEL_CODE16) // %3 - Output
, "i" (CALLREALMODE_OFFSET) // %4 - Output
, "d" ((u32)SEG_SEL_DATA32) // %5 - Output
);
// копируем с результаты работы с 16ти битного стека в p_param
memcpy (p_param, (void*)(u32)sp16, sizeof(*p_param));
}
u64 GetAvalibleRAMSize()
{
struct callrealmode_Data param; // структура, в которой передаются параметры
// для кода в RM, и возвращается результат
u64 avalible_ram_sz = 0;
param.func = CALLREALMODE_FUNC_GETSYSMEMMAP;
param.getsysmemmap.next_num = 0;
do
{
param.getsysmemmap.num = param.getsysmemmap.next_num;
callrealmode_Call(¶m); // int 0x15, где EBX = param.getsysmemmap.num
// EAX = 0xE820, EDX = 0x534d4150, ECX = 20
// ES:DI = ¶m.getsysmemmap.base
// после вызова EBX сохраняется
// param.getsysmemmap.next_num = EBX
// нас интересуют только доступные диапазоны с типом SYSMEMMAP_TYPE_AVAILABLE
if (SYSMEMMAP_TYPE_AVAILABLE == param.getsysmemmap.type)
{
avalible_ram_sz += param.getsysmemmap.len;
}
printf("n 0x%08X nn 0x%08X b 0x%08llX l 0x%08llX(%lldMb) t 0x%08X\n",
param.getsysmemmap.num,
param.getsysmemmap.next_num,
param.getsysmemmap.base,
param.getsysmemmap.len,
param.getsysmemmap.len / 0x100000,
param.getsysmemmap.type);
}
while (param.getsysmemmap.next_num);
return avalible_ram_sz;
}
Мы дошли до самого интересного! В данном коде присутствует 2 функции: GetRamsize и callrealmode_Call. Функция GetRamsize формирует структуру callrealmode_Data param для вызова callrealmode_Call. Функция callrealmode_Call непосредственно переходит в 16ти битный код на ассемблер. На ее основе можно написать и другие функции, которые обращаются к BIOS, например, функцию чтения сектора с диска. Единственным условием будет использование структуры callrealmode_Data .
Функция GetRamsize реализует в своей логике механизм получения карты физической памяти через прерывание int0x15, многократно вызывая функцию callrealmode_Call (аналог int0x15), пока param.getsysmemmap.next_num (он же EBX) не станет равным нулю. Функция callrealmode_Call использует две обрамляющие код на ассемблере метки callrealmode_start и callrealmode_end для копирования всего 16ти битного кода в нижний мегабайт по адресу CALLREALMODE_OFFSET = 0x5000. Адрес выбран так, чтобы при копировании не перетереть структур BIOS’a. Наибольший интерес в функции представляет ассемблерная вставка, она хорошо прокомментирована, поэтому просто покажем, во что она превратилась в скомпилированном виде:
3. Добавим файл callrealmode_asm.h в папку include, файл можно взять здесь , и файл callrealmode_asm.s в корень исходников, который можно взять здесь. Первый файл содержит определения структур, использованных в callrealmode.c. Второй фал содержит 16ти битный код на ассемблер, в котором выполняется переход в RM, вызов BIOS, возврат в PM и затем в код на С. Код подробно прокомментирован и в нем можно разобраться. Нужно отметить, что процедуры protection_off и protection_on, использующиеся для перехода между PM и RM, сильно упрощены. Они забывают о части регистров, таких как CR3, некоторые MSR, значения которых нужно сохранять и восстанавливать, так, как это происходит с GDTR и IDTR. Более полную реализацию этих функций можно найти в проекте BitVisor, а именно в файле bitvisor-1.2\core\callrealmode_asm.s.
Шаг 4. Последние доработки и запуск
1. Внесем изменения в makefile. Заменим
OBJFILES = \
loader.o \
common/printf.o \
common/screen.o \
kernel.o
на
OBJFILES = \
loader.o \
common/printf.o \
common/screen.o \
common/string.o \
kernel.o \
callrealmode.o \
callrealmode_asm.o \
descriptor.o
И строчку
as -o $@ $<
на
as -I include -o $@ $<
2. Пересоберем проект:
make rebuild
sudo make image
3. Запустим с опцией “–m”, которая позволяет явно указать размер оперативной памяти. Должно получиться что-то вроде следующего:
sudo qemu-system-i386 -hda hdd.img –m 123
Программа печатает все имеющиеся диапазоны памяти. Как и в предыдущих частях, можно сделать dd образа hdd.img на флешку и проверить код на реальном железе, загрузившись с нее.
Дальнейшие планы
В результате мы получили механизм, позволяющий обращаться к BIOS из кода на С. Кроме того, была затронута теоретическая часть, касающаяся работы защищенного режима. В будущем эту статью можно использовать в качестве отправной точки для демонстрации работы с файловой системой FAT32, но об этом в следующий раз!
Ссылка на следующую статью цикла:
"Как запустить программу без операционной системы: часть 6. Поддержка работы с дисками с файловой системой FAT"