Linux工程管理器和GDB调试器

在实际的开发过程中,仅仅通过使用 gcc 命令对程序进行编译是非常低效的。原因主要有以下两点。

1)程序往往是由多个源文件组成的,源文件的个数越多,那么 gcc 的命令行就会越长。此外,各种编译规则也会加

大 gcc 命令行的复杂度。所以在开发调试程序的过程中,通过输入gcc 命令行来编译程序是很麻烦的。
2)在程序的整个开发过程中,调试的工作量占到了整体工作量的 70%以上。在调试程序的过程中,每次调试一般只

会修改部分源文件。而在使用 gcc 命令行编译程序时,gcc 会把那些没有被修改的源文件一起编译,这样就会影响编译的

总体效率。

GCC 提供了半自动化的工程管理器 Make。所谓半自动化是指在使用工程管理器前需要人工编写程序的编译规则

所有的编译规则都保存在 Makefile 文件中。全自动化的工程管理器在编译程序前会自动生成 Makefile 文件。Make工

程管理器的优越性具体体现在以下两个方面。

(1)使用方便
通过命令“make”就可以启动 Make 工程管理器对程序进行编译,所以不再需要每次都输入 gcc 命令行。Make 启动

后会根据 Makefile 文件中的编译规则命令自动对源文件进行编译和链接,最终生成可执行文件。

(2)调试效率高
为了提高编译程序的效率,Make 会检查每个源文件的修改时间(时间戳)。只有在上次编译之后被修改的源文件

才会在接下来的编译过程中被编译和链接,这样就能避免多余的编译工作量。为了保证源文件具有正确的时间戳,必须

保证操作系统时间的正确性(注意VMWare 虚拟机的 CMOS 时间是否正确)。

 

一、Makefile用法:

Makefile 文件由以下三项基本内容组成。

1)需要生成的目标文件(target file)。

2)生成目标文件所需要的依赖文件(dependency file)。
3)生成目标文件的编译规则命令行(command)。


这三项内容按照如下格式进行组织:
target file  : dependency file
(Tab)command

其中,Makefile 规定在书写 command 命令行前必须加一个<Tab>键。

Makefile 文件由以下三项基本内容组成。
1)需要生成的目标文件(target file)。
2)生成目标文件所需要的依赖文件(dependency file)。
3)生成目标文件的编译规则命令行(command)。
Make 工程管理器在编译程序时会检查每个依赖文件的时间戳,一旦发现某个依赖文件的时间戳比目标文件要新,就会执行目标文件的规则命令来重新生成目标文件。这个过程称为目标文件的依赖规则检查。依赖规则检查是 Make 工程管理器的最核心的工作任务之一。下面以编译程序 test(由 a.c、b.c 和 b.h 组成)为例来描述 Make 的工作过程。例如:

  a.c:

#include “b.h”
int main()
{
hello();
return 0;
}

  b.h:

 

 

 

 

void hello();

  b.c

#include “stdio.h”
void hello()
{
printf(“hello”);

  Makefile:

test : a.o b.o
cc -o test a.o b.o
a.o : a.c b.h
cc -c a.c
b.o : b.c
cc -c b.c

 

Make 工程管理器编译 test 程序的过程如下:
(1)Make 工程管理器首先会在当前目录下读取 Makefile 文件。

 

(2)查找 Makefile 文件中的第一个目标文件(在本例中为 test),该文件也是 Make 工程管理器本次编译任务的最

终目标。

(3)把目标文件 test 的依赖文件当作目标文件进行依赖规则检查。这是一个递归的检查过程。在本例中就是依次把

a.o 和 b.o 作为目标文件来检查各自的依赖规则。Make 会根据以下三种情况进行处理:

1) 如果当前目录下没有或缺少依赖文件,则执行其规则命令生成依赖文件(比如缺少 a.o文件,则执行命令“cc

-c a.c”生成 a.o)。

 

2) 如果存在依赖文件,则把其作为目标文件来检查依赖规则(假设 a.c 比 a.o 新,则执行命令“cc -c a.c”更新 a.o)。
3) 如果目标文件比所有依赖文件新,则不做处理。
(4)递归执行第三步后,就会得到目标文件 test 所有最新的依赖文件了。接着 Make 会根据以下三种情况进行处理:
1) 如果目标文件 test 不存在(比如第一次编译),则执行规则命令生成 test。
2) 如果目标文件 test 存在,但存在比 test 要新的依赖文件,则执行规则命令更新 test。

 

