Как запустить программу без операционной системы: часть 6. Поддержка работы с дисками с файловой системой FAT

в 8:51, , рубрики: fat16, fat32, howto, SMP, Блог компании НеоБИТ, системное программирование

В пятой части нашей серии статей мы показали, как можно использовать прерывания BIOS'а после перехода в защищенный режим, и в качестве примера определили размер оперативной памяти. Сегодня мы разовьем этот успех и реализуем полноценную поддержку работы с дисками с файловой системой FAT16 и FAT32. Работу с файлами на диске можно разбить на 2 части: работа с файловой системой и работа с диском на уровне чтения/записи секторов. Можно сказать, что для этого нам нужно написать «драйвер» файловой системы и «драйвер» диска.

Работа с диском на уровне чтения/записи секторов

Для начала научимся работать с диском.
Итак, мы можем вызывать прерывания BIOS'а. Помимо прочих возможностей, BIOS предоставляет интерфейс для работы с диском, а именно — прерывание int 0x13. Со списком сервисов, предоставляемых прерыванием, можно ознакомиться на википедии. Нас интересуют сервисы чтения и записи дисков.

Существует два способа адресации сектора на диске, с которыми работает BIOS – CHS(cylinder-head-sector) и LBA(logical block addressing). Адресация CHS основана на использовании геометрии диска, и адресом сектора является совокупность трех координат: цилиндр, головка, сектор. Способ позволяет адресовать до 8Гб. Прерывание int0x13 предоставляет возможность читать и писать на диск с использованием этой адресации.

Понятно, что 8Гб — это очень мало, и данный способ адресации является устаревшим, а все современные (и не очень) контроллеры жестких дисков поддерживают адресацию LBA. Адресация LBA абстрагируется от геометрии диска и присваивает каждому сектору собственный номер. Нумерация секторов начинается с нуля. LBA для задания номера блока использует 48 бит, что позволяет адресовать 128 ПиБ, с учетом размера сектора в 512 байт. Прерывание int0x13 предоставляет два сервиса для чтения и записи секторов на диск с использованием LBA. Их мы и будем использовать. Для чтения сектора прерывание in0x13 ожидает следующие параметры:

Как запустить программу без операционной системы: часть 6. Поддержка работы с дисками с файловой системой FAT - 1

Структура DAP:

Как запустить программу без операционной системы: часть 6. Поддержка работы с дисками с файловой системой FAT - 2

Прерывание возвращает следующие значения:

Как запустить программу без операционной системы: часть 6. Поддержка работы с дисками с файловой системой FAT - 3

Один из параметров – номер диска. Нужно как-то узнать номер диска, с которым мы собрались работать. Нумерация происходит следующим образом: флоппи-диски (fdd), и все, что эмулируется как флоппи, нумеруются с нуля, а жесткие диски (hdd), и все, что эмулируется как они(usb-флешки, например), нумеруются с 0x80. Этот номер никак не связан с последовательностью загрузки в настройках BIOS’а. В нашем случае, диск, с которым мы собираемся работать, является тем диском, с которого мы загрузились.

Когда BIOS передает управление MBR, он загружает его по адресу 0000h:7C00h, а в регистре DL передает нужный нам номер загрузочного устройства. Это является частью интерфейса взаимодействия между BIOS и MBR. Таким образом, этот номер попадает в GRUB, где далее используется для работы с диском. GRUB, в свою очередь, передает этот номер ОС как часть структуры Multiboot information.

Сразу после передачи управления от GRUB’а к ОС в регистре EBX находится указатель на эту структуру. Первое поле структуры – это flags, и если в нем выставлен 2-й бит, то поле boot_device корректно. Это поле так же принадлежит структуре Multiboot information и в его старшем байте (размер поля – 4 байта) хранится нужный нам номер диска, который понимает прерывание int0x13. Таким образом, используя GRUB, мы получили недостающий параметр для чтения/записи секторов на диск.

Мы научились читать и писать сектора на диск, это, безусловно, важно. Но файловая система привязана не к целому диску, а только к его части – разделу. Для работы с файловой системой нужно найти сектор, с которого начинается раздел, на котором она располагается. Информация об имеющихся на диске секторах хранится в первом секторе диска, там же, где располагается MBR. Существует много различных форматов MBR, но для всех них верна следующая структура:

Как запустить программу без операционной системы: часть 6. Поддержка работы с дисками с файловой системой FAT - 4

Информация о разделах хранится в таблице разделов. На диске может быть только 4 первичных раздела, с которых можно загрузиться. На запись о разделе приходится 8 байт. Первый байт — это флаги, если его значение 0x80, то раздел загрузочный. Код MBR в процессе своей работы пробегает по этим 4-м разделам в поиске загрузочного раздела. После его обнаружения, MBR копирует содержимое первого сектора этого раздела на адрес 0000h:7C00h и передает туда управление. Нас интересует LBA адрес первого сектора загрузочного раздела, так как именно на нем располагается наше ядро, и присутствует файловая система, которую мы собираемся читать. Для того чтобы получить этот адрес, нужно прочитать первый сектор диска, найти на нем таблицу разделов, в таблице разделов найти загрузочный раздел, а из его записи прочитать нужное поле.

Итак, у нас есть механизм для чтения сектора с диска и знание о расположении нужного нам раздела на диске. Осталось научиться работать с файловой системой на этом разделе.

Работа с файловой системой

Для работы с файловой системой мы будем использовать библиотеку fat_io_lib. Библиотека доступна под лицензией GPL. Она предоставляет интерфейс для работы с файлами и директориями, аналогичный имеющемуся в libc. Реализованы такие функции, как fopen(), fgets(), fputc(), fread(), fwrite() и т.д. Библиотека для своей работы требует всего лишь две функции: записать сектор и прочитать сектор, причем первая является необязательной. Функции имеют следующий прототип:

int media_read(uint32 sector, uint8 *buffer, uint32 sector_count);
int media_write(uint32 sector, uint8 *buffer, uint32 sector_count);
Return: int, 1 = success, 0 = failure.

Библиотека написана на чистом С, что опять-таки нам на руку. Для использования в своей мини-ОС нам не придется менять в ней ни строчки. Библиотека ожидает, что чтение секторов происходит в рамках раздела с файловой системой.

Итак, у нас есть функции чтения/записи сектора на раздел и есть библиотека для работы с FAT16/32, которая использует эти функции. Осталось собрать все воедино и продемонстрировать результат. Но прежде чем перейти к коду, хотелось бы показать, что подход, который мы собираемся использовать, вполне применим в реальной жизни. Ниже представлена небольшая часть VBR windows 7, в которой происходит чтение сектора диска посредством прерывания int0x13. Этот код многократно вызывается в процессе загрузки системы, вплоть до момента отрисовки загрузочной анимации.

Как запустить программу без операционной системы: часть 6. Поддержка работы с дисками с файловой системой FAT - 5

Для вызова этого кода, Windows 7, подобно тому, как это делаем мы, переходит из защищенного режима в реальный, и обратно. Это несложно проверить, запустив Windows 7 в QEMU. QEMU должен ожидать подключения отладчика. После подключения отладчика (gdb) ставим breakpoint на адрес (0x7c00 + 0x11d). Срабатывание breakpoint’а будет означать вызов этой функции. Кстати в Windows XP этот механизм отсутствует, для вызова прерываний BIOS'а там переходят в режим VM86.

! ВАЖНО! Все дальнейшие действия могут успешно осуществляться только после успешного прохождения всех шагов из пятой части нашей серии статей

Шаг 1. Изменим основную логику в kernel.c

1. Добавим в файле kernel.c следующие объявления:


<code>#include "multiboot.h"

#include "fat_io_lib/fat_filelib.h"

// переменные определены в loader.s

extern u32 mbd;
extern u32 magic;

Код, печатающий размер оперативной памяти

u64 ram_size = GetRamsize();
printf("ram_size = %llu(%lluMb)n", ram_size, ram_size / 0x100000);

заменим на следующий код:

// проверяем, что были загружены grub-ом
if (magic != MULTIBOOT_BOOTLOADER_MAGIC)
{
printf("Invalid magic number: 0x%xn", magic);
return;
}
multiboot_info_t *p_multiboot_info = (multiboot_info_t*)mbd;

// Is boot_device valid?
if ((p_multiboot_info->flags & 2) == 0)
{
printf("Error: boot_device(2) flag is clearn");
return;
}

// ищем первый сектор загрузочного раздела
if (InitBootMedia(p_multiboot_info->boot_device >> 24) == 0)
{
printf("Error: InitBootMedia failed.n");
return;
}

