OS X 上的 Hello World 原生程序的事实真相

581 查看

最近我意识到,对可执行文件如何与操作系统交互所知甚少。我写一些C代码,它被编译,汇编,并静态链接,然后一些魔法发生,我写的东西不知道怎么就被加载并运行了。这篇文章是关于魔法背后的一些玄机,特别是解剖 OS X 的 Mach-O ABI 机制。

我开始了这个探索的过程,通过写一个简单的”Hello World”,我推测它可能会生成一个易于解释的输出文件。当然,我可以通过大量阅读了解这一切,但那不好玩。我倒是更喜欢亲自去探索,看看它能带我到哪,要是被卡住就深入研究下。代码如下:

接下来在 2013 款 Macbook Yosemite 系统运行 gcc hello-world.c -o hello.out (如果我没有记错的话实际上是Clang在OS X上的伪装),完成后用十六进制编辑器打开结果并开始分析。说实话,我没想到仅两行C代码消耗了我这么多时间。我不想解释在这里详细地解释每个8548输出字节—-这将耗时颇靡,且读之乏味。相反,我会试图给出一个比较简短的概括。放轻松,如果你有着跟我类似的环境,一个人在家同你自个儿的二进制文件玩去吧。

生成文件的前四个字节是cf fa ed fe – 毫无疑问,某种标准文件头。谷歌一下发现,这确实是一个小端64位的Mach-O二进制文件的头文件。这是个好的开始!事实证明,Mach-O格式,是标准的OS X(和iOS)的程序和库文件存储格式 – 这儿甚至还有个官方参考文件,应该对你很有用。

那么有一个更好的想法,当然也是我们正要处理的,让我们后退到一秒前,来获取布局文件的概述。下面是生成的二进制(通过一个我写的应用程序生成)的Cortesi的风格的可视化。如果你不熟悉它,各个字节绘制在一个块填充曲线和并根据其权值染色,从而具有相似位置的字节显示在视觉上相关的区域,并染以相关的颜色。

从这我们可以看到,似乎是伴随着很多黑色块(零值字节)明显分开的区域。来看一下文件格式参考,苹果提供了以下图表来描述的Mach-O格式的布局:

了解格式的小知识之后,你就可以开始在上述两图之间画等号。该文件开头的数据是Mach-O的文件头和加载指令,然后我们有一些“段”里的数据(通常在段内的section),从0×00字节,一直填充段到页边界(在本例子是4096字节)。

特别是,上述的黄色区域是文件头,红色中包含的加载指令,以及绿色,蓝色,紫色区域是(部分的)段。让我们来谈谈这每一个的细节。

MACH-O文件头

Mach-O文件头相对易于理解—-它包括了文件的前32个字节,可以通过查看“mach_header_64” 结构逐字节理解 。otool工具自动为我们做好了,干得好。运行“otool -h hello.out ”揭示了文件头的所有重要信息:

苹果的开源代码(包括otool的代码)提供了内部的大量细节,整个过程中这对我很有用,除了总结输出:

  • 0xfeedfacf (从这个文件的小端表示重新排序了)是个64位文件头的魔数常量,(MH_MAGIC_64/MH_CIGAM_64 ,在loader.h)。
  • “cputype”是x86_64 CPU 类型值(CPU_TYPE_X86_64, 在machine.h)。
  • “cpusubtype”是所有x86_64 处理器的值(CPU_SUBTYPE_X86_64_ALL),附加64位库兼容需要的“capability bits”(CPU_SUBTYPE_LIB64 ,在machine.h)。
  • “filetype”是一种按需分页的可执行文件(loader.h里的MH_EXECUTE )。
  • “ncmds”和“sizeofcmds”域表明16个加载指令紧随其后,总大小1376字节。
  • “flags”域设置了一堆我们文件的标志:我们的文件无未定义引用 (MH_NOUNDEFS),是为动态链接器(MH_DYLDLINK),使用two-level名Cheng绑定(MH_TWOLEVEL)且应被加载到随机地址 (MH_PIE).

加载指令

根据文件格式参考,加载指令“指定虚拟内存中的文件逻辑结构和文件布局”。 他们在 Mach-O文件格式的中央,而我们的文件头说我们有16个指令紧随其后,让我们来看一下。

