2006年10月19日

从下列地址下载SquidNT (http://squid.acmeconsulting.it/download/dl-squid.html)。选择2.5.STABLE14-NT Delay Pools 版本,以下配置是针对2.5版的Squid,2.6版的配置略有不同。
将压缩包解压缩到C:\Squid。将文件C:\Squid\etc\squid.conf.default另存为C:\Squid\etc\squid.conf。

现有如下环境:两台WEB服务器,服务器A和服务器B,分别拥有2个独立的公网IP。服务器A上准备安装反向代理,并且要保留服务器A上原有的IIS服务。反向代理在绑定在80端口,通过服务器A的80端口可以访问服务器A和服务器B上的WEB服务。现有如下两种解决方案:
1、服务器A上,将SquidNT运行在80端口,WEB服务运行在81端口。但是这样会导致WEB服务器Redirect的时候暴露真实端口影响正常访问。当访问http://target/path,会跳转成http://target:81/path/
2、将Squid和IIS分别绑定到不同的IP,Squid运行在公网IP上,IIS站点运行在127.0.0.1上。但是IIS5.0有个BUG,就是不管你设置站点绑定到什么IP,他都会抢先占绑定所有的IP地址。所以必须让Squid先于IIS运行,先于IIS运行在127.0.0.1:80上。
现在采用第二种方案,并通过修改注册表将Squid设置为IIS的Dependence服务,使得Squid先于IIS启动。

打开配置文件Squid.conf,进行下面的设置:

httpd_accel_host virtual  #因为要代理多个WEB服务器,所以开启虚拟主机功能
httpd_accel_port 80
httpd_accel_with_proxy off  #关闭正常代理功能
httpd_accel_uses_host_header on

http_port 61.121.xxx.60:80  #反向代理监听端口,帮定到公网IP
icp_port 0    #关闭icp
htcp_port 0    #关闭htcp
snmp_port 0    #关闭snmp
hosts_file c:/squid/etc/hosts.conf #设置hosts文件进行方向代理的域名解析

hosts.conf格式如下:

IP1 DNSName1
IP2 DNSName2

 

DCOM 遇到错误“无法启动服务,原因可能是它被禁用或与它相关联的设备没有启动。 ”,试图以参数“”启动服务 MDM 以运行服务器:
{0C0A3666-30C9-11D0-8F20-00805F2CD064}

问题原因:Machine Debug Manager服务被禁用

解决方法:
1、开启Machine Debug Manager服务
2、运行“dcomcnfg.exe”,在应用程序一栏中找到“Machine Debug Manager”,打开属性,切换到到“位置”选项卡,取消“在这台计算机上运行应用程序”。
3、关闭DCOM。设置注册表“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Ole”下键“EnableDCOM”值为“N”

2005年06月04日

[删除自带的messager 4.7的方法]
运行中输入下面的命令
RunDll32 advpack.dll,LaunchINFSection %windir%\\INF\\msmsgs.inf,BLC.Remove


首先要保证MSN Messenger工具—选项—常规中的“连接到Internet时允许自动登录”和“当我登陆到windows时自动运行Messenger”处于uncheck状态。然后


[消除打开IE时自动启动MSN Messenger的方法]
键名:[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Internet Explorer\\Extensions\\
{FB5F1910-F110-11d2-BB9E-00C04F795683}]
方法:选择此键,右键,改名为
-{FB5F1910-F110-11d2-BB9E-00C04F795683}
即加一个\’-\’,反之则恢复

[消除登陆hotmail就自动启动MSN Messenger的方法]
打开注册表,定位到
HKEY_CLASSES_ROOT\\CLSID\\{4F07F79F-087F-42cf-8B36-7A88D06088E9}\\InprocServer32
把msgsc.dll改名为hidemsgsc.dll

[消除打开OE时就自动启动MSN Messenger的方法1]
1.在Outlook Express里设置:工具—选项—其他,不选“自动登录MSN messenger”
2.注册表中搜索{FB7199AB-79BF-11D2-8D94-0000F875C541} , 将里面的LocalServer32键值清空。

[消除打开OE时就自动启动MSN Messenger的方法2]
键名:[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Outlook Express]
健值名:Hide Messenger(没有则新建之)
类性:REG_DWORD (其值为DWORD)
值:2=启用

2005年05月21日

以前使用Win32 SDK编程时,经常会遇到回调函数。比如,枚举窗口函数EnumWindows API就用到回调函数。回调函数的本质就是将一函数指针传递给API函数(或者其它什么函数),在API函数内部会根据这个函数指针来调用这个函数。委托的实质也类似于回调函数,不过这是OOP中的专用说法而已。回调函数的作用就相当于委托模型中的代理(delegate)。

在BCB中,使用了__closure关键字扩展了标准C++,从而实现了委托:

typedef void __fastcall (__closure *FCallBack)(int);

class CDelegate
{
public:
    void __fastcall Callback(int param)
    {
        ShowMessage("Call Times: "+String(param));
        return;
    }
};

class CTest
{
public:
    FCallBack pcallback;
    CTest()
    {
        CDelegate delegate;
        pcallback=delegate.Callback;
        for(int i=0;i<10;i++){pcallback(i);}
    };
};

然而,在标准C++中不支持委托,只提供成员函数指针(member function pointers)。这个东西用起来繁琐,写出来的代码猥琐:

class CCallBack
{
public:
    void Callback(int param)
    {
        printf("Call Times: %d\n",param);
        return;
    }
};

typedef void (CCallBack::*FCallback)(int);

class CTest
{
public:
    FCallback pcallback;
    CTest()
    {
        CCallBack cb;
        pcallback=&(CCallBack::Callback);             //比较:delegate.Callback;
        for(int i=0;i<10;i++){(cb.*pcallback)(i);}    //比较:pcallback(i);
    }
};

使用"__closure"关键字,没有对象类型的限制;而使用成员函数指针必须在声明时就确定对象类型。换句话说,成员函数指针不支持透明调用。其实,实现成员函数的透明调用的汇编代码十分简单。来看看BCB的"__closure"是如何实现的。先来猜猜下面代码用BCB编译后的运行结果:

#include <stdio.h>

void (__closure *p)(int);

int main(int argc, char* argv[])
{
    printf("%d\n",sizeof(p));
    return 0;
}

该代码的功能是打印一个“闭包(closure)指针”的大小。结果是什么?是8。出乎意料么?看来这并不是一个普通的指针。那多出来的4字节究竟储存了什么?在解释这个问题前,先要说说对象成员函数是如何调用的。众所周知,普通函数调用,如__stdcall,__cdecl等,是先将参数逐一压入堆栈,然后使用call指令条转到目标函数中执行。而C++中调用对象成员函数采用__thiscall调用方式,先将参数逐一压入堆栈,然后将对象的this指针放入ecx寄存器,最后在调用call指令。所以调用一个对象的成员函数不但要知道成员函数的地址,还要知道对象的地址。描述闭包指针内存布局的C伪代码如下:

//执行闭包指针的赋值语句时会填充这个结构
typedef struct{
    void *ptr_this;    //存放对象的this指针
    void *ptr_mfunc;   //存放成员函数的地址
}PTR_CLOSURE;

知道了原理,就可以用标准C++模拟这个实现。只需要用两个变量来分别保存代理对象的地址和代理对象回调函数的地址就可以了。然后用内嵌汇编代码来模拟__thiscall调用。不过在实际操作中碰到了一点问题:编译器死活也不让我得到成员函数的地址,似乎只可以将成员函数地址赋值给成员函数函数指针。无论是用(void*)强制转换,还是xxxx_cast转换,都无法将成员函数地址赋值给一个void*类型的变量。最后,不得不采取了一些非常常规段才达到目的:

#include <stdio.h>

class CDelegate
{
public:
    void Callback(int param)    //回调函数
    {
        printf("this:0x%08X – param:%d\n",this,param);
        return;
    }
};

class CTest
{
private:
    void *pObjs;        //指向代理对象的指针
    void *pCallback;    //指向代理对象回调函数的指针
    void Delegate(int param){
        void* tmp1=pObjs;
        void* tmp2=pCallback;
        __asm{
            push ecx        ;保存this指针
            push param      ;压入回调函数的参数
            mov  ecx,tmp1   ;放入pObjs的this指针
            call tmp2       ;调用CDelegate的成员函数
            //add  esp,4    (*注)
            pop  ecx        ;恢复this指针
        }
    };
public:
    CTest()
    {
        pObjs=new CDelegate;
        typedef void (CDelegate::*FCallback)(int);
        (FCallback&)pCallback=&CDelegate::Callback;    //利用引用来强制类型转换
        for(int i=0;i<10;i++){
            Delegate(i);    //试试效果
        }
    };
};

CTest test;
int main()
{
    return(0);
}

(*注):在John Robbins的《应用程序调试技术》一书中,将__thiscall调用描述为“调用者平衡堆栈”,而我实际调试下来却发现是“调用对象平衡堆栈”。所以不需要"add  esp,4"操作。

用上面的代码能实现高效的委托,但是也存在一个致命的弱点——它不支持多重继承的代理类。也就是说,CDelegate中的Callback成员函数必须是由该类自己实现,或是从别的类中单一继承的。究竟为什么会产生这个问题还必须从C++的对象模型说起。

对于VC编译器,一个对象的所占内存是由虚函数表指针和非静态成员变量组成的。成员函数通过对象的this指针加上偏移量来访问成员变量。当一个类被继承时,编译器会将基类和派生类所占的内存空间合并。问题就此产生了:基类的成员函数并不知道他已经被继承,仍旧使用原来的偏移量。当一个类被单一继承时,编译器童过将基类对象放在派生类对象的首部来解决这个问题。这么一来,基类的成员函数仍旧能够使用原来的偏移量来访问基类对象的成员变量。但如果是多重继承的话,必然会有一个基类对象的偏移量会发生改变。例如:

class A{
    int ma;
};
class B{
    int mb;
    void funcb();
};
void B::funcb(){mb=1;}
class C:public A,public B{
    … …
};

在这种情况下,编译器只能将A放在C的首部,而B只能跟在A的后面了。这么一来,派生类C的成员函数funcb就不能通过原来的this指针加偏移量来访问成员变量mb了。如果有如下调用:

C cobj;
cobj->funcb();

编译器会自动修正this指针,使得传递的this指针指向cobj中正好是B对象的位置。也就是说,此时在funcb内部的this指针不指向cobj的首地址,而是指向cobj的内部,准确的说是cobj首地址偏移一个sizeof(A)的位置。不单单是多重继承,虚继承也会碰到类似的问题。

对于BCB的闭包指针,编译器会修正存放在PTR_CLOSURE结构中的this指针。但是在我们实现的代码中,this指针只是死板地指向对象的首地址。如果我们在写代码的时候去手工修正它显然是愚蠢的。我们只好保证代理类不是多重继承或是虚继承的,至少要保证用于回调成员函数不是从别的基类中多重继承来的。毕竟,MFC中的类也不支持多重继承的,那就将究着用吧。

2005年03月27日

怎样判断一个指针指向的对象是否已经被析构

几天前看到CSDN上有人问起这个问题。原贴见:http://community.csdn.net/Expert/TopicView.asp?id=3875259。因为我最近也在学习C++,所以对这个问题产生了兴趣。在这篇文章中,我会对该贴中提出的一些错误方法进行逐一分析,并给出解决这个问题的一点建议。

正如该贴中的有些回答所提到的那样——指针delete后,最好将它赋值为NULL。这样方便在以后通过检查该指针来确定对象是否已经析构。但在某些情况下,我们会将会将一个对象的地址赋于多个指针变量。这样一来,在delete对象时,很难做到将所有指向该对象的指针全部赋值为NULL。

首先,有人提出用IsBad*系列的Debug API来判断指针指向的内存是否可读写。当然,这种方法是不可行的。C++对象的内存是分配在栈或者堆中的。不管何种情况下,栈内存永远是可读写的。而在系统堆中分配的内存的一个特性就是无法直接干预内存分页的提交和回收。也既是说,用new操作符分配的内存,即使delete后,也无法预料该内存是否被系统回收,它仅仅被系统标记为未使用而已。所以IsBad*系列API是无法判断某指针指向的对象是否已经被析构。

有人又提出了使用使用C++的错误处理来处理异常。这也是不可行的。除非该对象包含虚函数,并且你调用了虚函数,才有可能发生异常。而在大多数情况下,使用一个被析构的对象并不会发生异常。但是你的代码会影响到程序中的其他的数据,最终导致程序的运行结果异常。

还有人提出了如下的方法:

class cfoo
{
public bool isValid()
{
    if (this != NULL)
        return true;
    else
        return false;
}
}

这种方法显然是可笑的。此人估计是个新手(当然,我也是新手)。对象内部的this指针时由调用者决定的。调用者在调用成员函数前,会将当前对象的地址保存到ECX寄存器。成员函数内部便使用ECX寄存器中保存的值来作为this指针的值。下面的代码也许可以让你更深刻得理解:

#include <stdio.h>

class ctest
{
public:
 void PrintThisPtr();
};

void ctest::PrintThisPtr()
{
 printf(“0x%08X\n”,this);
}

int main()
{
 ctest *p=(ctest*)0×12345678;
 p->PrintThisPtr(); //It will print “0×12345678″
 /*————————
 这句代码的汇编形式为:
 mov ecx, 12345678H
 call ctest::PrintThisPtr
 ————————–*/
 return 0;
}

最后又有人提出了一个方法:

class foo
{
public:
   foo():m_Valid(TRUE){}
   ~foo(){m_Valid = FALSE;}
   BOOL IsValid()
   {
      try
      {
         if(m_Valid)
            return TRUE;
         else
            return FALSE;
      }
      catch(…)
      {
         return FALSE;
      }
   }

private:
   BOOL m_Valid;
};

可惜,这个方法仍旧不可行。在一个对象被析构后,该对象原先占用的内存就被标识为未使用(堆和栈的做法是不一样的)。在程序运行一段时间后,原先的这块内存会被一些无法预见的数据所覆盖,m_Valid的值也无法保证其正确性。后来,提出该方法的人又改进了代码:给m_Valid赋一个特殊的值,比如0xEFEFEFEF。这仅仅是一个讨巧的办法,你也许可以在混乱编程大赛中使用该方法,但是你不能在正式的工程中使用这个方法。这个方法无法保证100%不出问题。一个最容易想到的反例就是:在这块内存中,又分配了一个新的foo对象,新对象的m_Valid成员和旧对象的重合。

经过前面的分析,能够得到一个结论:C++对象的指针指向的仅仅是块普通的内存区域,和C语言中的结构体没有什么区别。所以,要判断一个指针指向的对象是否已经被析构必须借助于不依赖对象的外部标记才能实现。一个经常使用的方法就是双重指针。直接指向对象的指针仅有一个,引用对象必须使用一个指向该指针的双重指针。当对象析构时,将那个直接指向对象的指针赋值为NULL。其他的双重指针就可以根据这个指针的值来判断对象是否被析构。

#include <stdio.h>

class CTest{
public:
    int m;
};

int main()
{
CTest *handle=new CTest;
CTest** p1=&handle;
CTest** p2=&handle;
CTest** p3=&handle;
delete handle;
handle=NULL;
if(NULL!=handle)
{
(*p1)->m=100;
(*p2)->m=200;
(*p3)->m=300;
}else{
printf(“对象已经析构”);
}
return 0;
}

这个直接指向对象的指针在此处就起到了句柄的作用。还有一种方法:

#include <stdio.h>

class CTest{
public:
    CTest(bool *pIsValid)
    {
        *pIsValid=true;
        m_pIsValid=pIsValid;
    };
    ~CTest()
    {
        *m_pIsValid=false;
    }; 
private:
    bool *m_pIsValid;
};

bool IsValid;

int main()
{
 CTest *ptr=new CTest(&IsValid);
 delete ptr;
 
 if(IsValid)
 {
  printf(“对象还存在”);
 }else{
  printf(“对象已经析构”);
 }
 return 0;
}

每创建一个对象,必须有一个IsValid对应。如果要动态创建多个对象的话,IsValid也需要动态创建,并且IsValid的生存时间必须大于它标示的对象。但是IsValid的生存时间也不能是永久的,这样就产生内存泄漏了。

不管采用什么方法,必须依靠外部标示来表示对象的生存期,而且要尊循以下原则:每个对象必须与唯一的外部标示对应;每个外部标示也对应唯一的对象;外部标示的生存时间必须大于对象的生存时间;当确定不再需要查询对象的生存期时,应当释放外部标示。如果情况复杂的话,最好实现句柄表来查询。

#include <stdio.h>
#define HT_LEN 256
class foo
{
public:
 foo(){};
 ~foo(){};
 void print(){printf(“%d\n”,m);};
 int m;
};

typedef struct
{
 unsigned int handle;
 foo *ptr;
} HTABLE;

HTABLE g_HandleTable[HT_LEN];
unsigned int g_MaxHandle;

foo* GetObject(unsigned int handle)
{
 int i;
 for(i=0;i<HT_LEN;i++)
 {
  if(handle==g_HandleTable[i].handle)
   return g_HandleTable[i].ptr;
 }return NULL;
}

bool DeleteObject(unsigned int handle)
{
 int i;
 for(i=0;i<HT_LEN;i++)
 {
  if(handle==g_HandleTable[i].handle)
  {
   delete g_HandleTable[i].ptr;
   g_HandleTable[i].handle=0;
   g_HandleTable[i].ptr=NULL;
   return true;
  }
 }return false;
}

unsigned int CreateObject()
{
 int i;
 for(i=0;i<HT_LEN;i++)
 {
  if(0==g_HandleTable[i].handle)
  {
   g_HandleTable[i].ptr=new foo;
   g_HandleTable[i].handle=++g_MaxHandle;
   return g_HandleTable[i].handle;
  }
 }return 0;
}

int main()
{
 unsigned int hfoo=CreateObject();
 foo *p=GetObject(hfoo);
 if(NULL!=p)
 {
  p->m=100;
  p->print();
  printf(“HANDLE VALUE:%d\n”,hfoo);
 }else{
  printf(“对象不存在”);
 }
 DeleteObject(hfoo);
 return 0;
}

以上代码还比较简陋,还可以慢慢完善。

2005年03月20日

VB中的函数或过程的参数有2种传递方式:一种是值传递;一种是引用传递。分别用关键字ByVal和关键字ByRef指出。如果参数是以引用传递的话,函数或过程内部就可以靠这个引用参数来改变外部变量的值。在C语言中,如果要实现在函数内部改变外部变量的值的话,就应该传递这个变量的指针。如果要通过指针访问变量,必须使用指针运算符“*”。这样在源代码中就会显得比较别扭:

void function(int *pval)
{
 *pval=100;
 //pval=100;先不考虑此处类型转换的错误
 //该代码只能改变堆栈中临时指针变量的地址,而不能改变指针指向对象的值
}

int main()
{
 int x=200;
 function(&x);
 return 0;
}

为了能透明地使用指针来访问变量,C++中引入了“引用”的概念:


void function(int &refval)
{
 refval=100;
}

int main()
{
 int x=200;
 function(x);
 //当然,如下调用也可以。但这样做就失去引入”引用”的原本意义了
 int &refx=x;
 function(refx);
 return 0;
}

这样一来,只要改一下函数声明,就可以在源代码的级别上实现指针访问和一般访问的一致性。可以把“引用”想象成一个不需要“*”操作符就可以访问变量的指针。上面的代码的C语言形式的伪代码:

void function(int *refal)
{
 *refval=100;
}

int main()
{
 int x=200;
 int *refx=&x;
 function(&x);
 function(refx);
 return 0;
}

根据函数的声明,C++编译器在遇到“function (x);”语句时,会自动转换成“function(&x);”形式的二进制代码。

来看“int **pp”和“int *&rp”区别。前者是一个指向指针的指针;后者是一个指针的引用。如果这样看不明白的话,变换一下就清楚了:

typedef int * LPINT;
LPINT *pp;
LPINT &rp;

下面这两个函数的二进制代码是一致的:

void function1(int **p)
{
 **p=100;
 *p=NULL;
}

void function2(int *&ref)
{
 *ref=100;
 ref=NULL;
}

在调用function1或function2时,编译器编译的二进制代码都将传递一个双重指针。

“引用”仅仅是为了给重载操作符提供了方便之门,其本质和指针是没有区别的。

2005年03月05日

有时,我为了编译仅有一个源文件的C/C++代码,不得不建立一个VC的工程,工程编译后会生成一堆文件,我不需要搞得如此庞大。后来我改用命令行编译,但是我需要输入冗长的参数来编译。为了一劳永逸地解决这个问题,我就试着将我常用的EditPlus设置为编译器的IDE。

只要修改一下VC安装目录下的”C:\Program Files\Microsoft Visual Studio\VC98\Bin\VCVARS32.BAT”文件,然后在EditPlus的“用户配置工具”中“添加工具”就可以了。

“VCVARS32.BAT”这个批处理文件的作用是设置编译器的环境变量,将该文件复制到EditPlus的安装目录下,在文件末尾添加如下命令:

@echo on
@cl %1 /ML /GX /O2 /D “WIN32″ /D “NDEBUG” /D “_CONSOLE” /D “_MBCS” /link “kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /incremental:no /machine:I386″

如果你还要生成asm中间代码的话可以在此处添加”/Fa”参数。

在EditPlus中添加一个用户工具,名叫“C/C++ 编译”。设置“命令”为”C:\Program Files\EditPlus 2\VCVARS32.BAT”,这个文件就是刚才修改过的VCVARS32.BAT文件。然后设置“参数”为”$(FilePath)”;“初始目录”为”$(FileDir)”即可。现在只要在EditPlus中按下Ctrl+1(如果没有设置其他用户工具的话)就可以编译C/C++了。

不过VC编译器默认编译的EXE的”Subsystem”是”console”。如果要编译windows程序,必须在源代码中添加:

#pragma comment(linker,”/subsystem:windows”)

现在编译后仅仅生成一个obj和一个exe文件。好了,这个世界清静多了。

2005年02月18日

最近写的一个软件需要用到类似Flashget那样的收集篮,能接收IE的拖放,所以要在程序中实现一个IDropTarget的COM接口。我一直喜欢用pure C & SDK来写程序,但是这次为了完成这个COM接口,不得不使用C++来写(当然,用pure c也可以,但是定义一个模拟C++类的结构太麻烦了)。

以前用C+SDK写程序时,为了减小程序的体积,我都用”/Entry”参数来定义程序入口的(过去写木马程序时保留的坏习惯),这次也一样:

#pragma comment(linker,”/entry:WinEntry”)
#define hInst ((HINSTANCE)0×00400000)
… …
HWND g_hMainWnd;
CDropTarget DropTarget;
… …
void WinEntry(void)
{
    MSG msg;
    OleInitialize(NULL);
    g_hMainWnd=CreateDialog(hInst,MAKEINTRESOURCE(IDD_MAIN),GetDesktopWindow(),(DLGPROC)MainProc);
    if(NULL==g_hMainWnd)goto End;
    DropTarget.RegisterDragDrop(g_hMainWnd);
    ShowWindow(g_hMainWnd,SW_SHOW);
    while(GetMessage(&msg,NULL,0,0)){
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
End:
    DropTarget.RevokeDragDrop();
    OleUninitialize();
    ExitProcess(0);
}

其中的CDropTarget是我完成的封装IDropTarget接口的类,定义如下:

class CDropTarget:public IDropTarget{
public:
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void **ppvObject);
    virtual ULONG   STDMETHODCALLTYPE AddRef(void);
    virtual ULONG   STDMETHODCALLTYPE Release(void);
    virtual HRESULT STDMETHODCALLTYPE DragEnter(IDataObject *pDataObject,DWORD grfKeyState,POINTL pt,DWORD *pdwEffect);
    virtual HRESULT STDMETHODCALLTYPE DragOver(DWORD grfKeyState,POINTL pt,DWORD *pdwEffect);
    virtual HRESULT STDMETHODCALLTYPE DragLeave(void);
    virtual HRESULT STDMETHODCALLTYPE Drop(IDataObject *pDataObject,DWORD grfKeyState,POINTL pt,DWORD * pdwEffect);
    BOOL RegisterDragDrop(HWND hWnd);
    BOOL RevokeDragDrop();
private:
    ULONG m_RefCount;    //引用计数
    HWND  m_hTargetWnd;    //注册为拖放窗口的窗口句柄
};

BOOL CDropTarget::RegisterDragDrop(HWND hWnd)
{
    HRESULT hResult=::RegisterDragDrop(hWnd,this);
    if(!SUCCEEDED(hResult))return FALSE;
    m_hTargetWnd=hWnd;
    return TRUE;
}

程序一开始先用OleInitialize函数初始化OLE库,然后调用RegisterDragDrop函数将程序主窗口注册为接受拖放的窗口。一切皆按照MSDN中所描述的那样进行,好像没什么错误。可是RegisterDragDrop函数总是返回”E_INVALIDARG”。我把代码从头到底看了半天,没有发现有什么错误。起初,我怀疑我的IDropTarget接口实现得有问题,但是检查后发现,直到RegisterDragDrop函数返回为止,CDropTarget中的所有成员函数根本没有被调用过。直到偶然瞥见MSDN中的一句话:”The RegisterDragDrop function also calls the IUnknown::AddRef method on the IDropTarget pointer.”。也就是说RegisterDragDrop函数执行过程中没有调用CDropTarget的AddRef成员函数是不正常的。看来必须用调试器跟到RegisterDragDrop中去看看才能知道究竟错在哪里。我这个人比较懒,最讨厌看汇编代码了。哎,没办法,这次只好用ollydbg一步步跟踪看看了。

跟到RegisterDragDrop函数里面,发现该函数分别调用IsValidInterface和IsWindow函数对传入的两个参数的有效性进行检查。当RegisterDragDrop调用IsValidInterface函数对IDropTarget指针进行检查时,发生访问违例,由于SEH保护,程序才得以继续运行。晕,难道还要跟到IsValidInterface里去看?

先到MSDN里查查IsValidInterface的描述,看看有什么有用信息。不曾想,MSDN里对IsValidInterface的描述只有一句话:”This function is obsolete.”。日啊,过时了你还用!操比尔他大爷的。

跟到IsValidInterface里一看,发现DropTarget的虚函数表指针竟然是NULL。超级晕!这才意识道,原来是DropTarget对象的没有初始化。也就是说,声明DropTarget对象时没有执行构造函数。对于VC编译器来说,一个包含有虚函数成员的类的默认构造函数的唯一作用就是初始化虚函数表指针。由于我用”/entry”编译参数定义程序的入口地址,跳过了RTL的初始化代码,导致在编译时,全局对象只分配了内存,而没有执行构造函数。

小贴士:对于C/C++语言来说,main或WinMain函数并不是程序真正的入口函数。在执行main或WinMain函数前,程序将先执行编译器生成的RTL代码。这部份的代码的作用包括:解析命令行参数,初始化全局对象等。

解决方法有2个:
1、声明全局对象的同时,不要自定义程序入口。
2、如果自定义程序入口的话,将对象放到入口函数中声明。

结论:我这个人不适合用太高级的东西。

2005年02月15日

上次说了一个C++对象在内存中的实际形式,现在来说说C++中以对象为参数或返回值的函数是如何实现的。在此之前如果你对函数调用的汇编形式毫无概念的话可以先看看这篇文章:http://www.20cn.net/ns/wz/sys/data/20040208183412.htm

来看下面的代码:

class test            //sizeof(test) is 24
{
public:
    int m1;
    int m2;
    int m3;
    int m4;
    int m5;
    int m6;
};

test function1()
{
    test cls2;
    return cls2;        // Line 15
}

int function2(test temp)
{
    temp.m1=1;        // Line 20
    return 0;        // Line 21
}

int main()
{
    test cls1;
    cls1=function1();    // Line 27
    function2(cls1);    // Line 28
    return 0;
}

由于VC编译的未优化代码和优化后的代码相差比较大,所以在编译时增加”/02″参数,使用命令行”cl test.cpp /Fa /02″对源文件进行编译。得到的中间汇编代码如下:

PUBLIC    ?function1@@YA?AVtest@@XZ            ; function1
;    COMDAT ?function1@@YA?AVtest@@XZ
_TEXT    SEGMENT
_cls2$ = -24
$T295 = 8
?function1@@YA?AVtest@@XZ PROC NEAR            ; function1入口
; File main.cpp
; Line 15
    mov    eax, DWORD PTR $T295[esp-4]        ; 将cls1的地址作为返回值保存到eax中
    sub    esp, 24                    ; 分配cls2的内存
    mov    ecx, 6
    push    esi
    push    edi
    lea    esi, DWORD PTR _cls2$[esp+32]
    mov    edi, eax
    rep movsd                    ; 将cls2对象复制到cls1中去
    pop    edi
    pop    esi
; Line 16
    add    esp, 24                    ; 00000018H
    ret    0
?function1@@YA?AVtest@@XZ ENDP                ; function1结束
_TEXT    ENDS
PUBLIC    ?function2@@YAHVtest@@@Z            ; function2
;    COMDAT ?function2@@YAHVtest@@@Z
_TEXT    SEGMENT
?function2@@YAHVtest@@@Z PROC NEAR            ; function2入口
; Line 21
    xor    eax, eax                ; Line 20 的操作被优化掉了
; Line 22
    ret    0
?function2@@YAHVtest@@@Z ENDP                ; function2结束
_TEXT    ENDS
PUBLIC    _main
;    COMDAT _main
_TEXT    SEGMENT
$T301 = -24
_main    PROC NEAR                    ; COMDAT
; Line 25
    sub    esp, 24                    ; 分配cls1的内存空间
; Line 27
    lea    eax, DWORD PTR $T301[esp+24]        ; 将cls1的地址送入eax寄存器
    push    esi
    push    edi
    push    eax                    ; 将cls1的地址作为参数传递给function1
    call    ?function1@@YA?AVtest@@XZ        ; 调用function1
; Line 28
    sub    esp, 20                    ; 恢复堆栈并分配temp对象的空间(24-4=20)
    mov    ecx, 6
    mov    esi, eax
    mov    edi, esp
    rep movsd                    ; 将cls1对象复制到temp对象中去
    call    ?function2@@YAHVtest@@@Z        ; 调用function2
    add    esp, 24                    ; 00000018H
; Line 29
    xor    eax, eax
; Line 30
    pop    edi
    pop    esi
    add    esp, 24                    ; 00000018H
    ret    0
_main    ENDP
_TEXT    ENDS
END


先来看function1的调用过程:1)程序现在堆栈中给cls1分配了24字节的内存;然后将cls1的地址作为参数传递给function1;随后调用function1。2)进入function1中,程序先给cls2分配内存;然后程序将cls2对象复制到cls1中去;把cls1的地址作为返回值。

根据上述过程,”test function1();”的实际形式其实是:

test *function1(test *ptr_cls)
{
    test cls2;
    memcpy(ptr_cls,&cls2,sizeof(test));
    return ptr_cls;
}

而调用代码的实际形式是:

int main()
{
    test cls1;
    function1(&cls1);
    … …
    return 0;
}

再来看function2的调用过程:程序先给temp分配了一个24字节的对象;然后将cls1对象复制到temp对象中去;随后就调用function2。比起function1,function2要简单很多。



2005年02月13日

文章中的C++代码未经特别声明,均为VC编译。

使用VC编译器生成汇编代码:
运行”cl filename.cpp /Fa”生成”filename.cpp”的中间汇编代码。这些代码没有经过编译器优化,所以要比编译成EXE后再返汇编得到的汇编代码来得更易读;更方便的是,编译器会在asm文件中生成注释,将C++代码的行号对应到asm代码中。
在运行cl.exe前,必须先运行”C:\Program Files\Microsoft Visual Studio\VC98\Bin\VCVARS32.BAT”注册环境变量。

一个C++的对象在内存中到底是个什么样子呢?先来看看下面的代码:

#include <stdio.h>

class test
{
public:
    int m1;
    int m2;
    int m3;
    virtual int function1(){return 1;}
    virtual int function2(){return 2;}
    int function3(){return 3;}
    int function4(){return 4;}
};

int main()
{
    test *ptr1=new test();
    test *ptr2=new test();
    printf(“Size of test:\t%d\n”,sizeof(test));
    printf(“Addr of ptr1:\t0x%08X\n”,ptr1);
    printf(“Addr of m1:\t0x%08X\n”,&(ptr1->m1));
    printf(“Addr of m2:\t0x%08X\n”,&(ptr1->m2));
    printf(“Addr of m3:\t0x%08X\n”,&(ptr1->m3));
    printf(“Addr of vtable:\t0x%08X\n”,*(unsigned int *)((void *)ptr1));
    printf(“\n”);
    printf(“Addr of ptr2:\t0x%08X\n”,ptr2);
    printf(“Addr of m1:\t0x%08X\n”,&(ptr2->m1));
    printf(“Addr of m2:\t0x%08X\n”,&(ptr2->m2));
    printf(“Addr of m3:\t0x%08X\n”,&(ptr2->m3));
    printf(“Addr of vtable:\t0x%08X\n”,*(unsigned int *)((void *)ptr2));
    printf(“\n”);
    printf(“Addr of vtable[0]:\t0x%08X\n”,**((int**)ptr1));
    printf(“Addr of vtable[1]:\t0x%08X\n”,*(*((int**)ptr1)+1));
    printf(“Addr of function1:\t0x%08X\n”,(test::function1));
    printf(“Addr of function2:\t0x%08X\n”,(test::function2));
    printf(“Addr of function3:\t0x%08X\n”,(test::function3));
    printf(“Addr of function4:\t0x%08X\n”,(test::function4));
    return 0;
}

