IOS可执行文件学习总结

发布时间:2024-04-04 08:01

了解iOS上的可执行文件和Mach-O格式

http://www.cocoachina.com/mac/20150122/10988.html
 
很多朋友都知道,在Windows上exe是可直接执行的文件扩展名,而在Linux(以及很多版本的Unix)系统上ELF是可直接执行的文件格式,那么在苹果的操作系统上又是怎样的呢?在iOS(和Mac OS X)上,主要的可执行文件格式是Mach-O格式。本文就关于iOS上的可执行文件和Mach-O格式做一个简要整理。


Mach-O格式是iOS系统上应用程序运行的基础,了解Mach-O的格式,对于调试、自动化测试、安全都有意义。在了解二进制文件的数据结构以后,一切就都显得没有秘密。


0. Mach与Mach-O


这里先提醒大家一下,Mach不是Mac,Mac是苹果电脑Macintosh的简称,而Mach则是一种操作系统内核。Mach内核被NeXT公司的NeXTSTEP操作系统使用。在Mach上,一种可执行的文件格是就是Mach-O(Mach Object file format)。1996年,乔布斯将NeXTSTEP带回苹果,成为了OS X的内核基础。所以虽然Mac OS X是Unix的“后代”,但所主要支持的可执行文件格式是Mach-O。


iOS是从OS X演变而来,所以同样是支持Mach-O格式的可执行文件。


1. iOS可执行文件初探


作为iOS客户端开发者,我们比较熟悉的一种文件是ipa包(iPhone Application)。但实际上这只是一个变相的zip压缩包,我们可以把一个ipa文件直接通过unzip命令解压。


解压之后,会有一个Payload目录,而Payload里则是一个.app文件,而这个实际上又是一个目录,或者说是一个完整的App Bundle。


在这个目录中,里面体积最大的文件通常就是和ipa包同名的一个二进制文件。找到它,我们用file命令来看一下这个文件的类型:


XXX: Mach-O universal binary with 2 architectures
XXX (for architecture armv7): Mach-O executable arm
XXX (for architecture armv7s): Mach-O executable arm
由此看来,这是一个支持armv7和armv7s两种处理器架构的通用程序包,里面包含的两部分都是Mach-O格式。


对于一个二进制文件来讲,每个类型都可以在文件最初几个字节来标识出来,即“魔数”。比如PNG图片的最初几个字节是\211 P N G \r \n \032 \n (89 50 4E 47 0D 0A 1A 0A)。我们再来看下这个Mach-O universal binary的:


0000000 ca fe ba be 00 00 00 02 00 00 00 0c 00 00 00 09
没错,开始的4个字节是cafe babe,即“Cafe baby”。了解Java或者说class文件格式的同学可能会很熟悉,这也是.class文件开头的“魔数”,但貌似是Mach-O在更早的时候就是用了它。在OS X上,可执行文件的标识有这样几个魔数(也就是文件格式):


cafebabe


feedface


feadfacf


还有一个格式,就是以#!开头的脚本


cafebabe就是跨处理器架构的通用格式,feedface和feedfacf则分别是某一处理器架构下的Mach-O格式,脚本的就很常见了,比如#!/bin/bash开头的shell脚本。


这里注意一点是,feedface和cafebabe的字节顺序不同,我们可以用lipo把上面cafebabe的文件拆出armv7架构的,看一下开头的几个字节:


0000000 ce fa ed fe 0c 00 00 00 09 00 00 00 02 00 00 00


2. Mach-O格式


接下来我们再来看看这个Mach-O格式到底是什么样的格式。我们可以通过二进制查看工具查看这个文件的数据,结果发现,不是所有数据都是相连的,而是被分成了几个段落。


在一位叫做JOE SAVAGE的老兄发布的图片上来看,Mach-O的文件数据显现出来是这个样子的:


Hello-World-Hilbert-Visualisation-Structure.png


(图形化的Mach-O文件数据)


大家可以对数据的分布感受下。


虽然被五颜六色的标记出来,可能这还不是特别直接。再来引用苹果官方文档的示意图:


mach_o_segments.gif


(Mach-O文件格式基本结构)


从这张图上来看,Mach-O文件的数据主体可分为三大部分,分别是头部(Header)、加载命令(Load commands)、和最终的数据(Data)。


回过头来,我们再看上面那张图,也许就都明白了。黄色部分是头部、红色是加载命令、而其它部分则是被分割成Segments的数据。


3. Mach-O头部


这里,我们用otool来看下Mach-O的头部信息,得到:


      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedface      12          9  0x00          2    45       4788 0x00218085
更详细的,我们可以通过otool的V参数得到翻译版:


Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
   MH_MAGIC     ARM         V7  0x00     EXECUTE    45       4788   NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
前面几个字段的意义,上下对比就能看懂,我这里主要说下这样几个字段:


filetype,这个可以有很多类型,静态库(.a)、单个目标文件(.o)都可以通过这个类型标识来区分。


ncmds和sizeofcmds,这个cmd就是加载命令,ncmds就是加载命令的个数,而sizeofcmds就是所占的大小。


flags里包含的标记很多,比如TWOLEVEL是指符号都是两级格式的,符号自身+加上自己所在的单元,PIE标识是位置无关的。


4. 加载命令


上面头部中的数据已经说明了整个Mach-O文件的基本信息,但整个Mach-O中最重要的还要数加载命令。它说明了操作系统应当如何加载文件中的数据,对系统内核加载器和动态链接器起指导作用。一来它描述了文件中数据的具体组织结构,二来它也说明了进程启动后,对应的内存空间结构是如何组织的。


我们可以用otool -l xxx来看一个Mach-O文件的加载命令:


Load command 0
      cmd LC_SEGMENT
  cmdsize 56
  segname __PAGEZERO
   vmaddr 0x00000000
   vmsize 0x00004000
  fileoff 0
 filesize 0
  maxprot ---
 initprot ---
   nsects 0
    flags (none)
Load command 1
      cmd LC_SEGMENT
  cmdsize 736
  segname __TEXT
   vmaddr 0x00004000
   vmsize 0x00390000
  fileoff 0
 filesize 3735552
  maxprot r-x
 initprot r-x
   nsects 10
    flags (none)
Section
  sectname __text
   segname __TEXT
      addr 0x0000b0d0
      size 0x0030e7f4
上面这段是执行结果的一部分,是加载PAGE_ZERO和TEXT两个segment的load command。PAGE_ZERO是一段“空白”数据区,这段数据没有任何读写运行权限,方便捕捉总线错误(SIGBUS)。TEXT则是主体代码段,我们注意到其中的r-x,不包含w写权限,这是为了避免代码逻辑被肆意篡改。


我再提一个加载命令,LC_MAIN。这个加载指令,会声明整个程序的入口地址,保证进程启动后能够正常的开始整个应用程序的运行。


除此之外,Mach-O里还有LC_SYMTAB、LC_LOAD_DYLIB、LC_CODE_SIGNATURE等加载命令,大家可以去官方文档查找其含义。


至于Data部分,在了解了头部和加载命令后,就没什么特别可说的了。Data是最原始的编译数据,里面包含了Objective-C的类信息、常量等。


本文是对Mach-O文件格式的一个理解小结,希望能够抛砖引玉,帮助各位朋友把握可执行文件的主题脉络,进而解决各类问题。


参考:


THE NITTY GRITTY OF “HELLO WORLD” ON OS X
OS X ABI Mach-O File Format Reference
========

iOS APP可执行文件的组成

http://blog.cnbang.net/tech/2296/


