细节决定一切——由一个函数声明引发的诡异崩溃

今天LibCHD成功登陆游戏,遂使用Release编译并在VB中调用,结果立崩,开始还以为是VB的问题,但是用C++调用居然也崩,而Debug编译没有出现任何问题。VC挂载后发现崩溃出在recv的thread中,但调用堆栈看不出哪里崩掉的。然后用OD挂载,崩溃现场显示堆栈已溢出,但调用框架还能看出,从堆栈最下面往上翻,总算找到一个返回地址在我的DLL里,看出来是在DynCode解密的调用中崩掉的。语句为

pFuncDecrypt(Pac.Data, Pac.Length - 4);

但是解密为什么会崩掉。我所了解的SDDynDll只有可能在线程不同步,同时进入加解密才会出问题,而这段代码已经是在EnterCriticalSection了,不可能因为线程问题。仔细观察周边情况,封包数据——普通的102号包,12字节,只是12字节后不知为什么全是乱码,突然发现堆栈中返回地址往下两格的位置写着00010008,囧,8字节怎么成了10008了,显然是直接把一个32位的值放进去了。一看汇编,果真如此:

6AB34AC6   8B5424 14        MOV EDX,DWORD PTR SS:[ESP+14]6AB34ACA   8B8E D0104000    MOV ECX,DWORD PTR DS:[ESI+4010D0]6AB34AD0   83C2 FC          ADD EDX,-46AB34AD3   52               PUSH EDX6AB34AD4   8D4424 1C        LEA EAX,DWORD PTR SS:[ESP+1C]6AB34AD8   50               PUSH EAX6AB34AD9   FFD1             CALL ECX

但是上面的语句,Pac.Length是个16位整数,那么第一句的汇编应该是mov edx,word [esp+14]才对啊,难道编译器出错了把一个16位整数弄成了32位?长这么大还没听说萎软的编译器出错的,这可是特大新闻了,不过本着对萎软的崇敬之情,我还是觉得我应该瞅瞅我自己的代码。。结果就还真瞅出了问题:

void (STDCALL* pFuncEncrypt)(uint8* Data, uint16 Length);void (STDCALL* pFuncDecrypt)(uint8* Data, uint16 Length);pFuncEncrypt = ((void(STDCALL*(STDCALL*)(uint32))(uint8*, uint16))m_DynCode)(1);pFuncDecrypt = ((void(STDCALL*(STDCALL*)(uint32))(uint8*, uint16))m_DynCode)(2);

问题出在函数声明上。尽管代码的使用,传入的参数都是int16,但是函数本身却不是为了int16而写的。如果函数本身参数是int16,则函数内部会只使用低16位,就算传入32位的数字,高16位也会被舍弃。但是现在函数设计的是32位的,但声明写的是16位,VC会认为里面不会处理高16位而放心大胆地为了节省时间而不把参数的高16位清空,于是立即出问题,在SDDynDll中表现为堆栈溢出,但即使不溢出,Packet后面的数据也会遭到毁坏(而这种没有立即崩溃的情况会更难以察觉)可以说,玩C++几年了,第一次遇到这种诡异问题,C++真的是无奇不有,幸好咱懂汇编。

有关C++类成员函数的指针调用

今天做libchd中,在写thread的时候,突然厌烦了重写run虚函数的那种thread,而怀念起了vb时代的addressof操作符,于是想能不能通过指针调用类的成员函数做一个通用的thread类,遂开始捣腾。。

首先,想到一个办法,看能否将一个函数指针声明为void(__thiscall *pFunc)(void),然后在类的构造函数中传入一个这样的类型,结果试了&a.Func,编译错误,再试&A::Func,提示void(__thiscall A::*pFunc)()不能转换成void(__thiscall *pFunc)(),失败。。

然后,又想到一个办法,this指针存在ecx寄存器中,能否通过嵌入汇编调用。于是构造函数中传入两个void*,结果编译提示void(__thiscall A::*pFunc)()不能转为void*,“万能指针”在这儿罢工了居然,再次失败。

最后,联想到mem_fun_t和mem_fun_ref_t模板类,遂退一步尝试用模板类解决,终于实现,代码如下:

template<class T>
class MethodThread
    : public Thread
{
public:
    MethodThread(T *pObj, void(T::*pFunc)())
        :m_pObj(pObj), m_pFunc(pFunc) {}
    ~MethodThread(){Thread::~Thread();}
protected:
    virtual bool Run()
    {
        (m_pObj->*m_pFunc)();
        return true;
    }
 
    void(T::*m_pFunc)();
    T *m_pObj;
};

最后小小佩服下模板类的强大~~

C++调用COM组件

COM是复杂的,尤其是对于C/C++这样的和COM无关的语言(VB、Delphi、E语言等快速开发工具都是从内核支持COM/ActiveX的)。

一般情况下,在C++中,一个COM组件的调用要经过以下步骤:

1、包含头文件
2、使用CoCreateInstance创建对象并获取接口指针
3、调用组件方法,夹杂着大堆的类型转换等工作
4、手工管理对象生存周期

代码看起来灰常灰常的复杂。

事实上,VC给我们提供了一种和VB一样直观的方便的调用方法。

如VB中写:

Dim http As New XMLHTTP
http.open "GET", "http://127.0.0.1/", False
http.send
Print http.responseText

在VC中完全可以写成:

IXMLHTTPRequestPtr http(__uuidof(XMLHTTP));
http->open("GET", "http://127.0.0.1/", false);
http->send();
puts(http->responseText);

当然不要忘了CoInitialize,并且文件头要写上

#import <msxml3.dll>
using namespace MSXML2;

之所以这么方便,是由于在#import的预处理中,VC编译器自动做好了用_bstr_t代替BSTR、接口的智能指针等,用户无需去纠结什么AddRef、Release啊,BSTR与char*转换这些一不小心要么是非法访问要么是内存泄漏的事情。

想起当年做彩虹插件系统的时候用WinINet API那叫一个郁闷,COM还是很强大的,但因为COM在VB、Delphi这些语言中太出风头,玩VC的童鞋就会有些郁闷,因为要深入到COM的原理,就不是一般的复杂,就算是在VC中调用,大部分的介绍都是MFC的类向导自动作出的、利用IDispatch的调用,看起来很囧的说,希望这篇文章能让玩VC的同学以后不再那么郁闷。。

我里个去。。以后就用MSXML了。。

今天想到要长远考虑,做一套完善的用户验证系统。

突然就想到XML技术,想把信息的POST做成XML格式,PHP对XML想必支持也很好的。

遂用VB对象浏览器打开msxml6.dll,好多好多的组件。。

突然看到一XMLHTTP类。

…………

于是发现,原来费多大力气的HttpPost验证,一步步调用WinINet API的那种,居然可以到手不费吹灰之力。。

太不可思议了。。。。。。

Private Sub Command1_Click()
    Dim http As New XMLHTTP
    http.open "GET", "http://127.0.0.1/", False
    http.send
    Print http.responseText
End Sub

AngelScript试用记

AS是什么时候发现的呢,很久很久以前,下VcAsm大侠的VProtector(怎么认识VcAsm的大名的呢,问SD的彩虹岛DynCode去。。),想知道他那内嵌的C编译器是怎么搞的,用Depends打开EXE,咦,怎么有几个导出函数,Google一下,得到AS脚本引擎~

这次是突然想卖挂生活技能软件,但想把程序和数据分立,也就是说不管是生活技能还是刷BUG还是强制组队隐身什么的都用同样的软件,而封包的逻辑不再硬编码到程序中,也就是要借助脚本语言。

出名的脚本就那么几个,Python、LUA、Ruby等。因为WOW的缘故,LUA依旧是目前世界上最流行的嵌入脚本(Python太大,可以单独开发了,加之python.org的纠结 – -),解释器体积也小,只有100K,但我怎么看怎么觉得不爽。。不支持面向对象是个原因,最烦的是那语法,C不像C、Basic不像Basic、Pascal不像Pascal的,而AS在这些方面要好的多,支持OO,C++语法,大小400K左右,只是中文文献——不是很少,是根本没有,连E文文献估计也就只有SDK了,看着累了点。

按照Your first script的步骤做了hello world。然后就急不迫待地打开许久前未完成的BUG工具MFC移植版乱搞一气。。(此处省略3000字)

最终成功写这样的脚本成功实现了隐身~还是OO版的脚本哦~

void main()
{
    Packet p;
    p.WriteInt(200000500);
    p.WriteInt(0x8000008);
    p.WriteInt(2);
    p.WriteInt(0);
    PutSend(p.GetText());
}

