目录
回顾静态库和共享库的制作和使用
熟练使用规则编写简单的makefile文件
熟练使用makefile中的变量
熟练使用makfile中的函数
熟练掌握gdb相关调试命令的使用
了解概念:pcb和文件描述符,虚拟地址空间
熟练掌握linux系统IO函数的使用
一、回顾静态库和共享库的制作和使用
编写一个四则运算代码,分别利用静态库和共享库
具体详情步骤可以看上一篇文章
二、makefile
makefile文件是用来管理项目工程文件,通过执行make命令,make就会解析并执行makefile文件。
makefile的命名:makefile或者Makefile
1、makefile的基本规则
makefile由一组规则组成,规则如下:
目标: 依赖
(tab)命令
makefile基本规则三要素:
⭐目标:要生成的目标文件
⭐依赖:目标文件由那些文件生成
⭐命令:通过执行该命令由依赖文件生成目标
下面以具体的例子来讲解:
当前目录下由main.c fun1.c fun2.c sum.c,根据这个基本规则编写一个简单的makefile文件,生成可执行文件main
第一个版本的makefile:
然后执行make
缺点:效率低,修改一个文件,所有的文件会全部重新编译
2、makefile的工作原理
(1)基本原则
⚪ 若想生成目标,检查规则中的所有的依赖文件是否都存在:
⭐如果有的依赖不存在,则向下搜索规则,看是否有生成该依赖文件的规则:
如果有规则用来生成该依赖文件,则执行规则中的命令生成依赖文件;
如果没有规则用来是生成该依赖文件,则报错。
⭐如果所有依赖都存在,检查规则中的目标是否需要更新,必须先检查它的所有依赖,依赖中有任何一个被更新,则目标必须更新.(检查的规则是那个时间大那个最新)
👉若目标时间 > 依赖的时间,不更新
👉若目标时间 < 依赖的时间,则更新
(2)总结
⚪分析各个目标和依赖之间的关系
⚪根据依赖关系自底向上执行命令
⚪根据依赖文件的时间和目标文件的时间确定是否需要更新
⚪如果目标不依赖任何条件,则执行对应命令,以示更新(如:伪目标)
第二个版本的makefile:
修改fun1.c之后执行make命令
缺点:冗余,若.c文件数量很多,编写起来比较麻烦
3、makefile中的变量
在makefile中使用变量有点类似于C语言中的宏定义,使用该变量相当于内容替换,使用变量可以使makefile易于维护,修改起来变得简单。
makefile有三种类型的变量:
👉普通变量
👉自带变量
👉自动变量
(1)普通变量
⭐变量定义直接用 =
⭐使用变量值用$(变量名)
如:下面是变量的定义和使用
foo = abc //定义变量并赋值
bar = $(foo) //使用变量,$(变量名)
定义了两个变量:foo、bar,其中bar的值是foo变量值的引用。
(2)自带变量
除了使用用户自定义变量,makefile中也提供了一些变量(变量名大写)供用户使用,我们可以直接对其进行赋值:
CC = gcc #arm-linux-gcc
CPPFLAGS:C预处理的选项 -I(大写i)
CFLAGS: C编译器的选项 -Wall -g -c
LDFLAGS: 链接器选项 -L -l
(3)自动变量
👉 $@:表示规则中的目标
👉 $<:表示规则中的第一个条件
👉 $^:表示规则中的所有条件,组成一个列表,以空格隔开,如果这个列表中有重复的项则消除重复项
特别注意:自动变量只能在规则的命令中使用
(4)模式规则
至少在规则的目标定义中要包含'%','%'表示一个或多个,在依赖条件中同样可以使用'%',依赖条件中的'%'的取值取决于其目标:
比如: main.o:main.c fun1.o: fun1.c fun2.o:fun2.c,说的简单点就是: XXX.O:XXX.C
第三个版本的makefile:
4、makefile函数
makefile中的函数有很多,在这里给大家介绍两个最常用的
1、wildcard - 查找指定目录下的指定类型的文件
src = $(wildcard *.c) //找到当前目录下所有后缀为.c的文件,赋值给src
2、patsubst - 匹配替换
obj = $(patsubst %c,%o,$(src)) //把src变量里所有后缀为.c的文件替换成.o
在makefile中所有的函数都是有返回值的。
当前目录下有main.c fun1.c fun2.c sum.c
src = $(wilidcard *.c)等价于 src = main.c fun1.c fun2.c sum.c
obj = $(patsubst %c, %o, $(src)) 等价于obj = main.o fun1.o fun2.o sum.o
第四个版本的makefile:
缺点:每次重新编译都需要手工清理中间.o文件和最终目标文件
5、makefile的清理操作
用途:清除编译生成的中间.o文件和最终目标文件
make clean 如果当前目录下有同名clean文件,则不执行clean对应的命令,解决方案:
👉伪目标声明:
.PHONY:clean
■ 声明目标为伪目标之后,makefile将不会检查该目标是否存在或者该目标是否需要更新
⚪ clean命令中的特殊符号:
👉 "-" 此条命令出错,make也会继续执行后续的命令。如:“-rm main.o”
rm -f:强制执行,比如若要删除的文件不存在使用-f不会出错
👉 “@”不显示命令本身,只显示结果。如:“@echo clean done”
⚪其他
👉使用“-f”可以指定makefile文件,如:make -f mainmak
第四个版本的makefile:增加清理功能
三、gdb调试
1、gdb介绍
GDB(GNU Debugger)是GCC的调试工具。其功能强大,现描述如下:
GDB主要帮助你完成以下四个方面的功能:
① 启动程序,可以按照你的自定义的要求随心所欲的运行程序
② 可让被调试的程序在你所指定的断点处停住。(断点可以是条件表达式)
③ 当程序被停住时,可以检查此时你的程序中所发生的事。
④ 动态的改变你程序的执行环境。
2、生成调试信息
一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的 -g 参数可以做到这一点。
如:
gcc -g hello.c -o hello
如果没有-g,你将看不见程序的函数名,变量名,所代替的全是运行时的内存地址。当你用 -g 把调试信息加入之后,并成功编译目标代码以后,让我们来看看如何用gdb来调试他。
3、启动gdb
⭐ 启动gdb:gdb program
program 也就是你的执行文件,一般在当前目录下。
⭐ 设置运行参数
■ set args 可指定运行时的参数。(如:set args 10 20 30 40 50)
■ show args 命令可以查看设置好的运行参数。
⭐ 启动程序
■ run:程序开始执行,如果有断点,停在第一个断点处
■ start:程序向下执行一行。(在第一条语句处停止)
4、显示源代码
GDB可以打印出所调试程序的源代码,当然,在程序编译时一定要加上-g的参数,把源程序信息编
译到执行文件中。不然就看不到源程序了。当程序停下来以后,GDB会报告程序停在了那个文件的
第几行上。你可以用list命令来打印程序的源代码,默认打印10行, list命令的用法如下所示:
👉 list linenum:打印第linenum行的上下文内容。
👉 list function:显示函数名为function的函数的源程序
👉 list:显示当前行后面的源程序
👉 list -:显示当前文件开始处的源程序
👉 list file:linenum:显示file文件下第n行
👉 list file:function:显示file文件的函数名为function的函数的源程序
一般是打印当前行的上5行和下5行,如果显示函数是上2行和下8行,默认是10行,当然,你也可以定制显示的范围,使用下面命令可以设置一次显示源程序的函数
👉 set listsize count:设置一次显示源代码的行数
👉 show listsize:查看当前listsize的设置
5、设置断点
(1)简单断点
⚪ break设置断点,可以简写为b
■ b 10设置断点,在源程序第10行
■ b func 设置断点,在func函数入口处
(2)多文件设置断点——其他文件
⚪ 在进入指定函数时停住:
■ b filename:linenum - 在源文件filename的linenum行处停住
■ b filename:function - 在源文件filename的function函数的入口处停住
(3)查询所有断点
⚪ info b == info break == i break == i b
(4)条件断点
一般来说,为断点设置一个条件,我们使用if关键词,后面跟其断点条件。设置一个条件断点:
b test.c:8 if intValue == 5
(5)维护断点
⚪ delete[range...]删除指定的断点,其简写命令为d
■ 如果不指定断点号,则表示删除所有的断点。range表示断点号的范围(如3-7)
♦ 删除某个断点:delete num
♦ 删除多个断点:delete num1 num2 ...
♦ 删除连续的多个断点:delete m-n
♦ 删除所有断点:delete
■ 比删除更好的一种方法是disable停止点,disable了停止点,GDB不会删除,当你还需要时,enable即可,就好像回收站一样
⚪ disable[range...]使指定断点无效,简写命令是dis
■ 如果什么都不指定,表示disable所有的停止点
♦ 使一个断点无效:disable num
♦ 使多个断点无效:disable num1 num2 ...
♦ 使连续的多个断点无效:disable m-n
♦ 使所有断点无效:disable
⚪ enable[range...]使无效断点生效,简写命令是ens
■ 如果什么都不指定,表示enable所有的停止点
♦ 使一个断点有效:enable num
♦ 使多个断点有效:en'able num1 num2 ...
♦ 使连续的多个断点有效:enable m-n
♦ 使所有断点无效:enable
6、调试代码
⚪ run 运行程序,可简写为r
⚪ next 单步跟踪,函数调用当作一条简单语句执行,可简写为n
⚪ step 单步跟踪,函数调用进入被调用函数体内,可简写为s
⚪ finish 退出进入的函数,如果出不去,看一下函数体中的循环是否有断点,如果有,删掉或者设置无效
⚪ until 在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体,可简写为u,如果出不去,看一下函数体中的循环是否有断点,如果有,删掉或者设置无效
⚪ continue 继续运行程序,可简写为c(若有断点则跳到下一个断点处)
7、查看变量的值
查看运行时变量的值
print 打印变量、字符串、表达式的值,可简写为p
p count ---- 打印count的值
自动显示变量的值
你可以设置一些自动显示的变量,当程序停住时,或是在你单步跟踪时,这些变量会自动显示。相关的GDB命令是display
⚪ display 变量名
⚪ info display --- 查看 display 设置的自动显示的信息。
⚪ undisplay num (info display 时显示的编号)
⚪ delete display dnums --- 删除自动显示,dnums意为所设置好了的自动显示的编号。如果要同时删除几个,编号可以用空格分隔,如果要删除一个范围内的编号,可以用减号表示(如:2-5)
■ 删除某个自动显示:undisplay num 或者 delete display num
■ 删除多个:delete display num1 num2
■ 删除一个范围: delete display m-n
查看修改变量的值
⚪ ptype width --- 查看变量width的类型
type = double
⚪ p width --- 打印变量width的值
$4 = 13
你可以使用set var 命令来告诉GDB,width不是你GDB 的参数,而是程序的变量名
如:set var width = 47 //将变量var的值设置为47
在你改变程序变量取值时,最好都使用set var格式的GDB命令。
四、文件IO
1、C库IO函数的工作流程
C语言操作文件相关问题:
使用fopen函数打开一个文件,返回一个FILE* fp,这个指针指向的结构体有三个重要的成员。
👉 文件描述符:通过文件描述可以找到文件的inode,通过inode可以找到对应的数据库
👉 文件指针:读和写共享一个文件指针,读或者写都会引起文件指针的变化
👉 文件缓冲区:读或者写会先通过文件缓冲区,主要目的是为了减少对磁盘的读写次数,提高读写磁盘的效率。
备注:
♦ 头文件stdio.h的第48行处:typedef struct _IO_FILE FILE;
♦ 头文件libio.h的第241行处:struct _IO_FILE,这个结构体定义中有一个_fileno 成员,这个就是文件描述符
2、C库函数与系统函数的关系
系统调用:由操作系统实现并提供给外部应用程序的编程接口,是应用程序系统之间数据交互的桥梁。
库函数与系统函数的关系是:调用和被调用的关系;库函数是对系统函数的进一步封装
3、虚拟地址空间
linux每一个运行的程序(进程)操作系统都会为其分配一个0~4G的地址空间(虚拟地址空间)
进程:正在运行的程序
进程的虚拟地址空间分为用户区和内核区,其中内核区是受保护的,用户是不能够对其进行读写操
作的;
内核区中很重要的一个就是进程管理,进程管理中有一个区域就是PCB(本质是一个结构体);PCB
中有文件描述符表,文件描述符表中存放着打开的文件描述符,涉及到文件的IO操作都会用到这个文件描述符。
4、pcb和文件描述符表

