win32api之进程的创建与使用(二)

什么是进程

在计算机操作系统中,进程是正在运行中的程序的实例。进程是操作系统进行资源分配和管理的基本单位,包括内存、文件句柄、系统状态等。每个进程都有自己的独立内存空间和运行状态,因此它们不会互相干扰,也不会互相影响。多个进程可以在操作系统上同时运行,每个进程都在自己的空间里执行自己的代码


进程内存空间的划分

在Windows X86环境下, 进程的内存空间通常被划分为以下三个区域

分区 描述 地址范围
用户模式区 用户模式区占据了进程地址空间的大部分,通常是4GB大小。它包含了程序代码、堆、栈、数据等,是进程中大部分数据的存放位置 0x00000000 ~ 0x7FFFFFFF (2GB)
系统保留区 由于内核区和用户模式区之间存在一定的限制和隔离,这个区域可以用来共享一些必要的数据,以便内核和用户模式的进程之间能够进行通信和数据共享 0x7FFE0000 - 0x7FFFFFFF (128KB)
内核区 内核模式区只占据了进程地址空间的一小部分,它通常包含了内核数据结构、操作系统代码和其他内核相关的资源。内核模式区对于应用程序来说是不可访问的,只能通过系统调用等方式来访问其中的数据和功能 0x80000000 ~ 0xFFFFFFFF (2GB)

进程的创建过程

首先有一点要清楚, 任何进程都是别的进程创建的, 而第一个进程是系统启动时由操作系统内核创建的, 它的进程ID为0, 称为系统空闲进程或系统进程

在Windows中,进程的创建可以使用CreateProcess函数,该函数会返回一个指向新进程的句柄

1.为进程分配内存空间,加载EXE文件, 并将其映射到内存

image-20230216194610827


2.创建进程内核对象EPROCESS

EPROCESS是Windows操作系统内核中的一种数据结构,它代表了一个进程对象。EPROCESS结构体包含了进程的许多信息,例如进程的PID、进程的线程列表、虚拟内存映射、访问权限、I/O访问权限等等

image-20230216194949260


3.映射系统dll(ntdll.dll)至内存

image-20230216195410482


4.创建线程内核对象ETHREAD

创建进程的时候系统会默认创建一个线程, 也就是说每个进程都至少有一个线程

image-20230216195620381


5.系统启动线程

线程启动之前, 还需要映射运行可执行文件所需的其他dll文件, 随后线程才开始执行

image-20230216200411778


进程涉及API

CreateProcess

CreateProcess函数用于创建一个新的进程并返回进程句柄, 其原型如下

BOOL CreateProcess(
  LPCTSTR lpApplicationName,  //需要运行的可执行文件名
  LPTSTR lpCommandLine,  //命令行参数字符串
  LPSECURITY_ATTRIBUTES lpProcessAttributes,  //进程的安全属性
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  //线程的安全属性
  BOOL bInheritHandles,  //是否继承父进程的句柄
  DWORD dwCreationFlags,  //进程标志 
  LPVOID lpEnvironment,  //新进程的环境变量,如果为NULL,则将使用当前进程的环境变量
  LPCTSTR lpCurrentDirectory,  //新进程的当前目录
  LPSTARTUPINFO lpStartupInfo,  //指向STARTUPINFO结构的指针,包含了启动进程时的窗口状态和标志等信息
  LPPROCESS_INFORMATION lpProcessInformation  //指向PROCESS_INFORMATION结构的指针,用于返回新进程的进程句柄和主线程句柄
);

如下是PROCESS_INFORMATION结构的成员

typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;  //进程句柄	
    HANDLE hThread;  //线程句柄
    DWORD dwProcessId;  //进程ID
    DWORD dwThreadId;  //线程ID
}

如下是STARTUPINFO结构的成员

typedef struct _STARTUPINFOW {
    DWORD   cb;   //结构体大小
    LPWSTR  lpReserved;  //保留,置为NULL
    LPWSTR  lpDesktop;  //指定进程的窗口站和桌面名称,或者是一个空字符
    LPWSTR  lpTitle; //指定进程的窗口标题
    DWORD   dwX;  //指定主窗口左上角的初始位置,以屏幕坐标表示
    DWORD   dwY;  
    DWORD   dwXSize;  //指定主窗口的宽度和高度,以像素表示
    DWORD   dwYSize;  
    DWORD   dwXCountChars;   //指定主窗口的字符宽度和字符高度
    DWORD   dwYCountChars;
    DWORD   dwFillAttribute;  //指定用于填充主窗口的初始颜色和属性
    DWORD   dwFlags;  //指定控制进程如何创建的一组标志
    WORD    wShowWindow;  //指定主窗口最初如何显示
    WORD    cbReserved2;  //保留成员
    LPBYTE  lpReserved2;  //保留成员
    HANDLE  hStdInput;  //标准输入设备的句柄
    HANDLE  hStdOutput;  //标准输出设备的句柄
    HANDLE  hStdError;  //标准错误设备的句柄
}

