4.2.1 保存函数返回地址以便在函数执行完成时返回控制权

x86

当使用CALL指令去调用一个函数时,CALL后面一条指令的地址会被保存到栈中,使用无条件跳转指令跳转到CALL中执行。  CALL指令等价于PUSH函数返回地址和JMP跳转。

  1. void f()
  2. {
  3. f();
  4. };

MSVC 2008显示了一些报错信息:

  1. c:\tmp6>cl ss.cpp /Fass.asm
  2. Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86
  3. Copyright (C) Microsoft Corporation. All rights reserved.
  4. ss.cpp
  5. c:\tmp6\ss.cpp(4) : warning C4717: f : recursive on all control paths, function will cause
  6. runtime stack overflow

但无论如何还是生成了正确的代码:

  1. ?f@@YAXXZ PROC ; f
  2. ; File c:\tmp6\ss.cpp
  3. ; Line 2
  4. push ebp
  5. mov ebp, esp
  6. ; Line 3
  7. call ?f@@YAXXZ ; f
  8. ; Line 4
  9. pop ebp
  10. ret 0
  11. ?f@@YAXXZ ENDP ; f

如果我们设置优化(/0x)标识,生成的代码将不会出现栈溢出,并且会运行的很好。

  1. ?f@@YAXXZ PROC ; f
  2. ; File c:\tmp6\ss.cpp
  3. ; Line 2
  4. $LL3@f:
  5. ; Line 3
  6. jmp SHORT $LL3@f
  7. ?f@@YAXXZ ENDP ; f

GCC 4.4.1 在这两种条件下,会生成同样的代码,而且不会有任何警告。

ARM

ARM程序员经常使用栈来保存返回地址,但有些不同。像是提到的“Hello,World!(2.3), RA保存在LR(链接寄存器)。然而,如果需要调用另外一个函数,需要多次使用LR寄存器,它的值会被保存起来。通常会在函数开始的时候保存。像我们经常看到的指令“PUSH R4-R7, LR”,在函数结尾处的指令“POP R4-R7, PC”,在函数中使⽤用到的寄存器会被保存到栈中,包括LR。

尽管如此,如果一个函数从未调用其它函数,在ARM专用术语中被叫称作叶子函数。因此,叶⼦函数不需要LR寄存器。如果一个函数很小并使用了少量的寄存器,可能不会⽤到栈。因此,是可以不使用栈而实现调用叶子函数的。在扩展ARM上不使用栈,这样就会比在x86上运行要快。在未分配栈内存或栈内存不可用的情况下,这种方式是非常有用的。