APPのエントリ関数がmain()であることは誰もが知っていますが、main()関数が呼び出される前に、APPのロードプロセスは何ですか?次に、APPの読み込みプロセスを一緒に分析しましょう。
1.ブレークポイントを使用して追跡する
-
まず、プロジェクトを作成します。コードを記述せず、main()関数にブレークポイントを設定します。次のような状況が表示されます。
01
- 上の図から、コールスタックでは、スターとメインのみが表示され、メインスレッドがオンになっていることがわかりますが、それ以外は何も表示されていません。コールスタックの詳細情報を確認するにはどうすればよいですか?main()関数よりも前に呼び出されるメソッド、つまりload()関数があることは誰もが知っています。このとき、次の図に示すように、コントローラーにload関数を記述し、ブレークポイントで実行します。
02
- 上の図から、13行目の_dyld_startから3行目のdyld:notifySingleまでの、より詳細な関数呼び出しシーケンスを確認できます。このdyldガイが最も頻繁に表示されるので、dyldとは何ですか。何してるの?簡単に言えば、dyldは、すべてのライブラリと実行可能ファイルをロードする動的リンカーです。次に、図2に示す呼び出し関係を使用して、dyldがどこにあるかを追跡します。
2、dyldローディングプロセス分析
1.最初にdyldソースコードをダウンロードします。
2. dyldソースコードプロジェクトを開き、次の図に示すように、図2の12行目のdyldbootstrap:startキーワードに従って、dyldbootstrapで呼び出されるstartメソッドを検索します。
3.メソッドのソースコードは次のとおりです。次に、メソッドの主要部分を分析します。
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[], intptr_t slide)
{
// 读取macho文件的头部信息
const struct macho_header* dyldsMachHeader = (const struct macho_header*)(((char*)&_mh_dylinker_header)+slide);
// 滑块,设置偏移量,用于重定位
if ( slide != 0 ) {
rebaseDyld(dyldsMachHeader, slide);
}
uintptr_t appsSlide = 0;
// 针对偏移异常的监测
dyld_exceptions_init(dyldsMachHeader, slide);
// 初始化machO文件
mach_init();
// 设置分段保护,这里的分段下面会介绍,属于machO文件格式
segmentProtectDyld(dyldsMachHeader, slide);
//环境变量指针
const char** envp = &argv[argc+1];
// 环境变量指针结束的设置
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// 在dyld中运行所有c++初始化器
runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
// 如果主可执行文件被链接-pie,那么随机分配它的加载地址
if ( appsMachHeader->flags & MH_PIE )
appsMachHeader = randomizeExecutableLoadAddress(appsMachHeader, envp, &appsSlide);
// 传入头文件信息,偏移量等。调用dyld的自己的main函数(这里并不是APP的main函数)。
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple);
}
-
3.1関数のパラメーターに、macho_headerのパラメーターがあることがわかります。これは何ですか。Mach-Oは、実際にはMach Object file formatの略です。これはmacおよびiOSで実行可能なファイル形式であり、独自のファイル形式ディレクトリがあります。Appleが提供するmachファイルは次のとおりです。
04
- 3.2まず、macho_header構造をクリックして、その定義を次のように確認します。
struct mach_header_64 {
uint32_t magic; /* 区分系统架构版本 */
cpu_type_t cputype; /*CPU类型 */
cpu_subtype_t cpusubtype; /* CPU具体类型 */
uint32_t filetype; /* 文件类型 */
uint32_t ncmds; /* loadcommands 条数,即依赖库数量*/
uint32_t sizeofcmds; /* 依赖库大小 */
uint32_t flags; /* 标志位 */
uint32_t reserved; /* 保留字段,暂没有用到*/
};
-
3.3ここで、macho_headerは、マッチョファイルのヘッダー情報を読み取ります。ヘッダーには、バイト順序、アーキテクチャタイプ、ロード命令の数など、バイナリファイルに関する情報が含まれます。現在のファイルが32ビットと64ビットのどちらで使用されているか、ファイルタイプなど、いくつかの情報をすばやく確認するために使用できます。では、マッチョファイルはどこにありますか?以下に示すように、マッチョを見つけ、MachOViewを使用して以下を表示します。
05
-
3.4上の暗いファイルは、実行可能ファイルであるマッチョファイルです。ロードされるヘッダー情報を見てみましょう。この情報は次の関数に渡されます。これは、22個のライブラリファイルを表す22個のロードコマンドの数の簡単な紹介です。LoadCommandsには、ライブラリのロードに対応する関係があります。セクションは、コード、定数、その他のデータを含むデータDATAです。
06
-
3.5まとめ:star関数は、主にマッチョファイルのヘッダー情報を最初に読み取り、仮想アドレスオフセットを設定することです。ここでのオフセットは、主にリダイレクトに使用されます。次のステップは、ライブラリファイルとDATAデータを後でロードするためにマッチョファイルを初期化し、次にC ++初期化子を実行し、最後にdylyのメイン関数を入力することです。
-
4.次に、トレースを続けます。図2の呼び出しスタックによると、次の図に示すように、dyld :: _ mainメソッドがdyldbootstrap:starメソッドで呼び出されていることがわかります。これは、上記のdyldに入るメインプログラムです。
07
- 4.1次の図に示すように、いくつかのソースを追跡および傍受する方法を入力します。いくつかのif判定があります。ここでは、環境変数を設定しています。つまり、これらの環境変数が設定されている場合、Xcodeは関連する詳細情報をコンソールに出力します。 :
if ( sProcessIsRestricted ) pruneEnvironmentVariables(envp, &apple); else checkEnvironmentVariables(envp, ignoreEnvironmentVariables); if ( sEnv.DYLD_PRINT_OPTS ) printOptions(argv); if ( sEnv.DYLD_PRINT_ENV ) printEnvironmentVariables(envp); getHostInfo();
- 4.2関連する環境変数を設定すると、Xcodeは、次の図に示すように、プログラム関連のディレクトリ、ユーザーレベル、挿入された動的ライブラリ、動的ライブラリパスなどを出力します。
-
08
- 4.3環境変数を設定した後、次にgetHostInfo()を呼び出してmachOヘッダーを取得し、現在実行中のアーキテクチャの情報を取得します。関数コードは次のとおりです。
-
static void getHostInfo() { #if 1 struct host_basic_info info; mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT; mach_port_t hostPort = mach_host_self(); kern_return_t result = host_info(hostPort, HOST_BASIC_INFO, (host_info_t)&info, &count); if ( result != KERN_SUCCESS ) throw "host_info() failed"; sHostCPU = info.cpu_type; sHostCPUsubtype = info.cpu_subtype; #else size_t valSize = sizeof(sHostCPU); if (sysctlbyname ("hw.cputype", &sHostCPU, &valSize, NULL, 0) != 0) throw "sysctlbyname(hw.cputype) failed"; valSize = sizeof(sHostCPUsubtype); if (sysctlbyname ("hw.cpusubtype", &sHostCPUsubtype, &valSize, NULL, 0) != 0) throw "sysctlbyname(hw.cpusubtype) failed"; #endif }
- 4.4次に、見下ろします。ここでマッチョファイルをインスタンス化します。
-
try { // 实例化主程序,也就是machO这个可执行文件 sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); sMainExecutable->setNeverUnload(); gLinkContext.mainExecutable = sMainExecutable; gLinkContext.processIsRestricted = sProcessIsRestricted; // 加载共享缓存库 checkSharedRegionDisable(); #if DYLD_SHARED_CACHE_SUPPORT if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) mapSharedCache(); #endif
- 4.5インスタンス化されたメインプログラムコードを次のように入力します。ロード後、ImageLoaderイメージロードクラスが返されます。これは、特定の実行可能ファイル形式でクラスをロードするために使用される抽象クラスです。プログラムに必要な依存ライブラリとプラグインライブラリが作成されます。対応する画像オブジェクト、これらの画像のリンク、各画像の初期化メソッドの呼び出しなど(ランタイムの初期化を含む)。
-
{ // isCompatibleMachO 是检查mach-o的subtype是否是当前cpu可以支持 if ( isCompatibleMachO((const uint8_t*)mh, path) ) { ImageLoader* image = ImageLoaderMachO::instantiateMainExecutable(mh, slide, path, gLinkContext); //将image添加到imagelist。所以我们在Xcode使用image list命令查看的第一个便是我们的machO addImage(image); return image; } throw "main executable not a known format"; }
-
4.6 image listコマンドを使用して、次の図を示します。最初に表示されるアドレス0x000000010401c000は、実行可能ファイルmachoのアドレスです。
-
4.7マッチョファイルをインスタンス化すると、共有キャッシュライブラリがロードされるcheckSharedRegionDisable()メソッドが表示されます。この共有キャッシュライブラリとは何ですか?実際、システムによって共有される動的ライブラリとして理解できます(Appleはサードパーティによる動的ライブラリの使用を禁止しています)。たとえば、最も一般的に使用されるUIKitフレームワークは、共有キャッシュライブラリにあります。たとえば、WeChat、QQ、Alipay、TmallなどのアプリはUIKitフレームワークを使用します。すべてのアプリがUIKitをロードすると、必然的にメモリ不足につながります。したがって、実際には、これらのアプリはUIKitフレームワークのセットを共有し、UIKitフレームワークの対応するメソッドがアプリで使用され、dyldはこれらのアプリに対応するリソースを使用します。次の図は、ジェイルブレイクされた電話のシステムライブラリにあるフレームワークライブラリを示しています。これもこれを証明しています。
共有キャッシュライブラリ
-
5.ライブラリの挿入:このメソッドの残りのソースコードを引き続き見ていきましょう。挿入されたすべてのライブラリがここにロードされます。逆方向のコードインジェクションはこのステップで完了します。フレームワークの詳細なコードインジェクションプロセスについては、私の記事を参照してください。sAllImages.size()-1の操作があり、実際にはメインプログラムを除外します。
-
// load any inserted 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;
6.メインプログラムをリンクします。imageLoaderのインスタンスオブジェクトを介して内部的にlinkメソッドを呼び出し、依存システムライブラリとサードパーティライブラリを再帰的にロードします。
// link main executable gLinkContext.linkingMainExecutable = true; link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL)); gLinkContext.linkingMainExecutable = false; if ( sMainExecutable->forceFlat() ) { gLinkContext.bindFlat = true; gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding; } result = (uintptr_t)sMainExecutable->getMain();
7.初期化機能
10
8.初期化プログラムを実行します。
-
8.1再帰:必要な依存システムライブラリとサードパーティライブラリをロードします。
12
-
9.ランタイムとの接続を確立するための重要な関数であるnotifySingle関数:
13
- 9.1 load_imagesメソッドがnotifySingle関数で呼び出され、クリックして、これが関数ポインターであり、load_imagesの呼び出しが見つからなかったことがわかりました。dyldファイルのグローバル検索では見つかりませんでした。したがって、現時点では、objcランタイムコードもオープンソースであるという理由だけで、実行時に呼び出されると推測し、分析のためにobjcソースコードをダウンロードします。
void (*notifySingle)(dyld_image_states, const ImageLoader* image);
- 9.2 objc_initには、ここでload_imagesという呼び出しがあります。
-
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
14
- 9.3 load_imagesでcall_load_methods呼び出しを完了します。これは、すべてのクラスファイルと分類ファイルをロードするためのloadメソッドです。
-
load_images(const char *path __unused, const struct mach_header *mh) { // 如果这里没有+load方法,则返回时不带锁 if (!hasLoadMethods((const headerType *)mh)) return; recursive_mutex_locker_t lock(loadMethodLock); // 发现load方法 { mutex_locker_t lock2(runtimeLock); prepare_load_methods((const headerType *)mh); } // 加载所有load方法 call_load_methods(); }
- 9.4 call_load_methodsメソッド呼び出し、call_load_methodsで、call_class_loadsを呼び出して、doWhileループを介して各クラスのloadメソッドをロードしてから、分類されたloadsメソッドをロードします。
-
void call_load_methods(void) { static bool loading = NO; bool more_categories; loadMethodLock.assertLocked(); // Re-entrant calls do nothing; the outermost call will finish the job. if (loading) return; loading = YES; void *pool = objc_autoreleasePoolPush(); do { // 1. 循环调用所有类文件的laod方法 while (loadable_classes_used > 0) { call_class_loads(); } // 2.调用所有分类方法 more_categories = call_category_loads(); // 3. Run more +loads if there are classes OR more untried categories } while (loadable_classes_used > 0 || more_categories); objc_autoreleasePoolPop(pool); loading = NO; }
-
9.5上記の呼び出しシーケンスによれば、クラスファイルのloadメソッドが最初にロードされ、次にクラスファイルのloadメソッドがロードされることがわかります。デモを図に示します。
-
15
-
10. notifySiginを呼び出した後、引き続きdoInitializationを呼び出すことがわかりました。doModInitFunctionsは、ファイルで定義したグローバルC ++コンストラクターであるmachOファイルの_mod_init_funcセクションの関数を呼び出します。
// let objc know we are about to initalize this image fState = dyld_image_state_dependents_initialized; oldState = fState; context.notifySingle(dyld_image_state_dependents_initialized, this); // initialize this image this->doInitialization(context);
-
10.1したがって、上記のコードの呼び出しシーケンスを通じて、最初のクラスファイルのロード、次にクラスファイルのロード、次にC ++コンストラクター、そして最後にメインプログラムに入ることがわかります。デモンストレーションは次のとおりです。
-
上記の分析を通じて、ブレークポイントから開始し、メソッドのスタック呼び出しシーケンスを確認し、dyldのロードプロセスを段階的に追跡します。これにより、メイン関数呼び出しの前に謎が明らかになります。上記の手順に従って、APPを自分で追跡することもできます。ロードプロセスはさらに印象的です!
概要:main()関数が呼び出される前に、実際には多くの準備作業が行われ、主に動的リンカーdyldが担当します。コアプロセスは次のとおりです。
1.プログラムの実行は_dyld_starから始まります
- 1.1。マッチョファイル情報を読み取り、リダイレクト用の仮想アドレスオフセットを設定します。
- 1.2。dyld :: _ mainメソッドを呼び出して、マッチョファイルのメインプログラムに入ります。
2.いくつかの環境変数を構成します
- 2.1。設定された環境変数は、より多くの情報を印刷するのに便利です。
- 2.1。getHostInfo()を呼び出してmachOヘッダーを取得し、現在実行中のアーキテクチャに関する情報を取得します。
3.マッチョ実行可能ファイルであるメインプログラムをインスタンス化します。
4.共有キャッシュライブラリをロードします。
5.動的キャッシュライブラリを挿入します。
6.メインプログラムをリンクします。
7.初期化機能。
- 7.1。一連の初期化関数の後、notifSingle関数が最終的に呼び出されます。
- 7.2。このコールバックは、ランタイム_objc_initによって初期化されたときに割り当てられる関数load_imagesです。
- 7.3。call_load_methods関数はload_imagesで実行され、使用されるクラスと分類のloadメソッドが周期的に呼び出されます。
- 7.4。doModInitFunctions関数は、グローバルC ++オブジェクトのコンストラクター、つまり_ _ attribute _ _((constructor))などの関数を内部的に呼び出します。
8.メインプログラムの入力関数に戻り、メインプログラムのmain()関数の入力を開始します。
リンク:https://www.jianshu.com/p/1b9ca38b8b9f