源码地址

源码分析

上一篇博客已经介绍到dyld被加载,以及设置dyld的入口点为__dyld_start,这篇博客就从__dyld_start开始看下应用执行main之前都经历了哪些过程,在开始之前我们需要先对iOS中对动态库有一定对了解,如果你之前接触过其他平台你可能会听说过Windows系统下的DLL文件,Linux 系统下的so文件,iOS系统则是使用dylib作为动态库,至于iOS动态库与静态库的对比我们会专门用一篇博客进行介绍,dylib和普通的可执行文件在文件结构上大同小异,都是Mach O格式文件,只不过在文件类型上,一个是MH_DYLIB,一个是MH_EXECUTE,但是动态库不能像可执行文件那样直接运行,而是需要通过dyld加载到内存与主程序链接后才可以执行,在主程序启动后会通过dyld将它所依赖的全部动态链接库链接起来,最终组成可运行的应用。

如果想查看某个可执行文件所依赖的动态链接库可以通过命令:jtool -L IDLFundation 来查看,我们接下来就来看下整个过程:

__dyld_start:
popq %rdi # param1 = mh of app
pushq $0 # push a zero for debugger end of frames marker
movq %rsp,%rbp # pointer to base of kernel frame
andq $-16,%rsp # force SSE alignment
subq $16,%rsp # room for local variables

//调用dyldbootstrap::start
# call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
movl 8(%rbp),%esi # param2 = argc into %esi
leaq 16(%rbp),%rdx # param3 = &argv[0] into %rdx
movq __dyld_start_static(%rip), %r8
leaq __dyld_start(%rip), %rcx
subq %r8, %rcx # param4 = slide into %rcx
leaq ___dso_handle(%rip),%r8 # param5 = dyldsMachHeader
leaq -8(%rbp),%r9
call __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
movq -8(%rbp),%rdi
cmpq $0,%rdi
jne Lnew

在__dyld_start会通过汇编调用dyldbootstrap::start方法

uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], 
intptr_t slide, const struct macho_header* dyldsMachHeader,
uintptr_t* startGlue)
{
//.........
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

dyldbootstrap::start方法中会进入dyld的入口main函数。

_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;

// 设置ImageLoader的context
setContext(mainExecutableMH, argc, argv, envp, apple);
//......
try {
//...
// instantiate ImageLoader for main executable
// 加载Image(代表镜像)
// 为主程序初始化imageLoader,用于后续的链接等过程
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.processIsRestricted = sProcessIsRestricted;
gLinkContext.processRequiresLibraryValidation = sProcessRequiresLibraryValidation;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);

//.....
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
// 将共享库加载到内存中
mapSharedCache();
// .......
// 如果有插入的库,加载
// load any inserted libraries
if( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
//// 加载环境变量DYLD_INSERT_LIBRARIES中的动态库,使用loadInsertedDylib进行加载
loadInsertedDylib(*lib);//把环境变量DYLD_INSERT_LIBRARIES 中的动态库调用loadInsertedDylib进行加载
}
// record count of inserted libraries so that a flat search will look at
// inserted libraries, then main, then others.
sInsertedDylibCount = sAllImages.size()-1;

// link main executable
// 链接主程序
gLinkContext.linkingMainExecutable = true;
//继续回到dyld的_main函数中来,继续加载根目录Framework目录下的其他动态库. 加载完所有的dylibs之后,每个dylib之间还是没有关联,
//不知道怎么调用,这时候就该进行link操作了,link操作分成rebase和binding辆部分.

//ASLR(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,
//通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。
//据研究表明ASLR可以有效的降低缓冲区溢出攻击的成功率,如今Linux、FreeBSD、Windows等主流操作系统都已采用了该技术。

//rebase: 由于ASLR访问地址被随机化,所以rebase在动态库内部进行修正访问地址,并创建访问地址存储在__DATA段,这个期间可能会产生缺页并进行IO操作
//binding: 主要负责动态库之间的调用地址的修正和创建

//initialize
//这里就比较简单了, 这个时候各个库都已经load完成,访问地址指针也已经修正过,就可以初始化所有dylib了, 会调用C++的初始化构造方法,
//也就是用__attribute__((constructor))修饰的方法,被修饰的方法都会在main()方法之前调用
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}

// link any inserted libraries
// 链接任何插入的库
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
image->setNeverUnloadRecursive();
}
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing();
}
}

