linux开发过程

 

书籍《Linux高级程序设计》    Jon Masters,Richard Blum,陈健译

 

Linux高级程序设计》第2章工具链,这一章将向大家介绍GNU工具链中的各种工具,以及其他一些相关的工具——它们也被Linux开发人员用于编译和调试应用程序,本节为您介绍Linux开发过程。

 

第2章 工具链

工具链(toolchain)是在每一个大型开放源码项目(包括Linux内核本身)背后默默支撑的力量。它们由一组必要的工具和软件构成,用于编译和调试从最小的工具软件到你可以想象的最复杂的具有Linux内核特征的各种软件。如果你曾经编写过Linux程序,那么你很可能已用过了GNU编译器集(GCC),但要完成一个优秀的应用程序,要做的事情可比简单的编译源代码多得多,你需要借助一个完整的工具集来做到这一点,这套工具集通常被称为工具链。

工具链中包括编译器、连接器、汇编器以及调试器——用于跟踪所有程序(除了那些非常简单的程序)中的不可避免的错误。此外,还有各种其他的工具用于在必要的时候控制应用程序的二进制代码——例如,将Linux内核的二进制代码转换为机器的启动映像。绝大多数的Linux应用程序都使用GNU的工具链来编译,该工具链由GNU工程中发行的工具构成。

本章将向你介绍GNU工具链中的各种工具,以及其他一些相关的工具——它们也被Linux开发人员用于编译和调试应用程序。这些工具包含了许多非标准的特性,它们常被Linux应用程序以及内核使用。你将学习如何使用GNU工具链并熟悉它们的一些更高级的特性。在阅读完本章之后,你将具备编译和调试应用程序的能力,并将熟悉诸联汇编的概念和GNU二进制工具集(binutil)等的强大功能。

2.1  Linux开发过程

要充分了解Linux中用于编译软件的每个工具,首先必须对整个软件开发过程以及工具链中每个工具的设计目的有一个高层次的理解。只有这样才能在本章后面实验这些工具时能够更容易地将这些概念应用到它们的高级应用中去。

现代Linux软件由大量单个的组件构成,它们在编译时被合并到少量的可执行程序和其他文件中。这包括应用程序的可执行文件本身,以及许多配套资源、文档和用于源代码管理(SCM)和版本控制的额外数据。这些单个的文件可能被包装进一个单独的面向特定发行版的可安装软件包中,然后将该软件包发送给用户。

当然,某些应用程序可能并没有设计为最终用户可安装,这通常发生在嵌入式设备或其他OEM解决方案中。在这些情况下,运行Linux的整个系统只是作为一个大型产品的一部分,你仍然以标准方式编写和编译软件,但需要将包装过程替换为一个自动化过程以将软件安装到目标设备上。

在本节中,你将学习如何使用源代码、如何获取它,以及如何将一组程序源代码配置为一个可工作的开发树。你还将开始研究一个通用的编译过程,Linux系统中大多数的应用程序都使用这一过程,你在编写自己的应用程序时也需要熟悉它。

 

 

2.1.1 使用源代码

 

2.1.1  使用源代码

开发人员很少直接与最终用户可安装的软件包打交道。相反,他们使用的是源代码软件包或包含应用程序源代码以及重新编译应用程序所必需的额外脚本和配置数据的打包文件。这些外部支持脚本和配置数据用于自动判断软件需求以及那些在指定Linux目标环境中使用该软件所必需具备的特性。

Linux软件通常以两种方式分发给开发人员。第一种(也是最基本的一种)方式是源代码打包文件,它常被称为tarball文件(这是因为标准的压缩打包文件的后缀名为.tar.gz或.tar.bz2)。源代码打包文件在使用标准过程编译它之前必须首先被解包,这可以通过使用合适的工具如命令行的tar命令或图形化打包文件管理工具(由你的图形化桌面环境提供)来完成。
第二种分发源代码给开发人员的方式是通过SCM工具。这些工具将获取源代码、跟踪本地修改、提交补丁或源代码的修改版本到中心库或更高一层开发人员的过程变得自动化。这些工具的选择将在本书后面进行讨论。如果你确实要使用SCM,你就必须遵循在开发团队的成员之间约定好的步骤和方法。

