【Linux扫盲】Linux新手常用工具合集

81 阅读28分钟

0 读前须知

  • 请先学习完基础篇再来本篇学习

1 包管理器--yum

1.1 Linux中安装软件的途径

1.1.1 源码安装
  • 源码安装即在开源网站上下载了其他人的源代码,自己在Linux下手动编译源代码让他跑起来
  • 但Linux版本多而杂,软件的版本也是多而杂,你根本就不知道这个软件在当前Linux版本下的兼容性,风险是相对很大的
  • 并且源码安装很容易会出现一个致命的问题,这点我们在下一小节会提到
1.1.2 软件包安装--rpm
  • 软件包安装指这个软件的作者手动将软件打包好了,用户只需要下载这个包再执行对应文件就行了

  • 但问题是,现在很多软件需要很多"前置库",类似于我的世界游戏里打模组,某个模组可能需要一个甚至多个前置模组,你要用的这个软件可能用了很多前置的库,这样的前置库我们称为"依赖"

  • 而现在很多软件的作者并不会一并将软件的依赖一并打包进rpm中,就会出现依赖缺失的情况

  • 不知道你有没有玩过学习版游戏,error:XXX.dll缺失就可以理解为依赖缺失

  • 如果你访问这个软件作者的开源网站的主页,你可能会看到这个作者提到要安装某些库,但往往会出现安装了依赖但是依旧出现问题,一般是因为依赖的版本对不上,就会很容易出现依赖版本兼容性问题

  • 著名博主"Cherno"在似乎它的视频里提到过,寻找依赖是一件非常麻烦且痛苦的事情,并且毫无意义,其他语言的包管理器都或多或少有一定意义,但CPP的包管理器实在是太垃圾了,所以他会倾向于打包好对应版本的库

  • 当然哈,在Linux上,包管理器还是很好用的,也就引出了我们下面这小节

1.1.3 包管理器安装--yum(CentOS),apt/apt-get(ubuntu)
  • 用包管理器安装软件,会自动找好对应的依赖,完全不需要担心依赖缺失和依赖版本兼容性问题

1.2 安装与目录

  • Linux的软件安装并不是安装在某一个单独的目录下的,而是安装在多个目录下,例如/usr/bin(可执行程序的存放位置),/usr/lib(库的存放位置),/etc(配置文件存放位置),/tmp(临时文件存放位置)等等

  • 其实不光是Linux,哪怕是Windows,安装软件也不一定是在一个单独的目录下,如果你试图寻找过游戏存档,你会发现游戏的存档不一定是在游戏自己的目录下,也有可能在C:\Users\oldking\AppData\Local这个目录下

  • 甚至说安卓也不是在一个单独的目录下,我曾经不小心删过一个目录在/storage/emulated/0/Android/obb/下的一个文件,导致Musedash的存档直接没了

1.3 安装与权限

  • 我们在上一小节提到过,安装软件会在根目录的多个目录下拷贝文件,既然是在根目录下做这些操作,所以安装软件一定是需要root权限的

  • 那为什么root安装的软件其他普通成员也可以用呢?

  • 我们把/usr/usr/bin的属性看一看

drwxr-xr-x. 13 root root  4096 Jul 11  2019 usr
dr-xr-xr-x.  2 root root 20480 Sep 23 12:36 bin
  • 不难发现,这两个目录对other开放了读与执行,所以说所有人都可以访问这个目录,使用这个目录里的可执行文件,但不允许other修改/新增/删除文件

  • 所以说我们只需要安装一次软件,就可以让这台机器的所有用户同时使用这个软件了

1.4 包管理器与其他

  • 简单来说,包管理器更像是应用商店一样,系统可以不预装这些软件,用户根据系统的使用情况自己在包管理器找配套的软件,且大部分知名好用的软件在包管理器中都是有的
1.4.1 生态与包管理器
  • 包管理器是保证系统生态中非常重要的一环,一个操作系统上软件够多,才能吸引来更多用户和更多开发者,这个系统有更多开发者在上面编写程序,那这款操作系统的软件才多,这是一个正向循环
1.4.2 开发者与钱
  • 我们知道,每一个linux系统背后都会有一个社区,市区里面会有很多的开发者大佬把源码编译成软件,然后上传自己的软件到社区的服务器中,用户可以通过包管理器下载社区服务器中的软件进行使用

  • 所有软件都是开源的,所有软件都是免费的,服务器的维护费用怎么办,难道开发者做这个软件都是为爱发电吗?

  • 并不是这样的,如果你的软件非常强大,非常受欢迎,有很多人用你开发的软件,在你的软件的基础上再做开发,钱根本就不是问题

  • 因为有这么多的公司依赖你的软件,以来你的系统,你说不维护就不维护了,说寄了就寄了,那公司还怎么玩啊,所以一般情况下,只要开发者发起募捐,就一定不会缺钱,总会有不缺钱的大佬给你打钱的,生怕你死了,就好比是在迪拜当乞丐

  • 所以开源本质上是一种商业模式

1.4.3 源
  • 我们知道,软件的下载一定是需要下载链接的,我们通过官方的下载链接来下载软件,我们称为通过官方源安装

  • 但我们知道国内的网络因为某些原因,访问国外的服务器会比较困难,所以就会有中文社区的开发者自发的建一个服务器,专门把官方源的所有内容镜像到国内的服务器中,我们称其为镜像源

  • 如果我们是购买的国内云服务器厂商的服务器,就不需要更改下载源,如果我们是在本地安装的Linux,可能就需要修改一下下载源,否则很难通过包管理器下载软件

  • /etc/yum.repos.d/目录下就是源的配置文件,我们可以通过修改配置文件的方式来更换源

  • 例如这个CentOS-Base.repo的文件就是基本(稳定)源的配置文件

1.3 包管理器的常用指令

1.3.1 包管理器的软件目录
yum list
  • 我们可以通过搭配grep和管道来筛选所需的软件,例如(因为软件数量很多,所以这里只打印前四行)
yum list | grep "gcc" | head -4
  • 输出
gcc.x86_64                               4.8.5-44.el7                   @base
gcc-c++.x86_64                           4.8.5-44.el7                   @base
libgcc.x86_64                            4.8.5-44.el7                   @base
avr-gcc.x86_64                           4.9.2-1.el7                    epel
  • 其中第一列的.x86_64之前的部分是该软件的名字,x86_64代表该软件运行的平台,即该软件运行在x86架构下,64位机器上

  • 第二列中除了el7以外的部分都是该软件的版本号,el7代表安装在centos7操作系统下

  • 最后一列,epel我们可以理解为测试版本,就是软件没有经过长时间运行,没有测试过稳定性,而base则是稳定版,经过了长时间大量地测试,一般是没有稳定性问题的

  • 安装epel软件可能会出错,我们可能得安装一下扩展软件源

yum install -y epel-realse
1.3.2 安装软件
yum install [软件名]
  • 如果询问了是否安装,回答y

  • 可以在yum后面加-y跳过询问强制安装

yum -y install [软件名]
1.3.3 删除软件
yum remove [软件名]
  • 同样可以通过-y跳过询问
yum -y remove [软件名]
1.3.4 清理/生成yum缓存
  • 清理缓存
yum clean all
  • 清理yum缓存一般用在更换yum源之后

  • 清理完缓存之后需要生成新的缓存

yum makecache
1.3.5 更新yum
yum update
  • 如果你的yum没法工作,可以试着更新一下yum
1.3.6 查看yum源的速度
yum repolist

2 编辑器--vim

  • 我们之前使用的VS2022,我们称为集成开发环境,即字面意思,就是集成了多套工具的开发环境,编辑代码的时候VS自动给你调出来编辑器,调试的时候就自动给你调出来调试器

  • 但是在Linux终端下,就只能将这些工具打散而不是整合到一个软件中,我们现在所学习的就是其中一个,即编辑器 -- vim

2.1 vim的三大模式

2.1.1 命令模式
  • 当我们新建一个test.c文件之后,使用以下命令可以用vim打开此文件
vim [文件名]
  • 最开始进入的时候vim无法进行编辑文本的工作,因为此时vim处于命令模式下

  • 该模式允许用户使用一些命令对文本进行一些操作,例如删除某个字符/单词/行/段等等,同时也包含了复制,黏贴,快速翻页,搜索等等功能

  • 这些命令只需要按键盘上对应的按键就行了,不需要回车来输入

2.1.2 插入模式(编辑模式)
  • 顾名思义,允许我们对文档进行编写,一般左下角会出现一个-- INSERT --表示我们正处在插入模式
2.1.3 底行模式
  • 底行模式可以执行一些特殊的命令,例如文件的保存和退出,显示行号等等

  • 底行模式顾名思义就是在编辑器的最底行留一行给你输指令,进入该模式之后你会看到有个光标在一直闪,说明你可以在该模式下输入指令了,这个底行模式的指令需要回车才可以执行

2.1.4 模式切换
  • 命令模式切换到插入模式:在命令模式下输入命令i即可

  • 插入模式切换到命令模式:按下Esc键即可

  • 命令模式切换到底行模式:输入:(按下Shift + ;)

  • 底行模式切换到命令模式:按下Esc

  • 模式切换的变种情况:

    1. 命令模式下按o:切换到编辑模式并新增一行空行
    2. 命令模式下按a:切换到编辑模式并让指针向后移一格
    3. 命令模式下按s:切换到编辑模式并删除当前光标的字符

2.2 命令模式的指令

  • 光标
  1. gg:回到文件最顶行

  2. shift + g (G):转到文件最底行

  3. [number] + shift + g (number + G):转到指定行号所在的行

  4. shift + 4 ($):转到行尾

  5. shift + 6 (^):转到行首

  6. h:光标左移

  7. j:光标下移

  8. k:光标上移

  9. l:光标右移

  10. b:光标以单词为单位左移

  11. w:光标以单词为单位右移

  12. #(shift + 3):高亮所选单词

  13. n:让光标在高亮单词中切换

  14. Ps:光标操作中指令都可以在前面加上[数字],可以批量完成操作

  • 编辑
  1. yy(连按):复制当前行

  2. dd(连按):剪切当前行

  3. x:删除光标所在位置的字符

  4. X(shift + x):删除光标所在位置之前的字符

  5. r + [目标字符]:替换当前光标处的字符为目标字符

  6. ~(shift + `):将光标处的字母大小写转换

  7. Ps:编辑操作中指令都可以在前面加上[数字],可以批量完成操作,同时除了第五条以外的所有指令都会把文字保存在剪切板中,都可以使用p来完成粘贴

  • 操作
  1. u:撤销

  2. ctrl + r:撤销撤销操作(恢复操作)

2.3 底行模式常用命令

  1. w/q/wq + !:强制执行保存/退出/保存并推出

  2. ZZ(shift + zz):强制退出

  3. set nu/nonu:开/关行号显示

  4. vs [filename]:打开当前目录下的文件并分屏(此时分屏出来之后,光标在哪一屏就说明我们正在对哪一屏进行操作,ctrl + ww可以快速换窗口)

  5. ![command]:允许再不退出vim的情况下输入Linux命令

  6. %/dst/src:将内容"dst"替换成内容"src"

  7. /dst:查询dst

  8. noh:取消查询高亮

2.4 其他模式

2.4.1 块操作模式
  • shift + v可以进入块模式,这个模式相当于Windows中用鼠标多选,在块模式中可以使用非常多的命令对选中的文本进行批量化操作,例如批量化删除(选中之后按dd),或是批量化新增文本(选中之后按shift + i进入编辑模式,然后输入新增内容,最后按esc回到命令模式,此时被选中的行列就会新增内容)
2.4.2 替换模式
  • ctrl + r可以进入替换模式,这个模式下可以任意替换你想替换的所有内容

  • 按esc可以回到命令模式

2.5 vim在系统上的指令

  • 在打开文件时,自动定位光标到第[number]行
vim src +[number]
  • Ps:如果wim后的文件并不存在,他会帮你现场创建一个,你只要修改并保存之后他就会出现了

  • 快捷调用上一个指令(这里假设我想调用上一个调用的vim指令)

!vim

甚至

!v

3 gcc/g++

  • gcc是用于编译c语言的编译器,g++是用于编译c/c++的编译器

  • gcc在使用和选项方面和g++并无差异,我们只需要学习一个就行了

3.1 编译原理

  • 这里只简单温故一下编译原理
    1. 预处理 -- 将头文件展开,宏定义替换,删除注释,执行条件编译 -- 生成.i文件
    2. 编译 -- 将预处理过的文件(.i文件)翻译成汇编代码 -- 生成.s文件
    3. 汇编 -- 将编译过的文件(.s文件)转化为机器码(二进制代码) -- 生成.o文件
    4. 链接 -- 将汇编过的文件(.o文件)链接起来 -- 生成可执行程序

3.2 编译原理与命令

  • 如果我们想要使用gcc编译器对.c文件编译并生成可执行程序
gcc src.c -o dst
  • 如果我们只想把.c文件预处理,可以增加以下选项
gcc -E src.c -o dst.i
  • 如果我们想把.c/.i文件处理到编译过后的这一步,可以更改成以下选项
gcc -S [src.c/src.i] -o dst.s
  • 如果我们想把.c/.i/.s文件处理到汇编后的这一步,可以更改成以下选项
gcc -c [src.c/src.i/src.s] -o dst.o
  • 链接这一步可以将多个相互联系的.o文件链接生成可执行文件
gcc src1.o src2.o src3.o -o dst

3.3 条件编译

  • 我们在C语言中学过,条件编译的本质,其实是检测宏定义是否存在,以此来在预处理的过程中,选择性的删除或保留一些代码

  • 这个条件编译有什么用?

  • 事实上软件的多版本,一部分其实就是利用了条件编译,如果一个软件分为完整版和阉割版,完整版付费而阉割版免费,就是利用了条件编译

  • 我们在软件还是代码的时候就写好条件编译,等到要发布版本了的时候,只需要改一下宏定义,编译出来的可执行程序功能相差就会比较大,因为我们在预处理之后就通过条件编译删去了本来应该付费的功能

  • 这样做能提高开发效率,意味着一个开发者或团队只需要维护一个付费版本,就相当于维护了付费和免费两个版本

  • 除了付费和免费,还有包括内核代码的调整等等,只要是需要搞一个阉割版的场景,都可以用条件编译

  • Linux有一条指令,是一个特殊的编译指令,这条指令除了完成正常的编译之外,还会在预处理之前,往文件的第一行添加条件编译

gcc src.c -o dst -D[宏定义类型名称]
  • 这个编译指令不会修改源文件,是在内存层面进行的

3.4 库

  • 我们说的库包含静态库与动态库,简单来说,我们在写代码时包的头文件,实际上里面只包含函数的声明,而函数的定义其实在库文件中

  • 库的文件后缀

    1. Windows动态库:.dll
    2. Windows静态库:.lib
    3. Linux动态库:.so
    4. Linux静态库:.a
3.4.1 Linux中库的命名
  • 动态库
lib[libname].so
  • 例如Linux中c语言库的命名为
libc.so
  • 静态库
lib[libname].a
  • 例如Linux中c语言库的命名为
libc.a
3.4.2 静态库和动态库的使用原理和使用方法
3.4.2.1 动态库
  • 我们编写完一段程序,这个程序中使用了某个动态库的方法,然后进行编译,此时程序会通过头文件找到这个动态库,并做好记录,然后将可执行程序中的所需函数的地址暂时置为空,

  • 如果我们编译完之后需要执行这个可执行程序,在真正执行程序之前,系统会帮我们把需要用到的动态库加载进内存,然后加载可执行程序到内存,并为需要跳转函数的地方添加动态库中该在内存的地址

  • 等到程序执行到需要调用动态库的函数的时候,就直接call到对应的地址

  • 使用方式上,Linux默认不加任何选项的情况下,默认使用的就是动态编译(就是使用动态库进行编译)

3.4.2.2 静态库
  • 区别于动态库,静态库在链接阶段把需要的函数拷贝进源文件中,然后生成可执行程序,这就相当于可执行程序中,直接包含了静态库的代码,执行程序的时候可以直接调用,不需要临时找地址

  • 静态库的使用方式上,需要在编译命令末尾添加选项-static

  • 例如

gcc [srcfilename].c -o [dstfilename] -static
3.4.3 动态库与静态库在使用上的区别
  1. 灵活性:

  2. 使用动态库的时候,因为可执行程序和动态库文件是分离的,所以你可以对任意一个文件进行修改,或者修改其源文件再编译,本质上,我们在正常写C/CPP的代码的时候,就是更改源文件,而动态库文件咱们压根就没动过

  3. 而使用静态库则可能需要整个重新编译

  4. 内存浪费:

  5. 动态库可能不止会有一个程序会使用,可能会有非常多的程序使用同一个动态库,例如C语言的标准库文件,系统里几乎所有的程序都会使用这个库文件,而此时内存中其实只需要加载一份库文件就行了,不需要加载很多份,本质上动态库就是一个公共库,程序要方法拿就行了

  6. 静态库因为是和可执行程序打包好了,所以如果这个库被经常调用,但所有用这个库的程序全都使用了其静态库的版本,意味着一个使用该库的程序,就会占用一份该库申请的内存资源,一百个该情况的程序就需要占用一百份库申请的内存资源,会很大程度上造成内存浪费

  7. 磁盘空间浪费和内存浪费同理,本质上是存储了多份相同的代码

  8. 文件缺失

  9. 动态库文件如果需要被调用,就一定不能缺失

  10. 相对的,可执行程序就对静态库是否存在不感冒,只需要编译的时候把静态库加进来就行了

3.4.4 库的相关指令
  • 查看该可执行程序需要调用哪些动态库
ldd [可执行程序名称]
  • 查看该可执行程序是静态链接还是动态链接
file [可执行程序名称]
3.4.5 库与文件
  • 如果有人想要用我们自己写的的接口,但你不想提供源文件给他,此时你可以提供给他.o文件,它只需要自己链接一边就行了,但如果此时他需要你的大量的.o文件,又不可能手敲一个个链接,这样太麻烦了,所以你干脆把所有的.o文件全都打包好,成为一个库,别人想用的时候,直接动态链接你提供的库就行了

  • 这也是为什么很多人会热衷于保留.o文件

4 Makefile

  • 想象一个场景,如果你想编译一个很大很大的工程,这个工程包含几十上百个.c文件,如果还老是手打命令是不是就会非常麻烦,于是我们迫切希望有一个类似于脚本的东西能帮助我们快速编译

  • 于是Makefile便诞生了

4.1 Makefile的使用初识

  • 假设我们在一个目录中创建了一个test.c文件写了一些内容,如果我想使用Makefile实现半自动地完成编译工作

    1. 在同目录下创建一个文件取名叫Makefile(不要后缀,名字固定不能变,首字母可以大/小写)
    2. 编辑文件
    test:test.c
    [tab]gcc -o test test.c
    

    3. 回到目录下执行命令make

  • 此时Makefile就会自动帮你编译生成可执行文件

4.2 依赖关系与依赖方法

  • 注意这里的[tab]不能是四个空格,只能是[tab]
test:test.c
[tab]gcc -o test test.c
  • 简单来说,可执行文件和源文件有着某种联系,更确切地说,是可执行程序依赖着源文件,即test:test.c,我们称其为"依赖关系"
  • 而你不仅要给工具阐述他们俩的关系,同时还要阐述源文件如何做才可以生成可执行程序,[tab]gcc -o test test.c即这也就是"依赖方法"
[目标文件]:[依赖文件列表]
[tab][依赖方法]
  • 注意,这里是依赖文件列表,意味着依赖文件可以不只是一个(多个文件用空格隔开)

  • 举个例子来说,你的生活依赖着写代码赚钱,这就是依赖关系,那具体该怎么做才可以挣到钱(是做前端开发?还是干运维?具体怎么写代码?),就是依赖方法

4.3 伪目标的使用

  • 咱直接看Makefile
test:test.c
	gcc -o test test.c

.PHONY:clean
clean:
	rm -f test
  • 这串内容的上半部分我们已经学习过了,我们着重了解一下下半部分
.PHONY:clean #这里的clean是伪目标,目的是告诉make始终执行依赖方法(这一点我们后面会了解到)
clean:       #这里的clean和上半部分一样,这里的clean也算是目标文件,依赖文件为空
	rm -f test #这里也是依赖方法,只不过这个依赖方法不会生成叫clean的目标文件
  • 此时我们使用命令make clean,make就会自动调用rm -f test删除可执行文件,或者说删除当前目录下存在的名字为test的文件

4.4 Makefile命令的调用逻辑(初识)

  • 还是原来的实例,但我们对其做一些修改
.PHONY:clean
clean:
	rm -f test
test:test.c
	gcc -o test test.c
  • 我们能看到这里直接将上下两半部分的代码交换了,此时使用make,你会发现实际调用的是rm -f test,而使用make test调用的是gcc -o test test.c,这意味着默认前两行对应的调用指令始终是make(我们暂时先这么理解),后面所有行的调用指令都是make [目标文件名]

  • 接着我们改回原来的样子

  • 我们试着make3次

$ make
gcc -o test test.c
$ make
make: `test' is up to date.
$ make
make: `test' is up to date.
  • 发现了什么?如果进行多次make,make会提醒你test已经是最新的了,这是怎么做到的?我们接着看
4.4.1 文件的属性 -- 时间
  • 我们使用以下命令可以查看文件的详细属性,其中就有文件属性的三大时间,即"ACM"时间
$ stat test.c
  • 返回
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 17:35:01.783494506 +0800
Modify: 2024-11-28 17:34:59.748405352 +0800
Change: 2024-11-28 17:34:59.748405352 +0800
  1. Access就是查看时间 -- 它会记录用户最后读取该文件的时间
  2. Modify就是修改(内容)时间 -- 它会记录用户最后一次更改该文件内容的时间
  3. Change就是修改(属性)时间 -- 它会记录用户最后一次更改该文件属性的时间
4.4.1.1 Access时间
  • 比方说我们读一下test.c文件
$ stat test.c
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 17:35:01.783494506 +0800
Modify: 2024-11-28 17:34:59.748405352 +0800
Change: 2024-11-28 17:34:59.748405352 +0800
 Birth: -
$ cat test.c
#include<stdio.h>

int main()
{
    printf("hello linux!\n");
    return 0;
}

$ stat test.c
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 17:35:01.783494506 +0800
Modify: 2024-11-28 17:34:59.748405352 +0800
Change: 2024-11-28 17:34:59.748405352 +0800
 Birth: -
  • 不难看出,Access时间并没有被修改啊,这是因为用户对文件做的绝大部分操作都是读取文件,如果修改Access时间过于频繁,会对系统造成很多不必要的损耗,所以一般情况下之后很久之后不读文件或者读几十上百次文件才能修改到Access时间
4.4.1.2 Modify时间
  • 我们接着修改一下test.c文件
$ stat test.c
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 17:35:01.783494506 +0800
Modify: 2024-11-28 17:34:59.748405352 +0800
Change: 2024-11-28 17:34:59.748405352 +0800
 Birth: -
$ vim test.c
$ stat test.c
  File: ‘test.c’
  Size: 109       	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 17:35:01.783494506 +0800
Modify: 2024-11-28 18:33:06.553155728 +0800
Change: 2024-11-28 18:33:06.553155728 +0800
 Birth: -
  • 可以发现Modify时间确实被改变了,但为什么Change时间也改变了呢?因为修改文件这个行为一定会更改文件属性,文件大小也算是文件属性的一部分,所以Change时间一定会更改
4.4.1.3 Change时间
  • 接着我们来修改一下Change时间
$ stat test.c
  File: ‘test.c’
  Size: 109       	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 17:35:01.783494506 +0800
Modify: 2024-11-28 18:33:06.553155728 +0800
Change: 2024-11-28 18:33:06.553155728 +0800
 Birth: -
$ chmod u+x test.c
$ stat test.c
  File: ‘test.c’
  Size: 109       	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0764/-rwxrw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 17:35:01.783494506 +0800
Modify: 2024-11-28 18:33:06.553155728 +0800
Change: 2024-11-28 18:40:37.894928164 +0800
 Birth: -
  • 可以看到Change时间被修改了,其他时间都没有被修改
4.4.2 判断目标文件修改的逻辑
  • 前面我们提到过多次make,make会提示你,你的可执行文件已经是最新的了

    1. 这里make会检查有没有同名的目标文件
    2. 然后对比目标文件和依赖文件的Modify时间,如果依赖文件的Modify时间比目标文件的更新,就代表依赖文件已经更新过了,此时需要根据依赖方法重新生成一个更新的目标文件
    3. 如果目标文件的Modify时间比依赖文件的更新,就代表依赖文件文件没有修改过,就提醒用户目标文件已经是最新的了
  • 如何验证make确实是靠时间判断有没有修改而不是对比新老文件呢?

  • 例如我们写一个test.c

#include<stdio.h>

int main()
{
    printf("hello linux!\n");
    return 0;
}
  • 此时wq保存退出

  • 使用make命令生成可执行程序

gcc -o test test.c
  • 执行stat test.c
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0764/-rwxrw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 19:07:25.567357277 +0800
Modify: 2024-11-28 19:07:19.856107078 +0800
Change: 2024-11-28 19:07:19.856107078 +0800
 Birth: -
  • 然后再次打开test.c并编辑,修改返回值为1
#include<stdio.h>

int main()
{
    printf("hello linux!\n");
    return 1;
}
  • 此时wq再次保存退出

  • 然后再次打开test.c并编辑,修改回返回值为0

#include<stdio.h>

int main()
{
    printf("hello linux!\n");
    return 0;
}
  • 再次执行stat test.c
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0764/-rwxrw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 19:10:42.065965513 +0800
Modify: 2024-11-28 19:10:47.175189338 +0800
Change: 2024-11-28 19:10:47.176189382 +0800
 Birth: -
  • 可以发现Modify时间已经被修改了
  • 此时我们再使用make命令生成可执行程序
gcc -o test test.c
  • 你会发现它没有报目标文件已经是最新了,并且依旧生成了目标文件,即便依赖文件没有修改任何实质性内容

4.5 伪目标的实际作用

  • 我们知道make判断目标文件修改的逻辑是先判断目标文件存不存在,而伪目标的作用是,告诉make,无论该目录下存不存在名字跟伪目标一样的文件,一律执行该代码段的依赖方法,即取消make对目标文件是否修改的判断逻辑

  • 我们试着把Makefile修改成这样

.PHONY:test
test:test.c
	gcc -o test test.c
.PHONY:clean
clean:
	rm -f test
  • 意味着如果我们一直make,make也不会检查有没有目标文件test,而是死脑筋始终帮我们执行依赖方法
$ make
gcc -o test test.c
$ make
gcc -o test test.c
$ make
gcc -o test test.c
$ make
gcc -o test test.c
$ make
gcc -o test test.c
  • 事实也确实如此

4.5 touch与时间

  • 事实上touch的标准作用从来都不是新建文件,而是更新文件的时间
$ stat test.c
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0764/-rwxrw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 19:12:11.740894003 +0800
Modify: 2024-11-28 19:10:47.175189338 +0800
Change: 2024-11-28 19:10:47.176189382 +0800
 Birth: -
$ touch test.c
$ ls
Makefile  test  test.c
$ stat test.c
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0764/-rwxrw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 20:08:55.828876508 +0800
Modify: 2024-11-28 20:08:55.828876508 +0800
Change: 2024-11-28 20:08:55.828876508 +0800
 Birth: -
  • 可以看到三项时间全部更新了

  • 当然也有一些选项可以指定更新的时间类型

  • -a 修改文件的Access时间,因为Access时间也属于属性的一部分,所以也会同时修改Change时间

  • -m 修改文件的Modify时间,因为Modify时间也属于属性的一部分,所以也会同时修改Change时间

$ stat test.c
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0764/-rwxrw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 20:08:55.828876508 +0800
Modify: 2024-11-28 20:08:55.828876508 +0800
Change: 2024-11-28 20:08:55.828876508 +0800
 Birth: -
$ touch -a test.c
$ stat test.c
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0764/-rwxrw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 20:11:42.803182517 +0800
Modify: 2024-11-28 20:08:55.828876508 +0800
Change: 2024-11-28 20:11:42.803182517 +0800
 Birth: -
$ touch -m test.c
$ stat test.c
  File: ‘test.c’
  Size: 79        	Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d	Inode: 1971179     Links: 1
Access: (0764/-rwxrw-r--)  Uid: ( 1001/ oldking)   Gid: ( 1001/ oldking)
Access: 2024-11-28 20:11:42.803182517 +0800
Modify: 2024-11-28 20:12:25.864066658 +0800
Change: 2024-11-28 20:12:25.864066658 +0800
 Birth: -

4.6 .PHONY [伪目标文件名]的意义

  • 事实上我们不为make进行.PHONY [伪目标文件名]在于,我们希望它每次都进行目标文件的检查,而不是每次都编译,要知道大型项目的编译是非常费时间的,这样会浪费很多时间

  • 而对clean添加.PHONY [伪目标文件名]的原因在于,我们希望每次使用make clean的时候,make都帮我们强制清理临时文件,要知道我们大型项目的临时文件也是非常多的,如果清理不完全,很可能对编译产生一定问题,这就有点像是VS中的清理解决方案,有时候我们修改了一个bug,但重新编译之后发现bug还在,此时我们可以清理完解决方案并再次进行

4.7 Makefile命令的调用逻辑(进阶)

4.7.1 调用链
  • 我们前面有提到过:"默认前两行对应的调用指令始终是make(我们暂时先这么理解)"

  • 事实是这么理解是错误的,我们需要更深地挖掘使用make之后,他帮我们做了什么

  • 我们重新编辑一下Makefile

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

.PHONY:clean
clean:
	rm -f *.i *.s *.o test
  • 当我们使用make命令后,实际会执行的是一大串内容
gcc -E test.c -o test.i
gcc -S test.i -o test.s
gcc -c test.s -o test.o
gcc test.o -o test
  • 准确来讲,make命令后会做的事,应该是执行第一条调用链

    1. 调用make命令后,make会在底层维护一个类似于栈的东西
    2. 检测第一条依赖方法中依赖文件,但此时第一条依赖方法的依赖文件不存在,于是它把第一条依赖方法放进栈中
    3. 检测第二条依赖方法中依赖文件,但此时第二条依赖方法的依赖文件不存在,于是它把第二条依赖方法放进栈中
    4. 检测第三条依赖方法中依赖文件,但此时第三条依赖方法的依赖文件不存在,于是它把第三条依赖方法放进栈中
    5. 检测第四条依赖方法中依赖文件,此时第四条依赖方法的依赖文件存在,于是开始执行第四条依赖方法,借此也生成了第三条的依赖文件
    6. 所以第三条依赖方法从栈顶出栈,并执行生成第二条的依赖文件
    7. 第二条依赖方法从栈顶出栈,并执行生成第一条的依赖文件
    8. 第一条依赖方法从栈顶出栈,并执行生成最终的目标文件
  • 这里相当于模拟调用了递归,没有依赖文件就先"递",有了再"归"回来,这里我们所称的整个递归链,就被称为调用链

4.7.2 变量,抽象代码,提示简单化
  • 假设我需要给依赖文件改个名字,如果调用链很长很长,或者说整个Makefile文件很多地方都用到了依赖文件的名字,那这样改起来就会很费时间,也不好灵活调整

  • 假设我们只想把test.c文件编译生成test可执行程序

BIN=test    # 定义变量BIN表示目标文件
SRC=test.c  # 定义变量SRC表示依赖文件
CC=gcc      # 定义变量CC表示执行命令的名称
FLAGE=-o    # 定义变量FLAGE表示执行命令的选项
RM=rm -f    # 定义变量RM表示删除命令的名称即选项(可以带空格)
            # 我们可以把Makefile的这种变量理解为宏定义和指针的结合体

# 使用时,需要使用`$`来获取变量的值,也就是直接被变量值替换当前变量所占的位置
$(BIN):$(SRC)
	$(CC) $(SRC) $(FLAGE) $(BIN)

.PHONY:clean
clean:
	$(RM) $(BIN)
  • 此时make命令和make clean命令被正常调用

  • gcc test.c -o test

  • rm -f test

  • 此时我们就可以仅通过改变量的值来对整个文件进行修改了

  • 同时,Makefile还为我们提供了更为简便的方式代表目标文件和依赖文件列表

BIN=test
SRC=test.c
CC=gcc
FLAGE=-o
RM=rm -f

$(BIN):$(SRC)

# 使用`@^`代表依赖文件列表,使用`$@`代表目标文件
# 这么干的好处是,依赖文件列表的文件数量此时就可以随意修改了,也会很方便
	$(CC) $^ $(FLAGE) $@

.PHONY:clean
clean:
	$(RM) $(BIN)
  • 我们知道,执行完make或者make clean等等命令之后,实际执行的命令会回显,有时候一个项目文件很多,名字也很长,实际执行完之后,太多了,让用户啃完这一堆玩意不显示,效率也不高,所以我们搞一个法子,简化回显的内容,或者说自定义回显的内容

  • 事实上,一个依赖关系下的依赖方法,可以不止一行,可以有无数行

BIN=test
SRC=test.c
CC=gcc
FLAGE=-o
RM=rm -f

$(BIN):$(SRC)

# 在依赖方法前加`@`,能禁止回显当前行的内容,此时我把所有行的所有内容的回显都ban了
	@$(CC) $^ $(FLAGE) $@
# 同时我增加一行`echo`指令,用于自定义表示回显的内容,命令里也可以用变量,`$@`,`$^`这些东西
	@echo "linking $^ to $@"

.PHONY:clean
clean:
	@$(RM) $(BIN)
	@echo "clean $(BIN)"
  • 所以此时我们执行完makemake clean之后,回显的是我们自定义的内容

  • linking test.c to test

  • clean test

  • 我们还可以接着抽象化我们的代码

  • 我们知道,保留.o文件绝对是一个好习惯,所以实际应用中,我们还需要再抽象一些

BIN=test
SRC=test.c
OBJ=test.o
CC=gcc
LFLAGS=-o
TFLAGS=-c
RM=rm -f

$(BIN):$(OBJ)
	@$(CC) $^ $(LFLAGS) $@
	@echo "linking $^ to $@"

# `%.c`可以获取当前目录下的所有`.c`文件作为依赖文件
# `%.o`会将所有依赖方法生成的`.o`作为目标文件
# 假设我们有100个`.c`文件,此时就会执行一百次依赖方法,每次依赖方法的依赖文件都不一样,每次依赖方法的目标文件也都不一样,以此生成所有的`.o`文件
# `$<`其实就代表所有依赖文件,区别于`$^`,`$<`不代表所有依赖文件列表中的文件,而是每次仅接收一个文件
# 这么做的好处是,可以一键把所有`.c`文件全部生成`.o`文件

%.o:%.c

# `gcc -c code.c`可以自动生成同名`.o`文件
	@$(CC) $(TFLAGS) $<
	@echo "templing $< to $@"

.PHONY:clean
clean:
	@$(RM) $(BIN) $(OBJ)
	@echo "remove $(BIN) $(OBJ)"
  • 我们再进一步,如何一键将所有的.o一次性全部链接成可执行文件呢?
BIN=test
# `Makefile`文件可以直接调用系统指令,意味着可以直接获取当前列表的所有`.c`文件,使用规范如下
# $(shell [命令])
SRC=$(shell ls *.c)
# SRC=$(wildcard *.c) 也能完成同样的操作,`wildcard`就是用来获取同目录下的文件的

# 同时,`Makefile`的变量还可以一键替换"值"中的内容
# 例如以下,将`SRC`的`.c`全部替换成`.o`(`SRC`本身不改变)
OBJ=$(SRC:.c=.o)
CC=gcc
LFLAGS=-o
TFLAGS=-c
RM=rm -f

$(BIN):$(OBJ)
	@$(CC) $^ $(LFLAGS) $@
	@echo "linking $^ to $@"

%.o:%.c
	@$(CC) $(TFLAGS) $<
	@echo "templing $< to $@"

.PHONY:clean
clean:
	@$(RM) $(BIN) $(OBJ)
	@echo "remove $(BIN) $(OBJ)"
  • 我们试着使用一下试试(code1.c code1.h code2.c code2.h Makefile test.c)

  • 执行make

templing code1.c to code1.o
templing code2.c to code2.o
templing test.c to test.o
linking code1.o code2.o test.o to test
  • 执行make clean
remove test code1.o code2.o test.o

5 进度条

  • 实现一个进度条程序

5.1 实现进度条的前置条件

5.1.1 回车换行
  • 现代键盘其实是经过时代发展从老式键盘改进而来的,因为那时候年代还比较久远,民用计算机还没有普及,老式键盘一般只用在打字机上

  • 回车:回车源自打字机的操作,指的是将打字机的打印头从当前字符的位置回到行的起始位置(通常是行的左侧),在旧式打印机中,按下“回车”键时,打印机的打印头会向左移动到行首,但并不会开始新的一行,仍然位于当前行 -- ChatGPT

  • 换行:换行则是指将打印机纸张或打字机的滚轴向下移动一行,这使得下一行的打印内容会显示在下一行,而不是覆盖在当前行的下方 -- ChatGPT

  • 放在C语言中,就是\r\n,只不过C语言中\n集成了\r功能罢了,这是因为现代计算机能够把\n当作回车换行用,于是只用在物理上的\r也就渐渐不常用了

5.1.2 缓冲区
  • 我们可以理解为,缓冲区就是一个中间层,用于平衡硬件之间数据传输和处理速度的速度不平衡的问题

  • 我们创建一个文件test.c

#include<stdio.h>
#include<unistd.h>

int main()
{
    printf("hello linux!");
    sleep(3);

    return 0;
}
  • 执行编译之后的可执行程序之后,你会发现hello linux!根本没有输出在屏幕上,而是等待了3秒之后才输出在屏幕上的吗?难道说sleep(3)执行完之后才执行printf()的吗?

  • 事实是,执行依旧是从printf()开始的,然后才是sleep(3)

  • 那为什么还会产生先sleep的情况呢

  • 这是因为正常情况下,输出的内容不会直接输出在屏幕上,而是先输出在缓冲区,如果向缓冲区输出了一个\n,此时缓冲区就会更新,把缓冲区的内容全部输出在屏幕上,当然还有一种情况,就是程序已经执行完毕了,缓冲区也会进行一次清空

  • 不难发现我们实现的这个版本属于后者,sleep让数据一直停留在了缓冲区里了,然后又没有\n帮助缓冲区输出数据到屏幕,就造成了先休眠再打印的现象

5.2 实现

  • main.c
#include<stdio.h>
#include"progress_bar.h" //bar_flush
#include<unistd.h> //usleep

//模拟网速
double speed = 1;

//模拟下载的函数
void download(double total)
{
    //当前下载量
    double current = 0;
    while(current <= total)
    {
        //刷新进度条
        bar_flush(current, total);
        usleep(3000);
        current += speed;
    }
    //开头加\n用于换行,因为刷新进度条函数不会换行
    printf("\ndownload %.2lfMB done!\n", total);
}

int main()
{
    download(1024);
    download(2024);
    download(140);
    download(194);
    download(100400);
    return 0;
}
  • progress_bar.c
#include<stdio.h>
#include<string.h> //memset
#include<unistd.h> //usleep

void bar_flush(double current, double total)
{
    char membar[101];
    //填充\n
    memset(membar, 0, sizeof(membar));
    //按下载完成比例填充'#'
    memset(membar, '#', (int)(current * 100 / total));
    //模拟旋转小风扇,代表正在下载,用于判断是网络不好还是真的卡死了
    const char* str = "|\\-/";

    //当前刷写函数的执行次数,主要用于更新旋转小风扇
    static int n = 0;
    printf("[%-100s][%.2lf%%][%c]\r", membar, current * 100 / total, str[n]);
    n++;
    //防越界
    n %= 4;
    //清空缓冲区
    fflush(stdout);
}
  • progress_bar.h
#pragma once

//函数声明
void bar_flush(double current, double totall);

6 git

  • 关于git的使用部分,在作者写这篇笔记的时候还未完整学完git,后续会进行git操作的补充(可能会单独开一篇文章),顺带做一个用来玩的小程序,模拟一下Linux和Windows协同开发

  • 留坑:

    • git提交只提交源文件
    • gitignore文件
    • push冲突解决方案

7 gdb调试器

7.1 版本

  • 我们的编译连接后的可执行程序的版本包含两个版本,即release版本和debug版本,一般(没错的话)开发岗会把代码写好,在debug版本下做好基本的测试,然后将release送测给测试岗

  • 我们知道,如果需要对代码进行调试,就必须使用debug版本,但Linux中,gcc/g++默认使用的是release版本,如果需要使用debug版本,就必须在编译过程中加-g选项,否则会报错

gcc -c -g main.c main.o
  • 我们可以使用一个命令查看有没有调试信息(暂时不用管这条命令是什么含义,未来会学的)
  • 假设我们生成了一个debug版本的可执行程序,应该返回以下信息
$ readelf -S test.exe | grep -i "debug"

# 返回
  [27] .debug_aranges    PROGBITS         0000000000000000  0000108d
  [28] .debug_info       PROGBITS         0000000000000000  000010ed
  [29] .debug_abbrev     PROGBITS         0000000000000000  0000152e
  [30] .debug_line       PROGBITS         0000000000000000  000016c1
  [31] .debug_str        PROGBITS         0000000000000000  000017f3
  • 如果生成的是release版本的可执行程序,则什么都不会返回

7.2 gdb的基本操作

  • 使用gdb打开可执行文件(release)
gdb [filename]
  • 打开文件之后,事实上不会显示文件的任何内容,需要我们使用命令来显示文件的文本内容
l                            # 显示文件的部分内容,默认是10行,莫名感觉这个显示文件的起始行可能是随机的(输入回车可以让它接着显示)
l [number]                   # 显示文件从[number]行上下数的5行内容
l [filename]:[number]        # 显示名字为[filename]的文件的从[number]行上下数5行的内容
l [filename]:[functionname]  # 显示名字为[filename]的文件中的名字为[functionname]的函数的定义(?)的上下5行的内容
  • 退出gdb
quit

7.3 cgdb -- 对新人更友好的gdb(增加了些许可视化功能的gdb)

  • 不难看出,原始的gdb其实还是有点难用的,这里也是装了一个cgdb的玩意,相当于gdb的套壳,多套了一层可视化功能,看代码会更方便一点
sudo yum install -y cgdb

image-4.png

  • 相比最初的gdb,cgdb增加了可视化运行位置的功能,意味着我们在调试过程中,不仅可以查看调试到哪一行了,还可以查看其附近的内容

  • cgdb的几乎所有命令都和gdb相同,后面的所有内容我们都使用cgdb来演示

  • 使用cgdb打开可执行文件

cgdb [filename]

7.4 基础命令

  • 打断点
b [number]                   # 在[number]行打断点
b [filename]:[number]        # 在名字为[filename]的文件的[number]行打断点
b [filename]:[functionname]  # 在名字为[filename]的文件的名字为[functionname]的函数所在行打断点
  • 开始运行/重新运行
r  # r是简写,全写的话就是run
  • 查询断点信息
$ info

# 返回
Num     Type           Disp Enb Address            What
1	      breakpoint     keep y   0x0000000000400573 in main at test.c:16
2	      breakpoint     keep y   0x000000000040059f in main at test.c:18
类型NumTypeDispEnbAddressWhat
行11breakpointkeepy0x0000000000400573in main at test.c:16
行22breakpointkeepy0x000000000040059fin main at test.c:18
解释编号类型暂时不用管断点有没有被启用地址位置(哪个文件的哪个函数栈帧)
  • 移除断点
d [breakpoint_number]   # 删除编号为[breakpoint_number]的断点
  • 值得注意的是,新增的断点的编号一定不会是已经删除的断点的编号或者是存在的断点的编号

  • 比方说我新增了编号为1,2,3的断点,然后删除编号为2的断点,接着再新增断点,此断点的编号一定是4一定不会是2

  • PS:退出cgdb/gdb会让断点全部被删除,后续重新打开gdb,新增断点编号会重新从1开始排

  • 逐过程

n   # 全称是next
  • 逐语句
s   # 全称是step
  • PS:在gdb/cgdb中,程序会帮用户保存最近一次的调试指令,直接使用回车就可以直接调用最近的使用的指令

  • 调用堆栈关系

(gdb) bt

# 返回
#0  func (left=1, right=100) at test.c:10
#1  0x0000000000400582 in main () at test.c:16
  • 可以看到,栈底是main(),而栈顶是func()

  • 执行,直到当前函数栈帧被释放(或者说当前函数退出)

(gdb) finish

# 返回
Run till exit from #0  func (left=1, right=100) at test.c:5
0x0000000000400582 in main () at test.c:16
Value returned is $1 = 5050
  • PS:如果当前函数还有断点没有执行到,程序会先执行到最近的断点处,并不会直接退出

  • cgdb/gdb和VS2022一样,支持对断点使能,即可以禁用断点或者启用断点

  • 使用禁用断点

disable [breakpointnumber]  # 禁用断点标号为[breakpointnumber]的断点
(gdb) b	15
Breakpoint 1 at 0x400565: file test.c, line 15.
(gdb) b 16
Breakpoint 2 at 0x400573: file test.c, line 16.
(gdb) b	17
Breakpoint 3 at 0x400585: file test.c, line 17.
(gdb) info b
Num     Type           Disp Enb Address            What
1	breakpoint     keep y   0x0000000000400565 in main at test.c:15
2	breakpoint     keep y   0x0000000000400573 in main at test.c:16
3	breakpoint     keep y   0x0000000000400585 in main at test.c:17
(gdb) disable 1
(gdb) info b
Num     Type           Disp Enb Address            What
1	breakpoint     keep n   0x0000000000400565 in main at test.c:15
2	breakpoint     keep y   0x0000000000400573 in main at test.c:16
3	breakpoint     keep y   0x0000000000400585 in main at test.c:17
  • 此时使用命令r,就会发现,程序没有在第15行停下来,而是在第16行停下来

  • 启用断点

enable [breakpointnumber]  # 启用断点标号为[breakpointnumber]的断点
(gdb) enable 1
(gdb) info b
Num     Type           Disp Enb Address            What
1	breakpoint     keep y   0x0000000000400565 in main at test.c:15
2	breakpoint     keep y   0x0000000000400573 in main at test.c:16
3	breakpoint     keep y   0x0000000000400585 in main at test.c:17
  • 执行到下一个断点
c   # 全称continue
  • 执行到指定行
until [number]   # 执行到第[number]行然后停下
  • 查看变量值
p [variablename]   # 查看变量名为[variablename]的值
  • 固定/取消固定 查看变量名的值
display [variablename]   # 固定变量名为[variablename]的变量,使得每次向后运行都能显式返回其值
undisplay [variablename]   # 取消固定变量名为[variablename]的变量
(gdb) info b
Num     Type           Disp Enb Address            What
2	breakpoint     keep y   0x0000000000400537 in func at test.c:5
        breakpoint already hit 1 time
(gdb) display count
1: count = 0
(gdb) p	count
$1 = 0
(gdb) n
1: count = 0
(gdb) n
1: count = 0
(gdb) display n
2: n = 1
(gdb) n
2: n = 1
1: count = 1
(gdb) n
2: n = 2            # 每个固定下来的变量在行的最开头都会有一个专属的编号
1: count = 1
(gdb) undisplay	2   # 删除固定监视的变量时,需要指定要删的编号而不是变量名
(gdb) n
1: count = 3
(gdb) until 10
func (left=1, right=100) at test.c:10
1: count = 5050
  • 固定当前函数中所有变量的值
info locals

7.5 调试思想

  • 即,调试,就是寻找代码错误的地方的过程,而不是修正代码,修正代码需要在编辑器中做而不是调试器
  • 本质上,调试就是打断点,即为代码分块,两个断点之间的代码统一为一个"块",当程序出现问题之后,就需要锁定某个"块",不断细分"块"直到找到出现问题的地方

7.6 gdb/cgdb的进阶玩法

7.6.1 watch -- 监视点
  • watch用于监视变量/表达式是否改变,如果你认为,导致错误的原因可能是因为某个变量/表达式的更改,就可以使用watch监视某个本不应该更改的变量/表达式
watch <expression>
  • 当程序向后执行时,发现被监视的变量或者表达式发生改变时,就会提示你
Hardware watchpoint 3: count

Old value = 6
New value = 10
func (left=1, right=100) at test.c:6
  • 我们称watch监视的位置为"监视点"

  • 监视点可以像断点一样被管理,例如info b查看断点时,会发现监视点也在列表里,也可以用d [watchpointnumber]删除对应编号的监视点,也可以用disableenable来禁用或启用监视点

7.6.2 set var
  • 当我们发现被监视的值可能就是罪魁祸首的时候,就可以尝试进行修改了,但直接退出gdb/cgdb,然后打开vim编辑,然后再重新编译运行太麻烦了,所以我们可以在调试过程中就临时更改其值
set var <expression> = <value>
  • 例如
set var count = 0
  • 此时对应变量就被更改成了0
7.6.3 条件断点以及条件监视点
  • 条件断点
b [number] if <condition>   # 新增条件为<condition>的条件断点
  • 如果<condition>表达式的值为true,c之后才会在该断点停下,即满足某个条件才停下
b 8 if count == 15
  • 更改某个普通断点为条件断点
condition [breakpointnumber] if <condision>   # 更改断点编号为[breakpointnumber]的断点为条件是<condition>的条件断点
  • 条件监视点
watch <expression> if <condition>   # 新增条件为<condition>的条件监视点
  • 条件监视点只有在<condition>表达式的值为true,才会触发监视点