pusha与popa指令工作原理及自制简易OS打印函数调用异常原因
问题分析:print_string仅打印首个字符串的原因
问题重现
你编写的第一个print_string实现存在栈溢出问题,导致程序在打印完第一个字符串后崩溃,无法执行后续代码。
第一个版本的print_string代码:
print_string: pusha cmp byte[bx], 0 je exit mov ah, 0x0e mov al, [bx] int 0x10 inc bx jmp print_string exit: mov ah, 0x0e mov al, 13 int 0x10 mov al, 10 int 0x10 popa ret
主调用代码:
[org 0x7c00] mov bx, hello call print_string mov bx, hi call print_string jmp $ %include "print_string.s" hello: db "Hello, World!",0 hi: db "This is a test.",0 times 510-($-$$) db 0 dw 0xaa55
问题根源
你的print_string用jmp print_string做循环,每次循环都会重新执行pusha指令:
- 第一次调用
print_string时,执行pusha把8个通用寄存器压入栈; - 打印一个字符后
inc bx,然后jmp print_string回到函数开头,再次执行pusha,又把8个寄存器压栈; - 这个过程会重复到字符串结束,
Hello, World!有13个字符,就会执行13次pusha,栈里堆积了13×16=208字节的数据; - 实模式下BIOS留给引导扇区的栈空间非常有限(通常只有几百字节),反复压栈会直接导致栈溢出,程序执行到
exit段的popa和ret时已经处于异常状态,根本无法回到主函数执行mov bx, hi和第二次call print_string。
而修改后的版本把pusha/popa移到主函数,print_string内部没有pusha,循环时不会反复压栈,栈空间不会被耗尽,因此能正常执行两次调用。
pusha和popa的工作原理
- pusha:一次性将8个16位通用寄存器按固定顺序压入栈,顺序是:
ax→cx→dx→bx→sp(压入pusha之前的栈指针值) →bp→si→di。每个寄存器占2字节,所以一次pusha会占用16字节的栈空间。 - popa:按与
pusha相反的顺序从栈中弹出数据到寄存器,顺序是:di→si→bp→sp(这个值会被忽略,栈指针仍按正常弹出逻辑推进) →bx→dx→cx→ax,刚好恢复执行pusha之前的所有通用寄存器状态。
修复建议
如果想在print_string内部保留pusha/popa,应该把递归式的jmp print_string改成循环结构,避免反复执行pusha:
print_string: pusha ; 仅在函数开头压栈一次 print_loop: cmp byte[bx], 0 je exit mov ah, 0x0e mov al, [bx] int 0x10 inc bx jmp print_loop ; 跳转到循环体开头,而非函数开头 exit: mov ah, 0x0e mov al, 13 int 0x10 mov al, 10 int 0x10 popa ; 函数结束时仅弹栈一次 ret
这样pusha和popa只会执行一次,不会导致栈溢出,同时也能正确保护寄存器状态。
内容的提问来源于stack exchange,提问作者Davy133
相关产品推荐
相关产品推荐