3) 目标文件 test 存在,且比所有依赖文件新,则不做处理。


例如:下面通过 make 的运行结果来印证上述流程。
(1)第一次编译时,由于没有 test、a.o 和 b.o,Make 会先执行命令“cc -c a.c”生成a.o;然后接着执行命令“cc -c b.c”

生成 b.o;最后执行命令“cc -o test a.o b.o”生成 test 文件。如下所示:

下面通过 make 的运行结果来印证上述流程。

(1)第一次编译时,由于没有 test、a.o 和 b.o,Make 会先执行命令“cc -c a.c”生成a.o;然后接着执行命令“cc -c b.c”

生成 b.o;最后执行命令“cc -o test a.o b.o”生成 test 文件。如下所示:

[root@localhost home]#make
cc –c a.c
cc –c b.c
cc –o test a.o b.o

(2)如果修改了 a.c 文件,Make 会先执行命令“cc -c a.c”生成 a.o;由于 b.o 没有修改,所以 Make 就接着执行命令

“cc -o test a.o b.o”生成 test 文件。如下所示:

[root@localhost home]#make
cc –c a.c
cc –o test a.o b.o

(3)如果删除了 b.o 文件,由于 a.o 没有修改,所以 Make 就先执行命令“cc -c b.c”生成 b.o;然后接着执行命令“cc

 

-o test a.o b.o”生成 test 文件。如下所示:

[root@localhost home]#make
cc –c b.c
cc –o test a.o b.o

(4)如果再运行一次 make 时,因为所有的源文件都没有改动,所以 Make 不会有任何动作。
如下所示:

 

[root@localhost home]#make
make: “test”是最新的。

 

二、Makefile 特性介绍:

源文件数量越是多的程序,其编译规则就会越复杂,导致 Makefile 文件也越复杂。为了简化 Makefile 的编写,丰富编

译程序的方法和手段。Makefile 提供了很多类似高级编程语言的语法机制。

1. 变量

在 Makefile 文件中,存在着大量的文件名,而且这些文件名都是重复出现的。所以在源文件比较多的情况下,很

容易发生遗漏或写错文件名。而且一旦源文件的名称发生了变化,还容易造成与其他文件名不一致的错误。于是,Ma

kefile 提供了变量来代替文件名。变量的使用方式为:$(变量名)

例如:

 

obj = a.o b.o
test : $(obj)
cc -o test $(obj)
a.o : a.c b.h
cc -c a.c
b.o : b.c
cc -c b.c


该 Makefile 使用了变量 obj 来代替“a.o b.o”。当源文件名发生改动或增删源文件时,只要对变量 obj 的值进行相应的修改就可以了,这样可以避免文件名不一致或遗漏的错误。Makefile 中变量的命名可以使用字符、数字和下划线,但要注意变量名对大小写是敏感的。

 

此外,Make 工程管理器提供了灵活的变量定义方式,具体有以下几种实现方式。

(1)通过“=”来实现
例如:

a1= $(a2)
a2= $(a3)
a3= a.o


这种方式下变量 a1 的值是 a.o,也就是说前面的变量可以通过后面的变量来定义。但使用这种方式定义变量时,要防止出现死循环的情况

 

 

 

(3)通过“+=”来实现
例如:

a1= a.o
a1+= b.o

 

这种方式下变量 a1 的值是“a.o b.o”。也就是说“+=”可以实现给变量追加值。等同于如下示例:

 

a1= a.o
a1:= $(a1) b.o

可以看到,Makefile 的“+=”和 C 语言中的“+=”是非常相似的。

 

2. 自动推导

为了进一步简化 Makefile 的书写,Make 工程管理器提供了自动推导的功能。自动推导功能默认每个目标文件都有一个与之对应的依赖文件。比如 a.o 文件有依赖文件 a.c 与之对应)。这样在 Makefile 中就不需要指定与目标文件对应的依赖文件名了。此外,自动推导功能还能推导出与目标文件对应的基本编译规则命令。比如 a.o 文件的规则命令为“gcc –c–o a.c”。
例如:

 