现在算是熟悉GlobalFunction还有值类型的class了。不过毕竟是托管的,许多东西和NativeC还是差别很大,比如数组和指针的概念。并且前人的测试看来AS的执行速度只有NativeC的1/160(同样的测试LUA是1/60)。不过总的来看还是很不错的,我个人觉得AS很有发展潜力,从文档看来预编译、JIT这些功能都有,只不过要用户自己来实现,而目前研究的人还很少而已。

有关new() T()

在看AngelScript文档的时候,讲到值对象构造函数和析构函数的注册,看到这样的内容:

void Constructor(void *memory)
{
  // Initialize the pre-allocated memory by calling the
  // object constructor with the placement-new operator
  new(memory) Object();
}

这里的new运算符有点怪哈,以前没见过,但只要不是傻子应该都能从上下文猜出含义。不过为了科学的严谨性,还是上网查了一下。这里就是不分配内存,而是直接在memory的地方进行构造。

顺便也把析构贴上。析构代码:

析构比较简单,直接调用析构即可。

void Destructor(void *memory)
{
  // Uninitialize the memory by calling the object destructor
  ((Object*)memory)->~Object();
}

反过来看为什么构造函数不是直接调用构造函数呢。我想应该还有虚函数表啊什么的一大堆乱七八糟事情要在构造函数前调用(事实上,好像有虚函数的类构造函数就是虚函数,至少分析彩虹岛的RTTI信息都是这样的)

参考文章:《new的六种重载形式》

http://dev.firnow.com/course/3_program/c++/cppjs/200827/99702.html

EDIT:用原来WP的插件插源代码指针符号总是会被变成&gt;,下了个Source Code Formatter试试看~~

EDIT2: WLW会给Source Code Formatter的HTML代码自动换行,IE8会使换行也显示出来,要用兼容模式才能正常查看代码

以后多用SAFE_DELETE

前些天羽毛汇报说那天杀的彩虹岛插件服务端又崩了。研究加壳过的程序崩掉后的dmp文件确实是个体力活。从eip来看程序是跑飞了,看了下esp处是个在dll领空内的地址,说明是call飞的,用这个地址减去dll的起始地址,然后用od加载错的dll,把刚才减出的偏移加到这个hmodule上,算是找到了出错的指令了,谢天谢地这段指令没有混淆过。这还没完,要根据这里的汇编码找出对应的源代码位置。。还算比较幸运,这个函数下面一个函数就有了字符串的引用,于是找到代码是OnDisconnect中delete m_session一句出错,从dump来看内存里已经不是SocketSession的内容了,应该是call析构函数call飞的,那么推断下来只可能是两种情况,要不就是哪里缓冲区溢出了,要不就是二次delete。我自己自然能保证我的代码中不会出现重复delete逻辑,但是OnDisconnect这个socket类库却不是我写的,天知道这个庞大的socket框架哪里又出了啥毛病,本来我已经为它改了N多bug了。。

然而如果是delete的问题,这个却不能说不能从我这里防止。因为多年来从vb混到.net,也就是代码一直是被托管着的,永远不会缓冲区溢出永远不会随便一个小问题就退出了函数调用都是微软的不用担心失败等等等等,使得我的程序一直在出错预防和处理上很糟糕,在正式做C++的project后,天啊。。

弄C++的应该都知道一个好习惯,就是free或者delete后立即赋值null,我写程序也不是不常用,只是有的时候记不住而已。

昨天弄D3D,在DXUT framework的dxstdafx.h中看到几个很有趣的宏

#define SAFE_DELETE(p) { if(p) { delete (p); (p)=NULL; } }
#define SAFE_DELETE_ARRAY(p) { if(p) { delete[] (p); (p)=NULL; } }
#define SAFE_RELEASE(p) { if(p) { (p)->Release(); (p)=NULL; } }

在《DirectX 9.0游戏开发编程基础》中框架设计也有类似功能的代码,因为是D3D,似乎用的更多是com中的release。

如果说delete *p; p = NULL;这样写有点不好看的话用上面这个宏写出来貌似还是蛮帅的,嘻嘻。以后就用它了~~

DXUT里还有几个好玩的排错宏,比如V_RETURN等,我猜那个V应该是verify的意思。有机会写套自己的代码异常机制。这些日子真的是被那个dll各种稀奇古怪的bug给整疯了