C++(Qt)软件调试---bug排查记录(36)
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 # 打印链接路径