理解aarch64汇编函数调用,栈是如何操作的
understanding aarch64 assembly function call, how is stack operated
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
我用<--
标记添加了我的理解。有人可以看到代码并给我一些更正吗?任何小评论将不胜感激。 (请看来自<main>
)
添加:感谢您的评论。我想我忘了问我真正的问题。在 main 的开始,调用 main 的程序应该把它的 return 地址(在 main 之后)放在 x30 中。由于 main 本身应该调用另一个函数,它应该修改 x30,所以它将 x30 保存在它的堆栈中。但为什么它存储在 sp - #0x20 中?为什么变量 x、y、z 存储在 sp + #20、sp + #24、sp + #28 中?如果主函数调用 printf,我猜 sp 和 x29 会减少一些。这个数量是否取决于被调用函数(这里是 printf)使用了多少堆栈区域?还是恒定的?以及如何确定 main 中的 x29、x30 存储位置?是否确定这两个值位于被调用函数(printf)的堆栈区域的正上方?抱歉问题太多。
在为 main
布置堆栈时,编译器必须满足以下约束:
x29
和 x30
需要保存在堆栈上。它们每个占用8个字节。
局部变量x,y,z
需要堆栈space,每个4字节。 (如果你正在优化,你会看到它们保存在寄存器中,或者完全不存在。)这使我们总共 8+8+4+4+4=28
字节。
堆栈指针sp
必须始终保持16字节对齐;这是架构和 ABI 约束(OS 可以选择放宽此要求,但通常不会)。所以我们不能只从 sp
中减去 28;我们必须四舍五入到下一个 16 的倍数,即 32。
这就是您提到的 32 或 0x20
的来源。请注意,它完全用于 main
本身使用的堆栈内存。它不是一个普遍的常数;如果您从 main
.
添加或删除足够的局部变量,您会看到它发生变化
它与 printf
的任何需求无关。如果 printf
需要堆栈 space 作为它自己的局部变量,那么 printf
中的代码将不得不相应地调整堆栈指针。编译器在编译 main
时不知道 space 是多少,也不关心。
现在编译器需要在它将为自己创建的 32 字节堆栈 space 中组织这五个对象 x29, x30, x, y, z
。除了以下几点外,几乎可以完全随意选择放置什么。
函数的序言需要从堆栈指针中减去 32,并将寄存器 x29, x30
存储在分配的 space 中的某个位置。这一切都可以通过预索引存储对指令 stp x29, x30, [sp, #-32]!
在一条指令中完成。它从 sp
中减去 32
,然后将 x29
和 x30
存储在从 sp
现在指向的地址开始的 16 个字节中。所以为了使用这条指令,我们必须接受将 x29
和 x30
放在分配的 space 的底部,相对偏移 [sp+0]
和 [sp+8]
到 sp
的 new 值。将它们放在其他地方需要额外的说明并且效率较低。
(实际上,因为这是最方便的方法,ABI 实际上要求以这种方式设置堆栈帧,x29, x30
按此顺序在堆栈上连续,当它们被使用时 (5.2.3).)
我们还有从 [sp+16]
开始的 16 个字节可以玩,其中必须放置 x,y,z
。编译器已选择将它们分别放在地址 [sp+28], [sp+24], [sp+20]
处。 [sp+16]
处的 4 个字节未使用,但请记住,我们必须在某处浪费 4 个字节才能实现正确的堆栈对齐。安排这些对象的选择,以及不使用哪个插槽,完全是任意的,任何其他安排也同样有效。
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
我用<--
标记添加了我的理解。有人可以看到代码并给我一些更正吗?任何小评论将不胜感激。 (请看来自<main>
)
添加:感谢您的评论。我想我忘了问我真正的问题。在 main 的开始,调用 main 的程序应该把它的 return 地址(在 main 之后)放在 x30 中。由于 main 本身应该调用另一个函数,它应该修改 x30,所以它将 x30 保存在它的堆栈中。但为什么它存储在 sp - #0x20 中?为什么变量 x、y、z 存储在 sp + #20、sp + #24、sp + #28 中?如果主函数调用 printf,我猜 sp 和 x29 会减少一些。这个数量是否取决于被调用函数(这里是 printf)使用了多少堆栈区域?还是恒定的?以及如何确定 main 中的 x29、x30 存储位置?是否确定这两个值位于被调用函数(printf)的堆栈区域的正上方?抱歉问题太多。
在为 main
布置堆栈时,编译器必须满足以下约束:
x29
和x30
需要保存在堆栈上。它们每个占用8个字节。局部变量
x,y,z
需要堆栈space,每个4字节。 (如果你正在优化,你会看到它们保存在寄存器中,或者完全不存在。)这使我们总共8+8+4+4+4=28
字节。堆栈指针
sp
必须始终保持16字节对齐;这是架构和 ABI 约束(OS 可以选择放宽此要求,但通常不会)。所以我们不能只从sp
中减去 28;我们必须四舍五入到下一个 16 的倍数,即 32。
这就是您提到的 32 或 0x20
的来源。请注意,它完全用于 main
本身使用的堆栈内存。它不是一个普遍的常数;如果您从 main
.
它与 printf
的任何需求无关。如果 printf
需要堆栈 space 作为它自己的局部变量,那么 printf
中的代码将不得不相应地调整堆栈指针。编译器在编译 main
时不知道 space 是多少,也不关心。
现在编译器需要在它将为自己创建的 32 字节堆栈 space 中组织这五个对象 x29, x30, x, y, z
。除了以下几点外,几乎可以完全随意选择放置什么。
函数的序言需要从堆栈指针中减去 32,并将寄存器 x29, x30
存储在分配的 space 中的某个位置。这一切都可以通过预索引存储对指令 stp x29, x30, [sp, #-32]!
在一条指令中完成。它从 sp
中减去 32
,然后将 x29
和 x30
存储在从 sp
现在指向的地址开始的 16 个字节中。所以为了使用这条指令,我们必须接受将 x29
和 x30
放在分配的 space 的底部,相对偏移 [sp+0]
和 [sp+8]
到 sp
的 new 值。将它们放在其他地方需要额外的说明并且效率较低。
(实际上,因为这是最方便的方法,ABI 实际上要求以这种方式设置堆栈帧,x29, x30
按此顺序在堆栈上连续,当它们被使用时 (5.2.3).)
我们还有从 [sp+16]
开始的 16 个字节可以玩,其中必须放置 x,y,z
。编译器已选择将它们分别放在地址 [sp+28], [sp+24], [sp+20]
处。 [sp+16]
处的 4 个字节未使用,但请记住,我们必须在某处浪费 4 个字节才能实现正确的堆栈对齐。安排这些对象的选择,以及不使用哪个插槽,完全是任意的,任何其他安排也同样有效。