Как сделать ядро для моего загрузчика?
Я пытаюсь создать свою собственную ОС и мне нужна помощь с моим кодом. Это мой bootloader.asm:
[ORG 0x7c00]
start:
cli
xor ax, ax
mov ds, ax
mov ss, ax
mov es, ax
mov [BOOT_DRIVE], dl
mov bp, 0x8000
mov sp, bp
mov bx, 0x9000
mov dh, 5
mov dl, [BOOT_DRIVE]
call load_kernel
call enable_A20
call graphics_mode
lgdt [gdtr]
mov eax, cr0
or al, 1
mov cr0, eax
jmp CODE_SEG:init_pm
[bits 32]
init_pm:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000
mov esp, ebp
jmp 0x9000
[BITS 16]
graphics_mode:
mov ax, 0013h
int 10h
ret
load_kernel:
; load DH sectors to ES:BX from drive DL
push dx ; Store DX on stack so later we can recall
; how many sectors were request to be read ,
; even if it is altered in the meantime
mov ah , 0x02 ; BIOS read sector function
mov al , dh ; Read DH sectors
mov ch , 0x00 ; Select cylinder 0
mov dh , 0x00 ; Select head 0
mov cl , 0x02 ; Start reading from second sector ( i.e.
; after the boot sector )
int 0x13 ; BIOS interrupt
jc disk_error ; Jump if error ( i.e. carry flag set )
pop dx ; Restore DX from the stack
cmp dh , al ; if AL ( sectors read ) != DH ( sectors expected )
jne disk_error ; display error message
ret
disk_error :
mov bx , ERROR_MSG
call print_string
hlt
[bits 32]
; prints a null - terminated string pointed to by EDX
print_string :
pusha
mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem.
print_string_loop :
mov al , [ ebx ] ; Store the char at EBX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov [edx] , ax ; Store char and attributes at current
; character cell.
add ebx , 1 ; Increment EBX to the next char in string.
add edx , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
popa
ret ; Return from the function
[bits 16]
; Variables
ERROR_MSG db "Error!" , 0
BOOT_DRIVE: db 0
VIDEO_MEMORY equ 0xb8000
WHITE_ON_BLACK equ 0x0f
%include "a20.inc"
%include "gdt.inc"
times 510-($-$$) db 0
db 0x55
db 0xAA
Я собираю это с этим:
nasm -f bin -o boot.bin bootloader.asm
Это kernel.c:
call_main(){main();}
void main(){}
Я собираю это с этим:
gcc -ffreestanding -o kernel.bin kernel.c
а потом:
cat boot.bin kernel.bin > os.bin
Я хочу знать, что я делаю неправильно, потому что когда я тестирую с QEMU, это не работает. Может кто-нибудь дать несколько советов по улучшению kernel.c
так что мне не нужно использовать функцию call_main()?
При тестировании я использую:
qemu-system-i386 -kernel os.bin
Мои другие файлы
a20.inc:
enable_A20:
call check_a20
cmp ax, 1
je enabled
call a20_bios
call check_a20
cmp ax, 1
je enabled
call a20_keyboard
call check_a20
cmp ax, 1
je enabled
call a20_fast
call check_a20
cmp ax, 1
je enabled
mov bx, [ERROR]
call print_string
enabled:
ret
check_a20:
pushf
push ds
push es
push di
push si
cli
xor ax, ax ; ax = 0
mov es, ax
not ax ; ax = 0xFFFF
mov ds, ax
mov di, 0x0500
mov si, 0x0510
mov al, byte [es:di]
push ax
mov al, byte [ds:si]
push ax
mov byte [es:di], 0x00
mov byte [ds:si], 0xFF
cmp byte [es:di], 0xFF
pop ax
mov byte [ds:si], al
pop ax
mov byte [es:di], al
mov ax, 0
je check_a20__exit
mov ax, 1
check_a20__exit:
pop si
pop di
pop es
pop ds
popf
ret
a20_bios:
mov ax, 0x2401
int 0x15
ret
a20_fast:
in al, 0x92
or al, 2
out 0x92, al
ret
[bits 32]
[section .text]
a20_keyboard:
cli
call a20wait
mov al,0xAD
out 0x64,al
call a20wait
mov al,0xD0
out 0x64,al
call a20wait2
in al,0x60
push eax
call a20wait
mov al,0xD1
out 0x64,al
call a20wait
pop eax
or al,2
out 0x60,al
call a20wait
mov al,0xAE
out 0x64,al
call a20wait
sti
ret
a20wait:
in al,0x64
test al,2
jnz a20wait
ret
a20wait2:
in al,0x64
test al,1
jz a20wait2
ret
gdt.inc:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes dd 0
gdt_code:
dw 0FFFFh ; limit low
dw 0 ; base low
db 0 ; base middle
db 10011010b ; access
db 11001111b ; granularity
db 0 ; base high
gdt_data:
dw 0FFFFh ; limit low (Same as code)
dw 0 ; base low
db 0 ; base middle
db 10010010b ; access
db 11001111b ; granularity
db 0 ; base high
end_of_gdt:
gdtr:
dw end_of_gdt - gdt_start - 1 ; limit (Size of GDT)
dd gdt_start ; base of GDT
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
1 ответ
Есть ряд проблем, но в целом ваш ассемблерный код работает. Я написал ответ Stackru, в котором есть советы по общей разработке загрузчика.
Не думайте, что регистры сегментов установлены правильно
Исходный код в вашем вопросе не установил регистр сегмента стека SS. Совет № 1, который я даю:
Когда BIOS переходит к вашему коду, вы не можете полагаться на регистры CS,DS,ES,SS,SP, имеющие действительные или ожидаемые значения. Они должны быть настроены соответствующим образом при запуске вашего загрузчика.
Если вам нужна ES, она также должна быть установлена. Хотя в вашем коде это не так (за исключением функции print_string, о которой я расскажу позже).
Правильно определите GDT
Самая большая ошибка, которая помешала бы вам далеко перейти в защищенный режим, заключалась в том, что вы настроили глобальную таблицу дескрипторов (GDT) в gdt.inc, начиная с:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes dd 0
Каждый глобальный дескриптор должен быть 8 байтов, но dd 0
определяет только 4 байта (двойное слово). Так должно быть:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes
dd 0
На самом деле кажется, что второй dd 0
был случайно добавлен в конец комментария к предыдущей строке.
В 16-битном реальном режиме не используйте 32-битный код
Вы написали некоторые print_string
код, но это 32-битный код:
[bits 32]
; prints a null - terminated string pointed to by EBX
print_string :
pusha
mov edx , VIDEO_MEMORY ; Set edx to the start of vid mem.
print_string_loop :
mov al , [ ebx ] ; Store the char at EBX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov [edx] , ax ; Store char and attributes at current
; character cell.
add ebx , 1 ; Increment EBX to the next char in string.
add edx , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
popa
ret ; Return from the function
Вы вызываете print_string как обработчик ошибок в 16-битном коде, поэтому то, что вы делаете здесь, вероятно, приведет к перезагрузке компьютера. Вы не можете использовать 32-битные регистры и адресацию. Код можно сделать 16-битным с некоторыми настройками:
; prints a null - terminated string pointed to by EBX
print_string :
pusha
push es ;Save ES on stack and restore when we finish
push VIDEO_MEMORY_SEG ;Video mem segment 0xb800
pop es
xor di, di ;Video mem offset (start at 0)
print_string_loop :
mov al , [ bx ] ; Store the char at BX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov word [es:di], ax ; Store char and attributes at current
; character cell.
add bx , 1 ; Increment BX to the next char in string.
add di , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
pop es ;Restore ES that was saved on entry
popa
ret ; Return from the function
Основное отличие (в 16-битном коде) заключается в том, что мы больше не используем 32-битные регистры EAX и EDX. Чтобы получить доступ к видео ram @ 0xb8000, нам нужно использовать пару сегмент: смещение, представляющее одно и то же. 0xb8000 может быть представлен как сегмент: смещение 0xb800: 0x0 (вычисляется как (0xb800 << 4) + 0x0) = 0xb8000 физический адрес. Мы можем использовать эти знания для хранения b800 в регистре ES и использовать регистр DI в качестве смещения для обновления видеопамяти. Теперь мы используем:
mov word [es:di], ax
Чтобы переместить слово в видео RAM.
Сборка и соединение ядра и загрузчика
Одна из проблем, возникающих при построении ядра, заключается в том, что вы неправильно генерируете плоский двоичный образ, который можно напрямую загрузить в память. Вместо того, чтобы использовать gcc -ffreestanding -o kernel.bin kernel.c
Я рекомендую сделать это так:
gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin
Это собирает kernel.c к kernel.o с отладочной информацией (-g
). Затем компоновщик берет kernel.o (32-битный двоичный файл ELF) и создает исполняемый файл ELF с именем kernel.elf (этот файл будет полезен, если вы хотите отладить ядро). Затем мы используем objcopy, чтобы взять исполняемый файл ELF32 kernel.elf и преобразовать его в плоский двоичный образ kernel.bin, который может быть загружен BIOS. Ключевым моментом, который стоит отметить, является то, что с -Tlinker.ld
option мы просим LD (linker) прочитать опции из файла linker.ld. Это простой linker.ld
Вы можете использовать, чтобы начать:
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
Здесь следует отметить, что . = 0x9000
сообщает компоновщику, что он должен создать исполняемый файл, который будет загружен по адресу памяти 0x9000. 0x9000
где вы, кажется, поместили свое ядро в свой вопрос. Остальные строки делают доступными разделы C, которые нужно будет включить в ваше ядро для правильной работы.
Я рекомендую делать нечто подобное при использовании NASM, а не делать nasm -f bin -o boot.bin bootloader.asm
сделать это так:
nasm -g -f elf32 -F dwarf -o boot.o bootloader.asm
ld -melf_i386 -Ttext=0x7c00 -nostdlib --nmagic -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin
Это похоже на компиляцию ядра C. Здесь мы не используем скрипт компоновщика, но мы сообщаем компоновщику, чтобы он создавал наш код, предполагая, что код (загрузчик) будет загружен в 0x7c00.
Чтобы это работало, вам нужно удалить эту строку из bootloader.asm:
[ORG 0x7c00]
Очистить ядро (kernel.c)
Измените файл kernel.c так, чтобы он был:
/* This code will be placed at the beginning of the object by the linker script */
__asm__ (".pushsection .text.start\r\n" \
"jmp main\r\n" \
".popsection\r\n"
);
/* Place main as the first function defined in kernel.c so
* that it will be at the entry point where our bootloader
* will call. In our case it will be at 0x9000 */
int main(){
/* Do Stuff Here*/
return 0; /* return back to bootloader */
}
В bootloader.asm мы должны вызывать main
функция (которая будет размещена в 0x9000), а не прыгать к ней. Вместо:
jmp 0x9000
Измените это на:
call 0x9000
cli
loopend: ;Infinite loop when finished
hlt
jmp loopend
Код после вызова будет выполнен, когда вернется функция C main. Это простой цикл, который эффективно останавливает процессор и остается таким до бесконечности, так как нам некуда возвращаться.
Код после внесения всех рекомендуемых изменений
bootloader.asm:
[bits 16]
global _start
_start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x8000 ; Stack pointer at SS:SP = 0x0000:0x8000
mov [BOOT_DRIVE], dl; Boot drive passed to us by the BIOS
mov dh, 17 ; Number of sectors (kernel.bin) to read from disk
; 17*512 allows for a kernel.bin up to 8704 bytes
mov bx, 0x9000 ; Load Kernel to ES:BX = 0x0000:0x9000
call load_kernel
call enable_A20
; call graphics_mode ; Uncomment if you want to switch to graphics mode 0x13
lgdt [gdtr]
mov eax, cr0
or al, 1
mov cr0, eax
jmp CODE_SEG:init_pm
graphics_mode:
mov ax, 0013h
int 10h
ret
load_kernel:
; load DH sectors to ES:BX from drive DL
push dx ; Store DX on stack so later we can recall
; how many sectors were request to be read ,
; even if it is altered in the meantime
mov ah , 0x02 ; BIOS read sector function
mov al , dh ; Read DH sectors
mov ch , 0x00 ; Select cylinder 0
mov dh , 0x00 ; Select head 0
mov cl , 0x02 ; Start reading from second sector ( i.e.
; after the boot sector )
int 0x13 ; BIOS interrupt
jc disk_error ; Jump if error ( i.e. carry flag set )
pop dx ; Restore DX from the stack
cmp dh , al ; if AL ( sectors read ) != DH ( sectors expected )
jne disk_error ; display error message
ret
disk_error :
mov bx , ERROR_MSG
call print_string
hlt
; prints a null - terminated string pointed to by EDX
print_string :
pusha
push es ;Save ES on stack and restore when we finish
push VIDEO_MEMORY_SEG ;Video mem segment 0xb800
pop es
xor di, di ;Video mem offset (start at 0)
print_string_loop :
mov al , [ bx ] ; Store the char at BX in AL
mov ah , WHITE_ON_BLACK ; Store the attributes in AH
cmp al , 0 ; if (al == 0) , at end of string , so
je print_string_done ; jump to done
mov word [es:di], ax ; Store char and attributes at current
; character cell.
add bx , 1 ; Increment BX to the next char in string.
add di , 2 ; Move to next character cell in vid mem.
jmp print_string_loop ; loop around to print the next char.
print_string_done :
pop es ;Restore ES that was saved on entry
popa
ret ; Return from the function
%include "a20.inc"
%include "gdt.inc"
[bits 32]
init_pm:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000
mov esp, ebp
call 0x9000
cli
loopend: ;Infinite loop when finished
hlt
jmp loopend
[bits 16]
; Variables
ERROR db "A20 Error!" , 0
ERROR_MSG db "Error!" , 0
BOOT_DRIVE: db 0
VIDEO_MEMORY_SEG equ 0xb800
WHITE_ON_BLACK equ 0x0f
times 510-($-$$) db 0
db 0x55
db 0xAA
gdt.inc:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes
dd 0
gdt_code:
dw 0FFFFh ; limit low
dw 0 ; base low
db 0 ; base middle
db 10011010b ; access
db 11001111b ; granularity
db 0 ; base high
gdt_data:
dw 0FFFFh ; limit low (Same as code)
dw 0 ; base low
db 0 ; base middle
db 10010010b ; access
db 11001111b ; granularity
db 0 ; base high
end_of_gdt:
gdtr:
dw end_of_gdt - gdt_start - 1 ; limit (Size of GDT)
dd gdt_start ; base of GDT
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
a20.inc:
enable_A20:
call check_a20
cmp ax, 1
je enabled
call a20_bios
call check_a20
cmp ax, 1
je enabled
call a20_keyboard
call check_a20
cmp ax, 1
je enabled
call a20_fast
call check_a20
cmp ax, 1
je enabled
mov bx, [ERROR]
call print_string
enabled:
ret
check_a20:
pushf
push ds
push es
push di
push si
cli
xor ax, ax ; ax = 0
mov es, ax
not ax ; ax = 0xFFFF
mov ds, ax
mov di, 0x0500
mov si, 0x0510
mov al, byte [es:di]
push ax
mov al, byte [ds:si]
push ax
mov byte [es:di], 0x00
mov byte [ds:si], 0xFF
cmp byte [es:di], 0xFF
pop ax
mov byte [ds:si], al
pop ax
mov byte [es:di], al
mov ax, 0
je check_a20__exit
mov ax, 1
check_a20__exit:
pop si
pop di
pop es
pop ds
popf
ret
a20_bios:
mov ax, 0x2401
int 0x15
ret
a20_fast:
in al, 0x92
or al, 2
out 0x92, al
ret
[bits 32]
[section .text]
a20_keyboard:
cli
call a20wait
mov al,0xAD
out 0x64,al
call a20wait
mov al,0xD0
out 0x64,al
call a20wait2
in al,0x60
push eax
call a20wait
mov al,0xD1
out 0x64,al
call a20wait
pop eax
or al,2
out 0x60,al
call a20wait
mov al,0xAE
out 0x64,al
call a20wait
sti
ret
a20wait:
in al,0x64
test al,2
jnz a20wait
ret
a20wait2:
in al,0x64
test al,1
jz a20wait2
ret
kernel.c:
/* This code will be placed at the beginning of the object by the linker script */
__asm__ (".pushsection .text.start\r\n" \
"jmp main\r\n" \
".popsection\r\n"
);
/* Place main as the first function defined in kernel.c so
* that it will be at the entry point where our bootloader
* will call. In our case it will be at 0x9000 */
int main(){
/* Do Stuff Here*/
return 0; /* return back to bootloader */
}
linker.ld
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text.start) *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
Создать образ диска с помощью DD / отладки с QEMU
Если вы используете файлы выше, и производите требуемые файлы загрузчика и ядра с помощью этих команд (как упоминалось ранее)
nasm -g -f elf32 -F dwarf -o boot.o bootloader.asm
ld -melf_i386 -Ttext=0x7c00 -nostdlib --nmagic -o boot.elf boot.o
objcopy -O binary boot.elf boot.bin
gcc -g -m32 -c -ffreestanding -o kernel.o kernel.c -lgcc
ld -melf_i386 -Tlinker.ld -nostdlib --nmagic -o kernel.elf kernel.o
objcopy -O binary kernel.elf kernel.bin
Вы можете создать образ диска (в этом случае мы сделаем его размером с дискету) с помощью этих команд:
dd if=/dev/zero of=disk.img bs=512 count=2880
dd if=boot.bin of=disk.img bs=512 conv=notrunc
dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc
Это создает заполненный нулями образ диска размером 512*2880 байт (размер дискеты 1,44 мегабайта). dd if=boot.bin of=disk.img bs=512 conv=notrunc
записывает boot.bin в первый сектор файла без усечения образа диска. dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc
помещает kernel.bin в образ диска, начиная со второго сектора. seek=1
пропускает первый блок (bs=512) перед записью.
Если вы хотите запустить свое ядро, вы можете запустить его как дисковод A: (-fda
) в QEMU вот так:
qemu-system-i386 -fda disk.img
Вы также можете отлаживать 32-битное ядро, используя QEMU и GNU Debugger (GDB), с помощью отладочной информации, которую мы сгенерировали при компиляции / сборке кода с помощью приведенных выше инструкций.
qemu-system-i386 -fda disk.img -S -s &
gdb kernel.elf \
-ex 'target remote localhost:1234' \
-ex 'layout src' \
-ex 'layout reg' \
-ex 'break main' \
-ex 'continue'
Этот пример запускает QEMU с удаленным отладчиком и эмулирует дискету, используя файл disk.img
(что мы создали с DD). GDB запускается с использованием kernel.elf (файла, который мы создали с помощью отладочной информации), затем подключается к QEMU и устанавливает точку останова на функцию main() в коде C. Когда отладчик наконец готов, вам будет предложено нажать <return>
продолжать. Если вам повезет, вы должны увидеть функцию main в отладчике.