如何优♂雅地学习GDB调试(二)

简介: 本章我们将带着大家高雅的学一学令众多习惯图形化页面的朋友难受的 gdb 调试,这部分知识可以选择性学习学习,以后倘若遇到一些问题时能在 Linux 内简单调试,还是很香的。然后在讲讲 gcc 和 g++,系统讲解程序运行时的各个过程。

Ⅱ.  gcc 和 g++


0x00 引入:在 Linux 中如何编写 C/C++ 程式?

💭 以下是 C 和 C++ 的 Hello,World 示例程序编译的方式:

2ea0f28bf45adc55cd01611d7198ed01_4d9ebedf62e8425f9f3f26195ac1b49a.png



$ gcc [文件名]    # 编译C
$ g++ [文件名]    # 编译C++

此外,如果你输入"g++ test.cpp" 时显示并没有这样的指令,可以安装一下:


sudo yum install -y gcc-c++

我们知道,gcc 是不能用来编译 C++ 代码的,它只能用来编译 C 语言的代码:


$ gcc test.cpp    ❌ 
$ gcc test.c      ✅

0a029359a832f972513e6e9a512bf7d8_4373c62c748440499b389e871a6a06cd.png

(尝试用 gcc 编译 C++ 代码)


但是, g++ 是可以用来编译 C 语言的,这个相信大家是可以理解的,因为 C++ 是 C 的超集:


$ g++ test.c       ✅
$ g++ test.cpp     ✅

0x01 程序的翻译过程

程序(文本)要转换成机器语言(二进制),翻译有以下四个步骤:


① 预处理  

② 编译  

③ 汇编  

④ 链接

这个我们在前面已经讲过了,我们本节主要讲解预处理部分,并且对链接部分进行补充。


对于编译和汇编部分,本节我们只对他们做一个简单的讲解(当然并不是说它们不重要)


❓ 思考:我们知道文本要翻译成二进制的原因是计算机只认识二进制,那你有没有想过:


为什么计算机只认识二进制?


其实并不是计算机只认识二进制,而是计算机当中的各种硬件只认识二进制。


在计算机刚被设计的时候都是存储两派信息,常见的比如触发器这样的硬件设备,


实际上只能存储电信号的 "有无" 或者 "正负" 这样的概念,至于为什么选择二进制。


我再做一个补充,其实从计算机发明到现在,历史上也出现过其它进制的计算机。

f2da398f58b6eef065200de0eb631125_e4daa6dc9a41420e9e5a0b459450b59a.png


比如苏联的三进制计算机,只不过二进制计算机更简单,最后称为主流了。


所以,我们的计算机只认识二进制,因为它的各种硬件都是二进制的。


💡 答案:组成计算机的各种组件,只认识二进制。


0x01 预处理过程

📚 预处理:a. 宏替换     b. 头文件展开    c. 去注释    d. 条件编译


Linux 的 gcc 是如何进行上面的过程的呢?我们先看预处理的过程。


💬 我们来修改一下 test.c 源文件,让它有头文件、宏、注释和条件编译:

3cd9ca7ca0935b1f1dfe7b1bc3e41bcd_418e723c597d4cb6908cac83c9cf52a4.png



此时我们直接用 gcc 编译一下代码,"gcc + 要编译的文件" 默认生成的可执行程序叫 a.out。


顺便一提,如果你不想让生成的可执行程序为叫 a.out,你想指定名称,可以加上 -o 选项:

c614aedd0402a17a3e0abc4c8888303f_90a28b829d024e67b2c268ddafcf16a2.png



这样是直接一步到位地获得了可执行程序,可是我们现在想观察预处理,


也就是说我们只想让 gcc 完成预处理的操作,我们可以加上 "杠大易" 的选项。


gcc -E test.c -o test.i    
# -E: 从现在开始给我进行程序的翻译,当预处理完成就停下来
# -o test.i:将最后的结果写到 test.i 中

ef00ea71ea0b1de89b6168d69e665451_15b353fe3add4621a79606a76d28d08a.png

然后我们 gcc 打开 test.c,冒号进入底行模式后输入 vs test.i,就可以分屏观察:

8ea42b977cd6824585c3d1804d8ffcda_223d95f00b8e4654959d31a4fb7bbf3b.png



给人最明显的感受就是 —— test.i  明显要比 test.c 大很多。


但实际上观察后发现代码反而比 test.i 要少很多,并且我们翻到开头可以发现有大量的引入:

8a9b4699afae464d247710694c76cf08_30c3e928fc1042b2a0e90731604570fb.png



我们写代码写的 .h 文件 .c 文件,当你编译的时候实际上是把你的 .h 代码全部拷贝到 .c 中的。


