为什么传递参数时栈上有空洞?
Why there are holes on the stack when passing parameters?
我对汇编代码不是很熟悉。对不起,如果这个问题很幼稚。
我有一个简单的 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 [=11=]x3,-0x8(%rbp) ; c = 3
1f: c7 45 fc 04 00 00 00 movl [=11=]x4,-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 [=12=]x30,%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 [=12=]x9,0x10(%rsp)
73: 00
74: c7 44 24 08 08 00 00 movl [=12=]x8,0x8(%rsp)
7b: 00
7c: c7 04 24 07 00 00 00 movl [=12=]x7,(%rsp)
83: 41 b9 06 00 00 00 mov [=12=]x6,%r9d
89: 41 b8 05 00 00 00 mov [=12=]x5,%r8d
8f: b9 04 00 00 00 mov [=12=]x4,%ecx
94: ba 03 00 00 00 mov [=12=]x3,%edx
99: be 02 00 00 00 mov [=12=]x2,%esi
9e: bf 01 00 00 00 mov [=12=]x1,%edi
a3: b8 00 00 00 00 mov [=12=]x0,%eax
a8: e8 00 00 00 00 callq ad <main+0x50>
ad: c9 leaveq
ae: c3 retq
从main()
传递参数到f1()
时,堆栈上似乎有一些漏洞:
我的问题是:
为什么需要这些孔?
为什么我们需要下面两行汇编?如果它们用于上下文恢复,我看不到任何说明。 %rsi
寄存器甚至没有在其他地方使用。为什么还要在堆栈上保存 %rsi
?
65: 89 7d fc mov %edi,-0x4(%rbp)
68: 48 89 75 f0 mov %rsi,-0x10(%rbp)
- 又想出了一个问题,因为参数
1 ~ 6
已经通过 寄存器 传递了,为什么要将它们 移回内存在f1()
? 开头
传入 x86-64 System V ABI 的 Arg 在堆栈上使用 8 字节 "slots",用于不适合寄存器的 args。任何不是 8 字节倍数的东西都会在下一个堆栈 arg 之前有空洞(填充)。
这是跨操作系统/架构调用约定的非常标准。在 32 位调用约定中传递 short
将使用一个 4 字节堆栈槽(或占用整个 4 字节寄存器,无论它是否被符号扩展到整个寄存器宽度)。
你最后两个问题问的是同一件事:
您正在未经优化的情况下进行编译,因此为了进行一致的调试,包括函数 args 在内的每个变量都需要一个内存地址,调试器可以在断点处停止时修改该地址的值。这包括 main
的 argc
和 argv
,以及 f1
.
的寄存器参数
如果您将 main
定义为 int main(void)
(这是托管 C 实现中 main
的两个有效签名之一,另一个是 int main(int argc, char**argv)
),则d 没有传入的参数让 main 溢出。
如果您在启用优化的情况下编译,就会有 none 的废话 。见 for suggestions on how to get compilers to make asm that's nice to look at. e.g. from the Godbolt compiler explorer,用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 , %eax #, tmp105 # c+d = constant 7
ret
(我使用了 AT&T 语法而不是 Intel,因为你在问题中使用了它)
IDK 究竟为什么 gcc 保留比实际需要更多的堆栈 space;即使启用了优化,有时也会发生这种情况。例如gcc 的 main
看起来像这样:
# gcc -O3
main:
subq , %rsp # useless; the space isn't used and it doesn't change stack alignment.
movl , %r9d
movl , %r8d
movl , %ecx
pushq
movl , %edx
movl , %esi
movl , %edi
pushq
pushq
call f1@PLT
xorl %eax, %eax # implicit return 0
addq , %rsp
ret
在您的函数版本中发生的所有额外的废话都是一致调试所需的反优化的结果,您可以使用默认值 -O0
.(一致的调试意味着你可以 set
在断点处停止的变量,甚至 jump
到同一函数内的另一个源代码行,程序仍然 运行 并且可以工作正如您在 C 抽象机中所期望的那样。因此,编译器无法跨语句将任何内容保存在寄存器中,也无法根据语句中文字常量以外的任何内容进行优化。)
-O0
也意味着编译速度快,不要试图有效地分配堆栈 space。
脚注 1:-fPIC
阻止 gcc 优化掉 main
.
中的调用
没有它,即使使用 __attribute__((noinline))
,它也可以看到该函数没有副作用,因此它可以省略调用而不是内联它并对其进行优化。
但是 -fPIC
意味着为共享库生成代码,这(当以 Linux 为目标时)意味着符号插入是可能的,所以编译器不能假设 call f1@plt
会实际调用 f1
的定义,因此无法基于它进行优化,没有副作用。
clang 显然假设即使使用 -fPIC
它仍然可以优化这种方式,所以我猜 clang 假设不允许对同一函数的冲突定义或其他什么?这似乎会破坏 LD_PRELOAD 覆盖库函数以从库内调用。
我对汇编代码不是很熟悉。对不起,如果这个问题很幼稚。
我有一个简单的 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 [=11=]x3,-0x8(%rbp) ; c = 3
1f: c7 45 fc 04 00 00 00 movl [=11=]x4,-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 [=12=]x30,%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 [=12=]x9,0x10(%rsp)
73: 00
74: c7 44 24 08 08 00 00 movl [=12=]x8,0x8(%rsp)
7b: 00
7c: c7 04 24 07 00 00 00 movl [=12=]x7,(%rsp)
83: 41 b9 06 00 00 00 mov [=12=]x6,%r9d
89: 41 b8 05 00 00 00 mov [=12=]x5,%r8d
8f: b9 04 00 00 00 mov [=12=]x4,%ecx
94: ba 03 00 00 00 mov [=12=]x3,%edx
99: be 02 00 00 00 mov [=12=]x2,%esi
9e: bf 01 00 00 00 mov [=12=]x1,%edi
a3: b8 00 00 00 00 mov [=12=]x0,%eax
a8: e8 00 00 00 00 callq ad <main+0x50>
ad: c9 leaveq
ae: c3 retq
从main()
传递参数到f1()
时,堆栈上似乎有一些漏洞:
我的问题是:
为什么需要这些孔?
为什么我们需要下面两行汇编?如果它们用于上下文恢复,我看不到任何说明。
%rsi
寄存器甚至没有在其他地方使用。为什么还要在堆栈上保存%rsi
?
65: 89 7d fc mov %edi,-0x4(%rbp)
68: 48 89 75 f0 mov %rsi,-0x10(%rbp)
- 又想出了一个问题,因为参数
1 ~ 6
已经通过 寄存器 传递了,为什么要将它们 移回内存在f1()
? 开头
传入 x86-64 System V ABI 的 Arg 在堆栈上使用 8 字节 "slots",用于不适合寄存器的 args。任何不是 8 字节倍数的东西都会在下一个堆栈 arg 之前有空洞(填充)。
这是跨操作系统/架构调用约定的非常标准。在 32 位调用约定中传递 short
将使用一个 4 字节堆栈槽(或占用整个 4 字节寄存器,无论它是否被符号扩展到整个寄存器宽度)。
你最后两个问题问的是同一件事:
您正在未经优化的情况下进行编译,因此为了进行一致的调试,包括函数 args 在内的每个变量都需要一个内存地址,调试器可以在断点处停止时修改该地址的值。这包括 main
的 argc
和 argv
,以及 f1
.
如果您将 main
定义为 int main(void)
(这是托管 C 实现中 main
的两个有效签名之一,另一个是 int main(int argc, char**argv)
),则d 没有传入的参数让 main 溢出。
如果您在启用优化的情况下编译,就会有 none 的废话 。见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 , %eax #, tmp105 # c+d = constant 7
ret
(我使用了 AT&T 语法而不是 Intel,因为你在问题中使用了它)
IDK 究竟为什么 gcc 保留比实际需要更多的堆栈 space;即使启用了优化,有时也会发生这种情况。例如gcc 的 main
看起来像这样:
# gcc -O3
main:
subq , %rsp # useless; the space isn't used and it doesn't change stack alignment.
movl , %r9d
movl , %r8d
movl , %ecx
pushq
movl , %edx
movl , %esi
movl , %edi
pushq
pushq
call f1@PLT
xorl %eax, %eax # implicit return 0
addq , %rsp
ret
在您的函数版本中发生的所有额外的废话都是一致调试所需的反优化的结果,您可以使用默认值 -O0
.(一致的调试意味着你可以 set
在断点处停止的变量,甚至 jump
到同一函数内的另一个源代码行,程序仍然 运行 并且可以工作正如您在 C 抽象机中所期望的那样。因此,编译器无法跨语句将任何内容保存在寄存器中,也无法根据语句中文字常量以外的任何内容进行优化。)
-O0
也意味着编译速度快,不要试图有效地分配堆栈 space。
脚注 1:-fPIC
阻止 gcc 优化掉 main
.
没有它,即使使用 __attribute__((noinline))
,它也可以看到该函数没有副作用,因此它可以省略调用而不是内联它并对其进行优化。
但是 -fPIC
意味着为共享库生成代码,这(当以 Linux 为目标时)意味着符号插入是可能的,所以编译器不能假设 call f1@plt
会实际调用 f1
的定义,因此无法基于它进行优化,没有副作用。
clang 显然假设即使使用 -fPIC
它仍然可以优化这种方式,所以我猜 clang 假设不允许对同一函数的冲突定义或其他什么?这似乎会破坏 LD_PRELOAD 覆盖库函数以从库内调用。