dustland

dustball in dustland

win32程序设计-chapter21 动态链接库与消息钩取

windows SDK chapter 21 动态链接库

库的类型

库类型 拓展名 作用 举例 使用时期 是否链接进入可执行目标文件
对象库(静态库) .lib .obj目标模块集合 LIBC.lib 开发时
导入库 .lib 链接器使用它解析函数调用 kernel32.lib 开发时
动态链接库 .dll 运行时提供支持 kernel32.dll 运行时

库的位置

类似于头文件的搜索顺序,当一个exe程序需要dll支持的时候,windows搜索dll也是有顺序的,编译链接时不需要指定动态库位置

1.exe文件同在的目录

2.shell的当前目录(pwd)

3.windows系统目录,即C://Windows/system32文件夹

4.windows目录,即C://Windows文件夹

5.DOS环境下PATH环境变量

如果在某一步找到则不再继续寻找

Visual C++和Visual Studio

由于这本书年代比较久远,当时的集成开发环境还是VC++,而现在win11上装一个兼容的VC++都不容易了

VC++中的"workspace"对应到VS上就是解决方案"solution"

一个工作区/解决方案 下面可以建立多个项目,一个项目对应一个应用程序或者一个DLL

Linux上的链接与库

此部分在CSAPP-chapter -7链接已经学过,在此只回顾一下GNU GCC命令

预编译

预编译,将源文件.c或者.cpp的宏定义展开成.i文件

1
cpp main.c > main.i

需要输出重定向到main.i,否则cpp命令默认输出到屏幕

汇编

汇编,将源文件汇编成汇编语言文件.s

1
gcc main.c -S

编译

编译,将源文件编译成可重定位目标文件.o

1
gcc main.c -c -o main.o

链接

将多个可重定位目标模块链接成可执行目标模块.out

1
gcc main.o func.o -o main.out

创建静态库

将一个或多个可重定位目标模块.o,创建成静态库.a(归档文件)

1
ar rcs libmain.a main.o

创建动态库

1
gcc -shared -fPIC main.c -o main.so

Visual Studio 创建使用库

Microsoft Visual Studio 2022

静态库的使用

创建静态库

1.新建空白解决方案

(在VC++上"解决方案"叫做workspace工作区)

image-20220721155939293
2.配置新"项目"

解决方案名称就叫做"MATHTOOLS"吧,

一个解决方案可以包含多个项目,但是新建解决方案时也是通过"新建项目"选项卡实现的

image-20220721160058473
3.新建静态库工程

在"解决方案资源管理器"中(视图->解决方案资源管理器或者Ctrl+Alt+L唤醒),

在"解决方案'MATHTOOLS'(0个项目)"上右键->新建项目

image-20220721160318596
4.在添加新项目搜索框中搜索"静态库"
image-20220721160358962
5.配置静态库项目

起个名,然后创建

image-20220721160433974
6.建立静态库项目之后的解决方案资源管理器
image-20220721160547138

有一些vs自动帮我们生成的文件,比如pch.h,framework.h,pch.cpp,MATHLIB.cpp

然而这些都是可有可无的

7.不使用预编译头

在解决方案资源管理器->MATHLIB项目 上右键,属性

image-20220721160748513

在弹出的属性页中将陪着和平台都改成所有的

image-20220721160818483

然后配置属性->C/C++->预编译头->不使用预编译头

image-20220721160906154
8.新建MathLib.h和MathLib.cpp

删掉头文件和源文件中vs帮我们建立好的文件,然后在头文件和源文件夹下面分别新建MathLib.h和MathLib.cpp

MathLib.h

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
#pragma once
struct Point {
double x;
double y;
Point(const double &x,const double &y);
Point();
void setCoordinate(const double& x, const double& y);
double& X();
double& Y();
const double getEuclidianDistance(const Point& p)const;
const double getManHatonDistance(const Point& p)const;
double getSlope(const Point& p)const;
friend std::ostream& operator<<(std::ostream& os, const Point& p);

};
struct Line {
Point a;
Point b;
Line();
Line(const Point& a, const Point& b);
double length()const;
Point &A();
Point &B();
double getSlope()const;
friend std::ostream& operator<<(std::ostream& os, const Line& l);
bool isParallel(const Line &l)const;
};

MathLib.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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <cmath>
#include <algorithm>
#include "MathLib.h"

Point::Point(const double& x, const double& y) {
this->x = x;
this->y = y;
}
Point::Point() {
this->x = 0;
this->y = 0;
}
void Point::setCoordinate(const double& x, const double& y) {
this->x = x;
this->y = y;
}
double& Point::X() {
return this->x;
}
double& Point::Y() {
return this->y;
}
const double Point::getEuclidianDistance(const Point& p)const {
return sqrt(1.0*(x - p.x) * (x - p.x) + (y - p.y) * (y - p.y));
}
const double Point::getManHatonDistance(const Point& p) const{
return abs(x - p.x) + abs(y - p.y);
}
double Point::getSlope(const Point& p) const{
return 1.0*(y - p.y) / (x - p.x);
}
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << "," << p.y << ")";
return os;
}

Line::Line() {

}
Line::Line(const Point& a, const Point& b) {
this->a = a;
this->b = b;
}
double Line::length()const {
return a.getEuclidianDistance(b);
}
Point& Line::A() {
return a;
}
Point& Line::B() {
return b;
}
double Line::getSlope()const {
return a.getSlope(b);
}
std::ostream& operator<<(std::ostream& os, const Line& l ){
os << "[" << l.a << ";" << l.b << "]";
return os;
}
bool Line::isParallel(const Line& l)const {
return abs(getSlope() - l.getSlope()) < 0.001;
}



9.重新生成解决方案
image-20220721164200557

如果生成成功

image-20220721164221010

链接静态库

1.往解决方案中添加新项目
image-20220721164306283
image-20220721164320309
image-20220721164341661

新项目名叫MATHTEST

2.添加引用

此步的作用是

  1. 在链接A时,自动带上 b.lib,debug/release能够自动区分
  2. 当B项目有变化时,若编译A项目前先编译B项目
image-20220721170149106
image-20220721170201234
3.修改包含目录

此步的作用是给#include "MathLib.h"找到包含路径,即MathLib.h在哪里

image-20220721170408179
image-20220721170458649

到此MATHTEST中的源文件就能找到需要包含的MathLib.h头文件在哪里,链接器也能知道MathLib.lib在哪里了

4.测试静态库

在MATHTEST项目中新建一个源文件(或者利用自动创建的MATHTEST.cpp)

随便写点测试静态库的东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include "MathLib.h"
using namespace std;
int main()
{
Point A1(2, 4);
Point B1(4, 8);
Point A2(5, 10);
Point B2(15, 29);
Line L1(A1, B1);
Line L2(A2, B2);
cout << L1.isParallel(L2)<<endl;
cout << A1.getEuclidianDistance(B1) << " " << L1.length() << endl;
cout << A1 << endl;
cout << L1 << endl;
}

然后将MATHTEST项目设置为启动项目(如果让MATHLIB库项目作为启动项目根本起不来)

image-20220721171411117

然后Ctrl+F5开始运行(不调试)

image-20220721171456970

解决方案的结构

可以在解决方案根目录下面,终端上用tree /f命令打印整个结构

解决方案根目录

动态库的使用

创建动态库

1.建立空白解决方案
image-20220721182820966
image-20220721182929764
2.建立动态库项目
image-20220721182953735

具有导出项的动态库

image-20220721183016050

项目名称MATHDLL

image-20220721183052585

属性页设置不使用预编译头

image-20220721183151901

删除掉多余的pch等自动生成的文件之后,解决方案资源管理器是这样的

image-20220721183358281
3.充实头文件MATHDLL.h和原文件MATHDLL.cpp

自动生成的MATHDLL.h中有一个实用的宏定义MATHDLL_API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 MATHDLL_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// MATHDLL_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef MATHDLL_EXPORTS
#define MATHDLL_API __declspec(dllexport)
#else
#define MATHDLL_API __declspec(dllimport)
#endif

// 此类是从 dll 导出的
class MATHDLL_API CMATHDLL {
public:
CMATHDLL(void);
// TODO: 在此处添加方法。
};

extern MATHDLL_API int nMATHDLL;

MATHDLL_API int fnMATHDLL(void);

MATHDLL_API这个宏定义,对于DLL库的头文件来说被翻译为__declspec(dllexport),

而对于包含该头文件的其他项目的源文件来说被翻译为__declspec(dllimport)

关于__declspec(dllexport)干了啥事,现在不想知道,只需要知道导出函数时必须

