stdarg(3) variable argument lists 可变参数列表

这篇博客详细介绍了C语言中使用`stdarg.h`库进行可变参数列表的实现原理和操作。从函数调用的栈结构开始,解释如何通过`va_list`、`va_start`、`va_arg`和`va_end`宏来处理可变参数。文章讨论了如何确定可变参数的地址,并提供了例子说明如何安全地遍历和获取不同类型的可变参数。此外,还提到了Linux内核中的相关宏定义和宏的详细使用说明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 可变参数列表的实现

GCC 编译器在汇编过程中,调用 C 语言函数传递参数有两种方法:

  1. 通过堆栈
  2. 通过寄存器(默认)

若想通过堆栈传递参数,需在定义 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. 宏的简要处理过程

对可变参数列表的处理过程一般为:

  1. 用 va_list 定义一个可变参数列表
  2. 用 va_start 获取函数可变参数列表
  3. 用 va_arg 循环处理可变参数列表中的各个可变参数
  4. 用 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() 可以是宏或者函数。

10. 参考

C语言函数之可变参数原理:va_start、va_arg及va_end

va_start va_arg va_end 的原理与实例

printf,sprintf,vsprintf 区别

关于va_list的_INTSIZEOF(n)的疑问

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值