x86实模式16位Bootloader栈相关技术疑问
我在无相关经验的情况下编写了下面的x86实模式16位Bootloader,发现x86的实现差异很大,目前正尝试打造完整示例。当前代码可以通过qemu-system-x86_64 kern.img正常运行,但对栈相关问题有疑惑:
- 是否需要显式定义栈?不定义的话会发生什么?(当前未定义仍可运行)
- 未显式定义栈时,
call/ret指令如何工作?已知这两个指令会修改栈(push/popIP/BP)。
我的Bootloader代码
org 0x7C00 bits 16 start: mov ax, cs mov ds, ax mov si, title_str call print_str mov si, loading_str call print_str call load_kernel_to_mem jmp 0x0900:0x00 load_kernel_to_mem: mov ax, 0x0900 mov es, ax mov ah, 0x02 ; 从磁盘读取扇区到内存 mov al, 0x01 ; 读取扇区数量 mov ch, 0x00 ; 磁道号 mov cl, 0x02 ; 扇区号 mov dh, 0x00 ; 磁头号 mov dl, 0x80 ; 磁盘类型:第一块硬盘 mov bx, 0x00 ; ES:BX 缓冲区地址指针(bx为偏移量) int 0x13 ; 成功时CF=0,失败时CF=1且AX=错误码 jc handle_load_err ; 如果CF=1则处理错误 ret handle_load_err: mov si, load_error_str call print_str hlt ; 停止CPU执行后续指令(中断会唤醒) print_str: mov ah, 0x0E ; 电传打字输出模式 .loop lodsb ; 从si加载字节到al,然后si自增 cmp al, 0 ; 比较加载的字节是否为0 je finished_printing ; 如果是0则跳转到打印结束 int 0x10 ; 输出字节到屏幕 jmp .loop finished_printing: mov al, 10d ; 打印换行 int 0x10 mov ah, 0x03 ; 获取光标位置和形状 mov bh, 0 ; 页号 int 0x10 mov ah, 0x02 ; 设置光标位置 mov dl, 0 ; 列号0 int 0x10 ret title_str db "Bootloader", 0 loading_str db "Loading...", 0 load_error_str db "Failed to load", 0 times 510 - ($ - $$) db 0 dw 0xAA55
问题解答
1. 是否需要显式定义栈?不定义的话会发生什么?
不是必须显式定义,但强烈建议你这么做。BIOS完成硬件初始化后,会给SS(栈段寄存器)和SP(栈指针寄存器)设置一个默认值,指向某块可用内存区域(常见的比如SS=0x0000,SP=0xFFFE,栈顶在0x0000:0xFFFE)。
你当前代码能正常运行只是巧合——栈操作还没覆盖到Bootloader代码、内核数据或者BIOS依赖的内存区域。但不同BIOS的默认栈位置差异很大,有些可能把栈设在会和你的代码冲突的区域,一旦后续增加多层函数调用、局部变量入栈等操作,就会触发不可预测的崩溃。
2. 未显式定义栈时,call/ret指令如何工作?
call和ret的工作逻辑不会变:
call指令执行时,会把当前的IP(指令指针)压入栈顶,然后跳转到目标函数地址;ret指令执行时,会从栈顶弹出之前保存的IP值,跳回调用点继续执行。
没显式定义栈的话,这些操作就会使用BIOS初始化的SS:SP指向的内存区域作为栈。但因为这个栈的位置不受你控制,风险极高。
建议与修正
显式初始化栈:在Bootloader开头添加栈初始化代码,确保栈位置安全,比如:
start: mov ax, 0x0000 mov ss, ax mov sp, 0x7C00 ; 栈顶设在Bootloader加载地址(0x7C00)下方,避免覆盖Bootloader代码 mov ax, cs mov ds, ax ; 后续代码...这样栈会从0x0000:0x7C00向下生长,不会和Bootloader占用的0x0000:0x7C00~0x0000:0x7DFF区域冲突。
修复错误处理逻辑:当前
handle_load_err中的hlt会被系统中断唤醒,导致CPU继续执行后续错误指令。建议改成死循环:handle_load_err: mov si, load_error_str call print_str jmp $ ; 无限循环,防止CPU继续执行不要硬编码磁盘号:BIOS会把启动磁盘的编号存在
dl寄存器中,直接使用这个值即可,不用硬编码为0x80。可以在开头保存dl,后续读取磁盘时恢复:start: mov [boot_disk], dl ; 保存启动磁盘号 ; 栈初始化等代码... load_kernel_to_mem: ; ...其他代码... mov dl, [boot_disk] ; 恢复启动磁盘号 int 0x13 ; ...后续代码... boot_disk db 0 ; 新增变量保存磁盘号增加栈空间预留:如果后续要使用更多栈操作,可以把
SP设得更低一些,比如mov sp, 0x7000,预留2KB的栈空间,避免溢出。
内容的提问来源于stack exchange,提问作者Elia

