2009年3月26日星期四

tweak、oofice、apt-file使用

1. hiweed下安装使用ubuntu-tweak的方法
首先打开 控制中心 --> 会话和启动管理 --> 高级,勾选兼容性。
然后sudo apt-get install gnome-about 然后ubuntu-tweak 就可以工作了。

2. Openoffice3.0在Ubuntu8.10中不能输入中文
将libstdc++和libgcc的so文件软链到OOo的ure路径下即可:
sudo ln -snf /usr/lib/gcc/i486-linux-gnu/4.3.2/libstdc++.so \
/opt/openoffice.org/ure/lib/libstdc++.so.6
sudo ln -snf /usr/lib/gcc/i486-linux-gnu/4.3.2/libgcc_s.so \
/opt/openoffice.org/ure/lib/libgcc_s.so.1
3. 优化OOo启动速度
1. OOo"工具"对话框,选项->内存
2. 减少"撤销命令->步数",我设置为20
3.
增大"图形缓冲区->用于openoffice",我设置为128m,"每个对象的内存",我设置为20m
4. 取消Java选项页中的"使用Java运行环境"
4. 打开触摸板驱动
在其他版本和以前版本的ubuntu中,在xorg里添加一段话就可以使用gsynaptic了,可是Ubuntu8.10不行。解决办法是编辑/usr/share/hal/fdi/policy/20thirdparty/11-x11-synaptics.fdi,在里面找到<
match key="info.product" contains="Synaptics TouchPad">这行,在出现<
/match>之前加入一行:
< merge key="input.x11_options.SHMConfig" type="string">On<
/merge>(由于wp格式的问题,请自行取消<号之后的空格)即可。
5. 使用apt-file来查找安装软件时缺少的软件包。
当./configure或者其他安装命令执行时提示缺少XXX库没有安装,apt-cache
search和aptitude search无果之后,不妨尝试apt-file(当然你要先安装apt-file)。
语法:
apt-file search FILE

投资书籍推荐

一、巴菲特:《巴菲特致股东的信》以及一切有关巴菲特的书籍
二、彼得林奇:《彼得林奇的成功投资》
三、费雪:《怎样选择成长股》
四、迈克尔波特:〈竞争优势〉
五、马尔基尔:〈漫步华尔街〉
六、西格尔:〈投资者的未来〉
七、多尔西:〈股市真规则〉
八、约翰伯格:〈长赢投资〉
九、罗杰斯:《热门商品投资》
十、萨谬尔森:〈经济学〉
十一、曼昆:〈经济学〉
十二、黄友牛编:〈经济学的故事〉
十三、王福重:〈人人都爱经济学》
十四、特维德:《逃不开的经济周期》
十五、但斌:《时间的玫瑰》
十六、林森池:《投资王道》

中国银行业麦田最后的守望者

作者: 德邦证券徐明强
  银行历来被认为是最有实力、最稳定的企业,只有这样,你才敢把毕生积蓄放入其中而又能够安稳入梦。可是,近来的一连串事件让大家有些摸不着头脑。昔日世界最大银行花旗集团50美元的股价为什么能够被次贷一拳打至1美元?股神巴菲特的重仓股富国银行为何大跌2/3至10年前的价位?汇丰控股,这只被港人称作人人持股压箱底的"5号仔",从2008年9月至2009年1月,只用了4个月股价就第一次被腰斩;而从2009年1月至2009年3月,股价更是只用了2个月就第二次被腰斩,3月9日更是一日暴跌24%!其速度之快、跌幅之巨可谓触目惊心。风暴之下,中国的银行业能否泰然面对?还有几只靴子没掉下来?大跌之后,银行股现在是否具有投资价值?面对银行这一难懂的巨人,这笔账该如何计算清楚呢?

  当杠杠效应遭遇厚尾特性
  套用"美国是车轮上的国家"这一比喻,银行可称为"悬于杠杆上的企业"。这一形象肯定与老百姓印象中的百年老店、稳如泰山大相径庭。这一比喻绝非耸人听闻,这里要简要分析一下银行的盈利模式。
  银行作为融通资金的中介媒体,具有典型的薄利多销性质。即便是在中国这样利差较大的国家,银行的总资产收益率也仅1%左右。要想实现可观收益、给予股东期望的回报,途径有两条,一是提高银行经营杠杆比率,1%被放大十倍就是颇具诱惑力的10%;二是大力发展不占用资金、周转率高的中间业务等项目。提高杠杆比率,无疑会放大银行经营风险,因此采取这一途径必须十分小心谨慎。而发展中间业务着实是项辛苦活,需绞尽脑汁搞出金融创新发掘需求,没有放贷坐等收息那么舒坦。西方发达国家的银行,在激烈的竞争中已在试图摆脱依赖放大杠杆赚取利差的粗暴途径,计划凭借精细化的中间业务发展来稳定银行的业务收入来源。无奈,9・11后格林斯潘的宽松货币政策,使得传统业务油水收缩得过猛。为快速弥补这一损失,银行选择了挑战:将资金高息贷给低信用级别的人以扩大利差,放大杠杆并证券化信贷以提高资金周转速度。银行所作所为是在铤而走险,但其实这条钢丝绳银行已走了许多年了。
  之所以敢于长期从事高风险行业,并非都是艺高人胆大,银行这个杂技演员身后是有根保险绳的。这根保险绳就是统计学上称的概率分布,对于银行来说,就是短时间大量坏账出现的概率。银行认为,能从钢丝绳上摔下来且保险绳也同时断掉的可能性非常小,便敢于在钢丝上施展各种高难动作。但银行低估了某些潜在风险的可能性。试想,如果钢丝绳和保险绳是同一家公司生产,那么,在钢丝绳因质量伪劣磨损打滑断裂的同时,保险绳被拉断的可能性也就大增了。银行遇到的这两种劣质产品,便是带杠杆的房地产和低信用级别的客户。这种被低估的小概率事件在许多情况下都存在,被称为厚尾特性。从行为金融学的角度讲,二战后世界基本上处于平稳上升轨道,使得人们通常倾向于过分乐观地看待事物,那么,危机这条尾巴就会比人们想象的要厚得多。
  杠杆对杠杆的博弈。来看看美国新经济,自从美国的三大支柱―――信息技术、医药、金融,取代了旧三大支柱―――钢铁、汽车、房地产后,其为美国经济带来了新一轮快速上涨,但最近也面临着诸多问题。实际上,互联网泡沫后,美国的信息技术就由突破式发展转为小步慢跑,医药行业的研发速度也遭遇到瓶颈,多年没有重大医药研发突破、很多专利面临过期被仿制药顶替的危险。三大支柱中的金融行业,则将自己和房地产行业捆绑在一起。不幸的是,金融和房地产都是高杠杆行业,当杠杆激烈碰撞杠杆,注定有大戏上演。金融和房地产真是一对冤家,两者间的关系就像氢弹和核弹,能够有实力引爆氢弹的只有核弹。常态下银行业构筑在严密监管、审慎经营基础上的高大身躯,确实很难被撼动,不借助杠杆力量无法产生大波动。房地产作为普通老百姓财富的最大组成部分,实际上放大了家庭的财务杠杆,给家庭带来更多不确定性因素。银行将大量赌注压在房地产上,实在过于激进。

  汇控的眼泪
  失败的婚姻。汇丰控股2003年收购了美国次贷发放机构Household,赶了个时髦的尾巴。还没来得及和漂亮新媳妇磨合适应完毕,美国房价就于2006年开始掉头向下,而这一扭头,转眼间就到了危机继续恶化的2009年。前途依旧迷茫,2010年能否止跌谁也说不好。
  仔细端详这位旧宠,汇控不禁愕然:其应收账款的账面价值和公允价值间存在高达343亿美元的巨大差异,而可出售债券的浮亏亦达214亿美元,这些潜在的未来公允价值减记约总计560亿美元,远超汇丰拟向现有股东募集的177亿美元新增股本。此刻汇控想必是肠子都悔青了,其曾在2008年年报中检讨:"但愿不曾进行过这项收购。"后悔归后悔,昂贵的分手费用免不了。汇控2008年除税前利润(不包括商誉减损)为199亿美元,下跌18%。而商誉减值高达106亿美元,造成税前收益大跌62%,仅有93亿美元。如果不包括汇丰因债务公平价值变动而产生的66亿美元所得(此项将在以后拨回),则2008年其税后利润趋近于0。汇丰断然割臂,宣称不再以HFC及Beneficial品牌在美国承办消费融资业务,并关闭大部分经营网络。惊魂未定,汇丰在发布年报时重申,它打算对Household的债券持有人负起责任,虽然这些债务对汇丰集团没有追索权。这种藕断丝连的暧昧态度,让等待靴子落地的楼下住客根本无法安然入睡,这简直是一种折磨。投资者纷纷用脚投票,汇丰控股股价雪崩,仅6个月便跌去3/4至最低33港币。昔日恒指的定海神针已面目全非。
  细看汇丰的财务报表,确与内陆银行有较大区别。单看资产负债表资产端的项目,按金额排名前3位的分别是客户贷款、衍生工具、交易用途资产,金额为9329亿美元、4949亿美元和4273亿美元。衍生工具占2.5万亿美元总资产的比例竟达到近1/5,交易用途资产这种公允价值波动率较高的金融工具也达到1/6;相比之下,不足1000亿美元,占总资产1/25的股东权益总额显得如此渺小。衍生工具的不透明性,在于很难合理确定其公允价值,而看不清的东西就容易让人产生恐惧心理。对于各类金融资产的会计处理,银行通常具有一定的灵活性。且看汇丰在其年报中对于可供出售证券的处理描述:"2008年,虽然所持的可供出售证券价值下跌约165亿美元,但集团只就该等证券组合确认2.79亿美元的减值亏损。上述两个数值出现重大差异,反映所有资产抵押证券均缺乏流通性,而减值亏损处于低水平,则反映汇丰所持不同类别证券的偿债优先次序。"通俗说就是,汇丰乐观认为现在这些持有的证券虽然没人买、不值钱,但我一直捂着还是有望回本的。可是未来的事谁能说清?这样的会计处理自然让人生忧。
  风险与机遇。汇丰的确做了错事。但谁能无过?股价被砸成这样已是严重的惩罚,但浪子回头后是否能重新做个更强大的好人呢?汇丰报表还是反映了很多可圈可点的增长点。相比北美市场,汇丰在新兴市场的状况截然相反。年报显示,2008年汇丰除北美洲外,各地区的多元化业务模式均产生利润,尤其在新兴市场的业务持续增长。其中,印度税前利润6.66亿美元,上升26%;中东税前利润17亿美元,上升34%;中国内地税前利润16亿美元,上升25%,而如果比较基准不计入联营公司的收益,则内地利润劲升64%。而私人银行业务则有点受益于危机的味道,在世界诸多大银行境遇远比汇丰惨得多的时候,许多人选择了将资产转移至汇丰来管理。汇丰2008年吸纳了300亿美元的资金净额流入。总结下来就是:从业务划分来看,除个人理财业务,其他业务2008年都盈利;从地区划分看,除北美洲其他地区都盈利。如果汇丰能狠下心来与旧爱Household彻底决裂,则Household破产并不会将过多损失传导至汇丰总部,但将会影响到汇丰的全球声誉,那将是生存与声誉的权衡。
  美国信贷市场目前虽非常糟糕,但奥巴马总统的劫富济贫计划,确会将许多利益转移至被放贷折磨的美国房奴,进而缓解银行坏账压力。美国人民勒紧裤带、储蓄率上升,也在为还贷提供了一些乐观前景。汇丰对于新兴市场的侧重和分散化业务让其躲过致命一击,而此次汇丰的供股计划一旦成功,挺过这一关,汇丰的前景将会从欧美其他同行中胜出,在全球市场占据更大份额。

  谁是凤凰
  中国本土银行,曾在建立现代银行体系的改革初期做过乖学生。2005年,建设银行以1.15倍PB的价格邀请美洲银行作为战略投资者来传授经验。短短3年后,这位老师在自家火烧屁股,股价跌至仅剩高点的1/10,于是变卖股份仓惶逃走。不知这位学生此时感受如何,有一点是确定的,当前环境很恶劣,必须巡视四周、认清威胁,采取措施保全自己。
  次级债券与拨备率。金融危机肆虐,国内诸银行选择了主席曾经指示的"深挖洞、广积粮、备战备荒"的战术。为补充资本金提高抗风险能力,银行在2008、09年掀起了发行次级债券浪潮。公告显示,民生银行发行不超过50亿人民币混合资本债券;浦发银行发行100亿元混合资本债券及100亿元次级债;深圳发展银行发行100亿次级债、100亿金融债及80亿混合资本债;兴业银行将发行180亿元次级债;招商银行发行300亿次级债;工商银行有发行1000亿次级债计划;而中国银行、建设银行都将发行1200亿次级债券。按照银监会的资本充足率监管路线图,2008年底前全部达到8%的最低监管要求;2009年提升至10%;2010年达到12%。对于这样的监管要求,多数银行并没有补充资本金的迫切需要。截至2008年9月末,工、中、建、交四大行的资本充足率水平均超过12%。应该说,相比西方同行仓促狼狈寻求政府资金注入的窘状,中国的银行显得悠然许多。中国政府对于银行业的监管是很谨慎的,而银行业自身在经营过程中也表现得小心翼翼,这为国内银行应对此次危机提供了较有利条件。央行连续大幅降息下,发行次级债券是充分把握这次机遇,降低融资成本,并为息差收窄下通过扩大贷款资产规模维持收益打下基础。
  为使银行报表更加透明化,不给投资者带来过多震惊,根据审慎监管原则,中国银监会对银行的不良贷款拨备提出了更高要求。去年12月份,在央行公开宣布放松信贷规模控制不到一个月,银监会就传达了提高商业银行拨备覆盖率的要求,将五大国有银行的拨备覆盖率由原来的100%提高到130%,而股份制银行的拨备覆盖率由原来的100%提高到150%;然而今年银监会拟再次上调拨备覆盖率,这次上调的主角主要是国有商业银行。按照新标准,五大国有银行的拨备覆盖率要从130%提高到150%,两次调整累计增长50个百分点。为了达到银监会提高拨备的要求,不少银行开始在去年底加大拨备覆盖水平。深发展为2008年第四季度新增拨备约人民币56亿元,为2008年第四季度核销呆账本金及垫付诉讼费折人民币约94亿元。大量增加拨备,无疑会对银行的短期利润造成负面影响,但此时的拨备增加还不至于造成人们的心理恐慌。谨慎拨备挤出了资产质量的水分,增加了透明度,反而消除了人们对于不确定性的恐惧。比如花旗的股价跌至1美元,更多也是出于对不确定性的恐惧,如果说花旗确定有一笔比现在更糟糕的亏损,但未来继续亏损的程度和时限可知,昔日的银行霸主在政府明确撑腰下未必会落此境遇。问题早解决比一直拖着要强许多,最起码不会成为头顶的靴子不断折磨人。金融危机中不透明的财务报表披露和某些银行家今日信誓旦旦,明日就宣布破产的雷人行为导致了信心的缺失,信心的危机是更为严重的危机。

  巨人背后的巨人
  为应对当前世界范围的经济衰退,中国政府推出了4万亿经济刺激计划。且先不说这4万亿的用途,资金的来源问题已引发众多猜测,虽有些微差别,但总的共识是银行将会承担4万亿中的大部分资金。
  在经历了几年前成本高昂的注资和重组,以及最近的存贷比下降后,中国的银行正好处于有利的位置来配合政府,通过适当增加杠杆比率以维持内需增长。根据《商业银行法》规定,商业银行贷存比的比例不得超过75%。据各行三季报披露,截至去年9月底,工行贷存比为56%。工行是四大行中贷存比最低的,要达到75%贷存比上限,可新增投放贷款15040亿元。建行贷存比为57%,尚可投放贷款10500亿元。中行贷存比是四大行中最高的,达到65%,可净投放4655亿元。虽然近期央行去年连续下调存款准备金率,但存款准备金率仍高达15%,尚有下调空间。中国银行业总体处于较健康状态,大量为实体经济增加贷款会产生更多正面效应。
  4万亿配套贷款大部分用于基建项目贷款,风险相对较小。虽然利差可能继续趋于微薄,但2009年银行的整体思路显然已定位于以量补价,以规模增效益。因此,即便4万亿将会推高国内银行的杠杆比率,在一定程度上增加风险,但在全球货币政策奔向零利率的定量宽松范畴时,此举应是利大于弊。如果巨人站不稳,身后还有一位更加强壮的巨人相扶,这一角色由中投旗下的汇金公司来扮演。中投副总经理汪建熙在参加两会时,再次重申了汇金的维稳作用,当市场出现非理性波动时,汇金公司将增持三大行。既然银行在一定程度上充当了政策工具,为经济稳定发展起到至关重要的作用,那么巨人背后真正的超级巨人,便是拥有2万亿美元外汇储备、已经强大起来的中国。因此,相比于风雨飘摇的海外银行业,中国的银行投资价值较为明显。
  中国政府对银行业监管很谨慎,而银行业自身也在小心翼翼地经营。所幸的是,中国政府推出了4万亿经济刺激计划,让中国银行业处于有利位置配合政府。
  巨人背后真正的超级巨人,是拥有2万亿美元外汇储备、已经强大起来的中国。

如何成为伟大的投资者――马克?塞勒斯

马克・塞斯勒是对冲基金――塞勒斯资本管理基金的创始人和管理者,他曾是晨星公司(Morningstar)的首席证券策略分析师,塞勒斯资本管理基金现在管理着约8,500万美元。马克・塞斯勒恪守价值投资原则,风格类似于巴菲特,他喜欢竞争对手很少的大公司,也就是拥有宽阔的"护城河"的行业。他在这些公司不被看好且其股价达到他认为是便宜货的水准时买入股票。他也投资某些小公司,这些小公司拥有一些隐藏的价值,其他投资者估价时并未考虑到这些因素。他的基金投资组合非常集中,只持有5-15只股票,通常在5只左右,现在,6只股票就构成了他投资组合的90%;其中有一只股票占他基金总资产的一半。
首先我要感谢丹尼尔・哥德堡邀请我到这里,还要感谢所有出席的各位。我有一阵子没来波士顿了,但我1991-1992年间在伯克利音乐学校学习期间在本地短期生活过,我在那儿学习爵士乐钢琴演奏,但过了几个学期后我放弃了,到洛杉矶去参加了一个乐队。那时我很穷,没法玩遍波士顿,又没车,没法饱览新英格兰地区的风光,所以大部分时候只能每天窝在练习室内苦练钢琴。因此每当我重返波士顿,它对我来说总是像个全新的地方。
有件事我得给你们说:"我到这里不是来教你们如何成为一个伟大的投资者的,恰恰相反,我到这里来想告诉你们的是为什么你们中绝大多数人达不到那个境界。"
如果你已经花了足够多的时间研究像查理・芒格、巴菲特、布鲁斯・博克维兹、比尔・米勒、埃迪・兰伯特、比尔・阿克曼以及其他类似的伟大投资者,你就会明白我的意思。
我知道在座的各位都非常聪明,都是通过努力奋斗达到了今天的成就,你们是聪明人中的聪明人。即使你会忘了我所说的其他话,有件事你必须得记住:你成为伟大投资者的可能微乎其微,概率非常低,比如2%或更低,而且这个概率还是我考虑到在座各位都有高智商、工作勤奋且即将在顶尖商学院获得MBA学位这些事实后向上调整过的。如果在座的各位是从一般人中随机挑选出的话,这个概率或许只有0.02%或更低。虽然对一般投资者来说,你们具有很多优势,你们长时期保持惊人的优异业绩的可能还是微乎其微。
原因是你的智商、阅读量、拥有的经验等等并不是决定性因素,这些因素很多人都具备,然而他们中几乎没有人在职业生涯中能长期保持20%或者25%的复利增长。
我知道说这是有争议的,我可不想冒犯在座的各位。我并不是具体针对某人说"你几乎没有可能成为伟大的投资者"。也许在座的其中一两个会在未来的职业生涯中实现20%的复利增长,可是如果不了解你们中的每一个人的话,很难预言那会是谁。
幸运的是,虽然你们几乎不可能在整个职业生涯中实现20%的复利增长,你们中的许多人还是会成为优秀的超乎一般人的投资者,因为哈佛MBA学生属于精英。一个人可以通过学习成为超过一般人的投资者,如果你聪明勤奋有学问,能通过学习在投资业中立足,拿着高薪,在职业生涯中干得不错,不用成为一个伟大的投资者也能赚到大钱。通过勤奋工作、非同寻常的智商以及大量的学习和研究,你能每年超越平均回报几个百分点。因此你大可不必为我前面所说的一些话而沮丧,即使你不是下一个巴菲特,也有可能在职场取得成功,赚到大钱。
但你不可能永久地保持20%的复利增长速度,除非你在10-12岁间就在大脑中具备了那样的能力。我不确定那是与生俱来的本能还是早期教育带来的能力,但如果你十多岁的时候还不具备那样的能力,你就不会拥有那种能力了。到哈佛上学并不能改变这个事实,读关于投资的每一本书同样不能,长期的经验同样不能。要成为伟大的投资者,这些是必要条件但不是充分条件,因为所有这些条件都可以被竞争者模仿。
我确信你们在商学院一定会上一门策略课程,也许你会学习迈克・波特的研究成果和书籍,当初我在上商学院前也这么干过。读他的书我学到了很多东西,在分析公司时我一直运用到这些知识。
如果你是一家公司的CEO,哪些类型的竞争优势能在竞争中保护你呢?你如何才能拥有一条如巴菲特所形容的宽阔的"护城河"来保护自己呢?
科技并不是"护城河"的来源,如果它是你的唯一优势的话,它最终一定会被模仿。在这种情况下,你的最佳选择是被收购或者是上市并抢在投资者意识到你并不具备可持续的竞争优势前抛售自己所有的股份。科技是一种短命的优势,类似的优势还有管理团队、广告战或者热门流行趋势,这些因素会创造暂时的优势,但随着时间的变迁,它们会变化或者被竞争对手所模仿。
一条经济"护城河"是一种结构性的竞争优势,正如上世纪90年代的西南航空,它的优势扎根于企业文化中,扎根于每个雇员之中,没有对手能模仿它,虽然每个人都明白西南航空是怎么做的。如果你的竞争对手明知你的秘密却仍然无法模仿的话,你就拥有一种结构性竞争优势,那才是"护城河"。
在我看来,实际上仅有四种经济护城河难以被模仿因而得以持久。第一种"护城河"是规模经济,沃尔玛就是一个例子,类似的有制服租赁业的Cintas公司、Procter & Gamble公司、Home Depot公司和Lowe's公司。第二种"护城河"是网络效应,比如e-Bay、Mastercard、Visa和American Express。第三种"护城河"是知识产权,比如专利、商标、商誉等,迪斯尼、耐克、Genentech等都是很好的例子。最后一种"护城河"是客户转用其他公司产品所产生的高成本。Paychex和微软是这类护城河的绝佳例子。
仅有这四种竞争优势是持久的,因为竞争对手很难模仿。正如一家公司需要建立"护城河"以避免流于平庸,一个投资者也需要建立一些竞争优势,否则他将变得平庸。
每天有8,000只对冲基金、10,000只共同基金和成千上万的个人投资者在股票市场上角逐,你怎么才能建立相对所有这些人的竞争优势呢?你的"护城河"来源于何处呢?

用GDB调试程序

GDB是一个强大的命令行调试工具。大家知道命令行的强大就是在于,其可以形成执行序列,形成脚本。UNIX下的软件全是命令行的,这给程序开发提代供了极大的便利,命令行软件的优势在于,它们可以非常容易的集成在一起,使用几个简单的已有工具的命令,就可以做出一个非常强大的功能。

于是UNIX下的软件比Windows下的软件更能有机地结合,各自发挥各自的长处,组合成更为强劲的功能。而Windows下的图形软件基本上是各自为营,互相不能调用,很不利于各种软件的相互集成。在这里并不是要和Windows做个什么比较,所谓"寸有所长,尺有所短",图形化工具还是有不如命令行的地方。

用GDB调试程序

GDB概述
————

GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。或许,各位比较喜欢那种图形界面方式的,像VC、BCB等IDE的调试,但如果你是在UNIX平台下做软件,你会发现GDB这个调试工具有比VC、BCB的图形化调试器更强大的功能。所谓"寸有所长,尺有所短"就是这个道理。

一般来说,GDB主要帮忙你完成下面四个方面的功能:

1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
3、当程序被停住时,可以检查此时你的程序中所发生的事。
4、动态的改变你程序的执行环境。

从上面看来,GDB和一般的调试工具没有什么两样,基本上也是完成这些功能,不过在细节上,你会发现GDB这个调试工具的强大,大家可能比较习惯了图形化的调试工具,但有时候,命令行的调试工具却有着图形化工具所不能完成的功能。让我们一一看来。

一个调试示例
——————

源程序:tst.c

1 #include
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i 7 {
8 sum+=i;
9 }
10 return sum;
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
21 }
22
23 printf("result[1-100] = %d \n", result );
24 printf("result[1-250] = %d \n", func(250) );
25 }

编译生成执行文件:(Linux下)
hchen/test> cc -g tst.c -o tst

使用GDB调试:

hchen/test> gdb tst <---------- 启动GDB
GNU gdb 5.1.1
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you
are
welcome to change it and/or distribute copies of it under certain
conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-suse-linux"...
(gdb) l <-------------------- l命令相当于list,从第一行开始例出原码。
1 #include
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i 7 {
8 sum+=i;
9 }
10 return sum;
(gdb) <-------------------- 直接回车表示,重复上一次命令
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
(gdb) break 16 <-------------------- 设置断点,在源程序第16行处。
Breakpoint 1 at 0x8048496: file tst.c, line 16.
(gdb) break func <-------------------- 设置断点,在函数func()入口处。
Breakpoint 2 at 0x8048456: file tst.c, line 5.
(gdb) info break <-------------------- 查看断点信息。
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048496 in main at tst.c:16
2 breakpoint keep y 0x08048456 in func at tst.c:5
(gdb) r <--------------------- 运行程序,run命令简写
Starting program: /home/hchen/test/tst

Breakpoint 1, main () at tst.c:17 <---------- 在断点处停住。
17 long result = 0;
(gdb) n <--------------------- 单条语句执行,next命令简写。
18 for(i=1; i<=100; i++)
(gdb) n
20 result += i;
(gdb) n
18 for(i=1; i<=100; i++)
(gdb) n
20 result += i;
(gdb) c <--------------------- 继续运行程序,continue命令简写。
Continuing.
result[1-100] = 5050 <----------程序输出。

Breakpoint 2, func (n=250) at tst.c:5
5 int sum=0,i;
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p i <--------------------- 打印变量i的值,print命令简写。
$1 = 134513808
(gdb) n
8 sum+=i;
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p sum
$2 = 1
(gdb) n
8 sum+=i;
(gdb) p i
$3 = 2
(gdb) n
6 for(i=1; i<=n; i++)
(gdb) p sum
$4 = 3
(gdb) bt <--------------------- 查看函数堆栈。
#0 func (n=250) at tst.c:5
#1 0x080484e4 in main () at tst.c:24
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6
(gdb) finish <--------------------- 退出函数。
Run till exit from #0 func (n=250) at tst.c:5
0x080484e4 in main () at tst.c:24
24 printf("result[1-250] = %d \n", func(250) );
Value returned is $6 = 31375
(gdb) c <--------------------- 继续运行。
Continuing.
result[1-250] = 31375 <----------程序输出。

Program exited with code 027. <--------程序退出,调试结束。
(gdb) q <--------------------- 退出gdb。
hchen/test>

好了,有了以上的感性认识,还是让我们来系统地认识一下gdb吧。

使用GDB
————

一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的
-g 参数可以做到这一点。如:

> cc -g hello.c -o hello
> g++ -g hello.cpp -o hello

如果没有-g,你将看不见程序的函数名、变量名,所代替的全是运行时的内存地址。当你用-g把调试信息加入之后,并成功编译目标代码以后,让我们来看看如何用gdb来调试他。

启动GDB的方法有以下几种:

1、gdb
program也就是你的执行文件,一般在当然目录下。

2、gdb core
用gdb同时调试一个运行程序和core文件,core是程序非法执行后core
dump后产生的文件。

3、gdb
如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试他。program应该在PATH环境变量中搜索得到。


GDB启动时,可以加上一些GDB的启动开关,详细的开关可以用gdb
-help查看。我在下面只例举一些比较常用的参数:

-symbols
-s
从指定文件中读取符号表。

-se file
从指定文件中读取符号表信息,并把他用在可执行文件中。

-core
-c
调试时core dump的core文件。

-directory
-d
加入一个源文件的搜索路径。默认搜索路径是环境变量中PATH所定义的路径。

GDB的命令概貌
———————

启动gdb后,就你被带入gdb的调试环境中,就可以使用gdb的命令开始调试程序了,gdb的命令可以使用help命令来查看,如下所示:

/home/hchen> gdb
GNU gdb 5.1.1
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you
are
welcome to change it and/or distribute copies of it under certain
conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-suse-linux".
(gdb) help
List of classes of commands:

aliases -- Aliases of other commands
breakpoints -- Making program stop at certain points
data -- Examining data
files -- Specifying and examining files
internals -- Maintenance commands
obscure -- Obscure features
running -- Running the program
stack -- Examining the stack
status -- Status inquiries
support -- Support facilities
tracepoints -- Tracing of program execution without stopping the program
user-defined -- User-defined commands

Type "help" followed by a class name for a list of commands in that class.
Type "help" followed by command name for full documentation.
Command name abbreviations are allowed if unambiguous.
(gdb)

gdb的命令很多,gdb把之分成许多个种类。help命令只是例出gdb的命令种类,如果要看种类中的命令,可以使用help
命令,如:help breakpoints,查看设置断点的所有命令。也可以直接help
来查看命令的帮助。


gdb中,输入命令时,可以不用打全命令,只用打命令的前几个字符就可以了,当然,命令的前几个字符应该要标志着一个唯一的命令,在Linux下,你可以敲击两次TAB键来补齐命令的全称,如果有重复的,那么gdb会把其例出来。

示例一:在进入函数func时,设置一个断点。可以敲入break func,或是直接就是b
func
(gdb) b func
Breakpoint 1 at 0x8048458: file hello.c, line 10.

示例二:敲入b按两次TAB键,你会看到所有b打头的命令:
(gdb) b
backtrace break bt
(gdb)

示例三:只记得函数的前缀,可以这样:
(gdb) b make_ <按TAB键>
(再按下一次TAB键,你会看到:)
make_a_section_from_file make_environ
make_abs_section make_function_type
make_blockvector make_pointer_type
make_cleanup make_reference_type
make_command make_symbol_completion_list
(gdb) b make_
GDB把所有make开头的函数全部例出来给你查看。

示例四:调试C++的程序时,有可以函数名一样。如:
(gdb) b 'bubble( M-?
bubble(double,double) bubble(int,int)
(gdb) b 'bubble(
你可以查看到C++中的所有的重载函数及参数。(注:M-?和"按两次TAB键"是一个意思)

要退出gdb时,只用发quit或命令简称q就行了。

GDB中运行UNIX的shell程序
————————————

在gdb环境中,你可以执行UNIX的shell的命令,使用gdb的shell命令来完成:

shell
调用UNIX的shell来执行,环境变量SHELL中定义的UNIX的shell将会被用来执行,如果SHELL没有定义,那就使用UNIX的标准shell:/bin/sh。(在Windows中使用Command.com或cmd.exe)

还有一个gdb命令是make:
make
可以在gdb中执行make命令来重新build自己的程序。这个命令等价于"shell make "。

在GDB中运行程序
————————

当以gdb
方式启动gdb后,gdb会在PATH路径和当前目录中搜索的源文件。如要确认gdb是否读到源文件,可使用l或list命令,看看gdb是否能列出源代码。

在gdb中,运行程序使用r或是run命令。程序的运行,你有可能需要设置下面四方面的事。

1、程序运行参数。
set args 可指定运行时参数。(如:set args 10 20 30 40 50)
show args 命令可以查看设置好的运行参数。

2、运行环境。
path
可设定程序的运行路径。
show paths 查看程序的运行路径。
set environment varname [=value] 设置环境变量。如:set env USER=hchen
show environment [varname] 查看环境变量。


3、工作目录。
cd
相当于shell的cd命令。
pwd 显示当前的所在目录。


4、程序的输入输出。
info terminal 显示你程序用到的终端的模式。
使用重定向控制程序输出。如:run > outfile
tty命令可以指写输入输出的终端设备。如:tty /dev/ttyb


调试已运行的程序
————————

两种方法:
1、在UNIX下用ps查看正在运行的程序的PID(进程ID),然后用gdb
PID格式挂接正在运行的程序。
2、先用gdb
关联上源代码,并进行gdb,在gdb中用attach命令来挂接进程的PID。并用detach来取消挂接的进程。

暂停 / 恢复程序运行
—————————

调试程序中,暂停程序运行是必须的,GDB可以方便地暂停程序的运行。你可以设置程序的在哪行停住,在什么条件下停住,在收到什么信号时停往等等。以便于你查看运行时的变量,以及运行时的流程。

当进程被gdb停住时,你可以使用info program
来查看程序的是否在运行,进程号,被暂停的原因。

在gdb中,我们可以有以下几种暂停方式:断点(BreakPoint)、观察点(WatchPoint)、捕捉点(CatchPoint)、信号(Signals)、线程停止(Thread
Stops)。如果要恢复程序运行,可以使用c或是continue命令。

一、设置断点(BreakPoint)

我们用break命令来设置断点。正面有几点设置断点的方法:

break
在进入指定函数时停住。C++中可以使用class::function或function(type,type)格式来指定函数名。

break
在指定行号停住。

break +offset
break -offset
在当前行号的前面或后面的offset行停住。offiset为自然数。

break filename:linenum
在源文件filename的linenum行处停住。

break filename:function
在源文件filename的function函数的入口处停住。

break *address
在程序运行的内存地址处停住。

break
break命令没有参数时,表示在下一条指令处停住。

break ... if
...可以是上述的参数,condition表示条件,在条件成立时停住。比如在循环境体中,可以设置break
if i=100,表示当i为100时停住程序。

查看断点时,可使用info命令,如下所示:(注:n表示断点号)
info breakpoints [n]
info break [n]


二、设置观察点(WatchPoint)

观察点一般来观察某个表达式(变量也是一种表达式)的值是否有变化了,如果有变化,马上停住程序。我们有下面的几种方法来设置观察点:

watch
为表达式(变量)expr设置一个观察点。一量表达式值有变化时,马上停住程序。

rwatch
当表达式(变量)expr被读时,停住程序。

awatch
当表达式(变量)的值被读或被写时,停住程序。

info watchpoints
列出当前所设置了的所有观察点。

三、设置捕捉点(CatchPoint)

你可设置捕捉点来补捉程序运行时的一些事件。如:载入共享库(动态链接库)或是C++的异常。设置捕捉点的格式为:

catch
当event发生时,停住程序。event可以是下面的内容:
1、throw 一个C++抛出的异常。(throw为关键字)
2、catch 一个C++捕捉到的异常。(catch为关键字)
3、exec 调用系统调用exec时。(exec为关键字,目前此功能只在HP-UX下有用)
4、fork 调用系统调用fork时。(fork为关键字,目前此功能只在HP-UX下有用)
5、vfork 调用系统调用vfork时。(vfork为关键字,目前此功能只在HP-UX下有用)
6、load 或 load
载入共享库(动态链接库)时。(load为关键字,目前此功能只在HP-UX下有用)
7、unload 或 unload
卸载共享库(动态链接库)时。(unload为关键字,目前此功能只在HP-UX下有用)

tcatch
只设置一次捕捉点,当程序停住以后,应点被自动删除。

四、维护停止点

上面说了如何设置程序的停止点,GDB中的停止点也就是上述的三类。在GDB中,如果你觉得已定义好的停止点没有用了,你可以使用delete、clear、disable、enable这几个命令来进行维护。

clear
清除所有的已定义的停止点。

clear
clear
清除所有设置在函数上的停止点。

clear
clear
清除所有设置在指定行上的停止点。

delete [breakpoints] [range...]
删除指定的断点,breakpoints为断点号。如果不指定断点号,则表示删除所有的断点。range
表示断点号的范围(如:3-7)。其简写命令为d。

比删除更好的一种方法是disable停止点,disable了的停止点,GDB不会删除,当你还需要时,enable即可,就好像回收站一样。

disable [breakpoints] [range...]
disable所指定的停止点,breakpoints为停止点号。如果什么都不指定,表示disable所有的停止点。简写命令是dis.

enable [breakpoints] [range...]
enable所指定的停止点,breakpoints为停止点号。

enable [breakpoints] once range...
enable所指定的停止点一次,当程序停止后,该停止点马上被GDB自动disable。

enable [breakpoints] delete range...
enable所指定的停止点一次,当程序停止后,该停止点马上被GDB自动删除。

五、停止条件维护

前面在说到设置断点时,我们提到过可以设置一个条件,当条件成立时,程序自动停止,这是一个非常强大的功能,这里,我想专门说说这个条件的相关维护命令。一般来说,为断点设置一个条件,我们使用if关键词,后面跟其断点条件。并且,条件设置好后,我们可以用condition命令来修改断点的条件。(只有break和watch命令支持if,catch目前暂不支持if)

condition
修改断点号为bnum的停止条件为expression。

condition
清除断点号为bnum的停止条件。


