C语言:可变参数原理与printf的实现

编译器:GCC 4.6.3
运行环境:Ubuntu12.04
处理器:Intel i386(32位)

一、可变参数实现原理

约定:

  • 1、调用函数将被调用函数参数入栈,入栈顺序由调用约定规定,包括cdeclstdcallfastcallnaked call等,c编译器默认使用cdecl约定,参数从右往左入栈

  • 2、调用函数时,当实参为字符变量(1字节)则占用4字节入栈【估计是内存对齐考虑】,实参为单精度浮点型float(4字节)则扩展成8字节的双精度类型入栈【可变参数编译时适用:参考第五部分】;(这大概就是为什么__vasz(x)的宏这么定义,由1变4)

  • 3、实参、EIP、栈底寄存器的顺序依次入栈。

  • 4、可变参数,前面至少有一个确定的形参,比如void func( int a, ...)

#include <stdio.h>

int add(int x, int y)
{
	int val1 = x;
	int val2 = y;
	return (val1+val2); //其实可以直接return (x+y); 只是为了表现一下栈的通用性
}

int main(int argc, const char *argv[])
{
	int a = 1;
	int b = 2;
	int c = 0;
	c = add(a, b);
	printf("%d\n", c);
	return 0;
}

上述代码的栈分布图:
在这里插入图片描述
不同函数的栈空间有一定的差异,有些函数内没有定义局部变量,因此栈上也就不存在这一部分空间,但是对于函数调用而言,实参+EIP+EBP(红色框框部分)的内容是固定的。

因此,对于可变参数,只需要知道当前函数的栈底地址,往上偏移8个字节,就可以知道第一个参数的起始地址,按照第一个参数的数据类型,然后偏移第一个参数数据类型的大小,就可以找到第二个参数的起始地址,依次进行偏移就可以获取全部的参数。这里需要告诉可变参数函数的两个重要部分:有多少个参数,每个参数是什么类型。

对于 printf 函数,一般是依据%的个数确定,传递了多少参数,按%ld/%f/%c等中的ld/f/c来确定参数类型。

二、可变参数中所需要的宏定义

typedef char *va_list;

#define __vasz(x)	((sizeof(x)+sizeof(int)-1) & ~(sizeof(int) -1))    //char 当 4 字节算

#define va_start(ap, parmN)   {asm("lea 0x8(%%ebp), %%edx\n\t"\
		"mov %%edx,  %%eax\n\t"\
		:"=a"(ap));\
		ap += __vasz(parmN);} // 这里加上parmN为的是获取第二个参数地址

#define va_arg(ap, type)  (*((type *)((va_list)((ap) = (void *)((va_list)(ap) + __vasz(type))) - __vasz(type))))
//假设当前要访问的参数占用 4, ap指向当前访问参数的地址,则该宏等价于:
//ap = ap + 4; //ap指向下一个宏
//*(ap - 4); //此时ap已经指向下一个参数了,所以减去4,注意这里减完没有赋值给ap,所以ap还是指向下一个参数的地址
   
#define va_end(ap)

本代码参考了其它思想,但是不知道是编译器的不同还是什么,发现编译后的结果和想象的中的不同,后来发现是 va_start 这个函数问题。
原先的 va_start 宏定义:#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
本文选用的编译器,对于可变参数函数的栈帧中只分配了确定了的形参,对于不确定的形参并没有分配栈空间,因此通过第一个形参的指针加4是无法找到第二个形参的指针的。

修改的思路: 从上一节中,知道了 ebp 寄存器往高地址方向偏移 8 个字节,即为调用者传参的左边第一个参数的起始地址,再加上偏移第一个参数数据类型大小为左边第二个参数的起始地址,依据此基本原理,采用内联汇编的方式,修改宏定义。

注意: 修改的宏定义是在Ubuntu环境下GCC4.6.3版本下分析后修改的宏定义,在高版本的宏定义中,stdio头文件中定义了va_list,会出现重复定义的错误;并且在Windows环境下是不适用的,所以这个宏定义以学习为主。使用的话还是使用stdarg标准库!!!

参考:亲密接触C可变参数函数 ?

扩展阅读:揭密X86架构C可变参数函数实现原理 ?

三、可变参数实现(上一节定义的宏 & stdarg)

3.1、基于上一节定义的宏的可变参数实现

#include <stdio.h>
/*没有包含stdarg头文件*/

typedef char *va_list;

#define __vasz(x)	((sizeof(x)+sizeof(int)-1) & ~(sizeof(int) -1))

#define va_start(ap, parmN)   {asm("lea 0x8(%%ebp), %%edx\n\t"\
		"mov %%edx,  %%eax\n\t"\
		:"=a"(ap));\
		ap += __vasz(parmN);}

#define va_arg(ap, type)  (*((type *)((va_list)((ap) = (void *)((va_list)(ap) + __vasz(type))) - __vasz(type))))
   
#define va_end(ap)