如下代码是一个创建进程的简单实例, 用于启动计算器(calc.exe)

include <stdio.h>
include <Windows.h>

//创建子进程函数,传递两个参数,分别是应用程序名字和程序命令行参数,若子进程创建成功则返回子进程句柄
DWORD CreateChildProcess(TCHAR ApplicationName[],TCHAR CommandLine[]=NULL) {
	STARTUPINFO si;  //进程启动信息
	PROCESS_INFORMATION pi;  //进程信息
	ZeroMemory(&si, sizeof(si));  //将结构体si的所有成员都初始化为0
	ZeroMemory(&pi, sizeof(pi));  //将结构体pi的所有成员都初始化为0
	si.cb = sizeof(si);  //结构体大小

	if (!CreateProcess(
		ApplicationName,  //要执行的应用程序名称(包含路径)
		NULL,  //命令行参数
		NULL,  //进程句柄不可被继承
		NULL,  //线程句柄不可被继承
		FALSE,  //不继承句柄
		0,  //标志位为0
		NULL,  //使用父进程的环境变量
		NULL,  //使用父进程的工作目录
		&si,  //传递启动信息
		&pi)  //传递进程信息
		) {
		printf("CreateProcess failed (%d).\n", GetLastError());  //打印错误信息
		return 0;
	}
	
	return (DWORD)pi.hProcess;  //返回进程句柄
	//return (DWORD)pi.dwProcessId;  //返回进程ID

	//释放进程句柄和线程句柄
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);
}


int main()
{
	TCHAR ApplicationName[] = TEXT("E:\\calc.exe");
	DWORD ProcessHandle = CreateChildProcess(ApplicationName);
	return 0;
}

若需要以挂起的形式创建进程, 可将CreateProcess函数的第六个参数设置为CREARE_SUSPEND, 这样创建的进程一开始并不会自动启动线程, 而是需要自己手动执行ResumeThread函数恢复线程后才会启动

include <Windows.h>
include <stdio.h>

int main()
{
	STARTUPINFO si;  //进程启动信息
	PROCESS_INFORMATION pi;  //进程信息
	ZeroMemory(&si, sizeof(si));  //将结构体si的所有成员都初始化为0
	ZeroMemory(&pi, sizeof(pi));  //将结构体pi的所有成员都初始化为0
	si.cb = sizeof(si);  //结构体大小

	TCHAR ApplicationName[] = TEXT("E://test.exe");  //test.exe是一个只输出"进程执行"的文件

	if (!CreateProcess(
		ApplicationName,  //要执行的应用程序名称(包含路径)
		NULL,  //命令行参数
		NULL,  //进程句柄不可被继承
		NULL,  //线程句柄不可被继承
		FALSE,  //不继承句柄
		CREATE_SUSPENDED,  //以挂起的形式创建进程
		NULL,  //使用父进程的环境变量
		NULL,  //使用父进程的工作目录
		&si,  //传递启动信息
		&pi)  //传递进程信息
		) {
		printf("CreateProcess failed (%d).\n", GetLastError());  //打印错误信息
		return 0;
	}

	for (int i = 0; i < 5; i++){
		printf("#######\n");
		Sleep(1000);

	}

	ResumeThread(pi.hThread);


	//释放进程句柄和线程句柄
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);
}

如上代码所示, 线程会在for循环打印代码执行结束后才会启动线程, 执行结果如下

image-20230221085734005

OpeoProcess

OpenProcess函数用于打开一个已存在的进程对象,以便对该进程执行操作,例如向该进程发送信号或从该进程读取内存。此函数的调用者必须具有足够的权限来打开目标进程

如果函数执行成功,返回打开进程的句柄,否则返回NULL,并可通过调用GetLastError函数获取错误码

要注意的是, 使用CloseHandle函数释放句柄后, 就不能再使用OpenProcess函数来打开这个进程了, 因为CloseHandle函数会将句柄从进程的句柄表中移除,并且在所有引用计数都归零之后释放内存资源

OpenProcess函数的语法如下:

HANDLE OpenProcess(
  DWORD dwDesiredAccess,  // 指定进程的访问权限,可取值为PROCESS_ALL_ACCESS或其他指定的进程访问权限常量
  BOOL bInheritHandle,    // 指定句柄是否可被子进程继承,TRUE表示可继承,FALSE表示不可继承
  DWORD dwProcessId       // 指定要打开的进程的进程ID
);

以下是dwDesireAccess参数的可取值:

  • PROCESS_ALL_ACCESS:具有完全访问权限的进程访问权限。
  • PROCESS_CREATE_PROCESS:允许创建新进程。
  • PROCESS_CREATE_THREAD:允许在进程中创建新线程。
  • PROCESS_DUP_HANDLE:允许进程使用 DuplicateHandle 函数复制句柄。
  • PROCESS_QUERY_INFORMATION:允许查询进程信息,如进程ID、进程优先级等。
  • PROCESS_QUERY_LIMITED_INFORMATION:允许查询受限信息,如进程ID、进程优先级、进程占用内存等。
  • PROCESS_SET_INFORMATION:允许设置进程信息,如进程优先级、进程AffinityMask等。
  • PROCESS_SET_QUOTA:允许设置进程的工作集大小和默认的硬错误模式。
  • PROCESS_SUSPEND_RESUME:允许挂起和恢复进程。
  • PROCESS_TERMINATE:允许终止进程。
  • PROCESS_VM_OPERATION:允许进行虚拟内存操作,如 VirtualAlloc、VirtualProtect 等。
  • PROCESS_VM_READ:允许读取进程的虚拟内存。
  • PROCESS_VM_WRITE:允许写入进程的虚拟内存

TeminateProcess

TerminateProcess函数是Windows操作系统提供的函数之一,用于终止指定进程, 当调用TerminateProcess函数时,会向指定进程发送一个中断信号,强制其终止。

这个过程是非常暴力的,会直接终止进程的所有线程,不会给进程和线程任何清理资源的机会,因此使用该函数需要非常慎重

函数定义如下:

BOOL TerminateProcess(
  HANDLE hProcess,  //进程句柄,用于标识被终止的进程
  UINT   uExitCode  //进程的退出代码,表示进程退出的原因,可以随意填写
);

GetModuleFileName

GetModuleFileName函数用于获取指定模块的完整路径名。通常情况下,可以通过指定NULL作为参数hModule,来获取当前应用程序的完整路径名,该函数的声明如下:

DWORD GetModuleFileName(
  HMODULE hModule,  // 模块句柄,指定NULL表示获取当前应用程序的路径名
  LPTSTR lpFilename,  // 接收完整路径名的缓冲区
  DWORD nSize  // 缓冲区大小
);

GetCurrentDirectory

GetCurrentDirectory函数用于获取当前进程的工作目录。其函数原型为

DWORD GetCurrentDirectory(
  DWORD  nBufferLength,  // 缓冲区大小,单位为字节
  LPTSTR lpBuffer        // 存储路径的缓冲区
);

这个函数的路径是指当前进程的工作目录,而不是当前模块的目录。如果需要获取当前模块的目录,需要使用GetModuleFileName函数来获取模块文件的路径

include <Windows.h>
include <stdio.h>

int main()
{	
    //获取当前模块的完整路径
	char str1[256];
	GetModuleFileName(NULL, str1, 256);
	printf("当前应用程序路径名:%s\n", str1);

	//获取当前应用程序的工作目录
	char str2[256];
	GetCurrentDirectory(256, str2);
	printf("当前程序工作目录:%s", str2);

	return 0;
}

image-20230220162026966


GetStartupInfo

GetStartupInfo函数用于检索当前进程的启动信息,它的主要功能是获取STARTUPINFO结构体,其中包含了进程的启动信息,如命令行参数、标准输入输出句柄、窗口显示方式等

调用GetStartupInfo函数需传递一个指向STARTUPINFO类型的指针

int main()
{
// 定义一个 STARTUPINFO 结构体变量 si
STARTUPINFO si;

// 使用 ZeroMemory 函数将 si 清零
ZeroMemory(&si, sizeof(si));

// 设置 si 的 cb 字段
si.cb = sizeof(si);

// 使用 GetStartupInfo 函数获取启动信息
GetStartupInfo(&si);

// 输出当前进程窗口状态
printf("Show window command: %d\n", si.dwFlags);

// 返回 0,表示程序执行成功
return 0;
}

