如何为我的引导程序制作内核?
我正在尝试制作自己的自定义操作系统,我的代码需要一些帮助. 这是我的 bootloader.asm :
I'm trying to make my own custom OS and I need some help with my code. This is my 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()函数了吗?
I want to know what I am doing wrong because when I test with QEMU it doesn't work. Can someone give some tips to improve kernel.c
so I don't have to use the call_main() function?
在测试时,我使用:
qemu-system-i386 -kernel os.bin
我的其他文件
a20.inc :
My Other Files
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
有很多问题,但是总的来说,您的汇编代码确实可以工作.我写了一个*答案,其中包含有关常规Bootloader开发的提示.
There are a number of issues, but in general your assembly code does work. I have written a * answer that has tips for general bootloader development.
您问题中的原始代码未设置 SS 堆栈段寄存器.我给的提示#1是:
The original code in your question didn't set the SS stack segment register. Tip #1 I give is:
BIOS跳到您的代码时,您将不能依赖CS,DS,ES,SS,SP 具有有效或预期值的寄存器.他们应该成立 引导加载程序启动时适当地显示.
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函数中,我将在后面讨论).
If you need ES it should be set as well. Although in your code it doesn't appear to be the case (except in the print_string function which I'll discuss later).
使您无法进入保护模式的最大错误是,您在 gdt.inc 中设置了以以下内容开头的全局描述符表(GDT):
The single largest bug that would have prevented you from getting far into protected mode was that you set up the global descriptor table (GDT) in gdt.inc starting with:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes dd 0
每个全局描述符需要8个字节,但是dd 0
仅定义4个字节(双字).应该是:
Each global descriptor needs to be 8 bytes but dd 0
defines just 4 bytes (double word). It should be:
gdt_start:
dd 0 ; null descriptor--just fill 8 bytes
dd 0
实际上,似乎第二个dd 0
意外地添加到了前一行的注释末尾.
It actually appears that the second dd 0
was accidentally added to the end of the comment on the previous line.
您已经编写了一些print_string
代码,但是它是32位代码:
You have written some print_string
code but it is 32-bit code:
[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
您将16位代码中的 print_string 用作错误处理程序,因此您在此处执行的操作可能会强制重新启动计算机.您不能使用32位寄存器和寻址.可以通过一些调整将代码设置为16位:
You call print_string as an error handler in 16-bit code so what you are doing here will likely force a reboot of the computer. You can't use the 32-bit registers and addressing. The code can be made 16-bit with some adjustments:
; 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 0xb8000 物理地址.我们可以利用这些知识将 b800 存储在 ES 寄存器中,并使用 DI 寄存器作为更新视频内存的偏移量.我们现在使用:
The primary difference (in 16-bit code) is that we no longer use EAX and EDX 32-bit registers. In order to access the video ram @ 0xb8000 we need to use a segment:offset pair that represents the same thing. 0xb8000 can be represented as segment:offset 0xb800:0x0 (Computed as (0xb800<<4)+0x0) = 0xb8000 physical address. We can use this knowledge to store b800 in the ES register and use DI register as the offset to update video memory. We now use:
mov word [es:di], ax
将单词移入视频ram.
To move a word into video ram.
构建内核时遇到的问题之一是,您没有正确生成可以直接加载到内存中的平面二进制映像.建议不要使用gcc -ffreestanding -o kernel.bin kernel.c
,而是这样做:
One of the issues you have in building your Kernel is that you don't properly generate a flat binary image that can be loaded into memory directly. Rather than using gcc -ffreestanding -o kernel.bin kernel.c
I recommend doing it this way:
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
这会将带有调试信息(-g
)的 kernel.c 组装为 kernel.o .然后,链接器获取 kernel.o (32位 ELF 二进制文件),并生成一个名为 kernel.elf 的 ELF 可执行文件. >(如果您要调试内核,此文件将很方便).然后,我们使用 objcopy 来获取ELF32可执行文件 kernel.elf ,并将其转换为可通过以下方式加载的平面二进制映像 kernel.bin : BIOS.需要注意的关键是,使用-Tlinker.ld
选项时,我们要求 LD (链接器)从文件 linker.ld 中读取选项.这是一个简单的linker.ld
,您可以用来入门:
This assembles kernel.c to kernel.o with debugging info (-g
). The linker then takes kernel.o (32-bit ELF binary) and produces an ELF executable called kernel.elf (this file will be handy if you want to debug your kernel). We then use objcopy to take the ELF32 executable file kernel.elf and convert it into a flat binary image kernel.bin that can be loaded by the BIOS. A key thing to note is that with -Tlinker.ld
option we are asking the LD(linker) to read options from the file linker.ld . This is a simple linker.ld
you can use to get started:
OUTPUT_FORMAT(elf32-i386)
ENTRY(main)
SECTIONS
{
. = 0x9000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) *(COMMON) }
}
这里要注意的是. = 0x9000
告诉链接器它应该生成一个可执行文件,该可执行文件将在内存地址 0x9000 中加载. 0x9000
似乎是您在问题中放入内核的地方.其余各行提供了 C 部分,这些部分需要包含在内核中才能正常工作.
The thing to note here is that . = 0x9000
is telling the linker that it should produce an executable that will be loaded at memory address 0x9000 . 0x9000
is where you seem to have placed your kernel in your question. The rest of the lines make available the C sections that will need to be included into your kernel to work properly.
我建议在使用 NASM 时执行类似的操作,而不要这样执行nasm -f bin -o boot.bin bootloader.asm
:
I recommend doing something similar when using NASM so rather than doing nasm -f bin -o boot.bin bootloader.asm
do it this way:
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 处加载.
This is similar to compiling the C kernel. We don't use a linker script here, but we do tell the linker to produce our code assuming the code (bootloader) will be loaded at 0x7c00 .
要执行此操作,您需要从 bootloader.asm 中删除该行:
For this to work you will need to remove this line from 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处),而不是跳转到该函数.代替:
In bootloader.asm we should be calling the main
function (that will be placed at 0x9000) rather than jumping to it. Instead of:
jmp 0x9000
将其更改为:
call 0x9000
cli
loopend: ;Infinite loop when finished
hlt
jmp loopend
调用后的代码将在 C 函数main返回时执行.这是一个简单的循环,它将有效地暂停处理器并无限期保持该方式,因为我们无处可寻.
The code after the call will be executed when C function main returns. It is a simple loop that will effectively halt the processor and remain that way indefinitely since we have no where to go back to.
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进行调试
如果您使用上述文件,并使用这些命令生成所需的引导程序和内核文件(如前所述)
Create Disk Image Using DD / Debugging with QEMU
If you use the files above, and produce the required bootloader and kernel files using these commands (as mentioned previously)
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
您可以使用以下命令生成磁盘映像(在这种情况下,我们将其制作为软盘大小):
You can produce a disk image (in this case we'll make it the size of a floppy) with these commands:
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).
This creates a zero filled disk image of size 512*2880 bytes (The size of a 1.44 megabyte floppy). dd if=boot.bin of=disk.img bs=512 conv=notrunc
writes boot.bin to the first sector of the file without truncating the disk image. dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc
places kernel.bin into the disk image starting at the second sector. The seek=1
skips over the first block (bs=512) before writing.
如果您希望运行内核,可以将其作为软盘驱动器A:(-fda
)在 QEMU 中启动,如下所示:
If you wish to run your kernel you can launch it as floppy drive A: (-fda
) in QEMU like this:
qemu-system-i386 -fda disk.img
您还可以使用 QEMU 和GNU调试器( GDB )调试32位内核,其中包含我们根据说明编译/组装代码时生成的调试信息.以上.
You can also debug your 32-bit kernel using QEMU and the GNU Debugger (GDB) with the debug information we generated when compiling/assembling the code with the instructions above.
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 ,并在 function main( )在 C 代码中.当调试器终于准备就绪时,将提示您按<return>
继续.运气好的话,您应该在调试器中查看功能 main .
This example launches QEMU with the remote debugger and emulating a floppy disk using the file disk.img
(that we created with DD). GDB launches using kernel.elf (a file we generated with debug info), then connects to QEMU, and sets a breakpoint at function main() in the C code. When the debugger finally is ready you'll be prompted to press <return>
to continue. With any luck you should be viewing function main in the debugger.