为什么调用者栈的局部变量保存在被调用者栈的寄存器中?

Why Are Local Variables of Caller Stack Saved in Registers in Callee Stack?

我正在尽最大努力了解调用堆栈以及堆栈帧在 ARM Cortex-M0 中的结构,事实证明这有点困难,但我正在耐心学习。在这个问题中我有几个问题,所以希望你们能在所有方面帮助我。我的问题将在整个解释中以粗体突出显示。

我正在使用带有 GDB 的 ARM Cortex-M0 和一个简单的调试程序。这是我的程序:

int main(void) {
    static uint16_t myBits;
    myBits = 0x70;

    halInit();

    return 0;
}

我在 halInit() 上设置了一个断点。然后我在我的 GDB 终端上执行命令 info frame 得到这个输出:

Stack level 0, frame at 0x20000400:
pc = 0x80000d8 in main (src/main.c:63); saved pc 0x8002dd2
source language c.
Arglist at 0x200003e8, args: 
Locals at 0x200003e8, Previous frame's sp is 0x20000400
Saved registers:
 r0 at 0x200003e8, r1 at 0x200003ec, r4 at 0x200003f0, r5 at 0x200003f4, r6 at 0x200003f8, lr at 0x200003fc

我会解释我是如何解释这个的,如果我是正确的,请告诉我。

Stack level 0:堆栈帧的当前级别。 0 将始终代表堆栈的顶部,换句话说,当前正在使用的堆栈帧。

frame at 0x20000400:这表示堆栈帧在闪存中的位置。

pc = 0x80000d8 in main (src/main.c:63);:代表下一次要执行的程序,即程序计数器的值。因为程序计数器总是代表下一条要执行的指令。

saved pc 0x8002dd2:这个有点让我困惑,但我认为它的意思是return地址,本质上是return从执行halInit() 函数。但是,如果我在我的 GDB 终端中键入命令 info reg,我会看到 link 寄存器不是这个值,而是下一个地址:lr 0x8002dd3这是为什么?

source language c.: 这表示正在使用的语言。

Arglist at 0x200003e8, args::这表示传递到堆栈帧的参数的起始地址。由于 args: 为空,这意味着没有传递任何参数。这有两个原因:这是调用堆栈中的第一个堆栈帧,我的函数没有任何参数 int main(void)

Locals at 0x200003e8:这是我的局部变量的起始地址。正如您在我的原始代码片段中看到的那样,我应该有一个局部变量 myBits。我们稍后再谈。

Previous frame's sp is 0x20000400:这是指向调用者堆栈帧顶部的堆栈指针。由于这是第一个堆栈帧,我希望这个值应该等于当前帧的地址。

Saved registers:
r0 at 0x200003e8
r1 at 0x200003ec
r4 at 0x200003f0
r5 at 0x200003f4
r6 at 0x200003f8
lr at 0x200003fc

这些是已被压入堆栈以供当前堆栈帧稍后使用的寄存器。 这部分我很好奇,因为它是第一个堆栈帧,为什么它会保存这么多寄存器?如果我执行命令info reg,我得到以下输出:

r0             0x20000428   0x20000428
r1             0x0  0x0
r2             0x0  0x0
r3             0x70 0x70
r4             0x80000c4    0x80000c4
r5             0x20000700   0x20000700
r6             0xffffffff   0xffffffff
r7             0xffffffff   0xffffffff
r8             0xffffffff   0xffffffff
r9             0xffffffff   0xffffffff
r10            0xffffffff   0xffffffff
r11            0xffffffff   0xffffffff
r12            0xffffffff   0xffffffff
sp             0x200003e8   0x200003e8
lr             0x8002dd3    0x8002dd3
pc             0x80000d8    0x80000d8 <main+8>
xPSR           0x21000000   0x21000000

这告诉我,如果我通过执行命令p/x *(register)检查保存寄存器的每个内存地址中存储的值,那么这些值应该等于输出中显示的值多于。