// инициализируем библиотеку fat_io_lib
fl_init();
if (fl_attach_media(ReadBootMedia, WriteBootMedia) != FAT_INIT_OK)
{
printf("Error: Media attach failed.n");
return;
}

// выводим список файлов в папке /boot/grub
fl_listdirectory("/boot/grub");

// выводим содержимое /boot/grub/menu.lst на экран
char str[64];
void *file = fl_fopen("/boot/grub/menu.lst", "r");
if (file == 0)
{
printf("Error: can not open file.n");
return;
}
printf("nConntent of the file /boot/grub/menu.lst:n");
while (fl_fgets(str, sizeof(str), file))
{
printf("%s", str);
}

Память под переменные mbd и magic зарезервирована в файле loader.s, так что их можно использовать аналогично глобальным переменным из кода на С. Переменная magic содержит сигнатуру, подтверждающую, что для загрузки использовался стандарт Multiboot, эталонной реализацией которого является GRUB. Переменная mbd указывает на структуру multiboot_info_t, которая объявлена в multiboot.h. Номер загрузочного диска определяется следующим выражением — p_multiboot_info->boot_device >> 24. Функция InitBootMedia запоминает номер диска и ищет первый сектор файловой системы, чтобы затем все смещения считать от него.

Библиотека fat_io_lib для инициализации требует вызова двух функций: fl_init и fl_attach_media. Первая функция обнуляет внутренние структуры библиотеки, а вторая получает в качестве параметров функции чтения и записи секторов на диск, которые затем используются для обращения к файлам. Далее идет демонстрация работы с библиотекой: выводится список файлов в папке /boot/grub и распечатывается содержимое файла menu.lst.

2. Добавляем файл multiboot.h в папку include. Содержимое файла берем с сайта спецификации предыдущей версии.

Шаг 2. Добавим функции для работы с диском

1. В файл includecallrealmode.h добавим прототипы следующих функций:

u32 InitBootMedia(u8 bootDevice);
int ReadBootMedia(unsigned long sector, unsigned char *buffer, unsigned long sectorCount);
int WriteBootMedia(unsigned long sector, unsigned char *buffer, unsigned long sectorCount);

2. В файле includecallrealmode_asm.h добавим в enum callrealmode_Func новое значение так, чтобы получилось следующее:

enum callrealmode_Func
{
CALLREALMODE_FUNC_GETSYSMEMMAP = 0,
CALLREALMODE_FUNC_READ_DISK = 1
};

Добавим структуру, которая будет использоваться для обращения к диску:

struct callrealmode_read_disk
{
u64 start_sector_lba;
u32 buff_addr;
u32 sectors_count;
u16 disk_number;
u8 ret_code;
} __attribute__ ((packed));

Добавим в union внутри структуры callrealmode_Data только что объявленную структуру callrealmode_read_disk. Должно получиться следующее:

struct callrealmode_Data
{
enum callrealmode_Func func : 16;
union
{
struct callrealmode_GetSysMemMap getsysmemmap;
struct callrealmode_read_disk readdisk;
};
} __attribute__ ((packed));

3. В файл includestring.h добавим функции strncmp и strncpy, используемые в библиотеке fat_io_lib.

static inline int strncmp ( const char * str1, const char * str2, unsigned int num )
{

for ( ; num > 0; str1++, str2++, --num)
{
if (*str1 != *str2)
return ((*(unsigned char *)str1 < *(unsigned char *)str2) ? -1 : +1);
else if (*str1 == '')
return 0;
}
return 0;
}

static inline char* strncpy ( char * dst, const char * src, unsigned int num )
{
if (num != 0)
{
char *d = dst;
const char *s = src;
do
{
if ((*d++ = *s++) == 0)
{
while (--num)
*d++ = 0;
break;
}
}
while (--num);
}
return dst;
}

4. Добавим в файл callrealmode.c следующие объявления:

#include "fat_io_lib/fat_opts.h"
#include "mbr.h"

u64 g_BootPartitionStart = 0; // номер первого сектора раздела с файловой системой
u32 g_BootDeviceInt13Num = 0; // номер загрузочного диска

И несколько функций:

