引言
我們在做Linux開發時,常常會遇到程式崩潰的問題,這時會用gdb或者通過查看反匯編的方式去對程式進行分析,接下來,我們從底層的角度,去講述如何分析程式崩潰的原因,
一、常見BUG
在進行分析前,先看看我總結歸納的常見BUG:
1.記憶體錯誤:
記憶體錯誤往往出現在使用了未分配的記憶體,或者沒有及時釋放分配的記憶體,
2.指標錯誤:
指標錯誤往往出現在使用了空指標,或者是指向的地址在函式回傳后丟失,或者是偏移量出了問題,這個話題暫時不展開討論,
3.判斷條件出錯:
比如 if (a == 1) 寫成了 if (a=1),if (a && b) 寫成了 if (a & b),
4.引數未初始化
比較典型的就是申請結構體變數沒對其進行memset,加之介面內部沒做引數判斷,從而傳參導致介面例外,
5.未考慮位元組序
在跨平臺通信時要考慮位元組序的問題,arm一般是小端位元組序( little - endian ),x86一般是大端位元組序( big - endian),arm和x86進行通信時,要考慮到位元組序問題,
6.執行緒同步錯誤
往往體現在共享資料上鎖不當,導致執行緒死鎖,
7.位元組對齊錯誤
需要了解編譯器對于位元組對齊的默認屬性,一般是4位元組對齊,體現在結構體的設計方面,如果位元組對齊做的不好,軟體版本更新迭代會帶來極大的隱患,通常做法是加上reserve欄位,
8.配對操作沒呼叫完全
比如open沒有及時close,init沒有及時release,從而導致資源的浪費,
二、從匯編角度分析C程式
我們在嵌入式開發中,使用arm架構居多,所以這里討論的是arm架構的分析方案,如果用x86也可以用類似的思想,我們日常開發中遇到的程式崩潰,比較難查的問題往往是出現在記憶體訪問部分,下面我們通過底層的匯編程式來講述一段C語言程式,對于記憶體,硬體,是怎樣一個執行流程,
1.arm匯編相關理論基礎
下面我列舉一些arm匯編的暫存器,并對其進行描述

常用的arm用戶態暫存器如上表所示,有r0~r15這16個暫存器
r0~r3:通常在函式傳參時使用(從左到右的順序,大于4個引數時使用堆疊來傳遞)和回傳值(r0通常被用作回傳值),在函式內部 r0-r3 也可以用來存盤區域變數,
r4~r8,r10,r11:通常用來保存區域變數,r11通常用來作為(FP)堆疊基地址(下面會對這些概念進行講述)
r12:可能在函式呼叫時被聯結器使用,在函式內部,也可以存盤區域變數,
r13:是SP暫存器,就是當前函式的堆疊頂指標,
r14:是LR暫存器,存放當前函式的回傳地址,
r15:是PC暫存器,存放當前指令的地址,
上面講述的FP,SP,LR,PC暫存器,它暫存器里面的內容是地址,這點不要混淆,
2.記憶體中的堆疊幀結構
剛剛我們提到了FP,SP,LR,PC暫存器,現在我們來展開聊聊這幾個暫存器,
PC指標:剛剛提到PC指標里面存放著當前指令的地址,因為在我們arm架構,傳統上是五級流水線,簡單描述就是取址,然后取完代碼是二進制,對它進行譯碼,翻譯成各個動作,然后cpu參與計算,最后回傳,PC指標就存放著當前指令的地址,扮演的角色就是告訴cpu需要訪問的地址,也對應五級流水線中的取址操作,
SP指標:在函式申請變數的時候,會有一個動態壓堆疊的程序,堆疊的大小會隨著變數申請而逐漸增長,SP指標就指向你動態壓堆疊所處在的地址,
FP指標:當前函式的起始地址,在函式呼叫時,進入另一個函式介面,也會進入另一個堆疊幀結構,里面會保存呼叫者的的起始地址(FP),用于出現問題時回溯,同時也有當前函式的起始地址(FP),
LR指標:函式呼叫時,呼叫者的下一條指令地址,用于函式呼叫完回傳時,可以進入下一條指令,
我們不妨先看看記憶體中的堆疊幀結構,

這就是記憶體中的堆疊幀結構,上圖就是main函式在呼叫func1時的堆疊幀結構,綠色的是處在func1函式里面的,灰色的是在main函式里面的,
通過這張圖,我們可以看出,在發生函式呼叫時,FP暫存器會指向當前函式呼叫的起始地址,在func1內部(綠色部分)還有一個FP,這個FP不是FP暫存器,而是記憶體中的資料,也表示地址,它指向呼叫者(main)的起始地址,它主要是在程式崩潰時,用來回溯上一級的PC LR SP FP的值,所以在呼叫時會保存上一個堆疊幀的資料,用來崩潰的時候一層一層回溯回去,
3.舉例說明
在舉例說明例子之前,先講一下幾個常用的匯編指令
mov:給某個暫存器賦值
/* 給r3暫存器賦值為8 */
mov r3, #8
add:加法運算
/* r3 = r3 + 4 此處r3代表r3暫存器中的資料 */
add r3, r3, #4
sub:減法運算
/* r3 = r3 - 4 此處r3代表r3暫存器中的資料 */
sub r3, r3, #4
str:把暫存器的值寫入記憶體
/* r3和fp是arm的暫存器,剛剛有所提及 *
* 右邊中括號里面的代表地址,代表fp指標指向的地址向下偏移8個位元組 *
* 這條的指令意思就是把r3暫存器的資料寫入右邊的地址. */
str r3, [fp, #-8]
ldr:從記憶體中取資料到暫存器
/* r0和pc是arm的暫存器 剛剛有所提及 *
* 右邊中括號里面的代表地址,代表pc指標指向的地址向上偏移20個位元組 *
* 這條的指令意思就是從右邊的地址指向的記憶體中取資料到r0暫存器. */
ldr r0, [pc, #20]
bl:跳轉到指定地址
/* 跳轉到83ec地址,它是函式func_1的起始地址 */
bl 83ec <func_1>
下面我來寫一段很簡單的代碼
#include <stdio.h>
int func(int num1, int num2, int num3)
{
int n = 0;
n = num1 + num2;
n = n + num3;
return n;
}
int main()
{
int a = 0;
int b = 1;
a = func(1, 2, b);
printf("value = %d\n", a);
return 0;
}
翻譯成反匯編(這里采用海思的交叉編譯工具編譯,然后通過objdump生成反匯編)
arm-himix200-linux-gcc test.c -o test
arm-himix200-linux-objdump -d test > debug.txt
反匯編中的C語言部分
00010410 <func>:
/* 將呼叫者main的fp指標壓入堆疊中,保存用于回溯 */
10410: e52db004 push {fp} ; (str fp, [sp, #-4]!)
/* fp的地址此處不做偏移,因為只壓入一個資料 */
10414: e28db000 add fp, sp, #0
/* 將sp暫存器向低地址減28位元組,其實這里是個開辟堆疊記憶體的動作 */
10418: e24dd01c sub sp, sp, #28
/* 將剛剛函式傳參的r0暫存器的資料寫入fp指標再向低地址偏移16位元組的地址 */
1041c: e50b0010 str r0, [fp, #-16]
/* 將剛剛函式傳參的r1暫存器的資料寫入fp指標再向低地址偏移20位元組的地址 */
10420: e50b1014 str r1, [fp, #-20] ; 0xffffffec
/* 將剛剛函式傳參的r2暫存器的資料寫入fp指標再向低地址偏移24位元組的地址 */
10424: e50b2018 str r2, [fp, #-24] ; 0xffffffe8
/* 對應函式呼叫里面的int n = 0,將0直接賦值給r3暫存器 */
10428: e3a03000 mov r3, #0
/* 將r3暫存器的值寫入fp指標再向低地址偏移8位元組的地址 */
1042c: e50b3008 str r3, [fp, #-8]
/* 將fp指標再向低地址偏移16位元組的地址的資料拿出來,讀取到r2暫存器里,準備做加法運算 */
10430: e51b2010 ldr r2, [fp, #-16]
/* 將fp指標再向低地址偏移20位元組的地址的資料拿出來,讀取到r2暫存器里,準備做加法運算 */
10434: e51b3014 ldr r3, [fp, #-20] ; 0xffffffec
/* 將剛剛拿出來的兩個資料做加法運算,對應函式呼叫里面的n = num1 + num2; */
10438: e0823003 add r3, r2, r3
/* 將剛剛算到的結果存回堆疊中 */
1043c: e50b3008 str r3, [fp, #-8]
/* 再從堆疊中把剛剛的資料讀出來 */
10440: e51b2008 ldr r2, [fp, #-8]
/* 將fp指標再向低地址偏移24位元組的地址的資料拿出來,讀取到r2暫存器里,準備做第二個加法運算 */
10444: e51b3018 ldr r3, [fp, #-24] ; 0xffffffe8
/* 將剛剛拿出來的兩個資料做加法運算,對應函式呼叫里面的n = n + num3; */
10448: e0823003 add r3, r2, r3
/* 將剛剛的計算的結果再寫入堆疊記憶體中 */
1044c: e50b3008 str r3, [fp, #-8]
/* 從堆疊記憶體中取出資料放入r3暫存器 */
10450: e51b3008 ldr r3, [fp, #-8]
/* 將r3暫存器的資料轉給r0暫存器,r0暫存器一般用來做回傳值用 */
10454: e1a00003 mov r0, r3
/* 將sp指標歸位,釋放堆疊空間 */
10458: e28bd000 add sp, fp, #0
/* 將fp暫存器出堆疊 */
1045c: e49db004 pop {fp} ; (ldr fp, [sp], #4)
/* 跳轉到lr暫存器所指向的地址,也就是函式呼叫的下一行 */
10460: e12fff1e bx lr
00010464 <main>:
/* 運行到這里,堆疊幀已經生成,第一步push將fp, lr指標壓入堆疊中 */
10464: e92d4800 push {fp, lr}
/* 因為剛剛那步同時push了兩個值,fp和lr兩個資料存放的地址要區分開來, *
* sp代表當前動態壓堆疊所處在的地址,所以將fp暫存器的地址向高地址偏移4個單位 */
10468: e28db004 add fp, sp, #4
/* 將sp暫存器的地址向高地址偏移8個單位,存放在fp,lr的后面 */
1046c: e24dd008 sub sp, sp, #8
/* 對應剛剛的int a = 0,這里把0賦值給r3暫存器 */
10470: e3a03000 mov r3, #0
/* 將r3暫存器的值寫入記憶體,地址在fp指標指向的地址往低地址偏移8個單位(堆疊的生長方向是高地址到低地址) */
10474: e50b3008 str r3, [fp, #-8]
/* 對應剛剛的int b = 1,這里把1賦值給r3暫存器 */
10478: e3a03001 mov r3, #1
/* 將r3暫存器的值寫入記憶體,地址在fp指標指向的地址往低地址偏移12個單位(堆疊的生長方向是高地址到低地址) */
1047c: e50b300c str r3, [fp, #-12]
/* 在剛剛變數b寫入的地址中取出b的值,寫入r2暫存器,這里r2將作為函式引數用 */
10480: e51b200c ldr r2, [fp, #-12]
/* 將2賦值給r1,這里r1將作為函式引數用 */
10484: e3a01002 mov r1, #2
/* 將1賦值給r1,這里r1將作為函式引數用 */
10488: e3a00001 mov r0, #1
/* 引數準備完畢,跳轉到func函式,進行函式呼叫 */
1048c: ebffffdf bl 10410 <func>
/* 這邊就是lr暫存器指向的地址,函式呼叫完,回到了這里.
這里將函式的回傳值存入fp指向的地址向低地址偏移8個位元組的位置 */
10490: e50b0008 str r0, [fp, #-8]
/* 從剛剛存入的資料從記憶體中取出來,放入r1暫存器,準備呼叫printf函式 */
10494: e51b1008 ldr r1, [fp, #-8]
/* 將字串讀入r0暫存器,準備函式呼叫 */
10498: e59f0010 ldr r0, [pc, #16] ; 104b0 <main+0x4c>
/* 進入printf */
1049c: ebffff85 bl 102b8 <printf@plt>
/* 將r0 r3暫存器清0 */
104a0: e3a03000 mov r3, #0
/* 將r0 r3暫存器清0 */
104a4: e1a00003 mov r0, r3
/* 釋放堆疊大小 */
104a8: e24bd004 sub sp, fp, #4
/* 將fp pc暫存器出堆疊 */
104ac: e8bd8800 pop {fp, pc}
104b0: 00010524 .word 0x00010524
已經寫好了注釋,可以通過注釋從main函式開始一步一步看,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/328000.html
標籤:其他