// <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
for (int i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image->inSharedCache() )
continue;
image->registerInterposing();
}

// apply interposing to initial set of images
for(int i=0; i < sImageRoots.size(); ++i) {
sImageRoots[i]->applyInterposing(gLinkContext);
}
gLinkContext.linkingMainExecutable = false;

sMainExecutable->weakBind(gLinkContext);

initializeMainExecutable();

// 寻找主程序的入口
// find entry point for main executable
result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// 主可执行文件使用lc_main,需要返回libdyld.dylib中的glue
//调用main()
//当执行完dyld::_main方法之后,返回了main()函数地址,这个时候所有初始化工作都已经完成了,正式进入Objc声明周期
// main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getMain();
*startGlue = 0;
}
}
catch(const char* message) {
syncAllImages();
halt(message);
}
catch(...) {
dyld::log("dyld: launch failed\n");
}
return result;
}

在main函数中会执行如下操作:

  1. 将主程序初始化为imageLoader,用于后续的链接等操作
  2. 加载共享库到内存
  3. 加载插入的动态库
  4. 链接主程序,链接插入库
  5. 初始化主程序
  6. 寻找主程序入口点

接下来我们将按照上面的顺序进行展开:

将主程序初始化为imageLoader

在dyld main方法中会通过instantiateFromLoadedImage创建ImageLoader,其中第一个参数传入的是mainExecutableMH为主程序的Mach O Header,有了mainExecutableMH,dyld就可以从头开始遍历整个Mach O文件信息了。在开始创建ImageLoader之前会先调用isCompatibleMachO来查看mainExecutableMH中的cputype与cpusubtype是否与当前设备兼容,只有兼容的情况下才会继续创建ImageLoader,创建后的ImageLoader会通过addImage添加到sAllImages,然后调用addMappedRange()申请内存,更新主程序镜像映射的内存区。

static ImageLoader* instantiateFromLoadedImage(const macho_header* mh, uintptr_t slide, const char* path) {
// 检测mach-o header的cputype与cpusubtype是否与当前系统兼容
if ( isCompatibleMachO((const uint8_t*)mh, path) ) {
//初始化镜像加载器
ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext);
addImage(image);
return image;
}
throw "main executable not a known format";
}

我们看下ImageLoader的创建方法:

ImageLoader* ImageLoaderMachO::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, const LinkContext& context) {
bool compressed;
unsigned int segCount;
unsigned int libCount;
const linkedit_data_command* codeSigCmd;
const encryption_info_command* encryptCmd;
// sniffLoadCommands主要获取加载命令中compressed的值(压缩还是传统)以及segment的数量、libCount(需要加载的动态库的数量)
sniffLoadCommands(mh, path, false, &compressed, &segCount, &libCount, context, &codeSigCmd, &encryptCmd);
// instantiate concrete class based on content of load commands
if ( compressed )
return ImageLoaderMachOCompressed::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
else
#if SUPPORT_CLASSIC_MACHO
return ImageLoaderMachOClassic::instantiateMainExecutable(mh, slide, path, segCount, libCount, context);
#else
throw "missing LC_DYLD_INFO load command";
#endif
}

instantiateMainExecutable会首先调用sniffLoadCommands从加载信息中获取到当前的主程序是Compressed还是Classic类型的,以及segment的数量,需要加载的动态库的数量。然后根据主程序的类型来创建ImageLoaderMachOCompressed或者ImageLoaderMachOClassic类型的ImageLoader.

