我正在学习一些汇编的乐趣(目前在 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 个字节,并且程序会崩溃。
因此,除非您的调用约定将包含一个“隐藏”参数来告诉被调用函数要清理多少字节的参数,否则它无法工作。传递这样一个额外的参数需要更多的指令,而不是让调用者自己进行堆栈清理。
关于assembly - stdcall (callee-pops) 中变量参数的堆栈清理,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65863892/