win32api之链接库的创建与调用(八)

发布时间 2023-04-08 23:13:30作者: 亨利其实很坏

静态链接库

定义

静态链接库(Static Linking Library)是一种库文件,其中包含可重定位的二进制代码,以及与这些代码相关的数据和函数定义。在编译时,这些代码被静态地链接到程序的可执行文件中,以创建一个完全独立的可执行文件,该文件不需要在运行时与外部库文件进行交互,但也使得程序的可执行文件变得更大


创建静态库

Visual Studio新建项目时选择静态库, 然后再创建一个头文件StaticLib1.h

1


以下分别是StaticLib1.hStaticLib1.cpp的代码,StaticLib1.h用来声明静态库的函数,StaticLib1.cpp用来定义静态库的函数

//StaticLib1.h
pragma once

class MyClass {
public:
	int add(int x, int y);
};
// StaticLib1.cpp
include "pch.h"
include "framework.h"
include "StaticLib1.h"

int MyClass::add(int x, int y)
{
	return x+y;
}

然后鼠标右键项目处生成StaticLib1.lib文件

image-20230309105429145


调用静态库函数

方法一

若要调用静态库的函数, 需将静态库的头文件StaticLib1.h和库文件StaticLib1.lib复制到项目目录里

1


随后在项目添加引入头文件和引用库文件的代码, 如下所示

include <iostream>
include <windows.h>
include "StaticLib1.h"  //导入头文件
pragma comment(lib,"StaticLib1.lib")  //引用库文件

int main()
{	
	MyClass myclass;
	std::cout << myclass.add(1, 2);
}

方法二

将头文件和库文件复制到VS安装目录,与系统库文件放在一起,如下是我Visual Studio的系统库目录

E:\Microsoft Visual Studio\2017\Enterprise\VC\Tools\MSVC\14.16.27023\lib  //系统库文件
E:\Microsoft Visual Studio\2017\Enterprise\VC\Tools\MSVC\14.16.27023\include  //系统头文件

点击项目属性->链接器->输入, 在附加依赖项添加库文件

1


然后就可以像使用系统库函数那样来调用我们的静态库函数, 而无需将库文件和头文件放到项目目录里面, 如下代码所示

include <iostream>
include <windows.h>
include <StaticLib1.h>  //导入头文件

int main()
{	
	MyClass myclass;
	std::cout << myclass.add(1, 2);
}

动态链接库

定义

动态链接库(Dynamic Linking Library),也被称为共享库(Shared Library),是一种可以在运行时动态加载并链接的库文件。与静态链接库不同,动态链接库的代码在程序运行时才会被加载到内存中,因此动态链接库相对于静态链接库来说更加灵活,且不会使目标可执行文件变得过于庞大

使用动态链接库的可执行文件在运行时需要依赖相应的动态链接库文件(通常是以.dll为后缀名),因为动态链接库的代码在程序运行时才会被加载到内存中


创建动态库

有两种方法来声明动态链接库的导出函数,方法一通过定义宏来实现, 方法二通过创建.def文件来实现

方法一

如下代码用于定义导出函数的宏以及声明导出函数。其中, __declspec(dllexport)用于导出DLL函数,这样在链接DLL的应用程序中就可以调用这些函数,__declspec(dllimport)用于导入DLL函数,这样在编译DLL的应用程序中就可以使用这些函数

//DllDemo.h
ifdef ADD_EXPORTS //判断是否为导出DLL函数
define ADD_API __declspec(dllexport) //若是则定义ADD_API为__declspec(dllexport)
else
define ADD_API __declspec(dllimport)  //否则定义ADD_API为__declspec(dllimport)
endif

extern "C" ADD_API int add(int a, int b);  //声明导出函数
extern "C" ADD_API int sub(int a, int b);

//DllDemo.cpp

include "pch.h"
include "DllDemo.h"

int add(int x, int y)
{	
	return x+y;
}

int sub(int a, int b)
{
	return a-b;
}

将生成的dll文件拖入pe查看器查看其导出表,可以发现导出函数的名称是可见的,若你不想别人看到导出函数的名称,可以采用方法二来创建动态库

1

方法二

方法二可以通过函数编号来导出函数,首先工程新建一个.def文件,这里我创建的是Export.def文件

1


Export.def的代码如下所示, LIBRARY后接动态链接库的项目名称, EXPORTS表示DLL导出函数, NONAME表示不显示导出函数的名称

