跳转到内容

GNU C 编译器内部/函数调用 4 1

来自维基教科书,开放的书籍,为开放的世界

全局控制流分析

[编辑 | 编辑源代码]

文件中的函数用于在文件 cgraphunit.c 中生成一个调用图。两个相关的函数是 cgraph_finalize_compilation_unit(),它在解析完文件后从函数 pop_file_scope() 调用,以及 cgraph_finalize_function(),它从 finish_function() 调用。

每个函数的效果取决于编译模式。一次一个单元模式指示编译器仅在解析完每个函数后才构建调用图。当此选项不存在时,函数将在解析后立即转换为 RTL。

cgraph_finalize_function() 调用 cgraph_analyze_function(),后者将其转换为 RTL。否则,该函数将在 cgraph_nodes_queue 中排队。最后,cgraph_finalize_compilation_unit() 处理队列。cgraph_nodes 是表示调用图的全局变量。函数 dump_cgraph 允许打印调用图。

参数传递

[编辑 | 编辑源代码]

在本章中,我们将了解函数如何相互调用。通常,函数在进行调用时会传递多个参数。当函数开始时会创建一个栈帧。但是,前一个函数的栈帧可能会被重用。这种类型的函数调用称为兄弟调用。当函数体不够大时,设置栈帧的运行时开销过高。在这种情况下,被调用函数将被内联到父函数中。

函数 expand_call() 接收 CALL_EXPR 树并生成 RTL 表达式。它必须决定参数传递模式。struct arg_data 包含每个参数的必要信息。

struct arg_data
字段名称 解释
tree tree_value 此参数的树节点
enum machine_mode mode 值的模式
rtx value 参数的当前 RTL 值,如果未预计算则为 0
rtx initial_value 参数的初始计算 RTL 值;仅适用于 const 函数。
rtx reg 用于传递此参数的寄存器,如果在栈上传递则为 0
rtx tail_call_reg 在生成尾调用序列时用于传递此参数的寄存器
rtx parallel_value 如果 REG 是 PARALLEL,则这是 VALUE 的副本,被拉入 emit_group_move 的正确形式。
int unsignedp 如果 REG 从参数表达式的实际模式提升而来,则表示提升是符号扩展还是零扩展
int partial 要放入寄存器的字节数。0 表示将整个区域放入寄存器或不放入寄存器。
int pass_on_stack 如果参数必须在栈上传递则不为零。请注意,即使 pass_on_stack 为零,某些参数也可能在栈上传递,仅仅因为 FUNCTION_ARG 指示这样做
struct locate_and_pad_arg_data locate 为 locate_and_pad_parm 打包的一些字段
rtx stack 应该存储参数的栈上的位置
rtx stack_slot 此参数槽的起始位置的栈上的位置
rtx save_area 如果需要,此栈区域已被保存的位置
rtx *aligned_regs 如果参数的对齐方式不允许直接复制到寄存器,则将较小尺寸的部分复制到伪寄存器。这些存储在由此字段指向的块中。
int n_aligned_regs 表示我们创建了多少个字长伪寄存器

在没有某些机器特定的信息的情况下,生成函数调用是不可能的,例如不同类型的硬件寄存器的数量。在每个体系结构的 .h 文件中定义的许多宏负责将中端与后端连接起来

参数宏
宏名称 解释
INIT_CUMULATIVE_ARGS 为调用数据类型为 FNTYPE 的函数初始化 CUMULATIVE_ARGS 数据结构。
FUNCTION_ARG 定义将参数放到哪里。值为零表示将参数压入栈,或将参数存储其中的一个硬件寄存器。
FUNCTION_ARG_ADVANCE 更新 CUM 中的数据以在参数上进行前进。

文件 i386.c 中的函数 init_cumulative_args() 处理 x86 体系结构的情况。它考虑了用户可能指定的函数属性 regparm 和 fastcall,在这种情况下,可用寄存器的数量将相应地设置。但是,如果函数接受可变数量的参数,则所有参数都将通过栈传递。

参数的位置是在函数 initialize_argument_information() 中决定的。机器特定的 function_arg() 函数将返回参数的 rtl,如果它进入寄存器

  ret = gen_rtx_REG (mode, regno);

参数可能会在栈和寄存器中传递,例如,如果参数类型是可寻址的。

根据某些条件,除了正常链之外,还会生成兄弟调用指令链。让我们考虑只生成正常链的情况。变量 rtx argblock 是为栈参数预分配的空间的地址(在没有 push 指令的机器上),或者如果未预分配空间则为 0。

许多机器特定的变量决定了栈的形状。ACCUMULATE_OUTOING_ARGS 指示编译器在函数前导中为任何函数调用的所有参数预分配足够的字节数。之后,函数参数将保存在该区域中,而不会修改栈帧的大小。ACCUMULATE_OUTOING_ARGS 取决于变量 target_flags。它取决于机器配置和命令行选项。在 ACCUMULATE_OUTOING_ARGS 的情况下,i386 特定的变量

const int x86_accumulate_outgoing_args = m_ATHLON_K8 | m_PENT4 |
m_NOCONA | m_PPRO;

和一个命令行选项 -maccumulate-outgoing-args 使能了此功能。这意味着它在 Pentium4 上启用,并且不使用 push/pop 指令来传递函数参数。

如果我们预分配了栈空间,请计算每个参数的地址并将其存储到 ARGS 数组中。

根据需要预计算函数调用的参数。此例程将填充每个预计算参数的 INITIAL_VALUE 和 VALUE 字段。precompute_arguments()

给定 FNDECL 和 EXP,返回一个适合用作调用指令中目标地址的 rtx

 funexp = rtx_for_function_call (fndecl, addr);

预计算所有寄存器参数。一旦我们开始填充任何特定的硬件寄存器,计算任何东西就不安全了。precompute_register_parameters (num_actuals, args, &reg_parm_seen);

现在存储(如果需要,还计算)所有非寄存器参数。这些参数位于寄存器参数之前,因为它们可能需要块移动,这可能会破坏用于寄存器参数的寄存器。部分寄存器参数不会在这里存储,但如果它们需要,我们会在此处预分配空间。

          store_one_arg (&args[i], argblock, flags,
                             adjusted_args_size.var != 0,
                             reg_parm_stack_space)

对任何完全寄存器参数或在栈和寄存器中传递的参数执行所需的寄存器加载。它们的表达式已经计算出来了。

    load_register_parameters (args, num_actuals, &call_fusage, flags,
                              pass == 0, &sibcall_failure);

最后,emit_call_1() 生成指令来调用函数 FUNEXP,并可选地弹出结果。CALL_INSN 是生成的第一个指令。

当决定参数的位置时,变量 struct args_size args_size 会保存栈参数的总大小。它将一系列参数的大小记录为树表达式和常数的总和。树部分对于处理可变大小的参数(例如,在编译时大小未知的数组参数)是必需的。C 语言不允许可变大小的参数。

有人可能会想知道被调用函数是如何找到参数到达位置的。它也使用调用者使用的机器特定信息。在被调用者中重新运行 INIT_CUMULATIVE_ARGS、FUNCTION_ARG 和 FUNCTION_ARG_ADVANCE 可以决定参数是否应该在寄存器或栈中到达,与 expand_call 相同。

华夏公益教科书