Изучаем архитектуру Intel x86-64 при помощи ассемблера (Часть 4 — Загрузчик)

В предыдущей заметке мы успешно перешли в защищенный режим процессора 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.

Исходный код загрузчика

; boot_sector.asm
; 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
; bios_services.inc
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 (адрес, по которому с диска в память копируется наша программа) и продолжить выполнение программы до тех пор, пока не встретится точка останова:

lbreak 0x7c00
continue

Файл листинга

Если вы отлаживаете программу в Bochs, то вам очень пригодится файл листинга — файл, который содержит в человекочитаемом виде следующую информацию:

  • исходный код вашей программы на ассемблере
  • машинные коды ассемблерных команд в шестнадцатеричном виде
  • данные в шестнадцатеричном виде
  • адреса машинных команд в памяти и адреса относительно начала двоичного файла (Relative Virtual Address — RVA)

Без этой информации отлаживать сколько-нибудь сложную программу в Bochs практически невозможно. Вы ведь должны знать, по каким адресам в памяти ставить точки останова и по каким адресам в памяти расположены ваши переменные.
Если вы программируете на FASM’е, то для того чтобы сгенерировать листинг, вам надо сначала сгенерировать так называемый файл символов — symbolic information file. Это можно сделать, указав ключ -s в командной строке компилятора fasm.exe, например:

fasm source.asm -s source.fas

Здесь source.asm — файл исходного кода, source.fas — файл символов, который генерируется компилятором из файла исходного кода.
Затем вы вызываете утилиту listing.exe, которая и генерирует файл листинга:

listing -a source.fas source.lst

Здесь 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. В этом файле надо добавить в самое начало следующий текст:

[Environment]
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, текст которого приведен ниже:

all: floppy.img boot_sector.lst

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

В следующей заметке мы займемся обработкой прерываний в защищенном режиме.

Добавить комментарий

Ваш адрес email не будет опубликован.