c-QEMU调用到错误的地址



我一直在做一个小型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,无限循环!这本不应该发生,因为在函数调用(未触发)和函数(未触发器)之后有clihlt指令。如果我使用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条目并且您开始在未知地址执行垃圾)。

最新更新