Мы будем писать загрузочный сектор для трехдюймовой дискеты с файловой
системой FAT12. После окончания начальной загрузки программа POST находит
активное устройство и загружает с него короткую программу загрузки ОС -
загрузочный сектор. Загрузочный сектор это первый физический сектор устройства,
в данном случае дискеты и его размет равен всего ничего 512 байт. С помощью этих
512 байт кода мы должны найти основную часть загрузчика операционной системы,
загрузить его в память и передать ему управление. Заголовок файловой системы FAT
находится в первом секторе дискеты, благодаря чему этот заголовок, содержащий
всю необходимую информацию о файловой системе, загружается вместе нашим
загрузчиком. Наш загрузочный сектор будет искать в корневом каталоге некоторый
файл - загрузчик, загрузит его в память и передаст ему управление на его начало.
А загрузчик уже сам разберется, что ему делать дальше. Я использую NASM, т.к.
считаю, что он больше подходит для наших целей.
И так, приступим. Как я уже говорил, в начале нашего загрузочного сектора
располагается заголовок FAT, опишем его:
; Общая часть для всех типов FAT BS_jmpBoot: jmp short BootStart ; Переходим на код загрузчика nop BS_OEMName db '*-v4VIHC' ; 8 байт, что было на моей дискете, то и написал BPB_BytsPerSec dw 0x200 ; Байт на сектор BPB_SecPerClus db 1 ; Секторов на кластер BPB_RsvdSecCnt dw 1 ; Число резервных секторов BPB_NumFATs db 2 ; Количектво копий FAT BPB_RootEntCnt dw 224 ; Элементов в корневом катологе (max) BPB_TotSec16 dw 2880 ; Всего секторов или 0 BPB_Media db 0xF0 ; код типа устройства BPB_FATsz16 dw 9 ; Секторов на элемент таблицы FAT BPB_SecPerTrk dw 18 ; Секторов на дорожку BPB_NumHeads dw 2 ; Число головок BPB_HiddSec dd 0 ; Скрытых секторов BPB_TotSec32 dd 0 ; Всего секторов или 0 ; Заголовок для FAT12 и FAT16 BS_DrvNum db 0 ; Номер дика для прерывания int 0x13 BS_ResNT db 0 ; Зарезервировано для Windows NT BS_BootSig db 29h ; Сигнатура расширения BS_VolID dd 2a876CE1h ; Серийный номер тома BS_VolLab db 'X boot disk' ; 11 байт, метка тома BS_FilSysType db 'FAT12 ' ; 8 байт, тип ФС ; Структура элемента каталога struc DirItem DIR_Name: resb 11 DIR_Attr: resb 1 DIR_ResNT: resb 1 DIR_CrtTimeTenth resb 1 DIR_CrtTime: resw 1 DIR_CrtDate: resw 1 DIR_LstAccDate: resw 1 DIR_FstClusHi: resw 1 DIR_WrtTime: resw 1 DIR_WrtDate: resw 1 DIR_FstClusLow: resw 1 DIR_FileSize: resd 1 endstruc ;DirItem
Большинство полей мы использовать не будем, и так мало места для полета.
Загрузчик BIOS передает нам управление на начало загрузочного сектора, т.е. на
BS_jmpBoot, поэтому в начале заголовка FAT на отводится 3 байта для
короткой или длинной инструкции jmp. Мы в данном случае
использовали короткую, указав модификатор short, и в третьем байте
просто разместили однобайтовую инструкцию nop.
По инструкции jmp short BootStart мы переходим на наш код.
Проведем небольшую инициализацию:
; Наши не инициализированные переменные ; При инициализации они затрут не нужные нам ; поля заголовка FAT: BS_jmpBoot и BS_OEMName struc NotInitData SysSize: resd 1 ; Размер системной области FAT fails: resd 1 ; Число неудачных попыток при чтении fat: resd 1 ; Номер загруженного сектора с элементами FAT endstruc ;NotInitData ; По этому адресу мы будем загружать загрузчик %define SETUP_ADDR 0x1000 ; А по этому адресу нас должны были загрузить %define BOOT_ADDR 0x7C00 %define BUF 0x500 BootStart: cld xor cx, cx mov ss, cx mov es, cx mov ds, cx mov sp, BOOT_ADDR mov bp, sp ; Сообщим о том что мы загружаемся mov si, BOOT_ADDR + mLoading call print
Все сегментные регистры настраиваем на начало физической памяти. Вершину стека
настраиваем на начало нашего сектора, стек растет вниз (т.е. в сторону младших
адресов), так что проблем быть не должно. Туда же указывает регистр bp
- нам нужно обращаться к полям заголовка FAT и паре наших переменных. Мы
используем базовую адресацию со смещением, для чего используем регистр bp
т.к. в этом случае можно использовать однобайтовые смещения, вместо двухбайтовых
адресов, что позволяет сократить код. Процедуру print, выводящую
сообщение на экран, рассмотрим позже.
Теперь нам нужно вычислить номера первых секторов корневого каталога и данных
файлов.
mov al, [byte bp+BPB_NumFATs] cbw mul word [byte bp+BPB_FATsz16] add ax, [byte bp+BPB_HiddSec] adc dx, [byte bp+BPB_HiddSec+2] add ax, [byte bp+BPB_RsvdSecCnt] adc dx, cx mov si, [byte bp+BPB_RootEntCnt] ; dx:ax - Номер первого сектора корневого каталога ; si - Количество элементов в корневом каталоге pusha ; Вычислим размер системной области FAT = резервные сектора + ; все копии FAT + корневой каталог mov [bp+SysSize], ax ; осталось добавить размер каталога mov [bp+SysSize+2], dx ; Вычислим размер корневого каталога mov ax, 32 mul si ; dx:ax - размер корневого каталога в байтах, а надо в секторах mov bx, [byte bp+BPB_BytsPerSec] add ax, bx dec ax div bx ; ax - размер корневого каталога в секторах add [bp+SysSize], ax ; Теперь мы знаем размер системной adc [bp+SysSize+2], cx ; области FAT, и начало области данных popa ; В dx:ax - снова номер первого сектора корневого каталога ; si - количество элементов в корневом каталоге
Теперь мы будем просматривать корневой каталог в поисках нужного нам файла
NextDirSector: ; Загрузим очередной сектор каталога во временный буфер mov bx, 700h ; es:bx - буфер для считываемого сектора mov di, bx ; указатель текущего элемента каталога mov cx, 1 ; количество секторов для чтения call ReadSectors jc near DiskError ; ошибка при чтении RootDirLoop: ; Ищем наш файл ; cx = 0 после функции ReadSectors cmp [di], ch ; byte ptr [di] = 0? jz near NotFound ; Да, это последний элемент в каталоге ; Нет, не последний, сравним имя файла pusha mov cl, 11 ; длина имени файла с расширением mov si, BOOT_ADDR + LoaderName ; указатель на имя искомого файла rep cmpsb ; сравниваем popa jz short Found ; Нашли, выходим из цикла ; Нет, ищем дальше dec si ; RootEntCnt jz near NotFound ; Это был последний элемент каталога add di, 32 ; Переходим к следующему элементу каталога ; bx указывает на конец прочтенного сектора после call ReadSectors cmp di, bx ; Последний элемент в буфере? jb short RootDirLoop ; Нет, проверим следующий элемент jmp short NextDirSector ; Да последний, загрузим следующий сектор
Из этого кода мы можем выйти одну из трех точек: ошибка при чтении
DiskError, файл наден Found или файл не найден
NotFound.
Если файл найден, то загрузим его в память и передадим управление на его
начало.
Found: ; Загрузка загрузчика (извените, калабур) mov bx, SETUP_ADDR mov ax, [byte di+DIR_FstClusLow] ; Номер первого кластера файла ; Загружаем сектор с элемнтами FAT, среди которых есть FAT[ax] ; LoadFAT сохраняет значения всех регистров call LoadFAT ReadCluster: ; ax - Номер очередного кластера ; Загрузим его в память push ax ; Первые два элемента FAT служебные dec ax dec ax ; Число секторов для чтения ; cx = 0 после ReadSectors mov cl, [byte bp+BPB_SecPerClus] ; Секторов на кластер mul cx ; dx:ax - Смещение кластера относительно области данных add ax, [byte bp+SysSize] adc dx, [byte bp+SysSize+2] ; dx:ax - Номер первого сектора требуемого кластера ; cx еще хранит количество секторов на кластер ; es:bx - конец прошлого кластера и начало нового call ReadSectors ; читаем кластер jc near DiskError ; Увы, ошибка чтения pop ax ; Номер кластера ; Это конец файла? ; Получим значение следующего элемента FAT pusha ; Вычислим адрес элемента FAT mov bx, ax shl ax, 1 add ax, bx shr ax, 1 ; Получим номер сектора, в котором находится текущий элемент FAT cwd div word [byte bp+BPB_BytsPerSec] cmp ax, [bp+fat] ; Мы уже читали этот сектор? popa je Checked ; Да, читали ; Нет, надо загрузить этот сектор call LoadFAT Checked: ; Вычислим адрес элемента FAT в буфере push bx mov bx, ax shl bx, 1 add bx, ax shr bx, 1 and bx, 511 ; остаток от деления на 512 mov bx, [bx+0x700] ; а вот и адрес ; Извлечем следующий элемент FAT ; В FAT16 и FAT32 все немного проще :( test al, 1 jnz odd and bx, 0xFFF jmp short done odd: shr bx, 4 done: mov ax, bx pop bx ; bx - новый элемент FAT cmp ax, 0xFF8 ; EOF - конец файла? jb ReadCluster ; Нет, читаем следующий кластер ; Наконец-то загрузили mov ax, SETUP_ADDR>>4 ; SETUP_SEG mov es, ax mov ds, ax ; Передаем управление, наше дело сделано :) jmp SETUP_ADDR>>4:0
LoadFAT ;proc ; Процедура для загрузки сектора с элементами FAT ; Элемент ax должен находится в этом секторе ; Процедура не должна менять никаких регистров pusha ; Вычисляем адрес слова содержащего нужный элемент mov bx, ax shl ax, 1 add ax, bx shr ax, 1 cwd div word [byte bp+BPB_BytsPerSec] ; ax - смещение сектора относительно начала таблицы FAT mov [bp+fat], ax ; Запомним это смещение, dx = 0 cwd ; dx:ax - номер сектора, содержащего FAT[?] ; Добавим смещение к первой копии таблицы FAT add ax, [byte bp+BPB_RsvdSecCnt] adc dx, 0 add ax, [byte bp+BPB_HiddSec] adc dx, [byte bp+BPB_HiddSec+2] mov cx, 1 ; Читаем один сектор. Можно было бы и больше, но не быстрее mov bx, 700h ; Адрес буфера call ReadSectors jc DiskError ; Ошибочка вышла popa ret ;LoadFAT endp
В FAT12 на каждый элемент FAT отводится по 12 бит, что несколько усложняет нашу
работу, в FAT16 и FAT32 на каждый элемент отводится по 16 и 32 бита
соответственно и можно просто прочесть слово или двойное слово, а в FAT12
необходимо прочесть слово содержащее элемент FAT и правильно извлечь из
него 12 бит.
Теперь разберем процедуру загрузки секторов. Процедура получает номер сектора
в dx:ax (нумерация с нуля) и преобразует его к формату CSH (цилиндр, сектор,
сторона), используемому прерыванием BIOS int 0x13.
; ************************************************* ; * Чтение секторов с диска * ; ************************************************* ; * Входные параметры: * ; * dx:ax - (LBA) номер сектора * ; * cx - количество секторов для чтения * ; * es:bx - адрес буфера * ; ************************************************* ; * Выходные параметры: * ; * cx - Количество не прочтенных секторов * ; * es:bx - Указывает на конец буфера * ; * cf = 1 - Произошла ошибка при чтении * ; ************************************************* ReadSectors ;proc next_sector: ; Читаем очередной сектор mov byte [bp+fails], 3 ; Количество попыток прочесть сектор try: ; Очередная попытка pusha ; Преобразуем линейный адрес в CSH ; dx:ax = a1:a0 xchg ax, cx ; cx = a0 mov ax, [byte bp+BPB_SecPerTrk] xchg ax, si ; si = Scnt xchg ax, dx ; ax = a1 xor dx, dx ; dx:ax = 0:a1 div si ; ax = q1, dx = c1 xchg ax, cx ; cx = q1, ax = a0 ; dx:ax = c1:a0 div si ; ax = q2, dx = c2 = c inc dx ; dx = Sector? xchg cx, dx ; cx = c, dx = q1 ; dx:ax = q1:q2 div word [byte bp+BPB_NumHeads] ; ax = C (track), dx = H mov dh, dl ; dh = H mov ch, al ror ah, 2 or cl, ah mov ax, 0201h ; ah=2 - номер функции, al = 1 сектор mov dl, [byte bp+BS_DrvNum] int 13h popa jc Failure ; Ошибка при чтении ; Номер следующего сектора inc ax jnz next inc dx next: add bx, [byte bp+BPB_BytsPerSec] dec cx ; Все сектора прочтены? jnz next_sector ; Нет, читаем дальше return: ret Failure: dec byte [bp+fails] ; Последняя попытка? jnz try ; Нет, еще раз ; Последняя, выходим с ошибкой stc ret ;ReadSectors endp
Осталось всего ничего:
; Сообщения об ошибках NotFound: ; Файл не найден mov si, BOOT_ADDR + mLoaderNotFound call print jmp short die DiskError: ; Ошибка чтения mov si, BOOT_ADDR + mDiskError call print ;jmp short die die: ; Просто ошибка mov si, BOOT_ADDR + mReboot call print _die: ; Бесконечный цикл, пользователь сам нажмет Reset jmp short _die ; Процедура вывода ASCIIZ строки на экран ; ds:si - адрес строки print: ; proc pusha print_char: lodsb ; Читаем очередной символ test al, al ; 0 - конец? jz short pr_exit ; Да конец ; Нет, выводим этот символ mov ah, 0eh mov bl, 7 int 10h jmp short print_char ; Следующий pr_exit: popa ret ;print endp ; Перевод строки %define endl 10,13,0 ; Строковые сообщения mLoading db 'Loading...',endl mDiskError db 'Disk I/O error',endl mLoaderNotFound db 'Loader not found',endl mReboot db 'Reboot system',endl ; Выравнивание размера образа на 512 байт times 499-($-$$) db 0 LoaderName db 'BOOTOR ' ; Имя файла загрузчика BootMagic dw 0xAA55 ; Сигнатура загрузочного сектора
Ну вот вроде бы и все. Компилируется все это до безобразия просто:
> nasm -f bin boot.asm -lboot.lst -oboot.bin
Осталось только как-то записать этот образ в загрузочный сектор вашей дискеты и
разместить в корне этой дискеты файл загрузчика BOOTOR. Загрузочный
сектор можно записать с помощью такой вот простой программы на Turbo (Borland)
Pascal. Эта программа будет работать как в DOS, так и в Windows - пробовал на
WinXP - работает как ни странно, но только с floopy. Но все же я рекомендую
запускать эту утилиту из-под чистого DOS'а, т.к. WinXP обновляет не все поля в
заголовке FAT и загрузочный сектор может работать некорректно.
var fn:string; f:file; buf:array[0..511] of byte; ok:boolean; begin fn:=ParamStr(1); if fn='' then writeln('makeboot bootsect.bin') else begin writeln('Making boot floppy'); {$I-} assign(f,fn); reset(f,sizeof(buf)); BlockRead(f,buf,1); close(f); {$I+} if IOResult<>0 then begin Writeln('Failed to read file "',fn,'"'); Halt(1); end; ok:=false; asm mov ax, 0301h mov cx, 1 mov dx, 0 mov bx, seg buf mov es, bx mov bx, offset buf int 13h jc @error mov ok, true @error: end; if ok then writeln('Done :)') else begin writeln('Makeboot failed :('); Halt(1); end; end; end.