问题描述
为了好玩,我正在学习一些汇编(目前在 Windows 上使用 NASM),我有一个关于 stdcall
calling convention 和具有可变数量参数的函数的问题。例如,一个 sum
函数接受 X 个整数并将它们加在一起。
由于被调用者在使用 stdcall
时需要清理/重置堆栈,但您只能将常量值与 ret
一起使用,我一直想知道 pop
是否有什么问题ping 返回地址,移动 esp
,然后自己跳回调用者,而不是使用 ret
。我认为这会更慢,因为它需要更多的指令,但可以接受吗?
; int sum(count,...)
sum:
mov ecx,[esp+4] ; count
; calc args size
mov eax,ecx ; vars count
inc eax ; + count
mov edx,4 ; * 4 byte per var
mul edx
mov edx,eax
xor eax,eax ; result
cmp ecx,0 ; if count == 0
je .done
inc ecx ; count++,to start with last arg
.add:
add eax,[esp+4*ecx]
dec ecx ; if --ecx != 1,0 = return,1 = count
cmp ecx,1
jnz .add
.done:
pop ebx
add esp,edx
jmp ebx
我不明白为什么这不行,而且它似乎可以工作,但是我读过一些文章,这些文章讨论了 stdcall
如何无法处理变量参数,因为函数不能知道要传递给 ret
的值。我错过了什么吗?
解决方法
当然,如果参数的大小是常量,ret imm
会起作用。如果函数能够在运行时确定其参数的大小,那么您的想法会起作用,在这种情况下,它是从 count
参数中确定的,尽管作为 ecm points out 它可能效率低下,因为间接分支预测器不是为这种恶作剧而设计的。
但在某些情况下,被调用函数可能根本不知道参数的大小,甚至在运行时也不知道。考虑printf
。您可能会说它可以从格式字符串中推断出其参数的大小;例如,如果格式字符串是 "%d"
,那么它应该知道传递了一个 int
,因此从堆栈中清除了额外的 4 个字节。但是在 C 标准下调用是完全合法的
printf("%d",123,456,789,2222);
需要忽略多余的参数。但是根据您的调用约定,printf
会认为它只需要从堆栈中清理 4 个字节(加上它的非可变参数格式字符串参数),而它的调用者希望它清理 16 个字节,并且程序将崩溃。
因此,除非您的调用约定将包含一个“隐藏”参数,该参数告诉被调用函数要清理多少字节的参数,否则它无法工作。传递这样一个额外的参数需要更多的指令,而不是让调用者自己完成堆栈清理。