Windows API笔记(一)内核对象
Windows API笔记(二)进程和进程间通信、进程边界
Windows API笔记(三)线程和线程同步、线程局部存储
Windows API笔记(四)win32内存结构
Windows API笔记(五)虚拟内存
Windows API笔记(六)内存映射文件
Windows API笔记(七)堆
Windows API笔记(八)文件系统
Windows API笔记(九)窗口消息
Windows API笔记(十)动态链接库
Windows API笔记(十一)设备I/O
1. 内核对象和句柄
内核对象实际上是由内核分配的一块内存,而且只能由内核来访问。内核对象属于内核;句柄与进程相关,能被允许在该进程的所有线程正确使用!
内核对象内存块是一个数据结构,他的组成:
- 对象信息
- 对象名
- 安全描述
- 使用计数
- …
内核对象 | |
---|---|
Event objects | 事件对象 |
File-mapping objects | 文件映射对象 |
File objects | 文件对象 |
Mailslor objects | 邮件槽对象 |
Mutex objects | 互斥对象 |
Pipe objects | 管道对象 |
Semaphore objects | 信号量 |
Process objects | 进程对象 |
Thread objects | 线程对象 |
- 内核对象数据结构只能由内核访问(安全)
- 创建内核对象时返回一个标识该对象的句柄,进程通过句柄操作内核对象;该句柄与进程关联,无法使用非本进程创建的句柄
1.1 使用计数与内核对象释放
- 创建内核对象的进程终止不一定会释放内核对象;
- 内核对象创建时,使用计数设为1;
- 当另一进程也访问该对象时,使用计数+1;
- 当进程终结时,内核会自动将该进程使用的所有内核对象使用计数-1;
- 当内核对象使用计数为0时,内核就会释放该对象;
1.2 安全与安全描述符
- 内核对象能被一个安全描述符保护;
- 安全描述符描述了谁创建了该对象,谁能访问或使用该对象,谁对该对象的访问要被拒绝;描述对象的读写及执行权限;
- 缺省的安全属性指管理员和对象的创建者对它有完全的访问权限,其他都不能访问该对象;
- 在创建内核对象时可以通过传入SECURITY_ATTRIBUTES参数设置安全描述符;
- 在获取(open)已存在的内核对象的访问时,需指定将要对内核对象进行的操作,如果访问被允许将返回有效句柄,否则将返回NULL,通过GetLastError就会返回5或ERROR_ACCESS_DENIED;
- 在创建时释放需要指定SECURITY_ATTRIBUTES参数可判断是创建内核对象还是应用程序对象;
- 进程在使用内核对象前必须获得许可;
2. 进程的内核对象句柄表
句柄表结构:
索引指向内核对象内存块的指针 | 访问掩码 | 标志(是否可继承等) |
---|---|---|
1 | 0x??? | 0x??? |
2 | 0x??? | 0x??? |
- 进程初始化时,系统会分配一张空句柄表;
- 当进程创建了内核对象时,并将句柄数据(不单单是句柄值,所以句柄与进程相关)添加至句柄表;
- 进程需要根据句柄表中的数据在内核中查找内核对象;
- 不管内核对象是如何创建的,都是使用CloseHandle关闭;关闭时句柄表移除该句柄,内核对象使用计数-1;
- 如果忘记调用CloseHandle可能会发生内存泄漏,但是在进程终结时将释放所有资源;
- 句柄标识为可继承时,子进程将拥有父进程的句柄;
3. 进程间共享内核对象
- 文件映射(mmp)对象允许允许在同一计算机上的两个进程共享数据;
- 邮件槽和网络套接字允许应用程序在联网的计算机的进程之间传递数据;
- 互斥量、信号量(semaphore)和事件允许不同进程中的线程同步它们的行动
3.1 对象句柄继承
当进程存在父子关系时才能使用对象句柄继承。
子进程要继承父进程的内核对象权限,父进程必须执行若干步操作:
BOOL CreateProcess(
_In_opt_ LPCSTR lpApplicationName, // 可执行模块名称,绝对路径,相对路径,或为NULL(需在 lpCommandLine 第一个参数指定路径)
_Inout_opt_ LPSTR lpCommandLine, // 命令行参数,可指定执行模块名称,若不指定执行模块名称则需以空格开头
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, //进程安全性,决定新进程是否被标记为可被继承(写入父进程的句柄表,并标记可继承)
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全性,决定新线程是否可被继承,SECURITY_ATTRIBUTES.bInheritHandle = true(写入父进程的句柄表,并标记可继承)
_In_ BOOL bInheritHandles, //需要创建的子进程是否从父进程继承内核句柄,从父进程中拷贝句柄表中可被继承的句柄至子进程的句柄表(TRUE 继承)(为什么句柄不能被非子进程使用,因为无父子关系的进程没有获得句柄表)
_In_ DWORD dwCreationFlags, // 进程创建标识(CREATE_NEW_CONSOLE为新进程创建一个新的控制台窗口)
_In_opt_ LPVOID lpEnvironment, //设置进程环境变量,NULL则继承父进程的环境变量
_In_opt_ LPCSTR lpCurrentDirectory, //子进程的工作目录
_In_ LPSTARTUPINFOA lpStartupInfo, //指向一个用于决定新进程的主窗体如何显示的STARTUPINFO结构体。
_Out_ LPPROCESS_INFORMATION lpProcessInformation //指向一个用来接收新进程的识别信息的PROCESS_INFORMATION结构体。
);
- 父进程创建内核对象时,必须告诉系统它想让该对象的句柄能够被继承,Create*(内核对象创建函数)时传入SECURITY_ATTRIBUTES参数,并设置bInheritHandle=true;
- 创建子进程CreateProcess函数的设置参数bInheritHandle=true,子进程将拷贝父进程句柄表中可被继承的句柄至子进程句柄表,并且内核对象的使用计数+1;
- 将句柄参数传递至子进程,即可在子进程中使用父进程中获得的内核对象句柄;
示例:
//kernel_handle_inherit.cpp
/*
内核对象句柄继承:
当进程存在父子关系时才能使用对象句柄继承,子进程将会继承父进程句柄表,但是只会初始化被继承的句柄
CreateProcess函数的bInheritHandles参数可以设置子进程是否继承父进程句柄表内可被继承的句柄
要被子进程继承需两个条件:
1. 句柄对象在创建时设置安全描述符为可被继承
2. 创建子进程时设置为继承父进程句柄表
*/
/*
父子进程共享句柄示例:
1. 父进程中创建了一个文件句柄,并设置为可被继承,在句柄中写入数据
2. 创建一个子进程,并设置为继承父进程的句柄表,通过命令行将句柄参数传递至子进程
3. 在子进程中得到句柄参数,并使用句柄在文件中写入数据
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h> //包含了所有的windows api头文件
int main(int argc, char **argv)
{
//文件句柄
HANDLE file_hnd = NULL;
if (argc < 2) //无命令行参数,父进程
{
//安全描述
SECURITY_ATTRIBUTES sa;
sa.bInheritHandle = true; //可被继承(若不设为可被继承,创建子进程时将会拷贝句柄表内该至子进程,但不能继承的句柄将不会被初始化,不可用)
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
file_hnd = CreateFile( //创建文件句柄
"test.txt", //文件名
GENERIC_READ | GENERIC_WRITE, //读写权限
FILE_SHARE_READ | FILE_SHARE_READ, //共享权限
&sa, //安全性描述
OPEN_ALWAYS, //如何创建,OPEN_ALWAYS 文件不存在则创建,存在则打开
FILE_ATTRIBUTE_NORMAL, //文件属性
NULL //模板文件句柄,即将文件拷贝至设置的文件名
);
if (INVALID_HANDLE_VALUE == file_hnd) //打开失败
{
perror("CreateFile Error");
exit(1);
}
char *buf = "i'm father process\n";
DWORD realnum = 0;
//
bool ret = WriteFile( //写文件
file_hnd, //文件句柄
buf, //写入的字符串
strlen(buf), //写入的字符串长度
&realnum, //(输出参数)实际写入的长度
NULL //OVERLAPPED结构体指针,如果文件是以FILE_FLAG_OVERLAPPED方式打开的话,那么这个指针就不能为NULL
);
if (ret)
{
printf("write success,write size %ld\n", realnum);
}
else
{
perror("WriteFile Error");
}
char cmd[200];
// cmd若不带执行模块名,则开始需要输入空格
sprintf(cmd, " %d", (int)file_hnd);
//安全性描述符
SECURITY_ATTRIBUTES sa2;
sa2.bInheritHandle = true; //在句柄表内标记为可继承
sa2.nLength = sizeof(sa2);
sa2.lpSecurityDescriptor = NULL;
//
STARTUPINFO si;
memset(&si, 0, sizeof(STARTUPINFO)); //初始化si在内存块中的值
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = TRUE;
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));
char exec_file[] = TEXT("kernel_handle_inherit.exe"); // 执行程序名称,相对路径
if (!CreateProcess(exec_file, //执行模块
cmd, //命令行参数
NULL,
NULL,
TRUE, //继承父进程的句柄表
CREATE_NEW_CONSOLE, // 为新进程创建一个新的控制台窗口
NULL,
NULL,
&si,
&pi))
{
perror("CreateProcess Error");
exit(1);
}
else
{
printf("write success\n");
// 等待子进程结束(取下下面三行的注释将会等待子进程关闭)
// WaitForSingleObject(pi.hProcess, INFINITE);
// 既然我们不使用两个句柄,最好是立刻将它们关闭
// ::CloseHandle(pi.hThread);
// ::CloseHandle(pi.hProcess);
printf(" new process ID: %d \n", pi.dwProcessId);
printf(" new process's new thread ID: %d \n", pi.dwThreadId);
}
printf("main process exit\n");
}
else // 子进程
{
int lhnd = atoi(argv[1]); // 第二个参数为句柄参数
file_hnd = (HANDLE)lhnd;
char *buf = "next is child process input : \n";
DWORD realnum = 0;
bool ret = WriteFile(file_hnd, buf, strlen(buf), &realnum, NULL);
if (ret)
{
printf("write success,write size %ld \n", realnum);
char buf[200];
memset(buf, 0, sizeof(buf));
char input[200];
memset(input, 0, sizeof(input));
// 输入q退出
while ('q' != input[0] && ret)
{
printf("please input :\r\n");
scanf("%s", input);
sprintf(buf,"%s\r\n",input);
ret = WriteFile(file_hnd, buf, strlen(buf), &realnum, NULL);
if (ret)
{
printf("write success,write size %ld \n", realnum);
}
else
{
perror("WriteFile Error");
}
}
}
else
{
perror("WriteFile Error");
}
printf("child process exit\n");
system("pause");
}
return 0;
}
3.2 改变句柄标识
创建句柄的时候设置为可被继承,但是现在不想被继承了。
//设置句柄标识
BOOL SetHandleInformation(
HANDLE hObject, //句柄
DWORD dwMask, /*
HANDLE_FLAG_INHERIT 继承 dwFlags: 1 可继承,0 不可继承
HANDLE_FLAG_PROTECT_FROM_CLOSE 保护不被关闭,在程序运行期间不能被CloseHandle关闭,dwFlags: 1 yes,0 no
*/
DWORD dwFlags
);
// 获取句柄标识
DWORD dwFlags;
GetHandlwInformation(hObj,&dwFlags);
BOOL fHandleInheritable = (0!=(dwFlags & HANDLE_FLAG_INHERITABLE));
3.3 命名对象
如果能直接通过名字找内核对象,那就更方便了,很可惜不是全部内核对象都可以命名。
以下几种内核对象都能调用Create和Open时通过名称创建或获取命名对象:
- Mutex
- Event
- Semaphore
- WaitableTimer
- FileMapping
所有这些参数的第一个参数和最后一个参数都一样,下面以CreateMutex和OpenMutex举例:
/*
如果不存在名称为lpName的内核对象,则会创建新对象;若存在名称为lpName,且类型相同则返回存在的内核对象,其他设置参数都被忽略;存在同名但不同类型的内核对象则返回NULL,GetLastError()返回ERROR_INVALID_HANDLE错误
*/
HANDLE CreateMutex(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCSTR lpName //NULL 创建无名对象,其他:名称
);
/*
打开已存在的命名对象,不存在则返回NULL,GetLastError()得到ERROR_FILE_NOT_FOUND(2)
*/
HANDLE OpenMutex(
_In_ DWORD dwDesiredAccess, //访问是否被允许
_In_ BOOL bInheritHandle,
_In_ LPCSTR lpName
);
3.4 复制句柄对象
上面这些都太麻烦了,内核对象是在内核里,是在一个公共空间,为什么就不能我想要哪个就用哪个?当然有办法,你有这个句柄对象,我去你那里抄个地址来,我也可以去找这个对象了。
// 将一个进程的句柄拷贝至另一个进程
BOOL DuplicateHandle(
_In_ HANDLE hSourceProcessHandle, //内核对象所在的源进程句柄
_In_ HANDLE hSourceHandle, //源进程的内核对象句柄值
_In_ HANDLE hTargetProcessHandle, //目的进程
_Outptr_ LPHANDLE lpTargetHandle, //目的进程的句柄变量(输出参数)
_In_ DWORD dwDesiredAccess, //
_In_ BOOL bInheritHandle, //句柄是否能被继承
_In_ DWORD dwOptions /*
DUPLICATE_CLOSE_SOURCE 关闭源进程中的句柄
DUPLICATE_SAME_ACCESS 目的句柄的访问掩码同源进程的句柄(在句柄表中位置完全相同),将忽略dwDesiredAccess
*/
);