神奇的崩潰事件
作者:歐陽(yáng)大哥2013
鏈接:http://www.itdecent.cn/p/e6300594c966
來(lái)源:簡(jiǎn)書
簡(jiǎn)書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。
事件源于接入了一個(gè)第三方庫(kù)導(dǎo)致應(yīng)用出現(xiàn)了大量的crash記錄,很奇怪的是這么多的crash居然沒(méi)有收到用戶的反饋信息! 在這個(gè)過(guò)程中每個(gè)崩潰棧的信息都明確的指向了是那個(gè)第三方庫(kù)的某個(gè)工作線程產(chǎn)生的崩潰。這個(gè)問(wèn)題第三方提供者一直無(wú)法復(fù)現(xiàn),而且我們的RD、PM、QA同學(xué)在調(diào)試和測(cè)試過(guò)程中都沒(méi)有出現(xiàn)過(guò)這個(gè)問(wèn)題。后來(lái)再經(jīng)過(guò)仔細(xì)檢查分析,發(fā)現(xiàn)每次崩潰時(shí)的各線程的調(diào)用棧都大概是如下的情況:
Hardware Model: iPhone7,2
Code Type: ARM-64
Parent Process: ? [1]
Date/Time: 2018-05-10 10:22:32.000 +0800
OS Version: iOS 10.3.3 (14G60)
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Codes: 0x00000000 at 0xbadd0c44f948beb5
Crashed Thread: 33
//并非崩潰在主線程,而是用戶執(zhí)行了殺掉應(yīng)用的操作。下面主線程的調(diào)用棧可以看出是用戶主動(dòng)殺死的進(jìn)程。
Thread 0:
0 xxxx xxxx::Threads::Synchronization::AppMutex::~AppMutex() (xxxx.cpp:58)
1 libsystem_c.dylib __cxa_finalize_ranges + 384
2 libsystem_c.dylib exit + 24
3 UIKit +[_UIAlertManager hideAlertsForTermination] + 0
4 UIKit __102-[UIApplication _handleApplicationDeactivationWithScene:shouldForceExit:transitionContext:completion:]_block_invoke.2093 + 792
5 UIKit _runAfterCACommitDeferredBlocks + 292
6 UIKit _cleanUpAfterCAFlushAndRunDeferredBlocks + 528
7 UIKit _afterCACommitHandler + 132
8 CoreFoundation __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
9 CoreFoundation __CFRunLoopDoObservers + 372
10 CoreFoundation __CFRunLoopRun + 956
11 CoreFoundation CFRunLoopRunSpecific + 424
12 GraphicsServices GSEventRunModal + 100
13 UIKit UIApplicationMain + 208
14 xxxx main (main.m:36)
15 libdyld.dylib start + 4
/*
崩潰的線程調(diào)用棧,出現(xiàn)崩潰的機(jī)器指令段如下:
0x106bee318<+180>: add x8, x0, #0x8
0x106bee31c<+184>: ldaxr w9,[x8]
注意看下面的x0,x8寄存器中的值已經(jīng)是異常的數(shù)字了,這里對(duì)異常地址進(jìn)行讀取操作產(chǎn)生了崩潰
*/
Thread 33 name: xxxx
Thread 33 Crashed:
0 xxxx xxxx::Message::recycle() + 184
1 xxxx xxxx::Message::recycle() + 176
2 xxxx xxxx::BaseMessageLooper::onProcMessage(xxxx::Message*) + 192
3 xxxx xxxx::Looper::loop() + 60
4 xxxx xxxx::MessageThread::run() + 96
5 xxxx xxxx::Thread::runCallback(void*) + 108
6 libsystem_pthread.dylib _pthread_body + 240
7 libsystem_pthread.dylib _pthread_body + 0
Thread 33 crashed with ARM-64 Thread State:
cpsr: 0x00000000a0000000 fp: 0x000000017102ae60 lr: 0x00000001018744c4 pc: 0x00000001018744cc
sp: 0x000000017102ae40 x0: 0xbadd0c44f948bead x1: 0x0000000000000000 x10: 0x0000000000000000
x11: 0x0000000102a50178 x12: 0x000000000000002c x13: 0x000000000000002c x14: 0x0000000102a502d8
x15: 0x0000000000000000 x16: 0x0000000190f3fa1c x17: 0x010bcb01010bcb00 x18: 0x0000000000000000
x19: 0x00000001744a2460 x2: 0x0000000000000008 x20: 0x00000001027da5b8 x21: 0x0000000000000000
x22: 0x0000000000000001 x23: 0x0000000000025903 x24: 0x0000000000000000 x25: 0x0000000000000000
x26: 0x0000000000000000 x27: 0x0000000000000000 x28: 0x0000000000000000 x29: 0x000000017102ae60
x3: 0x0000000190f4f2c0 x4: 0x0000000000000002 x5: 0x0000000000000008 x6: 0x0000000000000000
x7: 0x00000000010bcb01 x8: 0xbadd0c44f948beb5 x9: 0x0000000000000000
從上面主線程的調(diào)用??梢钥闯隼锩嬗袌?zhí)行exit函數(shù),而exit是一個(gè)執(zhí)行進(jìn)程結(jié)束的函數(shù),因此從調(diào)用棧來(lái)看其實(shí)這正是用戶在主動(dòng)殺掉我們的App應(yīng)用進(jìn)程時(shí)主線程會(huì)執(zhí)行的邏輯。也就是說(shuō)出現(xiàn)崩潰的時(shí)機(jī)就是在主動(dòng)殺掉我們的應(yīng)用的時(shí)刻發(fā)生的!
這真的是一個(gè)非常神奇的時(shí)刻,當(dāng)我們主動(dòng)殺掉應(yīng)用時(shí)產(chǎn)生了崩潰,所以整個(gè)事件就出現(xiàn)了上面的場(chǎng)景:沒(méi)有用戶反饋異常、我們自身也很難復(fù)現(xiàn)出崩潰的場(chǎng)景(非連機(jī)運(yùn)行時(shí))。
問(wèn)題復(fù)現(xiàn)
分析出原因后為了驗(yàn)證問(wèn)題,通過(guò)不停的執(zhí)行手動(dòng)殺進(jìn)程的測(cè)試,在一個(gè)偶然的機(jī)會(huì)下終于復(fù)現(xiàn)了問(wèn)題:在主線程執(zhí)行exit的時(shí)機(jī),那個(gè)第三方庫(kù)的工作線程的某處出現(xiàn)非法地址訪問(wèn),而停止了執(zhí)行:


這個(gè)來(lái)之不易的崩潰信息起了非常大的作用,根據(jù)匯編代碼按圖索驥,并和對(duì)方進(jìn)行交流定位到了對(duì)應(yīng)的源代碼。第三方庫(kù)的一個(gè)線程是一個(gè)常駐線程,它會(huì)周期性并且高頻的訪問(wèn)一個(gè)全局C++對(duì)象實(shí)例的數(shù)據(jù),出現(xiàn)奔潰的原因就是這個(gè)全局C++對(duì)象的類的構(gòu)造函數(shù)中從堆里面分配了一塊內(nèi)存,而當(dāng)進(jìn)程被終止這個(gè)過(guò)程中,這個(gè)全局對(duì)象被析構(gòu),析構(gòu)函數(shù)中會(huì)將分配的堆內(nèi)存進(jìn)行回收。但是那個(gè)常駐線程因?yàn)榇丝踢€沒(méi)有被終止,它還像往常一樣繼續(xù)訪問(wèn)這個(gè)已經(jīng)被析構(gòu)了的全局對(duì)象的堆內(nèi)存,從而導(dǎo)致了上面圖中的內(nèi)存地址訪問(wèn)非法的問(wèn)題。下面就是問(wèn)題發(fā)生的過(guò)程:

