【问题标题】:understanding aarch64 assembly function call, how is stack operated了解aarch64汇编函数调用,栈是如何操作的
【发布时间】:2021-05-11 21:14:26
【问题描述】:

test.c(裸机)

#include <stdio.h>

int add1(int a, int b)
{
int c;
c = a + b;
return c;
}

int main()
{
int x, y, z;
x = 3;
y = 4;
z = add1(x,y);
printf("z = %d\n", z);
}

我做aarch64-none-elf-gcc test.c -specs=rdimon.specs 并得到a.out。我做aarch64-none-elf-objdump -d a.out 并得到了汇编代码。这是主要功能。

00000000004002e0 <add1>:
  4002e0:   d10083ff    sub sp, sp, #0x20       <-- reduce sp by 0x20 (just above it are saved fp and lr of main)
  4002e4:   b9000fe0    str w0, [sp, #12]       <-- save first param x at sp + 12
  4002e8:   b9000be1    str w1, [sp, #8]        <-- save second param y at sp + 8
  4002ec:   b9400fe1    ldr w1, [sp, #12]       <-- load w1 with x
  4002f0:   b9400be0    ldr w0, [sp, #8]        <-- load w0 with y
  4002f4:   0b000020    add w0, w1, w0          <-- w0 = w1 + w0
  4002f8:   b9001fe0    str w0, [sp, #28]       <-- store x0 to sp+28
  4002fc:   b9401fe0    ldr w0, [sp, #28]       <-- load w0 with the result (seems redundant)
  400300:   910083ff    add sp, sp, #0x20       <-- increment sp by 0x20
  400304:   d65f03c0    ret
0000000000400308 <main>:
  400308:   a9be7bfd    stp x29, x30, [sp, #-32]!   <-- save x29(fp) and x30(lr) at sp - 0x20
  40030c:   910003fd    mov x29, sp                 <-- set fp to new sp, the base of stack growth(down)
  400310:   52800060    mov w0, #0x3                    // #3
  400314:   b9001fe0    str w0, [sp, #28]           <-- x is assigned in sp + #28
  400318:   52800080    mov w0, #0x4                    // #4
  40031c:   b9001be0    str w0, [sp, #24]           <-- y is assiged in sp + #24
  400320:   b9401be1    ldr w1, [sp, #24]            <-- load func param for y
  400324:   b9401fe0    ldr w0, [sp, #28]           <-- load func param for x
  400328:   97ffffee    bl  4002e0 <add1>           <-- call main1 (args are in w0, w1)
  40032c:   b90017e0    str w0, [sp, #20]           <-- store x0(result z) to sp+20
  400330:   b94017e1    ldr w1, [sp, #20]           <-- load w1 with the result (why? seems redundant. it's already in w0)
  400334:   d0000060    adrp    x0, 40e000 <__sfp_handle_exceptions+0x28>
  400338:   91028000    add x0, x0, #0xa0  <-- looks like loading param x0 for printf
  40033c:   940000e7    bl  4006d8 <printf>
  400340:   52800000    mov w0, #0x0                    // #0 <-- for main's return value..
  400344:   a8c27bfd    ldp x29, x30, [sp], #32  <-- recover x29 and x30 (look's like values in x29, x30 was used in the fuction who called main)
  400348:   d65f03c0    ret
  40034c:   d503201f    nop

我用&lt;-- 标记添加了我的理解。有人可以看到代码并给我一些更正吗?任何小的评论将不胜感激。 (请看&lt;main&gt;

添加:感谢 cmets。我想我忘了问我真正的问题。在 main 的开头,调用 main 的程序应该把它的返回地址(在 main 之后)放在 x30 中。由于 main 应该调用另一个函数本身,它应该修改 x30,所以它将 x30 保存在它的堆栈中。但是为什么它把它存储在 sp-#0x20 中呢?为什么变量 x,y,z 存储在 sp + #20, sp + #24, sp + #28 中?如果 main 函数调用 printf,我猜 sp 和 x29 会减少一些。这个数量是否取决于被调用函数(此处为 printf)使用多少堆栈区域?还是恒定的?以及 main 中的 x29、x30 存储位置是如何确定的?是否确定这两个值位于被调用函数(printf)的堆栈区域上方?抱歉问了太多问题。

【问题讨论】:

  • “为什么?似乎是多余的。它已经在 w0 中了” 您正在查看未优化的程序集。此类代码可能包含大量冗余指令。
  • 是的。通常最好使用至少-O1 进行编译以避免奇怪的堆栈移动。
  • 您的分析看起来大致正确。请注意,使用z 加载w1 并不是多余的,因为这是传递给printf 所需的位置。但是mov w1, w0 会做得更简单;或者,如果代码经过优化,z 可以首先在 w1 中计算。但是您有没有更具体的问题想问?
  • 不优化时,编译器生成的代码好像每个变量的“真正的家”都在内存中,因此对该变量的每个引用都应该相应地读取或写入内存。它没有注意到其他一些寄存器(甚至是 same 寄存器!)可能已经包含该值的副本。如您所见,结果往往效率低下且冗余,对于您应该如何编写自己的汇编代码来说,这不是一个好的模型。
  • x0 不是w0 的字符串地址,但是是的。 “填写”%dprintf 所做的,它解析格式字符串,%d 告诉它取 w1 中的数字,转换为十进制,然后输出。

标签: assembly callstack arm64 abi


【解决方案1】:

在为main 布置堆栈时,编译器必须满足以下约束:

  • x29x30 需要保存在堆栈中。它们每个占用 8 个字节。

  • 局部变量x,y,z需要堆栈空间,每个4字节。 (如果您正在优化,您会看到它们被保存在寄存器中,或者完全不存在优化。)这使我们得到8+8+4+4+4=28 字节的总数。

  • 堆栈指针sp 必须始终保持对齐到 16 个字节;这是一个架构和 ABI 约束(操作系统可以选择放宽此要求,但通常不会)。所以我们不能只从sp中减去28;我们必须四舍五入到 16 的下一个倍数,即 32。

这就是您提到的 32 或 0x20 的来源。请注意,它完全用于main 本身使用的堆栈内存。它不是一个普遍的常数。如果您从 main 添加或删除足够多的局部变量,您会看到它发生了变化。

它与printf 的需求无关。如果printf 需要堆栈空间来存放它自己的局部变量,那么printf 中的代码将不得不相应地调整堆栈指针。编译main时的编译器不知道会有多少空间,也不在乎。

现在编译器需要将这五个对象x29, x30, x, y, z 组织在它将为自己创建的 32 字节堆栈空间内。除了以下几点之外,放置什么内容的选择几乎完全是任意的。

函数的序言需要从堆栈指针中减去 32,并将寄存器 x29, x30 存储在分配空间内的某个位置。这一切都可以在一条带有预索引存储对指令stp x29, x30, [sp, #-32]! 的指令中完成。它从sp 中减去32,然后将x29x30 存储在从sp 现在指向的地址开始的16 个字节中。所以为了使用这条指令,我们必须接受将x29x30放在分配空间的底部,相对于new值的偏移量[sp+0][sp+8] sp。将它们放在其他任何地方都需要额外的说明并且效率较低。

(实际上,因为这是最方便的方式,ABI 实际上要求堆栈帧以这种方式设置,x29, x30 按顺序在堆栈上连续,当它们被使用时(5.2.3)。)

我们还有从[sp+16] 开始的16 个字节可供使用,其中必须放置x,y,z。编译器选择将它们分别放在地址[sp+28], [sp+24], [sp+20][sp+16] 的 4 个字节仍未使用,但请记住,我们必须在某处浪费 4 个字节才能实现正确的堆栈对齐。安排这些对象的选择,以及让哪个插槽不用,完全是任意的,任何其他安排都可以。

【讨论】:

  • 回复:未优化代码:Why does clang produce inefficient asm with -O0 (for this simple floating point sum)? 解释了为什么编译器在默认情况下会以这种方式运行 -O0
  • 非常感谢。这解释了很多事情。我认为对我来说最令人困惑的部分是 stp x29, x30, [sp, #-32]。我认为 sp 被这条指令保留,但它不是。我不知道(可能忘记了..)索引前和索引后。
  • @ChanKim:不客气。不要忘记stp 指令中的! - 这就是请求预索引的原因。我希望他们选择了更好的语法。
猜你喜欢
  • 1970-01-01
  • 2014-09-30
  • 2015-11-16
  • 2020-04-05
  • 2020-09-23
  • 2016-03-14
  • 1970-01-01
  • 2021-01-28
  • 1970-01-01
相关资源
最近更新 更多