// Чтение сектора с диска
int ReadBootMedia(unsigned long sector, unsigned char *buffer, unsigned long sectorCount)
{
struct callrealmode_Data param; // структура, в которой передаются параметры
// для кода в RM, и возвращается результат
param.func = CALLREALMODE_FUNC_READ_DISK;

// Чтение сектора с диска происходит путем вызова прерывания int13.
// Но таким образом можно записать прочитанный сектор только в
// диапазон памяти ниже 1Mb, но адрес "buffer" может быть выше этой границы.
// В качестве решения используется временная область по адресу "low_mem_buff",
// которая расположена сразу после RM кода, копируемого по адресу CALLREALMODE_OFFSET < 1Mb
int i;
void *low_mem_buff = CALLREALMODE_OFFSET + (&callrealmode_end - &callrealmode_start);
for (i = 0; i < sectorCount; i++)
{
param.readdisk.start_sector_lba = sector + g_BootPartitionStart + i;
param.readdisk.buff_addr = (u32)low_mem_buff;
param.readdisk.disk_number = g_BootDeviceInt13Num;
param.readdisk.sectors_count = 1;

callrealmode_Call(&param); // int 0x13 с параметрами из "param"

if (param.readdisk.ret_code)
{
return 0; // error
}

memcpy(buffer + i * FAT_SECTOR_SIZE, low_mem_buff, FAT_SECTOR_SIZE);
}
return 1; // success
}

// Запись сектора на диск. Заглушка
int WriteBootMedia(unsigned long sector, unsigned char *buffer, unsigned long sectorCount)
{
return 0; // error
}

// Функция инициализации
u32 InitBootMedia(u8 bootDevice)
{
g_BootDeviceInt13Num = bootDevice;

// Читаем первый сектор диска
MBRSector_t mbr;
if (ReadBootMedia(0, (u8*)&mbr, 1) == 0)
{
return 0;
}

// проверяем сигнатуру
if (mbr.mbr_sign[0] != 0x55 ||
mbr.mbr_sign[1] != 0xaa)
{
return 0;
}

// ищем загрузочный раздел
int i;
for (i = 0; i < 4; i++)
{
if (mbr.part[i].boot_indicator == 0x80)
break;
}
if (i == 4)
{
return 0;
}

// сохраняем номер первого сектора загрузочного раздела
g_BootPartitionStart = mbr.part[i].start_lva;

printf("start sector = %lld boot dev int13 num = 0x%xn",
g_BootPartitionStart, g_BootDeviceInt13Num);
return 1;
}

Функции ReadBootMedia и WriteBootMedia используются библиотекой fat_io_lib для чтения/записи секторов. Функция WriteBootMedia не обязательная и является заглушкой, так как в данном примере нет записи на диск. Ее реализация выглядела бы аналогично функции ReadBootMedia. Функция ReadBootMedia похожа на функцию GetRamsize из прошлой статьи с точностью до типа param.func, а вместо param.getsysmemmap используется param.readdisk. Функция InitBootMedia должна быть вызвана раньше двух остальных, так как она инициализирует значения g_BootPartitionStart и g_BootDeviceInt13Num.

5. Изменим callrealmode_asm.s. Добавим еще один тип CALLREALMODE_FUNC_READ_DISK вызываемых функций, должно получиться следующее:

# это объявления из enum callrealmode_Func
CALLREALMODE_FUNC_GETSYSMEMMAP = 0x0
CALLREALMODE_FUNC_READ_DISK = 0x1

Далее добавим еще одну проверку на тип функции и непосредственно код, читающий с диска. Должно получиться следующее:

callrealmode_switch:
OFF_FUNC = 44 # на таком смещении относительно %bp
# находится поле func структуры callrealmode_Data

# Which function?
movw OFF_FUNC(%bp),%ax
cmp $CALLREALMODE_FUNC_GETSYSMEMMAP,%ax
je getsysmemmap
cmp $CALLREALMODE_FUNC_READ_DISK,%ax
je readdisk
ret

readdisk:
OFF_START_SECTOR = 50 # смещение до поля start_sector_lba структуры callrealmode_Data
OFF_BUFFER_ADDR = 58 # смещение до поля buff_addr структуры callrealmode_Data
OFF_SECTORS_COUNT = 62 # смещение до поля sectors_count структуры callrealmode_Data
OFF_DISK_NUMBER = 66 # смещение до поля disk_number структуры callrealmode_Data
OFF_RETURN_CODE = 68 # смещение до поля ret_code структуры callrealmode_Data

push %bp
mov %sp,%bp

