善意提醒

如果您打开本站很慢,布局排版混乱,并且看不到图片,那么可能是因为您还没有掌握用科学的方法上网的本领。

2015-08-19

蹊跷的 LNK1104 错误

最近在给公司开发下一代产品,所以比较忙,也没有太多的事情写 Blog。不过昨天在技术上遇到一件不大不小的事情,也许值得拿出来说一下。

这个下一代产品是从头全新开发的,为此我提供了一些基础库的代码,给到项目组的其它 Project 使用。其中有一个是用的动态链接库的形式。为了方便其它 Project 使用,我把如下的预编译宏写在了头文件中:
#pragma comment(lib, "XEngine")
这样,所有需要包含这个头文件的代码,就不需要自己去搞清楚到底要链接哪个 lib 文件了。

但是,同事在拿到代码做全新编译的时候,出现了 LNK1104 错误。Cannot Open File XEngine.lib。同事当然一头雾水,Baidu 上搜了一下之后,一头埋进去检查硬盘坏道去了。

我这边一开始没有问题,当把发布目录中的 XEngine.lib 删除了之后也出现了同样的问题。源代码对编译结果产生了依赖,这当然是不对的。问题当然出在那句预编译宏上,只不过代码的编写时间过去有点久了,我一时没想起来。
正好这个 DLL 具有导出函数,因此 VC 自动生成了相关的代码,只需要把前面的预编译宏放在这里面就可以了:
#ifdef XENGINE_EXPORTS
#define XENGINE_API __declspec(dllexport)
#else
#define XENGINE_API __declspec(dllimport)
#pragma comment(lib, "XEngine")
#endif
这样就解决这个问题了。如果没有这些自动生成的代码可以利用,也可以参考着自己写一个。

2015-06-04

按更新时间同步单个文件的简易方案

我曾经抱怨过,有个很简单的单个文件同步需求,却居然难以找到有软件能够解决。也许是需求太过于简单,反而没人去考虑。连 Allway Sync 这么好的东西,也做不到。于是一直都是人肉解决。

最近有点厌倦了每次 Copy 来 Copy 去的事情。而且中间也不是没有出过错。一出错就会丢一次的数据。索性简单地写了个 bat 批处理脚本来解决这种事情:
@echo off
xcopy D:\仓库\文件.txt .\ /D /Y >nul
xcopy .\文件.txt D:\仓库\ /D /Y >nul
运行这个 bat 脚本后,更新时间比较早的那个副本,会被另外一个覆盖掉。接下来只要每次编辑前或编辑后都记得运行一下这个脚本,就可以使两边文件版本一致,不需要再考虑复制方向对不对的问题了。

2015-03-11

在 Windows 上用 x64 的 Python 运行 GoAgent

首先声明:本文适用于至少懂编程,并且爱折腾的人。不一定要很熟悉 Python,但至少应该入过门了。

GoAgent 自带 Python27 运行环境,不过因为本机上也安装了 Python 2.7 x64,所以希望能直接使用我安装的 64 位版本。总结下来,要点有:
  • 无论你是否准备使用 x64 版 Python。GoAgent 与 Python 2.7.9 不兼容,应该安装 Python 2.7.8 版本。
  • 要运行 GoAgent 还需要 gevent 和 pyOpenSSL。可以手动安装,也可以通过 pip 来安装。推荐后者因为省事。
  • Python 2.7.8(无论是否 x64)及更早的版本,没有自带 pip。所以要去下载 get-pip.py,然后用 python 运行这个脚本安装 pip。
    ps: 用 get-pip.py 下载到的 pip.exe 的版本,比 Python 2.7.9 里面带的要新一些。所以就算有自带我也会换掉它。

另一个重点来了:
有些版本的 GoAgent 里面对于 crypt32.dll 的加载代码在 x64 的 Python 中有问题,需要修改。改成像这样就可以了:
crypt32 = ctypes.WinDLL(u'crypt32.dll')
crypt32_handle = crypt32._handle
释放的时候要这样:
ctypes.windll.kernel32.FreeLibrary.argtypes = [HMODULE]
ctypes.windll.kernel32.FreeLibrary(crypt32_handle)
在前面还要:
from ctypes.wintypes import *
否则 x64 下会报错,因为 HMODULE 和 long 的长度在 x64 下不一样了。

我用的 2.1.17 的 GoAgent 中,对于 crypt32.dll 的加载代码还位于 proxy.py 中。最新的 GoAgent 已经移到 proxylib.py 里面了。什么时候移的我没有去考察过。代码改法可能略有不同,不过思路应该是一样的。

折腾好以后。把本机上的 Python.exe 复制一份改名成 Python27.exe。接下来就可以把 GoAgent 自带的 Python27.* 给删掉啦。

2014-12-31

2014年年末座谈会

终:余,我们好像到早了呢,大哥二哥他们似乎还没到。
余:可茶点已经摆好了,是茉理姊姊放的吗?
终:那好,我先吃一个点心……
续:咳咳。
始:大家都到了,那么就开始吧。一年一度的竜堂家年末座谈会。
余:说是一年一度,可是感觉好像很久都没有召开过的样子了呢?
终:我来查一下……啊!上次是 2004 年。
续:今年是 2014 年。这么说来,上一次是十年之前了。作者也的确是够懒的。
终:严正抗议!害我这么多年没有茶点吃。
始:听说作者也曾经为此深深自责过,不过最终还是脸皮战胜了羞愧。
余:我想作者也是有他自己的苦衷吧,我们还是不要太苛刻了。
终:什么苦衷,其实是懒吧!?我的茶点!抗议!
始:好了,终。天上一日,人间一年。对于我们而言,也不过就是十天而已嘛。我们还是进入正题吧。续?
续:嗯,今年发生了很多事情。不过就作者本人而言,他托我们向各位读者道个歉。因为 Blog 更新得实在不能算得上勤快。
终:我来数数,一、二、三……连这篇在内,2014 年也就只写了 15 篇 Blog。而且还有一篇是转载的。
续:基本上是一个月一篇的频率。所以呢,作者表示很惭愧,并且许诺会在新的年度里超过这个数字。
余:其实我看作者在今年六月份的时候还是蛮努力的。曾经有过三天内发表两篇 Blog 的记录呢。
终:我看看,6 月 1 日,匪军……。大哥,什么是「匪军」?
续:匪军就是像你这样吃饭不给钱的混帐家伙。
终:我什么时候吃饭不给钱了?!
余:终哥哥,我记得好像是上次那个什么「大胃王」比赛……
终:噢,那个啊?哈哈!那是因为我最后赢了啊!赢了就不用给钱啊。胜者为王嘛。嘿嘿。
始:余,别听他的。「胜者为王」是低等动物的法则。
续:听到没?终,低等动物。
终:啊呸。
始:虽然是玩笑话,但事实也是这样的没错。看到有着悠久历史的中华民族如今堕落到与低等动物一个境界,大概连孔夫子也要哀叹吧。
余:说到孔夫子,好像也被匪军用来干坏事了。
终:咦……?
续:是那个什么「孔子学院」是吧?听说已经臭名远扬了。
始:好几个大学已经与其解除协议了。
终:等等……
始:这种做法的确很让人不齿。这种龌龊事情,恐怕即使是日本的无耻政客也做不出来。
余:听说控制这些的是一个女人?
始:没错,叫做许琳,是匪军下辖的所谓「汉办」的主任。听作者说以后打算用英文字母中正数第二个字母来称呼她。
续:……?
余:……?
始:怎么了?
余:续哥哥一定是在奇怪,终哥哥居然这次没有说「这女人看来最适合二哥您了」之类的话。
终:……为什么?说到「匪军」,连余似乎都一清二楚,而我却不知道?
余:终哥哥,你一定很久没有关注作者的 Blog 了。「匪军」就是指的中国共产党。
终:啊!我想起来了,就是把黄老关起来的那些人?
始:是的没错。而且被他们关进的监狱的,不只有黄老,还有许多别的人,都是出于类似的原因。
终:那末就是百分百的坏人没错了!
续:不过他们中间有许多自己人,最近也进去了。
终:真是活该!
始:因为匪军的头目最近在搞政治运动。每次政治运动都注定会有大清洗,这是历史规律。
余:而且听说我们上次去过的香港,也被匪军搞得乌烟瘴气,再也没有大不列颠统治时期的荣耀了。
终:这帮坏蛋。下次去中国内地的时候,我非好好教训他们不可。
始:终!人类的事情,我们不宜参与太多,静静看着就可以了。中国有句古话,叫「自作孽,不可活」。意思就是说多行不义必自毙。不管怎么说,人类自己的事情,要自己解决。
终:话虽这样说,可是……
始:续,下一个话题是……?
续:作者对明年的展望。
终:可恶,就这样岔开话题。
始:嗯,作者只是个小人物。所以,只要想好自己能做些什么就可以了。
终:报告大家一个好消息:作者明年又要加薪啦!
续:喂,可恶,被你抢先了。
始:呵呵,其实大家应该都已经猜到了。作者之前其实已经差不多算是透露过了。稍稍具体一点地说,明年作者可能要担任更重要的任务了。
余:就是传说中的「升职加薪」吗?
始:也不是,是「任务」不是「职务」哦,余。
续:简单地说,就是更累了。
终:这样啊?那明年座谈会是不是开不成了?如果开不成,今天的茶点我要双份。
始:那就从你压岁钱中扣,如何?金龙?
终:不好。那我还是不要了。
续:虽然明年可能会更累,但作者手下的「小弟」数量也会比今年多噢。
终:……所以?
续:所以,更新 Blog 比今年稍稍勤快一点,或许还是可以做到的。
余:不管怎么说,在这里要恭喜作者和他的家人了。
终:对了,作者也有了一个和余一样可爱的小儿子呢。
续:目前才只有不到两岁而已。不过的确是很可爱。而且主要是没有被终欺负过。
终:我什么时候欺负过余?
续:怎么?我有说过你欺负过余吗?
余:的确是没有。
终:你……你们……
始:哎呀,茶点已经被终吃完了,那么今天就到此为止吧。

