AArch64函数栈的分配,指令生成与GCC实现(下)

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

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

 上一篇 AArch64函数栈的分配,指令生成与GCC实现(上)_ashimida@的博客-CSDN博客 主要说明原理,本篇主要介绍gcc中的实现.

一、函数栈中的位置标记

  gcc编译过程中会为当前函数布局函数栈,函数栈中的每一个元素位置最终都会对应当基于硬件寄存器sp或fp的偏移,但在编译过程中这些偏移不是一次性确定的。gcc在编译过程中内部会使用一些虚拟寄存器/伪寄存器作为标记位来代表函数栈中的各个位置,这些标记位最终被转换为sp/fp + offset并在最终输出的汇编代码中不可见,但这些标记位在源码分析中十分重要,故这里先统一介绍其含义:

  • virtual_incoming_args_rtx: 标记当前函数传入参数栈的首地址
  • virtual_stack_vars_rtx: 标记当前函数第一个局部变量的尾地址
  • virtual_stack_dynamic_rtx: 标记当前函数执行alloca动态分配栈空间时分配到的首地址
  • virtual_outgoing_args_rtx: 标记当前函数outgoing栈的首地址
  • virtual_cfa_rtx: 标记当前函数CFA(Caonical Frame Address)的首地址
  • arg_pointer_rtx: 标记当前函数参数列表的地址,在aarch64指向栈参数首地址
  • frame_pointer_rtx: 标记gcc栈帧中自动生成变量的存储地址,在aarch64中指向局部变量区的尾地址
  • hard_frame_pointer_rtx: 标记当前硬件寄存器fp的当前位置,在aarch64中则是x29的当前值.
  • stack_pointer_rtx: 标记当前硬件寄存器SP的当前位置, 在aarch64中是sp的当前值.

这些标记位在栈帧中的位置见下:

+-------------------------------+ <- highmem
|                               |
|  incoming stack arguments     |
|                               |
+-------------------------------+ <-- incoming stack pointer (aligned)/virtual_incoming_args_rtx/arg_pointer_rtx
|                               | 
|  callee-allocated save area   | 
|  for register varargs         |
|                               |
+-------------------------------+ <-- virtual_stack_vars_rtx/frame_pointer_rtx
|  local variables              | 
|                               |
+-------------------------------+
|  padding                      | \
+-------------------------------+  |
|  callee-saved registers       |  | frame.saved_regs_size
+-------------------------------+  |
|  LR'                          |  |
+-------------------------------+  |
|  FP'                          | /
+-------------------------------+ <-- aligned/hard_frame_pointer_rtx(fp)/函数入口时的virtual_stack_dynamic_rtx
|  dynamic allocation           |
+-------------------------------+
|  padding                      |
+-------------------------------+
|  outgoing stack arguments     |
|                               |
+-------------------------------+ <-- aligned/stack_pointer_rtx(sp)/virtual_outgoing_args_rtx
|                               | 
    							| <- lowmem

  根据标记位置,函数栈也可以定义如下:

## 为了简化这里未考虑align
[virtual_incoming_args_rtx                                , virtual_incoming_args_rtx + ...         ] //传入参数区域, *virtual_incoming_args_rtx 保存第一个栈参数
[virtual_incoming_args_rtx - gr_saved * 8,                , virtual_incoming_args_rtx               ] //匿名GPRs区域, *(virtual_incoming_args_rtx - gr_saved * 8)保存第一个匿名通用寄存器参数
[virtual_incoming_args_rtx - gr_saved * 8 - vr_saved * 16 , virtual_incoming_args_rtx - gr_saved * 8] //匿名FPRs区域, *(virtual_incoming_args_rtx - gr_saved * 8 - vr_saved * 16)保存第一个匿名浮点寄存器参数
[virtual_stack_vars_rtx - x_frame_offset                  , virtual_stack_vars_rtx/frame_pointer_rtx] //局部变量区, virtual_stack_vars_rtx是第一个局部变量尾地址
[hard_frame_pointer_rtx                                   , virtual_stack_vars_rtx - x_frame_offset ] //callee-saved regs区域, 按照寄存器编号由低地址=>高地址存储(x29/x30可能例外)
//[stack_pointer_rtrx + outgoing_arg_size                 , virtual_stack_dynamic_rtx               ] //运行时才可能存在,alloca动态分配区
[stack_pointer_rtx/virtual_outgoing_args_rtx              , stack_pointer_rtrx + outgoing_arg_size  ] //outgoing区域, 运行时可能随sp变化, *virtual_outgoing_args_rtx保存第一个传出栈参数

二、caller的参数传递(incoming/outgoing参数的生成)

  在gcc编译过程中, 源码中的每一个函数调用在gimplify后都会对应到一条gcall指令,这些gcall指令最终在 caller gimple=>rtl(pass_expand) => expand_call 中展开。gcall语句的展开过程主要完成了以下操作:

1) 按照AAPCS64标准确定每个参数的存储位置

  caller向callee传递实参的过程中并不关注callee是否为可变参函数,caller总是知道此次调用需要向callee传递多少个参数,其只需按照AAPCS64标准确定每个参数的存储位置:

  • 参数会优先存储到硬件寄存器[R0,R7]/[V0,V7]
  • 若不能存储到硬件寄存器则将参数存储位置为caller的outgoing区域

  需要注意的是: aarch64中栈向低地址增长, 而栈参数则是基于outgoing区域(硬件寄存器sp)顺序向高地址增长,第一个栈参数(首地址)存储在 caller's sp的位置.

2) 发射寄存器参数复制指令

  在寄存器传参中, 寄存器只是caller/callee参数传递的中介,此参数在caller中有其来源,此过程中:

  • 首先要将硬件寄存器参数的编号转换为其对应的位置表达式(如 rtx (reg R0))
  • 然后向指令序列中发送如 *R0 = XXX; 的指令, 其中XXX是caller中此参数的来源(如 int x = 0; fun(x):, 这里的XXX就来自x)

3) 发射栈参数复制指令

  在栈传参中, 栈同样也只是caller/callee参数传递的中介,对于callerc此参数同样另有来源,此过程中:

  • 首先要将栈参数的偏移转换为基于virtual_outgoing_args_rtx的位置表达式(如 rtx (mem (plus virtual_outgoing_args_rtx args[i].locate.offset)))
  • 然后向指令序列中发送如 * ( virtual_outgoing_args_rtx + offset) = XXX; 的指令, 其中XXX同样是call中此参数的来源.(更多细节可参考[1]):

4) 更新caller中outging栈的最大使用量(若需)

