2.4 用 cc 编译

  这一章我们只讨论 GNU 的 C 和 C++ 编译器,因为在 FreeBSD 的基本系统中就 包含了。直接运行 ccgcc 就可以。而 用解释器写程序的细节对于不同的解释器都很不相同,通常可以在特定的解释器文档或 者在线帮助中找到。

  一旦你写完你的杰作,下一步就是把你的杰作转换成可以在 FreeBSD 上运行(希 望可以!)的东西。通常这包含几个步骤,不同的步骤由不同的程序来完成。

  1. 预处理你的源代码,去掉注释,以及其他技巧性的工作就像在 C 中展开宏。

  2. 检查代码的语法看你是否遵守了这个语言的规则。如果没有,编译器会给出 警告。

  3. 把源代码转换为汇编语言 ── 和机器代码很相似, 但是在一定情况下我们仍然可以理解。 [1]

  4. 把汇编语言转换为机器语言──是的,我们在说位元和字节,就是1和0。

  5. 检查你是否准确地使用了函数和全局变量类似的东西。例如,如果你调用了 一个不存在的函数,编译器就会给出警告。

  6. 如果你是从多个源代码文件编译,就要学会如何把这些文件组合到一起。

  7. 把产生出来的东西用系统的运行装载器装入内存并运行。

  8. 最后,把可执行文件写入文件系统。

  编译 这个词的意思通常指 1 到 4 步──其他的 步骤叫做 连接。有时侯第一步叫做 预处理 。第三和第四步叫做 汇编

  幸运的是,几乎所有这些细节都是隐藏的,因为 cc 只是 一个前端。它根据正确的参数调用程序来处理代码。只要输入

% cc foobar.c

  就会把 foobar.c 通过以上的步骤编译出来。如果你有 多个文件要编译,只要输入

% cc foo.c bar.c

  注意,语法检查就是──纯粹的检查语法。而不会检测你可能犯的任何逻辑 错误。比如无限循环,或者是你想用一元排序却使用了冒泡排序。 [2]

  cc 有很多选项,在帮助手册中都可以找到。这里列出了一 些最重要的选项,并且有例子。

-o filename

输出的文件名。如果你不使用这个选项,cc为产生 出一个叫 a.out 的执行文件。 [3]

% cc foobar.c
可执行文件是 a.out
% cc -o foobar foobar.c     可执行文件是 foobar
       
-c

仅仅编译文件,不会连接。如果你只想检查你写的测试程序的语法的话, 这个选项非常有用。或者你会使用 Makefile

% cc -c foobar.c
       

这会产生一个 目标文件 (不可执行) 叫做 foobar.o。这个文件可以和其他的目标文件连接在一起 构成一个可执行文件。

-g

产生一个可调试的可执行文件。编译器会在可执行文件中植入一些信息, 这些信息能够把源文件中的行数和被调用的函数联系起来。在你一步一步调试程 序的时候,调试器能够使用这些信息来显示源代码。这是 非常 有用的;缺点就是被植入的信息让程序变得更大。通常情况下,开 发一个程序的时候我们经常使用 -g,但是我们在编译一个 “release 版本” 的程序的时候,如果程序工作得让人满意了,我 们就不使用 -g 编译。

% cc -g foobar.c
       

这会产生一个可调试版本的程序。 [4]

-O

产生一个优化版本的可执行文件。编译器会使用一些聪明的技巧产生出比 普通编译产生的文件执行更快的可执行文件。可以在 -O 加 上数字来使用更高级的优化。但是这样做经常会暴露出编译器的优化器中的一些 错误。例如,2.1.0 版本的 FreeBSD 中的 cc 在某些情况 下使用了 -O2 的话,会产生出错误的代码。

优化通常只在编译一个 release 版本的时候才被打开。

% cc -O -o foobar foobar.c
       

这会产生一个优化版本的 foobar

  下面的三个参数会迫使 cc 检查你的代码是否符合一些国 际标准,经常被我们叫做 ANSI 标准,虽然严格的来说它是一个 ISO 标准。

-Wall

打开所有 cc 的作者认为值得注意的警告。不要只 看这个选项的名字,它并没有打开所有 cc 能够注意到的 所有警告。

-ansi

关闭大多数,但并不是所有,cc 提供的非 ANSI C 特性。不要只看选项的名字,它并不严格保 证你的代码会兼容标准。

-pedantic

关闭 所有 cc 的非 ANSI C 特性。

  没有这些选项,cc 能允许你按照标准使用一些非标准的扩 展。有一些扩展非常有用,但不能与其他编译器兼容──实际上,这个标准的主要 目的之一就是允许我们写出可以在任何系统上的由任何编译器编译的代码。这就叫做 可移植代码

  通常来说,你应该让你的代码尽可能的可以移植。否则你就不得不完全重写你的 代码以便能够在其他地方运行之──而且谁知道几年后你是否还会用它?

% cc -Wall -ansi -pedantic -o foobar foobar.c

  这会在检查 foobar.c对标准的兼容性以后产生一个 foobar 可执行文件。

-llibrary

在连接的时候指定一个函数库。