(以上内容纯属虚构,并且与田中芳树无关)

2014-11-28

不喜欢,没有为什么

图片来自 Google 图片,与本文内容无关。

刚才看到 Google+ 上有个 PO 说因为《三体》里面有政委,所以不喜欢。
具体内容不予评论,我只想说的是,不喜欢一个东西,只需要一个理由就足够了。你可以说出这东西有多么多么的好来,但我就一个理由,也许是很私人很难以理解的理由,但就是不喜欢,足够了。说再多也没用。

我没看过《三体》,也没有看的打算。虽然听到过很多的褒扬,我相信应该是的确有可取之处,也许是超越传统国产科幻小说的存在,但很奇怪,我就是没有兴趣。就好比不管谁跟我说红旗(牌小轿车)有多么多么好,而且就算它的确有多么多么的好,我就是不会去买它。

我是看叶永烈长大的人,当然,也看过郑渊洁。虽然我现在是个中共黑没错,但中国护照我也领,淘宝一号店京东什么的我也刷,海淘什么的我嫌麻烦还从来没去碰过。硬要说我崇洋媚外,我觉得自己也还没有那个资格。

有的人会说(嗯,我猜到他们会说):你连看都还没看过,就认为它不好看?
错了。我不是认为它不好看,我是不喜欢,没兴趣。Understand?

那到底是什么让我不喜欢?我自己也分析不太清楚,可能这涉及到很深层面的心理问题。也许是我不喜欢这个书名?也许是我觉得中国人的名字出现在西方绝对主导的作品体裁里很怪异?也许是习惯性地认为国产 XX 都那副尿性?也许是因为太多不应该喜欢科幻的人喜欢它所以我就觉得自己应该不喜欢它?

对了,我还不喜欢《货币战争》和《狼图腾》,也不喜欢《越狱》。这些,也都没有为什么。

2014-11-07

丢人现眼的脱口秀

上海台最近搞了一些脱口秀节目。舒悦,还有柏万青,都跑出来立在台上评论一些时事。——其实也不是最近,有一阵子了。公车上的移动电视还经常放,也经常性的让我听得想吐。前几天我又吐了一回,所以决定针对这个事情写篇 Blog 说一下。

对他们个人,我并没什么看法。舒悦在搞笑和扮老太太方面,真的是有一手的。柏万青呢,做调解的时候,能一眼看透矛盾的关键所在,的确厉害。这些都是让我极为佩服的本事。但是,人不可能全能。硬要捞过界,做自己原本并不擅长的事情,就得做好丢人现眼的打算。比如这两位跑去评论时事。

我觉得吧,舒悦大概是真的见识不够。一个搞传统戏曲艺术的,人生阅历又没到位,实在是讲不出啥好道理,只好拿些言语哄老阿姨开心,也是情理之中的事情。柏万青呢,本来以她的能力应该能拎得清,奈何屁股决定脑袋,老是容易把自己当政府,于是也经常讲出些让人摇头的话来。

比如他们经常拿来开涮的「上网有害论」。某某女孩被网友骗财骗色啦,某某小伙子被网友骗去做传销啦,某某 X 年网购上当受骗啦,某某大学生开设色情网站被抓啦……,等等等等,大约都是这之类的话题,然后一副「网上那些乱七八糟的东西」的口吻,以及拿「网上的东西好相信伐」当口头禅。反正就是「网络不是个好东西」。

其实这种事情都不需要我来吐槽。网络只不过是通信的一种方式而已。没网络以前有电话,没电话以前还有面基。网络只是让沟通联系更容易而已。会上当受骗的,倒退几百年,没网没电没石油,一样会上当受骗。跟网络搭什么界?

更何况,网络如果真能让信息来去更方便,其实反倒是会让人更不容易上当受骗才对。以前那种不开化的年代,同一个骗局可以全国各地翻来覆去随便骗,现在上 Google 搜一下你就能知道刚才那个骗子电话到底是怎么个玩法。在阿加莎·克里斯蒂那个年代,伪造身份冒名顶替连波洛都能差点骗过去。现在不要说冒名顶替,就是你想隐姓埋名,也分分钟给你人肉出来。不是网络上的东西不好相信,实在是有些人缺心眼儿。为什么缺心眼儿?怪爹怪妈怪政府呗!

能说出那些混帐逻辑,可见这些脱口秀的主持人实在是不合适去评论时事。脑子早就跟不上时代了,硬要去评,那不是丢人现眼么?

2014-10-31

SingleThread 下遇到的并发问题

手下某个小弟,有一天报告我说他写的某个 Win32 Application 有一个奇怪的 Bug,搞了半天搞不定,向我寻求支援。Bug 现象是:下载文件,完毕弹框提示,点掉之后报错,Crash。

通常而言,这种问题,往往是因为在释放、删除什么东西的时候,该做的事情没做对,比如对着一个对象的指针进行了重复 delete 之类。但看了下代码,没觉得这方面有什么问题。因为这是个 SingleThread 的程序,于是尝试用单步跟踪跟了一下,发现有一段代码似乎在所属对象析构之后还在跑。这就有点奇怪了:SingleThread 的 Application,不应该有这种属于 MultiThread 的毛病才对。Socket 模型用的是 AsyncSelect,也就是说「异步」是用 Windows 消息做出来的,并不是真的「并发」。那么到底是哪里不对劲呢?

