编译器:GCC 4.6.3
运行环境:Ubuntu12.04
处理器:Intel i386(32位)
目录
一、可变参数实现原理
约定:
-
1、调用函数将被调用函数参数入栈,入栈顺序由调用约定规定,包括
cdecl
,stdcall
,fastcall
,naked 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;
}
参考:【编写操作系统之路】-可变参数(…) 🚀
五、补充说明——固定参数与可变参数之间实参入栈的差异
- 对于字符型数据,两者之间没有差异,占用4字节(不是数据扩展,只改变低8位)。【示例1】
- 整形数据,两者之间没有差异,占用4字节。【示例2】
- 单精度数据,可变参数将单精度(4字节)扩展成双精度(8字节)入栈,而固定参数的函数,还是按单精度大小入栈。【示例3】
- 双精度数据,两者之间没有差异,占用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:
(注意:区分fld1
和fldl
)
(参考:汇编语言学习笔记(十二)-浮点指令 🚀)