APCS 简介(ARM 进程调用规范) |
- 介绍
- 寄存器命名
- 规划要害
- 共同性
- 栈
- 回溯结构
- 实践参数
- 函数退出
- 树立栈回溯结构
- APCS 规范
- 对编码有用的东西
介绍
APCS,ARM 进程调用规范(ARMProcedureCallStandard),供给了紧凑的编写例程的一种机制,界说的例程能够与其他例程交错在一起。最明显的一点是对这些例程来自哪里没有清晰的约束。它们能够编译自 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 0a2 RN 1a3 RN 2...等...r13 RN 13sp RN 13r14 RN 14lr RN r14pc RN 15
这个比方展现了一些重要的东西:
- 寄存器能够界说多个姓名 – 你能够界说‘r13’和‘sp’二者。
- 寄存器能够界说自前面界说的寄存器 – ‘lr’界说自叫做‘r14’的寄存器。
(关于 objasm 是正确的,其他汇编器或许不是这样)
规划要害
- 函数调用应当快、小、和易于(由编译器来)优化。
- 函数应当能够妥善处理多个栈。
- 函数应当易于写可重入和可重定位的代码;首要经过把可写的数据与代码别离来完结。
- 可是最重要的是,它应当简略。这样汇编编程者能够十分简略的运用它的设备,而调试者能够十分简略的盯梢程序。
共同性
程序的遵照 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 中。
#includevoid 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_structurereturn linkreturn spreturn fp ----> one_structure... return linkreturn spreturn fp ----> main_structure... return linkreturn spreturn fp ----> 0...
所以,咱们能够查看 fp 并参看给函数‘two’的结构,它指向给函数‘one’的结构,它指向给‘main’的结构,它指向零来完结。在这种办法下,咱们能够反向追溯整个程序并 确认咱们是怎么抵达当时的崩溃点的。值得指出‘zero’函数,由于它现已被履行并退出了,此刻咱们正在做它后边的打印,所以它曾经在回溯结构中,但现在 不在了。值得指出的还有关于给定代码不太或许总是生成象上面那样的一个 APCS 结构。原因是不调用任何其他函数的函数不要求彻底的 APCS 头部。
为了更详尽的了解,下面是代码是 Norcroft C v4.00 为上述代码生成的…
AREA |C$code|, CODE, READONLYIMPORT |__main||x$codeseg|B |__main|DCB &6d,&61,&69,&6eDCB &00,&00,&00,&00DCD &ff000008IMPORT |x$stack_overflow|EXPORT oneEXPORT mainmainMOV ip, spSTMFD sp!, {fp,ip,lr,pc}SUB fp, ip, #4CMPS sp, slBLLT |x$stack_overflow|BL oneMOV a1, #0LDMEA fp, {fp,sp,pc}^DCB &6f,&6e,&65,&00DCD &ff000004EXPORT zeroEXPORT twooneMOV ip, spSTMFD sp!, {fp,ip,lr,pc}SUB fp, ip, #4CMPS sp, slBLLT |x$stack_overflow|BL zeroLDMEA fp, {fp,sp,lr}B twoIMPORT |_printf|twoADD a1, pc, #L000060-.-8B |_printf|L000060DCB &6d,&61,&69,&6eDCB &2e,&2e,&2e,&6fDCB &6e,&65,&2e,&2eDCB &2e,&74,&77,&6fDCB &0a,&00,&00,&00zeroMOVS pc, lrAREA |C$data||x$dataseg|END
这个比方不遵照 32 为系统。APCS-32 规则仅仅简略的说明晰标志不需求被保存。所以删去 LDM 的‘^’后缀,并在函数 zero 中删去 MOVS 的‘S’后缀。则代码就与遵照 32-bit 的编译器生成的相同了。
保存代码指针包含这条设置回溯结构的指令(STMFD …)的地址再加上 12 字节。记住,关于 26-bit 代码,你需求去除其间的 PSR 来得到实践的代码地址。
现在咱们查看刚进入函数的时分:
- pc总是包含下一个要被履行的指令的方位。
- lr(总是)包含着退出时要装载到pc中的值。在 26-bit 位代码中它还包含着 PSR。
- sp指向当时的栈块(chunk)约束,或它的上面。这是用于仿制暂时数据、寄存器和相似的东西到其间的当地。在 RISC OS 下,你有可挑选的至少 256 字节来扩展它。
- fp要么是零,要么指向回溯结构的最当时的部分。
- 函数实参安置成(下面)描绘的那样。
实践参数
APCS 没有界说记载、数组、和相似的格式。这样言语能够自在的界说怎么进行这些活动。可是,假设你自己的完结实践上不契合 APCS 的精力,那么将不答应来自你的编译器的代码与来自其他编译器的代码衔接在一起。典型的,运用 C 言语的常规。
- 前 4 个整数实参(或许更少!)被装载到 a1 – a4。
- 前 4 个浮点实参(或许更少!)被装载到 f0 – f3。
- 其他任何实参(假设有的话)存储在内存中,用进入函数时紧接在 sp 的值上面的字来指向。换句话说,其他的参数被压入栈顶。所以要想简略。最好界说承受 4 个或更少的参数的函数。
函数退出
经过把回来衔接值传送到程序计数器中来退出函数,而且:
- 假设函数回来一个小于等于一个字巨细的值,则把这个值放置到 a1 中。
- 假设函数回来一个浮点值,则把它放入 f0 中。
- sp、fp、sl、v1-v6、和 f4-f7 应当被康复(假设被改动了)为包含在进入函数时它所持有的值。
我测验了成心的损坏寄存器,而结果是(经常在程序彻底不同的部分)呈现不期望的和奇特的毛病。 - ip、lr、a2-a4、f1-f3 和入栈的这些实参能够被损坏。
在 32 位办法下,不需求对 PSR 标志进行跨过函数调用的维护。在 26 位办法下有必要这样,并经过传送 lr 到 pc 中(MOVS、或 LDMFD xxx^)来私自康复。有必要从 lr 从头装载 N、Z、C 和 V,跨过函数维护这些标志不是满足的。
树立栈回溯结构
关于一个简略函数(固定个数的参数,不行重入),你能够用下列指令树立一个栈回溯结构:
function_name_labelMOV ip, spSTMFD sp!, {fp,ip,lr,pc}SUB fp, ip, #4
这个片段(来自上述编译后的程序)是最基本的办法。假设你要损坏其他不行损坏的寄存器,则你应该在这个 STMFD 指令中包含它们。
下一个使命是查看栈空间。假设不需求许多空间(小于 256 字节)则你能够运用:
CMPS sp, slBLLT |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, 0DCD &ff000010my_function_nameMOV ip, spSTMFD sp!, {fp, ip, lr, pc}SUB fp, ip, #4CMPS 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 规范
总的来说,有多个版别的 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。
- PRM (p4-411) 中说“用r12作为sp,而不是在系统上更天然的r13,是历史性的并先于 Arthur 和 RISC OS 二者。”
- 栈是分段的并可按需求来扩展。
- 26-bit 程序计数器。
- 不在 FP 寄存器中传递浮点实参。
- 不行重入。标志有必要被康复。
APCS-R
便是 APCS-RISC OS。用于 RISC OS 应用程序在 USR 办法下进行操作;或在 SVC 办法下的模块/处理程序。
- sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
- 它是仅有的最通用的 APCS 版别。由于一切编译的 C 程序都运用 APCS-R。
- 显式的栈约束查看。
- 26-bit 程序计数器。
- 不在 FP 寄存器中传递浮点实参。
- 不行重入。标志有必要被康复。
APCS-U
便是 APCS-Unix,Acorn 的 RISCiX 运用它。它用于 RISCiX 应用程序(USR 办法)或内核(SVC 办法)。
- sl = R10, fp = R11, ip = R12, sp = R13, lr = R14, pc = R15。
- 隐式的栈约束查看(运用 sl)。
- 26-bit 程序计数器。
- 不在 FP 寄存器中传递浮点实参。
- 不行重入。标志有必要被康复。
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_nameMOV ip, spSTMFD sp!, {fp, ip, lr, pc}SUB fp, ip, #4...处理...[ {CONFIG} = 26LDMEA fp, {fp, sp, pc}^|LDMEA fp, {fp, sp, pc}]
我未测验这个代码。它(或相似的东西)好象是坚持与两个版别的 APCS 相兼容的最佳办法,也是对 RISC OS 的不同版别,26 位版别和将来的 32 位版别的最佳办法。
测验是否处于 32 位? 假设你要求你的代码有适应性,有一个最简略的办法来确认处理器的 PC 状况:
TEQ PC, PC ; 关于 32 位是 EQ;关于 26 位是 NE
运用它你能够确认:
- 26 位 PC,或许是 APCS-R 或 APCS-32。
- 32 位 PC,不能 APCS-R。一切 26-bit 代码(TEQP 等)面临着失利!