为了使单独编译的c文件和汇编文件之间能够互相调用,需要制定一系列的规则,AAPCS就是ARM程序和Thumb程序中子程序调用的基本规则。
1、ATPCS概述
ATPCS规定了子程序调用过程中寄存器的使用规程、数据站的使用规则、参数的传递规则。为了适应一些特殊的需求,对这些规则进行改动可以得到几种不同的子程序调用规则,具体包括:
-
支持数据栈限制检查的ATPCS
-
支持只读段位置无关(ROPI)的ATPCS
-
支持可读写段位置无关(RWPI)的ATPCS
-
支持ARM程序和Thumb程序混合使用的ATPCS
-
处理浮点运算的ATPCS
相关调用程序必须遵守同一种ATPCS,编译器/汇编器在ELF格式的目标文件中设置相应的属性,标识用户选定 ATPSC类型。不同类型的ATPCS规则,有对应的C语言库,连接器根据用户的指定ATPCS类型链接相应的c库。
C语言的编译器编译的C子程序能够满足用户指定的ATPCS类型。而对于汇编语言,需要用户来满足子程序间的ATPCS类型。汇编子程序必须满足如下三个条件:
-
子程序编写时必须遵守相应的ATPCS规则
-
数据栈的使用要遵守相应的ATPCS规则
-
汇编器中使用-apcs选项
下面是keil中,C语言配置的选项。
下面是汇编器的配置。
2、基本ATPCS
基本的ATPCS规则包括下面三个方面内容:
-
各寄存器的使用规则及其相应的名称
-
数据栈的使用规则
-
参数传递的规则
满足基本类型的ATPCS程序运行速度更快,占用内存更少,但不支持以下功能:
-
ARM程序和Thumb程序的互相调用(注:在Cortex-M3中使用的是Thumb2指令,已经不区分ARM、Thumb指令了,《Cortex-M3权威手册》对此有论述。因此,该条存疑,本文参考的资料也较老,新版可能会更改,待验证。。。。)
-
数据以及代码的位置无关的支持
-
子程序的可重入性
-
数据栈检查的支持
派生的几种ATPCS规则是在基本的ATPCS基础上添加额外规则形成的,目的就是提供上述功能。
2.1 寄存器的使用规则
寄存器的使用必须满足如下规则:
-
子程序间通过R0--R3传递参数,被调用的子程序在返回前无需恢复寄存器R0--R3的内容。
-
在子程序中,使用R4--R11保存局部变量,如果在子程序中使用到了寄存器R4--R11中的某些寄存器,子程序进入前需要保存这些寄存器的值,在返回时需要恢复。未用到,不处理。在Thumb程序中,通常只能使用寄存器R4--R7来保存局部变量。
-
寄存器R12用作子程序间scratch寄存器,记作ip。在子程序间的连接代码段中常用这种使用规则。
-
寄存器R13用作数据栈指针,记作sp。在子程序中sp不能用作其它用途,进入子程序时的sp值和退出子程序时的sp值必须相等。
-
R14称为连接寄存器,记作lr。用于保存子程序的返回地址,如果在子程序中保存了返回地址,R14可以用作其它的用途。
-
R15是程序计数器,记作pc,不能用于其它的用途。
ATPCS中各寄存器的使用规则及其名称
寄存器 | 别名 | 特殊名称 | 使用规则 |
R15 | Pc | 程序计数器 | |
R14 | Lr | 连接寄存器 | |
R13 | Sp | 数据栈指针 | |
R12 | Ip | 子程序内部调用的scratch寄存器 | |
R11 | V8 | ARM状态局部变量寄存器8 | |
R10 | V7 | sl | ARM状态局部变量寄存器7 在支持数据栈检查的ATPCS中为数据栈限制指针 |
R9 | V6 | sb | ARM状态局部变量寄存器6 在支持RWPI的ATPCS中为静态基址寄存器 |
R8 | V5 | ARM状态局部变量寄存器5 | |
R7 | V4 | wr | 局部变量寄存器4 Thumb状态工作寄存器 |
R6 | V3 | 局部变量寄存器3 | |
R5 | V2 | 局部变量寄存器2 | |
R4 | V1 | 局部变量寄存器1 | |
R3 | A4 | 参数/结果/scratch 寄存器4 | |
R2 | A3 | 参数/结果/scratch 寄存器3 | |
R1 | A2 | 参数/结果/scratch 寄存器2 | |
R0 | A1 | 参数/结果/scratch 寄存器1 |
2.2 数据栈的使用规则
栈指针通常可以指向不同的位置。当栈指针指向栈顶元素(即最后一个入栈的数据元素)时,称为FULL栈;当栈指针指向与栈顶指针(即最后一个入栈的数据元素)相邻的一个可用数据单元时,称为EMPTY栈。
栈的增长方式也可以不同,向内存地址减小的方向增长时,称为DESCENDING栈,向地址增长的方向增长时,称为ASCENDING栈。故,有四种栈:
- FD FULL DESCENDING
- ED EMPTY DESCENDING
- FA FULL ASCENDING
- EA EMPTY ASCENDING
ATPCS规定数据栈为FD栈,并且数据栈的操作时8 Byte对齐。下面时数据栈的示例和名词解释。
- 数据栈栈指针(stack pointer)是指最后一个写入栈的数据的内存地址。
- 数据栈的基地址(stack base)是指数据栈的最高地址。由于ATPCS中数据栈式FD类型,实际上数据栈中最早入栈的数据占据的内存单元式基地址的下一个内存单元。
- 数据栈界限(stack limit)是指数据栈可以使用的最低的内存单元的地址。
- 已占用的数据栈(used stack)是指数据栈的基地址和数据栈栈指针之间的区域。其中包括数据栈栈指针对应的内存单元,但不包括数据栈的基地址对应的内存单元。
- 未占用的数据栈(unused stack)是指数据栈栈指针和数据栈界限之间的区域,其中包括数据栈界限对应的内存单元,但不包括数据栈栈指针对应的内存单元。
- 数据栈中的数据帧(stack frames)是指在数据栈中,为子程序分配的用来保存寄存器和局部变量的区域。
异常中断的处理程序可以使用被中断程序的数据栈,这时用户要保证中断的程序的数据栈足够大。
数据栈的示意图
在ARMv5RE(很老的版本了,cortex-m3式ARMv7版本)中,批量传输指令LDRD/STRD要求的数据栈是8 Byte对齐,以提高数据的传输速度。用ADS编译器产生的目标文件中,外部接口的数据栈都是8 Byte对齐的,并且编译器告诉连接器:本目标文件的数据栈是8字节对齐的,对于汇编程序来说,如果目标文件中包含了外部调用,则必须满足下列条件:
- 外部接口的数据栈必须是8字节对齐,保证在进入该汇编代码后,知道该汇编代码调用外部程序之间,数据栈的栈指针变化的是偶数个字(如栈指针加2个字,而非3个字)。
- 汇编程序中使用PRESERVE8位操作告诉连接器,本汇编程序数据栈是8字节对齐的。
2.3 参数传递规则
根据参数个数是否固定可以将子程序分为参数个数固定的子程序和参数个数可变的子程序,二者的参数传递规则不同。
2.3.1 参数个数可变的子程序参数传递规则(少见,对应C中的可变参数)
当参数不超过4个时,可以使用寄存器R0--R3来传递参数,超过4个,还可以使用数据栈来传递参数。
参数传递时,将所有参数看作时存放在连续的内存字单元中的字数据,然后,依次将各字数据传送到寄存器R0--R3中,如果大于4个,将剩余的字数据传送到数据栈中,入栈顺序与参数顺序相反。
按照上面的规则,一个浮点数参数可以通过寄存器传递,也可以通过数据栈传递,也可能一半通过寄存器传递,另一半通过数据栈传递。
2.3.2 参数不可变的子程序传递规则(常见,大部分C语言的情况)
如果系统包含了浮点运算的硬件部件,浮点参数将按照下面的规则传递:
- 各个浮点参数按顺序处理
- 为每个浮点参数分配FP寄存器
- 分配的方法:满足该浮点参数需要的且编号最小的一组连续FP寄存器
第一个整数参数,通过R0--R3传输,其余的参数按照数据栈传输。
2.3.3 子程序结果返回规则
子程序中结果返回的规则如下:
- 结果为一个32位整数时,可以通过R0返回.
- 结果为一个64位整数时,可以通过R0、R1返回,以此类推。
- 结果位一个浮点数时,可以通过浮点运算部件的寄存器f0、d0或者s0返回
- 结果为复合型的浮点数(如复数)时,可以通过寄存器f0--fN或者d0--dN来返回。
- 对于位数更多的结果,需要通过内存来传递。
3 几种特定的ATPCS
几种特定的ATPCS是在基本的ATPCS基础上增加一些规则形成的:
-
支持数据栈限制检查的ATPCS
-
支持只读段位置无关(ROPI)的ATPCS
-
支持可读写段位置无关(RWPI)的ATPCS
-
支持ARM程序和Thumb程序混合使用的ATPCS
-
处理浮点运算的ATPCS
3.1 支持数据栈限制检查的ATPCS
3.1.1 支持数据栈限制检查的ATPCS基本原理
如果在程序设计期间能够准确计算程序用到的内存大小,就不需要进行数据栈检查,但这在一般情况下很难做到,这时就需要进行数据栈的检查。
在进行数据栈的检查时,使用R10(又记作sl)作为数据栈限制指针。用户在程序中不能使用该寄存器。支持数据栈限制检查的ATPCS要满足下面的规则:
- 在已占用的栈的最低地址和sl之间必须有256字节的空间。即sl所指的内存地址必须比已经占用的栈的最低地址低256字节。当中断处理程序可以使用用户的数据栈时,在已经占用的栈的最低地址和sl之间除了必须保留的256字节的内存单元之外,还必须为中断处理预留足够的内存空间。
- 用户程序不能修改sl的值
- 数据栈栈指针sp的值必须不小于sl的值
与支持数据栈限制检查的ATPCS相关的编译/汇编选项有下面几种:
- 选项/swst(software stack limit checking)指示编译器生成的代码遵守支持数据栈限制检查的ATPCS。用户在程序设计期间不能准确计算出程序所需的所有数据栈大小时,需要指定该选项。
- 选项/noswst(no software stack limit checking)指示编译器生成的代码不支持数据栈限制检查功能。用户在程序设计期间能准确计算出程序所有的所有数据栈大小时,可以指定该选项,默认该选项。
- 选项/swstna(software stack limit checking not applicable)如果汇编程序对于是否进行数据栈检查无所谓,而与该汇编程序连续的其它程序指定了选项/swst或选项/noswst,这时该汇编程序使用选项/swstna
3.1.2 编写遵守支持数据栈限制检查的ATPCS的汇编语言程序
对于C或者C++来说,如果在编译时指定swst选项,生成的目标代码将遵守支持数据栈限制检查的ATPCS。
对于汇编程序来说,如果要遵守支持数据栈限制检查的ATPCS,用户在编写程序时,必须满足支持数据栈限制检查的ATPCS所要求的规则。然后在汇编时指定选项swst。
3.2 支持只读段位置无关(ROPI)的ATPCS
3.2.1 支持只读段位置无关的ATPCS的应用场合
位置无关的只读段可能为位置无关的代码段,也可能是只读数据段。使用 支持只读段位置无关的ATPCS可以避免必须将程序存放到特定的位置。常用于:
- 程序在运行期间动态加载到内存中。
- 程序在不同的场合,与不同的程序组合后加载到内存中。
- 在运行期间映射到不同的地址。在一些嵌入式系统中,将程序放到ROM中,运行时再加载到RAM中的不同地址(Linux常用这种方式,uboot加载Linux内核镜像文件到RAM中)。
3.2.2 支持只读段位置无关的ATPCS的程序设计
如果程序遵守支持只读段位置无关的ATPCS,需要满足如下规则:
- 当ROPI段中的代码引用同一个ROPI段中的符号时,必须是基于PC的。
- 当ROPI段中的代码引用另一个ROPI段中的符号时,必须是基于PC的。并且两个ROPI段的位置关系必须固定。
- 其它被ROPI段中的代码引用的必须是绝对地址,或基于sb的可写数据。
- ROPI段移动后,对ROPI中符号的引用要作相应的调整。
3.3 支持可读写段位置无关(RWPI)的ATPCS
如果一个程序中所有的可读写段都是位置无关,则称该程序遵守支持可读写段位置无关(RWPI)的ATPCS。使用支持可读写段位置无关的ATPCS可以避免必须将程序放到特定的位置。这时,R9通常用作静态基址寄存器,记作sb。可重入的子程序可以再内存中同时有多个实例,各个实例拥有独立的可读写段。在生成一个新的实例时,sb指向该实例的可读写段。RWPI段中的符号的计算方法为:连接器首先计算出该符号相对于RWPI段中某一特定位置的偏移量,通常该特定位置选为RWPI段的第一个字节处:在程序运行时,将该偏移量加上sb即可生成该符号的地址。
3.4 支持ARM程序和Thumb程序混合使用的ATPCS
据《Cortex-M3权威手册》中所述,Cortex-M3已经不再区分ARM指令和Thumb指令,M3中使用的时Thumb2指令集,Thumb2指令集兼容32位和16位指令,因此,该部分在目前已经用不到,就不再叙述。
3.5 处理浮点运算的ATPCS
ATPCS支持VFP体系和FPA体系两种不同的浮点硬件体系和指令集,两种体系对应的代码不兼容。
相应的,ADS的编译器和汇编器有下面6种与浮点数相关的选项。
- -fpu VFP
- -fpu FPA
- -fpu softVFP
- -fpu softVFP + VFP
- -fpu softFPA
- -fpu none
当系统种包含浮点运算部件时,可以选择上述选项。在M3内核种不包含浮点数运算单元,但M4内核中包含一个可选的浮点数运算单元(FPU),FPU符合IEEE 754标准(未实现完整的内容)。FPU为单精度浮点单元,浮点数据和计算基于IEEE Std 754-2008,IEEE二进制浮点运算标准,是VFP体系。
4 说明
本篇文章参考了《ARM体系结构与编程》(杜春雷 著)和《Cortex-M3权威指南》,属于学习笔记性质。参考资料链接:https://gitee.com/zichuanning520/htq_library