在小V机器人人编程的时候MOVL V=11.0 PL=0,是什么意思,我不要后面的PL=0怎么调

GNU CC(简称为Gcc)是GNU项目中符合ANSI C标准的编译系统,能够编译用C、C++和Object C等语言编写的程序。Gcc不仅功能强大,而且可以编译如C、C++、Object C、Java、Fortran、Pascal、Modula-3和Ada等多种语言,而且Gcc又是一个交叉平台编译器,它能够在当前CPU平台上为多种不同体系结构的硬件平台开发软件,因此尤其适合在嵌入式领域的开发编译。本章中的示例,除非特别注明,否则均采用Gcc版本为4.0.0。

表3.6 Gcc所支持后缀名解释

已经过预处理的C原始程序

已经过预处理的C++原始程序

如本章开头提到的,Gcc的编译流程分为了四个步骤,分别为:

下面就具体来查看一下Gcc是如何完成四个步骤的。

首先,有以下hello.c源代码

在该阶段,编译器将上述代码中的stdio.h编译进来,并且用户可以使用Gcc的选项”-E”进行查看,该选项的作用是让Gcc在预处理结束后停止编译过程。

Gcc指令的一般格式为:Gcc [选项] 要编译的文件 [选项] [目标文件]

其中,目标文件可缺省,Gcc默认生成可执行的文件,命为:编译文件.out

在此处,选项”-o”是指目标文件,由表3.6可知,”.i”文件为已经过预处理的C原始程序。以下列出了hello.i文件的部分内容:

由此可见,Gcc确实进行了预处理,它把”stdio.h”的内容插入到hello.i文件中。

接下来进行的是编译阶段,在这个阶段中,Gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,Gcc把代码翻译成汇编语言。用户可以使用”-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。

以下列出了hello.s的内容,可见Gcc已经将其转化为汇编了,感兴趣的读者可以分析一下这一行简单的C语言小程序是如何用汇编代码实现的。


汇编阶段是把编译阶段生成的”.s”文件转成目标文件,读者在此可使用选项”-c”就可看到汇编代码已转化为”.o”的二进制目标代码了。如下所示:

在成功编译之后,就进入了链接阶段。在这里涉及到一个重要的概念:函数库。

读者可以重新查看这个小程序,在这个程序中并没有定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现”printf”函数的呢?最后的答案是:系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有特别指定时,Gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数”printf”了,而这也就是链接的作用。

函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。Gcc在编译时默认使用动态库。

完成了链接之后,Gcc就可以生成可执行文件,如下所示。

运行该可执行文件,出现正确的结果如下。

Gcc有超过100个的可用选项,主要包括总体选项、告警和出错选项、优化选项和体系结构相关选项。以下对每一类中最常用的选项进行讲解。

Gcc的总结选项如表3.7所示,很多在前面的示例中已经有所涉及。

只是编译不链接,生成目标文件“.o”

只是编译不汇编,生成汇编代码

只进行预编译,不做其他处理

在可执行程序中包含标准调试信息

把输出文件输出到file里

打印出编译器内部编译各过程的命令行信息和编译器的版本

在头文件的搜索路径列表中添加dir目录

在库文件的搜索路径列表中添加dir目录

连接名为library的库文件

对于“-c”、“-E”、“-o”、“-S”选项在前一小节中已经讲解了其使用方法,在此主要讲解另外两个非常常用的库依赖选项“-I dir”和“-L dir”。

正如上表中所述,“-I dir”选项可以在头文件的搜索路径列表中添加dir目录。由于Linux中头文件都默认放到了“/usr/include/”目录下,因此,当用户希望添加放置在其他位置的头文件时,就可以通过“-I dir”选项来指定,这样,Gcc就会到相应的位置查找对应的目录。

这样,就可在Gcc命令行中加入“-I”选项:

这样,Gcc就能够执行出正确结果。

在include语句中,“<>”表示在标准路径中搜索头文件,““””表示在本目录中搜索。故在上例中,可把hello1.c的“#include<my.h>”改为“#include “my.h””,就不需要加上“-I”选项了。

选项“-L dir”的功能与“-I dir”类似,能够在库文件的搜索路径列表中添加dir目录。例如有程序hello_sq.c需要用到目录“/root/workplace/Gcc/lib”下的一个动态库libsunq.so,则只需键入如下命令即可:

需要注意的是,“-I dir”和“-L dir”都只是指定了路径,而没有指定文件,因此不能在路径中包含文件名。

另外值得详细解释一下的是“-l”选项,它指示Gcc去连接库文件libsunq.so。由于在Linux下的库文件命名时有一个规定:必须以lib三个字母开头。因此在用-l选项指定链接的库文件名时可以省去lib三个字母。也就是说Gcc在对”-lsunq”进行处理时,会自动去链接名为libsunq.so的文件。

Gcc的告警和出错选项如表3.8所示。

支持符合ANSI标准的C程序

允许发出ANSI C标准所列的全部警告信息

允许发出ANSI C标准所列的全部错误信息

允许发出Gcc提供的所有有用的报警信息

把所有的告警信息转化为错误信息,并在告警发生时终止编译过程

下面结合实例对这几个告警和出错选项进行简单的讲解。

这是一个很糟糕的程序,读者可以考虑一下有哪些问题?

该选项强制Gcc生成标准语法所要求的告警信息,尽管这还并不能保证所有没有警告的程序都是符合ANSI C标准的。运行结果如下所示:

warning.c:7 警告:在无返回值的函数中,“return”带返回值

可以看出,该选项并没有发现”long long”这个无效数据类型的错误。

