人, 既无虎狼之爪牙,亦无狮象之力量,却能擒狼缚虎,驯狮猎象,无他,唯智慧耳。
全部博文(167)
分类: WINDOWS
2012-02-25 09:57:14
DLL入门 by windhawk
自己看DLL也有一个礼拜了,接下来准备开始学习如何写注入程序,在这之前,先来小结一下自己对DLL的理解。这篇文章只是一个DLL的入门文档,不会涉及太深的问题,给出了两个DLL小例子,希望帮助第一次接触DLL的同学们建立感性认识。
一、DLL
什么是DLL呢?DLL其实就是动态链接库的英文缩写(dynamic-link library, DLL),它被广泛地用于Windows操作系统。笼统地来说,DLL就是一段可执行共享代码,但是这段代码不能单独执行,必须由其他的进程或者线程来调用、启动。这样,同样一段代码可以同时被许多进程同时使用,当然,这里涉及全局变量和静态变量的部分在各个进程的地址空间中都有自己的一份备份,使得维护各个独立的进程。
如何理解DLL,我的理解是从两个角度来理解:
1. DLL作为链接库
DLL首先是个链接库。链接库用于为不同的程序提供共用的可执行代码。比如C中的printf(), scanf()等标准I/O函数,各种程序都要用,为了方便就写成库的形式,用#include
2. DLL的动态性
进程好比一个程序运行所需要的所有资源的容器,进程执行的代码必须都在进程的私有地址空间中。所以库文件也必须加载到该进程的地址空间,普通的库文件加载是在程序加载时由系统的加载器负责的,先将程序的代码和数据加载进进程空间,再去检查导入段加载DLL,完成之后可能还会执行DllMain()函数进行一些初始化,完成之后才会将控制权交给主函数。如此内存中就会出现同个库文件的多个副本,比如printf(),在原本就稀缺珍贵的内存中这属于极度浪费的行为,而且也反过来限制了同时并发进程的数量。因此,人们将一部分库文件写成DLL,链接生成.exe文件时没有加入到其中,而是在程序运行需要时动态地加载进内存,而后映射到进程的虚拟地址空间,使用完毕后再予以释放。如此一来,库文件只在需要的时候占用内存,极大地提高了代码和内存的利用效率。这里值得强调的是,DLL文件只会在内存中加载一次,多个进程调用该DLL时只需要将内存地址(PA)映射到进程的虚拟地址空间即可,这其中,每个DLL的全局变量和静态变量在进程的地址空间中都有一份独立的拷贝,使得它们在多进程运行时不会相互影响。
二、简单的DLL程序——隐式加载DLL
在进一步叙述之前我们说明几个概念,DLL同正常的源文件编写一样,也包括函数和变量。其中只在DLL内部可以使用的函数或者变量我们统称为内部符号,而可以被外部的可执行文件调用的函数或者变量我们统称为导出符号。一个可以向外导出符号的模块我们称之为DLL模块,调用外部导出符号的模块我们称之为可执行模块。模块是相对而言的,DLL模块也可以从另一个DLL模块中导入符号,从而变为可执行模块。
好了,现在考虑如果写一个DLL工程,首要的要解决DLL中导出符号和内部符号的区别,这就需要一个声明:
_declspec(dllexports) 导出符号
_declspec(dllimports) 导入符号
现在,我们试着写我们的第一个DLL工程,这个工程中要包含DLL模块和可执行模块。我们在Visual Studio 2008下完成这个简单的项目。为了方便,我们统一放到一个解决方案下:
接下来,我们编写DLL模块:
首先是DLL头文件:
*******************************************************************************
//隐式加载DLL
//DLL头文件:
//头文件中需要声明DLL中的导出函数,
//为了简便,我们利用条件预编译头将程序文件中的导入函数声明也包含进来
//然后在DLL文件中添加导出函数声明即可
#ifdef MYLIBAPI //如果定义了符号,则什么也不做
#else //否则,定义导入符号
#define MYLIBAPI extern "C" _declspec(dllimport)
#endif
//用在可执行模块中的导入符号声明
MYLIBAPI int Add(int nLeft, int nRight);
PS:这里的extern “C”是告诉C++编译器不要对符号名进行改编,以防动态链接时找不到目标符号,这是为了考虑对C的兼容性
*******************************************************************************
接下来是DLL源文件。DLL源文件并不神秘,除了没有main()函数,其他基本都与源文件一样。这里我们包含进刚才编写的DLL头文件,定义一个简单的求和函数。
*******************************************************************************
#define MYLIBAPI extern "C" _declspec(dllexport)
#include "DLLHeader.h"
int Add(int nLeft, int nRight)
{
return nLeft + nRight;
}
*******************************************************************************
编译生成DLL文件后我们获得MyDLLTest.dll和MyDLLTest.lib两个文件。.DLL文件中存放着我们的执行代码,但是并不直接参与后续的.exe编译,而是由生成的库文件.lib代替同目标文件编译链接生成.exe。在.DLL文件中包含一个导出段,列出了该.DLL导出的符号和RVA(相对地址,便于后续寻址符号),而.lib文件则列出.DLL和导出符号的对应关系。我们可以用VS2008自带的dumpbin命令查看文件格式:
可以清晰看到其中导出的函数Add,左边第一个值为RVA-000110BE。
生成DLL后我们再来写可执行模块
*******************************************************************************
#include
#include
#include "..\MyDLLTest\DLLHeader.h" //需要包含我们的DLL头文件
#pragma comment(lib, "..\\Debug\\MyDLLTest.lib") //指明链接库文件所在地,起始目录从工程当前目录开始
int main(int argc, char *argv[])
{
int nLeft;
int nRight;
puts("请输入两个整数,用空格隔开:\n");
scanf("%d%d", &nLeft, &nRight);
printf("sum is %d\n", Add(nLeft, nRight));
system("pause");
return 0;
}
/*隐式加载DLL
因为需要在程序源文件中直接引用DLL中的导出符号,因而必须在文件头声明DLL导入符号,并且指定
*.lib文件的位置,用于实际加载*.exe时查找指定的DLL加载导入符号,如:
导入符号--->*.lib--->.dll--->DLL的导出段--->导出符号的RVA
*/
*******************************************************************************
查看下此时的MyExeFile.exe文件
可以看到,可执行模块的导入段中包含我们的MyDLLTest.dll以及系统的Kernel32.dll以及MSVCR90D.dll
运行结果:
三、简单的DLL程序——显示加载DLL
大多数的应用程序使用隐式加载就可以完成任务了,但是有时候我们希望DLL可以在程序运行的时候加载,而非初始化的时候加载。过多的初始化过程无疑会降低程序的评价。基本步骤同隐式加载方式,不再赘述,直接上代码。这回我们没有使用头文件,而是在需要的时候临时添加相关预处理命令。
DLL源文件:
*******************************************************************************
#define MYLIBAPI extern "C" _declspec(dllexport)
#include
#include
MYLIBAPI int MyMessageBox()
{
MessageBox(NULL, L"这是我的第一个DLL程序!", L"DLL-Dynamic", MB_OK); //如果不加L会提示不能实现字符串类型转换
return 0;
}
*******************************************************************************
生成DLLTestDna.dll和DLLTestDna.lib,查看DLL如下:
可执行模块源文件:
*******************************************************************************
#include
#include
#include
//定义函数指针原型
typedef int(*MyMessageBox)();
int main(int argc, char *argv[])
{
//装载DLL
HMODULE hModule = LoadLibrary(L"DLLTestDna.dll");
if (hModule == NULL)
{
printf("LoadLibrary error \n");
return 0;
}
//取得DLL中的导出函数地址
MyMessageBox NewMessageBox = (MyMessageBox)GetProcAddress(hModule, "MyMessageBox");
if (NewMessageBox == NULL)
{
printf("GetProcAddress error!\n");
return 0;
}
//利用函数指针调用DLL中函数
NewMessageBox();
//释放DLL
FreeLibrary(hModule);
system("pause");
return 0;
}
/*
PS:
1.显示加载DLL时可执行模块不再直接调用DLL中的导出符号,而是通过调用API实现;
2.使用LoadLibray()加载DLL到内存并且映射到进程地址空间,返回的句柄hModule即为DLL的虚拟地址;
3.使用GetProcAddress()获得DLL中指定函数或者符号的地址,返回的亦为地址,所以后续需要使用指针NewMessageBox引用;
4.使用指针引用函数,必须在开头定义函数类型指针;
5.使用完毕后记得立刻FreeLibrary()将DLL释放(内存中释放还要考虑计数因素)。
*/
*******************************************************************************
运行结果为:
我们查看下此时的.exe文件,会发现没有导入段了。
因为此时加载.exe时不需要同时连接DLL。
四、其他问题
其实DLL还有很多问题需要考虑,但是作为一个入门文档,我们今天并不涉及。比如DLL生成时会有一个首选基地址,默认都是相同的,比如DLL的是0x10000000。多个DLL加载会发生冲突,必须重新定位虚拟基地址,这样无疑会加大初始化时间。我们可以用Rebase工具重新设置默认基地址,加速初始化过程。
再比如DllMain()问题。对于DLL来说,这无疑是一个重要的问题。但是我们今天也没有过多讨论。作为一个入门文档只是希望可以给各位同学们建立一个感性认识就可以了。以后我们会陆续讨论。