Nuoeriris · 2021年11月05日

Cortex-M0中断控制和系统控制(七)

为了使单独编译的C语言程序和汇编程序之间能够相互调用,必须为子程序之间的调用规定一定的规则,ATPCS就是ARM程序和THUMB程序中子程序调用的基本规则。

1. ATPCS

ATPCS即ARM Thumb Procedure Call Standard(ARM-Thumb过程调用标准)的简称,ATPCS规定了一些调用和被调用程序之间调用的基本规则,这些基本规则包括子程序调用过程中寄存器的使用规则、数据栈的使用规则、参数的传递规则。为适应一些特定的需要,对这些基本的调用规则进行一些修改得到几种不同的子程序调用规则,这些特定的调用规则包括:

  • 支持数据栈限制检查的ATPCS
  • 支持只读段位置无关的ATPCS
  • 支持可读写段位置无关的ATPCS
  • 支持ARM程序和THUMB程序混合使用的ATPCS

有调用关系的所有子程序必须遵守同一种ATPCS,编译器或者汇编器在ELF格式的目标文件中设置相应的属性,标识用户选定的ATPCS类型。对应不同类型的ATPCS规则,有相应的C语言库,连接器根据用户指定的ATPCS类型连接相应的C语言库。

使用ADS的C语言编译器编译的C语言子程序满足用户指定的ATPCS类型。而对于汇编语言程序来说,完全要依赖用户来保证各子程序满足选定的ATPCS类型。具体来说,汇编语言子程序必须满足下面三个条件:在子程序编写时必须遵守相应的ATPCS规则;数据栈的使用要遵守ATPCS规则;在汇编编译器中使用“--apcs”选项,使用“--apcs”选项并不影响代码的产生,编译器只是在各段中放置相应的属性,标识用户选定的属性。

2. ATPCS基本规则

基本ATPCS规定了在子程序调用时的一些基本规则,包括以下四个方面的内容:

  • 各寄存器的使用规则及其相应的名字
  • 数据栈的使用规则
  • 参数传递的规则
  • 函数结果返回的规则

相对于其他类型的TPCS,满足基本ATPCS的程序的执行速度更快,所占用的内存更少。但是它不能提供以下的支持:ARM程序和THUMB程序相互调用;数据以及代码的位置无关的支持;子程序的可重入性;数据栈检查的支持。而派生的其他几种特定的ATPCS就是在基本ATPCS的基础上再添加其他的规则而形成的  ,其目的就是提供上述的功能。

2.1  寄存器的使用规则

 title=

前四个寄存器R0~R3用于将参数值传递到例程中并将结果值传递出例程,并在例程中保存中间值(但通常仅在子例程调用之间),子程序通过寄存器R0~R3来传递参数,这时寄存器可以记作:A1~A4,被调用的子程序在返回前无需恢复寄存器R0~R3的内容。

在子程序中,使用R4~R11来保存局部变量,这时寄存器R4~R11可以记作:V1~V8 。如果在子程序中使用到V1~V8的某些寄存器,子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值,对于子程序中没有用到的寄存器则不必执行这些操作。在THUMB程序中,通常只能使用寄存器R4~R7来保存局部变量。

寄存器R12用作子程序间暂存寄存器,记作IP;在子程序的连接代码段中经常会有这种使用规则。

寄存器R13用作数据栈指针,记做SP,在子程序中寄存器R13不能用做其他用途。寄存器SP在进入子程序时的值和退出子程序时的值必须相等。

寄存器R14用作连接寄存器,记作LR;它用于保存子程序的返回地址,如果在子程序中保存了返回地址,则R14可用作其它的用途。

寄存器R15是程序计数器,记作PC;它不能用作其它用途。

ATPCS中的各寄存器在ARM编译器和汇编器中都是预定义的。

2.2  数据栈的使用规则

栈指针通常可以指向不同的位置,当栈指针指向栈顶元素(即最后一个入栈的数据元素)时,称为Full栈。当栈指针指向与栈顶元素相邻的一个元素时,称为Empty栈。数据栈的增长方向也可以不同,当数据栈向内存减小的地址方向增长时,称为Descending栈。当数据栈向着内存地址增加的方向增长时,称为Ascending栈。

综合这两种特点可以由以下4种数据栈:

FD(FULL Descending):递增满栈
ED(Empty Descending):递增空栈
FA(FULL Ascending):递减满栈
EA(Empty Ascending):递减空栈

