The Linux GCC HOWTO中译版V0.2

2. 东东在哪儿?

2.1 GCC-HOWTO在哪儿?

这份文件是Linux HOWTO系列之一,换句话说,你可以在所有存放Linux HOWTO文件的网站上面找到她的芳踪,例如 2.2 GCC相关的数据又在哪儿?

标准的gcc说明文件是随附在发行的源代码(source distribution)内(往下看就有了!),里头有textinfo与.info两种文件。要是你的网络连接速率够快,或是有一片cdrom,不然,有高度的耐心也成,你可以自己把它untar,然后再把相对应的比特一一拷贝到/usr/info的目录底下。假如你的条件与上述的不符,不妨到

libc的文件说明有两种来源。一种是GNU libc,以.info的格式储存,除了stdio之外,其余Linux libc的说明都相当的详尽精确。另一种可以在Linux的archive 2.3 GCC

解答有二:

(a)你可以在 2.4 C程序馆与标头档

该 选哪一套程序馆是取决於(i)你的系统是ELF的或是a.out的;(ii)你希望你的系统变成哪一种?如果你是从libc 4升级到libc 5,那么给你一个良心的建议,先去看看ELF-HOWTO再说。你一定会问,在ELF文件的哪儿呢?嘿!嘿!不偏不倚,就差不多跟这份文件相同的位置。网 站 2.5 有关联的工具 (as, ld, ar, strings etc)

到目前为止,与之前所谈的都一样,从网站 3. GCC的安装与GCC的设定

3.1 GCC的版本

在shell的提示符号下键入gcc -v,屏幕上就会显示出你目前正在使用的GCC的版本。同时这也是一个相当可靠的方法,可以确定你现在所用的是ELF或是a.out。在我的系统上,执行gcc -v的结果是:

$ gcc -vReading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specsgcc version 2.7.2

上面的讯息指出了几件重要的事情:

  • i486 这是指明你现在正在用的gcc是为了486的微处理器而写的---你的电脑可能是386或是586。这3种微处理器的芯片所编译而成的程序代码,彼此间是 可以兼容使用的。差别之处是486的程序代码在某些地方有加上padding的功能,所以可以在486上面跑得比较快。这对386的机器而言,执行程序的 效能并不会有什么不良的影响,只不过真的会让程序代码变得稍稍的大了一些。
  • box 这可以说一点也重要;不过也可能另有所指(像是slackware或者是debian),或者根本什么也不是(所以罗!完整的目录名称是i486-linux)。假如你是实务派的佼佼者,亲自动手建立属於自己的gcc,那么你可以在建立的过程中设定这一项,以装点门面。就像我做的一样:-)
  • linux 其实这是指linuxelf或是linuxaout。这一项会令人引起不必要的困惑,究竟是指哪一种会根据你所用的版本而异。
    • linux 意指ELF若版本序号是2.7.0.(或是更新的版本);否则的话,就是a.out的了。
    • linuxaout 意指a.out的格式。当linux的定义从a.out更换到ELF时,linuxaout就会顺水推舟,摇身一变,成了一个目标物。因此,你不会看到任何版本新於2.7.0.的gcc有linuxaout的。
    • linuxelf 已经过时了。通常这是指2.6.3版的gcc,而且这个版本也可以用来产生ELF的可执行档。要注意的是,gcc 2.6.3版在产生ELF程序代码时会有bugs,所以如果你目前用的恰好是这个版本,建议你赶快升级。
  • 2.7.2 版本的序号。

所以,总结起来,我有2.7.2版的gcc,可以产生ELF格式的程序代码。就这么简单,惊讶吧!eh?

3.2 东东装好後都到哪儿去了?

