UNIX 环境编程GCC 编译器 | Makefile 基础入门 | GDB 调试教学

Posted 柠檬叶子C

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UNIX 环境编程GCC 编译器 | Makefile 基础入门 | GDB 调试教学相关的知识,希望对你有一定的参考价值。

💭 写在前面:本文将介绍如何使用 GCC 编译器编译,并详细介绍了 Makefile 的基本构造、创建Makefile 文件以及 Makefile 变量,以提高编译效率。此外,本文还将探讨GDB调试器的使用,包括调试前的准备、readelf 读取 ELF 文件信息、显示代码、断点、调试、监视、跳转等内容。 

📜 本章目录:

Ⅰ. 使用 GCC 编译

0x00 gcc 的使用

0x01 GCC 选项

Ⅱ. 快速入门 Makefile

0x00 为什么需要 Makefile?

0x01 Makefile 的基本构造

0x02 创建 Makefile 文件

0x03 Makefile 和普通的编译过程的对比

0x04 Makefile 变量

Ⅲ.  GDB 调试

0x00 调试前的准备

0x01 Linux 默认集成环境

0x02 readelf 读取 ELF 文件信息

0x03 显示代码 gcb(list)  (List file)

0x04 断点

0x05 调试

0x06 监视

0x07 跳转(until & c & finish)

0x08 对于 gdb 的态度


Ⅰ. GCC 编译器

0x00 gcc 的使用

编译顺序:C 预处理阶段 →  C编译 → 汇编 (Assemble)  → 链接 (Linking)

gcc,即 GNU Compiler Collection,是一个编译器套件。

gcc 不仅可以编译 C 语言,还可以编译其他语言。

在 C 语言中生成可执行文件的过程如下:

  • 编译(.c -> .o):由 cc1 完成。
  • 链接(.o -> 可执行文件 a.out):由名为 ld 的链接器完成。

gcc 通过调用这些编译器和链接器来生成可执行文件。

输入 gcc -v 可以确认 gcc 版本:

$ gcc -v

❓ 那么如何使用 gcc 呢?很简单:

  • 创建一个 C 文件
  • 编译:gcc 文件名.c
  • 如果编译成功,则会自动生成一个名为 a.out 的可执行文件。
  • 如果想要查看结果,可以使用 ./a.out 命令来运行它。
    点表示当前目录,a.out 是 Unix 中默认的 C 编译器生成的二进制文件。
$ ./a.out

如果想要创建其他名称的可执行文件,则可以使用:

gcc -o 可执行文件名 源文件名.c 
gcc 源文件名.c -o 可执行文件

💭 例如:gcc -o mytest test.c,注意,-o 后面必须跟输出的文件名!

* 我们可以通过 man 手册去查询 gcc 更多的信息:

$ man gcc

0x01 GCC 选项

-v 选项:可以显示编译过程

$ gcc -v 源文件名.c

可以查看当前使用的 gcc 版本以及插入到源代码中的 stdio.h 所在的目录等信息。gcc编译器版本为 5.4.0,gcc 访问头文件(如stdio.h)的路径为 /usr/lib/gcc/x86 64-linux-gnu/5/include 等。

-D 选项:在外部定义预处理器宏

$ gcc -D test.c

-c 选项:仅编译选项(当需要编译多个源文件时需要)

$ gcc -c test.c

有以下有两种方法:

1. gcc –o main main.c fun1.c fun2.c

2. gcc –c main.c
	gcc –c fun1.c
	gcc –c fun2.c
	gcc –o main main.o fun1.o fun2.o

虽然第二种方法看起来更麻烦,但是当fun1的源代码被修改时,通过使用目标文件,我们可以只重新编译已更改的文件,然后将其与现有文件链接,而不进行内部低效的操作。

-I 选项:指定 #include 语句中头文件所在的目录。用于链接库文件的

如果使用 #include <stdio.h>,则以系统标准目录 /usr/include 为基础查找文件并包含它。 如果使用 #include "stdio.h",则以当前正在执行编译器的目录为基础查找头文件并包含它。 如果不是这两个目录,则明确指定为 -I<目录>。

Ⅱ. 快速入门 Makefile

0x00 为什么需要 Makefile?

❓ 我们为什么需要 Makefile?举个最简单的例子:

如果需要跑的工程,源文件有十几个甚至上百个呢?难不成我们要:

gcc –c main.c
	gcc –c fun1.c
	gcc –c fun2.c
	…
	gcc –c fun100.c
	gcc –o program main.o fun1.o fun2.o … fun100.o

键盘寿命疯狂 -1,CV C 到冒烟?不用担心!Makefile 解君愁!

0x01 Makefile 的基本构造

target ... : prerequisites ...
		recipe
		....

target (目标):文件的名称,如可执行文件,目标文件等等。最先出现的目标文件称为默认目标(通常是可执行文件) ,目标文件是 make 的最终目标。

prerequisites (先决条件):为了生成目标文件所需的材料。

recipe (配方):Make 执行的操作。当先决条件发生变化时,按照规则生成目标文件。只有需要更新的目标才会进行更新。

如果先决条件发生变化  按照规则生成目标文件 (只有需要更新的目标才会进行更新) 

0x02 创建 Makefile 文件

Makefile 是用于在 Linux 系统中简化重复编译过程的配置文件。

通过 Makefile 可以管理库和编译环境,我们推荐将文件命名为 Makefile。

CC = gcc
target1 : dependency1 dependency2
  command1
  command2

target2 : dependency3 dependency4 dependency5
  command3
  command4

* 注意:command 前必须要加一个 tab!

什么是 Makefile 中的宏定义?在 Makefile 中一个预定义的环境变量。

一个 Makefile 由 目标 (target) 、依赖关系 (Dependency) 和 命令 (command) 组成。

0x03 Makefile 和普通的编译过程的对比

Makefile 和一般的编译过程的区别在于:

Makefile 能够通过自动化针对每个文件的重复命令,节省时间。

能够让你快速掌握程序的依赖结构,易于管理,最大限度地减少了简单的重复性工作和重写工作。

我们来做个对比,如果使用普通编译过程:

$ gcc -c -o director.o director.c
$ gcc -c -o file.o file.c
$ gcc -c -o main.o main.c

如果使用 Makefile:

$ gcc -o exefile main.o director.o file.

🔨 动手尝试:vi Makefile 并输入以下内容

exefile2: file.o director.o main.o
	gcc -o exefile2    file.o  director.o  main.o
file.o: file.c
	gcc -c -o file.o file.c

director.o: director.c
	gcc -c -o director.o director.c

main.o: main.c
	gcc -c -o main.o main.c

clean:
	rm *.o exefile2   # 清除所有扩展名为 .o 的文件和名为 exefile2 的可执行文件。

* 使用 make clean 命令:清除所有 扩展名为 .o 的文件和名为 exefile2 的可执行文件。

0x04 Makefile 变量

你可以使用变量,简化你的 Makefile!编写起来会更简单:

cc 用于指定 C 语言编译器的名称,通常为 gcc,可以它来指定编译器:

cc = gcc   # 指定 C 编译器 gcc
cc = g++   # 指定 C++ 编译器 g++

target 设置 Makefile 最终要生成的目标文件

target=movie

也可以定义多个 target 生成多个目标文件,并为每个 target 指定依赖关系和构建规则。

object 用于指定所有的目标文件(object file),通常以“.o”为扩展名,使编写 prerequisites 和 recipe 更容易。

object=main.o file.o director.o

❓ 一些问题:

为什么没有 main.o film.o director.o 的 recipe?

make 会推导 recipe,因此没有必要写出每一个 recipe 来编译每个 c 文件。

提到了.o 文件,但没有为它制定规则?因为 make 使用了一个隐含的规则。

隐含规则:如果你需要创建或更新一个新的.o文件,找到一个同名的 .c 文件并使用 cc -c 命令。

需要 film.o -> film.o 不存在?-> 找到 film.c -> 执行:cc -c film.c -o film.o 

cc 的含义和 Linux 上的 gcc 相同:

$(objects) : film.h

即使头文件改变了,它也会创建一个新的对象文件。

.PHONY : clean :伪目标

把它看成是 recipe 的简单表示,而不是实际文件的名称。这里的 recipe:

rm $(target) $(objects)

删除目标文件和当前目录下的所有对象文件。

为什么使用 PHONY?为了避免意外情况!比如:如果你的一个文件叫 clean 怎么办?

【一个经常出现的错误】

Makefile:17: ***缺少分隔符。 停止。
在第一章中,我们强调了在编写Makefile时,命令部分应该以TAB字符开始。上面的错误是因为我们没有使用TAB字符,所以我们无法判断make是否是一个命令。
解决方法:在第17行(接近结尾处)将命令改为以TAB字符开始。
 make。*** 没有规则来制作目标 "io.h","read.o "需要。停止。
上述错误是由于依赖关系的问题:read.c被定义为依赖io.h,但io.h没有找到。
行动:调查依赖关系中定义的io.h是否真的存在。如果不存在,想想为什么不存在。你也可以尝试再次运行make dep来重新创建依赖关系。
 Makefile:10: ***命令在第一个目标前开始。 停止。
上面的错误是一个模糊的错误信息,说 "命令在第一个目标之前开始"。根据我的经验,造成这个错误的原因似乎是在使用'\\'标记多行长句时误用了'\\':'\\'部分应该是该行的最后一个字符,但如果你不小心在'\\'后面加了几个空格,你就不可避免地会出现上述错误。
解决方法:在第10行(接近结尾处),如果有一个'\\'字符,确保它是该行的最后一个字符,即删除'\\'字符之后的一切(主要是空格)。
 如果你运行make,你将不会得到你想要的可执行文件,而且它的行为会很奇怪。例如,它的行为将与make clean相同。
你必须记住,make并不是一个天才。当 make 读取 Makefile 的内容时,它认为它看到的第一个目标就是它应该产生的结果文件。 所以,如果你把 Makefile 的 clean 部分作为 Makefile 的第一个目标,你会得到上述结果。
解决方法:在例7.1中,我们创建了一个不必要的目标,叫做all。所以,要么把你想生成的结果文件作为第一个目标,要么像例7.1那样,创建一个像all一样的假目标。把 make clean 和 make dep 这样的东西放在 Makefile 的最后是安全的。
 重新编译一个先前编译过的文件而不去修正它。
这种行为是因为make不知道依赖关系,也就是你没有设置它们。结果,make认为它的工作是编译所有文件并创建一个可执行文件。
解决方法:你需要设置目标文件、源文件和头文件的依赖关系。说gccmakedep *.c会在Makefile的末尾自动创建依赖关系。对于所有其他文件,你需要适当地设置依赖关系。
main.o : main.c io.h read.o : read.c io.h write.o : write.c io.h 

使用gcc时可能出现的错误信息!

GCC 컴파일러 에러 메세지 리스트(Error Message List) :: Block Busting

Ⅲ.  GDB 调试

0x00 调试前的准备

程序在运行时展现程序内部发生了什么?出现 BUG 的时候便于我们知道在哪。

为了找到 BUG,gdb 做四种事:

① 程序运行时,各种条件设定后,程序开始。
② 遇到特定的条件时使程序暂停
③ 程序暂停时,检查发生了什么事
④ 在程序内部把 BUG 修改完后,继续运行找其他出现的 BUG

具有上面的特性的 GDB 是找到程序中 BUG 的利器!

运行 GDB 前编译方法及实行:

编译 (Compile) :

Debug 之前,编译将要 debug 的程序的 debugging 信息。这样就能使用 GDB 运行使用的变量和运行的函数了。举个例子:

gcc -g test.c -o test

运行 GDB:

GDB 运行程序名

为了在 GDB 中查看 C 的调试信息,需要使用 -g 选项编译 C 程序:

$ gcc -g test

要使用 GDB 调试编译后的 C 程序,按照以下步骤进行:

$ gdb program

此时就能看到用于输入 GDB 命令的提示符了:

(gdb)

我们先来创建一个用来演示 GCD 调试功能的目录: 

既然要调试,我们就必须要有个代码,我们这里写一个数字累加的代码:

🚩 运行结果:

结果是5050,没有问题。如果我们代码出现了一些问题需要我们调试,我们就可以使用 gdb。

如果此时你出现了报错,说什么不支持 for 循环里面定义变量,你可以输入:

$ gcc hello.c -o hello -std=c99

0x01 Linux 默认集成环境

在你当前的代码目录下直接执行 gcb + 形成的可执行程序:

$ gdb [可执行程序]

此时就进入了 gdb 的调试命令行中:

(如果想退出,直接按 quit 就可以退出了)

gcb 读取我们的 hello 程序时出现了 "没有调试符号被发现" 的警告:

这是什么意思呢?

我们的 gdb 中有一条命令叫做 list(简写成 l),但是我们输入后出现以下提示:

因为 —— 默认形成的可执行程序无法调试!!!

相信大家都知道,C语言的发布方式有两种,一种是 debug 一种是 release。

我们在 VS 里面可以直接调的原因是,VS2019 的默认集成环境是 debug。

而在我们的 Linux 中的默认集成环境是 release,换言之,