你将发现本书所提供的大多数示例代码都存放在Linux的普通tar打包文件中,当然,你也可以很容易地将这些示例代码输入到自己的SCM并跟踪你对它们所作的改动(见第4章“软件配置管理”)。要解开包含本章示例代码的打包文件,你可以使用如下所示的Linux tar命令:

 

  

"xvfj"选项标记告诉标准的tar命令释放给定的以bzip2格式压缩的tar打包文件并进行验证。如果文件是存储在老式的但仍然被广泛使用的gzip格式压缩的tar打包文件(通常使用.tar.gz后缀名)中,你就需要使用"xvfz"选项标记来解压缩它。

 

 

2.1.2 配置本地环境

 

2.1.2  配置本地环境

我们所提供的示例项目打包文件中包含一个顶层目录和其他子目录。顶层目录中的README文件和INSTALL文件描述了(重新)编译软件的标准过程。这一过程首先需要配置本地编译环境,然后才是真正地运行命令来编译一个可执行应用程序。

当为Linux开发软件时,配置本地编译环境是一个必要的步骤,因为Linux可以运行在各种不同的目标平台上。每个不同的硬件环境可能都有其特定的限制。例如,内存的存储格式是大头字节序(big endian)还是小头字节序(little endian),或对要运行在指定目标处理器上的可执行程序有特殊的要求。而且每个不同的Linux发行版都有各自不同的软件环境——工具和系统函数库的版本随发行版的不同而不同。

因为只有极少数的Linux(或其他UNIX)环境是相同的,所以有必要通过一套工具以一种可移植和抽象的方式来处理这些差别。我们感兴趣的第一个工具是GNU Autoconf,它通过其生成的configure脚本来体现其功能。当执行configure脚本文件时,它将自动判断系统上是否已安装了必需的编译工具,并判断其版本是否有一些特殊的需求。

你可以执行如下命令来获得configure脚本的使用帮助:

 

为简洁起见,对命令的输出做了删减。

根据configure脚本自身的配置情况,它的输出列出了各种可能的选项和值。其中大多数选项在这里都没有必要使用,因为本例中的configure脚本只是用来判断你是否正确地安装了必需的GNU工具链来运行本章中的代码。本书后面的示例,如下一章中的一些示例将利用Autoconf中更强大的功能,并讨论其内部细节。

请运行本章源代码包中包括的configure脚本来配置示例代码:

 
 


configure脚本将自动判断你是否已安装了必需的GNU工具,并通过实际编译一个非常简单的test程序来验证它们是否可以正常工作。你将在本书的后面学习如何编写自己的configure脚本。

根据所使用的工作站的具体环境的不同,输出可能与上面列出的有所不同。如果在这一阶段出现任何错误,就说明你没有在系统上安装适当的工具。许多Linux发行版不再在它们的安装过程中自动安装完整的GNU工具链,所以需要你来安装这些缺少的开发工具。发行商通常会在安装过程中提供一个“开发”(Development)选项,选择它就会安装正确的开发工具(请参考第1章以了解更多关于如何获得一个基于正确配置的Linux系统的信息)。

 

 

2.1.3 编译源代码

 

2.1.3  编译源代码

示例源代码中的README文件对示例代码的作用进行了概述,它然后指向内容更详细的INSTALL文件,该文件描述了如何编译和安装示例源代码到你的工作站上。与所有其他Linux软件一样,文档还说明了当你不需要该软件时,应该如何删除它。

你可以使用如下命令来编译和测试示例代码:

   

和大多数类UNIX系统一样,Linux上的大多数软件是在GNU make的控制下进行编译的。这个便利的工具读取Makefile文件,并使用它来确定调用命令的顺序以及从指定的源代码(存储在示例源代码目录的src子目录下)产生可执行程序的步骤。GNU make通过几条特殊的指令就能理解如何编译大多数简单的可执行程序,但这也并不是绝对的。

make调用命令的正确顺序是由在Makefile文件中指定的依赖信息确定的。在本例中,源代码文件hello.c将被自动编译为可执行程序hello,然后你就可以执行它。make允许你根据期望的动作定义多个不同的make目标。例如,你可以使用命令"make clean"或"make distclean"来清理已准备好分发给开发人员的源代码。

示例Makefile文件包含如下所示的依赖规则:

  


没有必要告诉GNU make应该如何将源文件hello.c编译成一个可执行程序。因为它清楚一个C语言源文件依赖关系意味着需要调用GNU C编译器和其他一些必需的工具。事实上,GNU make在默认情况下支持各种文件后缀名,它通过这些后缀名来确定针对不同类型源代码所应采用的默认规则。

在某些情况下,Makefile文件可能也是由其他自动化工具产生的。为了产生可以广泛移植到各种不同类型的目标Linux机器上的软件,我们通常会将Makefile文件的生成作为configure脚本生成过程的一部分来完成。Makefile然后会自动使用由configure脚本检测到的特定GNU工具并根据在配置过程中其他一些选项的情况采取操作,例如使用一个特定的交叉编译器来为运行在一个不同处理器架构下的目标编译软件。

自由软件基金会建议总是使用这种自动配置工具,因为它们降低了日常Linux软件编译的复杂度。而且作为这样使用的结果,你也不需要了解编译和使用大多数Linux应用程序所需的资源。更重要的是,对于那些想要从源代码开始编译应用程序的用户来说,这个道理也同样适用。

 

 

2.2 GNU工具链的组成(编译单个源文件)

 

 

2.2  GNU工具链的组成(编译单个源文件)

上一节介绍了几乎通用的命令序列"configure,make",它用于编译大多数的Linux软件。正如你看到的,通过将GNU Autoconf和GNU make进行适当的结合,也许只需使用数条短短的命令就可以将源代码编译为一个应用程序。你将在本章后面发现我们可以通过使用一个图形化桌面环境如Eclipse将整个编译周期简化为点击一个按钮来完成。

自动化是软件开发的一个重要组成部分,因为它不仅简化了重复编译周期,而且通过清除出错的可能性增强了再现性。许多开发人员(包括作者)都是好不容易才懂得尽可能地简化Linux软件的编译过程是多么的重要。他们往往在浪费了一整天去调试根本就不存在的bug之后才发现这其实是由于编译过程过于复杂所产生的副作用。

虽然应该尽量使用手边的自动化工具,但仅仅依赖它们并不是一个好的编程习惯。对GNU工具链中的每个组成部分都有一个大概的了解是非常重要的,这样你就可以正确地处理自动化工具不能帮助你的情况了。因此,本章的其余部分将侧重于介绍隐藏在编译过程之后的每个工具,告诉你它们是做什么的,以及它们是如何纳入GNU工具集整体的。

GNU编译器集

GNU编译器集(其前身为GNU C编译器)诞生于1987年。当时Richard Stallman(GNU项目的创办人)想要创建一个编译器,它可以满足他定义的“自由软件”概念,并可用来编译GNU项目发布的其他软件。GNU C编译器迅速在自由软件社区中流行开来,而且以其健壮性和可移植性而闻名。它已成为许多集成开发工具的基础,被世界各地的发行商应用在Linux和其他操作系统之上。

GCC已不再是主要针对GNU项目自身的软件的小型C语言编译器了。如今,它已支持了许多不同的语言,包括C、C++、Ada、Fortran、Objective C,甚至还有Java。事实上,现代Linux系统除了可以自豪地炫耀那些由GNU工具直接支持的语言以外,它还支持大量其他语言。日益流行的脚本语言Perl、Python和Ruby,以及正在不断发展的mono 可移植C#实现的确有助于冲淡人们对Linux编程的传统看法,但这完全是另外一个问题了。