Saved registers:
r0 at 0x200003e8 -> 0x20000428
r1 at 0x200003ec -> 0x0
r4 at 0x200003f0 -> 0x80000c4
r5 at 0x200003f4 -> 0xffffffff
r6 at 0x200003f8 -> 0xffffffff
lr at 0x200003fc -> 0x8002dd3

有效,每个地址中的值代表info reg命令显示的值。但是,我注意到一件事。我有一个局部变量 myBits,其值为 0x70,它似乎存储在 r3 中。然而 r3 并没有被压入堆栈进行保存。

如果我们进入下一条指令,将为函数halInit() 创建一个新的堆栈帧。这是通过在我的终端上执行命令 bt 来显示的。它生成以下输出:

#0  halInit () at src/hal/src/hal.c:70
#1  0x080000dc in main () at src/main.c:63

如果我执行命令 info frame 然后我得到以下输出:

Stack level 0, frame at 0x200003e8:
pc = 0x8001842 in halInit (src/hal/src/hal.c:70); saved pc 0x80000dc
called by frame at 0x20000400
source language c.
Arglist at 0x200003e0, args: 
Locals at 0x200003e0, Previous frame's sp is 0x200003e8
Saved registers:
 r3 at 0x200003e0, lr at 0x200003e4

现在我们看到寄存器 r3 被压入了这个堆栈帧。该寄存器保存变量 myBits 的值。 如果调用者栈帧需要这个寄存器,为什么 r3 被压入这个栈帧?

抱歉这么长post,我只想涵盖所有需要的信息。

更新

我想我可能知道为什么 r3 被推送到被调用者堆栈而不是调用者堆栈,即使调用者是需要这个值的人。

是不是因为函数 halInit() 会修改中的值r3?

也就是说,被调用者栈帧知道调用者栈帧需要这个寄存器值,所以会把它压入自己的栈帧,以便自己修改r3,然后当堆栈帧被弹出时,它会将被压入堆栈帧的值 0x70 恢复为 r3 以供调用者再次使用。 这是否正确?如果正确,被调用者堆栈框架如何知道调用者堆栈框架将需要此值?

在 ARM 系统上,许多自动存储在寄存器中,而不是在堆栈上分配 space。与其他处理器相比,ARM 有很多寄存器。当一个函数(上下文)调用另一个函数时,这些寄存器可能会被覆盖。编译器编写者有两种选择,1) 在每个函数的入口(顶部)保存所有寄存器,或 2) 保存该函数在调用另一个函数时使用的寄存器。

调用者有完整的上下文,所以只保存正在使用的寄存器会更有效率。 ARM ABI 定义了大多数编译器使用的约定。这使得来自不同编译器的函数库可以互操作。

I'm trying my best to learn about the call stack and how stack frames are structured in an ARM Cortex-M0

所以根据那句话,首先 arm cortex-m0 没有堆栈帧,处理器真的是非常愚蠢的逻辑。编译器生成堆栈帧,这是编译器的事情,而不是指令集的事情。函数的概念是编译器的东西,实际上并没有什么低级的。编译器使用调用约定或一些设计的基本规则集,以便对于该语言,调用者和被调用者函数确切地知道参数在哪里,return 值,并且没有人会破坏其他数据。

编译器作者可以自由地做任何他们想做的事,只要它能工作并且符合指令集的规则,就像逻辑而不是汇编语言一样。 (汇编程序作者可以自由编写他们想要的任何汇编语言,助记符只要机器代码符合逻辑规则)。他们过去常常这样做,处理器供应商已经开始提出建议,并且编译器正在遵守这些建议。它不是关于跨编译器共享对象,而是 1) 我不必提出自己的 2) 我们信任 ip 供应商及其处理器,并希望他们的调用约定是为性能和我们想要的其他原因而设计的.

gcc 到目前为止一直在努力与 ARM 的 ABI 保持一致,因为它的发展和 gcc 的发展。

