1. 可变参数列表的实现
GCC 编译器在汇编过程中,调用 C 语言函数传递参数有两种方法:
- 通过堆栈
- 通过寄存器(默认)
若想通过堆栈传递参数,需在定义 C 函数时在函数前加上宏 asmlinkage
asmlinkage int printk(const char *fmt, ...)
正常来讲,函数原型中具有确定的参数类型和数量,保证了函数调用的准确性。
如果在调用函数时,使用不同类型的不同数量的参数进行调用,参数列表的数量和类型对于被调用函数是未知的。
我们就要想办法确定各个可变参数的类型,找到这些可变参数的地址。
头文件 <stdarg.h>
定义了 va_list 类型和用于逐个通过参数列表的三个宏。
被调用函数必须声明一个 va_list 类型的对象,用 va_list 这个指针指向这个可变参数列表的各个参数。
这个 va_list 对象由 va_start(), va_arg() 和 va_end() 使用。
2. 函数调用时的栈结构
C 函数调用时的栈结构:
栈结构 | 说明 |
---|---|
栈底 | 高地址(入栈方向为从高地址到低地址) |
…… | …… |
函数返回地址 | …… |
…… | …… |
函数最后一个可变参数 | 入栈顺序为从右向左 |
…… | …… |
函数第一个可变参数 | 调用 va_start 后 ap 指向这里 |
函数最后一个固定参数 | …… |
…… | …… |
函数第一个固定参数 | …… |
栈顶 | 低地址 |
3. 可变参数列表(第一个可变参数)的地址
我们需要一个基准,这个基准就是可变参数前面固定的那些参数中的最后一个,也即可变参数前面那个参数。
然后以此已知类型的参数为基准,来确定后面的可变参数。
先确定第一个可变参数的地址,方法是调用void va_start(va_list ap, last);
这样,基准有了,就是 last 指定的参数,然后由 last 计算 指向当前可变参数的指针 ap
要找到 ap 指向的第一个可变参数的地址,则依据栈结构知道,第一个可变参数的地址等于最后一个固定参数的地址加上偏移
此时,ap 指向可变参数列表中的第一个参数
4. 可变参数列表中随后的可变参数的获取
我们需要指定参数的类型,取得指向指定类型的指针,来获取各个可变参数
方法是调用va_arg(va_list ap, type);
宏,
先得到当前参数地址,返回强制转换成指向此参数的类型的指针,以后间接访问即可。
最后,用va_end(ap),初始化 ap 可变参数指针,保持健壮性。
5. 宏的简要处理过程
对可变参数列表的处理过程一般为:
- 用 va_list 定义一个可变参数列表
- 用 va_start 获取函数可变参数列表
- 用 va_arg 循环处理可变参数列表中的各个可变参数
- 用 va_end 结束对可变参数列表的处理
6. 各个宏调用的语法
#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);
7. Linux 内核中的宏定义
#ifndef va_arg
#ifndef _VALIST
#define _VALIST
typedef char *va_list;
#endif /* _VALIST */
/*
* Storage alignment properties
*/
#define _AUPBND (sizeof (acpi_native_int) - 1)
#define _ADNBND (sizeof (acpi_native_int) - 1)
/*
* Variable argument list macro definitions
*/
#define _bnd(X, bnd) ( ((sizeof (X)) + (bnd)) & (~(bnd)) )
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
#define va_arg(ap, T) (*(T *) (((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap) (void) 0
#endif /* va_arg */
8. 宏定义的另一种表达方式
#define _ADDRESSOF(v) ( &(v) )
/* 按 int 的倍数进行字节对齐 */
#define _INTSIZEOF(n) (( sizeof(n) + sizeof(int) - 1 ) & ~(sizeof(int) - 1))
#define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)( (ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
9. 宏的详细说明
1. va_list
va_list表示可变参数列表类型,实际上就是一个 char 指针
2. va_start( va_list ap, last )
va_start 用于获取函数参数列表中可变参数的首指针,即获取函数可变参数列表
参数 | 作用 |
---|---|
va_list ap | 保存函数参数列表中可变参数的首指针(即,可变参数列表 |
char *last | 函数参数列表中可变参数列表前最后一个固定参数的名字,也即,调用函数的知道其类型的最后一个参数 |
va_start()宏初始化 ap 供 va_arg() 和 va_end() 随后使用,并且 va_start() 必须首先被调用
由于此参数的地址可能在 va_start 宏中使用,它不应被声明为寄存器变量或者作为一个函数或者数组类型。
3. va_arg( va_list ap, type )
va_arg 用于获取当前 ap 所指的可变参数并将并将ap指针移向下一可变参数
参数 | 作用 |
---|---|
va_list ap | 指向当前正要处理的可变参数 |
type | 正要处理的可变参数的类型 |
返回值 | 当前可变参数的值 |
在C/C++中,默认调用方式_cdecl是由调用者管理参数入栈操作,且
入栈顺序为从右至左
入栈方向为从高地址到低地址
因此,第1个参数到第n个参数被存放在地址递增的堆栈里。
函数的可变参数列表的地址 = 函数参数列表中最后一个固定参数的地址 + 第一个可变参数对其的偏移量(va_start的实现);
下一可变参数的地址 = 当前可变参数的地址 + 下一可变参数对其的偏移量(va_arg的实现)。
这里提到的偏移量并不一定等于参数所占的字节数,
而是为参数所占的字节数再扩展为机器字长(acpi_native_int)倍数后所占的字节数(因为入栈操作针对的是一个机器字),
这也就是为什么_bnd那么定义的原因。
va_arg() 宏展开为一个表达式,这个表达式具有调用中的下一个参数的类型和值。
这里的 ap 参数即是由 va_start() 初始化过的 va_list ap.
每次对 va_arg() 的调用都修改 ap 参数,以便下次调用能够返回下个参数。
参数 type 即是指定的类型名,这样,指向特定类型对象的指针的类型可以简单的通过在类型后添加 * 操作符得到。
va_start() 宏使用后的 va_arg() 宏的第一次使用返回 last 后面的参数
连续的调用返回剩余参数的值。
如果没有下一个参数,或者如果类型和实际的下一个参数的类型不匹配(按照默认参数提升进行的提升),则会产生随机错误。
如果 ap 被传递给使用 va_arg(ap, type) 的函数,那么在这个函数返回后,ap 的值是未定义的。
4. va_end( va_list ap )
va_end 用于结束对可变参数的处理。实际上,va_end被定义为空.它只是为实现与va_start配对(实现代码对称和”代码自注释”功能)
每个 va_start() 调用必须匹配同一个函数中的 va_end() 的相关调用。
调用 va_end() 后,变量 ap 是未定义的。
列表的多重遍历是可能的,每个列表用 va_start() 和 va_end() 括起来。
va_end() 可以是宏或者函数。