如果安装gcc时没有仔细的看著屏幕,或者你是从一个完整的发行系统里把gcc单独抓出来安装的话,那么也许你会想知道到底这些东东装好後是住在整个文件系统的那些地方。几个重点如下:

  • /usr/lib/gcc-lib/target/version/ (与子目录)大部份的编译器就是住在这个地方的。在这儿有可执行的程序,实际在做编译的工作;另外,还有一些特定版本的程序库与标头档等也会储存在此。
  • /usr/bin/gcc 指的是编译器的驱动程序---也就是你实际在命令列(command line)上执行的程序。这个目录可供各种版本的gcc使用,只要你用不同的编译器目录(如上所述)来安装就可以了。要知道内定的版本是那一个,在shell提示符号下打gcc -v。要是想强迫执行某个版本,就换打gcc -V version。例如:
    # gcc -vReading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specsgcc version 2.7.2# gcc -V 2.6.3 -vReading specs from /usr/lib/gcc-lib/i486-box-linux/2.6.3/specsgcc driver version 2.7.2 executing gcc version 2.6.3
  • /usr/target/(bin|lib|include)/ 如果你装了数种的目标对象,例如a.out与elf,或是某一种的交叉编译器(cross-compiler)等等,那些属於非主流目标对象(non-native target(s))的程序库,binutils(asld等等)工具与标头档等都可以在这儿找到。即使你只安装了一种gcc,还是可以在这儿找到这些原本就是替它们准备的东东。如果不是在这儿,那么就应该是在/usr/(bin|lib|include)了。
  • /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目录底下的标头档排除在外的话,Linux还有另外3种主要的标头档:

  • /usr/include/与其子目录底下的标头档,大部份都是由H.J.Lu发展的libc套件(libc binary package)所提供的。我会只说‘大部份’的原因,是因为你可能有其它来源的标头档(像是cursesdbm程 序库等等)摆在这儿;尤其是,如果你现在用的是最新的libc发行系统的话(新版本不像旧版那样,已经不再支持curses或dbm了。),那东东之多是 人人为之咋舌的! [译者注:libc binary package意指以二进位形式(machine code)储存之套件,并非源代码(text),若要以中文全称译出,则成‘libc二进位档套件’,似有聱牙之嫌,故略去binary,以libc套件 通称。]
  • 在核心源代码的发行系统内(kernel source distribution) ,/usr/include/linux/usr/include/asm (里头有这些文件:<linux/*.h><asm/*.h>)应该有符号连结(symbolic links)可以连结至目录linux/include/linuxlinux/include/asm。如果你有鸿鹄之志的话,安装这些东东後,就不应该只是拿来编译核心(kernel)而已。 把源代码解压缩(unpacking)後,可能你也会发现,需要在核心的目录(kernel directory)底下做make config的动作。很多的文件都会依赖<linux/autoconf.h>的帮忙,可是这个文件却有可能因版本不同而不存在。若干核心版本里,asm就只是它自己的一个符号连结,仅仅是在make config时才建立出来而已。 [译者注:原文提及autoconf.h时是‘Many files depend on <linux/autoconf.h>,which otherwise may not exist,*’。此处之otherwise之词性应为形容词(adj),指‘另一情况’、‘另一种’、‘不同的’之意,将原文形容词子句拆开来应为:(i) Many files depend on <linux/autoconf.h>. (ii)<linux/autoconf.h> of other condition may not exist. 与下一句互相比对,此处应同指在不同版本之情况下。] 所以,当你在目录/usr/src/linux底下,解开核心的程序代码时,就照著下面指示的做吧!
    $ cd /usr/src/linux$ su# make config(回答接下来的问题。通常回答得正不正确并不重要,除非你打算继续□造核心。)# cd /usr/include# ln -s ../src/linux/include/linux .# ln -s ../src/linux/include/asm .
  • 诸如<float.h><limits.h><varargs.h><stdarg.h><stddef.h>之类的文件,会随著不同的编译器版本而异,属於你‘个人的’文件,可以在 /usr/lib/gcc-lib/i486-box-linux/2.7.2/include/与其它相类似(相同)的目录名称的地方找到。 11/11/97译 5/14/98修正

3.4 建立交叉编译器(Building cross compilers)

将Linux当作标的作业平台(target platform)

假设你已经拿到gcc的源代码,通常你只要依循INSTALL档的指示便可完成一切的设定。 make后面再接configure --target=i486-linux --host=XXX on platform XXX,就能帮你变把戏了。要注意的是,你会需要Linux还有核心的标头档;同时也需要建立交叉组译器(cross assembler)与交叉连结器(cross linker),来源是 4. 移植程序与编译程序

4.1 gcc自行定义的符号

只要执行gcc时,附加 -v这个参数,就能找出你所用的这版gcc,自动帮你定义了什么符号。例如,我的机器看起来会像这样:

$ echo 'main(){printf("hello world/n");}' | gcc -E -v -Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specsgcc 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独有的特性,那么把那些无法移植的程序代码,以条件式编译的前置命令封括起来,可是个不错的主意呢!如下所示∶

#ifdef __linux__/* ... funky stuff ... */#endif /* linux */

__linux__就可以达成目的;看仔细一点,不是linux喔。尽管linux也有定义,毕竟,这个仍然不是POSIX的标准。

4.2 在线求助说明

gcc编译器参数的说明文件是gcc info page(在Emacs内,按下C-h i,然后选‘gcc’的选项)。要是弄不出来,不是卖你CD-ROM的人没把这个东东压给你,不然就是你现在用的是旧版的。遇到这种情况,最好的方法是移动尊臀到archive 旗正飘飘 

在命令列上执行gcc时,只要在它的屁股后面加上-On的选项,就能让gcc乖乖的替你生出最佳编码的机器码。这里的n是一个可有可无的小整数,不同版本的gcc,n的意义与其正确的功效都不一样,不过,典型的范围是从0(不要鸡婆,我不要最佳编码。)变化到2(最佳编码要多一点。),再升级到3(最佳编码要再多一点,多一点)。

gcc在其内部会将这些数字转译成一系列的-f-m的选项。执行gcc时带上旗号-v-Q,你就能很清楚的看出每一种等级的-O是对应到那些选项。好比说,就-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

要是你用的最佳编码等级高於你的编译器所能支持的(e.g. -O6),那么它的效果就跟你用你的编译器所能提供的最高等级的效果是一样的。说实在的,发行出去的gcc程序代码,用在编译时竟是如此处理这等问题,真的不是什么好的构想。日後若是有更进步的最佳编码方法具体整合到新的版本里,而你(或是你的users)还是试著这样做的话,可能就会发现gcc会中断你的程序了。

从gcc 2.7.0升级到2.7.2的users应该注意一点,使用-O2时会有一个bug。更糟糕的是,强度折减参数(strength reduction)居然没有用!要是你喜欢重新编译gcc的话,是有那么一个修正的版本可以更正这项错误;不然的话,一定要确定每次编译时都有加上-fno-strength-reduce喔!

11/12/97译

有个性的微处理器

有一些-m的旗号十分有用处,但是却无法藉由各种等级的-O打开来使用。这之中最重要的有是-m386-m486这两种,用来告诉gcc该把正在编译的程序代码视作专为386或是486机器所写的。不论是用哪一种-m来编译程序代码,都可以在彼此的机器上执行,-m486编译出来的码会比较大,不过拿来在386的机器上跑也不会比较慢就是了。

目前尚无-mpentium或是-m586的旗号。Linus建议我们可以用-m486 -malign-loops=2 -malign-jumps=2 -malign-functions=2来得到最佳编码的486程序代码,这样做正好就可以避免alignment(Pentium并不需要)有过大的gaps发生。Michael Meissner说:

我的第六感告诉我, -mno-strength-reduce(嘿! 要晓得我可不是在谈强度折减参数的bug呀,那已经是另外一个争论的战场了。)一样也可以在x86的机器上产生较快的程序代码,这是因为x86的机器对暂 存器有著不可磨灭的□渴在,而且GCC's method of grouping registers into spill registers vs. other registers doesn't help either。传统上,强度折减的结果会使得编译器去利用加法暂存器以加法运算来取代乘法运算。事实上,我在怀疑 -fcaller-saves可能也只是个漏洞也说不定。
而我的第七感则再度的告诉我说, -fomit-frame-pointer可 能会也可能不会有任何的赚头。从这点来看,就是意谓著有另一个暂存器可以用来处理内存分配的问题。另方面,若纯粹从x86的机器在转换它的指令集成为机器 码的方法上来看,便意谓著堆叠所用到的内存空间要比frame所用到的还要来得多;换句话说,Icache对程序代码而言并没有实质上的帮助,若是阁下用 了 -fomit-frame-pointer的话,同时也是告诉编译器在每次调用函数之后,就必须修正堆叠的指标;然而,就frame来讲,若调用的次数不多的话,则允许堆叠暂时堆积起来。

有关这方面主题的最后一段话仍是来自於Linus:

要注意的是,如果你想要得到最佳状况的执行效能,可千万别相信我的话。无论如何,一定要进行测试。gcc编译器还有许多的参数可用,其中可能就有一种最特别的组合,可以给你最佳编码的结果。

11/14/97译 5/15/98修正

Internal compiler error: cc1 got fatal signal 11

Signal 11是指 SIGSEGV,或者 ‘segmentation violation’。通常这是指说gcc对自己所用的指标感到困惑,而且还尝试著把数据写入不属於它的内存里。所以,这可能是一个gcc的bug。然 而,大体而言,gcc是一支经过严密测试且可靠度良好的软件佳作。它也用了大量复杂的数据结构与惊人的指标数量。简言之,若是要评选本世纪最挑惕与最一丝 不苟的RAM测试程序,gcc绝对可以一摘后冠。假如你无法重新复制这只bug---当你重新开始编译时,错误的讯息并没有一直出现在同一个地方---那几乎可以确定,是你的硬件本身有问题(CPU,内存,主机板或是快取内存).千万不要因为你的电脑可以通过开机程序的测试、或是Windows可以跑得很顺、或者其它什么的,就回过头来大肆宣传说这是gcc的一个bug;你所做的这些测试动作,通常没有什么实际上的价值,这是很合理的结论。另外,也不要因为编译核心时,总是停留在‘make zImage’的阶段,就要大骂这是gcc的bug---当然它会停在那儿啊!做‘make zImage’时,需要编译的文件可能就超过200文件;我们正在研拟一个替代的方案。

如果你可以重覆产生这个bug,而且(最好是这样啦!)可以写一个短小的程序来展示这只bug的话,你就可以把它做成bug报告,然后email给FSF,或者是linux-gcc通信论坛。你可以去参考gcc的说明文件,看看有什么详细的信息,是他们所需要的。

4.3 移植能力

据报,近日来许多正面的消息指出,若是有某件东东到现在都还没移植到Linux上去,那么可以肯定的是,它一定一点价值也没有。:-)

