,ARM 过程调用标准(程序员
ARM编程
Procedure数组
Callide
Standard),提供了紧凑的编写例程的一种机制,定义的例程能够与其余例程交织在一块儿。最显著的一点是对这些例程来自哪里没有明确的限制。它们能够编译自 C、 Pascal、也能够是用汇编语言写成的。函数
APCS 定义了:工具
APCS 不一个单一的给定标准,而是一系列相似但在特定条件下有所区别的标准。例如,APCS-R (用于 RISC OS)规定在函数进入时设置的标志必须在函数退出时复位。在 32 位标准下,并非总能知道进入标志的(没有 USR_CPSR),因此你不须要恢复它们。如你所预料的那样,在不一样版本间没有相容性。但愿恢复标志的代码在它们未被恢复的时候可能会表现失常...学习
若是你开发一个基于 ARM 的系统,不要求你去实现 APCS。但建议你实现它,由于它不难实现,且可使你得到各类利益。可是,若是要写用来与编译后的 C 链接的汇编代码,则必须使用 APCS。编译器指望特定的条件,在你的加入(add-in)代码中必须获得知足。一个好例子是 APCS 定义 a1 到 a4 能够被破坏,而 v1 到 v6 必须被保护。如今我确信你正在挠头并自言自语“a 是什么? v 是什么?”。因此首先介绍 APCS-R 寄存器定义...测试
APCS 对咱们一般称为 R0 到 R14 的寄存器起了不一样的名字。使用汇编器预处理器的功能,你能够定义 R0 等名字,但在你修改其余人写的代码的时候,最好仍是学习使用 APCS 名字。优化
寄存器名字 | ||
Reg # |
APCS |
意义 |
R0 |
a1 |
工做寄存器 |
R1 |
a2 |
" |
R2 |
a3 |
" |
R3 |
a4 |
" |
R4 |
v1 |
必须保护 |
R5 |
v2 |
" |
R6 |
v3 |
" |
R7 |
v4 |
" |
R8 |
v5 |
" |
R9 |
v6 |
" |
R10 |
sl |
栈限制 |
R11 |
fp |
桢指针 |
R12 |
ip |
|
R13 |
sp |
栈指针 |
R14 |
lr |
链接寄存器 |
R15 |
pc |
程序计数器 |
译注:ip 是指令指针的简写。编码
这些名字不是由标准的 Acorn 的 objasm(版本 2.00)所定义的,可是 objasm 的后来版本,和其余汇编器(好比 Nick Robert 的 ASM)定义了它们。要定义一个寄存器名字,典型的,你要在程序最开始的地方使用 RN
宏指令(directive):
a1 RN 0 a2 RN 1 a3 RN 2 ...等... r13 RN 13 sp RN 13 r14 RN 14 lr RN r14 pc RN 15
这个例子展现了一些重要的东西:
程序的遵循 APCS 的部分在调用外部函数时被称为“一致”。在程序执行期间的全部时候都遵循 APCS (典型的,由编译器生成的程序)被称为“严格一致”。协议指出,假如你遵照正确的进入和退出参数,你能够在你本身的函数范围内作你须要的任何事情,而仍然 保持一致。这在有些时候是必须的,好比在写 SWI 假装(veneers)的时候使用了许多给实际的 SWI 调用的寄存器。
栈是连接起来的‘桢’的一个列表,经过一个叫作‘回溯结构’的东西来连接它们。这个结构存储在每一个桢的高端。按递减地址次序分配栈的每一块。寄存器
sp
老是指向在最当前桢中最低的使用的地址。这符合传统上的满降序栈。在 APCS-R 中,寄存器
sl
持有一个栈限制,你递减
sp
不能低于它。在当前栈指针和当前栈之间,不该该有任何其余 APCS 函数所依赖的东西,在被调用的时候,函数能够为本身设置一个栈块。
能够有多个栈区(chunk)。它们能够位于内存中的任何地址,这里没有提供规范。典型的,在可重入方式下执行的时候,这将被用于为相同的代码提供 多个栈;一个类比是 FileCore,它经过简单的设置‘状态’信息和并按要求调用相同部分的代码,来向当前可得到的 FileCore 文件系统(ADFS、RAMFS、IDEFS、SCSIFS 等)提供服务。
寄存器
fp
(桢指针)应当是零或者是指向栈回溯结构的列表中的最后一个结构,提供了一种追溯程序的方式,来反向跟踪调用的函数。
回溯结构是:
地址高端 保存代码指针 [fp] fp 指向这里 返回 lr 值 [fp, #-4] 返回 sp 值 [fp, #-8] 返回 fp 值 [fp, #-12] 指向下一个结构 [保存的 sl] [保存的 v6] [保存的 v5] [保存的 v4] [保存的 v3] [保存的 v2] [保存的 v1] [保存的 a4] [保存的 a3] [保存的 a2] [保存的 a1] [保存的 f7] 三个字 [保存的 f6] 三个字 [保存的 f5] 三个字 [保存的 f4] 三个字 地址低端
这个结构包含 4 至 27 个字,在方括号中的是可选的值。若是它们存在,则必须按给定的次序存在(例如,在内存中保存的 a3 下面能够是保存的 f4,但 a2-f5 则不能存在)。浮点值按‘内部格式’存储并占用三个字(12 字节)。
fp 寄存器指向当前执行的函数的栈回溯结构。返回 fp 值应当是零,或者是指向由调用了这个当前函数的函数创建的栈回溯结构的一个指针。而这个结构中的返回 fp 值是指向调用了调用了这个当前函数的函数的函数的栈回溯结构的一个指针;并以此类推直到第一个函数。
在函数退出的时候,把返回链接值、返回 sp 值、和返回 fp 值装载到 pc、sp、和 fp 中。
#include <stdio.h> void one(void); void two(void); void zero(void); int main(void) { one(); return 0; } void one(void) { zero(); two(); return; } void two(void) { printf("main...one...two\n"); return; } void zero(void) { return; } 当它在屏幕上输出消息的时候, APCS 回溯结构将是: fp ----> two_structure return link return sp return fp ----> one_structure ... return link return sp return fp ----> main_structure ... return link return sp return fp ----> 0 ...
所 以,咱们能够检查 fp 并参看给函数‘two’的结构,它指向给函数‘one’的结构,它指向给‘main’的结构,它指向零来终结。在这种方式下,咱们能够反向追溯整个程序并 肯定咱们是如何到达当前的崩溃点的。值得指出‘zero’函数,由于它已经被执行并退出了,此时咱们正在作它后面的打印,因此它曾经在回溯结构中,但如今 不在了。值得指出的还有对于给定代码不太可能老是生成象上面那样的一个 APCS 结构。缘由是不调用任何其余函数的函数不要求彻底的 APCS 头部。
为了更细致的理解,下面是代码是 Norcroft C v4.00 为上述代码生成的...
AREA |C$code|, CODE, READONLY IMPORT |__main| |x$codeseg| B |__main| DCB &6d,&61,&69,&6e DCB &00,&00,&00,&00 DCD &ff000008 IMPORT |x$stack_overflow| EXPORT one EXPORT main main MOV ip, sp STMFD sp!, {fp,ip,lr,pc} SUB fp, ip, #4 CMPS sp, sl BLLT |x$stack_overflow| BL one MOV a1, #0 LDMEA fp, {fp,sp,pc}^ DCB &6f,&6e,&65,&00 DCD &ff000004 EXPORT zero EXPORT two one MOV ip, sp STMFD sp!, {fp,ip,lr,pc} SUB fp, ip, #4 CMPS sp, sl BLLT |x$stack_overflow| BL zero LDMEA fp, {fp,sp,lr} B two IMPORT |_printf| two ADD a1, pc, #L000060-.-8 B |_printf| L000060 DCB &6d,&61,&69,&6e DCB &2e,&2e,&2e,&6f DCB &6e,&65,&2e,&2e DCB &2e,&74,&77,&6f DCB &0a,&00,&00,&00 zero MOVS pc, lr AREA |C$data| |x$dataseg| END
这个例子不听从 32 为体系。APCS-32 规定只是简单的说明了标志不须要被保存。因此删除 LDM 的‘^’后缀,并在函数 zero 中删除 MOVS 的‘S’后缀。则代码就与听从 32-bit 的编译器生成的同样了。
保存代码指针包含这条设置回溯结构的指令(STMFD ...)的地址再加上 12 字节。记住,对于 26-bit 代码,你须要去除其中的 PSR 来获得实际的代码地址。
如今咱们查看刚进入函数的时候:
APCS 没有定义记录、数组、和相似的格局。这样语言能够自由的定义如何进行这些活动。可是,若是你本身的实现实际上不符合 APCS 的精神,那么将不容许来自你的编译器的代码与来自其余编译器的代码链接在一块儿。典型的,使用 C 语言的惯例。
经过把返回链接值传送到程序计数器中来退出函数,而且:
在 32 位模式下,不须要对 PSR 标志进行跨越函数调用的保护。在 26 位模式下必须这样,并经过传送 lr 到 pc 中(MOVS、或 LDMFD xxx^)来暗中恢复。必须从 lr 从新装载 N、Z、C 和 V,跨越函数保护这些标志不是足够的。
对于一个简单函数(固定个数的参数,不可重入),你能够用下列指令创建一个栈回溯结构:
function_name_label MOV ip, sp STMFD sp!, {fp,ip,lr,pc} SUB fp, ip, #4
这个片断(来自上述编译后的程序)是最基本的形式。若是你要破坏其余不可破坏的寄存器,则你应该在这个 STMFD 指令中包含它们。
下一个任务是检查栈空间。若是不须要不少空间(小于 256 字节)则你可使用:
CMPS sp, sl BLLT |x$stack_overflow| 这是 C 版本 4.00 处理溢出的方式。在之后的版本中,你要调用 |__rt_stkovf_split_small|。
接着作你本身的事情...
经过下面的指令完成退出:
LDMEA fp, {fp,sp,pc}^
还有,若是你入栈了其余寄存器,则也在这里从新装载它们。选择这个简单的 LDM 退出机制的缘由是它比分支到一个特殊的函数退出处理器(handler)更容易和更合理。
用在回溯中的对这个协议的一个扩展是把函数名字嵌入到代码中。紧靠在函数(和 MOV ip, sp
)的前面的应该是:
DCD &ff0000xx
这里的‘xx’是函数名字符串的长度(包括填充和终结符)。这个字符串是字对齐、尾部填充的,而且应当被直接放置在 DCD &ff....的前面。
因此一个完整的栈回溯代码应当是:
DCB "my_function_name", 0, 0, 0, 0 DCD &ff000010 my_function_name MOV ip, sp STMFD sp!, {fp, ip, lr, pc} SUB fp, ip, #4 CMPS sp, sl ; 若是你不使用栈 BLLT |x$stack_overflow| ; 则能够省略 ...处理... LDMEA fp, {fp, sp, pc}^
要使它听从 32-bit 体系,只须简单的省略最后一个指令的‘^’。注意你不能在一个编译的 26-bit 代码中使用这个代码。实际上,你能够去除它,但这不是我愿意打赌的事情。
若是你不使用栈,而且你不须要保存任何寄存器,而且你不调用任何东西,则没有必要设置 APCS 块(但在调试阶段对跟踪问题还是有用的)。在这种状况下你能够:
my_simple_function ...处理... MOVS pc, lr
(再次,对 32 位 APCS 使用 MOV 而不是 MOVS,可是不要冒险与 26 位代码链接)。
总的来讲,有多个版本的 APCS (其实是 16 个)。咱们只关心在 RISC OS 上可能遇到的。
APCS-A
就是 APCS-Arthur;由早期的 Arthur 所定义。它已经被废弃,缘由是它有不一样的寄存器定义(对于熟练的 RISC OS 程序员它是某种异类)。它用于在 USR 模式下运行的 Arthur 应用程序。不该该使用它。
sl = R13, fp = R10, ip = R11, sp = R12, lr = R14, pc = R15。
r12
做为 sp
,而不是在体系上更天然的 r13
,是历史性的并先于 Arthur 和 RISC OS 两者。”APCS-R
就是 APCS-RISC OS。用于 RISC OS 应用程序在 USR 模式下进行操做;或在 SVC 模式下的模块/处理程序。
sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
APCS-U
就是 APCS-Unix,Acorn 的 RISCiX 使用它。它用于 RISCiX 应用程序(USR 模式)或内核(SVC 模式)。
sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
APCS-32
它是 APCS-2(-R 和 -U)的一个扩展,容许 32-bit 程序计数器,而且从执行在 USR 模式下的一个函数中退出时,容许标志不被恢复。其余事情同于 APCS-R。
Acorn C 版本 5 支持生成 32-bit 代码;在用于广域调试的 32 位工具中,它是最完整的开发发行。一个简单的测试是要求你的编译器导出汇编源码(而不是制做目标代码)。你不该该找到:
MOVS PC, R14
或者
LDMFD R13!, {Rx-x, PC}^
首先要考虑的是该死的 26/32 位问题。 简单的说,不转弯抹角绝对没有方法为两个版本的 APCS 汇编同一个通用代码。可是幸运的这不是问题。APCS 标准不会忽然改变。RISC OS 的 32 位版本也不会马上变异。因此利用这些,咱们能够设计一种支持两种版本的方案。这将远远超出 APCS,对于 RISC OS 的 32 位版本你须要使用 MSR 来处理状态和模式位,而不是使用 TEQP。许多现存的 API 实际上不须要保护标志位。因此在咱们的 32 版本中能够经过把
MOVS PC,...
变成
MOV PC,...
,和把
LDM {...}^
变成
LDM {...}
,并从新建造来解决。objasm 汇编器(v3.00 和之后)有一个
{CONFIG}
变量能够是
26
或
32
。可使用它建造宏...
my_function_name MOV ip, sp STMFD sp!, {fp, ip, lr, pc} SUB fp, ip, #4 ...处理... [ {CONFIG} = 26 LDMEA fp, {fp, sp, pc}^ | LDMEA fp, {fp, sp, pc} ]
我未测试这个代码。它(或相似的东西)好象是保持与两个版本的 APCS 相兼容的最佳方式,也是对 RISC OS 的不一样版本,26 位版本和未来的 32 位版本的最佳方法。
测试是否处于 32 位? 若是你要求你的代码有适应性,有一个最简单的方法来肯定处理器的 PC 状态:
TEQ PC, PC ; 对于 32 位是 EQ;对于 26 位是 NE
使用它你能够肯定: