gcc.gdb.makefile简介

一.关于Linux上的GCC编译一个c的源文件的过程


首先要明确的一点是,虽然称GCC为编译器,但是实际上GCC只是起到了一个调用的工作,实际上在编译汇编的过程中由不同的程序来执行相应的工作,而这个程序自然也不是GCC。

接着开始讨论关于编译文件的过程,我们可以把Linux上的编译过程大致分成四个部分:

预处理---------->>编译 ---------->>>>汇编----------->>>>链接

在执行编译的过程中在一个开始,有一个叫做预编译或者说叫做预处理的过程,在这个过程中,编译器会将在源代码中声明的头文件,常见的比如stdio.h之类的导入到文件中,以及对预处理命令,比如说宏之类的进行处理,最终拼接成一个新的文本,在完成了这个工作之后,编译器才算开始进行真正的编译,在编译的过程中,GCC会调用ccl这个真正的编译器对预处理完成的文件进行编译,在完成这部工作之后,可以得到一个汇编文件,接着GCC调用汇编器as对上一步骤中生成的汇编文件进行汇编,得到了我们所想要的汇编文件,最后链接器ld会将我们在整个程序使用的库函数,比如说printf之类的函数进行链接,最终就得到一个可执行文件了。

在这个过程中,如果对每一步的文件进行观察,就会发现在预处理之后的文件相对于原本的源文件增加了很多的内容,这些就是预处理宏产生的结果。


二.gdb调试


无论多么有经验的程序员,开发的任何软件都不可能完全没有 bug。因此,排查及修复 bug 成为软件开发周期中最重要的任务之一。有许多办法可以排查 bug(测试、代码自审等等),但是还有一些专用软件(称为调试器)可以帮助准确定位问题的所在,以便进行修复。

如果你是 C/C++ 程序员,或者使用 Fortran 和 Modula-2 编程语言开发软件,那么你将会很乐意知道有这么一款优秀的调试器 - GDB - 可以帮你更轻松地调试代码 bug 以及其它问题。在这篇文章中,我们将讨论一下 GDB 调试器的基础知识,包括它提供的一些有用的功能/选项。

在我们开始之前,值得一提的是,文章中的所有说明和示例都已经在 Ubuntu 14.04 LTS 中测试过。教程中的示例代码都是 C 语言写的;使用的 shell 为 bash(4.3.11);GDB 版本为 7.7.1。

GDB 调试器基础

通俗的讲,GDB 可以让你看到程序在执行过程时的内部流程,并帮你明确问题的所在。我们将在下一节通过一个有效的示例来讨论 GDB 调试器的用法,但在此之前,我们先来探讨一些之后对你有帮助的基本要点。

首先,为了能够顺利使用类似 GDB 这样的调试器,你必须以指定的方式编译程序,让编译器产生调试器所需的调试信息。例如,在使用 gcc 编译器(我们将在本教程之后的章节用它来编译 C 程序示例)编译代码的时候,你需要使用 -g 命令行选项。

想要了解 gcc 编译器手册页中关于 -g 命令行选项相关的内容,请看这里

下一步,确保在你的系统中已经安装 GDB 调试器。如果没有安装,而且你使用的是基于 Debian 的系统(如 Ubuntu),那么你就可以使用以下命令轻松安装该工具:

  1. sudo apt-get install gdb

现在,当你按照上述的方式编译完程序(gcc -g 命令行选项),同时也已经安装好 GDB 调试器,那么你就可以使用以下命令让程序在调试模式中运行:

  1. gdb [可执行程序的名称]

这样做会初始化 GDB 调试器,但你的可执行程序此时还不会被启动。在这个时候你就可以定义调试相关的设置。例如,你可以在特定行或函数中设置一个断点让 GDB 在该行暂停程序的执行。

接着,为了启动你的程序,你必须输入执行以下 gdb 命令:

  1. run

在这里,值得一提的是,如果你的程序需要一些命令行参数,那么你可以在这里指定这些参数。例如:

  1. run [参数]

GDB 提供了很多有用的命令,在调试的时候总是能派的上用场。我们将在下一节讨论其中一部分命令。

GDB 调试器用例

现在我们对 GDB 及其用法有了基本的概念。因此,让我们举例来应用所学的知识。这是一段示例代码:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int out = 0, tot = 0, cnt = 0;
  5. int val[] = {5, 54, 76, 91, 35, 27, 45, 15, 99, 0};
  6. while(cnt < 10)
  7. {
  8. out = val[cnt];
  9. tot = tot + 0xffffffff/out;
  10. cnt++;
  11. }
  12. printf("\n Total = [%d]\n", tot);
  13. return 0;
  14. }

简单说明一下这段代码要做什么事。获取 val 数组中每一个值,将其赋值给 out 变量,然后将 tot之前的值与 0xffffffff/out 的结果值累加,赋值给 tot 变量。

这里遇到的问题是,当执行这段代码编译后的可执行程序时,产生以下错误:

  1. $ ./gdb-test
  2. Floating point exception (core dumped)

因此,要调试这段代码,第一步是使用 -g 选项编译程序。命令如下:

  1. gcc -g -Wall gdb-test.c -o gdb-test

接着,让我们运行 GDB 调试器并指定要调试的可执行程序。命令如下:

  1. gdb ./gdb-test

现在,我刚才得到的错误是 Floating point exception,大部分人可能已经知道,这是因为 n % x,当 x 为 0 时导致的错误。所以,考虑到这一点,我在 11 行代码除法运算的位置处添加了一个断点。如下:

  1. (gdb)&;break 11

注意 (gdb) 是调试器的提示信息,我只输入了 break 11 命令。

现在,让 GDB 开始运行程序:

run

当断点第一次被命中时,GDB 显示如下输出:

Breakpoint 1, main () at gdb-test.c:11
11 tot = tot + 0xffffffff/out;
(gdb)

正如你所看到的那样,调试器会显示断点所在的行代码。现在,让我们打印出此时 out 的值。如下:

  1. (gdb) print out
  2. $1 = 5
  3. (gdb)

如上所示,值 5 被打印出来了。这个时候一切都还是正常的。让调试器继续执行程序直到命中下一个断点,可以通过使用 c 命令来完成:

  1. c

重复上述操作,直到 out 值变为 0 时。

  1. ...
  2. ...
  3. ...
  4. Breakpoint 1, main () at gdb-test.c:11
  5. 11 tot = tot + 0xffffffff/out;
  6. (gdb) print out
  7. $2 = 99
  8. (gdb) c
  9. Continuing.
  10. Breakpoint 1, main () at gdb-test.c:11
  11. 11 tot = tot + 0xffffffff/out;
  12. (gdb) print out
  13. $3 = 0
  14. (gdb)

现在,为了进一步确认问题,我使用 GDB 的 s(或 step) 命令代替 c 命令。因为,我只想让当前程序在第 11 行之后暂停,再一步步执行,看看这个时候是否会发生崩溃。

以下是执行之后输出信息:

  1. (gdb) s
  2. Program received signal SIGFPE, Arithmetic exception.
  3. 0x080484aa in main () at gdb-test.c:11
  4. 11 tot = tot + 0xffffffff/out;

是的,如上输出的第一行内容所示,这就是抛出异常的地方。当我再次尝试运行 s 命令时,问题最终也得到了确认:

  1. (gdb) s
  2. Program terminated with signal SIGFPE, Arithmetic exception.
  3. The program no longer exists.

通过这种方式,你就可以使用 GDB 调试你的程序。

总结

GDB 提供了很多功能供用户研究和使用,在这里,我们仅仅只介绍了很少一部分内容。通过 GDB 的手册页可以进一步了解这个工具,当你在调试代码的时候,尝试使用一下它。GDB 调试器有一定的学习难度,但是它很值得你下功夫学习。


三.Makefile基础


在编译一个大型项目的时候,往往有很多目标文件、库文件、头文件以及最终的可执行文件。不同的文件之间存在依赖关系(dependency)。比如当我们使用下面命令编译时:


$gcc -c -o test.o test.c


$gcc -o helloworld test.o


可执行文件helloworld依赖于test.o进行编译的,而test.o依赖于test.c。


依赖关系


在我们编译一个大型项目时,我们往往要很多次的调用编译器,来根据依赖关系,逐步编译整个项目。这样的方式是自下而上的,即先编译下游文件,再编译上游文件。


UNIX系统下的make工具用于自动记录和处理文件之间的依赖关系。我们不用输入大量的”gcc”命令,而只需调用make就可以完成整个编译过程。所有的依赖关系都记录在makefile文本文件中。我们只需要make helloworld,make会根据依赖关系,自上而下的找到编译该文件所需的所有依赖关系,最后再自下而上的编译。


(make有多个版本,本文将基于GNU make。make会自动搜索当前目录下的makefile, Makefile或者GNUmakefile)


依赖


基本概念


我们使用一个示例C语言文件:


#include <stdio.h>

/*

* By Vamei

* test.c for makefile demo

*/

int main()

{

printf("Hello world!\n");

return 0;

}


下面是一个简单的makefile


# helloworld is a binary file

helloworld: test.o

  echo "good"

  gcc -o helloworld test.o

test.o: test.c

  gcc -c -o test.o test.c


观察上面的makefile


  • #号起始的行是注释行

  • target: prerequisite为依赖关系,即目标文件(target)依赖于前提文件(prerequisite)。可以有多个前提文件,用空格分开。

  • 依赖关系后面的缩进行是实现依赖关系进行的操作,即正常的UNIX命令。一个依赖关系可以附属有多个操作。


用直白的话说,就是:


  • 想要helloworld吗?那你必须有test.o,并执行附属的操作。

  • 如果没有test.o,那你必须搜索其他依赖关系,并创建test.o。


我们执行


$make helloworld


来创建helloworld。


make是一个递归创建的过程:


  • Base Case 1: 如果当前依赖关系中没有说明前提文件,那么直接执行操作。

  • Base Case 2: 如果当前依赖关系说明了目标文件,而目标文件所需的前提文件已经存在,而且前提文件与上次make时没有发生改变(根据最近写入时间判断),也直接执行该依赖关系的操作。

  • 如果当前目标文件依赖关系所需的前提文件不存在,或者前提文件发生改变,那么以前提文件为新的目标文件,寻找依赖关系,创建目标文件。


虚线: 依赖关系检索


上面是make的核心功能。有了上面的功能,我们可以记录项目中所有的依赖关系和相关操作,并使用make进行编译。下面的内容都是在此核心内容上的拓展。



make中可以使用宏(MACRO)。宏类似于文本类型的变量。比如下面的CC:


CC = gcc

# helloworld is a binary file

helloworld: test.o

  echo "good"

  $(CC) -o helloworld test.o

test.o: test.c

  $(CC) -c -o test.o test.c


我们用CC来代表”gcc”。在makefile中,使用$(CC)的方式来调用宏的值。make会在运行时,使用宏的值(gcc)来替代$(CC)。


shell的环境变量可以直接作为宏调用。如果同一个自定义的宏同时也有同名环境环境变量,make将优先使用自定义宏。


(可以使用$make -e helloworld来优先使用环境变量)


类似于C语言的宏,makefile中的宏可以方便的管理一些固定出现的文本,并方便替换操作。比如我们未来使用ifort编译器时,只需要更改宏定义为:


CC = ifort


就可以了


内部宏


make中有内部定义的宏,可以直接使用。$@中包含有当前依赖关系的目标文件名,而$^包含当前目标的前提文件:


CC = gcc

# helloworld is a binary file

helloworld: test.o

  echo $@

  $(CC) -o $@ $^

test.o: test.c

  $(CC) -c -o $@ $^


内部宏 功能


$* 当前依赖关系中的目标文件名,不包括后缀。


$* 当前依赖关系中,发生改变的前提文件


$$ 字符”$”


如果目标或者前提文件是一个完整路径,我们可以附加D和F来提取文件夹部分和文件名部分,比如$(@F)表示目标文件的文件名部分。


后缀依赖


在makefile中使用


.SUFFIXES: .c .o


来说明.c和.o是后缀。


我们可以使用后缀依赖的方式,比如:


CC = gcc

.SUFFIXES: .c .o

.c.o:

$(CC) -c -o $@ $^

#--------------------------

# helloworld is a binary file

helloworld: test.o

echo $@

$(CC) -o $@ $^

test.o: test.c


我们定义.c和.o为后缀。并有后缀依赖关系.c.o:。前者为前提,后者为目标。(注意,与一般的依赖关系顺序不同)


上面的test.o和test.c有依赖关系,但没有操作。make会发现该依赖关系符合.c.o的后缀依赖,并执行该后缀依赖后面的操作。


如果项目很大型的时候,后缀依赖非常有用。符合后缀依赖的文件往往有类似的操作,我们可以将这些操作用后缀依赖表示,而避免重复输入。


其他


makefile的续行符为


makefile中经常会定义下面依赖关系:


all:


如果make后没有跟随文件名,那么将执行该依赖关系。


clean:


常用于清理历史文件。


比如:


CC = gcc

.SUFFIXES: .c .o

.c.o:

$(CC) -c -o $@ $^

#--------------------------

all: helloworld

<a href="个人主页 - 伯乐在线">@echo</a> "ALL"

# helloworld is a binary file

helloworld: test.o

<a href="个人主页 - 伯乐在线">@echo</a> $@

$(CC) -o $@ $^

test.o: test.c

clean:

-rm helloworld *.o


注意: echo前面的@和rm前面的-。@后的命令将不显示命令本身。-后面的命令将忽略错误(比如删除不存在的文件)。


总结


make的核心功能是根据依赖关系来实现编译管理。


make的其他功能是让用户可以更加便捷的写出makefile。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值