在我们 Linux 中如果你想发布一个程序,可以直接发布,无需加任何选项。

但是如果你 想调试,以 debug 形式发布,那么你就需要在编译时在后面添加一个 -g 选项

$ gcc hello.c -o hello.g _g

🔺 总结:Linux 默认形成的可执行程序是动态链接且是 release 方式发布。

  • 如果想静态链接,加 -static
  • 如果想动态链接,加 -g

0x02 readelf 读取 ELF 文件信息

release 和 debug 的区别:你的可执行程序里本来就有调试信息, 只是 debug 中才有。

首先,这两个版本也都是可以运行的:

并且我要告诉你的是:debug 版本比 release 版本多几千个字节,这是什么?

毫无疑问,这些就是一个可执行程序的调试信息,它在 debug 版本中有所显现。

📚 如果你想看调试信息,你可以输入:

$ readelf -S [可执行程序]     # 以段的形式读取可执行程序,用于显示读取ELF文件中信息

💭 我们先看看 release 版的:

💭 我们再来读一读 debug 版的:

因为 debug 版本是能给你的可执行程序添加调试信息的,所以体积自然比 release 版本要大些。

所以我们调试的得是 debug 版本的可执行程序,预备工作全部做好,下面我们来正式学习 gdb。

0x03 显示代码 gcb(list)  (List file)

现在我们是 debug 版本了,我们也顺理成章地能够使用前面我们说的 list 了。

(gdb) list [n]             # 显示代码,可带行号
(gdb) list [function]      # 显示带某函数的代码块
(gdb) list [begin, end]    # 显示区间内的代码
...

💭 操作演示:

一般在 VS 下调试的时候,除了让你看到代码,还会让你看到进行到了哪里,这里也是一样的。

你按下回车后,gdb 会自动记录你的上一条指令,直接按回车就是上一条命令:

(这么做就能把代码从第一行开始,将所有代码块逐个显示出来了)

0x04 断点

💭 假设我们想在下面代码的第15行处打个断点:

这要是放在 VS 下我们直接滑鼠选中对应行然后无脑按 F11 就行了。

而在 gdb 下打断点的方式是用 breakpoint:

(gdb) breakpoint [n]     # 在第n行处打一个断点

💭 操作演示:我们在代码第15行打个断点看看:

此时如果你想查看断点,可以使用 info 查看你打的断点:

(gdb) info b        # 查看断点

💭 操作演示:查看断点信息

 

我们再在第17行新增一个断点,此时我们就能看到两个断点了:

如果想要删除断点,在 VS 下我们直接再点以下小红点就搞定了:

 (图形化界面无疑是成功的)

但是在 gdb 中,我们需要知道要删除的断点的编号:

(gdb) d [Num]        # 删除Num号断点

💭 操作演示:删除1号断点(记不得要删除的断点的编号可以 info b)

此时 1 号断点已被成功删除,再次删除则会显示已经没有这个断点:

0x05 调试

准备开始调试,记得把刚才删除的断点再打回去,调试的指令如下:

(gdb) run           # 开始调试

💭 操作演示:输入完 r 按回车开始调试:

(此时就悬停在了第15行)

如果我们把场上断点全部干掉了,此时按 r 调试程序就会直接跑完:

  (这和 VS 也是一样的)

如果你想查看变量的内容,我们可以使用 print 命令:

(gdb) print [val]

💭 操作演示:查看变量内容

💭 操作演示:逐语句

如果想逐语句执行(逐语句即一步一步往后走),逐语句指令如下:

(gdb) step         # 逐语句

我们 s 两次后,此时走到了函数的调用处。此时如果你不想进入该函数,就不要再按 s 逐语句了。

此时我们应该逐过程执行,我们可以使用 next 命令:

(gdb) next         # 逐过程

💭 操作演示:逐过程

0x06 监视

我们在 VS 中调试代码的时候,有时候要 细 ♂ 细 ♂ 观 ♂ 察 某个变量时,我们会打开监视窗口:

在 gdb 下我们就可以使用 display 常显示来监视:

$ display [val]             # 监视一个变量,每次停下来都显示它的值
$ display [v1, v2, v3...]   # 同时添加多个变量,用括号隔开

 💭 操作演示:常显示 i 变量

当然,我们也可以同时监视多个值:

  (同时常显示三个变量)

直接输入 display 可以查看监视列表:

$ display      # 查看当前监视列表

 💭 操作演示:查看监视列表