GetCurrentProcessID

GetCurrentProcessID函数是Windows API中的一部分,它返回当前进程的进程ID(Process ID, 其返回值是一个无符号长整型

其语法格式如下:

GetProcessId(
    _In_ HANDLE Process
);

GetCurrentProcess

GetCurrentProcess函数是Windows API提供的一个函数,用于获取当前进程的句柄。该函数没有任何参数,调用后将返回一个类型为HANDLE的句柄,该句柄指向当前进程

GetCurrentProcess 获取当前进程的一个伪句柄GetCurrentProcess 总是返回-1(即0xFFFFFFFF),代表当前进程。这个句柄不在句柄表中,不是真正的句柄,所以叫伪句柄


EnumProcesses

EnumProcesses函数是Windows API中的一个函数,用于列举当前正在运行的进程的ID号,通常用于获取系统中所有进程的ID号列表。若函数执行成功则返回TRUE, 否则返回FLASE, 它的声明如下:

BOOL EnumProcesses(
  DWORD  *pProcessIds, // 接收进程ID的缓冲区
  DWORD  cb,           // 缓冲区大小(以字节为单位)
  DWORD  *pBytesReturned  // 实际写入缓冲区的字节数
);

如果包含了Windows.h头文件, 还提示"EnumProcesses"未定义标识符, 请检查是否正确链接了psapi.lib库文件。

可以通过在Visual Studio中转到“项目”菜单,然后选择“属性”来查看和配置链接器选项。在属性页面的左侧选择“链接器”,然后选择“输入”。在“附加依赖项”字段中添加“Psapi.lib”,然后包含头文件:include "psapi.h"

如下实例枚举当前系统运行的所有进程ID, 并打印至控制台

include <Windows.h>
include <stdio.h>
include "psapi.h"

define ARRAY_SIZE 1024

int main()
{
	DWORD aProcesses[ARRAY_SIZE], cbNeeded, cProcesses;
	if (!EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded))
	{
		printf("EnumProcesses failed: %d\n", GetLastError());
		return 1;
	}

	// 计算枚举到的进程数
	cProcesses = cbNeeded / sizeof(DWORD);

	// 打印进程ID
	for (DWORD i = 0; i < cProcesses; i++)
	{
		if (aProcesses[i] != 0)
		{
			printf("Process ID: %u\n", aProcesses[i]);
		}
	}

	return 0;
}

image-20230223151412794


CreateToolhelp32Snapshot

CreateToolhelp32Snapshot函数是Windows系统提供的一个快照函数,可以获取系统中当前正在运行的进程和线程的快照。该函数可以通过枚举系统中所有进程和线程来帮助实现进程和线程的监控和管理。在调用该函数时,需要指定快照类型,如进程快照、线程快照等。函数会返回一个句柄,该句柄可以作为参数传递给其他Tool Help函数,以获取有关系统中进程和线程的详细信息。在使用完成后,需要调用CloseHandle函数关闭句柄

如下实例使用CreateToolhelp32Snapshot函数枚举系统所有进程:

include <windows.h>
include <tlhelp32.h>
include <tchar.h>
include <iostream>

int main()
{
    HANDLE hProcessSnap;
    PROCESSENTRY32 pe32;

    // 获取系统进程快照
    hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnap == INVALID_HANDLE_VALUE) {
        std::cout << "CreateToolhelp32Snapshot failed: " << GetLastError() << std::endl;
        return 1;
    }

    // 设置pe32结构体的大小,否则Process32First/Next函数会失败
    pe32.dwSize = sizeof(PROCESSENTRY32);

    // 获取第一个进程的信息
    if (!Process32First(hProcessSnap, &pe32)) {
        std::cout << "Process32First failed: " << GetLastError() << std::endl;
        CloseHandle(hProcessSnap);
        return 1;
    }

    // 遍历进程列表,输出每个进程的PID和名称
    do {
        _tprintf(TEXT("PID=%d, Name=%s\n"), pe32.th32ProcessID, pe32.szExeFile);
    } while (Process32Next(hProcessSnap, &pe32));

    // 关闭进程快照句柄
    CloseHandle(hProcessSnap);

    return 0;
}

posted @ 2023-03-19 20:34  亨利其实很坏  阅读(178)  评论(0编辑  收藏  举报