完全使用汇编语言来编写程序会非常的繁琐,因此通常情况下,只是使用汇编程序来完成少量必须由汇编程序才能完成的工作,而其它工作则由C语言程序来完成。这样一来,我们实际上就是在进行汇编和C的混合编程,甚至同一个程序的汇编源文件和C源文件是由不同的程序员编写的。在这种情况下,要想使不同程序员编写的汇编代码和C代码能耦合的很好,则必须有一个双方都必须遵守的规则,这就是ATPCS规则。
第一部分内容:ATPCS规则
ATPCS(ARM-Thumb Produce Call Standard)是ARM程序和Thumb程序中子程序调用的基本规则,目的是为了使单独编译的C语言程序和汇编程序之间能够相互调用。这些基本规则包括子程序调用过程中寄存器的使用规则、数据栈的使用规则和参数的传递规则。 1、寄存器的使用规则: a)、寄存器R0 -- R11被分为2组:a1 -- a4,v1 – v8。所以对于兼容ATPCS的编译器而言,在编程的时候可以使用a1替换R0 b)、除了R13 -- R15有别名外,对于兼容ATPCS的编译器而言,也可以使用其它寄存器的别名:wr, sb, sl, fp, ip,它们都有自己的一些特殊用法 c)、寄存器R0 -- R3用于传递子程序的参数和返回结果(详见本文后部) d)、寄存器a1 -- a4和ip是scratch寄存器(即:临时寄存器),其值在进行子程序调用时不需要保存和恢复。(详见本文后部) 2、数据栈的使用规则 在“其它寻址模式与其它指令”一文中,我讲到栈有4种类型:
FD (Full Descending) 满递减 ED (Empty Descending)空递减 FA (Full Ascending) 满递增 EA (Empty Ascending) 空递增 ATPCS规定数据栈为FD(满递减)类型,并且对数据栈的操作是8字节对齐的。这意味着我们在编写汇编子程序时,如果要进行出栈和入栈操作,则必须使用ldmfd和stmfd指令(或者ldmia和ldmdb);而兼容ATPCS的编译器在编译C代码时,也必须这样做。 3、参数的传递规则 参数个数固定的子程序参数传递规则:
前4个整数参数,通过寄存器R0~R3来传递。其他参数通过数据栈传递。
子程序结果返回规则
结果为一个32位的整数时,必须通过寄存器R0返回;结果为一个64位整数时,通过寄存器R0和R1返回,依次类推。 下面看一下编译器对于这几个规则的遵循(实现)情况。 参数传递(4个参数)以及结果返回 :很显然,主调程序在调用子程序(即:bl func1)之前,将要传递给子程序的4个参数准备在了R0~R3中,从而使得子程序可以通过该4个寄存器获得转递给它的参数(即:4个参数是通过寄存器R0~R3来传递的);子程序在返回之前,将返回值放在了寄存器R0中,从而使得主调函数可以通过R0来获得子程序的返回值(即:结果为一个32位的整数时,通过寄存器R0返回) 多于4个参数,前4个参数通过寄存器R0~R3来传递,其他参数通过数据栈传递。
7个参数的情景。下面是程序 int func(int a, int b, int c, int d, int e, int f, int g)
{
return(a+b+c+d+e+f+g);
}
int main()
{
int a=1,b=2,c=3,d=4,e=5,f=6,g=7;
return func(a,b,c,d,e,f,g);
} 的反汇编结果。我们通过它来分析一下当参数超过4个的时候,所谓“通过数据栈传递其它参数”是什么含义。 1 int func(int a, int b, int c, int d, int e, int f, int g)
2 {
3 func [0xe92d4010] stmfd r13!,{r4,r14}
4 000080ac [0xe59d4010] ldr r4,[r13,#0x10] //r4为第7个参数的值
5 000080b0 [0xe28de008] add r14,r13,#8 //r14指向了存放传入参数在栈中的位置
6 000080b4 [0xe89e5000] ldmia r14,{r12,r14} //r12为第5个参数的值,r14为第6个参数的值
7 return(a+b+c+d+e+f+g);
8 000080b8 [0xe0800001] add r0,r0,r1
9 000080bc [0xe0800002] add r0,r0,r2
10 000080c0 [0xe0800003] add r0,r0,r3
11 000080c4 [0xe080000c] add r0,r0,r12
12 000080c8 [0xe080000e] add r0,r0,r14
13 000080cc [0xe0800004] add r0,r0,r4
14 }
15 000080d0 [0xe8bd8010] ldmfd r13!,{r4,pc}
16 int main()
17 {
18 main [0xe92d401e] stmfd r13!,{r1-r4,r14}
19 int a=1,b=2,c=3,d=4,e=5,f=6,g=7;
20 000080d8 [0xe3a00001] mov r0,#1
21 000080dc [0xe3a0c002] mov r12,#2
22 000080e0 [0xe3a0e003] mov r14,#3
23 000080e4 [0xe3a04004] mov r4,#4
24 000080e8 [0xe3a01005] mov r1,#5
25 000080ec [0xe3a02006] mov r2,#6
26 000080f0 [0xe3a03007] mov r3,#7
27 return func(a,b,c,d,e,f,g);
28 000080f4 [0xe88d000e] stmia r13,{r1-r3} //main处的入栈操作,r1-r3实为占位符,是替第5、6、7个参数预先在栈内占位置的
29 000080f8 [0xe1a03004] mov r3,r4
30 000080fc [0xe1a0200e] mov r2,r14
31 00008100 [0xe1a0100c] mov r1,r12
32 00008104 [0xebffffe7] bl func
33 }
34 00008108 [0xe8bd801e] ldmfd r13!,{r1-r4,pc} 第3行采用的是stmfd指令实施入栈,这是因为要满足ATPCS中的“数据栈的使用规则”。而入栈的寄存器是r4和r14,r4入栈是因为r4在子程序中被破坏(使用)了,因此必须在子程序的入口入栈保存,在子程序的出口处出栈恢复(第28行);而r14要入栈则是因为r14存放的是子程序的返回地址,而r14又在子程序中被破坏(使用)了,如果不保存的话,在子程序返回(第34行)的时候,将不会正确地返回到主调程序。当然,你也许发现了r0,r1,r2,r3,r12同样在子程序中被破坏了,为什么它们不需要保存和恢复呢?这是因为“寄存器a1 -- a4和ip是scratch寄存器(即:临时寄存器),其值在进行子程序调用时不需要保存和恢复。”(见前文)。也就是说,对于主调程序的编写者而言,他应该很清楚他必须遵循ATPCS规则,所以他不会期望在子程序返回后,寄存器r0, r1, r2, r3, r12的值一定会维持原样。因此子程序的编写者也就不必保存和恢复这几个寄存器了,即使子程序破坏了它们的值。随便说一句,这条stmfd指令是由编译器自动加在子函数的第1条语句之前的,所以类推一下就应该明白,main函数运行时的第1条指令并不是程序员书写main函数的第1条语句,而是编译器添加的入栈指令。更进一步,为什么编译器要加这条入栈指令呢?因为main函数本质上也是个子函数而已,它也会被别人调用,也就是说,程序运行起来后,main函数并不是首先运行的。那么,是谁首先运行呢?当然是调用main函数的代码,这段代码被称之为:例行启动程序(boot routine),或称启动例程。它是由编译器在编译程序时自动加入的。 第20、21、22、23、29、30、31行显然是在准备(传递)前4个参数;第18、24、25、26、28行的执行,显然将后3个参数放到了栈中,而第4、5、6行完成后,子程序则将栈中的3个参数取出了。这样就完成了“多于4个的参数通过数据栈来传递”这个操作。 由此我们可以得到关于程序优化的一个结论:开始四个字大小的参数直接使用寄存器的R0-R3来传递(快速且高效的);如果需要更多的参数,将使用堆栈。(需要额外的指令和慢速的存储器操作) ;所以通常限制参数的个数,使它为4或更少,如果不可避免,把常用的参数前4个放在R0-R3中。 第二部分内容:C和ARM汇编程序间相互调用(点击下载示例代码 ) 在C和ARM汇编程序之间相互调用必须遵守ATPCS规则。C和汇编之间的相互调用可以从以下这四方面来说明: 在C语言程序中调用汇编程序
在汇编程序中调用C语言程序
汇编程序对C全局变量的访问
C程序对汇编全局变量的访问
C程序中内嵌汇编
1、在C语言程序中调用汇编子程序 为了保证程序调用时参数的正确传递,汇编程序的设计要遵守ATPCS。在汇编程序中需要使用EXPORT伪操作来声明,使得本程序可以被其它程序调用。同时,在C程序调用该汇编程序之前需要在C语言程序中使用extern关键词来声明该汇编程序。 参阅示例代码中xmain函数(在ledtest.c中)对delay函数(在delay.s中)的调用 extern int delay(int time); EXPORT delay 2、在汇编程序中调用C语言子程序 为了保证程序调用时参数的正确传递,汇编程序的设计要遵守ATPCS。在C程序中不需要使用任何关键字来声明将被汇编语言调用的C程序(只要该程序的声明前不要加static关键字),但是在汇编程序调用该C程序之前需要在汇编语言程序中使用IMPORT伪操作来声明该C程序。在汇编程序中通过BL指令来调用子程序。 参阅示例代码中init.s文件中的代码对xmain函数的调用 IMPORT xmain
bl xmain int xmain(int val) 3、汇编程序访问全局C变量 汇编程序可以通过C全局变量的地址间接访问在C语言程序中声明的全局变量。在汇编程序中,通过使用IMPORT关键词引人C全局变量,该C全局变量的名称在汇编程序中被认为是一个标号,从而汇编程序可以利用LDR和STR指令访问该标号所代表的地址处存放的内容(即:C全局变量的值)。 参阅示例代码中init.s文件中的如下几行: IMPORT i
ldr r0, i
sub r0, r0, #1
str r0, i 对于不同类型的变量,需要采用不同选项的LDR和STR指令,如下所示: unsigned char LDRB/STRB
unsigned short LDRH/STRH
unsigned int LDR/STR
char LDRSB/STRB
short LDRSH/STRH
4、C程序对汇编全局变量的访问
汇编程序中用DCD为全局变量分配空间并赋初值,并定义一个标号代表该存储位置,用EXPORT导出该标号。C程序将会将该标号视为全局变量的名称,在C程序中用extern声明该全局变量,之后就可以按正常的方式访问该全局变量了。
参阅示例代码中delay.s文件中的代码和xmain函数的代码:
EXPORT DELAYVAL
DELAYVAL
DCD 0xffff extern int DELAYVAL;
5、C程序中内嵌汇编
有些操作C语言程序是做不了的,例如:改变cpsr寄存器的值、初始化堆栈指针寄存器sp,等等,它们只能由汇编程序完成。但出于编程简洁以及其它一些因素的考虑,有时我们需要在C源代码中实现上述的操作,此时我们就必须采用在C源代码中嵌入少量汇编代码的方法来实现,这就是C程序中的内嵌汇编。
内嵌的汇编指令包括大部分的ARM指令和Thumb指令,但是不能直接引用C的变量定义,数据交换必须通过ATPCS进行,不支持诸如直接修改PC实现跳转等底层功能。嵌入式汇编语句在形式上是独立定义的函数体,其语法格式为:
__asm
{
指令[;指令]
……
[指令]
}
其中“__asm”为内嵌汇编语句的关键字,需要特别注意的是前面有两个下划线。同一行如有多条指令,则指令之间用分号分隔,如果一条指令占据多行,除最后一行外都要使用连字符“\”
例如,如果我们需要在C程序中禁用中断,那么内嵌的汇编代码如下:
__asm
{
MRS R0 CPSR
ORR R0, R0,#0x80
MSR CPSR_c,R0
} 出于完整性的考虑,最后将内嵌汇编相对于一般汇编的一些不同的特点罗列如下: 操作数可以是寄存器、常量或C表达式。它们可以是char、short或者int类型,而且是作为无符号数进行操作 。
内嵌的汇编指令中使用物理寄存器有一些限制。
常量前的符号“#”可以省略
只有指令B可以使用C程序中的标号,指令BL不能使用C程序中的标号。
不支持汇编语言中用于内存分配的伪操作。
指令中如果包含常量操作数,该指令可能会被汇编器展开成几条指令。
内嵌汇编器不支持通过“·”指示符或PC获取当前指令地址;
不支持LDR Rn,= expression伪指令,而使用MOV Rn, expression指令向寄存器赋值;
不支持标号表达式;
不支持ADR和ADRL伪指令;
不支持BX和BLX指令;
不可以向PC赋值;
使用0x前缀替代“&”表示十六进制数。
必须小心使用物理寄存器,如R0~R3,LR和PC。
不要使用寄存器寻址变量。
使用内嵌汇编时,编译器自己会保存和恢复它可能用到的寄存器,用户无须保存和恢复寄存器。
LDM和STM指令的寄存器列表只允许物理寄存器。 致谢:感谢安博中程的Michael Tang为本文制作了示意图。