显示标签为“makefile”的博文。显示所有博文
显示标签为“makefile”的博文。显示所有博文

2007年9月8日星期六

一个C++项目的Makefile编写-Tony与Alex的对话系列- [技术前沿] 2005-05-23

一个C++项目的Makefile编写-Tony与Alex的对话系列- [技术前沿] 2005-05-23

Tony : Hey Alex, How are you doing?
Alex : 不怎么样。(显得很消沉的样子)
Tony : Oh , Really ? What is the matter?
Alex : 事情是这样的。最近有一个Unix下的C++项目要求我独自完成,以前都是跟着别人做,现在让自己独立完成,还真是不知道该怎么办,就连一个最简单的项目的Makefile都搞不定。昨晚看了一晚上资料也没有什么头绪。唉!!
Tony : 别急,我曾经有一段时间研究过一些关于Makefile的东西,也许能帮得上忙,来,我们一起来设计这个项目的Makefile。
Alex : So it is a deal。(一言为定)
Tony : 我们现在就开始吧,给我拿把椅子过来。

(Tony坐在Alex电脑的旁边)
Tony : 把你的项目情况大概给我讲讲吧。
Alex : No Problem ! 这是一个“半成品”项目,也就是说我将提供一个开发框架供应用开发人员使用,一个类似MFC的东西。
Tony : 继续。
Alex : 我现在头脑中的项目目录结构是这样的:

APL (Alex's Programming Library)
-Make.properties
-Makefile(1)
-include //存放头文件
-Module1_1.h
-Module1_2.h
-Module2_1.h
-Module2_2.h
-src //存放源文件
-Makefile(2)
-module1
-Module1_1.cpp
-Module1_2.cpp
-Makefile(3)
-module2
-Module2_1.cpp
-Module2_2.cpp
-Makefile(3)
-...

-lib //存放该Project依赖的库文件,型如libxxx.a
-dist //存放该Project编译连接后的库文件libapl.a
-examples //存放使用该“半成品”搭建的例子应用的源程序
Makefile(4)
-appdemo1
-Makefile(5)
-src //存放应用源代码
-include
-bin //存放应用可执行程序
-appdemo2
-Makefile(5)
-src //存放应用源代码
-include
-bin //存放应用可执行程序
-...

Tony : I got it!
Alex : 下面我们该如何做呢?
Tony : 我们来分析一下各个Makefile的作用。你来分析一下各个级别目录下的Makefile的作用是什么呢?
Alex : (思考了一会儿)我想应该是这样的吧。
Makefile(3)负责将其module下的.cpp源文件编译为同名.o文件,同时其phony target "clean"负责删除该目录下的所有.o文件;
Makefile(2)负责调用src目录下所有module的Makefile文件。
Makefile(1)负责先调用src中的Makefile生成静态库文件,然后调用examples中的Makefile构建基于该框架的应用。
至于Make.properties,定义通用的目录信息变量、编译器参数变量和通用的依赖关系。

Tony : 说得很好。我们一点一点来,先从src中每个module下的Makefile着手,就如你所说在每个module下的Makefile负责将该module下的.cpp文件编译为同名的.o文件。
Alex : 好的,我来写吧,这个我还是能搞定的。看下面:
module1下的Makefile如下:

#
# Makefile for module1
#
all : Module1_1.o Module1_2.o

Module1_1.o : Module1_1.cpp
g++ -c $^ -I ../../include
Module1_2.o : Module1_2.cpp
g++ -c $^ -I ../../include

clean :
rm -f *.o

module2下的Makefile如下:

#
# Makefile for module2
#
all : Module2_1.o Module2_2.o

Module2_1.o : Module2_1.cpp
g++ -c $^ -I ../../include
Module2_2.o : Module2_2.cpp
g++ -c $^ -I ../../include

clean :
rm -f *.o

make一下,顺利产生相应的.o文件。

/*=============================================================
Note: 关于$^、$<和$@的用法说明:
$@ -- “$@”表示目标的集合,就像一个数组,“$@”依次取出目标,并执于命令。
$^ -- 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
$< -- 依赖目标中的第一个目标名字

举例: Module1_1.o Module1_2.o : Module1_1.cpp Module1_2.cpp
则$@ -- Module1_1.o Module1_2.o
$^ -- Module1_1.cpp Module1_2.cpp
$< -- Module1_1.cpp

==============================================================*/

Tony : Well done! 不过发现什么问题了么?
Alex : 什么问题?
Tony : 存在重复的东西。在重构中我们知道如果两个子类中都定义相同的接口函数,我们会将其pull up到基类中。同样我们可以重构我们的Makefile,把一些重复的东西拿到外层去。
Alex : (似乎略微明白了一些)我想有三处重复:a)查找头文件的路径是重复的; b)g++这个字符串可以用一个变量定义代替 c)编译器的编译参数可以也定义到一个变量中。我知道Make工具支持include一个文件,我们就建立一个公用的文件来存放一些通用的东西吧。
Tony : 没错,Just do it.
Alex : 就按我原先的想法,把这些公共的部分放到Make.properties中吧。

#
# Properties for demo's Makefile
#
MAKEFILE = Makefile

BASEDIR = $(HOME)/proj/demo

####################
# Directory layout #
####################
SRCDIR = $(BASEDIR)/src
INCLUDEDIR = $(BASEDIR)/include
LIBDIR = $(BASEDIRE)/lib
DISTDIR = $(BASEDIR)/dist

####################
# Compiler options #
# F_ -- FLAG #
####################
CC = g++

# Compiler search options
F_INCLUDE = -I$(INCLUDEDIR)
F_LIB = -L $(LIBDIR)

CFLAGS =
CPPFLAGS = $(CFLAGS) $(F_INCLUDE)

然后修改一下,各个module中的Makefile文件,以module1为例,修改后如下:
#
# Makefile for module1
#
include ../../Make.properties

all : Module1_1.o Module1_2.o

Module1_1.o : Module1_1.cpp
$(CC) -c $^ $(CPPFLAGS)
Module1_2.o : Module1_2.cpp
$(CC) -c $^ $(CPPFLAGS)

clean :
rm -f *.o

Tony : 其实这两个Makefile中还有一个隐含的重复的地方
Alex : 你是指依赖规则么?
Tony : 嗯,这个依赖规则在src中的各个module中都会用得到的。
Alex : 没错,我也是这么想的,我现在就把这个规则抽取出来,然后你来评审一下。我想利用make工具的传统的“后缀规则”来定义通用依赖规则,我在Make.properties加入下面的变量定义:

####################
# Common depends #
####################
DEPS = .cpp.o

然后还是以module1为例,修改module1的Makefile后如下:
#
# Makefile for module1
#
include ../../Make.properties

all : Module1_1.o Module1_2.o

$(DEPS):
$(CC) -c $^ $(CPPFLAGS)

clean :
rm -f *.o

Tony : 基本满足需求。我们可以进行上一个层次的Makefile的设计了。我们来设计Makefile(2)。Alex,你来回顾一下Makefile(2)的作用。
/*=============================================================
Note: 关于后缀规则的说明
后缀规则中所定义的后缀应该是make 所认识的,如果一个后缀是make 所认识的,那么这个规则就是单后缀规则,而如果两个
连在一起的后缀都被make 所认识,那就是双后缀规则。例如:".c"和".o"都是make 所知道。因而,如果你定义了一个规则是
".c.o"那么其就是双后缀规则,意义就是".c"是源文件的后缀,".o"是目标文件的后缀, ".c.o"意为利用.c文件构造同名.o文件。
==============================================================*/

Alex : No Problem! 正如前面说过的Makefile(2)负责调用src目录下所有module子目录下的Makefile文件,并负责将各个module下的.o文件打包 为libdemo.a文件放到dist目录中。所以存在简单的依赖关系就是libdemo.a依赖各个module子目录下的.o文件,而前面的 Makefile(3)已经帮我们解决了.o文件的生成问题了,即我们只需要逐个在各module子目录下make即可。我的Makefile(2)文件 设计如下:
#
# Makefile for src directory
#

include ../Make.properties

TARGET = libdemo.a

####################
# Subdirs define #
####################
MODULE1_PATH = module1
MODULE2_PATH = module2
SUBDIRS = $(MODULE1_PATH) $(MODULE2_PATH)

####################
# Objects define #
####################
MODULE1_OBJS = $(MODULE1_PATH)/Module1_1.o $(MODULE1_PATH)/Module1_2.o
MODULE2_OBJS = $(MODULE2_PATH)/Module2_1.o $(MODULE2_PATH)/Module2_2.o
DEMO_OBJS = $(MODULE1_OBJS) $(MODULE2_OBJS)

all : subdirs $(TARGET)
cp $(TARGET) $(DISTDIR)

subdirs:
@for i in $(SUBDIRS); do \
echo "===>$$i"; \
(cd $$i &&$(MAKE) -f $(MAKEFILE)) || exit 1; \
echo "<===$$i"; \
done

$(TARGET) : $(DEMO_OBJS)
ar -r $@ $^

clean:
@for i in $(SUBDIRS); do \
echo "===>$$i"; \
(cd $$i &&$(MAKE) clean -f $(MAKEFILE)) || exit 1; \
echo "<===$$i"; \
done
rm -f $(DISTDIR)/$(TARGET)

Tony : Alex你的进步真的是很大,分析问题的能力提高的很快,方法也不错。这个设计的缺点在于一旦新增了一个module子目录,这个Makefile文件就需要改动,不过改起来倒不是很难。有机会可以再想想,使这个Makefile更加通用。

Alex : 我记住了。我们继续么?
Tony : 歇一回吧^_^。

/*=============================================================
Alex and Tony are having a short break.
==============================================================*/

Tony : 你的咖啡味道真不错。
Alex : 这可是朋友从巴西带回来的极品咖啡豆,经过我精心研磨而成的。
Tony : 想不到你在这方面还有研究。
Alex : 呵呵。
Tony : Let's go on 。有了Makefile(2),后面的工作就轻松多了。
Alex : 现在我的信心也很足,我来设计Makefile(1),它负责先调用src中的Makefile生成静态库文件,然后调用examples中的 Makefile构建基于该框架的应用。我还是按照Makefile(2)的思路走,看我的Makefile(1):

#
# Makefile for whole project
#

include Make.properties

SRC_PATH = src
EXAMPLES_PATH = examples

SUBDIRS = $(SRC_PATH) $(EXAMPLES_PATH)

all : subdirs

subdirs:
@for i in $(SUBDIRS); do \
echo "===>$$i"; \
(cd $$i && $(MAKE) -f $(MAKEFILE)) || exit 1; \
echo "<===$$i"; \
done

clean:
@for i in $(SUBDIRS); do \
echo "===>$$i"; \
(cd $$i && $(MAKE) clean -f $(MAKEFILE)) || exit 1; \
echo "<===$$i"; \
done

运行一下,由于examples目录下的Makefile还是空的,所以没有成功。

Tony : 有了前面的经验,相信完成examples目录下的两个Makefile对你来说不成问题。
Alex : I could not agree with you any more(Alex脸上满是笑容),我来完成它。
每个appdemoX下的Makefile(5)我设计成这样:
#
# Makefile for appdemoX
#
include ../../Make.properties

TARGET = appdemoX
SRC = ./src/appdemoX.cpp

all :
$(CC) -o $(TARGET) $(SRC) $(CPPFLAGS) -L $(DISTDIR) -ldemo
mv $(TARGET).exe ./bin

clean :
rm -f ./src/*.o ./bin/$(TARGET).exe

而examples目录下的Makefile(4)的样子如下:
#
# Makefile for examples directory
#

include ../Make.properties

EXAMPLE1_PATH = appdemo1
EXAMPLE2_PATH = appdemo2

SUBDIRS = $(EXAMPLE1_PATH) $(EXAMPLE2_PATH)

all : subdirs

subdirs:
@for i in $(SUBDIRS); do \
echo "===>$$i"; \
(cd $$i &&$(MAKE) -f $(MAKEFILE)) || exit 1; \
echo "<===$$i"; \
done

clean:
@for i in $(SUBDIRS); do \
echo "===>$$i"; \
(cd $$i &&$(MAKE) clean -f $(MAKEFILE)) || exit 1; \
echo "<===$$i"; \
done

Tony : 可以,不知不觉间,我们的工作已经接近尾声,剩下的工作就是细节了,包括编译器参数的细化等。
Alex : 在Makefile(1)中加上install,tar等目标,使用户得到有更多的功能。十分感谢你的指导。
Tony : 那晚上去原味斋吧,想烤鸭了^_^。

/*=============================================================
Note : Makefile常识
a) "=" vs ":="
例子:
C_OPTIONS = $(C_EXTRA_OPTION) -O2
C_EXTRA_OPTION = -g
cfoo: foo.c exam.c
gcc $(C_OPTIONS) -o $@ $^
=>gcc -g -O2 -o cfoo foo.c exam.c


C_OPTIONS := $(C_EXTRA_OPTION) -O2
C_EXTRA_OPTION = -g
cfoo: foo.c exam.c
gcc $(C_OPTIONS) -o $@ $^
=>gcc -O2 -o cfoo foo.c exam.c

大 家发现不同了,Why? 使用“=”赋值的变量在使用时才被展开,并且每使用一次就会展开一次,其值每次展开的时候有可能是不同的,就如第一个C_OPTION由于在使用时展开, 所以C_EXTAR_OPTION定义的位置不影响C_OPTION的值。而使用“:=”进行赋值的变量,则在赋值的时候就被展开,并且仅仅展开一次,从 此以后其值将不会发生任何变化,就第二个C_OPTION由于定义时展开所以由于定义时看不到C_EXTAR_OPTION所以值为-02,而不是-g -02。

b) wildcard 函数
在 GNU Make 里有一个叫 'wildcard' 的函数,它有一个参数,功能是展开成一列所有符合由其参数描述的文件名,文件间以空格间隔。你可以像下面所示使用这个命令:SOURCES = $(wildcard *.cpp) <=> SOURCES = xx.cpp yy.cpp ... zz.cpp
==============================================================*/

2007年8月23日星期四

跟我一起写 Makefile

跟我一起写 Makefile


陈皓

概述
——

什 么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个工作,但我觉得要作一个好的和 professional的程序员,makefile还是要懂。这就好像现在有这么多的HTML的编辑器,但如果你想成为一个专业人士,你还是要了解 HTML的标识的含义。特别在Unix下的软件编译,你就不能不自己写makefile了,会不会写makefile,从一个侧面说明了一个人是否具备完 成大型工程的能力。

因为,makefile关系到了整个工程的编译规 则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需 要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

makefile 带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是 一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。

现 在讲述如何写makefile的文章比较少,这是我想写这篇文章的原因。当然,不同产商的make各不相同,也有不同的语法,但其本质都是在“文件依赖 性”上做文章,这里,我仅对GNU的make进行讲述,我的环境是RedHat Linux 8.0,make的版本是3.80。必竟,这个make是应用最为广泛的,也是用得最多的。而且其还是最遵循于IEEE 1003.2-1992 标准的(POSIX.2)。

在这篇文档中,将以C/C++的源码作为我们基础,所以必然涉及一些关于C/C++的编译的知识,相关于这方面的内容,还请各位查看相关的编译器的文档。这里所默认的编译器是UNIX下的GCC和CC。

关于程序的编译和链接
——————————

在 此,我想多说关于程序编译的一些规范和方法,一般来说,无论是C、C++、还是pas,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。

编译时,编译器 需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件 中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。

链 接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只 管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给 中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。

总结一下,源文件首先会生成中间目标文 件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成 Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error),在VC下,这种错误一般是:Link 2001错误,意思说是说,链接器未能找到函数的实现。你需要指定函数的Object File.

好,言归正传,GNU的make有许多的内容,闲言少叙,还是让我们开始吧。

Makefile 介绍
———————

make命令执行时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接程序。

首先,我们用一个示例来说明Makefile的书写规则。以便给大家一个感兴认识。这个示例来源于GNU的make使用手册,在这个示例中,我们的工程有8个C文件,和3个头文件,我们要写一个Makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:
1)如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
2)如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
3)如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。

只要我们的Makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。


一、Makefile的规则

在讲述这个Makefile之前,还是让我们先来粗略地看一看Makefile的规则。

target ... : prerequisites ...
command
...
...

target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。

prerequisites就是,要生成那个target所需要的文件或是目标。

command也就是make需要执行的命令。(任意的Shell命令)

这 是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说 白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是 Makefile的规则。也就是Makefile中最核心的内容。

说到底,Makefile的东西就是这样一点,好像我的这篇文档也该结束了。呵呵。还不尽然,这是Makefile的主线和核心,但要写好一个Makefile还不够,我会以后面一点一点地结合我的工作经验给你慢慢到来。内容还多着呢。:)


二、一个示例

正如前面所说的,如果一个工程有3个头文件,和8个C文件,我们为了完成前面所述的那三个规则,我们的Makefile应该是下面的这个样子的。

edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

反 斜杠(\)是换行符的意思。这样比较便于Makefile的易读。我们可以把这个内容保存在文件为“Makefile”或“makefile”的文件中, 然后在该目录下直接输入命令“make”就可以生成执行文件edit。如果要删除执行文件和所有的中间目标文件,那么,只要简单地执行一下“make clean”就可以了。

在这个makefile中,目标文件 (target)包含:执行文件edit和中间目标文件(*.o),依赖文件(prerequisites)就是冒号后面的那些 .c 文件和 .h文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质上就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。

在 定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个Tab键作为开头。记住,make并不管命令是怎么工作的,他只管执 行所定义的命令。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比 targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。

这 里要说明一点的是,clean不是一个文件,它只不过是一个动作名字,有点像C语言中的lable一样,其冒号后什么也没有,那么,make就不会自动去 找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个lable的名字。这样的方法非常有用,我们 可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。


2007年8月21日星期二

GNU make 指南

GNU make 指南

翻译: 哈少

译者按: 本文是一篇介绍 GNU Make 的文章,读完后读者应该基本掌握了 make 的用法。而 make 是所有想在 Unix (当然也包括 Linux )系统上编程的用户必须掌握的工具。如果你写的程序中没有用到 make ,则说明你写的程序只是个人的练习程序,不具有任何实用的价值。也许这么说有点 儿偏激,但 make 实在是应该用在任何稍具规模的程序中的。希望本文可以为中国的 Unix 编程初学者提供一点儿有用的资料。中国的 Linux 用户除了学会安装红帽子以外, 实在应该尝试写一些有用的程序。个人想法,大家参考。


C-Scene 题目 #2
多文件项目和 GNU Make 工具
作者: 乔治富特 (Goerge Foot)
电子邮件: george.foot@merton.ox.ac.uk
Occupation: Student at Merton College, Oxford University, England
职业:学生,默尔顿学院,牛津城大学,英格兰
IRC匿名: gfoot

拒绝承诺:作者对于任何因此而对任何事物造成的所有损害(你所拥有或不 拥有的实际的,抽象的,或者虚拟的)。所有的损坏都是你自己的责任,而 与我无关。

所有权: “多文件项目”部分属于作者的财产,版权归乔治富特1997年 五月至七月。其它部分属 CScene 财产,版权 CScene 1997年,保留所有 版权。本 CScene 文章的分发,部分或全部,应依照所有其它 CScene 的文章 的条件来处理。

0) 介绍
~~~~~~~~~~~~~~~
本文将首先介绍为什么要将你的C源代码分离成几个合理的独立档案,什么时 候需要分,怎么才能分的好。然后将会告诉你 GNU Make 怎样使你的编译和连 接步骤自动化。对于其它 Make 工具的用户来说,虽然在用其它类似工具时要 做适当的调整,本文的内容仍然是非常有用的。如果对你自己的编程工具有怀 疑,可以实际的试一试,但请先阅读用户手册。

