邪恶的member-function pointer

谁都用过指针。

更别说我和你了。

但是当member-function pointer出现的时候,

你还是要跪倒!

嗯……今天耗了些精力d掉一个老bug。症状是这样的,从某人处拷贝来的static lib链接到应用程序之后,lib里面的函数尝试执行应用程序传过去的callback,结果没反应。

传入的callback是一个包含了某member-function pointer(MFP)的结构体,大致定义如下:

typedef void (Object::*FunctionPtr)(void);

struct Callback {
char * szName;
FunctionPtr fp;
}

库函数大致为这个样子:

void foo(Callback* ptr, Object* pObj) {

if(pObj && ptr->fp)
(pObj->*(ptr->fp))(); //怎么这么难看,我写对了伐……

}

开始以为是什么参数错误,后来惊奇的发现在本地重新编译一次static lib,再链接就一切正常僚。既然这样肯定是编译出来的asm有问题,略去痛苦的过程,直接进入主题,一路跟过去发现了不同。在错误的lib中,ptr->fp的地址是ptr+0x8h,而正确的lib里面是ptr+0x10h。于是去看了一下我本地编译出来的sizeof(Callback),猜猜是多少?

答案是32。

为啥这么大啊,于是又求了下sizeof(fp)。猜猜看这次又是多少?

答案是16。