嗯!正经一点。一般而言,源代码只需要做一些局部的修改,就可以克服Linux 100%与POSIX兼容的特质。如果你做了任何的修改,而将此部份传回给原作者,会是很有建设性的举动。这样日後就只需要用到‘make’,就能得到一个可执行的文件了。

BSD教徒 (有 bsd_ioctldaemon<sgtty.h>)

编译程序时,可以配合-I/usr/include/bsd与连结-lbsd的程序库。(例如:在你的Makefile档内,把-I/usr/include/bsd加到CFLAGS那一行;把-lbsd加到LDFLAGS那一行)。如果你真的那么想要BSD格式的信号行为,也需要再加上-D__USE_BSD_SIGNAL了。那是因为当你用了-I/usr/include/bsd与含括了标头档<signal.h>之后,make时就会自动加入了。

失落的封印(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信号,那是因为它们的行为与具体的方式息息相关,而且也 无法进行适当的分类。确认具体方式後,便可以发送这些信号,可是必须以文件说明它们是在什么样的环境底下发送出来的,以及指出任何与它们的发展相关的限 制。”

想要修正这个问题,最简单也是最笨的方法就是用SIGUNUSED重新定义这些信号。正确的方法应该是以条件式的编译#ifdef来处理这些问题才对:

#ifdef SIGSYS/* ... non-posix SIGSYS code here .... */#endif

11/15/97译 5/22/98修正

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会打开-fwritable-strings,使得字串常数移至数据内存空间内(从程序代码内存空间移出来,这个地方是不能任意写入的)。这样做会让程序代码的内存空间无形中增加的。

前置处理器的符号卯上函数原型宣言

最常见的问题是,如众所皆知,Linux中有许多常用的函数都定义成宏存放在标头档内,此时若有相似的函数原型宣言出现在程序代码内,前置处理器会拒绝进行语法分析的前置作业。常见的有atoi()atol()

sprintf()

在大部份的Unix系统上,sprintf(string, fmt, ...)传回的是string的指标,然而,这方面Linux(遵循ANSI)传回的却是放入string内的字符数目.进行移植时,尤其是针对SunOS,需有警觉的心。

fcntl 与相关的函数;FD_*家族的定义到底摆在哪里?

就在<sys/time.h>里头。 为了真正的原型宣言,当你用了fcntl,可能你也想含括标头档<unistd.h>进来。

一般而言,函数的manual page会在SYNOPSIS章节内列出需要的标头档

select()的计时---程序执行时会处於忙碌-等待的状态

很久很久以前,,select()的计时参数只有唯读的性而已。即使到了最近,manual pages仍然有下面这段的警告:

select()应该是藉由修正时间的数值(如果有的话),再传回自原始计时开始後所剩馀的时间。未来的版本可能会使这项功能实现。因此,就目前而言,若以为调用select()之后,计时指标仍然不会被修正过,可是一种非常不明智的想法喔!

未来就在我们的眼前了!至少,在这儿你绝对可以看到。函数select()传回的,是扣除等待尚未到达的数据所耗费的时间後,其剩馀的时间数值。如果在计时结束时,都没有数据传送进来,计时引数便会设为0;如果接著还有任何的select(),以同样的计时structure来调用,那么select()便会立刻结束。

若要修正这项问题,只要每次调用select()前,都把计时数值放到计时 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的屏幕右上角,是不是有个圆圆的、会旋转的地球动画。那颗球转得愈快,就表示数据从网络上传送过来的速率愈慢!

产生中断的系统调用

特徵:

当一支程序以Ctrl-Z中止、然后再重新执行时□或者是其它可以产生Ctrl-C中断信号的情况,如子程序的终结等□系统就会抱怨说"interrupted system call"或是"write: unknown error",或者诸如此类的讯息。

问题点:

POSIX的系统检查信号的次数,比起一些旧版的Unix是要多那么一点。如果是Linux,可能就会执行signal handlers了□

  • 非同步地(计时器的滴答声)
  • 系统调用的传回值
  • 在下列系统调用的执行期间∶ 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.

就其它的操作系统而言,你需要的可能就是下面这些系统调用了: creat(), close(), getmsg(), putmsg(), msgrcv(), msgsnd(), recv(), send(), wait(), waitpid(), wait3(), tcdrain(), sigpause(), semop() to this list.

在系统调用期间,若有一信号(那支程序本身应准备好handler因应了)产生,handler就会被调用。当handler将控制权转移回系统调用时,它会侦测出它已经产生中断,而且传回值会立刻设定成-1,而errno设定成EINTR。程序并没有想到会发生这种事,所以就挂了。

有两种修正的方法可以选择:

(1) 对每个你自行安装的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()时,你仍然得自行检查EINTR。如下所示:

(2) 你自己得很明确地检查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,其内定的行为是重新执行系统调用。若要让系统调用中断,得使用 SV_INTERRUPTSA_INTERRUPT旗号。

可以写入的字串

gcc 对其users总怀抱著乐观的想法,相信当他们打算让某个字串当作常数来用时---那它就真的只是字串常数而已。因此,这种字串常数会储存在程序代码的内 存区段内。这块区域可以page到磁盘机的image上,避免耗掉swap的内存空间,而且任何尝试写入的举动都会造成分页的错误 (segmentation fault)。这可是一种特色呢!

对老旧一点的程序而言,这可能会产生一个问题。例如,调用mktemp(),传递的引数(arguments)是字串常数。 mktemp()会尝试著在*适当的位置*重新写入它的引数。

修正的方法不外乎(a)以-fwritable-strings编译,迫使gcc将此常数置放在数据内存空间内;或者(b)将侵犯地权的部份重新改写,配置一个不为常数的字串,在调用前,先以strcpy()将数据拷贝进去。

为什么调用execl()会失败?

那是因为你调用的方式不对。execl的第一个引数是你想要执行的程序名.第二个与接续的引数会变成你所调用的程序的argv数组。记住:传统上,argv[0]是只有当程序没有带著引数执行时,才会有设定值。所以罗,你应该这样写:

execl("/bin/ls","ls",NULL);

而不是只有,

execl("/bin/ls", NULL);

执行程序而不带任何引数,可解释成是一种邀请函,目的是把此程序的动态程序库独立的特性印出来。至少,a.out是这样的。就ELF而言。事情就不是这样了.

(如果你想得知此程序库的信息,有一些更简单的界面可用;参考动态载入那一章节,或是ldd的manual page。)

5. 调试与监管

5.1 预防重於治疗(lint)

lint对Linux而言并没有很广泛的用途,主要是因为大部份的人都能满足於gcc所提供的警告讯息。可能最有用的就是-Wall参数了---这个参数的用途是要求gcc将所有的警告讯息显现出来;but probably has more mnemonic value if thought of as the thing you bang your head against.

网络上有一个实用的public domain lint,位於 5.2 调试

我要怎样做才能将调试信息放到一支程序里头?

你需要添加-g的参数来编译与连结程序,而且不可以用-fomit-frame-pointer参数。事实上,你不需要重新编译所有的程序,只需重新编译目前你正在调试的部份即可。

就a.out的组态而言,共享程序库是以-fomit-frame-pointer编译而成,这个时候,gdb就变得英雄无用武之地了。连结时给定-g的选项,应该就隐含著静态连结的意义了;这就是为什么要加-g的原因了。

如果连结器连结失败,告诉你找不到libg.a,那就是在/usr/lib/的目录底下,少了libg.a。libg.a是一个C语言很特别的调试程序库。一般在libc的套件内就会提供libg.a;不然的话(新版是这样的),你可能需要拿libc的源代码自己设置了,不过,实际上你应该不需要才对。不管是什么目的,大部份的情况下,只需将libg.a连结到/usr/lib/libc.a,你就能得到足够的信息了。

那,能不能把调试信息给拿掉?

很多的GNU软件在编译连结时,都会设定-g的选项;这样做会造成执行档过大的问题(通常是静态的连结)。实际上,这并不是一个很热门的想法。

如果程序本身有autoconf,产生了configure脚本文件,通常你就可以用./configure CFLAGS=或是./configure CFLAGS=-O2来关掉调试信息。不然的话,你得检查检查Makefile了。当然啦,假如你用的是ELF,程序便会以动态的方式来连结,不论是否有-g的设定;因此你可以平常心把-g拿掉

实用的软件

据了解,大部份的人都是用gdb来调试。你可以从

核心文件

当Linux开机时,通常组态会设定成不要产生核心文件。要是你那么喜欢它们的话,可以用shell的builtin命令使其重新生效:就C-shell兼容的shell(如tcsh)而言,会是下面这样:

% limit core unlimited

而类似Bourne shell的shell(sh, bash, zsh, pdksh)则使用下面的语法:

$ ulimit -c unlimited

如 果你想要有个多才多艺的核心档命名(core file naming)(for example, if you're trying to conduct a post-mortem using a debugger that's buggy itself),那么你可以对你的核心程序做一点小小的更动。找一找fs/binfmt_aout.cfs/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] = '/0';#endif