void ImageLoaderMachO::sniffLoadCommands(const macho_header* mh, const char* path, bool inCache, bool* compressed,
unsigned int* segCount, unsigned int* libCount, const LinkContext& context,
const linkedit_data_command** codeSigCmd,
const encryption_info_command** encryptCmd)
{
//......
for (uint32_t i = 0; i < cmd_count; ++i) {
uint32_t cmdLength = cmd->cmdsize;
struct macho_segment_command* segCmd;
//.....
switch (cmd->cmd) {
case LC_DYLD_INFO:
case LC_DYLD_INFO_ONLY:
*compressed = true;
break;
case LC_SEGMENT_COMMAND:
segCmd = (struct macho_segment_command*)cmd;
//.........
// ignore zero-sized segments
if ( segCmd->vmsize != 0 )
*segCount += 1;
//......
break;
//.....
case LC_LOAD_DYLIB:
case LC_LOAD_WEAK_DYLIB:
case LC_REEXPORT_DYLIB:
case LC_LOAD_UPWARD_DYLIB:
*libCount += 1;
break;
case LC_CODE_SIGNATURE:
*codeSigCmd = (struct linkedit_data_command*)cmd; // only support one LC_CODE_SIGNATURE per image
break;
case LC_ENCRYPTION_INFO:
case LC_ENCRYPTION_INFO_64:
*encryptCmd = (struct encryption_info_command*)cmd; // only support one LC_ENCRYPTION_INFO[_64] per image
break;
}
cmd = nextCmd;
}

//.......

// fSegmentsArrayCount is only 8-bits
if ( *segCount > 255 )
dyld::throwf("malformed mach-o image: more than 255 segments in %s", path);

// fSegmentsArrayCount is only 8-bits
if ( *libCount > 4095 )
dyld::throwf("malformed mach-o image: more than 4095 dependent libraries in %s", path);

if ( needsAddedLibSystemDepency(*libCount, mh) )
*libCount = 1;
}

sniffLoadCommands 会根据Mach 文件中是否有LC_DYLD_INFOLC_DYLD_INFO_ONLY来判断当前Mach O文件是否是compressed的,并且获取segment 数量以及所依赖的动态库数量libCount,还有代码签名命令codeSigCmd以及加密命令encryptCmd。我们这里以compressed类型作为例子。那么上面返回的就是ImageLoaderMachOCompressed,

ImageLoaderMachOCompressed* ImageLoaderMachOCompressed::instantiateMainExecutable(const macho_header* mh, uintptr_t slide, const char* path, 
unsigned int segCount, unsigned int libCount, const LinkContext& context)
{
ImageLoaderMachOCompressed* image = ImageLoaderMachOCompressed::instantiateStart(mh, path, segCount, libCount);
//.....
image->disableCoverageCheck();
image->instantiateFinish(context);
image->setMapped(context);

//....
return image;
}

加载共享库到内存

dyld加载的时候会启动共享缓存技术对这个过程进行优化,共享缓存会在进程启动的时候被dyld映射到内存中,不同App间访问的共享库最终都会映射到同一块物理内存,之后在加载任何Mach O镜像的时候,都会先去检查该Mach O镜像以及所需的动态库文件是否已经存在在共享缓存中,如果存在则直接将它在共享内存中的内存地址映射到进程的内存地址空间,从而加快动态库的加载的速度,对启动性能会有较大的提高。在Mac OS系统中,动态库共享缓存以文件的形式存放在/var/db/dyld目录下,我们需要根据当前设备的CPU架构类型去匹配要打开的缓存文件,接着读取缓存文件的前8192字节,解析并加载缓存头dyld_cache_header信息到内存,我们可以通过/usr/bin/update_dyld_shared_cache来更新共享库。

static void mapSharedCache()
{
uint64_t cacheBaseAddress = 0;
// 检查当前共享缓存是否已经映射到共享区域
if ( _shared_region_check_np(&cacheBaseAddress) == 0 ) {
/// 共享库已经被映射到内存中
sSharedCache = (dyld_cache_header*)cacheBaseAddress;
// 检查共享库的兼容性,如果已经映射到内存中的共享库但是不兼容,这时候我们将忽略它
if ( strcmp(sSharedCache->magic, magic) != 0 ) {
sSharedCache = NULL;
//.....
}
//.........
}
else {
// 如果共享库没有加载到内存中,进行加载
int fd = openSharedCacheFile();
if ( fd != -1 ) {
uint8_t firstPages[8192];
// 获取共享库文件的句柄,然后进行读取解析
if ( ::read(fd, firstPages, 8192) == 8192 ) {
dyld_cache_header* header = (dyld_cache_header*)firstPages;
//共享缓存合法性检查
if ( strcmp(header->magic, magic) == 0 ) {
//.......
// 校验缓存文件的完整性
//.......
if ( goodCache ) {
//.......
// 将共享缓存映射到共享区域
if (_shared_region_map_and_slide_np(fd, mappingCount, mappings, codeSignatureMappingIndex, cacheSlide, slideInfo, slideInfoSize) == 0) {
// successfully mapped cache into shared region
sSharedCache = (dyld_cache_header*)mappings[0].sfm_address;
sSharedCacheSlide = cacheSlide;
dyld::gProcessInfo->sharedCacheSlide = cacheSlide;
if ( header->mappingOffset >= 0x68 ) {
memcpy(dyld::gProcessInfo->sharedCacheUUID, header->uuid, 16);
}
}
else {
//........
}
}
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file is invalid\n");
}
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file cannot be read\n");
}
close(fd);
}
else {
if ( gLinkContext.verboseMapping )
dyld::log("dyld: shared cached file cannot be opened\n");
}
}
//......
// tell gdb where the shared cache is
//........
}