还有一个比较特殊的维护命令ignore,你可以指定程序运行时,忽略停止条件几次。

ignore
表示忽略断点号为bnum的停止条件count次。

六、为停止点设定运行命令

我们可以使用GDB提供的command命令来设置停止点的运行命令。也就是说,当运行的程序在被停止住时,我们可以让其自动运行一些别的命令,这很有利行自动化调试。对基于GDB的自动化调试是一个强大的支持。


commands [bnum]
... command-list ...
end

为断点号bnum指写一个命令列表。当程序被该断点停住时,gdb会依次运行命令列表中的命令。

例如:

break foo if x>0
commands
printf "x is %d\n",x
continue
end
断点设置在函数foo中,断点条件是x>0,如果程序被断住后,也就是,一旦x的值在foo函数中大于0,GDB会自动打印出x的值,并继续运行程序。

如果你要清除断点上的命令序列,那么只要简单的执行一下commands命令,并直接在打个end就行了。

七、断点菜单

在C++中,可能会重复出现同一个名字的函数若干次(函数重载),在这种情况下,break
不能告诉GDB要停在哪个函数的入口。当然,你可以使用break
也就是把函数的参数类型告诉GDB,以指定一个函数。否则的话,GDB会给你列出一个断点菜单供你选择你所需要的断点。你只要输入你菜单列表中的编号就可以了。如:

(gdb) b String::after
[0] cancel
[1] all
[2] file:String.cc; line number:867
[3] file:String.cc; line number:860
[4] file:String.cc; line number:875
[5] file:String.cc; line number:853
[6] file:String.cc; line number:846
[7] file:String.cc; line number:735
> 2 4 6
Breakpoint 1 at 0xb26c: file String.cc, line 867.
Breakpoint 2 at 0xb344: file String.cc, line 875.
Breakpoint 3 at 0xafcc: file String.cc, line 846.
Multiple breakpoints were set.
Use the "delete" command to delete unwanted
breakpoints.
(gdb)

可见,GDB列出了所有after的重载函数,你可以选一下列表编号就行了。0表示放弃设置断点,1表示所有函数都设置断点。

八、恢复程序运行和单步调试

当程序被停住了,你可以用continue命令恢复程序的运行直到程序结束,或下一个断点到来。也可以使用step或next命令单步跟踪程序。

continue [ignore-count]
c [ignore-count]
fg [ignore-count]
恢复程序运行,直到程序结束,或是下一个断点到来。ignore-count表示忽略其后的断点次数。continue,c,fg三个命令都是一样的意思。


step
单步跟踪,如果有函数调用,他会进入该函数。进入函数的前提是,此函数被编译有debug信息。很像VC等工具中的step
in。后面可以加count也可以不加,不加表示一条条地执行,加表示执行后面的count条指令,然后再停住。

next
同样单步跟踪,如果有函数调用,他不会进入该函数。很像VC等工具中的step
over。后面可以加count也可以不加,不加表示一条条地执行,加表示执行后面的count条指令,然后再停住。

set step-mode
set step-mode on
打开step-mode模式,于是,在进行单步跟踪时,程序不会因为没有debug信息而不停住。这个参数有很利于查看机器码。

set step-mod off
关闭step-mode模式。

finish
运行程序,直到当前函数完成返回。并打印函数返回时的堆栈地址和返回值及参数值等信息。

until 或 u
当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。

stepi 或 si
nexti 或 ni
单步跟踪一条机器指令!一条程序代码有可能由数条机器指令完成,stepi和nexti可以单步执行机器指令。与之一样有相同功能的命令是"display/i
$pc"
,当运行完这个命令后,单步跟踪会在打出程序代码的同时打出机器指令(也就是汇编代码)

九、信号(Signals)

信号是一种软中断,是一种处理异步事件的方法。一般来说,操作系统都支持许多信号。尤其是UNIX,比较重要应用程序一般都会处理信号。UNIX定义了许多信号,比如SIGINT表示中断字符信号,也就是Ctrl+C的信号,SIGBUS表示硬件故障的信号;SIGCHLD表示子进程状态改变信号;SIGKILL表示终止程序运行的信号,等等。信号量编程是UNIX下非常重要的一种技术。

GDB有能力在你调试程序的时候处理任何一种信号,你可以告诉GDB需要处理哪一种信号。你可以要求GDB收到你所指定的信号时,马上停住正在运行的程序,以供你进行调试。你可以用GDB的handle命令来完成这一功能。

handle
在GDB中定义一个信号处理。信号可以以SIG开头或不以SIG开头,可以用定义一个要处理信号的范围(如:SIGIO-SIGKILL,表示处理从SIGIO信号到SIGKILL的信号,其中包括SIGIO,SIGIOT,SIGKILL三个信号),也可以使用关键字all来标明要处理所有的信号。一旦被调试的程序接收到信号,运行程序马上会被GDB停住,以供调试。其可以是以下几种关键字的一个或多个。

nostop
当被调试的程序收到信号时,GDB不会停住程序的运行,但会打出消息告诉你收到这种信号。
stop
当被调试的程序收到信号时,GDB会停住你的程序。
print
当被调试的程序收到信号时,GDB会显示出一条信息。
noprint
当被调试的程序收到信号时,GDB不会告诉你收到信号的信息。
pass
noignore
当被调试的程序收到信号时,GDB不处理信号。这表示,GDB会把这个信号交给被调试程序会处理。
nopass
ignore
当被调试的程序收到信号时,GDB不会让被调试程序来处理这个信号。


info signals
info handle
查看有哪些信号在被GDB检测中。

十、线程(Thread Stops)

如果你程序是多线程的话,你可以定义你的断点是否在所有的线程上,或是在某个特定的线程。GDB很容易帮你完成这一工作。

break thread
break thread if ...
linespec指定了断点设置在的源程序的行号。threadno指定了线程的ID,注意,这个ID是GDB分配的,你可以通过"info
threads"命令来查看正在运行程序中的线程信息。如果你不指定thread
则表示你的断点设在所有线程上面。你还可以为某线程指定断点条件。如:

(gdb) break frik.c:13 thread 28 if bartab > lim

当你的程序被GDB停住时,所有的运行线程都会被停住。这方便你你查看运行程序的总体情况。而在你恢复程序运行时,所有的线程也会被恢复运行。那怕是主进程在被单步调试时。

查看栈信息
—————

当程序被停住了,你需要做的第一件事就是查看程序是在哪里停住的。当你的程序调用了一个函数,函数的地址,函数参数,函数内的局部变量都会被压入"栈"(Stack)中。你可以用GDB命令来查看当前的栈中的信息。

下面是一些查看函数调用栈信息的GDB命令:

backtrace
bt
打印当前的函数调用栈的所有信息。如:

(gdb) bt
#0 func (n=250) at tst.c:6
#1 0x08048524 in main (argc=1, argv=0xbffff674) at tst.c:30
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6

从上可以看出函数的调用栈信息:__libc_start_main --> main() --> func()


backtrace
bt
n是一个正整数,表示只打印栈顶上n层的栈信息。

backtrace <-n>
bt <-n>
-n表一个负整数,表示只打印栈底下n层的栈信息。

如果你要查看某一层的信息,你需要在切换当前的栈,一般来说,程序停止时,最顶层的栈就是当前栈,如果你要查看栈下面层的详细信息,首先要做的是切换当前栈。

frame
f
n是一个从0开始的整数,是栈中的层编号。比如:frame 0,表示栈顶,frame
1,表示栈的第二层。

up
表示向栈的上面移动n层,可以不打n,表示向上移动一层。

down
表示向栈的下面移动n层,可以不打n,表示向下移动一层。

上面的命令,都会打印出移动到的栈层的信息。如果你不想让其打出信息。你可以使用这三个命令:

select-frame 对应于 frame 命令。
up-silently 对应于 up 命令。
down-silently 对应于 down 命令。


查看当前栈层的信息,你可以用以下GDB命令:

frame 或 f
会打印出这些信息:栈的层编号,当前的函数名,函数参数值,函数所在文件及行号,函数执行到的语句。

info frame
info f
这个命令会打印出更为详细的当前栈层的信息,只不过,大多数都是运行时的内内地址。比如:函数地址,调用函数的地址,被调用函数的地址,目前的函数是由什么样的程序语言写成的、函数参数地址及值、局部变量的地址等等。如:
(gdb) info f
Stack level 0, frame at 0xbffff5d4:
eip = 0x804845d in func (tst.c:6); saved eip 0x8048524
called by frame at 0xbffff60c
source language c.
Arglist at 0xbffff5d4, args: n=250
Locals at 0xbffff5d4, Previous frame's sp is 0x0
Saved registers:
ebp at 0xbffff5d4, eip at 0xbffff5d8

info args
打印出当前函数的参数名及其值。

info locals
打印出当前函数中所有局部变量及其值。

info catch
打印出当前的函数中的异常处理信息。


查看源程序
—————

一、显示源代码

GDB
可以打印出所调试程序的源代码,当然,在程序编译时一定要加上-g的参数,把源程序信息编译到执行文件中。不然就看不到源程序了。当程序停下来以后,GDB会报告程序停在了那个文件的第几行上。你可以用list命令来打印程序的源代码。还是来看一看查看源代码的GDB命令吧。

list
显示程序第linenum行的周围的源程序。

list
显示函数名为function的函数的源程序。

list
显示当前行后面的源程序。

list -
显示当前行前面的源程序。

一般是打印当前行的上5行和下5行,如果显示函数是是上2行下8行,默认是10行,当然,你也可以定制显示的范围,使用下面命令可以设置一次显示源程序的行数。

set listsize
设置一次显示源代码的行数。

show listsize
查看当前listsize的设置。

list命令还有下面的用法:

list ,
显示从first行到last行之间的源代码。

list ,
显示从当前行到last行之间的源代码。

list +
往后显示源代码。

一般来说在list后面可以跟以下这们的参数:

行号。
<+offset> 当前行号的正偏移量。
<-offset> 当前行号的负偏移量。
哪个文件的哪一行。
函数名。
哪个文件中的哪个函数。
<*address> 程序运行时的语句在内存中的地址。

二、搜索源代码

不仅如此,GDB还提供了源代码搜索的命令:

forward-search
search
向前面搜索。

reverse-search
全部搜索。

其中,就是正则表达式,也主一个字符串的匹配模式,关于正则表达式,我就不在这里讲了,还请各位查看相关资料。


三、指定源文件的路径

某些时候,用-g编译过后的执行程序中只是包括了源文件的名字,没有路径名。GDB提供了可以让你指定源文件的路径的命令,以便GDB进行搜索。

directory
dir
加一个源文件路径到当前路径的前面。如果你要指定多个路径,UNIX下你可以使用":",Windows下你可以使用";"。
directory
清除所有的自定义的源文件搜索路径信息。

show directories
显示定义了的源文件搜索路径。

四、源代码的内存

你可以使用info line命令来查看源代码在内存中的地址。info
line后面可以跟"行号","函数名","文件名:行号","文件名:函数名",这个命令会打印出所指定的源码在运行时的内存地址,如:

(gdb) info line tst.c:func
Line 5 of "tst.c" starts at address 0x8048456 and ends at 0x804845d .

还有一个命令(disassemble)你可以查看源程序的当前执行时的机器码,这个命令会把目前内存中的指令dump出来。如下面的示例表示查看函数func的汇编代码。

(gdb) disassemble func
Dump of assembler code for function func:
0x8048450 : push %ebp
0x8048451 : mov %esp,%ebp
0x8048453 : sub $0x18,%esp
0x8048456 : movl $0x0,0xfffffffc(%ebp)
0x804845d : movl $0x1,0xfffffff8(%ebp)
0x8048464 : mov 0xfffffff8(%ebp),%eax
0x8048467 : cmp 0x8(%ebp),%eax
0x804846a : jle 0x8048470
0x804846c : jmp 0x8048480
0x804846e : mov %esi,%esi
0x8048470 : mov 0xfffffff8(%ebp),%eax
0x8048473 : add %eax,0xfffffffc(%ebp)
0x8048476 : incl 0xfffffff8(%ebp)
0x8048479 : jmp 0x8048464
0x804847b : nop
0x804847c : lea 0x0(%esi,1),%esi
0x8048480 : mov 0xfffffffc(%ebp),%edx
0x8048483 : mov %edx,%eax
0x8048485 : jmp 0x8048487
0x8048487 : mov %ebp,%esp
0x8048489 : pop %ebp
0x804848a : ret
End of assembler dump.


查看运行时数据
———————

在你调试程序时,当程序被停住时,你可以使用print命令(简写命令为p),或是同义命令inspect来查看当前程序的运行数据。print命令的格式是:

print
print /
是表达式,是你所调试的程序的语言的表达式(GDB可以调试多种编程语言),是输出的格式,比如,如果要把表达式按16进制的格式输出,那么就是/x。


一、表达式

print和许多GDB的命令一样,可以接受一个表达式,GDB会根据当前的程序运行的数据来计算这个表达式,既然是表达式,那么就可以是当前程序运行中的const常量、变量、函数等内容。可惜的是GDB不能使用你在程序中所定义的宏。

表达式的语法应该是当前所调试的语言的语法,由于C/C++是一种大众型的语言,所以,本文中的例子都是关于C/C++的。(而关于用GDB调试其它语言的章节,我将在后面介绍)

在表达式中,有几种GDB所支持的操作符,它们可以用在任何一种语言中。

@
是一个和数组有关的操作符,在后面会有更详细的说明。

::
指定一个在文件或是一个函数中的变量。

{}
表示一个指向内存地址的类型为type的一个对象。


二、程序变量

在GDB中,你可以随时查看以下三种变量的值:
1、全局变量(所有文件可见的)
2、静态全局变量(当前文件可见的)
3、局部变量(当前Scope可见的)

如果你的局部变量和全局变量发生冲突(也就是重名),一般情况下是局部变量会隐藏全局变量,也就是说,如果一个全局变量和一个函数中的局部变量同名时,如果当前停止点在函数中,用print显示出的变量的值会是函数中的局部变量的值。如果此时你想查看全局变量的值时,你可以使用"::"操作符:

file::variable
function::variable
可以通过这种形式指定你所想查看的变量,是哪个文件中的或是哪个函数中的。例如,查看文件f2.c中的全局变量x的值:

gdb) p 'f2.c'::x

当然,"::"操作符会和C++中的发生冲突,GDB能自动识别"::"
是否C++的操作符,所以你不必担心在调试C++程序时会出现异常。

另外,需要注意的是,如果你的程序编译时开启了优化选项,那么在用GDB调试被优化过的程序时,可能会发生某些变量不能访问,或是取值错误码的情况。这个是很正常的,因为优化程序会删改你的程序,整理你程序的语句顺序,剔除一些无意义的变量等,所以在GDB调试这种程序时,运行时的指令和你所编写指令就有不一样,也就会出现你所想象不到的结果。对付这种情况时,需要在编译程序时关闭编译优化。一般来说,几乎所有的编译器都支持编译优化的开关,例如,GNU的C/C++编译器GCC,你可以使用"-gstabs"选项来解决这个问题。关于编译器的参数,还请查看编译器的使用说明文档。

三、数组

有时候,你需要查看一段连续的内存空间的值。比如数组的一段,或是动态分配的数据的大小。你可以使用GDB的"@"操作符,"@"的左边是第一个内存的地址的值,"@"的右边则你你想查看内存的长度。例如,你的程序中有这样的语句:

int *array = (int *) malloc (len * sizeof (int));

于是,在GDB调试过程中,你可以以如下命令显示出这个动态数组的取值:

p *array@len

@的左边是数组的首地址的值,也就是变量array所指向的内容,右边则是数据的长度,其保存在变量len中,其输出结果,大约是下面这个样子的:

(gdb) p *array@len
$1 = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36,
38, 40}

如果是静态数组的话,可以直接用print数组名,就可以显示数组中所有数据的内容了。


四、输出格式

一般来说,GDB会根据变量的类型输出变量的值。但你也可以自定义GDB的输出的格式。例如,你想输出一个整数的十六进制,或是二进制来查看这个整型变量的中的位的情况。要做到这样,你可以使用GDB的数据显示格式:

x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。

(gdb) p i
$21 = 101

(gdb) p/a i
$22 = 0x65

(gdb) p/c i
$23 = 101 'e'

(gdb) p/f i
$24 = 1.41531145e-43

(gdb) p/x i
$25 = 0x65

(gdb) p/t i
$26 = 1100101


五、查看内存

你可以使用examine命令(简写是x)来查看内存地址中的值。x命令的语法如下所示:

x/

n、f、u是可选的参数。

n
是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。
f
表示显示的格式,参见上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。
u
表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。

表示一个内存地址。

n/f/u三个参数可以一起使用。例如:

命令:x/3uh 0x54320
表示,从内存地址0x54320读取内容,h表示以双字节为一个单位,3表示三个单位,u表示按十六进制显示。


六、自动显示

你可以设置一些自动显示的变量,当程序停住时,或是在你单步跟踪时,这些变量会自动显示。相关的GDB命令是display。

display
display/
display/

expr是一个表达式,fmt表示显示的格式,addr表示内存地址,当你用display设定好了一个或多个表达式后,只要你的程序被停下来,GDB会自动显示你所设置的这些表达式的值。

格式i和s同样被display支持,一个非常有用的命令是:

display/i $pc

$pc是GDB的环境变量,表示着指令的地址,/i则表示输出格式为机器指令码,也就是汇编。于是当程序停下后,就会出现源代码和机器指令码相对应的情形,这是一个很有意思的功能。

下面是一些和display相关的GDB命令:

undisplay
delete display
删除自动显示,dnums意为所设置好了的自动显式的编号。如果要同时删除几个,编号可以用空格分隔,如果要删除一个范围内的编号,可以用减号表示(如:2-5)

disable display
enable display
disable和enalbe不删除自动显示的设置,而只是让其失效和恢复。

info display
查看display设置的自动显示的信息。GDB会打出一张表格,向你报告当然调试中设置了多少个自动显示设置,其中包括,设置的编号,表达式,是否enable。


七、设置显示选项

GDB中关于显示的选项比较多,这里我只例举大多数常用的选项。

set print address
set print address on
打开地址输出,当程序显示函数信息时,GDB会显出函数的参数地址。系统默认为打开的,如:

(gdb) f
#0 set_quotes (lq=0x34c78 "<<", rq=0x34c88 ">>")
at input.c:530
530 if (lquote != def_lquote)


set print address off
关闭函数的参数地址显示,如:

(gdb) set print addr off
(gdb) f
#0 set_quotes (lq="<<", rq=">>") at input.c:530
530 if (lquote != def_lquote)

show print address
查看当前地址显示选项是否打开。

set print array
set print array on
打开数组显示,打开后当数组显示时,每个元素占一行,如果不打开的话,每个元素则以逗号分隔。这个选项默认是关闭的。与之相关的两个命令如下,我就不再多说了。

set print array off
show print array

