变量参数的 stdcall (callee-pops) 中的堆栈清理

问题描述

为了好玩,我正在学习一些汇编(目前在 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 个字节,并且程序将崩溃。

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