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

时间:2018-08-28 22:53:01

标签: c arm callstack stackframe

我正在竭尽全力地了解调用堆栈以及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:这让我有些困惑,但是我认为它的意思是返回地址,本质上是从执行halInit()函数返回时要执行的指令。但是,如果我在GDB终端中键入命令info reg,则会看到链接寄存器不是此值,而是下一个地址: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 如果调用者堆栈帧是需要此寄存器的对象,则会被推送到该堆栈帧吗?

很抱歉,我的帖子很长,我只想覆盖所有必填信息。

更新

我想我也许知道为什么r3被推入被调用者堆栈而不是推入调用者堆栈,即使调用者是需要此值的人也是如此。

是因为功能 halInit() 将修改 r3 中的值吗?

换句话说,被调用方堆栈框架知道调用方堆栈框架需要此寄存器值,因此它将把它压入其自己的堆栈框架中,以便可以出于自己的目的修改r3,然后在弹出堆栈框架,它将恢复被压入堆栈框架的值0x70回到r3中,以供调用者再次使用。 这是正确的吗?如果是这样,被调用方堆栈框架如何知道调用方堆栈框架将需要此值?

2 个答案:

答案 0 :(得分:2)

  

我正在努力学习有关调用堆栈以及堆栈帧的方式   在ARM Cortex-M0中构建

因此,基于该报价,首先,cortex-m0没有堆栈框架,因此处理器确实是愚蠢的逻辑。编译器生成的堆栈帧是编译器对象,而不是指令集对象。函数的概念是编译器,并不是更低的东西。编译器使用调用约定或设计的一些基本规则,以便对于该语言,调用者和被调用者函数可以准确知道参数的位置,返回值,并且没有人浪费其他数据。

编译器作者可以自由地做他们想做的任何事情,只要它能工作并符合指令集的规则即可,就像逻辑而不是汇编语言一样。 (只要机器代码符合逻辑规则,汇编作者就可以自由编写所需的任何汇编语言,助记符)。而且他们曾经这样做,处理器供应商已经开始提出建议,可以这样说,编译器也符合它们。它不是要尽可能多地在编译器之间共享对象1)我不必提出自己的想法2)我们信任IP供应商的处理器,并希望它们的调用约定是为性能和我们期望的其他原因而设计的

到目前为止,

gcc一直在尝试与ARM ABI保持一致,因为它不断发展,gcc也在不断发展。

当您有“许多”寄存器时,有很多方法是个问题,但是您会看到约定将首先使用寄存器,然后使用堆栈传递参数。您还将看到,某些函数中的某些寄存器将被指定为易失性,从而在不必过多使用内存(堆栈)的情况下提高了性能。

通过使用调试器和断点,您在错误的位置查找了您要了解的有关调用堆栈和堆栈帧的语句,这是编译器,而不是逻辑中如何处理异常。除非您的问题确实如此,否则您的问题不够准确,无法理解。

像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}

首先,为什么功能在地址零?因为我反汇编了不是链接二进制文件的对象,所以也许以后再说。为何要反汇编还是编译为汇编?如果反汇编程序很好,那么您实际上将看到生成的内容,而不是将要包含的程序集,当然其中包含的是经过编译的代码,很多非指令语言以及最终汇编后会更改的伪代码。

当存在第二个指针(即帧指针)时,即为IMO的堆栈帧。您经常会在指令集中看到这种指令集,这些指令集或指令集倾向于此指令集。例如,一个指令集可能有一个堆栈指针寄存器,但您不能从中寻址,可能还有另一个帧寄存器指针,并且您可以。因此,典型的输入方法是将帧指针保存在堆栈上,因为调用者可能一直在使用它作为其帧,我们希望将其返回找到的位置,然后将堆栈指针的地址复制到帧指针,然后移动尽可能多地使用此函数所需的堆栈指针,以便中断或调用其他函数时,堆栈指针始终位于已使用和未使用的堆栈空间之间的边界上。在这种情况下,将使用帧指针来访问任何传入的参数,或者以帧指针加偏移量的方式(对于向下增长的堆栈)以及向负偏移方向访问本地数据访问任何传入的参数或返回地址。

