C++(Qt)软件调试---qmake编译优化和生成调试信息(9) 原创
C++(Qt)软件调试—qmake编译优化和生成调试信息(9)
更多精彩内容 |
---|
👉个人内容分类汇总 👈 |
👉C++软件调试、异常定位 👈 |
👉PDF版本下载 👈 |
1、前言
1.1 编译器优化是什么
编译器编译优化是指编译器在编译源代码为目标代码的过程中,通过对代码结构和语义的分析,自动优化目标代码的生成方式,编译器编译优化可以提高程序的性能和可靠性,但也可能会对程序的可读性和可维护性产生负面影响。因此,开发者需要在代码调试和维护的过程中进行权衡和选择,以获得最佳的程序性能和开发效率。
编译优化主要包括以下几个方面:
- 算法优化:通过优化算法的复杂度,减少程序执行的时间和空间消耗。
- 代码优化:通过优化代码的结构、指令的选择和数据存储方式等,减少程序执行的时间和空间消耗。
- 循环优化:通过分析循环结构,优化循环的执行过程,减少循环的执行时间和空间消耗。
- 内联函数优化:通过将函数的代码直接嵌入到调用该函数的代码中,减少函数调用的开销,提高程序的执行效率。
- 代码生成优化:通过对目标代码的生成方式进行优化,减少目标代码的大小和执行时间。
1.2 调试信息是什么
C++调试信息是在编译C++程序时生成的一些附加信息,它包括了程序中各个变量、函数以及类的定义和使用等详细信息。
调试信息可以帮助开发人员在程序运行时快速定位和解决问题,特别是在出现崩溃、错误或异常情况时,可以帮助开发人员追踪到具体的代码行数和错误原因。
调试信息可以通过编译器选项来开启和关闭,通常在开发和测试阶段开启,而在正式发布时关闭。
1.3 测试环境
系统环境:ubuntu22.04、Windows10;
编译器:g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0、MinGW7.3.0 64-bit、MSVC2017-64;
2、Qt编译模式说明
Qt是一个跨平台的GUI应用程序框架,支持C++编程语言。Qt提供了三种不同的构建模式:Debug、Profile和Release,每种模式都有不同的用途和优化级别。
有的Bug只有在Release模式下才能存在或者只有在Debug模式下高性能运行才能出现,这时就需要我们通过修改编译器选项来进行调试。
- Debug模式
Debug模式是开发阶段使用的模式,旨在提供最好的调试工具支持。
在Debug模式下,编译器会生成完整的符号表和调试信息,以便在程序崩溃或出现错误时,开发人员可以轻松地跟踪和调试代码。
此外,Debug模式还会关闭代码优化,以确保程序的行为与代码的预期行为一致。
因此,Debug模式下生成的可执行文件通常比其他模式下的可执行文件更大。
使用MSVC编译器时会生成符号表PDB文件;
使用GCC、MinGW编译器时会将调试信息全部写入可执行程序中,所以可执行程序比较大。
- Profile模式
Profile模式是用于性能分析的模式。(相当于Release模式下增加调试信息,使用的也是Release的库)
在Profile模式下,编译器会生成符号表和调试信息,但会开启一些代码优化,以提高程序的性能。
此外,编译器还会为程序生成额外的性能分析信息,以便开发人员可以查看程序的性能瓶颈和优化程序。
因此,Profile模式下生成的可执行文件比Debug模式下的文件稍小。
使用MSVC编译器时会生成符号表PDB文件;
使用GCC、MinGW编译器时会将调试信息写到一个
.debug
文件中,对可执行程序影响不大。
- Release模式
Release模式是用于发布的模式。
在Release模式下,编译器会进行更加严格的代码优化,以提高程序的性能和减小程序的大小。
此外,编译器还会删除符号表和调试信息,以减小可执行文件的大小。
因此,Release模式下生成的可执行文件比Debug和Profile模式下的文件更小,但不适合调试。
总的来说,Debug、Profile和Release模式都有各自的优点和缺点。
在开发过程中,建议使用Debug模式进行调试,以便更容易地查找和修复错误。
在发布之前,建议使用Profile模式进行性能分析和优化,并使用Release模式发布程序以提高程序的性能和减小程序的大小。
3、比较Linux下Qt三种编译模式
1.1 编译生成文件比较
-
如下图所示:
- Debug模式编译生成的可执行程序非常大;
- Release模式生成的可执行程序很小,几乎只有Debug模式的1/30;
- Profile模式生成的可执行程序和Release模式的差不多大,但是额外生成了一个
.debug
文件,包含了调试信息。
1.2 编译器配置比较
-
在Linux下Qt编译后会将QMake的pro文件转换为make使用的Makefile文件,可以通过对比Debug、Profile、Release三种模式编译生成的Makefile文件,了解这三个模式干了啥,有什么区别;
-
打开一个生成的Makefile文件,下图中框选部分就是编译器、选项的配置所在;
-
下图中比较了Debug模式和Release模式的区别,左侧为Debug模式,右侧为Release模式,主要对比CFLAGS、CXXFLAGS、LFLAGS这三个变量;
- Debug模式:
- CFLAGS、CXXFLAGS:使用
-g
参数生成调试信息,默认为-g2
,没有指定优化参数-O
则默认关闭优化。
- CFLAGS、CXXFLAGS:使用
- Release模式:
- CFLAGS、CXXFLAGS:使用了
-O2
参数开启了编译优化,没有使用-g
参数则默认不生成调试信息; - LFLAGS:增加了
-O1
参数表示链接器开启了1级别编译优化。
- CFLAGS、CXXFLAGS:使用了
- Debug模式:
-
下图中比较了Debug模式和Profile模式的区别,左侧为Debug模式,右侧为Profile模式,主要对比CFLAGS、CXXFLAGS、LFLAGS这三个变量;
- CFLAGS、CXXFLAGS:Profile模式在Debug模式的基础上使用了
-O2
参数开启了编译优化,同时使用-g
参数生成调试信息;
- CFLAGS、CXXFLAGS:Profile模式在Debug模式的基础上使用了
-
下图中比较了Release模式和Profile模式的区别,左侧为Profile模式,右侧为Release模式,主要对比CFLAGS、CXXFLAGS、LFLAGS这三个变量;
- CFLAGS、CXXFLAGS:Release和Profile都使用了
-O2
参数开启了编译优化,Profile增加了使用-g
参数生成调试信息; - LFLAGS:相对于Release,Profile没有使用
-O1
参数,关闭了链接器优化。
- CFLAGS、CXXFLAGS:Release和Profile都使用了
1.3 调试效果比较
-
新建一个Qt工程,在程序中添加下面测试代码;
inline void test() { int* a = nullptr; *a = 10; } void Widget::on_pushButton_clicked() { test(); }
-
切换为Release模式,点击调试运行;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HVHauCcp-1681024688399)(C++(Qt)]软件调试—Qt编译优化和生成调试信息(9).assets/image-20230409101100262.png)
-
程序运行起来后点击触发测试代码的按键,如下图所示,可以看出:
- 错误类型:段错误;
- 异常位置:Widget::on_pushButton_clicked() 0x555555557714
-
切换为Profile模式,点击调试运行,程序运行起来后点击触发测试代码的按键,如下图所示,可以看出:
- 错误类型:段错误;
- 异常所在文件:widget.cpp;
- 异常所在函数:void Widget::on_pushButton_clicked();
- 异常所在行号:25行;
- 并且直接在源码中定位到异常所在行号,可以看出定位的异常位置是调用test()函数的位置,而不是准确的异常位置。
-
切换为Debug模式,点击调试运行,程序运行起来后点击触发测试代码的按键,如下图所示,可以看出:
- 错误类型:段错误;
- 异常所在文件:widget.cpp;
- 异常所在函数:inline void test();
- 异常所在行号:20行;
- 并且直接在源码中定位到异常所在行号,可以看出定位的异常位置就是实际错误的位置,没有出现偏差。
1.4 GDB调试Profile程序
-
由于Qt在Profile模式编译时将调试信息额外写入到了
.debug
文件,所以在使用GDB调试时需要指定加载.debug
文件才可以调试程序; -
以下是使用GDB调试程序时如何使用.debug文件的详细说明:
-
确保可执行文件和.debug文件都在同一目录中。
-
启动GDB并加载可执行文件:
gdb test
-
GDB加载.debug文件:
symbol-file test.debug
-
开始运行程序:
run
-
4、比较Windows下Qt - Mingw三种编译模式
1.1 编译生成文件比较
-
如下图所示:
- Debug模式编译生成的可执行程序非常大;
- Release模式生成的可执行程序很小,几乎只有Debug模式的1/43;
- Profile模式生成的可执行程序是Release模式的差不多2.7被,还额外生成了一个
.debug
文件,包含了调试信息。 - 从比较结果看明显和linux下不一样,虽然说Mingw也是GCC,但是Qt使用Mingw和GCC肯定还是有一些区别。
1.2 编译器配置比较
-
在Windows下打开编译生成的build*文件夹会发现,和Linux下有很大区别,如下图所示,多了两个release、debug文件夹,又多了Makefile.Release、Makefile.Debug文件,到底是使用Makefile.Release、Makefile.Debug还是Makefile文件呢?
-
打开Makefile文件会发现没有编译器配置项;
-
其实编译器配置项都在Makefile.Release和Makefile.Debug中,那使用哪一个呢,其实这里有一个判断的小技巧,打开debug和release文件夹,哪个文件夹里有编译的中间文件就使用哪一个,一般情况下Debug模式编译就会在debug文件夹下生成中间文件,使用的就是Makefile.Debug文件,而Release模式下编译就会在release文件夹下生成编译的中间文件,使用Makefile.Release,那Profile模式呢,打开build-untitled5-Desktop_Qt_5_12_5_MinGW_64_bit-Release文件夹,在里面的release文件夹中发现有编译生成的中间文件,就说明使用的是Makefile.Release文件进行编译的。
-
下图中比较了Debug模式和Release模式的区别,左侧为Debug模式,右侧为Release模式,主要对比CFLAGS、CXXFLAGS、LFLAGS这三个变量;
- Debug模式:
- CFLAGS、CXXFLAGS:使用
-g
参数生成调试信息,默认为-g2
,没有指定优化参数-O
则默认关闭优化。
- CFLAGS、CXXFLAGS:使用
- Release模式:
- CFLAGS、CXXFLAGS:使用了
-O2
参数开启了编译优化,没有使用-g
参数则默认不生成调试信息; - LFLAGS:增加了
-s
参数从可执行文件中删除所有符号表和重新定位信息。(这里和Linux下不一样)
- CFLAGS、CXXFLAGS:使用了
- Debug模式:
-
下图中比较了Debug模式和Profile模式的区别,左侧为Debug模式,右侧为Profile模式,主要对比CFLAGS、CXXFLAGS、LFLAGS这三个变量;
- CFLAGS、CXXFLAGS:Profile模式在Debug模式的基础上使用了
-O2
参数开启了编译优化,同时使用-g
参数生成调试信息;
- CFLAGS、CXXFLAGS:Profile模式在Debug模式的基础上使用了
-
下图中比较了Release模式和Profile模式的区别,左侧为Profile模式,右侧为Release模式,主要对比CFLAGS、CXXFLAGS、LFLAGS这三个变量;
- CFLAGS、CXXFLAGS:Release和Profile都使用了
-O2
参数开启了编译优化,Profile增加了使用-g
参数生成调试信息; - LFLAGS:相对于Release,Profile没有使用
-s
参数从可执行文件中删除所有符号表和重新定位信息。
- CFLAGS、CXXFLAGS:Release和Profile都使用了
1.3 调试效果比较
-
使用相同的测试代码。
-
切换为Release模式,点击调试运行,程序运行起来后点击触发测试代码的按键,如下图所示,可以看出:
- 错误类型:段错误;
- 异常位置:0x401720
- 由于Release模式编译使用了
-s
参数,所以异常位置除了地址什么都看不到。
-
切换为Profile模式,点击调试运行,程序运行起来后点击触发测试代码的按键,如下图所示,可以看出:
- 错误类型:段错误;
- 异常所在文件:widget.cpp;
- 异常所在函数:void Widget::on_pushButton_clicked();
- 异常所在行号:25行;
- 并且直接在源码中定位到异常所在行号,可以看出定位的异常位置是调用test()函数的位置,而不是准确的异常位置,这时因为Profile使用了
-O2
参数优化编译,导致内联函数被展开了。
-
切换为Debug模式,点击调试运行,程序运行起来后点击触发测试代码的按键,如下图所示,可以看出:
- 错误类型:段错误;
- 异常所在文件:widget.cpp;
- 异常所在函数:inline void test();
- 异常所在行号:20行;
- 成功定位到源码中使用空指针的一行。
5、比较Windows下Qt - MSVC三种编译模式
1.1 编译生成文件比较
-
如下图所示:
- Debug模式编译产生了三个文件,exe、ilk、pdb,生成的可执行程序只是比Release模式编译的可执行程序大两倍多一点,;
- Release模式编译只生成了一个exe,生成的可执行程序很小比较小,只有29k;
- Profile模式编译生成了两个文件,exe和pdb,生成的可执行程序和Release模式的一样大。
1.2 编译器配置比较
-
下图中比较了Debug模式和Release模式的区别,左侧为Debug模式,右侧为Release模式,主要对比CFLAGS、CXXFLAGS、LFLAGS这三个变量;
- Debug模式:
- CFLAGS、CXXFLAGS:使用
-Zi
参数生成调试信息,没有指定优化参数-O
则默认关闭优化,-MDd
是指定-MD
的Debug库。 - LFLAGS:使用了
/DEBUG
参数,将链接对象和库文件中的调试信息放入程序数据库 (PDB) 文件。 它会在程序的后续生成期间更新 PDB(使用了这个参数后-Zi
才有效),指定/DEBUG
时隐式使用/INCREMENTAL
。
- CFLAGS、CXXFLAGS:使用
- Release模式:
- CFLAGS、CXXFLAGS:使用了
-O2
参数开启了编译优化,没有使用-Zi
参数则默认不生成调试信息; - LFLAGS:使用了
/INCREMENTAL:NO
参数,未选择增量链接,不会生成.ilk
文件。
- CFLAGS、CXXFLAGS:使用了
- Debug模式:
-
下图中比较了Debug模式和Profile模式的区别,左侧为Debug模式,右侧为Profile模式,主要对比CFLAGS、CXXFLAGS、LFLAGS这三个变量;
- CFLAGS、CXXFLAGS:Debug和Profile都使用
-Zi
参数生成调试信息,Profile使用了优化参数-O2
开启编译优化。 - LFLAGS:Debug和Profile都使用了
/DEBUG
参数生成PDB文件,并且Profile中使用了/INCREMENTAL:NO
参数,未选择增量链接,使用了/OPT:REF
参数开启了链接器优化,清除从未引用的函数和数据(称为 COMDAT)。
- CFLAGS、CXXFLAGS:Debug和Profile都使用
-
下图中比较了Profile模式和Release模式的区别,左侧为Profile模式,右侧为Release模式,主要对比CFLAGS、CXXFLAGS、LFLAGS这三个变量;
- CFLAGS、CXXFLAGS:Debug和Profile都使用
-O2
参数开启编译优化,Profile使用了-Zi
参数生成调试信息。 - LFLAGS:相对于Release,Profile使用了
/DEBUG
参数生成PDB文件,使用了/OPT:REF
参数开启了链接器优化,清除从未引用的函数和数据(称为 COMDAT)。
- CFLAGS、CXXFLAGS:Debug和Profile都使用
1.3 调试效果比较
-
使用相同的测试代码。
-
切换为Release模式,点击调试运行,程序运行起来后点击触发测试代码的按键,如下图所示,可以看出:
- 异常代码:0x0000005(表示”访问冲突异常“或者”内存访问异常“);
- 异常信息:写入访问冲突发生在:0x0(基本表示是空指针);
- 异常位置:0x7ff65fbb1222
- 由于Release模式编译开启了编译优化,未生成调试信息,所以异常位置除了地址什么都看不到。
-
切换为Profile模式,点击调试运行,程序运行起来后点击触发测试代码的按键,如下图所示,可以看出:
- 异常代码:0x0000005(表示”访问冲突异常“或者”内存访问异常“);
- 异常信息:写入访问冲突发生在:0x0(基本表示是空指针);
- 异常位置:0x7ff65fbb1222;
- 异常所在文件:widget.cpp;
- 异常所在函数:void Widget::on_pushButton_clicked();
- 异常所在行号:31 (不准确);
- 在两个button的槽函数中都有使用到test()函数,运行时点击的是button1,提示的异常函数名称正确,但是异常行号和在代码中定位的异常位置却是在button2的槽函数中,存在误差。
-
切换为Debug模式,点击调试运行,程序运行起来后点击触发测试代码的按键,如下图所示,可以看出:
- 异常代码:0x0000005(表示”访问冲突异常“或者”内存访问异常“);
- 异常信息:写入访问冲突发生在:0x0(基本表示是空指针);
- 异常位置:0x7ff622694210;
- 异常所在文件:widget.cpp;
- 异常所在函数:void Widget::on_pushButton_clicked();
- 异常所在行号:20;
- 成功定位到源码中使用空指针的一行。
6、Qt编译器链接器配置用到的变量
- C语言编译器使用:
- QMAKE_CC:指定在构建包含C源代码的项目时将使用的C编译器。
- QMAKE_CFLAGS:指定用于构建项目的C编译器标志(选项)。适用于Debug和Release模式,这个变量的值通常由qmake或qmake.conf处理,很少需要修改。
- QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO:为Profile模式指定c编译器标志。(profile就是带调试信息的Release模式)
- QMAKE_CFLAGS_DEBUG:为Debug模式指定C编译器标志。
- QMAKE_CFLAGS_RELEASE:指定Release模式的C编译器标志。
- C++编译器使用:
- QMAKE_CXX:指定在构建包含c++源代码的项目时将使用的c++编译器。
- QMAKE_CXXFLAGS:指定用于构建项目的c++编译器标志(选项)。适用于Debug和Release模式,这个变量的值通常由qmake或qmake.conf处理,很少需要修改。
- QMAKE_CXXFLAGS_RELEASE_WITH_DEBUGINFO:为Profile模式指定c++编译器标志。(profile就是带调试信息的Release模式)
- QMAKE_CXXFLAGS_DEBUG:为Debug模式指定c++编译器标志。
- QMAKE_CXXFLAGS_RELEASE:为Release模式指定c++编译器标志。
- 链接器使用:
- QMAKE_LFLAGS:指定传递给链接器的一组通用标志。如果需要更改用于特定平台或项目类型的标志,请使用专用变量之一而不是此变量。
- QMAKE_LFLAGS_CONSOLE:指定用于生成控制台程序的链接器标志。(仅在 Windows 上使用)
- QMAKE_LFLAGS_DEBUG:指定Debug版本的链接器标志。
- QMAKE_LFLAGS_PLUGIN:指定用于生成插件的链接器标志。
- QMAKE_LFLAGS_RPATH:指定使用来自QMAKE_RPATHDIR的值所需的链接器标志。(仅在 Unix 平台上使用)
- QMAKE_LFLAGS_RELEASE:指定Release版本的链接器标志。
- QMAKE_LFLAGS_APP:指定用于生成应用程序的链接器标志。
- QMAKE_LFLAGS_WINDOWS:指定用于生成 Windows GUI 项目(即非控制台应用程序)的链接器标志。(仅在 Windows 上使用)
7、Qt在Release模式下关闭优化生成调试信息配置
-
将下列代码加入到工程的Pro文件中可以支持在GCC、MinGW、MSVC编译器的Release模式下关闭优化,生成调试信息。
-
注意:下列代码仅使用于调试,会降低程序性能,如果要发布程序最好将下面代码注释掉再编译。
msvc:CONFIG(release, debug|release) { QMAKE_CFLAGS_RELEASE -= -O2 # 取消C优化 QMAKE_CFLAGS_RELEASE += -Zi # 生成调试信息,放到pdb文件中 QMAKE_CXXFLAGS_RELEASE -= -O2 # 取消C++优化 QMAKE_CXXFLAGS_RELEASE += -Zi # 生成调试信息 QMAKE_LFLAGS_RELEASE -= /INCREMENTAL:NO # 选择增量链接 QMAKE_LFLAGS_RELEASE += /DEBUG # 将调试信息放到PDB文件中 message(MSVC编译器Release关闭优化,生成调试信息使用) } mingw:CONFIG(release, debug|release) { QMAKE_CFLAGS_RELEASE -= -O2 # 取消C优化 QMAKE_CFLAGS_RELEASE += -O0 # 显示指定禁止优化 QMAKE_CFLAGS_RELEASE += -g # 生成C调试信息 QMAKE_CXXFLAGS_RELEASE -= -O2 # 取消C++优化 QMAKE_CXXFLAGS_RELEASE += -O0 # 显示指定禁止优化 QMAKE_CXXFLAGS_RELEASE += -g # 生成C++调试信息 QMAKE_LFLAGS_RELEASE -= -Wl,-s # 取消Release模式删除所有符号表和重新定位信息的设置 QMAKE_LFLAGS_RELEASE += -g # 链接器生成调试信息 message(Mingw编译器Release关闭优化,生成调试信息使用) } # 如果不加unix,MinGW也会进入这里 unix:gcc:CONFIG(release, debug|release) { QMAKE_CFLAGS_RELEASE -= -O2 # 取消C优化 QMAKE_CFLAGS_RELEASE += -O0 # 显示指定禁止优化 QMAKE_CFLAGS_RELEASE += -g # 生成C调试信息 QMAKE_CXXFLAGS_RELEASE -= -O2 # 取消C++优化 QMAKE_CXXFLAGS_RELEASE += -O0 # 显示指定禁止优化 QMAKE_CXXFLAGS_RELEASE += -g # 生成C++调试信息 QMAKE_LFLAGS_RELEASE -= -Wl,-O1 # 取消Release模式链接器优化 QMAKE_LFLAGS_RELEASE += -g # 链接器生成调试信息 message(GCC编译器Release关闭优化,生成调试信息使用) }
8、总结
通过查看Qt编译生成的Makefile文件可以学习到Qt内部对不同编译模式做了哪些工作;
Qt默认的三种编译模式基本可以适用于大部分开发调试的场景;
当我们遇见特殊的问题时就需要手动修改编译器选项,在程序编译时选择合适的优化参数和生成调试信息参数,在运行性能、程序大小、调试方便三个方向进行权衡利弊。
例如在不需要考虑性能时可以完全关闭优化,生成尽可能多的调试信息,以方便调试;
而有些程序运行需要一定的性能,就可以选择开启一定较低级别的优化。
文章中所述内容多有不足,欢迎一起交流学习。