linux 下va_start,va_end,va_arg,va_list这些宏到底是什么?

文章详细探讨了LinuxGCC环境下,`va_start`,`va_end`,`va_arg`和`va_list`宏的实现原理。通过GDB调试和汇编代码分析,揭示了这些宏如何在编译器内部工作,以及参数在堆栈中的存储和访问方式。在GCC中,这些宏依赖于`__builtin_`函数实现,而在简易内核如Linux0.11中,它们是显式定义的。作者还展示了如何通过GDB观察和理解这些宏在运行时的行为。
摘要由CSDN通过智能技术生成

/* author:         hjjdebug
 * date:         2023年 07月 27日 星期四 10:50:21 CST
 * descriptor:  linux 下va_start,va_end,va_arg,va_list这些宏到底是什么?
 */
#include <stdarg.h>
#include <stdio.h>
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
    }}
----------------------------------------------------------------------------------------------------

*/

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
通过va_listva_start、va_argva_end是C语言中用于处理可变参数的一组。它们通常用于函数中,当函数需要接受不定数量的参数时,可以使用这些来获取和处理这些参数。 具体介绍如下: 1. va_listva_list是一个类型,用于声明一个指向参数列表的指针。它在函数中用于存储可变参数的信息。 2. va_start:va_start用于初始化va_list指针。它接受两个参数,第一个参数是一个va_list类型的变量,第二个参数是可变参数列表中最后一个已知的固定参数的名称。通过调用va_start,可以将va_list指针指向可变参数列表中的第一个可变参数。 3. va_argva_arg用于获取可变参数列表中的下一个参数的值。它接受两个参数,第一个参数是va_list类型的变量,第二个参数是要获取的参数的类型。通过调用va_arg,可以依次获取可变参数列表中的每个参数的值,并且每次调用后,va_list指针会自动指向下一个参数。 4. va_endva_end用于结束对可变参数列表的访问。它接受一个参数,即va_list类型的变量。通过调用va_end,可以释放与可变参数列表相关的资源。 使用这些的一般步骤如下: 1. 在函数中声明一个va_list类型的变量。 2. 调用va_start,将va_list指针指向可变参数列表中的第一个可变参数。 3. 使用va_arg依次获取可变参数列表中的每个参数的值。 4. 调用va_end,结束对可变参数列表的访问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值