Mach-O 可执行文件

咱们用 Xcode 构建一个程序的过程当中,会把源文件 (.m.h) 文件转换为一个可执行文件。这个可执行文件中包含的字节码会将被 CPU (iOS 设备中的 ARM 处理器或 Mac 上的 Intel 处理器) 执行。html

本文将介绍一下上面的过程当中编译器都作了些什么,同时深刻看看可执行文件内部是怎样的。实际上里面的东西要比咱们第一眼看到的多得多。xcode

这里咱们把 Xcode 放一边,将使用命令行工具 (command-line tools)。当咱们用 Xcode 构建一个程序时,Xcode 只是简单的调用了一系列的工具而已。Florian 对工具调用是如何工做的作了更详细的讨论。本文咱们就直接调用这些工具,并看看它们都作了些什么。缓存

真心但愿本文能帮助你更好的理解 iOS 或 OS X 中的一个可执行文件 (也叫作 Mach-O executable) 是如何执行,以及怎样组装起来的。架构

xcrun

先来看一些基础性的东西:这里会大量使用一个名为 xcrun 的命令行工具。看起来可能会有点奇怪,不过它很是的出色。这个小工具用来调用别的一些工具。原先,咱们在终端执行以下命令:app

% clang -v
复制代码

如今咱们用下面的命令代替:编辑器

% xcrun clang -v
复制代码

在这里 xcrun 作的是定位到 clang,并执行它,附带输入 clang 后面的参数。函数

咱们为何要这样作呢?看起来没有什么意义。不过 xcode 容许咱们: (1) 使用多个版本的 Xcode,以及使用某个特定 Xcode 版本中的工具。(2) 针对某个特定的 SDK (software development kit) 使用不一样的工具。若是你有 Xcode 4.5 和 Xcode 5,经过 xcode-selectxcrun 能够选择使用 Xcode 5 中 iOS SDK 的工具,或者 Xcode 4.5 中的 OS X 工具。在许多其它平台中,这是不可能作到的。查阅 xcrunxcode-select 的主页内容能够了解到详细内容。不用安装 Command Line Tools,就能使用命令行中的开发者工具。工具

不使用 IDE 的 Hello World

回到终端 (Terminal),建立一个包含一个 C 文件的文件夹:布局

% mkdir ~/Desktop/objcio-command-line
% cd !$
% touch helloworld.c
复制代码

接着使用你喜欢的文本编辑器来编辑这个文件 -- 例如 TextEdit.app:性能

% open -e helloworld.c
复制代码

输入以下代码:

#include <stdio.h>
int main(int argc, char *argv[])
{
    printf("Hello World!\n");
    return 0;
}
复制代码

保存并返回到终端,而后运行以下命令:

% xcrun clang helloworld.c
% ./a.out
复制代码

如今你可以在终端上看到熟悉的 Hello World!。这里咱们编译并运行 C 程序,全程没有使用 IDE。深呼吸一下,高兴高兴。

上面咱们到底作了些什么呢?咱们将 helloworld.c 编译为一个名为 a.out 的 Mach-O 二进制文件。注意,若是咱们没有指定名字,那么编译器会默认的将其指定为 a.out。

这个二进制文件是如何生成的呢?实际上有许多内容须要观察和理解。咱们先看看编译器吧。

Hello World 和编译器

时下 Xcode 中编译器默认选择使用 clang(读做 /klæŋ/)。关于编译器,Chris 写了更详细的文章。

简单的说,编译器处理过程当中,将 helloworld.c 当作输入文件,并生成一个可执行文件 a.out。这个过程有多个步骤/阶段。咱们须要作的就是正确的执行它们。

预处理
  • 符号化 (Tokenization)
  • 宏定义的展开
  • #include 的展开
语法和语义分析
  • 将符号化后的内容转化为一棵解析树 (parse tree)
  • 解析树作语义分析
  • 输出一棵抽象语法树(Abstract Syntax Tree* (AST))
生成代码和优化
  • 将 AST 转换为更低级的中间码 (LLVM IR)
  • 对生成的中间码作优化
  • 生成特定目标代码
  • 输出汇编代码
汇编器
  • 将汇编代码转换为目标对象文件。
连接器
  • 将多个目标对象文件合并为一个可执行文件 (或者一个动态库)

咱们来看一个关于这些步骤的简单的例子。

预处理

编译过程当中,编译器首先要作的事情就是对文件作处理。预处理结束以后,若是咱们中止编译过程,那么咱们可让编译器显示出预处理的一些内容:

% xcrun clang -E helloworld.c
复制代码

喔喔。 上面的命令输出的内容有 413 行。咱们用编辑器打开这些内容,看看到底发生了什么:

% xcrun clang -E helloworld.c | open -f
复制代码

在顶部能够看到的许多行语句都是以 # 开头 (读做 hash)。这些被称为 行标记 的语句告诉咱们后面跟着的内容来自哪里。若是再回头看看 helloworld.c 文件,会发现第一行是:

#include <stdio.h>
复制代码

咱们都用过 #includeimport。它们所作的事情是告诉预处理器将文件 stdio.h 中的内容插入到 #include 语句所在的位置。这是一个递归的过程:stdio.h 可能会包含其它的文件。

因为这样的递归插入过程不少,因此咱们须要确保记住相关行号信息。为了确保无误,预处理器在发生变动的地方插入以 # 开头的 行标记。跟在 # 后面的数字是在源文件中的行号,而最后的数字是在新文件中的行号。回到刚才打开的文件,紧跟着的是系统头文件,或者是被看作为封装了 extern "C" 代码块的文件。

若是滚动到文件末尾,能够看到咱们的 helloworld.c 代码:

# 2 "helloworld.c" 2
int main(int argc, char *argv[])
{
 printf("Hello World!\n");
 return 0;
}
复制代码

在 Xcode 中,能够经过这样的方式查看任意文件的预处理结果:Product -> Perform Action -> Preprocess。注意,编辑器加载预处理后的文件须要花费一些时间 -- 接近 100,000 行代码。

编译

下一步:分析和代码生成。咱们能够用下面的命令让 clang 输出汇编代码:

% xcrun clang -S -o - helloworld.c | open -f
复制代码

咱们来看看输出的结果。首先会看到有一些以点 . 开头的行。这些就是汇编指令。其它的则是实际的 x86_64 汇编代码。最后是一些标记 (label),与 C 语言中的相似。

咱们先看看前三行:

.section    __TEXT,__text,regular,pure_instructions
    .globl  _main
    .align  4, 0x90
复制代码

这三行是汇编指令,不是汇编代码。.section 指令指定接下来会执行哪个段。

第二行的 .globl 指令说明 _main 是一个外部符号。这就是咱们的 main() 函数。这个函数对于二进制文件外部来讲是可见的,由于系统要调用它来运行可执行文件。

.align 指令指出了后面代码的对齐方式。在咱们的代码中,后面的代码会按照 16(2^4) 字节对齐,若是须要的话,用 0x90 补齐。

接下来是 main 函数的头部:

_main:                                  ## @main
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp2:
    .cfi_def_cfa_offset 16
Ltmp3:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp4:
    .cfi_def_cfa_register %rbp
    subq    $32, %rsp
复制代码

上面的代码中有一些与 C 标记工做机制同样的一些标记。它们是某些特定部分的汇编代码的符号连接。首先是 _main 函数真正开始的地址。这个符号会被 export。二进制文件会有这个位置的一个引用。

.cfi_startproc 指令一般用于函数的开始处。CFI 是调用帧信息 (Call Frame Information) 的缩写。这个调用 以松散的方式对应着一个函数。当开发者使用 debugger 和 step instep out 时,其实是 stepping in/out 一个调用帧。在 C 代码中,函数有本身的调用帧,固然,别的一些东西也会有相似的调用帧。.cfi_startproc 指令给了函数一个 .eh_frame 入口,这个入口包含了一些调用栈的信息(抛出异常时也是用其来展开调用帧堆栈的)。这个指令也会发送一些和具体平台相关的指令给 CFI。它与后面的 .cfi_endproc 相匹配,以此标记出 main() 函数结束的地方。

接着是另一个 label ## BB#0:。而后,终于,看到第一句汇编代码:pushq %rbp。从这里开始事情开始变得有趣。在 OS X上,咱们会有 X86_64 的代码,对于这种架构,有一个东西叫作 ABI ( 应用二进制接口 application binary interface),ABI 指定了函数调用是如何在汇编代码层面上工做的。在函数调用期间,ABI 会让 rbp 寄存器 (基础指针寄存器 base pointer register) 被保护起来。当函数调用返回时,确保 rbp 寄存器的值跟以前同样,这是属于 main 函数的职责。pushq %rbprbp 的值 push 到栈中,以便咱们之后将其 pop 出来。

接下来是两个 CFI 指令:.cfi_def_cfa_offset 16.cfi_offset %rbp, -16。这将会输出一些关于生成调用堆栈展开和调试的信息。咱们改变了堆栈和基础指针,而这两个指令能够告诉编译器它们都在哪儿,或者更确切的,它们能够确保以后调试器要使用这些信息时,能找到对应的东西。

接下来,movq %rsp, %rbp 将把局部变量放置到栈上。subq $32, %rsp 将栈指针移动 32 个字节,也就是函数会调用的位置。咱们先将老的栈指针存储到 rbp 中,而后将此做为咱们局部变量的基址,接着咱们更新堆栈指针到咱们将会使用的位置。

以后,咱们调用了 printf()

leaq    L_.str(%rip), %rax
movl    $0, -4(%rbp)
movl    %edi, -8(%rbp)
movq    %rsi, -16(%rbp)
movq    %rax, %rdi
movb    $0, %al
callq   _printf
复制代码

首先,leaq 会将 L_.str 的指针加载到 rax 寄存器中。留意 L_.str 标记在后面的汇编代码中是如何定义的。它就是 C 字符串"Hello World!\n"edirsi 寄存器保存了函数的第一个和第二个参数。因为咱们会调用别的函数,因此首先须要将它们的当前值保存起来。这就是为何咱们使用刚刚存储的 rbp 偏移32个字节的缘由。第一个 32 字节的值是 0,以后的 32 字节的值是 edi 寄存器的值 (存储了 argc)。而后是 64 字节 的值:rsi 寄存器的值 (存储了 argv)。咱们在后面并无使用这些值,可是编译器在没有通过优化处理的时候,它们仍是会被存下来。

如今咱们把第一个函数 printf() 的参数 rax 设置给第一个函数参数寄存器 edi 中。printf() 是一个可变参数的函数。ABI 调用约定指定,将会把使用来存储参数的寄存器数量存储在寄存器 al 中。在这里是 0。最后 callq 调用了 printf() 函数。

movl    $0, %ecx
    movl    %eax, -20(%rbp)         ## 4-byte Spill
    movl    %ecx, %eax
复制代码

上面的代码将 ecx 寄存器设置为 0,并把 eax 寄存器的值保存至栈中,而后将 ect 中的 0 拷贝至 eax 中。ABI 规定 eax 将用来保存一个函数的返回值,或者此处 main() 函数的返回值 0:

addq    $32, %rsp
    popq    %rbp
    ret
    .cfi_endproc
复制代码

函数执行完成后,将恢复堆栈指针 —— 利用上面的指令 subq $32, %rsp 把堆栈指针 rsp 上移 32 字节。最后,把以前存储至 rbp 中的值从栈中弹出来,而后调用 ret 返回调用者, ret 会读取出栈的返回地址。 .cfi_endproc 平衡了 .cfi_startproc 指令。

接下来是输出字符串 "Hello World!\n":

.section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz   "Hello World!\n"
复制代码

一样,.section 指令指出下面将要进入的段。L_.str 标记运行在实际的代码中获取到字符串的一个指针。.asciz 指令告诉编译器输出一个以 ‘\0’ (null) 结尾的字符串。

__TEXT __cstring 开启了一个新的段。这个段中包含了 C 字符串:

L_.str:                                 ## @.str
    .asciz     "Hello World!\n"
复制代码

上面两行代码建立了一个 null 结尾的字符串。注意 L_.str 是如何命名,以后会经过它来访问字符串。

最后的 .subsections_via_symbols 指令是静态连接编辑器使用的。

更过关于汇编指令的资料能够在 苹果的 OS X Assembler Reference 中看到。AMD 64 网站有关于 ABI for x86 的文档。另外还有 Gentle Introduction to x86-64 Assembly

重申一下,经过下面的选择操做,咱们能够用 Xcode 查看任意文件的汇编输出结果:Product -> Perform Action -> Assemble.

汇编器

汇编器将可读的汇编代码转换为机器代码。它会建立一个目标对象文件,通常简称为 对象文件。这些文件以 .o 结尾。若是用 Xcode 构建应用程序,能够在工程的 derived data 目录中,Objects-normal 文件夹下找到这些文件。

连接器

稍后咱们会对连接器作更详细的介绍。这里简单介绍一下:连接器解决了目标文件和库之间的连接。什么意思呢?还记得下面的语句吗:

callq   _printf
复制代码

printf()libc 库中的一个函数。不管怎样,最后的可执行文件须要能须要知道 printf() 在内存中的具体位置:例如,_printf 的地址符号是什么。连接器会读取全部的目标文件 (此处只有一个) 和库 (此处是 libc),并解决全部未知符号 (此处是 _printf) 的问题。而后将它们编码进最后的可执行文件中 (能够在 libc 中找到符号 _printf),接着连接器会输出能够运行的执行文件:a.out

Section

就像咱们上面提到的同样,这里有些东西叫作 section。一个可执行文件包含多个段,也就是多个 section。可执行文件不一样的部分将加载进不一样的 section,而且每一个 section 会转换进某个 segment 里。这个概念对于全部的可执行文件都是成立的。

咱们来看看 a.out 二进制中的 section。咱们可使用 size 工具来观察:

% xcrun size -x -l -m a.out 
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
    Section __text: 0x37 (addr 0x100000f30 offset 3888)
    Section __stubs: 0x6 (addr 0x100000f68 offset 3944)
    Section __stub_helper: 0x1a (addr 0x100000f70 offset 3952)
    Section __cstring: 0xe (addr 0x100000f8a offset 3978)
    Section __unwind_info: 0x48 (addr 0x100000f98 offset 3992)
    Section __eh_frame: 0x18 (addr 0x100000fe0 offset 4064)
    total 0xc5
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
    Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
    Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
    total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000
复制代码

如上代码所示,咱们的 a.out 文件有 4 个 segment。有些 segment 中有多个 section。

当运行一个可执行文件时,虚拟内存 (VM - virtual memory) 系统将 segment 映射到进程的地址空间上。映射彻底不一样于咱们通常的认识,若是你对虚拟内存系统不熟悉,能够简单的想象虚拟内存系统将整个可执行文件加载进内存 -- 虽然在实际上不是这样的。VM 使用了一些技巧来避免所有加载。

当虚拟内存系统进行映射时,segment 和 section 会以不一样的参数和权限被映射。

上面的代码中,__TEXT segment 包含了被执行的代码。它被以只读和可执行的方式映射。进程被容许执行这些代码,可是不能修改。这些代码也不能对本身作出修改,所以这些被映射的页历来不会被改变。

__DATA segment 以可读写和不可执行的方式映射。它包含了将会被更改的数据。

第一个 segment 是 __PAGEZERO。它的大小为 4GB。这 4GB 并非文件的真实大小,可是规定了进程地址空间的前 4GB 被映射为 不可执行、不可写和不可读。这就是为何当读写一个 NULL 指针或更小的值时会获得一个 EXC_BAD_ACCESS 错误。这是操做系统在尝试防止引发系统崩溃

在 segment中,通常都会有多个 section。它们包含了可执行文件的不一样部分。在 __TEXT segment 中,__text section 包含了编译所获得的机器码。__stubs__stub_helper 是给动态连接器 (dyld) 使用的。经过这两个 section,在动态连接代码中,能够容许延迟连接。__const (在咱们的代码中没有) 是常量,不可变的,就像 __cstring (包含了可执行文件中的字符串常量 -- 在源码中被双引号包含的字符串) 常量同样。

__DATA segment 中包含了可读写数据。在咱们的程序中只有 __nl_symbol_ptr__la_symbol_ptr,它们分别是 non-lazylazy 符号指针。延迟符号指针用于可执行文件中调用未定义的函数,例如不包含在可执行文件中的函数,它们将会延迟加载。而针对非延迟符号指针,当可执行文件被加载同时,也会被加载。

_DATA segment 中的其它常见 section 包括 __const,在这里面会包含一些须要重定向的常量数据。例如 char * const p = "foo"; -- p 指针指向的数据是可变的。__bss section 没有被初始化的静态变量,例如 static int a; -- ANSI C 标准规定静态变量必须设置为 0。而且在运行时静态变量的值是能够修改的。__common section 包含未初始化的外部全局变量,跟 static 变量相似。例如在函数外面定义的 int a;。最后,__dyld 是一个 section 占位符,被用于动态连接器。

苹果的 OS X Assembler Reference 文档有更多关于 section 类型的介绍。

Section 中的内容

下面,咱们用 otool(1) 来观察一个 section 中的内容:

% xcrun otool -s __TEXT __text a.out 
a.out:
(__TEXT,__text) section
0000000100000f30 55 48 89 e5 48 83 ec 20 48 8d 05 4b 00 00 00 c7 
0000000100000f40 45 fc 00 00 00 00 89 7d f8 48 89 75 f0 48 89 c7 
0000000100000f50 b0 00 e8 11 00 00 00 b9 00 00 00 00 89 45 ec 89 
0000000100000f60 c8 48 83 c4 20 5d c3 
复制代码

上面是咱们 app 中的代码。因为 -s __TEXT __text 很常见,otool 对其设置了一个缩写 -t 。咱们还能够经过添加 -v 来查看反汇编代码:

% xcrun otool -v -t a.out
a.out:
(__TEXT,__text) section
_main:
0000000100000f30    pushq   %rbp
0000000100000f31    movq    %rsp, %rbp
0000000100000f34    subq    $0x20, %rsp
0000000100000f38    leaq    0x4b(%rip), %rax
0000000100000f3f    movl    $0x0, 0xfffffffffffffffc(%rbp)
0000000100000f46    movl    %edi, 0xfffffffffffffff8(%rbp)
0000000100000f49    movq    %rsi, 0xfffffffffffffff0(%rbp)
0000000100000f4d    movq    %rax, %rdi
0000000100000f50    movb    $0x0, %al
0000000100000f52    callq   0x100000f68
0000000100000f57    movl    $0x0, %ecx
0000000100000f5c    movl    %eax, 0xffffffffffffffec(%rbp)
0000000100000f5f    movl    %ecx, %eax
0000000100000f61    addq    $0x20, %rsp
0000000100000f65    popq    %rbp
0000000100000f66    ret
复制代码

上面的内容是同样的,只不过以反汇编形式显示出来。你应该感受很熟悉,这就是咱们在前面编译时候的代码。惟一的不一样就是,在这里咱们没有任何的汇编指令在里面。这是纯粹的二进制执行文件。

一样的方法,咱们能够查看别的 section:

% xcrun otool -v -s __TEXT __cstring a.out
a.out:
Contents of (__TEXT,__cstring) section
0x0000000100000f8a  Hello World!\n
复制代码

或:

% xcrun otool -v -s __TEXT __eh_frame a.out 
a.out:
Contents of (__TEXT,__eh_frame) section
0000000100000fe0    14 00 00 00 00 00 00 00 01 7a 52 00 01 78 10 01 
0000000100000ff0    10 0c 07 08 90 01 00 00 
复制代码

性能上须要注意的事项

从侧面来说,__DATA__TEXT segment对性能会有所影响。若是你有一个很大的二进制文件,你可能得去看看苹果的文档:关于代码大小性能指南。将数据移至 __TEXT 是个不错的选择,由于这些页历来不会被改变。

任意的片断

使用连接符号 -sectcreate 咱们能够给可执行文件以 section 的方式添加任意的数据。这就是如何将一个 Info.plist 文件添加到一个独立的可执行文件中的方法。Info.plist 文件中的数据须要放入到 __TEXT segment 里面的一个 __info_plist section 中。能够将 -sectcreate segname sectname file 传递给连接器(经过将下面的内容传递给 clang):

-Wl,-sectcreate,__TEXT,__info_plist,path/to/Info.plist
复制代码

一样,-sectalign 规定了对其方式。若是你添加的是一个全新的 segment,那么须要经过 -segprot 来规定 segment 的保护方式 (读/写/可执行)。这些全部内容在连接器的帮助文档中都有,例如 ld(1)

咱们能够利用定义在 /usr/include/mach-o/getsect.h 中的函数 getsectdata() 获得 section,例如 getsectdata() 能够获得指向 section 数据的一个指针,并返回相关 section 的长度。

Mach-O

在 OS X 和 iOS 中可执行文件的格式为 Mach-O

% file a.out 
a.out: Mach-O 64-bit executable x86_64
复制代码

对于 GUI 程序也是同样的:

% file /Applications/Preview.app/Contents/MacOS/Preview 
/Applications/Preview.app/Contents/MacOS/Preview: Mach-O 64-bit executable x86_64
复制代码

关于 Mach-O 文件格式 苹果有详细的介绍。

咱们可使用 otool(1) 来观察可执行文件的头部 -- 规定了这个文件是什么,以及文件是如何被加载的。经过 -h 能够打印出头信息:

% otool -v -h a.out           a.out:
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64  X86_64        ALL LIB64     EXECUTE    16       1296   NOUNDEFS DYLDLINK TWOLEVEL PIE
复制代码

cputypecpusubtype 规定了这个可执行文件可以运行在哪些目标架构上。ncmdssizeofcmds 是加载命令,能够经过 -l 来查看这两个加载命令:

% otool -v -l a.out | open -f
a.out:
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __PAGEZERO
   vmaddr 0x0000000000000000
   vmsize 0x0000000100000000
...
复制代码

加载命令规定了文件的逻辑结构和文件在虚拟内存中的布局。otool 打印出的大多数信息都是源自这里的加载命令。看一下 Load command 1 部分,能够找到 initprot r-x,它规定了以前提到的保护方式:只读和可执行。

对于每个 segment,以及segment 中的每一个 section,加载命令规定了它们在内存中结束的位置,以及保护模式等。例如,下面是 __TEXT __text section 的输出内容:

Section
  sectname __text
   segname __TEXT
      addr 0x0000000100000f30
      size 0x0000000000000037
    offset 3888
     align 2^4 (16)
    reloff 0
    nreloc 0
      type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
 reserved1 0
 reserved2 0
复制代码

上面的代码将在 0x100000f30 处结束。它在文件中的偏移量为 3888。若是看一下以前 xcrun otool -v -t a.out 输出的反汇编代码,能够发现代码实际位置在 0x100000f30。

咱们一样看看在可执行文件中,动态连接库是如何使用的:

% otool -v -L a.out
a.out:
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)
    time stamp 2 Thu Jan  1 01:00:02 1970
复制代码

上面就是咱们可执行文件将要找到 _printf 符号的地方。

一个更复杂的例子

咱们来看看有三个文件的复杂例子:

Foo.h:

#import <Foundation/Foundation.h>

@interface Foo : NSObject

- (void)run;

@end
复制代码

Foo.m:

#import "Foo.h"

@implementation Foo

- (void)run
{
    NSLog(@"%@", NSFullUserName());
}

@end
复制代码

helloworld.m:

#import "Foo.h"

int main(int argc, char *argv[])
{
    @autoreleasepool {
        Foo *foo = [[Foo alloc] init];
        [foo run];
        return 0;
    }
}
复制代码

编译多个文件

在上面的示例中,有多个源文件。因此咱们须要让 clang 对输入每一个文件生成对应的目标文件:

% xcrun clang -c Foo.m
% xcrun clang -c helloworld.m
复制代码

咱们历来不编译头文件。头文件的做用就是在被编译的实现文件中对代码作简单的共享。Foo.mhelloworld.m 都是经过 #import 语句将 Foo.h 文件中的内容添加到实现文件中的。

最终获得了两个目标文件:

% file helloworld.o Foo.o
helloworld.o: Mach-O 64-bit object x86_64
Foo.o:        Mach-O 64-bit object x86_64
复制代码

为了生成一个可执行文件,咱们须要将这两个目标文件和 Foundation framework 连接起来:

xcrun clang helloworld.o Foo.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
复制代码

如今能够运行咱们的程序了:

% ./a.out 
2013-11-03 18:03:03.386 a.out[8302:303] Daniel Eggert
复制代码

符号表和连接

咱们这个简单的程序是将两个目标文件合并到一块儿的。Foo.o 目标文件包含了 Foo 类的实现,而 helloworld.o 目标文件包含了 main() 函数,以及调用/使用 Foo 类。

另外,这两个目标对象都使用了 Foundation framework。helloworld.o 目标文件使用了它的 autorelease pool,并间接的使用了 libobjc.dylib 中的 Objective-C 运行时。它须要运行时函数来进行消息的调用。Foo.o 目标文件也有相似的原理。

