基于 Linux 的 GCC 与 GDB 应用程序调试

GNU 的正确发音为[g'noo],名称由英文句子GNU's Not Unix递归缩写组成,是一项由自由软件基金会推动的操作系统计划。GNU 计划最早开始于 1984 年 1 月,目标是完成一个由Hurd内核与一系列应用程序、系统库、开发工具组成的GNU 操作系统。但由于 Hurd 的开发工作迟迟未能完成,因而普遍选择采用 Linux Kernel 作为操作系统的内核,这一套技术组合正是闻名遐迩的 GNU/Linux 操作系统。

GCCGDB 组成的编译套件正是 GNU 计划下诞生的优秀开源项目,也是 GNU/Linux 技术体系不可或缺的构成要素。虽然当前 ClangLLVM 编译套件的发展风头正劲,但是由于嵌入式 Linux 设备通常只提供基于 GCC 的交叉编译工具链,加之两者在使用上差异不大,而 GDB 又同时提供了两者编译后程序的完整 Debug 支持,因而笔者依然选择 GCCGDB 组合来作为本文的撰写的目标。

【GCC】概述

GNU 编译器套件GCCGNU Compiler Collection)最初的目标是作为一款 GNU 操作系统的通用编译器,包含有 C、C++、Objective-C、Objective-C++、Fortran、Ada、Go、BRIG(HSAIL)等语言的前端及其相关的libstdc++libgcj等库,目前已经移植到 Windows、Mac OS X 等商业化操作系统。GCC 编译器套件当中包含了诸多的软件包,主要的软件包如下面表格所示:

名称 描述
cpp C 预处理器。
gcc C 编译器。
g++ C++ 编译器。
gccbug 用于创建 BUG 报告的 Shell 脚本。
gcov 覆盖测试工具,用于分析程序需要优化的位置。
libgcc GCC 运行库。
libstdc++ 标准 C++库。
libsupc++ C++语言支持函数库。

Ubuntu、Mint 等使用 deb 格式软件包的 Linux 发行版通常会默认安装 GCC 编译器,但是由于相关的软件包可能并不完整,因此可以通过如下命令安装完整的 GCC 编译环境。

1
sudo apt-get install build-essential

基本使用

由于当前需要编译的是 C 语言程序,因此需要使用到gcc软件包提供的命令,这些命令的基本使用格式如下:

1
gcc [-options] [filename]

将上一节编写的 Hello World 程序保存至一个main.c源代码文件当中,然后执行gcc编译命令得到可执行的a.out文件:

1
2
3
4
5
6
➜  gcc main.c
ls
a.out main.c

➜ ./a.out
hello world!

如果需要指定输出的可执行文件名称,那么可以添加-o选项:

1
2
3
4
5
6
➜  gcc main.c -o main
ls
main main.c

➜ ./main
hello world!

编译信息

如果需要查看编译的过程,那么可以使用-v命令选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  gcc -v main.c
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.3.0-27ubuntu1~18.04' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04)
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-linux-gnu/7/cc1 -quiet -v -imultiarch x86_64-linux-gnu main.c -quiet -dumpbase main.c -mtune=generic -march=x86-64 -auxbase main -version -fstack-protector-strong -Wformat -Wformat-security -o /tmp/cc9yluga.s
GNU C11 (Ubuntu 7.3.0-27ubuntu1~18.04) version 7.3.0 (x86_64-linux-gnu)
compiled by GNU C version 7.3.0, GMP version 6.1.2, MPFR version 4.0.1, MPC version 1.1.0, isl version isl-0.19-GMP
... ...

优化选项

GCC 编译优化等级由低到高分为-O0-O1-O2-O3o即单词 Optimization 的首字母,不同优化等级下得到的文件体积与执行效率各不相同。此外,嵌入式开发当中还经常使用到一个-Os,其优化等级介于-O2-O3之间。

1
2
3
4
5
➜  gcc -os main.c -o main
➜ ll
总用量 16K
-rwxrwxr-x 1 hank hank 8.2K 2月 24 17:51 main
-rw-rw-r-- 1 hank hank 136 2月 21 18:12 main.c

调试信息

开启优化选项后编译的代码,并不会保留任何关于调试与 debug 的信息,如果需要保留这些信息,可以开启-g选项(gdb),此时得到的文件体积会增大。

1
2
3
4
5
6
7
➜  gcc main.c
➜ ll
-rwxrwxr-x 1 hank hank 8.2K 2月 24 17:58 a.out

➜ gcc -g main.c
➜ ll
-rwxrwxr-x 1 hank hank 11K 2月 24 17:58 a.out

包含头文件

Linux C 语言程序当中存在如下两种头文件的包含情况:

  • #include <head.h>:预处理程序会在编译系统指定的目录当中去搜索头文件。
  • #include "head.h":预处理器会在当前目标文件所在的文件夹内搜索头文件,如果未找到则进入编译系统指定目录搜索。

GCC 当中可以通过-I参数(include)将指定目录添加到头文件的搜索列表当中:

1
2
3
4
5
6
7
8
9
➜  gcc -v main.c -I /workspace
#include "..." search starts here:
#include <...> search starts here:
/workspace
/usr/lib/gcc/x86_64-linux-gnu/7/include
/usr/local/include
/usr/lib/gcc/x86_64-linux-gnu/7/include-fixed
/usr/include/x86_64-linux-gnu
/usr/include

编译步骤

实质上从hello.c源代码到helloa.out可执行文件,GCC 的编译过程大致经历了下面 4 个步骤:

  • 预处理:C 编译器对各种预处理命令进行处理,包括头文件包含、宏定义的扩展、条件编译的选择等(使用gcc -E);
1
2
3
➜  gcc -E main.c -o main.i
ls
main.c main.i
  • 编译:对预处理得到的源代码文件进行翻译转换,产生由机器语言描述的汇编文件使用gcc -S);
1
2
3
➜  gcc -S main.i
ls
main.c main.i main.s
  • 汇编:将汇编代码转译成为机器码使用gcc -c);
1
2
3
➜  gcc -c main.s
ls
main.c main.i main.s main.o
  • 链接:将机器码中的各种符号引用与定义转换为可执行文件中的相应信息(例如虚拟地址);
1
2
3
➜  gcc main.o -o main
ls
main.c main.i main.o main.s main

为了便于查找,下表列出了编译和链接 C/C++ 程序时各类文件扩展名的释义:

后缀名称 描述内容
.c C 语言源码,必须经过预处理。
.C.cc.cxx C++源代码,必须经过预处理。
.h C/C++语言源代码的头文件。
.i .c文件预处理后生成。
.ii .C.cc.cxx源码预处理后生成。
.s 汇编语言文件,是.i文件编译后得到的中间文件。
.o 目标文件,是编译过程得到的中间文件。
.a 由目标文件构成的文件库,也称为静态库
.so 共享对象库,也称为动态库

链接库文件

GCC 对于库文件的链接存在动态静态两种方式:

  • 静态链接方式:使用静态链接库进行链接,由于包含了程序运行所需的所有库,因此生成的文件体积较大但是能够直接运行。
  • 动态链接方式:使用动态链接库进行链接,类似于 Windows 系统下的.dll文件,执行时需要依赖相应的动态链接库。

GCC 默认使用动态链接-shared方式,可以通过加入-static参数来指定使用静态链接方式。注意观察下面以不同方式编译代码后,所得到的可执行文件的体积:

1
2
3
4
5
6
7
8
9
10
11
➜  gcc main.c
➜ ll
总用量 16K
-rwxrwxr-x 1 hank hank 8.2K 2月 22 18:25 a.out
-rw-rw-r-- 1 hank hank 136 2月 21 18:12 main.c