备注:
pcb:结构体:task_struct,该结构体在:
/usr/src/linux-headers-4.4.0-97/include/linux/sched.h:1390
一个进程有一个文件描述符表:1024
⚪ 前三个被占用,分别是STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO
⚪ 文件描述符的作用:通过文件描述符找到inode,通过inode找到磁盘数据块
虚拟地址→内核区→PCB→文件描述符表→文件IO操作使用文件描述符
5、open函数
■ 函数描述:打开或新建一个文件
■ 函数原型:
int open(const char * pathname, int flags);
int open(const char * pathname, int flags, mode_t mode);
■ 函数参数:
→ pathname参数是要打开或创建的文件名,和fopen一样,pathname既可以是相对路径也可
以是绝对路径
→ flags参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所
以这些常数的宏定义都以O_开头,或表示or。
※ 必选项:以下三个常数中必须指定一个,且仅允许指定一个
⚪ O_RDONLY 只读打开
⚪ O_WRONLY 只写打开
⚪ RDWR 可读可写打开
※ 以下可选项可以同时指定0个或多个,和必选项按位或起来作为flags参数。
可选项有很多,这里只介绍几个常用选项:
⚪ O_APPEND 表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾
而不覆盖原来的内容。
⚪ O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode,表示该
文件的访问权限。
※ 文件最终权限:mode & ~umask
⚪ O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回
⚪ O_TRUNC 如果文件已存在,将其长度截断为0字节。
⚪ O_NONBLOCK 对于设备文件,以O_NONBLOCK 方式打开可以做非阻I/O
(Nonblock/O),非阻塞I/O。
■ 函数返回值:
→ 成功:返回一个最小且未被占用的文件描述符
→ 失败:返回-1,并且设置errno值
6、close函数
■ 函数描述:关闭文件
■ 函数原型:int close(int fd);
■ 函数参数:fd文件描述符
■ 函数返回值:
→ 成功返回0
→ 失败返回-1,并设置errno值
需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长
年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越
来越多,会占用大量文件描述符和系统资源。
7、read函数
■ 函数描述:从打开的设备或文件中读取数据
■ 函数原型:ssize_t read(int fd, void* buf, size_t count);
■ 函数参数:
→ fd:文件描述符
→ buf:读上来的数据保存在缓冲区buf中
→ count:buf缓冲区存放的最大字节数
■ 函数返回值:
→ >0:读取到的字节数
→ =0:文件读取完毕
→ -1:出错,并设置errno
8、write函数
■ 函数描述:向打开的设备或文件中写数据
■ 函数原型:ssize_t write(int fd, const void* buf, size_t count);
■ 函数参数:
→ fd:文件描述符
→ buf:缓冲区,要写入文件或设备的数据
→ count:buf中数据的长度
■ 函数返回值:
→ 成功:返回写入字节数
→ -错误:返回-1,并设置errno
9、lseek函数
所有打开的文件都有一个当前文件偏移量(current file offset),以下简称为cfo. cfo通常是一个
非负整数,用于表明文件开始处到文件当前位置的字节数.读写操作通常开始于cfo,并且使cfo增
大,增量为读写的字节数.文件被打开时, cfo会被初始化为0,除非使用了O_APPEND.
使用lseek函数可以改变文件的cfo
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
■ 函数描述:移动文件指针
■ 函数原型:off_t lseek(int fd, off_t offset, int whence);
■ 函数参数:
→ fd:文件描述符
→ 参数offset的含义取决于参数whence:
♦ 如果whence是SEEK_SET,文件偏移量将设置为offset
♦ 如果whence是SEEK_CUR,文件偏移量将设置为cof加上offset,offset可以为正也可
以为负
■ 函数返回值:若lseek成功执行,则返回新的偏移量
■ lseek函数常用操作
→ 文件指针移动到头部
lseek(fd, 0, SEEK_SET);
→ 获取文件指针当前位置
lseek(fd, 0, SEEK_CUR);
→ 获取文件长度
int len = lseek(fd, 0, SEEK_END);
→ lseek实现文件拓展
off_t currpos;
//从文件尾部开始向后扩展1000个字节
currpos = lseek(fd, 1000, SEEK_END)
//额外执行一次写操作,否则文件无法完成拓展
write(fd, "a", 1); //数据随便写
10、perror和errno
errno是一个全局变量,当系统调用后若出错会将errno进行设置,perror可以将errno,对应的描述信
息打印出来。
如:perror("open");如果报错的话打印: open:(空格)错误信息
11、阻塞和非阻塞
思考:阻塞和非阻塞是文件的属性还是read函数的属性?
通过读普通文件测试得知:read在读完文件内容之后,若再次read,则read函数会立刻返回
,表明read函数读普通文件是非阻塞的
设备文件: /dev/tty 标准输入STDIN_FILENO
通过读/dev/tty终端设备文件,表明read函数读设备文件是阻塞的
结论:阻塞和非阻塞不是read函数的属性,而是文件本身的属性。
socket、pipe这两种文件都是阻塞的。