makefile与gdb调试
使用 GCC 的命令行进行程序编译在单个文件下是比较方便的,当工程中的文件逐渐增多,甚至变得十分庞大的时候,使用 GCC 命令编译就会变得力不从心。这种情况下我们需要借助项目构造工具 make 帮助我们完成这个艰巨的任务。 make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Visual C++的nmake,QtCreator的qmake等。
makefile带来的好处就是“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
makefile文件有两种命名方式 makefile 和 Makefile,构建项目的时候在哪个目录下执行构建命令 make这个目录下的 makefile 文件就会被加载,因此在一个项目中可以有多个 makefile 文件,分别位于不同的项目目录中
1. gcc工作流程
GCC编译器对程序的编译下图所示,分为4个阶段:预处理(预编译)、编译、汇编、链接。GCC的编译器可以将这4个步骤合并成一个。
- 预处理:使用的是
预处理器
- 在这个阶段主要做了三件事:展开头文件、宏替换、求掉注释行。这个阶段需要GCC调用预处理器来完成,最终得到的还是源文件,文本格式。
gcc -E mycode1.c -o mycode1.i
- 编译(整个过程中最耗时的):使用的是
编译器
- 逐行检查程序中出现的语法、词法错误和逻辑错误,并翻译成汇编指令,最终生成一个汇编文件。
gcc -S mycode1.i -o mycode1.s
- 汇编:使用的是
汇编器
- 将汇编文件里面的汇编指令翻译成二进制的机器码,这个过程没有错误检查,只是机械的翻译工作,最终生成一个二进制文件。
gcc -c mycode1.s -o mycode1.o
- 链接:使用的是
链接器
- 将二进制文件链接库文件、数据段合并、地址回填,最终生成一个可执行的二进制文件。
gcc mycode1.o -o mycode1
文件名后缀 | 说明 | gcc参数 |
---|---|---|
.c | 源文件 | 无 |
.i | 预处理后的c文件 | -E |
.s | 编译之后得到的汇编语言的源文件 | -S |
.o | 汇编后得到的二进制文件 | -C |
1 | 编程: |
2. gcc参数与g++
下面的表格中列出了常用的一些gcc参数, 这些参数在 gcc命令中没有位置要求,只需要编译程序的时候将需要的参数指定出来即可
gcc编译选项 | 选项的意义 |
---|---|
-E | 预处理指定的源文件,不进行编译 |
-S | 编译指定的源文件,但是不进行汇编 |
-c | 编译、汇编指定的源文件,但是不进行链接 |
-I directory (大写的i) | 指定 include 包含文件的搜索目录 |
-g | 在编译的时候,生成调试信息,该程序可以被调试器调试 |
-D | 在程序编译的时候,指定一个宏 |
-w | 不生成任何警告信息, 不建议使用, 有些时候警告就是错误 |
-L | 指定编译的时候,搜索的库的路径 |
-fPIC/fpic | 生成与位置无关的代码 |
-shared | 生成共享目标文件。通常用在建立共享库时 |
-std | 指定C方言,如:-std=c99,gcc默认的方言是GNU C |
-Wall | 显示所有的警告信息 |
关于对gcc和g++的理解,下边从三个方面介绍一下二者的区别:
在代码编译阶段(第二个阶段):
后缀为 .c 的,gcc 把它当作是C程序,而 g++ 当作是 C++ 程序
后缀为.cpp的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些
g++会调用gcc,对于C++代码,两者是等价的, 也就是说 gcc 和 g++ 都可以编译 C/C++代码
在链接阶段(最后一个阶段)
- gcc 和 g++ 都可以自动链接到标准C库
- g++ 可以自动链接到标准C++库, gcc如果要链接到标准C++库需要加参数 -lstdc++
综上所述:
- 不管是 gcc 还是 g++ 都可以编译 C 程序,编译程序的规则和参数都相同
- g++可以直接编译C++程序, gcc 编译 C++程序需要添加额外参数 -lstdc++
3. makefile规则语法格式
每条规则由三个部分组成分别是目标,依赖和命令。下面通过一个例子来阐述一下:
1 | # 举例1: 假设有源文件a.c、b.c、c.c和head.h, 现在需要生成可执行程序test1 |
4. 工作原理
在调用make命令编辑程序的时候,make会首先找到该目录下的makefile文件中的第一条规则,分析并执行相关操作,但需要注意的是,好多时候要执行的动作(命令)中使用的依赖是不存在的,如果使用的依赖不存在,这个动作也就不会被执行。
对应的解决方案是先将需要的依赖生成出来,我们就可以在makefile中添加新的规则,将不存在的依赖作为这个新的规则中的目标,当这条新的规则对应的命令执行完毕,对应的目标就被生成了,同时另一条规则中需要的依赖也就存在了。
这样,makefile中的某一条规则在需要的时候,就会被其他的规则调用,直到makefile中的第一条规则中的所有的依赖全部被生成,第一条规则中的命令就可以基于这些依赖生成对应的目标,make的任务也就完成了。
4.1 目标文件的更新
依赖文件存在,目标文件不存在,make就会根据依赖来生成目标文件
依赖和目标都是存在的:
目标的时间大于依赖的时间,此时不更新目标文件;
目标的时间小于依赖的时间,此时make会根据依赖更新目标文件;
4.2 自动推导
虽然make需要根据makefile中指定的规则来完成源文件的编译,但是我们会发现当漏写一些构建规则时,程序还是会被编译成功,这是因为make有自动推导的能力,不会完全依赖makefile。
注意:命令行前面是一个tab健距离;
4.3 变量
使用makefile进行规则定义的时候,为了写起来更加灵活,我们可以在里边使用变量。makefile中的变量分为三种:自定义变量、预定义变量和自动变量。
自定义变量:用makefile进行规则定义的时候,用户可以定义自己的变量,称为用户自定义变量。
预定义变量:在makefile中有一些已经定义的变量,用户可以直接使用这些变量,不用进行定义。
变量名 | 含义 | 默认值 |
---|---|---|
AR | 生成静态库库文件的程序名称 | ar |
AS | 汇编编译器的名称 | as |
CC | C 语言编译器的名称 | cc |
CPP | C 语言预编译器的名称 | $(CC) -E |
CXX | C++语言编译器的名称 | g++ |
FC | FORTRAN 语言编译器的名称 | f77 |
RM | 删除文件程序的名称 | rm -f |
ARFLAGS | 生成静态库库文件程序的选项 | 无 |
ASFLAGS | 汇编语言编译器的编译选项 | 无 |
CFLAGS | C 语言编译器的编译选项 | 无 |
CPPFLAGS | C 语言预编译的编译选项 | 无 |
CXXFLAGS | C++语言编译器的编译选项 | 无 |
FFLAGS | FORTRAN 语言编译器的编译选项 | 无 |
例子:
1 | # 这是一个规则,普通写法 |
- 自动变量:自动变量用来代表这些规则中的目标文件和依赖文件,并且它们只能在规则的命令中使用。
变量 | 含义 |
---|---|
$* | 表示目标文件的名称,不包含目标文件的扩展名 |
$+ | 表示所有的依赖文件,这些依赖文件之间以空格分开,按照出现的先后为顺序,其中可能 包含重复的依赖文件 |
$< | 表示依赖项中第一个依赖文件的名称 |
$? | 依赖项中,所有比目标文件时间戳晚的依赖文件,依赖文件之间以空格分开 |
$@ | 表示目标文件的名称,包含文件扩展名 |
$^ | 依赖项中,所有不重复的依赖文件,这些文件之间以空格分开 |
例子:
1 | # *****************#例1******************** |
4.4 wildcard函数
这个函数的主要作用是获取指定目录下指定类型的文件名,器返回值是以空格分割的、指定目录下的所有符合条件的文件列表。
使用:$(wildcard,参数,参数,…..)
1 | # 使用举例: 分别搜索三个不同目录下的 .c 格式的源文件 |
4.5 patsubst函数
这个函数的功能是按照指定的模式替换指定文件名的后缀。
例子:
1 | src = a.cpp b.cpp c.cpp e.cpp # 把变量 src 中的所有文件名的后缀从 .cpp 替换为 .o |
补:在命令前面加上-,当该命令不能执行时,也可以继续执行下面的的命令(-mkdir /abc)
5. gdb调试
5.1 要求
- 程序必须是自己编写的(能完全看懂)
- 只能用来调试逻辑错误
- 必须添加-g参数,使用gcc编译生成的可执行文件才能条件
5.2 基础指令
- -g:必须使用该参数编译可执行文件(主要是链接阶段使用-g),否则没有调试表。
gcc gdbtest.c -o appgdb -g
- gdb ./a,out:通过gdb启动可执行文件(必须是要有调试表的)。
- list:list 1是列出源码,根据源码指定行号设置端点。1代表从第1行开始。
- b:b 55是在第55行添加端点。
- run或r:运行程序,启动调试
- 代码会自动运行,停止在端点处,端点对应的代码行,是没有执行的。
- n或next:下一条指令(越过函数,不进入函数)
- s或step:下一条执行(进入函数)
- p或print:打印变量值,如 p var 是查看var变量的值
- continue:继续执行端点后续的指令,到下一个端点处,如果没有了就结束程序了
- finish:结束当前函数调用
- quit:退出当前gdb调试
5.3 其它指令
start:不使用断点,直接启动程序,开始单步调试(从main函数的第一条语句开始)
run或r:找出程序出现段错误的位置。
- 用法:gdb启动调试,直接run,停止的位置,就是出现段错误的代码位置
设置main函数命令行参数:
- set args 参1 参2 参3 … (在start/run之前设置)
- run 参1 参2 参3…
b 23 if i=5:设置条件断点,只有满足该条件时,断点才生效
设置断点生效、失效:
- disable 2 :设置编号为2号的断点失效,使用info b查看
- enable 3 : 设置编号为3号的断点生效,使用info b查看
delete 1 :删除编号为1号的断点
ptype:查看变量类型
display:设置跟踪变量
- display i:跟踪变量i(每次n时,都会打印i的值)
undisplay:取消跟踪变量,使用跟踪变量的编号
- undisplay 2:取消编号为2的变量跟踪
bt:列出当前程序,正存活着的栈帧
frame:根据栈帧编号,切换栈帧
5.4 具体步骤
1.先编译可执行文件,带有gdb调试表:gcc gdbtest.c -o appgdb -g
2.启动gdb:gdb appgdb
3.列出源码(从第1行开始,就是1):list 1
4.设置断点(比如说在代码的第52行设置):b 52
5.查看设置的所有断点:info b
6.启动调试:run