Linux之基础开发工具二(makefile,git,gdb)
目录
一、自动化构建-make/makefile
1.1、背景
1.2、基本使用
1.3、推导过程
1.4、语法拓展
二、进度条小程序
2.1、回车与换行
2.2、行缓冲区
2.3、练手-倒计时程序
2.4、进度条程序
三、版本控制器-Git
3.1、版本控制器
3.2、gitee的使用
3.2.1、如何创建仓库
3.2.2、如何将仓库克隆到本地
3.2.3、gitee三板斧
3.2.4、git pull
四、调试器-gdb/cgdb
4.1、预备
4.2、常见使用
4.3、常见技巧
4.3.1、watch
4.3.2、set var 确定问题原因
4.3.3、条件断点
一、自动化构建-make/makefile
1.1、背景
- 会不会写makefile,从⼀个侧⾯说明了⼀个⼈是否具备完成⼤型⼯程的能⼒。
- ⼀个⼯程中的源⽂件不计数,其按类型、功能、模块分别放在若⼲个⽬录中,makefile定义了⼀系列的规则来指定,哪些⽂件需要先编译,哪些⽂件需要后编译,哪些⽂件需要重新编译,甚⾄于进⾏更复杂的功能操作。
- makefile带来的好处就是⸺“⾃动化编译”,⼀旦写好,只需要⼀个make命令,整个⼯程完全 ⾃动编译,极⼤的提⾼了软件开发的效率。
- make是⼀个命令⼯具,是⼀个解释makefile中指令的命令⼯具,⼀般来说,⼤多数的IDE都有这个命令,⽐如:Delphi 的 make,Visual C++ 的nmake,Linux 下 GNU 的 make。可⻅,makefile 都成为了⼀种在⼯程⽅⾯的编译⽅法。
- make是⼀条命令,makefile是⼀个⽂件,两个搭配使⽤,完成项⽬⾃动化构建。
1.2、基本使用
示例代码:
makefile文件:
使用:
注释:在makefile文件中使用 # 进行注释。
依赖关系:
proc:proc.c 这一行叫做依赖关系,冒号右边的叫做依赖文件列表,依赖文件列表可以有一个或多个文件,也可以有零个文件,如 clean 这一行,它也是一行依赖关系,但它的依赖文件列表中没有文件。
依赖方法:
依赖关系下面缩进的部分就是依赖方法,表示执行依赖关系时执行的方法/指令,它必须以一个tab开头。gcc -o proc proc.c 和 rm -f proc 都是依赖方法。一个依赖关系下面可以有多行依赖方法,但都要以一个tab开头。依赖方法可以是任意指令。如图:
伪目标:
.PHONY表示声明一个伪目标,冒号后面跟伪目标的名称,需要执行它的依赖方法时只需要 make + 伪目标名称即可。.PHONY的作用是让被修饰的目标文件对应的方法总是被执行的。
makefile/make原理:
- makefile文件会被make从上向下扫描,第一个目标名是缺省形成的,即执行第一个依赖关系的方法时只需要make命令就行,但如果我们想执行其他组的依赖关系和依赖方法,需要make + 目标名。
- make,makefile在执行gcc命令时,如果发生了语法错误,就会终止推导过程。
- make解释makefile的时候,是会自动推导的。一直推导,推导过程,不执行依赖方法。直到推导到有依赖文件存在,然后再逆向的执行所有的依赖方法。
- make默认只形成一个可执行程序。
取消回显:
默认情况下,我们使用make命令时会回显依赖方法,如图:
- makefile文件:
- 使用:
想要取消回显,我们只需要在依赖方法前加@符号,如图:
- makefile文件:
- 使用:
项目清理:
- ⼯程是需要被清理的。
- 像clean这种,没有被第⼀个⽬标⽂件直接或间接关联,那么它后⾯所定义的命令将不会被⾃动执⾏,不过,我们可以显⽰要make执⾏。即命令⸺“make clean”,以此来清除所有的⽬标 ⽂件,以便重编译。
- 但是⼀般我们这种clean的⽬标⽂件,我们将它设置为伪⽬标,⽤ .PHONY 修饰,伪⽬标的特性是,总是被执⾏的。
什么叫做总是被执行:
我们看下面示例
- makefile文件:
- 效果:
- makefile文件:
- 效果:
从上面两组示例可以看出,只有当编译指令所在的依赖关系被.PHONY修饰后,才能每次make都成功,这是为什么呢?其实,无论源文件还是最终生成的可执行文件本质都是文件,只要是文件就会有时间属性,如图:
上图中Access是文件最近被访问的时间,Modify是文件内容最近被修改的时间,Change是文件属性最近被修改的时间。决定一个文件是否需要被重新编译的是Modify时间,源文件和可执行程序都有自己的Modify时间,我们通过make执行编译命令时,它会自动对比这两个文件的Modify时间,如果可执行程序的时间新,就说明没有必要重新编译,也就如上面示例所示,编译命令不会执行,如果源文件的时间新,那就说明需要重新编译生成新的可执行程序,make命令就会成功,所以当我们不加.PHONY时,make命令成功与否取决于这两个时间的比较,而加上.PHONY后,make命令就会忽略时间的比较,直接去执行。
注意:有些命令需要比较时间,而有些命令本身就和时间无关,这些和时间无关的命令加不加.PHONY修饰都一样,但有些时候依赖方法有很多命令,只要有和时间有关的,就可能需要.PHONY修饰。
结论:
- .PHONY:让make忽略源⽂件和可执⾏⽬标⽂件的M时间对⽐。
1.3、推导过程
下面我们看一下myproc.c生成myproc的完整推导过程:
myproc:myproc.o
gcc myproc.o -o myproc
myproc.o:myproc.s
gcc -c myproc.s -o myproc.o
myproc.s:myproc.i
gcc -S myproc.i -o myproc.s
myproc.i:myproc.c
gcc -E myproc.c -o myproc.i
.PHONY:clean
clean:
rm -f *.i *.s *.o myproc
编译图解:
make是如何⼯作的,在默认的⽅式下,也就是我们只输⼊make命令。那么:
- make会在当前⽬录下找名字叫“Makefile”或“makefile”的⽂件。
- 如果找到,它会找⽂件中的第⼀个⽬标⽂件(target),在上⾯的例⼦中,他会找到 myproc 这个⽂件,并把这个⽂件作为最终的⽬标⽂件。
- 如果myproc⽂件不存在,或是myproc所依赖的后⾯的 myproc.o ⽂件的⽂件修改时间要比myproc 这个⽂件新(可以⽤ touch 测试),那么,他就会执⾏后⾯所定义的命令来⽣成myproc 这个⽂件。
- 如果 myproc 所依赖的 myproc.o ⽂件不存在,那么 make 会在当前⽂件中找⽬标为myproc.o ⽂件的依赖性,如果找到则再根据那⼀个规则⽣成 myproc.o ⽂件。(这有点像⼀个堆栈的过程)
- 如果.o文件不存在,它会继续找生成.o文件所需的依赖文件,然后看该文件是否存在,如果存在就执行依赖方法生成,如果不存在则继续查找该文件的依赖文件。
- 在这个逐层查找的过程中,每找一层就会将它们的方法入栈,直到找到某一个依赖文件存在时,该依赖关系对应的依赖方法也进栈,之后就可以开始出栈了。
- 这就是整个make的依赖性,make会⼀层⼜⼀层地去找⽂件的依赖关系,直到最终编译出第⼀个⽬标⽂件。
- 在找寻的过程中,如果出现错误,⽐如最后被依赖的⽂件找不到,那么make就会直接退出,并报错,⽽对于所定义的命令的错误,或是编译不成功,make根本不理。
- make只管⽂件的依赖性,即,如果在我找了依赖关系之后,冒号后⾯的⽂件还是不在,那么对不起,我就不⼯作啦。
注意:上面这种写法太麻烦了,实践中一般不会这么写,一般只生成.o文件和可执行文件。
1.4、语法拓展
- 变量:在makefile文件中可以定义变量,且变量没有类型。语法:变量名= ... 。
- $(变量名):使用定义好的变量。
- %:makefile文件中的通配符。
- $<:把依赖文件依次放入指令中,放入一个执行一次指令。
- $^:把依赖文件一次全部放入指令中,然后执行指令(只执行一次)。
- $@:表示依赖关系中冒号左侧的内容。
- @:取消指令回显
示例如下:
BIN=proc.exe # 定义变量
CC=gcc
#SRC=$(shell ls *.c) # 采⽤shell命令⾏⽅式,获取当前所有.c⽂件名
SRC=$(wildcard *.c) # 或者使⽤ wildcard 函数,获取当前所有.c⽂件名
OBJ=$(SRC:.c=.o) # 将SRC的所有同名.c 替换 成为.o 形成⽬标⽂件列表
LFLAGS=-o # 链接选项
FLAGS=-c # 编译选项
RM=rm -f # 引⼊命令
$(BIN):$(OBJ)
@$(CC) $(LFLAGS) $@ $^ # $@:代表⽬标⽂件名。 $^: 代表依赖⽂件列表
@echo "linking ... $^ to $@ "
%.o:%.c # %.c 展开当前⽬录下所有的.c。 %.o: 同时展开同名.o
@$(CC) $(FLAGS) $< # %对展开的依赖.c⽂件,⼀个⼀个的交给gcc。
@echo "compling ... $< to $@ " # @:不回显命令
.PHONY:clean
clean:
$(RM) $(OBJ) $(BIN)
.PHONY:test
test:
@echo $(SRC)
@echo $(OBJ)
如何使用make命令形成多个可执行程序:
我们可以先定义一个依赖关系,它的依赖方法为空,它的依赖文件就是我们要生成的所有可执行程序,这样在推导时就会先去生成它的依赖文件,即我们需要的可执行程序,生成后因为它的依赖方法为空,make命令执行结束。如图:
- makefile文件:
- 效果:
二、进度条小程序
2.1、回车与换行
- 回车概念:光标跳到当前行的起始位置。符号: 。
- 换行概念:光标相对于行起始位置的距离不变,跳转到下一行。符号: 。
在C/C++语言中,对 进行了处理,在语言上这一个符号即进行了回车,也进行了换行。
2.2、行缓冲区
示例代码一:
效果:先输入hello,Linux,再睡眠两秒。
示例代码二:
效果:先睡眠两秒,再输出hello,Linux。
是什么造成上面两段代码的差异的呢?首先,这两段代码的执行顺序都是一样的,都是先执行printf函数,再执行sleep函数。其次,我们要知道printf函数并不是直接将内容输出到显示器上,而是将内容输出到缓冲区,当缓冲区满了或者程序结束时会对缓冲区进行刷新,这里之所以会有差异是因为 除了换行还会强制刷新缓冲区。
如何不通过 强制刷新缓冲区:使用fflush函数。
2.3、练手-倒计时程序
1 #include
2 #include
3
4 int main()
5 {
6 int count = 10;
7 while(count >= 0)
8 {
9 printf("%-2d
", count); //
回车,但是没有换行,也就没有刷新
10 fflush(stdout);
11 count--;
12 sleep(1);
13 }
14 printf("
");
15 return 0;
16 }
17
解释:上面代码通过每次输出内容都带上 来实现倒计时的数字在同一行,并且每次下一个数会在该行覆盖上一个数,不过需要注意的是,第一次输出的是10,占两位,后面输出的都是一位数字,因为每次还向显示器文件输出了回车,所以新的内容会在显示器文件起始处写入,这样每次都只能覆盖第一个数字,在显示器文件中0一直存在,导致我们看到的效果就是除10以外的其他数字后面也都跟着一个0。解决办法就是以%2d的格式向显示器文件进行写入,这样即使是一个数字也会占两位,这样就可以将0覆盖掉。但是因为C语言中默认这种格式是右对齐的,数字在右边,左侧会有空格,展现出来不好看,所以我们加一个负号,使其左对齐。
2.4、进度条程序
- process.h:
#pragma once
//version
//void Process();
//void Process(double total, double current);
void FlushProcess(double total, double current);
- process.c:
#include "process.h"
#include
#include
#include
#include
#define NUM 101
#define STYLE '='
#define POINT '.'
#define SPACE ' '
const int pnum = 6;
// version 2:真实的进度条,应该根据具体的比如下载的量,来动态刷新进度
void FlushProcess(double total, double current)
{
// 1. 更新当前进度的百分比
double rate = (current/total)*100;
// printf("test: %.1lf%%
", rate);
// fflush(stdout);
// 2. 更新进度条主体
char bar[NUM]; // 我们认为,1% 更新一个等号
memset(bar, ' ', sizeof(bar));
for(int i = 0; i < (int)rate; i++)
{
bar[i] = STYLE;
}
// 3. 更新旋转光标或者是其他风格
static int num = 0;
num++;
num%=pnum;
char points[pnum+1];
memset(points, ' ', sizeof(points));
for(int i = 0; i < pnum; i++)
{
if(i < num) points[i] = POINT;
else points[i] = SPACE;
}
// 4. test && printf
printf("[%-100s][%.1lf%%]%s
", bar, rate, points);
fflush(stdout);
//sleep(1);
}
// version 1
//void Process()
//{
// const char *lable = "|/-";
// int len = strlen(lable);
// char bar[NUM];
// memset(bar, ' ', sizeof(bar));
// int cnt = 0;
// while(cnt <= 100)
// {
// printf("[%-100s][%d%%][%c]
", bar, cnt, lable[cnt%len]);
// fflush(stdout);
// bar[cnt] = STYLE;
// cnt++;
// usleep(100000);
// }
//
// printf("
");
//}
- main.c:
#include
#include
#include
#include
#include "process.h"
typedef void (*flush_t)(double total, double current);// 这是一个刷新的函数指针类型
const int base = 100;
double total = 2048.0; // 2048MB
double once = 0.1; // 0.5MB
// 进度条的调用方式
void download(flush_t f)
{
double current = 0.0;
while(current < total)
{
// 模拟下载行为
int r = rand() % base + 1; // [1, 10]
double speed = r * once;
current += speed;
if(current >= total) current = total;
usleep(10000);
// 更新除了本次新的下载量
// 根据真实的应用场景,进行动态刷新
//Process(total, 1.0);
f(total, current);
//printf("test: %.1lf/%.1lf
", current, total);
//fflush(stdout);
}
printf("
");
}
int main()
{
srand(time(NULL));
download(FlushProcess);
download(FlushProcess);
download(FlushProcess);
return 0;
}
- makefile:
process:main.c process.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f process
效果:
三、版本控制器-Git
3.1、版本控制器
为了能够更⽅便我们管理这些不同版本的⽂件,便有了版本控制器。所谓的版本控制器,就是能让你 了解到⼀个⽂件的历史,以及它的发展过程的系统。通俗的讲就是⼀个可以记录⼯程的每⼀次改动和版本迭代的⼀个管理系统,同时也⽅便多⼈协同作业。
⽬前最主流的版本控制器就是Git。Git可以控制电脑上所有格式的⽂件,例如doc、excel、dwg、 dgn、rvt等等。对于我们开发⼈员来说,Git最重要的就是可以帮助我们管理软件开发项⽬中的源代码⽂件!
安装git:
yum install git #centOS版本
3.2、gitee的使用
3.2.1、如何创建仓库
首先我们需要先注册一个gitee账号,然后登录。登录后点击右上角加号,点击新建仓库,如下图:
然后会进入如下界面:
这里仓库名称可以随意起,路径会自动生成,不用管,仓库介绍根据自己的实际情况写就可以。仓库可以开源也可以私有,根据情况选择。
建议对仓库进行初始化,希望仓库存储的是哪种语言的代码,就选择哪种语言就可以,.gitignore模版和语言选择一样的就行,开源许可证可以不选,对于模版的选择,如果这个仓库是自己用的选Readme就可以,后面两个多人协作会涉及,分支模型可以不选,不选默认单分支,如果需要多分支可以根据情况选择。然后点击创建就可以了。
3.2.2、如何将仓库克隆到本地
首先找到刚刚创建好的仓库,点击克隆/下载。如图:
点击后:
选择HTTPS,点击蓝框里的赋值按钮。然后回到Linux平台,输入命令:git clone + HTTPS链接。
然后该仓库就会被同步到本地了。
3.2.3、gitee三板斧
- git add:
作用:将代码放到刚才下载好的⽬录中。
语法:git add [⽂件名]
- git commit:
作用:提交改动到本地。
语法:git commit -m "XXX"
提交的时候应该注明提交⽇志,描述改动的详细内容。
- git push:
作用:同步到远端服务器上。
语法:git push
需要填⼊用户名密码(gitee的)。同步成功后,刷新Gitee⻚⾯就能看到代码改动了。
配置免密提交:git本地免密码和账号pull、push-CSDN博客
注意:第一次提交会提示设置用户名和邮箱,邮箱要和注册gitee时的邮箱相同,否则看不到提交后的小绿点。
3.2.4、git pull
作用:将远端仓库同步到本地
语法:git pull
注意:当远端仓库和本地仓库不一致时,会导致无法提交,这时需要通过该指令使本地仓库和远端同步。
四、调试器-gdb/cgdb
4.1、预备
- 程序的发布⽅式有两种, debug 模式和 release 模式, Linux gcc/g++ 出来的⼆进制程序,默认是 release 模式。
- 要使⽤gdb调试,必须在源代码⽣成⼆进制程序的时候,加上 -g 选项,如果没有添加,程序⽆法被 编译。
$ gcc mycmd.c -o mycmd # 默认模式,不⽀持调试
$ file mycmd
mycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=82f5cbaada10a9987d9f325384861a88d278b160, for GNU/Linux3.2.0, not stripped
$ gcc mycmd.c -o mycmd -g # debug模式
$ file mycmd
mycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3d5a2317809ef86c7827e9199cfefa622e3c187f, for GNU/Linux
3.2.0, with debug_info, not stripped
4.2、常见使用
开始:gdb binFile
退出:ctrl + d 或 quit 调试命令
命令 | 作用 | 样例 |
---|---|---|
list/l | 显⽰源代码,从上次位置开始,每次列出10⾏ | list/l 10 |
list/l 函数名 | 列出指定函数的源代码 | list/l main |
list/l ⽂件名:⾏号 | 列出指定⽂件的源代码 | list/l mycmd.c:1 |
r/run | 从程序开始连续执⾏,遇到断点则停下,没有断点跑完程序 | run |
n/next | 单步执⾏,不进⼊函数内部 | next |
s/step | 单步执⾏,进⼊函数内部 | step |
break/b 行号 | 在指定行号设置断点 | b 10 |
break/b [⽂件名:]⾏号 | 在指定文件的指定⾏号设置断点 | break 10 ;break test.c:10 |
break/b 函数名 | 在函数开头设置断点 | break main |
info break/b | 查看当前所有断点的信息 | info break/b |
finish | 执⾏到当前函数返回,然后停⽌ | finish |
print/p 表达式 | 打印表达式的值 | print start+end |
p 变量 | 打印指定变量的值 | p x |
set var 变量=值 | 修改变量的值 | set var i=10 |
continue/c | 从当前位置开始连续执⾏程序 | continue |
delete/d breakpoints | 删除所有断点 | delete breakpoints |
delete/d breakpoints n | 删除序号为n的断点(breakpoints可不加) | delete (breakpoints) 1 |
disable breakpoints | 禁⽤所有断点 | disable breakpoints |
enable breakpoints | 启⽤所有断点 | enable breakpoints |
info/i breakpoints | 查看当前设置的断点列表 | info breakpoints |
display 变量名 | 跟踪显⽰指定变量的值(每次停⽌时) | display x |
undisplay 编号 | 取消对指定编号的变量的跟踪显⽰ | undisplay 1 |
until X⾏号 | 执⾏到指定⾏号 | until 20 |
backtrace/bt | 查看当前执⾏栈的各级函数调⽤及参数 | backtrace |
info/i locals | 查看当前栈帧的局部变量值 | info locals |
quit | 退出GDB调试器 | quit |
注意:
- gdb会记录最新的一条命令,按回车默认是执行该命令。如 list 1 后按回车,会从上次代码结束的位置继续显示。
- 在一个调试周期内,断点的编号是递增的。例如我们设置了两个断点,它们的序号是1,2,然后我们删除它们在重新设置断点,这时断点的编号是从3开始的。
- 禁用和启用断点的关键字(disable、enable)后可以直接加断点编号,表示禁用或启用某一个断点。
4.3、常见技巧
上⾯的基本调试还是麻烦,虽然是⿊屏,但是还是想看到代码调试,推荐安装cgdb:
Ubuntu: sudo apt-get install -y cgdb
Centos: sudo yum install -y cgdb
4.3.1、watch
执⾏时监视⼀个表达式(如变量)的值。如果监视的表达式在程序运⾏期间的值发⽣变化,GDB会暂停程序的执⾏,并通知使⽤者。
(gdb) watch result
Hardware watchpoint 2: result
(gdb) c
Continuing.
Hardware watchpoint 2: result
Old value = 0
New value = 1
如果你有⼀些变量不应该修改,但是你怀疑它修改导致了问题,你可以watch它,如果变化了,就会通知你。
4.3.2、set var 确定问题原因
作用:更改某个变量值,帮助我们进一步锁定问题。
(gdb) set var flag=1 # 更改flag的值,确认是否是它的原因
有时我们调试bug时可能发现是某一个变量值的错误导致的问题,但我们无法确定判断是否准确,这时我们可以通过 set var 修改变量值,在重新通过 run 命令再次调试看结果来观察我们的判断是否准确。这样的好处是避免频繁退出调试去修改代码。
4.3.3、条件断点
- 添加条件断点:
(gdb) b 9 if i == 30 # 9是⾏号,表⽰新增断点的位置
- 给已经存在的断点新增条件:
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555551c3 in main at mycmd.c:20
breakpoint already hit 1 time
2 breakpoint keep y 0x0000555555555186 in Sum at mycmd.c:9
(gdb) condition 2 i==30 #给2号断点,新增条件i==30
注意:
- 条件断点添加常⻅两种⽅式:1. 新增 2. 给已有断点追加。注意两者的语法有区别,不要写错了。
- 新增:b ⾏号/⽂件名 : ⾏号/函数名 if i == 30(条件)
- 给已有断点追加:condition 2 i==30,其中2是已有断点编号,没有if。