dyld(the dynamic link editor)是苹果的动态连接器,是苹果操做系统的一个重要组成部分,当系统内核作好启动程序的准备工做以后,余下的工做会交给dyld来负责处理。那它存在的意义是什么?它又具体都负责作些什么呢?这一篇咱们一块儿来一探究竟。前方长篇预警~程序员
存在即合理,但咱们要弄清楚其合理性所在。先从可执行文件是如何由源码生成的提及。算法
先看下面这段代码:编程
#include<stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
复制代码
假设这段代码源文件为hello.c,咱们输入最简单的命令:$gcc hello.c
$./a.out
,那么终端会输出:Hello World,在这个过程当中,事实上通过了四个步骤:预处理、编译、汇编和连接。咱们来具体看每一步都作了些什么。bootstrap
预编译的主要处理规则以下:数组
结合上述规则,当咱们没法判断宏定义是否正确或者头文件是否包含时能够查看预编译后的文件来肯定问题,预编译的过程至关于以下命令:
$gcc -E hello.c -o hello.i
或
$cpp hello.c > hello.i
缓存
编译的过程就是把预处理完的文件进行一些列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,这个过程每每是咱们整个程序构建的核心部分,也是最复杂的部分之一,编译的具体步骤涉及到编译原理等内容,这里就不展开了。咱们使用命令:
$gcc -S hello.c -o hello.s
能够获得汇编输出文件hello.s。bash
对于 C 语言的代码来讲,这个预编译和编译的程序是 ccl,可是对于 C++ 来讲,对应的程序是 ccplus;Objective-C 的是 ccobjc;Java 是 jcl。因此实际上 gcc 这个命令只是这些后台程序的包装,它会根据不一样的参数要求去调用预编译编译程序 ccl、汇编器 as、连接器 ld。数据结构
汇编器是将汇编代码转变成机器能够执行的指令,每个汇编语句几乎都对应一条机器指令。因此汇编器的汇编过程相对于编译器来说比较简单,它没有复杂的语法,也没有语义,也不须要作指令优化,只是根据汇编指令和机器指令的对照表一一翻译就能够了,咱们使用命令:
$as hello.s -o hello.o
或
$gcc -c hello.s -o hello.o
来完成汇编,输出目标文件(Object File):hello.o。ide
连接是让不少人费解的一个过程,为何汇编器不直接输出可执行文件而是一个目标文件呢?连接过程到底包含了什么内容?为何要连接?函数
这就要扯一下计算机程序开发的历史了,最先的时候程序员是在纸带上用机器语言经过打孔来实现程序的,连汇编语言都没有,每当程序修改的时候,修改的指令后面的位置要相应的发生移动,程序员要人工计算每一个子程序或跳转的目标地址,这个过程叫重定位。很显然这样修改程序的代价随着程序的增大会变得遥不可及,而且很容易出错,因而有先驱发明了汇编语言,汇编语言使用接近人类的各类符号和标记来帮助记忆,更重要的是,这种符号使得人们从具体的指令地址中逐步解放出来,当人们使用这种符号命名子程序或者跳转目标之后,无论目标指令以前修改了多少指令致使目标指令的地址发生了变化,汇编器在每次汇编程序的时候都会从新计算目标指令的地址,而后把全部引用到该指令的指令修正到正确的地址,这个过程不须要人工参与。
有了汇编语言,生产力极大地提升了,随之而来的是软件的规模与日俱增,代码量快速膨胀,致使人们开始考虑将不一样功能的代码以必定的方式组织起来,使得更加容易阅读和理解,更便于往后修改和复用。天然而然的,咱们开始习惯用若干个变量和函数组成一个模块(好比类),而后用目录结构来组织这些源代码文件,在一个程序被多个模块分割之后,这些模块最终如何组合成一个单一的程序是需要解决的问题。这个问题归根结底是模块之间如何通讯的问题,也就是访问函数须要知道函数的地址,访问变量须要知道变量的地址,这两个问题都是经过模块间符号的引用的方式来解决。这个模块间符号引用拼接的过程就是连接。
连接的主要内容就是把各个模块之间相互引用的部分处理好,使得各个模块之间可以正确地衔接。本质上跟前面描述的“程序员人工调整地址”没什么区别,只不过现代的高级语言的诸多特性和功能,使得编译器、连接器更为复杂,功能更强大。连接过程包括了地址和空间分配、符号决议(也叫“符号/地址绑定”,“决议”更倾向于静态连接,而“绑定”更倾向于动态连接,即适用范围的区别)和重定位,连接器将通过汇编器编译成的全部目标文件和库进行连接造成最终的可执行文件,而最多见的库就是运行时库(RunTime Library),它是支持程序运行的基本函数的集合。库其实就是一组最经常使用的代码编译成目标文件后的打包存放。
知道了可执行文件是如何生成的,咱们再来看看它又是如何被装载进系统中运行起来的。
装载与动态连接其实内容特别多,不少细节须要对计算机底层有很是扎实的理解,鉴于目前个人能力尚浅,这里只作粗略的介绍,推荐有兴趣的同窗购买《程序员的自我修养--连接、装载与库》这本书了解更多细节。
可执行文件(程序)是一个静态的概念,在运行以前它只是硬盘上的一个文件;而进程是一个动态的概念,它是程序运行时的一个过程,咱们知道每一个程序被运行起来后,它会拥有本身独立的虚拟地址空间,这个地址空间大小的上限是由计算机的硬件(CPU的位数)决定的,好比32位的处理器理论最大虚拟空间地址为0~2^32-1。即0x00000000~0xFFFFFFFF,固然,咱们的程序运行在系统上时是不可能任意使用所有的虚拟空间的,操做系统为了达到监控程序运行等一系列目的,进程的虚拟空间都在操做系统的掌握之中,且在操做系统中会同时运行着多个进程,它们彼此之间的虚拟地址空间是隔离的,若是进程访问了操做系统分配给该进程之外的地址空间,会被系统当作非法操做而强制结束进程。
将硬盘上的可执行文件映射到虚拟内存中的过程就是装载,但内存是昂贵且稀有的,因此将程序执行时所需的指令和数据所有装载到内存中显然是行不通的,因而人们研究发现了程序运行时是有局部性原理的,能够只将最经常使用的部分驻留在内存中,而不太经常使用的数据存放在磁盘里,这也是动态装载的基本原理。覆盖装入和页映射就是利用了局部性原理的两种经典动态装载方法,前者在发明虚拟内存以前使用比较普遍 ,如今基本已经淘汰,主要使用页映射。装载的过程也能够理解为进程创建的过程,操做系统只须要作如下三件事情:
前面咱们在生成可执行文件时说的连接是静态连接。最后一步是将通过汇编后的全部目标文件与库进行连接造成可执行文件,这里的提到的库,包括了不少运行时库。运行时库一般是支持程序运行的基本函数的集合,也就意味着每一个进程都会用到它,若是每个可执行文件都将其打包进本身的可执行文件,都用静态连接的方式,虽然原理上更容易理解,可是这种方式对计算机的内存和磁盘的空间浪费很是严重!在如今的Linux系统中,一个普通的程序会使用到的C语言静态库至少在1M以上,若是系统中有2000个这样的程序在运行,就要浪费将近2G的空间。为了解决这个问题,把运行时库的连接过程推迟到了运行时在进行,这就是动态链接(Dynamic Linking)的基本思想。动态连接的好处有如下几点:
至此,终于说回了咱们今天的主角:dyld,如今我们知道了它存在的意义——动态加载的支持。
如今,咱们理解了为何须要动态连接,dyld做为苹果的动态连接器,但本质上dyld也是一个共享对象:
上述第一个条件在编写动态连接器时能够人为的控制,第二个条件要求动态连接器在启动时必须有一段代码能够在得到自身的重定位表和符号表的同时又不能用到全局和静态变量,甚至不能调用函数,这样的启动代码被称为自举(Bootstrap)。当操做系统将进程控制权交给动态连接器时,自举代码开始执行,它会找到动态连接器自己的重定位入口(具体过程和原理暂未深究),进而完成其自身的重定位,在此以后动态连接器中的代码才能够开始使用本身的全局、静态变量和各类函数了。
完成基本的自举之后,动态连接器将可执行文件和连接器自己的符号表合并为一个,称为全局符号表。而后连接器开始寻找可执行文件所依赖的共享对象,若是咱们把依赖关系看做一个图的话,那么这个装载过程就是一个图的便利过程,连接器可能会使用深度优先或者广度优先也可能其余的算法来遍历整个图,比较常见的算法都是广度优先的。
每当一个新的共享对象被装载进来,它的符号表会被合并到全局符号表中,装载完毕后,连接器开始从新遍历可执行文件和共享对象的重定位表,将每一个须要从新定位的位置进行修正,这个过程与静态连接的重定位原理基本相同。重定位完成以后,动态连接器会开始共享对象的初始化过程,但不会开始可执行文件的初始化工做,这将由程序初始化部分的代码负责执行。当完成了重定位和初始化以后,全部的准备工做就宣告完成了,这时动态连接器就如释重负,将进程的控制权交给程序的入口而且开始执行。
咱们来经过分析dyld的源码验证上述过程:
从图中frame9的汇编信息中,你必定发现了在dyld的入口函数__dyld_start
里出现了dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*)
的函数调用,那这段代码是干吗的呢?上源码:
在dyld::_main中主要作了如下几件事
这一篇咱们从dyld出发,将程序从编译到装载的整个过程串了一遍,并结合分析了dyld的源码,这些资源都是开源的,有兴趣必定要本身去本身啃一下,经过看苹果对数据结构的使用和设计,仍是有不少启发的。在后续的逆向学习中,这一篇的研究或许能让我不只知其然,并且知其因此然。路过的大神还望多多指教~
下篇速递:fishhook的实现原理浅析。