在ARM 汇编程序和C 程序之间相互调用时必须遵守 ATPCS 规则,ATPCS 规定了一些函数间调用的基本规则。
一、ATPCS 规则
ATPCS 即 ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)的简称,是基于ARM指令集和THUMB指令集过程调用的规范,规定了调用函数如何传递参数,被调用函数如何获取参数,以何种方式传递函数返回值。基本的ATPCS规则包括寄存器使用规则、数据栈使用规则、参数传递规则。
1. 寄存器使用规则
ATPCS中ARM寄存器的使用规则及其名称:
寄存器 | 别名 | 使用规则 |
R15 | pc | 程序计数器 |
R14 | lr | 链接寄存器 |
R13 | sp | 数据栈寄存器 |
R12 | ip | 子程序内部调用的scratch寄存器 |
R11 | v8、fp | ARM状态局部变量寄存器8、帧指针 |
R10 | v7、sl | ARM状态局部变量寄存器7、栈限制 |
R9 | v6 | ARM状态局部变量寄存器6 |
R8 | v5 | ARM状态局部变量寄存器5 |
R7 | v4 | ARM状态局部变量寄存器4 |
R6 | v3 | ARM状态局部变量寄存器3 |
R5 | v2 | ARM状态局部变量寄存器2 |
R4 | v1 | ARM状态局部变量寄存器1 |
R3 | a4 | 参数/结果/scratch寄存器4 |
R2 | a3 | 参数/结果/scratch寄存器3 |
R1 | a2 | 参数/结果/scratch寄存器2 |
R0 | a1 | 参数/结果/scratch寄存器1 |
寄存器 R0~R15 在 ATPCS 规则中的使用总结如下:
在函数中,通过寄存器R0~R3来传递参数,被调用的函数在返回前无需恢复寄存器R0~R3的内容
在函数中,通过寄存器R4~R11来保存局部变量
寄存器R12用作函数间scratch寄存器(别名ip,过程调用中间临时寄存器)
寄存器R13用作栈指针,记作SP,在函数中寄存器R13不能用做其他用途,寄存器SP在进入函数时的值和退出函数时的值必须相等
寄存器R14用作链接寄存器,记作LR,它用于保存函数的返回地址,如果在函数中保存了返回地址,则 R14 可用作其它的用途
寄存器R15是程序计数器,记作PC,它不能用作其他用途
2. 数据栈使用规则
数据栈有两个增长方向:向内存地址减小的方向增长时,称为DESCENDING栈;向内地址增加的方向增长时,称为ASCENDING栈。
所谓数据栈的增长就是移动栈指针。当栈指针指向栈顶元素(最后一个入栈的数据)时,称为FULL栈;当栈指针指向栈顶元素(最后一个入栈的数据)相邻的一个空的数据单元时,称为EMPTY栈。
综合以上这两个特点,数据栈可以分为以下4种:
FD Full Descending,满递减
ED Empty Descending,空递减
FA Full Ascending,满递增
EA Empty Ascending,空递增
缩写 | 名称 | 指令 | 含义 |
---|---|---|---|
FD | 满递减 | stmfd/ldmfd | 堆栈通过减小存储器的地址向下增长,堆栈指针指向内含有效数据项的最低地址 |
ED | 空递减 | stmed/ldmed | 堆栈通过减小存储器的地址向下增长,堆栈指针指向堆栈下的第一个空位置 |
FA | 满递增 | stmfa/ldmfa | 堆栈通过增大存储器的地址向上增长,堆栈指针指向内含有效数据项的最高地址 |
EA | 空递增 | stmea/ldmea | 堆栈通过增大村吃器的地址向上增长,堆栈指针指向堆栈上的第一个空位置 |
ATPCS规定数据栈为FD类型,并且对数据栈的操作是8字节对齐的。使用stmdb/ldmia批量内存访问指令来操作FD数据栈。
使用stmdb命令往数据栈中保存内容时,"先递减sp指针,再保存数据";
使用ldmia命令从数据栈中恢复数据时,"先获得数据,再递增sp指针";
sp指针总是指向栈顶元素,这刚好是FD栈的定义。
3. 参数传递规则
一般来说,当参数个数不超过4个时,使用r0~r3这4个寄存器来传递参数;如果参数个数超过4个,剩余的参数通过数据栈来传递。
二、汇编程序如何向 C 程序的函数传递参数
当参数个数 <= 4,使用寄存器R0~R3来进行参数传递
当参数个数 > 4,前四个参数按照上面方法传递,剩余参数传送到栈中,入栈的顺序与参数顺序相反,即最后一个参数先入栈
三、C 程序如何返回结果给汇编程序
结果为一个32位的整数时,通过寄存器R0返回
结果为一个64位整数时,通过R0和R1返回,依此类推.
结果为一个浮点数时,通过浮点运算部件的寄存器f0,d0或s0返回
结果为一个复合的浮点数时,通过寄存器f0-fN或者d0~dN返回
对于位数更多的结果,通过调用内存来传递
四、C 函数为何要用栈
总的来说,栈的作用就是: 保存现场/上下文,传递参数。
1. 保存现场/上下文
保存现场,也叫保存上下文。
现场,相当于案发现场,总有一些现场的情况,要记录下来的,否则被别人破坏掉之后,你就无法恢复现场了。而此处说的现场,就是指 CPU 运行的时候,用到了一些寄存器,比如 R0~R3,LR 等等,对于这些寄存器的值,如果你不保存而直接跳转到函数中去执行,那么很可能会被破坏了,因为函数执行需要用到这些寄存器。
因此在函数调用之前,应该将这些寄存器等现场,暂时保存起来,等调用函数执行完毕返回后,再恢复现场,这样 CPU 就可以正确的继续执行了。
保存寄存器的值,一般用的是 push 指令,将对应的某些寄存器的值,一个个放到栈中,即所谓的入栈。
然后待被调用的子函数执行完毕的时候,再调用 pop,把栈中的一个个的值,赋值给对应的入栈的寄存器,即所谓的出栈。
2. 传递参数
当函数被调用并且参数大于 4 个时,(不包括第 4 个参数)第 4 个参数后面的参数就保存在栈中。
五、分析一个实例
汇编程序:
.text.global _start_start: ldr sp, =4096 /* 调用main */ bl mainhalt: b halt
C程序:
int main(){ unsigned int *pGPFCON = (unsigned int *)0x56000050; unsigned int *pGPFDAT = (unsigned int *)0x56000054; *pGPFCON = 0x100; *pGPFDAT = 0; return 0;}
将上面程序进行反汇编:
Disassembly of section .text:00000000 <_start>: 0: e3a0da01 mov sp, #4096 ; 0x1000 4: eb000000 bl c 00000008 : 8: eafffffe b 8 0000000c : c: e1a0c00d mov ip, sp 10: e92dd800 stmdb sp!, {fp, ip, lr, pc} 14: e24cb004 sub fp, ip, #4 ; 0x4 18: e24dd008 sub sp, sp, #8 ; 0x8 1c: e3a03456 mov r3, #1442840576 ; 0x56000000 20: e2833050 add r3, r3, #80 ; 0x50 24: e50b3010 str r3, [fp, #-16] 28: e3a03456 mov r3, #1442840576 ; 0x56000000 2c: e2833054 add r3, r3, #84 ; 0x54 30: e50b3014 str r3, [fp, #-20] 34: e51b2010 ldr r2, [fp, #-16] 38: e3a03c01 mov r3, #256 ; 0x100 3c: e5823000 str r3, [r2] 40: e51b2014 ldr r2, [fp, #-20] 44: e3a03000 mov r3, #0 ; 0x0 48: e5823000 str r3, [r2] 4c: e3a03000 mov r3, #0 ; 0x0 50: e1a00003 mov r0, r3 54: e24bd00c sub sp, fp, #12 ; 0xc 58: e89da800 ldmia sp, {fp, sp, pc}
简单分析下上面的反汇编:
mov sp, #4096:设置栈地址在4k RAM的最高处,sp=4096;bl c :调到c地址处的main函数,并保存下一行代码地址到lr,即lr=8;mov ip, sp:给ip赋值sp的值,ip=sp=4096stmdb sp!, {fp, ip, lr, pc}:按高编号寄存器存在高地址,依次将pc、lr、ip、fp存入sp-4中;sub fp, ip, #4:fp的值为ip-4=4096-4=4092;sub sp, sp, #8:sp的值为sp-8=(4096-4x4)-8=4072;mov r3, #1442840576:r3赋值0x5600 0000; add r3, r3, #80:r3的值加0x50,即r3=0x5600 0050;str r3, [fp, #-16]:r3存入[fp-16]所在的地址,即地址4076处存放0x5600 0050;mov r3, #1442840576:r3赋值0x5600 0000; add r3, r3, #84:r3的值加0x54,即r3=0x5600 0054;str r3, [fp, #-20]:r3存入[fp-20]所在的地址,即地址4072处存放0x5600 0054;ldr r2, [fp, #-16]:r2取[fp-16]地址处的值,即[4076]地址的值,r2=0x5600 0050;mov r3, #256:r3赋值为0x100;str r3, [r2]:将r3写到r2内容所对应的地址,即0x5600 0050地址处的值为0x100;;对应c语言*pGPFCON = 0x100;;ldr r2, [fp, #-20]:r2取[fp-20]地址处的值,即[4072]地址的值,r2=0x5600 0054;mov r3, #0:r3赋值为0x00;str r3, [r2]:将r3写到r2内容所对应的地址,即0x5600 0054地址处的值为0x00;对应c语言*pGPFDAT = 0;mov r3, #0:r3赋值为0x00;mov r0, r3:r0=r3=0x00;sub sp, fp, #12:sp=fp-12=4092-12=4080;ldmia sp, {fp, sp, pc}:从栈中恢复寄存器,fp=4080地址处的值=原来的fp,sp=4084地址处的值=4096,pc=4088地址处的值=8,随后调到0x08地址处继续执行。
内存数据情况: