如何为我的引导加载程序制作内核?
How to make the kernel for my bootloader?
我正在尝试自定义 OS,我的代码需要一些帮助。
这是我的 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
存在许多问题,但总的来说您的汇编代码确实有效。我写了一个 Whosebug 答案,其中包含 的提示。
不要假设段寄存器设置正确
您问题中的原始代码没有设置 SS 堆栈段寄存器。我给出的提示 #1 是:
When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP
registers having valid or expected values. They should be set up
appropriately when your bootloader starts.
如果你需要 ES 也应该设置它。尽管在您的代码中似乎并非如此(我稍后将讨论的 print_string 函数除外)。
正确定义 GDT
阻止您进入保护模式的最大错误是您在 gdt.inc[=177 中设置了全局描述符 table (GDT) =] 开始于:
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 位代码中)是我们不再使用 EAX 和 EDX 32 位寄存器。为了访问视频 ram @ 0xb8000 我们需要使用 segment:offset 对来表示相同的东西。 0xb8000 可以表示为 segment:offset 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 与调试信息 (-g
) 组装成 kernel.o。然后链接器获取 kernel.o(32 位 ELF 二进制文件)并生成 ELF executable 调用了 kernel.elf(如果你想调试你的内核,这个文件会很方便)。然后我们使用 objcopy 获取 ELF32 executable 文件 kernel.elf 并将其转换为平面二进制图像 kernel.bin 可以被BIOS加载。需要注意的关键是,使用 -Tlinker.ld
选项,我们要求 LD(链接器)从文件 linker.ld。这是一个简单的 linker.ld
,您可以使用它来开始:
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
这里要注意的是,. = 0x9000
告诉链接器它应该生成一个 executable,它将加载到内存地址 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函数mainreturns时执行。这是一个简单的循环,可以有效地停止处理器并无限期地保持这种状态,因为我们无处可退。
进行所有建议更改后的代码
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 写入文件的第一个扇区而不 运行cating 磁盘映像。 dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc
将 kernel.bin 放入磁盘映像,从第二个扇区开始。 seek=1
在写入之前跳过第一个块 (bs=512)。
如果你想 运行 你的内核,你可以在 QEMU 中将它作为软盘驱动器 A: (-fda
) 启动,像这样:
qemu-system-i386 -fda disk.img
您还可以使用 QEMU 和 GNU 调试器 (GDB) 使用我们在compiling/assembling 带有上述说明的代码。
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[=177 创建的)模拟软盘=]). GDB 使用 kernel.elf(我们使用调试信息生成的文件)启动,然后连接到 QEMU,并在 [=] 处设置断点C 代码中的 126=]function main()。当调试器最终准备就绪时,系统会提示您按 <return>
继续。运气好的话,您应该在调试器中查看函数 main。
我正在尝试自定义 OS,我的代码需要一些帮助。 这是我的 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
存在许多问题,但总的来说您的汇编代码确实有效。我写了一个 Whosebug 答案,其中包含
不要假设段寄存器设置正确
您问题中的原始代码没有设置 SS 堆栈段寄存器。我给出的提示 #1 是:
When the BIOS jumps to your code you can't rely on CS,DS,ES,SS,SP registers having valid or expected values. They should be set up appropriately when your bootloader starts.
如果你需要 ES 也应该设置它。尽管在您的代码中似乎并非如此(我稍后将讨论的 print_string 函数除外)。
正确定义 GDT
阻止您进入保护模式的最大错误是您在 gdt.inc[=177 中设置了全局描述符 table (GDT) =] 开始于:
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 位代码中)是我们不再使用 EAX 和 EDX 32 位寄存器。为了访问视频 ram @ 0xb8000 我们需要使用 segment:offset 对来表示相同的东西。 0xb8000 可以表示为 segment:offset 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 与调试信息 (-g
) 组装成 kernel.o。然后链接器获取 kernel.o(32 位 ELF 二进制文件)并生成 ELF executable 调用了 kernel.elf(如果你想调试你的内核,这个文件会很方便)。然后我们使用 objcopy 获取 ELF32 executable 文件 kernel.elf 并将其转换为平面二进制图像 kernel.bin 可以被BIOS加载。需要注意的关键是,使用 -Tlinker.ld
选项,我们要求 LD(链接器)从文件 linker.ld。这是一个简单的 linker.ld
,您可以使用它来开始:
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
这里要注意的是,. = 0x9000
告诉链接器它应该生成一个 executable,它将加载到内存地址 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函数mainreturns时执行。这是一个简单的循环,可以有效地停止处理器并无限期地保持这种状态,因为我们无处可退。
进行所有建议更改后的代码
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 写入文件的第一个扇区而不 运行cating 磁盘映像。 dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc
将 kernel.bin 放入磁盘映像,从第二个扇区开始。 seek=1
在写入之前跳过第一个块 (bs=512)。
如果你想 运行 你的内核,你可以在 QEMU 中将它作为软盘驱动器 A: (-fda
) 启动,像这样:
qemu-system-i386 -fda disk.img
您还可以使用 QEMU 和 GNU 调试器 (GDB) 使用我们在compiling/assembling 带有上述说明的代码。
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[=177 创建的)模拟软盘=]). GDB 使用 kernel.elf(我们使用调试信息生成的文件)启动,然后连接到 QEMU,并在 [=] 处设置断点C 代码中的 126=]function main()。当调试器最终准备就绪时,系统会提示您按 <return>
继续。运气好的话,您应该在调试器中查看函数 main。