不定参数实现的宏分析

发信人: IceVolcano (一个昵称), 板面: C++
标 题: Re: C支持默认参数函数吗?
发信站: 飘渺水云间 (Thu Dec 8 19:28:08 2005), 站内信件

我以前写的,参考参考把


标 题: 宏——不定参数


以前看书时一直对不定参数迷惑,虽然scanf,printf照用,但没深入了解。
直到在大半年前向lazybug友问起这一事,他给了我一个msdn的例子。我研究了一下,初步
弄懂,写了些文字,不巧被删了。
前几天与zerox友聊起,说预编译器太弱了,我们要联合预编译器耍耍编译器。结果想破了
头宏函数还是没弄出来,倒又对不定参数研究了一把。
再研究的缘起于airmodel的一片帖子,于是起了兴趣,写了文字。
又想起QuoVadis前几天也发过一个帖子,看了,有人回答了,我就不回复了,何况也提不出
别的东西,不重复解答。
以上算作缘起
addplus后来提出的问题直接导致了我再次思考一个细节,并更正了错误,再次表示鸣谢。

此文献给lazybug,zerox,airmodel,QuoVadis,addplus和其他C/C++板的网友。各位权当
小说看看,愉悦身心而扩展知识之事何乐而不为呢?如有错误及欠妥之处,还望各位不吝指
正。


在实现不定参数函数的时候,要用到va_size,va_start,va_arg三个宏。
在stdarg.h里有对这三个宏的实现。
此头文件如下,略去了开头的一大段注释:

#ifndef _MACHINE_STDARG_H_
#define _MACHINE_STDARG_H_

#include
#include

#ifndef _VA_LIST_DECLARED
#define _VA_LIST_DECLARED
typedef __va_list va_list;
#endif

#if (defined(__GNUC__) && (__GNUC__ == 2 && __GNUC_MINOR__ > 95 || __GNUC__ >=
3) && !defined(__INTEL_COMPILER))

#define va_start(ap, last) \
__builtin_stdarg_start((ap), (last))

#define va_arg(ap, type) \
__builtin_va_arg((ap), type)

#if __ISO_C_VISIBLE >= 1999
#define va_copy(dest, src) \
__builtin_va_copy((dest), (src))
#endif

#define va_end(ap) \
__builtin_va_end(ap)

#else /* ! (__GNUC__ post GCC 2.95 || __INTEL_COMPILER) */

#define __va_size(type) \
(((sizeof(type) + sizeof(int) - 1) / sizeof(int)) * sizeof(int))

#if defined(__GNUC__) && !defined(__INTEL_COMPILER)
#define va_start(ap, last) \
((ap) = (va_list)__builtin_next_arg(last))
#else /* non-GNU compiler */
#define va_start(ap, last) \
((ap) = (va_list)&(last) + __va_size(last))
#endif /* __GNUC__ */

#define va_arg(ap, type) \
(*(type *)((ap) += __va_size(type), (ap) - __va_size(type)))

#if __ISO_C_VISIBLE >= 1999
#define va_copy(dest, src) \
((dest) = (src))
#endif

#define va_end(ap)

#endif /* __GNUC__ post GCC 2.95 */

#endif /* !_MACHINE_STDARG_H_ */




开始是一些基本的头文件保护和一些基本的define宏,以备后用,跳过。
然后碰到一个小兵:va_list。
va_list是__va_list,追

根揭底是char *,已解决(小兵就是小兵,唉)。
然后关注三个关键宏va_size,va_start,va_arg,三元猛将。
然而进一步考察发现,他们是双胞胎,根据是否用gcc决定是哥哥还是弟弟出征。
先考虑不用gcc的情况,暂且称之为哥哥。