本来以为fp至多再存个虚表,更何况我这个是non-virtual的,不要虚表,结果没想到搞出这么大一坨。这样看来,因为要对齐所以整个Callback结构体被搞成了32字节,这个char*可真是浪费呢。由此推出错误的lib里面是把fp当做8byte大小来储存的,所以ptr->fp就是ptr+0x8h。(好吧,这一段的分析在事后证明是错的,fp总是16byte,出错是因为错误的对齐规则,前者错误的使用了#pragma pack(16)的规则,邪恶的vc6.0 sp5……)

比较巧合的是,由于传入的Callback*来自一个储存在mem pool里的数据,而这个mem pool是由自己的内存分配器来管理的,一开始会把pool里的数据全部写0,所以错误的指针恰好指向了一段为0的内存,阴差阳错的通过了if判断,所以没有报access violation。

最重要的是为啥会有这么大的fp?放狗查到一篇文章,《Member Function Pointers and the Fastest Possible C++ Delegates》,原来这么复杂。其实我以前还用过这个fastdelegate,但是完全没注意到文章中有个描述不同编译器下的member-function pointer大小的表格。真是热闹啊,从4到20无奇不有……回头看看我的环境,应该是由于MSVC的编译选项不同导致了大小不同。具体是被当做那种情况处理了还得再仔细研究下……

裸用member-function pointer真是罪孽啊,还是封一层比较好。比如boost::bind那种东西……

嗯,总之是爽了。

算你狠

第一次突然掉电,我忍了。

第二次突然掉电,我在Buzz上吐了个槽。

第三次突然掉电……好吧,我败了……我去睡觉还不行么。

莫非是因为大家都开空调了?还好第二次掉点之后我就把硬盘电源拔了,要不然可能会悲剧。哼哼哼哼。

记录一个FPU的猥琐Bug

貌似n多人遇到过,知道问题的原因之后用合适的关键字可以搜出很多帖子。核心问题归结为:float单精度浮点数有效位数不足,而FPU控制寄存器CTRL的bit8和bit9被置为0时,使得FPU被强制工作在单精度(24bit)模式,导致结果出错。

起因是一个朴素的string转浮点数函数,由于输入数据的范围被设定在0~1,000,000且包含两位小数,所以最初的版本使用float储存结果就会导致精度不足。发现问题之后,第一反应就是采用double来储存,理论上不会出错了。结果神奇的事情发生了,改成double之后精度还是不足。

接下来省略掉各种无用处的猜想,再去掉其他的逻辑,这个函数其实就这么一行:

double foo( int input ) { return (double)input / 100; }

简化的症状就是,我调用foo(100,000,000)期待返回1,000,000.01,实际返回的是1,000,000.00

当我尝试在程序最开始调用这个函数,例如放到main的最前面,结果是正确的。如果初始化完其他模块之后,结果就不对了。这样重现n次之后,脑子顿时就不转了,居然存在同一个函数同样的输入但输出不同!我确定这个函数木有inline,就算有啥优化,跑起来的汇编指令也完全一样的说。所以开始考虑是不是thread context有什么不同导致……其实这时候如果对FPU的工作机制比较熟,肯定马上就可以想到。可惜我早就把这种东西还给课本了,只好傻乎乎的肉眼看寄存器有什么问题。

最初想复杂了,脑子一抽想到了以前看过云风一篇帖子,模糊记得是什么东西导致浮点运算出错。搜出来一看,原来人家说的是编译器的问题,比我这个严重的多。不过因为有这么个模糊的印象,又通读了一遍这个帖子,接下来就放在研究FPU那坨诡异的寄存器上面了。

对比发现那条浮点除法指令FDIVR调用过后,结果正确的那次STAT寄存器有所变化,bit8置为了1。google之了解到bit8是如果为1,一切正常,如果为0,代表运算精度不足,FPU对结果做了roundup,大概就是这个意思吧。看来确实是精度问题,但double肯定是完全能存下我要的结果的说……疑问没有消除,继续肉眼看半天,又发现二者的CTRL寄存器也有所不同:正确的那次是0x27F,错误的那次是0x07F。马上google之,果然查到CTRL的bit8和bit9是用来控制运算精度的,(CTRL&0x300)之后,默认状态下应该为0x200,表示双精度(53bit),如果是0x000则表示单精度(24bit)。

好吧,如此看来,第一次调用FPU的时候,CTRL还是用双精度,第二次就单精度了,怪不得出错。问题是这个东东怎么会改变呢?继续google,换了几次关键字终于找到,居然是D3D的CreateDevice干的。DX的文档《Top Issues for Windows Titles》一节中有说,如果没有设置D3DCREATE_FPU_PRESERVE,则FPU被强制设成单精度。更加诡异的是这个东西的影响是per-thread,其他的thread如果没有碰过FPU的CTRL,那么还是默认的精度。为何D3D要做这种处理?彰显它的浮点运算能力?我不是很理解。再次回到程序中验证,的确是图像模块的Init被调用之后,CTRL就奇迹般的从0x27F变成了0x07F了。

解决方案就不说 了,总之问题原因探究到此为止。果然,就如同江湖上传闻的那样,使用浮点数存在各种诡异陷阱。这次悲剧的debug之后,最令人欣慰的一点是让我对float这个该死的单精度浮点数更加敏感……虽说丫是32bit的,其实也就7位有效数字,唉……

那帮人

翻出仙四看credit,然后开古剑看credit。

听说当年仙四的预算很少,开发过程应该不会很享受吧。也许曾经怀着出个“大作”的梦,结果折腾了半天连公司都散了,应该会有很多苦闷吧。

但还是那帮人,就这么折腾了2年多,又弄了个古剑。

本来对古剑各种失望,各种不满,看着两份几乎一样的credit,突然就不想喷了。

喵了个咪的,只有爱果然是不行的啊。

月志加一

基本上一个月没更新了。其实就是一个月没更新了。但那又怎么样嘛~╮(╯_╰)╭

入了好几本《独唱团》,到手了就散出去。估计有些人想看但是懒得买吧,那我就主动传播一下~

最近天气热了,各种效率变低,决定减少刷reader和twitter的频率。看了几本推理小说和科幻之后,对自己脑中yy的那个设定又燃起来了,有机会一定要完善一下。

晚上看到CD同学对《Dragon Age》乐此不疲的样子也捡起来玩了玩,为了加快进度载了一份某处制作的汉化包,结果有些失望。开场那么简单的句子,就翻译错了,是缺少爱的人制作的么……

“But those, who once called us heroes, have forgotten”

给译成了“那些被我们叫做英雄的人已经忘记”,但这句是“把”的意思吧……

“Maker help us all”

译成“造物主帮助我们很多”,应该是恳求的语气说“造物主请帮助我们大家”的意思吧……

哎呀呀,结果还是只能卸载掉这个民间汉化包么=_=b