使用OpenWatcom编译链接的小模型DOS .exe程序崩溃排查
我来帮你拆解导致程序在DOSBox中无限循环的核心问题,尤其是当存在未调用函数中的字符串时的异常行为:
1. _printmsg_函数的致命指令顺序错误
你的_printmsg_函数开头第一行就是ret,这意味着任何对该函数的调用都会立即返回,完全跳过后续的函数逻辑。更严重的是,这个函数是供C代码调用的,遵循cdecl调用约定:
- C代码调用
_printmsg(msg)时,会先把msg的地址压入栈中,再执行call指令。 - 但你的函数直接
ret,不会弹出栈上的参数,导致栈平衡被彻底破坏。
即使other()函数未被显式调用,如果编译器没有优化掉该函数的代码(比如-Os可能保留未调用函数),一旦程序因某种意外跳转到other()(比如内存访问错误导致的指令流错乱),调用_printmsg_就会打乱栈结构,最终引发无限循环或崩溃。
另外,函数内部还有两个逻辑错误:
- 硬编码使用
hello_msg,完全忽略了C代码传入的msg参数,违背了函数声明的意图。 - 没有妥善处理寄存器:
xchg ax, dx修改了ax寄存器,虽然void函数影响较小,但不符合调用约定中寄存器保存的规范。
修正后的_printmsg_应该符合cdecl约定:
_printmsg_: push bp ; 建立栈帧,遵循cdecl调用规范 mov bp, sp push dx ; 保存dx寄存器(DOS中断会修改它) mov dx, [bp+4] ; 从栈中取出传入的msg参数(bp+4是第一个参数,bp+2是返回地址) mov ah, 9 ; DOS WRITE_STDOUT功能调用 int 0x21 pop dx ; 恢复dx寄存器 pop bp ; 销毁栈帧 ret ; 返回,调用者会负责清理栈上的参数
2. 段命名与OpenWatcom的兼容性问题
OpenWatcom在DOS小模型(-mcmodel=s)下,编译C代码时默认使用的段名是:
- 代码段:
_TEXT - 数据段:
_DATA
而你的汇编代码中使用的是segment code和segment data,这会导致链接器将汇编的段与C代码生成的段视为独立的段。当你在C代码中添加字符串字面量时,该字符串会被放入_DATA段,但你的程序初始化时只将ds指向了汇编的data段,这会导致:
- 若程序意外访问到
_DATA段中的内容(比如栈溢出覆盖到该段,或指令流错乱),会因为ds指向错误而读取到无效内存,进而触发异常行为(比如无限循环)。
修正方法是将汇编中的段名改为与OpenWatcom一致:
segment _TEXT class=CODE use16 segment _DATA class=DATA use16 segment STACK class=STACK use16
3. 入口点符号的冗余定义
你的汇编代码中同时定义了global _small_code_和..start:
global _small_code_ segment code _small_code_: ..start:
..start是NASM的特殊符号,专门用于告知链接器这是程序的入口点,而global _small_code_是多余的,甚至可能导致链接器混淆入口点(虽然在你的案例中似乎没有直接引发问题,但会增加潜在风险)。
建议移除global _small_code_,只保留..start作为入口点标记:
segment _TEXT class=CODE use16 ..start: mov ax, _DATA mov ds, ax mov ax, STACK mov ss, ax mov sp, stacktop ; ... 后续初始化代码
4. 栈段的简化处理
你的汇编代码中单独定义了STACK段,这在小模型下是允许的,但OpenWatcom默认会将栈放在_DATA段中。如果你想简化段管理,可以将栈合并到_DATA段中:
segment _DATA class=DATA use16 hello_msg: db 'Hello, World!', 13, 10, '$' stack resb 1024 stacktop:
然后初始化栈时只需:
mov ax, _DATA mov ds, ax mov ss, ax mov sp, stacktop
为什么移除字符串后程序正常?
当你移除other()函数中的字符串字面量时,C代码生成的_DATA段为空,链接器可能会忽略该段,此时程序的ds指向汇编的data段(或合并后的段),不会出现段不匹配的问题。同时,other()函数的代码可能被编译器优化掉,避免了潜在的栈平衡问题触发的异常。
内容的提问来源于stack exchange,提问作者pts