ATPCS规定数据栈为FD类型,并对数据栈的操作是8字节对齐的,下面是一个数据栈的示例及相关的名词:

  1. 数据栈栈指针,stack pointer指向最后一个写入栈的数据的内存地址。
  2. 数据栈的基地址,stack base是指数据栈的最高地址。由于ATPCS中的数据栈是FD类型的,实际上数据栈中最早入栈数据占据的内存单元是基地址的下一个内存单元。
  3. 数据栈界限,stack limit是指数据栈中可以使用的最低的内存单元地址。
  4. 已占用的数据栈,used stack是指数据栈的基地址和数据栈栈指针之间的区域,其中包括数据栈栈指针对应的内存单元。
  5. 数据栈中的数据帧(stack frames) 是指在数据栈中,为子程序分配的用来保存寄存器和局部变量的区域。

    VAL(SP) <= stack base,
    VAL(SP) >= VAL(SL) >= stack limit + 256,
    VAL(LR) = return address

异常中断的处理程序可以使用被中断程序的数据栈,这时用户要保证中断的程序数据栈足够大。使用ADS编译器产生的目标代码中包含了DRFAT2格式的数据帧。在调试过程中,调试器可以使用这些数据帧来查看数据栈中的相关信息。而对于汇编语言来说,用户必须使用FRAME伪操作来描述数据栈中的数据帧。ARM汇编器根据这些伪操作在目标文件中产生相应的DRFAT2格式的数据帧。

在ARMv5TE中,批量传送指令LDRD/STRD要求数据栈是8字节对齐的,以提高数据的传送速度。用ADS编译器产生的目标文件中,外部接口的数据栈都是8字节对齐的,并且编译器将告诉连接器:本目标文件中的数据栈是8字节对齐的。而对于汇编程序来说,如果目标文件中包含了外部调用,则必须满足以下条件:外部接口的数据栈一定是8位对齐的,也就是要保证在进入该汇编代码后,直到该汇编程序调用外部代码之间,数据栈的栈指针变化为偶数个字;在汇编程序中使用PRESERVE8伪操作告诉连接器,本汇编程序是8字节对齐的。

 title=

2.3  参数的传递规则

根据参数个数是否固定,可以将子程序分为参数个数固定的子程序和参数个数可变的子程序。这两种子程序的参数传递规则是不同的。

参数个数可变的子程序参数传递规则,对于参数个数可变的子程序,当参数不超过4个时,可以使用寄存器R0~R3来进行参数传递,当参数超过4个时,还可以使用数据栈来传递参数。在参数传递时,将所有参数看做是存放在连续的内存单元中的字数据。然后,依次将各名字数据传送到寄存器R0,R1,R2,R3;如果参数多于4个,将剩余的字数据传送到数据栈中,入栈的顺序与参数顺序相反,即最后一个字数据先入栈。按照上面的规则,一个浮点数参数可以通过寄存器传递,也可以通过数据栈传递,也可能一半通过寄存器传递,另一半通过数据栈传递。

参数个数固定的子程序参数传递规则,对于参数个数固定的子程序,参数传递与参数个数可变的子程序参数传递规则不同,如果系统包含浮点运算的硬件部件,浮点参数将按照下面的规则传递:各个浮点参数按顺序处理;为每个浮点参数分配FP寄存器;分配的方法是,满足该浮点参数需要的且编号最小的一组连续的FP寄存器。第一个整数参数通过寄存器R0~R3来传递,其他参数通过数据栈传递。

2.4  子程序结果返回规则
  1. 结果为一个32位的整数时或小于32位的整数值以保留符号和符号的方式扩展为32位值的范围,可以通过寄存器R0返回。
  2. 结果为一个64位整数时,一个64位整数值被视为两个32位整数值,可以通过R0和R1返回,依此类推。
  3. 对于位数更多的结果,需要通过调用内存来传递,任何其它类型的值(例如结构化值)将转换为32位整数字序列,通过将其复制到连续的内存字中。

 title=

根据上面简单的测试可以看出:

MOVS     r2,#0x03
MOVS     r1,#0x02
MOVS     r0,#0x01

通过寄存器R0~R3来传递参数:

ADDS     r0,r3,r1
ADDS     r0,r0,r2

通过寄存器R0返回。

3. 特定的ATPCS

3.1  支持数据栈限制检查的ATPCS

如果在程序设计期间能够准确地计算出程序所需的内存总量,就不需要进行数据栈的检查,但是在通常情况下这是很难做到的,这时需要进行数据栈的检查。在进行数据栈的检查时,使用寄存器R10作为数据栈限制指针,这时寄存器R10又记作SL。用户在程序中不能控制该寄存器。具体来说,支持数据栈限制的ATPCS要满足下面的规则:在已经占有的栈的最低地址和SL之间必须有256字节的空间,也就是说,SL所指的内存地址必须比已经占用的栈的最低地址低256个字节。当中断处理程序可以使用用户的数据栈时,在已经占用的栈的最低地址和SL之间除了必须保留的256个字节的内存单元外,还必须为中断处理预留足够的内存空间;用户在程序中不能修改SL的值;数据栈栈指针SP的值必须不小于SL的值。

与支持数据栈限制检查的ATPCS相关的编译/汇编选项有下面几种:选项./ swst (编译过程中对输入文件使用堆栈检测)指示编译器生成的代码遵守支持数据栈限制检查的ATPCS,用户在程序设计期间不能够准确计算程序所需的数据栈大小时,需要指定该选项;选项./ noswst (编译过程中对输入文件不使用堆栈检测,这是编译器默认选项)指示编译器生成的代码不支持数据栈限制检查的功能,用户在程序设计期间能够准确计算出程序所需的数据栈大小,可以指定该选项;选项./ swst如果汇编程序对于是否进行数据栈检查无所谓,而与该汇编程序连接的其他程序指定了选项./ swst。

对于256字节或更少的帧,可以按如下方式检查:

CMP sp, sl
BHS no_ovf
BL |__16__rt_stkovf_split_small|
no_ovf

对于大于 256 字节的帧,可以如下方式检查:

LDR wr, framesize
ADD wr, sp
CMP wr, sl
BHS no_ovf
BL |__16__rt_stkovf_split_big|
no_ovf
MOV sp,wr
; ...
ALIGN
Framesize
DCD –Framesize
3.2  编写遵守支持数据栈限制检查的ATPCS的汇编语言程序

对于C程序和C++程序来说,如果在编译时指定了选项./swst,生成的目标代码将遵守支持数据栈限制检查的ATPCS。对于汇编语言程序来说,如果要遵守支持数据栈限制检查的ATPCS,用户在编写程序时必须满足支持数据栈限制检查的ATPCS所要求的规则,然后指定选项./swst,下面介绍用户编写汇编语言程序时的一些要求。

 title=

3.3  叶子子程序是指不调用别的程序的子程序

数据栈小于256字节的叶子子程序不许要进行数据栈检查,如果几个子程序组合起来构成的叶子子程序数据栈也小于256字节,这个规则同样适用;数据栈小于256字节的非叶子子程序可以使用下面的代码段来进行数据栈检查。

ARM程序使用:

SUB sp,sp,#size ;      #size 为sp和sl之间必须保留的空间大小
CMP sp,sl;
BLLO _ARM_stack_overflow

THUMB程序使用:

ADD sp,#-size ;         #size为sp和sl之间必须保留的空间大小
CMP sp,sl;
BLLO _THUMB_stack_overflow

数据栈大于256字节的子程序,为了保证SP的值不小于数据栈可用的内存单元最小的地址值,需要引入相应的寄存器。在使用超过 256 字节堆栈空间的例程中检查溢出更为复杂,不能简单地从SP减去帧大小。

在这种情况下,必须使用如下序列向限制检查代码建议SP的新值:

ARM程序使用下列代码:

SUB ip,sp,#size;
CMP ip,sl;
BLLO _ARM_stack_overflow

THUMB程序使用下列代码:

LDR wr,#-size;
ADD wr,sp;
CMP wr,sl;
BLLO _THUMB_stack_overflow

在编译或汇编时,/interwork (指定输入文件符合ARM/Thumb交互标准)告诉编译器或汇编器生成的目标代码遵守支持ARM-THUMB的ATPCS,它用在以下场合:

  • 程序中存在ARM程序调用THUMB程序的情况
  • 程序中存在THUMB程序调用ARM程序的情况
  • 需要连接器来进行ARM状态和THUMB状态切换的情况

在下述情况下使用选项/nointerwork:程序中不包含THUMB程序;用户自己进行ARM程序和THUMB程序切换。需要注意的是:在同一个C/C++程序中不能同时有ARM指令和THUMB指令。

 title=

\\_\\_asm 关键字用于调用内联汇编程序,可以用在C或C++源码中内嵌汇编语言,如下所示:

 title=

ATPCS规则就是定义了函数传参以及返回数据的标准,定义了寄存器在函数调用时的作用。

本篇文章也是结合网上的资料和官方的资料加上自己的理解进行解读,可能不是很全面,详细的内容大家有兴趣可以自行查阅官方文档《The ARM-THUMB Procedure Call Standard》和《ARM Software Development Toolkit Version 2.50》。

推荐阅读
关注数
6100
内容数
272
灵动MM32 MCU相关技术知识,欢迎关注~
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息