0换成1.

5.3 监管

监管(Profiling)是用来检核一支程序中那些部份是最常调用或是执行的时间最久的方法。这对程序的最佳化与找出何时时间是浪费掉的而言,是相当好的方式。你必须就你所要的时程信息(timing information)的目的档加上-p来编译,而且如果要让输出的文件有意义,你也会需要gprof(来自binutils套件的命令)。参阅gprof的manual page,可得知其细节。

6. 连结

由 於静态与共享程序库两者间不兼容的格式的差异性与动词*link*过量使用于指称*编译完成後的事情*与*当编译好的程序使用时所发生的事情*这两件事上 头,使得这一章节变得复杂了许多。( and, actually, the overloading of the word `load' in a comparable but opposite sense)不过,再复杂也就是这样了,所以阁下不必过於担心。

为了稍微减轻读者的困惑,我们称执行期间所发生的事为*动态载入*,这一主题会在下一章节中谈到。你也会在别的地方看到我把动态载入描述成*动态连结*,不过不会是在这一章节中。换句话说,这一章节所谈的,全部是指发生在编译结束後的连结。

6.1 共享程序库 vs静态程序库

建 立程序的最后一个步骤便是连结;也就是将所有分散的小程序组合起来,看看是否遗漏了些什么。显然,有一些事情是很多程序都会想做的---例如,开启文件, 接著所有与开档有关的小程序就会将储存程序库的相关文件提供给你的程序使用。在一般的Linux系统上,这些小程序可以在/lib/usr/lib/目录底下找到。

当 你用的是静态的程序库时,连结器会找出程序所需的模组,然后实际将它们拷贝到执行档内。然而,对共享程序库而言,就不是这样了。共享程序库会在执行档内留 下一个记号,指明*当程序执行时,首先必须载入这个程序库*。显然,共享程序库是试图使执行档变得更小,等同於使用更少的内存与磁盘空间。Linux内定 的行为是连结共享程序库,只要Linux能找到这些共享程序库的话,就没什么问题;不然,Linux就会连结静态的了。如果你想要共享程序库的话,检查这 些程序库(*.sa for a.out, *.so for ELF)是否住在它们该在的地方,而且是可读取的。

在Linux上,静态程序库会有类似libname.a这样的名称;而共享程序库则称为libname.so.x.y.z,此处的x.y.z是指版本序号的样式。共享程序库通常都会有连结符号指向静态程序库(很重要的)与相关联的.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浏览器*lynx*会依赖libc.so.5 (the C library)与libncurses.so.1(终端机屏幕的控制)的存在。若某支程序缺乏独立性, ldd就会说‘statically linked’或是‘statically linked (ELF)’。

6.2 终极审判(‘sin() 在哪个程序库里?’)

nm 程序库名称应该会列出此程序库名称所参考到的所有符号。这个指令可以应用在静态与共享程序库上。假设你想知道tcgetattr()是在哪儿定义的:你可以如此做,

$ nm libncurses.so.1 |grep tcget         U tcgetattr

*U*指出*未定义*---也就是说ncurses程序库有用到tegetattr(),但是并没有定义它。你也可以这样做,

$ nm libc.so.5 | grep tcget00010fe8 T __tcgetattr00010fe8 W tcgetattr00068718 T tcgetpgrp

*W*说明了*弱态(weak)*,意指符号虽已定义,但可由不同程序库中的另一定义所替代。最简单的*正常*定义(像是tcgetpgrp)是由*T*所标示:

标题所谈的问题,最简明的答案便是libm.(so|a)了。所有定义在<math.h>的函数都保留在maths程序库内;因此,当你用到其中任何一个函数时,都需要以-lm的参数连结此程序库。

6.3 X文件?

ld: Output file requires shared library `libfoo.so.1`

ld与其相类似的命令在搜寻文件的策略上,会依据版本的差异而有所不同,但是唯一一个你可以合理假设的内定目录便是/usr/lib了。如果你希望身处它处的程序库也列入搜寻的行列中,那么你就必须以-L选项告知gcc或是ld。

要是你发现一点效果也没有,就赶紧察看看那文件是不是还乖乖的躺在原地。就a.out而言,以-lfoo参数来连结,会驱使ld去寻找libfoo.sa(shared stubs);如果没有成功,就会换成寻找libfoo.a(static)。就ELF而言, ld会先找libfoo.so,然后是libfoo.alibfoo.so通常是一个连结符号,连结至libfoo.so.x

6.4 建立你自己的程序库

控制版本

与其它任何的程序一样,程序库也有修正不完的bugs的问题存在。它们也可能产生出一些新的特点,更改目前存在的模组的功效,或是将旧的移除掉。这对正在使用它们的程序而言,可能会是一个大问题。如果有一支程序是根据那些旧的特点来执行的话,那怎么办?

所 以,我们引进了程序库版本编号的观念。我们将程序库*次要*与*主要*的变更分门别类,同时规定*次要*的变更是不允许用到这程序库的旧程序发生中断的现 象。你可以从程序库的文件名分辨出它的版本(实际上,严格来讲,对ELF而言仅仅是一场天大的谎言;继续读将下去,便可明白为什么了): libfoo.so.1.2的主要版本是1,次要版本是2。次要版本的编号可能真有其事,也可能什么都没有---libc在这一点上用了*修正程度*的观念,而订出了像libc.so.5.2.18这样的程序库名称。次要版本的编号内若是放一些字母、底线、或是任何可以打印的ASCII字符,也是很合理的。

ELF与a.out格式最主要的差别之一就是在设置共享程序库这件事上;我们先看ELF,因为它比较简单一些。

ELF?它到底是什么东东ㄋㄟ?

ELF (Executable and Linking Format)最初是由USL(UNIX System Laboratories)发展而成的二进位格式,目前正应用于Solaris与System V Release 4上。由於ELF所增涨的弹性远远超过Linux过去所用的a.out格式,因此GCC与C程序库的发展人士於1995年决定改用ELF为Linux标准 的二进位格式。

怎么又来了?

这一节是来自於‘/news-archives/comp.sys.sun.misc’的文件。

ELF (“Executable Linking Format”)是於SVR4所引进的新式改良目的档格式。ELF比起COFF可是多出了不少的功能。以ELF而言,它*是*可由使用者自行延伸的。 ELF视一目的档为节区(sections),如串行般的组合;而且此串行可为任意的长度(而不是一固定大小的数组)。这些节区与COFF的不一样,并不 需要固定在某个地方,也不需要以某种顺序排列。如果使用者希望捕捉到新的数据,便可以加入新的节区到目的档内。ELF也有一个更强而有力的调试法式,称为 DWARF(Debugging With Attribute Record Format)□目前Linux并不完全支持。DWARF DIEs(Debugging Information Entries)的连结串行会在ELF内形成 .debug的节区。DWARF DIEs的每一个 .debug节区并非一些少量且固定大小的信息记录的集合,而是一任意长度的串行,拥有复杂的属性,而且程序的数据会以有范围限制的树状数据结构写出来。 DIEs所能捕捉到的大量信息是COFF的 .debug节区无法望其项背的。(像是C++的继承图。)
ELF 文件是从SVR4(Solaris 2.0 ?)ELF存取程序库(ELF access library)内存取的。此程序库可提供一简便快速的界面予ELF。使用ELF存取程序库最主要的恩惠之一便是,你不再需要去察看一个ELF档的qua 了。就UNIX的文件而言,它是以Elf*的型式来存取;调用elf_open()之后,从此时开始,你只需调用elf_foobar()来处理文件的某 一部份即可,并不需要把文件实际在磁盘上的image搞得一团乱。

ELF的优缺点与升级至ELF等级所需经历的种种痛苦,已在ELF-HOWTO内论及;我并不打算在这儿涂浆糊。ELF HOWTO应该与这份文件有同样的主题才是。

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.1libfoo.so.1.0的连结会由ldconfig依日期不断的更新,就大部份的系统来说,ldconfig会在开机过程中执行。libfoo.so的连结必须由手动方式更新。如果你对程序库所有组成份子(如标头档等)的升级,总是抱持著一丝不苟的态度,那么最简单的方法就是让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 )

版本编号、soname与符号连结

每一个程序库都有一个soname。当连结器发现它正在搜寻的程序库中有这样的一个名称,连结器便会将soname箝入连结中的二进位档内,而不是它正在运作的实际的文件名。在程序执行期间,动态载入程序会搜寻拥有soname这样的文件名的文件,而不是程序库的文件名。因此,一个名为libfoo.so的程序库,就可以有一个libbar.so的soname了。而且所有连结到libbar.so的程序,当程序开始执行时,会寻找的便是libbar.so了。

这听起来好像一点意义也没有,但是这一点,对于了解数个不同版本的同一个程序库是如何在单一系统上共存的原因,却是关键之钥。Linux程序库标准的命名方式,比如说是libfoo.so.1.2,而且给这个程序库一个libfoo.so.1的soname。如果此程序库是加到标准程序库的目录底下(e.g. /usr/lib),ldconfig会建立符号连结libfoo.so.1 -> libfoo.so.1.2,使其正确的image能於执行期间找到。你也需要连结libfoo.so -> libfoo.so.1,使ld能於连结期间找到正确的soname。

所 以罗,当你修正程序库内的bugs,或是添加了新的函数进去(任何不会对现存的程序造成不利的影响的改变),你会重建此程序库,保留原本已有的 soname,然后更改程序库文件名。当你对程序库的变更会使得现有的程序中断,那么你只需增加soname中的编号---此例中,称新版本为libfoo.so.2.0,而soname变成libfoo.so.2。紧接著,再将libfoo.so的连结转向新的版本;至此,世界又再度恢复了和平!

其实你不须要以此种方式来替程序库命名,不过这的确是个好的传统。ELF赋予你在程序库命名上的弹性,会使得人气喘呼呼的搞不清楚状况;有这样的弹性在,也并不表示你就得去用它。

ELF总结:假设经由你睿智的观察发现有个惯例说:程序库主要的升级会破坏兼容性;而次要的升级则可能不会;那么以下面的方式来连结,所有的一切就都会相安无事了。

gcc -shared -Wl,-soname,libfoo.so.major -o libfoo.so.major.minor

a.out---旧旧的格式□

建立共享程序库的便利性是升级至ELF的主要原因之一。那也是说,a.out可能还是有用处在的。上ftp站去抓 ZMAGIC vs QMAGIC

QMAGIC 是一种类似旧格式的a.out(亦称为ZMAGIC)的可执行档格式,这种格式会使得第一个分页无法map。当0-4096的范围内没有mapping存 在时,则可允许NULL dereference trapping更加的容易。所产生的边界效应是你的执行档会比较小(大约少1K左右)。

只有即将作废的连结器有支持ZMAGIC,一半已埋入棺材的连结器有支持这两种格式;而目前的版本仅支持QMAGIC而已。事实上,这并没有多大的影响,那是因为目前的核心两种格式都能执行。

*file*命令应该可以确认程序是不是QMAGIC的格式的。

文件配置

一a.out(DLL)的共享程序库包含两个真实的文件与一个连结符号。就*foo*这个用于整份文件做为范例的程序库而言,这些文件会是libfoo.salibfoo.so.1.2;连结符号会是libfoo.so.1,而且会指向libfoo.so.1.2。这些是做什么用的?

在编译时,ld会寻找libfoo.sa。这是程序库的*stub*文件。而且含有所有执行期间连结所需的exported的数据与指向函数的指标。

执行期间,动态载入程序会寻找libfoo.so.1。这仅仅是一个符号连结,而不是真实的文件。故程序库可更新成较新的且已修正错误的版本,而不会损毁任何此时正在使用此程序库的应用程序。在新版---比如说libfoo.so.1.3---已完整呈现时,ldconfig会以一极微小的操作,将连结指向新的版本,使得任何原本使用旧版的程序不会感到丝毫的不悦。

DLL程序库(我知道这是无谓的反覆---所以对我提出诉讼吧!)通常会比它们的静态副本要来得大多。它们是以*洞(holes)*的形式来保留空间以便日後的扩充。这种*洞*可以不占用任何的磁盘空间。一个简单的cp调用,或是使用makehole程序,就可以达到这样效果。因为它们的地址是固定在同一位置上,所以在建立程序库後,你可以把它们拿掉。不过,千万不要试著拿掉ELF的程序库

``libc-lite''?

libc-lite是轻量级的libc版本。可用来存放在磁盘片上,也可以替大部份低微的UNIX任务收尾。它没有包含curses, dbm, termcap等等的程序代码。如果你的/lib/libc.so.4是连结到一个lite的libc,那么建议你以完整的版本取代它。

连结:常见的问题

把你连结时所遭遇的问题寄给我!我可能什么事也不会做,但是只要累积了足够的数量,我会把它们写起来*。

你想共享,偏偏程序却连结成静态的!

检查你提供给ld的连结是否正确,使ld能找到每一个对应的共享程序库,就ELF而言,这是指一个符号连结libfoo.so,连结至image;就a.out而言,就是libfoo.sa档 了。很多人将ELF binutils 2.5升级至2.6之后,就产生了这个问题---早期的版本搜寻共享程序库时较有智慧,所以并没有将所有的连结建立起来。後来,为了与其它的架构兼容,这 项充满智慧的行为被人给删除掉了,另外,这样的*智慧*判断错误的机率相当高,所造成的麻烦比它所解决的问题还多,所以留著也是害人精;不如归去兮!

DLL的工具程序‘mkimage’找不到libgcc?

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 ?

这一条神秘的讯息最有可能的原因是,在原始的jump.vars文件内,由於保留的空间太少,以致於造成其中一个jump table slots溢满。你可以执行工具程序□由2.17.tar.gz套件所提供的‘getsize’命令,定出所有嫌疑犯的踪迹。可能唯一的解决方法是,解除此程序库主要的版本编号,强迫它回到不兼容的年代。

ld: output file needs shared library libc.so.4

通常这是发生在当你连结的程序库不是libc(如X程序库),而且在命令列用了-g的参数,却没有一并使用-static,所发出的错误讯息。

共享程序库的.sa stubs通常有一个未定义的符号_NEEDS_SHRLIB_libc_4;这一点可藉由libc.sa stub来解决,然而,以-g来编译时,会使得连结以libg.alibc.a来结束;因此这个符号一直就没有解决,也就会导致上面的错误讯息了。

总之,以-g的旗号编译时别忘了加上-static,不然就别用-g来连结。通常,以-g编译各个独立的文件时,所获得的调试信息已经足够,连结时就可以不需要它了。

7. 动态载入

这一章节目前是简短了一点;当我掠尽ELF HOWTO时,就是这部份再度扩展的时候了。

7.1 基本概念

Linux有共享程序库,如果之前你已坐著读完上一章节,想必现在一听到像这样的说词,便会立刻感到头昏。有一些照惯例而言是在连结时期便该完成的工作,必须延迟到载入时期才能完成。

7.2 错误讯息

把你连结的错误寄给我!我不会做任何的事,不过我可以把它们写起来**

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 --- 正常来讲,函数在调用之前是不会让程序寻找的。设定这个旗号会使得程序库一载入,所有的寻找便会发生,同时也造成起始的时间较慢。当你想测试程序,确定所有的连结都没有问题时,这项旗号就变得很有用。
  • LD_PRELOAD可以设定一个文件,使其具有*覆盖*函数定义的能力。例如,如果你要测试内存分配的方略,而且还想置换*malloc*,那么你可以写好准备替换的副程序,并把它编译成mallolc.,然后:
    $ LD_PRELOAD=malloc.o; export LD_PRELOAD$ some_test_program
    LD_ELF_PRELOADLD_AOUT_PRELOAD 很类似,但是仅适用于正确的二进位格式。如果设定了 LD_something_PRELOADLD_PRELOAD ,比较明确的那一个会被用到。
  • LD_LIBRARY_PATH是一连串以分号隔离的目录名称,用来搜寻共享程序库。对ld而言,并没有任何的影响;这项只有在执行期间才有影响。另外,对执行setuid与setgid的程序而言,这一项是无效的。而LD_ELF_LIBRARY_PATHLD_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的套件上找到)上有广泛的讨论。这里有个不错的简单范例:以-ldl连结。

#include <dlfcn.h>#include <stdio.h>main(){  void *libc;  void (*printf_call)();  if(libc=dlopen("/lib/libc.so.5",RTLD_LAZY))  {    printf_call=dlsym(libc,"printf");    (*printf_call)("hello, world/n");  }}




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通信论坛。如果你只是想看看通信论坛在讨论些什么,这里有一个论坛的archives,位於

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值