一、dyld
dyld (动态库加载器),负责加载程序和程序所以依赖的动态库。内核读取 Mach-O 文件后将读取的内容交给 dyld 进行加载,dyld 加载完毕后才会执行 main 函数。
下面下载一下 dyld 源码,看一下 dyld 在 main 函数之前做了哪些操作。dyld源码下载地址,这里使用的源码版本是433.6。
首先我们要找到 dyld 的入口,我们尝试在 main 函数打断点看调用栈,结果发现没有有用信息。这时想到我们经常使用 load 方法,它是在 main 之前调用的,我们随便找一个类写一个 load 方法并设置断点,结果如下图:
** _dyld_start** 是 dyld 启动函数,我们 dyld 调用了 dyldbootstrap::start() , dyldbootstrap::start() 中又调用了 dyld::_main() 函数,我们打开 dyld.cpp 文件,找到 _main() 函数:
uintptr_t
_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;
#if __MAC_OS_X_VERSION_MIN_REQUIRED
// if this is host dyld, check to see if iOS simulator is being run
const char* rootPath = _simple_getenv(envp, "DYLD_ROOT_PATH");
if ( rootPath != NULL ) {
// Add dyld to the kernel image info before we jump to the sim
notifyKernelAboutDyld();
// look to see if simulator has its own dyld
char simDyldPath[PATH_MAX];
strlcpy(simDyldPath, rootPath, PATH_MAX);
strlcat(simDyldPath, "/usr/lib/dyld_sim", PATH_MAX);
int fd = my_open(simDyldPath, O_RDONLY, 0);
if ( fd != -1 ) {
const char* errMessage = useSimulatorDyld(fd, mainExecutableMH, simDyldPath, argc, argv, envp, apple, startGlue, &result);
if ( errMessage != NULL )
halt(errMessage);
return result;
}
}
#endif
CRSetCrashLogMessage("dyld: launch started");
// 设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
// 获取可执行文件路径
sExecPath = _simple_getenv(apple, "executable_path");
// <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
if (!sExecPath) sExecPath = apple[0];
// 将相对路径转换成绝对路径
if ( sExecPath[0] != '/' ) {
char cwdbuff[MAXPATHLEN];
if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
// maybe use static buffer to avoid calling malloc so early...
char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
strcpy(s, cwdbuff);
strcat(s, "/");
strcat(s, sExecPath);
sExecPath = s;
}
}
// 获取文件的名字
sExecShortName = ::strrchr(sExecPath, '/');
if ( sExecShortName != NULL )
++sExecShortName;
else
sExecShortName = sExecPath;
// 配置进程是否受限
configureProcessRestrictions(mainExecutableMH);
#if __MAC_OS_X_VERSION_MIN_REQUIRED
if ( gLinkContext.processIsRestricted ) {
pruneEnvironmentVariables(envp, &apple);
// set again because envp and apple may have changed or moved
setContext(mainExecutableMH, argc, argv, envp, apple);
}
else
#endif
{
// 检查环境变量
checkEnvironmentVariables(envp);
// 如果DYLD_FALLBACK为nil,则将其设置为默认值
defaultUninitializedFallbackPaths(envp);
}
// 如果设置了DYLD_PRINT_OPTS环境变量,则打印参数
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
// 如果设置了DYLD_PRINT_ENV环境变量,则打印环境变量
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
// 读取Mach-O文件的header,获取当前运行架构信息
getHostInfo(mainExecutableMH, mainExecutableSlide);
// install gdb notifier
stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
stateToHandlers(dyld_image_state_mapped, sSingleHandlers)->push_back(updateAllImages);
// make initial allocations large enough that it is unlikely to need to be re-alloced
sImageRoots.reserve(16);
sAddImageCallbacks.reserve(4);
sRemoveImageCallbacks.reserve(4);
sImageFilesNeedingTermination.reserve(16);
sImageFilesNeedingDOFUnregistration.reserve(8);
#if !TARGET_IPHONE_SIMULATOR
#ifdef WAIT_FOR_SYSTEM_ORDER_HANDSHAKE
// <rdar://problem/6849505> Add gating mechanism to dyld support system order file generation process
WAIT_FOR_SYSTEM_ORDER_HANDSHAKE(dyld::gProcessInfo->systemOrderFlag);
#endif
#endif
try {
// add dyld itself to UUID list
addDyldImageToUUIDList();
notifyKernelAboutDyld();
#if SUPPORT_ACCELERATE_TABLES
bool mainExcutableAlreadyRebased = false;
reloadAllImages:
#endif
CRSetCrashLogMessage(sLoadingCrashMessage);
// 加载可执行文件并生成一个ImageLoader实例对象
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
#if TARGET_IPHONE_SIMULATOR
// check main executable is not too new for this OS
{
if ( ! isSimulatorBinary((uint8_t*)mainExecutableMH, sExecPath) ) {
throwf("program was built for a platform that is not supported by this runtime");
}
uint32_t mainMinOS = sMainExecutable->minOSVersion();
// dyld is always built for the current OS, so we can get the current OS version
// from the load command in dyld itself.
uint32_t dyldMinOS = ImageLoaderMachO::minOSVersion((const mach_header*)&__dso_handle);
if ( mainMinOS > dyldMinOS ) {
#if TARGET_OS_WATCH
throwf("app was built for watchOS %d.%d which is newer than this simulator %d.%d",
mainMinOS >> 16, ((mainMinOS >> 8) & 0xFF),
dyldMinOS >> 16, ((dyldMinOS >> 8) & 0xFF));
#elif TARGET_OS_TV
throwf("app was built for tvOS %d.%d which is newer than this simulator %d.%d",
mainMinOS >> 16, ((mainMinOS >> 8) & 0xFF),
dyldMinOS >> 16, ((dyldMinOS >> 8) & 0xFF));
#else
throwf("app was built for iOS %d.%d which is newer than this simulator %d.%d",
mainMinOS >> 16, ((mainMinOS >> 8) & 0xFF),
dyldMinOS >> 16, ((dyldMinOS >> 8) & 0xFF));
#endif
}
}
#endif
#if __MAC_OS_X_VERSION_MIN_REQUIRED
// <rdar://problem/22805519> be less strict about old mach-o binaries
uint32_t mainSDK = sMainExecutable->sdkVersion();
gLinkContext.strictMachORequired = (mainSDK >= DYLD_MACOSX_VERSION_10_12) || gLinkContext.processUsingLibraryValidation;
#else
// simulators, iOS, tvOS, and watchOS are always strict
gLinkContext.strictMachORequired = true;
#endif
// 检查共享缓存是否开启,iOS中必须开启
checkSharedRegionDisable();
#if DYLD_SHARED_CACHE_SUPPORT
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
// 将共享缓存映射到共享区域
mapSharedCache();
} else {
dyld_kernel_image_info_t kernelCacheInfo;
bzero(&kernelCacheInfo.uuid[0], sizeof(uuid_t));
kernelCacheInfo.load_addr = 0;
kernelCacheInfo.fsobjid.fid_objno = 0;
kernelCacheInfo.fsobjid.fid_generation = 0;
kernelCacheInfo.fsid.val[0] = 0;
kernelCacheInfo.fsid.val[0] = 0;
task_register_dyld_shared_cache_image_info(mach_task_self(), kernelCacheInfo, true, false);
}
#endif
#if SUPPORT_ACCELERATE_TABLES
sAllImages.reserve((sAllCacheImagesProxy != NULL) ? 16 : INITIAL_IMAGE_COUNT);
#else
sAllImages.reserve(INITIAL_IMAGE_COUNT);
#endif
// Now that shared cache is loaded, setup an versioned dylib overrides
#if SUPPORT_VERSIONED_PATHS
// 检查库的版本是否有更新,如果有则覆盖原有的,如两个应用依赖不同版本的动态库,后面加载的动态库需要覆盖前面的版本
checkVersionedPaths();
#endif
// dyld_all_image_infos image list does not contain dyld
// add it as dyldPath field in dyld_all_image_infos
// for simulator, dyld_sim is in image list, need host dyld added
#if TARGET_IPHONE_SIMULATOR
// get path of host dyld from table of syscall vectors in host dyld
void* addressInDyld = gSyscallHelpers;
#else
// get path of dyld itself
void* addressInDyld = (void*)&__dso_handle;
#endif
char dyldPathBuffer[MAXPATHLEN+1];
int len = proc_regionfilename(getpid(), (uint64_t)(long)addressInDyld, dyldPathBuffer, MAXPATHLEN);
if ( len > 0 ) {
dyldPathBuffer[len] = '\0'; // proc_regionfilename() does not zero terminate returned string
if ( strcmp(dyldPathBuffer, gProcessInfo->dyldPath) != 0 )
gProcessInfo->dyldPath = strdup(dyldPathBuffer);
}
// 加载 DYLD_INSERT_LIBRARIES 指定的库,这里就是插入动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
// 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;
#if SUPPORT_ACCELERATE_TABLES
if ( mainExcutableAlreadyRebased ) {
// previous link() on main executable has already adjusted its internal pointers for ASLR
// work around that by rebasing by inverse amount
sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
}
#endif
// 链接主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
// 链接插入动态库
// 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), -1);
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 (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image->inSharedCache() )
continue;
image->registerInterposing();
}
#if SUPPORT_ACCELERATE_TABLES
if ( (sAllCacheImagesProxy != NULL) && ImageLoader::haveInterposingTuples() ) {
// Accelerator tables cannot be used with implicit interposing, so relaunch with accelerator tables disabled
ImageLoader::clearInterposingTuples();
// unmap all loaded dylibs (but not main executable)
for (long i=1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image == sMainExecutable )
continue;
if ( image == sAllCacheImagesProxy )
continue;
image->setCanUnload();
ImageLoader::deleteImage(image);
}
// note: we don't need to worry about inserted images because if DYLD_INSERT_LIBRARIES was set we would not be using the accelerator table
sAllImages.clear();
sImageRoots.clear();
sImageFilesNeedingTermination.clear();
sImageFilesNeedingDOFUnregistration.clear();
sAddImageCallbacks.clear();
sRemoveImageCallbacks.clear();
sDisableAcceleratorTables = true;
sAllCacheImagesProxy = NULL;
sMappedRangesStart = NULL;
mainExcutableAlreadyRebased = true;
gLinkContext.linkingMainExecutable = false;
resetAllImages();
goto reloadAllImages;
}
#endif
// apply interposing to initial set of images
for(int i=0; i < sImageRoots.size(); ++i) {
sImageRoots[i]->applyInterposing(gLinkContext);
}
gLinkContext.linkingMainExecutable = false;
// <rdar://problem/12186933> do weak binding only after all inserted images linked
// 若符号绑定
sMainExecutable->weakBind(gLinkContext);
#if DYLD_SHARED_CACHE_SUPPORT
// If cache has branch island dylibs, tell debugger about them
if ( (sSharedCache != NULL) && (sSharedCache->mappingOffset >= 0x78) && (sSharedCache->branchPoolsOffset != 0) ) {
uint32_t count = sSharedCache->branchPoolsCount;
dyld_image_info info[count];
const uint64_t* poolAddress = (uint64_t*)((char*)sSharedCache + sSharedCache->branchPoolsOffset);
// <rdar://problem/20799203> empty branch pools can be in development cache
if ( ((mach_header*)poolAddress)->magic == sMainExecutableMachHeader->magic ) {
for (int poolIndex=0; poolIndex < count; ++poolIndex) {
uint64_t poolAddr = poolAddress[poolIndex] + sSharedCacheSlide;
info[poolIndex].imageLoadAddress = (mach_header*)(long)poolAddr;
info[poolIndex].imageFilePath = "dyld_shared_cache_branch_islands";
info[poolIndex].imageFileModDate = 0;
}
// add to all_images list
addImagesToAllImages(count, info);
// tell gdb about new branch island images
gProcessInfo->notification(dyld_image_adding, count, info);
}
}
#endif
CRSetCrashLogMessage("dyld: launch, running initializers");
#if SUPPORT_OLD_CRT_INITIALIZATION
// Old way is to run initializers via a callback from crt1.o
if ( ! gRunInitializersOldWay )
initializeMainExecutable();
#else
// run all initializers
initializeMainExecutable();
#endif
// notify any montoring proccesses that this process is about to enter main()
notifyMonitoringDyldMain();
// 寻找入口并执行,main函数
result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// 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");
}
CRSetCrashLogMessage(NULL);
return result;
}
从代码中可以看到 dyld 的加载流程主要包括如下 9 个步骤:
- 设置上下文信息,配置进程是否受限
- 配置环境变量,获取当前运行架构
- 加载可执行文件,生成一个 ImageLoader 实例对象
- 检查共享缓存是否映射到共享区域
- 加载所有的插入动态库
- 链接主程序
- 链接所有插入动态库,执行符号替换
- 执行初始化方法
- 寻找主程序入口
二、懒加载与非懒加载
1.懒加载绑定
懒加载的符号在第一次调用之前的符号地址不是真实的符号地址,只有在第一次调用时才会绑定真实的符号地址,第二次就会直接调用真实的符号地址。
通过一个例子来看懒加载绑定的过程,新建一个 Test 工程,写 2 句输出语句,如下:
读取 __stubs 中存放函数地址的地址
在两处代码设置断点,运行程序,运行到断点出点击菜单栏 Debug -> Debug Workflow -> Always Show Disassembly 查看汇编代码。
可以看到第一处 printf
调用地址 0x100cf2584
,该地址是当前 printf
加载到内存中的地址,而且也可以看出该地址是在 Mach-O 文件中的 __stubs
节中,而该节又在 __TEXT
段中。
我们要在 Mach-O 中查看该地址对应的内容就需要计算出内容在 Mach-O 文件中的偏移地址。
在计算偏移地址之前需要介绍一下 ALSR(地址空间配置随机加载)。ALSR 是一种防范内存损坏漏洞被利用的计算机安全技术。ASLR通过随机放置进程关键数据区域的地址空间来防止攻击者能可靠地跳转到内存的特定位置来利用函数。
由于 ALSR 技术的存在,应用每次加载到内存中的其实地址是不同,我们可以在 LLDB 调试窗口通过 image list | grep <Mach-O name>
获取当前应用的初始地址。
这时候我们就可以计算偏移地址了,偏移地址 = 目标内存地址 - Mach-O起始内存地址 + 当前架构起始地址
,我们可以用 LLDB 进行计算偏移大小:
0x6584
就是我们要查看内容在 Mach-O 文件在当前架构内容的偏移大小,然后通过 MachOView 软件查看 Mach-O 文件发现,改可执行文件只有一个架构,所以当前架构起始地址为 0x0
,最终我们得出我们要找的 printf
在 **Mach-O 文件中的偏移地址就是 0x6584
。
读取 __la_symobol_ptr 中函数地址
接下来我们通过 si
命令跟进查看读取地址指令
由于还未绑定此时读取的并非是函数真实地址,而是函数绑定指令
此处 ldr
指令的意思就是取 pc + 0x1c40
的值
0x100cf41c8
在文件中的偏移地址是 0x81c8
,然后通过 Mach-O 查看,可以看出上面其实取的是 __la_symbol_ptr
中 __printf
的值:
从动态库中获取函数真实地址
继续分析汇编指令,通过命令 p/xg 0x100cf41c8
可以查看 0x100cf41c8
地址的值是 0x0000000100cf2800
。0x0000000100cf2800
对应文件中的 0x6800
在 __stub_helper
节中。
使用 dis -s 0x0000000100cf2800
命令反汇编查看
可以看到该指令在
0x100cf2808
取值 0x0000040c
,这是一个偏移值,是距离 Dynamic Loader Info
中 Lazy Binding Info
起始位置的偏移。从文件中可以看到起始地址是
0xc368
,加上 0x0000040c
最终结果是 0x0000c774
,通过 Mach-O 应用查看 0x0000c774
处:分析一下上图的内容, dylib(2)
表示该符号要从当前文件的第 2 个 LC_LOAD_DYLIB 中寻找,也就是在 libSystem.B.dylib
中寻找 printf
的真实地址:segment(2)
与 offset(456)
表示将找到的真实地址写入到当前架构第 2 个 segment(不包括 __PAGEZERO) 偏移 456 的地方。
第 2 个 segment 是 __DATA
段,其起始地址为 0x8000
,偏移 456 的位置就是 0x8000 + 456 = 0x81c8
,而 0x81c8
就是 __la_symbol_ptr
中 printf
符号的指针位置,也就是将找到的真实地址写到 __la_symbol_ptr
中。
执行完 0x100cf2808
处代码,执行 0x100cf2810
处的代码,这里跳转到 0x100cf25f0
,此处对应与文件中 __stub_helper
节起始位置, 反汇编查看此处代码:
此处可以看到调用了
dyld_stub_binder
, 使用 dis -a 0x00000001acf3cb94
继续跟进 dyld_stub_binder
:在
0x1acf3cbcc
处下断点,程序断下后 x1 寄存器的值是 0x0000040c
,就是上面所说的偏移值。此处调用了 _dyld_fast_stub_entry
函数,该函数根据偏移值进行真正的绑定。
动态绑定到此结束,继续指定到了我们第二次的 printf
函数断点处,通过 si
跟进发现我们再次从 __la_symbol_ptr
去除的 printf
符号的指针地址是真实的 printf
符号地址。
懒加载函数绑定流程
- 程序读取
__TEXT
段__stubs
节中获取 函数间接地址 - 懒加载函数存放在
__DATA
段__la_symbol_str
中,由于还未绑定,此时去的地址不是真实的地址