//./gcc/calls.c
/*
   expand_call函数负责展开一条函数调用(gcall指令), 其根据AAPCS64确定每一个参数的存储位置(寄存器/outgoing栈),
  并向指令序列中发射指令以确保参数被正确的复制, 具体包括:
   1) 确定此次调用的实参个数
   2) 根据AAPCS64标准,确定前8个参数通过R0-R7寄存器传入(不考虑浮点参数); 其他参数通过栈传入; 并计算出这些参数在outgoing栈中的偏移(initialize_argument_information)
   3) 更新当前caller的outgoing最大使用量(若需)
   4) 确定所有栈参数的位置表达式, 如: (mem (plus virtual_outgoing_args_rtx args[i].locate.offset))    (compute_argument_addresses)
   5) 确定所有寄存器实参值的来源(src),如来自局部变量 (precompute_register_parameters)
   6) 确定所有栈实参的来源(src),并向指令序列发射将 src复制到实参所在outgoing栈的指令,如*(virtual_outgoing_args_rtx + args[i].locate.offset) = args[i].value;
   7) 向指令序列发射将寄存器实参来源复制到硬件寄存器的指令,如  Ri = args[i].value;
*/
rtx expand_call (tree exp, rtx target, int ignore)
{
  //part1. 为所有实参分配存储位置
  ......
  /* 计算此调用中传入的实参的个数,实参个数是编译期间可确定的 */
  num_actuals = call_expr_nargs (exp) + num_complex_actuals + structure_value_addr_parm;
  ......
  n_named_args = num_actuals;
  /* 初始化args_so_far_v结构体, 此结构体后续负责记录按照AAPSC64已经分配的传入参数信息 */
  INIT_CUMULATIVE_ARGS (args_so_far_v, funtype, NULL_RTX, fndecl, n_named_args);
  args_so_far = pack_cumulative_args (&args_so_far_v);

  /* 分配一个arg_data类型的(逆序)数组, 数组大小为参数个数, 每个参数的信息都记录在其中的一个元素中*/
  args = XCNEWVEC (struct arg_data, num_actuals);

  /*
     根据AAPCS64标准,计算出每个实参应该如何传给callee:
     * 若参数通过硬件寄存器传入则args[i].reg为一个非空的rtx(REG)表达式,代表此传入参数来自哪个硬件寄存器(参数位置相当于确定了)
     * 若参数通过caller栈传入,则args[i].reg为空,args[i].locate记录此参数在caller栈上(基于 virtual_outgoing_args_rtx)的偏移(此时只是偏移)
     args_size返回此次调用的实参总共需要在栈中需要分配多少空间
  */
  initialize_argument_information (num_actuals, args, &args_size, n_named_args, exp,
				   structure_value_addr_value, fndecl, fntype, args_so_far, reg_parm_stack_space,
				   &old_stack_level, &old_pending_adj, &must_preallocate, &flags, &try_tail_call, CALL_FROM_THUNK_P (exp))
  ...... 
      
  /* 更新当前函数outgoing区域的最大使用空间(若需) */
  crtl->outgoing_args_size = upper_bound (crtl->outgoing_args_size, args_size);	
  ......
  
  //part2. 确定所有实参存储位置,并发射参数复制到实参位置的指令
  argblock = virtual_outgoing_args_rtx;			/* outgoing 区域基地址 */

  /* 确定所有栈参数在outgoing区域的位置, 若参数i为栈参数, 则其位置最终记录到args[i].stack中, 表达式为:
     (mem (plus virtual_outgoing_args_rtx args[i].locate.offset))
   */
  compute_argument_addresses (args, argblock, num_actuals);

  /*  expand寄存器参数i的参数来源,并将结果记录到args[i].value中; 如
      int x = 0;		
      func(x, y);		//此时寄存器参数R0的来源是局部变量x, 代表局部变量x的rtx表达式最终被记录到args[i].value中
  */
  precompute_register_parameters (num_actuals, args, &reg_parm_seen);

  /* 对于所有栈参数,同样先确定参数来源(args[i].value), 并发射指令将其复制到outgoing区域;
     最终发射的指令类似: *(virtual_outgoing_args_rtx + args[i].locate.offset) = args[i].value;
  */
  for (i = 0; i < num_actuals; i++)
  { 
	  /* 对于栈参数,则通过store_one_arg 发射指令将实参赋值到栈内存中 */
	  if (args[i].reg == 0 || args[i].pass_on_stack)
	  {
	      rtx_insn *before_arg = get_last_insn ();
	      if (store_one_arg (&args[i], argblock, flags, adjusted_args_size.var != 0, reg_parm_stack_space) ...
	  }
  }
  .......
              
  /* 发射指令将所有硬件寄存器参数值复制到对应的硬件寄存器中, * args[i].reg = args[i].value; */
  load_register_parameters (args, num_actuals, &call_fusage, flags, pass == 0, &sibcall_failure);
  .......
}

三、callee局部变量、传入参数的处理(local variable/incoming/varargs)

  在gimple => rtl(pass_expand)阶段, 对函数的展开首先就是要展开其局部变量和传入参数(传入参数的展开中确定了一个函数作为callee如何接受参数)。此过程在pass_expand::execute中实现:

  1. 调用expand_used_vars展开所有临时/局部变量
  2. 调用expand_function_start 处理传入参数(包括为匿名参数预留栈空间)

  二者都在pass_expand::execute中实现,故一并介绍:

//gcc/cfgexpand.c
/* 函数中变量与参数的存储位置:
   * 局部变量:
      局部变量从局部变量首地址(virtual_stack_vars_rtx)开始分配, 如(mem (plus virtual_stack_vars_rtx, frame_offset))
     由于其分配方向是高地址=>低地址,故 frame_offset 总是负数.
   * 命名参数:
     - 命名参数的来源(AAPCS64)记录在tree_parm_decl parm 的 DECL_INCOMING_RTL(parm)字段中:
       - 寄存器参数来自某个硬件寄存器,故其对应表达式为 rtx(REG no),  no为硬件寄存器编号
       - 栈参数来自当前函数(作为callee)的传入栈,故对于callee来说其表达式为: (mem (plus virtual_incoming_args_rtx, offset)), offset 为正数
     - 命名参数在callee中的存储位置记录在 DECL_RTL(parm)中:
       - 开启优化时,命名寄存器/栈参数均保存在伪寄存器中,位置表达式如: rtx(REG no), no为伪寄存器编号
       - 未开启优化时, 命名寄存器/栈参数均保存在栈中:
       	 - 对于寄存器参数,在局部变量空间为其分配存储位置: mem(plus(virtual_stack_vars_rtx, frame_offset)), frame_offset 为负数
         - 对于栈参数,直接复用其在incoming栈中的位置: (mem (plus virtual_incoming_args_rtx, offset)), offset为正数 
     - 此函数中需要将命名参数复制到其在callee的存储位置(若需)
   * 匿名参数:
     - 匿名栈参数(若有)和命名栈参数一样,其来源均为incoming区域:  (mem (plus virtual_incoming_args_rtx, offset)), offset为正数 
     - 匿名寄存器参数(若有)同样来自硬件寄存器,但在callee中需要为每个可能出现的匿名寄存器参数预留存储位置, 此存储位置通常和栈参数是相邻的:
       [virtual_incoming_args_rtx - gr_saved * 8 - vr_saved * 8, virtual_incoming_args_rtx]
     - 当前函数每次都需要无条件将每个可能的匿名寄存器参数由硬件寄存器复制到其对应的存储位置,以确保不论caller传入多少个匿名参数均可正确解析.
*/
unsigned int pass_expand::execute (function *fun)
{
  start_sequence ();
  var_ret_seq = expand_used_vars ();				/* 局部/临时变量的展开 */
  var_seq = get_insns ();
  end_sequence ();
  .......
  expand_function_start (current_function_decl);	/* 传入参数的处理 */
  .......
  return 0;
}

3.1 callee中局部变量栈空间的

  此过程在expand_used_vars中实现, 在[1]中实际上已经对此过程做过详细的分析,这里只提及其中的关键点:

/*
   此函数负责展开当前函数中所有局部变量, 此展开基于栈中的标记位virtual_stack_vars_rtx,
  局部变量区由高地址=>低地址增长,故virtual_stack_vars_rtx实际上是第一个局部变量的尾地址.
   virtual_stack_vars_rtx 最终会被替换为硬件寄存器fp/sp + 一个固定偏移.
   * 被展开的变量的rtl表达式类似: (mem (plus virtual_stack_vars_rtx, offset))
   * 栈向低地址增长时(即通常,或aarch64下)offset是个负数, 此offset对于每个变量均不同(根据已分配情况计算出来的).
   需要注意的是延迟分配和空间优化可能会导致局部变量分配顺序和定义顺序不同(这里不再展开细节)。
*/
static rtx_insn * expand_used_vars (void)
{
  ......
  FOR_EACH_LOCAL_DECL (cfun, i, var)
  {
    bool expand_now = false;
    .......
    if (expand_now)							/* 对于需要立即展开的变量, 直接为其在局部变量栈分配空间 */
      expand_one_var (var, true, true);
  }

  if (stack_vars_num > 0)
  {
    ......
    expand_stack_vars (NULL, &data);		/* 对于没有立即展开的变量, 这里统一在局部变量栈为其分配空间 */
  }
  .......
}

/* 逻辑上展开一个变量, 此变量可能是通过寄存器直接展开/直接展开到栈/延迟展开 */
static poly_uint64
expand_one_var (tree var, bool toplevel, bool really_expand)
{
  unsigned int align = BITS_PER_UNIT;
  tree origvar = var;
  ......
  else if (defer_stack_allocation (var, toplevel))
    add_stack_var (origvar);						/* 若延迟分配,则将此变量记录到stack_vars数组中,后续expand_stack_vars一并分配 */
  else
    {
      if (really_expand)
      {
          expand_one_stack_var (origvar);			/* 在栈上立即展开(分配) */
      }
      
      return size;									/* 立即分配返回分配空间大小 */
    }
  return 0;											/* 没有立即分配则返回 0 */
}

//expand_one_stack_var => expand_one_stack_var_1
static void
expand_one_stack_var_1 (tree var)
{ 
  .......  
  /* 计算此变量的offset,并最终展开为:  (mem (plus virtual_stack_vars_rtx, offset)) */
  offset = alloc_stack_frame_space (size, byte_align);
  expand_one_stack_var_at (var, virtual_stack_vars_rtx, crtl->max_used_stack_slot_alignment, offset);
}

/* 此函数负责为没有立即展开的局部变量分配栈空间 */
static void
expand_stack_vars (bool (*pred) (size_t), struct stack_vars_data *data)
{
  size_t si, i, j, n = stack_vars_num;		/* n 为之前mark但没有立即在栈中展开的变量的数量 */
  .......

  for (si = 0; si < n; ++si)				/* 此过程和 expand_one_stack_var_1 类似 */
  {
    rtx base;
    .......
    base = virtual_stack_vars_rtx;			/* offset基于 virtual_stack_vars_rtx */
    ......
    /* 在前面已经分配的局部变量空间基础上为此变量预留栈空间, 这里返回的offset是一个负数 */
    offset = alloc_stack_frame_space (stack_vars[i].size, alignb);
    ......
    /* 最终表达式同样类似: (mem (plus virtual_stack_vars_rtx, offset)) */
	expand_one_stack_var_at (stack_vars[j].decl, base, base_align, offset);
  }
}

3.2 callee传入参数的处理

  传入参数的处理是在 expand_function_start 中完成的,[1]中有过介绍,这里同样只提及关键步骤:

//expand_function_start 中通过调用 assign_parms 处理传入参数
/*
   assign_parms流程分为三部分:
   1. 确定所有命名参数的来源, 按照AAPCS64标准:
      * 所有命名寄存器参数都来自某个寄存器, 对应表达式: rtx(REG no), no为标准中对应的硬件寄存器编号
      * 所有命名栈参数都来自incomming栈, 对应表达式: (mem (plus virtual_incoming_args_rtx, offset))
      DECL_INCOMING_RTL(parm) 记录此命名参数的来源位置.
   2. 确定在callee中这些命名传入参数的存储位置
      * 若开启-Ox优化, 则所有命名寄存器/栈参数在callee中都保存到伪寄存器中, 对应表达式: rtx(Reg no), no为一个伪寄存器编号
      * 若未开启优化, 则:
        - 所有命名寄存器参数都类似局部变量一样分配空间,对应表达式: mem(plus(virtual_stack_vars_rtx, frame_offset)), frame_offset 为负数, 是局部变量栈中的偏移
        - 所有命名栈参数都使用incoming栈中的位置,不必重新分配,对应表达式: (mem (plus virtual_incoming_args_rtx, offset))
      最终如果为某参数分配了位置,则要发送对应的mov指令将此参数从传入参数位置复制到callee的存储位置.
      DECL_RTL(parm) 记录此参数在callee中的最终存储位置.
   3. 若当前是不定参函数,则还需为所有可能的匿名寄存器参数预留存储空间,并无条件发送mov指令复制所有可能出现的匿名寄存器参数=>匿名寄存器参数区域;
      匿名栈参数和普通栈参数的存储位置没有区别,均是依次来自incoming区域, 此时需要:
      * 先在函数栈中为GPRs/FPRs中可能出现的所有匿名参数预留空间
      * 发射mov指令将所有可能的匿名寄存器均复制到匿名寄存器参数区域, 以确保不论caller传入多少个参数, callee中都能正确存储.
      最终预留的栈空间:
      * [virtual_incoming_args_rtx - gr_saved * 8				 , virtual_incoming_args_rtx				  ] 为所有匿名GPR参数预留
      * [virtual_incoming_args_rtx - gr_saved * 8 - vr_saved * 8 , virtual_incoming_args_rtx - gr_saved * 8] 	为所有匿名VPR参数预留(这里忽略了对齐)
*/
static void assign_parms (tree fndecl)
{
  struct assign_parm_data_all all;						/* 此结构体记录当前函数的所有命名参数信息 */
  /* 参数结点(tree_parm_decl parm)中:
     * DECL_INCOMING_RTL(parm)记录此命名参数的传入位置(incoming栈/寄存器编号)
     * DECL_RTL(parm) 记录此命名参数在当前函数中的存储位置(伪寄存器/某内存位置)
  */
  tree parm;
  vec<tree> fnargs;

  /* 通常这里是标记位virtual_incoming_args_rtx, 代表传入栈参数区域的首地址(caller的outging区域首地址, callee的incoming区域首地址) */
  crtl->args.internal_arg_pointer = targetm.calls.internal_arg_pointer ();
  ......
  fnargs = assign_parms_augmented_arg_list (&all);		/* 此函数的每个命名参数都记录到此数组中 */

  FOR_EACH_VEC_ELT (fnargs, i, parm)					/* 遍历所有命名参数 */
  {
    struct assign_parm_data_one data;					/* 记录当前正在分析的参数的信息 */
    assign_parm_find_data_types (&all, parm, &data);	/* 确定此参数的类型和机器类型 */
    .......
        
    //part1. 确定所有命名参数来源位置, 此表达式最终被记录到每个参数的 DECL_INCOMING_RTL(parm)中
    /* 
      根据参数类型、AAPCS64标准和已有的分析结果确定此传入参数的来源:
      * 若参数应该通过硬件寄存器传入,则data->entry_parm 为代表硬件寄存器编号的rtx(REG)表达式
      * 若参数应该来自caller栈, 则date->locate (计算并)返回此参数基于incoming栈的偏移
    */
    assign_parm_find_entry_rtl (&all, &data);
     
    /* 对于栈参数, 将此偏移转换为基于标记位virtual_incoming_args_rtx的位置表达式, 即:
       data->entry_parm = (mem (plus virtual_incoming_args_rtx, offset));
    */
    if (assign_parm_is_stack_parm (&all, &data))
	{
	  assign_parm_find_stack_rtl (parm, &data);		/* data->stack_parm = (mem (plus virtual_incoming_args_rtx, offset)); */
	  assign_parm_adjust_entry_rtl (&data);			/* data->entry_parm = data->stack_parm; */
	}
	......
    /* 将所有命名参数的来源位置表达式保存在 DECL_INCOMING_RTL(parm)中:
      * 对于命名寄存器参数, 通过一个rtx(REG)表达式记录其来自哪个硬件寄存器(编号)
      * 对于命名栈参数, 通过一个(mem (plus virtual_incoming_args_rtx, offset)) 表达式记录其逻辑位置
    */
	set_decl_incoming_rtl (parm, data.entry_parm, false);

      
    //part2. 确定所有命名参数在callee中的存储位置, 并将结果保存到每个参数的DECL_RTL(parm)中
	......
	else if (data.passed_pointer || use_register_for_decl (parm))
      /* 开启-Ox优化时通常走这里, 所有的命名传入参数都被保存到一个伪寄存器中.
         此时在callee栈无需为任何传入参数分配存储空间, 此函数最终设置所有参数的:
         DECL_RTL(parm) = rtx(reg n);	n是一个伪寄存器编号  
         (注意: 后面伪寄存器消除过程中若需要使用栈做临时存储,还有可能会按需分配空间)
      */
	  assign_parm_setup_reg (&all, parm, &data);
	else
      /* 若未开启编译优化通常走这里, 所有传入的命名参数均会在这里落实到一个存储位置:
         * 对于寄存器参数, 会在局部变量区域为其分配空间(并发射对应mov指令),最终:
           DECL_RTL(parm) = mem(plus(virtual_stack_vars_rtx, frame_offset));	//这里frame_offset为负数,类似局部变量展开
         * 对于栈参数, 直接使用其caller传入参数的位置即可,最终:
           DECL_RTL(parm) = (mem (plus virtual_incoming_args_rtx, offset));		//这里offset是正数, 为incoming栈中此参数偏移
      */
	  assign_parm_setup_stack (&all, parm, &data);

    //part3. 所有命名参数解析完毕后若发现当前函数拥有可变参数(...), 则需要为(GPR/FPR中的)匿名寄存器参数预留空间.
    //同时还需发射对应的mov指令将这些匿名寄存器参数复制到其栈空间中
    if (cfun->stdarg && !DECL_CHAIN (parm))
		assign_parms_setup_varargs (&all, &data, false);
  }
}

//aarch64平台:
//assign_parms_setup_varargs => targetm.calls.setup_incoming_varargs => aarch64_setup_incoming_varargs
/*
   负责确定当前函数匿名寄存器参数占用的空间, 并发射mov指令确保每个匿名寄存器参数都复制到其对应的存储空间中.
   对于一个不定参函数, 每次调用时匿名通用/浮点寄存器参数的个数是其caller决定的,但根据AAPCS64标准各最多(8 - 命名参数个数)个.
   在预留时需要考虑到最多的情况,所以需要为每个可能的匿名寄存器参数预留空间:
   * 对于GRR来说,这段空间是 [virtual_incoming_args_rtx - gr_saved * 8, virtual_incoming_args_rtx ],
     第一个匿名寄存器参数保存在 virtual_incoming_args_rtx - gr_saved * 8;
   * 对于FPR, 这段空间是 [virtual_incoming_args_rtx - gr_saved * 8 - vr_saved * 16, virtual_incoming_args_rtx - gr_saved * 8](忽略对齐),
     第一个匿名寄存器参数保存在 virtual_incoming_args_rtx - gr_saved * 8 - vr_saved * 8;
*/
static void
aarch64_setup_incoming_varargs (cumulative_args_t cum_v, machine_mode mode,
				tree type, int *pretend_size ATTRIBUTE_UNUSED, int no_rtl)
{
  ......
  /* 对于不定参函数, 若未开启-fstdarg-opt优化或函数内调用了 va_xxx, 
    则必须要为此函数中所有可能的匿名寄存器参数预留空间. */
  int gr_saved = cfun->va_list_gpr_size;
  int vr_saved = cfun->va_list_fpr_size;

  /* 根据AAPCS64标准, arm64中通用寄存器参数最多为8个(NUM_ARG_REGS), 而此函数已有的命名寄存器
    参数为local_cum.aapcs_ncrn个,  gr_saved 为此函数可能出现的匿名寄存器参数的最大值, 如
    int main (int p0, int p1, ...);  函数中可能出现的最大匿名寄存器为 8-2 = 6个.
  */
  if (cfun->va_list_gpr_size)
    gr_saved = MIN (NUM_ARG_REGS - local_cum.aapcs_ncrn, cfun->va_list_gpr_size / UNITS_PER_WORD);
   
  /* V0-V7 同理 */
  if (cfun->va_list_fpr_size)
    vr_saved = MIN (NUM_FP_ARG_REGS - local_cum.aapcs_nvrn, cfun->va_list_fpr_size / UNITS_PER_VREG);
  .......
  if (gr_saved > 0)
  {
    rtx ptr, mem;
      
	/* 匿名通用寄存器参数存储位置是 [virtual_incoming_args_rtx - gr_saved * 8, virtual_incoming_args_rtx] (未考虑对齐),
      按照寄存器编号由低地址=>高地址存储, 首个匿名寄存器存储位置为 rtx(mem(virtual_incoming_args_rtx - gr_saved * 8))
    */
    ptr = plus_constant (Pmode, virtual_incoming_args_rtx, - gr_saved * UNITS_PER_WORD);
    mem = gen_frame_mem (BLKmode, ptr);
    ......

    /* 发送 mov指令将所有可能的匿名通用寄存器参数复制到 [virtual_incoming_args_rtx - gr_saved * 8, virtual_incoming_args_rtx] */
    move_block_from_reg (local_cum.aapcs_ncrn + R0_REGNUM, mem, gr_saved);
  }
  //FPRs类似
  if (vr_saved > 0)
  {
    ......
  }

  /* 计算所有匿名寄存器参数存储空间大小(对齐), 即varargs需要的空间(最终栈空间分配参考aarch64_layout_frame)  */
  cfun->machine->frame.saved_varargs_size = (ROUND_UP (gr_saved * UNITS_PER_WORD,
       STACK_BOUNDARY / BITS_PER_UNIT) + vr_saved * UNITS_PER_VREG);
}

四、callee-saved register

  callee-saved register区域是用来保存 callee-saved register的, 按照AAPCS64标准 [r19, r28] 是callee-saved register,这些寄存器在子函数返回时要保持不变,也就是说如果子函数中使用了这些寄存器,则子函数需要保存并在返回前恢复其原始值。 函数栈中callee-saved register区域就是用来保存这些寄存器原始值的。callee-saved register区域的计算和寄存器保存发生在aarch64_layout_frame和pro/epilogue生成阶段,在后面一并介绍。

五、动态内存区域分配

  alloca函数可用于动态分配,其实际上是gcc的一个builtin函数:

//#include <alloca.h>

#ifdef  __GNUC__
# define alloca(size)   __builtin_alloca (size)
#endif /* GCC.  */ 

  __builtin_alloca 和 __builtin_va_start 函数的流程相同(见[5]),都是在 builtins.def中定义并最终在 expand_builtin中展开:

./gcc/builtins.def
DEF_EXT_LIB_BUILTIN    (BUILT_IN_ALLOCA, "alloca", BT_FN_PTR_SIZE, ATTR_ALLOCA_SIZE_1_NOTHROW_LEAF_LIST)

./gcc/builtins.c
rtx expand_builtin (tree exp, rtx target, rtx subtarget, machine_mode mode, int ignore)
{
    tree fndecl = get_callee_fndecl (exp);
    enum built_in_function fcode = DECL_FUNCTION_CODE (fndecl);
    switch (fcode)
    {
      case BUILT_IN_ALLOCA:
          target = expand_builtin_alloca (exp);				/* __builtin_alloca 函数最终在此展开 */
          if (target)
            return target;
      break;
      .......
    }
    .......
}

expand_builtin_alloca:

/* 此函数负责展开 __builtin_alloca(size); 这里的exp是一个CALL_EXPR, 被调用函数即为 __builtin_alloca. 
   展开后的伪代码类似:
     sp = sp - size;
     result = sp + outgoing_arg_size;
     return result;
     返回的result为在栈中动态分配的空间的首地址.
*/
static rtx expand_builtin_alloca (tree exp)
{
  rtx op0;
  rtx result;
  ......
  op0 = expand_normal (CALL_EXPR_ARG (exp, 0));		/* 先expand __builtin_alloca 的参数0(size),对应的rtx表达式返回到op0 */
    
  /* aarch64 alloca 要16 byte对齐 */
  align = (fcode == BUILT_IN_ALLOCA ? BIGGEST_ALIGNMENT : TREE_INT_CST_LOW (CALL_EXPR_ARG (exp, 1)));
  .......
      
  /* result = sp = sp - op0; return result; 最终返回的result为在栈中动态分配的空间的首地址 */
  result = allocate_dynamic_stack_space (op0, 0, align, max_size, alloca_for_var);
  ......
  return result;
}

/*
  此函数返回的rtx表达式代表alloca的分配结果,其返回的总是align后的virtual_stack_dynamic_rtx
  * 如果size 是0则无需任何操作直接返回virtual_stack_dynamic_rtx即可.
  * 如果size 非0, 则:
    1) 标记cfun->calls_alloca = 1;
    2) 发射指令 sp = sp - size;
    3) 返回表达式 target = virtual_stack_dynamic_rtx (最终被替换为 sp + outgoing_arg_size);
  每次都返回 virtual_stack_dynamic_rtx,并不意味着每次alloca都分配到同一个地址, 由于
 virtual_stack_dynamic_rtx最终被映射为 sp + outging_arg_size, 而sp在步骤2)中已经调整过,
 故运行时每次alloca返回的地址总是动态变化的.
*/
rtx allocate_dynamic_stack_space (rtx size, unsigned size_align,
			      unsigned required_align, HOST_WIDE_INT max_size, bool cannot_accumulate)
{
  rtx target;

  /* 若size=0则直接返回 virtual_stack_dynamic_rtx, 也就是说 alloca(0) 通常会直接返回callee-saved regs区域首地址 */
  if (size == const0_rtx)
    return virtual_stack_dynamic_rtx;

  cfun->calls_alloca = 1;			/* 如果alloca分配大小非0, 则要标记此函数中出现了动态分配了内存 */		

  target = gen_reg_rtx (Pmode);		/* 分配一个指针类型的伪寄存器 */ 
  ......
  anti_adjust_stack (size);			/* 发射sp = sp - size;指令在栈中动态预留空间,对应汇编代码通常为 sub sp, sp, Rx */	

  /* 发射指令 target = virtual_stack_dynamic_rtx; 由于virtual_stack_dynamic_rtx最终总是被替换为 sp + outgoing_arg_size,
    故虽然每次返回的都是 virtual_stack_dynamic_rtx,但由于前面 已经做了sp-=size;操作,其每次返回的地址总是变化的;
     对应伪代码如  result = sp + outgoing_arg_size; return result;
  */
  if (STACK_GROWS_DOWNWARD)
	emit_move_insn (target, virtual_stack_dynamic_rtx);

  target = align_dynamic_address (target, required_align);	/* 16 byte align并返回 */
  ......
  return target;
}

  alloca和outgoing区域并不冲突, 当一个函数中存在outgoing区域同时也调用了alloca函数时,随着程序的执行其sp可能一直在变化,但:

  • outgoing区域总是在[sp, sp + crtl->outgoing_args_size].
  • 此每次调用alloca分配的内存总是(执行完 sp = sp - size; 之后的) [sp + outgoing_args_size, sp + crtl->outgoing_args_size + size].

六、gcc中虚拟寄存器与硬件寄存器的转换

  在gimple=>rtl阶段(pass_expand), gcc中先使用了多个虚拟寄存器来标记栈中的位置以便于指令生成, 包括:

/*
  * virtual_incoming_args_rtx: 标记当前函数传入参数栈的首地址,对应伪寄存器编号 VIRTUAL_INCOMING_ARGS_REGNUM.
  * virtual_stack_vars_rtx: 标记当前函数第一个局部变量的尾地址(不是首地址),对应伪寄存器编号为 VIRTUAL_STACK_VARS_REGNUM.
  * virtual_stack_dynamic_rtx: 标记当前函数执行alloca动态分配栈空间时分配到的首地址,对应伪寄存器编号 VIRTUAL_STACK_DYNAMIC_REGNUM.
  * virtual_outgoing_args_rtx: 标记当前函数outgoing栈的首地址, 对应伪寄存器编号 VIRTUAL_OUTGOING_ARGS_REGNUM.
  * virtual_cfa_rtx: 标记当前函数CFA(Caonical Frame Address)的首地址, 对应伪寄存器编号 VIRTUAL_CFA_REGNUM.
*/ 

这些虚拟寄存器最终会被映射到某个硬件寄存器(+offset)中, 此转换发生在两个过程中:

1) pass_instantiate_virtual_regs:

     此pass负责虚拟寄存器消除,其主要将所有虚拟寄存器转换基于stack_pointer_rtx(31)/arg_pointer_rtx(65)/frame_pointer_rtx (64)的表达式.