允许发出ANSI C标准所列的全部警告信息,同样也保证所有没有警告的程序都是符合ANSI C标准的。其运行结果如下所示:

warning.c:7 警告:在无返回值的函数中,“return”带返回值

可以看出,使用该选项查看出了”long long”这个无效数据类型的错误。

允许发出Gcc能够提供的所有有用的报警信息。该选项的运行结果如下所示:

warning.c:7 警告:在无返回值的函数中,”return”带返回值

使用“-Wall”选项找出了未使用的变量tmp,但它并没有找出无效数据类型的错误。

另外,Gcc还可以利用选项对单独的常见错误分别指定警告,有关具体选项的含义感兴趣的读者可以查看Gcc手册进行学习。

Gcc可以对代码进行优化,它通过编译选项“-On”来控制优化代码的生成,其中n是一个代表优化级别的整数。对于不同版本的Gcc来讲,n的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从0变化到2或3。

不同的优化级别对应不同的优化处理工作。如使用优化选项“-O”主要进行线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。使用优化选项“-O2”除了完成所有“-O1”级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。选项“-O3”则还包括循环展开和其他一些与处理器特性相关的优化工作。

虽然优化选项可以加速代码的运行速度,但对于调试而言将是一个很大的挑战。因为代码在经过优化之后,原先在源程序中声明和使用的变量很可能不再使用,控制流也可能会突然跳转到意外的地方,循环语句也有可能因为循环展开而变得到处都有,所有这些对调试来讲都将是一场噩梦。所以笔者建议在调试的时候最好不使用任何优化选项,只有当程序在最终发行的时候才考虑对其进行优化。

(4)体系结构相关选项

Gcc的体系结构相关选项如表3.9所示。

表3.9Gcc体系结构相关选项列表

使用IEEE标准进行浮点数的比较

不使用IEEE标准进行浮点数的比较

输出包含浮点库调用的目标代码

强行将函数参数个数固定的函数用ret NUM返回,节省调用函数的一条指令

这些体系结构相关选项在嵌入式的设计中会有较多的应用,读者需根据不同体系结构将对应的选项进行组合处理。在本书后面涉及到具体实例会有针对性的讲解。

调试是所有程序员都会面临的问题。如何提高程序员的调试效率,更好更快地定位程序中的问题从而加快程序开发的进度,是大家共同面对的。就如读者熟知的Windows下的一些调试工具,如VC自带的如设置断点、单步跟踪等,都受到了广大用户的赞赏。那么,在Linux下有什么很好的调试工具呢?

本文所介绍的Gdb调试器是一款GNU开发组织并发布的UNIX/Linux下的程序调试工具。虽然,它没有图形化的友好界面,但是它强大的功能也足以与微软的VC工具等媲美。下面就请跟随笔者一步步学习Gdb调试器。

首先,笔者给出了一个短小的程序,由此带领读者熟悉一下Gdb的使用流程。强烈建议读者能够实际动手操作。

首先,打开Linux下的编辑器Vi或者Emacs,编辑如下代码。(由于为了更好地熟悉Gdb的操作,笔者在此使用Vi编辑,希望读者能够参见3.3节中对Vi的介绍,并熟练使用Vi)。


在保存退出后首先使用Gcc对test.c进行编译,注意一定要加上选项”-g”,这样编译出的可执行代码中才包含调试信息,否则之后Gdb无法载入该可执行文件。

虽然这段程序没有错误,但调试完全正确的程序可以更加了解Gdb的使用流程。接下来就启动Gdb进行调试。注意,Gdb进行调试的是可执行文件,而不是如”.c”的源代码,因此,需要先通过Gcc编译生成可执行文件才能用Gdb进行调试。

可以看出,在Gdb的启动画面中指出了Gdb的版本号、使用的库文件等信息,接下来就进入了由“(gdb)”开头的命令行界面了。

在Gdb中键入”l”(list)就可以查看所载入的文件,如下所示:

在Gdb的命令中都可使用缩略形式的命令,如“l”代便“list”,“b”代表“breakpoint”,“p”代表“print”等,读者也可使用“help”命令查看帮助信息。

可以看出,Gdb列出的源代码中明确地给出了对应的行号,这样就可以大大地方便代码的定位。

设置断点是调试程序中是一个非常重要的手段,它可以使程序到一定位置暂停它的运行。因此,程序员在该位置处可以方便地查看变量的值、堆栈情况等,从而找出代码的症结所在。

在Gdb中设置断点非常简单,只需在”b”后加入对应的行号即可(这是最常用的方式,另外还有其他方式设置断点)。如下所示:

要注意的是,在Gdb中利用行号设置断点是指代码运行到对应行之前将其停止,如上例中,代码运行到第五行之前暂停(并没有运行第五行)。

在设置完断点之后,用户可以键入”info b”来查看设置断点情况,在Gdb中可以设置多个断点。

接下来就可运行代码了,Gdb默认从首行开始运行代码,可键入”r”(run)即可(若想从程序中指定行开始运行,可在r后面加上行号)。

可以看到,程序运行到断点处就停止了。

在程序停止运行之后,程序员所要做的工作是查看断点处的相关变量值。在Gdb中只需键入”p”+变量值即可,如下所示:

在此处,为什么变量”i”的值为如此奇怪的一个数字呢?原因就在于程序是在断点设置的对应行之前停止的,那么在此时,并没有把”i”的数值赋为零,而只是一个随机的数字。但变量”n”是在第四行赋值的,故在此时已经为零。