1) 多文件项目
~~~~~~~~~~~~~~~~~~~~~~
    1.1为什么使用它们?

    首先,多文件项目的好处在那里呢?
    它们看起来把事情弄的复杂无比。又要 header 文件,又要 extern 声明,而且如果需要查找一个文件,你要在更多的文件里搜索。

    但其实我们有很有力的理由支持我们把一个项目分解成小块。当你改 动一行代码,编译器需要全部重新编译来生成一个新的可执行文件。 但如果你的项目是分开在几个小文件里,当你改动其中一个文件的时 候,别的源文件的目标文件(object files)已经存在,所以没有什么 原因去重新编译它们。你所需要做的只是重现编译被改动过的那个文 件,然后重新连接所有的目标文件罢了。在大型的项目中,这意味着 从很长的(几分钟到几小时)重新编译缩短为十几,二十几秒的简单 调整。

    只要通过基本的规划,将一个项目分解成多个小文件可使你更加容易 的找到一段代码。很简单,你根据代码的作用把你的代码分解到不同 的文件里。当你要看一段代码时,你可以准确的知道在那个文件中去 寻找它。

    从很多目标文件生成一个程序包 (Library)比从一个单一的大目标文件 生成要好的多。当然实际上这是否真是一个优势则是由你所用的系统 来决定的。但是当使用 gcc/ld (一个 GNU C 编译/连接器) 把一个程 序包连接到一个程序时,在连接的过程中,它会尝试不去连接没有使 用到的部分。但它每次只能从程序包中把一个完整的目标文件排除在 外。因此如果你参考一个程序包中某一个目标档中任何一个符号的话, 那么这个目标文件整个都会被连接进来。要是一个程序包被非常充分 的分解了的话,那么经连接后,得到的可执行文件会比从一个大目标 文件组成的程序包连接得到的文件小得多。

    又因为你的程序是很模块化的,文件之间的共享部分被减到最少,那 就有很多好处——可以很容易的追踪到臭虫,这些模块经常是可以用 在其它的项目里的,同时别人也可以更容易的理解你的一段代码是干 什么的。当然此外还有许多别的好处……

    1.2 何时分解你的项目

    很明显,把任何东西都分解是不合理的。象“世界,你们好”这样的 简单程序根本就不能分,因为实在也没什么可分的。把用于测试用的 小程序分解也是没什么意思的。但一般来说,当分解项目有助于布局、 发展和易读性的时候,我都会采取它。在大多数的情况下,这都是适 用的。(所谓“世界,你们好”,既 'hello world' ,只是一个介 绍一种编程语言时惯用的范例程序,它会在屏幕上显示一行 'hello world' 。是最简单的程序。)

    如果你需要开发一个相当大的项目,在开始前,应该考虑一下你将 如何实现它,并且生成几个文件(用适当的名字)来放你的代码。 当然,在你的项目开发的过程中,你可以建立新的文件,但如果你 这么做的话,说明你可能改变了当初的想法,你应该想想是否需要 对整体结构也进行相应的调整。

    对于中型的项目,你当然也可以采用上述技巧,但你也可以就那么开 始输入你的代码,当你的码多到难以管理的时候再把它们分解成不同 的档案。但以我的经验来说,开始时在脑子里形成一个大概的方案, 并且尽量遵从它,或在开发过程中,随着程序的需要而修改,会使开 发变得更加容易。

    1.3 怎样分解项目

    先说明,这完全是我个人的意见,你可以(也许你真的会?)用别的 方式来做。这会触动到有关编码风格的问题,而大家从来就没有停止 过在这个问题上的争论。在这里我只是给出我自己喜欢的做法(同时 也给出这么做的原因):
      i) 不要用一个 header 文件指向多个源码文件(例外:程序包 的 header 文件)。用一个 header定义一个源码文件的方式 会更有效,也更容易查寻。否则改变一个源文件的结构(并且 它的 header 文件)就必须重新编译好几个文件。

      ii) 如果可以的话,完全可以用超过一个的 header 文件来指向同 一个源码文件。有时将不可公开调用的函数原型,类型定义 等等,从它们的C源码文件中分离出来是非常有用的。使用一 个 header 文件装公开符号,用另一个装私人符号意味着如果 你改变了这个源码文件的内部结构,你可以只是重新编译它而 不需要重新编译那些使用它的公开 header 文件的其它的源文 件。

      iii) 不要在多个 header 文件中重复定义信息。 如果需要, 在其中一个 header 文件里 #include 另一个,但 是不要重复输入相同的 header 信息两次。原因是如果你以后改 变了这个信息,你只需要把它改变一次,不用搜索并改变另外一 个重复的信息。

      iv) 在每一个源码文件里, #include 那些声明了源码文件中的符 号的所有 header 文件。这样一来,你在源码文件和 header 文件对某些函数做出的矛盾声明可以比较容易的被编译器发现。

    1.4 对于常见错误的注释

      a) 定义符 (Identifier) 在源码文件中的矛盾:在C里,变量和函数的缺 省状态是公用的。因此,任何C源码档案都可以引用存在于其它源 码档中的通用 (global) 函数和通用变量,既使这个档案没有那个变 量或函数的声明或原型。因此你必须保证在不同的两个档案里不能 用同一个符号名称,否则会有连接错误或者在编译时会有警告。

      一种避免这种错误的方法是在公用的符号前加上跟其所在源文件有 关的前缀。比如:所有在 gfx.c 里的函数都加上前缀“gfx_”。如果 你很小心的分解你的程序,使用有意义的函数名称,并且不是过分 使用通用变量,当然这根本就不是问题。

      要防止一个符号在它被定义的源文件以外被看到,可在它的定义前 加上关键字“static”。这对只在一个档案内部使用,其它档案都 都不会用到的简单函数是很有用的。

      b) 多次定义的符号: header 档会被逐字的替换到你源文件里 #include 的位置的。因此,如果 header 档被 #include 到一个以上的源文件 里,这个 header 档中所有的定义就会出现在每一个有关的源码文件 里。这会使它们里的符号被定义一次以上,从而出现连接错误(见 上)。

      解决方法: 不要在 header 档里定义变量。你只需要在 header 档里声明它们然后在适当的C源码文件(应该 #include 那个 header 档的那个)里定义它们(一次)。对于初学者来说,定义和声明是 很容易混淆的。声明的作用是告诉编译器其所声明的符号应该存在, 并且要有所指定的类型。但是,它并不会使编译器分配贮存空间。 而定义的做用是要求编译器分配贮存空间。当做一个声明而不是做 定义的时候,在声明前放一个关键字“extern”。

      例如,我们有一个叫“counter”的变量,如果想让它成为公用的, 我们在一个源码程序(只在一个里面)的开始定义它:“int counter;”,再在相关的 header 档里声明它:“extern int counter;”。

      函数原型里隐含着 extern 的意思,所以不需顾虑这个问题。

      c) 重复定义,重复声明,矛盾类型:
      请考虑如果在一个C源码文件中 #include 两个档 a.h 和 b.h, 而 a.h 又 #include 了 b.h 档(原因是 b.h 档定义了一些 a.h 需要的类型),会发生什么事呢?这时该C源码文件 #include 了 b.h 两次。因此每一个在 b.h 中的 #define 都发生了两次,每一 个声明发生了两次,等等。理论上,因为它们是完全一样的拷贝, 所以应该不会有什么问题,但在实际应用上,这是不符合C的语法 的,可能在编译时出现错误,或至少是警告。

      解决的方法是要确定每一个 header 档在任一个源码文件中只被包 含了一次。我们一般是用预处理器来达到这个目的的。当我们进入 每一个 header 档时,我们为这个 header 档 #define 一个巨集 指令。只有在这个巨集指令没有被定义的前提下,我们才真正使用 该 header 档的主体。在实际应用上,我们只要简单的把下面一段 码放在每一个 header 档的开始部分:

      #ifndef FILENAME_H
      #define FILENAME_H

      然后把下面一行码放在最后:

      #endif

      用 header 档的档名(大写的)代替上面的 FILENAME_H,用底线 代替档名中的点。有些人喜欢在 #endif 加上注释来提醒他们这个 #endif 指的是什么。例如:

      #endif /* #ifndef FILENAME_H */

      我个人没有这个习惯,因为这其实是很明显的。当然这只是各人的 风格不同,无伤大雅。

      你只需要在那些有编译错误的 header 档中加入这个技巧,但在所 有的 header 档中都加入也没什么损失,到底这是个好习惯。

    1.5 重新编译一个多文件项目

    清楚的区别编译和连接是很重要的。编译器使用源码文件来产生某种 形式的目标文件(object files)。在这个过程中,外部的符号参考并 没有被解释或替换。然后我们使用连接器来连接这些目标文件和一些 标准的程序包再加你指定的程序包,最后连接生成一个可执行程序。 在这个阶段,一个目标文件中对别的文件中的符号的参考被解释,并 报告不能被解释的参考,一般是以错误信息的形式报告出来。

    基本的步骤就应该是,把你的源码文件一个一个的编译成目标文件的格 式,最后把所有的目标文件加上需要的程序包连接成一个可执行文件。 具体怎么做是由你的编译器决定的。这里我只给出 gcc (GNU C 编译 器)的有关命令,这些有可能对你的非 gcc 编译器也适用。

    gcc 是一个多目标的工具。它在需要的时候呼叫其它的元件(预处理 程序,编译器,组合程序,连接器)。具体的哪些元件被呼叫取决于 输入文件的类型和你传递给它的开关。

    一般来说,如果你只给它C源码文件,它将预处理,编译,组合所有 的文件,然后把所得的目标文件连接成一个可执行文件(一般生成的 文件被命名为 a.out )。你当然可以这么做,但这会破坏很多我们 把一个项目分解成多个文件所得到的好处。

    如果你给它一个 -c 开关,gcc 只把给它的文件编译成目标文件, 用源码文件的文件名命名但把其后缀由“.c”或“.cc”变成“.o”。 如果你给它的是一列目标文件, gcc 会把它们连接成可执行文件, 缺省文件名是 a.out 。你可以改变缺省名,用开关 -o 后跟你指定 的文件名。

    因此,当你改变了一个源码文件后,你需要重新编译它: 'gcc -c filename.c' 然后重新连接你的项目: 'gcc -o exec_filename *.o'。 如果你改变了一个 header 档,你需要重新编译所有 #include 过 这个档的源码文件,你可以用 'gcc -c file1.c file2.c file3.c' 然后象上边一样连接。

    当然这么做是很繁琐的,幸亏我们有些工具使这个步骤变得简单。 本文的第二部分就是介绍其中的一件工具:GNU Make 工具。

    (好家伙,现在才开始见真章。您学到点儿东西没?)

