Windows 内存之DLL 共享机制

1,166 阅读9分钟

在(Windows 应用内存的那些词儿 - 掘金 (juejin.cn)) 中我们说了 DLL 是占用的可共享内存,那么 DLL 的共享机制是什么呢?

DLL 简介

动态链接库(DLL,即“Dynamic Link Library”)是Microsoft Windows最重要的组成元素之一,打开windows系统文件夹,会发现很多DLL文件,windows就是将一些主要的系统功能以DLL模块的形式实现。动态链接库是不能直接执行的,也不能接收消息,它是一个独立的文件,其中包含被程序或其他DLL调用来完成一定操作的函数(方法)。但这些函数不是执行程序本身的一部分,而是根据进程的需要按需载入,此时才能发挥作用。

常见的 EXE、DLL、OCX、SYS、COM 都是PE文件( portable File Format - 可移植文件的简写),所以,我们先来看一下 PE 文件的结构。 PE 文件是指 32 位可执行文件,也称为 PE32。64 位的可执行文件称为 PE+ 或 PE32+ ,是 PE(PE32) 的一种扩展形式

PE 文件基本概念

PE文件使用的是一个平面地址空间,所有代码和数据都被合并在一起,组成一个很大的组织结构。文件的内容分割为不同的区块(Setion,又称区段,节等),区段中包含代码数据,各个区块按照页边界来对齐,区块没有限制大小,是一个连续的结构。每块都有他自己在内存中的属性,比如:这个块是否可读可写,或者只读等等。

PE文件不是作为单一内存映射文件被装入内存,windows加载器(PE加载器)遍历 PE 文件并决定文件的哪个部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。在磁盘的数据结构中寻找一些内容,那么几乎能在被装入到内存映射文件中找到相同的信息。但是数据之间的位置可能改变,其某项的偏移地址可能区别于原始的偏移位置,不管怎么样,所表现出来的信息都允许从磁盘文件到内存偏移的转换,如下图:

image.png

  • DOS头是用来兼容MS-DOS操作系统的,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode. 还有一个目的,就是指明NT头在文件中的位置。
  • NT头包含windows PE文件的主要信息,其中包括一个‘PE’字样的签名,PE文件头(IMAGE_FILE_HEADER)和PE可选头(IMAGE_OPTIONAL_HEADER32)。PE文件头以下的地址无论在内存映射中还是在磁盘映射中都是一样的,当内存分页和磁盘分页一致时无需进行地址转换,只有当磁盘分页和内存分页不一样时才要进行地址转化。
  • 节区头:是PE文件后续节的描述,windows根据节表的描述加载每个节。
  • 节区:每个节实际上是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义,未必是上图中的三个。

基地址(ImageBase)

定义:当PE文件通过 Windows 加载器被装入内存后,内存中的版本被称作模块(Module)。映射文件的起始地址被称作模块句柄(hMoudule),可以通过模块句柄访问其他的数据结构。这个初始内存地址就是基地址

内存中的模块代表着进程从这个可执行文件中所需要的代码数据资源输入表输出表以及其他有用的数据结构所使用的内存都放在一个连续的内存块中,编程人员只要知道装载程序文件映像到内存的基地址即可。在32位系统中可以直接调用GetModuleHandle以取得指向DLL的指针,通过指针访问DLL module的内容,例如:

HMODULE GetmoduleHandle(LPCTSRT lpModuleName);

当调用该函数时,传递一个可执行文件或者DLL文件名字字符串。如果系统找到该文件,则返回该可执行文件的或者 DLL 文件映像加载到的基地址。也可以调用 GetModuleHandle,传递 NULL 参数,则返回调用的可执行文件的基地址。

相对虚拟地址(Relative Virtual Address)

在可执行文件中,有相当多的地方需要指定内存的地址。

引用全局变量时,需要指定它的地址。PE文件尽管有一个首选的载入地址(基地址),但是他们可以载入到进程空间的任意地方,所以不能依赖与PE的载入点。由于这个原因,必须有一个方法来指定一个地址而不是依赖于PE载入点,编译器便根据这个地址求出代码中一些全局变量和函数的地址,并将这些地址用到对应的指令中。

为了在PE文件中避免有确定的内存地址,出现了相对虚拟地址(Relative Virtual Addres,简称RVA)的概念。RVA 只是内存中的一个简单的相对于 PE 文件装入地址的偏移地址,它是一个“相对”地址,或者称位“偏移量”地址。

假设一个EXE文件从地址 40000h 处载入,并且它的代码区块开始于4010000h,代码区的RVA将是:目标地址401000h ;转入地址 400000h 则 RVA=1000h。

