时间:2022-06-25 08:06:52 | 栏目:C代码 | 点击:次
可变参数是指函数的参数的数据类型和数量都是不固定的。
printf函数的参数就是可变的。这个函数的原型是:int printf(const char *format, ...)。
用一段代码演示printf的用法。
// code-A #include <stdio.h> int main(int argc, char **argv) { printf("a is %d, str is %s, c is %c\n", 23, "Hello, World;", 'A'); printf("T is %d\n", 78); return 0; }
在code-A中,第一条printf语句有4个参数,第二条printf语句有2个参数。显然,printf的参数是可变的。
code-A
先看两段代码,分别是code-A和code-B。
// file stack-demo.c #include <stdio.h> // int f(char *fmt, int a, char *str); int f(char *fmt, ...); int f2(char *fmt, void *next_arg); int main(int argc, char *argv) { char fmt[20] = "hello, world!"; int a = 10; char str[10] = "hi"; f(fmt, a, str); return 0; } // int f(char *fmt, int a, char *str) int f(char *fmt, ...) { char c = *fmt; void *next_arg = (void *)((char *)&fmt + 4); f2(fmt, next_arg); return 0; } int f2(char *fmt, void *next_arg) { printf(fmt); printf("a is %d\n", *((int *)next_arg)); printf("str is %s\n", *((char **)(next_arg + 4))); return 0; }
编译执行,结果如下:
# 编译
[root@localhost c]# gcc -o stack-demo stack-demo.c -g -m32
# 反汇编并把汇编代码写入dis-stack.asm中
[root@localhost c]# objdump -d stack-demo>dis-stack.asm
[root@localhost c]# ./stack-demo
hello, world!a is 10
str is hi
code-B
// file stack-demo.c #include <stdio.h> // int f(char *fmt, int a, char *str); int f(char *fmt, ...); int f2(char *fmt, void *next_arg); int main(int argc, char *argv) { char fmt[20] = "hello, world!"; int a = 10; char str[10] = "hi"; char str2[10] = "hello"; f(fmt, a, str, str2); return 0; } // int f(char *fmt, int a, char *str) int f(char *fmt, ...) { char c = *fmt; void *next_arg = (void *)((char *)&fmt + 4); f2(fmt, next_arg); return 0; } int f2(char *fmt, void *next_arg) { printf(fmt); printf("a is %d\n", *((int *)next_arg)); printf("str is %s\n", *((char **)(next_arg + 4))); printf("str2 is %s\n", *((char **)(next_arg + 8))); return 0; }
编译执行,结果如下:
# 编译
[root@localhost c]# gcc -o stack-demo stack-demo.c -g -m32
# 反汇编并把汇编代码写入dis-stack.asm中
[root@localhost c]# objdump -d stack-demo>dis-stack.asm
[root@localhost c]# ./stack-demo
hello, world!a is 10
str is hi
str2 is hello
在code-A中,调用f的语句是f(fmt, a, str);;在code-B中,调用f的语句是f(fmt, a, str, str2);。
很容易看出,int f(char *fmt, ...);就是参数可变的函数。
实现可变参数的关键语句是:
char c = *fmt; void *next_arg = (void *)((char *)&fmt + 4); printf("a is %d\n", *((int *)next_arg)); printf("str is %s\n", *((char **)(next_arg + 4))); printf("str2 is %s\n", *((char **)(next_arg + 8)));
内存地址的计算方法
先看一段伪代码。这段伪代码是f函数的对应的汇编代码。假设f有三个参数。当然f也可以有四个参数或2个参数。我们用三个参数的情况来观察一下f。
f:
; 入栈ebp
; 把ebp设置为esp
; ebp + 0 存储的是 eip,由call f入栈
; ebp + 4 存储的是 旧ebp
; 第一个参数是 ebp + 8
; 第二个参数是 ebp + 12
; 第三个参数是 ebp + 16
; 函数f的逻辑
; 出栈ebp。ebp恢复成了刚进入函数之前的旧ebp
; ret
调用f的伪代码是:
; 入栈第三个参数
; 入栈第二个参数
; 入栈第一个参数
; 调用f,把eip入栈
在汇编代码中,第一个参数的内存地址很容易确定,第二个、第三个还有第N个参数的内存地址也非常容易确定。无法是在ebp的基础上增加特定长度而已。
可是,我们只能确定,必定存在第一个参数,不能确定是否存在的二个、第三个还有第N个参数。没有理由使用一个可能不存在的参数作为参照物、并且还要用它却计算其他参数的地址。
第一个参数必定存在,所以,我们用它作为确定其他参数的内存地址的参照物。
在f函数的C代码中,&fmt是第一个参数占用的f的栈的元素的内存地址,换句话说,是一个局部变量的内存地址。
局部变量的内存地址不能作为函数的返回值,却能够在本函数执行结束前使用,包括在本函数调用的其他函数中使用。这就是在f2中仍然能够使用fmt计算出来的内存地址的原因。
当参数是int类型时,获取参数的值使用*(int *)(next_arg)。
当参数是char str[20]时,获取参数的值使用*(char **)(next_arg + 4)。
为什么不直接使用next_arg、(next_arg + 4)呢?
分析*(int *)(next_arg)。
在32位操作系统中,任何内存地址的值看起来都是一个32位的正整数。可是这个正整数的值的类型并不是unsigned int,而是int *。
关于这点,我们可以在gdb中使用ptype确认一下。例如,有一小段代码int *a;*a = 5;,执行ptype a,结果会是int *。
next_arg只是一个正整数,损失了它的数据类型,我们需要把数据类型补充进来。我们能够把这个操作理解成”强制类型转换“。
至于*(int *)(next_arg)前面的*,很容易理解,获取一个指针指向的内存中的值。
用通用的方式分析*(char **)(next_arg+4)。
给出一段验证第3点的代码。
char str[20] = "hello"; char *ptr = str; // 使用gdb的ptype 打印 ptype &ptr
打印结果如下:
Breakpoint 1, main (argc=1, argv=0xffffd3f4) at point.c:13
13 char str7[20] = "hello";
(gdb) s
14 char *ptr = str7;
(gdb) s
19 int b = 7;
(gdb) p &str
$1 = (char **) 0xffffd2fc
在code-A和code-B中,我们人工根据参数的类型来获取参数,使用*(int *)(next_arg)或*(char **)(next_arg + 4)。
库函数printf显然不是人工识别参数的类型。
这个函数的第一个参数中包含%d、%x、%s等占位符。遍历第一个参数,识别出%d,就用*(int *)next_arg替换%d。识别出
%s,就用*(char **)next_arg。
实现了识别占位符并且根据占位符选择指针类型的功能,就能实现一个完成度很高的可变参数了。