C++(Qt)软件调试---bug排查记录(36)

发布于:2025-09-06 ⋅ 阅读:(22) ⋅ 点赞:(0)

C++(Qt)软件调试—bug排查记录(36)


更多精彩内容
👉内容导航 👈
👉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语句,则在函数汇编中缺失popret指令;

    • pop 通常用于恢复先前保存的寄存器值(如 ebp 或者 rbp 在 x86 架构上),或者弹出参数等。如果没有执行 pop 操作,那么这些值将不会被正确地恢复,这可能会导致后续函数调用或程序逻辑出现问题。
    • 当函数调用发生时,返回地址会被压入栈中。如果函数结束时没有使用 ret 来处理这个返回地址,栈指针将指向错误的位置,破坏栈的结构,影响后续的函数调用和返回操作。
    • 缺少正确的 popret 指令使得调试更加复杂,因为正常的调用堆栈信息不再可靠。

在这里插入图片描述


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模式下,这些调试信息通常被移除,导致难以通过调试工具捕捉到问题。

解决方法

  1. 使用静态分析工具

    • 使用静态分析工具(如Clang Static Analyzer、Cppcheck)来检测代码中的潜在问题。
  2. 启用运行时检查

    • 在Release模式下启用运行时检查工具,如AddressSanitizer、Valgrind等,可以帮助检测内存错误。
  3. 确保一致的初始化

    • 确保所有变量在使用前都已正确初始化,避免依赖Debug模式下的默认初始化行为。
  4. 检查内存分配和释放

    • 检查动态内存分配和释放是否正确,确保没有内存泄漏或双重释放的问题。
  5. 审查多线程代码

    • 如果程序涉及多线程,确保线程同步机制正确无误,避免竞争条件和死锁。
  6. 对比宏定义

    • 检查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中的定义:

    在这里插入图片描述

  • 解决办法:

    1. 在编译时加上选项-fsigned-char
    2. 不使用char,改成使用int8_t或者qint8;

5 linux下找不到动态库

  1. 使用ldd命令查看可执行程序或者动态库的链接路径,是否找得到动态库;

  2. 使用下面命令查看、修改动态库链接路径

    patchelf --set-rpath '$ORIGIN/lib/' ./RadarServer     # 设置程序动态库链接路径
    patchelf --print-rpath ./RadarServer   # 打印链接路径
    



网站公告

今日签到

点亮在社区的每一天
去签到