为了防止重复包含头文件,我们在《树锯结构》专栏还提到了用 #pragma once 防止重复包含。


观察 test.o 前面部分后我们发现 Linux 中 C语言标准头文件在 /usr/include 下,我们这就去看看:


$ ls /usr/include

6e2a8cf88ed9379f048080486833b2c2_f207088acea946b1949747e874639276.png


一般都会安装在 usr/include 目录下,当然!不排除以后会出现安装在其他目录下的可能性。


现在你只需要记住:以前我们编写C语言代码时 #include<stdio.h> 时,其实并不是说 #include 就一定能成功,前提是你平台必须得装了你引入的头文件,不然也没东西在你的源文件中展开。


当然,我们最熟悉的头文件莫过于 stdio.h:

1fcc894361465c36542440e6d9eaa2a9_062b010b06464fa1bec9516bf07802f6.png



📌 注意:编译器内部都必须通过一定的方式,知道你所包含的头文件所在的路径。


现在在回头想一想,为什么一个新的语法老的编译器不支持的问题。其根本原因是因为老的编译器配套的头文件和库,压根就没有这一套东西。我们在装 VS 编译器的时所谓的环境安装勾选 C/C++ 后,实际上就是在给你装 C语言 C++ 的头文件和库。


🔍 观察:我们再来比对一下它们的代码部分有什么差别:

ba9e185cf71ddfaf1612df1065964a4f_0f9ad1aa4c4f4ef8acd77d5e8bf38a93.png



此时,就完成了预处理的工作。


我们刚才的条件编译只保留了 release,现在我们如果 #define 出一个 DEBUG,


再重新编译打开比对,保留的就不是 "hello,release!" 而是 "hello, debug!" 了,这里就不演示了。



❓ 思考:程序进行完预处理操作后,还是C语言吗?


💡 答案:仍然是C语言!你看这些代码你还认不认识,它当然还是C语言了。


换言之,这个预处理工作仅仅是为了让程序翻译的更顺畅,帮助我们做了一些文本替换处理操作而已。比如宏替换(当然,例子里面我不小心忘记了在代码中用到宏,所以自然前后也没有替换)、去掉给人读的注释(机器才懒得读你写的注释呢,跟它也没有半毛钱关系)、根据条件编译的结果把不要的选项去掉……最后它还是C语言,只不过时一份干净的C语言。你可以理解为 "文章的润色" 。


📚 命令格式:


gcc [选项] 要编译的文件 [选项] [目标文件]

预处理(进行宏替换):


预处理功能主要包括宏定义、文件包含、条件编译、去注释等。

预处理指令以 # 号开头的代码行。

实例:gcc -E hello.c -o hello.i

选项:"-E"  该选项的作用是让 gcc 在预处理结束过后停止编译过程。

选项:“-o"  是指目标文件, "i" 文件为已经过预处理的C初始程序。

0x03 编译过程

编译的核心工作就是将C语言翻译成汇编语言!


如果看不懂也没有关系,你只要知道 —— 这一步会让你的C语言代码大变样!


$ gcc -S test.i -o test.s

# -S: 从现在开始进行程序的编译,当我们编译完成之后,就停下来!

这次我们从 test.i 开始走,当然也可以是 test.c,那会重新走一遍预处理的过程然后再编译。


(汇编语言的后缀一般都叫 .s,所以我们这里取名为 .s,我们前面章节也说过 Linux 的类型和文件后缀没有关系,这里你用 .lbwnb 都没有人拦你,只是叫 .s 更符合常理)


输入完上面命令后就形成了 test.s 的文件:

ecfdd9cf9824a8bd4a203963903b09c2_50ea8c0d363d44af8a164aacf235fd37.png



之后用 vim 打开 test.s 之后会让人很头大:

3bc14fda7ff53bf0543e38665a480822_5d08bdb50b1742ca8d5152d70e4be1d6.png



这是 x86 环境下的汇编指令,其中有一些汇编的助记符,即使看不明白也没有关系。


但是你可以发现代码的数量 从刚才 test.i 的八百多行,变成了现在短短的 45 行!


0x04 汇编过程(简单了解)

该过程是将汇编语言翻译翻译成二进制文件。


准确来说应该是 "可重定位二进制文件",它一般以 .o 结尾,VS 下是 .obj 结尾的:

05afc9d484909b4ad2c5fedc3b8961ca_da55af88bce84e399a53d762d420db77.png



值得一提的是,这里的 "重定位" 和我们前面说的 "重定向" 是完全没有任何关系的,就像雷锋和雷峰塔、Java 和 Javascript 一样完全没有任何关系。


$ gcc -c test.s -o test.o
# -c:从现在开始进行程序的编译,当我们汇编结束之后,就停下来!


b10eed1b3a185ad879aa4f749aa2e35d_13a76372c8d845228dabab0d340de6d5.png


此时就已经是二进制了,gcc 打开后会是很大的一坨乱码。


我们可以用一些二进制查看工具去查看,但是我相信已经没有地球人可以看懂了。


刚才的汇编语言确实有人可以看懂,但这里我直接说没有人能直接看懂应该不过分吧:

789b2ceed7b3ee0a46efd11d729ed23a_28b8b9e9c57640da85771d5d5a7bdd94.png



$ ./test.o

虽然现在代码已经是二进制的了,但是仍然是不能运行的:

7c22ca773e1c05083ebc48d77aff1022_5bc3b7276ed34e0c957c5c781e7bf37a.png



其原因也很简单,因为这里面有些符号目前还没有和系统关联起来。


0x05 链接过程


所有的包含头文件的操作,本质是因为想使用头文件所声明的方法!

d4b4f26b345b28961417660057020e27_d49023133ee34a379f59b25af79e9242.png

$gcc test.o -o mytest

7c1ecd540ecc591238912df8a6794c43_a9b775920aa14618af9bc67553d45a42.png

而这最后一步,隐含的就是链接我们自己的程序和库,形成可执行程序!


当然了,直接到程序的翻译过程:


$ gcc test.c -o mytest.c

一步就可以到位了,我们之前是为了研究一步步的过程,所以又是 -E 又是 -S 又是 -c 的。


0x06 巧记程序的翻译过程

我们先来回故前三个翻译过程,按顺序分别为预处理、编译和汇编。


$ gcc -E test.c -o test.i     # 预处理
$ gcc -S test.i -o test.s     # 编译
$ gcc -e test.s -o test.o     # 汇编

这里有个记忆方法:预处理 (E) → 编译 (S)→ 汇编 (c) ,三个过程就是 。


如果你记不得,可以看看你键盘的左上角就行了(当然前提你要记住程序翻译过程的顺序)。


另外它们形成的临时文件为 test.i、test.s、test.o,也同样有个记忆的方法:


(国际标准化组织:ISO)


当然,iso 也是镜像文件的后缀,如果你比较熟悉也可以拿这个记。


ISO(Isolation)文件一般以iso为扩展名,是复制光盘上全部信息而形成的镜像文件


Ⅲ.  函数库(Function library)


0x00 引入:你早就与库有着千丝万缕的联系

我们的C程序中,并没有定义 printf 的函数实现,且在预编译中包含的 stdio.h 中也只有该函数的声明,而没有定义函数的实现。那么是在哪里实 printf 函数的呢?


系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径 /usr/lib 下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数 printf 了,而这也就是链接的作用。


 从我们敲下第一行代码,打印 Hello,World 时就已经和库有着千丝万缕的联系了。


看上去像是我们在写代码打印这个语句,实际上是调用了大佬写的 prinf 打印函数。


"其实我们无形中就已经站在了巨人的肩膀上"


你自己用的C语言头文件,从库中取的所有的函数,全部都需要在头文件当中声明,在库文件当中实现,然后在编译的时候再把你的可执行程序和库文件关联起来,此时关联之后,才能形成可执行程序。否则一切都是扯淡!


如果今天我们是在写 C++ 代码,C++ 对应的库也必须在安装 gcc 的时候都必须具备好,我们所说的装环境,实际上不仅仅在装环境。比如我们在装 VS2022 的时候我们不仅仅在装 VS2022,你也在装头文件也在装环境中所支持的语言。

d7ac89bc4ea47a69e8836501e84862e3_ffbf57e10f96408fa698c658b0b8d438.png



0x01 头文件与库文件(Header file and Library file)

头文件:给我们提供了可以使用的方法,所有的开发环境,具有语法提示,本质是通过头文件帮我们搜索的。


库文件:给我们提供了可以使用的方法的实现,以供链接,形成我们自己的可执行程序。


0x02 动态库与静态库(Dynamic library and static library)

我们必须承认一个事实,计算机存在两类库:一类库叫动态库,一类库叫静态库。


静态库:Linux (.so),Windows (.dll)    —— 动态链接


静态库:Linux (.a),Windows (,lib)      —— 静态链接


静态链接:将库中的相关代码,直接拷贝到自己的可执行程序中。


动态链接:


优点:大家共享一个库,可以节省资源。

缺点:一旦库丢失,会导致几乎所有的程序失效!

那 gcc 中如何体现呢?


形成的可执行程序体积一定是不一样的,静态链接体积大,动态链接体积小。


那么我们在 Linux 中用 gcc 编译程序


默认情况下形成的的可执行程序就是动态链接的:

e93c9e865a6170fbd69b315f8397508b_421e3f18c9414ddfa0e546ccbf63609a.png



如果你想进行静态链接,你需要在编译代码时在后面加上 -static 选项:


$ gcc test.c -o mytest2 -static      # 静态链接

0x03 静态库的安装

此时如果出现了像下面这样找不到的情况:

b79614b468dce75cc1e40d4295b935a3_3c0658031d6744c6b9fee0e877ad237f.png



那么你就需要安装一下静态库,记得切换到 root 下去安装。


🔧 安装 C 的静态库:


# sudo yum install -y glibc-static


🔧 安装 C++ 静态库:

f6bc80f2cb845d5c9d4d1e52f2345c0d_36ea5022328e462fa61d2ddeb5742206.png

# sudo yum install -y libstdc++-static

0x04 动态链接和静态链接推荐使用哪个?

默认是动态链接,我们也更推荐动态链接,


因为生成体积小,无论是编译时间还是占资源的成本,一般都比静态链接要好。


但这并不是绝对的!如果你要发布一款软件是动态链接的,程序短小精悍但库相对显得累赘,


如果此时你发布这款软件就不想带库了,你把它静态链接就是完全合适的。

716a27c3b4d6a3ec393f340f3b9f0888_5d89b943ccb4461990483a0a247a1fc4.png

07c03ae6d77b4b153f6d1ec710be7c14_7a80245f0b5f4021a033b3789a9efdeb.png



相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
2月前
|
NoSQL 编译器 C语言
C语言调试是开发中的重要技能,涵盖基本技巧如打印输出、断点调试和单步执行,以及使用GCC、GDB、Visual Studio和Eclipse CDT等工具。
C语言调试是开发中的重要技能,涵盖基本技巧如打印输出、断点调试和单步执行,以及使用GCC、GDB、Visual Studio和Eclipse CDT等工具。高级技巧包括内存检查、性能分析和符号调试。通过实践案例学习如何有效定位和解决问题,同时注意保持耐心、合理利用工具、记录过程并避免过度调试,以提高编程能力和开发效率。
53 1
|
5月前
|
NoSQL Linux C语言
Linux GDB 调试
Linux GDB 调试
73 10
|
5月前
|
NoSQL Linux C语言
嵌入式GDB调试Linux C程序或交叉编译(开发板)
【8月更文挑战第24天】本文档介绍了如何在嵌入式环境下使用GDB调试Linux C程序及进行交叉编译。调试步骤包括:编译程序时加入`-g`选项以生成调试信息;启动GDB并加载程序;设置断点;运行程序至断点;单步执行代码;查看变量值;继续执行或退出GDB。对于交叉编译,需安装对应架构的交叉编译工具链,配置编译环境,使用工具链编译程序,并将程序传输到开发板进行调试。过程中可能遇到工具链不匹配等问题,需针对性解决。
178 3
|
5月前
|
NoSQL
技术分享:如何使用GDB调试不带调试信息的可执行程序
【8月更文挑战第27天】在软件开发和调试过程中,我们有时会遇到需要调试没有调试信息的可执行程序的情况。这可能是由于程序在编译时没有加入调试信息,或者调试信息被剥离了。然而,即使面对这样的挑战,GDB(GNU Debugger)仍然提供了一些方法和技术来帮助我们进行调试。以下将详细介绍如何使用GDB调试不带调试信息的可执行程序。
152 0
|
7月前
|
NoSQL Linux C语言
Linux gdb调试的时候没有对应的c调试信息库怎么办?
Linux gdb调试的时候没有对应的c调试信息库怎么办?
57 1
|
7月前
|
NoSQL Linux C语言
Linux gdb调试的时候没有对应的c调试信息库怎么办?
Linux gdb调试的时候没有对应的c调试信息库怎么办?
40 0
|
7月前
|
NoSQL Linux C++
Linux C/C++ gdb调试正在运行的程序
Linux C/C++ gdb调试正在运行的程序
|
7月前
|
NoSQL Linux C++
Linux C/C++ gdb调试core文件
Linux C/C++ gdb调试core文件
|
7月前
|
NoSQL Linux C++
Linux C/C++ gdb调试
Linux C/C++ gdb调试
|
8月前
|
NoSQL Ubuntu 测试技术
【GDB自定义指令】core analyzer结合gdb的调试及自定义gdb指令详情
【GDB自定义指令】core analyzer结合gdb的调试及自定义gdb指令详情
104 1