iOS 系列译文:Mach-O 可执行文件

原文链接:  Daniel Eggert    翻译:  伯乐在线  一水流年

译文链接: http://blog.jobbole.com/51527/


当我们在Xcode中构建一个程序的时候,其中有一部分就是把源文件(.m和.h)文件转变成可执行文件。这个可执行文件包含了将会在CPU(iOS设备上的arm处理器或者你mac上的Intel处理器)运行的字节码。

我们将会过一遍编译器这个过程的做了些什么,同时也看一下可执行文件的内部到底是怎样的。其实,里面的东西比你看到的要多很多。

让我们先把Xcode放一边,踏入Commond-Lines的大陆。当我们在Xcode中构建一个App时,Xcode只是简单的调用了一系列的工具而已。希望这将会让你更好的明白一个可执行文件(被称之为Mach-O可执行文件),是怎样组装起来的,并且是怎样在iOS或者os x上执行的

 

XCrun

先从一些基础性的东西开始:我们将会使用一个叫做Xcrun的命令行工具。他看起来很奇怪,但是的确相当出色。这个小工具是用来调用其他工具的。 原先的时候我们执行:

1
% clang -v

现在在终端中,我们可以执行:

1
% 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文件的目录:

1
2
3
% mkdir ~ /Desktop/objcio-command-line
% cd !$
% touch helloworld.c

现在使用你喜欢的文本编辑器来编辑这个文件,例如TextEdit.app:

1
% open -e helloworld.c

录入下面的代码:

1
2
3
4
5
6
#include <stdio.h>
int main(int argc, char *argv[])
{
     printf ( "Hello World!\n" );
     return 0;
}

保存,并且回到终端执行:

1
2
% 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展示一下这个过程:

1
% xcrun clang -E helloworld.c

欧耶。输出了413行内容。打开个编辑器看看到底发生了什么:

1
% xcrun clang -E helloworld.c | open -f

在文件顶部我们能看到很多以”#”开头的行。这些被称之为行标记语句的语句告诉我们它后面的内容来自哪里。我们需要这个。如果我再看一下hellowrold.c,第一行是:

1
#include <stdio.h>

我们都用过#include和#import。它们做的就是告诉于处理器在#include语句的地方插入stdio.h的内容。在刚刚的文件里就是插入了一个以#开头的行标记。跟在#后面的数字是在源文件中的行号。每一行最后的数字是在新文件中的行号。回到刚才打开的文件,接下来是系统头文件,或者一些被看成包裹着extern “C”的文件。

如果你滚动到文件末尾,你将会发现我们的helloworld.c的代码:

1
2
3
4
5
6
# 2 "helloworld.c" 2
int main( int argc, char *argv[])
{
      printf ( "Hello World!\n" );
      return 0;
}

在Xcode中,你可以通过使用Product->Perform Action-> Preprocess来查看任何一个文件的预处理输出。一定要注意这将会花费一些时间来加载预处理输出文件(接近100,000行)。

 

编译

下一个步骤:文本处理和代码生成。我们可以调用clang输出汇编代码就像这样:

1
% xcrun clang -S -o - helloworld.c | open -f

看一看输出。我们首先注意到的是一些以点开头的行。这些是汇编指令。其他的是真正的x86_64汇编代码。最后是些标记,就像C中的那些标记一样。

我们从前三行开始:

1
2
3
.section    __TEXT,__text,regular,pure_instructions
.globl  _main
.align  4, 0x90

这三行是汇编指令,不是汇编代码。”.section”指令指出了哪一个段接下来将会被执行。比用二进制表示好看多了。

下一个,.global指令说明_main是一个外部符号。这就是我们的main()函数。它能够从我们的二进制文件之外看到,因为系统要调用它来运行可执行文件。

.align指令指出了下面代码的对齐方式。从我们的角度看,接下来的代码将会按照16比特对齐并且如果需要的时候用0×90补齐。

下面是main函数的头部:

1
2
3
4
5
6
7
8
9
10
11
12
_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():

1
2
3
4
5
6
7
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函数。

1
2
3
movl    $0, %ecx
movl    %eax, -20(%rbp)         ## 4-byte Spill
movl    %ecx, %eax

这将设置ecx寄存的值为0,并且把eax的值压栈。然后从ecx复制0到eax。ABI指定eax将会存储函数的返回值,我们man函数的返回值是0:

1
2
3
4
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字符串:

1
2
3
     .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
% 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像这样:

1
2
3
4
5
6
7
% 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% 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

这里有些内容反汇编的代码中的一样,你应该感觉很熟悉,这就是我们在前面编译时候的代码。唯一的不同就是,在这里我们没有任何的汇编指令在里面。这是纯粹的二进制执行文件。

同样的方法,我们可以查案一下其他片段:

1
2
3
4
% xcrun otool -v -s __TEXT __cstring a.out
a.out:
Contents of (__TEXT,__cstring) section
0x0000000100000f8a  Hello World!\n

或者:

1
2
3
4
5
% 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来实现:

1
-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格式的:

1
2
% file a.out
a.out: Mach-O 64-bit executable x86_64

对于GUI的程序来说也是这样:

1
2
% 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参数来打印头部信息。

1
2
3
4
% 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参数来查看:

1
2
3
4
5
6
7
8
9
% 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片段的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
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处看到我们的代码。

我们同样可以看一下在可执行文件中,动态链接库是怎样使用的:

1
2
3
4
% 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符号链接将要用到的库。

 

一个更复杂的例子

让我们来看一个有三个文件的复杂的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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来生成目标文件:

1
2
% xcrun clang -c Foo.m
% xcrun clang -c helloworld.m

我们从来不编译头文件。头文件的目的是在实现文件中贡献代码,并通过这种方式来呗编译。通过#import语句Foo.m和helloworld.m中都被插入了foo.h的内容。 我们得到了两个文件:

1
2
3
% file helloworld.o Foo.o
helloworld.o: Mach-O 64-bit object x86_64
Foo.o:        Mach-O 64-bit object x86_64

为了生成可执行文件,我们需要链接这两个目标文件和Foundation系统库:

1
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你会发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
% 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文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% 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的符号表,就能够发现连接器是怎样确定这些符号的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
% 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运行时的一些符号依然是未确定的。但是符号表中,记录了怎样去确定它们。例如那些它们可以去查找的动态链接库。

可执行文件一样也知道去哪找这些库:

1
2
3
4
5
6
% 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中的情况,实际上,如下:

1
2
% 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将会输出已经加载的东戴链接库:

1
2
3
4
5
6
7
8
% (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库也依赖于其他很多动态链接库, 你可以运行:

1
% 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上程序的加载时间。


你可能感兴趣的:(可执行文件,Mach-O)