MATHDLL.h

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
44
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 MATHDLL_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// MATHDLL_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef MATHDLL_EXPORTS
#define MATHDLL_API __declspec(dllexport)
#else
#define MATHDLL_API __declspec(dllimport)
#endif

struct MATHDLL_API Point {//使用MATHDLL_API修饰意思是导出类
double x;
double y;
Point(const double& x, const double& y);
Point();
void setCoordinate(const double& x, const double& y);
double& X();
double& Y();
const double getEuclidianDistance(const Point& p)const;
const double getManHatonDistance(const Point& p)const;
double getSlope(const Point& p)const;
friend std::ostream& operator<<(std::ostream& os, const Point& p);

};
struct MATHDLL_API Line {
Point a;
Point b;
Line();
Line(const Point& a, const Point& b);
double length()const;
Point& A();
Point& B();
double getSlope()const;
friend std::ostream& operator<<(std::ostream& os, const Line& l);
bool isParallel(const Line& l)const;
};


//extern MATHDLL_API int nMATHDLL;

//MATHDLL_API int fnMATHDLL(void);

MATHDLL.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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <cmath>
#include <algorithm>
#include "MATHDLL.h"

Point::Point(const double& x, const double& y) {
this->x = x;
this->y = y;
}
Point::Point() {
this->x = 0;
this->y = 0;
}
void Point::setCoordinate(const double& x, const double& y) {
this->x = x;
this->y = y;
}
double& Point::X() {
return this->x;
}
double& Point::Y() {
return this->y;
}
const double Point::getEuclidianDistance(const Point& p)const {
return sqrt(1.0 * (x - p.x) * (x - p.x) + (y - p.y) * (y - p.y));
}
const double Point::getManHatonDistance(const Point& p) const {
return abs(x - p.x) + abs(y - p.y);
}
double Point::getSlope(const Point& p) const {
return 1.0 * (y - p.y) / (x - p.x);
}
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << "," << p.y << ")";
return os;
}

Line::Line() {

}
Line::Line(const Point& a, const Point& b) {
this->a = a;
this->b = b;
}
double Line::length()const {
return a.getEuclidianDistance(b);
}
Point& Line::A() {
return a;
}
Point& Line::B() {
return b;
}
double Line::getSlope()const {
return a.getSlope(b);
}
std::ostream& operator<<(std::ostream& os, const Line& l) {
os << "[" << l.a << ";" << l.b << "]";
return os;
}
bool Line::isParallel(const Line& l)const {
return abs(getSlope() - l.getSlope()) < 0.001;
}



dllMain.cpp保持原样不用管

此时重新生成解决方案没有错误,则动态库创建完毕了

此时在MATHTOOLS/debug/下面生成四个文件

1
2
3
4
5
6
7
8
9
10
11
12
PS C:\Users\86135\Desktop\testDLL\MATHTOOLS\Debug> ls


目录: C:\Users\86135\Desktop\testDLL\MATHTOOLS\Debug


Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2022/7/21 19:01 52736 MATHDLL.dll
-a---- 2022/7/21 19:01 3532 MATHDLL.exp
-a---- 2022/7/21 19:01 6308 MATHDLL.lib
-a---- 2022/7/21 19:01 1200128 MATHDLL.pdb

有动态库dll这不足为奇,我们要的就是它

有pdb数据库也不足为奇,调试时用的符号就存在这个数据库里

那exp和lib是来干啥的?留作后话

到此动态库创建完毕,下面新建测试项目

链接动态库

链接一个动态库好麻烦,需要三个东西

1.包含头文件路径

2.lib导入库的路径

3.dll本尊的路径

其中头文件在编译的时候给一个符号,lib是一个药引子,它干了啥事留作后话

1.新建控制台测试项目
image-20220721190552713
image-20220721190612424
image-20220721190626516
2.添加引用

在MATHTEST的引用上添加引用,作用同静态库时的用法,即将lib文件链接进入exe文件

image-20220721190715859
image-20220721190737071

此步的作用是添加dll库文件本身的路径

3.添加头文件包含路径

头文件的作用是提供符号引用,只是提供引用的作用,没有其他作用了,不用想太多

有时甚至不包含头文件只链接对应源文件也不会链接报错,警告罢了

image-20220721190917081
image-20220721190857128
4.添加导入库(lib文件)路径

此处需要在链接器->输入->附加依赖项 还有 链接器->常规->附加库目录两个地方进行添加

