LinuxC语言编程02:编译与调试C程序
编译器与调试器
编译器GCC
GCC编译过程
GCC编译过程分为4步:
- 预处理(Pre-Processing): 进行头文件展开,宏替换,条件编译,生成预处理文件
*.i
. - 编译(Compiling): 将预处理文件编译成汇编代码
*.s
. - 汇编(Assembling): 将汇编代码编译成二进制库文件
*.o
. - 链接(Linking): 将二进制文件链接起来,生成可执行文件
a.out
.
GCC的运行
GCC命令格式如下:
gcc [options] [filenames]
常用参数如下:
参数 | 意义 |
---|---|
-o <filename> | 指定生成的可执行文件名为filename ,默认生成的可执行文件名为a.out |
-Wall | 显示所有警告 |
-E | 只激活预处理,需要将生成重定义到一个文件中 |
-S | 只激活预处理和编译,生成汇编代码 |
-c | 只激活预处理,编译和汇编,生成obj文件 |
-O | 优化编译链接,生成的可执行文件效率较高,但是编译,链接的速度会变慢 |
-I <dirname> | 指定优先在dirname 目录下查找头文件,作用于预编译过程 |
-L <dirname> | 指定优先在dirname 目录下查找库文件,作用链接过程 |
调试器GDB
在使用gcc
命令编译时,加上参数-g
,生成的可执行文件就可以用GDB进行调试了.使用gdb <可执行文件名>
命令进入调试状态.
gcc hello.c -o hello
gdb hello
在调试界面,输入指令即可执行对应的功能:
指令 | 简写 | 功能 |
---|---|---|
list [num] | l [num] | 从显示第num 行开始的源代码 |
break [num] | b [num] | 在第num 行设置断点 |
run | r | 重新从头运行程序 |
continue | c | 继续运行程序 |
next | n | 单步执行,不进入函数体内部 |
step | s | 单步执行,进入函数体内部 |
print [var] | p [var] | 打印变量var 的值和地址 |
set <var>=<value> | s <var>=<value> | 将变量var 的值设为value |
backtrace | bt | 查看当前的函数栈帧 |
info breakpoints | info b | 查看当前设置的所有断点 |
构建工具make
使用GNU make工具可以管理项目并进行自动化编译.它可以根据时间戳自动发现更新过的文件,在每次执行编译时,只编译那些改动的代码文件,而不用完全编译,以减少编译的工作量.
关于make的使用,推荐一篇文章跟我一起写Makefile(https://seisman.github.io/how-to-write-makefile/index.html).
Makefile的基本结构
make工具根据项目目录下名为Makefile
的配置文件中定义的编译规则来执行编译过程.一个Makefile文件是由很多条规则组成的,一条规则的语法如下:
target : prerequisites
command
target
: 生成的目标文件(object file)或一个标签(label).prerequisites
: 生成该target
所依赖的文件或其它target
.command
: 生成target
过程中要执行的shell命令.
使用make <targetname>
命令以执行targetname
目标文件对应的指令(若存在依赖文件,则先执行依赖文件对应的指令);若不指定targetname
,则构建Makefile文件中的第一个目标.
下面例子演示一个典型的Makefile文件的编写:
-
编写C文件
file1.c
,file2.c
,head.h
和main.c
,内容分别如下:void print_file1() { printf("this is file1\n"); }
void print_file2() { printf("this is file2\n"); }
void print1(); void print2();
#include<stdio.h> #include"head.h" int main(const int argc, const char *argv[]) { print1(); print2(); printf("this is main\n"); return 0; }
-
根据上述文件结构,编写Makefile文件如下:
test : file1.o file2.o main.o # 目标文件为可执行文件test gcc file1.o file2.o main.o -o test file2.o : file2.c # 目标文件为库文件file2.o gcc -c -Wall file2.c -o file2.o file1.o : file1.c # 目标文件为库文件file1.o gcc -c -Wall file1.c -o file1.o main.o : main.c # 目标文件为库文件main.o gcc -c -Wall main.c -o main.o .PHONY : clean # 声明目标clean是伪目标,没有生成对应的文件 clean : # 没有目标文件,该目标的作用是清理编译过程中产生的垃圾文件 rm -f *.o test
-
执行
make
或make test
以编译生成可执行文件test
,在这个过程中依次执行了test
,file1.o
,file2.o
,main.o
所对应的命令,命令行输出如下:gcc -c -Wall file1.c -o file1.o gcc -c -Wall file2.c -o file2.o gcc file1.o file2.o main.o -o test gcc -c -Wall main.c -o main.o
因为
clean
没有被关联依赖到,所以不会被执行.当程序执行完我们可以显示调用make clean
来清理编译过程产生的垃圾文件.
像clean
这种不对应文件输出的目标被称为伪目标,为防止当前目录下的同名文件干扰编译过程,需要使用.PHONY
声明其为伪目标.
make的运行参数
make命令的常用运行参数如下:
运行参数 | 意义 |
---|---|
-C <dir> | 指定读取目录<dir> 下的Makefile文件 |
-f <filename> | 指定读取名为filename 的Makefile文件 |
-I <dir> | 指定在目录<dir> 下搜索被包含的Makefile文件 |
-n | 只打印要被执行的命令,但不执行它们(模拟执行) |
-i | 在执行时忽略所有的错误 |
-s | 在执行时不显示执行的命令 |
-w | 在执行过程中若改变目录,则打印目录名 |
变量
在Makefile中定义和使用变量本质上是在进行字符串展开,类似C语言的宏定义.
变量的定义与使用
-
变量的声明有两种方式:
-
简单方式: 使用
:=
运算符为变量赋值.使用这种赋值方式只能引用前面已经定义好的变量,如:x := foo y := $(x) bar x := later
变量
x
的值为"later"
,变量y
的值为"foo bar"
. -
递归展开方式: 使用
=
运算符为变量赋值.这种定义方式可以引用在脚本任何地方定义的变量(包括后面定义的变量),这有可能带来循环引用的问题.例如下面的代码,变量
A
与变量B
互相引用,带来了循环引用的问题,make会报错.A = $(B) B = $(A)
-
-
使用变量的方式: 使用
$(变量名)
引用变量.
通过定义变量,我们可以改进之前的Makefile:
OBJS = file1.o file2.o main.o
CFLAGS = -c -Wall
test : $(OBJS) # 目标文件为可执行文件test
gcc $(OBJS) -o test
file2.o : file2.c # 目标文件为库文件file2.o
gcc $(CFLAGS) file2.c -o file2.o
file1.o : file1.c # 目标文件为库文件file1.o
gcc $(CFLAGS) file1.c -o file1.o
main.o : main.c # 目标文件为库文件main.o
gcc $(CFLAGS) main.c -o main.o
.PHONY : clean # 声明目标clean是伪目标,没有生成对应的文件
clean : # 没有目标文件,该目标的作用是清理编译过程中产生的垃圾文件
rm -f *.o test
自动化变量
自动化变量 | 含义 |
---|---|
$@ | 目标文件的完整名称 |
$% | 若目标是库文件,则表示库文件的完整名称 |
$< | 第一个依赖文件的名称 |
$^ | 所有不重复的目标依赖文件,以空格分隔 |
$? | 所有更新的不重复的目标依赖文件,以空格分隔 |
$+ | 所有目标依赖文件(有可能重复),以空格分隔 |
通过使用自动化变量,我们可以改进之前的Makefile:
OBJS = file1.o file2.o main.o
CFLAGS = -c -Wall
test : $(OBJS)
gcc $^ -o $@
file2.o : file2.c
gcc $(CFLAGS) $^.c -o $@
file1.o : file1.c
gcc $(CFLAGS) $^ -o $@
main.o : main.c
gcc $(CFLAGS) $^ -o $@
.PHONY : clean
clean :
rm -f *.o test
环境变量
make在启动时会读取当前系统中的环境变量,并支持在Makefile文件中改变它们.因此要注意我们定义的变量名不应覆盖现有的环境变量名.
隐含规则
编译C程序的隐含规则
<n>.o
目标的依赖目标会自动推导为<n>.c
,并且其生成命令是$(CC) –c $(CPPFLAGS) $(CFLAGS)
.其中预定义变量CC
为C编译器的名称,默认为cc
,变量CPPFLAGS
和CFLAGS
变量默认为空.
根据该隐含规则,我们可以改进之前的Makefile:
OBJS = file1.o file2.o main.o
CFLAGS = -c -Wall
test : $(OBJS)
gcc $^ -o $@
# 根据隐含规则,省略对file1.o,file2.o,main.o文件生成规则的声明
.PHONY : clean
clean :
rm -f *.o test
链接库文件的隐含规则
<n>
目标的依赖目标会自动推导为<n>.o
,并且其生成命令是$(CC) $(LDFLAGS) <n>.o $(LOADLIBES) $(LDLIBS)
.其中预定义变量CC
为C编译器的名称,默认为cc
,变量CPPFLAGS
和CFLAGS
变量默认为空.
例如对于下面的规则:
x : y.o z.o
并且当x.c
,y.c
,z.c
文件都存在时,隐含规则将执行如下命令:
cc -c x.c -o x.o
cc -c y.c -o y.o
cc -c z.c -o z.o
cc x.o y.o z.o -o x
rm -f x.o
rm -f y.o
rm -f z.o
虚路径
变量VPATH
用于指定虚路径,其值应该是一系列以:
为分隔的路径,在编译时,make会到VPATH
指定的目录下寻找依赖文件.
例如我们将上面例子中的文件file1.c
移动到路径src1/
下,文件file2
移动到路径src2/
下,文件main.c
移动到路径main/
下,则我们的Makefile文件应该变成这样:
CFLAGS = -c -Wall
# 依赖的目标都需要写出完整路径
test : src1/file1.o src2/file2.o main/main.o
gcc src1/file1.o src2/file2.o main/main.o -o $@
src2/file2.o : src2/file2.c
gcc $(CFLAGS) $^.c -o $@
src1/file1.o : src1/file1.c
gcc $(CFLAGS) $^ -o $@
main/main.o : main/main.c
gcc $(CFLAGS) $^ -o $@
.PHONY : clean
clean :
find ./ -name "*.o" -exec rm {} ;; rm test
当我们定义VPATH
变量,指定寻找目标文件的路径后,Makefile会变得更简洁:
CFLAGS = -c -Wall
VPATH = src1:src2:main
# 依赖的目标都需要写出完整路径
test : file1.o file2.o main.o
gcc file1.o file2.o main.o -o $@
file2.o : file2.c
gcc $(CFLAGS) $^.c -o $@
file1.o : file1.c
gcc $(CFLAGS) $^ -o $@
main.o : main.c
gcc $(CFLAGS) $^ -o $@
.PHONY : clean
clean :
find ./ -name "*.o" -exec rm {} ;; rm test