Основная проблема заключается в том, что вы не читаете все свое ядро в память. В конечном итоге ваш код выполняет неинициализированную память (скорее всего, заполненную нулями), достигает расширенной области данных BIOS (чуть ниже видеопамяти в 0xa0000), а затем в конечном итоге начинает выполнять видеопамяти в 0xa0000. QEMU не позволяет использовать видеопамять, поэтому вы получаете источник ошибки.
Исправить это не так просто, как может показаться на первый взгляд. Ваш код в моей системе был около 47300 байт. 1 сектор для MBR и 92 для ядра. Первая проблема заключается в том, что не все оборудование (и эмуляторы) могут считывать 92 сектора одновременно. QEMU и BOCH максимально на 72 для гибких дисков и 128 для жестких дисков. Это число может быть меньше для некоторых аппаратных средств (до числа секторов на дорожку).
Некоторое оборудование не будет читать секторы:
- Это выходит за пределы сегмента 64 КБ.
- Это охватывает более одного трека. Не все BIOS поддерживают многодорожечное чтение и запись. QEMU и BOCHS поддерживают их.
- Если BIOS использует передачи прямого доступа к памяти (DMA) для доступа к диску, вы не сможете записать несколько секторов, которые пересекают границу 64 КБ (в физической памяти). Это означает, что вы не можете гарантировать успешную запись, если она начинается до физического адреса 0x10000 и заканчивается после. То же самое для 0x20000, 0x30000, 0x40000 ... 0x90000. QEMU и BOCHS не допускают перенос дисков через такие границы.
Простой способ загрузить ядро размером до 64 КБ с помощью BOCHS и QEMU - это прочитать 64 сектора (32 КБ) по физическому адресу 0x0000: 0x8000, а затем сделать вторую копию из 64 секторов в 0x1000: 0x0000. Вы можете прочитать ядро большего размера, прочитав дополнительные куски по 32 КБ. 512 байт между 0x0000: 0x7e00 и 0x0000: 0x8000 будут неиспользованными. Единственный реальный улов - определение значений сектора цилиндра (CHS) 1 , которые будут использоваться для чтения диска Int 21h / AH = 02 .
Другие вопросы:
- При чтении секторов диска в память вы должны установить в стек ( SS: SP ) место, которое вы случайно не перезапишите. Если вы загружаете ядро после загрузчика, хорошее расположение будет SS: SP 0x0000: 0x7c000 ниже загрузчика. Чтобы избежать прерываний, возникающих при установке SS: SP , установите SP в инструкции, следующей сразу за инструкцией, которая загружает SS .
- Никогда не полагайтесь на значение любого регистра общего назначения или сегмента, содержащего ожидаемое вами значение. DL является исключением, поскольку почти во всех случаях на современном оборудовании он будет содержать номер загрузочного диска. См. Мои советы по загрузчику для получения дополнительной информации.
- QEMU и другие эмуляторы могут не считывать сектора, которых нет в файле. Если вы читаете больше секторов, чем указано в образе диска, чтение сектора может завершиться ошибкой. Чтобы обойти это, создайте образ диска (удобно использовать дискету размером 1,44 МБ) и скопируйте содержимое ядра и загрузчика в начало файла без усечения образа диска. DD может использоваться для этой цели.
- Чтобы облегчить отладку, а не выводить сценарий компоновщика как двоичный , по умолчанию используйте его для вывода в формате ELF. Используйте OBJCOPY, чтобы скопировать файл ELF в двоичный файл. Файл ELF может использоваться для хранения отладочной информации. Это полезно при использовании QEMU и GDB в качестве удаленного отладчика.
- Вы не можете полагаться на память, содержащую нули. GCC требует, чтобы раздел
.bss
был заполнен нулями. Используйте скрипт компоновщика, чтобы определить экстенты раздела .bss
и обнулить память перед вызовом точки входа C .
- Перед вызовом точки входа C GCC требует, чтобы флаг направления (DF) был очищен, чтобы строковые инструкции по умолчанию передавали движение вперед.
- В вашем make-файле вы используете GCC для создания ссылок.Если кросс-компилятор не используется, GCC может сгенерировать специальный раздел с именем
.note.gnu.build-id
, который может помешать вашему скрипту компоновщика.Чтобы это исправить, вы можете указать GCC подавить этот специальный раздел с помощью LDFLAGS:=-Wl,--build-id=none
.Если вы связались с LD напрямую, этот раздел не был бы создан.
С учетом всех этих изменений:
linker.ld :
ENTRY(boot)
SECTIONS {
. = 0x7c00;
.boot :
{
*(.boot)
}
/* Place kernel right after boot sector on disk but set the
* VMA (ORiGin point) to 0x8000 */
. = 0x8000;
__kernel_start = .;
__kernel_start_seg = __kernel_start >> 4;
.text : AT(0x7e00)
{
*(.text.start)
*(.text*)
}
.rodata :
{
*(.rodata*)
}
.data :
{
*(.data)
}
/* Compute number of sectors that the kernel uses */
__kernel_end = .;
__kernel_size_sectors = (__kernel_end - __kernel_start + 511) / 512;
.bss :
{
__bss_start = .;
*(COMMON)
*(.bss)
. = ALIGN(4);
__bss_end = .;
/* Compute number of DWORDS that BSS section uses */
__bss_sizel = (__bss_end - __bss_start) / 4;
}
}
boot.asm :
section .boot
bits 16 ; We're working at 16-bit mode here
global boot
boot:
xor ax, ax
mov ds, ax
mov ss, ax
mov sp, 0x7c00 ; Set SS:SP just below bootloader
cld ; DF=0 : string instruction forward movement
mov ax, 0x2401
int 0x15 ; Enable A20 bit
mov ax, 0x3 ; Set VGA text mode 3
int 0x10 ; Otherwise, call interrupt for printing the char
mov [disk],dl
; Read 64 sectors from LBA 1, CHS=0,0,2 to address 0x0800:0
mov ax, 0x0800
mov es, ax ;ES = 0x800
mov ah, 0x2 ;read sectors
mov al, 64 ;sectors to read
mov ch, 0 ;cylinder idx
mov dh, 0 ;head idx
mov cl, 2 ;sector idx
mov dl, [disk] ;disk idx
mov bx, 0 ;target pointer, ES:BX=0x0800:0x0000
int 0x13
; Read 64 sectors from LBA 65, CHS=1,1,12 to address 0x1000:0
mov ax, 0x1000
mov es, ax ;ES=0x1000
mov ah, 0x2 ;read sectors
mov al, 64 ;sectors to read
mov ch, 1 ;cylinder idx
mov dh, 1 ;head idx
mov cl, 12 ;sector idx
mov dl, [disk] ;disk idx
mov bx, 0x0000 ;target pointer, ES:BX=0x1000:0x0000
int 0x13
cli ; Disable the interrupts
lgdt [gdt_pointer] ; Load the gdt table
mov eax, cr0 ; Init swap cr0...
or eax,0x1 ; Set the protected mode bit on special CPU reg cr0
mov cr0, eax
jmp CODE_SEG:boot32 ; Long jump to the code segment
; base a 32 bit value describing where the segment begins
; limit a 20 bit value describing where the segment ends, can be multiplied by 4096
; if granularity = 1
; present must be 1 for the entry to be valid
; ring level an int between 0-3 indicating the kernel Ring Level
; direction:
; > 0 = segment grows up from base, 1 = segment grows down for a data segment
; > 0 = can only execute from ring level, 1 = prevent jumping to higher ring levels
; read/write if you can read/write to this segment
; accessed if the CPU has accessed this segment
; granularity 0 = limit is in 1 byte blocks, 1 = limit is multiples of 4KB blocks
; size 0 = 16 bit mode, 1 = 32 bit protected mode
gdt_start:
dq 0x0
gdt_code:
dw 0xFFFF
dw 0x0
db 0x0
db 10011010b
db 11001111b
db 0x0
gdt_data:
dw 0xFFFF
dw 0x0
db 0x0
db 10010010b
db 11001111b
db 0x0
gdt_end:
gdt_pointer:
dw gdt_end - gdt_start
dd gdt_start
disk:
db 0x0
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
;; Magic numbers
times 510 - ($ - $$) db 0
dw 0xaa55
section .data
msg: db "Hello, World more than 512 bytes!", 0
bits 32
section .text.start
boot32:
mov ax, DATA_SEG
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esi, msg ; SI now points to our message
mov ebx, 0xb8000 ; vga memory position (0)
.loop:
lodsb ; Loads SI into AL and increments SI [next char]
or al, al ; Checks if the end of the string
jz halt ; Jump to halt if the end
or eax,0x0200 ; The top byte defines the character colour in the buffer as
; an int value from 0-15 with 0 = black, 1 = blue and 15 = white.
; The bottom byte defines an ASCII code point
mov word [ebx], ax
add ebx, 2
jmp .loop ; Next iteration of the loop
halt:
mov esp, kernel_stack_top
extern __start
extern __bss_start
extern __bss_sizel
; Zero the BSS section
mov ecx, __bss_sizel
mov edi, __bss_start
xor eax, eax
rep stosd
; Call C entry point
call __start
cli
hlt ; CPU command to halt the execution
section .bss
align 4
kernel_stack_bottom:
resb 16384 ; 16 KB stack
kernel_stack_top:
Изменить makefile , добавив следующие переменные make:
OC:=objcopy
DD:=dd
ELF:=$(DEPLOY)/boot.elf
Изменить makefile , изменив LDFLAGS
на:
LDFLAGS:=-Wl,--build-id=none
Измените makefile , изменив правило all
на:
all: $(DEPENDENCIES)
mkdir -p $(DEPLOY)
mkdir -p $(BUILD)
$(NASM) $(SRC_NASM) -f elf32 -o $(OBJ_NASM)
$(CC) $(SRC_C) $(OBJ_NASM) -o $(ELF) $(CFLAGS) -T $(LINKER) $(LDFLAGS)
$(OC) -O binary $(ELF) $(BIN)
$(DD) if=/dev/zero of=$(BIN).tmp count=1440 bs=1024
$(DD) if=$(BIN) of=$(BIN).tmp conv=notrunc
mv $(BIN).tmp $(BIN)
Альтернативное решение
Учитывая, что существует множество способовчтение с Int 13 / AH = 2 может привести к сбою, можно избежать большинства проблем, читая по одному сектору за раз и всегда читая в область памяти, равномерно делимую на 512.
Когдаиспользуя сценарий компоновщика для создания загрузчика вдоль ядра, вы можете использовать компоновщик для определения размера ядра и вычисления количества секторов, которые необходимо прочитать.
Пересмотр предыдущего кода выше, который можетВыполнить необходимую работу можно следующим образом.
linker.ld
ENTRY(boot)
SECTIONS {
. = 0x7c00;
.boot :
{
*(.boot)
}
__kernel_start = .;
__kernel_start_seg = __kernel_start >> 4;
.text :
{
*(.text.start)
*(.text*)
}
.rodata :
{
*(.rodata*)
}
.data :
{
*(.data)
}
/* Compute number of sectors that the kernel uses */
__kernel_end = .;
__kernel_size_sectors = (__kernel_end - __kernel_start + 511) / 512;
.bss :
{
__bss_start = .;
*(COMMON)
*(.bss)
. = ALIGN(4);
__bss_end = .;
/* Compute number of DWORDS that BSS section uses */
__bss_sizel = (__bss_end - __bss_start) / 4;
}
}
Основным отличием являетсячто этот скрипт компоновщика начинает загрузку ядра в физическую память с 0x07e00 вместо 0x08000.Более усовершенствованный boot.asm может использовать значения, сгенерированные компоновщиком, для циклического обхода необходимых секторов, считывая их по одному до завершения:
extern __kernel_size_sectors ; Size of kernel in 512 byte sectors
extern __kernel_start_seg ; Segment start of kernel will be laoded at
global boot
STAGE2_LBA_START equ 1 ; Logical Block Address(LBA) Stage2 starts on
; LBA 1 = sector after boot sector
; Logical Block Address(LBA) Stage2 ends at
STAGE2_LBA_END equ STAGE2_LBA_START + __kernel_size_sectors
DISK_RETRIES equ 3 ; Number of times to retry on disk error
bits 16
section .boot
boot:
; Include a BPB (1.44MB floppy with FAT12) to be more compatible with USB floppy media
;%include "src/init/bpb.inc"
boot_start:
xor ax, ax ; DS=SS=ES=0 for stage2 loading
mov ds, ax
mov ss, ax ; Stack at 0x0000:0x7c00
mov sp, 0x7c00
cld ; Set string instructions to use forward movement
; Read Stage2 1 sector at a time until stage2 is completely loaded
load_stage2:
mov [bootDevice], dl ; Save boot drive
mov di, __kernel_start_seg ; DI = Current segment to read into
mov si, STAGE2_LBA_START ; SI = LBA that stage2 starts at
jmp .chk_for_last_lba ; Check to see if we are last sector in stage2
.read_sector_loop:
mov bp, DISK_RETRIES ; Set disk retry count
call lba_to_chs ; Convert current LBA to CHS
mov es, di ; Set ES to current segment number to read into
xor bx, bx ; Offset zero in segment
.retry:
mov ax, 0x0201 ; Call function 0x02 of int 13h (read sectors)
; AL = 1 = Sectors to read
int 0x13 ; BIOS Disk interrupt call
jc .disk_error ; If CF set then disk error
.success:
add di, 512>>4 ; Advance to next 512 byte segment (0x20*16=512)
inc si ; Next LBA
.chk_for_last_lba:
cmp si, STAGE2_LBA_END ; Have we reached the last stage2 sector?
jl .read_sector_loop ; If we haven't then read next sector
.stage2_loaded:
jmp stage2 ; Jump to second stage
.disk_error:
xor ah, ah ; Int13h/AH=0 is drive reset
int 0x13
dec bp ; Decrease retry count
jge .retry ; If retry count not exceeded then try again
error_end:
; Unrecoverable error; print drive error; enter infinite loop
mov si, diskErrorMsg ; Display disk error message
call print_string
cli
.error_loop:
hlt
jmp .error_loop
; Function: print_string
; Display a string to the console on display page 0
;
; Inputs: SI = Offset of address to print
; Clobbers: AX, BX, SI
print_string:
mov ah, 0x0e ; BIOS tty Print
xor bx, bx ; Set display page to 0 (BL)
jmp .getch
.repeat:
int 0x10 ; print character
.getch:
lodsb ; Get character from string
test al,al ; Have we reached end of string?
jnz .repeat ; if not process next character
.end:
ret
; Function: lba_to_chs
; Description: Translate Logical block address to CHS (Cylinder, Head, Sector).
; Works for all valid FAT12 compatible disk geometries.
;
; Resources: http://www.ctyme.com/intr/rb-0607.htm
; https://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion
; https://stackoverflow.com/q/45434899/3857942
; Sector = (LBA mod SPT) + 1
; Head = (LBA / SPT) mod HEADS
; Cylinder = (LBA / SPT) / HEADS
;
; Inputs: SI = LBA
; Outputs: DL = Boot Drive Number
; DH = Head
; CH = Cylinder (lower 8 bits of 10-bit cylinder)
; CL = Sector/Cylinder
; Upper 2 bits of 10-bit Cylinders in upper 2 bits of CL
; Sector in lower 6 bits of CL
;
; Notes: Output registers match expectation of Int 13h/AH=2 inputs
;
lba_to_chs:
push ax ; Preserve AX
mov ax, si ; Copy LBA to AX
xor dx, dx ; Upper 16-bit of 32-bit value set to 0 for DIV
div word [sectorsPerTrack] ; 32-bit by 16-bit DIV : LBA / SPT
mov cl, dl ; CL = S = LBA mod SPT
inc cl ; CL = S = (LBA mod SPT) + 1
xor dx, dx ; Upper 16-bit of 32-bit value set to 0 for DIV
div word [numHeads] ; 32-bit by 16-bit DIV : (LBA / SPT) / HEADS
mov dh, dl ; DH = H = (LBA / SPT) mod HEADS
mov dl, [bootDevice] ; boot device, not necessary to set but convenient
mov ch, al ; CH = C(lower 8 bits) = (LBA / SPT) / HEADS
shl ah, 6 ; Store upper 2 bits of 10-bit Cylinder into
or cl, ah ; upper 2 bits of Sector (CL)
pop ax ; Restore scratch registers
ret
; Uncomment these lines if not using a BPB (via bpb.inc)
%ifndef WITH_BPB
numHeads: dw 2 ; 1.44MB Floppy has 2 heads & 18 sector per track
sectorsPerTrack: dw 18
%endif
bootDevice: db 0x00
diskErrorMsg: db "Unrecoverable disk error!", 0
; Pad boot sector to 510 bytes and add 2 byte boot signature for 512 total bytes
TIMES 510-($-$$) db 0
dw 0xaa55
section .data
msg: db "Hello, World more than 512 bytes!", 0
; base a 32 bit value describing where the segment begins
; limit a 20 bit value describing where the segment ends, can be multiplied by 4096
; if granularity = 1
; present must be 1 for the entry to be valid
; ring level an int between 0-3 indicating the kernel Ring Level
; direction:
; > 0 = segment grows up from base, 1 = segment grows down for a data segment
; > 0 = can only execute from ring level, 1 = prevent jumping to higher ring levels
; read/write if you can read/write to this segment
; accessed if the CPU has accessed this segment
; granularity 0 = limit is in 1 byte blocks, 1 = limit is multiples of 4KB blocks
; size 0 = 16 bit mode, 1 = 32 bit protected mode
gdt_start:
dq 0x0
gdt_code:
dw 0xFFFF
dw 0x0
db 0x0
db 10011010b
db 11001111b
db 0x0
gdt_data:
dw 0xFFFF
dw 0x0
db 0x0
db 10010010b
db 11001111b
db 0x0
gdt_end:
gdt_pointer:
dw gdt_end - gdt_start
dd gdt_start
disk:
db 0x0
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
bits 16
section .text.start
stage2:
cli ; Disable the interrupts
mov ax, 0x2401
int 0x15 ; Enable A20 bit
lgdt [gdt_pointer] ; Load the gdt table
mov eax, cr0 ; Init swap cr0...
or eax,0x1 ; Set the protected mode bit on special CPU reg cr0
mov cr0, eax
jmp CODE_SEG:startpm ; FAR JMP to the code segment
bits 32
startpm:
mov ax, DATA_SEG
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esi, msg ; SI now points to our message
mov ebx, 0xb8000 ; vga memory position (0)
.loop:
lodsb ; Loads SI into AL and increments SI [next char]
or al, al ; Checks if the end of the string
jz halt ; Jump to halt if the end
or eax,0x0200 ; The top byte defines the character colour in the
; buffer as an int value from 0-15 with 0 = black,
; 1 = blue and 15 = white.
; The bottom byte defines an ASCII code point
mov word [ebx], ax
add ebx, 2
jmp .loop ; Next iteration of the loop
halt:
mov esp, kernel_stack_top
extern __start
extern __bss_start
extern __bss_sizel
; Zero the BSS section
mov ecx, __bss_sizel
mov edi, __bss_start
xor eax, eax
rep stosd
; Call C entry point
call __start
cli
hlt ; CPU command to halt the execution
section .bss
align 4
kernel_stack_bottom:
resb 16384 ; 16 KB stack
kernel_stack_top:
Эта загрузка.asm свободно основан на загрузчике, который я предложил в другом вопросе Stackoverflow и ответе .Основное отличие состоит в том, что компоновщик вычисляет большую часть необходимой информации с помощью сценария компоновщика, а не кодируется / включается непосредственно в файл сборки.Этот код также перемещает включение линии A20 и переход в защищенный режим на второй этап.Это освобождает пространство, если вам необходимо расширить возможности загрузчика в будущем.
Если вы строите свой загрузчик для использования на реальном оборудовании в качестве неразмеченного носителя - копия блока параметров BIOS 1.44MiB(BPB) можно найти в файле bpb.inc .Это может быть полезно для загрузки с USB-носителя с использованием эмуляции дискеты (FDD).Чтобы включить его, просто удалите ;
из этой строки:
; %include "src/init/bpb.inc"
Сноски
1 Существует формула для преобразования нулевого логического адреса Bock в набор значений CHS:
C = LBA ÷ (HPC × SPT)
H = (LBA ÷ SPT) mod HPC
S = (LBA mod SPT) + 1
LBA 0 - это загрузочный сектор.Если ядро находится в смежных секторах после загрузчика, то начало ядра находится на LBA 1. Второй кусок ядра 32 КБ будет на LBA 65 (64 + 1).Для 1.44MiB дискеты HPC = 2 и SPT = 18.Из расчета LBA 0 = CHS (0,0,2) и LBA 65 = CHS (1,1,12).Это значения, используемые 64-секторными считываниями диска в первой версии boot.asm .