我试图理解链接寄存器和帧指针在ARM中是如何工作的。我去过几个网站,我想证实我的理解。
假设我有以下代码:
int foo(void)
{
// ..
bar();
// (A)
// ..
}
int bar(void)
{
// (B)
int b1;
// ..
// (C)
baz();
// (D)
}
int baz(void)
{
// (E)
int a;
int b;
// (F)
}
我打电话给foo()。链接寄存器是否包含点(A)处代码的地址,帧指针是否包含代码(B)处的地址?在声明了所有本地语之后,堆栈指针可以是bar()内的任何位置吗?
[edit]添加了另一个函数调用baz()
答案 0 :(得分:67)
某些寄存器调用约定依赖于ABI(应用程序二进制接口)。 APCS 标准中需要FP
,而较新的 AAPCS (2003)则不需要FP
。对于 AAPCS (GCC 5.0+),static
不会使用,但肯定可以;调试信息使用堆栈和帧指针进行注释,用于堆栈跟踪和使用 AAPCS 展开代码。如果函数是lr
,则编译器实际上不必遵守任何约定。
通常所有ARM寄存器都是通用。 pc
(链接寄存器,也是R14)和lr
(程序计数器也是R15)是特殊的,并且在指令集中包含。你是正确的pc
指向 A 。 lr
和sp
是相关的。一个是“你在哪里”,另一个是“你在哪里”。它们是函数的代码方面。
通常,我们有fp
(堆栈指针,R13)和foo()
(frame pointer,R11)。这两者也有关系。这个
Microsoft layout做得很好。 堆栈用于在您的函数中存储临时数据或 locals 。 bar()
和fp
中的任何变量都存储在此处,位于堆栈或可用寄存器中。 lr
跟踪从函数到函数的变量。它是该功能的堆栈上的帧或图片窗口。 ABI 定义了此框架的布局。通常,fp
和其他寄存器由编译器保存在幕后,以及main()
的先前值。这会生成堆栈帧的链接列表,如果需要,您可以将其追溯到fp
。 root 是struct
,它指向一个堆栈帧(如struct
),fp
中的一个变量是前一个fp
。您可以查看列表,直至最终NULL
,通常为sp
。
所以fp
是堆栈的位置,pc
是堆栈的位置,很像lr
和lr
。每个旧fp
(链接寄存器)都存储在旧sp
(帧指针)中。 fp
和pc
是函数的数据方面。
您的观点 B 是有效sp
和fp
。点 A 实际上是lr
和fp
;除非你调用另一个函数,然后编译器可能已准备好设置; 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.
以指向 B 中的数据。
; 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.
以下是一些ARM汇编程序,可能会演示这一切是如何工作的。这将根据编译器的优化方式而有所不同,但它应该给出一个想法,
lr
这就是foo()
的样子。如果您不调用bar()
,则编译器会执行叶优化,而不需要保存框架;只需要bx lr
。最有可能的原因可能是您对Web示例感到困惑。它并不总是一样的。
外卖应该是,
pc
和lr
是相关的代码寄存器。一个是“你在哪里”,另一个是“你在哪里”。sp
和fp
是相关的本地数据寄存器。这些概念对所有CPU和编译语言都是通用的,但细节可能有所不同。使用链接寄存器,帧指针是function prologue和结尾的一部分,如果您了解了所有内容,就会知道堆栈是如何溢出的适用于ARM。
另请参阅:ARM calling convention
MSDN ARM stack article
University of Cambridge APCS overview
ARM stack trace blog
Apple ABI link
基本框架布局是,
pc
,我们存储了此框架。lr
,此函数的返回地址。 sp
,此函数吃堆栈之前。fp
,最后一个堆栈框架。ABI 可能会使用其他值,但上述内容对于大多数设置都是典型的。
附录:这不是汇编程序中的错误;这是正常的。 ARM generated prologs问题中有一个解释。
答案 1 :(得分:0)
免责声明:我认为这是大致正确的;请根据需要进行更正。
如本问答中其他地方所示,请注意,可能不需要编译器生成使用帧指针的(ABI)代码。调用堆栈中的帧通常可能需要将无用的信息放在那里。
如果编译器选项要求“无帧”(伪选项标志),则编译器可以生成较小的代码,从而使调用堆栈数据较小。调用函数被编译为仅将所需的调用信息存储在堆栈中,而被调用函数被编译为仅从堆栈中弹出所需的调用信息。
这节省了执行时间和堆栈空间-但使调用代码中的向后跟踪变得异常困难(我放弃了尝试...)
有关堆栈上调用信息的大小和形状的信息仅由编译器知道,并且该信息在编译后被丢弃。