image-20220721191127957
image-20220721192104087

到此程序就可以运行了

.obj->.lib->.dll

两种库的制作过程中,都有.lib文件,但是静态库中.lib是最终产品,动态库中.lib是一个药引子导入库

MSVC创建使用库

用Vscode打开一个文件夹作为工作目录,然后在本文件夹下面写EDRLIB.C和EDRLIB.H,EDRTEST.C

制作动态库

首先在本目录下面编写EDRLIB.H,EDRLIB.C两个源代码文件

EDRLIB.H

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifdef __cplusplus //防止C++的命名重整,使得本库文件可以被C和C++程序使用
#define EXPORT extern "C" __declspec(dllexport)
#else
#define EXPORT __declspec(dllexport)
#endif

EXPORT BOOL CALLBACK EdrCenterTextA(HDC,PRECT,PCSTR);
EXPORT BOOL CALLBACK EdrCenterTextW(HDC,PRECT,PCWSTR);

#ifdef UNICODE
#define EdrCenterText EdrCenterTextW
#else
#define EdrCenterText EdrCenterTextA
#endif

EDRLIB.C

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <windows.h>
#include "EDRLIB.H"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("StrProg");
HWND hwnd;
MSG msg;

WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;

if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("requires Windows NT"), szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(
szAppName,
TEXT("DLL Demonstrate Program"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);

ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (message)
{
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rect);
EdrCenterText(hdc, &rect, TEXT("in DLLLLLLLer"));
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

return DefWindowProc(hwnd, message, wParam, lParam);
}

使用MSVC编译链接制作动态库

1
2
cl /c EDRLIB.C
link /dll EDRLIB.obj user32.lib kernel32.lib gdi32.lib

到此本目录下面就生成了dll动态库文件和lib导入库文件

链接导入库

编写测试程序EDRTEST.C

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <windows.h>
#include "EDRLIB.H"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("StrProg");
HWND hwnd;
MSG msg;

WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;

if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("requires Windows NT"), szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(
szAppName,
TEXT("DLL Demonstrate Program"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);

ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (message)
{
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &rect);
EdrCenterText(hdc, &rect, TEXT("in DLLLLLLLer"));
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

return DefWindowProc(hwnd, message, wParam, lParam);
}

MSVC编译链接导入库

1
2
cl /c EDRTEST.c
link EDRTEST.obj EDRLIB.lib kernel32.lib user32.lib gdi32.lib

运行时使用动态库

由于EDRTEST.exe和EDRLIB.dll在同一目录下,因此装载器可以找到EDRLIB.dll,直接运行即可

image-20220819155710984

TDM-GCC创建使用库

我电脑上这个TDM-GCC来自于Dev-Cpp,把TDM-GCC的bin目录添加到环境变量path即可在终端上使用

文件都不变

制作动态库

1
gcc -shared EDRLIB.C -o EDRLIB.dll -luser32 -lgdi32 -lkernel32 

然而这一步完成之后只生成了dll动态库文件,没有生成lib导入库文件

链接动态库

1
gcc EDRTEST.C -o EDRTEST.exe -lkernel32 -lgdi32 -luser32 -L./ -lEDRLIB ;执行在当前目录下寻找库文件,库文件名EDRLIB

运行时使用动态库

dll和exe同目录,可以直接运行

1
./EDRTEST.exe
image-20220819161937488

效果和MSVC相同

如果把MSVC形成的exe和TDM-GCC形成的dll放在同一目录则运行报错,反过来也这样,总之就是谁的exe就得有谁的dll支持

DllMain

Dll文件也可以有入口点,DllMain函数,该函数会在Dll被装载或者卸载等等各时期被调用

1
int WINAPI DllMain(HINSTANCE hInstance,DWORD fdwReason,PVOID pvReserved);

参数意义

hInstance是本DLL模块的句柄,实际上是本模块加载进入进程地址空间中的基地址

fdwReason本DllMain函数被调用的原因

