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文件
需要输出重定向到main.i,否则cpp命令默认输出到屏幕
汇编
汇编,将源文件汇编成汇编语言文件.s
编译
编译,将源文件编译成可重定位目标文件.o
链接
将多个可重定位目标模块链接成可执行目标模块.out
1 gcc main.o func.o -o main.out
创建静态库
将一个或多个可重定位目标模块.o,创建成静态库.a(归档文件)
创建动态库
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.添加引用
此步的作用是
在链接A时,自动带上 b.lib,debug/release能够自动区分
当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.l ength() << 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 MATHDLL_EXPORTS #define MATHDLL_API __declspec(dllexport) #else #define MATHDLL_API __declspec(dllimport) #endif class MATHDLL_API CMATHDLL {public: CMATHDLL(void ); }; 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 MATHDLL_EXPORTS #define MATHDLL_API __declspec(dllexport) #else #define MATHDLL_API __declspec(dllimport) #endif struct MATHDLL_API 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 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 ; };
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 #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同目录,可以直接运行
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 )
时,都会终止或调用 FreeLibrary
。 lpvReserved 参数指示是否由于 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, DWORD fdwReason, LPVOID lpReserved ) { switch ( fdwReason ) { case DLL_PROCESS_ATTACH: break ; case DLL_THREAD_ATTACH: break ; case DLL_THREAD_DETACH: break ; case DLL_PROCESS_DETACH: break ; } return TRUE; }
Windows消息钩取(Dll注入)
学习windows程序设计有一段时间了,我就知道了一个事,即消息会从操作系统的消息队列分发到应用程序的消息队列.比如键盘按下之后操作系统首先捕捉该消息,然后看看焦点窗口是谁,再把这个消息发给焦点窗口.
而消息钩取就发生在这个消息分发的过程中,钩子程序可以监视从操作系统消息队列发往应用程序消息队列的所有消息
windows消息流
当发生键盘按下的时间时,WM_KEYDOWN消息首先被添加到操作系统消息队列
操作系统判断是哪个应用程序发生的事件,然后从自己的消息队列中取出这个消息添加到对应应用程序的消息队列
应用程序的消息循环不停地从自己的消息队列中取出消息并调用相应的事件处理程序(即窗口过程)
这里窗口过程是回调形式的,因为同一个应用程序可以有多个窗口,自然可以有多个窗口过程,消息循环中的DispatchMessage(&msg);
可以决定将消息发往哪个窗口过程
消息钩取发生在消息从OS消息队列到应用消息队列的路上
image-20220819173944182
SetWindowsHookEx数注册钩子
1 2 3 4 5 6 HHOOK SetWindowsHookEx ( int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId ) ;
参数意义
idHook
钩子类型,决定钩子的类型和范围
img
lpfn
钩子过程函数,需要存在于某个DLL内部
该函数是一个回调函数,有固定的参数,返回值要求
1 2 3 4 5 6 LRESULT CALLBACK HookProc ( int nCode, WPARAM wParam, LPARAM lParam, );
这里(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; 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 #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 ){ 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); }
当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