我正在努力了解链接寄存器和帧指针在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()内的任意位置?
一些寄存器调用约定依赖于ABI(应用程序二进制接口)。FP
在APCS标准中是必需的,而在较新的AAPCS(2003)中则不是必需的。对于AAPCS(GCC 5.0+),FP
没有要使用,但肯定可以使用;调试信息用堆栈和帧指针进行注释,用于使用AAPCS进行堆栈跟踪和展开代码。如果一个函数是static
,那么编译器实际上不必遵守任何约定。
通常,所有ARM寄存器都是通用。lr
(链接寄存器,也称R14)和pc
(程序计数器,也称R15)是特殊的,包含在指令集中。您认为lr
将指向A是正确的。CCD_ 7和CCD_。一个是"你在哪里",另一个是"你在哪里"。它们是函数的代码方面。
通常,我们有sp
(堆栈指针,R13)和fp
(帧指针,R11)。这两者也有关联。这Microsoft布局很好地描述了事物。堆栈用于在函数中存储临时数据或局部变量。foo()
和bar()
中的任何变量都存储在此处,在堆栈上或可用寄存器中。fp
跟踪各个函数之间的变量。它是该函数堆栈上的帧或图片窗口。ABI定义了该框架的布局。通常,lr
和其他寄存器由编译器在后台保存,以及fp
的先前值。这会生成堆栈帧的链表,如果您愿意,可以一直追溯到main()
。根是fp
,它指向一个堆栈帧(如struct
),其中struct
中的一个变量是前一个fp
。您可以沿着列表一直走到最后的fp
,它通常是NULL
。
因此,sp
是堆栈所在的位置,fp
是堆栈所在位置,与pc
和lr
非常相似。每个旧的CCD_ 27(链路寄存器)被存储在旧的CCD_ 28(帧指针)中。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 returnlr
存储在堆栈帧中。bl-baz。。。;结语-返回ldm-sp,{fp,sp,lr};恢复堆栈、帧指针和旧链接。;也许这里还有更多的东西。bx-lr;返回
这就是foo()
的样子。如果不调用bar()
,则编译器执行叶优化,并且不需要保存帧;只需要CCD_ 39。很可能这就是你被网络例子弄糊涂的原因。它并不总是一样的。
外卖应该是
pc
和lr
是相关的代码寄存器。一个是"你在哪里",另一个则是"你曾经在哪里">- CCD_ 42和CCD_ 43是相关的本地数据寄存器
一个是"本地数据在哪里",另一个是是"最后一个本地数据在那里"> - 与参数传递一起工作以创建函数机械
- 很难描述一般情况,因为我们希望编译器尽可能快,所以他们会尽可能地使用各种技巧
这些概念对所有CPU和编译语言都是通用的,尽管细节可能有所不同。链接寄存器、帧指针的使用是函数序言和尾声的一部分,如果您了解了所有内容,就知道堆栈溢出是如何在ARM上工作的。
另请参阅:ARM调用约定
nbsp nbsp nbsp nbsp nbsp nbsp nbsp MSDN ARM堆栈文章
nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp;剑桥大学APCS概述
nbsp nbsp nbsp nbsp nbsp nbsp nbsp ARM堆栈跟踪博客
nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp;Apple ABI链接
基本框架布局是
- fp[-0]保存了
pc
,我们将该帧存储在其中 - fp[-1]保存了此函数的返回地址
lr
- fp[-2]之前的
sp
,在此函数吃堆栈之前 - fp[-3]前一个
fp
,最后一个堆栈帧 - 许多可选寄存器
ABI可以使用其他值,但以上是大多数设置的典型值。上面的索引是针对32位值的,因为所有ARM寄存器都是32位。如果以字节为中心,请乘以四。该帧还与至少四个字节对齐。
附录:这不是汇编程序中的错误;这是正常的。ARM生成的序言问题中有一个解释。
免责声明:我认为这大致是正确的;请根据需要进行更正。
正如本问答中其他地方所指出的;A、 请注意,编译器可能不需要生成使用帧指针的(ABI)代码。调用堆栈上的帧通常需要将无用的信息放在那里。
如果编译器选项调用"无帧"(伪选项标志),则编译器可以生成较小的代码,使调用堆栈数据保持较小。调用函数被编译为只在堆栈上存储所需的调用信息,而被调用函数被编辑为只从堆栈中弹出所需的呼叫信息。
这节省了执行时间和堆栈空间,但它使调用代码中的向后跟踪变得非常困难(我放弃了尝试…)
关于堆栈上调用信息的大小和形状的信息只有编译器知道,并且该信息在编译时被丢弃。