含义
DLL_PROCESS_ATTACH(1) 由于进程启动或调用 LoadLibrary,DLL 正在加载到当前进程的虚拟地址空间中。 DLL 可以使用此机会初始化任何实例数据,或使用 TlsAlloc 函数分配线程本地存储 (TLS) 索引。 lpvReserved 参数指示是静态还是动态加载 DLL。
DLL_PROCESS_DETACH(0) DLL 正在从调用进程的虚拟地址空间中卸载,因为它加载失败,或者引用计数已达到零, (进程每次调用 LoadLibrary) 时,都会终止或调用 FreeLibrarylpvReserved 参数指示是否由于 FreeLibrary 调用、加载失败或进程终止而卸载 DLL。 DLL 可以使用此机会调用 TlsFree 函数,以释放使用 TlsAlloc 分配的任何 TLS 索引,并释放任何线程本地数据。 请注意,接收 DLL_PROCESS_DETACH 通知的线程不一定是接收 DLL_PROCESS_ATTACH通知的 线程。
DLL_THREAD_ATTACH(2) 当前进程正在创建新线程。 发生这种情况时,系统会调用当前附加到进程的所有 DLL 的入口点函数。 调用是在新线程的上下文中进行的。 DLL 可以使用此机会初始化线程的 TLS 槽。 使用 DLL_PROCESS_ATTACH 调用 DLL 入口点函数的线程不会使用 DLL_THREAD_ATTACH 调用 DLL 入口点函数。 请注意,DLL 的入口点函数仅由进程加载 DLL 后创建的线程调用此值。 使用 LoadLibrary 加载 DLL 时,现有线程不会调用新加载 DLL 的入口点函数。
DLL_THREAD_DETACH(3) 线程已干净退出。 如果 DLL 已存储指向 TLS 槽中已分配内存的指针,则应使用此机会释放内存。 系统使用此值调用当前加载的所有 DLL 的入口点函数。 调用是在退出线程的上下文中进行的。

pvReserved尚未使用

分拣消息

DllMain的调用,和窗口过程函数WndProc的调用方式很像,

WndProc调用时,message参数记录消息类型.

DllMain调用时,fdwReason记录调用原因

因此DllMain中可以根据调用原因的不同,设计不同的处理过程

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
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpReserved ) // reserved
{
// Perform actions based on the reason for calling.
switch( fdwReason )
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;

case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;

case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;

case DLL_PROCESS_DETACH:
// Perform any necessary cleanup.
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH.
}

Windows消息钩取(Dll注入)

学习windows程序设计有一段时间了,我就知道了一个事,即消息会从操作系统的消息队列分发到应用程序的消息队列.比如键盘按下之后操作系统首先捕捉该消息,然后看看焦点窗口是谁,再把这个消息发给焦点窗口.

而消息钩取就发生在这个消息分发的过程中,钩子程序可以监视从操作系统消息队列发往应用程序消息队列的所有消息

windows消息流

当发生键盘按下的时间时,WM_KEYDOWN消息首先被添加到操作系统消息队列

操作系统判断是哪个应用程序发生的事件,然后从自己的消息队列中取出这个消息添加到对应应用程序的消息队列

应用程序的消息循环不停地从自己的消息队列中取出消息并调用相应的事件处理程序(即窗口过程)

这里窗口过程是回调形式的,因为同一个应用程序可以有多个窗口,自然可以有多个窗口过程,消息循环中的DispatchMessage(&msg);可以决定将消息发往哪个窗口过程

消息钩取发生在消息从OS消息队列到应用消息队列的路上

image-20220819173944182

SetWindowsHookEx数注册钩子

1
2
3
4
5
6
HHOOK SetWindowsHookEx(
int idHook, //hook type
HOOKPROC lpfn, //hook procedure
HINSTANCE hMod, //hook procedure所属的DLL句柄
DWORD dwThreadId //将要挂钩的目标线程ID
);

参数意义

idHook

钩子类型,决定钩子的类型和范围

img
lpfn

钩子过程函数,需要存在于某个DLL内部

该函数是一个回调函数,有固定的参数,返回值要求

1
2
3
4
5
6
LRESULT CALLBACK HookProc//程序员对于钩子过程的名字还是有支配权的
(
int nCode,//钩子代码,一种代码对应一种情况,钩子过程可以根据情况就事论事
WPARAM wParam,//两个附属参数都是nCode的更详细信息,nCode确定了wParam才有意义
LPARAM lParam,
);//返回值一般直接写返回CallNextHookEx的返回值就行了,根WndProc返回DefWndProc差不多

这里(nCode,wParam,lParam)的消息组合起始和WndProc中的(message,wParam,lParam)很像

nCode/message指定消息类型,wParam和lParam是具体的解释

hMod

钩子函数所在实例的句柄,即lpfn所在的DLL句柄

如果是本地钩子,并且钩子就写在本程序中,那么hMod可以是本程序实例句柄,此时lpfn注册的函数也在本程序中,编译之后一运行,程序和钩子都在同一个进程地址空间,钩子可以猥琐欲为

然而远程钩子,即这个进程钩取那个进程消息这种,就一定需要把钩子放在dll中,然后指望操作系统把这个dll,注入到远程进程的地址空间中了

dwThreadId

挂钩的线程ID

若该值为0则全局钩子,影响所有进程

返回值

HHOOK类型,即钩子句柄,钩子实际上也是一种内核数据结构,通过HHOOK句柄可以索引到该钩子

SetWindowsHookEx如果调用成功则返回其注册的钩子,方便后来使用完毕后删除钩子

UnhookWindowsHookEx删除钩子

1
2
3
BOOL UnhookWindowsHookEx(
[in] HHOOK hhk
);

只需要一个钩子句柄HHOOK,这个值是SetWindowsHookEx的返回值,即可注销这次SetWindowsHookEx注册的钩子

钩链

每调用一次SetWindowsHookEx就会注册一层钩子,调用多了就形成一条钩链,

最新注册的钩子最先截获消息

当前钩子回调函数处理完成之后调用CallNextHookEx将消息传递给钩链上下一个钩子回调函数进行处理

1
2
3
4
LRESULT CALLBACK HookProc(int nCode,WPARAM wParam,LPARAM lParam){//钩子过程,回调函数
...
return CallNextHookEx(g_hHook,nCode,wParam,lParam);//轮到下一个钩子了,如果这里不再调用下一个钩子则一般情况下钩链到此为止(但是就有那特殊情况)
}

钩取notepad.exe键盘消息

当钩子和CPU架构不同的时候容易死机,死了n次得到的规律,64位的电脑上就老实编译64位程序,开64位notepad做实验吧

钩子模块KeyHook.dll

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
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <windows.h>

#define DEF_PROCESS_NAME "notepad.exe"

HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;//钩子句柄,由本模块自己维护
HWND g_hWnd = NULL;

BOOL WINAPI DllMain(HINSTANCE hInstDLL, DWORD dwReason, LPVOID lpvReserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH://如果是因为进程附加则g_hInstance获取hInstDLL值
g_hInstance = hInstDLL;
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}


LRESULT CALLBACK KeyBoardProc(int nCode,WPARAM wParam,LPARAM lParam){//钩子过程,回调函数
char szPath[MAX_PATH]={0,};
char *p=NULL;
if(nCode=0){
if(!(lParam&0x80000000)){
GetModuleFileNameA(NULL,szPath,MAX_PATH);
p=strrchr(szPath,'\\');
if(!_stricmp(p+1,DEF_PROCESS_NAME))return 1;
}
}
return CallNextHookEx(g_hHook,nCode,wParam,lParam);//轮到狗链上的下一个钩子了
}

#ifdef __cplusplus //防止C++的命名重整,使得本库文件可以被C和C++程序使用
#define EXPORT extern "C" __declspec(dllexport)
#else
#define EXPORT __declspec(dllexport)
#endif

EXPORT void HookStart(){//导出函数,启动钩子
g_hHook=SetWindowsHookEx(WH_KEYBOARD,KeyBoardProc,g_hInstance,0);
}

EXPORT void HookStop(){//导出函数,停止钩子
if(g_hHook){
UnhookWindowsHookEx(g_hHook);
g_hHook=NULL;
}
}
1
gcc KeyHook.cpp -shared -o KeyHook.dll

本模块导出了两个函数,注册和注销钩子

HookStart设置钩子类型为键盘型,并且注册了钩子过程回调函数KeyBoardProc,设置本模块为钩子所在模块,钩最后一个参数为0表明本钩子模块将被注入所有进程的地址空间

关键在于钩子过程回调函数KeyBoardProc,这个回调函数的返回值,参数个数,类型,意义,都是有固定要求的,除了函数名可以随便写

1
2
3
4
5
6
7
8
9
10
11
12
LRESULT CALLBACK KeyBoardProc(int nCode,WPARAM wParam,LPARAM lParam){//钩子过程,回调函数
char szPath[MAX_PATH]={0,};
char *p=NULL;
if(nCode==0){//当nCode=时对应键盘消息
if(!(lParam&0x80000000)){
GetModuleFileNameA(NULL,szPath,MAX_PATH);
p=strrchr(szPath,'\\');//返回szpath中最后一次出现'\'的位置
if(!_stricmp(p+1,DEF_PROCESS_NAME))return 1;
}
}
return CallNextHookEx(g_hHook,nCode,wParam,lParam);//轮到下一个钩链了
}