Gdb在显示变量值时都会在对应值之前加上”$N”标记,它是当前变量值的引用标记,所以以后若想再次引用此变量就可以直接写作”$N”,而无需写冗长的变量名。

单步运行可以使用命令”n”(next)或”s”(step),它们之间的区别在于:若有函数调用的时候,”s”会进入该函数而”n”不会进入该函数。因此,”s”就类似于VC等工具中的”step in”,”n”类似与VC等工具中的”step over”。它们的使用如下所示:

可见,使用”n”后,程序显示函数sum的运行结果并向下执行,而使用”s”后则进入到sum函数之中单步运行。

在查看完所需变量及堆栈情况后,就可以使用命令”c”(continue)恢复程序的正常运行了。这时,它会把剩余还未执行的程序执行完,并显示剩余程序中的执行结果。以下是之前使用”n”命令恢复后的执行结果:

可以看出,程序在运行完后退出,之后程序处于“停止状态”。

在Gdb中,程序的运行状态有“运行”、“暂停”和“停止”三种,其中“暂停”状态为程序遇到了断点或观察点之类的,程序暂时停止运行,而此时函数的地址、函数参数、函数内的局部变量都会被压入“栈”(Stack)中。故在这种状态下可以查看函数的变量值等各种属性。但在函数处于“停止”状态之后,“栈”就会自动撤销,它也就无法查看各种信息了。

Gdb的命令可以通过查看help进行查找,由于Gdb的命令很多,因此Gdb的help将其分成了很多种类(class),用户可以通过进一步查看相关class找到相应命令。如下所示:

上述列出了Gdb各个分类的命令,注意底部的加粗部分说明其为分类命令。接下来可以具体查找各分类种的命令。如下所示:

至此,若用户想要查找call命令,就可键入“help call”。

当然,若用户已知命令名,直接键入“help [command]”也是可以的。

Gdb中的命令主要分为以下几类:工作环境相关命令、设置断点与恢复命令、源代码查看命令、查看运行数据相关命令及修改运行参数命令。以下就分别对这几类的命令进行讲解。

Gdb中不仅可以调试所运行的程序,而且还可以对程序相关的工作环境进行相应的设定,甚至还可以使用shell中的命令进行相关的操作,其功能极其强大。表3.10所示列出了Gdb常见工作环境相关命令。

表3.10 Gdb工作环境相关命令

进入到dir目录,相当于shell中的cd命令

2.设置断点与恢复命令

Gdb中设置断点与恢复的常见命令如表3.11所示。

表3.11 Gdb设置断点与恢复相关命令

设置临时断点,到达后被自动删除

删除指定断点,其断点号为”info b”中的第一栏。若缺省断点号则删除所有断点

停止指定断点,使用”info b”仍能查看此断点。同delete一样,省断点号则停止所有断点

激活指定断点,即激活被disable停止的断点

在程序执行中,忽略对应断点num次

单步恢复程序运行,且进入函数调用

单步恢复程序运行,但不进入函数调用

运行程序,直到当前函数完成返回

继续执行函数,直到函数结束或遇到新的断点

由于设置断点在Gdb的调试中非常重要,所以在此再着重讲解一下Gdb中设置断点的方法。

Gdb中设置断点有多种方式:其一是按行设置断点,设置方法在3.5.1节已经指出,在此就不重复了。另外还可以设置函数断点和条件断点,在此结合上一小节的代码,具体介绍后两种设置断点的方法。

Gdb中按函数设置断点只需把函数名列在命令”b”之后,如下所示:

要注意的是,此时的断点实际是在函数的定义处,也就是在16行处(注意第16行还未执行)。

Gdb中设置条件断点的格式为:b 行数或函数名 if 表达式。具体实例如下所示:

可以看到,该例中在第8行(也就是运行完第7行的for循环)设置了一个“i==0”的条件断点,在程序运行之后可以看出,程序确实在i为10时暂停运行。

3.Gdb中源码查看相关命令

在Gdb中可以查看源码以方便其他操作,它的常见相关命令如表3.12所示:

表3.12 Gdb源码查看相关相关命令

显示定义了的源文件搜索路径

显示加载到Gdb内存中的代码

4.Gdb中查看运行数据相关命令

Gdb中查看运行数据是指当程序处于“运行”或“暂停”状态时,可以查看的变量及表达式的信息,其常见命令如表3.13所示:

表3.13 Gdb查看运行数据相关命令

查看程序运行时对应表达式和变量的值

查看内存变量内容。其中n为整数表示显示内存的长度,f表示显示的格式,u表示从当前地址往后请求显示的字节数

设定在单步运行或其他情况中,自动显示的对应表达式的内容

5.Gdb中修改运行参数相关命令

Gdb还可以修改运行时的参数,并使该变量按照用户当前输入的值继续运行。它的设置方法为:在单步执行的过程中,键入命令“set 变量=设定值”。这样,在此之后,程序就会按照该设定的值运行了。下面,笔者结合上一节的代码将n的初始值设为4,其代码如下所示:

可以看到,最后的运行结果确实比之前的值大了4。

· 在Gcc编译选项中一定要加入”-g”。

· 只有在代码处于“运行”或“暂停”状态时才能查看变量值。

· 设置断点后程序在指定行之前停止。

到此为止,读者已经了解了如何在Linux下使用编辑器编写代码,如何使用Gcc把代码编译成可执行文件,还学习了如何使用Gdb来调试程序,那么,所有的工作看似已经完成了,为什么还需要Make这个工程管理器呢?

