http://blog./uid-27164517-id-3308726.html 2012 一段神奇的代碼在論壇里看到下面一段代碼: int createproc(); int main() { pid_t pid=createproc(); printf("%d\n", pid); exit(0); } int createproc() { pid_t pid; if(!(pid=vfork())) { printf("child proc:%d\n", pid); return pid; } else return -1; } 輸出結(jié)果: child proc:0 0 child proc:0 Killed 感覺非常奇怪,為什么vfork以后,父子進(jìn)程都走了“子進(jìn)程”的分支呢? 什么是vfork? “寫時(shí)復(fù)制”其實(shí)還是有復(fù)制,進(jìn)程的mm結(jié)構(gòu)、頁表都還是被復(fù)制了(“寫時(shí)復(fù)制”也必須由這些信息來支撐。否則內(nèi)核捕捉到CPU訪存異常,怎么區(qū)分這是“寫時(shí)復(fù)制”引起的,還是真正的越權(quán)訪問呢?)。 而 vfork就把事情做絕了,所有有關(guān)于內(nèi)存的東西都不復(fù)制了,父子進(jìn)程的內(nèi)存是完全共享的。但是這樣一來又有問題了,雖然用戶程序可以設(shè)計(jì)很多方法來避免 父子進(jìn)程間的訪存沖突。但是關(guān)鍵的一點(diǎn),父子進(jìn)程共用著棧,這可不由用戶程序控制的。一個(gè)進(jìn)程進(jìn)行了關(guān)于函數(shù)調(diào)用或返回的操作,則另一個(gè)進(jìn)程的調(diào)用棧(實(shí) 際上就是同一個(gè)棧)也被影響了。這樣的程序沒法運(yùn)行下去。 所以,vfork有個(gè)限制,子進(jìn)程生成后,父進(jìn)程在vfork中被內(nèi)核掛起,直 到子進(jìn)程有了自己的內(nèi)存空間(exec**)或退出(_exit)。并且,在此之前,子進(jìn)程不能從調(diào)用vfork的函數(shù)中返回(同時(shí),不能修改棧上變量、 不能繼續(xù)調(diào)用除_exit或exec系列之外的函數(shù),否則父進(jìn)程的數(shù)據(jù)可能被改寫)。 問題的思考 說到這里,可以看出文章開頭的那段代碼是存在問題的了。子進(jìn)程不但調(diào)用了printf,還從createproc函數(shù)中返回了。 但是,子進(jìn)程的違規(guī)為什么會(huì)使父進(jìn)程走上“child proc”這條路呢?父進(jìn)程在子進(jìn)程退出前被阻塞在vfork里面,vfork的返回值是如何變成0的呢? 前面一直在說vfork,其實(shí)它是兩個(gè)東西,庫(libc)函數(shù)vfork和系統(tǒng)調(diào)用vfork。用戶程序調(diào)用的是庫函數(shù),而庫函數(shù)再去調(diào)用系統(tǒng)調(diào)用。用戶 程序中幾乎所有的系統(tǒng)調(diào)用都是通過庫函數(shù)去調(diào)用的。因?yàn)椴煌w系結(jié)構(gòu)下(甚至相同體系結(jié)構(gòu)),系統(tǒng)調(diào)用的指令和參數(shù)傳遞規(guī)則都可能不同,這些細(xì)節(jié)被庫函數(shù) 隱藏了。 前面提到,父進(jìn)程被掛起在vfork中,這是指的系統(tǒng)調(diào)用vfork。在系統(tǒng)調(diào)用中,進(jìn)程使用的是內(nèi)核棧(每個(gè)進(jìn)程有著自己獨(dú)有 的內(nèi)核棧)。此時(shí),父進(jìn)程在內(nèi)核里面是安全的,隨便子進(jìn)程怎么違規(guī)。內(nèi)核會(huì)保證系統(tǒng)調(diào)用vfork的完整性,系統(tǒng)調(diào)用的返回值也不會(huì)有問題(它是通過寄存 器傳回用戶空間的,跟棧無關(guān))。 而vfork的返回值變成0的問題,則是在庫函數(shù)vfork中產(chǎn)生的。既然子進(jìn)程已經(jīng)違規(guī)了,庫函數(shù)沒辦法保證程 序的正確性。而庫函數(shù)vfork是否返回0也是不確定的,可能不同版本的libc、不同的程序上下文、不同的系統(tǒng)、等等、都會(huì)有不同的返回值(或者就直接 “段錯(cuò)誤”了)。還有可能是,父進(jìn)程中庫函數(shù)vfork并沒有返回0,但是棧上的返回地址被改寫了,從函數(shù)createproc返回,返回到 printf("child proc")這句話去了。 再深入一點(diǎn) vfork后,庫函數(shù)沒法保證子進(jìn)程在進(jìn)行函數(shù)調(diào)用或返回的操作后程序還正常,但是庫函數(shù)vfork本身就是一個(gè)函數(shù)呀,從系統(tǒng)調(diào)用vfork返回后,庫函數(shù)vfork接著又返回了。這時(shí),程序的正確性又是如何保證的呢? 關(guān)于函數(shù)調(diào)用,一般而言:調(diào)用前-調(diào)用者將需要傳遞的參數(shù)放到棧上;調(diào)用時(shí)-調(diào)用者使用call指令,該指令自動(dòng)將返回地址入棧;調(diào)用后,在被調(diào)用的函數(shù)中,第一件事是做調(diào)用棧的調(diào)整,如createproc函數(shù)如是做: 08048487 8048487: 55 push %ebp 8048488: 89 e5 mov %esp,%ebp 804848a: 83 ec 28 sub $0x28,%esp ...... 其中ESP是當(dāng)前棧的指針,而EBP是上一層調(diào)用棧的指針。調(diào)用棧調(diào)整之前,EBP保存著上上一層棧的指針,這個(gè)值不能丟,需要放在棧上,以便函數(shù)返回時(shí)恢復(fù)。 每層調(diào)用都有自己的調(diào)用棧,“深”的調(diào)用不會(huì)影響到之前的調(diào)用棧。所以,vfork后子進(jìn)程調(diào)用其他函數(shù)應(yīng)該是沒有問題的(但是可能會(huì)改寫掉屬于父進(jìn)程的某些數(shù)據(jù),造成邏輯上的錯(cuò)誤),只要它不從調(diào)用vfork的函數(shù)中返回就行了。 但是,庫函數(shù)vfork本身卻不是這樣做的。在這個(gè)函數(shù)中沒有使用棧上的內(nèi)存空間,它沒有去進(jìn)行調(diào)用棧的切換,如: 000983f0 <__vfork>: 983f0: 59 pop %ecx 983f1: 65 8b 15 6c 00 00 00 mov %gs:0x6c,%edx 983f8: 89 d0 mov %edx,%eax 983fa: f7 d8 neg %eax ...... 9840e: cd 80 int $0x80 98410: 51 push %ecx ...... 所以父進(jìn)程在庫函數(shù)中運(yùn)行時(shí),不用擔(dān)心棧上的數(shù)據(jù)已經(jīng)被子進(jìn)程修改(它根本不去使用棧上的數(shù)據(jù))。 然而call/ret指令卻不得不使用棧(因?yàn)榉祷氐刂纷詣?dòng)會(huì)被CPU放在棧上),如果子進(jìn)程在vfork后調(diào)用其他函數(shù),會(huì)使得父進(jìn)程在進(jìn)入庫函數(shù)vfork時(shí)通過call指令在棧上留下的“返回地址”被擦掉。 事 情的確是這樣。于是庫函數(shù)vfork為了解決這個(gè)問題,做了一些手腳,它并沒有讓棧上的“返回地址”一直留在棧上。注意上面的匯編代碼,進(jìn)入庫函數(shù) vfork的第一條指令就是“pop %ecx”,把放在棧上的“返回地址”彈到了ECX中去,保存起來。然后在系統(tǒng)調(diào)用vfork返回后(int 0x80是用于系統(tǒng)調(diào)用的指令),再“push %ecx”,把“返回地址”放回去。 |
|