使用 Linux ,不可避免的需要使用 GCC 套件,从原本的只能编译C语言,到现在的系列的套件,而今天在这里主要讨论的是:GCC 是如何一步步的将代码编译成可执行文件的,以及如何使用 GDB 进行调试。
一、GCC是什么
GCC是什么,GCC是一个软件,GNU编译器套件(GNU Compiler Collection),最初由GNU组织为C语言开发,之后发展为一个套件。
二、GCC编译过程
1、预处理
预处理的过程是编译器去将头文件进行包含,将宏定义进行替换,在这个过程中,inlude和define命令生效。
预处理过程中的常用知识点有:
- 头文件未包含,<> 和 “” 的使用范围是不一样的,对于用户自定义的头文件,需要用 “” 指出路径。 可以使用 -I 命令指定头文件的目录路径
宏的重定义,因为不同头文件可能会重复引用同样的宏,会出现类似的问题,所以,使用
1234…来避免重复定义宏,这种写法同样可以用来进行条件编译:
-D XXX
。include与define不是关键字,他们是GCC可以识别的标识码。
- 宏定义中可以进行函数替换,但是必须是在同一行,多行函数一般使用
do{ … }while(0)
。 常见的预定义宏
- FILE :代表当前源文件的文件名
- LINE :代表展开成当前代码行的行号,是编译器内建的特殊宏定义
- FUNCTION : 函数名
# 和 ## 是预处理运算符
写法 | 含义 |
---|---|
#define ABC(x) #x | 字符串化 |
#define ABC(x) day##x | 字符连接 |
最后的问题,在预处理的过程中,头文件中的定义是如何被包含在文件中的。
2. 编译
编译是将C语言程序编译成汇编文件的一个过程。
编译的过程主要经过了六步:扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
在编译过程中可能出现的问题有:
- 语法错误,根据错误提示进行修改代码
- 优化错误,在嵌入式系统中会出现编译器的过度优化,有时候需要设定优化选项
3. 汇编
汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。
4. 链接
通过调用链接器ld来链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件。链接的过程主要分为:地址和空间分配(Address and Storage Allocation),符号决议(Symbol Resolution),重定位(Relocation)等。
链接分为静态链接和动态链接:
- 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。
- 编译静态链接库
- 生成目标文件 .o
- ar crv [.a] [.o]
- 调用静态链接库
gcc -o [file] [file.c] -L. [file.a]
- 动态链接是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。
- 首先、在任何给定的文件系统中,对于一个库只有一个.so文件
- 第二、所有引用该库德可执行目标文件共享这个.so文件中的代码和数据
- 第三、在存储器中,一个共享库的.text节只有一个副本可以被不同的正在运行的进程共享.
- 编译动态链接库
- 生成位置无关的目标代码 gcc-fPIC -c [*.c]
- gcc -shared -o [.so] [.o]
- 调用动态链接库(与静态链接库类似)
- gcc -o [file] [file.c] -L. [file.so]
- 编译动态链接库
链接过程详解
目标文件
目标文件分为三种:
可重定位的目标文件:
包含二进制代码和数据,其形式可以再编译时与其他可定位目标文件合并起来,创建一个可执行目标文件。可执行目标文件:
包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行.共享目标文件:
一种特殊的可重定位目标文件,可以再加载或运行时,被动态地夹在到存储器并执行.
编译器和汇编器生成可重定位目标文件(包括共享目标文件),链接器生成可执行目标文件.
可重定位的目标文件的结构如下:
标识 | 作用 |
---|---|
.text: | 已编译程序的机器代码 |
.rodata: | 只读数据 |
.data: | 已初始化的全局C变量 |
.bss: | 未初始化的全局C变量.在目标文件中这个节不占实际空间,仅是一个占位符. |
.sysmtab: | 一个符号表,存放在程序中被定义和引用的函数和全局变量的信息. |
.rel.text: | 当链接器把这个目标文件和其他文件结合时,.text节中的许多位置都需要修改.一般而言,任何调用外部函数或者引用全局变量的指令都要修改.另一个方面,调用本地函数的指令则不需要修改. |
.rel.data: | 被模块定义或引用的任何全局变量的信息. |
.debug: | 一个调试符号表 |
.line: | 原始C源程序中的行号和.text节中机器指令之间的映射. |
.strtab: | 一个字符串表,其中内容包括.symtab和.debug节中的符号表,以及节头部中的节名字. |
一个m文件的符号表有三种不同的符号
类型 | 作用 |
---|---|
由m定义并能被其他模块引用的全局符号. | 1.m定义的非静态的C函数 2.m定义的不带static属性的全局变量. |
由其他模块定义并被模块m引用的全局符号.这些符号成为外部符号, | 在其他模块中的C函数和变量. |
只被模块m定义和引用的本地符号. | 由m定义的带static属性的C函数和全局变量 |
符号解析
将每个引用和它输入的可重定位目标文件按的符号表中的一个确定的符号定义联系起来.
局部和static变量等:
对于那些和引用定义在相同模块的本地符号的引用,符号解析式非常简单明了的.编译器只允许每个模块中的每个本地符号只有一个定义.编译器还确保静态本地变量,它们会有本地链接器符号,拥有唯一的名字.外部引用:对于全局符号的引用解析,当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,它会假设该符号式在其他某个模块中定义的,生成一个链接器符号表表目,并把它交给链接器处理.如果链接器在它的任何输入模块中都找不到这个被引用的符号,它就输出一条错误信息并终止.
- 在编译时,编译器输出的每个全局符号给汇编器,或者是强,或者是弱,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表中.函数和以初始化的全局变量是强符号,未初始化的全局变量是弱符号.
编译系统提供一种机制,将所有相关的目标模块打包为一个单独的文件,称为静态库,它可以用做链接器的输入.当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序引用的目标模块。
输入命令时要考虑到,静态库和目标文件的位置,库文件放在目标文件的后面,如果库文件之间有引用关系,则被引用的库放在后面。
链接过程常见错误:
- 原材料不够
undefined reference to fun
寻找标签是否实现了,链接时是否加入一起链接 - 原材料重复
multiple definition of fun
多次实现了标签,只保留一个标签实现
5. GCC的主要选项
选项 | 含义 |
---|---|
1. 编译过程 | |
预处理 -E | 不经过编译预处理程序的输出而输送至标准输出 |
编译 -S | 要求编译程序生成来自源代码的汇编程序输出 |
汇编 -c | 通知GCC取消链接步骤,即编译源码并在最后生成目标文件 |
链接 -o | 生成可执行文件 |
调试 -g | 包含调试信息 |
头文件 -I | 包含指定路径的头文件 |
2. 警告选项 | |
-v | 启动所有警报 |
-Wall | 在发生警报时取消编译操作,即将警报看作是错误 |
-w | 禁止所有的报警 |
3. 库文件 | |
-static | 静态编译 |
-shared | 1、生成动态库文件 2、进行动态编译 |
-L dir | 库文件搜索中添加路径 |
-fPIC | 生成使用相对位置无关的目标代码(Position Independent Code)然后通常用于使用gcc的-static选项从该PIC目标文件生成动态库文件 |
4. 特定功能 | |
-D | 定义指定的宏,使它能够通过源码中的#ifdef进行检验; |
-O、 | -O2、-O3 将优化状态打开,该选项不能与-g选项联合使用; |
三、GDB的使用
GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。一般来说,GDB主要帮忙你完成下面四个方面的功能:
- 启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
- 可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
- 当程序被停住时,可以检查此时你的程序中所发生的事。
- 动态的改变你程序的执行环境。
1. 启动调试
首先,要进行调试,需要在生成可执行文件的时候使用gcc -g
选项:
-g[level] 默认为2
- 0 表示不生成任何调试信息
- 1 表示生成最少的调试信息,不提供局部变量及源代码行列等信息。
- 2 标准模式
- 3 较2而言,包含了宏定义等额外信息
然后有两种方式启动调试
方式一: 直接在命令行下输入gdb ./[filename]
方式二:运行gdb后输入file ./[filename]
2. 设置断点
除了断点外还有Watchpoints(观测点)及Catchpoints (异常捕捉点)
设置断点
输入b或break加上断点位置或断点函数名,如
b main 在main函数入口设置断点
b text2bin.c:50 在源代码第50行设置断点
info breakpoints 查看断点信息
clear 清除所有断点
clear text2bin.c:50 清除特定的断点
clear main 清除特定函数断点
disable及enable 开关某个特定的断点
b +5 及b -5 表示在当前行的后五行及前五行位置设置断点
对于观测点(Watchpoints),是指在某个条件下触发的断点,如text2bin中77行:Buffer2[nCount++] = ConvertTextToInt(sData);
我们要查看当nCount为10时的运行状况,我们可以通过下面的步骤完成:- 执行b 77,返回这个断点号是<3>3>
- 执行condition 3 nCount=10
这样就可以控制当nCount为10时在77行处中断.
设置捕捉点
你可设置捕捉点来补捉程序运行时的一些事件。如:载入共享库(动态链接库)或是C++的异常。设置捕捉点的格式为:catch
其它相关命令
命令 | 含义 |
---|---|
info b | 查看所设断点 |
break 行号或函数名 <条件表达式> | 设置断点 |
tbreak 行号或函数名 <条件表达式> | 设置临时断点,到达后被自动删除 |
delete [断点号] | 删除指定断点,若缺省断点号则删除所有断点 |
disable [断点号]] | 停止指定断点,使用”info b”仍能查看此断点。 |
enable [断点号] | 激活指定断点,即激活被disable停止的断点 |
condition [断点号] <条件表达式> | 修改对应断点的条件 |
ignore [断点号] |
在程序执行中,忽略对应断点num次 |
step | 单步恢复程序运行,且进入函数调用 |
next | 单步恢复程序运行,但不进入函数调用 |
finish | 运行程序,直到当前函数完成返回 |
c | 继续执行函数,直到函数结束或遇到新的断点 |
3. 检查程序 可以使用以下命令查询程序
命令 | 含义 |
---|---|
list<行号>/<函数名> | 查看指定位置代码 |
forward-search 正则表达式 | 源代码前向搜索 |
reverse-search 正则表达式 | 源代码后向搜索 |
dir dir | 停止路径名 |
show directories | 显示定义了的源文件搜索路径 |
info line | 显示加载到Gdb内存中的代码 |
print 表达式/变量 | 查看程序运行时对应表达式和变量的值 |
x |
查看内存变量内容。其中n为整数表示显示内存的长度,f表示显示的格式,u表示从当前地址往后请求显示的字节数 |
display 表达式 | 设定在单步运行或其他情况中,自动显示的对应表达式的内容 |
info thread | 查看当前有多少线程 |
thread # | 切换到指定线程 |
set print thread-events on/off | 设定是否打印线程状态 |
b/break [location] thread # | 当设置中断时,也可以专为某个线程设置 |
print详解
操作符:
- /@ 是一个和数组有关的操作符,在后面会有更详细的说明。
- :: 指定一个在文件或是一个函数中的变量。
- {} 表示一个指向内存地址的类型为type的一个对象。
程序变量
在GDB中,你可以随时查看以下三种变量的值:- 全局变量(所有文件可见的)
- 静态全局变量(当前文件可见的)
- 局部变量(当前Scope可见的)
数组
“@”的左边是第一个内存的地址的值,“@”的右边则你你想查看内存的长度。
例如,你的程序中有这样的语句:int *array = (int *) malloc (len * sizeof (int));
于是,在GDB调试过程中,你可以以如下命令显示出这个动态数组的取值:p *array@len
输出格式
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。
4. 改变程序的执行
修改变量值
修改被调试程序运行时的变量值,在GDB中很容易实现,使用GDB的print
命令即可完成。如:(gdb) print x=4
跳转执行
一般来说,被调试程序会按照程序代码的运行顺序依次执行。GDB提供了乱序执行的功能,也就是说,GDB可以修改程序的执行顺序,可以让程序执行随意跳跃。这个功能可以由GDB的jump
命令来完:
jump 指定下一条语句的运行点。可以是文件的行号,可以是file:line
格式,可以是+num
这种偏移量格式。表式着下一条运行语句从哪里开始。产生信号量
使用singal
命令,可以产生一个信号量给被调试的程序。如:中断信号Ctrl+C
。这非常方便于程序的调试,可以在程序运行的任意位置设置断点,并在该断点用GDB产生一个信号量,这种精确地在某处产生信号非常有利程序的调试。single命令和shell的kill命令不同,系统的kill命令发信号给被调试程序时,是由GDB截获的,而single命令所发出一信号则是直接发给被调试程序的。
强制函数返回
如果你的调试断点在某个函数中,并还有语句没有执行完。你可以使用return
命令强制函数忽略还没有执行的语句并返回。
使用return命令取消当前函数的执行,并立即返回,如果指定了,那么该表达式的值会被认作函数的返回值。强制调用函数
call
表达式中可以一是函数,以此达到强制调用函数的目的。并显示函数的返回值,如果函数返回值是void,那么就不显示。