文档库 最新最全的文档下载
当前位置:文档库 › C专家编程6

C专家编程6

C专家编程6
C专家编程6

C专家编程

·它有助于优化代码,获得最佳的效率。

·它有助于理解更高级的材料。

·当陷入麻烦时,它可以使分析问题更加容易。

6。1 a.OUl及其传说

6.2段

"7

C专家编程

●●●

l

1

1

1

j

1

0x00000028

0x00000004

0x00000004

0x00000058

0x00000038

Type

OBJT

OBJT

OBJT

OBJT

FUNC

FUNC

Bind Segment Name

LOCL

GLOB I

GLOB l

LOCL I

GLOB I

GLOB I

图6—1显示了编译器和链接器分别在这些段中写入了什么东西118

关键

.bss

.J3SS

.data

.data

.text

UNDEF

peach

Dear

mango

melon

maln

malloc

i……◆局部变量并不进入a-out,它们在运行时创建。

第6章运动的诗章:运行时数据结构

}

6.3操作系统在a.out文件里千了些什么 120

C专家编程

6.5当函数被调用时发生了佧么:过程活动记录

第6章运动的诗章:运行时数据结构

char+favorite—fruit() {

char deciduous[】=”apple”;

return deciduous;

}

当进入该函数时,自动变量deciduous在堆栈中分配。当函数结束后,变量不复存在,它所占用的堆栈空间被回收,可能在任何时候被覆盖。这样,指针就失去了有效性(引用不存在的东西),被称为“悬垂指针(dangling pointer)”——它们并不引用有用的东西,而是悬

在地址空间内。如果想返回一个指向在函数内部定义的变量的指针时,要把那个变量声明为static。这样就能保证该变量被保存在数据段中而不是堆栈中。该变量的生命期就和程序一样

长,当定义该变量的函数退出时,该变量的值依然能保持。当该函数下一次进入时,该值依然有效。

存储类型说明符aut0关键字在实际中从来用不着。它通常由编译器设计者使用,用于标

记符号表的条目——它表示“在进入该块后,自动分配存储”(与编译时静态分配或在堆上动

态分配不同)。对于其他程序员来说,aut0关键字几乎没什么用处,因为它只能用于函数内部。

但是在函数内部声明的数据缺省就是这种分配。惟一能用到aut0的地方就是使你的声明更加

清楚整齐,例如:

register int filbert;

auto int almond;

static int hazel j

而不是:

过程活动记录可能并不位于堆栈中

尽管我们谈到了“将过程活动记录压到堆栈中”,但过程活动记录并不一定要存在于堆栈

中。事实上,尽可能地把过程活动记录的内容放到寄存器中会使函数调用的速度更快,效果更好。SPARC架构引入了一个概念,称为“寄存器窗I](registerwindow)”,CPU拥有一组寄

存器,它们只用于保存过程活动记录中的参数。每当函数调用时,空的活动记录依然压入到堆栈中。当函数调用链非常深而寄存器窗口不够用时,寄存器的内容就会被保存到堆栈中保留的活动记录空间中,以便重新利用这些寄存器。

有些语言,如Xerox PARC的Mesa和Cedar,它们的过程活动记录以链表的形式分配在堆中。在PL/I最早的编译器中,用于递归过程的过程活动记录也是分配在堆中(导致了性

差的批评,因为在通常情况下,从堆栈中获取内存的速度更快一些)。

127

●,

C专家编程

6。7控锻线程

现在,对于如何在进程中支持不同的控制线程(以前称为“轻量级进程”)是比较清楚的了,只要简单地为每个控制线程分配不同的堆栈即可。如果线程函数to00调用了bar(),而后

者又调用了baz0,而主程序此时正执行其他的程序,它们中的每一个都需要自己的堆栈来保

存自己所处的位置。每个线程的堆栈为1Mb(当需要时增长),在各个线程的堆栈间有一个red zone页。线程是一种非常强大的编程模式,即使在单个处理器上也可以提高性能。然而,

本书是关于C语言的,.而不是关于线程的。你应该参阅其他书,了解更多有关线程的细节。6.8 setjmp耱longjmp

现在可以讨论一下s叫mp0和longjmp()的用途,因为它们是通过操纵过程活动记录实现

的。许多程序员新手并不知道这个强大的机制,因为它是c语言所独有的。它们部分弥补了C语言有限的转移能力。这两个函数协同工作,如下所示:

·setjmp(jmp buf j)必须首先被调用。它表示“使用变量J记录现在的位置。函数返回

零.”

