【Linux】Linux 编译器与调试器 -- gcc/g++/gdb 的使用

简介: 【Linux】Linux 编译器与调试器 -- gcc/g++/gdb 的使用

一、Linux编译器 - gcc/g++ 使用

1、程序运行的四个阶段

在C语言 程序员内功心法之程序环境和预处理 博文中,我们就学习到 – 一个程序要被运行起来需要经历四个阶段:预处理 (预编译)、编译、汇编、链接,下面我们来简单回顾一下这四个阶段会进行的操作。

1.1 预处理

预处理也叫预编译,程序在预处理阶段会完成如下操作:

  • 注释的删除。
  • #define 定义的符号、宏的替换以及删除。
  • 条件编译的执行。
  • 头文件的包含:将头文件中的代码拷贝到当前代码中来。

在Linux下我们可以通过如下命令来得到预处理之后的代码:

gcc -E test.c -o test.i
# gcc:表示用 gcc 编译器来编译此代码
# -E:表示让代码在完成预处理后停下来,不再继续往后编译
# test.c:我们要编译的代码
# test.i 预处理产生的文件一般以.i为后缀
# -o test.i:用于指明临时文件的名称(test.i),它会将预处理之后的代码保存到指明的临时文件中,而不是直接打印到终端上
//测试案例 -- test.c
#include <stdio.h>
#define M 1024
int main()
{
    //测试注释
    //printf("hello 1\n");
    //printf("hello 2\n");
    //printf("hello 3\n");
    printf("hello 4\n");
    printf("hello 5\n");
    printf("hello 6\n");
    printf("hello 7\n");
    //测试宏    
    printf("%d\n", M);
    //测试条件编译    
#ifdef SHOW                                                                                                                       
    printf("hello SHOW\n");    
#else    
    printf("hello DEFAULT\n");    
#endif    
    return 0;    
}    

2020062310470442.png

20200623104134875.png

可以看到,预处理后 stdio.h 里面的内容会被拷贝到 test.i 里面,所以 test.i 一共有800多行;同时,预处理阶段也完成了我们上述提到的其他工作。

1.2 编译

程序在编译阶段会完成如下操作:

  • 语法分析。
  • 词法分析。
  • 语义分析。
  • 符号汇总。

在Linux下我们可以通过如下命令来得到编译之后的代码:

gcc -S test.i -o test.s      
# -S:表示让代码在完成编译后停下来,不再继续往后编译
# 编译产生的文件一般以.s为后缀

2020062310470442.png

20200623104134875.png

可以看到,编译阶段会将高级语言转换为汇编语言。

1.3 汇编

汇编阶段是把编译阶段生成汇编代码转成计算机可以识别的二进制目标代码,其中生成的 .o 文件被称为可重定向二进制目标文件。

在Linux下我们可以通过如下命令来得到编译之后的代码:

gcc -c test.s -o test.o  
# -c:表示让代码在完成编译后停下来,不再继续往后编译
# 汇编产生的文件一般以.o为后缀

2020062310470442.png

20200623104134875.png

20200623104650275.png

如上,汇编得到的二进制目标文件使用一般的文本编辑器打开时是一堆我们看不懂的符号 (与符号的编码有关 – utf-8),我们可以使用 od 指令以指定格式来打开它 (默认是以八进制打开)。

1.4 链接

程序在链接阶段会完成如下操作:

合并段表:编译器会把在汇编阶段生成的多个目标文件中相同格式的数据合并在一起,最终形成一个 .exe 文件。

符号表的合并和重定位:符号表的合并是指编译器会把在汇编阶段生成的多个符号表合并为一个符号表;重定位则是指当同一个符号出现在两个符号表中时,编译器会选取其中和有效地址相关的那一个,舍弃另外一个。

在Linux中,链接我们直接使用 gcc 即可,没有额外选项,因为链接是程序的最后一个阶段;同时,链接的结果默认存放在 a.out 中。

gcc test.o -o test.out

2020062310470442.png

20200623104134875.png

链接得到的文件被称为可执行程序,它里面存放的也是计算机能够识别的二进制指令。

2020062310470442.png