加载插入的动态库

插入的动态库会存储在DYLD_INSERT_LIBRARIES,在这时候会遍历DYLD_INSERT_LIBRARIES环境变量中指定的动态库列表,并调用loadInsertedDylib()将其加载。

if( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
// 加载环境变量DYLD_INSERT_LIBRARIES中的动态库,使用loadInsertedDylib进行加载
loadInsertedDylib(*lib);//把环境变量DYLD_INSERT_LIBRARIES 中的动态库调用loadInsertedDylib进行加载
}
sInsertedDylibCount = sAllImages.size()-1;

loadInsertedDylib会调用load方法完成加载工作。load方法会先调用loadPhase0尝试从文件加载,然后一直从loadPhase0一直到loadPhase6,查找动态库的路径,每个阶段都会为下一阶段生成搜索的路径。整个查找顺序会顺着: ****DYLD_ROOT_PATH -> LD_LIBRARY_PATH -> DYLD_FRAMEWORK_PATH -> raw path - >DYLD_FALLBACK_LIBRARY_PATH **** 如果找到则调用ImageLoaderMachO::instantiateFromFile来实例化一个ImageLoader,之后调用checkandAddImage验证映像并将其加入到全局映像列表中。如果loadPhase0返回空,表示在路径中没有找到动态库,就尝试从共享缓存中查找,找到就调用ImageLoaderMachO::instantiateFromCache()从缓存中加载,否则就抛出没找到映像的异常。


ImageLoader* load(const char* path, const LoadContext& context)
{
//.....
// 尝试所有路径排列并检查现有加载的镜像
// try all path permutations and check against existing loaded images
ImageLoader* image = loadPhase0(path, orgPath, context, NULL);
if ( image != NULL ) {
return image;
}

//......
image = loadPhase0(path, orgPath, context, &exceptions);
if ( (image == NULL) && cacheablePath(path) && !context.dontLoad ) {
//......
if ( (myerr == ENOENT) || (myerr == 0) ) {
// see if this image is in shared cache
//......
if ( findInSharedCacheImage(resolvedPath, false, NULL, &mhInCache, &pathInCache, &slideInCache) ) {
//......
image = ImageLoaderMachO::instantiateFromCache(mhInCache, pathInCache, slideInCache, stat_buf, gLinkContext);
image = checkandAddImage(image, context);
}
}
}
}
static ImageLoader* loadPhase6(int fd, const struct stat& stat_buf, const char* path, const LoadContext& context)
{
//.......
// try mach-o loader
if ( shortPage )
throw "file too short";
if ( isCompatibleMachO(firstPage, path) ) {
// 只有MH_BUNDLE,MH_DYLIB,以及一些MH_EXECUTE 才能被动态加载
switch ( ((mach_header*)firstPage)->filetype ) {
case MH_EXECUTE:
case MH_DYLIB:
case MH_BUNDLE:
break;
default:
throw "mach-o, but wrong filetype";
}
//实例化镜像
ImageLoader* image = ImageLoaderMachO::instantiateFromFile(path, fd, firstPage, fileOffset, fileLength, stat_buf, gLinkContext);
// validate
return checkandAddImage(image, context);
}
//.....
}

链接主程序

link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
//......
// 4.link any inserted libraries
// 链接任何插入的库
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
image->setNeverUnloadRecursive();
}
//......
}
//......
gLinkContext.linkingMainExecutable = false;
sMainExecutable->weakBind(gLinkContext);
void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths)
{
//......
//递归加载所有依赖库进内存。
this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths);
context.notifyBatch(dyld_image_state_dependents_mapped);

//.....
uint64_t t1 = mach_absolute_time();
context.clearAllDepths();
this->recursiveUpdateDepth(context.imageCount());