2) GNU Make 工具
~~~~~~~~~~~~~~~~
    2.1 基本 makefile 结构

    GNU Make 的主要工作是读进一个文本文件, makefile 。这个文 件里主要是有关哪些文件(‘target’目的文件)是从哪些别的 文件(‘dependencies’依靠文件)中产生的,用什么命令来进行 这个产生过程。有了这些信息, make 会检查磁碟上的文件,如果 目的文件的时间戳(该文件生成或被改动时的时间)比至少它的一 个依靠文件旧的话, make 就执行相应的命令,以便更新目的文件。 (目的文件不一定是最后的可执行档,它可以是任何一个文件。)

    makefile 一般被叫做“makefile”或“Makefile”。当然你可以 在 make 的命令行指定别的文件名。如果你不特别指定,它会寻 找“makefile”或“Makefile”,因此使用这两个名字是最简单 的。

    一个 makefile 主要含有一系列的规则,如下:

    : ...
    (tab)
    (tab)
    .
    .
    .

    例如,考虑以下的 makefile :

    === makefile 开始 ===
    myprog : foo.o bar.o
    gcc foo.o bar.o -o myprog

    foo.o : foo.c foo.h bar.h
    gcc -c foo.c -o foo.o

    bar.o : bar.c bar.h
    gcc -c bar.c -o bar.o
    === makefile 结束 ===

    这是一个非常基本的 makefile —— make 从最上面开始,把上 面第一个目的,‘myprog’,做为它的主要目标(一个它需要保 证其总是最新的最终目标)。给出的规则说明只要文件‘myprog’ 比文件‘foo.o’或‘bar.o’中的任何一个旧,下一行的命令将 会被执行。

    但是,在检查文件 foo.o 和 bar.o 的时间戳之前,它会往下查 找那些把 foo.o 或 bar.o 做为目标文件的规则。它找到的关于 foo.o 的规则,该文件的依靠文件是 foo.c, foo.h 和 bar.h 。 它从下面再找不到生成这些依靠文件的规则,它就开始检查磁碟 上这些依靠文件的时间戳。如果这些文件中任何一个的时间戳比 foo.o 的新,命令 'gcc -o foo.o foo.c' 将会执行,从而更新 文件 foo.o 。

    接下来对文件 bar.o 做类似的检查,依靠文件在这里是文件 bar.c 和 bar.h 。

    现在, make 回到‘myprog’的规则。如果刚才两个规则中的任 何一个被执行,myprog 就需要重建(因为其中一个 .o 档就会比 ‘myprog’新),因此连接命令将被执行。

    希望到此,你可以看出使用 make 工具来建立程序的好处——前 一章中所有繁琐的检查步骤都由 make 替你做了:检查时间戳。 你的源码文件里一个简单改变都会造成那个文件被重新编译(因 为 .o 文件依靠 .c 文件),进而可执行文件被重新连接(因为 .o 文件被改变了)。其实真正的得益是在当你改变一个 header 档的时候——你不再需要记住那个源码文件依靠它,因为所有的 资料都在 makefile 里。 make 会很轻松的替你重新编译所有那 些因依靠这个 header 文件而改变了的源码文件,如有需要,再 进行重新连接。

    当然,你要确定你在 makefile 中所写的规则是正确无误的,只 列出那些在源码文件中被 #include 的 header 档……

    2.2 编写 make 规则 (Rules)

    最明显的(也是最简单的)编写规则的方法是一个一个的查 看源码文件,把它们的目标文件做为目的,而C源码文件和被它 #include 的 header 档做为依靠文件。但是你也要把其它被这些 header 档 #include 的 header 档也列为依靠文件,还有那些被 包括的文件所包括的文件……然后你会发现要对越来越多的文件 进行管理,然后你的头发开始脱落,你的脾气开始变坏,你的脸 色变成菜色,你走在路上开始跟电线杆子碰撞,终于你捣毁你的 电脑显示器,停止编程。到低有没有些容易点儿的方法呢?

    当然有!向编译器要!在编译每一个源码文件的时候,它实在应 该知道应该包括什么样的 header 档。使用 gcc 的时候,用 -M 开关,它会为每一个你给它的C文件输出一个规则,把目标文件 做为目的,而这个C文件和所有应该被 #include 的 header 文 件将做为依靠文件。注意这个规则会加入所有 header 文件,包 括被角括号(`<', `>')和双引号(`"')所包围的文件。其实我们可以 相当肯定系统 header 档(比如 stdio.h, stdlib.h 等等)不会 被我们更改,如果你用 -MM 来代替 -M 传递给 gcc,那些用角括 号包围的 header 档将不会被包括。(这会节省一些编译时间)

    由 gcc 输出的规则不会含有命令部分;你可以自己写入你的命令 或者什么也不写,而让 make 使用它的隐含的规则(参考下面的 2.4 节)。

    2.3 Makefile 变量

    上面提到 makefiles 里主要包含一些规则。它们包含的其它的东 西是变量定义。

    makefile 里的变量就像一个环境变量(environment variable)。 事实上,环境变量在 make 过程中被解释成 make 的变量。这些 变量是大小写敏感的,一般使用大写字母。它们可以从几乎任何 地方被引用,也可以被用来做很多事情,比如:
      i) 贮存一个文件名列表。在上面的例子里,生成可执行文件的 规则包含一些目标文件名做为依靠。在这个规则的命令行 里同样的那些文件被输送给 gcc 做为命令参数。如果在这 里使用一个变数来贮存所有的目标文件名,加入新的目标 文件会变的简单而且较不易出错。

      ii) 贮存可执行文件名。如果你的项目被用在一个非 gcc 的系 统里,或者如果你想使用一个不同的编译器,你必须将所 有使用编译器的地方改成用新的编译器名。但是如果使用一 个变量来代替编译器名,那么你只需要改变一个地方,其 它所有地方的命令名就都改变了。

      iii) 贮存编译器旗标。假设你想给你所有的编译命令传递一组 相同的选项(例如 -Wall -O -g);如果你把这组选项存 入一个变量,那么你可以把这个变量放在所有呼叫编译器 的地方。而当你要改变选项的时候,你只需在一个地方改 变这个变量的内容。
    要设定一个变量,你只要在一行的开始写下这个变量的名字,后 面跟一个 = 号,后面跟你要设定的这个变量的值。以后你要引用 这个变量,写一个 $ 符号,后面是围在括号里的变量名。比如在 下面,我们把前面的 makefile 利用变量重写一遍:

    === makefile 开始 ===
    OBJS = foo.o bar.o
    CC = gcc
    CFLAGS = -Wall -O -g

    myprog : $(OBJS)
    $(CC) $(OBJS) -o myprog

    foo.o : foo.c foo.h bar.h
    $(CC) $(CFLAGS) -c foo.c -o foo.o

    bar.o : bar.c bar.h
    $(CC) $(CFLAGS) -c bar.c -o bar.o
    === makefile 结束 ===

    还有一些设定好的内部变量,它们根据每一个规则内容定义。三个 比较有用的变量是 $@, $< 和 $^ (这些变量不需要括号括住)。 $@ 扩展成当前规则的目的文件名, $< 扩展成依靠列表中的第 一个依靠文件,而 $^ 扩展成整个依靠的列表(除掉了里面所有重 复的文件名)。利用这些变量,我们可以把上面的 makefile 写成:

    === makefile 开始 ===
    OBJS = foo.o bar.o
    CC = gcc
    CFLAGS = -Wall -O -g

    myprog : $(OBJS)
    $(CC) $^ -o $@

    foo.o : foo.c foo.h bar.h
    $(CC) $(CFLAGS) -c $< -o $@

    bar.o : bar.c bar.h
    $(CC) $(CFLAGS) -c $< -o $@
    === makefile 结束 ===

    你可以用变量做许多其它的事情,特别是当你把它们和函数混合 使用的时候。如果需要更进一步的了解,请参考 GNU Make 手册。 ('man make', 'man makefile')

    2.4 隐含规则 (Implicit Rules)

    请注意,在上面的例子里,几个产生 .o 文件的命令都是一样的。 都是从 .c 文件和相关文件里产生 .o 文件,这是一个标准的步 骤。其实 make 已经知道怎么做——它有一些叫做隐含规则的内 置的规则,这些规则告诉它当你没有给出某些命令的时候,应该 怎么办。

    如果你把生成 foo.o 和 bar.o 的命令从它们的规则中删除, make 将会查找它的隐含规则,然后会找到一个适当的命令。它的命令会 使用一些变量,因此你可以按照你的想法来设定它:它使用变量 CC 做为编译器(象我们在前面的例子),并且传递变量 CFLAGS (给 C 编译器,C++ 编译器用 CXXFLAGS ),CPPFLAGS ( C 预 处理器旗标), TARGET_ARCH (现在不用考虑这个),然后它加 入旗标 '-c' ,后面跟变量 $< (第一个依靠名),然后是旗 标 '-o' 跟变量 $@ (目的文件名)。一个C编译的具体命令将 会是:

    $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $< -o $@

    当然你可以按照你自己的需要来定义这些变量。这就是为什么用 gcc 的 -M 或 -MM 开关输出的码可以直接用在一个 makefile 里。

    2.5 假象目的 (Phony Targets)

    假设你的一个项目最后需要产生两个可执行文件。你的主要目标 是产生两个可执行文件,但这两个文件是相互独立的——如果一 个文件需要重建,并不影响另一个。你可以使用“假象目的”来 达到这种效果。一个假象目的跟一个正常的目的几乎是一样的, 只是这个目的文件是不存在的。因此, make 总是会假设它需要 被生成,当把它的依赖文件更新后,就会执行它的规则里的命令 行。

    如果在我们的 makefile 开始处输入:

    all : exec1 exec2

    其中 exec1 和 exec2 是我们做为目的的两个可执行文件。 make 把这个 'all' 做为它的主要目的,每次执行时都会尝试把 'all' 更新。但既然这行规则里没有哪个命令来作用在一个叫 'all' 的 实际文件(事实上 all 并不会在磁碟上实际产生),所以这个规 则并不真的改变 'all' 的状态。可既然这个文件并不存在,所以 make 会尝试更新 all 规则,因此就检查它的依靠 exec1, exec2 是否需要更新,如果需要,就把它们更新,从而达到我们的目的。

    假象目的也可以用来描述一组非预设的动作。例如,你想把所有由 make 产生的文件删除,你可以在 makefile 里设立这样一个规则:

    veryclean :
    rm *.o
    rm myprog

    前提是没有其它的规则依靠这个 'veryclean' 目的,它将永远 不会被执行。但是,如果你明确的使用命令 'make veryclean' , make 会把这个目的做为它的主要目标,执行那些 rm 命令。

    如果你的磁碟上存在一个叫 veryclean 文件,会发生什么事?这 时因为在这个规则里没有任何依靠文件,所以这个目的文件一定是 最新的了(所有的依靠文件都已经是最新的了),所以既使用户明 确命令 make 重新产生它,也不会有任何事情发生。解决方法是标 明所有的假象目的(用 .PHONY),这就告诉 make 不用检查它们 是否存在于磁碟上,也不用查找任何隐含规则,直接假设指定的目 的需要被更新。在 makefile 里加入下面这行包含上面规则的规则:

    .PHONY : veryclean

    就可以了。注意,这是一个特殊的 make 规则,make 知道 .PHONY 是一个特殊目的,当然你可以在它的依靠里加入你想用的任何假象 目的,而 make 知道它们都是假象目的。

    2.6 函数 (Functions)

    makefile 里的函数跟它的变量很相似——使用的时候,你用一个 $ 符号跟开括号,函数名,空格后跟一列由逗号分隔的参数,最后 用关括号结束。例如,在 GNU Make 里有一个叫 'wildcard' 的函 数,它有一个参数,功能是展开成一列所有符合由其参数描述的文 件名,文件间以空格间隔。你可以像下面所示使用这个命令:

    SOURCES = $(wildcard *.c)

    这行会产生一个所有以 '.c' 结尾的文件的列表,然后存入变量 SOURCES 里。当然你不需要一定要把结果存入一个变量。

    另一个有用的函数是 patsubst ( patten substitude, 匹配替 换的缩写)函数。它需要3个参数——第一个是一个需要匹配的 式样,第二个表示用什么来替换它,第三个是一个需要被处理的 由空格分隔的字列。例如,处理那个经过上面定义后的变量,

    OBJS = $(patsubst %.c,%.o,$(SOURCES))

    这行将处理所有在 SOURCES 字列中的字(一列文件名),如果它的 结尾是 '.c' ,就用 '.o' 把 '.c' 取代。注意这里的 % 符号将匹 配一个或多个字符,而它每次所匹配的字串叫做一个‘柄’(stem) 。 在第二个参数里, % 被解读成用第一参数所匹配的那个柄。

    2.7 一个比较有效的 makefile

    利用我们现在所学的,我们可以建立一个相当有效的 makefile 。 这个 makefile 可以完成大部分我们需要的依靠检查,不用做太大 的改变就可直接用在大多数的项目里。

    首先我们需要一个基本的 makefile 来建我们的程序。我们可以让 它搜索当前目录,找到源码文件,并且假设它们都是属于我们的项 目的,放进一个叫 SOURCES 的变量。这里如果也包含所有的 *.cc 文件,也许会更保险,因为源码文件可能是 C++ 码的。

    SOURCES = $(wildcard *.c *.cc)

    利用 patsubst ,我们可以由源码文件名产生目标文件名,我们需 要编译出这些目标文件。如果我们的源码文件既有 .c 文件,也有 .cc 文件,我们需要使用相嵌的 patsubst 函数呼叫:

    OBJS = $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCES)))

    最里面一层 patsubst 的呼叫会对 .cc 文件进行后缀替代,产生的结 果被外层的 patsubst 呼叫处理,进行对 .c 文件后缀的替代。

    现在我们可以设立一个规则来建可执行文件:

    myprog : $(OBJS)
    gcc -o myprog $(OBJS)

    进一步的规则不一定需要, gcc 已经知道怎么去生成目标文件 (object files) 。下面我们可以设定产生依靠信息的规则:

    depends : $(SOURCES)
    gcc -M $(SOURCES) > depends

    在这里如果一个叫 'depends' 的文件不存在,或任何一个源码文件 比一个已存在的 depends 文件新,那么一个 depends 文件会被生 成。depends 文件将会含有由 gcc 产生的关于源码文件的规则(注 意 -M 开关)。现在我们要让 make 把这些规则当做 makefile 档 的一部分。这里使用的技巧很像 C 语言中的 #include 系统——我 们要求 make 把这个文件 include 到 makefile 里,如下:

    include depends

    GNU Make 看到这个,检查 'depends' 目的是否更新了,如果没有, 它用我们给它的命令重新产生 depends 档。然后它会把这组(新) 规则包含进来,继续处理最终目标 'myprog' 。当看到有关 myprog 的规则,它会检查所有的目标文件是否更新——利用 depends 文件 里的规则,当然这些规则现在已经是更新过的了。

    这个系统其实效率很低,因为每当一个源码文件被改动,所有的源码 文件都要被预处理以产生一个新的 'depends' 文件。而且它也不是 100% 的安全,这是因为当一个 header 档被改动,依靠信息并不会 被更新。但就基本工作来说,它也算相当有用的了。

    2.8 一个更好的 makefile

    这是一个我为我大多数项目设计的 makefile 。它应该可以不需要修 改的用在大部分项目里。我主要把它用在 djgpp 上,那是一个 DOS 版的 gcc 编译器。因此你可以看到执行的命令名、 'alleg' 程序包、 和 RM -F 变量都反映了这一点。

    === makefile 开始 ===

    ######################################
    #
    # Generic makefile
    #
    # by George Foot
    # email: george.foot@merton.ox.ac.uk
    #
    # Copyright (c) 1997 George Foot
    # All rights reserved.
    # 保留所有版权
    #
    # No warranty, no liability;
    # you use this at your own risk.
    # 没保险,不负责
    # 你要用这个,你自己担风险
    #
    # You are free to modify and
    # distribute this without giving
    # credit to the original author.
    # 你可以随便更改和散发这个文件
    # 而不需要给原作者什么荣誉。
    # (你好意思?)
    #
    ######################################

    ### Customising
    # 用户设定
    #
    # Adjust the following if necessary; EXECUTABLE is the target
    # executable's filename, and LIBS is a list of libraries to link in
    # (e.g. alleg, stdcx, iostr, etc). You can override these on make's
    # command line of course, if you prefer to do it that way.
    #
    # 如果需要,调整下面的东西。 EXECUTABLE 是目标的可执行文件名, LIBS
    # 是一个需要连接的程序包列表(例如 alleg, stdcx, iostr 等等)。当然你
    # 可以在 make 的命令行覆盖它们,你愿意就没问题。
    #

    EXECUTABLE := mushroom.exe
    LIBS := alleg

    # Now alter any implicit rules' variables if you like, e.g.:
    #
    # 现在来改变任何你想改动的隐含规则中的变量,例如

    CFLAGS := -g -Wall -O3 -m486
    CXXFLAGS := $(CFLAGS)

    # The next bit checks to see whether rm is in your djgpp bin
    # directory; if not it uses del instead, but this can cause (harmless)
    # `File not found' error messages. If you are not using DOS at all,
    # set the variable to something which will unquestioningly remove
    # files.
    #
    # 下面先检查你的 djgpp 命令目录下有没有 rm 命令,如果没有,我们使用
    # del 命令来代替,但有可能给我们 'File not found' 这个错误信息,这没
    # 什么大碍。如果你不是用 DOS ,把它设定成一个删文件而不废话的命令。
    # (其实这一步在 UNIX 类的系统上是多余的,只是方便 DOS 用户。 UNIX
    # 用户可以删除这5行命令。)

    ifneq ($(wildcard $(DJDIR)/bin/rm.exe),)
    RM-F := rm -f
    else
    RM-F := del
    endif

    # You shouldn't need to change anything below this point.
    #
    # 从这里开始,你应该不需要改动任何东西。(我是不太相信,太NB了!)

    SOURCE := $(wildcard *.c) $(wildcard *.cc)
    OBJS := $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCE)))
    DEPS := $(patsubst %.o,%.d,$(OBJS))
    MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))
    MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS)) \
    $(patsubst %.d,%.cc,$(MISSING_DEPS)))
    CPPFLAGS += -MD

    .PHONY : everything deps objs clean veryclean rebuild

    everything : $(EXECUTABLE)

    deps : $(DEPS)

    objs : $(OBJS)

    clean :
    @$(RM-F) *.o
    @$(RM-F) *.d

    veryclean: clean
    @$(RM-F) $(EXECUTABLE)

    rebuild: veryclean everything

    ifneq ($(MISSING_DEPS),)
    $(MISSING_DEPS) :
    @$(RM-F) $(patsubst %.d,%.o,$@)
    endif

    -include $(DEPS)

    $(EXECUTABLE) : $(OBJS)
    gcc -o $(EXECUTABLE) $(OBJS) $(addprefix -l,$(LIBS))

    === makefile 结束 ===

    有几个地方值得解释一下的。首先,我在定义大部分变量的时候使 用的是 := 而不是 = 符号。它的作用是立即把定义中参考到的函 数和变量都展开了。如果使用 = 的话,函数和变量参考会留在那 儿,就是说改变一个变量的值会导致其它变量的值也被改变。例 如:

    A = foo
    B = $(A)
    # 现在 B 是 $(A) ,而 $(A) 是 'foo' 。
    A = bar
    # 现在 B 仍然是 $(A) ,但它的值已随着变成 'bar' 了。
    B := $(A)
    # 现在 B 的值是 'bar' 。
    A = foo
    # B 的值仍然是 'bar' 。

    make 会忽略在 # 符号后面直到那一行结束的所有文字。

    ifneg...else...endif 系统是 makefile 里让某一部分码有条件的 失效/有效的工具。 ifeq 使用两个参数,如果它们相同,它把直 到 else (或者 endif ,如果没有 else 的话)的一段码加进 makefile 里;如果不同,把 else 到 endif 间的一段码加入 makefile (如果有 else )。 ifneq 的用法刚好相反。

    'filter-out' 函数使用两个用空格分开的列表,它把第二列表中所 有的存在于第一列表中的项目删除。我用它来处理 DEPS 列表,把所 有已经存在的项目都删除,而只保留缺少的那些。

    我前面说过, CPPFLAGS 存有用于隐含规则中传给预处理器的一些 旗标。而 -MD 开关类似 -M 开关,但是从源码文件 .c 或 .cc 中 形成的文件名是使用后缀 .d 的(这就解释了我形成 DEPS 变量的 步骤)。DEPS 里提到的文件后来用 '-include' 加进了 makefile 里,它隐藏了所有因文件不存在而产生的错误信息。

    如果任何依靠文件不存在, makefile 会把相应的 .o 文件从磁碟 上删除,从而使得 make 重建它。因为 CPPFLAGS 指定了 -MD , 它的 .d 文件也被重新产生。

    最后, 'addprefix' 函数把第二个参数列表的每一项前缀上第一 个参数值。

    这个 makefile 的那些目的是(这些目的可以传给 make 的命令行 来直接选用):

    everything:(预设) 更新主要的可执行程序,并且为每一个 源码文件生成或更新一个 '.d' 文件和一个 '.o' 文件。

    deps: 只是为每一个源码程序产生或更新一个 '.d' 文件。

    objs: 为每一个源码程序生成或更新 '.d' 文件和目标文件。

    clean: 删除所有中介/依靠文件( *.d 和 *.o )。

    veryclean: 做 `clean' 和删除可执行文件。

    rebuild: 先做 `veryclean' 然后 `everything' ;既完全重建。

    除了预设的 everything 以外,这里头只有 clean , veryclean , 和 rebuild 对用户是有意义的。

    我还没有发现当给出一个源码文件的目录,这个 makefile 会失败的 情况,除非依靠文件被弄乱。如果这种弄乱的情况发生了,只要输入 `make clean' ,所有的目标文件和依靠文件会被删除,问题就应该 被解决了。当然,最好不要把它们弄乱。如果你发现在某种情况下这 个 makefile 文件不能完成它的工作,请告诉我,我会把它整好的。

3 总结
~~~~~~~~~~~~~~~

我希望这篇文章足够详细的解释了多文件项目是怎么运作的,也说明了 怎样安全而合理的使用它。到此,你应该可以轻松的利用 GNU Make 工 具来管理小型的项目,如果你完全理解了后面几个部分的话,这些对于 你来说应该没什么困难。

GNU Make 是一件强大的工具,虽然它主要是用来建立程序,它还有很多 别的用处。如果想要知道更多有关这个工具的知识,它的句法,函数, 和许多别的特点,你应该参看它的参考文件 (info pages, 别的 GNU 工具也一样,看它们的 info pages. )。


C Scene 官方网站: http://cscene.differnet.org
C Scene 官方电邮: cscene@mindless.com

This page is Copyright ? 1997 By C Scene. All Rights Reserved

 

2007年7月27日星期五

gcc

gcc

目 录

  1. gcc
    1. makefile写法
    2. gcc_egcs使用
    3. gdb使用
    4. gcc常用选项对代码的影响
      1. 一般情况
      2. -O 编译选项
      3. -O2 编译选项
      4. -fomit-frame-pointer 编译选项
      5. -fomit-frame-pointer && -O2
      6. -fPIC 编译选项
      7. -static 编译选项
    5. AT&T的汇编格式
    6. x86内联汇编
      1. 简述
      2. 内联汇编
      3. 程序模板
      4. 操作数
      5. 修饰寄存器列表
      6. 操作数约束
      7. 示例
        1. 寄存器约束
        2. 匹配约束
        3. 内存操作数约束
        4. 修饰寄存器
    7. 不同的CPU下最佳编译参数
    8. 代码维护
      1. 简单cvs
      2. automake
      3. diff
      4. rcs
      5. 内核重编译常见故障
      6. cvs
      7. 共享库工具
      8. 代码优化
      9. GNU 编码标准
      10. 书籍


gcc

[目录]


makefile写法

蓝森林 http://www.lslnet.com 2001年3月22日 08:44

作 者: 许明彦

Abstract:

在 Unix 上写程式的人大概都碰过 Makefile,尤其是用 C 来开发程式的人。用 make来开发和编译程式的确很方便,可是要写出一个 Makefile就不简单了。偏偏介绍 Makefile 的文件不多,GNU Make 那份印出来要几百页的文件,光看完 Overview 就快阵亡了,难怪许多
人闻 Unix 色变。

本文将介绍如何利用 GNU Autoconf 及 Automake 这两套软体来协助我们『自动』产生 Makefile 档,并且让开发出来的软体可以像 Apache, MySQL 和常见的 GNU 软体一样,只要会 ``./configure'', ``make'', ``make install'' 就可以把程式安装到系统中。如果您有
心开发 Open Source 的软体,或只是想在 Unix 系统下写写程式。希望这份介绍文件能帮助您轻松地进入 Unix Programming 的殿堂。

1. 简介

Makefile 基本上就是『目标』(target), 『关连』(dependencies) 和『动作』三者所组成的一连串规则。而 make 就会根据 Makefile 的规则来决定如何编译 (compile) 和连结 (link) 程式。实际上,make 可做的不只是编译和连结程式,例如 FreeBSD 的 port collect
ion 中, Makefile 还可以做到自动下载原始程式套件,解压缩 (extract) ,修补 (patch),设定,然後编译,安装至系统中。

Makefile 基本构造虽然简单,但是妥善运用这些规则就也可以变出许多不同的花招。却也因此,许多刚开始学习写 Makefile 时会感到没有规范可循,每个人写出来的 Makefile 长得都不太一样,不知道从何下手,而且常常会受限於自己的开发环境,只要环境变数不同或路
径改一下,可能Makefile 就得跟着修改。虽然有 GNU Makefile Conventions (GNU Makefile 惯例) 订出一些使用 GNU 程式设计时撰写 Makefile 的一些标准和规范,但是内容很长而且很复杂, 并且经常做些调整,为了减轻程式设计师维护 Makefile 的负担,因此有了Automake。

程式设计师只需写一些预先定义好的巨集 (macro),交给 Automake 处理後会产生一个可供Autoconf 使用的 Makefile.in 档。再配合利用Autoconf 产生的自动设定档 configure即可产生一份符合 GNU Makefile惯例的 Makeifle 了。

2. 上路之前

在开始试着用 Automake 之前,请先确认你的系统已经安装以下的软体:
1. GNU Automake
2. GNU Autoconf
3. GNU m4
4. perl
5. GNU Libtool (如果你需要产生 shared library)

我会建议你最好也使用 GNU C/C++ 编译器 、GNU Make 以及其它 GNU 的工具程式来做为开发的环境,这些工具都是属於 Open Source Software不仅免费而且功能强大。如果你是使用Red Hat Linux 可以找到所有上述软体的 rpm 档,FreeBSD 也有现成的 package 可以直
接安装,或着你也可以自行下载这些软体的原始档回来 DIY。以下的范例是在 Red Hat Linux 5.2 + CLE2 的环境下所完成的。

3. 一个简单的例子

Automake 所产生的 Makefile 除了可以做到程式的编译和连结,也已经把如何产生程式文件(如 manual page, info 档及 dvi 档) 的动作,还有把原始程式包装起来以供散 的动作都考虑进去了,所以原始程式所存放的目录架构最好符合 GNU 的标准惯例,接下来我拿hello.c 来做为例子。

在工作目录下建立一个新的子目录 ``devel'',再在 devel 下建立一个``hello'' 的子目录,这个目录将作为我们存放 hello 这个程式及其相关档案的地方:

% mkdir devel
% cd devel
% mkdir hello
% cd hello

用编辑器写个 hello.c 档,
#include stdio.h
int main(int argc, char** argv)
{
printf(``Hello, GNU! '');
return 0;
}

接下来就要用 Autoconf 及 Automake 来帮我们产生 Makefile 档了,

1. 用 autoscan 产生一个 configure.in 的雏型,执行 autoscan 後会产生一个configure.scan 的档案,我们可以用它做为configure.in档的蓝本。

% autoscan
% ls
configure.scan hello.c

2. 编辑 configure.scan 档,如下所示,并且把它的档名改成configure.in
dnl Process this file with autoconf to produce a con figure script.
AC_INIT(hello.c)
AM_INIT_AUTOMAKE(hello, 1.0)
dnl Checks for programs.
AC_PROG_CC
dnl Checks for libraries.
dnl Checks for header files.
dnl Checks for typedefs, structures, and compiler ch aracteristics.
dnl Checks for library functions.
AC_OUTPUT(Makefile)

3. 执行 aclocal 和 autoconf ,分别会产生 aclocal.m4 及 configure 两个档案
% aclocal
% autoconf
% ls
aclocal.m4 configure configure.in hello.c

4. 编辑 Makefile.am 档,内容如下
AUTOMAKE_OPTIONS= foreign
bin_PROGRAMS= hello
hello_SOURCES= hello.c

5. 执行 automake --add-missing ,Automake 会根据 Makefile.am 档产生一些档案,包含最重要的 Makefile.in
% automake --add-missing
automake: configure.in: installing `./install-sh'
automake: configure.in: installing `./mkinstalldirs'
automake: configure.in: installing `./missing'

6. 最後执行 ./configure ,
% ./configure
creating cache ./config.cache
checking for a BSD compatible install... /usr/bin/in stall -c
checking whether build environment is sane... yes
checking whether make sets ${MAKE}... yes
checking for working aclocal... found
checking for working autoconf... found
checking for working automake... found
checking for working autoheader... found
checking for working makeinfo... found
checking for gcc... gcc
checking whether the C compiler (gcc ) works... yes
checking whether the C compiler (gcc ) is a cross-co mpiler... no
checking whether we are using GNU C... yes
checking whether gcc accepts -g... yes
updating cache ./config.cache
creating ./config.status
creating Makefile

现在你的目录下已经产生了一个 Makefile 档,下个 ``make'' 指令就可以开始编译 hello.c 成执行档,执行 ./hello 和 GNU 打声招呼吧!

% make
gcc -DPACKAGE="hello" -DVERSION="1.0" -I. -I. -g -O2 -c he llo.c
gcc -g -O2 -o hello hello.o
% ./hello
Hello! GNU!

你还可以试试 ``make clean'',''make install'',''make dist'' 看看会有什麽结果。你也可以把产生出来的 Makefile 秀给你的老板,让他从此对你刮目相看 :-)

4. 一探究竟

上述产生 Makefile 的过程和以往自行编写的方式非常不一样,舍弃传统自行定义 make 的规则,使用 Automake 只需用到一些已经定义好的巨集即可。我们把巨集及目标 (target)写在 Makefile.am 档内,Automake读入 Makefile.am 档後会把这一串已经定义好的巨集展
开并且产生对应的Makefile.in 档, 然後再由 configure 这个 shell script 根据Makefile.in 产生适合的 Makefile。
在此范例中可藉由 Autoconf 及 Automake 工具所产生的档案有 configure.scan、aclocal.m4、configure、Makefile.in,需要我们加入设定者为 configure.in 及 Makefile.am。

4.1 编辑 configure.in 档

Autoconf 是用来产生 'configure' 档的工具。'configure' 是一个shell script,它可以自动设定原始程式以符合各种不同平台上 Unix 系统的特性,并且根据系统叁数及环境产生合适的 Makefile 档或是C 的标头档 (header file),让原始程式可以很方便地在这些不同
的平台上被编译出来。Autoconf 会读取 configure.in 档然後产生 'configure' 这个shell script。

configure.in 档的内容是一连串 GNU m4 的巨集,这些巨集经过autoconf 处理後会变成检查系统特徵的 shell script。configure.in 内巨集的顺序并没有特别的规定,但是每一个 configure.in 档必须在所有巨集前加入 AC_INIT 巨集,然後在所有巨集的最後面加上 AC
_OUTPUT 巨集。我们可先用 autoscan 扫描原始档以产生一个 configure.scan 档,再对 configure.scan 做些修改成 configure.in 档。在范例中所用到的巨集如下:

dnl
这个巨集後面的字不会被处理,可视为注解。

AC_INIT(FILE)
这个巨集用来检查原始码所在的路径,autoscan 会自动产生,我们不必修改它。

AM_INIT_AUTOMAKE(PACKAGE,VERSION)
这是使用 Automake 所必备的巨集,PACKAGE 是我们所要产生软体套件的名称,VERSION 是版本编号。

AC_PROG_CC
检查系统可用的 C 编译器,如果原始程式是用 C 写的就需要这个巨集。

AC_OUTPUT(FILE)
设定 configure 所要产生的档案,如果是 Makefile 的话,configure 便会把它检查出来的结果带入 Makefile.in 档然後产生
合适的 Makefile。

实际上,我们使用 Automake 时,还须要一些其它的巨集,这些额外的巨集我们用 aclocal来帮我们产生。执行 aclocal 会产生 aclocal.m4档,如果没有特别的用途,我们可以不必修改它,用 aclocal 所产生的巨集会告诉 Automake 怎麽做。

有了 configure.in 及 aclocal.m4 两个档案後,便可以执行 autoconf来产生 configure档了。

4.2 编辑 Makefile.am 档

接下来我们要编辑 Makefile.am 档,Automake 会根据 configure.in 中的巨集把Makefile.am 转成 Makefile.in 档。Makefile.am 档定义我们所要产的目标:

AUTOMAKE_OPTIONS
设定 automake 的选项。Automake 主要是帮助开发 GNU 软体的人员维护软体套件,所以在执行 automake 时,会检查目录下是否存在标准 GNU 软体套件中应具备的文件档案,例如 'NEWS'、'AUTHOR'、'ChangeLog' 等文件档。设成 foreign 时,automake 会改用一般软
体套件的标准来检查。

bin_PROGRAMS
定义我们所要产生的执行档档名。如果要产生多个执行档,每个档名用空白字元隔开。

hello_SOURCES
定义 'hello' 这个执行档所需要的原始档。如果 'hello' 这个程式是由多个原始档所产生,必须把它所用到的原始档都列出来,以空白字元隔开。假设 'hello' 这个程式需要 'hello.c'、'main.c'、

'hello.h'
三个档案的话,则定义
hello_SOURCES= hello.c main.c hello.h
如果我们定义多个执行档,则对每个执行档都要定义相对的filename_SOURCES。

编辑好 Makefile.am 档,就可以用 automake --add-missing 来产生Makefile.in。加上 --add-missing 选项是告诉 automake 顺便帮我们加入包装一个软体套件所必备的档案。Automake 产生出来的 Makefile.in档是完全符合 GNU Makefile 的惯例,我们只要执行 confi
gure 这个shell script 便可以产生合适的 Makefile 档了。

4.3 使用 Makefile

利用 configure 所产生的 Makefile 档有几个预设的目标可供使用,我们只拿其中几个简述如下:

make all
产生我们设定的目标,即此范例中的执行档。只打 make 也可以,此时会开始编译原始码,然後连结,并且产生执行档。

make clean
清除之前所编译的执行档及目的档 (object file, *.o)。

make distclean
除了清除执行档和目的档外,也把 configure 所产生的 Makefile也清除掉。

make install
将程式安装至系统中。如果原始码编译无误,且执行结果正确,便可以把程式安装至系统预设的执行档存放路径。如果我们用bin_PROGRAMS 巨集的话,程式会被安装至 /usr/local/bin 这个目录。

make dist
将程式和相关的档案包装成一个压缩档以供散播 (distribution) 。执行完在目录下会产生一个以 PACKAGE-VERSION.tar.gz 为名称的档案。PACKAGE 和 VERSION 这两个变数是根据 configure.in 档中AM_INIT_AUTOMAKE(PACKAGE, VERSION) 的定义。在此范例中会产生
'hello-1.0.tar.gz' 的档案。

make distcheck
和 make dist 类似,但是加入检查包装後的压缩档是否正常。这个目标除了把程式和相关档案包装成 tar.gz 档外,还会自动把这个压
缩档解开,执行 configure,并且进行 make all 的动作,确认编译无误後,会显示这个 tar.gz 档已经准备好可供散播了。这个检查非
常有用,检查过关的套件,基本上可以给任何一个具备 GNU 发展境的人去重新编译。就 hello-1.tar.gz 这个范例而言,除了在 Red
Hat Linux 上,在 FreeBSD 2.2.x 版也可以正确地重新编译。

要注意的是,利用 Autoconf 及 Automake 所产生出来的软体套件是可以在没有安装 Autoconf 及 Automake 的环境上使用的,因为 configure 是一个 shell script,它己被设计可以在一般 Unix 的 sh 这个 shell 下执行。但是如果要修改 configure.in 及 Makefile.a
m 档再产生新的configure 及 Makefile.in 档时就一定要有 Autoconf 及 Automake 了。

5. 相关讯息

Autoconf 和 Automake 功能十分强大,你可以从它们所附的 info 档找到详细的用法。你也可以从许多现存的 GNU 软体或 Open Source 软体中找到相关的 configure.in 或 Makefile.am 档,它们是学习 Autoconf 及Automake 更多技巧的最佳范例。

这篇简介只用到了 Autoconf 及 Automake 的皮毛罢了,如果你有心加入Open Source 软体开发的行列,希望这篇文件能帮助你对产生 Makefile有个简单的依据。其它有关开发 GNU程式或 C 程式设计及 Makefile 的详细运用及技巧,我建议你从 GNU Coding Standards3
(GNU 编码标准规定) 读起,里面包含了 GNU Makefile 惯例,还有发展 GNU 软体套件的标准程序和惯例。这些 GNU 软体的线上说明文件可以在http://www.gnu.org/ 这个网站上找到。

6. 结语

经由 Autoconf 及 Automake 的辅助,产生一个 Makefile 似乎不再像以前那麽困难了,而使用 Autoconf 也使得我们在不同平台上或各家 Unix之间散播及编译程式变得简单,这对於在 Unix 系统上开发程式的人员来说减轻了许多负担。妥善运用这些 GNU 的工具软体,可
以帮助我们更容易去发展程式,而且更容易维护原始程式码。

一九九八年是 Open Source 运动风起云涌的一年,许多 Open Source 的软体普遍受到网路上大众的欢迎和使用。感谢所有为 Open Source 奉献的人们,也希望藉由本文能吸引更多的人加入『自由』、『开放』的 OpenSource 行列。

About this document ...

轻轻松松产生 Makefile1

This document was generated using the LaTeX2HTML translator Version 98.2 beta6 (August 14th, 1998) Copyright (C) 1993, 1994, 1995, 1996, Nikos Drakos, ComputerBased Learning Unit, University of Leeds.Copyright (C) 1997, 1998, Ross Moore, Mathematics Department,Macquarie University, Sydney.

The command line arguments were:
latex2html -split 0 -show_section_numbers automake.tex
The translation was initiated by on 1999-02-11

Footnotes
... itle1
本文件使用 ChiLaTeX 制作。
... CLE2
CLE (Chinese Linux Extension,Linux 中文延伸套件),
http://cle.linux.org.tw/

... Standards3
GNU Coding Standards, Richard Stallman.

[目录]


gcc_egcs使用

1.使用egcs

Linux 中最重要的软件开发工具是 GCC。GCC 是 GNU 的 C 和 C++ 编译器。实际上,GCC 能够编译三种语言:C、C++ 和 Object C(C 语言的一种面向对象扩展)。利用 gcc 命令可同时编译并连接 C 和 C++ 源程序。

#DEMO#: hello.c

如果你有两个或少数几个 C 源文件,也可以方便地利用 GCC 编译、连接并生成可执行文件。例如,假设你有两个源文件 main.c 和 factorial.c 两个源文件,现在要编译生成一个计算阶乘的程序。

-----------------------
清单 factorial.c
-----------------------
#include
#include

int factorial (int n)
{
if (n <= 1)
return 1;

else
return factorial (n - 1) * n;
}
-----------------------

-----------------------
清单 main.c
-----------------------
#include
#include

int factorial (int n);

int main (int argc, char **argv)
{
int n;

if (argc < 2) {
printf ("Usage: %s n\n", argv [0]);
return -1;
}
else {
n = atoi (argv[1]);
printf ("Factorial of %d is %d.\n", n, factorial (n));
}

return 0;
}
-----------------------

利用如下的命令可编译生成可执行文件,并执行程序:

$ gcc -o factorial main.c factorial.c
$ ./factorial 5
Factorial of 5 is 120.

GCC 可同时用来编译 C 程序和 C++ 程序。一般来说,C 编译器通过源文件的后缀名来判断是 C 程序还是 C++ 程序。在 Linux 中,C 源文件的后缀名为 .c,而 C++ 源文件的后缀名为 .C 或 .cpp。

但是,gcc 命令只能编译 C++ 源文件,而不能自动和 C++ 程序使用的库连接。因此,通常使用 g++ 命令来完成 C++ 程序的编译和连接,该程序会自动调用 gcc 实现编译。假设我们有一个如下的 C++ 源文件(hello.C):

#include

void main (void)
{
cout << "Hello, world!" << endl;
}

则可以如下调用 g++ 命令编译、连接并生成可执行文件:

$ g++ -o hello hello.C
$ ./hello
Hello, world!

2. gcc/egcs 的主要选项

表 1-3 gcc 命令的常用选项
选项 解释
-ansi 只支持 ANSI 标准的 C 语法。这一选项将禁止 GNU C 的某些特色,
例如 asm 或 typeof 关键词。
-c 只编译并生成目标文件。
-DMACRO 以字符串“1”定义 MACRO 宏。
-DMACRO=DEFN 以字符串“DEFN”定义 MACRO 宏。
-E 只运行 C 预编译器。
-g 生成调试信息。GNU 调试器可利用该信息。
-IDIRECTORY 指定额外的头文件搜索路径DIRECTORY。
-LDIRECTORY 指定额外的函数库搜索路径DIRECTORY。
-lLIBRARY 连接时搜索指定的函数库LIBRARY。
-m486 针对 486 进行代码优化。
-o FILE 生成指定的输出文件。用在生成可执行文件时。
-O0 不进行优化处理。
-O 或 -O1 优化生成代码。
-O2 进一步优化。
-O3 比 -O2 更进一步优化,包括 inline 函数。
-shared 生成共享目标文件。通常用在建立共享库时。
-static 禁止使用共享连接。
-UMACRO 取消对 MACRO 宏的定义。
-w 不生成任何警告信息。
-Wall 生成所有警告信息。

#DEMO#

[目录]


gdb使用

1.简介

GNU 的调试器称为 gdb,该程序是一个交互式工具,工作在字符模式。在 X Window 系统中,有一个 gdb 的前端图形工具,称为 xxgdb。gdb 是功能强大的调试程序,可完成如下的调试任务:

* 设置断点;
* 监视程序变量的值;
* 程序的单步执行;
* 修改变量的值。
在可以使用 gdb 调试程序之前,必须使用 -g 选项编译源文件。可在 makefile 中如下定义CFLAGS 变量:

CFLAGS = -g
运行 gdb 调试程序时通常使用如下的命令:

gdb progname

在 gdb 提示符处键入help,将列出命令的分类,主要的分类有:

* aliases:命令别名
* breakpoints:断点定义;
* data:数据查看;
* files:指定并查看文件;
* internals:维护命令;
* running:程序执行;
* stack:调用栈查看;
* statu:状态查看;
* tracepoints:跟踪程序执行。

键入 help 后跟命令的分类名,可获得该类命令的详细清单。

2.gdb 的常用命令
表 1-4 常用的 gdb 命令
命令 解释
break NUM 在指定的行上设置断点。
bt 显示所有的调用栈帧。该命令可用来显示函数的调用顺序。
clear 删除设置在特定源文件、特定行上的断点。其用法为:clear FILENAME:NUM。
continue 继续执行正在调试的程序。该命令用在程序由于处理信号或断点而
导致停止运行时。
display EXPR 每次程序停止后显示表达式的值。表达式由程序定义的变量组成。
file FILE 装载指定的可执行文件进行调试。
help NAME 显示指定命令的帮助信息。
info break 显示当前断点清单,包括到达断点处的次数等。
info files 显示被调试文件的详细信息。
info func 显示所有的函数名称。
info local 显示当函数中的局部变量信息。
info prog 显示被调试程序的执行状态。
info var 显示所有的全局和静态变量名称。
kill 终止正被调试的程序。
list 显示源代码段。
make 在不退出 gdb 的情况下运行 make 工具。
next 在不单步执行进入其他函数的情况下,向前执行一行源代码。
print EXPR 显示表达式 EXPR 的值。

3.gdb 使用范例

-----------------
清单 一个有错误的 C 源程序 bugging.c
-----------------
#include
#include

static char buff [256];
static char* string;
int main ()
{

printf ("Please input a string: ");
gets (string);

printf ("\nYour string is: %s\n", string);
}
-----------------

上面这个程序非常简单,其目的是接受用户的输入,然后将用户的输入打印出来。该程序使用了一个未经过初始化的字符串地址 string,因此,编译并运行之后,将出现 Segment Fault 错误:

$ gcc -o test -g test.c
$ ./test
Please input a string: asfd
Segmentation fault (core dumped)

为了查找该程序中出现的问题,我们利用 gdb,并按如下的步骤进行:

1.运行 gdb bugging 命令,装入 bugging 可执行文件;
2.执行装入的 bugging 命令;
3.使用 where 命令查看程序出错的地方;
4.利用 list 命令查看调用 gets 函数附近的代码;
5.唯一能够导致 gets 函数出错的因素就是变量 string。用 print 命令查看 string 的值;
6.在 gdb 中,我们可以直接修改变量的值,只要将 string 取一个合法的指针值就可以了,为
此,我们在第 11 行处设置断点;
7.程序重新运行到第 11 行处停止,这时,我们可以用 set variable 命令修改 string 的取值;
8.然后继续运行,将看到正确的程序运行结果。

[目录]


gcc常用选项对代码的影响

by alert7
2001-12-21
测试环境 redhat 6.2

★ 前言
本文讨论gcc的一些常用编译选项对代码的影响。当然代码变了,它的内存布局也就会变了,随之exploit也就要做相应的变动。
gcc的编译选项实在太多,本文检了几个最常用的选项。

★ 演示程序
[alert7@redhat62 alert7]$ cat > test.c
#include
void hi(void)
{
printf("hi");
}
int main(int argc, char *argv[])
{
hi();
return 0;
}

[目录]


一般情况

★ 一般情况
[alert7@redhat62 alert7]$ gcc -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11773 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483e4 : push %ebp
0x80483e5 : mov %esp,%ebp
0x80483e7 : call 0x80483d0
0x80483ec : xor %eax,%eax
0x80483ee : jmp 0x80483f0
0x80483f0 : leave
0x80483f1 : ret
....
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483d0 : push %ebp
0x80483d1 : mov %esp,%ebp
0x80483d3 : push $0x8048450
0x80483d8 : call 0x8048308
0x80483dd : add $0x4,%esp
0x80483e0 : leave
0x80483e1 : ret
0x80483e2 : mov %esi,%esi
End of assembler dump.
来看看部分的内存映象
(内存高址)
+--------+
|bffffbc4| argv的地址(即argv[0]的地址)
0xbffffb84 +--------+
|00000001| argc的值
0xbffffb80 +--------+
|400309cb|main的返回地址
0xbffffb7c +--------+ <-- 调用main函数前的esp
|bffffb98| 调用main函数前的ebp
0xbffffb78 +--------+ <-- main函数的ebp
|080483ec| hi()的返回地址
0xbffffb74 +--------+
|bffffb78| 调用hi()前的esp
0xbffffb70 +--------+
|08048450| "hi"的地址
0xbffffb6c +--------+
| ...... |
(内存低址)
leave 指令所做的操作相当于MOV ESP,EBP 然后 POP EBP
ret 指令所做的操作相当于POP EIP

[目录]


-O 编译选项

