jun's profileBigbearZhu,Fighting!PhotosBlogListsMore Tools Help

Blog


    January 14

    精神生活是很重要地...

    教育小孩子的东西有时发现也能教育自己。

    林格的博客:

    http://blog.sina.com.cn/linge

    January 03

    如何用PC架设Xbox Live下载服务器(2008新年版)

    需要准备的东西,可以先下载着,再慢慢看文章:

    Simple DNS Plus(DNS服务器):

    http://www.crsky.com/soft/2762.html

    Abyss Web Server(也可以用Apache,不过Abyss的图形界面感觉比较好,也小很多):

    http://www.skycn.com/soft/15752.html

    WireShark(网络消息包分析工具):

    http://jaist.dl.sourceforge.net/sourceforge/wireshark/wireshark-setup-0.99.7.exe

    文件时间属性修改工具:

    http://www.skycn.com/soft/8603.html

     

    接下来是主要内容:

    以前可以通过在路由器中添加静态路由或者添加虚拟网卡来将Xbox360发送的TCP包转向到PC上,新年到了,发现这几个方案都不能用,后来自己截包分析之后,才发现,现在的Xbox live下载服务器真是太多太多了,今天从这里下载,明天完全有可能从另一个地方下载,而且光服务器的域名都有好几个(如果发现新的,我会尽快更新)。

    最早的时候Xbox360可能都是从这个域名下载的:

    download.xbox.live.com.

    IP address = 65.55.206.154

    现在除了上面这个服务器,也有可能从这个域名下载:

    msxb.vo.llnwd.net.

    IP address = 208.111.144.45

    IP address = 208.111.144.156

    IP address = 208.111.144.164

    IP address = 208.111.144.181

    IP address = 208.111.145.40

    IP address = 208.111.145.47

    IP address = 68.142.79.107

    IP address = 68.142.79.238

    IP address = 69.28.145.13

    IP address = 69.28.145.24

    IP address = 69.28.145.61

    IP address = 69.28.183.199

    IP address = 69.28.183.216

    IP address = 69.28.183.217

    IP address = 208.111.144.35

    甚至这个域名:

    msxb-d1.vo.llnwd.net.

    IP address = 218.83.175.154

    有这么多个ip地址,以前的虚拟网卡方式就得一个一个网卡添加,或者一个一个ip添加,真的是太麻烦了,而且子网掩码,网关地址很多人也不知道怎么设定才好。设得不对,可能弄很长时间都不会从自己的PC上下载。

    曾经有人提出了伪造DNS转向,我做了试验,发现转向成功了,用迅雷都可以从自己架设的http服务器正常下载。但是Xbox 360只是朝我的PC上瞥了一眼,然后瞬间就离我而去...怎么会这样啊~!

    下面是http服务器的访问日志:

    192.168.1.2 [03/Jan/2008:15:22:54 +0800] "GET /content/485587de/7284…bfdce84.xcp HTTP/1.1" 200 270336 "" "Mozilla/4.0"(200代表成功)

    192.168.1.3 [03/Jan/2008:15:24:13 +0800] "GET /content/485587de/7284…bfdce84.xcp HTTP/1.0" 412 413 "" "Xbox Live Client/2.0.6683.0"(412代表失败了)

    后来经过在网上找了很多资料,然后又分析了一下失败的消息包,发现原来秋季更新后(个人猜测,因为我的360是2007年11月27号的,而以前的文章里从来没有提到这个问题)的Xbox 360会在下载时检测文件是不是在某一个时间被修改过的标记,如果被修改过,那么http服务器就会返回失败(比如Apache和Abyss)。

    这个问题发现后,我用文件时间属性修改工具改了文件的修改时间后成功从PC下载游戏Demo!

    具体步骤:

    1. 网络布局

    clip_image002

    2. 把PC变成一个DNS服务器(这里特指WindowsXP 或Media Center Edition系统,Windows2003Server可以参考相关配置DNS的文章)

    安装Simple DNS Plus,一路Next下去。

    运行Simple DNS Plus,会在屏幕右下角出现一个小图标,右键单击,选择“Open Simple DNS Plus”。

    clip_image004

    然后选择Tools->Options

    clip_image006

    然后选择DNS->Forwarding->Add,这里是添加一个上一级的DNS服务器,这样Xbox 360在登录Live时就不会受到任何影响了,否则可能连登录都不行。

    clip_image008

    然后添加一个DNS服务器,也就是我现在的路由器,192.168.1.1,上面一行什么都不要填。

    clip_image010

    添加好后一路OK到主界面,然后点击主界面上的Records,我们要开始添加自己的DNS信息了。

    clip_image012

    在弹出的窗口上点击“Quick…”

    clip_image014

    如此设置,OK确定出来。

    clip_image016

    继续添加两个指向PC的域名,msxb.vo.llnwd.net和msxb-d1.vo.llnwd.net

    clip_image018

    好了,这个时候我们可以试试看DNS是不是已经正确转向了,在主界面点击Look Up,输入这些信息,再点击Look Up,应该可以看到已经原来的域名已经指向我们自己了。

    clip_image020

    3. 在PC上架设http服务器

    接下来安装Abyss服务器,一路Next下去,最后一步选择第一项,手动启动,当然也可以选后两项,不过如果我们只需要在必要的时候再启动服务器,选第一项比较好。

    clip_image022

    在Abyss服务器自己启动之后,会打开一个http://127.0.0.1:9999/console/language的链接,开始设置

    clip_image024

    选择English。

    clip_image026

    随便设置管理帐号和密码,比如test,然后会提示我们用它登录:

    clip_image028

    然后点击中间的Configure

    clip_image030

    再点击左上角的General,把端口改成3074,这是默认的live下载端口。第三行的htdocs/就是我们之后要放下载好的xcp文件的目录,当然也可以改成其他的目录。

    clip_image032

    点击OK之后,会提示重新启动服务器程序,点击Restart,http服务器就设置好了。

    clip_image034

    4. 安装WireShark

    也是一路Next,中间可能会提示安装winpcap,选是,一路Next

    5. 配置Xbox360的DNS服务器

    好了,我们现在需要把360的DNS服务器地址改成手动,ip为192.168.1.2,让它接受我们替换的DNS信息。

    6. 尝试下载,得到地址和文件的最后一次修改时间

    打开WireShark,为了少抓一点包,我们先什么都不要做。

    用360登录live,然后选择一个文件下载,我这里是选择的Winning Eleven 2008 Demo,在出现确认下载的时候,先不要按A键,这时候点击PC上WireShark的Capture->Interfaces,选择PC上的网卡,点击Start开始抓包。

    clip_image036

    clip_image038

    然后按下手柄上的A键确认下载。一会儿之后,PC界面上应该会出现一大堆花花绿绿的信息

    clip_image040

    按下Ctrl+F,打开搜索窗口,如下设置

    clip_image002[5]

    选择String,输入框填GET, 左下选Packet bytes,右边选中Case sensitive,点击OK。

    如果正常的话,应该会找到一个像这样的包。中间有”Get /content/********/***…***.xcp”的字符串(注意下面第一个红框)

    clip_image004[4]

    这段文字后面应该有一个“IF-UNMODIFIED-SINCE”并紧接着一个时间,比如”Fri, 02 Nov 2007 20:58:15 GMT”(代表2007年11月2日 20点58分15秒),这就是文件最后一次修改的时间。

    就是它了!

    真实的地址,可以用download.xbox.live.com或msxb.vo.llnwd.net或msxb-d1.vo.llnwd.net加端口3074加那一段字符串,比如:

    http://msxb.vo.llnwd.net:3074/content/4b4e87ed/8fdfaf647739862db3183cb7ccb79f9b4952c635.xcp

    反正只要链接可以下载,那么就对啦!迅雷,FlashGet,单线程,多线程随便你,只是记得下载好的文件要放在Abyss刚才的那个htdocs的文件夹里,默认在Abyss的安装目录里,记得要创建content目录和那个8位字符串的目录!

    clip_image006[4]

    7. 用PC下载Demo文件,用PC下载真是要快好多啊。

    clip_image008[5]

    8. 修改文件的时间

    下载下来的文件很有可能已经跟原来的修改时间不一致了,现在更新过秋季更新的360需要文件的修改时间和原来的一致,所以我们必须要改文件的时间属性。

    打开文件时间属性修改工具,选择下载的文件,把创建时间和修改时间都改成刚才得到的时间,保存退出。

    clip_image010[4]

    9. 用360从PC下载文件

    好了,现在在用360下载原来的文件吧,正常的话,在开始菜单点击运行,输入cmd进入控制台模式,输入netstate -an,会看到360已经和PC建立了连接:

    clip_image012[4]

    而且360正在以飞快的速度下载我们的文件。下载完成后,就可以好好的享受新的游戏Demo或者视频啦!

    前天收到Kazuya从日本寄来的圣诞礼物,《Ninja Gaiden》! hoho!

    image

    传说中的忍者龙剑传XBOX版,这个可以在我的360上跑了,yeah~

    目前进度,第一关见到老大了,被狂扁一顿,感觉Boss和普通小忍者相差真是太远啊,近身被他直接抓住狂殴,远了又打不到,动作还没有他快...谁来救我!!

    December 12

    自己发明的减肚子肥肉的方法

    减肥~!减肥~!这种事情总是让我觉得太枯燥,一直不停的做一个重复的动作,又没有什么好玩的东西,不像打球的时候战胜别人能够不断刺激神经要坚持下去。但是晚上在家又不可能跟几个人打球,眼看肚子曲线一天天越来越平滑,真是头大!

    在网上找了个方法,坐在沙发上,身体慢慢向后倒,到45度角时保持5秒钟,然后再坐起来,一组10次,一天5组。

    我做了点小改动如下:坐在沙发上,旁边放一包洽洽瓜子,一次取10颗,放在茶几上,然后,从10颗里拿一颗,身体慢慢向后倒,到45度角时保持5秒钟,同时开始嗑那颗瓜子,然后再坐起来,再拿一颗,继续...一组10次,做完一组休息一下,再取10颗,继续...一天5组...

    希望有效!!

    December 04

    超级爆笑视频,存此与宝贝儿分享~!

    简单的祝福最能表达真情,愿宝贝儿天天开心~!
    November 30

    这几天看的一点东西总结

    线程栈栈顶从高往低递减
    每执行一句指令,都会改变eip的值,指向下句指令的地址
    每一个push或者pop指令都会改变esp的值
    Call 将会改变esp的值,并将call后面的一句指令地址push到栈中
    call后默认会将上次的ebp推入栈中保存,然后将当前的ebp值改为esp的值
    debug模式下每一个函数调用都会在栈上预留c0个字节的空间以供临时变量使用

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

    另收藏一篇好文,居然在我生日那天发的文,呵呵!

    转自:http://www.cpper.com/innocentius/comments/crash_dump_info/

    通过地址获取对应的源代码信息

    你写了一个程序,很开心地把它发布给用户。用户满心欢喜地运行它,突然Windows弹出了一个熟悉的窗口:

    Application Error:
    The instruction at “0x00411a28” referenced memory at “0x12345678”. The memory could not be “written”.
    Click on OK to terminate the program.
    Click CANCEL to debug program.

    顿时用户的心中升起对你的无比仇恨,然而你就会收到用户愤怒的电话,并且知道了Windows的这个几乎没用的信息。

    在这个信息中,你知道出现了一个内存访问错误,而且Windows也告诉你了这个错误发生在0x00411a28这个地方……但是,但是,等一下,即使你精通C++,即使你精通汇编语言,即使你精通计算机原理,你还是不知道这个0x00411a28表示什么,不是吗?在客户的计算机上没有调试器,即使有调试器也没有调试信息,即使有调试信息你也不可能到客户的机器上去调试,这该怎么办呢?

    这是一种很常见并且很尴尬的情况。那怎么办呢?我们的第一反应通常就是在我们的开发机上设法重现这个错误,然而实际情况并不是很理想,因为客户机的软硬件环境和开发机的软硬件环境可能有很大的差别,客户机的运行数据和开发机的数据也有很大差别,这些都导致了错误很难重现。

    为了避免这种情况,你需要学会事后调试,进一步的,如果能够让你的程序自己能够提供一些调试所必须的信息则更好。

    让我们首先来看一下我们实际上需要的是什么:

    Windows告诉了你一个地址:0x00411a28,你最想知道的是这个地址对应的是哪一句源代码,然后想知道在这个时刻的计算机运行的上下文信息,包括当时的堆栈、变量以及寄存器的值。让我们一个一个来解决:

    第一个是源代码,我们希望能够知道的是问题发生在哪一个源码文件的第几行,退而求其次的是希望知道问题发生在哪个函数里。这就需要有一个源代码和目标代码对应的关系表,但是谁知道这个关系表呢?显然,对这件事情最清楚的就是编译程序了,毕竟是它把源代码翻译成机器指令的。

    显然Visual C++的调试程序是知道这个对应关系的,否则它怎么显示正在执行到源码的什么地方?C++编译器在生成目标代码的时候同时还会生成很多调试信息,这些调试信息是包含在OBJ文件中,然后由连接程序把这些调试信息整合起来成为一个调试文件,最典型的就是PDB文件。在这个文件中包含了和程序相关的所有信息,包括源代码和目标代码的对应行号、类型、变量、函数等等。那太好了,有了这个文件我们就可以知道那个该死的地址对应于什么地方了。

    但是,等一下,这个文件是 Microsoft 的专有文件格式,我们对它是如何组织的无从得知,这该如何是好呢?先不要着急,Microsoft也不是这么绝情,它提供了一个动态链接库叫做 DBGHELP.DLL,通过这个库我们就可以访问PDB文件了。然而这个库使用起来并不简单,需要编写一个程序,我们现在是火烧眉毛,来不及做这件事情了,容后续再说。

    连接程序另外还会生成一个MAP文件,增加下面的命令行参数可以让LINK产生这个文件:

    LINK /MAP:filename.map /MAPINFO:LINES …

    这个命令行参数告诉LINK,产生一个名字为filename.map的文件,并且这个文件包含行号信息。这两个参数必须在连接你的程序时指定。有了这个文件以后我们可以来查找源文件行号和地址的对应关系了。注意,使用这个选项需要配合 /INCREMENTAL:NO 选项使用。

    下面是一个简单的例子,假设有下面这个简单的C++程序:

    1 void func()
    2 {
    3 int *p=0;
    4 *p=0;
    5 }
    6
    7 int main()
    8 {
    9 func();
    10 return 0;
    11 }
    12

    显然,在第4行应该出现一个内存访问错误,运行这个程序,出现了下面这个信息:

    Test.exe – Application Error
    The instruction at “0x00401028” referenced memory at “0x00000000”. The memory could not be “written”.

    这里报告了一个错误,地址是0x00401028。接下来让我们来看看MAP文件。文件很大,我们只看其中的一部分。

    test

    Timestamp is 42257ef9 (Wed Mar 02 16:53:13 2005)

    Preferred load address is 00400000

    Start Length Name Class
    0001:00000000 0000d886H .text CODE
    0002:00000000 000000fcH .idata$5 DATA
    0002:00000100 00001f6bH .rdata DATA
    0002:0000206c 00000040H .rdata$debug DATA
    ...

    Address Publics by Value Rva+Base Lib:Object

    0000:00000000 ___safe_se_handler_table 00000000
    0000:00000000 __except_list 00000000
    0000:00000000 ___safe_se_handler_count 00000000
    0001:00000000 ?func@@YAXXZ 00401000 f test.obj
    0001:00000040 _main 00401040 f test.obj
    0001:00000080 __RTC_InitBase 00401080 f LIBCD:init.obj
    0001:000000b0 __RTC_Shutdown 004010b0 f LIBCD:init.obj
    0001:000000d0 __RTC_CheckEsp 004010d0 f LIBCD:stack.obj
    ...
    Line numbers for .\debug\test.obj(d:\projects\private\test\test.cpp) segment .text

    2 0001:00000000 3 0001:0000001e 4 0001:00000025 5 0001:0000002e
    8 0001:00000040 9 0001:0000005e 10 0001:00000063 11 0001:00000065

    一个MAP文件被分成这么几个部分:

    test

    这表示这个MAP文件的模块名称,虽然这里看不出什么用途,但是这一点实际上很重要,我们后面就会看到。

    Timestamp is 42257ef9 (Wed Mar 02 16:53:13 2005)

    这是文件的时间戳,这个时间戳并不是文件日期,而是保存在 EXE 文件内部的一个时间戳,通过这个时间戳可以用于确定MAP文件和EXE是对应的。

    Preferred load address is 00400000

    这是最佳载入地址,对于EXE来说通常都是 0x00400000,但是对于DLL来说可能实际的载入地址是不同的。这个基地址对于后面的计算很重要。

    Start Length Name Class
    0001:00000000 0000d886H .text CODE
    0002:00000000 000000fcH .idata$5 DATA
    0002:00000100 00001f6bH .rdata DATA
    0002:0000206c 00000040H .rdata$debug DATA

    这里是段表,我们目前关心的是 .text 段。这一段中包含了程序的实际代码。上面的数据表示,第一个段就是.text段(0001段),它的起始地址是0x00000000,长度是 0xd886。按照x86的规定,一个段地址乘上0x10才是实际地址,因此实际地址是从0到0xd8860。由于Windows的PE文件就是内存映像,而PE文件有0x1000字节的头部,因此第一段的起始地址是在PE文件的0x1000处。如果PE文件被装入到0x400000地址处,那么第一段的实际地址应该在0x401000处。

    Address Publics by Value Rva+Base Lib:Object

    0000:00000000 ___safe_se_handler_table 00000000
    0000:00000000 __except_list 00000000
    0000:00000000 ___safe_se_handler_count 00000000
    0001:00000000 ?func@@YAXXZ 00401000 f test.obj
    0001:00000040 _main 00401040 f test.obj
    0001:00000080 __RTC_InitBase 00401080 f LIBCD:init.obj
    0001:000000b0 __RTC_Shutdown 004010b0 f LIBCD:init.obj
    0001:000000d0 __RTC_CheckEsp 004010d0 f LIBCD:stack.obj

    这是公共符号表,在这个表中将列出所有公共符号的地址和名称,所谓公共符号就是在汇编语言中声明为PUBLIC的符号,也就是在其它汇编文件中可以通过 EXTERN得到的符号名称。对于C++来说,如果一个全局变量、常量和函数没有被声明为static,那么它就自动声明为公共符号。

    这个公共符号表是按照地址顺序排列的,第一列是Address,是这个符号所在的地址,以 段号:偏移地址的形式表示,段号根据前面段表确定,偏移地址表示在这个段中的位置。这意味着如果我们要知道这个符号的确切地址,则需要知道段的首地址,然后加上偏移地址,段的首地址根据段表确定。

    第二列是 Publics by Value,是公共符号的名称,也就是我们一般意义上的变量名、常量名以及函数名。这里需要注意的是,在这里列出来的名字是经过修饰的名字,例如我们写的 func()函数实际上的名字是?func@@YAXXZ,main函数的实际名字是_main。关于这一点我们会在后面再详细讨论的。

    第三列是Rva+Base,表示对象的实际地址。对于Visual C++ 6.0以后的LINK会在MAP文件里面列出这个字段,但是较早的版本以及其它软件开发商,比如Borland的连接程序则没有列出这个字段,因此我们需要知道一下这个字段是怎么得到的。RVA是“相对虚拟地址”,前面已经说过,EXE文件就是程序的内存映像,它和在内存中程序的保存形式是完全一样的,因此在程序中的所有使用地址的地方都应该确定下来。然而由于EXE和DLL可能被装入到内存的任意地方,在编译时不会知道最终的地址是什么,因此只能将程序中所有使用地址的地方用一个相对于这个EXE文件的头部的形式表示,这个地址形式称为RVA。实际地址则是由RVA加上装入EXE或DLL文件时的基地址得到的(为了提高装入程序的性能,实际上LINK会把它希望的实际地址保存在EXE和DLL文件中,也就是把RVA加上前面所提到的默认装入地址(Preferred load address),如果实际的装入地址和默认装入地址相同,那么装入程序就可以省去一次重定位的过程,使得装入速度有所提高,这对于EXE来说通常都是可行的,然而对于DLL一般来说做不到。然而你可以在LINK的时候指定DLL的默认装入地址,这样可以提高DLL的装入速度,也可以使用REBASE实用程序改变一个现有DLL的默认装入地址)。

    我们来看一下main函数。main函数的公共名字是_main,它所在的地址是0001: 00000040,它所在的段是0001,从前面的段表可以查到它是第一个段,起始地址是0x00000000,而我们前面提到过,第一个段的起始地址实际上距离EXE文件的头部是0x1000,因此这个段的实际开始地址是0x1000,加上段内偏移地址0x40,那么可以得到_main的RVA是0x1040,再加上这个模块的默认装入地址 0x400000,那么可以得到结果是0x401040,也就是第三列看到的Rva+Base的值。

    第四列Lib:Object是这个符号所在的OBJ文件,我们知道OBJ文件和CPP文件基本上是一一对应的,因此通过这个信息可以知道对应的CPP文件是什么。

    Line numbers for .\debug\test.obj(d:\projects\private\test\test.cpp) segment .text

    2 0001:00000000 3 0001:0000001e 4 0001:00000025 5 0001:0000002e
    8 0001:00000040 9 0001:0000005e 10 0001:00000063 11 0001:00000065

    最后一部分是行号信息。第一句话表示源文件名,以及这个文件中哪个段的行号信息是包含在下面的列表中的。上面的例子可以看到文件名是 .\debug\test.obj 和 d:\projects\private\test\test.cpp,段是 .text。如果一个文件有好几个段,那么它可能被分布在不同的行号信息列表中。接下来的部分就是行号信息,第一个数字是行号,第二个地址是对应的段地址。这里就表示第8行对应于地址0001:00000040,就是刚才我们看到的main函数的地址,也就是源代码中main函数的开始地方。

    好了,有了上面的知识,我们再来看地址0x00401028表示什么信息。

    这个地址是一个绝对内存地址,而行号信息中只有段偏移地址,我们需要做一个转换才能完成这件事情。首先把0x00401028减去基地址 0x00400000,得到RVA0x1028,然后减去EXE文件头的0x1000,得到0x28,然而我们查段表,发现0001段从 0x00000000开始,长度是0xd886,因此0x28肯定就包含在0001段内,这样我们就可以得到绝对地址0x00401028的段偏移地址是 0001:00000028。然后我们搜索公共符号表,发现?func@@YAXXZ函数的段地址从0001:00000000到0001: 00000040,那么0001:00000028就包含在这个地址范围内,因此我们可以确定,这个错误地址是属于func函数的。进一步的,我们查行号表,看到在test.cpp文件中,第4行的地址是0001:00000025,第5行的地址是0001:0000002e,那么这说明0001: 00000028地址应该位于由test.cpp的第4行源代码生成的机器指令之中(我们应该知道一行C++程序通常会生成好几条机器指令,因此错误地址很可能没有和行号对准)。

    一般来说到这一步我们就能知道问题出现在什么地方了,如果需要更加详细的信息,那么我们可以继续看C++编译器生成的汇编语言文件。下面是这个文件的片断:

    _TEXT SEGMENT
    _p$ = -8 ; size = 4
    ?func@@YAXXZ PROC NEAR ; func, COMDAT

    ; 2 : {

    00000 55 push ebp
    00001 8b ec mov ebp, esp
    00003 81 ec cc 00 00
    00 sub esp, 204 ; 000000ccH
    00009 53 push ebx
    0000a 56 push esi
    0000b 57 push edi
    0000c 8d bd 34 ff ff
    ff lea edi, DWORD PTR [ebp-204]
    00012 b9 33 00 00 00 mov ecx, 51 ; 00000033H
    00017 b8 cc cc cc cc mov eax, -858993460 ; ccccccccH
    0001c f3 ab rep stosd

    ; 3 : int *p=0;

    0001e c7 45 f8 00 00
    00 00 mov DWORD PTR _p$[ebp], 0

    ; 4 : *p=0;

    00025 8b 45 f8 mov eax, DWORD PTR _p$[ebp]
    00028 c7 00 00 00 00
    00 mov DWORD PTR [eax], 0

    ; 5 : }

    0002e 5f pop edi
    0002f 5e pop esi
    00030 5b pop ebx
    00031 8b e5 mov esp, ebp
    00033 5d pop ebp
    00034 c3 ret 0
    ?func@@YAXXZ ENDP ; func
    _TEXT ENDS

    在这个文件中我们可以找到具体错误是发生在哪一条指令中的。需要注意的是C++生成的汇编语言文件中都以函数开始作为偏移基准,因此还需要把一个段偏移地址转换为相对于函数开始的偏移地址,方法是在公共符号表中找到这个函数,然后把段偏移地址减去这个函数的开始段偏移地址就可以了。在我们的这个例子中函数的段偏移地址是0001:00000000,因此函数内的偏移地址和它的段偏移地址是一样的。

    在这里我们可以找到地址0x0028的指令是 mov DWORD PTR [eax], 0,这条指令表示把数值0写入由eax寄存器所保存的地址中去。而eax寄存器保存的地址在前一条指令中赋值:mov eax, DWORD PTR _p$[ebp],这里ebp是当前函数的栈帧基址寄存器,_p$被定义为-8,表示变量p在堆栈上的相对位置,再前面一条指令是mov DWORD PTR _p$[ebp], 0,表示把0赋值到变量p里面去。这样这三条指令完成了这样一个操作序列:把0赋值给p,把p赋值给eax,把0写入到eax所指定的内存,这里eax就是0,因此实际执行的结果就是把数值0写入地址0。我们知道Win98/2000/XP的进程地址空间中,把从地址0开始的64K(Win98是32K)作为不可写/不可读的内存页保护起来了,所有在这个地址空间中进行的读写操作都会引起操作系统结构化异常,而且如果这个异常没有被处理,则会被 Windows捕获,然后就显示了这样一个错误信息。

    至此,我们算是彻底把这个错误找到了根源。然而这还不是全部……

    这个步骤有些复杂,如果难得查一次,或许你有这个兴趣,如果需要经常查这些信息,你就会很郁闷,因为其中涉及到很多数据和计算。大家知道计算机科学的发展源于人的惰性,因此我们为了让我们更加舒服一些就需要做进一步的考虑。

    如果发生这种关键性错误时我们能够捕获这个错误并且让我们的程序自己来显示出现在什么地方,那有多好呢?

    要实现这个技术,我们需要解决下面这些问题:

    1、怎么来捕获这个错误?
    2、捕获错误以后怎么通过程序来完成上述的动作?
    3、获得这些信息以后如何把它记录下来?

    这三个问题实际上就覆盖了一个很大范围的知识。让我们来一个个解决。

    1、 怎么来捕获这个错误?

    从机制上讲,这个错误是一个未处理SEH,也就是所谓的结构化异常。我们知道C++等语言支持异常处理,但是这些异常仅仅限制在这种语言的范围内(.NET 的异常覆盖整个 CLR,但是对我们来说范围还是不够广)。而SEH 是整个操作系统范围内的异常处理,它包括硬件和软件异常两种情况。我们在C++中可以通过下面的语句形式来捕获SEH的异常:

    __try
    {
    ...
    }
    __except(...)
    {
    }

    一种可行的处理方法是在 main 函数或者 WinMain 函数中增加一个最顶层的__try和__except,然而这种情况仅仅对单线程程序有效,而且更大的问题是在某些情况下main和WinMain函数不是我们写的(例如MFC),这时这个方法就没有办法了。

    幸运的是操作系统提供了一个函数: SetUnhandledExceptionFilter,通过这个函数可以给进程安装一个未处理异常过滤器—— 这个名字是和所谓__except部分的异常过滤器对应的——当一个进程中任何一个线程出现异常并且没有被处理时都会调用这个异常过滤器。这是一个好机会,作为一个异常过滤器,它可以得到很多有用的信息。让我们来看一下能得到些什么:
    下面是这个函数的原型:

    LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
    LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
    );

    其中lpTopLevelExceptionFilter是一个函数指针,应该具有下面的原型:

    LONG WINAPI UnhandledExceptionFilter(
    STRUCT _EXCEPTION_POINTERS* ExceptionInfo
    );

    它有一个参数,指向_EXCEPTION_POINTERS结构,这个结构包含下面内容:

    typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord;
    PCONTEXT ContextRecord;
    } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

    在这个结构中包含了两个指针,一个指向异常记录,另外一个指向上下文环境记录。异常记录包含了发生异常的详细信息,上下文环境记录包含了发生异常时的CPU状态信息。

    typedef struct _EXCEPTION_RECORD {
    DWORD ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD* ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
    } EXCEPTION_RECORD, *PEXCEPTION_RECORD;

    在异常记录结构中,最重要的信息是ExceptionCode,它包含了异常代码。对于内存访问违例来说,它的异常代码就是EXCEPTION_ACCESS_VIOLATION。当然还包括其它的异常代码。

    第二个字段是ExceptionFlags,如果它是0,那么表示这个异常是可以恢复的,如果是EXCEPTION_NONCONTINUABLE 那么表明这个异常是不能恢复的(在C++中,一旦抛出一个异常那么它就不可能再回到抛出异常的地方继续执行,而SEH则可以)。

    第三个字段是前一个未处理异常结构。和C++异常不同,SEH异常可以嵌套抛出,可以在异常处理过程中继续抛出异常。从这里可以看到多个异常被组织成了一个链表,因此你在异常过滤器中可以追踪到底发生了多少异常。

    第四个字段表明了发生异常的地址,就是Windows显示给你看到的那个地址。

    第五个字段表明,对于这个异常,有多少个附加的参数。参数是保存在第六个字段中的。到目前为止,只有 EXCEPTION_ACCESS_VIOLATION异常会包含2个参数,第一个参数,也就是 ExceptionInformation[0] 表示对内存读写状态。如果是因为读内存造成异常的,那么这个参数是0,如果是因为写内存造成异常的,那么这个参数就是1。第二个参数是访问违例的内存地址。

    上面提到的这个错误实际上就是这个异常结构中内容的一种可视形式,现在我们可以重新回头来看一下造成这个错误的异常信息:

    ExceptionCode: EXCEPTION_ACCESS_VIOLATION
    ExceptionFlags: 0
    ExceptionRecord: NULL
    ExceptionAddress: 0x00401028
    NumberParameters: 2
    ExceptionInformation[0]: 1
    ExceptionInformation[1]: 0x00000000

    上下文环境记录是用于保存发生异常时的CPU状态,它是在Windows SDK中唯一一个和硬件相关的数据结构,目前我们不需要用它,但是后面会用到。

    我们所要做的第一步现在已经清楚了,我们只需要安装一个全局的未处理异常过滤器,在这个过滤器中就能得到出现异常的详细信息。下一步就是要把这些信息转换成更加容易理解的形式。

    【注意】需要特别注意的一个问题是,如果当前处于调试程序的控制下,例如由Visual C++调试你的程序时,这个全局未处理异常过滤器是不会被调用的,有些资料上说这是一个BUG,然而我觉得不是。不管怎么样这个目前是事实。可以通过调用 IsDebuggerPresent函数来判断。这个函数在Windows 95下不存在,因此需要在包含Windows.h前加上下面语句:

    #define _WIN32_WINNT 0x0400

    2 捕获到这个错误以后如何通过程序来处理它

    前面我们已经说到可以通过异常来捕获这个错误(当然,需要声明的是这是对于未处理异常来说通过这个方式来捕获),一旦捕获这个异常后我们就需要通过程序来完成前面手工完成的操作,也就是把一个地址对应到一个源代码行去。

    手工对应是通过阅读map文件进行的,那么程序该如何做呢?同样的,当发生一个异常时我们已经知道了对应的地址,然后我们查阅map文件。然而我们直接处理map文件是不方便的,因此需要有一种内部格式,也就是和map具有类似信息,但是处理起来更加方法的格式。

    我把这种格式称为SDB(Symbol Database,符号数据库),与map文件对应的里面包含了:段表、符号表和行号表。差别在于我们用对于程序来说更加方便理解的二进制形式来保存,而不是使用对人来说更加容易理解的文本形式保存。这个文件的具体格式按照如下定义:

    文件首部,是一个SDBHeader结构,定义为:

    struct SDBHeader
    {
    DWORD mMagic; // “SDB0”
    DWORD mSizeOfHeader; // 首部长度,包括 mModuleName 的所有字符。
    DWORD mTimeStamp; // 时间戳
    DWORD mBase; // 参考基准地址
    DWORD mFileNum; // 文件表表项数
    DWORD mFileTableOffset; // 文件表偏移地址,相对与文件首部的起始字节
    DWORD mSegmentNum; // 段表表项数
    DWORD mSegmentTableOffset; // 段表偏移地址,相对与文件首部的起始字节
    DWORD mSymbolNum; // 符号表表项数
    DWORD mSymbolTableOffset; // 符号表偏移地址,相对与文件首部的起始字节
    DWORD mLineNum; // 行号表表项数
    DWORD mLineTableOffset; // 行号表偏移地址,相对与文件首部的起始字节
    DWORD mStringTableSize; // 字符串表长度
    DWORD mStringTableOffset; // 字符串表偏移地址,相对与文件首部的起始字节
    DWORD mReserved0; // 保留(0)
    DWORD mReserved1; // 保留(0)
    DWORD mReserved2; // 保留(0)
    DWORD mReserved3; // 保留(0)
    DWORD mModuleNameSize; // 模块名长度
    CHAR mModuleName[1]; // 模块名
    };

    这里mMagic是一个标识符,表示这是一个SDBHeader,值为SDB_HEADER_MAGIC(0x30424453)。在这个结构中包含了五个表的开始地址:

    文件表是用来记录在这个符号文件中出现的源代码文件名;
    段表是用来记录map文件中的段信息;
    符号表用来记录map文件中的公共符号信息;
    行号表用来记录map文件中的行号信息;
    字符串表用来保存SDB文件中用到的所有字符串。为了节省空间,每个字符串在SDB文件中只出现一次,并且需要使用字符串的地方都用一个指针去引用而不是包含字符串本身。

    需要注意的是这个结构是变长的,因为模块名长度可变。

    文件表,是多个FileIndexItem字段,其数量由文件首部的mFileNum确定,这个结构定义为:

    struct FileIndexItem
    {
    DWORD mFileNameLength; // 文件名长度
    DWORD mFileNameOffset; // 文件名在字符串表中的偏移地址,相对与字符串表的起始字节
    };

    段表,是多个SegmentIndexItem字段,其数量由文件首部的mSegmentNum确定,这个结构定义为:

    struct SegmentIndexItem
    {
    DWORD mAddress; // 段起始地址
    DWORD mSegmentLength; // 段长度
    DWORD mSegmentNameLength; // 段名长度
    DWORD mSegmentNameOffset; // 段名在字符串表中的偏移地址,相对于字符串表的起始字节
    DWORD mClassNameLength; // 段类名长度
    DWORD mClassNameOffset; // 段类名在字符串表中的偏移地址,相对与字符串表的起始字节
    };

    在上表中我们在创建SDB文件的时候就把段起始地址转换为RVA的形式。

    符号表,是多个SymbolIndexItem字段,其数量由文件首部的mSymbolNum确定,这个结构定义为:

    struct SymbolIndexItem
    {
    DWORD mAddress; // 符号所在地址
    DWORD mSymbolNameLength; // 符号名长度
    DWORD mSymbolNameOffset; // 符号名在字符串表中的偏移地址,相对于于字符串表的起始字节
    DWORD mSymbolFlag; // 符号类型(目前未用)
    };

    上表中,我们在创建SDB文件的时候就把符号地址转换为RVA形式。

    行号表,是多个LineIndexItem字段,其数量由文件首部的mLineNum确定,这个结构定义为:

    struct LineIndexItem
    {
    DWORD mAddress; // 行号所在地址
    DWORD mLineNum; // 行号
    DWORD mFileIndex; // 文件表索引
    };

    同样,上表中把mAddress转换成RVA形式。

    字符串表,就是多个以0结尾的字符串序列,其总长度(字节数)由文件首部mStringTableSize确定。

    最后还有一个文件尾部结构SDBTail,定义如下:

    struct SDBTail
    {
    DWORD mMagic; // “SDBT”
    DWORD mHeaderOffset; // 首部偏移地址,计算方法为 SDBTail 的偏移地址减去 mHeaderOffset
    // 即为首部的偏移地址
    };

    其中mMagic是一个标识符,表示这是一个SDBHeader,值为SDB_TAIL_MAGIC(0x54424453)。

    为了确保EXE文件和SDB文件能够一一对应,我决定把SDB文件附加在EXE文件的后面,因此需要有一个文件尾部结构来标记一下,并且可以通过它找到文件首部在什么地方。

    我编写了一个工具程序,mapcvt.exe,可以通过它把VC6/7生成的map文件转换成sdb文件并且绑定到一个exe文件去。

    定义了这样一个文件格式,并且有了一个可以把map文件转换成sdb文件的工具,那么我们就可以把这些操作自动化了:在VC的Post-Build事件中添加下列命令:

    mapcvt aaa.map aaa.exe

    这里aaa.map就是你的项目生成的map文件名,aaa.exe就是你最终生成的exe文件名。VC会在每次成功编译后调用mapcvt来完成这个转换。

    接下来一步,就是我们如何来读取这个文件,有了这个文件格式,那么读取已经不是很复杂的事情了。我编写了另外一个库,sdbhelp.dll,可以协助你完成这个操作。在这个库中导出了下面这些函数:

    // SDB Image API
    SHAPI HSHIMG WINAPI SDBInitializeImage(HANDLE hProcess, BOOL fInvadeProcess);
    SHAPI BOOL WINAPI SDBGetSymbolFromAddress(HSHIMG hImg, DWORD dwAddress,
    LPSTR lpszSymbolName, DWORD dwSymbolNameLength,
    LPDWORD lpdwDisplacement);
    SHAPI BOOL WINAPI SDBGetLineFromAddress(HSHIMG hImg, DWORD dwAddress,
    LPDWORD lpdwLine, LPSTR lpszFileName,
    DWORD dwFileNameLength, LPDWORD lpdwDisplacement);
    SHAPI BOOL WINAPI SDBDestroyImage(HSHIMG hImg);

    SDBInitializeImage用于初始化,提供给它一个进程句柄,然后它会搜索这个进程中所有模块是否具备SDB符号信息。如果 fInvadeProcess为TRUE,则它试图装入所有可能找到的符号信息,否则将在用到的时候才装入符号信息。在这种情况下,它会自动确定每个符号文件的基地址。这个函数返回一个HSHIMG句柄,用来表示一个符号处理器。

    SDBGetSymbolFromAddress函数将一个物理地址转换成对应的符号名,如果转换成功则返回TRUE,否则返回FALSE。符号名保存在lpszSymbolName所指向的缓冲区,这个缓冲区的长度由输入参数dwSymbolNameLength指定,另外一个参数 lpdwDisplacement用于保存这个地址相对于符号开始地址的偏移量。

    SDBGetLineFromAddress函数将一个物理地址转换成对应的源文件名和行号,如果转换成功则返回TRUE,否则返回FALSE。行号保存在lpdwLine所指的空间中,文件名保存在lpszFileName所指向的缓冲区中,这个缓冲区的长度由输入参数 dwFileNameLength指定,lpdwDisplacement用于保存这个地址相对于行开始地址的偏移量。

    SDBDestroyImage用于关闭符号表。

    有了这样一个dll后,我们就可以编写一些函数来使用它了。下面我对这个dll进行了一些封装。

    在封装过程中我们需要考虑另外一件事情。前面我们说过,VC本身其实也是具备调试信息的,就是PDB文件,然而这个文件格式没有公开,因此我们没法直接调用它。但是微软提供了一个DBGHELP.DLL文件,它能够满足我们的要求,关于DBGHELP.DLL的详细信息在MSDN的下列位置(这是最新MSDN位置,如果你使用的MSDN较早,可能有所不同):

    MSDN Library-January 2005
    Win32 and COM Development
    System Services
    Debugging and Error Handling
    Debug Help Library

    在 DBGHELP.DLL中有一个函数叫做SymGetLineFromAddr64,另外还有一个SymGetSymbolFromAddr64(这个函数已经过时,应该使用SymFromAddr),这两个函数的作用和SDBHELP中提供的两个函数作用差不多。

    我们为什么不直接使用DBGHELP,而要自己写一个SDBHELP呢?最主要的是基于移植的原因。DBGHELP只能解析Visual C++的专有PDB格式,而其它编译器并不生成这个文件。但是几乎所有的编译器都生成非常类似的map文件,我们只需要把mapcvt程序做一点修改,就能使SDBHELP支持Borland C++ Builder,支持 Delphi甚至gcc。另外一个原因是PDB格式文件很大,远比SDB要大,并且不能和EXE结合在一个文件内,导致分发不方便。但是如果能够支持 PDB,那么也是一件很好的事情。

    class DebugSymbolHandler: public DebugObject
    {
    public:
    virtual bool Open(const DebugString & search_path="”, HANDLE process=GetCurrentProcess()) =0;
    virtual void Close() =0;
    virtual bool GetSymbolFromAddress(ADDRESS addr, DebugSymbol & symbol,
    ADDRESS & displacement) const =0;
    virtual bool GetLineFromAddress(ADDRESS addr, DebugLine & line,
    ADDRESS & displacement) const =0;
    static bool GetModuleFromAddress(HANDLE process, ADDRESS addr, DebugModule & module,
    ADDRESS & displacement);
    };

    上面定义了一个基本的DebugSymbolHandler类,用于完成从地址到符号、到行号的转换,这里使用DebugSymbol、DebugLine和DebugModule表示符号、行号和模块名。

    DebugObject是所有调试对象的基类,它重载了所有内存分配函数,下面是一个简单的实现:

    class DebugObject
    {
    public:
    virtual ~DebugObject()
    {
    }
    static void * operator new (size_t size)
    {
    return malloc(size);
    }
    static void * operator new (size_t size, void * ptr)
    {
    return ptr;
    }
    static void * operator new [] (size_t size)
    {
    return malloc(size);
    }
    static void * operator new [] (size_t size, void * ptr)
    {
    return ptr;
    }
    static void operator delete (void * ptr)
    {
    free(ptr);
    }
    static void operator delete (void *, void *)
    {
    }
    static void operator delete [] (void * ptr)
    {
    free(ptr);
    }
    static void operator delete [] (void *, void *)
    {
    }
    };

    【注】上面的类中没有实现 nothrow 的重载版本。

    由于我们还将把这些调试对象类用于内存泄漏分析,我们不能让它们使用任何标准的new和delete,需要非常小心地进行内存管理,因此我们重载了这些函数。

    由于这个原因,我们如果要使用字符串也需要小心。虽然可以通过自定义分配器来实现一个自定义内存管理的std::string,但是一来这是我另外一个更大的但是尚未完全实现的一个框架中的东西,二来对于调试这么基本的代码来说,使用std::string有些不受控制。因此我自己写了一个简单但是不使用标准new和delete的字符串类DebugString。

    class DebugString: public DebugObject{
    private:
    static char * mNullBuffer;
    private:
    char * mBuffer;
    DWORD mSize;
    public:
    DebugString(const char * str=0);
    DebugString(const DebugString & rhs);
    virtual ~DebugString();
    operator const char * () const;
    DWORD Length() const;
    DebugString & operator = (const char * str);
    DebugString & operator = (const DebugString & rhs);
    static const char * GetNullBuffer();
    };

    调试符号名、行号和模块由下面几个类定义:

    typedef ULONG64 ADDRESS;

    struct DebugLine: public DebugObject
    {
    DebugString mFileName;
    DWORD mLineNumber;
    ADDRESS mAddress;

    DebugLine(const DebugString & filename="”, DWORD linenum=0, ADDRESS addr=0)
    :mFileName(filename),
    mLineNumber(linenum),
    mAddress(addr)
    {
    }
    };

    struct DebugSymbol: public DebugObject
    {
    ADDRESS mModuleBase;
    ADDRESS mAddress;
    DebugString mSymbolName;

    DebugSymbol(ADDRESS base=0, ADDRESS address=0, const DebugString & symbol_name="")
    :mModuleBase(base),
    mAddress(address),
    mSymbolName(symbol_name)
    {
    }
    };

    struct DebugModule: public DebugObject
    {
    ADDRESS mImageBase;
    DWORD mImageSize;
    DebugString mModuleName;

    DebugModule(ADDRESS base=0, DWORD size=0, const DebugString & module_name="")
    :mImageBase(base),
    mImageSize(size),
    mModuleName(module_name)
    {
    }
    };

    这几个都是很简单的结构,仅仅用于表示这些信息而已。

    最后我们需要实现自己的SDBSymbolHandler:

    struct SDBSymbolHandlerImpl;

    class SDBSymbolHandler: public DebugSymbolHandler
    {
    private:
    SDBSymbolHandlerImpl * mImpl;

    public:
    SDBSymbolHandler();
    ~SDBSymbolHandler();

    virtual bool Open(const DebugString & search_path="”, HANDLE process=GetCurrentProcess());
    virtual void Close();
    virtual bool GetSymbolFromAddress(ADDRESS addr, DebugSymbol & symbol,
    ADDRESS & displacement) const;
    virtual bool GetLineFromAddress(ADDRESS addr, DebugLine & line,
    ADDRESS & displacement) const;
    };

    这个类的实现相当简单,只需要直接调用SDBHELP中的函数即可:

    struct SDBSymbolHandlerImpl: public DebugObject
    {
    HANDLE mProcessHandle;
    HSHIMG mImage;
    };

    SDBSymbolHandler::SDBSymbolHandler()
    :mImpl(0)
    {
    }

    SDBSymbolHandler::~SDBSymbolHandler()
    {
    Close();
    }

    bool SDBSymbolHandler::Open(const DebugString & search_path/* ="” */, HANDLE process/* =GetCurrentProcess( */)
    {
    if(mImpl==0)
    {
    if(!LoadSdbHelp())
    {
    return false;
    }

    mImpl=new SDBSymbolHandlerImpl;

    mImpl->mImage=PSDBInitializeImage(process,FALSE);
    if(mImpl->mImage!=0)
    {
    return true;
    }
    else
    {
    Close();
    return false;
    }
    }
    else
    return true;
    }

    void SDBSymbolHandler::Close()
    {
    if(mImpl!=0)
    {
    PSDBDestroyImage(mImpl->mImage);
    delete mImpl;
    mImpl=0;
    }
    }

    bool SDBSymbolHandler::GetSymbolFromAddress(ADDRESS addr, DebugSymbol & symbol, ADDRESS & displacement) const
    {
    if(mImpl!=0)
    {
    DWORD disp;
    CHAR buf[1024+1];
    if(PSDBGetSymbolFromAddress(mImpl->mImage,static_cast(addr),buf,1024,&disp))
    {
    symbol.mAddress=addr;
    symbol.mSymbolName=buf;
    displacement=disp;
    return true;
    }
    else
    {
    return false;
    }
    }
    else
    {
    return false;
    }
    }

    bool SDBSymbolHandler::GetLineFromAddress(ADDRESS addr, DebugLine & line_info, ADDRESS & displacement) const
    {
    if(mImpl!=0)
    {
    DWORD line, disp;
    CHAR buf[1024+1];
    if(PSDBGetLineFromAddress(mImpl->mImage,static_cast(addr),&line,buf,1024,&disp))
    {
    line_info.mAddress=addr;
    line_info.mLineNumber=line;
    line_info.mFileName=buf;
    displacement=disp;
    return true;
    }
    else
    {
    return false;
    }
    }
    else
    {
    return false;
    }
    }

    LoadSdbHelp函数用于装入SDBHELP.DLL。我们使用动态的方式装入这个程序库,是因为如果不存在这个文件虽然使调试信息无法读取,但是不应该导致程序不能正常运行。

    typedef HSHIMG(WINAPI*TSDBInitializeImage)(HANDLE hProcess, BOOL fInvadeProcess);
    typedef BOOL(WINAPI*TSDBGetSymbolFromAddress)(HSHIMG hImg, DWORD dwAddress,
    LPSTR lpszSymbolName, DWORD dwSymbolNameLength, LPDWORD lpdwDisplacement);
    typedef BOOL(WINAPI*TSDBGetLineFromAddress)(HSHIMG hImg, DWORD dwAddress, LPDWORD lpdwLine,
    LPSTR lpszFileName, DWORD dwFileNameLength, LPDWORD lpdwDisplacement);
    typedef BOOL(WINAPI*TSDBDestroyImage)(HSHIMG hImg);

    static TSDBInitializeConverter PSDBInitializeConverter;
    static TSDBSetModuleInfo PSDBSetModuleInfo;
    static TSDBAddSegment PSDBAddSegment;
    static TSDBAddSymbol PSDBAddSymbol;
    static TSDBAddLine PSDBAddLine;
    static TSDBWriteImage PSDBWriteImage;
    static TSDBDestroyConverter PSDBDestroyConverter;
    static TSDBInitializeImage PSDBInitializeImage;
    static TSDBGetSymbolFromAddress PSDBGetSymbolFromAddress;
    static TSDBGetLineFromAddress PSDBGetLineFromAddress;
    static TSDBDestroyImage PSDBDestroyImage;

    static bool LoadSdbHelp()
    {
    static HINSTANCE lib=0;

    if(lib!=0)
    {
    return true;
    }

    lib=LoadLibrary(TEXT("sdbhelp.dll"));

    if(lib)
    PSDBInitializeImage=(TSDBInitializeImage)GetProcAddress(lib,"SDBInitializeImage");
    if(PSDBInitializeImage)
    PSDBGetSymbolFromAddress=(TSDBGetSymbolFromAddress)GetProcAddress
    (lib,"SDBGetSymbolFromAddress");
    if(PSDBGetSymbolFromAddress)
    PSDBGetLineFromAddress =(TSDBGetLineFromAddress)GetProcAddress
    (lib,"SDBGetLineFromAddress");
    if(PSDBGetLineFromAddress)
    PSDBDestroyImage =(TSDBDestroyImage)GetProcAddress(lib,"SDBDestroyImage");
    if(PSDBDestroyImage)
    turn true;

    FreeLibrary(lib);
    lib=0;
    return false;
    }

    有了这几个类,我们就可以在自己的程序中根据出错的地址查找对应的符号。具体的可以参见相应的源代码。

    不过,如果仅仅知道当时的行号和文件还不是完全有用,比如说,如果错误出现在一个很常见的实用函数中,而这个函数会在各个地方被调用,如果我们仅仅知道问题出现在这个函数中,那么还是无法找到问题的根源。此时我们需要知道的是当时的执行上下文,这包括两个方面的内容:CPU状态和堆栈状态。

    CPU状态就是在EXCEPTION_POINTERS中的ContextRecord,这里面包含了CPU的所有寄存器值。也可以通过GetThreadContext函数来获取。

    在DBGHELP.DLL中存在一个函数称为StackWalk64,如果知道了栈顶地址,那么我们可以使用这个函数来搜索整个堆栈中的函数调用情况。在此我们需要先了解一下系统的堆栈组织结构。

    每个线程具有一个运行栈,这个运行栈是一段内存,其栈顶地址由寄存器ESP指出。每当调用了一个函数,通常会生成下列标准调用代码(C调用规范):

    Push arg1
    Push arg2
    Call func
    Add esp, 4

    这段代码将在堆栈中形成2个参数和一个函数返回地址,当函数返回时,函数返回地址由ret指令弹出,所以随后的add指令可以用于平衡压入参数后的堆栈。

    进入函数后,一般会生成下列标准前序代码:

    Push ebp
    Mov ebp, esp
    Sub esp, xxxx

    该代码会先把ebp压入堆栈,然后把esp赋值给ebp,此时ebp的值就称为堆栈帧的基址,以此作为当前函数在堆栈上所有对象的访问基准。例如,函数的参数就是ebp+8(需要注意的是,堆栈是向低地址增长;ebp本身指向的是压入堆栈的前一个ebp值,ebp+4是函数返回地址,因此第一个参数就是 ebp+8,第二个参数就是ebp+12(设参数为4字节),以此类推。Sub esp, xxxx指令把esp寄存器减去一个数值,这个数值就是这个函数所有局部变量的总大小。如果函数只有一个局部变量,且其大小为4字节,那么这里就应该是 sub esp, 4。同样可以通过ebp寄存器来访问局部变量,不同的是,访问参数时使用正偏移量,而使用局部变量时使用负偏移量,因此第一个局部变量就应该是:ebp- 8,第二个局部变量就是ebp-12(假设变量为4个字节),以次类推。

    任意一个程序总是由操作系统来调用它的启动代码,而它的启动代码再依次调用其各个函数。因此ebp寄存器就始终被用作堆栈帧的基址,而且由于存在 push ebp指令,使当前函数在堆栈中总能够找到它的调用者的ebp值。这就形成了一个链表,通过遍历这个链表就能追踪整个堆栈直到第一个启动当前线程的地方了(这个地方在Windows内部)。

    函数在返回前,会生成下列标准后序代码,用于平衡堆栈:

    Add esp, xxxx
    Pop ebp
    Ret

    所有上面所说的这些代码形成所谓的标准堆栈帧。

    前面我们说到可以以访问ebp寄存器以及堆栈内存的方式遍历整个堆栈帧链表,从而知道到底是谁调用了谁。这个过程完全可以这样做,但是也可以通过StackWalk64函数来完成这个操作。

    使用这个函数之前必须要准备好CPU上下文,如果你已经知道了EBP、ESP等寄存器的值,那么没什么问题,否则就需要使用 GetThreadContext函数来获取这些信息。需要注意的是,如果当前正在执行的线程用于获取当前线程的CPU上下文,则此时得到的EIP、 EBP以及ESP寄存器是不正确的(实际上这些寄存器的值表示的是GetThreadContext内部执行时的值,而不是我们所期望的调用 GetThreadContext之前的值),因此对于这种情况需要对这三个寄存器值做一个修正,在Visual C++下可以通过下面指令进行:

    DWORD regEIP, regEBP, regESP;
    __asm
    {
    call l1 // call 指令将当前 EIP 压入堆栈
    l1: pop edx // 弹出 EIP
    mov dword ptr [regEIP], edx // 设置 EIP
    mov dword ptr [regEBP], ebp // 设置 EBP
    mov dword ptr [regESP], esp // 设置 ESP
    }
    mImpl->mContext.Eip=regEIP;
    mImpl->mContext.Ebp=regEBP;
    mImpl->mContext.Esp=regESP;

    由于我们无法直接获取EIP寄存器的值,因此执行一个call指令,它会把EIP寄存器的值保存到堆栈,下一条指令就直接把它从堆栈中弹出,保存在edx中。

    有了当前CPU上下文以后就可以构建起堆栈帧的参数结构了:

    mImpl->mStackFrame.AddrPC.Mode =AddrModeFlat;
    mImpl->mStackFrame.AddrPC.Offset =mImpl->mContext.Eip;
    mImpl->mStackFrame.AddrFrame.Mode =AddrModeFlat;
    mImpl->mStackFrame.AddrFrame.Offset =mImpl->mContext.Ebp;
    mImpl->mStackFrame.AddrStack.Mode =AddrModeFlat;
    mImpl->mStackFrame.AddrStack.Offset =mImpl->mContext.Esp;

    AddrModeFlat表明使用的平面地址模式。

    有了这些结构后就可以通过调用StackWalk64来遍历整个堆栈帧了。DebugStackFrame类实现了对这个过程的封装。

    需要注意的是,某些编译器可以有一个“省略堆栈帧”的优化选项,该选项针对没有参数和没有局部变量的函数有效,可以减少生成的代码。但是由于没有堆栈帧的构建代码,因此如果遇到这样的函数就无法继续遍历堆栈了。

    Posted by innocentius on 01/22 at 06:27 AM

    October 05

    LNK2005错误详解(ZT)

    许多Visual C++的使用者都碰到过LNK2005:symbol already defined和LNK1169:one or more multiply defined symbols found这样的链接错误,而且通常是在使用第三方库时遇到的。对于这个问题,有的朋友可能不知其然,而有的朋友可能知其然却不知其所以然,那么本文就试图为大家彻底解开关于它的种种疑惑。

        大家都知道,从C/C++源程序到可执行文件要经历两个阶段:(1)编译器将源文件编译成汇编代码,然后由汇编器(assembler)翻译成机器指令(再加上其它相关信息)后输出到一个个目标文件(object file,VC的编译器编译出的目标文件默认的后缀名是.obj)中;(2)链接器(linker)将一个个的目标文件(或许还会有若干程序库)链接在一起生成一个完整的可执行文件。

        编译器编译源文件时会把源文件的全局符号(global symbol)分成强(strong)和弱(weak)两类传给汇编器,而随后汇编器则将强弱信息编码并保存在目标文件的符号表中。那么何谓强弱呢?编译器认为函数与初始化了的全局变量都是强符号,而未初始化的全局变量则成了弱符号。比如有这么个源文件:

    extern int errorno;
    int buf[2] = {1,2};
    int *p;

    int main()
    {
       return 0;
    }

    其中main、buf是强符号,p是弱符号,而errorno则非强非弱,因为它只是个外部变量的使用声明。

        有了强弱符号的概念,链接器(Unix平台)就会按如下规则(参考[1],p549~p550)处理与选择被多次定义的全局符号:

    规则1: 不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);

    规则2: 如果一个符号在某个目标文件中是强符号,在其它文件中都是弱符号,那么选择强符号;

    规则3: 如果一个符号在所有目标文件中都是弱符号,那么选择其中任意一个;

        虽然上述3条针对的是Unix平台的链接器,但据作者试验,至少VC6.0的linker也遵守这些规则。由此可知多个目标文件不能重复定义同名的函数与初始化了的全局变量,否则必然导致LNK2005和LNK1169两种链接错误。可是,有的时候我们并没有在自己的程序中发现这样的重定义现象,却也遇到了此种链接错误,这又是何解?嗯,问题稍微有点儿复杂,容我慢慢道来。

        众所周知,ANSI C/C++ 定义了相当多的标准函数,而它们又分布在许多不同的目标文件中,如果直接以目标文件的形式提供给程序员使用的话,就需要他们确切地知道哪个函数存在于哪个目标文件中,并且在链接时显式地指定目标文件名才能成功地生成可执行文件,显然这是一个巨大的负担。所以C语言提供了一种将多个目标文件打包成一个文件的机制,这就是静态程序库(static library)。开发者在链接时只需指定程序库的文件名,链接器就会自动到程序库中寻找那些应用程序确实用到的目标模块,并把(且只把)它们从库中拷贝出来参与构建可执行文件。几乎所有的C/C++开发系统都会把标准函数打包成标准库提供给开发者使用(有不这么做的吗?)。

        程序库为开发者带来了方便,但同时也是某些混乱的根源。我们来看看链接器(Unix平台)是如何解析(resolve)对程序库的引用的(参考[1],p556)。
        在符号解析(symbol resolution)阶段,链接器按照所有目标文件和库文件出现在命令行中的顺序从左至右依次扫描它们,在此期间它要维护若干个集合:(1)集合E是将被合并到一起组成可执行文件的所有目标文件集合;(2)集合D是所有之前已被加入E的目标文件定义的符号集合;(3)集合U是未解析符号(unresolved symbols,即那些被E中目标文件引用过但在D中还不存在的符号)的集合。一开始,E、D、U都是空的。

    (1): 对命令行中的每一个输入文件f,链接器确定它是目标文件还是库文件,如果它是目标文件,就把f加入到E,并把f中未解析的符号和已定义的符号分别加入到U、D集合中,然后处理下一个输入文件。

    (2): 如果f是一个库文件,链接器会尝试把U中的所有未解析符号与f中各目标模块定义的符号进行匹配。如果某个目标模块m定义了一个U中的未解析符号,那么就把m加入到E中,并把m中未解析的符号和已定义的符号分别加入到U、D集合中。不断地对f中的所有目标模块重复这个过程直至到达一个不动点(fixed point),此时U和D不再变化。而那些未加入到E中的f里的目标模块就被简单地丢弃,链接器继续处理下一输入文件。

    (3): 当扫描完所有输入文件时如果U非空或者有同名的符号被多次加入D,链接器报告错误信息并退出。否则,它把E中的所有目标文件合并在一起生成可执行文件。

        上述规则针对的是Unix平台链接器,而VC(至少VC6.0)linker则有相当的不同: 它首先依次处理命令行中出现的所有目标文件,然后依照顺序不停地扫描所有的库文件,直至U为空或者某遍(从头到尾依次把所有的库文件扫描完称为一遍)扫描过程中U、D无任何变化时结束扫描,此刻再根据U是否为空以及是否有同名符号重复加入D来决定是出错退出还是生成可执行文件。很明显Unix链接器对输入文件在命令行中出现的顺序十分敏感,而VC的算法则可最大限度地减少文件顺序对链接的影响。作者不清楚Unix下新的开发工具是否已经改进了相应的做法,欢迎有实践经验的朋友补充这方面的信息(补充于2005年10月10日: 经试验,使用gcc 3.2.3的MinGW 3.1.0的链接器表现与参考[1]描述的一致)。

        VC带的编译器是cl.exe,它有这么几个与标准程序库有关的选项: /ML、/MLd、/MT、/MTd、/MD、/MDd。这些选项告诉编译器应用程序想使用什么版本的C标准程序库。/ML(缺省选项)对应单线程静态版的标准程序库(libc.lib);/MT对应多线程静态版标准库(libcmt.lib),此时编译器会自动定义_MT宏;/MD对应多线程DLL版(导入库msvcrt.lib,DLL是msvcrt.dll),编译器自动定义_MT和_DLL两个宏。后面加d的选项都会让编译器自动多定义一个_DEBUG宏,表示要使用对应标准库的调试版,因此/MLd对应调试版单线程静态标准库(libcd.lib),/MTd对应调试版多线程静态标准库(libcmtd.lib),/MDd对应调试版多线程DLL标准库(导入库msvcrtd.lib,DLL是msvcrtd.dll)。虽然我们的确在编译时明白无误地告诉了编译器应用程序希望使用什么版本的标准库,可是当编译器干完了活,轮到链接器开工时它又如何得知一个个目标文件到底在思念谁?为了传递相思,我们的编译器就干了点秘密的勾当。在cl编译出的目标文件中会有一个专门的区域(关心这个区域到底在文件中什么地方的朋友可以参考COFF和PE文件格式)存放一些指导链接器如何工作的信息,其中有一项就叫缺省库(default library),它指定了若干个库文件名,当链接器扫描该目标文件时将按照它们在目标模块中出现的顺序处理这些库名: 如果该库在当前输入文件列表中还不存在,那么便把它加入到输入文件列表末尾,否则略过。说到这里,我们先来做个小实验。写个顶顶简单的程序,然后保存为main.c :

    /* main.c */
    int main() { return 0; }

    用下面这个命令编译main.c(什么?你从不用命令行来编译程序?这个......) :

    cl /c main.c

    /c是告诉cl只编译源文件,不用链接。因为/ML是缺省选项,所以上述命令也相当于: cl /c /ML main.c 。如果没什么问题的话(要出了问题才是活见鬼!当然除非你的环境变量没有设置好,这时你应该去VC的bin目录下找到vcvars32.bat文件然后运行它。),当前目录下会出现一个main.obj文件,这就是我们可爱的目标文件。随便用一个文本编辑器打开它(是的,文本编辑器,大胆地去做别害怕),搜索"defaultlib"字符串,通常你就会看到这样的东西: "-defaultlib:LIBC -defaultlib:OLDNAMES"。啊哈,没错,这就
    是保存在目标文件中的缺省库信息。我们的目标文件显然指定了两个缺省库,一个是单线程静态版标准库libc.lib(这与/ML选项相符);一个是oldnames.lib(它是为了兼容微软以前的C/C++开发系统,基本不用了,为了简化讨论可以忽略它)。另外,如果在源程序中用了

    /* xxxx代表实际的库文件名 */
    #pragma comment(lib,"xxxx")

    编译指示命令(compiler directive)指定要链接的库,那么这个信息也会被保存到目标文件的缺省库信息项中,且位于缺省标准库之前。如果有多个这样的命令,那么对应库名在目标文件中出现的顺序与它们在源程序中出现的顺序完全一致(且都在缺省标准库之前)。

        VC的链接器是link.exe,因为main.obj保存了缺省库信息,所以可以用

    link main.obj libc.lib

    或者

    link main.obj

    来生成可执行文件main.exe,这两个命令是等价的。但是如果你用

    link main.obj libcd.lib

    的话,链接器会给出一个警告: "warning LNK4098: defaultlib "LIBC" conflicts with use of other libs; use /NODEFAULTLIB:library",因为你显式指定的标准库版本与目标文件的缺省值不一致。通常来说,应该保证链接器合并的所有目标文件指定的缺省标准库版本一致,否则编译器一定会给出上面的警告,而LNK2005和LNK1169链接错误则有时会出现有时不会。那么这个有时到底是什么时候?呵呵,别着急,下面的一切正是为喜欢追根究底的你准备的。

        建一个源文件,就叫mylib.c,内容如下:

    /* mylib.c */
    #include <stdio.h>

    void foo(void)
    {
       printf("%s","I am from mylib!\n");
    }

    cl /c /MLd mylib.c

    命令编译,注意/MLd选项是指定libcd.lib为默认标准库。lib.exe是VC自带的用于将目标文件打包成程序库的命令,所以我们可以用

    lib /OUT:my.lib mylib.obj

    将mylib.obj打包成库,输出的库文件名是my.lib。接下来把main.c改成:

    /* main.c */
    void foo(void);

    int main()
    {
       foo();
       return 0;
    }

    cl /c main.c

    编译,然后用

    link main.obj my.lib

    进行链接。这个命令能够成功地生成main.exe而不会产生LNK2005和LNK1169链接错误,你仅仅是得到了一条警告信息:"warning LNK4098: defaultlib "LIBCD" conflicts with use of other libs; use /NODEFAULTLIB:library"。我们根据前文所述的扫描规则来分析一下链接器此时做了些啥(加一个/VERBOSE选项就可以看到详尽的链接过程,但要注意,几乎所有的C编译器都会在符号前加一个下划线后再输出,所以在目标文件和链接输出信息中看到的符号名都比在源程序中见到的多出一个'_',此点不可不察。)。

        一开始E、U、D都是空集。链接器首先扫描main.obj,把它的默认标准库libc.lib加入到输入文件列表末尾,它自己加入E集合,同时未解析的foo加入U,main加入D。接着扫描my.lib,因为这是个库,所以会拿当前U中的所有符号(当然现在就一个foo)与my.lib中的所有目标模块(当然也只有一个mylib.obj)依次匹配,看是否有模块定义了U中的符号。结果mylib.obj确实定义了foo,于是它加入到E,foo从U转移到D,未解析的printf加入到U,指定的默认标准库libcd.lib也加到输入文件列表末尾(在libc.lib之后)。不断地在my.lib库的各模块上进行迭代以匹配U中的符号,直到U、D都不再变化。很明显,现在就已经到达了这么一个不动点,所以接着扫描下一个输入文件,就是libc.lib。链接器发现libc.lib里的printf.obj里定义有printf,于是printf从U移到D,printf.obj加入到E,它定义的所有符号加入到D,它里头的未解析符号加入到U。如果链接时没有指定/ENTRY(程序入口点选项),那么链接器默认的入口点就是函数mainCRTStartup(GUI程序的默认入口点则是WinMainCRTStartup),它在crt0.obj中被定义,所以crt0.obj及它直接或间接引用的模块(比如malloc.obj、free.obj等)都被加入到E中,这些目标模块指定的默认库(只crt0init.obj指定了kernel32.lib)加到输入文件列表末尾,同时更新U和D。不断匹配libc.lib中各模块直至到达不动点,然后处理libcd.lib,但是它里面的所有目标模块都没有定义U中的任何一个符号,所以链接器略过它进入到最后一个输入文件kernel32.lib。事实上,U中已有和将要加入的未解析符号都可以在其中找到定义,那么当处理完kernel32.lib时,U必然为空,于是链接器合并E中的所有模块生成可执行文件。

        上文描述了虽然各目标模块指定了不同版本的缺省标准库但仍然链接成功的例子,接下来你将目睹因为这种不严谨而导致的悲惨失败。

        修改mylib.c成这个样子:

    #include <crtdbg.h>

    void foo(void)
    {
    // just a test , don't care memory leak
       _malloc_dbg( 1, _NORMAL_BLOCK, __FILE__, __LINE__ );
    }

    其中_malloc_dbg不是ANSI C的标准库函数,它是VC标准库提供的malloc的调试版,与相关函数配套能帮助开发者抓各种内存错误。使用它一定要定义_DEBUG宏,否则预处理器会把它自动转为malloc。继续用

    cl /c /MLd mylib.c
    lib /OUT:my.lib mylib.obj

    编译打包。当再次用

    link main.obj my.lib

    进行链接时,我们看到了什么?天哪,一堆的LNK2005加上个贵为"fatal error"的LNK1169垫底,当然还少不了那个LNK4098。链接器是不是疯了?不,你冤枉可怜的链接器了,我拍胸脯保证它可是一直在尽心尽责地照章办事。

        一开始E、U、D为空,链接器扫描main.obj,把libc.lib加到输入文件列表末尾,把main.obj加进E,把foo加进U,把main加进D。接着扫描my.lib,于是mylib.obj加入E,libcd.lib加到输入文件列表末尾,foo从U转移到D,_malloc_dbg加进U。然后扫描libc.lib,这时会发现libc.lib里任何一个目标模块都没有定义_malloc_dbg(它只在调试版的标准库中存在),所以不会有任何一个模块因为_malloc_dbg而加入E。但因为libc.lib中的crt0.obj定义了默认入口点函数mainCRTStartup,所以crt0.obj及它直接或间接引用的模块(比如malloc.obj、free.obj等)都被加入到E中,这些目标模块指定的默认库(只crt0init.obj指定了kernel32.lib)加到输入文件列表末尾,同时更新U和D。不断匹配libc.lib中各模块直至到达不动点后再处理libcd.lib,发现dbgheap.obj定义了_malloc_dbg,于是dbgheap.obj加入到E,它的未解析符号加入U,它定义的所有其它符号加入D,这时灾难便来了。之前malloc等符号已经在D中(随着libc.lib里的malloc.obj加入E而加入的),而dbgheap.obj及因它而引入的其它模块又定义了包括malloc在内的许多同名符号,导致了重定义冲突。所以链接器在处理完所有输入文件(是的,即使中途有重定义冲突它也会处理所有的文件以便生成一个完整的冲突列表)后只好报告: 这活儿没法儿干。

        现在我们该知道,链接器完全没有责任,责任在我们自己的身上。是我们粗心地把缺省标准库版本不一致的目标文件(main.obj)与程序库(my.lib)链接起来,引发了大灾难。解决办法很简单,要么用/MLd选项来重编译main.c;要么用/ML选项重编译mylib.c;再或者干脆在链接时用/NODEFAULTLIB:XXX选项忽略默认库XXX,但这种方法非常不保险(想想为什么?),所以不推荐。

        在上述例子中,我们拥有库my.lib的源代码(mylib.c),所以可以用不同的选项重新编译这些源代码并再次打包。可如果使用的是第三方的库,它并没有提供源代码,那么我们就只有改变自己程序的编译选项来适应这些库了。但是如何知道库中目标模块指定的默认库呢?其实VC提供的一个小工具便可以完成任务,这就是dumpbin.exe。运行下面这个命令

    dumpbin /DIRECTIVES my.lib

    然后在输出中找那些"Linker Directives"引导的信息,你一定会发现每一处这样的信息都会包含若干个类似"-defaultlib:XXXX"这样的字符串,其中XXXX便代表目标模块指定的缺省库名(注意,如果在编译时指定了/Zl选项,那么目标模块中将不会有defaultlib信息)。

        知道了第三方库指定的默认标准库,再用合适的选项编译我们的应用程序,就可以避免LNK2005和LNK1169链接错误。喜欢IDE的朋友,你一样可以到 "Project属性" -> "C/C++" -> "代码生成(code generation)" -> "运行时库(run-time library)" 项下设置应用程序的默认标准库版本,这与命令行选项的效果是一样的。

    September 02

    转载一文,近日太忙,日后再做整理

    VC++ 6.0 中如何使用 CRT 调试功能来检测内存泄漏

    作者:JerryZ

    下载例子源代码
      最近看了周星星 Blog 中的一篇文章:“VC++6.0中内存泄漏检测”,受益匪浅,便运行其例子代码想看看 Output 窗口中的输出结果,可惜怎么弄其输出都不是预期的东西,郁闷了半天,便到水坛里找到周星星,请求他指点一、二,然而未果。没有办法,最后我一头栽进 MSDN 库狂搜了一把,功夫不负有心人,我搜出很多有关这方面的资料,没过多久我便基本上就找到了答案......
      首先,检测内存泄漏的基本工具是调试器和 CRT 调试堆函数。为了使用调试堆函数,必须在要检测内存泄漏和调试的程序中添加下面的语句:

    #define _CRTDBG_MAP_ALLOC 
    #include<stdlib.h> 
    #include<crtdbg.h> 
    
    #include "debug_new.h" 

      MSDN 如是说:“必须保证上面声明的顺序,如果改变了顺序,可能不能正常工作。”至于这是为什么,我们不得而知。MS 的老大们经常这样故弄玄虚。
      针对非 MFC 程序,再加上周星星的头文件:debug_new.h,当然如果不加这一句,也能检测出内存泄漏,但是你无法确定在哪个源程序文件中发生泄漏。Output 输出只告诉你在 crtsdb.h 中的某个地方有内存泄漏。我测试时 REG_DEBUG_NEW 没有起作用。加不加这个宏都可以检测出发生内存分配泄漏的文件。
      其次,一旦添加了上面的声明,你就可以通过在程序中加入下面的代码来报告内存泄漏信息了:

          _CrtDumpMemoryLeaks(); 
      这就这么简单。我在周星星的例子代码中加入这些机关后,在 VC++ 调试会话(按 F5 调试运行) Output 窗口的 Debug 页便看到了预期的内存泄漏 dump。该 dump 形式如下:
    Detected memory leaks! 
    Dumping objects -> 
    c:\Program Files\...\include\crtdbg.h(552) : {45} normal block at 0x00441BA0, 2 bytes long. 
    Data: <AB> 41 42 
    c:\Program Files\...\include\crtdbg.h(552) : {44} normal block at 0x00441BD0, 33 bytes long. 
    Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD 
    c:\Program Files\...\include\crtdbg.h(552) : {43} normal block at 0x00441C20, 40 bytes long. 
    Data: < C > E8 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00 
    Object dump complete. 

    更具体的细节请参考本文附带的源代码文件。
      下面是我看过 MSDN 资料后,针对“如何使用 CRT 调试功能来检测内存泄漏?”的问题进行了一番编译和整理,希望对大家有用。如果你的英文很棒,那就不用往下看了,建议直接去读 MSDN 库中的技术原文。
      C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:“最大的长处也可能成为最大的弱点”,那么 C/C++ 应用程序正好印证了这句话。在 C/C++ 应用程序开发过程中,动态分配的内存处理不当是最常见的问题。其中,最难捉摸也最难检测的错误之一就是内存泄漏,即未能正确释放以前分配的内存的错误。偶尔发生的少量内存泄漏可能不会引起我们的注意,但泄漏大量内存的程序或泄漏日益增多的程序可能会表现出各种 各样的征兆:从性能不良(并且逐渐降低)到内存完全耗尽。更糟的是,泄漏的程序可能会用掉太多内存,导致另外一个程序垮掉,而使用户无从查找问题的真正根源。此外,即使无害的内存泄漏也可能殃及池鱼。
      幸运的是,Visual Studio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法。下面请和我一起分享收获——如何使用 CRT 调试功能来检测内存泄漏?

    1. 如何启用内存泄漏检测机制?
    2. 解释内存块类型
    3. 如何在内存分配序号处设置断点?
    4. 如何比较内存状态?
    5. 结论

    如何启用内存泄漏检测机制
      VC++ IDE 的默认状态是没有启用内存泄漏检测机制的,也就是说即使某段代码有内存泄漏,调试会话的 Output 窗口的 Debug 页不会输出有关内存泄漏信息。你必须设定两个最基本的机关来启用内存泄漏检测机制。
    一是使用调试堆函数:

    #define _CRTDBG_MAP_ALLOC 
    #include<stdlib.h> 
    #include<crtdbg.h> 

    注意:#include 语句的顺序。如果更改此顺序,所使用的函数可能无法正确工作。
      通过包含 crtdbg.h 头文件,可以将 malloc 和 free 函数映射到其“调试”版本 _malloc_dbg 和 _free_dbg,这些函数会跟踪内存分配和释放。此映射只在调试(Debug)版本(也就是要定义 _DEBUG)中有效。发行版本(Release)使用普通的 malloc 和 free 函数。
      #define 语句将 CRT 堆函数的基础版本映射到对应的“调试”版本。该语句不是必须的,但如果没有该语句,那么有关内存泄漏的信息会不全。
    二是在需要检测内存泄漏的地方添加下面这条语句来输出内存泄漏信息:

    _CrtDumpMemoryLeaks();
      当在调试器下运行程序时,_CrtDumpMemoryLeaks 将在 Output 窗口的 Debug 页中显示内存泄漏信息。比如:
    Detected memory leaks!
    Dumping objects ->
    C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.
    Data: <AB> 41 42 
    c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {44} normal block at 0x00441BD0, 33 bytes long.
    Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD 
    c:\program files\microsoft visual studio\vc98\include\crtdbg.h(552) : {43} normal block at 0x00441C20, 40 bytes long.
    Data: < C > 08 02 43 00 16 00 00 00 00 00 00 00 00 00 00 00 
    Object dump complete.

    如果不使用 #define _CRTDBG_MAP_ALLOC 语句,内存泄漏的输出是这样的:

    Detected memory leaks!
    Dumping objects ->
    {45} normal block at 0x00441BA0, 2 bytes long.
    Data: <AB> 41 42 
    {44} normal block at 0x00441BD0, 33 bytes long.
    Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD 
    {43} normal block at 0x00441C20, 40 bytes long.
    Data: < C > C0 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00 
    Object dump complete.
      根据这段输出信息,你无法知道在哪个源程序文件里发生了内存泄漏。下面我们来研究一下输出信息的格式。第一行和第二行没有什么可说的,从第三行开始:
    xx}:花括弧内的数字是内存分配序号,本文例子中是 {45},{44},{43};
    block:内存块的类型,常用的有三种:normal(普通)、client(客户端)或 CRT(运行时);本文例子中是:normal block; 
    用十六进制格式表示的内存位置,如:at 0x00441BA0 等;
    以字节为单位表示的内存块的大小,如:32 bytes long; 
    前 16 字节的内容(也是用十六进制格式表示),如:Data: <AB> 41 42 等;

      仔细观察不难发现,如果定义了 _CRTDBG_MAP_ALLOC ,那么在内存分配序号前面还会显示在其中分配泄漏内存的文件名,以及文件名后括号中的数字表示发生泄漏的代码行号,比如:

    C:\Temp\memleak\memleak.cpp(15) 
      双击 Output 窗口中此文件名所在的输出行,便可跳到源程序文件分配该内存的代码行(也可以选中该行,然后按 F4,效果一样) ,这样一来我们就很容易定位内存泄漏是在哪里发生的了,因此,_CRTDBG_MAP_ALLOC 的作用显而易见。
    使用 _CrtSetDbgFlag
      如果程序只有一个出口,那么调用 _CrtDumpMemoryLeaks 的位置是很容易选择的。但是,如果程序可能会在多个地方退出该怎么办呢?在每一个可能的出口处调用 _CrtDumpMemoryLeaks 肯定是不可取的,那么这时可以在程序开始处包含下面的调用:
    _CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

      这条语句无论程序在什么地方退出都会自动调用 _CrtDumpMemoryLeaks。注意:这里必须同时设置两个位域标志:_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。
    设置 CRT 报告模式
      默认情况下,_CrtDumpMemoryLeaks 将内存泄漏信息 dump 到 Output 窗口的 Debug 页, 如果你想将这个输出定向到别的地方,可以使用 _CrtSetReportMode 进行重置。如果你使用某个库,它可能将输出定向到另一位置。此时,只要使用以下语句将输出位置设回 Output 窗口即可:

    _CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );

    有关使用 _CrtSetReportMode 的详细信息,请参考 MSDN 库关于 _CrtSetReportMode 的描述。
    解释内存块类型
      前面已经说过,内存泄漏报告中把每一块泄漏的内存分为 normal(普通块)、client(客户端块)和 CRT 块。事实上,需要留心和注意的也就是 normal 和 client,即普通块和客户端块。

    • normal block(普通块):这是由你的程序分配的内存。
    • client block(客户块):这是一种特殊类型的内存块,专门用于 MFC 程序中需要析构函数的对象。MFC new 操作符视具体情况既可以为所创建的对象建立普通块,也可以为之建立客户块。
    • CRT block(CRT 块):是由 C RunTime Library 供自己使用而分配的内存块。由 CRT 库自己来管理这些内存的分配与释放,我们一般不会在内存泄漏报告中发现 CRT 内存泄漏,除非程序发生了严重的错误(例如 CRT 库崩溃)。

    除了上述的类型外,还有下面这两种类型的内存块,它们不会出现在内存泄漏报告中:

    • free block(空闲块):已经被释放(free)的内存块。
    • Ignore block(忽略块):这是程序员显式声明过不要在内存泄漏报告中出现的内存块。

    如何在内存分配序号处设置断点?
      在内存泄漏报告中,的文件名和行号可告诉分配泄漏的内存的代码位置,但仅仅依赖这些信息来了解完整的泄漏原因是不够的。因为一个程序在运行时,一段分配内存的代码可能会被调用很多次,只要有一次调用后没有释放内存就会导致内存泄漏。为了确定是哪些内存没有被释放,不仅要知道泄漏的内存是在哪里分配的,还要知道泄漏产生的条件。这时内存分配序号就显得特别有用——这个序号就是文件名和行号之后的花括弧里的那个数字。
      例如,在本文例子代码的输出信息中,“45”是内存分配序号,意思是泄漏的内存是你程序中分配的第四十五个内存块:

    Detected memory leaks!
    Dumping objects ->
    C:\Temp\memleak\memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.
    Data: <AB> 41 42 
    ......
    Object dump complete. 

      CRT 库对程序运行期间分配的所有内存块进行计数,包括由 CRT 库自己分配的内存和其它库(如 MFC)分配的内存。因此,分配序号为 N 的对象即为程序中分配的第 N 个对象,但不一定是代码分配的第 N 个对象。(大多数情况下并非如此。)
      这样的话,你便可以利用分配序号在分配内存的位置设置一个断点。方法是在程序起始附近设置一个位置断点。当程序在该点中断时,可以从 QuickWatch(快速监视)对话框或 Watch(监视)窗口设置一个内存分配断点:
      例如,在 Watch 窗口中,在 Name 栏键入下面的表达式:

    _crtBreakAlloc

    如果要使用 CRT 库的多线程 DLL 版本(/MD 选项),那么必须包含上下文操作符,像这样:

    {,,msvcrtd.dll}_crtBreakAlloc

      现在按下回车键,调试器将计算该值并把结果放入 Value 栏。如果没有在内存分配点设置任何断点,该值将为 –1。
      用你想要在其位置中断的内存分配的分配序号替换 Value 栏中的值。例如输入 45。这样就会在分配序号为 45 的地方中断。
      在所感兴趣的内存分配处设置断点后,可以继续调试。这时,运行程序时一定要小心,要保证内存块分配的顺序不会改变。当程序在指定的内存分配处中断时,可以查看 Call Stack(调用堆栈)窗口和其它调试器信息以确定分配内存时的情况。如果必要,可以从该点继续执行程序,以查看对象发生了什么情况,或许可以确定未正确释放对象的原因。
      尽管通常在调试器中设置内存分配断点更方便,但如果愿意,也可在代码中设置这些断点。为了在代码中设置一个内存分配断点,可以增加这样一行(对于第四十五个内存分配):

    _crtBreakAlloc = 45;

    你还可以使用有相同效果的 _CrtSetBreakAlloc 函数:

    _CrtSetBreakAlloc(45);

    如何比较内存状态?
      定位内存泄漏的另一个方法就是在关键点获取应用程序内存状态的快照。CRT 库提供了一个结构类型 _CrtMemState。你可以用它来存储内存状态的快照:

    _CrtMemState s1, s2, s3;

      若要获取给定点的内存状态快照,可以向 _CrtMemCheckpoint 函数传递一个 _CrtMemState 结构。该函数用当前内存状态的快照填充此结构:

    _CrtMemCheckpoint( &s1 );

      通过向 _CrtMemDumpStatistics 函数传递 _CrtMemState 结构,可以在任意地方 dump 该结构的内容:

    _CrtMemDumpStatistics( &s1 );

    该函数输出如下格式的 dump 内存分配信息:

    0 bytes in 0 Free Blocks.
    75 bytes in 3 Normal Blocks.
    5037 bytes in 41 CRT Blocks.
    0 bytes in 0 Ignore Blocks.
    0 bytes in 0 Client Blocks.
    Largest number used: 5308 bytes.
    Total allocations: 7559 bytes.

      若要确定某段代码中是否发生了内存泄漏,可以通过获取该段代码之前和之后的内存状态快照,然后使用 _CrtMemDifference 比较这两个状态:

    _CrtMemCheckpoint( &s1 );// 获取第一个内存状态快照
    
    // 在这里进行内存分配
    
    _CrtMemCheckpoint( &s2 );// 获取第二个内存状态快照
    
    // 比较两个内存快照的差异
    if ( _CrtMemDifference( &s3, &s1, &s2) )
         _CrtMemDumpStatistics( &s3 );// dump 差异结果

      顾名思义,_CrtMemDifference 比较两个内存状态(前两个参数),生成这两个状态之间差异的结果(第三个参数)。在程序的开始和结尾放置 _CrtMemCheckpoint 调用,并使用 _CrtMemDifference 比较结果,是检查内存泄漏的另一种方法。如果检测到泄漏,则可以使用 _CrtMemCheckpoint 调用通过二进制搜索技术来分割程序和定位泄漏。
    结论
      尽管 VC ++ 具有一套专门调试 MFC 应用程序的机制,但本文上述讨论的内存分配很简单,没有涉及到 MFC 对象,所以这些内容同样也适用于 MFC 程序。在 MSDN 库中可以找到很多有关 VC++ 调试方面的资料,如果你能善用 MSDN 库,相信用不了多少时间你就有可能成为调试高手。
    本人水平不高,谬误在所难免,请大家拍砖,不要客气。顺祝大家圣诞快乐!
    JerryZ 于 2004 年平安夜,

    August 21

    xmlprovi.dll引发svchost.exe启动过程中停止响应

    前几天更新了一个MSXML相关的更新包,包号记不得了,更新完成后,电脑启动时发生奇怪现象:

    原本启动时要加载很多系统服务的,因此吃掉不少内存,硬盘也会闪较长时间。更新后,启动时硬盘闪的时间明显减短,内存消耗降低100M左右。

    更新后无法打开浏览器,资源管理器,许多应用程序工作不正常。

    关闭以 -k netsvcs为参数启动的svchost.exe进程后系统继续启动,浏览器可以使用,但许多系统服务都被相应关闭,如DHCPClient被关闭,导致无法自动分配ip地址。

    因为启动时无法打开VS调试进程,只能用360安全卫士查看,发现svchost.exe -k netsvcs进程加载的dll最后一项为c:\windows\system32\xmlprovi.dll,此后一直保持不变,估计是这个dll出了问题,用services.msc将相关的Network Provisioning via XML Class服务禁用后,一切恢复正常。

    XFCE4 on Cygwin!

    费了老大半天劲在Cygwin上装了XFCE4,其实原因是因为Gnome和KDE实在装不上去了,好像很多项目对Cygwin的移植工作都停止了,不知道是不是因为兼容性问题,sigh~感觉Cygwin的想法挺好的,速度还比VM快,希望能好好的走下去。

    Cygwin Port of XFCE4

    August 15

    try catch throw惹得祸

    今天在写乌龙学院代码的时候遇见了一件奇特的事情,Debug的时候不出错,Release却每次都出错,找了好半天没有发现问题所在,出错的地方指出一个本来不应该为空的指针为0,结果调用这个指针指向的对象的函数时出错。

    经过仔细跟踪研究,终于发现错误的产生是由几个因素共同作用产生的:自己粗心+Debug和Release版返回值不同+try/catch/thow隐藏了编译报错。!!-_-

     

    在创建这个指针所指向的对象之前有个这样的逻辑判断:

    if(!function())return false; //这里如果失败就返回了,所以不会创建pObj指向的对象

    ...

    pObj = new CClass();//创建新的对象

     

    跟踪发现function函数在debug和release方式下返回的值不一样,debug返回了不为0的值,release却是0!!进入function看看。

    bool funtion()
    {
        try
        {
            DoSomeThing(); //!!!!!!居然没有写返回值

        }
        catch(...)

        {

            throw; //隐藏了编译报错

        }
    }

    看到红色字标出的地方了吗?没有返回值!这样的错误居然出现在我写的代码里,真是惭愧啊。在没有返回值的时候,天知道调用函数得到的结果会是什么,这就导致了debug和release版返回值不同的结果。但是如果没有返回值,编译器不是应该报错么?第二个红字标的地方的throw让编译器把一个错误变成了警告!

    最后改成这样,不闹“鬼”了!

    bool funtion()
    {
        try
        {
            return DoSomeThing();

        }
        catch(...)

        {

            return false;

        }
    }

    August 12

    该减肥了!

    最近身体不是很好,老是觉得肚子胀,还爱出虚汗,昨天逛超市称了一下体重,哦,146了,真的已经胖的不行了!对自己做了许多动员工作,下了很大的决心,终于今天去买了个篮球,跑到复旦南区的球场锻炼身体。打了不到一个小时就已经累的快走不动了,浑身上下都是汗,好久没有过这种大汗淋漓的感觉了,不过感觉还是很棒的,似乎瞬间就让身体轻松了不少,嗯,以后有时间要多出来跑跑,呵呵。回到家洗了澡,还在不停地流汗,舒服!抱了个大西瓜回家,晚上有的爽啦,希望每天减二两的目标达成,这样两个月就能减下去12斤了。丹丹的办法更绝,每天让我少吃二两不减消耗量,呜呜。。。
    August 05

    1.5.2TGE中文输入完美解决方案_By_BigbearZhu

    前几天在网上遇到一个叫Andy的朋友,说他在TGE 1.5.2下中文输入遇到新的问题,在控制台中可以正常输入中文,一旦进入Mission就不能输入了。我已经好长时间没有摸过TGE了,当时改过的版本会不会有这个问题已经不记得了,Andy的热情邀请无法拒绝,于是我决定帮他看看到底遇到了什么问题。

    我从Andy那里拿到了他改动过的文件:

    platformWin32\winWindow.cc

    platform\event.h

    gui\core\guiControl.h

    gui\core\guiControl.cc

    gui\core\guiCanvas.cc

    gui\controls\guiTextEditCtrl.h

    gui\controls\guiTextEditCtrl.cc

    这7个文件可以正常编译通过,并且是按照我的“TGE中文输入的问题解决方案2_By_BigbearZhu.doc”进行的改动。进入TGE主界面后,打开控制台界面,中文输入正常,进入Mission,再打开控制台,确实无法再进行中文输入了,输入法栏显示“输入法无效”。

    clip_image002

    为什么会出现这样的情况呢?我们来看看winWindow.cc这个文件第346行:

    //winWindow.cc(line 346)

    void Platform::enableKeyboardTranslation(void)

    {

    #ifdef UNICODE

    // Con::printf("translating...");

    ImmAssociateContext( winState.appWindow, winState.imeHandle );

    #endif

    }

    //--------------------------------------

    void Platform::disableKeyboardTranslation(void)

    {

    #ifdef UNICODE

    // Con::printf("not translating...");

    ImmAssociateContext( winState.appWindow, NULL );

    #endif

    }

    这是TGE进行打开或者关闭输入法的两个函数,在什么地方用到它们了呢?这里:

    //guiTextEditCtrl.cc(line 1009)

    void GuiTextEditCtrl::setFirstResponder()

    {

    Parent::setFirstResponder();

    Platform::enableKeyboardTranslation();

    }

    void GuiTextEditCtrl::onLoseFirstResponder()

    {

    Platform::disableKeyboardTranslation();

    }

    这说明当EditCtrl被激活或者失去焦点的时候,会执行操作系统函数来打开或者关闭输入法,这是正确的,如果在玩动作游戏的时候忽然因为不小心按到了ctrl+space键导致输入法打开,那么很有可能会干扰到游戏的正常进行。

    那么winState.imeHandle又是什么呢?全局搜索“winState.imeHandle = ”

    //platformWin32\winInput.cc(line 358)

    void Input::activate()

    {

    #ifdef UNICODE

    winState.imeHandle = ImmGetContext( winState.appWindow ); //这里出错了,如果在Platform::disableKeyboardTranslation之后执行这句话,那么将永远丢失IME的Handle,故再也无法使用输入法了,hoho!

    ImmReleaseContext( winState.appWindow, winState.imeHandle );

    }

    出现问题的地方我用绿色文字注释了,考虑这样一个代码执行顺序:

    GuiTextEditCtrl::onLoseFirstResponder();

    Platform::disableKeyboardTranslation();

    ImmAssociateContext( winState.appWindow, NULL );

    Input::activate();

    winState.imeHandle = ImmGetContext( winState.appWindow );

    //此时肯定只能得到NULL!!

    GuiTextEditCtrl::setFirstResponder();

    Platform::enableKeyboardTranslation();

    ImmAssociateContext( winState.appWindow, winState.imeHandle );

    这种执行序列是经常可能发生的,比如先关闭控制台后再进入Mission!这也是我们遇到这样问题的根源所在。

    于是我们不能让winState.imeHandle变成NULL!

    我们将Input::activate()函数里#ifdef后的两行改成这样:

    if (winState.imeHandle == NULL) //所以,我们需要只在初始化程序的时候才尝试记录这个Handle,如果已经有了,就不记录了

    {

    winState.imeHandle = ImmGetContext( winState.appWindow );

    ImmReleaseContext( winState.appWindow, winState.imeHandle );

    }

    然后将GuiControl::setFirstResponder函数里最后添加这一行:

    Platform::disableKeyboardTranslation(); //默认情况下,我们需要禁止IME输入,不然一开始可能没有EditControl,但是IME还是可用的

    重新编译,执行,哈哈,进入Mission后也可以用IME了!

    我们现在相对于原始代码改动过的文件是这8个:

    platformWin32\winWindow.cc

    platformWin32\winInput.cc

    platform\event.h

    gui\core\guiControl.h

    gui\core\guiControl.cc

    gui\core\guiCanvas.cc

    gui\controls\guiTextEditCtrl.h

    gui\controls\guiTextEditCtrl.cc

    我把更新后的代码给了Andy,他又给我提了个建议,能不能在进入Mission后按u弹出聊天框的时候不要显示一个u字母在聊天框里,我想了想,这个问题挺麻烦的,引擎在收到KeyDown事件时做了操作,弹出了一个EditCtrl,然后在收到Char事件时又将这个事件传给了EditCtrl处理。这两件事是相对独立的,EditCtrl并不知道自己收到的Char是不是刚才被引擎处理过KeyDown事件。如果是这样的话,那么就需要对引擎底层消息处理进行些改动了。我在使劲想了很久之后暂时没有想到有什么情况是需要在处理了KeyDown事件后还必须处理Char事件,所以我决定让引擎处理过KeyDown事件的Char事件总是被忽略。

    我们来看game\ main.cc第584行:

    void DemoGame::processInputEvent(InputEvent *event)

    {

    PROFILE_START(ProcessInputEvent);

    if (!ActionMap::handleEventGlobal(event)) //这里处理了Keydown事件

    {

    // Other input consumers here...

    if (!(Canvas && Canvas->processInputEvent(event)))//这里处理了Char事件

    ActionMap::handleEvent(event);

    }

    PROFILE_END();

    }

    改成这样:

    bool DemoGame::processInputEvent(InputEvent *event)

    {

    PROFILE_START(ProcessInputEvent);

    if (!ActionMap::handleEventGlobal(event))

    {

    // Other input consumers here...

    if (!(Canvas && Canvas->processInputEvent(event)))

    ActionMap::handleEvent(event);

    PROFILE_END();

    return false; //告诉调用的函数,这个事件被其他控件处理了

    }

    PROFILE_END();

    return true; //告诉调用的函数,这个事件被引擎处理了

    }

    然后我们在调用DemoGame::processInputEvent的地方这样改:

    platform\gameInterface.cc第62行

    static InputEvent s_lastKeyDownEvent; //add by BigbearZhu

    switch(event->type)

    {

    case PacketReceiveEventType:

    processPacketReceiveEvent((PacketReceiveEvent *) event);

    break;

    case MouseMoveEventType:

    processMouseMoveEvent((MouseMoveEvent *) event);

    break;

    case InputEventType:

    if (((InputEvent*)event)->action == SI_CHAR && ((InputEvent*)event)->ascii == s_lastKeyDownEvent.ascii)

    {

    s_lastKeyDownEvent.action = 0; //clear last keydown record

    s_lastKeyDownEvent.ascii = 0;

    break; //then me ignore this SI_CHAR event

    }

    if(processInputEvent((InputEvent *) event))

    {

    if (((InputEvent*)event)->action == SI_MAKE && ((InputEvent*)event)->ascii > 0) //we need to record this event

    {

    s_lastKeyDownEvent = *(InputEvent*)event;

    }

    }

    这样我们就把上次执行的KeyDown事件记住了,并且可以在执行下一个Char事件的时候用作比对,一旦相同就忽略这个Char事件。

    另外,我们改动了DemoGame::processInputEvent的返回类型,而这个函数又继承自GameInterface,所以不能忘了要把它们的声明和定义都改成返回bool型。

    于是,现在我们改动过的文件变成了12个:

    platformWin32\winWindow.cc

    platformWin32\winInput.cc

    platform\event.h

    gui\core\guiControl.h

    gui\core\guiControl.cc

    gui\core\guiCanvas.cc

    gui\controls\guiTextEditCtrl.h

    gui\controls\guiTextEditCtrl.cc

    +

    game\demoGame.h

    game\ main.cc

    platform\gameInterface.h

    platform\gameInterface.cc

    好了,现在的中文输入法可以正常使用了。:)

    1.5.2TGE中文输入完美解决方案_By_BigbearZhu.rar(选择“TGE中文输入”目录并下载)

    TGE中文输入解决方案2_By_BigbearZhu.rar(选择“TGE中文输入”目录并下载)

    July 31

    向Microsoft学习!

    寻寻觅觅,终得一解,下面的都是精华,修炼必备!做为任何一个IT公司的开发,测试,管理人员想要帮助公司走向成功都有必要认真学习这些...

    Recommended Reading

    The resources below come highly recommended from select managers across Microsoft to help you to become more familiar with our two primary technical positions and broaden your skill base overall.

    SDE:

    • Essential .Net, Volume I: The Common Language Runtime. Box, Don. Addison-Wesley Professional, 2003.

    • The Mythical Man-Month: Essays on Software Engineering. Brooks, Fredrick. Addison-Wesley Professional, 1995.

    • Introduction to Algorithms. Cormen, T.H., Leiserson, C.E., Reivert, R.L., Stein, Cliff, eds. McGraw-Hill, 1990.

    • Writing Secure Code. Howard, Michael, LeBlanc, David, eds. Microsoft Press, 2001.

    • Code Complete: A Practical Handbook of Software Construction. McConnell, Steve. Microsoft Press, 1993.

    • Writing Solid Code: Microsoft's Techniques for Developing Bug-Free C Programs. Maguire, Steve. Microsoft Press, 1993.

    • Rapid Development: Taming Wild Software Schedules. McConnell, Steve. Microsoft Press, 1996.

    • Modern Operating Systems. 2nd ed. Tanenbaum, Andrew. Prentice Hall, 2001.

    SDET:

    You'll find helpful info in any or all of the above references plus:

    • Lessons Learned in Software Testing. Kaner, Cem, Bach, James, and Pettichor, Bret, eds. John Wiley & Sons, 2002.

    • Testing Computer Software (2nd Ed.). Kaner, Cem, Falk, Jack, & Nguyen, Hung Quoc, eds. International Thomson Computer Press, 1993.

    • The Art of Software Testing. Myers, Glenford. John Wiley and Sons, 1979.

    • Software Testing. Patton, Ron. SAMS Publishing, 2000.

    • Building Secure Software: How to Avoid Security Problems the Right Way. Viega, John, McGraw, Gary, eds. Addison-Wesley, 2001.

    Links to Online Testing Resources:

    http://www.informationweek.com/756/testers.htm

    http://www.qacity.com/General+Testing/Techniques/Links/Links.htm

    http://www.softwareqatest.com/qatfaq2.html#FAQ2_6

    http://www.testingeducation.org/articles/domain_testing_cseet.rtf

    http://www.testingeducation.org/articles/scenario_intro_ver4.doc

    http://www.testingeducation.org/coursenotes/kaner_cem/ac_200108_blackboxtesting/

    http://www.testingcraft.com/techniques.html

     

    Program Manager

    The resources below come highly recommended from select PMs across Microsoft to help you to become more familiar with the PM position and to broaden your skills overall.

    • Programming Pearls: Second Edition. Bentley, Jon. Addison-Wesley, Inc., 2000.

    • Design Patterns. Elements of Reusable Object-Oriented Software. Gamma, E., Helm, R., Johnson, R., Vlissides, J., eds. Addison-Wesley, 1994.

    • The Practice of Programming. Kernighan, Brian, Pike, Rob, eds. Addison-Wesley, 1999.

    See what our VP of Windows and Windows Live has to say about the role of Program Managers in our company in his blog at:

    http://blogs.msdn.com/techtalk/archive/2005/12/16/504872.aspx

    July 27

    有什么好玩的东东呢?

    今天好不容易把P2PVod的切换视频功能完成了,呵呵。It's time to play now~!可是到新浪游戏上找单机游戏介绍,把最近的单机游戏列表统统扫了一遍,居然发现没有什么可以让我提得起兴趣来的!rpg的玩起来时间太长,fps的太滥了,action的总是觉得没有街机模拟器爽,即时战略...花时间还不容易上手......天哪,我还可以玩点什么呢?感觉赛车类的倒是还让我觉得舒服一些,或者微软模拟飞行X,下来看看吧~

    July 26

    Windows Live Writer Test!

    本来想装一个英文版的,可是装了之后死活登不上去,说用户名密码不正确,只好装中文版的了,呵呵。

    (Hyper Link Insertion Test)Fighting, Bigbearzhu

    (Image Insertion Test)image

    漂亮的乌龙学院T-Shirt Logo,可以直接用qq截屏粘帖,8错8错!

    (Table Insertion Test)

    1

    2

    3

    4

    嗯,单元格对齐方式统一更改有些麻烦,选了多个格子就不能在右键菜单里显示单元格属性了,还好在顶上格式菜单里可以更改对齐方式。

    (Map Insertion Test???)

    想法不错,信息还不全,不支持中文,放大最大也只能看到环线的公路,呵呵。

    (Tag Insertion Test??????)

    Technorati 标记:

    真的是不知道这个是干什么用的,可能是我太老土了,哈哈哈~

    (Plugins Insertion Test...give up)

    追加一句,每次发布的时候都会自动更新,不会重新发布一条日志,这个真的很人性化,当时在改了一点格式之后考虑要不要再点一次发布按钮时花了不少时间,担心又发一条上去,看来担心是不必要的。:)草稿功能也很强大,离线也可以编辑,还可以用web方式预览,其实是把space里的样式先读取到了本地然后加入新的日志做的预览,感觉和浏览器里的完全一样,但是速度没话说!

    一条很有意思的评论...Xbox360 Falcon平台相关

     今天在网上找Microsoft Xbox360的新Falcon平台什么时候会出,最后所有的信息来源地都是一篇Dean Takahashi在7月9号写的文章,文章原文在这里:http://blogs.mercurynews.com/aei/2007/07/microsofts_next_move_code-name_falcon.html
    大致的意思是Microsoft的Falcon平台“希望”会在今年秋天发布,也就是说是作者的个人估计。另外,还讲了许多为啥要换65nm的处理器,有多少好处。感觉实际意义还不是很大,毕竟微软没有其他消息了,555...不知道啥时候才能入手啊。
     
    不过很有意思的是文章下面的一条评论: 
     
    注意第三句话,sigh,真的是倒出了无数米饭的心声啊,等了好久了...Oh...Just wait until we do 65nm, 2 years after launch...
    July 18

    输了,输得彻彻底底

    又看了一场国足的比赛,到80分钟的时候,我就希望比赛马上结束了。真的是太气愤了,不是气愤技术不好,也不觉得球员没有尽力,气愤的是,从拥有十几亿人的中国挑出来的11个男人里居然都不能在场上撑到最后!配合再好有什么用?脚法再精确有什么用?马拉松前9公里都跑第一,最后一公里跑不动了的人能拿到冠军么?评论员还在一个劲的说中国要进球要怎么样,我觉得在现在这样的情况下,中国队最需要的是踢足球基本的体力和保持状态完成比赛的意志力!看到伤停补时韩鹏倒下的时候,说实话我一点不想骂,真的很心痛,人都累成这样了还让他在场上。中国队失败的关键原因就是体力,在目前的状况下选球员,应该首先选能够挺得住90分钟高强度运动的队员,如果这一点都不能过,那就去踢室内足球吧。我就不信全中国就选不出11个体力能挺得住英超的。尼日利亚脚法好吗?为什么能拿到那样的成绩?当别人累的不行了的时候,他们的球员还能跑得像风一样,这就是差距!这就是问题的关键点!今天的所有球员和教练都是好样的,不过我希望他们回国以后不要再想着自己怎么为国争光,他们实力确实无法达到那个水平,我希望他们从心里想一想如何让下一代的球员在球场上至少能让对手知道:中国人是能坚持到最后的!
    July 11

    真快!离上次发文都快一年了!

    整天都还觉得本本才买了不久,原来都快一岁了。
    今天发现自己以前用map时的bug,又一次给自己敲了下警钟。就是这些小小的让我忽视了的问题,才让我没能逃过谢老大的眼睛,自己code的质量确实还不能达到微软所要求的质量,以后一定不能放过这些细节问题,一点一点的把丢掉的珍贵的东西捡起来。
     
    转一篇别人的帖子,象所有人学习!为以后的征途准备!
     
    删除所有偶数项,并打印出删除的项
    1. vector/queue

    正确方法1:
    void erase(vector<int> &v)
    {
        
    for(vector<int>::iterator vi=v.begin();vi!=v.end();)
        {
            
    if(*vi % 2 == 0)
            {
                cout 
    << "Erasing " << *vi << endl;
                vi 
    = v.erase(vi);
            }
            
    else ++vi;
        }
    }

    正确方法2:
    void erase2(vector<int> &v)
    {
        
    for(vector<int>::reverse_iterator ri=v.rbegin();ri!=v.rend();)
        {
            
    if(*ri % 2 == 0)
            {
                cout 
    << "Erasing " << *ri << endl;
                v.erase((
    ++ri).base());    //erase()函数期待的是正向iterator,故而这里要调
                                                        //用base()函数将逆向iterator转换为正向的
            }
            
    else ++ri;
        }
    }



    2.map/list
    正确方法

    void erase(map<int,int> &m)
    {
        
    for(map<int,int>::iterator mi=m.begin();mi!=m.end();)
        {
            
    if(mi->second % 2 == 0)
            {
                cout 
    << "Erasing " << mi->second << endl;
                m.erase(mi
    ++);
            }
            
    else ++mi;
        }
    }
    August 03

    今天查什么是Top Level Design时找到的

    What is Software Design?

    by

    Jack W. Reeves
    ©C++ Journal - 1992

    Object oriented techniques, and C++ in particular, seem to be taking the software world by storm. Numerous articles and books have appeared describing how to apply the new techniques. In general, the questions of whether O-O techniques are just hype have been replaced by questions of how to get the benefits with the least amount of pain. Object oriented techniques have been around for some time, but this exploding popularity seems a bit unusual. Why the sudden interest? All kinds of explanations have been offered. In truth, there is probably no single reason. Probably, a combination of factors has finally reached critical mass and things are taking off. Nevertheless, it seems that C++ itself is a major factor in this latest phase of the software revolution. Again, there are probably a number of reasons why, but I want to suggest an answer from a slightly different perspective: C++ has become popular because it makes it easier to design software and program at the same time.

    If that comment seems a bit unusual, it is deliberate. What I want to do in this article is take a look at the relationship between programming and software design. For almost 10 years I have felt that the software industry collectively misses a subtle point about the difference between developing a software design and what a software design really is. I think there is a profound lesson in the growing popularity of C++ about what we can do to become better software engineers, if only we see it. This lesson is that programming is not about building software; programming is about designing software.

    Years ago I was attending a seminar where the question came up of whether software development is an engineering discipline or not. While I do not remember the resulting discussion, I do remember how it catalyzed my own thinking that the software industry has created some false parallels with hardware engineering while missing some perfectly valid parallels. In essence, I concluded that we are not software engineers because we do not realize what a software design really is. I am even more convinced of that today.

    The final goal of any engineering activity is the some type of documentation. When a design effort is complete, the design documentation is turned over to the manufacturing team. This is a completely different group with completely different skills from the design team. If the design documents truly represent a complete design, the manufacturing team can proceed to build the product. In fact, they can proceed to build lots of the product, all without any further intervention of the designers. After reviewing the software development life cycle as I understood it, I concluded that the only software documentation that actually seems to satisfy the criteria of an engineering design is the source code listings.

    There are probably enough arguments both for and against this premise to fill numerous articles. This article assumes that final source code is the real software design and then examines some of the consequences of that assumption. I may not be able to prove that this point of view is correct, but I hope to shown that it does explain some of the observed facts of the software industry, including the popularity of C++.

    There is one consequence of considering code as software design that completely overwhelms all others. It is so important and so obvious that it is a total blind spot for most software organizations. This is the fact that software is cheap to build. It does not qualify as inexpensive; it is so cheap it is almost free. If source code is a software design, then actually building software is done by compilers and linkers. We often refer to the process of compiling and linking a complete software system as "doing a build". The capital investment in software construction equipment is low -- all it really takes is a computer, an editor, a compiler, and a linker. Once a build environment is available, then actually doing a software build just takes a little time. Compiling a 50,000 line C++ program may seem to take forever, but how long would it take to build a hardware system that had a design of the same complexity as 50,000 lines of C++.

    Another consequence of considering source code as software design is the fact that a software design is relatively easy to create, at least in the mechanical sense. Writing (i.e., designing) a typical software module of 50 to 100 lines of code is usually only a couple of day's effort (getting it fully debugged is another story, but more on that later). It is tempting to ask if there is any other engineering discipline that can produce designs of such complexity as software in such a short time, but first we have to figure out how to measure and compare complexity. Nevertheless, it is obvious that software designs get very large rather quickly.

    Given that software designs are relatively easy to turn out, and essentially free to build, an unsurprising revelation is that software designs tend to be incredibly large and complex. This may seem obvious but the magnitude of the problem is often ignored. School projects often end up being several thousand lines of code. There are software products with 10,000 line designs that are given away by their designers. We have long since passed the point where simple software is of much interest. Typical commercial software products have designs that consist of hundreds of thousands of lines. Many software designs run into the millions. Additionally, software designs are almost always constantly evolving. While the current design may only be a few thousand lines of code, many times that may actually have been written over the life of the product.

    While there are certainly examples of hardware designs that are arguably as complex as software designs, note two facts about modern hardware. One, complex hardware engineering efforts are not always as free of bugs as software critics would have us believe. Major microprocessors have been shipped with errors in their logic, bridges collapsed, dams broken, airliners fallen out of the sky, and thousands of automobiles and other consumer products have been recalled - all within recent memory and all the result of design errors. Second, complex hardware designs have correspondingly complex and expensive build phases. As a result, the ability to manufacture such systems limits the number of companies that produce truly complex hardware designs. No such limitations exist for software. There are hundreds of software organizations, and thousands of very complex software systems in existence. Both the number and the complexity are growing daily. This means that the software industry is not likely to find solutions to its problems by trying to emulate hardware developers. If anything, as CAD and CAM systems have helped hardware designers to create more and more complex designs, hardware engineering is becoming more and more like software development.

    Designing software is an exercise in managing complexity. The complexity exits within the software design itself, within the software organization of the company, and within the industry as a whole. Software design is very similar to systems design. It can span multiple technologies and often involves multiple sub-disciplines. Software specifications tend to be fluid, and change rapidly and often, usually while the design process is still going on. Software development teams also tend to be fluid, likewise often changing in the middle of the design process. In many ways, software bears more resemblance to complex social or organic systems than to hardware. All of this makes software design a difficult and error prone process. None of this is original thinking, but almost 30 years after the software engineering revolution began, software development is still seen as an undisciplined art compared to other engineering professions.

    The general consensus is that when real engineers get through with a design, no matter how complex, they are pretty sure it will work. They are also pretty sure it can be built using accepted construction techniques. In order for this to happen, hardware engineers spend a considerable amount of time validating and refining their designs. Consider a bridge design, for example. Before such a design is actually built the engineers do structural analysis; they build computer models and run simulations; they build scale models and test them in wind tunnels or other ways. In short, the designers do everything they could think of to make sure the design is a good design before it is built. The design of new airliner is even worse; for those, full scale prototypes must be built and test flown to validate the design predictions.

    It seems obvious to most people that software designs do not go through the same rigorous engineering as hardware designs. However, if we consider source code as design, we see that software designers actually do a considerable amount of validating and refining their designs. Software designers do not call it engineering, however, we call it testing and debugging. Most people do not consider testing and debugging as real "engineering"; certainly not in the software business. The reason has more to do with the refusal of the software industry to accept code as design than with any real engineering difference. Mock-ups, prototypes, and bread-boards are actually an accepted part of other engineering disciplines. Software designers do not have or use more formal methods of validating their designs because of the simple economics of the software build cycle.

    Revelation number two: it is cheaper and simpler to just build the design and test it than to do anything else. We do not care how many builds we do -- they cost next to nothing in terms of time and the resources used can be completely reclaimed later if we discard the build. Note that testing is not just concerned with getting the current design correct, it is part of the process of refining the design. Hardware engineers of complex systems often build models (or at least they visually render their designs using computer graphics). This allows them to get a "feel" for the design that is not possible by just reviewing the design itself. Building such a model is both impossible and unnecessary with a software design. We just build the product itself. Even if formal software proofs were as automatic as a compiler, we would still do build/test cycles. Ergo, formal proofs have never been of much practical interest to the software industry.

    This is the reality of the software development process today. Ever more complex software designs are being created by an ever increasing number of people and organizations. These designs will be coded in some programming language and then validated and refined via the build/test cycle. The process is error prone and not particularly rigorous to begin with. The fact that a great many software developers do not want to believe that this is the way it works compounds the problem enormously.

    Most current software development processes try to segregate the different phases of software design into separate pigeon-holes. The top level design must be completed and frozen before any code is written. Testing and debugging are necessary just to weed out the construction mistakes. In between are the programmers, the construction workers of the software industry. Many believe that if we could just get programmers to quit "hacking" and "build" the designs as given to them (and in the process, make fewer errors) then software development might mature into a true engineering discipline. Not likely to happen as long as the process ignores the engineering and economic realities.

    For example, no other modern industry would tolerate a rework rate of over 100% in its manufacturing process. A construction worker who can not build it right the first time, most of the time, is soon out of a job. In software, even the smallest piece of code is likely to be revised or completely rewritten during testing and debugging. We accept this sort of refinement during a creative process like design, not as part of a manufacturing process. No one expects an engineer to create a perfect design the first time. Even if she does, it must still be put through the refinement process just to prove that it was perfect.

    If we learn nothing else from Japanese management techniques, we should learn that it is counter-productive to blame the workers for errors in the process. Instead of continuing to force software development to conform to an incorrect process model, we need to revise the process so that it helps rather than hinders efforts to produce better software. This is the litmus test of "software engineering." Engineering is about how you do the process, not about whether the final design document needs a CAD system to produce it.

    The overwhelming problem with software development is that everything is part of the design process. Coding is design, testing and debugging are part of design, and what we typically call software design is still part of design. Software may be cheap to build, but it is incredibly expensive to design. Software is so complex that there are plenty of different design aspects and their resulting design views. The problem is that all the different aspects interrelate (just like they do in hardware engineering). It would be nice if top level designers could ignore the details of module algorithm design. Likewise, it would be nice if programmers did not have to worry about top level design issues when designing the internal algorithms of a module. Unfortunately, the aspects of one design layer intrude into the others. The choice of algorithms for a given module can be as important to the overall success of the software system as any of the higher level design aspects. There is no hierarchy of importance among the different aspects of a software design. An incorrect design at the lowest module level can be as fatal as a mistake at the highest level. A software design must be complete and correct in all its aspects, or all software builds based on the design will be erroneous.

    In order to deal with the complexity, software is designed in layers. When a programmer is worrying about the detailed design of one module, there are probably hundreds of other modules and thousands of other details that he can not possibly worry about at the same time. For example, there are important aspects of software design that do not fall cleanly into the categories of data structures and algorithms. Ideally, programmers should not have to worry about these other aspects of a design when designing code.

    This is not how it works, however, and the reasons start to make sense. The software design is not complete until it has been coded and tested. Testing is a fundamental part of the design validation and refinement process. The high level structural design is not a complete software design; it is just a structural framework for the detailed design. We have very limited capabilities for rigorously validating a high level design. The detailed design will ultimately influence (or should be allowed to influence) the high level design at least as much as other factors. Refining all the aspects of a design is a process that should be happening throughout the design cycle. If any aspect of the design is frozen out of the refinement process, it is hardly surprising that the final design will be poor or even unworkable.

    It would be nice if high level software design could be a more rigorous engineering process, but the real world of software systems is not rigorous. Software is too complex and it depends on too many other things. Maybe some hardware does not work quite the way the designers thought it did, or a library routine has an undocumented restriction. These are the kinds of problems that every software project encounters sooner or later. These are the kinds of problems discovered during testing (if we do a good job of testing), for the simple reason that there was no way to discover them earlier. When they are discovered, they force a change in the design. If we are lucky, the design changes are local. More often than not, the changes will ripple through some significant portion of the entire software design (Murphy's Law). When part of the effected design can not change for some reason, then the other parts of the design will have to be weakened to accommodate. This often results is what managers perceive as "hacking", but it is the reality of software development.

    For example, I recently worked on a project where a timing dependency was discovered between the internals of module A and another module B. Unfortunately, the internals of module A were hidden behind an abstraction that did not permit any way to incorporate the invocation of module B in its proper sequence. Naturally, by the time the problem was discovered, it was much too late to try to change the abstraction of A. As expected, what happened was an increasingly complex set of "fixes" applied to the internal design of A. Before we finished installing version 1, there was the general feeling that the design was breaking down. Every new fix was likely to break some older fix. This is a normal software development project. Eventually, my colleagues and I argued for a change in the design, but we had to volunteer free overtime in order to get management to agree.

    On any software project of typical size, problems like these are guaranteed to come up. Despite all attempts to prevent it, important details will be overlooked. This is the difference between craft and engineering. Experience can lead us in the right direction. This is craft. Experience will only take us so far into uncharted territory. Then we must take what we started with and make it better through a controlled process of refinement. This is engineering.

    As just a small point, all programmers know that writing the software design documents after the code instead of before, produces much more accurate documents. The reason is now obvious. Only the final design, as reflected in code, is the only one refined during the build/test cycle. The probability of the initial design being unchanged during this cycle is inversely related to the number of modules and number of programmers on a project. It rapidly becomes indistinguishable from zero.

    In software engineering, we desperately need good design at all levels. In particular, we need good top level design. The better the early design, the easier detailed design will be. Designers should use anything that helps. Structure charts, Booch diagrams, state tables, PDL, etc. -- if it helps, then use it. We must keep in mind, however, that these tools and notations are not a software design. Eventually, we have to create the real software design, and it will be in some programming language. Therefore, we should not be afraid to code our designs as we derive them. We simply must be willing to refine them as necessary.

    There is as yet no design notation equally suited for use in both top level design and detailed design. Ultimately, the design will end up coded in some programming language. This means that top level design notations have to be translated into the target programming language before detailed design can begin. This translation step takes time and introduces errors. Rather than translate from a notation that may not map cleanly into the programming language of choice, programmers often go back to the requirements and redo the top level design, coding it as they go. This, too, is part of the reality of software development.

    It is probably better to let the original designers write the original code, rather than have someone else translate a language independent design later. What we need is a unified design notation suitable for all levels of design. In other words, we need a programming language that is also suitable for capturing high level design concepts. This is where C++ comes in. C++ is a programming language suitable for real world projects that is also a more expressive software design language. C++ allows us to directly express high level information about design components. This makes it easier to produce the design, and easier to refine it later. With its stronger type checking, it also helps the process of detecting design errors. This results in a more robust design, in essence a better engineered design.

    Ultimately, a software design must be represented in some programming language, and then validated and refined via a build/test cycle. Any pretense otherwise is just silliness. Consider what software development tools and techniques have gained popularity. Structured programming was considered a breakthrough in its time. Pascal popularized it and in turn became popular. Object oriented design is the new rage and C++ is at the heart of it. Now think about what has not worked. CASE tools? Popular, yes; universal, no. Structure charts? Same thing. Likewise, Warner-Orr diagrams, Booch diagrams, object diagrams, you name it. Each has its strengths, and a single fundamental weakness -- it really isn't a software design. In fact the only software design notation that can be called widespread is PDL, and what does that look like.

    This says that the collective subconscious of the software industry instinctively knows that improvements in programming techniques and real world programming languages in particular are overwhelmingly more important than anything else in the software business. It also says that programmers are interested in design. When more expressive programming languages become available, software developers will adopt them.

    Also consider how the process of software development is changing. Once upon a time we had the waterfall process. Now we talk of spiral development and rapid prototyping. While such techniques are often justified with terms like "risk abatement" and "shortened product delivery times", they are really just excuses to start coding earlier in the life cycle. This is good. This allows the build/test cycle to start validating and refining the design earlier. It also means that it is more likely that the software designers that developed the top level design are still around to do the detailed design.

    As noted above -- engineering is more about how you do the process than it is about what the final product looks like. We in the software business are close to being engineers, but we need a couple of perceptual changes. Programming and the build/test cycle are central to the process of engineering software. We need to manage them as such. The economics of the build/test cycle, plus the fact that a software system can represent practically anything, makes it very unlikely that we will find any general purpose methods for validating a software design. We can improve this process, but we can not escape it.

    One final point: the goal of any engineering design project is the production of some documentation. Obviously, the actual design documents are the most important, but they are not the only ones that must be produced. Someone is eventually expected to use the software. It is also likely that the system will have to be modified and enhanced at a later time. This means that auxiliary documentation is as important for a software project as it is for a hardware project. Ignoring for now users manuals, installation guides, and other documents not directly associated with the design process, there are still two important needs that must be solved with auxiliary design documents.

    The first use of auxiliary documentation is to capture important information from the problem space that did not make it directly into the design. Software design involves inventing software concepts to model concepts in a problem space. This process requires developing an understanding of the problem space concepts. Usually this understanding will include information that does not directly end up being modeled in the software space, but which nevertheless helped the designer determine what the essential concepts were, and how best to model them. This information should be captured somewhere in case the model needs to be changed at a later time.

    The second important need for auxiliary documentation is to document those aspects of the design that are difficult to extract directly from the design itself. These can include both high level and low level aspects. Many of these aspects are best depicted graphically. This makes them hard to include as comments in the source code. This is not an argument for a graphical software design notation instead of a programming language. This is no different from the need for textual descriptions to accompany the graphical design documents of hardware disciplines. Never forget that the source code determines what the actual design really is, not the auxiliary documentation. Ideally, software tools would be available that post processed a source code design and generated the auxiliary documentation. That may be too much to expect. The next best thing might be some tools that let programmers (or technical writers) extract specific information from the source code that can then be documented in some other way. Undoubtedly, keeping such documentation up to date manually is difficult. This is another argument for the need for more expressive programming languages. It is also an argument for keeping such auxiliary documentation to a minimum and keeping it as informal as possible until as late in the project as possible. Again, we could use some better tools, otherwise we end up falling back on pencil, paper, and chalk boards.

    To summarize:

    o Real software runs on computers. It is a sequence of ones and zeros that is stored on some magnetic media. It is not a program listing in C++ (or any other programming language).

    o A program listing is a document that represents a software design. Compilers and linkers actually build software designs.

    o Real software is incredibly cheap to build, and getting cheaper all the time as computers get faster.

    o Real software is incredibly expensive to design. This is true because software is incredibly complex and because practically all the steps of a software project are part of the design process.

    o Programming is a design activity -- a good software design process recognizes this and does not hesitate to code when coding makes sense.

    o Coding actually makes sense more often than believed. Often the process of rendering the design in code will reveal oversights and the need for additional design effort. The earlier this occurs, the better the design will be.

    o Since software is so cheap to build, formal engineering validation methods are not of much use in real world software development. It is easier and cheaper to just build the design and test it than to try to prove it.

    o Testing and debugging are design activities -- they are the software equivalent of the design validation and refinement processes of other engineering disciplines. A good software design process recognizes this and does not try to short change the steps.

    o There are other design activities -- call them top level design, module design, structural design, architectural design, or whatever. A good software design process recognizes this and deliberately includes the steps.

    o All design activities interact. A good software design process recognizes this and allows the design to change, sometimes radically, as various design steps reveal the need.

    o Many different software design notations are potentially useful -- as auxiliary documentation and as tools to help facilitate the design process. They are not a software design.

    o Software development is still more a craft than an engineering discipline. This is primarily because of a lack of rigor in the critical processes of validating and improving a design.

    o Ultimately, real advances in software development depend upon advances in programming techniques, which in turn mean advances in programming languages. C++ is such an advance. It has exploded in popularity because it is a mainstream programming language that directly supports better software design.

    o C++ is a step in the right direction, but still more advances are needed.