➜ gcc main.c -static
➜ ll
总用量 832K
-rwxrwxr-x 1 hank hank 825K 2月 22 18:25 a.out
-rw-rw-r-- 1 hank hank 136 2月 21 18:12 main.c

创建静态链接库

静态链接库是由 GCC 在汇编阶段产生的.o文件构成的集合,以.a作为文档后缀名称,Linux 下也称存档(archive),通常使用ar工具命令来进行打包管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ls
func1.c func2.c main.c

➜ gcc -c func1.c func2.c
ls
func1.c func1.o func2.c func2.o main.c

➜ ar -r libmain.a func1.o func2.o
ar: 正在创建 libmain.a
➜ ll
总用量 36K
-rw-rw-r-- 1 hank hank 84 2月 24 20:06 func1.c
-rw-rw-r-- 1 hank hank 1.6K 2月 24 20:16 func1.o
-rw-rw-r-- 1 hank hank 84 2月 24 20:16 func2.c
-rw-rw-r-- 1 hank hank 1.6K 2月 24 20:16 func2.o
-rw-rw-r-- 1 hank hank 3.3K 2月 24 20:16 libmain.a
-rw-rw-r-- 1 hank hank 136 2月 21 18:12 main.c

创建动态链接库

动态链接库也称为共享对象(shared object),通常以.so作为文件后缀名,由 GCC 编译器通过添加-fpic参数(pic 指位置独立代码,即 Position Independent Code 缩写)方式生成,共享对象模块的每个地址(函数调用和变量引用)都是相对地址,允许程序在执行时动态的加载与运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ls
func1.c func2.c main.c

➜ gcc -c -fpic func1.c func2.c
ls
func1.c func1.o func2.c func2.o main.c

➜ gcc -shared func1.o func2.o -o libmain.so
➜ ll
总用量 28K
-rw-rw-r-- 1 hank hank 84 2月 24 20:06 func1.c
-rw-rw-r-- 1 hank hank 1.6K 2月 24 21:31 func1.o
-rw-rw-r-- 1 hank hank 84 2月 24 20:16 func2.c
-rw-rw-r-- 1 hank hank 1.6K 2月 24 21:31 func2.o
-rwxrwxr-x 1 hank hank 7.8K 2月 24 21:32 libmain.so
-rw-rw-r-- 1 hank hank 136 2月 21 18:12 main.c

上面的步骤比较繁琐,可以将汇编链接两条命令合并为一条命令,编译 C 语言代码的同时得到.so动态链接库文件。

1
2
3
4
5
ls
func1.c func2.c main.c
➜ gcc -fpic -shared func1.c func2.c -o libmain.so
ls
func1.c func2.c main.c libmain.so

指定编译规范

由于 GCC 同时支持多套 C 程序语言规范,因而编译时可以通过选项指定当前需要遵循的语言规范,具体请参考下表:

规范 规范 选项 补充
C89 / C90 ANSI C (X3.159-1989) 或 ISO/IEC 9899:1990 -std=c90 -std=iso9899:1990-ansi
C94 / C95 95 年发布的 C89/C90 修正版,此次修正通常称作 AMD1 - -std=iso9899:199409
C99 ISO/IEC 9899:1999 -std=c99 -std=iso9899:1999
C11 ISO/IEC 9899:2011 -std=c11 -std=iso9899:2011
GNU C89 / C90 带 GNU 扩展的 C89/C90 -std=gnu90 -
GNU C99 带 GNU 扩展的 C99 -std=gnu99 -
GNU C11 带 GNU 扩展的 C11 -std=gnu11 -

例如下面代码当中,指定了 GCC 的编译过程遵循 C89/C90 规范,结果编译时提示错误信息:C++ style comments are not allowed in ISO C90

1
2
3
4
5
6
7
➜  gcc main.c -std=c90

main.c: In function ‘main’:
main.c:7:36: error: C++ style comments are not allowed in ISO C90
printf("hello world!\n"); // 行注释
^
main.c:7:36: error: (this will be reported only once per input file)