★ -O 编译选项
With `-O', the compiler tries to reduce code size and execution time.
When you specify `-O', the two options `-fthread-jumps' and
`-fdefer-pop' are turned on
优化,减少代码大小和执行的时间
[alert7@redhat62 alert7]$ gcc -O -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11757 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483d8 : push %ebp
0x80483d9 : mov %esp,%ebp
0x80483db : call 0x80483c8
0x80483e0 : xor %eax,%eax
0x80483e2 : leave
0x80483e3 : ret
0x80483e4 : nop
...
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483c8 : push %ebp
0x80483c9 : mov %esp,%ebp
0x80483cb : push $0x8048440
0x80483d0 : call 0x8048308
0x80483d5 : leave
0x80483d6 : ret
0x80483d7 : nop
End of assembler dump.

在main()中,把一条jmp指令优化掉了,很显然,这条指令是可以不需要的。
在hi()中,把add $0x4,%esp优化掉了,这会不会使stack不平衡呢?

来看看部分的内存映象

(内存高址)
+--------+
|bffffbc4| argv的地址(即argv[0]的地址)
0xbffffb84 +--------+
|00000001| argc的值
0xbffffb80 +--------+
|400309cb|main的返回地址
0xbffffb7c +--------+ <-- 调用main函数前的esp
|bffffb98| 调用main函数前的ebp
0xbffffb78 +--------+ <-- main函数的ebp
|080483e0| hi()的返回地址
0xbffffb74 +--------+
|bffffb78| 调用hi()前的esp
0xbffffb70 +--------+
|08048440| "hi"的地址
0xbffffb6c +--------+
| ...... |
(内存低址)
leave指令所做的操作相当于把MOV ESP,EBP 然后 POP EBP。看到leave指令操作了没有,先把ebp-->esp,再pop ebp,这样即使在过程内堆栈的esp,ebp是不平衡的,但只要返回时候碰到leave指令就会平衡了,所以把add $0x4,%esp优化掉也是没有问题的。


[目录]


-O2 编译选项

★ -O2 编译选项
-O2
Optimize even more. Nearly all supported optimizations that do
not involve a space-speed tradeoff are performed. Loop unrolling
and function inlining are not done, for example. As compared to -O,
this option increases both compilation time and the performance of
the generated code.
[alert7@redhat62 alert7]$ gcc -O2 -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11757 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483d8 : push %ebp
0x80483d9 : mov %esp,%ebp
0x80483db : call 0x80483c8
0x80483e0 : xor %eax,%eax
0x80483e2 : leave
0x80483e3 : ret
...
0x80483ef : nop
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483c8 : push %ebp
0x80483c9 : mov %esp,%ebp
0x80483cb : push $0x8048440
0x80483d0 : call 0x8048308
0x80483d5 : leave
0x80483d6 : ret
0x80483d7 : nop
End of assembler dump.
由于程序比较简单,再优化也没有好优化的了,所以跟-O出来的一样。


[目录]


-fomit-frame-pointer 编译选项

★ -fomit-frame-pointer 编译选项
-fomit-frame-pointer
Don't keep the frame pointer in a register for functions
that don't need one. This avoids the instructions to save,
set up and restore frame pointers; it also makes an extra
register available in many functions. It also makes
debugging impossible on most machines.

忽略帧指针。这样在程序就不需要保存,安装,和恢复ebp了。这样ebp也就是一个free的register了,在函数中就可以随便使用了。

[alert7@redhat62 alert7]$ gcc -fomit-frame-pointer -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11773 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483e0 : call 0x80483d0
0x80483e5 : xor %eax,%eax
0x80483e7 : jmp 0x80483f0
0x80483e9 : lea 0x0(%esi,1),%esi
0x80483f0 : ret
....
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483d0 : push $0x8048450
0x80483d5 : call 0x8048308
0x80483da : add $0x4,%esp
0x80483dd : ret
0x80483de : mov %esi,%esi
End of assembler dump.
在main()和hi()中都去掉了以下指令
push %ebp
mov %esp,%ebp//这两条指令安装
leave//这条指令恢复
来看看部分的内存映象
(内存高址)
+--------+
|bffffbc4| argv的地址(即argv[0]的地址)
0xbffffb84 +--------+
|00000001| argc的值
0xbffffb80 +--------+
|400309cb|main的返回地址
0xbffffb7c +--------+
|080483e5| hi()的返回地址
0xbffffb78 +--------+
|08048450| "hi"字符串的地址
0xbffffb74 +--------+
| ...... |
(内存低址)
没有保存上层执行环境的ebp.

[目录]


-fomit-frame-pointer && -O2

★ -fomit-frame-pointer && -O2
-fomit-frame-pointer编译选项去掉了
push %ebp
mov %esp,%ebp//这两条指令安装
leave//这条指令恢复
-O2编译选项去掉了
add $0x4,%esp
两个加起来会不会这四条指令一起去掉,从而使stack不平衡呢?
[alert7@redhat62 alert7]$ gcc -fomit-frame-pointer -O2 -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11741 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483d8 : call 0x80483c8
0x80483dd : xor %eax,%eax
0x80483df : ret
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483c8 : push $0x8048430
0x80483cd : call 0x8048308
0x80483d2 : add $0x4,%esp
0x80483d5 : ret
0x80483d6 : mov %esi,%esi
End of assembler dump.
来看看部分的内存映象
(内存高址)
+--------+
|bffffbc4| argv的地址(即argv[0]的地址)
0xbffffb84 +--------+
|00000001| argc的值
0xbffffb80 +--------+
|400309cb|main的返回地址
0xbffffb7c +--------+
|080483dd| hi()的返回地址
0xbffffb78 +--------+
|08048430| "hi"字符串的地址
0xbffffb74 +--------+
| ...... |
(内存低址)
此时就没有把add $0x4,%esp优化掉,如果优化掉的话,整个stack就
会变的不平衡,从而会导致程序出错。

[目录]


-fPIC 编译选项

★ -fPIC 编译选项
-fPIC If supported for the target machine, emit position-independent
code, suitable for dynamic linking,even if branches need large
displacements.

产生位置无关代码(PIC),一般创建共享库时用到。
在x86上,PIC的代码的符号引用都是通过ebx进行操作的。

[alert7@redhat62 alert7]$ gcc -fPIC -o test test.c
[alert7@redhat62 alert7]$ wc -c test
11805 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80483f8 : push %ebp
0x80483f9 : mov %esp,%ebp
0x80483fb : push %ebx
0x80483fc : call 0x8048401
0x8048401 : pop %ebx//取得该指令的地址
0x8048402 : add $0x1093,%ebx//此时ebx里面存放着是GOT表的地址
0x8048408 : call 0x80483d0
0x804840d : xor %eax,%eax
0x804840f : jmp 0x8048411
0x8048411 : mov 0xfffffffc(%ebp),%ebx
0x8048414 : leave
0x8048415 : ret
...
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80483d0 : push %ebp
0x80483d1 : mov %esp,%ebp
0x80483d3 : push %ebx
0x80483d4 : call 0x80483d9
0x80483d9 : pop %ebx
0x80483da : add $0x10bb,%ebx
0x80483e0 : lea 0xffffefdc(%ebx),%edx
0x80483e6 : mov %edx,%eax
0x80483e8 : push %eax
0x80483e9 : call 0x8048308
0x80483ee : add $0x4,%esp
0x80483f1 : mov 0xfffffffc(%ebp),%ebx
0x80483f4 : leave
0x80483f5 : ret
0x80483f6 : mov %esi,%esi
End of assembler dump.
来看看部分的内存映象

(内存高址)
+--------+
|bffffbc4| argv的地址(即argv[0]的地址)
0xbffffb84 +--------+
|00000001| argc的值
0xbffffb80 +--------+
|400309cb|main的返回地址
0xbffffb7c +--------+ <-- 调用main函数前的esp
|bffffb98| 调用main函数前的ebp
0xbffffb78 +--------+ <-- main函数的ebp
|401081ec| 保存的ebx
0xbffffb74 +--------+
|0804840d| (存放过call 0x8048401的下一条指令地址)
0xbffffb70 +--------+
|bffffb78| 调用hi()前的esp
0xbffffb6c +--------+
|08049494| GOT表地址
0xbffffb68 +--------+
|08048470|(存放过call 0x80483d9的下一条指令地址)
0xbffffb64 +--------+
| ...... |
(内存低址)

[目录]


-static 编译选项

★ -static 编译选项
-static
On systems that support dynamic linking, this prevents
linking with the shared libraries. On other systems,
this option has no effect.
把一些函数都静态的编译到程序中,而无需动态链接了。
[alert7@redhat62 alert7]$ gcc -o test -static test.c
[alert7@redhat62 alert7]$ wc -c test
962808 test
[alert7@redhat62 alert7]$ gdb -q test
(gdb) disass main
Dump of assembler code for function main:
0x80481b4 : push %ebp
0x80481b5 : mov %esp,%ebp
0x80481b7 : call 0x80481a0
0x80481bc : xor %eax,%eax
0x80481be : jmp 0x80481c0
0x80481c0 : leave
0x80481c1 : ret
...
End of assembler dump.
(gdb) disass hi
Dump of assembler code for function hi:
0x80481a0 : push %ebp
0x80481a1 : mov %esp,%ebp
0x80481a3 : push $0x8071528
0x80481a8 : call 0x804865c
0x80481ad : add $0x4,%esp
0x80481b0 : leave
0x80481b1 : ret
0x80481b2 : mov %esi,%esi
End of assembler dump.
[alert7@redhat62 alert7]$ ldd test

not a dynamic executable
-static出来的代码已经没有PLT了,GOT虽然有,已经全部为0了。

[目录]


AT&T的汇编格式

一 基本语法

语法上主要有以下几个不同.

★ 寄存器命名原则

AT&T: %eax Intel: eax

★源/目的操作数顺序

AT&T: movl %eax,%ebx Intel: mov ebx,eax

★常数/立即数的格式

AT&T: movl $_value,%ebx Intel: mov eax,_value
把_value的地址放入eax寄存器

AT&T: movl $0xd00d,%ebx Intel: mov ebx,0xd00d

★ 操作数长度标识

AT&T: movw %ax,%bx Intel: mov bx,ax

★寻址方式

AT&T: immed32(basepointer,indexpointer,indexscale)
Intel: [basepointer + indexpointer*indexscale + imm32)

Linux工作于保护模式下,用的是32位线性地址,所以在计算地址时不用考虑segment:offset的问题.上式中的地址应为:
imm32 + basepointer + indexpointer*indexscale

下面是一些例子:

★直接寻址

AT&T: _booga ; _booga是一个全局的C变量

注意加上$是表示地址引用,不加是表示值引用.
注:对于局部变量,可以通过堆栈指针引用.

Intel: [_booga]

★寄存器间接寻址

AT&T: (%eax)
Intel: [eax]

★变址寻址

AT&T: _variable(%eax)
Intel: [eax + _variable]

AT&T: _array(,%eax,4)
Intel: [eax*4 + _array]

AT&T: _array(%ebx,%eax,8)
Intel: [ebx + eax*8 + _array]

二 基本的行内汇编

·基本的行内汇编很简单,一般是按照下面的格式:
asm("statements");
例如:asm("nop"); asm("cli");

·asm 和 __asm__是完全一样的.

·如果有多行汇编,则每一行都要加上 "\n\t"
例如:

asm( "pushl %eax\n\t"
"movl $0,%eax\n\t"
"popl %eax");

实际上gcc在处理汇编时,是要把asm(...)的内容"打印"到汇编文件中,所以格式控制字符是必要的.

再例如:
asm("movl %eax,%ebx");
asm("xorl %ebx,%edx");
asm("movl $0,_booga);

在上面的例子中,由于我们在行内汇编中改变了edx和ebx的值,但是由于gcc的特殊的处理方法,即先形成汇编文件,再交给GAS去汇编,所以GAS并 不知道我们已经改变了edx和ebx的值,如果程序的上下文需要edx或ebx作暂存,这样就会引起严重的后果.对于变量_booga也存在一样的问题. 为了解决这个问题,就要用到扩展的行内汇编语法.

三 扩展的行内汇编

扩展的行内汇编类似于Watcom.

基本的格式是:
asm ( "statements" : output_regs : input_regs : clobbered_regs);
clobbered_regs指的是被改变的寄存器.

下面是一个例子(为方便起见,我使用全局变量):

int count=1;
int value=1;
int buf[10];
void main()
{
asm(
"cld \n\t"
"rep \n\t"
"stosl"
:
: "c" (count), "a" (value) , "D" (buf[0])
: "%ecx","%edi" );
}

得到的主要汇编代码为:

movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP

cld,rep,stos就不用多解释了.这几条语句的功能是向buf中写上count个value值.冒号后的语句指明输入,输出和被改变的寄存器.通 过冒号以后的语句,编译器就知道你的指令需要和改变哪些寄存器,从而可以优化寄存器的分配.
其中符号"c"(count)指示要把count的值放入ecx寄存器

类似的还有:

a eax
b ebx
c ecx
d edx
S esi
D edi
I 常数值,(0 - 31)
q,r 动态分配的寄存器
g eax,ebx,ecx,edx或内存变量
A 把eax和edx合成一个64位的寄存器(use long longs)

我们也可以让gcc自己选择合适的寄存器.
如下面的例子:
asm("leal (%1,%1,4),%0"
: "=r" (x)
: "0" (x) );

这段代码实现5*x的快速乘法.
得到的主要汇编代码为:
movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x

几点说明:

1.使用q指示编译器从eax,ebx,ecx,edx分配寄存器.使用r指示编译器从eax,ebx,ecx,edx,esi,edi分配寄存器.

2.我们不必把编译器分配的寄存器放入改变的寄存器列表,因为寄存器已经记住了它们.

3."="是标示输出寄存器,必须这样用.

4.数字%n的用法:

数字表示的寄存器是按照出现和从左到右的顺序映射到用"r"或"q"请求的寄存器.如果我们要重用"r"或"q"请求的寄存器的话,就可以使用它们.

5.如果强制使用固定的寄存器的话,如不用%1,而用ebx,则asm("leal (%%ebx,%%ebx,4),%0"

: "=r" (x)
: "0" (x) );

注意要使用两个%,因为一个%的语法已经被%n用掉了.

下面可以来解释letter 4854-4855的问题:


1、变量加下划线和双下划线有什么特殊含义吗?
加下划线是指全局变量,但我的gcc中加不加都无所谓.

2、以上定义用如下调用时展开会是什么意思?
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
/* __res应该是一个全局变量 */
__asm__ volatile ("int $0x80" \
/* volatile 的意思是不允许优化,使编译器严格按照你的汇编代码汇编*/
: "=a" (__res) \
/* 产生代码 movl %eax, __res */
: "0" (__NR_##name),"b" ((long)(arg1))); \
/* 如果我没记错的话,这里##指的是两次宏展开.
  即用实际的系统调用名字代替"name",然后再把__NR_...展开.
  接着把展开的常数放入eax,把arg1放入ebx */
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}

[目录]


x86内联汇编

本文提供了在 Linux 平台上使用和构造 x86 内联汇编的概括性介绍。他介绍了内联汇编及其各种用法的基础知识,提供了一些基本的内联汇编编码指导,并解释了在 Linux 内核中内联汇编代码的一些实例。
如果您是 Linux 内核的开发人员,您会发现自己经常要对与体系结构高度相关的功能进行编码或优化代码路径。您很可能是通过将汇编语言指令插入到 C 语句的中间(又称为内联汇编的一种方法)来执行这些任务的。让我们看一下 Linux 中内联汇编的特定用法。(我们将讨论限制在 IA32 汇编。)
[目录]


简述

GNU 汇编程序简述
让我们首先看一下 Linux 中使用的基本汇编程序语法。GCC(用于 Linux 的 GNU C 编译器)使用 AT&T 汇编语法。下面列出了这种语法的一些基本规则。(该列表肯定不完整;只包括了与内联汇编相关的那些规则。)

寄存器命名
寄存器名称有 % 前缀。即,如果必须使用 eax,它应该用作 %eax。

源操作数和目的操作数的顺序
在所有指令中,先是源操作数,然后才是目的操作数。这与将源操作数放在目的操作数之后的 Intel 语法不同。
mov %eax, %ebx, transfers the contents of eax to ebx.

操作数大小
根据操作数是字节 (byte)、字 (word) 还是长型 (long),指令的后缀可以是 b、w 或 l。这并不是强制性的;GCC 会尝试通过读取操作数来提供相应的后缀。但手工指定后缀可以改善代码的可读性,并可以消除编译器猜测不正确的可能性。

movb %al, %bl -- Byte move
movw %ax, %bx -- Word move
movl %eax, %ebx -- Longword move

立即操作数
通过使用 $ 指定直接操作数。

movl $0xffff, %eax -- will move the value of 0xffff into eax register.

间接内存引用
任何对内存的间接引用都是通过使用 ( ) 来完成的。

movb (%esi), %al -- will transfer the byte in the memory
pointed by esi into al register

[目录]


内联汇编

内联汇编
GCC 为内联汇编提供特殊结构,它具有以下格式:
asm ( assembler template
: output operands (optional)
: input operands (optional)
: list of clobbered registers (optional)
);

本例中,汇编程序模板由汇编指令组成。输入操作数是充当指令输入操作数使用的 C 表达式。输出操作数是将对其执行汇编指令输出的 C 表达式。

内联汇编的重要性体现在它能够灵活操作,而且可以使其输出通过 C 变量显示出来。因为它具有这种能力,所以 "asm" 可以用作汇编指令和包含它的 C 程序之间的接口。

一个非常基本但很重要的区别在于简单内联汇编只包括指令,而扩展内联汇编包括操作数。要说明这一点,考虑以下示例:

内联汇编的基本要素

{
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* output */
:"r"(a) /* input */
:"%eax"); /* clobbered register */
}

在上例中,我们使用汇编指令使 "b" 的值等于 "a"。请注意以下几点:
"b" 是输出操作数,由 %0 引用,"a" 是输入操作数,由 %1 引用。
"r" 是操作数的约束,它指定将变量 "a" 和 "b" 存储在寄存器中。请注意,输出操作数约束应该带有一个约束修饰符 "=",指定它是输出操作数。
要在 "asm" 内使用寄存器 %eax,%eax 的前面应该再加一个 %,换句话说就是 %%eax,因为 "asm" 使用 %0、%1 等来标识变量。任何带有一个 % 的数都看作是输入/输出操作数,而不认为是寄存器。
第三个冒号后的修饰寄存器 %eax 告诉将在 "asm" 中修改 GCC %eax 的值,这样 GCC 就不使用该寄存器存储任何其它的值。
movl %1, %%eax 将 "a" 的值移到 %eax 中,movl %%eax, %0 将 %eax 的内容移到 "b" 中。
因为 "b" 被指定成输出操作数,因此当 "asm" 的执行完成后,它将反映出更新的值。换句话说,对 "asm" 内 "b" 所做的更改将在 "asm" 外反映出来。


[目录]


程序模板

汇编程序模板是一组插入到 C 程序中的汇编指令(可以是单个指令,也可以是一组指令)。每条指令都应该由双引号括起,或者整组指令应该由双引号括起。每条指令还应该用一个定界符结尾。 有效的定界符为新行 (\n) 和分号 (;)。 '\n' 后可以跟一个 tab(\t) 作为格式化符号,增加 GCC 在汇编文件中生成的指令的可读性。 指令通过数 %0、%1 等来引用 C 表达式(指定为操作数)。

如果希望确保编译器不会在 "asm" 内部优化指令,可以在 "asm" 后使用关键字 "volatile"。如果程序必须与 ANSI C 兼容,则应该使用 __asm__ 和 __volatile__,而不是 asm 和 volatile。

[目录]


操作数

C 表达式用作 "asm" 内的汇编指令操作数。在汇编指令通过对 C 程序的 C 表达式进行操作来执行有意义的作业的情况下,操作数是内联汇编的主要特性。

每个操作数都由操作数约束字符串指定,后面跟用括弧括起的 C 表达式,例如:"constraint" (C expression)。操作数约束的主要功能是确定操作数的寻址方式。

可以在输入和输出部分中同时使用多个操作数。每个操作数由逗号分隔开。

在汇编程序模板内部,操作数由数字引用。如果总共有 n 个操作数(包括输入和输出),那么第一个输出操作数的编号为 0,逐项递增,最后那个输入操作数的编号为 n-1。总操作数的数目限制在 10,如果机器描述中任何指令模式中的最大操作数数目大于 10,则使用后者作为限制。

[目录]


修饰寄存器列表

如果 "asm" 中的指令指的是硬件寄存器,可以告诉 GCC 我们将自己使用和修改它们。这样,GCC 就不会假设它装入到这些寄存器中的值是有效值。通常不需要将输入和输出寄存器列为 clobbered,因为 GCC 知道 "asm" 使用它们(因为它们被明确指定为约束)。不过,如果指令使用任何其它的寄存器,无论是明确的还是隐含的(寄存器不在输入约束列表中出现,也不在输出约束列 表中出现),寄存器都必须被指定为修饰列表。修饰寄存器列在第三个冒号之后,其名称被指定为字符串。

至于关键字,如果指令以某些不可预知且不明确的方式修改了内存,则可能将 "memory" 关键字添加到修饰寄存器列表中。这样就告诉 GCC 不要在不同指令之间将内存值高速缓存在寄存器中。

[目录]


操作数约束

前面提到过,"asm" 中的每个操作数都应该由操作数约束字符串描述,后面跟用括弧括起的 C 表达式。操作数约束主要是确定指令中操作数的寻址方式。约束也可以指定:

·是否允许操作数位于寄存器中,以及它可以包括在哪些种类的寄存器中
·操作数是否可以是内存引用,以及在这种情况下使用哪些种类的地址
·操作数是否可以是立即数

约束还要求两个操作数匹配。

常用约束

在可用的操作数约束中,只有一小部分是常用的;下面列出了这些约束以及简要描述。有关操作数约束的完整列表,请参考 GCC 和 GAS 手册。

寄存器操作数约束 (r)
使用这种约束指定操作数时,它们存储在通用寄存器中。请看下例:
asm ("movl %%cr3, %0\n" :"=r"(cr3val));
这里,变量 cr3val 保存在寄存器中,%cr3 的值复制到寄存器上,cr3val 的值从该寄存器更新到内存中。指定 "r" 约束时,GCC 可以将变量 cr3val 保存在任何可用的 GPR 中。要指定寄存器,必须通过使用特定的寄存器约束直接指定寄存器名。

a %eax
b %ebx
c %ecx
d %edx
S %esi
D %edi

内存操作数约束 (m)
当操作数位于内存中时,任何对它们执行的操作都将在内存位置中直接发生,这与寄存器约束正好相反,后者先将值存储在要修改的寄存器中,然后将它写回内存位 置中。但寄存器约束通常只在对于指令来说它们是绝对必需的,或者它们可以大大提高进程速度时使用。当需要在 "asm" 内部更新 C 变量,而您又确实不希望使用寄存器来保存其值时,使用内存约束最为有效。例如,idtr 的值存储在内存位置 loc 中:
("sidt %0\n" : :"m"(loc));

匹配(数字)约束
在某些情况下,一个变量既要充当输入操作数,也要充当输出操作数。可以通过使用匹配约束在 "asm" 中指定这种情况。

asm ("incl %0" :"=a"(var):"0"(var));

在匹配约束的示例中,寄存器 %eax 既用作输入变量,也用作输出变量。将 var 输入读取到 %eax,增加后将更新的 %eax 再次存储在 var 中。这里的 "0" 指定第 0 个输出变量相同的约束。即,它指定 var 的输出实例只应该存储在 %eax 中。该约束可以用于以下情况:

·输入从变量中读取,或者变量被修改后,修改写回到同一变量中
·不需要将输入操作数和输出操作数的实例分开
·使用匹配约束最重要的意义在于它们可以导致有效地使用可用寄存器。


[目录]


示例

一般内联汇编用法示例
以下示例通过各种不同的操作数约束说明了用法。有如此多的约束以至于无法将它们一一列出,这里只列出了最经常使用的那些约束类型。


[目录]


寄存器约束

"asm" 和寄存器约束 "r"
让我们先看一下使用寄存器约束 r 的 "asm"。我们的示例显示了 GCC 如何分配寄存器,以及它如何更新输出变量的值。

int main(void)
{
int x = 10, y;
asm ("movl %1, %%eax;
"movl %%eax, %0;"
:"=r"(y) /* y is output operand */
:"r"(x) /* x is input operand */
:"%eax"); /* %eax is clobbered register */
}

在该例中,x 的值复制为 "asm" 中的 y。x 和 y 都通过存储在寄存器中传递给 "asm"。为该例生成的汇编代码如下:

main:
pushl %ebp
movl %esp,%ebp
subl $8,%esp
movl $10,-4(%ebp)
movl -4(%ebp),%edx /* x=10 is stored in %edx */
#APP /* asm starts here */
movl %edx, %eax /* x is moved to %eax */
movl %eax, %edx /* y is allocated in edx and updated */
#NO_APP /* asm ends here */
movl %edx,-8(%ebp) /* value of y in stack is updated with the value in %edx */

当使用 "r" 约束时,GCC 在这里可以自由分配任何寄存器。在我们的示例中,它选择 %edx 来存储 x。在读取了 %edx 中 x 的值后,它为 y 也分配了相同的寄存器。

因为 y 是在输出操作数部分中指定的,所以 %edx 中更新的值存储在 -8(%ebp),堆栈上 y 的位置中。如果 y 是在输入部分中指定的,那么即使它在 y 的临时寄存器存储值 (%edx) 中被更新,堆栈上 y 的值也不会更新。

因为 %eax 是在修饰列表中指定的,GCC 不在任何其它地方使用它来存储数据。

输入 x 和输出 y 都分配在同一个 %edx 寄存器中,假设输入在输出产生之前被消耗。请注意,如果您有许多指令,就不是这种情况了。要确保输入和输出分配到不同的寄存器中,可以指定 & 约束修饰符。下面是添加了约束修饰符的示例。

int main(void)
{
int x = 10, y;
asm ("movl %1, %%eax;
"movl %%eax, %0;"
:"=&r"(y) /* y is output operand, note the & constraint modifier. */
:"r"(x) /* x is input operand */
:"%eax"); /* %eax is clobbered register */
}

以下是为该示例生成的汇编代码,从中可以明显地看出 x 和 y 存储在 "asm" 中不同的寄存器中。

main:
pushl %ebp
movl %esp,%ebp
subl $8,%esp
movl $10,-4(%ebp)
movl -4(%ebp),%ecx /* x, the input is in %ecx */
#APP
movl %ecx, %eax
movl %eax, %edx /* y, the output is in %edx */
#NO_APP
movl %edx,-8(%ebp)

特定寄存器约束的使用
现在让我们看一下如何将个别寄存器作为操作数的约束指定。在下面的示例中,cpuid 指令采用 %eax 寄存器中的输入,然后在四个寄存器中给出输出:%eax、%ebx、%ecx、%edx。对 cpuid 的输入(变量 "op")传递到 "asm" 的 eax 寄存器中,因为 cpuid 希望它这样做。在输出中使用 a、b、c 和 d 约束,分别收集四个寄存器中的值。

asm ("cpuid"
: "=a" (_eax),
"=b" (_ebx),
"=c" (_ecx),
"=d" (_edx)
: "a" (op));

在下面可以看到为它生成的汇编代码(假设 _eax、_ebx 等... 变量都存储在堆栈上):

movl -20(%ebp),%eax /* store 'op' in %eax -- input */
#APP
cpuid
#NO_APP
movl %eax,-4(%ebp) /* store %eax in _eax -- output */
movl %ebx,-8(%ebp) /* store other registers in
movl %ecx,-12(%ebp)
respective output variables */
movl %edx,-16(%ebp)

strcpy 函数可以通过以下方式使用 "S" 和 "D" 约束来实现:

asm ("cld\n
rep\n
movsb"
: /* no input */
:"S"(src), "D"(dst), "c"(count));

通过使用 "S" 约束将源指针 src 放入 %esi 中,使用 "D" 约束将目的指针 dst 放入 %edi 中。因为 rep 前缀需要 count 值,所以将它放入 %ecx 中。

在下面可以看到另一个约束,它使用两个寄存器 %eax 和 %edx 将两个 32 位的值合并在一起,然后生成一个64 位的值:

#define rdtscll(val) \
__asm__ __volatile__ ("rdtsc" : "=A" (val))
The generated assembly looks like this (if val has a 64 bit memory space).
#APP
rdtsc
#NO_APP
movl %eax,-8(%ebp) /* As a result of A constraint
movl %edx,-4(%ebp)
%eax and %edx serve as outputs */
Note here that the values in %edx:%eax serve as 64 bit output.


[目录]


匹配约束

使用匹配约束
在下面将看到系统调用的代码,它有四个参数:

#define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \
type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \
"d" ((long)(arg3)),"S" ((long)(arg4))); \
__syscall_return(type,__res); \
}

在上例中,通过使用 b、c、d 和 S 约束将系统调用的四个自变量放入 %ebx、%ecx、%edx 和 %esi 中。请注意,在输出中使用了 "=a" 约束,这样,位于 %eax 中的系统调用的返回值就被放入变量 __res 中。通过将匹配约束 "0" 用作输入部分中第一个操作数约束,syscall 号 __NR_##name 被放入 %eax 中,并用作对系统调用的输入。这样,这里的 %eax 既可以用作输入寄存器,又可以用作输出寄存器。没有其它寄存器用于这个目的。另请注意,输入(syscall 号)在产生输出(syscall 的返回值)之前被消耗(使用)。

[目录]


内存操作数约束

内存操作数约束的使用
请考虑下面的原子递减操作:

__asm__ __volatile__(
"lock; decl %0"
:"=m" (counter)
:"m" (counter));

为它生成的汇编类似于:

#APP
lock
decl -24(%ebp) /* counter is modified on its memory location */
#NO_APP.

您可能考虑在这里为 counter 使用寄存器约束。如果这样做,counter 的值必须先复制到寄存器,递减,然后对其内存更新。但这样您会无法理解锁定和原子性的全部意图,这些明确显示了使用内存约束的必要性。

[目录]


修饰寄存器

使用修饰寄存器
请考虑内存拷贝的基本实现。

asm ("movl $count, %%ecx;
up: lodsl;
stosl;
loop up;"
: /* no output */
:"S"(src), "D"(dst) /* input */
:"%ecx", "%eax" ); /* clobbered list */

当 lodsl 修改 %eax 时,lodsl 和 stosl 指令隐含地使用它。%ecx 寄存器明确装入 count。但 GCC 在我们通知它以前是不知道这些的,我们是通过将 %eax 和 %ecx 包括在修饰寄存器集中来通知 GCC 的。在完成这一步之前,GCC 假设 %eax 和 %ecx 是自由的,它可能决定将它们用作存储其它的数据。请注意,%esi 和 %edi 由 "asm" 使用,它们不在修饰列表中。这是因为已经声明 "asm" 将在输入操作数列表中使用它们。这里最低限度是,如果在 "asm" 内部使用寄存器(无论是明确还是隐含地),既不出现在输入操作数列表中,也不出现在输出操作数列表中,必须将它列为修饰寄存器。

[目录]


不同的CPU下最佳编译参数

gcc在不同的体系机构/CPU下编译效果有不同,需要使用不同的编译参数达到最佳效果。


一、1.2版(gcc 2.9.x版)

-pipe -fomit-frame-pointer"
CXXFLAGS="-march=i486 -O3 -pipe -fomit-frame-pointer"

Pentium, Pentium MMX+, Celeron (Mendocino) (Intel)
CHOST="i586-pc-linux-gnu"
CFLAGS="-march=pentium -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=pentium -O3 -pipe -fomit-frame-pointer"

Pentium Pro/II/III/4, Celeron (Coppermine), Celeron (Willamette?) (Intel)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=i686 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=i686 -O3 -pipe -fomit-frame-pointer"

Eden C3/Ezra (Via)
CHOST="i586-pc-linux-gnu"
CFLAGS="-march=i586 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=i586 -O3 -pipe -fomit-frame-pointer"

Quote : I did the original gentoo install using 1.2, with gcc 2.95 using -march=i586. i686 won't work.

K6 or beyond (AMD)
CHOST="i586-pc-linux-gnu"
CFLAGS="-march=k6 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=k6 -O3 -pipe -fomit-frame-pointer"

(A Duron will report "Athlon" in its /proc/cpuinfo)

Athlon (AMD)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=k6 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=k6 -O3 -pipe -fomit-frame-pointer"

For the following, i don't know of any flag that enhance performances..., do you ?

PowerPC
CHOST="powerpc-unknown-linux-gnu"
CFLAGS="-O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-O3 -pipe -fomit-frame-pointer"

Sparc
CHOST="sparc-unknown-linux-gnu"
CFLAGS="-O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-O3 -pipe -fomit-frame-pointer"

Sparc 64
CHOST="sparc64-unknown-linux-gnu"
CFLAGS="-O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-O3 -pipe -fomit-frame-pointer"

二、1.4版(gcc 3.x版):


i386 (Intel), do you really want to install gentoo on that ?
CHOST="i386-pc-linux-gnu"
CFLAGS="-march=i386 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=i386 -O3 -pipe -fomit-frame-pointer"

i486 (Intel), do you really want to install gentoo on that ?
CHOST="i486-pc-linux-gnu"
CFLAGS="-march=i486 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=i486 -O3 -pipe -fomit-frame-pointer"

Pentium 1 (Intel)
CHOST="i586-pc-linux-gnu"
CFLAGS="-march=pentium -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=pentium -O3 -pipe -fomit-frame-pointer"

Pentium MMX (Intel)
CHOST="i586-pc-linux-gnu"
CFLAGS="-march=pentium-mmx -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=pentium-mmx -O3 -pipe -fomit-frame-pointer"

Pentium PRO (Intel)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=pentiumpro -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=pentiumpro -O3 -pipe -fomit-frame-pointer"

Pentium II (Intel)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=pentium2 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=pentium2 -O3 -pipe -fomit-frame-pointer"

Celeron (Mendocino), aka Celeron1 (Intel)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=pentium2 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=pentium2 -O3 -pipe -fomit-frame-pointer"

Pentium III (Intel)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=pentium3 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=pentium3 -O3 -pipe -fomit-frame-pointer"

Celeron (Coppermine) aka Celeron2 (Intel)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=pentium3 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=pentium3 -O3 -pipe -fomit-frame-pointer"

Celeron (Willamette?) (Intel)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=pentium4 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=pentium4 -O3 -pipe -fomit-frame-pointer"

Pentium 4 (Intel)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=pentium4 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=pentium4 -O3 -pipe -fomit-frame-pointer"

Eden C3/Ezra (Via)
CHOST="i586-pc-linux-gnu"
CFLAGS="-march=i586 -m3dnow -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=i586 -m3dnow -O3 -pipe -fomit-frame-pointer"

quote : the ezra doesn't have any special instructions that you could optimize for, just consider is a K6-3...basically
a p2 with 3dnow

K6 (AMD)
CHOST="i586-pc-linux-gnu"
CFLAGS="-march=k6 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=k6 -O3 -pipe -fomit-frame-pointer"

K6-2 (AMD)
CHOST="i586-pc-linux-gnu"
CFLAGS="-march=k6-2 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=k6-2 -O3 -pipe -fomit-frame-pointer"

K6-3 (AMD)
CHOST="i586-pc-linux-gnu"
CFLAGS="-march=k6-3 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=k6-3 -O3 -pipe -fomit-frame-pointer"

Athlon (AMD)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=athlon -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=athlon -O3 -pipe -fomit-frame-pointer"

Athlon-tbird, aka K7 (AMD)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=athlon-tbird -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=athlon-tbird -O3 -pipe -fomit-frame-pointer"

Athlon-tbird XP (AMD)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=athlon-xp -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=athlon-xp -O3 -pipe -fomit-frame-pointer"

Athlon 4(AMD)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=athlon-4 -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=athlon-4 -O3 -pipe -fomit-frame-pointer"

Athlon XP (AMD)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=athlon-xp -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=athlon-xp -O3 -pipe -fomit-frame-pointer"

Athlon MP (AMD)
CHOST="i686-pc-linux-gnu"
CFLAGS="-march=athlon-mp -O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-march=athlon-mp -O3 -pipe -fomit-frame-pointer"

603 (PowerPC)
CHOST="powerpc-unknown-linux-gnu"
CFLAGS="-O3 -pipe -fomit-frame-pointer -fsigned-char"
CXXFLAGS="-O3 -pipe -fomit-frame-pointer -fsigned-char"

603e (PowerPC)
CHOST="powerpc-unknown-linux-gnu"
CFLAGS="-O3 -pipe -fomit-frame-pointer -fsigned-char"
CXXFLAGS="-O3 -pipe -fomit-frame-pointer -fsigned-char"

604 (PowerPC)
CHOST="powerpc-unknown-linux-gnu"
CFLAGS="-O3 -pipe -fomit-frame-pointer -fsigned-char"
CXXFLAGS="-O3 -pipe -fomit-frame-pointer -fsigned-char"

604e (PowerPC)
CHOST="powerpc-unknown-linux-gnu"
CFLAGS="-O3 -pipe -fomit-frame-pointer -fsigned-char"
CXXFLAGS="-O3 -pipe -fomit-frame-pointer -fsigned-char"

750 aka as G3 (PowerPC)
CHOST="powerpc-unknown-linux-gnu"
CFLAGS="-mcpu=750 -O3 -pipe -fomit-frame-pointer
-fsigned-char"
CXXFLAGS="-mcpu=750 -O3 -pipe -fomit-frame-pointer
-fsigned-char"

Note: do not use -march=

7400, aka G4 (PowerPC)
CHOST="powerpc-unknown-linux-gnu"
CFLAGS="-mcpu=7400 -O3 -pipe -fomit-frame-pointer
-fsigned-char -maltivec"
CXXFLAGS="-mcpu=7400 -O3 -pipe -fomit-frame-pointer
-fsigned-char -maltivec"

Note: do not use -march=

7450, aka G4 second generation (PowerPC)
CHOST="powerpc-unknown-linux-gnu"
CFLAGS="-mcpu=7450 -O3 -pipe -fomit-frame-pointer
-fsigned-char -maltivec"
CXXFLAGS="-mcpu=7450 -O3 -pipe -fomit-frame-pointer
-fsigned-char -maltivec"

Note: do not use -march=

PowerPC (If you don't know which one)
CHOST="powerpc-unknown-linux-gnu"
CFLAGS="-O3 -pipe -fomit-frame-pointer -fsigned-char"
CXXFLAGS="-O3 -pipe -fomit-frame-pointer -fsigned-char"

Sparc
CHOST="sparc-unknown-linux-gnu"
CFLAGS="-O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-O3 -pipe -fomit-frame-pointer"

Sparc 64
CHOST="sparc64-unknown-linux-gnu"
CFLAGS="-O3 -pipe -fomit-frame-pointer"
CXXFLAGS="-O3 -pipe -fomit-frame-pointer"

[目录]


代码维护

[目录]


简单cvs

简单CVS
一、所要解决的问题
由于软件项目越来越大,也增加了软件项目管理的难度。在开发组中,每个成员都要保留一个副本,在开发中非常容易引起冲突。CVS 就是为了解决这个问题的。

a、修改同步,防止一名开发人员的修改覆盖其他人的成果。(check out、read only)

b、维护不同的版本。(按 version 查找)

c、可查找历史记录。防止 bug 的再引入。(diff)

CVS 为了解决这个问题,采用的方式是:

当开发人员对源代码进行修改时,修改的内容被登记(check in)到了 CVS 仓库(repository)中。仓库中保存了代码的主控副本,以及历次修改的历史信息。它不保存文件的每个版本,而只是简单的记录发生在每个版本间的不 同,节省磁盘空间。它能做到:

a、使开发人员的目录和仓库保持一致。可以把自己的修改提交(commit)给仓库,让仓库更新自己。

b、允许代码派生。可以进行测试,如果失败,可以消除所做的修改,维持原结果。

c、检索任何一个版本。

二、使用 CVS:

a、建立仓库:设置并 export CVSROOT 变量,并设置仓库目录。比如创建 /home/cvsroot 目录,并合理设置权限,在 .bash_profile 中加入:export CVSROOT=$HOME/cvsroot;运行 cvs init;设置让用户输入日志信息的默认的编辑器:export EDITOR=vim。如果使用的是网络,则 CVSROOT 变量的形式是: export CVSROOT=:exit:user@server:/path,比如:export CVSROOT=:exit:david@power/home/projects/repository

b、导入文件或目录:cvs import filename_or_directory vender_tag release_tag;这三个参数的含义是:在仓库中这些导入的文件所在的目录、供应商标记、发行标记。比如 cvs import step1 david start。导入时的 N 表示所导入的文件都是新文件。

c、使用时导出文件,用:cvs checkout directory_name;参数含义是仓库中所在的目录。建立新的目录,而不是获取他人的改动

d、对文件修改后,保存修改到仓库:cvs commit。

e、如果要获得他人的修改,使用 cvs update,U 表示本地的一个文件已经被更新。如果已经对文件进行了修改,而此是他人已经修改了该文件并提交,cvs 将告诉用户发生冲突和冲突的位置。

f、添加一个文件:cvs add filename; cvs commit。

g、删除一个文件:先在本地删除,然后使用 cvs remove file_name; cvs commit。

三、使用标记

可以使用标记记录某个时刻文件的内容,这在制作发行版本的过程中非常有用:

cvs tag release1.0

改动后发现不正常,重新获得这个版本,则使用:cvs checkout -r release1.0

四、测试性代码:

当其中一个开发者对代码进行改进,但未来结果不能确定时,可以使用 cvs 产生出一个代码的分之,这并不改变主控代码:cvs tag -b for_test。导出该代码的命令是:cvs checkout -r for_test example。由于在已有的目录树中不会使用该分之,因此必须重新建立目录树。如果测试成功,则要求将主控代码和测试代码合并,则先导出主控代码,然后 合并:

cvs checkout;cvs update -jfor_test;cvs commit。


[目录]


automake

通常情况下,在写完自己代码后,使用 make 命令进行编译。make 命令其实什么也不做,知识读取一个叫 Makefile 的文件,从中获得 dependence and rule,然后调用 gcc 进行编译。但是 Makefile 比较复杂,变化技巧也比较多。对于一个大的工程项目来说,如果没有一个统一的风格,在工程延续的时候改动 Makefile 会很麻烦,也容易出错误。所以这时就有了使用 automake 的需求。使用 automake,只需要掌握一点点规则,定义一些变量,就能自动生成 Makefile。而这些 Makefile 有着统一的格式和结构,便于修改。下面就如何使用 automake 举出一个实际的例子。

2.1 使用 automake 的前提条件


在使用 automake 前,请先确认在系统中安装了如下软件:

GNU Automake

GNU Autoconf

GNU m4

perl

GNU Libtool(如果需要产生 shared library)如果没有的话,请在发行版中找相应的 rpm 包。

2.2 制作 configure 脚本

autoconf 是用来生成自动配置软件源代码脚本(configure)的工具。configure 脚本独立于 autoconf 运行,而且在运行的过程中,不需要用户的干预,通常不需要附带参数。它是用来检验软件必须的参数的。autoconf 从一个列举编译软件时所需要各种参数的模板文件中创建 configure。autoconf 需要 GNU m4 来生成该脚本。

由 autoconf 生成的脚本一般起名为 configure。当运行时,configure 创建了多个文件,并对这些文件中的配置参数赋予适当的值。由 configure 创建生成的文件有:

1。一个或多个 Makefile,在软件源代码的每个目录中都生成一个 Makefile。

2。还可选的生成 C 头文件——configurable,包含了各种 #define 声明。

3。一个名为 config.status 的脚本,当运行时,重新生成上面的文件。

4。一个名为 config.cache 的脚本,保存运行检测的结果。

5。一个名为 config.log 的文件,保存有编译器生成的信息,用于调试 configure。

为了让 autoconf 生成 configure 脚本,需要以 configure.in 为参数调用 autoconf。如果要检测自己的各种参数,以作为对 autoconf 的补充,则需要写 aclocal.m4 和 acsite.m4 的文件。如果要使用 C 头文件,需要写 acconfig.h,并且将 autoconf 生成的 config.h.in 同软件一起发行。

your source files --> [autoscan*] --> [configure.scan] --> configure.in


configure.in --. .------> autoconf* -----> configure

+---+

[aclocal.m4] --+ `---.

