我可以在 MacOS 的 _start 处从代码执行 `ret` 指令吗? Linux?

Can I do `ret` instruction from code at _start in MacOS? Linux?

我想知道从程序入口点使用 ret return 是否合法。

示例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 从堆栈中弹出一个 return 地址并跳转到它。

但是堆栈的顶部字节是程序入口点的有效 return 地址,还是我必须调用 exit?

此外,上面的程序不会在 OS X 上出现段错误。它 return 到哪里去了?

MacOS 动态可执行文件

当您使用 MacOS 和 link 时:

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 文档有以下段落:

A Mach-O executable file contains a header consisting of a set of load commands. For programs that use shared libraries or frameworks, one of these commands specifies the location of the linker to be used to load the program. If you use Xcode, this is always /usr/lib/dyld, the standard OS X dynamic linker.

When you call the execve routine, the kernel first loads the specified program file and examines the mach_header structure at the start of the file. The kernel verifies that the file appear to be a valid Mach-O file and interprets the load commands stored in the header. The kernel then loads the dynamic linker specified by the load commands into memory and executes the dynamic linker on the program file.

The dynamic linker loads all the shared libraries that the main program links against (the dependent libraries) and binds enough of the symbols to start the program. It then calls the entry point function. At build time, the static linker adds the standard entry point function to the main executable file from the object file /usr/lib/crt1.o. This function sets up the runtime environment state for the kernel and calls static initializers for C++ objects, initializes the Objective-C runtime, and then calls the program’s main function

你的情况是 _start。在您创建动态 linked 可执行文件的环境中,您可以执行 ret 并将其 return 返回调用 _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 应该会崩溃,因为没有有效的 return 堆栈上的地址。在上面引用的文档中是这样的:

For programs that use shared libraries or frameworks, one of these commands specifies the location of the linker to be used to load the program.

静态构建的可执行文件不使用嵌入了 crt1.o 的动态加载程序 dyldCRT = C 运行时间库涵盖 C++/Objective-C 以及 MacOS。处理动态加载的过程没有完成,C/C++/Objective-C初始化代码没有执行,控制权直接转移到你的入口点。

要静态构建,请从 linker 命令中删除 -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 and the dynamic linker documentation 中有关于 ELF 可执行文件如何在 Linux 上 运行 的很好的信息。应该注意的关键点是 Linux 没有提到做 C/C++/Objective-C 运行time初始化不同于 MacOS 动态 linker 文档。

Linux 动态加载器 (ld.so) 和 MacOS 动态加载器 (dynld) 之间的主要区别是 MacOS 动态加载器执行 C/C++ /Objective-C 通过包含来自 crt1.o 的入口点来启动初始化。 crt1.o 中的代码然后将控制转移到您使用 -e 指定的入口点(默认为 main)。在 Linux 中,动态加载器不假设代码的类型将是 运行。在共享对象被处理和初始化后,控制权直接转移到入口点。


进程创建时的堆栈布局

FreeBSD(MacOS 的基础)和 Linux 有一个共同点。加载 64 位可执行文件时,创建进程时用户堆栈的布局是相同的。 32 位进程的堆栈类似,但指针和数据的宽度为 4 个字节,而不是 8 个字节。

虽然栈上没有return地址,但是还有其他数据表示参数个数、参数、环境变量等信息。此布局C/C++中的main函数所期望的相同。它是 C 启动代码的一部分,用于将进程创建时的堆栈转换为与 C 调用约定和函数期望兼容的东西 main (argc, argv, envp).

我在这个 中写了更多关于这个主题的信息,显示了 静态 linked MacOS 可执行文件如何遍历通过进程创建时的内核。

补充 Michael Petch 已经回答的内容: 从可运行的 Mach-o 可执行文件的角度来看,程序启动是由于在进程中使用 DYLD 的加载命令 LC_MAIN(自 10.7 以来的大多数现代可执行文件)或向后兼容的加载命令 LC_UNIXTHREAD。前者是允许 ret 的变体,实际上更可取,因为你 return 控制 DYLD __mh_execute_header。这之后将进行缓冲区刷新。 作为 ret 的替代方案,您可以通过未记录的 syscall 内核 API(64 位,int 0x80 用于 32 位)或 DYLD 包装器 C lib(已记录)使用系统退出调用。 如果您的可执行文件未使用 LC_MAIN,您将留下遗留的 LC_UNIXTHREAD,您无法替代系统退出调用,ret 将导致 segmentation fault.