iOS 系列译文:Mach-O 可执行文件
当我们在Xcode中构建一个程序的时候,其中有一部分就是把源文件(.m和.h)文件转变成可执行文件。这个可执行文件包含了将会在CPU(iOS设备上的arm处理器或者你mac上的Intel处理器)运行的字节码。
我们将会过一遍编译器这个过程的做了些什么,同时也看一下可执行文件的内部到底是怎样的。其实,里面的东西比你看到的要多很多。
让我们先把Xcode放一边,踏入Commond-Lines的大陆。当我们在Xcode中构建一个App时,Xcode只是简单的调用了一系列的工具而已。希望这将会让你更好的明白一个可执行文件(被称之为Mach-O可执行文件),是怎样组装起来的,并且是怎样在iOS或者os x上执行的
XCrun
先从一些基础性的东西开始:我们将会使用一个叫做Xcrun的命令行工具。他看起来很奇怪,但是的确相当出色。这个小工具是用来调用其他工具的。 原先的时候我们执行:
- % clang -v
现在在终端中,我们可以执行:
- % xcrun clang -v
Xcrun定位Clang,并且使用相关的参数来执行Clang。
为什么我们要做这个事情?这看起来毫无重点,胡扯八道。但是Xcrun允许我们使用多个版本的Xcode,或者使用特定Xcode版本里面的工具,或者针对特点的SDK使用不同的工具。如果你恰好有Xcode4.5和xcode5、使用xcode-select和xcrun你可以选择选择使用来自Xcode4.5里面的SDK的工具,或者来自Xcode5里面的SDK的工具。在大多数其他平台上,这将是一个不可能的事情。如果你看一下帮助手册上xcode-select和xcrun的一些细节。你就能在不安装命令行工具的情况下,使用在终端中使用开发者工具。
一个不使用IDE的Hello World
回到终端,创建一个包含一个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的情况下做的。深呼吸一下,高兴高兴。我们在这里做了些什么?我们将hellowrold.c编译成了叫a.out的Mach-o二进制文件。a.out是编译器的默认名字,除非你指定一个别的。
Hello World和编译器
现在可选择的编译器是Clang(读作:/’kl /)。Chris写了一些更多关于Clang细节的介绍,可以参考:
about the compiler
概括一下就是,编译器将会读入处理hellowrold.c,输出可执行文件a.out。这个过程包含了非常多的步骤。我们所要做的就是正确的执行它们。
预处理:
序列化
宏定义展开
#include展开(引用文件展开)
语法和语义分析:
使用预处理后的单词构建词法树
执行语义分析生成语法树
输出AST (Abstract Syntax Tree)
代码生成和优化
将AST转化成更低级的中间码(LLVM IR)
优化生成代码
目标代码生成
输出汇编代码
汇编程序
将汇编代码转化成目标文件
连接器
将多个目标文件合并成可执行文件(或者一个动态库) 我们来看一个关于这些步骤的简单的例子。
预处理
编译器将做的第一件事情是处理文件。使用Clang展示一下这个过程:
- % xcrun clang -E helloworld.c
欧耶。输出了413行内容。打开个编辑器看看到底发生了什么:
- % xcrun clang -E helloworld.c | open -f
在文件顶部我们能看到很多以”#”开头的行。这些被称之为行标记语句的语句告诉我们它后面的内容来自哪里。我们需要这个。如果我再看一下hellowrold.c,第一行是:
- #include <stdio.h>
我们都用过#include和#import。它们做的就是告诉于处理器在#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汇编代码。最后是些标记,就像C中的那些标记一样。
我们从前三行开始:
- .section __TEXT,__text,regular,pure_instructions
- .globl _main
- .align 4, 0x90
这三行是汇编指令,不是汇编代码。”.section”指令指出了哪一个段接下来将会被执行。比用二进制表示好看多了。
下一个,.global指令说明_main是一个外部符号。这就是我们的main()函数。它能够从我们的二进制文件之外看到,因为系统要调用它来运行可执行文件。
.align指令指出了下面代码的对齐方式。从我们的角度看,接下来的代码将会按照16比特对齐并且如果需要的时候用0×90补齐。
下面是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函数真正的开始地址。这个也是被抛出的符号。二进制文件将会在这个地方产生一个引用。
.cfi_startproc指令一半会在函数开始的地方使用。CFI是Call Frame Information的缩写。帧松散的与一个函数交互。当你使用调试器,并且单步执行的时候,你实际上是在调用帧中跳转。在C代码中,函数有自己的调用帧,除了函数之外的一些结构也会有调用站。.cfi_startproc指令给了函数一个.en_frame的入口,这个入口包含了堆栈展开信息(表示异常如何展开调用帧堆栈)。这个指令也会发送一些和具体平台相关的指令给CFI。文件后面的.cfi_endproc与.cfi_startproc相匹配,来表示结束main函数。
下一步,这里有另外一个Label ## BB#0.然后,终于来了第一句汇编代码:pushq %rbp。从这里开始事情开始变得有趣。在OS X上,我们将会有x84_64的代码。对于这种架构,有一个东西叫做ABI(application binary interface),ABI表示函数调用是怎样在汇编代码层面上工作的。ABI指出在函数调用时,rbp寄存器必须被保护起来。这是main函数的责任,来确保返回时,rbp寄存器中有数据。pushq %rbp将它的数据推进堆栈,以便我们以后使用。
下面是,两个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比特是零,之后32个比特是edi的值(存储了argc),然后是64bit的rsi寄存器的值。我们在后面不会使用这些数据。但是如果编译器没有使用优化的时候,它们还是会被存下来。
现在,我们将会把第一个函数(printf)的参数加载进寄存器edi。printf函数是一个可变参数的函数。ABI调用约定指定,将会把使用来存储参数的寄存器数量存储在寄存器al中。对我们来讲是0。最后callq调用了printf函数。
- movl $0, %ecx
- movl %eax, -20(%rbp) ## 4-byte Spill
- movl %ecx, %eax
这将设置ecx寄存的值为0,并且把eax的值压栈。然后从ecx复制0到eax。ABI指定eax将会存储函数的返回值,我们man函数的返回值是0:
- addq $32, %rsp
- popq %rbp
- ret
- .cfi_endproc
函数执行完成后,将恢复堆栈指针,通过上移32bit在rsp中的堆栈指针。我们将会出栈我们早先存储的rbp的值,然后调用ret来返回,ret将会读取离开堆栈的地址。.cfi_endproc平衡了.cfi_startproc指令。
下一步是一个字一个字的输出我们的字符串:“hello world!\n”:
之后.section指令指出下面将要跳入的段。L_.str标记允许获取一个字符转的指针。.asciz指定告诉汇编器输出一个0的字符串结尾。
__TEXT __cstring开始了一个新的段。这个段包含了C字符串:
- .section __TEXT,__cstring,cstring_literals
- tr: ## @.str
- .asciz "Hello World!\n"
这两行创建了一个没有结束符的字符创。注意L_.str是怎样命名,和来获取字符串的。
最后.subseciton_via_symbols指令是静态链接编辑器使用的。
更多关于汇编指令的信息可以从苹果的
Apple’s assemebler reference获取。AMD64网站有关于
ABI for x86的文档。同时也有
Gentle Introduction to x86-64 Assemble。 再一次,Xcode允许你查看任何文件的汇编代码通过 Product->Perform Action -> Assemble.
汇编编译器:
汇编编译器,只是简单的将汇编代码转换成机器码。它创建了一个目标文件。这些文件以.o结尾。如果你使用Xcode构建一个app,你将会在Derived Data目录下面的你的工程目录中的objects-normal目录下面发现这些文件。
连接器:
我们将会多谈一点关于链接的东西。但是简单的说,连接器确定了目标文件和库之间的链接。这是什么意思? 重新调用 callq _printf. printf是在libc库中的一个函数。无论怎样,最后的可执行文件需要能知道printf()在内存中的什么位置。例如符号_printf的地址。连接器将会读取所有的目标文件,所有的库和结束任何未定义的符号。然后将它们编码进最后的可执行文件,然后输出最后的可执行文件:a.out。
段
就像我们上面提到的一样,这里有些东西叫做段。一个可执行文件包含多个段。可执行文件不同的部分将会加载进不同的段。并且每个段将会转化进一个“Segment”中。这对我们随便写的app如此,对我们用心写的app也一样。
我们来看看在a.out中的段。我们可以使用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文件有四个段。其中一些有section。
当我们执行一个可执行文件。虚拟内存系统会将segment映射到进程的地址空间中。映射完全不同于我们一般的认识,但是如果你对虚拟内存系统不熟悉,可以简单的想象VM会将整个文件加载进内存,虽然在实际上这不会发生。VM使用了一些技巧来避免全部加载。
当虚拟内存系统进行映射时,数据段和可执行段会以不同的参数和权限被映射。
__TEXT段包含了可执行的代码。它们被以只读和可执行的方式映射。进程被允许执行这些代码,但是不能修改。这些代码也不能改变它们自己,并且这些页从来不会被污染。
__DATA段以可读写和不可执行的方式映射。它包含了将会被更改的数据。
第一个段是__PAGEZERO。这个有4GB大小。这4GB并不是文件的真实大小,但是说明了进程的前4GB地址空间将会被映射为,不能执行,不能读,不能写。这就是为什么在去写NULL指针或者一些低位的指针的时候,你会得到一个EXC_BAD_ACCESS错误。这是操作系统在尝试防止你引起系统崩溃。
在每一个段内有一些片段。它们包含了可执行文件的不同的部分。在_TEXT段,_text片段包含了编译得到的机器码。_stubs和_stub_helper是给动态链接器用的。着允许动态链接的代码延迟链接。_const是不可变的部分,就像_cstring包含了可执行文件的字符串一样。
_DATA段包含了可读写数据。从我们的角度,我们只有_nl_sysmol_ptr 和__la_symble_ptr,它们是延迟链接的指针。延迟链接的指针被用来执行未定义的函数。例如,那些没有包含在可执行文件本身内部的函数。它们将会延迟加载。那些非延迟链接的指针将会在可执行文件被夹在的时候确定。
其他在_DATA中共同的段是_const。她包含了那些需要重定位的不可变数据。一个例子是chat* const p = “foo”; p指针指向的数据不是静态的。_bss片段包含了没有被初始化的静态变量例如static int a; ANSI C标准指出这些静态变量将会被设置为零。但是在运行时可以被改变。_common片段包含了被动态链接器使用的占位符片段。
苹果的文档
OSX Assembler Reference有更多关于片段定义的内容。
段内容:
我们能检查每一个片段的内容,使用otool像这样:
- % 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
这里有些内容反汇编的代码中的一样,你应该感觉很熟悉,这就是我们在前面编译时候的代码。唯一的不同就是,在这里我们没有任何的汇编指令在里面。这是纯粹的二进制执行文件。
同样的方法,我们可以查案一下其他片段:
- % 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段会影响性能。如果你有一个非常大的二进制文件,你可能回想查看苹果的
代码大小优化指南。将数据移到__TEXT段是个不错的选择,因为这些页从来不会变脏。
任意的片段
你可以以片段的方式向你的二进制文件添加任何的数据,通过-sectcreate链接参数。这就是你怎样添加info.plist到一个独立的二进制文件。Info.plist的数据需要被放在_TEXT段的_info_plist片段。你可以使用连接器的命令-sectcreate segname sectname file来实现:
- -Wl,-sectcreate,__TEXT,__info_plist,path/to/Info.plist
同样的,-sectalign也致命了对齐方式。如果你添加一个全新的段,通过-segprot来制定数据的保护方式。这些都是在连接器中的帮助手册中的。
你能够到达在/usr/include/mach-o/getsect.h中定义的函数在二进制文件中的那些片段,通过使用getsectdata(),它将会返回片段数据的指针和大小。
Mch-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来看一看mach-o文件的头部。这说明了这个文件是什么,和怎样被加载的。我们将会使用-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 comand 1部分,我们看到了initprot r-x,这指明了我们上面提到的数据保护模式:只读并且可执行。
对于每一个段和每一个段中的片段,加载命令说明了它们会在内存中的位置和它们的保护模式,例如,这是关于__TEXT __text片段的输出:
- 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。如果你看一下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
我们从来不编译头文件。头文件的目的是在实现文件中贡献代码,并通过这种方式来呗编译。通过#import语句Foo.m和helloworld.m中都被插入了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系统库:
- xcrun clang helloworld.o Foo.o -Wl,`xcrun --show-sdk- path`/System/Library/Frameworks/Foundation.framework/Foundation
现在,我们可以运行我们的程序了。
符号表和链接
我们这个简单的app是通过两个目标文件合并到一起得到的。Foo.o包含了Foo类的实现,同事helloworld.o包含了调用Foo类方法run的main函数。 进一步,两个文件都使用了Foundation库。在helloworld.o中autorelease pool使用了这个库,以简洁的方式使用了libobjc.dylib中的Objctive-c运行时。它需要使用运行时的函数来发送消息调用。foo.o也是一样的。
这些被形象的称之为符号。我们可以把符号看成一些在运行时将会变成指针的东西。虽然实际上并不是这样能够。 每一个函数,全局变量,类等等都是通过符号的方式来使用的。当我们为可执行文件连接一个目标文件,连接器将会按需要决定目标文件和动态库之间的所有符号。 可执行文件和目标文件都有一个符号表来存储这些符号。如果你使用nm工具来查看一下helloworld.o你会发现:
- % 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的符号链接。它还没有被决定成Foo类的外部链接。外部表示它对不是私有的。与此相反non-external表明符号链接对于特定的文件是私有的。 我们的helloworld.o文件引用了Foo类,但是并没有实现它。于是符号最后以未确定结尾。
下面,main函数同样是外部链接,因为它需要能够被外部看到并被调用。无论怎样,main函数是在helloworld中实现的。并且放在了地址0,和放在__TEXT __text片段中。然后是四个objc运行时的函数。它们同样是未定义的,需要连接器来决定。
我们再来看看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的实现。 Foo.o也有未定义的符号。最前面的是它使用过的NSFullUserName(),NSLog()和NSObject。 当我们连接着两个文件还有Foundation库的时候,将会确定这些在动态链接库中的符号。临界期记录了输出文件以来特定的动态链接库和它们的位置。这就是NSFullName()等将会发生的事情。
我们可以看一下最后的执行文件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和Objctive-C运行时的一些符号依然是未确定的。但是符号表中,记录了怎样去确定它们。例如那些它们可以去查找的动态链接库。
可执行文件一样也知道去哪找这些库:
- % 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)确定。当我们执行程序的时候,dyld将会在Foundation中确定指向_NSFullUserName等的实现的指针,等等等等
我们可以再次使用nm来查看你这些符号在Foundation中的情况,实际上,如下:
- % 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的时候加载的动态链接库。这是因为Foundation库也依赖于其他很多动态链接库, 你可以运行:
- % xcrun otool -L `xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
来查看五十多个Foundation依赖的库。
dyld的共享缓存
当你构建一个真正的程序的时候,你将会链接各种各样的库。它们又会依赖其他的一些框架和动态链接库。于是要加载的动态链接库会非常多。同样非独立的符号也非常多。这里就会有成千上万的符号要确定。这个工作将会话费很多时间——几秒钟。 为了优化这个过程,OS X和iOS上动态链接器使用了一个共享缓存,在/var/db/dyld/。对于每一种架构,操作系统有一个单独的文件包含了绝大多数的动态链接库,这些库已经互相连接并且符号都已经确定。当一个Mach-o文件被加载的时候,动态链接器会首先检查共享缓存,如果存在相应的库就是用。每一个进程都把这个共享缓存映射到了自己的地址空间中。这个方法戏剧性的优化了OS X和iOS上程序的加载时间。