编译

Unix 平台下有很多编译和解释代码的工具,它们用法各异。然而,但是概念上很多步骤是一样的。这里我将讨论用 GNU 编译器集里的 gcc 编译 C,并简要介绍用 perl 作为解释器。

GCC

GCC 是个非常成熟的 GPL 许可的编译器集。也许大家知道得最多的还是用它来编译 C 和 C++ 程序。它的免费以及它广泛的装载在 Unix 类似的系统上(例如 Linux 和 BSD),使得它如此长久地流行。当然,现在还有一些更加现代化的使用 LLVM 架构的替代编译器,比如 Clang

最好别把 GNU 编译器集的前端二进制码当作是一组各自为政的完整的编译器,而是把它当作将一组离散的工具串联起来的驱动器,用以执行分析、编译和链接等步骤。这就意味着你既可以把 GCC 当作相对简单的命令行来把 C 源文件直接编译到可执行二进制,你也可以用它来检查和调试编译过程中的每个小步骤。

在这里我不会去讨论 make 文件,虽说对于任何多于一个文件的 C 项目必不可少。我们会在下一章有关创建和自动化工具的文章中讨论。

目标码的编译和汇编

你可以如此将 C 源码编译到目标码:

$ gcc -c example.c -o example.o

假设这是个 C 程序代码没有问题,这将会在当前目录有下生成一个未链接的二进制目标文件 example.o,或者它会告诉你编译为何失败。你可以用 objdump 来检查它的汇编码0:

$ objdump -D example.o

此外,你还可以用加 -S 参数来使得 gcc 输出适当的汇编码:

$ gcc -c -S example.c -o example.s

当汇编码和程序源代码被一起打印出来的时候可以是很有用的,或者至少是很有趣的:

$ gcc -c -g -Wa,-a,-ad example.c > example.lst

预处理器

C 预处理器 cpp 是用来将头文件和宏定义加入到代码里的。一般来说它是 gcc 的一部分,但你也可以来直接调用它来查看它生成的 C 代码:

$ cpp example.c

这将会打印出将要被编译的完整版代码,即包含了头文件并已实施了相关的宏。

目标码的连接

一个或多个目标码可以像这样被连接成适当的二进制文件:

$ gcc example.o -o example

在这个例子里,GCC 只是抽象化呼叫了 GNU 连接器 ld。以上的命令生成了一个可执行二进制文件 example

编译、装配和链接

以上所有的步骤其实可以一步做完:

$ gcc example.c -o example

这看起来简单了一些。但是,单个地编译目标码其实在实际的效率上有其过人之处,因为当重新编译的时候有些不必要的代码就不用再编译了。至于这一点,我会在下一篇文章中讨论。

包含和连接

C 文件和头文件可以被显式地包含在编译命令里,即用参数 -I

$ gcc -I/usr/include/somelib.h example.c -o example

类似的,如果代码需要被动态地连接到一个已编译好的系统库,这些库通常在像 /lib/usr/lib 的共有位置。假设是 ncurses,我们可以在命令里包含一个 -l 参数:

$ gcc -lncurses example.c -o example

如果在你的编译中有很多必要的包含和连接,将其放进环境变量是很明智的:

$ export CFLAGS=-I/usr/include/somelib.h
$ export CLIBS=-lncurses
$ gcc $CFLAGS $CLIBS example.c -o example

这种常见的步骤也是 Makefile 可以帮你省略的东西之一。

编译计划

为了查看 gcc 都用那些命令干了些什么,你可以在编译命令里加上 -v 开关。这样它就会把它的编译计划从标准错误流里打印出来:

$ gcc -v -c example.c -o example.o

如果你不想要编译器真正地去生产目标文件或者已连接的二进制文件。有时用 -### 更好(译者按:这个方法貌似在zsh下不能使用):

$ gcc -### -c example.c -o example.o

这有助于让你看到哪些步骤 gcc 帮你简化了。但同时,在一些特殊的情况下,你也可以用它来发现哪些是你不希望编译器帮你做的步骤却在编译时被执行了。

更冗长的错误查看

在用 gcc 编译的时候加上 -Wall 和/或 -pedantic 来让它输出不一定会产生错误的警告:

$ gcc -Wall -pedantic -c example.c -o example.o

将它放进你的 Makefile 或 Vim 的 makeprg 声明是个好主意,如前一篇文章所讨论的那样,它们在快速修正(quickfix)窗口里的输出效果很好。这种高强度的错误警告往往会使你写出可读性更强、兼容性更好、更少错误的代码。

编译时间剖析

你可以将 -time 标记放进 gcc,让它输出编译每一步所用的时间:

$ gcc -time -c example.c -o example.o

优化

传如一般的优化选项给 gcc 就可以让它为你构建更加搞笑的目标文件和连接好的二进制文件,当然优化需要花更长的编译时间。我发现 -O2 对于产品来说是个不错的适中选择:

  • gcc -O1
  • gcc -O2
  • gcc -O3

就像其他 Bash 命令一样,它们都可以直接从 Vim 呼叫:

:!gcc % -o example

解释器

类 Unix 系统里对解释型语言代码的处理方式就很不一样了。在下面的例子里,我将使用 Perl,但是大多数原则都适用于解释如 Python 或 Ruby 的代码。

内联

以下任意一种方式你都能够运行 Perl 代码的字符串。此例为在屏幕上打印一行“Hello, world.”,并且另起一行。第一种可能是最简约最标准的方法;而第二种则使用了 heredoc 字符串;第三种则是使用了经典的 Unix shell pipe。

$ perl -e 'print "Hello world.\n";'
$ perl <<<'print "Hello world.\n";'
$ echo 'print "Hello world.\n";' | perl

当然,更典型的是将代码保存在文件中,那文件也可以直接被运行:

$ perl hello.pl

不管用以上何种方式运行,你都可以在不运行的前提情况下用 -c 来检查代码的语法:

$ perl -c hello.pl

但是如果要把这种脚本当作逻辑化的二进制文件用,即你可以直接运行它而不需要知道或关系此脚本的种类。你就得在代码最前面加上所谓的“shebang”特殊的一行,它会指定用什么解释器来运行下面的脚本。

#!/usr/bin/env perl
print "Hello, world.\n";

然后需要用 chmod 把脚本设置成可执行模式。在此,将其后缀抹去也是个很好的实践,因为现在脚本已经被当作逻辑化二进制文件了:

$ mv hello{.pl,}
$ chmod +x hello

这样就可以像运行编译好的二进制文件一样来直接运行它了:

$ ./hello

这种用法很好用以至于现在很多现代化的 Linux 系统中都在用 Perl 甚至 Python 做常用工具,比如 adduser 就是 useradd 的友好前端工具。

下一篇文章,我将介绍用 make 来可匹敌 IDE 的定义和自动化建立项目。让我们向拥抱同样想法 Ruby 的 rake 打个招呼。