自制编译器 青木峰郎 笔记 Ch14 函数和变量

14.1 程序调用约定

C语言本身的语法特性是与平台无关的,但是具体实现语法特性本身,也就是编译往往会随着OS和CPU软硬件条件而有所不同。比如参数传递是入栈还是用寄存器,返回值的传递方法是用什么寄存器等。规定这些的就是程序调用约定calling convention。
Linux的调用约定在LSB(Linux Standard Base)上有明确规定。
下面是Linux在LA-32上面的部分calling convention:

项目 内容
参数传递方法 入栈传递
返回值传递方法 eax
返回地址指定方法 入栈传递
标准栈帧结构 按照参数,返回地址,原ebp,局部变量的顺序

14.2 Linux/x86下的函数调用

x86下有enter指令用来生成栈帧,leave指令用来释放栈帧。leave指令很常被使用,但是enter指令因为速度过慢所以很少用。本文直接采用

pushl %ebp
movl %esp, %ebp
subl $8, %esp

用以下代码释放栈帧。

movl %ebp, %esp
popl %ebp
# 如果有返回值,需要mov到%eax中
ret
# 注意函数运行之后需要清理栈帧,把为了传递参数而保存进去的变量pop掉
addl $8, %esp

14.3 Linux/x86下函数调用细节

寄存器的保存和复原

x86架构提供了6个可以随意使用的通用寄存器。
多个函数共同使用这6个寄存器存在类似全局变量的访问控制问题,最简单粗暴的方法当然是开始执行时把所有寄存器压栈,直到Return时再出栈还原执行环境,但是执行效率比较低。
书中提出的优化方案

  1. 对被调用函数不会覆写到的寄存器不必变更
    • 如何判断
  2. 利用caller-save寄存器和callee-save寄存器

caller-save寄存器,callee-save寄存器

caller-save寄存器: 被调用函数可以直接更改caller-save寄存器,所以调用方如果还需要这个寄存器中的值,就需要自行将寄存器存入栈中。适合保存生成的临时变量或者寿命比较短的变量。
callee-save寄存器: 被调用函数如果要用该显示器,则必须在返回之前恢复原值。所以被调用函数需要将原值压入栈中,适合保存需要一直访问的变量。
在IA-32体系下,caller-save寄存器有eax,ecx,edx,esp;callee-save寄存器有ebx, ebp, esi, edi

大数据和浮点数的返回方法

  1. Linux/x86约定将返回值放入eax,但是这只能保存32位数据。对64位数据,我们通常用edx和eax来返回。
  2. 需要返回浮点数时,可以放入st(0)这个浮点数寄存器中,而无需eax。
  3. 返回结构体和联合体需要函数调用方和函数本身协作,本书给出的方案:
    1. 函数调用方申请保存返回值的区域
    2. 该区域的内存地址作为第1个参数压栈
    3. 被调用函数把返回值写入该区域
    4. 把返回值对应得内存地址保存到eax寄存器中
    5. 为了清除掉栈中传入的返回值区域地址,执行ret $4

其他平台的程序调用约定

  • Linux/IA-32程序调用约定cdecl。
  • Windows使用stdcall,与cdecl相似,但是释放栈上的保存参数时,规定被调用函数而不是调用方释放参数(博客作者上学时学的调用约定)。
    - 本书认为因此不能很好地支持printf这样参数个数可变的函数??
  • Linux/AMD64-fastcall尽量使用寄存器来传递参数,比较快但是需要更多的寄存器
原文地址:https://www.cnblogs.com/xuesu/p/14383357.html