[acsite.m4] ---' |

+--> [autoheader*] -> [config.h.in]

[acconfig.h] ----. |

+-----'

[config.h.top] --+

[config.h.bot] --'


Makefile.in -------------------------------> Makefile.in

Files used in configuring a software package:

.-------------> config.cache

configure* ------------+-------------> config.log

|

[config.h.in] -. v .-> [config.h] -.

+--> config.status* -+ +--> make*

Makefile.in ---' `-> Makefile ---'


编辑 configure.in 文件:

configure.in 文件中包含了对 autoconf 宏的调用,这些宏是用来检测软件所必须的各项参数的。为了能够得到 configure.in 文件,需要使用 autoscan。configure.in 文件中,在进行各项检测前,必须在最开始调用 AC_INIT,在最后调用 AC_OUTPUT。另外有些宏由于检测的关系是和在文件中的位置相关的。最好每一个宏占用一行。


使用 autoscan 创建 configure.in 文件

可以将目录做为参数调用 autoscan,如果不使用参数的化,则认为是当前目录。autoscan 将检查指定目录中的源文件,并创建 configure.scan 文件。在将 configure.scan 改名为 configure.in 文件前,需要手工改动它以进行调整。


使用 autoconf 创建 configure 脚本

不带任何参数的运行 autoconf。autoconf 将使用 m4 宏处理器和 autoconf 宏,来处理处理 configure.in 中的宏。


configure.in 中的宏:

AC_INIT(在源代码中唯一的一个文件):configure 将检查该文件是否存在,并检查包含它的目录是否存在。

AC_OUTPUT(文件):指定创建的输出文件。在 configure.in 文件中调用一次。文件名间用空格分开。比如:AC_OUTPUT(Makefile:templates/top.mk lib/Makefile:templates/lib.mk)

在 configure.in 中,有一些被 autoconf 宏预先定义的变量,重要的有如下几个:

bindir:安装可执行文件的目录。

includedir:C 头文件目录。

infodir:info 页安装目录。

mandir:安装手册页的目录。

sbindir:为管理员运行该该程序提供的安装路径。

srcdir:为 Makefile 提供的源代码路径。

top_srcdir:源代码的最上层目录。

LIBS:给连接程序的 -l 选项

LDFLAGS:给连接程序的 stripping(-s)和其他一些选项。

DEFS:给 C 编译器的 -D 选项。

CFLAGS:给 C 编译器的 debug 和优化选项。当调用了 AC_PROG_CC 才有效。

CPPFLAGS:头文件搜索路径(-I)和给 C 预处理器和编译器的其他选项。

CXXFLAGS:给 C++ 编译器的 debug 和优化选项。当调用了 AC_PROG_CXX 才有效。

如果在同一个目录下编译多个程序的话,使用 AC_CONFIG_SUBDIRS 宏,它的语法是:

AC_CONFIG_SUBDIRS(DIR....):

其他重要的宏:

AC_PROG_CC:选择 C 编译器。如果在环境中不设置 CC 的话,则检测 gcc。

AC_PROG_CXX:选择 C++ 编译器。

