Создание bootsector'а

Как я уже упоминал, boot sector загружается в память по адресу 0:7c00h и имеет длину 512 байт. Это не слишком много, поэтому возможности boot sector'a ограничиваются загрузкой какого либо вторичного загрузчика.

Наш boot sector, по образу и подобию linux, будет загружать в память два блока. Первым является тот самый вторичный загрузчик, у нас он, как и в linux, называется setup. Вторым является собственно ядро.

Этот boot sector служит для загрузки ядра с дискет, поэтому, на первых порах, он жестко привязан к диску "a:".

BIOS предоставляет возможность читать по нескольку секторов сразу, но не более чем до границы дорожки. Такая возможность, конечно, ускоряет чтение с диска, но представляет собой большие сложности в программировании, так как надо учитывать границы сегментов (в реальном режиме сегмент может быть не больше, чем 64к) и границы дорожек, получается достаточно хитрый алгоритм.

Я пошел немного другим путем. Я читаю с диска по секторам. Это, конечно, медленнее, но я думаю, что здесь скорость не очень критична. За то это гораздо проще и компактнее реализуется.

А теперь давайте разбираться, как это все работает.

%define SETUP_SEG 0x07e0
%define SETUP_SECTS 10

%define KERNEL_SEG      0x1000
%define KERNEL_SECTS 1000

Для начала описываем место и размер для каждого загружаемого блока.
Размеры пока произвольные, поскольку все остальное еще предстоит написать.

section .text
        BITS    16

        org     0x7c00

Как я уже говорил, boot sector загружается и запускается по адресу 0:7c00h Содержимое регистров при старте таково:

  • cs содержит 0
  • ip содержит 7с00h
Прерывания запрещены! Про содержание остальных регистров мне ничего не известно, если кто-то, что-то знает, напишите мне. Остальные регистры мы будем инициализировать самостоятельно.

entry_point:
        mov     ax, cs

        cli
        mov     ss, ax
        mov     sp, entry_point
        sti

        mov     ds, ax

Стек у нас будет располагаться перед программой, до служебной области BIOS еще остается порядка 30 килобайт, для стека больше чем достаточно. Прерывания изначально запрещены, но я все равно сделаю это самостоятельно, на всякий случай. и разрешу после установки стека. Никаких проблем это вызвать, по-моему, не должно.
Так же, нулевым значением, инициализируем сегментный регистр ds.

        ; Сохpаняем фоpму куpсоpа
        mov     ah, 3
        xor     bh, bh
        int     0x10

        push    cx

        ; отключаем куpсоp
        mov     ah, 1
        mov     ch, 0x20
        int     10h

Чтобы все было красиво и радовало глаз, мы на время чтения отключим курсор. Иначе он будет мелькать на экране. Чтобы его потом восстановить, как и был, мы сохраняем его форму в стеке.

        ; Загpужаем setup
        mov     ax, SETUP_SEG
        mov     es, ax

        mov     ax, 1
        mov     cx, SETUP_SECTS

        mov     si, load_setup_msg
        call    load_block

        call    outstring

        mov     si, complete_msg
        call    outstring

Загружаем первый блок (setup). Процедуру загрузки блока мы рассмотрим немного позже. А в остальном здесь, по-моему, все понятно.

        ; загpужаем ядpо.
        mov     ax, KERNEL_SEG
        mov     es, ax

        mov     ax, 1 + SETUP_SECTS
        mov     cx, KERNEL_SECTS

        mov     si, load_kernel_msg
        call    load_block

        call    outstring

        mov     si, complete_msg
        call    outstring

Загружаем второй блок (kernel). Здесь все в точности аналогично первому блоку.

        ; Восстанавливаем куpсоp
        pop     cx
        mov     ah, 1
        int     0x10

Восстанавливаем форму курсора.

        ; Пеpедаем упpавление на setup
        jmp     SETUP_SEG:0

На этом работа boot sector'а заканчивается. Дальним переходом мы передаем управление программе setup.

Далее располагаются функции.

; Загрузка блока
; cx - количество сектоpов
; ax - начальный сектоp
; es - указатедь на память
; si - loading message

Функция загрузки блока. Она же занимается выводом на экран процентного счетчика.

load_block:
        mov     di, cx ; сохpаняем количество блоков

 .loading:
        xor     bx, bx
        call    load_sector
        inc     ax
        mov     bx, es
        add     bx, 0x20
        mov     es, bx

        ; Выводим сообщение о загpузке.
        call    outstring

        push    ax

        ; Выводим пpоценты
        ; ((di - cx) / di) * 100
        mov     ax, di
        sub     ax, cx
        mov     bx, 100
        mul     bx
        div     di

        call    outdec

        push    si
        mov     si, persent_msg
        call    outstring
        pop     si

        pop     ax

        loop    .loading

        ret

В этой функции, по-моему, ничего сложного нет. Обыкновенный цикл.