再接下来分析发现,虽然是 SingleThread,但最后出错前弹的那个提示框,是在 OnReceive 的时候通过 SendMessage 去弹的。这样就有眉目了:ModalDialog 并不阻塞 ParentWindow 的消息循环,所以在弹框等待用户确认的时候,消息循环收到了 OnClose,于是 Socket 对象在用户点击确认按钮之前,其实已经被 Destroy 了。之前还没跑完的 OnReceive,接着再跑的话,当然只能 Crash 了。

分析到这里,问题就已经很明白了:这就跟 MultiThread 下临界区没加锁一样嘛。你以为 SingleThread 下每个函数就都是原子操作,不会被乱入的东西打搅?呵呵,你一 DoModal 就会给你再嵌个消息循环进去的。可怜很多小弟连 DoModel 的原理都没搞懂就开始写程序了。我上次还听几个小弟在争论相关问题呢。不是说写程序必须啥都弄明白才能开始,但若是只拎半壶水就开跑,将来就难免会碰上这种「奇怪」的问题。

要修正这个问题,也很简单,改成用 PostMessage 让 MainWindow 自己去处理弹框的事情就可以了。不过有点奇怪的是,在 XP 下好像不会看到错误现象。Win7 下直接运行 EXE 也不报错。只有通过两层以上的 CreateProcess 去调用,才会看到现象。难怪没什么用户报告这个问题。是不是 OS 觉得这个 EXE 反正会很快地 Over 掉,有些错误就不报算了?看来微软在私底下还是有一些没告诉大家的小动作的哈哈。

总结一下,这个案例教育我们:
  1. 不要以为只要是 SingleThread 就一定不会遇到并发问题。
  2. 前/后台逻辑应该要区分清晰,是后台代码就别抢前台的活儿。
  3. 还有,SendMessage / PostMessage 不要不经大脑就乱用。

2014-07-31

Alipaybsm.exe 是个有意思的东西

在接下来的篇幅中,我要讲一个目前还没结束的故事。故事可能还会继续发展下去,也可能因为我的懒而就此打住。但至少我觉得目前已经有足够有意思的信息可以让诸位知道了。这件事,跟支付宝有关,跟(网络)信息安全也可能有一些关系。有兴趣的朋友,可以接着看下去。


我以前曾写过一个服务器 Ping 值测试程序(参见这里《写了个批量测试服务器Ping值的小工具》)。这个程序一直都能满足我的需要,直到有一天在我太太的笔记本 Win7 x64 系统上遇到了问题:对几乎所有的 IP,我这个程序的 Ping 都很快收到了回应,快得不正常,几乎就像做了个本地调用一样,与实际情况不相符。于是我打算看看这是怎么回事情。

当时我人在公司,VC6 远程调试又不方便。最后靠着 DbgView 终于搞清楚了:接收到的数据中,多出来了一份不正常的东西。我之前的代码,并没有估计到这份不正常的数据可能会出现,所以处理上出了些问题。

OK,这算是我的 Bug。可这「不正常的数据」到底是什么东西?我把它 Dump 出来一看,还真是有点奇怪!ICMP Type 是 8,源地址和目的地址则与预期的 Echo 回应包刚好相反。算上 sendto 时候系统自己加上的 IP 包的包头,跟我送去发送缓冲区里的数据那是一模一样。

要解决我程序里的这个问题非常简单。但是另一个问题就不那么好回答了:为什么其它电脑上不会这样,偏偏这台电脑会出现这种奇怪的事情?
直接答案很简单——它一定跟别的电脑有什么地方不一样!
那么还有第二个问题:到底是什么地方不一样呢?

可以说是我的幸运,也可以说是阿里集团的不幸。因为我的 Taskmgr 里面进程列表设置为按 ASCII 字母序排升序的缘故,我很快就找到了这第二个问题的答案:Alipaybsm.exe。杀掉Alipaybsm.exe 这个进程,前面提到的那份「不正常的数据」就不再出现。而这个 Alipaybsm.exe 似乎由 AlipaySecSvc.exe 在守护,过了一会儿就又自己启动起来了。它一出现在进程列表中,我一试,哈,那个奇怪的现象就又出现了。