所谓工程管理器,顾名思义,是指管理较多的文件的。读者可以试想一下,有一个上百个文件的代码构成的项目,如果其中只有一个或少数几个文件进行了修改,按照之前所学的Gcc编译工具,就不得不把这所有的文件重新编译一遍,因为编译器并不知道哪些文件是最近更新的,而只知道需要包含这些文件才能把源代码编译成可执行文件,于是,程序员就不能不再重新输入数目如此庞大的文件名以完成最后的编译工作。

但是,请读者仔细回想一下本书在3.1.2节中所阐述的编译过程,编译过程是分为编译、汇编、链接不同阶段的,其中编译阶段仅检查语法错误以及函数与变量的声明是否正确声明了,在链接阶段则主要完成是函数链接和全局变量的链接。因此,那些没有改动的源代码根本不需要重新编译,而只要把它们重新链接进去就可以了。所以,人们就希望有一个工程管理器能够自动识别更新了的文件代码,同时又不需要重复输入冗长的命令行,这样,Make工程管理器也就应运而生了。

实际上,Make工程管理器也就是个“自动编译管理器”,这里的“自动”是指它能够根据文件时间戳自动发现更新过的文件而减少编译的工作量,同时,它通过读入Makefile文件的内容来执行大量的编译工作。用户只需编写一次简单的编译语句就可以了。它大大提高了实际项目的工作效率,而且几乎所有Linux下的项目编程均会涉及到它,希望读者能够认真学习本节内容。

Makefile是Make读入的惟一配置文件,因此本节的内容实际就是讲述Makefile的编写规则。在一个Makefile中通常包含如下内容:

· 需要由make工具创建的目标体(target),通常是目标文件或可执行文件;

· 创建每个目标体时需要运行的命令(command)。

例如,有两个文件分别为hello.c和hello.h,创建的目标体为hello.o,执行的命令为gcc编译指令:gcc –c hello.c,那么,对应的Makefile就可以写为:

接着就可以使用make了。使用make的格式为:make target,这样make就会自动读入Makefile(也可以是首字母小写makefile)并执行对应target的command语句,并会找到相应的依赖文件。如下所示:

可以看到,Makefile执行了“hello.o”对应的命令语句,并生成了“hello.o”目标体。

在Makefile中的每一个command前必须有“Tab”符,否则在运行make命令时会出错。

上面示例的Makefile在实际中是几乎不存在的,因为它过于简单,仅包含两个文件和一个命令,在这种情况下完全不必要编写Makefile而只需在Shell中直接输入即可,在实际中使用的Makefile往往是包含很多的文件和命令的,这也是Makefile产生的原因。下面就可给出稍微复杂一些的Makefile进行讲解:

在这个Makefile中有三个目标体(target),分别为sunq、kang.o和yul.o,其中第一个目标体的依赖文件就是后两个目标体。如果用户使用命令“make sunq”,则make管理器就是找到sunq目标体开始执行。

这时,make会自动检查相关文件的时间戳。首先,在检查“kang.o”、“yul.o”和“sunq”三个文件的时间戳之前,它会向下查找那些把“kang.o”或“yul.o”做为目标文件的时间戳。比如,“kang.o”的依赖文件为:“kang.c”、“kang.h”、“head.h”。如果这些文件中任何一个的时间戳比“kang.o”新,则命令“gcc –Wall –O -g –c kang.o”将会执行,从而更新文件“kang.o”。在更新完“kang.o”或“yul.o”之后,make会检查最初的“kang.o”、“yul.o”和“sunq”三个文件,只要文件“kang.o”或“yul.o”中的任比文件时间戳比“sunq”新,则第二行命令就会被执行。这样,make就完成了自动检查时间戳的工作,开始执行编译工作。这也就是Make工作的基本流程。

接下来,为了进一步简化编辑和维护Makefile,make允许在Makefile中创建和使用变量。变量是在Makefile中定义的名字,用来代替一个文本字符串,该文本字符串称为该变量的值。在具体要求下,这些值可以代替目标体、依赖文件、命令以及makefile文件中其它部分。在Makefile中的变量定义有两种方式:一种是递归展开方式,另一种是简单方式。

递归展开方式定义的变量是在引用在该变量时进行替换的,即如果该变量包含了对其他变量的应用,则在引用该变量时一次性将内嵌的变量全部展开,虽然这种类型的变量能够很好地完成用户的指令,但是它也有严重的缺点,如不能在变量后追加内容(因为语句:CFLAGS = $(CFLAGS) -O在变量扩展过程中可能导致无穷循环)。

为了避免上述问题,简单扩展型变量的值在定义处展开,并且只展开一次,因此它不包含任何对其它变量的引用,从而消除变量的嵌套引用。

递归展开方式的定义格式为:VAR=var

简单扩展方式的定义格式为:VAR:=var

Make中的变量使用均使用格式为:$(VAR)

变量名是不包括“:”、“#”、“=”结尾空格的任何字符串。同时,变量名中包含字母、数字以及下划线以外的情况应尽量避免,因为它们可能在将来被赋予特别的含义。

变量名是大小写敏感的,例如变量名“foo”、“FOO”、和“Foo”代表不同的变量。

推荐在makefile内部使用小写字母作为变量名,预留大写字母作为控制隐含规则参数或用户重载命令选项参数的变量名。

下面给出了上例中用变量替换修改后的Makefile,这里用OBJS代替kang.o和yul.o,用CC代替Gcc,用CFLAGS代替“-Wall -O –g”。这样在以后修改时,就可以只修改变量定义,而不需要修改下面的定义实体,从而大大简化了Makefile维护的工作量。

经变量替换后的Makefile如下所示:

可以看到,此处变量是以递归展开方式定义的。

Makefile中的变量分为用户自定义变量、预定义变量、自动变量及环境变量。如上例中的OBJS就是用户自定义变量,自定义变量的值由用户自行设定,而预定义变量和自动变量为通常在Makefile都会出现的变量,其中部分有默认值,也就是常见的设定值,当然用户可以对其进行修改。

预定义变量包含了常见编译器、汇编器的名称及其编译选项。下表3.14列出了Makefile中常见预定义变量及其部分默认值。

库文件维护程序的名称,默认值为ar

汇编程序的名称,默认值为as

C编译器的名称,默认值为cc

C预编译器的名称,默认值为$(CC) –E

C++编译器的名称,默认值为g++

FORTRAN编译器的名称,默认值为f77

文件删除程序的名称,默认值为rm –f

库文件维护程序的选项,无默认值

汇编程序的选项,无默认值

C编译器的选项,无默认值

C预编译的选项,无默认值

C++编译器的选项,无默认值

FORTRAN编译器的选项,无默认值

可以看出,上例中的CC和CFLAGS是预定义变量,其中由于CC没有采用默认值,因此,需要把“CC=Gcc”明确列出来。

由于常见的Gcc编译语句中通常包含了目标文件和依赖文件,而这些文件在Makefile文件中目标体的一行已经有所体现,因此,为了进一步简化Makefile的编写,就引入了自动变量。自动变量通常可以代表编译语句中出现目标文件和依赖文件等,并且具有本地含义(即下一语句中出现的相同变量代表的是下一语句的目标文件和依赖文件)。下表3.15列出了Makefile中常见自动变量。

不包含扩展名的目标文件名称

所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件

所有时间戳比目标文件晚的依赖文件,并以空格分开

所有不重复的依赖文件,以空格分开

如果目标是归档成员,则该变量表示目标的归档成员名称

自动变量的书写比较难记,但是在熟练了之后会非常的方便,请读者结合下例中的自动变量改写的Makefile进行记忆。

另外,在Makefile中还可以使用环境变量。使用环境变量的方法相对比较简单,make在启动时会自动读取系统当前已经定义了的环境变量,并且会创建与之具有相同名称和数值的变量。但是,如果用户在Makefile中定义了相同名称的变量,那么用户自定义变量将会覆盖同名的环境变量。

Makefile的规则是Make进行处理的依据,它包括了目标体、依赖文件及其之间的命令语句。一般的,Makefile中的一条语句就是一个规则。在上面的例子中,都显示地指出了Makefile中的规则关系,如“$(CC) $(CFLAGS) -c $< -o $@”,但为了简化Makefile的编写,make还定义了隐式规则和模式规则,下面就分别对其进行讲解。

隐含规则能够告诉make怎样使用传统的技术完成任务,这样,当用户使用它们时就不必详细指定编译的具体细节,而只需把目标文件列出即可。Make会自动搜索隐式规则目录来确定如何生成目标文件。如上例就可以写成:

在隐式规则只能查找到相同文件名的不同后缀名文件,如”kang.o”文件必须由”kang.c”文件生成。

下表3.16给出了常见的隐式规则目录:

模式规则是用来定义相同处理规则的多个文件的。它不同于隐式规则,隐式规则仅仅能够用make默认的变量来进行操作,而模式规则还能引入用户自定义变量,为多个文件建立相同的规则,从而简化Makefile的编写。

模式规则的格式类似于普通规则,这个规则中的相关文件前必须用“%”标明。使用模式规则修改后的Makefile的编写如下:

使用make管理器非常简单,只需在make命令的后面键入目标名即可建立指定的目标,如果直接运行make,则建立Makefile中的第一个目标。

此外make还有丰富的命令行选项,可以完成各种不同的功能。下表3.17列出了常用的make命令行选项。

忽略所有的命令执行错误

指定被包含的Makefile所在目录

只打印要执行的命令,但不执行这些命令

显示make变量数据库和隐含规则

在执行命令时不显示命令

如果make在执行过程中改变目录,则打印当前目录名

在上一小节,读者已经了解到了make项目管理器的强大功能。的确,Makefile可以帮助make完成它的使命,但要承认的是,编写Makefile确实不是一件轻松的事,尤其对于一个较大的项目而言更是如此。那么,有没有一种轻松的手段生成Makefile而同时又能让用户享受make的优越性呢?本节要讲的autotools系列工具正是为此而设的,它只需用户输入简单的目标文件、依赖文件、文件目录等就可以轻松地生成Makefile了,这无疑是广大用户的所希望的。另外,这些工具还可以完成系统配置信息的收集,从而可以方便地处理各种移植性的问题。也正是基于此,现在Linux上的软件开发一般都用autotools来制作Makefile,读者在后面的讲述中就会了解到。

正如前面所言,autotools是系列工具,读者首先要确认系统是否装了以下工具(可以用which命令进行查看)。

使用autotools主要就是利用各个工具的脚本文件以生成最后的Makefile。其总体流程是这样的:

· 使用aclocal生成一个“aclocal.m4”文件,该文件主要处理本地的宏定义;

接下来,笔者将通过一个简单的hello.c例子带领读者熟悉autotools生成makefile的过程,由于在这过程中有涉及到较多的脚本文件,为了更清楚地了解相互之间的关系,强烈建议读者实际动手操作以体会其整个过程。

它会在给定目录及其子目录树中检查源文件,若没有给出目录,就在当前目录及其子目录树中进行检查。它会搜索源文件以寻找一般的移植性问题并创建一个文件“configure.scan”,该文件就是接下来autoconf要用到的“configure.in”原型。如下所示:

如上所示,autoscan首先会尝试去读入“configure.ac”(同configure.in的配置文件)文件,此时还没有创建该配置文件,于是它会自动生成一个“configure.in”的原型文件“configure.scan”。

下面对这个脚本文件进行解释:

· 以“#”号开始的行为注释。

· AM_INIT_AUTOMAKE是笔者另加的,它是automake所必备的宏,也同前面一样,PACKAGE是所要产生软件套件的名称,VERSION是版本编号。

· AC_CONFIG_SRCDIR宏用来侦测所指定的源码文件是否存在,来确定源码目录的有

效性。在此处为当前目录下的hello.c。

· 中间的注释间可以添加分别用户测试程序、测试函数库、测试头文件等宏定义。

接下来首先运行aclocal,生成一个“aclocal.m4”文件,该文件主要处理本地的宏定义。如下所示:

再接着运行autoconf,生成“configure”可执行文件。如下所示:

接着使用autoheader命令,它负责生成config.h.in文件。该工具通常会从“acconfig.h”文件中复制用户附加的符号定义,因此此处没有附加符号定义,所以不需要创建“acconfig.h”文件。如下所示:

这一步是创建Makefile很重要的一步,automake要用的脚本配置文件是Makefile.am,用户需要自己创建相应的文件。之后,automake工具转换成Makefile.in。在该例中,笔者创建的文件为Makefile.am如下所示:

下面对该脚本文件的对应项进行解释。

· 其中的AUTOMAKE_OPTIONS为设置automake的选项。由于GNU(在第1章中已经有所介绍)对自己发布的软件有严格的规范,比如必须附带许可证声明文件COPYING等,否则automake执行时会报错。automake提供了三种软件等级:foreign、gnu和gnits,让用户选择采用,默认等级为gnu。在本例使用foreign等级,它只检测必须的文件。

· bin_PROGRAMS定义要产生的执行文件名。如果要产生多个执行文件,每个文件名用空格隔开。

· hello_SOURCES定义“hello”这个执行程序所需要的原始文件。如果”hello”这个程序是由多个原始文件所产生的,则必须把它所用到的所有原始文件都列出来,并用空格隔开。例如:若目标体“hello”需要“hello.c”、“sunq.c”、“hello.h”三个依赖文件,则定义hello_SOURCES=hello.c sunq.c hello.h。要注意的是,如果要定义多个执行文件,则对每个执行程序都要定义相应的file_SOURCES。

接下来可以使用automake对其生成“configure.in”文件,在这里使用选项“—adding-missing”可以让automake自动添加有一些必需的脚本文件。如下所示:

在这一步中,通过运行自动配置设置文件configure,把Makefile.in变成了最终的Makefile。如下所示:

可以看到,在运行configure时收集了系统的信息,用户可以在configure命令中对其进行方便地配置。在./configure的自定义参数有两种,一种是开关式(--enable-XXX或--disable-XXX),另一种是开放式,即后面要填入一串字符(--with-XXX=yyyy)参数。读者可以自行尝试其使用方法。另外,读者可以查看同一目录下的”config.log”文件,以方便调试之用。

到此为止,makefile就可以自动生成了。回忆整个步骤,用户不再需要定制不同的规则,而只需要输入简单的文件及目录名即可,这样就大大方便了用户的使用。下面的图3.9总结了上述过程:

autotools生成的Makefile除具有普通的编译功能外,还具有以下主要功能(感兴趣的读者可以查看这个简单的hello.c程序的makefile):

键入make默认执行”make all”命令,即目标体为all,其执行情况如下所示:

此时在本目录下就生成了可执行文件“hello”,运行“./hello”能出现正常结果,如下所示:

此时,会把该程序安装到系统目录中去,如下所示:

此时,若直接运行hello,也能出现正确结果,如下所示:

此时,make会清除之前所编译的可执行文件及目标文件(object file, *.o),如下所示:

此时,make将程序和相关的文档打包为一个压缩文档以供发布,如下所示:

由上面的讲述读者不难看出,autotools确实是软件维护与发布的必备工具,也鉴于此,如今GUN的软件一般都是由automake来制作的。

对于automake制作的这类软件,应如何安装呢?

通过指定指令的Vi操作练习,使读者能够熟练使用Vi中的常见操作,并且熟悉Vi的三种模式,如果读者能够熟练掌握实验内容中所要求的内容,则表明对Vi的操作已经很熟练了。

(1)在“/root”目录下建一个名为“/Vi”的目录。

(2)进入“/Vi”目录。

(6)将光标移到该行。

(8)将光标移到最后一行行首。

(9)粘贴复制行的内容。

(10)撤销第9步的动作。

(11)将光标移动到最后一行的行尾。

(12)粘贴复制行的内容。

(15)存盘但不退出。

(16)将光标移到首行。

(18)返回命令行模式。

(19)向下查找字符串“0:wait”。

(20)再向上查找字符串“halt”。

(21)强制退出Vi,不存盘。

分别指出每个命令处于何种模式下?

(15):w(底行模式)

(21):q!(底行模式)

用Gdb调试有问题的程序

通过调试一个有问题的程序,使读者进一步熟练使用Vi操作,而且熟练掌握Gcc编译命令及Gdb的调试命令,通过对有问题程序的跟踪调试,进一步提高发现问题和解决问题的能力。这是一个很小的程序,只有35行,希望读者认真调试。

(1)使用Vi编辑器,将以下代码输入到名为greet.c的文件中。此代码的原意为输出倒序main函数中定义的字符串,但结果显示没有输出。代码如下所示:

