设为首页 加入收藏

TOP

C编译器剖析_C语言的变参函数(一)
2015-03-19 03:34:57 来源: 作者: 【 】 浏览:294
Tags:编译器 剖析 语言 函数

C语言的变参函数

UCC编译器中有不少地方使用了C语言的变参函数,这里我们专门用一小节来对C语言变参函数的实现原理进行分析。C标准库中的printf函数就是一个典型的变参函数,其接口如下所示,函数声明中的省略号…表明这是一个变参函数。

int printf(const char *format, ...);

下面我们举一个简单的例子来说明printf函数的调用过程,如图4.2.12所示。图中第1至11行对应是hello.c,而第12至25行是由UCC编译器生成的抽象语法树hello.ast,第26至33行则是UCC的中间代码hello.uil,第34至52行则是UCC生成的部分汇编代码hello.s。

\

图4.2.12 printf()函数

我们注意到第9行的实参a是float类型,而d是char类型,第17行的抽象语法树的功能是把实参a转换成double类型,而第21行则用于把实参d转换成int类型。我们在2.4节讨论C语言的类型系统时介绍过,对于形如int f()的旧式风格函数声明,C编译器会按C标准的要求进行实参提升的操作,在UCC编译器中,这个动作由函数PromoteArgument()来完成。对于变参函数的参数,例如上述第9行printf函数调用中格式化字符串之后从a开始的参数,C编译器也要进行相应的实参提升,即把小于int型的char和short提升为int类型,把float类型提升为double类型。图4.2.12第29至31行的中间代码很直观地反映了这个实参提升的过程。与之对应的汇编代码如第39至52行所示,第39至40行的代码把float类型的a转换为double类型,把转换后的结果存到临时变量-12(%ebp)中,我们在1.5节时介绍过与浮点运算相关的汇编指令。按照C函数的调用约定,参数按从右到左的次序依次入栈,第41行的指令完成了对参数d由char到int的转换,第42行把转换后的结果入栈,第43行则把参数c入栈,第44至46行则从全局静态数据区加载双精度浮点数b并入栈,第47至49行则从临时变量-12(%ebp)中加载双精度浮点数,并入栈。第50行把格式化字符串的首地址入栈,我们在第38行时已将其地址存到寄存器eax中,第51行则进行真正的函数调用,因为所有的参数都是存放在栈中,共占去了4+8+8+4+4(即28)字节,当函数printf返回时,我们在第52行把esp指针进行加28的操作。

库函数printf()的代码在我们编写上述hello.c时就已经存在,这意味着对被调函数printf其实并不知道我们在调用它时到底传递了几个实参。对printf而言,它只是按照格式化字符串的说明,从栈中取出相应的参数。

// 实际上只有10这一个参数,但printf看到有两个%d,

// 于是仍试图从栈中取两个参数,打印出形如10,1074172310的垃圾值

printf(“%d, %d “,10);

// 实际上有10,20,30这3个参数,但printf只看到一个%d,

// 于是只打印出参数10

printf(“%d”, 10,20,30);

图4.2.13更清楚地描述了上述过程,虚线的左侧为数据构中的栈区,右侧为全局静态数据区。在栈区中,我们标出了真正入栈的参数类型依次为int、int、double、double和char *,它们共占用了28个字节的栈空间。而格式化字符串实际上是存放在全局静态数据区的,压入栈中的只是该字符串的首地址。

\

图4.2.13 栈示意图

下面,让我们换个角度来看问题,假设我们是printf库函数的实现者,在printf函数的函数体内,我们通过形参format就可以访问到全局静态数据区中的格式化字符串,通过表达式&format我们就可以知道format在栈中的内存地址。由图4.2.13所示的内存布局,我们可由&format计算出其他参数的地址,有了这些参数的内存地址,我们就可以访问它们。为计算方便,让我们不妨假设&format的地址为十进制的10000,由图可算出其上方的4个参数对应的地址依次为十制制的10004、10012、10020和10024。其计算过程如下所示:

int printf(const char *format, ...){

unsigned int addr =&format;

// 与“a = %f”中%f对应的参数类型为double,地址为

addr + sizeof(char *),即10004

// 与“b = %f”中%f对应的参数类型为double,地址为

addr + sizeof(char *)+sizeof(double),即10012

// 与“c = %d”中%f对应的参数类型为int,地址为

addr + sizeof(char*)+sizeof(double) + sizeof(double),即10020

// 与”d = %c”中%c对应的参数类型为char,地址为

addr + sizeof(char*)+sizeof(double) + sizeof(double)+sizeof(int),即10024

}

在知道内存单元的地址,且知道该内存单元对应的类型的前提下,访问该单元的内容则是件容易的事情,例如要访问上述地址为10004的double类型的内存单元,我们只要用C语言写出如下代码即可。通过表达式*dptr进行“提领”操作,我们就可以为所欲为了。

double * dptr = (double *) (addr +sizeof(char *));

按照这样的思路,我们可以写出如下所示的OurPrintfV1变参函数,用于从栈中取出format之上的其他“无名的”参数,这个函数纯粹是为了演示如何根据形参format的地址来访问其他“无名的”参数,如图4.2.13所示。需要注意的是,我们有意忽略了对格式化字符串的处理过程,虽然这只是一个简单的字符串判断,并不会太复杂。在函数体中添加的printf调用,纯粹是为了验证我们确实正确地从栈中取出了实参。

\

图4.2.14 OurPrintfV1()

为了让图4.2.14中的代码看起来更优雅些,我们会引入一些宏来处理“由format的地址来定位其他参数”的过程,图4.2.15中的OurPrintfV2()完成的工作与OurPrintfV1()类似,但看起来简法多了。

\

图4.2.15 OurPrintfV2()

对比图4.2.14和图4.2.15,我们可以很清楚地看到宏定义va_start()所做的工作就是取形参format的地址,并对其做&format+sizeof(format)的运算,我们在前面介绍过,C编译器会对变参函数中的“匿名”参数进行实参提升的操作,所以真正入栈的实参所占的内存大小都会是sizeof(int)的整数倍,图4.2.15第9行的宏定义ALIGN_INT(n)完成了这个对齐的操作。假设sizeof(char)为1,sizeof(short)为2且sizeof(int)为4,则对以下宏来说,

宏ALIGN_INT(char)展开后为(1+3)&(~3),即4

宏ALIGN_INT(short)展开后为(2+3) &(~3) ,即4

我们知道,一个整数乘以4,相当将其左移2位,这意味着任何一个为4倍数的整数的最低2位都是0。上述(1+3

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇Objective-C单例模式 下一篇C语言记忆化搜索_漫步校园(Hdu 14..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: