arm调用过程标准.docx
《arm调用过程标准.docx》由会员分享,可在线阅读,更多相关《arm调用过程标准.docx(15页珍藏版)》请在冰豆网上搜索。
arm调用过程标准
ARM过程调用标准收藏
新一篇:
第17届Jolt大奖名单
APCS简介
(ARM过程调用标准)
∙介绍
∙寄存器命名
∙设计关键
∙一致性
∙栈
∙回溯结构
∙实际参数
∙函数退出
∙建立栈回溯结构
∙APCS标准
∙对编码有用的东西
介绍
APCS,ARM过程调用标准(ARMProcedureCallStandard),提供了紧凑的编写例程的一种机制,定义的例程可以与其他例程交织在一起。
最显著的一点是对这些例程来自哪里没有明确的限制。
它们可以编译自C、Pascal、也可以是用汇编语言写成的。
APCS定义了:
∙对寄存器使用的限制。
∙使用栈的惯例。
∙在函数调用之间传递/返回参数。
∙可以被‘回溯’的基于栈的结构的格式,用来提供从失败点到程序入口的函数(和给予的参数)的列表。
APCS不一个单一的给定标准,而是一系列类似但在特定条件下有所区别的标准。
例如,APCS-R(用于RISCOS)规定在函数进入时设置的标志必须在函数退出时复位。
在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的后来版本,和其他汇编器(比如NickRobert的ASM)定义了它们。
要定义一个寄存器名字,典型的,你要在程序最开始的地方使用RN宏指令(directive):
a1RN0
a2RN1
a3RN2
...等...
r13RN13
spRN13
r14RN14
lrRNr14
pcRN15
这个例子展示了一些重要的东西:
1.寄存器可以定义多个名字-你可以定义‘r13’和‘sp’二者。
2.寄存器可以定义自前面定义的寄存器-‘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中。
#include
voidone(void);
voidtwo(void);
voidzero(void);
intmain(void)
{
one();
return0;
}
voidone(void)
{
zero();
two();
return;
}
voidtwo(void)
{
printf("main...one...two\n");
return;
}
voidzero(void)
{
return;
}
当它在屏幕上输出消息的时候,
APCS回溯结构将是:
fp---->two_structure
returnlink
returnsp
returnfp---->one_structure
...returnlink
returnsp
returnfp---->main_structure
...returnlink
returnsp
returnfp---->0
...
所以,我们可以检查fp并参看给函数‘two’的结构,它指向给函数‘one’的结构,它指向给‘main’的结构,它指向零来终结。
在这种方式下,我们可以反向追溯整个程序并确定我们是如何到达当前的崩溃点的。
值得指出‘zero’函数,因为它已经被执行并退出了,此时我们正在做它后面的打印,所以它曾经在回溯结构中,但现在不在了。
值得指出的还有对于给定代码不太可能总是生成象上面那样的一个APCS结构。
原因是不调用任何其他函数的函数不要求完全的APCS头部。
为了更细致的理解,下面是代码是NorcroftCv4.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|
EXPORTone
EXPORTmain
main
MOVip,sp
STMFDsp!
{fp,ip,lr,pc}
SUBfp,ip,#4
CMPSsp,sl
BLLT|x$stack_overflow|
BLone
MOVa1,#0
LDMEAfp,{fp,sp,pc}^
DCB&6f,&6e,&65,&00
DCD&ff000004
EXPORTzero
EXPORTtwo
one
MOVip,sp
STMFDsp!
{fp,ip,lr,pc}
SUBfp,ip,#4
CMPSsp,sl
BLLT|x$stack_overflow|
BLzero
LDMEAfp,{fp,sp,lr}
Btwo
IMPORT|_printf|
two
ADDa1,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
MOVSpc,lr
AREA|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)限制,或它的上面。
这是用于复制临时数据、寄存器和类似的东西到其中的地方。
在RISCOS下,你有可选择的至少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、或LDMFDxxx^)来暗中恢复。
必须从lr重新装载N、Z、C和V,跨越函数保护这些标志不是足够的。
建立栈回溯结构
对于一个简单函数(固定个数的参数,不可重入),你可以用下列指令建立一个栈回溯结构:
function_name_label
MOVip,sp
STMFDsp!
{fp,ip,lr,pc}
SUBfp,ip,#4
这个片段(来自上述编译后的程序)是最基本的形式。
如果你要破坏其他不可破坏的寄存器,则你应该在这个STMFD指令中包含它们。
下一个任务是检查栈空间。
如果不需要很多空间(小于256字节)则你可以使用:
CMPSsp,sl
BLLT|x$stack_overflow|
这是C版本4.00处理溢出的方式。
在以后的版本中,你要调用|__rt_stkovf_split_small|。
接着做你自己的事情...
通过下面的指令完成退出:
LDMEAfp,{fp,sp,pc}^
还有,如果你入栈了其他寄存器,则也在这里重新装载它们。
选择这个简单的LDM退出机制的原因是它比分支到一个特殊的函数退出处理器(handler)更容易和更合理。
用在回溯中的对这个协议的一个扩展是把函数名字嵌入到代码中。
紧靠在函数(和MOVip,sp)的前面的应该是:
DCD&ff0000xx
这里的‘xx’是函数名字符串的长度(包括填充和终结符)。
这个字符串是字对齐、尾部填充的,并且应当被直接放置在DCD&ff....的前面。
所以一个完整的栈回溯代码应当是:
DCB"my_function_name",0,0,0,0
DCD&ff000010
my_function_name
MOVip,sp
STMFDsp!
{fp,ip,lr,pc}
SUBfp,ip,#4
CMPSsp,sl;如果你不使用栈
BLLT|x$stack_overflow|;则可以省略
...处理...
LDMEAfp,{fp,sp,pc}^
要使它遵从32-bit体系,只须简单的省略最后一个指令的‘^’。
注意你不能在一个编译的26-bit代码中使用这个代码。
实际上,你可以去除它,但这不是我愿意打赌的事情。
如果你不使用栈,并且你不需要保存任何寄存器,并且你不调用任何东西,则没有必要设置APCS块(但在调试阶段对跟踪问题仍是有用的)。
在这种情况下你可以:
my_simple_function
...处理...
MOVSpc,lr
(再次,对32位APCS使用MOV而不是MOVS,但是不要冒险与26位代码连接)。
APCS标准
总的来说,有多个版本的APCS(实际上是16个)。
我们只关心在RISCOS上可能遇到的。
APCS-A
就是APCS-Arthur;由早期的Arthur所定义。
它已经被废弃,原因是它有不同的寄存器定义(对于熟练的RISCOS程序员它是某种异类)。
它用于在USR模式下运行的Arthur应用程序。
不应该使用它。
∙sl=R13,fp=R10,ip=R11,sp=R12,lr=R14,pc=R15。
∙PRM(p4-411)中说“用r12作为sp,而不是在体系上更自然的r13,是历史性的并先于Arthur和RISCOS二者。
”
∙栈是分段的并可按需要来扩展。
∙26-bit程序计数器。
∙不在FP寄存器中传递浮点实参。
∙不可重入。
标志必须被恢复。
APCS-R
就是APCS-RISCOS。
用于RISCOS应用程序在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。
AcornC版本5支持生成32-bit代码;在用于广域调试的32位工具中,它是最完整的开发发行。
一个简单的测试是要求你的编译器导出汇编源码(而不是制作目标代码)。
你不应该找到:
MOVSPC,R14
或者
LDMFDR13!
{Rx-x,PC}^
对编码有用的东西
首先要考虑的是该死的26/32位问题。
简单的说,不转弯抹角绝对没有方法为两个版本的APCS汇编同一个通用代码。
但是幸运的这不是问题。
APCS标准不会突然改变。
RISCOS的32位版本也不会立刻变异。
所以利用这些,我们可以设计一种支持两种版本的方案。
这将远远超出APCS,对于RISCOS的32位版本你需要使用MSR来处理状态和模式位,而不是使用TEQP。
许多现存的API实际上不需要保护标志位。
所以在我们的32版本中可以通过把MOVSPC,...变成MOVPC,...,和把LDM{...}^变成LDM{...},并重新建造来解决。
objasm汇编器(v3.00和以后)有一个{CONFIG}变量可以是26或32。
可以使用它建造宏...
my_function_name
MOVip,sp
STMFDsp!
{fp,ip,lr,pc}
SUBfp,ip,#4
...处理...
[{CONFIG}=26
LDMEAfp,{fp,sp,pc}^
|
LDMEAfp,{fp,sp,pc}
]
我未测试这个代码。
它(或类似的东西)好象是保持与两个版本的APCS相兼容的最佳方式,也是对RISCOS的不同版本,26位版本和将来的32位版本的最佳方法。
测试是否处于32位?
如果你要求你的代码有适应性,有一个最简单的方法来确定处理器的PC状态:
TEQPC,PC;对于32位是EQ;对于26位是NE
使用它你可以确定:
∙26位PC,可能是APCS-R或APCS-32。
∙32位PC,不能APCS-R。
所有26-bit代码(TEQP等)面临着失败!