• 函数调用时栈在做什么?


     以一段简单的函数调用开始,看看程序的上下文是如何切换的。

    工具: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: } 
  • 相关阅读:
    从源码观测STLstd::vector
    如何设计一个更通用的查询接口
    spring内嵌cglib包,这里藏着一个大坑
    JUC之线程池基础与简单源码分析
    JUC之线程池的实现原理以及拒绝策略
    扩容新生代为什么能够提高GC的效率
    JUC之文章整理以及汇总
    JUC之Fork/Join框架
    关于Synchronized你了解多少?
    (数据科学学习手札134)pyjanitor:为pandas补充更多功能
  • 原文地址:https://www.cnblogs.com/miaoxiong/p/11087113.html
Copyright © 2020-2023  润新知