分类: LINUX
2011-09-09 10:01:49
APCS,ARM 过程调用标准(ARM Procedure Call Standard),提供了紧凑的编写例程的一种机制,定义的例程可以与其他例程交织在一起。最显著的一点是对这些例程来自哪里没有明确的限制。它们可以编译自 C、 Pascal、也可以是用汇编语言写成的。
APCS 定义了:
对寄存器使用的限制。
使用栈的惯例。
在函数调用之间传递/返回参数。
可以被‘回溯’的基于栈的结构的格式,用来提供从失败点到程序入口的函数(和给予的参数)的列表。
APCS 不一个单一的给定标准,而是一系列类似但在特定条件下有所区别的标准。例如,APCS-R (用于 RISC OS)规定在函数进入时设置的标志必须在函数退出时复位。在 32 位标准下,并不是总能知道进入标志的(没有 USR_CPSR),所以你不需要恢复它们。如你所预料的那样,在不同版本间没有相容性。希望恢复标志的代码在它们未被恢复的时候可能会表现失常...
如果你开发一个基于 ARM 的系统,不要求你去实现 APCS。但建议你实现它,因为它不难实现,且可以使你获得各种利益。但是,如果要写用来与编译后的 C 连接的汇编代码,则必须使用 APCS。编译器期望特定的条件,在你的加入(add-in)代码中必须得到满足。一个好例子是 APCS 定义 a1 到 a4 可以被破坏,而 v1 到 v6 必须被保护
标准:
总的来说,有多个版本的 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}^
APCS-R 寄存器定义:
APCS 对我们通常称为 R0 到 R14 的寄存器起了不同的名字。使用汇编器预处理器的功能,你可以定义 R0 等名字,但在你修改其他人写的代码的时候,最好还是学习使用 APCS 名字。
(注: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
这个例子展示了一些重要的东西
寄存器可以定义多个名字 - 可以定义‘r13’和‘sp’二者。
寄存器可以定义自前面定义的寄存器 - ‘lr’定义自叫做‘r14’的寄存器。
(对于 objasm 是正确的,其他汇编器可能不是这样)
函数调用应当快、小、和易于(由编译器来)优化。
函数应当可以妥善处理多个栈。
函数应当易于写可重入和可重定位的代码;主要通过把可写的数据与代码分离来实现。
但是最重要的是,它应当简单。这样汇编编程者可以非常容易的使用它的设施,而调试者能够非常容易的跟踪程序。
栈是链接起来的‘桢’的一个列表,通过一个叫做‘回溯结构’的东西来链接它们。这个结构存储在每个桢的高端。按递减地址次序分配栈的每一块。寄存器 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 中。
三 APCS举例:
test.c
#include
void func1(int p1, int p2, int p3, int p4, int p5)
{
return;
}
int main(int argc, char*argv[])
{
int i = 10, j=5;
func1(i, j, 0, 1, 2);
return 0;
}
将test.c转换为汇编test.s
func1:
@ args = 4, pretend = 0, frame = 16
@ frame_needed = 1, current_function_anonymous_args =
mov ip, sp
stmfd sp!, {fp, ip, lr, pc}
sub fp, ip, #4
sub sp, sp, #16
str r0, [fp, #-16]
str r1, [fp, #-20]
str r2, [fp, #-24]
str r3, [fp, #-28]
b .L2
.L2:
ldmea fp, {fp, sp, pc}
main:
@ args = 0, pretend = 0, frame = 16
@ frame_needed = 1, current_function_anonymous_args =0
mov ip, sp
stmfd sp!, {fp, ip, lr, pc}
sub fp, ip, #4
sub sp, sp, #20
str r0, [fp, #-16]
str r1, [fp, #-20]
bl __gccmain
mov r3, #10
str r3, [fp, #-24]
mov r3, #5
str r3, [fp, #-28]
mov r3, #2
str r3, [sp, #0]
ldr r0, [fp, #-24]
ldr r1, [fp, #-28]
mov r2, #0
mov r3, #1
bl func1
mov r0, #0
b .L3
.L3:
ldmea fp, {fp, sp, pc}
上边代码体现了回溯结构,但是代码总是不直观,下面上图。
从上图可以看出:
1. 栈是链接起来的‘桢’的一个列表,通过一个叫做‘回溯结构’的东西来链接它们。这
个结构存储在每个桢的高端。按递减地址次序分配栈的每一块。
2. 寄存器sp 总是指向在最当前桢中最低的使用的地址。寄存器fp(桢指针)应当是零
或者是指向栈回溯结构的列表中的最后一个结构,提供了一种追溯程序的方式,
来反向跟踪调用的函数。
3. 函数参数传递会尽可能的用寄存器,当函数参数大于4个时会用栈传递剩余参数。
而x86下只有有限的寄存器,仅当指定regparm时,才会使用寄存器传递参数。
4. 从上层函数传递过来的参数会保存在该函数的栈中。当然我们也可以用-fomitframe-
pointer 的gcc 编译选项把fp 给去掉,好处是代码变短了,但是函数变得不
可重入,所以如果有递归函数则不可去加入此选项.
四 APCS编码:
大概来说,回溯结构是在编译程序时就应该存在的一个东西,每个帧之间通过回溯结构连接起来。现在要做的就是找出当前执行函数
的fp(帧指针)。帧结构之间是通过fp进行连接的。找到当前的fp,就可以回溯这整个回溯结构。也即可以实现函数执行流程的跟踪。
在基于Linux系统实现的backtrace.S的情况下,现在只需要找到FP,GNU在C语言中使用汇编的结构如下:
struct pt_regs ptr[1];
asm volatile(
"stmia %0, {r0 - r15}\n\t"
:
: "r" (ptr)
: "memory"
);
struct pt_regs是一个ARM寄存器的结构体。使用汇编将r0-r15的值copy到ptr里面去。
这样就获取了当前的FP指针。
c_backtrace函数是backtrace.S中实现的函数,该函数有两个参数。一个是fp的值,一个是使用32的APCS,还是使用26位的APCS。
c_backtrace函数内部的实现大概流程是:
在函数传递了fp指针后,函数首先看看是使用26位还是32位APCS。然后检查整个fp是否为0,如果是0就说明没有回溯结构,就不需要回溯。
如果不为0,那么就需要回溯。因为参数传递了fp , 那么fp的地址存储的就是上一个帧的pc寄存器处。这样,可以按照APCS的结构依次访问
APCS里面每个寄存器了。在pc向下12个字节处就是另外一个fp了,这样就又可以指向上上一个帧结构了。循环做同样的事,直到fp中的内容为0
就结束。
APCS代码编译:
在现在的代码里面编译是没有回溯结构的,原因在于使用-O2,其优化了程序,所以把回溯结构优化掉了,为了使程序结构变小。但是现在既要优化程序
又要保留回溯结构。所以在编译时加上了-mapcs-frame。请参看GNU手册《Using the GNU Compiler Collection》
如果想得到system.map ,使用arm-eabi-nm app.elf > system.map