应用程序二进制接口(ABI) --- AAPCS(函数调用/中断上下文保护)

ABI

应用二进制接口(Application Binary Interface,简称ABI)是一种定义了应用程序与操作系统或者硬件之间的接口标准。ABI为开发人员提供了在不同平台上编写、编译和执行应用程序的一致性。

ABI 规定了:

  • 数据类型的大小、布局和对齐;
  • 调用约定(控制着函数的参数如何传送以及如何接受返回值),例如,是所有的参数都通过栈传递,还是部分参数通过寄存器传递;哪个寄存器用于哪个函数参数;通过栈传递的第一个函数参数是最先push到栈上还是最后;
  • 系统调用的编码和一个应用如何向操作系统进行系统调用;
  • 以及在一个完整的操作系统ABI中,目标文件二进制格式、程序库等等。

编译器依据 ABI 把C程序编译为汇编

开发者根据EABI编写汇编代码,可以作为与兼容的编译器生成的汇编语言的接口。 支持EABI的编译器创建的目标文件可以和使用类似编译器产生的代码兼容,这样允许开发者链接一个由不同编译器产生的库。EABI与关于通用计算机的ABI的主要区别是应用程序代码中允许使用特权指令,不需要动态链接(有时是禁止的),和更紧凑的堆栈帧组织用来节省内存。 广泛使用EABI的有Power PC和ARM。

AAPCS

旧时,ARM 过程调用标准叫做 APCS (ARM Procedure Call Standard),Thumb的过程调用标准为 TPCS。如今这两种叫法已经废弃,统一称作 AAPCS ( Arm Architecture Procedure Calling Standard)。

AAPCS 是 ARM ABI(Application Binary Interface) 接口文档的一份,ARM架构有16个寄存器,这个标准规定:

  • 哪些寄存器是函数调用者负责保护(被调用者使用了这些寄存器不需要恢复,调用者调用函数前压栈,调用后出栈),哪些是被调用者负责保护(被调用者使用这些寄存器前需要压栈,推出函数前出栈)。
  • 函数传参、返回值使用哪些寄存器

C函数依据某种规则编译为一组汇编指令,在ARM架构中,汇编指令可操作的对象分别是内存和寄存器。汇编对内存和寄存器的使用并没有什么限制,而C语言则不同,C语言是以函数的形式体现的(数据则是各种形式的变量),一个应用程序往往是由多个函数组成,试想一下,函数(指令块)若随意的使用资源,各个函数就会产生冲突,如:在函数中定义了一个变量,假设这个变量使用寄存器存储,这个时候调用另一个函数,如果另一个函数也有权修改这个寄存器,那么之前定义的那个变量极有可能会被破坏。那么这个时候就需要一个协议来规定资源的分配。

当前 ARM 官方已经通过 github 来维护相关文档,想获取最新的文档可以访问。Application Binary Interface for the Arm® Architecture。

 

 

这个规则不仅适用于函数调用,也适用于中断。区别是中断场景,调用者负责保护的寄存器的入栈出栈是由CPU自动完成的,函数调用场景,入栈出栈是由编译器处理的,C语言编译为汇编后,就可以看到这些入栈出栈的汇编指令。

1、寄存器使用规则

ARM处理器中有r0-r15共16个寄存器,它们的用途有一些约定的习惯,并依据这些用途定义了别名,如表所示。

项目别名使用规则
r15 pc 程序计数器
r14 lr 连接寄存器
r13 sp 数据栈指针
r12 ip 子程序内部调用的scratch寄存器
r11 v8 ARM状态局部变量寄存器8
r10 v7、s1 ARM状态局部变量寄存器7、在支持数据栈检查的ATPCS中为数据栈限制指针
r9 v6、sb ARM状态局部变量寄存器6、在支持RWPI的ATPCS中为静态基址寄存器
r8 v5 ARM状态局部变量寄存器5
r7 v4、wr ARM状态局部变量寄存器4、Thumb状态工作寄存器
r6 v3 ARM状态局部变量寄存器3
r5 v2 ARM状态局部变量寄存器2
r4 v1 ARM状态局部变量寄存器1
r3 a4 参数/结果/scratch寄存器4
r2 a3 参数/结果/scratch寄存器3
r1 a2 参数/结果/scratch寄存器2
r0 a1 参数/结果/scratch寄存器1