现在看起来编译器正在使用帧指针,这很浪费,让我们不要这样做:

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在函数中易失。编译器不必将找到的那些寄存器返回给调用者,它们可以在函数中随意使用。在这种情况下,编译器选择从堆栈中取出加法操作数,而不是使用第一个到释放的操作数。一旦r0和r1保存到堆栈中,则自由寄存器的“池”将假定以r0,r1,r2,r3开始。因此,是的,它的确看上去很破损,但这是事实,它在功能上是正确的,这是编译器的工作,以产生在功能上实现已编译代码的代码。此编译器使用的调用约定指出,如果合适,返回值将进入r0。

因此设置了堆栈帧,从sp中减去8。传入的参数将保存到堆栈中。现在,该函数从堆栈中拉入传入的参数开始,添加它们,然后将结果放入返回寄存器中。

然后bx lr用于返回,并与pop一起查找该指令(对于armv6m,对于armv4t pop不能用于切换模式,因此编译器会在弹出至lr的情况下选择bx lr)。

armv4t thumb,如果此代码与arm混合使用,则不能使用pop来返回,因此返回信息会弹出到volatile寄存器中并执行bx lr,您不能直接通过thumb弹出到lr中。您可能可以告诉编译器我没有将此代码与arm代码混合使用,因此可以保存以使用pop返回。取决于编译器。

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}

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

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

虽然这里是您的问题之一,但到目前为止,我对此已作了间接评论。完整尺寸的手臂处理器armv4t至armv7支持手臂指令和拇指指令。并不是每个人都支持一个进化,但是您可以将手臂和拇指指令共存,作为该核心逻辑定义的规则的一部分。支持ARM的设计是因为arm指令必须是字对齐的,因此arm指令地址的低两位始终为零。所需的也对齐的16位指令集将始终具有地址零的低位。因此,为什么不使用地址的lsbit作为切换模式的方式呢?这就是他们选择要做的。首先是几条指令,然后是armv7架构所允许的更多指令,如果分支的地址(先查找bx,分支交换)的lsbit为1,则处理器在开始获取时会切换到拇指模式在该地址的指令中,程序计数器不保留该指令,而是由指令剥离,它只是一个信号,用于告诉指令切换模式。如果lsbit为0,则处理器切换到布防模式。如果它已经处于上述模式,那么它将停留在该模式。

现在,这些cortex-m内核只是拇指机器,没有手臂模式。这些工具到位了,所有这些都无须更改,如果您尝试在cortex-m上进入手臂模式,则会出现故障。

现在看上面的代码,有时我们用bx lr返回,有时用pop pc返回,在两种情况下lr都持有“返回地址”。为了使bx lr情况起作用,必须设置lr的lsbit。调用者无法知道我们将使用哪个指令来返回,并且调用者不必但可能使用bl进行调用,因此逻辑实际上将位设置为编译器。这就是为什么您的寄信人地址少一个字节的原因。

尽管您想了解编译器和堆栈框架,但未优化的绝对使用堆栈,如您所见,如果您拥有经过适当优化的编译器,那么经过优化的代码可以更容易地理解编译器的输出,一旦您学会了避免无效代码。

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

r0和r1是传入的参数,r0是返回值所在的位置,链接寄存器是返回地址。这就是您希望编译器将为类似函数产生的结果。

现在让我们尝试一些更复杂的事情。

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,更多的乐趣不是零?这是一个未链接的对象,链接后,链接器将修改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并保存返回地址,因为我们自己进行调用,将其丢弃。调用r4后,保存我们需要的参数。使调用将返回值放在返回寄存器中并返回。随手清理堆栈。因此,这里的堆栈框架很小。没有使用太多堆栈。

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中,然后假定您所指的“调用堆栈”是第五个参数。拇指指令集使用bl作为其主要调用指令,该指令使用r14作为返回地址,与其他可能使用堆栈存储返回地址的指令集不同,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

答案 1 :(得分:0)

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

调用方具有完整的上下文,因此仅保存正在使用的寄存器更为有效。 ARM ABI定义了大多数编译器使用的约定。这使来自不同编译器的函数库可以互操作。