ARM procedure call standard(APCS),ARM过程调用标准提供了紧凑编写历程的一种机制,方便C语言和汇编或者其他类型的语言之间相互进行调用。基本的ATPCS规则包括寄存器的使用规则、数据栈使用规则、参数传递规则。
1. APCS规则
1.1. 寄存器的使用规则
ARM处理器中有r0 - r15共16个寄存器,寄存器用途给定约定的使用规则,并给这些寄存器命名了相应的别名。寄存器具体的使用规则总结:
- 子程序通过寄存器r0 ~ r3来传递参数,这是可以使用他们的别名a0 ~ a3,被调用的子程序返回时无需回复r0 ~ r3的内容。函数返回值可以使用r0寄存器来完成。
- r4 ~ r10寄存器来保存局部变量,如果子程序中大量使用这些寄存器,在子函数中调用子函数也大量使用这些寄存区,这个时候这些局部变量在函数调用时也需要和r11 ~ r15一起压栈。
- r11 ~ r15寄存器用作函数栈调用,分别去了不同的名字完成特定的功能。
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 | 程序计数器,记录程序当前执行地址 |
ATPCS规则寄存器命令与功能说明表
1.2. 数据栈使用规则
数据栈有两个增长方向:向内存地址减小的方向增长时,称为descending栈;向内存地址增加的方向增长时,称为ascending栈。所谓数据栈的增长就是移动栈指针。当栈指针指向栈顶元素(最后一个入栈的数据)时,称为FULL栈;当栈指针指向栈顶元素(最后一个入栈的数据)相邻的一个空的数据单元时,称为empty栈。综合这两个点,数据栈共有4种组合:
- FD: full descending,满递减
- ED:empty descending,空递减
- FA: full ascending,满递增
- EA: empty ascending,空递减
APCS规定数据栈为FD栈类型,并且对数据栈的操作是8字节对齐的,使用stmdb/ldmia批量内存访问指令来操作FD数据栈。使用stmdb命令往数据占中保存内容时,先递减sp指针,在保存数据,使用ldmia命令从数据栈中恢复数据时,先获取数据,在递增sp指针,sp指针总是指向栈顶元素,这刚好是FD栈的定义。
2. C代码调用分析
代码结构首先通过start.S函数进行启动引导,然后跳转到C代码处理函数中。在汇编引导函数中,必须根据自己实际的硬件情况指定sp寄存器的栈地址,这个地址保证数据是可以实现存取,主要是用来进行压栈和临时变量的存取功能。
.text
.global _start
_start:
/* 设置内存: sp 栈 */
ldr sp, =4096 /* nand启动 */
// ldr sp, =0x40000000+4096 /* nor启动 */
/* 调用main */
bl main
halt:
b halt
int copy_code_to_sdram(unsigned char *buf, unsigned long start_addr, int size)
{
int ret = 0;
int i = 0;
if (0 != buf)
{
for (i = 0; i < size; i++)
*buf = *((char *)start_addr);
}
return ret;
}
int main()
{
unsigned int *pGPFCON = (unsigned int *)0x56000050;
unsigned int *pGPFDAT = (unsigned int *)0x56000054;
unsigned int *SDRAM_BASE = (unsigned int *)0x30000000;
/* 配置GPF4为输出引脚 */
*pGPFCON = 0x100;
/* 可执行文件内容拷贝 */
const long start_addr = 0x0;
const int file_size = 1024;
int ret = copy_code_to_sdram((char *)SDRAM_BASE, start_addr, file_size);
if (ret != 0)
return ret;
/* 设置GPF4输出0 */
*pGPFDAT = 0;
return 0;
}
产生的汇编代码
led.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
0: e3a0da01 mov sp, #4096 ; 0x1000
4: eb000020 bl 8c <main>
00000008 <halt>:
8: eafffffe b 8 <halt>
0000000c <copy_code_to_sdram>:
c: e1a0c00d mov ip, sp
10: e92dd800 stmdb sp!, {fp, ip, lr, pc}
14: e24cb004 sub fp, ip, #4 ; 0x4
18: e24dd014 sub sp, sp, #20 ; 0x14
1c: e50b0010 str r0, [fp, #-16]
20: e50b1014 str r1, [fp, #-20]
24: e50b2018 str r2, [fp, #-24]
28: e3a03000 mov r3, #0 ; 0x0
2c: e50b301c str r3, [fp, #-28]
30: e3a03000 mov r3, #0 ; 0x0
34: e50b3020 str r3, [fp, #-32]
38: e51b3010 ldr r3, [fp, #-16]
3c: e3530000 cmp r3, #0 ; 0x0
40: 0a00000d beq 7c <copy_code_to_sdram+0x70>
44: e3a03000 mov r3, #0 ; 0x0
48: e50b3020 str r3, [fp, #-32]
4c: e51b2020 ldr r2, [fp, #-32]
50: e51b3018 ldr r3, [fp, #-24]
54: e1520003 cmp r2, r3
58: aa000007 bge 7c <copy_code_to_sdram+0x70>
5c: e51b2010 ldr r2, [fp, #-16]
60: e51b3014 ldr r3, [fp, #-20]
64: e5d33000 ldrb r3, [r3]
68: e5c23000 strb r3, [r2]
6c: e51b3020 ldr r3, [fp, #-32]
70: e2833001 add r3, r3, #1 ; 0x1
74: e50b3020 str r3, [fp, #-32]
78: eafffff3 b 4c <copy_code_to_sdram+0x40>
7c: e51b301c ldr r3, [fp, #-28]
80: e1a00003 mov r0, r3
84: e24bd00c sub sp, fp, #12 ; 0xc
88: e89da800 ldmia sp, {fp, sp, pc}
0000008c <main>:
8c: e1a0c00d mov ip, sp
90: e92dd800 stmdb sp!, {fp, ip, lr, pc}
94: e24cb004 sub fp, ip, #4 ; 0x4
98: e24dd01c sub sp, sp, #28 ; 0x1c
9c: e3a03456 mov r3, #1442840576 ; 0x56000000
a0: e2833050 add r3, r3, #80 ; 0x50
a4: e50b3010 str r3, [fp, #-16]
a8: e3a03456 mov r3, #1442840576 ; 0x56000000
ac: e2833054 add r3, r3, #84 ; 0x54
b0: e50b3014 str r3, [fp, #-20]
b4: e3a03203 mov r3, #805306368 ; 0x30000000
b8: e50b3018 str r3, [fp, #-24]
bc: e51b2010 ldr r2, [fp, #-16]
c0: e3a03c01 mov r3, #256 ; 0x100
c4: e5823000 str r3, [r2]
c8: e3a03000 mov r3, #0 ; 0x0
cc: e50b301c str r3, [fp, #-28]
d0: e3a03b01 mov r3, #1024 ; 0x400
d4: e50b3020 str r3, [fp, #-32]
d8: e51b0018 ldr r0, [fp, #-24]
dc: e51b101c ldr r1, [fp, #-28]
e0: e51b2020 ldr r2, [fp, #-32]
e4: ebffffc8 bl c <copy_code_to_sdram>
e8: e1a03000 mov r3, r0
ec: e50b3024 str r3, [fp, #-36]
f0: e51b3024 ldr r3, [fp, #-36]
f4: e3530000 cmp r3, #0 ; 0x0
f8: 0a000002 beq 108 <main+0x7c>
fc: e51b3024 ldr r3, [fp, #-36]
100: e50b3028 str r3, [fp, #-40]
104: ea000004 b 11c <main+0x90>
108: e51b2014 ldr r2, [fp, #-20]
10c: e3a03000 mov r3, #0 ; 0x0
110: e5823000 str r3, [r2]
114: e3a03000 mov r3, #0 ; 0x0
118: e50b3028 str r3, [fp, #-40]
11c: e51b0028 ldr r0, [fp, #-40]
120: e24bd00c sub sp, fp, #12 ; 0xc
124: e89da800 ldmia sp, {fp, sp, pc}
Disassembly of section .comment:
00000000 <.comment>:
0: 43434700 cmpmi r3, #0 ; 0x0
4: 4728203a undefined
8: 2029554e eorcs r5, r9, lr, asr #10
c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1}
10: Address 0x10 is out of bounds.
2.2. 具体分析
根据上面的和调用结构,进行函数调用结果的分析。
- 入口首先完成栈pc寄存器4096地址设置,然后通过bl跳转到main函数,bl跳转自动将当前bl指令的下一条指令地址赋值给lr寄存器。
- 进入main函数,计算栈之后的一个地址并给ip寄存器,使用降序栈stmdb指令进行pc、lr、ip、fp四个寄存器共16直接数据压栈。
- 函数中局部变量栈顶16后字节开始,也就是现在sp寄存器保存的地址开始为局部变量预留空间。main函数中共6个局部变量,其中包含了一个返回值复用的变量同28个字节数值。这个就是入口sp = sp-28原因。
- 中间调用copy_code_to_sdram子函数,子函数的sp指针在4096 -16 - 28位置,使用bl将当前指令下一条指令放在当前的lr寄存器。
- 之后子函数copy_codeto_sdram开始压栈,共16个字节。子函数共3个入参,之后是2个局部变量,共20字节数据,这个函数调用过程中sp寄存器现在计算结果是4096 - 44 - 36字节位置。
- 进行完逻辑处理以后,将返回值赋值给r0寄存器,然后将子函数的栈保存数据恢复给寄存器fp,sp,pc,这个和开始压栈命令是前三个进行数据对应。也就是入口位置的pc寄存器保存的内容不需要了。由于pc指针恢复成立了main函数调用是的lr寄存器值,所以进入main函数中调用copy_code_to_sdram的下一条指令进行代码执行。
- main函数继续往下执行,执行结束类似于子函数一样进行栈恢复,然后跳转到start.S文件中bl main函数下一条指令进行执行。
另外一幅比较经典的调用关系图像