2006年01月22日

祝福所有我认识的朋友,及路过此地的朋友春节愉快,2006运运畅通.

完毕,回家过年!! Go !!

2005年12月31日

下雪了,2005最后的一天,北京终于下了雪,很多人都说这是个好兆头,所谓“瑞雪照丰年”,也许这天的小雪正是要洗去每个人身上的烦恼,洗去疲惫。的确,这一年,是一个奔波、烦劳的一年。回想一年间,从一个小县城里邀望远方的小孩子,走到这个繁杂的都市,经历是值得回忆的。

还有半个小时就下班了,刚刚重新编译了一下nasl 的解释器,正如glacier 所说,的确会出现800多个警告,但不影响编译后的使用。今天只是大概编译了一下,并没有仔细阅读代码,看来这是明年的活了。收拾了以下桌面,调整一下心情,告诉自己:“2006,理想从这里开始!”

晚上和老婆去踏雪~~ ^_^ 。

2005年12月22日

这是我来北京的第一份工作。

同样张真人也是我工作中的第一个同事。

还记得刚来的时候很拘谨,我叫他“张哥”,熟悉过后,转而直呼名号。

还记得刚来的时候不太懂交换机,即而多次请教,张真人同样不厌其烦的解答,受益非潜。

还记得很多事情。。

今此处留声,祝愿朋友未来路途,一帆风顺。坚持自己的方向,实现自己的梦想。

<赠友诗–白居易>

绿蚁新醅酒,

红泥小火炉.

 晚来天欲雪,

能饮一杯无

2005年12月16日

片段:

“各方态度:

  网络游戏公司:对于我们游戏公司,将坚决支持并拥护“反沉迷系统”

  网络游戏投资商:我们欣慰的看到网络游戏方面的投资将因“反沉迷系统”而更健康,稳定。

  网络游戏点(月)卡销售/代理商:对我们有影响,但不大。

  家长:终于有实用的强硬的措施出台来拯救孩子了,希望该系统能尽早在所有游戏上运行,从而从根本上防止网络游戏对青少年的危害。

  玩家:据国新闻出版总署调查,79%以上玩家持支持态度。 

       基本上不懂这个的家长连防沉迷是咋回事都不知道,基本上玩家还是该玩多少就玩多少。。。。

       背后的利益。。。哼哼~~~

       大清早的,就让我笑成这样~~~ 小记一下

2005年11月04日

好久没有贴东西了,想了想,自己的事情真的很多,在公司手里有一堆的东西需要调试,闲事还得不断的给自己补充点知识,多亏自己现在还是年轻黄金时段,多学点东西,还是不错的。今天下午什么也不想干,逛回自己的BLOG,于是狂发了一些下载下来的文档。(ps:donews 的服务也不是很稳定,经常出来 script 了什么的错误,我重开IE 达N次,重贴文件N次,终于写上了这么点东西)。

上个月经417的介绍,出去讲了4次课,可谓是给自己忙里加忙啊,在公司忙完,回家还得搞点讲课的资料,那一月,经常2点左右睡。第二天困的不行~~,没办法,谁让自己想多找点机会锻炼一下,也能多挣点零花的币子。

十一也没有什么大举动,带着GF去了趟山东,看了看我爷,GF一直想我们还没有结婚,见太多的家人不好,可是我想让她见见我爷,因为我也好久没见了,老爷子的身体也说不好,让他见见,总是好的。刚去的时候,看见老叔,老姑他们的生活,突然感觉很低落,虽然并非所有人都有大志,但是生活的琐碎却他们老了许多。关心他们的同时,也掂量着自己的妹妹,是否能走出个成绩出来。也许对一个还没有定性的小孩不要期望太多。

我和GF从3月到北京了,经历了各种事情,上当,受骗,失落,高兴,等等。终于在十一的时候感觉走到了一个新的起点,我俩一直决定要把经历写出来,给自己以后做个纪念。等着段时间忙完,我门就要开始回忆。好多事情真的是永远不能忘。希望我们的感情也能像这些事情一样,经得起考验。

公司的一个朋友走了,去了一家外企,挺有名气的,挺羡慕,想来自己刚进公司时,也真傻,开的条件那么低,还以为很高,也许是迫与得到工作的压力吧。朋友发来一句短信,他说有一种叫做后发制人,他并不局限与赶路,同样使用与工作和生活。也许是吧,祝他一切顺利,也同样祝我和GF。

平台的设计已经完成了界面的一些部分,说实话,我对界面设计并不怎么在行,下一步我准备将界面交给其他人来做,毕竟有现成的界面库及示例,他们理解也会快一些,我转做底层数据通信及TCL解释部分,不过在我的想法里,我打算加进自己的shell 控制台及远程类似sensor的功能,这样可以实现一些其他的测试,譬如测试攻击,远程一些进程操作等。不过这些都是后话,光靠我一人实现还是挺有困难的。这个工具我也想了,最后不管公司怎么对待,都是我以后做什么的一个筹码。

GF的工作一直很累,早出晚归的,最近还不断的身体不好,心里还挂着是否能得到满分,不管我行动上关心是否足够,心里我始终支持她,挂念着她,我们最需要的,是彼此的平安。平安才有事业。

我也准备开始看房了,打算结婚,打算有自己的天地~`,生活就是这样匆忙起来。

也许这就是都市的节奏,匆匆忙忙的脚步。

调用约定(Calling Convention)是指在程序设计语言中为了实现函数调用而建立的一种协议。这种协议规定了该语言的函数中的参数传送方式、参数是否可变和由谁来处理堆栈等问题。不同的语言定义了不同的调用约定。

 

 

C++中,为了允许操作符重载和函数重载,C++编译器往往按照某种规则改写每一个入口点的符号名,以便允许同一个名字(具有不同的参数类型或者是不同的作用域)有多个用法,而不会打破现有的基于C的链接器。这项技术通常被称为名称改编(Name Mangling)或者名称修饰(Name Decoration)。许多C++编译器厂商选择了自己的名称修饰方案。

 

 

因此,为了使其它语言编写的模块(如Visual Basic应用程序、PascalFortran的应用程序等)可以调用C/C++编写的DLL的函数,必须使用正确的调用约定来导出函数,并且不要让编译器对要导出的函数进行任何名称修饰。

1.调用约定(Calling Convention

调用约定用来处理决定函数参数传送时入栈和出栈的顺序(由调用者还是被调用者把参数弹出栈),以及编译器用来识别函数名称的名称修饰约定等问题。在Microsoft VC++ 6.0中定义了下面几种调用约定,我们将结合汇编语言来一一分析它们:

1__cdecl

__cdeclC/C++MFC程序默认使用的调用约定,也可以在函数声明时加上__cdecl关键字来手工指定。采用__cdecl约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。__cdecl可以写成_cdecl

 

 

下面将通过一个具体实例来分析__cdecl约定:

 

 

VC++中新建一个Win32 Console工程,命名为cdecl。其代码如下:

 

 

int __cdecl Add(int a, int b);         //函数声明

 

 

void main()

{

       Add(1,2);                                   //函数调用

}

 

 

int __cdecl Add(int a, int b)          //函数实现

{

       return (a + b);

}

 

 

函数调用处反汇编代码如下:

 

 

;Add(1,2);

push                     2                                        ;参数从右到左入栈,先压入2

push        1                                         ;压入1

call          @ILT+0(Add) (00401005)    ;调用函数实现

add           esp,8                                   ;由函数调用清栈

2__stdcall

__stdcall调用约定用于调用Win32 API函数。采用__stdcal约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆栈。__stdcall可以写成_stdcall

 

 

还是那个例子,将__cdecl约定换成__stdcall

 

 

int __stdcall Add(int a, int b)

{

return (a + b);

}

 

 

函数调用处反汇编代码:

      

       ; Add(1,2);

push                     2                                               ;参数从右到左入栈,先压入2

push        1                                                ;压入1

call          @ILT+10(Add) (0040100f)          ;调用函数实现

 

 

函数实现部分的反汇编代码:

 

 

;int __stdcall Add(int a, int b)

push                     ebp

mov          ebp,esp

sub                esp,40h

push               ebx

push               esi

push               edi

lea           edi,[ebp-40h]

mov          ecx,10h

mov        eax,0CCCCCCCCh

rep stos       dword ptr [edi]

;return (a + b);

mov          eax,dword ptr [ebp+8]

add                eax,dword ptr [ebp+0Ch]

pop           edi

pop          esi

pop           ebx

mov          esp,ebp

pop          ebp

ret           8                 ;清栈

3__fastcall

__fastcall约定用于对性能要求非常高的场合。__fastcall约定将函数的从左边开始的两个大小不大于4个字节(DWORD)的参数分别放在ECXEDX寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈。__fastcall可以写成_fastcall

 

 

依旧是相类似的例子,此时函数调用约定为__fastcall,函数参数个数增加2个:

 

 

int __fastcall Add(int a, double b, int c, int d)

{

return (a + b + c + d);

}

 

 

函数调用部分的汇编代码:

 

 

;Add(1, 2, 3, 4);

push                     4                          ;后两个参数从右到左入栈,先压入4

mov          edx,3                    ;int类型的3放入edx

push         40000000h            ;压入double类型的2

push         0

mov          ecx,1                    ;int类型的1放入ecx

call          @ILT+0(Add) (00401005)                ;调用函数实现

 

 

函数实现部分的反汇编代码:

             

; int __fastcall Add(int a, double b, int c, int d)

push                     ebp

mov        ebp,esp

sub          esp,48h

push               ebx

push               esi

push               edi

push               ecx

lea           edi,[ebp-48h]

mov          ecx,12h

mov           eax,0CCCCCCCCh

rep stos       dword ptr [edi]

pop          ecx

mov        dword ptr [ebp-8],edx

mov        dword ptr [ebp-4],ecx

;return (a + b + c + d);

fild           dword ptr [ebp-4]

fadd          qword ptr [ebp+8]

fiadd         dword ptr [ebp-8]

fiadd         dword ptr [ebp+10h]

call          __ftol (004011b8)

pop          edi

pop          esi

pop          ebx

mov          esp,ebp

pop          ebp

ret           0Ch                              ;清栈

 

 

关键字__cdecl__stdcall__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting…->C/C++->Code Generation项选择。它们对应的命令行参数分别为/Gd/Gz/Gr。缺省状态为/Gd,即__cdecl。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。

 

4thiscall

thiscall调用约定是C++中的非静态类成员函数的默认调用约定。thiscall只能被编译器使用,没有相应的关键字,因此不能被程序员指定。采用thiscall约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,只是另外通过ECX寄存器传送一个额外的参数:this指针。

 

 

这次的例子中将定义一个类,并在类中定义一个成员函数,代码如下:

 

 

class CSum

     {

public:

int Add(int a, int b)

{

return (a + b);

}

};

 

 

void main()

{    

       CSum sum;

       sum.Add(1, 2);

}

 

 

函数调用部分汇编代码:

 

 

;CSum  sum;

     ;sum.Add(1, 2);

     push                     2                                 ;参数从右到左入栈,先压入2

     push               1                                 ;压入1

     lea           ecx,[ebp-4]                   ;ecx存放了this指针

call          @ILT+5(CSum::Add) (0040100a)        ;调用函数实现

 

 

函数实现部分汇编代码:

 

 

;int Add(int a, int b)

       push                     ebp

mov          ebp,esp

sub                esp,44h                        ;多用了一个4bytes的空间用于存放this指针

push               ebx

push               esi

push               edi

push               ecx

lea           edi,[ebp-44h]

mov        ecx,11h

mov          eax,0CCCCCCCCh

rep stos       dword ptr [edi]

pop          ecx

mov          dword ptr [ebp-4],ecx

;return (a + b);

mov          eax,dword ptr [ebp+8]

add          eax,dword ptr [ebp+0Ch]

pop           edi

pop          esi

pop           ebx

mov          esp,ebp

pop           ebp

ret           8                                 ;清栈

5naked属性

采用上面所述的四种调用约定的函数在进入函数时,编译器会产生代码来保存ESIEDIEBXEBP寄存器中的值,退出函数时则产生代码恢复这些寄存器的内容。对于定义了naked属性的函数,编译器不会自动产生这样的代码,需要你手工使用内嵌汇编来控制函数实现中的堆栈管理。由于naked属性并不是类型修饰符,故必须和__declspec共同使用。下面的这段代码定义了一个使用了naked属性的函数及其实现:

 

 

__declspec ( naked ) func()

{

int i;

     int j;

      

_asm

{

push              ebp

              mov      ebp, esp

              sub            esp, __LOCAL_SIZE

}

      

    _asm

       {

mov        esp, ebp

              pop         ebp

              ret

       }

}

 

 

naked属性与本节关系不大,具体请参考MSDN

6WINAPI

还有一个值得一提的是WINAPI宏,它可以被翻译成适当的调用约定以供函数使用。该宏定义于windef.h之中。下面是在windef.h中的部分内容:

 

 

#define CDECL             _cdecl

#define WINAPI           CDECL

#define CALLBACK       __stdcall

#define WINAPI        __stdcall

#define APIENTRY      WINAPI

 

 

       由此可见,WINAPICALLBACKAPIENTRY等宏的作用。

2.名称修饰(Name Decoration

CC++函数在内部(编译和链接)通过修饰名(Decoration Name)识别。函数的修饰名是编译器在编译函数定义或者原型时生成的字符串。编译器在创建.obj文件时对函数名称进行修饰。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出C++重载函数、构造函数、析构函数,又如在汇编代码里调用CC++函数等。

 

 

VC++中,函数修饰名由编译类型(CC++)、函数名、类名、调用约定、返回类型、参数等多种因素共同决定。下面分C编译、C++编译(非类成员函数)和C++类及其成员函数编译三种情况说明:

1C编译时函数名称修饰

当函数使用__cdecl调用约定时,编译器仅在原函数名前加上一个下划线前缀,格式为_functionname。例如:函数int __cdecl Add(int a, int b),输出后为:_Add

 

 

当函数使用__stdcall调用约定时,编译器在原函数名前加上一个下划线前缀,后面加上一个@符号和函数参数的字节数,格式为_functionname@number。例如:函数int __stdcall Add(int a, int b),输出后为:_Add@8

 

 

当函数是用__fastcall调用约定时,编译器在原函数名前加上一个@符号,后面是加一个@符号和函数参数的字节数,格式为@functionname@number。例如:函数int __fastcall Add(int a, int b),输出后为:@Add@8

 

 

    以上改变均不会改变原函数名中的字符大小写。

 

2C++编译时函数(非类成员函数)名称修饰

当函数使用__cdecl调用约定时,编译器进行以下工作:

 

 

1.以?标识函数名的开始,后跟函数名;

2.函数名后面以@@YA标识开始,后跟返回值和参数表;

3.当函数的返回值或者参数与C++类无关的时候,返回值和参数表以下列代号表示:

       Bconst

Dchar

Eunsigned char

Fshort

Gunsigned short

Hint

Iunsigned int

Jlong

Kunsigned long

Mfloat

Ndouble

_Nbool

PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0

代替,一个0代表一次重复)

       PBconst指针

       AA:引用(&

       ABconst引用

U:类或结构体

VInterface(接口)

W4enum

Xvoid

4@@YA标识之后紧跟的是该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。当函数的返回值或者参数与C++类无关的时候,其处理符合本条规则,否则按照56规则处理;

5、当函数返回值为某个类或带有const性质的类的时候,返回值的命名为:?A/?B+V+类名+@@(不带加号)。当函数返回值为某个类的指针/引用或者带有const性质的类的指针/引用的时候,返回值的命名为:PA/AA或者PB/AB+V+类名+@@(不带加号);

6、函数参数为某个类的时候,并且该参数所使用的类曾经出现过的话(也就是与函数返回值所使用的类相同或者与前一个参数使用的类相同),则该参数类型格式为:V+1+@(不带加号)。如果该参数所使用的类没有出现过的话,则该参数类型格式为:V+类名+@@(不带加号)。函数参数为某个类的指针/引用或者带有const性质指针/引用的时候,则该参数类型格式是在上述格式的基础上在V前面加上代表指针/引用类型或者带有const性质指针/引用类型的标识符(PA/AAPB/AB);

7、参数表后以@Z标识整个名字的结束,如果该函数无参数,则以Z标识结束。

 

 

当函数使用__stdcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YG

 

 

当函数使用__fastcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YI

3C++编译类及其成员函数时名称修饰

       对于导出的C++类,仅能使用__cdecl调用约定。在编译器编译过程中,编译器会对C++类进行处理。如:class __declspec(dllexport) MyClass会被处理为class MyClass & MyClass::operator=(class MyClass const &)。在C++编译器对C++类进行名称修饰的时候,编译器进行以下工作:

 

 

1.以?标识函数名的开始,后跟?4+类名;

2.类名后面跟@@QAE标识,对于导出类来说这是固定的;

3@@QAE后面跟AAV0@ABV0@,即引用类型标识符AA+V+0(重复的类的标识符)+@(不带加号)和const性质的引用AB+V+ 0(重复的类的标识符)+@(不带加号);

4.最后以@Z标识整个名字的结束。

 

 

       对于导出的C++类中的成员函数(非构造函数和析构函数),可以使用不同的调用约定。当导出的C++类中的成员函数使用__cdecl调用约定时,编译器进行以下工作:

 

 

1.以?标识函数名的开始,后跟函数名+@+类名(不带加号);

2.之后以@@QAE标识开始,后跟返回值和参数表;

3.当函数的返回值或者参数与C++类无关的时候,返回值和参数表以下列代号表示:

       Bconst

Dchar

Eunsigned char

Fshort

Gunsigned short

Hint

Iunsigned int

Jlong

Kunsigned long

Mfloat

Ndouble

_Nbool

PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0

代替,一个0代表一次重复)

       PBconst指针

       AA:引用(&

       ABconst引用

U:类或结构体

VInterface(接口)

W4enum

Xvoid

4@@QAE标识之后紧跟的是该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。当函数的返回值或者参数与C++类无关的时候,其处理符合本条规则,否则按照56规则处理;

5、当函数返回值为当前类或带有const性质的当前类的时候,返回值的命名为:?A?B+V+1+@@(不带加号)。当函数返回值为当前类的指针/引用或者带有const性质的当前类的指针/引用的时候,返回值的命名为:PA/AAPB/AB+V+1+@@(不带加号);

6、当函数返回值为某个类或带有const性质的类的时候,返回值的命名为:?A/?B+V+类名+@@(不带加号)。当函数返回值为某个类的指针/引用或者带有const性质的类的指针/引用的时候,返回值的命名为:PA/AA或者PB/AB+V+类名+@@(不带加号);

7、函数参数为某个类的时候,并且该参数所使用的类曾经出现过的话(也就是当前要导出的类、与函数返回值所使用的类相同或者与前一个参数使用的类相同的类),则该参数类型格式为:V+1+@(不带加号)。如果该参数所使用的类不是当前要导出的类的话,则该参数类型格式为:V+类名+@@(不带加号)。函数参数为某个类的指针/引用或者带有const性质指针/引用的时候,则该参数类型格式是在上述格式的基础上在V前面加上代表指针/引用类型或者带有const性质指针/引用类型的标识符(PA/AAPB/AB);

8、参数表后以@Z标识整个名字的结束,如果该函数无参数,则以Z标识结束。

 

 

当函数使用__stdcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YG

 

 

当函数使用__fastcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YI

4C++编译导出数据时名称修饰

       对于导出的数据,仅使用__cdecl调用约定。在C++编译器对C++类进行名称修饰的时候,编译器进行以下工作:

 

 

1.以?标识数据的开始,后跟数据名;

2.数据名后面以@@3标识开始,后跟数据类型;

3.当数据类型与C++类无关的时候,数据类型以下列代号表示:

       Bconst

Dchar

Eunsigned char

Fshort

Gunsigned short

Hint

Iunsigned int

Jlong

Kunsigned long

Mfloat

Ndouble

_Nbool

PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0

代替,一个0代表一次重复)

       PBconst指针

       AA:引用(&

       ABconst引用

U:类或结构体

VInterface(接口)

W4enum

Xvoid

4.如果数据类型是某个类的时候,数据类型的命名为:V+类名+@@(不带加号)。当数据类型为当前类的指针/引用或者带有const性质的当前类的指针/引用的时候,数据类型的命名为:PA/AAPB/AB+V+类名+@@(不带加号);

5.最后,如果数据类型是const性质,则修饰名以B结尾。如果数据类型是非const性质,则修饰名以A结尾。

 

2.名称修饰(Name Decoration

CC++函数在内部(编译和链接)通过修饰名(Decoration Name)识别。函数的修饰名是编译器在编译函数定义或者原型时生成的字符串。编译器在创建.obj文件时对函数名称进行修饰。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出C++重载函数、构造函数、析构函数,又如在汇编代码里调用CC++函数等。

 

 

VC++中,函数修饰名由编译类型(CC++)、函数名、类名、调用约定、返回类型、参数等多种因素共同决定。下面分C编译、C++编译(非类成员函数)和C++类及其成员函数编译三种情况说明:

1C编译时函数名称修饰

当函数使用__cdecl调用约定时,编译器仅在原函数名前加上一个下划线前缀,格式为_functionname。例如:函数int __cdecl Add(int a, int b),输出后为:_Add

 

 

当函数使用__stdcall调用约定时,编译器在原函数名前加上一个下划线前缀,后面加上一个@符号和函数参数的字节数,格式为_functionname@number。例如:函数int __stdcall Add(int a, int b),输出后为:_Add@8

 

 

当函数是用__fastcall调用约定时,编译器在原函数名前加上一个@符号,后面是加一个@符号和函数参数的字节数,格式为@functionname@number。例如:函数int __fastcall Add(int a, int b),输出后为:@Add@8

 

 

    以上改变均不会改变原函数名中的字符大小写。

 

2C++编译时函数(非类成员函数)名称修饰

当函数使用__cdecl调用约定时,编译器进行以下工作:

 

 

1.以?标识函数名的开始,后跟函数名;

2.函数名后面以@@YA标识开始,后跟返回值和参数表;

3.当函数的返回值或者参数与C++类无关的时候,返回值和参数表以下列代号表示:

       Bconst

Dchar

Eunsigned char

Fshort

Gunsigned short

Hint

Iunsigned int

Jlong

Kunsigned long

Mfloat

Ndouble

_Nbool

PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0

代替,一个0代表一次重复)

       PBconst指针

       AA:引用(&

       ABconst引用

U:类或结构体

VInterface(接口)

W4enum

Xvoid

4@@YA标识之后紧跟的是该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。当函数的返回值或者参数与C++类无关的时候,其处理符合本条规则,否则按照56规则处理;

5、当函数返回值为某个类或带有const性质的类的时候,返回值的命名为:?A/?B+V+类名+@@(不带加号)。当函数返回值为某个类的指针/引用或者带有const性质的类的指针/引用的时候,返回值的命名为:PA/AA或者PB/AB+V+类名+@@(不带加号);

6、函数参数为某个类的时候,并且该参数所使用的类曾经出现过的话(也就是与函数返回值所使用的类相同或者与前一个参数使用的类相同),则该参数类型格式为:V+1+@(不带加号)。如果该参数所使用的类没有出现过的话,则该参数类型格式为:V+类名+@@(不带加号)。函数参数为某个类的指针/引用或者带有const性质指针/引用的时候,则该参数类型格式是在上述格式的基础上在V前面加上代表指针/引用类型或者带有const性质指针/引用类型的标识符(PA/AAPB/AB);

7、参数表后以@Z标识整个名字的结束,如果该函数无参数,则以Z标识结束。

 

 

当函数使用__stdcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YG

 

 

当函数使用__fastcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YI

3C++编译类及其成员函数时名称修饰

       对于导出的C++类,仅能使用__cdecl调用约定。在编译器编译过程中,编译器会对C++类进行处理。如:class __declspec(dllexport) MyClass会被处理为class MyClass & MyClass::operator=(class MyClass const &)。在C++编译器对C++类进行名称修饰的时候,编译器进行以下工作:

 

 

1.以?标识函数名的开始,后跟?4+类名;

2.类名后面跟@@QAE标识,对于导出类来说这是固定的;

3@@QAE后面跟AAV0@ABV0@,即引用类型标识符AA+V+0(重复的类的标识符)+@(不带加号)和const性质的引用AB+V+ 0(重复的类的标识符)+@(不带加号);

4.最后以@Z标识整个名字的结束。

 

 

       对于导出的C++类中的成员函数(非构造函数和析构函数),可以使用不同的调用约定。当导出的C++类中的成员函数使用__cdecl调用约定时,编译器进行以下工作:

 

 

1.以?标识函数名的开始,后跟函数名+@+类名(不带加号);

2.之后以@@QAE标识开始,后跟返回值和参数表;

3.当函数的返回值或者参数与C++类无关的时候,返回值和参数表以下列代号表示:

       Bconst

Dchar

Eunsigned char

Fshort

Gunsigned short

Hint

Iunsigned int

Jlong

Kunsigned long

Mfloat

Ndouble

_Nbool

PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0

代替,一个0代表一次重复)

       PBconst指针

       AA:引用(&

       ABconst引用

U:类或结构体

VInterface(接口)

W4enum

Xvoid

4@@QAE标识之后紧跟的是该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。当函数的返回值或者参数与C++类无关的时候,其处理符合本条规则,否则按照56规则处理;

5、当函数返回值为当前类或带有const性质的当前类的时候,返回值的命名为:?A?B+V+1+@@(不带加号)。当函数返回值为当前类的指针/引用或者带有const性质的当前类的指针/引用的时候,返回值的命名为:PA/AAPB/AB+V+1+@@(不带加号);

6、当函数返回值为某个类或带有const性质的类的时候,返回值的命名为:?A/?B+V+类名+@@(不带加号)。当函数返回值为某个类的指针/引用或者带有const性质的类的指针/引用的时候,返回值的命名为:PA/AA或者PB/AB+V+类名+@@(不带加号);

7、函数参数为某个类的时候,并且该参数所使用的类曾经出现过的话(也就是当前要导出的类、与函数返回值所使用的类相同或者与前一个参数使用的类相同的类),则该参数类型格式为:V+1+@(不带加号)。如果该参数所使用的类不是当前要导出的类的话,则该参数类型格式为:V+类名+@@(不带加号)。函数参数为某个类的指针/引用或者带有const性质指针/引用的时候,则该参数类型格式是在上述格式的基础上在V前面加上代表指针/引用类型或者带有const性质指针/引用类型的标识符(PA/AAPB/AB);

8、参数表后以@Z标识整个名字的结束,如果该函数无参数,则以Z标识结束。

 

 

当函数使用__stdcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YG

 

 

当函数使用__fastcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YI

4C++编译导出数据时名称修饰

       对于导出的数据,仅使用__cdecl调用约定。在C++编译器对C++类进行名称修饰的时候,编译器进行以下工作:

 

 

1.以?标识数据的开始,后跟数据名;

2.数据名后面以@@3标识开始,后跟数据类型;

3.当数据类型与C++类无关的时候,数据类型以下列代号表示:

       Bconst

Dchar

Eunsigned char

Fshort

Gunsigned short

Hint

Iunsigned int

Jlong

Kunsigned long

Mfloat

Ndouble

_Nbool

PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0

代替,一个0代表一次重复)

       PBconst指针

       AA:引用(&

       ABconst引用

U:类或结构体

VInterface(接口)

W4enum

Xvoid

4.如果数据类型是某个类的时候,数据类型的命名为:V+类名+@@(不带加号)。当数据类型为当前类的指针/引用或者带有const性质的当前类的指针/引用的时候,数据类型的命名为:PA/AAPB/AB+V+类名+@@(不带加号);

5.最后,如果数据类型是const性质,则修饰名以B结尾。如果数据类型是非const性质,则修饰名以A结尾。

 

2.名称修饰(Name Decoration

CC++函数在内部(编译和链接)通过修饰名(Decoration Name)识别。函数的修饰名是编译器在编译函数定义或者原型时生成的字符串。编译器在创建.obj文件时对函数名称进行修饰。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出C++重载函数、构造函数、析构函数,又如在汇编代码里调用CC++函数等。

 

 

VC++中,函数修饰名由编译类型(CC++)、函数名、类名、调用约定、返回类型、参数等多种因素共同决定。下面分C编译、C++编译(非类成员函数)和C++类及其成员函数编译三种情况说明:

1C编译时函数名称修饰

当函数使用__cdecl调用约定时,编译器仅在原函数名前加上一个下划线前缀,格式为_functionname。例如:函数int __cdecl Add(int a, int b),输出后为:_Add

 

 

当函数使用__stdcall调用约定时,编译器在原函数名前加上一个下划线前缀,后面加上一个@符号和函数参数的字节数,格式为_functionname@number。例如:函数int __stdcall Add(int a, int b),输出后为:_Add@8

 

 

当函数是用__fastcall调用约定时,编译器在原函数名前加上一个@符号,后面是加一个@符号和函数参数的字节数,格式为@functionname@number。例如:函数int __fastcall Add(int a, int b),输出后为:@Add@8

 

 

    以上改变均不会改变原函数名中的字符大小写。

 

2C++编译时函数(非类成员函数)名称修饰

当函数使用__cdecl调用约定时,编译器进行以下工作:

 

 

1.以?标识函数名的开始,后跟函数名;

2.函数名后面以@@YA标识开始,后跟返回值和参数表;

3.当函数的返回值或者参数与C++类无关的时候,返回值和参数表以下列代号表示:

       Bconst

Dchar

Eunsigned char

Fshort

Gunsigned short

Hint

Iunsigned int

Jlong

Kunsigned long

Mfloat

Ndouble

_Nbool

PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0

代替,一个0代表一次重复)

       PBconst指针

       AA:引用(&

       ABconst引用

U:类或结构体

VInterface(接口)

W4enum

Xvoid

4@@YA标识之后紧跟的是该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。当函数的返回值或者参数与C++类无关的时候,其处理符合本条规则,否则按照56规则处理;

5、当函数返回值为某个类或带有const性质的类的时候,返回值的命名为:?A/?B+V+类名+@@(不带加号)。当函数返回值为某个类的指针/引用或者带有const性质的类的指针/引用的时候,返回值的命名为:PA/AA或者PB/AB+V+类名+@@(不带加号);

6、函数参数为某个类的时候,并且该参数所使用的类曾经出现过的话(也就是与函数返回值所使用的类相同或者与前一个参数使用的类相同),则该参数类型格式为:V+1+@(不带加号)。如果该参数所使用的类没有出现过的话,则该参数类型格式为:V+类名+@@(不带加号)。函数参数为某个类的指针/引用或者带有const性质指针/引用的时候,则该参数类型格式是在上述格式的基础上在V前面加上代表指针/引用类型或者带有const性质指针/引用类型的标识符(PA/AAPB/AB);

7、参数表后以@Z标识整个名字的结束,如果该函数无参数,则以Z标识结束。

 

 

当函数使用__stdcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YG

 

 

当函数使用__fastcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YI

3C++编译类及其成员函数时名称修饰

       对于导出的C++类,仅能使用__cdecl调用约定。在编译器编译过程中,编译器会对C++类进行处理。如:class __declspec(dllexport) MyClass会被处理为class MyClass & MyClass::operator=(class MyClass const &)。在C++编译器对C++类进行名称修饰的时候,编译器进行以下工作:

 

 

1.以?标识函数名的开始,后跟?4+类名;

2.类名后面跟@@QAE标识,对于导出类来说这是固定的;

3@@QAE后面跟AAV0@ABV0@,即引用类型标识符AA+V+0(重复的类的标识符)+@(不带加号)和const性质的引用AB+V+ 0(重复的类的标识符)+@(不带加号);

4.最后以@Z标识整个名字的结束。

 

 

       对于导出的C++类中的成员函数(非构造函数和析构函数),可以使用不同的调用约定。当导出的C++类中的成员函数使用__cdecl调用约定时,编译器进行以下工作:

 

 

1.以?标识函数名的开始,后跟函数名+@+类名(不带加号);

2.之后以@@QAE标识开始,后跟返回值和参数表;

3.当函数的返回值或者参数与C++类无关的时候,返回值和参数表以下列代号表示:

       Bconst

Dchar

Eunsigned char

Fshort

Gunsigned short

Hint

Iunsigned int

Jlong

Kunsigned long

Mfloat

Ndouble

_Nbool

PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0

代替,一个0代表一次重复)

       PBconst指针

       AA:引用(&

       ABconst引用

U:类或结构体

VInterface(接口)

W4enum

Xvoid

4@@QAE标识之后紧跟的是该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。当函数的返回值或者参数与C++类无关的时候,其处理符合本条规则,否则按照56规则处理;

5、当函数返回值为当前类或带有const性质的当前类的时候,返回值的命名为:?A?B+V+1+@@(不带加号)。当函数返回值为当前类的指针/引用或者带有const性质的当前类的指针/引用的时候,返回值的命名为:PA/AAPB/AB+V+1+@@(不带加号);

6、当函数返回值为某个类或带有const性质的类的时候,返回值的命名为:?A/?B+V+类名+@@(不带加号)。当函数返回值为某个类的指针/引用或者带有const性质的类的指针/引用的时候,返回值的命名为:PA/AA或者PB/AB+V+类名+@@(不带加号);

7、函数参数为某个类的时候,并且该参数所使用的类曾经出现过的话(也就是当前要导出的类、与函数返回值所使用的类相同或者与前一个参数使用的类相同的类),则该参数类型格式为:V+1+@(不带加号)。如果该参数所使用的类不是当前要导出的类的话,则该参数类型格式为:V+类名+@@(不带加号)。函数参数为某个类的指针/引用或者带有const性质指针/引用的时候,则该参数类型格式是在上述格式的基础上在V前面加上代表指针/引用类型或者带有const性质指针/引用类型的标识符(PA/AAPB/AB);

8、参数表后以@Z标识整个名字的结束,如果该函数无参数,则以Z标识结束。

 

 

当函数使用__stdcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YG

 

 

当函数使用__fastcall调用约定时,编译器所做工作的规则同上面的__cdecl调用约定,只是参数表的开始标识由上面的@@YA变为@@YI

4C++编译导出数据时名称修饰

       对于导出的数据,仅使用__cdecl调用约定。在C++编译器对C++类进行名称修饰的时候,编译器进行以下工作:

 

 

1.以?标识数据的开始,后跟数据名;

2.数据名后面以@@3标识开始,后跟数据类型;

3.当数据类型与C++类无关的时候,数据类型以下列代号表示:

       Bconst

Dchar

Eunsigned char

Fshort

Gunsigned short

Hint

Iunsigned int

Jlong

Kunsigned long

Mfloat

Ndouble

_Nbool

PA:指针(*,后面的代号表明指针类型,如果相同类型的指针连续出现,以0

代替,一个0代表一次重复)

       PBconst指针

       AA:引用(&

       ABconst引用

U:类或结构体

VInterface(接口)

W4enum

Xvoid

4.如果数据类型是某个类的时候,数据类型的命名为:V+类名+@@(不带加号)。当数据类型为当前类的指针/引用或者带有const性质的当前类的指针/引用的时候,数据类型的命名为:PA/AAPB/AB+V+类名+@@(不带加号);

5.最后,如果数据类型是const性质,则修饰名以B结尾。如果数据类型是非const性质,则修饰名以A结尾。

 

一、基本概念:

    钩子(Hook),是Windows消息处理机制的一个平台,应用程序可以在上面设置子程以监视指定窗口的某种消息,而且所监视的窗口可以是其他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。钩子机制允许应用程序截获处理window消息或特定事件。

    钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。

二、运行机制:

1、钩子链表和钩子子程:

    每一个Hook都有一个与之相关联的指针列表,称之为钩子链表,由系统来维护。这个列表的指针指向指定的,应用程序定义的,被Hook子程调用的回调函数,也就是该钩子的各个处理子程。当与指定的Hook类型关联的消息发生时,系统就把这个消息传递到Hook子程。一些Hook子程可以只监视消息,或者修改消息,或者停止消息的前进,避免这些消息传递到下一个Hook子程或者目的窗口。最近安装的钩子放在链的开始,而最早安装的钩子放在最后,也就是后加入的先获得控制权。

 Windows 并不要求钩子子程的卸载顺序一定得和安装顺序相反。每当有一个钩子被卸载,Windows 便释放其占用的内存,并更新整个Hook链表。如果程序安装了钩子,但是在尚未卸载钩子之前就结束了,那么系统会自动为它做卸载钩子的操作。

    钩子子程是一个应用程序定义的回调函数(CALLBACK Function),不能定义成某个类的成员函数,只能定义为普通的C函数。用以监视系统或某一特定类型的事件,这些事件可以是与某一特定线程关联的,也可以是系统中所有线程的事件。

    钩子子程必须按照以下的语法:
    LRESULT CALLBACK HookProc
 (
   int nCode,
      WPARAM wParam,
      LPARAM lParam
     );
HookProc是应用程序定义的名字。

nCode参数是Hook代码,Hook子程使用这个参数来确定任务。这个参数的值依赖于Hook类型,每一种Hook都有自己的Hook代码特征字符集。
wParam和lParam参数的值依赖于Hook代码,但是它们的典型值是包含了关于发送或者接收消息的信息。

2、钩子的安装与释放:

    使用API函数SetWindowsHookEx()把一个应用程序定义的钩子子程安装到钩子链表中。SetWindowsHookEx函数总是在Hook链的开头安装Hook子程。当指定类型的Hook监视的事件发生时,系统就调用与这个Hook关联的Hook链的开头的Hook子程。每一个Hook链中的Hook子程都决定是否把这个事件传递到下一个Hook子程。Hook子程传递事件到下一个Hook子程需要调用CallNextHookEx函数。
   
HHOOK SetWindowsHookEx(
     int idHook,      // 钩子的类型,即它处理的消息类型
     HOOKPROC lpfn,   // 钩子子程的地址指针。如果dwThreadId参数为0
      // 或是一个由别的进程创建的线程的标识,
      // lpfn必须指向DLL中的钩子子程。
      // 除此以外,lpfn可以指向当前进程的一段钩子子程代码。
      // 钩子函数的入口地址,当钩子钩到任何消息后便调用这个函数。
     HINSTANCE hMod,  // 应用程序实例的句柄。标识包含lpfn所指的子程的DLL。
      // 如果dwThreadId 标识当前进程创建的一个线程,
      // 而且子程代码位于当前进程,hMod必须为NULL。
      // 可以很简单的设定其为本应用程序的实例句柄。
     DWORD dwThreadId // 与安装的钩子子程相关联的线程的标识符。
      // 如果为0,钩子子程与所有的线程关联,即为全局钩子。
                 );
  函数成功则返回钩子子程的句柄,失败返回NULL。

  以上所说的钩子子程与线程相关联是指在一钩子链表中发给该线程的消息同时发送给钩子子程,且被钩子子程先处理。

    在钩子子程中调用得到控制权的钩子函数在完成对消息的处理后,如果想要该消息继续传递,那么它必须调用另外一个SDK中的API函数CallNextHookEx来传递它,以执行钩子链表所指的下一个钩子子程。这个函数成功时返回钩子链中下一个钩子过程的返回值,返回值的类型依赖于钩子的类型。这个函数的原型如下:

LRESULT CallNextHookEx
   (
    HHOOK hhk;
    int nCode;
    WPARAM wParam;
    LPARAM lParam;
    );
   
hhk为当前钩子的句柄,由SetWindowsHookEx()函数返回。
NCode为传给钩子过程的事件代码。
wParam和lParam 分别是传给钩子子程的wParam值,其具体含义与钩子类型有关。
  
    钩子函数也可以通过直接返回TRUE来丢弃该消息,并阻止该消息的传递。否则的话,其他安装了钩子的应用程序将不会接收到钩子的通知而且还有可能产生不正确的结果。

    钩子在使用完之后需要用UnHookWindowsHookEx()卸载,否则会造成麻烦。释放钩子比较简单,UnHookWindowsHookEx()只有一个参数。函数原型如下:

UnHookWindowsHookEx
  (
  HHOOK hhk;
  );
函数成功返回TRUE,否则返回FALSE。

3、一些运行机制:

    在Win16环境中,DLL的全局数据对每个载入它的进程来说都是相同的;而在Win32环境中,情况却发生了变化,DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。当进程在载入DLL时,操作系统自动把DLL地址映射到该进程的私有空间,也就是进程的虚拟地址空间,而且也复制该DLL的全局数据的一份拷贝到该进程空间。也就是说每个进程所拥有的相同的DLL的全局数据,它们的名称相同,但其值却并不一定是相同的,而且是互不干涉的。
 
 因此,在Win32环境下要想在多个进程中共享数据,就必须进行必要的设置。在访问同一个Dll的各进程之间共享存储器是通过存储器映射文件技术实现的。也可以把这些需要共享的数据分离出来,放置在一个独立的数据段里,并把该段的属性设置为共享。必须给这些变量赋初值,否则编译器会把没有赋初始值的变量放在一个叫未被初始化的数据段中。

 #pragma data_seg预处理指令用于设置共享数据段。例如:
#pragma data_seg("SharedDataName")
HHOOK hHook=NULL;
#pragma data_seg()
 在#pragma data_seg("SharedDataName")和#pragma data_seg()之间的所有变量 将被访问该Dll的所有进程看到和共享。

    当进程隐式或显式调用一个动态库里的函数时,系统都要把这个动态库映射到这个进程的虚拟地址空间里(以下简称"地址空间")。这使得DLL成为进程的一部分,以这个进程的身份执行,使用这个进程的堆栈。

4、系统钩子与线程钩子:

    SetWindowsHookEx()函数的最后一个参数决定了此钩子是系统钩子还是线程钩子。
   
    线程勾子用于监视指定线程的事件消息。线程勾子一般在当前线程或者当前线程派生的线程内。
   
    系统勾子监视系统中的所有线程的事件消息。因为系统勾子会影响系统中所有的应用程序,所以勾子函数必须放在独立的动态链接库(DLL) 中。系统自动将包含"钩子回调函数"的DLL映射到受钩子函数影响的所有进程的地址空间中,即将这个DLL注入了那些进程。

几点说明:
 (1)如果对于同一事件(如鼠标消息)既安装了线程勾子又安装了系统勾子,那么系统会自动先调用线程勾子,然后调用系统勾子。

 (2)对同一事件消息可安装多个勾子处理过程,这些勾子处理过程形成了勾子链。当前勾子处理结束后应把勾子信息传递给下一个勾子函数。

 (3)勾子特别是系统勾子会消耗消息处理时间,降低系统性能。只有在必要的时候才安装勾子,在使用完毕后要及时卸载。

三、钩子类型

    每一种类型的Hook可以使应用程序能够监视不同类型的系统消息处理机制。下面描述所有可以利用的Hook类型。

1、WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks

    WH_CALLWNDPROC和WH_CALLWNDPROCRET Hooks使你可以监视发送到窗口过程的消息。系统在消息发送到接收窗口过程之前调用WH_CALLWNDPROC Hook子程,并且在窗口过程处理完消息之后调用WH_CALLWNDPROCRET Hook子程。

    WH_CALLWNDPROCRET Hook传递指针到CWPRETSTRUCT结构,再传递到Hook子程。CWPRETSTRUCT结构包含了来自处理消息的窗口过程的返回值,同样也包括了与这个消息关联的消息参数。

2、WH_CBT Hook

    在以下事件之前,系统都会调用WH_CBT Hook子程,这些事件包括:
    1. 激活,建立,销毁,最小化,最大化,移动,改变尺寸等窗口事件;
    2. 完成系统指令;
    3. 来自系统消息队列中的移动鼠标,键盘事件;
    4. 设置输入焦点事件;
    5. 同步系统消息队列事件。
   
    Hook子程的返回值确定系统是否允许或者防止这些操作中的一个。

3、WH_DEBUG Hook

    在系统调用系统中与其他Hook关联的Hook子程之前,系统会调用WH_DEBUG Hook子程。你可以使用这个Hook来决定是否允许系统调用与其他Hook关联的Hook子程。

4、WH_FOREGROUNDIDLE Hook

    当应用程序的前台线程处于空闲状态时,可以使用WH_FOREGROUNDIDLE Hook执行低优先级的任务。当应用程序的前台线程大概要变成空闲状态时,系统就会调用WH_FOREGROUNDIDLE Hook子程。

5、WH_GETMESSAGE Hook

    应用程序使用WH_GETMESSAGE Hook来监视从GetMessage or PeekMessage函数返回的消息。你可以使用WH_GETMESSAGE Hook去监视鼠标和键盘输入,以及其他发送到消息队列中的消息。

6、WH_JOURNALPLAYBACK Hook

    WH_JOURNALPLAYBACK Hook使应用程序可以插入消息到系统消息队列。可以使用这个Hook回放通过使用WH_JOURNALRECORD Hook记录下来的连续的鼠标和键盘事件。只要WH_JOURNALPLAYBACK Hook已经安装,正常的鼠标和键盘事件就是无效的。WH_JOURNALPLAYBACK Hook是全局Hook,它不能象线程特定Hook一样使用。WH_JOURNALPLAYBACK Hook返回超时值,这个值告诉系统在处理来自回放Hook当前消息之前需要等待多长时间(毫秒)。这就使Hook可以控制实时事件的回放。WH_JOURNALPLAYBACK是system-wide local hooks,它們不會被注射到任何行程位址空間。

7、WH_JOURNALRECORD Hook

    WH_JOURNALRECORD Hook用来监视和记录输入事件。典型的,可以使用这个Hook记录连续的鼠标和键盘事件,然后通过使用WH_JOURNALPLAYBACK Hook来回放。WH_JOURNALRECORD Hook是全局Hook,它不能象线程特定Hook一样使用。WH_JOURNALRECORD是system-wide local hooks,它們不會被注射到任何行程位址空間。

8、WH_KEYBOARD Hook

    在应用程序中,WH_KEYBOARD Hook用来监视WM_KEYDOWN and WM_KEYUP消息,这些消息通过GetMessage or PeekMessage function返回。可以使用这个Hook来监视输入到消息队列中的键盘消息。

9、WH_KEYBOARD_LL Hook

    WH_KEYBOARD_LL Hook监视输入到线程消息队列中的键盘消息。

10、WH_MOUSE Hook

    WH_MOUSE Hook监视从GetMessage 或者 PeekMessage 函数返回的鼠标消息。使用这个Hook监视输入到消息队列中的鼠标消息。

11、WH_MOUSE_LL Hook

    WH_MOUSE_LL Hook监视输入到线程消息队列中的鼠标消息。

12、WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks

    WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以监视菜单,滚动条,消息框,对话框消息并且发现用户使用ALT+TAB or ALT+ESC 组合键切换窗口。WH_MSGFILTER Hook只能监视传递到菜单,滚动条,消息框的消息,以及传递到通过安装了Hook子程的应用程序建立的对话框的消息。WH_SYSMSGFILTER Hook监视所有应用程序消息。
   
    WH_MSGFILTER 和 WH_SYSMSGFILTER Hooks使我们可以在模式循环期间过滤消息,这等价于在主消息循环中过滤消息。
   
通过调用CallMsgFilter function可以直接的调用WH_MSGFILTER Hook。通过使用这个函数,应用程序能够在模式循环期间使用相同的代码去过滤消息,如同在主消息循环里一样。

13、WH_SHELL Hook

    外壳应用程序可以使用WH_SHELL Hook去接收重要的通知。当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,系统调用WH_SHELL Hook子程。
  WH_SHELL 共有5钟情況:
1. 只要有个top-level、unowned 窗口被产生、起作用、或是被摧毁;
2. 当Taskbar需要重画某个按钮;
3. 当系统需要显示关于Taskbar的一个程序的最小化形式;
4. 当目前的键盘布局状态改变;
5. 当使用者按Ctrl+Esc去执行Task Manager(或相同级别的程序)。

    按照惯例,外壳应用程序都不接收WH_SHELL消息。所以,在应用程序能够接收WH_SHELL消息之前,应用程序必须调用SystemParametersInfo function注册它自己。

                                                                     rivershan原创于2002年9月18日

 

DLL(Dynamic Link Libraries)专题:

    比较大的应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作。可能存在一些模块的功能较为通用,在构造其它软件系统时仍会被使用。在构造软件系统时,如果将所有模块的源代码都静态编译到整个应用程序EXE文件中,会产生一些问题:一个缺点是增加了应用程序的大小,它会占用更多的磁盘空间,程序运行时也会消耗较大的内存空间,造成系统资源的浪费;另一个缺点是,在编写大的EXE程序时,在每次修改重建时都必须调整编译所有源代码,增加了编译过程的复杂性,也不利于阶段性的单元测试。

    Windows系统平台上提供了一种完全不同的较有效的编程和运行环境,你可以将独立的程序模块创建为较小的DLL(Dynamic Linkable Library)文件,并可对它们单独编译和测试。在运行时,只有当EXE程序确实要调用这些DLL模块的情况下,系统才会将它们装载到内存空间中。这种方式不仅减少了EXE文件的大小和对内存空间的需求,而且使这些DLL模块可以同时被多个应用程序使用。Windows自己就将一些主要的系统功能以DLL模块的形式实现。

    一般来说,DLL是一种磁盘文件,以.DLL、.DRV、.FON、.SYS和许多以.EXE为扩展名的系统文件都可以是DLL。它由全局数据、服务函数和资源组成,在运行时被系统加载到进程的虚拟空间中,成为调用进程的一部分。如果与其它DLL之间没有冲突,该文件通常映射到进程虚拟空间的同一地址上。DLL模块中包含各种导出函数,用于向外界提供服务。DLL可以有自己的数据段,但没有自己的堆栈,使用与调用它的应用程序相同的堆栈模式;一个DLL在内存中只有一个实例;DLL实现了代码封装性;DLL的编制与具体的编程语言及编译器无关。

    在Win32环境中,每个进程都复制了自己的读/写全局变量。如果想要与其它进程共享内存,必须使用内存映射文件或者声明一个共享数据段。DLL模块需要的堆栈内存都是从运行进程的堆栈中分配出来的。Windows在加载DLL模块时将进程函数调用与DLL文件的导出函数相匹配。Windows操作系统对DLL的操作仅仅是把DLL映射到需要它的进程的虚拟地址空间里去。DLL函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有.       

一、关于调用方式:

1、静态调用方式:由编译系统完成对DLL的加载和应用程序结束时DLL卸载的编码(如还有其它程序使用该DLL,则Windows对DLL的应用记录减1,直到所有相关程序都结束对该DLL的使用时才释放它),简单实用,但不够灵活,只能满足一般要求。

 隐式的调用:需要把产生动态连接库时产生的.LIB文件加入到应用程序的工程中,想使用DLL中的函数时,只须说明一下。隐式调用不需要调用LoadLibrary()和FreeLibrary()。程序员在建立一个DLL文件时,链接程序会自动生成一个与之对应的LIB导入文件。该文件包含了每一个DLL导出函数的符号名和可选的标识号,但是并不含有实际的代码。LIB文件作为DLL的替代文件被编译到应用程序项目中。当程序员通过静态链接方式编译生成应用程序时,应用程序中的调用函数与LIB文件中导出符号相匹配,这些符号或标识号进入到生成的EXE文件中。LIB文件中也包含了对应的DLL文件名(但不是完全的路径名),链接程序将其存储在EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows根据这些信息发现并加载DLL,然后通过符号名或标识号实现对DLL函数的动态链接。所有被应用程序调用的DLL文件都会在应用程序EXE文件加载时被加载在到内存中。可执行程序链接到一个包含DLL输出函数信息的输入库文件(.LIB文件)。操作系统在加载使用可执行程序时加载DLL。可执行程序直接通过函数名调用DLL的输出函数,调用方法和程序内部其他的函数是一样的。


2、动态调用方式:是由编程者用API函数加载和卸载DLL来达到调用DLL的目的,使用上较复杂,但能更加有效地使用内存,是编制大型应用程序时的重要方式。

 显式的调用:是指在应用程序中用LoadLibrary或MFC提供的AfxLoadLibrary显式的将自己所做的动态连接库调进来,动态连接库的文件名即是上面两个函数的参数,再用GetProcAddress()获取想要引入的函数。自此,你就可以象使用如同本应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary或MFC提供的AfxFreeLibrary释放动态连接库。直接调用Win32 的LoadLibary函数,并指定DLL的路径作为参数。LoadLibary返回HINSTANCE参数,应用程序在调用GetProcAddress函数时使用这一参数。GetProcAddress函数将符号名或标识号转换为DLL内部的地址。程序员可以决定DLL文件何时加载或不加载,显式链接在运行时决定加载哪个DLL文件。使用DLL的程序在使用之前必须加载(LoadLibrary)加载DLL从而得到一个DLL模块的句柄,然后调用GetProcAddress函数得到输出函数的指针,在退出之前必须卸载DLL(FreeLibrary)。

    Windows将遵循下面的搜索顺序来定位DLL:
1.包含EXE文件的目录,
2.进程的当前工作目录,
3.Windows系统目录,
4.Windows目录,
5.列在Path环境变量中的一系列目录。

二、MFC中的dll:

a、Non-MFC DLL:指的是不用MFC的类库结构,直接用C语言写的DLL,其输出的函数一般用的是标准C接口,并能被非MFC或MFC编写的应用程序所调用。

b、Regular DLL:和下述的Extension Dlls一样,是用MFC类库编写的。明显的特点是在源文件里有一个继承CWinApp的类。其又可细分成静态连接到MFC和动态连接到MFC上的。

静态连接到MFC的动态连接库只被VC的专业般和企业版所支持。该类DLL应用程序里头的输出函数可以被任意Win32程序使用,包括使用MFC的应用程序。输入函数有如下形式:
extern "C" EXPORT YourExportedFunction( );
如果没有extern “C”修饰,输出函数仅仅能从C++代码中调用。
DLL应用程序从CWinApp派生,但没有消息循环。

动态链接到MFC的规则DLL应用程序里头的输出函数可以被任意Win32程序使用,包括使用MFC的应用程序。但是,所有从DLL输出的函数应该以如下语句开始:
AFX_MANAGE_STATE(AfxGetStaticModuleState( ))
此语句用来正确地切换MFC模块状态。

Regular DLL能够被所有支持DLL技术的语言所编写的应用程序所调用。在这种动态连接库中,它必须有一个从CWinApp继承下来的类,DllMain函数被MFC所提供,不用自己显式的写出来。

c、Extension DLL:用来实现从MFC所继承下来的类的重新利用,也就是说,用这种类型的动态连接库,可以用来输出一个从MFC所继承下来的类。它输出的函数仅可以被使用MFC且动态链接到MFC的应用程序使用。可以从MFC继承你所想要的、更适于你自己用的类,并把它提供给你的应用程序。你也可随意的给你的应用程序提供MFC或MFC继承类的对象指针。Extension DLL使用MFC的动态连接版本所创建的,并且它只被用MFC类库所编写的应用程序所调用。Extension DLLs 和Regular DLLs不一样,它没有一个从CWinApp继承而来的类的对象,所以,你必须为自己DllMain函数添加初始化代码和结束代码。

和规则DLL相比,有以下不同:

1、它没有一个从CWinApp派生的对象;
2、它必须有一个DllMain函数;
3、DllMain调用AfxInitExtensionModule函数,必须检查该函数的返回值,如果返回0,DllMmain也返回0;
4、如果它希望输出CRuntimeClass类型的对象或者资源(Resources),则需要提供一个初始化函数来创建一个CDynLinkLibrary对象。并且,有必要把初始化函数输出;
5、使用扩展DLL的MFC应用程序必须有一个从CWinApp派生的类,而且,一般在InitInstance里调用扩展DLL的初始化函数。

三、dll入口函数:

1、每一个DLL必须有一个入口点,DllMain是一个缺省的入口函数。DllMain负责初始化(Initialization)和结束(Termination)工作,每当一个新的进程或者该进程的新的线程访问DLL时,或者访问DLL的每一个进程或者线程不再使用DLL或者结束时,都会调用DllMain。但是,使用TerminateProcess或TerminateThread结束进程或者线程,不会调用DllMain。

DllMain的函数原型:
BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{
 switch(ul_reason_for_call)
 {
 case DLL_PROCESS_ATTACH:
 …….
 case DLL_THREAD_ATTACH:
 …….
 case DLL_THREAD_DETACH:
 …….
 case DLL_PROCESS_DETACH:
 …….
 return TRUE;
 }
}

参数:
hMoudle:是动态库被调用时所传递来的一个指向自己的句柄(实际上,它是指向_DGROUP段的一个选择符);
ul_reason_for_call:是一个说明动态库被调原因的标志。当进程或线程装入或卸载动态连接库的时候,操作系统调用入口函数,并说明动态连接库被调用的原因。它所有的可能值为:
DLL_PROCESS_ATTACH: 进程被调用;
DLL_THREAD_ATTACH: 线程被调用;
DLL_PROCESS_DETACH: 进程被停止;
DLL_THREAD_DETACH: 线程被停止;
lpReserved:是一个被系统所保留的参数。

2、_DllMainCRTStartup

 为了使用“C”运行库(CRT,C Run time Library)的DLL版本(多线程),一个DLL应用程序必须指定_DllMainCRTStartup为入口函数,DLL的初始化函数必须是DllMain。

 _DllMainCRTStartup完成以下任务:当进程或线程捆绑(Attach)到DLL时为“C”运行时的数据(C Runtime Data)分配空间和初始化并且构造全局“C++”对象,当进程或者线程终止使用DLL(Detach)时,清理C Runtime Data并且销毁全局“C++”对象。它还调用DllMain和RawDllMain函数。

 RawDllMain在DLL应用程序动态链接到MFC DLL时被需要,但它是静态的链接到DLL应用程序的。在讲述状态管理时解释其原因。

四、关于约定:

动态库输出函数的约定有两种:调用约定和名字修饰约定。

1)调用约定(Calling convention):决定函数参数传送时入栈和出栈的顺序,由调用者还是被调用者把参数弹出栈,以及编译器用来识别函数名字的修饰约定。

函数调用约定有多种,这里简单说一下:

   1、__stdcall调用约定相当于16位动态库中经常使用的PASCAL调用约定。在32位的VC++5.0中PASCAL调用约定不再被支持(实际上它已被定义为__stdcall。除了__pascal外,__fortran和__syscall也不被支持),取而代之的是__stdcall调用约定。两者实质上是一致的,即函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈,但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明)。

    _stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。

    2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。

    _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。

    3、__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。

    _fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。   

    4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。

    5、naked call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。

    关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting…\C/C++ \Code Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。

    要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。

2)名字修饰约定

1、修饰名(Decoration name)

“C”或者“C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出“C++”重载函数、构造函数、析构函数,又如在汇编代码里调用“C””或“C++”函数等。

修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。

2、名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。

    a、C编译时函数名修饰约定规则:

 __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number

 __cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
  
 __fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。

    它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。

    b、C++编译时函数名修饰约定规则:

__stdcall调用约定:
          1、以“?”标识函数名的开始,后跟函数名;
          2、函数名后面以“@@YG”标识参数表的开始,后跟参数表;
          3、参数表以代号表示:
             X–void ,
             D–char,
             E–unsigned char,
             F–short,
             H–int,
             I–unsigned int,
             J–long,
             K–unsigned long,
             M–float,
             N–double,
             _N–bool,
             ….
             PA–表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;
          4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
          5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。

    其格式为“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如
          int Test1(char *var1,unsigned long)—–“?Test1@@YGHPADK@Z
          void Test2()                       —–“?Test2@@YGXXZ

__cdecl调用约定:
 规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。

__fastcall调用约定:
 规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的“@@YG”变为“@@YI”。

    VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用.
   
五、关于DLL的函数:

    动态链接库中定义有两种函数:导出函数(export function)和内部函数(internal function)。导出函数可以被其它模块调用,内部函数在定义它们的DLL程序内部使用。

输出函数的方法有以下几种:

1、传统的方法

 在模块定义文件的EXPORT部分指定要输入的函数或者变量。语法格式如下:
entryname[=internalname] [@ordinal[NONAME]] [DATA] [PRIVATE]

其中:

entryname是输出的函数或者数据被引用的名称;

internalname同entryname;

@ordinal表示在输出表中的顺序号(index);

NONAME仅仅在按顺序号输出时被使用(不使用entryname);

DATA表示输出的是数据项,使用DLL输出数据的程序必须声明该数据项为_declspec(dllimport)。

上述各项中,只有entryname项是必须的,其他可以省略。

 对于“C”函数来说,entryname可以等同于函数名;但是对“C++”函数(成员函数、非成员函数)来说,entryname是修饰名。可以从.map映像文件中得到要输出函数的修饰名,或者使用DUMPBIN /SYMBOLS得到,然后把它们写在.def文件的输出模块。DUMPBIN是VC提供的一个工具。

如果要输出一个“C++”类,则把要输出的数据和成员的修饰名都写入.def模块定义文件。

2、在命令行输出

 对链接程序LINK指定/EXPORT命令行参数,输出有关函数。

3、使用MFC提供的修饰符号_declspec(dllexport)

 在要输出的函数、类、数据的声明前加上_declspec(dllexport)的修饰符,表示输出。__declspec(dllexport)在C调用约定、C编译情况下可以去掉输出函数名的下划线前缀。extern "C"使得在C++中使用C编译方式成为可能。在“C++”下定义“C”函数,需要加extern “C”关键词。用extern "C"来指明该函数使用C编译方式。输出的“C”函数可以从“C”代码里调用。
   
    例如,在一个C++文件中,有如下函数:
    extern "C" {void  __declspec(dllexport) __cdecl Test(int var);}
其输出函数名为:Test
 
 MFC提供了一些宏,就有这样的作用。

AFX_CLASS_IMPORT:__declspec(dllexport)
 
AFX_API_IMPORT:__declspec(dllexport)
 
AFX_DATA_IMPORT:__declspec(dllexport)
 
AFX_CLASS_EXPORT:__declspec(dllexport)
 
AFX_API_EXPORT:__declspec(dllexport)
 
AFX_DATA_EXPORT:__declspec(dllexport)
 
AFX_EXT_CLASS: #ifdef _AFXEXT
    AFX_CLASS_EXPORT
    #else
    AFX_CLASS_IMPORT
 
AFX_EXT_API:#ifdef _AFXEXT
    AFX_API_EXPORT
    #else
    AFX_API_IMPORT
 
AFX_EXT_DATA:#ifdef _AFXEXT
     AFX_DATA_EXPORT
     #else
     AFX_DATA_IMPORT

 像AFX_EXT_CLASS这样的宏,如果用于DLL应用程序的实现中,则表示输出(因为_AFX_EXT被定义,通常是在编译器的标识参数中指定该选项/D_AFX_EXT);如果用于使用DLL的应用程序中,则表示输入(_AFX_EXT没有定义)。

 要输出整个的类,对类使用_declspec(_dllexpot);要输出类的成员函数,则对该函数使用_declspec(_dllexport)。如:

class AFX_EXT_CLASS CTextDoc : public CDocument
{
 …
}

extern "C" AFX_EXT_API void WINAPI InitMYDLL();

 这几种方法中,最好采用第三种,方便好用;其次是第一种,如果按顺序号输出,调用效率会高些;最次是第二种。 

六、模块定义文件(.DEF)

 模块定义文件(.DEF)是一个或多个用于描述DLL属性的模块语句组成的文本文件,每个DEF文件至少必须包含以下模块定义语句:

* 第一个语句必须是LIBRARY语句,指出DLL的名字;
* EXPORTS语句列出被导出函数的名字;将要输出的函数修饰名罗列在EXPORTS之下,这个名字必须与定义函数的名字完全一致,如此就得到一个没有任何修饰的函数名了。
* 可以使用DESCRIPTION语句描述DLL的用途(此句可选);
* ";"对一行进行注释(可选)。

七、DLL程序和调用其输出函数的程序的关系

1、dll与进程、线程之间的关系

DLL模块被映射到调用它的进程的虚拟地址空间。
DLL使用的内存从调用进程的虚拟地址空间分配,只能被该进程的线程所访问。
DLL的句柄可以被调用进程使用;调用进程的句柄可以被DLL使用。
DLL使用调用进程的栈。

2、关于共享数据段

 DLL定义的全局变量可以被调用进程访问;DLL可以访问调用进程的全局数据。使用同一DLL的每一个进程都有自己的DLL全局变量实例。如果多个线程并发访问同一变量,则需要使用同步机制;对一个DLL的变量,如果希望每个使用DLL的线程都有自己的值,则应该使用线程局部存储(TLS,Thread Local Strorage)。

    在程序里加入预编译指令,或在开发环境的项目设置里也可以达到设置数据段属性的目的.必须给这些变量赋初值,否则编译器会把没有赋初始值的变量放在一个叫未被初始化的数据段中。                             

(13)“整型变量仅仅意味着一个整数”

  当我们还是一个新手,看整型就是整数;

  当我们成为高手,看什么都是整型。

  整型,在所有C/C++基本数据类型中最富有艺术魅力和奇幻色彩。

  我们从某著名论坛的一篇帖子开始一窥整型的奥妙。

  问:Vxworks操作系统启动一个任务的函数是taskSpawn(char* name, int priority, int options, int stacksize, FUNCPTR function, int arg1,.. , int arg10),它只接受整型参数,我该怎么办才能给它传一个结构体(在32位PowerPC平台下)?

  答:可以传入结构体的指针,在32位PowerPC平台下,指针本质上就是一个32位整数,在函数体内将整型强制转化为结构体指针就可访问结构体的每一个元素。

  如:

//启动任务1
taskSpawn(“task1”, 180, NULL, 10000, Task1Fun, &pStructAr,0,0,0,0,0,0,0,0,0);
//task1函数
Task1Fun ( int arg1 )
{
struct_x * pStructx = (struct_x *) arg1; //将整型强制转化为结构体指针

}
  在此提出“泛整型”的概念,(unsigned)char、(unsigned)short int、(unsigned)int、(unsigned)long int等都属于这个范畴,指针必然属于“泛整型”的范围。用指针的高超境界,也为将其看做一个“泛整型”。

  看看软件的详细设计文档,其数据结构定义部分经常看到“INT8、UINT8、INT16、UINT16、INT32、UINT32、INT64、UINT64”或“BYTE、WORD、DWORD”等数据类型,它们在本质上都是(unsigned)char、(unsigned)short int、(unsigned)int、(unsigned)long int宏定义的结果,都属于“泛整型”。所以,“泛整型”的概念真实地体现在日常的软件设计当中。
正因为各种指针类型在本质上都是“泛整型”,因此它们可以互相转化:

int a, b;
memset( (char*) &a, (char*) &b, sizeof(int) );


  等价于:

int a, b;
a = b;


  从来没有人会用memset( (char*) &a, (char*) &b, sizeof(int) )来代替a = b,这里只是为了说明问题。下面的代码则经常用到:

int *p = (int *) malloc(100*sizeof(int));
memset ( p, 0, 100*sizeof(int) ); //将申请的内存空间清0


  我们看memset的函数原型为:

void * memset ( void * buffer, int c, size_t num );


  实际上它接受的第一个参数是无类型指针,在memset函数体内,其它任意类型的指针都向void *转化了。类似的内存操作函数memcpy所接受的源和目的内存地址也是无类型指针。

  char *转化为int *后的值虽然不变(还是那个地址),但是其++、–等操作的含义却发生了变化,这也是要注意的。
char *p;
++p;

  与
char *p;
++(int *)p;

  的结果是不一样的,前者的p值加了1,而后者的则增加了sizeof(int)。

  下面来剥Windows程序设计中消息传递函数两个参数的皮,看看它们究竟是什么:

typedef UINT WPARAM;
typedef LONG LPARAM;


  原来,WPARAM和LPARAM其实都属于“泛整型”,所以不要报怨消息处理函数只能接受“泛整型”。实际上,从指针的角度上来讲,在C/C++中,可以获得任何类型实例(变量、结构、类)的指针,所以Windows的消息处理函数实际上可以接受一切类型的参数。

  惊天动地一句话:“泛整型”可表征一切。
(14)“值传递一定不会改变参数”

  理论而言,值传递的确不会改变参数的内容。但是,某年某月的某一天,隔壁office的硕士mm写了这么一段程序,参数的值却被改变了:

int n = 9;
char a[10];
example ( n, a ); //调用函数example(int n,char *pStr)
printf (“%d”, n );  //输出结果不是9


  大概整个office的人都被搞懵了,都说编译器瞎搞,有问题。找到笔者,笔者凭借以往的经常,一眼就看出来不是什么编译器出错,而是在函数example内对字符串a的访问越界!

  当在函数example内对a的访问越界后,再进行写操作时,就有可能操作到了n所在的内存空间,于是改变了n的值。

  给出这个语录,并非为了推翻“值传递不会改变参数”的结论,而是为了从侧面证明在C/C++语言中,数组越界是多么危险的错误!

  下面的两个函数有明显的数组越界:

void example1()
{
char string[10];
char* str1 = "0123456789";
strcpy( string, str1 );
}
void example 2(char* str1)
{
char string[10];
if( strlen( str1 ) <= 10 )
{
strcpy( string, str1 );
}
}


  而这个函数的越界就不这么明显:

void example3()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1[i] = ‘a’;
}
strcpy( string, str1 );
}


  其实,这个函数危险到了极点。因为对于strcpy函数而言,拷贝的时候要碰到’\0’才结束,str1并没有被赋予结束符,因而你根本就不知道strcpy( string, str1 )的结果究竟会是拷贝多大一片内存!

  遗憾的是,C/C++永远不会在编译和连接阶段提示数组越界,它只会在运行阶段导致程序的崩溃。

  数组越界,是大多数C/C++编程新手常犯的错误,而它又具有极大的隐蔽性,新手们一定要特别注意。
(15)“C不高级,学C++、JAVA、C#才够味”

  也许谭浩强老师的C语言教材是绝大多数高校学生学习的第一门编程课程,所以在许多学生的心目中,觉得C是一种入门级的语言,他们舍弃基础而追逐花哨的Visual XXX、Java、ASP、PHP、.net,他们以为这样做“赚大了”。

  非也!

  C是一种多么富有魅力的语言!在今时的绝对多数底层开发中,仍然几乎被C完全垄断。这些领域包括操作系统、嵌入式系统、数字信号处理等。舍弃C的经济基础搭.net的高层建筑实在是危险。

  我们总是以为自己掌握了C,那么请写一个strcpy的标准函数。您的答案若是:

void strcpy( char *strDest, char *strSrc )
{
  while( (*strDest++ = * strSrc++) != ‘\0’ );
}


  很遗憾,您的程序只能拿到E。看看拿A的strcpy:

char * strcpy( char *strDest, const char *strSrc )
{
 assert( (strDest != NULL) && (strSrc != NULL) );
char *address = strDest;
 while( (*strDest++ = * strSrc++) != ‘\0’ );
return address;
}


  这个程序考虑了什么?

  (1)程序要强大:为了实现链式操作,将目的地址返回,函数返回类型改为char *

  (2)程序要可读:源字符串指针参数加const限制,表明为输入参数

  (3)程序要健壮:验证strDest和strSrc非空

  如果这三点中您只考虑到0点或1点,那么请回家好好修炼一下C。因为这个最简单的strcpy已验证出您的C语言基础只能叫做“入门”。

  再写个简单的strlen,这么写就好了:

int strlen( const char *str ) //输入参数为const
{
assert( strt != NULL ); //断言字符串地址非0
int len;
while( (*str++) != ‘\0′ )
{
len++;
}
return len;
}


  由此可见,写好这些简单的函数也需要深厚的基本功,永远不要放弃对基本功的培养。
(16)“语言学得越多越好”

  许多的初学者都经历过这样的一个阶段,面对大量的编程语言和开发环境,他们俩感到难以取舍,不知道自己究竟应该学习什么。于是他们什么都学,今天看一下Visual Basic,明天看学一下C++,后天在书点看到了本Java便心血来潮买回来翻翻,大后天又发现必须学.net了。他们很痛苦,什么都在看,结果什么都没学会,忙忙碌碌而收获甚微。

  我们真的没有必要在什么语言都不甚精通的情况下乱看一气。认准了一种真正语言就应该坚持不懈地努力。因为任何一门语言的掌握都非一朝一夕一事,笔者从六年前开始接触C++,直到现在,每一阶段仍有新的启发,在项目开发的过程中也不断有新的收获。今日我还是绝对不敢宣称自己“精通”这门语言。

  许多刚毕业的大学生,动不动就在简历上写上自己精通一堆语言。与之相反,大多数优秀的工程师都不敢这么写。也许,研究越深,便越敢自身的无知。

  在下认为,一个成熟的语言体系应该是:
  程序员的语言体系 = 一种汇编 + C + 一种面向对象(C++、JAVA、C#等)

  如果还要加,那就加一种解释型语言,perl或tcl(也许其它)。

  语言具有极大的相似性,从C++过渡到JAVA只需要很短的一段时间。各种语言的发展历史也体现了编程思想的发展史。我们学习一种语言,语法也许并不是最重要的,最重要的是蕴藏在语法外表下的深层特性和设计用意。

  本回书着落此处,更多错误语录,当然是待续。

废话不说,直接进入正题,本文承接先前发布的《C/C++编程新手错误语录》,继续归纳错误语录。

(8)“我想用malloc”、“我用不好malloc”
  来看看一个变态程序:

/* xx.c:xx模块实现文件 */
int *pInt;
/* xx模块的初始化函数 */
xx_intial()
{
pInt = ( int * ) malloc ( sizeof( int ) );

}
/* xx模块的其他函数(仅为举例)*/
xx_otherFunction()
{
*Int = 10;

}


  这个程序定义了一个全局整型变量指针,在xx模块的初始化函数中对此指针动态申请内存,并将pInt指向该内存首地址,并在xx模块的其他函数中都使用pInt指针对其指向的整数进行读取和赋值。

  这个程序让我痛不欲生了好多天,扼腕叹息!这是我母校计算机系一位硕士的作品!作者为了用上malloc,拼命地把本来应该用一个全局整型变量摆平的程序活活弄成一个全局整型指针并在初始化函数中“动态”申请内存,自作聪明而正好暴露自己的无知!我再也不要见到这样的程序。

  那么malloc究竟应该怎么用?笔者给出如下规则:
  规则1 不要为了用malloc而用malloc,malloc不是目的,而是手段;

  规则2 malloc的真正内涵体现在“动态”申请,如果程序的特性不需动态申请,请不要用malloc;

  上面列举的变态程序完全不具备需要动态申请的特质,应该改为:

/* xx.c:xx模块实现文件 */
int example;
/* xx模块的初始化函数 */
xx_intial()
{

}
/* xx模块的其他函数(仅为举例) */
xx_otherFunction()
{
example = 10;

}

        规则3 什么样的程序具备需要动态申请内存的特质呢?包含两种情况:
  (1)不知道有多少要来,来了的又走了
  不明白?这么说吧,譬如你正在处理一个报文队列,收到的报文你都存入该队列,处理完队列头的报文后你需要取出队列头的元素。

  你不知道有多少报文来(因而你不知道应该用多大的报文数组),这些来的报文处理完后都要走(释放),这种情况适合用malloc和free。

  (2)慢慢地长大
  譬如你在资源受限的系统中编写一文本编辑器程序,你怎么做,你需要这样定义数组吗?

  char str[10000];

  不,你完全不应该这么做。即使你定义了一个10000字节大的字符串,用户如果输入10001个字符你的程序就完完了。

  这个时候适合用malloc,因为你根本就不知道用户会输入多少字符,文本在慢慢长大,因而你也应慢慢地申请内存,用一个队列把字符串存放起来。

  那么是不是应该这样定义数据结构并在用户每输入一个字符的情况下malloc一个CharQueue空间呢?

typedef struct tagCharQueue
{
char ch;
struct tagCharQueue *next;
}CharQueue;


  不,这样做也不对!这将使每个字符占据“1+指针长度”的开销。

  正确的做法是:

typedef struct tagCharQueue
{
char str[100];
struct tagCharQueue *next;
}CharQueue;


  让字符以100为单位慢慢地走,当输入字符数达到100的整数倍时,申请一片CharQueue空间。


        规则4 malloc与free要成对出现
  它们是一对恩爱夫妻,malloc少了free就必然会慢慢地死掉。成对出现不仅体现在有多少个malloc就应该有多少个free,还体现在它们应尽量出现在同一函数里,“谁申请,就由谁释放”,看下面的程序:

char * func(void)
{
char *p;
p = (char *)malloc(…);
if(p!=NULL)
…; /* 一系列针对p的操作 */
return p;
}
/*在某处调用func(),用完func中动态申请的内存后将其free*/
char *q = func();

free(q);


  上述代码违反了malloc和free的“谁申请,就由谁释放”原则,代码的耦合度大,用户在调用func函数时需确切知道其内部细节!正确的做法是:

/* 在调用处申请内存,并传入func函数 */
char *p=malloc(…);
if(p!=NULL)
{
func(p);

free(p);
p=NULL;
}
/* 函数func则接收参数p */
void func(char *p)
{
… /* 一系列针对p的操作 */
}


  规则5 free后一定要置指针为NULL,防止其成为“野”指针

(9)“函数add编译生成的符号就是add”

int add(int x,int y)
{
return x + y;
}
float add(float x,float y)
{
return x + y;
}

          即便是在C语言中,add函数被多数C编译器编译后在符号库中的名字也不是add,而是_add。而在C++编译器中,int add(int x,int y)会编译成类似_add_int_int这样的名字(称为“mangled name”),float add(float x,float y)则被编译成_add_float _float,mangled name包含了函数名、函数参数数量及类型信息,C++依靠这种机制来实现函数重载。

  所以,在C++中,本质上int add( int x, int y )与float add( float x, float y )是两个完全不同的函数,只是在用户看来其同名而已。

  这就要求初学者们能透过语法现象看问题本质。本质上,语言的创造者们就是在玩各种各样的花样,以使语言具备某种能力,譬如mangled name花样的目的在于使C++支持重载。而C语言没有玩这样的花样,所以int add( int x, int y )与float add( float x, float y )不能在C程序中同时存在。

(10)“没见过在C语言中调用C++的函数”、“C/C++不能调用Basic、Pascal语言的函数”
  这又是一个奇天下之大怪的问题,“打死我都不相信C、C++、basic、pascal的函数能瞎调来调去”,可是有句话这么说:
  没有你见不到的,只有你想不到的!

  既然芙蓉姐姐也有其闻名天下的道理,那么C、C++、Basic、Pascal的函数为什么就不能互相调用呢?
  能!

  你可以用Visual C++写一个DLL在Visual Basic、Delphi(Pascal的孙子,Object Pascal的儿子)中调用,也可以在Visual Basic、Delphi中写一个DLL在Visual C++中调用不是?

  让我们来透过现象看本质。首先看看函数的调用约定(以Visual C++来说明):
  (1) _stdcall调用
  _stdcall是Pascal程序的缺省调用方式,参数采用从右到左的压栈方式,被调函数自身在返回前清空堆栈。
  WIN32 Api都采用_stdcall调用方式,这样的宏定义说明了问题:

  #define WINAPI _stdcall

  按C编译方式,_stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number。

       (2) _cdecl调用
  _cdecl是C/C++的缺省调用方式,参数采用从右到左的压栈方式,传送参数的内存栈由调用者维护。_cedcl约定的函数只能被C/C++调用,每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。

  由于_cdecl调用方式的参数内存栈由调用者维护,所以变长参数的函数能(也只能)使用这种调用约定。关于C/C++中变长参数(…)的问题,笔者将另文详述。

  由于Visual C++默认采用_cdecl 调用方式,所以VC中中调用DLL时,用户应使用_stdcall调用约定。
  按C编译方式,_cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。

  (3) _fastcall调用
  _fastcall调用较快,它通过CPU内部寄存器传递参数。

  按C编译方式,_fastcall调用约定在输出函数名前面加“@”符号,后面加“@”符号和参数的字节数,形如@functionname@number。

  关键字_stdcall、_cdecl和_fastcall可以直接加在函数前,也可以在Visual C++中设置

  在创建DLL时,一般使用_stdcall调用(Win32 Api方式),采用_functionname@number命名规则,因而各种语言间的DLL能互相调用。也就是说,DLL的编制与具体的编程语言及编译器无关,只要遵守DLL的开发规范和编程策略,并安排正确的调用接口,不管用何种编程语言编制的DLL都具有通用性。

  推而广之,如果有这样一个IDE开发环境,它能识别各种语言,所有语言采用相同的调用约定和命名规则,一个软件内各种语言书写的函数将能互相调用!

  这个世界上可能永远不需要这样一个IDE。

(11)“英语、数学不好就学不好C/C++”

         这也许是20世纪最大的谎言,这句话最先是哪位大师的名人名言已无可考证,可此后一批批的人被它误导。许多初学者因为这句话被吓倒,放弃了做程序员的理想。还有许多后来成为优秀程序员的人,在他们的成长过程中并没有依靠深奥的数学,可他们还是在总结经验时制造恐慌,号称一定要具备高深的数学知识,唯恐别人笑话其学术水平不高。

  在下则认为,大多数情况下,程序设计不需要太深奥的数学功底,除非你所从事的程序设计涉及特定的专业领域(如语音及图像处理、数字通信技术等)。在下这一观点也许是革旧立新,而革命必然要流血牺牲(谭嗣同),所以恭候大家板砖。

  那么英语在C/C++的学习中处于什么地位呢?那就是能看懂资料,看懂MSDN。

  学编程的终极之道不在看书,而在大量地不断地实践。

(12)“C++太难了,我学不会”
  又不知是谁的悲观论调,许多初学者被C++吓倒,“太难了,我学不好”,如弱者自怜。如果C++真的难到学不会,那么C++的创造者们所从事的工作岂不是“非人力所能及也”?  
 
  在下认为,学习C++的态度应该是:战略上藐视它,战术上重视它,要敢于胜利(《毛主席语录》)。当然也不可轻敌,不能因为掌握了一点皮毛就以为自己牛B轰轰了(笔者曾经牛B轰轰了好一阵子,现在想来,甚觉当时幼稚)。

  如果你征服了C++,透彻理解了C++的语言特性及STL,那么,其他语言想不被你征服都难了。

  本回书着落此处,更多错误语录,当然是待续。