多線(xiàn)程程序可能存在很多潛在的bug,如data race,dead lock,信號bug等,而這些bug一向很難調試,現在有很多論文都是基于多線(xiàn)程程序的調試技術(shù)的,比如model check,死鎖檢測,replay技術(shù)等,也有很多對應的工具,如intel的pinplay,微軟的Zing等。關(guān)于這些技術(shù)和工具,如果感興趣可以 google相應的論文進(jìn)一步了解。這里我主要講述的是我在對二進(jìn)制翻譯下多線(xiàn)程程序調試中經(jīng)常使用的一些方法以及一些調試經(jīng)驗,雖然我的調試的是二進(jìn)制翻譯器,但是這些方法也同樣適用于大多數多線(xiàn)程程序。
1、最直接的方法就是在源程序插入printf語(yǔ)句來(lái)打印出一些有用的變量。這種方法的優(yōu)點(diǎn)是不用借助其他工具就可以對程序的運行進(jìn)行觀(guān)察,缺點(diǎn)是插入語(yǔ)句的位置、粒度等都需要調試者自己去權衡,如果插入過(guò)多的打印語(yǔ)句,則頻繁的IO操作會(huì )使程序運行變慢,線(xiàn)程行為改變,有些bug甚至不會(huì )再出現。至于需要在什么地方插入語(yǔ)句,首先,只打印有必要的變量,一個(gè)語(yǔ)句可以打印多個(gè)變量;其次,在循環(huán)中,我們可以通過(guò)設置一些條件來(lái)降低打印的粒度,比如下面這段代碼:
1 2 3 4 5 6 7 8 | while(flag){ pc = getpc(); printf(“pc is:0x%x\n”, pc);//我們插入的打印語(yǔ)句 ...... ...... //do somthing using pc; } |
假設我們對pc的取值很感興趣,需要打印出所有pc取到過(guò)的值,但是大多數情況下,getpc()的返回值都同上一次的返回值相同,這樣我們printf出來(lái)的就會(huì )有很多重復值。這種情況下我們可以用下面這種插樁方式來(lái)去處重復值:
1 2 3 4 5 6 7 8 9 10 11 | int lastpc; //定義為全局變量或局部靜態(tài)變量 while(flag){ pc = getpc(); if(pc !=lastpc){ lastpc = pc; printf(“pc is:0x%x\n”, pc); } ...... ......//do somthing using pc } |
這樣通過(guò)一個(gè)簡(jiǎn)單的判斷就可以省掉很多沒(méi)有必要的輸出。很多別的情形,比如我們只關(guān)心某一變量等于特定值(比如0)時(shí)其他變量的狀態(tài),我們就沒(méi)有必要把改變量不等于0時(shí)的狀態(tài)打印出來(lái)??傊?,能省則省,只打印我們需要的。
2、利用gdb的attach功能和sleep()函數。gdb是由gnu維護的功能強大的調試工具,并且支持多線(xiàn)程程序的調試,可以在gdb下直接運行一個(gè)多線(xiàn)程程序,通過(guò)thread等命令進(jìn)行調試。但是很多多線(xiàn)程程序在其他工具(gdb,pin,strace等)監管下,原有的bug就不會(huì )出現。這的確是很讓人頭疼的事情,也是我十分不喜歡這個(gè)方法的原因,想象一下,一個(gè)程序直接跑就出錯,但是放到gdb下就能得到正確的結果,好像故意在耍我們一樣。我更喜歡使用gdb的attach功能,我們可以通過(guò)下面的命令來(lái)讓gdb接管一個(gè)運行的線(xiàn)程:
1 | gdb attach <pid> |
這種方法的好處是能夠使gdb對程序執行的影響最小,而且可以只接管程序中某一條我們所關(guān)心的線(xiàn)程,而其他線(xiàn)程不受影響。
這時(shí)有人會(huì )問(wèn),如果線(xiàn)程執行過(guò)快,我們還沒(méi)來(lái)得及attach線(xiàn)程就已經(jīng)執行完或者dump掉了,這種情況該怎么辦?解決方法很簡(jiǎn)單,既然線(xiàn)程執行過(guò)快,我們就讓它等一等,可以在源代碼中讓我們關(guān)心這個(gè)線(xiàn)程sleep()一小會(huì )兒,這樣我們就有足夠的時(shí)間來(lái)attach它,并且attach的位置我們也可以進(jìn)行控制,想在哪里attach,就在哪里sleep。
3、第三種方法是利用信號處理函數來(lái)獲取一些信息。在多線(xiàn)程程序的壓力測試中,很多錯誤要每隔幾百幾千次運行才能出現一次,而這種錯誤的replay是很困難的,因此捕捉到這種錯誤的現場(chǎng)很重要。這里我習慣利用信號處理程序來(lái)保存這樣的現場(chǎng),這樣你可以晚上寫(xiě)個(gè)腳本讓程序無(wú)限跑,早上起來(lái)你會(huì )發(fā)現程序停在出錯的地方,這是很愜意的事情。
多數多線(xiàn)程程序出錯,都是訪(fǎng)問(wèn)非法內存,也就是我們常說(shuō)的“段錯誤”(segmentation fault),程序發(fā)生非法內存的訪(fǎng)問(wèn),系統會(huì )發(fā)給線(xiàn)程一個(gè)SIGSEGV信號,這個(gè)信號默認處理為core掉該線(xiàn)程。我們可以對這個(gè)信號進(jìn)行利用,為其注冊一個(gè)信號處理函數:
1 2 3 4 | struct sigaction act; act.sa_flags = SA_SIGINFO; act.sa_sigaction = signal_handler; sigaction(SIGSEGV, &act, NULL); //SIGSEGV表示該信號的值 |
信號處理函數如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void signal_handler(int host_signum, siginfo_t *info, void *puc){ struct ucontext *uc = (struct ucontext *)puc; int loopflag = 1; while(loopflag) //可以在gdb中手動(dòng)更改loopflag的值跳出循環(huán) sleep(1); ...... //這里可以打印一些感興趣的變量 } |
函數參數中,puc是一個(gè)體系結構相關(guān)的指針,不同的體系結構,指針指向的結構不一樣,里面存放了發(fā)生信號時(shí)線(xiàn)程的寄存器的值,程序地址等信息,函數內第一句話(huà)的目的就是把void類(lèi)型轉換成ucontext結構類(lèi)型,這樣在gdb中可以直接print出該結構的成員。
函數中sleep的作用是讓程序停在信號處理程序中,以給我們足夠的時(shí)間進(jìn)行attach。如果想讓程序繼續運行下去,手動(dòng)把loopflag修改為1即可。用while循環(huán)的目的是我們可以在運行時(shí)手動(dòng)控制sleep的時(shí)間。
這種方法同樣適用于其他信號帶來(lái)的bug,比如SIGBUS等。在二進(jìn)制翻譯下,還可以使用這種方法對二進(jìn)制翻譯器信號處理進(jìn)行跟蹤和調試,具體使用讀者可以自己去發(fā)掘。
4、利用strace得到我們關(guān)心的信息。大多數情況下我們用strace的目的是跟蹤系統調用,但其實(shí)strace對多線(xiàn)程程序的調試有很大的幫助,使用strace打印多線(xiàn)程程序信息的命令如下:
1 | strace -F ./test |
如果我們對某些系統調用,如gettimeofday,ioctl不感興趣,可以屏蔽掉
1 | strace -F -etrace=\!gettimeofday,ioctl ./test |
通過(guò)strace打印出的信息,我們可以對什么時(shí)候產(chǎn)生了一個(gè)子線(xiàn)程,那個(gè)線(xiàn)程在等待,哪個(gè)線(xiàn)程被喚醒,哪個(gè)線(xiàn)程收到信號,哪個(gè)線(xiàn)程core掉有一個(gè)綜合的了解,這些信息對多線(xiàn)程調試會(huì )起到很大的作用。
還有很多方法比如利用core文件等,很多地方可以查到,我不做累贅的介紹??傊夹g(shù)是死的,但是方法是靈活的,當傳統方法解決不了一個(gè)問(wèn)題的時(shí)候,可以放開(kāi)思路嘗試其他的方法。
如果有任何問(wèn)題,歡迎大家留言和我進(jìn)行討論,我會(huì )在第一時(shí)間進(jìn)行回復。
聯(lián)系客服