double average(int num,...)
{
 
    va_list valist;
    double sum = 0.0;
    int i;
 
    /* 为 num 个参数初始化 valist */
    va_start(valist, num);
 
    /* 访问所有赋给 valist 的参数 */
    for (i = 0; i < num; i++)
    {
       sum += va_arg(valist, int);
    }
    /* 清理为 valist 保留的内存 */
    va_end(valist);
 
    return sum/num;
}
 
int main()
{
   printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
   printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}


/*
Windows环境下运行错误,Linux环境下正常
运行结果为:
Average of 2, 3, 4, 5 = 3.500000
Average of 5, 10, 15 = 10.000000
*/

3.2、基于stdarg库的可变参数实现

#include <stdio.h>
#include <stdarg.h>
 
double average(int num,...)
{
 
    va_list valist;
    double sum = 0.0;
    int i;
 
    /* 为 num 个参数初始化 valist */
    va_start(valist, num);
 
    /* 访问所有赋给 valist 的参数 */
    for (i = 0; i < num; i++)
    {
       sum += va_arg(valist, int);
    }
    /* 清理为 valist 保留的内存 */
    va_end(valist);
 
    return sum/num;
}
 
int main()
{
   printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
   printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}


/*
运行结果为:
Average of 2, 3, 4, 5 = 3.500000
Average of 5, 10, 15 = 10.000000
*/

代码参考:C 可变参数 ?

四、printf实现(部分功能)

大概思路: 格式化输出中第一个参数(即字符串)中未知的数据(%号作为标记),通过查找之后几个参数对应的数据,将其转换成待显示格式,按照顺序添加到第一个参数(即字符串)中对应的%位置处,填充完成后,通过屏幕打印函数,将字符串完整的打印出来。

(这个函数没有实现%f)

#include <stdio.h>

#define INT_MAX       2147483647
#define isdigit(c)    ((unsigned) ((c) - '0') <  (unsigned) 10)

typedef char *va_list;

#define __vasz(x)		((sizeof(x)+sizeof(int)-1) & ~(sizeof(int) -1))
#define va_start(ap, parmN)   {asm("lea 0x8(%%ebp), %%edx\n\t"\
		"mov %%edx,  %%eax\n\t"\
		:"=a"(ap));\
		ap += __vasz(parmN);}
#define va_arg(ap, type)  (*((type *)((va_list)((ap) = (void *)((va_list)(ap) + __vasz(type))) - __vasz(type))))
   
#define va_end(ap)

int vsprintf(char *buf, const char *fmt, char *argp){
    int c;
    enum { LEFT, RIGHT } adjust;
    enum { LONG, INT } intsize;
    int fill;
    int width, max, len, base;
    static char X2C_tab[]= "0123456789ABCDEF";
    static char x2c_tab[]= "0123456789abcdef";
    char *x2c;
    char *p;
    long i;
    unsigned long u;
    char temp[8 * sizeof(long) / 3 + 2];
    int buf_len = 0;

    /* 只要还没有访问到字符串的结束符0,就继续 */
    while ((c = *fmt++) != 0) {
        if(c != '%'){
            /* 普通字符,将其回显 */
            *buf = c;
            buf++;
            buf_len++;
            continue;
        }

        /* 格式说明符,格式为:
         * %[adjust] [fill] [width] [.max]keys
         * [adjust] 有-表示左对齐输出,如省略表示右对齐输出
         * [fill] 有0表示指定空位填0,如省略表示指定空位不填
         * [width] 指域宽,即对应的输出项在输出设备上所占的字符数
         * [.max]
         */
        c = *fmt++;  //获取%后的符号

        adjust = RIGHT;
        if (c == '-') {
            adjust= LEFT;
            c= *fmt++;
        }

        fill = ' ';
        if (c == '0') {
            fill= '0';
            c= *fmt++;
        }

        width = 0;
        if (c == '*') {
            /* 宽度被指定为参数,例如 %*d。 */
            width = (int) va_arg(argp, int);   //等价于width = (int)*(argp + 4); argp += 4
            c= *fmt++;
        } else
        if (isdigit(c)) {
            /* 数字表示宽度,例如 %10d。 */
            do {
                width= width * 10 + (c - '0');
            } while (isdigit(c= *fmt++));      //以%10.2d为例,查找到 . 之前的数值
        }

        max = INT_MAX;
        if (c == '.') {
            /* 就要到最大字段长度了 */
            if ((c = *fmt++) == '*') {
                max = (int) va_arg(argp, int);
                c = *fmt++;
            } else
            if (isdigit(c)) {
                max = 0;
                do {
                    max = max * 10 + (c - '0');
                } while (isdigit(c = *fmt++));
            }
        }

        /* 将一些标志设置为默认值 */
        x2c = x2c_tab;
        i = 0;
        base = 10;
        intsize = INT;
        if (c == 'l' || c == 'L') {
            /* “Long”键,例如 %ld。 */
            intsize = LONG;
            c = *fmt++;
        }
        if (c == 0) break;   // 这条语句的意义在哪里???

        switch (c) {
            /* 十进制 */
            case 'd':
                i = intsize == LONG ? (long)va_arg(argp, long)
                                    : (long) va_arg(argp, int);
                u = i < 0 ? -i : i;
                goto int2ascii;

                /* 八进制 */
            case 'o':
                base= 010; // 对应的十进制是8
                goto getint;

                /* 指针,解释为%X 或 %lX。 */
            case 'p':
                if (sizeof(char *) > sizeof(int)) intsize= LONG;

                /* 十六进制。 %X打印大写字母A-F,而不打印%lx。 */
            case 'X':
                x2c = X2C_tab;
            case 'x':
                base = 0x10;
                goto getint;

                /* 无符号十进制 */
            case 'u':
            getint:
                u = intsize == LONG ? (unsigned long)va_arg(argp, unsigned long)
                                    : (unsigned long)va_arg(argp, unsigned int);
            int2ascii:
                p = temp + sizeof(temp) - 1;
                *p = 0;
                do {
                    *--p= x2c[(int) (u % base)];
                } while ((u /= base) > 0);
                goto string_length;

                /* 一个字符 */
            case 'c':
                p = temp;
                *p = (int)va_arg(argp, int);
                len = 1;
                goto string_print;

                /* 只是一个百分号 */
            case '%':
                p = temp;
                *p = '%';
                len = 1;
                goto string_print;

                /* 一个字符串,其他情况将加入这里。 */
            case 's':
                p = va_arg(argp, char *);

            string_length:
                for (len= 0; p[len] != 0 && len < max; len++) {}

            string_print:
                width -= len;
                if (i < 0) width--;
                if (fill == '0' && i < 0) {      // 如果空位填充为0且为负数,那么无论左对齐还是右对齐,第一位为负号
                    *buf++ = '-';
                    buf_len++;
                }
                if (adjust == RIGHT) {           // 右对齐
                    while (width > 0) {
                        *buf = fill;
                        buf++;
                        buf_len++;
                        width--;
                    }
                }
                if (fill == ' ' && i < 0) *buf++ = '-';
                while (len > 0) {
                    *buf = (unsigned char) *p++;
                    buf++;
                    buf_len++;
                    len--;
                }
                while (width > 0) {              // 左对齐
                    *buf = fill;
                    buf++;
                    buf_len++;
                    width--;
                }
                break;

            /* 无法识别的格式键,将其回显。 */
            default:
                *buf = '%';
                *buf = c;
                /*从两句*buf的赋值,就是只对一个空间进行了赋值,
                *这里buf指针却往后移动了2个单位,由于buf指向的数组空间均为0,
                *所以buf相当于跳过了一个为0的一个字节空间,在后面打印的时候
                *就会出现只打印前面内容的情况。*/
                buf += 2;
                buf_len += 2;
        }
    }

    /* 字符串已经格式化完毕,最后将结尾设置为字符串结束符0 */
    *buf++ = 0;
    return buf_len;
}

