最近,我发现自己对可执行文件如何与操作系统交互知之甚少。我写了一些C代码,它被编译、汇编和静态链接,然后一些神奇的事情发生了,我写的东西以某种方式被加载和运行。这篇帖子是关于在某种程度上揭开这种魔力的神秘面纱-特别是剖析MacOS Mach-O ABI。
我从编写一个简化的Hello World程序开始这个探索过程,我认为它可能会产生一个易于解释的输出文件。当然,我可以通过大量阅读来了解所有这些东西,但这并不有趣。我更喜欢自己探索事物,看看它会把我带到哪里,当我陷入困境时做研究。这两行内容如下:
接下来,我在我2013年底运行优胜美地的Macbook上运行了GCC hello-world.c-o hello.out(如果我没记错的话,它实际上是在MacOS上变相地叮当作响),然后在十六进制编辑器中打开结果开始分析。老实说,我从来没有想到两行C代码会占用我这么多时间。我不想在这里详细解释8548个输出字节中的每一个-这会花费太长的时间,而且阅读起来不会很有趣。取而代之的是,我将尝试对我的发现做一个相对简短的概述。如果您的环境与我的环境相似,请随意在家里使用您自己的二进制代码。
生成的文件的前四个字节是cf fa ed fe-毫无疑问是某种标准文件头。运行`file hello.out`很快就会发现,这确实是一个小端64位Mach-O二进制文件的头文件。这是一个很好的开始!事实证明,Mach-O格式是在MacOS(和iOS)上存储程序和库的标准文件格式--甚至还有一个官方参考文档,在这里应该非常有用。
因此,对我们正在处理的内容有了更好的了解后,让我们退一步来了解一下该文件的布局。这里是产生的二进制文件的Cortesi风格的可视化(由我编写的Mac应用程序生成)。如果您不熟悉,每个字节都绘制在一条空间填充曲线上,并根据它的值着色,这样位置相似的字节显示在视觉上相关的区域,而值相似的字节显示相关的颜色。
从这里我们可以看到,似乎有几个明显分离的区域以及大量的黑色空间(值为0的字节)。看一看文件格式参考,苹果提供了以下图表来描述Mach-O格式的布局:
在对该格式知之甚少的情况下,您可以开始在上面的两个图之间画出相似之处。文件开头的数据是Mach-O文件头和LOAD命令,然后我们在0x00字节的海洋中的';段';(通常在段内的节中)中有一些数据,它们将段填充到页面边界(在本例中为4096字节)。
具体地说,上面的黄色区域是标题,红色区域包含加载命令,绿色、蓝色和紫色区域是段(的一部分)。让我们详细地讨论一下这些问题。
Mach-O头相对容易理解-它由文件的前32个字节组成,可以通过查看';MACH_HEADER_64';结构逐个字节地理解它。然而,otool实用程序在为我们自动化这方面做得很好。运行otool-h hello.out将显示有关文件标题的所有重要信息:
$otool-h hello.outhello.out:Mach头魔术cputype cpusubtype caps filetype ncmds sizeofcmds标志0xfeed facf 16777223 3 0x80 2 16 1376 0x00200085。
苹果的开源代码(包括otool的源代码)提供了关于所有这些东西的很多细节,在整个过程中对我很有用,但在这里总结一下输出:
0xfeed facf(从文件中的小端表示重新排序)是64位标头的';Magic';常量(loader.h中的MH_MAGIC_64/MH_CIGAM_64)。
cpusubtype';是所有x86_64处理器(CPU_SUBTYPE_X86_64_ALL)的值,加上需要与64位库兼容的';功能位(MACHIN.H中的CPU_SUBTYPE_LIB64)。
ncmds';和';sizeofcmds';字段指示紧随其后的16个加载命令,总大小为1376字节。
FLAGS';字段设置了关于我们的文件的一系列标志:我们的文件没有未定义的引用(MH_NOUNDEFS),用于动态链接器(MH_DYLDLINK),使用两级名称绑定(MH_TWOLEVEL),并且应该在随机地址(MH_PIE)加载。
根据文件格式参考,LOAD命令指定文件的逻辑结构和虚拟内存中的文件布局。它们是Mach-O文件格式的核心,我们的头上说我们马上就会有16个,所以让我们来看看它们。
同样,您可以根据文件格式参考读取字节(这次查看';LOAD_COMMAND';结构及其密友),但是otool使事情变得简单。运行otool-l hello.out提供了有关该文件中所有加载命令的详细信息-不过,我不会在本文中详细介绍所有这些细节。Mach-O格式参考提供了几个LOAD命令类型的摘要,但不是我们文件中的所有类型,因此我将自己提供这些类型的概述。
LC_SEGMENT_64:定义在加载文件时将映射到地址空间的(64位)段。包括段内包含的节的定义。
LC_SYMTAB:定义此文件的符号表(Style)和字符串表。链接器和调试器使用它们将某些符号(例如,在原始源文件中)映射到编译后的二进制文件的区域。特别地,符号表定义了仅用于调试的本地符号,以及定义的和未定义的外部符号。
LC_DYSYMTAB:向动态链接器提供有关符号表中存在的动态链接器应该处理的符号的附加符号信息。包括专门用于此目的的间接符号表的定义。
LC_DYLD_INFO_ONLY:定义一个附加的压缩动态链接器信息部分,其中包含用于动态绑定的符号和操作码的元数据等。处理动态间接链接的存根绑定器(';dyld_stub_binder';)利用这一点进行链接。名称的';_ONLY';扩展表示程序需要此LOAD命令才能运行,因此不理解此LOAD命令的较旧链接器应在此停止。
LC_LOAD_DYLIB:加载动态链接的共享库。例如";/usr/lib/libSystem.B.dylib";,它是C标准库加上一系列其他东西(syscall和内核服务、其他系统库等)的实现。每个库都由动态链接器加载,并包含一个将符号名称链接到地址的符号表,该符号表将搜索匹配的符号。
LC_MAIN:指定程序的入口点。在我们的示例中,这是main()函数的位置。
LC_Function_STARTS:定义函数起始地址表,以便调试器和其他程序轻松查看地址是否位于函数内。
哇,技术上的进展真的很快!我们在这里甚至没有深入研究我们的可执行文件中的LOAD命令,只研究了存在的LOAD命令的类型!如果你没有完全理解这里的理论,也不要担心。从本质上说,LOAD命令只是提供了一堆不同的信息,要么是关于文件其余部分的数据(定义/引用发生的数据块),要么是关于可执行文件的直接信息。这些信息在很大程度上支撑了我们其余文件的全部内容。
更详细地看加载命令,某个段/段结构是通过';LC_SEGMENT_64';命令定义的,并被许多其他加载命令引用。文件的其余部分基本上用有意义的数据填充此结构。我们的文件中定义的所有段和节如下所示:
__PAGEZERO:一个通常充满零的段,用于捕获空指针取消引用。这通常不会占用磁盘(或RAM)上的空间,因为它在运行时映射为零。另外,此段可能是隐藏恶意代码的好地方。
__Text:可执行代码和其他只读数据的段。__存根:间接符号存根。对于非惰性(";使用可执行文件";加载)和惰性(";首次使用";时加载)间接引用,这些跳转到(可写)位置的值(例如,我们将很快看到';__la_symbol_ptr'中的条目)。对于惰性引用,跳转到的地址将首先指向解析过程,但在初始解析之后将指向已解析的地址。对于非惰性引用,跳转到的地址将始终指向已解析的地址,因为动态链接器将在加载可执行文件时修复该地址。
__stub_helper:提供帮助程序来解析延迟加载的符号。如上所述,在解析之前,延迟加载的间接符号指针将在这里指向内部。
__cstring:用于常量(只读)C样式字符串的部分(如";Hello,world!\n\0";)。链接器在生成最终产品时删除重复项。
__UNWIND_INFO:用于存储堆栈展开信息的紧凑格式,用于异常处理。此部分由链接器根据';__eh_frame';中的信息生成,用于MacOS上的异常处理。
__EH_FRAME:用于异常处理的标准部分,它以DWARF调试数据格式提供堆栈展开信息(有时还提供额外的调试信息)。
__data:可读写数据段。__la_symbol_ptr:指向惰性导入符号的指针表。本节从指向解析帮助器的指针开始,如前所述。
__LINKEDIT:包含链接器原始数据的片段(';链接编辑器';),本例中包括符号表和字符串表(其内容可以通过`nm`显示)、压缩的动态链接信息、代码签名DRS和间接符号表-所有这些都占用了Load命令指定的区域。
有了LOAD命令、段和节及其所有用途的知识,应该不会太难看到运行二进制文件时发生的事情的总体情况。我们已经讨论了动态链接和运行二进制文件时发生的许多过程。本质上:来自构建和静态链接的Mach-O输出成为动态链接器的输入,动态链接器使用由LOAD命令指定的文件中的数据以各种方式链接依赖项。
执行从';LC_Main';指定的点开始,在本例中是__TEXT.__TEXT的开始。
为了更详细地说明(在本文的帮助下),下面是专门针对我们的Hello World二进制文件的整个过程的粗略概述:
确定该文件是有效的Mach-O文件,因此内核为该程序创建一个进程(Fork)并开始程序执行进程(Execve)。
内核检查Mach-O头,并将程序与指定的动态链接器(";/usr/lib/dyld";)一起加载到LOAD命令中指定的某个分配的地址空间。段虚拟内存保护标志也按规定应用(例如,__text为只读)。
内核执行动态链接器,该动态链接器加载任何引用库-在本例中为";/usr/lib/libSystem.B.dylib";,并执行启动程序所需的符号绑定(即非惰性引用),在加载的库中搜索匹配符号。
假设符号已被正确解析,动态链接器将结果地址放入对间接符号表(由';LC_DYSYMTAB';定义)中的相应条目拥有所有权(在其加载命令条目中指定)的部分中。(#39;LC_DYSYMTAB';由';LC_DYSYMTAB';定义)。在这种情况下,解析的地址放入';__NL_SYMBOL_PTR';和';__GET';中。
执行一些初始化代码来设置运行时状态,之后调用LC_MAIN指定的入口点!
当第一次使用惰性绑定引用时(通过';__stubs';),';__la_symbol_ptr';条目应该指向';__stub_helpers';中的解析例程(由于构建过程中静态链接器的准备),该例程调用';dyld_stub_binder';(在加载程序时动态链接该例程)来执行。
当然,如果您想研究一下,这个过程中还有更多具体的细节。对于Mach-O探索,我建议使用otool和MachOView,以及大量开放源代码、规范和晦涩难懂的在线资源。Mach-O和动态链接还有很多我们都没有触及的部分。例如,弱绑定是一种不同类型的符号绑定,仅当符号在系统上可用时才链接它们。
如果您有兴趣查看Hello World程序的__text.__text中的汇编代码,则objdump可以简化反汇编工作-当然,我们也可以从编译器获得此输出的第一手信息:GCC-S hello-world.c-o hello.s。已经有很多资源介绍了CHello World程序集的细节,所以我不想在这里介绍这部分内容。然而,我们在这里介绍的是现代系统中似乎经常被遗忘的二进制执行层的一些细节。