GCC 编译 / GDB 调试详解

· · 科技·工程

这篇文章是上次闲来无事翻 CCF 大纲看到这个:

[5] gdb 调试器的使用

具体是啥我也忘了,就是 gdb 居然是五级,机房一堆人甚至只会用上古神器 Dev-C++,所以就有了此文。

准备

下载和安装

:::info[如果你用 Linux]{open} Linux 用户可以跳过本章了,包管理器是个好东西。

# Debian 系(Debian/(L/K/X)Ubuntu/Deepin/UOS/etc.)
apt install gcc gdb
# Arch 系(Arch/Manjaro)
pacman -S base-devel

:::

众所周知,GNU 工具链大多数都是为 Linux 设计的,所以你直接去百度 GCC下载 估计除了广告啥也搜不出来。

Windows 下的 GCC 作为工具包提供,主要有 MinGWTDM-GCCMSYS2

MinGW

打开 MinGW Sourceforge 页面 下载 MinGW,你应该可以在 Files 里面看到好几个选项,有一个 WebDL 版本和离线安装器版。

如果你的网络环境比较好,可以下载 WebDL,下载好后打开文件,等待一会,就可以打开安装器。

我们需要选择 mingw32-gcc-g++mingw32-gcc-gdb 这两个包,点击后选择 Mark as Install,选好后点击 Installation -> Apply Changes,等待一会大概半个小时,你会发现装好了。

如果你的网络没法一直稳定上 Sourceforge,你可以下载离线版本,然后找个下载管理器一直挂着,总有一天会下完的,这会比 webdl 稳定很多。

TDM-GCC

如果你的网络没那么好,并且你不想要自动更新和类 Linux 环境,你可以使用 TDM-GCC

打开 TDM-GCC 官网,同理,如果对自己网络有信心的可以去下 MinGW可以下载第一个在线版本,不过这个是 GitHub,比上面那个还要难下一点,建议下离线版,一般使用 64-bit 的版本。

下载好之后,打开安装器,取消勾选下面的“从 GitHub 仓库同步”的选项,点击 Create,等待装好就可以了。

MSYS2

打开 MSYS2 官网 找到 Installation -> Download Installer,下载安装包(又是 GitHub)。

安装过程没啥要注意的,就一路 Next 就行。

安装好后在开始菜单打开 MSYS2 MSYS,就可以看到收悉的 Linux 终端。

JohnCh@PC_JohnCh MSYS ~
$ 

首先更新系统(MSYS 使用 pacman,用法参见我的 Arch 系列文章):

pacman -Syu
# pacman 包管理器,执行 [S]earch,S[y]nchorize,[U]pdate

安装工具包:

pacman -S --needed base-devel mingw-w64-x86_64-toolchain
# 安装两个包,--needed:如果已安装则跳过

现在关闭 MSYS2 就安装好了。

配置环境变量

打开 设置 -> 系统 -> 关于 -> 高级系统设置,点击 环境变量(N)...,在 系统变量 里找到 Path,点击 编辑 -> 添加,粘贴安装目录。

关闭这些窗口,注销后重新登录,打开 cmd,输入:

gcc --version

如果看到版本号,就说明装好了。

GCC:编译 C/C++ 程序

单文件编译

假设我们现在的目录结构是这样的:

- MyCode
|-- example.cpp

如果我们想要编译这个 C++ 文件,可以在命令行使用:

gcc example.cpp -o example.exe

-o 参数指定输出文件名,输出为 example.exe,这个参数在 Linux 并不是必要的,但是因为 Windows 下只有 exe 文件才能运行,所以需要加上。

然后执行 example.cpp 就可以运行代码了。

注意,这里编译的程序不会自带 pause,如果你不想使用程序内 pause,你可以使用这条命令运行:

./example.exe && timeout -1

编译优化和链接

GCC 提供了很多优化级别:

比赛时一般使用 O2,这可以在副作用较少的情况下有效拉平评测机差距。

g++ example.cpp -o example.exe -O2

注意:有优化时的 GDB 调试行为很怪异,请勿在打开优化时调试。

-static:启用静态链接,把依赖库打包到文件里,这可以使你的程序在没有运行时库的系统中运行,但会显著增大文件大小。

g++ example.cpp -o example.exe -static
g++ dynamic.cpp -o dynamic.exe
# 假设这里有一个远程机器 192.168.114.114(\\remote)
copy example.exe \\remote\share\example.exe
copy dynamic.exe \\remote\share\dynamic.exe
ssh 192.168.114.114
# 远程机器,没有安装 GCC/MSVC 之类
D:\share\example.exe
# 会发现可以正常运行
D:\share\dynamic.exe
# Error!

标准

C++ 的版本几年就会更新一次,常见的有 C++98,C++11,C++14,C++17,C++20,C++23 等。

新的 C++ 版本会推出新版的特性,比如 Range Based Forfor (int i : array)) 就是在 C++11 引进的。

还有这些是 C++11/14 的新特性:

使用 -std=c++xx 指定 C++ 标准。

警告和 Sanitizer

有时候我们会不经意间写出一些不会报错的 Bug 和 UB(Undefined Behavior 未定义行为),这时候需要用到警告和 Sanitizer。

-Wall 会打开常见的警告信息(注意:不是全部),包含这些:

比如这样:

// wrong.cpp
#include <iostream>
using namespace std;

int main()
{
    int x;
    cout << x << endl;  // x 未初始化
    return 0;
}

此时这样编译:

g++ wrong.cpp -o a.exe -Wall

就会报错:

wrong.cpp:8:10: warning: 'x' is used uninitialized in this function.
  8 |     cout << x << endl;

-Wextra 包含更多的警告信息,比如:

-fsaniziter 可以用来检查未定义行为,比如 -fsanitizer=address 会在地址漂移或指针越界时给出警告。

这些警告信息可以在编译阶段帮助我们发现代码的问题,建议平常开着 -Wall -fsanitizer=address,undefined

调试信息和宏

-g 可以用来生成调试信息,这是 GDB 调试所必须的。

-DSOMETHING 会添加一个名为 SOMETHING 的宏,可以通过 #if(n)def 来识别。

比如这段代码可以有效地防止考场上忘开 freopen 见祖宗:

#ifndef DEBUG // 如果没有定义 DEBUG(评测机上)
freopen("fileio.in", "r", stdin);
freopen("fileio.out", "w", stdout);
#endif // DEBUG

GCC 的东西就讲到这里,下面开始 GDB。

GDB:调试代码,发现问题

链接符号

在调试之前,我们需要先使用 -g 来编译一次程序:

g++ debug.cpp -g -o debug.exe

然后输入 gdb ,你可以看到这个:

:::info[提示符]

GNU gdb (GDB) 7.8.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-w64-mingw32".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)

:::

首先,如果你不在当前代码目录运行 GDB,你可以 cd 到代码目录中。

使用 file debug.exe 命令将 debug.exe 加载。

(gdb) file debug.exe
Reading symbols from debug.exe...done.

添加断点和查看

在程序执行到 断点 时会暂停,并且给予我们控制台来调试代码。

使用 break x 来添加第 x 行为断点(注意是数字,用了 \LaTeX),使用 disable 禁用断点,enable 启用,delete 删除断点。

使用 display 添加查看,查看的变量每次更改会自动显示。

开始调试

输入 run 开始调试,运行到断点时会弹出提示符。

可以使用 print x 来查看 x 的值,输入 next 执行一步,输入 continue 执行到下一个断点。

如果你找到了错误的来源,可以 stop 停止调试,然后关闭 GDB,修改后重新编译,重新 file 加载文件开始。

其他命令

假设这一段代码:

#include <bits/stdc++.h>
using namespace std;
int f(int n)
{
    if (n == 0) return 1; // breakpoint
    return f(n - 1) * n;
}

int main()
{
    int n;
    scanf("%d",&n);
    printf("%d\n",f(n));
    return 0;
}

Backtrace 查看调用栈

执行到一个断点后,输入 backtrace,可以看到:

(gdb) bt
#0  f (n=3) at example.cpp:6
#1  0x000055555555519e in f (n=4) at example.cpp:6
#2  0x000055555555519e in f (n=5) at example.cpp:6
#3  0x000055555555519e in f (n=6) at example.cpp:6
#4  0x000055555555519e in f (n=7) at example.cpp:6
#5  0x000055555555519e in f (n=8) at example.cpp:6
#6  0x000055555555519e in f (n=9) at example.cpp:6
#7  0x000055555555519e in f (n=10) at example.cpp:6
#8  0x00005555555551dd in main () at example.cpp:12

可以看到,backtrace 显示了每一层的函数参数,便于调试 DFS 等树形调用的代码。

可以使用 up, down, frame 等浏览调用详细信息。

(gdb) up
#1  0x000055555555519e in f (n=4) at example.cpp:6
6           return f(n-1)*n;
(gdb) down
#0  f (n=3) at example.cpp:6
6           return f(n-1)*n;
(gdb) frame 5 // 前面bt输出的结果中,第一项是序号,frame 切换到对应序号的帧
#5  0x000055555555519e in f (n=8) at example.cpp:6
6           return f(n-1)*n;
(gdb) 

Ignore 忽略指定次数的断点击中

ignore #x num:击中 x 号断点前 num 次时不停止。

比如:

(gdb) b 6
Breakpoint 1 at 0x1191: file example.cpp, line 6.
(gdb) ig 1 4
Will ignore next 4 crossings of breakpoint 1.
(gdb) r
10
Breakpoint 1, f (n=6) at example.cpp:6
6           return f(n-1)*n;
# 前面4次经过断点,分别为f(10)、f(9)、f(8)、f(7),都跳过了
# 因此f(6)才触发

Condition 为断点设置条件

condition #x c:击中 x 号断点时判断,只有 c 满足才停下。

(gdb) b 6
Breakpoint 1 at 0x1191: file example.cpp, line 6.
(gdb) cond 1 n==5
(gdb) r
10

Breakpoint 1, f (n=5) at example.cpp:6
6           return f(n-1)*n;

Save/Source 保存断点到文件

save breakpoints FILE 保存断点到 FILE

source FILEFILE 加载数据。

(gdb) b 5
Breakpoint 1 at 0x1184: file example.cpp, line 5.
(gdb) b 6
Breakpoint 2 at 0x1191: file example.cpp, line 6.
(gdb) b 7
Breakpoint 3 at 0x11a2: file example.cpp, line 7.
(gdb) save breakpoints 123 //保存断点信息
Saved to file '123'.
(gdb) quit
# ---------------------关闭 GDB------------------
> gdb
(gdb) source 123 // 从这个文件中引入断点信息
Breakpoint 1 at 0x1184: file example.cpp, line 5.
Breakpoint 2 at 0x1191: file example.cpp, line 6.
Breakpoint 3 at 0x11a2: file example.cpp, line 7.
(gdb) 

Call 调用函数

call func:调用函数。

比如:call min(a, b) 调用 min,计算 a 和 b 的最小值

Checkpoint/Restart 在调试中保存状态

checkpoint:新建一个存档点,保存所有状态。

restart x:读取第 x 号存档,执行之前的所有操作。

End Of File

大概就是这么多了,如果需要补充请评论,我有时间会统一补充