注:gcc 预处理编译链接三个阶段对应的选项和文件后缀有一个记忆技巧 – ESc 与 iso,其中 ESc 分别代表 -E -S -c,iso 分别代表 .i,.s,.o;ESc 可以对比电脑上的 [Esc] 键,将其中的 s 改为大写即可;iso 可以对比苹果 ios,将 o 和 s 的顺序调换即可。


同时,我们此处将 gcc 编译代码分为预处理、编译、汇编、链接四个阶段是为了让大家更深层次的理解一个程序的运行过程;日常编译代码的时候直接使用 “gcc test.c -o test.out” 或 “gcc test.c” 编译源文件得到可执行程序即可。

2020062310470442.png

2、链接方式与函数库

2.1 动态链接与静态链接

我们在编写代码的时候,除了自己实现函数之外,我们还会去调用函数库中的代码,比如 scanf/printf/malloc/fopen;但是我们要明白,库中的代码是别人给我们写好供我们直接使用的,即我们只有该函数的调用,而没有函数的实现;


同时,程序在预处理、编译和汇编阶段处理的都是我们自己编写的代码,只有在链接的时候,库函数的实现才会和我们的代码关联起来 (符号表的重定位);所以,链接的本质是我们在调用库函数时如何与标准库相关联的问题。


程序一共有两种链接方式:动态链接与静态链接;动态链接是指执行代码时,如果遇到库函数调用就跳转到动态库中对应函数的定义处,然后执行该函数,执行完毕后再跳转回原程序并继续往下执行;它的优点是形成的可执行程序小,缺点是受到动态库变动 (删除、升级等) 的影响。


静态链接则是直接将本程序内部要使用的库函数从对应的静态库中拷贝一份过来;它的优点是不与静态库产生关联,即不受静态库变动 (删除、升级等) 的影响;缺点是形成的可执行程序非常大。

2.2 动态库与静态库

函数库是一些事先写好的,用于给别人复用的函数的集合,函数库一般分为静态库和动态库两种:

静态库是指编译链接时,把需要的库文件代码全部拷贝到可执行文件中,因此生成的文件非常大,但在运行时也就不再需要库文件了,在Linux下其后缀名为 “.a”,在Windows下其后缀名为 “.lib”;


动态库也被称为共享库,它与静态库相反,在编译链接时并没有把相应的库文件代码加入到可执行文件中,而是在程序执行时由运行时链接文件来加载库,这样可以节省系统的开销,在Linux下其后缀名为 “.so”,在Windows下其后缀名为 “.dll”;


注:动态链接必须使用动态库,静态链接必须使用静态库;即进行动态链接时只能跳转到动态库中对应函数的实现处,进行静态链接时只能拷贝静态库中的函数。

Linux中默认使用动态库进行动态链接,原因如下:


    程序形成的可执行程序大不仅仅是占用的磁盘空间大,其被运行时加载到内存所占用的内存空间也会非常大,而目前我们使用的机器的内存基本上都是8/16GB的,所以运行不了过大的可执行程序;

虽然动态链接受函数库变动的影响,但是函数库一般很少会变动,即使变动也必须兼容以前的版本,所以影响不大;

在Linux下,我们可以使用 “file” 指令来辨识文件类型,使用 “ldd” 来打印或者查看程序运行所需的共享库。

2020062310470442.png

Linux 一般都会自动安装C语言动态库,因为Linux下的大多数指令以及我们默认使用 gcc 编译得到的可执行程序都是进行动态链接,依赖C动态库的;但是C静态库、C++静态库可能就需要我们自己安装了。

2020062310470442.png

我们可以使用如下命令来安装C和C++静态库:

sudo yum install -y glibc-static
sudo yum install -y libstdc++-static

2020062310470442.png

我们也可以使用 “-static” 选项来指定程序使用静态方式来进行链接:

2020062310470442.png

20200623104134875.png

可以看到,以静态链接方式形成的可执行程序比动态链接形成的要大100~200倍,即一个动态链接只有100M的文件,静态链接就会变成十几个G,二者之间相差非常大。

3、gcc/g++ 的使用

其实上面在学习预处理、编译、汇编、链接时我们就已经在使用 gcc/g++ 了,只是比较零散,下面我们来系统的学习一下 gcc/g++。

gcc/g++ 的安装

在Linux下,我们可以使用如下指令来安装 gcc 与 g++:

sudo yum install -y gcc
yum install -y gcc-c++ libstdc++-devel

2020062310470442.png