你可以再一次按照Mach-O文件格式参考逐字节阅读(这次看一下“load_command’”结构和它的密友们),但otool让事情变简单了。 运行“otool -l hello.out ”提供文件中所有加载指令的详细信息—- 我不想在本贴分析过一下细节,无论如何。Mach-O格式参考总结了一部分加载指令格式而非所有,所以我会自己来做个格式综览。

  •  LC_SEGMENT_64:定义一个(64位)段, 当文件加载后它将被映射到地址空间。包括段内节(section)的定义。
  • LC_SYMTAB:为该文件定义符号表(‘stabs’ 风格)和字符串表。 他们在链接文件时被链接器使用,同时也用于调试器映射符号到源文件。具体来说,符号表定义本地符号仅用于调试,而已定义和未定义external 符号被链接器使用。
  • LC_DYSYMTAB:提供符号表中给出符号的额外符号信息给动态链接器,以便其处理。 包括专门为此而设的一个间接符号表的定义。
  • LC_DYLD_INFO_ONLY:定义一个附加 压缩的动态链接器信息节,它包含在其他事项中用到的 动态绑定符号和操作码的元数据。stub 绑定器(“dyld_stub_binder”),它涉及的动态间接链接利用了这点。 “_ONLY” 后缀t表明这个加载指令是程序运行必须的,, 这样那些旧到不能理解这个加载指令的链接器就在这里停下。
  • LC_LOAD_DYLINKER: 加载一个动态链接器。在OS X上,通常是“/usr/lib/dyld”。LC_LOAD_DYLIB: 加载一个动态链接共享库。举例来说,“/usr/lib/libSystem.B.dylib”,这是C标准库的实现再加上一堆其他的事务(系统调用和内核服务,其他系统库等)。每个库由动态链接器加载并包含一个符号表,符号链接名称是查找匹配的符号地址。
  • LC_MAIN:指明程序的入口点。在本案例,是函数themain()的地址。
  • LC_UUID:提供一个唯一的随机UUID,通常由静态链接器生成。
  • LC_VERSION_MIN_MACOSX:程序可运行的最低OS X版本要求
  • LC_SOURCE_VERSION::构建二进制文件的源码版本号。
  • LC_FUNCTION_STARTS:定义一个函数起始地址表,使调试器和其他程序易于看到一个地址是否在函数内。
  • LC_DATA_IN_CODE:定义在代码段内的非指令的表。
  • LC_DYLIB_CODE_SIGN_DRS: 为已链接的动态库定义代码签名 指定要求

哇,这么快就理解了!我们甚至还没有看到这个可执行文件加载指令的更深处,只是看到加载指令的类型!如果你现在还没有得到所有的理论,不要担心。本质上,加载指令只是提供了一堆各种各样的信息或是有关文件空闲处的数据(定义/引用中发生的数据块),或者直接有关的可执行文件。这个信息大大巩固了我们文件空闲部分的全部内容。

节和段(SECTION & SEGMENTS)

