GCC编译

生成可执行程序的过程

按照细致的小过程来说,这个过程包含:预处理、编译、汇编、链接四个小过程。

C程序编译和链接的过程

预处理

预处理器处理源代码中的预处理指令:将#include 指令包含的头文件内容插入到源文件中;将#define 定义的宏进行文本替换;执行#ifdef#ifndef#if 等条件编译指令;删除源代码中的注释;为调试信息添加行号标记。

输入:源文件(.c/.cpp) 输出:预处理后的源文件(.i/.ii)

编译

编译器将预处理后的源代码转换为汇编代码:将源代码分解为词法单元(tokens),构建抽象语法树(AST),进行类型检查、作用域检查等,然后生成中间表示(IR),进行各种优化(循环优化、死代码消除等),最后生成特定平台的汇编代码。

输入:预处理文件(.i/.ii) 输出:汇编文件(.s/.asm)

汇编

汇编器将汇编代码转换为机器代码:将汇编指令转换为机器指令,为指令和数据分配地址,记录符号(函数名、变量名)的地址信息,标记需要重定位的地址。

输入:汇编文件(.s/.asm) 输出:目标文件(.o/.obj)

链接

链接器将多个目标文件和库文件组合成可执行文件:

符号解析:解析外部符号引用,匹配符号定义和引用,处理函数调用和全局变量访问

地址重定位:确定最终的内存布局,修正目标文件中的地址引用,合并相同类型的段(代码段、数据段等)

库链接静态链接将库代码直接复制到可执行文件中,如果是动态链接就在运行时加载共享库。

输入:目标文件(.o/.obj)+ 库文件(.a/.lib/.so/.dll) 输出:可执行文件(.exe/.out)

GCC指令

预处理

编译

汇编

链接

不加任何选项,直接使用gcc指令,该指令可能激活预处理、编译、汇编和链接四个步骤。大多数情况下,都会选择使用该指令,一步到位。


其它指令

-Wall添加警告信息:

-O0,-O1,-O2,-O3编译器优化级别:

O --Optimization

编译器的4个优化级别,-O0表示不优化,-O1为默认值,开发时常选择,-O2为生产环境下常用的优化级别,-O3的优化级别最高。

-O3的优化手段比较激进,生产环境一般不会选择。

-g添加调试信息:

g --generate debugging information

-I选项

I -Include

-I选项的作用实际上是:

改变头文件包含语法的搜索目录优先级,总是优先去搜索该选项指定的目录,搜索不到时,才按照既定的搜索的路径搜索。比如:

  1. <>方式:表示搜索头文件时,总是先去"../header"下搜索,搜索不到时,再去操作系统头文件目录中寻找。

  2. ""方式:表示搜索头文件时,总是先去"../header"下搜索,搜索不到再去当前目录"."中寻找头文件,如果还找不到再去操作系统头文件目录中寻找。

GDB

带调试信息编译代码

编译过程会去掉代码中诸如变量名这样的调试信息,从汇编代码开始这些调试信息就被替换成了内存地址。

所以要想使用GDB调试程序。首先第一步就是:使用带"-g"的指令编译生成可执行程序。

进入GDB调试界面

查看源代码

打断点

断点信息

这段信息从左往右内容是:

  1. Num:断点的唯一性标识编号,一次Debug过程断点编号会从1开始,且不会重置一直累加。

  2. Type:断点的类型,这里显示"breakpoint",表示断点都只是普通断点。

  3. Disp(Disposition,性格):它表示断点触发后,是否会继续触发。

    1. keep就表示它是一个持久的断点,只要不删除就一直存在,每次启动都会触发。

    2. 这个值如果是del就表示,该断点是一个一次性断点。

  4. Enb(enable):指示断点是否生效,可以通过dis指令设置该属性。

  5. What:指示断点在源代码的哪个位置

GDB常用调试指令

逐语句/单步调试

跳出并执行完函数

逐过程

监视窗口/查看变量取值

如果想要持续的,展示某个表达式的值,使用格式如下:

如果需要查看所有局部变量的值,局部变量窗口,使用格式如下:

继续,跳过一次断点:

忽略断点n次

查看堆栈信息

查看内存,内存窗口

x --examine

内存数据的输出格式有:

  1. o(octal),八进制整数

  2. x(hex),十六进制整数

  3. d(decimal),十进制整数

  4. u(unsigned decimal),无符号整数

  5. t(binary),二进制整数

  6. f(float),浮点数

  7. c(char),字符

  8. a(address),地址值

  9. c(character):字符

  10. s(string),字符串

一个内存单元的大小的表示,有以下格式:

  1. b(byte),一个字节

  2. h(halfword, 2 bytes),二个字节

  3. w(word, 4 bytes),四个字节

  4. g(giant, 8 bytes),八个字节

输入命令行参数

当main函数的形参列表是int argc, char *argv[]时,允许可执行程序传参命令行参数。如果想要使用GDB调试带命令行参数的可执行程序,有以下两种方式可以选择:

  1. 在启动GDB时,使用指令gdb --args ./a.out arg1 arg2 arg3...即可表示传递命令行参数,其中a.out表示可执行程序的名字。

  2. 如果已经启动了GDB,可以使用以下两种方式都可以传递命令行参数:

    1. 使用指令set args arg1 arg2....,其中指令部分是set args,后面的部分则是参数。

    2. 使用指令run/r arg1 arg2启动,也表示传递命令行参数。

利用display,持续显示数组特定范围的取值:

观察断点:

调试Coredump文件

Coredump 文件常用于辅助分析和 Debug,下面简单介绍一下这种调试手段。

ulimit --user limit

默认情况下,该指令输出的第一行就是:

core file size (blocks, -c) 0

表示此时系统允许生成的core文件最大是0个字节,即不允许生成。

所以我们需要用下列指令将core文件的大小设置为不受限制:

默认情况下,上述操作后可能还是无法生成Core文件,可以切换到root用户或者使用sudo权限,然后补充一下core文件的配置信息到目标文件里。

具体的操作如下,先打开配置文件:

将下列信息补充到配置文件末尾(注意前面不要加#号)

紧跟着还需要执行以下指令让配置信息生效:

这段配置信息的目的是给core文件设定一个固定的格式,这样设置后,再次执行报错可执行程序就会生成core文件了。

但是要注意:一般只有段错误才会生成对应core文件,像上面数组越界引发未定义行为是没有段错误的,也就不会生成core文件。

然后就可以用指令:

查看报错的一些信息,此时再利用bt等指令就可以进行正常的程序调试了。