对dll调用的理解

0x00:dll和lib及其区别[^1]

静态库:在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中,这种库称为静态库,其特点是可执行文件中包含了库代码的一份完整拷贝;缺点就是被多次使用就会有多份冗余拷贝。即静态库中的指令都全部被直接包含在最终生成的 EXE 文件中了。在vs中新建生成静态库的工程,编译生成成功后,只产生一个.lib文件

动态库:动态链接库是一个包含可由多个程序同时使用的代码和数据的库,DLL不是可执行文件。动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个 DLL 中,该 DLL 包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。在vs中新建生成动态库的工程,编译成功后,产生一个.lib文件和一个.dll文件

那么上述静态库和动态库中的lib有什么区别呢?

静态库中的lib:该LIB包含函数代码本身(即包括函数的索引,也包括实现),在编译时直接将代码加入程序当中

动态库中的lib:该LIB包含了函数所在的DLL文件和文件中函数位置的信息(索引),函数实现代码由运行时加载在进程空间中的DLL提供

总之,lib是编译时用到的,dll是运行时用到的。如果要完成源代码的编译,只需要lib;如果要使动态链接的程序运行起来,只需要dll。

0x01:dll的生成

打开vs:文件->新建->项目->Win32控制台应用程序->dll(可以使用预编译)
产生的结果如图:

其中 addfun.h 中的内容为:

1
2
3
4
5
6
7
8
#include "stdafx.h"


extern "C"
{
_declspec(dllexport) int add(int a, int b);
typedef int(*ApiAdd)(int, int);
}

_declspec(dllexport)是生成 dll 必须要使用的

然后1.cpp 中的内容为:

1
2
3
4
5
6
7
8
9
// 1.cpp : 定义 DLL 应用程序的导出函数。

#include "stdafx.h"
#include "addfun.h"

int add(int a, int b)
{
return a + b;
}

为了理解 dll 中相关内容的结构和调用,需要查看和更改 dllmain.cpp 中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"
#include <stdio.h>

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//printf中的内容都是自定义的
printf("***DLL_PROCESS_ATTACH***\n");
break;
case DLL_THREAD_ATTACH:
printf("***DLL_THREAD_ATTACH***\n");
break;
case DLL_THREAD_DETACH:
printf("***DLL_THREAD_DETACH***\n");
break;
case DLL_PROCESS_DETACH:
printf("***DLL_PROCESS_DETACH***\n");
break;
}
return TRUE;
}

这时候我们需要理解一下这个 dll 中的这些参数:^2

  • hModule参数:指向DLL本身的实例句柄;

  • ul_reason_for_call参数:指明了DLL被调用的原因,可以有以下4个取值:

  • DLL_PROCESS_ATTACH

    当DLL被进程第一次调用时,导致DllMain函数被调用,同时ul_reason_for_call 的值为 DLL_PROCESS_ATTACH,如果同一个进程后来再次调用此DLL时,操作系统只会增加DLL的使用次数,不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。

  • DLL_PROCESS_DETACH

    当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的ul_reason_for_call值是DLL_PROCESS_DETACH

    注意:如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。

  • DLL_THREAD_ATTACH

    当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。 新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许线程开始执行它的线程函数。

  • DLL_THREAD_DETACH:如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。

    注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。

  • lpReserved参数:保留,目前没什么意义。

所以我们通过打印分支语句中的内容来判断进程和线程的调用情况。
此时直接在vs中点击生成即可生成相关dll和lib文件(位于项目的Debug目录下,不要调试执行)

0x02:dll的调用

直接在vs中新建一个C++项目,而在这个项目中需要用到上一步的就是dll文件和.h头文件,直接将两个文件复制到新建的项目中,然后在vs里面右键项目里面的头文件、添加刚刚复制过来的.h文件就可以,但是要注意需要修改如下:

1
2
3
4
5
6
7
#include "stdafx.h"
extern "C"
{
//将生成dll的语句删除即可
int add(int a, int b);
typedef int(*ApiAdd)(int, int);
}

接下来就是编写主程序.cpp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// use1.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <Windows.h>
#include "addfun.h"
#include<iostream>
#include<stdlib.h>
using namespace std;

DWORD WINAPI ThreadFunc(LPVOID);

int _tmain(int argc, _TCHAR* argv[])
{
int a = 2, b = 1, c = 0;
HINSTANCE hDllInst = LoadLibrary("1.dll");
ApiAdd myfun = 0;
myfun = (ApiAdd)GetProcAddress(hDllInst, "add");

//创建线程
HANDLE hThread;
DWORD threadId;
hThread = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threadId); // 创建线程
printf("我是主线程, pid = %d\n", GetCurrentThreadId()); //输出主线程pid
//ExitThread(0);
//CloseHandle(hThread);

// youFuntionName 在DLL中声明的函数名
if (myfun)
{
c = myfun(a, b);
}
Sleep(500);
cout<< " 1 + 2 = " << c << endl;
FreeLibrary(hDllInst);
system("pause");
return 0;
}

DWORD WINAPI ThreadFunc(LPVOID p)
{
printf("我是子线程, pid = %d\n", GetCurrentThreadId()); //输出子线程pid
return 0;
}

上述代码中HINSTANCE hDllInst = LoadLibrary("1.dll");会出现波浪线错误,此时点击 项目->属性->常规->字符集 修改为“使用多字节字符集”即可。

这时通过手动创建线程才能产生 dll 中的DLL_THREAD_ATTACH条件,而创建的线程在这里是一个子线程,主线程不会产生调用条件。

注意一定要在调用 dll 的进程代码中间创建线程,不然在进程结束后就无法产生调用线程的条件了

在创造线程条件时,我遇到了一点小问题,当时一开始并没有使用 Sleep() 函数,导致最终出现的结果如下:

我的猜测是可能是线程创建的有点慢,导致先打印了“1+2”后来查了一下,可能是因为不Sleep就可能造成线程一直占用CPU,从而使CPU得不到释放,故后在代码中尝试添加了“Sleep(500);” ,最终成功如下:

对比代码和结果就可以大概分析出整个流程,在此不做细述。

注:其实有两种方法调用动态库,一种隐式链接,一种显示链接。具体情况可自行百度或查看参考博客 1


参考博客: [^1]: https://www.cnblogs.com/TenosDoIt/p/3203137.html