uint64_t t2 = mach_absolute_time();
//递归对自己以及依赖库进行复基位操作。在以前,程序每次加载其在内存中的堆栈基地址都是一样的,这意味着你的方法,变量等地址每次都一样的,这使得程序很不安全,后面就出现ASLR(Address space layout randomization,地址空间配置随机加载),
//程序每次启动后地址都会随机变化,这样程序里所有的代码地址都是错的,需要重新对代码地址进行计算修复才能正常访问
// 递归修正自己和依赖库的基地址,因为ASLR的原因,需要根据随机slide修正基地址
this->recursiveRebase(context);
context.notifyBatch(dyld_image_state_rebased);

uint64_t t3 = mach_absolute_time();
//对库中所有nolazy的符号进行bind,一般的情况下多数符号都是lazybind的,他们在第一次使用的时候才进行bind。
// recursiveBind对于noLazy的符号进行绑定,lazy的符号会在运行时动态绑定
this->recursiveBind(context, forceLazysBound, neverUnload);

uint64_t t4 = mach_absolute_time();
if ( !context.linkingMainExecutable )
this->weakBind(context);
uint64_t t5 = mach_absolute_time();

context.notifyBatch(dyld_image_state_bound);
//........
}

recursiveLoadLibraries会先获取当前镜像所需要的库列表,然后会先从缓存库中加载所需要的库,如果没在缓存库中找到则调用loadLibrary进行加载,loadLibrary 实际上调用的是之前介绍的load方法。将依赖的库加载进来并装载。recursiveUpdateDepth 会对镜像及其依赖库进行排序。

接下来我们看下recursiveRebase

void ImageLoader::recursiveRebase(const LinkContext& context)
{
if ( fState < dyld_image_state_rebased ) {
// break cycles
fState = dyld_image_state_rebased;
try {
// rebase lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL )
dependentImage->recursiveRebase(context);
}
// rebase this image
doRebase(context);
// notify
context.notifySingle(dyld_image_state_rebased, this);
}
//......
}
}

在之前的步骤中我们都在完成库的加载,但是这些dylibs之间是没有关联的,需要rebase,binding对地址修正,不知道大家看过高达之类的动画片没有,在变身过程中装备会从四面八方飞过来,然后会一个个安装到身上,嗯,link就是这个过程,动态库中的地址是相对的,因为它需要保证它内部逻辑的独立性,同时为了降低缓冲区溢出攻击的成功率主流的操作系统都会采用ASLR(Address space layout randomization)技术,它通过对堆、栈、共享库映射等线性区布局的随机化来增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。

****Rebase:在镜像内部调整指针的指向 ****

就像上面提到的未加载到内存的动态库镜像里面的地址都是相对的,是距第一个Segement的偏移量,而当加载到内存中的时候,起始地址就是申请的内存的起始地址,就不再是0,那么如何再能够找到这些引用的正确内存位置呢?这个就是rebase需要解决的问题,rebase这一步就是将相对地址调整为绝对地址,以及修正ASLR带来的地址不确定性:在没有使用ASLR技术之前,动态库加载的时候会把 dylib 加载到指定地址,所有指针和数据对于代码来说都是确定的,使用了ASLR后每次加载都会将dylib 加载到新的随机地址,这个随机的地址跟代码和数据指向的旧地址会有偏差,dyld 在rebase这个阶段需要修正这个偏差,做法就是将 dylib 内部的指针地址都加上这个偏移量,然后重复不断地对 __DATA 段中需要 rebase 的指针加上这个偏移量。也就是说经过rebase处理后镜像内部在内存中的映射已经确定下来了,不再是相对于动态库内部的相对地址,并创建访问地址存储在__DATA段。总的来说经过rebase处理后我们会在内存中获得一个有明确入口的,并且内部地址明确的动态库,但是对其他外部依赖库调用的地址在这步还没修正,需要在binding阶段进行处理。我们先继续往下看:

recursiveRebase 里面调用的是doRebase,而doRebase会调用ImageLoaderMachOCompressed::rebase

