赞
踩
一些自己实现的代码的环境都是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.对作业添加限制 & 查询关于作业的一些信息
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/262826
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。