代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
1. Makefile
什么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个工作,但我觉得要作一个好的和professional的程序员,makefile还是要懂。这就好像现在有这么多的HTML的编辑器,但如果你想成为一个专业人士,你还是要了解HTML的标识的含义。特别在Unix下的软件编译,你就不能不自己写makefile了,会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。
因为,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Visual C++的nmake,Linux下GNU的make。
1.1 文件命名
- 两种书写格式:
- Makefile
- makefile
1.2 makefile中的规则
# 有了makefile, 这个文件中有多个规则, 执行make构建命令, 生成的是第一条规则的目标
gcc a.c b.c c.c -o app
# make的构建规则
目标 ... : 依赖 ...
命令 # 需要有一个tab缩进
...
# 目标: 最终要生成的文件, 一般写一个, 可以有多个, 通过命令生成的
# 依赖: 生成目标需要使用的文件, 在命令中被使用, 依赖可以有多个
# 命令: 一般是一个gcc命令, 通过对依赖进行编译生成目标, 命令可以有多个
# 参考:
$ gcc a.c b.c c.c -o app
# 改成一个规则
app:a.c b.c c.c
gcc a.c b.c c.c -o app
###################### 例子 #################
app:a.o b.o c.o
gcc a.o b.o c.o -o app
a.o:a.c
gcc -c a.c
b.o:b.c
gcc -c b.c
c.o:c.c
gcc -c c.c
1.3 makefile中的知识点
1.3.1 工作原理
- 命令在执行之前会检测依赖是否存在
- 如果依赖不存在:
- make会向下检测其他的规则, 看有没有一条规则是用来生成这个依赖的
- 如果找到了这个规则, 执行规则中的命令, 得到了需要的依赖
- 依赖存在就不向下检测了
- 如果依赖不存在:
- 关于文件更新 (不适用于伪目标)
- 执行make命令, 规则中的依赖和目标这些文件会进行一个文件时间的比较
- 如果目标文件不存在
- 目标文件被生成 -> 执行对应的命令
- 如果依赖时间早, 目标时间晚 -> 正常
- 目标不需要重写更新, 不需要重新生成, 使用旧的就可以
- 依赖时间晚, 目标的时间早 -> 不正常
- 说明依赖被重新修改 -> 目标需要重新生成
- 对应的命令就被执行了
- 说明依赖被重新修改 -> 目标需要重新生成
- 如果目标文件不存在
- 执行make命令, 规则中的依赖和目标这些文件会进行一个文件时间的比较
1.3.2 变量
-
自定义变量
# 没有类型 # 自定义变量, 变量名小写 变量名=变量值 # 举例: var=a.c b.c c.c # 取变量的值 $(变量名)
-
默认的自带变量
# 内部变量, 变量名大写 CC # 取值, gcc var=$(CC)
-
自动变量
# 只能在规则中的命令中使用 $@: 规则中的目标 $<: 规则中的第一个依赖 $^: 规则中的所有的依赖 # 例子 app:a.o b.o c.o gcc a.o b.o c.o -o app # 改进 app:a.o b.o c.o gcc $^ -o $@ # 例子 a.o:a.c gcc -c a.c # 改进 a.o:a.c gcc -c $< -o $@ gcc -c $^ -o $@
1.3.3 模式匹配
app:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o app
# 语法
%.o:%.c
gcc -c $< -o $@
%: 通配符, 前后对应的是同一字符串
1.3.4 函数
makefile中所有的函数都是有返回值的, 因此所有函数的样式:
$(函数名 参数1, 参数2, 参数3, ...)
-
获取指定目录下指定类型的文件列表 – wildcard
$(wildcard PATTERN...)
- 功能:
- PATTERN 指的是某个或多个目录下的对应的某种类型的文件
- 如果有多个目录,一般使用空格间隔
- 返回值:
- 得到的若干个文件的文件列表, 文件名之间使用空格间隔
- 示例:
$(wildcard *.c ./sub/*.c)
- 返回值格式: a.c b.c c.c d.c e.c f.c ./sub/aa.c ./sub/bb.c
- 功能:
-
模式字符串替换函数——patsubst
$(patsubst <pattern>,<replacement>,<text>) # 举例: # % 通配符 和第二个参数的 % 对应, 代表的是同一个字符串 $(patsubst %.c, %.o, a.c b.c d.c e.c) # 得到的返回值: a.o b.o d.o e.o
- 功能:
- 查找
中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式,如果匹配的话,则以模式替换。 - 这里,可以包括通配符
%
,表示任意长度的字串。如果中也包含%
,那么,中的这个%
将是中的那个%
所代表的字串。(可以用“\”来转义,以\%
来表示真实含义的%
字符)
- 查找
- 返回值:
- 函数返回被替换过后的字符串。
- 示例:
$(patsubst %.c, %.o, x.c.c bar.c)
- 把字串“x.c.c bar.c”符合模式[%.c]的单词替换成[%.o]
- 返回结果是“x.c.o bar.o”
- 功能:
1.4 makefile的编写
# 环境准备:
.
├── add.c
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c
版本1
# 如果对其中某个.c进行修改, 需要重新编译, 会编译多少个文件: 全部
# 效率很低
app:add.c div.c main.c mult.c sub.c
gcc add.c div.c main.c mult.c sub.c -o app
# 改进方案: 将这个唯一的规则拆分为若干个
版本2
# 这个版本只适合文件比较少的情况, 太冗余
# 使用变量进行精简
app:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o app
add.o:add.c
gcc -c add.c
div.o:div.c
gcc -c div.c
main.o:main.c
gcc -c main.c
mult.o:mult.c
gcc -c mult.c
sub.o:sub.c
gcc -c sub.c
版本3
# 缺点: 如果源文件比较多, 指定.o时候很麻烦
# 改进: 自动搜索指定目录下的源文件(.c), 将.c -> .o文件就可以了
# 需要使用makefile中的函数
target=app
objs=add.o div.o main.o mult.o sub.o
$(target):$(objs)
gcc $(objs) -o $(target)
%.o:%.c
gcc -c $< -o $@
版本4
# 缺点: 功能不完善, 只能编译代码, 不能删除生成的目标
# 改进: 添加一个规则用来删除文件
target=app
# 搜索指定目录下的指定格式的文件 .c
src=$(wildcard ./*.c)
# 将得到的文件列表中文件的后缀.c -> .o
objs=$(patsubst %.c, %.o, $(src))
$(target):$(objs)
gcc $(objs) -o $(target)
%.o:%.c
gcc -c $< -o $@
版本5
target=app
# 搜索指定目录下的指定格式的文件 .c
src=$(wildcard ./*.c)
# 将得到的文件列表中文件的后缀.c -> .o
objs=$(patsubst %.c, %.o, $(src))
$(target):$(objs)
gcc $(objs) -o $(target)
%.o:%.c
gcc -c $< -o $@
# 将clean声明为伪目标
.PHONY:clean
clean:
rm $(objs) $(target)
##############################################################
# 伪目标: 这个目标在磁盘上没有实体, 不对应一个磁盘文件, 不会进行依赖和目标的时间检测
# 需要关键字:
.PHONY:伪目标文件的名字
2. GDB调试
gdb 是由 GNU 软件系统社区提供的调试器,同 gcc 配套组成了一套完整的开发环境,可移植性很好,支持非常多的体系结构并被移植到各种系统中(包括各种类 Unix 系统与 Windows 系统里的 MinGW 和 Cygwin )。此外,除了 C 语言之外,gcc/gdb 还支持包括 C++、Objective-C、Ada 和 Pascal 等各种语言后端的编译和调试。 gcc/gdb 是 Linux 和许多类 Unix 系统中的标准开发环境,Linux 内核也是专门针对 gcc 进行编码的。
gdb 的吉祥物是专门捕杀 bug 的射手鱼:
For a fish, the archer fish is known to shoot down bugs from low hanging plants by spitting water at them.
2.1 调试准备
通常,在为调试而编译时,我们会(在尽量不影响程序行为的情况下)关掉编译器的优化选项(
-O
), 并打开调试选项(-g
)。另外,-Wall
选项打开所有 warning,也可以发现许多问题,避免一些不必要的 bug:
$ gcc -g -Wall program.c -o program
-g
选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb能找到源文件。
2.2 gdb调试命令
1.gdb启动、传参、和退出
# 1.启动, shell命令
gdb 可执行程序
# 2.给程序传递参数
(gdb) set args 10 9
(gdb) show args
Argument list to give program being debugged when it is started is "10 9".
# 3.退出gdb
(gdb) quit //q == quit
2.查看代码
1.当前文件
# 1.从默认位置显示代码
(gdb) l
(gdb) list
# 2.从指定的行显示
(gdb) l 行号
# 3.从指定的函数开始显示
(gdb) l 函数名
2.非当前文件
# 1.指定文件的指定的行
(gdb) l 文件名:行号
# 2.指定文件的指定的函数
(gdb) l 文件名:函数名
3.设置显示的行数
# 1.查看默认显示的行数
# list == listsize
(gdb) show list
Number of source lines gdb will list by default is 10.
(gdb) show listsize
Number of source lines gdb will list by default is 10.
# 2.更改显示的函数
(gdb) set list 行数
(gdb) set listsize 行数
3.断点操作
1.设置断点
break == b
# 在当前文件中设置
(gdb) b 行号
(gdb) b 函数名
# 在非当前文件中设置
(gdb) b 文件名:行号
(gdb) b 文件名:函数名
# 1.在当前文件设置断点
(gdb) b 行号
Breakpoint 1 at 0x400c23: file test.cpp, line 13.
# b 函数名
(gdb) b main
Breakpoint 2 at 0x400bb8: file test.cpp, line 10.
# 2.在非当前文件设置断点
# b 文件名:行号
(gdb) b insert.cpp:15
Breakpoint 3 at 0x400991: file insert.cpp, line 15
# b 文件名:函数名
(gdb) b insert.cpp:insertionSort
Breakpoint 4 at 0x400951: file insert.cpp, line 7.
2.查看断点
# i == info
# 查看设置的断点信息
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400c23 in main() at test.cpp:13
2 breakpoint keep y 0x0000000000400bb8 in main() at test.cpp:10
3 breakpoint keep y 0x0000000000400991 in insertionSort(int*, int) at insert.cpp:15
4 breakpoint keep y 0x0000000000400951 in insertionSort(int*, int) at insert.cpp:7
3.删除断点
# d = del = delete
# d 断点的编号Num
(gdb) d 4
4.设置断点无效
# dis == disable
# dis 断点编号
(gdb) dis 1
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000000000400c23 in main() at test.cpp:13
2 breakpoint keep y 0x0000000000400bb8 in main() at test.cpp:10
3 breakpoint keep y 0x0000000000400991 in insertionSort(int*, int) at insert.cpp:15
5.无效断点生效
# ena == enable
# ena 断点的编号
(gdb) ena 2
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000000000400c23 in main() at test.cpp:13
2 breakpoint keep y 0x0000000000400bb8 in main() at test.cpp:10
3 breakpoint keep y 0x0000000000400991 in insertionSort(int*, int) at insert.cpp:15
6.设置条件断点
# 一般用在循环的位置
# b 行号 if 变量==某个值
(gdb) b 18 if i==6
Breakpoint 5 at 0x400c52: file test.cpp, line 18.
4.调试命令
1.运行gdb程序
# 通过gdb工具将应用程序运行起来
# 1. start, 程序只运行一行
# 通过start, 只运一行, 就停止了
(gdb) start
# 2. run == r, 程序一直运行, 直到遇到断点停止
# 通过run, 一直运行, 直到遇到了断点
(gdb) run
2.从当前断点的位置继续运行, 停到下一个断点的位置
# 程序停到了断点的位置, 继续运行, 运行到下一个断点停止
# c = continue
(gdb) c
3.向下单步调试
1.进入函数体
# s == step向下执行一行, 遇到函数, 进入函数体内部
(gdb) s
1.跳出函数体
# 要跳出的函数中不能有断点, 如果有要删除或者设置为无效的断点
(gdb) finish
2.不进入函数体
# next == n 向下执行一行, 遇到函数, 不进入函数体内部(直接把函数执行完)
(gdb) n
4.打印变量
1.打印变量
# 1.p == print 变量名
(gdb) print i
$1 = 6
# 2.进制转换: p/x p/o
# p/x -> 16进制 p/o -> 八进制 p/d -> 十进制
(gdb) print/x i
$1 = 0x6
2.打印变量类型
# ptype 变量名
(gdb) ptype array
type = int [12]
(gdb) ptype i
type = int
3.设置变量等于某一个值
# 一般在循环中用的比较多
# set var 变量名=变量值
(gdb) set var i=9
4.变量自动显示
# 1.自动打印指定变量的值
# 语法: display 变量名
(gdb) display j
# 2.查看追踪的变量
# 语法: i display
i == info
(gdb) i display
Auto-display expressions now in effect:
Num Enb Expression
1: y i
2: y j
# 3.取消自动追踪变量不打印变量值
# 语法: undisplay 变量编号(通过 i display 查看的)
(gdb) undisplay 1
5.跳出循环
# until
# 要跳出的循环代码块中不能有断点, 如果有要删除或者设置为无效的断点
# 执行完循环体的最后一行 == 循环的开始位置 才能跳出