当前位置: 首页 > article >正文

【Linux】--- 基础开发工具之makefile、git、gdb

 Welcome to 9ilk's Code World

       

(๑•́ ₃ •̀๑) 个人主页:       9ilk

(๑•́ ₃ •̀๑) 文章专栏:   Linux网络编程    


本篇博客我们介绍剩下的三个工具 ---  自动化构建make/Makefile , 版本控制器Git , 调试器gdb/cgdb。


🏠 make/Makefile

🎸 什么是make/Makefile

在大型项目中会有许多源文件,那么这些文件的编译工作和编译顺序是一个较为繁重的事情,如果有工具能帮助我们进行这些编译工作,就会节约时间提高效率,因此产生了自动化构建代码的工具:make/Makefile。

  • 会不会写makefile,从侧面说明一个人是否具备完成大型工程的能力。
  • 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一
    系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。
  • makefile带来的好处就是 -- "自动化编译",一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率
  • make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这
    个命令,比如:Delphi的make,VisualC++的nmake,Linux下0GNU的make。可见,makefile
    都成为了一种在工程方面的编译方法

总结make是一个命令,而makefile是一个在当前目录下存在的一个具有特定格式的文本文件,内容是如何编译代码形成可执行程序;两者搭配使用,完成项目自动化构建

🎸 makefile基本语法

(1)准备自动化构建所需源文件和makefile

zhuang@VM-8-14-ubuntu:~/Test$ touch pro.c
zhuang@VM-8-14-ubuntu:~/Test$ touch makefile
zhuang@VM-8-14-ubuntu:~/Test$ ls
makefile  pro.c

注意 :  创建makefile时首字母可以小写也可以大写,但不能写成其他形式,否则make识别不出。

(2)编写makefile

proc:proc.c
    gcc -o proc proc.c

上面语句的意思是:一开始的proc表示要生成的可执行程序名字,而生成proc需要借助proc.c。对proc.c的操作是用gcc进行编译,这样就能生成proc文件,这里需要理解两个概念:依赖关系和依赖文件。

依赖关系:对于proc:proc.就代表一组依赖关系,即proc文件的生成需要依赖于proc.c文件。冒号右边代表需要依赖的文件,左边是依赖这些文件所生成的文件。依赖文件列表可以是0-n个。

依赖方法:光有物质还要实践,即我们如何操作依赖文件才能得到目标文件!其中图中gcc -o proc proc.c就是依赖方法。依赖方法必须以tab开头!

注意:依赖方法其实也有多个命令

  proc:proc.c
     gcc -o proc proc.c
     echo "haha"
     echo "haha"

现象:

  • echo是用来帮我们打印内容到显示器的方法,Makefile在执行时会自动打印方法名称。
  • 如果不想回显可以使用@关闭回显。
proc:proc.c
   @gcc -o proc proc.c
   @echo "haha"
   @echo "haha"

例子理解

比如你是个大学生,你月底需要跟父亲拿生活费,你跟你父亲形成的就是一种依赖关系,依赖他生存,而向父亲要生活费就是声明依赖方法。

总结:我们要做成一件事都必须有依赖关系+依赖方法!因此makefile本质是依赖方法和依赖关系的集合!

(3)编写依赖文件

  #include<stdio.h>
   
  int main()
  {
    printf("hello proc\n");
    return 0;
  }

(4)make命令构建

(5)清理项目

proc:proc.c
        @gcc -o proc proc.c
        @echo "haha"
        @echo "haha"
.PHONY:clean
clean:
        rm -f proc

说明:

  • 项目也是需要清理的。
  • .PHONY:clean。意思是声明一个伪目标,clean是目标名称,可以随便取名。

现象:

🎸 理解PHONY

(1) 几个现象

  • .PHONY修饰clean
proc:proc.c
        @gcc -o proc proc.c
.PHONY:clean
clean:
        rm -f proc

我们可以看到,第一次使用make指令是可以成功的,可以让proc.c经过gcc编译,但是如果继续使用make,就会提示上面的报错信息,而这个报错信息产生的原因其实是因为proc文件不需要被更新,也就是说,通过一些比较,是可以让一些代码不再重新编译的,或者说,可以让一些操作不被重复执行,因为执行这些操作没有必要。

  • .PHONY修饰proc
#.PHONY:proc
proc:proc.c
        @gcc -o proc proc.c
#.PHONY:clean
clean:
        rm -f proc

此时我们可以看到proc.c文件可以不断被重新编译,不断形成proc目标文件

  • 两者都不修饰
#.PHONY:proc
proc:proc.c
        @gcc -o proc proc.c
#.PHONY:clean
clean:
        rm -f proc

我们可以看到make clean可以不断进行,但是make生成目标文件pro则不允许多次重新编译:

(2)文件的ACM时间

如果我们想比较文件的相关时间属性,可以使用Linux一条命令进行查看:

stat 文件名

通过这个指令我们可以了解到文件的各种信息,其中就包括文件的几个修改时间,我们需要了解一下文件的这几个修改时间:

  • 源文件和可执行程序都是文件,而文件 = 内容 + 属性,文件修改时间包括文件内容修改时间和文件属性修改时间。
  • Access:文件最近访问时间。在Linux早期版本中,每当文件被访问时,其atime都会更新。但这种机制会导致大量的IO操作。
  • Modify : 文件最近内容被修改的时间。
  • Change : 文件最近属性被改变的时间. 

注意:文件内容修改其实也会影响文件属性,因为文件大小这个属性收到影响,因此,Modify的改变会连锁影响Change和Access。

( 3 )  几个问题

Q1: 为什么Makefile和make不让使用者随时重新编译,而是会做出一定限制?怎么判断何时要进行修改?

1. 原因是为了提高效率,Makefile通过时间判断,源文件内未发生改变,和之前的编译结果是一样的,此时不会让用户进行重新编译,提高效率。

2. Makefile通过对比各自的Modify Time来判断的。如果编译出来的可执行程序时间晚于源文件,此时意味着源文件可能进行了某些修改,需要重新编译;但是如果源文件修改时间晚于可执行程序,就可以认为源文件在编译形成可执行程序这段时间内没有修改,没有必要再进行重新编译,为了提高效率使用原来已经编译好的即可。

3.证明touch -m能更新文件的时间都到最新,我们不让PHONY修饰proc,当提示无法编译一下,我们touch一下就能重新编译:

Q2 :  .PHONY是怎么做到能重复编译的?

既然Makefile是通过对比可执行程序和源文件之间Modify Time的新旧来决定是否重新编译的,那么PHONY就是能让make忽略源文件和可执行目标文件的M时间对比,即PHONY可以让依赖方法忽略时间对比的

Q3 : 为什么PHONY不修饰clean,照样可以重复编译?

rm本来就不关心时间,直接删就行,但是还是建议带上PHONY,因为依赖方法可能有除了rm之外的命令,其中有可能关心时间的!

🎸 拓展语法

proc:proc.o
    gcc proc.o -o proc
%.o:%.c
    gcc -c $<

1. % : makefile语法中的通配符,这里%.c指的是会把当前目录下所有.c文件展开到依赖依赖列表中

2. $< : 指的是将依赖关系冒号右侧的依赖文件列表一个一个的交给gcc -c选项,形成同名的.o文件。

理解:比如当前目录下有proc1.c proc2.c proc3.c三个文件,然后在依赖关系替换成%.o : proc1.c proc2.c proc3.c。也就是说会执行三条gcc -c,依次形成对应的.o文件。

bin=proc
src=proc.o

$(bin):$(src)
    gcc $^ -o $@
%.o:%.c
    gcc -c $<

1. makefile中也可以定义变量,如上面的bin变量。

2. $()表示替换变量

3. $@:代表目标文件名

4. $^代表所有的依赖文件列表,注意是一次性给全部列表,跟$<不同

bin=test
src=test.o

$(bin):$(src)
    gcc $^ -o $@
%.o:%.c
    gcc -c $<

.PHONY:clean
clean:
    rm -f $(src) $(bin)

如上就是一个通用形成可执行程序的makefile模板。

其他拓展选项:

SRC=$(wildcard *.c)  //使用wildcard函数获取当前所有.c文件名
OBJ=$(SRC:.c=.o)     //将SRC的同名.c替换成.o 形成目标文件列表

🎸 理解make/Makefile基本原理

(1)基本原理1

Q : 为什么我们之前代码进行生成proc目标文件时,可以不使用命令make proc,而可以直接make呢? 

我们看以下代码:

.PHONY:clean
clean:
        rm -f proc

proc:proc.c
        @gcc -o proc proc.c

现象:

由此我们可以得到结论 : makefile文件会被make从上到下开始扫描,第一个目标名会作为缺省形成,如果我们要执行其他组的依赖关系和依赖方法,我们可以make+name(不建议把清理项目放最前防止误操作)

(2) 基本原理2

Q : 如果make在执行gcc命令时发生错误会怎么样?

我们可以打印信息进行观察:

proc:proc.c
        @echo "开始编译.."
        @gcc -o proc proc.c
        @echo "结束编译.."

.PHONY:clean
clean:
        @echo "清理完成"
        @rm -f proc

源文件:

#include<stdio.h>
#include<stdlib.h>
int main()
{
  printf("hello proc\n") //语法错误
  return 0;
}

现象:

我们可以得到:makefile/make在执行gcc命令时,如果发生语法错误就会终止。

(3) 基本原理3

Q:make是如何工作的?

proc:proc.o
        gcc proc.o -o proc
proc.o:proc.s
        gcc -c proc.s -o proc.o
proc.s:proc.i
        gcc -S proc.i -o proc.s
proc.i:proc.c
        gcc -E proc.c -o proc.i

.PHONY:clean
clean:
        @echo "清理完成"
        @rm -f proc

现象:

在默认方式下,我们只输入make命令,那么:

1. make会在当前目录下找名字叫"Makefile"或"makefile"的文件。

2. 如果找到,它会找⽂件中的第⼀个⽬标⽂件(target),在上⾯的例⼦中,他会找到 proc这

个⽂件,并把这个⽂件作为最终的⽬标⽂件

3. 如果proc文件不存在,或是proc所依赖的后面的proc.o文件的文件修改时间要比proc这个文件新(可以用touch测试),那么,他就会执行后面所定义的命令来生成proc这个文件。

4. 如果proc所依赖的proc.o文件不存在,那么make 会在当前文件中找目标为proc.o 文件的依赖性,如果找到则再根据那一个规则生成 proc.o文件。(这有点像一个入栈的过程)。

5.依次类推,直到找到存在的文件,然后依次执行依赖方法,依次得到proc.i proc.s proc.o 最终得到proc(类似弹栈的过程)。

6. 这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件

7. 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make是不理的

8. make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,就不进行自动化构建了。

总结 : make解释makefile时,是会自动推导的,推导过程不执行依赖方法直到推导有依赖文件存在,然后再逆向执行所有的依赖方法。

(4) 基本原理4

bin1=proc
src1=proc.c

bin2=code
src2=code.c

$(bin1):$(src1)
    gcc $^ -o $@

$(bin2):$(src2)
    gcc $^ -o $@

现象:

我们可以得到结论:makefile默认只形成一个可执行程序如果想形成多个呢?

bin1=proc
src1=proc.c

bin2=code
src2=code.c

.PHONY:all
all:$(bin1) $(bin2)

$(bin1):$(src1)
         gcc $^ -o $@

$(bin2):$(src2)
        gcc $^ -o $@

我们可以利用原理三形成多个可执行程序,PHONY保证all被总是被形成,all依赖于bin1,bin2,而他们又依赖于向下的依赖关系,往下推导形成bin1和bin2,再逆向执行空的依赖方法,就能形成多个可执行程序了。

 🏠 Git

🎸 什么是git

(1) 安装git:

yum/apt install -y git

(2) 如何理解git && gitee && github

🌰理解:

1. 假设A大学的学生张三和李四是室友,你们有一个过分严格的"大学物理实验"老师,张三去交作业的时候,起初老师让他去修改,修改几次之后却让他交最初版本,可是张三修改了好几次已经丢失了最初版本了,李四知道后,每次向老师上交的时候都会保存当前版本,这样老师要哪个版本他都能直接交哪个版本的实验报告。 --- 这里李四相当于是在进行版本控制的工作

2. 李四顺利完成实验报告之后,想帮助其他同学通过这个老师的考核,但是一个年级的同学还是比较多的,于是他编写了一个实验报告版本控制程序,部署了服务器搭建网站,用来保存不同同学的不同版本。 ---- 这个实验报告版本控制程序就相当于是git。

3. 李四的好友,也就是B大学的小王,在他们大学也遇到类似这样的老师,由于client和server是一体的,版本控制的功能,任何人都可以随意架设,于是小王找李四要了源程序,然后部署服务器搭建网站。 --- 李四和小王依托git搭建的网站就相当于gitee和github

总结:git是一款版本控制器,便于我们应对事故的发生,进行版本的更迭和替换;而gitee和github就是基于git进行的版本控制。

(3)版本控制器

所谓版本控制器,就是能让你了解到一个文件的历史,以及它发展过程的系统。通俗讲就是一个可以记录工程的每一次改动和版本更迭的一个管理系统,同时也方便多人协同作业。目前最主流的版本控制器就是Git,Git可以控制电脑上所有格式的文件,对于我们开发人员来说,Git最重要的就是帮助我们管理软件开发项目中的源代码文件!

🎸 Git特点机及本质

1. 去中心化 : 每个开发者在本地拥有一个完整的代码仓库副本,而不是依赖一个中央服务器来存储和管理代码,开发者可以在本地独立完成大部分操作且互不影响。

2. 本地管理同步远端:我们本地可能会因为磁盘坏了/电脑坏了等原因而导致版本丢失,此时我们可以在远端部署git,需要的时候可以将本地多个版本同步到远端,如果磁盘坏了等意外就可以从远端拉取,这个远端就类似gitee/github。

3. 用于共享协作:项目往往是需要多人协作的,个人如果想了解项目代码,可以从项目的远端仓库进行clone到本地,你写完自己负责部分的代码想让团队其他成员部分共享协作,此时就可以同步到远端仓库。

总结:git本质是多版本控制,去中心化,分布式用于共享协作的软件,它在本地管理,然后把本地多个版本同步到远端,客户端和服务器是一体的。

🎸 git操作

  • 获取远端仓库
git clone 仓库链接
  • 查看git仓库状态
git status

  • 文件添加到git中(存放到git暂存区中,临时添加到本地仓库)
git add 文件/目录
  • 真正提交到本地仓库
git commit -m "描述"
  • 本地仓库和远端仓库同步
git push

🎸 补充细节

1. git add时是将文件提交到git的暂存区中,它的作用是让用户可以有选择地提交变更,而不是一次性提交所有修改。同时当我们把源文件放入到项目仓库中,默认这个源文件和git没有任何关系,因为还没进行git三板斧,文件未进入.git

2. 第一次使用需要你进行配置用户名和密码,将命令复制下来改成自己邮箱和用户名即可。邮箱=gitee/github注册的邮箱。

3. .gitignore:表示忽略掉不想本地托管/上传的文件,把特定后缀添加到.gitignore

文件里都是不能上传的文件的后缀:

4. git仓库要提交,必须保证本地仓库的内容和远端仓库内容一致! 比如linux和windows要同时用,这时就会冲突,此时需要pull拉取。如果修改了同一个文件git不会覆盖,而是会把修改暴露出来,由程序员自己解决冲突。

<<<<<<< HEAD
这是当前分支的修改内容
=======
这是另一个分支的修改内容
>>>>>>> branch-name

🏠 gdb/cgdb

🎸 gdb

gdb(GNU Debugger)是一个开源的、功能强大的调试工具,主要用于调试程序。它最初是为C语言开发的,但如今也支持多种编程语言,包括C++、Fortran、Pascal等。GDB是GNU工具链(GNU Compiler Collection,GCC)的重要组成部分,广泛应用于软件开发和调试过程中。

(1)安装gdb

  • 验证是否安装gdb
gdb --version
  • 安装
yum/apt install -y gdb

(2) 使用gdb调试前提

  • 程序发布方式有两种,debug模式和release模式,linux gcc/g++出来的二进制程序,默认是release模式。
  • 要使用gdb调试,必须在源代码生成二进制程序的时候,加上-g选项,如果没有添加,程序无法被编译。

现在我们准备两个可执行程序,一个添加-g选项,一个没有:

我们可以使用readelf命令来读取可执行程序的管理信息:

zhuang@VM-8-14-ubuntu:~/Test$ readelf -S proc | grep -i debug
zhuang@VM-8-14-ubuntu:~/Test$ readelf -S proc-debug | grep -i debug

我们可以看到加上-g选项之后,可执行程序会多出来一部分调试信息,体积比正常的可执行程序要大:

(3)基本使用

  • gdb调试后面直接带可执行程序,也可以直接带路径

gdb myexe --- 当前路径
gdb ../myexe
  • 退出调试:quit / ctrl d

  • 使用list/l来罗列我们想要看到的源代码

list / l + 行号 : 显示binFile源代码,接着上次的位置往下列,每次列10行。

