使用 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
3
4
5
6
7
8
9
10
11
12
13
14
15
#编程:
#方法1:
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 #链接,生成可执行文件
./mycode1 #执行可执行文件

#方法2
gcc mycode1.c -o aa #直接生成可执行文件
./aa #执行可执行文件

#方法3
gcc mycode1.c #默认会生成a.out这个文件
./a.out #执行可执行文件

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++的理解,下边从三个方面介绍一下二者的区别:

  1. 在代码编译阶段(第二个阶段):

    • 后缀为 .c 的,gcc 把它当作是C程序,而 g++ 当作是 C++ 程序

    • 后缀为.cpp的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些

    • g++会调用gcc,对于C++代码,两者是等价的, 也就是说 gcc 和 g++ 都可以编译 C/C++代码

  2. 在链接阶段(最后一个阶段)

  • gcc 和 g++ 都可以自动链接到标准C库
  • g++ 可以自动链接到标准C++库, gcc如果要链接到标准C++库需要加参数 -lstdc++

综上所述:

  • 不管是 gcc 还是 g++ 都可以编译 C 程序,编译程序的规则和参数都相同
  • g++可以直接编译C++程序, gcc 编译 C++程序需要添加额外参数 -lstdc++

3. makefile规则语法格式

每条规则由三个部分组成分别是目标,依赖和命令。下面通过一个例子来阐述一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 举例1: 假设有源文件a.c、b.c、c.c和head.h, 现在需要生成可执行程序test1
# 有一个目标,多个依赖,一条命令
################# 例1 #################
test1:a.c b.c c.c
gcc a.c b.c c.c -o test1

################# 例2 #################
# 举例2: 假设有源文件a.c、b.c、c.c和d.c, 现在需要生成可执行程序test1,test2
# 有多个目标, 多个依赖, 多个命令
test1,test2:a.c b.c c.c d.c
gcc a.c b.c -o test1
gcc c.c d.c -o test2

################# 例3 #################
# 举例3: 假设有源文件a.c、b.c、c.c和head.h, 现在需要生成可执行程序test1
# 有一个目标,多个依赖,多条命令
test1:a.o b.o c.o
gcc a.o b.o c.o -o test1
# a.o 是第一条规则中的依赖
a.o:a.c
gcc -c a.c
# b.o 是第一条规则中的依赖
b.o:b.c
gcc -c b.c
# c.o 是第一条规则中的依赖
c.o:c.c
gcc -c c.c

4. 工作原理

在调用make命令编辑程序的时候,make会首先找到该目录下的makefile文件中的第一条规则,分析并执行相关操作,但需要注意的是,好多时候要执行的动作(命令)中使用的依赖是不存在的,如果使用的依赖不存在,这个动作也就不会被执行。

对应的解决方案是先将需要的依赖生成出来,我们就可以在makefile中添加新的规则,将不存在的依赖作为这个新的规则中的目标,当这条新的规则对应的命令执行完毕,对应的目标就被生成了,同时另一条规则中需要的依赖也就存在了。

这样,makefile中的某一条规则在需要的时候,就会被其他的规则调用,直到makefile中的第一条规则中的所有的依赖全部被生成,第一条规则中的命令就可以基于这些依赖生成对应的目标,make的任务也就完成了。

4.1 目标文件的更新

  1. 依赖文件存在,目标文件不存在,make就会根据依赖来生成目标文件

  2. 依赖和目标都是存在的:

    • 目标的时间大于依赖的时间,此时不更新目标文件;

    • 目标的时间小于依赖的时间,此时make会根据依赖更新目标文件;

4.2 自动推导

虽然make需要根据makefile中指定的规则来完成源文件的编译,但是我们会发现当漏写一些构建规则时,程序还是会被编译成功,这是因为make有自动推导的能力,不会完全依赖makefile。

注意:命令行前面是一个tab健距离;

4.3 变量

使用makefile进行规则定义的时候,为了写起来更加灵活,我们可以在里边使用变量。makefile中的变量分为三种:自定义变量、预定义变量和自动变量。

  1. 自定义变量:用makefile进行规则定义的时候,用户可以定义自己的变量,称为用户自定义变量。

  2. 预定义变量:在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
2
3
4
5
6
7
8
9
10
# 这是一个规则,普通写法
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc

# 这是一个规则,里边使用了自定义变量和预定义变量
obj=add.o div.o main.o mult.o sub.o
target=calc
CFLAGS=-O3 # 代码优化
$(target):$(obj)
$(CC) $(obj) -o $(target) $(CFLAGS)
  1. 自动变量:自动变量用来代表这些规则中的目标文件和依赖文件,并且它们只能在规则的命令中使用。
变量 含义
$* 表示目标文件的名称,不包含目标文件的扩展名
$+ 表示所有的依赖文件,这些依赖文件之间以空格分开,按照出现的先后为顺序,其中可能 包含重复的依赖文件
$< 表示依赖项中第一个依赖文件的名称
$? 依赖项中,所有比目标文件时间戳晚的依赖文件,依赖文件之间以空格分开
$@ 表示目标文件的名称,包含文件扩展名
$^ 依赖项中,所有不重复的依赖文件,这些文件之间以空格分开

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# *****************#例1********************
target=calc
obj=add.o div.o main.o mult.o sub.o
$(target):$(obj)
gcc $(obj) -o $(target)
#简化后的代码:$<和-c互换位置也可以
%.o:%.c
gcc $< -c
# ****************************************
# *****************#例2********************
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc
# 这是一个规则,里边使用了自定义变量
# 使用自动变量, 替换相关的内容
calc:add.o div.o main.o mult.o sub.o
gcc $^ -o $@ # 自动变量只能在规则的命令中使用

4.4 wildcard函数

这个函数的主要作用是获取指定目录下指定类型的文件名,器返回值是以空格分割的、指定目录下的所有符合条件的文件列表。

使用:$(wildcard,参数,参数,…..)

1
2
3
4
# 使用举例: 分别搜索三个不同目录下的 .c 格式的源文件
src = $(wildcard /home/robin/a/*.c /home/robin/b/*.c *.c) # *.c == ./*.c
# 返回值: 得到一个大的字符串, 里边有若干个满足条件的文件名, 文件名之间使用空格间隔
/home/robin/a/a.c /home/robin/a/b.c /home/robin/b/c.c /home/robin/b/d.c e.c f.c

4.5 patsubst函数

这个函数的功能是按照指定的模式替换指定文件名的后缀。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
src = a.cpp b.cpp c.cpp e.cpp              # 把变量 src 中的所有文件名的后缀从 .cpp 替换为 .o
obj = $(patsubst %.cpp, %.o, $(src)) # obj 的值为: a.o b.o c.o e.o

#**************修改后的代码*********************

target=calc
#搜索当前目录下的源文件
src=$(wildcard *.c)
#后缀的替换
obj=$(patsubst %.c,%.o,$(src))
$(target):$(obj)
gcc $(obj) -o $(target)
%.o:%.c
gcc $< .c
#构建伪目标
.PHONY:clean
clean:
rm $(obj) $(target) #添加伪目标后,以后可以直接通过make clean方便第删除*.p和calc文件

补:在命令前面加上-,当该命令不能执行时,也可以继续执行下面的的命令(-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