void ImageLoaderMachO::doRebase(const LinkContext& context)
{
//.......
// do actual rebasing
this->rebase(context);
}
void ImageLoaderMachOCompressed::rebase(const LinkContext& context)
{
//......
try {
//.....
bool done = false;
while ( !done && (p < end) ) {
uint8_t immediate = *p & REBASE_IMMEDIATE_MASK;
uint8_t opcode = *p & REBASE_OPCODE_MASK;
++p;
switch (opcode) {
case REBASE_OPCODE_DONE:
done = true;
break;
case REBASE_OPCODE_SET_TYPE_IMM:
type = immediate;
break;
case REBASE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB:
segmentIndex = immediate;
//.....
address = segActualLoadAddress(segmentIndex) + read_uleb128(p, end);
segmentEndAddress = segActualEndAddress(segmentIndex);
break;
case REBASE_OPCODE_ADD_ADDR_ULEB:
address += read_uleb128(p, end);
break;
case REBASE_OPCODE_ADD_ADDR_IMM_SCALED:
address += immediate*sizeof(uintptr_t);
break;
case REBASE_OPCODE_DO_REBASE_IMM_TIMES:
for (int i=0; i < immediate; ++i) {
if ( address >= segmentEndAddress )
throwBadRebaseAddress(address, segmentEndAddress, segmentIndex, start, end, p);
rebaseAt(context, address, slide, type);
address += sizeof(uintptr_t);
}
fgTotalRebaseFixups += immediate;
break;
case REBASE_OPCODE_DO_REBASE_ULEB_TIMES:
count = read_uleb128(p, end);
for (uint32_t i=0; i < count; ++i) {
rebaseAt(context, address, slide, type);
address += sizeof(uintptr_t);
}
fgTotalRebaseFixups += count;
break;
case REBASE_OPCODE_DO_REBASE_ADD_ADDR_ULEB:
rebaseAt(context, address, slide, type);
address += read_uleb128(p, end) + sizeof(uintptr_t);
++fgTotalRebaseFixups;
break;
case REBASE_OPCODE_DO_REBASE_ULEB_TIMES_SKIPPING_ULEB:
count = read_uleb128(p, end);
skip = read_uleb128(p, end);
for (uint32_t i=0; i < count; ++i) {
rebaseAt(context, address, slide, type);
address += skip + sizeof(uintptr_t);
}
fgTotalRebaseFixups += count;
break;
default:
dyld::throwf("bad rebase opcode %d", *p);
}
}
}
catch (const char* msg) {
//......
}
CRSetCrashLogMessage2(NULL);
}

在ImageLoaderMachOCompressed::rebase方法中就是对这些地址进行修正,从而获得明确的绝对地址。

接下来我们看下Bind 过程的代码recursiveBind

void ImageLoader::recursiveBind(const LinkContext& context, bool forceLazysBound, bool neverUnload)
{
// Normally just non-lazy pointers are bound immediately.
// The exceptions are:
// 1) DYLD_BIND_AT_LAUNCH will cause lazy pointers to be bound immediately
// 2) some API's (e.g. RTLD_NOW) can cause lazy pointers to be bound immediately
if ( fState < dyld_image_state_bound ) {
// break cycles
fState = dyld_image_state_bound;
try {
// bind lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL )
dependentImage->recursiveBind(context, forceLazysBound, neverUnload);
}
// bind this image
this->doBind(context, forceLazysBound);
// ......
context.notifySingle(dyld_image_state_bound, this);
}
catch (const char* msg) {
//......
}
}
}
void ImageLoaderMachOCompressed::doBind(const LinkContext& context, bool forceLazysBound)
{
CRSetCrashLogMessage2(this->getPath());

// if prebound and loaded at prebound address, and all libraries are same as when this was prebound, then no need to bind
// note: flat-namespace binaries need to have imports rebound (even if correctly prebound)
if ( this->usablePrebinding(context) ) {
// don't need to bind
}
else {
#if TEXT_RELOC_SUPPORT
// if there are __TEXT fixups, temporarily make __TEXT writable
if ( fTextSegmentBinds )
this->makeTextSegmentWritable(context, true);
#endif

// run through all binding opcodes
eachBind(context, &ImageLoaderMachOCompressed::bindAt);
#if TEXT_RELOC_SUPPORT
// if there were __TEXT fixups, restore write protection
if ( fTextSegmentBinds )
this->makeTextSegmentWritable(context, false);
#endif
// if this image is in the shared cache, but depends on something no longer in the shared cache,
// there is no way to reset the lazy pointers, so force bind them now
if ( forceLazysBound || fInSharedCache )
this->doBindJustLazies(context);

// this image is in cache, but something below it is not. If
// this image has lazy pointer to a resolver function, then
// the stub may have been altered to point to a shared lazy pointer.
if ( fInSharedCache )
this->updateOptimizedLazyPointers(context);

// tell kernel we are done with chunks of LINKEDIT
if ( !context.preFetchDisabled )
this->markFreeLINKEDIT(context);
}
// set up dyld entry points in image
// do last so flat main executables will have __dyld or __program_vars set up
this->setupLazyPointerHandler(context);
CRSetCrashLogMessage2(NULL);
}