在VC中编译运行后的结果是:

Size of test:   16
Addr of ptr1:   0×00340758
Addr of m1:     0×0034075C
Addr of m2:     0×00340760
Addr of m3:     0×00340764
Addr of vtable: 0×004060B0

Addr of ptr2:   0×00340770
Addr of m1:     0×00340774
Addr of m2:     0×00340778
Addr of m3:     0×0034077C
Addr of vtable: 0×004060B0

Addr of vtable[0]:      0×00401210
Addr of vtable[1]:      0×00401220
Addr of function1:      0×00401230
Addr of function2:      0×00401240
Addr of function3:      0×004011E0
Addr of function4:      0×004011F0

可以确定,test对象在内存中的大小是16字节,结构如下:

  

其中pvtable是一个指向虚函数表的指针,C++依赖vtable实现动态编联,在程序运行时,依靠vtable中的函数指针来执行相应的虚函数。但是执行的结果却与这个模型有些出入:

Addr of vtable[0]:      0×00401210
Addr of vtable[1]:      0×00401220
Addr of function1:      0×00401230
Addr of function2:      0×00401240


vtable[0]、vtable[1]和function1、function2并不对应,虽然它们的内存地址十分接近。究竟是怎么回事,还是反汇编看看:

:00401230 8B01                    mov eax, dword ptr [ecx] ;将vtble地址放到eax寄存器
:00401232 FF20                    jmp dword ptr [eax] ;跳转到vtable指向的function1
:00401234 CC                      int 03
 … …
:0040123F CC                      int 03
:00401240 8B01                    mov eax, dword ptr [ecx] ;将vtble地址放到eax寄存器
:00401242 FF6004                  jmp [eax+04] ;跳转到vtable指向的function2

注:对于thiscall函数调用,ecx寄存器中保存的是该对象的this指针。这两段代码样子差不多,都是从vtable中找到对应的虚函数地址,然后跳转到虚函数里。VC之所以不暴露真正的虚函数地址是为了实现对象的多态性,因为在程序执行前,虚函数的地址是不能确定的;也不应该是确定的。

(待续)