Assembly 变量参数的stdcall(被调用方pops)中的堆栈清理

Assembly 变量参数的stdcall(被调用方pops)中的堆栈清理,assembly,x86,variadic-functions,calling-convention,Assembly,X86,Variadic Functions,Calling Convention,我正在学习一点汇编的乐趣(目前在Windows上使用NASM),我有一个关于参数数目可变的and函数的问题。例如,一个sum函数,它接受X个整数并将它们全部相加 由于被调用方在使用stdcall时需要清理/重置堆栈,但只能对ret使用常量值,因此我一直想知道popping返回地址、移动esp、自己跳回调用方而不是使用ret是否有什么问题。我想这会比较慢,因为它需要更多的说明,但它可以接受吗 ; int sum(count, ...) sum: mov ecx, [esp+4] ; cou

我正在学习一点汇编的乐趣(目前在Windows上使用NASM),我有一个关于参数数目可变的and函数的问题。例如,一个
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
参数来确定参数的大小的,尽管它可能效率低下,因为间接分支预测器不是专为此类恶作剧而设计的

但在某些情况下,被调用函数可能根本不知道参数的大小,甚至在运行时也不知道。考虑<代码> Prtff < /代码>。您可能会说它可以从格式字符串推断其参数的大小;例如,如果格式字符串是
“%d”
,那么它应该知道传递了一个
int
,因此从堆栈中清除额外的4个字节。但在C标准下,调用

printf("%d", 123, 456, 789, 2222);
需要忽略多余的参数。但是根据您的调用约定,
printf
会认为它只需要从堆栈中清除4个字节(加上它的非可变格式字符串参数),而它的调用者则希望它清除16个字节,程序就会崩溃


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

即使在
stdcall
下,可变参数也会切换到调用者清理。我认为在Windows上,可变函数永远不是stdcall。如果大多数库函数都是stdcall或fastcall(32位版本的fastcall的callee pops),那么printf之类的函数就是cdecl(caller pops)。好吧,让我们不要挂断电话,我把它称为“stdcall”,而是关注我自己的调用约定是否有什么问题,在这里,我清理堆栈,即使是在一个具有可变参数的函数上^^^^而不是性能,并且需要一个临时寄存器,是的,你可以跳转到一个寄存器,调整
(r/e)sp
,然后跳转到该寄存器的内容。性能可能会受到影响,因为您可能会混淆分支预测器的函数调用处理。注册表显然需要可用,因为您以后无法还原它。请注意,
add
sub
到堆栈指针将修改状态标志;使用
lea
pop
来避免这种情况。@ecm:有趣的事实:如果您使用
push-reg
/
ret
而不是
jmp-ebx
(因为您仍然返回到相同的位置。返回地址预测通过正确假设call/ret-nest来工作(类似于返回地址的硬件数据结构的内部堆栈),与ESP指向的位置无关)。仅供记录:确认您是对的:定义良好。但是,转换和它引用的参数之间的不匹配是UB,(),但这是一个单独的问题。UB允许x86-64 SysV这样的东西按照xmm2中的第三个FP arg在单独的寄存器中传递FP和整数arg,而不是xmm2中的第三个总arg(如果它是FP的话)(就像Windows x64那样)