****Bind:将指针指向镜像外部的内容 ****

我们前面提到了经过rebase处理后我们将会在内存中获取该库的一个明确的入口地址,并且库内部的相对地址会被确定为一个明确的地址,但是对其他库引用的地方还是没明确,这部分就需要Binding阶段进行处理了,Binding阶段主要的任务就是根据被导出符号重定位表项,去符号表中找出符号的基本信息,再去其他库符号表中resolve符号,将resolve结果bind到需要重定位地址上。说句人话就是,在其他库的符号表中去查找明确地址,然后在当前动态库中使用这个地址。一般我们使用另一个动态库的某个方法的时候会有一个间接引用,这个地址中存放的就是我们需要跳转的地址,在binding阶段会将其他库中查找的地址这需要很多计算,去符号表里查找。找到后会将内容存储到 __DATA 段中的那个指针中,下次调用的时候就会从这个地方去拿这个地址去跳转到要调用其他动态库的函数地址。

这里还有两种特殊的绑定:

Lazy Bind:

我们启动优化中常常会考虑将某些动态依赖库改为静态依赖,虽然静态依赖会增大包的体积,但是会改善启动缓慢的问题,上面看到Bind的过程,发现Bind的过程需要查到对应的符号再进行bind. 如果在启动的时候,所有的符号都立即Bind成功,那么势必拖慢启动速度,那么我们可以将某些不必要的符号绑定延后吗?嗯,肯定可以的,Lazy Bind就是第一次调用到才会进行真正的Bind.这里其实用到dyld_stub_binder,在需要绑定的时候会将调用信息传递给它,由它来找到最终要跳转的地址,从而完成绑定。

Weak Bind:

Weak Bind这一步是将相同的弱符号统一化,如果有强符号则统一成强符号,否则统一成按装载顺序的首个弱符号,在这个过程中会去寻找,找到一个相同的符号后,如果是强符号则不需要再查找,否则从装载顺序找到首个弱符号地址,将所有库中的相同符号都覆盖为该地址。

初始化主程序:

void initializeMainExecutable()
{

// 初始化任意插入的dylibs
// run initialzers for any inserted dylibs
if ( rootCount > 1 ) {
//这里需要注意的是sImageRoots 中的第一个变量是主程序镜像, 因此这里初始化的时候需要跳过第一个数据, 对其他后面插入的dylib进行调用ImageLoader::runInitializers进行初始化
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// 最后运行主程序的初始化器
// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

//.........
}

我们看到initializeMainExecutable方法中会先对插入的动态链接库进行初始化,然后再对主程序进行初始化,初始化都是调用runInitializers

void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
//......
//处理初始化器
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized);
//......
}

runInitializers方法中就做两件事情,一个就是调用processInitializers,以及在初始化结束发出通知,告诉监听者,当前已经初始化完毕。

void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
//在镜像列表中的所有镜像进行递归初始化
for (uintptr_t i=0; i < images.count; ++i) {
images.images[i]->recursiveInitialization(context, thisThread, timingInfo, ups);
}
//......
}
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);

if ( fState < dyld_image_state_dependents_initialized-1 ) {
//......
// let objc know we are about to initialize this image
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
// 通知 runtime, 当前状态发生变化 -- image的依赖已经完全加载. 如果在runtime中注册了状态监听,当状态发送变化时, 会触发回调函数.
context.notifySingle(dyld_image_state_dependents_initialized, this);

//递归的调用当前image的依赖的dylib动态库的初始化函数进行初始化
// initialize this image
bool hasInitializers = this->doInitialization(context);

// let anyone know we finished initializing this image
fState = dyld_image_state_initialized;
oldState = fState;
// 通知runtime, 当前镜像初始化完成
context.notifySingle(dyld_image_state_initialized, this);
//.....
}
recursiveSpinUnLock();
}
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
// 初始化镜像
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
return (fHasDashInit || fHasInitializers);
}
void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
if ( fHasDashInit ) {
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_ROUTINES_COMMAND:
Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
//.......
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}

doImageInit方法主要获取Mach O的init方法的地址并调用它,完成初始化操作。