最常见的情况就是当你编译一个使用了一些 C 中的数学函数的时候。不 像大多数其他的平台,这些函数都不在 C 的标准库里面。你必须告诉编译器加 上这些库。

这个规则就是,如果库的名字叫做 libsomething.a,你就必 须给 cc 这样的选项 -lsomething。例如,数学库 叫做 libm.a,因此你给 cc 的选 项就是 -lm。一般情况下,我们要把这个选项放到命令行的 最后。

% cc -o foobar foobar.c -lm
       

这个会把数学函数库连接到 foobar 里面。

如果你要编译 C++ 代码,你需要 -lg++,或者 -lstdc++ 如果你使用的是 FreeBSD 2.2 或者更高版本,来 连接 C++ 库。或者,你可以运行 c++ 而不是 cc 来编译 C++ 代码。在 FreeBSD 上, c++ 也可以通过运行 g++ 来唤醒。

% cc -o foobar foobar.cc
-lg++     对于 FreeBSD 2.1.6 或者更低的版本
% cc -o foobar foobar.cc -lstdc++
对 FreeBSD 2.2 或者更高的版本
% c++ -o foobar foobar.cc
       

两种情况都会从 C++ 源文件 foobar.cc产生一个 可执行文件 foobar。注意,在 UNIX® 系统中,C++ 源 文件的传统后缀是 .C.cxx 或 者 .cc,而不是 MS-DOS® 类型的 .cpp (这个后缀已经被用到了其他的地方)。 gcc 根据这个约定来确定应该使用何种类型的编译器来编 译源文件。但是,这个限制不再起作用了,因此现在你可以自由的使用 .cpp 这个后缀来命名你的 C++ 源文件!

2.4.1 常见 cc 问题

2.4.1.1. 我尝试写一个程序,其中使用了 sin() 这个函 数。但是我却得到了如下的错误。这个错误是什么意思?
2.4.1.2. 好的,我写了一个简单的程序,练习使用 -lm。也 就是计算 2.1 的 6 次方。
2.4.1.3. 那么我怎么才能改正这个错误?
2.4.1.4. 我编译了一个文件叫 foobar.c 但是我没有找 到叫 foobar 的执行文件。这个文件到哪里去了?
2.4.1.5. 好的,我有一个执行文件 foobar,我用命令 ls 可以看见,但是在命令行我输入 foobar 却得到提示说没有这个文件。为什么找不到呢?
2.4.1.6. 我的可执行文件叫做 test,但是我运行之后却 什么也没发生。到底怎么了?
2.4.1.7. 我编译了一个程序,开始看起来运行得不错。但是后来调试了,说什么 “core dumped”。这个是什么意思?
2.4.1.8. 挺不错,但现在我该怎么办呢?
2.4.1.9. 我的程序把 core dump 以后,说有一个什么 “segmentation fault”。这是什么?
2.4.1.10. 有时候当我得到一个 core dump,提示说 “bus error”。我的 UNIX 教材里面说这意味这硬件错误,但是计算 机看起来运行很正常。这是真的吗?
2.4.1.11. 如果我可以让 core dump 在需要的时候产生,那就真的很不错。我能 这样做吗,或者我得等直到发生一个错误?

2.4.1.1. 我尝试写一个程序,其中使用了 sin() 这个函 数。但是我却得到了如下的错误。这个错误是什么意思?

/var/tmp/cc0143941.o: Undefined symbol `_sin' referenced from text segment
         

当使用像 sin() 这样的数学函数的时候,你必 须告诉 cc 把数学函数库给连接进来,就像这样:

% cc -o foobar foobar.c -lm
         

2.4.1.2. 好的,我写了一个简单的程序,练习使用 -lm。也 就是计算 2.1 的 6 次方。

#include <stdio.h>

int main() {
    float f;

    f = pow(2.1, 6);
    printf("2.1 ^ 6 = %f\n", f);
    return 0;
}
         

然后我编译:

% cc temp.c -lm
         

就像你说的我应该做的那样。但是我在运行的时候却有如下提示:

% ./a.out
2.1 ^ 6 = 1023.000000
         

这个 是正确的答案!到底怎么了?

当编译器看见你调用了一个函数,它会检查是否已经有了一个相配合的 原始类型 (prototype),如果没有,编译器会假定函数的返回值是 整 数,恰恰不是你的程序想要的结果。

2.4.1.3. 那么我怎么才能改正这个错误?

数学函数的声明原型都在 math.h 里面。如果 你引用了这个文件,编译器就能找到这个原型然后就不会对你的计算做奇怪的 干扰。

#include <math.h>
#include <stdio.h>

int main() {
...
         

像以前一样编译,然后再运行:

% ./a.out
2.1 ^ 6 = 85.766121
         

如果你使用了任何一个数学函数,一定要记得 引用 math.h 这个文件,并且连接数学函数库。

2.4.1.4. 我编译了一个文件叫 foobar.c 但是我没有找 到叫 foobar 的执行文件。这个文件到哪里去了?

记住,除非你指定一个名字,cc会把编译出的文 件叫做 a.out。使用 -o filename 这个选 项:

% cc -o foobar foobar.c
         

2.4.1.5. 好的,我有一个执行文件 foobar,我用命令 ls 可以看见,但是在命令行我输入 foobar 却得到提示说没有这个文件。为什么找不到呢?

不像 MS-DOSUNIX 不会在当前目录寻找你想执行的文件,除非你 指定这样做。可以输入 ./foobar,意思是 “在当 前目录下运行文件 foobar。”,也可以改变环 境变量 PATH 像这个样子

bin:/usr/bin:/usr/local/bin:.
         

最后的那个点的意思就是 “如果在其他任何目录找不到,在当前 目录中寻找。”

2.4.1.6. 我的可执行文件叫做 test,但是我运行之后却 什么也没发生。到底怎么了?

大多数 UNIX 系统在 /usr/bin 下有一个程 序叫做 test。Shell 会先检查这个程序然后在检查当前 目录寻找可执行文件。可以输入:

% ./test
         

或者给你的程序选一个更好的名字!

2.4.1.7. 我编译了一个程序,开始看起来运行得不错。但是后来调试了,说什么 “core dumped”。这个是什么意思?

core dump 这个名字可以追溯到 UNIX 的早 期历史,当时的计算机都使用线圈内存储存数据。通常情况下,如果一个程序 在一定的情况下执行失败了,系统就会把线圈内存的内容写到磁盘上的一个文 件中,这个文件就叫 core。通过研究这个文件,程序 员就可以发现问题之所在。

2.4.1.8. 挺不错,但现在我该怎么办呢?

使用 gdb 分析这个 core 文件 (见 第 2.6 节)。

2.4.1.9. 我的程序把 core dump 以后,说有一个什么 “segmentation fault”。这是什么?

基本上是你的程序尝试对内存进行某种非法的操作导致的。UNIX 在 设计上要保护操作系统本身和其他程序不受非法程序的干扰。

通常的原因有如下这些:

  • 尝试赋值给一个 NULL 指针,例如

    char *foo = NULL;
    strcpy(foo, "bang!");
           
    
  • 使用一个未被初始化的指针,例如

    char *foo;
    strcpy(foo, "bang!");
           
    

    在某种情况下,指针所包含的值会指向内存中某个区域,这个区域 对你的程序是不可操作的。在你的程序造成任何破坏之前,内核会终止程 序。如果你运气不好,这个指针会指向你自己的程序在内存中的某个区域, 从而破坏自身的某些数据结构,导致程序奇怪地崩溃。

  • 数组越界,例如

    int bar[20];
    bar[27] = 6;
           
    
  • 尝试在只读内存中储存数据,例如

    char *foo = "My string";
    strcpy(foo, "bang!");
           
    

    UNIX 编译器经常把像 "My string" 这样 的字符串放到只读内存中。

  • 错误的使用函数 malloc()free(),例如

    char bar[80];
    free(bar);
           
    

    or

    char *foo = malloc(27);
    free(foo);
    free(foo);
           
    

这些错误并不总会导致你的程序崩溃,但这些都是坏的习惯。有些系统 和编译器比其他的系统和编译器有更多的容错性,这就是为什么一些程序在一 个系统上可以运行很好,而在另一个系统上却会崩溃。

2.4.1.10. 有时候当我得到一个 core dump,提示说 “bus error”。我的 UNIX 教材里面说这意味这硬件错误,但是计算 机看起来运行很正常。这是真的吗?

不是真的,很幸运不是(除非你真的遇见了一个硬件问题...)。这 通常是用另一种方式说你尝试读写一块无权读写的内存。

2.4.1.11. 如果我可以让 core dump 在需要的时候产生,那就真的很不错。我能 这样做吗,或者我得等直到发生一个错误?

可以,切换到另一个控制台或者起动 xterm, 执行

% ps
       

找到你的程序的进程号,然后执行

% kill -ABRT pid
       

其中的 pid 就是你找到的进程号。

如果你的程序陷入了一个无限循环,这样做就很有用处。如果你的程序 偶然得到了 SIGABRT 信号,还有一些类似的信号也有同样 的功用。

或者,你可以使用函数 abort() 在自己的程序 中产生一个 core dump。请参考手册的 abort(3) 来了解更多。

如果你想在自己的程序之外产生一个 core dump,而不让程序终止, 那么你可以用命令 gcore。请参考手册的 gcore(1) 了解更多。

备注

[1]

严格的来说,cc 会把源代码转换为它自己的机器 无关的 p-code 而不是通常意义下的汇编语言。

[2]

一元排序可以快速地排序而冒泡排序不可以。

[3]

这个可执行文件的名字产生的原因深藏在历史的迷雾中。

[4]

注意,我们没有使用 -o 这个参数来确定输出的可 执行文件的名字。因此我们会得到一个可执行文件,叫做 a.out。产生一个可调试版本的 foobar 就留给读者作为练习了!

本文档和其它文档可从这里下载:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.

如果对于FreeBSD有问题,请先阅读文档,如不能解决再联系<questions@FreeBSD.org>.
关于本文档的问题请发信联系 <doc@FreeBSD.org>.