系列文章目录
[笔记]Windows核心编程《一》错误处理、字符编码
[笔记]Windows核心编程《二》内核对象
[笔记]Windows核心编程《三》进程
[笔记]Windows核心编程《四》作业
[笔记]快乐的LInux命令行《五》什么是shell
[笔记]Windows核心编程《五》线程基础
[笔记]Windows核心编程《六》线程调度、优先级和关联性
[笔记]Windows核心编程《七》用户模式下的线程同步
[笔记]Windows核心编程《八》用内核对象进行线程同步
[笔记]Windows核心编程《九》同步设备I/O和异步设备I/O
[笔记]Windows核心编程《十一》Windows线程池
[笔记]Windows核心编程《十二》纤程
[笔记]Windows核心编程《十三》windows内存体系结构
[笔记]Windows核心编程《十四》探索虚拟内存
[笔记]Windows核心编程《十五》在应用程序中使用虚拟内存
[笔记]Windows核心编程《十六》线程栈
[笔记]Windows核心编程《十七》内存映射文件
[笔记]Windows核心编程《十八》堆栈
[笔记]Windows核心编程《十九》DLL基础
[笔记]Windows核心编程《二十》DLL的高级操作技术
[笔记]Windows核心编程《二十一》线程本地存储器TLS
[笔记]Windows核心编程《二十二》注入DLL和拦截API
[笔记]Windows核心编程《二十三》结构化异常处理
文章目录
系列文章目录
前言
常用DLL
使用DLL的一些原因
一、DLL与进程的地址空间
关于跨DLL释放 解释
二、 DLL的总体运行情况
创建DLL模块步骤
创建可执行模块步骤
运行可执行模块时,操作系统的加载程序执行步骤
三、 创建D L L模块
3.1 输出的真正含义是什么
3.2 创建用于非Visual C++工具的D L L
调用规则 __stdcall(WINAPI)
其他编译器工具创建exe时 使用Microfost编译器创建DLL需要注意
四、创建可执行模块
查看输入节
五、运行可执行模块
总结
1.为什么静态C/C++ 运行时库(MT)被其他模块释放内存 会报错?
2.__stdcall、__cdecl、extern "C"区别 和使用场景。
前言
常用DLL
Windows API中的所有函数都包含在 D L L中。3个最重要的 D L L是:
Kernel32.dll:它包含用于管理内存、进程和线程的各个函数;
User32.dll:它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数;
GDI32.dll,它包含用于画图和显示文本的各个函数。
其他:
AdvAPI32. dll 包含用于实现对象安全性、注册表操作和事件记录的函数;
ComDlg32.dll 包含常用对话框(如File Open和File Save);
ComCtl32.dll 则支持所有的常用窗口控件。
使用DLL的一些原因
优点:
它们扩展了应用程序的特性。 由于 DLL能够动态地装入进程的地址空间,因此应用程序能够在运行时确定需要执行什么操作,然后装入相应的代码,以便根据需要执行这些操作。
它们可以用许多种编程语言来编写。 可以选择手头拥有的最好的语言来编写 D L L。系统允许Visual Basic程序加载C++ DLL、Cobol DLL和Fortran DLL等。
它们简化了软件项目的管理。 项目小组分工可以使用DLL,项目管理会容易一些。但是过多的DLL会使程序加载时间过长。
它们有助于节省内存。 如果两个或多个应用程序使用同一个 D L L,那么该D L L的页面只要放入R A M一次,所有的应用程序都可以共享它的各个页面。 C/C++运行期库就是个极好的例子。许多应用程序都使用这个库。如果所有的应用程序都链接到这个静态库,那么sprintf、strcpy和malloc等函数的代码就要多次存在于内存中。但是,如果所有这些应用程序链接到DLL C/C++运行期库,那么这些函数的代码就只需要放入内存一次,这意味着内存的使用将更加有效。
它们有助于资源的共享。 DLL可以包含对话框模板、字符串、图标和位图等资源。多个应用程序能够使用DLL来共享这些资源。
它们有助于应用程序的本地化。 应用程序常常使用 D L L对自己进行本地化。例如,只包含代码而不包含用户界面组件的应用程序可以加载包含本地化用户界面组件的 D L L。
它们有助于解决平台差异。 它们有助于解决平台差异。不同版本的 Wi d n o w s配有不同的函数。开发人员常常想要调用新的函数(如果它们存在于主机的 Wi n d o w s版本上的话)。但是,如果你的源代码包含了对一个新函数的调用,而你的应用程序将要在不能提供该函数的 Wi n d o w s版本上运行,那么操作系统的加载程序将拒绝运行你的进程。即使你实际上从不调用该函数,情况也是这样。如果将这些新函数保存在 D L L中,那么应用程序就能够将它们加载到 Windows的老版本上。当然,你仍然可以成功地调用该函数。
它们可以用于一些特殊的目的。 Wi n d o w s使得某些特性只能为 D L L所用。例如,只有当D L L中包含某个挂钩通知函数的时候,才能安装某些挂钩(使用 SetWindowsHookEx和SetWinEventHook来进行安装)。可以通过创建必须在DLL中生存的COM对象来扩展Windows Explorer的外壳程序。对于可以由We b浏览器加载的、用于创建内容丰富的 Web页的ActiveX控件来说,情况也是一样.
缺点:
过多的DLL会使程序加载时间过长。
一、DLL与进程的地址空间
DLL特点:
DLL仅包含一组应用程序可以使用的自主函数。在DLL中通常没有用来处理消息循环或创建窗口的支持代码。
应用程序(或另一个D L L)能够调用D L L中的函数条件:
DLL文件 映像必须被映射到调用进程的地址空间中。
D L L映射到调用进程的地址空间的方式:
加载时的隐含链接
运行期的显式链接
DLL文件映射完后,就会像在进程的地址空间中的额外代码和数据一样:
DLL函数查看线程堆栈,检索所传递的参数。
使用线程的堆栈初始化DLL函数需要的局部变量
注意:
exe加载dll时也会加载dll的全局变量和静态变量的实例 到静态区
关于跨DLL释放 解释
单个地址空间是由一个可执行模块和若干个 D L L模块组成的。
一般三种情况的模块:
链接到静态版本的 C/C++运行期库的模块
链接到一个DLL版本的 C/C++ 运行期库的模块
不需要C/C++运行期库的模块
静态运行时库(MT): 编译时会包含一些C/C++运行时库,但是使用多个模块的大型软件来说,如果每个模块均选择静态链接C或C++运行库,程序运行时就会存在多个运行库。在链接时也会出现重复定义的问题。
在这里插入图片描述
动态运行时库 (MD): 程序在运行时动态的加载对应的DLL。程序体积变小,但一个很大的问题就是一旦找不到对应DLL,程序将无法运行(比如所要移植的电脑没有安装VC++)。
假设使用VC6.0并选择使用MD选项构建,那么当用户使用VC2005来使用这个DLL时很可能出现找不到MSVCRT.DLL或MSVCP60.DLL的情况。
终于理解了什么是c/c++运行时库,以及libcmt msvcrt等内容
请看下面的代码:
vOID EXEFunc( ) { PvOID pv = DLLFunc( ); //Access the storage pointed to by pv. .. //Assumes that pv is in EXE's C/C++ run-time heap free(pv) ; } PVOID DLLFunc( ) { //A11ocate block from DLL's C/C++ run-time heap return(ma11oc(100 )); }
上面这个代码能够正确运行吗?
D L L函数分配的内存块是由EXE的函数释放的吗?
两种可能:
如果EXE和DLL都链接到DLL的C/C++运行期库(MD),那么上面的代码将能够很好地运行。
但是,如果两个模块中的一个或者两个都链接到静态C/C + +运行期库(MT),那么对free函数的调用就会失败。
解决方案就是:
当一个模块提供一个用于分配内存块的函数时,该模块也必须提供释放内存的函数。(谁申请内存 谁就去释放内存)、
VOID EXEFunc( ) { PVOID pv = DLLFunc( ) ; //Access the storage pointed to by pv.. . //Makes no assumptions about C/C++ run-time heap DLLFreeFunc(pv); } PVOID DLLFunc( ) { //A11ocate b1ock from DLL's C/C++ run-time heap PVOID pv = ma11oc(100) ; return(pv); } B00L DLLFreeFunc( PVOID pv) { //Free block from DLL's C/C++ run-time heap return( free(pv ) ); )
这个代码是正确的,它始终都能正确地运行。
当你编写一个模块时,不要忘记其他模块中的函数也许没有使用C/C + +来编写,因此可能无法使用malloc和free函数进行内存的分配。
二、 DLL的总体运行情况
本节重点介绍可执行模块和 DLL模块之间是如何隐含地互相链接。
当一个模块(比如一个可执行文件)使用DLL中的函数或变量时,将有若干个文件和组件参与发挥作用。如下图:
创造DLL:
1)建立带有输出原型/结构/符号的头文件。
2)建立实现输出函数/变量的C/C++源文件。
3)编译器为每个C/C++源文件生成 .obj模块 。
4)链接程序将生成DLL的 .obj模块链接起来。
5)如果至少输出一个函数/变量,那么链接程序也生成lib 文件。
创造EXE:
6) 建立带有输入原型/结构/符号的头文件。
7) 建立引用输入函数/变量的C/C++源文件。
8) 编译器为每个C/C++源文件生成 .obj源文件。
9) 链接程序将各个 .obj模块链接起来,产生一个 .exe文件(它包含了所需要DLL模块的名字和输入符号的列表)。
运行应用程序:
10) 加载程序为 .exe 创建地址空间。
11) 加载程序将需要的DLL加载到地址空间中进程的主线程开始执行;应用程序启动运行。
几个文件解释
.h文件:一般是函数的声明,或者记录要导出函数的文件。
.cpp文件:一般就是函数的实现,一般不导出,只用于生成.obj文件。
.obj文件:cpp文件的编译后的产物。
.lib:列出所有已输出函数和变量的符号名。
.dll:记录函数的实现。
.exe :
执行文件的所有二进制代码和全局/静态变量。
输入节,列出可执行文件需要的所有DLL模块名以及所引用的函数和变量符号。
简单的来说就是编译链接运行的过程:
编译:把cpp编译成.obj。
链接:就是链接所有.obj到一个文件(dll/exe)中。
运行:为exe创建地址空间,加载dll到地址空间执行。
创建DLL模块步骤
若要创建D L L模块,必须执行下列操作步骤:
- 必须创建一个头文件,它包含你想要从D L L输出的函数原型、结构和符号。当使用DLL时,也是需要头文件的。
- 要创建一个.cpp文件,主要编写函数实现。对外隐藏。
- 编译.cpp为.obj。
- 链接所有.obj,产生一个DLL映象文件。该映像文件(即模块)包含了用于 DLL的所有二进制代码和全局 /静态数据变量。
- 生成一个.lib文件,lib文件包含所有已输出函数和变量的符号名。
创建可执行模块步骤
- 包含DLL模块的头文件。
- 创建Cpp文件,并引用dll模块所包含的函数和变量。
- 编译,生成.obj模块。
- 链接程序便将所有的. o b j模块的内容组合起来,生成一个可执行的映像文件。一旦DLL和可执行模块创建完成,一个进程就可以执行。
运行可执行模块时,操作系统的加载程序执行步骤
10. 加载程序为新进程创建一个虚拟地址空间:
1.exe被映射到新进程的地址空间。
2.加载程序对可执行模块的输入节进行分析。对于该节中列出的每个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);
其他编译器工具创建exe时 使用Microfost编译器创建DLL需要注意
若要使用与其他编译器供应商的工具链接的 M i c r o s o f t的工具创建一个可执行模块,必须告诉Microsoft的编译器输出没有经过改变的函数名。
有两种方法:
- 第一种方法是为编程项目建立一个.def文件,并在该.def文件中加上类似下面的EXPORTS节:
EXPORTS MyFunc # 输出函数的最终名
- 如果想避免使用.def文件,可以使用第二种方法输出未截断的函数版本。在 D L L的源代码模块中,可以添加下面这行代码:
#pragma comment(1inker,"/export:MyFunc=_MyFunc@8") •
这行代码使得编译器发出一个链接程序指令,告诉链接程序,一个名叫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库都有一个独立的堆。一个堆的地址到另一个堆的同名地址去释放,自然会有非法访问问题。