我可以在 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 的动态加载程序dyldCRT = 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 类型)

如何从 MacOS 中删除 Java Web Start

WKWebview 在 macOS 上的默认浏览器中打开目标 =“_blank”链接

使用 Qt 获取当前的 macOS shell

在 indexPath 处的行的 Tableview 高度崩溃