The Decaf Book
  • Introduction
  • 语言规范
  • Java 框架分阶段指导
    • 本章导引
    • PA1-A:语法分析器的自动构造
      • 阶段任务
      • 词法分析
      • 抽象语法树
      • 文法分析
    • PA1-B:基于 LL(1) 的语法分析器半自动构造
      • 阶段任务
      • LL(1) 文法分析
      • 错误恢复
    • PA2:语义分析
      • 阶段任务
      • 类型、符号和作用域
      • 访问者模式
      • 符号表构建
      • 类型检查
    • PA3:中间代码生成
      • 阶段任务
      • TAC 程序
      • 面向对象机制
      • 控制流的翻译
    • PA4:中间代码优化
      • 实验内容
      • 基本块
      • 数据流分析概述
      • 数据流优化概述
      • 公共表达式提取
      • 复写传播
      • 常量传播
      • 死代码消除
  • Scala 框架分阶段指导
    • 本章导引
    • PA1:语法分析器的自动构造
      • 阶段任务
      • 词法分析
      • 抽象语法树
      • 文法分析
      • 访问者模式
    • PA2:语义分析
      • 阶段任务
      • 类型、符号和作用域
      • 符号表构建
      • 类型检查
    • PA3:中间代码生成
      • 阶段任务
      • TAC 程序
      • 面向对象机制
      • 控制流的翻译
    • PA3-JVM:JVM 字节码生成
      • 阶段任务
      • JVM 字节码简介
      • 翻译过程
    • PA4:中间代码优化
      • 实验内容
      • 基本块
      • 数据流分析概述
      • 数据流优化概述
      • 公共表达式提取
      • 复写传播
      • 常量传播
      • 死代码消除
  • Rust 框架分阶段指导
    • 本章导引
    • PA1-A:语法分析器的自动构造
      • 实验内容
      • lalr1使用指导
        • 编写lexer
        • impl块的可选属性
        • 产生式和语法动作
        • 解决冲突
        • 一个完整的例子
      • 抽象语法树
      • 框架中部分实现的解释
      • 文件结构
    • PA1-B:基于 LL(1) 的语法分析器半自动构造
      • 实验内容
      • lalr1使用指导
      • 错误恢复
      • 文件结构
    • PA2:语义分析
      • 实验内容
      • 语义分析
      • 符号表
      • visitor模式
    • PA3:中间代码生成
      • 实验内容
      • 中间代码
      • 中间代码中的类型信息
      • 运行时存储布局
      • 面向对象机制
      • tacvm简述
    • PA4:中间代码优化
      • 基本块
      • 数据流分析概述
      • 数据流优化概述
      • 公共表达式提取
      • 复写传播
      • 常量传播
      • 死代码消除
    • PA5:寄存器分配
      • 实验内容
      • 图着色基本原理
      • 着色算法
      • 预着色节点
      • 干涉图节点合并
      • 调用约定
Powered by GitBook
On this page

Was this helpful?

  1. Rust 框架分阶段指导
  2. PA5:寄存器分配

调用约定

Previous干涉图节点合并

Last updated 5 years ago

Was this helpful?

比较详细的调用约定参见[4]。与寄存器相关的调用约定如下:

其中灰色的寄存器为被调用者保存的寄存器,白色的为调用者保存的寄存器。值得注意的是,虽然$ra由被调用者保存,它的值也的确在进入和离开被调用的函数时保持一致,但是函数调用指令本身会修改它。

我们把寄存器分配的范围限制在mips的32个通用寄存器中的26个:除了寄存器$zero,$k0/1,$gp,$at,$sp外其他寄存器都参与分配。参与分配的寄存器中,$ra,$v0等寄存器可能有自己特殊的用途,但是仍然有可能像其他寄存器一样参与一般的分配。

对于被调用者保存的寄存器,首先在指令选择阶段非常暴力地在函数的prologue部分生成代码保存所有寄存器,在epilogue部分生成代码恢复所有寄存器(只需要保存和恢复实际参与分配的那些):

foo:
  ...
  move _Rxx0, $s0
  move _Rxx1, $s1
  ...
  move _Rxx7, $s7
  move _Rxx8, $ra

  ... # 函数体,所有的返回都翻译为到epilogue的跳转

  move $s0, _Rxx0
  move $s1, _Rxx1
  ...
  move $s7, _Rxx7
  move $ra, _Rxx8
  ...
  jr $ra # 返回

其中_Rxx0-8就是普通的虚拟寄存器。对于一个正常大小的函数,寄存器的第一次分配不太可能成功,那么首选的spill的节点很有可能就是这些,因为它们和函数中间的所有定值节点都冲突了。然而如果函数很小(准确来说是函数的寄存器压力很小),则这些move很可能被成功合并,所以虽然生成了这么多移动指令,最终可能并没有产生任何运行开销。

除了添加这些额外的移动之另外,还需要特殊处理函数调用相关的指令,它们虽然表面看起来不会写入或者读取其他寄存器的值,实际上因为调用约定的存在,可以假装它们需要写入或者读取,这样上层就可以统一处理。

  • 调用指令:读取$a0-3,写入所有调用者保存的寄存器和$ra寄存器

  • 返回指令:读取被调用者保存的寄存器和$v0

mips