首页 > 解决方案 > 变量参数的 stdcall (callee-pops) 中的堆栈清理

问题描述

stdcall我正在学习一些组装的乐趣(目前在 Windows 上使用 NASM),并且我对调用约定和具有可变数量参数的函数有疑问。例如,一个sum函数接受 X 个整数并将它们加在一起。

由于被调用者在使用时需要清理/重置堆栈,但stdcall您只能使用常量值ret与. 我认为这会更慢,因为它需要更多指令,但可以接受吗?popespret

; 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。我错过了什么吗?

标签: assemblyx86variadic-functionscalling-convention

解决方案


ret imm如果参数的大小是常数,当然可以。如果函数能够在运行时确定其参数的大小,您的想法将起作用,在这种情况下,它可以从count参数中确定,尽管正如ecm 指出的那样,它可能效率低下,因为间接分支预测器不是为此类恶作剧而设计的.

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

printf("%d", 123, 456, 789, 2222);

需要忽略多余的参数。但是根据你的调用约定,printf它会认为它只需要从堆栈中清理 4 个字节(加上它的非可变格式字符串参数),而它的调用者会期望它清理 16 个字节,程序就会崩溃。

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


推荐阅读