永不返回的exit函數是如何返回到main函數的調用者的?
題外話:最近有點忙,可能比較少有寫作。
exit函數這個名字就很狂野:不管別的任何線程身在何處就開始啟動所有全局變數/static/局部static變數的析構,而main函數之後構造的局部變數則全部泄漏。線程局部變數的析構相對於線程死亡的時間關係我一時也無法觀察清楚各個系統的實現,所以就不在此展開了。不過exit函數本身是不會返回的,調用他就是一條不歸路。
不過講exit如何收刀之前,我們有必要講講main函數的調用者。
嗯,相信很多朋友甚至根本不知道也沒法知道main函數的調用者長啥樣:沒錯,當你給main函數打斷點時,有以下幾種可能性會導致你在調試器中看不到main函數的調用者,取決於平台實現:
1. main函數可能並不是通過正常的函數調用方法來調用進來的,調用者可能使用了類似於call_in_stack的方法操縱了main函數的返回地址和堆棧。這種方法可以保證:main函數自己無需自己對齊堆棧(老x86 nix編譯出來的二進位很多main函數里都把堆棧「與」一下來保證堆棧4位元組對齊,應該是考慮到很多老nix環境下 的main函數的glibc環境未必對齊了堆棧,從而保證在不同glibc環境下的可移植性)。
2. 你的調試器先天性幫你隱藏了main函數的調用者。。。
3. 你可能平時沒注意去看。。。
不管怎麼說,main函數的調用者或者父調用者或者祖父調用者等間接調用者最終會去調用exit函數來調用通過atexit註冊的函數,全局變數/static/局部static變數的析構函數。。啊啊, 不是說exit不返回的嗎,這個調用最後會去哪裡?
上面這個疑問和標題「永不返回的exit函數是如何返回到main函數的調用者的?」實際上有異曲同工之妙:exit 函數是如何實現
不管exit函數在哪裡調用,結束之後總是能跳到一個指定的位置?
第一種視角:調用main函數前找個地方setjmp, 然後exit函數結尾處進行longjmp。
int main_started = 0;nvolatile int* pmain_started =&main_started;nsetjmp(xxx); //xxx是全局變數,可以給exit 進行longjmp。longjmp會直接返回到這裡的下一行。nif(*pmain_started == 0){n *pmain_started = 1;n exit(main());n}n
很多朋友不熟悉setjmp的返回值含義,所以我故意使用一個標記變數而不直接使用返回值來闡述這裡的流程。
第二種視角:視main函數為一個協程的代碼,exit後的跳轉點是另外一個協程的代碼。嗯,exit就只是一種協程的切換方法了。
//協程0的代碼開始 nint main_started = 0; nvolatile int* pmain_started =&main_started; nsetjmp(xxx); //xxx是全局變數,可以給exit 進行longjmp。longjmp會直接返回到這裡的下一行。 nif(*pmain_started == 0){ n *pmain_started = 1; n//協程0的代碼結束,協程1的代碼開始。 n exit(main()); n//unreachable codes. n} //協程0的代碼開始n
也就是說,C語言在規定必須用main函數做入口,同時又允許exit隨時退出這個事情上,就已經暗含了一個簡單的,對C++局部變數的析構不友好的協程模型(完)。
推薦閱讀:
※考不上三本也能給自己心愛的語言加上Coroutine(一)
※from yield to await——Python協程演進過程(一)
※協程和纖程的區別?
※Gevent的協程,能夠非同步是為什麼呢?