obj = a.o b.o
test : $(obj)
cc -o test $(obj)
a.o : b.h

结果为:

 

 

 

 

[root@localhost home]#make
cc –c –o a.o a.c
cc –c –o b.o b.c
cc –o test a.o b.o		

可以看到,Makefile 分别推导出了目标文件 a.o 和 b.o 的规则命令“cc -c -o a.o a.c”与“cc -c -o b.o b.c”。

 

 

3. 伪目标

 

伪目标不是真正的目标文件,所以通过伪目标可以让 Make 工程管理器只执行规则命令,而不用创建实际的目标文件。伪目标的使用方式为:make  伪目标名

 

 

由于伪目标不是真正的目标文件,只是一个符号。为了不和真实的目标文件混淆,最好
使用“.PHONY”对伪目标进行标识。

例如:

obj = a.o b.o
.PHONY : all
all : test $(obj)
test : $(obj)
cc –o test $(obj)
.PHONY : clean
clean :
rm –rf test $(obj)
test_dir = /home/t_d
.PHONY : install
install :
mkdir $(test_dir)
cp test $(test_dir)
.PHONY : uninstall
uninstall :
rm -rf $(test_dir)

 

 

(1)all

运行命令“make all”后,Make 会把 all 看成是最终的目标。由于伪目标和真实目标一样都有依赖文件,所以 Make 会更新 all 的依赖文件 test、a.o 和 b.o。如下所示:

[root@localhost home]#make all
cc –c –o a.o a.c
cc –c –o b.o b.c
cc –o test a.o b.o

(2)clean

运行命令“make clean”后,Make 会执行命令“rm -rf test $(obj)”。这样 test、a.o 和 b.o 文件就全被删除了。如下所示:

[root@localhost home]#make clean
rm –rf test a.o b.o

(3)install

运行命令“make clean”后,Make 会顺序执行命令“mkdir $(test_dir)”和“cp test$(test_dir)”,把 test 文件复制到 test_dir 变量指定的目录中去(这里只是模拟安装过程,并不是真正的实现安装方法)。如下所示:

[root@localhost home]#make install
mkdir /home/t_d
cp test /home/t_d

(4)uninstall

运行命令“make clean”后,Make 会执行命令“rm -rf $(test_dir)”。这样就可以把变量 test_dir 指定的目录以及目录中的文件全部删除。如下所示:

[root@localhost home]#make uninstall
rm -rf /home/t_d

在 Makefile 文件中,伪目标是非常有用的。比如在递归编译、并行编译等场合中, 使
用伪目标可以方便地控制编译过程。

 

 


三、GDB调试器

程序的调试工作在整个程序的开发过程中占据了相当大的比例。使用 gcc 调试 C 程序时,只能依靠 gcc 发出的

警告或错误信息来进行,所以调试的效率非常低。为此,GNU 开发了 GDB 调试器(GNU Debugger)。GDB 的调

试功能非常强大,甚至可以和 Visual C++、Visual Basic、Jbuilder 等开发工具的调试器相媲美。但 GDB 的缺点是没
有图形调试界面。尽管如此,对于从事嵌入式 Linux 应用开发的人员还是有必要知道 GDB的使用方法的。
例如:
  test.c:

#include <stdio.h>
int cal(int n)
{
if(n == 1)
return 1;
else
return n * cal(n - 1);
}
int main()
{
int n = 5;
n = cal(n);
printf(“%d”,n);
return 0;
}

test.c文件是一个通过递归调用来计算5的阶乘的程序。通过运行命令“gcc –g test.c-o test”对 test.c 进行编译其中参数 g 的作用是把调试信息加入生成的 test 可执行文件中,否则 GDB 就无法对 test 进行调试

接下来可以使用命令“gdb test”启动 GDB 对 test 进行调试了。如下所示:

[root@localhost home]# gdb test
GNU gdb Everest Linux (6.4-1)
Copyright 2005 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnu"...Using host libthread_db
library "/lib/libthread_db.so.1".

可以看到,GDB 首先显示了版本信息和库信息。随后 GDB 停留在符号“(gdb)”处等待用户输入调试命令。GD

B 提供了大量的命令来实现各种调试功能,下面仅对一些常用的命令进行介绍。

(1)查看源文件

在调试程序时,gcc 会给出产生警告或错误的代码行数。但在普通的文本环境中是无法直接获得语句行数的。在

 

GDB 中通过命令 l(list 的缩写)可以查看所有的代码行数。如下所示:

(gdb) l
2 int cal(int n)
3 {
4 if(n == 1)
5 return 1;
6 else
7 return n * cal(n - 1);
8 }
9
10 int main()
11 {
(gdb) l
12 int n = 5;
13 n = cal(n);
14 printf("%/d",n);
15 return 0;
16 }
(gdb) l
Line number 17 out of range; test.c has 16 lines.

 

 

 

可以看到,GDB 以 10 行为单位进行显示。再运行一次命令 l 就会显示下 10 行代码。这样设计方便了源代码的

阅读。

(2)设置断点

断点是调试程序的重要方法,通过断点可以知道程序每一步的执行状况(比如当前变量的值、函数是否调用、堆

 

栈使用情况等)。在 GDB 中通过命令 b(breakpoint 的缩写)进行断点设置。如下所示:

(gdb) b 7
Breakpoint 1 at 0x8048389: file test.c, line 7.

 

可以看到,GDB 在程序的第 7 行处设置了第一个断点,并显示了断点的位置信息。

 

(4)运行程序

在 GDB 中通过命令 r(run 的缩写)运行程序。GDB 默认从代码的首行开始运行(也可以通过“r 行数”的方式让

 

程序从指定行数开始运行)。如果程序中有断点,则程序会在断点行数的前一行暂停运行,结果如下所示:

 

(gdb) r
Starting program: /home/test
Breakpoint 1, cal (n=5) at test.c:7
7 return n * cal(n - 1);

 

可以看到,程序在运行到第 7 行时就暂停了,没有继续执行第 8 行的代码。

 

(5)查看变量值

程序暂停运行后就可以查看当前的状态了。在 GDB 中通过命令“p 变量名”(print 的缩写)查看当前变量 n 的值

如下所示:

(gdb) p n
$1 = 5

GDB 通过“$N”(“$1”、“$2”)来显示变量的值。这样在下次查看变量值时,就可以用“$N”代替变量名了。可以看到,当前变量 n 的值为 5。

 

(6)继续运行程序

查看完当前程序的情况后,就可以让程序继续往下运行了。在 GDB 中通过命令 c 让程序继续往下运行在 test.c 中,由于函数 cal 是递归调用运行,所以程序会再次在断点处暂停。如下所示:

 

 

(gdb) c
Continuing.
Breakpoint 1, cal (n=4) at test.c:7
7 return n * cal(n - 1);


程序暂停后可以再次查看当前变量 n 的值。如下所示:

 

 

 

 

(gdb) p n
$2 = 4

 

(7)单步运行

在程序逻辑比较复杂的时候往往需要程序能一步一步的往下运行,但如果每行都设置一个断点的话又会很麻烦。

 

在 GDB 中可以通过命令 s(step 的缩写)和 n(next 的缩写)让程序一步一步的往下运行。其中s 可以在发生

函数调用时进入函数内部运行,而 n 不会进入函数内部运行。在 test.c 中。由于函数 cal 是递归调用运行,所以只能

选择 s 才能看到变量n 的值。如下所示:

 

(gdb) s
cal (n=3) at test.c:4
4 if(n == 1)
(gdb) s
Breakpoint 1, cal (n=3) at test.c:7
7 return n * cal(n - 1);
(gdb) s
cal (n=2) at test.c:4
4 if(n == 1)
(gdb) s
104
Breakpoint 1, cal (n=2) at test.c:7
7 return n * cal(n - 1);
(gdb) s
cal (n=1) at test.c:4
4 if(n == 1)
(gdb) s
5 return 1;

由于在使用 s 前函数 cal 已经调用了两次,所以运行 s 后当前变量 n 的值为 3。可以看到,函数 cal 进行 3 次调用

 

后返回了值 1。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值