Linux系统编程--基础开发工具
基础开发工具
- 基础开发工具
- 1. 软件包管理器
- 1.1 基础知识的介绍
- 1.2 Linux 软件生态
- 1.2.1 Linux 下载软件的过程
- 1.2.2 操作系统生态问题
- 1.2.3 免费共享软件包的原因
- 1.2.4 国内镜像源
- 1.2.5 软件包的依赖问题
- 2. yum 具体操作
- 2.1 查看软件包
- 2.2 安装软件
- 2.3 卸载软件
- 2.4 注意事项
- 2.5 安装源
- 2.5.1 查找本地软件源
- 2.5.2 认识软件源
- 2.5.3 安装软件源
- 3. 编辑器 vim
- 3.1 vim 的基本概念
- 3.2 vim 中的各种模式
- 3.3 vim 的基本操作
- 3.4 vim 命令模式下的命令集
- 3.5 vim 底行模式命令集
- 3.6 vim 的补充模式
- 3.7 简单的 vim 配置
- 3.7.1 配置文件的位置
- 3.7.2 常用配置选项,用来测试
- 3.7.3 使用插件
- 4. 编译器 gcc/g++
- 4.1 程序运行的四个阶段
- 4.1.1 预处理
- 4.1.2 编译
- 4.1.3 汇编
- 4.1.4 链接
- 4.1.5 总结
- 4.2 链接方式和函数库
- 4.2.1 动态链接和静态链接
- 4.2.2 动态库和静态库
- 4.2.3 理解动静态库和动静态链接
- 4.2.3.1 什么是动态链接
- 4.2.3.2 什么是静态链接
- 4.2.3.3 动态链接与静态链接的核心区别
- 5. 自动化构建 make/Makefile
- 5.1 基本概念
- 5.2 核心思想
- 5.3 基本使用
- 5.3.1 具体示例
- 5.3.2 make 的工作原理
- 5.3.2.1 make 的使用
- 5.3.2.2 make 的依赖性
- 5.3.2.3 项目清理
- 5.3.3.4 .PHONY 伪目标
- 5.4 更加具有通用性的 makefile
- 6. 版本控制器 Git
- 6.1 理解版本控制
- 6.2 Git 的发展历史
- 6.3 Git 的安装和使用
- 6.3.1 判断是否安装了 git
- 6.3.2 安装 git
- 6.3.3 git 操作
- 6.3.3.1 新建仓库
- 6.3.3.2 克隆远程仓库到本地
- 6.3.3.3 配置 .gitignore
- 6.3.3.4 推送文件到远端的三板斧
- 6.3.3.5 查看日志信息
- 6.3.3.6 查看仓库状态
- 6.3.3.7 重命名已提交的文件
- 6.3.3.7 拉取远端仓库
- 7. 调制器 gdb/cgdb
- 7.1 debug 和 release
- 7.2 gdb 基础使用
- 7.3 cgdb 基础使用
- 7.3.1 cgdb 的安装
- 7.3.2 cgdb 的界面
- 7.4 调试器的使用
- 7.4.1 基础命令
- 7.4.2 调试技巧
- 7.4.2.1 理解调试
- 7.4.2.2 watch
- 7.4.2.3 set var确定问题原因
- 7.4.2.4 条件断点
- 8. Linux 第一个系统程序 -- 进度条
- 8.1 概念补充
- 8.1.1 回车与换行
- 8.1.2 行缓冲
- 8.2 进度条的编写
基础开发工具
1. 软件包管理器
1.1 基础知识的介绍
Linux 下安装软件的方式
- 下载到程序的源代码,自行进行编译,得到可执行程序。
- 获取rpm安装包,通过rpm命令进行安装。(未解决软件的依赖关系)
- 通过yum进行安装软件。(常用)
因为在Linux下安装软件,前两种方法太麻烦了,于是有人把一些常用的软件提前编译好,做成软件包(可以理解为windows上的安装程序)放在一个服务器上,通过包管理器可以很方便的获取到这个编译好的软件包,直接进行安装。
而方法3种的 yum 就是一种包管理器。
软件包和软件包管理器,就好比 “App” 和 “应用商店” 这样的关系。
常用的包管理器
-
yum (Yellow dog Updater, Modified) 是Linux下非非常用的一种包管理器。主要应用在Fedora、RedHat、Centos等发行版上。
-
Ubuntu:主要使用apt(Advanced Package Tool)作为其它包管理器。apt同样提供了自动解决依赖关系、下载和安装软件包的功能。
1.2 Linux 软件生态
1.2.1 Linux 下载软件的过程
相当于手机中的应用商店的包管理器,包管理器系那个软件包服务器提供查找和下载的请求,之后由软件包服务器下载返回在本地进行安装,同时 yum/apt 可以解决依赖、下载、安装和卸载等问题。
1.2.2 操作系统生态问题
操作系统的生态是一个操作系统最重要的东西,也是评判一个操作系统好坏的关键因素。
如上图一个操作系统的生态包括社区论坛、官方文档、软件体系、维护更新速度、操作系统自身和富有针对性的客户群体等方面。
一个操作系统的生态好,就会有很多人去使用,使用的人多了就会有一个活跃的社区,同时也会暴露出很多问题,暴露出来的问题可以因为有活跃的社区而被很快解决,这样也可以减少后来人排查问题的成本,同时社区因为活跃操作系统的维护更新也会十分稳定,并且会配有详细的文档资料对于每次更新会进行详细介绍,这些原因导致了会吸引更多的人来使用这款操作系统从而达到良性循环。(社区论坛、官方文档、维护更新)
同时随着一款操作系统社区的不断发展,一个社区会逐渐聚集某一种占比极大的人群,例如虽然都是以 Linux 为内核的操作系统但是因为使用的主要人群不同而存在的 Centos 和 Ubnutu 。Centos主要由企业中的工程师使用,其社区的主要组成人员也就是各种企业的工程师,Ubnutu主要是由学校的学生使用,其社区的主要组成人员就是各个高校中的学生等等。这些富有针对性的客户群体组成的社区,会当你在使用对应操作系统的时候遇到的问题若进行会得到很快的解答,因为他们以后也有可能会遇到问题。而这些不同的客户群体才是这两款操作系统最大的不同。(客户群体)
并且为了保证这款操作系统有着更强的竞争力,所以操作系统的制作者和论坛贡献者会提供各种软件,有良好的软件体系。(软件体系)
综上可以看出一个好的操作系统最重要的是有一个好的生态,这也是为什么华为做鸿蒙之前先要做手机、平板、汽车等设备原因,硬件设备有了就可以开发自己的操作系统,这样就会有人用,使用的人多了就会逐渐形成社区,暴露问题逐步完善生态。逐渐形成一个属于我们中国自己的好的操作系统。
1.2.3 免费共享软件包的原因
首先毋庸置疑的是软件包底层一定是各种代码,而代码一定是由工程师写的,那么工程师将这些自己辛辛苦苦写的软件包为什么要进行免费共享。因为其本质目的是为了吸引更多人使用自己的操作系统,提升操作系统的竞争力,维护好操作系统的生态,然而有一个良好的软件体系是形成一个好生态必不可少的一部分。
当然也有一些软件是由一些企业、组织或个人提供的,这些企业、组织以及个人为了某种利益编写出了软件包,然后将其放在了对应的服务器上。自此,一条简单的商业生态链就出来了 ,一部分人编写出了软件包供其他人使用,并通过收费或者内置广告等方式从中获取利益;同时,它们也向手机厂商(包管理厂商)支付费用,使得自己的软件能够在对应手机的应用商店(包管理器) 中被下载使用。
同样有人编写软件是为了赚钱,自然也有的人不为赚钱,他们可能是为了提高技术与获得成就感、也可能是为了提高自己的知名度、又异或是无聊等等原因。总之,有的人会将自己编写出的软件包的源代码公开,让别人能够免费随意使用,这种就叫开源。在托瓦兹编写出了Linux操作系统并开源之后,世界上有很多人参与到了Linux的完善与扩展中来,其中也不乏为Linux免费编写软件的人,这些人会加入相应的Linux社区,然后将自己编写的软件放在社区对应的服务器上。而不同的社区会在自己的Linux版本中内置服务器和软件对应的下载链接,而这个用于存放下载链接的软件就是 yum。
1.2.4 国内镜像源
因为下载软件首先需要下载此软件的二进制文件等,那么程序员自己的服务器怎么知道去哪里可以下载这些软件文件呢,这是因为自己的 Linux 系统中有一个软件源其中有对应的给 yum/apt 提供的配置文件,包含 URL 或者 ip 地址用于告诉系统去哪里可以进行软件的下载。
同时由于西方在计算机方面起步与发展比我国要早很多,所以上面所说的开源生态最先在西方形成,即大多数 Linux 社区,包括社区对应的服务器都是部署在国外的,所以在国内通过链接下载软件时访问会比较慢,再加上我国的一些特殊国情,有时候还会访问失败。
针对上面这种情况,我国的一些高校以及公司就镜像了国外的软件服务,即把国外服务器上的软件拷贝到了国内自己公司的服务器上,使得我们可以直接访问国内的服务器来下载软件。
同时国内的一些云服务器已经对软件源进行过了一些定制化处理,使用了国内的配置文件替换了国外的。但是如果是使用的虚拟机还需要自行更换软件源,会在下面 yum 的具体操作中有所介绍。
以下是一些国内Linux软件安装源的官方链接【由心一言生成】:
- 阿里云官方镜像站
- 官方链接:https://developer.aliyun.com/mirror/
- 阿里云提供了丰富的Linux发行镜像,包括CentOS、Ubuntu、Debian等,用户可以通过镜像站点快速下载和更新软件包。
- 清华大学开源软件镜像站
- 官方链接:https://mirrors.tuna.tsinghua.edu.cn/
- 清华大学镜像站提供了多种Linux发行版的镜像,以及Python、Perl、Ruby等编程语言的扩展包。该镜像站还提供了丰富的文档和教程,帮助用户更好地使用这些软件包。
- 中国科学技术大学开源镜像站
- 官方链接:https://mirrors.ustc.edu.cn/
- 中科大镜像站提供了多种Linux发行版的镜像,以及常用的编程语言和开发工具。用户可以通过该镜像站方便地获取所需的软件包和工具。
- 北京交通大学自由与开源软件镜像站
- 官方链接:https://mirror.bjtu.edu.cn/
- 北京交通大学镜像站提供了多种Linux发行版的镜像,以及相关的软件仓库和工具。该镜像站还提供了详细的文档和指南,帮助用户配置和使用这些软件资源。
- 中国科学院软件研究所镜像站(ISCAS)
- 官方链接:http://mirror.iscas.ac.cn/
- ISCAS镜像站提供了多种Linux发行版、编程语言和开发工具的镜像。用户可以通过该镜像站快速获取所需的软件包和更新。
- 上海交通大学镜像站
- 官方链接:https://ftp.sjtu.edu.cn/
- 上海交大镜像站提供了丰富的Linux软件资源,包括多种发行版的镜像和软件仓库。用户可以通过该镜像站方便地下载和安装所需的软件包。
- 网易开源镜像站
- 官方链接:http://mirrors.163.com/
- 网易镜像站提供了多种Linux发行版的镜像,以及相关的软件仓库和工具。该镜像站还提供了便捷的搜索功能,帮助用户快速找到所需的软件包。
此外,还有一些其他的国内镜像源,如独立源镜像站等,但可能由于时间间隔或政策调整,部分镜像站的链接状态可能有所变化。因此,建议用户在使用前访问官网或咨询相关社区以获取最新的信息和帮助。
1.2.5 软件包的依赖问题
如果想要使用一个软件,单独把软件本身下载下来没有用,其还可以需要一些其他的库和配置文件,而软件所依赖的文件可能还依赖于其他文件一环套一环,这也是 rpm 下载软件的劣势,其需要手动解决软件包的依赖问题。而包管理器在下载软件的时候可以自动解决依赖问题,自动安装软件所以要的库和配置文件。
2. yum 具体操作
2.1 查看软件包
可以通过 yum list
命令罗列出当前一共有哪些软件包。但由于包的数目非常之多, 所以一般使用 grep 命令来筛选出我们关注的包。如:
补充:
- 软件包名称构成:主版本号.次版本号.源程序发行号-软件包的发行号.主机平台.cpu架构
- “
x86_64
” 后缀表示64位系统的安装包,“i686
” 后缀表示32位系统安装包,选择包时要和系统匹配。 - “
el7
” 表示操作系统发行版的版本: “el7
” 表示的是centos7/redhat7
,“el6
” 表示centos6/redhat6
。 - 最后一列中
base
表示的是 “软件源” 的名称, 类似于 “小米应用商店”, “华为应用商店” 这样的概念。
2.2 安装软件
可以通过如下命令来安装软件包 (其中 -y
代表不询问直接安装):
yum install -y 软件名
补充:
yum/apt
会自动找到那些需要下载的软件包,下载时候按y
确认安装。- 出现
"complete"
字样或者中间未出现报错,说明安装完成。
注意:
- 安装软件的本质就是将其二进制文件下载下来并拷贝到特定文件的目录下,而这个目录是系统指定目录。涉及文件的拷贝也就需要收到权限的约束。一般需要
sudo
或者切到root
账户下才能完成。 - 同时因为 Linux 的软件都默认安装台系统指定目录下,所以只要 root 安装了,所有普通用户都能看都能用。
yum/apt
安装软件只能完成一个软件的安装。正在使用yum/apt
安装一个软件的过程中,如果再尝试用yum/apt
安装另外一个软件,yum/apt
会报错。- 如果
yum/apt
报错,请自行百度。
2.3 卸载软件
卸载软件的指令如下 (其中 -y
代表不询问直接卸载):
yum remove -y 软件名
2.4 注意事项
关于 yum/apt
的所有操作必须保证主机(虚拟机)网络畅通!!!
可以通过 ping
指令验证:
1 ping www.baidu.com
2
3 # 当时yum/apt也能离线安装,但是和我们当前无关,暂不关心。
2.5 安装源
2.5.1 查找本地软件源
yum 源相关知识已经在 1.2.4 有所介绍,平时使用的云服务器中的 yum 源已经是被定制化过替换成国内的了。
云服务器本地配置的 yum 软件源的配置文件如下:
同样 yum 源一定也不止一个,在 Centos 中最基础最核心就是就是第一个 CentOS-Base.repo
。
打开这个文件可以看到使用的是腾讯云内部的一个链接,因为使用的云服务器是腾讯云所以在打包定制的时候使用的就是自己内部的 yum 源链接。
2.5.2 认识软件源
首先需要知道宏观上可以将软件源分为稳定软件源和拓展软件源。其中稳定软件源中的软件是经过时间的验证性能稳定的软件,反之拓展软件源就是用来存一些并没有被时间验证过性能不确定是否稳定的新软件使用的。当拓展软件源中的某个软件经过了时间的迭代稳定性得到了提高之后再将其迁移到稳定软件源中。
在开发一款软件之后,并不是开发完直接将这个软件放在 yum 源上供使用者下载安装,因为这些软件可能有一些缺陷没有被长期的使用而发现,所以会将其先放进拓展软件源中。
2.5.3 安装软件源
但是一个新的 Linux 系统并不一定各种软件源都有,所以如果有些软件源在系统中没有的话,有些软件可能没有办法正常安装。
#安装扩展源
$ sudo yum install -y epel-release
并且如果需要进行软件源的切换或者是更新可以借助ai ,进行辅助操作。这里只需要记住切换软件源的本质就是更改配置文件,将 CentOS-Base.repo
此文件中的内容进行更改即可。
3. 编辑器 vim
3.1 vim 的基本概念
vim 被誉为世界上最强大的编辑器,是一个类似于 vi 的著名的功能强大、高度可定制的文本编辑器,在 vi 的基础上改进和增加了很多特性。vi/vim 都是多模式编辑器,不同的是 vim 是 vi 的升级版本,它不仅兼容 vi 的所有指令,而且还有一些新的特性在里面,例如语法加亮、可视化操作,其不仅可以在终端运行,也可以运行于 mac os、windows 等。
3.2 vim 中的各种模式
这里先介绍 vim 的三种模式(其实有很多模式,目前常握这3种即可),分别是命令模式(command mode)、插入模式(Insert mode)和底行模式(last line mode),各模式的功能区分如下:
-
正常/普通/命令模式 (Normal mode)
- 控制屏幕光标的移动,字符、行的删除,移动复制某段及进入 Insert mode 下,或者到 last line mode。
-
插入模式 (Insert mode)
- 只有在 Insert mode 下,才可以做文字输入,按
ESC
键可以回到命令行模式。该模式是后面用的最频繁的编辑模式。
- 只有在 Insert mode 下,才可以做文字输入,按
-
底行模式 (last line mode)
- 文件保存或退出,也可以进行文件替换,找字符串,列出行号等操作。
- 在命令模式下,
shift + :
即可进入该模式。要查看你的所有模式:打开 vim,底行模式直接输入:help vim-modes
3.3 vim 的基本操作
-
进入vim,在系统提示符下输入
vim
及文件名后,就进入vim全屏编辑界面。$ vim test.c
- 不过有一点需要特别注意,就是进入vim之后,是处于**[命令模式],要切换到[插入模式]**才能够输入文字。
-
[命令模式]切换至[插入模式]
- 输入
a
- 输入
i
(最常用) - 输入
o
- 输入
-
[插入模式]切换至[正常模式]
当前处于**[插入模式]时,只能一直输入文字,如果发现输入错了字,想用光标键往回移动,将该字删除,可以先按下
ESC
键切换到[命令模式]**再删除文字。当然,也可以直接删除。 -
[正常模式]切换至[底行模式]
shift + ;
,其实就是输入:
-
退出 vim 及保存文件,在**[命令模式]**下,按一下
:
命令键进入Last line mode
,如下::w
(保存当前文件):wq
(输入wq
,保存并退出vim):q!
(输入q!
,不保存强制退出vim)
3.4 vim 命令模式下的命令集
插入模式
- 按
i
切换进入插入模式insert mode
,按i
进入插入模式后是从光标当前所在位置前开始输入文字。- 按
a
进入插入模式,是从当前光标所在位置的下一个位置开始输入文字。- 按
o
进入插入模式,是插入新的一行,从行首开始输入文字。
插入模式切换至命令模式
- 按
ESC
键。
移动光标
vim 可以直接用按键上的光标来上下左右移动,但正规的 vim 是用小写字母母
h
、j
、k
、l
分别控制光标左、下、上、右移动一格,如果移动大于一格在字母前面加上想要移动的格数即可。
- 按
G (Shift + g)
: 移动到文章的最后。- 按
$ (Shift + 4)
: 移动到光标所在行的 “行尾”。- 按
w
: 光标向右跳到下个字的开头。- 按
e
: 光标向右跳到下个字的字尾。- 按
b
: 光标向左跳到下个字的开头。- 按
gg
: 光标跳到文本开始。- 按
ctrl + [b]
: 屏幕往“后”移动一页。- 按
ctrl + [f]
: 屏幕往“前”移动一页。- 按
ctrl + [u]
: 屏幕往“后”移动半页。- 按
ctrl + [d]
: 屏幕往“前”移动半页。
删除文字
x
: 每按一次,删除光标所在位置的一个字符。#x
: 例如,6x
表示删除光标所在位置的“后面(包括自己在内)”6个字符。X
: 大写的X
,每按一次,删除光标所在位置的 “前面” 一个字符。#X
: 例如,20X
表示删除光标所在位置 “前面” 20个字符。dd
: 剪切/删除光标所在行。#dd
: 从光标所在行开始删除行。
复制文本
yw
: 将光标所在处到字母的字符复制到缓冲区中。yy
: 复制光标所在行到缓冲区。#yy
: 例如,6yy
表示将从光标所在行的该行“往下数” 6行。p
: 将缓冲区内的字符贴到光标所在位置。注意:所有与 “y” 相关的复制命令都必须与p
配合才能完成复制与粘贴功能。
替换
r
: 选中光标所在的字符,再按下想替换的目标字符即可完成替换。R
: 选中光标所在到的字符,再按下想替换的目标字符即可完成替换,并接着向后选中,直到按下ESC
键为止。(进入替换模式)~
: 快速替换大小写。
撤销一次操作
u
: 如果误执行了一个命令,可以马上按下u
,回到上一个操作。按多次u
可以执行更多次回退。
ctrl + r
: 撤销的恢复。注意:只要不退出 vim ,都可以撤销。
更改
cw
: 更改光标所在处的字符字尾处。#cw
: 例如,c3w
表示更改3个字。
跳至指定的行
ctrl + g
: 切出光标所在行的行号。#G
: 例如,15G
,表示移动光标至文章的第15行行首。
3.5 vim 底行模式命令集
列出行号
set nu
: 输入set nu
后,会在文件中的每一行前面列出行号。
跳到文件中的某一行
#
:#
号表示一个数字,在命令后输入一个数字,再按回车键就会跳到该行,如输入数字 15,再回车,就会跳到文章的第15行。
查找字符
/
键符:按下/
键,再输入想要查找的字符,如果第一次找到的关键词不是您想要的,可以一直按n
会往后找到您需要的关键词为止。?
键符:先按?
键,再输入想要查找的字符,如果第二次找到的关键词不是您想要的,可以一直按n
会往前找到您需要的关键词为止。
保存文件
:w
: 在正常模式输入字母w
就可以将文件保存起来。
退出vim
:q
: 按q
就是退出,如果文件没有保存,可以直接退出vim。:wq
: 一般建议离开时,搭配:w
使用,这样退出的时候可以保存文件。
不退出 vim 执行命令行指令
!command
: command 是要执行的指令,如进行代码编写的时候想进行编译调试代码,可以使用!./a.out
即可不退出 vim,完成代码的编译。
分屏操作
vs other
: other 是想和当前文件分屏的另一个文件,比如现在在test.c
中,想要和test.h
进行分屏,使用vs test.h
即可。ctrl + ww
: 分屏之后可以使用此指令让光标在两屏幕之间进行切换。
集体替换
%s/printf/print/g
将文本中所有的
printf
替换成
3.6 vim 的补充模式
补充命令:
批量化文本遍历:
- 批量化注释:Ctrl + v -> hjkl(调整位置)-> shift + i(I)-> //(在插入模式中插入//)-> Esc(回到命令模式)
- **批量化去注释:**Ctrl + v -> hjkl -> d
**补充:**以批量化注释和去注释为载体,本质是进行批量化的文本编辑。
3.7 简单的 vim 配置
3.7.1 配置文件的位置
-
在目录
/etc/
下面,有个名为vimrc
的文件,这是系统中公共的 vim 配置文件,对所有用户都有效。 -
而在每个用户的主目录下,都可以自己建立私有的配置文件,命名为:
.vimrc
。例如,/root
目录下,通常已经存在一个.vimrc
文件,如果不存在,则创建之。 -
切换用户成为自己执行
su
,进入自己的主工作目录,执行cd ~
。 -
打开自己目录下的
.vimrc
文件,执行vim .vimrc
。
3.7.2 常用配置选项,用来测试
- 设置语法高亮:
syntax on
- 显示行号:
set nu
- 设置缩进的空格数为 4:
set shiftwidth=4
3.7.3 使用插件
-
安装TagList插件,下载taglist_xx.zip,解压完成,将解压出来的doc的内容拷贝到
~/.vim/doc
,将解压出来的plugin下的内容拷贝到~/.vim/plugin
。 -
在
~/.vimrc
中添加:let Tlist_Show_One_File=1 let Tlist_Exit_OnlyWindow=1 let Tlist_Use_Right_Window=1
-
安装文件浏览器和窗口管理器插件:WinManager
-
下载 winmanager.zip,2.X版本以上的。
-
解压 winmanager.zip,将解压出来的doc的内容放到
~/.vim/doc
,将解压出来的plugin下的内容拷贝到~/.vim/plugin
。 -
在
~/.vimrc
中添加:let g:winManagerWindowLayout=‘FileExplorer|TagList nmap wm :WMToggle<cr>
- 然后重新 vim,打开
XXX.c
或XXX.cpp
,在 normal 状态下输入"wm"
即可。
4. 编译器 gcc/g++
4.1 程序运行的四个阶段
gcc 和 g++ 分别是 GNU 的 C 和 C++ 的编译器,gcc 和 g++ 在执行编译的时候一般有以下四个步骤:
- 预处理(头文件展开、去注释、宏替换、条件编译)。
- 编译(C代码翻译成汇编语言)。
- 汇编(汇编代码转为二进制目标代码)。
- 链接(将汇编过程产生的二进制代码进行链接)。
4.1.1 预处理
预处理也叫预编译,程序在预处理阶段会完成如下操作:
- 注释的删除。
- #define 定义的符号、宏的替换以及删除。
- 条件编译的执行。
- 头文件的包含:将头文件中的代码拷贝到当前代码中来。
在Linux下可以通过如下命令来得到预处理之后的代码:
gcc -E code.c -o code.i
# gcc:表示用 gcc 编译器来编译此代码
# -E:表示让代码在完成预处理后停下来,不再继续往后编译
# code.c:我们要编译的代码
# code.i 预处理产生的文件一般以.i为后缀
# -o test.i:用于指明临时文件的名称(test.i),它会将预处理之后的代码保存到指明的临时文件中,而不是直接打印到终端上
可以看到,预处理后 stdio.h
里面的内容会被拷贝到 code.i 里面,所以 code.i 一共有800多行,同时,预处理阶段也完成了上述提到的其他工作。
4.1.2 编译
程序在编译阶段会完成如下操作:
- 语法分析。
- 词法分析。
- 语义分析。
- 符号汇总。
在Linux下可以通过如下命令来得到编译之后的代码:
gcc -S code.i -o code.s
# -S:表示让代码在完成编译后停下来,不再继续往后编译
# 编译产生的文件一般以.s为后缀
可以看到,编译阶段会将高级语言转换为汇编语言。
4.1.3 汇编
汇编阶段是把编译阶段生成汇编代码转成计算机可以识别的二进制目标代码,其中生成的 .o 文件被称为可重定向二进制目标文件。
在Linux下可以通过如下命令来得到编译之后的代码:
gcc -c code.s -o code.o
# -c:表示让代码在完成编译后停下来,不再继续往后编译
# 汇编产生的文件一般以.o为后缀
如上,汇编得到的二进制目标文件使用一般的文本编辑器打开时是一堆看不懂的符号 (与符号的编码有关 – utf-8),可以使用 od 指令以指定格式来打开它 (默认是以八进制打开)。
od code.o
补充:
.o
文件是可重定位目标二进制文件,其是无法执行的,即使该文件有可执行权限。
4.1.4 链接
程序在链接阶段会完成如下操作:
- 合并段表:编译器会把在汇编阶段生成的多个目标文件中相同格式的数据合并在一起,最终形成一个 .exe 文件。
- 符号表的合并和重定位:符号表的合并是指编译器会把在汇编阶段生成的多个符号表合并为一个符号表;重定位则是指当同一个符号出现在两个符号表中时,编译器会选取其中和有效地址相关的那一个,舍弃另外一个。
在Linux中,链接直接使用 gcc 即可,没有额外选项,因为链接是程序的最后一个阶段。同时,链接的结果默认存放在 a.out 中。
gcc code.o
链接得到的文件被称为可执行程序,它里面存放的也是计算机能够识别的二进制指令。
4.1.5 总结
gcc 预处理编译链接三个阶段对应的选项和文件后缀有一个记忆技巧 – ESc
与 iso
,其中 ESc
分别代表 -E
-S
-c
,iso
分别代表 .i
,.s
,.o
。ESc 可以对比电脑上的 [Esc] 键,将其中的 s 改为大写即可,iso 可以对比苹果 ios,将 o 和 s 的顺序调换即可。
同时,此处将 gcc 编译代码分为预处理、编译、汇编、链接四个阶段是为了让大家更深层次的理解一个程序的运行过程;日常编译代码的时候直接使用 “gcc code.c -o code.out
” 或 “gcc test.c
” 编译源文件得到可执行程序即可。
问题1:如何理解条件编译?
条件编译是在编译前的预处理阶段,根据设定的条件,选择性地编译一部分代码,从而实现在一套源代码中产出不同功能的程序。
这也是市面上许多软件分为免费版社区版专业版的关键,虽然分类了很多版本但并不是一个版本维护一套代码,其背后都是通过条件编译对同一个代码进行裁剪然后发布的结果这样不仅降低维护的成本和提高了效率。
跨平台开发也是条件编译最广泛的用途之一。不同的操作系统(如 Windows, macOS, Linux)有不同的API和头文件。条件编译可以让程序员在同一份代码中兼容不同的平台。
问题2:如何理解编译器自举?
要理解这个问题首先需要知道最早期编程是使用纸带打孔的方式进行二进制编程,但是因为太过麻烦逐渐出现了汇编语言,但是因为计算机底层还是使用的二进制编程,所以编译器就诞生了他在这里承担将汇编语言转化成二进制机器语言的功能。
但是这里又引出一个问题是先有语言还是先有编译器,毋庸置疑一定是先有编译器,因为如果没有编译器这语言写出来的代码没有任何意义的他没有办法被机器使用,并且同样因为编译器本质也是软件,所以第一个汇编语言编译器的软件编写也一定是使用二进制语言进行编写,但是在第一套编译器已经实现,汇演语言也同步出现之后,就可以使用汇编语言重构编译器,代替原有的用复杂二进制实现的编译器,而这个过程也就是编译器自举。
总结一下编译器是一个程序,它的开发也需要遵循软件开发的基本规律。一个新语言的第一个编译器,必须由一个已存在的、更底层的语言来编写。当这个过程完成后,再用这个新语言本身来重写它的编译器,并用第一个编译器来编译它,这个“自己编译自己”的过程,就是自举。并且由汇演语言转化为二进制语言的编译器有一个特殊的名字汇编器。
后面出现的C语言C++等都是一样的,第一个编译器首先由汇编实现,再用自己的语言实现编译器的重构,最后形成编译器的自举。
问题3:为什么要有编译?为什么要有汇编?
首先随着语言的发展会出现越来越多的语言,但是最终都需要转化为二进制的机器语言才可以被使用,根据上一个回答,因为已经汇编语言编译器已经得到实现,后面新出的语言只需要转化为汇编语言再通过汇编器即可实现高级语言转化为二进制语言的目的,这大大减少了语言开发的成本
4.2 链接方式和函数库
4.2.1 动态链接和静态链接
在实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数。但是每个 *.c
文件都是独立编译的,即每个 *.c
文件会形成一个 *.o
文件。为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。静态链接的缺点很明显:
- 浪费空间:因为每个可执行程序中对所有需要的目标文件都有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了
printf()
函数,则这多个程序中都会有printf.o
,同一个目标文件在内存中存在多个副本; - 更新比较困难:因为当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
动态链接的出现解决了静态链接中提到的问题。动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
动态链接其实比静态链接要常用得多。比如查看下 code
这个可执行程序依赖的动态库,会发现它用到了一个 C 动态链接库:
$ gcc code.c -o code
$ ldd codelinux-vdso.so.1 => (0x00007fffeb1ab000)libc.so.6 => /lib64/libc.so.6 (0x00007fff76af5000)/lib64/ld-linux-x86-64.so.2 (0x00007fff76ec3000)
# ldd命令用于打印程序或者库文件所依赖的共享库列表。
在这里涉及到一个重要的概念:库
- C 程序中,并没有定义
printf
的函数实现,且在预编译中包含的stdio.h
中也只有该函数的声明,而没有定义函数的实现。那么,是在哪里实现printf
函数的呢?- 最后的答案是:系统把这些函数实现都放到了名为
libc.so.6
的库文件中。在没有特别指定时,gcc
会到系统默认的搜索路径/usr/lib
下进行查找,也就是链接到libc.so.6
库函数中去,这样就能实现函数printf
了,而这也就是链接的作用。
4.2.2 动态库和静态库
-
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为“.a”
-
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接器加载库,这样可以节省系统的开销。动态库一般后缀名为“.so”,如前面所述的
libc.so.6
就是动态库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件,如下所示:gcc code.o -o code
-
gcc 默认生成的二进制程序是动态链接的,这点可以通过
file
命令验证如果想使用静态链接的方案可以使用以下命令
gcc code.c -o code-s -static
(确保服务器中C/C++的静态库已经安装)
注意:
- Linux 下,动态库
XXX.so
,静态库XXX.a
- Windows 下,动态库
XXX.dll
,静态库XXX.lib
补充:
一般来说云服务器,C/C++的静态库并没有安装,可使用如下命令安装:
# Centos yum install glibc-static libstdc++-static -y #C语言和C++的静态库
4.2.3 理解动静态库和动静态链接
4.2.3.1 什么是动态链接
动态链接是一种在程序运行前,才将库的地址信息关联到可执行程序中的过程。简单来说,动态链接并不会直接把库的完整内容拷贝到程序文件中,而是仅仅记录下所需库的地址信息。当程序实际运行时,再根据这些地址信息去加载和调用库中的功能。
执行过程:
- 程序启动时被加载到内存,此时链接器会根据记录的地址信息找到所需的动态库。
- 当程序需要调用库中的某个方法时,系统会跳转到该动态库执行相应的方法。
- 库方法执行完毕后,程序会返回到原来的位置继续执行后续代码。
主要特点:
- 共享性:动态链接最大的优点是库的共享。多个不同的程序可以共同使用内存中同一份动态库,无需为每个程序都复制一份,有效节省了系统资源。
- 依赖性:其缺点也十分明显,一旦某个动态库缺失或版本不兼容,所有依赖该库的程序都将无法正常运行。
4.2.3.2 什么是静态链接
与动态链接相反,静态链接是在程序编译时,就将所需库的全部内容直接复制并打包到可执行文件中的过程。这样生成的程序是一个完全独立的文件,不依赖于外部的任何共享库。
执行过程:
- 在编译阶段,静态链接器会将程序用到的库方法和变量的完整代码,直接复制到最终生成的可执行文件中。
- 生成的可执行文件包含了所有必需的代码,因此可以在没有外部库支持的环境下独立运行。
主要特点:
- 体积较大:由于包含了所有需要的库内容,静态链接生成的可执行文件体积通常会比动态链接的程序大很多。
- 独立性高:程序具有极高的独立性。一旦编译完成,程序就不再受外部库文件缺失或变动的影响,部署和分发更为简单。
4.2.3.3 动态链接与静态链接的核心区别
资源依赖:动态链接的程序依赖外部共享库;静态链接的程序不依赖外部库。
链接时机:动态链接主要在程序运行时完成链接;静态链接在程序编译时就已完成。
程序体积:动态链接的程序体积较小;静态链接的程序体积通常较大。
库缺失影响:动态链接下,如果库文件缺失,程序将无法运行;静态链接则不受影响。
5. 自动化构建 make/Makefile
5.1 基本概念
什么是 makefile
在以后的工作环境中,一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,那么如何对这些源文件进行管理呢?比如哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行一些更复杂的功能操作。
Linux 提供了项目自动化构建工具 makefile 来解决这个问题。makefile 定义了一系列的规则来指定如何对众多的源文件进行管理。makefile带来的好处就是“自动化编译”,即 makefile 一旦写好,以后就只需要一个 make 命令,整个工程就可以完全自动编译,极大的提高了软件开发的效率。
在一个企业中,会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。
什么是 make
make 是一个用来解释 makefile 中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,比如:Delphi 的 make,Visual C++的 nmake,Linux 下 GNU 的 make。可见,makefile 已经成为了一种在工程方面的编译方法。
总结:make是一条命令,makefile是一个文件,二者搭配使用,实现项目自动化构建。
5.2 核心思想
编写 makefile,最重要的是编写 依赖关系和依赖方法,最核心的思想是依赖思想。依赖关系是指一个文件依赖另外一个文件,即想要得到一个文件,目录下必须先有另外一个文件。依赖方法则是指如何根据依赖文件来得到目标文件。只有依赖关系和依赖方法同时存在,才可以达成目标。
mytest:test.cgcc test.c -o mytest
5.3 基本使用
在编写 makefile 时有几个需要注意的地方:
- makefile 的文件名必须是 makefile/Makefile,不能是其他名称,否则 make 识别不了。
- 依赖文件可以有多个,也可以没有。
- 依赖方法必须以 [Tab] 键开头,特别注意不能是四个空格。
5.3.1 具体示例
下面以一个C语言的例子来说明应如何编写 makefile:
test.c:
#include <stdio.h>
int main()
{printf("hello makefile\n");printf("hello makefile\n");printf("hello makefile\n");return 0;
}
makefile:
mytest:test.c #依赖关系gcc test.c -o mytest #依赖方法.PHONY:clean #伪目标
clean:rm -f mytest
分析:
**依赖关系:**mytest 依赖 test.c 。clean 不依赖任何文件。
**依赖方法:**第一个的依赖方法是 gcc 编译,用于生成 mytest 可执行文件。第二个依赖方法是 rm -f 指令,用于删除指定的可执行文件。
其中 .PHONY 修饰 clean 表示其是一个伪目标,总是被执行。
5.3.2 make 的工作原理
5.3.2.1 make 的使用
在上面的示例中可以看到,第一个命令的执行不需要加目标文件,第二个命令则需要加目标文件。
这是因为在 Linux 下,输入 make 命令后,make 会在当前目录下找寻名为 “Makefile” 或 “makefile” 的文件。如果找到,它会把目录中的第一个目标文件作为最终的目标文件;如果找不到,就打印提示信息。
在上面的C语言例子中,makefile 中一共有两个目标文件 mytest和 clean.如下,输入 make 它默认只会执行第一个目标文件;当然,也可以通过指定目标文件来让它形成指定的目标文件。
没有 makefile 的情况:
生成默认文件和指定文件的情况:
5.3.2.2 make 的依赖性
关于 make 的依赖性,还是以上面这个例子来说明,只不过需要修改它的 makefile 文件:
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 会在当前目录下找寻名为 “Makefile” 或 “makefile” 的文件。如果找到,它会把文件中的第一个目标文件作为最终的目标文件 (上面例子中的 myproc),但是如果 myproc 所依赖的 myproc.o 文件不存在,那么 make 会在当前文件中找目标为 myproc.o 文件的依赖性,再根据该一个规则来生成 myproc.o 文件 (类似于数据结构栈后进先出)。如果 myproc.o 的依赖文件也不存在,则继续执行该规则,直到找到存在依赖文件的目标文件,得到目标文件后层层返回形成路径上的其他目标文件,或者最后被依赖的文件找不到,直接退出并报错。
这就是整个 make 的依赖性,make 会一层又一层地去找文件的依赖关系,直到最终编译出最开始需要的目标文件。
在上面的例子中,myproc 依赖的 myproc.o 不存在,make 会去寻找以 myproc.o 为目标文件的依赖关系;myproc.o 依赖的 myproc.s 也不存在,make 又会去找 以 myproc.s 为目标文件的依赖关系;然后 myproc.s 依赖 myproc.i,最后,myproc.i 的依赖文件 myproc.c 终于存在了,make 就会根据 myproc.i 的依赖方法形成 myproc.i,再逐步形成 myproc.s、myproc.o,直到最后形成 myproc。
5.3.2.3 项目清理
一个工程是需要清理的,在 makefile 中,常用 clean 来作为项目清理的目标文件,同时,由于项目清理不需要依赖其他文件,所以 clean 也不存在依赖关系。
另外,由于 clean 没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,所以需要显示指定:make clean
。
执行结果:
最后,像 clean 这种目标文件,一般都会用 .PHONY 将其设置为伪目标,伪目标的特性是:该目标文件总是被执行。
5.3.3.4 .PHONY 伪目标
如果对同一个源文件多次 make,会发现第一次程序正常编译,但第二次及以后就不再编译,而是提示:“make: `mytest’ is up to date.”。
上面这种现象是 make 为了防止对已经编译好且未做修改的源文件重复编译而浪费时间。也就是说,如果 test.c 已经编译得到了 mytest,并且并没有对 test.c 做改动,那么再次 make 时 make 不会被执行。实际上 make 这样做是很有必要的,因为在工作中,编译一个工程往往需要几十分钟甚至几个小时,如果一不小心再输入一次 make 都重新编译,势必会浪费很多时间。
那么如何判断一个文件是否需要重新编译就是一个非常重要的事情,编译器会对比源文件(src.c)和可执行文件(bin)的修改时间,源文件比可执行文件早的即可以编译,反之则无法编译。
因为在之前提到过文件 = 内容 + 属性和文件ACM时间这两大类观念,这里的修改时间也就是上面编译器所参考的时间。
然而当一个目标文件被 .PHONY 修饰之后,就不根据文件的修改时间先后来判断是否需要重新执行,从而达到总是被执行的效果。
5.4 更加具有通用性的 makefile
如果想要写出一个更加具有通用性的 makefile ,就需要知道可执行文件到底是怎么得到的。在平时开发的过程中,会编写许多C语言文件,如下:a.c
、b.c
、d.c
和 e.c
等等,先通过编译得到对应的 .o
文件再与动静态库进行链接再得到可执行文件,所以 makefile 的编写思路同理,先由 .c
文件编译得到 .o
文件,再由 .o
文件链接得到可执行文件。
mytest:mytest.ogcc mytest.o -o mytest
mytest.o:test.cgcc -c test.c -o mytest.o.PHONY:clean
clean: rm -f *.o mytest
以上的代码也不是最终版的 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) # $(RM): 替换,⽤变量内容替换它 .PHONY:test
test: @echo $(SRC) @echo $(OBJ)
6. 版本控制器 Git
6.1 理解版本控制
版本控制是一种记录文件内容变化,以便将来查阅特定版本修订情况的系统。版本控制其实最重要的是可以记录文件修改历史记录,从而让用户能够查看历史版本,方便版本切换。
举个通俗易懂的例子,就好比写毕业论文的时候,不可能写一遍就能得到导师的肯定,要多次修改。为了保留一开始的毕业论文,通常会拷贝原来的毕业论文,生成一个副本,对毕业论文的副本进行修改。最后,也可以通过对比原本和副本之间的优劣。下图就是简单的版本控制。
6.2 Git 的发展历史
同生活中的许多伟大事物一样,Git 诞生于一个极富纷争大举创新的年代。
Linux 内核开源项目有着为数众多的参与者。绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991–2002 年间)。到 2002 年,整个项目组开始启用一个专有的分布式版本控制系统 BitKeeper
来管理和维护代码。
到了 2005 年,开发 BitKeeper
的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了 Linux 内核社区免费使用 BitKeeper
的权力。这就迫使 Linux 开源社区(特别是 Linux 的缔造者 Linus Torvalds
)基于使用 BitKeeper
时的经验教训,开发出自己的版本系统。他们对新的系统制订了若干目标:
- 速度
- 简单的设计
- 对非线性开发模式的强力支持(允许成千上万个并行开发的分支)
- 完全分布式
- 有能力高效管理类似 Linux 内核一样的超大规模项目(速度和数据量)
自诞生于 2005 年以来,Git
日臻成熟完善,在高度易用的同时,仍然保留着初期设定的目标。它的速度飞快,极其适合管理大项目,有着令人难以置信的非线性分支管理系统。
6.3 Git 的安装和使用
6.3.1 判断是否安装了 git
$ git --version
$ which git
使用如上命令,如果出现上图内容则表示已经安装,反之则需要安装。
6.3.2 安装 git
# Centos
sudo yum install git# Ubnutu
sudo apt install -y git
如果需要安装则使用上面两条命令进行提权安装即可。
6.3.3 git 操作
6.3.3.1 新建仓库
登录自己的 gitee 账号,点击右上方的 “+” 号新建仓库。新建仓库的语言和.gitignore
根据自己学习的语言来选择,其余选项可以如下图选择的一样,最后点击创建。
6.3.3.2 克隆远程仓库到本地
创建完成后,复制克隆 / 下载里的 HTTPS 链接,然后在自己想要在的路径下 Linux 里输入命令git clone HTTPS链接
,即可完成克隆。
克隆完成后,可以进入刚克隆好的仓库,输入 ll -a
指令来查看仓库里的内容。(注:下图有每个文件的说明)
当前路径下的 linux-test
目录就表示为当前工作区,而 .git
内包含暂存区和本地 git 仓库。
同时所谓的 git 仓库,本质就是一个目录 .git 加上该目录里面的内容,当使用 push 命令的时候,本质是将 .git 里面的内容同步到远端的 gitee 中的仓库去。
6.3.3.3 配置 .gitignore
因为 git 版本管理,只管理源文件,所以文件 .gitignore 的作用就是去除目录中源文件的文件。
文件 .gitignore 里面内容是一些文件或者是文件名,只要在 .gitignore 里出现的,相应的文件都不会被提交到远端仓库。
以下是配置好的 .gitignore 文件,可以参考一下。
# Build and Release Folders
bin-debug/
bin-release/
[Oo]bj/
[Bb]in/# Other files and folders
.settings/# Executables
*.swf
*.air
*.ipa
*.apk#过滤掉不想要文件和文件夹
*.sln
*.vcxproj
*.filters
*.user
*.suo
*.db
*.ipch
Debug/
.vs
Release/#ignore makefile/Makefile
makefile
Makefile# Prerequisites
*.d# Compiled Object files
*.slo
*.lo
*.o
*.obj# Precompiled Headers
*.gch
*.pch# Compiled Dynamic libraries
*.so
*.dylib
*.dll# Fortran module files
*.mod
*.smod# Compiled Static libraries
*.lai
*.la
*.a
*.lib# Executables
*.exe
*.out
*.app
注:# 号后面的内容为注释信息。大家可以自行测试 在 .gitignore 里出现的文件是否会被推送到远程仓库。
6.3.3.4 推送文件到远端的三板斧
将新增文件添加到暂存区
git add . 或者 git add --all #将当前目录添加到暂存区
git add 文件名 #将指定文件添加到暂存区
将新增文件添加到本地仓库
git commit -m '日志信息' #将新增的内容添加到本地仓库.git
注:日志信息不能没有,也不能乱写。
将新增文件推送到远端仓库
git push #将新增内容推送到远端仓库
具体示例:
补充:
git 提交的时候,只会提交变化的部分。
比如原来有100行代码,现在删除了第99行,git 并不会把这100行和99行的代码都记录下来,只会在100行的基础上先记录以一个删除的字符串再将99行的内容 ping 在后面,如果后期需要代码回退只需要执行逆过程,将第99行代码再插入进去即可。如果是对代码进行修改,只需要对修改前和修改后字符串信息和行号都记录下来等等。
并且在 git 仓库中,其会按照自己的语法记录一段代码的增删修改,并不是单纯的一碰到不一样的代码就完整的记录下来,这样十分浪费空间。而是使用自己的语法对特定的语句进行记录,后期恢复再按固定操作恢复即可。
6.3.3.5 查看日志信息
git log #查看日志信息
6.3.3.6 查看仓库状态
git status #查看仓库状态
在更新了 .gitignore
文件中的内容之后,通过查看仓库的状态可以看出已经提示了被修改的文件的文件名。
6.3.3.7 重命名已提交的文件
git mv 原来的文件名 想要的文件名 #将原来的文件重命名
6.3.3.7 拉取远端仓库
git pull #拉取远端仓库,将本地仓库的信息和远端仓库的信息保持一致
这种情况是当一个已经完成 push 已经存在在远端仓库的一个文件发生了改变(比如在 windows 平台克隆仓库并修改文件 test2.c 再将其上传到远端仓库),然后再在本地新建一个文件,再利用三板斧上传的时候会发生报错,提醒发生了冲突。
如上图,在进行 push 操作的时候提示本地仓库和远程仓库冲突了,push 之前,使用 git pull
对远程仓库进行拉取,让本地仓库和远程仓库保持一致。
输入git pull
命令后,会出现很多提示信息。这些提示信息,都不需要管,输入:q!
命令强行退出就行了。
再输入git push
命令后,可以看到刚才新增的文件也被推送到远程仓库了。也能够看到,在远程仓库上做的修改也同步到本地仓库了。
使用 pull 命令后会不会对本地的源文件造成影响?
答案是:并不会,git 无权对本地的文件进行覆盖修改。
如果同一个文件
code.c
A程序员在里面加了一行代码,B程序员在里面减了一行代码,此时A程序员使用 pull 命令同步本地仓库时的code.c
既有添的代码也有减的代码,这个操作会在源文件中体现出来,由程序员本身决定源文件的修改再统一提交。
7. 调制器 gdb/cgdb
7.1 debug 和 release
在 Windows 中使用 VS 的时候知道:程序的发布方式一共有两种 debug 模式和 release 模式。其中 debug 模式是给程序员用的,其中包含调试信息,程序员可以根据这些调试信息对程序进行修改与完善,而 release 模式则是给用户用的,它不包含调试信息,因为用户不负责也不关心如何对程序进行调试。
Linux 中使用 gcc/g++ 编译链接得到的程序默认是 release 模式的,如果要使用 gdb 进行调试,必须在源代码生成二进制程序的时候添加 -g
选项。程序要调试必须是 debug 模式,也就是说明编译时要加 -g 选项,gdb 也只能作用于携带调试信息的可执行文件。
$ gcc mycmd.c -o mycmd # 默认release模式,不支持调试
$ 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/Linux 3.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
7.2 gdb 基础使用
当指定 -g 得到以 debug 模式发布的可执行程序后,就可以使用 gdb 对其进行调试了。下面进行调试的样例代码如下。
样例代码:
// mycmd.c
#include <stdio.h>int Sum(int s, int e)
{int result = 0;for(int i = s; i <= e; i++){result += i;}return result;
}int main()
{int start = 1;int end = 100;printf("开始了\n");int n = Sum(start, end);printf("运行中,结果是: [%d-%d]=%d\n", start, end, n);return 0;
}
这是一个求和代码,这里求解的是 1~100
中所有数的和。运行结果如下:
下面是使用 gdb 做一些简单的调试:
代码的显示:
运行代码:
打断点并调试:
可以看到代码运行到第18行停了下来。
**总结:**可以看到虽然 gdb 可以对代码进行简单的调试,但是实际的体验还是十分糟糕。所以下面的介绍主要以 cgdb 为主。
7.3 cgdb 基础使用
7.3.1 cgdb 的安装
# Centos
sudo yum insatll -y cgdb# Ubnutu
sudo apt-get install -y cgdb
7.3.2 cgdb 的界面
与 gdb 的界面相比 cgdb 的使用体验可以说是肉眼可见的提升,所以后续的命令介绍的内容主要利用 cgdb 进行介绍。
7.4 调试器的使用
7.4.1 基础命令
- 开始:
gdb/cgdb binFile
- 退出:
Ctrl + d
或quit
调试命令
命令 | 作用 | 例子 |
---|---|---|
list / l | 显示源代码,从上次位置开始,每次列出10行 | list 10 |
list / l 函数名 | 列出指定函数的源代码 | list main |
list / l 文件名:行号 | 列出指定文件的源代码 | list mycmd.c:1 |
run / r | 从程序开头继续执行 | run |
next / n | 单步执行,不进入函数内部 | next |
step / s | 单步执行,进入函数内部 | step |
break / b 函数名 | 在函数开头设置断点 | break main |
break / b [文件名:]行号 | 在指定行设置断点 | break 10 或 break test.c:10 |
break / b | 在指定文件的某行设置断点 | break test.c:10 |
info break / info b | 查看当前所有断点的信息 | info break |
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 breakpoints n | 删除序号为 n 的断点 | 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 |
7.4.2 调试技巧
7.4.2.1 理解调试
调试的本质是什么?
- 找到问题
- 查看代码上下文,解决问题
断点的本质是什么?
把代码进行块级别划分,以块单位进行快速定位区域。
其中
finish
常用于问题是否在函数内,until
常用于局部区域快速执行。
7.4.2.2 watch
执行时监视⼀个表达式(如变量)的值。如果监视的表达式在程序运行期间的值发⽣变化,GDB会暂停程序的执行,并通知使用者。如果有一些变量不应该被修改,但是怀疑他修改导致了问题,可以 watch
他,如果变化了就会进行通知。
具体示例:
(gdb) l main
11
12 return result;
13 }
14
15 int main()
16 {
17 int start = 1;
18 int end = 100;
19 printf("开始了\n");
20 int n = Sum(start, end);(gdb) b 2
Breakpoint 1 at 0x11c3: file mycmd.c, line 20.(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000000011c3 in main at mycmd.c:20(gdb) r
Starting program: /home/whb/test/test/mycmd
开始了Breakpoint 1, main () at mycmd.c:20
20 int n = Sum(start, end);(gdb) s
Sum (s=32767, e=-7136) at mycmd.c:5
5 {(gdb) n
6 int result = 0;(gdb) watch result
Hardware watchpoint 2: result(gdb) c
Continuing.Hardware watchpoint 2: resultOld value = -6896
New value = 0
Sum (s=1, e=100) at mycmd.c:7
7 for(int i = s; i <= e; i++)(gdb) c
Continuing.Hardware watchpoint 2: resultOld value = 0
New value = 1
Sum (s=1, e=100) at mycmd.c:7
7 for(int i = s; i <= e; i++)(gdb) c
Continuing.Hardware watchpoint 2: resultOld value = 1
New value = 3
Sum (s=1, e=100) at mycmd.c:7
7 for(int i = s; i <= e; i++)(gdb) c
Continuing.Hardware watchpoint 2: resultOld value = 3
New value = 6
Sum (s=1, e=100) at mycmd.c:7
7 for(int i = s; i <= e; i++)(gdb) info b
Num Type Disp Enb Address What1 breakpoint keep y 0x00005555555551c3 in main at mycmd.c:20breakpoint already hit 1 time(gdb) finish
Run till exit from #0 Sum (s=1, e=100) at mycmd.c:7
0x00005555555551d2 in main () at mycmd.c:20
20 int n = Sum(start, end);
Value returned is $1 = 5050
7.4.2.3 set var确定问题原因
当理解了错误,知道如何修改的时候可以使用此命令修改某个变量的值,并以新的值带入运行,如果运行正确即说明找到错误并解决了问题。
示例代码:
更改标志位,假设这里的目的是让 Sum 函数的返回值可以收到标志位 flag 的控制从而返回 +-result
但是这里故意写错为 flag = 0
。
// mycmd.c
#include <stdio.h>int flag = 0; // 故意错误
//int flag = -1;
//int flag = 1;int Sum(int s, int e)
{int result = 0;for(int i = s; i <= e; i++){result += i;}return result*flag;
}int main()
{int start = 1;int end = 100;printf("开始了\n");int n = Sum(start, end);printf("等待中,结果是: [%d-%d]=%d\n", start, end, n);return 0;
}
调试过程:
(gdb) l main
15
16 return result*flag;
17 }
18
19 int main()
20 {
21 int start = 1;
22 int end = 100;
23 printf("开始了\n");
24 int n = Sum(start, end);(gdb) b 24
Breakpoint 1 at 0x11ca: file mycmd.c, line 24.(gdb) r
Starting program: /home/whb/test/test/mycmd
开始了Breakpoint 1, main () at mycmd.c:24
24 int n = Sum(start, end);(gdb) n
25 printf("running done, result is: [%d-%d]=%d\n", start, end,
n);(gdb) n
running done, result is: [1-100]=0 # 这⾥结果为什么是0?26 return 0;(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/whb/test/test/mycmd
开始了Breakpoint 1, main () at mycmd.c:24
24 int n = Sum(start, end);(gdb) s
Sum (s=32767, e=-7136) at mycmd.c:9
9 {(gdb) n
10 int result = 0;
(gdb) n
11 for(int i = s; i <= e; i++)
(gdb)
13 result += i;
(gdb)
11 for(int i = s; i <= e; i++)
(gdb)
13 result += i;(gdb) until 14
Sum (s=1, e=100) at mycmd.c:16
16 return result*flag;(gdb) p result
$1 = 5050(gdb) p flag
$2 = 0(gdb) set var flag=1 # 更改flag的值,确认是否是它的原因(gdb) p flag
$3 = 1(gdb) n
17 }
(gdb) n
main () at mycmd.c:25
25 printf("等待中,结果是: [%d-%d]=%d\n", start, end,
n);
(gdb) n
running done, result is: [1-100]=5050 # 是它的原因
26 return 0;
7.4.2.4 条件断点
注意:
- 条件断点添加常见两种方式:1. 新增 2. 给已有断点追加
- 注意两者的语法有区别,不要写错了。
- 新增:
b 行号/文件名:行号/函数名 if i==30
(条件) - 给已有断点追加:
condition 2 i==30
,其中2
是已有断点编号,没有if
新加条件断点:
(gdb) l main
15
16 return result*flag;
17 }
18
19 int main()
20 {
21 int start = 1;
22 int end = 100;
23 printf("开始了\n");
24 int n = Sum(start, end);(gdb) b 20
Breakpoint 1 at 0x11c3: file mycmd.c, line 20.(gdb) r
Starting program: /home/whb/test/test/mycmd
开始了
Breakpoint 1, main () at mycmd.c:20
20 int n = Sum(start, end);(gdb) s
Sum (s=32767, e=-7136) at mycmd.c:5
5 {(gdb) n
6 int result = 0;
(gdb) n
7 for(int i = s; i <= e; i++)
(gdb) n
9 result += i;(gdb) display i
1: i = 1(gdb) n
7 for(int i = s; i <= e; i++)
1: i = 1(gdb) n
9 result += i;
1: i = 2(gdb) n
7 for(int i = s; i <= e; i++)
1: i = 2(gdb) n
9 result += i;
1: i = 3(gdb)
7 for(int i = s; i <= e; i++)
1: i = 3(gdb) info b
Num Type Disp Enb Address What1 breakpoint keep y 0x00005555555551c3 in main at mycmd.c:20breakpoint already hit 1 time(gdb) b 9 if i == 30 # 9是⾏号,表⽰新增断点的位置
Breakpoint 2 at 0x555555555186: file mycmd.c, line 9.(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555551c3 in main at mycmd.c:20breakpoint already hit 1 time
2 breakpoint keep y 0x0000555555555186 in Sum at mycmd.c:9stop only if i == 30(gdb) finish
Run till exit from #0 Sum (s=1, e=100) at mycmd.c:7Breakpoint 2, Sum (s=1, e=100) at mycmd.c:9
9 result += i;
1: i = 30(gdb) finish
Run till exit from #0 Sum (s=1, e=100) at mycmd.c:9
0x00005555555551d2 in main () at mycmd.c:20
20 int n = Sum(start, end);
Value returned is $1 = 5050
给已存在的断点新增条件:
(gdb) l main
15
16 return result*flag;
17 }
18
19 int main()
20 {
21 int start = 1;
22 int end = 100;
23 printf("开始了\n");
24 int n = Sum(start, end);(gdb) b 20
Breakpoint 1 at 0x11c3: file mycmd.c, line 20.(gdb) r
Starting program: /home/whb/test/test/mycmd
开始了
Breakpoint 1, main () at mycmd.c:20
20 int n = Sum(start, end);(gdb) s
Sum (s=32767, e=-7136) at mycmd.c:5
5 {(gdb) n
6 int result = 0;
(gdb) n
7 for(int i = s; i <= e; i++)
(gdb) n
9 result += i;
(gdb)
7 for(int i = s; i <= e; i++)
(gdb)
9 result += i;
(gdb)
7 for(int i = s; i <= e; i++)
(gdb)
9 result += i;
(gdb)
7 for(int i = s; i <= e; i++)
(gdb) b 9 # 在第9⾏新增⼀个断点,⽤来开始测试
Breakpoint 2 at 0x555555555186: file mycmd.c, line 9.(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555551c3 in main at mycmd.c:20breakpoint already hit 1 time
2 breakpoint keep y 0x0000555555555186 in Sum at mycmd.c:9(gdb) n
Breakpoint 2, Sum (s=1, e=100) at mycmd.c:9
9 result += i;
(gdb) n
7 for(int i = s; i <= e; i++)
(gdb) n
Breakpoint 2, Sum (s=1, e=100) at mycmd.c:9
9 result += i;(gdb) condition 2 i==30 #给2号断点,新增条件i==30
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555551c3 in main at mycmd.c:20breakpoint already hit 1 time
2 breakpoint keep y 0x0000555555555186 in Sum at mycmd.c:9stop only if i==30breakpoint already hit 2 times(gdb) n
7 for(int i = s; i <= e; i++)
(gdb) n
9 result += i;
(gdb) c
Continuing.Breakpoint 2, Sum (s=1, e=100) at mycmd.c:9
9 result += i;(gdb) p i
$1 = 30(gdb) p result
$2 = 435
8. Linux 第一个系统程序 – 进度条
8.1 概念补充
8.1.1 回车与换行
对于 ‘\n’ 想必大家已经很熟悉了,因为在C语言的 printf 函数中会频繁的用到它,但是实际上C语言学习的 ‘\n’ 是 ‘\r’ + ‘n’。
\r
:回车,即将光标移动到当前行的行首。\n
:换行,即将光标移动到下一行。
可以看到,C语言中的 \n
的作用是 回车 + 换行,而不仅仅是换行,这也是为什么许多台式机的 enter 键是下面这样的:
8.1.2 行缓冲
在C语言的学习中就知道,从键盘输入的字符以及向显示器输出的内容,并不会直接读入或输出,而是会先被存放到输入缓冲区与输出缓冲区中,待缓冲区刷新时数据才会才会被读入或输出。
而行缓冲是缓冲区类型的一种,在行缓冲下,当 在输入和输出中遇到换行符时,才执行真正的I/O操作,即输入的字符会先存放在缓冲区,等按下回车键时才进行真正的I/O操作。
8.2 进度条的编写
有了回车换行和行缓冲的概念之后,就可以编写进度条代码了。
process.h:
#pragma once
#include <stdio.h>// 第一版:展示进度条基本功能
void process_v1();
// 第二版:根据进度,动态刷新一次进度条
void FlushProcess(double total, double current);
process.c:
#include "process.h"
#include <string.h>
#include <unistd.h>#define NUM 101
#define STYLE '='// vesion1:展示进度条基本功能
void process_v1()
{char buffer[NUM];memset(buffer, 0, sizeof(buffer));const char *lable="|/-\\";int len = strlen(lable);int cnt = 0;while(cnt <= 100){printf("[%-100s][%d%%][%c]\r", buffer, cnt, lable[cnt%len]);fflush(stdout);buffer[cnt]= STYLE;cnt++;usleep(50000);}printf("\n");// verison2:根据进度,动态刷新一次进度条
void FlushProcess(double total, double current)
{char buffer[NUM];memset(buffer, 0, sizeof(buffer));const char *lable="|/-\\";int len = strlen(lable);static int cnt = 0;// 不需要⾃⼰循环,填充# int num = (int)(current*100/total); // 11.0 / 1000int i = 0;for(; i < num; i++){buffer[i] = STYLE;}double rate = current/total;cnt %= len;printf("[%-100s][%.1f%%][%c]\r", buffer, rate*100, lable[cnt]);cnt++;fflush(stdout);
}
main.c:
#include "process.h"
#include <stdio.h>
#include <unistd.h>double total = 1024.0; // 下载总量
double speed = 1.0; // 下载速率void DownLoad()
{double current = 0.0; // 当前下载量while(current <= total){FlushProcess(total, current);// 下载代码usleep(3000); // 充当下载数据current += speed;}printf("\ndownload %.2lfMB Done\n", current);
}int main()
{DownLoad();DownLoad();DownLoad();DownLoad();DownLoad();DownLoad();DownLoad();DownLoad();return 0;
}
makefile:
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
BIN=processbar$(BIN):$(OBJ)gcc -o $@ $^
%.o:%.cgcc -c $<.PHONY:
clean:rm -f $(OBJ) $(BIN)
在 proces.c 中,每次打印数据之后让光标回到行首,然后刷新缓冲区,再增加 buffer 数组里面的标志字符 cnt,这样使得下一次打印数据时可以直接覆盖掉之前的数据,并且增加一格,从而达到进度条的效果.
同时,为了使进度条更加真实,还增加了一个进度值 cnt 和 旋转光标 label,以及每次打印后让程序休眠0.05秒,使得进度条有一个加载进度以及一个旋转的等待符号;
最后,为了丰富进度条字符的样式,我们把进度条字符设置成了一个字符数组,用户可以根据自己的需要通过调整 STYLE 的值来改变进度条的字符样式。
注意:在 printf 函数中,% 具有特殊意义,所以需要输入 %%
来对其进行转义;同样,在 label 数组中,字符 ‘\’ 也是特殊字符,我们需要输入 \\
。
最终效果: