stdcall中可变参数函数的堆栈清理(被调用者弹出)

3
我正在为了娱乐学习一些汇编知识(目前在Windows下使用NASM),关于stdcall calling convention和具有可变参数数量的函数,我有一个问题。例如,一个sum函数,它接受X个整数并将它们全部加在一起。

由于调用方需要在使用< code> stdcall 时清除/重置堆栈,但您只能使用常量值< code> ret ,因此我一直在思考是否使用< code> pop 弹出返回地址,移动< code> esp 并跳回到调用者本身,而不是使用< code> 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 的值。我有什么遗漏的吗?

2
变量参数在 stdcall 下切换到调用者清理。 - Jester
3
我认为在Windows平台上,可变参数函数从来不是stdcall调用方式。如果大多数库函数是stdcall或fastcall(32位fastcall使用被调用者清理堆栈),像printf这样的函数是cdecl调用方式(调用者清理堆栈)。 - Peter Cordes
我明白了。好的,我们不要纠结于我称之为“stdcall”,而是关注一下我的调用约定是否有问题,即使在具有可变参数的函数中,我也清理堆栈^^ - Mars
2
除了性能和需要一个临时寄存器之外,是的,你可以将内容弹出到一个寄存器中,调整 (r/e)sp,然后跳转到该寄存器的内容。性能可能会受到影响,因为你可能会混淆分支预测器的函数调用处理。显然需要可用的寄存器,因为你无法在以后恢复它。请注意,对堆栈指针进行 addsub 操作会修改状态标志;使用 leapop 来避免这种情况。 - ecm
1
@ecm:有趣的事实:如果你使用push reg / ret而不是jmp ebx返回,分支预测就会很好(因为你仍然返回到相同的位置。返回地址预测通过假定调用/返回嵌套正确(一个类似于内部堆栈的硬件数据结构的返回地址),与ESP指向的位置无关)。 - Peter Cordes
相关:C语言中printf()使用哪种调用约定?解释了为什么我们不这样做,因为即使你有正确的字节数,这也是低效的。 - Peter Cordes
1个回答

4
当参数的大小是一个常量时,ret imm可以起作用。如果函数能够在运行时确定其参数的大小,那么你的想法将会起作用,而在本例中,它是通过count参数来确定参数大小的,尽管如ecm指出这可能效率较低,因为间接分支预测器并不设计用于此类把戏。
但在某些情况下,调用函数可能完全不知道参数的大小,甚至在运行时也不知道。考虑printf。你可能会说它可以从格式字符串推断其参数的大小;例如,如果格式字符串为"%d",那么它应该知道传递了一个int,因此需要从堆栈清除额外的4个字节。但在C标准下这是完全合法的。
printf("%d", 123, 456, 789, 2222);

多余的参数应该被忽略。但是在你的调用约定下,printf 只会认为它只需要从堆栈上清理 4 字节(加上其非变参格式字符串参数),而它的调用者则期望它清理 16 字节,程序将会崩溃。

所以,除非你的调用约定包括一个“隐藏”的参数告诉被调用函数需要清理多少字节的参数,否则它是不能工作的。传递这样一个额外的参数将需要比让调用者自己进行堆栈清理更多的指令。


只是为了记录:使用过多的参数调用printf是否未定义行为?确认您是正确的:定义良好。 但是,转换和它*实际引用的参数不匹配是UB,(为什么在printf中与转换说明符不匹配的参数是未定义行为?),但这是一个不同的问题。那些被视为UB的允许x86-64 SysV将FP和整数参数分别传递到第3个FP参数中的xmm2寄存器中,而不是FP的第3个总体参数(如Windows x64所做的那样) - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接