======= 源码 =========
static int foo(char i,...)
{
va_list var;
double d ;
va_start(var, i);
printf("var=%p\n", var);
d = va_arg(var, double);
va_end( var );
printf("var=%p\n", var);
}
======= 源码 =========
======= 预处理后的 =======
static int foo(char i,...)
{
va_list var;
double d ;
( var = (va_list)( &(i) ) + ( (sizeof(i) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) );
printf("var=%p\n", var);
d = ( *(double *)((var += ( (sizeof(double) + sizeof(int) - 1) & ~(sizeof(int) - 1) )) - ( (sizeof(double) + sizeof(int) - 1) & ~(sizeof(int) - 1) )) );
( var = (va_list)0 );
printf("var=%p\n", var);
}
======= 预处理后的 =======
va_start 的宏定义比较复杂, 但也必需复杂, (sizeof(i)+sizeof(int)-1) & ~(sizeof(int)-1)
这整个式子的目的是求出的字节必需是 sizeof(int)的整数倍, 但又可能大于
sizeof(int). 保证整数倍的部分是 & ~(sizeof(int)-1) 操作. 确保变量本身的大小小于
sizeof(int)时不会得到0的是+sizeof(int), 确保变量本身大小等于 sizeof(int)时不至
于得到一个大于sizeof(int)的是 sizeof(i)-1. 整个式子的得来不是那么直观.
考虑一般平台上被压线的参数类型, 典型的大小可以是1, 2, 4, 8. 这个宏必需对所有这
些可能的大小都能正常工作.
另外这个宏定义假定了函数的第一个参数与其第二个参数在内存布局上的关系, 第二个参
数的地址必需大于第一个参数. 也就是说压栈顺序是从右至左的. 所以带... 可变元参数
的函数的调用协议也被限制. 清栈的工作必需由调用者进行. 因为编译器无法静态地得知
几个参数以及什么类型的参数被压栈, 所以也就无法在函数尾部提供一段万能的指令来清
理堆栈.
第二个va_arg的宏更复杂一些, 不过跟第一个已经十分接近了.
va_start 这个宏之后, 此时var变量实质上是一个指针, 指向由...所指代的自左边数的第
一个参数.
在你心知肚明压过来的参数是什么类型时, 比如知道是 double类型, 可以这样:
printf("%f = %d\n", *(double*) var );
va_end 也许只是为了概念上的完整性. 它只是把var简单地置0. 知道这套机理后可以完全
心无挂碍地把它省略. 而不会带来有始(va_start)无终(va_end)的惴惴不安.
======= 汇编码 =======
; 9 : {
00020 55 push ebp
00021 8b ec mov ebp, esp
00023 83 ec 0c sub esp, 12 ; 0000000cH
; 10 : va_list var;
; 11 : double d ;
; 12 : va_start(var, i);
00026 8d 45 0c lea eax, DWORD PTR _i$[ebp+4]
00029 89 45 f4 mov DWORD PTR _var$[ebp], eax
; 13 : printf("var=%p\n", var);
0002c 8b 4d f4 mov ecx, DWORD PTR _var$[ebp]
0002f 51 push ecx
00030 68 00 00 00 00 push OFFSET FLAT:$SG1053
00035 e8 00 00 00 00 call _printf
0003a 83 c4 08 add esp, 8
; 14 :
; 15 : d = va_arg(var, double);
0003d 8b 55 f4 mov edx, DWORD PTR _var$[ebp]
00040 83 c2 08 add edx, 8
00043 89 55 f4 mov DWORD PTR _var$[ebp], edx
00046 8b 45 f4 mov eax, DWORD PTR _var$[ebp]
00049 dd 40 f8 fld QWORD PTR [eax-8]
0004c dd 5d f8 fstp QWORD PTR _d$[ebp]
; 16 : va_end( var );
0004f c7 45 f4 00 00
00 00 mov DWORD PTR _var$[ebp], 0
; 17 : printf("var=%p\n", var);
00056 8b 4d f4 mov ecx, DWORD PTR _var$[ebp]
00059 51 push ecx
0005a 68 00 00 00 00 push OFFSET FLAT:$SG1062
0005f e8 00 00 00 00 call _printf
00064 83 c4 08 add esp, 8
; 18 : }
======= 汇编码 =======
涉及到了几个浮点数操作的汇编指令
fld:
把一个浮点数加载到FPU(float process unit)的栈顶(跟CPU的栈不一样), 也即ST(0)的位置.
fstp:
把FPU栈顶的浮点数弹出来, 放到指定的内存地址去. 总是会占用8个字节, 哪怕它只是
想弹出一个float
象下面这样的汇编指令
lea eax, DWORD PTR _i$[ebp+4]
意思是装入 变量i的地址再加4, 这个4就是前面那个宏 va_start后面的那一大堆sizeof算出来的.
这样, eax寄存器里放的就是变量i右边的那个元素的地址.
那么这是否意味要着用...还必需得有一个确定类型的参数作为前导? 不一定. 但却不大实
用. 考虑printf的情况, 第一个参数的const char *是为了对后面的...参数作出指示,
printf的内部实现也正是根据里面的conversion specifier如d, f来决定 va_arg的第二个
参数写上什么类型.
//假设该函数接受数目不定的整数, 最后一个是-1,
void foo(...)
{
volatile int dummy;
register int * p = &dummy + 2;
//+2的讲究: 假设生成的汇编码有一个典型的栈帧. 即bp-4 指向
//第一个局部变量dummy, [bp] 指向BP寄存器本身的值. [BP+4]则指向函数自左至右的
//第一个参数. 显然, 这种写法对语言标准不予保证的实现细节做了太多假设. 不可移植
//依赖性太强.
while( *p != -1)
{
printf("args : %d\n", *p);
p += i;
}
}
int main()
{
foo(1,2,3);
foo(101, 102, 103);
return 0;
}
这是我假想中的可执行的代码, 只要能严格保证-1是最后一个参数, 不过它却是ANSI C禁
止的做法. 也就是说...前面必需至少有一个类型确定的参数.
有一个因...随带出的影响是 默认参数类型提升, 这个提升规则跟float相关的一条是它会
被转换成double, 这也是printf 的所有 conversion specifier中最容易误导的一个:%f,
它不是指定一个float类型, 而是double. 跟它不对称的是scanf, 它的conversion
specifier中的%f 是指float, %lf是指double.
也就是说
void foo1(float f);
void foo2(float f, ...);
编译器在生成下面两条语句时压栈时作的处理不同:
foo1( 3.14f );
foo2(3.14f, 3.14f );
对于foo1, ANSI C现在规定(老式的C则是一律把float在运算前提升至double) float这时
不作提升了. 所以编译器把 3.14f 这个float的存储表示弄成一个常数, 加到寄存器里,
直接push到堆栈, 下一条指令就是call _foo1.
而对于foo2, 虽然用尾辍的f指定了3.14是一个float类型, 但它必需被提升至double, 就
是因为它对应的是原型中...的部分. 所以仅仅对于3.14f这一个参数, 就有8个字节的数据
被放到堆栈上. 而下一个要压栈参数同样是3.14f, 它却被作为一个4字节的对象, 一个真
正的float被压入堆栈.
类似地, 在用 va_arg作处理时, 应该避免出现宏的第二个参数是float, 那几乎一定兆示
着一个错误.
这一点C语言的规定我猜想很可能来自printf的格式符带来的副作用. 就因为它永远是把
float当作double来压栈, 所以后来的C标准要跟老的代码兼容, 才保留了这一规定.
关于调用协议的影响, 显然, 声明了...原型的函数只能是由调用者来维护堆栈, 但VC编译
器却并不对下面的定义语句产生警告:
static int __stdcall foo2(float f, ...)
它只是在生成汇编代码的时候实际上仍然按__cdecl处理罢了.
__cdecl 的作用:(1)维护堆栈由调用者负责(2)压栈顺序是自右至左(3)符号名字前面加一
个_
__stdcall的作用: (1)维护堆栈由被调用函数负责(2)压栈顺序仍然是自右至左. (3)关于
符号名字是前面加个_, 后面加上@符号, 然后跟一个表示函数参数占用字节大小的数字.
关于...左边紧挨着的参数的存储类型.
static int foo2(register int i, register float f, ...)
这里参数i前面指定 register是可以的. 但是f不行, 原因在于 va_start宏要取f的地址.
对于VC编译器, 它也只是在展开va_start宏的时候才报告这个错误, 其实在检查函数的原
型时就有足够的信息来生成警告了.
阅读(1106) | 评论(0) | 转发(0) |