后来,我把这事情在 Twitter 上说了一下,还引发了一场小小的讨论。
我目前还没完全想明白 Alipaybsm.exe 这样做的目的是什么。初步感觉,有可能是跟背地里监控网络流量有关。毕竟,目的地址不正确的数据,就算被放入 Socket 接收缓冲里面,在网络层与传输层之间估计也被滤掉了。我这次是因为用了 SOCK_RAW,需要自己下到网络(IP)层来处理数据,才碰巧发现了这个情况。如果只是在传输层(TCP / UDP)从事工作,估计不会有任何察觉。
只不过,反过来讲,如果能做到复制数据到 Socket 接收缓冲,那应该完全可以做到监控流量而不带任何痕迹才对。所以我目前还只能理解为,Alipaybsm.exe 想完全监控网络流量,所以利用了这个手段(复制发送的数据到接收缓冲中),但干这事屁股没擦干净(也可能没法擦干净),才产生了我遇到的这些情况。

我本来以为当时那个 Alipaybsm.exe 是个假货。但看 EXE 的详细信息,以及绑定的数字证书,都像是支付宝官方的真货。我又以为那只是一个不成熟的版本,可能有 Bug,但我前两天为了转一笔账,又去下载并安装了一个支付宝安全控件,然后它又出现了,带着它那奇怪的行为又出现了。
所以,我们来仔细看看这货吧:

看上去挺正常吧?

在 Twitter 上讨论的时候,有人表示,在 Mac 上用防火墙没观察到有这个现象。为此,我今天特意去确认了一下:在 Windows 上抓包,也观察不到这个现象。我估计,只有自己写基于 SOCK_RAW 的程序,才能收到这些数据。为了检查这种特殊的行为,我专门写了个小程序 AlipaybsmTester,基本上就是一个单地址单次单线程的 PingTester。

从这幅截图中可以看到,Microsoft Network Monitor 只抓到了一来一回共两个包,但我的测试程序发了一个包收到了两个,内容各不相同。如果杀掉 Alipaybsm.exe,那就只会收到后一个包了。

接下来再看看这个 Alipaybsm.exe 的一些更好玩的事情:
很奇怪的是,它其实并不是随着「支付宝安全控件」(Aliedit.exe)装上去的。当你登录支付宝,根据 Web 页面上的提示安装了「支付宝安全控件」时,只会在 Program Files (x86)\alipay 下面建一个名字叫 alieditplus 的目录。

但是过一会儿(我这次过了 30 分钟左右),在 alieditplus 下面会出现一个 update 目录,并下载一个 SafeTransaction_Setup.exe 放在其 \job\file\tmp\zip_1009_ 子目录中(不同时期不同环境中路径可能会有所不同)。随后 Program Files (x86)\alipay\SafeTransaction 目录便出现,里面就有 Alipaybsm.exe(当然还有一些别的)。

我在网上想搜一下关于这个 Alipaybsm.exe 或 SafeTransaction_Setup.exe 的相关信息,发现少得可怜。有一篇 「百度知道」的问题 在问为什么 Alipaybsm.exe 可以提升网速。我估计提问者是从迅雷或 360 流量监控浮窗上观察到了这种现象吧?其实这就是 Alipaybsm.exe 在偷偷复制数据包到接收缓冲中的结果。那些在接收缓冲中突然多出来的数据,在第三方看来就是网速翻倍了。

Alipay 官方则完全没有提到过这些东西,好像它们是感染了 AIDS 的私生子一样。不过每个安装了支付宝安全控件的电脑上,估计都会有这些个东西(还有个 AlipayDHC 也值得注意)。我认为以这种方式进行推广的程序,很可能另有其目的,不见得真的是保障个浏览器安全这么简单。如果真是为了保障浏览器安全,完全可以公开(乃至大张旗鼓地)宣传,然后打包到安装包里一起分发下去正大光明地安装,不是吗?