2) pass_reload:

    此pass中将arg_pointer_rtx(65)/frame_pointer_rtx (64) 转换为真正的硬件寄存器,在aarch64生成的最终代码中只通过两个硬件寄存器体现位置:

  • stack_pointer_rtx(31): 代表当前硬件寄存器sp
  • hard_frame_pointer_rtx(29): 代表当前硬件寄存器 fp

6.1 虚拟寄存器消除

  pass_instantiate_virtual_regs 负责虚拟寄存器消除, 关键代码如下:

/* 此函数首先确定各个虚拟寄存器如何映射到
   stack_pointer_rtx(31)/arg_pointer_rtx(65)/frame_pointer_rtx(64)
   然后遍历所有指令做寄存器替换.
*/
static unsigned int instantiate_virtual_regs (void)
{
  rtx_insn *insn;
    
  /* FIRST_PARM_OFFSET 是arg_pointer到第一个传入栈参数(incoming arg)之间的偏移, 在aarch64中其总是0 */
  in_arg_offset = FIRST_PARM_OFFSET (current_function_decl);
    
  /* TARGET_STARTING_FRAME_OFFSET 为当前平台第一个局部变量尾地址到 frame_pointer_rtx之间的偏移,aarch64中总是0 */
  var_offset = targetm.starting_frame_offset ();
    
  /* dynamic_offset = crtl->outgoing_args_size + STACK_POINTER_OFFSET; 其中:
     * outgoing_args_size 是当前函数outgoing栈的大小(编译时确定)
     * STACK_POINTER_OFFSET 是硬件寄存器sp到第一个outgoing参数首地址的偏移, 在aarch64中总是0
  */
  dynamic_offset = STACK_DYNAMIC_OFFSET (current_function_decl);
  out_arg_offset = STACK_POINTER_OFFSET;						/*  同上 */
    
  /* cfa_offset = FIRST_PARM_OFFSET (FNDECL) + crtl->args.pretend_args_size, 其中:
    * FIRST_PARM_OFFSET 为arg_pointer到第一个incoming栈参数之间的偏移,aarch64中总是0(如上)
    * pretend_args_size 是函数prologue中应该先push的大小, aarch64中总是为0  
  */
  cfa_offset = ARG_POINTER_CFA_OFFSET (current_function_decl);
  
  ......
  /* 遍历当前函数的整个rtx指令序列中的 INSN/JUMP_INSN/CALL_INSN/DEBUG_INSN 指令 */
  for (insn = get_insns (); insn; insn = NEXT_INSN (insn))
    if (INSN_P (insn))
    {
      ...... 
	  instantiate_virtual_regs_in_insn (insn);		/* 根据规则替换虚拟寄存器 */
      ......
    }
  ......
  return 0;
}