gcc/g++ 的使用

gcc 和 g++ 的使用方法非常类似,因为他们的选项基本都是一样的:

-E 在完成预处理后停下来,不再继续往后编译,生成的文件以 .i 为后缀;

-S 在完成编译后停下来,不再继续往后编译,生成的文件以 .s 为后缀;

-c 在完成汇编后停下来,不再继续往后编译,生成的文件以 .o 为后缀;

-o 将输出到终端的内容保存到指定文件中;

-static 对生成的文件采用静态链接;

-g 以debug方式发布软件,即生成调试信息,GNU 调试器可利用该信息;

-shared 尽量使用动态库,需要系统存在动态库;

-O0 -O1 -O2 -O3 编译器优化选项的4个级别:

-O0 不做任何优化,这是默认的编译选项;

-O1 优化会消耗少多的编译时间,它主要对代码的分支,常量以及表达式等进行优化;

-O2 会尝试更多的寄存器级的优化以及指令级的优化,它会在编译期间占用更多的内存和编译时间;

-O3 在O2的基础上进行更多的优化;

-w 不生成任何警告信息;

-Wall 生成所有警告信息;

2020062310470442.png

二、Linux调试器 - gdb 使用

1、debug 和 release

在Windows中使用VS的时候我们知道:程序的发布方式一共有两种 – debug 模式和 release 模式;其中 debug 模式是给程序员用的,其中包含调试信息,程序员可以根据这些调试信息对程序进行修改与完善;而 release 模式则是给用户用的,它不包含调试信息,因为用户不负责也不关心如何对程序进行调试。

Linux 中使用 gcc/g++ 编译链接得到的程序默认是 release 模式的,如果我们要使用 gdb 进行调试,必须在源代码生成二进制程序的时候添加 -g 选项;

//测试代码
#include <stdio.h>
int Add(int begin, int end)
{
    int ret  = 0;
    int i = 0;
    for(i=begin; i<=end; ++i)
    {
        ret += i;
    }
    return ret;
}
int Fac(int x)
{
    int ret = 1;
    int i = 0;
    for(i=1; i<=x; ++i)
    {
        ret *= i;
    }
    return ret;
}
int main()                                                                                                                                                                                  
{
    int a = 0;
    int b = 100;
    int add = Add(a, b);
    printf("ret = %d\n", add);
    printf("ret = %d\n", add);
    printf("ret = %d\n", add);
    int fac = Fac(10);
    printf("fac = %d\n", fac);
    printf("fac = %d\n", fac);
    printf("fac = %d\n", fac);
    return 0;
}

2020062310470442.png

20200623104134875.png

可以看到,以 debug 和 release 模式发布的程序无论是在程序大小、程序内部包含的有关调试的二进制信息,还是 gdb 模式下是否具有调试样例都是有明显区别的。

2、gdb 的使用

当我们指定 -g 得到以 debug 模式发布的可执行程序后,我们就可以使用 gdb 对其进行调试了;

gdb 的安装

在Linux下,我们可以使用如下命令来安装 gdb:

sudo yum install -y gdb

2020062310470442.png

gdb 调试的常见选项如下:

l 行号:从指定的行号开始往下显示源代码,每次显示10行 (l – list);(注:gdb 有自动记忆命令的功能,即当我们第一次使用 l 显示源代码后,我们下一次再使用 l 或者下一次按下 enter 键时,它会接着上次的位置往下显示)

l 函数:列出某个函数的源代码 (l – list);

r:运行程序 (r – run);

b 行号:在某一行打一个断点,相当于VS中的F9 (b – breakpoint);

info b:查看断点;

d 断点编号:删除断点 (d – delete);(注:每个断点都有自己的编号,我们删除断点时需要指明对应的断点编号)

r:调试运行,如果程序中有断点,则在断点处停下来,如果没有,则直接将程序跑完,相当于VS中的F5 (r – run);

n:逐过程调试,相当于VS中的F10 (n – next);

s:逐语句调试,相当于VS中的F11 (s – step);

c:运行至下一个断点处停下 (c – continue);(注:如果断点所在行不是一条语句,比如 “{” “}” 或者 空行,那么它会继续往下到有效行处停下 )

bt:查看调用堆栈 (breaktrace);

p 变量:查看变量值 (p – print);

display/undisplay 变量:跟踪查看一个变量,每次停下来都显示它的值,undisplay 取消对先前设置的那些变量的跟踪;

finish:把当前函数运行完;

disable breakpoints:禁用断点;

enable breakpoints:启用断点;

quit:退出 gdb;

指令演示

l 行号显示源代码:

2020062310470442.png

l 函数显示该函数的源代码:

2020062310470442.png

r 运行程序:

2020062310470442.png

b 行号打断点,info b 查看断点,d 断点编号 删除断点:

2020062310470442.png

r 调试运行:

20200623104134875.png

n 逐过程调试,s 逐语句调试:

2020062310470442.png

c 运行至下一个断点处停下:

2020062310470442.png

bt 查看调用堆栈:

2020062310470442.png

p 变量 查看变量值,display 跟踪查看变量,undisplay 取消跟踪:

2020062310470442.png

finish 把当前函数运行完,q 退出 gdb:

2020062310470442.png

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
7天前
|
NoSQL 编译器 Linux
【Linux】--- Linux编译器-gcc/g++、调试器-gdb、项目自动化构建工具-make/Makefile 使用
【Linux】--- Linux编译器-gcc/g++、调试器-gdb、项目自动化构建工具-make/Makefile 使用
14 0
|
10天前
|
NoSQL Linux C语言
Linux gdb调试的时候没有对应的c调试信息库怎么办?
Linux gdb调试的时候没有对应的c调试信息库怎么办?
14 1
|
11天前
|
NoSQL Linux C语言
Linux gdb调试的时候没有对应的c调试信息库怎么办?
Linux gdb调试的时候没有对应的c调试信息库怎么办?
13 0
|
程序员 编译器 Linux
linux gdb-多线程调试
相信使用C/C++语言开发软件的程序猿们都经历过‘栈溢出’的问题。‘栈溢出’问题通常十分的隐蔽,有的时候问题复现也十分的困难。每当软件出现莫名其妙的问题时,总是有人怀疑是不是栈溢出了,但是问题的排查又十分的困难,所以,‘栈溢出’就是广大C/C++开发者的噩梦。
126 0
|
NoSQL IDE Linux
linux gdb-多线程调试
linux下应用程序的调试工具主要就是gdb,可能你已经习惯了IDE形式的调试工具。也许刚开始使用gdb作为调试工具,会有诸多的不变,但是一旦你学会了如何使用gdb你就会被其富有魔力的功能所吸引的,下面开始逐步的学习linux下gdb的使用方式。
376 0
|
1天前
|
Linux 数据处理 调度
深入探索Linux的renice命令:优化进程优先级
`renice`命令在Linux中用于调整运行进程的优先级,范围-20(最高)至19(最低)。它可以实时改变进程的优先级,无需重启,支持按PID、进程组或用户批量修改。例如,`renice -n 10 -p 1234`将PID为1234的进程优先级设为10。使用时要注意不要滥用高优先级,避免频繁修改,了解系统负载,并记录变更,通常需root权限。
|
1天前
|
Linux 数据处理 Perl
深入探索Linux的rename命令:文件重命名利器
**Linux的`rename`命令是批量重命名文件的利器,基于正则表达式或Perl,支持预览、交互式确认及多种操作模式。常用示例:用`s/ /_/g`替换空格为下划线,`s/$/.txt/`添加扩展名,`s/\..*//`删除扩展名。使用时注意备份、测试命令、权限和正则表达式知识。**
|
1天前
|
Linux Shell
Linux中的realpath命令:深入解析与实用指南
**Linux的`realpath`命令详解** `realpath`用于获取文件或目录的规范化绝对路径,解析相对路径、符号链接及冗余元素。它接受路径输入,返回最短、唯一的绝对路径。支持 `-e`(确保路径存在)、`-m`(允许缺失组件)、`-s`(删除多余斜杠)和`-q`(静默模式)等参数。在脚本中使用能确保路径一致性,但需注意权限和路径检查。了解`pwd`、`find`和`readlink`等命令的用法也有助于选择合适的路径处理工具。
|
1天前
|
缓存 Linux 虚拟化
Linux下top命令指标说明
Linux下top命令指标说明
8 0
|
2天前
|
Linux
Linux的top命令是什么,如何使用
【6月更文挑战第30天】Linux的top命令是什么,如何使用
6 1