创建DLL模块步骤
若要创建D L L模块,必须执行下列操作步骤:
- 必须创建一个头文件,它包含你想要从D L L输出的函数原型、结构和符号。当使用DLL时,也是需要头文件的。
- 要创建一个.cpp文件,主要编写函数实现。对外隐藏。
- 编译.cpp为.obj。
- 链接所有.obj,产生一个DLL映象文件。该映像文件(即模块)包含了用于 DLL的所有二进制代码和全局 /静态数据变量。
- 生成一个.lib文件,lib文件包含所有已输出函数和变量的符号名。
创建可执行模块步骤
- 包含DLL模块的头文件。
- 创建Cpp文件,并引用dll模块所包含的函数和变量。
- 编译,生成.obj模块。
- 链接程序便将所有的. o b j模块的内容组合起来,生成一个可执行的映像文件。一旦DLL和可执行模块创建完成,一个进程就可以执行。
运行可执行模块时,操作系统的加载程序执行步骤
- 加载程序为新进程创建一个虚拟地址空间:
- exe被映射到新进程的地址空间。
- 加载程序对可执行模块的输入节进行分析。对于该节中列出的每个DLL名字,加载程序递归找出用户系统上的DLL模块,再将DLL以及DLL所需的映射到进程的地址空间。
注意,由于DLL模块可以从另一个DLL模块输入函数和变量,因此DLL模块可以拥有它自己的输入节(即记录了所需的DLL的函数名和变量符号)。若要对进程进行全面的初始化,加载程序要分析每个模块的输入节,并将所有需要的DLL模块映射到进程的地址空间。
如你所见,对进程进行初始化,DLL越多越费时间。
三、 创建D L L模块
D L L可以输出以下几种类型到其他模块。:
- 变量
- 函数
- C / C + +类
注意:
1.应当避免输出变量。因为这会删除你的代码中的一个抽象层,使它更加难以维护你的 D L L代码。
2.应避免输出C + +类。只有当DLL模块开发人员和EXE开发人员使用的编译工具相同时,才可输出类。
下面的代码说明了应该如何对单个头文件进行编码,以便同时包含可执行文件和 D L L的源代码文件:
/**************************************** Module:MyLib.h ****************************************/ #ifdef MYLIBAPI //MYLIBAPI should be defined in all of the DLL's sourcel / code modules before this header file is included.1l A11 functions/variab1es are being exported. #e1se //This header file is included by an EXE source code module.ll Indicate that a11 functions/variables are being imported. #define MYLIBAPI extern "C" __declspec(d11import) // Define any data structures and symbo1s here. // Define exported variables here. ( NOTE: Avoid exporting variables.) MYLIBAPI int g_nResult; //Define exported function prototypes here. MYLIBAPI int Add( int nLeft. int nRight) ;
/**************************************** Module:MyLib.cpp ****************************************/ //Include the standard windows and C-Runtime header files here. #include <windows.h> // This DLL source code file exports functions and variab1es. #define MYLIBAPI extern "c" __dec1spec(d11export) //Include the exported data structures,symbo1s,functions,and variables. #include "MyLib.h" //Place the code for this DLL source code file here.int g...nResult; int Add( int nLeft, int nRight) { g_nResu1t = nLeft + nRight;return( g nResu1t); }
在MyLib.h头文件的前面使用__declspec(dllexport)作用是:
- 当编译器看到负责修改变量、函数或 C + +类的__declspec(dllexport)时,它就知道该变量、函数或C + +类是从产生的DLL模块输出的。
注意,MYLIBAPI标志要放在头文件中要输出的变量的定义之前和要输出的函数前面。
MYLIBAPI标志包含了extern “C” 修改符作用是:
- 使用C风格导出函数
注意:
只有当你编写C++代码而不是直接编写C代码时,才能使用这个修改符。
C + +编译器可能会改变函数和变量的名字,extern “C”,就可以告诉编译器不要改变变量名或函数名,这样,变量和函数就可以供使用 C、C + +或任何其他编程语言编写的可执行模块来访问。
补充:
c++编译器允许同名函数(重载)
c编译器不允许重载。
例如:void foo(int n,int m);
void foo(int n);
C++导出成_foo_int_int和_foo_int,链接器读入时不会报错。
C都导出成_foo_;导致链接会提示函数重命名;
3.1 输出的真正含义是什么
查看dll的输出节
dumpbin.exe -exports xxx.dll
RVA:这一列下面的数字用于指明在 D L L文件映像中的什么位置能够找到输出符号的位移量。
hint(提示码):列可供系统用来改进代码的运行性能,在此并不重要。
3.2 创建用于非Visual C++工具的D L L
当使用同一个编译器供应商的工具,创建DLL是不需要做额外工作的。
当使用不同供应商的工具时,则需要做一些额外工作。
调用规则 __stdcall(WINAPI)
当使用 __stdcall将C函数输出时,Microsoft的编译器就会改变函数的名字,设置一个前导下划线,再加上一个 @符号的前缀,后随一个数字,表示作为参数传递给函数的字节数。
例如,下面的函数是作为DLL的输出节中的_MyFunc@8输出的:
_declspec(dllexport) LONG __stdcall MyFunc(int a. int b); • 1
其他编译器工具创建exe时 使用Microfost编译器创建DLL需要注意
若要使用与其他编译器供应商的工具链接的 M i c r o s o f t的工具创建一个可执行模块,必须告诉Microsoft的编译器输出没有经过改变的函数名。
有两种方法:
- 第一种方法是为编程项目建立一个.def文件,并在该.def文件中加上类似下面的EXPORTS节:
EXPORTS MyFunc # 输出函数的最终名 • 1 • 2
- 如果想避免使用.def文件,可以使用第二种方法输出未截断的函数版本。在 D L L的源代码模块中,可以添加下面这行代码:
#pragma comment(1inker,"/export:MyFunc=_MyFunc@8") • 1
这行代码使得编译器发出一个链接程序指令,告诉链接程序,一个名叫MyFunc的函数将被输出,其进入点与称为_MyFunc@8的函数的进入点相同。
第二种方法没有第一种方法容易,因为你必须自己截断函数名,以便创建该代码行。
另外,当使用第二种方法时,DLL实际上输出用于标识单个函数的两个符号,即 MyFunc和_MyFunc@8,而第一种方法只输出符MyFunc。第二种方法并没有给你带来更多的好处,它只是使你可以避免使用 .def的文件而已。
四、创建可执行模块
查看输入节
Visual Studio的DumpBin . e x e实用程序(带有- i m p o r t s开关),能够看到模块的输入节的样子。下面是 Calc.exe文件的输入节的一个代码段。
C:\WINNT\SYSTEM32>DUMPBIN -imports Calc.EXE
Dump of file C:\Windows\System32\calc.exe File Type: EXECUTABLE IMAGE Section contains the following imports: SHELL32.dll 1400021B0 Import Address Table 1400028C0 Import Name Table 0 time date stamp 0 Index of first forwarder reference 1AE ShellExecuteW KERNEL32.dll 140002148 Import Address Table 140002858 Import Name Table 0 time date stamp 0 Index of first forwarder reference 224 GetCurrentThreadId 2F2 GetSystemTimeAsFileTime 310 GetTickCount 4D4 RtlCaptureContext 220 GetCurrentProcessId 4E2 RtlVirtualUnwind 5BE UnhandledExceptionFilter 57D SetUnhandledExceptionFilter 21F GetCurrentProcess 59C TerminateProcess 451 QueryPerformanceCounter 4DB RtlLookupFunctionEntry msvcrt.dll 1400021F0 Import Address Table 140002900 Import Name Table 0 time date stamp 0 Index of first forwarder reference 90 __setusermatherr 17D _initterm 57 __C_specific_handler 382 _wcmdln 127 _fmode D2 _commode 2F ?terminate@@YAXXZ C1 _cexit 9D __wgetmainargs AE _amsg_exit 55 _XcptFilter 432 exit 8E __set_app_type 10E _exit ADVAPI32.dll 140002128 Import Address Table 140002838 Import Name Table 0 time date stamp 0 Index of first forwarder reference 122 EventSetInformation 129 EventWriteTransfer 121 EventRegister api-ms-win-core-synch-l1-2-0.dll 1400021E0 Import Address Table 1400028F0 Import Name Table 0 time date stamp 0 Index of first forwarder reference 2D Sleep api-ms-win-core-processthreads-l1-1-0.dll 1400021D0 Import Address Table 1400028E0 Import Name Table 0 time date stamp 0 Index of first forwarder reference 20 GetStartupInfoW api-ms-win-core-libraryloader-l1-2-0.dll 1400021C0 Import Address Table 1400028D0 Import Name Table 0 time date stamp 0 Index of first forwarder reference 14 GetModuleHandleW Summary 1000 .data 1000 .pdata 1000 .rdata 1000 .reloc 5000 .rsrc 1000 .text
,这些 DLL是SHELL32.dll、
msvcrt.dll、ADVAPI32.dll、KERNEL32.dll、GDI32 . dll和User32.dll。
(我的运行的是win10 会有些dll依赖的区别)
五、运行可执行模块
当一个可执行文件被启动时,操作系统加载程序将为该进程创建虚拟地址空间。然后,加载程序将可执行模块映射到进程的地址空间中。加载程序查看可执行模块的输入节,并设法找出任何需要的DLL,并将它们映射到进程的地址空间中。
搜索DLL顺序是:
- 包含可执行映像文件exe的目录。
- 进程的当前目录。
- Windows系统目录。
- Windows目录。
- PATH环境变量中列出的各个目录。
应该知道其他的东西也会影响加载程序对一个 DLL的搜索:
- 当D L L模块映射到进程的地址空间中时,加载程序要检查每个 D L L的输入节。如果存在输入节(通常它确实是存在的),那么加载程序便继续将其他必要的 DLL模块映射到进程的地址空间中。加载程序将保持对 DLL模块的跟踪,使模块的加载和映射只进行一次(尽管多个模块需要该模块)。
总结
1.为什么静态C/C++ 运行时库(MT)被其他模块释放内存 会报错?
简单讲,MT库编译时各会包含libcmt.lib,然后每个MT库都有一个独立的堆。一个堆的地址到另一个堆的同名地址去释放,自然会有非法访问问题。
2.__stdcall、__cdecl、extern "C"区别 和使用场景。
1. __stdcall: __stdcall是一种函数调用约定(calling convention),用于指定函数的参数传递和堆栈清除方式。 __stdcall约定要求被调用函数从堆栈中清除其参数,并且函数的参数是从右往左依次入栈。 __stdcall常用于Windows API函数,如LoadLibrary和MessageBox。 在使用__stdcall约定声明函数时,可以使用__declspec(dllexport)或__declspec(dllimport)来指定函数的导出或导入。 2. __cdecl: __cdecl也是一种函数调用约定,但与__stdcall不同,它要求调用者清除堆栈,并且函数的参数是从左往右依次入栈。 默认情况下,C++函数都是按照__cdecl约定进行调用的,可以省略__cdecl宏定义。 __cdecl约定在函数的定义和声明中都可以使用。 extern "C": 3. extern "C"是用于指定C语言风格的函数名和参数名的链接约定。 C++编译器对函数和变量进行了名称修饰(name mangling),以支持函数重载和命名空间等特性,而C语言没有支持这些特性,所以需要使用extern "C"来取消C++的名称修饰。 C语言和C++语言的函数之间可以通过extern "C"进行链接,以实现互相调用。 使用场景: - 如果你在使用Windows API函数,应该注意使用__stdcall约定声明和调用这些函数。 - 如果你定义的函数需要与其他C语言或第三方库进行交互,可以使用extern "C"来取消名称修饰,确保链接成功。 - 对于一般情况下的函数声明和定义,默认使用__cdecl约定即可,不需要额外指定。