CODE
BOOL APIENTRY DllMain( HMODULE 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:
break;
}
return TRUE;
}
1.DLLMain函数
?1.1DLL函数简介
??(1). .dll,动态链接库英文为DLL,是Dynamic Link Library的缩写。DLL是一个包含可由多个程序,同时使用的代码和数据的库,可以更为容易地将更新应用于各个模块,而不会影响该程序的其他部分;其次,动态链接库不一定要有DLLMain函数,很多仅仅包含资源信息的DLL是没有DLLMain函数的。 ??(2). 当程序使用 DLL 时,具有以下的优点: 使用较少的资源,当多个程序使用同一个函数库时,DLL 可以减少在磁盘和物理内存中加载的代码的重复量。这不仅可以大大影响在前台运行的程序,而且可以大大影响其他在 Windows 操作系统上运行的程序。 推广模块式体系结构DLL 有助于促进模块式程序的开发。这可以帮助您开发要求提供多个语言版本的大型程序或要求具有模块式体系结构的程序。模块式程序的一个示例是具有多个可以在运行时动态加载的模块的计帐程序。 简化部署和安装当 DLL 中的函数需要更新或修复时,部署和安装 DLL 不要求重新建立程序与该 DLL 的链接。此外,如果多个程序使用同一个 DLL,那么多个程序都将从该更新或修复中获益。当您使用定期更新或修复的第三方 DLL 时,此问题可能会更频繁地出现。 ??(3). 当某个程序或 DLL 使用其他 DLL 中的 DLL 函数时,就会创建依赖项。因此,该程序就不再是独立的,并且如果该依赖项被损坏,该程序就可能遇到问题。例如,如果发生下列操作之一,则该程序可能无法运行: 依赖 DLL 升级到新版本。 修复了依赖 DLL。 依赖 DLL 被其早期版本覆盖。 从计算机中删除了依赖 DLL。这些操作通常称为 DLL 冲突。如果没有强制实现向后兼容性,则该程序可能无法成功运行。 ??(4). Dependency WalkerDependency Walker 工具可以递归扫描以寻找程序所使用的所有依赖 DLL。当在 Dependency Walker 中打开程序时,Dependency Walker 会执行下列检查: Dependency Walker 检查是否丢失 DLL。 Dependency Walker 检查是否存在无效的程序文件或 DLL。 Dependency Walker 检查导入函数和导出函数是否匹配。 Dependency Walker 检查是否存在循环依赖性错误。 Dependency Walker 检查是否存在由于针对另一不同操作系统而无效的模块。通过使用 Dependency Walker,可以记录程序使用的所有 DLL。这可能有助于避免和更正将来可能发生的 DLL 问题。当安装 Microsoft Visual Studio 6.0 时,Dependency Walker 将位于以下目录中: drive\Program Files\Microsoft Visual Studio\Common\Tools。 ??(5). DLL Universal Problem Solver (DUPS) 工具用于审核、比较、记录和显示 DLL 信息。下表说明了组成 DUPS 工具的实用工具: Dlister.exe该实用工具枚举计算机中的所有 DLL,并且将此信息记录到一个文本文件或数据库文件中。 Dcomp.exe该实用工具比较在两个文本文件中列出的 DLL,并产生包含差异的第三个文本文件。 Dtxt2DB.exe该实用工具将通过使用 Dlister.exe 实用工具和 Dcomp.exe 实用工具创建的文本文件加载到 dllHell 数据库中。 DlgDtxt2DB.exe该实用工具提供 Dtxt2DB.exe 实用工具的图形用户界面 (GUI) 版本。
?1.2何时调用DLLMain函数
??系统在静态链接(加载时链接)或动态链接(运行时链接)时,调用LoadLibrary和FreeLibrary都会调用DllMain函数。DllMain的第二个参数ul_reason_for_call指明了系统调用Dll的原因,它可能是以下四种状态:DLL_PROCESS_ATTACH、DLL_PROCESS_DETACH、DLL_THREAD_ATTACH和DLL_THREAD_DETACH。
??1.2.1DLL_PROCESS_ATTACH
???(1). 一个程序要调用DLL里的函数,首先要先把DLL文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法:静态链接和动态链接的LoadLibrary或者LoadLibraryEx。 ???(2). 当一个DLL文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数,传递的fdwReason参数为DLL_PROCESS_ATTACH。这种调用只会发生在第一次映射时。如果同一个 进程后来为已经映射进来的DLL再次调用LoadLibrary或者LoadLibraryEx,操作系统只会增加DLL的使用次数,它不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。不同进程用LoadLibrary加载同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DLL的DllMain函数。
??1.2.2DLL_PROCESS_DETACH
???(1). 当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递给ul_reason_for_call的值是DLL_PROCESS_DETACH。当DLL处理该值时,它应该执行进程相关的清理工作。 ???(2). 以下两种情况下DLL会被从进程的地址空间中解除映射: ????a.FreeLibrary解除DLL映射(有几个LoadLibrary,就要有几个FreeLibrary) ????b.进程结束而解除DLL映射,在进程结束前还没有解除DLL的映射,进程结束后会解除DLL映射。(如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。) ???(3). 当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain函数。因此,必须确保没有清理那些没有成功初始化的东西。 ???(4). 如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。
??1.2.3DLL_THREAD_ATTACH
???(1). 当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许进程开始执行它的线程函数。 ???(2). 注意跟DLL_PROCESS_ATTACH的区别,在前面说过,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,是不再用DLL_PROCESS_ATTACH调用DllMain的。而DLL_THREAD_ATTACH不同,进程中的每次建立线程,都会用值 DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样。
??1.2.3DLL_THREAD_DETACH
???(1). 如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
2.DLLMain函数名修饰-APIENTRY
??宏定义: ??#define CALLBACK __stdcall ??//WIN32编程中的回调函数类型 ??#define WINAPI __stdcal ??#define WINAPIV __cdecl ??#define APIENTRY WINAPI ??//DllMain的入口就在这里 ??#define APIPRIVATE __stdcall ??#define PASCAL __stdcall
??APIENTRY根据宏定义#define APIENTRY WINAPI 以及#define WINAPI __stdcall 可知,APIENTRY可以用WINAPI、__stdcall以及CALLBACK替代,其中__stdcall是一种函数调用约定,函数调用约定主要约束了两件事:a.参数传递顺序;b.调用堆栈由谁 (调用函数或被调用函数)清理,常见的函数调用约定:__cdecl 、__stdcall 、__fastcall 、__thiscall。
?2.1__cdecl
??(1). 是C Declaration的缩写,表示C语言默认的函数调用方法,实际上也是C++的默认的函数调用方法。 ??(2). 所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。具体所示:调用方的函数调用->被调用函数的执行->被调用函数的结果返回->调用方清除调整堆栈。 ??(3). 被调用函数无需要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。总的来说函数的参数个数可变的(就像printf函数一样),因为只有调用者才知道它传给被调用函数几个参数,才能在调用结束时适当地调整堆栈。 ??(4). 因为每个调用的地方都需要生成一段调整堆栈的代码,所以最后生成的文件较大。 ??(5). __beginthread需要__cdecl的线程函数地址。
?2.2__stdcall
??(1). 是Standard Call的缩写,要想函数按照此调用方式必须在函数名加入_stdcall,通常_ win32 api 应该是_stdcall调用规则。通过VC++编写的DLL欲被其他语言编写的程序调用,应将函数的调用方式声明为_stdcall 方式,WINAPI都采用这种方式。 ??(2). 所有参数从右到左依次入栈,如果是调用类成员的话,最后一个入栈的是this指针。具体所示:调用方的函数调用->被调用函数的执行-> 被调用方清除调整堆栈->被调用函数的结果返回。 ??(3). 这些堆栈中的参数由被调用的函数在返回后清除,使用的指令是 retn X,X表示参数占用的字节数,CPU在ret之后自动弹出X个字节的堆栈空间。称为自动清栈。 ??(4). 函数在编译的时候就必须确定参数个数,并且调用者必须严格的控制参数的生成,不能多,不能少,否则返回后会出错。总的来说,就是函数的参数个数不能是可变的。是从 _cdecl 修改而来, _stdcall 不支持可变参数,并且清栈由被调用者负责,其他的都一样 ??(5). 因为只需在被调用函数的地方生成一段调整堆栈的代码,所以最后生成的文件较小。 ??(6). C中不加说明默认函数为_cdecl方式(C中也只能用这种方式),C++也一样,但是默认的调用方式可以在IDE环境中设置。带有可变参数的函数必须且只能使用_cdecl方式,例如下面的函数:a. int printf(char * fmtStr, …); b. int printf(char * fmtStr, …); ??(7). __beginthreadex和__CreateThread需要__stdcall的线程函数地址。
?2.3__fastcall
??__fastcall 是编译器指定的快速调用方式。由于大多数的函数参数个数很少,使用堆栈传递比较费时。因此_fastcall通常规定将前两个(或若干个)参数由寄存器传递,实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈。不同编译器编译的程序规定的寄存器不同。返回方式和_stdcall相当。
?2.4__thiscall
??(1). __thiscall 是为了解决类成员调用中this指针传递而规定的。_thiscall要求把this指针放在特定寄存器中,该寄存器由编译器决定。VC使用ecx,Borland的C++编译器使用eax。返回方式和_stdcall相当。 ??(2). thiscall仅仅应用于“C++”成员函数。this指针存放于CX/ECX寄存器中,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。 ??(3). __fastcall 和 __thiscall涉及的寄存器由编译器决定,因此不能用作跨编译器的接口。所以Windows上的COM对象接口都定义为__stdcall调用方式。
?2.5函数名修饰
??2.5.1去除函数名修饰
???(1). __cdecl :对于__cdecl而言,如果对于定义在C程序文件(编译器会通过后缀名为.C判断)的输出函数,函数名会保持原样;对于定义在C++程序文件中的输出函数,函数名会被修饰(见2.5.3)。为使函数名不被修饰,有两种方法:A.可通过在前面加上extern “C”以去除函数名修饰;B. 可通过.def文件去除函数名修饰。 ???(2). __stdcall:无论是C程序文件中的输出函数还是C++程序文件中的输出函数,函数名都会被修饰。对于定义在C++程序文件中的输出函数,好像更复杂,和__cdecl的情况类似。去除函数名修饰方法:只能通过.def文件去除函数名修饰。
??2.5.2进行函数名修饰的原因
???函数名修饰就是编译器在编译期间创建的一个字符串,用来指明函数的定义和原型。LINK程序或其他工具有时需要指定函数的名字修饰来定位函数的正确位置。多少情况下程序员并不需要知道函数的名字修饰,LINK程序或其他工具会自动区分他们。当然,在某些情况下需要指定函数名修饰,例如在c++程序中,为了让LINK程序或其他工具能够匹配到正确的函数名字,就必须为重载函数后一些特殊函数(如构造函数和析构函数)指定名字修饰。另一种需要指定函数名修饰的情况是在汇编程序中调用C或C++函数。
??2.5.3函数名修饰规则
???(1). C语言:对于__stdcall调用约定,编译器和链接器会在输出函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其参数的字节数,例如_functionname@number。_cdecl调用约定仅在输出函数名前加上一个下划线前缀,如_functionname。__fastcall调用约定在输出函数名前加上一个 “@“符号,后面也是一个”@“符号和其参数的字节数,如@functionname@number。 ???(2). C++语言:C++的函数名修饰规则有些复杂,但是信息更充分,通过分析修饰名不仅能够知道函数的调用方式,返回值类型,参数个数甚至参数类型。不管__cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”,对于__fastcall方式则是“@@YI”。参数表的拼写代号如下所示: X–void D–char E–unsigned char F–short H–int I–unsigned int J–long K–unsigned long(DWORD) M–float N–double _N—bool U—struct … 指针的方式有些特别,用PA表示指针,用PB表示const类型的指针。后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复。U表示结构类型,通常后跟结构体的类型名,用“@@”表示结构类型名的结束。函数的返回值不作特殊处理,它的描述方式和函数参数一样,紧跟着参数表的开始标志,也就是说,函数参数表的第一项实际上是表示函数的返回值类型。参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。下面举两个例子,假如有以下函数声明: int Function1(char *var1,unsigned long); 其函数修饰名为“?Function1@@YGHPADK@Z”,而对于函数声明: void Function2(); 其函数修饰名则为“?Function2@@YGXXZ” 。 ??对于C++的类成员函数(其调用方式是thiscall),函数的名字修饰与非成员的C++函数稍有不同,首先就是在函数名字和参数表之间插入以“@”字符引导的类名;其次是参数表的开始标识不同,公有(public)成员函数的标识是“@@QAE”,保护(protected)成员函数的标识是“@@IAE”,私有(private)成员函数的标识是“@@AAE”,如果函数声明使用了const关键字,则相应的标识应分别为“@@QBE”,“@@IBE”和“@@ABE”。如果参数类型是类实例的引用,则使用“AAV1”,对于const类型的引用,则使用“ABV1”。
??2.5.4如何查看函数被编译器修改后的函数名
???(1). 使用EXEInfoPE、StudyPE+ x64、Dependency Walker(查dll文件依赖项)等工具进行查看。 ???(2). 使用dumpbin.exe /SYMBOLS命令也可以获得obj文件或lib文件中的函数或变量名字列表。该工具直接去Windows左下角菜单栏找到文件“VS 2017的开发人员命令提示符 ”打开即可,可以查看.obj文件、.lib库、.dll库、.exe执行文件。而Dependcy Walker只能查看dll和exe文件。用法如下: 查看a.dll库中包含哪些函数:dumpbin /exports a.dll >1.txt 查看b.exe中加载了哪些动态库:dumpbin /imports b.exe >2.txt 查看c.lib中包含哪些函数:dumpbin /all /rawdata:none c.lib >3.txt 查看d.obj中包含哪些函数:dumpbin /all /rawdata:none d.obj >4.txt 此外,还可以使用 undname.exe 将修饰名转换为未修饰形式。 ???(3). 使用编译输出列表。使用/FAc,/FAs或/FAcs命令行参数可以让编译器输出函数或变量名字列表。
SOURCE
|