GCC是GNU开发的编程语言套件(GNU Compiler Collection),在默认情况下ubuntu不会提供C/C++的编译环境,ubuntu提供了build-essential包可以一次性把相关软件安装好:
sudo apt install build-essential
apt depends build-essential #查看哪些包被build-essential依赖
一个C语言程序的运行准备工作:
源程序通过GCC编译为汇编程序,汇编程序进一步编译到输出文件(Linux下为*.o,Windows下为*.obj),输出文件通过链接器连接得到可执行文件,加载到内存运行;
当文件过多时,一个一个进行gcc编译很麻烦,所以会借助makefile构建工程,帮助开发者组织程序
现在已经新建了一个C程序hello.c:
下一步借助工具gcc进行编译链接得到可执行文件a.out:
gcc hello.c
./a.out #执行a.out文件
通过对常用用法的举例,实际体会文件运行的过程;
1.源程序预处理
依然是对hello.c文件进行编译,通过-E参数进行预处理,-o参数指定输出文件名:
gcc -E -o hello.cpp hello.c
借助vi打开hello.cpp发现:
hello.c中的内容在预处理文件的最后,前面都是新载入的预处理语句;
2.编译到汇编文件
将预处理后的hello.cpp编译到汇编文件hello.s:
gcc -x cpp-output -S -o hello.s hello.cpp
如果要直接将C源程序编译到汇编文件dihello.s则可以:
gcc -S -o dihello.s hello.c
3.将汇编程序编译到机器码
现在将汇编程序编译到机器码:
gcc -x assembler -c hello.s -o hello.o
#直接利用源程序编译到机器码
gcc -c hello.c -o hello.o
用vi打开机器码观察到:
虽然已经是机器码,但依然不能执行,因为还没有经过链接,所以机器依然不知道这个机器码文件想要发出什么指令;
4.链接生成可执行文件
通过gcc可以直接自动链接到库,生成可执行文件:
gcc -o hello hello.o
#也可以直接从源程序到可执行文件
gcc -o hello hello.c
#执行
./hello
make是一个命令工具,是一个解释Makefile中指令的工具,一般来说,大多数IDE都有这个命令,比如:Linux下GNU的make,Visual C++的nmake;
make命令执行时,需要一个Makefile文件,用以告诉make命令需要如何编译和链接程序;
Makefile被称为工程文件,每个工程都有工程文件,工程文件的作用:
可以看出,基于make,大幅度节约了构建工程的时间;
makefile的格式为:
<target>:<prerequisites>
[tab]<commands> #任意的shell命令
makefile是shell脚本的进一步封装,专用于工程构建;
makefile如何工作
1.make会在当前目录下找名字叫"Makefile"或"makefile"的文件;
2.如果make命令运行时没有指定目标,默认会执行Makefile文件的第一个目标,第一个目标习惯写为all;
3.make会一层一层找目标的依赖关系;
4.目标的前置依赖都执行完毕后,执行该目标下的
假设当前目录下有几个头文件.h,几个C源文件.c,一个文件makefile,一个README.md,一个LICENSE,在当前目录执行make
能够自动对文件编译并链接再输出可执行文件;
目标target
一个目标构成一条规则,目标通常是文件名,指明make命令要构建的对象,目标可以是一个文件名,也可以是多个文件名,之间用空格分隔;
目标还可以是某个操作的名字,这称为"伪目标"(phony target),比如clean,如果当前目录中,正好有一个文件叫clean,那么这个目标不会执行。因为make发现clean文件已经存在,就认为没有必要重新构建,从而不会执行clean"伪目标",为了避免这种情况,可以明确声明clean是"伪目标":".PHONY:clean
"
前置条件prerequisites
前置条件通常是一组文件名,之间用空格分隔,前置条件决定了目标是否需要重新构建:只要有一个前置文件不存在,或者有过更新(根据时间戳判断),"目标"就需要重新构建
命令commands
命令表示如何更新目标文件,由一行或多行shell命令组成,它是构建目标的具体指令,它的运行结果通常就是生成目标文件;
每行命令之前必须有一个tab键,如果想用其他键,可以用内置变量.RECIPEPREFIX
声明。用.RECIPEPREFIX
指定大于号>
替代tab键:
.RECIPEPREFIX = >
需要注意的是,每行命令在一个单独的shell中执行,这些shell之间没有继承关系,即孤立的命令;
这是文件的依赖关系,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中
1.makefile中用#
表示注释;
2.正常情况下,make会打印每条命令,然后再执行,这叫回声(echoing),在命令前加上@,可以关闭回声;
3.通配符(wildcard)用来指定一组符合条件的文件名,makefile的通配符与Bash一致,主要有*
,?
等,比如*.o
表示所有后缀名为o的文件;
模式匹配
make命令允许对文件名进行正则运算匹配,主要用到的匹配符号是%
,假设当前目录下有f1.c和f2.c两个文件,需要将其编译为输出文件,则可简写为%.c:%.o
;
基于通配符%
,可以将大量同类型文件使用一条规则完成构建;
变量和赋值
1.使用等号自定义变量,调用时需写在$()
中:
txt = Hello
echo $(txt)
2.调用shell变量,需要在美元符号前,再加一个美元符号(因为make命令会对美元符号转义):
echo $$HOME
3.变量的值可能指向另一个变量,例如v1 = $(v2)
;
赋值运算符
VARIABLE = value #执行时扩展
VARIABLE := value #定义时扩展
VARIABLE ?= value #变量为空时才设置值
VARIABLE += value #将值追加到变量的尾部
内置变量Implicit Variables
make命令提供了一系列的内置变量,主要是为了跨平台的兼容;
$(CC)
指向当前使用的编译器,$(MAKE)
指向当前使用的make工具;
自动变量Automatic Variables
$@ 指代当前目标,即make命令当前构建的目标
$< 指代第一个前置条件
$? 指代比目标更新的所有前置条件,之间用空格分隔
$^ 指代所有前置条件,之间用空格分隔
$(@D)和$(@F) 分别指向$@的目录名和文件名
$(<D)和$(<F) 分别指向$<的目录名和文件名
判断和循环
Makefile使用Bash语法完成循环和判断:
#判断
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
#循环
LIST = one two three
all:
for i in $(LIST); do
echo $$i;
done
函数
makefile也可以使用函数,格式为:
$(function arguments)
#或者
${function arguments}
假设现在有一个Makefile文件:
#cleanall cleanobj cleandiff均不是文件
#如果目录下有例如cleanobj的文件,将不会构建目标cleanobj
#为了避免这一现象,可以用.PHONY声明是伪目标
.PHONY:cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
前面已经提到,make指令依据Makefile的描述去执行工程构建的操作,现在目录下有文件:
如果执行:
make cleanobj
会选择出输出文件*.o,并删除;
如果执行:
make cleanall
根据回声echoing看出:
执行顺序是深度优先搜索,先执行前置条件,再执行目标;
如果只输入指令make,会默认执行第一个目标
原始的调试是在源码中插入printf逐步打印变量状态,后来出现了专用的调试工具GDB,将程序装载到GDB下更方便进行调试
GDB:GNU project Debugger;在开始调试前,必须用程序中的调试信息编译要调试的程序,GDB才能调试所使用的变量,函数;首先,使用gcc -g
编译得到程序,参数-g会附加符号信息用于调试:
gcc -g -o hello hello.c
在shell中,可以使用gdb命令并指定程序名参数运行gdb:
gdb hello
程序装入gdb后可以运行
run
或者在gdb中,使用file命令调试:
file hello
如果一切正常,程序将执行到结束,如果出错,gdb会中断程序,让开发者检查变量状态;
退出gdb:
quit
可以在源码的某一行设置断点,gdb会在遇到断点时中断执行:
gdb hello #将hello.c编译后的程序hello载入gdb
break main #在main函数行设置断点
break 6 #在源码hello.c第6行设置断点
next #单步执行,子函数作为一步
step #单步执行,具体到子函数的每一语句
info break #列出当前设置的所有断点
back trace可以查看函数调用的层级:
bt