(2)使用Gcc编译这段代码,注意要加上“-g”选项以方便之后的调试。

(3)运行生成的可执行文件,观察运行结果。

(4)使用Gdb调试程序,通过设置断点、单步跟踪,一步步找出错误所在。

(5)纠正错误,更改源程序并得到正确的结果。

(2)在Vi中输入以上代码。

(3)在Vi中保存并退出:wq。

可见,该程序没有能够倒序输出。

(7)查看源代码,使用命令“l”。

(8)在30行(for循环处)设置断点,使用命令“b 30”。

(9)在33行(printf函数处)设置断点,使用命令“b 33”。

(10)查看断点设置情况,使用命令“info b”。

(11)运行代码,使用命令“r”。

(12)单步运行代码,使用命令“n”。

(14)继续单步运行代码数次,并使用命令查看,发现string2[size-1]的值正确。

(15)继续程序的运行,使用命令“c”。

(16)程序在printf前停止运行,此时依次查看string2[0]、string2[1]…,发现string[0]没有被正确赋值,而后面的复制都是正确的,这时,定位程序第31行,发现程序运行结果错误的原因在于“size-1”。由于i只能增到“size-1”,这样string2[0]就永远不能被赋值而保持NULL,故输不出任何结果。

(17)退出Gdb,使用命令q。

将原来有错的程序经过Gdb调试,找出问题所在,并修改源代码,输出正确的倒序显示字符串的结果。

通过对包含多文件的Makefile的编写,熟悉各种形式的Makefile,并且进一步加深对Makefile中用户自定义变量、自动变量及预定义变量的理解。

(1)用Vi在同一目录下编辑两个简单的Hello程序,如下所示:

(2)仍在同一目录下用Vi编辑Makefile,且不使用变量替换,用一个目标体实现(即直接将hello.c和hello.h编译成hello目标体)。然后用make验证所编写的Makefile是否正确。

(3)将上述Makefile使用变量替换实现。同样用make验证所编写的Makefile是否正确

(5)将上述Makefile1使用变量替换实现。

(1)用Vi打开上述两个代码文件“hello.c”和“hello.h”。

(2)在shell命令行中用Gcc尝试编译,使用命令:”Gcc hello.c –o hello”,并运行hello可执行文件查看结果。

(3)删除此次编译的可执行文件:rm hello。

(5)退出保存,在shell中键入:make,查看结果。

(6)再次用Vi打开Makefile,用变量进行替换,如下所示:

(7)退出保存,在shell中键入:make,查看结果。

在这里请注意区别“$^”和“$<”。

各种不同形式的makefile都能完成其正确的功能。

通过使用autotools生成包含多文件的Makefile,进一步掌握autotools的正确使用方法。同时,掌握Linux下安装软件的常用方法。

(1)在原目录下新建文件夹auto。

(2)利用上例的两个代码文件“hello.c”和“hello.h”,并将它们复制到该目录下。

(11)使用make生成hello可执行文件,并在当前目录下运行hello查看结果。

(12)使用make install将hello安装到系统目录下,并运行,查看结果。

(14)解压hello压缩包。

(15)进入解压目录。

(16)在该目录下安装hello软件。

(7)运行:autoconf,并用ls查看是否生成了configure可执行文件。

(13)运行:./hello,查看结果是否正确。

(15)运行:hello,查看结果是否正确。

(21)运行:./hello(在正常安装时这一步可省略)。

(23)运行:hello,查看结果是否正确。

能够正确使用autotools生成Makefile,并且能够安装成功短小的Hello软件。

这种方法主要是利用 gadgets 和 string 来组成注入的代码。具体来说是使用 popmov 指令加上某些常数来执行特定的操作。也就是说,利用程序已有的代码,重新组合成我们需要的东西,这样就绕开了系统的防御机制。

所以在这里我们其实不需要关心如何在 buffer 中运行我们的代码,而只需要知道 buffer 的 size,从而改写返回地址,即可以利用程序中原有的代码进行我们的操作。

在这个例子中,因为 address2 中的代码是把栈顶的值弹出到 %rbx 中,所以执行的时候,就会把 0xBBBBBBBB 放到 %rbx 中,现在程序就指向 address1 了,然后就会继续执行 address1,也就达到我们的目的,把 0xBBBBBBBB 放到了

ctargetrtarget 都会从标准输入中读取字符串,然后保存在一个大小为 BUFFER_SIZE 的 char 数组中(具体的大小每个人的程序都不大一样)。我们可以通过两次输入测试来看看程序具体的行为,一次是正常输入,第二次会输入超出 BUFFER_SIZE 个数的字符串。

可以看到这里把 %rsp 移动了 0x28(40) 位,也就是说,我们的缓冲区有 40 位,再上面的四位就是原来正常需要返回到 test 的返回地址(注意看之前的栈帧图),我们要做的就是利用缓冲区溢出把这个返回地址改掉。

前四十位是啥都不重要,后面四位按照 little endian 的规则逆向填上地址就好(注意这里为了排版用了换行,实际上都应该在一行,用空格分开),这样就改写了属于原来的返回地址。

那么现在问题来了,我们要如何才能让机器开始执行这几行代码呢?简单,利用第一阶段的方式,跳转到缓冲区所在的位置即可,那么问题又来了,缓冲区的位置在哪里呢?这个就需要实际跑一次程序,用 gdb 查看了。

和上次的实验一样 gdb ctarget 开始调试,因为我想知道缓冲区从哪里开始,所以在 getbuf 中看看 %rsp 的值即可,我们在 0x401828 处设置断点,然后查看对应寄存器的值:

我们可以看到,和第二阶段的差别在于,这里会调用另一个函数来进行检验,而且传入一个字符串的话,是传入一个地址,并且字符串需要以 0 结尾(查找 ascii 码表来确定),还有一个要注意的地方是,调用 hexmatchstrncmp 时会把数据存入栈中,也就是会覆盖一部分 getbuf 的缓冲区,所以要看看到底需要把传入的字符串放到哪里。

因为知道在调用 hexmatch 的时候会覆盖缓冲区,所以需要找到一个位置来放这八个字符。光看代码比较难懂,不妨直接上手实验一下,我们需要知道的是到底覆盖了多少,所以从 touch3 入手:

可以看到在 0x401985 的时候调用了 hexmatch,所以我们只要在前一句和后一句各设置一个断点,看看缓冲区有没有什么变化(这里稍微改了一下第二阶段的字节码用作测试)

可以看到在调用 hexmatch 之前我们的缓冲区一切正常,主要留意 0x 这里,保存着我们的 cookie,其他部分其实已经执行了,所以反而无所谓。

这就出问题了,我们之前存放在 0x 的传入参数给弄没了,而且可以看到从缓冲区开始 0x 到缓冲区结束 0x 都不安全。所以我们得给字符串找个新家,不会被覆盖的新家。

仔细观察 0x 之后的内容,在 0x00401f94 之后有几个空位置,刚好放得下我们的字符串。为了保证格式的一致,我们需要溢出到 0x 的位置(当然前一个也可以,不过我选择的位置换行了,比较容易看)

那么现在怎么办呢?可以利用已有的可执行的代码,来完成我们的操作,称为 retrun-oriented programming(ROP),策略就是找到现存代码中的若干条指令,这些指令后面跟着指令 ret,如下图所示

每次 return 相当于从一个 gadget 跳转到另一个 gadget 中,然后通过这样不断跳转来完成我们想要的操作。举个具体的例子,假设程序中有一个像下面这样的函数:

这里 48 89 c7 就编码了 movq %rax, %rdi 指令(参加后面的表格),后面跟着一个 c3(也就是返回),于是这段代码就包含一个 gadget,起始地址是 0x400f18,我们就可以利用这个来做一些事情了。

这个阶段我们需要重复之前第二阶段的工作,但是因为程序的限制,只能另辟蹊径了,这里我们只需要利用下表给出的指令类型,以及前八个寄存器(%rax - %rdi)。表格如下:

结合上表,我们想要插入一个数字,肯定需要 popq 指令,对应下来就是 58 - 5f 这个范围,因为 ROP 的缘故,我们还需要后面有个 c3,经过搜索,可以看到在 addval_104 中,有一段 58 c3,也就是把栈中的值弹入到

现在我们要做的就是把存放在 %rax 的值放到 %rdi 中,因为这样才能当做参数传给 touch2 函数。根据表里的内容,继续找,这次的目标是 48 89 c7,也就是 movq %rax, %rdi,很幸运,又在 getval_341 中找到了,后面还正好跟了个 c3,赶紧记下这个地址 0x401a2b

但是这样居然会遇到段错误,这是我万万没想到的,问题出在哪里呢?我尝试把这四条语句拆开来执行,发现第一句和第四句没问题,但是中间两句有问题。这说明了一个问题,就是某条语句的执行依赖于后面的语句,再联想到这是 64 位的机器,就明白了为什么会出现段错误了,应该在每个语句后面补 0,那么好,修正之后的字符串是

接下来到最后一个阶段,其实做的工作是类似的,就是需要把 cookie 转换成 ascii 码通过缓冲区溢出放到栈的某个位置,然后把指向这个字符串的指针放到 %rdi 中,最后调用 touch3 即可。给出的提示是使用 movl(对前四位进行操作)和诸如 andb %al,%al 的指令(只对低2位的部分操作),标准答案中最少需要使用 8 个 gadget。

 

对应的十六进制代码为(同样需要注意不全十六位的 0,不然会出段错误),这里还有一个需要注意的地方是偏移量,在执行第一句时,%rsp 已经是指向下一句了(指向的是当前的栈顶,正在执行的语句是不需要考虑的),所以可以数出来,在 cookie 之前一共有 9 条指令,每个 8 byte,所以一共的偏移量是 0x48(十进制的 72)。

这次作业的两个部分,有不同的难点。利用缓冲区溢出跳转到栈中并在栈中执行代码虽然需要的步骤多一些,但是调试还是比较方便的,可以走一步看一步,根据具体的内存分布来进行处理,就是第三阶段的随机部分可能需要多试几次才能找到正确的存放位置。

ROP 的部分,因为跳转来跳转去,难点在于思路,有了一个大概的思路,就可以利用已有的代码跳来跳去来『凑』出最终的结果了。最后部分需要考虑到偏移量的问题,需要对 %rsp 具体所指向的内存位置有比较清晰地了解,这里我有点犯迷糊,在同学的帮助下才找到了问题所在。不同的字长和位数也有影响,虽然大概的意思差不多,不过我看前一两年的作业中的汇编代码,就和现在的汇编代码有挺大的差异了。

越接近硬件层面,越容不得丝毫差池,越来越多的数值和偏移都变得和机器相关,才更加意识到现在能写几乎与机器无关的代码是多么幸福。不过也不能因为前人的工作就忽略不同机器的差异,还是要多考虑不同的层面,才能写出让更多机器能跑得更快的代码。

我要回帖

更多关于 小V机器人 的文章

 

随机推荐