# формируем на стеке структуру DAP
pushl OFF_START_SECTOR+4(%bp)
pushl OFF_START_SECTOR+0(%bp)
pushl OFF_BUFFER_ADDR(%bp)
pushw OFF_SECTORS_COUNT(%bp)
pushw $0x10
mov %sp,%si # ds:si указываем на вершину стека, т.е. на DAP
mov OFF_DISK_NUMBER(%bp),%dl # номер диска в dl
mov $0x42,%ah # EXTENDED READ
int $0x13 # CALL DISK BIOS
mov %ah,OFF_RETURN_CODE(%bp) # сохраняем результат
add $0x10,%sp # очищаем стек от DAP

pop %bp
ret

Метка readdisk указывает на код, который формирует структуру DAP из структуры callrealmode_Data и вызывает int0x13. В коде после метки callrealmode_switch добавилось 2 инструкции, проверяющие, не нужно ли вызывать readdisk.

6. Добавим файл includembr.h, содержащий определения для работы с MBR. Его содержимое:

#ifndef _MBR_H_
#define _MBR_H_

#include "types.h"

struct MBRPartitionEntry
{
unsigned char boot_indicator;
unsigned char start_head;
unsigned short start_sector : 6;
unsigned short start_cylinder : 10;
unsigned char sys_id;
unsigned char end_head;
unsigned short end_sector : 6;
unsigned short end_cylinder : 10;
unsigned int start_lva;
unsigned int size_in_sectors;
} __attribute__ ((packed));
typedef struct MBRPartitionEntry MBRPartitionEntry_t;

struct MBRSector
{
u8 code[446];
MBRPartitionEntry_t part[4];
u8 mbr_sign[2];
} __attribute__ ((packed));
typedef struct MBRSector MBRSector_t;

#endif

Структура MBRSector используется в функции InitBootMedia.

Шаг 3. Добавим библиотеку fat_io_lib и запустим

1. Скачаем архив fat_io_lib.zip и распакуем его в папку fat_io_lib в корне проекта.
2. Добавим пустые файлы assert.h и stdlib.h в папку include. Они нужны, что бы библиотека скомпилировалась.
3. Исправим Makefile. Добавим файлы из библиотеки в список целей для компиляции. Должно получиться следующее:

FAT_LIB_OBJFILES =
./fat_io_lib/fat_access.o
./fat_io_lib/fat_cache.o
./fat_io_lib/fat_filelib.o
./fat_io_lib/fat_format.o
./fat_io_lib/fat_misc.o
./fat_io_lib/fat_string.o
./fat_io_lib/fat_table.o
./fat_io_lib/fat_write.o

OBJFILES =
loader.o
common/printf.o
common/screen.o
common/string.o
kernel.o
callrealmode.o
callrealmode_asm.o
descriptor.o
$(FAT_LIB_OBJFILES)

Заменим строчку
@dd if=/dev/zero of=./hdd.img bs=512 count=16065 1>/dev/null 2>&1
На
@dd if=/dev/zero of=./hdd.img bs=1M count=10 1>/dev/null 2>&1

Теперь размер образа равен 10Mb. Это делается для того, чтобы команда mkdosfs отформатировала раздел в FAT16 вместо FAT12. FAT12 не поддерживается библиотекой fat_io_lib.

Заменим строчку

$(CC) -Iinclude $(CFLAGS) -o $@ -c $<

На

$(CC) -Iinclude -DFAT_PRINTF_NOINC_STDIO $(CFLAGS) -o $@ -c $<

С этим дефайном библиотека не будет включать stdio.h, но будет использовать готовый прототип функции printf, который совпадает с нашим, и который уже реализован.

4. Пересоберем проект

make rebuild
sudo make image

5. Запустим

sudo qemu-system-i386 -hda hdd.img

Должно получиться следующее:

Как запустить программу без операционной системы: часть 6. Поддержка работы с дисками с файловой системой FAT - 6

Как и в предыдущих частях, можно сделать dd образа hdd.img на флешку и проверить код на реальном железе, загрузившись с нее.

В результате мы реализовали работу с файловыми системами FAT16 и FAT32. Мы немного схитрили, использовав готовую библиотеку, но разбираться в устройстве FAT'а было бы менее интересно, да и вряд ли бы мы тогда уложились в 1 статью. Надеюсь, вам было интересно читать. Пишите в комментариях, если у вас возникнут проблемы в прохождении описанных шагов!

Подборка ссылок на предыдущие части:

Автор: NWOcs

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js