LIBRARY MyDll  
EXPORTS
add @11 
sub @12 NONAME

以下分别是DllDemo.hDllDemo.cpp文件的代码

//DllDemo.h
int add(int a, int b);  
int sub(int a, int b);
//DllDemo.cpp
include "pch.h"
include "DllDemo.h"

int add(int x, int y)
{	
	return x+y;
}

int sub(int a, int b)
{
	return a-b;
}

将生成的Dll放入PE查看器查看,可以发现不能查看sub函数的名称,但是可以看到其编号

1

调用动态库函数

显示调用

显示调用模块函数通常是指使用LoadLibrary函数和GetProcAddress函数,首先使用LoadLibrary函数加载动态链接库,并返回模块句柄,然后使用GetProcAddress函数获取导出函数的地址,最后使用函数指针来调用导出函数。

include <iostream>
include <windows.h>
include <StaticLib1.h>

//声明函数指针
typedef int(*p_add)(int, int);
typedef int(*p_sub)(int, int);

//定义函数指针变量
p_add add;
p_sub sub;

int main()
{	
	//导入dll
	HMODULE hModule = LoadLibrary("MyDll.dll");

	//获取dll的导出函数
	add = (p_add)GetProcAddress(hModule, "add");  //通过名称获取导出函数
	sub = (p_sub)GetProcAddress(hModule, (char*)12);  //通过序号获取导出函数

	//调用导出函数
	int result1 = add(1, 2);
	int result2 = sub(2, 1);

	std::cout << "Add_Result: " << result1 << std::endl;
	std::cout << "Sub_Result: " << result2 << std::endl;

	//释放DLL
	FreeLibrary(hModule);
}

隐式调用

隐式调用需将lib文件和dll文件放到当前项目目录中,隐式调用模块函数通常是指在编译期间就将动态链接库中的导出函数作为外部函数链接到可执行文件中,在代码中直接调用函数名即可

include <iostream>
include <windows.h>
include <StaticLib1.h>

//引入动态链接库
pragma comment(lib,"MyDll.lib")

//声明dll的导出函数
__declspec(dllimport) int add(int, int);
__declspec(dllimport) int sub(int, int);


int main()
{	

	int result1 = add(1, 2);
	int result2 = sub(2, 1);

	std::cout << "add_result: " << result1 << std::endl;
	std::cout << "sub_result: " << result2 << std::endl;
	
}	

两者的区别

隐式调用模块函数是在编译期间就链接了导出函数,因此代码中直接调用函数名即可,但是会增加可执行文件的大小,且不能动态加载;而显示调用模块函数可以在运行时动态加载导出函数,可以更加灵活,但需要手动使用LoadLibrary和GetProcAddress函数来获取导出函数的地址

隐式调用动态链接库的可执行文件会有一个导入表,导入表是一个数据结构,它记录了可执行文件需要引用的动态链接库中的函数及其地址


两者Lib文件的区别

静态链接库的LIB文件包含函数的导出表和函数代码,而动态链接库的LIB文件则只包含函数的导出表

静态链接库的LIB文件在编译时被链接到目标程序中,使得目标程序可以直接调用静态链接库中的函数。而动态链接库的LIB文件只是为了在链接时提供函数的导出信息,实际的函数代码在运行时从DLL文件中加载


DllMain函数

DLLMain 是 DLL 的入口函数,用于在加载或卸载 DLL 时执行一些初始化或清理操作,DLLMain 函数在 DLL 加载和卸载过程中被系统自动调用,其语法格式如下:

BOOL WINAPI DllMain(
  HINSTANCE hinstDLL,   // DLL句柄,表示Dll模块的基址
  DWORD fdwReason,      // 调用原因
  LPVOID lpvReserved   // 保留参数
){
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE; 
}

fdwReason 参数有以下几种取值:

  • DLL_PROCESS_ATTACH: DLL 被加载到进程地址空间中(LoadLibrary),此时可以进行初始化操作。
  • DLL_PROCESS_DETACH: DLL 被从进程地址空间中卸载(FreeLibrary),此时可以进行清理操作。
  • DLL_THREAD_ATTACH: 进程创建了一个新线程并且此线程使用了 DLL,此时可以进行线程相关的初始化操作。
  • DLL_THREAD_DETACH: 线程结束时调用,可以进行线程相关的清理操作