assembly - 添加非内联函数时内核崩溃
问题描述
我用C编写了简单的引导加载程序和内核(使用 g++ 编译器编译)。当我尝试创建非内联函数时,内核崩溃指的是 0xefffff54。寄存器 SS、DS 和其他寄存器为零,但以前在保护模式下是选择器 0x10。这是引导加载程序、加载程序和内核以及我如何链接它:
引导程序
use16
[org 0x7c00]
;;;;;;;;;;;;;;;;;;;;;;;;;;;
section .text
mov bp, 0x9990
mov sp, bp
call loadKernel
cli
lgdt [gdt_desc]
in al, 0x92
or al, 2
out 0x92, al
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 0x8:init_pm
;;;;;;;;;;;;;;;;;;;;;;;;
use32
init_pm:
mov ax, 0x10
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov esp, 0x9990
push ecx
push 1500
call clearConsole
add esp, 4
pop ecx
push eax
push edx
push 0x1B
push hello_world
push 0
call printStr
add esp, 12
pop edx
pop eax
jmp 0x1000
jmp $
;void loadKernel()
loadKernel:
use16
mov bx, 0x1000
mov ah, 0x2
mov dl, 0x80
mov al, 0x1
mov ch, 0x0
mov cl, 0x2
mov dh, 0x0
int 0x13
ret
;void clearConsole(int value)
use32
clearConsole:
mov ecx, 0
loop_2:
cmp ecx, [esp+4]
jz exit_2
mov al, 0
mov ah, 0
push ecx
call printChar
pop ecx
add ecx, 2
jmp loop_2
exit_2:
ret
;void printStr(byte num, char* str, byte color)
printStr:
mov ecx, [esp+8]
loop_1:
mov al, [ecx]
inc ecx
test al, al
jz exit_1
mov ah, [esp+12]
push dword [esp+4]
call printChar
add esp, 4
inc dword [esp+4]
inc dword [esp+4]
jmp loop_1
exit_1:
ret
;void printChar(byte num, unsigned char c, byte color)
printChar:
mov edx, 0xB8000
add edx, [esp+4]
mov [edx], al
mov [edx+1], ah
ret
;;;;;;;;;;;;;;;;;;;;
hello_world:
db "Loading kernel...", 0
GDT:
;null
dd 0
dd 0
code:
dw 0xffff ; limit
dw 0 ; base
db 0 ; base
db 0x9a ; access rights
db 11001111b ; 4 left - flags, 4 right = limit
db 0 ; base
data:
dw 0xffff
dw 0
db 0
db 0x92
db 11001111b
db 0
gdt_desc:
dw $ - GDT -1
dd GDT
;;;;;;;;;;;;;;;
times 510-($-$$) db 0
dw 0xAA55
加载器.asm
use32
section .bss
align 16
stack_bottom:
resb 16384
stack_top:
section .text
extern kernel_main
global _start
_start:
mov esp, stack_top
call kernel_main
jmp $
kernel_main.c
typedef unsigned short uint16_t;
typedef unsigned char uint8_t;
uint16_t* g_pTerminalBuffer;
#define MAX_HEIGHT 25
#define MAX_WIDTH 80
#define true 1
#define false 0
uint8_t g_iTerminalRow;
uint8_t g_iTerminalColumn;
inline uint8_t encodeColor(uint8_t foreground, uint8_t background)
{
return foreground | background << 4;
}
inline uint16_t encodeChar(uint8_t c, uint8_t color)
{
return (uint16_t)color << 8 | (uint16_t)c;
}
void initializeTerminal() // fails when i add this function
{
g_iTerminalRow = 0;
g_iTerminalColumn = 0;
}
extern "C" void kernel_main()
{
g_pTerminalBuffer = (uint16_t*)0xB8000;
g_pTerminalBuffer[2] = encodeChar('T', encodeColor(15, 0));
while(true){}
}
构建.sh
#!/bin/bash
nasm -f bin boot.asm -o boot.bin
nasm -f elf32 loader.asm -o loader.o
~/cross/bin/i386-elf-c++ -ffreestanding -c /home/name/os/kernel_main.c -o /home/name/os/kernel_main.o
ld -m elf_i386 -Ttext 0x1000 -o kernel_main.elf kernel_main.o loader.o
objcopy -R .note -R .comment -S -O binary kernel_main.elf kernel_main.bin
dd if=/dev/zero of=image.bin bs=512 count=2880
dd if=boot.bin of=image.bin conv=notrunc
dd if=kernel_main.bin of=image.bin conv=notrunc bs=512 seek=1
rm ./boot.bin ./kernel_main.bin ./kernel_main.o ./loader.o ./kernel_main.elf
qemu-system-i386 -d guest_errors image.bin
解决方案
如果没有链接描述文件,默认的 ELF 将放置.text
(和.text.startup
)部分,后跟.rodata*
,.data
和.bss
. 该部分中的函数.text
将按照遇到的顺序输出到可执行文件。链接器LD将按照在命令行中遇到的顺序处理对象。你做:
ld -m elf_i386 -Ttext 0x1000 -o kernel_main.elf kernel_main.o loader.o
kernel_main.o是第一个,所以 kernel_main.o 中的函数会被优先处理。当您定义initializeTerminal
时,它可能会出现在kernel_main
. 如果initializeTerminal
是第一个函数,并且您尝试从引导加载程序(地址 0x1000)开始执行,您将进入未定义状态,并且可能会导致三重故障。三重故障会使您回到实模式,这就是为什么转储中的段可能似乎已重置为 0x0000。
如果删除initializeTerminal
遇到的第一个函数将是kernel_main
. 精明的观察者可能会指出,您也真的不想kernel_main
直接执行!你想_start
先被处决!您很幸运,kernel_main
可以按原样执行而不会出错。你真的想出_start
现在其他功能之前。
快速修复应该是移动loader.o
,使其成为链接器命令行中的第一个对象:
ld -m elf_i386 -Ttext 0x1000 -o kernel_main.elf loader.o kernel_main.o
现在将首先处理.text
带有的部分_start
并输出到最终的可执行文件。这应该可以解决您的问题。
或者,我更喜欢创建一个链接描述文件,将该.text
部分放在loader.o
可执行文件的第一个位置。适当的链接器脚本可以避免担心在命令行中指定目标文件的顺序的麻烦。
下面我提供的文件版本是:
- 包括一个链接器脚本,用于放置 loader.o 的 .text` 部分,然后是其余部分。
- 链接描述文件为 0x1000 的内核定义了一个起点 (VMA)。
- 链接描述文件定义 BSS 部分的开始和结束符号
loader.asm
在调用C++入口点之前将 BSS 部分初始化为零。- 将内联函数放在头文件中是个好主意。我将视频例程从 kernel_main 中分离出来,并将全局视频变量放在video.c中。内联函数在video.h中。
- 使用BIOS 传递给包含引导驱动器号的引导加载程序的DL值。
- CHS 磁盘读取 (Int 13h/AH=02h) 需要在 ES:BX 中指定目标。因为我们希望内核加载在 0x0000:0x1000 显式设置 ES 为 0。当您的引导加载程序开始运行时,它不能保证为零。
- 我通过优化构建
- 我使用调试信息构建。这在使用远程 GDB 调试器在 QEMU 中运行代码时很有用。
这些文件是:
链接.ld:
OUTPUT_FORMAT("elf32-i386");
/* We define an entry point to keep the linker quiet. This entry point */
ENTRY(_start);
KERNEL_BASE = 0x1000;
SECTIONS
{
. = KERNEL_BASE;
.kernel : SUBALIGN(4) {
/* Ensure .text section of loader.o is first */
loader.o(.text*);
*(.text*);
*(.rodata*);
*(.data*);
}
/* Place the unitialized data in the area after our kernel */
.bss : SUBALIGN(4) {
__bss_start = .;
*(COMMON);
*(.bss)
. = ALIGN(4);
__bss_end = .;
}
__bss_sizeb = SIZEOF(.bss);
__bss_sizel = __bss_sizeb / 4;
/* Remove sections that won't be relevant to us */
/DISCARD/ : {
*(.eh_frame);
*(.comment);
}
}
启动.asm:
[ORG 0x7c00]
use16
section .text
mov bp, 0x9990
mov sp, bp
call loadKernel
cli
lgdt [gdt_desc]
in al, 0x92
or al, 2
out 0x92, al
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 0x8:init_pm
;;;;;;;;;;;;;;;;;;;;;;;;
use32
init_pm:
mov ax, 0x10
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov esp, 0x9990
push ecx
push 80*25*2
call clearConsole
add esp, 4
pop ecx
push eax
push edx
push 0x1B
push hello_world
push 0
call printStr
add esp, 12
pop edx
pop eax
jmp 0x1000 ; Jump to kernel
;void loadKernel()
loadKernel:
use16
; ES:BX point to input buffer. Ensure ES=0
xor ax, ax
mov es, ax
; Use valueof DL passed by bootloader for dirve number
mov bx, 0x1000
mov ah, 0x2
mov al, 0x1
mov ch, 0x0
mov cl, 0x2
mov dh, 0x0
int 0x13
ret
;void clearConsole(int value)
use32
clearConsole:
mov ecx, 0
loop_2:
cmp ecx, [esp+4]
jz exit_2
mov al, 0
mov ah, 0
push ecx
call printChar
pop ecx
add ecx, 2
jmp loop_2
exit_2:
ret
;void printStr(byte num, char* str, byte color)
printStr:
mov ecx, [esp+8]
loop_1:
mov al, [ecx]
inc ecx
test al, al
jz exit_1
mov ah, [esp+12]
push dword [esp+4]
call printChar
add esp, 4
inc dword [esp+4]
inc dword [esp+4]
jmp loop_1
exit_1:
ret
;void printChar(byte num, unsigned char c, byte color)
printChar:
mov edx, 0xB8000
add edx, [esp+4]
mov [edx], al
mov [edx+1], ah
ret
;;;;;;;;;;;;;;;;;;;;
hello_world:
db "Loading kernel...", 0
GDT:
;null
dd 0
dd 0
code:
dw 0xffff ; limit
dw 0 ; base
db 0 ; base
db 0x9a ; access rights
db 11001111b ; 4 left - flags, 4 right = limit
db 0 ; base
data:
dw 0xffff
dw 0
db 0
db 0x92
db 11001111b
db 0
gdt_desc:
dw $ - GDT -1
dd GDT
;;;;;;;;;;;;;;;
times 510-($-$$) db 0
dw 0xAA55
装载机.asm:
; These symbols are defined by the linker. We use them to zero BSS section
extern __bss_start
extern __bss_sizel
use32
section .bss
align 16
stack_bottom:
resb 16384
stack_top:
section .text
extern kernel_main
global _start
_start:
; We need to zero out the BSS section. We'll do it a DWORD at a time
mov edi, __bss_start ; Start address of BSS
mov ecx, __bss_sizel ; Length of BSS in DWORDS
xor eax, eax ; Set to 0x00000000
rep stosd ; Do clear using string store instruction
; Clear 4 bytes at a time
mov esp, stack_top
call kernel_main
jmp $
视频.h:
typedef unsigned short uint16_t;
typedef unsigned char uint8_t;
extern uint16_t* g_pTerminalBuffer;
#define MAX_HEIGHT 25
#define MAX_WIDTH 80
#define true 1
#define false 0
extern uint8_t g_iTerminalRow;
extern uint8_t g_iTerminalColumn;
extern void initializeTerminal();
inline uint8_t encodeColor(uint8_t foreground, uint8_t background)
{
return foreground | background << 4;
}
inline uint16_t encodeChar(uint8_t c, uint8_t color)
{
return (uint16_t)color << 8 | (uint16_t)c;
}
视频.c:
#include "video.h"
uint16_t* g_pTerminalBuffer;
uint8_t g_iTerminalRow;
uint8_t g_iTerminalColumn;
void initializeTerminal()
{
g_iTerminalRow = 0;
g_iTerminalColumn = 0;
}
kernel_main.c:
#include "video.h"
extern "C" void kernel_main()
{
g_pTerminalBuffer = (uint16_t*)0xB8000;
g_pTerminalBuffer[2] = encodeChar('T', encodeColor(15, 0));
while(true){}
}
构建命令:
nasm -f bin boot.asm -o boot.bin
nasm -f elf32 -g -F dwarf loader.asm -o loader.o
i686-elf-c++ -O3 -g -ffreestanding -c kernel_main.c -o kernel_main.o
i686-elf-c++ -O3 -g -ffreestanding -c video.c -o video.o
ld -m elf_i386 -T link.ld -o kernel_main.elf loader.o video.o kernel_main.o
objcopy -O binary kernel_main.elf kernel_main.bin
dd if=/dev/zero of=image.bin bs=512 count=2880
dd if=boot.bin of=image.bin conv=notrunc
dd if=kernel_main.bin of=image.bin conv=notrunc bs=512 seek=1
您可以像以前一样运行它:
qemu-system-i386 -d guest_errors image.bin
您可以使用调试信息和以下命令远程运行 QEMU:
qemu-system-i386 -d guest_errors image.bin -S -s &
gdb kernel_main.elf \
-ex 'target remote localhost:1234' \
-ex 'break *kernel_main' \
-ex 'layout src' \
-ex 'layout reg' \
-ex 'continue'
这会在符号上设置断点kernel_main
并使用命令行 TUI 界面来显示寄存器和源代码。
推荐阅读
- python - groupby.col.diff 给出意外错误
- android - 如果App在后台,当用户在奥利奥解锁手机时如何接收广播
- json - 使用 Swift decodable 解析 JSON 字典时出错
- linux - 现代平台上是否隐含 -fPIC
- bash - 进程终止后终端不关闭
- angular - 取消订阅不适用于 NgOnDestroy
- vb.net - 如何连接 app.config 文件中的值?
- flask - 为什么浏览器会省略 XML 标签?
- rdf - 根据语义网猫头鹰模型推断,Rocky 拥有哪些电影?
- android - 如何在 mikepenz 导航抽屉中使用 Glide 更改用户配置文件图标?