//instantiate_virtual_regs_in_insn => instantiate_new_reg 中确定虚拟寄存器的替换规则
/* 最终替换规则为:
   virtual_incoming_args_rtx => arg_pointer_rtx + in_arg_offset
   virtual_stack_vars_rtx 	 => frame_pointer_rtx + var_offset
   virtual_stack_dynamic_rtx => stack_pointer_rtx + dynamic_offset
   virtual_outgoing_args_rtx => stack_pointer_rtx + out_arg_offset
   virtual_cfa_rtx 		     => arg_pointer_rtx + cfa_offset
*/
static rtx instantiate_new_reg (rtx x, poly_int64_pod *poffset)
{
  rtx new_rtx;
  poly_int64 offset;

  if (x == virtual_incoming_args_rtx)			/* virtual_incoming_args_rtx => arg_pointer_rtx + in_arg_offset */
  {
    ......
    new_rtx = arg_pointer_rtx, offset = in_arg_offset;
  }
  else if (x == virtual_stack_vars_rtx)			/* virtual_stack_vars_rtx => frame_pointer_rtx + var_offset */
    new_rtx = frame_pointer_rtx, offset = var_offset;
  else if (x == virtual_stack_dynamic_rtx)		/* virtual_stack_dynamic_rtx => stack_pointer_rtx + dynamic_offset */
    new_rtx = stack_pointer_rtx, offset = dynamic_offset;
  else if (x == virtual_outgoing_args_rtx)		/* virtual_outgoing_args_rtx => stack_pointer_rtx + out_arg_offset */
    new_rtx = stack_pointer_rtx, offset = out_arg_offset;
  else if (x == virtual_cfa_rtx)				/* virtual_cfa_rtx => arg_pointer_rtx + cfa_offset */
  {
    ......
    new_rtx = arg_pointer_rtx;
    offset = cfa_offset;
  }
  .......
  *poffset = offset;
  return new_rtx;
}

  pass_instantiate_virtual_regs 中最终虚拟寄存器的替换规则:

* virtual_incoming_args_rtx   => arg_pointer_rtx
* virtual_stack_vars_rtx 	  => frame_pointer_rtx
* virtual_stack_dynamic_rtx   => stack_pointer_rtx + crtl->outgoing_args_size
  这里的stack_pointer_rtx为硬件寄存器sp,运行时每次调用alloca函数时都会调整sp, 
  故虽然每次alloca的地址都是virtual_stack_dynamic_rtx,但运行时每次分配到的真实栈地址均不同.
* virtual_outgoing_args_rtx   => stack_pointer_rtx
  类似virtual_stack_dynamic_rtx,最终映射到硬件寄存器sp, 故一个函数中如果存在动态栈分配,则运行时outgoing首地址可能会动态变化.
* virtual_cfa_rtx 		      => arg_pointer_rtx

6.2 转换为平台相关的硬件寄存器

  在pass_reload中会将arg_pointer_rtx(65)/frame_pointer_rtx (64)转换为真正的硬件寄存器,此过程发生在:

pass_reload => do_reload => lra => lra_constraints => lra_eliminate

6.2.1 替换规则数组

  lra_eliminate => init_elim_table 中先初始化了一个替换规则数组lra_elim_table[NUM_ELIMINABLE_REGS], 在aarch64中展开后如下:

/* 
  此结构体记录从寄存器编号from(如ARG_POINTER_REGNUM) 到寄存器编号to(如STACK_POINTER_REGNUM)的转换规则
*/
struct lra_elim_table
{
  int from;				/* 要被消除的寄存器 */		
  int to;				/* 被替换为哪个寄存器 */
  ......
  poly_int64 offset;	/* 二者的替换偏移 */
  bool can_eliminate;	/* 当前替换规则是否使用于当前函数 */
  ......
};

