15 开发工具
Linux深受程序员的欢迎,这不仅是因为它提供了大量的工具和环境,还因为该系统的文档特别完善,而且非常透明。在Linux机器上,你不一定非得是程序员才能利用开发工具,这是个好消息,因为与其他操作系统相比,开发工具在Linux系统管理中发挥着更大的作用。至少,你应该能够识别开发工具,并知道如何运行它们。
本章用很小的篇幅介绍了大量信息,但你并不需要掌握这里的所有内容。示例将非常简单;你不需要知道如何编写代码就能跟上。您也可以轻松地略读材料,稍后再来看。关于共享库的讨论可能是你需要了解的最重要的内容,但要理解共享库的来源,你首先需要了解一些关于如何构建程序的背景知识。
15.1 C 编译器
了解如何使用 C 语言编译器可以让你对 Linux 系统中程序的来源有更深入的了解。大多数 Linux 实用程序和 Linux 系统上的许多应用程序都是用 C 或 C++ 编写的。本章将主要使用 C 语言编写的示例,但你也可以将这些信息移植到 C++ 中。
C 程序遵循传统的开发流程:编写程序、编译程序、运行程序。也就是说,当你编写 C 程序代码并希望运行它时,你必须将人类可读的代码编译成计算机处理器能理解的二进制低级形式。您编写的代码称为源代码,可以包含许多文件。您可以将其与我们稍后讨论的脚本语言进行比较,在脚本语言中,您不需要编译任何东西。
注意:大多数发行版默认不包含编译 C 代码所需的工具。如果你没有看到这里描述的某些工具,可以安装 Debian/Ubuntu 的 build-essential 软件包,或 Fedora/CentOS 的 “yum groupinstall Development Tools”。也可如果做不到这一点,可以尝试在软件包中搜索 “gcc”或 “C compiler”。
大多数 Unix 系统上可执行的 C 编译器是 GNU C 编译器 gcc(通常使用传统名称 cc),不过 LLVM 项目最新推出的 clang 编译器也越来越受欢迎。C 源代码文件以 .c 结尾。请看 Brian W. Kernighan 和 Dennis M. Ritchie 合著的《C 编程语言》(第 2 版)(Prentice Hall,1988 年)中的这个独立的 C 源代码文件 hello.c:- #include <notfound.h><stdio.h>
- int main() {
- printf("Hello, World.\n");
- }
复制代码 将源代码保存在名为 hello.c 的文件中,然后使用以下命令运行编译器:结果是一个名为 a.out 的可执行文件,你可以像运行系统中的其他可执行文件一样运行它。不过,您也许应该给该可执行文件取另一个名字(如 hello)。为此,请使用编译器的 -o 选项:对于小型程序来说,编译过程并不复杂。你可能需要添加一个额外的库或包含目录,但在讨论这些主题之前,让我们先看看稍大一些的程序。
15.1.1 编译多个源文件
大多数C程序都过于庞大,单个源代码文件难以容纳。对于程序员来说,庞大的文件会变得杂乱无章,编译器有时甚至难以处理大型文件。因此,开发人员通常会将源代码分割成多个部分,每个部分都有自己的文件。
编译大多数.c文件时,不会立即创建可执行文件。相反,在每个文件上使用编译器的 -c 选项来创建包含二进制目标代码的目标文件,这些二进制目标代码最终将进入最终的可执行文件。为了了解其工作原理,我们假设有两个文件,main.c(用于启动程序)和 aux.c(用于执行实际工作),它们的外观如下:
main.c:- void hello_call();
- int main() {
- hello_call();
- }
复制代码 aux.c:- #include <notfound.h><stdio.h>
- void hello_call() {
- printf("Hello, World.\n");
- }
复制代码 下面两条编译器命令完成了编译程序的大部分工作:创建对象文件。- $ cc -c main.c
- $ cc -c aux.c
复制代码 这些命令完成后,你将得到两个对象文件:main.o 和 aux.o。
对象文件是一种处理器几乎可以理解的二进制文件,但仍有一些问题。首先,操作系统不知道如何启动对象文件;其次,你可能需要将多个对象文件和一些系统库组合起来,才能生成一个完整的程序。
要从一个或多个对象文件编译出功能完备的可执行程序,必须运行链接器,即 Unix 中的 ld 命令。不过,程序员很少在命令行中使用ld,因为 C 编译器知道如何运行链接程序。要从这两个对象文件创建名为 myprog 的可执行文件,请运行以下命令将它们链接起来:- $ cc -o myprog main.o aux.o
复制代码 注意:虽然可以用单独的命令编译每个源文件,但前面的例子已经暗示,在编译过程中很难跟踪所有的源文件;当源文件的数量成倍增加时,这一挑战就会变得更加严峻。第 15.2 节中介绍的 make 系统是管理和自动编译的传统 Unix 标准。在接下来的两节中,当我们处理文件管理问题时,你会发现拥有一个像 make 这样的系统是多么重要。
如前所述,文件 aux.c 中的代码完成了程序的实际工作,而且可能有许多文件,如文件aux.o 生成的对象文件,都是编译程序所必需的。现在想象一下,其他程序可能会使用我们编写的例程。我们能重复使用这些对象文件吗?这就是我们的下一个目标。
15.1.2 与库链接
在源代码上运行编译器通常不会产生足够的目标代码,无法单独创建有用的可执行程序。您需要使用库来构建完整的程序。C 库是预编译过的常用组件的集合,您可以将其编入您的程序,它实际上只是一捆对象文件(以及一些头文件,我们将在第 15.1.4 节中讨论)。例如,许多可执行程序都会使用标准数学库,因为它提供了三角函数等。
库主要在链接时发挥作用,即链接程序 (ld) 从目标文件创建可执行文件时。使用库进行链接通常被称为针对库进行链接。这时最容易出现问题。例如,如果你有一个使用 curses 库的程序,但你忘记告诉编译器针对该库进行链接,你就会看到如下链接错误:- badobject.o(.text+0x28): undefined reference to 'initscr'
复制代码 这些错误信息中最重要的部分用粗体标出。当链接程序检查 badobject.o 对象文件时,它找不到粗体显示的函数,因此无法创建可执行文件。在这种特殊情况下,你可能会怀疑自己忘了使用 curses 库,因为缺少的函数是 initscr();如果在网上搜索该函数名,几乎总能找到手册页面或其他有关该库的参考资料。
注意:未定义的引用并不总是意味着缺少一个库。程序的某个对象文件可能在链接命令中丢失了。通常很容易区分函数库中的函数和对象文件中的函数,因为你很可能能认出自己编写的函数,或者至少能搜索到它们。
要解决这个问题,首先必须找到 curses 库,然后使用编译器的 -l 选项与该库链接。库分散在整个系统中,但大多数库都位于名为 lib 的子目录中(系统默认位置为 /usr/lib)。在前面的例子中,curses 库的基本文件是 libcurses.a,因此库名为 curses。将所有文件放在一起,就可以像这样链接程序了:- $ cc -o badobject badobject.o -lcurses
复制代码 你必须告诉链接器非标准库的位置;参数是 -L。比方说,badobject 程序需要使用 /usr/junk/lib 中的 libcrud.a。要编译并创建可执行文件,请使用如下命令:- $ cc -o badobject badobject.o -lcurses -L/usr/junk/lib -lcrud
复制代码 注意:如果要搜索库中的某个特定函数,请使用带有 --defined-only 符号过滤器的 nm 命令。请做好大量输出的准备。在许多发行版中,你也可以使用 less 命令来查看库的内容。(你可能需要使用 locate 命令来查找 libcurses.a;现在许多发行版都将库放在 /usr/lib 中特定架构的子目录下,如 /usr/lib/x86_64-linux-gnu/)。
系统中有一个名为 C 标准库的库,其中包含被视为 C 编程语言一部分的基本组件。它的基本文件是 libc.a。当你编译程序时,这个库总是包含在内,除非你特别排除它。系统中的大多数程序都使用共享版本,下面我们就来谈谈它是如何工作的。
参考资料
- 软件测试精品书籍文档下载持续更新 https://github.com/china-testing/python-testing-examples 请点赞,谢谢!
- 本文涉及的python测试开发库 谢谢点赞! https://github.com/china-testing/python_cn_resouce
- python精品书籍下载 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md
- Linux精品书籍下载 https://www.cnblogs.com/testing-/p/17438558.html
15.1.3 使用共享库
以 .a 结尾的库文件(如 libcurses.a)称为静态库。在将程序与静态库链接时,链接器会将必要的机器代码从库文件复制到可执行文件中。这样,最终的可执行文件在运行时就不再需要原始库文件了,而且由于可执行文件拥有自己的库代码副本,因此即使 .a 文件发生变化,可执行文件的行为也不会改变。
然而,库的大小一直在增加,使用中的库的数量也在增加,这就使得静态库在磁盘空间和内存方面造成了浪费。此外,如果后来发现静态库不完善或不安全,那么除非找到并重新编译每个可执行文件,否则就无法更改与之链接的可执行文件。
共享库可以解决这些问题。根据共享库链接程序并不会将代码复制到最终的可执行文件中;它只是在库文件的代码中添加名称引用。运行程序时,系统仅在必要时才会将库代码加载到进程内存空间。许多进程可以共享同一内存中的共享库代码。如果需要对库代码稍作修改,一般无需重新编译任何程序即可完成。在更新 Linux 发行版上的软件时,你要更新的软件包可能包括共享库。当你的更新管理器要求你重启机器时,有时是为了确保你系统的每一部分都在使用新版本的共享库。
共享库有其自身的代价:管理困难,链接过程有些复杂。不过,只要掌握以下四点,就能控制共享库:
- 如何列出可执行文件需要的共享库
- 可执行程序如何查找共享库
- 如何根据共享库链接程序
- 如何避免常见的共享库陷阱
下文将介绍如何使用和维护系统的共享库。如果你对共享库的工作原理感兴趣,或者想了解链接器的一般知识,可以参阅 John R. Levine 所著的《链接器与加载器(Linkers and Loaders)》(Morgan Kaufmann,1999 年);David M. Beazley、Brian D. Ward 和 Ian R. Cooke 所著的《共享库与动态加载的内幕(The Inside Story on Shared Libraries and Dynamic Loading)》(Computing in Science & Engineering,2001 年 9 月/10 月);或在线资源,如程序库 HOWTO (https://web.archive.org/web/20200916035336/https://dwheeler.com/program-library/)。ld.so(8)手册页面也值得一读。
共享库文件通常与静态库存放在相同的位置。Linux 系统中的两个标准库目录是 /lib 和 /usr/lib,不过系统中还可能散布着更多目录。/lib 目录不应包含静态库。
共享库的后缀包含 .so(共享对象),如 libc-2.15.so 和 libc.so.6。要查看程序使用了哪些共享库,请运行 ldd prog,其中 prog 是可执行文件的名称。下面是 shell 的示例:
为了获得最佳性能和灵活性,可执行文件通常并不知道共享库的位置;它们只知道共享库的名称,也许还知道在哪里可以找到它们。一个名为 ld.so(运行时动态链接器/加载器)的小程序会在运行时为程序查找并加载共享库。前面的 ldd 输出显示了 => 左侧的库名;这是可执行文件所知道的。=> 的右侧显示了 ld.so 查找库的位置。
最后一行输出显示了 ld.so 的实际位置: /lib/ld-linux.so.2。
共享库的常见问题之一是动态链接器找不到库。通常,动态链接器查找共享库的第一个地方是可执行文件预先配置的运行库搜索路径(rpath)(如果存在的话)。你很快就会看到如何创建这个路径。
接下来,动态链接器会查看系统缓存 /etc/ld.so.cache,以确定库是否位于标准位置。这是缓存配置文件 /etc/ld.so.conf 所列目录中库文件名的快速缓存。
注意:正如你所见过的许多典型 Linux 配置文件一样,ld.so.conf 可能反过来包括来自/etc/ld.so.conf.d 等目录的许多文件。
ld.so.conf(或其包含的文件)中的每一行都是你希望包含在缓存中的目录名称。目录列表通常很短,包含如下内容- /lib/i686-linux-gnu
- /usr/lib/i686-linux-gnu
复制代码 标准库目录 /lib 和 /usr/lib 是隐含的,这意味着无需在 /etc/ld.so.conf 中包含它们。
如果更改了 ld.so.conf,或对其中一个共享库目录进行了修改,则必须使用以下命令手动重建 /etc/ld.so.cache 文件:-v 选项提供了 ldconfig 添加到缓存的库的详细信息,以及它检测到的任何更改。
此外,ld.so 还会在一个地方查找共享库:环境变量 LD_LIBRARY_PATH。
不要养成向 /etc/ld.so.conf 添加内容的习惯。你应该知道系统缓存中都有哪些共享库,如果你把每个奇怪的小共享库目录都放到缓存中,你就会面临冲突和系统极度混乱的风险。当你编译的软件需要一个模糊的库路径时,给你的可执行文件一个内置的运行时库搜索路径。让我们看看如何做到这一点。
假设在 /opt/obscure/lib 中有一个名为 libweird.so.1 的共享库,需要与 myprog 进行链接。/etc/ld.so.conf中不应该有这个奇怪的路径,因此需要将该路径转达给链接器。链接程序如下- $ cc -o myprog myprog.o -Wl,-rpath=/opt/obscure/lib -L/opt/obscure/lib -lweird
复制代码 选项 -Wl,-rpath 告诉链接器将指定目录纳入可执行文件的运行库搜索路径。不过,即使使用了 -Wl,-rpath,仍需要使用 -L 标志。
如果需要更改现有二进制文件的运行库搜索路径,可以使用 patchelf 程序,但通常最好在编译时进行。(ELF:Executable and Linkable Format,,即可执行和可链接格式,是 Linux 系统中可执行文件和库的标准格式)。
共享库提供了非凡的灵活性,更不用说一些令人难以置信的黑客技术,但你也可能滥用它们,以至于你的系统变得一团糟。有三种特别糟糕的情况可能发生:丢失库;糟糕的性能;不匹配的库。
所有共享库问题的首要原因是环境变量 LD_LIBRARY_PATH。将该变量设置为一组以冒号分隔的目录名,ld.so 在查找共享库时就会先搜索给定的目录,然后再查找其他目录。如果你没有程序的源代码,无法使用 patchelf,或者懒得重新编译可执行文件,那么当你移动共享库时,这是一种让程序正常运行的廉价方法。不幸的是,付出总有回报。
切勿在 shell 启动文件或编译软件时设置 LD_LIBRARY_PATH。当动态运行时链接器遇到这个变量时,它往往必须搜索每个指定目录的全部内容,其次数之多是你无法想象的。这会对性能造成很大影响,但更重要的是,由于运行时链接器会为每个程序查找这些目录,因此可能会出现冲突和不匹配的库。
如果你必须使用 LD_LIBRARY_PATH 来运行一些你没有源代码的糟糕程序(或者你不想重新编译的应用程序,如 Firefox ),请使用封装脚本。比方说,你的可执行文件是 /opt/crummy/bin/crummy.bin,需要一些共享库,这些共享库位于 /opt/crummy/lib。编写一个名为 crummy 的封装脚本,看起来就像这样:- #!/bin/sh
- LD_LIBRARY_PATH=/opt/crummy/lib
- export LD_LIBRARY_PATH
- exec /opt/crummy/bin/crummy.bin $@
复制代码 避免使用 LD_LIBRARY_PATH 可以避免大多数共享库问题。但开发人员偶尔会遇到的另一个重要问题是,从一个次版本到另一个次版本,库的 API 可能会略有变化,从而破坏已安装的软件。这里最好的解决办法是预防性的:要么使用一致的方法安装共享库并使用 -Wl,-rpath 创建运行时链接路径,要么直接使用模糊库的静态版本。
15.1.4 使用头文件(包含)和目录
C 语言头文件是附加的源代码文件,通常包含类型和库函数声明,这些声明通常是针对您刚刚看到的库。例如,stdio.h 就是一个头文件(参见第 15.1 节中的简单程序)。
许多编译问题都与头文件有关。大多数此类故障都发生在编译器找不到头文件和库的情况下。甚至在某些情况下,程序员忘记在包含所需头文件的代码中添加 #include <notfound.h>指令,导致部分源代码编译失败。
15.1.4.1 包含文件问题
要找到正确的头文件并非易事。有时你会很幸运地通过 locate 找到它们,但在其他情况下,在不同的目录中会有多个同名的包含文件,而且不清楚哪个才是正确的。当编译器找不到包含文件时,错误信息如下:- badinclude.c:1:22: fatal error: notfound.h: No such file or directory
复制代码 这条信息说明编译器找不到 badinclude.c 文件引用的 notfound.h 头文件。如果我们查看 badinclude.c(在第 1 行,正如错误提示的那样),我们会发现有这样一行内容:类似这样的包含指令并不指定头文件的存放位置,只是指定头文件应位于默认位置或编译器命令行指定的位置。这些位置的名称中大多包含 include。在 Unix 中,默认的 include 目录是 /usr/include;编译器总是在那里查找,除非你明确告诉它不要这样做。当然,如果包含文件在默认位置,就不太可能出现前面提到的错误,所以我们来看看如何让编译器在其他包含目录中查找。
例如,你在 /usr/junk/include 目录中找到了 notfound.h。要让编译器将该目录添加到搜索路径中,请使用 -I 选项:- $ cc -c -I/usrr/junk/include badinclude.c
复制代码 现在编译器应该不会再碰到 badinclude.c 中引用头文件的那行代码了。
注意:关于如何查找丢失的包含文件,你将在第 16 章学到更多。
您还应注意使用双引号(“”)而不是角括号(< >)的包含文件,如下面这样:- #include <notfound.h>"myheader.h"
复制代码 双引号表示头文件不在系统包含目录中,通常表示包含文件与源文件在同一目录中。如果遇到双引号问题,很可能是试图编译不完整的源代码。
15.1.4.2 C 预处理器
C 编译器实际上并不负责查找这些包含文件。这项工作由 C 预处理器来完成,它是编译器在解析实际程序之前在源代码上运行的一个程序。预处理器将源代码改写成编译器能理解的形式;它是一种使源代码更易于阅读(并提供快捷方式)的工具。
源代码中的预处理器命令称为指令,以 # 字符开头。指令有三种基本类型:
- 包含文件 #include <notfound.h>指令指示预处理器包含整个文件。请注意,编译器的 -I 标志实际上是一个选项,它可以使预处理器搜索指定目录中的包含文件,这在上一节中已经介绍过。
- 宏定义 像 #define BLAH something 这样的一行命令预处理器用某些内容来代替源代码中所有出现的 BLAH。按照惯例,宏应使用大写字母,但程序员有时会使用名称看起来像函数和变量的宏,这一点也不奇怪。(时不时地,这种情况会让人头疼不已。许多程序员把滥用预处理器当作一种乐趣)。
注意:您也可以通过向编译器传递参数来定义宏,而不是在源代码中定义宏: -DBLAH=something 的作用与前面的 #define 指令类似。
- 条件 您可以使用 #ifdef、#if 和 #endif 标记某些代码片段。#ifdef MACRO 指令检查是否定义了预处理器宏 MACRO,而 #if condition 指令则检查条件是否为非零。对于这两条指令,如果 “if ”语句后面的条件为假,预处理器不会将 #if 和下一个 #endif 之间的任何程序文本传递给编译器。如果你打算查看任何 C 代码,最好习惯这一点。
让我们来看一个条件指令的例子。当预处理程序看到下面的代码时,它会检查是否定义了宏 DEBUG,如果是,则将包含 fprintf() 的行传递给编译器。否则,预处理器跳过这一行,继续处理 #endif 之后的文件:
- #ifdef DEBUG
- fprintf(stderr, “This is a debugging message.\n”);
- #endif
复制代码 注意:C预处理器对C语法、变量、函数和其他元素一无所知。它只了解自己的宏和指令。
在Unix上,C 预处理器的名称是cpp,但也可以用 gcc -E 运行它。不过,您很少需要单独运行预处理器。
15.2 make
一个程序如果有多个源代码文件,或者需要奇怪的编译器选项,那么手工编译就太麻烦了。这个问题已经存在多年,解决这个问题的传统Unix编译管理工具叫做make。如果你运行的是Unix系统,就应该对 make 稍知一二,因为系统实用程序有时需要依赖 make 才能运行。不过,本章只是冰山一角。有很多关于 make 的书籍,如 Robert Mecklenburg 所著的《使用 GNU Make 管理项目》(Managing Projects with GNU Make)第 3 版(O'Reilly,2005 年)。此外,大多数 Linux 软件包都是使用围绕 make 或类似工具的附加层构建的。目前有许多构建系统,我们将在第 16 章介绍一个名为 autotools 的系统。
以上书籍网上有电子版,也可给包烟钱钉钉或v信: pythontesting获取。
make 是一个庞大的系统,但要了解它的工作原理并不难。当你看到一个名为 Makefile 或 makefile 的文件时,你就知道你正在使用 make。(试着运行 make 看看能否编译出任何东西)。
make 背后的基本思想是目标,即你想要实现的目标。目标可以是一个文件(.o 文件、可执行文件等),也可以是一个标签。此外,有些目标还依赖于其他目标;例如,在链接可执行文件之前,你需要一套完整的 .o 文件。这些要求称为依赖关系。
要编译一个目标,make 会遵循一条规则,例如指定如何从 .c 源文件转到 .o 目标文件。make 已经知道几条规则,但您也可以自定义它们,创建自己的规则。
15.2.1 Makefile 示例
以第 15.1.1 节中的示例文件为基础,下面这个非常简单的 Makefile 将从 aux.c 和 main.c构建myprog- 1 # object files
- 2 OBJS=aux.o main.o
- 3 all: 4myprog
- myprog: 5$(OBJS)
- 6$(CC) -o myprog $(OBJS)
复制代码 Makefile 1 第一行中的 # 表示注释。
下一行只是一个宏定义,将 OBJS 变量设置为两个对象文件名 2。这在后面会很重要。现在,请注意您定义宏的方式,以及稍后引用它的方式 ($(OBJS))。
Makefile 中的下一项包含第一个目标 3。第一个目标总是默认的,也就是当你在命令行上运行 make 时,make 希望构建的目标。
构建目标的规则在冒号之后。对于所有目标,这个 Makefile 说你需要满足一个叫做 myprog 4 的东西。这是该文件中的第一个依赖项,所有内容都依赖于 myprog。请注意,myprog 可以是一个实际的文件,也可以是另一条规则的目标文件。在本例中,它两者都是(所有规则和 OBJS 的目标)。
为了编译 myprog,此 Makefile 在依赖关系 5 中使用了 $(OBJS) 宏。该宏扩展为 aux.o 和 main.o,表明 myprog 依赖于这两个文件(它们必须是实际存在的文件,因为 Makefile 中没有任何目标文件使用这两个文件名)。
注意:$(CC) 6 前面的空白是制表符。
此 Makefile 假定您在同一目录下有两个名为 aux.c 和 main.c 的 C 源文件。在 Makefile 上运行 make 会产生以下输出,显示 make 正在运行的命令:- $ make
- cc -c -o aux.o aux.c
- cc -c -o main.o main.c
- cc -o myprog aux.o main.o
复制代码 15.2.2 内置规则
make 如何知道如何从 aux.c 转到 aux.o?毕竟 aux.c 并不在 Makefile 中。答案是 make 遵循了一些内置规则。当你需要 .o 文件时,它知道要查找 .c 文件,此外,它还知道如何在 .c 文件上运行 cc -c 来达到创建 .o 文件的目的。
15.2.3 最终编译程序
实现 myprog 的最后一步有点麻烦,但思路很清晰。在 $(OBJS) 中生成两个对象文件后,就可以按照下面的命令行运行 C 编译器了(其中 $(CC) 扩展为编译器名称):如前所述,$(CC) 前面的空白是制表符。您必须在任何系统命令前插入制表符,并在其独立的一行中插入。
请注意这一点:- Makefile:7: *** missing separator. Stop.
复制代码 出现这样的错误意味着 Makefile 出错了。制表符是分隔符,如果没有分隔符或有其他干扰,就会出现这个错误。
15.2.4 依赖关系更新
最后一个基本概念是,一般来说,目标是使目标与它们的依赖关系保持一致。此外,它的设计是只采取最少的必要步骤来实现这一目标,这可以节省大量时间。在前面的例子中,如果你连续输入两次 make 命令,第一条命令会编译 myprog,但第二条命令会产生这样的输出结果:- make: Nothing to be done for 'all'.
复制代码 在第二次执行时,make 查看了它的规则,发现 myprog 已经存在,所以它没有再次编译 myprog,因为自上次编译以来,依赖关系都没有改变。要试验一下,请执行以下操作:
- 运行 touch aux.c。
- 再次运行 make。这次,make 发现 aux.c 比目录中的 aux.o 新,于是再次编译 aux.o。
- myprog 依赖于 aux.o,而现在 aux.o 又比已经存在的 myprog 新,所以 make 必须再次创建 myprog。
这种连锁反应非常典型。
15.2.5 命令行参数和选项
如果你知道 make 的命令行参数和选项是如何工作的,你就能从它那里得到很多好处。
最有用的选项之一是在命令行中指定一个目标。在前面的 Makefile 中,如果你只想运行 aux.o 文件,可以运行 make aux.o。
您还可以在命令行中定义一个宏。例如,要使用 clang 编译器,请尝试在这里,make 会使用你定义的 CC,而不是默认的编译器 cc。命令行宏在测试预处理器定义和库时非常有用,尤其是我们即将讨论的 CFLAGS 和 LDFLAGS 宏。
事实上,运行 make 甚至不需要 Makefile。如果内置的 make 规则与目标相匹配,你可以直接要求 make 尝试创建目标。例如,如果你有一个名为 blah.c 的非常简单程序的源代码,请尝试 make blah。make 运行过程如下- $ make blah
- cc blah.o -o blah
复制代码 make 的这种用法只适用于最基本的 C 程序;如果你的程序需要一个库或特殊的包含目录,你可能应该编写一个 Makefile。在处理 Fortran、Lex 或 Yacc 等程序时,如果不知道编译器或工具是如何工作的,运行 make 而不编写 Makefile 实际上是最有用的。为什么不让 make 帮你弄明白呢?即使 make 无法创建目标,它也可能会给你一个很好的提示,告诉你如何使用该工具。
有两个 make 选项与众不同,它们是
- -n 打印构建所需的命令,但阻止 make 实际运行任何命令
- -f 文件 告诉 make 从文件读取,而不是从 Makefile 或 makefile 读取
15.2.6 标准宏和变量
make 有许多特殊的宏和变量。宏和变量之间的区别很难区分,但这里的宏指的是在 make 开始构建目标之后通常不会改变的东西。
如前所述,您可以在 Makefile 的开头设置宏。这些是最常见的宏:
- CFLAGS C 编译器选项。从 .c 文件创建目标代码时,make 会将其作为参数传递给编译器。
- LDFLAGS 与 CFLAGS 类似,但这些选项是用于链接器从目标代码创建可执行文件时使用的。
- LDLIBS 如果使用 LDFLAGS 但不想将库名称选项与搜索路径相结合,则将库名称选项放在此文件中。
- CC C 编译器。默认为 cc。
- CPPFLAGS C 预处理器选项。当 make 以某种方式运行 C 预处理器时,它会将此宏的扩展作为参数传递。
- CXXFLAGS GNU make 用于 C++ 编译器标志。
make 变量会随着编译目标而改变。变量以美元符号 ($) 开头。设置变量的方法有多种,但一些最常见的变量是在目标规则中自动设置的。下面是你可能会看到的内容:
- $@ 在规则内部时,该变量会扩展到当前目标。
- $< 在规则中,该变量会展开为目标的第一个依赖项。
- $* 该变量会展开为当前目标的基名或词干。例如,如果您正在构建 blah.o,则该变量会扩展为 blah。
下面是一个常见模式的示例--使用 myprog 从 .in 文件生成 .out 文件的规则:
- .SUFFIXES: .in
- .in.out: $<
- myprog $< -o $*.out
复制代码 你会在许多 Makefile 中遇到 .c.o: 这样的规则,它定义了一种运行 C 编译器创建对象文件的定制方式。
Linux 上最全面的 make 变量列表是 make info 手册。
GNU make 有许多其他变体所没有的扩展、内置规则和功能。只要你运行的是 Linux,这一点都没有问题,但如果你跑到 Solaris 或 BSD 机器上,指望同样的选项也能工作,那你可能会大吃一惊。然而,这正是 GNU autotools 等多平台编译系统要解决的问题。
15.2.7 常规目标
大多数开发者都会在 Makefile 中包含几个额外的常规目标,用于执行与编译相关的辅助任务:
clean 目标无处不在; make clean 通常会指示 make 删除所有对象文件和可执行文件,以便重新开始或打包软件。下面是 myprog Makefile 的一个示例规则:- clean:
- rm -f $(OBJS) myprog
复制代码 distclean 通过 GNU autotools 系统创建的 Makefile 总是有一个 distclean 目标,用于删除所有不属于原始发行版的内容,包括 Makefile。您将在第 16 章看到更多这方面的内容。在极少数情况下,你可能会发现开发者选择不使用此目标删除可执行文件,而是使用类似 realclean 的目标。
install 该目标将文件和编译后的程序复制到 Makefile 认为合适的系统位置。这可能很危险,所以在实际运行任何命令之前,一定要运行 make -n install 看看会发生什么。
test 或 check 一些开发者提供了 test 或 check 目标,以确保在执行联编后一切正常。
depend 该目标通过调用编译器的 -M 来检查源代码,从而创建依赖关系。这是一个看起来很不寻常的目标,因为它通常会更改 Makefile 本身。这已不再是常见的做法,但如果你遇到某些说明告诉你使用这一规则,请务必这样做。
all 如前所述,这通常是 Makefile 中的第一个目标。您经常会看到对该目标的引用,而不是实际的可执行文件。
15.2.8 Makefile 组织
尽管 Makefile 有许多不同的风格,但大多数程序员都遵守一些一般的经验法则。首先,在 Makefile 的第一部分(宏定义内部),你应该看到按照软件包分组的库和包含:- MYPACKAGE_INCLUDES=-I/usr/local/include/mypackage
- MYPACKAGE_LIB=-L/usr/local/lib/mypackage -lmypackage
- PNG_INCLUDES=-I/usr/local/include
- PNG_LIB=-L/usr/local/lib -lpng
复制代码 每种类型的编译器和链接器标志通常都有一个这样的宏:- CFLAGS=$(CFLAGS) $(MYPACKAGE_INCLUDES) $(PNG_INCLUDES)
- LDFLAGS=$(LDFLAGS) $(MYPACKAGE_LIB) $(PNG_LIB)
复制代码 对象文件通常按可执行文件分组。例如,有一个软件包创建了名为 boring 和 trite 的可执行文件。每个可执行文件都有自己的 .c 源文件,并需要 util.c 中的代码:- UTIL_OBJS=util.o
- BORING_OBJS=$(UTIL_OBJS) boring.o
- TRITE_OBJS=$(UTIL_OBJS) trite.o
- PROGS=boring trite
复制代码 Makefile 的其余部分可能如下所示:- all: $(PROGS)
- boring: $(BORING_OBJS)
- $(CC) -o $@ $(BORING_OBJS) $(LDFLAGS)
- trite: $(TRITE_OBJS)
- $(CC) -o $@ $(TRITE_OBJS) $(LDFLAGS)
复制代码 您可以将这两个可执行目标合并为一条规则,但这样做通常不是好主意,因为您无法轻易将规则移到另一个 Makefile 中、删除可执行文件或以不同方式对可执行文件进行分组。此外,依赖关系也会不正确:如果你只为 boring 和 trite 制定一条规则,trite 会依赖 boring.c,boring 会依赖 trite.c,而且每当你更改这两个源文件中的一个时,make 都会尝试重建这两个程序。
如果需要为对象文件定义特殊规则,请将对象文件的规则放在编译可执行文件的规则之上。如果多个可执行文件使用同一个对象文件,则应将对象规则置于所有可执行文件规则之上。
15.3 Lex 和 Yacc
在编译读取配置文件或命令的程序时,您可能会遇到 Lex 和 Yacc。这些工具是编程语言的构件。
Lex 是一种标记符号转换器,可将文本转换为带标签的编号标记。GNU/Linux 版本名为 flex。在使用 Lex 时,可能需要使用 -ll 或 -lfl 连接器标志。
Yacc 是一种试图根据语法读取标记的解析器。GNU 解析器是 bison;要获得 Yacc 兼容性,请运行 bison -y。您可能需要 -ly 连接器标志。
15.4 脚本语言
很久以前,除了 Bourne shell 和 awk 之外,普通的 Unix 系统管理员无需过多担心脚本语言。shell 脚本(将在第 11 章中讨论)仍然是 Unix 的重要组成部分,但 awk 已经淡出脚本领域。不过,许多强大的后继者已经出现,许多系统程序实际上已经从 C 语言转为脚本语言(如合理版本的 whois 程序)。让我们来了解一些脚本语言的基础知识。
对于任何脚本语言,你首先需要知道的是,脚本的第一行看起来就像 Bourne shell 脚本的 Shebang。例如,Python 脚本的开头是这样的或者这个版本,运行命令路径中的第一个 Python 版本,而不是总是运行 /usr/bin:正如您在第 11 章中看到的,以 #! 开头的可执行文本文件就是脚本。这个前缀后面的路径名就是脚本语言解释器的可执行文件。当 Unix 试图运行以 #! 开头的可执行文件时,它会运行 #! 后面的程序,并将文件的其余部分作为标准输入。因此,即使这是一个脚本:- #!/usr/bin/tail -2
- This program won't print this line,
- but it will print this line...
- and this line, too.
复制代码 shell 脚本的第一行通常包含一个最常见的基本脚本问题:脚本语言解释器的无效路径。例如,你把前面的脚本命名为 myscript。如果 tail 位于 /bin 而不是 /usr/bin,会怎样呢?在这种情况下,运行 myscript 会产生以下错误:- bash: ./myscript: /usr/bin/tail: bad interpreter: No such file or directory
复制代码 不要指望脚本第一行中有一个以上的参数能起作用。也就是说,前面例子中的 -2 可能有效,但如果您添加了另一个参数,系统可能会决定将 -2 和新参数视为一个大参数,包括空格和所有内容。不同的系统会有不同的做法,不要在这种无关紧要的事情上考验你的耐心。
现在,让我们来看看几种常用的语言。
15.4.1 Python
Python 是一种脚本语言,拥有众多追随者和一系列强大的功能,如文本处理、数据库访问、网络和多线程。它具有强大的交互模式和非常有条理的对象模型。
Python 的可执行文件是 python,通常位于 /usr/bin。然而,Python 并不仅仅用于命令行脚本。从数据分析到网络应用,Python 无处不在。David M. Beazley 所著的《Python Distilled》(Addison-Wesley,2021 年)是入门的好帮手。
15.4.2 Perl
Perl 是较早的第三方 Unix 脚本语言之一。它是编程工具中最原始的 “瑞士军刀”。虽然近年来 Perl 在 Python面前失去了不少优势,但它在文本处理、转换和文件操作方面表现尤为突出,你可能会发现许多工具都是用它构建的。Randal L. Schwartz、brian d foy 和 Tom Phoenix 合著的《Learning Perl》(第 7 版)(O'Reilly,2016 年)是一本教程式的入门读物;更大的参考书是 chromatic 合著的《Modern Perl》(第 4 版)(Onyx Neon Press,2016 年)。
15.4.3 其他脚本语言
你可能还会遇到这些脚本语言:
- PHP 这是一种超文本处理语言,通常用于动态网页脚本。有些人使用 PHP 编写独立脚本。PHP 网站是 http://www.php.net/。
- Ruby 面向对象狂热者和许多网络开发人员都喜欢用这种语言编程 (http://www.ruby-lang.org/)。
- JavaScript 这种语言在网络浏览器中主要用于处理动态内容。由于它有很多缺陷,大多数有经验的程序员都不把它作为独立的脚本语言,但在进行网络编程时,几乎不可能避免使用它。近年来,一种名为 Node.js 的实现在服务器端编程和脚本中变得越来越普遍;它的可执行文件名为 node。
- Emacs Lisp 这是 Emacs 文本编辑器使用的一种 Lisp 编程语言。
- MATLAB、Octave MATLAB 是一种商业矩阵和数学编程语言及程序库。Octave 是一个非常类似的免费软件项目。
- R 这是一种流行的免费统计分析语言。更多信息,请参阅 http://www.r-project.org/ and The Art of R Programming by Norman Matloff(No Starch Press,2011 年)。
- Mathematica 这是另一种带库的商业数学编程语言。
- m4 这是一种宏处理语言,通常只出现在 GNU autotools 中。
- Tcl Tcl(工具命令语言)是一种简单的脚本语言,通常与 Tk 图形用户界面工具包和自动化工具 Expect 相关联。虽然 Tcl 已不像以前那样被广泛使用,但不要忽视它的威力。许多资深开发人员更喜欢 Tk,尤其是它的嵌入式功能。更多信息请参见 http://www.tcl.tk/。
15.5 Java
Java 是一种类似于 C 语言的编译语言,语法更简单,对面向对象编程有强大的支持。它在 Unix 系统中占有一席之地。例如,它经常被用作网络应用程序环境,在专业应用程序中很受欢迎。Android 应用程序通常用 Java 编写。尽管在典型的 Linux 桌面上并不常见到 Java,但你还是应该知道 Java 是如何工作的,至少对于独立应用程序是这样。
Java 有两种编译器:一种是本地编译器,用于为系统生成机器代码(类似于 C 编译器);另一种是字节码编译器,用于字节码解释器(有时也称为虚拟机,与第 17 章所述的管理程序提供的虚拟机不同)。在 Linux 上,你几乎总会遇到字节码。
Java 字节码文件以 .class 结尾。Java 运行时环境 (JRE) 包含运行 Java 字节码所需的所有程序。要运行字节码文件,请使用您可能还会遇到以 .jar 结尾的字节码文件,它们是归档 .class 文件的集合。要运行 .jar 文件,请使用以下语法:有时,您需要将 JAVA_HOME 环境变量设置为 Java 安装前缀。如果运气不好,可能还需要使用 CLASSPATH 来包含程序所需的包含类的任何目录。这是一组以冒号分隔的目录,就像用于可执行文件的常规 PATH 变量一样。
如果需要将 .java 文件编译成字节码,则需要 Java 开发工具包(JDK)。您可以运行 JDK 中的 javac 编译器来创建一些 .class 文件:JDK 还附带了 jar,这是一个可以创建和拾取 .jar 文件的程序。它的工作原理类似于 tar。
15.6 展望未来: 编译软件包
编译器和脚本语言的世界非常广阔,而且还在不断扩展。截至本文撰写之时,Go (golang) 和 Rust 等新编译语言在应用程序和系统编程中越来越受欢迎。
LLVM 编译器基础架构集 (http://llvm.org/) 极大地简化了编译器的开发。如果您对如何设计和实现编译器感兴趣,有两本好书:《编译器》(Compilers: Principles, Techniques, and Tools, 2nd edition, by Alfred V. Aho et al.(Addison-Wesley,2006 年)和 Modern Compiler Design, 2nd edition, by Dick Grune et al.(Springer,2012 年)。对于脚本语言的开发,由于实现方法千差万别,通常最好查找在线资源。
现在你已经了解了系统中编程工具的基础知识,可以看看它们能做些什么了。下一章将介绍如何从源代码在 Linux 上构建软件包。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |