句柄表
在上一章节中,我们用代码创建了一个进程,在CreateProcess函数中,最后一个参数
[out] LPPROCESS_INFORMATION lpProcessInformation // process information
表示的是进程和线程相关的东西,进程句柄、线程句柄、进程ID、线程ID,要想了解其中的信息,要先学习学习下句柄表
在学句柄表之前,还要先认识一下什么是内核对象
像进程、线程、文件、互斥体、事件等在内核都有一个对应的结构体,这些结构体由内核负责管理。我们管这样的对象叫做内核对象
如图,这个就是内核对象,当然不止这么点,要想知道有哪些内核对象,可在MSDN里查,方法如下
在索引里搜索closehandle,这个是关闭句柄的意思,找到Remarks
这里列出的就是内核对象
如何管理内核对象
如图,假如有个进程,对应一个内核对象,即整个绿色的部分对应红色的内核对象,然后在这个进程里我又创建了四个内核对象,那么在内核中又产生了四个对应的结构体,了解了之后,这些怎么来管理呢,或者换句话说怎么使用呢?
以CreateProcess为例,在平常可能会想到不就是把内核里的结构体的地址返回给应用层,不就行了吗?
但是有个问题,如果应用层不小心把地址给改了,刚好又是内核的地址,是以0x80000000开始的地址,就会出现内存访问错误的问题,导致直接蓝屏,因为这是零环的内存。所以微软不会把内核层的地址直接暴露给应用层,那么怎么解决呢?
通过表的方式
如图右下角的表,这就是句柄表。
可以看到,这里只有一张句柄表,说明不是一个内核对象就有一张句柄表,而是一个进程就有一张句柄表
这张表就是存的结构体的地址,那应用层怎么用呢,就用编号,比如说,当应用层想用A,就返回1,想用B就返回2。
说白了,句柄表就是一种映射关系,用户层的索引就是内核层的地址
代码演示
继续用上次的代码
//环境:win XP VC++ 6.0
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
//启动参数的相关信息
void GetStartInfo()
{
STARTUPINFO si;
GetStartupInfo(&si);
printf("%X %X %X %X %X %X %X %X\n", si.dwX, si.dwY, si.dwXCountChars, si.dwYCountChars, si.dwFillAttribute, si.dwXSize, si.dwYSize, si.dwFlags);
}
BOOL CreateChildProcess(PTCHAR ChildProcessName, PTCHAR CommandLine)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
//创建子进程 返回是否成功
if (!CreateProcess(
ChildProcessName, //对象名称的完整路径
CommandLine, //命令行参数
NULL, //不继承进程句柄
NULL, //不继承线程句柄
FALSE, //不继承句柄
0, //没有创建标志
NULL, //使用父进程环境变量
NULL, //使用父进程目录作为当前目录,可以自己设置目录
&si, //STARTUPINFO结构体
&pi) //PROCESS_INFORMATION结构体
)
{
printf("创建进程错误 代码:%d\n", GetLastError());
return FALSE;
}
//printf("进程句柄:%X\t进程ID:%X\n线程句柄:%X\t线程ID:%X\n", pi.hProcess,pi.dwProcessId, pi.hThread,pi.dwThreadId);
SuspendThread(pi.hThread); //挂起指定的线程(下断点)
ResumeThread(pi.hThread); //递减线程的挂起计数,当暂停计数减为零时,将恢复线程的执行
//释放句柄
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return TRUE;
}
int main(int argc,char* argv[])
{
//C:\\Program Files\\Internet Explorer\\iexplore.exe
//C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe
TCHAR ApplicationName[] = TEXT("C:\\Program Files\\Internet Explorer\\iexplore.exe");
TCHAR CmdLine[] = TEXT(" https://www.baidu.com");
CreateChildProcess(ApplicationName, NULL);
//GetStartInfo();
//getchar();
return 0;
}
如代码所示,执行会发现跟平常一样,可以操作,放大缩小等,当我们在单步执行一下,会发现不动了,点不开了等,想要恢复的话在执行一步
说明一下,SuspendThread()函数要传一个线程句柄,下面的函数也是一样,具体的话请自行查资料
我们了解了句柄的作用,和所谓的内核对象都在内核有一个结构在零环,还知道这个零环是所有进程共用的内存,既然这样,那意味着内核对象是可以跨进程共享的
多线程共享一个内核对象
如图,首先A进程有个内核对象,然后A进程里又创建一个内核对象即为A,B进程有个内核对象,如果B进程里是CreateProcess,那就跟A进程没半毛钱关系了,但是如果是OpenProcess函数的话,他的作用是打开一个别人创建好了的进程的内核对象,具体的参数可自行查阅资料了解
这样的话,AB两个进程共用了同一份的内核对象,那A进程怎么使用A呢,用句柄表,当然是A进程的,那B呢?用B进程的句柄表
由此可知,句柄表是一个私有的概念,仅对当前的相对应的进程有意义
A的句柄表的索引是1,而B的句柄表的索引是10,所以更能说明句柄表是私有的概念,如果都是1的索引的话,那B进程的原来的索引为1的对像,就冲突了,所以如果我把A句柄的值传给B,有意义吗?显然没有意义
接下来再说A对象里的2是什么意思,这个2代表一个计数器,比如,A进程创建了这个对象,那么这个计数器就+1,然后B进程又打开了这个对象,那么这个计数器在+1,如果再有人打开,就在+1,以此类推
如果这个句柄我不想用了,就可以关闭这个句柄,用CloseHandle()来执行
CloseHandle(pi.hProcess);
看名字会觉得是直接把这个对象给关掉,实际上只是把这个计数器-1,比如说,我把A进程的句柄关闭了,计数器只是-1,还是1(原来是2),这个内核对象(注意我说的是内核对象)不会死掉,那怎么才死呢?
那我B进程在调用一个CloseHandle,此时这里的计数器变成了0,此时,这个内核对象变成了没有任何人指向的对象,那么就会死掉
说白了,只要计数器不为0,那这个内核对象就不会死掉
上面说的这些特征,适合于上面提到过的其他所有的内核对象,但是有一个例外,那就是线程
我们在回到MSDN里去看看
Closing a thread handle does not terminate the associated thread. To remove a thread object, you must terminate the thread, then close all handles to the thread.
翻译过来的意思是:关闭线程句柄不会终止相关联的线程。要移除线程对象,必须先终止线程,然后关闭所有指向该线程的句柄
什么意思呢,就是线程也是一个内核对象,所以在内核中也有一个结构体,必须要满足1.把线程关掉2.线程的内核对象的计数器为0,这两个条件必须同时成立,才能关掉线程的内核对象
实验
还是上面的代码,就不贴上去了
SuspendThread(pi.hThread); //挂起指定的线程(下断点)
ResumeThread(pi.hThread); //递减线程的挂起计数,当暂停计数减为零时,将恢复线程的执行
这两个函数注释掉,执行,你会发现会打开一个网页,在回到代码里,发现
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
我们已经执行了,却发现这个网页还能运作,就是因为里面的线程还没有死掉,所以能运作
这里CloseHandle(pi.hThread);只是让计数器-1,变成0了,但是线程没有关闭,所以线程还活着
换句话说,只要线程不死,那这个进程就不会死,进程的唯一线程死了,那这个进程也就死了
想让他死掉的话,点击关闭网页就可以了,线程死了,计数器清零,内核对象被销毁,噶了
句柄是否可以被继承
上面讲过了怎么识别是不是内核对象,还有个方法
以CreateEvent为例,要知道他是不是,在MSDN里看看
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // SD
BOOL bManualReset, // reset type
BOOL bInitialState, // initial state
LPCTSTR lpName // object name
);
看到有LPSECURITY_ATTRIBUTES lpEventAttributes, // SD的,那就是内核对象
再看看CreateFile
HANDLE CreateFile(
LPCTSTR lpFileName, // file name
DWORD dwDesiredAccess, // access mode
DWORD dwShareMode, // share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // SD
DWORD dwCreationDisposition, // how to create
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile // handle to template file
);
一样的有
回到CreateEvent()的定义
LPSECURITY_ATTRIBUTES lpEventAttributes
这里就只用知道这是个安全描述符(结构体指针),点击SECURITY_ATTRIBUTES (安全属性,也是个结构体指针),由于我的MSDN有点问题,就直接手写他的结构体
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES;
nLength
存储当前结构体大小的值
lpSecurityDescriptor
指向安全描述符的指针,意思为不同用户被赋予了何种权限。在我们写代码的时候通常不用去特别关注他,他默认采用的安全设置就跟你的父进程是一样的
bInheritHandle
确定在创建新进程时是否继承返回的句柄。如果此字段设置为非零值,则新进程将继承句柄。如果为 0,则新进程不会继承句柄。
介绍完后,所以如果不继承的话那就
CreateEvent(NULL,FALSE,FALSE,NULL)
(先不管其他参数的意思)
如果要继承,那就要创建SECURITY_ATTRIBUTES结构体
SECURITY_ATTRIBUTES sa;
ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES));//初始化操作
sa.nLength = sizeof(SECURITY_ATTRIBUTES);//结构体大小
sa.bInheritHandle = TRUE;//可以继承
CreateEvent(sa,FALSE,FALSE,NULL);
这样就能继承了,因此,为了彰显句柄表是否能够继承,那就应该还要在添加一个成员,就是能否被继承的标志
句柄表的0代表不继承,1代表继承
如果是父子进程的话,那么子进程是有机会直接继承过去的
比如说,在创建进程时
if (!CreateProcess(
ChildProcessName, //对象名称的完整路径
CommandLine, //命令行参数
NULL, //不继承进程句柄
NULL, //不继承线程句柄
FALSE, //不继承句柄
0, //没有创建标志
NULL, //使用父进程环境变量
NULL, //使用父进程目录作为当前目录,可以自己设置目录
&si, //STARTUPINFO结构体
&pi) //PROCESS_INFORMATION结构体
FALSE, //不继承句柄
这里我写的是FALSE,子进程是IE,父进程的句柄表跟子进程没有关系,也就是说子进程会有一个独立的句柄表,哪怕父进程里有可以的继承的,也没有任何关系
如果改成TRUE,那子进程就会得到父进程中允许继承的句柄表
这样子进程就可以直接使用父进程的句柄了
NULL, //不继承进程句柄
NULL, //不继承线程句柄
再看进程与线程
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
如果那个需要继承,就把那个定义出来,写成TRUE
总结
- 每个进程对应一个内核对象
- 有两种方式来共享内核
这里的知识有点绕,需要多多消化,查阅资料