将RVA地址转换成真实地址,只需简单的翻转这个过程:将实际装入地址加上RVA即可得到虚拟地址(Vritual Address,简称VA)。既:

虚拟地址(VA)=基地址(ImageBase)+相对虚拟地址(RVA)

PE头内部信息大多是RVA形式存在。原因在于(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他的PE文件(DLL)。此时必须通过重定向(Relocation)将其加载到其他空白的位置,若PE头信息使用的是VA,则无法正常访问。因此使用RVA来重定向信息,即使发生了重定向,只要相对于基准位置的相对位置没有变化,就能正常访问到指定信息,不会出现任何问题。

文件偏移地址(File Offset)

PE文件在磁盘上和在内存里是不完全一样的,被加载到内存以后其占用的虚拟地址空间要比在磁盘上占用的空间大一些,这是因为各个节在硬盘上是连续的,而在内存中是按页对齐的,所以加载到内存以后节之间会出现一些“空洞”

当PE文件存储在磁盘上时,某个数据的位置相对于文件头的偏移量也称文件偏移地址(FileOffset)或者物理地址(RAW Offset)。文件偏移地址从PE文件的第一个字节开始计数,起始为零。

注意这个物理地址和虚拟地址的区别:物理地址是文件在磁盘上相对于文件头的地址,而虚拟地址是 PE 可执行程序加载在内存中的地址。

PE文件执行流程如下图:

Untitled Diagram (1).jpg

DLL 共享机制

DLL动态链接库是WIN32环境下代码共享的一种机制,只在磁盘上保存一份二进制代码,可以被多个程序加载、调用。 由上面的PE文件重定向可知。.text段若DLL映射到各自进程中的虚拟内存地址相同则:内存中只保存一份,共享;若DLL映射到各自进程中的虚拟内存地址不同,既DLL发生了重定向:不同进程拥有各自不同的拷贝。但WIN32系统的DLL被广泛使用,如果每个进程都使用独立的拷贝,会极大的占用物理内存空间,微软使用了一种特殊的处理方式,让WIN32系统的DLL不再进行重定位,总是映射到虚拟内存的高地址空间中,这样,在不同的进程中,WIN32系统DLL代码段都使用物理地址的同一份拷贝,以达到代码共享和节省物理内存空间的效果。

image.png

C#调用

基本格式:

[DLLImport(“DLL文件路径”)]

修饰符 extern 返回值类型 方法名称(参数列表)


例如:

[DllImport("kernel32.dll", SetLastError = true, EntryPoint = "SetLocalTime")]

      public static extern int SetSystemTime(ref SystemTime lpSystemTime);

注意事项:

1、DLL文件必须位于程序当前目录或系统定义的查询路径中(即:系统环境变量中Path所设置的路径)。

2、DLLImport会按照顺序去查找DLL文件(程序当前目录>System32目录>环境变量Path所设置路径)。

3、返回类型变量、方法名称、参数列表一定要与DLL文件中的定义相一致。

如果启用了SafeDllSearchMode,则搜索顺序如下:

  1. 从中加载应用程序的目录。
  2. 系统目录。使用GetSystemDirectory函数获取此目录的路径。
  3. 16位系统目录。没有获取该目录路径的函数,但会对其进行搜索。
  4. Windows目录。使用GetWindowsDirectory函数获取此目录的路径。
  5. 当前目录。
  6. PATH环境变量中列出的目录。请注意,这不包括“应用程序路径”注册表项指定的每个应用程序路径。计算DLL搜索路径时不使用“ 应用程序路径”键。

如果SafeDllSearchMode被禁用,则搜索顺序如下:

  1. 从中加载应用程序的目录。
  2. 当前目录。
  3. 系统目录。使用GetSystemDirectory函数获取此目录的路径。
  4. 16位系统目录。没有获取该目录路径的函数,但会对其进行搜索。
  5. Windows目录。使用GetWindowsDirectory函数获取此目录的路径。
  6. PATH环境变量中列出的目录。请注意,这不包括“应用程序路径”注册表项指定的每个应用程序路径。计算DLL搜索路径时不使用“ 应用程序路径”键。

参考文档: docs.microsoft.com/en-us/windo…

结语

当两个小时的上班路,宛如一场噩梦,当没完没了的加班,挤占了所有的生活空间,当你就算倾尽所有也没办法成为更好的自己,离开的理由我们或许可以找到很多,但留下来的理由,一群志同道合的伙伴,一份美好甜蜜的爱情,也可能是一个只属于自己的夜晚,就是这些平凡而美好的东西支撑着我们,一次次穿越风浪去遇见更美的风景。————《我在他乡挺好的》