А вот следующая функция загружает с диска отдельный сектор, при этом оперируя его линейным адресом.
Есть так называемое int13 extension, разработанное совместно фирмами MicroSoft и Intel. Это расширение BIOS работает почти аналогичным образом, Считывая сектора по их линейным адресам, но оно поддерживается не всеми BIOS, имеет несколько разновидностей и работает в основном для жестких дисков. Поэтому нам не подходит.

В своей работе мы пока ориентируемся только на чтение с floppy диска, размером 1,4 мегабайта. Поэтому будем использовать старомодную функцию, которой в качестве параметров задается номер дорожки, головки и сектора.

; Загрузка сектора
; ax - номеp сектоpа (0...max (2880))
; es:bx - адpес для pазмещения сектоpа.

Абсолютный номеp сектоpа вычисляется по фоpмуле: AbsSectNo = (CylNo * SectPerTrack * Heads) + (HeadNo * SectPerTrack) + (SectNo - 1) Значит обpатное спpаведливо так: CylNo = AbsSectNo / (SectPerTrack * Heads) HeadNo = остаток / SectorPerTrack SectNo = остаток + 1

load_sector: push ax push cx mov cl, 18 div cl mov cx, dx inc cx ; количество сектоpов

Поделив номер сектора на количество секторов на дорожке, мы в остатке получаем номер сектора на дорожке. Это значение хранится в 6 младших битах регистра cl.

        xor     dx, dx  ; dl - диск - 0!

Номер диска храниться в dl и устанавливается в 0 (это диск a:)

        shr     ax, 1
        rcl     dh, 1 ; номер головки

Младший бит частного определяет для нас номер головки. (0 или 1)

        mov     ch, al
        shl     ah, 4
        or      cl, ah ; количество доpожек

Оставшиеся биты частного определяют для нас номер цилиндра (или дорожки).
восемь младших бит номера хранятся в регистре ch, два старших бита номера хранятся в двух старших битах регистра cl.

 .rept:
        mov     ax, 0x201
        int     0x13

        jnc     .read_ok

        push    si
        mov     si, read_error
        call    outstring

        movzx   ax, ah
        call    outdec

        mov     si, crlf
        call    outstring

        xor     dl, dl
        xor     ah, ah
        int     0x13

        pop     si

        jmp     short .rept

В случае ошибки чтения мы не будем возвращать из функции какие-либо результаты, а будем повторять чтение, пока оно не окажется успешным. Ведь в случае неуспешного чтения у нас все равно ничего не будет работать! Для верности мы, в случае сбоя, производим сброс устройства.

 .read_ok:

        pop     cx
        pop     ax
        ret

Далее идет две интерфейсные функции, обеспечивающие вывод на экран строк и десятичных цифр. Ничего особенного они из себя не представляют а для вывода пользуются телетайпным прерыванием BIOS (ah = 0eh, int 10h), которое обеспечивает вывод одного символа с обработкой некоторых служебных кодов.

; Вывод стpоки.
; ds:si - стpока.

outstring:
        push    ax
        push    si

        mov     ah, 0eh

        jmp     short .out
 .loop:
        int     10h
 .out:
        lodsb
        or      al, al
        jnz     .loop

        pop     si
        pop     ax
        ret

Эта функция ограничена выводом чисел до 99 включительно, случай с большим числом обрабатывается как переполнение и отображается как '##'.

; Вывод десятичных чисел от 0 до 99
; ax - число!
outdec:
        push    ax
        push    si

        mov     bl, 10
        div     bl
        cmp     al, 10

        jnc     .overflow

        add     ax, '00'
        push    ax
        mov     ah, 0eh
        int     0x10
        pop     ax
        mov     al, ah
        mov     ah, 0eh
        int     0x10

        jmp     short .exit

 .overflow:
        mov     si, overflow_msg
        call    outstring

 .exit:
        pop     si
        pop     ax
        ret

Далее располагаются несколько служебных сообщений.

load_setup_msg:
        db      'Setup loading: ', 0

load_kernel_msg:
        db      'Kernel loading: ', 0

complete_msg:
        db      'complete.'

crlf:
        db      0ah, 0dh, 0

persent_msg:
        db      '%', 0dh, 0

overflow_msg:
        db      '##', 0

read_error:
        db      0ah, 0dh
        db      'Read error #', 0

        TIMES   510-($-$$) db 0

Эта комбинация заполняет оставшееся место в секторе нулями. А остается у нас еще около 200 байт.

        dw      0aa55h

Последние два байта называются "Partition table signature", что не совсем корректно. Фактически эта сигнатура говорит BIOS'у о том, что этот сектор является загрузочным.

Этот boot sector, помимо того, что читает по секторам, отличается от линуксового еще и размещением в памяти. После загрузки он не перемещает себя в памяти, и работает по тому же адресу, по которому его загрузил BIOS. Так же setup загружается непосредственно следом за boot sector'ом, с адреса 7e00h, что в принципе не помешает ему работать в других адресах, если мы будем загружать наше ядро через LILO, например.



Опубликовал admin
24 Апр, Суббота 2004г.



Программирование для чайников.