为什么较小的堆栈边界不会发生分段错误?
Why segmentation fault doesn't occur with smaller stack boundary?
我试图了解使用 GCC 选项 -mpreferred-stack-boundary=2
编译的代码与默认值 -mpreferred-stack-boundary=4
.
之间的行为差异
我已经阅读了很多关于此选项的 Q/A 但我无法理解我将在下面描述的情况。
让我们考虑一下这段代码:
#include <stdio.h>
#include <string.h>
void dumb_function() {}
int main(int argc, char** argv) {
dumb_function();
char buffer[24];
strcpy(buffer, argv[1]);
return 0;
}
在我的 64 位架构上,我想将其编译为 32 位,因此我将使用 -m32
选项。因此,我创建了两个二进制文件,一个带有 -mpreferred-stack-boundary=2
,一个带有默认值:
sysctl -w kernel.randomize_va_space=0
gcc -m32 -g3 -fno-stack-protector -z execstack -o default vuln.c
gcc -mpreferred-stack-boundary=2 -m32 -g3 -fno-stack-protector -z execstack -o align_2 vuln.c
现在,如果我用两个字节的溢出来执行它们,我会遇到默认对齐的分段错误,但在其他情况下不会:
$ ./default 1234567890123456789012345
Segmentation fault (core dumped)
$ ./align_2 1234567890123456789012345
$
我试图用 default
来探究为什么会出现这种行为。下面是主要功能的反汇编:
08048411 <main>:
8048411: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048415: 83 e4 f0 and [=13=]xfffffff0,%esp
8048418: ff 71 fc pushl -0x4(%ecx)
804841b: 55 push %ebp
804841c: 89 e5 mov %esp,%ebp
804841e: 53 push %ebx
804841f: 51 push %ecx
8048420: 83 ec 20 sub [=13=]x20,%esp
8048423: 89 cb mov %ecx,%ebx
8048425: e8 e1 ff ff ff call 804840b <dumb_function>
804842a: 8b 43 04 mov 0x4(%ebx),%eax
804842d: 83 c0 04 add [=13=]x4,%eax
8048430: 8b 00 mov (%eax),%eax
8048432: 83 ec 08 sub [=13=]x8,%esp
8048435: 50 push %eax
8048436: 8d 45 e0 lea -0x20(%ebp),%eax
8048439: 50 push %eax
804843a: e8 a1 fe ff ff call 80482e0 <strcpy@plt>
804843f: 83 c4 10 add [=13=]x10,%esp
8048442: b8 00 00 00 00 mov [=13=]x0,%eax
8048447: 8d 65 f8 lea -0x8(%ebp),%esp
804844a: 59 pop %ecx
804844b: 5b pop %ebx
804844c: 5d pop %ebp
804844d: 8d 61 fc lea -0x4(%ecx),%esp
8048450: c3 ret
8048451: 66 90 xchg %ax,%ax
8048453: 66 90 xchg %ax,%ax
8048455: 66 90 xchg %ax,%ax
8048457: 66 90 xchg %ax,%ax
8048459: 66 90 xchg %ax,%ax
804845b: 66 90 xchg %ax,%ax
804845d: 66 90 xchg %ax,%ax
804845f: 90 nop
感谢 sub [=26=]x20,%esp
指令,我们可以了解到编译器为堆栈分配了 32 个字节,这是一致的 -mpreferred-stack-boundary=4
选项:32 是 16 的倍数。
第一个问题:为什么,如果我有一个 32 字节的堆栈(24 个字节用于缓冲区和其余的垃圾),我会得到一个仅溢出一个字节的分段错误?
让我们看看 gdb 发生了什么:
$ gdb default
(gdb) b 10
Breakpoint 1 at 0x804842a: file vuln.c, line 10.
(gdb) b 12
Breakpoint 2 at 0x8048442: file vuln.c, line 12.
(gdb) r 1234567890123456789012345
Starting program: /home/pierre/example/default 1234567890123456789012345
Breakpoint 1, main (argc=2, argv=0xffffce94) at vuln.c:10
10 strcpy(buffer, argv[1]);
(gdb) i f
Stack level 0, frame at 0xffffce00:
eip = 0x804842a in main (vuln.c:10); saved eip = 0xf7e07647
source language c.
Arglist at 0xffffcde8, args: argc=2, argv=0xffffce94
Locals at 0xffffcde8, Previous frame's sp is 0xffffce00
Saved registers:
ebx at 0xffffcde4, ebp at 0xffffcde8, eip at 0xffffcdfc
(gdb) x/6x buffer
0xffffcdc8: 0xf7e1da60 0x080484ab 0x00000002 0xffffce94
0xffffcdd8: 0xffffcea0 0x08048481
(gdb) x/x buffer+36
0xffffcdec: 0xf7e07647
就在调用strcpy
之前,我们可以看到保存的eip是0xf7e07647
。我们可以从缓冲区地址(堆栈堆栈的 32 个字节 + esp 的 4 个字节 = 36 个字节)返回此信息。
让我们继续:
(gdb) c
Continuing.
Breakpoint 2, main (argc=0, argv=0x0) at vuln.c:12
12 return 0;
(gdb) i f
Stack level 0, frame at 0xffff0035:
eip = 0x8048442 in main (vuln.c:12); saved eip = 0x0
source language c.
Arglist at 0xffffcde8, args: argc=0, argv=0x0
Locals at 0xffffcde8, Previous frame's sp is 0xffff0035
Saved registers:
ebx at 0xffffcde4, ebp at 0xffffcde8, eip at 0xffff0031
(gdb) x/7x buffer
0xffffcdc8: 0x34333231 0x38373635 0x32313039 0x36353433
0xffffcdd8: 0x30393837 0x34333231 0xffff0035
(gdb) x/x buffer+36
0xffffcdec: 0xf7e07647
我们可以看到缓冲区后的下一个字节溢出:0xffff0035
。此外,存储 eip 的位置没有任何变化:0xffffcdec: 0xf7e07647
因为溢出仅为两个字节。但是,info frame
保存的eip 发生了变化:saved eip = 0x0
如果我继续,就会出现分段错误:
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x00000000 in ?? ()
发生了什么事?为什么我保存的eip变了,而溢出只有两个字节?
现在,让我们将其与使用另一种对齐方式编译的二进制文件进行比较:
$ objdump -d align_2
...
08048411 <main>:
...
8048414: 83 ec 18 sub [=17=]x18,%esp
...
堆栈恰好是 24 个字节。这意味着 2 个字节的溢出将覆盖 esp(但仍然不是 eip)。让我们用 gdb 检查一下:
(gdb) b 10
Breakpoint 1 at 0x804841c: file vuln.c, line 10.
(gdb) b 12
Breakpoint 2 at 0x8048431: file vuln.c, line 12.
(gdb) r 1234567890123456789012345
Starting program: /home/pierre/example/align_2 1234567890123456789012345
Breakpoint 1, main (argc=2, argv=0xffffce94) at vuln.c:10
10 strcpy(buffer, argv[1]);
(gdb) i f
Stack level 0, frame at 0xffffce00:
eip = 0x804841c in main (vuln.c:10); saved eip = 0xf7e07647
source language c.
Arglist at 0xffffcdf8, args: argc=2, argv=0xffffce94
Locals at 0xffffcdf8, Previous frame's sp is 0xffffce00
Saved registers:
ebp at 0xffffcdf8, eip at 0xffffcdfc
(gdb) x/6x buffer
0xffffcde0: 0xf7fa23dc 0x080481fc 0x08048449 0x00000000
0xffffcdf0: 0xf7fa2000 0xf7fa2000
(gdb) x/x buffer+28
0xffffcdfc: 0xf7e07647
(gdb) c
Continuing.
Breakpoint 2, main (argc=2, argv=0xffffce94) at vuln.c:12
12 return 0;
(gdb) i f
Stack level 0, frame at 0xffffce00:
eip = 0x8048431 in main (vuln.c:12); saved eip = 0xf7e07647
source language c.
Arglist at 0xffffcdf8, args: argc=2, argv=0xffffce94
Locals at 0xffffcdf8, Previous frame's sp is 0xffffce00
Saved registers:
ebp at 0xffffcdf8, eip at 0xffffcdfc
(gdb) x/7x buffer
0xffffcde0: 0x34333231 0x38373635 0x32313039 0x36353433
0xffffcdf0: 0x30393837 0x34333231 0x00000035
(gdb) x/x buffer+28
0xffffcdfc: 0xf7e07647
(gdb) c
Continuing.
[Inferior 1 (process 6118) exited normally]
不出所料,这里没有分段错误,因为我没有覆盖 eip。
我不明白这种行为差异。在这两种情况下,eip 都不会被覆盖。唯一的区别是堆栈的大小。发生什么事了?
附加信息:
- 如果
dumb_function
不存在,则不会发生此行为
- 我正在使用以下版本的 GCC:
$ gcc -v
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
- 关于我的系统的一些信息:
$ uname -a
Linux pierre-Inspiron-5567 4.15.0-107-generic #108~16.04.1-Ubuntu SMP Fri Jun 12 02:57:13 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
您没有覆盖保存的 eip,这是真的。但是您正在覆盖函数用来查找保存的 eip 的指针。您实际上可以在 i f
输出中看到这一点;查看“Previous frame's sp”并注意两个低字节是如何00 35
; ASCII 0x35 是 5
并且 00
是终止空值。因此,尽管保存的 eip 完好无损,但机器正在从其他地方获取其 return 地址,因此崩溃。
更详细:
GCC 显然不信任将堆栈对齐到 16 字节的启动代码,因此它自己处理事情 (and [=14=]xfffffff0,%esp
)。但它需要跟踪之前的堆栈指针值,以便在需要时可以找到它的参数和 return 地址。这是 lea 0x4(%esp),%ecx
,它用堆栈上保存的 eip 上方 的双字地址加载 ecx。 gdb 称此地址为“前一帧的 sp”,我猜是因为它是调用者执行其 call main
指令前 立即 的堆栈指针的值。我就简称为P吧。
对齐堆栈后,编译器将 -0x4(%ecx)
(即 argv
参数)从堆栈中压入,以便于访问,因为稍后将需要它。然后它用 push %ebp; mov %esp, %ebp
设置它的栈帧。从现在开始,我们可以跟踪所有相对于 %ebp
的地址,就像编译器在不优化时通常做的那样。
push %ecx
几行之后将地址 P 存储在堆栈中的偏移量 -0x8(%ebp)
处。 sub [=23=]x20, %esp
在堆栈上增加了 32 个字节的 space(以 -0x28(%ebp)
结尾),但问题是,space 在哪里结束 buffer
被安置?我们看到它发生在调用 dumb_function
之后,使用 lea -0x20(%ebp), %eax; push %eax
;这是 strcpy
被推送的第一个参数,即 buffer
,因此 buffer
实际上位于 -0x20(%ebp)
,而不是您可能猜到的 -0x28
。因此,当您在那里写入 24 (=0x18
) 个字节时,您会覆盖 -0x8(%ebp)
处的两个字节,这是我们存储的 P 指针。
从这里开始都是下坡路。 P 的损坏值(称为 Px)被弹出到 ecx 中,就在 return 之前,我们执行 lea -0x4(%ecx), %esp
。现在 %esp
是垃圾,指向不好的地方,所以下面的 ret
肯定会带来麻烦。也许 Px
指向未映射的内存,只是试图从那里获取 return 地址会导致错误。也许它指向可读内存,但从该位置获取的地址并不指向可执行内存,因此控制传输出错。也许后者确实指向可执行内存,但位于那里的指令不是我们想要执行的指令。
如果您 take out the call to dumb_function()
,堆栈布局会略有变化。不再需要围绕对 dumb_function()
的调用推送 ebx,因此来自 ecx 的 P 指针现在结束在 -4(%ebp)
,有 4 个未使用的字节 space(以保持对齐),然后 buffer
在 -0x20(%ebp)
。所以你的两个字节溢出进入 space 根本没有使用,因此没有崩溃。
而here是用-mpreferred-stack-boundary=2
生成的程序集。现在不需要重新对齐堆栈,因为编译器确实相信启动代码会将堆栈对齐到至少 4 个字节(如果不是这种情况是不可想象的)。堆栈布局更简单:压入 ebp,并为 buffer
再减去 24 个字节。因此,您的溢出会覆盖保存的 ebp 的两个字节。这最终从堆栈弹出回到 ebp,因此 main
returns 到它的调用者,ebp 中的值是
和入境时不一样。这很顽皮,但碰巧系统启动代码没有将 ebp 中的值用于任何事情(实际上在我的测试中它在进入 main 时设置为 0,可能标记堆栈顶部以进行回溯),并且所以之后没有什么不好的事情发生。
我试图了解使用 GCC 选项 -mpreferred-stack-boundary=2
编译的代码与默认值 -mpreferred-stack-boundary=4
.
我已经阅读了很多关于此选项的 Q/A 但我无法理解我将在下面描述的情况。
让我们考虑一下这段代码:
#include <stdio.h>
#include <string.h>
void dumb_function() {}
int main(int argc, char** argv) {
dumb_function();
char buffer[24];
strcpy(buffer, argv[1]);
return 0;
}
在我的 64 位架构上,我想将其编译为 32 位,因此我将使用 -m32
选项。因此,我创建了两个二进制文件,一个带有 -mpreferred-stack-boundary=2
,一个带有默认值:
sysctl -w kernel.randomize_va_space=0
gcc -m32 -g3 -fno-stack-protector -z execstack -o default vuln.c
gcc -mpreferred-stack-boundary=2 -m32 -g3 -fno-stack-protector -z execstack -o align_2 vuln.c
现在,如果我用两个字节的溢出来执行它们,我会遇到默认对齐的分段错误,但在其他情况下不会:
$ ./default 1234567890123456789012345
Segmentation fault (core dumped)
$ ./align_2 1234567890123456789012345
$
我试图用 default
来探究为什么会出现这种行为。下面是主要功能的反汇编:
08048411 <main>:
8048411: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048415: 83 e4 f0 and [=13=]xfffffff0,%esp
8048418: ff 71 fc pushl -0x4(%ecx)
804841b: 55 push %ebp
804841c: 89 e5 mov %esp,%ebp
804841e: 53 push %ebx
804841f: 51 push %ecx
8048420: 83 ec 20 sub [=13=]x20,%esp
8048423: 89 cb mov %ecx,%ebx
8048425: e8 e1 ff ff ff call 804840b <dumb_function>
804842a: 8b 43 04 mov 0x4(%ebx),%eax
804842d: 83 c0 04 add [=13=]x4,%eax
8048430: 8b 00 mov (%eax),%eax
8048432: 83 ec 08 sub [=13=]x8,%esp
8048435: 50 push %eax
8048436: 8d 45 e0 lea -0x20(%ebp),%eax
8048439: 50 push %eax
804843a: e8 a1 fe ff ff call 80482e0 <strcpy@plt>
804843f: 83 c4 10 add [=13=]x10,%esp
8048442: b8 00 00 00 00 mov [=13=]x0,%eax
8048447: 8d 65 f8 lea -0x8(%ebp),%esp
804844a: 59 pop %ecx
804844b: 5b pop %ebx
804844c: 5d pop %ebp
804844d: 8d 61 fc lea -0x4(%ecx),%esp
8048450: c3 ret
8048451: 66 90 xchg %ax,%ax
8048453: 66 90 xchg %ax,%ax
8048455: 66 90 xchg %ax,%ax
8048457: 66 90 xchg %ax,%ax
8048459: 66 90 xchg %ax,%ax
804845b: 66 90 xchg %ax,%ax
804845d: 66 90 xchg %ax,%ax
804845f: 90 nop
感谢 sub [=26=]x20,%esp
指令,我们可以了解到编译器为堆栈分配了 32 个字节,这是一致的 -mpreferred-stack-boundary=4
选项:32 是 16 的倍数。
第一个问题:为什么,如果我有一个 32 字节的堆栈(24 个字节用于缓冲区和其余的垃圾),我会得到一个仅溢出一个字节的分段错误?
让我们看看 gdb 发生了什么:
$ gdb default
(gdb) b 10
Breakpoint 1 at 0x804842a: file vuln.c, line 10.
(gdb) b 12
Breakpoint 2 at 0x8048442: file vuln.c, line 12.
(gdb) r 1234567890123456789012345
Starting program: /home/pierre/example/default 1234567890123456789012345
Breakpoint 1, main (argc=2, argv=0xffffce94) at vuln.c:10
10 strcpy(buffer, argv[1]);
(gdb) i f
Stack level 0, frame at 0xffffce00:
eip = 0x804842a in main (vuln.c:10); saved eip = 0xf7e07647
source language c.
Arglist at 0xffffcde8, args: argc=2, argv=0xffffce94
Locals at 0xffffcde8, Previous frame's sp is 0xffffce00
Saved registers:
ebx at 0xffffcde4, ebp at 0xffffcde8, eip at 0xffffcdfc
(gdb) x/6x buffer
0xffffcdc8: 0xf7e1da60 0x080484ab 0x00000002 0xffffce94
0xffffcdd8: 0xffffcea0 0x08048481
(gdb) x/x buffer+36
0xffffcdec: 0xf7e07647
就在调用strcpy
之前,我们可以看到保存的eip是0xf7e07647
。我们可以从缓冲区地址(堆栈堆栈的 32 个字节 + esp 的 4 个字节 = 36 个字节)返回此信息。
让我们继续:
(gdb) c
Continuing.
Breakpoint 2, main (argc=0, argv=0x0) at vuln.c:12
12 return 0;
(gdb) i f
Stack level 0, frame at 0xffff0035:
eip = 0x8048442 in main (vuln.c:12); saved eip = 0x0
source language c.
Arglist at 0xffffcde8, args: argc=0, argv=0x0
Locals at 0xffffcde8, Previous frame's sp is 0xffff0035
Saved registers:
ebx at 0xffffcde4, ebp at 0xffffcde8, eip at 0xffff0031
(gdb) x/7x buffer
0xffffcdc8: 0x34333231 0x38373635 0x32313039 0x36353433
0xffffcdd8: 0x30393837 0x34333231 0xffff0035
(gdb) x/x buffer+36
0xffffcdec: 0xf7e07647
我们可以看到缓冲区后的下一个字节溢出:0xffff0035
。此外,存储 eip 的位置没有任何变化:0xffffcdec: 0xf7e07647
因为溢出仅为两个字节。但是,info frame
保存的eip 发生了变化:saved eip = 0x0
如果我继续,就会出现分段错误:
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x00000000 in ?? ()
发生了什么事?为什么我保存的eip变了,而溢出只有两个字节?
现在,让我们将其与使用另一种对齐方式编译的二进制文件进行比较:
$ objdump -d align_2
...
08048411 <main>:
...
8048414: 83 ec 18 sub [=17=]x18,%esp
...
堆栈恰好是 24 个字节。这意味着 2 个字节的溢出将覆盖 esp(但仍然不是 eip)。让我们用 gdb 检查一下:
(gdb) b 10
Breakpoint 1 at 0x804841c: file vuln.c, line 10.
(gdb) b 12
Breakpoint 2 at 0x8048431: file vuln.c, line 12.
(gdb) r 1234567890123456789012345
Starting program: /home/pierre/example/align_2 1234567890123456789012345
Breakpoint 1, main (argc=2, argv=0xffffce94) at vuln.c:10
10 strcpy(buffer, argv[1]);
(gdb) i f
Stack level 0, frame at 0xffffce00:
eip = 0x804841c in main (vuln.c:10); saved eip = 0xf7e07647
source language c.
Arglist at 0xffffcdf8, args: argc=2, argv=0xffffce94
Locals at 0xffffcdf8, Previous frame's sp is 0xffffce00
Saved registers:
ebp at 0xffffcdf8, eip at 0xffffcdfc
(gdb) x/6x buffer
0xffffcde0: 0xf7fa23dc 0x080481fc 0x08048449 0x00000000
0xffffcdf0: 0xf7fa2000 0xf7fa2000
(gdb) x/x buffer+28
0xffffcdfc: 0xf7e07647
(gdb) c
Continuing.
Breakpoint 2, main (argc=2, argv=0xffffce94) at vuln.c:12
12 return 0;
(gdb) i f
Stack level 0, frame at 0xffffce00:
eip = 0x8048431 in main (vuln.c:12); saved eip = 0xf7e07647
source language c.
Arglist at 0xffffcdf8, args: argc=2, argv=0xffffce94
Locals at 0xffffcdf8, Previous frame's sp is 0xffffce00
Saved registers:
ebp at 0xffffcdf8, eip at 0xffffcdfc
(gdb) x/7x buffer
0xffffcde0: 0x34333231 0x38373635 0x32313039 0x36353433
0xffffcdf0: 0x30393837 0x34333231 0x00000035
(gdb) x/x buffer+28
0xffffcdfc: 0xf7e07647
(gdb) c
Continuing.
[Inferior 1 (process 6118) exited normally]
不出所料,这里没有分段错误,因为我没有覆盖 eip。
我不明白这种行为差异。在这两种情况下,eip 都不会被覆盖。唯一的区别是堆栈的大小。发生什么事了?
附加信息:
- 如果
dumb_function
不存在,则不会发生此行为 - 我正在使用以下版本的 GCC:
$ gcc -v
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
- 关于我的系统的一些信息:
$ uname -a
Linux pierre-Inspiron-5567 4.15.0-107-generic #108~16.04.1-Ubuntu SMP Fri Jun 12 02:57:13 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
您没有覆盖保存的 eip,这是真的。但是您正在覆盖函数用来查找保存的 eip 的指针。您实际上可以在 i f
输出中看到这一点;查看“Previous frame's sp”并注意两个低字节是如何00 35
; ASCII 0x35 是 5
并且 00
是终止空值。因此,尽管保存的 eip 完好无损,但机器正在从其他地方获取其 return 地址,因此崩溃。
更详细:
GCC 显然不信任将堆栈对齐到 16 字节的启动代码,因此它自己处理事情 (and [=14=]xfffffff0,%esp
)。但它需要跟踪之前的堆栈指针值,以便在需要时可以找到它的参数和 return 地址。这是 lea 0x4(%esp),%ecx
,它用堆栈上保存的 eip 上方 的双字地址加载 ecx。 gdb 称此地址为“前一帧的 sp”,我猜是因为它是调用者执行其 call main
指令前 立即 的堆栈指针的值。我就简称为P吧。
对齐堆栈后,编译器将 -0x4(%ecx)
(即 argv
参数)从堆栈中压入,以便于访问,因为稍后将需要它。然后它用 push %ebp; mov %esp, %ebp
设置它的栈帧。从现在开始,我们可以跟踪所有相对于 %ebp
的地址,就像编译器在不优化时通常做的那样。
push %ecx
几行之后将地址 P 存储在堆栈中的偏移量 -0x8(%ebp)
处。 sub [=23=]x20, %esp
在堆栈上增加了 32 个字节的 space(以 -0x28(%ebp)
结尾),但问题是,space 在哪里结束 buffer
被安置?我们看到它发生在调用 dumb_function
之后,使用 lea -0x20(%ebp), %eax; push %eax
;这是 strcpy
被推送的第一个参数,即 buffer
,因此 buffer
实际上位于 -0x20(%ebp)
,而不是您可能猜到的 -0x28
。因此,当您在那里写入 24 (=0x18
) 个字节时,您会覆盖 -0x8(%ebp)
处的两个字节,这是我们存储的 P 指针。
从这里开始都是下坡路。 P 的损坏值(称为 Px)被弹出到 ecx 中,就在 return 之前,我们执行 lea -0x4(%ecx), %esp
。现在 %esp
是垃圾,指向不好的地方,所以下面的 ret
肯定会带来麻烦。也许 Px
指向未映射的内存,只是试图从那里获取 return 地址会导致错误。也许它指向可读内存,但从该位置获取的地址并不指向可执行内存,因此控制传输出错。也许后者确实指向可执行内存,但位于那里的指令不是我们想要执行的指令。
如果您 take out the call to dumb_function()
,堆栈布局会略有变化。不再需要围绕对 dumb_function()
的调用推送 ebx,因此来自 ecx 的 P 指针现在结束在 -4(%ebp)
,有 4 个未使用的字节 space(以保持对齐),然后 buffer
在 -0x20(%ebp)
。所以你的两个字节溢出进入 space 根本没有使用,因此没有崩溃。
而here是用-mpreferred-stack-boundary=2
生成的程序集。现在不需要重新对齐堆栈,因为编译器确实相信启动代码会将堆栈对齐到至少 4 个字节(如果不是这种情况是不可想象的)。堆栈布局更简单:压入 ebp,并为 buffer
再减去 24 个字节。因此,您的溢出会覆盖保存的 ebp 的两个字节。这最终从堆栈弹出回到 ebp,因此 main
returns 到它的调用者,ebp 中的值是
和入境时不一样。这很顽皮,但碰巧系统启动代码没有将 ebp 中的值用于任何事情(实际上在我的测试中它在进入 main 时设置为 0,可能标记堆栈顶部以进行回溯),并且所以之后没有什么不好的事情发生。