Создание операционной системы на ассемблере

Чтение ext2fs


В прошлом выпуске я описывал структуру этой файловой системы. Как вы поняли, (я надеюсь) в файловой системе присутствует Super Block и дескрипторы групп. Эта информация хранится в начале раздела. Super Block во 2-м килобайте, дескрипторы групп - в третьем.
Стоит заметить, что первый килобайт для нужд файловой системы не используется и может быть целиком использован для boot sector'а (правда он уже будет не сектор, а килобайт :). Но для этого следует подгрузить второй сектор boot'а.
А для инициализации файловой системы нам нужно загрузить super block и дескрипторы групп, они же понадобятся нам для работы с файловой системой.
Это все можно загрузить одновременно, как мы и сделаем.

mov ax, 0x7e0 mov es, ax mov ax, 1 mov cx, 5 call load_block

Для этого мы используем уже знакомую процедуру загрузки блока, но эта процедура станет значительно короче, потому что никаких процентов мы больше не будем выводить.
В es засылается адрес, следующий за загруженным загрузочным сектором (Загружается он, как мы помним, по адресу 7c00h, и имеет длину 200h байт, следовательно свободная память начинается с адреса 7e00h, а сегмент для этого адреса равен 7e0h). В ax засылается номер сектора с которого начинается блок (в нашем случае это первый сектор, загрузочный сектор является нулевым). в cx засылается длина загружаемых данных в секторах (1 - дополнительная часть boot sector'а, 2 - Super Block ext2, 2 - дескрипторы групп. Всего 5 секторов).

Теперь вызовем процедуру инициализации файловой системы. Эта процедура достаточно проста, и проверяет только соответствие magic номера файловой системы и вычисляет размеры блока для работы.

sb equ 0x8000

ext2_init: pusha cmp word [sb + ext2_sb.magic], 0xef53 jz short .right

mov si, bad_sb call outstring

popa stc ret

bad_sb: db 'Bad ext2 super block!', 0ah, 0dh, 0

В случае несоответствия magic номера происходит вывод сообщения об ошибке и выход из подпрограммы. Чтобы сигнализировать об ошибке используется бит C регистра flags.

.right: mov ax, 1024 mov cl, [sb + ext2_sb.log_block_size] shl ax, cl mov [block_size], al ; Размер блока в байтах shr ax, 2 mov [block_dword_size], ax ; Размер блока в dword shr ax, 2 mov [block_seg_size], ax ; Размер блока в параграфах shr ax, 5 mov [block_sect_size], ax ; Размер блока в секторах popa clc ret




block_size: dw 1024 block_dword_size: dw 256 block_seg_size: dw 64 block_sect_size: dw 2

Все эти значения нам понадобятся для работы. А теперь рассмотрим процедуру загрузки одного блока файловой системы.



ext2_load_block: pusha

mov cx, [block_sect_size] mul cx call load_block

mov ax, es add ax, [block_seg_size] mov es, ax ; смещаем es

popa ret

При входе в эту процедуру ax содержит номер блока (блоки нумеруются с нуля), es содержит адрес памяти для загрузки содержимого блока.
Номер блока нам надо преобразовать в номер сектора, для этого мы умножаем его на длину блока в секторах. А в cx у нас уже записана длина блока в секторах, то есть все готово для вызова процедуры load_block.
После считывания блока мы модифицируем регистр es, чтобы последующие блоки грузить следом за этим... в принципе модифицирование указателя можно перенести в другое место, в процедуру загрузки файла, это будет наверное даже проще и компактнее, но сразу я об этом не подумал. :(

Но пошли дальше... основной структурой описывающей файл в ext2fs является inode. Inode храняться в таблицах, по одной таблице на каждую группу. Количество inode в группе зафиксировано в супер блоке. Итак, процедура загрузки inode:

ext2_get_inode: pusha push es

dec ax xor dx, dx div word [sb + ext2_sb.inodes_per_group]

Поделив номер inode на количество inode в группе, в ax мы получаем номер группы, в которой находится inode, в dx получаем номер inode в группе.

shl ax, gd_bit_size mov bx, ax mov bx, [gd + bx + ext2_gd.inode_table]

ax умножаем на размер записи о группе (делается это сдвигом, но, по сути, то же самое умножение) и получаем смещение группы в таблице дескрипторов групп. gd - базовый адрес таблицы групп. Последняя операция извлекает из дескриптора группы адрес таблицы inode этой группы (адрес задается в блоках файловой системы) который у нас пока будет храниться в bx.

mov ax, dx shl ax, inode_bit_size

Теперь разберемся с inode. Определим его смещение в таблице inode группы.

xor dx, dx div word [block_size] add ax, bx



Поделив это значение на размер блока мы получим номер блока относительно начала таблицы inode (ax), и смещение inode в блоке (dx). К номеру блока (bx) прибавим блок, в котором находится inode.

mov bx, tmp_block >> 4 mov es, bx call ext2_load_block

Загрузим этот блок в память.

push ds pop es

mov si, dx add si, tmp_block mov di, inode mov cx, ext2_i_size >> 1 rep movsw

Восстановим содержимое сегментного регистра es и перепишем inode из блока в отведенное для него место.

pop es popa ret

Inode загружен. Теперь по нему можно загружать файл. Здесь все не столь однозначно. Процедура загрузки файла состоит из нескольких модулей. Потому что помимо прямых ссылок inode может содержать косвенные ссылки на блоки. В принципе можно ограничить возможности считывающей подпрограммы необходимым минимумом, полная поддержка обеспечивает загрузку файлов до 4 гигабайт размером. Естественно в реальном режиме мы такими файлами оперировать не сможем, да это и не нужно. Но сейчас мы рассмотрим полную поддержку:

ext2_load_inode: pusha

xor ax, ax mov si, inode + ext2_i.block

mov cx, EXT2_NDIR_BLOCKS call dir_blocks

cmp ax, [inode + ext2_i.blocks] jz short .exit

В inode храняться прямые ссылки на 12 блоков файловой системы. Такие блоки мы загружаем с помощью процедуры dir_blocks (она будет описана ниже). Данный этап может загрузить максимум 12/24/48 килобайт файла (в зависимости от размера блока fs 1/2/4 килобайта). После окончания работы процедуры проверяем, все ли содержимое файла уже загружено или еще нет. Если нет, то загрузка продолжается по косвенной таблице блоков. Косвенная таблица - это отдельный блок в файловой системе, который содержит в себе таблицу блоков.

mov cx, 1 call idir_blocks

cmp ax, [inode + ext2_i.blocks] jz short .exit

В inode только одна косвенная таблица первого уровня (cx=1). Для загрузки блоков из такой таблицы мы используем процедуру idir_blocks. Это позволяет нам, в зависимости от размера блока загрузить 268/1048/4144 килобайта файла. Если файл еще не загружен до конца, то используется косвенная таблица второго уровня.



mov cx, 1 call ddir_blocks

cmp ax, [inode + ext2_i.blocks] jz short .exit

В inode также только одна косвенная таблица второго уровня (cx=1). Для загрузки блоков из такой таблицы мы используем процедуру ddir_blocks. Это позволяет нам, в зависимости от размера блока загрузить 64.2/513/4100 мегабайт файла. Если файл опять не загружен до конца (где же столько памяти взять??), то используется косвенная таблица третьего уровня. Ради этого мы уже не будем вызывать подпрограмм, а обработаем ее в этой процедуре.

push ax push es

mov ax, tmp3_block >> 4 mov es, ax lodsw call ext2_load_block

pop es pop ax

mov si, tmp3_block mov cx, [block_dword_size] call ddir_blocks

В inode и эта таблица присутствует только в одном экземпляре (куда же больше?). Это, крайняя возможность, позволяет нам, в зависимости от размера блока, загрузить 16/256.5/4100 гигабайт файла. Что уже является пределом даже для размера файловой системы (4 терабайта).

.exit: popa ret

Конечно, такие крайности нам при старте будут не к чему, с учетом, что мы находимся в реальном режиме и не можем адресовать больше ~600к памяти.
Кратко рассмотрю вспомогательные функции:

dir_blocks: .repeat: push ax lodsw call ext2_load_block add si, 2 pop ax

inc ax cmp ax, [inode + ext2_i.blocks] jz short .exit

loop .repeat .exit: ret

Эта функция загружает прямые блоки. Ради простоты я пока не обрабатывал блоки номер которых превышает 16 бит. Это создает ограничение на размер файловой системы в 65 мегабайт, а реально еще меньше, поскольку load_block у нас тоже не оперирует с секторами, номер которых больше 16 бит, ограничение по размеру уменьшается до 32 мегабайт. В дальнейшем эти ограничения мы конечно обойдем, а пока достаточно.
В этой функции стоит проверка количества загруженных блоков, для того чтобы вовремя выйти из процедуры считывания.

idir_blocks: .repeat: push ax push es

mov ax, tmp_block >> 4 mov es, ax lodsw call ext2_load_block

add si, 2 pop es pop ax

push si push cx

mov si, tmp_block mov cx, [block_dword_size] call dir_blocks



pop cx pop si

cmp ax, [inode + ext2_i.blocks] jz short .exit

loop .repeat .exit: ret

Эта функция обращается в свою очередь к функции dir_blocks, предварительно загрузив в память содержимое косвенного блока. так же имеет контроль длины файла.
Функция ddir_blocks в точности аналогична этой, только для считывания вызывает не dir_blocks, а idir_blocks, поскольку адреса блоков в ней дважды косвенны.

Но мы еще не рассмотрели самого главного. Процедуры, которая по пути файла может загрузить его с диска. Начнем.

ext2_load_file: pusha

cmp byte [si], '/' jnz short .error_exit

Если путь файла не начинается со слэш, то это в данном случае является ошибкой. Мы не оперируем понятием текущий каталог!

mov ax, INODE_ROOT ; root_inode call ext2_get_inode

Загружаем корневой inode - он имеет номер 2.

.cut_slash: cmp byte [si], '/' jnz short .by_inode

inc si jmp short .cut_slash

Уберем лидирующий слэш... или несколько слэшей, такое не является ошибкой.

.by_inode: push es call ext2_load_inode pop es

Загрузим содержимое файла. Директории, в том числе и корневая, являются такими же файлами, как и все остальные, только содержат в себе записи о находящихся в директории файлах.

mov ax, [inode + ext2_i.mode] and ax, IMODE_MASK cmp ax, IMODE_REG jnz short .noreg_file

По inode установим тип файла.
Если файл не регулярный, то это может быть директорией. Это проконтролируем ниже.

cmp byte [si], 0 jnz short .error_exit

Если это файл, который нам надлежит скачать - то в [si] будет содержаться 0, означающий что мы обработали весь путь.

.ok_exit: clc jmp short .exit

А поскольку содержимое файла уже загружено, то можем со спокойной совестью вернуть управление. Битом C сообщив, что все закончилось хорошо.

.noreg_file: cmp ax, IMODE_DIR jnz short .error_exit

Если этот inode не является директорией, то это или не поддерживаемый тип файла или ошибка в пути.

mov dx, [inode + ext2_i.size] xor bx, bx

Если то, что мы загрузили, является директорией, то со смещения 0 (bx) в этом файле содержится список записей о файлах. Нам нужно выбрать среди них нужную. В dx сохраним длину файла, по ней будем определять коней директории.



.walk_dir: lea di, [es:bx + ext2_de.name] mov cx, [es:bx + ext2_de.name_len] ; длина имени

push si repe cmpsb

mov al, [si] pop si

test cx, cx jnz short .notfind

Сравниваем имена из директории с именем, на которое указывает si. Если не совпадает - перейдем на следующую запись (чуть ниже)

cmp al, '/' jz short .normal_path

test al, al jnz short .notfind

Если совпал, то в пути после имени должно содержаться либо '/' либо 0 - символ конца строки. Если это не так, значит это не подходящий файл.

.normal_path: mov ax, [es:bx + ext2_de.inode] call ext2_get_inode

Загружаем очередной inode.

add si, [es:bx + ext2_de.name_len] cmp byte [si], '/' jz short .cut_slash jmp short .by_inode

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

.notfind: sub dx, [es:bx + ext2_de.rec_len] add bx, [es:bx + ext2_de.rec_len]

test dx, dx jnz short .walk_dir

Если путь не совпадает, и если в директории еще есть записи - продолжаем проверку.

.error_exit: mov si, bad_dir call outstring stc

Иначе выводим сообщение об ошибке

.exit: popa ret

И прекращаем работу.

Вот и весь алгоритм. Не смотря на большой размер этого повествования, код занимает всего около 450 байт. А если убрать параноидальные функции, то и того меньше. Не стоит пытаться откомпилировать этот код, все эти модули вы сможете найти на нашем сайте, ссылка на который приведена ниже. Здесь я все это привел для того чтобы объяснить как и что. Надеюсь у меня это получается хоть как-то. Если кто-то что-то не понимает - пишите мне, мой адрес вы всегда можете найти чуть ниже.

В следующем выпуске мы с вами рассмотрим форматы выполняемых файлов, используемые в unix. Это нам тоже потребуется на этапе загрузки.

И наберитесь немного терпения... скоро мы начнем писать ядро.

Отправлено 2001-09-06 для 4463 подписчиков.
ведущий рассылки Dron
Сайт проекта
Архив Рассылки

При поддержке Kalashnikoff.ru



Содержание раздела