本章重点介绍GCC作为C编译器的用法。Linux内核和许多其他自由软件以及开放源码应用程序都是用C语言编写并使用GCC编译的。因此,即便你不准备直接在项目中使用GCC或将GCC用于其他用途,你最好也能对本章所介绍的概念有一定程度的理解。通过它们自己的文档和第三方在线资源的帮助,你应该能将本章所介绍的一般概念应用到其他类似的语言工具中。

除了其广泛的语言支持以外,GCC受到许多开发人员的喜爱还因为它已被移植到广泛的硬件目标(基于不同处理器架构的机器)上。标准的GCC版本支持超过20种微处理器,包括用在许多工作站和服务器中的Intel IA32(“x86”)和AMD64处理器。GCC还支持高端的SPARC和POWER/PowerPC处理器,以及越来越多地专用嵌入式微处理器——用于使用Linux的小装置和其他设备。只要商业上允许,GCC可能还可以为它们编译代码。

1. 编译单个源文件

正如你已看到的,在Linux上编译软件的过程涉及许多工作在一起的不同工具。GCC是一个非常灵活和强大的现代C编译器,但不管怎么说,它终究只是一个C编译器。它非常擅于解析C源文件并为特定的处理器输出汇编语言代码,但由于它缺少一个针对某一特定机器的内置汇编器或连接器,所以它自身并无法真正产生一个可工作的可执行程序。

这个设计是有意而为之的。它遵循了UNIX哲学,即要做一件事,就把这件事做好。GCC支持各种不同的目标处理器,但它需要依赖外部工具来完成汇编和连接工作,具体步骤是首先由汇编器将已编译的源代码转换为目标代码,然后由连接器将它连接进一个合适的容器文件中,该文件可以被特定的Linux目标机器真正装载并执行。

虽然GCC自身并不能产生一个可工作的可执行程序,但它知道要真正实现这一目标还需要哪些GNU工具的帮助。作为GCC前端的gcc驱动程序,知道一个C语言源文件必须经过GNU汇编器和连接工具的汇编和连接,因此,当你递交给它一个C语言源文件要求编译时,它在默认情况下就会执行这些额外步骤。

为了进行测试,你可以创建“Hello World”程序:

 
  
使用如下命令编译并测试这个代码:
 

在默认情况下,gcc驱动程序会执行所有必需的步骤以将hello.c源文件编译为一个可执行二进制程序。这包括调用作为GCC内的一部分的真正的C编译器(cc1),以及调用从GNU C编译器的输出中实际产生可执行代码的外部GNU汇编器和连接器工具。由于历史原因,在默认情况下产生的可执行程序名为a.out,但你通常可以通过gcc的“-o”选项来指定自己的可执行程序名称,如上例所示。

 

2.2 GNU工具链的组成(编译多个源文件)

 

2.2  GNU工具链的组成(编译多个源文件)

2. 编译多个源文件

大多数应用程序是基于多个单个源代码文件的,它们被单独编译,然后连接在一起以构成最终的可执行程序。这既简化了开发过程并允许不同的团队开发一个项目的不同部分,同时也鼓励适当地进行代码复用。在本书的后面,你将学习到Linux内核是如何由数以千计的单个源文件构成,以及图形化GNOME桌面应用程序是如何通过许多单独的组件编译而成的,但就目前而言,我们首先来学习如何将两个源文件编译为一个单独的程序。
gcc驱动程序不仅懂得如何将单个源文件编译为一个可执行程序,而且它通过适当地调用GNU连接器,还可以将多个不同的目标文件连接在一起。当你指定多个目标文件(.o)作为GCC的输入参数时,GCC会自动将它们连接成一个单独的可执行输出文件。为了对此进行测试,你可以创建一个由两个单独的源文件组成的程序,它们将被连接成一个单独的可执行文件。

源文件message.c包含一个简单的消息打印函数:

   