iOS APP编译后,除了一些资源文件,剩下的就是一个可执行文件,有时候项目大了,引入的库多了,可执行文件很大,想知道这个可执行文件的构成是怎样,里面的内容都是些什么,哪些库占用空间较高,可以用以下方法勘察:
1.XCode开启编译选项Write Link Map File
XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置
linkmap
2.编译后,到编译目录里找到该txt文件,文件名和路径就是上述的Path to Link Map File
位于~/Library/Developer/Xcode/DerivedData/XXX-eumsvrzbvgfofvbfsoqokmjprvuh/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/
这个LinkMap里展示了整个可执行文件的全貌,列出了编译后的每一个.o目标文件的信息(包括静态链接库.a里的),以及每一个目标文件的代码段,数据段存储详情。
1
以伊书项目为例,在LinkMap里首先列出来的是目标文件列表:
# Object files:
[ 0] linker synthesized
[ 1] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator7.0.sdk/usr/lib/crt1.o
[ 2] /Users/bang/Library/Developer/Xcode/DerivedData/yishu-eyzgphknrrzpevagadjtwpzzeqag/Build/Intermediates/yishu.build/Debug-iphonesimulator/yishu.build/Objects-normal/i386/TKPFileInfo.o
...
[280] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANJob.o)
[281] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANWorker.o)
[282] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(MobClick.o)
[283] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANLaunch.o)
...
前面中括号里的是这个文件的编号,后面会用到,像项目里引用到静态链接库libMobClickLibrary.a里的目标文件都会在这里列出来。
2
接着是一个段表,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值)
# Sections:
# Address   Size     Segment   Section
0x00002740 0x00273890 __TEXT __text
0x00275FD0 0x00000ADA __TEXT __symbol_stub
0x00276AAC 0x00001222 __TEXT __stub_helper
0x00277CCE 0x00019D9E __TEXT __objc_methname
0x00291A70 0x00012847 __TEXT __cstring
0x002A42B7 0x00001FC1 __TEXT __objc_classname
0x002A6278 0x000046A7 __TEXT __objc_methtype
0x002AA920 0x000061CE __TEXT __ustring
0x002B0AF0 0x00000764 __TEXT __const
0x002B1254 0x000028B8 __TEXT __gcc_except_tab
0x002B3B0C 0x00004EBC __TEXT __unwind_info
0x002B89C8 0x0003662C __TEXT __eh_frame
0x002EF000 0x00000014 __DATA __program_vars
0x002EF014 0x00000284 __DATA __nl_symbol_ptr
0x002EF298 0x0000073C __DATA __la_symbol_ptr
0x002EF9E0 0x000030A4 __DATA __const
0x002F2A84 0x00000590 __DATA __objc_classlist
0x002F3014 0x0000000C __DATA __objc_nlclslist
0x002F3020 0x0000006C __DATA __objc_catlist
0x002F308C 0x000000D8 __DATA __objc_protolist
0x002F3164 0x00000008 __DATA __objc_imageinfo
0x002F3170 0x0002BC80 __DATA __objc_const
0x0031EDF0 0x00003A30 __DATA __objc_selrefs
0x00322820 0x00000014 __DATA __objc_protorefs
0x00322834 0x000006B8 __DATA __objc_classrefs
0x00322EEC 0x00000394 __DATA __objc_superrefs
0x00323280 0x000037C8 __DATA __objc_data
0x00326A48 0x000096D0 __DATA __cfstring
0x00330118 0x00001424 __DATA __objc_ivar
0x00331540 0x00006080 __DATA __data
0x003375C0 0x0000001C __DATA __common
0x003375E0 0x000018E8 __DATA __bss
首列是数据在文件的偏移位置,第二列是这一段占用大小,第三列是段类型,代码段和数据段,第四列是段名称。
每一行的数据都紧跟在上一行后面,如第二行__symbol_stub的地址0x00275FD0就是第一行__text的地址0x00002740加上大小0x00273890,整个可执行文件大致数据分布就是这样。
这里可以清楚看到各种类型的数据在最终可执行文件里占的比例,例如__text表示编译后的程序执行语句,__data表示已初始化的全局变量和局部静态变量,__bss表示未初始化的全局变量和局部静态变量,__cstring表示代码里的字符串常量,等等。
3
接着就是按上表顺序,列出具体的按每个文件列出每个对应字段的位置和占用空间
# Address Size File Name
0x00002740 0x0000003E [ 1] start
0x00002780 0x00000400 [ 2] +[TKPFileInfo parseWithDictionary:]
0x00002B80 0x00000030 [ 2] -[TKPFileInfo fileID]
...
同样首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应上述Object files列表,最后是名字。
例如第二行代表了文件序号为2(反查上面就是TKPFileInfo.o)的parseWithDictionary方法占用了1000byte大小。
使用
这个文件可以让你了解整个APP编译后的情况,也许从中可以发现一些异常,还可以用这个文件计算静态链接库在项目里占的大小,有时候我们在项目里链了很多第三方库,导致APP体积变大很多,我们想确切知道每个库占用了多大空间,可以给我们优化提供方向。LinkMap里有了每个目标文件每个方法每个数据的占用大小数据,所以只要写个脚本,就可以统计出每个.o最后的大小,属于一个.a静态链接库的.o加起来,就是这个库在APP里占用的空间大小。
========

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

http://www.cocoachina.com/industry/20131121/7392.html
当我们在Xcode中构建一个程序的时候,其中有一部分就是把源文件(.m和.h)文件转变成可执行文件。这个可执行文件包含了将会在CPU(iOS设备上的arm处理器或者你mac上的Intel处理器)运行的字节
“”
阅读器
iOS
当我们在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  
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  
我们都用过#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  
@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上程序的加载时间。
========

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号