关于参数处理那点事,C标准库反汇编解析
1 stdarg.h 内容概览
这个头文件用于提供访问无名参数(既没有命名也没有类型)的类型和宏。
假设函数形如:
void functionWithMltipleInput(normalType n, ...)
第一个参数名为n,后续省略号表示有不定量个类型未知的参数。
为了在函数中处理这些参数,我们需要一组方法来获取无名参数的值。
stdarg.h提供了1个类型和4个函数式的宏来满足上述需求。可以说这个头文件相当简单了。
- va_list 变量参数列表(指针),用于存放参数的信息
- va_start 初始化变量参数列表的宏
- va_arg 用于获取下一个参数的宏
- va_end 用于表明结束使用va_list的宏
- va_copy 复制变量参数列表
va_start()
void va_start (va_list ap, paramN);
初始化变量ap, 使其指向第一个无名参数
- ap 未初始化的va_list,实质上是一个char*类型的指针。
- paramN 最后一个显示参数(named parameter)的变量名
va_arg()
type va_arg (va_list ap, type)
将p指向的参数解析为type类型,同时修改ap,使其指向参数列表中的下一个参数。
也就是说,初始化ap后连续两次调用va_arg,第一次返回第一个无名参数的值,第二次返回的就是第二个无名参数的值了。
va_end()
void va_end (va_list ap);
要求只要使用了va_start,就必须在退出函数之前调用va_end,不知道为啥,不清楚有啥作用。
va_copy()
没啥用。
example
下面的例程将首先用
va_start(vl, argn);
初始化参数列表指针vl,而后用
val = va_arg(vl,double);
获取下一个参数并将其视为double赋值给val \
src
#include <stdarg.h>
#include <stdio.h>
typedef int normalType;
void minput(normalType argn, ...)
{int i;int val;printf ("Printing unnamed arguments:");va_list vl;va_start(vl, argn);for (i=0;i<argn;i++){val=va_arg(vl,int);printf(" [%2d]",val);}va_end(vl);printf("\n");
}int main()
{minput(3, 1, 2, 3);
}
output
Printing unnamed arguments: [ 1] [ 2] [ 3]
如下可以看到打印出无名参数的值与调用输入一致。
2 用法反汇编分析
处理
编译
D:\TempWorkSpace\c> gcc .\args.c -o arg.exe
运行
D:\TempWorkSpace\c> .\arg.exe
Printing unnamed arguments: [ 1] [ 2] [ 3]
反汇编
D:\TempWorkSpace\c> objdump -D .\arg.exe > arg.asm
分析
下图可见0x4015c3为main函数的起始地址
00000000004015c3 <main>
运行到0x4015e6处调用函数minput(),跳转到地址0x401550,可见传参3,2,1,3与调用输入一致
callq 401550 <[minput]>
而后在0x4015f5处退出主函数
4015f6 90 retq
下图可见函数minput()的反汇编结果,函数起始地址为0x401550
图中右侧蓝色高亮部分对应图中左侧函数体中的for循环,可见遍历变量参数列表过程,总共循环三次
理解
传参意味着将参数值压入栈中,访问参数也就是按参数偏移取值。
va_list就是一个用于取值的指针,使用前用va_start(AP, LASTARG)找到第一个无名变量的基地址,也就是va_list的初始化。
va_arg(AP, TYPE)的作用就是把对应的值设定类型解析出来,操作上类似于*(type*)<va_list>(把va_list当成type类型的指针解引用)。
va_end(va_list)就有点像是注脚了,表明完事了,基本没啥用处,而va_copy()就属于锦上添花,意义不明了。
3 回到标准库源码
标准库通常是宏名下划线预处理乱飞,为了聚焦重点,这里把各种乱七八糟的东西都剔掉了。
#ifndef _STDARG_H
#define _STDARG_H#ifdef __GNUC__
/* The GNU C-compiler uses its own, but similar varargs mechanism. */
typedef char *va_list;
/* Amount of space required in an argument list for an arg of type TYPE.
TYPE may alternatively be an expression whose type is used. */
#define __va_rounded_size(TYPE)\(((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))
#if __GNUC__ < 2
#else /* __GNUC__ >= */
#ifndef __sparc__
#define va_start(AP, LASTARG)\(AP = ((char *) __builtin_next_arg ()))
#else
#endif
void va_end (va_list); /* Defined in libgcc.a */
#define va_end(AP)
#define va_arg(AP, TYPE)\(AP = ((char *) (AP)) += __va_rounded_size (TYPE),\*((TYPE *) ((char *) (AP) - __va_rounded_size (TYPE))))
#endif /* __GNUC__ >= */
#else /* not __GNUC__ */
#endif /* __GNUC__ */
#endif /* _STDARG_H */
可以看到va_start(AP, LASTARG)的实现就是返回一个char*类型的指针,通过__builtin_next_arg获取最后一个显式参数的下一个参数占据的地址。
#define va_start(AP, LASTARG) (AP = ((char *) __builtin_next_arg ()))
va_arg(AP, TYPE)则包含两个操作,修改AP的值
((char *) (AP)) += __va_rounded_size (TYPE)
返回AP当前指向的参数
*((TYPE *) ((char *) (AP) - __va_rounded_size (TYPE)))
拼在一起
#define va_arg(AP, TYPE) \(AP = ((char *) (AP)) += __va_rounded_size (TYPE), \*((TYPE *) ((char *) (AP) - __va_rounded_size (TYPE))))
va_end(AP)则真的啥也没干,挺好,不用分析了
#define va_end(AP)
4 参考
- GNU doc, Varargs.html
- Learn.microsoft, c-runtime-library