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)。
我在Stackoverflow answer 中写了关于这个主题的更多信息,它展示了一个静态链接的 MacOS 可执行文件如何遍历内核在进程创建时传递的程序参数。