更深入地来看加载指令,某一段/节结构通过“LC_SEGMENT_64”指令定义,且被许多其他加载指令引用。该文件的其余基本上是用有意义的数据填充这个结构。所有在我们文件中定义的段描述如下:

  •  __PAGEZERO:一个全用0填充的段,用于抓取空指针引用。这通常不会占用磁盘空间 (或内存空间),因为它运行时映射为一群0啊。顺便说一句,这个段是隐藏恶意代码的好地方。
  • __TEXT:本段只有可执行代码和其他只读数据。
  1.  __text:本段是可执行机器码。
  2. __stubs:间接符号存根。这些跳转到非延迟加载 (“随运行加载”) 和延迟加载(“初次使用时加载”) 间接引用的(可写)位置的值 (例如条目“__la_symbol_ptr”,我们很快就会看到。对于延迟加载引用,其地址跳转讲首先指向一个解析过程,但初始化解析后会指向一个确定的地址。 对于非延迟加载引用,其地址跳转会始终指向一个确定的地址,因为动态链接器在加载可执行文件时就修正好了。
  3. __stub_helper:提供助手来解决延迟加载符号。如上所述,延迟加载的间接符号指针将指到这里面,直到得到确定地址。
  4. __cstring: constant (只读) C风格字符串(如”Hello, world!n”)的节。链接器在生成最终产品时会清除重复语句。
  5. __unwind_info:一个紧凑格式,为了存储堆栈展开信息供处理异常。此节由链接器生成,通过“__eh_frame”里供OS X异常处理的信息。
  6. __eh_frame: 一个标准的节,用于异常处理,它提供堆栈展开信息,以DWARF格式。
  • __DATA:用于读取和写入数据的一个段。
  1. __nl_symbol_ptr:非延迟导入符号指针表。
  2. __la_symbol_ptr:延迟导入符号指针表。本节开始时,指针们指向解析助手,如前所讨述。
  3. __got:全局偏移表 — (非延迟)导入全局指针表。
  • __LINKEDIT:包含给链接器(链接编辑器的原始数据的段,在本案例中,包括符号和字符串表,压缩动态链接信息,代码签名存托凭证,以及间接符号表 – 所有这一切的占区都被加载指令指定了。

全局思考

随着对加载指令,段,节的知识以及所有他们的用途,这样应该不会太难去看出当二进制被运行时发生了什么的全局思考。我们已经讨论了许多发生在动态链接和运行二进制文件时候的过程。从本质上讲:

  1.  我们编译和静态链接产生的Mach-O输出成为动态链接器的输入,它使用在我们文件中被加载指令指明的数据,以各种方式链接依赖。
  2. 可执行文件的段被映射到内存中,按加载指令中指明的。
  3. 执行开始于“LC_MAIN”指定的点,在本案例是“__TEXT.__”文本开头。

进入稍微更深的细节(携此文档的帮助下),这里是专门为我们的“Hello World”二进制文件的整个过程的大致轮廓:

  1.  用户表明他们希望要运行这个二进制文件。
  2. 它确定该文件是一个有效的Mach-O文件,所以内核为程序(for)创建一个进程并开始程序执行过程(execve)。
  3. 内核检查的Mach-O文件头并加载程序,配合以指定的动态连接器(“/ usr / lib/ dyld”),进入加载指令指定分配的地址空间。段的虚拟内存保护标志也按指示添加上(例如__TEXT是只读)。
  4. 内核执行动态链接器,它加载所有引用的库 —- 本案例是“/usr/lib/libSystem.B.dylib” —- 并执行启动程序必须的符号绑定(即非延迟引用),搜索加载库以匹配符号。
  5. 假设符号已经被正确解析,动态连接器把结果地址置入section,即那些它在间接符号表(由“LC_DYSYMTAB”定义)中掌控主导(如它们的加载指令条目指定)的相应条目的部分。在本案例,确定地址被置于“_nl_symbol_ptr”和“__got”。
  6. 一些初始化代码执行设置运行时状态,在所指定的入口点“LC_MAIN”之后!
  7. 当一个延迟绑定的应用第一次使用(通过“__stubs”),“__la_symbol_ptr”条目应该指向一个在“__stub_helpers”里的解析例程(由于构建过程静态链接器的准备),它调用“dyld_stub_binder”(这是当我们的程序被加载时动态链接的),以执行解析和更新“__la_symbol_ptr”里的地址。

当然,如果你想进一步了解的话还有很多更具体的细节,如果你想要看。对于Mach-O的探索,我建议使用otoolMachOView以及一个合适量的开源代码,说明书和模糊的在线资源。有一堆的Mach-O部分和动态链接我们甚至还没有触及到。例如,弱绑定,是一个不同类型的符号绑定,它仅当链接符号是在系统上可用时才链接。

如果你有兴趣看“Hello World”程序的__TEXT.__文本的汇编代码,objdump使反编译工作更容易 —- 虽然,当然我们也可以从编译器第一手得到这样的输出:“gcc -S hello-world.c -o hello.s”。目前已经有一些资源,仔细检查了C“Hello World”的汇编,所以我没兴趣涵盖这一部分。无论如何,本文还是侧重于介绍现代系统二进制执行过程中常被遗忘的一些细节。