C++全局對(duì)象
可以肯定一點(diǎn)的就是那個(gè)第三方庫(kù)由于對(duì)全局C++對(duì)象的使用不當(dāng)而產(chǎn)生了問(wèn)題。我們知道每個(gè)C++對(duì)象在創(chuàng)建時(shí)都會(huì)調(diào)用對(duì)應(yīng)的構(gòu)造函數(shù),而對(duì)象銷毀時(shí)則會(huì)調(diào)用對(duì)應(yīng)的析構(gòu)函數(shù)。構(gòu)造和析構(gòu)函數(shù)都是一段代碼,對(duì)象的創(chuàng)建和銷毀一般都是在某個(gè)函數(shù)中進(jìn)行,這時(shí)候?qū)ο蟮臉?gòu)造/析構(gòu)函數(shù)也是在那個(gè)調(diào)用者函數(shù)中執(zhí)行,比如下面的代碼:
class CA{
public:
CA(){
printf("CA::CA()");
}
void ~CA(){
printf("CA::~CA()");
}
};
CA b; //定義一個(gè)全局變量
int main()
{
CA a; //函數(shù)內(nèi)建立一個(gè)對(duì)象
printf("hello");
return 0;
}
系統(tǒng)在編譯C++代碼時(shí)會(huì)進(jìn)行一些特定的處理(這里以C語(yǔ)言的形式來(lái)描述):
//定義結(jié)構(gòu)體
struct CA{
};
//CA類名稱被重新修飾了的構(gòu)造函數(shù)
void __ZN2CAC1Ev(CA * const this)
{
printf("CA::CA()");
}
//CA類名稱被重新修飾了的析構(gòu)函數(shù)
void __ZN2CAD1Ev(CA * const this)
{
printf("CA::~CA()");
}
//?? b對(duì)象的構(gòu)造和析構(gòu)又是在哪里被調(diào)用執(zhí)行的呢?因?yàn)檎也坏綀?zhí)行的上下文。
struct CA b;
int main()
{
struct CA a;
__ZN2CAC1Ev(&a); //局部對(duì)象在對(duì)象創(chuàng)建后調(diào)用構(gòu)造函數(shù)
printf("hello");
__ZN2CAD1Ev(&a); //這里調(diào)用析構(gòu)函數(shù)
return 0;
}
上面的源代碼中b這個(gè)全局對(duì)象并不是在某個(gè)函數(shù)或者方法內(nèi)部定義,
所以它并沒(méi)有執(zhí)行構(gòu)造函數(shù)以及析構(gòu)函數(shù)的上下文環(huán)境,那么是否創(chuàng)建一個(gè)全局對(duì)象時(shí)它的構(gòu)造函數(shù)以及析構(gòu)函數(shù)就無(wú)法被執(zhí)行呢了?答案是否定的。只要任何一個(gè)C++類定義了構(gòu)造函數(shù)或者析構(gòu)函數(shù),那么在對(duì)象創(chuàng)建時(shí)總是會(huì)調(diào)用構(gòu)造函數(shù),并且在對(duì)象銷毀時(shí)會(huì)調(diào)用對(duì)應(yīng)的析構(gòu)函數(shù)。那么全局對(duì)象的構(gòu)造函數(shù)和析構(gòu)函數(shù)又是在什么時(shí)候被調(diào)用執(zhí)行的呢?
+load方法
在一個(gè)Objective-C類中,可以定義一個(gè)+load方法,這個(gè)+load方法會(huì)在所有OC對(duì)象創(chuàng)建前被執(zhí)行,同時(shí)也會(huì)在main函數(shù)調(diào)用前被執(zhí)行。一般情況下我們會(huì)在類的+load方法中實(shí)現(xiàn)一些全局初始化的邏輯。OC類的方法也是要求一定的上下文環(huán)境下才能被執(zhí)行,那么+load方法又是在什么時(shí)候被調(diào)用執(zhí)行的呢?
全局構(gòu)造/析構(gòu)C函數(shù)
除了建立C++全局對(duì)象、實(shí)現(xiàn)OC類的+load方法來(lái)進(jìn)行一些全局的初始化邏輯外,我們還可以定義帶有特殊標(biāo)志的C函數(shù)來(lái)實(shí)現(xiàn)main函數(shù)執(zhí)行前以及main函數(shù)執(zhí)行完畢后的處理邏輯。
//main函數(shù)執(zhí)行前被執(zhí)行的函數(shù)
void __attribute__ ((constructor)) beginfunc()
{
printf("beginfunc\n");
}
//main函數(shù)執(zhí)行完畢后被執(zhí)行的函數(shù)
void __attribute__ ((destructor)) endfunc()
{
printf("endfunc\n");
}
int main()
{
printf("main\n");
return 0;
}
//程序運(yùn)行時(shí)分別輸出
// beginfunc
// main
// endfunc
上面的代碼中可以看出,我們并沒(méi)有顯式的調(diào)用beginfunc和endfunc函數(shù)的情況下,函數(shù)依然被調(diào)用執(zhí)行。那么這些函數(shù)又是如何被調(diào)用執(zhí)行的呢?
main函數(shù)執(zhí)行前發(fā)生了什么?
操作系統(tǒng)在啟動(dòng)一個(gè)程序時(shí),內(nèi)核會(huì)為程序創(chuàng)建一個(gè)進(jìn)程空間,并且會(huì)為進(jìn)程創(chuàng)建一個(gè)主線程,主線程會(huì)執(zhí)行各種初始化操作,完成后才開(kāi)始執(zhí)行我們?cè)诔绦蛑卸x的main函數(shù)。也就是說(shuō)main函數(shù)其實(shí)并不是主線程最開(kāi)始執(zhí)行的函數(shù),在main函數(shù)執(zhí)行前其實(shí)還發(fā)生了很多的事情:操作系統(tǒng)內(nèi)核為可執(zhí)行程序創(chuàng)建進(jìn)程空間后,會(huì)分別將可執(zhí)行程序文件以及可執(zhí)行程序所依賴的動(dòng)態(tài)庫(kù)文件中的內(nèi)容加載到進(jìn)程的虛擬內(nèi)存地址空間。可執(zhí)行程序以及動(dòng)態(tài)庫(kù)文件中的內(nèi)容是符合蘋果操作系統(tǒng)ABI規(guī)則的mach-o格式的二進(jìn)制數(shù)據(jù),我們必須要將這些數(shù)據(jù)加載到內(nèi)存中,對(duì)應(yīng)的代碼才能被執(zhí)行以及變量才能被訪問(wèn)。我們稱每個(gè)映射到內(nèi)存空間中的可執(zhí)行文件以及動(dòng)態(tài)庫(kù)文件的副本為image(映像)。注意此時(shí)只是將文件加載到內(nèi)存中去并沒(méi)有執(zhí)行任何用戶進(jìn)程的代碼,也沒(méi)有調(diào)用庫(kù)中的任意初始化函數(shù)。當(dāng)所有image加載完畢后,內(nèi)核會(huì)為進(jìn)程創(chuàng)建一個(gè)主線程,并將可執(zhí)行程序的image在內(nèi)存中的地址做為參數(shù)壓入用戶態(tài)的堆棧中,把dyld庫(kù)中的_dyld_start函數(shù)作為主線程執(zhí)行的入口函數(shù)。這時(shí)候內(nèi)核將控制權(quán)交給用戶,系統(tǒng)由核心態(tài)轉(zhuǎn)化為用戶態(tài),dyld庫(kù)來(lái)實(shí)現(xiàn)進(jìn)程在用戶態(tài)下的可執(zhí)行文件以及所有動(dòng)態(tài)庫(kù)的加載和初始化的邏輯。可見(jiàn)一個(gè)程序運(yùn)行時(shí)可執(zhí)行文件以及所有依賴的動(dòng)態(tài)庫(kù)其實(shí)是經(jīng)歷過(guò)了兩次的加載過(guò)程:核心態(tài)下的image的加載,以及用戶態(tài)下的二次加載以及初始化操作。 dyld庫(kù)接管進(jìn)程后,進(jìn)程的主線程將從__dyld_start處開(kāi)始所有用戶態(tài)下代碼的執(zhí)行。
dyld庫(kù)最新版本的開(kāi)源源代碼以及_dyld_start函數(shù)的代碼可以從蘋果的開(kāi)源站點(diǎn):https://opensource.apple.com/source/dyld/dyld-519.2.2/處獲取到。你也可以打開(kāi)URL:https://opensource.apple.com/source/ 來(lái)瀏覽所有蘋果已經(jīng)開(kāi)源了的系統(tǒng)庫(kù)。還有一點(diǎn)需要注意的就是開(kāi)源的代碼不一定是最新的代碼,而且有可能和運(yùn)行時(shí)的代碼有差異,所以如果想了解真實(shí)的實(shí)現(xiàn)原理,最好是配合調(diào)試時(shí)的匯編代碼來(lái)一起分析和閱讀。
我們可以在dyldStartup.s中看到__dyld_start函數(shù)的各種平臺(tái)下的實(shí)現(xiàn),下面是一段arm64架構(gòu)下的匯編代碼,函數(shù)的定義大體如下:
#if __arm64__
.data
.align 3
__dso_static:
.quad ___dso_handle
.text
.align 2
.globl __dyld_start
__dyld_start:
mov x28, sp
and sp, x28, #~15 // force 16-byte alignment of stack
mov x0, #0
mov x1, #0
stp x1, x0, [sp, #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp, #16 // make room for local variables
ldr x0, [x28] // get app's mh into x0
ldr x1, [x28, #8] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add x2, x28, #16 // get argv into x2
adrp x4,___dso_handle@page
add x4,x4,___dso_handle@pageoff // get dyld's mh in to x4
adrp x3,__dso_static@page
ldr x3,[x3,__dso_static@pageoff] // get unslid start of dyld
sub x3,x4,x3 // x3 now has slide of dyld
mov x5,sp // x5 has &startGlue
// call dyldbootstrap::start(app_mh, argc, argv, slide, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPK12macho_headeriPPKclS2_Pm
mov x16,x0 // save entry point address in x16
ldr x1, [sp]
cmp x1, #0
b.ne Lnew
// LC_UNIXTHREAD way, clean up stack and jump to result
add sp, x28, #8 // restore unaligned stack pointer without app mh
br x16 // jump to the program's entry point
// LC_MAIN case, set up stack for call to main()
Lnew: mov lr, x1 // simulate return address into _start in libdyld.dylib
ldr x0, [x28, #8] // main param1 = argc
add x1, x28, #16 // main param2 = argv
add x2, x1, x0, lsl #3
add x2, x2, #8 // main param3 = &env[0]
mov x3, x2
Lapple: ldr x4, [x3]
add x3, x3, #8
cmp x4, #0
b.ne Lapple // main param4 = apple
br x16 //調(diào)用main函數(shù)
#endif // __arm64__
將匯編代碼翻譯為高級(jí)語(yǔ)言的偽代碼可以簡(jiǎn)單理解為:
void __dyld_start(const struct macho_header* appsMachHeader, int argc, char *[] argv)
{
intptr_t slide = dyld的image在內(nèi)存中的偏移量。
const struct macho_header *dyldsMachHeader = dyld庫(kù)的macho_header的地址。
void (*startGlue)(int); //膠水函數(shù)地址。
//調(diào)用dyldbootstrap::start函數(shù)并返回用戶的main函數(shù)的入口地址,并且最后一個(gè)參數(shù)返回一個(gè)膠水函數(shù)地址
int (*main)(int argc, char*[] argv) = dyldbootstrap::start(appsMachHeader, argc, argv, slide, dyldsMachHeader, &startGlue);
//執(zhí)行用戶定義的main函數(shù)
int ret = main(argc, argv);
//執(zhí)行膠水代碼,內(nèi)部其實(shí)就是調(diào)用了exit函數(shù)來(lái)結(jié)束進(jìn)程
startGlue(ret);
}
這里需要說(shuō)明一下,上面的匯編代碼并沒(méi)有出現(xiàn)調(diào)用startGlue的地方,但是高級(jí)語(yǔ)言偽代碼中又出現(xiàn)了,原因是最后的 br x16 指令只是一個(gè)簡(jiǎn)單的跳轉(zhuǎn)到main函數(shù)的指令而非是函數(shù)調(diào)用指令,而dyldbootstrap::start函數(shù)的最后一個(gè)輸出參數(shù)&startGlue其實(shí)是保存到棧頂sp中的,因此當(dāng)main函數(shù)執(zhí)行完畢并返回后就會(huì)把棧頂sp中保存的startGlue地址賦值給pc寄存器,從而實(shí)現(xiàn)了對(duì)startGlue函數(shù)的調(diào)用。那么dyldbootstrap::start最后一個(gè)參數(shù)返回并保存到startGlue中的又是一個(gè)什么函數(shù)地址呢?這個(gè)函數(shù)地址是libdyld.dylib(注意dyld和libdyld.dylib是兩個(gè)不同的庫(kù))庫(kù)中的一個(gè)靜態(tài)函數(shù)start。它的實(shí)現(xiàn)很簡(jiǎn)單:
//注意這個(gè)函數(shù)是在libdyld.dylib中被定義,而非在dyld中定義。
void start(int ret)
{
exit(ret);
}
小知識(shí)點(diǎn):當(dāng)我們查看主線程的調(diào)用棧時(shí)發(fā)現(xiàn)調(diào)用棧的最底端的函數(shù)是libdyld庫(kù)中的start函數(shù),而非dyld庫(kù)中的__dyld_start函數(shù)。同時(shí)當(dāng)你切換到start函數(shù)的匯編代碼處時(shí),你會(huì)發(fā)現(xiàn)它并沒(méi)有調(diào)用main函數(shù)的痕跡。原因就是在調(diào)用main函數(shù)之前,其實(shí)棧頂寄存器中的值保存的是start函數(shù)的地址,而非br x16的下條指令的地址 并且br指令只是跳轉(zhuǎn)并不會(huì)執(zhí)行壓棧的動(dòng)作,所以在查看主線程調(diào)用棧時(shí)您所看到的棧底函數(shù)就是start而非__dyld_start了。
從__dyld_start函數(shù)的實(shí)現(xiàn)中可以看出它總共做了三件事:
- dyldbootstrap::start函數(shù)執(zhí)行所有庫(kù)的初始化,執(zhí)行所有OC類的+load的方法,執(zhí)行所有C++全局對(duì)象的構(gòu)造函數(shù),執(zhí)行帶有*attribute*(constructor)定義的C函數(shù)。
- main函數(shù)執(zhí)行用戶的主功能代碼。
- startGlue函數(shù)執(zhí)行exit退出程序,收回資源,結(jié)束進(jìn)程。
在這里我不打算深入的去介紹dyldbootstrap::start函數(shù)的實(shí)現(xiàn),詳細(xì)情況大家可以去閱讀源代碼。
- dyldbootstrap::start函數(shù)內(nèi)部主要調(diào)用了dyld::_main函數(shù)。
- dyld::main函數(shù)內(nèi)部會(huì)根據(jù)依賴關(guān)系遞歸的為每個(gè)加載的動(dòng)態(tài)庫(kù)構(gòu)建一個(gè)對(duì)應(yīng)的ImageLoaderMachO對(duì)象,并添加到一個(gè)全局的數(shù)組sImageRoots中去,最后再調(diào)用dyld::initializeMainExecutable函數(shù)。
- dyld::initializeMainExecutable函數(shù)內(nèi)部的實(shí)現(xiàn)主要就是則遍歷全局?jǐn)?shù)組sImageRoots中的每個(gè)ImageLoaderMachO對(duì)象,并分別調(diào)用每個(gè)對(duì)象的runInitializers方法來(lái)執(zhí)行動(dòng)態(tài)庫(kù)的各種初始化邏輯,最后再調(diào)用主程序的ImageLoaderMachO的runInitializers方法來(lái)執(zhí)行主程序的各種初始化邏輯。
- ImageLoaderMachO是一個(gè)C++類,類里面的runInitializers方法內(nèi)部主要是調(diào)用類中的成員函數(shù)processInitializers來(lái)處理各種初始化邏輯。
- processInitializers方法內(nèi)部的實(shí)現(xiàn)主要調(diào)用動(dòng)態(tài)庫(kù)自身所依賴的其他動(dòng)態(tài)庫(kù)的ImageLoaderMachO對(duì)象的recursiveInitialization方法。
- recursiveInitialization方法內(nèi)部的主要實(shí)現(xiàn)是首先調(diào)用dyld::notifySingle函數(shù)來(lái)初始化所有objc相關(guān)的信息,比如執(zhí)行這個(gè)庫(kù)里面的所有類定義的+load的方法;然后再調(diào)用doInitialization方法來(lái)進(jìn)一步執(zhí)行初始化的動(dòng)作。
- doInitialization方法內(nèi)部首先調(diào)用doImageInit來(lái)執(zhí)行映像的初始化函數(shù),也就是LC_ROUTINES_COMMAND中記錄的函數(shù)(這個(gè)函數(shù)就是在構(gòu)建動(dòng)態(tài)庫(kù)時(shí)的編譯選項(xiàng)中指定的那個(gè)初始執(zhí)行函數(shù));然后再執(zhí)行doModInitFunctions方法來(lái)執(zhí)行所有庫(kù)內(nèi)的全局C++對(duì)象的構(gòu)造函數(shù),以及所有帶有*attribute*(constructor)標(biāo)志的C函數(shù)。

自此,所有main函數(shù)之前的邏輯代碼都已經(jīng)被執(zhí)行完畢了??赡苣銜?huì)問(wèn)整個(gè)過(guò)程中還是沒(méi)有看到關(guān)于C++全局對(duì)象構(gòu)造函數(shù)是如何被執(zhí)行的?關(guān)于這個(gè)問(wèn)題,我們先暫停一下,而是首先來(lái)考察一下當(dāng)一個(gè)進(jìn)程被結(jié)束前系統(tǒng)到底做了什么。
進(jìn)程結(jié)束時(shí)我們能做什么?
當(dāng)我們雙擊home鍵然后滑動(dòng)手勢(shì)來(lái)終止某個(gè)進(jìn)程或者手動(dòng)調(diào)用exit函數(shù)時(shí)會(huì)結(jié)束進(jìn)程的執(zhí)行。當(dāng)進(jìn)程被結(jié)束時(shí)操作系統(tǒng)會(huì)回收進(jìn)程所使用的資源,比如打開(kāi)的文件、分配的內(nèi)存等等。進(jìn)程有可能會(huì)主動(dòng)結(jié)束,也有可能被動(dòng)的結(jié)束,因此操作系統(tǒng)提供了一系列注冊(cè)進(jìn)程結(jié)束回調(diào)函數(shù)的能力。在進(jìn)程結(jié)束前會(huì)調(diào)用這些回調(diào)函數(shù),因此我們可以通過(guò)進(jìn)程結(jié)束回調(diào)函數(shù)來(lái)執(zhí)行一些特定資源回收或者一些善后收尾的工作。注冊(cè)進(jìn)程結(jié)束回調(diào)函數(shù)的函數(shù)定義如下:
#include <stdlib.h>
//注冊(cè)一個(gè)進(jìn)程結(jié)束時(shí)會(huì)被調(diào)用的C函數(shù),函數(shù)的格式為:void func()。 atexit如果注冊(cè)成功返回0,否則返回負(fù)數(shù)。
int atexit(void (*func)(void));
//注冊(cè)一個(gè)進(jìn)程結(jié)束時(shí)會(huì)被調(diào)用的block塊,block塊的格式為:^{}。 atexit_b如果注冊(cè)成功返回0,否則返回負(fù)數(shù)。
int atexit_b(void (^block)(void));
//注冊(cè)一個(gè)進(jìn)程結(jié)束時(shí)會(huì)被調(diào)用的C++無(wú)參數(shù)成員函數(shù),__cxa_atexit并沒(méi)有對(duì)外公開(kāi),而只是供編譯器來(lái)使用,后面的C++對(duì)象的析構(gòu)函數(shù)調(diào)用就要用到它!
int __cxa_atexit(void (*func)(void *), void *arg, void *dso)
上面的三個(gè)函數(shù)分別用來(lái)注冊(cè)進(jìn)程結(jié)束時(shí)的標(biāo)準(zhǔn)C函數(shù)、block代碼、C++函數(shù)??梢宰?cè)多個(gè)進(jìn)程結(jié)束回調(diào)函數(shù),并且系統(tǒng)是按照后注冊(cè)先執(zhí)行的后進(jìn)先出的順序來(lái)執(zhí)行所有回調(diào)函數(shù)代碼的。比如下面的代碼:
void foo1()
{
printf("foo1\n");
}
void foo2()
{
printf("foo2\n");
}
int main(int argc, char* [] argv)
{
atexit(&foo1);
atexit(&foo2);
printf("main\n");
return 0;
}
//當(dāng)程序結(jié)束時(shí)顯示的結(jié)果如下:
//main
//foo2
//foo1
從上面提供的三種注冊(cè)方法,以及回調(diào)函數(shù)的執(zhí)行順序其實(shí)我們可以大體了解到系統(tǒng)是如何存儲(chǔ)這些回調(diào)函數(shù)的,我們可以通過(guò)如下的數(shù)據(jù)結(jié)構(gòu)清楚的看到:
//代碼來(lái)自于:https://opensource.apple.com/source/Libc/Libc-1044.1.2/stdlib/FreeBSD/atexit.c.auto.html
//注冊(cè)回調(diào)函數(shù)的類型。
#define ATEXIT_FN_EMPTY 0
#define ATEXIT_FN_STD 1
#define ATEXIT_FN_CXA 2
#define ATEXIT_FN_BLK 3
struct atexit {
struct atexit *next; /* next in list */
int ind; /* next index in this table */
struct atexit_fn {
int fn_type; /* ATEXIT_? from above */
union { //聯(lián)合體中保存的是注冊(cè)函數(shù)的函數(shù)地址
void (*std_func)(void);
void (*cxa_func)(void *);
void (^block)(void);
} fn_ptr; /* function pointer */
void *fn_arg; /* argument for CXA callback */
void *fn_dso; /* shared module handle */
} fns[ATEXIT_SIZE]; /* the table itself ATEXIT_SIZE = 32*/
};
//系統(tǒng)定義的一個(gè)后進(jìn)行先出的表頭全局變量。
static struct atexit *__atexit; /* points to head of LIFO stack */
struct atexit是一個(gè)鏈表和數(shù)組的結(jié)合體。用圖形表示所有注冊(cè)的函數(shù)的存儲(chǔ)結(jié)構(gòu)大體如下:

從數(shù)據(jù)結(jié)構(gòu)的定義以及atexit函數(shù)的描述和上面的圖形我們應(yīng)該可以很容易的去實(shí)現(xiàn)那三個(gè)注冊(cè)函數(shù)。大家可以去閱讀上面三個(gè)函數(shù)的實(shí)現(xiàn),這里就不再列出了。
上面說(shuō)了進(jìn)程結(jié)束回調(diào)注冊(cè)函數(shù)會(huì)在進(jìn)程結(jié)束時(shí)被調(diào)用,而進(jìn)程結(jié)束的函數(shù)是exit函數(shù),因此可以很容易就想到這些回調(diào)函數(shù)的執(zhí)行肯定是在exit函數(shù)內(nèi)部調(diào)用的,事實(shí)也確實(shí)如此,通過(guò)匯編代碼查看exit的實(shí)現(xiàn)如下:
libsystem_c.dylib`exit:
0x1838a7088 <+0>: stp x20, x19, [sp, #-0x20]!
0x1838a708c <+4>: stp x29, x30, [sp, #0x10]
0x1838a7090 <+8>: add x29, sp, #0x10 ; =0x10
0x1838a7094 <+12>: mov x19, x0
0x1838a7098 <+16>: mov x0, #0x0
0x1838a709c <+20>: bl 0x1838fdc30 ; __cxa_finalize
0x1838a70a0 <+24>: adrp x8, 200782
0x1838a70a4 <+28>: ldr x8, [x8, #0xf20]
0x1838a70a8 <+32>: cbz x8, 0x1838a70b0 ; <+40>
0x1838a70ac <+36>: blr x8
0x1838a70b0 <+40>: mov x0, x19
0x1838a70b4 <+44>: bl 0x1839702e4 ; __exit
上面的匯編翻譯為高級(jí)語(yǔ)言偽代碼大體如下:
void exit(int ret)
{
__cxa_finalize(NULL);
__exit(ret);
}
__cxa_finalize函數(shù)字面上是用于結(jié)束所有C++對(duì)象,但實(shí)際上卻負(fù)責(zé)調(diào)用所有注冊(cè)了進(jìn)程結(jié)束回調(diào)函數(shù)的代碼。__exit函數(shù)內(nèi)部則是實(shí)際的進(jìn)程結(jié)束操作。 __cxa_finalize函數(shù)的源代碼大體如下:
//代碼來(lái)自于:https://opensource.apple.com/source/Libc/Libc-1044.1.2/stdlib/FreeBSD/atexit.c.auto.html
void __cxa_finalize(const void *dso)
{
if (dso != NULL) {
// Note: this should not happen as only dyld should be calling
// this and dyld has switched to call __cxa_finalize_ranges directly.
struct __cxa_range_t range;
range.addr = dso;
range.length = 1;
__cxa_finalize_ranges(&range, 1);
} else {
__cxa_finalize_ranges(NULL, 0);
}
}
//__cxa_finalize函數(shù)內(nèi)部調(diào)用了__cxa_finalize_ranges函數(shù),下面是這個(gè)函數(shù)的定義。
//這個(gè)函數(shù)和實(shí)際的函數(shù)有出入,并且為了讓大家更加容易理解我把一些認(rèn)為不必要的代碼給刪除了.
/*
* Call handlers registered via __cxa_atexit/atexit that are in a
* a range specified.
* Note: rangeCount==0, means call all handlers.
*/
void
__cxa_finalize_ranges(const struct __cxa_range_t ranges[], unsigned int count)
{
struct atexit *p;
struct atexit_fn *fn;
int n;
for (p = __atexit; p; p = p->next) {
for (n = p->ind; --n >= 0;) {
fn = &p->fns[n];
if (fn->fn_type == ATEXIT_FN_EMPTY) {
continue; // already been called
}
// Clear the entry to indicate that this handler has been called.
int fn_type = fn->fn_type;
fn->fn_type = ATEXIT_FN_EMPTY;
// Call the handler. 下面會(huì)根據(jù)不同的類型來(lái)執(zhí)行不同的回調(diào)函數(shù)。
if (fn_type == ATEXIT_FN_CXA) {
fn->fn_ptr.cxa_func(fn->fn_arg);
} else if (fn_type == ATEXIT_FN_STD) {
fn->fn_ptr.std_func();
} else if (fn_type == ATEXIT_FN_BLK) {
fn->fn_ptr.block();
}
}
}
}
三種進(jìn)程結(jié)束回調(diào)函數(shù)中只有注冊(cè)類型為C++的函數(shù)才帶有一個(gè)參數(shù),而其他兩類函數(shù)都不帶參數(shù),這樣的做的原因就是專門為調(diào)用全局C++對(duì)象的析構(gòu)函數(shù)而服務(wù)的。
異常退出和abort函數(shù)
如果進(jìn)程正常退出,最終都會(huì)執(zhí)行exit函數(shù)。exit函數(shù)內(nèi)部會(huì)調(diào)用atexit函數(shù)注冊(cè)的所有回調(diào),以便有時(shí)間進(jìn)行一些資源的回收工作。而如果我們的應(yīng)用出現(xiàn)了異常而導(dǎo)致進(jìn)程結(jié)束則并不會(huì)激發(fā)進(jìn)程結(jié)束回調(diào)函數(shù)的調(diào)用,系統(tǒng)異常出現(xiàn)時(shí)會(huì)產(chǎn)生中斷,操作系統(tǒng)會(huì)接管異常,并對(duì)異常進(jìn)行分析,最后將分析的結(jié)果再交給用戶進(jìn)程,并執(zhí)行用戶進(jìn)程的std::terminate方法來(lái)終止進(jìn)程。std::terminate方法內(nèi)部會(huì)調(diào)用通過(guò)NSSetUncaughtExceptionHandler函數(shù)注冊(cè)的未處理異?;卣{(diào)函數(shù),來(lái)給我們機(jī)會(huì)處理產(chǎn)生崩潰的異常,處理完成最后再結(jié)束進(jìn)程。
我們也可以調(diào)用abort函數(shù)來(lái)終止進(jìn)程的執(zhí)行,abort函數(shù)的內(nèi)部并不會(huì)調(diào)用atexit函數(shù)注冊(cè)的所有回調(diào),也就是說(shuō)通過(guò)abort函數(shù)來(lái)終止進(jìn)程時(shí),并不會(huì)給我們機(jī)會(huì)來(lái)進(jìn)行任何資源的回收處理,而是簡(jiǎn)單的在內(nèi)部簡(jiǎn)單粗暴的調(diào)用__pthread_kill方法來(lái)殺死主線程,并終止進(jìn)程。
通過(guò)上面對(duì)main函數(shù)執(zhí)行前所做的事情,以及進(jìn)程結(jié)束前我們能做的事情的介紹,您是否又對(duì)程序的啟動(dòng)時(shí)和結(jié)束時(shí)所發(fā)生的一切有了更加深入的理解??墒沁@似乎離我要說(shuō)的C++全局對(duì)象的構(gòu)造和析構(gòu)更加遙遠(yuǎn)了,當(dāng)然也許你不會(huì)這么認(rèn)為,因?yàn)橥ㄟ^(guò)我上面的介紹,你也許對(duì)C++全局對(duì)象的構(gòu)造和析構(gòu)的時(shí)機(jī)有了一些想法,這些都沒(méi)有關(guān)系,這也是我下面將要詳細(xì)介紹的。
再論C++的全局對(duì)象的構(gòu)造和析構(gòu)
就如本文的開(kāi)始部分的一個(gè)例子,對(duì)于非全局的C++對(duì)象的構(gòu)造和析構(gòu)函數(shù)的調(diào)用總是在調(diào)用者的函數(shù)內(nèi)部完成,這時(shí)候存在著明顯的函數(shù)上下文的調(diào)用結(jié)構(gòu)。但是當(dāng)我們定義了一個(gè)C++全局對(duì)象時(shí)因?yàn)闆](méi)有明顯的可執(zhí)行代碼的上下文,所以我們無(wú)法很清楚的了解到全局對(duì)象的構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)用時(shí)機(jī)。為了實(shí)現(xiàn)全局對(duì)象的構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)用,此時(shí)我們就需要編譯器來(lái)出馬幫助我們做一些事情了! 我們知道其實(shí)C++編譯器會(huì)在我們的源代碼的基礎(chǔ)上增加非常多的隱式代碼,對(duì)于每個(gè)定義的全局對(duì)象也是如此的。
當(dāng)我們?cè)谀硞€(gè).mm文件或者.cpp文件中定義了全局變量時(shí)比如下面某個(gè)文件的代碼:
//CA.h
class CA
{
public:
CA();
void ~CA();
};
//CA.mm
#include "CA.h"
CA::CA()
{
printf("CA::CA()\n");
}
void CA::~CA()
{
printf("CA::~CA()\n");
}
//MyTest.cpp
#include "CA.h"
//假設(shè)這里定義了兩個(gè)全局變量
CA a;
CA b;
當(dāng)編譯器在編譯MyTest.cpp文件時(shí)發(fā)現(xiàn)其中定義了全局C++對(duì)象,那么除了會(huì)將全局對(duì)象變量保存在數(shù)據(jù)段(.data)外,還會(huì)為每個(gè)全局變量定義一個(gè)靜態(tài)的全局變量初始化函數(shù)。其命名的規(guī)則如下:
//按照全局對(duì)象在文件中定義的順序,第一個(gè)沒(méi)有數(shù)字序列,后面定義的則按數(shù)字序列遞增。
static ___cxx_global_var_init.<數(shù)字序列>();
同時(shí)會(huì)以定義全局變量的文件名為標(biāo)志定義一個(gè)如下的靜態(tài)函數(shù):
static void _GLOBAL__sub_I_<文件名>(int argc, char **argv, char** env, char **apple, void * programVars);
因此當(dāng)編譯上面的MyTest.cpp文件時(shí),其實(shí)最真實(shí)的文件的內(nèi)容是如下的:
//MyTest.cpp
#include "CA.h"
struct CA a;
struct CA b;
//全局對(duì)象a的初始化函數(shù)。
static void ___cxx_global_var_init()
{
CA::CA(&a);
//這代碼很有意思,將CA類的析構(gòu)函數(shù)的地址和a的地址通過(guò)__cxa_atexit函數(shù)進(jìn)行注冊(cè),以便當(dāng)進(jìn)程結(jié)束時(shí)調(diào)用。
__cxa_atexit(&CA::~CA(), &a, NULL);
}
//全局對(duì)象b的初始化函數(shù)。
static void ___cxx_global_var_init.1()
{
CA::CA(&b);
__cxa_atexit(&CA::~CA(), &b, NULL);
}
//本文件內(nèi)的所有全局對(duì)象的初始化函數(shù)。
static void _GLOBAL__sub_I_MyTest.cpp(int argc, char **argv, char** env, char **apple, void * programVars)
{
___cxx_global_var_init();
___cxx_global_var_init.1();
}
從上面的代碼中我們可以看出每個(gè)全局對(duì)象的初始化函數(shù)都其實(shí)是做了兩件事:
- 調(diào)用對(duì)象類的構(gòu)造函數(shù)。
- 通過(guò)__cxa_atexit函數(shù)來(lái)注冊(cè)進(jìn)程結(jié)束時(shí)的析構(gòu)回調(diào)函數(shù)。
前面我曾經(jīng)說(shuō)過(guò)__cxa_atexit這個(gè)函數(shù)并沒(méi)有對(duì)外暴露,而是留給編譯器以及內(nèi)部使用,這個(gè)函數(shù)接收三個(gè)參數(shù):一個(gè)函數(shù)指針,一個(gè)對(duì)象指針,一個(gè)庫(kù)指針。我們知道所有C++類定義的函數(shù)其實(shí)都是有一個(gè)隱藏的this參數(shù)的,析構(gòu)函數(shù)也一樣。還記得上面的__cxa_finalize_ranges函數(shù)內(nèi)部是如何調(diào)用注冊(cè)的C++函數(shù)的嗎?
fn->fn_ptr.cxa_func(fn->fn_arg);
//因?yàn)槲覀冏?cè)時(shí),注冊(cè)的是類的析構(gòu)函數(shù)的地址,以及全局對(duì)象本身:
__cxa_atexit(&CA::~CA(), &a, NULL);
//所以在最終進(jìn)程終止時(shí)其實(shí)調(diào)用的是:
CA::~CA(&a) 方法,也就是調(diào)用的是全局對(duì)象的析構(gòu)函數(shù)??!
可以看出系統(tǒng)采用了一個(gè)非常巧妙的方法,借助__cxa_atexit函數(shù)來(lái)實(shí)現(xiàn)全局對(duì)象析構(gòu)函數(shù)的調(diào)用。那么問(wèn)題又來(lái)了?對(duì)象的構(gòu)造函數(shù)又是再哪里調(diào)用的呢?換句話說(shuō)_GLOBAL__sub_I_MyTest.cpp()這個(gè)函數(shù)又是在哪里被調(diào)用的呢?
這就需要我們?nèi)チ私庖幌耺ach-o文件的結(jié)構(gòu)了,關(guān)于mach-o文件結(jié)構(gòu)的介紹這就不再贅述,大家可以到網(wǎng)上去參考閱讀相關(guān)的文章。
可以明確的就是當(dāng)我們定義了全局對(duì)象并生成了_GLOBAL__sub_I_XXX系列的函數(shù)時(shí)或者當(dāng)我們的代碼中存在著attribute(constructor)聲明的C函數(shù)時(shí),系統(tǒng)在編譯過(guò)程中為了能在進(jìn)程啟動(dòng)時(shí)調(diào)用這些函數(shù)來(lái)初始化全局對(duì)象,會(huì)在數(shù)據(jù)段__DATA下建立一個(gè)名為_(kāi)_mod_init_func的section,并把所有需要在程序啟動(dòng)時(shí)需要執(zhí)行的初始化的函數(shù)的地址保存到__mod_init_func這個(gè)section中。 我們可以從下面mach-o view這個(gè)工具中看到我們所有的注冊(cè)的信息。

您是否還記得前面介紹的main函數(shù)執(zhí)行前所執(zhí)行的代碼流程,在那些代碼中,有一個(gè)名叫ImageLoaderMachO::doModInitFunctions的函數(shù)就是專門用來(lái)負(fù)責(zé)執(zhí)行__DATA下的__mod_init_func中注冊(cè)的所有函數(shù)的,我們可以來(lái)看看這段代碼的實(shí)現(xiàn):
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 = §ionsStart[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 uint32_t count = sect->size / sizeof(uintptr_t);
for (uint32_t i=0; i < count; ++i) {
Initializer func = inits[i];
if ( context.verboseInit )
dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
//這里執(zhí)行所有注冊(cè)了的需要初始化就被執(zhí)行的代碼。
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
}
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
}
因此可以看出上面定義的__GLOBAL__sub_I_MyTest.cpp函數(shù)就是在doModInitFunctions函數(shù)內(nèi)部被執(zhí)行。
從上面的macho-view展示的圖表來(lái)看,全局對(duì)象的構(gòu)造函數(shù)以及聲明了attribute(constructor)的C函數(shù)都會(huì)記錄在*DATA*,_mod_init_func這個(gè)section中并且會(huì)在doModInitFunctions函數(shù)內(nèi)部被執(zhí)行。那么對(duì)于一個(gè)聲明了attribute(destructor)的C函數(shù)呢?它又是如何在進(jìn)程結(jié)束前被執(zhí)行的呢?答案就在DATA,_mod_term_func這個(gè)section中,系統(tǒng)在編譯時(shí)會(huì)將所有帶attribute(destructor)聲明的函數(shù)地址記錄到這個(gè)section中。還記得上面程序啟動(dòng)初始化時(shí)會(huì)有一個(gè)環(huán)節(jié)調(diào)用dyld::initializeMainExecutable函數(shù)嗎?
//dyld.cpp中的代碼
//為了能夠看得更加清晰,這里面我會(huì)刪除一些不必要的代碼。
void initializeMainExecutable()
{
//..... 其他邏輯。
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
//.... 其他邏輯。
}
可以清楚的看到里面又是用了cxa_atexit方法來(lái)注冊(cè)了一個(gè)進(jìn)程結(jié)束時(shí)的回調(diào)函數(shù)runAllStaticTerminators。繼續(xù)來(lái)跟蹤函數(shù)的實(shí)現(xiàn):
//dyld.cpp中的代碼
static void runAllStaticTerminators(void* extra)
{
try {
const size_t imageCount = sImageFilesNeedingTermination.size();
for(size_t i=imageCount; i > 0; --i){
ImageLoader* image = sImageFilesNeedingTermination[i-1];
//這里遍歷每個(gè)動(dòng)態(tài)庫(kù)并執(zhí)行其中的doTermination方法。
image->doTermination(gLinkContext);
}
sImageFilesNeedingTermination.clear();
notifyBatch(dyld_image_state_terminated, false);
}
catch (const char* msg) {
halt(msg);
}
}
繼續(xù)來(lái)看ImageLoaderMachO::doTermination的內(nèi)部實(shí)現(xiàn):
//ImageLoaderMachO.cpp
void ImageLoaderMachO::doTermination(const LinkContext& context)
{
if ( fHasTerminators ) {
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 = §ionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
//type == S_MOD_TERM_FUNC_POINTERS的section就是上面說(shuō)到的名為_(kāi)mod_term_func的section.
if ( type == S_MOD_TERM_FUNC_POINTERS ) {
// <rdar://problem/23929217> Ensure section is within segment
if ( (sect->addr < seg->vmaddr) || (sect->addr+sect->size > seg->vmaddr+seg->vmsize) || (sect->addr+sect->size < sect->addr) )
dyld::throwf("DOF section has malformed address range for %s\n", this->getPath());
Terminator* terms = (Terminator*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);
for (size_t j=count; j > 0; --j) {
Terminator func = terms[j-1];
// <rdar://problem/8543820&9228031> verify terminators are in image
if ( ! this->containsAddress((void*)func) ) {
dyld::throwf("termination function %p not in mapped image for %s\n", func, this->getPath());
}
if ( context.verboseInit )
dyld::log("dyld: calling termination function %p in %s\n", func, this->getPath());
func(); //這就是那些注冊(cè)了的函數(shù)。
}
}
}
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
可見(jiàn)帶有attribute(destructor)聲明的函數(shù),也是在系統(tǒng)初始化時(shí)通過(guò)了atexit的機(jī)制來(lái)實(shí)現(xiàn)進(jìn)程結(jié)束時(shí)的調(diào)用的。
上面就是我要介紹的C++全局對(duì)象的構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)用以及實(shí)現(xiàn)的所有過(guò)程。我們從上面的章節(jié)中還可以了解到程序在啟動(dòng)和退出這個(gè)階段所做的事情,以及我們所能做的事情。
最后還有一個(gè)問(wèn)題需要解決:那就是我們知道所有的庫(kù)的加載以及初始化操作都是通過(guò)dyld這個(gè)庫(kù)來(lái)處理的。也就是一個(gè)進(jìn)程在用戶態(tài)最先運(yùn)行的代碼是dyld庫(kù)中的代碼,但是dyld庫(kù)中本身也用到了一些全局的C++對(duì)象比如vector數(shù)組來(lái)存儲(chǔ)所有的ImageLoaderMachO對(duì)象:
//https://opensource.apple.com/source/dyld/dyld-519.2.2/src/dyld.cpp.auto.html
static std::vector<ImageLoader*> sAllImages;
static std::vector<ImageLoader*> sImageRoots;
static std::vector<ImageLoader*> sImageFilesNeedingTermination;
static std::vector<RegisteredDOF> sImageFilesNeedingDOFUnregistration;
static std::vector<ImageCallback> sAddImageCallbacks;
static std::vector<ImageCallback> sRemoveImageCallbacks;
dyld要加載所有其他的庫(kù)并且調(diào)用每個(gè)庫(kù)的初始化函數(shù)來(lái)構(gòu)造庫(kù)內(nèi)定義的全局C++對(duì)象,那么dyld庫(kù)本身所定義的全局C++對(duì)象的構(gòu)造函數(shù)又是如何被初始化的呢?很顯然我們不可能在doModInitFunctions中進(jìn)行初始化操作,而是必須要將初始化全局對(duì)象的邏輯放到加載其他庫(kù)之前做處理。要想回答這個(gè)問(wèn)題我們可以再次考察一下dyldbootstrap::start函數(shù)的實(shí)現(xiàn):
uintptr_t start(const struct macho_header* appsMachHeader, int argc, const char* argv[],
intptr_t slide, const struct macho_header* dyldsMachHeader,
uintptr_t* startGlue)
{
// if kernel had to slide dyld, we need to fix up load sensitive locations
// we have to do this before using any global variables
if ( slide != 0 ) {
rebaseDyld(dyldsMachHeader, slide);
}
// allow dyld to use mach messaging
mach_init();
// kernel sets up env pointer to be just past end of agv array
const char** envp = &argv[argc+1];
// kernel sets up apple pointer to be just past end of envp array
const char** apple = envp;
while(*apple != NULL) { ++apple; }
++apple;
// set up random value for stack canary
__guard_setup(apple);
#if DYLD_INITIALIZER_SUPPORT
// run all C++ initializers inside dyld
//這句話是關(guān)鍵,dyld在初始化其他庫(kù)之前會(huì)調(diào)用這個(gè)函數(shù)來(lái)調(diào)用庫(kù)自身的所有全局C++對(duì)象的構(gòu)造函數(shù)。
runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif
// now that we are done bootstrapping dyld, call dyld's main
//下面的代碼是用來(lái)初始化可執(zhí)行程序以及其所依賴的所有動(dòng)態(tài)庫(kù)的。
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
start函數(shù)中在加載并初始化其他庫(kù)之前有調(diào)用函數(shù)runDyldInitializers
下面的代碼就是runDyldInitializers的實(shí)現(xiàn),可以看出其他就是一個(gè)doModInitFunctions函數(shù)的簡(jiǎn)化版本的實(shí)現(xiàn)。
extern const Initializer inits_start __asm("section$start$__DATA$__mod_init_func");
extern const Initializer inits_end __asm("section$end$__DATA$__mod_init_func");
//
// For a regular executable, the crt code calls dyld to run the executables initializers.
// For a static executable, crt directly runs the initializers.
// dyld (should be static) but is a dynamic executable and needs this hack to run its own initializers.
// We pass argc, argv, etc in case libc.a uses those arguments
//
static void runDyldInitializers(const struct macho_header* mh, intptr_t slide, int argc, const char* argv[], const char* envp[], const char* apple[])
{
for (const Initializer* p = &inits_start; p < &inits_end; ++p) {
(*p)(argc, argv, envp, apple);
}
}
小知識(shí)點(diǎn):如果我們?cè)诰幊虝r(shí)想要訪問(wèn)自身mach-o文件中的某個(gè)段下的某個(gè)section的數(shù)據(jù)結(jié)構(gòu)時(shí),我們就可以借助上面的匯編代碼:__asm("section[圖片上傳失敗...(image-eb2a1c-1545901486118)]
__DATA$__mod_init_func"); 來(lái)獲取section的開(kāi)頭和結(jié)束的地址區(qū)間。
一個(gè)疑惑的地方
整個(gè)例子中我們定義了一個(gè)C++的類,還定義了beginfunc, endfunc函數(shù),建立了全局對(duì)象,以及一個(gè)main函數(shù)。我們可以通過(guò)nm命令來(lái)看可執(zhí)行程序所有導(dǎo)出的符號(hào)表:
nm /Users/apple/Library/Developer/Xcode/DerivedData/cpptest1-bwxlgbiudmjsyadeqbnivxsezipu/Build/Products/Debug/cpptest1
0000000100001c80 t __GLOBAL__sub_I_MyTest.cpp
0000000100001000 T __Z7endfuncv
0000000100000fe0 T __Z9beginfuncv
0000000100001020 t __ZN2CAC1Ev
0000000100001060 t __ZN2CAC2Ev
0000000100001040 t __ZN2CAD1Ev
0000000100001bc0 t __ZN2CAD2Ev
0000000100001c00 t ___cxx_global_var_init
0000000100001c40 t ___cxx_global_var_init.2
00000001000020f0 S _a
00000001000020f1 S _b
0000000100000fb0 T _main
上面的符號(hào)表我刪除了一些其他的符號(hào),在這里可以看到大寫T標(biāo)志的函數(shù)是非靜態(tài)全局函數(shù),小寫t標(biāo)志的函數(shù)是靜態(tài)函數(shù),S標(biāo)志的符號(hào)是全局變量??梢钥闯龀绦?yàn)榱酥С諧++的全局對(duì)象并初始化需要定義一些附加的函數(shù)來(lái)完成。這里有一個(gè)讓人疑惑的地方就是:
0000000100001020 t __ZN2CAC1Ev
0000000100001060 t __ZN2CAC2Ev
0000000100001040 t __ZN2CAD1Ev
0000000100001bc0 t __ZN2CAD2Ev
這里面定義了2個(gè)CA類的構(gòu)造函數(shù)和析構(gòu)函數(shù),差別只是序號(hào)的不同。根據(jù)匯編代碼轉(zhuǎn)化為高級(jí)語(yǔ)言偽代碼如下:
//這個(gè)函數(shù)只是一個(gè)殼
static void __ZN2CAC1Ev(CA * const this)
{
__ZN2CAC2Ev(this);
}
//這個(gè)是類構(gòu)造函數(shù)的真實(shí)實(shí)現(xiàn)。
static void __ZN2CAC2Ev(CA *const this)
{
printf("CA::CA()\n");
}
//這個(gè)函數(shù)只是一個(gè)殼
static void __ZN2CAD1Ev(CA * const this)
{
__ZN2CAD2Ev(this);
}
//這個(gè)是類析構(gòu)函數(shù)的真實(shí)實(shí)現(xiàn)。
static void __ZN2CAD2Ev(CA *const this)
{
printf("CA::~CA()\n");
}
static void ___cxx_global_var_init()
{
__ZN2CAC1Ev(&a);
__cxa_atexit(& __ZN2CAD1Ev, &a, NULL);
}
上面的代碼中可以看出,系統(tǒng)在編譯時(shí)分別實(shí)現(xiàn)了2個(gè)構(gòu)造函數(shù)和析構(gòu)函數(shù),而且標(biāo)號(hào)為1的函數(shù)內(nèi)部其實(shí)只是簡(jiǎn)單的調(diào)用了標(biāo)號(hào)為2的真實(shí)函數(shù)的實(shí)現(xiàn)。所以當(dāng)我們?cè)谡{(diào)試或者查看崩潰日志時(shí),如果問(wèn)題出現(xiàn)在了全局對(duì)象的構(gòu)造函數(shù)或者析構(gòu)函數(shù)內(nèi)部,我們看到的函數(shù)調(diào)用棧里面會(huì)出現(xiàn)兩個(gè)相同的函數(shù)名字

這個(gè)實(shí)現(xiàn)機(jī)制非常令我迷惑!希望有高手為我答疑解惑。
后記:崩潰的修復(fù)方法
最后我想再來(lái)說(shuō)說(shuō)那個(gè)崩潰事件,本質(zhì)的原因還是對(duì)于全局對(duì)象的使用不當(dāng)導(dǎo)致,當(dāng)進(jìn)程將要被殺死時(shí),主線程執(zhí)行了exit方法的調(diào)用,exit方法內(nèi)部析構(gòu)了所有定義的全局C++對(duì)象,并且當(dāng)主線程在執(zhí)行在全局對(duì)象的析構(gòu)函數(shù)時(shí),如果我們的應(yīng)用中還有其他的常駐線程還在運(yùn)行時(shí),此時(shí)那些線程還并沒(méi)有銷毀或者殺死,也就是一個(gè)進(jìn)程的所有其他線程的終止處理其實(shí)是發(fā)生在exit函數(shù)調(diào)用結(jié)束后才會(huì)發(fā)生的,因此如果一個(gè)常駐線程一直在訪問(wèn)一個(gè)全局對(duì)象時(shí)就有可能存在著隱患以及不確定性。一個(gè)解決的方法就是在全局對(duì)象析構(gòu)函數(shù)調(diào)用前先終止所有其他的線程;另外一個(gè)解決方案是對(duì)全局對(duì)象的訪問(wèn)進(jìn)行加鎖處理以及進(jìn)行是否為空的判斷處理。我們使用的那個(gè)第三方庫(kù)所采用的一個(gè)解決方案是在程序啟動(dòng)后通過(guò)調(diào)用atexit函數(shù)來(lái)注冊(cè)了一個(gè)進(jìn)程結(jié)束回調(diào)函數(shù),然后再那個(gè)回調(diào)函數(shù)里面終止了所有工作線程。因?yàn)榘凑誥texit后進(jìn)先出的規(guī)則,我們手動(dòng)注冊(cè)的進(jìn)程結(jié)束回調(diào)函數(shù)要比C++析構(gòu)的進(jìn)程結(jié)束回調(diào)函數(shù)后添加,所以工作線程的終止邏輯回調(diào)函數(shù)就會(huì)比析構(gòu)函數(shù)調(diào)用要早,從而可以防止問(wèn)題的發(fā)生了。