缺省情况下,GCC 默认使用的是-std=gnu11规范,即携带 GNU 扩展的 C11 标准。

【GDB】概述

GNU 项目调试器GDBGNU Project Debugger)是一款可以调试 Ada、汇编、C++、D、Fortran、Go、Objective-C、OpenCL、Modula-2、Pascal、Rust 等多种语言的跨平台程序调试工具。为了捕获程序中的各类 Bug,GNU 项目调试器可以胜任下面 4 方面的工作:

  • 启动程序,并指定能够影响其行为的任意内容。
  • 在指定条件下停止程序的执行。
  • 当程序停止时,检查发生了什么问题。
  • 通过修改程序中的内容,从而尝试修复 bug。

向 Linux 命令控制台键入gdb即可运行 GDB 调试程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  gdb

GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git
Copyright (C) 2018 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-linux-gnu".
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 之后直接输入help即可以获取各类型命令的使用帮助,如果需要进一步查看指定类型命令的帮助则可以键入相应的命令分类,例如help data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) help
List of classes of commands:

aliases -- Aliases of other commands
breakpoints -- Making program stop at certain points
data -- Examining data
files -- Specifying and examining files
internals -- Maintenance commands
obscure -- Obscure features
running -- Running the program
stack -- Examining the stack
status -- Status inquiries
support -- Support facilities
tracepoints -- Tracing of program execution without stopping the program
user-defined -- User-defined commands

Type "help" followed by a class name for a list of commands in that class.
Type "help all" for the list of all commands.
Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.

如果需要退出当前的 GDB 命令行调试界面,则可以输入quit,下面的表格列出了 GDB 当中常用的一些命令:

命令 描述 命令 描述
break 设置断点,break 断点所在行号 list 列出产生执行文件的部分源码。
clear 清除断点,clear 断点所在行号 next 执行一行源码,但是不进入函数内部。
delete 清除断点和自动显示的表达式 step 执行一行源码,并且进入函数内部。
disable 使所设断点暂时失效,多个行号可用空格分隔。 run 正常执行当前被调试的程序。
enable 生效所设的断点,与disable作用相反。 quit 退出当前 GDB 命令行调试。
run 运行调试程序。 watch 监视指定变量的值。
countinue 继续执行正在调试的程序。 make 在 GDB 重新生成可执行文件。
file 装载需要调试的可执行文件。 shell 在 GDB 当中执行 UNIX Shell 命令。
kill 终止正在调试的程序。 file 加载可执行的文件。

提示:GDB 当中即可以像 Bash 或 Z-Shell 那样使用 Tab 键命令自动补齐,也能够通过方向键上下翻阅历史命令。

调试范例

接下来,下面例程用于打印当前执行程序的名称以及命令行执行时所携带的参数,我们将会通过它来演示 GDB 调试程序的过程。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, char *argv[]) {
printf("当前执行程序的名称:%s\n", argv[0]);
int index;
for (index = 1; index < argc; index++) {
printf("执行命令时输入的第%d个参数为:%s\n", index, argv[index]);
}
return 0;
}

(1)首先需要使用gcc -g main.c命令编译程序并保留调试 debug 信息:

1
2
3
➜  gcc -g main.c
ls
a.out main.c

(2)进入 GDB 然后装载需要进行调试的可执行文件:

1
2
3
4
➜  gdb
... ...
(gdb) file a.out
Reading symbols from a.out...done.

(3)输入 GDB 的run命令,执行已经装载的 bugging 文件,并在命令后跟随需要传入程序的参数。

1
2
3
4
5
(gdb) run 这是一个Hank的测试程序!
Starting program: /workspace/c-test/a.out 这是一个Hank的测试程序!
当前执行程序的名称:/workspace/c-test/a.out
执行命令时输入的第1个参数为:这是一个Hank的测试程序!
[Inferior 1 (process 7997) exited normally]

(4)通过where命令,查看程序运行中出现的错误堆栈:

1
2
(gdb) where
No stack.

