咱们用 Xcode 构建一个程序的过程当中,会把源文件 (.m
和 .h
) 文件转换为一个可执行文件。这个可执行文件中包含的字节码会将被 CPU (iOS 设备中的 ARM 处理器或 Mac 上的 Intel 处理器) 执行。html
本文将介绍一下上面的过程当中编译器都作了些什么,同时深刻看看可执行文件内部是怎样的。实际上里面的东西要比咱们第一眼看到的多得多。xcode
这里咱们把 Xcode 放一边,将使用命令行工具 (command-line tools)。当咱们用 Xcode 构建一个程序时,Xcode 只是简单的调用了一系列的工具而已。Florian 对工具调用是如何工做的作了更详细的讨论。本文咱们就直接调用这些工具,并看看它们都作了些什么。缓存
真心但愿本文能帮助你更好的理解 iOS 或 OS X 中的一个可执行文件 (也叫作 Mach-O executable) 是如何执行,以及怎样组装起来的。架构
先来看一些基础性的东西:这里会大量使用一个名为 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-select
和 xcrun
能够选择使用 Xcode 5 中 iOS SDK 的工具,或者 Xcode 4.5 中的 OS X 工具。在许多其它平台中,这是不可能作到的。查阅 xcrun
和 xcode-select
的主页内容能够了解到详细内容。不用安装 Command Line Tools,就能使用命令行中的开发者工具。工具
回到终端 (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。
这个二进制文件是如何生成的呢?实际上有许多内容须要观察和理解。咱们先看看编译器吧。
时下 Xcode 中编译器默认选择使用 clang
(读做 /klæŋ/)。关于编译器,Chris 写了更详细的文章。
简单的说,编译器处理过程当中,将 helloworld.c
当作输入文件,并生成一个可执行文件 a.out
。这个过程有多个步骤/阶段。咱们须要作的就是正确的执行它们。
#include
的展开咱们来看一个关于这些步骤的简单的例子。
编译过程当中,编译器首先要作的事情就是对文件作处理。预处理结束以后,若是咱们中止编译过程,那么咱们可让编译器显示出预处理的一些内容:
% xcrun clang -E helloworld.c
复制代码
喔喔。 上面的命令输出的内容有 413 行。咱们用编辑器打开这些内容,看看到底发生了什么:
% xcrun clang -E helloworld.c | open -f
复制代码
在顶部能够看到的许多行语句都是以 #
开头 (读做 hash
)。这些被称为 行标记 的语句告诉咱们后面跟着的内容来自哪里。若是再回头看看 helloworld.c
文件,会发现第一行是:
#include <stdio.h>
复制代码
咱们都用过 #include
和 import
。它们所作的事情是告诉预处理器将文件 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 in 或 step 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 %rbp
将 rbp
的值 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"
。 edi
和 rsi
寄存器保存了函数的第一个和第二个参数。因为咱们会调用别的函数,因此首先须要将它们的当前值保存起来。这就是为何咱们使用刚刚存储的 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 会转换进某个 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-lazy 和 lazy 符号指针。延迟符号指针用于可执行文件中调用未定义的函数,例如不包含在可执行文件中的函数,它们将会延迟加载。而针对非延迟符号指针,当可执行文件被加载同时,也会被加载。
在 _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 类型的介绍。
下面,咱们用 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 的长度。
在 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
复制代码
cputype
和 cpusubtype
规定了这个可执行文件可以运行在哪些目标架构上。ncmds
和 sizeofcmds
是加载命令,能够经过 -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.m
和 helloworld.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_$_Foo
是 Foo
Objective-C 类的符号。该符号是 undefined, external 。External 的意思是指对于这个目标文件该类并非私有的,相反,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 个动态库。
当你构建一个真正的程序时,将会连接各类各样的库。它们又会依赖其余一些 framework 和 动态库。须要加载的动态库会很是多。而对于相互依赖的符号就更多了。可能将会有上千个符号须要解析处理,这将花费很长的时间:通常是好几秒钟。
为了缩短这个处理过程所花费时间,在 OS X 和 iOS 上的动态连接器使用了共享缓存,共享缓存存于 /var/db/dyld/
。对于每一种架构,操做系统都有一个单独的文件,文件中包含了绝大多数的动态库,这些库都已经连接为一个文件,而且已经处理好了它们之间的符号关系。当加载一个 Mach-O 文件 (一个可执行文件或者一个库) 时,动态连接器首先会检查 共享缓存 看看是否存在其中,若是存在,那么就直接从共享缓存中拿出来使用。每个进程都把这个共享缓存映射到了本身的地址空间中。这个方法大大优化了 OS X 和 iOS 上程序的启动时间。