《Windows核心编程》读书笔记四 进程

本文详细介绍了Windows操作系统中进程的概念,包括进程的实例句柄、命令行、环境变量、关联性、错误模式、当前驱动器和目录等。还探讨了CreateProcess函数的使用,以及如何创建、终止进程和管理子进程。此外,文章还讨论了管理员权限运行时的情况,如何提升进程权限以及枚举系统运行的进程。
摘要由CSDN通过智能技术生成

第四章 进程


本章内容


4.1 编写第一个Windows应用程序

4.2 CreateProcess函数

4.3 终止进程

4.4 子进程

4.5 管理员以标准用户权限运行时


进程定义为一个正在运行的程序的一个实例,它可以由一下两部分构成。

a. 一个内核对象,操作系统用它来管理进程。内核对象也是系统保存进程统计信息的地方。

b.一个地址空间,其中包含所有可执行文件或DLL模块的代码和数据。还包含动态内存分配,比如线程栈和堆的分配。


进程是有“惰性”的,进程要做任何事情,都必须让一个线程在他的上下文(环境 内存地址空间等)中运行。该线程执行进程地址空间包含的代码。

进程可以包含多个线程,所有线程都在进程的地址空间中“同时”执行代码。每个线程有自己的堆栈和自己的一组CPU寄存器

系统创建进程的时候会创建一个主线程。然后由主线程再创建更多的子线程。

如果没有线程要继续执行的代码,进程就失去了存在的理由。系统回自动销毁进程和其地址空间。


操作系统会轮流为每一个线程调度一些cpu时间。它采取循环(round-robin,轮询或轮流)方式,为每个线程都分配时间片(称为“量”或者“量程”)从而营造出并发的假象。

如果计算机装载多cpu或者(多核cpu),操作系统会采用更复杂的算法为线程分配cpu时间。




4.1 编写第一个Windows应用程序

Windows支持两种类型的应用程序,GUI(Graphic User Interface)和CUI(Console User Interface)


事实上CUI程序也能显示出图形界面。也可以在一个GUI程序中像控制台输出文本。

GUI和CUI程序在VS中主要取决于连接器的设置。 CUI /SUBSYSTEM:CONSOLE

GUI :/SUBSYSTEM:WINDOWS


用户运行应用程序时,操作系统加载程序(loader)会检查可执行文件映像的文件头,并获取这个子系统值。然后进行相应的加载(开启一个命令行窗口 或是创建一个主窗口)

等程序运行以后,操作系统就不再关心是CUI还是GUI了。

Windows应用程序的入口函数

INT WINAPI _tWinMain(
	_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPSTR lpCmdLine,
	_In_ int nShowCmd);

int _tmain(
	_In_ int _Argc,
	_In_reads_(_Argc) _Pre_z_ wchar_t ** _Argv,
	_In_z_ wchar_t ** _Env);


操作系统本身并不会调用入口函数main和Winmain 而是调用C/C++运行实现并在连接时使用-entry:命令选项来设置的一个C/C++运行时的启动函数。

该函数初始化C/C++运行库,使我们可以调用malloc free之类的函数。 还确保在代码执行前任何全局和静态的C++对象都被正确构造。


连接器选择正确的C/C++运行库启动函数。如果指定SUBSYSTEM:WINDOWS 连接器就会寻找WinMain

如果没有找到WinMain则返回“unresolved external symbol”

如果选择/SUBSYSTEM:CONSOLE 连接器默认会寻找main或者wmain 如果找不到则返回"unresolved external symbol"

可以自行关闭SUBSYSTEM连接器开关,让连接器自动判断(入口是main 还是winmain)。


可以从VC++自带的运行库的源代码 crtexe.c文件中找到4个启动函数的源代码。所有启动函数的用途简单总结如下

1)获取指向新进程的完整命令行的一个指针

2)获取指向新进程的环境变量的一个指针

3)初始化C/C++运行库的全局变量。如果保含了Stdlib.h 就可以访问这些变量。

4)初始化C运行库内存分配函数(malloc 和 calloc)和底层的I/O例程使用的堆

5)调用所有全局和静态C++类对象的构造函数


完成以上所有初始化以后,C/C++启动函数就会调用应用程序的入口函数。

如果我们定义了Unicode C/C++标准库将执行以下代码

	STARTUPINFO StartupInfo;
	GetStartupInfo(&StartupInfo);
	int nMainRetVal = wWinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineUnicode,
		(STARTUPINFO.dwFlags & STARTF_USESHOWWINDOW)
		? STARTUPINFO.wShowWindow : SW_SHOWDEFAULT);