当你有 "many" 寄存器时,有多少含义是见仁见智的,但你会看到约定首先使用寄存器,然后使用堆栈来传递参数。您还将看到一些寄存器将在函数中指定为易失性寄存器,以提高性能,而不是必须大量使用内存(堆栈)。

通过使用调试器和断点,您在错误的地方查找了您的语句,您想了解调用堆栈和堆栈帧,这是编译器的事情,而不是关于如何在逻辑中处理异常。除非那真的是你的问题,因为你的问题不够准确,无法理解。

像 GCC 这样的编译器有优化器,尽管它们在死代码方面造成了混淆,但从优化版本学习比非优化版本更容易。让我们开始吧

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
    return(a+b);
}

优化

 <fun>:
   0:   1840        adds    r0, r0, r1
   2:   4770        bx  lr

没有

00000000 <fun>:
   0:   b580        push    {r7, lr}
   2:   b082        sub sp, #8
   4:   af00        add r7, sp, #0
   6:   6078        str r0, [r7, #4]
   8:   6039        str r1, [r7, #0]
   a:   687a        ldr r2, [r7, #4]
   c:   683b        ldr r3, [r7, #0]
   e:   18d3        adds    r3, r2, r3
  10:   0018        movs    r0, r3
  12:   46bd        mov sp, r7
  14:   b002        add sp, #8
  16:   bd80        pop {r7, pc}

首先为什么函数地址为零?因为我反汇编的对象不是 linked 二进制文件,也许我以后会。为什么反汇编与编译成汇编?如果反汇编器有任何好处,那么您实际上会看到生成的内容而不是汇编,其中肯定会包含编译代码、大量非指令语言以及最终汇编时会更改的伪代码。

当有第二个指针(帧指针)时,IMO 就是一个栈帧。您经常会在指令集中看到这一点,这些指令集的指令或限制倾向于这一点。例如,一个指令集可能有一个堆栈指针寄存器,但你不能从它寻址,可能还有另一个帧寄存器指针,你可以。所以典型的条目是将帧指针保存在堆栈上,因为调用者可能一直在为他们的帧使用它,我们希望 return 它被发现,然后将堆栈指针的地址复制到帧指针,然后将堆栈指针移动到该函数所需的最远位置,以便中断或调用其他函数堆栈指针位于已使用和未使用堆栈之间的边界 space,因为它应该始终如此。在这种情况下,帧指针将用于访问任何传入的参数或 return 地址,帧指针加上偏移方式(对于向下增长的堆栈)和本地数据的负偏移方向。

现在看起来编译器确实在使用帧指针,真是浪费,让我们不要这样:

00000000 <fun>:
   0:   b082        sub sp, #8
   2:   9001        str r0, [sp, #4]
   4:   9100        str r1, [sp, #0]
   6:   9a01        ldr r2, [sp, #4]
   8:   9b00        ldr r3, [sp, #0]
   a:   18d3        adds    r3, r2, r3
   c:   0018        movs    r0, r3
   e:   b002        add sp, #8
  10:   4770        bx  lr

所以首先编译器确定有 8 个字节的东西要保存在堆栈上。几乎所有未优化的东西都在堆栈中占有一席之地,传递的参数以及局部变量,在这种情况下没有任何局部变量,所以我们只有传入的参数,两个 32 位数字,所以 8 个字节。使用的调用约定尝试将 r0 用作第一个参数,将 r1 用作第二个参数(如果合适),在这种情况下它们会这样做。所以堆栈指针减8就形成了堆栈帧,堆栈帧指针就是这种情况下的堆栈指针。此处使用的调用约定允许 r0-r3 在函数中可变。编译器不必 return 向调用者提供这些寄存器,它们可以在函数中随意使用。在这种情况下,编译器选择使用下一个寄存器而不是第一个释放寄存器从堆栈中提取加法操作数。一旦 r0 和 r1 被保存到堆栈,那么 "pool" 的空闲寄存器将假定从 r0、r1、r2、r3 开始。所以是的,它看起来确实被破坏了,但它就是这样,它在功能上是正确的,这是编译器的工作来生成在功能上实现编译代码的代码。该编译器使用的调用约定声明 return 值在适合时进入 r0,它确实如此。

栈帧设置完毕,sp减8。传入的参数保存到栈中。现在该函数首先从堆栈中提取传入的参数,将它们相加,并将结果放入 return 寄存器。

然后 bx lr 用于 return,查看该指令以及 pop(对于 armv6m,对于 armv4t,pop 不能用于切换模式,因此如果编译器可以弹出到 lr,那么编译器将弹出到 bx lr) .

armv4t 拇指,不能使用 pop 到 return,以防此代码与 arm 混合,所以 return 弹出到易失性寄存器并执行 bx lr,你不能直接弹出到 lr在拇指。您可能可以告诉编译器我没有将它与 arm 代码混合,因此它保存为使用 pop 到 return。取决于编译器。

00000000 <fun>:
   0:   b580        push    {r7, lr}
   2:   b082        sub sp, #8
   4:   af00        add r7, sp, #0
   6:   6078        str r0, [r7, #4]
   8:   6039        str r1, [r7, #0]
   a:   687a        ldr r2, [r7, #4]
   c:   683b        ldr r3, [r7, #0]
   e:   18d3        adds    r3, r2, r3
  10:   0018        movs    r0, r3
  12:   46bd        mov sp, r7
  14:   b002        add sp, #8
  16:   bc80        pop {r7}
  18:   bc02        pop {r1}
  1a:   4708        bx  r1

查看帧指针

00000000 <fun>:
   0:   b580        push    {r7, lr}
   2:   b082        sub sp, #8
   4:   af00        add r7, sp, #0
   6:   6078        str r0, [r7, #4]
   8:   6039        str r1, [r7, #0]
   a:   687a        ldr r2, [r7, #4]
   c:   683b        ldr r3, [r7, #0]
   e:   18d3        adds    r3, r2, r3
  10:   0018        movs    r0, r3
  12:   46bd        mov sp, r7
  14:   b002        add sp, #8
  16:   bd80        pop {r7, pc}

首先将帧指针保存到堆栈,因为调用者或调用者调用者等可能正在使用它,它是我们必须保留的寄存器。现在一些调用约定从一开始就开始发挥作用。我们知道编译器知道我们不是在调用另一个函数所以我们不需要保留 return 地址(存储在 link 寄存器 r14 中),那么为什么将它压入堆栈为什么浪费 space和时钟周期?好吧,不久前约定更改为堆栈应该是 64 位对齐的,因此您基本上是成对地压入和弹出寄存器(偶数个寄存器)。正如我们在 armv4t return 中看到的那样,有时它们会为一对使用多个指令。所以编译器需要压入另一个寄存器,它可以而且你有时会看到它只是选择一些它没有使用的寄存器并将其压入堆栈,也许我们可以在这里稍微做到这一点。在这种情况下,作为 armv6-m,您可以使用 pop 切换模式,因此使用 pop pc 生成 return 是安全的,因此您可以在此处使用 link 寄存器而不是其他寄存器来保存指令登记。尽管是未优化的代码,但有一点优化。

保存帧指针,然后将帧指针与栈指针相关联,在这种情况下,它首先移动栈指针,使帧指针与栈指针匹配,然后使用帧指针进行栈访问。哦,多么浪费,即使对于未优化的代码也是如此。但也许这个编译器在被告知要像这样编译时默认为一个帧指针。

虽然这是你的一个问题,但到目前为止我已经间接地对此发表了评论。全尺寸的 arm 处理器 armv4t 到 armv7 支持 arm 指令和 thumb 指令。不是每个人都支持每一个进化,但你可以让手臂和拇指指令共存,作为该核心逻辑定义的规则的一部分。支持这一点的 ARM 设计是因为 arm 指令必须是字对齐的,因此 arm 指令地址的低两位始终为零。一个理想的 16 位指令集,也是对齐的,地址的低位总是为零。那么为什么不使用地址的 lsbit 作为切换模式的方式。这就是他们选择做的。一开始有一些指令,然后变得更多,这是 armv7 架构所允许的,如果分支的地址(首先查找 bx,分支交换)的 lsbit 为 1,则处理器在开始获取时切换到拇指模式在那个地址的指令,程序计数器不保留这个,它被指令剥离,它只是一个用来告诉指令切换模式的信号。如果 lsbit 为 0,则处理器切换到 arm 模式。如果它已经处于所述模式,它就保持在该模式。

现在出现了这些 cortex-m 内核,它们是只有拇指的机器,没有 arm 模式。工具已到位,一切正常,无需更改,如果您尝试在 cortex-m 上进入 arm 模式,则会出错。

现在看看上面的代码,有时我们 return 使用 bx lr,有时使用 pop pc,在这两种情况下,lr 都持有 "return address"。要使 bx lr 案例起作用,必须设置 lr 的 lsbit。调用者无法知道我们将为 return 使用哪条指令,调用者不必知道但可能使用 bl 来进行调用,因此逻辑实际上设置了位而不是编译器。这就是为什么您的 return 地址偏移了一个字节。

如果你想了解编译器和堆栈帧,虽然未优化肯定会使用堆栈,但如果你有一个优化良好的编译器,那么一旦你不学习优化代码,就可以更容易理解编译器输出制作死代码。

00000000 <fun>:
   0:   1840        adds    r0, r0, r1
   2:   4770        bx  lr

r0和r1是传入的参数,r0是return值所在的地方,link寄存器是return地址。这就是您希望编译器为这样的函数生成的结果。

所以现在让我们尝试更复杂的东西。

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
    return(more_fun(a,b));
}

00000000 <fun>:
   0:   b510        push    {r4, lr}
   2:   f7ff fffe   bl  0 <more_fun>
   6:   bd10        pop {r4, pc}

一些事情,首先为什么优化器不这样做:

fun:
   b more_fun

我不知道。

为什么说bl 0,更有趣的不是零?这是一个未 linked 代码的对象,一旦 linked,linker 将修改该 bl 指令以指向 more_fun()。

第三,我们已经让编译器推送一个我们没有使用的寄存器。它正在推送和弹出 r4,以便它可以根据此编译器使用的调用约定保持堆栈对齐。它几乎可以选择任何一个寄存器,您可能会发现使用 r3 而不是 r4 的 gcc 或 llvm/clang 版本。 gcc 现在已经使用 r4 一段时间了。它是寄存器列表中的第一个,您必须首先在寄存器列表中保留它,如果他们想在调用中保留某些内容,他们将使用它们(我们将在一秒钟内看到)。大概就是这样吧,谁知道问作者。

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
    more_fun(a,b);
    return(a);
}

00000000 <fun>:
   0:   b510        push    {r4, lr}
   2:   0004        movs    r4, r0
   4:   f7ff fffe   bl  0 <more_fun>
   8:   0020        movs    r0, r4
   a:   bd10        pop {r4, pc}

现在我们正在取得进展。所以我们告诉编译器它必须在函数调用中保存传入的参数。每个函数都会重新开始规则,因此每个调用的函数都可以丢弃 r0-r3,因此如果您将 r0-r3 用于某些事情,您需要将它们保存在某个地方。所以这是一个非常明智的选择,而不是将传入的参数保存在堆栈上,并且可能不得不执行多个代价高昂的内存周期来访问它。而是将被调用者或被调用者的被调用者等值保存在堆栈上,并在我们的函数中使用寄存器来保存该参数,作为一种设计,它节省了很多浪费的周期。无论如何,我们都需要对齐堆栈,所以这一切都解决了保留 r4 并保存 return 地址的问题,因为我们自己进行调用会丢弃它。将调用后我们需要的参数保存到r4中。使调用在 return 寄存器和 return 中放置 return 值。边走边清理堆栈。所以这里的栈帧是最小的。堆栈使用不多。

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
    b<<=more_fun(a,b);
    return(a+b);
}

00000000 <fun>:
   0:   b570        push    {r4, r5, r6, lr}
   2:   0005        movs    r5, r0
   4:   000c        movs    r4, r1
   6:   f7ff fffe   bl  0 <more_fun>
   a:   4084        lsls    r4, r0
   c:   1960        adds    r0, r4, r5
   e:   bd70        pop {r4, r5, r6, pc}

我们又做了一次,我们让编译器必须保存一个我们没有用来保持对齐的寄存器。我们正在使用更多的堆栈,但你会称之为堆栈框架吗?我们强制编译器必须通过子例程调用保留两个传入参数。

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d )
{
    b<<=more_fun(b,c);
    c<<=more_fun(c,d);
    d<<=more_fun(b,d);
    return(a+b+c+d);
}


 0: b5f8        push    {r3, r4, r5, r6, r7, lr}
   2:   000c        movs    r4, r1
   4:   0007        movs    r7, r0
   6:   0011        movs    r1, r2
   8:   0020        movs    r0, r4
   a:   001d        movs    r5, r3
   c:   0016        movs    r6, r2
   e:   f7ff fffe   bl  0 <more_fun>
  12:   0029        movs    r1, r5
  14:   4084        lsls    r4, r0
  16:   0030        movs    r0, r6
  18:   f7ff fffe   bl  0 <more_fun>
  1c:   0029        movs    r1, r5
  1e:   4086        lsls    r6, r0
  20:   0020        movs    r0, r4
  22:   f7ff fffe   bl  0 <more_fun>
  26:   4085        lsls    r5, r0
  28:   19a4        adds    r4, r4, r6
  2a:   19e4        adds    r4, r4, r7
  2c:   1960        adds    r0, r4, r5
  2e:   bdf8        pop {r3, r4, r5, r6, r7, pc}

需要什么?我们至少确实得到了它来保存 r3 以平衡堆栈。我打赌我们现在可以推动它...

extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d, unsigned int e, unsigned int f )
{
    b<<=more_fun(b,c);
    c<<=more_fun(c,d);
    d<<=more_fun(b,d);
    e<<=more_fun(e,d);
    f<<=more_fun(e,f);
    return(a+b+c+d+e+f);
}

00000000 <fun>:
   0:   b5f0        push    {r4, r5, r6, r7, lr}
   2:   46c6        mov lr, r8
   4:   000c        movs    r4, r1
   6:   b500        push    {lr}
   8:   0011        movs    r1, r2
   a:   0007        movs    r7, r0
   c:   0020        movs    r0, r4
   e:   0016        movs    r6, r2
  10:   001d        movs    r5, r3
  12:   f7ff fffe   bl  0 <more_fun>
  16:   0029        movs    r1, r5
  18:   4084        lsls    r4, r0
  1a:   0030        movs    r0, r6
  1c:   f7ff fffe   bl  0 <more_fun>
  20:   0029        movs    r1, r5
  22:   4086        lsls    r6, r0
  24:   0020        movs    r0, r4
  26:   f7ff fffe   bl  0 <more_fun>
  2a:   4085        lsls    r5, r0
  2c:   9806        ldr r0, [sp, #24]
  2e:   0029        movs    r1, r5
  30:   f7ff fffe   bl  0 <more_fun>
  34:   9b06        ldr r3, [sp, #24]
  36:   9907        ldr r1, [sp, #28]
  38:   4083        lsls    r3, r0
  3a:   0018        movs    r0, r3
  3c:   4698        mov r8, r3
  3e:   f7ff fffe   bl  0 <more_fun>
  42:   9b07        ldr r3, [sp, #28]
  44:   19a4        adds    r4, r4, r6
  46:   4083        lsls    r3, r0
  48:   19e4        adds    r4, r4, r7
  4a:   1964        adds    r4, r4, r5
  4c:   4444        add r4, r8
  4e:   18e0        adds    r0, r4, r3
  50:   bc04        pop {r2}
  52:   4690        mov r8, r2
  54:   bdf0        pop {r4, r5, r6, r7, pc}
  56:   46c0        nop         ; (mov r8, r8)

好的,事情就是这样...

extern unsigned int more_fun ( unsigned int, unsigned int );
extern void not_dead ( unsigned int *);
unsigned int fun ( unsigned int a, unsigned int b )
{
    unsigned int x[16];
    unsigned int ra;
    for(ra=0;ra<16;ra++)
    {
        x[ra]=more_fun(a+ra,b);
    }
    not_dead(x);
    return(ra);
}


00000000 <fun>:
   0:   b5f0        push    {r4, r5, r6, r7, lr}
   2:   0006        movs    r6, r0
   4:   b091        sub sp, #68 ; 0x44
   6:   0004        movs    r4, r0
   8:   000f        movs    r7, r1
   a:   466d        mov r5, sp
   c:   3610        adds    r6, #16
   e:   0020        movs    r0, r4
  10:   0039        movs    r1, r7
  12:   f7ff fffe   bl  0 <more_fun>
  16:   3401        adds    r4, #1
  18:   c501        stmia   r5!, {r0}
  1a:   42b4        cmp r4, r6
  1c:   d1f7        bne.n   e <fun+0xe>
  1e:   4668        mov r0, sp
  20:   f7ff fffe   bl  0 <not_dead>
  24:   2010        movs    r0, #16
  26:   b011        add sp, #68 ; 0x44
  28:   bdf0        pop {r4, r5, r6, r7, pc}
  2a:   46c0        nop         ; (mov r8, r8)

还有你的堆栈帧,但它实际上没有帧指针,也不使用堆栈来访问内容。必须继续努力才能看到​​这一点,非常可行。但希望现在你明白我的意思了。你的问题是关于堆栈帧在编译代码中的结构,特别是编译器如何为特定目标实现它。

顺便说一句,这就是 clang 对该代码所做的。

00000000 <fun>:
   0:   b5b0        push    {r4, r5, r7, lr}
   2:   af02        add r7, sp, #8
   4:   b090        sub sp, #64 ; 0x40
   6:   460c        mov r4, r1
   8:   4605        mov r5, r0
   a:   f7ff fffe   bl  0 <more_fun>
   e:   9000        str r0, [sp, #0]
  10:   1c68        adds    r0, r5, #1
  12:   4621        mov r1, r4
  14:   f7ff fffe   bl  0 <more_fun>
  18:   9001        str r0, [sp, #4]
  1a:   1ca8        adds    r0, r5, #2
  1c:   4621        mov r1, r4
  1e:   f7ff fffe   bl  0 <more_fun>
  22:   9002        str r0, [sp, #8]
  24:   1ce8        adds    r0, r5, #3
  26:   4621        mov r1, r4
  28:   f7ff fffe   bl  0 <more_fun>
  2c:   9003        str r0, [sp, #12]
  2e:   1d28        adds    r0, r5, #4
  30:   4621        mov r1, r4
  32:   f7ff fffe   bl  0 <more_fun>
  36:   9004        str r0, [sp, #16]
  38:   1d68        adds    r0, r5, #5
  3a:   4621        mov r1, r4
  3c:   f7ff fffe   bl  0 <more_fun>
  40:   9005        str r0, [sp, #20]
  42:   1da8        adds    r0, r5, #6
  44:   4621        mov r1, r4
  46:   f7ff fffe   bl  0 <more_fun>
  4a:   9006        str r0, [sp, #24]
  4c:   1de8        adds    r0, r5, #7
  4e:   4621        mov r1, r4
  50:   f7ff fffe   bl  0 <more_fun>
  54:   9007        str r0, [sp, #28]
  56:   4628        mov r0, r5
  58:   3008        adds    r0, #8
  5a:   4621        mov r1, r4
  5c:   f7ff fffe   bl  0 <more_fun>
  60:   9008        str r0, [sp, #32]
  62:   4628        mov r0, r5
  64:   3009        adds    r0, #9
  66:   4621        mov r1, r4
  68:   f7ff fffe   bl  0 <more_fun>
  6c:   9009        str r0, [sp, #36]   ; 0x24
  6e:   4628        mov r0, r5
  70:   300a        adds    r0, #10
  72:   4621        mov r1, r4
  74:   f7ff fffe   bl  0 <more_fun>
  78:   900a        str r0, [sp, #40]   ; 0x28
  7a:   4628        mov r0, r5
  7c:   300b        adds    r0, #11
  7e:   4621        mov r1, r4
  80:   f7ff fffe   bl  0 <more_fun>
  84:   900b        str r0, [sp, #44]   ; 0x2c
  86:   4628        mov r0, r5
  88:   300c        adds    r0, #12
  8a:   4621        mov r1, r4
  8c:   f7ff fffe   bl  0 <more_fun>
  90:   900c        str r0, [sp, #48]   ; 0x30
  92:   4628        mov r0, r5
  94:   300d        adds    r0, #13
  96:   4621        mov r1, r4
  98:   f7ff fffe   bl  0 <more_fun>
  9c:   900d        str r0, [sp, #52]   ; 0x34
  9e:   4628        mov r0, r5
  a0:   300e        adds    r0, #14
  a2:   4621        mov r1, r4
  a4:   f7ff fffe   bl  0 <more_fun>
  a8:   900e        str r0, [sp, #56]   ; 0x38
  aa:   350f        adds    r5, #15
  ac:   4628        mov r0, r5
  ae:   4621        mov r1, r4
  b0:   f7ff fffe   bl  0 <more_fun>
  b4:   900f        str r0, [sp, #60]   ; 0x3c
  b6:   4668        mov r0, sp
  b8:   f7ff fffe   bl  0 <not_dead>
  bc:   2010        movs    r0, #16
  be:   b010        add sp, #64 ; 0x40
  c0:   bdb0        pop {r4, r5, r7, pc}

现在您使用了术语调用堆栈。该编译器使用的调用约定说,尽可能使用 r0-r3 传递第一个参数,然后使用堆栈。

unsigned int fun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d, unsigned int e )
{
    return(a+b+c+d+e);
}
00000000 <fun>:
   0:   b510        push    {r4, lr}
   2:   9c02        ldr r4, [sp, #8]
   4:   46a4        mov r12, r4
   6:   4463        add r3, r12
   8:   189b        adds    r3, r3, r2
   a:   185b        adds    r3, r3, r1
   c:   1818        adds    r0, r3, r0
   e:   bd10        pop {r4, pc}

所以有四个以上的参数,前四个在 r0-r3 中,然后 "call stack" 假设这就是你所指的是第五个参数。 Thumb 指令集使用 bl 作为其主要调用指令,它使用 r14 作为 return 地址,与其他可能使用堆栈存储 return 地址的指令集不同,arm 使用寄存器。流行的 arm 调用约定对前几个操作数使用寄存器,然后使用堆栈。

您可能希望查看其他指令集以查看更多调用堆栈

00000000 <_fun>:
   0:   1d80 0008       mov 10(sp), r0
   4:   6d80 000a       add 12(sp), r0
   8:   6d80 0006       add 6(sp), r0
   c:   6d80 0004       add 4(sp), r0
  10:   6d80 0002       add 2(sp), r0
  14:   0087            rts pc