荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: Ohoh (Linux), 信区: Linux
标  题: GCC HOWTO中译版V0.2 --- 4. 移植程式与编译程式
发信站: 荔园晨风BBS站 (Thu Nov 22 23:19:52 2001), 转信

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/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独有的特性,那麽把那些无法移植的程式
码,以条件式编译的前置命令封括起来,可是个不错的主意呢!如下所示∶



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

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



4.2 线上求助说明
gcc编译器参数的说明文件是gcc info page(在Emacs内,按下C-h i,然後选‘
gcc’的选项)。要是弄不出来,不是卖你CD-ROM的人没把这个东东压给你,不然
就是你现在用的是旧版的。遇到这种情况,最好的方法是移动尊臀到archive
ftp://prep.ai.mit.edu/pub/gnu或是它的mirrors站台,去把gcc的原始档案抓回
家,重新烹饪一番。

gcc manual page(gcc.1) 可以说是已经过时了,要是你吃饱了撑著没事干硬是
想看,它就会告诉你说别无聊了。


旗正飘飘
在命令列上执行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_ioctl、daemon 与 <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_INTERRUPT或SA_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。)

--
                ╔═══════════════════╗
                ║  欢迎来  深圳大学荔园晨风  Linux 版  ║
                ╚═══════════════════╝

※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.1.119]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店