本例中的goodbye_world函数构成了一个简单的软件库,它可以被其他源代码调用。这个函数依赖标准的C库例程printf来真正执行必需的底层IO以在终端上打印一条消息。C函数库printf将在稍后的连接阶段调入程序。由于这段代码并没有构成一个完整的程序(没有main函数),所以如果你试图像在前一个例子中那样编译并连接它,GCC将会报错,如下所示:

   

相反,我们需要告诉GCC不要执行任何额外的连接操作,而是使用GNU汇编器将源文件转换为目标代码后就结束。因为在这种情况下连接器并没有被执行,所以输出的目标文件不会包含作为一个Linux程序在被装载和执行时所必须包含的信息,但它可以在以后被连接到一个程序。
使用gcc的“-c”标记来编译支持库代码:

这将告诉gcc驱动程序调用它内部的C编译器并将其输出传递给外部的GNU汇编器。这一过程的输出结果是一个名为message.o的文件,它包含适合连接到一个较大程序的已编译目标代码。

为了在一个较大的程序中使用消息函数goodbye_world,可以创建一个简单的示例包裹程序,它包含一个调用goodbye_world的main函数。

这个文件将外部消息打印函数声明为一个不带参数的void函数。如你所知,这样的声明是必要的,否则GCC将假设任何未声明的外部函数为integer类型并隐含地将它们声明为这样。这可能会导致在编译时产生不必要的警告。你通常会遵循最佳的编程习惯,就像你以前做的那样,将这些定义放入自己的头文件中。

使用GCC编译这个包裹程序:

  


这将告诉gcc驱动程序调用它内部的C编译器并将其输出传递给外部的GNU汇编器。这一过程的输出结果是一个名为message.o的文件,它包含适合连接到一个较大程序的已编译目标代码。

为了在一个较大的程序中使用消息函数goodbye_world,可以创建一个简单的示例包裹程序,它包含一个调用goodbye_world的main函数。

 
这个文件将外部消息打印函数声明为一个不带参数的void函数。如你所知,这样的声明是必要的,否则GCC将假设任何未声明的外部函数为integer类型并隐含地将它们声明为这样。这可能会导致在编译时产生不必要的警告。你通常会遵循最佳的编程习惯,就像你以前做的那样,将这些定义放入自己的头文件中。

使用GCC编译这个包裹程序:

 
现在你有了两个目标文件:message.o和main.o。它们包含能够被你的Linux工作站执行的目标代码。要从这个目标代码创建Linux可执行程序,你需要再一次调用GCC来执行连接阶段的工作:
 
GCC认识目标代码的.o后缀名,并知道应该如何为你调用外部GNU连接器。请记住GCC在默认情况下将把所有可执行文件命名为a.out,所以你需要在命令行中指定可执行程序名。在成功将多个源文件编译并连接进单个可执行文件后,你就可以以正常的方式来执行这个程序了。
 
前面这些单独的步骤也可以简化为一个命令,这是因为GCC对如何将多个源文件编译为一个可执行程序有内置的规则。
 

 

2.2 GNU工具链的组成(使用外部函数库)

 

2.2  GNU工具链的组成(使用外部函数库)

3. 使用外部函数库

GCC常常与包含标准例程的外部软件库结合使用,这些软件库为在Linux上运行的C程序提供必需的功能。这实际上是C编程语言设计上的一个副作用。作为一个特点鲜明的语言,它甚至连一些基本的任务,如I/O、读写文件或终端显示窗口也要依赖于某些标准的外部库例程来执行。
几乎每一个Linux应用程序都依赖于由GNU C函数库GLIBC所提供的例程。这个函数库提供了基本的I/O例程,如printf以及上例中的exit函数,后者用于请求Linux内核正常终止程序(事实上,不论你是否明确地调用它,像exit这样的函数都会被调用——我们在本书后面讨论Linux内核时还会介绍更多这方面的内容)。

