В предыдущей заметке мы успешно перешли в защищенный режим процессора Intel x86. Прежде чем нам двинуться дальше в изучении защищенного режима, нам надо решить одну проблему. Загрузочный сектор, который загружается в оперативную память при старте компьютера и в котором находится наша программа, имеет размер всего 512 байт. Скоро нам перестанет хватать этого размера. Поэтому мы должны научиться загружать в оперативную память секторы с дискеты, на которой находится наша программа. Программа, которая умеет это делать, называется bootloader. Вот его мы и напишем. Работать будем в реальном режиме, чтобы пользоваться сервисами BIOS для работы с дисками.
Процедурное прогарммирование
Исходный код загрузчика обещал быть довольно большим, поэтому, чтобы не запутаться, я написал его в стиле процедурного программирования. Подробный рассказ о функциях и о том, как они пишутся на языке ассемблера, вы найдете в книге Kip Irvine — Assembly Language for x86 Processors, 7th edition — 2014 Глава 5 — Procedures.
Структура программы
Задача представленной ниже программы — загрузить 2-й сектор с дискеты в оперативную память и передать управление находящемуся там (во 2-ом секторе) машинному коду, который всего лишь выводит на экран текстовое сообщение, чтобы показать, что 2-й сектор действительно загружен. В BIOS есть функции, которые умеют загружать информацию с диска в оперативную память, другое дело, что вызвать эти функции не так просто, как хотелось бы, в силу их заковыристого интерфейса. Поэтому я написал несколько функций-оберток над нужными мне сервисами BIOS и пометил их в отдельный заголовочный файл bios_services.inc. Функции, которые я поместил в заголовочный файл, позволяют загрузить с любого диска любые сектора в оперативную память по любому адресу (в пределах адресного пространства реального режима, разумеется). Причем функции эти принимают свои параметры привычным для меня образом — через стек (а не через регистры, как большинство функций BIOS). В основном же файле программы boot_sector.asm я директивой include подключаю файл bios_services.inc и просто вызываю из него одну из функций, которая загружает с дискеты 2-й сектор. Ниже показано содержимое файлов boot_sector.asm и bios_services.inc.
Исходный код загрузчика
; Real mode bootloader
; ============================================== CODE ==========================================================
use16 ; generate 16-bit code
org 7C00h ; the code starts at 0x7C00 memory address
start:
jmp far dword 0x0000:entr ; makes CS=0, IP=entr
entr:
xor ax, ax ; ax = 0
mov ds, ax ; setup data segment ds=ax=0
cli ; when we set up stack we need disable interrupts because stack is involved in interrupts handling
mov ss, ax ; setup stack segment ss=ax=0
mov sp, 0x7C00 ; stack will grow starting from 0x7C00 memory address
sti ; enable interrupts
mov byte [disk_id], dl ; save disk identifier (it appears in dl after the start of PC)
; Print message
push message
call BIOS_PrintString
; Reset disk subsystem
push [disk_id]
call BIOS_ResetDiskSubsystem
push [disk_id] ; push disk identifier
push dword 1 ; push LBA of the 1st sector to load
push 1 ; push number of sectors to load
push second_sector ; push memory address at which sectors are to be loaded
call BIOS_LoadSectorsSmart
jmp second_sector
include 'bios_services.inc'
; =============================================== DATA =========================================================
disk_id dw 0x0000 ; disk identifier (0...3 - floppy disks; greater or equal to 0x80 - other disk types)
message db 'Starting bootloader...', 0x0D, 0x0A, 0
finish:
; The size of a disk sector is 512 bytes. Boot sector signature occupies the two last bytes.
; The gap between the end of the source code and the boot sector signature is filled with zeroes.
times 510-finish+start db 0
db 55h, 0AAh ; boot sector signature
; =============================================== 2nd SECTOR ===================================================
second_sector:
; Print message from the 2nd sector
push message_sector2
call BIOS_PrintString
cli ; disable maskable interrupts
hlt ; halt the processor
message_sector2 db 'The second sector has been loaded successfully!', 0x0D, 0x0A, 0
use16 ; generate 16-bit code
; <---- BIOS_ClearScreen ----------
BIOS_ClearScreen:
mov ax, 0x0003 ; ah=0 means clear screen and setup video mode, al=3 means text mode, 80x25 screen, CGA/EGA adapter, etc.
int 0x10 ; call BIOS standard video service function
ret
; -------------------------------->
; <---- BIOS_PrintString ----------
; Prints out a string. The string must be zero-terminated.
; Parameters: word [bp + 4] - address of the string
BIOS_PrintString:
push bp
mov bp, sp
; save registers in stack
push ax
push bx
push si
mov si, [bp + 4] ; si = string address
cld ; clear direction flag (DF is a flag used for string operations)
mov ah, 0x0E ; (int 0x10) BIOS function index (write a charachter to the active video page)
mov bh, 0x00 ; (int 0x10) video page number
.puts_loop:
lodsb ; load to al the next charachter located at [si] address in memory (si is incremented automatically because the direction flag DF = 0)
test al, al ; zero in al means the end of the string
jz .puts_loop_exit
int 0x10 ; call BIOS standard video service
jmp .puts_loop
.puts_loop_exit:
; restore registers from stack
pop si
pop bx
pop ax
mov sp, bp
pop bp
ret 2
; -------------------------------->
; <---- BIOS_ResetDiskSubsystem ---
; Resets disk subsystem.
; Parameters: word [bp + 4] - disk identifier
BIOS_ResetDiskSubsystem:
push bp
mov bp, sp
push dx ; save dx in stack
mov dl, [bp + 4]
.reset:
mov ah, 0 ; (int 0x13) Device reset. Causes controller recalibration.
int 0x13 ; call disk input/output service
jc .reset ; if CF=1 an error has happened and we try again.
pop dx ; restore dx
mov sp, bp
pop bp
ret 2
; -------------------------------->
; <---- BIOS_GetDiskParameters ----
; Determines parameters of a specified disk.
; Parameters: word [bp + 4] - disk identifier
; Returns: dl - overall number of disks in the system
; dh - maximum head index
; ch - maximum cylinder index
; cl - number of sectors per a track
; Remarks: floppy disk 1.44 MB has 2 heads (0 and 1), 80 tracks (0...79) and 18 sectors per a track (1...18)
BIOS_GetDiskParameters:
push bp
mov bp, sp
push es ; save es in stack
; set ES:DI to 0000h:0000h to work around some buggy BIOS
;(http://en.wikipedia.org/wiki/INT_13H)
xor ax, ax ; ax = 0
mov es, ax ; es = 0
xor di, di ; di = 0
mov dl, [bp + 4] ; drive index
mov ah, 0x08 ; read drive parameters
int 0x13 ; call disk input/output service
pop es ; restore es
mov sp, bp
pop bp
ret 2
; -------------------------------->
; <---- BIOS_IsExDiskServiceSupported --
; Returns 1 (true) if BIOS supports extended disk service and 0 (false) otherwise.
; Returns: ax=1 or ax=0
BIOS_IsExDiskServiceSupported:
mov ah, 0x41 ; check extensions present
mov bx, 0x55AA ; signature
int 0x13 ; call disk input/output service
jc .not_supported ; if extended disk service is not supported CF flag will be set to 1
mov ax, 1 ; ax = 1
ret
.not_supported:
xor ax, ax ; ax = 0
ret
; -------------------------------->
; <---- LBAtoCHS ------------------
; Function accepts linear sector number (Linear Block Address - LBA) and converts it into a format CYLINDER:HEAD:SECTOR (CHS).
; Parameters: dword [bp + 10] - LBA
; word [bp + 6] - Sectors per Track (SPT)
; word [bp + 4] - Heads per Cylinder (HPC)
; Returns: ch - CYLINDER
; dh - HEAD
; cl - SECTOR
; Remarks: LBA = ((CYLINDER * HPC + HEAD) * SPT) + SECTOR - 1
; CYLINDER = LBA / ( HPC * SPT )
; temp = LBA % ( HPC * SPT )
; HEAD = temp / SPT
; SECTOR = temp % SPT + 1
LBAtoCHS:
push bp
mov bp, sp
push ax ; save ax in stack
; dx:ax = LBA
mov dx, [bp + 10]
mov ax, [bp + 8]
movzx cx, byte [ebp + 6] ; cx = SPT
div cx ; divide dx:ax (LBA) by cx (SPT) (AX = quotient, DX = remainder)
mov cl, dl ; CL = SECTOR = remainder
inc cl ; sectors are indexed starting from 1
div byte [bp + 4] ; AL = LBA % HPC; AH = remainder
mov dh, ah ; DH = HEAD = remainder
mov ch, al ; CH = CYLINDER = quotient
pop ax ; restore ax from stack
mov sp, bp
pop bp
ret 8
; -------------------------------->
; <---- BIOS_LoadSectors ----------
; Loads sequential sectors from disk into RAM.
; Parameters: word [bp + 12] - disk identifier
; dword [bp + 8] - Linear Block Address (LBA) of the first sector to load
; word [bp + 6] - number of sectors to load
; word [bp + 4] - memory address (in data segment) at which sectors are to be loaded
; Remarks: this function allows for loading sectors from disk within first 8 GiB
BIOS_LoadSectors:
push bp
mov bp, sp
; save necessary registers to stack
push es
push ax
push bx
push dx
mov ax, ds
mov es, ax
mov dl, [bp + 12] ; DL = disk identifier
call BIOS_GetDiskParameters
push dword [bp + 8] ; push LBA
push cx ; push Sectors Per Track
push dx ; push Heads Per Cylinder
call LBAtoCHS ; ch:dh:cl = Cylinder:Head:Sector
.read:
mov dl, [bp + 12] ; DL = disk identifier
mov bx, [bp + 4] ; ES:BX = memory address at which sectors are to be loaded
mov al, [bp + 6] ; AL = number of sectors to load
mov ah, 0x02 ; (int 0x13) Read Sectors From Drive function
int 0x13 ; call disk input/output service
jc .read ; if CF=1, an error occured and we try again
; restore registers from stack
pop dx
pop bx
pop ax
pop es
mov sp, bp
pop bp
ret 10
; -------------------------------->
; <---- BIOS_LoadSectorsEx --------
; Loads sequential sectors from disk into RAM (uses extended disk input/output service).
; Parameters: word [bp + 12] - disk identifier
; dword [bp + 8] - Linear Block Address (LBA) of the first sector to load
; word [bp + 6] - number of sectors to load
; word [bp + 4] - memory address (in data segment) at which sectors are to be loaded
; Remarks: this function assumes that stack segment and data segment are the same (ss == ds)
; this function allows for loading sectors from disk within first 2 TiB
BIOS_LoadSectorsEx:
push bp
mov bp, sp
sub sp, 16 ; allocate memory (16 bytes) in stack for storing a special
; structure needed for calling bios load-from-disk service
; save necessary registers to stack
push ax
push dx
push si
mov byte[bp - 16], 16 ; structure size = 16 bytes
mov byte[bp - 15], 0 ; unused, should be zero
mov ax, [bp + 6] ; number of sectors to load
mov [bp - 14], ax
mov ax, [bp + 4] ; memory address at which sectors are to be loaded
mov [bp - 12], ax
mov ax, ds ; segment to which sectors are to be loaded
mov [bp - 10], ax
mov ax, [bp + 8] ; 64-bit sector number (1st word)
mov [bp - 8], ax
mov ax, [bp + 10] ; 64-bit sector number (2nd word)
mov [bp - 6], ax
mov word[bp - 4], 0 ; 64-bit sector number (3rd word)
mov word[bp - 2], 0 ; 64-bit sector number (4th word)
mov dl, [bp + 12] ; DL = disk identifier
lea si, [bp - 16] ; si = structure's address
mov ah, 0x42 ; (int 0x13) Extended Read Sectors From Drive function
int 0x13 ; call disk input/output service
; restore registers from stack
pop si
pop dx
pop ax
mov sp, bp
pop bp
ret 10
; -------------------------------->
; <---- BIOS_LoadSectorsSmart -----
; Loads sequential sectors from disk into RAM.
; Parameters: word [bp + 12] - disk identifier
; dword [bp + 8] - Linear Block Address (LBA) of the first sector to load
; word [bp + 6] - number of sectors to load
; word [bp + 4] - memory address (in data segment) at which sectors are to be loaded
; Remarks: this function assumes that stack segment and data segment are the same (ss == ds)
BIOS_LoadSectorsSmart:
call BIOS_IsExDiskServiceSupported
cmp ax, 0
je BIOS_LoadSectors
jmp BIOS_LoadSectorsEx
; -------------------------------->
Объяснение работы программы
Идентификатор диска
Как вам уже известно, первые 512 байт нашей программы загружаются с дискеты. Но ведь мы могли бы записать нашу программу не только на дискету, но и на CD или даже на жесткий диск. Чтобы программа могла узнать, с какого именно носителя она была скопирована в оперативную память, BIOS помещает в регистр dl целое число — так называемый идентификатор диска. Зачем программе знать, с какого носителя она была загружена? Чтобы дозагрузить свою часть, оставшуюся на этом носителе. Как мы можем использовать идентификатор диска? В BIOS есть функции, которые относятся к т. н. сервису дискового ввода-вывода (disk input/output service) — прерывание 0x13. Эти функции принимают идентификатор диска в качестве параметра. Для дискет идентификатор может принимать значения от 0 до 3, для всех остальных дисков — значения от 128 и выше.
Дисковый сервис
Чтобы скопировать сектора с диска в оперативную память, мы можем воспользоваться функцией 0x02 (ah=0x02) дискового сервиса BIOS (int 0x13). Но тут есть одна проблема: функция, которая загружает сектора в память, принимает в качестве параметра адрес 1-ого загружаемого сектора в формате Цилиндр:Головка:Сектор (Cylinder:Head:Sector — CHS). Программисту оперировать адресами секторов в таком формате неудобно, ему более привычен т. н. формат LBA (Linear Block Address), в котором сектора диска предстают в виде однородного массива. Поэтому программисту приходится преобразовывать один формат в другой (LBA to CHS).
Преобразование линейного адреса сектора (LBA) в формат Цилиндр:Головка:Сектор (CHS)
Прежде всего, вы должны понимать, как устроен диск. Например, жесткий диск — это набор круглых стеклянных пластин, покрытых ферромагнитным материалом и нанизанных на вращающийся шпиндель (ось). У каждой пластины есть две поверхности, и над каждой поверхностью расположена считывающая/записывающая головка (head). Поверхность состоит из концентрических кругов, которые называются дорожками (tracks). Совокупность дорожек одинакового радиуса, расположенных на разных пластинах, называется цилиндром (cylinder). Дорожки делятся на сектора размером 512 байт. В процессе считывания или записи головка располагается над нужной дорожкой и ждет, когда под ней проедет нужный сектор. Формат жесткого и гибкого диска одинаков. Подробнее читайте тут: Wikipedia — Cylinder-head-sector. Есть простые формулы преобразования LBA в CHS и наоборот, они приведены в исходном коде функции LBAtoCHS нашей программы.
Определение параметров диска
Определение параметров диска (количество головок, цилиндров и секторов на одну дорожку) необходимо, чтобы мы могли преобразовать адрес LBA в адрес CHS. Параметры диска можно получить при помощи функции 0x08 прерывания 0x13. Входной параметр для этой функции — идентификатор диска. Я поместил определение параметров диска в функцию BIOS_GetDiskParameters. Однако есть в BIOS функция, которая избавит нас от необходимости получать параметры диска и преобразовывать LBA в CHS — она называется «расширенный дисковый сервис».
Расширенный дисковый сервис
Расширенный дисковый сервис может поддерживаться либо не поддерживаться той или иной версией BIOS. Узнать, поддерживается ли он можно, вызвав функцию 0x41 прерывания 0x13. Если сервис поддерживается, то флаг переноса CF будет сброшен в 0, в противном случае — установлен в 1. Все это я поместил в функцию BIOS_IsExDiskServiceSupported. Загрузить секторы диска в память при помощи расширенного дискового сервиса можно, вызвав функцию 0x42 прерывания 0x13. Функция принимает в качестве параметра указатель на специальную структуру, которую мы должны создать в оперативной памяти (целесообразно делать это в стеке). Структура содержит LBA первого сектора и количество секторов для загрузки, а также адрес в памяти, по которому надо загружать сектора. Вызов функции расширенного дискового сервиса я поместил в функцию BIOS_LoadSectorsEx.
Отладчик Bochs
Программа стала достаточно сложной, поэтому при ее написании я допускал различные ошибки. О наличии ошибок программист узнает обычно, когда программа у него не работает. Как узнать, в чем конкретно состоит ошибка? Для этого надо выполнить программу пошагово (т. е. с остановкой после каждой машинной команды) и выяснить, в каком месте программа делает не то, что нужно. Этот процесс называется отладкой. Виртуальная машина Bochs позволяет программисту выполнять отладку — т. е. пошагово выполнять программу, ставить точки останова, манипулировать регистрами и оперативной памятью компьютера.
В папке установки Bochs (у меня это C:\Program Files (x86)\Bochs-2.6.9) находится файл bochsdbg.exe. Это версия виртуальной машины, предназначенная специально для отладки программ. После ее запуска и нажатия вами в окне Bochs Start Menu кнопки Start, машина остановится на первой же машинной инструкции BIOS, и в окне Bochs for Windows — Console вы увидите приглашение bochs:1>
. Далее вы можете вводить различные команды для отладки, их полный список приведен в разделе Using Bochs internal debugger документации, которая поставляется в составе дистрибутива Bochs (у меня документация находится в папке C:\Program Files (x86)\Bochs-2.6.9\docs). Вы можете например пошагово выполнять программу при помощи команды step
, ставить точки останова при помощи команды lbreak
, просматривать содержимое регистров при помощи команды registers
и многое другое (обратите внимание, что у команд есть сокращенные варианты).
После старта машина останавливается на первой команде BIOS. Нас программа в BIOS вряд ли интересует, нас интересует наша собственная программа, поэтому после запуска отладчика полезно установить точку останова на машинную команду с линейным адресом 0x7c00 (адрес, по которому с диска в память копируется наша программа) и продолжить выполнение программы до тех пор, пока не встретится точка останова:
continue
Файл листинга
Если вы отлаживаете программу в Bochs, то вам очень пригодится файл листинга — файл, который содержит в человекочитаемом виде следующую информацию:
- исходный код вашей программы на ассемблере
- машинные коды ассемблерных команд в шестнадцатеричном виде
- данные в шестнадцатеричном виде
- адреса машинных команд в памяти и адреса относительно начала двоичного файла (Relative Virtual Address — RVA)
Без этой информации отлаживать сколько-нибудь сложную программу в Bochs практически невозможно. Вы ведь должны знать, по каким адресам в памяти ставить точки останова и по каким адресам в памяти расположены ваши переменные.
Если вы программируете на FASM’е, то для того чтобы сгенерировать листинг, вам надо сначала сгенерировать так называемый файл символов — symbolic information file. Это можно сделать, указав ключ -s
в командной строке компилятора fasm.exe, например:
Здесь source.asm — файл исходного кода, source.fas — файл символов, который генерируется компилятором из файла исходного кода.
Затем вы вызываете утилиту listing.exe, которая и генерирует файл листинга:
Здесь source.fas — файл символов, source.lst — файл листинга, который нам и нужен.
Но тут есть небольшая проблема: утилита listing.exe поставляется в составе дистрибутива FASM, но не в виде скомпилированной программы, а в виде исходного кода на ассемблере, который вы должны сами скомпилировать. Исходный код программы listing.exe расположен в файле C:\FASM\TOOLS\WIN32\LISTING.ASM
. Этот исходный код написан на ассемблере FASM. Попытка скомпилировать этот файл компилятором fasm.exe из командной строки не приводит к успеху — компилятор не может найти заголовочные файлы (с расширением .inc
), на которые ссылался файл исходного кода. Оказывается, в этом файле есть директивы INCLUDE, которые ссылаются на различные заголовочные файлы, причем путь к этим заголовочным файлам не указан (поиск по папке установки FASM показал, что нужные заголовочные файлы лежат в папках TOOLS
и INCLUDE
). Решение проблемы нашлось в файле руководства C:\FASM\FASM.PDF
в разделе 1.1.1 System requirements: надо либо создать переменную окружения INCLUDE и присвоить ей путь к папке C:\FASM\INCLUDE
, либо запустить графический вариант компилятора, который называется fasmw.exe. После первого запуска fasmw.exe в той же папке появится файл FASMW.INI. В этом файле надо добавить в самое начало следующий текст:
Include=C:\FASM\INCLUDE
Затем в программе fasmw.exe мы открываем файл C:\FASM\TOOLS\WIN32\LISTING.ASM
и щелкаем пункт меню Run > Compile
, после чего в папке C:\FASM\TOOLS\WIN32
появится файл LISTING.EXE.
Про генерацию листинга всё. Информация была почерпнута мной из файлов C:\FASM\TOOLS\README.TXT
и C:\FASM\FASM.PDF
(раздел 1.1.1 System requirements).
Makefile
Поскольку теперь я хотел, чтобы при построении программы генерировался файл листинга, я должен был написать новый makefile, текст которого приведен ниже:
floppy.img : boot_sector.bin
dd if="/dev/zero" of="floppy.img" bs=1024 count=1440
dd if=boot_sector.bin of=floppy.img conv=notrunc
boot_sector.bin : boot_sector.asm bios_services.inc
fasm boot_sector.asm boot_sector.bin
boot_sector.lst: boot_sector.fas
listing -a boot_sector.fas boot_sector.lst
boot_sector.fas: boot_sector.asm bios_services.inc
fasm boot_sector.asm -s boot_sector.fas
В следующей заметке мы займемся обработкой прерываний в защищенном режиме.