PS: 我后来发现,杀掉 AlipaySecSvc.exe 也会导致复制数据包的现象中断,并且重启该服务之后,恢复现象花的时间比单单杀掉 Alipaybsm.exe 要长。可见 Alipaybsm.exe 的角色大概只是一个行动的发起者和结果的分析者,具体对流量实施监控的行为,很可能是它去调用 AlipaySecSvc.exe 中的某些个服务来完成的。这说明对于「支付宝安全控件」本身也不能掉以轻心。相关功能其实可能一直就放在 AlipaySecSvc.exe 中,只是没有人来扣扳机而已。而这个扣扳机的可以是 Alipaybsm.exe,也可以是别的谁,那谁谁谁。

2014-05-07

BCB5 在 Win7 x64 上启动时报错「1 transfer item(s) contain syntax errors」

由于 WinXP 已经被微软官方宣告服务终止,最近把工作环境升级到了 Win7,并且安装的是 x64 版本。装了之后发现,BCB5 启动的时候会弹出一个报错对话框,里面的信息很奇怪:
1 transfer item(s) contain syntax errors
点「确定」关闭对话框之后,BCB5 使用起来也没有什么问题。但每次启动都会弹框,很讨厌。那么,这是什么情况呢?

一般来讲,Win7 与 WinXP 之间,出现类似兼容性问题的原因大致有:
  • 管理员权限问题
  • 注册表键值问题
  • 系统目录问题
  • DEP 问题
在 x64 系统上,目录问题尤其突出。Program Files 现在还有个 Program Files (x86)。System32 那边也有个 SysWOW64。后者一般跟应用软件关系还不太大,但前者常常会导致很多问题。我就见过有的软件安装包都会运行出现问题。

这次的情况其实也类似。照例,先上国外网站的链接。
http://codeverge.com/embarcadero.cppbuilder.install/at-start-up-1-transfer-item-s/1096695
最后那个回复,把操作步骤写得很详尽。做 C/C++ 开发的,英语阅读一般还是不会有问题,我就不翻译了。

总之呢,这个问题就是因为 Program Files (x86) 直接引起的。另一个回复里面说把 BCB5 卸载后重新安装在 Program Files 下也能解决。当然,有问题的地方其实只有一处,所以完全没必要如此大动干戈。

2014-03-20

为什么不用动态内存分配?

在写这篇 Blog 的时候,我考虑了几分钟,在想要不要把标题写成《为什么有的程序员喜欢用动态内存分配?》。最后我还是把那些修饰词和定语给删了。虽然那个标题更准确一点,但是本文基本上是一篇吐槽文,我还是比较喜欢这种反问句的感觉。

事情是这样开始的:
在工作中,遇到了别的同事以前写的一段代码。作用是显示从某些网上下载的文件的内容。文件下载完后,也在本地保存了一份副本,这样如果下次发现本地有副本,就直接显示不用下载了。
这基本上是一个类似浏览器缓存的功能,实现起来也不难。不过这次我碰到一个 Bug,有个文件的副本,在解析的时候报错了。
因为第一次下载的时候并没有报错,所以焦点就集中到这个缓存机制上。这里面有个值得关注的地方在于,大概是出于节省本地硬盘空间的考虑,本地的副本在保存时是压缩过的。于是问题可能出在两个地方:

  • 压缩算法有问题,压缩保存的时候,把文件给弄坏了。
  • 解压缩算法有问题,无法正确还原这个文件。