参考文献:

Autoconf.htm
2.3 使用 automake
一般操作

Automake 工作时,读取一个叫'Makefile.am'的文件,并生成一个'Makefile.in'文件。Makefile.am中定义的宏和目标,会指导 automake生成指定的代码。例如,宏'bin_PROGRAMS'将导致编译和连接的目标被生成。

Makefile.am中包含的目标和定义的宏被拷贝到生成的文件中去,这允许你添加任意代码到生成的Makefile.in文件中去。例如,使一个 Automake发布中包含一个非标准的dvs-dist目标,Automake的维护者用它来从它的源码控制系统制作一个发布。

请注意,GNU生成的扩展名不被Automake所识别,在一个'Makefile.am'中使用这样一个扩展名会导致错误。

Automake试图以一种聪明的方式将相邻的目标(或变量定义)注释重组。

通常,Makefile.am中定义的目标会覆盖任何由automake自动生成的有相似名字的这样的目标。尽管这是种被支持的属性,但最好避免这么做,因为有些时候,生成的规则很严格。

类似的,Makefile.am中定义的变量会覆盖任何由automake自动生成的变量定义。这一特性经常要比目标定义的覆盖能力更常用。请注意,很多 automake生成的变量只用于内部使用,在将来发布时他们的名字可能会变化。

当测试一个变量定义时,Automake降递归的测试在定义中引用的变量。例如,如果Automake看到这段snippet程序中的'foo_SOURCES':

xs = a.c b.c

foo_SOURCES = c.c $(xs)

它将使用文件:'a.c','b.c'和'c.c' 作为foo_SOURCES的内容.

Automake 也允许不被拷贝到输出的注释形式,所有以'##'开头的行将被Automake 完全忽略.


深度

Automake 支持三种目录层次:'flat', 'shallow', 'deep'.

flat: 所有的文件都在一个目录中. 相应的Makefile.am中缺少SUBDIRS宏. termutils 是一个例子.

deep: 所有的资源都在子目录中,指定曾目录主要包含配置信息.GNU cpio 是一个很好的例子.GNU tar.相应的最顶层Makefile.am中将包含一个SUBDIR宏,但没有其他的宏来定义要创建的对象.

shallow: 主资源存在于最顶层目录,而不同的部分(典型的,库函数)在子目录中.Automake 就是这样的一个包.


严格性

当Automake 被GNU包维护者使用时,它的确努力去适应,但不要试图使用所有的GNU惯例.

目前,Automake 支持三种严格性标准:

foreign:Automake 将只检查绝对必须的东西.

gnu:Automake 将尽可能多的检查以适应GNU标准, 这是默认项.

gnits:Automake 将进行检查,以适应“尚未成文”的Gnits标准。 他们基于GNU标准,但更详尽。除非您是Gnits标准的制定者。建议您最好避免这个选项,指导该标准正式发布。


统一命名规范

Automake变量一般遵循一套统一的命名规范以很容易的决定如何创建和安装程序(和其他派生对象)。给规范还支持configure时动态决定创建规则。

在make时,一些变量被用于决定那些对象要被创建。写变量叫做primary variables。例如,PROGRAM变量包括一个要被编译和连接的程序列表。

另一个变量集用于决定被创建变量被安装到哪里。这些变量以相应的主变量命名,但加一个前缀,表示那些标准目录应被用作安装路径。这些标准目录的名称规定在 GNU标准中。Automake用pkglibdir, pkgincludedir 和 pkgdatadir来展开这一列表。他们和没有pkg前缀的版本一样,只不过有‘@PACHAGE@’扩展,PKGLIBDIR被定义为 $(DATADIR)/@PACKAGE@.

对每一个主变量,有一个EXTRA_前缀的变量。这个变量用于列出所有对象,至于哪些变量被创建,哪些变量不被创建则取决于configure。之所以需 要这个变量,是因为Automake必须静态的指导要创建对象的完整列表以便生成一个‘Makefile。in’文件。

例如,cpio 在configure时决定创建那些程序。一些程序被安装在bindir,一些被安装在sbindir:

EXTRA_PROGRAMS = mt rmt

bin_PROGRAMS = cpoi pax

sbin_PRGRAMS = @PROGRAMS@

定义没有前缀的主变量是错误的(如:PROGRAMS)。值得注意的是,“dir”在作为构成变量名时会被忽略。一次,我们写成bin_PROGRAMS 而不是bindir_PROGRAMS.

不是每一种对象都得以安装在每个目录下。Automake 将标记那些他认为是错误的尝试,他也能够诊断一些明显的目录名拼写错误。

有时标准目录--被Automake使用的--不过用。特别的,有时为了清晰,将对象安装在一些预定义的子目录,是很有用的。Automake允许你增加 安装目录。如果以一个变量名(如,zar)加上dir的另一个变量(如,zardir)被定义了,那么他也是合法的。

例如,如果HTML没支持Automake的一下部分,你就可以用他来安装HTML源文件:

htmldir = $(prefix)/html

html_DATA = automake.html

“noinst”前缀专门指定有问题的对象不被安装。

“check”前缀表示有问题的对象知道make check命令被执行猜被创建。

可用的主变量是 'PROGRAMS','LIBRARIES','LISP','SCRIPTS','DATA','HEADERS','MANS'和'TEXINFOS' 导出变量是如何命名的

有时一个Makefile变量名有一些用户支持的文本导出。例如程序名被重写进Makefile宏名称。Automake读取这些文本,所以他不必遵循命 名规则。当生成后引用时名称中的字符除了字母,数字,下划线夺回被转换为下划线。例如,如果你的程序里有sniff-glue,则导出变量名将会是 sniff_glue_SOURCES,而不是sniff-glue_SOURCES.


一些例子


一个完整简单的例子

假设你写了一个名为zardoz的程序。

第一步,更新你的configure.in文件以包含automake所需的命令。最简单的办法就是在AC_INIT后加一个AM_INIT_AUTOMAKE调用:

AM_INIT_AUTOMAKE(zardoz, 1.0)

如果你的程序没有任何复杂的因素。这是最简单的办法。

现在,你必须重建‘configure’文件。这样做,你必须告诉autoconf如何找到你所用的新宏。最简单的方式是使用aclocal程序来 生成你的‘aclocal.m4’.aclocal让你将你的宏加进‘acincluide.m4’,所以你只需重命名并运行他


mv aclocal.m4 acinclude.m4

aclocal

autoconf


现在是为你的zardoz写Makefile.am的时候了。zardoz是一个用户程序,所以逆向将他安装在其他用户程序安装的目录。zardoz还有 一些Texinfo文档。你的configure.in脚本使用AC_REPLACE_FUNCS,所以你需要链接‘@LIBOBJS@’

bin_PROGRAMS = zardoz

zardoz_SOURCES = main.c head.c float.c vortex9.c gun.c

zardoz_LDADD = @LIBOBJS@


info_TEXINFOS = zardoz.texi


现在你可以运行Automake以生成你的Makefile.in文件。

一个经典的程序

hello 以其简单和多面幸而闻名。着一段将显示在Hello包Automake如何被使用。

下面是

dnl用 autoconf 处理它以产生一个 configure 脚本.

AC_INIT(src/hello.c)

AM_INIT_AUTOMAKE(hello, 1.3.11)

AM_CONFIG_HEADER(config.h)


dnl Set of available languages

ALL_LINGUAS="de fr es ko nl no pl pt sl sv"

dnl Checks for programs.

AC_PROG_CC

AC_ISC_POSIX

dnl Checks for libraries.

dnl Checks for header files.

AC_STDC_HEADERS

AC_HAVE_HEADERS(string.h fcntl.h sys/file.h sys/param.h)

dnl Checks for library functions.

AC_FUNC_ALLOCA

dnl Check for st_blksize in struct stat

AC_ST_BLKSIZE

dnl internationalization macros

AM_GNU_GETTEXT

AC_OUTPUT([Makefile doc/Makefile intl/Makefile po/Makefile.in \ src/Makefile tests/Makefile tests/hello], [chmod +x tests/hello])

'AM_'宏由Automake(或Gettext 库)提供;其余的是Autoconf标准宏。

top-level ‘Makefile.am’:

EXTRA_DIST = BUGS ChangeLog.O

SUBDIRS = doc intl po src tests

--如你所见,这里所有的工作时在子目录中真正完成的.

--'po' 和 'intl'目录是用 gettextize自动生成的,这里不做进一步讨论.

在'doc/Makefile.am'文件中我们看到:

info_TEXINFOS = hello.texi

hello_TEXINFOS = gpl.texi

--这已足以创建,安装和发布手册.


这里是'tests/Makefile.am'文件:

TESTS = hello

EXTRA_DIST = hello.in testdata

--脚本'hello'被configure创建,并且是唯一的测试.make check将运行它.

最后是,'src/Makefile.am',所有的真正的工作是在这里完成的:

bin_PROGRAMS = hello

hello_SOURCES = hello.c version.c getopt.c getopt1.c getopt.h system.h

hello_LDADD = @INTLLIBS@ @ALLOCA@

localedir = $(datadir)/locale

INCLUDES = -I../intl -DLOCALEDIR=\"$(localedir)\"


创建一个'Makefile.in'文件

要为一个包创建所有的'Makefile.in',在顶层目录运行automake不带参数的程序.automake将自动查找每个适当的 'Makefile.am'文件,并声称相应的'Makefile.in'文件.请注意,automake会简要的察看一下包的构成;它假定一个包在顶层 只有一个'configure.in'文件.如果你的包有多个'configure.in'文件,那么你必须在每一个含有'configure.in'文 件的目录运行一次'automake'程序.

你可以随意送给automake一个参数;'.am'会加到变量名的后面,并且作为输入文件的文件名.这一特性通常被用来自动重建一个'过时的 ''Makefile.in'文件.注意,automake必须在最顶级的目录运行,及时仅仅为了重新生成某个子目录中的'Makefile.in'文 件.

automake接受以下可选参数:

-a

--add-missing

在某些特殊情况下,Automake需要某些公共文件存在.例如,如果'configure.in'运行 AC_CANONICAL_HOST时,就需要'config.guess'.Automake需要几个这样的文件,这一选项会让automake自动的 将一些缺失文件加进包中(如果可能的话).一般的,如果Automake告诉你某个文件不存在,你可以试一试这个选项.

--amdir = dir

在dir所制定的目录而不是安装目录下寻找Automake的数据文件,经常用于调试.

--build-dir = dir

告诉Automake创建目录在哪里.这一选项只用于将'dependencies'加进一个由make dist生成的'Makefile.in'文件中.

--cygnus

将使得所生成的'Makefile.in'文件遵循Cygnus规则,而不是GNU或Gnits规则.

--foreign

设置全局严格性为'gnits'.

--gnu

设置全局严格性为'gnu'.

--help

帮助

-i

--include-deps

将所有自动创建时的依赖信息包括在'Makefile.in'中.通常在制作发行时用到.

--generate-deps

创建一个'.dep_segment'文件,它合并了所有的自动生成时的依赖信息,通常用于制作发行.这在维护'SMakefile'或其他平台上的 'Makefile'(如Makefile.Dos)时非常有用.它只能与--include-deps, --srcdir-name和 --build-dir 联合使用.注意这一选项不作其他任何处理.

--no-force

一般的,automake创建在'configure.in'中提及的所有'Makefile.in'文件.这一选项将导致,automake只更新那些对于与自身相关的东西过了时的'Makefile.in'文件.

-o dir

--output-dir = dir

将生成的'Makefile.in'放在指定的dir目录中.通常,'Makefile.in'放在与之相对应的'Makefile.am'所在目录中的.在制作发布时使用.

--srcdir-name = dir

告诉Automake与当前创建动作相关的源文件目录的名字.个选项仅被用于将自动创建时的依赖信息包括在'Makefile.in'中.

-v

--verbose

时Automake打印出正在读取或创建的文件的信息.

--version

打印Automake的版本号.


扫描'configure.in'文件

Automake扫描包的'configure.in'文件来决定某些关于包的信息.'configure.in'中必须定义一些变量并且需要一些 autoconf 宏.Automake也会利用'configure.in'文件的信息来定制它的输出.

Automake还支持一些autoconf宏以使维护更为简单.


配置需求

满足Automake基本要求的最简单方法是使用'AM_INIT_AUTOMAKE'宏.但如果你愿意,你可以手工来做.


自动生成 aclocale.m4

Automake包括很多Autoconf宏,其中有些在某些情况下游Automake使用,这些宏必须被定义在你的'aclocal.m4'中.否则,autoconf将看不到他们.

acloal程序将根据'configure.in'自动生成'aclocal.m4'.这提供了一个方便的途径来得到Automake提供的宏.


Automake所支持的Autoconf宏

AM_CONFIG_HEADER

AM_CYGWIN32

AM_FUNC_STRTOD

AM_FUNC_ERROR_AT_LINE

AM_FUNC_MKTIME

AM_FUNC_OBSTACK

AM_C_PROTOTYPES

AM_HEADER_TOCGWINSZ_NEEDS_SYS_IOCTL

AM_INIT_AUTOMAKE

AM_PATH_LISPDIR

AM_PROG_CC_STDC

AM_PROG_INTALL

AM_PROG_LEX

AM_SANITY_CHECK

AM_SYS_POSIX_TERMIOS

AM_TYPE_PTRDIFF_T

AM_WITH_DMALLOC

AM_WITH_REGEX


编写你自己的aclocal宏

Aclocal并没有特定的宏机制,因此你可以用你自己的宏来扩展它.这以特性经常被用以制作做那些想让自己的Autoconf宏被其他应用程序使用的 库.例如,gettext库支持一个AM_GNU_GETTEXT宏,它可以被任何使用gettext库的包所使用.当该库被安装后,它会安装这个宏,这 样,aclocal就能够找到他了.

宏的名称应以'.m4'结尾,这样的文件将被安装在'$(datadir)/aclocal'中.


最顶层'Makefile.am'

在non-flat包中,顶层'Makefile.am'必须告诉Automake哪些子目录将被创建.这是通过SUBDIRS变量定义的.

SUBDIRS宏包含一个子目录列表,以规定各种创建的过程.'Makefile'中很多目标(如,all)将不止运行所在目录中,还要运行于所有指定的 子目录中.注意,在SUBDIRS中列出的目录并不需要包含'Makefile.am'文件而只需要'Makefile'文件.这将允许包含位于不使用 Automake的包(如,gettext)中的库.另外,SUBDIRS之中的目录必须是当前目录的直接子目录.比如,你不能在SUBDIRS中指定 'src/subdir'.

在deep型的包中,顶层'Makefile.am'文件通常非常短.例如,下面是HELLO发布中的'Makefile.am':


EXTRA_DIST = BUGS ChangeLog.O README-alpha

SUBDIRS = doc intl po src tests


如果你只想创建一个包的子集,你可以覆盖SUBDIRS(就如同GNU inetutils的情形)在你的'Makefile.am'中包括:

SUBDIRS = @SUBDIRS@

让后在你的'configure.in'中你可以指定:


SUBDIRS = "src doc lib po"

AC_SUBST(SUBDIRS)


这样做的结果是automake被欺骗了,它会在指定目录创建包,但并不真正绑定那个列表直到运行configure.


SUBDIRS可以包含配置的替代(如,'@DIRS@');Automake 自己并不真正检测这一变量的内容.

如果 SUBDIRS 被定义了,那么你的'configure.in' 就必须包含AC_PROG_MAKE_SET.

SUBDIRS 的使用并不局限于顶层'Makefile.am'.Automake 可被用来构造任意深度的包.


参考文献

Automake.htm

[目录]


diff

diff

  diff是生成源码补丁的必备工具。其命令格式为:

 diff [命令行选项] 原始文件 新文件

常用命令行选项如下:

   -r 递归处理目录     -u 输出统一格式(unified format)

   -N patch里包含新文件   -a patch里可以包含二进制文件

  它的输出在stdout上,所以你可能需要把它重定向到一个文件。diff的输出有“传统格式”和“统一格式”之分,现在大都使用统一格式:

  传统格式示例:

   [hahalee@builder]$ diff a.txt b.txt

   1a2

   > here we insert a new line

   3d3

   <>

  统一格式示例:

   [hahalee@builder]$ diff -u a.txt b.txt

   --- a.txt Thu Apr 6 15:58:34 2000

   +++ b.txt Thu Apr 6 15:57:53 2000

   @@ -1,3 +1,3 @@

   This is line one

   +here we insert a new line

   and this is line two

   -why not this third line?

  通过比较可以看出,传统格式的patch文件比较小,除了要删除/插入的行外没有冗余信息。统一格式则保存了上下文(缺省是上下各三行,最少需要两 行),这样,patch的时候可以允许行号不精确匹配的情况出现。另外,在patch文件的开头明确地用---和+++标示出原始文件和当前文件,也方便 阅读。要选用统一格式,用 u 开关。

  通常,我们需要对整个软件包做修改,并生成一个patch文件,下面是典型的操作过程。这里就要用到前面介绍的几个命令行开关了:

  tar xzvf software.tar.gz # 展开原始软件包,其目录为software

  cp _a software software-orig # 做个修改前的备份

  cd software

  [修改,测试……]

  cd ..

  diff _ruNa software-orig software > software-my.patch

  现在我们就可以保存software-my.patch做为这次修改的结果,至于原始软件包,可以不必保存。等到下次需要再修改的时候,可以用 patch命令把这个补丁打进原始包,再继续工作。比如是在linux kernel 上做的工作,就不必每次保存几十兆修改后的源码了。这是好处之一,好处之二是维护方便,由于unified patch格式有一定的模糊匹配能力,能减少原软件包升级带来的维护工作量(见后)

patch

  patch命令跟diff配合使用,把生成的补丁应用到现有代码上。常用命令行选项:

  patch [命令行选项] [待patch的文件[patch]]

  -pn patch level(n是数字) -b[后缀] 生成备份,缺省是.orig

为了说明什么是patch level,这里看一个patch文件的头标记。

  diff -ruNa xc.orig/config/cf/Imake.cf xc.bsd/config/cf/Imake.cf

  --- xc.orig/config/cf/Imake.cf Fri Jul 30 12:45:47 1999

  +++ xc.new/config/cf/Imake.cf Fri Jan 21 13:48:44 2000

  这个patch如果直接应用,它会去找xc.orig/config/cf目录下的Imake.cf文件,假如你的源码树的根目录是缺省的xc而不是 xc.orig,除了mv xc xc.orig之外,有无简单的方法应用此patch呢?patch level就是为此而设:patch会把目标路径名砍去开头patch level个节(由/分开的部分)。在本例中,可以用下述命令:cd xc; patch _p1 < /pathname/xxx.patch 完成操作。注意,由于没有指定patch文件,patch程序默认从stdin读入,所以用了输入重定向。

  如果patch成功,缺省是不建备份文件的(注:FreeBSD下的patch工具缺省是保存备份),如果你需要,可以加上 b 开关。这样把修改前的文件以“原文件名.orig”的名字做备份。如果你喜欢其它后缀名,也可以用“b 后缀”来指定。

  如果patch失败,patch会把成功的patch行给patch上,同时(无条件)生成备份文件和一个.rej文件。.rej文件里是没有成功提交的patch行,需要手工patch上去。这种情况在原码升级的时候有可能会发生。

  关于二进制文件的说明:binary文件可以原始方式存入patch文件。diff可以生成(加-a选项),patch也可以识别。如果觉得这样的patch文件太难看,解决方法之一是用uuencode处理该binary文件。

[目录]


rcs

  单个文件的版本控制/管理,适合对少量文件进行版本控制,不适合小组进行项目协作开发。优点:使用简便;缺点:功能有限。RCS常用命令有ci/co/rcsdiff。

  rcs用一个后缀为“,v”的文件保存一文件的内容和所有修改的历史信息,你可以随时取出任意一个版本,用rcs保存程序就不必为不同版本分别备份。下面是一个“,v”文件的例子:

 (太长,忽略。请看演示或自己找一个样本)

  rcs文件里记载了每次更新的日期、作者、还有更新说明(Log)。文件的最新版本内容放在Log之后,再后面是历次版本与其后一版本的差别,按 check in的时间做倒序排列。这么做的原因是因为新版本的check out机会大些,倒序排列可优化check out时间。

   ci _ check in,保存新版本

  此命令把指定文件添加到rcs历史文件中,同时把该文件删除。如果当前目录下有个RCS目录,ci会把历史文件存在此处,否则放在当前目录下。

   [hahalee@builder]$ mkdir RCS

   [hahalee@builder]$ ci wood.txt

   RCS/wood.txt,v <-- wood.txt

   enter description, terminated with single '.' or end of file:

   NOTE: This is NOT the log message!

   >> initial checkin #NOTE: 这里是给本次checkin做的说明

   >> .

   initial revision: 1.1

   done

   [hahalee@builder]$ ls -l RCS/

   总共 4

   -r--r--r-- 1 hahalee hahalee 451 Apr 7 07:27 wood.txt,v

  ci也有丰富的命令行选项,比如,可以指定check in的版本号,甚至可以用字符串来做版本号,请查阅ci的manpage。

   co _ check out,取出当前(或任意)版本

常用命令行选项:

  -r[rev] 指定版本的checkout -l[rev] 指定版本,加锁的checkout

  如不加可选的版本号,缺省是最近的版本。如果只需要一份只读的拷贝,用-r(特殊情况,如需要一份只读的当前拷贝,可以不要任何选项)。如需要对checkout的文件进行修改,请用-l选项。常见的操作流程是:

   ci xxx.c; co _l xxx.c; 编辑,测试; ci xxx.c …………

  在每次checkin的时候,版本号会自动在最低位上加1。为了在文件中反映版本控制信息,rcs提供了几个特殊的关键字,这里介绍$Id$和$Log$,其它的请参考info cvs。

  $Id$代表文件的版本标识,由文件名/版本号/最后一次checkin时间/最后一次checkin的用户这几项组成,比如:

$Id: wood.txt,v 1.3 2000/04/07 00:06:52 hahalee Exp $

如果需要更详细的信息,可以用$Log$,$Log$被扩展为该文件的所有修改日期和备注,例:

    /* $Log: wood.txt,v $

    * Revision 1.2 2000/04/07 00:29:12 hahalee

    * This is my second checkin

    *

    * Revision 1.1 2000/04/07 00:28:39 hahalee

    * Initial revision

    * /

  顺便介绍一下ident命令。它的功能比较简单,就是从文件中搜索出RCS标示符并打印出来。可以用ident /usr/sbin/sendmail 来看看。不用说,如果想在最终的binary文件里找到$Id$串,得要把它声明到一个字符串里去。很多程序里这么写:

    #ifndef lint //这里是为了避免lint报告“变量未使用”

    static const char rcsid[] =

   "$Id: bin/sh.c,v 1.15 1999/08/27 23:13:43 wp Exp $"; //这是从 $Id$ 扩展                                                                                                                                                    //出来的

    #endif

   rcsdiff _ 比较revision之间的差异.运行diff命令,生成patch文件

命令行格式:rcsdiff [选项] [-r版本[-r版本]] [diff选项] 文件名

  说明:如果没给出版本号,把上次checkin的内容同当前工作文件比较;如给出了一个版本号,就把那个版本的内容同当前工作文件比较;若给出了两个 版本号,则用第一个来跟第二个比较。由于rcsdiff调用diff命令,所有的diff选项都可用。它的输出也是加了额外信息的diff格式内容,可以 给patch使用。

  rcs里面还有rcs,rcsclean,rlog,merge,rcsmerge我们没有提到,有的特别简单有的特别繁琐且用得少。其中rcs命令可以用来对rcs文件进行各种精细的维护,最为复杂。


[目录]


内核重编译常见故障

该程序只是简单地录一小段纯音频数据存储在test.wav中,用命令cat test.wav >/dev/audio就可以播放出来。因为程序要读写声卡的设备文件,所以你必须用相应的权限,或者你是root

#include
#include
#include
#include
#include

main()
{
int id,fd,i,j;
char testbuf[4096];
id=open("/dev/audio",O_RDWR);
//这里使用声卡驱动程序,linux里当作一个文件读写
fd=open("test.wav",O_RDWR);
//一个文件

if(id<0||fd<0){
printf("error in open the device!\n");
printf("id %d\t dsp%d\t seq%d\n",id,dsp,seq);
exit(-1);
}

post=0;
ioctl(id,SNDCTL_DSP_RESET,(char *)&i);
ioctl(id,SNDCTL_DSP_SYNC,(char *)&i);
i=1;
ioctl(id,SNDCTL_DSP_NONBLOCK,(char *)&i);
i=8000;
ioctl(id,SNDCTL_DSP_SPEED,(char *)&i);
i=1;
ioctl(id,SNDCTL_DSP_CHANNELS,(char *)&i);
i=8;
ioctl(id,SNDCTL_DSP_SETFMT,(char *)&i);
i=3;
ioctl(id,SNDCTL_DSP_SETTRIGGER,(char *)&i);
i=3;
ioctl(id,SNDCTL_DSP_SETFRAGMENT,(char *)&i);
i=1;
ioctl(id,SNDCTL_DSP_PROFILE,(char *)&i);

for(j=0;j<10;){
i=read(id,testbuf,4096);
if(i>0){
write(fd,filebuf,i);
j++;
}
}

close(fd);
close(id);

}

[目录]


cvs

  CVS,顾名思义,是个可以用在小组协作环境下的源码版本管理系统。同类的软件有AT&T的SCCS(Source Code Control System),还有PVCS等。在OpenSource项目里CVS用得最为广泛, Linux kernel不使用CVS来维护,下面我们将会参考FreeBSD的源码管理来做示例。CVS是个相当复杂的系统,FreeBSD甚至设有专门的“CVS 管理员”(CVS “Maester”)来全面负责项目的CVS repository维护。

  下面介绍与CVS相关的若干概念和术语:

    Repository:   “源码仓库”,CVS存放的项目源码历史档案

    CVSROOT:     该环境变量指明CVS Repository存放的目录

    Module:      模块。就是CVSROOT下的顶级目录名

    Vendor Branch:  分支。在一套Repository里可以存放多个代码分支的历史

    Release Tag:   发行标记。对于每一个版本,可以用符号来做标记

  下面是一个CVS repository的版本衍生图,大致描绘了FreeBSD的版本发行情况。图中的RELENG_3和RELENG_4表示“Release Engine”,也就是Vendor Branch,每个Branch分头发展,等某个Branch的开发到了一定的质量水准,就做个Release Tag。比如最近的4.0-RELEASE的Release Tag是REL_4_0。

  这些不同的Branch都存放在同一个Repository Tree里。

  CVS是个很复杂的系统,可以参考下面两个URL获得进一步的信息:

http://www.cyclic.com

http://www.loria.fr/~molli/cvs-index.html

(在cvs软件包里含有详细的文档,应当查阅info版本。几个ps文件都太老了)

下面介绍CVS的基本用法。

① Import 导入/创建CVS Repository Tree

  首先建一个目录作为你的CVSROOT,然后用cvs init命令对其初始化(建立一系列log,config文件)。然后到工作目录下使用cvs import命令:

   [hahalee@builder]$ mkdir /home/hahalee/CVS

   [hahalee@builder]$ export CVSROOT=/home/hahalee/CVS

   [hahalee@builder]$ cvs init

   [hahalee@builder]$ cvs import _b 0.5.0 hftpd RELENG_0 REL_0

   N hftpd/tar.h

   N hftpd/auth.h

   [blah...blah...blah...]

   N hftpd/docs/rfcs/rfc0959.txt

   N hftpd/docs/rfcs/rfc2428.txt

   No conflicts created by this import

  上述操作在$CVSROOT下生成hftpd目录,可以看到里面都是后缀为“,v”的文件,这就是import进来的Repository。 RELENG_0是vendor-tag,REL_0是release-tag。vendor-tag就是vendor branch tag,可以理解为”code name”。

② Checkout 创建私有工作目录/Export

  换一个空目录,运行 cvs checkout modules_name 即可:

   [hahalee@builder]$ cvs checkout hftpd# hftpd是我们的module name

   cvs checkout: Updating hftpd

   U hftpd/AUTHORS

   U hftpd/COPYING

   [blah blah blah] # 省略许多

   [hahalee@builder t]$ ls -l

   总共 0

   drwxrwxr-x 5 hahalee hahalee 1253 Apr 7 20:08 hftpd

   [hahalee@builder t]$ find ./ -type d

  从最后一条命令的输出可看到,checkout的工作目录里多了CVS目录。里面记载了CVS相关的信息,可以方便后续的cvs操作。如果纯粹是为了拷贝出最新的source tree,可以用export,此时不会建立CVS目录。

③ Update 更新

  当你完成某一部分代码的时候,先不忙提交,可以把别人可能做了的其他修改update过来然后统一编译调试无误后再提交,这是team work的准则。在checkout出来的工作目录下(不管什么子目录),直接cvsup update 就可以了,当然你要先把CVSROOT环境变量设置好。

④ Commit 提交

  很简单,cvs commit。但你必须要在checkout出来的工作目录里提交才行:

   [hahalee@builder]$ cvs commit

   cvs commit: Examining .

   cvs commit: Examining docs

   cvs commit: Examining docs/man

   cvs commit: Examining docs/rfcs

   cvs commit: Examining tools

   Checking in AUTHORS;

   /home/hahalee/CVS/hftpd/AUTHORS,v <-- AUTHORS

   new revision: 0.6; previous revision: 0.5

   done

  关于并发提交冲突:任何用户可以随意checkout他们自己的工作拷贝,commit也是不受限制的。这样,当用户a和b分别checkout了1.2版的c.c,然后各自对c.c做了修改,a提交了他的修改,然后,当b提交的时候,冲突就产生了。

  这时候,cvs会做以下动作:

告诉用户b,对c.c的提交发生冲突
对用户b当前的c.c做备份文件.#c.c.1.2
试图合并a和b的修改,生成新的c.c
然后,用户b应当修改c.c,去掉/合并冲突的行,并以版本1.4提交。

⑤ Diff

  可以用类似rcsdiff的方法用cvs生成patch,命令行语法也类似

   [hahalee@builder]$ cvs diff -u -r0.5 AUTHORS

   Index: AUTHORS

   ===================================================================

   RCS file: /home/hahalee/CVS/hftpd/AUTHORS,v

   retrieving revision 0.5

   retrieving revision 0.6

   diff -u -r0.5 -r0.6

   --- AUTHORS 2000/04/07 10:46:02 0.5

   +++ AUTHORS 2000/04/07 14:05:57 0.6

   @@ -1,3 +1,4 @@

   +ah! let me in!

   So then, who can't spell

   Develloppopotamus?

   Quite a lot of us.

  还有一个rdiff,用来生成两个不同的release 之间的patch。

⑥ 其他操作

   cvs 的其他操作还包括有:

   admin 管理功能

   tag 对某一版本做符号标记

   release 取消checkout,删除工作目录(release在这里是“释放”的意思)

   add,remove 往repository里添加/删除文件

   history 查看repository操作历史记录

⑦ CVS 的多平台特性以及C/S扩展

  cvs是多平台的,开发可以在多种平台比如,可以把linux上的CVS Repository通过samba export出来在Windows平台上做开发。现在很多软件包里包含有*NIX/Windows/MacOS等多平台支持代码,cvs的跨平台特性可提 供最好的多平台开发支持。

  不过,cvs的操作是直接基于文件系统的,在需要大量远程协作的场合问题很多,远程的NFS mount效率太差,也会有安全问题。新版本的cvs自身内建了Client/Server支持,也可以利用Unix上传统的远程交互手段来通讯。

  1,通过rsh(也可用ssh替换)

  2,使用cvs自带的C/S用户认证:pserver(缺省端口2401)

  3,使用kerberos的gserver、kserver


[目录]


共享库工具

strip:
nm:
size:
string:

1 创建和使用静态库

创建一个静态库是相当简单的。通常使用 ar 程序把一些目标文件(.o)组合在一起,
成为一个单独的库,然后运行 ranlib,以给库加入一些索引信息。

2 创建和使用共享库
特殊的编译和连接选项
-D_REENTRANT 使得预处理器符号 _REENTRANT 被定义,这个符号激活一些宏特性。
-fPIC 选项产生位置独立的代码。由于库是在运行的时候被调入,因此这个
选项是必需的,因为在编译的时候,装入内存的地址还不知道。如果
不使用这个选项,库文件可能不会正确运行。
-shared 选项告诉编译器产生共享库代码。
-Wl,-soname -Wl 告诉编译器将后面的参数传递到连接器。而 -soname 指定了
共享库的 soname。


# 可以把库文件拷贝到 /etc/ld.so.conf 中列举出的任何目录中,并以
root 身份运行 ldconfig;或者
# 运行 export LD_LIBRARY_PATH='pwd',它把当前路径加到库搜索路径中去。


3 使用高级共享库特性
1> ldd 工具
ldd 用来显示执行文件需要哪些共享库, 共享库装载管理器在哪里找到了需要的共享库.

2> soname

共享库的一个非常重要的,也是非常难的概念是 soname--简写共享目标名(short for shared object name)。这是一个为共享库(.so)文件而内嵌在控制数据中的名字。如前面提到的,每一个程序都有一个需要使用的库的清单。这个清单的内容是一系列库 的 soname,如同 ldd 显示的那样,共享库装载器必须找到这个清单。

soname 的关键功能是它提供了兼容性的标准。当要升级系统中的一个库时,并且新库的 soname 和老的库的 soname 一样,用旧库连接生成的程序,使用新的库依然能正常运行。这个特性使得在 Linux 下,升级使用共享库的程序和定位错误变得十分容易。

在 Linux 中,应用程序通过使用 soname,来指定所希望库的版本。库作者也可以通过保留或者改变 soname 来声明,哪些版本是相互兼容的,这使得程序员摆脱了共享库版本冲突问题的困扰。

查看/usr/local/lib 目录,分析 MiniGUI 的共享库文件之间的关系

3> 共享库装载器

当程序被调用的时候,Linux 共享库装载器(也被称为动态连接器)也自动被调用。它的作用是保证程序所需要的所有适当版本的库都被调入内存。共享库装载器名字是 ld.so 或者是 ld-linux.so,这取决于 Linux libc 的版本,它必须使用一点外部交互,才能完成自己的工作。然而它接受在环境变量和配置文件中的配置信息。

文件 /etc/ld.so.conf 定义了标准系统库的路径。共享库装载器把它作为搜索路径。为了改变这个设置,必须以 root 身份运行 ldconfig 工具。这将更新 /etc/ls.so.cache 文件,这个文件其实是装载器内部使用的文件之一。

可以使用许多环境变量控制共享库装载器的操作(表1-4+)。

表 1-4+ 共享库装载器环境变量
变量 含义
LD_AOUT_LIBRARY_PATH 除了不使用 a.out 二进制格式外,与 LD_LIBRARY_PATH 相同。
LD_AOUT_PRELOAD 除了不使用 a.out 二进制格式外,与 LD_PRELOAD 相同。
LD_KEEPDIR 只适用于 a.out 库;忽略由它们指定的目录。
LD_LIBRARY_PATH 将其他目录加入库搜索路径。它的内容应该是由冒号
分隔的目录列表,与可执行文件的 PATH 变量具有相同的格式。
如果调用设置用户 ID 或者进程 ID 的程序,该变量被忽略。
LD_NOWARN 只适用于 a.out 库;当改变版本号是,发出警告信息。
LD_PRELOAD 首先装入用户定义的库,使得它们有机会覆盖或者重新定义标准库。
使用空格分开多个入口。对于设置用户 ID 或者进程 ID 的程序,
只有被标记过的库才被首先装入。在 /etc/ld.so.perload 中指定
了全局版本号,该文件不遵守这个限制。


4> 使用 dlopen

另外一个强大的库函数是 dlopen()。该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号,这些符号在编译的时候是不知道的。比如 Apache Web 服务器利用这个函数在运行过程中加载模块,这为它提供了额外的能力。一个配置文件控制了加载模块的过程。这种机制使得在系统中添加或者删除一个模块时,都 不需要重新编译了。

可以在自己的程序中使用 dlopen()。dlopen() 在 dlfcn.h 中定义,并在 dl 库中实现。它需要两个参数:一个文件名和一个标志。文件名可以是我们学习过的库中的 soname。标志指明是否立刻计算库的依赖性。如果设置为 RTLD_NOW 的话,则立刻计算;如果设置的是 RTLD_LAZY,则在需要的时候才计算。另外,可以指定 RTLD_GLOBAL,它使得那些在以后才加载的库可以获得其中的符号。

当库被装入后,可以把 dlopen() 返回的句柄作为给 dlsym() 的第一个参数,以获得符号在库中的地址。使用这个地址,就可以获得库中特定函数的指针,并且调用装载库中的相应函数。


[目录]


代码优化

Linux 是一个多用户系统,因此对用户的管理是系统管理的基本组成部分。安装 Linux 的用户很可能就是该系统的管理员,也就是权限最高的 root。通过对用户的管理,分清了用户之间的责、权、利,保证了系统安全。

对软件的评价:代码的稳定性、友好性、代码的易读性、统一的风格、技巧。

1。尽量少的使用全局变量

2。局部变量一定要初始化,特别是指针变量

3。成员函数功能单一,不要过分追求技巧,函数体不要过长。

{i = i + j; {int k = i;

j = i - j; i=j;

i = i = j; j=k;

} }

4。最好有头文件

5。关于变量名的长短问题

6。设计函数时考虑到通用性

7。申请内存时,一定先要释放。注意 if 问题。

8。对浮点数比较大小时注意不要使用 ==

9。最好不要用 goto 语句

10。所有成员函数要单出口单入口

11。函数中,要先检验参数的合法性

12。最好所有的函数都有返回值,表明错误的原因。

13。注释问题

14。类型转化一律用显示转换。

15。定义宏说,参数使用括号,结果也应该括起来

#define SUB(a,b) ((a)-(b))

3*SUB(3,4-5);

16。变量长度一定要用 sizeof 来求

17。malloc 后千万别忘 free 及使指针等于 NULL。

18。字符串拷贝时尽量少使用 sprintf,而使用 memcpy,最后别忘加上'\0'

19。慎重选择编译时的优化选项。

20。小组开发时,注意代码风格的统一。

[目录]


GNU 编码标准

GNU 编码标准(Coding Standards)

原文在

http://gnu.clinux.org/prep/standards.html
看过之后随手记录了一点儿

Linux 命令行参数处理,
getopt();
getopt_long();
getopt_long_only();

在调用过程中多使用高层的接口;
eg. readdir;

signal handling facilities 信号处理:
1. BSD: signal the Best
#include
2. POSIX: sigaction
3. USG: signal

使用临时文件,请检查环境变量TMPDIR
使用由它指定的目录

编码格式:
or, if you want to use ANSI C, format the definition like this:

static char *
concat (char *s1, char *s2)
{
...
}

In ANSI C, if the arguments don't fit nicely on one line, split it
like this:

int
lots_of_args (int an_integer, long a_long, short a_short,
double a_double, float a_float)
...

Try to avoid having two operators of different precedence at the same
level of indentation. For example, don't write this:

mode = (inmode[j] == VOIDmode
|| GET_MODE_SIZE (outmode[j]) > GET_MODE_SIZE (inmode[j])
? outmode[j] : inmode[j]);

Instead, use extra parentheses so that the indentation shows the
nesting:

mode = ((inmode[j] == VOIDmode
|| (GET_MODE_SIZE (outmode[j]) > GET_MODE_SIZE (inmode[j])))
? outmode[j] : inmode[j]);

Insert extra parentheses so that Emacs will indent the code properly.
For example, the following indentation looks nice if you do it by hand,


v = rup->ru_utime.tv_sec*1000 + rup->ru_utime.tv_usec/1000
+ rup->ru_stime.tv_sec*1000 + rup->ru_stime.tv_usec/1000;

but Emacs would alter it. Adding a set of parentheses produces something
that looks equally nice, and which Emacs will preserve:

v = (rup->ru_utime.tv_sec*1000 + rup->ru_utime.tv_usec/1000
+ rup->ru_stime.tv_sec*1000 + rup->ru_stime.tv_usec/1000);

Format do-while statements like this:

do
{
a = foo (a);
}
while (a > 0);

清洗的使用C语言的构造:
1.不要省略int类型的声明;
2.-Wall
3.不要在函数内部进行extern声明;
4.在函数中使用另外的形参命名方式;
5.不要在局部变量和参数中映射全局变量;

变量和函数的命名方法:
1.在定义全局变量和函数的时候,不要使用过于简单的命名方法,要通过名字反映它
们的用途;
2.不要过分使用缩写;

3.使用下划线来分割名字中的单词;
4.使用枚举类型来定义constant int,而不要用#define

不同系统间的可移植性:
1.使用Autoconf来进行配置;
2.define the "feature test macro" _GNU_SOURCE when compiling your C
files.

调用系统函数:
1.不要使用sprintf的返回值;
2.vfprintf不是都提供的;
3.main要返回int;
4.不要明确的声明系统函数;
5.如果必须定义系统函数的话,不要指明参数类型;
6.对于处理string的函数需要特别对待;

i18n,国际化:
1.要在每一个程序中使用gettext库;
eg. printf (gettext ("Processing file `%s'..."));

程序的文档化:

发布过程:


Makefile约定:

[目录]


书籍

"Beginning Linux Programming"

published by Wrox Press
author: Neil Matthew and Richard Stones

"LINUX kernel Internals"

published by Addison Wesley

"Advanced Programming Language in the UNIX Environment"

published by Addison Wesley
ISBN 0-201-56317-7

author: W. Richard Stevens

"UNIX Network Programming"

published by Prentice Hall
ISBN 981-3026-53-7
author: W. Richard Stevens

"The UNIX C Shell field guide"

published by Prentice-Hall
ISBN 0-13-937468-X 025

[目录]