set print elements
这个选项主要是设置数组的,如果你的数组太大了,那么就可以指定一个来指定数据显示的最大长度,当到达这个长度时,GDB就不再往下显示了。如果设置为0,则表示不限制。

show print elements
查看print elements的选项信息。

set print null-stop
如果打开了这个选项,那么当显示字符串时,遇到结束符则停止显示。这个选项默认为off。

set print pretty on
如果打开printf pretty这个选项,那么当GDB显示结构体时会比较漂亮。如:

$1 = {
next = 0x0,
flags = {
sweet = 1,
sour = 1
},
meat = 0x54 "Pork"
}

set print pretty off
关闭printf pretty这个选项,GDB显示结构体时会如下显示:

$1 = {next = 0x0, flags = {sweet = 1, sour = 1}, meat = 0x54 "Pork"}

show print pretty
查看GDB是如何显示结构体的。


set print sevenbit-strings
设置字符显示,是否按"\nnn"的格式显示,如果打开,则字符串或字符数据按\nnn显示,如"\065"。

show print sevenbit-strings
查看字符显示开关是否打开。

set print union
设置显示结构体时,是否显式其内的联合体数据。例如有以下数据结构:

typedef enum {Tree, Bug} Species;
typedef enum {Big_tree, Acorn, Seedling} Tree_forms;
typedef enum {Caterpillar, Cocoon, Butterfly}
Bug_forms;

struct thing {
Species it;
union {
Tree_forms tree;
Bug_forms bug;
} form;
};

struct thing foo = {Tree, {Acorn}};

当打开这个开关时,执行 p foo 命令后,会如下显示:
$1 = {it = Tree, form = {tree = Acorn, bug = Cocoon}}

当关闭这个开关时,执行 p foo 命令后,会如下显示:
$1 = {it = Tree, form = {...}}

show print union
查看联合体数据的显示方式

set print object
在C++中,如果一个对象指针指向其派生类,如果打开这个选项,GDB会自动按照虚方法调用的规则显示输出,如果关闭这个选项的话,GDB就不管虚函数表了。这个选项默认是off。

show print object
查看对象选项的设置。

set print static-members
这个选项表示,当显示一个C++对象中的内容是,是否显示其中的静态数据成员。默认是on。

show print static-members
查看静态数据成员选项设置。

set print vtbl
当此选项打开时,GDB将用比较规整的格式来显示虚函数表时。其默认是关闭的。

show print vtbl
查看虚函数显示格式的选项。


八、历史记录

当你用GDB的print查看程序运行时的数据时,你每一个print都会被GDB记录下来。GDB会以$1,
$2, $3
.....这样的方式为你每一个print命令编上号。于是,你可以使用这个编号访问以前的表达式,如$1。这个功能所带来的好处是,如果你先前输入了一个比较长的表达式,如果你还想查看这个表达式的值,你可以使用历史记录来访问,省去了重复输入。


九、GDB环境变量

你可以在GDB的调试环境中定义自己的变量,用来保存一些调试程序中的运行数据。要定义一个GDB的变量很简单只需。使用GDB的set命令。GDB的环境变量和UNIX一样,也是以$起头。如:

set $foo = *object_ptr

使用环境变量时,GDB会在你第一次使用时创建这个变量,而在以后的使用中,则直接对其賦值。环境变量没有类型,你可以给环境变量定义任一的类型。包括结构体和数组。

show convenience
该命令查看当前所设置的所有的环境变量。

这是一个比较强大的功能,环境变量和程序变量的交互使用,将使得程序调试更为灵活便捷。例如:

set $i = 0
print bar[$i++]->contents

于是,当你就不必,print bar[0]->contents, print
bar[1]->contents地输入命令了。输入这样的命令后,只用敲回车,重复执行上一条语句,环境变量会自动累加,从而完成逐个输出的功能。


十、查看寄存器

要查看寄存器的值,很简单,可以使用如下命令:

info registers
查看寄存器的情况。(除了浮点寄存器)

info all-registers
查看所有寄存器的情况。(包括浮点寄存器)

info registers
查看所指定的寄存器的情况。

寄存器中放置了程序运行时的数据,比如程序当前运行的指令地址(ip),程序的当前堆栈地址(sp)等等。你同样可以使用print命令来访问寄存器的情况,只需要在寄存器名字前加一个$符号就可以了。如:p
$eip。

改变程序的执行
———————

一旦使用GDB挂上被调试程序,当程序运行起来后,你可以根据自己的调试思路来动态地在GDB中更改当前被调试程序的运行线路或是其变量的值,这个强大的功能能够让你更好的调试你的程序,比如,你可以在程序的一次运行中走遍程序的所有分支。


一、修改变量值

修改被调试程序运行时的变量值,在GDB中很容易实现,使用GDB的print命令即可完成。如:

(gdb) print x=4

x=4这个表达式是C/C++的语法,意为把变量x的值修改为4,如果你当前调试的语言是Pascal,那么你可以使用Pascal的语法:x:=4。

在某些时候,很有可能你的变量和GDB中的参数冲突,如:

(gdb) whatis width
type = double
(gdb) p width
$4 = 13
(gdb) set width=47
Invalid syntax in expression.

因为,set width是GDB的命令,所以,出现了"Invalid syntax in
expression"的设置错误,此时,你可以使用set
var命令来告诉GDB,width不是你GDB的参数,而是程序的变量名,如:

(gdb) set var width=47

另外,还可能有些情况,GDB并不报告这种错误,所以保险起见,在你改变程序变量取值时,最好都使用set
var格式的GDB命令。

二、跳转执行

一般来说,被调试程序会按照程序代码的运行顺序依次执行。GDB提供了乱序执行的功能,也就是说,GDB可以修改程序的执行顺序,可以让程序执行随意跳跃。这个功能可以由GDB的jump命令来完:

jump
指定下一条语句的运行点。可以是文件的行号,可以是file:line格式,可以是+num这种偏移量格式。表式着下一条运行语句从哪里开始。

jump

这里的
是代码行的内存地址。

注意,jump命令不会改变当前的程序栈中的内容,所以,当你从一个函数跳到另一个函数时,当函数运行完返回时进行弹栈操作时必然会发生错误,可能结果还是非常奇怪的,甚至于产生程序Core
Dump。所以最好是同一个函数中进行跳转。

熟悉汇编的人都知道,程序运行时,有一个寄存器用于保存当前代码所在的内存地址。所以,jump命令也就是改变了这个寄存器中的值。于是,你可以使用"set
$pc"来更改跳转执行的地址。如:

set $pc = 0x485

三、产生信号量

使用singal命令,可以产生一个信号量给被调试的程序。如:中断信号Ctrl+C。这非常方便于程序的调试,可以在程序运行的任意位置设置断点,并在该断点用GDB产生一个信号量,这种精确地在某处产生信号非常有利程序的调试。

语法是:signal ,UNIX的系统信号量通常从1到15。所以取值也在这个范围。

single命令和shell的kill命令不同,系统的kill命令发信号给被调试程序时,是由GDB截获的,而single命令所发出一信号则是直接发给被调试程序的。

四、强制函数返回

如果你的调试断点在某个函数中,并还有语句没有执行完。你可以使用return命令强制函数忽略还没有执行的语句并返回。

return
return
使用return命令取消当前函数的执行,并立即返回,如果指定了,那么该表达式的值会被认作函数的返回值。


五、强制调用函数

call
表达式中可以一是函数,以此达到强制调用函数的目的。并显示函数的返回值,如果函数返回值是void,那么就不显示。

另一个相似的命令也可以完成这一功能——print,print后面可以跟表达式,所以也可以用他来调用函数,print和call的不同是,如果函数返回void,call则不显示,print则显示函数返回值,并把该值存入历史数据中。


在不同语言中使用GDB
——————————

GDB支持下列语言:C, C++, Fortran, PASCAL, Java, Chill, assembly, 和
Modula-2。一般说来,GDB会根据你所调试的程序来确定当然的调试语言,比如:发现文件名后缀为".c"的,GDB会认为是C程序。文件名后缀为".C,
.cc, .cp, .cpp, .cxx, .c++"的,GDB会认为是C++程序。而后缀是".f,
.F"的,GDB会认为是Fortran程序,还有,后缀为如果是".s,
.S"的会认为是汇编语言。

也就是说,GDB会根据你所调试的程序的语言,来设置自己的语言环境,并让GDB的命令跟着语言环境的改变而改变。比如一些GDB命令需要用到表达式或变量时,这些表达式或变量的语法,完全是根据当前的语言环境而改变的。例如C/C++中对指针的语法是*p,而在Modula-2中则是p^。并且,如果你当前的程序是由几种不同语言一同编译成的,那到在调试过程中,GDB也能根据不同的语言自动地切换语言环境。这种跟着语言环境而改变的功能,真是体贴开发人员的一种设计。


下面是几个相关于GDB语言环境的命令:

show language
查看当前的语言环境。如果GDB不能识为你所调试的编程语言,那么,C语言被认为是默认的环境。

info frame
查看当前函数的程序语言。

info source
查看当前文件的程序语言。

如果GDB没有检测出当前的程序语言,那么你也可以手动设置当前的程序语言。使用set
language命令即可做到。

当set language命令后什么也不跟的话,你可以查看GDB所支持的语言种类:

(gdb) set language
The currently understood settings are:

local or auto Automatic setting based on source file
c Use the C language
c++ Use the C++ language
asm Use the Asm language
chill Use the Chill language
fortran Use the Fortran language
java Use the Java language
modula-2 Use the Modula-2 language
pascal Use the Pascal language
scheme Use the Scheme language

于是你可以在set language后跟上被列出来的程序语言名,来设置当前的语言环境。


后记
——

GDB是一个强大的命令行调试工具。大家知道命令行的强大就是在于,其可以形成执行序列,形成脚本。UNIX下的软件全是命令行的,这给程序开发提代供了极大的便利,命令行软件的优势在于,它们可以非常容易的集成在一起,使用几个简单的已有工具的命令,就可以做出一个非常强大的功能。

于是UNIX下的软件比Windows下的软件更能有机地结合,各自发挥各自的长处,组合成更为强劲的功能。而Windows下的图形软件基本上是各自为营,互相不能调用,很不利于各种软件的相互集成。在这里并不是要和Windows做个什么比较,所谓"寸有所长,尺有所短",图形化工具还是有不如命令行的地方。(看到这句话时,希望各位千万再也不要认为我就是"鄙视图形界面",和我抬杠了

我是根据版本为5.1.1的GDB所写的这篇文章,所以可能有些功能已被修改,或是又有更为强劲的功能。而且,我写得非常仓促,写得比较简略,并且,其中我已经看到有许多错别字了(我用五笔,所以错字让你看不懂),所以,我在这里对我文中的差错表示万分的歉意。

文中所罗列的GDB的功能时,我只是罗列了一些带用的GDB的命令和使用方法,其实,我这里只讲述的功能大约只占GDB所有功能的60%吧,详细的文档,还是请查看GDB的帮助和使用手册吧,或许,过段时间,如果我有空,我再写一篇GDB的高级使用。

我个人非常喜欢GDB的自动调试的功能,这个功能真的很强大,试想,我在UNIX下写个脚本,让脚本自动编译我的程序,被自动调试,并把结果报告出来,调试成功,自动checkin源码库。一个命令,编译带着调试带着checkin,多爽啊。只是GDB对自动化调试目前支持还不是很成熟,只能实现半自动化,真心期望着GDB的自动化调试功能的成熟。

UNIX/LINUX 平台可执行文件格式分析

本文讨论了 UNIX/LINUX 平台下三种主要的可执行文件格式:a.out(assembler
and link editor output 汇编器和链接编辑器的输出)、COFF(Common Object
File Format 通用对象文件格式)、ELF(Executable and Linking Format
可执行和链接格式)。首先是对可执行文件格式的一个综述,并通过描述 ELF
文件加载过程以揭示可执行文件内容与加载运行操作之间的关系。随后依此讨论了此三种文件格式,并着重讨论
ELF
文件的动态连接机制,其间也穿插了对各种文件格式优缺点的评价。最后对三种可执行文件格式有一个简单总结,并提出作者对可文件格式评价的一些感想。

可执行文件格式综述
相对于其它文件类型,可执行文件可能是一个操作系统中最重要的文件类型,因为它们是完成操作的真正执行者。可执行文件的大小、运行速度、资源占用情况以及可扩展性、可移植性等与文件格式的定义和文件加载过程紧密相关。研究可执行文件的格式对编写高性能程序和一些黑客技术的运用都是非常有意义的。

不管何种可执行文件格式,一些基本的要素是必须的,显而易见的,文件中应包含代码和数据。因为文件可能引用外部文件定义的符号(变量和函数),因此重定位信息和符号信息也是需要的。一些辅助信息是可选的,如调试信息、硬件信息等。基本上任意一种可执行文件格式都是按区间保存上述信息,称为段(Segment)或节(Section)。不同的文件格式中段和节的含义可能有细微区别,但根据上下文关系可以很清楚的理解,这不是关键问题。最后,可执行文件通常都有一个文件头部以描述本文件的总体结构。

相对可执行文件有三个重要的概念:编译(compile)、连接(link,也可称为链接、联接)、加载(load)。源程序文件被编译成目标文件,多个目标文件被连接成一个最终的可执行文件,可执行文件被加载到内存中运行。因为本文重点是讨论可执行文件格式,因此加载过程也相对重点讨论。下面是LINUX平台下ELF文件加载过程的一个简单描述。

1:内核首先读ELF文件的头部,然后根据头部的数据指示分别读入各种数据结构,找到标记为可加载(loadable)的段,并调用函数
mmap()把段内容加载到内存中。在加载之前,内核把段的标记直接传递给
mmap(),段的标记指示该段在内存中是否可读、可写,可执行。显然,文本段是只读可执行,而数据段是可读可写。这种方式是利用了现代操作系统和处理器对内存的保护功能。著名的Shellcode(参考资料
17)的编写技巧则是突破此保护功能的一个实际例子。

2:内核分析出ELF文件标记为 PT_INTERP
的段中所对应的动态连接器名称,并加载动态连接器。现代 LINUX
系统的动态连接器通常是 /lib/ld-linux.so.2,相关细节在后面有详细描述。

3:内核在新进程的堆栈中设置一些标记-值对,以指示动态连接器的相关操作。

4:内核把控制传递给动态连接器。

5:动态连接器检查程序对外部文件(共享库)的依赖性,并在需要时对其进行加载。

6:动态连接器对程序的外部引用进行重定位,通俗的讲,就是告诉程序其引用的外部变量/函数的地址,此地址位于共享库被加载在内存的区间内。动态连接还有一个延迟(Lazy)定位的特性,即只在"真正"需要引用符号时才重定位,这对提高程序运行效率有极大帮助。

7:动态连接器执行在ELF文件中标记为 .init
的节的代码,进行程序运行的初始化。在早期系统中,初始化代码对应函数
_init(void)(函数名强制固定),在现代系统中,则对应形式为


void
__attribute((constructor))
init_function(void)
{
……
}

其中函数名为任意。

8:动态连接器把控制传递给程序,从 ELF
文件头部中定义的程序进入点开始执行。在 a.out
格式和ELF格式中,程序进入点的值是显式存在的,在 COFF
格式中则是由规范隐含定义。

从上面的描述可以看出,加载文件最重要的是完成两件事情:加载程序段和数据段到内存;进行外部定义符号的重定位。重定位是程序连接中一个重要概念。我们知道,一个可执行程序通常是由一个含有
main() 的主程序文件、若干目标文件、若干共享库(Shared
Libraries)组成。(注:采用一些特别的技巧,也可编写没有 main
函数的程序,请参阅参考资料 2)一个 C
程序可能引用共享库定义的变量或函数,换句话说就是程序运行时必须知道这些变量/函数的地址。在静态连接中,程序所有需要使用的外部定义都完全包含在可执行程序中,而动态连接则只在可执行文件中设置相关外部定义的一些引用信息,真正的重定位是在程序运行之时。静态连接方式有两个大问题:如果库中变量或函数有任何变化都必须重新编译连接程序;如果多个程序引用同样的变量/函数,则此变量/函数会在文件/内存中出现多次,浪费硬盘/内存空间。比较两种连接方式生成的可执行文件的大小,可以看出有明显的区别。

a.out 文件格式分析
a.out 格式在不同的机器平台和不同的 UNIX 操作系统上有轻微的不同,例如在
MC680x0 平台上有 6 个 section。下面我们讨论的是最"标准"的格式。

a.out 文件包含 7 个 section,格式如下:
exec header(执行头部,也可理解为文件头部)
text segment(文本段)
data segment(数据段)
text relocations(文本重定位段)
data relocations(数据重定位段)
symbol table(符号表)
string table(字符串表)

执行头部的数据结构:


struct exec {
unsigned long a_midmag; /* 魔数和其它信息 */
unsigned long a_text; /* 文本段的长度 */
unsigned long a_data; /* 数据段的长度 */
unsigned long a_bss; /* BSS段的长度 */
unsigned long a_syms; /* 符号表的长度 */
unsigned long a_entry; /* 程序进入点 */
unsigned long a_trsize; /* 文本重定位表的长度 */
unsigned long a_drsize; /* 数据重定位表的长度 */
};

文件头部主要描述了各个 section 的长度,比较重要的字段是
a_entry(程序进入点),代表了系统在加载程序并初试化各种环境后开始执行程序代码的入口。这个字段在后面讨论的
ELF 文件头部中也有出现。由 a.out 格式和头部数据结构我们可以看出,a.out
的格式非常紧凑,只包含了程序运行所必须的信息(文本、数据、BSS),而且每个
section
的顺序是固定的。这种结构缺乏扩展性,如不能包含"现代"可执行文件中常见的调试信息,最初的
UNIX 黑客对 a.out 文件调试使用的工具是 adb,而 adb 是一种机器语言调试器!

a.out
文件中包含符号表和两个重定位表,这三个表的内容在连接目标文件以生成可执行文件时起作用。在最终可执行的
a.out 文件中,这三个表的长度都为 0。a.out
文件在连接时就把所有外部定义包含在可执行程序中,如果从程序设计的角度来看,这是一种硬编码方式,或者可称为模块之间是强藕和的。在后面的讨论中,我们将会具体看到ELF格式和动态连接机制是如何对此进行改进的。

a.out 是早期UNIX系统使用的可执行文件格式,由 AT&T 设计,现在基本上已被 ELF
文件格式代替。a.out
的设计比较简单,但其设计思想明显的被后续的可执行文件格式所继承和发扬。可以参阅参考资料
16 和阅读参考资料 15 源代码加深对 a.out 格式的理解。参考资料 12
讨论了如何在"现代"的红帽LINUX运行 a.out 格式文件。

COFF 文件格式分析
COFF 格式比 a.out 格式要复杂一些,最重要的是包含一个节段表(section
table),因此除了 .text,.data,和 .bss
区段以外,还可以包含其它的区段。另外也多了一个可选的头部,不同的操作系统可一对此头部做特定的定义。

COFF 文件格式如下:
File Header(文件头部)
Optional Header(可选文件头部)
Section 1 Header(节头部)
………
Section n Header(节头部)
Raw Data for Section 1(节数据)
Raw Data for Section n(节数据)
Relocation Info for Sect. 1(节重定位数据)
Relocation Info for Sect. n(节重定位数据)
Line Numbers for Sect. 1(节行号数据)
Line Numbers for Sect. n(节行号数据)
Symbol table(符号表)
String table(字符串表)

文件头部的数据结构:


struct filehdr
{
unsigned short f_magic; /* 魔数 */
unsigned short f_nscns; /* 节个数 */
long f_timdat; /* 文件建立时间 */
long f_symptr; /* 符号表相对文件的偏移量 */
long f_nsyms; /* 符号表条目个数 */
unsigned short f_opthdr; /* 可选头部长度 */
unsigned short f_flags; /* 标志 */
};

COFF
文件头部中魔数与其它两种格式的意义不太一样,它是表示针对的机器类型,例如
0x014c 相对于 I386 平台,而 0x268 相对于 Motorola 68000系列等。当 COFF
文件为可执行文件时,字段 f_flags 的值为
F_EXEC(0X00002),同时也表示此文件没有未解析的符号,换句话说,也就是重定位在连接时就已经完成。由此也可以看出,原始的
COFF
格式不支持动态连接。为了解决这个问题以及增加一些新的特性,一些操作系统对
COFF 格式进行了扩展。Microsoft 设计了名为 PE(Portable
Executable)的文件格式,主要扩展是在 COFF
文件头部之上增加了一些专用头部,具体细节请参阅参考资料 18,某些 UNIX
系统也对 COFF 格式进行了扩展,如 XCOFF(extended common object file
format)格式,支持动态连接,请参阅参考资料 5。

紧接文件头部的是可选头部,COFF 文件格式规范中规定可选头部的长度可以为
0,但在 LINUX 系统下可选头部是必须存在的。下面是 LINUX
下可选头部的数据结构:


typedef struct
{
char magic[2]; /* 魔数 */
char vstamp[2]; /* 版本号 */
char tsize[4]; /* 文本段长度 */
char dsize[4]; /* 已初始化数据段长度 */
char bsize[4]; /* 未初始化数据段长度 */
char entry[4]; /* 程序进入点 */
char text_start[4]; /* 文本段基地址 */
char data_start[4]; /* 数据段基地址 */
}
COFF_AOUTHDR;

字段 magic 为 0413 时表示 COFF
文件是可执行的,注意到可选头部中显式定义了程序进入点,标准的 COFF
文件没有明确的定义程序进入点的值,通常是从 .text
节开始执行,但这种设计并不好。

前面我们提到,COFF 格式比 a.out
格式多了一个节段表,一个节头条目描述一个节数据的细节,因此 COFF
格式能包含更多的节,或者说可以根据实际需要,增加特定的节,具体表现在 COFF
格式本身的定义以及稍早提及的 COFF 格式扩展。我个人认为,节段表的出现可能是
COFF 格式相对 a.out 格式最大的进步。下面我们将简单描述 COFF
文件中节的数据结构,因为节的意义更多体现在程序的编译和连接上,所以本文不对其做更多的描述。此外,ELF
格式和 COFF格式对节的定义非常相似,在随后的 ELF
格式分析中,我们将省略相关讨论。


struct COFF_scnhdr
{
char s_name[8]; /* 节名称 */
char s_paddr[4]; /* 物理地址 */
char s_vaddr[4]; /* 虚拟地址 */
char s_size[4]; /* 节长度 */
char s_scnptr[4]; /* 节数据相对文件的偏移量 */
char s_relptr[4]; /* 节重定位信息偏移量 */
char s_lnnoptr[4]; /* 节行信息偏移量 */
char s_nreloc[2]; /* 节重定位条目数 */
char s_nlnno[2]; /* 节行信息条目数 */
char s_flags[4]; /* 段标记 */
};

有一点需要注意:LINUX系统中头文件coff.h中对字段 s_paddr的注释是"physical
address",但似乎应该理解为"节被加载到内存中所占用的空间长度"。字段s_flags标记该节的类型,如文本段、数据段、BSS段等。在
COFF的节中也出现了行信息,行信息描述了二进制代码与源代码的行号之间的对映关系,在调试时很有用。

参考资料 19是一份对COFF格式详细描述的中文资料,更详细的内容请参阅参考资料
20。

ELF文件格式分析
ELF文件有三种类型:可重定位文件:也就是通常称的目标文件,后缀为.o。共享文件:也就是通常称的库文件,后缀为.so。可执行文件:本文主要讨论的文件格式,总的来说,可执行文件的格式与上述两种文件的格式之间的区别主要在于观察的角度不同:一种称为连接视图(Linking
View),一种称为执行视图(Execution View)。

首先看看ELF文件的总体布局:
ELF header(ELF头部)
Program header table(程序头表)
Segment1(段1)
Segment2(段2)
………
Sengmentn(段n)
Setion header table(节头表,可选)

段由若干个节(Section)构成,节头表对每一个节的信息有相关描述。对可执行程序而言,节头表是可选的。参考资料
1中作者谈到把节头表的所有数据全部设置为0,程序也能正确运行!ELF头部是一个关于本文件的路线图(road
map),从总体上描述文件的结构。下面是ELF头部的数据结构:


typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* 魔数和相关信息 */
Elf32_Half e_type; /* 目标文件类型 */
Elf32_Half e_machine; /* 硬件体系 */
Elf32_Word e_version; /* 目标文件版本 */
Elf32_Addr e_entry; /* 程序进入点 */
Elf32_Off e_phoff; /* 程序头部偏移量 */
Elf32_Off e_shoff; /* 节头部偏移量 */
Elf32_Word e_flags; /* 处理器特定标志 */
Elf32_Half e_ehsize; /* ELF头部长度 */
Elf32_Half e_phentsize; /* 程序头部中一个条目的长度 */
Elf32_Half e_phnum; /* 程序头部条目个数 */
Elf32_Half e_shentsize; /* 节头部中一个条目的长度 */
Elf32_Half e_shnum; /* 节头部条目个数 */
Elf32_Half e_shstrndx; /* 节头部字符表索引 */
} Elf32_Ehdr;

