可变参数函数的调用约定[关闭]

问题描述 投票:0回答:1

初始化可变参数列表时,使用宏

va_start
并传递
list_name
,后接
the last fixed parameter before the va list starts
,因为 “最后一个固定参数与第一个变量相邻” 以及 不知何故 这有助于识别 var arg 长度/ 堆栈中的位置(我说是因为我不明白如何)。

使用

cdecl
调用约定(意味着从右到左将参数压入堆栈)
the last fixed parameter before the va list starts
在识别列表长度方面有何用处?例如,如果该参数是一个整数
3
并且变量参数也有一个
3
被调用者如何知道可变参数列表不会在这里结束,因为还有另一个
3
(固定参数)并且应该结束了吧?例如
f(int a, int b, ... )
-> 呼叫
f(1, 3, 1, 2, 3)
)

相反,有 guardian “样式”,例如,在调用函数时,您可以在可变参数的末尾添加

NULL
指针。再说一次:如果将第一个推入堆栈,那么
NULL
有什么用呢? NULL 不应该被推入参数的固定部分和可变部分之间吗? (例如
f(int a, int b, ... )
-> 呼叫
f(a, b, NULL, param1, param2)

c calling-convention cdecl
1个回答
3
投票

如果我正确理解你的疑虑,你基本上要问的是:如果所有参数都被推入堆栈而没有附加信息,那么可变参数函数如何确定其可变参数从哪里开始?

正如您已经注意到的,参数以与声明相反的顺序压入堆栈:这意味着调用为

void f(int a, ...)
f(1, 2, 3)
首先压入
3
,然后压入
2
,最后压入
1
,然后再调用。

那么如何找到可变参数的开始位置呢?

你总是知道:

  1. 堆栈顶部所在的位置。
  2. 可变参数之前需要多少个参数(固定)。

因此,按相反顺序推送值是了解变量参数列表从哪里开始的最简单方法。您将总是找到固定数量的变量,其数量等于所需(固定)参数的数量,后面是所有变量参数(如果有)。这使得无论传递的参数数量如何,都可以计算参数列表的开头,而无需在其他任何地方传递附加信息。换句话说,可变参数的起始位置距堆栈顶部的偏移量始终相同,因为它仅取决于所需参数的数量。


一个例子会让这一点更清楚。让我们假设一个函数定义为:

int f(int n, ...) {
    // ...
}

然后,编译调用

f(2, 123, 456)
。在 cdecl 下,这会产生:

push 456
push 123
push 2
call f

f
启动时,会发现堆栈处于以下状态:

--- lower addresses ----
[ return address ] <-- esp
[ 2              ]
[ 123            ]
[ 456            ]
--- higher addresses ---

现在

f
很容易知道参数列表从哪里开始,知道
n
是最后一个“固定”(非可变参数)参数:它只需要计算
esp - 4 - 4
。也就是说:从
esp
中减去保存的返回地址的固定量 (4),然后为每个固定参数减去 4(注意:这是假设
sizeof(int) == 4
)。这样做您最终将得到第一个可变参数的位置。

这适用于任意数量的可变参数:

; f(5, 1, 2, 3, 4, 5)      --- lower addresses ----
push 5                     [ return address ] <-- esp
push 4                     [ 5              ]
push 3                     [ 1              ]
push 2                     [ 2              ]
push 1                     [ 3              ]
push 5                     [ 4              ]
call f                     [ 5              ]
                           --- higher addresses ---

现在想象一下相反的场景,其中参数以相反的顺序推送,最终会得到

f(2, 123, 456)
编译为:

; f(2, 123, 456)     --- lower addresses ----
push 2               [ return address   ] <-- esp
push 123             [ 456              ]
push 456             [ 123              ]
call f               [ 2                ]
                     --- higher addresses ---

f(5, 1, 2, 3, 4, 5)
编译为:

; f(5, 1, 2, 3, 4, 5)      --- lower addresses ----
push 5                     [ return address ] <-- esp
push 1                     [ 5              ]
push 2                     [ 4              ]
push 3                     [ 3              ]
push 4                     [ 2              ]
push 5                     [ 1              ]
call f                     [ 5              ]
                           --- higher addresses ---

现在参数列表从哪里开始?仅根据堆栈指针(ESP)的值和所需参数的数量是不可能判断的,因为距堆栈顶部的偏移量不再相同,而是随着可变参数的数量而变化。为了弄清楚它,您要么必须使用基指针(EBP,假设您的函数甚至使用它,因为它不是必需的)进行一些数学运算,要么传递一些附加信息。


当变量参数被压入堆栈时,函数什么时候知道它们何时结束?

这不是调用约定所规定的。程序员必须找出一种方法来了解基于非可变参数(或其他参数)存在多少可变参数。例如,在上面的示例中,我只是将

n
作为第一个参数传递,
printf
函数系列根据字符串中格式标识符的数量(例如
%d
%s
)、
syscall 
函数根据系统调用号(第一个参数)计算出来,依此类推...

© www.soinside.com 2019 - 2024. All rights reserved.