首页
社区
课程
招聘
[原创] AArch64函数栈的分配,指令生成与GCC实现(上)
发表于: 2022-4-5 08:50 19503

[原创] AArch64函数栈的分配,指令生成与GCC实现(上)

2022-4-5 08:50
19503

本篇主要介绍原理,源码分析见  AArch64函数栈的分配,指令生成与GCC实现(下)

  为便于理解,本文中后续术语含义解释如下:

  aarch64下函数栈是向低地址方向增长, 按照由高=>低的顺序其函数栈可以包含如下区域:

  虽然函数栈整体是由高地址=>低地址增长的,但每个区域内部有自己的变量分配顺序。这些区域的主要作用为:

  即传入栈参数区域, 内部元素分配方向: 低地址 => 高地址

  严格来讲这段区域是caller分配的, 并不属于callee栈帧的一部分, 当caller调用callee时, callee 通过incoming 区域接收caller的参数。

  对于caller来说这段区域称为outgoing stack argument(传出栈参数区域), 一个函数的 incoming 区域首地址等于其父函数的outgoing 区域的首地址。

  即匿名寄存器参数区域, 内部元素分配方向: 低地址 => 高地址

  对于可变参函数,此区域用来保存函数中可能用到的所有匿名寄存器参数,va_xxx系列函数的参数解析依赖于此区域。

  从此区域开始(以及向低地址方向的内存)均为callee分配, 此区域按照高地址=>低地址实际上分为GPRs/FPRs两个子区域; 每个区域内部元素分配方向均是低地址=>高地址;

  即局部变量区域, 内部元素分配方向: 高地址 => 低地址

  函数中显示定义的变量,编译器内部生成的临时变量均存放在此区域中。

  需要注意的是,编译优化可能导致变量分配顺序与源码中定义顺序不同,在不考虑优化的情况下其内部元素分配方向默认是高地址=>低地址。

  即callee-saved寄存器区, 内部元素分配方向: 低地址 => 高地址

  在AAPCS64标准中[3]:

  即动态栈分配区, 内部元素分配方向: 高地址 => 低地址

  当函数中调用alloca时会在栈中dynamic allocation区域分配需内存,这部分区域在编译期间不存在, 在运行期间大小可以随执行变化。

  即栈参数传出区域, 内部元素分配方向: 低地址=> 高地址;

  当caller需要通过栈向子函数传递参数(如大于8个GPR参数)时, 栈参数保存在caller的outgoing区域。

  一个函数(作为caller)其outgoing区域的大小是固定的,取决于其调用的callees中使用了最多栈参数的那个函数。当函数中存在动态分配时,其outging区域的首地址是可能动态变化的,但总是等于当前硬件寄存器sp的值, caller调用callee时也是通过此sp将其outgoing区域首地址传递给callee(作为callee的incoming区域首地址)。

  按照AAPCS64标准,以通用寄存器(GPRs)传参为例(不考虑浮点参数和结构体参数等) :

  如以下函数:

  其中:

1. func1/func2的栈参数来自其caller(main1)的函数栈

2. 两次函数调用的前两个寄存器参数在栈中的地址是相同的, 其中:

  在incoming/outgoing区域, 栈参数是基于callee入口时sp向高地址方向寻址的, 正是由于参数首地址相同且ABI规则相同(AAPCS64), caller和callee之间才能正确的传递栈参数.

以上代码的函数栈如图:

  这里需要注意的是:

  通过汇编代码可以更好的理解outgoing/incoming区域:

  可变参函数指的是一个可以接受可变个参数的函数, 调用此函数时只有caller知道为此函数传入了多少个参数, 可变参函数callee只知道caller最少传入了多少个参数:

  对于可变参函数(callee), 其需要:

运行时通常需要通过已知的某个命名参数来确定caller到底传入了多少个参数(caller在调用可变参数函数时通常需要显式或隐式的告知callee自己传入了多少个参数), 如:

  可变参函数的参数分为四类, 即寄存器命名参数, 寄存器匿名参数, 栈命名参数, 栈匿名参数, 举例如下:

  可变参函数的参数传递同样满足AAPCS64:

* 对caller来说, 编译期间即可确定其向可变参callee传入了多少个参数,故其直接按照AAPCS64标准布局参数(寄存器参数直接赋值给硬件寄存器,栈参数保存到outgoing栈对应位置).

* 对callee来说, 编译期间只能确定有多少个命名参数:

* 对callee来说, 匿名参数的个数需直接或间接通过某个命名参数得知:

  举例如下:

  输出结果:

  其函数栈分布如图:

  由此函数可以看到:

若func有大于8个整型, 8个浮点型参数,则其函数栈中不再需要为匿名寄存器参数预留空间,如:

  在不开启优化时, 局部变量通常是按照其定义顺序依次分配到局部变量区域的(高地址=>低地址), 但开启优化时局部变量的分配可能经过延迟展开,重排等优化, 此时源码中定义顺序与分配顺序可能不同。

  局部变量区的尾地址(即第一个局部变量的尾地址):

  故对于定参函数,通常会看到如下布局:

  局部变量区除了用来保存源码中定义的局部变量外,还用来存储gcc编译过程中生成的临时变量以及硬件寄存器参数的原始值(若需要), 以三.2中代码为例,由其局部变量区可见:

1. 局部变量区域的起始地址为变量var0的尾地址

2. 源码中的局部变量var0/var1/vl按照高地址=>低地址的顺序依次分布(但并非总是如此,编译优化可能改变分配顺序)

3. 源码中存在对参数p0/p1/p2的解引用, 在局部变量区依次为其分配存储空间(参数的存储位置总是在源码中局部变量之后),实际上:

  callee-saved regs区域用来保存 callee-saved register的, 按照AAPCS64标准 [r19, r28] 是callee-saved register,这些寄存器在子函数返回时要保持不变,也就是说如果子函数中使用了这些寄存器,则子函数需要保存并在返回前恢复其原始值。 函数栈中callee-saved register区域就是用来保存这些寄存器原始值的。callee-saved regs区域内部也是由低地址=>高地址增长的, 但区别在于此区域按照寄存器的编号顺序保存,而不是其在源码中被使用的顺序,如:

   需要注意的是,当当前函数需要保存frame chain时, x29,x30总是放在callee-saved regs最低端,同样是如上代码:

  当源码中调用alloca/__builtin_alloca时会在当前函数栈中为此变量分配存储空间, alloca函数转换成伪c代码可以表示为:

动态栈分配和outgoing区域并不冲突,当前函数栈中:

以下面的函数为例:

  函数栈帧是在其prologue中构建,在其epilogue中释放的。prologue的主要工作包括两点: 1) 通过减小sp预留栈空间 2) 保存所有callee-saved 寄存器; epilogue则相反。gcc内部主要依靠4个变量决定如何生成prologue中的指令,分别是:

  如下:

  在汇编代码中我们通常可以看到4种不同的指令生成方式, 按照gcc中的判断顺序:

  以如下函数为例:

  prologue的主要作用是调整sp以及保存callee-saved regs,优先选用stp/str Rm, Rn, [sp, -frame_size]! 模板的原因是因为此指令可以在调整栈帧的同时直接在栈顶 save两个寄存器,但同时此模板也受到两个限制:

1. frame_size 不能过大:

    frame_size多少算过大取决于当前函数保存的callee-saved reg 的数量:

2. 当前函数不能存在outgoing区域:

   若当前函数存在outgoing区域,则 stp/str Rm, Rn, [sp, -frame_size]! 会直接将Rm/Rn 保存到outgoing栈,故不能实用此模板

  若1不成立则优先考虑此模板,以如下函数为例:

  若1不成立,且此时 outgoing + callee_saved reg区域也较大时,则只能使用最麻烦的方法,即:

  但这实际上又分为两种情况:

  若此时hard_fp_offset区域较小,则上面1)/2)有部分可以合并处理, 即通过stp reg1, reg2, [sp, -hard_fp_offset]! 一条指令即将sp先指向callee-saved regs区域,又同时保存了两个寄存器,如:

那只能使用最慢的方式,如:

[1] GCC源码分析(十五) — gimple转RTL(pass_expand)(上)_ashimida@的博客-CSDN博客_gcc gimple

[2] GCC源码分析(八) — 语法/语义分析之声明与函数定义的解析_ashimida@的博客-CSDN博客_gcc源码分析

[3] 《Procedure Call Standard for the ARM 64-bit Architecturn》

[4] GCC源码分析—shrink-wrapping_ashimida@的博客-CSDN博客

[5] https://blog.csdn.net/lidan113lidan/article/details/123962416


版权声明:本文为笔者本人「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/123961152

更多内容可关注微信公众号 


/* 从高地址=>低地址, 这些区域包括:
  * incoming stack: 传入栈参数区域 (这段区域实际上属于caller函数, 在callee中作为栈参数使用)
  * varargs: 匿名寄存器参数区域
  * local variables: 局部变量区域
  * callee-saved regs: callee-saved 寄存器存储区域
  * dynamic allocation: 动态栈分配区域
  * outgoing stack: 传出栈参数区域
*/
+-------------------------------+ <- highmem
|                               |
|  incoming stack arguments     |
|                               |
+-------------------------------+ <-- incoming stack (aligned)/caller sp
|                               | 
|  callee-allocated save area   |
|  for register varargs         |
|                               |
+-------------------------------+ 
|  local variables              | 
|                               |
+-------------------------------+
|  padding                      | \
+-------------------------------+  |
|  callee-saved registers       |  | frame.saved_regs_size
+-------------------------------+  |
|  LR'                          |  |
+-------------------------------+  |
|  FP'                          | /
+-------------------------------+ <-- 硬件寄存器fp(x29)指向这里(若有)
|  dynamic allocation           |
+-------------------------------+
|  padding                      |
+-------------------------------+
|  outgoing stack arguments     |
|                               |
+-------------------------------+ <-- 硬件寄存器sp指向这里/callee sp
|                               | 
    				| <- lowmem

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2022-4-5 09:02 被ashimida编辑 ,原因:
收藏
免费 5
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//