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

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

asmlinkage int printk(const char *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.
  */


#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_arg(ap, T) (*(*)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap) (void) 0
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

#endif                /* va_arg */

1、va_list
    va_list表示可变参数列表类型,实际上就是一个char指针
2、va_start
    va_start 用于获取函数参数列表中可变参数的首指针(获取函数可变参数列表)
  * 输出参数ap(类型为 va_list): 用于保存函数参数列表中可变参数的首指针(即,可变参数列表)
  * 输入参数A: 为函数参数列表中最后一个固定参数
3、va_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_list、va_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 (< 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(const char *fmt, ...)
{
    char str[100];
    unsigned int 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(const char *); 
void printhex(unsigned long value, int nbytes); 
void printhex2(unsigned char value); 
void printhex4(unsigned short value); 
void printhex8(unsigned long value);

    这些函数都是用汇编实现的,并直接从低层操作s3c2410的串口,并显示信息。因此不需要等到console_init后就可以显示信息。
    为了使用这些函数,需要特性的内核选项支持:make menuconfig 
      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里:

extern void printascii(const char *);

static void debug(const char *fmt, ...)
{
    va_list va;                
    char buff[256];

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

    vsprintf(buff, fmt, va);
    va_end(va);

    printascii(buff);
}



示意图:


C语言函数之可变参数原理:va_start、va_arg及va_end_第1张图片

你可能感兴趣的:(C语言函数之可变参数原理:va_start、va_arg及va_end)