第一个宏__va_size:
#define __va_size(type) \
(((sizeof(type) + sizeof(int) - 1) / sizeof(int)) * sizeof(int))
这个宏并不在“应用层”(套用一下tcp/ip的说法)出现,我们用不定参数的时候并不会接
触到这个宏。他是为了另外两个存在的。确切的说,他是秘书。但我们还得了解它,不然编
译器会报错,实际运行会core dump。
咋一眼看来,是一个关于类型大小的映射。
映射结果如下:
1 4
2 4
3 4
4 4
5 8
6 8
7 8
8 8且恍┗镜耐肺募;ず鸵恍┗镜膁efine宏,以备后用,跳过。
然后碰到一个小兵:va_list。
va_list是__va_list,追根揭底是char *,已解决(小兵就是小兵,唉)。
然后关注三个关键宏va_size,va_start,va_arg,三元猛将。
然而进一步考察发现,他们是双胞胎,根据是否用gcc决定是哥哥还是弟弟出征。
先考虑不用gcc的情况,暂且称之为哥哥。

第一个宏__va_size:
#define __va_size(type) \
(((sizeof(type) + sizeof(int) - 1) / sizeof(int)) * sizeof(int))
这个宏并不在“应用层”(套用一下tcp/ip的说法)出现,我们用不定参数的时候并不会接
触到这个宏。他是为了另外两个存在的。确切的说,他是秘书。但我们还得了解它,不然编
译器会报错,实际运行会core dump。
咋一眼看来,是一个关于类型大小的映射。
映射结果如下:
1 4
2 4
3 4
4 4
5 8
6 8
7 8
8 8
9 12
10 12
11 12
12 12
. .
. .
. .
看起来像“对齐”。估计这就是做这样映射的原因。具体情况待下面给出示例代码后再说。

第二个宏va_start:
#define va_start(ap, last) \
((ap) = (va_list)&(last) + __va_size(last))
可见,他是跳过last段,让栈指针指向下一个参数地址。
这里无端端的以跳过一个参数号称start,是不是犯傻?给出示例代码后再说。

刚刚开始就扔下两个问题,搞得人心里痒痒,确有不负责任之嫌。但其实,我写这个东西也
蛮头疼的,我知道看的人也很不爽,说实话如果我看也很不爽,总是有个疙瘩在心里,不免
心里会骂作者,“死没良心的,居然调人胃口,好,老子看你有何能耐”。
上面两个问题的完全解答依赖对示例代码的分析,而对示例代码的分析依赖于对va_start的
分析,所以这里只好先提出这么一个问题。因为不提的话,后来的对示例代码的分析会放松
对va_start的警戒,以为没什么名堂,可有可无,或者机械的套用。

其实很多书都这样,开头写的东西不能太深入,因为只有说道后面的知识才能深入下去,而
后面的知识是在前面的知识的基础上。这样只能靠读者去反复阅读,其意自显。而且有的书
,在我细读前一两章的东西后,再看后面,居然发现自己已经得出了结论。这说明整个书所
述的知识是一个
体系,相互依赖。而好的书,在于作者能够把握这个体系,能够把整个知识当作一个网络来
讲述……
唉!说着说着就入了无数个栈,还好我是学过的,自认对栈的管理还是可以,现在就退栈,
接着第三个宏。


第三个宏va_arg:
#define va_arg(ap, type) \
(*(type *)((ap) += __va_size(type), (ap) - __va_size(type)))
这个宏有三个部分,他做了2件事
1、返回为type所标明的类型的值,就是取值。
如:int i = va_arg(format, int);
2、让栈指针移到下一个参数的头位置。
三个部分的解释:
*(type *):返回为type所标明的类型的值,非地址。
(ap) += __va_size(type):ap移到后一个参数。
(ap) - __va_size(type):给出此宏所取的参数的地址。
这里,用了一个逗号句法,实现移动栈指针和返回参数地址。

哥哥被考察完了,用它来实现点东西,示例代码如下:

#undef __GNUC__
#include
#include
//#define GOINT