/* 最终初始化后:
  lra_elim_table[0]: [From => To]: ARG_POINTER_REGNUM => STACK_POINTER_REGNUM
  lra_elim_table[1]: [From => To]: ARG_POINTER_REGNUM => HARD_FRAME_POINTER_REGNUM
  lra_elim_table[2]: [From => To]: FRAME_POINTER_REGNUM=> STACK_POINTER_REGNUM
  lra_elim_table[3]: [From => To]: FRAME_POINTER_REGNUM=> HARD_FRAME_POINTER_REGNUM
*/ 
static struct lra_elim_table *reg_eliminate = lra_elim_table[4];

  在寄存器消除时, 每次替换时会按照 0-3 的顺序逐个尝试, 使用最先匹配的规则. 如当替换 ARG_POINTER_REGNUM时:

  • 若对于当前函数可以做 ARG_POINTER_REGNUM => STACK_POINTER_REGNUM 替换(即lra_elim_table[0].can_eliminate = 1),则优先将arg_pointer替换为硬件寄存器sp
  • 若不可以则将 arg_pointer替换为硬件寄存器sp (使用lra_elim_table[1]中的规则)

  在aarch64平台:

  • 如果当前函数需要栈帧(frame_pointer_needed = 1), 则arg_pointer/frame_pointer只能替换为fp.
  • 如果当前函数不需要栈帧(frame_pointer_needed = 0), 则arg_pointer/frame_pointer可以替换为sp/fp, 但按照优先级顺序最终会被替换为 sp.

6.2.2 替换偏移量的计算

  init_elimination => update_reg_eliminate中计算了每种替换中的偏移:

static bool update_reg_eliminate (bitmap insns_with_changed_offsets)
{
  ......
  /* 遍历替换规则数组并计算每种替换的最终偏移并保存在reg_eliminate[x].offset中 */
  for (ep = reg_eliminate; ep < &reg_eliminate[NUM_ELIMINABLE_REGS]; ep++)
  {
    ......
    /* 展开后: ep->offset = aarch64_initial_elimination_offset(ep->from, ep->to); */
    INITIAL_ELIMINATION_OFFSET (ep->from, ep->to, ep->offset);
  }
  ......
  return result;
}

/*
   此函数计算最终寄存器替换时的偏移:
   frame_pointer_rtx(64) = stack_pointer_rtx(31) + frame_size - locals_offset;
   frame_pointer_rtx(64) = hard_frame_pointer_rtx(29) + hard_fp_offset - locals_offset;
   arg_pointer_rtx(65) = stack_pointer_rtx(31) + hard_fp_offset;
   arg_pointer_rtx(65) = hard_frame_pointer_rtx(29) + hard_fp_offset;
*/
poly_int64 aarch64_initial_elimination_offset (unsigned from, unsigned to)
{
  if (to == HARD_FRAME_POINTER_REGNUM)
  {
    if (from == ARG_POINTER_REGNUM)
	  return cfun->machine->frame.hard_fp_offset;

    if (from == FRAME_POINTER_REGNUM)
	  return cfun->machine->frame.hard_fp_offset - cfun->machine->frame.locals_offset;
  }

  if (to == STACK_POINTER_REGNUM)
  {
      if (from == FRAME_POINTER_REGNUM)
	    return cfun->machine->frame.frame_size - cfun->machine->frame.locals_offset;
  }

  return cfun->machine->frame.frame_size;
}

6.3 最终的寄存器替换规则

  综合以上代码,最终寄存器替换规则为:

- virtual_incoming_args_rtx   => arg_pointer_rtx
- virtual_stack_vars_rtx 	  => frame_pointer_rtx
- virtual_stack_dynamic_rtx   => stack_pointer_rtx + crtl->outgoing_args_size
- virtual_outgoing_args_rtx   => stack_pointer_rtx
- virtual_cfa_rtx 		      => arg_pointer_rtx

* frame_pointer_needed = 0:
  - arg_pointer_rtx = stack_pointer_rtx + frame_size;
  - frame_pointer_rtx = stack_pointer_rtx + frame_size - locals_offset;

* frame_pointer_needed = 1;
  - arg_pointer_rtx = hard_frame_pointer_rtx + hard_fp_offset;
  - frame_pointer_rtx = hard_frame_pointer_rtx + hard_fp_offset - locals_offset;

其中:
* frame_size为 cfun->machine->frame.frame_size
* locals_offset为 cfun->machine->frame.locals_offset
* frame_pointer_needed 为1的条件(ira_setup_eliminable_regset):
  frame_pointer_needed = 
    (! flag_omit_frame_pointer
    || (cfun->calls_alloca && EXIT_IGNORE_STACK)
    || (STACK_CHECK_MOVING_SP && flag_stack_check && ...
    || crtl->accesses_prior_frames
    || (SUPPORTS_STACK_ALIGNMENT && crtl->stack_realign_needed)
    || targetm.frame_pointer_required ());

  替换为fp/sp举例:

//main.c
int func2(int x1)
{
        __builtin_frame_address(0);
        return 1;
}

int func3(int x1)
{
        return 1;
}

tangyuan@ubuntu:~/tests/gcc/aarch64_stack/alloca_test$ ./make.sh  2>&1|tee 1.log
func:func2 frame_pointer_needed:1							## 编译器中插入的日志
func:func3 frame_pointer_needed:0

## aarch64-linux-gnu-gcc main.c -O0 -S -o main.s
func2:
        stp     x29, x30, [sp, -32]!
        mov     x29, sp
        str     w0, [x29, 28] 			## func2中 x1基于fp寻址
        mov     w0, 1
        ldp     x29, x30, [sp], 32
        ret 
func3:
        sub     sp, sp, #16 
        str     w0, [sp, 12] 			## func3中 x1基于sp寻址
        mov     w0, 1
        add     sp, sp, 16
        ret                  

七、函数栈布局的确定与pro/epilogue指令生成

  函数栈最终的布局是在aarch64_layout_frame中执行的,此函数在编译过程中被执行了多次(通常栈帧发生变化时需要重新计算)。此函数中还同时计算出需要为callee-saved register预留多少空间,并确定最终pro/epilogue中应该如何生成指令(在pro/epilogue生成的过程中, aarch64_layout_frame 负责确定指令如何生成, pro/epilogue负责按照要求生成指令)。

7.1 函数栈布局

  aarch64_layout_frame 函数首先确定此函数中有哪些寄存器需要callee-saved,并为每个寄存器计算出其在callee-saved reg区域的存储位置(offset), 然后根据栈中不同区域是否存在以及大小确定pro/epilogue的最优指令选择:

static void aarch64_layout_frame (void)
{
  HOST_WIDE_INT offset = 0;
  int regno, last_fp_reg = INVALID_REGNUM;
  bool simd_function = aarch64_simd_decl_p (cfun->decl);

  /* 确定当前函数的pro/epilogue中是否需要frame chain, 需要frame chain则意味着函数中要 save/restore LR(x30)/FP(x29) */
  cfun->machine->frame.emit_frame_chain = aarch64_needs_frame_chain ();
  crtl->outgoing_args_size = STACK_DYNAMIC_OFFSET (cfun);	/* outgoing_args_size 自身的padding, 代表此函数中outging区域大小 */

#define SLOT_NOT_REQUIRED (-2)		/* 代表此寄存器不需要保存 */
#define SLOT_REQUIRED     (-1)		/* 代表此寄存器需要被保存,但尚未计算偏移 */

  /* step1: 标记那些需要被callee-saved的寄存器并计算其偏移 */
    
  /* step1.1: 先标记所有寄存器均无需保存 */
  /* 若最终发射stp!/str!指令, 则candidate1/2为其附属参数, 其若非空则记录前两个callee-saved register 编号,初始化为空 */
  cfun->machine->frame.wb_candidate1 = INVALID_REGNUM;		
  cfun->machine->frame.wb_candidate2 = INVALID_REGNUM;
    
  for (regno = R0_REGNUM; regno <= R30_REGNUM; regno++)			/* 标记所有通用寄存器无需保存 */
    cfun->machine->frame.reg_offset[regno] = SLOT_NOT_REQUIRED;

  for (regno = V0_REGNUM; regno <= V31_REGNUM; regno++)			/* 标记所有浮点寄存器无需保存 */
    cfun->machine->frame.reg_offset[regno] = SLOT_NOT_REQUIRED;
  ......

  /* step1.2: 确定哪些寄存器是需要callee-saved */
  /* 若当前函数中使用了LR/[r19, r28],则标记这些寄存器需要保存, 注意x29不属于callee-saved register */
  for (regno = R0_REGNUM; regno <= R30_REGNUM; regno++)
    if (df_regs_ever_live_p (regno) && (regno == R30_REGNUM || !call_used_regs[regno]))
      cfun->machine->frame.reg_offset[regno] = SLOT_REQUIRED;

  for (regno = V0_REGNUM; regno <= V31_REGNUM; regno++)		/* 浮点寄存器同理 */
    if (df_regs_ever_live_p (regno) && (!call_used_regs[regno] || (simd_function && FP_SIMD_SAVED_REGNUM_P (regno))))
	  cfun->machine->frame.reg_offset[regno] = SLOT_REQUIRED;

  /* 若当前函数需要frame chain, 则callee-saved 区域必须先保存x29, x30;
     这里先为x29, x30确定其在callee-saved 区域的偏移(简单的累加),并将这两个寄存器编号记录到
     wb_candidate1/2中以备后续stp!指令使用(若不需要frame chain时, x30被当做普通寄存器,按照编号顺序保存).
  */
  if (cfun->machine->frame.emit_frame_chain)
  {
      cfun->machine->frame.reg_offset[R29_REGNUM] = 0;
      cfun->machine->frame.wb_candidate1 = R29_REGNUM;
      cfun->machine->frame.reg_offset[R30_REGNUM] = UNITS_PER_WORD;
      cfun->machine->frame.wb_candidate2 = R30_REGNUM;
      offset = 2 * UNITS_PER_WORD;					/* offset累加计算当前寄存器保存到callee-saved 区域的偏移 */
  }
  ......
  /*
     为所有需要需要callee-saved寄存器确定存储偏移(offset),此时若wb_candidate1/2
    中还没有寄存器 则将最先分配的两个记录其中, 浮点寄存器同理,这里省略.
  */
  for (regno = R0_REGNUM; regno <= R30_REGNUM; regno++)
    if (cfun->machine->frame.reg_offset[regno] == SLOT_REQUIRED)
    {
        cfun->machine->frame.reg_offset[regno] = offset;
        
        if (cfun->machine->frame.wb_candidate1 == INVALID_REGNUM)
          cfun->machine->frame.wb_candidate1 = regno;
        else if (cfun->machine->frame.wb_candidate2 == INVALID_REGNUM)
          cfun->machine->frame.wb_candidate2 = regno;
        offset += UNITS_PER_WORD;
    }
  ......
      
  cfun->machine->frame.saved_regs_size = offset;		/* 记录当前函数callee-saved寄存器最终占用多少空间(aligned) */

  /* hard_fp_offset 是除了outgoing区域外此函数栈的大小,包括 匿名寄存器区域 + 局部变量区域 + callee-saved reg区域 */
  HOST_WIDE_INT varargs_and_saved_regs_size = offset + cfun->machine->frame.saved_varargs_size;
  cfun->machine->frame.hard_fp_offset
    = aligned_upper_bound (varargs_and_saved_regs_size + get_frame_size (), STACK_BOUNDARY / BITS_PER_UNIT);
  ......

  /* frame_size是编译期间确定的此函数栈帧的总大小 */
  cfun->machine->frame.frame_size = (cfun->machine->frame.hard_fp_offset + crtl->outgoing_args_size);

  /* 局部变量首地址偏移取决于匿名寄存器参数占用空间大小 */
  cfun->machine->frame.locals_offset = cfun->machine->frame.saved_varargs_size;

  /* step2: 确定pro/epilogue中如何生成指令 */
  cfun->machine->frame.initial_adjust = 0;     
  cfun->machine->frame.final_adjust = 0;
  cfun->machine->frame.callee_adjust = 0;
  cfun->machine->frame.callee_offset = 0;

  /* 若candidate只有一个, 此时若使用带!的指令发射则需用str!指令发射; 有两个则需用stp!发射;
    由于二者可访问的地址范围不同, 这里max_push_offset用来确定此时带!的指令可访问的地址范围.
  */
  HOST_WIDE_INT max_push_offset = 0;
  if (cfun->machine->frame.wb_candidate2 != INVALID_REGNUM)
    max_push_offset = 512;
  else if (cfun->machine->frame.wb_candidate1 != INVALID_REGNUM)
    max_push_offset = 256;

  HOST_WIDE_INT const_size, const_fp_offset;

  if (cfun->machine->frame.frame_size.is_constant (&const_size)				//case 1
      && const_size < max_push_offset		
      && known_eq (crtl->outgoing_args_size, 0))
  {
    /* 构建pro/epilogue时优先选用此模板(case 1),此时prologue中第一条指令为:
       stp Rn, Rm, [sp, -callee_adjust]! / str Rn, [sp, -callee_adjust]!
       之一, 优先选用此模板是因为其在调整sp的同时push了寄存器, 如:
         stp reg1, reg2, [sp, -frame_size]!
         stp reg3, reg4, [sp, 16]
       但若frame_size过大或当前函数有outgoing区域则无法使用此模板(有outgoing则stp!/str!会将寄存器保存到错误的位置)。
    */
    cfun->machine->frame.callee_adjust = const_size;
  } else if (known_lt (crtl->outgoing_args_size + cfun->machine->frame.saved_regs_size, 512)		//case 2
	   && !(cfun->calls_alloca 
            && known_lt (cfun->machine->frame.hard_fp_offset, max_push_offset)))
  {
    /* 如果不能使用case 1, 则尝试直接修改sp后再通过stp/str save所有callee-saved register,但
      此时要求 callee-saved register + outgoing区域加在一起不能超过stp指令范围,如:
	   sub sp, sp, frame_size                 
	   stp reg1, reg2, [sp, outgoing_args_size]            //outgoing_args_size 需 < 512
	   stp reg3, reg4, [sp, outgoing_args_size + 16]
       动态栈分配(calls_alloca)时有个特殊的case见后.
    */
    cfun->machine->frame.initial_adjust = cfun->machine->frame.frame_size;
    cfun->machine->frame.callee_offset = cfun->machine->frame.frame_size - cfun->machine->frame.hard_fp_offset;
  } else if (cfun->machine->frame.hard_fp_offset.is_constant (&const_fp_offset)						//case 3
	   && const_fp_offset < max_push_offset)
  {
    /* 如果outgoing + saved_reg_size区域过大, 则需要两次调整:
       1) 先将sp指向callee-saved register首地址(sp-=hard_fp_offset)
       2) save 所有callee-saved regs
       3) 再将sp调整到frame_size位置(sp-=outgoing_arg_size)
       而如果此时hard_fp_offset较小,则1/2可以合并为一步,即为case 3:
        stp reg1, reg2, [sp, -hard_fp_offset]!	//将sp指向callee-saved register首地址的同时save两个寄存器
        stp reg3, reg4, [sp, 16]
        sub sp, sp, outgoing_args_size
    */
    cfun->machine->frame.callee_adjust = const_fp_offset;
    cfun->machine->frame.final_adjust = cfun->machine->frame.frame_size - cfun->machine->frame.callee_adjust;
  } else																							//case 4
  {
    /* case 4和case 3逻辑上是一致的,此时hard_fp_offset较大, 1/2无法合并, 此时生成的指令如:
	   sub sp, sp, hard_fp_offset     // sp -= hard_fp_offset
	   stp x29, x30, [sp, 0]          // save callee-saved regs
	   stp reg3, reg4, [sp, 16]       
	   sub sp, sp, outgoing_args_size // sp -= outgoing_arg_size
    */
    cfun->machine->frame.initial_adjust = cfun->machine->frame.hard_fp_offset;
    cfun->machine->frame.final_adjust = cfun->machine->frame.frame_size - cfun->machine->frame.initial_adjust;
  }

  cfun->machine->frame.laid_out = true;				/* 栈帧布局完毕 */
}    

  这里需要额外解释一下pro/epilouge指令的选择, 以prologue为例, 其栈帧的的设置最多不超过5步(epilogue正好相反):

1. sub sp, sp, initial_adjust				  	//initial_adjust=0 则此指令不发射
2. stp reg1, reg2, [sp, -callee_adjust]!      	//callee_adjust=0 则此指令不发射
3. add fp, sp, callee_offset                  	//此指令用来保存frame_chain,是否发射只取决于 emit_frame_chain是否为1
4. stp reg3, reg4, [sp, callee_offset + N*16] 	//这里是多条指令,发射与否与数量取决于有多少个callee-saved regs, 与callee_offset的值无关
5. sub sp, sp, final_adjust						//final_adjust=0 则此指令不发射

  pro/epilogue的工作主要就是两个,以prologue为例:

  1. 为当前函数预留栈空间(最终调整sp -= frame_size;此空间大小编译期间确定)
  2. 将所有callee-saved regs 保存到栈中

  在aarch64平台这两步本来可以是一个标准操作,但标准操作同时意味着效率不会总是最优, 为了性能考虑 aarch64_layout_frame 函数中通过4个字段决定最终如何生成最优的的pro/epilogue指令,这些字段分别是:

cfun->machine->frame.initial_adjust = 0;     
cfun->machine->frame.final_adjust = 0;
cfun->machine->frame.callee_adjust = 0;
cfun->machine->frame.callee_offset = 0;

1. 首先最快捷的方式是使用 stp Rm, Rn, [sp, xxx]! / str Rn, [sp, xxx]!指令, 因为这样的一条指令中不仅save了两个寄存器,同时还完成了sp-=xxx; 操作。使用此指令后,其他callee-saved寄存器可以通过 stp reg3, reg4, [sp, N*16] 来保存。但使用此指令有两个前提条件:

  1) xxx要在stp!/str!指令的可操作范围内

      ARM指令集限制 stp!的操作范围<512, str!的操作范围 <256;

  2) 此函数不能有outgoing区域

      因为callee-saved regs 的基地址并不是 callee sp, 而是sp-outgoing, 如果此函数有outgoing区域那么stp!/str!会直接将Rm/Rn save到错误的位置(outgoing区域)