(5)使用list命令查看当前执行程序的源码,每次能够查看 10 行,需要查看更多可以直接回车重新执行上一次输入的命令。

1
2
3
4
5
6
7
8
9
10
11
(gdb) list
1 #include <stdio.h>
2
3 int main(int argc, char *argv[]) {
4 printf("当前执行程序的名称:%s\n", argv[0]);
5 int index;
6 for (index = 1; index < argc; index++) {
7 printf("执行命令时输入的第%d个参数为:%s\n", index, argv[index]);
8 }
9 return 0;
10 }(gdb)

(6)利用break命令在程序的第 5 行位置设置一个断点:

1
2
(gdb) break 5
Breakpoint 1 at 0x555555554674: file main.c, line 5.

(7)重新输入run命令,此时程序会运行到第 5 行断点位置并停止:

1
2
3
4
5
6
(gdb) run
Starting program: /workspace/c-test/a.out 这是一个Hank的测试程序!
当前执行程序的名称:/workspace/c-test/a.out

Breakpoint 1, main (argc=2, argv=0x7fffffffded8) at main.c:6
6 for (index = 1; index < argc; index++) {

(8)输入next命令,在断点位置开始单步执行:

1
2
3
4
5
(gdb) next
7 printf("执行命令时输入的第%d个参数为:%s\n", index, argv[index]);
(gdb) next
执行命令时输入的第1个参数为:这是一个Hank的测试程序!
6 for (index = 1; index < argc; index++) {

(9)断点执行过程中,可以使用print命令查看程序中指定变量的当前值:

1
2
3
4
5
6
(gdb) print index
$1 = 1
(gdb) print argc
$2 = 2
(gdb) print argv
$3 = (char **) 0x7fffffffded8

(10)当发现程序状态出现错误的原因之后,就可以使用kill退出当前 debug 的程序,然后quit离开 GDB 调试器。

1
2
3
(gdb) kill
Kill the program being debugged? (y or n) y
(gdb) quit

TUI 模式

直接通过 GDB 命令行进行 debug 工作显然比较繁琐,因此 GDB 内置的TUITextUser Interface)模式提供了一套文本 UI 界面,能够方便的显示源码、汇编、寄存器的状态。可以直接通过gcb -tui命令直接进入 TUI 模式,或者在进入 GDB 命令行后使用 CTRL+X+A 快捷键进入。进入 TUI 模式后,GDB 窗口划分为源代码查看GDB 命令行两个子窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   ┌──main.c───────────────────────────────────────────────────────────────────┐
│1 #include <stdio.h> │
│2 │
│3 int main(int argc, char *argv[]) { │
│4 printf("当前执行程序的名称:%s\n", argv[0]); │
│5 int index; │
│6 for (index = 1; index < argc; index++) { │
│7 printf("执行命令时输入的第%d个参数为:%s\n", index, argv[index]); │
│8 } │
│9 return 0; │
│10 }^? │
│11 │
│12 │
│13 │
│14 │
└───────────────────────────────────────────────────────────────────────────┘
exec No process In: L?? PC: ??
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"...
Reading symbols from a.out...done.
---Type <return> to continue, or q <return> to quit---2 in /workspace/c-test/main.c
(gdb) |

DDD 图形前端

DDDData Display Debugger) 是一款简洁的 GDB 图形调试界面,Ubuntu 系统当中可以通过如下命令进行安装:

1
sudo apt-get install ddd

DDD 的使用与 TUI 类似,窗口依然被划分为源代码查看GDB 命令行两个子窗口,同时右侧还增加了一个方便的操作面板。

注意:DDD 不支持中文注释,如果打开携带有中文注释的源代码,会导致这些代码在 DDD 窗口显示不完整。

CGDB

cgdb

基于 Linux 的 GCC 与 GDB 应用程序调试

http://www.uinio.com/Linux/GCC/

作者

Hank

发布于

2018-06-21

更新于

2018-07-01

许可协议