以一段简单的函数调用开始,看看程序的上下文是如何切换的。
工具:Keil5
平台:Cortex-M7
1. 简单函数调用
1 int func(int a, int b, int c, int d, int e, int f) 2 { 3 int buf[10], i; 4 5 buf[0] = 0; 6 i = a + b + c + d + e + f; 7 8 if (i > 0) 9 { 10 buf[0] = 1; 11 } 12 return i + buf[0]; 13 } 14 15 int main() 16 { 17 int value = func(5, 6, 7, 8, 9, 10); 18 19 return value; 20 }
编译成功后仿真,看看汇编里做了啥:
1. 程序执行到C代码的17行 时调用子函数,准备切换下文,首先将func()的传参从右向左扫描,依次暂存在寄存器中(见下列汇编 1-7行)。由于传参数多达6个,而寄存器R0-R3不够用(备注1),只好将末尾的两个传参压入栈中(栈指针会偏移8个字节,备注2)。
1 0x080002E8 200A MOVS r0,#0x0A 2 0x080002EA 2109 MOVS r1,#0x09 3 0x080002EC 2308 MOVS r3,#0x08 4 0x080002EE 2207 MOVS r2,#0x07 5 0x080002F0 E9CD1000 STRD r1,r0,[sp,#0] 6 0x080002F4 2106 MOVS r1,#0x06 7 0x080002F6 2005 MOVS r0,#0x05 8 0x080002F8 F7FFFFE1 BL.W func (0x080002BE)
2. 进入func函数内,首先将上文的R4-R7寄存器及 lr 链接地址压入栈中(备注3),然后继续准备下文。
首先为数组buf开辟内存空间,大小为sp栈指针偏移0x28=40字节。
接下来要将传参准备好,由于有两个传参在栈中,需要透过sp指针取出,见下列汇编 第7行,此处对sp的指针偏移了0x3C=60字节,刚好就是(0x28=40 + (R4-R7)*4 =16 + lr*4=4)的长度,偏移回了传参压栈时的位置。
汇编9-10行要对buf[0]赋0操作,由于buf刚在栈中分配完,且栈的生长方向向下,因此sp此时指向的地址为buf[10]的最低位,即buf[0],STR指令的sp偏移即为0x00.
继续往下看,r1-r4,r6-r7保存了6个传参值,求和的结果最终存储在r5中
后续对sp及r0、r5的操作大同小异,不再赘述。
1 57: { 2 58: int buf[10], i; 3 59: 4 0x080002BE B5F0 PUSH {r4-r7,lr} 5 0x080002C0 B08A SUB sp,sp,#0x28 6 0x080002C2 4604 MOV r4,r0 7 0x080002C4 E9DD670F LDRD r6,r7,[sp,#0x3C] 8 60: buf[0] = 0; 9 0x080002C8 2000 MOVS r0,#0x00 10 0x080002CA 9000 STR r0,[sp,#0x00] 11 61: i = a + b + c + d + e + f; 12 62: 13 0x080002CC 1860 ADDS r0,r4,r1 14 0x080002CE 4410 ADD r0,r0,r2 15 0x080002D0 4418 ADD r0,r0,r3 16 0x080002D2 4430 ADD r0,r0,r6 17 0x080002D4 19C5 ADDS r5,r0,r7 18 63: if (i > 0) 19 64: { 20 0x080002D6 2D00 CMP r5,#0x00 21 0x080002D8 DD01 BLE 0x080002DE 22 65: buf[0] = 1; 23 66: } 24 0x080002DA 2001 MOVS r0,#0x01 25 0x080002DC 9000 STR r0,[sp,#0x00] 26 67: return i + buf[0]; 27 0x080002DE 9800 LDR r0,[sp,#0x00] 28 0x080002E0 4428 ADD r0,r0,r5 29 68: }
思考
备注1. 为什么传参只用到r0-r3寄存器?
经搜索,在《The ARM-THUMB Procedure Call Standard》手册上对寄存器的使用做了如下限定:
备注2. 什么时候sp指针会移动?
特殊的指令在执行时SP指针会移动,例如:PUSH/POP LDR/STR 等。
备注3. r4-r10保持上文的信息,每次入栈的寄存器数为何不一样?
根据本文例子1/2的观察看,不是每次都会将所有的临时寄存器r4-r11入栈,而是要看上文环境所使用到的寄存器个数而定。
2. 函数递归调用
若将func() 函数改造为递归调用,则栈的情况又如何呢?
1 int func(int a, int b, int c, int d, int e, int f) 2 { 3 int buf[10], i; 4 5 buf[0] = 0; 6 i = a + b + c + d + e + f; 7 8 if (i > 0) 9 { 10 buf[0] = func(a-1, b-1, c-1, d-1, e-1, f-1); 11 } 12 return i + buf[0]; 13 } 14 15 int main() 16 { 17 int value = func(5, 6, 7, 8, 9, 10); 18 19 return value; 20 }
与章节1相比,只是改动了第10行,编译下看看效果。
1. 首先是传参的扫描处理,跟章节1完全一样
1 0x0800030C 200A MOVS r0,#0x0A 2 0x0800030E 2109 MOVS r1,#0x09 3 0x08000310 2308 MOVS r3,#0x08 4 0x08000312 2207 MOVS r2,#0x07 5 0x08000314 E9CD1000 STRD r1,r0,[sp,#0] 6 0x08000318 2106 MOVS r1,#0x06 7 0x0800031A 2005 MOVS r0,#0x05 8 0x0800031C F7FFFFCF BL.W func (0x080002BE)
2. 进入函数内,首先不同的是压栈的指针数,由之前的 r4-r7 增加到 r4-r10,
sp指针的偏移也由0x28增加到0x30,多出8个字节,
汇编第6行-20行为buf[0]及i的初始化,执行过程没有变化,除了采用了不同的寄存器组
在if语句中,改成递归后的变化为:因为传参众多,此时寄存器r0-r10都被占用(r10存着 i的临时值),这就是为什么在递归调用本函数之前,入栈的寄存器增加到r10
此外还记得章节1中寄存器不足,需要压栈两个传参吗,这两个传参刚好放在sp多分配出来的8个字节空间里,其它的传参依然还是在r0-r3中。
最终递归调用的上下文环境与第一次调用时保持了一致。
1 58: { 2 59: int buf[10], i; 3 60: 4 0x080002BE E92D47F0 PUSH {r4-r10,lr} 5 0x080002C2 B08C SUB sp,sp,#0x30 6 0x080002C4 4604 MOV r4,r0 7 0x080002C6 460D MOV r5,r1 8 0x080002C8 4616 MOV r6,r2 9 0x080002CA 461F MOV r7,r3 10 0x080002CC E9DD8914 LDRD r8,r9,[sp,#0x50] 11 61: buf[0] = 0; 12 0x080002D0 2000 MOVS r0,#0x00 13 0x080002D2 9002 STR r0,[sp,#0x08] 14 62: i = a + b + c + d + e + f; 15 63: 16 0x080002D4 1960 ADDS r0,r4,r5 17 0x080002D6 4430 ADD r0,r0,r6 18 0x080002D8 4438 ADD r0,r0,r7 19 0x080002DA 4440 ADD r0,r0,r8 20 0x080002DC EB000A09 ADD r10,r0,r9 21 64: if (i > 0) 22 65: { 23 0x080002E0 F1BA0F00 CMP r10,#0x00 24 0x080002E4 DD0C BLE 0x08000300 25 66: buf[0] = func(a-1, b-1, c-1, d-1, e-1, f-1); 26 67: } 27 0x080002E6 F1A90101 SUB r1,r9,#0x01 28 0x080002EA F1A80001 SUB r0,r8,#0x01 29 0x080002EE 1E7B SUBS r3,r7,#1 30 0x080002F0 1E72 SUBS r2,r6,#1 31 0x080002F2 E9CD0100 STRD r0,r1,[sp,#0] 32 0x080002F6 1E69 SUBS r1,r5,#1 33 0x080002F8 1E60 SUBS r0,r4,#1 34 0x080002FA F7FFFFE0 BL.W func (0x080002BE) 35 0x080002FE 9002 STR r0,[sp,#0x08] 36 68: return i + buf[0]; 37 0x08000300 9802 LDR r0,[sp,#0x08] 38 0x08000302 4450 ADD r0,r0,r10 39 69: }