·longimp(jmp_bufj,int i)可以接着被调用。它表示“回到J所记录的位置,让它看上去

像是从原先的setjmp()i蚕l数返回一样。但是函数返回i,使代码能够知道它是实际上是通过

longjmp0返回的。”拗不拗口?

·当使用于longjmp0时,J的内容被销毁。

setjmp保存了一份程序的计数器和当前的栈顶指针。如果喜欢也可以保存一些初始值。longjmp恢复这些值,有效地转移控制并把状态重置回保存状态的时候。这被称作“展开堆栈(unwinding stack)”,因为你从堆栈中展开过程活动记录,直到取得保存在其中的值。尽管

longjmp会导致转移,但它和goto又有不同,区别如下:

·got0语句不能跳出C语言当前的函数(这也是“longjmp”取名的由来,它可以跳得很远,甚至可以跳到其他文件的函数中)。

·用longimp只能跳回到曾经到过的地方。在执行setjmp的地方仍留有一个过程活动记

录。从这个角度讲,longjmp更像是“从何处来(come from)”而不是“往哪里去(go to)”。longjmp

接受一个额外的整型参数并返回它的值,这可以知道是由longjmp转移到这里的还是从上一

条语句执行后自然而然来到这里的。

下面的代码显示了setjmp0和longjmp()--例。 #incl.ude1

Jmp buf buf;

128

printf(’’in banana()\n”);

longjmp(buf,1)j

/*以下代码不会被执行*/

printf(”you 7 ll never see this,

)

。 o

》:l main()

毫 {

+ if(setjmp(buf))

“printf(”back in main\n”);

else(

printf(--first time through\n”),

banana();

)

。) ‘

输出结果如下:

右a.0U【

first time through

in banana()

back in main

需要注意的地方是:保证局部变量在longjmp过程中一直保持它的值的惟一可靠方法是把它声明为volatile(这适用于那些值在setjmp执行和longjmp返回之间会改变的变量)。 setjmp/10ngjmp最大的用途是错误恢复。只要还没有从函数中返回,一旦发现一个不可恢复的错误,可以把控制转移到主输入循环,并从那里重新开始。有些人使用setjmp/longjmp

从一串无数的函数调用中立即返回。还有一些人用它们防范潜在的危险代码,例如,当对下面例子中的可疑指针进行解除引用操作时,

switch(setjmp(Jbuf)){

case 0:

apple=+suspicious;

break;’

case l:

printf f”suspicious is a bad pointer\n”);

break;

default:

die f”unexpected value returned by setjmp”);

)

这里需要一个处理程序来处理段违规信号,后者进行相应的longimp(jbuf,1)操作,具体

内容在下一章解释。setjmp和longjmp在c++中变异为更普通的异常处理机制“catch”和“throw”。

129

C专家编程

跳向它

在已经编写好的程序源文件中增加setjmp/longjmp,使得程序在接受某些特别的输入时

会重新开始。

在使用setjmp和longjmp的任何源文件中,必须包含头文件

像got0一样,setjmp和longjmp使得程序难以理解和调试。如果不是出于特殊需要,最

好避免使用它们。

6.9 UNIX中的堆栈段

第6章运动的诗章:运行时数据结构

6.11有用的C语言工具

本节包括了一些你应该知道的有用的C语言工具列表,并描述了它们的作用,从表6—1 至6-4。我们已经在前面的内容中讲到了其中一些工具,用于帮助你窥探进程和a.out文件的

内部。有些工具是SunOS所特有的。本节提供了一个易于阅读的总结材料,告诉你这些工具中的每一个是用来干什么的以及可以在哪里找到它们。在学完这个总结材料之后,请接着阅读每个工具的主文档,并在几个不同的a.out中运行每个工具。可以使用“Hello World”程序,

也可以使用其他较大的程序。

请仔细研究这些工具,如果你花l5分钟时间对每个工具进行一下试验,将来在解决Bug 问题时,它会大大节约你的时间。

表6.1

用于检查源代码的工具

医生可以使用x射线、声谱仪、内窥镜和探查术来查看病人的身体内部。这些上面这些

131

0专承骊程———————————————————————————————————————————————————

工具就是软件世界的x射线。

表6—2 用于检查可执行文件的工具———————————————————————————————————————————————————

工具位于何处所做工作———————————————————————————————_’———————————————————————

dis /usr/ccs/bin 目标代码反汇编工具

dump-LV /usr/ccs/bin 打印动态链接信息

ldd /usr/bin 打印文件所需的动态.

