融汇贯通系列之--栈(二)实战巩固

上一章节中讲了不少理论,纸上得来终觉浅,绝知此事要躬行。今天我们就在arm-linux平台下,做一些测试,加深我们的理解。看看编译器是如何使用栈的。话不多说,上代码:

#include <stdio.h>

int fun(int a, int b)
{
    int c = 10;
    return c * (a + b);
}

int main()
{
    int  a1 = 10;
    int  a2 = 10;
    char b = 'h';
    int  c[10];
    int res = fun(a1, a2);
    printf("res = %d\n", res);
    return 0;
}

对生成的可执行文件test_stack, 执行objdump -SD test_stack, 得到的反汇编的部分关键结果如下

00010440 <fun>:
   10440:       e52db004        push    {fp}            ; (str fp, [sp, #-4]!)
   10444:       e28db000        add     fp, sp, #0
   10448:       e24dd014        sub     sp, sp, #20
   1044c:       e50b0010        str     r0, [fp, #-16]
   10450:       e50b1014        str     r1, [fp, #-20]  ; 0xffffffec
   10454:       e3a0300a        mov     r3, #10
   10458:       e50b3008        str     r3, [fp, #-8]
   1045c:       e51b2010        ldr     r2, [fp, #-16]
   10460:       e51b3014        ldr     r3, [fp, #-20]  ; 0xffffffec
   10464:       e0823003        add     r3, r2, r3
   10468:       e51b2008        ldr     r2, [fp, #-8]
   1046c:       e0030392        mul     r3, r2, r3
   10470:       e1a00003        mov     r0, r3
   10474:       e28bd000        add     sp, fp, #0
   10478:       e49db004        pop     {fp}            ; (ldr fp, [sp], #4)
   1047c:       e12fff1e        bx      lr

00010480 <main>:
   10480:       e92d4800        push    {fp, lr}
   10484:       e28db004        add     fp, sp, #4
   10488:       e24dd038        sub     sp, sp, #56     ; 0x38
   1048c:       e3a0300a        mov     r3, #10
   10490:       e50b3008        str     r3, [fp, #-8]
   10494:       e3a0300a        mov     r3, #10
   10498:       e50b300c        str     r3, [fp, #-12]
   1049c:       e3a03068        mov     r3, #104        ; 0x68
   104a0:       e54b300d        strb    r3, [fp, #-13]
   104a4:       e51b100c        ldr     r1, [fp, #-12]
   104a8:       e51b0008        ldr     r0, [fp, #-8]
   104ac:       ebffffe3        bl      10440 <fun>
   104b0:       e50b0014        str     r0, [fp, #-20]  ; 0xffffffec
   104b4:       e51b1014        ldr     r1, [fp, #-20]  ; 0xffffffec
   104b8:       e59f0010        ldr     r0, [pc, #16]   ; 104d0 <main+0x50>
   104bc:       ebffff89        bl      102e8 <printf@plt>
   104c0:       e3a03000        mov     r3, #0
   104c4:       e1a00003        mov     r0, r3
   104c8:       e24bd004        sub     sp, fp, #4
   104cc:       e8bd8800        pop     {fp, pc}
   104d0:       00010544        andeq   r0, r1, r4, asr #10

// 从上main反汇编结果中可以看到fp对一个函数还是很重要的,因为这个函数中的局部变量基本上都是靠fp+偏移量来索引的,所以一旦发生函数跳//转的时候,是需要把当前的老的fp先压栈保存,后面函数返回的时候还能接着恢复原先的fp, 要不然局部变量岂不是全乱套了。

 

先从main函数看起,进入main函数的时候,先执行了push {fp, lr}  这个是把当前fp寄存器和lr寄存器的内容压栈,这也意味着它俩是在main返回之前要恢复的内容。然后是add fp,sp, #4 意思是fp = sp + 4. 这个地方是啥意思呢,刚刚不是才入栈吗。是这样的,刚才只是保存它,但是我们还是要用fp来索引各个局部变量啊后面,所以刚才push完事后,sp 指向的是lr, sp + 4正好是fp.

然后是sub sp, sp, #56 这个比较明显,把sp指针-56, 我们这里记得arm是满减栈,这里先一下子划走了一部分空间,sp指向栈顶,那么现在开始,fp到sp之间的这段空间就是main函数的活动记录。这里我们来算一下main里面局部变量占用的size:

3 * sizeof(int) + sizeof(char) + 10 * sizeof(int) = 53,奇怪了,为啥这里是56呢。我们先往下看:

   1048c:       e3a0300a        mov     r3, #10
   10490:       e50b3008        str     r3, [fp, #-8]
   10494:       e3a0300a        mov     r3, #10
   10498:       e50b300c        str     r3, [fp, #-12]
   1049c:       e3a03068        mov     r3, #104        ; 0x68
104a0: e54b300d strb r3, [fp, #-13]

r3 = 10, 然后str r3, [fp, #-8] 是把r3的内容存到fp-8的地址中,正好是main函数活动记录中的第一个有效的空间, strb是一个Byte的str, 正好对应我们的char类型。刚才我们算的是53,实际上是56,所谓我合理猜测,那个char在栈中也是占了4个字节,可能是有对齐的原因在里面吧。做了一个简单的实验验证了我的猜想。令c[0] = 0, c[9] =0; 然后再反汇编就可以大概清楚整个栈的分布如下:

 

 然后就是调用fun的环节了,可以看到

   104a4:       e51b100c        ldr     r1, [fp, #-12]
   104a8:       e51b0008        ldr     r0, [fp, #-8]
   104ac:       ebffffe3        bl      10440 <fun>

这里可以明显的看到参数是从右往左load到r1, r0的,这里竟然不是压栈,看来可能是因为传的参数不够大,编译器把参数优化到cpu寄存器里去了。然后就是一个bl 10400 跳到了fun的地盘。到了fun的地盘,栈又要开始生长了。

  10440:       e52db004        push    {fp}            ; (str fp, [sp, #-4]!)
   10444:       e28db000        add     fp, sp, #0
   10448:       e24dd014        sub     sp, sp, #20
   1044c:       e50b0010        str     r0, [fp, #-16]
   10450:       e50b1014        str     r1, [fp, #-20]  ; 0xffffffec

首先是fp压栈,这里压的是main的fp, 然后是fp = sp + 0; 这个fp就是fun自己的fp了,然后是sp  = sp - 20;

这两步执行完,fp寄存器指向了新的位置,sp也指向了新的位置。这里有点奇怪的是fun的栈竟然有20个Byte?

可是我们明明只有一个int c啊?带着这个疑问继续往下跟:

   10454:       e3a0300a        mov     r3, #10
   10458:       e50b3008        str     r3, [fp, #-8]
   1045c:       e51b2010        ldr     r2, [fp, #-16]
   10460:       e51b3014        ldr     r3, [fp, #-20]  ; 0xffffffec
   10464:       e0823003        add     r3, r2, r3
   10468:       e51b2008        ldr     r2, [fp, #-8]
   1046c:       e0030392        mul     r3, r2, r3
   10470:       e1a00003        mov     r0, r3
   10474:       e28bd000        add     sp, fp, #0
   10478:       e49db004        pop     {fp}            ; (ldr fp, [sp], #4)
   1047c:       e12fff1e        bx      lr

可以看到,fun一开始是把r0, r1的值压倒自己的栈中,这一步其实就是把函数的参数入栈,然后后面是把栈中的参数再ldr到r2,r3寄存器中,

再然后就是用add,mul完成运算,最终的结果存在r0中。再往后就是sp = fp + 0; 这一步很猛。sp的值现在变成了fp,也就是说sp现在指向的位置是fun的栈帧的基地址,然后后面的连招是pop {fp}, 好家伙,这个是把sp当前指向的地址的内容pop出来给到fp志存器,也就是现在fp又变回指向main的栈帧的基地址了,同时由于pop, sp还要再-4。最后是bx lr, PC跳转到bl之后的位置继续运行,这么一搞,我们就又回到了main作用域内。下面通过一张图来形象的表达这个阶段:

 

 回到main的世界后,我们继续往下看:

   104b0:       e50b0014        str     r0, [fp, #-20]  ; 0xffffffec
   104b4:       e51b1014        ldr     r1, [fp, #-20]  ; 0xffffffec
   104b8:       e59f0010        ldr     r0, [pc, #16]   ; 104d0 <main+0x50>
   104bc:       ebffff89        bl      102e8 <printf@plt>
   104c0:       e3a03000        mov     r3, #0
   104c4:       e1a00003        mov     r0, r3
   104c8:       e24bd004        sub     sp, fp, #4
   104cc:       e8bd8800        pop     {fp, pc}
   104d0:       00010544        andeq   r0, r1, r4, asr #10

还记得前面res的位置是fp - 20吗,以及fun函数中把返回值存在了r0里面,这里就是先把r0的内容存到fp -20 的位置,然后后面就是调用printf之前先把res给ldr到r1中,然后呢,是把pc + 16的这个地址给 ldr到r0。

//这里有个知识点,pc + 16是如何等于104d0的?
其实是流水线导致的,当指令执行到ldr r0, [pc, #16]时,pc的值要超前这个指令的地址应该是+8
也就是说虽然当前的指令的地址是104b8, 但是pc是104b8 + 8,然后再加上16就正好是104d0了。
原因就在于arm的三级流水线架构,也就是说pc指向的是取指令的地址,pc-4是解码的地址,pc-8是执行的地址。

再然后就是bl到printf处执行;

   000102e8 <printf@plt>:
   102e8:       e28fc600        add     ip, pc, #0, 12  //ip = pc+0x00>>12
   102ec:       e28cca10        add     ip, ip, #16, 20 ; 0x10000
   102f0:       e5bcfd1c        ldr     pc, [ip, #3356]!        ; 0xd1c

这一块有点难啃,涉及系统调用和动态库,我们放到后面去看。

 

posted on 2021-11-01 00:12  疾速瓜牛  阅读(109)  评论(0编辑  收藏  举报

导航