什么是dll:
dll只是一组源代码的模块,每个模块包含一些可供应用程序或者其他dll调用的函数,在应用程序调用一个dll里面的函数的时候,操作系统会将dll的文件映像映射到进程的地址空间中,这样进程中所有的线程就可以调用dll中的函数了
dll加载完成后,这个时候dll对于进程中的线程来说只是一些被放在地址进程空间附加的代码和数据,操作系统为了节省内存空间,同一个dll在内存中只有一个,也就是说如果你的的两个应用程序都需要加载user32.dll,那么操作系统也只会加载一次user32.dll到内存中
因为代码段在内存中的权限都是为只读的,所以当多个应用程序加载同一个dll的时候,不用担心应用程序会修改dll的代码段。当线程调用dll的一个函数,函数会在线程栈中取得传递给他的参数,并使用线程栈来存放他需要的变量,dll函数创建的任何对象都为调用线程或者调用进程拥有,dll不会拥有任何对象,也就是说如果dll中的一个函数调用了VirtualAlloc,系统会从调用进程的地址空间预定地址,即使撤销了对dll的映射,调用进程的预定地址依然会存在,直到 用户取消预定或者进程结束
示例代码:
mylib.h
1 #ifdef MYLIBAPI 2 #else 3 #define MYLIBAPI extern "C" __desclspec(dllimport) 4 #endif 5 6 MYLIBAPI int g_nResult; 7 8 MYLIBAPI int Add(int nLeft,int nRight)
mylib.cpp
1 #include <windows.h> 2 3 #define MYLIBAPI extern "C" __declspec(dllexport) 4 #include "mylib.h" 5 int g_nResult; 6 7 int Add(int nLeft,int nRight) 8 { 9 g_nResult = nLeft + nRight; 10 return g_nResult; 11 }
输入命令:
cl /LDd mylib.cpp
可以生成可供调试的dll
这个时候会多出四个文件,分别是mylib.exp,mylib.lib,mylib.dll,mylib.obj
mylib.obj保存的是在链接器生成dll的需要的信息
当链接器检测到应用程序导出了一个函数或者变量,链接器就会生成mylib.lib文件,这个只是列出了导出的函数和变量的符号名
输入命令查看lib里面的导出段
dumpbin -exports mylib.lib
我们可以看到这个lib里面export了_Add和_g_nResult
如果我们使用dumpbin -imports mylib.lib
imports里面没有变量或者函数,这是因为lib里面记录的只是导出的函数和变量,只有在声明有导出函数或者变量的时候,才会生成这个文件
mylib.dll则是我们最终生成的模块
如果使用dumpbin查看mylib.dll的导出
导入则因为太多,所以不贴出来
到我们需要将一个函数导出的时候,可以使用__desclspec(dllexport)来声明为导出函数,需要从dll使用一个函数的时候,可以使用__desclspecc(dllimport)来前置声明一个函数,当然,也可以不使用import前置声明,但是使用improt可以明确告诉编译器这些函数是从dll导入的,提高效率
什么是导出?
当将函数或者变量声明为导出后,编译器在生成obj的时候会嵌入一些额外的信息,以便于让链接器在生成dll的时候使用,并且会生成一个记录导出函数和变量的lib文件,在生成可执行文件的时候,我们需要通过链接这个lib来取得dll的一些信息,链接器在生成dll的时候,会在dll文件中嵌入一个导出符号表,这个符号表记录了导出的函数和变量的符号名,并且保存对应的文件偏移量地址,这样当可执行文件需要调用dll里面的函数的时候,可以通过这个符号表来找到对应函数的地址
最后我们开始构建可执行文件,代码如下
myexe.cpp
#include <cstdio>
#include "mylib.h"
int main(void)
{
int nLeft = 10,nRight = 20;
printf("%d\n",Add(nLeft,nRight));
}
#include "mylib.h"
int main(void)
{
int nLeft = 10,nRight = 20;
printf("%d\n",Add(nLeft,nRight));
}
cl myexe.cpp mylib.lib
我们在编译的时候一定要链接mylib.lib,这样编译器才知道要到哪里去找mylib的变量和函数相关信息,并且可执行文件也才知道程序需要mylib.dll这个dll,这样程序在加载的时候会搜索用户磁盘上的dll,如果没找到则会报错,找到则将dll映射到进程的内存空间里面
当dll映射到进程的内存空间里面后,加载程序会查看在对应的dll的导出段符号是否存在,如果不存在,则报错,如果存在,那么加载程序会将该符号加载到该符号的所在的文件偏移量(RVA,虚拟地址,但在dll里面实际上是该符号所在文件的位置),加上该dll加载的虚拟地址,保存到可执行程序的导入段中,当代码引用到导入符号的时候,可执行文件会去查看导入段并且得到导入符号的地址,这样就能访问导入的变量或者函数
例如我们生成的mylib.dlll文件,利用dumpbin可以得到输出:
我们可以看到Add的RVA是1000,假设我们的dll被应用程序映射到1000的地址空间中,那么在应用程序执行的时候,Add函数最终会被加载到1000+1000即2000处,这个就是我们前面所说的dll在映射到地址空间后,对于应用程序来说不过是一堆附加的代码和数据