nm /usr/ccs/bin 打印目标文件的符号表

strings /usr/bin 查看嵌入于二进制文件中的字符串。用于查看二进制文件可能产生

的错误信息、内置文件名和(有时候)符号名或版本和版权信息

SHill /usr/bin 打印文件的检验和与程序块计数。回答下面这样的问题:“这些可

执行文件是同一版本的吗?”“传输是否成功?,,

—————————————————————————————●—————————————————————————————————————————————————~

表6-3 帮助调试的工具—————————————————————————————————————————————————

工具位于何处所做工作

truss /usr/bin trace的SVr4版本,这个工具打印可执行文件所进行的系统调用。ps

C仃ace

/usr/bin

随编译器附带

debugger 随编译器附带

file /usr/bin

它可用于查看二进制文件正在干什么,为什么阻塞或者失败。这将

非常有用

显示进程的特征

修改你的源文件,文件执行时按行打印。是一个对小程序非常有用

的工具

交互式调试器

告诉你一个文件包含的内容(如可执行文件、数据、ASCIl、shell

script、archive等) ——————————————————————————————————————————————————————

表6_4 ’ 性能优化辅助工具——————————————————————————————————————————————————————

工具位于何·处所做工作

—————————————————●—————————————————————————————————

collector 随编译器附带(SunOS独有)在调试器控制下收集运行时性能数据

analyzer 随编译器附带

gprof

prof

tcov

/usr/ccs/bin

/usr/ccs/bin

随编译器附带

(SunOS独有)分析已收集的性能数据

显示调用图配置数据(确定计算密集的函数)

显示每个程序所消耗时间的百分比

显示每条语句执行次数的计数(确定一个函数中计算密集循环)

time /usr/bin/time 显示程序所使用的实际时间和CPU时间————————————————————————————————————————————————————

732

第6章运动的诗章:运行时数据结构

如果你工作于操作系统的内核模式,则无法使用绝大多数运行时工具,因为内核并不像用户进程那样运行。可以使用编译时工具如lint,但除此之外我们只能使用石刀和燧斧了:将有序模式放入内存中,看看它们何时被覆盖(最常使用的两个是十六进制常量deadbeef 和

abadeafe),使用printf或类似的函数并记录跟踪信息。

软件僖条

用9rep调试内核

当内核检测到“不会出现”的情况时,它就会“惊慌失措”,引起突然停止。例如,当

它寻找一些具体数据时,却发现了一个null指针。由于它无法从这种情况中恢复,最安全的

方法就是在数据消失前中断处理器。为解决内核的“惊慌”问题,首先必须考虑有哪些事情有可能吓坏操作系统。

Sun的内核开发小组有一个很隐蔽的Bu9,非常难以被发现。其症状是内核的内存偶尔会被覆盖,这会使系统“惊慌”。

我们队伍中的两个顶尖工程师着手处理这个fs]题,他们注意到总是一个内存块的前l9 个字节被涂抹。这是一个不寻常的偏移量,不像别处出现的2,4,8等常见值。其中一个工程师灵机一动,使用这个偏移量来寻找这个Bu9。他建议用内核调试器kadb来反汇编内核二

进制文件的映像(花了一个小时时间),将结果输出到一个ASCII文件中。然后,他们用9rep 对这个文件进行搜索,寻找操作数指示偏移量为19的“store”指令!

这些指令中的其中一个肯定是引起问题的根源。

一共有八条这样的指令,它们都位于处理进程控制的子系统中。现在,他们对问题出在什么地方已经比较明确了,现在要做的就是找出它。进一步努力之后,他们终于找到了罪魁祸首:位于一个进程控制结构的竞争条件。它的用意是一个线程在其他线程(调用了该线程) 真正完成.Y-作之前先在内存中作个标记,以便以后返回系统。结果:内核内存分配器把这块

内存分配给了别人,但进程控制块仍以为它还保留有这块内存,所以向其写入,这样就导致了这个极难发现的Bu9。

用grep来调试操作系统内核一.一个非与寻常的概念。有时候甚至连源代码工具都可以

帮助解决运行时问题!

在讨论这些有用工具的同时,表6.5列出了一些识别Sun系统确切配置的方法。然而,除非你在实践中使用它们,否则它们对你不会有多大帮助。

133

C专家编程

内核体系

任何用于0S的补丁

各种硬件

CPU时钟频率

主机ID

内存

序列号

ROM版本

安装的磁盘

交换区

以太网地址

IP地址

浮点数硬件

sun4c

未安装补丁

许多

40MHz处理器

554176fe

