神奇的崩潰事件
作者:歐陽大哥2013
鏈接:http://www.reibang.com/p/e6300594c966
來源:簡書
簡書著作權歸作者所有仰挣,任何形式的轉載都請聯(lián)系作者獲得授權并注明出處翘簇。
事件源于接入了一個第三方庫導致應用出現(xiàn)了大量的crash記錄,很奇怪的是這么多的crash居然沒有收到用戶的反饋信息维蒙! 在這個過程中每個崩潰棧的信息都明確的指向了是那個第三方庫的某個工作線程產(chǎn)生的崩潰掰吕。這個問題第三方提供者一直無法復現(xiàn),而且我們的RD颅痊、PM殖熟、QA同學在調(diào)試和測試過程中都沒有出現(xiàn)過這個問題。后來再經(jīng)過仔細檢查分析斑响,發(fā)現(xiàn)每次崩潰時的各線程的調(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í)行了殺掉應用的操作。下面主線程的調(diào)用椊⒎#可以看出是用戶主動殺死的進程膊毁。
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)崩潰的機器指令段如下:
0x106bee318<+180>: add x8, x0, #0x8
0x106bee31c<+184>: ldaxr w9,[x8]
注意看下面的x0,x8寄存器中的值已經(jīng)是異常的數(shù)字了,這里對異常地址進行讀取操作產(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是一個執(zhí)行進程結束的函數(shù),因此從調(diào)用棧來看其實這正是用戶在主動殺掉我們的App應用進程時主線程會執(zhí)行的邏輯蛙讥。也就是說出現(xiàn)崩潰的時機就是在主動殺掉我們的應用的時刻發(fā)生的坎藐!
這真的是一個非常神奇的時刻,當我們主動殺掉應用時產(chǎn)生了崩潰能颁,所以整個事件就出現(xiàn)了上面的場景:沒有用戶反饋異常杂瘸、我們自身也很難復現(xiàn)出崩潰的場景(非連機運行時)。
問題復現(xiàn)
分析出原因后為了驗證問題伙菊,通過不停的執(zhí)行手動殺進程的測試败玉,在一個偶然的機會下終于復現(xiàn)了問題:在主線程執(zhí)行exit的時機,那個第三方庫的工作線程的某處出現(xiàn)非法地址訪問镜硕,而停止了執(zhí)行:
這個來之不易的崩潰信息起了非常大的作用运翼,根據(jù)匯編代碼按圖索驥,并和對方進行交流定位到了對應的源代碼兴枯。第三方庫的一個線程是一個常駐線程血淌,它會周期性并且高頻的訪問一個全局C++對象實例的數(shù)據(jù),出現(xiàn)奔潰的原因就是這個全局C++對象的類的構造函數(shù)中從堆里面分配了一塊內(nèi)存,而當進程被終止這個過程中悠夯,這個全局對象被析構癌淮,析構函數(shù)中會將分配的堆內(nèi)存進行回收。但是那個常駐線程因為此刻還沒有被終止沦补,它還像往常一樣繼續(xù)訪問這個已經(jīng)被析構了的全局對象的堆內(nèi)存乳蓄,從而導致了上面圖中的內(nèi)存地址訪問非法的問題。下面就是問題發(fā)生的過程:
C++全局對象
可以肯定一點的就是那個第三方庫由于對全局C++對象的使用不當而產(chǎn)生了問題夕膀。我們知道每個C++對象在創(chuàng)建時都會調(diào)用對應的構造函數(shù)虚倒,而對象銷毀時則會調(diào)用對應的析構函數(shù)。構造和析構函數(shù)都是一段代碼产舞,對象的創(chuàng)建和銷毀一般都是在某個函數(shù)中進行魂奥,這時候對象的構造/析構函數(shù)也是在那個調(diào)用者函數(shù)中執(zhí)行,比如下面的代碼:
class CA{
public:
CA(){
printf("CA::CA()");
}
void ~CA(){
printf("CA::~CA()");
}
};
CA b; //定義一個全局變量
int main()
{
CA a; //函數(shù)內(nèi)建立一個對象
printf("hello");
return 0;
}
系統(tǒng)在編譯C++代碼時會進行一些特定的處理(這里以C語言的形式來描述):
//定義結構體
struct CA{
};
//CA類名稱被重新修飾了的構造函數(shù)
void __ZN2CAC1Ev(CA * const this)
{
printf("CA::CA()");
}
//CA類名稱被重新修飾了的析構函數(shù)
void __ZN2CAD1Ev(CA * const this)
{
printf("CA::~CA()");
}
//?? b對象的構造和析構又是在哪里被調(diào)用執(zhí)行的呢易猫?因為找不到執(zhí)行的上下文捧弃。
struct CA b;
int main()
{
struct CA a;
__ZN2CAC1Ev(&a); //局部對象在對象創(chuàng)建后調(diào)用構造函數(shù)
printf("hello");
__ZN2CAD1Ev(&a); //這里調(diào)用析構函數(shù)
return 0;
}
上面的源代碼中b這個全局對象并不是在某個函數(shù)或者方法內(nèi)部定義,
所以它并沒有執(zhí)行構造函數(shù)以及析構函數(shù)的上下文環(huán)境擦囊,那么是否創(chuàng)建一個全局對象時它的構造函數(shù)以及析構函數(shù)就無法被執(zhí)行呢了违霞?答案是否定的。只要任何一個C++類定義了構造函數(shù)或者析構函數(shù)瞬场,那么在對象創(chuàng)建時總是會調(diào)用構造函數(shù)买鸽,并且在對象銷毀時會調(diào)用對應的析構函數(shù)。那么全局對象的構造函數(shù)和析構函數(shù)又是在什么時候被調(diào)用執(zhí)行的呢贯被?
+load方法
在一個Objective-C類中眼五,可以定義一個+load方法,這個+load方法會在所有OC對象創(chuàng)建前被執(zhí)行彤灶,同時也會在main函數(shù)調(diào)用前被執(zhí)行看幼。一般情況下我們會在類的+load方法中實現(xiàn)一些全局初始化的邏輯。OC類的方法也是要求一定的上下文環(huán)境下才能被執(zhí)行幌陕,那么+load方法又是在什么時候被調(diào)用執(zhí)行的呢诵姜?
全局構造/析構C函數(shù)
除了建立C++全局對象、實現(xiàn)OC類的+load方法來進行一些全局的初始化邏輯外搏熄,我們還可以定義帶有特殊標志的C函數(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;
}
//程序運行時分別輸出
// beginfunc
// main
// endfunc
上面的代碼中可以看出,我們并沒有顯式的調(diào)用beginfunc和endfunc函數(shù)的情況下心例,函數(shù)依然被調(diào)用執(zhí)行宵凌。那么這些函數(shù)又是如何被調(diào)用執(zhí)行的呢?
main函數(shù)執(zhí)行前發(fā)生了什么止后?
操作系統(tǒng)在啟動一個程序時瞎惫,內(nèi)核會為程序創(chuàng)建一個進程空間,并且會為進程創(chuàng)建一個主線程,主線程會執(zhí)行各種初始化操作瓜喇,完成后才開始執(zhí)行我們在程序中定義的main函數(shù)逗扒。也就是說main函數(shù)其實并不是主線程最開始執(zhí)行的函數(shù),在main函數(shù)執(zhí)行前其實還發(fā)生了很多的事情:操作系統(tǒng)內(nèi)核為可執(zhí)行程序創(chuàng)建進程空間后欠橘,會分別將可執(zhí)行程序文件以及可執(zhí)行程序所依賴的動態(tài)庫文件中的內(nèi)容加載到進程的虛擬內(nèi)存地址空間∠帜眨可執(zhí)行程序以及動態(tài)庫文件中的內(nèi)容是符合蘋果操作系統(tǒng)ABI規(guī)則的mach-o格式的二進制數(shù)據(jù)肃续,我們必須要將這些數(shù)據(jù)加載到內(nèi)存中,對應的代碼才能被執(zhí)行以及變量才能被訪問叉袍。我們稱每個映射到內(nèi)存空間中的可執(zhí)行文件以及動態(tài)庫文件的副本為image(映像)始锚。注意此時只是將文件加載到內(nèi)存中去并沒有執(zhí)行任何用戶進程的代碼,也沒有調(diào)用庫中的任意初始化函數(shù)喳逛。當所有image加載完畢后瞧捌,內(nèi)核會為進程創(chuàng)建一個主線程,并將可執(zhí)行程序的image在內(nèi)存中的地址做為參數(shù)壓入用戶態(tài)的堆棧中润文,把dyld庫中的_dyld_start函數(shù)作為主線程執(zhí)行的入口函數(shù)姐呐。這時候內(nèi)核將控制權交給用戶,系統(tǒng)由核心態(tài)轉化為用戶態(tài)典蝌,dyld庫來實現(xiàn)進程在用戶態(tài)下的可執(zhí)行文件以及所有動態(tài)庫的加載和初始化的邏輯曙砂。可見一個程序運行時可執(zhí)行文件以及所有依賴的動態(tài)庫其實是經(jīng)歷過了兩次的加載過程:核心態(tài)下的image的加載,以及用戶態(tài)下的二次加載以及初始化操作骏掀。 dyld庫接管進程后鸠澈,進程的主線程將從__dyld_start處開始所有用戶態(tài)下代碼的執(zhí)行。
dyld庫最新版本的開源源代碼以及_dyld_start函數(shù)的代碼可以從蘋果的開源站點:https://opensource.apple.com/source/dyld/dyld-519.2.2/處獲取到截驮。你也可以打開URL:https://opensource.apple.com/source/ 來瀏覽所有蘋果已經(jīng)開源了的系統(tǒng)庫笑陈。還有一點需要注意的就是開源的代碼不一定是最新的代碼,而且有可能和運行時的代碼有差異葵袭,所以如果想了解真實的實現(xiàn)原理涵妥,最好是配合調(diào)試時的匯編代碼來一起分析和閱讀。
我們可以在dyldStartup.s中看到__dyld_start函數(shù)的各種平臺下的實現(xiàn)坡锡,下面是一段arm64架構下的匯編代碼妹笆,函數(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__
將匯編代碼翻譯為高級語言的偽代碼可以簡單理解為:
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庫的macho_header的地址。
void (*startGlue)(int); //膠水函數(shù)地址。
//調(diào)用dyldbootstrap::start函數(shù)并返回用戶的main函數(shù)的入口地址,并且最后一個參數(shù)返回一個膠水函數(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)部其實就是調(diào)用了exit函數(shù)來結束進程
startGlue(ret);
}
這里需要說明一下踊沸,上面的匯編代碼并沒有出現(xiàn)調(diào)用startGlue的地方肥哎,但是高級語言偽代碼中又出現(xiàn)了,原因是最后的 br x16
指令只是一個簡單的跳轉到main函數(shù)的指令而非是函數(shù)調(diào)用指令项乒,而dyldbootstrap::start函數(shù)的最后一個輸出參數(shù)&startGlue其實是保存到棧頂sp中的廷支,因此當main函數(shù)執(zhí)行完畢并返回后就會把棧頂sp中保存的startGlue地址賦值給pc寄存器施敢,從而實現(xiàn)了對startGlue函數(shù)的調(diào)用仆嗦。那么dyldbootstrap::start最后一個參數(shù)返回并保存到startGlue中的又是一個什么函數(shù)地址呢?這個函數(shù)地址是libdyld.dylib(注意dyld和libdyld.dylib是兩個不同的庫)庫中的一個靜態(tài)函數(shù)start碍岔。它的實現(xiàn)很簡單:
//注意這個函數(shù)是在libdyld.dylib中被定義,而非在dyld中定義蔼啦。
void start(int ret)
{
exit(ret);
}
小知識點:當我們查看主線程的調(diào)用棧時發(fā)現(xiàn)調(diào)用棧的最底端的函數(shù)是libdyld庫中的start函數(shù)榆纽,而非dyld庫中的__dyld_start函數(shù)。同時當你切換到start函數(shù)的匯編代碼處時捏肢,你會發(fā)現(xiàn)它并沒有調(diào)用main函數(shù)的痕跡奈籽。原因就是在調(diào)用main函數(shù)之前,其實棧頂寄存器中的值保存的是start函數(shù)的地址鸵赫,而非br x16的下條指令的地址 并且br指令只是跳轉并不會執(zhí)行壓棧的動作衣屏,所以在查看主線程調(diào)用棧時您所看到的棧底函數(shù)就是start而非__dyld_start了。
從__dyld_start函數(shù)的實現(xiàn)中可以看出它總共做了三件事:
- dyldbootstrap::start函數(shù)執(zhí)行所有庫的初始化辩棒,執(zhí)行所有OC類的+load的方法勾拉,執(zhí)行所有C++全局對象的構造函數(shù)煮甥,執(zhí)行帶有*attribute*(constructor)定義的C函數(shù)。
- main函數(shù)執(zhí)行用戶的主功能代碼藕赞。
- startGlue函數(shù)執(zhí)行exit退出程序成肘,收回資源,結束進程斧蜕。
在這里我不打算深入的去介紹dyldbootstrap::start函數(shù)的實現(xiàn)双霍,詳細情況大家可以去閱讀源代碼。
- dyldbootstrap::start函數(shù)內(nèi)部主要調(diào)用了dyld::_main函數(shù)批销。
- dyld::main函數(shù)內(nèi)部會根據(jù)依賴關系遞歸的為每個加載的動態(tài)庫構建一個對應的ImageLoaderMachO對象洒闸,并添加到一個全局的數(shù)組sImageRoots中去,最后再調(diào)用dyld::initializeMainExecutable函數(shù)均芽。
- dyld::initializeMainExecutable函數(shù)內(nèi)部的實現(xiàn)主要就是則遍歷全局數(shù)組sImageRoots中的每個ImageLoaderMachO對象丘逸,并分別調(diào)用每個對象的runInitializers方法來執(zhí)行動態(tài)庫的各種初始化邏輯,最后再調(diào)用主程序的ImageLoaderMachO的runInitializers方法來執(zhí)行主程序的各種初始化邏輯掀宋。
- ImageLoaderMachO是一個C++類深纲,類里面的runInitializers方法內(nèi)部主要是調(diào)用類中的成員函數(shù)processInitializers來處理各種初始化邏輯。
- processInitializers方法內(nèi)部的實現(xiàn)主要調(diào)用動態(tài)庫自身所依賴的其他動態(tài)庫的ImageLoaderMachO對象的recursiveInitialization方法劲妙。
- recursiveInitialization方法內(nèi)部的主要實現(xiàn)是首先調(diào)用dyld::notifySingle函數(shù)來初始化所有objc相關的信息湃鹊,比如執(zhí)行這個庫里面的所有類定義的+load的方法;然后再調(diào)用doInitialization方法來進一步執(zhí)行初始化的動作镣奋。
- doInitialization方法內(nèi)部首先調(diào)用doImageInit來執(zhí)行映像的初始化函數(shù)币呵,也就是LC_ROUTINES_COMMAND中記錄的函數(shù)(這個函數(shù)就是在構建動態(tài)庫時的編譯選項中指定的那個初始執(zhí)行函數(shù));然后再執(zhí)行doModInitFunctions方法來執(zhí)行所有庫內(nèi)的全局C++對象的構造函數(shù)侨颈,以及所有帶有*attribute*(constructor)標志的C函數(shù)余赢。
自此,所有main函數(shù)之前的邏輯代碼都已經(jīng)被執(zhí)行完畢了哈垢∶挥樱可能你會問整個過程中還是沒有看到關于C++全局對象構造函數(shù)是如何被執(zhí)行的?關于這個問題温赔,我們先暫停一下蛤奢,而是首先來考察一下當一個進程被結束前系統(tǒng)到底做了什么。
進程結束時我們能做什么陶贼?
當我們雙擊home鍵然后滑動手勢來終止某個進程或者手動調(diào)用exit函數(shù)時會結束進程的執(zhí)行啤贩。當進程被結束時操作系統(tǒng)會回收進程所使用的資源,比如打開的文件拜秧、分配的內(nèi)存等等痹屹。進程有可能會主動結束,也有可能被動的結束枉氮,因此操作系統(tǒng)提供了一系列注冊進程結束回調(diào)函數(shù)的能力志衍。在進程結束前會調(diào)用這些回調(diào)函數(shù)暖庄,因此我們可以通過進程結束回調(diào)函數(shù)來執(zhí)行一些特定資源回收或者一些善后收尾的工作。注冊進程結束回調(diào)函數(shù)的函數(shù)定義如下:
#include <stdlib.h>
//注冊一個進程結束時會被調(diào)用的C函數(shù)楼肪,函數(shù)的格式為:void func()培廓。 atexit如果注冊成功返回0,否則返回負數(shù)春叫。
int atexit(void (*func)(void));
//注冊一個進程結束時會被調(diào)用的block塊肩钠,block塊的格式為:^{}。 atexit_b如果注冊成功返回0暂殖,否則返回負數(shù)价匠。
int atexit_b(void (^block)(void));
//注冊一個進程結束時會被調(diào)用的C++無參數(shù)成員函數(shù),__cxa_atexit并沒有對外公開呛每,而只是供編譯器來使用踩窖,后面的C++對象的析構函數(shù)調(diào)用就要用到它!
int __cxa_atexit(void (*func)(void *), void *arg, void *dso)
上面的三個函數(shù)分別用來注冊進程結束時的標準C函數(shù)晨横、block代碼洋腮、C++函數(shù)⊥嵌簦可以注冊多個進程結束回調(diào)函數(shù)徐矩,并且系統(tǒng)是按照后注冊先執(zhí)行的后進先出的順序來執(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;
}
//當程序結束時顯示的結果如下:
//main
//foo2
//foo1
從上面提供的三種注冊方法叁幢,以及回調(diào)函數(shù)的執(zhí)行順序其實我們可以大體了解到系統(tǒng)是如何存儲這些回調(diào)函數(shù)的,我們可以通過如下的數(shù)據(jù)結構清楚的看到:
//代碼來自于:https://opensource.apple.com/source/Libc/Libc-1044.1.2/stdlib/FreeBSD/atexit.c.auto.html
//注冊回調(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)合體中保存的是注冊函數(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)定義的一個后進行先出的表頭全局變量曼玩。
static struct atexit *__atexit; /* points to head of LIFO stack */
struct atexit是一個鏈表和數(shù)組的結合體。用圖形表示所有注冊的函數(shù)的存儲結構大體如下:
從數(shù)據(jù)結構的定義以及atexit函數(shù)的描述和上面的圖形我們應該可以很容易的去實現(xiàn)那三個注冊函數(shù)窒百。大家可以去閱讀上面三個函數(shù)的實現(xiàn)黍判,這里就不再列出了。
上面說了進程結束回調(diào)注冊函數(shù)會在進程結束時被調(diào)用篙梢,而進程結束的函數(shù)是exit函數(shù)顷帖,因此可以很容易就想到這些回調(diào)函數(shù)的執(zhí)行肯定是在exit函數(shù)內(nèi)部調(diào)用的,事實也確實如此渤滞,通過匯編代碼查看exit的實現(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
上面的匯編翻譯為高級語言偽代碼大體如下:
void exit(int ret)
{
__cxa_finalize(NULL);
__exit(ret);
}
__cxa_finalize函數(shù)字面上是用于結束所有C++對象贬墩,但實際上卻負責調(diào)用所有注冊了進程結束回調(diào)函數(shù)的代碼。__exit函數(shù)內(nèi)部則是實際的進程結束操作妄呕。 __cxa_finalize函數(shù)的源代碼大體如下:
//代碼來自于: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ù)陶舞,下面是這個函數(shù)的定義。
//這個函數(shù)和實際的函數(shù)有出入绪励,并且為了讓大家更加容易理解我把一些認為不必要的代碼給刪除了.
/*
* 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. 下面會根據(jù)不同的類型來執(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();
}
}
}
}
三種進程結束回調(diào)函數(shù)中只有注冊類型為C++的函數(shù)才帶有一個參數(shù)唠粥,而其他兩類函數(shù)都不帶參數(shù),這樣的做的原因就是專門為調(diào)用全局C++對象的析構函數(shù)而服務的停做。
異常退出和abort函數(shù)
如果進程正常退出晤愧,最終都會執(zhí)行exit函數(shù)。exit函數(shù)內(nèi)部會調(diào)用atexit函數(shù)注冊的所有回調(diào)雅宾,以便有時間進行一些資源的回收工作养涮。而如果我們的應用出現(xiàn)了異常而導致進程結束則并不會激發(fā)進程結束回調(diào)函數(shù)的調(diào)用,系統(tǒng)異常出現(xiàn)時會產(chǎn)生中斷眉抬,操作系統(tǒng)會接管異常贯吓,并對異常進行分析,最后將分析的結果再交給用戶進程蜀变,并執(zhí)行用戶進程的std::terminate方法來終止進程悄谐。std::terminate方法內(nèi)部會調(diào)用通過NSSetUncaughtExceptionHandler函數(shù)注冊的未處理異常回調(diào)函數(shù)库北,來給我們機會處理產(chǎn)生崩潰的異常爬舰,處理完成最后再結束進程。
我們也可以調(diào)用abort函數(shù)來終止進程的執(zhí)行寒瓦,abort函數(shù)的內(nèi)部并不會調(diào)用atexit函數(shù)注冊的所有回調(diào)情屹,也就是說通過abort函數(shù)來終止進程時,并不會給我們機會來進行任何資源的回收處理杂腰,而是簡單的在內(nèi)部簡單粗暴的調(diào)用__pthread_kill方法來殺死主線程垃你,并終止進程。
通過上面對main函數(shù)執(zhí)行前所做的事情喂很,以及進程結束前我們能做的事情的介紹惜颇,您是否又對程序的啟動時和結束時所發(fā)生的一切有了更加深入的理解∩倮保可是這似乎離我要說的C++全局對象的構造和析構更加遙遠了凌摄,當然也許你不會這么認為,因為通過我上面的介紹漓帅,你也許對C++全局對象的構造和析構的時機有了一些想法锨亏,這些都沒有關系,這也是我下面將要詳細介紹的忙干。
再論C++的全局對象的構造和析構
就如本文的開始部分的一個例子器予,對于非全局的C++對象的構造和析構函數(shù)的調(diào)用總是在調(diào)用者的函數(shù)內(nèi)部完成,這時候存在著明顯的函數(shù)上下文的調(diào)用結構豪直。但是當我們定義了一個C++全局對象時因為沒有明顯的可執(zhí)行代碼的上下文劣摇,所以我們無法很清楚的了解到全局對象的構造函數(shù)和析構函數(shù)的調(diào)用時機。為了實現(xiàn)全局對象的構造函數(shù)和析構函數(shù)的調(diào)用弓乙,此時我們就需要編譯器來出馬幫助我們做一些事情了! 我們知道其實C++編譯器會在我們的源代碼的基礎上增加非常多的隱式代碼末融,對于每個定義的全局對象也是如此的钧惧。
當我們在某個.mm文件或者.cpp文件中定義了全局變量時比如下面某個文件的代碼:
//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"
//假設這里定義了兩個全局變量
CA a;
CA b;
當編譯器在編譯MyTest.cpp文件時發(fā)現(xiàn)其中定義了全局C++對象,那么除了會將全局對象變量保存在數(shù)據(jù)段(.data)外勾习,還會為每個全局變量定義一個靜態(tài)的全局變量初始化函數(shù)浓瞪。其命名的規(guī)則如下:
//按照全局對象在文件中定義的順序,第一個沒有數(shù)字序列巧婶,后面定義的則按數(shù)字序列遞增乾颁。
static ___cxx_global_var_init.<數(shù)字序列>();
同時會以定義全局變量的文件名為標志定義一個如下的靜態(tài)函數(shù):
static void _GLOBAL__sub_I_<文件名>(int argc, char **argv, char** env, char **apple, void * programVars);
因此當編譯上面的MyTest.cpp文件時,其實最真實的文件的內(nèi)容是如下的:
//MyTest.cpp
#include "CA.h"
struct CA a;
struct CA b;
//全局對象a的初始化函數(shù)艺栈。
static void ___cxx_global_var_init()
{
CA::CA(&a);
//這代碼很有意思英岭,將CA類的析構函數(shù)的地址和a的地址通過__cxa_atexit函數(shù)進行注冊,以便當進程結束時調(diào)用湿右。
__cxa_atexit(&CA::~CA(), &a, NULL);
}
//全局對象b的初始化函數(shù)诅妹。
static void ___cxx_global_var_init.1()
{
CA::CA(&b);
__cxa_atexit(&CA::~CA(), &b, NULL);
}
//本文件內(nèi)的所有全局對象的初始化函數(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();
}
從上面的代碼中我們可以看出每個全局對象的初始化函數(shù)都其實是做了兩件事:
- 調(diào)用對象類的構造函數(shù)毅人。
- 通過__cxa_atexit函數(shù)來注冊進程結束時的析構回調(diào)函數(shù)吭狡。
前面我曾經(jīng)說過__cxa_atexit這個函數(shù)并沒有對外暴露,而是留給編譯器以及內(nèi)部使用丈莺,這個函數(shù)接收三個參數(shù):一個函數(shù)指針划煮,一個對象指針,一個庫指針缔俄。我們知道所有C++類定義的函數(shù)其實都是有一個隱藏的this參數(shù)的弛秋,析構函數(shù)也一樣。還記得上面的__cxa_finalize_ranges函數(shù)內(nèi)部是如何調(diào)用注冊的C++函數(shù)的嗎牵现?
fn->fn_ptr.cxa_func(fn->fn_arg);
//因為我們注冊時铐懊,注冊的是類的析構函數(shù)的地址邀桑,以及全局對象本身:
__cxa_atexit(&CA::~CA(), &a, NULL);
//所以在最終進程終止時其實調(diào)用的是:
CA::~CA(&a) 方法瞎疼,也就是調(diào)用的是全局對象的析構函數(shù)!壁畸!
可以看出系統(tǒng)采用了一個非常巧妙的方法贼急,借助__cxa_atexit函數(shù)來實現(xiàn)全局對象析構函數(shù)的調(diào)用。那么問題又來了捏萍?對象的構造函數(shù)又是再哪里調(diào)用的呢太抓?換句話說_GLOBAL__sub_I_MyTest.cpp()這個函數(shù)又是在哪里被調(diào)用的呢?
這就需要我們?nèi)チ私庖幌耺ach-o文件的結構了令杈,關于mach-o文件結構的介紹這就不再贅述走敌,大家可以到網(wǎng)上去參考閱讀相關的文章。
可以明確的就是當我們定義了全局對象并生成了_GLOBAL__sub_I_XXX系列的函數(shù)時或者當我們的代碼中存在著attribute(constructor)聲明的C函數(shù)時逗噩,系統(tǒng)在編譯過程中為了能在進程啟動時調(diào)用這些函數(shù)來初始化全局對象掉丽,會在數(shù)據(jù)段__DATA下建立一個名為__mod_init_func的section跌榔,并把所有需要在程序啟動時需要執(zhí)行的初始化的函數(shù)的地址保存到__mod_init_func這個section中。 我們可以從下面mach-o view這個工具中看到我們所有的注冊的信息捶障。
您是否還記得前面介紹的main函數(shù)執(zhí)行前所執(zhí)行的代碼流程僧须,在那些代碼中,有一個名叫ImageLoaderMachO::doModInitFunctions的函數(shù)就是專門用來負責執(zhí)行__DATA下的__mod_init_func中注冊的所有函數(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í)行所有注冊了的需要初始化就被執(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展示的圖表來看锭部,全局對象的構造函數(shù)以及聲明了attribute(constructor)的C函數(shù)都會記錄在*DATA*,_mod_init_func這個section中并且會在doModInitFunctions函數(shù)內(nèi)部被執(zhí)行暂论。那么對于一個聲明了attribute(destructor)的C函數(shù)呢?它又是如何在進程結束前被執(zhí)行的呢拌禾?答案就在DATA,_mod_term_func這個section中空另,系統(tǒng)在編譯時會將所有帶attribute(destructor)聲明的函數(shù)地址記錄到這個section中。還記得上面程序啟動初始化時會有一個環(huán)節(jié)調(diào)用dyld::initializeMainExecutable函數(shù)嗎蹋砚?
//dyld.cpp中的代碼
//為了能夠看得更加清晰扼菠,這里面我會刪除一些不必要的代碼。
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方法來注冊了一個進程結束時的回調(diào)函數(shù)runAllStaticTerminators。繼續(xù)來跟蹤函數(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];
//這里遍歷每個動態(tài)庫并執(zhí)行其中的doTermination方法墨坚。
image->doTermination(gLinkContext);
}
sImageFilesNeedingTermination.clear();
notifyBatch(dyld_image_state_terminated, false);
}
catch (const char* msg) {
halt(msg);
}
}
繼續(xù)來看ImageLoaderMachO::doTermination的內(nèi)部實現(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就是上面說到的名為_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(); //這就是那些注冊了的函數(shù)秧饮。
}
}
}
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
可見帶有attribute(destructor)聲明的函數(shù),也是在系統(tǒng)初始化時通過了atexit的機制來實現(xiàn)進程結束時的調(diào)用的泽篮。
上面就是我要介紹的C++全局對象的構造函數(shù)和析構函數(shù)的調(diào)用以及實現(xiàn)的所有過程盗尸。我們從上面的章節(jié)中還可以了解到程序在啟動和退出這個階段所做的事情,以及我們所能做的事情帽撑。
最后還有一個問題需要解決:那就是我們知道所有的庫的加載以及初始化操作都是通過dyld這個庫來處理的泼各。也就是一個進程在用戶態(tài)最先運行的代碼是dyld庫中的代碼,但是dyld庫中本身也用到了一些全局的C++對象比如vector數(shù)組來存儲所有的ImageLoaderMachO對象:
//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要加載所有其他的庫并且調(diào)用每個庫的初始化函數(shù)來構造庫內(nèi)定義的全局C++對象亏拉,那么dyld庫本身所定義的全局C++對象的構造函數(shù)又是如何被初始化的呢扣蜻?很顯然我們不可能在doModInitFunctions中進行初始化操作,而是必須要將初始化全局對象的邏輯放到加載其他庫之前做處理及塘。要想回答這個問題我們可以再次考察一下dyldbootstrap::start函數(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
//這句話是關鍵莽使,dyld在初始化其他庫之前會調(diào)用這個函數(shù)來調(diào)用庫自身的所有全局C++對象的構造函數(shù)。
runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
#endif
// now that we are done bootstrapping dyld, call dyld's main
//下面的代碼是用來初始化可執(zhí)行程序以及其所依賴的所有動態(tài)庫的笙僚。
uintptr_t appsSlide = slideOfMainExecutable(appsMachHeader);
return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}
start函數(shù)中在加載并初始化其他庫之前有調(diào)用函數(shù)runDyldInitializers
下面的代碼就是runDyldInitializers的實現(xiàn)芳肌,可以看出其他就是一個doModInitFunctions函數(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);
}
}
小知識點:如果我們在編程時想要訪問自身mach-o文件中的某個段下的某個section的數(shù)據(jù)結構時,我們就可以借助上面的匯編代碼:__asm("section[圖片上傳失敗...(image-eb2a1c-1545901486118)]
__DATA$__mod_init_func"); 來獲取section的開頭和結束的地址區(qū)間亿笤。
一個疑惑的地方
整個例子中我們定義了一個C++的類檬嘀,還定義了beginfunc, endfunc函數(shù),建立了全局對象责嚷,以及一個main函數(shù)鸳兽。我們可以通過nm命令來看可執(zhí)行程序所有導出的符號表:
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
上面的符號表我刪除了一些其他的符號,在這里可以看到大寫T標志的函數(shù)是非靜態(tài)全局函數(shù)罕拂,小寫t標志的函數(shù)是靜態(tài)函數(shù)揍异,S標志的符號是全局變量”啵可以看出程序為了支持C++的全局對象并初始化需要定義一些附加的函數(shù)來完成衷掷。這里有一個讓人疑惑的地方就是:
0000000100001020 t __ZN2CAC1Ev
0000000100001060 t __ZN2CAC2Ev
0000000100001040 t __ZN2CAD1Ev
0000000100001bc0 t __ZN2CAD2Ev
這里面定義了2個CA類的構造函數(shù)和析構函數(shù),差別只是序號的不同柿菩。根據(jù)匯編代碼轉化為高級語言偽代碼如下:
//這個函數(shù)只是一個殼
static void __ZN2CAC1Ev(CA * const this)
{
__ZN2CAC2Ev(this);
}
//這個是類構造函數(shù)的真實實現(xiàn)戚嗅。
static void __ZN2CAC2Ev(CA *const this)
{
printf("CA::CA()\n");
}
//這個函數(shù)只是一個殼
static void __ZN2CAD1Ev(CA * const this)
{
__ZN2CAD2Ev(this);
}
//這個是類析構函數(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)在編譯時分別實現(xiàn)了2個構造函數(shù)和析構函數(shù)懦胞,而且標號為1的函數(shù)內(nèi)部其實只是簡單的調(diào)用了標號為2的真實函數(shù)的實現(xiàn)。所以當我們在調(diào)試或者查看崩潰日志時凉泄,如果問題出現(xiàn)在了全局對象的構造函數(shù)或者析構函數(shù)內(nèi)部躏尉,我們看到的函數(shù)調(diào)用棧里面會出現(xiàn)兩個相同的函數(shù)名字
這個實現(xiàn)機制非常令我迷惑!希望有高手為我答疑解惑后众。
后記:崩潰的修復方法
最后我想再來說說那個崩潰事件胀糜,本質(zhì)的原因還是對于全局對象的使用不當導致,當進程將要被殺死時蒂誉,主線程執(zhí)行了exit方法的調(diào)用教藻,exit方法內(nèi)部析構了所有定義的全局C++對象,并且當主線程在執(zhí)行在全局對象的析構函數(shù)時右锨,如果我們的應用中還有其他的常駐線程還在運行時括堤,此時那些線程還并沒有銷毀或者殺死,也就是一個進程的所有其他線程的終止處理其實是發(fā)生在exit函數(shù)調(diào)用結束后才會發(fā)生的陡蝇,因此如果一個常駐線程一直在訪問一個全局對象時就有可能存在著隱患以及不確定性痊臭。一個解決的方法就是在全局對象析構函數(shù)調(diào)用前先終止所有其他的線程哮肚;另外一個解決方案是對全局對象的訪問進行加鎖處理以及進行是否為空的判斷處理登夫。我們使用的那個第三方庫所采用的一個解決方案是在程序啟動后通過調(diào)用atexit函數(shù)來注冊了一個進程結束回調(diào)函數(shù),然后再那個回調(diào)函數(shù)里面終止了所有工作線程允趟。因為按照atexit后進先出的規(guī)則恼策,我們手動注冊的進程結束回調(diào)函數(shù)要比C++析構的進程結束回調(diào)函數(shù)后添加,所以工作線程的終止邏輯回調(diào)函數(shù)就會比析構函數(shù)調(diào)用要早,從而可以防止問題的發(fā)生了涣楷。