int k_printf(const char *fmt, ...)
{
    char *ap;
    int len;
    char buf[256] = {0};
	
	int s = 0x10;
    /* 准备访问可变参数 */
    va_start(ap, fmt);
	
    len = vsprintf(buf, fmt, ap);

    /* 打印出格式化完成的字符串 */
    puts(buf);

    /* 访问结束 */
    va_end(ap); 

    return len;
}

int main()
{
	char a = 1;
	char b = 2;
	char *str = "hello,%d xxx %d\n";
	k_printf(str,a, b);
	return 0;
}

参考:【编写操作系统之路】-可变参数(…) ?

五、补充说明——固定参数与可变参数之间实参入栈的差异

  1. 对于字符型数据,两者之间没有差异,占用4字节(不是数据扩展,只改变低8位)。【示例1】
  2. 整形数据,两者之间没有差异,占用4字节。【示例2】
  3. 单精度数据,可变参数将单精度(4字节)扩展成双精度(8字节)入栈,而固定参数的函数,还是按单精度大小入栈。【示例3】
  4. 双精度数据,两者之间没有差异,占用8字节。【示例4】

在固定参数函数被调函数使用时,按照被调函数中确定的形参数据类型来进行运算,而对于可变参数函数,使用按照数据的指针和使用强制类型来进行运算。

#include <stdio.h>

#define _CHAR   1
#define _INT    0
#define _FLOAT  0
#define _DOUBLE 0


#define _varg 0

#if _CHAR
typedef char TYPE;
#endif

#if _INT
typedef int TYPE;
#endif

#if _FLOAT
typedef float TYPE;
#endif

#if _DOUBLE
typedef double TYPE;
#endif

#if _varg
int add(TYPE x, ...)
{
	return x; //只是传参,不做计算
}
#else
int add(TYPE x, TYPE y)
{
	return (x+y);
}
#endif



int main(int argc, const char *argv[])
{
	TYPE a = -1;
	TYPE b = -2;
	TYPE c = 0;
	c = add(a, b);
	printf("%f\n", c);
	return 0;
}

示例1:
在这里插入图片描述
示例2:
在这里插入图片描述
示例3:
在这里插入图片描述

示例4:
在这里插入图片描述
(注意:区分fld1fldl

(参考:汇编语言学习笔记(十二)-浮点指令 ?)


版权声明:本文为weixin_42258222原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。