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

说到C语言函数可变参数,我们最先想到的可能就是printf、scanf、printk了。在Linux-2.6.24.7内核源码里,printk函数原型如下:

asmlinkage int printk(constchar*fmt,...)

asmlinkage表示通过堆栈传递参数。gcc编译器在汇编过程中调用c语言函数时传递参数有两种方法:一种是通过堆栈,另一种是通过寄存器。缺省时采用寄存器,假如你要在你的汇编过程中调用c语言函数,并且想通过堆栈传递参数,你定义的c函数时要在函数前加上宏asmlinkage。

从printk函数原型可知,printk除了接收一个固定参数fmt外,后面的参数用...表示。在C/C++语言中,...表示可以接收定数量的参数(0或0以上个参数)。那么printk到底是怎么支持可变参数的呢?

我们先来看几个宏:va_list、va_start、va_arg及va_end(va的意思应该是variable),Linux-2.6.24.7内核源码里,其定义(内核里的定义与C语言库的定义是类似的)如下

/*
* Use local definitions of C library macros and functions
* NOTE: The function implementations may not be as efficient
* as an inline or assembly code implementation provided by a
* native C library.
*/

#ifndefva_arg

#ifndef _VALIST
#define _VALIST
typedefchar*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)))
#defineva_arg(ap, T)(*(T *)(((ap)+=(_bnd (T, _AUPBND)))-(_bnd (T,_ADNBND))))
#defineva_end(ap)(void) 0
#defineva_start(ap, A)(void)((ap)=(((char*)&(A))+(_bnd (A,_AUPBND))))

#endif/* va_arg */

1va_list

va_list表示可变参数列表类型,实际上就是一个char指针

2va_start

va_start用于获取函数参数列表中可变参数的首指针(获取函数可变参数列表)
* 输出参数ap(类型为va_list): 用于保存函数参数列表中可变参数的首指针(即,可变参数列表)
* 输入参数A: 为函数参数列表中最后一个固定参数

3va_arg

va_arg用于获取当前ap所指的可变参数并将并将ap指针移向下一可变参数
*
输入参数ap(类型为va_list): 可变参数列表,指向当前正要处理的可变参数
*
输入参数T: 正要处理的可变参数的类型
* 返回值: 当前可变参数的值

在C/C++中,默认调用方式_cdecl是由调用者管理参数入栈操作,且入栈顺序为从右至左,入栈方向为从高地址到低地址。因此,第1个到第n个参数被放在地址递增的堆栈里。所以,函数参数列表中最后一个固定参数的地址加上第一个可变参数对其的偏移量就是函数的可变参数列表了(va_start的实现);当前可变参数的地址加上下一可变参数对其的偏移量的就是下一可变参数的地址了(va_arg的实现)。这里提到的偏移量并不一定等于参数所占的字节数,而是为参数所占的字节数再扩展为机器字长(acpi_native_int)倍数后所占的字节数(因为入栈操作针对的是一个机器字),这也就是为什么_bnd那么定义的原因。

4、va_end

va_end用于结束对可变参数的处理。实际上,va_end被定义为空.它只是为实现与va_start配对(实现代码对称和"代码自注释"功能)

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

1、用va_list定义一个可变参数列表

2、用va_start获取函数可变参数列表

3、用va_arg循环处理可变参数列表中的各个可变参数

4、用va_end结束对可变参数列表的处理

下面是一个例子:

#include<stdio.h>
#include<stdarg.h> /* 使用va_listva_start等必须包含的头文件 */
#include<string.h>

/* linux C没有itoa函数,所以要自己写 */
char*itoa(int i,char*str)
{
int mod,div=fabs(i), index = 0;
char*start,*end, temp;

do
{
mod =div% 10;
str[index++]='0'+ mod;
div=div/ 10;
}while(div!= 0);

if(i < 0)
str[index++]='-';

str[index]='\0';

for(start = str, end = str +strlen(str)- 1;
start < end; start++, end--)
{
temp =*start;
*start =*end;
*end = temp;
}

return str;
}

void print(constchar*fmt,...)
{
char str[100];
unsignedint len, i, index;
int iTemp;
char*strTemp;
va_list args;

va_start(args, fmt);
len =strlen(fmt);
for(i=0, index=0; i<len; i++)
{
if(fmt[i]!='%')/* 非格式化参数 */
{
str[index++]= fmt[i];
}
else/* 格式化参数 */
{
switch(fmt[i+1])
{
case'd':/* 整型 */
case'D':
iTemp =va_arg(args,int);
strTemp = itoa(iTemp, str+index);
index +=strlen(strTemp);
i++;
break;
case's':/* 字符串 */
case'S':
strTemp =va_arg(args,char*);
strcpy(str + index, strTemp);
index +=strlen(strTemp);
i++;
break;
default:
str[index++]= fmt[i];
}
}
}
str[index]='\0';
va_end(args);

printf(str);
}

int main()
{
print("Version: %d; Modifier: %s\n",-958,"lingd");
return 0;
}

另一个比较实用的例子:printk只能在内核初始化完控制台(console_init)后才能使用。因此,在Linux内核初始化控制台前,只能使用其它函数来打印内核消息。这些函数有:

void printascii(constchar*);
void printhex(unsignedlong value,int nbytes);
void printhex2(unsignedchar value);
void printhex4(unsignedshort value);
void printhex8(unsignedlong value);

这些函数都是用汇编实现的,并直接从低层操作s3c2410的串口,并显示信息。因此不需要等到console_init后就可以显示信息。
为了使用这些函数,需要特性的内核选项支持:makemenuconfig
Kernel hack ->
[*]Kernel low-level debugging functions
[*]Kernel low-level debugging messages via S3C UART
(0)S3C UART to use for low-level debug

但是,这些函数并没有printk功能那么强大,支持可变参数,支持格式化字符串。为了Linux内核初始化控制台前,能使用了一个类似于printk的函数,我们将printascii函数封装到新函数debug里:

externvoid printascii(constchar*);

staticvoid debug(constchar*fmt,...)
{
va_list va;
char buff[256];

va_start(va, fmt);

/* 函数vsprintf:用于输出格式化字符串到缓冲区
* 输出参数buff:用于保存结果的缓冲区
* 输入参数fmt: 格式化字符串
* 输入参数va: 可变参数列表
* 返回值: 实际输出到缓冲区的字符数
*/
vsprintf(buff, fmt, va);
va_end(va);

printascii(buff);
}

 


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值