龙盟编程博客 | 无障碍搜索 | 云盘搜索神器
快速搜索
主页 > 软件开发 > C/C++开发 >

Gcc HowTo

时间:2009-12-22 15:42来源:未知 作者:admin 点击:
分享到:
1. 生火上路(Preliminaries)! 1.1. ELF vs. a.out 目前,Linux的发展正波涛汹涌的进行著.简单一点讲,Linux有两种执行档的格式(formats)可用,取决於你的系统是怎麽整合起来的;你可能两种都有.读了这份

  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程式库的正确的主要版本.可别以为随随便便弄个连结到你目前拥有的版本就可以了,假

精彩图集

赞助商链接