ARM: 链接寄存器和帧指针

40

我试图了解在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
我不确定你所说的“堆栈指针可能在bar()内任何位置”的意思。此外,你似乎询问的是foo()在调用bar()时这些变量的状态,而不是当其他东西调用foo()时(但也许我误解了问题)。 - Michael Burr
3
可能是ARM中的SP(堆栈)和LR是什么? 的重复问题。 - artless noise
2
不是重复;SP != FP。该链接没有提到FP。 - Nick Desaulniers
是的,在堆栈溢出的意义上,它不是重复的。它包含相关信息。SP和FP是相关的,但并不是同一件事。同样,LR和PC也是相关的,但问题可能没有提到它们。我相信那个问题的观众会想要了解函数机制。也许我应该说“相关”而不是“重复”。毫无疑问,这些问题是相关的。 - artless noise
以下链接提供了有关FP的更多详细信息,包括在编译过程中是否包含(作为标志)以及包含时的汇编输出情况,并附带示例。https://community.arm.com/developer/tools-software/tools/f/arm-compilers-forum/4553/what-is-fp-r11-used-for#:~:text=the%20fp%20stands%20for%20the,of%20offsets%20for%20the%20stack. - vijayanand1231
显示剩余2条评论
2个回答

87

一些寄存器调用约定取决于ABI(应用程序二进制接口)。在旧的APCS标准中需要FP,但在更新的AAPCS(2003)中不需要。对于AAPCS(GCC 5.0+),FP不一定需要使用,但肯定可以使用;调试信息会注释堆栈和帧指针的使用情况以便使用AAPCS进行堆栈跟踪和展开代码。如果函数是static,编译器实际上并不需要遵守任何约定。

一般来说,所有的ARM寄存器都是通用的。 lr(链接寄存器,也是R14)和pc(程序计数器,也是R15)是特殊的并在指令集中体现。你说的lr会指向A是正确的。 pclr是相关的。一个是“你在哪里”,另一个是“你曾经在哪里”。它们是函数的代码方面。

通常我们有 sp (堆栈指针,R13) 和 fp (frame pointer,R11)。这两个指针也是相关的。Microsoft layout 做了一个很好的描述。堆栈用于存储函数中的临时数据或局部变量。在 foo()bar() 中的任何变量都存储在这里,在堆栈上 或可用寄存器中。 fp 从一个函数到另一个函数跟踪变量。它是该函数在堆栈上的一个或图片窗口。 ABI 定义了这个的布局。通常编译器会将 lr 和其他寄存器以及先前的 fp 值保存在此处。这样就形成了一个堆栈帧的链接列表,如果需要,您可以一直追溯到 main()。根是 fp,它指向一个堆栈帧(类似于struct)并且结构体中的一个变量是前一个 fp。您可以沿着列表走,直到最终的 fp,通常为NULL
所以sp是栈的位置,fp是栈曾经的位置,很像pclr。每个旧的lr(链接寄存器)都存储在旧的fp(帧指针)中。spfp是函数的数据方面。
您的点B是活动的pcsp。点A实际上是fplr;除非您调用另一个函数,然后编译器可能准备设置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.

关键点是,

  1. pclr 是相关的代码寄存器。一个是“你所在的地方”,另一个是“你之前所在的地方”。
  2. spfp 是相关的本地数据寄存器。
    一个是“本地数据所在的位置”,另一个是“上一个本地数据所在的位置”。
  3. 它们与parameter passing一起协作,创建函数机制。
  4. 很难描述一个通用情况,因为我们希望编译器尽可能快,所以它们会使用所有技巧。

这些概念对于所有 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 中的解释。


好的,lr 是代码所在的位置,fp 是堆栈所在的位置。这意味着如果我有另一个函数 baz()sp 将位于点(F),因为堆栈指针被移动以分配变量 abbaz() 中;fp 将位于点(E),因为 sp 位于 baz() 的顶部;而 lr 将位于(D)? - user2233706
1
我更新了答案。 相较于标签,澄清可能更好。 当在 baz() 时,sp 将指向 baz() 数据。pcbaz()代码。 fp 指向要恢复bar()上下文的内容,包括代码和数据 或者 旧的sp 和旧的lr == foo() 返回值。 lr 是返回到bar()的返回值,除非baz()调用更多函数,那么编译器必须在另一个堆栈帧中保存lr,因为调用将破坏lr - artless noise
这很有道理。当前活动的 lr 包含了在前一个堆栈帧中返回的地址(它是“你所在的位置”)。活动的 fp 是当前堆栈帧内的地址。在跳转到新函数之前,fplrip 会被保存到堆栈中。当你执行 bx lr 时,你会返回到前一个堆栈帧。但是在你的代码中,难道你不应该在 bx lr 之后从堆栈中恢复 lr 吗?因为 lr 已经包含了你需要返回的地址。否则,你将会跳转到前前一个堆栈帧。 - user2233706
哇,我应该说lr包含了代码的地址,从上一个函数调用中返回到不是前一个堆栈帧中要返回的地址。 lr指向文本段中的某个位置,而fp指向堆栈中的某个位置。 - user2233706

0

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

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

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

这样可以节省执行时间和堆栈空间,但是它使得在调用代码中向后跟踪变得极其困难(我试图放弃了...)

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


这基本上是正确的。fp 也可以作为通用寄存器使用;对于某些函数来说,这是一个很大的优势,因为它们可能不需要使用堆栈。您需要两个由编译器生成的表来使用新的 AAPCS 进行堆栈跟踪。我提供了 ARM extab QA 作为参考。 - artless noise

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接