void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
if ( fHasInitializers ) {
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
//初始化器
Initializer* inits = (Initializer*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);
for (size_t i=0; i < count; ++i) {
//......
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
}
}
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}

doModInitFunctions方法主要获取Mach O的static initializer的地址并调用

这里需要注意的是这里的initalizer并非指的是名为initalizer的方法, 而是使用 attribute((constructor) 进行修饰的方法, 在ImageLoader类中initializer函数指针所指向该初始化方法的地址。

这里有一个十分重要的动态库初始化方法,libSystem_initializer就是在这个阶段被调用的。

static __attribute__((constructor)) 
void libSystem_initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct ProgramVars* vars)
{
//......
_libkernel_init(libkernel_funcs);

bootstrap_init();
mach_init();
pthread_init();
__libc_init(vars, libSystem_atfork_prepare, libSystem_atfork_parent, libSystem_atfork_child, apple);
__keymgr_initializer();
_dyld_initializer();

libdispatch_init();

_libxpc_initializer();
//......
errno = 0;
}

libSystem_initializer会调用libdispatch_init再到_objc_init初始化runtime在_objc_init中通过注册了几个关键通知, 从dyld这里接手了关键时机的处理,这个方法会在下一篇介绍runtime的博客中进行介绍,我们先继续往下看。对于Libsystem代码地址可以在该地址进行下载.

获取主程序入口

result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// 主可执行文件使用lc_main,需要返回libdyld.dylib中的glue
//调用main()
//当执行完dyld::_main方法之后,返回了main()函数地址,这个时候所有初始化工作都已经完成了,正式进入Objc声明周期
// main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getMain();
*startGlue = 0;
}

main 地址有两种方式获取,一种是存放在LC_MAIN命令中,这时候需要调用getThreadPC

void* ImageLoaderMachO::getThreadPC() const
{
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
// 遍历loadCommand,加载loadCommand中的'LC_MAIN'所指向的偏移地址
if ( cmd->cmd == LC_MAIN ) {
entry_point_command* mainCmd = (entry_point_command*)cmd;
// 偏移量 + header所占的字节数,就是main的入口
void* entry = (void*)(mainCmd->entryoff + (char*)fMachOData);
if ( this->containsAddress(entry) )
return entry;
else
throw "LC_MAIN entryoff is out of range";
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
return NULL;
}

getThreadPC主要逻辑就是遍历loadCommand,找到’LC_MAIN’指令,得到该指令所指向的偏移地址,经过处理后,就得到了main函数的地址,将此地址返回给__dyld_start。__dyld_start中将main函数地址保存在寄存器后,跳转到对应的地址,开始执行main函数,另一种Mach O不支持LC_MAIN仅支持LC_UNIXTHREAD,这时候就需要调用getMain方法。

void* ImageLoaderMachO::getMain() const
{
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_UNIXTHREAD:
{
#if __i386__
const i386_thread_state_t* registers = (i386_thread_state_t*)(((char*)cmd) + 16);
void* entry = (void*)(registers->eip + fSlide);
#elif __x86_64__
const x86_thread_state64_t* registers = (x86_thread_state64_t*)(((char*)cmd) + 16);
void* entry = (void*)(registers->rip + fSlide);
//.....
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
throw "no valid entry point";
}

总结

上一篇博客我们主要介绍了从点击应用到加载dyld,再将地址设置到dyld到入口地址,这篇博客就从dyld入口地址__dyld_start作为起点,在这个阶段中主要做了如下工作:

1. 将主程序初始化为imageLoader
2. 加载共享库到内存
3. 加载插入的动态库
4. 链接主程序,链接插入库
5. 初始化主程序,插入库
6. 寻找主程序入口点

而下一篇博客要给大家介绍的是十分关键的runtime,它的初始化就发生在初始化主程序,插入库这个阶段,这个阶段会调用使用 attribute((constructor) 进行修饰的方法, 其中这里有一个十分重要的动态库初始化方法,libSystem_initializer就是在这个阶段被调用的libSystem_initializer会调用libdispatch_init再到_objc_init初始化runtime在_objc_init中通过注册了几个关键通知, 从dyld这里接手了关键时机的处理,包括镜像映射,镜像加载,镜像卸载,这些都是下一篇博客将要介绍的。从下一篇开始就要正式介入介绍Runtime了。

Contents
  1. 1. 源码地址
  2. 2. 源码分析
  3. 3. 总结