FreeBSD 开发手册

The FreeBSD Documentation Project

FreeBSD 中文计划

  欢迎您阅读《FreeBSD开发手册》。 这本手册还在不断由许多人继续书写。 许多章节还是空白,有的章节亟待更新。 如果您对这个项目感兴趣并愿意有所贡献,请发信给 FreeBSD 文档计划邮件列表

   本文档的最新英文原始版本可从 FreeBSD Web 站点 获得, 最新中文译本可从 FreeBSD 中文计划 Web 站点获得。 此外, 您也可以从 FreeBSD FTP 服务器 或众多的 镜像站点 得到这份文档的各种其他格式以及压缩形式的版本。

重要: 本文中许可证的非官方中文翻译仅供参考, 不作为判定任何责任的依据。如与英文原文有出入,则以英文原文为准。

在满足下列许可条件的前提下, 允许再分发或以源代码 (SGML DocBook) 或 “编译” (SGML, HTML, PDF, PostScript, RTF 等) 的经过修改或未修改的形式:

  1. 再分发源代码 (SGML DocBook) 必须不加修改的保留上述版权告示、 本条件清单和下述弃权书作为该文件的最先若干行。

  2. 再分发编译的形式 (转换为其它DTD、 PDF、 PostScript、 RTF 或其它形式), 必须将上述版权告示、本条件清单和下述弃权书复制到与分发品一同提供的文件, 以及其它材料中。

重要: 本文档由 FREEBSD DOCUMENTATION PROJECT “按现状条件” 提供, 并在此明示不提供任何明示或暗示的保障, 包括但不限于对商业适销性、 对特定目的的适用性的暗示保障。 任何情况下, FREEBSD DOCUMENTATION PROJECT 均不对任何直接、 间接、 偶然、 特殊、 惩罚性的, 或必然的损失 (包括但不限于替代商品或服务的采购、 使用、 数据或利益的损失或营业中断) 负责, 无论是如何导致的并以任何有责任逻辑的, 无论是否是在本文档使用以外以任何方式产生的契约、 严格责任或是民事侵权行为(包括疏忽或其它)中的, 即使已被告知发生该损失的可能性。

Redistribution and use in source (SGML DocBook) and 'compiled' forms (SGML, HTML, PDF, PostScript, RTF and so forth) with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code (SGML DocBook) must retain the above copyright notice, this list of conditions and the following disclaimer as the first lines of this file unmodified.

  2. Redistributions in compiled form (transformed to other DTDs, converted to PDF, PostScript, RTF and other formats) must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

重要: THIS DOCUMENTATION IS PROVIDED BY THE FREEBSD DOCUMENTATION PROJECT "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD DOCUMENTATION PROJECT BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

FreeBSD 是 FreeBSD基金会的注册商标

Apple, FireWire, Mac, Macintosh, Mac OS, Quicktime, 以及 TrueType 是 Apple Computer, Inc. 在美国以及其他国家的注册商标。

IBM, AIX, EtherJet, Netfinity, OS/2, PowerPC, PS/2, S/390, 和 ThinkPad 是 国际商用机器公司在美国和其他国家的注册商标或商标。

IEEE, POSIX, 和 802 是 Institute of Electrical and Electronics Engineers, Inc. 在美国的注册商标。

Intel, Celeron, EtherExpress, i386, i486, Itanium, Pentium, 和 Xeon 是 Intel Corporation 及其分支机构在美国和其他国家的商标或注册商标。

Linux 是 Linus Torvalds 的注册商标。

Microsoft, IntelliMouse, MS-DOS, Outlook, Windows, Windows Media, 和 Windows NT 是 Microsoft Corporation 在美国和/或其他国家的商标或注册商标。

Motif, OSF/1, 和 UNIX 是 The Open Group 在美国和其他国家的注册商标; IT DialTone 和 The Open Group 是其商标。

Sun, Sun Microsystems, Java, Java Virtual Machine, JavaServer Pages, JDK, JSP, JVM, Netra, Solaris, StarOffice, Sun Blade, Sun Enterprise, Sun Fire, SunOS, 和 Ultra 是 Sun Microsystems, Inc. 在美国和其他国家的商标或注册商标。

许多制造商和经销商使用一些称为商标的图案或文字设计来彰显自己的产品。 本文档中出现的, 为 FreeBSD Project 所知晓的商标,后面将以 '™' 或 '®' 符号来标注。


目录
第I部分. 基础
第1章 介绍
1.1 在 FreeBSD 上进行开发
1.2 BSD 理念
1.3 指导性架构设计原则
1.4 /usr/src的层次结构
第2章 编程工具
2.1 概述
2.2 介绍
2.3 编程初步
2.4 cc 编译
2.5 Make
2.6 调试
2.7 使用 Emacs 作为开发环境
2.8 补充阅读
第3章 安全的编程
3.1 提要
3.2 安全的设计方法
3.3 缓冲区溢出
3.4 SetUID 问题
3.5 限制你的程序环境
3.6 信任
3.7 竞态条件
第4章 本地化与国际化 - L10N 和 I18N
4.1 编写适应国际化的应用程序
第5章 源代码树指南和维护发展策略
5.1 Makefile 中的 MAINTAINER
5.2 第三方软件
5.3 妨碍性的 (Encumbered) 文件
5.4 共享库
第6章 回归与性能测试
6.1. 微性能测试列表
第II部分. 进程间通信
第7章 套接字
7.1 概述
7.2 联网和多样性
7.3 协议
7.4 套接字模型
7.5 重要的套接字函数
7.6 辅助函数
7.7 并发服务器
第8章 IPv6内部
8.1 IPv6/IPsec的实现
第III部分. 内核
第9章 DMA
9.1 DMA: What it is and How it Works
第10章 调试内核
10.1 如何将内核的崩溃转存数据保存成文件
10.2 使用 kgdb 调试内核的崩溃转存
10.3 使用 DDD 调试崩溃转存文件
10.4 对于崩溃转存的验尸式分析
10.5 使用 DDB 进行在线内核调试
10.6 使用远程 GDB 进行联机内核调试
10.7 用 GDB 调试可加载模块
10.8 如何调试控制台驱动
第IV部分. 系统结构
第11章 x86 汇编语言
11.1 概述
11.2 工具
11.3 系统调用
11.4 返回值
11.5 建立可移植的代码
11.6 编写第一个程序
11.7 编写 UNIX® 过滤程序
11.8 缓存 I/O
11.9 Command Line Arguments
11.10 UNIX 中的环境
11.11 文件处理
11.12 One-Pointed Mind
11.13 Using the FPU
11.14 忠告
11.15 致谢
第V部分. 附录
参考书目
索引
范例清单
例2-1. 一个 .emacs 配置文件的例子

第I部分. 基础


第1章  介绍

供稿:Murray Stokely 和 Jeroen Ruigrok van der Werven. 翻译:李 鑫.

1.1 在 FreeBSD 上进行开发

  欢迎您的到来。 现在您已经安装好操作系统, 并准备开始编程了。 但是, 从哪里开始呢? FreeBSD 提供了一些什么工具? 它能够为我, 一个程序员提供什么呢?

  这些都是本章准备回答的问题。 当然, 与所有其他职业类似, 人们对程序设计的熟练程度总是存在差异的。 对有些人而言, 它只是一种爱好; 而对另一些人而言, 这则是他们的职业。 这一章中的内容主要是针对初学程序设计的人而撰写; 当然, 对于那些不熟悉 FreeBSD 平台的程序员来说, 它也十分有用。


1.2 BSD 理念

  对最初的软件工具设计观念以及可用性、 性能与稳定性给予应有的尊重, 并制作最佳的类 UNIX® 操作系统软件包。


1.3 指导性架构设计原则

  下面的指导性设计原则描述了我们的设计理念

  • 只要某一功能的缺失不会导致无法完成某个实际的应用程序, 就不新增该功能。

  • 决定系统不做成什么样子, 与决定将它做成什么样子同样重要。 不去满足所有的需要, 而是让系统具备可扩展性, 使其能够向上兼容。

  • 尽可能抽象代码中的通用部分, 除非没有可以用来抽象的实例。

  • 如果没有完全理解一个问题, 最好干脆不提供任何解决方案。

  • 如果能用 10% 的工作完成 90% 的工作, 则选择较简单的解决方案。

  • 尽可能隔离复杂性。

  • 提供机制而非策略。 具体而言, 将用户界面策略交由客户去选定。

  摘自 Scheifler & Gettys: "X Window System"


1.4 /usr/src的层次结构

  FreeBSD 的完整源代码都可以从我们公开的 CVS 代码库中获取。 源代码通常会安装到 /usr/src 目录中, 它包括了下面这些目录:

  

目录 说明
bin/ /bin 中的文件的源代码
contrib/ 由其他开发组织维护的源代码
crypto/ 与密码学有关的源代码
etc/ /etc 中的文件的源代码
games/ /usr/games 中的文件的源代码
gnu/ 采用 GNU Public License 授权的工具
include/ /usr/include 中的文件的源代码
kerberos5/ 第 5 版 Kerberos 的源代码
lib/ /usr/lib 中的文件的源代码
libexec/ /usr/libexec 中的文件的源代码
release/ 用于制作 FreeBSD 发行版本的文件
rescue/ 建造系统时 /rescue中的工具
sbin/ /sbin 中的文件的源代码
secure/ FreeSec 的源代码
share/ /usr/share 中的文件的源代码
sys/ 内核的源代码文件
tools/ 用于维护和自动测试 FreeBSD 的工具
usr.bin/ /usr/bin 中的文件的源代码
usr.sbin/ /usr/sbin 中的文件的源代码



第2章  编程工具

供稿:James Raynard 和 Murray Stokely. 翻译:Jokhva.

2.1 概述

  这一章介绍了 FreeBSD 提供的一些编程工具,很多这些工具在其他版本的 UNIX 中都能使用。这里我们不会尝试描述任何编程细节。大 多数章节假设您以前没有或只有很少的编程知识,但我们希望程序员也能从中受益。


2.2 介绍

  FreeBSD 提供了一个非常优秀的开发环境。有 C,C++ 和 Fortran 编译器以及 基本系统自带的一个汇编器。更不要说 Perl 解释器和很多经典 UNIX 工具,比如 sedawk。如果还不够,Ports 中还有更 多的编译器。FreeBSD 与多种标准兼容, 比如 POSIX®ANSI C, 当然还有它自己的 BSD 传统。 因此, 我们在 FreeBSD 平台上写的应用不加修改或稍加修改就能在很多平台上运行。

  如果你从未在 UNIX 平台上写过程序,所有这些强大之处看起来会让人炫目。 这篇文档的目的就是帮助你迅速上手,而不许要深入更多高级论题。我们的目的就是让 这篇文档给你提供足够的基础知识来理解我们的文档。

  这篇文档不要求你有编程知识,或者你只有很少的编程知识。当然,我们假定你 会使用 UNIX 并且愿意来学。


2.3 编程初步

  程序就是一系列指令的集合,这些指令能驱使计算机去做不同的事情。有时候计 算机执行的一个指令取决于它所执行的前一个指令。本章将介绍两种主要的给出指令, 也叫 “命令”,的方式。一种方式是使用 解释器 ,另一种方式是使用 编译器。人类语言对 于计算机来说语义模糊太难于理解,因此计算机命令都用为了特定目的设计的的一种或 其他种计算机语言写的。


