调用_exit(0);
或exit_group(0)
结束_start
。(链接 withgcc -static -nostartfiles
而不是,-nostdlib
因此您可以调用 libc 系统调用包装函数;即使 glibc init 函数尚未运行,它们也应该可以工作,因此 malloc 或 printf 会崩溃)。
或者exit_group(0)
使用内联汇编手动进行系统调用。在 x86-64 Linux 上:
asm("mov $231, %eax; xor %edi,%edi; syscall");
另请参阅如何在没有 Glibc 的情况下使用 C 中的内联汇编获取参数值?有关编写 hacky x86-64_start
以运行您自己的 C 函数作为您进程中的第一件事的更多信息。(但大部分答案是关于破解调用约定以访问 argc / argv,这很讨厌,我不推荐它。)Matteo 对这个问题的回答是_start
用 asm 编写的一个完整的最小值,它调用了一个普通的 Cmain
函数。
这本书的代码完全被破坏了,原因有两个。(我不知道它是如何在 i386 或 x86-64 上工作的。对我来说似乎超级奇怪。你确定它不应该只是崩溃,但你看看它在发生之前做了什么?)
_start
不是 Linux 中的函数;您(或编译器生成的代码)不能ret
从中获取。您需要进行_exit
系统调用。堆栈1上没有返回地址。
在函数有返回地址的地方,ELF 入口点_start
有argc
,如 ABI 文档中所指定。(x86-64 System V 或 i386 System V 取决于您构建的是 64 位还是gcc -m32
32 位可执行文件。)
leave
在编译器生成的代码中插入(它mov %ebp, %esp
/pop %ebp
或 RBP/RSP 等效项)在这里没有意义。它有点像一个额外的,但会破坏编译器的/所以如果它碰巧选择而不是它自己的序言,那么编译器生成的代码将会出错。(在静态链接的可执行文件中,入口的 RBP为 0。或者在跳转到PIE 可执行文件之前,保留 RBP 中留下的任何动态链接器。)pop
EBP
RBP
leave
pop %rbp
_start
_start
但最终,GCC 将_start
作为正常函数编译,从而最终运行一条ret
指令。任何地方都没有有效/有用的返回地址,所以根本没有办法ret
工作。
如果你在没有优化的情况下编译(默认),gcc 将默认为-fno-omit-frame-pointer
,因此它的函数序言将设置 EBP 或 RBP 作为帧指针,使其leave
自身不会出错。如果您使用优化(-O1
和更高启用-fomit-frame-pointer
)进行编译,则 gcc 不会与 RBP 混淆,并且在您运行时它将为零leave
,从而直接导致段错误。(因为它做了 RSP=RBP 然后使用新的 RSP 作为堆栈指针pop %rbp
。)
无论如何,如果它没有出错,那将argc
在编译器生成pop %rbp
作为正常函数结尾的一部分之前再次指向堆栈指针。 所以编译器生成的ret
会尝试返回argv[0]
. 由于默认情况下堆栈是不可执行的,因此会出现段错误。 (而且它指向 ASCII 字符,这些字符可能不会被解码为有用的 x86-64 机器代码。)
您可以通过使用 GDB 单步执行 asm 来自己发现这一点。(layout reg
并使用stepi
aka si
)。
一般来说,你在编译器后面弄乱堆栈指针和其他寄存器通常只会让事情崩溃。如果在堆栈中存在更高的返回地址,那pop %rcx
将比leave
.
脚注1:
在进程的地址空间中甚至没有任何机器代码可以指向有用的返回地址来进行这样的系统调用,除非您将一些机器代码作为 arg 或环境变量注入。
你链接了-nostdlib
所以没有 libc 链接。如果您确实动态链接 libc 但仍然编写自己的_start
(例如 withgcc -nostartfiles
而不是 full -nostdlib
),则 ASLR 将意味着 libc_exit
函数位于某个运行时变量地址。
如果您静态链接 libc ( gcc -nostartfiles -static
),_exit()
除非您实际引用它,否则代码不会被复制到您的可执行文件中,而这段代码不会。但是您仍然需要以某种方式调用它;没有指向它的返回地址。