为什么调用者栈的局部变量保存在被调用者栈的寄存器中?
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
我正在尽最大努力了解调用堆栈以及堆栈帧在 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