MASM汇编中调用C语言printf函数连续调用异常问题
嘿,我来帮你拆解这个连续调用printf出问题的坑——核心原因其实是调用约定不匹配加上栈平衡没处理好,这在MASM里混用stdcall模型和C语言函数时特别容易踩雷。
先给你理清楚关键的背景知识:你声明的printf PROTO C :VARARG是对的,因为C语言的printf用的是cdecl调用约定;但你的MASM程序默认用了.model flat, stdcall,这俩调用约定的核心区别就是谁来清理栈:
stdcall:被调用的函数(比如Windows API)自己负责清理栈(通过带参数长度的RET n指令)cdecl:调用者负责在调用完函数后清理栈——因为cdecl支持可变参数(比如printf的可变参数列表),被调用函数根本不知道传了多少参数,没法自己清理栈。
接下来逐个分析你的三个案例问题:
案例1:自定义proc连续调用仅第一次输出
你的print_proc是stdcall约定的proc,里面用invoke printf调用了cdecl的printf,但调用后没有清理栈。第一次调用完print_proc执行RET时,栈已经处于不平衡状态;第二次调用print_proc时,参数的位置完全错乱,printf找不到正确的输入参数,自然就没输出了。
案例2:自定义proc后直接调用printf仍无输出
和案例1的问题本质一样:_print@main里调用printf后没清理栈,RET之后栈还是歪的,后续直接调用printf时,栈上残留的旧数据会干扰新参数,导致printf读不到正确的值,所以没输出。
案例3:主程序连续调用printf输出垃圾值
第一次invoke printf后,栈上的参数(格式字符串的4字节指针、_2的2字节值,栈按4字节对齐所以总占8字节)没被清理;第二次调用printf时,新压入的参数和旧数据混在一起,printf读取到的是错误的内存数据,所以输出垃圾值。
解决方法
针对这个问题,有三种靠谱的解决思路,你可以根据自己的需求选:
方法1:手动清理栈(推荐,适合混合调用stdcall和cdecl函数)
每次调用printf后,用add esp, 参数总字节数把栈指针恢复到调用前的位置。比如案例3的代码修改后:
.386 .model flat, stdcall .stack 200h option casemap :none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib include \masm32\include\masm32rt.inc printf PROTO C :VARARG .DATA _2 DW 2 .CODE START: invoke printf, cfm$("%hd\n"),_2 add esp, 8 ; 格式字符串是4字节指针,_2按栈对齐占4字节,总8字节 invoke printf, cfm$("%hd\n"),_2 add esp, 8 invoke ExitProcess, 0 ; 别忘了加这句,不然程序结束可能出问题 END START
方法2:把程序模型改成cdecl(适合主要调用C函数的场景)
把.model flat, stdcall改成.model flat, cdecl,这样MASM会默认用cdecl调用约定,invoke指令会自动帮你处理栈清理(只要proto声明正确)。修改后的案例1代码:
.386 .model flat, cdecl ; 切换到cdecl模型 .stack 200h option casemap :none include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib include \masm32\include\masm32rt.inc printf PROTO C :VARARG .DATA _2 DW 2 _3 DW 3 _x@main@print_proc DW ? .CODE print_proc PROC ; 现在默认是cdecl约定 invoke printf, cfm$("%hd\n"),_x@main@print_proc RET print_proc ENDP START: MOV BX,_2 MOV _x@main@print_proc,BX CALL print_proc MOV BX,_3 MOV _x@main@print_proc,BX CALL print_proc invoke ExitProcess, 0 END START
注意:改成cdecl模型后,调用Windows API(比如ExitProcess)也没问题,因为masm32rt.inc里已经给这些API声明了STDCALL调用约定。
方法3:在自定义proc里手动处理栈平衡
如果不想改全局模型,那就在自定义proc里调用printf后手动清理栈,再执行RET。比如案例1的print_proc修改后:
print_proc PROC invoke printf, cfm$("%hd\n"),_x@main@print_proc add esp, 8 ; 先清理printf的参数栈 RET print_proc ENDP
这样每次调用print_proc后,栈都是平衡的,第二次调用就能正常输出了。
最后提个小细节:你的所有代码都没加ExitProcess调用,程序执行完后可能会出现未定义行为,记得加上invoke ExitProcess, 0来正确退出程序哦。
内容的提问来源于stack exchange,提问作者Mattii

