36

我试图了解链接寄存器和帧指针在 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() 内的任何位置吗?

4

2 回答 2

80

一些寄存器调用约定依赖于ABI(应用程序二进制接口)。在APCSFP标准中是必需的,而在较新的AAPCS (2003) 中则不需要。对于AAPCS (GCC 5.0+),不一定使用,但肯定可以;调试信息使用堆栈和帧指针进行注释,用于堆栈跟踪和使用AAPCS展开代码。如果一个函数是,编译器真的不需要遵守任何约定。FPstatic

通常所有 ARM 寄存器都是通用的。(lr链接寄存器,也就是 R14)和pc(程序计数器,也就是 R15)是特殊的,并且在指令集中供奉。你是正确的,lr将指向Apclr是相关的。一个是“你在哪里”,另一个是“你在哪里”。它们是函数的代码方面。

通常,我们有sp(堆栈指针,R13)和fp帧指针,R11)。这两者也有关系。这个 Microsoft 布局很好地描述了事物。堆栈用于在您的函数中存储临时数据或局部变量foo()和, 中的任何变量bar()都存储在此处、堆栈或可用寄存器中。fp跟踪函数之间的变量。它是该函数的堆栈上的框架或图片窗口。ABI定义了这个框架的布局。通常情况下lr和其他寄存器在后台被编译器保存在这里以及之前的值fp。这将创建一个堆栈帧的链接列表,如果您愿意,您可以将其一直追溯到main(). 根是,它指向fp一个堆栈帧(如 a struct),其中一个变量struct是前一个fp。您可以按照列表进行,直到fp通常的最终结果NULL

所以 thesp是堆栈所在的位置,而 thefp是堆栈所在的位置,很像pcand lr。每个旧的lr(链接寄存器)都存储在旧的fp(帧指针)中。spfp是函数的数据方面

你的B点是主动的pcspA点实际上是fpand 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.
这就是看起来的foo()样子。如果不调用bar(),则编译器会进行叶子优化,不需要保存;只有bx lr是需要的。这很可能是您对网络示例感到困惑的原因。它并不总是一样的。

外卖应该是,

  1. pclr是相关的代码寄存器。一个是“你在哪里”,另一个是“你在哪里”。
  2. sp并且fp是相关的本地数据寄存器。
    一个是“本地数据在哪里”,另一个是“最后一个本地数据在哪里”。
  3. 与参数传递一起工作以创建功能机制。
  4. 很难描述一个普遍的情况,因为我们希望编译器尽可能,所以他们使用了所有可以使用的技巧。

这些概念对所有 CPU 和编译语言都是通用的,尽管细节可能会有所不同。链接寄存器帧指针的使用是函数序言和尾声的一部分,如果您了解所有内容,您就会知道堆栈溢出在 ARM 上的工作原理。

另请参阅:ARM 调用约定
                MSDN ARM 堆栈文章
                剑桥大学 APCS 概述
                ARM 堆栈跟踪博客
                Apple ABI 链接

基本的框架布局是,

  • fp[-0] saved pc,我们存储这个帧的地方。
  • fp[-1] saved lr,这个函数的返回地址。
  • fp[-2] previous sp,在这个函数栈之前。
  • fp[-3] previous fp,最后一个堆栈帧
  • 许多可选寄存器...

ABI可以使用其他值,但以上是大多数设置的典型值。上面的索引适用于 32 位值,因为所有 ARM 寄存器都是 32 位的。如果您以字节为中心,请乘以四。该帧也与至少四个字节对齐。

附录: 这不是汇编程序的错误;这是正常的。ARM 生成的序言问题中有一个解释。

于 2013-04-01T22:00:50.003 回答
0

免责声明:我认为这大致正确;请根据需要更正。

如本问答中的其他部分所述,请注意编译器可能不需要生成使用帧指针的 (ABI) 代码。调用堆栈上的帧通常需要放在那里的无用信息。

如果编译器选项调用“无帧”(伪选项标志),则编译器可以生成更小的代码,从而使调用堆栈数据更小。调用函数被编译为只在堆栈中存储需要的调用信息,被调用函数被编译为只从堆栈中弹出需要的调用信息。

这节省了执行时间和堆栈空间——但它使得在调用代码中向后追溯非常困难(我放弃了尝试......)

有关堆栈上调用信息的大小和形状的信息只有编译器知道,并且该信息在编译后被丢弃。

于 2018-11-14T17:24:34.577 回答