APCS 简介(ARM 过程调用标准) |
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这个例子展示了一些重要的东西:
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 来得到实际的代码地址。
现在我们查看刚进入函数的时候:
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-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}^
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
使用它你可以确定: