winodws核心编程随笔
- 第一章 错误处理
- 第二章 字符及字符处理
- 第三章 内核对象
- 第四章 进程
- 第五章 作业
- 第六章 线程
- 第七章 线程的上下文、调度、优先级、关联性
- 第八章 第九章 线程同步
- 第十章 同步设备与异步设备的IO
- 第十一章 windows线程池
- 第十二章 纤程
- 第十三章 windows内存体系结构
- 第十四章 探索虚拟内存
- 第十五章 虚拟内存的使用
- 第十六章 线程栈
- 第十七章 内存映射文件
- 第十八章 堆
- 第十九章 DLL基础
- 第二十章 DLL高级
- 第二十一章 线程局部存储区TLS
- 第二十二章 DLL注入和API拦截
- 第二十三章~第二十六章关 异常与恢复
一些自己实现的代码的环境都是vs2019。
第一章 错误处理
1.WINDOWS版本的errno
当函数出错,不同的返回值类型有不同的描述,分别是:
①VOID 该类型的API不会失败
②BOOL 失败返回0,否则返回一个非0值
③HANDLE HANDLE标识一个可以操纵的对象,失败一般返回NULL,有些函数返回INVALID_HANDLE_VALUE(-1)。
④PVOID 该类型标识一块内存地址,出错返回NULL
⑤LONG / DOWRD 这两个都是数值类型,一般情况下返回0或-1
当函数出错,会在一个名为“线程本地存储区”的地方存储错误代码,其实说白了就是线程安全的errno,只不过errno可以直接访问来取得其错误代码,而windows下则通过函数DWORD GetLastError()
来取得。
另外,发现函数出错后,要及时调用GetLastError()
来取得信息,因为有些函数成功调用也会改变其值。
2.将错误代码转换为字符串
通过DWORD FormatMessage(好几个参数不想写)
将错误代码转为字符串,但是书中的例子有点复杂,好多类型也看不懂,先留个坑
3.自定义错误以及设置错误代码
首先自定义函数可以通过返回FALSE、NULL、-1等来表示出错。
在返回之前通过函数VOID SetLastError(DWORD dwErrCode)
来设置错误代码。
关于该参数的传递,应优先使用”WinError.h“中的现有代码,如果要自定义该代码的值,传递规则在 本章以及第24章,本章第二个坑
2021年1月4日22:22:18
第二章 字符及字符处理
1.字符编码
ANSI:以0结尾的单字节字符数组
Unicode:
UTF-8:有些字符编码1字节,有些2字节,有些3字节,有些4字节
UTF-16:本书默认,也是一直强调推荐使用的,每个字符编码都是2字节
UTF-32:每个字符都是4字节
默认情况下,C编译器默认将其作为ANSI的单字节数组。
内建类型wchar_t,表示16的Unicode,即UTF-16。typedef unsigned short wchar_t;
使用Unicode:
wchar_t C = L'A';
wchar_t buff[100] = L"str";
为了和C的类型有一些些区别,windows用typedef重封装了一下,诸如:
typedef char CHAR;
typedef wchar_t WCHAR;
然后使用宏定义,让其兼容ANSI和Unicode:
#ifdef UNICODE
typedef WCHAR TCHAR;
#else
typedef CHAR TCHAR;
endif
2.windows中函数对 ANSI和Unicode 的不同行为
从某时起,windows所有版本的函数完全用Unicode构建。
如果参数传递的ANSI,会先把ANSI转换为Unicode再传递。
如果返回值是ANSI,则会把Unicode转换为ANSI返回。
需要使用字符串的函数一般有两个版本,后缀W(Unicode)和A(ANSI)
其中W的版本是正常工作的那个版本
而A的版本,则是一个包装版本,即转换为Unicode后调用W的版本,返回时又将其转换为ANSI的格式返回。
他们同样使用宏定义来实现兼容:
#ifdef UNICODE
#define xxxx xxxxW
#else
#define xxxx xxxxA
endif
p16到本章末尾都是云里雾里的,大部分是告诉我是什么,但是完全不知道涉及到具体场景里应该如何操作,又一个坑
第三章 内核对象
1.何为内核对象
感觉首先应该从用户态和内核态说起,
内核对象是 应用程序 和 系统内核 交互的媒介。
而一个32位系统最多能寻址4GB的空间(后见第十三章,第一节):
(用户态)其中0x00000000~0x7FFFFFFF是各个进程的私有空间(感觉像是虚拟的?),每个进程都可以访问这段内存,但是相互独立,不同进程间的相同地址实际上也是不一样的。
(内核态)随后0x80000000~0xFFFFFFF,这一块不是进程私有,而是系统持有,而且是独一一份,所有进程共享,不会每个进程都是不同的空间,内核对象就放在这里。
当用户调用系统函数,应用程序从用户模式切换到内核模式,这中间便涉及到了内核对象。
内核对象有很多类型,比如说io端口对象、线程对象、进程对象、互斥量对象等等。
每个内核对象都是一个内存块,是一个数据结构,有系统内核分配,并且只能由内核访问。其中存储的信息不能直接更改,应用程序能做的只有通过Windows提供的函数,传入一定格式的数据来改变这些内核对象。
2.内核对象句柄
看起来像是一个指针,指向内核对象,但是实际上只是概念上的相似。
首先,每个进程初始化时会有一个名为进程句柄表的东西
然后,句柄值是作为该表索引的存在。
再接着,句柄值是是进程相关的,也就是不同进程中可能会有相同的句柄值,但是所代表的确是不同的对象。如果通过某种方式将句柄传递到另一个进程,那么会将该句柄值作为索引,在另一个进程的句柄表中查找,这是严重的错误。
还有可能会遇到的问题,在之后关闭内核对象的小结详述。
3.对象句柄的标志以及get/set
句柄有两个标志
HANDLE_FLAG_INHERIT 表示句柄是否可继承
HANDLE_FLAG_PROTECE_FORM_CLOSE 表示句柄是否可被关闭
两个相关函数
BOOL SetHandleInformation(HANDLE hObject, DWORD dwMask, DWORD dwFlags);
BOOL GetHandleInformation(HANDLE hObject, PDWORD pdwFlags);
设置和获取的例子
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); // 打开标志
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0); // 关闭标志
DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags); // 获取标志
if(dw & HANDLE_FLAG_INHERIT)... // 判断标志
4.使用计数
跟引用计数类似,存在的意义是存在跨进程间共享的内核对象,当引用计数为0,才会真正的释放对象。
5.对象安全性
内核对象有一个安全描述符来保护,其描述了谁拥有对象、哪些组或用户可以访问或使用此对象,大概是权限控制之类的。
其类型名为SECURITY_ATTRIBUTYES
不过对于该安全性本身的使用,书中虽然说它很有用,大概是兼容性那方面的有用,但是却几乎被人遗忘,该参数都是NULL。
不过还是有两个很有用的特性:
①判断一个对象是否是内核对象
有内核对象,自然也有非内核对象,而内核对象的创建,都需要这个类型为SECURITY_ATTRIBUTYES的对象作为参数,所以判断对象是否是内核对象,只需要观察其创建时参数是否需要这个结构即可。
②子进程共享父进程内核对象时,使用该类型的一个成员来实现目的
详见之后跨进程共享内核对象的小节。
6.创建内核对象
如果创建失败,有的返回NULL,有的返回-1,一定要搞清楚到底返回什么
基本都是HANDLE CreateXxxx(.....)
当进程内一个线程 调用 一个会创建内核对象的函数(比如CreateFileMapping),内核为这个对象分配内存,然后初始化
接着内核扫描进程句柄表,找到一个空白项将其填入,
句柄值作为该表的索引值,索引内容有指向内核对象的指针、访问掩码、标志
调用一个系统函数,把该句柄传递给该函数,然后通过索引找到指向对象的指针,通过指针来完成一些操作,当然这些都是不可见的。
7.关闭内核对象
BOOL CloseHandle(HANDLE hobject)
在内部检查主调进程句柄表,验证进程确实有权限访问该对象,然后计数递减,为0则销毁。
如果传递的句柄是无效的,有两种情况:
①进程被调试:抛出异常说明“指明了无效的句柄”
②进程正常运行:函数本身返回FALSE,GetLastError返回ERROR_INVALID_HANDLE。
关闭内核对象,应该及时将存储句柄的对象设为NULL,不然它的行为就会看起来像野指针一样。如果没有及时清除又被使用了的话,会遇到以下情况:
①该句柄值索引的条目是空的,那么会产生无效句柄的错误
②该句柄值索引项处填充了一个另一个类型不同的内核对象,这种情况也会产生错误
③该句柄值索引项处填充了一个和先前类型一样的对象,这是最严重最难发现的情况,还不会报错
当然,只要及时设为NULL,就不会有这些麻烦。
关于对象资源的泄露,当进程结束,系统会检查句柄表确保所有对象被清除。
但是运行时如果没有及时关闭也没有被有效使用,那么理所当然导致运行时的泄露。
2021年1月8日0:50:28
8.跨进程共享内核对象的三种方式
8.1.对象句柄继承
首先,继承的是对象句柄,而不是内核对象本身。
对象本身是位于0x80000000~0xFFFFFFF所有进程共享的这一块内存,
而对象句柄是位于0x000000~0x7FFFFFFF各个进程独立的那块内存的句柄表中。
使用这种方式共享,需要两个条件:
①父进程创建内核对象时,参数是被设为可继承的,也就是说返回的句柄是可继承的。
②父进程调用CreateProcess
创建子进程时,参数bInheritHandles被设为TRUE。
在继承的过程中,被继承的句柄,在父进程和子进程的句柄表中的位置 是一模一样的,复制过程中还会增加对象的使用计数。
另外,句柄的继承只发生在创建子进程时,父进程后来创建的可继承内核对象,不会被继承。子进程也不知道自己继承了哪些句柄,需要额外手段告知。
一个创建可继承对象的例子:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = null;
sa.bInheritHandle = true;
// 设为可继承
HANDLE hmutex - CreateMutex(&sa, False, NULL);
CreateProcess略
8.2.为对象命名
本应该写在本节最后却写在开头,以示强调。
为内核对象命名是有安全性问题的的,关于其具体描述及解决方法在第9节。
还有一个小技巧,可以通过CreateXxxx某个对象的方式,来防止程序多次运行。
思路是通过CreateXxxx后,调用GetLastError,判断其是否返回ERROR_ALREADY_EXISTS,当得到该值,说明该名称对象以及被创建,也就意味着先前已经有程序运行,此时调用exit结束。
为对象命名的方式有两种方法:
①多次create同名对象
首先,创建具名对象的方式是在创建时传递一个字符串作为其名称,如:CreateMutex(NULL, FALSE, TEXT("OBJ"));
,一般创建内核对象的函数,都会有名称这个参数,这个名称的长度最多MAX_PATH
,其值为260。
然后,需要注意的是,具名对象的命名空间 是 整个操作系统中所有的对象 共享一个命名空间,也就是说如果这个名字简单,极易和其他程序的命名冲突而导致不可预见错误。冲突的时候不区分其到底是什么类型,只认名字本身。如果检查到名称已存在,则返回NULL。通过GetLastError,得到(6)ERROR_INVALID_HANDLE。
一个进程A第一次CreateXxxx某个具名对象,如果其不存在,则创建。
一个进程B第二次CreateXxxx同名对象,检查到同名对象存在,随后检查其对象是否相同,再接着检查访问权限,如果都通过,那么在进程B的句柄表中创建对象,否则因为类型不同或者权限不足而导致返回NULL。另外因为对象已经存在,所以第二次调用时传递的可继承参数(第二参数)是无效的。
HANDLE ProcessA = CreateMutex(NULL, FALSE, TEXT("OBJ"));
HANDLE ProcessB = CreateMutex(NULL, FALSE, TEXT("OBJ"));
②用open打开某个具有名字的对象
OpenXxxx的形式,
首先在那个全局共享的内核对象命名空间中查找某个名字,
如果没有找到返回NULL,GetLastError
返回(2)ERROT_FILE_NOTFOUND。
如果找到相同名字但类型不同,返回NULL,GetLastError
返回(6)ERROR_INVALID_HANDLE.
名称相同,类型相同,检查权限,为其在句柄表中添加句柄,如果OpenXxxx时,继承参数传入true,那么该句柄就是可继承的,注意是对象本身。
8.3.复制对象句柄
函数原型,各个参数在注释中说明,
一个示例:DuplicateHandle(ProcessHandleA, HandleA, ProcessHandleB, &HandleB, 0, TRUE, DUPLICATE_SAME_ACCESS)
这种方式在进程B中,B自己也不知道是否复制了某些句柄,需要额外通知。
一个小技巧:一个文件句柄有读写权限,在某段逻辑中只需要读权限,那么在当前进程中复制一份句柄,使其只具有读权限,这就用到了参数dwDesiredAccess。
BOOL WINAPI DuplicateHandle(
__in HANDLE hSourceProcessHandle, // (进程A)被复制句柄的 进程的句柄,划重点,其对象类型一定是进程句柄
__in HANDLE hSourceHandle, // 被复制的句柄值,该句柄之和第一参数指明的进程相关
__in HANDLE hTargetProcessHandle, // (进程B)目标进程的句柄,句柄被复制到的那个进程,划重点,其对象类型一定是进程句柄
__out LPHANDLE lpTargetHandle, // 句柄指针,被填写为目标进程中新建的句柄值,这个参数有点奇特
// 可能的情况是,进程A调用了该函数,进程A通过该参数取得了那个新建,被新复制的句柄,但是这个句柄却是和进程B相关的
// 也就是说,如果进程A通过该句柄值操作,是不可预计行为
__in DWORD dwDesiredAccess, // 一般被忽略
__in BOOL bInheritHandle, // 创建的句柄是否可被继承
__in DWORD dwOptions // 该参数可为0或 DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE的任意组合
// DUPLICATE_SAME_ACCESS表示和原句柄一样的访问掩码,该标志导致忽略参数dwDesiredAccess
// DUPLICATE_CLOSE_SOURCE表示关闭源进程的句柄
);
9.终端命名空间、专有命名空间
在windows中有多个内核对象的命名空间,其中有一个叫全局命名空间,就是默认所有内核对象都在的那个。
其他的命名空间由用户的客户端创建,诸如远程登陆,用户切换之类的。
这样分开的好处同名对象不会互相干扰。
安全隐患是:如果采用了本章8.2节那个小技巧,那么可能会有恶意程序先一步创建这个具名对象,从而导致正常进程无法启动。
而解决方法是p52下方开始描述,创建边界和管理员组的安全描述符关联等一系列操作,偷懒了,看的一大堆程序头疼,毕竟目前为止还没动手写过一个win程序,全是理论,又一个坑。
2021年1月10日21:25:38
第四章 进程
获取进程io信息,读写、次数读写字节数等。如果进程在一个作业中,详见第五章。
BOOL GetProcessIoCounters(HANDLE hProcess, PIO_COUNTERS pioCounters);
PIO_COUNTERS结构见第五章。
关于进程的执行时间(用户态和内核态时间),见第七章第三节。
1.一些概念
进程的构成:
①一个内核对象,操作系统用其来管理进程。
②一个地址空间,包含可执行文件或DLL模块的代码和数据。
windows两种类型应用程序:
①CUI,控制台形式
②GUI,图形界面形式
子系统
为了区分是CUI还是GUI,将CUI和GUI统称为子系统。
vs2019中,右击项目-》属性-》链接器-》系统第一项即可看到子系统条目。其中
/SUBSYSTEM:CONSOLE 即代表CUI
/SUBSYSTEM:WINDOWS 代表GUI。
不同类型及相应的入口点函数名称
ANSI字符的CUI程序 main
Unicode字符的CUI程序 wmain(这个书中是Wmain,但是尝试时Wmain是不行的,wmain才行)
ANSI字符的GUI程序 WinMain
Unicode字符的GUI程序 wWinMain
程序启动时,操作系统通过 加载程序 将 可执行文件 加载到本章开头的地址空间中,然后检查该文件映像的文件头,读取这个子系统值,
定义了/SUBSYSTEM:CONSOLE就寻找main和wmain
定义了/SUBSYSTEM:WINDOWS就寻找WinMian和wWinMain
2.进程实例据句柄 & 内存基址
加载到进程地址空间的每一个可执行文件或者DLL文件都有其独一无二的实例句柄,代表的就是其内存基址(见后)。
其中可执行文件的实例被当作WinMain的第一个参数传入,其类型为HINSTANCE。
类型 HINSTANCE 和 HMODULE 是一样的,等价的,有些函数需要后者,是为了兼容而存在的。
HINSTANCE代表的就是一个内存基址,是被加载到进程地址空间后的地址。
获取某个模块的地址(这个模块或许是可执行文件或许是DLL):
HMODULE GetModuleHandle(LPCTSTR lpModuleName);
如果参数传空,返回的是 主调进程 的 可执行文件的基址。(例子见后)
如果传递某个字符串,那么就是寻找对应名字的可执行文件或DLL。(例子见后)
获取当前运行模块的基址:
伪变量 __ImageBase,这个变量指向当前运行模块的基址。(例子见后)
例子(所需注意内容在注释说明):
#include <stdio.h>
#include <Windows.h>
// __ImageBase的使用需要,且需要定义在#include <Windows.h>之后
extern "C" const IMAGE_DOS_HEADER __ImageBase;
int wmain()
{
HMODULE hmodule = GetModuleHandle(NULL); // 传递控制获取当前主调进程的可执行文件基址
printf("0x%x\r\n", hmodule); // 打印
hmodule = NULL;
hmodule = GetModuleHandle(L"createprocees.exe"); // 传递可执行文件的名字,获取其基址,createprocees.exe就是vs生成的可执行文件的名称
printf("0x%x\r\n", hmodule); // 打印
printf("0x%x\r\n", (HINSTANCE)&__ImageBase); // 通过这个伪变量来取得当前运行模块的基址
// 当然,这三个打印出来的值是一样的,
getchar();
return 0;
}
3.获取命令行参数 & 将其拆解为独立参数
LPTSTR GetCommandLine(void);
用于取得整个命令行参数返回,需要注意的是多次调用,都是返回的同一个缓冲区,也就是说如果修改它,下一次调用时就看不到原来的值了
LPWSTR * CommandLineToArgvW(LPCWSTR lpCmdLine, int* pNumArgs);
用于将取得的命令行参数拆解为独立的字串,然后返回,返回的地址是在内部申请的数组,会在程序结束自动释放,当然也可手动释放,见下例子。
这个函数只有后缀w的版本,所以调用的GetCommandLineW也需要指明是W版本。
#include <stdio.h>
#include <Windows.h>
int wmain()
{
printf("%S\n", GetCommandLine());
int myargc = 0;
PWSTR* myargv = CommandLineToArgvW(GetCommandLineW(), &myargc);
for (int i = 0; i < myargc; ++i)
printf("%S\n", myargv[i]);
HeapFree(GetProcessHeap(), 0, myargv);
getchar();
return 0;
}
4.进程的环境变量 & 对其的各种操作
用户登录windows时,系统创建shell进程,并将一组环境变量该shell关联。
系统通过检查两个注册表项来获得初始的环境字符串。
①HKEY_LOCAL_MACHIN\SYSTEM\CurrentControlSet\Control\Session Manager\Environment
②HKEY_CURRENT_USER\Environment
第一个注册表项包含:应用于系统的所有环境变量列表
第二个注册表项包含:应用于当前登录用户的环境变量列表
然后在该用户下启动的进程就会有这些环境变量(这句不是书上的,是我这么认为的)。
他们是存放在进程地址空间中的。
读取环境变量
这里要说明一下GetEnvironmentStrings()
函数返回值的格式。
类似于“name1=val1\0name2=val2\0name3=val3\0\0”
称之为环境块
所以出现了下方例子中奇怪的写法。
#include <stdio.h>
#include <Windows.h>
int wmain()
{
PTSTR pEnvBlock = GetEnvironmentStrings();
PTSTR tmp = pEnvBlock; // 备份操作,需要释放空间
while (1)
{
printf("%S\n", tmp); // 打印当前指向的环境变量
while (*tmp != L'\0') // 接下来三行操作指针越过当前环境变量
tmp++;
tmp++;
if (*tmp == L'\0') // 如果是连续两个\0,就代表块结束
break;
}
FreeEnvironmentStrings(pEnvBlock);
getchar();
return 0;
}
判断一个环境变量是否存在
DWORD GetEnvironmentVariable(LPCTSTR lpName, LPTSTR lpBuffer, DWORD nSize);
、
返回值设为0表示不存在,否则返回变量对应值所需的字符数
第一个参数是查询的变量名
第二个参数是输出缓冲区,变量值在此返回
第三参数填是第二参数缓冲区的长度
// 这块代码在书中是调用两次
// 第一次调用时第三参数传递0,通过返回值确定值所需的空间长度,然后申请一块刚好足够的空间
// 第二次调用传递缓存区(第二参数)以及缓冲区长度(第三参数)
// 这里方便直接弄个超大的空间,一次调用完成
PTSTR VAL = new WCHAR[2048];
if (0 != GetEnvironmentVariable(L"USERNAME", VAL, 2048));
printf("USERNAME=%S", VAL);
delete[] VAL;
对环境变量的添加、修改、删除
BOOL SetEnvironmentVariable (PCTSTR name, PCTSTR value);
如果指定变量不存在,就添加“name=value”
如果指定变量存在,就修改为“name=value”
如果value传递NULL,那么删除环境变量(程序测试的时候,好像删除了,自己添加一个再删除也不行)
将环境变量的值扩展到某个字符串中
xxx\%xxx%\xxx,对于这种形式的字串来说,两个%号之间的就是可被替换的
// 输出结果:MYVAR=hkzyhl
// hkzyhl是变量USERNAME的值
// 字串中%之间的部分,被替换成了对应环境变量的值
// 书中的代码同样是调用两次,和GetEnvironmentVariable()相似,这里偷懒写法
PTSTR VAL = new WCHAR[2048];
ExpandEnvironmentStrings(L"MYVAR=%USERNAME%", VAL, 2048);
printf("%S\n", VAL);
delete[] VAL;
2021年1月11日23:13:06
5.关于进程一些不太重要的东西
进程 前一个实例 的句柄4.1.2
这个是WinMain函数的第二个参数,一般传空
int _stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
进程所在的驱动器和目录4.1.7 4.1.8
①进程对于每个驱动器都有一个当前目录,即在C盘会有一个,在D盘也有一个,默认为其根目录。
②进程的当前目录,这两个东西应该是不一样的,前面的当前目录是进程相对于驱动器的目录。而这里的进程当前目录是当下进程运行时的目录。
这里好乱,书上分了两节,也不知道这么理解对不对。
一个线程可以使用以下两个函数设置获取进程的当前的驱动器和目录:
DWORD GetCurrentDirectory(DWORD nBufferLength, LPTSTR lpBuffer);
一般缓冲区传递260大小就够了,因为MAX_PATH被如此定义。
BOOL SetCurrentDirectory(LPCTSTR lpPathName);
子进程不会继承父进程的当前目录,默认为根目录,如果想要继承,要在创建子进程之前,创建驱动器号的环境变量,然后将他们添加到环境块中。
c运行库的_chdir()
这个函数调用SetCurrentDirectory()
,同时还会调用SetEnvironmentVariable()
来添加和修改环境变量,从而使不同驱动器的当前目录得以保留。
进程的错误模式4.1.6
略
系统版本信息4.1.9
略
6.CreateProcess函数
BOOL CreateProcess
(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES psaProcess,
LPSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD fdwCreateFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
该函数在新进程完全初始化好之前就返回了。(书中的说明是创建新进程后和主线程后就就返回,而DLL之类的可能定位不到)
只有在创建新进程的时候他们才是父子关系,创建完毕后,这种关系就不存在了。
①lpApplicationName
lpApplicationName的存在是为了支持POSIX子系统的。一般填空。
②lpCommandLine
lpCommandLine是一个非常量字符串,在函数内部它会被修改,在函数返回时又被改回原样。传递该参数时最好是一个缓冲区。
该参数是供CreateProcess创建进程时的命令行参数,其中字符串的第一个标记被假定为可执行文件的名称,若该名称没有扩展名,默认是.exe。会依次从主调进程.exe所在的目录、主调进程当前目录、windows系统目录、windows目录、PATH环境变量中的目录查找。当然如果是绝对路径那就没问题了。
③psaProcess
④psaThread
这两个参数的类型是第三章第五节的对象安全性的那个结构,主要还是为了使用其的可继承性。
因为在父进程中会创建子进程的进程句柄和主线程句柄,作用是说明这两个句柄本身是否可被继承。
这两个句柄最终在第10参数中返回。
SECURITY_ATTRIBUTES saProcess;
saProcess.nLength = sizeof(saProcess);
saProcess.lpSecurityDescriptor = NULL;
saProcess.bInheritHandle = TRUE; // 可继承属性为TRUE
CreateProcess(..., ..., &sasaProcess ....); // 让父进程中创建的 子进程的句柄 本身可继承。
⑤bInheritHandles
这个参数是在创建子进程时,扫描父进程的句柄表,查看哪些句柄可继承,然后让子进程继承这些句柄。
⑥fdwCreateFlags
该参数按以下各个标志 或运算 组合起来。
DEBUG_PROCESS
父进程调试子进程,子进程发生某些事,要通知父进程。还有影响子进程的进程等。
DEBUF_ONLY_THIS_PROCESS
同上,但是只会影响直属子进程,而子进程的子进程则不受影响。
CREATE_SUSPENDED
创建新进程时挂起新进程的主线程,然后父进程可以修改子进程的内存、更改子进程主线程的优先级、将此进程添加到一个作业。修改完毕后调用ResumeThread()
恢复执行。
DETACHED_PROCESS
禁止基于CUI的子进程访问父进程的控制台窗口。
CREATE_NEW_CONSOLE
为子进程创建新的控制台窗口。
CREATE_NO_WINDOW
不创建任何新控制台窗口。
CREATE_NEW_PROCESS_GROUP
创建一个新的新进程组,组中的一个进程处于活动状态,一旦按下组合键ctrl+c或ctrkbreak,向这个组的进程发出通知。
CAEATE_DEFAULT_ERROR_MODE
不继承父进程的错误模式,使用默认。、
CREATE_UNICODE_ENVIRONMENT
子进程的环境块应包含Unicode字符。
CREATE_BREAKAWAY_FROM_JOB
允许一个作业中的进程生成一个和作业无关的进程。
EXTENDED_STARTUPINFO_PRESENT
表示使用第9参数使用STARTUPINFOEX结构,而不是LPSTARTUPINFO。
CREATE_SEPARATE_WOW_VDM
CREATE_SHARED_WOW_VDM
CREATE_FORCEDOS
以及还有一个优先级的标志,不过一般默认就可以。
⑦lpEnvironment
指向一块内存,包含新进程使用的环境字符串,为NULL时和父进成使用同样的环境字符串。
也可用GetEnvironmentStrings()
。
⑧lpCurrentDirectory
设置子进程的当前驱动器和目录。
如果为NULL,则与父进程的工作目录一样。
否则就需要传递一个C风格字符串,但是必须在路径中包含驱动器号。
⑨lpStartupInfo
这个结构需要清0,栈上的垃圾数据会影响。
如果不特殊说明,默认就是窗口和控制台都会影响。
typedef struct _STARTUPINFO {
DWORD cb; // 有点像版本控制,通过不同的长度来区别不同的版本,填为:sizeof(SSTARTUPINFO)。
LPTSTR lpReserved; // 保留。必须为0。
LPTSTR lpDesktop; // 标识一个名称,指明在哪个桌面上启动应用程序。
LPTSTR lpTitle; // 指明窗口标题,只影响控制台窗口,如果为NULL,可执行文件名称作为标题。
DWORD dwX; // 还需指定对应的dwFlags中的位。dwX和dwY是窗口在屏幕上的位置,单位像素。但是只有第一次创建时会用到。
DWORD dwY; // 还需指定对应的dwFlags中的位。
DWORD dwXSize; // 还需指定对应的dwFlags中的位。窗口的高度宽度,单位像素。只有第一次调用CreateWindow且CW_USEDEFAULT作为参数时才会用到。
DWORD dwYSize; // 还需指定对应的dwFlags中的位。
DWORD dwXCountChars; // 还需指定对应的dwFlags中的位。只影响控制台,控制台窗口的高度宽度,单位字符。
DWORD dwYCountChars; // 还需指定对应的dwFlags中的位。
DWORD dwFillAttribute; // 还需指定对应的dwFlags中的位。只影响控制台,指定子进程控制台窗口的文本和背景色
DWORD dwFlags; // 详见后。
WORD wShowWindow; // 还需指定对应的dwFlags中的位。只影响窗口,第一次调用ShowWindow()调用,使用该成员的值,同时忽略ShowWindow()的nCmdShow参数。
// 之后调用ShowWindow(),只有将SW_SHOWDEAFULT作为参数是,才使用该成员,否则使用该参数。
WORD cbReserved2; // 保留。必须为0。
LPBYTE lpReserved2; // 保留。必须为0。
HANDLE hStdInput; // 还需指定对应的dwFlags中的位。只影响控制台,指定到标准输入的句柄。
HANDLE hStdOutput; // 还需指定对应的dwFlags中的位。只影响控制台,指定到标准输出的句柄。
HANDLE hStdError; // 还需指定对应的dwFlags中的位。只影响控制台,指定到标准错误的句柄。
} STARTUPINFO, *LPSTARTUPINFO;
成员dwFlags
以下各个标志的 或运算。
STARTF_USEPOSITION 使成员dwX、dwY可用
STARTF_USESZIE 使成员dwXSize、dwYSize可用
STARTF_USECOUNTCHARS 使成员dwXCountChars、dwYCountChars可用
STARTF_USEFILLATTRIBUTE 使成员dwFillAttribute可用
STARTF_USESHOWWINDOW 使成员wShowWindow可用
STARTF_USESTDHANDLES 使成员hStdInput、hStdOutput、hStdError可用
STARTF_RUNFULLSCREEN 控制台程序全屏运行
⑩lpProcessInformation
该参数指向的类型是PROCESS_INFORMATION
。
该结构应该由使用者提供,而函数则在返回之前,将各个信息填入(详见下定义)。
要注意在父进程中closehandle这两个句柄,否则可能泄露。
子进程和其主线程的id绝不会重复,而且如果这些个id被收回,可能会被立即重用。
GetCurrentProcessId()
获取当前进程ID
GetCurrentThreadId()
获取当前线程ID
GetThread()
获得与句柄关联的线程ID
GetProcessIdOfThread()
根据线程句柄获取其进程ID
struct PROCESS_INFORMATION
{
HANDLE hProcess; // 子进程句柄
HANDLE hThread; // 子进程主线程句柄
DWORD dwProcessId; // 子进程id
DWORD dwThreadId; // 子进程主线程id
};
7.进程终止
进程最稳妥的结束方式就是从主线程的入口点函数返回。
当从入口点函数返回,C++运行库调用ExitProcess
终止进程,然后其他线程可能会不正常结束。
ExitProcess
也可在程序中主动调用,当然最后不这么做。
当在入口点函数调用ExitThread
,主线程结束,但是其他线程不会结束,只有当所有线程结束后,整个进程才会终止。
而进程的退出代码被设为最后终止的那个线程的退出代码。
调用GetExitCodeProcess获得一个已终止的进程的退出代码。
7.管理员权限
①大概是当登录windows系统时,一般都是以管理员的身份。
②但是程序运行时却不是直接以管理员权限,而是通过一个被阉割后的权限运行,书中称之为 筛选后的令牌,只有标准用户的权限。
③当一个进程启动,会与这个被筛选后的令牌权限关联。一旦程序启动,那么这个权限就不可改了,想要提升权限,就只能在程序启动时做文章。
提升权限的两种方式:
①自动
在应用的可执行文件中嵌入了资源RT_MANIFEST,然后它是一个XML文件,解析其中的<trustInfo>字段。
②手动
通过函数ShellExecuteEx(LPSHELLEXECUTEINFO pInfo);
关于这两种方式操作的细节书中没有提到,记个概念吧。
windows完整性机制:
有低、中、高、系统四个等级。
通过在系统访问控制列表增加一个名为强制标签的访问控制项,为每个受保护的资源分配一个完整性级别,如果没有分配。默认其为中等级。
当代码试图访问某个内核对象,系统将主调进程的完整性级别与内核对象的完整性级别比较,若后者高于前者,就会拒绝删除和修改操作。这个行为是在访问控制列表之前进行的,也就是说,进程拥有对齐的访问权限,却又因为完整性级别低而被拒绝。
但是如果有DEBUG权限,那么低等级的完整性也能访问高等级的完整性。
2021年1月14日18:11:39
第五章 作业
这一章里面全是关于对作业操作的讲解,诸如各个函数如何使用,参数如何填写等。
①作业像一个容器,容纳一组进程。通过作业可以对一组进程来施加一些限制。
②通过作业的对象句柄来操纵作业。
③如果一个进程已与一个作业关联,就没法将他移除,这是出于安全性考虑。
④关闭作业句柄不会使作业对象消失,只是为对象本身添加了标记,作业中的所有进程终止后自动销毁。但是关闭后却不能再打开了。
⑤作业中的进程生成了另一个进程,新进程自动属于该作业。但是这种默认行为是可以改变的,通过本章第六节的SetInformationJobObject
函数,使用结构JOBOBJECT_BASIC_LIMIT_INFORMATION。
将其LimitFlags成员添加以下任一标志:
JOB_OBJECT_LIMIT_BREAKAWAY_OK,新进程可在作业外部,但是还需再CreatProcess时再一次指定该标志。
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK,强制新进程脱离作业。
1.判断一个进程是否在某个作业下:
BOOL IsProcessInJob(HANDLE hProcess, HANDLE hJob, PBOOL pbInJob);
2.创建一个作业对象,返回句柄:
HANDLE CreateJobObject(PSECURITY_ATTRIBUTES psa,PCTSTR pszNmae)
第一个参数是安全性对象,主要还是决定句柄是否可以被继承。第二个参数是为对象命名。
3.打开一个作业对象,返回句柄:
HANDLE OpenJobObject(DWORD dwDesiredAccess, BOOL bInheritHanlde, PCTSTR pszName);
4.把进程加入作业:
BOOL AssiagnProcessToJobObject(HANLDE hJob, HANDLE hProcess);
5.终止作业中所有进程
BOOL TerminateJobObject(HANDLE hJob, UINT uExitCode);
所有进程的退出代码被设为第二参数。
6.对作业添加限制 & 查询关于作业的一些信息
👇👇👇查询用到的函数👇👇👇
BOOL QueryInformationJobObject(
HANDLE hJob,
JOBOBJECTINFOCLASS JobObjectInformationClass,
LPVOID lpJobObjectInformation,
DWORD cbJobObjectInformationLength,
LPDWORD lpReturnLength // 指出返回值有多少字节
);
👇👇👇添加限制用到的函数👇👇👇
BOOL SetInformationJobObject(
HANDLE hJob,
JOBOBJECTINFOCLASS JobObjectInformationClass,
LPVOID lpJobObjectInformation,
DWORD cbJobObjectInformationLength
);
这两个函数原型很相似,一起说明。
第一参数就是要操作的作
第二参数和第三参数是相关联的,根据第二参数值不同,第三参数则需要不同的结构。
第四参数是第三参数结构体的大小。
关于下表中,前四行结构体的详述见后。
第二参数值 | 第三参数需要的结构 | 说明 |
---|---|---|
JobObjectBasicLimitInformation | JOBOBJECT_BASIC_LIMIT_INFORMATION | 一些基本限制,诸如分配的时间、 |
JobObjectExtendLimitInformation | JOBOBJECT_EXTENDED_LIMIT_INFORMATION | 关于存储空间 |
JobObjectBasicUIRestrictions | JOBOBJECT_BASIC_UI_RESTRICTIONS | 关机、电源、剪切板、修改系统参数、桌面、阻止访问作业外进程创建的内核对象等 |
JobObjectSecurityLimitInformation | JOBOBJECT_BASIC_SECURITY_LIMIT_INFORMATION | 关于安全、权限的操作 |
JobObjectBasicAccountingInformation | JOBOBJECT_BASIC_ACCOUNTING_INFORMATION | 查询作业统计信息的结构 |
JobObjectBasicAndIoAccountingInformation | JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION | 上一行的结构+查询io读写、读写次数 |
JobjectAssociateCompletionPortInfomation | JOBOBJECT_ASSOCIATE_COMPLETION_PORT | iocp,见本章第七节作业通知 |
结构JOBOBJECT_BASIC_LIMIT_INFORMATION
typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION {
LARGE_INTEGER PerProcessUserTimeLimit; // 分配给每个 !!进程的!! 最大用户模式时间,超时会终止运行。
// 须在LimitFlags成员指定JOB_OBJECT_LIMIT_PROCESS_TIME标志
LARGE_INTEGER PerJobUserTimeLimit; // 分配给 !!作业的!! 最大用户模式时间,超时会终止运行。设置时会扣除已终止运行的进程的CPU时间统计信息
// 如果想设置这个,又不想信息被清除,使用标志JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME
// 须在LimitFlags成员指定JOB_OBJECT_LIMIT_JOB_TIME标志,这两个标志是互斥的
DWORD LimitFlags; // 各个标志或运算
SIZE_T MinimumWorkingSetSize; // 最小工作集
SIZE_T MaximumWorkingSetSize; // 最大工作集,当进程抵达限额,会执行换页操作
// 这两个成员须在LimitFlags成员指定JOB_OBJECT_LIMIT_WORKINGSET标志
DWORD ActiveProcessLimit; // 作业中能并发运行的进程最大数量,超出的新进程被终止
// 须在LimitFlags成员指定JOB_OBJECT_LIMIT_ACTIVE_PROCESS标志
ULONG_PTR Affinity; // 指定在哪些CPU上运行
// 须在LimitFlags成员指定JOB_OBJECT_LIMIT_AFFINITY标志
DWORD PriorityClass; // 指定作业中所有进程的优先级
// 须在LimitFlags成员指定JOB_OBJECT_LIMIT_PRIORITY标志
DWORD SchedulingClass; // 指定作业中 !!线程!! 的相对时间量差,0~9,默认5,值越大,分配的时间越长
// 须在LimitFlags成员指定JOB_OBJECT_LIMIT_SCHEDULING_CLASS标志
} JOBOBJECT_BASIC_LIMIT_INFORMATION, *PJOBOBJECT_BASIC_LIMIT_INFORMATION;
结构JOBOBJECT_EXTENDED_LIMIT_INFORMATION
typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
IO_COUNTERS IoInfo; // 保留成员
SIZE_T ProcessMemoryLimit; // 每个进程最大存储峰值
SIZE_T JobMemoryLimit; // 所有进程最大存储峰值
SIZE_T PeakProcessMemoryUsed; // 只读,已调拨给作业中任一进程所需存储空间峰值
SIZE_T PeakJobMemoryUsed; // 只读,已调拨给作业中全部进程所需存储空间峰值
} JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION;
结构JOBOBJECT_BASIC_UI_RESTRICTIONS
typedef struct _JOBOBJECT_BASIC_UI_RESTRICTIONS {
DWORD UIRestrictionsClass;
} JOBOBJECT_BASIC_UI_RESTRICTIONS, *PJOBOBJECT_BASIC_UI_RESTRICTIONS;
仅有的成员是按位或操作,有以下标志
标志 | 描述 |
---|---|
JOB_OBJECT_UILIMIT_EXITWINDOWS | 防止进程通过ExitWindowsEx函数退出、关闭、重启或关闭系统电源 |
JOB_OBJECT_UILIMIT_READCLIPBOARD | 阻止读剪切板 |
JOB_OBJECT_UILIMIT_WRITECLIPBOARD | 阻止写剪切板 |
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS | 阻止调用SystemParameterInfo修改系统参数 |
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS | 阻止调用ChangerDisplaySettings更改显示设置 |
JOB_OBJECT_UILIMIT_GLOBALATOMS | 防止进程访问全局的基本结构表,为作业分配自己的基本结构表,作业中进程只能访问该表 |
JOB_OBJECT_UILIMIT_DESKTOP | 阻止调用CreateDesktop和SwitchDesktop创建和切换桌面 |
JOB_OBJECT_UILIMIT_DESKTOP | 防止进程使用作业外部的进程创建的用户对象的句柄,单向限制,外部可以访问内部 |
结构JOBOBJECT_BASIC_SECURITY_LIMIT_INFORMATION
typedef struct _JOBOBJECT_SECURITY_LIMIT_INFORMATION {
DWORD SecurityLimitFlags; // 应该是一个按位或的,标志书中没有提到,也许官方文档有吧
HANDLE JobToken; // 作业中所有进程使用的访问令牌
PTOKEN_GROUPS SidsToDisable; // 禁止对哪些SID检查
PTOKEN_PRIVILEGES PrivilegesToDelete; // 从访问令牌中删除特权
PTOKEN_GROUPS RestrictedSids; // 指定一组只能拒绝的SID
} JOBOBJECT_SECURITY_LIMIT_INFORMATION, *PJOBOBJECT_SECURITY_LIMIT_INFORMATION;
结构JOBOBJECT_BASIC_ACCOUNTING_INFORMATION
typedef struct _JOBOBJECT_BASIC_ACCOUNTING_INFORMATION {
LARGE_INTEGER TotalUserTime; // 指出作业中的进程使用了多少 !!用户!! 模式CPU时间
LARGE_INTEGER TotalKernelTime; // 指出作业中的进程使用了多少 !!内核!! 模式CPU时间
LARGE_INTEGER ThisPeriodTotalUserTime; // 和第一个成员一样,区别看结构JOBOBJECT_BASIC_LIMIT_INFORMATION的第二成员PerJobUserTimeLimit,这就是那个会被清除为0的信息
LARGE_INTEGER ThisPeriodTotalKernelTime; // 和第二个成员一样,区别同上
DWORD TotalPageFaultCount; // 进程产生的页面错误总数
DWORD TotalProcesses; // 从作业产生到目前为止,所有的进程总数
DWORD ActiveProcesses; // 当前或者的进程数
DWORD TotalTerminatedProcesses; // 因超出CPU时间限额被杀死的进程数
} JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, *PJOBOBJECT_BASIC_ACCOUNTING_INFORMATION;
结构JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION
typedef struct _JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION {
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION BasicInfo;
IO_COUNTERS IoInfo;
} JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION, *PJOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION;
typedef struct _IO_COUNTERS {
ULONGLONG ReadOperationCount; // 读操作次数
ULONGLONG WriteOperationCount; // 写操作次数
ULONGLONG OtherOperationCount; // 读写操作除外的IO操作次数
ULONGLONG ReadTransferCount; // 读的字节数
ULONGLONG WriteTransferCount; // 写的字节数
ULONGLONG OtherTransferCount; // 读写操作外传输的字节数
} IO_COUNTERS;
7.作业通知
作业对象的触发状态: 作业用完了分配的时间,所有进程被杀死,对象变为触发状态。
通过WaitForSingleObject函数来捕捉这个事件。
还可通过SetInformationJobObject
来重新设置事件,将其变为未触发状态。
然后是如何获取更高级的通知,通过IO完成端口(IOCP):
①创建IO完成端口
②通过调用SetInformationJobObject
将其和作业关联
③当事件发生,被投递到IOCP
④调用QueryInformationJobObject
或GetQueuedCompletionStatus
来获取相关信息
一个例子:
JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp;
joacp.CompletionKey = 1; // 任意一个唯一的标识符
joacp.CompletionProt = hIOCP; // 其中hIOCP是一个IOCP的句柄
SetInformationJobObject(hJob, JobjectAssociateCompletionPortInfomation, &joacp, sizeof(joacp));
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, // 查询的IOCP句柄
LPDWORD lpNumberOfBytesTransferred, // 书中:是按位或的标志,不同标志代表不同事件,另有一张表说明各个标志含义
// 关于各个事件,贴个官方文档链接吧,实在不想抄书了(其中一个值JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)
// https://docs.microsoft/en-us/windows/win32/api/winnt/ns-winnt-jobobject_associate_completion_port
PULONG_PTR lpCompletionKey, // 应该是joacp.CompletionKey这个值,用于区分是哪个作业
LPOVERLAPPED *lpOverlapped, // 根据事件不同,返回的类型也不同
DWORD dwMilliseconds // 该函数调用等待的事件,超时返回FALSE,lpOverlapped设为NULL,为0时立即返回
);
注:当作业的用完了分配的时间,那么所有进程被杀死,通知JOB_OBJECT_MSG_END_OF_JOB_TIME也不会被投递到IOCP,如果想要得到这个通知,那么需要:
...
joacp.EndOfJobTimeAction = JOB_OBJECT_POST_AT_END_OF_JOB;
SetInfor...
...
2021年1月17日19:41:58
第六章 线程
接下来几章都是关于线程的,这个就不详细写了,挑一些没见过的概念写一下。
而且,标准库的线程简单易用,windows原生的各种注意事项。
一个线程由一个内核对象和线程栈,后者维护线程执行时所需的所有函数参数和局部变量。
每个线程都有一组自己的CPU寄存器,称为线程上下文。
在线程对象中,被启动的线程函数会被包裹一层放在某个函数内调用,包裹的这层处理线程的退出和线程产生的异常。
HANDLE GetCurrentProcess();
// 返回当前进程句柄
HANDLE GetCurrentThread();
// 返回当前线程句柄
需要注意的是,这两个函数返回的都是 伪句柄,他们不会在句柄表中创建,关闭他们也只是得到返回值 :无效句柄
还有一个问题,假如在不同进程间传递伪句柄,那么在不同的线程中,他们都是指向当前线程句柄的伪句柄,所以不应该传递他们,直接使用即可。
可以将伪句柄转换为真正的句柄,使用DuplicateHandle
即可,第三章第八节。
第七章 线程的上下文、调度、优先级、关联性
有些线程是不可调度的,比如被挂起。
饥饿线程:因为优先级低长时间未得到调度(这个长时间大概是3~4秒?)。
1.线程挂起与恢复
线程的挂起由一个挂起计数控制,大于0时即被挂起,当被多次挂起,那么也需要多次恢复。
线程可以自己挂起自己,但是无法自己恢复。
挂起线程时,线程可能正在执行系统调用,即在内核态,线程没有被立即挂起,而是执行完系统调用回到用户态时才会挂起。
挂起函数:DWORD SuspendThread(HANDLE hThread);
返回线程 前一个挂起次数,最多被挂起MAXIMUM_SUSPEND_COUNT(127)次。
恢复函数:DWORD ResumeThread(HANDLE hThread);
成功时返回线程的 前一个挂起次数,否则返回0xFFFFFFFF。
2.线程睡眠和切换
VOID Sleep(DOWRD dwMilliseconds);
使线程资源放弃当前时间片的剩余部分
如果传入参数INFINITE,表示该线程永远不会调度,没什么用。
传入0表示放弃剩余的时间片,由系统调度其他线程。
BOOL SwitchToThread();
和传入0的Sleep函数相似,区别是该函数会优先饥饿线程和低优先级线程。
3.线程的执行时间
线程的调度很玄,如果只是简单的取两个时间点相减计算执行时间,而取两个时间点之间线程有可能被调度,这个时间就不准确了。
BOOL GetThreadTimes(
HANDLE hThread, // 标识线程的句柄
PFILETIME pftCreationTime, // 线程的创建时间,100ns为单位,从1601年1月1日开始计算
PFILETIME pftExitTime, // 线程的退出时间,100ns为单位,从1601年1月1日开始计算
PFILETIME pftKernelTime, // 线程的内核态执行时间,100ns为单位
PFILETIME pftUserTime); // 线程的用户态执行时间,100ns为单位
对应的还有一个进程的执行时间,是进程中所有线程的累计
BOOL GetProcessTimes(
HANDLE hProcess,
PFILETIME pftCreationTime,
PFILETIME pftExitTime,
PFILETIME pftKernelTime,
PFILETIME pftUserTime);
4.CONTEXT结构
每个线程都有一个上下文,是一个CONTEXT结构,该结构保存在线程的内核对象中,存储着线程上一次执行时CPU寄存器的状态。下一次调度时,把CONTEXT中的值载入CPU寄存器。
该结构是特定于CPU的。
获取/设置线程的CONTEXT结构
BOOL GetThreadContext(HANDLE hThread, PCONTEXT pContext);
BOOL SetThreadContext(HANDLE hThread, CONST CONTEXT * pContext);
调用这两个函数前,应该先挂起线程,否则可能那个线程正在调度,此时获取,就可能出现上下文不一致。
关于这两个函数,在设置或获取前,需要设置对应的CONTEXT.ContextFlags成员。
比如:Context.ContextFlags = CONTEXT_CONTROL;
CONTEXT_CONTROL 包含CPU的控制寄存器,包含指令指针、栈指针、标志、函数返回地址
CONTEXT_INTEGER 标识CPU的整数寄存器
CONTEXT_FLOATING_POINT 标识CPU的浮点寄存器
CONTEXT_SEGMENTS 标识CPU的端寄存器
CONTEXT_DEBUG_REGISTERS 标识CPU的调试寄存器
CONTEXT_EXTENDED_REGISTERS 标识CPU的扩展寄存器
5.线程优先级
线程分为0~31优先级,31最高。
优先级高的进程会抢占低优先级进程,即打断低优先级进程的执行,取而代之。
优先级0的线程只有一个,称为页面清零线程,在系统中没有进程需要执行时,清零所有闲置页面。
一般来说,较高优先级线程都是不可调度的,大多在等待事件,高优先级是为了让这些线程尽快得到CPU时间。
进程定义了六个优先级类(见后)。
线程定义了七个优先级(见后)。
这个6X7组合起来,分布于线程定义的0~31个优先级。
基本优先级:进程优先级类加上线程优先级的值。
设置/获取 进程/线程 的优先级
BOOL SetPriorityClass(HANDLE hProcess, DWORD fdwPriority);
//第二参数填随后的符号常数
DWORD GetPriorityClass(HANDLE hProcess);
BOOL SetThreadPriority(HANDLE hThread, int nPriority);
int GetThreadPriority(HANDLE hThread);
动态提升优先级:有时系统会自动给一些线程/进程(优先级低于15的)提升优先级,然后优先级经过几次调度后慢慢掉落回基本优先级。
关于是否开启这个行为的函数:
BOOL SetProcessPriorityBoost(HANDLE hProcess, BOOL bDisablePriorityBoos);
BOOL SeThreadPriorityBoost(HANDLE hThread, BOOL bDisablePriorityBoos);
BOOL GetProcessPriorityBoost(HANDLE hProcess, PBOOL bDisablePriorityBoos));
BOOL GetThreadPriorityBoost(HANDLE hThread, PBOOL bDisablePriorityBoos));
进程的六个优先级类的 符号 & 描述:
优先级名称 | 符号常数 | 描述 |
---|---|---|
real-time | REALTIME_PRIORITY_CLASS | 最好不使用,该优先级会立即响应,抢占操作系统其他组件CPU时间 |
high | HIGH_PRIORITY_CLASS | 必须立即相应,任务管理器运行在这一级 |
above normal | ABOVE_NORMAL_PRIORITY_CLASS | 高于标准级别 |
normal | NORMAL_PRIORITY_CLASS | 正常级别 |
below normal | BELOW_NORMAL_PRIORITY_CLASS | 低于标准级别 |
idle | IDLE_PRIORITY_CLASS | 空闲时运行,屏幕保护程序等 |
线程的七个优先级的 符号 & 描述:
优先级名称 | 符号常数 | 描述 |
---|---|---|
time-critical | THREAD_PRIORITY_TIME_CAITICAL | 该级别对上进程的最高级类,是31的优先级,对上进程的其他等级类,优先级都是15 |
highest | THREAD_PRIORITY_HIGHEST | 高于正常两个级别 |
above normal | THREAD_PRIORITY_ABOVE_NORMAL | 高于正常一个级别 |
normal | THREAD_PRIORITY_ NORMAL | 正常级别 |
below normal | THREAD_PRIORITY_BELOW_NORMAL | 低于正常一个级别 |
lowest | THREAD_PRIORITY_LOWEST | 低于正常两个级别 |
idle | THREAD_PRIORITY_IDLE | 对上进程的最高级类,是16的优先级,对上进程的其他等级类,优先级都是1 |
6.线程/进程 指定运行于某个CPU子集
让线程始终运行在同一个处理器有助于重用高速缓存中的数据。
关于这个函数不想写了,好累,记得以前用过其中某个,但是书中说的mask填写方法和印象里有点不一样,以后再说吧。
BOOL SetProcessAffinityMask(HANDLE hProcess, DWORD_PTR dwProcessAffinityMask);
BOOL GetProcessAffinityMask(HANDLE hProcess,PDWORD_PTR lpProcessAffinityMask, PDWORD_PTR lpSystemAffinityMask);
DWORD_PTR SetThreadAffinityMask(HANDLE hThread, DWORD_PTR dwThreadAffinityMask);
2021年1月18日23:54:22
第八章 第九章 线程同步
这两章都是关于线程同步的东西。
区别在于,第八章是用户态的同步,第九章是是使用内核对象在内核态的同步。当然后者慢。
这里还是不写了,关于线程同步,还是使用C++标准库,见另一篇博客.。
以下是书中一些以前没见过的概念,主要是高速缓存行、对象的触发状态等。
1.高速缓存行
当CPU从内存中取一个字节,不仅仅取了一个字节,而是取一行的字节,被取的那一个字节包含在这一行中。
这里的一行可能是32字节,可能是64字节,可能是128字节。
一般来说应用程序可能会对一组相邻字节操作,此举是为了提高效率而不必多次读内存。
当高速缓存行遇上多线程,就可能发生问题:
两个CPU执行不同的线程,两条线程都读了同一块内存,那么两个CPU的高速缓存行中,都会存储同样的数据,此时若是其中一个线程修改了缓存行的数据,那么另一个线程就会收到通知,这个缓存行失效,然后第一个线程的CPU需要将数据写回内存,第二个线程的CPU重新到内存读,以此来解决不一致的情况。当然这导致了效率的下降。
2.内核对象的 触发 & 未触发 状态
首先,内核对象要么触发,要么未触发,一般情况下,都是未触发状态,当发生某些事件时,有未触发状态转为触发状态。
不同的对象触发条件不同,比如进程内核对象的触发条件是进程终止,作业对象的触发条件是作业超时,互斥量的触发条件是不被线程占用等。
书中给了一张表说明各个对象的触发条件,以及本章大部分内容都是使用这些对象来实现同步操作。
这里并不打算写任何细节,因为std中有更简单易用且灵活性更大的东西代替。
一言概之:调用系统提供的两个等待函数之一,传入句柄标识一个内核对象,以及一个愿意等待的时间,因为不同类型的内核对象有不同的触发方式,所以灵活性很大,调用这个函数线程被阻塞挂起,当内核对象变成触发状态,那么意味某个事件发生,线程被唤醒,继续执行。
3.一个觉得会有用的函数
DWORD WaitForInputIdle(HANDLE hProcess, DWORD dwMilliseconds);
调用该函数的线程导致自身被挂起,等待hProcess标识的进程,创建应用程序第一个窗口的线程没有待处理输入为止。
作用:父进程需要子进程创建窗口的句柄,而知道子进程初始化完毕的方法,就是等待子进程不处理任何输入。在CreateProcess后调用该函数。
2021年1月20日21:28:56
第十章 同步设备与异步设备的IO
首先是设备,这里的设备大概可以直接理解为句柄,套接字也可以直接类型转换成句柄。
而winodws通过句柄这一存在,以相同的方式读写不同类型的设备。
1.打开/关闭设备
打开设备,来一张书上的图,该图描述了如何打开不同的设备,然后取得其句柄。
关闭设备则是调用CloseHandle
,如果是socket,则需要对其调用closesocket(SOCKET s)
查询设备类型:DWORD GetFileType(HANDLE hDevice);
返回值是以下四个之一。
FILE_TYPE_UNKNOWN 未知类型
FILE_TYPE_DISK 磁盘文件
FILE_TYPE_CHAR 字符文件、并口、控制台
FILE_TYPE_PIPE 命名、匿名管道
关于CreateFile函数详细见下节。
2.CreateFile函数详细
// 函数名为创建文件,实际上是打开文件,创建一个对应的内核对象,返回该对象句柄
HANDLE WINAPI CreateFile(
LPCTSTR lpFileName, // 既可表示设备类型,也可表示某个设备实例,具体文件
DWORD dwDesiredAccess, // 读打开、写打开、还是读写打开
DWORD dwShareMode, // 共享模式,是否允许和其他地方共享读或者共享写,还是独占的读写
LPSECURITY_ATTRIBUTES psa, // 安全属性,主要还是使用那个是否可继承的成员,见第三章
DWORD dwCreationDisposition, // 要打开的东西,存在还是不存在,存在如何操作,不存在又该如何操作
DWORD dwFlagsAndAttributes, // 一些标志,主要内容有:缓存相关:大小写区分,关闭时删除、异步访问、以及文件的只读等属性
HANDLE hTemplateFile // 和倒数第二个参数作用一样,不过是按该参数的句柄的属性一模一样的方式打开新的设备。
);
句柄是否有效
一般情况下都是判断NULL,但是CreateFile是:handle != INVALID_HANDLE_VALUE
第二参数:dwDesiredAccess:
该参数有以下四个值:
0 不读也不写,只修改其一些配置,时间戳之类的
GENERIC_READ 读
GENERIC_WRITE 写
GENERIC_READ | GENERIC_WRITE 读写
GENERIC_EXECUTE 执行
第三参数:dwShareMode:
当一个设备未被CloseHandle前,又被调用CreateFile。
也就是说多次打开时以何种方式共享。
0
要求独占访问,如果设备已被打开,CreateFile失败。打开成功,后续CreateFile会失败。
FILE_SHARE_READ
不允许修改设备,只许读。如果设备已经使用写共享的模式打开,则会失败。反之亦然,成功打开导致后续写共享模式打开失败。
FILE_SHARE_WRITE
FILE_SHARE_READ | FILE_SHARE_WRITE
这两个就不写了,总之读和写是互斥的。
FILE_SHARE_DELETE
这里表示不关心设备被移动或删除。在所有句柄被关闭时,将导致设备被删除。
第五参数:dwCreationDisposition:
注:使用CreateFile打开文件之外的其他设备,必须传递OPEN_EXISTING
CREATE_NEW 创建新文件,若同名文件存在,则失败
CREATE_ALWAYS 无论如何都会创建新文件,覆盖原来的
OPEN_EXISTING 打开一个已存在的文件或设备,如果不存在,失败
OPEN_ALWAYS 打开已有文件,不存在或创建
TRUNCATE_EXISTING 打开已存在文件并截断,不存在会失败
第七参数:hTemplateFile :
这里先写第七参数,最后再写第六参数。
如果该参数不为空,那么第六参数就被忽略。
根据第七参数指向的句柄的文件属性,来设置新打开设备的属性。
该参数指向的句柄需要使用GENERIC_READ标志打开。
第六参数:dwFlagsAndAttributes:
FILE_FLAG_OVERLAPPED
最重要的标志放在第一个,异步访问设备。
FILE_FLAG_NO_BUFFERING
不使用缓存,我们会自己进行缓存。
但是有使用限制:①访问时偏移量需要正好是扇区大小整数倍②读写也需要是整数倍③缓存在进程地址空间的起始地址也需要是整数倍
GetDiskFreeSpace
获取扇区大小。
导致忽略另外两个标志:FILE_FLAG_SEQUENTIAL_SCAN、FILE_FLAG_RANDOM_ACCESS
文件过大导致无法打开文件,使用该标志打开文件。
FILE_FLAG_SEQUENTIAL_SCAN
导致系统认为我们将顺序访问文件,然后实际读取量会多余需求读取量。
FILE_FLAG_RANDOM_ACCESS
告诉系统不要提前读取。
FILE_FLAG_WRITE_THROUGH
直接将文件写入磁盘而不是通过内存中转。
FILE_FLAG_DELETE_ON_CLOSE
句柄都关闭后删除文件,可以和下一个标志配合来处理临时文件。
FILE_ATTRIBUTE_TEMPORARY
临时文件,尽可能将其保存在内存而不是磁盘中。
FILE_FLAG_BACKUP_SEMANTICS
指定该标志导致检查调用者的存储令牌是否对文件和目录进行备份/恢复特权。(看不懂)
FILE_ATTRIBUTE_ARCHIVE
创建新文件时自动设置该标志,将文件标记为带备份或待删除。
FILE_ATTRIBUTE_ENCRYPTED
文件经过加密。
FILE_ATTRIBUTE_HIDDEN
文件是隐藏的。
FILE_ATTRIBUTE_NORMAL
单独使用才有效,表示无其他属性。
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED
内容索引服务不会索引该文件。
FILE_ATTRIBUTE_OFFLINE
文件虽存在,但已被转移到脱机存储器。
FILE_ATTRIBUTE_READONLY
只读。
FILE_ATTRIBUTE_SYSTEM
系统文件,专供操作系统使用。
以下几个比较偏或者不推荐用。
FILE_FLAG_POSIX_SEMANTICS
FILE_FLAG_OPEN_REPARSE_POINT
FILE_FLAG_OPEN_NO_RECALL
3.关于文件的一些操作
获取文件大小
BOOL GetFileSizeEx(HANDLE hFile, PLARGE_INTEGER pLiFileSize);
DWORD GetCompressedFileSizeA(LPCSTR lpFileName, LPDWORD lpFileSizeHigh);
第一个函数通过句柄找到文件,通过第二参数返回。
第二个函数通过字符串来寻找文件,第二参数返回的是物理大小,而不是逻辑大小。
物理大小是磁盘实际占用大小(可能被压缩存储)。
// 这是一个有符号类型的,还有一个无符号类型结构相似的类型
typedef union _LARGE_INTEGER { // 表示一个64位值
struct {
DWORD LowPart; // 低32位
LONG HighPart; // 高32位
};
LONGLONG QuadPart; // 联合体的另一个成员,64位
} LARGE_INTEGER, *PLARGE_INTEGER;
typedef union _ULARGE_INTEGER {
struct {
DWORD LowPart;
DWORD HighPart;
} DUMMYSTRUCTNAME;
ULONGLONG QuadPart;
} ULARGE_INTEGER;
文件指针
关于文件指针的操作不详细写了,写一个概念。
每个内核对象中都会有一个文件指针,指向当前读到哪了。
如果多次打开创建多个内核对象,那么各个文件指针是互不影响的。
如果多个句柄共享同一个对象,那么文件指针只有一个,是相互影响的。
4.同步和异步的IO操作
同步操作直接用ReadFile
或者WriteFile
,不过参数有点多,c库的也行。
同步方式调用这两个函数时,成功会返回非0值。
然后如果想要取消同步IO的阻塞,调用:BOOL WINAPI CancelSynchronousIo(HANDLE hThread);
BOOL ReadFile(
HANDLE hFile, // 文件的句柄
LPVOID lpBuffer, // 用于保存读入数据的一个缓冲区
DWORD nNumberOfBytesToRead, // 要读入的字节数
LPDWORD lpNumberOfBytesRead, // 返回指向实际读取字节数的指针
LPOVERLAPPED lpOverlapped // 异步读取时用到的参数,否则为空
);
接下来就是最重要的异步了
这里以IOCP为主线写下去。
先说明OVERLAPPED结构
typedef struct _OVERLAPPED {
DWORD Internal; // 输出参数,发出异步请求时,该值立刻被设为STATUS_PENDING,该值表示没有错误
DWORD InternalHigh; // 输出参数,IO请求完成后,该值被设为已传输的字节数。
DWORD Offset; // 输入参数,低32位,表示从哪里开始IO操作,非文件设备会忽略该参数
DWORD OffsetHigh; // 输入参数,高32位,表示从哪里开始IO操作,非文件设备会忽略该参数
HANDLE hEvent; // 输入参数,通过IOCP接收IO通知时用到该参数。
} OVERLAPPED;
①在CreateFile时,需要指定FILE_FLAG_OVERLAPPED(异步)。在对这个句柄读写时(ReadFile
或者WriteFile
),会检查该标志。
注一:异步方式调用这两个函数时,返回FALSE,GetLastError
得到ERROR_IO_PENDING,表示IO请求已经成功添加了,至于什么时候完成就是另一回事了。如果GetLastError
返回其他值,就说明IO请求失败了。
注二:每个IO请求都需要一个OVERLAPPED结构,然后在IO请求完成之前,这个结构不能移动或者销毁。
注三:如果要取消发出的IO请求,算了不写了,P296。/
②通过①中的描述,发送出异步IO请求后,有四种方法接收IO通知。接下来简述这四种方法,主要写一下第四种IOCP。
触发设备内核对象
没什么用
触发事件内核对象
可以向一个设备同时发出多个IO请求,一个线程请求,一个线程处理。
使用可提醒IO
可以向一个设备同时发出多个IO请求,哪个线程发出,哪个线程处理。
使用IOCP
可以向一个设备同时发出多个IO请求,一个线程请求,一个线程处理。
③创建IOCP和关闭
因为IOCP也是句柄,所以直接CloseHandle就行了。
HANDLE WINAPI CreateIoCompletionPort(
HANDLE FileHandle, // 要关联到IOCP的设备
HANDLE ExistingCompletionPort, // 是一个已经创建的IOCP端口,
ULONG_PTR CompletionKey, // 这个值是自定义的,大概就是用来标识参数1
DWORD NumberOfConcurrentThreads // 会创建一些线程来和IOCP关联,该参数表示最多同时可以运行的线程数,为0就是默认,CPU核心数量,详细作用见后
);
这个函数完成两个任务:一是创建IOCP对象,二是将一个设备和IOCP关联起来。
// 第一次调用,创建IOCP
HANDLE hCompletion = CreateIoCompletionPort(INVALID_HANDLE_VALUE,0,0,0) ;
// 第二次调用,这里将一个socket关联到IOCP上
SOCKET s; // 假定这个socket已经初始化好
CreateIoCompletionPort((HANDLE)s,hCompletion,(DWORD)s,0);
// 启动一个和IOCP关联的线程
// threadfun函数中,则是调用GetQueuedCompletionStatus来取得已经完成的IO请求
CreateThread(NULL,0,threadfun,(LPVOID)hCompletion,0,0) ;
接下来是创建IOCP时,对应的五个相关联的数据结构
设备列表:其中每一项都是<hDeivce:CompletionKey>这样的对,代表一个关联的设备和其标识符
IO完成队列(先进先出):每一项都是<已传输字节数:CompletionKey:调用read、wirte是传递的OVERLAPPED:错误码>
当一个设备异步IO完成,同时其和一个IOCP关联,那么就会把这些信息放入到这个队列。
等待线程队列 | 释放线程列表 | 暂停线程列表
烦死了烦死了,不写了,完全不知道自己写的这些有什么用,还不如趁寒假学学qt,也许得换换思路了,不纠结这些细节了,之后大概是着重看一些win的概念吧。
④获取已经完成的IO请求GetQueuedCompletionStatus()
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort, // IOCP
LPDWORD lpNumberOfBytes, // 传输的字节数
PULONG_PTR lpCompletionKey, // 通过key明确对应的设备
LPOVERLAPPED *lpOverlapped, // 发出IO请求时的结构
DWORD dwMilliseconds); // 等待的事件
2021年1月23日23:30:47
第十一章 windows线程池
本章主要是关于如何使用windows线程池的,没有任何关于线程池实现的细节。
首先windows有一个默认线程池,之后的一些函数就是把任务提交到这个默认线程池。
然后,用户可调用API自己创建一个线程池,对其进行一些设置,任务也可以提交到用户创建的这个线程池。
书中提供了四种不同使用线程池的方式:
异步调用一个函数
每隔一段时间调用一个函数
内核对象触发时调用一个函数
IO请求完成时调用一个函数
1.异步调用一个函数
①被异步执行的函数首先需要遵循以下原型:
VOID CALLBACK SimpleCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context );
②然后把这个函数提交到线程池:
BOOL TrySubmitThreadpoolCallback( // 提交成功返回true
PTP_SIMPLE_CALLBACK pfns, // 这个参数就是第一步中根据原型定义的函数
PVOID pv, // 这个参数会被传递第一步中那个函数的第二参数
PTP_CALLBACK_ENVIRON pcbe // 这里是线程回调环境,把任务提交到自定义的线程池会用到,见本章第五节
);
还有另一种方式,调用TrySubmitThreadpoolCallback
时会在内部创建一个叫工作项的东西,我们可以自己创建一个工作项,然后再提交它。
①需要遵循的原型:
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work);
②创建一个工作项,参数意义和TrySubmitThreadpoolCallback
一样:
PTP_WORK CreateThreadpoolWork(PTP_WORK_CALLBACK pfnwk, PVOID Context, PTP_CALLBACK_ENVIRON pcbe);
③将工作项提交到线程池:
void SubmitThreadpoolWork(PTP_WORK pwk);
④等待调用完成或者将其取消
void WaitForThreadpoolWorkCallbacks(PTP_WORK pwk, BOOL fCancelPendingCallbacks);
第一个参数表示要取消或等待的工作项,
第二个参数传TURE,试图取消,如果已提交未被处理,会被移除,如果正在处理则会等待完成后再返回。
第二个参数传FALSE,等待工作项处理完毕返回。
2.每隔一段时间调用一个函数
①被调函数需要遵循的原型:
VOID CALLBACK TimeoutCallback(
PTP_CALLBACK_INSTANCE pInstance, // 回调函数返回后的终止操作,是一个函数,位于11.5,p339,笔记里没写
PVOID pvContext,
PTP_TIMER pTimer);
②将其提交到线程池:
PTP_TIMER CreateThreadpoolTimer( PTP_TIMER_CALLBACK pfnti, PVOID pv, TP_CALLBACK_ENVIRON pcbe);
第一参数是第一步中的函数,第二第三参数和TrySubmitThreadpoolCallback
一样。
③设置计时器对象,说明何时调用:
void SetThreadpoolTimer(
PTP_TIMER pti, // 第二步里函数的返回值
PFILETIME pftDueTime, // 第一次调用时间,单位微妙,负值表示相对时间
// 正值表示绝对时间,单位100纳秒,从1600.1.1计算
DWORD msPeriod, // 0表示只调用一次,非0表示调用间隔,单位微妙
DWORD msWindowLength // 下一次调用会在msPeriod~msWindowLength之间的随机时间调用
);
3.内核对象触发时调用一个函数
①需要遵循的原型(第一参数同TimeoutCallback
):
VOID CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE pInstance, PVOID Context, PTP_WAIT Wait, TP_WAIT_RESULT WaitResult)
最后一个参数是输出参数,说明什么情况下内核对象被触发:
WAIT_OBJECT_0 对象超时前被触发,关于超时见第三步
WAIT_TIMEOUT 对象超时前没有触发
WAIT_ABANDONED_0 传递的对象是一个互斥量对象,且该对象被遗弃(互斥量所属的线程在释放互斥量前终止)
②创建一个类似于工作项的东西提交到线程池,这里叫线程池等待对象:
PTP_WAIT CreateThreadpoolWait(PTP_WAIT_CALLBACK pfnwa, PVOID Context, PTP_CALLBACK_ENVIRON pcbe);
③把等待触发的对象绑定到线程池对象:
PTP_WAIT pwa, // 第二步中的返回值
HANDLE h, // 要绑定内核对象的句柄
PFILETIME pftTimeout // 等待时间,0表示不等待,负值表示相对时间,正值表示绝对时间,NULL表示无限长
);
4.IO请求完成时调用一个函数
①遵循的原型(第一参数同TimeoutCallback
):
VOID WINAPI OverlappedCompletionRoutine(
PTP_CALLBACK_INSTANCE pInstance,
PVOID pvContext, // 参数意义和TrySubmitThreadpoolCallback一样:
PVOID pOverlapped, // io成功时获得一个OVERLAPPED对象,就是在ReadFile或者WriteFile的那个
ULONG IoResult, // IO成功时候值为NO_ERROR
ULONG_PTR NumberOfBytesTransferred, // 已传输字节数
PTP_IO pIo); // 指向线程池中一个IO项的指针,看不懂,大概是第二步里面的返回值?反正是线程来传递
②创建一个线程池的IO对象(并且关联一个设备):
PTP_IO CreateThreadpoolIo(HANDLE hDev, PTP_WIN32_IO_CALLBACK fnio, PVOID pv, PTP_CALLBACK_ENVIRON pcbe);
第一参数是要关联的设备
第二参数,官方文档是每次IO时都会调用的回调函数,应该就是传递第一步定义的函数了了。
第三第四参数前面都有。
③让线程池开始监视IO操作的完成并调用回调函数:
void StartThreadpoolIo(PTP_IO pio);
参数就是第二步创建的函数,开始每次IO后都调用函数
④取消在IO后调用函数的行为:
void CancelThreadpoolIo(PTP_IO pio);
⑤解除线程池IO对象和线程池的关联
void CloseThreadpoolIo(PTP_IO pio);
5.自己创建线程池
CreateThreadpoolWork、CreateThreadpoolTimer、CreateThreadpoolWait、CreateThreadpoolIo等函数,有一个PTP_CALLBACK_ENVIRON参数,称之为回调环境。
回调环境中有成员指向自己创建的那个线程池。
回调环境的结构:TP_CALLBACK_ENVIRON。
回调环境的初始化和销毁
void InitializeThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);
void DestroyThreadpoolEnvironment(PTP_CALLBACK_ENVIRON pcbe);
线程池的创建、设置、关闭
PTP_POOL CreateThreadpool(PVOID reserved);
// 参数保留,传NULL
BOOL SetThreadpoolThreadMinimum(PTP_POOL ptpp, DWORD cthrdMic);
void SetThreadpoolThreadMaximum( PTP_POOL ptpp, DWORD cthrdMost);
这里主要就是设置一个线程数量,线程池中的线程也会被创建和销毁的。
如果最大数量和最小数量一样,那么线程数量固定,就不会创建和销毁。
将线程池对象关联到回调环境中
void SetThreadpoolCallbackPool(PTP_CALLBACK_ENVIRON pcbe, PTP_POOL ptpp);
关闭线程池时的清理工作
如何直接关闭,那么线程池中的工作项,都会消失,还有可能出现未定义行为。
①创建一个线程池清理组对象
PTP_CLEANUP_GROUP CreateThreadpoolCleanupGroup();
②将该清理组和回调环境关联
void SetThreadpoolCallbackCleanupGroup(
PTP_CALLBACK_ENVIRON pcbe, // 回调环境对象
PTP_CLEANUP_GROUP ptpcg, // 清理组对象
PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng); // 在CloseThreadpoolCleanupGroupMembers第二参数传递true,每隔被取消的工作项都会调用该函数,其原型见后
void PtpCleanupGroupCancelCallback(
PVOID ObjectContext, // 被取消项的上下文,就是先前许多函数中,第二参数,传递的那个context参数
PVOID CleanupContext // 清理组的上下文,CloseThreadpoolCleanupGroupMembers的第三参数那个值
)
③关闭之前调用函数清理线程池中的工作项
void CloseThreadpoolCleanupGroupMembers(
PTP_CLEANUP_GROUP ptpcg, // 清理组对象
BOOL fCancelPendingCallbacks, // True表示正在执行的执行完,其余的都取消,书上没有,但是Fakses应该是等待所有执行完毕
PVOID pvCleanupContext //
);
④关闭该清理组对象
void CloseThreadpoolCleanupGroup(PTP_CLEANUP_GROUP ptpcg);
第十二章 纤程
进程是在操作系统层面上的多任务。
线程是在进程上的多任务。
纤程是在线程上的任务。
书中讲到应该避免使用纤程。
纤程的调度,不是抢占式的,其何时执行,完全由我们的代码控制。
梳理一下流程:
①在某个线程中,调用ConvertThreadToFiber
来将当前线程转换为纤程,将自己转换为纤程后,还是接着执行原来那份代码。
②调用CreateFiber
创建其他纤程。
纤程函数应遵循的原型:VOID FiberFunc(PVOID pvParam)
③调用SwitchToFiber
在纤程之前切换。
④销毁纤程DeleteFiber
。
⑤取得当前纤程地址和当前纤程上下文:GetCurrentFiber、GetFiberData
这里的上下文,就是第二步函数原型里面的那个参数,用户自定义的那个。
各个函数:
// 这两个函数将线程转换为纤程
LPVOID ConvertThreadToFiber(LPVOID lpParameter); // 这两个函数返回值都是:纤程执行上下文的内存地址
LPVOID ConvertThreadToFiberEx(
LPVOID lpParameter, // 这个参数是一个用户自定义的值,大概作为标识的把
DWORD dwFlags // 可传入一个FIBER_FLAG_FLOAT_SWITCH,因为CPU的浮点状态信息不属于其寄存器的一部分
); // 不会每个纤程都维护,如果纤程进行浮点操纵就会破坏,这是默认行为,该标志来覆盖默认行为,使纤程维护浮点信息
// 这两个函数用来创建其他纤程
LPVOID CreateFiber( // 返回值:纤程执行上下文的内存地址
SIZE_T dwStackSize, // 栈大小
LPFIBER_START_ROUTINE lpStartAddress, // 指定纤程函数地址
LPVOID lpParameter // 会被传递给先前提到的纤程函数原型的参数
);
LPVOID CreateFiberEx( // 返回值:纤程执行上下文的内存地址
SIZE_T dwStackCommitSize, // 栈大小
SIZE_T dwStackReserveSize, // 预定虚拟内存
DWORD dwFlags, // 接收FIBER_FLAG_FLOAT_SWITCH标志,和ConvertThreadToFiberEx一样
LPFIBER_START_ROUTINE lpStartAddress,// 指定纤程函数地址
LPVOID lpParameter // 会被传递给先前提到的纤程函数原型的参数
);
// 切换纤程
void SwitchToFiber(PVOID lpFiber); // 传入创建纤程时返回的地址
// 销毁纤程
void DeleteFiber(LPVOID lpFiber); // 传入创建纤程时返回的地址
PVOID GetCurrentFiber(); // 取得当前纤程地址
PVOID GetFiberData(); // 取得当前线程上下文
2021年1月27日21:45:30
第十三章 windows内存体系结构
本章内容全是概念,然后笔记中使用书上的名词,名词的解释按自己理解的整合。
前见第三章第一节。
在32位系统中,4个G的空间被分为了用户态和内核态两段(见前)。
默认情况下,访问内核态的空间会导致异常,然后程序被结束。
在用户态的0x000000~0x7FFFFFFF的这部分,又被掐头去尾各去掉64K,其中:
0x00000000~0x0000FFFF 64K 空指针赋值区
0x00010000~0x7FFEFFFF 用户模式分区
0x7FFF0000~0x7FFFFFFF 64K 禁入分区
空指针赋值区和禁入分区都不可访问。
随手写一下,可以通过配置BCD,使用户态的空间扩展到3GB。
分配粒度:诸如4K、8K这样等,按固定单位分配空间,不同的CPU平台大小不同。
页面:内存中的空间都按相同的大小划分的单位。
页交换文件:在磁盘上,当内存空间不足时,把内存空间的一些页面来转移的磁盘,为内存页面腾出空间,需要时在由批磁盘换到内存中。
颠簸:硬盘和内存交换页面的过程,交换的次数越多,颠簸越厉害,效率随之降低。
预订:前述的用户态那块指针区域的使用权(虚拟空间)。仅仅是使用权,没有实际与之对应的空间。
这个对应的空间可能来自磁盘,也可能来自内存,在内存或磁盘上的实际存储空间,和从用户态申请的虚拟空间地址以某种方式关联起来,这个过程称为映射。这样,就能以虚拟空间的地址,访问对应的实际存储空间。
虚拟空间地址的起始地址:是分配粒度的整数倍(下对齐,下对齐见15章)。
虚拟空间地址的大小:页面大小的整数倍(自动对齐)。
页面的保护属性:
PAGE_NOACCESS 读写执行皆不可
PAGE_READONLY 只读
PAGE_WRITE 不允许执行
PAGE_EXECUTE 不允许读写
PAGE_EXECUTE_READ 不允许写操作
PAGE_EXECUTE_READWRITE 读写执行权限
PAGE_WRITECOPY 不允许执行,写操作导致写时复制(创建页面副本)。
PAGE_EXECUTE_WRITECOPY 不禁止任何操作,写操作导致写时复制(创建页面副本)。
PAGE_NOCACHE禁止对已调拨的页面缓存。驱动程序使用。
PAGE_WRITECOMBINE单个设备的多次写操作合并。驱动程序使用。
PAGE_GUARD页面字节被写入时得到通知。创建线程栈会用到。
第十四章 探索虚拟内存
本章主要内容是通过一些函数获取系统信息。
1.系统信息
void WINAPI GetSystemInfo(LPSYSTEM_INFO lpSystemInfo);
// 输出参数
typedef struct _SYSTEM_INFO {
union {
DWORD dwOemId;
struct {
WORD wProcessorArchitecture;
WORD wReserved;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
DWORD dwPageSize; // CPU页面的大小,x86和x64是4096字节
LPVOID lpMinimumApplicationAddress; // 每个进程可用地址空间的最小地址,因为钱64k别用作空指针赋值区,所以一般为65534或0x00001000
LPVOID lpMaximumApplicationAddress; // 同上,最大地址
DWORD_PTR dwActiveProcessorMask; // 预订地址空间的分配粒度,预订概念见十三章
DWORD dwNumberOfProcessors; // CPU核心数量
DWORD dwProcessorType; // 已作废
DWORD dwAllocationGranularity; // 处理器题解结构
WORD wProcessorLevel; // 对体系结构进行划分
WORD wProcessorRevision; // 对wProcessorLevel进行划分
} SYSTEM_INFO, *LPSYSTEM_INFO;
2.虚拟内存信息
这里的虚拟内存就是用户态0x00010000~0x7FFEFFFF那一块了。
void GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer);
两个版本区别在于,前者是32位,超过4g时就不足以表示了。
typedef struct _MEMORYSTATUS {
DWORD dwLength; // 该结构的长度,区分不同的版本
DWORD dwMemoryLoad; // 内存管理系统繁忙值,0~100
SIZE_T dwTotalPhys; // 物理内存总量
SIZE_T dwAvailPhys; // 可分配内存总量
SIZE_T dwTotalPageFile; // 硬盘上页面交换最多可放多少字节
SIZE_T dwAvailPageFile; // 未被使用的
SIZE_T dwTotalVirtual; // 32位下是0x00010000~0x7FFEFFFF,掐头去尾这么多
SIZE_T dwAvailVirtual; // 前一个成员中未被使用的数量
} MEMORYSTATUS, *LPMEMORYSTATUS;
typedef struct _MEMORYSTATUSEX {
DWORD dwLength;
DWORD dwMemoryLoad;
DWORDLONG ullTotalPhys;
DWORDLONG ullAvailPhys;
DWORDLONG ullTotalPageFile;
DWORDLONG ullAvailPageFile;
DWORDLONG ullTotalVirtual;
DWORDLONG ullAvailVirtual;
DWORDLONG ullAvailExtendedVirtual;
} MEMORYSTATUSEX, *LPMEMORYSTATUSEX;
3.工作集
进程地址空间映射到实际内存空间中的那些页面,称为工作集。
没办法直接确定进程使用实际内存的数量,只能通过工作集的方式。
BOOLWINAPIGetProcessMemoryInfo(
HANDLE Process, // 进程句柄
PPROCESS_MEMORY_COUNTERS ppsmemCounters, // 输出参数,返回该结构
DWORD cb // 第二参数的长度
);
typedef struct _PROCESS_MEMORY_COUNTERS {
DWORD cb;
DWORD PageFaultCount;
SIZE_T PeakWorkingSetSize;
SIZE_T WorkingSetSize; // 进程所使用的字节数
SIZE_T QuotaPeakPagedPoolUsage; // 进程使用字节数的峰值
SIZE_T QuotaPagedPoolUsage;
SIZE_T QuotaPeakNonPagedPoolUsage;
SIZE_T QuotaNonPagedPoolUsage;
SIZE_T PagefileUsage;
SIZE_T PeakPagefileUsage;
} PROCESS_MEMORY_COUNTERS;
4.确定地址空间状态
// 两者区别在于后者可以查询指定进程
SIZE_T VirtualQuery(
LPCVOID lpAddress, // 指向要查询的页区域的基址的指针,没看明白
PMEMORY_BASIC_INFORMATION lpBuffer,
SIZE_T dwLength // 第二参数的长度
);
SIZE_T VirtualQueryEx(
HANDLE hProcess,
LPCVOID lpAddress,
PMEMORY_BASIC_INFORMATION lpBuffer,
SIZE_T dwLength
);
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress; // VirtualQuery参数lpAddress下取整的页面大小
PVOID AllocationBase; // 区域的基址
DWORD AllocationProtect; // 区域的保护属性
WORD PartitionId; // 书中没有这个成员,官方文档里有
SIZE_T RegionSize; // 页面的大小,起始地址是BaseAddress,书中描述是区域的大小,感觉差不多
DWORD State; // 页面状态,类型为闲置时,该成员无效
DWORD Protect; // 相邻页面的保护属性
DWORD Type; // 页面类型MEM_FREE(闲置)、MEM_PRIVATE(私有)、MEM_IMAGE(映像)、MEM_MAPPED(已映射)p371
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
2021年1月28日20:42:43
第十五章 虚拟内存的使用
本章主要是在第十三章的基础上,对虚拟内存的操作。
需要说明的是,其中函数提供的接口是按字节来算的,但在其内部,确是按页面的单位操作的。
也就是需要自己根据字节单位来换算成页面数,以免不小心多操作一个页面或者少操作一个页面。
关于如何确定内存页面大小,可通过十四章第四节的内容。
1.VirtualAlloc
首先这个函数,同时兼具预订区域和为区域分配实际存储空间两个功能。
可以分成两次调用,也可以合成一次调用。
原型:
LPVOID VirtualAlloc( // 返回值是预订区域的虚拟地址,成功的情况下都是返回地址,否则NULL。
LPVOID lpAddress, // 要预订的区域的地址
SIZE_T dwSize, // 要预订区域的大小
DWORD flAllocationType, // 预订区域的同时是否为其分配空间
DWORD flProtect // 各个属性见第十三章最后,另外还有一些新的,后面补充
);
具体说明下各个参数:
lpAddress
该参数如果为空,那么系统就会随意寻找一块符合要求的起始地址。
该参数如果不空,那就是要求系统返回该地址作为起始地址,但是系统不会如此,会首先将该参数下对齐到分配粒度的整数倍(其实就是对齐到页面那么大,方便按页面操作),然后看看需求的空间够不够。
dwSize
这个参数是按字节计算的,然后这个值会被上对齐到分配粒度的整数倍。其实就是对齐到页面那么大,方便按页面操作。
flAllocationType
MEM_RESERVE 只预订指针区域,不分配实际空间
MEM_TOP_DOWN 随机返回地址时,不是在地址空间中从上到下或者从下到上寻找打,这个标志会导致从上到下寻找。
MEM_COMMIT 表示会分配实际存储空间。
MEM_DECOMMIT VirtualFree释放空间时候传递
flProtect
该参数是保护属性,各个标志见十三章最后。
无论区域是什么属性,只要没有分配实际存储空间,访问就会引发访问异常。
例子:
分两次调用
// 两次调用的第二参数可以不一样,见后
// 两次调用的第三参数不一样了,一次是预订区域,一次是分配实际存储。
// 两次调用的保护权限这里都是读写,虽然他们可以不一样,但是一样的情况下效率会好。
PVOID P = VirtualAlloc(NULL, 521, MEM_RESERVE, PAGE_READWRITE);
VirtualAlloc(P, 521, MEM_COMMIT, PAGE_READWRITE);
合成一次调用
PVOID P = VirtualAlloc(NULL, 521, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
2.VirtualFree
BOOL VirtualFree(
LPVOID lpAddress, // VirtualAlloc返回的那个地址
SIZE_T dwSize, // 一般为0,因为操作系统知道
DWORD dwFreeType // MEM_DECOMMIT,表示释放空间
);
另外如果想只释放一部分,lpAddress标识页面地址,dwSize表示要释放的字节数,然后在内部会被转换成页面单位。假如页面大小64k,然后传递的值为64k+1字节,那么就会释放两个页面。
3.改变页面保护属性
改变页面保护属性有一个比较有用的地方,比如说可以在修某块数据时,把这块数据地址传入,然后由地址找到页面,将其改为读写属性,修改完之后再改为无权限属性,访问会导致引发访问异常,结束程序。实现了一种另类的保护。
BOOL VirtualProtect(
LPVOID lpAddress, // 传入的地址
SIZE_T dwSize, // 修改页面的大小,字节数
DWORD flNewProtect, // 新的权限
PDWORD lpflOldProtect // 返回旧的权限
);
第十六章 线程栈
本章主要说明线程栈的增长过程和边界控制。
后面三幅图,关于线程栈的内容都得靠这三张图。
线程栈一般1M大小。
在线程栈中,内存被分为固定大小的页面,以上三幅图中,应该是64KB,没有算。
一开始,会通过虚拟内存预订1M大小的指针区域使用权,在图中他们是已预订页面。
其中有一个叫带保护属性标志的已调拨页面这个页面是用来边界控制的。
这个页面具有PAGE_GUARD属性,意为:当写这个页面时会得到通知。
图一和图二是线程栈正常增长的过程。
图三是边界处理的。
①当程序里声明一个变量,那么栈就要沿着高地址忘低地址方向增长,也意味着对其读写。
②当写到带保护属性标志的已调拨页面,系统得到通知,需要新的页面了,那么当前页面被设为已调拨页面。下一个页面设为带保护属性标志的已调拨页面,如此便实现了线程栈的增长。
③为了防止栈的溢出,关键在于最后两个页面,即0x08001000和0x08000000那两个页面。
④最后一个页面0x08000000不会被设为带保护属性标志的已调拨页面,也永远不会被使用,永远是被预定的却没有实际空间,它是一个类似边界、隔离带的页面。
⑤当倒数第二个0x08001000这个页面被设为带保护属性标志的已调拨页面,接下来如果0x08001000被写入,会引发EXCEPTION_STACK_OVERFLOW栈溢出异常(处理异常的内容在二十三章,还没看到)。
⑥接下来需要处理这个异常,比如说让当前函数返回以减少栈(这句是我瞎说的)?但是如果解决了异常了,栈因为变量的声明导致访问到了0x08000000的页面,就会导致程序被结束。
2021年1月29日20:01:59
第十七章 内存映射文件
和虚拟内存相似。
虚拟内存是预订区域使用权,即指针范围的使用权,然后分配一块内存关联,
内存映射是预订区域使用权,把磁盘上文件对应的空间,和区域关联。
段的概念:每个.exe或者DLL的映像都由段组成,段存储的信息各异,诸如未初始化的数据、初始化的数据、调试信息、.exe或DLL的代码、线程本地存储等等。
视图:本章第二节,经过第三步的操作后,映射到地址空间的部分称为视图。
映射文件的一致性:系统不保证不同文件映射对象的各个视图一致,保证同一文件映射对象的多个视图一致。
也就是说不同进程CreateFile
打开同一个磁盘文件,再通过CreateFileMapping
创建不同的文件映射对象,最后用这些不同文件映射对象映射到内存,是不保证一致性的。
而通过通过同一个文件映射对象创建的不同句柄,这样的行为是是可以保证的。
根本区别在于前者是多个文件映射对象句柄对应各自的文件映射对象。
后者是多个文件映射对象句柄对应同一个的文件映射对象。
①当一个程序启动或者CreateProcess,系统预订一块足够大的地址空间,来放置.exe文件,.exe中会指定预订哪一块地址空间。预订的地址默认是0x00400000。
②系统会对其进行标注,表示该指针区域关联的是磁盘上的空间而非内存。
③接着系统会访问.exe的一个段,这个段里存储着关联的DLL。
④同理也会对每一个DLL来预订地址空间,然后把DLL在磁盘中的地址映射到地址空间中,DLL中也指定了预定哪一块地址空间,当DLL预订的地址空间冲突,会根据重定位信息来载入,具体这个信息是什么,不清楚。
1.同一可执行文件和DLL是否可共享静态数据
.exe或DLL中一些全局变量之类的东西,当运行多个或者载入多次,这些数据默认是不会共享的,他们是写时复制的。
虽然指向的都是同样的磁盘上的空间,但是只是访问的话,什么事都不会发生,如果试图写他们,就会引发复制,修改的都是副本。
当然也可通过某些方式,让多次运行的.exe或者被载入多个程序的DLL,来共享一些变量:
一、自己建立一个段区,把需要共享数据放在段之间
其中data_seg应该是段的语法,shared是自定义的段名
#pragma data_seg("shared")
long a = 0;
#pragma data_seg()
二、通过链接器将其嵌入到源代码
shared是要链接的段名,逗号后面跟着的是段的属性R表示读,W表示写,E表示执行,S表示共享
#pragma comment(linker, "/SECTIONL:shared,RWS")
2.将磁盘文件映射到预订的地址空间
①使用CreateFile
打开一个文件
②使用第一步得到的文件句柄作为CreateFileMapping
的参数,得到一个文件映射的内核对象。
③使用第二步得到的文件映射对象句柄作为MapViewOfFile
的参数,将部分或者全部内容映射到地址空间。
④将第三步映射到地址空间的内容取消,释放地址空间。
⑤关闭文件映射对象。
⑥关闭文件映射内核对象。
注:在①②③三步中,每次都需要指定对应的保护属性,三次调用属性最好一样。
接下来是各个步骤的详细说明:
①关于CreateFile
函数详细见第十章第二节。
1.5:CreateFile
失败返回INVALID_HANDLE_vALUE,CreateFileMapping
失败返回NULL。
②CreateFileMapping
HANDLE CreateFileMappingA(
HANDLE hFile, // 第一步中打开的文件句柄
LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // 安全性队形,主要还是 文件映射 对象本身是否可继承
DWORD flProtect, // 这里是对磁盘上文件页面的保护属性,详见后
DWORD dwMaximumSizeHigh, // 这两个参数表示一个64位的值,这里是高32位
DWORD dwMaximumSizeLow, // 这里是低32位,详见后
LPCSTR lpName // 为 文件映射 对象命名,见第三章内核对象共享
);
flProtect参数的各种值:
PAGE_READONLY
读权限,需要在CreateFile
时传递GENERIC_READ
PAGE_READWRITE
读写权限,需要在CreateFile
时传递GENERIC_READ | GENERIC_WRITE
PAGE_WRITECOPY
写时复制权限,需要在CreateFile
时传递GENERIC_READ或GENERIC_READ | GENERIC_WRITE
PAGE_EXECUTE_READ
可以读数据,也可以执行其中的代码,需要在CreateFile
时传递GENERIC_READ | GENERIC_EXECUTE
PAGE_EXECUTE_READWRITE
读写执行,需要在CreateFile
时传递GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE
SEC_NOCACHE
不对映射的文件进行缓存,直接读写磁盘。
SEC_IMAGE
表示要映射的文件是一个PE文件映像
SEC_RESERVE
创建文件映射时,不应该使用,只预订区域。
SEC_COMMIT
创建文件映射时,不应该使用,只预订区域后,分配空间。
SEC_LARGE_PAGES
使用大页面的内存。
参数dwMaximumSizeHigh 和 dwMaximumSizeLow
单位字节,告诉系统内存映射文件的最大大小。
如果传递了PAGE_READWRITE属性,那么该参数应该不小于磁盘上文件的大小。
如果传递了PAGE_READONLY或者PAGE_WRITECOPY属性,那么该参数应该不大于磁盘上文件的大小。
如果传递了比磁盘上文件大小更大的值,那么该文件会被加长。
③MapViewOfFile
‘
创建文件映射对象之后,需要为其预订一块地址空间。
LPVOID MapViewOfFile( // 返回值是预订的地址空间,与地址空间关联的是磁盘上的文件
HANDLE hFileMappingObject, // 第二步中返回的句柄
DWORD dwDesiredAccess, // 返回的预订的地址空间的权限,见后
DWORD dwFileOffsetHigh, // 这两个函数指明文件偏移量,即从文件的哪里开始映射,单位字节,必须是分配粒度的整数倍
DWORD dwFileOffsetLow, // 低32位
SIZE_T dwNumberOfBytesToMap // 从偏移量开始要映射多少字节过来
);
dwDesiredAccess参数
FILE_MAP_READ
读,在CreateFileMapping
时需要传递PAGE_READONLY或PAGE_READWRITE
FILE_MAP_WRITE
写,在CreateFileMapping
时需要传递PAGE_READWRITE
FILE_MAP_ALL_ACCESS
等价于FILE_MAP_READ | FILE_MAP_WRITE | FILE_MAP_COPY
FILE_MAP_COPY
,写时复制,导致从内存中获得存储空间,在CreateFileMapping
时需要传递PAGE_WRITECOPY
FILE_MAP_EXECUTE
执行,在CreateFileMapping
时需要传递PAGE_EXECUTE_READ或PAGE_EXECUTE_READWRITE
④BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
参数是第三步中的返回值。取消映射。
⑤CloseHandle(第二步的文件映射内核对象);
⑥CloseHandle(第一步中的文件对象);
2021年1月30日22:14:49
第十八章 堆
1.概念
虚拟内存和内存映射文件都是按页面和分配粒度的单位来操作的。
堆不是,堆比另外两个都慢,但是堆是用来操作小块内存的。
系统内部,会有一个默认堆,堆一开始也是预订一块地址空间区域,其中有1MB是有实际内存的,其他大部分区域没有调拨物理存储器,也就是分配实际内存。
在程序运行过程中,调用new/malloc时,给这些预订的地址空间区域分配实际内存并返回相应的地址空间的地址。
delete/free时,对传递进来的地址撤销物理存储器,也就是取消地址空间和内存的映射。
注:这里书中原话是从堆中分配内存和释放堆中的内存块时,所以直接理解成了对new等的调用。
关于堆:
其内部会通过调BOOL HeapLock(HANDLE hHeadp)
和BOOL UnHeapLock(HANDLE hHeadp)
来保证堆的多线程访问安全。
当我们自己创建堆时,可以通过某些方式让堆内部放弃调用这两个函数,比如确定某个堆只会被单线程访问,那么让其取消线程安全的行为也是可以的,毕竟加锁解锁对单线程来说是无意义的消耗。
获取默认堆的句柄:HANDLE GetProcessHeap()
2.建立默认堆之外的堆
书中关于自建堆的好处不一一写,我认为其中最重要的有两个:
①对某个经常使用的类或结构,建立一个专用的堆,这样每次申请和释放都是同样的大小,无需担心内存碎片。
更甚在一个类内部建立一个身为静态成员的堆,然后为该类重载operator new和operator delete操作符。
②解决对默认堆的并发访问而带来的效率问题。
2.1建立堆
HANDLE WINAPI HeapCreate(
DWORD flOptions, // 建立堆的标志,见后
SIZE_T dwInitialSize, // 堆的初始大小,会被上取整到CPU页面大小
SIZE_T dwMaximumSize ); // 堆的最大可增长大小,如果为0表示没有上限,知道内存用尽
相关标志:
HEAP_NO_SERIALIZE 传递该标志即取消堆的线程安全,在内部不会调用HeapLock
和UnHeapLock
来保证线程安全
HEAP_GENERATE_EXCEPTIONS 在调用堆分配内存时,可能因为空间不足而分配失败,默认情况下返回NULL,传递该标志表示抛出异常来表明失败,而不是返回NULL。
抛出的异常有两个:
①STATUS_NOMEMORY内存不足
②STATUS_ACCESS_VIOLATION传递的参数不正确或者堆被破坏
HEAP_CREATE_ENABLE_EXECUTE 如果未指定该标志,并且应用程序试图从受保护的页面运行代码,则应用程序将接收一个状态代码为status_access_breach的异常。
2.2从堆中分配内存
LPVOID HeapAlloc(
HANDLE hHeap,
DWORD dwFlags, // HEAP_NO_SERIALIZE、HEAP_GENERATE_EXCEPTIONS、HEAP_ZEOR_MEMORY,前两个和HeapCreate是一样的,第三个就是把分配的空间初始化为0
SIZE_T dwBytes // 分配的内存大小
);
2.3调整已从堆中分配内存块的大小
LPVOID HeapReAlloc(
HANDLE hHeap,
DWORD dwFlags, // 标志和HeapAlloc的那三个一样,还有一个额外HEAP_REALLOC_IN_PLACE_ONLY,详见后
LPVOID lpMem, // 被调整的内存块
SIZE_T dwBytes // 被调整后的大小
);
当dwBytes比原来的大小小的时,会直接在原来的空间上进行缩小。
当dwBytes比原来的大小小的时,传递了HEAP_REALLOC_IN_PLACE_ONLY,表示原地增长,不允许移动内存块到另一个地方,如果无法增长,书中也没有说。不传递这个标志,那么就可能导致找一块新的大的内存块,然后把数据移动过去。
2.4查询一块已分配内存的大小
SIZE_T HeapSize(
HANDLE hHeap,
DWORD dwFlags, // 只能传0或HEAP_NO_SERIALIZE
LPCVOID lpMem // 应该是输出参数作为返回值吧
);
2.5释放内存
BOOL HeapFree(
HANDLE hHeap,
DWORD dwFlags, // 只能传0或HEAP_NO_SERIALIZE
_Frees_ptr_opt_ LPVOID lpMem // 要释放的地址
);
2.6销毁堆
BOOL HeapDestroy(HANDLE hHeap);
3.返回一个进程中的所有堆的句柄
DWORD GetPRocessHeaps(DWORD dwNumHeaps, PHANDLE phHeaps)
4.检查一块内存或整个堆是否被破坏
BOOL HeapValidate(HANDLE hHeap, DWORD dwFlags, LPCVOID lpMem);
第十九章 DLL基础
Kernel32.dll 包含的函数用来管理内存、进程、线程
User32.dll 包含的函数用来执行与用户界面相关的任务
GDI32.dll 包含的函数用来绘制图像和显示文字
1.概念
一旦系统将一个DLL映射到进程的地址空间,进程中所有线程都可以调用该DLL的函数。
对进程的线程来说,DLL中的代码和数据就想附加的代码和数据,碰巧放在了进程地址空间中。
在DLL中创建的对象、用VirtualAlloc预订的区域,都是属于进程的,即便撤出对DLL的映射,他们也不会消失。
①一个DLL的源文件,大概形式就是一个.h的头文件,以及一组.cpp的相关文件。其中.h文件包含了各个.cpp文件中要导出的变量、函数、类名等。
②编译时,对每个源文件产生一个对应的.obj模块。
③将这些.obj链接到一起,形成一个单独的DLL文件。同时还会产生一个.lib文件,其中列出了所有被导出的函数和变量的符号名。
④而在要导入DLL的那个可执行文件中,需要包含那个.h的文件,然后好像还需要链接.lib文件(书上没有说,但是我以前好像搞过一次,记不清了)。
2.一个例子
关于这个例子,是书上的稍稍改了一下,有点复杂,网上的其他教程好像只用__declspec(dllexport)
就可以了,总之先把概念记下来。
详细说明见后。
MyDll.h
// MyDll.h
#ifdef MYDLLAPI
#else
#define MYDLLAPI extern "C" __declspec(dllimport)
#endif
MYDLLAPI int a1;
MYDLLAPI int add(int a, int b);
MYDLLAPI int a2;
MYDLLAPI int sub(int a, int b);
MyDllSouce1.cpp
// MyDllSouce1.cpp
#include <windows.h>
#define MYDLLAPI extern "C" __declspec(dllexport) // 这里顺序很重要,见后
#include “MyDll.h”
int a1;
int add(int a, int b){ return a + b; }
MyDllSouce2.cpp
// MyDllSouce2.cpp
#include <windows.h>
#define MYDLLAPI extern "C" __declspec(dllexport) // 这里顺序很重要,见后
#include “MyDll.h”
int a2;
int sub(int a, int b){ return a - b; }
这三个文件需要合起来看。
__declspec(dllimport)
表示导入
__declspec(dllexport)
表示导出
extern "C"
的意义:按c的方式在内部对函数名和变量名改编(C和C++的改编规则是不一样的),DLL就可以兼容C。
接下来就是MYDLLAPI
这个宏的多次定义和顺序了。
首先MyDll.h这个文件会在两个地方被包含。
第一处就是在生成DLL的那个项目里的各个源文件,如本例中的MyDllSouce1.cpp和1MyDllSouce2.cpp这两个文件。
第二处就是要使用这个DLL的.exe程的项目里包含。
在第一处时,各个源文件里是如下操作:
#define MYDLLAPI extern "C" __declspec(dllexport)
#include “MyDll.h”
在包含MyDll.h
之前定义MYDLLAPI
,MyDll.h
就会检查到已经定义,接下来就会在内部替换成诸如:extern "C" __declspec(dllexport) int a1;
,表示要导出a1这个变量,全部替换后,那么在生成DLL的这个项目里,所有的变量和函数的符号名都被声明成要导出的。
在第二处时:
因为没有提前定义MYDLLAPI
,在MyDll.h
内部那个MYDLLAPI
的定义就生效了,替换成诸如:extern "C" __declspec(dllimport) int a1;
表示要导入a1这个变量,那么在这个生成.exe的项目中,所有的变量和函数的符号名都被声明成要导入的。
2021年1月31日20:56:18
第二十章 DLL高级
DLL载入到进程中的地址空间有两种方法。一种就是十九章里面的方法,称为隐式链接。另一种就是接下来的显式链接。
显式链接的方式是通过在程序运行时调用函数将DLL载入。
已知DLL:系统提供的一些DLL,这些DLL在注册表中有一个类似键值对的东西,如果是name:xxx.dll,这样的映射,在LoadLibrary
有一些需要注意的东西。
LoadLibrary(TEXT("OneDll"));
和LoadLibrary(TEXT("OneDll.dll"));
的搜索规则是不一样的。
LoadLibrary(TEXT("OneDll"));
没有.dll后缀,按正常规则搜索。
LoadLibrary(TEXT("OneDll.dll"));
有.dll,查找时会去掉.dll后缀,尝试在注册表中优先搜索,将OneDll作为先前提到键值对的name,来查找对应的xxx.dll。
本章有主要有四个内容:
一是关于显示载入以及使用方法。
二是关于DLL入口点函数,入口点函数做了什么,以及一些零散的进程启动时和DLL关联的概念。
三是通过一个工具将DLL的基址重定位以提高效率。
四是通过另一个工具将DLL和.exe绑定,先一步计算出程序运行时函数和变量的虚拟地址,以提高效率。
1.显式载入 & 使用 & 卸载
重点是在载入DLL后,如果要使用某个函数或者变量,那么先要通过GetProcAddress
取得对应的符号名,详见后。
HMODULE LoadLibrary(PCTSTR lpLibFileName);
HMODULE LoadLibraryEx(PCTSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
其中HMODULE类型在第四章第二节提到过,等价HINSTANCE类型。后续在DLL的入口点函数中,会作为参数参数。
一个需要注意的问题:
LoadLibraryEx
传递了某些标志后,如LOAD_LIBRARY_AS_DATAFILE,那么其返回值是不可用的,因为DLL并没有真的载入。
第一个函数不必多说,传入DLL的地址或者路径名。
第二个函数的第二参数是保留的,必须为NULL。第三参数是一些符号,接下来说明这些符号:
符号名 | 作用 |
---|---|
DONT_RESOLVE_DLL_REFERENCES | DLL加载到地址空间时,不调用入口点函数,如果该DLL关联了其他DLL,那么其他DLL不会被加载。 |
LOAD_LIBRARY_AS_DATAFILE | 将DLL作为数据文件映射到地址空间,不会花时间准备可执行的代码。 |
OAD_LIBRARY_AS_DATAFILE_EXCLUSIVE | 和前一个符号相同,区别是这个标志是独占打开。 |
LOAD_LIBRARY_AS_IMAGE_RESOURCE | 对相对虚拟地址(RVA)进行修复,RVA在DLL是一个相对地址,比如说一个变量的RVA是100,DLL加载到虚拟地址空间后,会有一个基址,基址加上这个100,就是这个变量在虚拟地址空间的地址。 |
LOAD_LIBRARY_AS_IMAGE_RESOURCE | 搜索方式,不想写了 |
LOAD_IGNORE_CODE_AUTHZ_LEVEL | 关于XP系统的一个权限问题。 |
FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName);
这个就是本节开始那个获取符号名地址的方法。第一参数就是载入DLL的返回值,第二参数则是查找额符号名,书中说的是符号名,而在搜索时都说的是函数,不清楚变量名可不可以。
BOOL FreeLibrary(HMODULE hLibModule);
void FreeLibraryAndExitThread(HMODULE hLibModule, DWORD dwExitCode);
以上两个函数用来卸载DLL,第一个函数不必多说。
第二个函数第二参数是线程的退出代码,其存在的意义:DLL创建了一个线程,那么就出现了一个问题,如果调用FreeLibrary,那么FreeLibrary返回后,就会导致线程执行不存在代码,引发访问异常,导致进程被终止,该函数为解决该问题而存在。
2.DLL入口点函数
在入口点函数中应避免调用LoadLibrary
或FreeLibrary
因为可能导致循环依赖,在其中只进行打开文件、创建内核对象等简单操作。
DLL入口点函数的调用是原子的,同时创建多个线程,那么对于同一个DLL的入口点函数,一个线程调用完毕后,另一个函数才能继续调用,否则就是处于挂起状态。
不要再入口点函数调用WaitForSingleObject
。
原型:
BOOL WINAPI DllMain( // !!!这里的Dll是小写,不是DLL
HINSTANCE hinstDLL, // 载入DLL时的那个返回值
DWORD fdwReason, // 这个函数有会有四个值,代表不同时刻调用该该入口点函数,详见后
LPVOID lpvReserved); // 隐式载入时,该值非0,显式载入时为0
入口点函数会在进程加载DLL到地址空间、DLL从地址空间卸载、线程创建、线程销毁时分别调用。区别就在于第二参数。
关于第二参数,代表这几种情况不同的符号分别是:DLL_PROCESS_ATTACH、DLL_PROCESS_DETACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH。
书中提供的实例代码在DllMain
用switch语句来处理了不同情况。
符号 | 说明 |
---|---|
DLL_PROCESS_ATTACH | DLL也是有引用技计数的,第一次载入时,会用这个标志调用入口点函数,之后每次只是递增计数。这种情况下,进程创建时载入又主线程调用入口点函数,或者由调用LoadLibrary 的那个线程执行调用入口点函数。 |
DLL_PROCESS_DETACH | 如果是进程结束时,那么一般是主线程来调用入口点函数,如果是线程调用FreeLibrary 来卸载,那么从入口点函数返回后才能继续。 |
DLL_THREAD_ATTACH | 当创建一个线程,那么会对每个已载入的DLL都调用入口点函数。如果运行时加载一个DLL,则不会对加载DLL之前的已存在线程调用。 |
DLL_THREAD_DETACH | 每次线程终止,都会用该符号调用入口点。可能出现的情况:一个线程创建,然后某个DLL被加载,DLL又被卸载,为其用DLL_THREAD_DETACH 调用了入口点函数,却没有用DLL_THREAD_ATTACH调用入口点函数。 |
3.模块基址重定位
每个DLL和可执行文件都有一个首先基地址。如果一个程序加载很多DLL,基地址可能冲突,这时候就需要对某些DLL的基地址进行重定位。重定位的过程会修改DLL中的代码。
大概意思是打包DLL时,使用DLL的基址加上某个符号的相对虚拟地址,比如说基址是0x00400000,符号地址的相对虚拟地址是5,然后打包后DLL中这个符号的地址就是0x00400005,程序运行时DLL,加载到0x00400000,就通过0x00400005直接访问这个符号。
而重定位就是基址载入冲突后,修改某个DLL的基址(比如说0x00200000)并加载后,DLL这些符号的地址也需要根据新的基地址进行修改,如:0x00200005(也不知道这么理解对不对)。
接下来就是vs中提供了一个名为Rebase.exe.的工具,它接收一组映像文件名(DLL),模拟进程加载DLL的这一过程,模拟过程中以最优的方式,提前修改各个DLL的基址。
这样,在真正执行.exe的时候,就不会发生重定位的现象,也就不会去修改代码,提高了效率。
需要注意的是,那些随操作系统发布的DLL,不需要也不应该被传递到这个程序里面,因为他们的基址已经进行过重定位看。
4.模块绑定
vs提供一个Bind.exe的工具,它接收一个DLL,把一个符号的相对虚拟地址和基址相加,得到一个预期地址,直接写到导入段中,就不用导入时再根据实际的基地址计算了。
这个工具假设DLL不会被重定位(即DLL经过第三节那个工具提前重定位),会被直接加载到预期的基地址。
感觉和第三节的重定位理解有点冲突,要么就是我理解错了。或者说模块绑定这个东西会在DLL打包时被自动执行,那么第三节的理解就没错。
没有写的几节:延迟载入、函数转发器、DLL重定向(目录查找顺序相关)
第二十一章 线程局部存储区TLS
系统给定的一种统一的将数据和线程关联到一起的方式。
一般程序中都可以通过某些方式取代TLS,书中说一般在DLL中使用,但是没怎么看明白,就记一下使用方式吧。
分为动态TLS和静态TLS。
1.动态TLS
符号:TLS_MINIMUM_AVAILABLE。
进程中的每个线程,在TLS中都有一个无类型指针数组,索引范围是 0 ~ TLS_MINIMUM_AVAILABLE-1。
// 从那个无类型指针数组中取得可以用的索引
DWORD TlsAlloc();
// 通过索引设置一个指针到索引对应的位置
BOOL TlsSetValue(DWORD dwTlsIndex, PVOID pvTlsValue);
// 通过索引取得一个值,不存在就是NULL
PVOID TlsGetValue(DOWRD dwTlsIndex);
// 释放TlsAlloc中获得的那个索引
BOOL TlsFree(DWORD dwTlsIndex);
2.静态TLS
__declsped(thread) DOWRD gt_dwStartTime = 0;
这种形式只能被声明为全局变量或者静态变量。
原理是编译器会把TLS变量编导一个名为.tls的段中,链接时,所有对象模块的.tls合并成一个更大的.tls,然后放在DLL或.exe中。
系统加载程序时,查看.tls段,分配一块足够大的内存保存所有TLS变量,应用程序访问他们时,就会解析到这里。
2021年2月2日00:49:22
第二十二章 DLL注入和API拦截
这里只记录了一些思路概念之类的东西。
1.使用注册表注入DLL
在注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows中,有AppInit_DLLs和LoadAppInit_DLLs两个键。
AppInit_DLLs对应的是一个或一组DLL的文件名,通过空格来分隔。
LoadAppInit_DLLs对应的则是一个值。
这两个键顾名思义,AppInit_DLLs表示在初始化应用程序时要加载的DLL,LoadAppInit_DLLs表示是否开启这个行为。
我查看自己电脑上的注册表时,前者为空,后者为0。
要使用这种方式注入,在AppInit_DLLs条目中添加要注入的DLL路径名,并将LoadAppInit_DLLs值设为1即可。加载后传递DLL_PROCESS_ATTACH来调用DLL入口点函数。
这种方式会对所有的GUI程序生效。
2.使用windows挂钩注入DLL
突然发现没看明白这里,不写了,哎嘿。
HHOOK WINAPI SetWindowsHookEx(
int idHook, \\ 钩子类型
HOOKPROC lpfn, \\ 回调函数地址,在我们的地址空间中,
HINSTANCE hMod, \\ 标识一个DLL
DWORD dwThreadId); \\ 线程ID,标识线程,为0表示所有GUI线程都生效
3.远程线程注入DLL
本章中写的最明了最详细的就是这里了。
①用VirtualAllocEX函数在另一个进程中分配一块内存。
②用WrtieProcessMemory函数把DLL的路径名写道第一步分配的内存中。
③用GetProcAddress函数得到LoadLibraryW或LoadLibraryA的实际地址。
④用CreateRemoteThread函数在另一个进程中创建一个线程,启动函数是LoadLibraryW或LoadLibraryA,同时传入第一二步写入的DLL路径,来加载DLL,这时就会传递DLL_PROCESS_ATTACH来调用DLL入口点函数,要执行的代码放在入口点函数。
⑤用VirtualFreeEX释放第一步分配的内存。
⑥用GetProcAddress得到FreeLibrary函数的地址。
⑦用CreateRemot eThread启动FreeLibrary,来卸载DLL。
4.使用木马DLL来注入
如果进程必然会载入一个DLL,用一个同名的DLL替换掉。新的DLL中则导出了所有旧的DLL中的符号。
或者修改.exe的导入段,在导入段中替换掉那个要被替换的DLL的名称。
5.DLL作为调试器注入
…
6.使用CreateProcess注意
①进程创建一个被挂起的子进程。
②从.exe模块的头文件取得主线程的起始内存地址。
③把这个地址处的机器指令保存起来。
④强制将手写的机器指令写入到该内存地址处,这些之类调用LoadLibrary来加载DLL。
⑤让子进程恢复,执行这些代码。
⑥然后把保存的机器指令恢复原状。
⑦让后进程从起始地址开始执行。
7.覆盖代码拦截API
①在内存中对要拦截的函数定位,得到其内存地址。
②把这个函数起始的几个字节保存起来。
③用JUMP指令覆盖这几个字节,跳转到要替代的函数的地址。这个被跳转的地址需要与被拦截的函数的签名完全相同,返回值也是。
④这时调用这个函数就会跳转到我们的函数。
⑤要撤销拦截,把第二步保存的字节重新写回去。
8.修改导入段拦截API
一个模块的导入段包含一组DLL,导入段还包含一个符号表,列出了模块从各DLL导出的符号,当调用一个导入函数时,线程实际上先从导入表得到导入函数的地址,然后再跳转过去。
要拦截一个函数u,可以修改其在导入段中的地址。
2021年2月3日19:47:23
第二十三章~第二十六章关 异常与恢复
略
2021年2月4日19:06:59
更多推荐
《winodws核心编程》随笔
发布评论