寄存器的使用规则总结如下。

  1. 子程序间通过寄存器r0-r3来传递参数,这时可以使用它们的别名a0-a3.被调用的子程序返回前无需恢复r0~r3的内容。
  2. 在子程序中,使用r4-r11来保存局部变量,这时可以使用它们的别名v1-v8.如果在子程序中使用了它们的某些寄存器,子程序进入时要保存这些寄存器的值,在返回前恢复它们;对于子程序中没有使用到的寄存器,则不必进行这些操作。在Thumb程序中,通常只能使用寄存器r4~r7来保存局部变量。
  3. 寄存器r12用作子程序间scratch寄存器,别名为ip.
  4. 寄存器rl3用作数据栈指针,别名为sp。在子程序中寄存器r13不能用作其他用途。它的值在进入、退出子程序时必须相等。
  5. 寄存器r14称为连接寄存器,别名为lr。它用于保存子程序的返回地址。如果在子程序中保存了返回地址(比如将Ir值保存到数据栈中),r14可以用作其他用途。
  6. 寄存器r15是程序计数器,别名为pc。它不能用作其他用途。

2、栈使用规则

栈有两个增长方向:向内存地址减小的方向增长时,称为DESCENDING栈;向内存地址增加的方向增长时,称为ASCENDING栈。

所谓栈的增长就是移动栈指针。当栈指针指向栈顶元素(最后一个入栈的数据)时,称为FULL栈;当栈指针指向栈顶元素(最后一个入栈的数据)相邻的一个空的数据单元时,称为EMPTY栈。

综合这两个特点,数据栈可以分为以下4种。

  • ①FD:Full Descending,满递减。
  • ②ED:Empty Descending,空递减。
  • ③FA:Full Ascending,满递增。
  • ④EA:Empty Ascending,空递增。

ATPCS规定数据栈为FD类型,并且对数据栈的操作是8字节对齐的。使用stmdb/ldmia批量内存访问指令来操作FD数据栈。

使用stmdb命令往数据栈中保存内容时,先递减sp指针,再保存数据,使用ldmia命令从数据栈中恢复数据时,先获得数据,再递增sp指针,sp指针总是指向栈顶元素,这刚好是FD栈的定义。

3、参数传递规则

一般来说,当参数个数不超过4个时,使用r0~r3这4个寄存器来传递参数:如果参数个数超过4个,剩余的参数通过栈来传递。

编译器会尽量使用R0-R1进行参数返回,如果不够用,会使用栈传递。

 

示例:假设CopyCode2SDRAM函数是用C语言实现的,它的数据原型如下:

int Copycode2sDRM(unsigned char*buf,unsigned long startaddr,int size)

在汇编代码中,使用下面的代码调用它,并判断返回值。

01 ldr r0,=0x30000000 //目标地址=0x30000000,这是SDRAM的起始地址
02 mov r1,#0//e2.源地址=0
03 mov r2,#16*1024 //复制长度16K
04 b1 CopyCode2SDRAM //调用c函数CopyCode2SDRAM
05 cmp a0,#0//判断函数返回值
  • 第1行将r0设为0x30000000,则CopyCode2SDRAM函数执行时,它的第一个参数buf的指向的内存地址为0x30000000。
  • 第2行将r1设为0,CopyCode2SDRAM函数的第二个参数start addr等于0。
  • 第3行将r2设为16*1024,CopyCode2SDRAM函数的第三个参数size等于16*1024。
  • 第4行进行跳转执行c程序
  • 第5行判断返回值。

实际案例

从汇编跳转到C函数,汇编的参数传到C函数,把参数放在r0~r3,C函数被编译器编译为汇编,会从r0~r3取参数。汇编编写者和C编译器都遵循AAPCS,才能正确的传递获取参数。

 

posted @ 2022-12-10 22:56  流水灯  阅读(419)  评论(0编辑  收藏  举报