gcc系编译器、调试器的使用和c/c++编译原理
- 编译器
- 为什么要有编译器
- 编译器的自举
- gcc和g++
- 复习c语言的编译过程
- 预处理(预编译)
- 编译(生成汇编)
- 汇编(生成机器可识别代码)
- 链接(生成可执行文件或库文件)
- 拓展:标准支持
- 为什么要调试
- 调试器 gdb
- 开始调试
- readelf 验证程序可被调试
- 使用gdb
- l 查看代码
- r 启动debug程序
- b 操作断点信息
- ns 步进和跳转到下一断点
- f 结束当前函数运行
- p 打印表达式的值
- d 设置和取消对变量的监视
- bt 查看函数栈帧的调用
- 其他
- gdb 总结
编译器
编译器也是一种软件,用于将计算机语言翻译成计算机能够识别的可执行程序。早期没有编译器的概念,因为早期的人们和计算机交互是通过打孔的纸带完成,人将要执行的程序翻译成纸带上的孔洞,计算机会处理纸带上的程序。
为什么要有编译器
文本文件(源文件)变成可执行程序可以称为程序的翻译过程,这个过程可以通过编译器实现。
之所以有这个翻译过程,是因为计算机不认识人的语言。编程语言是比较接近人的自然语言,但计算机的磁盘、内存、CPU寄存器等都是由2种状态的电子元件组成,所以计算机只认识二进制。也有3种或多种状态的物理设备组成计算机,这里不做讨论。
自然语言→设计编程语言→编译汇编语言→汇编二进制⏞计算机只认识这个⏟大部分编译器的工作\text{自然语言} \xrightarrow{设计} \underbrace{\text{编程语言}\xrightarrow{编译}\text{汇编语言}\xrightarrow{汇编}\overbrace{\text{二进制}}^{计算机只认识这个}}_{大部分编译器的工作}自然语言设计大部分编译器的工作编程语言编译汇编语言汇编二进制计算机只认识这个
当然也有解释型语言(Python、JavaScript),属于边翻译边执行的语言。
编译器的自举
CPU可以识别很多被预设的指令,这就是最早的指令集。写在纸带上的孔洞就是表示指令。这些二进制指令可以直接被计算机执行。
只要是涉及芯片制作都会有指令集,除了基本的工业设备(例如光刻机),还要设计芯片能支持的指令集,这个指令集是现代计算机最底层的技术。
后来人们觉得这个二进制编程有点费劲(指完成一个程序纸带需要几天,而计算机几秒钟就运行完整个纸带),人们就人们就将二进制指令用若干助记符(字符串)表示,这些字符串组成了一门汇编语言。
汇编语言比起刻有二进制语言的带孔纸带,观感上好了很多。因为是用字符表示指令,所以汇编语言算是人类的自然语言中选出部分词汇组成的助记符语言。计算机并不认识汇编,于是先为每一个需要表示的字符分配一个二进制数字编号即编码,然后用二进制机器码编写负责将编码形式的汇编语言(人能看的懂)转换成二进制程序(计算机”看“得懂)的 “软件”。
开始时各个公司的程序员都自己指定一套字符转二进制的编码,使得沟通十分混乱(类比中国古代的春秋战国,各国都有自己的文字),直到美国国家标准学会(ANSI)在1963年推出ASCII码,解决了字符表示不统一的问题的同时,
梳理这个过程:
- 假设已经有了(纯二进制)机器码形式的汇编语言以及辅助理解的助记符。用这个汇编语言编写了一个能够理解汇编语言的机器码编译器(指直接由 “二进制” 借助计算机组成编译器)。
- 用助记符编写一个汇编语言的编译器,用已准备好的纯二进制编译器对助记符写的编译器文本源码进行编译生成可执行程序。
- 最开始的纯二进制编译器可被新生成的可执行程序替代。以后的汇编文本代码都可通过这个可执行程序进行编译。

这个过程就是自举,即用一种语言来编写该语言自身的编译器或解释器,也可以理解为汇编语言的自我更新过程。早期的汇编语言的初代编译器全靠对纸带打孔交给计算机运行得到。用一种语言来编写其他语言的编译器严格来说不能算自举,例如用汇编语言写 c 语言的初代编译器就不是自举,但 c 用初代编译器生成自己的编译器时就是自举。
汇编诞生的时候顺带诞生了汇编语言翻译成二进制语言的软件(用二进制“写”的翻译软件),这个软件可以认为是最早的编译器。这个编译器的工作就是将汇编指令翻译成二进制并交给计算机去执行。
虽然已经能将汇编语言翻译成二进制,再后来人们觉得汇编语言用起来也不是特别好用,于是就诞生了各种高级语言(c、c++等),这些高级语言更接近人的自然语言,新诞生的语言也会伴随适配高级语言的编译器。
这些高级语言同样可以代入自举的思考方式。
例如c++:
- 已经有了 c 语言。用 c 语言编写了一个能够理解c++文本源码的编译器。
- 用 c++ 写一个同样功能的编译器(例如g++),用已准备好的通过 c 源码,和 c 编译器生成的 c++ 编译器,对 c++ 写的编译器文本源码进行编译生成可执行程序。
- 最开始的通过 c 源码,和 c 编译器生成的 c++ 编译器,可被新生成的可执行程序替代。以后的 c++ 文本代码都可通过这个可执行程序进行编译。
c 语言的编译器也是借助汇编通过 “自举” 的过程得到,这里可以自己代入语言加深理解。
尽管可以实现一个高级语言如 c 语言直接转换成机器语言的编译器,即重走汇编语言的老路,不同的是用二进制实现能识别 c 语言文本的编译器,但没有必要。先将 c 语言转换成汇编语言,再通过原有的汇编语言编译器翻译,这个模式在现代使用的更多。
历史上的语言的发展就是从二进制到汇编,再到高级语言的过程,这个过程用了几十年。若想绕过汇编语言直接将高级语言翻译成二进制机器语言,首先要面临的问题是成本太高:之前写的 c 语言的编译器就得用二进制机器语言重写,相当于是重新走一遍历史,而且 c 语言的概念比汇编语言多的不是一星半点;而将 c 语言翻译成汇编也只是一个字符文本的转换。这也是为什么没有必要再生成直接将 c 语言转换成二进制语言的编译器。
再后来的一些应用场景,c、c++ 这种编译型语言在有些场景处理数据的成本还是比较高,例如对字符串做切割处理,拓展更多的功能包。而且 c++ 的更新也比较慢,所以在 c、c++ 的基础上诞生了解释型语言。
- Java(半解释型语言,因为有字节码(.class)这个中间产物)
- Python
- PHP
解释型语言都要先安装各种工具包和解释器,而 c 语言只需要安装对应的库和编译器。这些解释器都是用 c、c++ 写的,这也是为什么有一种“c生万物”的说法。
gcc和g++
gcc 就是通过汇编编写并通过自举得到的,用于将 c 语言编译成可执行程序的编译器; g++则是通过 c 语言编写的将 c++ 编译成可执行程序的编译器。后通过多次通过自举实现的版本更迭,二者已经发展成了两个不同的语言(就像英国和美国)。
因二者在指令使用上基本相同,这里只介绍 gcc 。
在Linux,gcc 和 g++被放在了 bin 目录,这意味着 gcc 和 g++ 可以像指令一样使用。
[Bjarne@VM-8-8-centos ~]$ which gcc
/usr/bin/gcc
[Bjarne@VM-8-8-centos ~]$ which g++
/usr/bin/g++
[Bjarne@VM-8-8-centos ~]$
格式:gcc [选项] 要编译的文件 [选项] [目标文件]
功能:用于将c、c++的文本文件(.c或.cpp)变成可执行程序(.exe)。
c++的文件拓展名有很多写法:.cpp、.cc、.cxx。
它们没有区别,g++ 都能识别,包括MSVC。尽管 Linux 认为文件后缀没有意义,但 gcc 和 g++ 会识别后缀。
Linux 使用 .cc 居多,.cpp 是所有场合都有用。
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c makefile
[Bjarne@VM-8-8-centos cppTest]$ cat a.c
#include <stdio.h>int main(){printf("hello, world\n");return 0;}[Bjarne@VM-8-8-centos cppTest]$ gcc a.c -o a.exe # 简单使用[Bjarne@VM-8-8-centos cppTest]$ ./a.exehello, world[Bjarne@VM-8-8-centos cppTest]$
选项:
-E:只激活预处理,这个不生成文件,需要把它重定向到一个输出文件里面。可以理解成进行程序的翻译,直到处理做完就停止。
-S:编译到汇编语言不进行汇编和链接。
-c:编译到目标代码。即将汇编语言整理成二进制的可执行文件之后就停下来。
-o:文件输出到 文件。选项后需跟一个文件名。
-static:此选项对生成的文件采用静态链接。加了这个选项就可以生成静态库。若没有安装静态库,则使用这个选项可能会报错。可执行yum install glibc-static。若是c++,则执行yum install libstdc++-static。关于什么是静态库,什么是动态库,需要结合文件进行描述,这里先暂时不讨论。
-g:生成调试信息。GNU 调试器可利用该信息。只有加了调试信息,才能使用 gdb 对程序进行调试。
-shared:此选项将尽量使用动态库,所以生成文件比较小,但是需要系统有动态库。
-O0、-O1、-O2、-O3:编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高。
-w:不生成任何警告信息。
-Wall:生成所有警告信息。
-std=:指定编译器所使用的标准。例如-std=c99 则是指定 c 语言在编译时使用 c99 的标准。
gcc 和 g++ 在不同的操作系统使用的指令完全一样。例如 windows,可以通过安装 MinGW-w64 来使用 gcc 和 g++,而 Linux 通常都是自带 gcc 和 g++。
复习c语言的编译过程
大致过程:c源文件 →\rightarrow→ 预处理 →\rightarrow→ 编译 →\rightarrow→ 汇编 →\rightarrow→ 链接 →\rightarrow→ 可执行程序。
c++ 和 c 语言的编译过程完全相同。
下面将通过这个程序来模拟这个过程:
t.h 内容:
int b = 66;
a.c 内容:
#include "t.h"
#define Ten 10
int main(){
#if TEST
int a = 9;
#else
int c = Ten;//毕业即失业什么的不要鸭
#endif
return 0;
}
各个阶段的任务如下:
预处理(预编译)
预处理功能主要包括宏和标识符替换,文件包含,条件编译,去注释等。
预处理指令是以 # 号开头的代码行。
处理完之后的语言还是 C 语言,但很多内容被替换或删除了。
实例: gcc –E a.c –o a.i
选项 -E ,该选项的作用是让 gcc 在预处理结束后停止编译过程。若不使用这 -E 及其类似的选项控制进程,则 gcc 会直接将文本文件翻译成可执行文件。
选项 -o 是指目标文件,.i 为拓展名的文件是已经过预处理的C原始程序。
效果:
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c makefile t.h
[Bjarne@VM-8-8-centos cppTest]$ gcc -E a.c -o a.i
[Bjarne@VM-8-8-centos cppTest]$ cat a.i
# 1 "a.c"
# 1 "<built-in>"# 1 "<command-line>"# 1 "/usr/include/stdc-predef.h" 1 3 4# 1 "<command-line>" 2# 1 "a.c"# 1 "t.h" 1int b = 66; # 头文件被成功展开# 2 "a.c" 2int main(){# 这里将条件编译给去掉int c = 10; # 标识符也被替换,注释也被清理return 0;}[Bjarne@VM-8-8-centos cppTest]$
编译器在安装时都要有自己的默认搜索路径,所以使用 <> 包裹的头文件展开时会将指定路径的文件的内容根据需要选择性地拷贝到现在的代码中,使用 "" 包括的头文件只会在指定路径查找要展开的头文件。
预处理会裁掉条件编译为假的内容。通过条件编译,可以实现对代码的动态裁剪。
在生活中有很多软件,都是同一种软件,但是功能却有差别,有的是收费的,而有的是免费的,例如 Visual Studio 2022,xshell,vmware,都会分专业版和社区版。
若这些软件都是用c、c++实现的,则社区版和专业版都是维护 1 份代码,通过条件编译来裁剪部分高级功能,想升级成专业版也可以修改条件编译,若软件出了 BUG 也只需要修改这一份源码即可。
编译(生成汇编)
在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。
用户可以使用 -S 选项来进行查看,该选项能将进度截止到生成汇编语言文本。
实例: gcc –S a.i –o a.s(可以是a.c,但有了预处理之后的代码,何必再预处理一次)。
效果:
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c a.i makefile t.h
[Bjarne@VM-8-8-centos cppTest]$ gcc -S a.i -o a.s
[Bjarne@VM-8-8-centos cppTest]$ cat a.s.file "a.c".globl b.data.align 4.type b, @object.size b, 4
b:.long 66.text.globl main.type main, @function
main:
.LFB0:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl $10, -4(%rbp) # 左侧的movl、movq等是助记符,右侧是寄存器,详细见汇编语言movl $0, %eaxpopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE0:.size main, .-main.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)".section .note.GNU-stack,"",@progbits
[Bjarne@VM-8-8-centos cppTest]$
汇编(生成机器可识别代码)
汇编阶段是把编译阶段生成的 “.s” 文件转成二进制的目标文件。这个目标文件可称为可重定位二进制文件。
在此可使用选项 “-c” 就可看到汇编代码已转化为 “.o” 的二进制目标代码了。
实例: gcc –c a.s –o a.o
查看二进制代码,可使用 od file,用 cat 和 vim 打开它只能得到乱码或看不懂的字符。但我想绝大部分人都不会想看这个。将它以八进制的方式打印出来。这个 .o 已经是准可执行程序了,相当于已经打好孔的纸带。但即使赋予可执行的权限,它依旧不能运行。
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c a.i a.s makefile t.h
[Bjarne@VM-8-8-centos cppTest]$ gcc -c a.s -o a.o
[Bjarne@VM-8-8-centos cppTest]$ od a.o # 说没人想看是有原因的,不信往下看
0000000 042577 043114 000402 000001 000000 000000 000000 000000
0000020 000001 000076 000001 000000 000000 000000 000000 000000
0000040 000000 000000 000000 000000 001060 000000 000000 000000
0000060 000000 000000 000100 000000 000000 000100 000013 000012
0000100 044125 162611 042707 005374 000000 134000 000000 000000
0000120 141535 000000 000102 000000 043400 041503 020072 043450
0000140 052516 020051 027064 027070 020065 030062 032461 033060
0000160 031462 024040 062522 020144 060510 020164 027064 027070
0000200 026465 032064 000051 000000 000024 000000 000000 000000
0000220 075001 000122 074001 000420 006033 004007 000620 000000
0000240 000034 000000 000034 000000 000000 000000 000022 000000
0000260 040400 010016 001206 006503 046406 003414 000010 000000
0000300 000000 000000 000000 000000 000000 000000 000000 000000
0000320 000000 000000 000000 000000 000001 000000 000004 177761
0000340 000000 000000 000000 000000 000000 000000 000000 000000
0000360 000000 000000 000003 000001 000000 000000 000000 000000
0000400 000000 000000 000000 000000 000000 000000 000003 000002
0000420 000000 000000 000000 000000 000000 000000 000000 000000
0000440 000000 000000 000003 000003 000000 000000 000000 000000
0000460 000000 000000 000000 000000 000000 000000 000003 000005
0000500 000000 000000 000000 000000 000000 000000 000000 000000
0000520 000000 000000 000003 000006 000000 000000 000000 000000
0000540 000000 000000 000000 000000 000000 000000 000003 000004
0000560 000000 000000 000000 000000 000000 000000 000000 000000
0000600 000005 000000 000021 000002 000000 000000 000000 000000
0000620 000004 000000 000000 000000 000007 000000 000022 000001
0000640 000000 000000 000000 000000 000022 000000 000000 000000
0000660 060400 061456 061000 066400 064541 000156 000000 000000
0000700 000040 000000 000000 000000 000002 000000 000002 000000
0000720 000000 000000 000000 000000 027000 074563 072155 061141
0000740 027000 072163 072162 061141 027000 064163 072163 072162
0000760 061141 027000 062564 072170 027000 060544 060564 027000
0001000 071542 000163 061456 066557 062555 072156 027000 067556
0001020 062564 043456 052516 071455 060564 065543 027000 062562
0001040 060554 062456 057550 071146 066541 000145 000000 000000
0001060 000000 000000 000000 000000 000000 000000 000000 000000
*
0001160 000033 000000 000001 000000 000006 000000 000000 000000
0001200 000000 000000 000000 000000 000100 000000 000000 000000
0001220 000022 000000 000000 000000 000000 000000 000000 000000
0001240 000001 000000 000000 000000 000000 000000 000000 000000
0001260 000041 000000 000001 000000 000003 000000 000000 000000
0001300 000000 000000 000000 000000 000124 000000 000000 000000
0001320 000004 000000 000000 000000 000000 000000 000000 000000
*
0001360 000047 000000 000010 000000 000003 000000 000000 000000
0001400 000000 000000 000000 000000 000130 000000 000000 000000
0001420 000000 000000 000000 000000 000000 000000 000000 000000
0001440 000001 000000 000000 000000 000000 000000 000000 000000
0001460 000054 000000 000001 000000 000060 000000 000000 000000
0001500 000000 000000 000000 000000 000130 000000 000000 000000
0001520 000056 000000 000000 000000 000000 000000 000000 000000
0001540 000001 000000 000000 000000 000001 000000 000000 000000
0001560 000065 000000 000001 000000 000000 000000 000000 000000
0001600 000000 000000 000000 000000 000206 000000 000000 000000
0001620 000000 000000 000000 000000 000000 000000 000000 000000
0001640 000001 000000 000000 000000 000000 000000 000000 000000
0001660 000112 000000 000001 000000 000002 000000 000000 000000
0001700 000000 000000 000000 000000 000210 000000 000000 000000
0001720 000070 000000 000000 000000 000000 000000 000000 000000
0001740 000010 000000 000000 000000 000000 000000 000000 000000
0001760 000105 000000 000004 000000 000100 000000 000000 000000
0002000 000000 000000 000000 000000 000700 000000 000000 000000
0002020 000030 000000 000000 000000 000010 000000 000006 000000
0002040 000010 000000 000000 000000 000030 000000 000000 000000
0002060 000001 000000 000002 000000 000000 000000 000000 000000
0002100 000000 000000 000000 000000 000300 000000 000000 000000
0002120 000360 000000 000000 000000 000011 000000 000010 000000
0002140 000010 000000 000000 000000 000030 000000 000000 000000
0002160 000011 000000 000003 000000 000000 000000 000000 000000
0002200 000000 000000 000000 000000 000660 000000 000000 000000
0002220 000014 000000 000000 000000 000000 000000 000000 000000
0002240 000001 000000 000000 000000 000000 000000 000000 000000
0002260 000021 000000 000003 000000 000000 000000 000000 000000
0002300 000000 000000 000000 000000 000730 000000 000000 000000
0002320 000124 000000 000000 000000 000000 000000 000000 000000
0002340 000001 000000 000000 000000 000000 000000 000000 000000
0002360
[Bjarne@VM-8-8-centos cppTest]$
MSVC(Visual Stuiod 系列IDE使用的编译器) 下的拓展名是 .obj。
链接(生成可执行文件或库文件)
在成功编译之后,就进入了链接阶段。
命令: gcc a.o –o a.exe
这些 .o 文件加上系统库就形成了可执行程序。链接过程用户一般看不到,都是 .o 一下子就生成了。
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c a.i a.o a.s makefile t.h
[Bjarne@VM-8-8-centos cppTest]$ gcc a.o -o a.exe
[Bjarne@VM-8-8-centos cppTest]$ ./a.exe
a.o 只是针对单一文件,若是多个有关联的文件,需要用 *.o 代替。
例如 a.c :
#include <stdio.h>#include "t.h"int main(){printf("%d\n",Add(5,6));return 0;}
t.h:
int add(int,int);
t.c:
#include "t.h"
int add(int a,int b){
int c = 0;
c = a + b;
return c;
}
分别形成 .o 文件再链接,或省略这个过程直接生成:
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c makefile t.c t.h
[Bjarne@VM-8-8-centos cppTest]$ gcc -c a.c -o a.o # 分别生成 .o 文件
[Bjarne@VM-8-8-centos cppTest]$ gcc -c t.c -o t.o
[Bjarne@VM-8-8-centos cppTest]$ gcc *.o -o a.exe # 最后进行链接
[Bjarne@VM-8-8-centos cppTest]$ ./a.exe # 生成的文件可执行
11 # 正常运行
[Bjarne@VM-8-8-centos cppTest]$ rm a.o t.o a.exe
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c makefile t.c t.h
[Bjarne@VM-8-8-centos cppTest]$ gcc *.c -o a.exe # 也可以直接把当前路径下的.c都拉来编译
[Bjarne@VM-8-8-centos cppTest]$ ./a.exe
11
[Bjarne@VM-8-8-centos cppTest]$ rm a.o t.o a.exe
[Bjarne@VM-8-8-centos cppTest]$ gcc a.c t.c -o a.exe # 也可以直接指定要哪些文件参与编译
[Bjarne@VM-8-8-centos cppTest]$ ./a.exe
11
[Bjarne@VM-8-8-centos cppTest]$
仔细观察可以发现,c语言的编译过程产生的中间文件的拓展名可以组成 iso ,有特殊含义。包括选项 ESc ,和键位ESC有相似之处,但 “c” 用的小写。
拓展:标准支持
c 语言和 c++ 的编译过程基本差不多,但用的软件是 g++。
gcc默认版本是 4.8.5,可能不支持 C99 及之后的语法,例如 for(int i;;){}。
[Bjarne@VM-8-8-centos awork]$ gcc -v
# 省略
gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
[Bjarne@VM-8-8-centos awork]$
想要 gcc 支持 c99 的语法,可以指定标准。
[Bjarne@VM-8-8-centos awork]$ gcc a.c -std=c99
类似的 g++ 也可以指定。
[Bjarne@VM-8-8-centos cppTest]$ ls
a.cpp makefile t.cpp t.h
[Bjarne@VM-8-8-centos cppTest]$ g++ *.cpp -o a.exe -std=c++11
[Bjarne@VM-8-8-centos cppTest]$ ./a.exe
11 # 代码没变,只改了拓展名为 cpp
[Bjarne@VM-8-8-centos cppTest]$
为什么要调试
这里为经验总结,详细见大学的课程和相关的书籍《软件工程》和《IT项目管理》。
程序的发布方式有两种,debug (Debug)模式和 release (Release)模式。
两种模式的应用场景:
在公司里,使用编程语言实现产品的研发团队属于整个产品最末端的一批人。开始是老板和产品经理通过各种信息觉得有什么产品可以做,他们沟通好后决定要做这个项目,然后再联系相关程序员准备开发,这个过程是立项的过程。这个基本不是程序员关心的。
立项之后会给程序员搭配资源。这个搭配资源有很多形式,比如最常见的形式是给钱,找人,给开发、测试环境相关的设备。其中有的人是做开发的,开发有前端、有后端,这么多人需要有人来主导,比如开发到什么程度就要进行测试,测试到什么程度要开始进行适度的上线,上线之后由谁来收集、反馈用户的结果来迭代产品。
总之就是参与整个项目的成员中,需要有一批程序员,有一批给程序员配好的一套开发环境和线上环境,还要有负责人。
技术上的负责人是项目经理(程序员中的负责人,负责安排哪些人做哪些事,以及心理疏导),负责开发的进度。
然后就是产品经理。这类人通常做的事是给程序员提需求,或和项目经理沟通技术问题。部分大公司将项目经理和产品经理两个职务给分开,但很多不正规或开始发展,又或者是特殊考虑的的公司基本都是产品经理和项目经理是同一人担任。
立项、搭配资源完成后,就已经有了一个能完成某个项目的团队了。之后就是正式开发,会有很多人普及软件工程上的开发模式例如敏捷模式、瀑布模式等,直白点说就是开始写代码。
一个程序员在开发时,开发的所有产品在发布时基本都是Debug模式,因为负责的程序员在发布之前自己肯定要对自己做出的产品进行初步的测试,这会直接影响到程序员的KPI(关键绩效指标(Key Performance Indicator,简称KPI)是通过对组织内部流程的输入端、输出端的关键参数进行设置、取样、计算、分析,衡量流程绩效的一种目标式量化管理指标,是把企业的战略目标分解为可操作的工作目标的工具,是企业绩效管理的基础。详细见百度),比如找到多少 bug ,测试多少 bug ,修正多少 bug。
程序员在 debug 模式进行若干测试,觉得没问题之后,就使用 release 模式进行发布,这个过程在小公司就由自己去做,一般的大公司都有自己的发布平台。发布时自己也可以继续进行测试。
提测,就是由专门的人进行测试,这个时候测试的就是 release 版本的程序组合,因为提测必须是用户使用的最终版本。这是为了防止有的问题在 debug 模式下可能不是特别明显,但更换为 release 模式后问题会被放大。
若提测人员发现问题就将问题反馈给开发人员,开发人员重复开发测试的流程。
在反复的提测、打回重做若干次之后,能完成一定的基础功能,且没有太过明显的过程,则开始上线。
上线,即让用户能看到产品,可以将产品部署到公司的云服务器上,也可以交给。上线的方式例如灰度上线(先更新一小部分人的使用,每次运行没有出现问题时再进行推广,否则回归开发流程)。
例如游戏的内测版本就是灰度上线,先让小部分玩家进行内测版本的体验,没有问题后再发布正式版本。典型的就是王者荣耀出新的角色后,会现在体验服或者说内测服进行对局测试,找不出问题后再发布正式服,虽然很多时候做出来的都是一堆超标怪三体人。
运营人员推广一系列的产品推广拉新活动。
运维人员发现已发布的产品存在问题,影响到产品的体验和稳定,则进行小幅度的修补。
其中 debug 模式的特点是可通过调试工具进行追踪。
调试器 gdb
在Linux下进行调试代码,首先需要了解2点:
- 调试代码的思路,
- 调试代码的工具。
若写过一定量的代码,一旦出现了问题都是先根据已知信息(例如编译器给出的信息)判断出问题的地方在哪,然后分析出问题的地方的上下文或给可能出问题的地方加注释,找到问题后再尝试解决。这个调试代码的思路和编程语言、工具无关。
代码出问题时有很多种选择,调试器是其中一种,而且很大可能是最后的选择。gdb 就是 gcc 系列工具下的调试器。这个调试器在大部分 Linux 中默认是安装了的。
[Bjarne@VM-8-8-centos work]$ gdb --v
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 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-redhat-linux-gnu".For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>.[Bjarne@VM-8-8-centos work]$
和 gcc、g++ 一样,gdb 无论安装在哪个操作系统,指令都是通用的。
开始调试
gcc/g++ 生成的二进制程序,默认是 release 模式,无论是 Linux 还是 Windows。只有 debug 模式编译出来的可执行程序才能被调试。调试方式是在命令行窗口输入 gdb 待调试的可执行程序 指令。
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c makefile
[Bjarne@VM-8-8-centos cppTest]$ make
gcc a.c -o a.exe
[Bjarne@VM-8-8-centos cppTest]$ gdb a.exe # 开始调试
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7 # 和调试无关的信息
Copyright (C) 2013 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-redhat-linux-gnu".For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>... # 和调试无关的信息Reading symbols from /home/Bjarne/work/cppTest/a.exe...(no debugging symbols found)...done. # 这里直接告诉了找不到调试符号,即不可调试(gdb) list # 无论能否调试,都会进入gdb专属的对话窗口中No symbol table is loaded. Use the "file" command.(gdb) r # 后续的指令可以运行,但不是程序员想看的代码。Starting program: /home/Bjarne/work/cppTest/a.exe[====================================================================================================][100.0%][|][Inferior 1 (process 21897) exited normally]Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.3.x86_64(gdb) quit # 退出调试[Bjarne@VM-8-8-centos cppTest]$
要使可执行程序是 debug 模式,必须在源代码生成二进制程序的时候,加上 -g 选项。
gcc a.c -o a.exe -g
加上 -g 选项后就可以正常编译
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c makefile
[Bjarne@VM-8-8-centos cppTest]$ gcc a.c -o a.exe -g # 增加了选项
[Bjarne@VM-8-8-centos cppTest]$ gdb a.exe
# 这里省略大量无关信息
<http://www.gnu.org/software/gdb/bugs/>... # 没有了之前的报错Reading symbols from /home/Bjarne/work/cppTest/a.exe...done.(gdb) quit[Bjarne@VM-8-8-centos cppTest]$
readelf 验证程序可被调试
软件可以被调试,一定是新增了调试信息。在 debug 模式生成程序时,会给可执行程序内部添加 debug 信息,因此 debug 程序的大小一般都大于 release 程序。
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c makefile
[Bjarne@VM-8-8-centos cppTest]$ make
gcc a.c -o a.exe -g
[Bjarne@VM-8-8-centos cppTest]$ gcc a.c -o a2.exe
[Bjarne@VM-8-8-centos cppTest]$ ll |grep exe
-rwxrwxr-x 1 Bjarne Bjarne 8736 Sep 22 15:53 a2.exe # release程序
-rwxrwxr-x 1 Bjarne Bjarne 11528 Sep 22 15:52 a.exe # debug程序
[Bjarne@VM-8-8-centos cppTest]$
release 程序的 a2.exe 明显比 debug 程序的 a.exe 要小(8736 byte < 11528 byte)。
通过指令readelf 可查看调试信息。.exe 可执行程序无论是 debug 模式还是 release 程序,它形成的二进制可执行程序,格式都是 ELF (Executable and Linkable Format,可执行与可链接格式)。
这是编译原理的问题,只点提一下,以后有机会再补充。在 Linux ,c源码形成的可执行程序,内部会按照格式生成,格式包括哪一部分处于代码段的区域(从哪个地址到哪个地址),处于数据段的区域,处于BSS(Block Started by Symbol,由符号开始的块)段,处于可读区域,处于可读可写区域,以及各种库的地址,再包括代码的入口地址,以及程序是如何被加载的等等。
这里通过 readelf 指令查看可调试的程序和不可调试的程序相比都多了哪些东西。readelf 的 -S 指令表示列出文件中所有的 “段”,也就是 .text, .data, .bss 等部分。因为内容太多,这里借助管道和grep 指令筛选被添加的 debug 字段,并用选项 -i 忽视大小写。
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c makefile
[Bjarne@VM-8-8-centos cppTest]$ gcc a.c -o re.exe
[Bjarne@VM-8-8-centos cppTest]$ gcc a.c -o de.exe -g
[Bjarne@VM-8-8-centos cppTest]$ readelf -S re.exe|grep -i debug
[Bjarne@VM-8-8-centos cppTest]$ readelf -S de.exe|grep -i debug
[27] .debug_aranges PROGBITS 0000000000000000 00001061
[28] .debug_info PROGBITS 0000000000000000 00001091
[29] .debug_abbrev PROGBITS 0000000000000000 0000116d
[30] .debug_line PROGBITS 0000000000000000 000011d8
[31] .debug_str PROGBITS 0000000000000000 0000122a
[Bjarne@VM-8-8-centos cppTest]$ readelf -S re.exe
There are 30 section headers, starting at offset 0x1948:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
其中类型(Type)是 LOAD 的部分是要加载到可执行程序中的,具体自己去查询如何解读。
因为 debug 模式的可执行程序会多出一些信息,用户并不需要,而且会占用程序的大小,浪费资源,所以发布的版本基本都是 release 模式。
编译器自作主张做的优化,使得 debug 阶段做的测试可能不准确,需要进一步测试。
使用gdb
将以这个 c 代码为例演示调试。
#include <stdio.h>int sigma100(){int sum = 0;int i=1;for(i=1;i<=100;i++){sum += i;}return sum;}int main(){int sum = 0;sum = sigma100();printf("%d\n",sum);return 0;}
gdb exe 开始调试,退出: ctrl + d 或 quit 。
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c de.exe makefile re.exe
[Bjarne@VM-8-8-centos cppTest]$ gdb de.exe
# 省略和gdb有关的信息
(gdb) quit # 真正意义上的调试界面
[Bjarne@VM-8-8-centos cppTest]$
先有个保底,中间不会操作了可退出重来。
l 查看代码
list/l 行号:显示exe 程序的源代码,接着上次的位置往下列,每次列10行。gdb会记录最近的历史命令,直接回车就是上一个命令。
list/l 函数名:列出某个函数的源代码。效果:
[Bjarne@VM-8-8-centos cppTest]$ ls
a.c de.exe makefile re.exe
[Bjarne@VM-8-8-centos cppTest]$ gdb de.exe
# 省略
(gdb) list # 可以只输入l,效果相同
4 int sum = 0;
5 int i=1;
6 for(i=1;i<=100;i++){
7 sum += i;
8 }
9 return sum;
10 }
11
12 int main(){
13 int sum = 0;
(gdb) # 这里按了下回车
14 sum = sigma100();
15 printf("%d\n",sum);
16 return 0;
17 }
(gdb) # 源码走到了尽头
Line number 18 out of range; a.c has 17 lines.
(gdb)
r 启动debug程序
r 或 run :运行程序。若没有断点,则程序会直接运行完成,否则运行到程序步骤中的低1个断点。除此之外使用 r 都会重新运行。
[Bjarne@VM-8-8-centos cppTest]$ gdb de.exe
# 省略
(gdb) list # 可以只输入l,效果相同
4 int sum = 0;
5 int i=1;
6 for(i=1;i<=100;i++){
7 sum += i;
8 }
9 return sum;
10 }
11
12 int main(){
13 int sum = 0;
(gdb) r
Starting program: /home/Bjarne/work/cppTest/de.exe
5050
[Inferior 1 (process 24128) exited normally]
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.3.x86_64
(gdb)
b 操作断点信息
info/i locals:查看当前栈帧局部变量的值。可被 display 替代。除了局部变量,断点信息、被监视(见后文 display )的变量的信息也可通过info/i 查看,通过info/i break/b 查看断点信息。
break/b 行号:在某一行设置断点。在一个gdb的调试周期(从gdb 程序 开始,到quit结束)中,断点的编号会持续递增。结束调试后所有的断点会被清除。
break/b 函数名:在某个函数开头设置断点。
delete breakpoints:删除所有断点。
delete breakpoints n / delete n:删除序号为 n 的断点。
disable breakpoints:禁用断点。断点表中的一个属性 Enb 是断点是否被禁止。
enable breakpoints:启用断点。
[Bjarne@VM-8-8-centos cppTest]$ gdb de.exe
# 省略
(gdb) info b
No breakpoints or watchpoints.
(gdb) b 4 # 加断点在第4行 int sum = 0;
Breakpoint 1 at 0x400531: file a.c, line 4.
(gdb) b 13 # 加断点在第13行 int sum = 0;
Breakpoint 2 at 0x400565: file a.c, line 13.
(gdb) b 15 # 加断点在第15行 printf("%d\n",sum);
Breakpoint 3 at 0x400579: file a.c, line 15.
(gdb) r # 会运行到逻辑意义上的第1个断点
Starting program: /home/Bjarne/work/cppTest/de.exe
Breakpoint 2, main () at a.c:13
13 int sum = 0;
(gdb) r # 再次启动 r 则重新开始调试
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/Bjarne/work/cppTest/de.exe
Breakpoint 2, main () at a.c:13
13 int sum = 0;
(gdb) disable 1 # 禁用第1个断点
(gdb) c # 跳转到下一个断点
Continuing.
Breakpoint 3, main () at a.c:15
15 printf("%d\n",sum); # 下一个断点不再是第4行
(gdb) enable 1 # 重启被禁用的第4行的断点
(gdb) r # 重新运行
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/Bjarne/work/cppTest/de.exe
Breakpoint 2, main () at a.c:13
13 int sum = 0;
(gdb) c # 再次跳转到下一个断点
Continuing.
Breakpoint 1, sigma100 () at a.c:4
4 int sum = 0; # 会发现来到了第4行的断点处
(gdb)
ns 步进和跳转到下一断点
这 3 个指令需要保证调试的程序开始运行才能使用。
n 或 next:单条执行。每条语句执行都会显示,遇到函数会直接执行不进入函数。
s 或step:和n 相比,会进入函数调用。
continue(或c):从当前位置开始连续而非单步执行程序,或从一个断点到另一个断点。
但一些的调试器在实际使用时,无论是 n 还是 s 都会进入函数,目前没找到原因。
(gdb) b 4 # 加断点在第4行 int sum = 0;
Breakpoint 1 at 0x400531: file a.c, line 4.
(gdb) b 13 # 加断点在第13行 int sum = 0;
Breakpoint 2 at 0x400565: file a.c, line 13.
(gdb) b 15 # 加断点在第15行 printf("%d\n",sum);
Breakpoint 3 at 0x400579: file a.c, line 15.
(gdb) r # 会运行到逻辑意义上的第1个断点
Starting program: /home/Bjarne/work/cppTest/de.exe
Breakpoint 2, main () at a.c:13
13 int sum = 0;
(gdb)
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.3.x86_64
(gdb) n
14 sum = sigma100();
(gdb) n # 这里原本应该是不会进入sigma100()函数,但还是进了。
Breakpoint 1, sigma100 () at a.c:4
4 int sum = 0;
(gdb) c
Continuing.
Breakpoint 3, main () at a.c:15
15 printf("%d\n",sum);
(gdb) s
5050
16 return 0;
(gdb)
f 结束当前函数运行
finish:执行到当前函数返回,然后挺下来等待命令。
(gdb) r
Starting program: /home/Bjarne/work/cppTest/de.exe
Breakpoint 2, main () at a.c:13
13 int sum = 0;
(gdb) s
14 sum = sigma100();
(gdb) # 回车,进入sigma100函数
Breakpoint 1, sigma100 () at a.c:4
4 int sum = 0;
(gdb) finish # 运行到返回值处
Run till exit from #0 sigma100 () at a.c:4
0x0000000000400576 in main () at a.c:14
14 sum = sigma100();
Value returned is $1 = 5050
(gdb)
p 打印表达式的值
print/p 表达式:打印表达式(可以是单个变量)的值,通过表达式可以修改变量的值或者调用函数。
(gdb) b 4
Breakpoint 1 at 0x400531: file a.c, line 4.
(gdb) b 13
Breakpoint 2 at 0x400565: file a.c, line 13.
(gdb) b 15
Breakpoint 3 at 0x400579: file a.c, line 15.
(gdb) r
Starting program: /home/Bjarne/work/cppTest/de.exe
Breakpoint 2, main () at a.c:13
13 int sum = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.3.x86_64
(gdb) s
14 sum = sigma100();
(gdb) print sum # 查看最近的变量的值
$1 = 0
(gdb) p sum
$2 = 0
(gdb) s
Breakpoint 1, sigma100 () at a.c:4
4 int sum = 0;
(gdb) s
5 int i=1;
(gdb) s
6 for(i=1;i<=100;i++){
(gdb) s
7 sum += i;
(gdb) s
6 for(i=1;i<=100;i++){
(gdb) p sum
$3 = 1
(gdb) print i
$4 = 1
(gdb)
d 设置和取消对变量的监视
display 变量名:跟踪查看一个变量,每次停下来(n/s)都显示它的值。还可用info/i display 查看监视的变量的值。
undisplay n:取消对先前设置的第 n 号变量的跟踪。
(gdb) b 4
Breakpoint 1 at 0x400531: file a.c, line 4.
(gdb) b 13
Breakpoint 2 at 0x400565: file a.c, line 13.
(gdb) b 15
Breakpoint 3 at 0x400579: file a.c, line 15.
(gdb) r
Starting program: /home/Bjarne/work/cppTest/de.exe
Breakpoint 2, main () at a.c:13
13 int sum = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.3.x86_64
(gdb) c
Continuing.
Breakpoint 1, sigma100 () at a.c:4
4 int sum = 0;
(gdb) display sum # 对sum实行监视
1: sum = 0
(gdb) display i # 对i实行监视
2: i = 0
(gdb) i display # 查看所有被监视的变量
Auto-display expressions now in effect:
Num Enb Expression
2: y i
1: y sum
(gdb) s
5 int i=1;
2: i = 0
1: sum = 0
(gdb)
6 for(i=1;i<=100;i++){
2: i = 1
1: sum = 0
(gdb)
7 sum += i;
2: i = 1
1: sum = 0
(gdb)
6 for(i=1;i<=100;i++){
2: i = 1
1: sum = 1
(gdb)
7 sum += i;
2: i = 2
1: sum = 1
(gdb) undisplay 2 //解除对 i 的监视
(gdb) s
6 for(i=1;i<=100;i++){
1: sum = 3
(gdb) display i
3: i = 2
(gdb) s
7 sum += i;
3: i = 3
1: sum = 3
(gdb)
6 for(i=1;i<=100;i++){
3: i = 3
1: sum = 6
(gdb) undisplay sum
warning: bad display number at or near 'sum' # 只能指定监视编号清除监视
(gdb) s
7 sum += i;
3: i = 4
1: sum = 6
(gdb)
bt 查看函数栈帧的调用
breaktrace(或bt):查看各级函数调用及参数(查看已经调用的堆栈)。在检查递归时使用。
(gdb) bt
#0 sigma100 () at a.c:7
#1 0x0000000000400576 in main () at a.c:14
(gdb) c
Continuing.
Breakpoint 3, main () at a.c:15
15 printf("%d\n",sum);
(gdb) bt
#0 main () at a.c:15
(gdb)
其他
这些不常用,就省略交互信息,以后有机会再补充。
set var:修改变量的值。但并不影响程序的进度,通常用于循环。
until X行号:跳至 X 行后的完整语句。
gdb 总结
gdb 虽然用的并不多,比起 windows 的图形化界面(Visual Studio系列编译器)来说很难用,到多进程、网络等场景的调试时使用 gdb 反而不好用,但仍然需要熟练使用,用于应急时使用,例如直接操作服务器。