void func(char * list, ...)
{
va_list l;
int i;
char c;
printf("list: 0x%08x\n", &list);
va_start(l,list);
printf("va_start: 0x%08x\n", l);
#ifdef GOINT
i = va_arg(l, int);
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = va_arg(l, int);
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = va_arg(l, int);
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = va_arg(l, int);
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
#else
c = va_arg(l, char);
printf("va_arg: 0x%08x", l); printf(" - %c\n", c);
c = va_arg(l, char);
printf("va_arg: 0x%08x", l); printf(" - %c\n", c);
c = va_arg(l, char);
printf("va_arg: 0x%08x", l); printf(" - %c\n", c);
c = va_arg(l, char);
printf("va_arg: 0x%08x", l); printf(" - %c\n", c);
#endif
}

int main(void)
{
int i = 1;
char c = 'a';
#ifdef GOINT
func("ABCDEF",i,i,i,i);
#else
func("ABCDEF",c,c,c,c);
#endif
printf("%d\n", sizeof("ABCDEF"));
return 0;

}



这里有几点值得关注:
1、第一行的#undef __GNUC__是被逼的
否则就会依照gnu c complier的方法把__builtin_xxx编进去
2、#define GOINT的结果:
list: 0xbfbfecd0
va_start: 0xbfbfecd4
va_arg: 0xbfbfecd8 - 1
va_arg: 0xbfbfecdc - 1
va_arg: 0xbfbfece0 - 1
va_arg: 0xbfbfece4 - 1
没有#define GOINT的结果:
list: 0xbfbfecd0
va_start: 0xbfbfecd4
va_arg: 0xbfbfecd8 - a
va_arg:

0xbfbfecdc - a
va_arg: 0xbfbfece0 - a
va_arg: 0xbfbfece4 - a

两者一样,char和int在栈里的所占空间一样大。
这可以解释为什么要提供一个va_size而不直接用sizeof。
看来“对齐”是无处不在。

栈结构如下:
#define GOINT的结果:

|d0 |d4 |d8 |dc |e0 |
+------+------+------+------+------+
|char *| 1| 1| 1| 1|
+------+------+------+------+------+
|4bytes|4bytes|4bytes|4bytes|4bytes|

没有#define GOINT的结果:

|d0 |d4 |d8 |dc |e0 |
+------+------+------+------+------+
|char *| 1| 1| 1| 1|
+------+------+------+------+------+
|4bytes|4bytes|4bytes|4bytes|4bytes|

其中char *指向ABCDEF\n在内存里的位置,所以,如下的话:
printf("l=list: 0x%08x\n", l);
打印出:
l=list: 0x0804875c

这里有一点,C的进栈是从右到左,也就是说char *至最后进入的。
结合第1、2个宏,可以看出,为了使栈内参数顺序和数据位存放顺序一致,栈向小的那个方
向增长,最后指向最小的那个位置。


代码流程大致如下:
1、用va_start得到栈“开头”地址,开头打引号。
因为牺牲了第一个参数。
2、用va_arg得到设计的变量类型的值,即传入的形参值。
3、重复2,直至所有参数取完。
实际应用中第3步由传入函数的一个参数保证,以printf为例,他的第一个参数是字符串。
函数在得到这个字符串后,对之分析,提取%d%x等。可见,第一个参数一般担当决定参数个
数和类型的任务。
但是这个任务是由程序员控制的,所以这里就可以做点文章。喜欢搞鬼的家伙们就爱这些由
程序员实际控制的代码。他绕过了编译器,或者说利用了编译器,或者说玩了一把编译器,
结果得到了一个不受束缚的指针,一般就会忍不住指向root shell。


接下来理应分析弟弟了,可是,上面说到的#undef __GNUC__让我想各位致歉。近期我没能
力也没太有时间和兴趣深入研究gcc。就算man以下,也是搜索字符串看重点。
起先我的示例代码里并没有#undef __GNUC__。用gcc -E main.c | less一看,结果是
i = __builtin_va_arg((l), int);
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = __builtin_va_arg((l), int);
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = __builtin_va_arg((l), int);
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = __builtin_va_arg((l), int);
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
预编译器并没有解释 __builtin_va_arg,我想到一定是定义了__GNUC__。于是加上#undef
__GNUC__,结果对了
i = (*(int *)((l) += (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int)), (l) - (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int))));
printf("va_arg: 0x%08x", l); printf(" - %d\n", i

);
i = (*(int *)((l) += (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int)), (l) - (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int))));
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = (*(int *)((l) += (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int)), (l) - (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int))));
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = (*(int *)((l) += (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int)), (l) - (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int))));
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
而且#undef __GNUC__要加在第一行,看起来是gcc默认了__GNUC__。如果-E后的结果不再预
处理,直接送入编译器,那么看起来是编译器认识__builtin_xxx了,就没有头文件可以看
了。没办法,i服了u。
不过这f("va_arg: 0x%08x", l); printf(" - %d\n", i);
预编译器并没有解释 __builtin_va_arg,我想到一定是定义了__GNUC__。于是加上#undef
__GNUC__,结果对了
i = (*(int *)((l) += (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int)), (l) - (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int))));
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = (*(int *)((l) += (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int)), (l) - (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int))));
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = (*(int *)((l) += (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int)), (l) - (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int))));
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
i = (*(int *)((l) += (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int)), (l) - (((sizeof(int) + sizeof(int) - 1) / sizeof(int)) *
sizeof(int))));
printf("va_arg: 0x%08x", l); printf(" - %d\n", i);
而且#undef __GNUC__要加在第一行,看起来是gcc默认了__GNUC__。如果-E后的结果不再预
处理,直接送入编译器,那么看起来是编译器认识__builtin_xxx了,就没有头文件可以看
了。没办法,i服了u。
不过这里倒是引出一个问题。
我先写的示例代码是没有#undef __GNUC__,也就是以__builtin_xxx编译的。这个时候
c = va_arg(l, char);
printf("va_arg: 0x%08x", l); printf(" - %c\n", c);
c = va_arg(l, char);
printf("va_arg: 0x%08x", l); printf(" - %c\n", c);
c = va_arg(l, char);
printf("va_arg: 0x%08x", l); printf(" - %c\n", c);
c = va_arg(l, char);
printf("va_arg: 0x%08x", l); printf(" - %c\n", c);
这个段是warning的,(既然是编译器给出的warning,似乎是由编译器解释__builtin_xxx
了)
main.c: In function `func':
main.c:24: warning: `char' is promoted to `int' when passed through `...'
main.c:24: warning: (so you should pass `int' not `char' to `va_arg')
main.c:24: note: if this code is reache

d, the program will abort
main.c:26: warning: `char' is promoted to `int' when passed through `...'
main.c:26: note: if this code is reached, the program will abort
main.c:28: warning: `char' is promoted to `int' when passed through `...'
main.c:28: note: if this code is reached, the program will abort
main.c:30: warning: `char' is promoted to `int' when passed through `...'
main.c:30: note: if this code is reached, the program will abort
实际的运行也导致了core dump,即使是配合func("ABCDEF",i,i,i,i);的传入,也照样
dump。




这里对QuoVadis的帖子再作分析,他的给出代码如下:
float Test(float first,...)
{
float sum = 0, i = first;
va_list marker;
va_start(marker, first);
for (int j = 0; j < 3; ++j)
{
sum += i;
i = va_arg(marker, float);
}
va_end( marker );
return sum;
}
float x = Test(1.0, 2.0, 3.0);

不出我所料
main.c: In function `Test':
main.c:42: error: 'for' loop initial declaration used outside C99 mode
main.c:45: warning: `float' is promoted to `double' when passed through `...'
main.c:45: warning: (so you should pass `double' not `float' to `va_arg')
main.c:45: note: if this code is reached, the program will abort
*** Error code 1

Stop in /home/ycheng/programs/c.

修改关于j,编译通过,但是运行dump。
加上#undef __GNUC__,编译ok
#undef __GNUC__
#include
#include

