-
-
[原创] AArch64中va_list/va_start/va_arg/...的实现
-
发表于: 2022-4-28 20:49 19803
-
可变参函数指的是一个可以接受可变个参数的函数, 调用此函数时只有caller知道为此函数传入了多少个参数, 可变参函数callee只知道caller最少传入了多少个参数:
callee中可以确定的参数称为命名参数(Named arguments)
对于可变参函数(callee), 其需要:
在运行时通常需要通过已知的某个命名参数来确定caller到底传入了多少个参数(caller在调用可变参数函数时也需要显式或隐式的告知callee自己传入了多少个参数), 如:
可变参函数的参数分为四类, 即寄存器命名参数, 寄存器匿名参数, 栈命名参数, 栈匿名参数, 举例如下:
可变参函数的参数传递同样满足AAPCS64:
对caller来说, 编译期间即可确定其向可变参callee传入了多少个参数,故其直接按照AAPCS64标准布局参数(寄存器参数直接赋值给硬件寄存器,栈参数保存到outgoing栈对应位置).
对callee来说, 编译期间只能确定有多少个命名参数:
命名寄存器参数直接通过硬件寄存器获取
命名栈参数通过incoming栈获取
对callee来说, 匿名参数的个数通常需直接或间接通过某个命名参数得知:
匿名栈参数同样继续通过incoming区域获取
匿名寄存器参数则是从匿名寄存器存储区域(callee-allocated save area for register varargs)获取
在可变参函数中通常会使用va_xxx系列函数来获取匿名参数,va_xxx函数在gcc中会对应到其对应的一个builtin函数上:
gcc中定义的此系列内联函数包括:
这里只介绍其中部分,其余类似的函数见源码.
__builtin_va_list是gcc内置的一个结构体类型, 在gcc内部定义如下:
各字段在函数栈中的位置如下:
一个经过初始化后的__builtin_va_list结构体(vl)中:
__builtin_va_start(vl, x)的作用是初始化vl结构体,在aarch64中这里的x没有实际的作用,但调用此函数时x必须是此函数的最后一个命名参数,否则会编译报错。__builtin_va_list vl; 就是一个简单的结构体局部变量定义,故在使用vl之前必须调用__builtin_va_start函数对其初始化, __builtin_va_start的作用为:
需要注意的是, 编译器按照AAPCS64布局函数栈,一个函数的第一个匿名栈参数/通用/浮点寄存器参数的位置(基于sp的偏移)只与当前函数声明的(命名)参数列表有关,故这些偏移在编译阶段即可确定, __builtin_va_start 基于当前sp + offset计算此次运行时这些参数的位置。
__builtin_va_arg(vl, type) 的作用是返回当前尚未访问的一个类型为type的匿名参数的值(注意返回的是值而不是地址), 并调整vl中的__stack/__gr_offset/ __vr_offset指向下一个匿名参数位置。__builtin_va_arg从哪个区域返回匿名参数是由当前vl中的信息和此次要获取的参数类型type共同决定的:
以通用寄存器为例, 判断匿名通用寄存器参数区是否还有参数的标准为:
其伪代码[1]简化后如下:
举例如下:
输出结果:
其函数栈分布如图:
在man 手册中对于va_end的描述是:所有vl结构体在使用完毕后都要调用va_end, 以标记在此后的代码中vl结构体不再可用.
未开启编译优化时,__builtin_va_end(vl)语句并没有什么实质性作用; 但当开启编译优化时若在 __builtin_va_end(vl)之后再使用 vl,则可能因优化导致不可预期的错误,如:
__builtin_va_list 是gcc内部定义的一个结构体类型, 在源码解析前 __builtin_va_list 类型声明结点已经定义好了,当语法分析解析到 符号 "__builtin_va_list"时会直接将其识别为一个结构体类型,其转换为c代码类似:
此结构体在cc1初始化阶段定义:
__builtin_va_xxx系列函数包括:
gcc中所有的内联函数都记录在 "builtins.def"文件中,如:
在cc1初始化阶段会先为这些内联函数定义声明树节点(FUNCTION_DECL):
2.2 gcc解析源码中的内联函数
内联函数声明结点定义后,当词法分析中发现一个内联函数名token时会将其自动转换为对应的内联函数声明树结点,此过程发生在:
细节参考[2], 这里不再做展开.
在rtl_expand阶段, 源码中调用的所有内联函数都会被展开:
需要注意的是,这里介绍的只是这些内联函数自身代码的展开,当调用某些函数时可能本身存在副总用(如BUILT_IN_VA_END会影响编译优化), 相关话题不在本文讨论范围内。
源码中调用的__builtin_va_start函数最终通过 expand_builtin_va_start 函数展开:
__builtin_va_end(vl) 通过函数expand_builtin_va_end展开, 其作用是注销变量vl, 此后的代码中vl结构体不再可用。在expand_builtin_va_end函数中本身并没有做什么:
因此在未开启优化时源码中的 __builtin_va_end(vl); 语句并没有什么实质性的作用; 但当开启优化时可能导致不确定行为,大体原因可能是(这里笔者没有深入研究) __builtin_va_end(vl)会被认为是变量vl的def, 此后若再使用变量vl则会认为此时的vl与调用__builtin_va_end之前的vl无关:
4. __builtin_va_copy
__builtin_va_copy(dest, src) 通过函数 expand_builtin_va_copy 展开,其作用是将src这个__va_list的各个当前值复制到 dest这个__va_list中, 细节见源码.
__builtin_next_arg 通过函数expand_builtin_next_arg 展开, 用来获取此函数第一个匿名栈参数的起始地址。
__builtin_saveregs通过expand_builtin_saveregs展开,aarch64不支持此函数(很少有平台支持此函数), 逻辑上此函数负责确保 varargs 都存到了栈上。
和其他__builtin_va_xxx函数不同, 在gcc中__builtin_va_arg实际上是一个关键字:
1. 当源码中解析到 __builtin_va_arg 关键字时会先将其转换为一个VA_ARG_EXPR表达式:
2. gimplify阶段会将 VA_ARG_EXPR 表达式转换为一个 函数编号为 IFN_VA_ARG 的gcall指令:
3. 在pass_stdarg中通过平台相关函数单独处理 gcall(IFN_VA_ARG):
在aarch64中 __builtin_va_arg最终通过 aarch64_gimplify_va_arg_expr 函数展开, 此过程中构建了一系列表达式。源码全是表达式展开较复杂,还是结合AAPCS64[1]以一个简化的伪代码说明原理:
__builtin_va_arg(vl, type)获取下一个参数匿名的流程受到三个因素影响:
其中:
1. 函数声明:
函数声明决定了此函数中已经有多少个命名参数,同时也影响了其不定参数的存储位置,如:
2. 参数类型(type)
源码中使用va_arg(vl, type)时,编译器即可根据类型确定运行时应先从GPRs还是VPRs中动态获取参数. 对于如没有匿名通用寄存器参数的函数(如func2), 编译期间可直接优化为从incoming栈中获取匿名参数,如:
3. vl结构体
vl结构体中记录已经获取过了哪些匿名寄存器,在__builtin_va_start(vl, ...)初始化时,其记录的是第一个匿名通用寄存器/浮点寄存器/栈参数的地址, 每调用一次va_arg获取一个参数后,此结构体中对应字段会指向下一个通用寄存器/浮点寄存器/栈参数。
[1] 《Procedure Call Standard for the ARM 64-bit Architecturn》
[2] GCC源码分析(八) — 语法/语义分析之声明与函数定义的解析_ashimida@的博客-CSDN博客_gcc源码分析
[3] AArch64函数栈的分配,指令生成与GCC实现(上)_ashimida@的博客-CSDN博客
[4] AArch64函数栈的分配,指令生成与GCC实现(下)_ashimida@的博客-CSDN博客
版权声明:本文为笔者本人「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/123962416
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
赞赏
- [原创] AArch64中va_list/va_start/va_arg/...的实现 19804
- [原创] AArch64函数栈的分配,指令生成与GCC实现(上) 19504
- [原创] 内核模块的加载流程 55123
- [原创] linux中的信号处理与SROP 26163
- [原创] AARCH64平台的栈回溯 30316