当GetMessage或者PeekMessage被调用时或者键盘消息发生时(WM_KEYUP,WM_KEYDOWN),操作系统会调用KeyBoardProc函数

这个函数叫啥无所谓,KeyBoardProc,HookProc等等,只要是SetWindowsEx把他注册上就行

对于键盘类型的钩子,其nCode有两种

HC_ACTION(0)和HC_NOREMOVE(3)

当nCode=HC_ACTION,此时wParam和lParam携带击键信息,和击键消息时的作用差不多

当nCode=HC_NOREMOVE,此时wParam和lParam携带击键信息,并且该消息尚未被从应用程序消息队列中移除

wParam是虚拟键编码

lParam

Bits Description
0-15 重复击键次数
16-23 OEM扫描码,基本用不到了
24 是否为拓展键
25-28 保留
29 如果Alt按下则为1
30 本键的先前状态,如果先前按下则为1
31 本键状态,如果正在按下则为0
if(nCode==0)

例子中首先判断nCode==0,这就确定了lParam和wParam代表的意义

if(!(lParam&0x80000000))

然后判断lParam的第31位是否是0,如果是0即对应键正在按下,则通过判断

GetModuleFileNameA(NULL,szPath,MAX_PATH);

本函数用于获取本进程已经加载的模块的完整路径名

1
2
3
4
5
DWORD GetModuleFileNameA(
[in, optional] HMODULE hModule,
[out] LPSTR lpFilename,
[in] DWORD nSize
);

hModule指定一个本进程已经加载的模块,如果是NULL则返回当前exe应用程序的完整路径

lpFilename用于承载本函数返回的路径的缓冲区

nSize指定lpFilename的大小,防止缓冲区溢出

本例子中这样写GetModuleFileNameA(NULL,szPath,MAX_PATH);

意思就是获取当前exe程序完整路径,放到szPath字符串数组中,该路径最长不能超过MAX_PATH长度

p=strrchr(szPath,'\\')

strrchr用于返回szPath字符串中最后一次出现\字符的指针位置

这里写了两个斜杠,第一个斜杠是转义的意思

if(!_stricmp(p+1,DEF_PROCESS_NAME))

stricmp相对于strcmp来说不区分字母大小写

这里DEF_PROCESS_NAME宏定义为"notepad.exe"

1
#define DEF_PROCESS_NAME "notepad.exe"

即这里判断了刚才返回的本exe文件的名称是否是"notepad.exe"

如果是则返回1,那么该键盘消息就在钩链上中止了,无法到达notepad应用程序的消息队列了,自然表现为无法获取键盘输入

这里钩子函数明明在KeyHook.dll中,那么怎么获取"本exe文件的名称"呢?

这是因为后来本dll文件会被操作系统强行注入exe进程的地址空间

前面进行了三次判断,要求

nCode==1表示键盘消息

lParam最高位为0表示键按下

当前应用程序名叫"notepad.exe"

如果有一次不满足则

1
return CallNextHookEx(g_hHook,nCode,wParam,lParam);

如果都满足则不再继续钩子,直接返回1

HookMain.exe

钩子以dll模块的形式打包,HookMain.exe相当于一个注射器,只要HookMain.exe以运行,钩子模块就会被注入所有进程的地址空间.

钩子自己判断了当前注入的进程,是不是notepad进程,如果是则中断键盘消息的传送,否则继续传递键盘消息

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
#include <stdio.h>
#include <conio.h>
#include <windows.h>
#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"
typedef void (*PFN_HOOKSTART)();
typedef void (*PFN_HOOKSTOP)();
PFN_HOOKSTART HookStart = NULL;
PFN_HOOKSTOP HookStop = NULL;
HMODULE hDll = NULL;
int main()
{
hDll = LoadLibraryA(DEF_DLL_NAME);

HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);

HookStart();//注册钩子
getchar();//如果不输入则钩子一直起作用
HookStop();
FreeLibrary(hDll);//注销钩子

return 0;
}
1
gcc HookMain.cpp -o HookMain.exe