float Test(float first,...)
{
float sum = 0, i = first;
int j;
va_list marker;
va_start(marker, first);
for(j = 0; j < 3; ++j)
{
sum += i;
i = va_arg(marker, float);
}
va_end(marker);
return sum;
}

int main(void)
{
float x = Test(1.0, 2.0, 3.0);
printf("%f\n", x);
return 0;

}

运行结果3.000000。
原因显然是1.0, 2.0, 3.0默认是double所致。
但还有一个问题,就是i = first一句。
我看了好久,i的值在for循环里的i = va_arg(marker, float);后分别是0 2 0,怎么会出
来3?
原因就在这一句,在sum += i;的配合下,sum的值在整个过程里分别为
0 //float sum = 0
1 //1st for(), i = first; sum += 1;
1 //2nd for(), sum += 0
3 //3rd for(), sum += 2

| 4bytes | 4bytes | 4bytes | 4bytes | 4bytes |
+-----------+-----------+-----------+-----------+-----------+
|00|00|80|3F|00|00|00|00|00|00|00|40|00|00|00|00|00|00|80|40|
+-----------+-----------+-----------+-----------+-----------+
| 1.0f | 2.0 | 3.0 |
^
|
栈指针

这里左面地址小右面大,结合进栈顺序,3.0 2.0 1.0f的进站顺序为3.0->2.0->1.0f。
3.0先进,占据最高8bytes,然后2.0,再8bytes,最后1.0f,只有4bytes。栈指针指向1.0
的最低位

这个很要紧,关系到va_arg(marker, float);取哪4个bytes

结合代码:
float sum = 0, i = first;
//sum=0, i=1,这个1是由于类型转换,根

栈里数据存关系不大,有还是有的
int j;
va_list marker;
va_start(marker, first);
//marker跳过了开始的4bytes,也就是跳过了1.0f
{
sum += i;
//sum+=1 sum=1
i = va_arg(marker, float);
//取出2.0的高4位当成float,结果是0,marker走
}
{
sum += i;
//sum+=2 sum=3
i = va_arg(marker, float);
//取出2.0的低4位当成float,结果是2,marker走
}
{
sum += i;
//sum+=0 sum=3
i = va_arg(marker, float);
//取出3.0的高4位当成float,结果是0,marker走
}
va_end(marker);
return sum;
//不用再说了
由此还可得一点,第一个参数虽然是double,但是函数声明里是float。
C里的栈是归调用者管的,这里是main管理到底给Test什么东西,多大的空间。
由于Test的声明里有float first,main还是能认出来。
所以main在这里把1.0降成1.0f入栈,first是一个的的确确的4bytes的float。
而此后,main实在无力知晓了,只能把double入栈,这也非main的错。

还有一个问题,就是addplus提出来的。
无论是float还是double,入栈的均为8bytes的double。
我试了一下还真的有这回事,再试了一下printf("%f %f", 1.0f, 2.0);
真能正确的打印出来。
看来这里还有名堂,这涉及编译器对类型的做法,只能在写不定参数函数的时候自己多试试
了。



宏是简单的,简单到会出错;
指针是复杂的,复杂到会出错。
这就是C

文章至此,应该结束了。
Wish you enjoy!
不知什么时候能再对宏进行分析。套用一句经典台词结束本文
I will be back.





【 在 morningcs (BG5HLI) 的大作中提到: 】
: 有没有人知道在函数内部,对那些可变的参数应该怎么引用阿?
: 比如说有这样的函数原型
: void f(int i, ... )
: 在f的内部,怎么样引用后面的参数?
: 【 在 vbvan (van) 的大作中提到: 】
: : printf的不是默认参数,而是可变参数~


--
人若向耶和华许愿或起誓,要约束自己,就不可食言,必要按口中所出的一切话行。

——民数记 三十章 二节

※ 来源:·飘渺水云间 https://www.360docs.net/doc/bb16197036.html,·[FROM: IceVolcano]

相关文档
最新文档