多線程調(diào)試的困惑
不少人在調(diào)試多線程程序時(shí),遇到過各種各樣看似奇怪的問題。比如,本來很容易復(fù)現(xiàn)的問題,調(diào)試時(shí)卻莫名其妙地消失了;或者,本來程序可以正常運(yùn)行的,調(diào)試時(shí)卻總是異常退出。
究其原因,是因?yàn)槌绦虻恼?zhí)行邏輯在調(diào)試的過程中受到了干擾。比如,調(diào)試時(shí)我們可能需要經(jīng)常設(shè)置斷點(diǎn),查看變量、內(nèi)存、寄存器等狀態(tài)信息,這可能會(huì)導(dǎo)致多個(gè)線程間的時(shí)序邏輯受到干擾,改變了程序的正常執(zhí)行邏輯。
有人可能會(huì)說,我不用調(diào)試器,就用printf。然而,即便是最簡(jiǎn)單的printf,也可能會(huì)對(duì)程序邏輯產(chǎn)生很大的影響。不過這不是本文重點(diǎn),不再展開,后續(xù)會(huì)有專門文章來分析,感興趣的可以右上角關(guān)注一下!
GDB作為調(diào)試神器,為我們提供了很多專門針對(duì)多線程調(diào)試的工具和方法,本文會(huì)以實(shí)例的形式,逐一詳細(xì)講解!
本文以下面這個(gè)程序?yàn)槔敿?xì)介紹GDB調(diào)試多線程代碼的方法:

GDB提供了多個(gè)針對(duì)多線程程序的調(diào)試命令。
info threads 命令可以查詢被調(diào)試程序中的線程的信息。
使用GDB調(diào)試程序時(shí),每個(gè)線程至少有三個(gè)ID:
??Pthread庫為線程分配的pthread ID,也就是用pthread_self()返回的ID
??Linux kernel為線程分配的thread ID,也就是gettid()返回的ID
? GDB為線程分配的ID。執(zhí)行GDB調(diào)試命令時(shí)要指定的線程ID,如無特殊說明,都是指的這個(gè)ID
如:

info threads顯示,當(dāng)前共有三個(gè)線程。以主線程(第一行)為例:1是GDB為主線程分配的ID,接下來0x7ffff7da3740是pthread庫為主線程分配的pthread ID,后面括號(hào)中的355298是kernel為主線程分配的thread ID,再后面是該線程被中斷時(shí),代碼正在執(zhí)行的代碼位置。
可用通過ps命令或者proc文件系統(tǒng)查看確認(rèn):
root@ubuntu:~#?ps?-eT?|?grep?test
?355298??355298?pts/9????00:00:02?test
?355298??355301?pts/9????00:00:02?test
?355298??355302?pts/9????00:00:02?test
root@ubuntu:~#?
root@ubuntu:~#?ls?/proc/355298/task/
355298??355301??355302
root@ubuntu:~#?線程1前面有個(gè)星號(hào)*,表示當(dāng)前線程。默認(rèn)情況下,執(zhí)行的GDB命令是針對(duì)當(dāng)前線程。比如此時(shí)執(zhí)行bt(backtrace)命令,獲取的是線程1的調(diào)用棧:

thread?

此時(shí)用info threads查看,星號(hào)*已經(jīng)切到了線程2的前面。
thread apply [thread-id-list | all] command 可以針對(duì)指定線程執(zhí)行命令
如:
? thread apply all bt:打印所有線程的調(diào)用棧信息
? thread apply 3 bt:打印線程3的調(diào)用棧信息
? thread apply 2-3 bt:打印線程2和線程3的調(diào)用棧信息

