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之所以不暴露真正的虚函数地址是为了实现对象的多态性,因为在程序执行前,虚函数的地址是不能确定的;也不应该是确定的。

(待续)










2005年02月02日

   当地时间1月26日,德国西南部城市海德堡一群女大学生扯起示威横幅抗议政府取消大学补助资金。横幅上画着一名花枝招展的街头妓女模样的年轻女子,旁边写着:没钱上学了,女儿被迫靠卖身赚学费。
  
  德国最高法院当日裁定大学可以向学生收取学费。
  
  之前几十年在德国不论家庭出身与收入,接受高等教育都是免费的。
  
  德国大学引入学费制是近年来德国各界争论不休的问题,今年一些大学已不声不响地行动起来,新学期一开始就相继宣布今年起实行有针对性的收费制。虽然不少学生再次走上街头示威反对,但更多的学生默默接受了这个现实。
  
  2004年2月汉堡市教育局决定,在校时间过长的学生从4月1日起每学期需交500欧元学费。2月23日汉堡大学向学时过长的学生寄发收费通知。汉堡大学目前有4万名大学生,其中近6400人学时过长,占学生比例的16%。北威州的教育部门也宣布,今年夏季学期开始,如果大学生学习时间超过规定的1.5倍,每学期要交650欧元学费,不包括读第二学位的学生。2月27日柏林主管教育的副市长宣布,柏林将于明年对在校学习15个学期以上的学生每学期收费500欧元。
  
  莱法州和萨尔州也宣布实行一种“学习账户”模式,即学生入大学后根据学业难度获得一定数量的学期额度,超过规定时间将不再免费学习,须缴纳学费。
  
  尽管这些收费规定很有限,但仍然引发各城市学生示威浪潮此起彼伏,他们举着“我要富爸爸!州长大人,领养我吧!”等标语在州政府门前安营扎寨。一名学生领袖说,如此收费,将来就只有富家孩子上得起大学。柏林一群男女学生甚至一丝不挂在街上裸奔,唯一一块遮羞纸板上书:我们是穷得光腚的学生,你从我们身上掏不出一分钱。一些媒体批评说,用学校收费的办法弥补州政府财政漏洞不理智。德国“世界报”一篇文章称,德国教育体制的这种变革将毁掉诗人和哲学家的国度。
  
  然而,也有专家认为,德国大学不收费体制养了一大批懒人。长时间的学生生活很轻松,坐车免费,吃饭便宜,住宿廉价,文化生活也少掏腰包。每个大学里都有很多长年不毕业或不愿毕业的学生。德国各大学超过13学期的学生约为三分之一。有6万多学生的科隆大学将近1万名学生根本不在校园露面。德国教育部门估计,如各州采取有限收费,许多大学生将被迫退学。北威州50万名大学生中起码有1万人将离开学校。科隆大学称,这一措施将使该校退学学生人数达五位数。
  
  德国大部分人支持高校有限收费制,他们认为这是一种激励机制,鞭策学生更快、目的更明确地学习。萨尔州教育部长与大学生辩论时说,年轻人加快学习速度很重要,可以在一生最有创造性、最有工作能力的时段里开始他们的职业生涯。他强调,纳税人为学生支付的学费不允许被滥用。
  
  欧洲大部分国家实行收费教育制。只有德国、丹麦、瑞典、芬兰和希腊等国的高等教育学费由国家承担。但现在,德国大学教育不收费也将成为历史。