概念
1、什么是动态链接库?
动态链接库是为了实现代码的重用是出现的,它们都是一些独立的文件,其中包含能被可执行程序或其他DLL调用来完成某些工作的函数。动态链接库通常都是不能直接运行的,只有在其他模块调用动态链接库中的函数时,它才发挥作用。
2、windows的动态链接库:
1.windows的静态库生成的是.lib文件,其中包含了函数和数据实体,链接时合到程序中;
2.windows的动态库生成.dll文件并导出一个.lib文件,该.lib文件中的函数没有实体[不是一个 准确的说法],函数内部是一个跳转,指向.dll中的函数实体;当然,在.lib中的跳转地址是一个相对地址[函数在dll的偏移地址];而且.dll文件是一个地址可重定位文件。
3.动态库的使用有两种方法:
1)动态链接:
链接时将动态库导出的.lib文件链接到可执行文件中去。
当程序加载到内存时,会有动态链接器将要加载的动态库加载到内存[已在内存则不用加载],此时该.dll内存地址确定,动态链接器将程序中的.lib部分中函数跳转地址改为原来的偏移地址加上该.dll起始地址的结果。这样就可以找到要调用的函数位置了。
至于多进程共享一个动态库,每次动态连接器都将内存中的同一个库的地址映射到不同的进程空间[虚拟内存];并且映射到不同进程空间的地址很可能是不同的。
2)动态加载:
即在程序运行的过程中及时的将需要的库加载进内存[已存在则不用加载],通过相应的API拿到需要使用的动态库函数接口地址,使用函数指针的方式去使用,在使用完后可以动态的卸载动态库。
两种方式对比:
(1) 第一种方法需要函数、变量的声明[即C/C++中要头文件],并且使用.lib来"欺骗"静态链接器;而第二种方法没有函数声明,使用函数时是采取函数指针的方式,所以静态链接器也没办法"找茬"。
(2) 第一种方法由系统来维护动态库加载和卸载;第二种则需要程序员自行解决。那么同时第一种方法该动态库在该进程空间的生命周期与程序生命周期相同;第二种则由程序员决定。
(3) 第一种在发布新的dll时,要用相应的lib文件重新链接程序;第二种则不需要,只需替换掉旧的dll.
Windows API 的所有函数都包含在DLL中。其中有三个最重要的DLL,Kernal32.dll,它包含用于管理内存、进程和线程的各个函数;User32.dll,它包含用于执行用户界面任务(如窗口的创建和消息的传递)的各个函数;GDI32.dll,它包含用户画图和显示文本的各个函数。
工作原理:
加载时机:
第一种方式浪费内存,再自身程序执行时就将需要的dll全部映射到进程地址空间中,有的dll或许是用不到的。内存占用大
第二种方式在使用dll前加载,缺点是响应速度慢
当主线程加载函数A,函数A在DLL_1中实现,便把DLL_1加载到进程的整体空间中,主线程创建的子线程,也想调用函数A,不需要重新载入DLL_1,只需要重新使用之前载入的那一份
2、静态库和动态库的区别
静态库:函数和数据被编译进一个二进制文件(通常扩展名为.lib)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中赋值这些函数和数据并把他们和应用程序的其他模块组合起来创建最终的可执行文件.EXE。
使用时,包含头文件和库文件就可以了。
如果我们使用静态库,那么我们发布我们产品的时候只需要发布产品就可以了,不需要附带静态库。
动态库:在使用动态库的时候,往往提供三个文件:一个后缀名为.h的头文件,一个后缀名同样为.lib的引入库文件和一个DLL动态链接库文件。.h文件包含导出函数的声明,引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要连接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去动态加载DLL,访问DLL中的到处函数。
使用时,有两种方法,见动态链接库加载的两种方式。
如果我们在产品开发中使用动态链接库,那么发布产品的时候需要附带发布动态链接库。
注意:静态库.lib 和动态库的引入库.lib文件的区别。
在动态链接库中可以使用别的动态链接库,但是静态库不能使用别的静态库或动态库的函数。
动态链接库中的函数只有导出的函数才能被调用。必须在需要导出的函数前面添加关键字:_declspec(dllexport)
3 多进程共享动态链接库原理
当多个进程共享dll时,其实内存中只保留一份dll代码,而每个进程调用dll的导出函数或类等等时是单独在进程的堆栈上分配空间的,也就是说每个调用dll的进程单独保留自己使用到的数据,各个进程互不影响。所谓的多进程共享其实就在内存中保留dll代码的空间内“做文章”,如多个进程共享一个dll时,使用LoadLibrary和GetProcAddress得到的地址是一样的,也说明内存中保留一份dll,每个进程共享dll
通常来说动态链接库是不能够直接运行,也不能直接接收消息的,他们是一些独立的文件(后缀名一般为.dll,当然还有其它的一些后缀名也是可以的),其中包含能被可执行程序或其它DLL调用来完成某项工作的函数,也就是说动态链接库也就是由一些函数组成而已。并且只有在其它模块调用动态链接库中的函数时,动态链接库才发挥作用,在实际的编程中,通常可以完成某种功能的函数放在一个动态链接库中,然后提供给其它函数调用。当这个访问了的动态链接库的进程被加载时,系统会为这个进程分配4GB的私有地址空间(如果是32位机的话),然后系统就会分析这个可执行模块,找到这个可执行模块中将所要调用的DLL,然后系统就负责搜索这些DLL找到这些DLL后便将这些DLL加载到内存中,并为他们分配虚拟内存空间,最后将DLL的页面映射到调用进程的地址空间汇总,DLL的虚拟内存有代码页和数据页,他们被分别映射到进程A的代码页面和数据页面,如果这时进程B也启动了,并且进程B也许要访问该DLL,这时,只需要将该DLL在虚拟内存中的代码页面和数据页面映射到第二个进程的地址空间即可。这也表明了在内存中,只需要存在一份DLL的代码和数据。
多个进程共享 DLL 的同一份代码,很明显这样做可以节省内存空间的。
但是在 Windows 下(Linux中也是一样的),由于系统会为每一个进程分配 4GB 的私有地址空间,而 DLL 中的代码和数据也只是映射到了这个私有地址空间中,所以这些应用程序之间还是不能够相互影响的,也就是说多个应用程序虽然是可以共享同一个 DLL 中的相同的代码的,但是 DLL 为每一个进程保存的数据都是不相同的,并且每一个进程都为 DLL 使用的全部数据分配了自己的地址空间,
举个最简单的例子,我的 DLL 中有一个函数为 int Add(int num1 , int num2),这个函数的作用是实现 num1 和 num2 相加并返回相加后的结果。然后我有一个 进程 A 使用了这个 DLL ,并且其调用了函数 Add(10, 20),然后我还有一个 进程 B 其也使用了这个 DLL ,并且其调用了函数 Add(30, 40),那么对于 进程 A 中的数据 10 和 20 其实是保存在 进程 A 的私有地址空间中的,而对于 进程 B 中的数据 30 和 40 则是保存在 进程 B 的私有地址空间中的
或者
dumpbin使用
代码
无参dll
建立dll工程
新建hello.h和hello.c文件,不要新建.cpp
头文件内容:
#pragma once
__declspec(dllexport) void SayHello();
.c文件内容
#include "hello.h"
#include <stdio.h>
void SayHello() {
printf("hello\n");
}
修改项目配置;
生成后:
使用dumpbin查看
输出:
使用dll文件
将
拷贝至需要使用dll文件的工程中
在工程中加入hello.h头文件
在C工程中使用dll:初始化加载法
新建testHello.c文件:
#include "hello.h"
#include <stdio.h>
int main() {
SayHello();
getchar();
return 0;
}
附加依赖项中添加helloworld.lib,因为helloworld.lib是在当前文件夹下,因为可以直接写入
点击运行:
或者:#pragma comment(lib, "XXX.lib")
#pragma comment ( lib,"wpcap.lib" )
是导入1个库文件,以使程序可以调用相应的动态链接库。
和在工程设置里写上链入wpcap.lib的效果一样,不过这种方法写的程序别人在使用你的代码的时候就不用再设置工程settings了。告诉连接器连接的时候要找ws2_32.lib,这样你就不用在linker的lib设置里指定这个lib了。
#pragma comment(lib, "c:\\1\\autozen\\debug\\mess.lib")
只影响build时link的行为。
#pragma 仅仅影响编译器编译的时候,link .lib 库的问题。和运行时没有任何关系。此时仅仅告诉系统需要静态加载一个 .dll 文件。当程序运行时,需要加载这个 .dll 文件。
#include "hello.h"
#include <stdio.h>
#pragma comment(lib,"helloworld.lib")
int main() {
SayHello();
getchar();
return 0;
}
在C工程中使用dll:随用随加载
只需要用到helloworld.dll文件
/**
API:LoadLibrary FreeLibrary GetProcAddress
GetModuleHandle GetModuleFileName
*/
#include <stdio.h>
#include <Windows.h>
int main(void)
{
char path[255] = { 0 };
//获取动态链接库的模块句柄,只有欲获取的模块已映射到调用该函数的进程内,才会正确得到模块句柄
HMODULE hModule = GetModuleHandle("helloworld.dll");
DWORD len = 0;
if (NULL == hModule)//如果该模块没有加载
{
//载入指定的动态链接库,并将它映射到当前进程使用的地址空间。一旦载入,即可访问库内保存的资源
hModule = LoadLibrary("helloworld.dll");
if (NULL == hModule)
{
printf("load library failed...\n");
getchar();
return 1;
}
len = GetModuleFileName(hModule, path, 255);//获取当前进程已加载模块的文件的完整路径
printf("%s\n", path);
}
//检索指定的动态链接库(DLL)中的输出库函数地址。lpProcName参数能够识别DLL中的函数。
FARPROC fn = GetProcAddress(hModule, "SayHello");
fn();//使用
FreeLibrary(hModule);//释放指定的动态链接库
getchar();
return 0;
}
有参dll
dll工程建立如无参
calc.h
#pragma once
__declspec(dllexport) int add1(int a, int b);
__declspec(dllexport) double add2(double a, double b);
__declspec(dllexport) int add3(char *str1, int size, char *str2);
calc.c
#include <string.h>
#include "calc.h"
int add1(int a, int b)
{
return a + b;
}
double add2(double a, double b)
{
return a + b;
}
int add3(char *str1, int size, char *str2)
{
int total = strlen(str1) + strlen(str2);
if (total >= size)
{
return total;
}
strcat_s(str1, size, str2);
return total;
}
在C工程中使用dll:初始化加载法
调用方式同无参
在C工程中使用dll:随用随加载
需要定义有参函数指针
include <stdio.h>
#include <Windows.h>
typedef int (*Add1)(int a, int b);
typedef double(*AAA2)(double a, double b);
typedef int(*STRCAT)(char *str1, int size, char *str2);
int main(void)
{
char str1[255] = "hello ";
char str2[] = "world";
char path[255] = { 0 };
HMODULE hModule = GetModuleHandle("Calculater.dll");
DWORD len = 0;
if (NULL == hModule)
{
hModule = LoadLibrary("Calculater.dll");
if (NULL == hModule)
{
printf("load library failed...\n");
getchar();
return 1;
}
len = GetModuleFileName(hModule, path, 255);
printf("%s\n", path);
}
Add1 add1 = (Add1)GetProcAddress(hModule, "add1");
printf("%d\n",add1(19, 20));
AAA2 add2 = (AAA2)GetProcAddress(hModule, "add2");
printf("%lf\n", add2(1.9, 2.2));
STRCAT add3 = (STRCAT)GetProcAddress(hModule, "add3");
add3(str1, 255, str2);
printf("%s\n", str1);
FreeLibrary(hModule);
getchar();
return 0;
}
C++项目中调用dll
在随用随加载中不会出现问题,但是在初始化加载时报错:
这是因为采用从初始化方式就加载动态库的方式时,它会找以C++规则的函数编号,形式如上图,与C规则的函数编号不同:
这使得在编译链接时报错,解决:使用exten "C"
实现c++代码能够调用其他c语言代码,加上extern "C"后,这部分代码编译器以c语言的方式进行编译和链接,而不是按c++方式
原因:c和c++对同一个函数经过编译后生成的函数名是不同的,由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。如果在c++中调用一个使用c语言编写的模块中的某个函数,那么c++是根据c++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误
calc.h
#pragma once
extern "C"
{
__declspec(dllexport) int add1(int a, int b);
__declspec(dllexport) double add2(double a, double b);
__declspec(dllexport) int add3(char *str1, int size, char *str2);
}
C++生成DLL
将calc.c修改成cal.cpp 生成dll,使用dumpbin查看
生成的函数符号与C的不同,c++的带函数的参数类型。
C++中使用C++生成的dll库,初始化时加载没有问题。在随用随加载中,需要修改GetProcAddress中加载的函数名称。
在C生成的:
C++生成的:
#include <stdio.h>
#include <Windows.h>
typedef int(*Add1)(int a, int b);
typedef double(*AAA2)(double a, double b);
typedef int(*STRCAT)(char *str1, int size, char *str2);
int main(void)
{
char str1[255] = "hello ";
char str2[] = "world";
char path[255] = { 0 };
HMODULE hModule = GetModuleHandle("Calculater.dll");
DWORD len = 0;
if (NULL == hModule)
{
hModule = LoadLibrary("Calculater.dll");
if (NULL == hModule)
{
printf("load library failed...\n");
getchar();
return 1;
}
len = GetModuleFileName(hModule, path, 255);
printf("%s\n", path);
}
Add1 add1 = (Add1)GetProcAddress(hModule, "?add1@@YAHHH@Z");
printf("%d\n", add1(19, 20));
AAA2 add2 = (AAA2)GetProcAddress(hModule, "?add2@@YANNN@Z");
printf("%lf\n", add2(1.9, 2.2));
STRCAT add3 = (STRCAT)GetProcAddress(hModule, "?add3@@YAHPADH0@Z");
add3(str1, 255, str2);
printf("%s\n", str1);
FreeLibrary(hModule);
getchar();
return 0;
}
Dll中导出类
Student.h 在类名前加 __declspec(dllexport)
class __declspec(dllexport) Student
{
public:
void SayHello();
};
Student.cpp
#include <stdio.h>
#include "Student.h"
void Student::SayHello()
{
printf("hello\n");
}
初始化加载法
工程中添加Student.h
#include <stdio.h>
#include "Student.h"
#pragma comment(lib, "Student.lib")
int main(void)
{
Student stu;
stu.SayHello();
getchar();
return 0;
}
随用随加载
不能实现,loadLibrary加载库后,GetProcAddress的方法不能直接使用,需要定义对象,但是可以采用另外一种方式:仅仅到处一个C风格的函数:extern "C" __declspec(dllexport) long CreateIFsWrapObj(IDLLIFsWrap** ppObj);
student.h
class IDLLIFsWrap
{
public:
virtual void DeleteSelf(void) = 0;
virtual int Add(int x, int y) = 0;
};
extern "C" __declspec(dllexport) long CreateIFsWrapObj(IDLLIFsWrap** ppObj);
student.cpp
#include <stdio.h>
#include "Student.h"
class CMyDLLIFsWrap : public IDLLIFsWrap
{
public:
CMyDLLIFsWrap();
virtual ~CMyDLLIFsWrap();
virtual void DeleteSelf(void) override;
virtual int Add(int x, int y) override;
};
long CreateIFsWrapObj(IDLLIFsWrap** ppObj)
{
*ppObj = new CMyDLLIFsWrap();
return 0;
}
CMyDLLIFsWrap::CMyDLLIFsWrap()
{
}
CMyDLLIFsWrap::~CMyDLLIFsWrap()
{
}
void CMyDLLIFsWrap::DeleteSelf(void)
{
delete this;
}
int CMyDLLIFsWrap::Add(int x, int y)
{
return x + y;
}
使用:
#include <stdio.h>
#include <windows.h>
#include "Student.h"
int main(void)
{
typedef long(*FunCreateIFsWrapObj)(IDLLIFsWrap **pp_obj);
HMODULE m_myDllHInst = LoadLibrary("Student.dll");
if (nullptr == m_myDllHInst)
return FALSE;
FunCreateIFsWrapObj funCreateObj = (FunCreateIFsWrapObj)GetProcAddress(m_myDllHInst, "CreateIFsWrapObj");
if (nullptr == funCreateObj)
{
FreeLibrary(m_myDllHInst);
m_myDllHInst = nullptr;
return FALSE;
}
IDLLIFsWrap *m_pIFsWrapObj=nullptr;
if (0 != funCreateObj(&m_pIFsWrapObj))
{
FreeLibrary(m_myDllHInst);
m_myDllHInst = nullptr;
return FALSE;
}
int sum = m_pIFsWrapObj->Add(1, 2);
printf("%d",sum);
getchar();
return 0;
}
类库的工业化设计
首先新建一个抽象类 ,如
Persion.h
#pragma once
class Person
{
public:
virtual void say() = 0;
virtual void eat() = 0;
virtual ~Person() = 0;
};
新建一个dll项目:如Chinese继承Persion
Chinese.h
#pragma once
#include "Person.h"
class Chinese : public Person
{
public:
Chinese();
virtual ~Chinese();
virtual void say();
virtual void eat();
};
extern "C"
{
__declspec(dllexport) Person* GetPersonInstance();
__declspec(dllexport) void ReleaseInstance(Person *p);
}
这里建议将导出函数使用C语言编译,导出的函数接口名称比较易懂
.cpp
#include <iostream>
#include "Chinese.h"
using namespace std;
Person::~Person()
{
cout << "~Person" << endl;
}
Chinese::Chinese()
{
cout << "Chinese" << endl;
}
Chinese::~Chinese()
{
cout << "~Chinese" << endl;
}
void Chinese::say()
{
cout << "普通话" << endl;
}
void Chinese::eat()
{
cout << "饺子" << endl;
}
Person* GetPersonInstance()
{
return new Chinese();
}
void ReleaseInstance(Person *p)
{
delete p;
}
导出函数
使用:
/*
编码:袁春旭
讲解:袁春旭
内容:工业类库的玩法
*/
#include <stdio.h>
#include <Windows.h>
#include "Person.h"
typedef Person* (*GetPersonInstance1)();
typedef void (*ReleaseInstance1)(Person *p);
int main(void)
{
HMODULE hModule = LoadLibrary("CompanyA.dll");
if (NULL == hModule)
{
printf("加载失败\n");
return 0;
}
GetPersonInstance1 pfn = (GetPersonInstance1)GetProcAddress(hModule, "GetPersonInstance");
Person *p = pfn();
p->say();
p->eat();
ReleaseInstance1 pfn2 = (ReleaseInstance1)GetProcAddress(hModule, "ReleaseInstance");
pfn2(p);
FreeLibrary(hModule);
getchar();
return 0;
}
通过向导自动生成dll
vs2017 community
DllMain简介
跟exe有个main或者WinMain入口函数一样,DLL也有一个入口函数,就是DllMain。以“DllMain”为关键字,来看看MSDN帮助文档怎么介绍这个函数的。
The DllMain function is an optional method of entry into a dynamic-link library (DLL) 。(简要翻译:对于一个Dll模块,DllMain函数是可选的。)这句话很重要,很多初学者可能都认为一个动态链接库肯定要有DllMain函数。其实不然,像很多仅仅包含资源信息的DLL是没有DllMain函数的。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
何时调用DllMain
系统是在什么时候调用DllMain函数的呢?静态链接时,或动态链接时调用LoadLibrary和FreeLibrary都会调用DllMain函数。DllMain的第三个参数fdwReason指明了系统调用Dll的原因,它可能是DLL_PROCESS_ATTACH、DLL_PROCESS_DETACH、DLL_THREAD_ATTACH和DLL_THREAD_DETACH。以下从这四种情况来分析系统何时调用了DllMain。
DLL_PROCESS_ATTACH
大家都知道,一个程序要调用Dll里的函数,首先要先把DLL文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法:静态链接和动态链接的LoadLibrary或者LoadLibraryEx。
当一个DLL文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数,传递的fdwReason参数为DLL_PROCESS_ATTACH。这种调用只会发生在第一次映射时。如果同一个 进程后来为已经映射进来的DLL再次调用LoadLibrary或者LoadLibraryEx,操作系统只会增加DLL的使用次数,它不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。 不同进程用LoadLibrary同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DLL的DllMain函数。
可参考DllMainTest的DLL_PROCESS_ATTACH_Test函数。
DLL_PROCESS_DETACH
当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的fdwReason值是DLL_PROCESS_DETACH。当DLL处理该值时,它应该执行进程相关的清理工作。
那么什么时候DLL被从进程的地址空间解除映射呢?两种情况:
◆ FreeLibrary解除DLL映射(有几个LoadLibrary,就要有几个FreeLibrary)
◆ 进程结束而解除 DLL映射,在进程结束前还没有解除DLL的映射,进程结束后会解除DLL映射。(如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。)
注意:当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain函数。因此,必须确保没有清理那些没有成功初始化的东西。
可参考DllMainTest的DLL_PROCESS_DETACH_Test函数。
DLL_THREAD_ATTACH
当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。
新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许进程开始执行它的线程函数。
注意跟DLL_PROCESS_ATTACH的区别,我们在前面说过,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,是不再用DLL_PROCESS_ATTACH调用DllMain的。而DLL_THREAD_ATTACH不同, 进程中的每次建立线程,都会用值DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样。
DLL_THREAD_DETACH
如果线程调用了 ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并 用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。
注意:如果线程的结束是因为系统中的一个线程 调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
工程目录:
在pch.h中:添加需要引入的头文件
// pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。
#ifndef PCH_H
#define PCH_H
// 添加要在此处预编译的标头
#include "framework.h"
#include <stdio.h>
#endif //PCH_H
在dllmain中:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
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,并没有导出函数符号
使用dll:
#include <stdio.h>
#include <Windows.h>
int main(void)
{
unsigned uiThread1ID;
HMODULE hModule = LoadLibrary("AutoDll.dll");
if (NULL == hModule)
{
printf("load library failed\n");
getchar();
return 1;
}
FreeLibrary(hModule);
getchar();
return 0;
}
输出:
#include <stdio.h>
#include <Windows.h>
#include <process.h>
FARPROC pfn;
HMODULE hModule;
unsigned __stdcall threadfunc(void * pThis)
{
hModule = LoadLibrary("AutoDll.dll");//通过子线程将库加载到进程空间中,输出DLL_PROCESS_ATTACH
if (NULL == hModule)
{
printf("load library failed\n");
getchar();
return 1;
}
return 0;
}
int main(void)
{
unsigned uiThread1ID;
_beginthreadex(NULL, 0, threadfunc, NULL, 0, &uiThread1ID);
Sleep(1000);
//分成两步:先从子线程结束输出,输出 DLL_THREAD_DETACH,再从主线程中卸载FreeLibrary(hModule,输出:DLL_PROCESS_DETACH
FreeLibrary(hModule);
getchar();
return 0;
}
输出:
#include <stdio.h>
#include <Windows.h>
#include <process.h>
FARPROC pfn;
HMODULE hModule;
unsigned __stdcall threadfunc(void * pThis)
{
return 0;
}
int main(void)
{
hModule = LoadLibrary("AutoDll.dll");
if (NULL == hModule)
{
printf("load library failed\n");
getchar();
return 1;
}
unsigned uiThread1ID;
_beginthreadex(NULL, 0, threadfunc, NULL, 0, &uiThread1ID);
Sleep(1000);
FreeLibrary(hModule);
getchar();
return 0;
}
__cdecl和__stdcall
visual C++默认使用__cdecl的调用约定
函数的调用约定,顾名思义就是对函数调用的一个约束和规定(规范),描述了函数参数是怎么传递和由谁清除堆栈的。它决定以下内容:(1)函数参数的压栈顺序,(2)由调用者还是被调用者把参数弹出栈,(3)以及产生函数修饰名的方法
声明和定义处调用约定必须要相同:在VC++中,调用约定是函数类型的一部分,因此函数的声明和定义处调用约定要相同,不能只在声明处有调用约定,而定义处没有或与声明不同。
函数的调用过程
要深入理解函数调用约定,你须要了解函数的调用过程和调用细节。
假设函数A调用函数B,我们称A函数为”调用者”,B函数为“被调用者”。如下面的代码,ShowResult为调用者,add为被调用者。
int add(int a, int b)
{
return a + b;
}
void ShowResult()
{
std::cout << add(5, 10) << std::endl;
}
函数调用过程可以这么描述:
(1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前任务的信息。
(2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B的栈底)。
(3)然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作被调用者B的栈空间。
(4)函数B返回后,从当前栈帧的ebp即恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置;然后调用者A再从恢复后的栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebp和esp就都恢复了调用函数B前的位置,也就是栈恢复函数B调用前的状态。
__cdecl的特点
__cdecl 是 C Declaration 的缩写,表示 C 和 C++ 默认的函数调用约定。是C/C++和MFCX的默认调用约定。
按从右至左的顺序压参数入栈、。
由调用者把参数弹出栈。切记:对于传送参数的内存栈是由调用者来维护的,返回值在EAX中。因此对于像printf这样可变参数的函数必须用这种约定。
编译器在编译的时候对这种调用规则的函数生成修饰名的时候,在输出函数名前加上一个下划线前缀,格式为_function。如函数int add(int a, int b)的修饰名是_add。
#pragma once
__declspec(dllexport) int add1(int a, int b);
__declspec(dllexport) double add2(double a, double b);
__declspec(dllexport) int add3(char *str1, int size, char *str2);
#include <string.h>
#include "calc.h"
int add1(int a, int b)
{
return a + b;
}
double add2(double a, double b)
{
return a + b;
}
int add3(char *str1, int size, char *str2)
{
int total = strlen(str1) + strlen(str2);
if (total >= size)
{
return total;
}
strcat_s(str1, size, str2);
return total;
}
__stdcall的特点
如果要导出个c#,python使用,需要使用这样方式
__stdcall是Standard Call的缩写,是C++的标准调用方式,当然这是微软定义的标准,__stdcall通常用于Win32 API中(可查看WINAPI的定义)。
按从右至左的顺序压参数入栈。
由被调用者把参数弹出栈。切记:函数自己在退出时清空堆栈,返回值在EAX中。
__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_function@number。如函数int sub(int a, int b)的修饰名是_sub@8。
#pragma once
__declspec(dllexport) int __stdcall add1(int a, int b);
__declspec(dllexport) double __stdcall add2(double a, double b);
__declspec(dllexport) int __stdcall add3(char *str1, int size, char *str2);
#include <string.h>
#include "calc.h"
int __stdcall add1(int a, int b)
{
return a + b;
}
double __stdcall add2(double a, double b)
{
return a + b;
}
int __stdcall add3(char *str1, int size, char *str2)
{
int total = strlen(str1) + strlen(str2);
if (total >= size)
{
return total;
}
strcat_s(str1, size, str2);
return total;
}
add1 参数是两个int 4*2 =8
add2 参数是两个double 8*2=16
add 参数是两个指针,一个int,在32位中 4*2+4 =12
通过def文件规范C语言导出符号
在使用__stdcall中,函数符号如下:
导出的符号不好用,没有cdecl的清晰,在其他语言中使用出现问题。
新建def文件:
添加内容:
EXPORTS
add1
add2
add3
这里的内容要和函数名相同
#pragma once
__declspec(dllexport) int __stdcall add1(int a, int b);
__declspec(dllexport) double __stdcall add2(double a, double b);
__declspec(dllexport) int __stdcall add3(char *str1, int size, char *str2);
添加到:
生成后
这时候导出符号就发生了变化
通过def文件规范C++语言导出符号
函数无参时
同C语言方式
函数含参时
添加extern "C"以c语言方式编译