可变参数函数(二)

2014-11-24 07:30:26 · 作者: · 浏览: 2
l,cdecl,fastcall ,thiscal,naked call。cdel是C语言中的标准调用约定,如果在定义函数中不指明调用约定(在函数名前加上约定名称即可),那编译器认为是cdel约定,从上面的几种约定来看,只有cdel约定才可以定义可变参数函数。下面是cdel约定的重要特征:如果函数A调用函数B,那么称函数A为调用者(caller),函数B称为被调用者(callee)。caller把向callee传递的参数存放在栈中,并且压栈顺序按参数列表中从右向左的顺序;callee不负责清理栈,而是由caller清理。 我们用一个简单的例子来说明问题,并采用Nasm的汇编格式写相应的汇编代码,程序段如下:
voidcallee(int a, int b)
{
int c= 0;
c = a+b;
}
voidcaller()
{
callee(1,2);
}
来分析一下在调用过程发生了什么事情。程序执行点来到caller时,那将要执行调用callee函数,在跳到callee函数前,它先要把传递的参数压到栈上,并按右到左的顺序,即翻译成汇编指令就是push 2; push 1;
图2
函数栈如图中(a)所示。接着跳到callee函数,即指令callcalle。CPU在执行call时,先把当前的EIP寄存器的值压到栈中,然后把EIP值设为callee(地址),这样,栈的图变为如图2(b)。程序执行点跳到了callee函数的第一条指令。C语言在函数调用时,每个函数占用的栈段称为stackframe。用ebp来记住函数stackframe的起始地址。故在执行callee时,最前的两条指令为:
pushebp
movesp, ebp
经过这两条语句后,callee函数的stackframe就建好了,栈的最新情况如图2(c)所示。 函数callee定义了一个局部变量int c,该变量的储存空间分配在callee函数占用的栈中,大小为4字节(insizeof int)。那么callee会在如下指令:
subesp, 4
mov[ebp-4], 0
这样栈的情况又发生了变化,最新情况如图2(d)所示。注意esp总是指向栈顶,而ebp作为函数的stackframe基址起到很大的作用。ebp地址向下的空间用于存放局部变量,而它向上的空间存放的是caller传递过来的参数,当然编译器会记住变量c相对ebp的地址偏移量,在这里为-4。跟着执行c = a+ b语句,那么指令代码应该类似于:
moveax , [ebp + 8] ;这里用eax存放第一个传递进来的参数,记住第一个参数与ebp的偏移量肯定为8addeax, [ebp + 12] ;第二个参数与ebp的偏移量为12,故计算eax =a+b
mov[ebp -4], eax ;执行 c =eax, 即c =a+b
栈又有了新了变化,如图2(e)。至此,函数callee的计算指令执行完毕,但还要做一些事情:释放局部变量占用的栈空间,销除函数的stack-frame过程会生成如下指令:movesp, ebp;把局部变量占用的空间全部略过,即不再使用,ebp以下的空间全部用于局部变量pop ebp;弹出caller函数的stack-frame 基址在Intel CPU里上面两条指令可以用指令leave来代替,功能是一样。这样栈的内容如图2(f)所示。最后,要返回到caller函数,因此callee的最后一条指令是ret,ret指令用于把栈上的保存的断点弹出到EIP寄存器,新的栈内容如图2(g)所示。函数callee的调用与返回全部结束,跟着下来是执行callcallee的下一条语句。
从caller函数调用callee前,把传递的参数压到栈中,并且按从右到左的顺序;函数返回时,callee并不清理栈,而是由caller清楚传递参数所占用的栈(如上图,函数返回时,1和2还放在栈中,让caller清理)。栈元素的大小为4个字节,每个参数占用栈空间大小为4字节的倍数,并且任何两个参数都不能共用同一个栈元素。
从C语言的函数调用约定可知,参数列表从右向左依次压栈,故可变参数压在栈的地址比最后一个命名参数还大,如下图3所示:
由图3可知,最后一个命名参数a上面都放着可变参数,每个参数占用栈的大小必为4的倍数。因此:可变参数1的地址 = 参数a的地址 + a占用栈的大小,可变参数2的地址 = 可变参数1的地址 + 可变参数1占用栈的大小,可变参数3的地址 = 可变参数2的地址 + 可变参数2占用栈的大小,依此类推。如何计算每个参数占用栈的大小呢?
2. 数据对齐问题
对于两个正整数 x, n 总存在整数 q, r 使得
x = nq+ r, 其中0<= r
q, r 是唯一确定的。q =[x/n], r = x - n[x/n]. 这个是带余除法的一个简单形式。在 c 语言中, q, r 容易计算出来: q =x/n, r = x % n.
所谓把 x 按 n 对齐指的是:若 r=0, 取 qn, 若 r>0, 取 (q+1)n. 这也相当于把 x 表示为:
x = nq+ r', 其中 -n< r' <=0 //最大非正剩余
nq 是我们所求。关键是如何用 c 语言计算它。由于我们能处理标准的带余除法,所以可以把这个式子转换成一个标准的带余除法,然后加以处理:
x+n =qn + (n+r'),其中0
x+n-1= qn + (n+r'-1), 其中0<= n+r'-1
所以 qn =[(x+n-1)/n]n. 用 c 语言计算就是:
((x+n-1)/n)*n
若 n 是 2 的方幂, 比如 2^m,则除为右移 m 位,乘为左移 m 位。所以把 x+n-1的最低 m 个二进制位清 0就可以了。得到:
(x+n-1)& (~(n-1))
根据这些推导,相信已经了解#define__va_rounded_size(TYPE) (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) *sizeof (int))的涵义。
3. 再看va_* 宏定义
va_start(va_listap, last)
last为最后一个命名参数,va_start宏使ap记录下第一个可变参数的地址,原理与“可变参数1的地址 = 参数a的地址 + a占用栈的大小”相同。从ap记录的内存地址开始,认为参数的数据类型为type并把它的值读出来;把ap记录的地址指向下一个参数,即ap记录的地址 +=occupy_stack(type)
va_arg(va_litap, type)
这里是获得可变参数的值,具体工作是:从ap所指向的栈内存中读取类型为type的参数,并让ap根据type的大小记录它的下一个可变参数地址,便于再次使用va_arg宏。从ap记录的内存地址开始,认为存的数据类型为type并把它的值读出来;把ap记录的地址指向下一个参数,即ap记录的地址 +=occupy_stack(type)
va_end(va_listap)
用于“释放”ap变量,它与va_start对称使用。在同一个函数内有va_start必须有va_end。
可变参数宏定义
printf()和fprintf()这些输出函数的参数是可变的,在调试程序时,你可能希望定义自己的参数可变的输出函数,
那么可变参数宏会是一个选择。
C99中规定宏可以像