2005年03月30日

  PE文件中呼叫另一模块中的函数的运行机制

要点:PE文件中,当呼叫另一模块中的函数,编译器产生的CALL指令并不会把控制权直接传给DLL中的函数,而是传给一个JMP
DWORD PTR[xxxxxxxx]
指令。

呼叫外部的DLLs,其实并不是直接呼叫DLL本身,而是跳到一块存放有JMP
DWORD PTR [XXXXXXXX]
指令的存储区域去(可能放在.text或是.icode)。不过若在VC++中使用__declspec(dllimport)进行函数呼叫,编译器不会在模块的其他地方产生JMP指令,而是直接就会产生CALL DWORD PTR[XXXXXXXX]。不论哪种情况,JMPCALL指令中的位址都存放在.idata节的一个DWORD
值中(这个DWORD内含该函数的真正位址,即函数入口点)。JMPCALL指令会把控制权转给该位址。.

一个PE文件呼叫imported function的图示( User32.dll中的GetMessage函数)

上述图示中的偏移地址BFC0847D才是真正指向User32.dll模块中的GetMessage()函数。

   为什么DLL的呼叫需要以此方式实现?

原因是把对同一个DLL函数的所有呼叫都集中到一处,加载器就不再需要修补每一个呼叫DLL的指令。PE加载器要做的,只是把DLL函数的真实位址放到.idata的那个DWORD之中,根本就没有程序码需要修改。(不象NE文件的每一个节段内含一串待修正记录fixup records)。PE文件这种处理方式的缺点:不能够以DLL函数的真正位址初始化一个变量,如:FARPROC pfnGetMessage=GetMessage ;

关于引入表中OrignalFirstThunkFirstThunk两值的作用?

系统在程序初始化时,根据OrignalFirstThunk的值找到函数名,然后调用GetProcAddress函数,根据函数名取得函数的入口地址,然后用函数入口地址取代FirstThunk指向的地址串中对应的值。有的程序OrignalFirstThunk的值为0,则初始化时系统根据FirstThunk的值找到指向函数名的地址串,由地址串找到函数名,再根据函数名得到入口地址,然后用入口地址取代FirstThunk指向的地址串中的原值。

根据这个说明可以看出,在添加新的引入函数的时候,真正重要的是FirstThunk处的值,OrignalFirstThunk填不填无关紧要,要填的话,就得填入一个真正有效的值,不然就使其为0

为什么由两个并行的指针数组指向IMAGE_IMPORT_BY_NAME结构呢?第一个Characteristics是单独的一项,而且不可改写,它有时被称为提示名表(Hint Name Table。第二个数组(FirstThunk所指)是由PE装入器重写的。装载程序迭代搜索数组中的每一个指针,找到每一个IMAGE_IMPORT_BY_NAME结构所指的输入函数的地址,然后装载器找到程序的地址改写IMAGE_IMPORT_BY_NAME指针。Jmp dword ptr [xxxxxxxx]中的[xxxxxxxx]是指First Thunk数组中的一个入口。因为它被称为输入地址表(Import Address Table


1DOS MZ header   (64(0×40)个字节,偏移0×00—0×3F)

这是一个常规的实模式下的DOS文件头,为的是保持和DOS的兼容。从编程角度看,DOS MZ header 又定义成结构IMAGE_DOS_HEADER 。查询windows.inc,我们知道
IMAGE_DOS_HEADER 结构的e_magic成员应包含字符“MZ,e_lfanew成员就是指向 PE header 的文件偏移量。

2, DOS
stub  
(一般为112(0×70)个字节,偏移0×40—0xAF,但不是一定的)

这是个DOS的代理程序块,具体内容根据链接参数决定,不过大多数是一个简单的显示语句,如:This
Program is intended to run under Windows System

3PE header  (4+20+224=248字节,其各成员可表示为e_lfanew+偏移量)

PE header PE相关结构 IMAGE_NT_HEADERS 的简称,其中包含了许多PE装载器用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从 DOS MZ header 中找到 PE header 的起始偏移量(一般是0×3C处)。因而跳过了 DOS stub 直接定位到真正的文件头 PE header从编程角度看,PE header 实际就是一个 IMAGE_NT_HEADERS 结构。

定义如下:

          IMAGE_NT_HEADERS STRUCT

Signature
dd ?

FileHeader
IMAGE_FILE_HEADER <>

OptionalHeader
IMAGE_OPTIONAL_HEADER32 <>

        
IMAGE_NT_HEADERS ENDS

A.      
Signature是个DWORD类型的值,值为50h, 45h,
00h, 00h
PE\0\0)。它是PE签名。我们可以据此识别给定文件是否为有效PE文件。

注:如果IMAGE_NT_HEADERSsignature域值等于“PE\0\0″,那么就是有效的PE文件。实际上,为了比较方便,Microsoft已定义了常量IMAGE_NT_SIGNATURE供我们使用。

             IMAGE_DOS_SIGNATURE
equ 5A4Dh

IMAGE_OS2_SIGNATURE
equ 454Eh

IMAGE_OS2_SIGNATURE_LE
equ 454Ch

IMAGE_VXD_SIGNATURE
equ 454Ch

IMAGE_NT_SIGNATURE
equ 4550h

B.      
FileHeader 该结构域包含了关于PE文件物理分布的信息,比如节数目、文件执行机器等。

               
Typedef struct _IMAGE_FILE_HEADER{

USHORT Machine;

USHORT NumberOfSections;     //这个文件中的节的数目。

ULONG  TimeDateStamp;

ULONG  PointerToSymbolTable;

ULONG  NumberOfSymbols;

USHORT SizeOfOptionalHeader;

USHORT Characteristics;

                      }IMAGE_FILE_HEADER,
*PIMAGE_FILE_HEADER;

C.        
OptionalHeader 该结构域包含了关于PE文件逻辑分布的信息,虽然域名有可选字样,但实际上本结构总是存在的。

运用:

如何才能校验指定文件是否为一有效PE文件呢? 这个问题很难回答,完全取决于想要的精准程度。您可以检验PE文件格式里的各个数据结构,或者仅校验一些关键数据结构。大多数情况下,没有必要校验文件里的每一个数据结构,只要一些关键数据结构有效,我们就认为是有效的PE文件了。正如上述:如果IMAGE_NT_HEADERSsignature域值等于“PE\0\0″,那么就是有效的PE文件。

如何定位 PE header? 答案很简单: DOS MZ header 已经包含了指向 PE header 的文件偏移量。DOS MZ header 又定义成结构 IMAGE_DOS_HEADER 。查询windows.inc,我们知道 IMAGE_DOS_HEADER 结构的e_lfanew成员就是指向 PE header 的文件偏移量。

现将所有的步骤简单列出:

1.首先检验文件头部第一个字的值是否等于 IMAGE_DOS_SIGNATURE是则
DOS
MZ header
有效。

2.一旦证明文件的 DOS header 有效后,就可用e_lfanew来定位
PE
header
了。

3.比较 PE header 的第一个字的值是否等于 IMAGE_NT_HEADER。如果前后两个值都匹配,那我们就认为该文件是一个有效的PE文件。

2004年12月20日

面向对象设计原则
面向对象设计的基石是“开—闭”原则
   “开一闭”原则讲的是:一个软件实体应当对扩展开放,对修改关闭。
    这个规则说的是,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。
    从另外一个角度讲,就是所谓的“对可变性封装原则”。“对可变性封装原则”意味着两点:
    1 .一种可变性不应当散落在代码的很多角落里,而应当被封装到一个对象里面。同一种可变性的不同表象意味着同一个继承等级结构中的具体子类。
    2.一种可变性不应当与另一种可变性混合在一起。即类图的继承结构一般不应超过两层。
    做到“开—闭”原则不是一件容易的事,但是也有很多规律可循,这些规律同样也是设计原则,它们是实现开—闭原则的工具。


里氏代换原则
    里氏代换原则:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都换成o2时,程序P的行为没有变化,那么类型T2是T1的子类型。
    即如果一个软件实体使用的是基类的话那么也一定适用于子类。但反过来的代换不成立。
    如果有两个具体类A和B之间的关系违反了里氏代换原则,可以在以下两种重构方案中选择一种:
    1 .创建一个新的抽象类C,作为两个具体类的超类,将A和B共同的行为移动到C中,从而解决A和B行为不完全一致的问题。
    2 .从B到A的继承关系改写为委派关系。

依赖倒转原则
    依赖倒转原则讲的是:要依赖于抽象,不要依赖于具体。即针对接口编程,不要针对实现编程。针对接口编程的意思是,应当使用接口和抽象类进行变量的类型声明、参量的类型声明,方法的返还类型声明,以及数据类型的转换等。不要针对实现编程的意思就是说,不应当使用具体类进行变量的类型声明、参量的类型声明,方法的返还类型声明,以及数据类型的转换等。
    依赖倒转原则虽然强大,但却不易实现,因为依赖倒转的缘故,对象的创建很可能要使用对象工厂,以避免对具体类的直接引用,此原则的使用还会导致大量的类。维护这样的系统需要较好的面向对象的设计知识。
    此外,依赖倒转原则假定所有的具体类都是变化的,这也不总是正确的。有一些具体类可能是相当稳定、不会发生变化的,消费这个具体类实例的客户端完全可以依赖于这个具体类。

接口隔离原则
    接口隔离原则讲的是:使用多个专门的接口比使用单一的接口要好。从客户的角度来说:一个类对另外一个类的依赖性应当是建立在最小的接口上的。如果客户端只需要某一些方法的话,那么就应当向客户端提供这些需要的方法,而不要提供不需要的方法。提供接口意味着向客户端作出承诺,过多的承诺会给系统的维护造成不必要的负担。

合成、聚合复用原则
    合成、聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部份,新的对象通过向这些对象的委派达到复用已有功能的目的。这个原则有一个简短的描述:要尽量使用合成、聚合,尽量不要使用继承。
合成、聚合有如下好处:
新对象存取成分对象的唯一方法是通过成分对象的接口。

这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不到的。

这种复用可以在运行时间内动态进行,新对象可以动态的引用与成分对象类型相同的对象。
合成、聚合可以应用到任何环境中去,而继承只能应用到一些有限环境中去。
    导致错误的使用合成、聚合与继承的一个常见原因是错误的把“Has-a”关系当作“Is-a”关系。如果两个类是“Has-a”关系那么应使用合成、聚合,如果是“Is-a”关系那么可使用继承。

迪米特法则
    迪米特法则说的是一个对象应该对其它对象有尽可能少的了解。即只与你直接的朋友通信,不要跟陌生人说话。如果需要和陌生人通话,而你的朋友与陌生人是朋友,那么可以将你对陌生人的调用由你的朋友转发,使得某人只知道朋友,不知道陌生人。换言之,某人会认为他所调用的是朋友的方法。
以下条件称为朋友的条件:
     当前对象本身。
     以参量的形式传入到当前对象方法中的对象。
     当前对象的实例变量直接引用的对象。
     当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友。
     当前对象所创建的对象。
     任何一个对象,如果满足上面的条件之一,就是当前对象的朋友,否则就是陌生人。

迪米特法则的主要用意是控制信息的过载,在将其运用到系统设计中应注意以下几点:
    在类的划分上,应当创建有弱耦合的类。类之间的耦合越弱,就越有利于复用。
    在类的结构设计上,每一个类都应当尽量降低成员的访问权限。一个类不应当public自己的属性,而应当提供取值和赋值的方法让外界间接访问自己的属性。
    在类的设计上,只要有可能,一个类应当设计成不变类。
    在对其它对象的引用上,一个类对其它对象的引用应该降到最低。

2004年12月17日

1.把C++当成一门新的语言学习(和C没啥关系!真的。);

2.看《Thinking In C++》,不要看《C++变成死相》;

3.看《The C++ Programming Language》和《Inside The C++ Object Model》,不要因为他们很难而我们自己是初学者所以就不看;

4.不要被VC、BCB、BC、MC、TC等词汇所迷惑——他们都是集成开发环境,而我们要学的是一门语言;

5.不要放过任何一个看上去很简单的小编程问题——他们往往并不那么简单,或者可以引伸出很多知识点;

6.会用Visual C++,并不说明你会C++;

7.学class并不难,template、STL、generic programming也不过如此——难的是长期坚持实践和不遗余力的博览群书;

8.如果不是天才的话,想学编程就不要想玩游戏——你以为你做到了,其实你的C++水平并没有和你通关的能力一起变高——其实可以时刻记住:学C++是为了编游戏的;

9.看Visual C++的书,是学不了C++语言的;

10.浮躁的人容易说:XX语言不行了,应该学YY;——是你自己不行了吧!?

11.浮躁的人容易问:我到底该学什么;——别问,学就对了;

12.浮躁的人容易问:XX有钱途吗;——建议你去抢银行;

13.浮躁的人容易说:我要中文版!我英文不行!——不行?学呀!

14.浮躁的人容易问:XX和YY哪个好;——告诉你吧,都好——只要你学就行;

15.浮躁的人分两种:a)只观望而不学的人;b)只学而不坚持的人;

16.把时髦的技术挂在嘴边,还不如把过时的技术记在心里;

17.C++不仅仅是支持面向对象的程序设计语言;

18.学习编程最好的方法之一就是阅读源代码;

19.在任何时刻都不要认为自己手中的书已经足够了;

20.请阅读《The Standard C++ Bible》(中文版:标准C++宝典),掌握C++标准;

21.看得懂的书,请仔细看;看不懂的书,请硬着头皮看;

22.别指望看第一遍书就能记住和掌握什么——请看第二遍、第三遍;

23.请看《Effective C++》和《More Effective C++》以及《Exceptional C++》;

24.不要停留在集成开发环境的摇篮上,要学会控制集成开发环境,还要学会用命令行方式处理程序;

25.和别人一起讨论有意义的C++知识点,而不是争吵XX行不行或者YY与ZZ哪个好;

26.请看《程序设计实践》,并严格的按照其要求去做;

27.不要因为C和C++中有一些语法和关键字看上去相同,就认为它们的意义和作用完全一样;

28.C++绝不是所谓的C的“扩充”——如果C++一开始就起名叫Z语言,你一定不会把C和Z语言联系得那么紧密;

29.请不要认为学过XX语言再改学C++会有什么问题——你只不过又在学一门全新的语言而已;

30.读完了《Inside The C++ Object Model》以后再来认定自己是不是已经学会了C++;

31.学习编程的秘诀是:编程,编程,再编程;

32. 请留意下列书籍:《C++面向对象高效编程(C++ Effective Object-Oriented Software Construction)》《面向对象软件构造(Object-Oriented Software Construction)》《设计模式(Design Patterns)》《The Art of Computer Programming》;

33.记住:面向对象技术不只是C++专有的;

34.请把书上的程序例子亲手输入到电脑上实践,即使配套光盘中有源代码;

35.把在书中看到的有意义的例子扩充;

36.请重视C++中的异常处理技术,并将其切实的运用到自己的程序中;

37.经常回顾自己以前写过的程序,并尝试重写,把自己学到的新知识运用进去;

38.不要漏掉书中任何一个练习题——请全部做完并记录下解题思路;

39.C++语言和C++的集成开发环境要同时学习和掌握;

40.既然决定了学C++,就请坚持学下去,因为学习程序设计语言的目的是掌握程序设计技术,而程序设计技术是跨语言的;

41.就让C++语言的各种平台和开发环境去激烈的竞争吧,我们要以学习C++语言本身为主;

42.当你写C++程序写到一半却发现自己用的方法很拙劣时,请不要马上停手;请尽快将余下的部分粗略的完成以保证这个设计的完整性,然后分析自己的错误并重新设计和编写(参见43);

43.别心急,设计C++的class确实不容易;自己程序中的class和自己的class设计水平是在不断的编程实践中完善和发展的;

44.决不要因为程序“很小”就不遵循某些你不熟练的规则——好习惯是培养出来的,而不是一次记住的;

45.每学到一个C++难点的时候,尝试着对别人讲解这个知识点并让他理解——你能讲清楚才说明你真的理解了;

46.记录下在和别人交流时发现的自己忽视或不理解的知识点;

47.请不断的对自己写的程序提出更高的要求,哪怕你的程序版本号会变成Version 100.XX;

48.保存好你写过的所有的程序——那是你最好的积累之一;

49.请不要做浮躁的人;

50.请热爱C++!