C++(Qt)软件调试---GCC编译参数学习-程序检测(13) 原创

C++(Qt)软件调试—GCC编译参数学习-程序检测(13)


更多精彩内容
👉个人内容分类汇总 👈
👉C++软件调试、异常定位 👈

1、前言

1.1 概述

  • 在前面学习了C++常用编译器(MSVC、GCC、MinGW)的一些常用的编译器参数,主要是用于【预处理】、【编译】、【优化】、【调试】等方面的选项/参数,有助于我们优化程序性能或者调试软件bug;
  • 而我们常说的编译器其实并不是一个软件,而是一套强大的编译器工具集。
  • 在本章内会学习GCC编译器的【警告选项】、【程序检测选项】,从静态、动态两方面检测、调试我们的程序,提高软件质量,减少bug数量。

1.2 测试环境

g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0

2、GCC编译警告选项

1.1 编译警告的作用

编译器警告是指在编译源代码时,编译器检测到可能存在问题或潜在错误时发出的警告信息(不会影响程序编译)。

这些警告信息可以帮助开发人员识别潜在的bug、不良的编程习惯以及代码中的一些不规范之处,从而帮助开发人员改善代码质量。

  • 潜在错误检测: 编译器警告能够识别一些可能导致程序运行错误的代码模式,例如未初始化的变量、不兼容的数据类型转换等。
  • 代码质量提升: 警告信息可以帮助开发人员识别代码中的一些不良习惯,如未使用的变量、无用的代码、冗余的操作等,从而改善代码的可维护性和可读性。
  • 可移植性增强: 编译器警告还可以帮助开发人员遵循语言规范和标准,以确保代码在不同的编译器和平台上都能正确地编译和执行。

所以:在开发中还是不要忽略警告信息。

1.2 GCC常用的编译警告选项

  1. -w:禁止所有警告消息。

  2. -Wall: 启用所有常见的警告信息,需要注意的是,虽然-Wall选项会开启很多常见的警告,但并不一定涵盖所有可能的问题。有时候,特定项目的要求可能需要更严格的警告设置或者其他编译选项的结合使用。

  3. -Wextra: 启用一组额外的警告检查,这些警告通常不会被启用,即使使用了-Wall选项(开启大部分常见的警告)。这个选项允许编译器检查更多的代码模式,以帮助开发人员捕获更多潜在问题。。

  4. -Werror: 将警告视为错误,编译时如果有警告会导致编译失败

  5. -Wpedantic: 在编译C和C++代码时启用严格的警告,以便检测出与语言标准不一致的代码。这个选项会提示可能违反C和C++标准的代码,以帮助开发者编写更加规范和可移植的代码。

  6. -Wunused: 报告未使用的变量、参数等警告;

    • 由很多子项组成,部分子项包含在-Wall -Wextra中了。
    • 如果要检测函数参数未使用,需要单独指定-Wextra或者-Wunused-parameter
    • 对于全局变量、成员变量未使用检测不到。
    #include <iostream>
    
    using namespace std;
    
    class Test{
    
    public:
        Test()
        {
        }
        int m_c;
    private:
        int m_a;
        int* m_b;
    };
    
    int g_b;
    
    void fun(int v)
    {
        cout << "fun" << endl;
    }
    
    int main()
    {
        Test t;
        fun(123);
        int a;
        int b;
        int *c;
    
        return 0;
    }
    
  7. -Wuninitialized: 报告使用了未初始化的变量,但是这个功能基本只对未初始化的局部变量生效,由于全局变量、成员变量的复杂性,检测不是很准确(在-Wall中包含)。

    • 测试时使用纯C++编写测试代码,使用命令行编译会检测到成员编译(但不全),在Qt中检测不到。
    #include <iostream>
    
    using namespace std;
    
    class Test{
    
    public:
        Test()
        {
    	cout << m_a << endl;
            cout << m_b << endl;
        }
    private:
        int m_a;
        int* m_b;
    };
    
    int g_b;
    
    int main()
    {
        Test t;
        int a;
        int b;
        int *c;
        cout << a << endl;
        cout << b << endl;
        cout << c << endl;
        cout << g_b << endl;
        return 0;
    }
    
  8. -Wconversion: 报告隐式类型转换警告,如果用到库,则可能会出现很多警告,例如Qt开发。

    • 隐式类型转换可能导致数据截断或溢出、精度损失、不正确的值、内存访问问题等后果;
    double d = 10;
    int a = d;                        // double类型转int 
    uint ui = 10;
    
    if(ui > a)                       // 无符号和有符号比较会隐式类型转换
    {
        qDebug() << "int uint";
    }
    
  9. -Wformat: 检查printfscanf格式字符串的问题(在-Wall中包含)。

  10. -Wshadow: 用来检测变量名的遮蔽(shadowing)问题的,遮蔽问题指的是在嵌套作用域中,内部作用域的变量名与外部作用域的变量名相同,可能导致程序逻辑错误或混淆。

    int a = 10;
    if(true)
    {
        int a = 20;
    }
    
  11. -Wcast-qual: 报告指针类型转换时丢失了限定符(const、volatile)的警告。例如:

    const char* a = new char[10];
    char* b= (char*)a;
    
  • 演示

