我一直在做一个小型osdev项目。到目前为止,我已经使用A20、GDT、保护模式(32位)和磁盘加载来运行C代码,但函数调用不起作用。我已经确认实际的二进制文件没有问题(ndisasm -b 32 lizard.bin
):
... irrelevant bootloader code ...
00000200 8D4C2404 lea ecx,[esp+0x4]
00000204 83E4F0 and esp,byte -0x10
00000207 FF71FC push dword [ecx-0x4]
0000020A 55 push ebp
0000020B 89E5 mov ebp,esp
0000020D 51 push ecx
0000020E 83EC14 sub esp,byte +0x14
00000211 C745F400000000 mov dword [ebp-0xc],0x0
00000218 83EC0C sub esp,byte +0xc
0000021B 8D45F4 lea eax,[ebp-0xc]
0000021E 50 push eax
0000021F E82F000000 call 0x253
00000224 83C410 add esp,byte +0x10
00000227 8945F4 mov [ebp-0xc],eax
0000022A FA cli
0000022B F4 hlt
0000022C 83EC0C sub esp,byte +0xc
0000022F 8D45F4 lea eax,[ebp-0xc]
00000232 50 push eax
00000233 E81B000000 call 0x253
00000238 83C410 add esp,byte +0x10
0000023B 8945F4 mov [ebp-0xc],eax
0000023E 83EC0C sub esp,byte +0xc
00000241 8D45F4 lea eax,[ebp-0xc]
00000244 50 push eax
00000245 E809000000 call 0x253
0000024A 83C410 add esp,byte +0x10
0000024D 8945F4 mov [ebp-0xc],eax
00000250 90 nop
00000251 EBFD jmp short 0x250
00000253 55 push ebp
00000254 89E5 mov ebp,esp
00000256 83EC10 sub esp,byte +0x10
00000259 FA cli
0000025A F4 hlt
0000025B C745FC01000000 mov dword [ebp-0x4],0x1
00000262 8B55FC mov edx,[ebp-0x4]
00000265 89D0 mov eax,edx
00000267 C1E002 shl eax,byte 0x2
0000026A 01D0 add eax,edx
0000026C 8945FC mov [ebp-0x4],eax
0000026F 8B55FC mov edx,[ebp-0x4]
00000272 89D0 mov eax,edx
00000274 C1E003 shl eax,byte 0x3
00000277 29D0 sub eax,edx
00000279 8945FC mov [ebp-0x4],eax
0000027C 836DFC06 sub dword [ebp-0x4],byte +0x6
00000280 8B55FC mov edx,[ebp-0x4]
00000283 89D0 mov eax,edx
00000285 C1E003 shl eax,byte 0x3
00000288 01D0 add eax,edx
0000028A 8945FC mov [ebp-0x4],eax
0000028D 8B4508 mov eax,[ebp+0x8]
00000290 8B55FC mov edx,[ebp-0x4]
00000293 8910 mov [eax],edx
00000295 8B45FC mov eax,[ebp-0x4]
00000298 C9 leave
00000299 C3 ret
cli&hlt对用于使用qemu进行调试,qemu没有在它们上停止。正如你所看到的,3个调用指令是完全正常的。然而,运行qemu和运行info registers
会产生:
QEMU 6.2.0 monitor - type 'help' for more information
(qemu) info registers
... irrelevant ...
EIP=00007e50 ... irrelevant ...
... irrelevant ...
正如你所看到的,eip
就是7e50
,无限循环!这本不应该发生,因为在函数调用(未触发)和函数(未触发器)之后有cli
和hlt
指令。如果我使用gdb
,在内核的内存地址7e00
上设置一个断点,然后继续使用si
,就会看到gdb
进入对函数的调用,结果下一条指令处于无限循环中!
最后我会提供文件。
Makefile
:
PRINTDIRECTORY = --no-print-directory
BOOTLOADER-PARTFILE = int/parts/boot.prt
BOOTLOADER-OBJECTFILE = int/boot.o
BOOTLOADER-SOURCEFILE = src/boot.s
KERNEL-PARTFILE = int/parts/detailed-boot.prt
KERNEL-OBJECTFILE = int/detailed-boot.o
KERNEL-SOURCEFILE = src/detailed-boot.c
GCC = ~/opt/cross/bin/i686-elf-gcc
LD = ~/opt/cross/bin/i686-elf-ld
VM = qemu-system-i386
SYSFILE = lizard.bin
full:
make bootloader $(PRINTDIRECTORY)
make kernel $(PRINTDIRECTORY)
truncate -s 32768 ./int/parts/detailed-boot.prt
make join $(PRINTDIRECTORY)
bootloader:
as -o $(BOOTLOADER-OBJECTFILE) $(BOOTLOADER-SOURCEFILE)
ld -o $(BOOTLOADER-PARTFILE) --oformat binary -e init $(BOOTLOADER-OBJECTFILE) -Ttext 0x7c00
kernel:
$(GCC) -ffunction-sections -ffreestanding $(KERNEL-SOURCEFILE) -o $(KERNEL-OBJECTFILE) -nostdlib -Wall -Wextra -O0
$(LD) -o $(KERNEL-PARTFILE) -Ttext 0x7e00 --oformat binary $(KERNEL-OBJECTFILE) -e main --script=LDfile -O 0 -Ttext-segment 0x7e00
join:
cat $(BOOTLOADER-PARTFILE) $(KERNEL-PARTFILE) > $(SYSFILE)
run:
$(VM) $(SYSFILE)
debug:
$(VM) $(SYSFILE) -gdb tcp:localhost:6000 -S
LDfile
:
ENTRY(main)
SECTIONS {
. = 0x7e00;
.text . : { *(.text) }
.data . : { *(.data) }
.bss . : { *(.bss ) }
}
src/detailed-boot.c
:
//#include "stdc/stdbool.h"
//#include "stdc/stdio.h"
asm(".code32");
int a(int *d);
int main() {
int c = 0;
c = a(&c);
asm("cli");
asm("hlt");
c = a(&c);
c = a(&c);
while(1);
}
int a(int *d) {
asm("cli");
asm("hlt");
int b = 1;
b *= 5;
b *= 7;
b -= 6;
b *= 9;
*d = b;
return b;
}
//#include "stdc/stdio.c"
src/boot.s
:
.code16 # 16 bit mode
.global init # make label init global
init:
call enableA20
reset:
mov $0x00, %ah # 0 = reset drive
mov $0x80, %dl # boot disk
int $0x13
jc reset
load:
mov $0x42, %ah # 42 = extended read
mov $0x8000, %si
xor %bx, %bx
movl $0x00007e00, %ds:4 (%si,1)
movl $0x00400010, %ds:0 (%si,1)
mov %cs, %ds:6 (%si,1)
movl $0x00000001, %ds:8 (%si,1) # start sector in lba
movl $0x00000000, %ds:12(%si,1) # start sector in lba
int $0x13
# 1. Disable interrupts
cli
# 2. Load GDT
lgdt (gdt_descriptor)
# set 32 bit mode
mov %cr0, %eax
or $1, %eax
mov %eax, %cr0
# Far jmp
jmp %cs:(code32)
checkA20:
push %ds
xor %ax, %ax
mov %ax, %ds
movw $0xAA55, %ax
movw $0x7DFE, %bx
movw (%bx), %bx
cmpw %ax, %bx
jnz checkA20_enabled
checkA20_disabled:
xor %ax, %ax
jmp checkA20_done
checkA20_enabled:
xor %ax, %ax
inc %ax
checkA20_done:
pop %ds
ret
enableA20:
call checkA20
jnz enableA20_enabled
enableA20_int15:
mov $0x2403, %ax # A20 gate support
int $0x15
jb enableA20_keyboardController # INT 15 aint supported
cmp $0, %ah
jnz enableA20_keyboardController # INT 15 aint supported
mov $0x2402, %ax # A20 status
int $0x15
jb enableA20_keyboardController # couldnt get status
cmp $0, %ah
jnz enableA20_keyboardController # couldnt get status
cmp $1, %al
jz enableA20_enabled # A20 is activated
mov $0x2401, %ax # A20 activation
int $0x15
jb enableA20_keyboardController # couldnt activate
cmp $0, %ah
jnz enableA20_keyboardController # couldnt activate
enableA20_keyboardController:
call checkA20
jnz enableA20_enabled
cli
call enableA20_wait
mov $0xAD, %al
out %al, $0x64
call enableA20_wait
mov $0xD0, %al
out %al, $64
call enableA20_wait2
in $0x60, %al
push %eax
call enableA20_wait
mov $0xD1, %al
out %al, $0x64
call enableA20_wait
pop %eax
or $2, %al
out %al, $0x60
call enableA20_wait
mov $0xAE, %al
out %al, $0x64
call enableA20_wait
sti
enableA20_fastA20:
call checkA20
jnz enableA20_enabled
in $0x92, %al
test $2, %al
jnz enableA20_postFastA20
or $2, %al
and $0xFE, %al
out %al, $92
enableA20_postFastA20:
call checkA20
jnz enableA20_enabled
cli
hlt
enableA20_enabled:
ret
enableA20_wait:
in $0x64, %al
test $2, %al
jnz enableA20_wait
ret
enableA20_wait2:
in $0x64, %al
test $1, %al
jnz enableA20_wait2
ret
setGDT: ret
# NOTE limit is the length
# NOTE base is the start
# NOTE base + limit = last address
gdt_start:
gdt_null:
# null descriptor
.quad 0
gdt_data:
.word 0x01c8 # limit: bits 0-15
.word 0x0000 # base: bits 0-15
.byte 0x00 # base: bits 16-23
# segment presence: yes (+0x80)
# descriptor priviledge level: ring 0 (+0x00)
# descriptor type: code/data (+0x10)
# executable: no (+0x00)
# direction bit: grows up (+0x00)
# writable bit: writable (+0x02)
# accesed bit [best left 0, cpu will deal with it]: no (+0x00)
.byte 0x80 + 0x10 + 0x02
# granularity flag: limit scaled by 4kib (+0x80)
# size flag: 32 bit pm (+0x40)
# long mode flag: 32pm/16pm/data (+0x00)
# reserved: reserved (+0x00)
.byte 0x80 + 0x40 # flags: granularity @ 4-7 limit: bits 16-19 @ 0-3
.byte 0x00 # base: bits 24-31
gdt_code:
.word 0x0100 # limit: bits 0-15
.word 0x8000 # base: bits 0-15
.byte 0x1c # base: bits 16-23
# segment presence: yes (+0x80)
# descriptor priviledge level: ring 0 (+0x00)
# descriptor type: code/data (+0x10)
# executable: yes (+0x08)
# conforming bit [0: only ring 0 can execute this]: no (+0x00)
# readable bit: yes (0x02)
# accessed bit [best left 0, cpu will deal with it]: no (0x00)
.byte 0x80 + 0x10 + 0x08 + 0x02
# granularity flag: limit scaled by 4kib (+0x80)
# size flag: 32 bit pm (+0x40)
# long mode flag: 32pm/16pm/data (+0x00)
# reserved: reserved (+0x00)
.byte 0x80 + 0x40 + 0x00 # flags: granularity @ 4-7 limit: bits 16-19 @ 0-3
.byte 0x00 # base: bits 24-31
gdt_end:
gdt_descriptor:
.word gdt_end - gdt_start - 1
.long gdt_start
.code32
code32:
mov %ds, %ax
mov %ax, %ds
# mov %ax, %ss
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
movl $0x4000, %ebp
mov %esp, %ebp
push $0x7e00
ret
.fill 500-(.-init)
.quad 1
.word 1
.word 0xaa55
kernel:
我知道这不是一个最低限度的情况,我道歉。最后,我将提供一个github回购链接:https://github.com/saltq144/lizard以及我遵循的交叉编译器教程:https://wiki.osdev.org/GCC_Cross-Compiler
为了解决一些问题:我没有配置IDT,NMI会导致三重故障或跳转到垃圾,而不是循环。试图修改SS导致我的有限测试出现三重错误。我同意.c文件中的.code32
是毫无意义的,但交叉编译器是i686,所以64位代码应该不是问题,但我会研究一下
注:
使用内联汇编,我能够插入两条nop
指令,以允许函数调用工作。这不是一个理想的解决方案,但在这个问题完全解决之前,它必须发挥作用。编译器优化可能会打破这一局面,但目前还没有。
当您切换到保护模式(在jmp %cs:(code32)
)时,CS加载";"受保护模式兼容";来自GDT的信息。
在code32:
开始时,所有其他段寄存器都包含实模式值将与保护模式不兼容的真实模式值从DS(位于mov %ds, %ax
)复制到所有数据段寄存器中。这个来自DS的实模值可能是0x0000。在保护模式中;空描述符";。
这就是为什么你不能做CCD_;空描述符";对于堆栈段(它将为您提供一般保护故障)。因为您没有用受保护模式兼容的值加载SS,所以它使用真实模式中的旧值——未知的基址、64KiB段限制和16位默认堆栈指针大小。
所有这些的结果是……只要您进行任何正常的内存访问(例如,使用DS作为隐含段寄存器的push dword [ecx-0x4]
,如[ds: ecx-0x4]
),您就会出现一般保护故障,因为DS被设置为空描述符。因为您没有设置保护模式IDT,所以CPU将只使用真实模式用于其IVT的值,导致CPU认为未知垃圾(在真实模式中为"中断0x1A",而不是"中断0x0D",因为IDT条目是普通保护故障处理程序的IDT条目的两倍大)。没有简单的方法来预测之后会发生什么(也许未知垃圾不是受保护模式的有效IDT条目,它会导致双重故障,也许它是一个"足够有效"的IDT条目并且您开始在未知地址执行垃圾)。