我可以在 MacOS 的 _start 处的代码中执行 `ret` 指令吗? Linux?
Posted
技术标签:
【中文标题】我可以在 MacOS 的 _start 处的代码中执行 `ret` 指令吗? Linux?【英文标题】:Can I do `ret` instruction from code at _start in MacOS? Linux? 【发布时间】:2018-05-27 20:21:06 【问题描述】:我想知道从程序的入口点返回ret
是否合法。
NASM 示例:
section .text
global _start
_start:
ret
; Linux: nasm -f elf64 foo.asm -o foo.o && ld foo.o
; OS X: nasm -f macho64 foo.asm -o foo.o && ld foo.o -lc -macosx_version_min 10.12.0 -e _start -o foo
ret
从堆栈中弹出一个返回地址并跳转到它。
但是堆栈的顶部字节是程序入口点的有效返回地址,还是我必须调用 exit?
另外,上面的程序在 OS X 上没有段错误。它返回到哪里?
【问题讨论】:
不,你不能。您需要执行退出系统调用,因为在这种情况下堆栈上没有返回地址。如果您链接到 C 运行时库并将您的函数更改为main
,那么它将起作用,因为 ret
将返回到 C 运行时,而后者最终会执行自己的退出系统调用。
@MichaelPetch 您可以发布我会接受的答案。如果地址无效,你知道为什么它不会在 OS X 上崩溃吗?
这取决于您的操作系统。在 DOS 下是合法的,在 UNIX 上一般不合法。
@fuz :这取决于。 DOS EXE 你将无法执行ret
,但 COM 程序可以,因为 DOS COM 程序有一个 16 位字放置在堆栈上的 0xfffe 处,值为 0x0000。当您执行 ret
时,它会返回当前段中的 0x0000。 0x0000(在 PSP 中)包含一个 int 20h
指令,该指令反过来终止 COM 程序。 EXE 没有在堆栈上传递任何返回地址,因此ret
可能会挂起/崩溃 DOS。
【参考方案1】:
MacOS 动态可执行文件
当您使用 MacOS 并链接到:
ld foo.o -lc -macosx_version_min 10.12.0 -e _start -o foo
您将获得动态加载的代码版本。 _start
不是真正的入口点,动态加载器才是。动态加载器作为其最后一步之一执行 C/C++/Objective-C 运行时初始化,然后调用您指定的使用 -e
选项指定的入口点。有关Forking and Executing the Process 的Apple 文档有以下段落:
一个 Mach-O 可执行文件包含一个由一组加载命令组成的标题。对于使用共享库或框架的程序,这些命令之一指定用于加载程序的链接器的位置。如果你使用 Xcode,它总是 /usr/lib/dyld,标准的 OS X 动态链接器。
当您调用 execve 例程时,内核首先加载指定的程序文件并检查文件开头的 mach_header 结构。内核验证该文件似乎是一个有效的 Mach-O 文件并解释存储在头文件中的加载命令。然后内核将加载命令指定的动态链接器加载到内存中,并在程序文件上执行动态链接器。
动态链接器加载主程序链接到的所有共享库(依赖库)并绑定足够的符号以启动程序。 然后调用入口点函数。在构建时,静态链接器将标准入口点函数添加到主可执行文件来自目标文件/usr/lib/crt1.o。此函数为内核设置运行时环境状态并为 C++ 对象调用静态初始化器,初始化 Objective-C 运行时,然后然后调用程序的主函数
在你的情况下是_start
。在创建动态链接可执行文件的环境中,您可以执行ret
并让它返回到调用_start
的代码,该代码为您执行退出系统调用。这就是它不会崩溃的原因。如果您使用gobjdump -Dx foo
查看生成的目标文件,您应该得到:
start address 0x0000000000000000
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000001 0000000000001fff 0000000000001fff 00000fff 2**0
CONTENTS, ALLOC, LOAD, CODE
SYMBOL TABLE:
0000000000001000 g 03 ABS 01 0010 __mh_execute_header
0000000000001fff g 0f SECT 01 0000 [.text] _start
0000000000000000 g 01 UND 00 0100 dyld_stub_binder
Disassembly of section .text:
0000000000001fff <_start>:
1fff: c3 retq
注意start address
为 0。0 处的代码为dyld_stub_binder
。这是一个动态加载程序存根,它最终建立一个 C 运行时环境,然后调用您的入口点_start
。如果您不覆盖入口点,则默认为main
。
MacOS 静态可执行文件
但是,如果您构建为 static 可执行文件,则在您的入口点之前不会执行任何代码,ret
应该会崩溃,因为堆栈上没有有效的返回地址。在上面引用的文档中是这样的:
对于使用共享库或框架的程序,这些命令之一指定用于加载程序的链接器的位置。
静态构建的可执行文件不使用嵌入了crt1.o
的动态加载程序dyld
。 CRT = C 运行时库,涵盖 C++/Objective-C 以及 MacOS。处理动态加载的过程没有完成,C/C++/Objective-C初始化代码没有执行,控制权直接转移到你的入口点。
要静态构建,请从链接器命令中删除-lc
(或-lSystem
)并添加-static
选项:
ld foo.o -macosx_version_min 10.12.0 -e _start -o foo -static
如果您运行此版本,它应该会产生分段错误。 gobjdump -Dx foo
产生
start address 0x0000000000001fff
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000001 0000000000001fff 0000000000001fff 00000fff 2**0
CONTENTS, ALLOC, LOAD, CODE
1 LC_THREAD.x86_THREAD_STATE64.0 000000a8 0000000000000000 0000000000000000 00000198 2**0
CONTENTS
SYMBOL TABLE:
0000000000001000 g 03 ABS 01 0010 __mh_execute_header
0000000000001fff g 0f SECT 01 0000 [.text] _start
Disassembly of section .text:
0000000000001fff <_start>:
1fff: c3 retq
您应该注意到start_address
现在是 0x1fff。 0x1fff 是您指定的入口点 (_start
)。没有作为中介的动态加载程序存根。
Linux
在 Linux 下,当您指定自己的入口点时,无论您是构建为静态可执行文件还是共享可执行文件,都会出现分段错误。在 article 和 dynamic linker documentation 中有关于如何在 Linux 上运行 ELF 可执行文件的好信息。应该注意的关键点是,Linux 没有提到进行 C/C++/Objective-C 运行时初始化,这与 MacOS 动态链接器文档不同。
Linux 动态加载器 (ld.so) 和 MacOS 动态加载器 (dynld) 之间的主要区别在于,MacOS 动态加载器通过包含条目来执行 C/C++/Objective-C 启动初始化来自crt1.o
。然后crt1.o
中的代码将控制权转移到您使用-e
指定的入口点(默认为main
)。在 Linux 中,动态加载器不对将要运行的代码类型做出任何假设。在共享对象被处理和初始化后,控制被直接转移到入口点。
进程创建时的堆栈布局
FreeBSD(MacOS 所基于)和 Linux 有一个共同点。加载 64 位可执行文件时,创建进程时用户堆栈的布局是相同的。 32 位进程的堆栈类似,但指针和数据是 4 字节宽,而不是 8 字节。
虽然堆栈上没有返回地址,但还有其他数据表示参数的数量、参数、环境变量和其他信息。此布局不与 C/C++ 中的 main
函数所期望的相同。它是 C 启动代码的一部分,用于将进程创建时的堆栈转换为与 C 调用约定和函数main
(argc
) 的期望兼容的东西, argv
, envp
)。
我在*** answer 中写了关于这个主题的更多信息,它展示了一个静态链接的 MacOS 可执行文件如何遍历内核在进程创建时传递的程序参数。
【讨论】:
不错的答案。您是否有一些链接可以解释这一点以及如何在 OS X / Linux 中使用静态和动态可执行文件调用入口点? @Bilow 我添加了特定于 Apple 的有关加载过程(静态和动态)的信息。我添加了指向 Apple 文档的链接和来自这些文档的关于该过程的 sn-p。我还对 Linux 上的动态链接器进行了扩展和澄清,包括一些外部文档和信息。【参考方案2】:补充 Michael Petch 已经回答的内容:
从可运行的 Mach-o 可执行的角度来看,程序的启动是由于加载命令 LC_MAIN
(自 10.7 以来的大多数现代可执行文件)在进程中使用 DYLD
或向后兼容的加载命令 LC_UNIXTHREAD
而发生的。前者是允许您的ret
的变体,实际上更可取,因为您将控制权返回给 DYLD __mh_execute_header。随后将进行缓冲区刷新。
除了ret
,您可以通过未记录的syscall
内核API(64 位,int 0x80
用于32 位)或 DYLD 包装器 C 库执行它(记录)来使用系统退出调用。
如果您的可执行文件没有使用LC_MAIN
,那么您将留下旧版LC_UNIXTHREAD
,在此您别无选择系统退出调用,ret
将导致segmentation fault
。
【讨论】:
系统调用接口已记录在案。即使对于 64 位也是如此。syscalls.master
维护调用和参数的列表。如果您阅读 FreeBSD 文档(MacOS 所基于),则更容易找到 32 位接口文档。在 int 0x80
之前分配了 4 个虚拟字节的参数压入堆栈。 64 位系统调用调用约定使用 64 位 System V ABI 内核调用约定(与 Linux 相同)。 32位和64位系统调用号的区别在于,64位系统调用号必须加0x2000000。
如果我没记错的话,MacOS 64 位系统调用使用r10
表示4th arg
与常规System V ABI 下的rcx
。
当我说 64 位 System V ABI 时,您可能错过了 kernel
调用约定这个词。您将在 64-bit System V ABI 中找到 syscall 的寄存器调用约定(包括 RCX 和 R11 被破坏的事实)。第 A.2.1 节规定 2。系统调用是通过 syscall 指令完成的。内核销毁寄存器 %rcx 和 %r11。 和 内核接口使用 %rdi、%rsi、%rdx、%r10、%r8 和 %r9。
酷,这解释了差异。感谢您的澄清。
没问题。我赞成你的回答。我只是在观察。以上是关于我可以在 MacOS 的 _start 处的代码中执行 `ret` 指令吗? Linux?的主要内容,如果未能解决你的问题,请参考以下文章
MacOS下搭建开发环境React Native 目标平台ios
/writeReview/1 __str__ 处的 Django 模型 TypeError 返回非字符串(int 类型)