2.3.1 解释器

  使用解释器的计算机语言就像一个环境。解释器给出提示符后,你输入一个命 令,解释器就会执行这个命令。对于更复杂的程序,可以把命令写入一个文件然后 让解释器装入这个文件再执行其中的命令。如果有错误发生,许多解释器会进入一个 调试环境让你来追踪问题。

  这种方式的优点就是可以马上看见命令的执行结果,并且迅速的改正错误。如 果你想和别人分享自己的程序,最大的坏处就显现出来了。别人需要和你一样的解释 器,或者你必须把解释器给他们,而且他们还要知道怎么使用这个解释器。用户也不 希望在按错一个键的情况下就被扔到调试环境中。从程序执行效率来看,解释器会 使用很多内存,而且通常情况下生成的代码也不如编译器生成的有效率。

  我觉得,如果你从未编过程序,最好从解释类语言开始。Lisp,Smalltalk, Perl 和 Basic 语言就提供了这样的环境。UNIX 中的 shell (sh, csh) 自身就是一个解释器。实际情 况中,很多人都在他们自己的机器上写 shell “scripts” 来做很多 “维护”工作。的确,UNIX 的哲学中有一部分就是提供很多小工具, 并使用 shell scripts 把这些工具组合起来去做有用的工作。


2.3.2 FreeBSD 提供的解释器

  在 FreeBSD Ports Collection 中,有一个列表列出了提供的解释器。同时还简 单讨论了一些更受欢迎的解释类语言。

  从 Ports Collection 中如何取得并安装应用程序的教程可以从手册的 Ports section 中找到。

BASIC

Beginner's All-purpose Symbollic Instruction Code 的缩写。20世 纪50年代被开发出来给大学生学习编程。在20世纪80年代生产的个人电脑中包 含了这种语言。BASIC 语言是很多程序员学习的第一个 编程语言。同时,它也是 Visual Basic 的基础。

Bywater 的 Basic 解释器能在 Ports Collection 中的 lang/bwbasic 找到。Phil Cockroft 的 Basic 解释器 (以前叫 Rabbit Basic) 在 lang/pbasic 中。

Lisp

20世纪50年代开发的一种语言,作为当时那些“基于数字 ”的语言的补充。Lisp不是基于数字的,而是基于列表;实际上这个语 言的名字就是 “List Processing”的缩写。在 AI (Artificial Intelligence) 圈子中非常受欢迎。

Lisp 是一种非常强大而复杂的语言。但也可能变得异常庞大而臃肿。

在 FreeBSD 中的 Ports Collection 里面有各种可以在 UNIX 系统上运 行的 Lisp 实现。GNU Common Lisp 在 lang/gcl 里可以找到。Bruno Haible 和 Michael Stoll 的 CLISP 在 lang/clisp 中可以找到。而像包含一个可以高 度优化代码的编译器的 CMUCL,或者像比较简单的 Lisp 的实现的 SLisp 则 用几百行 C 代码实现了大部分 Common Lisp 的功能。两个语言分别在 lang/cmucllang/slisp 中可以找到。

Perl

对于系统管理员来说非常受欢迎的脚本语言;同时也经常被用来写万维 网服务器的 CGI 脚本。

Perl 在 Ports Collection 中的 lang/perl5 可以找到,适合所有 FreeBSD 版本。 而且在 4.X 版本中是作为基本系统的 /usr/bin/perl 来安装的。

Scheme

Lisp 的一个变种。比 Common Lisp 更加紧凑而清晰。在大学里面相当 流行因为足够简单。往往当作第一门语言教给大学生,而且在研究领域也有一 定程度的吸引力。

Scheme 在 Ports Collection 中的 lang/elk 可以找到 Elk Scheme 解释器。 lang/mit-scheme 中的是 MIT 的 Scheme 解释器。在 lang/scm 中是 SCM Scheme 解释器。

Icon

Icon 是一种高级语言,在很多方面适合处理字符和结构。FreeBSD 中的 Icon 版本在 lang/icon 中可以找到。

Logo

Logo 是一种容易学习的语言,并且在许多课程里面都作为一个介绍性 的编程语言存在。如果给小孩子上编程课程,Logo 是一个非常棒的开始。因 为,即使对小孩子来说,要用 Logo 画图形也是很容易的事情。

FreeBSD 中的 Logo 最新版本可以在 lang/logo 中找到。

Python

Python 是种面向对象的解释类语言。拥护的人都说 Python 是开始学 习编程的最佳语言。因为相对来说比较容易起步,而且与那些流行的能开发庞大而复 杂的解释类语言来比(Perl 和 Tcl 在这个方面很流行)一点也不差。

最新版本的 Python 可以在 Ports Collection 的 lang/python 中找到。

Ruby

Ruby 是一个解释类语言,并且是纯面向对象的。因为其语法容易理解 而变化多端并且适合开发以及维护庞大而复杂的程序而广泛流行。

Ruby 可以在 Ports Collection 中的 lang/ruby18 中找到。

Tcl 和 Tk

Tcl 是一个嵌入式的解释类语言,能够被移植到很多平台上面。因此变 得非常流行。它既可以快速地写出小的应用程序,也可以(和 Tk 一起使用, 一种图形工具)写出功能繁多的程序。

FreeBSD 中不同版本的 Tcl 都在 ports 里面。最新版本,Tcl 8.4,可以 在 lang/tcl84 中找到。




2.3.3 编译器

  编译器则非常不同。首先,代码可以使用编辑器写到一个或多个文件里面。然 后再使用编译器来编译代码,看这些代码是否能被接受。如果编译不能通过,咬紧牙 关您再打开编辑器重新修改吧;如果编译通过,并且编译器给了你一个程序。您可以 在命令行下执行或者到调试环境中执行以便查看代码有没有被正确执行。 [1]

  很明显,这种方式并不如解释器直接。但却可以让你做很多解释类语言无法做 的困难的甚至无法完成的工作。例如直接与操作系统交互──或者,你甚至可以 写自己的操作系统。如果你要写非常有效率的代码,编译器也很有用。编译器可以花 一些时间来优化代码,而解释器是无法完成的。另外,分享你写的编译类程序比解释 类语言要直接得多。只要把编译好的程序给别人就行,当然我们假定别人和你都有同 一类操作系统。

  编译类语言包括 Pascal,C 和 C++。C 和 C++ 很严格,适合那些有更多经验 的程序员;而 Pascal,从另一方面来说,被设计成为一个教学语言,适合初学者。 FreeBSD 在基本系统中没有提供 Pascal。但是在 Ports Collection 中有 GNU Pascal Compiler (GPC) 和 Free Pascal Compiler,分别在 lang/gpclang/fpc 中可以找到。

  如果你使用不同的程序来写编译类语言,编辑-编译-运行-调试 这个循环会变 得很烦人。很多商业编译器开发了 Integrated Development Environments (缩写为 IDE)。FreeBSD 在基本系统中没有包含 IDE,但是在 devel/kdevelop 提供了一个例子。你也可 以使用 Emacs 当作 IDE。把 Emacs 当作 IDE 在 第 2.7 节 中有讨 论。


2.4 用 cc 编译

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

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

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

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

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

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

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

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

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

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

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

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

% cc foobar.c

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

% cc foo.c bar.c

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

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

-o filename

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

% 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
       

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

-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) 了解更多。


2.5 Make

2.5.1 什么是 make

  当你写一个简单的程序,只有一到两个源文件的时候,输入

% cc file1.c file2.c

  就没什么问题,但如果有很多源文件就会很烦人──编译的时间也会很长。

  一个方法就是使用目标文件,只在源文件有改变的情况下才重新编译源文件。 因此你可以这样做:

% cc file1.o file2.o ... file37.c ...

  上次编译后,file37.c 发生了改变,但其他文 件没有。这样做可以让编译过程快很多,但是也不能解决累人的输入问题。

  或者我们可以使用一个 shell script 来解决输入问题,但是也需要重新编译 所有文件,在大型项目上很没有效率。

  如果有成百上千的源文件的话怎么办?如果我们在与很多人合作写程序,别人 对源文件进行了修改,又没有告诉你,该怎么办?

  也许我们可以把以上两种方法结合,写一种像 shell script 一样的东西。这 种文件包含某种技巧可以决定什么时候该对源文件进行编译。现在所有我们要的就是 一个程序可以懂得这种技巧,因为要懂得这种技巧,shell 还没那么大的能耐。

  这个程序就叫 make。它读入一个文件,叫 makefile,这个文件决定了源文件之间的依赖关系。而且 决定了源文件什么时候该编译什么时候不应该编译。例如,某个规则可以说 “ 如果 fromboz.ofromboz.c 要旧, 意思就是有人修改了 fromboz.c,因此我们需要重新编译这 个文件。”这个 makefile 还有规则通知 make 该 怎么 重新编译源文件,因此 make 是一个强大得多的工具。

  makefile 通常和相关的源文件保存在同一个目录下,可以叫做 makefileMakefile 或者 MAKEFILE。大多数程序员会使用 Makefile 这个名字,因为这样可以让这个文件被放在目录列 表的顶端,可以很容易得看见。 [6]


2.5.2 使用 make 的例子

  这是一个非常简单的 make 文件:

foo: foo.c
    cc -o foo foo.c

  包含两行,一行是依赖关系,一行是执行动作。

  依赖关系的那一行包含了程序的名字 (叫做 target),紧跟着一个冒号,然后是空格,最后是源文件的 名字。当 make读入这一行的时候,会检查 foo 是否存在;如果存在,就比较 foofoo.c 最后的修改时间有什 么不同。如果 foo 不存在,或者比 foo.c 要旧,就检查执行动作那一行看看该怎么做。换句话 说,就是 foo.c 需要重新编译的时候该怎么办。

  执行动作那一行以一个 tab (按下 tab) 开始,然后是你在命令行下产生 foo 所执行的命令。如果 foo 过期了,或者不存在,make 就会 执行这个命令来产生 foo。换句话说,这就是重新编译 foo.c 的规则。

  因此,当你输入 make 时,它会确定 foofoo.c 在修改时间上是否同 步。这个原则可以在 Makefile 里扩展到成百上千的目标文 件上──实际上,在 FreeBSD 里,你只要在合适的目录下输入 make world 就可以编译整个操作系统!

  makefile 另一个有用的特点就是目标文件不一定就是程序。例如,我们可以 有这样的 make 文件。

foo: foo.c
    cc -o foo foo.c

install:
    cp foo /home/me

  我们可以输入如下的命令告诉 make 该执行哪个目标:

% make target

  make 会只执行这个目标而忽略其他的目标。例如,如果 我们输入 make foo,就只有 foo 被执行,必要的情况下重新编译 foo 而不会继续执行 install 这个目标。

  如果我们只是输入 make 这个命令,make 总会寻找 第一个目标,并且在执行完以后就不管其他的目标了。例如,如果我们输入 make foo,make 就会转到 foo 这个目标,在必要的情况下重新编译 foo,而不会执行 install 目标, 然后就停止了。

  一定要注意,install 这个目标不依赖任何其他 的东西!这意味着我们一旦输入 make install,这个目标 下的所有命令都将被执行。这种情况下,foo 将被安装到用 户的家目录下。应用程序的 makefile 正是这样写的,以便程序在正确编译后可以被 安装到正确的目录。

  要尝试解释的话会比较容易让人混淆。如果你不太懂 make 是如何工作的,最好的办法就是先写一个简单的程序例如 “hello world” 以及和上面的例子相同的 make 文件再去实验。然后 再进一步,使用多个源文件,或者让你的源文件包含一个头文件。 touch 命令在这里就非常有用了──它能让在不改变文件内 容的情况下改变文件的日期。