这套压缩 / 解压缩的算法,是开源的(zlib)。所以我认为问题不应该出在算法本身,更可能是用法没用对。调用代码大概是这个样子的:
#define chunk 16384
void compress_file(const char* source_file , const char* dest_file)
{
    unsigned char datein[chunk];
    unsigned char dateout[chunk];
    unsigned long datelong = chunk;
    unsigned long sourcelong;
    FILE* source;
    FILE* dest;
    source = fopen(source_file , "r");
    dest = fopen(dest_file, "w+b");
    while (!feof(source))
    {
        sourcelong = fread(datein, 1, chunk, source);
        compress(dateout, &datelong, datein, sourcelong, 1);
        fwrite(dateout, datelong, 1, dest);
    }
    fclose(source);
    fclose(dest);
}
void un_compress_file(const char* source_file , const char* dest_file)
{
    unsigned char datein[chunk];
    unsigned char dateout[chunk];
    unsigned long datelong = chunk;
    unsigned long sourcelong;
    FILE* source;
    FILE* dest;
    source = fopen(source_file , "r+b");
    dest = fopen(dest_file , "w");
    while (!feof(source))
    {
        sourcelong = fread(datein, 1, chun, source);
        datelong = chunk;
        if (uncompress(dateout, &datelong, datein, sourcelong))
        {
            fwrite(dateout, datelong, 1, dest );
        }
    }
    fclose(source);
    fclose(dest);
}
这段代码我也不打算在这里分析太多,问题很明显:代码编写的初衷,是想把文件分块处理。但每块数据压缩之后的大小并没有记录在压缩文件中,也没有采取一些诸如分隔符或区块补齐之类的定位措施,所以解压缩的时候实际上是无法忠实地按压缩时的分块来还原数据的。而出问题的那个文件,大小的确是超过了 16384,于是就被弄坏了。

这里就引出了一个问题:为什么要分块?
事实上,如果这段代码没有采用固定长度的 C-style 数组,而是用动态内存分配的解决方案,压根都不会需要分块,也就不会出现这个 Bug。当然,这只是解决这个 Bug 的方案之一。对分块压缩算法的理解有问题,也是造成这个 Bug 的原因之一。从这方面着手进行改进也是可以的,各有利弊而已。
但这不是我要表达的重点。在这个案例里,下载的文件并不会很大,几十 KB 就顶天了。我真正疑惑的地方在于:为什么不用动态内存分配?
可能的解释有:
  • 担心内存碎片问题
  • 担心忘记释放
  • 嫌动态分配内存麻烦
  • 习惯了这种固定长度缓冲区的写法
  • ……
也许还有别的原因,一时半会儿我是想不到了。

那么换个问题:什么时候该用动态内存分配?
这个答案会比较明确一点:
  • 空间大小不确定(运行期确定)
  • 栈上空间不够
  • 方便与线程外部传递 / 分享数据

在本文的这个例子中,文件的长度是不确定的,每块数据压缩后的长度也是不确定的。很明显,这就是属于应该用上动态内存分配的时候。
该用的时候不用,带来的恶果就是程序的可读性和可维护性就会变得差,出 Bug 的机会更高。毕竟固定长度的内存区域就一定要处理溢出问题。而且用固定长度去处理变长内容,要分块 / 分次,要做循环,要留意退出条件,测试时要覆盖 1 和 N……,这些都带来了不必要的开销。
还不如直接分配一块内存出来,只要到时候记得回收就 OK。性能方面值得担心的话,也可以自己优化内存管理,这是可以集中处理掉的事情。而那种用固定长度的栈缓冲区来解决此类问题的办法,好听一点叫做「质朴」,难听一点叫「土」。总不能每个需要动态内存分配的地方,都用这种土办法来应对吧。

我其实是觉得,有些程序员,会有意识(或下意识)地避免用动态内存分配。从写代码的时候就开始重视性能,是好事情,但写程序不能只看功能和性能。你写的程序,好不好懂,容不容易出问题,有没有定时炸弹,好不好改,方不方便扩展,这些也都是很重要的。性能不佳可以优化,这种代码级的性能问题(相比架构级而言)优化起来尤其容易。但其它的方面,要改善起来绝非一日之功。
往开了说,作为程序员,应该避免陷入「某个东西就是不好」的思维方式中。思维开始变得狭隘,是自身没法继续再提高(达到上限了)的标志之一。