一些寄存器调用约定取决于ABI(应用程序二进制接口)。在旧的APCS标准中需要FP
,但在更新的AAPCS(2003)中不需要。对于AAPCS(GCC 5.0+),FP
不一定需要使用,但肯定可以使用;调试信息会注释堆栈和帧指针的使用情况以便使用AAPCS进行堆栈跟踪和展开代码。如果函数是static
,编译器实际上并不需要遵守任何约定。
一般来说,所有的ARM寄存器都是通用的。 lr
(链接寄存器,也是R14)和pc
(程序计数器,也是R15)是特殊的并在指令集中体现。你说的lr
会指向A是正确的。 pc
和lr
是相关的。一个是“你在哪里”,另一个是“你曾经在哪里”。它们是函数的代码方面。
通常我们有
sp
(堆栈指针,R13) 和
fp
(
frame pointer,R11)。这两个指针也是相关的。
Microsoft layout 做了一个很好的描述。堆栈用于存储函数中的临时数据或局部变量。在
foo()
和
bar()
中的任何变量都存储在这里,
在堆栈上 或可用寄存器中。
fp
从一个函数到另一个函数跟踪变量。它是该函数在堆栈上的一个
帧或图片窗口。
ABI 定义了这个
帧的布局。通常编译器会将
lr
和其他寄存器以及先前的
fp
值保存在此处。这样就形成了一个堆栈帧的
链接列表,如果需要,您可以一直追溯到
main()
。根是
fp
,它指向一个堆栈帧(类似于
struct
)并且结构体中的一个变量是前一个
fp
。您可以沿着列表走,直到最终的
fp
,通常为
NULL
。
所以
sp
是栈的位置,
fp
是栈曾经的位置,很像
pc
和
lr
。每个旧的
lr
(链接寄存器)都存储在旧的
fp
(帧指针)中。
sp
和
fp
是函数的
数据方面。
您的点
B是活动的
pc
和
sp
。点
A实际上是
fp
和
lr
;除非您调用另一个函数,然后编译器可能准备设置
fp
以指向
B中的数据。
以下是一些ARM汇编代码,可以演示所有这些内容是如何工作的。这将因编译器优化方式而异,但应该可以给出一个想法。
; Prologue - setup
mov ip, sp ; get a copy of sp.
stmdb sp!, {fp, ip, lr, pc} ; Save the frame on the stack. See Addendum
sub fp, ip, #4 ; Set the new frame pointer.
...
; Maybe other functions called here.
; Older caller return lr
stored in stack frame.
bl baz
...
; Epilogue - return
ldm sp, {fp, sp, lr} ; restore stack, frame pointer and old link.
... ; maybe more stuff here.
bx lr ; return.
This is what
foo()
would look like. If you don't call
bar()
, then the compiler does a
leaf optimization and doesn't need to save the
frame; only the
bx lr
is needed. Most likely this maybe why you are confused by web examples. It is not always the same.
关键点是,
pc
和 lr
是相关的代码寄存器。一个是“你所在的地方”,另一个是“你之前所在的地方”。
sp
和 fp
是相关的本地数据寄存器。
一个是“本地数据所在的位置”,另一个是“上一个本地数据所在的位置”。
- 它们与parameter passing一起协作,创建函数机制。
- 很难描述一个通用情况,因为我们希望编译器尽可能快,所以它们会使用所有技巧。
这些概念对于所有 CPU 和编译语言都是通用的,尽管细节可能会有所不同。使用链接寄存器、帧指针是function prologue和 epilogue 的一部分,如果你理解了其中的内容,就知道 ARM 上的堆栈溢出是如何工作的了。
请参阅:
ARM调用约定。
MSDN ARM堆栈文章
剑桥大学APCS概述
ARM堆栈跟踪博客
Apple ABI链接
基本框架布局如下:
- fp[-0] 保存了
pc
,即我们存储这个帧的地方。
- fp[-1] 保存了
lr
,即此函数的返回地址。
- fp[-2] 保存了此函数“吃掉”堆栈之前的上一个
sp
。
- fp[-3] 保存了上一个 堆栈帧 的
fp
。
- 许多可选寄存器...
ABI 可能使用其他值,但上述值是大多数设置的典型值。上面的索引是针对 32 位值的,因为所有 ARM 寄存器都是 32 位的。如果您以字节为中心,请乘以四。该帧也至少对齐到四个字节。
补充说明: 这不是汇编器中的错误;这是正常现象。请参阅 ARM generated prologs 中的解释。
foo()
在调用bar()
时这些变量的状态,而不是当其他东西调用foo()
时(但也许我误解了问题)。 - Michael Burr