2.5.3 Make 和 include-文件

  C 源码的开头经常有一系列被包含的头文件,例如 stdio.h。有一些是系统级的 头文件,有一些是你正在写的项目的头文件:

#include <stdio.h>
#include "foo.h"

int main(....

  要确定在你的 foo.h 被改变之后,这个文件也会被重 新编译,就要在你的 Makefile 这样写:

foo: foo.c foo.h

  当你的项目变得越来越大,你自己的头文件越来越多的时候,要追踪所有这些 头文件和所有依赖它的文件会是一件痛苦的事情。如果你改变了其中一个头文件,却 忘了重新编译所有依赖它的源文件,结果会是很失望的。gcc 有一个选项可以分析你的源文件然后产生一个头文件的列表和它的依赖关系: -MM

  如果你把下面的内容加到你的 Makefile 里面:

depend:
    gcc -E -MM *.c > .depend

  然后运行 make depend,就会产生一个 .depend,里面包含了目标文件,C 文件和头文件的列表:

foo.o: foo.c foo.h

  如果你改变了 foo.h,下一次运行 make 的时候,所有依赖 foo.h 的文件 就会被重新编译。

  每一次你增加一个头文件的时候,别忘了运行一次 make depend


2.5.4 FreeBSD 的 Makefile 文件

  写 Makefile 文件可以是很难的一件事情。幸运的是,像 FreeBSD 这样基于 BSD 的系统,系统本身就自带了一些非常强大的 Makefile 文件。一个很好的例子就 是 FreeBSD 的 ports 系统。这里列出了一个典型的 ports 的 Makefile 的重要部分:

MASTER_SITES=   ftp://freefall.cdrom.com/pub/FreeBSD/LOCAL_PORTS/
DISTFILES=      scheme-microcode+dist-7.3-freebsd.tgz

.include <bsd.port.mk>

  现在,如果我们进入这个 port 的目录然后输入 make,就有如下的步骤发生:

  1. 检查这个 port 的源文件在系统中是否存在。

  2. 如果不存在,根据 MASTER_SITES 指定的 URL,系统 将会使用 FTP 连接下载这个源文件。

  3. 计算源文件的校检码,然后与先前被记录的,已知的,完好的源文件校检 码对比。这样做就是要确认源文件在传送过程中没有损坏。

  4. 执行任何让源文件可以在 FreeBSD 上运行的修改──这叫做 打补丁

  5. 完成源文件需要的特殊的配置。(很多 UNIX 程序在被编译的时候都会 检测自己正在哪个版本的 UNIX 上面以及哪些可选的 UNIX 特性是存在的 ──这一步里面,在 FreeBSD 的 ports 框架中这些程序将会取得这些信息。

  6. 编译程序的源代码。实际上,我们将进入到源代码解压后生成的目录中, 然后再执行 make──程序自身的 make 文件包含了编 译程序所需要的信息。

  7. 现在我们有了一个编译好的程序。如果我们愿意,可以测试一下;当我们 对程序满意了以后,就输入 make install。这个命令 将把程序自身还有任何它依赖的文件都复制到正确的位置;在一个 包数据库 中会加上一条记录以便我们改变想法的时候可 以很简单的删除掉这个 port。

  现在我想你一定会同意,对于一个只有四行的脚本,这让人印象非常深刻!

  秘密就在最后一行,这一行告诉 make 去找系统级的 make 文件,叫做 bsd.port.mk。要忽略这一行很简单,但是 就是这一行做了所有聪明的工作──有人已经写好了一个 make 文件,让 make 去做刚才提到的步骤 (加上一些我没提到的,包括对可能 的错误的处理)。而且任何人都可以在自己的 make 文件中简单的加上一行命令来使 用这个文件!

  如果你想看看这些系统级的 make 文件,可以到 /usr/share/mk 里面找找。不过最好等到你对 make 文件有 了一点点感性的经验以后再去看。因为这些文件都非常复杂(而且如果你在看的时 候,最好准备一大杯浓浓的绿茶!)。


2.5.5 更多 make 的高级用法

  Make 是一个非常强大的工具,甚至还可以做比以上提到 的更复杂的工作。不幸的是,make 有不同的版本,各个版本之 间差别还很大。学习使用这个命令的最好的方法可能就是阅读文档──希望这一 章能够给你的学习打一个基础。

  FreeBSD 自带的 make 叫做 Berkeley make/usr/share/doc/psd/12.make 是一个教程。要看这个教程, 在那个目录中执行

% zmore paper.ascii.gz

   Ports 中的很多应用程序使用 GNU make,它 包含一个非常棒的 “info” 页面的集合。如果你安装了任何一个这样 的 port,GNU make 就会自动被安装为 gmake 命令。当然你也可以用正常的 port 或 package 的方式 来安装。

  要查看 GNU make 的 info 页面,你必须编辑 /usr/local/info 路径下的 dir 文 件,在其中添加一个条目。需要添加的内容可以是这样

 * Make: (make).                 The GNU Make utility.

  一旦你完成编辑,就可以输入命令 info 然后从菜 单中选择 make (或者在 Emacs 中,输入 C-h i)。


2.6 调试

2.6.1 调试器

  FreeBSD 自带的调试器叫 gdb (GNU debugger)。要运行,输入

% gdb progname

  然而大多数人喜欢在 Emacs 中运行这个命令。 可以这样来起动这个命令:

M-x gdb RET progname RET

  调试器能让你在一个可控制的环境中运行一个程序。例如,你可以一次运行程 序的一行代码,检查变量的值,改变这些值,或者让程序运行到某个定点然后停止等 等。你甚至可以调试内核,当然这样会比我们将要讨论的问题要多一点点技巧。

  gdb 有非常棒的在线帮助,还有同样棒的 info 页面。 因此这一章我们会把注意力集中到一些基本的命令上。

  最后,如果你不习惯这个命令的命令行界面,在 ports 中还有一个它的图形 前端 (xxgdb)。

  这一章准备只介绍 gdb 的使用方法,而不会牵涉到特殊 的问题比如调试内核。


2.6.2 在调试器中运行一个程序

  要最大限度的利用 gdb,需要使用 -g 这个选项来编译你的程序。如果你没有这样做,那么你只会看 到你正在调试的函数名字,而不是它的源代码。如果 gdb起动 时提示:

... (no debugging symbols found) ...

  你就知道你的程序在编译的时候没有使用 -g 选项。

  当 gdb 给出提示符,输入 break main。这就是告诉调试器跳过程序中预先设置的代码,从你的代码的最 开头开始。然后输入 run 来开始你的程序──这会从 预先设置的代码开始然后在调试器调用 main() 的时候就停 下来。(如果你曾迷惑 main() 是在哪里被调用的,现在应该 明白了吧!)

  现在你可以一步一步来检查你的程序,按下 n一次就查 一行。一旦你碰见了一个函数调用,可以输入 f 从函数调用中 退出来。你可以输入 updown 来快速 检查这个调用。

  这里列出了一个简单的例子。展示了怎样用 gdb 定位一个错 误。这是我们的程序(其中有一个明显的错误):

#include <stdio.h>

int bazz(int anint);

main() {
    int i;

    printf("This is my program\n");
    bazz(i);
    return 0;
}

int bazz(int anint) {
    printf("You gave me %d\n", anint);
    return anint;
}

  这个程序给 i 赋值 5 并把它传递给 一个函数 bazz(),这个函数将打印出我们给出的数值。

  我们现在编译并运行这个程序,我们会得到

% cc -g -o temp temp.c
% ./temp
This is my program
anint = 4231

  但这并不是我们想要的!应该看看到底发生了什么!

% gdb temp
GDB is free software and you are welcome to distribute copies of it
 under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.13 (i386-unknown-freebsd), Copyright 1994 Free Software Foundation, Inc.
(gdb) break main               Skip the set-up code
Breakpoint 1 at 0x160f: file temp.c, line 9.    gdb puts breakpoint at main()
(gdb) run                   Run as far as main()
Starting program: /home/james/tmp/temp      Program starts running

Breakpoint 1, main () at temp.c:9       gdb stops at main()
(gdb) n                       Go to next line
This is my program              Program prints out
(gdb) s                       step into bazz()
bazz (anint=4231) at temp.c:17          gdb displays stack frame
(gdb)

  停住!怎么 anint 会是 4231?难道 我们没有在函数 main() 中设定为 5 吗?现在我们转到 main() 来看看。

(gdb) up                   Move up call stack
#1  0x1625 in main () at temp.c:11      gdb displays stack frame
(gdb) p i                   Show us the value of i
$1 = 4231                   gdb displays 4231

  哦,天哪!看看这代码,我们忘了初始化 i 了。本来我们 是想的

...
main() {
    int i;

    i = 5;
    printf("This is my program\n");
...

  但是我们忘了 i=5; 这一行。因为我们没有初始化 i,这个变量在程序运行的时候就储存了偶然在那块内存中存在的 任意值。

注意: gdb 会显示我们进入或离开一个函数时的栈的值。即 使是我们在使用 updown 的时候。 这会显示函数的名称还有参数的值,让我们知道自己的位置以及正在发生什么事情。 (栈能储存程序在调用函数的时使用的参数,以及调用时的位置,以便程序在从函 数调用结束后知道自己的位置。)


2.6.3 检查 core 文件

  基本上 core 文件就是一个包含了程序崩溃时这个进程的所有信息的文件。在那 “遥远的黄金年代”,程序员不得不把 core 文件以十六进制的方式显示 出来,然后满头大汗的阅读机器码的手册,但是现在事情就简单得多了。顺便说一下, 在 FreeBSD 和其他的 4.4BSD 系统下,core 文件都叫作 progname.core 而不是简单叫 core,这样可以很清楚的表示出这个 core 文件是属于哪个 程序。

  要检查一个 core 文件,以通常的方式起动 gdb。不要 输入 break 或者 run,而要输入

(gdb) core progname.core

  如果你没有和 core 文件在同一个目录,首先要执行 dir /path/to/core/file

  你应该可以看见:

% gdb a.out
GDB is free software and you are welcome to distribute copies of it
 under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.13 (i386-unknown-freebsd), Copyright 1994 Free Software Foundation, Inc.
(gdb) core a.out.core
Core was generated by `a.out'.
Program terminated with signal 11, Segmentation fault.
Cannot access memory at address 0x7020796d.
#0  0x164a in bazz (anint=0x5) at temp.c:17
(gdb)

  这种情况下,运行的程序叫 a.out,因此 core 文件 就叫 a.out.core。我们知道程序崩溃的原因就是函数 bazz 试图访问一块不属于它的内存。

  有时候,能知道一个函数是怎么被调用的是非常有用处的。因为在一个复杂的 程序里面问题可能会发生在函数调用栈上面很远的地方。命令 bt 会让 gdb 输出函数调用栈的回溯追踪。

(gdb) bt
#0  0x164a in bazz (anint=0x5) at temp.c:17
#1  0xefbfd888 in end ()
#2  0x162c in main () at temp.c:11
(gdb)

  函数 end() 在一个程序崩溃的时候将被调用;在本例 中,函数 bazz() 是从 main() 中被 调用的。


2.6.4 粘付到一个正在运行的程序

  gdb 一个最精致的特性就是它能粘付到一个已经在运行 的程序上。当然,我们得首先假定你有足够的权限这样去做。一个常见的问题就是, 当我们在追踪一个包含子进程的程序时,如果你要追踪子进程,但是调试器只允许你 追踪父进程。

  你要做的就是起动另一个 gdb,然后用 ps 找出子进程的进程号。然后在 gdb中执行

(gdb) attach pid

  就可以像平时一样调试了。

  “这很好,”你可能在想,“当我这样做了以后,子进程就 会不见了”。别怕,亲爱的读者,我们可以这样来做(参照 gdb 的 info 页)

...
if ((pid = fork()) < 0)      /* _Always_ check this */
    error();
else if (pid == 0) {        /* child */
    int PauseMode = 1;

    while (PauseMode)
        sleep(10);  /* Wait until someone attaches to us */
    ...
} else {            /* parent */
    ...

  现在所有你要做的就是粘付到子进程,设置 PauseMode0,然后等待函数 sleep 返回!


2.7 使用 Emacs 作为开发环境

2.7.1 Emacs

  很不幸,UNIX 系统不像其他的系统那样带有一种“你要的全有,不要的更多”的,包含所有 的,巨大的程序开发环境。 [7] 但是,你可以搭建一个自己的开发环境。可能不会很漂亮,也不会非常集成化。但 是你可以按自己的需求来搭建。而且是免费的。你将拥有所有的源码。

  问题的答案就是 Emacs。如今有很多人厌恶它,也有很多喜欢它。如果你是前 者之一,恐怕这一章不会引起你的兴趣。而且,你需要一定量的内存来运行 Emacs──文字界面我推荐 8MB,而在 X 下最少需要 16MB 来获得合理的性能。

  Emacs 基本上是一个高度可配置的编辑器──实际上,Emacs 更像一个操 作系统而不像一个编辑器!很多开发人员和系统管理员把所有的时间都花在 Emacs 里面,只在退出登陆的时候才退出这个编辑器。

  要在这里概括所有 Emacs 能做的事情是不可能的,但是这里列出了一些开发人员可能感 兴趣的特性:

  • 非常强大的编辑器,允许对字串和正则表达式(类型)进行搜索和替代。跳 至块结构的开始/末尾,等等。

  • 下拉菜单和在线帮助。

  • 语言相关的语法高亮显示和缩进。

  • 完全可配置。

  • 你可以在 Emacs 中编译和调试程序。

  • 出现编译错误以后,你可以直接跳至出问题的那一行代码。

  • 比较友好的 info 的前端,可以阅读 GNU 超文本文 档。当然包括 Emacs 自己的文档。

  • 友好的 gdb 的前端,允许你在追踪程序的时候查看 源代码。

  • 你可以在编译程序的同时查看 Usenet 新闻和阅读邮件。

  毫无疑问还有很多被我忽略的。

  在 FreeBSD 上可以用 the Emacs port 来安装 Emacs。

  一旦安装好了,就可以运行 Emacs,然后输入 C-h t 阅读 Emacs 教程──意思就是说按住 control,再按 h,松开 control,然后再按 t。(或者,你可以使用鼠 标从 Help 菜单重选择 Emacs Tutorial)。

  尽管 Emacs 有菜单,最好还是学习一下键组合。因为在你编辑的时候,连续 地按下一系列按键,比找到鼠标然后点击正确的地方要快得多。而且,当你和一个老 Emacs 用户交流的时候,你经常会碰到下列的表达 “M-x replace-s RET foo RET bar RET”,因此知道这些东西会很有用。而且在任 何情况下,Emacs 的菜单里永远放不下所有它实际上拥有的有用的功能。

  幸运的是,很容易学习键组合。因为菜单每个项目的后面都标示了对应的键组 合。我的建议就是,首先使用菜单项,比如,打开一个文件,直到你明白了其中的奥 妙,并且可以自信的使用这个菜单项,再尝试使用 C-x C-f。当你一点困难也没有的 时候,就可以转到下一个菜单项继续练习。

  如果记不住一个特殊的键组合到底能做什么,可以从 Help 菜单中选择 Describe Key ,然后输入这个键组合──Emacs 会告诉你它到底能干什么。你也可以点击 Command Apropos来寻找包含一个特定词的命令, 后面紧跟的就是键组合。

  另外,刚才那个表达式的意思就是按住 Meta 键,按下 x 键,松开 Meta 键,输入 replace-s (replace-string的简写 ──Emacs 另一个特性就是命令的缩写),按下 return键,输 入 foo(你要替换的字串),输入 bar (你要用来替换 foo 的字串) 然后再次按下 return 键。 Emacs 就会按你的要求进行搜索和替换操作。

  你一定在疑惑 Meta 键是个什么键。这是一个很多 UNIX 工作站都有的特殊的键。很不幸,PC没有这样一个键。通常在 PC 上这个键是 alt 键(如果你运气不好,这个键在你的 PC 上会是 escape 键)。

  哦,要退出 Emacs,键入 C-x C-c (意思就是按住 control 键,按下 x,按下 c,再松开 control 键)。如果你还有已经打 开的未保存的文件,Emacs 会问你是否要保存文件。(不要理会文档中说的退出 Emacs 的常用方法 C-z──这个键组合会把 Emacs 放到后 台,而且这个方法只在没有虚拟控制台的系统上有用)。


2.7.2 配置 Emacs

  Emacs 能做很多有用的事情;一些是内置的,另外一些需要我们进行配置。

  Emacs 没有用一种私有的宏语言来配置自身,而是使用了某种特别适应编辑器 的 Lisp 版本,叫做 Emacs Lisp。如果你要继续读下去并且想学习一点 Common Lisp,学习使用 Emacs Lisp 是很有用的。Emacs Lisp 有很多 Common Lisp 的特性, 虽然前者相当小 (因此更容易掌握)。

  学习 Emacs Lisp 最好的方法就是下载 Emacs Tutorial

  但是,要配置 Emacs 并不需要任何实际的 Lisp 知识,因为我已经列出了一 个 .emacs 例子,足够让你顺利的开始工作。只要把这个文 件复制到你的家目录,如果 Emacs 已经在运行,就重新起动;Emacs 会从这个文件 中读取命令,然后(希望)能给你一个有用的基本设置。


2.7.3 一个 .emacs 配置文件的例子

  不幸的是,要详细解释的话话就长了;但是还是有一两点值得注意。

  • ; 开头的是注释,会被 Emacs 忽略掉。

  • 第一行里面的 -*- Emacs-Lisp -*- 能 让我们在 Emacs 里面编辑这个 .emacs,并且打开所 有 Emacs Lisp 的编辑特性。Emacs 一般会尝试根据文件名来猜测,而且很有可 能猜错。

  • 在某些模式下,tab 键被绑定到一个缩进函数上。因 此按下 tab 键后,它能缩进一行代码。如果你想把 tab 当作 一个字符插入到你编辑的东西里面,需要在按下 tab 键的时 同时按住 control 键。

  • 这个文件通过识别文件名后缀来支持 C,C++,Perl,Lisp 和 Scheme 的语法高亮。

  • Emacs 已经有一个预先定义的函数叫 next-error。 在一个编译错误输出窗口,按下 M-n 能让从一个编译错误 移动到另一个;我们还定义了一个类似的函数, previous-error,这个函数在你按下 M-p 后,能让你回到上一个编译错误。其中最好的特性就是按 下 C-c C-c 后,能根据错误打开相应的文件并且跳到相应 的那行代码。

  • 我们打开了 Emacs 作为 服务端运行的特性,这样当你在 Emacs 外做一 些事情的时候,又需要编辑一个文件的时候,只需要输入

    % emacsclient filename
         
    

    然后就可以在 Emacs 编辑那个文件了! [8]

例 2-1. 一个 .emacs 配置文件的例子

;; -*-Emacs-Lisp-*-

;; This file is designed to be re-evaled; use the variable first-time
;; to avoid any problems with this.
(defvar first-time t
  "Flag signifying this is the first time that .emacs has been evaled")

;; Meta
(global-set-key "\M- " 'set-mark-command)
(global-set-key "\M-\C-h" 'backward-kill-word)
(global-set-key "\M-\C-r" 'query-replace)
(global-set-key "\M-r" 'replace-string)
(global-set-key "\M-g" 'goto-line)
(global-set-key "\M-h" 'help-command)

;; Function keys
(global-set-key [f1] 'manual-entry)
(global-set-key [f2] 'info)
(global-set-key [f3] 'repeat-complex-command)
(global-set-key [f4] 'advertised-undo)
(global-set-key [f5] 'eval-current-buffer)
(global-set-key [f6] 'buffer-menu)
(global-set-key [f7] 'other-window)
(global-set-key [f8] 'find-file)
(global-set-key [f9] 'save-buffer)
(global-set-key [f10] 'next-error)
(global-set-key [f11] 'compile)
(global-set-key [f12] 'grep)
(global-set-key [C-f1] 'compile)
(global-set-key [C-f2] 'grep)
(global-set-key [C-f3] 'next-error)
(global-set-key [C-f4] 'previous-error)
(global-set-key [C-f5] 'display-faces)
(global-set-key [C-f8] 'dired)
(global-set-key [C-f10] 'kill-compilation)

;; Keypad bindings
(global-set-key [up] "\C-p")
(global-set-key [down] "\C-n")
(global-set-key [left] "\C-b")
(global-set-key [right] "\C-f")
(global-set-key [home] "\C-a")
(global-set-key [end] "\C-e")
(global-set-key [prior] "\M-v")
(global-set-key [next] "\C-v")
(global-set-key [C-up] "\M-\C-b")
(global-set-key [C-down] "\M-\C-f")
(global-set-key [C-left] "\M-b")
(global-set-key [C-right] "\M-f")
(global-set-key [C-home] "\M-<")
(global-set-key [C-end] "\M->")
(global-set-key [C-prior] "\M-<")
(global-set-key [C-next] "\M->")

;; Mouse
(global-set-key [mouse-3] 'imenu)

;; Misc
(global-set-key [C-tab] "\C-q\t")   ; Control tab quotes a tab.
(setq backup-by-copying-when-mismatch t)

;; Treat 'y' or <CR> as yes, 'n' as no.
(fset 'yes-or-no-p 'y-or-n-p)
(define-key query-replace-map [return] 'act)
(define-key query-replace-map [?\C-m] 'act)

;; Load packages
(require 'desktop)
(require 'tar-mode)

;; Pretty diff mode
(autoload 'ediff-buffers "ediff" "Intelligent Emacs interface to diff" t)
(autoload 'ediff-files "ediff" "Intelligent Emacs interface to diff" t)
(autoload 'ediff-files-remote "ediff"
  "Intelligent Emacs interface to diff")

(if first-time
    (setq auto-mode-alist
      (append '(("\\.cpp$" . c++-mode)
            ("\\.hpp$" . c++-mode)
            ("\\.lsp$" . lisp-mode)
            ("\\.scm$" . scheme-mode)
            ("\\.pl$" . perl-mode)
            ) auto-mode-alist)))

;; Auto font lock mode
(defvar font-lock-auto-mode-list
  (list 'c-mode 'c++-mode 'c++-c-mode 'emacs-lisp-mode 'lisp-mode 'perl-mode 'scheme-mode)
  "List of modes to always start in font-lock-mode")

(defvar font-lock-mode-keyword-alist
  '((c++-c-mode . c-font-lock-keywords)
    (perl-mode . perl-font-lock-keywords))
  "Associations between modes and keywords")

(defun font-lock-auto-mode-select ()
  "Automatically select font-lock-mode if the current major mode is in font-lock-auto-mode-list"
  (if (memq major-mode font-lock-auto-mode-list)
      (progn
    (font-lock-mode t))
    )
  )

(global-set-key [M-f1] 'font-lock-fontify-buffer)

;; New dabbrev stuff
;(require 'new-dabbrev)
(setq dabbrev-always-check-other-buffers t)
(setq dabbrev-abbrev-char-regexp "\\sw\\|\\s_")
(add-hook 'emacs-lisp-mode-hook
      '(lambda ()
         (set (make-local-variable 'dabbrev-case-fold-search) nil)
         (set (make-local-variable 'dabbrev-case-replace) nil)))
(add-hook 'c-mode-hook
      '(lambda ()
         (set (make-local-variable 'dabbrev-case-fold-search) nil)
         (set (make-local-variable 'dabbrev-case-replace) nil)))
(add-hook 'text-mode-hook
      '(lambda ()
         (set (make-local-variable 'dabbrev-case-fold-search) t)
         (set (make-local-variable 'dabbrev-case-replace) t)))

;; C++ and C mode...
(defun my-c++-mode-hook ()
  (setq tab-width 4)
  (define-key c++-mode-map "\C-m" 'reindent-then-newline-and-indent)
  (define-key c++-mode-map "\C-ce" 'c-comment-edit)
  (setq c++-auto-hungry-initial-state 'none)
  (setq c++-delete-function 'backward-delete-char)
  (setq c++-tab-always-indent t)
  (setq c-indent-level 4)
  (setq c-continued-statement-offset 4)
  (setq c++-empty-arglist-indent 4))

(defun my-c-mode-hook ()
  (setq tab-width 4)
  (define-key c-mode-map "\C-m" 'reindent-then-newline-and-indent)
  (define-key c-mode-map "\C-ce" 'c-comment-edit)
  (setq c-auto-hungry-initial-state 'none)
  (setq c-delete-function 'backward-delete-char)
  (setq c-tab-always-indent t)
;; BSD-ish indentation style
  (setq c-indent-level 4)
  (setq c-continued-statement-offset 4)
  (setq c-brace-offset -4)
  (setq c-argdecl-indent 0)
  (setq c-label-offset -4))

;; Perl mode
(defun my-perl-mode-hook ()
  (setq tab-width 4)
  (define-key c++-mode-map "\C-m" 'reindent-then-newline-and-indent)
  (setq perl-indent-level 4)
  (setq perl-continued-statement-offset 4))

;; Scheme mode...
(defun my-scheme-mode-hook ()
  (define-key scheme-mode-map "\C-m" 'reindent-then-newline-and-indent))

;; Emacs-Lisp mode...
(defun my-lisp-mode-hook ()
  (define-key lisp-mode-map "\C-m" 'reindent-then-newline-and-indent)
  (define-key lisp-mode-map "\C-i" 'lisp-indent-line)
  (define-key lisp-mode-map "\C-j" 'eval-print-last-sexp))

;; Add all of the hooks...
(add-hook 'c++-mode-hook 'my-c++-mode-hook)
(add-hook 'c-mode-hook 'my-c-mode-hook)
(add-hook 'scheme-mode-hook 'my-scheme-mode-hook)
(add-hook 'emacs-lisp-mode-hook 'my-lisp-mode-hook)
(add-hook 'lisp-mode-hook 'my-lisp-mode-hook)
(add-hook 'perl-mode-hook 'my-perl-mode-hook)

;; Complement to next-error
(defun previous-error (n)
  "Visit previous compilation error message and corresponding source code."
  (interactive "p")
  (next-error (- n)))

;; Misc...
(transient-mark-mode 1)
(setq mark-even-if-inactive t)
(setq visible-bell nil)
(setq next-line-add-newlines nil)
(setq compile-command "make")
(setq suggest-key-bindings nil)
(put 'eval-expression 'disabled nil)
(put 'narrow-to-region 'disabled nil)
(put 'set-goal-column 'disabled nil)
(if (>= emacs-major-version 21)
    (setq show-trailing-whitespace t))

;; Elisp archive searching
(autoload 'format-lisp-code-directory "lispdir" nil t)
(autoload 'lisp-dir-apropos "lispdir" nil t)
(autoload 'lisp-dir-retrieve "lispdir" nil t)
(autoload 'lisp-dir-verify "lispdir" nil t)

;; Font lock mode
(defun my-make-face (face color &optional bold)
  "Create a face from a color and optionally make it bold"
  (make-face face)
  (copy-face 'default face)
  (set-face-foreground face color)
  (if bold (make-face-bold face))
  )

(if (eq window-system 'x)
    (progn
      (my-make-face 'blue "blue")
      (my-make-face 'red "red")
      (my-make-face 'green "dark green")
      (setq font-lock-comment-face 'blue)
      (setq font-lock-string-face 'bold)
      (setq font-lock-type-face 'bold)
      (setq font-lock-keyword-face 'bold)
      (setq font-lock-function-name-face 'red)
      (setq font-lock-doc-string-face 'green)
      (add-hook 'find-file-hooks 'font-lock-auto-mode-select)

      (setq baud-rate 1000000)
      (global-set-key "\C-cmm" 'menu-bar-mode)
      (global-set-key "\C-cms" 'scroll-bar-mode)
      (global-set-key [backspace] 'backward-delete-char)
                    ;      (global-set-key [delete] 'delete-char)
      (standard-display-european t)
      (load-library "iso-transl")))

;; X11 or PC using direct screen writes
(if window-system
    (progn
      ;;      (global-set-key [M-f1] 'hilit-repaint-command)
      ;;      (global-set-key [M-f2] [?\C-u M-f1])
      (setq hilit-mode-enable-list
        '(not text-mode c-mode c++-mode emacs-lisp-mode lisp-mode
          scheme-mode)
        hilit-auto-highlight nil
        hilit-auto-rehighlight 'visible
        hilit-inhibit-hooks nil
        hilit-inhibit-rebinding t)
      (require 'hilit19)
      (require 'paren))
  (setq baud-rate 2400)         ; For slow serial connections
  )

;; TTY type terminal
(if (and (not window-system)
     (not (equal system-type 'ms-dos)))
    (progn
      (if first-time
      (progn
        (keyboard-translate ?\C-h ?\C-?)
        (keyboard-translate ?\C-? ?\C-h)))))

;; Under UNIX
(if (not (equal system-type 'ms-dos))
    (progn
      (if first-time
      (server-start))))

;; Add any face changes here
(add-hook 'term-setup-hook 'my-term-setup-hook)
(defun my-term-setup-hook ()
  (if (eq window-system 'pc)
      (progn
;;  (set-face-background 'default "red")
    )))

;; Restore the "desktop" - do this as late as possible
(if first-time
    (progn
      (desktop-load-default)
      (desktop-read)))

;; Indicate that this file has been read at least once
(setq first-time nil)

;; No need to debug anything now

(setq debug-on-error nil)

;; All done
(message "All done, %s%s" (user-login-name) ".")
   

2.7.4 扩展 Emacs 所支持语言的范围

  现在,如果你只是想用 .emacs 设定好的语言 (C, C++, Perl, Lisp 和 Scheme) 来编程,事情就很好办。但是,如果突然一个新的语 言,叫 “whizbang”,有很多激动人心的特性,出来了,会发生什么事情?

  第一件要做的事情就是找到是否有任何文件能够告诉 Emacs 关于这个语言的 信息。这种文件通常以 .el 结尾,是 “Emacs Lisp” 的缩写。例如,如果 whizbang 是 FreeBSD 的一个 port,那么我们 可以用如下命令来定位这些文件

% find /usr/ports/lang/whizbang -name "*.el" -print

  然后安装这些文件到 Emacs 的系统级 Lisp 目录。在 FreeBSD 2.1.0-Release 里,这个目录就是 /usr/local/share/emacs/site-lisp

  例如,如果刚才的定位命令的输出是

/usr/ports/lang/whizbang/work/misc/whizbang.el

  我们可以执行

# cp /usr/ports/lang/whizbang/work/misc/whizbang.el /usr/local/share/emacs/site-lisp

  下一步,我们需要确定 whizbang 的源文件是以什么后缀结尾。我们假定这些 源文件都是以 .wiz 结尾。我们需要在 .emacs 加上一条使 Emacs 能够使用 whizbang.el 中的信息。

  在 .emacs 中找到 auto-mode-alist entry,为 whizbang 添加一行,例如:

...
("\\.lsp$" . lisp-mode)
("\\.wiz$" . whizbang-mode)
("\\.scm$" . scheme-mode)
...

  意思就是,当你编辑一个以 .wiz 结尾的文件的时候, Emacs 会自动进入 whizbang-mode

  就在下面,你会发现 font-lock-auto-mode-list 这一条。 添加 whizbang-mode

;; Auto font lock mode
(defvar font-lock-auto-mode-list
  (list 'c-mode 'c++-mode 'c++-c-mode 'emacs-lisp-mode 'whizbang-mode 'lisp-mode 'perl-mode 'scheme-mode)
  "List of modes to always start in font-lock-mode")

  这意味着当你编辑 .wiz 文件的时候,Emacs 会自动 打开 font-lock-mode(就是语法高亮)。

  这就是所有必要的步骤。如果在你打开一个 .wiz 文 件的时候,还有需要自动执行的任何其他步骤,你可以添加一个 whizbang-mode hook (查看 my-scheme-mode-hook 中添加 auto-indent 的步骤作为例子)。


2.8 补充阅读

  关于 FreeBSD 下搭建开发环境的信息的修改,可以参看 development(7)

  • Brian Harvey and Matthew Wright Simply Scheme MIT 1994. ISBN 0-262-08226-8

  • Randall Schwartz Learning Perl O'Reilly 1993 ISBN 1-56592-042-2

  • Patrick Henry Winston and Berthold Klaus Paul Horn Lisp (3rd Edition) Addison-Wesley 1989 ISBN 0-201-08319-1

  • Brian W. Kernighan and Rob Pike The Unix Programming Environment Prentice-Hall 1984 ISBN 0-13-937681-X

  • Brian W. Kernighan and Dennis M. Ritchie The C Programming Language (2nd Edition) Prentice-Hall 1988 ISBN 0-13-110362-8

  • Bjarne Stroustrup The C++ Programming Language Addison-Wesley 1991 ISBN 0-201-53992-6

  • W. Richard Stevens Advanced Programming in the Unix Environment Addison-Wesley 1992 ISBN 0-201-56317-7

  • W. Richard Stevens Unix Network Programming Prentice-Hall 1990 ISBN 0-13-949876-1


第3章  安全的编程

供稿:Murray Stokely. 翻译:susn @NewSMTH.

3.1 提要

  本章描述了十年间一些令UNIX程序员感到困惑的安全问题, 并提供了一些新的工具来帮助程序员避免生成可被利用的代码。


3.2 安全的设计方法

  编写安全的应用程序要带着谨慎和略有悲观的生活观点。程序应该本着 “最小特权”的原则运行,这样就不会有带着大于足够能完成 其功能的权限的进程在运行。预先测试的代码应该随时可以重用以避免遇到 一些本已经修复的通常错误。

  UNIX环境的陷阱之一就是很容易的制造一个稳健环境的假象。程序 应该永远不要相信用户的输入(以各种形式),系统资源,进程间通讯,或者 触发事件的时钟。UNIX进程不是同步运行,所以逻辑操作很少是原子类型。


3.3 缓冲区溢出

  缓冲区溢出的漏洞随着冯·诺依曼 1 构架的出 现就已经开始出现了。 在1988年随着莫里斯互联网蠕虫的广泛传播他们开始声名狼藉。不幸的是, 同样的这种攻击一直持续到今天。 1999年的17个CERT(Computer Emergency Response Team, 卡内基梅隆大学计算机紧急响应小组)的安全通告中, 他们中的10个直接是由软件的缓冲区溢出而 导致的。到目前为止,大部分的缓冲区溢出的攻击都是基于摧毁栈的方式。

  大部分现代计算机系统使用栈来给进程传递参数并且存储局部变量。 栈是一种在进程映象内存的高地址内的后进先出(LIFO)的缓冲区。 当程序调用一个函数时一个新的“栈帧”会被创建。这个栈帧包含着 传递给函数的各种参数和一些动态的局部变量空间。“栈指针”记录着当前 栈顶的位置。 由于栈指针的值会因为新变量的压入栈顶而经常的变化,许多实现也提供了 一种"帧指针"来定位在栈帧的起始位置,以便局部变量可以更容易的被访问。 1调用函数的返回地址也同样存储在栈中, 由于在函数中的局部变量覆盖了函数的返回地址成为了栈溢出的一个原因, 这就潜在的准许了一个恶意用户可以执行他(她)所想运行的任何代码。

  虽然基于栈的攻击是目前最广泛的,这也可以使基于堆的攻击(malloc/ free)变成可能。

  C程序语言并不像其他一些编程语言一样自动的做数组或者指针的边 界检查。另外,C标准库还具有相当一些非常危险的操作函数。

strcpy(char *dest, const char *src)

可导致dest缓冲区溢出

strcat(char *dest, const char *src)

可导致dest缓冲区溢出

getwd(char *buf)

可导致buf缓冲区溢出

gets(char *s)

可导致s缓冲区溢出

[vf]scanf(const char *format, ...)

可导致参数溢出

realpath(char *path, char resolved_path[])

可导致path缓冲区溢出

[v]sprintf(char *str, const char *format, ...)

可导致str缓冲区溢出


3.3.1 缓冲区溢出示例

  下面的示例代码包含了一个缓冲区溢出的情况,它会覆盖函数的返回地址并且 立即跳过了紧随此函数之后调用。(授权于5)

#include <stdio.h>

void manipulate(char *buffer) {
  char newbuffer[80];
  strcpy(newbuffer,buffer);
}

int main() {
  char ch,buffer[4096];
  int i=0;

  while ((buffer[i++] = getchar()) != '\n') {};
  
  i=1;
  manipulate(buffer);
  i=2;
  printf("The value of i is : %d\n",i);
  return 0;
}

  让我们来查看一下如果在输入回车之前输入160个空格后这个小程序 的内存映象是个什么样子。

  [XXX figure here!]

  很明显更多的恶意输入能被设计出执行实际的编译指令(例如 exec(/bin/sh))。


3.3.2 避免缓冲区溢出

  对于栈溢出的最直接的解决方法就是总是使用长度有限的内存和 字符串复制函数。strncpystrncat 是C标准库的一部分。 这些函数接收一个不大于目标缓冲区长度的值作为参数。这些函数会从 源地址复制此值长的字节数到目标地址。然而这些函数还是有一些问题。 如果输入缓冲区的长度和目标缓冲区的一样长则函数不保证两者都以NUL 作为结束符。 长度参数在strncpy和strncat函数中同样的不一致很容易导致程序员在 正常使用时感到困惑。同时当复制一个较短的字符串到一个很大的缓冲 区中时相对于strcpy也有很重大的性能损失, 因为strncpy会用NUL填充所指定的长度。

  在OpenBSD中,另一个内存复制的实现已经规避了这些问题。 函数strlcpystrlcat 保证了当指定了非零的长度参数时目标字符串总是以NUL作为结束符。 关于这些函数的更多信息请参考7。OpenBSD 的strlcpystrlcat 自从FreeBSD3.3的版本已经被引入了。


3.3.2.1 基于编译器运行时边界检查

  不幸的是扔然有相当数量的代码在广泛使用盲目的内存复制功能 而不是我们所提及到的任何有限制的复制例程。幸运的是还有另一个解 决方案。有一些编译器插件和库在C/C++中一直在做运行时的边界检查。

  作为gcc代码生成器的一个小补丁StackGuard就是这样一款插件。 源自StackGuard 站点:

“StackGuard 检测并靠着保护在栈中的返回地址不 受到更改来防御针对栈的剧烈攻击。当函数被调用时StackGuard在栈中 紧邻返回地址放置了一个‘canary’(哨兵或探针)。如果函数返回时哨兵 已经被改变了,就是有针对栈的攻击实施了,那么程序会在syslog中发 出一个入侵警报并且停止运行。

“StackGuard作为gcc代码生成器的一个小补丁来实 现,特别是function_prolog()和function_epilog()程序。 增强的function_prolog()在函数开始时在栈中安装了哨兵,而 function_epilog()在函数退出时检查哨兵的完整性。任何其他破坏返 回地址的行为在函数返回时就这样被检测到了。



  使用StackGuard重新编译你的程序可以有效的防止大部分的缓冲 区溢出的攻击,但是这仍然是个折衷的办法。


3.3.2.2 基于库运行时边界检查

  基于编译器的机制对于不能重新编译的只有二进制的软件完 全无用。对于这些情况仍还是有很多库可以对C库中的不安全的函数 (strcpy, fscanf, getwd等)重新实现并确保这些函数决不回写 栈指针。

  • libsafe

  • libverify

  • libparanoia

  不幸的是这些基于库的防护有一些缺点。这些库仅仅保护和安全 相关的一小部分集合,他们忽略了实际的问题。如果程序使用参数 -fomit-frame-pointer进行编译的话这些防护也许会失败。同样,环境 变量LD_PRELOAD和LD_LIBRARY_PATH也可以被用户取消或者重置。


3.4 SetUID 问题

  对于给定的进程至少有6个不同的ID与之关联。因此你不得不非常 关注你的程序在任何特定时刻的权限问题。特别的,所有seteuid的程序 在不需要的时候会立刻放弃他们的特权。

  实际用户ID只能被超级用户进程改变。当用户初始登陆时 login程序设置它并且极少进行更改。

  如果程序准许seteuid位设置的话有效用户ID会被exec() 函数设置。应用程序可以调用seteuid() 在任何时候设置有效的用户ID为任意的实际用户ID或者保存 设置-用户-ID。当有效用户ID被 exec()函数设置后, 前一个ID的值会被保存在设置-用户-ID中。


3.5 限制你的程序环境

  传统的限制进程的方法是使用系统调用chroot() 。这个系统调用使得从进程及其任何子进程所引用的其他的 路径变为根路径。对于要使程序运行成功这个调用必须在引用的目录上 拥有执行(搜索)的权限。直到你使用了chdir() 在你的新环境中它才会实际的生效。同时应该注意到如果程序具有超级 用户的权限它很容易的摆脱chroot所设置的环境。它可能靠创建设备节点 来读取内核的内容,对程序在jail外绑定一个调试器,或者靠其他创造性 的方法来完成操作。

  系统调用chroot()的行为可以被 sysctl变量kern.chroot_allow_open_directories 的值在一定程度上控制。当此值为0时,如果有任何目录被打开 chroot()将会返回EPERM并失败。当被置为默认值1,如果 任何目录被打开并且进程已经准备调用 chroot() 那么chroot()将会返回EPERM并失败。对于其他的 值,对打开目录的检查会被完全的忽视。


3.5.1 FreeBSD的jail功能

  Jail的概念在chroot()之上作了延伸,它 靠限制超级用户的权力来创建了一个真正的"虚拟服务器"。一旦一个监 狱被设置好后整个网络必须通过特别的IP地址才能到达,在这里"超级用 户权限"的力量完全的受到限制。

  当在jail中时,所有在内核中使用suser() 调用的超级用户权限的尝试都会失败。然而,一些对suser() 的调用已经被更改为新的接口suser_xxx() 。这个函数对认可或者拒绝被限制的进程去取得超级用户的权 限的行为负责。

  一个在Jail环境中的超级用户进程有以下权力:

  • 使用可信任的操作: setuid, seteuid, setgid, setegid, setgroups, setreuid, setregid, setlogin

  • 使用setrlimit设置资源限制

  • 编辑一些sysctl节点值 (kern.hostname)

  • chroot()

  • 在 vnode 上设置标志: chflagsfchflags

  • 设置 vnode 节点的属性, 如访问权限、 所有者、 所有组、 尺寸、 上次访问时间, 以及修改时间。

  • 在互联网域上绑定特权端口 (端口号 < 1024)

  Jail是一个对于在一个安全环境中 运行一个仍有一些缺点的程序非常有用的工具。目前,IPC机制还没有被 更改到suser_xxx以至于像MySQL之类的程序还不 能运行在jail中。在jail中超级用户的存取可能还有非常有限的含义, 但是没有途径能正确的指出"非常有限"意味着什么。


3.5.2 POSIX®.1e 处理能力

  POSIX已经发布了一个工作草案,增加了事件审计,访问控制列表, 精细特权控制,信息标签和强制访问控制。

  这是一个正在进展中的工作并且是 TrustedBSD项目的重点。一些初始化的工作已经被提交到 FreeBSD-CURRENT(cap_set_proc(3))。


3.6 信任

  一个程序应该永远不要假设用户环境是健全的。这包括(但是决不限于此): 用户输入,信号,环境变量,资源,IPC,mmap(内存映射),工作目录的文件系统, 文件描述符,打开文件的数量,等等

  你永远不要假设你可以捕捉到所有的用户可能产生的非法输入样式。 换言之,你的程序应该过滤只准许一些你认为安全的特别的输入子集。 不正确的确认数据会导致各种错误,特别是在互联网上的CGI脚本。 对于文件名你应该额外小心比如路径("../", "/"),符号连接和shell的退出符。

  Perl有一个非常棒的特性叫做“Taint”模式能避免脚本从外部程序在不 安全的途径得到使用的数据。这个方式会检查命令行参数,环境变量,位置 信息确定系统调用(readdir(),readlink() ,getpwxxx())的结果和所有文件的输入。


3.7 竞态条件

  竞态条件是由和事件时间相关的意料之外的依赖所导致的反常行为。 换句话说,一个程序员不正确的假设一个特殊的事件总是在另一个事件之前发生。

  一些通常的导致竞态条件的原因是信号,存取检查和打开文件操作。 由于信号生来就是异步事件所以在处理他们时要特别当心。存取检查中使 用access(2)然后使用open(2) 是很明显的非原子操作。用户可以在两次调用中移走文件。换言之,有特 权的程序应该使用seteuid()然后直接调用 open()。沿着同一思路,一个程序应该总是在 open()之前设置正确的掩码来排除不合逻辑的 chmod()调用。


第4章  本地化与国际化 - L10N 和 I18N

翻译:susn @NewSMTH.

4.1 编写适应国际化的应用程序

  为了使你的程序对于使用其他语言的用户更加有用, 我们希望你的程序应该国际化。 GNU的gcc编译器和GUI库比如QT或者GTK等通过对字符串的特殊处理来支持国际化。 生成一个支持国际化的程序非常容易。 它使得发布者很快的把你的程序移植成其他语言。 请参看详细的I18N文档来获得更多的信息。

  对比与通常的理解,兼容国际化的代码非常容易编写。 通常,它只需要使用一些特殊的功能函数来包装一下你的字符串。 另外,请确认支持了宽字符或者多字节字符。


4.1.1 整合I18N成果的号召

  值得我们注意的是各个国家自己的I18N/L10N 的工作已经变成了重复彼此的工作。 我们中的许多人一直在做着重复的无效率的重复发明工作。 我们希望各个主要I18N的组织能聚集成一个大的组织担负起像 核心团队一样的责任。

  当前,我们希望,当你在编写或者移植I18N程序时, 能够发布给各个国家相关的FreeBSD邮件列表作为测试。 将来,我们希望能生成可以直接使用各种语言的应用程序 而不是还要做一些混乱的破解。

  FreeBSD 国际化邮件列表 已经建立。如果你是一个I18N/L10N的开发者, 请发送给我们任何你能想到的与之相关的说明,想法,问题等等。

  Michael C. Wu 将会维护I18N的工作,主页在 http://www.FreeBSD.org/~keichii/i18n/index.html。 请同时阅读BSDCon2000 I18N的由Clive Lin,Chia-Liang Kao和Michael C.Wu 在http://www.FreeBSD.org/~keichii/papers/ 提出的文章。


4.1.2 Perl 和 Python

  Perl和Python拥有I18N和处理宽字符的库。请使用他们来兼容I18N。

  在旧的FreeBSD版本中,Perl会给出一些没有在你的系统中安装了本地宽 字符的警告。你可以在你的shell中设置环境变量LD_PRELOAD/usr/lib/libxpg4.so

  在基于sh的shell中:

LD_PRELOAD=/usr/lib/libxpg4.so

  在基于C-sh的shell中:

setenv LD_PRELOAD /usr/lib/libxpg4.so

第5章  源代码树指南和维护发展策略

供稿:Poul-Henning Kamp. 翻译:CnYouker.

  这一章记述了 FreeBSD 源代码树各种各样的指南和有效的维护发展策略。


5.1 Makefile 中的 MAINTAINER

  如果 FreeBSD 发布的某一部分正由一个人或一群人来维护, 他们可以通过在源代码树中该部分的顶级目录里的 Makefile 中添加一行

MAINTAINER= 电子信箱地址
来把这一情况告诉全世界。 (译者注: 目前 src/ 的维护策略要求全部此类说明放在 MAINTAINERS 文件, 而非其他目录中的 Makefile 里。)

  具体意义如下:

  维护者拥有代码并负责维护代码。 这意味着他有责任修正错误和回应关于该代码的问题报告; 对于来自第三方的软件, 维护者还有责任在适当的时候更新其版本 (译者注: 维护者可以视第三方软件新版本是否已满足品质要求决定是否跟进)。

  在提交对有维护者的目录中的内容进行更改之前, 应先交给维护者复审。 只有代码维护者太长时间没有回复邮件的时候, 才可以不经维护者复审直接提交改动。 尽管如此, 只要可能的话, 仍然建议您找另外一些人来复审希望进行的改动。

  当然, 不可能随意让一个人或团体加入维护者的行列, 因为维护者必须同意承担相关的责任。 另一方面, 维护者并不一定只是一个 committer, 它也完全可以是一个团队。


5.2 第三方软件

供稿:Poul-Henning Kamp 和 David O'Brien. 翻译:李 鑫.

  FreeBSD 的发行版中, 可能有某些部分包含在 FreeBSD 项目之外活跃地维护着的软件。 由于历史原因, 我们将其称为 contributed 软件。 举例说来, 有 sendmailgccpatch 等等。

  在过去几年中, 我们尝试了许多不同的方法来处理这类软件, 这些方法都各有利弊, 因而也就没有明确的胜者。

  基于这种情况, 在经历了一些争吵之后, 我们选定了一种作为在未来引入此类软件的 “官方” 做法。 更进一步, 我们强烈建议已有的第三方软件都逐渐过渡到使用这种方法, 因为与先前使用的做法相比, 它具有十分明显的优越性, 例如, 取得与其他软件作者 (即使没有提供 cvs 访问权) “官方” 版本之间的差异会更容易, 等等。 这会使得将修正内容回馈给第三方软件的主要开发者变得非常容易。

  当然, 最终这些方法是需要由具体的人来落实的。 如果这一模式十分不适于某个具体的软件包, 则在得到 core team 以及其他开发者认可的前提下, 可以适当地进行例外处理。 是否能够持续维护第三方软件包, 则是进行这类决策的关键因素。

注意: 由于 RCS 文件格式以及 CVS 使用 vendor 分支时的一些不当设计, 对于那些仍处于 vendor 分支的文件是 强烈建议不要 进行任何小规模的以及对代码修饰性修改的。 类似 “拼写错误” 这样的问题, 就属于前面提到的 “修饰性” 修改一类, 因此对那些版本号为 1.1.x.x 的文件, 应该尽一切可能避免。 不恰当地修改一个字符, 都有可能会使代码库产生严重的膨胀。

  接下来将利用嵌入式语言 Tcl 来展示前面所提到的这种模式如何运作:

  src/contrib/tcl 目录中包含了由软件包作者发布的代码。 完全不适于 FreeBSD 的那些部分可以完全删去。 对于 Tcl 而言, 在导入之前就可以删去 macwin 以及 compat 这些子目录了。

  src/lib/libtcl 中只包含 bmake 风格的 Makefile, 它使用标准的 bsd.lib.mk makefile 规则来生成库文件, 并安装文档。

  src/usr.bin/tclsh 中只包含使用标准的 bsd.prog.mk 规则, 用于生成和安装 tclsh 程序及其所关联的联机手册的 bmake 风格的 Makefile

  src/tools/tools/tcl_bmake 中包含一组 shell-脚本, 这些脚本可以用于帮助人们更新 tcl 软件。 这样的脚本并不作为软件构建或安装过程的组成部分。

  接下来的重要注意事项就是, src/contrib/tcl 目录是按照以下的规则创建的: 这个目录中的源代码应与其作者发布的形式保持一致 (通过适当使用 CVS 的 vendor-分支, 并关闭 RCS 关键字扩展), 只加入尽可能少的 FreeBSD-专有的改动。 在 freefall 上的 'easy-import' 工具可以帮助完成这一导入过程, 但如果对此有任何疑问, 则一定要先询问一下有经验的人, 而不要在对它能 “正常运转” 的期望中铸成大错。 CVS 本身并不能忽略由于导入时的疏忽引发的事故, 而回退这种问题, 更是需要大量的额外操作才能做到。

  由于前面提到的那些关于 CVS 的 vendor 分支设计的制约, 我们要求来自软件原作者的 “官方” 补丁, 必须首先打在其分发的源代码之上, 然后再将修改过的代码重新导入到 vendor 分支。 官方补丁在任何时候, 都不应直接应用于从 FreeBSD 源代码库检出的源代码, 并执行 “commit” 操作, 因为这会破坏 vendor 分支的一致性, 并且, 由于这样的操作会导致产生修改冲突, 会给未来导入新版时带来麻烦。

  由于许多软件包可能会包含用于 FreeBSD 以外的体系结构和环境的文件, 我们允许在导入前从官方发行的代码中删去那些对 FreeBSD 无用的部分, 以期节省磁盘空间。 包含版权声明和发行说明, 以及其他类似的用于说明其他文件的文档, 则 应删除。

  如果方便的话, 应尽可能使用使用某些工具生成的 bmake Makefile 文件, 这些工具可以使升级到未来的新版本时的工作变得简单。 如果您完成了这类工作, 请务必将这些工具 (如果需要的话) 放到 src/tools 目录中与您所移植的程序对应的目录中, 以便为将来的维护者所利用。

  在 src/contrib/tcl 的顶级目录中, 应增加一个名为 FREEBSD-upgrade 的文件, 在其中说明一些类似下面的内容:

  • 删去了哪些文件。

  • 从何处可以获得原始的发行版本, 以及官方网站。

  • 如果有补丁, 应如何反馈给原作者。

  • 如果需要的话, 对 FreeBSD-专有 改动的概要说明。

  不过, 请不要将 FREEBSD-upgrade 随第三方软件的源代码一同导入。 相反, 您应在最初的 import 操作之后用 cvs add FREEBSD-upgrade ; cvs ci 来完成这一工作。 在 src/contrib/cpio 中, 这个文件的内容如下:

This directory contains virgin sources of the original distribution files
on a "vendor" branch.  Do not, under any circumstances, attempt to upgrade
the files in this directory via patches and a cvs commit.  New versions or
official-patch versions must be imported.  Please remember to import with
"-ko" to prevent CVS from corrupting any vendor RCS Ids.

For the import of GNU cpio 2.4.2, the following files were removed:

        INSTALL         cpio.info       mkdir.c             
        Makefile.in     cpio.texi       mkinstalldirs

To upgrade to a newer version of cpio, when it is available:
        1. Unpack the new version into an empty directory.
           [Do not make ANY changes to the files.]

        2. Remove the files listed above and any others that don't apply to
           FreeBSD.

        3. Use the command:
                cvs import -ko -m 'Virgin import of GNU cpio v<version>' \
                        src/contrib/cpio GNU cpio_<version>

           For example, to do the import of version 2.4.2, I typed:
                cvs import -ko -m 'Virgin import of GNU v2.4.2' \
                        src/contrib/cpio GNU cpio_2_4_2

        4. Follow the instructions printed out in step 3 to resolve any
           conflicts between local FreeBSD changes and the newer version.

Do not, under any circumstances, deviate from this procedure.

To make local changes to cpio, simply patch and commit to the main
branch (aka HEAD).  Never make local changes on the GNU branch.

All local changes should be submitted to "cpio@gnu.ai.mit.edu" for
inclusion in the next vendor release.

obrien@FreeBSD.org - 30 March 1997

5.3 妨碍性的 (Encumbered) 文件

  偶尔可能会需要在 FreeBSD 源代码树上包含某些妨碍性的文件。 例如, 如果某个设备需要首先加载一小段二进制代码才能正常工作, 而我们并没有这些代码的源文件, 则这个二进制文件就被认为是妨碍性的。 在 FreeBSD 源码树上引入这类妨碍性文件时的规则如下。

  1. 由系统 CPU 解释或执行的任何以非源代码格式保存的文件, 都被认为是妨碍性的。

  2. 授权限制多于 BSD 或 GNU 的任何文件都是妨碍性的。

  3. 除非适用 (1) 或 (2) 条款, 包含可以下载到硬件设备的文件并不被认为是妨碍性的。 这些文件必须保存为平台中立的 ASCII 格式 (推荐使用 file2c 或 uuencode 来进行编码)。

  4. 妨碍性的文件, 在加入到 CVS 代码库之前, 必须获得 核心小组 的明示批准。

  5. 妨碍性文件应置于 src/contribsrc/sys/contrib

  6. 应保持模块的整体性。 除非在非妨碍性代码之间存在代码复用, 否则不应将其割裂开来。

  7. 预编译的目标文件, 应命名为 体系结构名/文件名.o.uu

  8. 内核文件:

    1. 都应在 conf/files.* 中加以引用 (以简化构建过程)。

    2. 都应作为 LINT 的一部分, 但 核心小组 可以根据个案决定是否将其注释掉。 此外, 核心小组 可以在稍后改变这些决定。

    3. 正式发行工程师 (Release Engineer) 有权决定是否允许这些文件进入正式的发行版本。

  9. 用户级文件:

    1. 核心小组 有权决定这些代码是否应成为 make world 的一部分。

    2. 正式发行工程师 有权决定这些代码是否能进入正式发行版。


5.4 共享库

供稿:浅见 贤、 Peter Wemm 和 David O'Brien. 翻译:CnYouker.

  如果你想添加共享库支持到一个原来不包含共享库支持的 port 或是其它软件, 共享库的版本号应该遵循如下规则。通常来说,由此得出的数字与软件的发行版本无关。

  建立共享库的三个原则是:

  • 1.0开始

  • 如果改动与以前版本相兼容,增加副版本号(注意,ELF系统忽略副版本号)。

  • 如果是个不兼容的改动,增加主版本号。

  例如,添加函数和修正错误导致副版本号增加, 而删除函数、函数调用语法改变等,会迫使主版本号改变。

  保持这种形式的版本号:主版本号.副版本号 (x.y)。 我们的 a.out 动态链接器不能很好的处理 x.y.z 形式的版本号。在比较共享库版本号以决定跟哪个库文件链接的时候, 任何y以后的版本号(那是指第三个数字) 总是会被忽略。 如果给定的两个共享库的不同在于“细微”版本 (“micro” revision)的话, ld.so将会与较高修订版本的链接。即,如果你要与 libfoo.so.3.3.3链接,链接器只在(ELF文件的)头部记录 3.3, 并且在连接时,与文件名以 libfoo.so.3.(任何数字 >= 3).(现有的最高数字) 开头的任何文件链接。

注意: ld.so 总是会使用 “副”版本号最高的。例如,即使一个程序最初是(被设定)与 libc.so.2.0链接的, ld.so也会优先选择使用 libc.so.2.2,而不是 libc.so.2.0

  另外,我们的 ELF 动态链接器完全不处理副版本号。 可我们还是应该指定一个主版本号和副版本号,因为我们的 Makefile 会按系统类型“做正确的事”。

  对于不属于某个 port 的库文件,我们的原则是在各个 FreeBSD 正式发行版 (RELEASE)之间只改变一次共享库版本号(译者注:一般只是副版本号)。 并且,在 FreeBSD 正式发行版 (RELEASE) 主版本之间(那是指像从 3.x 到 4.x), 也应该仅改变一次共享库主版本号。 当你需要对系统库做一些改变并要增加版本号时, 请查看 Makefile的CVS提交日志。 这是 committer 的责任:确保自(最近的)正式发行版 (RELEASE) 之后只有第一次这样的改动会让在 Makefile 里的共享库版本号更新, 而随后的(在下一个 RELEASE 之前的)改动不会使共享库版本号更新。


第6章  回归与性能测试

翻译:Jokhva.

  回归测试通常用来检测系统中的特定部分是否如期工作, 并且要确定旧的错误没有重新出现。

  FreeBSD 的回归测试工具能够在 FreeBSD 的源代码树 src/tools/regression 中找到。


6.1. 微性能测试列表

这一章包含了一些在 FreeBSD 上或者 FreeBSD 自身做适合的微性能测试的建议。

要在每一次单独的测试的时候使用所有我们给出的建议是不可能的。 但是你用得越多,你测试小差别的能力就会越好。

  • 关闭 APM 和任何其他干扰时钟的东西 (ACPI ?)。

  • 进入单用户模式。例如,cron(8) 和其他的守护进程只会增加测试的不准确性。 sshd(8) 这个守护进程也会造成问题。如果在测试的时候需要 ssh 连接, 那么你或者关闭 SSHv1 的密匙再生功能,或者在测试的时候杀死 sshd 父进程。

  • 不要运行 ntpd(8)

  • 如果有 syslog(3) 事件发生,使用一个空白的 /etc/syslogd.conf 运行 syslogd(8), 或者,不要运行它。

  • 最小化磁盘 I/O,可能的话,要完全避免。

  • 不要挂载不必要的文件系统。

  • 可能的话,把 //usr, 和其他任何文件挂载为只读。 这样的话可以从 I/O 方面去掉到磁盘的异步更新(等等)。

  • 使用 newfs(8) 来生成要测试读写的文件系统。 在每次测试运行前使用 tar(1) 或者 dump(8) 给测试文件系统灌输文件。测试开始前先卸载文件系统, 然后在挂载。这样做的话能得到一个连续的文件系统格局。 对于经典测试,我们要测试的目录是 /usr/obj(用 newfs 重新初始化,然后再挂载)。 要获得 100% 的再现,请使用 dd(1) 产生的文件来灌输文件系统。(也就是: dd if=myimage of=/dev/ad0s1h bs=1m)

  • 使用基于 malloc 的或者预先装载的 md(4) 分区。

  • 测试的每次单独迭代之间重启系统。 这可以给你一个更连续的状态。

  • 从内核中去掉所有不重要的设备驱动。例如,如果测试不需要 USB, 那么就不要把 USB 放到内核中。驱动加载经常会产生延时。

  • 不要配置不使用的硬件。如果测试不使用硬盘,使用 atacontrol(8)camcontrol(8) 去掉硬盘。

  • 除非必要,否则不要配置网络, 或者等到测试完要把测试结果传输到另一台机器的时候再启动网络。

    如果系统必须连接到公共网络,一定要注意广播数据。 即使这些数据很难被注意到,也会占用 CPU 的时钟周期。 组播也有类似的情况。

  • 把每一个文件系统放到它自己的硬盘上。 这可以最小化磁盘的磁头搜索优化的抖动。

  • 尽量减少把结果输出到串口或 VGA 控制台。 将结果导入文件可以减少震动干扰。 (串口终端很容易变成一个瓶颈。) 在测试的时候不要触碰键盘,甚至spaceback-space 键也会以数字形式显示出来。

  • 确认你的测试足够长,但不是太长。如果测试太短, 打印时戳就是一个问题。 如果太长,温度的改变会影响计算机内的石英晶体的频率。 首要原则:多余一分钟,少于一个小时。

  • 尽量保证机器所在环境的温度恒定。 这会同时影响石英晶体和磁盘驱动器的算法。 要得到稳定的时钟,可以考虑使用 稳定时钟注入(stabilized clock injection)。例如,使用 OCXO + PLL, 把测试结果注入时钟电路而不是主板上的xtal。 要了解更多,请联系 Poul-Henning Kamp

  • 测试至少要运行三次。但是对于 “测试前” 和 “测试后” 的代码,最好分别都运行二十次以上。 尽可能交错执行测试,这样可以侦测测试环境对测试的影响。 不要 1:1 的交错,要 3:3 的交错, 这样就可以检测人机交互对测试的影响。

    好的类型,比如:bababa{bbbaaa}*, 可以在 1+1 的运行后给出一些提示(在测试出错时可以停止测试), 以及在首次 3+3 运行后的标准差(如果测试时间较长可以给出一些), 和测试运行的趋势与稍后一些交互数字。

  • 使用 usr/src/tools/tools/ministat 来查看数字是否重要。你可以买一本 “Cartoon guide to statistics” ISBN: 0062731025,高度推荐, 如果你已经忘记或者根本不知道标准差和 Student's T 测试。

  • 不要使用后台 fsck(8) 除非你是在对后台 fsck进行 benchmark。同时,在 /etc/rc.conf 中关闭 background_fsck,除非在系统起动后, 你的 benchmark 在 fsck 运行 60+ 秒后仍然没开始。 因为一旦开启了后台 fsckrc(8) 会把 fsck 唤醒并检查是否要在文件系统上运行之。 类似的,除非你的测试对象包括 snapshots, 就要确定系统中没有任何 snapshots。

  • 如果你的 benchmark 有预期之外的性能低下问题, 就要检测有没有来自未知系统资源的高频率中断。 有一些版本的 ACPI 就有 “运行混乱” 的问题,并且会产生过多的中断。 要诊断奇怪的测试结果,可以抓一些 vmstat -i 的片段然后查看是否有不正常的现象。

  • 要谨慎对待内核和用户空间的优化参数,对于调试也是这样。 因为很容易就会忽略一些东西, 然后才会意识到测试不能用来比较同样的事情。

  • 除非你的测试对内核参数 WITNESSINVARIANTS 有兴趣,否则不要在开启了这些内核参数后进行你的 benchmark。 WITNESS 会导致 400%+ 的性能损失。 类似的,用户态的 malloc(3) 参数在 -CURRENT 版本和生产版本中默认值都是不同的。

第II部分. 进程间通信

目录
第7章 套接字
第8章 IPv6内部

第7章  套接字

供稿:G. Adam Stanislav. 翻译:intron@intron.ac.

7.1 概述

  BSD 套接字(socket)将进程间通信推到一个新的水平。 彼此通信的进程可不再必须运行在同一计算机上。它们仍然还 能够运行在同一计算机上,但不再必须那样。

  不仅这些进程不必运行在同一计算机上, 它们也不必运行在同一种操作系统上。 有了 BSD 套接字,你的 FreeBSD 软件能够与运行在 Macintosh®中的程序顺利的协同工作,也可以与另一个在Sun™ 工作站上的,或是另一个运行在 Windows® 2000中的, 只要这些系统用以太网型的局域网相连。

  你的软件还可以很好的与运行在另一幢大楼,或是在另一个大陆、 在一艘潜艇中的,或是一架航天飞机中的进程协同工作。

  它也能够与并非属于计算机一部分(至少从术语的严格意义上说不是) 的组件协同工作,这种设备像打印机、数码相机、医疗设备, 大致只要是任何能够进行数字通信的东西。


7.2 联网和多样性

  我们已经暗示了联网的多样性问题。 许多不同的系统要彼此对话。它们必须说同一种语言。与此同时, 它们也必须理解同一种语言。

  人们常常认为肢体语言是通用的。 事实并非如此。回想在我刚刚十几岁时,我的父亲带我去保加利亚。 一次我们正坐在索非亚一座公园里的桌子旁,一个小贩上来向我们 推销烤杏仁。

  那时我还没有学习多少保加利亚语,我没有说“不”,而是摇了摇头, 那是“通用的”说的肢体语言。 小贩很快开始装给我们一些杏仁。

  然后我想起我曾被告知在保加利亚摇头表示。 很快,我又开始上下点头。小贩注意到了,就拿起他的杏仁走开了。 对于一个统一的观察者,我没有改变肢体语言:我继续使用摇头