在設(shè)置斷點(diǎn)時(shí),我們可以選擇讓斷點(diǎn)對(duì)所有的線程生效,或只對(duì)特定線程生效。命令格式為:
break???#?在設(shè)置斷點(diǎn),并對(duì)所有線程生效
break??thread??#?在設(shè)置斷點(diǎn),僅對(duì)指定的線程生效
break??thread??if??#?在設(shè)置條件斷點(diǎn)斷點(diǎn),僅對(duì)指定的線程生效 比如 break do_stuff thread 2, 這個(gè)命令在do_stuff()入口設(shè)置斷點(diǎn),只有thread 2調(diào)用這個(gè)函數(shù)是才會(huì)觸發(fā)斷點(diǎn),其他thread調(diào)用這個(gè)函數(shù)不會(huì)觸發(fā)斷點(diǎn)。
GDB中的其他所有類型的斷點(diǎn),也支持對(duì)特定線程設(shè)置斷點(diǎn),如tbreak、watch等。
斷點(diǎn)相關(guān)的東西,相對(duì)比較簡(jiǎn)單,不再單獨(dú)演示。
? set print thread-events on/off 設(shè)置是否打印線程創(chuàng)建和退出信息,如上面例子中的:
[New?Thread?0x7ffff7da2700?(LWP?355301)]
[New?Thread?0x7ffff75a1700?(LWP?355302)]??命令縮寫
有時(shí)候使用thread apply [thread-id-list | all] command會(huì)稍顯繁瑣,GDB貼心的為我們提供了幾種命令縮寫:
taas command 相當(dāng)于 thread apply all -s command
tfaas command 相當(dāng)于 thread apply all -s -- frame apply all -s command?
這個(gè)命令非常有用。比如,有時(shí)我們只記得一個(gè)變量或參數(shù)的名字,卻忘了或不知道它是在哪個(gè)具體的函數(shù)中,就可以用這個(gè)命令:tfaas p var_name,這個(gè)命令會(huì)搜索所有線程的調(diào)用棧,找到名字為var_name的變量,并打印它的值,如:
(gdb)?tfaas?p?x1
Thread?2?(Thread?0x7ffff7da2700?(LWP?370514)?"test"):
#0??thread_1?(arg=0x0)?at?test.c:10
$5?=?432504243
(gdb)?tfaas?p?y2
Thread?3?(Thread?0x7ffff75a1700?(LWP?370515)?"test"):
#0??thread_2?(arg=0x0)?at?test.c:18
$6?=?428735002
(gdb)為了更好地調(diào)試多線程程序,GDB提供了兩種模式來控制程序的執(zhí)行:
? All-Stop Mode:在該模式下,不管因?yàn)槭裁丛颍粋€(gè)線程被中斷執(zhí)行,其他所有的線程都會(huì)同時(shí)被中斷執(zhí)行。
? Non-Stop Mode:在該模式下,一個(gè)線程被中斷執(zhí)行,不會(huì)影響其他線程的正常執(zhí)行。
接下來,我們分別詳細(xì)講解一下這兩種模式的區(qū)別和用法。
在GDB中調(diào)試多線程程序,默認(rèn)處于All-Stop Mode,即只要有一個(gè)線程被中斷執(zhí)行,其他所有的線程都會(huì)被中斷執(zhí)行。
比如,程序在運(yùn)行時(shí),按Ctrl+C,所有的線程都會(huì)被中斷執(zhí)行,同樣的,當(dāng)恢復(fù)程序執(zhí)行時(shí),所有的線程都會(huì)同時(shí)恢復(fù)執(zhí)行。比如執(zhí)行continue、step、next等命令。

也就是說,在All-Stop模式下,只要有一個(gè)線程由于觸發(fā)斷點(diǎn)等原因停止執(zhí)行,整個(gè)進(jìn)程都會(huì)被停下來,這在一定程度上可以保證所有線程狀態(tài)的同步,保持?jǐn)?shù)據(jù)的一致性。比如,在查看某個(gè)可能會(huì)被多個(gè)線程修改的數(shù)據(jù)時(shí),不需要擔(dān)心在查看的同時(shí)被其他線程修改掉。
但是,這也給程序調(diào)試帶來了一些困難,比如,無法100%精確地進(jìn)行單步調(diào)試。有時(shí)你會(huì)發(fā)現(xiàn),在執(zhí)行step命令之后,程序卻停在了另外一個(gè)線程中。

在線程2和線程3都會(huì)調(diào)用do_stuff(),在do_stuff()設(shè)置斷點(diǎn)后,線程2先觸發(fā)斷點(diǎn)停在第4行,同時(shí)當(dāng)前線程切換為線程2,然后執(zhí)行step命令單步執(zhí)行一條語句,理論上講,線程2執(zhí)行一條語句應(yīng)該停在第5行代碼,然而實(shí)際上卻是執(zhí)行完step命令后,線程3觸發(fā)了斷點(diǎn),而此時(shí)線程3并未按照step命令的預(yù)期執(zhí)行一行代碼,而是仍然停留在第4行。再次單步執(zhí)行,線程2卻再次觸發(fā)了斷點(diǎn),說明線程2執(zhí)行了不止一條語句(思考題:這是為什么呢?)!
為了解決這個(gè)問題,GDB提供了set scheduler-locking mode命令,鎖定當(dāng)前線程。(思考題:針對(duì)這個(gè)特定案例,還有其他方法嗎?)
set scheduler-locking mode
mode 可能是:
? off:不鎖定任何線程,當(dāng)恢復(fù)程序執(zhí)行時(shí),所有線程都可以自由執(zhí)行
? on:鎖定當(dāng)前線程,執(zhí)行continue、step、next、finish等命令時(shí),只有當(dāng)前線程恢復(fù)執(zhí)行,其余線程仍然處于中斷狀態(tài)
? step:當(dāng)單步執(zhí)行時(shí),和on一樣,其余情況和off一樣。簡(jiǎn)單來說,就是在這個(gè)鎖定模式下執(zhí)行單步操作,只有當(dāng)前線程會(huì)執(zhí)行,其他線程仍然處于中斷狀態(tài)。而執(zhí)行其他命令時(shí),如continue、finish等,和off一樣,即所有線程都可以自由執(zhí)行。
? replay:在反向調(diào)試時(shí)和on一樣,其余情況下,和off一樣。后續(xù)會(huì)有專門文章詳細(xì)介紹反向調(diào)試。
默認(rèn)是replay模式,可以通過show scheduler-locking查看當(dāng)前鎖定模式。
我們演示一下:

簡(jiǎn)單解釋一下:
??先在do_stuff()設(shè)置斷點(diǎn),并執(zhí)行continue恢復(fù)程序執(zhí)行。隨后,線程2觸發(fā)斷點(diǎn),停在第4行代代碼。
??此時(shí)程序的狀態(tài)是:線程2由于觸發(fā)斷點(diǎn),停在第4行;由于在All-Stop模式,所以線程3也被中斷,恰巧也停在第4行
??然后執(zhí)行set scheduler-locking on 鎖定當(dāng)前線程2。
??此時(shí)單步執(zhí)行,線程2停在第5行,而線程3仍然停在第4行。再執(zhí)行continue,線程2會(huì)再次觸發(fā)斷點(diǎn),此時(shí)發(fā)現(xiàn)線程3仍然停在第4行。
可見,執(zhí)行set scheduler-locking on開啟鎖定模式之后,無論用什么命令恢復(fù)程序執(zhí)行,只有當(dāng)前線程2會(huì)執(zhí)行,其余線程一直處于中斷狀態(tài)。
不過,同時(shí)中斷所有的線程,可能會(huì)導(dǎo)致有些程序無法正常運(yùn)行。比如,有些程序需要有專門的線程實(shí)時(shí)處理外部的消息或事件等,如果把所有的線程都中斷的話,會(huì)由于外部事件得不到及時(shí)響應(yīng)而導(dǎo)致程序異常終止。
這時(shí),就需要用到Non-Stop模式
在Non-Stop模式下,一個(gè)線程被中斷執(zhí)行,并不會(huì)影響到其他線程。比如,一個(gè)線程觸發(fā)斷點(diǎn),只有這一個(gè)線程會(huì)被中斷執(zhí)行,其余線程不受影響繼續(xù)執(zhí)行。同樣的,在程序運(yùn)行時(shí),執(zhí)行Ctrl+C,也只會(huì)中斷一個(gè)線程。
要開啟Non-Stop模式,需要在程序開始運(yùn)行前,執(zhí)行下面兩個(gè)命令:
set?pagination?off
set?non-stop?on可以通過下面這個(gè)命令關(guān)閉Non-Stop模式,即進(jìn)入All-Stop模式
set?non-stop?off查看Non-Stop模式是否開啟:
show?non-stop重新在GDB中加載測(cè)試程序,在開始運(yùn)行之前,執(zhí)行set non-stop on命令開啟Non-Stop模式:

執(zhí)行run(r)命令開始運(yùn)行程序,然后Ctrl+C中斷執(zhí)行,并用info threads命令查看所有線程狀態(tài):

可見,在Non-Stop模式下,Ctrl+C只中斷了線程1,線程2和3并未受到影響,仍處于運(yùn)行狀態(tài)。因此,此時(shí)無法通過bt命令獲取線程2和線程3的調(diào)用棧:

interrupt -a命令可以中斷所有線程的執(zhí)行,有時(shí)候這是非常必要的:

continue -a命令可以恢復(fù)所有線程的執(zhí)行:

我們知道,在Shell中執(zhí)行程序時(shí),后面加一個(gè)&符號(hào),可以把程序放在后臺(tái)執(zhí)行。在GDB中你同樣可以在命令后面加一個(gè)&符號(hào),這樣就能把命令放在后臺(tái)執(zhí)行。
之所以提供后臺(tái)執(zhí)行的模式,其實(shí)就是讓GDB始終可以接收用戶輸入,即便被調(diào)試程序正常運(yùn)行時(shí),仍然可以在GDB中執(zhí)行命令。這在Non-Stop模式下,有時(shí)會(huì)非常有用。
前面講過,Non-Stop模式下,按Ctrl+C只會(huì)中斷那個(gè)接收到SIGINT信號(hào)的線程,其他線程不會(huì)受到影響繼續(xù)運(yùn)行。但是,有時(shí)候,為了盡量保證線程狀態(tài)的同步,我們可能想要讓所有的線程同時(shí)被中斷或同時(shí)恢復(fù)運(yùn)行,這種情況下,就可以通過后臺(tái)執(zhí)行命令的方式來實(shí)現(xiàn)。

并不是所有的命令都支持后臺(tái)執(zhí)行,目前支持后臺(tái)執(zhí)行的命令有:continue、run、attach、step、stepi、next、nexti、finish、until
我們知道,在執(zhí)行系統(tǒng)調(diào)用的時(shí)候,如read(), write(), select()等,如果收到信號(hào),這些系統(tǒng)調(diào)用會(huì)異常返回,errno會(huì)被賦值為EINTR。
一般來說,程序中需要判斷系統(tǒng)調(diào)用的返回值和errno來對(duì)這種情況進(jìn)行適當(dāng)?shù)奶幚恚热缰卦囈淮巍2贿^,有些實(shí)現(xiàn)不太規(guī)范的程序可能不會(huì)對(duì)這種情況進(jìn)行判斷和處理。
因此在用GDB調(diào)試這類程序的時(shí)候,可能會(huì)發(fā)現(xiàn)程序的執(zhí)行邏輯發(fā)生了很大的變化。這是因?yàn)椋褂肎DB進(jìn)行調(diào)試時(shí),我們經(jīng)常需要中斷一個(gè)或多個(gè)線程的執(zhí)行來觀察當(dāng)前程序的運(yùn)行狀態(tài),如變量、內(nèi)存數(shù)據(jù)、寄存器等,而這有可能會(huì)導(dǎo)致一些系統(tǒng)調(diào)用異常返回,從而給人產(chǎn)生某種錯(cuò)覺,似乎程序正常執(zhí)行時(shí)的邏輯和在GDB中執(zhí)行時(shí)的邏輯有很大的差異。
不過,這也算是提前暴露了程序中隱藏的問題,從這個(gè)角度講,也是一件好事!
- info threads:查看線程狀態(tài)信息
- thread :切換當(dāng)前線程
- thread apply [thread-id-list | all]?:針對(duì)指定線程執(zhí)行命令
- break thread if :設(shè)置條件斷點(diǎn),僅對(duì)指定線程生效
- set scheduler-locking on/off/step/replay:控制是否鎖定當(dāng)前線程,如果當(dāng)前程序被鎖定,恢復(fù)程序運(yùn)行時(shí),只有當(dāng)前線程可以運(yùn)行
- set non-stop on:開啟Non-Stop模式,該模式下,一個(gè)線程被中斷執(zhí)行,不會(huì)影響其他線程
- set non-stop off:關(guān)閉Non-Stop模式,即進(jìn)入All-Stop模式。該模式下,一個(gè)線程被中斷執(zhí)行,其他所有線程都會(huì)被中斷
- interrut -a:中斷所有線程執(zhí)行
- command &:后臺(tái)執(zhí)行 本文是程序調(diào)試系列專題的第七篇。本系列專題旨在介紹一些高階調(diào)試技巧、調(diào)試器的工作原理以及常見問題的定位方法和思路等內(nèi)容。
《嵌入式Linux驅(qū)動(dòng)大全》
如何高效閱讀嵌入式項(xiàng)目代碼?
推薦一個(gè)好用的嵌入式靜態(tài)代碼掃描工具!