32Mb

4290302

2.4.1

198Mb磁盘

40Mb

8:O:20:f.8c:60

le0=129.144.248.36

FPu的频率显示

为38.2MHz

/usr/kvm/arch-k

/usr/bin/showrev-p

/usr/sbin/prtconf

/usr/sbin/psrinfo--V

/usr/ucb/hostid

在开机时启示

在开机时启示

在开机时启示

/usr/bin/df-F ufs-k

/etc/swap—S

/usr/sbin/ifconfig-a以太网地址被建立到机器中

/usr/sbin/ifconfig-a IP地址被建立到网络中

fpversion随编译器附带

6.12轻松一下一卡耐基一梅隆大学的编程难题

几年前,卡耐基.梅隆大学(CMU)的计算机科学系有一个常规性的小型编程竞赛,参赛对

象是刚入学的研究生。竞赛的目的是让这些新的研究人员得到一些关于计算机科学系的直接经验,并让他们展示自己的强大潜力。CMU在计算机领域的研究历史悠久,可以追溯到计算机的先驱时代,它在这个领域所取得的成就可以说是非同凡响。所以,对于CMU举办的编程竞赛,其水准可想而知。

比赛的形式每年都不一样,其中有一年非常简单。参赛者必须读入一个文件(文件的内容是一些数值),并打印这些数值的平均数。只有两个规则:

1.程序的运行速度要尽可能地快。

2.程序必须用Pascal或C编写。

参赛选手的程序集中之后由一名系工作人员分批上交。学生们可以自愿上交尽可能多的作品,这可以鼓励非确定性随机算法(就是猜测某些数据集的特征,利用猜测结果获得尽可能快的效率)的使用。决定性的规则是:运行时间最短的程序将获得优胜。

734

第6章运动的诗章:运行时数据结构

这些研究生们纷纷钻进各个角落,开始折腾各种各样的程序。他们中的绝大多数都准备了3到4个程序参加竞赛。在此,读者们也可以想想有什么样的技巧可以使程序运行得更快。1繁潦i囊鬻纛瓣瓣

1鲠渊

瓣慧黧鬻嚣黧搿慧:魏l

编程挑战

怎样突破速度限制

想象一下,假如你接到一个任务,要求读入一个内容是l0 000个数值的文件,并计算这些数值的平均数。你的程序的运行时间必须尽可能地短。

你会采用什么样的编程和编译技巧来提高速度?

大多数人都猜想最大的赢家一定采用了代码优化措施,不管是显式地在代码中使用,或是通过正确设置编译器选项隐式地使用。标准的代码优化技巧包括:消除循环、函数代码就地扩展、公共子表达式消除、改进寄存器分配、省略运行时对数组边界的检查、循环不变量代码移动(100p—invariant code motion)、操作符长度削减(把指数操作转变为乘法操作,把乘

法操作转变为移位操作或加法操作等)等。

数据文件大约包含了10 000个数值,假定读入和处理每个数需要一毫秒(当时的系统差不多就是这个速度),最快的程序也要用10秒左右。

实际结果非常令人吃惊。其中最快的一个程序,操作系统报告用时为一3秒。确实如此——优胜程序的运行时间是负数!第二快的程序大约用了几毫秒,而排名第三的作品恰好比预期的l0秒稍微少一点。显然,获胜者在编程中作了弊,但他是怎样作弊的昵?评委们在对优胜程序进行仔细审查后,答案揭晓了。

这个运行时间为负的程序充分利用了操作系统。程序员知道进程控制块相对于堆栈底部的存储位置,他用一个指针来访问进程控制块,并用一个非常大的值覆盖“CPU已使用时问”字段l。操作系统未曾想到CPU时间会有如此之大,因此错误地以二进制补码方案把这个非常大的数解释为负数。

至于那个费时仅几毫秒的亚军程序得主同样狡猾,他用的方法有所不同。他使用的是竞争规则而不是怪异的编码。他提交了两个不同的程序,其中一个读入数据,用正常的方法计算平均值,并将答案写入一个文件。第二个程序绝大部分时间都处于睡眠状态,它每隔几秒醒来一次检查答案文件是否已存在,如果已经存在,就打印其结果。第二个程序总共只占用了几毫秒的CPU时间。由于参赛者允许递交多件作品,所以这个用时极少的程序就把他推上了亚军的位置。

这是一种对规则的臭名昭著的滥用,类似于阿根廷足球巨星马拉多纳在1986年世界杯用上臂打入英格兰队一球的那个“上帝

之手”。

135

相关文档