下面我们对ELF头表中一些重要的字段作出相关说明,完整的ELF定义请参阅参考资料
6和参考资料7。

e_ident[0]-e_ident[3]包含了ELF文件的魔数,依次是0x7f、'E'、'L'、'F'。注意,任何一个ELF文件必须包含此魔数。参考资料
3中讨论了利用程序、工具、/Proc文件系统等多种查看ELF魔数的方法。e_ident[4]表示硬件系统的位数,1代表32位,2代表64位。
e_ident[5]表示数据编码方式,1代表小印第安排序(最大有意义的字节占有最低的地址),2代表大印第安排序(最大有意义的字节占有最高的地址)。e_ident[6]指定ELF头部的版本,当前必须为1。e_ident[7]到e_ident[14]是填充符,通常是0。ELF格式规范中定义这几个字节是被忽略的,但实际上是这几个字节完全可以可被利用。如病毒Lin/Glaurung.676/666(参考资料
1)设置e_ident[7]为0x21,表示本文件已被感染;或者存放可执行代码(参考资料
2)。ELF头部中大多数字段都是对子头部数据的描述,其意义相对比较简单。值得注意的是某些病毒可能修改字段e_entry(程序进入点)的值,以指向病毒代码,例如上面提到的病毒Lin/Glaurung.676/666。

一个实际可执行文件的文件头部形式如下:(利用命令readelf)


ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x80483cc
Start of program headers: 52 (bytes into file)
Start of section headers: 14936 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 6
Size of section headers: 40 (bytes)
Number of section headers: 34
Section header string table index: 31


紧接ELF头部的是程序头表,它是一个结构数组,包含了ELF头表中字段e_phnum定义的条目,结构描述一个段或其他系统准备执行该程序所需要的信息。


typedef struct {
Elf32_Word p_type; /* 段类型 */
Elf32_Off p_offset; /* 段位置相对于文件开始处的偏移量 */
Elf32_Addr p_vaddr; /* 段在内存中的地址 */
Elf32_Addr p_paddr; /* 段的物理地址 */
Elf32_Word p_filesz; /* 段在文件中的长度 */
Elf32_Word p_memsz; /* 段在内存中的长度 */
Elf32_Word p_flags; /* 段的标记 */
Elf32_Word p_align; /* 段在内存中对齐标记 */
} Elf32_Phdr;

在详细讨论可执行文件程序头表之前,首先查看一个实际文件的输出:


Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4
INTERP 0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00684 0x00684 R E 0x1000
LOAD 0x000684 0x08049684 0x08049684 0x00118 0x00130 RW 0x1000
DYNAMIC 0x000690 0x08049690 0x08049690 0x000c8 0x000c8 RW 0x4
NOTE 0x000108 0x08048108 0x08048108 0x00020 0x00020 R 0x4

Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version
.gnu.version_r .rel.dyn .rel.plt
.init .plt .text .fini .rodata .eh_frame
03 .data .dynamic .ctors .dtors .jcr .got .bss
04 .dynamic
05 .note.ABI-tag

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk
Inf Al
[ 0] NULL 00000000 000000 000000 00
0 0 0
[ 1] .interp PROGBITS 080480f4 0000f4 000013 00 A
0 0 1
[ 2] .note.ABI-tag NOTE 08048108 000108 000020 00 A
0 0 4
[ 3] .hash HASH 08048128 000128 000040 04 A
4 0 4
[ 4] .dynsym DYNSYM 08048168 000168 0000b0 10 A
5 1 4
[ 5] .dynstr STRTAB 08048218 000218 00007b 00 A
0 0 1
[ 6] .gnu.version VERSYM 08048294 000294 000016 02 A
4 0 2
[ 7] .gnu.version_r VERNEED 080482ac 0002ac 000030 00 A
5 1 4
[ 8] .rel.dyn REL 080482dc 0002dc 000008 08 A
4 0 4
[ 9] .rel.plt REL 080482e4 0002e4 000040 08 A
4 b 4
[10] .init PROGBITS 08048324 000324 000017 00 AX
0 0 4
[11] .plt PROGBITS 0804833c 00033c 000090 04 AX
0 0 4
[12] .text PROGBITS 080483cc 0003cc 0001f8 00 AX
0 0 4
[13] .fini PROGBITS 080485c4 0005c4 00001b 00 AX
0 0 4
[14] .rodata PROGBITS 080485e0 0005e0 00009f 00 A
0 0 32
[15] .eh_frame PROGBITS 08048680 000680 000004 00 A
0 0 4
[16] .data PROGBITS 08049684 000684 00000c 00 WA
0 0 4
[17] .dynamic DYNAMIC 08049690 000690 0000c8 08 WA
5 0 4
[18] .ctors PROGBITS 08049758 000758 000008 00 WA
0 0 4
[19] .dtors PROGBITS 08049760 000760 000008 00 WA
0 0 4
[20] .jcr PROGBITS 08049768 000768 000004 00 WA
0 0 4
[21] .got PROGBITS 0804976c 00076c 000030 04 WA
0 0 4
[22] .bss NOBITS 0804979c 00079c 000018 00 WA
0 0 4
[23] .comment PROGBITS 00000000 00079c 000132 00
0 0 1
[24] .debug_aranges PROGBITS 00000000 0008d0 000098 00
0 0 8
[25] .debug_pubnames PROGBITS 00000000 000968 000040 00
0 0 1
[26] .debug_info PROGBITS 00000000 0009a8 001cc6 00
0 0 1
[27] .debug_abbrev PROGBITS 00000000 00266e 0002cc 00
0 0 1
[28] .debug_line PROGBITS 00000000 00293a 0003dc 00
0 0 1
[29] .debug_frame PROGBITS 00000000 002d18 000048 00
0 0 4
[30] .debug_str PROGBITS 00000000 002d60 000bcd 01 MS
0 0 1
[31] .shstrtab STRTAB 00000000 00392d 00012b 00
0 0 1
[32] .symtab SYMTAB 00000000 003fa8 000740 10 33
56 4
[33] .strtab STRTAB 00000000 0046e8 000467 00
0 0 1


对一个ELF可执行程序而言,一个基本的段是标记p_type为PT_INTERP的段,它表明了运行此程序所需要的程序解释器(/lib/ld-
linux.so.2),实际上也就是动态连接器(dynamic
linker)。最重要的段是标记p_type为PT_LOAD的段,它表明了为运行程序而需要加载到内存的数据。查看上面实际输入,可以看见有两个可
LOAD段,第一个为只读可执行(FLg为R
E),第二个为可读可写(Flg为RW)。段1包含了文本节.text,注意到ELF文件头部中程序进入点的值为0x80483cc,正好是指向节.
text在内存中的地址。段二包含了数据节.data,此数据节中数据是可读可写的,相对的只读数据节.rodata包含在段1中。ELF格式可以比
COFF格式包含更多的调试信息,如上面所列出的形式为.debug_xxx的节。在I386平台LINUX系统下,用命令file查看一个ELF可执行程序的可能输出是:a.out:
ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux
2.2.5, dynamically linked (uses shared libs), not stripped。

ELF文件中包含了动态连接器的全路径,内核定位"正确"的动态连接器在内存中的地址是"正确"运行可执行文件的保证,参考资料
13讨论了如何通过查找动态连接器在内存中的地址以达到颠覆(Subversiver)动态连接机制的方法。

最后我们讨论ELF文件的动态连接机制。每一个外部定义的符号在全局偏移表(Global
Offset Table GOT)中有相应的条目,如果符号是函数则在过程连接表(Procedure
Linkage Table
PLT)中也有相应的条目,且一个PLT条目对应一个GOT条目。对外部定义函数解析可能是整个ELF文件规范中最复杂的,下面是函数符号解析过程的一个描述。

1:代码中调用外部函数func,语句形式为call
0xaabbccdd,地址0xaabbccdd实际上就是符号func在PLT表中对应的条目地址(假设地址为标号.PLT2)。

2:PLT表的形式如下


.PLT0: pushl 4(%ebx) /* GOT表的地址保存在寄存器ebx中 */
jmp *8(%ebx)
nop; nop
nop; nop
.PLT1: jmp *name1@GOT(%ebx)
pushl $offset
jmp .PLT0@PC
.PLT2: jmp *func@GOT(%ebx)
pushl $offset
jmp .PLT0@PC


3:查看标号.PLT2的语句,实际上是跳转到符号func在GOT表中对应的条目。

4:在符号没有重定位前,GOT表中此符号对应的地址为标号.PLT2的下一条语句,即是pushl
$offset,其中$offset是符号func的重定位偏移量。注意到这是一个二次跳转。

5:在符号func的重定位偏移量压栈后,控制跳到PLT表的第一条目,把GOT[1]的内容压栈,并跳转到GOT[2]对应的地址。

6:GOT[2]对应的实际上是动态符号解析函数的代码,在对符号func的地址解析后,会把func在内存中的地址设置到GOT表中此符号对应的条目中。

7:当第二次调用此符号时,GOT表中对应的条目已经包含了此符号的地址,就可直接调用而不需要利用PLT表进行跳转。

动态连接是比较复杂的,但为了获得灵活性的代价通常就是复杂性。其最终目的是把GOT表中条目的值修改为符号的真实地址,这也可解释节.got包含在可读可写段中。

动态连接是一个非常重要的进步,这意味着库文件可以被升级、移动到其他目录等等而不需要重新编译程序(当然,这不意味库可以任意修改,如函数入参的个数、数据类型应保持兼容性)。从很大程度上说,动态连接机制是ELF格式代替a.out格式的决定性原因。如果说面对对象的编程本质是面对接口(interface)的编程,那么动态连接机制则是这种思想的地一个非常典型的应用,具体的讲,动态连接机制与设计模式中的桥接(BRIDGE)方法比较类似,而它的LAZY特性则与代理(PROXY)方法非常相似。动态连接操作的细节描述请参阅参考资料
8,9,10,11。通过阅读命令readelf、objdump 的源代码以及参考资料
14中所提及的相关软件源代码,可以对ELF文件的格式有更彻底的了解。

总结
不同时期的可执行文件格式深刻的反映了技术进步的过程,技术进步通常是针对解决存在的问题和适应新的环境。早期的UNIX系统使用a.out格式,随着操作系统和硬件系统的进步,a.out格式的局限性越来越明显。新的可执行文件格式COFF在UNIX
System VR3中出现,COFF格式相对a.out格式最大变化是多了一个节头表(section
head
table),能够在包含基础的文本段、数据段、BSS段之外包含更多的段,但是COFF对动态连接和C++程序的支持仍然比较困难。为了解决上述问题,
UNIX系统实验室(UNIX SYSTEM Laboratories USL)
开发出ELF文件格式,它被作为应用程序二进制接口(Application binary
Interface
ABI)的一部分,其目的是替代传统的a.out格式。例如,ELF文件格式中引入初始化段.init和结束段.fini(分别对应构造函数和析构函数)则主要是为了支持C++程序。1994年6月ELF格式出现在LINUX系统上,现在ELF格式作为UNIX/LINUX最主要的可执行文件格式。当然我们完全有理由相信,在将来还会有新的可执行文件格式出现。

上述三种可执行文件格式都很好的体现了设计思想中分层的概念,由一个总的头部刻画了文件的基本要素,再由若干子头部/条目刻画了文件的若干细节。比较一下可执行文件格式和以太数据包中以太头、IP头、TCP头的设计,我想我们能很好的感受分层这一重要的设计思想。参考资料
21从全局的角度讨论了各种文件的格式,并提出一个比较夸张的结论:Everything
Is Byte!

最后的题外话:大多数资料中对a.out格式的评价较低,常见的词语有黑暗年代(dark
ages)、丑陋(ugly)等等,当然,从现代的观点来看,的确是比较简单,但是如果没有曾经的简单何来今天的精巧?正如我们今天可以评价石器时代的技术是ugly,那么将来的人们也可以嘲讽今天的技术是非常ugly。我想我们也许应该用更平和的心态来对曾经的技术有一个公正的评价。

参考资料

1. 《LINUX VIRUSES - ELF FILE FORMAT》 Marius Van Oers
2. 《A Whirlwind Tutorial on Creating Really Teensy ELF Executables for
Linux 》 breadbox
3. 《The Linux Virus Writing And Detection HOWTO》Alexander Bartolich
4. 《从程序员角度看ELF》Hongjiu Lu alert7(译)
5. 《XCOFF Object File Format》
6. 《Executable and Linkable Format(ELF)》
7. 《elf文件格式--另一文本方式的elf文档》alert7(译)
8. 《如何修改动态库符号表》wangdb
9. 《分析ELF的加载过程》opera
10. 《Before main() 分析》 alert7
11. 《Linkers & Loaders》John R. Levine
12. 《Running a.out executables on modern Red Hat Linux》
13. 《Cheating the ELF》
14. 《ELF Binary Analysis Tools》
15. 《dbxread.c》
16. 《Manual Reference Pages - A.OUT (5)》
17. 《Linux下缓冲区溢出攻击的原理及对策》
18. 《Microsoft Portable Executable and Common Object File Format
Specification》
19. 《COFF的文件结构》redleaves
20. 《Common Object File Format (COFF)》
21. 《Everything Is Byte》 mala

关于作者
施聪,成都人,高级程序员、网络设计师。长期从事基于UNIX/LINUX下的c/c++程序设计和数据库建模工作。可通过javer@163.commemncmp@yahoo.com.cn和他联系。

IP组播与组播协议

在Internet上,多媒体业务诸如:流媒体,视频会议和视频点播等,正在成为信息传送的重要组成部分。点对点传输的单播方式不能适应这一类业务传输特 性--单点发送多点接收,因为服务器必须为每一个接收者提供一个相同内容的IP报文拷贝,同时网络上也重复地传输相同内容的报文,占用了大量资源。如图 1.1所示。虽然IP广播允许一个主机把一个IP报文发送给同一个网络的所有主机,但是由于不是所有的主机都需要这些报文,因而浪费了网络资源。在这种情 况下组播(multicast)应运而生,它的出现解决了一个主机向特定的多个接收者发送消息的方法。1989年,IETF通过RFC1112,定义了 Internet上的组播方式。

                                                                        图1.1
1. IP组播

IP组播是指一个IP报文向一个“主机组”的传送,这个包含零个或多个主机的主机组由一个单独的IP地址标识。主机组地址也称为“组播地址”,或者D类地址。除了目的地址部分,组播报文与普通报文没有区别,网络尽力传送组播报文但是并不保证一定送达。
主 机组的成员可以动态变化,主机有权选择加入或者退出某个主机组。主机可以加入多个主机组,也可以向自己没有加入的主机组发送数据。主机组有两种:永久组和 临时组。永久组的IP地址是周知的,由Internet管理机构分配,是保留地址。临时组的地址则使用除永久组地址外的非保留D类地址。
IP组播 分组在互联网上的转发由支持组播的路由器来处理。主机发出的IP组播分组在本子网内被所有主机组成员接收,同时与该子网直接相连的组播路由器会把组播报文 转发到所有包含该主机组成员的网络上。组播报文传递的范围由报文的生存期值(TTL, Time-to-Live)决定,如果TTL值等于或者小于设置的路由器端口TTL门限值(TTL Threshold),路由器将不再转发该报文。
2. 组播地址

IP组播地址,或称为主机组地址,由D类IP地址标记。D类IP地址的最高四位为“1110”,起范围从 224.0.0.0到239.255.255.255。如前所述,部分D类地址被保留,用作永久组的地址,这段地址从 224.0.0.0-224.0.0.255。比较重要的地址有:
224.0.0.1 - 网段中所有支持组播的主机
224.0.0.2 - 网段中所有支持组播的路由器
224.0.0.4 - 网段中所有的DVMRP路由器
224.0.0.5 - 所有的OSPF路由器
224.0.0.6 - 所有的OSPF指派路由器
224.0.0.9 - 所有RIPv2路由器
224.0.0.13 -所有PIM路由器
临时主机组的组播地址由网络管理员选择,他需要保证这个地址在一定的范围内没有其他的主机组在使用这个组播地址。
第 2层的组播地址(组播MAC地址)可以从IP组播地址中衍生。计算方法是把IP地址的最后23位拷贝到MAC地址的最后23位,然后把这23位前面的那一 位置为0。MAC地址的前24位必须为0x01-00-5E。例如:组播IP地址224.0.1.128,16进制表示为0xE0-00-01-10,最 低的23位为0x00-01-10,计算得出的MAC地址为:0x01-00-5E-00-01-10。
3.Internet组管理协议(IGMP)

IGMP协议由主机成员关系协议发展而来,目前有两个版本:IGMPv1(RFC1112),IGMPv2 (RFC2326)。主机使用IGMP消息通告本地的组播路由器它想接收组播流量的主机组地址。如果主机支持IGMPv2,它还可以通告组播路由器它退出 某主机组。组播路由器通过IGMP协议为其每个端口都维护一张主机组成员表,并定期的探询表中的主机组的成员,以确定该主机组是否存活。

IGMP消息被置于IP报文中传送。IGMPv1的报文如图1.2所示。IGMPv1中定义了两种消息类型:主机成员询 问和主机成员报告。当某主机想要介绍某个组播流量时,它向本地的组播路由器发送"主机成员报告"消息,告知欲接收的组播地址。组播路由器收到"主机成员报 告"消息后把该主机加入指定的主机组,并在设定的周期内向组播地址224.0.0.1(代表所有支持组播的主机) 发送"主机成员询问"消息。主机如果还想继续接收组播流量,必须发送"主机成员报告"消息。

IGMPv2的报文如图1.3所示。与IGMPv1不同的是它将版本字段和消息类型字段融合,把未使用字段作了"最大响应时间"字段。IGMPv2报文的消息类型字段定义了四种消息类型:

                                                                          图1.3
0x11 - 成员询问
0x12 - IGMPv1 成员报告
0x16 - IGMPv2 成员报告
0x17 - 退出主机组

IGMPv2向前兼容IGMPv1协议, IGMPv1的设备可以接收处理IGMPv2的消息报文。 IGMPv2中允许路由器对指定的主机组地址做"成员询问",非该组的主机不必响应。如果某主机想退出,它可以主动向路由器发送"推出主机组"消息,而不 必像IGMPv1中那样只能被动退出。
4. CGMP协议

在交换网络中,2层交换机可能即不了解哪个端口有哪些组播组,也不能在其源MAC地址表中找到组播MAC地址的表项。从 而,交换机只能简单地把组播报文向所有端口转发,组播的优势将大大削弱。因此,Cisco提出CGMP协议,让组播路由器来配置交换机的组播转发表,从而 彻底解决交换网络中的组播问题。
CGMP ( Cisco Group management protocol)全称Cisco组管理协议,采用CGMP的路由器将主机加入或者退出组播组的IGMP消息通知交换机,交换机则根据该消息将该主机所在 端口从组播转发表中加入或者删除。通过CGMP协议的使用,2层交换机可以掌握接收组播的主机的情况,从而提高整个网络的性能和利用率。
5. 分布树(Distribution Tree)

在传送组播分组时,指派路由器需要构造一个连接所有组播组成员的树。根据这个树,路由器得出转发分组的一条唯一路径。这个树就称为分布树。由于成员可以动态的加入和退出,分布树也必须动态更新。
根 据构造方法的不同,分布树分为源分布树(Source Distribution Tree)和共享分布树(Shared Distribution Tree)。源分布树以组播源为根节点构造到所有组播组成员的生成树,通常也称为最短路径树(SPT)。共享分布树,也称为RP树或基于核心的树 (CBT, Core_based Tree)。它的构造方法是以网络中的某一个指定的路由器为根节点,该路由器称为集合点或中心点,由此节点生成包含所有组成员的树。使用共享分布树时,组 播源需要首先把组播分组发送给集合点路由器,再由这个路由器转发给其他的组成员。
6. 组播路由协议

组播路由协议的主要任务就是构造组播的分布树,使组播分组能够传送到相应的组播组成员。根据对网络中的组播成员的分布和使用的不同,组播路由协议分为两类:密集模式路由协议(DM)和稀疏模式路由协议(SM)。
DM 路由协议通常用于组播成员较为集中、数量较多-网络的大部分用户、并且有足够带宽的网路环境,比如公司或园区的局域网。因此,DM路由协议用定期广播组播 报文的方法维护组播分布树。DM协议只使用源分布树(SPT),组播流量被广播到网络中所有的组播路由器。DM路由协议有:
DVMRP:距离向量组播路由协议。这是一种基于距离向量算法的组播路由协议。目前已基本上被PIM和MOSPF所取代。

MOSPF:组播OSPF协议。

PIM-DM:协议无关组播协议-密集模式。它不需要单独的组播协议,利用路由器上单播路由协议的路由表作反向路径转发 检查,由此获得组播分布树。相比另两种协议,PIM-DM的开销要小很多,它用于组播源和目的非常靠近、接收者数量大于发送者数量并且组播流量比较大的环 境中效果很好。在网路中稀疏分布、网络也没有充足带宽的情况,如广域网环境,可以使用SM路由协议。因此,SM路由协议采用选择性的建立和维护分布树的方 式,由空树开始,仅当成员显式的请求加入分布树才做出修改。SM路由协议有:
CBT:基于中心的分布树协议(RFC 2201)。协议由以一个中心的路由器为根构造一个共享分布树,所有的组播流量都经由这个中心路由器转发。

PIM-SM:协议无关组播协议-稀疏模式。工作原理与PIM-DM类似,但专门针对稀疏环境优化。适用于组播组中接收 者较少、间歇性组播流量的情况。不同于PIM-DM的广播方式,PIM-SM定义了一个集合点(RP),所有的接收者在RP注册,组播分组由RP转发给接 收者。

7.总结
单个数据流可以发送到多个客户端的组播能力已成为大多数多媒体应用的传输手段。组播技术利用一个IP地址使IP数据报文 发送到用户组。IP组播采用了特殊定义的目的IP地址和目的MAC地址。IGMP为客户端提供加入和离开组播组的方式。CGMP使路由器为交换机配置组播 转发表,并告诉交换机当前的组播成员。指派路由器根据对网络中的组播成员的分布和使用的不同采用密集模式DM或稀疏模式SM组播路由协议来构造组播的分布 树,而这个分布树将在源子网和组播组之间确定一条唯一路径以提高数据传输效率。

__attribute__详解

GNU
C的一大特色就是__attribute__机制。__attribute__可以设置函数属性(Function
Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。
__attribute__书写特征是:__attribute__前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__参数。
__attribute__语法格式为:
__attribute__ ((attribute-list))
其位置约束为:
放于声明的尾部";"之前。
函数属性(Function Attribute)
函数属性可以帮助开发者把一些特性添加到函数声明中,从而可以使编译器在错误检查方面的功能更强大。__attribute__机制也很容易同非GNU应用程序做到兼容之功效。
GNU CC需要使用
–Wall编译器来击活该功能,这是控制警告信息的一个很好的方式。下面介绍几个常见的属性参数。

__attribute__ format
该__attribute__属性可以给被声明的函数加上类似printf或者scanf的特征,它可以使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。该功能十分有用,尤其是处理一些很难发现的bug。
format的语法格式为:
format (archetype, string-index, first-to-check)
format属性告诉编译器,按照printf, scanf,
strftime或strfmon的参数表格式规则对该函数的参数进行检查。"archetype"指定是哪种风格;"string-index"指定传入函数的第几个参数是格式化字符串;"first-to-check"指定从函数的第几个参数开始按上述规则进行检查。
具体使用格式如下:
__attribute__((format(printf,m,n)))
__attribute__((format(scanf,m,n)))
其中参数m与n的含义为:
m:第几个参数为格式化字符串(format string);
n:参数集合中的第一个,即参数"…"里的第一个参数在函数参数总数排在第几,注意,有时函数参数里还有"隐身"的呢,后面会提到;
在使用上,__attribute__((format(printf,m,n)))是常用的,而另一种却很少见到。下面举例说明,其中myprint为自己定义的一个带有可变参数的函数,其功能类似于printf:
//m=1;n=2
extern void myprint(const char *format,...)
__attribute__((format(printf,1,2)));
//m=2;n=3
extern void myprint(int l,const char *format,...)
__attribute__((format(printf,2,3)));
需要特别注意的是,如果myprint是一个函数的成员函数,那么m和n的值可有点"悬乎"了,例如:
//m=3;n=4
extern void myprint(int l,const char *format,...)
__attribute__((format(printf,3,4)));
其原因是,类成员函数的第一个参数实际上一个"隐身"的"this"指针。(有点C++基础的都知道点this指针,不知道你在这里还知道吗?)
这里给出测试用例:attribute.c,代码如下:
1:
2:extern void myprint(const char *format,...)
__attribute__((format(printf,1,2)));
3:
4:void test()
5:{
6: myprint("i=%d\n",6);
7: myprint("i=%s\n",6);
8: myprint("i=%s\n","abc");
9: myprint("%s,%d,%d\n",1,2);
10:}

运行$gcc –Wall –c attribute.c attribute后,输出结果为:

attribute.c: In function `test':
attribute.c:7: warning: format argument is not a pointer (arg 2)
attribute.c:9: warning: format argument is not a pointer (arg 2)
attribute.c:9: warning: too few arguments for format

如果在attribute.c中的函数声明去掉__attribute__((format(printf,1,2))),再重新编译,既运行$gcc
–Wall –c attribute.c attribute后,则并不会输出任何警告信息。
注意,默认情况下,编译器是能识别类似printf的"标准"库函数。

__attribute__ noreturn
该属性通知编译器函数从不返回值,当遇到类似函数需要返回值而却不可能运行到返回值处就已经退出来的情况,该属性可以避免出现错误信息。C库函数中的abort()和exit()的声明格式就采用了这种格式,如下所示:

extern void exit(int) __attribute__((noreturn));extern void
abort(void) __attribute__((noreturn));
为了方便理解,大家可以参考如下的例子:

//name: noreturn.c ;测试__attribute__((noreturn))
extern void myexit();

int test(int n)
{
if ( n > 0 )
{
myexit();
/* 程序不可能到达这里*/
}
else
return 0;
}

编译显示的输出信息为:

$gcc –Wall –c noreturn.c
noreturn.c: In function `test':
noreturn.c:12: warning: control reaches end of non-void function

警告信息也很好理解,因为你定义了一个有返回值的函数test却有可能没有返回值,程序当然不知道怎么办了!
加上__attribute__((noreturn))则可以很好的处理类似这种问题。把
extern void myexit();修改为:
extern void myexit()
__attribute__((noreturn));之后,编译不会再出现警告信息。

__attribute__ const
该属性只能用于带有数值类型参数的函数上。当重复调用带有数值参数的函数时,由于返回值是相同的,所以此时编译器可以进行优化处理,除第一次需要运算外,其它只需要返回第一次的结果就可以了,进而可以提高效率。该属性主要适用于没有静态状态(static
state)和副作用的一些函数,并且返回值仅仅依赖输入的参数。
为了说明问题,下面举个非常"糟糕"的例子,该例子将重复调用一个带有相同参数值的函数,具体如下:

extern int square(int n) __attribute__ ((const));...
for (i = 0; i < 100; i++ ) { total += square (5) +
i; }
通过添加__attribute__((const))声明,编译器只调用了函数一次,以后只是直接得到了相同的一个返回值。
事实上,const参数不能用在带有指针类型参数的函数中,因为该属性不但影响函数的参数值,同样也影响到了参数指向的数据,它可能会对代码本身产生严重甚至是不可恢复的严重后果。
并且,带有该属性的函数不能有任何副作用或者是静态的状态,所以,类似getchar()或time()的函数是不适合使用该属性的。
-finstrument-functions
该参数可以使程序在编译时,在函数的入口和出口处生成instrumentation调用。恰好在函数入口之后并恰好在函数出口之前,将使用当前函数的地址和调用地址来调用下面的

profiling
函数。(在一些平台上,__builtin_return_address不能在超过当前函数范围之外正常工作,所以调用地址信息可能对profiling函数是无效的。)

void __cyg_profile_func_enter(void *this_fn, void *call_site);
void __cyg_profile_func_exit(void *this_fn, void *call_site);

其中,第一个参数this_fn是当前函数的起始地址,可在符号表中找到;第二个参数call_site是指调用处地址。

instrumentation
也可用于在其它函数中展开的内联函数。从概念上来说,profiling调用将指出在哪里进入和退出内联函数。这就意味着这种函数必须具有可寻址形式。如果函数包含内联,而所有使用到该函数的程序都要把该内联展开,这会额外地增加代码长度。如果要在C
代码中使用extern inline声明,必须提供这种函数的可寻址形式。
可对函数指定no_instrument_function属性,在这种情况下不会进行
Instrumentation操作。例如,可以在以下情况下使用no_instrument_function属性:上面列出的profiling函数、高优先级的中断例程以及任何不能保证profiling正常调用的函数。
no_instrument_function
如果使用了-finstrument-functions
,将在绝大多数用户编译的函数的入口和出口点调用profiling函数。使用该属性,将不进行instrument操作。

constructor/destructor
若函数被设定为constructor属性,则该函数会在main()函数执行之前被自动的执行。类似的,若函数被设定为destructor属性,则该函数会在main()函数执行之后或者exit()被调用后被自动的执行。拥有此类属性的函数经常隐式的用在程序的初始化数据方面。
这两个属性还没有在面向对象C中实现。
同时使用多个属性
可以在同一个函数声明里使用多个__attribute__,并且实际应用中这种情况是十分常见的。使用方式上,你可以选择两个单独的__attribute__,或者把它们写在一起,可以参考下面的例子:

/* 把类似printf的消息传递给stderr 并退出 */extern void die(const char
*format, ...) __attribute__((noreturn))
__attribute__((format(printf, 1, 2))); 或者写成 extern void die(const char
*format, ...) __attribute__((noreturn, format(printf, 1,
2)));
如果带有该属性的自定义函数追加到库的头文件里,那么所以调用该函数的程序都要做相应的检查。

和非GNU编译器的兼容性
庆幸的是,__attribute__设计的非常巧妙,很容易作到和其它编译器保持兼容,也就是说,如果工作在其它的非GNU编译器上,可以很容易的忽略该属性。即使__attribute__使用了多个参数,也可以很容易的使用一对圆括弧进行处理,例如:

/* 如果使用的是非GNU C, 那么就忽略__attribute__ */#ifndef __GNUC__#
define __attribute__(x) /*NOTHING*/

linux时间函数

UNIX及Linux的时间系统是由「新纪元时间」Epoch开始计算起,单位为秒,Epoch则是指定为1970年一月一日凌晨零点零分零秒,格林威治时间。
目前大部份的UNIX系统都是用32位元来记录时间,正值表示为1970以後,负值则表示1970年以前。我们可以很简单地计算出其时间领域:
2^31/86400(s) = 24855.13481(天) ~ 68.0958(年)
1970+68.0958 = 2038.0958
1970-68.0958 = 1901.9042
时间领域为[1901.9042,2038.0958]。
准确的时间为2038年一月十八日星期一晚上十点十四分七秒。那一刻,时间将会转为负数,变成1901年十二月十三日黑色星期五下午三点四十五分五十二秒,然後Jason就会跑出来用斧头砸掉您的电脑。
这就是所谓的UNIX 2038 BUG,或者您也可戏称为Jason hatchet
bug。在大部份的UNIX上,并没有所谓Y2K问题,不过都有2038年问题。
在一些64位元的平台上,例如Digital
Alpha、SGI、Sparc等等,则用64位元来表示时间。
2^63/86400 ~ 1E14(天) ~ 2.92E11(年)
大约是292亿年。
因此,使用64位元的电脑可能会有Armageddon
bug的问题。届时位於猎户座旋臂的太阳,已经是黑矮星或暗黑物质,猎户座旋臂大概也已经被重力波震断,银河系大概则已经变成小型似星体了。
虽然许多人认为UNIX的2038年问题会随着科技的进步,而将电脑逐步汰换成64位元电脑,因此无须担心。但我个人相信,在2038年,依然会
有许多状况出现。因为,就事实而言,目前许多UNIX系统都有足够的能力服役到2038年而毫无问题。因此,如果有意添购电脑主机,而且有预期会使用到那
个时候,最好是选购64位元电脑,确认只有世界末日问题(除非您想要把资料流传给下一个宇宙,那就要另当别论了)。
--------------------------------------------------------------------------------
取得目前时间
在所有的UNIX下,都有个time()的函数
#include
time_t time(time_t *t);
这个函数会传回从epoch开始计算起的秒数,如果t是non-null,它将会把时间值填入t中。
对某些需要较高精准度的需求,Linux提供了gettimeofday()。
#include
#include
int gettimeofday(struct timeval * tv,struct timezone *tz);
int settimeofday(const struct timeval * tv,const struct timezone *tz);
struct timeval {
int tv_sec;
int tv_usec;
};
其中tv_sec是由凌晨开始算起的秒数,tv_usec则是微秒(10E-6 second)。
struct timezone {
int tv_minuteswest;
int tv_dsttime;
};
tv_minuteswest是格林威治时间往西方的时差,tv_dsttime则是时间的修正方式。
在Linux下timezone的使用已经废除而不再使用。因为有许多地区都有日光节约时间,日光节约时间的使用与否,往往与无可预测的政治因素相关,没有简单的方法来实作这项设计。
在sys/time.h中,有三个有用的巨集用於操作timeval:
#define timerisset(tvp) ((tvp)->tv_sec || (tvp)->tv_usec)
#define timercmp(tvp, uvp, cmp)
((tvp)->tv_sec cmp (uvp)->tv_sec ||\
(tvp)->tv_sec == (uvp)->tv_sec &&\
(tvp)->tv_usec cmp (uvp)->tv_usec)
#define timerclear(tvp) ((tvp)->tv_sec = (tvp)->tv_usec = 0)
timerisset检查tvp是否有被设定值进去,timercmp比较时间,timerclear设tvp为零。
cmp为比较操作子如">"、"<"、"=="等等。
在POSIX.1b的即时处理标准中允许较高的时间解析度。
struct timespec
{
long int tv_sec;
long int tv_nsec;
};
tv_nsec是nano second(10E-9 second)。
--------------------------------------------------------------------------------
时间表述
电脑使用秒及epoch来表示其时间,但对人脑来说实在太残忍一点,大概没有人可以用人脑来计算。因此,UNIX下提供了其它两种基本方式来表述时间,struct
tm及文字格式时间。
struct tm
{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
};
tm_sec表「秒」数,在[0,61]之间,多出来的两秒是用来处理跳秒问题用的。
tm_min表「分」数,在[0,59]之间。
tm_hour表「时」数,在[0,23]之间。
tm_mday表「本月第几日」,在[1,31]之间。
tm_mon表「本年第几月」,在[0,11]之间。
tm_year要加1900表示那一年。
tm_wday表「本第几日」,在[0,6]之间。
tm_yday表「本年第几日」,在[0,365]之间,闰年有366日。
tm_isdst表是否为「日光节约时间」。
struct tm格式时间函数
struct tm * gmtime(const time_t * t);
转换成格林威治时间。有时称为GMT或UTC。
struct tm * localtime(const time_t *t);
转换成本地时间。它可以透过修改TZ环境变数来在一台机器中,不同使用者表示不同时间。
time_t mktime(struct tm *tp);
转换tm成为time_t格式,使用本地时间。
tme_t timegm(strut tm *tp);
转换tm成为time_t格式,使用UTC时间。
double difftime(time_t t2,time_t t1);
计算秒差。
文字时间格式函数
char * asctime(struct tm *tp);
char * ctime(struct tm *tp);
这两个函数都转换时间格式为标准UNIX时间格式。
Mon May 3 08:23:35 1999
ctime一率使用当地时间,asctime则用tm结构内的timezone资讯来表示。
size_t strftime(char *str,size_t max,char *fmt,struct tm *tp);
strftime有点像sprintf,其格式由fmt来指定。
%a : 本第几天名称,缩写。
%A : 本第几天名称,全称。
%b : 月份名称,缩写。
%B : 月份名称,全称。
%c : 与ctime/asctime格式相同。
%d : 本月第几日名称,由零算起。
%H : 当天第几个小时,24小时制,由零算起。
%I : 当天第几个小时,12小时制,由零算起。
%j : 当年第几天,由零算起。
%m : 当年第几月,由零算起。
%M : 该小时的第几分,由零算起。
%p : AM或PM。
%S : 该分钟的第几秒,由零算起。
%U : 当年第几,由第一个日开始计算。
%W : 当年第几,由第一个一开始计算。
%w : 当第几日,由零算起。
%x : 当地日期。
%X : 当地时间。
%y : 两位数的年份。
%Y : 四位数的年份。
%Z : 时区名称的缩写。
%% : %符号。
char * strptime(char *s,char *fmt,struct tm *tp);
如同scanf一样,解译字串成为tm格式。
%h : 与%b及%B同。
%c : 读取%x及%X格式。
%C : 读取%C格式。
%e : 与%d同。
%D : 读取%m/%d/%y格式。
%k : 与%H同。
%l : 与%I同。
%r : 读取"%I:%M:%S %p"格式。
%R : 读取"%H:%M"格式。
%T : 读取"%H:%M:%S"格式。
%y : 读取两位数年份。
%Y : 读取四位数年份。
--------------------------------------------------------------------------------
进入「冬眠状态」:Sleeping
unsigned int sleep(unsigned int seconds);
sleep()会使目前程式陷入「冬眠」seconds秒,除非收到「不可抵」的信号。
如果sleep()没睡饱,它将会返回还需要补眠的时间,否则一般返回零。
void usleep(unsigned long usec);
usleep与sleep()类同,不同之处在於秒的单位为10E-6秒。
int select(0,NULL,NULL,NULL,struct timeval *tv);
可以利用select的实作sleep()的功能,它将不会等待任何事件发生。
int nanosleep(struct timespec *req,struct timespec *rem);
nanosleep会沉睡req所指定的时间,若rem为non-null,而且没睡饱,将会把要补眠的时间放在rem上。
--------------------------------------------------------------------------------
定时闹钟:Interval Timers
定时闹钟一但启动後,会定期送信号给行程,读者最好要解一下signal的处理。
struct itimerval {
struct timeval * it_interval;
struct timeval * it_value;
};
unsigned int alarm(unsigned int seconds);
alarm()会在seconds时,送出SIGALRM信号,这不是「定期」的。
int getitimer(int which,struct itimerval *val);
读取which指定的Timer目前状态。
int setitimer(int which,struct itimerval *val,struct itimerval *old);
设定which指定的Timer目前状态。
每个行程都有三个定期闹钟(which参数):
ITIMER_REAL :
以系统真实的时间来计算,它送出SIGALRM信号。
ITIMER_VIRTUAL :
以该行程真正有执行的时间来计算,它送出SIGVTALRM信号。
ITIMER_PROF :
以行程真正有执行及在核心中所费的时间来计算,它送出SIGPROF信号。

Linux的时间表示和计算函数 
1.时间的表示 
2.时间的测量 
3.计时器的使用 
-------------------------------------------------------------------------------- 
1。时间表示 在程序当中,我们经常要输出系统当前的时间,比如我们使用date命令的输出结果.这个时候我们可以使用下面两个函数 
#include 
time_t time(time_t *tloc); 
char *ctime(const time_t *clock); 
time函数返回从1970年1月1日0点以来的秒数.存储在time_t结构之中.不过这个函数的返回值对于我们来说没有什么实际意义.这个时 候我们使用第二个函数将秒数转化为字符串. 这个函数的返回类型是固定的:一个可能值为. Thu Dec 7 14:58:59 2000 这个字符串 的长度是固定的为26 
2。时间的测量 有时候我们要计算程序执行的时间.比如我们要对算法进行时间分析.这个时候可以使用下面这个函数. 
#include 
int gettimeofday(struct timeval *tv,struct timezone *tz); 
strut timeval { 
long tv_sec; /* 秒数 */ 
long tv_usec; /* 微秒数 */ 
}; 
gettimeofday将时间保存在结构tv之中.tz一般我们使用NULL来代替. 
#include < 
#include < 
#include < 
void function() 

unsigned int i,j; 
double y; 
for(i=0;i<1000;i++) 
for(j=0;j<1000;j++) 
  y=sin((double)i); 

main() 

struct timeval tpstart,tpend; 
float timeuse; 
gettimeofday(&tpstart,NULL); 
function(); 
gettimeofday(&tpend,NULL); 
timeuse=1000000*(tpend.tv_sec-tpstart.tv_sec)+ 
tpend.tv_usec-tpstart.tv_usec; 
timeuse/=1000000; 
printf("Used Time:%f\n",timeuse); 
exit(0); 

这个程序输出函数的执行时间,我们可以使用这个来进行系统性能的测试,或者是函数算法的效率分析.在我机器上的一个输出结果是: Used Time:0.556070 
3。计时器的使用 Linux操作系统为每一个进程提供了3个内部间隔计时器. 
ITIMER_REAL:减少实际时间.到时的时候发出SIGALRM信号. 
ITIMER_VIRTUAL:减少有效时间(进程执行的时间).产生SIGVTALRM信号. 
ITIMER_PROF:减少进程的有效时间和系统时间(为进程调度用的时间).这个经常和上面一个使用用来计算系统内核时间和用户时间.产生SIGPROF信号. 
具体的操作函数是: 
#include 
int getitimer(int which,struct itimerval *value); 
int setitimer(int which,struct itimerval *newval, 
struct itimerval *oldval); 
struct itimerval { 
struct timeval it_interval; 
struct timeval it_value; 

getitimer函数得到间隔计时器的时间值.保存在value中 setitimer函数设置间隔计时器的时间值为newval.并将旧值保 存在oldval中. which表示使用三个计时器中的哪一个. itimerval结构中的it_value是减少的时间,当这个值为0的时候就发出 相应的信号了. 然后设置为it_interval值. 
#include 
#include 
#include 
#include 
#include 
#define PROMPT "时间已经过去了两秒钟\n\a" 
char *prompt=PROMPT; 
unsigned int len; 
void prompt_info(int signo) 

  write(STDERR_FILENO,prompt,len); 

void init_sigaction(void) 

  struct sigaction act; 
  act.sa_handler=prompt_info; 
  act.sa_flags=0; 
  sigemptyset(&act.sa_mask); 
  sigaction(SIGPROF,&act,NULL); 

void init_time() 

  struct itimerval value; 
  value.it_value.tv_sec=2; 
  value.it_value.tv_usec=0; 
  value.it_interval=value.it_value; 
  setitimer(ITIMER_PROF,&value,NULL); 

int main() 

len=strlen(prompt); 
init_sigaction(); 
init_time(); 
while(1); 
exit(0); 

这个程序每执行两秒中之后会输出一个提示.

Grep学习笔记

Copyright © 2009 本文遵从GPL协议,欢迎转载、修改、散布。
Table of Contents
1. grep简介
2. grep正则表达式元字符集(基本集)
3. 用于egrep和 grep -E的元字符扩展集
4. POSIX字符类
5. Grep命令选项
6. 实例
1. grep简介
grep (global search regular expression(RE) and print out the
line,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。Unix的grep家族包
括grep、egrep和fgrep。egrep和fgrep的命令只跟grep有很小不同。egrep是grep的扩展,支持更多的re元字符,
fgrep就是fixed grep或fast
grep,它们把所有的字母都看作单词,也就是说,正则表达式中的元字符表示回其自身的字面意义,不再特殊。linux使用GNU版本的grep。它功能
更强,可以通过-G、-E、-F命令行选项来使用egrep和fgrep的功能。
grep的工作方式是这样的,它在一个或多个文件中搜索字符串模板。如果模板包括空格,则必须被引用,模板后的所有字符串被看作文件名。搜索的结果被送到屏幕,不影响原文件内容。
grep可用于shell脚本,因为grep通过返回一个状态值来说明搜索的状态,如果模板搜索成功,则返回0,如果搜索不成功,则返回1,如果搜索的文件不存在,则返回2。我们利用这些返回值就可进行一些自动化的文本处理工作。
2. grep正则表达式元字符集(基本集)
^
锚定行的开始 如:'^grep'匹配所有以grep开头的行。
$
锚定行的结束 如:'grep$'匹配所有以grep结尾的行。
.
匹配一个非换行符的字符 如:'gr.p'匹配gr后接一个任意字符,然后是p。
*
匹配零个或多个先前字符 如:'*grep'匹配所有一个或多个空格后紧跟grep的行。
.*一起用代表任意字符。
[]
匹配一个指定范围内的字符,如'[Gg]rep'匹配Grep和grep。
[^]
匹配一个不在指定范围内的字符,如:'[^A-FH-Z]rep'匹配不包含A-R和T-Z的一个字母开头,紧跟rep的行。
\(..\)
标记匹配字符,如'\(love\)',love被标记为1。
\<
锚定单词的开始,如:'\<grep'匹配包含以grep开头的单词的行。
\>
锚定单词的结束,如'grep\>'匹配包含以grep结尾的单词的行。
x\{m\}
重复字符x,m次,如:'0\{5\}'匹配包含5个o的行。
x\{m,\}
重复字符x,至少m次,如:'o\{5,\}'匹配至少有5个o的行。
x\{m,n\}
重复字符x,至少m次,不多于n次,如:'o\{5,10\}'匹配5--10个o的行。
\w
匹配文字和数字字符,也就是[A-Za-z0-9],如:'G\w*p'匹配以G后跟零个或多个文字或数字字符,然后是p。
\W
\w的反置形式,匹配一个或多个非单词字符,如点号句号等。
\b
单词锁定符,如: '\bgrep\b'只匹配grep。
3. 用于egrep和 grep -E的元字符扩展集
+
匹配一个或多个先前的字符。如:'[a-z]+able',匹配一个或多个小写字母后跟able的串,如loveable,enable,disable等。
?
匹配零个或多个先前的字符。如:'gr?p'匹配gr后跟一个或没有字符,然后是p的行。
a|b|c
匹配a或b或c。如:grep|sed匹配grep或sed
()
分组符号,如:love(able|rs)ov+匹配loveable或lovers,匹配一个或多个ov。
x{m},x{m,},x{m,n}
作用同x\{m\},x\{m,\},x\{m,n\}
4. POSIX字符类
为 了在不同国家的字符编码中保持一至,POSIX(The Portable Operating System
Interface)增加了特殊的字符类,如[:alnum:]是A-Za-z0-9的另一个写法。要把它们放到[]号内才能成为正则表达式,如[A-
Za-z0-9]或[[:alnum:]]。在linux下的grep除fgrep外,都支持POSIX的字符类。
[:alnum:]
文字数字字符
[:alpha:]
文字字符
[:digit:]
数字字符
[:graph:]
非空字符(非空格、控制字符)
[:lower:]
小写字符
[:cntrl:]
控制字符
[:print:]
非空字符(包括空格)
[:punct:]
标点符号
[:space:]
所有空白字符(新行,空格,制表符)
[:upper:]
大写字符
[:xdigit:]
十六进制数字(0-9,a-f,A-F)
5. Grep命令选项
-?
同时显示匹配行上下的?行,如:grep -2 pattern
filename同时显示匹配行的上下2行。
-b,--byte-offset
打印匹配行前面打印该行所在的块号码。
-c,--count
只打印匹配的行数,不显示匹配的内容。
-f File,--file=File
从文件中提取模板。空文件中包含0个模板,所以什么都不匹配。
-h,--no-filename
当搜索多个文件时,不显示匹配文件名前缀。
-i,--ignore-case
忽略大小写差别。
-q,--quiet
取消显示,只返回退出状态。0则表示找到了匹配的行。
-l,--files-with-matches
打印匹配模板的文件清单。
-L,--files-without-match
打印不匹配模板的文件清单。
-n,--line-number
在匹配的行前面打印行号。
-s,--silent
不显示关于不存在或者无法读取文件的错误信息。
-v,--revert-match
反检索,只显示不匹配的行。
-w,--word-regexp
如果被\<和\>引用,就把表达式做为一个单词搜索。
-V,--version
显示软件版本信息。
6. 实例
要用好grep这个工具,其实就是要写好正则表达式,所以这里不对grep的所有功能进行实例讲解,只列几个例子,讲解一个正则表达式的写法。
$ ls -l | grep '^a'
通过管道过滤ls -l输出的内容,只显示以a开头的行。
$ grep 'test' d*
显示所有以d开头的文件中包含test的行。
$ grep 'test' aa bb cc
显示在aa,bb,cc文件中匹配test的行。
$ grep '[a-z]\{5\}' aa
显示所有包含每个字符串至少有5个连续小写字符的字符串的行。
$ grep 'w\(es\)t.*\1' aa
如果west被匹配,则es就被存储到内存中,并标记为1,然后搜索任意个字符(.*),这些字符后面紧跟着另外一个es(\1),找到就显示该行。如果用egrep或grep
-E,就不用"\"号进行转义,直接写成'w(es)t.*\1'就可以了。
http://man.chinaunix.net/newsoft/grep/open.htm
-------------------------------------------------------grep命令介绍
本文出自:OHaHa的学习心得[ohaha.ks.edu.tw] 作者:蓝色泡泡(panda@ks.edu.tw)
(2002-03-15 08:02:00)
◎grep -- print lines matching a pattern (将符合样式的该行列出)
◎语法: grep [options]
PATTERN [FILE...]
grep用以在file内文中比对相对应的部分,或是当没有指定档案时,
由标准输入中去比对。 在预设的情况下,grep会将符合样式的那一行列出。
此外,还有两个程式是grep的变化型,egrep及fgrep。
其中egrep就等同於grep -E ,fgrep等同於grep -F 。
◎参数
1. -A NUM,--after-context=NUM
除了列出符合行之外,并且列出後NUM行。

ex: $ grep -A 1 panda file
(从file中搜寻有panda样式的行,并显示该行的後1行)

2. -a或--text
grep原本是搜寻文字档,若拿二进位的档案作为搜寻的目标,
则会显示如下的讯息: Binary file 二进位档名 matches
然後结束。

若加上-a参数则可将二进位档案视为文字档案搜寻,
相当於--binary-files=text这个参数。

ex: (从二进位档案mv中去搜寻panda样式)
(错误!!!)
$ grep panda mv
Binary file mv matches
(这表示此档案有match之处,详见--binary-files=TYPE )
$
(正确!!!)
$ grep -a panda mv

3. -B NUM,--before-context=NUM
与 -A NUM 相对,但这此参数是显示除符合行之外
并显示在它之前的NUM行。

ex: (从file中搜寻有panda样式的行,并显示该行的前1行)
$ grep -B 1 panda file
4. -C [NUM], -NUM, --context[=NUM]
列出符合行之外并列出上下各NUM行,预设值是2。

ex: (列出file中除包含panda样式的行外并列出其上下2行)
(若要改变预设值,直接改变NUM即可)
$ grep -C[NUM] panda file

5. -b, --byte-offset
列出样式之前的内文总共有多少byte ..

ex: $ grep -b panda file
显示结果类似於:
0:panda
66:pandahuang
123:panda03

6. --binary-files=TYPE
此参数TYPE预设为binary(二进位),若以普通方式搜寻,只有2种结果:
1.若有符合的地方:显示Binary file 二进位档名 matches
2.若没有符合的地方:什麽都没有显示。

若TYPE为without-match,遇到此参数,
grep会认为此二进位档案没有包含任何搜寻样式,与-I 参数相同。

若TPYE为text, grep会将此二进位档视为text档案,与-a
参数相同。

Warning: --binary-files=text
若输出为终端机,可能会产生一些不必要的输出。

7. -c, --count
不显示符合样式行,只显示符合的总行数。
若再加上-v,--invert-match,参数显示不符合的总行数。
8. -d ACTION, --directories=ACTION
若输入的档案是一个资料夹,使用ACTION去处理这个资料夹。
预设ACTION是read(读取),也就是说此资料夹会被视为一般的档案;
若ACTION是skip(略过),资料夹会被grep略过:
若ACTION是recurse(递),grep会去读取资料夹下所有的档案,
此相当於-r 参数。
9. -E, --extended-regexp
采用规则表示式去解释样式。

10. -e PATTERN, --regexp=PATTERN
把样式做为一个partern,通常用在避免partern用-开始。
11. -f FILE, --file=FILE
事先将要搜寻的样式写入到一个档案,一行一个样式。
然後采用档案搜寻。
空的档案表示没有要搜寻的样式,因此也就不会有任何符合。

ex: (newfile为搜寻样式档)
$grep -f newfile file
12. -G, --basic-regexp
将样式视为基本的规则表示式解释。(此为预设)
13. -H, --with-filename
在每个符合样式行前加上符合的档案名称,若有路径会显示路径。

ex: (在file与testfile中搜寻panda样式)
$grep -H panda file ./testfile
file:panda
./testfile:panda
$

14. -h, --no-filename
与-H参数相类似,但在输出时不显示路径。
15. --help
产生简短的help讯息。
16. -I
grep会强制认为此二进位档案没有包含任何搜寻样式,
与--binary-files=without-match参数相同。

ex: $ grep -I panda mv
17. -i, --ignore-case
忽略大小写,包含要搜寻的样式及被搜寻的档案。

ex: $ grep -i panda mv

18. -L, --files-without-match
不显示平常一般的输出结果,反而显示出没有符合的档案名称。
19. -l, --files-with-matches
不显示平常一般的输出结果,只显示符合的档案名称。
20. --mmap
如果可能,使用mmap系统呼叫去读取输入,而不是预设的read系统呼叫。
在某些状况,--mmap 能产生较好的效能。 然而,--mmap
如果运作中档案缩短,或I/O 错误发生时,
可能造成未定义的行为(包含core dump),。

21. -n, --line-number
在显示行前,标上行号。

ex: $ grep -n panda file
显示结果相似於下:
行号:符合行的内容
22. -q, --quiet, --silent
不显示任何的一般输出。请参阅-s或--no-messages
23. -r, --recursive
递地,读取每个资料夹下的所有档案,此相当於 -d recsuse 参数。
24. -s, --no-messages
不显示关於不存在或无法读取的错误讯息。

小: 不像GNU grep,传统的grep不符合POSIX.2协定,
因为缺乏-q参数,且他的-s 参数表现像GNU grep的 -q 参数。
Shell Script倾向将传统的grep移植,避开-q及-s参数,
且将输出限制到/dev/null。

POSIX: 定义UNIX及UNIX-like系统需要提供的功能。

25. -V, --version
显示出grep的版本号到标准错误。
当您在回报有关grep的bugs时,grep版本号是必须要包含在内的。
26. -v, --invert-match
显示除搜寻样式行之外的全部。

27. -w, --word-regexp
将搜寻样式视为一个字去搜寻,完全符合该"字"的行才会被列出。
28. -x, --line-regexp
将搜寻样式视为一行去搜寻,完全符合该"行"的行才会被列出。

ubuntu8.10安装vim7.2及使用说明

vim7.2出来好长时间了,ubuntu源里面却一直没有更新。常用软件是我是常新的,今天去vim.org下载了源码编译安装,可惜打开中文的时候,有的中文显示不正常,换了encoding也不解决。换了sourelist下载deb包安装中文显示也不正常,网上搜的方法不奏效。最后,发现了一个deb包最新的源,最近更新是2009年3月16日编译的,很满足我求新的需求。地址:http://trinitum.org/files/vim-7.2.130-ubuntu-8.10/
下载下面三个包依次安装:
vim-runtime_7.2.130-1_all.deb
vim-common_7.2.130-1_i386.deb
vim_7.2.130-1_i386.deb
使用原来的配置文件,正确显示中文。

安装中文帮助:
下载最新的中文帮助文件http://sourceforge.net/project/showfiles.php?group_id=56777
届押后,安装之: ./vimcdoc.sh -i

vim字符编码:
Vim 可以很好的编辑各种字符编码的文件,包括 UCS-2、UTF-8 等流行的 Unicode
编码方式:
Vim
有四个跟字符编码方式有关的选项,encoding、fileencoding、fileencodings、termencoding,它们的意义如下:
* encoding: Vim 内部使用的字符编码方式,包括 Vim 的 buffer
(缓冲区)、菜单文本、消息文本等;
* fileencoding: Vim 中当前编辑的文件的字符编码方式,Vim
保存文件时也会将文件保存为这种字符编码方式 (不管是否新文件都如此);
* fileencodings: Vim
启动时会按照它所列出的字符编码方式逐一探测即将打开的文件的字符编码方式,并且将
fileencoding 设置为最终探测到的字符编码方式。因此最好将 Unicode
编码方式放到这个列表的最前面,将拉丁语系编码方式 latin1 放到最后面;
* termencoding: Vim 所工作的终端 (或者 Windows 的 Console 窗口)
的字符编码方式。

Vim 的多字符编码的工作方式:
1. Vim 启动,根据 .vimrc 中设置的 encoding 的值来设置
buffer、菜单文本、消息文的字符编码方式;
2. 读取需要编辑的文件,根据 fileencodings
中列出的字符编码方式逐一探测该文件编码方式。并设置 fileencoding
为探测到的,看起来是正确的 (注1) 字符编码方式;
3. 对比 fileencoding 和 encoding 的值,若不同则调用 iconv
将文件内容转换为 encoding
所描述的字符编码方式,并且把转换后的内容放到为此文件开辟的 buffer
里,此时我们就可以开始编辑这个文件了。注意,完成这一步动作需要调用外部的
iconv.dll (注2),你需要保证这个文件存在于 $VIMRUNTIME 或者其他列在 PATH
环境变量中的目录里;
4. 编辑完成后保存文件时,再次对比 fileencoding 和 encoding
的值。若不同,再次调用 iconv 将即将保存的 buffer 中的文本转换为
fileencoding 所描述的字符编码方式,并保存到指定的文件中。同样,这需要调用
iconv.dll。

由于 Unicode 能够包含几乎所有的语言的字符,而且 Unicode 的 UTF-8
编码方式又是非常具有性价比的编码方式 (空间消耗比 UCS-2 小),因此建议
encoding 的值设置为 utf-8。这么做的另一个理由是 encoding 设置为 utf-8
时,Vim 自动探测文件的编码方式会更准确 (或许这个理由才是主要的
;)。我们在中文 Windows
里编辑的文件,为了兼顾与其他软件的兼容性,文件编码还是设置为 GB2312/GBK
比较合适,因此 fileencoding 建议设置为 chinese (chinese 是个别名,在 Unix
里表示 gb2312,在 Windows 里表示 cp936,也就是 GBK 的代码页)。

注1: 事实上,Vim 的探测准确度并不高,尤其是在 encoding 没有设置为 utf-8
时。因此强烈建议将 encoding 设置为 utf-8,虽然如果你想 Vim
显示中文菜单和提示消息的话这样会带来另一个小问题。


vim插件使用简介
1.tags
1)安装ctags
2)进入源码根目录,输入命令"ctags -R",会在当前目录下生成tags文件
3)用vim打开文件后,先输入命令"set tags=tags文件的路径",最好写在.vimrc中
4)vim中tag命令的使用
ctrl + ] 跳转函数、宏
ctrl + o 回到跳转前的地方
ctrl + T 跳到标签栈中较早的标签

