/* author: hjjdebug
* date: 2023年 07月 27日 星期四 10:50:21 CST
* descriptor: linux 下va_start,va_end,va_arg,va_list这些宏到底是什么?
*/
#include
#include
void test_va(int num,...)
{
va_list args; // typedef __builtin_va_list va_list;
va_start(args,num); //#define va_start(v,l) __builtin_va_start(v,l)
for(int i=0; i < num; i++)
printf("%d\n", va_arg(args, int)); //#define va_arg(v,l) __builtin_va_arg(v,l)
va_end(args); // #define va_end(v) __builtin_va_end(v)
}
// 上面的注释是通过宏展开后获得的, 我用的是ubuntu20 环境
// 所以 va_start,va_arg,va_end,及 va_list 都是__builtin_ 内置变量
// 都依赖于编译器的实现, 成了黑箱操作了.
//
// 在简易内核linux0.11 上 , va_*操作并不是黑箱操作,而是显示定义的.如下:
// va_list args: va_list被定义成char *, 即args 为一个char *
// va_start(args,para)初始化 args 为第一个参数地址+1. 下一个参数地址
// va_arg(args,type), 循环调用该宏,可依次得到传来的参数
// va_end(args), 没有操作
//
// 对于linux gcc下的黑箱操作,我们还想进一步了解一下其实现,
// 方法有2
// 1. 用gdb 跟踪看其参数(本博客进行了这一步)
// 2. 反编译为汇编代码或用gdb 汇编跟踪
int main()
{
test_va(3,1,2,3);
return 0;
}
/*
args 被定义成包含一个元素的结构数组.
吐槽一下,这不就是一个结构吗, 非要搞成数组形式?! 服了,是避开俗套吗?
(gdb) ptype args
type = struct typedef __va_list_tag __va_list_tag {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} [1]
(gdb) ptype va_list
type = struct typedef __va_list_tag __va_list_tag {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} [1]
我们看到, args 就是va_list类型, va_list类型就是一个元素的结构数组.
未初始化的args 是这样的.
(gdb) display args
1: args = {{
gp_offset = 2496,
fp_offset = 2496,
overflow_arg_area = 0x9c0000009c0,
reg_save_area = 0x9c0000009c0
}}
初始化args指向第一个参数后面, 是这样的.
(gdb) next
1: args = {{
gp_offset = 8,
fp_offset = 48,
overflow_arg_area = 0x7fffffffdcc0,
reg_save_area = 0x7fffffffdc00
}}
我们看看num 的地址(第一个固定参数的地址)及其附近堆栈中的数据
(gdb) p &num
$1 = (int *) 0x7fffffffdbcc
(gdb) x/32bx 0x7fffffffdbcc
0x7fffffffdbcc: 0x03 0x00 0x00 0x00 0xc0 0x09 0x00 0x00
0x7fffffffdbd4: 0xc0 0x09 0x00 0x00 0xc0 0x09 0x00 0x00
0x7fffffffdbdc: 0xc0 0x09 0x00 0x00 0x08 0x00 0x00 0x00
0x7fffffffdbe4: 0x30 0x00 0x00 0x00 0xc0 0xdc 0xff 0xff
意外的发现,num 是对的,但其附近并没有发现推入的变参参数, 那看一下args 所指的地址: reg_save_area
(gdb) x/32bx 0x7fffffffdc00
0x7fffffffdc00: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffdc08: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffdc10: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffdc18: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00
我们看到第一项没有使用, 后面依次存放了1,2,3 参数, 间隔是8bytes, 于时知道了堆栈是从右向左推入参数的,
间隔是8btes
同时推断出 reg_save_area+gp_offset = 0x7fffffffdc00+8 = 0x7fffffffdc08 第一个参数的地址
----------------------------------------------------------------------------------------------------
我们访问va_arg(args,int), 会使args 内的gp_offset值增加8,从而指向了下一个参数
(gdb) next
8 printf("%d\n", va_arg(args, int)); //#define va_arg(v,l) __builtin_va_arg(v,l)
1: args = {{
gp_offset = 16,
fp_offset = 48,
overflow_arg_area = 0x7fffffffdcc0,
reg_save_area = 0x7fffffffdc00
}}
推断出 reg_save_area+gp_offset = 0x7fffffffdc00+16 = 0x7fffffffdc10 第二个参数的地址
----------------------------------------------------------------------------------------------------
8 printf("%d\n", va_arg(args, int)); //#define va_arg(v,l) __builtin_va_arg(v,l)
1: args = {{
gp_offset = 24,
fp_offset = 48,
overflow_arg_area = 0x7fffffffdcc0,
reg_save_area = 0x7fffffffdc00
}}
推断出 reg_save_area+gp_offset = 0x7fffffffdc00+24 = 0x7fffffffdc18 第三个参数的地址
----------------------------------------------------------------------------------------------------
8 printf("%d\n", va_arg(args, int)); //#define va_arg(v,l) __builtin_va_arg(v,l)
我们只有3个参数,更大的地址就不用关心了, fp_offset 我估计是最大分配了这么多,而overflow_arg_area则是超过此区域就
溢出了,不安全了的意思吧. gcc 代码没看,这里就望文生义,不影响理解就足够了.! 可见gcc具体实现在安全性上还考虑了不少.
1: args = {{
gp_offset = 32,
fp_offset = 48,
overflow_arg_area = 0x7fffffffdcc0,
reg_save_area = 0x7fffffffdc00
}}
----------------------------------------------------------------------------------------------------
*/