全部的这些东西都被形象的称之为符号。咱们能够把符号当作是一些在运行时将会变成指针的东西。虽然实际上并非这样的。

每一个函数、全局变量和类等都是经过符号的形式来定义和使用的。当咱们将目标文件连接为一个可执行文件时,连接器 (ld(1)) 在目标文件盒动态库之间对符号作了解析处理。

可执行文件和目标文件有一个符号表,这个符号表规定了它们的符号。若是咱们用 nm(1) 工具观察一下 helloworld.0 目标文件,能够看到以下内容:

% xcrun nm -nm helloworld.o
                 (undefined) external _OBJC_CLASS_$_Foo
0000000000000000 (__TEXT,__text) external _main
                 (undefined) external _objc_autoreleasePoolPop
                 (undefined) external _objc_autoreleasePoolPush
                 (undefined) external _objc_msgSend
                 (undefined) external _objc_msgSend_fixup
0000000000000088 (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_
000000000000008e (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_1
0000000000000093 (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_2
00000000000000a0 (__DATA,__objc_msgrefs) weak private external l_objc_msgSend_fixup_alloc
00000000000000e8 (__TEXT,__eh_frame) non-external EH_frame0
0000000000000100 (__TEXT,__eh_frame) external _main.eh
复制代码

上面就是那个目标文件的全部符号。_OBJC_CLASS_$_FooFoo Objective-C 类的符号。该符号是 undefined, externalExternal 的意思是指对于这个目标文件该类并非私有的,相反,non-external 的符号则表示对于目标文件是私有的。咱们的 helloworld.o 目标文件引用了类 Foo,不过这并无实现它。所以符号表中将其标示为 undefined。

接下来是 _main 符号,它是表示 main() 函数,一样为 external,这是由于该函数须要被调用,因此应该为可见的。因为在 helloworld.o 文件中实现了 这个 main 函数。这个函数地址位于 0处,而且须要转入到 __TEXT,__text section。接着是 4 个 Objective-C 运行时函数。它们一样是 undefined的,须要连接器进行符号解析。

若是咱们转而观察 Foo.o 目标文件,能够看到以下输出:

% xcrun nm -nm Foo.o
0000000000000000 (__TEXT,__text) non-external -[Foo run]
                 (undefined) external _NSFullUserName
                 (undefined) external _NSLog
                 (undefined) external _OBJC_CLASS_$_NSObject
                 (undefined) external _OBJC_METACLASS_$_NSObject
                 (undefined) external ___CFConstantStringClassReference
                 (undefined) external __objc_empty_cache
                 (undefined) external __objc_empty_vtable
000000000000002f (__TEXT,__cstring) non-external l_.str
0000000000000060 (__TEXT,__objc_classname) non-external L_OBJC_CLASS_NAME_
0000000000000068 (__DATA,__objc_const) non-external l_OBJC_METACLASS_RO_$_Foo
00000000000000b0 (__DATA,__objc_const) non-external l_OBJC_$_INSTANCE_METHODS_Foo
00000000000000d0 (__DATA,__objc_const) non-external l_OBJC_CLASS_RO_$_Foo
0000000000000118 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000000000140 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo
0000000000000168 (__TEXT,__objc_methname) non-external L_OBJC_METH_VAR_NAME_
000000000000016c (__TEXT,__objc_methtype) non-external L_OBJC_METH_VAR_TYPE_
00000000000001a8 (__TEXT,__eh_frame) non-external EH_frame0
00000000000001c0 (__TEXT,__eh_frame) non-external -[Foo run].eh
复制代码

第五行至最后一行显示了 _OBJC_CLASS_$_Foo 已经定义了,而且对于 Foo.o 是一个外部符号 -- ·Foo.o· 包含了这个类的实现。

Foo.o 一样有 undefined 的符号。首先是使用了符号 NSFullUserName()NSLog()NSObject

当咱们将这两个目标文件和 Foundation framework (是一个动态库) 进行连接处理时,连接器会尝试解析全部的 undefined 符号。它能够解析 _OBJC_CLASS_$_Foo。另外,它将使用 Foundation framework。

当连接器经过动态库 (此处是 Foundation framework) 解析成功一个符号时,它会在最终的连接图中记录这个符号是经过动态库进行解析的。连接器会记录输出文件是依赖于哪一个动态连接库,并连同其路径一块儿进行记录。在咱们的例子中,_NSFullUserName_NSLog_OBJC_CLASS_$_NSObject_objc_autoreleasePoolPop 等符号都是遵循这个过程。

咱们能够看一下最终可执行文件 a.out 的符号表,并注意观察连接器是如何解析全部符号的:

% xcrun nm -nm a.out 
                 (undefined) external _NSFullUserName (from Foundation)
                 (undefined) external _NSLog (from Foundation)
                 (undefined) external _OBJC_CLASS_$_NSObject (from CoreFoundation)
                 (undefined) external _OBJC_METACLASS_$_NSObject (from CoreFoundation)
                 (undefined) external ___CFConstantStringClassReference (from CoreFoundation)
                 (undefined) external __objc_empty_cache (from libobjc)
                 (undefined) external __objc_empty_vtable (from libobjc)
                 (undefined) external _objc_autoreleasePoolPop (from libobjc)
                 (undefined) external _objc_autoreleasePoolPush (from libobjc)
                 (undefined) external _objc_msgSend (from libobjc)
                 (undefined) external _objc_msgSend_fixup (from libobjc)
                 (undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000e50 (__TEXT,__text) external _main
0000000100000ed0 (__TEXT,__text) non-external -[Foo run]
0000000100001128 (__DATA,__objc_data) external _OBJC_METACLASS_$_Foo
0000000100001150 (__DATA,__objc_data) external _OBJC_CLASS_$_Foo
复制代码

能够看到全部的 Foundation 和 Objective-C 运行时符号依旧是 undefined,不过如今的符号表中已经多了如何解析它们的信息,例如在哪一个动态库中能够找到对应的符号。

可执行文件一样知道去哪里找到所需库:

% xcrun otool -L a.out
a.out:
    /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1056.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1)
    /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 855.11.0)
    /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
复制代码

在运行时,动态连接器 dyld(1) 能够解析这些 undefined 符号,dyld 将会肯定好 _NSFullUserName 等符号,并指向它们在 Foundation 中的实现等。

咱们能够针对 Foundation 运行 nm(1),并检查这些符号的定义状况:

% xcrun nm -nm `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation | grep NSFullUserName
0000000000007f3e (__TEXT,__text) external _NSFullUserName 
复制代码

动态连接编辑器

有一些环境变量对于 dyld 的输出信息很是有用。首先,若是设置了 DYLD_PRINT_LIBRARIES,那么 dyld 将会打印出什么库被加载了:

% (export DYLD_PRINT_LIBRARIES=; ./a.out )
dyld: loaded: /Users/deggert/Desktop/command_line/./a.out
dyld: loaded: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
dyld: loaded: /usr/lib/libSystem.B.dylib
dyld: loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
dyld: loaded: /usr/lib/libobjc.A.dylib
dyld: loaded: /usr/lib/libauto.dylib
[...]
复制代码

上面将会显示出在加载 Foundation 时,同时会加载的 70 个动态库。这是因为 Foundation 依赖于另一些动态库。运行下面的命令:

% xcrun otool -L `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
复制代码

能够看到 Foundation 使用了 15 个动态库。

dyld 的共享缓存

当你构建一个真正的程序时,将会连接各类各样的库。它们又会依赖其余一些 framework 和 动态库。须要加载的动态库会很是多。而对于相互依赖的符号就更多了。可能将会有上千个符号须要解析处理,这将花费很长的时间:通常是好几秒钟。

为了缩短这个处理过程所花费时间,在 OS X 和 iOS 上的动态连接器使用了共享缓存,共享缓存存于 /var/db/dyld/。对于每一种架构,操做系统都有一个单独的文件,文件中包含了绝大多数的动态库,这些库都已经连接为一个文件,而且已经处理好了它们之间的符号关系。当加载一个 Mach-O 文件 (一个可执行文件或者一个库) 时,动态连接器首先会检查 共享缓存 看看是否存在其中,若是存在,那么就直接从共享缓存中拿出来使用。每个进程都把这个共享缓存映射到了本身的地址空间中。这个方法大大优化了 OS X 和 iOS 上程序的启动时间。


原文: Mach-O Executables

译文 objc.io 第6期 Mach-O 可执行文件

相关文章
相关标签/搜索