1. 生火上路(Preliminaries)!
1.1. ELF vs. a.out
目前,Linux的发展正波涛汹涌的进行著.简单一点讲,Linux有两种执行档的格式(formats)可用,取决於你的系统是怎麽整合起来的;你可能两种都有.读了这份文件之後,你就会知道是那一种了.
那,要怎麽区别呢?执行公用程式(utility)'file' (例如,file /bin/bash)就对了.就ELF格式的程式码来讲,显示出来的讯息会含有ELF的字眼;假如是a.out格式的,讯息内就会箝有 Linux/i386的字样了.
ELF与a.out格式的差异之处,会在後续的章节中讨论(很广泛喔).ELF是比较新的格式,一般而言,接受的程度较佳.
1.2. 作者的私语(Administrata)
版权说明(copyright information)与合法的行迳规定(legalese),就摆在这份文件的尾端.除此之外,我......,我还有一些不得不提醒你的话要讲:就算你□著没事干,也不要在Usenet上丢一些呆瓜问的问题;还有啊,不要老以为自己C的功力深厚,专门发表一些不是bugs的bugs出来丢人现眼, 告诉别人你不学无术.最後;嚼口香糖的时候,不妨挖挖你的鼻孔(,and picking your nose while chewing gum)! [译者注:不知道这是那一国的幽默? eh? :-)另一种可能是原文有缺漏字汇, 像是"and not picking your nose while chewing gum."]
1.3. 印刷与排版(typography)
假如你现在读的是Postscript,dvi或者是Html格式的话,那麽你所看到的字型变化就会比只读纯文字格式的人多一些.非凡的是,档案名称(filenames),命令(commands),命令的输出(command output)与摘录出来的原始码(source code)等,统统都是打字机的字型样式(form).这样做的话,对於某些需要强调的变数(variables)以及没有特定结果的□例(random things)而言,就可以达到强调的效果了.
读这份文件的同时,你也会得到一个有用的(usable)索引(index).假若是dvi, postscript之类的版本,索引的数字就是章节(section)的编号;假如是HTML的话,这些数字会按顺序排列,你可以用滑鼠左键来连结(linking)相对的索引;假如你看的是纯(plain)文字版本的话, 数字就只是数字, 没别的含意;建议你赶紧升级为妙哩!
我所用的shell是Bourne shell(不是C shell),举的例子自然是Bourne shell的语法.假如你用的是C shell的话, 环境变数设定的语法会像下面这样:
% setenv FOO bar
要是用Bourne shell的话, 我会这样子写:
$ FOO=bar; eXPort FOO
假如提示符号(prompt)显示的是井字符号#,而不是钱字符号 $,那麽,很有可能是这个命令只适用root而已.当然啦!要是你试了这些□例,结果弄得你的系统发生灾变,我可是一点责任也不会负的喔!祝你心情好啊!:-) [译者注:牵拖(闽南语) _ .]
11/8/97译.
2. 上哪抓这些东东?
2.1. 这份文件座落之处
这份文件是Linux HOWTO系列之一.易言之,你可以在所有存放Linux HOWTO文件的网站上面找到它的芳踪,例如http://sunsite.unc.edu/pub/linux/docs/HOWTO/.HTML格式的版本(可能会是较新的版本)可以从http://FTP.linux.org.uk/~barlow/howto/gcc-howto.html上面抓下来.
2.2. 其它相关的说明文件
gcc正式的说明文件是附在发行的原始码(source distribution)内(往下看就有了!),里头有textinfo与.info两种档案.要是你的网路连接速率够快,或者是有一片cdrom;不然的话,有高度的耐心也成,你可以自己把它untar,然後再把相对应的位元一一拷贝到/usr/info的目录底下.假如你的条件与上述的不符,不妨到 tsx-11站上去找一找.不过,我想,没有必要老是惦记著最新的版本吧.
libc的文件说明有两种来源.一种是GNU libc,以.info的格式储存,除了stdio之外,其馀Linux libc的说明都相当详尽精确.另一种可以在Linux的archivemanpages 上找到系统呼叫(system call)(第2节)与libc函数(function)(第3节)的文件说明.
2.3. GCC
解答有二:
(a)你可以在ftp://tsx-11.mit.edu:/pub/linux/packages/GCC/的网站上找到正式的Linux GCC发行系统(distribution),且已编译好的(read-compiled)可执行档(in binary).当我在写这份文件时,2.7.2(gcc-2.7.2.bin.tar.gz)是最新的版本.
(b)自由软体基金会(Free Software Foundation)所发布的GCC最新原始码可以从网站GNU archives上取得.没有必要非得与上述的版本一致才行,不过这个版本的确是目前最新的.Linux GCC的维护人士(maintainers)让你可以很轻松的自行编译这个最新的版本.configure命令稿(script)会帮你自动建好(set it all up)所有该做的事.建议你有空不妨到tsx-11看看,说不定会有修正的版本(patches)是你会想要用的(apply).
假如想要编译出一些有用的东东(non-trivial)(不是我罗唆,还是有不少细琐的东东在哩!),下面一小节所谈的也是你要具备的:
2.4. C程式库与标头档
在这儿你该选的是取决於(i)你的系统是ELF亦或是a.out的;(ii)你希望你的系统变成哪一种?假如你是从libc 4升级到libc 5,那麽给你一个良心的建议,去看看ELF-HOWTO文件.你一定会问,在ELF文件的哪儿呢?嘿!嘿!不偏不倚,就差不多跟这份文件一样的位置.你可以在网站tsx-11上面找到你想要的.
libc-5.2.18.bin.tar.gz
--- ELF共享程式库(ELF shared library images),静态程式库(static libraries)与标头档(include files)(针对C语言与数学程式库的).
libc-5.2.18.tar.gz
---libc-5.2.18.bin.tar.gz的原始码.这两个档案你都需要,.bin.套件(package)内含有标头档(header files).假如此时你正犹豫不决,不晓得是要老身亲自下海,动手编译C程式库;还是直接用编译好的二进位档(binaries)就可以了.有这种困扰的人,来,看我的嘴形:用人家编译好的二进位档不就解决了嘛.只有在你想要NYS或是shadow passWord的情况下,你才需要自己的手来推动摇篮.
libc-4.7.5.bin.tar.gz
--- 这个档案的内容是a.out的共享程式库(shared library images)与静态程式库.这个档案的用途是为了与前述的libc 5套件共存共荣(coexist)而设计的,不过除非你想要继续使用或者发展a.out格式的程式,不然的话,是不需要它的.
2.5. 相关联的工具 (as, ld, ar, strings etc)
到目前为止,与之前所谈的都一样,从网站tsx-11上,就可以找到这些工具程式.目前的版本是binutils-2.6.0.2.bin.tar.gz.
需注重的是binutils只适用於ELF格式,目前libc的版本也都是ELF的;当然啦, 习惯a.out的人假如有个ELF的libc与a.out的libc联合(in conjunction with)起来一起使用, 那对他们来讲是再好不过的美事了.不可否认的,C程式库的发展正以果断的(emphatically)脚步迈向ELF格式,除非你真的有很好的理由,需要 a.out的东东(things),不然啊,大家都会鼓励(encourage)你勇於突破,趁早加入锐不可挡的大潮流里.
11/9/97译
3. GCC的安装(installation)与启用(setup)
3.1. GCC的版本
你可以在shell的提示符号下键入gcc -v,萤幕上就会显示出你目前正在使用的GCC的版本.而这也是一个相当可靠的方法,可以确定你现在所用的是ELF或是a.out.在我的系统上,执行gcc -v的结果是:
$ gcc -v
Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specsc c version 2.7.2
上面的讯息说明了几件重要的事情:
i486. 这是指出(indicates)你目前在用的gcc是为了486的微处理器(processor)而写的-可能你的电脑是386或者是586.这3种微处理器的晶片(chips)所编译而成的程式码,彼此间是可以相容使用的.差别之处是486的程式码在某些地方有加上padding的功能,所以可以在 486上面跑得比较快.这对386的机器而言,在执行程式的效能(performance)上并没有什麽不良的影响detrimental effect),只不过真的(does)会让程式码变得稍稍的大了些.
box. 这可以说一点也不重要;不过也可能另有所指(像是slackware或者是debian),或者根本什麽也不是(所以罗!完整的目录名称是i486-linux).假如你是实践派的代表,亲自动手建立属於自己的gcc,那麽你可以在建立的过程中(build time)设定这一项,以装点门面
(cosmetic effect).就像我做的一样:-).
linux. 其实这是指linuxelf,或者是linuxaout.这一点会令人引起不必要的困惑,究竟是指哪一种会根据你所用的版本而异.
linux 意指ELF若版本序号是2.7.0或是更新的版本;否则的话,就是a.out的了.
linuxaout 意指a.out的格式.当linux的定义(definition)从a.out更换到ELF时,linuxaout就会顺水推舟,摇身一变,成了一个目标物件(target).因此,你不会看到任何版本新於2.7.0的gcc有linuxaout格式的.
linuxelf 已经过时了.通常那是指2.6.3版的gcc,而这个版本也可用来产生ELF的可执行档(executables).要注重的是,gcc 2.6.3版在产生ELF程式码时会有bugs-假如你目前用的是这个版本,建议你赶紧升级.
2.7.2 版本的序号.
所以,总结起来,我有2.7.2版的gcc,可以产生ELF格式的程式码.就这麽简单,惊奇吧!eh?
3.2. 东东装好後都到哪儿去了?
假如安装gcc时没有仔细的看著萤幕,或者你是从一个完整的发行系统内把gcc单独抓出来安装的话,那麽也许你会想知道到底这些东东装好後是住在整个档案系统(file-system)的那个地方.几个重点如下:
/usr/lib/gcc-lib/target/version/ (与子目录(sub-Directories))大部份的编译器(compilers)就是住在这儿的.在这儿有可执行的程式,实际在做编译的工作;另外,还有一些特定版本的(version-specific)程式库与标头档include files)等.
/usr/bin/gcc 指编译器的驱动程式(driver)--就是你实际在命令列(command line)上执行的程式.这个目录可供各种版本的gcc使用,只要你用不同的编译器目录(如上所述)来安装就可以了.要知道内定的版本是那一个,在shell提示符号下打gcc -v.要是想强迫执行某个版本,就换打gcc -V version.例如:
# gcc -v
Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specs
gcc version 2.7.2
# gcc -V 2.6.3 -v
Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.6.3/specs
gcc driver version 2.7.2 executing gcc version 2.6.3
/usr/target/(binlibinclude)/. 假如你装了数种的目标物件(multiple targets),例如a.out与elf,或者某一种的交叉编译器(cross-compiler)等等;那些属於非主流目标物件(non-native target(s))的程式库,binutils(as, ld等等)工具与标头档(header files)等都可以在这儿找到.即使你只安装了一种gcc,还是可以在这儿找到这些原本就是替它们预备的东东.假如不是在这儿,那麽就应该是在/usr/(binlibinclude)了.
/lib/,/usr/lib 与其它的目录等,都是主流系统(native-system)的程式库目录.许多的应用程式都会用到/lib/cpp ,因此你也需要它---作法上,不是从/usr/lib/gcc-lib/target/version/ 目录里拷贝,就是弄个符号连结(symlink)指向那儿. [译者注:所谓native,是指目前你的系统是以a.out或elf的格式为主,或者内定的gcc是哪一种版本等等.native的意思是'本土的', '本国的'与'天生的'......等等;当你拿到一片CD-ROM重头至尾将Linux安装完成,让Linux出生,成为你个人特色浓厚的作业平台之後,假如再加装一些不一样的目标物件,自然就有'本土'与'外省'( 无关政治),'本国'与'外国','天生'与'人为'等等的区别,同时也含有内定(default)的意思在.假若再附加上你个人的价值观判定与喜好,我想用主流(native)与非主流(non-native)来翻译应该还算恰当.]
3.3. 标头档
把你自己自行安装在/usr/local/include目录下的标头档(header files)排除在外的话,Linux还有另外3种主要的标头档(header files):
/usr/include/与其子目录下的标头档,大部份都是由H.J.Lu发展的libc套件(libc binary package)内所提供的.我会只说'大部份(most)'的原因, 是因为你可能有其它来源的标头档(header files)(像是curses与dbm程式库等等)摆在这儿;尤其是,假如你现在用的是最新的libc进位形式(machine code)储存之套件,并非原始码(text),若要以中文全称译出,则成'libc二进位档套件',似有聱牙之嫌,故略去binary,以libc套件通称.]
在核心原始码的发行系统内(kernel source distribution) ,/usr/include/linux 与 /usr/include/asm (里头有这些档案:
and )应该有符号连结(symbolic links),可连结至目录linux/include/linux 与 linux/include/asm.假如你有鸿鹄之志的话,安装这些东东後,就不应该只是拿来编译核心(kernel)而已. 把原始码解压缩(unpacking)後,可能你也会发现,需要在核心的目录(kernel directory)底下做make config的动作.很多的档案都会依靠的帮忙,可是这个档案却有可能因版本不同而不存 在.若干核心版本里,asm就只是它自己的一个符号连结,仅仅是在make config时建立出来而已. [译者注:原文提及autoconf.h时是 'Many files depend on ,which otherwise may not exist,*'.此处之otherwise之词性应为形容词(adj),指'另一情 况','另一种','不同的'之意,将原文形容词子句拆开来应为:
(i). Many files depend on .
(ii). of other condition may not exist.
与下一句互相比对,此处应同指在不同版本之情况下.] 所以,当你在目录/usr/src/linux底下,解开核心的程式码时,就照著下面指示的做吧!
$ cd /usr/src/linux
$ su
# make config
[回答接下来的问题.通常回答得正不正确并不重要,除非你打算继续□起(go on and build)你的核心.]
# cd /usr/include
# ln -s ../src/linux/include/linux .
# ln -s ../src/linux/include/asm .
诸如, ,, 与之类的档案,会随著不同的编译器版本而异,属於你自己'个人'的档案,可以在 /usr/lib/gcc-lib/i486-box-linux/2.7.2/include/与其它有相类似(相同)目录名称的地方(places of that ilk)找到.
11/11/97译
3.4. 建立交叉编译器(Building cross compilers)
3.4.1. 将Linux当作目标作业平台(target platform)
假设你已经拿到gcc的原始码,通常你只要依循INSTALL档内的指示便可一切ok. make後面黏个configure --target=i486-linux --host=XXX on platform XXX,就能帮你变把戏了(do the trick).要注重的是,你会需要Linux与核心的标头档的;而且你也需要建立交叉组译器(cross assembler)与交叉连结器(cross linker),来源是ftp://tsx-11.mit.edu/pub/linux/packages/GCC/
3.4.2. Linux当成来源作业平台(source platform),MSDOS作为目标作业平台Ugh.很明显的,这个大概需要用到"emx"套件(package)或者是"go"延伸套件 (extender).请自行去ftp://sunsite.unc.edu/pub/Linux/devel/msdos看看.我并没有测试过这个,因此也无法保证(voUCh)它的功能(abilities).
4. 移植(Porting)与编译(Compiling)程式
4.1. gcc自行定义的符号
只要执行gcc时,附加 -v这个参数(switch),就能找出你所用的这版gcc,自动帮你定义了什麽符号(symbols).例如,我的机器看起来会像这样:
$ echo 'main(){printf("hello world");}' gcc -E -v -
Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specs
gcc version 2.7.2
/usr/lib/gcc-lib/i486-box-linux/2.7.2/cpp -lang-c -v -undef -
D__GNUC__=2 -D__GNUC_MINOR__=7 -D__ELF__ -Dunix -Di386 -Dlinux
-D__ELF__ -D__unix__ -D__i386__ -D__linux__ -D__unix -D__i386
-D__linux -Asystem(unix) -Asystem(posix) -Acpu(i386)
-Amachine(i386) -D__i486__ -
假若目前你正在写的程式码,会用到一些Linux独有的特性(Linux-specific features),那麽把那些无法移植的程式码(non-portable bits),以条件式编译(conditional compilation)的前置命令封括(enclose in)起来,可是个不错的主意呢!
#ifdef __linux__
/* ... funky stuff ... */
#endif /* linux */
用__linux__即可达成目的;看仔细一点,不是linux啊.仅管後者也有定义,究竟,仍然不是POSIX的标准(not POSIX compliant).
4.2. 线上求助说明(invocation)
gcc编译器参数(switches)的说明文件是gcc info page(在Emacs内,按下C-h i,然後选'gcc'的选项).要是弄不出来,不是卖你CD-ROM的人,没把这个东东压给你,不然就是你现在用的是旧版的.这种情况下,最好的方法是移动尊臀到archiveftp://prep.ai.mit.edu/pub/gnu或是它的mirrors站台上,把gcc的原始档案抓回家,重新烹饪一番.
gcc manual page (gcc.1) 可以说是已经过时了.一旦你吃饱撑著没事干要去看看它的话,它就会告诉你这件事,叫你别无聊了.
4.2.1. 旗正飘飘~(flags)
在命令列(command line)上执行gcc时,只要在它的屁股後面加上-On的选项,就能让gcc乖乖的替你生出最佳化後的机器码(output code).这里的n是一个可有可无的小整数.不同的gcc版本,n的意义与其正确的(exact)功效都不一样;不过,典型的□围是从0(不要鸡婆,我不要最佳化)变化到2(最佳化要多一点),再到3(最佳化要再多一点,多一点).
gcc在其内部会将这些转译成一系列的-f 与-m选项(options).执行gcc时带上旗号(flags)-v与-Q,你就能很清楚的看出每一种等级的-O是对应(maps)到那些选项(options).例如,就-O2来讲,我的gcc告诉我说:
enabled: -fdefer-pop -fcse-follow-jumps -fcse-skip-blocks -fexpensive-optimizations
-fthread-jumps -fpeephole -fforce-mem -ffunction-cse -finline
-fcaller-saves -fpcc-struct-return -frerun-cse-after-loop
-fcommon -fgnu-linker -m80387 -mhard-float -mno-soft-float
-mno-386 -m486 -mieee-fp -mfp-ret-in-387
要是你用的最佳化等级(optimization level)高於你的编译器所能支援的(e.g. -O6),那麽它的效果,就跟你用你的编译器所能提供的最高等级的,是一样的结果.说实在的,发行出去的gcc程式码,用在编译时竟是如此处理这等问题, 实非什麽好的构想.日後若是有更进步的最佳化方法具体整合到新的版本里,而你(或你的users)还是试著这样做的话,可能就会发现,gcc会中断你的程式(break your code)了.
从gcc 2.7.0到2.7.2的users应该注重到,使用时-O2会有一个bug存在.更糟糕的是,强度折减(strength reduction)居然没有用
(doesn't work)!要是你喜欢重新编译gcc的话,是有那麽一个修正的版本(patch)可以更正这项错误;不然的话,一定要确定每次编译时都会加上-fno-strength-reduce喔!
11/12/97译
4.2.1.1. 有个性的微处理器(Processor-specific)
有一些-m的旗号无法藉由各种等级的-O来打开,然而却是十分有用的.这之中最主要的是-m386与-m486两种,用来告诉gcc该把正在编译的程式码视作专为386或是486机器所写的.不论是用哪一种来编译程式码,都可以在彼此的机器上执行,-m486编译出来的码会比较大,可是拿来在386的机器上跑也不会比较慢就是了.
目前尚无-mpentium或是-m586的旗号.Linus建议我们,可以用-m486 -malign-loops=2 -malign-jumps=2 -malign-functions=2,来得到最佳化的486程式码(486 code optimizations),而这样做正好就可以避免alignment(Pentium并不需要)有过大的gaps发生.
Michael Meissner说:
我的第六感(hunch)告诉我, -mno-strength-reduce(嘿!我可不是在谈强度折减的bug啊,那已经是另外一个争论的战场了.)一样也可以在x86的机器上,产生较快的程式码,这是因为x86的机器对暂存器(register)有著不可磨灭的□渴在(and GCC's method of grouping registers into spill registers vs. other registers doesn't help either).传统上,强度折减的结果会使得编译器利用加法暂存器(additional registers)以加法运算(addition)来取代乘法运算(multiplication).而且,我也在怀疑(suspect)-fcaller-saves,可能也只是个漏洞(loss)也说不定. 而我的第七感则再度的告诉我, -fomit-frame-pointer可能会,也可能不会有任何的赚头.从这点来看,即意谓著有另一个暂存器可用来处
理记忆体分配(allocation)的问题.另方面,若纯粹从x86的机器在转换(encodes)它的指令集(instruction set)成为机器码的方法上来看,便意谓著堆叠(stack)所用到的记忆体空间要比frame所用到的还要来的多;换句话说,Icache对程式码而言并没有实质上的益处.若是阁下用了-fomit-frame-pointer的话,同时,也就是告诉编译器在每次呼叫函数(calls)之後,就必须修正堆叠的指标(stack pointer);然而,就frame来讲,若呼叫的次数不多的话,则答应堆叠暂时堆积(accumulate)起来.
有关这方面主题的最後一段话仍是来自於Linus:
要注重的是,假如你想要得到最佳状况的执行成果(optimal performance),可千万别相信我的话.无论如何,一定要进行测试.gcc编译器还有许多的参数(switches)可用,其中可能就有一种最非凡的组合(set),可以给你最佳化的结果喔.
11/14/97译
4.2.2. Internal compiler error: cc1 got fatal signal 11 的记忆体.所以,这可能是一个gcc的bug.
然而,大部份而言,gcc是一件经过严密测试且可靠度佳的软体佳作.它也用了大量复杂的资料结构与惊人的指标数量.简言之,若是要评选本世纪最挑惕与最一丝不苟的RAM测试程式(RAM tester)的话,gcc绝对可以一摘后冠的.假如你无法重新复制这只bug---当你重新开始编译时,错误的讯息并没有一直出现在同一个地方---那几乎可以确定,是你的硬体本身有问题(CPU,记忆体,主机板或是快取记忆体).千万不要因为你的电脑可以通过开机程序的测试(power-on checks),或者Windows可以跑得很顺,或者其它什麽的,就回过头来大肆宣传说这是gcc的一个bug;你所做的这些测试动作,通常没有什麽实际上的价值,而且没有价值也是很合理的结论.另外,也不要因为编译核心时,总是停留在
`make zImage'的阶段,就要大骂这是gcc的bug---当然它会停在那儿啊!'做'make zImage'时,需要编译的档案可能超过200档案;我们正在研拟一个比较小的地方来取代.
假如你可以重覆产生这个bug,而且(最好是这样啦)可以写一个短小的程式来展示这只bug的话,你就可以把它做成bug报表(bug report),然後email给FSF,或者是linux-gcc邮件表列(linux-gcc mailing list).你可以去参考gcc的说明文件,看看有什麽具体的资讯,是他们所需要的.
4.3. 移植能力(Portability)
据报,近日来许多正面消息指出,若有某件东东到现在都还没移植到Linux上去,那麽可以肯定的是,它一定一点价值也没有.:-)
嗯!正经一点.一般而言,原始码只需要做一些局部的修改(minor changes),就可以克服(get over)Linux 100%与POSIX相容的特质(compliance).假如你做了任何的修改,而将此部份传回(passing back)给原作者,会是很有建设性的举动(worthwhile).这样日後就只需要用到'make',就能得到一个可执行的档案了.
4.3.1. BSD教派(BSDisms) (有 bsd_ioctl, daemon 与 )
编译程式时,可以配合-I/usr/include/bsd与连结-lbsd的程式库.(例如:在你的Makefile档内,把- I/usr/include/bsd加到CFLAGS那一行;把-lbsd加到LDFLAGS那一行).假如你真的那麽想要BSD型态的信号行为(BSD type signal behavior),也不再需要加上-D__USE_BSD_SIGNAL了.那是因为当你用了-I/usr/include/bsd与含括了标头档之後,make就自动会把它加入了.
4.3.2. 失落的封印(`Missing' signals)(SIGBUS, SIGEMT, SIGIOT, SIGTRAP, SIGSYS etc)
Linux与POSIX是完全相容的.不过,有些信号并不是POSIX定义的---ISO/IEC 9945-1:1990 (IEEE Std 1003.1-1990), paragraph B.3.3.1.1 sez:
"在POSIX.1中省略了SIGBUS, SIGEMT, SIGIOT, SIGTRAP, 与SIGSYS信号,那是因为它们的行为(behavior)与实作方式是息息相关的(implementations dependent),而且也无法进行适当的分门别类(adequately categorized).确认实作方式後(conforming implementations),便可以生产出(deliver)这些信号,可以必须以文件说明(document)它们是在什麽样的环境(circumstances)下生产出来的,以及指出与它们的发展相关的任何限制(any restrictions concerning their delivery)".
如欲修正此点,最简单,也是最笨的(cheesy)方法就是以SIGUNUSED重新定义这些信号.而正确的方法应是以条件式的编译#ifdef来处理这些问题才对:
#ifdef SIGSYS
/* ... non-posix SIGSYS code here .... */
#endif
1/15/97译
4.3.3. K & R
gcc是个与ANSI相容的编译器;希奇的是,目前大多数的程式码都不符合ANSI所定的标准.假如你热爱ANSI,喜欢用ANSI提供的标准来撰写C程式,似乎除了在编译器的旗号上加上-traditional之外,就没有什麽其它的可以多谈的了.
There is a certain amount of finer-grained control over which varieties of brain damage to emulate;
请自行查阅gcc info page.
要注重的是,尽管你用了-traditional来改变语言,它的效果也仅局限在gcc所能够接受的□围.例如, -traditional会打开(turn on)-fwritable-strings,使得字串常数(string constants)移至资料记忆体空间(data space)内(从程式码记忆体空间(text space),这地方是不能任意写入的).这样做会让程式码的记忆体空间无形中增加的.
4.3.4. 前置处理器(Preprocessor)的符号卯上函数原型宣告(prototypes)
最常见的问题是,如众所皆知,Linux中有许多常用的函数都定义成巨集(macros)存放在标头档(header files)内,此时若有相似的函数原型宣告出现在程式码内,前置处理器会拒绝进行语法分析(parse)的前置作业.常见的有atoi()与atol().
4.3.5. sprintf()
在大部份的Unix系统上, sprintf(string, fmt, ...)传回的是string的指标,然而,这方面Linux(遵循ANSI)传回的却是放入string内的字元数目.进行移植时,尤其是针对SunOS,需有警觉的心.
4.3.6. fcntl 与相关的函数; FD_*家族的定义到底摆在哪里?
就在 里头. 为了真正的原型宣告,当你用了fcntl,可能你也想含括标头档进来.
一般而言,函数的manual page会在SYNOPSIS章节内列出需要的标头档.
4.3.7. select() 的计时(time-out)---程式执行时会处於忙碌-等待的状态(busy-waiting). 很久很久以前, select()的计时参数(time-out parameter)只有读的属性(read-only)而已.即使到了最近,manual pages仍然有下面这段的
警告:
select()照理讲应该是藉由适当的修正时间的数值,再传回自原始计时(original time-out)开始後所剩馀的时间.未来的版本可能会使这项功能实现.因此,就目前而言,若假定在呼叫select()之後,计时指标(time-out pointer)仍然不会让人给修正过,可是一种非常不明智的想法喔!
未来就在我们的眼前了!至少,在这儿你绝对可以看到. 函数select()传回的,是扣除等待尚未到达的资料所耗费的时间後,其剩馀的时间值.假如在计时结束时,都没有资料传送进来,计时引数(time-out argument)便会设为0;假如接著还有任何的select(),以同样的time-out structure来呼叫,那麽select()便会马上结束.
若要修正这项问题,只要每次呼叫select()前,都把计时数值(time-out value)放到time-out structure内,就没有问题了.把下面的程式码,
struct timeval timeout;
timeout.tv_sec = 1; timeout.tv_usec = 0;
while (some_condition)
select(n,readfds,writefds,exceptfds,&timeout);
改成,
struct timeval timeout;
while (some_condition) {
timeout.tv_sec = 1; timeout.tv_usec = 0;
select(n,readfds,writefds,exceptfds,&timeout);
}
这个问题,在有些版本的Mosaic里是相当闻名的,只消一次的等待,Mosaic就挂了.Mosaic的萤幕右上角,是不是有个圆圆的,会旋转的地球动画.那颗球转得愈快,就表示资料从网路上传送过来的速率愈慢!
4.3.8. 产生中断的系统呼叫(Interrupted system calls)
4.3.8.1. 徵兆(Symptom):
当一支程式以Ctrl-Z中止(stop),然後再重新执行(restart)时--或者是其它可以产生Ctrl-C中断(interruption)信号的情况,如子程序(child process)终结(termination)等--系统就会抱怨说"interrupted system call"或是"write: unknown error",或者诸如此类的讯息.
4.3.8.2. 问题点:
POSIX的系统检查信号的次数,比起一些旧版的Unix是要多那麽一点.假如是Linux,可能就会执行signal handlers了--非同步地(asynchronously)(计时器的滴答声) 系统呼叫的传回值(on return from any system call) 在下列系统呼叫的执行期间:
select(), pause(), connect(),accept(), read() on terminals, sockets, pipes or files in /proc, write() on terminals, sockets,
pipes or the line printer, open() on FIFOs, PTYs or serial lines,ioctl() on terminals, fcntl() with command F_SETLKW, wait4(),
syslog(), any TCP or NFS operations.
就其它的作业系统而言,你需要的可能就是下面这些系统呼叫(system calls)了: creat(), close(), getmsg(), putmsg(), msgrcv(),
msgsnd(), recv(), send(), wait(), waitpid(), wait3(), tcdrain(), sigpause(), semop() to this list.
在系统呼叫期间,若有一信号(那支程式本身应预备好handler因应了)产生,handler就会被呼叫.当handler将控制权转移回系统呼叫时, 它会侦测出它已经产生中断,而且传回值会马上设定成-1,errno设定成EINTR.程式并没有想到会发生这种事,所以就会bottles out了.
有两种修正的方法可以选择:
(1) 对每个你自行安装(install)的signal handler,都须在sigaction旗号加上SA_RESTART.例如,把下列的程式,
signal (sig_nr, my_signal_handler);
改成,
signal (sig_nr, my_signal_handler);
{ struct sigaction sa;
sigaction (sig_nr, (struct sigaction *)0, &sa);
#ifdef SA_RESTART
sa.sa_flags = SA_RESTART;
#endif
#ifdef SA_INTERRUPT
sa.sa_flags &= ~ SA_INTERRUPT;
#endif
sigaction (sig_nr, &sa, (struct sigaction *)0);
}
要注重的是,当这部份的变更大量应用到系统呼叫之後,呼叫read(), write(),ioctl(), select(), pause() 与 connect()时,你仍然得自行检查(check for)EINTR.如下所示.
(2) 你自己得很明确地(explicitly)检查EINTR:
这里有两个针对read()与ioctl()的例子.
原始的程式片段,使用read().
int result;
while (len > 0) {
result = read(fd,buffer,len);
if (result < 0) break;
buffer += result; len -= result;
}
修改成,
int result;
while (len > 0) {
result = read(fd,buffer,len);
if (result < 0) { if (errno != EINTR) break; }
else { buffer += result; len -= result; }
}
原始的程式片段,使用ioctl().
int result;
result = ioctl(fd,cmd,addr);
修改成,
int result;
do { result = ioctl(fd,cmd,addr); }
while ((result == -1) && (errno == EINTR));
注重一点,有些版本的BSD Unix,其内定的行为(default behaviour)是重新执行系统呼叫.若要让系统呼叫中断,得使用 SV_INTERRUPT或SA_INTERRUPT旗号.
4.3.9. 可以写入的字串(Writable strings)
gcc对其users总怀抱著乐观的想法(optimistic view),相信当他们打算让某个字串当作常数来用时---那它就真的只是字串常数而已.因此,这种字串常数会储存在程式码的记忆体区段内(in the code area of the program).这块区域可以page到磁碟机的image上,避免耗掉swap的记忆体空间,而且任何尝试写入的举动都会造成分页的错误(segmentation fault).这可是一种特色呢!
对老旧一点的程式而言, 这可能会产生一个问题.例如,呼叫mktemp(),传递引数(arguments)是字串常数. mktemp()会尝试著在*适当的位置(in place)*重新写入它的引数.
修正的方法不外乎(a)以-fwritable-strings编译,迫使gcc将此常数置放在资料记忆体空间(data space)内.或者(b)将侵犯地权的部份(offending parts)重新改写,配置一个不为常数的字串(non-constant string),在呼叫前,先以strcpy()将资料拷贝进去.
4.3.10. 为什麽呼叫execl()会失败?
那是因为你呼叫的方式不对.execl的第一个引数是你想要执行的程式名.第二个与接续的引数会变成你所呼叫的程式的argv阵列(array).记住:传统上,argv[0]是只有当程式没有带著引数执行时,才会有设定值.所以罗,你应该这样写:
execl("/bin/ls","ls",NULL);
而不是只有,
execl("/bin/ls", NULL);
执行程式而不带任何引数(with no arguments),可解释成(construe)是一种邀请函(invitation),目的是把此程式的动态程式库独立(dynamic library dependencies)的特性印出来(print out).至少,a.out是这样的.就ELF而言,事情就不是这样了.
(假如你想得知此程式库的资讯,有一些更简单的介面可用;参考动态载入(dynamic loading)那一章节,或是ldd的manual page.)
11/16/97译
5. Debugging and Profiling
5.1. Preventative maintenance (lint)
lint对Linux而言并没有很广泛的用途,主要是因为大部份的人都能满足於gcc所提供的警告讯息(warnings).可能最有用的就是-Wall参数了---这个参数的用途是要求gcc将所有的警告讯息显现出来.
but probably has more mnemonic value if thought of as the thing you bang your head against.
网路上有一个实用的public domain lint,位於
ftp://larch.lcs.mit.edu/pub/Larch/lclint.我并不知道这个站到底有多好就是了.
5.2. 除错(Debugging)
5.2.1. 我要怎样做才能将除错资讯放到一支程式里头?
你需要添加-g的参数来编译与连结程式,而且不可以用-fomit-frame-pointer参数.事实上,你不需要重新编译所有的程式,只需重新编译目前你正在除错的部份即可.
就a.out的格式(configurations)而言,共享程式库(shared libraries)是以-fomit-frame-pointer编译而成,这个时候,gdb就变得英雄无用武之地了.连结时给定-g的选项,应该就隐含著静态连结(static linking)了;这就是为什麽要加-g的原因了.
假如连结器(linker)连结失败,告诉你找不到libg.a,那就是在/usr/lib/的目录底下,少了libg.a.libg.a是非凡的C语言侦错程式库(special debugging-enabled C library).一般在libc的套件内就会提供libg.a;不然的话(新版是这样的),你可能需要拿libc的原始码自己建立了.不过,实际上你应该不需要才对.不管是什麽目的,大部份的情况下,只需将libg.a连结到/usr/lib/libc.a,你就能得到足够的资讯了.
5.2.1.1. 那,能不能把除错资讯给拿掉?
很多的GNU软体在编译连结时,都会设定-g的选项,而这样做会造成执行档过大的问题(通常是静态的).实际上,这并不是一个很热门的想法.
假如程式本身有autoconf,产生了configure命令稿,通常你就可以用./configure CFLAGS=或是./configure CFLAGS=-O2来关掉除错资讯.不然的话,你得检查检查Makefile了.当然啦,假如你用的是ELF,程式便会以动态的方式连结(dynamically linked),不论是否有-g的设定;因此你可以平常心把-g拿掉(strip).
5.2.2. 实用的软体(Available software)
据了解,一般人都使用gdb.你可以从GNU archive sites拿到原始程式;或者是到tsx-11拿可执行档.xxgdb是一个X介面的除错程式(debugger),植基於gdb(也就是说你得先安装好gdb,才能再装xxgdb).xxgdb的原始码可以在
ftp://ftp.x.org/contrib/xxgdb-1.08.tar.gz找到.
另外,UPS除错程式已由Rick Sladkey移植成功.UPS可以在X底下活得很好,不像xxgdb那样---仅仅是gdb的X前端介面(X front end).这支除错程式有一大堆优良的特点,and if you spend any time debugging stuff, you probably should check it out.先前编译(precompiled)好的Linux版与修正版(patches)的原始码可以在ftp://sunsite.unc.edu/pub/Linux/devel/debuggers/找到.而最初的原始程式则放在 ftp://ftp.x.org/contrib/ups-2.45.2.tar.Z.
你可能会发现另一个用来除错的工具strace,也是相当的有用.它可以显示出由程序(process)所产生的系统呼叫,而且还拥有其它众多繁复的功能(multiplicity),像是假如你手边没有原始码的话,strace可以帮你找出(figure out)有那些路径(path-names)已编译进执行档(binaries)内; exacerbating race conditions in programs that you suspect contain them;还有,strace可拿来学习程式是怎麽在电脑中执行的.最新的版本(目前是3.0.8)可在找到
ftp://ftp.std.com/pub/jrs/.
5.2.3. 背景程式(Background (daemon) programs)
早期典型的常驻程式(daemon programs)是执行fork(),然後终止(terminate)父程序(parent).这样的做法使得除错的时间减短了.
了解(get around)这点的最简单的方法就是替fork()设一个breakpoint.当程式停止时,强迫fork()传回0.
(gdb) list
1 #include
2
3 main()
4 {
5 if(fork()==0) printf("child
");
6 else printf("parent
");
7 }
(gdb) break fork
Breakpoint 1 at 0x80003b8
(gdb) run
Starting program: /home/dan/src/hello/./fork
Breakpoint 1 at 0x400177c4
Breakpoint 1, 0x400177c4 in fork ()
(gdb) return 0
Make selected stack frame return now? (y or n) y
#0 0x80004a8 in main ()
at fork.c:5
5 if(fork()==0) printf("child
");
(gdb) next
Single stepping until exit from function fork,
which has no line number information.
child
7 }
5.2.4. 核心档案(Core files)
当Linux开机时,通常组态(configuration)会设定成不要产生核心档案.要是你那麽喜欢它们的话,可以用shell的builtin命令使其重新生效:就C-shell相容的shell(如tcsh)而言,会是下面这样:
% limit core unlimited
而类似Bourne shell的shell(sh,bash,zsh,pdksh)则使用下面的语法:
$ ulimit -c unlimited
假如你想要有个多才多艺(versatility)的核心档命名(core file naming)(for example, if you're trying to conduct a post-mortem using a debugger that's buggy itself) ,那麽你可以对你的核心程式(kernel)做一点小小的更动(mod).找一找fs/binfmt_aout.c与fs/binfmt_elf.c档内与下列相符的程式片段(in newer kernels, you'll have to grep around a little in older ones):
memcpy(corefile,"core.",5);
#if 0
memcpy(corefile+5,current->comm,sizeof(current->comm));
#else
corefile[4] = '';
#endif
将0换成1.
5.3. 旁敲侧击(Profiling)
Profiling是用来检核一支程式中那些部份(which bits)是最常呼叫或是执行的时间最久的方法.这对程式的最佳化与找出何时时间是浪费掉的而言,是相当好的方式.你必须就你所要的时程资讯(timing information)的目的档案(object files)加上-p来编译,而且假如要让输出的档案(output files)有意义(make sense),你也会需要gprof(来自binutils套件的命令).参阅gprof的manual page,可得知其细节.
11/18/97译
6. 连结(Linking)
由於静态(static)与共享(shared)程式库两者间不相容的格式(incompatible binary formats)的差异性(distinction)与动词*link*过量使用(overloading)於指称*编译完成後的事情*与*当编译过的程式使用时(invoke)所发生的事情*这两件事上头,使得这一章节变得复杂了许多.( and, actually, the overloading of the word `load' in a comparable but opposite sense)不过,再复杂也就是这样了,所以阁下不必过於担心.
为了稍微减轻读者的困惑,我们称执行期间(runtime)所发生的事为*动态载入(dynamic loading)*,这一主题会在下一章节中谈到.你也会在别的地方看到我把动态载入描述成*动态连结(dynamic linking)*,不过不会是在这一章节中.换句话说,这一章节所谈的,全部是指发生在编译结束後的连结(linking).
6.1. 共享程式库 vs静态程式库
建立程式的最後一个步骤便是连结;也就是将所有分散的小程式(pieces)组合起来,看看是否遗漏了些什麽.很明显的,有一些事情是很多程式都会想做的---例如,开启档案(open files),接著所有与开档有关的小程式(pieces)就会以程式库的档案型态提供给你的程式.在普通的Linux系统上,这些小程式可以在/lib与/usr/lib/目录底下找到.
当你用的是一静态的程式库时,连结器会找出程式所需的模组(bits),然後实际(physically)将它们拷贝到执行档内.然而,对共享程式库而言,就不是这样了.共享程式库会在执行档内留下一个符号(note),指明*当程式执行时,首先必须载入这个程式库*.很明显的,共享程式库是试图使执行档变得更小;也等同於使用更少的记忆体与磁碟空间.Linux内定的行为是连结共享程式库,只要Linux能找到这些共享程式库的话,就没什麽问题;不然,Linux就会连结静态的了.假如你想要共享程式库的话,检查这些程式库(*.sa for a.out, *.so for ELF)是否住在它们该在的地方,而且是可读取的.
在Linux上,静态程式库会有类似libname.a这样的名称;而共享程式库则称做libname.so.x.y.z,此处的x.y.z是指版本序号的样式.共享程式库通常都会有连结符号指向静态程式库(很重要的)与(on a.out configurations)相关联的.sa档案.标准的程式库会包含共享与静态程式库两种格式.
你可以用ldd (List Dynamic Dependencies)来查出某支程式需要哪些共享程式库.
$ ldd /usr/bin/lynx
libncurses.so.1 => /usr/lib/libncurses.so.1.9.6
libc.so.5 => /lib/libc.so.5.2.18
这是说在我的系统上,WWW浏览器(browser)*lynx*会依靠libc.so.5 (the C library)与libncurses.so.1(终端机萤幕的控制)的存在(presence).若某支程式缺乏独立性(dependencies), ldd就会说`statically linked'或是`statically linked (ELF)'.
6.2. Interrogating libraries (`which library is sin() in?')
nm 程式库名称 应该会列出此程式库名称所参考到的所有符号(symbols).这个指令可以应用在静态与共享程式库上.假设你想知道tcgetattr()是在哪儿定义的:你可以如此做
$ nm libncurses.so.1 grep tcget
U tcgetattr
*U*说明了*未定义(undefined)*---也就是说ncurses程式库有用到tegetattr(),但是并没有定义它.你也可以这样做,
$ nm libc.so.5 grep tcget
00010fe8 T __tcgetattr
00010fe8 W tcgetattr
00068718 T tcgetpgrp
*W*说明了*弱势(weak)*,意指符号虽已定义,但可由不同程式库中的另一定义所取代(overridden).而最直接的(straightforward)*正常(normal)*定义(像是tcgetpgrp)是由*T*所标示.
标题所谈的问题,最简明的答案便是libm.(soa)了.所有定义在的函数都保留在maths程式库内;因此,当你用到其中任何一个函数时,都需要以-lm的参数连结此程式库.
6.3. X档案???
ld: Output file requires shared library `libfoo.so.1`
ld与其相类似的命令在搜寻档案的策略(strategy)上,会依据版本的差异而有所不同,但是唯一一个你可以合理假设的内定目录便是/usr/lib了.假如你希望身处它处的程式库也列入搜寻的行列中,那麽你就必须以-L选项告知gcc或是ld.
要是你发现一点效果也没有,就赶紧察看看那档案是不是还乖乖的躺在原地.就a.out而言,以-lfoo参数来连结,会驱使ld去寻找libfoo.sa (shared stubs);假如没有成功,就会换成寻找libfoo.a (static).就ELF而言, ld会先找libfoo.so,然後是libfoo.a.libfoo.so通常是一 个符号连结,连结至libfoo.so.x.
6.4. 建立你自己的程式库(Building your own libraries)
6.4.1. 版本控制(Version control)
与其它任何的程式一样,程式库也有修正不完的bugs的问题存在.它们也可能产生出一些新的特点,更改目前存在的模组的功效,或是将旧的移除掉.这对正在使用它们的程式而言,可能会是一个大问题.假如有一支程式是根据那些旧的特点来执行的话,那怎麽办?
所以,我们引进(introduce)了程式库版本编号(versioning)的观念.我们将程式库*次要(minor)*与*主要(major)*的变更分门别类(categorize),同时我们规定*次要*的变更是不答应用到这程式库的旧程式发生中断的现象(break).你可以从程式库的档名分辨出它的版本(实际上,严格来讲,对ELF而言仅仅是一场天大的谎言;继续读将下去,便可明白为什麽了): libfoo.so.1.2的主要版本是1,次要版本是2.次要版本的编号可能真有其事,也可能什麽都没有---libc在这一点上用了*修正程度 (patch-level)*的观念,而给出了名称像libc.so.5.2.18这样的程式库.次要版本的编号内若是放一些字母,底线,或是任何可以列印的ASCII字元,也是很合理的说.
ELF与a.out格式最主要的差别之一就是在建立共享程式库上.我们先看ELF,因为它比较简单一些.
6.4.2. ELF? 它到底是什麽东东ㄋㄟ?
ELF (Executable and Linking Format) 最初是由USL(UNIX System Laboratories)所发展的二进位格式(binary format),而目前正应用於Solaris与System V Release 4上面.由於ELF所增涨的弹性(flexibility)远远超过Linux过去所用的a.out格式,因此GCC与C程式库的发展人士於去年(1995)决定改用ELF为Linux标准的二进位格式.
6.4.2.1. 怎麽又来了?
这一节是来自於'/news-archives/comp.sys.sun.misc'的文件.
ELF("Executable Linking Format")是於SVR4所引进的新式改良目的档格式.ELF比起COFF可是多出了不少的功能.以ELF而言,它*是*可由使用者自行延伸的 (user-extensible).ELF视一目的档为节区(sections)如串列般的组合,而且此串列可为任意的(arbitrarily)长度 (而不是一固定大小的阵列).这些节区与COFF的不一样,并不需要固定在某个地方,也不需要以某种顺序排列.假如users希望能补捉到新的资料,他们便可以加入新的节区到目的档内.ELF也有一个更强而有力的除错格式,称为DWARF(Debugging With Attribute Record Format)-目前Linux并不完全支援.DWARF DIEs(Debugging Information Entries)的连结串列会在ELF内形成.debug的节区.DWARF DIEs的每一个.debug节区并非一些少量的(small)且固定大小的(fixed-size)资讯记录(information records)的集合(collection),而是一任意长度的串列,拥有复杂的属性,而且程式的资料会以□围为根据的树状资料结构(scope- based tree)写出来.DIEs所能补捉到的大量资讯是COFF的.debug节区无法望其项背的.(像是C++的继续图).
ELF档案是从SVR4(Solaris 2.0 ?)ELF存取程式库(ELF Access library)内存取的.此程式库可提供一简便快速的介面予ELF.使用ELF存取程式库最主要的恩惠之一便是,你不再需要去察看一个ELF档的qua 了.就UNIX的档案而言,它是以Elf*的型式来存取;呼叫elf_open()之後,从此时开始,你只需呼叫elf_Foobar()来处理档案的某一部份(components)即可,并不需要把档案实际在磁碟上的image搞得一团乱.
ELF的优缺点与升级至ELF等级所需经历的种种痛苦(contortions),已在ELF-HOWTO内论及;我并不打算在这儿涂浆糊.ELF HOWTO应该与这份文件相同之处有同样的主题才是.
6.4.2.2. ELF共享程式库
若想建立libfoo.so成为共享程式库,基本的步骤会像下面这样:
$ gcc -fPIC -c *.c
$ gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0 *.o
$ ln -s libfoo.so.1.0 libfoo.so.1
$ ln -s libfoo.so.1 libfoo.so
$ LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH ; export LD_LIBRARY_PATH
这会产生一个名为libfoo.so.1.0的共享程式库,以及给予ld适当的连结(libfoo.so)还有使得动态载入程式(dynamic loader)能找到它(libfoo.so.1).为了进行测试,我们将目前的目录加到LD_LIBRARY_PATH里.
当你津津乐道於程式库制做成功之时, 别忘了把它移到如/usr/local/lib的目录底下,并且重新建立正确的连结路径. libfoo.so.1与libfoo.so.1.0的连结会由ldconfig依日期不断的更新;就大部份的系统来说,ldconfig会在开机程序中执行. libfoo.so的连结必须由手动方式更新.假如你对程式库所有组成份子(如标头档等)的升级,总是抱持著一丝不□的态度(scrupulous),那麽最简单的方法就是让libfoo.so -> libfoo.so.1;如此一来,ldconfig便会替你同时保留最新的连结.要是你没有这麽做,你自行设定的东东就会在数日後造成千奇百怪的花样出现.到时候,可别说我没提醒你啊!
$ su
# cp libfoo.so.1.0 /usr/local/lib
# /sbin/ldconfig
# ( cd /usr/local/lib ; ln -s libfoo.so.1 libfoo.so )
6.4.2.3. 版本编号, soname与符号连结
每一个程式库都有一个soname.当连结器发现它正在搜寻的程式库中有这样的一个名称,连结器便会将soname箝入(embed)连结中的二进位档内, 而不是它正在运作的实际的档名.在程式执行期间,动态载入程式会搜寻拥有soname这样的档名的档案,而不是程式库的档名.因此,一个名为 libfoo.so的程式库,就可以有一个libbar.so的soname了.而且所有连结到libbar.so的程式,当程式开始执行时,会寻找的便是libbar.so了.
这听起来似乎一点意义也没有,但是这一点,对於了解数个不同版本的同一个程式库是如何在单一系统上共存(coexist)的原因,却是要害之钥. Linux程式库标准的命名方式,比如说是libfoo.so.1.2,而且给这个程式库一个libfoo.so.1的soname.假如此程式库是加到标准程式库的目录底下(e.g. /usr/lib),ldconfig会建立符号连结libfoo.so.1 -> libfoo.so.1.2,使其正确的image能於执行期间(run-time)找到.
你也需要连结libfoo.so -> libfoo.so.1,使ld能於连结期间(link-time)找到正确的soname.
所以罗,当你修正程式库内的bugs,或是添加了新的函数进去(任何不会对现存的程式造成不利的(adversely)影响的改变),你会重建此程式库, 保留原本已有的soname,然後更改程式库档名.而当你对程式库的变更会使得现有的程式(binaries)中断(break),那麽你只需增加 soname中的编号---此例中,称新版本为libfoo.so.2.0,而soname变成libfoo.so.2.紧接著,再将libfoo.so 的连结转向新的版本;至此,世界又再度恢复了和平!
其实你不须要以此种方式来替程式库命名,不过这的确是个好的传统(convention).ELF所赋予你在程式库命名上的弹性,会使得人气喘呼呼的搞不清楚状况;有这样的弹性在,也并不表示你就得去用它.
ELF总结:假设经由你睿智的观察发现有个惯例说:程式库主要的升级会破坏(break)相容性(compatibility);而次要的升级则可能不会;那麽以下面的方式来连结,所有的一切就都会相安无事了.
gcc -shared -Wl,-soname,libfoo.so.major -o libfoo.so.major.minor
6.4.3. a.out---旧旧的格式~
建立共享程式库的便利性(ease)是升级至ELF的主要原因之一.那也是说,a.out可能还是有用处在的.上ftp站去抓
ftp://tsx-11.mit.edu/pub/linux/packages/GCC/src/tools-2.17.tar.gz;解压缩後你会发现有20页的文件可以慢慢的读哩.我很不喜欢自己党派的偏见(partisan)表现得那麽的淋璃尽致,可是从上下文间,应该也可以很清楚的嗅出我从来不拿石头砸自己的脚的脾气吧!:-)
6.4.3.1. ZMAGIC vs QMAGIC
QMAGIC是一种类似旧格式的a.out(亦称为ZMAGIC)的可执行档格式,这种格式会使得第一个分页无法map.当0-4096的□围内没有mapping存在时,则可答应NULL dereference trapping更加的轻易.所产生的边界效应(side effect)是你的执行档会比较小(大约是1K左右).
只有即将作废的连结器有支援ZMAGIC,一半已埋入棺材的连结器有支援这两种格式;而目前的版本仅支援QMAGIC而已.事实上,这并没有多大的影响,那是因为目前的核心两种格式都能执行.
*file*命令应该可以确认程式是不是QMAGIC的格式的.
6.4.3.2. 档案配置(File Placement)
一a.out(DLL)的共享程式库包含两个真实的档案与一个符号连结.就*foo*这个用於整份文件做为□例的程式库而言,这些档案会是 libfoo.sa与libfoo.so.1.2;符号连结会是libfoo.so.1,而且会指向libfoo.so.1.2.这些是做什麽用的?
在编译时, ld会寻找libfoo.sa.这是程式库的*stub*档案,而且含有所有执行期间连结所需的exported的资料与指向函数的指标.
执行期间,动态载入程式会寻找libfoo.so.1.这仅仅是一个符号连结,而不是真实的档案,故程式库可更新成较新的且已修正错误的版本,而不会损毁任何此时正在使用此程式库的应用程式.在新版---比如说libfoo.so.1.3---已完整呈现时,ldconfig会以一极微小的操作,将连结指向新的版本,使得任何原本使用旧版的程式不会感到丝毫的不悦.
DLL程式库(我知道这是无谓的反覆(tautology)---所以对我提出告诉吧!)通常会比它们的静态副本(static counterparts)要来得大多了.它们是以*洞(holes)*的形式来保留空间以便日後的扩充.这种*洞*可以不占用任何的磁碟空间.一个简单的cp呼叫,或是使用makehole程式,就可以达到这样效果(achieve).因为它们的位址是固定在同一位置上,所以在建立程式库後,你可以把它们拿掉.千万不要试著夺走ELF的程式库.
6.4.3.3. ``libc-lite''?
libc-lite是轻量级(light-weight)的libc版本.可用来存放在磁碟片上,与替大部份低微的(menial)UNIX任务收尾(suffice).它没有包含curses, dbm, termcap等等的程式码.假如你的/lib/libc.so.4是连结到一个lite的libc,那麽建议你以完整的版本取代它.
6.4.4. 连结:常见的问题
把你连结时所遭遇的问题寄给我!我可能什麽事也不会做,但是只要累积了足够的数量, 我会把它们写起来*.
你想共享,偏偏程式却连结成静态的!
检查你提供给ld的连结是否正确,使ld能找到每一个对应的共享程式库.就ELF而言,这是指一个符号连结libfoo.so,连结至image;就 a.out而言,就是libfoo.sa档了.很多人将ELF binutils 2.5升级至2.6之後,就产生了这个问题---早期的版本搜寻共享程式库时较有聪明,所以并没有将所有的连结建立起来.後来,为了与其它的架构相容,这项布满聪明的行为被人给删除掉了,另外,这样的*聪明*判定错误的机率相当高,所造成的麻烦比它所解决的问题还多,所以留著也是害人精,不如归去兮!
The DLL tool `mkimage' fails to find libgcc, or
从libc.so.4.5.x之後,libgcc已不再是共享的格式.因此,你必须在*-lgcc*出现之处以`gcc -print-libgcc-file-name`取代(完整的倒单引号(back-quotes)).另外,删除所有/usr/lib/libgcc*的档案.这点很重要哩.
__NEEDS_SHRLIB_libc_4 multiply defined messages
是同样的问题所造成的另一种结果.
``Assertion failure'' message when rebuilding a DLL ?
这一条神秘的(cryptic)讯息最有可能的原因是,在原始的jump.vars档案内,由於保留的空间太少, 以致於造成其中一个jump table slots溢满(overflow).你可以执行tools-2.17.tar.gz套件所提供的`getsize'命令,定出所有嫌疑犯(culprit(s))的踪迹.可能唯一的解决方法是,解除(bump)此程式库主要的版本编号,强迫它回到不相容的年代(be backward incompatible).
ld: output file needs shared library libc.so.4
通常这是发生在当你连结的程式库不是libc(如X程式库),而且在命令列用了-g的参数,却没有一并使用-static,所发出的错误讯息.
共享程式库的.sa stubs通常有一个未定义的符号_NEEDS_SHRLIB_libc_4;而这一点可藉由libc.sa stub来解决.然而,以-g来编译时,会使得连结以libg.a或libc.a来结束;因此这个符号一直就没有解决,也就会导致上面的错误讯息了.
总之,以-g的旗号编译时别忘了加上-static,不然就别用-g来连结.通常,以-g编译各个独立的档案时,所获得的除错资讯已经足够,连结时就可以不需要它了.
7. 动态载入(Dynamic Loading)
这一章节目前是简短了一点;当我掠尽ELF HOWTO时,就是这部份再度扩展的时候了.
7.1. 基本概念
Linux有共享程式库,假如之前你已坐著读完上一章节,想必现在一听到像这样的说词,便会马上感到头昏.有一些照惯例而言是在连结时期便该完成的工作(matching-names-to-places),必须延迟到载入时期(load-time)才能完成.
7.2. 错误讯息(Error messages)
把你连结的错误寄给我!我不会做任何的事,不过我可以把它们写起来**
can't load library: /lib/libxxx.so, Incompatible version
(a. out only) 这是指你没有xxx程式库的正确的主要版本.可别以为随随便便弄个连结到你目前拥有的版本就可以了,假如幸运的话, 就只会造成你的程式分页错误而已.去抓新的版本.ELF类似的情况会造成像下面这样的讯息:
ftp: can't load library 'libreadline.so.2'
warning using incompatible library version xxx
(a. out only)你的程式库的次要版本比起这支程式用来编译的还要旧.程式依然可以执行.只是可能啦!我想,升个级应该没什麽伤害吧!
7.3. 控制动态载入器的运作
有一组环境变数会让动态载入器有所反应.大部份的环境变数对ldd的用途要比起对一般users的还要来得更多.而且可以很方便的设定成由ldd配合各种参数来执行.这些变数包括
LD_BIND_NOW --- 正常来讲,函数在呼叫之前是不会让程式寻找(looked up)的.设定这个旗号会使得程式库一载入,所有的寻找(lookups)便会发生,同时也造成起始的时间(startup time)较慢.当你想测试程式,确定所有的连结都没有问题时,这项旗号就变得很有用.
LD_PRELOAD 可以设定一个档案,使其具有*覆盖*(overriding)函数定义的能力.例如,假如你要测试记忆体分配的方略(strategies),而且还想置换*malloc*,那麽你可以写好预备替换的副程式(routine),并把它编译成mallolc.,然後:
$ LD_PRELOAD=malloc.o; export LD_PRELOAD
$ some_test_program
LD_ELF_PRELOAD 与 LD_AOUT_PRELOAD 很类似,但是仅适用於正确的二进位型态.假如设定了 LD_something_PRELOAD 与 LD_PRELOAD ,比较明确的那一个会被用到.
LD_LIBRARY_PATH 是一连串以分号隔离的目录名称,用来搜寻共享程式库.对ld而言,并没有任何的影响;这项只有在执行期间才有影响.
另外,对执行setuid与setgid的程式而言,这一项是无效的.而
LD_ELF_LIBRARY_PATH与LD_AOUT_LIBRARY_PATH这两种旗号可根据各别的二进位型式分别导向不同的搜寻路径.一般正常的运作下,不应该会用到
LD_LIBRARY_PATH;把需要搜寻的目录加到/etc/ld.so.conf/里;然後重新
执行ldconfig.
LD_NOWARN 仅适用於a.out.一旦设定了这一项(LD_NOWARN=true; export
LD_NOWARN),它会告诉载入器必须处理fatal-warnings(像是次要版本不相容等)的警告讯息.
LD_WARN 仅适用於ELF.设定这一项时,它会将通常是致命讯息的"Can*t find library"转换成警告讯息.对正常的操作而言,这并没有多大的用处,可是对ldd就很重要了.
LD_TRACE_LOADED_OBJECTS 仅适用於ELF.而且会使得程式以为它们是由ldd所执行的:
$ LD_TRACE_LOADED_OBJECTS=true /usr/bin/lynx
libncurses.so.1 => /usr/lib/libncurses.so.1.9.6
libc.so.5 => /lib/libc.so.5.2.18
7.4. 以动态载入撰写程式
假如你很熟悉Solaris 2.x所支援的动态载入的工作的话,你会发现Linux在这点上与其非常的相近.这一部份在H.J.Lu的ELF程式设计文件内与dlopen(3)的manual page(可以在ld.so的套件上找到)上有广泛的含盖(cover).这里有个不错的简单□例:以-ldl连结.
#include
#include
main()
{
void *libc;
void (*printf_call)();
if(libc=dlopen("/lib/libc.so.5",RTLD_LAZY))
{
printf_call=dlsym(libc,"printf");
(*printf_call)("hello, world
");
}
}
8. 与发展人士联络
8.1. Bug报表
把问题写下来.这是针对Linux的,亦或是gcc在其它系统上所发生的问题.与kernel的版本相关吗?或者是程式库的版本?假如改用静态方式连结,问题是不是就消失了?你可以节录一小段程式来展示这只bug吗?
当你做了这些事情之後,你将会知道程式内的bugs是什麽.就gcc而言,bug报表程序是以info档来说明的.假如是ld.so或是C,maths程式库,将email寄到linux-gcc@vger.rutgers.edu.假如可能的话,包含一支自己自足的小程式以展示这个bug,而且附上说明,描述你想要让这支程式做些什麽与实际上它又做了些什麽.
8.2. 协助发展
假如你想要帮忙发展gcc或是C程式库,第一件事便是加入linux-gcc@vger.rutgers.edu邮件表列(mailing list).假如你只是想看看mailing list在讨论些什麽,这里有一个表列的archives,位於
http://homer.ncm.com/linux-gcc/.接下来的事,就看你想做什麽了.
9. 结语
9.1. 名人榜
Only presidents, editors, and people with tapeworms have the right to use the editorial ``we''.
(Mark Twain)
这份HOWTO文件几乎完全根植於Mitchum Dsouza的GCC-FAQ; 文件内大部份的资讯(not to mention a reasonable amount of the text)是直接来自於GCC-FAQ的. 这份HOWTO文件用到的第一人称代名词,可视为我们两人其中一个;通常,要是有人说"我还没有测试过这些;假如它烧了(toast)你的硬碟/系统/配偶,可别怪我!",那这样的话适用於我俩身上.
对这份文件有贡献的名人雅士如下所列(以名字的ASCII码的顺序): Andrew
Tefft, Axel Boldt, Bill Metzenthen, Bruce Evans, Bruno Haible, Daniel
Barlow, Daniel Quinlan, David Engel, Dirk Hohndel, Eric Youngdale,
Fergus Henderson, H.J. Lu, Jens Schweikhardt, Kai Petzke, Michael
Meissner, Mitchum DSouza, Olaf Flebbe, Paul Gortmaker, Rik Faith,
Steven S. Dick, Tuomas J Lukka, 当然还有Linux Torvalds,没有了他,这整个运动就会变得一点意义也没有了,所以不可能让他孤单的.:-)
请不要觉得有任何的冒犯之处,假如您的名字没有出现在这儿,而您对这份文件(HOWTO或是FAQ)又曾经有过贡献的话,请email给我,我会改正过来的.
9.2. 翻译
到目前为止,这份文件还没有已知的翻译版出现.假如你希望生一个出来,请尽管去做,不过一定得告诉我相关的事宜.我讲的语言会是你想要翻的语言,那机率(很遗憾)只有好几百分之一,不过还是先把这摆一旁吧!不管是什麽方式,我都会很乐意帮忙的.
9.3. 欢迎任何的回馈(Feedback)
寄信给我dan@detached.demon.co.uk.我的PGP public key (ID 5F263625) 可在我的烘培鸡web pages上使用, 假如你觉得事情有必要保密的话.
9.4. 合法的行迳规定
All trademarks used in this document are acknowledged as being
owned by their respective owners.
This document is copyright (C) 1996 Daniel Barlow
It may be reproduced and distributed in whole or in part, in any
medium physical or electronic, as long as this copyright notice is
retained on all copies. Commercial redistribution is allowed and
encouraged; however, the author would like to be notified of any such
distributions.
All translations, derivative works, or aggregate works
incorporating any Linux HOWTO documents must be covered under this
copyright notice. That is, you may not produce a derivative work from
a HOWTO and impose additional restrictions on its distribution.
Exceptions to these rules may be granted under certain conditions;
please contact the Linux HOWTO coordinator at the address given below.
In short, we wish to promote dissemination of this information
through as many channels as possible. However, we do wish to
retain copyright on the HOWTO documents, and would like to be
notified of any plans to redistribute the HOWTOs.
If you have questions, please contact Greg Hankins, the Linux
HOWTO coordinator, at gregh@sunsite.unc.edu via email.'