list / l + 函数名:列出某个函数的源代码(也是每次展现十行)

list / l 行号之后不断回车可以显示完全部代码,因为gdb会记录最新的一条命令,而直接回车是默认直接执行最新命令,比如l 1之后不断回车则会自动执行最近的l 1命令

  • run / r:直接把程序跑完(没打断点时)

  • 断点调试操作

1. 打断点:b + 行号

2. 查看断点:info b

3. 删除断点:d + 断点编号

指定源文件的行号和函数打断点:

注意:在一个调试周期下,断点值是线性递增的,比如你打了三个断点编号是1 2 3 ,删除之后,下次从4开始编号;如果退出调试之后重新进入调试打断点,编号从1开始

4. 禁用断点:disable breakpoint + 断点编号

5. 启用断点 : enable breakpoint

6. 打了断点之后再r相当于是在vs下打断点之后按f5进行跳转到断点处

7.  n 或 next:单条执行  (相当于是vs调试中的F10)

8. r到端点处后想重新开始跑的话就再run一下:

9. s或step:进入函数调用.相当于是vs下f11

bt:查看当前调用堆栈

  • 变量跟踪

display + 变量名:跟踪查看一个变量,每次停下来都显示它的值,每个跟踪的变量都会给一个编号,比如下方count变量编号为1

undisplay + 编号 :   取消对变量跟踪

注:变量编号也是在一个调试周期内线性递增的。

  • p +变量:打印变量的值  可以打印表达式值(比如p 1+1)也可以打印指针

  • until + 行号:跳转到当前作用域的x行

注:如果行号不在当前函数作用域内则直接跳出当前函数作用域

🎸 cgdb

(1)什么是cgdb

我们发现使用gdb调试时无法看到完整代码,而cgdb可以让代码可视化。

安装:

sudo yum/apt install -y cgdb

使用:

cgdb + 文件

退出使用quit,调试按回车:

光标窗口切换:gdb窗口按 Esc 按键,会切换到源码窗口 ; 源码窗口,按i键会切换到gdb窗口

(2)常见操作

一些操作跟gdb的使用是一样的,我们讲解一些其他操作。

1. continue / c :从一个断点到下一个断点

2. Finish:把当前函数代码跑完

总结 : 断点 + finish + until + c :快速定位问题

3. info locals:自动化显示当前函数栈帧中定义的变量(相当于vs中的自动窗口)

4. watch:执行时监视一个表达式(如变量)的值。如果监视的表达式在程序运行期间的值发生变化,gdb会暂停程序的执行,并通知使用者

 其实类似也打了个断点,只不过是watchpoint:

注:我们可以用watch来监视一些常量(我们认为不应该变的变量),看是否发生变化。它变化时才会提示,也就是不会有垃圾数据

5. set var 变量 = : 修改变量的值

6. 条件断点:达到某个条件才能触发的断点

(1) 新增(一个天然有条件的断点)

格式 : b + 行号 + if 条件

此时条件断点会按照设定条件,当条件满足时触发一次

(2) 给已有断点追加

格式:condition 断点编号 + 条件


完。


http://www.kler.cn/a/548672.html

相关文章:

  • Python 面向对象的三大特征
  • 4.buuctf [SWPU2019]Web1及知识点
  • matlab飞行姿态pid控制
  • Ubuntu 下 nginx-1.24.0 源码分析 -ngx_ssl_error 函数
  • HAProxy 实现 MySQL 服务器负载均衡实验
  • Vue 发送 PDF 文件链接到 WinForm 程序进行打印
  • 如何通过5G和边缘计算提升工业远程运维效能?
  • 神经网络新手入门(4)Transformer的创世纪(2017)
  • Win10下安装 Redis
  • 【云安全】云原生- K8S kubeconfig 文件泄露
  • 147,[2] BUUCTF WEB [BSidesCF 2019]Kookie
  • 算法1-1 玩具谜题
  • 2.buuctf [CISCN 2019 初赛]Love Math
  • c++中std::thread构造函数的注意事项
  • 2.4.2 常量的定义与使用
  • sql注入中information_schema被过滤的问题
  • windows使用中碰到的一些问题
  • 基于 Ollama 工具的 LLM 大语言模型如何部署,以 DeepSeek 14B 本地部署为例
  • MATLAB计算反映热需求和能源消耗的度数日指标(HDD+CDD)(全代码)
  • 循环学习率CLR——Cyclical Learning Rates