C++(Qt)軟件調試—bug排查記錄(36)
文章目錄
- C++(Qt)軟件調試---bug排查記錄(36)
- @[toc]
- 1 無返回值函數風險
- 2 空指針調用隱患
- 3 Debug/Release差異
- 4 ARM架構char符號問題
- 5 linux下找不到動態庫
文章目錄
- C++(Qt)軟件調試---bug排查記錄(36)
- @[toc]
- 1 無返回值函數風險
- 2 空指針調用隱患
- 3 Debug/Release差異
- 4 ARM架構char符號問題
- 5 linux下找不到動態庫
更多精彩內容 |
---|
👉內容導航 👈 |
👉C++軟件調試 👈 |
1 無返回值函數風險
-
如果一個函數的返回值為void,則編譯器會自動插入一個默認return;
-
如果非void的函數沒有寫return,有些版本的編譯器會默認插入一個
ret
(例如gcc7.3,高版本gcc會插入ud2
),不過這個代碼還是不安全的。 -
如果一個函數的返回值不為void,但是忘記寫return語句了,會破壞棧幀的出棧,導致未定義異常,可能會軟件崩潰,也可能不會崩潰,調用者會試圖從棧上的某個位置讀取返回值。由于這個位置沒有被正確設置,讀取的內容將是隨機的數據,這可能導致程序繼續運行但行為異常;
-
Release版本與Debug版本的差異:在Debug版本中,由于內存受到保護,即使函數沒有return語句,也可能不會立即導致程序崩潰。但在Release版本中,由于所有保護都被移除,訪問錯誤內存或寄存器值很可能導致程序異常退出或崩潰。
-
使用基本數據類型有可能不會導致程序崩潰。
-
例如:
int fun1()
{int a = 123;
}
QByteArray fun2()
{QByteArray arr("123");
}
int main()
{fun1();fun2();return 0;
}
-
并且這種情況導致的程序崩潰使用調試工具很難定位,定位的位置非常隨機。
-
不過要視編譯器而定,有些編譯器會編譯報錯,例如MSVC,有些不會,例如gcc默認只是會報警告
-Wreturn-type
。 -
gcc可以通過
-Werror=return-type
選項將警告設置為錯誤信息,防止忽略;gcc編譯選項 -
或者使用MSVC編譯器編譯程序,也可以檢測出未寫return的錯誤。
-
QMake可以通過下面配置將缺失返回值警告設置為錯誤。
QMAKE_CC += -Werror=return-type QMAKE_CXX += -Werror=return-type
-
如下圖所示,如果非void返回值函數沒有寫return語句,則在函數匯編中缺失
pop
和ret
指令;pop
通常用于恢復先前保存的寄存器值(如ebp
或者rbp
在 x86 架構上),或者彈出參數等。如果沒有執行pop
操作,那么這些值將不會被正確地恢復,這可能會導致后續函數調用或程序邏輯出現問題。- 當函數調用發生時,返回地址會被壓入棧中。如果函數結束時沒有使用
ret
來處理這個返回地址,棧指針將指向錯誤的位置,破壞棧的結構,影響后續的函數調用和返回操作。 - 缺少正確的
pop
和ret
指令使得調試更加復雜,因為正常的調用堆棧信息不再可靠。
2 空指針調用隱患
- 當一個對象為空指針或者野指針時如果調用成員函數,可能會出現未定義異常導致崩潰,也可能不會崩潰;
- 如果調用的成員函數沒有使用this指針寫入數據,則不會導致崩潰;
- 情況1:沒有使用到任何成員變量;
- 情況2:調用的是static成員函數;
- 情況3:只是讀取成員變量,沒有寫入成員變量。
- 這種不崩潰是危險的
- 不可預測性:行為依賴于編譯器、平臺和運行時狀態
- 隱蔽性:錯誤可能在生產環境中突然出現
- 調試困難:問題表現不穩定,難以復現
#include <iostream>
using namespace std;
class A {
public :void f(int x) {int a = x;// m_a = 123; // 崩潰for(int i = 0; i < x; i++){cout << i << endl;}cout << a <<" " << &m_a <<" "<<this << endl;}
private:int m_a;
};int main() {A* a ;a -> f(10);a->f(2);return 0;
}
3 Debug/Release差異
Debug模式下通常會有更多的內存保護機制,這有助于捕獲潛在的內存錯誤。
這些保護機制在Release模式下可能被禁用或簡化,從而導致某些問題在Debug模式下不明顯但在Release模式下暴露出來。具體來說:
-
內存初始化:
- Debug模式下,編譯器可能會自動將未初始化的變量設置為特定值(如0或特殊標記值),以幫助檢測未初始化變量的使用。
- Release模式下,未初始化的變量保持未初始化狀態,可能導致不可預測的行為。
-
邊界檢查:
- Debug模式下,可能會啟用額外的數組和指針邊界檢查,防止越界訪問。
- Release模式下,這些檢查通常被移除以提高性能,因此越界訪問可能導致崩潰或未定義行為。
-
堆棧保護:
- Debug模式下,堆棧可能會有更多的保護措施,例如填充“安全”值來檢測堆棧溢出。
- Release模式下,這些保護措施可能被移除或簡化。
-
調試信息:
- Debug模式下,程序會包含更多的調試信息和符號表,便于調試工具(如GDB、Visual Studio Debugger)進行更詳細的分析。
- Release模式下,這些調試信息通常被移除,導致難以通過調試工具捕捉到問題。
解決方法
-
使用靜態分析工具:
- 使用靜態分析工具(如Clang Static Analyzer、Cppcheck)來檢測代碼中的潛在問題。
-
啟用運行時檢查:
- 在Release模式下啟用運行時檢查工具,如AddressSanitizer、Valgrind等,可以幫助檢測內存錯誤。
-
確保一致的初始化:
- 確保所有變量在使用前都已正確初始化,避免依賴Debug模式下的默認初始化行為。
-
檢查內存分配和釋放:
- 檢查動態內存分配和釋放是否正確,確保沒有內存泄漏或雙重釋放的問題。
-
審查多線程代碼:
- 如果程序涉及多線程,確保線程同步機制正確無誤,避免競爭條件和死鎖。
-
對比宏定義:
- 檢查Debug和Release模式下的宏定義差異,確保兩種模式下的行為一致,特別是與內存管理相關的宏定義。
4 ARM架構char符號問題
-
在vs編譯器、x86架構linux中的gcc編譯器ux中的gcc編譯器都是把char定義為signed char;
-
arm-linux-gcc把char定義為unsigned char;
-
所以直接使用char有移植性問題,例如在x86架構中開發的程序,在arm架構系統(例如國產銀河麒麟、樹莓派、Android等)中可能就會出現問題,并且這種情況很隱蔽,比較難排查;
-
例如在cppreference中的定義:
-
解決辦法:
- 在編譯時加上選項
-fsigned-char
; - 不使用char,改成使用int8_t或者qint8;
- 在編譯時加上選項
5 linux下找不到動態庫
-
使用
ldd
命令查看可執行程序或者動態庫的鏈接路徑,是否找得到動態庫; -
使用下面命令查看、修改動態庫鏈接路徑
patchelf --set-rpath '$ORIGIN/lib/' ./RadarServer # 設置程序動態庫鏈接路徑 patchelf --print-rpath ./RadarServer # 打印鏈接路徑