Загрузочный сектор

Дата: 06/07/05;  Автор: shade; 


Мы будем писать загрузочный сектор для трехдюймовой дискеты с файловой системой 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.