assembly - 为什么传参数时栈上有洞?
问题描述
我对汇编代码不太熟悉。如果这个问题很幼稚,请原谅。
我有一个简单的 C 程序:
int f1(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9)
{
int c = 3;
int d = 4;
return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + c + d;
}
int main(int argc, char** argv)
{
f1(1, 2, 3, 4, 5, 6, 7, 8, 9);
}
我将它编译成elf64-x86-64并得到下面的反汇编代码:
f1():
0000000000000000 <f1>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d ec mov %edi,-0x14(%rbp) ; 1
7: 89 75 e8 mov %esi,-0x18(%rbp) ; 2
a: 89 55 e4 mov %edx,-0x1c(%rbp) ; 3
d: 89 4d e0 mov %ecx,-0x20(%rbp) ; 4
10: 44 89 45 dc mov %r8d,-0x24(%rbp) ; 5
14: 44 89 4d d8 mov %r9d,-0x28(%rbp) ; 6
18: c7 45 f8 03 00 00 00 movl $0x3,-0x8(%rbp) ; c = 3
1f: c7 45 fc 04 00 00 00 movl $0x4,-0x4(%rbp) ; d = 4
26: 8b 45 e8 mov -0x18(%rbp),%eax ;2
29: 8b 55 ec mov -0x14(%rbp),%edx ; 1
2c: 01 c2 add %eax,%edx
2e: 8b 45 e4 mov -0x1c(%rbp),%eax ;3
31: 01 c2 add %eax,%edx
33: 8b 45 e0 mov -0x20(%rbp),%eax ;4
36: 01 c2 add %eax,%edx
38: 8b 45 dc mov -0x24(%rbp),%eax ;5
3b: 01 c2 add %eax,%edx
3d: 8b 45 d8 mov -0x28(%rbp),%eax ; 6
40: 01 c2 add %eax,%edx
42: 8b 45 10 mov 0x10(%rbp),%eax ;7
45: 01 c2 add %eax,%edx
47: 8b 45 18 mov 0x18(%rbp),%eax ; 8
4a: 01 c2 add %eax,%edx
4c: 8b 45 20 mov 0x20(%rbp),%eax ; 9
4f: 01 c2 add %eax,%edx
51: 8b 45 f8 mov -0x8(%rbp),%eax ; c =3
54: 01 c2 add %eax,%edx
56: 8b 45 fc mov -0x4(%rbp),%eax ; d =4
59: 01 d0 add %edx,%eax
5b: 5d pop %rbp
5c: c3 retq
主要的():
000000000000005d <main>:
5d: 55 push %rbp
5e: 48 89 e5 mov %rsp,%rbp
61: 48 83 ec 30 sub $0x30,%rsp
65: 89 7d fc mov %edi,-0x4(%rbp)
68: 48 89 75 f0 mov %rsi,-0x10(%rbp)
6c: c7 44 24 10 09 00 00 movl $0x9,0x10(%rsp)
73: 00
74: c7 44 24 08 08 00 00 movl $0x8,0x8(%rsp)
7b: 00
7c: c7 04 24 07 00 00 00 movl $0x7,(%rsp)
83: 41 b9 06 00 00 00 mov $0x6,%r9d
89: 41 b8 05 00 00 00 mov $0x5,%r8d
8f: b9 04 00 00 00 mov $0x4,%ecx
94: ba 03 00 00 00 mov $0x3,%edx
99: be 02 00 00 00 mov $0x2,%esi
9e: bf 01 00 00 00 mov $0x1,%edi
a3: b8 00 00 00 00 mov $0x0,%eax
a8: e8 00 00 00 00 callq ad <main+0x50>
ad: c9 leaveq
ae: c3 retq
从to传递参数时,堆栈上似乎有一些洞:main()
f1()
我的问题是:
为什么需要这些孔?
为什么我们需要低于 2 条装配线?如果它们用于上下文恢复,我看不到任何说明。而且该
%rsi
寄存器甚至没有在其他地方使用。为什么还要保存%rsi
在堆栈上?
65: 89 7d fc mov %edi,-0x4(%rbp)
68: 48 89 75 f0 mov %rsi,-0x10(%rbp)
- 又提出了一个问题,既然 args
1 ~ 6
已经通过寄存器传递了,为什么要在开头将它们移回内存f1()
?
解决方案
传入 x86-64 System V ABI 的 arg 使用堆栈上的 8 字节“槽”,用于不适合寄存器的 args。任何不是 8 字节的倍数的东西都会在下一个堆栈 arg 之前有孔(填充)。
这是跨操作系统/架构调用约定的相当标准。在 32 位调用约定中传递 ashort
将使用 4 字节堆栈槽(或占用整个 4 字节寄存器,无论它是否符号扩展至整个寄存器宽度)。
您的最后两个问题实际上是在问同样的事情:
您在没有优化的情况下进行编译,因此为了进行一致的调试,包括函数 args 在内的每个变量都需要一个内存地址,调试器可以在断点处停止时修改该值。这包括main
'sargc
和argv
,以及寄存器 args 到f1
。
如果您定义main
为(这是托管 C 实现中int main(void)
的两个有效签名之一,另一个是),则 main 不会有传入参数溢出。main
int main(int argc, char**argv)
如果您在启用优化的情况下进行编译,则不会有任何废话。请参阅如何从 GCC/clang 程序集输出中删除“噪音”?有关如何让编译器生成好看的 asm 的建议。例如,从Godbolt 编译器资源管理器,用gcc -O3 -fPIC
1编译,你得到:
f1:
addl %esi, %edi # a2, tmp106 # tmp106 = a1 + a2
movl 8(%rsp), %eax # a7, tmp110
addl %edx, %edi # a3, tmp107
addl %ecx, %edi # a4, tmp108
addl %r8d, %edi # a5, tmp109
addl %r9d, %edi # a6, tmp110
addl %edi, %eax # tmp110, tmp110
addl 16(%rsp), %eax # a8, tmp112
addl 24(%rsp), %eax # a9, tmp113
addl $7, %eax #, tmp105 # c+d = constant 7
ret
(我使用 AT&T 语法而不是 Intel,因为您在问题中使用了它)
IDK 正是为什么 gcc 保留了比实际需要更多的堆栈空间;即使启用了优化,有时也会发生这种情况。例如 gcc 的main
样子是这样的:
# gcc -O3
main:
subq $16, %rsp # useless; the space isn't used and it doesn't change stack alignment.
movl $6, %r9d
movl $5, %r8d
movl $4, %ecx
pushq $9
movl $3, %edx
movl $2, %esi
movl $1, %edi
pushq $8
pushq $7
call f1@PLT
xorl %eax, %eax # implicit return 0
addq $40, %rsp
ret
在您的函数版本中发生的所有额外废话都是一致调试所需的反优化的结果,您可以使用默认的-O0
. (一致的调试意味着您可以set
在断点处停止变量,甚至jump
可以在同一函数内的另一个源代码行,并且程序仍将按照您在 C 抽象机中的预期运行和工作。所以编译器不能跨语句保留寄存器中的任何内容,或基于语句中的文字常量以外的任何内容进行优化。)
-O0
也意味着编译快,不要试图有效地分配堆栈空间。
脚注 1:-fPIC
防止 gcc 优化掉main
.
没有它,即使使用__attribute__((noinline))
,它也可以看到该函数没有副作用,因此它可以省略调用而不是内联并优化它。
但是-fPIC
意味着为共享库生成代码,这(在针对 Linux 时)意味着符号插入是可能的,因此编译器不能假设call f1@plt
实际上会调用的这个定义f1
,因此不能基于它没有副作用进行优化。
clang 显然假设它仍然可以优化,即使使用-fPIC
,所以我猜 clang 假设同一函数的冲突定义是不允许的还是什么?这似乎会破坏库函数的 LD_PRELOAD 覆盖,以便从库中调用。
推荐阅读
- c++ - constexpr 和 CRTP 的问题
- c# - C# 值单元化
- r - 从 R 连接到 Microsoft SQL 服务器
- firebase - 没有创建 Firebase 应用“[DEFAULT]” - 即使调用 Firebase.InitializeApp(),错误仍然存在
- karate - 空手道 UI:通过 CSS 定位文本
- python - Python3 - 写入 CSV 文件时的 .replace() 字符串
- swift - 如何在 Swift 中合并核心数据背景上下文和主上下文之间的更改
- java - 尝试在空对象引用 Firebase 上调用虚拟方法“boolean java.lang.String.equals(java.lang.Object)”
- python - 如何解释 PySurvival 的 predict_survival 输出?
- windows - AWS Windows EC2 在前台运行 Selenium Webdriver