正如你将在以后章节中看到的那样,GNU C函数库构成Linux内核之上的一个薄层并提供了许多有用的例程,如果这些例程都由Linux内核自身来提供,付出的代价将相当昂贵(根据代码效率和增加的复杂性)。事实上,当你要求GCC执行编译任务时,GCC在默认情况下将假设GLIBC将被包括进你的程序。由于这一过程发生在连接阶段,所以还需要你在应用程序源代码中添加函数库的头文件以提供库函数自身的原型定义。
给GNU C函数库的这一特殊待遇是由其几乎普遍的实用性以及几乎所有应用程序都会使用到它这一事实决定的。虽然我们很少会在编译源代码时不使用GLIBC,但我们的确可以这样做。事实上,Linux内核就完全没有使用GLIBC,而是在内核中包含了它自己对许多标准C库函数的简化实现,这是因为GLIBC依赖于Linux内核来代表它执行各种服务。

通过遵循一些简单的准则,你就可以为Linux应用程序创建自己的软件库。我们将在这里对此进行解释,首先创建一个简单的程序trig.c,它以一个给定的角度值来计算各种三角函数值:

  
   

这个程序依赖于由系统数学库(作为Linux系统上GLIBC软件包的一部分——附带提一下,当你使用GCC编译普通应用程序时,它不会被自动包含)提供的外部数学函数。因此,在连接阶段,我们需要告诉GCC在它搜索库函数的路径中应该包括这个外部函数库:
  


注意GCC的"-lm"选项,它告诉GCC查看系统提供的数学库(libm)。因为Linux和UNIX的系统函数库通常以"lib"为前缀,所以我们假设它存在。真正的函数库位置随系统的不同而不同,但它一般会位于目录/lib或/usr/lib中,在这些目录中还有数以百计的其他必需的系统函数库,你可能在后面会用到它们。
 
共享函数库与静态函数库

Linux系统上的函数库分为两种不同的类型:共享的和静态的。后者继承自往昔的UNIX系统,那时所有的软件库都静态连接到使用这些库中例程的代码。每次当应用程序和静态连接的函数库一起编译时,任何引用的库例程中的代码都会被直接包含进最终的二进制程序。由于每个可执行程序都将包含标准例程的副本,所以导致它的容量较大。

现代Linux(和UNIX)系统在大多数情况下使用共享函数库。共享函数库和它对应的静态函数库包含相同的例程,但这些例程并不是直接插入每个要连接它的程序。相反,共享函数库包含每个库例程的单一全局版本,它在所有应用程序之间共享。这一过程背后所涉及的机制相当复杂,但主要依靠的是现代计算机的虚拟内存能力,它允许包含库例程的物理内存安全地在多个独立用户程序之间共享。

使用共享函数库不仅减少了文件的容量和Linux应用程序在内存中覆盖的区域,而且它还提高了系统的安全性。如今,软件中新的安全漏洞每天都会被发现,相应的补丁修复出来得也一样的快。如果安全问题存在于系统函数库中,那么大量使用这些函数库的应用程序将会突然成为你的安全梦魇。通过尽可能地使用共享函数库资源,你就可以在不对软件进行重新编译的情况下确保它是最新的——只需一个全局的函数库新就可以自动修复应用程序。

使用共享函数库的另一个好处是:一个被许多不同程序同时调用的共享函数库很可能会驻留在内存中,以在需要使用它时被立即使用,而不是位于磁盘的交换分区中。这有助于进一步减少一些大型Linux应用程序的装载时间。
 
一个共享函数库的例子

创建共享函数库的过程是比较简单的。事实上,你可以重新编译前面的多源文件示例代码以使用共享函数库,而不是将不同的源文件静态连接为一个单独的可执行程序。由于共享函数库会同时被许多不同的应用程序使用,所以我们需要以一种“位置无关”的方式来编译共享函数库代码(即,它可以在任意内存位置被装载并仍然可以执行)。

使用GCC的“-fPIC”选项重新编译源文件message.c:

 
“PIC”命令行标记告诉GCC产生的代码不要包含对函数和变量具体内存位置的引用,这是因为现在还无法知道使用该消息代码的应用程序会将它连接到哪一段内存地址空间。这样编译输出的文件message.o可以被用于建立共享函数库,我们只需使用gcc的“-shared”标记即可:
 
你可以让前面例子中的包裹程序main.c使用这里创建的共享函数库。与将消息打印函数连接到最终可执行程序goodbye不同,GCC可以通知连接器使用共享函数库资源libmessage.so:
 

注意,我们使用“-lmessage”标记来告诉GCC驱动程序在连接阶段引用共享函数库libmessage.so。“-L.”标记告诉GCC函数库可能位于当前目录(包含程序源文件的目录)中,否则GNU的连接器会查找标准系统函数库目录,在本例的情况下,就找不到可用的函数库了。

你所建立的任何共享函数库都可以像你的Linux系统上安装的其他函数库一样使用,但它必须被安装到正确的位置以使这样的使用完全自动化。当有使用共享函数库的应用程序被加载时,现代Linux系统所提供的运行时动态连接器/加载器-ld-linux会被自动调用并在标准系统目录/lib和/usr/lib(当然在许多系统中,我们也可以使用配置文件/etc/ld.so.conf来进行修改)下查找函数库。当你发布应用程序时,你需要在软件安装过程中将一些必需的函数库安装到系统可以在运行时自动搜索到的目录中。根据系统具体情况的不同,你可能还需要再次运行命令ldconfig。
你可以使用命令ldd来发现一个特定应用程序需要使用的函数库。ldd搜索标准系统函数库路径并显示一个特定程序使用的函数库版本。我们尝试将ldd使用到上面的例子中:

 
库文件libmessage.so不能在任何一个标准搜索路径中找到,而且系统提供的配置文件/etc/ld.so.conf也没有包含一个额外的条目来指定包含该库文件的目录。因此,如果我们运行这个程序,它将产生如下所示的输出:
 

如果你仅仅是想测试这个示例,那不用修改你的标准Linux系统设置或将库文件libmessage.so安装到系统函数库目录中,你只需设置一个环境变量LD_LIBRARY_PATH即可,它可以包含额外的函数库搜索路径。运行时连接器将搜索这些额外路径以发现没有在标准路径中找到的函数库。

首先设置一个适当的LD_LIBRARY_PATH,然后运行示例代码:

 

 

 

 

2.2 GNU工具链的组成(GCC选项)

 

 

2.2  GNU工具链的组成(GCC选项)

4. GCC选项

GCC拥有数不清的命令行选项标记,它们几乎可以被用于控制编译过程中的每一个方面以及GCC可能会依赖的任何外部工具的操作。通常不会在编译应用程序时指定太多的选项,但当在自己的项目中使用GCC时,你会很快习惯使用GCC提供的各种调试和警告选项。
GCC选项可以被分为如下几类:

一般选项 
语言选项 
警告级别 
调试 
优化
硬件选项
一般选项

一般选项包括指定GCC产生的可执行文件名以及是否需要GCC完成整个编译过程或在它完成基本编译之后就终止。你已看到可以在命令行上指定“-c”标记来告诉GCC只执行编译和汇编而不执行连接。我们还可以通过指定“-s”标记来让GCC在执行完编译之后就停止并输出汇编语言代码。
 
语言选项

特定语言选项允许你控制GCC对当前源文件所使用的编程语言的相关标准的解释。在使用C语言的情况中,GCC默认会使用它自己对ANSI C的扩展版本(GNU89或GNU99),这个版本支持一些受到程序员欢迎的较为宽松的约定,但可能并不严格符合官方的C89或C99语言规范。你可以使用这些特定语言选项来指定语言行为。几个比较常见的选项如表2-1所示:

表2-1 常见的可指定语言行为的语言选项

  

除了一些极端情况(例如,你正在编写自己的C函数库)或你正在运行自己的语言验证测试外,你通常不需要改变语言选项。
 
警告级别