如果没有定义Unicode则调用过程如下

	STARTUPINFO StartupInfo;
	GetStartupInfo(&StartupInfo);
	int nMainRetVal = WinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineAnsi,
		(STARTUPINFO.dwFlags & STARTF_USESHOWWINDOW)
		? STARTUPINFO.wShowWindow : SW_SHOWDEFAULT);

_ImageBase是连接器定义的一个伪变量,表明可执行文件被映射到应用程序内存中的什么位置。


如果是CUI程序main函数的调用如下

int nMainRetVal = main(argc, argv, envp);

注意用Visual studio生存的默认main函数没有第三个参数。可以自行增加表示环境变量

int main(int argc, char* argv[], char * env[])
{

	return 0;
}

main函数返回以后,启动函数调用C运行库的exit,向其返回值nMainRetVal


exit函数执行以下任务

调用_onexit函数所注册的一个回调函数。

调用所有全局和静态C++类对象的析构函数

在DEBUG生成中,如果设置了_CRTDBG_LEAK_CHECK_DF标志, 会调用_CrtDumpMemoryLeaks函数来生成内存泄漏的报告。

调用操作系统的ExitProcess函数,向其传入nMainRetVal,这会导致操作系统杀死进程,并设置他的退出代码。


4.1.1 进程的实例句柄

加载到进程地址空间的每一个可执行文件或DLL文件都被赋予了一个独一无二的实例句柄。可执行文件的实例句柄被当做WinMain函数的第一个参数hInstanceExe传入。

在需要加载资源的函数中需要用到此句柄。例如

WINUSERAPI
HICON
WINAPI
LoadIconW(
    _In_opt_ HINSTANCE hInstance,
    _In_ LPCWSTR lpIconName);

有的函数需要一个HMODULE类型的参数和HINSTANCE一致

WINBASEAPI
_Success_(return != 0)
_Ret_range_(1, nSize)
DWORD
WINAPI
GetModuleFileNameW(
    _In_opt_ HMODULE hModule,
    _Out_writes_to_(nSize, ((return < nSize) ? (return + 1) : nSize)) LPWSTR lpFilename,
    _In_ DWORD nSize
    );

hInstanceExe参数实际是一个内存基地址,系统将可执行文件的映像加载到进程地址空间中的这个位置。

例如打开一个exe文件,并将他的内容加载到地址0x0040 0000  则WinMain的hInstanceExe参数值为  0x0040 0000.

基地址是由连接器决定的,使用/BASE:address 可以设置要将应用程序加载到哪个基地址。


为了知道一个可执行文件或DLL文件被加载到进程地址空间的什么位置,可以使用GetModuleHandle来返回一个句柄/基地址

WINBASEAPI
_When_(lpModuleName == NULL, _Ret_notnull_)
_When_(lpModuleName != NULL, _Ret_maybenull_)
HMODULE
WINAPI
GetModuleHandleW(
    _In_opt_ LPCWSTR lpModuleName
    );

可以传入NULL 就会获得主调进程可执行文件的地址。


也可以通过连接器的伪变量__ImageBase查看



第二种方法是调用调用GetModuleHandleEx, 将GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS作为他的第一个参数,将当前函数的地址作为第二个参数,

最后一个参数是一个HMODULE的指针。GetModuleHandleEx会传入函数所在DLL的基地址来填写指针。

一个测试代码

#include <tchar.h>
#include <windows.h>

extern "C" const IMAGE_DOS_HEADER	__ImageBase;


void DumpModule() {
	// Get the base address of the running application.
	// Can be different from the running module if this code is in a DLL.
	HMODULE hModule = GetModuleHandle(NULL);
	_tprintf(TEXT("with GetModuleHandle(NULL) = 0x%x\r\n"), hModule);

	// Use the pseudo-variable __ImageBase to get
	// the address of the current module hModule/hInstance.
	_tprintf(TEXT("with __ImageBase = 0x%x\r\n"), (HINSTANCE)&__ImageBase);

	// Pass the address of the current method DumpModule
	// as parameter to GetModuleHandleEx to get the address
	// of the current module hModule/hInstance.
	hModule = NULL;
	GetModuleHandleEx(
		GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
		(PCTSTR)DumpModule,
		&hModule);
	_tprintf(TEXT("with GetModuleHandleEx = 0x%x\r\n"), hModule);
}

