Linux下gcc编译器和gdb调试

gcc编译

gcc的文件类型约定规则

文件类型 说明
.c C语言源代码文件
.a 由目标文件构成的档案库文件
.C、.cc、.cxx C++源代码文件
.h 程序所包含的头文件
.i 经预处理过的C源代码文件
.ii 经预处理过的C++源代码文件
.o 编译后的目标文件
.s 汇编语言源代码文件
.S 经过预编译的汇编语言源代码文件

编译四步骤

gcc -o hello.c不指定文件名,默认会生成一个名为a.out文件
在这里插入图片描述
gcc hello.c -o hello 前者为源文件,后者为编译后的文件名
为了更好地理解GCC的工作过程,可以把上述编译过程分成几个步骤单独进行,并观察每一步的运行结果。
第一步进行的是预处理。 在预处理阶段,gcc把预处理命令扫描处理完毕,输入C语言的源文件,通常为*.c,它们通常带有.h之类头文件的包含文件。这个阶段主要处理源文件中的#ifdef#include#define等预处理命令。该阶段会生成一个中间文件*.i,但实际工作中通常不会专门生成这种文件,基本上用不到,若非要生成这种文件,可以使用-E参数让gcc在预处理结束后停止编译过程:
gcc -E hello.c -o hello.i
此时若查看hello.i文件中的内容,会发现头文件stdio.h的内容确实都插入到文件里去了,而其它应当被预处理的宏定义也都做了相应的处理。第二步是编译阶段。在编译阶段,gcc把预处理后的结果编译成汇编或者目标模块。输入的是中间文件*.i,编译后生成汇编语言文件*.s。这个阶段对应的gcc命令如下所示:
gcc -S hello.i -o hello.s

下一步进行的是汇编。在汇编阶段,编译器把编译出来的结果汇编成具体CPU上的目标代码模块。输入汇编文件*.s,输出机器语言*.o。这个阶段可以通过使用-c参数来完成:
gcc -c hello.s -o hello.o
最后,在链接阶段把多个目标代码模块连接生成一个大的目标模块。输入机器代码文件*.o (与其他的机器代码文件和库文件),汇集成一个可执行的二进制代码文件。这一步骤可以利用下面的示例命令完成:gcc hello.o -o hello到这里gcc就完成了整个编译过程,得到可执行文件。

总结一下就是四步骤:
gcc -E hello.c -o hello.i 预处理
gcc -S hello.i -o hello.s 汇编
gcc -c hello.s -o hello.o 编译
gcc hello.o -o hello 链接

加上-pedantic参数就可以看到警告信息,如下图所示。
在这里插入图片描述
需要注意的是,-pedantic编译选项并不能保证被编译程序与ANSI/ISO C标准的完全兼容,它仅仅只能用来帮助Linux程序员离这个目标越来越近,换句话说,-pedantic选项能够帮助程序员发现一些不符合ANSI/ISO C标准的代码,但不是全部。
事实上只有ANSI/ISO C语言标准中:进行编译器诊断的那些情况,才有可能被gcc发现并提出警告。
除了-pedantic之外,gcc还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以-W开头,其中最有价值的当数-Wall
在处理警告方面,另一个常用的编译选项是-Werror,它要求gcc将所有的警告当成错误进行处理,这在使用自动编译工具(如Make等)时非常有用。

优化gcc

代码优化指的是编译器通过分析源代码,找出其中尚未达到最优的部分,然后对其重新进行组合,目的是改善程序的执行性能。gcc提供的代码优化功能非常强大,它通过编译选项**-On**来控制优化代码的生成其中n是一个代表优化级别的整数。对于不同版本的gcc来讲,n的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从0变化到2或3。

编译时使用选项-O可以告诉gcc同时减小代码的长度和执行时间,其效果等价于-O1。在这一级别上能够进行的优化类型虽然取决于目标处理器,但一般都会包括线程跳转(Thread Jump)和延迟退(Deferred Stack Pops)两种优化。选项-O2告诉gcc除了完成所有-O1级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。选项-O3则除了完成所有-O2级别的优化之外,还包括循环展开和其它一些与处理器特性相关的优化工作。通常来说,数字越大优化的等级越高,同时也就意味着程序的运行速度越快。许多Linux程序员都喜欢使用-O2选项,因为它在优化长度、编译时间和代码大小之间,取得了一个比较理想的平衡点。

例如:优化下面这段低效率代码
注:time测量指定程序的执行时间 time ./inefficient_code

#include <stdio.h>

int main(void)
{
    
    
    unsigned long int counter;//定义相关变量
    unsigned long int result;
    unsigned long int temp;
    unsigned int five;
    int i;

    for (counter - 0; counter < 2019 * 2009 * 100 / 4 + 2010; counter += (10 - 6) / 4)
    {
    
    
        temp = counter / 1979;
        for (i - 0; i < 20; i++)
        {
    
    
            five - 200 * 200 / 8000; // 每次循环都会进行复杂的计算
        }
        result = counter;
    }
    printf("Result is %ld \n");

    return 0;
}

没有任何优化的结果
real进程总的执行时间和系统负载有关,保罗进程调度切换时间
user被测量的进程中用户指令得到执行时间
sys被测量进程中内核代替用户指令执行的时间

在这里插入图片描述
一级优化
在这里插入图片描述
二级优化
在这里插入图片描述
三级优化
在这里插入图片描述

调试选项

默认情况下,gcc在编译时不会将调试符号插入到生成的二进制代码中,因为这样会增加可执行文件的大小。如果需要在编译时生成调试符号信息,可以使用gcc的-g或者-ggdb选项。

gcc在产生调试符号时,同样采用了分级的思路,开发人员可以通过在-g选项后附加数字1、2或3来指定在代码中加入调试信息的多少。默认的级别是2(-g2),此时产生的调试信息包括扩展的符号表、行号、局部或外部变量信息。级别3(-g3)包含级别2中的所有调试信息,以及源代构中定义的宏。级别1(-g1)不包含局部变量和与行号有关的调试信息,凶此只能够用于回溯跟踪和堆栈转储之用。

gcc产生的调试符号具有普遍的适应性,可以被许多调试器加以利用,但如果使用的是gcc,那么还可以通过-ggdb选项在生成的二进制代码中包含gdb专用的调试信息。这种做法的优点是可以方便gdb的调试工作,但缺点是可能导致其它调试器(如DBX)无法进行正常的调试。选项-ggdb能够接受的调试级别和-g是完全一样的,它们对输出的调试符号有着相同的影响。

使用任何一个调试选项都会使最终生成的二进制文件的大小急剧增加,同时增加程序在执行时的开销,因此调试选项通常仅在软件的开发和调试阶段使用。这里还是以inefficient_code.c为例。

经过我们测试加入-g选项文件变大了
在这里插入图片描述

gdb调试

**Linux下的GDB(GNU Debugger)是一个用来调试C、C++程序的功能强大的调试器,它能够在程序运行的过程中观察程序的内部结构和内存的使用情况。**程序员也可以使用gdb来跟踪程序中的错误,从而减少了程序员的工作量。

一般来说,gdb主要提供以下功能:

  1. 设置断点(断点可以是条件表达式),使程序在指定的代码行上暂停执行,便于观察;
  2. 单步执行程序便于调试;
  3. 查看程序中变量值的变化;
  4. 动态改变程序的执行环境;
  5. 分析崩溃程序产生的core文件。

gdb是一个命令行方式的调试工具,它不同于我们在Windows下常见的Turbo CVC等图形化程序开发工具。gdb的使用非常简单,只要在Linux的命令提示符下输入gdb,系统便会启动gdb。也可以在gdb后面给出文件名,直接指定想要调试的程序,gdb就会自动调用这个可执行文件进行调试。
命令形式:gdb filename
告诉gdb装入名为filename的可执行文件进行调试。另外,为了使adb 正常工作,必须使程序在编译的时候包含调试信息,这需要在gcc编译时加上-g或者-ggdb选项。调试信息包含了程序中的每个变量的类型和在可执行文件中的地址映射以及源代码的行号。而gdb正是利用这些信息使源代码和机器码相关联。

进入gdb调试界面:
在这里插入图片描述
退出命令:q和quit

gdbc常用命令

gdb支持很多的命令使用户能实现不同的功能,有简单的文件装入命令,有允许程序员检查所调用的堆栈内容的复杂命令,为方便本章后续内容的讲解,和方便读者查阅,这里先将gdb的常用命令列出,gdb支持很多与UNIX shell程序一样的命令编辑特征,例如用户能像在 bash或tcsh里那样使用“Tab”键让gdb帮你补齐一个唯一的命令,如果不唯一的话 gdb会列出所有匹配的命令,用户还可以使用光标键上下翻动历史命令。

命令 含义描述
file 装入想要调试的可执行文件
run 执行当前被调试的程序
kill 终止正在调试的程序
step 执行一行源代码而且进入函数内部
next 执行一行源代码但不进入函数内部
break 在代码里设置断点,这将使程序执行到这里时被挂起
print 打印表达式或变量的值,或打印内存中某个变量开始的一段连续区域的值,还以用来对变量进行赋值
display 设置自动显示的表达式或变量,当程序停住或在单步跟踪时,这些变量会自动显示其当前值
list 列出产生执行文件的源代码的一部分
quit 退出gdb
watch 使你能监视一个变量的值而不管它何时被改变
backtrace 回溯跟踪
framen 定位到发生错误的代码段,n为backtrace命令的输出结果中的行号
examine 查看内存地址中的值强制函数返回
jump 使程序跳转执行
signal 产生信号量
return 强制函数返回
call 强制调用函数
make 使用户不退出gdb就可以重新产生可执行文件
shell 使用户不离开gdb就执行Linux 的she1l命令

break命令的用法

命令 含义描述
break <function> 在进入指定函数时停住。C++中可以使用class:function或function(type,type)格式来指定函数名
break <linenum> 在指定行号停住
break +offset 在当前行号的前面的offset行停住。offiset为自然数
break -offset 在当前行号的后面的offset行停住
break filename:linenum 在源文件filename的 linenum行处停住·
break filename:functions 在源文件filename的 function函数的入口处停住
break *address 在程序运行的内存地址处停住e
break 该命令没有参数时,表示在下一条指令处停住·
break …if<condition> condition表示条件,在条件成立时停住。比如在循环体中,可以设置break if i=100,表示当i为100时停住程序

例如:多加个参数 -ggdb3
在这里插入图片描述

进入gdb break main 表示在主函数入口处设置断点
在这里插入图片描述

查看运行时的数据

在调试程序的过程中,往往需要查看程序中某些表达式或变量的值,以判断程序运行是否正确。使用gdb调试时,常用到的是print、display命令,以及查看内存、寄存器的信息等。

print命令

在调试程序时,当程序被停住时,可以使用print命令(简写为p),或是同义命令inspect来查看当前程序的运行数据。print命令的格式是:
print <expr>
print /<f><expr>
<expr>是表达式,是所调试程序的语言的表达式(gdb可以调试多种编程语言);<f>是输出的格式。比如要把表达式按16进制的格式输出,那么就是/×。

自动显示命令display

可以设置一些自动显示的变量,当程序停住时,或是在单步跟踪时,这些变量会自动显示。命令是display,格式如下:

  1. display <expr>
  2. display/<fmt> <expr>
  3. display/<fmt> <addr>

expr是一个表达式,fmt表示显示的格式,addr表示内存地址。当用display设定好了一个或多个表达式后,只要程序停下来,gdb会自动显示所设置的这些表达式的值。格式is同样被display支持

下表是关于display的相关命令

命令 含义描述
undisplay<dnums…> delete display <dnums…> 删除自动显示,dnums为已设置好了的自动显示的编号。如果要同时删除几个编号,可以用空格分隔;如果要删除一个范围内的编号,可以用减号表示
disable display <dnums…> enable display <dnums…> 不删除自动显示的设置,而只是让其失效或恢复
info display 查看 display设置的自动显示的信息。gdb会显示出一张表格,报告调试中设置了多少个自动显示设置,其中包括已设置的编号、表达式及是否enable等

例如
调试中查看变量的值
在这里插入图片描述

设置自动显示的变量
在这里插入图片描述
在这里插入图片描述

查看源程序

在程序调试的过程中,有时候需要查看源程序的内容,以及源代码在内存中的情况,

显示源代码。

gdb可以打印出所调试程序的源代码,在程序编译时一定要加上-g参数,把源程序信息编译到执行文件中,不然就看不到源程序了。当程序停下来以后,gdb会报告程序停在了程序的第几行上。可以用list命令来显示程序的源代码

list命令说明

命令 含义描述
list <linenum> 显示程序第linenum行的周围的源程序;
list <function> 显示函数名为function的函数的源程序
list 显示当前行后面的源程序
list - 显示当前行前面的源程序。一般是显示当前行的上5行和下5行,或者显示当前行的上2行和下8行,默认共显示10行
setlistsize <count> 设置一次显示源代码的行数
show listsize 查看当前1listsize的设置
list <first>,<last> 显示从first行到1ast行之间的源代码
list,<last> 显示从当前行到1ast行之间的源代码
list + 向后显示源代码·

显示当前附近的行 和查看默认显示行数
set listsize 10 就是设置显示默认行数

在这里插入图片描述
在这里插入图片描述

源代码的内存

可以使用info line命令来查看源代码在内存中的地址。和大多数gdb命令相同,info line后面也可以跟“行号”、“函数名”、“文件名:行号”、“文件名:函数名”的参数形式,
还有一个命令disassemble,可以查看源程序的当前执行时的机器码,这个命令会把目前内存中的指令dump出来。

查看函数在内存中的情况 从什么开始到什么结束 变量在内存中的地址
在这里插入图片描述

改变程序的执行

一旦使用gdb挂上被调试程序,当程序运行起来后,可以根据自己的调试思路来动态地在gdb中更改当前被调试程序的运行线路或是其变量的值。这个强大的功能能够让用户更好地调试程序。比如,可以在程序的一次运行中走遍程序的所有分支。

修改变量值

print命令还有一个功能是,修改被调试程序中运行时的变量值,比如:(gdb) print x=8
x=8这个表达式是C/C++的语法,意为把变量x的值修改为8,如果当前调试的语言是Pascal,那么可以使用Pascal的语法x:=8。

跳转执行

一般来说,被调试程序会按照程序代码的运行顺序依次执行。gdb提供了乱序执行的功能,也就是说,gdb可以修改程序的执行顺序,可以让程序执行随意跳跃。这个功能可以由gdb的jump命令来实现:jump <linespec>
指定下一条语句的运行点。<linespce>可以是文件的行号,可以是file:line格式,可以是+num这种偏移量格式,表示下一条运行语句从哪里开始。jump <address>这里的<address>是代码行的内存地址。

产生信号量

使用singal命令可以产生一个信号量给被调试的程序。如中断信号Ctrl+C。这非常方便于程序的调试,可以在程序运行的任意位置设置断点,并在该断点用gdb产生一个信号量。精确地在某处产生信号非常有利程序的调试。其语法是:signal <singal>
Linux的系统信号量通常从1到15。所以的取值也在这个范围。
signal命令和shell的kill命令不同,系统的kill命令发信号给被调试程序时,是由gdb截获的,而signal命令所发出的信号则是直接发给被调试程序的。

强制函数返回

如果调试断点在某个函数中,还有语句没有执行完,可以使用return命令强制函数忽略还没有执行的语句并返回。
return
return <expression>
使用return命令取消当前函数的执行,并立即返回。如果指定了<expression>,那么该表达式的值会被当作函数的返回值。

猜你喜欢

转载自blog.csdn.net/qq_45254369/article/details/126079873