GCC提供了各种警告级别,它们可以帮助追查某些类型的不良编程习惯,或当你误用了某些你已告诉GCC不要提供的语言特征时,它们会向你发出警告。几个比较常见的警告选项如表2-2所示:

表2-2 常见的警告选项

   

养成在编译自己的程序时总是使用“-Wall”选项的习惯将对你大有帮助。此外,我们还建议你尽可能考虑使用“-pedantic-errors”选项,因为这将有助于在许多常见问题发生之前孤立它们,同时还有助于确保你的代码是尽可能接近标准规范的。

一个使用了额外选项的编译示例如下所示:

  


调试

GCC提供了各种选项来方便你对应用程序的调试,正如你将在本章后面看到的一样。其中主要的是“-g”选项,它将在可执行程序的调试数据段中包含调试信息。这个信息可以被GDB用于执行源代码级别的调试以及方便在本书后面提到的其他更高级的调试方式。

为了在编译前面的应用程序时提供有用的调试信息以及启用适当的警告,你可以使用如下命令:

 

优化

GCC能够对它所编译的代码执行各种优化。可以使用的选项包括数字优化级别“-O0”到“-O3”——从不进行优化到要求GCC尽可能地执行优化。根据优化级别的不同,它们分别会启用或禁用各种额外选项。

优化并不是没有代价的。它通常会大大增加编译时间和编译过程中编译器所需要的内存资源。它也不能保证不增加最终可执行文件的大小,这是因为它通常会将循环或函数展开,使它们以内联的方式循环而不是通过函数调用,这样做将显著地提高性能。如果你想对可执行文件的大小进行优化,可以使用“-Os”选项标记。

要注意的是,有些优化是必要的。GCC默认不会使用内联函数 (在本章后面会介绍更多这方面的内容),除非我们在调用它时使用一组适当的优化标记。但在编译Linux内核时,我们可能需要选择降低使用的优化级别以避免引起问题。真正要降低优化级别的原因之一是想避免GCC为了在现代处理器上更有效地执行指令而对指令重新排序,虽然这样做是有益的,但指令的重新排序将使得一些调试更加困难,对非常大型和复杂的程序(如Linux内核)尤其如此。

 
硬件选项

GCC是一个可移植性非常强的C编译器,它支持多种不同的硬件目标。一个硬件目标是一种类型的机器,其特性由安装在其中的微处理器类型决定。你的工作站很可能是基于Intel兼容处理器的,该处理器有各种不同的模型,每种模型又各自具备不同的能力。当编译代码时,你的GCC版本在默认情况下使用你的Linux发行版厂商所配置的处理器模型。

如果你编译的代码只用在自己的计算机上(或类似类型的机器上),你并不需要设置任何硬件选项以让编译的代码成功执行。但是,当你使用交叉编译器或希望编译的代码能够在类型完全不同的机器上执行时,情况就不同了。

根据所用硬件的不同,你可以使用表2-3中列出的部分或全部选项:

表2-3 GCC的硬件选项

 

附带说明一下,一些开发人员(和越来越多的渴望将他们的机器性能用到极致的爱好者)会使用GCC的硬件选项标记来改变默认的处理器目标或设置特定于他们本地CPU模型的其他选项。这样做,他们可以让本地编译的应用程序获得最大程度的性能提高,付出的代价是降低了程序的可移植性。但是,如果你从未打算发布你的应用程序,或你是一个嵌入式Linux开发者,希望能利用一切可能的硬件性能优化,那这并不是一个问题。

更详细的文档

介绍GCC可用选项的完整文档见本地GCC安装的在线帮助。要阅读GCC文档,你可以从任何终端窗口使用Linux的man命令或GNU的info工具。传统的man页面包含了对命令选项的概述,你可以使用如下命令来查看:

 
自由软件基金会通常并不鼓励在自己的项目中使用man页面,而是建议使用它认为更灵活的GNU的info系统。可以使用如下命令通过GNU info来查看扩展的文档:
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值