在这里插入图片描述

3、GCC程序检测选项

1.1 性能分析选项(-pg)

软件的性能是软件质量的重要考察点,不论是在线服务程序还是离线程序,甚至是终端应用,性能都是用户体验的关键。再好的程序、在牛掰的功能算法如果性能不满足要求,占用资源过高也是失败的。

  • -p / -pg: gcc 中的一个编译选项,用于在生成的可执行文件中插入性能分析代码,以便进行性能分析和 profiling(性能剖析)。以帮助开发人员了解程序的运行时间分布、函数调用关系以及性能瓶颈,从而指导优化工作。需要注意的是,由于插入了额外的代码,使用 -pg 选项可能会稍微影响程序的执行速度,因此在进行性能分析时需要权衡是否使用该选项。

使用方式

  1. 在程序编译链接时使用-pg选项,会在程序中插入性能分析代码,编译后的程序会大一些;
    • g++ -pg main.cpp
  2. 编译后的程序会在运行时收集函数调用信息和计时数据。运行程序后,会生成 gmon.out 文件,其中包含了运行时收集的数据;(注意,没执行到的函数不会进行分析)
  3. 使用gprof命令来分析记录程序运行信息的gmon.out文件。
    • gprof [options] a.out gmon.out > outfile 将分析结果输出到outfile文件中;
    • [options]为gprof的命令选项,可省略,可通过gprof -h命令查看可用参数;
    • 其中包含了函数调用图、函数执行时间、调用次数等信息。

gprof说明

  • 一种用于分析程序性能的工具,它能够生成函数级别的性能分析报告,帮助开发人员找出程序中的瓶颈和性能问题。gprof 通常与 GNU 编译器集合中的 gcc 配合使用,它通过在程序中插入一些特殊的代码来收集运行时的函数调用信息,然后生成性能分析报告。

  • gprof官方文档

  • 示例

    在这里插入图片描述

1.2 运行检测(-fsanitize)

GCC的-fsanitize选项用于在编译的程序运行时进行检测,主要有下列功能:

1.1.1 fsanitize=address
  • -fsanitize=address:使用 AddressSanitizer(ASan)C/C++的内存错误检测器,它可以检测内存访问错误,如堆栈缓冲区溢出、堆区越界等;编译指令:g++ -g -fsanitize=address main.cpp

    • GCC版本:4.8+
    • AddressSanitizer(ASan)是一种用于检测和调试内存错误的工具。它是由Google开发的,并内置于GNU编译器套件(GCC)和LLVM编译器中。
  • 注意:如果编译时不加-g,则在显示的信息中可能无法定位到具体位置。

    1. 释放后使用(野指针)

      #include <iostream>
      
      using namespace std;
      
      int main()
      {
          int* arr = new int[10];
          delete[] arr;
       //   arr[1] = 123;                   // 写入错误可检测
          cout << arr[1] << endl;           // 读取错误可检测
          return 0;
      }
      

      在这里插入图片描述

    2. 堆缓冲区溢出

      #include <iostream>
      
      using namespace std;
      
      int main()
      {
          int* arr = new int[10];
          arr[1] = 123;
          cout << arr[10] << endl;
          delete[] arr;
          return 0;
      }
      
      

      在这里插入图片描述

    3. 栈缓冲区溢出

      #include <iostream>
      
      using namespace std;
      
      int main()
      {
          int arr[10];
          arr[1] = 123;
          cout << arr[10] << endl;
          
          return 0;
      }
      

      在这里插入图片描述

    4. 全局缓冲区溢出

      #include <iostream>
      
      using namespace std;
      
      int g_arr[10] = {0};
      int main()
      {
          g_arr[1] = 123;
          cout << g_arr[10] << endl;
          
          return 0;
      }
      

      在这里插入图片描述

    5. 返回局部堆区地址后使用(经过测试没检测出来)

      #include <iostream>
      
      using namespace std;
      
      int* g_p = nullptr;
      void fun()
      {
          int arr[10];
          g_p = arr;
      }
      int main()
      {
          fun();
          cout << g_p << endl;
          cout << g_p[1] << endl;
          
          return 0;
      }
      
    6. 作用域外使用栈内存

      #include <iostream>
      
      using namespace std;
      
      int main()
      {
          int* p = nullptr;
          {
      	    int a = 10;        // 离开大括号作用域后就被释放了
      	    p = &a;
          }
          *p = 20;               // 使用不在作用域的栈内存
          cout << *p << endl;
          return 0;
      }
      

      在这里插入图片描述

    7. 初始化顺序错误(经过测试未检测出来)

      // 1.cpp
      #include <iostream>
      using namespace std;
      
      extern int extern_global;
      int __attribute__((noinline)) read_extern_global() 
      {
        return extern_global;
      }
      int x = read_extern_global() + 1;
      
      int main() 
      {
        cout << x << endl;
        return 0;
      }
      
      // 2.cpp
      int foo() 
      { 
          return 42; 
      }
      int extern_global = foo();
      
      • 由于编译时指定1.cpp和2.cpp的顺序不同会导致初始化顺序不同,得到的输出结果也会不同;

      在这里插入图片描述

    8. 内存泄漏

      #include <iostream>
      
      using namespace std;
      
      
      void fun()
      {
          int* a = new int();
      }
      
      int main()
      {
          fun();
          return 0;
      }
      

      在这里插入图片描述

1.1.2 fsanitize=thread
  • -fsanitize=thread:使用 ThreadSanitizer(TSan)工具,它可以检测多线程程序中的数据竞争和其他线程相关的错误。数据争用是并发系统中最常见和最难调试的错误类型之一。当两个线程同时访问同一变量并且至少有一个访问是写入时,就会发生数据争用。

    • 注意:不能和-fsanitize=address -fsanitize=leak 一起使用。
    #include <iostream>
    #include <thread>
    using namespace std;
    
    int g_a = 0;
    
    // 在线程中修改全局变量
    void fun1() 
    {
        for(int i = 0; i < 10; i++)
        {
            g_a++;
    	cout << g_a << endl;
        }
    }
    
    int main() 
    {
        // 创建两个线程
        thread t1(fun1);
        thread t2(fun1);
    
        // 等待新线程结束
        t1.join(); 
        t2.join();
    
        return 0;
    }
    
    
    • 编译:g++ -g -fsanitize=thread -lpthread main.cpp
    • 可能会出现报错:/usr/bin/ld: 找不到 libtsan_preinit.o: 没有那个文件或目录

    在这里插入图片描述

1.1.3 fsanitize=leak
  • -fsanitize=leak:使用 LeakSanitizer(LSan)工具,它可以检测程序中的内存泄漏。

    #include <iostream>
    
    using namespace std;
    
    
    void fun()
    {
        int* a = new int();
    }
    
    int main()
    {
        fun();
        return 0;
    }
    

    在这里插入图片描述

1.1.4 fsanitize=undefined
  • -fsanitize=undefined:使用 UndefinedBehaviorSanitizer(UBSan)工具,它可以检测代码中的未定义行为,如除以零、空指针解引用等。

    • 由于未定义行为很多,这里就例举了3种进行演示;
    #include <iostream>
    #include <limits.h>
    using namespace std;
    
    int main()
    {
        int arr[10];
        cout << arr[-1] << endl;     // 数组越界
    
        int a = INT_MAX;
        unsigned int b = a + 1;      // 溢出
    
        int c = 0;
        int d = a / c;              // 除0
        return 0;
    }
    
    

    在这里插入图片描述

4、总结

  • GCC编译选项就是非常非常的多,功能非常强大,这里只是介绍了常用的一些功能选项,值得花一些时间去研究一下;
  • 我们软件开发并不只是单纯的编写代码哦。
posted @ 2023-09-14 21:39  mahuifa  阅读(0)  评论(0编辑  收藏  举报  来源