亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

蟲蟲首頁| 資源下載| 資源專輯| 精品軟件
登錄| 注冊

您現在的位置是:首頁 > 技術閱讀 >  高端 | 如何快速定位程序Core?

高端 | 如何快速定位程序Core?

時間:2024-02-11
導讀:程序core是指應用程序無法保持正常running狀態而發生的崩潰行為。程序core時會生成相關的core-dump文件,是程序崩潰時程序狀態的數據備份。core-dump文件中包含內存、處理器、寄存器、程序計數器、棧指針等狀態信息。本文將介紹一些利用core-dump文件定位程序core原因的方法和技巧。
全文7023字,預計閱讀時間 13分鐘。

一、程序Core定義及分類


程序core是指應用程序無法保持正常running狀態而發生的崩潰行為。程序core時會生成相關的core-dump文件,core-dump文件是程序崩潰時程序狀態的狀態數據備份。core-dump文件包含內存、處理器、寄存器、程序計數器、棧指針等狀態信息。我們可以借助core-dump文件來分析定位程序Core的原因。

這里我們從三個方面對程序Core進行分類:機器、資源、程序Bug。下表對常見的Core原因進行了分類:


二、函數棧介紹

當我們打開core文件時,首先關注的是程序崩潰時的函數調用棧狀態,為了方便理解后續定位core的一些技巧,這里先簡單介紹一下函數棧。

2.1 寄存器介紹

目前生產環境都為64位機,這里只介紹64位機的寄存器,如下:



對于x86-64架構,共有16個64位寄存器,每個寄存器的用途并不單一,如%rax通常保存函數返回結果,但也被應用于imul和idiv指令。這里重點關注%rsp(棧頂指針寄存器)、%rbp(棧底指針寄存器)、%rdi、%rsi、%rdx、%rcx、%r8、%r9(分別對應第1~6函數參數)。

Callee Save說明是否需要被調用者保存寄存器的值。

2.2 函數調用


2.2.1 調用函數棧幀:

在調用一個函數時首先進行的是參數壓棧,參數壓棧的順序跟參數定義的順序相反。注意,并不是參數一定會壓棧,在x86-64架構中會針對可以使用寄存器傳遞的變量,直接通過寄存器傳值,如數字、指針、引用等。

接著是返回地址壓棧,返回地址為被調用函數執行完后,調用函數執行的下一個指令地址。這里牢記返回地址的位置,后續章節會利用到這個返回地址的特性。

針對上面的介紹舉個例子說明:



如上圖,在main函數中調用了foo函數,首先對參數壓棧,三個參數都可以直接用寄存器傳遞(分別對應%edi、%esi、%edx),然后call指令將下一個指令壓棧。

2.2.2 被調用函數棧幀:

被調用函數首先會將上一個函數的棧底指針(%rbp)保存,即%rbp壓棧。然后再保存需要被保存的寄存器值,即Callee Save為True的寄存器。接著為臨時變量、局部變量申請棧空間。


針對被調用函數,舉個例子說明


如上圖,在foo函數執行時,先對main函數的%rbp壓棧,再把寄存器中的參數值存放到局部變量(a, b, c)中。

2.3 總結

通過對函數調用的簡單介紹,我們可以發現函數棧是一個縝密且脆弱的結構,內存結構必須按照嚴格的方式被訪問,如稍有不慎就可能導致程序崩潰。

三、GDB定位Core

這一節將介紹從core文件打開到定位全流程中可能會遇到的問題以及解決技巧。

3.1 Core文件

core文件在哪里?

查看“/proc/sys/kernel/core_pattern”確定core文件生成規則。


3.2 變量打印

程序debug過程中常常要查看各種變量(內存、寄存器、函數表等)的值是否正確,維持單獨用一節介紹下常用的變量打印方法以及一些冷門小技巧。

3.2.1 print命令

print [Expression]print $[Previous value number]print {[Type]}[Address]print [First element]@[Element count]print /[Format] [Expression]

Format格式:o - 8進制x - 16進制u - 無符號十進制t - 二進制f - 浮點數a - 地址c - 字符s - 字符串

3.2.2 x命令

x /<n/f/u>  <addr>n:是正整數,表示需要顯示的內存單元的個數,即從當前地址向后顯示n個內存單元的內容,一個內存單元的大小由第三個參數u定義。
f:表示addr指向的內存內容的輸出格式,s對應輸出字符串,此處需特別注意輸出整型數據的格式: x 按十六進制格式顯示變量. d 按十進制格式顯示變量。 u 按十進制格式顯示無符號整型。 o 按八進制格式顯示變量。 t 按二進制格式顯示變量。 a 按十六進制格式顯示變量。 c 按字符格式顯示變量。 f 按浮點數格式顯示變量。
u:就是指以多少個字節作為一個內存單元-unit,默認為4。u還可以用被一些字符表示: 如b=1 byte, h=2 bytes,w=4 bytes,g=8 bytes.
<addr>:表示內存地址。

3.2.3 容器對象打印

利用上面的print和x命令,再結合容器的數據結構,我們就能知道容器的詳細信息。這里舉個完整打印二進制string的例子,string的數據結構如下:



string為空時,_M_dataplus._M_p是指向nullptr的。當賦值后會在堆上申請一段內存,分為兩段,前半段是meta信息(類型為std::string::_Rep),如length、capacity、refcount,后半段為數據區,_M_p指向數據區。

通常情況下非二進制的string,直接print即可顯示數據內容,但當數據為二進制時,'\0'會截斷打印內容。因此,打印二進制string的首要任務是確認string的size。

string的size信息保存在std::string::_Rep結構體中,根據上面的數據結構可以發現,_Rep與_M_dataplus._M_p相差一個結構體大小,因此打印_Rep結構體的命令為:

#先把_M_p轉成_Rep指針,再讓指針向低地址偏移一個結構體大小p *((std::string::_Rep*)(s._M_dataplus._M_p) - 1)


找到string的size(_M_length)后,再通過x命令打印相關的內存區即可,命令為:

#這里的n是_Rep._M_lengthx /ncb s._M_dataplus._M_p


運行效果如下:


為了方便,這里推薦一個方便的腳本:stl-views.gdb(鏈接:https://sourceware.org/gdb/wiki/STLSupport?action=AttachFile&do=view&target=stl-views-1.0.3.gdb直接在gdb終端source stl-views.gdb即可,支持常見的容器打印,如vector、map、list、string等。

3.2.4 靜態變量打印

程序中經常會使用到靜態變量,有時我們需要查看某個靜態對象的值是否正確,就涉及到靜態對象的打印。看如下例子:

void foo() {    static std::string s_foo("foo");}


這里可以借助nm -C ./bin  | grep xx找到靜態變量的內存地址,再通過gdb的print打印。

3.2.5 內存dump

dump [format] memory filename start_addr end_addrdump [format] value filename exprformat一般使用binary,其他的可以查看gdb手冊。
比如我們可以結合上面查看string內容的例子dump整個string數據到文件中。dump binary memory file1 s._M_dataplus._M_p s._M_dataplus._M_p + length
如果想查看文件內容的話可把vim -b和xxd結合使用。

接上面string的例子,舉一個dump string內存數據到文件的例子:

3.3 定位代碼行

定位core的原因,首先要定位崩潰時正在執行的代碼行,這一節主要介紹一些定位代碼行的方法。通常情況下直接通過gdb的breaktrace即可一覽整個函數棧,但有時候函數棧信息并非如此清晰明了,這時就可利用一些小技巧來查看函數棧。

3.3.1 去編譯優化

有時候會發現core的函數棧跟實際的代碼行不匹配,如果是在線下環境中,可以嘗試把編譯優化設置成-O0,然后再重新復現core問題。

3.3.2 程序計數器 + addr2line

對于線上core問題,一般沒法再對程序進行去編譯優化操作,只能在現有的core文件基礎上進行代碼定位。這一節我們采用一個例子來介紹如何使用程序計數器 + addr2line來定位代碼行。



從截圖可以發現frame 20指示的代碼行與實際的代碼行是不匹配的,定位步驟如下:


# 跳轉到第20號棧frame 20 # 使用display命令顯示程序計數器display /i $rip # 使用addrline工具做地址轉換shell /opt/compiler/gcc-8.2/bin/addr2line -e bin address


3.3.3 函數棧修復

有時候我們會發現函數調用棧里面會出現很多??的情況,這常發生于棧被寫花,某些情況下手動進行修復。函數棧的修復利用的函數棧內存分布知識,見第一節。


-----------------------------------Low addresses-----------------------------------0(%rsp)  | top of the stack frame          | (this is the same as -n(%rbp))---------|--------------------------n(%rbp) | variable sized stack frame-8(%rbp) | varied0(%rbp)  | previous stack frame address8(%rbp)  | return address-----------------------------------High addresses


從上面的棧示意圖可以發現,利用%rbp寄存器即可找到上一個函數的返回地址棧底指針,再利用addr2line命令找到對應的代碼行。這里舉一個例子:


#首先找到當前被調用棧上一個棧的棧底指針值和返回地址x /2ag $rbp # 2個單位,a=十六進制,g=8字節單元  #使用上一條命令得到的棧底指針值依次遞歸x /2ag address

3.3.4 無規律core棧

無規律core棧問題一般發生于堆內存寫壞。函數調用是一個非常精密的過程,任何一個位置發生非預期的讀寫都會導致程序崩潰。這里可以舉個小例子來說明:


int main(int argc, char* argv[]) {    std::string s("abcd");    *reinterpret_cast<uint64_t*>(&s) = 0x11;    return 0;}


上面的例子core在string析構上,原因是因為string的_M_ptr被改寫成了0x11,析構流程變成了非法內存操作。

同理,由于進程堆空間是共享的,一個線程對堆的非法操作就可能會影響另一個線程的正常操作,由于堆分配的隨機性,表現出來的現象就是無規律core棧。

針對無規律core棧最好的方式還是借助AddressSanitizer


#設置編譯參數CXXFLAGSCXXFLAGS="-fPIC -fsanitize=address  -fno-omit-frame-pointer" #設置鏈接參數LDFLAGS="-lasan" # 設置啟動環境變量export ASAN_OPTIONS=halt_on_error=0:abort_on_error=1:disable_coredump=0 # 啟動LD_PRELOAD=/opt/compiler/gcc-8.2/lib/libasan.so ./bin/xxx


3.3.5 總結

上面提到的幾種方法都是為了找到具體的問題代碼行,為后續分析core的具體原因提供線索。


3.4 定位Core原因

這一節主要介紹定位Core原因的方法以及一些常見原因的介紹。


3.4.1 確認信號量

從上面的Core分類我們可以發現某些場景的core是由于機器故障導致的,如SIGBUS,因此可以先通過信號量排除掉一些core原因。


3.4.2 定位異常匯編指令

通過上面的代碼行定位我們可以大致找到程序core在哪一行,比較簡單的core直接print程序上下文即可找到core的原因。

但有些場景下,通過排查上下文無任何異常,這個時候就需要準確定位具體的異常匯編指令,根據指令找原因。

查看匯編指令比較簡單的方法是使用layout asm命令,frame指向那個棧,就顯示對應棧的匯編。這里舉個core例子,如下:



程序顯示core在start函數,查看相關上下文變量均無異常。使用layout asm打開正在執行的匯編指令,如下:

查看匯編定位到程序core在mov指令,mov指令上一個指令為sub,為棧申請了3M空間,懷疑是棧空間不足。采用frame 0的%rsp - frame N的%rbp排查為棧空間不足。

通過上面的例子,可以發現定位異常匯編指令位置后,我們能夠把異常點進一步壓縮,定位到是哪個指令、變量、地址導致的core問題。


3.4.3 排查異常變量

通過上面的操作我們可以準確定位到具體是哪一行代碼的哪一條指令出現了問題,根據異常指令我們可以排查相關的變量,確定變量值是否符合預期。


這里舉一個比較經典的空指針例子,如下:

int main(int argc, char* argv[]) {    int* a = nullptr;    *a = 1;    return *a;}


通過匯編指令我們可以發現是movl $0x1, (%rax)出現了問題,%rax的值來自于0x8(%rbp)x命令打印相關的地址就可以發現為空指針錯誤。


3.4.4 查看被優化變量

通常情況下程序都是開啟了編譯優化的,就會出現變量無法被print,提示變量被優化,有時可利用匯編 + 寄存器的方式查看被優化的變量。

這里舉一個例子說明下:

void foo(char const* str) {    char buf[1024] = {'\0'};    memcpy(buf, str, sizeof(buf));}
int main(int argc, char* argv[]) {    foo("abcd");    return 0;}


通常情況下在foo函數內部,str變量是會直接別優化掉的,因為可以直接利用%rdi寄存器傳遞參數。為了能夠打印出str的值,這個時候我們可以借助匯編 + 寄存器的方式找到具體的變量值,如下:



首先找到main函數調用foo函數的參數壓棧匯編:mov $0x402011, %edi,這里的0x402011即為str的內存地址,通過x命令即可顯示str的值了。

比較復雜的場景可能沒法直接找到被優化變量,這時可以采用匯編回溯的方式找到變量。


3.4.5 異常函數地址排查

有時的core問題是因為數據異常導致,有時也可能是優化函數地址導致,如調用虛函數地址錯誤、函數返回地址錯誤、函數指針值錯誤。

異常函數地址排查同理于異常變量排查,根據匯編指令確認調用是否異常即可。這里舉一個虛函數地址異常的例子,如下:

classA {public:    virtual ~A() = default;    virtual void foo() = 0;};classB : public A {public:    void foo() {}}; int main(int argc, char* argv[]){    A* a = new B;    a->foo();    A* b = new B;    *reinterpret_cast<void**>(b) = 0x0;    b->foo();     return 0;}


從匯編指令看是core在了mov (%rax), %rax,結合指令上下文可發現是在虛函數地址尋址操作,對比兩個變量的虛函數表即可發現是函數地址load錯誤導致的core。


3.4.6 總結

定位core的基本流程可總結為以下幾步:

  1. 明確core的大致觸發原因。機器問題?自身程序問題?
  2. 定位代碼行。哪一行代碼出現了問題。
  3. 定位執行指令。哪一行指令干了什么事。
  4. 定位異常變量。指令不會有問題,是指令操作的變量不符合預期。
善于利用匯編指令以及打印指令(x、print、display)可以更有效的定位Core。

參考資料:

匯編查看工具:https://godbolt.org/ https://cppinsights.io/
標準GDB文檔:https://sourceware.org/gdb/current/onlinedocs/gdb/


往期推薦



推薦幾個開源庫

C 語言的那些坑!

高端知識點 — socket fd 是什么?

Linux C++ 服務器端這條線怎么走?

百家號在線視頻編輯器的技術演進


一鍵三連,好運連連,bug不見????

主站蜘蛛池模板: 肇东市| 梨树县| 汾阳市| 永丰县| 汝城县| 东乡族自治县| 陕西省| 陆川县| 维西| 汉川市| 松溪县| 镇原县| 五指山市| 田东县| 周宁县| 鄄城县| 林周县| 扎囊县| 鄱阳县| 弋阳县| 兴城市| 东兰县| 鸡东县| 凤台县| 东海县| 永年县| 习水县| 古丈县| 湖口县| 香格里拉县| 上思县| 易门县| 勐海县| 舞钢市| 新化县| 海淀区| 赣州市| 内乡县| 化州市| 高雄县| 永昌县|