目录
[4. 自动化构建-make/Makefile](#4. 自动化构建-make/Makefile)
[4.1 背景](#4.1 背景)
[4.2 基本使用](#4.2 基本使用)
[4.2.1 依赖关系](#4.2.1 依赖关系)
[4.2.2 依赖方法](#4.2.2 依赖方法)
[4.2.3 make执行流程(如果依赖项不存在或更旧,则会先执行形成依赖项的依赖关系)](#4.2.3 make执行流程(如果依赖项不存在或更旧,则会先执行形成依赖项的依赖关系))
[4.2.4 .PHONY声明伪目标、make执行条件](#4.2.4 .PHONY声明伪目标、make执行条件)
[4.2.5 变量定义及其他指令](#4.2.5 变量定义及其他指令)
[5. Linux第一个系统程序-进度条](#5. Linux第一个系统程序-进度条)
[5.1 回车与换行的区别](#5.1 回车与换行的区别)
[5.2 行缓冲区](#5.2 行缓冲区)
[5.3 倒计时程序](#5.3 倒计时程序)
[5.4 进度条代码实现](#5.4 进度条代码实现)
[6. 版本控制器Git](#6. 版本控制器Git)
[6.1 版本控制器](#6.1 版本控制器)
[6.2 git 简史](#6.2 git 简史)
[6.3 安装git](#6.3 安装git)
[6.4 在git创建项目并下载到本地](#6.4 在git创建项目并下载到本地)
[6.5 三板斧(add添加、commit提交、push同步)](#6.5 三板斧(add添加、commit提交、push同步))
[7. 调试器 -gdb/cgdb的使用](#7. 调试器 -gdb/cgdb的使用)
[7.1 gdd/cgdb安装](#7.1 gdd/cgdb安装)
[7.2 示例代码](#7.2 示例代码)
[7.3 预备知识](#7.3 预备知识)
[7.4 常见cgdb/gdb的使用](#7.4 常见cgdb/gdb的使用)
[7.5 调试技巧](#7.5 调试技巧)
4. 自动化构建-make/Makefile
4.1 背景
• 会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力
• 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作
• makefile带来的好处就是------"自动化编译",一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
• make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。
• make是一条命令,makefile(Makefile中的M大写也可以)是一个文件,两个搭配使用,完成项目自动化构建。
4.2 基本使用
makefile文件的内容主要是依赖关系 、依赖方法 和命令。
makefile中代码的缩进一定要使用Tab不能使用空格!!!
实例代码
cpp
#include <stdio.h>
int main()
{
printf("hello Makefile!\n");
return 0;
}
bash
myproc:myproc.c
gcc -o myproc myproc.c
.PHONY:clean
clean:
rm -f myproc
**依赖关系:**上面的文件myproc,它依赖myproc.c
**依赖方法:**gcc -o myproc myproc.c ,就是与之对应的依赖方法
伪目标:.PHONY修饰的是伪目标
clean指令:运行make clean,则执行伪目标clean的指令,rm -f myproc
4.2.1 依赖关系
依赖关系的声明
bash
target: prerequisites
左边 (target):称为 目标 ;右边 (prerequisites):称为 先决条件(或依赖项)
依赖关系决定依赖方法的源文件和目标文件。
一个目标的声明到下一个目标声明之间,都是本目标要执行的依赖方法或者命令。
4.2.2 依赖方法
根据依赖关系执行相应的指令。以下的每个依赖关系下面的gcc一行就是依赖方法。
例如:
bash
process:myprog.o dep1.o dep2.o
gcc -o process myprog.o dep1.o dep2.o
myprog.o:myprog.c
gcc -c myprog.c
dep1.o:dep1.c
gcc -c dep1.c
dep2.o:dep2.c
gcc -c dep2.c
.PHONY:clean
clean:
rm -f *.o myprog
4.2.3 make执行流程(如果依赖项不存在或更旧,则会先执行形成依赖项的依赖关系)
指令"make"只会执行第一个目标!!但是特殊情况会递归执行。如下:
在 shell 输入 make 确实只执行了第一个目标 process。但是,为了完成"执行 process"这个任务,make 发现 process依赖于myprog.o、dep1.o 和 dep2.o,而这三个文件还不存在或需要更新。于是,make 自动地、递归地先去执行能生成这些依赖文件的目标规则。这就像是为了完成一个任务,你必须先完成它的所有子任务。(类似将每个依赖关系的gcc依赖方法入栈,然后依次出栈)
当您输入 make 时,make 会按照以下步骤执行:
-
找到默认目标:make 读取 Makefile,确定第一个目标是 process。
-
检查 process的依赖:它发现 process依赖于 myprog.o 、dep1.o和 dep2.o 这三个文件。
-
递归解决依赖(关键步骤):
对于 myprog.o:make 检查是否存在 myprog.o 文件,以及它是否比它的依赖 myprog.c 更旧。由于 myprog.o 还不存在,make 发现有一条规则明确描述了如何构建它:myprog.o: myprog.c。于是,make 先跳转去执行那条规则的命令:gcc -c myprog.c。这样就生成了 myprog.o。
对于 dep1.o:同样的过程发生。make 检查 dep1.o,发现它不存在,于是去执行 dep1.o: dep1.c 规则的命令:gcc -c dep1.c,生成了 dep1.o。
对于dep2.o同理。
-
所有依赖就绪,执行最终目标:现在,myprog.o 、dep1.o和 dep2.o 都已经生成且是最新的。make 回到最初的目标 process,检查发现所有依赖都已就绪,于是执行它的命令:gcc -o process myprog.o dep1.o dep2.o
-
结束:最终目标 process 生成完成,make 退出。
bash
# 终端执行make之后,打印make的指令流程,可见先执行生成myprog.o、dep1.o、dep2.o之后,再链接生成process
$ make
gcc -c myprog.c
gcc -c dep1.c
gcc -c dep2.c
gcc -o process myprog.o dep1.o dep2.o
4.2.4 .PHONY声明伪目标、make执行条件
1. make执行条件
对于普通文件目标:目标文件相对于其依赖项是否过期决定是否执行
目标文件不存在,执行。
目标文件比它的任何一个依赖文件旧,执行。(取决于目标文件和依赖文件Modify时间的新旧)
目标文件存在且比所有依赖文件新,跳过。
对于伪目标 (用 .PHONY 声明):
无条件执行。
2. 声明伪目标.PHONY:不进行检查,无条件执行命令
伪目标是为了解决名字冲突问题而生的。假设你有一个名为 clean 的文件:
如果没有 .PHONY 声明:当你运行 make clean 时,make 发现已经有一个叫 clean 的文件,而且这个文件没有任何依赖,因此它会被判定为"最新",从而拒绝执行删除命令。这与你的意图完全相反。
有了 .PHONY: clean 声明:你明确告诉 make:"clean 这个名字代表的不是一个文件,而是一个动作标签"。这样,无论文件系统中是否存在 clean 文件,make clean 命令都会坚决地执行清理动作。
bash
# 也可以声明目标文件,则会无条件执行,不推荐
.PHONY:myprog
myprog:myprog.c
gcc -o myprog myprog.c
# 声明clean标签,无条件执行clean目标要执行的指令内容
.PHONY:clean
clean:
rm -f *.o myprog
4.2.5 变量定义及其他指令
在依赖关系 或者命令 行前加**@**,会让 Make 工具不显示该命令本身的打印信息(只执行命令,不输出命令内容)。
1. 变量定义
makefile文件中的变量定义类似C语言中的define宏定义。但是变量的使用需要用$()进行解引用。
bash
BIN=process
CC=gcc
SRC=myproc.c
FLAGS=-o
RM=rm -f
$(BIN):$(SRC)
$(CC) $(FLAGS) $@ $^
.PHONY:clean
clean:
$(RM) $(BIN)
2. 多文件变量定义
代码功能:1.将目录下所有的.c文件编译成.o文件;2.将所有.o文件链接成可执行文件process。3.可以通过make clean删除.o和可执行文件。
多文件变量定义格式见代码
bash
BIN=process
CC=gcc
# 可以使用shell指令给变量赋值
#SRC=$(shell ls *.c) # 1.将当前目录下所有以 .c 结尾的源文件的文件名列表赋值给变量 SRC。
SRC=$(wildcard *.c) # 功能同上
OBJ=$(SRC:.c=.o) # 2.将SRC中文件名的后缀全部换成.o
LFLAGS=-o
FLAGS=-c
RM=rm -f
$(BIN):$(OBJ) # 将所有的.o文件链接成process
$(CC) $(LFLAGS) $@ $^
%.o:%.c #将所有的.c编译成对应的.o文件
$(CC) $(FLAGS) $< -o $@
.PHONY:clean
clean:
$(RM) $(OBJ) $(BIN)
3. 多文件变量操作
|------|----------------------------------|------------------------------------|--------------------------------------------------------------------------------------------|
| 自动变量 | 含义 | 特点 | 典型场景 |
| @ | 指代当前规则中的目标文件名称(即 : 左侧的文件名) | 1. 随目标变化而自动替换 2. 适用于任何目标(文件目标或伪目标) | 编译或链接时指定输出文件名,例如: a.o: a.c gcc -c a.c -o @ 等价于 gcc -c a.c -o a.o |
| \^ | 指代当前目标的所有依赖文件 | 1. 包含所有依赖(不止一个) 2. 会自动去除重复的依赖项 | 链接阶段(需要所有目标文件),例如: myprog: a.o b.o c.o gcc ^ -o @ 等价于 gcc a.o b.o c.o -o myprog |
| < | 指代当前目标的第一个依赖文件 | 1. 只取依赖列表中的第一个文件 2. 不会处理后续依赖 | 编译阶段(单个源文件生成目标文件),例如: a.o: a.c head.h gcc -c \< -o @ 等价于 gcc -c a.c -o a.o(只使用第一个依赖 a.c) |
| %.c | 模式通配符,表示所有以 .c 结尾的源文件(% 匹配任意字符串) | 1. 匹配一类文件(而非单个文件) 2. 用于定义通用规则的依赖 | 定义通用编译规则的依赖,例如: %.o: %.c 表示 "所有 .o 文件依赖于同名 .c 文件" |
| %.o | 模式通配符,表示所有以 .o 结尾的目标文件 | 1. 匹配一类文件(而非单个文件) 2. 用于定义通用规则的目标 | 定义通用编译规则的目标,例如: %.o: %.c gcc -c \< -o @ 表示 "所有 .o 文件都由对应 .c 文件编译生成" |
5. Linux第一个系统程序-进度条
5.1 回车与换行的区别
简单来说:
回车 (Carriage Return, CR, \r):将光标移动到当前行的行首。
换行 (Line Feed, LF, \n): 将光标移动到下一行的相同位置(垂直向下移动一行)。
把它们想象成一台老式打字机:
回车就是你用手把打字机的滚筒推回最左边的动作。
换行就是你转动滚筒,把纸向上推一行的动作。
要完成"新起一行"这个我们现代人认知中的操作,需要同时执行 回车 + 换行。
注意:有时我们的\n会被转换成\r\n。
5.2 行缓冲区
在Linux系统下运行对比以下两份代码:

发现:有\n的代码先打印再睡眠,没\n的代码先睡眠再打印。
实际程序都是按照顺序执行的,只是**\n有刷新行缓冲区的功能** ,所以会先打印;而没有\n的代码不会刷新行缓冲区,所以就不会显示,而在程序运行结束以后也会刷新行缓冲区,所以右边的代码在程序运行结束后会打印"hello world!"
在C标准库中有一个fflush函数(头文件是stdio.h),可以手动刷新缓冲区。对上图右边的代码添加fflush(stdout),刷新输出缓冲区,就会先打印hello world,再睡眠3秒。
cpp
#include <stdio.h>
#include <unistd>
int main()
{
printf("hello bite!");
fflush(stdout);
sleep(3);
return 0;
}
5.3 倒计时程序
cpp
#include<stdio.h>
#include<unistd.h>
int main()
{
int i = 10;
while(i >= 0)
{
//限定宽度为2,"-"表示左对齐,\r表示回车,回到行的开头位置
printf("%-2d\r",i);
fflush(stdout); //刷新行缓冲区
--i;
sleep(1);
}
printf("\n");
return 0;
}
5.4 进度条代码实现
注意事项:
- 即使进度条停止,但label仍在变化,说明仅仅可能是网络问题导致下载速度慢导致,而并不是下载停止了。
6. 版本控制器Git
6.1 版本控制器
为了能够更方便我们管理这些不同版本的文件,便有了版本控制器。所谓的版本控制器,就是能让你了解到一个文件的历史,以及它的发展过程的系统。通俗的讲就是一个可以记录工程的每一次改动和版本迭代的一个管理系统,同时也方便多人协同作业。
目前最主流的版本控制器就是 Git 。Git 可以控制电脑上所有格式的文件,例如 doc、excel、dwg、dgn、rvt等等。对于我们开发人员来说,Git 最重要的就是可以帮助我们管理软件开发项目中的源代码文件!
6.2 git 简史
同生活中的许多伟大事物一样,Git 诞生于一个极富纷争大举创新的年代。
Linux 内核开源项目有着为数众多的参与者。 绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991-2002年间)。 到 2002 年,整个项目组开始启用一个专有的分布式版本控制系统 BitKeeper 来管理和维护代码。
到了 2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了 Linux内核社区免费使用 BitKeeper 的权力。 这就迫使 Linux 开源社区(特别是 Linux 的缔造者 LinusTorvalds)基于使用 BitKeeper 时的经验教训,开发出自己的版本系统。 他们对新的系统制订了若干目标:速度、简单的设计、对非线性开发模式的强力支持(允许成千上万个并行开发的分支)、完全分布式、有能力高效管理类似 Linux 内核一样的超大规模项目(速度和数据量)。
自诞生于 2005 年以来,Git 日臻成熟完善,在高度易用的同时,仍然保留着初期设定的目标。 它的速度飞快,极其适合管理大项目,有着令人难以置信的非线性分支管理系统。
6.3 安装git
bash
# Centos
sudo yum install git
# Ubuntu
sudo apt install -y git
# 查看git版本
$ git --version
git version 1.8.3.1
6.4 在git创建项目并下载到本地
1. 创建仓库
2. 拉取远端仓库到本地
a. 复制仓库的链接
b. 拉取远端仓库到本地
然后git就直接把你创建的仓库linux-system-programming拉取到了本地,本地就多出一个名为linux-system-programming的目录。
查看此目录发现里面有以下几个文件:README.en和README分别是英文和中文的仓库说明;.gitignore用来设置需要忽略的文件类型;.git才是把远端仓库拉取到本地的本地仓库。
6.5 三板斧(add添加、commit提交、push同步)
当前工作区是指:linux-system-programming目录
1. git add
将代码放到刚才下载好的目录中,然后add添加到.git本地仓库的暂存区。
bash
git add [文件名]
2. git commit
提交改动到本地,提交的时候应该注明提交日志, 描述改动的详细内容。
bash
git commit -m "XXX"
3. git push
同步到远端服务器上,需要填入用户名密码. 同步成功后, 刷新 gitee 页面就能看到代码改动了。
bash
git push
4. 补充指令
|------------|------------------------------------------------------------------------|
| 指令 | 功能 |
| git status | 查看本次git仓库的状态(即修改、新增、add、commit的状态) |
| git log | 查看git提交日志,显示每次提交的详细信息 |
| git pull | 如果有人在其他设备往你的仓库push内容,等你下一次使用时需要pull拉取远端仓库的最新内容以进行同步。(远程仓库和任何人相比,都是最新的) |
5. 其他
a. ".gitignore"需要忽略的特定后缀的文件列表。保证上传的都是源文件。
b. 首次提交会让输入提交者的名称和邮箱,方便溯源。在git log中也可以看到。

7. 调试器 -gdb/cgdb的使用
7.1 gdd/cgdb安装
bash
Ubuntu: sudo apt-get install -y gdb
Centos: sudo yum install -y gdb
Ubuntu: sudo apt-get install -y cgdb
Centos: sudo yum install -y cgdb
cgdb和gdb的功能相同,相比于gdb的纯命令行,cgdb有分屏界面,上窗口:显示源代码(可滚动查看,类似代码编辑器)。下窗口:传统的 gdb 命令窗口(可输入 gdb 命令)。以cgdb为例学习。
7.2 示例代码
cpp
// mycode.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("I will begin\n");
int n = Sum(start, end);
printf("running done, result is: [%d-%d]=%d\n", start, end, n);
return 0;
}
7.3 预备知识
• 程序的发布方式有两种, debug 模式和release 模式, Linux gcc/g++ 出来的二进制程序,默认是release 模式。
• 要使用gdb调试,必须在源代码生成二进制程序的时候, 加上-g 选项,如果没有添加,程序无法被编译
bash
$ 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/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.4 常见cgdb/gdb的使用
• 开始: gdb binFile
• 退出: ctrl + d 或 quit 调试命令
|------------------------|-----------------------------------------------------------------------|------------------------------------|
| 命令 | 作用 | 样例 |
| 显示源代码 | | |
| list/l n | 显示源代码,从n位置开始,每次列出10行 | list/l 1 |
| list/l 函数名 | 列出指定函数的源代码 | list/l main |
| list/l 文件名:行号/函数名 | 列出指定文件的源代码 | list/l mycode.c:1 |
| 调试运行 | | |
| r/run | 从程序头开始连续执行 | run |
| n/next | 单步执行,不进入函数内部, 逐过程 F10 | next |
| s/step | 单步执行,进入函数内部, 逐语句 F11 | step |
| finish | 执行到当前函数返回,然后停止 | finish |
| continue/c | 从当前位置开始连续执行程序 | continue |
| until 行号 | 执行到指定行号 | until 20 |
| 设置断点,设置条件断点 | | |
| break/b 行号 | 在指定行号设置断点 | break 10 |
| break/b 函数名 | 在函数开头(入口处)设置断点 | break main |
| break/b [文件名]:行号/函数名 | 在指定行号设置断点 | break mycode.c:10 |
| b 行号 条件 | 添加条件断点 | b 9 if i == 30 # 9是行号,表示新增断点的位置 |
| condition 断点号 条件 | 给已有断点添加条件 | condition 2 i==30 #给2号断点,新增条件i==30 |
| 删除断点 | | |
| delete/d breakpoints | 删除所有断点 | delete breakpoints |
| delete/d breakpoints n | 删除序号为n的断点 | delete breakpoints 1 |
| delete/d n | 删除序号为n的断点 | delete 1 |
| 启用/禁用断点 | | |
| disable n | 禁用序号为n的断点 | disable 4 |
| disable breakpoints | 禁用所有断点 | disable breakpoints |
| enable breakpoints | 启用所有断点 | enable breakpoints |
| 查看断点列表 | | |
| info/i breakpoints | 查看当前设置的断点列表 | info breakpoints |
| info/i break/b | 查看当前所有断点的信息 | info b |
| 查看变量值 | | |
| print/p 表达式 | 打印表达式的值 | print start+end |
| p 变量 | 打印指定变量的值 | p x |
| display 变量名 | 跟踪显示指定变量的值(每次停止时)(监视) | display x |
| undisplay 编号 | 取消对指定编号的变量的跟踪显示 | undisplay 1 |
| info/i locals | 查看当前栈帧的局部变量值 | info locals |
| watch 变量名 | 执行时监视一个表达式(如变量)的值。如果监视的表达式在程序运行期间的值发生变化,GDB 会暂停程序的执行,并通知使用者。(l类似变量断点) | watch x |
| set var 变量=值 | 修改变量的值 | set var i=10 |
| 查看函数栈帧 | | |
| backtrace/bt | 查看当前执行栈的各级函数调用及参数 | backtrace |
| 退出调试 | | |
| quit | 退出GDB调试器 | quit |
gdb不退出,断点编号依次递增。
gdb会自动记忆最新的一条指令,Enter执行gdb记忆的指令。
bash
# 查看可执行文件(或目标文件)process 中包含的与调试相关的段(section)信息。
readelf -S process | grep -i debug
7.5 调试技巧
-
调试的本质:找到问题的代码。断点的本质:是把代码进行块级别划分,以块为单位进行快速定位区域。finish:确定问题是否在函数内。until:局部区域快速执行。
-
watch的使用:如果有一些变量不应该修改,但是你怀疑他修改导致了问题,你可以watch他,如果变化了,就会通知你。
-
set var确定问题的原因,并进行临时修改。
-
条件断点方便在循环中查找问题。