2. 若不满足1, 那意味着至少需要多发射一条栈帧调整指令,若此时outgoing + callee-saved 区域很小(<512), 则可以:

  1) 一步到位先调整栈帧:

      sub sp, sp, frame_size

  2) 然后依次反向save callee-saved寄存器

      stp reg3, reg4, [sp, outgoing_arg_size + N*16]

3. 如果outgoing区域过大, stp指令无法存储所有callee-saved register, 则只能使用最麻烦的方法,即:

  1) 先将sp移动到callee-saved register位置

  2) saved 所有callee-saved register

  3) 再将sp移动到 outgoing区域结尾

但这又分为两种情况:

  • hard_fp_offset区域足够小(<max_push_offset),则此时我们可以稍作优化,使用:
1) stp reg1, reg2, [sp, -hard_fp_offset]! // 一步完成调整并saved前两个寄存器
2) stp reg3, reg4, [sp, N*16] // 继续save其他寄存器
3) sub sp, sp, outgoing_arg_size // 调整到outoging区域
  • 若hard_fp_offset区域也很大, 则只能依次执行:
1) sub sp, sp, hard_fp_offset
2) stp reg3, reg4, [sp, N*16] // (这里只写1条,但实际上要保存r1/r2/r3/r4)
3) sub sp, sp, outgoing_arg_size

需要注意的是:

  1. add fp, sp, callee_offset 指令是否发射不取决于 callee_offset, 在prologue生成过程中如果fp/sp之间有差值则会默认设置callee_offsset(case 2), 但这条指令是否发射只取决于当前是否需要保存栈帧(emit_frame_chain).
  2. 当hard_fp_offset区域和outging区域都很小时优先选用case2, 但一种情况例外,就是若当前函数存在动态分配(alloca的调用), 此时若hard_fp_offset区域和outging区域都很小则选择case 3.1

因为此时总是需要保存栈帧(epilogue中用fp来恢复sp,这样不必理会动态分配了多少空间), 最终指令可能3.1更快:

case2: 
  sub sp, sp, frame_size
  stp reg0, reg1, [sp, outgoing_arg_size + N*16]
  add fp, sp, outgoing_arg_size
  
case3.1:
  stp reg1, reg2, [sp, -hard_fp_offset]!
  mov fp, sp
  sub sp, sp, outgoing_arg_size

  而正常情况下应该是case2更快:

case2: 
  sub sp, sp, frame_size
  stp reg0, reg1, [sp, outgoing_arg_size + N*16]

case3.1:
  stp reg1, reg2, [sp, -hard_fp_offset]!
  sub sp, sp, outgoing_arg_size

7.2 prologue指令序列的发射

  prologue与epilogue是对称的两个函数,这里仅介绍prologue的实现。prologue的主要作用是发射指令为当前函数预留栈空间(frame_size), 并将此函数中需要callee-saved的寄存器保存到新开辟栈的callee-saved regs区域. prologue在 pass_thread_prologue_and_epilogue 中发射:

//pass_thread_prologue_and_epilogue => rest_of_handle_thread_prologue_and_epilogue => rest_of_handle_thread_prologue_and_epilogue
// => make_prologue_seq => targetm.gen_prologue => aarch64_expand_prologue
/* 此函数完成step1-5
    1. sub sp, sp, initial_adjust				  	//initial_adjust=0 则此指令不发射
    2. stp reg1, reg2, [sp, -callee_adjust]!      	//callee_adjust=0 则此指令不发射
    3. add fp, sp, callee_offset                  	//此指令用来保存frame_chain,是否发射只取决于 emit_frame_chain是否为1
    4. stp reg3, reg4, [sp, callee_offset + N*16] 	//这里是多条指令,发射与否与数量取决于有多少个callee-saved regs, 与callee_offset的值无关
    5. sub sp, sp, final_adjust						//final_adjust=0 则此指令不发射
*/
void aarch64_expand_prologue (void)
{
  /* 获取aarch64_layout_frame 和当前函数分析过程中的一些影响指令生成的变量 */
  poly_int64 frame_size = cfun->machine->frame.frame_size;
  poly_int64 initial_adjust = cfun->machine->frame.initial_adjust;
  HOST_WIDE_INT callee_adjust = cfun->machine->frame.callee_adjust;
  poly_int64 final_adjust = cfun->machine->frame.final_adjust;
  poly_int64 callee_offset = cfun->machine->frame.callee_offset;
  unsigned reg1 = cfun->machine->frame.wb_candidate1;
  unsigned reg2 = cfun->machine->frame.wb_candidate2;
  bool emit_frame_chain = cfun->machine->frame.emit_frame_chain;
  rtx_insn *insn;
  ......
      
  /* step1:
     若initial_adjust 非空,则先发射调整 initial_adjust 的指令; 若initial_adjust 为空,这里不发射指令.
  */
  aarch64_allocate_and_probe_stack_space (tmp0_rtx, tmp1_rtx, initial_adjust, true, false);
    
  /* step2:
     有callee_adjust 则会发射 stp!/str!指令, 如: stp reg1, reg2, [sp, -callee_adjust]!
     没有 callee_adjust 则这里不发射指令(这里的reg1/2是candidate1/2,但未必是x29/x30)
  */
  if (callee_adjust != 0)
    aarch64_push_regs (reg1, reg2, callee_adjust);

  /* step3:
     如果要保存栈帧则发射 add fp, sp, callee_offset 指令, 此时
     * 若前面发射了stp!/str!指令,其必定已经保存了x29/x30(即candidate1/2分别是x29/x30)
     * 若之前未发射stp!/str!指令, 则在修改fp之前还需先发射 stp x29, x30, [sp, callee_offset] 保存其原值
  */
  if (emit_frame_chain)
  {
    poly_int64 reg_offset = callee_adjust;
    /*
      若前面已经发射了stp!/str!指令(callee_adjust!=0), 此时(由于需要frame_chain)其一定附带保存了x29, x30,这里不必再保存了.
      若前面没发射stp!/str!,则在修改fp前(add fp, sp, callee_offset),需要先发射 stp x29, x30, [sp,callee_offset] 保存frame_chain
    */
    if (callee_adjust == 0)
	{
	  reg1 = R29_REGNUM;
	  reg2 = R30_REGNUM;
	  reg_offset = callee_offset;
      /* 发射指令: stp x29, x30, [sp, callee_offset],在callee-saved reg区域保存x29/x30 */
	  aarch64_save_callee_saves (DImode, reg_offset, reg1, reg2, false);
	}
	
    /* 发射指令: add fp, sp, callee_offset */
    aarch64_add_offset (Pmode, hard_frame_pointer_rtx, stack_pointer_rtx, callee_offset, tmp1_rtx, tmp0_rtx, frame_pointer_needed);
  }
  .......
  /* step4: 
     发射 stp reg3, reg4, [sp, callee_offset + N*16]; 保存所有callee-saved regs,发射几次取决于有多少个要保存的寄存器
     以下两种情况candidate1/2已经保存过了,在此函数的遍历中无需再次保存:
     * callee_adjust != 0: 此时前面 stp reg1, reg2, [sp, -callee_adjust]! 已经保存了两个candidate
     * emit_frame_chain != 0: 此时candidate1/2(fp/sp)也已经保存过了
     此函数中仅保存寄存器, 不会修改sp; 同时shrink-wrapping seperate过的寄存器也不用再保存了(见[4]).
  */
  aarch64_save_callee_saves (DImode, callee_offset, R0_REGNUM, R30_REGNUM, callee_adjust != 0 || emit_frame_chain);
  aarch64_save_callee_saves (TFmode, callee_offset, V0_REGNUM, V31_REGNUM, callee_adjust != 0 || emit_frame_chain);

  /* step5: 
    发射 sub sp, sp, final_adjust; 如果 final_adjust 为空则这里不发射任何指令.
  */
  aarch64_allocate_and_probe_stack_space (tmp1_rtx, tmp0_rtx, final_adjust, !frame_pointer_needed, true);
}

参考资料:

[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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值