04 linux之C 语言高级编程
gcc和gdb
GNU工具
- 编译工具:把一个源程序编译为一个可执行程序
- 调试工具:能对执行程序进行源码或汇编级调试
- 软件工程工具:用于协助多人开发或大型软件项目的管理,如make、CVS、Subvision
- 其他工具:用于把多个目标文件链接成可执行文件的链接器,或者用作格式转换的工具。
GCC
GCC简介:
- 全称为GNU CC ,GNU项目中符合ANSI C标准的编译系统
- 编译如C、C++、Object C、Java、Fortran、Pascal、Modula-3和Ada等多种语言
- GCC是可以在多种硬体平台上编译出可执行程序的超级编译器,其执行效率与一般的编译器相比平均效率要高20%~30%
- 一个交叉平台编译器 ,适合在嵌入式领域的开发编译
- GNU Compiler Collection
- C, C++, Objective-C, Fortran, Java, Ada
gcc所支持后缀名解释:
- .c C原始程序
- .C/.cc/.cxx C++原始程序
- .m Objective-C原始程序
- .i 已经过预处理的C原始程序
- .ii 已经过预处理的C++原始程序
- .s/.S 汇编语言原始程序
- .h 预处理文件(头文件)
- .o 目标文件
- .a/.so 编译后的库文件
编译器的主要组件
- 分析器:分析器将源语言程序代码转换为汇编语言。因为要从一种格式转换为另一种格式(C到汇编),所以分析器需要知道目标机器的汇编语言。
- 汇编器:汇编器将汇编语言代码转换为CPU可以执行字节码。
- 链接器:链接器将汇编器生成的单独的目标文件组合成可执行的应用程序。链接器需要知道这种目标格式以便工作。
- 标准C库:核心的C函数都有一个主要的C库来提供。如果在应用程序中用到了C库中的函数,这个库就会通过链接器和源代码连接来生成最终的可执行程序。
GCC的基本用法和选项
Gcc最基本的用法是∶gcc [options] [filenames]
- -c,只编译,不连接成为可执行文件,编译器只是由输入的.c等源代码文件生成.o为后缀的目标文件,通常用于编译不包含主程序的子程序文件。
- -o output_filename,确定输出文件的名称为output_filename,同时这个名称不能和源文件同名。如果不给出这个选项,gcc就给出预设的可执行文件a.out。
- -g,产生符号调试工具(GNU的gdb)所必要的符号资讯,要想对源代码进行调试,我们就必须加入这个选项。
- -O,对程序进行优化编译、连接,采用这个选项,整个源代码会在编译、连接过程中进行优化处理,这样产生的可执行文件的执行效率可以提高,但是,编译、连接的速度就相应地要慢一些。
- -O2,比-O更好的优化编译、连接,当然整个编译、连接过程会更慢。
- -I dirname,将dirname所指出的目录加入到程序头文件目录列表中,是在预编译过程中使用的参数。
- -L dirname,将dirname所指出的目录加入到程序函数档案库文件的目录列表中,是在链接过程中使用的参数。
GCC的错误类型及对策
-
第一类∶C语法错误
错误信息∶文件source.c中第n行有语法错误(syntex errror)。有些情况下,一个很简单的语法错误,gcc会给出一大堆错误,我们最主要的是要保持清醒的头脑,不要被其吓倒,必要的时候再参考一下C语言的基本教材。
-
第二类∶头文件错误
错误信息∶找不到头文件head.h(Can not find include file head.h)。这类错误是源代码文件中的包含头文件有问题,可能的原因有头文件名错误、指定的头文件所在目录名错误等,也可能是错误地使用了双引号和尖括号。
-
第三类∶档案库错误
错误信息∶链接程序找不到所需的函数库(ld: -lm: No such file or directory )。这类错误是与目标文件相连接的函数库有错误,可能的原因是函数库名错误、指定的函数库所在目录名称错误等,检查的方法是使用find命令在可能的目录中寻找相应的函数库名,确定档案库及目录的名称并修改程序中及编译选项中的名称。
-
第四类∶未定义符号
错误信息∶有未定义的符号(Undefined symbol)。这类错误是在连接过程中出现的,可能有两种原因∶一是使用者自己定义的函数或者全局变量所在源代码文件,没有被编译、连接,或者干脆还没有定义,这需要使用者根据实际情况修改源程序,给出全局变量或者函数的定义体;二是未定义的符号是一个标准的库函数,在源程序中使用了该库函数,而连接过程中还没有给定相应的函数库的名称,或者是该档案库的目录名称有问题,这时需要使用档案库维护命令ar检查我们需要的库函数到底位于哪一个函数库中,确定之后,修改gcc连接选项中的-l和-L项。
GCC编译过程
GCC的编译流程分为四个步骤:
- 预处理(Pre-Processing)
- 编译(Compiling)
- 汇编(Assembling)
- 链接(Linking)
生成预处理代码
$ gcc –E test.c -o test.i
用wc
命令,查看这两个阶段代码大小:$ wc test.c test.i
9 16 127 test.c
842 1934 16498 test.i
851 1950 16625 总用量
test.i比test.c增加了很多内容,主要是放在系统提供的include文件中的。
生成汇编代码
检查语法错误,并生成汇编文件
$ gcc –S test.c –o test.s
生成目标代码
- 方法一,用gcc直接从C源代码中生成目标代码:
$ gcc –c test.s –o test.o
- 方法二,用汇编器从汇编代码生成目标代码:
$ as test.s –o test.o
生成可执行程序
将目标程序链接库资源,生成可执行程序$ gcc test.s –o test
、再运行./test
。
Gdb调试流程
- 首先使用gcc对test.c进行编译,注意一定要加上选项‘
-g
’ - 设置断点后程序在指定行之前停止。
- 只有在代码处于“运行”或“暂停”状态时才能查看变量值。
# gcc -g test.c -o test
# gdb test
命令 | 按键 |
---|---|
查看断点情况 | (gdb) info b |
单步运行 | (gdb) n/(gdb) s |
查看文件 | (gdb) l |
设置断点 | (gdb) b 6 |
查看变量值 | (gdb) p n |
运行代码 | (gdb) r |
恢复程序运行 | (gdb) c |
帮助 | (gdb) help [command] |
退出 | (gdb) q |
条件编译
编译器根据条件的真假决定是否编译相关的代码。
其语法如下:
#ifdef <macro>
……
#else
……
#endif
根据宏是否定义
定义:
根据宏的值
还有一种方式就是直接放数值0、1。
内存管理
C/C++定义了4个内存区间:
- 代码区/全局变量与静态变量区/局部变量区即栈区/动态存储区,即堆区。
静态存储分配
- 通常定义变量,编译器在编译时都可以根据该变量的类型知道所需内存空间的大小,从而系统在适当的时候为他们分配确定的存储空间。
- 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
动态存储分配
有些操作对象只有在程序运行时才能确定,这样编译器在编译时就无法为他们预定存储空间,只能在程序运行时,系统根据运行时的要求进行内存分配,这种方法称为。
所有动态存储分配都在堆区中进行。
从堆上分配,亦称动态内存分配。程序在运行的时候用malloc申请任意多少的内存,程序员自己负责在何时用free释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
堆内存的分配与释放
当程序运行到需要一个动态分配的变量或对象时,必须向系统申请取得堆中的一块所需大小的存贮空间,用于存贮该变量或对象。当不再使用该变量或对象时,也就是它的生命结束时,要显式释放它所占用的存贮空间,这样系统就能对该堆空间进行再次分配,做到重复使用有限的资源。
堆区是不会自动在分配时做初始化的(包括清零),所以必须用初始化式(initializer)来显式初始化。
malloc/free
void * malloc(size_t num)
:分配空间
void free(void *p)
:释放空间
- malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
- malloc申请到的是一块连续的内存,有时可能会比所申请的空间大。其有时会申请不到内存,返回NULL。
- malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。
- 如果free的参数是NULL的话,没有任何效果。
- 释放一块内存中的一部分是不被允许的。
注意事项:
- 删除一个指针p(
free(p);
),实际意思是删除了p所指的目标(变量或对象等),释放了它所占的堆空间,而不是删除p本身,释放堆空间后,p成了空悬指针。 - 动态分配失败。返回一个空指针(NULL),表示发生了异常,堆资源不足,分配失败。
- malloc与free是配对使用的, free只能释放堆空间。如果malloc返回的指针值丢失,则所分配的堆空间无法回收,称内存泄漏,同一空间重复释放也是危险的,因为该空间可能已另分配,所以必须妥善保存malloc返回的指针,以保证不发生内存泄漏,也必须保证不会重复释放堆内存空间。
野指针
不是NULL指针,是指向“垃圾”内存的指针。“野指针”是很危险的。 “野指针”的成因主要有两种:
- 指针变量没有被初始化。
- 指针p被free之后,没有置为NULL,让人误以为p是个合法的指针。指针操作超越了变量的作用范围。这种情况让人防不胜防。
Makefile
Make简介
工程管理器,顾名思义,是指管理较多的文件。
Make工程管理器也就是个“自动编译管理器”,这里的“自动”是指它能够根据文件时间戳自动发现更新过的文件而减少编译的工作量,同时,它通过读入Makefile文件的内容来执行大量的编译工作。
Make将只编译改动的代码文件,而不用完全编译。
Makefile基本结构
Makefile是Make读入的唯一配置文件。
- 由make工具创建的目标体(target),通常是目标文件或可执行文件。(生成什么)
- 要创建的目标体所依赖的文件(dependency_file)(由谁生成)
- 创建每个目标体时需要运行的命令(command)(怎么生成)
注意:命令行前面必须是一个“TAB键”,否则编译错误为:*** missing separator. Stop。
Makefile格式:
target : dependency_files
<TAB> command
例子:
f1.c:
f2.c:
head.h:
main.c:
通过makefile生成一个目标代码文件:(.c->.o->obj)。
如果有一个文件和clean重命名,报make:'test' is up to date.
的话,我们可以加一行语句,生成一个假目标,就可以继续执行了:
创建和使用变量
创建变量的目的:用来代替一个文本字符串:
- 系列文件的名字
- 传递给编译器的参数
- 需要运行的程序
- 需要查找源代码的目录
- 你需要输出信息的目录
- 你想做的其它事情。
变量定义的两种方式
-
递归展开方式
VAR=var
它可以向后引用变量,但不能对该变量进行任何扩展,例如
CFLAGS = $(CFLAGS) -O
-
简单方式
VAR:=var
用这种方式定义的变量,会在变量的定义点,按照被引用的变量的当前值进行展开,这种定义变量的方式更适合在大的编程项目中使用,因为它更像我们一般的编程语言。
变量的使用:$(VAR)
。如果想用"$
"字符的话必须要用"$$
"。变量名一般大写。
用?=定义变量
dir := /foo/bar
FOO ?= bar
含义是,如果FOO没有被定义过,那么变量FOO的值就是“bar”,如果FOO先前被定义过,那么这条语将什么也不做,其等价于:
ifeq ($(origin FOO), undefined)
FOO = bar
endif
为变量添加值
可以通过+=
为已定义的变量添加新的值
Main=hello.o hello-1.o
Main+=hello-2.o
预定义变量
变量名 | 作用 |
---|---|
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编译器的选项,无默认值。 |
自动变量
变量名 | 作用 |
---|---|
$* | 不包含扩展名的目标文件名称 |
$+ | 所有的依赖文件,以空格分开,并以出现的先后为序,可能 包含重复的依赖文件 |
$< | 第一个依赖文件的名称 |
$? | 所有时间戳比目标文件晚的的依赖文件,并以空格分开 |
$@ | 目标文件的完整名称 |
$^ | 所有不重复的目标依赖文件,以空格分开 |
$% | 如果目标是归档成员,则该变量表示目标的归档成员名称 |
环境变量
- make在启动时会自动读取系统当前已经定义了的环境变量,并且会创建与之具有相同名称和数值的变量
- 如果用户在Makefile中定义了相同名称的变量,那么用户自定义变量将会覆盖同名的环境变量
Make使用
直接运行make。
选项
-C dir
读入指定目录下的Makefile-f file
读入当前目录下的file文件作为Makefile-i
忽略所有的命令执行错误-I dir
指定被包含的Makefile所在目录-n
只打印要执行的命令,但不执行这些命令-p
显示make变量数据库和隐含规则-s
在执行命令时不显示命令-w
如果make在执行过程中改变目录,打印当前目录名
Makefile的隐含规则
隐含规则1:编译C程序的隐含规则
- “
.o”的目标的依赖目标会自动推导为“ .c”,并且其生成命令是“\((CC) –c\)(CPPFLAGS) $(CFLAGS)”
隐含规则2:链接Object文件的隐含规则
- “
” 目标依赖于“ .o”,通过运行C的编译器来运行链接程序生成(一般是“ld”),其生成命令是:“\((CC)\)(LDFLAGS) .o \((LOADLIBES)\)(LDLIBS)”。这个规则对于只有一个源文件的工程有效,同时也对多个Object文件(由不同的源文件生成)的也有效。例如如下规则:
x : x.o 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
如果没有一个源文件(如上例中的x.c)和你的目标名字(如上例中的x)相关联,那么,你最好写出自己的生成规则,不然,隐含规则会报错的。
比如:下面的test就会报错,而要用f1
或者f2
、main
。
.c会自动生成.o,把三个.o生成target。
VPATH的用法
VPATH : 虚路径
在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当make需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make在自动去找。
Makefile文件中的特殊变量“VPATH”就是完成这个功能的,如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。
VPATH = src:../headers
上面的的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)
没用vpath:
用vpath:
Makefile的嵌套
下面是一个例子:
当前路径下的makefile文件:
当前目录下的所有文件和嵌套文件:
f1、f2、main下面的makefile文件内容:与下面类似。
这个执行过程就是当前下的makefile会把变量值export
传给各个嵌套文件夹下的makefile文件。就可以实现嵌套了。
执行结果: