7、函数调用

这一小节我们来看下arm函数调用处理流程
参考文档:
IHI0042J_2020Q2_aapcs32.pdf

7.1 函数调用代码

最简单情况,main函数中调用一个空的test函数

void test(void)
{

}

int main(void)
{
    test();
    return 0;
}

对应汇编:

00000024 <test>:
24:     b480            push    {r7}
26:     af00            add     r7, sp, #0
28:     bf00            nop
2a:     46bd            mov     sp, r7
2c:     bc80            pop     {r7}
2e:     4770            bx      lr

0000008c <main>:
8c:     b580            push    {r7, lr}
8e:     af00            add     r7, sp, #0
90:     f7ff ffc8       bl      24 <test>
94:     2300            movs    r3, #0
96:     4618            mov     r0, r3
98:     bd80            pop     {r7, pc}
9a:     bf00            nop

Note

1. 0x90行bl执行跳转指令,这时候core会把下一条指令地址0x94赋值给lr,然后把test函数的地址0x24赋值给pc。
2. r0-r3 4个寄存器用于传递参数,子程序需要负责保护的寄存器:r4-r8, r10, r11 and SP,子程序返回后这些寄存器不能变化。
3. 看我们这里的test子程序,入口先把r7保存到栈上,接下来把sp保存到r7,然后执行程序本身(因为是空函数,所以只有一条nop指令)。
4. 返回时先把r7赋值给sp,其实就是恢复入口时的sp,然后把r7从栈上取出来恢复。这样这个程序用到的sp、r7做了保存恢复工作,如果需要用到其他寄存器,处理方法一样的。
5. 最后执行bx lr,之前bl跳到子程序时,core已经帮我们把程序返回后下一条要执行的指令(0x94)赋值给lr了,所以这里bx lr,即跳转到0x94去执行,实现了函数返回。

7.2 传参和返回值

int test(int a)
{
        return 100;
}

int main(void)
{
        int ret;

        ret = test(10);
        return 0;
}
00000024 <test>:
24:     b480            push    {r7}
26:     b083            sub     sp, #12
28:     af00            add     r7, sp, #0
2a:     6078            str     r0, [r7, #4]
2c:     2364            movs    r3, #100        ; 0x64
2e:     4618            mov     r0, r3
30:     370c            adds    r7, #12
32:     46bd            mov     sp, r7
34:     bc80            pop     {r7}
36:     4770            bx      lr

00000094 <main>:
94:     b580            push    {r7, lr}
96:     b082            sub     sp, #8
98:     af00            add     r7, sp, #0
9a:     200a            movs    r0, #10
9c:     f7ff ffc2       bl      24 <test>
a0:     6078            str     r0, [r7, #4]
a2:     2300            movs    r3, #0
a4:     4618            mov     r0, r3
a6:     3708            adds    r7, #8
a8:     46bd            mov     sp, r7
aa:     bd80            pop     {r7, pc}

Note

相对上一个程序,多了入参和返回,main函数中0x9a地址的指令,把入参10赋值给了r0,子程序中0x2c地址的指令把返回值100赋值给了r3,0x2e地址的指令把r3赋值给了r0,最终100放在了r0中返回给main函数。

7.3 栈的使用

代码如下:在入口函数中增加一个Assemble_learn函数的调用,在子函数中通过push和pop操作栈

        .thumb_func
Assemble_learn:
        push    {r0-r7}                                         /* Save registers to stack , thumbe only support r0-r7*/
        add     r7, sp, #0                                      /* Save sp to r7 */

        mov             r0, #0
        mov             r1, #0
        mov             r2, #0
        mov             r3, #0
        mov             r4, #0
        mov             r5, #0
        mov             r6, #0

        mov     sp, r7                                          /* Restore sp*/
        pop             {r0-r7}                                         /* Restore registers*/
        bx              lr                                                      /* Return*/

        .thumb_func
        .globl   Reset_Handler
Reset_Handler:
        mov             r0, #0
        mov             r1, #1
        mov             r2, #2
        mov             r3, #3
        mov             r4, #4
        mov             r5, #5
        mov             r6, #6
        mov             r7, #7
        bl              Assemble_learn
        mov             r7, #6
        bl              main
        mov             r3, #4
        b               .
        mov             r2, #4

1.进入Assemble_learn之前打印sp内容: sp 0x400000 0x400000

(gdb) x/16xw 0x400000-16*4
0x3fffc0:       0x00000000      0x00000000      0x00000000      0x00000000
0x3fffd0:       0x00000000      0x00000000      0x00000000      0x00000000
0x3fffe0:       0x00000000      0x00000000      0x00000000      0x00000000
0x3ffff0:       0x00000000      0x00000000      0x00000000      0x00000000

当前sp指向 0x400000, 在栈顶,栈里面的内容都是0

2.接下来跑完 push {r0-r7} 指令后再看现场:

sp 0x3fffe0 0x3fffe0

(gdb) x/16xw 0x400000-16*4
0x3fffc0:       0x00000000      0x00000000      0x00000000      0x00000000
0x3fffd0:       0x00000000      0x00000000      0x00000000      0x00000000
0x3fffe0:       0x00000000      0x00000001      0x00000002      0x00000003
0x3ffff0:       0x00000004      0x00000005      0x00000006      0x00000007

可以看到sp指针指向了0x3fffe0,stack中最后8个4字节保存了r0-r7,其中r7最先保存

3.接下来看跑完pop {r0-r7} 指令后,现场情况

sp 0x400000 0x400000

(gdb)  x/16xw 0x400000-16*4
0x3fffc0:       0x00000000      0x00000000      0x00000000      0x00000000
0x3fffd0:       0x00000000      0x00000000      0x00000000      0x00000000
0x3fffe0:       0x00000000      0x00000001      0x00000002      0x00000003
0x3ffff0:       0x00000004      0x00000005      0x00000006      0x00000007

栈顶回到了0x400000,栈中数据还在(因为没人清),但是这部分数据不会再有人用了,当下次执行push指令时,会覆盖掉

Note

总结下函数调用:
1、执行BL跳转指令时,硬件把下一条要执行的指令放入LR,然后把子函数地址赋值给pc后执行子函数。
2、字函数执行完成后,BX lr 即可执行主函数中下一条指令,即实现了函数返回。
3、c程序入参和返回通过r0-r4来传递(超过了会使用sp),编译器会处理。
4、栈从高地址向低地址增长,通过push指令把寄存器值保存到栈上,通过pop指令恢复。