在命令行中输入"vim -t 函数名" 直接打开文件并跳转函数
:tag 函数名(不分大小写) 跳转函数
:tags 显示跳转标签栈
5)vimrc设置
if has("ctags")
if filereadable("tags")
set tags=tags
endif
endif

2.cscope (创建一个数据库索引,用于查找)
1)安装cscope
2)进入源码根目录,输入命令"cscope -Rbq",会在当前目录下生成cscope.out,
cscope.in.out, cscope.po.out三个文件
3)cscope默认情况下不解析c++和java文件,可以把把这些文件的名字和路径保存在cscope.files文件中,当cscope发现在当前目录中存在cscope.files时,就会为cscope.files中列出的所有文件生成索引数据库
cd src
find . -type f > cscope.files
cscope -bq -i cscope.files
4)cscope命令行选项参数
-R: 在生成索引文件时,搜索子目录树中的代码
-b: 只生成索引文件,不进入cscope的界面
-q: 生成cscope.in.out和cscope.po.out文件,加快cscope的索引速度
-k: 在生成索引文件时,不搜索/usr/include目录
-i:
如果保存文件列表的文件名不是cscope.files时,需要加此选项告诉cscope到哪儿去找源文件列表。可以使用"-",表示由标准输入获得文件列表。
-I dir: 在-I选项指出的目录中查找头文件
-u: 扫描所有文件,重新生成交叉索引文件
-C: 在搜索时忽略大小写
-P path:
在以相对路径表示的文件前加上的path,这样,你不用切换到你数据库文件所在的目录也可以使用它了。
5)输入":cscope add 路径/cscope.out",添加cscope数据库
6)vim中":cscope find"命令的选项参数
s: 查找C语言符号,即查找函数名、宏、枚举值等出现的地方
g: 查找函数、宏、枚举等定义的位置,类似ctags所提供的功能
d: 查找本函数调用的函数
c: 查找调用本函数的函数
t: 查找指定的字符串
e: 查找egrep模式,相当于egrep功能,但查找速度快多了
f: 查找并打开文件,类似vim的find功能
i: 查找包含本文件的文件
7)vimrc设置
if has("cscope")
set csprg=/usr/bin/cscope
set csto=1
set cst
set nocsverb
if filereadable("cscope.out")
cs add cscope.out
endif
set csverb
endif

3.taglist
1)到http://www.vim.org/scripts/script.php?script_id=273处下载taglist
2)在根目录下创建.vim目录,把taglist.zip解压到此目录下
plugin/taglist.vim taglist插件
doc/taglist.txt taglist帮助文件
3)vim操作taglsit命令
:Tlist 打开taglsit
:TlistClose 关闭taglist
:TlistToggle 在打开和关闭间切换
(可以在.vimrc中定义一个映射,使用快捷键",tl"来打开或关闭taglist:
let mapleader = ","
map <silent> <leader>tl :TlistToogle<cr>
)
4).vimrc设置
let Tlist_Ctags_Cmd = '/usr/bin/ctags' 设定linux系统中ctags程序的位置
let Tlist_Show_One_File = 1 不同时显示多个文件的tag,只显示当前文件的
let Tlist_Exit_OnlyWindow = 1 如果taglist窗口是最后一个窗口,则退出vim
let Tlist_Use_Right_Window = 1 在右侧窗口中显示taglist窗口
let Tlist_Sort_Type = "name" 使taglist以tag名字进行排序
let Tlist_Use_SingleClick = 1 单击tag就跳转
let Tlist_Auto_Open = 1 启动vim后自动打开taglist窗口
let Tlist_Close_On_Select = 1 选择了tag后自动关闭taglist窗口
let Tlist_WinHeight = "" taglist窗口的高度
let Tlist_WinWidth = "" taglist窗口的宽度
let Tlist_Use_Horiz_Window = 1 taglist窗口横向显示
let Tlist_File_Fold_Auto_Close =1
同时显示多个文件中的tag时使taglist只显示当前文件tag,其它文件的tag都被折叠起来
let Tlist_GainFocus_On_ToggleOpen = 1
TlistToggle打开taglist窗口时,输入焦点在taglist窗口中
let Tlist_Process_File_Always = 1
taglist始终解析文件中的tag,不管taglist窗口有没有打开
5)taglist命令
<CR> 跳到光标下tag所定义的位置,用鼠标双击此tag功能也一样
o 在一个新打开的窗口中显示光标下tag
<Space> 显示光标下tag的原型定义
u 更新taglist窗口中的tag
s 更改排序方式,在按名字排序和按出现顺序排序间切换
x taglist窗口放大和缩小,方便查看较长的tag
+ 打开一个折叠,同zo
- 将tag折叠起来,同zc
* 打开所有的折叠,同zR
= 将所有tag折叠起来,同zM
[[ 跳到前一个文件
]] 跳到后一个文件
q 关闭taglist窗口
<F1> 显示帮助

4.lookupFile (默认使用tags文件查找文件、缓冲区、目录下文件)
1)到http://www.vim.org/scripts/script.php?script_id=1581处下载
2)解压到.vim目录下
3)lookupFile需要genutils的支持,到http://www.vim.org/scripts/script.php?script_id=197处下载,然后也解压在.vim目录下
4)按F5键或输入":LookupFile"打开查找窗口,然后用"ctrl+n"或"ctrl+p"键在列表中选择
5)输入":LUBufs"命令可以在缓冲区中查找
6)输入":LUWalk"命令输入目录名后会在下拉列表中列出这个目录中的所有子目录及文件供选择
7).vimrc设置
let g:LookupFile_MinPatLength = 2 最少输入2个字符才开始查找
let g:LookupFile_PreserveLastPattern = 0 不保存上次查找的字符串
let g:LookupFile_PreservePatternHistory = 1 保存查找历史
let g:LookupFile_AlwaysAcceptFirst = 1 回车打开第一个匹配项目
let g:LookupFile_AllowNewFiles = 0 不允许创建不存在的文件

5.colorscheme (vim顔色)
1)到http://www.vim.org/scripts/script.php?script_id=625处下载
2)解压到.vim目录下
3)使用命令"colorscheme .vim/colors/文件名"可以修改vimrc的顔色
4)vimrc设置
colorscheme darkblue

有几个插件强烈推荐
(1) A:在同名的cpp和h文件之间切换
http://www.vim.org/scripts/script.php?script_id=31
(2)
NERD_comments:超强的注释插件,支持很多语言的注释风格,按照上面的设置,只需要",cc"就可以添加注释了
http://www.vim.org/scripts/script.php?script_id=1218
(3) csupport:写C/C++代码必备
http://www.vim.org/scripts/script.php?script_id=213
(4) LargeFile:再也不用担心打开大文件会慢了
http://www.vim.org/scripts/script.php?script_id=1506
(5) TipOfTheDay:多读读vim的tips会有很多收获的,记得要经常更新tips文件啊
http://www.vim.org/scripts/script.php?script_id=88
(6) cppomnicomplete:提供C++中类/对象成员的补全(需要Vim7)
http://www.vim.org/scripts/script.php?script_id=1520
(7)
ColorSamplerPack:提供上百种颜色主题,必有一款适合你,不过大多数都是在GUI下才有相应的效果
http://www.vim.org/scripts/script.php?script_id=625
(8) vimcdoc:汉化的vim帮助 http://vimcdoc.sourceforge.net/

以下是vimrc配置文件,仅供参考:
set nu "显示行号
set background=light
"设定字体顔色模式,light使字体以较暗的顔色显示出来,适用于亮色系的背景;dark使字体以较亮的顔色显示出来,适用于暗色系的背景
set cindent "编程时c语言自动缩进
set cino=j1 "设定c/c++自动缩进的风格
set fileencodings=utf8,gb18030,big5,gb2312 "支持的字符编码
set formatoptions=rotcql "设置Vim中文本和注释的换行方式
set incsearch
"输入查找内容的同时,vim就开始对输入的内容进行匹配,并显示匹配的位置
set hlsearch "对匹配的所有项目进行高亮显示
set ignorecase "在查找时忽略大小写
set nocompatible "去掉有关vi一致性模式,避免以前版本的一些bug和局限
set shiftwidth=4 "反向制表符中的空格数目
set tabstop=4 "指定tab缩进的字符数目
set smarttab "解决shiftwidth和tabstop不等时的麻烦
set wildmenu "自动补全命令时候使用菜单式匹配列表
set mouse=a "使用鼠标

syntax on "开启语法高亮
filetype plugin indent on "打开文件类型检测功能

let mapleader = ","
let g:mapleader = ","

"tags setup
if has("ctags")
if filereadable("tags")
set tags=tags
endif
endif

"Tlist setup
let Tlist_Ctags_Cmd = '/usr/bin/ctags' "设定linux系统中ctags程序的位置
let Tlist_Sort_Type = "name" "使taglist以tag名字进行排序
let Tlist_Show_One_File = 1 "不同时显示多个文件的tag,只显示当前文件的
let Tlist_Exit_OnlyWindow = 1 "如果taglist窗口是最后一个窗口,则退出vim
let Tlist_Use_Right_Window = 1 "在右侧窗口中显示taglist窗口
let Tlist_Use_SingleClick = 1 "单击tag就跳转
let Tlist_WinWidth = 15 "taglist窗口的宽度
map <silent> <leader>tl :TlistToogle<cr>

"LookupFile setup
let g:LookupFile_MinPatLength = 2 "最少输入2个字符才开始查找
let g:LookupFile_PreserveLastPattern = 0 "不保存上次查找的字符串
let g:LookupFile_PreservePatternHistory = 1 "保存查找历史
let g:LookupFile_AlwaysAcceptFirst = 1 "回车打开第一个匹配项目
let g:LookupFile_AllowNewFiles = 0 "不允许创建不存在的文件

"cscope setup
if has("cscope")
set csprg=/usr/bin/cscope
set csto=1
set cst
set nocsverb
if filereadable("cscope.out")
cs add cscope.out
endif
set csverb
endif