如果想把某个变量从监视窗口移除,我们可以使用 undisplay:

$ undisplay [n]      # 删除n号变量

 💭 操作演示:删除3号变量

0x07 跳转(until & c & finish)

我们调试的时候在文本特别大的时候我们有时候会跳转,VS 里我们可以直接拖动箭头跳转。

gdb 调试下我们可以使用 until 指令跳转到指定行:

$ until [n]      # 跳转到指定行

 💭 操作演示:跳转到20行

如果想从一个断点跳转至另一个断点,我们可以使用 c:

$ c       # 直接跳转到另一个断点

有时候难免手贱不小心进了不想进入了函数,就比如不小心逐语句进了 printf 函数。

这个在 VS 下逐语句是不会进去的,但是在 Linux 下会进入,此时如果你反悔了象出来,

就可以输入 finish,它可以做到直接执行完成一个函数就停下来。

$ finish      # 执行到当前函数返回,然后停下来等待命令

0x08 对于 gdb 的态度

掌握上面单独介绍的 b、d、l、s、n、display、until、r、c、finish 其实就差不多了。

还有一些 gdb 的指令我们上面没有介绍,这里做一个整合:

  • list/l 行号:显示binFile源代码,接着上次的位置往下列,每次列10行。
  • list/l 函数名:列出某个函数的源代码。
  • r 或 run:运行程序。
  • n 或 next:单条执行。
  • s或step:进入函数调用。
  • break(b) 行号:在某一行设置断点。
  • break 函数名:在某个函数开头设置断点。
  • info break :查看断点信息。
  • finish:执行到当前函数返回,然后挺下来等待命令。
  • print(p):打印表达式的值,通过表达式可以修改变量的值或者调用函数。
  • p 变量:打印变量值。
  • set var:修改变量的值。
  • continue(或c):从当前位置开始连续而非单步执行程序。
  • run(或r):从开始连续而非单步执行程序。
  • delete breakpoints:删除所有断点。
  • delete breakpoints n:删除序号为n的断点。
  • disable breakpoints:禁用断点。
  • enable breakpoints:启用断点。
  • info(或i) breakpoints:参看当前设置了哪些断点。
  • display 变量名:跟踪查看一个变量,每次停下来都显示它的值。
  • undisplay:取消对先前设置的那些变量的跟踪。
  • until X行号:跳至X行。
  • breaktrace(或bt):查看各级函数调用及参数。
  • info(i) locals:查看当前栈帧局部变量的值。
  • quit:退出gdb。

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2023.3.21
❌ [ 勘误 ]   /* 暂无 */
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

GCC 컴파일러 에러 메세지 리스트(Error Message List) :: Block Busting

第一篇:《UNIX 网络编程 第二版》编译环境的搭建

第一步:搭建基本的编译环境

  安装gcc, g++, bulid-essential等编译软件

第二步:下载本书示例源码包

第三步:解压下载到的包并放在用户主目录中

第四步:进入包内并执行以下命令

1 sudo chmod u+x configure
2 ./configure

第五步:进入包内lib子目录下执行make命令

第六步:进入包内libfree子目录下执行make命令

第七步:进入包内libgai子目录下执行make命令

第八步:执行以下命令,将前面生成的libunp.a库复制到/usr/lib和/usr/lib64中

1 sudo cp ~/unpv13e/libunp.a /usr/lib/
2 sudo cp ~/unpv13e/libunp.a /usr/lib64/

第九步:为了以后包含头文件方便,修改包内子目录lib中的unp.h并将它和config.h拷贝到/usr/include中

1 gedit ~/unpv13e/lib/unp.h    
2 #将其中的 #include "../config.h" 修改为 #include "config.h"  
3 sudo cp ~/unpv13e/lib/unp.h /usr/include/
4 sudo cp ~/unpv13e/config.h /usr/include

第十步:进入intro子目录,编译一个示例代码测试一下

1 gcc daytimetcpcli.c -o 1 -lunp    # 别漏了后面的连接库参数

 

以上是关于UNIX 环境编程GCC 编译器 | Makefile 基础入门 | GDB 调试教学的主要内容,如果未能解决你的问题,请参考以下文章

源码包安装

CentOS 7如何安装 libbsd-dev && 编译apue.3e时出错处理(以便使用Unix环境高级编程中的apue.h库)

GccMingWCygwinMsys简介

c 各种编译器(gcc clang)

《unix环境高级编程·第三版》源代码编译及使用

GCC基本简介