You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

x86实模式16位Bootloader栈相关技术疑问

x86实模式16位Bootloader栈相关问题解答

我在无相关经验的情况下编写了下面的x86实模式16位Bootloader,发现x86的实现差异很大,目前正尝试打造完整示例。当前代码可以通过qemu-system-x86_64 kern.img正常运行,但对栈相关问题有疑惑:

  1. 是否需要显式定义栈?不定义的话会发生什么?(当前未定义仍可运行)
  2. 未显式定义栈时,call/ret指令如何工作?已知这两个指令会修改栈(push/pop IP/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指向的内存区域作为栈。但因为这个栈的位置不受你控制,风险极高。


建议与修正

  1. 显式初始化栈:在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区域冲突。

  2. 修复错误处理逻辑:当前handle_load_err中的hlt会被系统中断唤醒,导致CPU继续执行后续错误指令。建议改成死循环:

    handle_load_err:
        mov si, load_error_str
        call print_str
        jmp $  ; 无限循环,防止CPU继续执行
    
  3. 不要硬编码磁盘号:BIOS会把启动磁盘的编号存在dl寄存器中,直接使用这个值即可,不用硬编码为0x80。可以在开头保存dl,后续读取磁盘时恢复:

    start:
        mov [boot_disk], dl  ; 保存启动磁盘号
        ; 栈初始化等代码...
    
    load_kernel_to_mem:
        ; ...其他代码...
        mov dl, [boot_disk]  ; 恢复启动磁盘号
        int 0x13
        ; ...后续代码...
    
    boot_disk db 0  ; 新增变量保存磁盘号
    
  4. 增加栈空间预留:如果后续要使用更多栈操作,可以把SP设得更低一些,比如mov sp, 0x7000,预留2KB的栈空间,避免溢出。

内容的提问来源于stack exchange,提问作者Elia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 17:15:13