[09] - чтение ext2fs
Чтение 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 из блока в отведенное для него место.
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 терабайта).
Конечно, такие крайности нам при старте будут не к чему, с учетом, что мы находимся в реальном режиме и не можем адресовать больше ~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
Иначе выводим сообщение об ошибке
Вот и весь алгоритм. Не смотря на большой размер этого повествования, код занимает всего около 450 байт. А если убрать параноидальные функции, то и того меньше.