int main(int argc, char* argv[], char * env[])
{
	DumpModule();
	return 0;
}

执行结果



4.1.2 进程的前一个实例的句柄

hPrevInstance 参数用于16位windows系统,在32位系统中不要使用此参数。


可以不在参数列表中写参数变量,也可以通过宏

UNREFERENCED_PARAMETER(hPrevInstance);  来让编译器不发出警告



4.1.3  进程的命令行

命令行至少会有一个参数,也就是可执行文件的文件名。 C运行库会调用GetCommandLine来获取完整的命令行,忽略可执行文件的名称,然后将剩余的部分指针传递给WinMain的pszCmdLine参数


PTSTR GetCommandLine(); 获取完整命令行的指针

CUI的命令行参数传入的为 argc 和 argv 可以利用 Shell32.dll的导出函数CommandLineToArgv 将完整的命令行参数转换为argc和argv

SHSTDAPI_(LPWSTR *)  CommandLineToArgvW(_In_ LPCWSTR lpCmdLine, _Out_ int* pNumArgs);

改函数会在内部分配内存,需要释放。或者等进程退出时由操作系统释放(leak)

一个例子

int main(int argc, char* argv[], char * env[])
{
	int nNumArgs;
	PWSTR *ppArgv = CommandLineToArgvW(GetCommandLineW(), &nNumArgs);

	// Use the arguments...

	// Free the memory block
	HeapFree(GetProcessHeap(), 0, ppArgv);
	return 0;
}

在watch中查看



4.1.4 进程的环境变量

每个进程都有一个与他关联的环境块,这是在进程地址空间内分配的内存块,其中包含字符串类似下面



前面是环境变量名,后面是环境变量值

使用GetEnvironmentStrings函数能获得完整的环境块,类似上面的字符串。

以下例子展示了如何在这样的串中提取内容

void DumpEnvStrings() {
	PTSTR pEnvBlock = GetEnvironmentStrings();
	// Parse the block with the following format:
	// =::=::\
	// =...
	// var=value\0
	// ...
	// var=value\0\0
	// Note that some other strings might begin with '='.
	// Here is an example when the application is started from a network share.
	// [0] =::=::\
	// [1] =C:=C:\Windows\System32
	// [2] =ExitCode=00000000
	//

	TCHAR szName[MAX_PATH];
	TCHAR szValue[MAX_PATH];
	PTSTR pszCurrent = pEnvBlock;
	HRESULT hr = S_OK;
	PCTSTR pszPos = NULL;
	int current = 0;

	while (pszCurrent != NULL) {
		// Skip the meaningless strings like:
		// "=::=::\"
		if (*pszCurrent != TEXT('=')) {
			// Look for '=' separator.
			pszPos = _tcschr(pszCurrent, TEXT('='));

			// Point now to the first character of the value.
			pszPos++;

			// Copy the variable name.
			size_t cbNameLength = // Without the '='
				(size_t)pszPos - (size_t)pszCurrent - sizeof(TCHAR);

			hr = StringCbCopyN(szName, MAX_PATH, pszCurrent, cbNameLength);
			if (FAILED(hr)) {
				break;
			}

			// Copy the variable value with the last NULL character
			// and allow truncation because this is for UI only.
			hr = StringCchCopyN(szValue, MAX_PATH, pszPos, _tcslen(pszPos) + 1);
			if (SUCCEEDED(hr)) {
				_tprintf(TEXT("[%u] %s=%s\r\n"), current, szName, szValue);
			}
			else if (hr == STRSAFE_E_INSUFFICIENT_BUFFER) { // something wrong happened; check for truncation.
				_tprintf(TEXT("[%u] %s=%s...\r\n"), current, szName, szValue);
			}
			else { // This should never occur.
				_tprintf(TEXT("[%u] %s=???\r\n"), current, szName);
				break;
			}	
		}
		else {
			_tprintf(TEXT("[%u] %s\r\n"), current, pszCurrent);
		}

		// Next variable please.
		current++;

		// Move to the end of the string.
		while (*pszCurrent != TEXT('\0'))
			pszCurrent++;
		pszCurrent++;


		// Check if it was not the last string.
		if (*pszCurrent == TEXT('\0'))
			break;
	}

	// Don't forget to free the memory.
	FreeEnvironmentStrings(pEnvBlock);
}

运行结果



访问环境变量的第二种方式是CUI程序专用。他通main函数入口的TCHAR *env[]参数来实现。env是一个字符串指针数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值