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

第四章 进程


本章内容


4.1 编写第一个Windows应用程序

4.2 CreateProcess函数

4.3 终止进程

4.4 子进程

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


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

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

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


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

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

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

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


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

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

《Windows核心编程》读书笔记四 进程_第1张图片



4.1 编写第一个Windows应用程序

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


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

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

GUI :/SUBSYSTEM:WINDOWS

《Windows核心编程》读书笔记四 进程_第2张图片

用户运行应用程序时,操作系统加载程序(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++对象都被正确构造。

《Windows核心编程》读书笔记四 进程_第3张图片

连接器选择正确的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++类对象的构造函数

《Windows核心编程》读书笔记四 进程_第4张图片

完成以上所有初始化以后,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 就会获得主调进程可执行文件的地址。

《Windows核心编程》读书笔记四 进程_第5张图片

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

《Windows核心编程》读书笔记四 进程_第6张图片


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

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

一个测试代码

#include 
#include 

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;
}

执行结果

《Windows核心编程》读书笔记四 进程_第7张图片


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中查看

《Windows核心编程》读书笔记四 进程_第8张图片


4.1.4 进程的环境变量

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


《Windows核心编程》读书笔记四 进程_第9张图片

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

使用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);
}

运行结果

《Windows核心编程》读书笔记四 进程_第10张图片


访问环境变量的第二种方式是CUI程序专用。他通main函数入口的TCHAR *env[]参数来实现。env是一个字符串指针数组,每个指针都指向一个不同的环境变量。(名称=值)的格式。

在指向自后一个环境变量字符串的指针后面会有一个NULL指针,表明这是数组末尾。

代码如下

void DumpEnvVariables(PTSTR pEnvBlock[]) {
	int current = 0;
	PTSTR *pElement = (PTSTR*)pEnvBlock;
	PTSTR pCurrent = NULL;
	while (pElement != NULL) {
		pCurrent = (PTSTR)(*pElement);
		if (pCurrent == NULL) {
			pElement = NULL;
		}
		else {
			_tprintf(TEXT("[%u] %s\r\n"), current, pCurrent);
			current++;
			pElement++;
		}		
	}
}

运行结果

《Windows核心编程》读书笔记四 进程_第11张图片


环境变量的值和名称可以包含空格,只是靠等号分割。

例如 XYZ= Windows

ABC=Windows

两个变量的值不同

XYZ =home

XYZ=Work

两个变量的名称不同

系统环境变量

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment

当前用户环境变量

HKEY_CURRENT_USER\Environment


可以使用各种api来修改环境变量,但是用户必须注销。

也可以发送消息WM_SETTINGCHANGE

SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)TEXT("Environment"));


父进程可以通过CreateProcess来控制子进程继承哪些环境变量。子进程继承后环境块属于自己,可以自行增删而不影响父进程。


使用环境变量GetEnvironmentVariable

WINBASEAPI
_Success_(return != 0 && return < nSize)
DWORD
WINAPI
GetEnvironmentVariableW(
    _In_opt_ LPCWSTR lpName,
    _Out_writes_to_opt_(nSize, return + 1) LPWSTR lpBuffer,
    _In_ DWORD nSize
    );

第三个参数传入0的时候返回 保持环境变量值所需要字符个数,包含结尾的'\0'

一个测试代码

void PrintEnvironmentVariable(PCTSTR pszVariableName) {
	PTSTR pszValue = NULL;
	// Get the size of the buffer that is required to store the value
	DWORD dwResult = GetEnvironmentVariable(pszVariableName, NULL, 0);
	if (dwResult != 0) {
		// Allocate the buffer to store the environment variable value
		DWORD size = dwResult * sizeof(TCHAR);
		pszValue = (PTSTR)malloc(size);
		GetEnvironmentVariable(pszVariableName, pszValue, size);
		_tprintf(TEXT("%s=%s\n"), pszVariableName, pszValue);
		free(pszValue);
	}
	else {
		_tprintf(TEXT("'%s'=\n"), pszVariableName);
	}
}

int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	PrintEnvironmentVariable(TEXT("PATH"));
	return 0;
}

运行结果

《Windows核心编程》读书笔记四 进程_第12张图片


可以将环境变量展开的函数 ExpandEnvironmentStrings

WINBASEAPI
_Success_(return != 0 && return <= nSize)
DWORD
WINAPI
ExpandEnvironmentStringsW(
    _In_ LPCWSTR lpSrc,
    _Out_writes_to_opt_(nSize, return) LPWSTR lpDst,
    _In_ DWORD nSize
    );

例子

int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	const TCHAR szSrc[] = TEXT("PATH='%PATH%'");
	DWORD chValue = ExpandEnvironmentStrings(szSrc, NULL, 0);
	if (chValue != 0){
		PTSTR pszBuffer = new TCHAR[chValue];
		chValue = ExpandEnvironmentStrings(szSrc, pszBuffer, chValue);
		_tprintf(TEXT("%s\r\n"), pszBuffer);
		delete[] pszBuffer;
	}
	return 0;
}

SetEnvironmentVariable函数可以添加一个变量,删除一个或者修改一个变量的值。

将pszName所表示的变量设置成pszValue的值。 设置为NULL则删除


4.1.5 进程的关联性

通常进程中的线程可以在主机的任何cpu上运行,然而,也可以强迫线程在可用cpu的一个子集上运行,这称为处理器相关性(processor affinity)


4.1.6 进程的错误模式

每个进程都关联了一组标志,这些标志的作用是让进程知道进程如何响应严重错误,包括磁盘介质错误,未处理的异常,文件查找错误以及数据对齐错误。

进程可以调用SetErrorMode来通知系统如何处理这些错误

UINT SetErrorMode(UINT fuErrorMode);

《Windows核心编程》读书笔记四 进程_第13张图片

错误模式会被子进程继承,除非在CreatePcocess中设置CREATE_DEFAULT_ERROR_MODE


4.1.7 进程当前所在的驱动器和目录

WINBASEAPI
_Success_(return != 0 && return < nBufferLength)
DWORD
WINAPI
GetCurrentDirectoryW(
    _In_ DWORD nBufferLength,
    _Out_writes_to_opt_(nBufferLength, return + 1) LPWSTR lpBuffer
    );

WINBASEAPI
BOOL
WINAPI
SetCurrentDirectoryW(
    _In_ LPCWSTR lpPathName
    );
如果进程中的某一线程调用了SetCurrentDirectory修改了默认的进程目录,则所有使用相对路径处理的可能会导致意外的错误。

GetCurrentDirectory可以传入  (0, NULL) 会返回需要字符的个数包括末尾的'\0'

或者直接传入MAX_PATH 尺寸的buffer

4.1.8 进程的当前目录

系统跟着记录这进程的当前驱动器和目录,但没有记录每个驱动器的当前目录。可以通过环境变量来支持

=C:=C:\Utility\Bin

=D:=D:\Program Files

例如假定当前进程的目录是C:\Utility\Bin ,而我们调用CreateFile来打开D:ReadMe.Txt 那么系统会查找环境变量=D:

由于=D:是存在的,系统将尝试从D:\Program Files目录打开ReadMe.txt文件,如果=D:不存在,系统会尝试从D盘根目录打开ReadMe.txt文件

Windows的文件函数从来不会添加或更改驱动器号。他们只是读取这种变量。

_chdir也可以更改当前目录其内部调用SetCurrentDirectory, _chdir还会调用SetEnvironmentVariable来添加或修改环境变量,从而使不同驱动器的当前目录得以保留。


获取每个驱动器的当前目录

	TCHAR szCurDir[MAX_PATH] = { 0 };
	DWORD cchLength = GetFullPathName(TEXT("D:"), MAX_PATH, szCurDir, NULL);
	_tprintf(TEXT("%s\n"), szCurDir);

驱动器号环境变量通常必须放在环境块的开始处。

以下例子修改D盘的当前目录并调用GetFullPathName获取

	//SetEnvironmentVariable(TEXT("=D:"), TEXT("D:\\Program Files (x86)"));
	//_chdir("D:\\Program Files (x86)");
	SetCurrentDirectory(TEXT("D:\\Program Files (x86)"));
	TCHAR szCurDir[MAX_PATH] = { 0 };
	DWORD cchLength = GetFullPathName(TEXT("d:"), MAX_PATH, szCurDir, NULL);
	_tprintf(TEXT("%s\n"), szCurDir);

!注意修改当前目录是必须真实存在的目录,否则会设置失败。但SetEnvironmentVariable并不会检查目录是否存在。

而SetCurrentDirectory又会直接更改当前的驱动器和目录。



如果一个父进程创建了一个希望传递给子进程的环境块,子进程的环境块不会自动继承父进程的当前目录。子进程的默认当前目录为每个驱动器的根目录。

如果希望子进程继承父进程的当前目录,父进程就必须在生成子进程之前,创建这些驱动器号的环境变量,并把它添加到环境块中。


GetCurrentDirectory

SetCurrentDirectory

GetFullPathName 在多线程应用中应该特别小心,因为他们的值是进程相关的。在获取过程中可能被其他线程所修改了。而且也应该避免使用相对路径


4.1.9 系统版本

DWORD GetVersion(); //主版本号在低字节,次版本号在高字节


NOT_BUILD_WINDOWS_DEPRECATE
WINBASEAPI
__drv_preferredFunction("IsWindows*", "Deprecated. Use VerifyVersionInfo* or IsWindows* macros from VersionHelpers.")
BOOL
WINAPI
GetVersionExW(
    _Inout_ LPOSVERSIONINFOW lpVersionInformation
    );
此函数传入一个OSVERSIONINFO的结构体并赋值给他。

typedef struct _OSVERSIONINFOW {
    DWORD dwOSVersionInfoSize;
    DWORD dwMajorVersion;
    DWORD dwMinorVersion;
    DWORD dwBuildNumber;
    DWORD dwPlatformId;
    WCHAR  szCSDVersion[ 128 ];     // Maintenance string for PSS usage
} OSVERSIONINFOW, *POSVERSIONINFOW, *LPOSVERSIONINFOW, RTL_OSVERSIONINFOW, *PRTL_OSVERSIONINFOW;

OSVERSIONINFOEX结构

typedef struct _OSVERSIONINFOEXW {
    DWORD dwOSVersionInfoSize;
    DWORD dwMajorVersion;
    DWORD dwMinorVersion;
    DWORD dwBuildNumber;
    DWORD dwPlatformId;
    WCHAR  szCSDVersion[ 128 ];     // Maintenance string for PSS usage
    WORD   wServicePackMajor;
    WORD   wServicePackMinor;
    WORD   wSuiteMask;
    BYTE  wProductType;
    BYTE  wReserved;
} OSVERSIONINFOEXW, *POSVERSIONINFOEXW, *LPOSVERSIONINFOEXW, RTL_OSVERSIONINFOEXW, *PRTL_OSVERSIONINFOEXW;
《Windows核心编程》读书笔记四 进程_第14张图片

参考MSDN 文档

Getting the System Version

https://msdn.microsoft.com/en-gb/library/ms724429.aspx


VISTA以上的系统还提供了VerifyVersionInfo 能比较主机系统和应用程序需求的版本。


需要构建一个OSVERSIONINFOEX结构,并用宏(VER_SET_CONDITION)设置一个  dwConditionMask

就可以调用VerifyVersionInfo来判断操作系统版本了。

以下例子展示了如何测试主机系统是不是Windows Vista


	// Prepare the OSVERSIONINFOEX structure to indicate Windows Vista.
	OSVERSIONINFOEX osver = { 0 };
	osver.dwOSVersionInfoSize = sizeof(osver);
	osver.dwMajorVersion = 6;
	osver.dwMinorVersion = 0;
	osver.dwPlatformId = VER_PLATFORM_WIN32_NT;

	// Prepare the conditionmask.
	DWORDLONG dwlConditionMask = 0;	// you must initialize this to 0.
	VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_EQUAL);
	VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, VER_EQUAL);
	VER_SET_CONDITION(dwlConditionMask, VER_PLATFORMID, VER_EQUAL);

	// Perform the version test.
	if (VerifyVersionInfo(&osver, VER_MAJORVERSION | VER_MINORVERSION | VER_PLATFORMID,
		dwlConditionMask)) {
		_tprintf(TEXT("The host system is Windows Vista exactly.\n"));
	}
	else {
		_tprintf(TEXT("The host system is NOT Windows Vista.\n"));
	}

这里进行的是VER_EQUAL对比,笔者的测试机器是Win7 x64 sp1这里返回非VISTA系统。

The host system is NOT Windows Vista.


4.2 CreateProcess函数

使用CreateProcess来创建进程原型如下

WINBASEAPI
BOOL
WINAPI
CreateProcessW(
    _In_opt_ LPCWSTR lpApplicationName,
    _Inout_opt_ LPWSTR lpCommandLine,
    _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ BOOL bInheritHandles,
    _In_ DWORD dwCreationFlags,
    _In_opt_ LPVOID lpEnvironment,
    _In_opt_ LPCWSTR lpCurrentDirectory,
    _In_ LPSTARTUPINFOW lpStartupInfo,
    _Out_ LPPROCESS_INFORMATION lpProcessInformation
    );

1)系统将创建一个进程内核对象,其初始引用计数器为1.进程内核对象并不是进程本身,而是操作系统用来管理该进程用的一个小型数据结构。

2)然后系统为进程创建一个虚拟地址空间,并将可执行文件(和所有必要的DLL)的代码以及数据加载到进程的地址空间。

3)接着系统为进程的主线程创建一个线程内核对象(引用计数器为1)。线程的内核对象也是一个小型数据结构用来管理线程。这个主线程也会调用C/C++运行库的启动例程(由连接器设定的应用程序入口)最终调用WinMain 或者main函数。如果成功创建了新进程和主线程,CreateProcess返回TRUE。


CreateProcess在进程完全初始化好之前就返回TRUE。OS的进程Loader尚未尝试定位所有DLL。如果一个DLL找不到或者无法正确初始化,进程就会终止。但因为CreateProcess返回TRUE,所以父进程并不会注意到任何初始化问题。


4.2.1 pszApplicationName和pszCommandLine参数

前者是新进程可执行文件的名字

后者是要传递给新进程的命令行参数。 CreateProcess实际会修改我们传给他的命令行字符。但在CreateProcess返回前,他会将此字符还要成原来的形式。

这是很重要的,不能传递只读内存区。否则可能会报错


《Windows核心编程》读书笔记四 进程_第15张图片

在调用之前将字符串常量复制到一个临时变量则不会报错。如下 。cc编译器的/Gf开关可以消除重复的字符串,并判断是否将那些字符串放在一个只读的区域。注意/ZI开关,会关闭/Gf

	STARTUPINFO si = { sizeof(si) };
	PROCESS_INFORMATION pi;
	TCHAR szCommandLine[] = TEXT("NOTEPAD");
	CreateProcess(NULL, szCommandLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

另外在Vista系统上调用ANSI版本的CreateProcess不会发生违规,因为其内部创建了在堆上一个Unicode的副本(可读可写)

CreateProcess会解析pszCommandLine的第一个token 并假定标记是我们要运行的可执行文件名称。并且默认扩展名是exe。CreateProcess按照以下顺序搜索可执行文件

1)主调进程EXE文件所在的目录

2)主调进程当前的目录(GetCurrentDirectory)

3)Windows系统目录,(GetSystemDirectory 通常是system32)

4)Windows目录

5)PATH环境变量中列出的目录

当然如果包含了绝对路径,则会直接使用绝对路径来查找可执行文件。


只要pszApplicationName设置为NULL,就会发生上述情况。

也可以在pszApplicationName只指定应用程序的名字,但是必须指定扩展名。CreateProcess假定当前目录,除非文件名之前有指定目录。并且CreateProcess不会在其他路径查找文件。


4.2.2 psaProcess,psaThread和bInheritHandles参数

psaProcess和psaThread指定进程对象和线程对象的安全性。也可传入NULL(默认的安全描述符。)

为psaProcess和psaThread参数使用SECURITY_ATTRIBUTES结构可以使其支持继承(继承父进程的句柄表)

一个继承的例子

1.进程A创建了进程B 并且设置进程B的进程内核对象可继承,主线程内核对象不可继承。(两个对象在进程B创建以后会更新A的句柄表,并且可以看到两个内核对象的继承标志)

2.由于创建进程B的时候CreateProcess的bInheritHandles设置为FALSE,所以进程B不会继承进程A中的任何“可继承”的内核对象。

3.接着进程A又创建了进程C,并设置CreateProcess的两个SECURITY_ATTRIBUTES为NULL,表明进程C的进程对象和线程对象在进程A的句柄表中是不可被继承的。

4.创建C的时候CreateProcess设置了bInheritHandles为TRUE,此时进程C将继承进程A中的所有可继承内核对象。这里有之前创建的进程B的进程内核对象,但不包含进程B的主线程内核对象。

代码如下。

	// Prepare a STARTUPINFO structure for spawning process.
	STARTUPINFO si = { sizeof(si) };
	SECURITY_ATTRIBUTES saProcess, saThread;
	PROCESS_INFORMATION piProcessB, piProcessC;
	TCHAR szPath[MAX_PATH];

	// Prepare to spawn Process B from Process A.
	// The handle identifying the new process
	// object should be inheritable.
	saProcess.nLength = sizeof(saProcess);
	saProcess.lpSecurityDescriptor = NULL;
	saProcess.bInheritHandle = TRUE;

	// The handle identifying the new thread
	// object should NOT be inheritable.
	saThread.nLength = sizeof(saThread);
	saThread.lpSecurityDescriptor = NULL;
	saThread.bInheritHandle = FALSE;

	// Spawn Process B.
	_tcscpy_s(szPath, _countof(szPath), TEXT("ProcessB"));
	CreateProcess(NULL, szPath, &saProcess, &saThread,
		FALSE, 0, NULL, NULL, &si, &piProcessB);

	// The pi structure contains two handles
	// relative to Process A:
	// hProcess, which identifies Process B's process
	// object and is inheritable, and hTrhead, which identifies
	// Process B's primary thread object and is NOT inheritable.
	// Prepare to spawn Process C from Process A.
	// Since NULL is passed for the psaProcess and psaThread
	// parameters, the handle to Process's process and
	// primary thread objects default to "noninheritable."
	// If Process A were to spawn another process, this new
	// process would NOT inherit handles to Process's process
	// and thread object.
	// Because TRUE is passed for the bInheritHandles parameter,
	// Process C will inherit the handle that identifies Process
	// B's process object but will not inherit a handle to
	// Process B's primary thread object.
	_tcscpy_s(szPath, _countof(szPath), TEXT("ProcessC"));
	CreateProcess(NULL, szPath, NULL, NULL,
		TRUE, 0, NULL, NULL, &si, &piProcessC);


4.2.3 fdwCreate参数

fdwCreate参数影响了创建新进程创建方式的flag


DEBUG_PROCESS 标志父进程希望调试子进程以及子进程将来创建的所有进程。(父进程现在的身份是调试器)


DEBUG_ONLY_THIS_PROCESS 最近创建的一个进程会通知父进程。而其创建的子进程将不会通知父进程。


这两项通常用来写调试器用。参考MSDN

http://www.cppblog.com/cxl82116/archive/2007/06/05/25535.html


CREATE_SUSPENDED 让系统创建新进程的同时挂起子进程的主线程。这样父进程就可以修改子进程地址空间中的内存,更改子进程主线程的优先级,或者在进程执行任何代码以前将次进程添加到一个作业中。父进程修改好子进程,可以调用ResumeThread函数来允许子进程执行代码。


DETACHED_PROCESS 标志阻止一个基于CUI的子进程访问其父进程的控制台窗口,并告诉系统它的输出发送到一个新的控制台窗口。

通常一个一个CUI进程创建了一个新的CUI子进程,那么默认情况下新进程也会使用父进程的控制台。如果指定了此标志那么子进程必须调用AllocConsole来创建自己的控制台。

一个例子

main.cpp

#include 
#include 

int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	TCHAR szPath[MAX_PATH] = TEXT("SubProcess");
	PROCESS_INFORMATION piProcess;
	STARTUPINFO si = { sizeof(si) };
	_tprintf(TEXT("Start to Create Sub Process\n"));
	CreateProcess(NULL, szPath, NULL, NULL,
		TRUE, 0, NULL, NULL, &si, &piProcess);
	return 0;
}

SubProcess.cpp

#include 
#include 

int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	_tprintf(TEXT("Print from the Sub Process\n"));
	return 0;
}

运行主进程main.exe 查看控制台打印的字符

《Windows核心编程》读书笔记四 进程_第16张图片


如果使用了DETACH_PROCESS子进程除了必须AllocConsole以后还需要重定向stdout

main.cpp

#include 
#include 

int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	TCHAR szPath[MAX_PATH] = TEXT("SubProcess");
	PROCESS_INFORMATION piProcess;
	STARTUPINFO si = { sizeof(si) };
	_tprintf(TEXT("Start to Create Sub Process\n"));
	DWORD dwCreationFlag = DETACHED_PROCESS;
	CreateProcess(NULL, szPath, NULL, NULL,
		TRUE, dwCreationFlag, NULL, NULL, &si, &piProcess);
	return 0;
}
Subprocess.cpp

#define _CRT_SECURE_NO_WARNINGS
#include 
#include 
#include 

int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	if (AllocConsole()){
		freopen("CONOUT$", "w", stdout);
		_tprintf(TEXT("Print from the Sub Process\n"));
	}
	else{
	}
	system("pause");
	return 0;
}

运行结果

《Windows核心编程》读书笔记四 进程_第17张图片


CREATE_NEW_CONSOLE,会自动为子进程创建自己的CONSOLE不能和DETACHED_PROCESS一起使用。

例子

main.cpp

#include 
#include 

int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	TCHAR szPath[MAX_PATH] = TEXT("SubProcess");
	PROCESS_INFORMATION piProcess;
	STARTUPINFO si = { sizeof(si) };
	_tprintf(TEXT("Start to Create Sub Process\n"));
	DWORD dwCreationFlag = CREATE_NEW_CONSOLE;
	CreateProcess(NULL, szPath, NULL, NULL,
		TRUE, dwCreationFlag, NULL, NULL, &si, &piProcess);
	return 0;
}


Subprocess.cpp

#define _CRT_SECURE_NO_WARNINGS
#include 
#include 
#include 

int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	if (AllocConsole()){
		freopen("CONOUT$", "w", stdout);
		_tprintf(TEXT("Print from the Sub Process\n"));
	}
	else{
		_tprintf(TEXT("Sub Process has owned a console!\n"));
		_tprintf(TEXT("Print from the Sub Process\n"));
	}
	system("pause");
	return 0;
}

运行结果

《Windows核心编程》读书笔记四 进程_第18张图片


CREATE_NO_WINDOW  标志应用程序不要为子进程创建任何控制台,用于执行没有用户界面的程序。


CREATE_NEW_PROCESS_GROUP 对CUI程序而言的。用于创建新进程组。在同一组中的所有进程,当按下Ctrl+C中断当前操作时,系统会向这个组的进程发出通知。


CREATE_DEFAULT_ERROR_MODE 新进程不会继承父进程的错误模式


CREATE_SEPARATE_WOW_VDM 表明创建一个16位windows程序(Virtual DOS Machine) 默认创建的16位进程会共享一个VDM(因为创建一个会消耗较多的资源)


CREATE_SHARED_WOW_VDM 在运行16位应用程序才有用, 可以修改注册表 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\WOW\

DefaultSeparateVDM 设置为yes

可以调用IsWow64Process 来判断检测进程句柄是不是32位进程(在64bit系统下)


CREATE_UNICODE_ENVIRONMENT 告诉系统子进程的环境块包含UNICODE字符,进程环境块默认包含ANSI字符


CREATE_FORCEDOS 强制系统运行嵌入一个在16位OS中的MS-DOS程序


CREATE_BREAKAWAY_FROM_JOB 允许一个作业中的进程生成一个和作业无关的进程


EXTENDED_STARTUPINFO_PRESENT  告知操作系统传给psiStartInfo参数的是STARTUPINFOEX结构


fdwCreate还运行给进程分配优先级。但是大部分应用没这个必要。

IDLE_PRIORITY_CLASS

低于标准BELOW_NORMAL_PRIORITY_CLASS

标准NORMAL_PRIORITY_CLASS

高于标准ABOVE_NORMAL_PRIORITY_CLASS

HIGH_PRIORITY_CLASS

实时REALTIME_PRIORITY_CLASS


4.2.4 pvEnvironment参数

pvEnvironment参数指向一块内存,其中包含新进程要使用的环境字符串。大多数时候这个参数传入NULL,

子进程会继承其父进程使用的一组环境字符串。

还可以通过GetEnvironmentString函数获得父进程的环境块的串地址,传递给CreateProcess用于创建子进程。 该功能和传入NULL的行为一致。

不使用环境字符串的时候调用FreeEnvironmentStrings释放其空间。


4.2.5 pszCurDir参数

允许父进程设置当前进程的当前驱动器和目录。如果为NULL则默认和主进程一致。如果非NULL

pszCurDir必须执行一个以'\0'结尾的字符串,其中包含了工作驱动器和目录。


4.2.6 psiStartInfo参数

指向一个STARTUPINFO 或者 STARTUPINFOEX的结构体

typedef struct _STARTUPINFOW {
    DWORD   cb;
    LPWSTR  lpReserved;
    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;
} STARTUPINFOW, *LPSTARTUPINFOW;
typedef struct _STARTUPINFOEXW {
    STARTUPINFOW StartupInfo;
    LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
} STARTUPINFOEXW, *LPSTARTUPINFOEXW;


默认值只要生成次结构

STARTUPINFO si = {sizeof(si)};

CreateProcess(..., &si, ...);

如果没有把结构内容清0,则成员将包含线程栈上的垃圾数据。会造成CreateProcess 未知运行结果。

《Windows核心编程》读书笔记四 进程_第19张图片


《Windows核心编程》读书笔记四 进程_第20张图片


关于dwFlags成员,包含一组标志用于修改子进程的创建方式。

《Windows核心编程》读书笔记四 进程_第21张图片


另外还有两个标志

STARTF_FORCEONFEEDBACKCreateProcess会监控进程的初始化过程,并根据结果更改光标的形状。一旦子进程调用了GetMessage(表明初始化完毕)CreateProcess则停止监控。

START_FORCEOFFFEEDBACK 改为等待图标

在启动进程时候控制鼠标指针。CreateProcess临时将系统的护镖指针改为 加载等待 图片


wShowWindow会作为参数传递给子进程WinMain函数中的nCmdShow参数。  可能值是SW_SHOWNORMAL, SW_SHOWMINNOACTIVE 和 SW_SHOWDEFAULT

也可以在应用程序的快捷方式中修改此值。

《Windows核心编程》读书笔记四 进程_第22张图片


Microsoft为了避免创建多个CreateProcess版本,仅仅是通过扩展STARTUPINFOEX结果来升级新的feature。

STARTUPIINFOEX保护一个字段 lpAttributeList用于传递额外属性。

PROC_THREAD_ATTRIBUTE_HANDLE_LIST 该属性告知CreateProcess进程究竟应该继承哪一些内核对象的句柄。这些对象句柄必须在创建时指定可继承(SECURIT_ATTRIBUTES结构中保护设置为TRUE的bInheritHandle字段)。

使用这个属性,子进程只能继承一组选定的句柄,而不是继承所有可继承的句柄。

Note  if you use this attribute, pass in a value of TRUE for the bInheritHandles parameter of the CreateProcessfunction.


PROC_THREAD_ATTRIBUTE_PARENT_PROCESS  自行指定进程成为当前要创建的进程的父进程。 但是不改变调试关系。调用CreateProcess的进程仍然能收到调试通知。

调用以下函数两次,才能创建一个空白的属性列表。

WINBASEAPI
_Success_(return != FALSE)
BOOL
WINAPI
InitializeProcThreadAttributeList(
    _Out_writes_bytes_to_opt_(*lpSize, *lpSize) LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
    _In_ DWORD dwAttributeCount,
    _Reserved_ DWORD dwFlags,
    _When_(lpAttributeList == nullptr, _Out_) _When_(lpAttributeList != nullptr, _Inout_) PSIZE_T lpSize
    );
dwFlags参数是保留的始终传入0.第一次调用是获得保存属性的内存块大小

	SIZE_T cbAttributeListSize = 0;
	BOOL bReturn = InitializeProcThreadAttributeList(
		NULL, 1, 0, &cbAttributeListSize);
	//bReturn is FALSE but GetLastError() return ERROR_INSUFFICIENT_BUFFER

	PPROC_THREAD_ATTRIBUTE_LIST pAttributeList =
		(PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, cbAttributeListSize);

	bReturn = InitializeProcThreadAttributeList(
		pAttributeList, 1, 0, &cbAttributeListSize);

接下来可以根据需要用下面函数添加键值对。

WINBASEAPI
BOOL
WINAPI
UpdateProcThreadAttribute(
    _Inout_ LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
    _In_ DWORD dwFlags,
    _In_ DWORD_PTR Attribute,
    _In_reads_bytes_opt_(cbSize) PVOID lpValue,
    _In_ SIZE_T cbSize,
    _Out_writes_bytes_opt_(cbSize) PVOID lpPreviousValue,
    _In_opt_ PSIZE_T lpReturnSize
    );

lpAttributeList是之前初始化的attribute列表, 他接受 PROC_THREAD_ATTRIBUTE_PARENT_PROCESS  或者   PROC_THREAD_ATTRIBUTE_HANDLE_LIST的值。

如果是前者pValue 必须执行一个变量的地址,包含了新的父进程句柄,而cbSize应该使用sizeof(HANDLE)作为其值。

如果是后者pValue必须执行一个数组的起始地址,包含了运行进程访问的,可继承的内核对象句柄, cbSize = sizeof(HANDLE)*句柄数。

dwFlags, PreviousValue和pReturnSize是保留参数,必须设定为0,NULL和NULL

一个初始化Attributelist 并使用STARTUPINFOEX创建进程的例子

创建了一个mutex 并设定AttributeList让子进程继承此Mutex

	// 1.Create a mutex.
	SECURITY_ATTRIBUTES sa;
	sa.nLength = sizeof(sa);
	sa.lpSecurityDescriptor = NULL;
	sa.bInheritHandle = TRUE; // Make the returned handle inheritable.  

	HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
	
	// 2. Create the attributelist and fill the attribute
	SIZE_T cbAttributeListSize = 0;
	BOOL bReturn = InitializeProcThreadAttributeList(
		NULL, 1, 0, &cbAttributeListSize);
	//bReturn is FALSE but GetLastError() return ERROR_INSUFFICIENT_BUFFER

	PPROC_THREAD_ATTRIBUTE_LIST pAttributeList =
		(PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, cbAttributeListSize);

	bReturn = InitializeProcThreadAttributeList(
		pAttributeList, 1, 0, &cbAttributeListSize);


	UpdateProcThreadAttribute(
		pAttributeList,
		0,
		PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
		(PVOID)&(hMutex),		// pointer to the inheritable mutex handle.
		sizeof(HANDLE),
		NULL, NULL);

	// 3. prepare the attribute for the process.
	PROCESS_INFORMATION processInfo = { 0 };
	STARTUPINFOEX esi = { sizeof(STARTUPINFOEX) };
	esi.lpAttributeList = pAttributeList;
	TCHAR szPath[] = TEXT("SubProcess");
	DWORD dwCreationFlag = EXTENDED_STARTUPINFO_PRESENT;
	// you must set the bInheritable = TRUE ,otherwise the createProcess will be failed.
	// if you just specified the parent handle. it would be OK.
	bReturn = CreateProcess(NULL, szPath,
		NULL, NULL, TRUE,
		dwCreationFlag, NULL, NULL,
		(LPSTARTUPINFO)&esi, &processInfo);	

	//...do something.
	// free the attributelist
	DeleteProcThreadAttributeList(pAttributeList);

最后应用程序可以调用以下函数获得 STARTUPINFO结构的一个副本。此结构是由父进程初始化的。子进程可以检查这个结构并根据成员值来修改其行为。

VOID GetStartupInfo(LPSTARTUPINFO pStartupInfo);

StartupInfo中的一些句柄值在父进程中创建的,其地址是父进程的地址空间。


4.2.7 ppiProcInfo参数

指向一个PROCESS_INFORMATION的结构

typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;
    HANDLE hThread;
    DWORD dwProcessId;
    DWORD dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

CreateProcess返回的与当前进程相关的 进程内核对象句柄 和 线程内核对象句柄。引用计数器为1. 而CreateProcess内部打开这些对象引用计数器又会加1

因此若要让子进程和子进程的主线程在父进程退出之前关闭,子进程必须先终止(引用计数器-1)必须在父进程中调用CloseHandle将其引用计数器再-1. 

释放子进程的主线程内核对象同理。

注意:关闭句柄并不会真正关闭子进程和子线程(只改变其引用计数器)!


进程会被操作系统分配一个独一无二的ProcessID, PID=0 是System Idle Process 其线程数等于CPU数量

《Windows核心编程》读书笔记四 进程_第23张图片

PID主要供一些系统工具辨识进程使用。

PID由操作系统管理会被回收分配和重用。所以要特备注意。

GetCurrentProcessId获得PID

GetCurrentThreadId 获得当前正在运行线程的ID

根据指定句柄获得PID  GetProcessId

根据指定句柄获得线程ID  GetThreadId

获得当前线程所在进程的PID GetProcessIdOfThread


ToolHelp函数允许进程通过PROCESSENTRY32 结构查询其父进程的PID。

但是由于PID具有实效性,可能不准确。最好使用内核对象,窗口句柄等来定位一个进程的父进程。

如果一定要使用PID,唯一的办法就是保证进程或线程的内核对象不被销毁。(例如将父进程的内核对象继承给子进程) 在不需要使用以后调用CloseHandle


4.3 终止进程

4种方式可以终止进程:

1)主线程的入口点函数返回(强烈推荐)

2)进程中有一个线程调用ExitProcess函数(要避免这种方式)

3)另一个进程中的线程调用TerminateProcess函数(要避免这种方式)

4)进程中所有线程都“自然死亡”(这种情况几乎不会发生)


4.3.1 主进程的入口点函数返回

应该确保只有在主线程的入口点函数返回之后,这个应用程序才终止。这样主线程的所以资源才能被正确清理。确保以下操作会被执行

1)该线程创建的任何C++对象都将由这些对象的析构函数正确销毁。

2)操作系统将正确释放线程栈使用的内存

3)系统将进程的退出代码(在进程的内核对象中维护)设为入口点函数的返回值

4)系统递减内核对象的使用计数器


4.3.2 ExitProcess函数

WINBASEAPI
DECLSPEC_NORETURN
VOID
WINAPI
ExitProcess(
    _In_ UINT uExitCode
    );

该函数将终止进程,并忽略其后的所有代码。

C运行库在Main函数返回后将清理所有C运行时资源,最后调用ExitProcess

crt0dat.c中最终调用ExitProcess退出

void __cdecl __crtExitProcess (
        int status
        )
{
#if defined (_CRT_APP) && !defined (_KERNELX)
        (status);
        __crtExitProcessWinRT();
#else  /* defined (_CRT_APP) && !defined (_KERNELX) */

#if !defined (_KERNELX)
    __crtCorExitProcess(status);
#endif  /* !defined (_KERNELX) */

        /*
         * Either mscoree.dll isn't loaded,
         * or CorExitProcess isn't exported from mscoree.dll,
         * or CorExitProcess returned (should never happen).
         * Just call ExitProcess.
         */

        ExitProcess(status);
#endif  /* defined (_CRT_APP) && !defined (_KERNELX) */
}

SDK中指出,一个进程在其所有线程都终止以后才会终止。 C/C++运行库采用了一个不同的策略:

不管进程中是否还有其他线程在运行,只要英语程序的主线程从他的入口函数返回,C/C++就会调用ExitProcess来终止进程。

如果在入口函数中调用的是ExitThread那么只会终止主线程,其他线程继续运行,进程就不会终止。


注意:ExirProcess或ExitThread会导致进程或线程直接终止运行再也不会返回当前函数的调用。对操作系统而言这样没什么问题。(进程或线程的资源会被清理)

但C/C++应用程序应该避免这样调用,因为C/C++运行库也许不能执行正确的清理

看一下例子

#include 
#include 
#include 

class CSomeObj{
public:
	CSomeObj() { printf("Constructor \n"); }
	~CSomeObj() { printf("Destructor \n"); }
};

CSomeObj g_GlobalObj;

int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{
	CSomeObj LocalObj;
	ExitProcess(0);

	return 0;
}

《Windows核心编程》读书笔记四 进程_第24张图片

全局对象和局部对象都没有调用析构函数,C++对象没有被正确析构,因为ExitProcess造成进程当场终止运行。C/C++运行库没有机会执行清理工作。

只需要主线程的入口函数返回,C/C++运行库就能执行其清理工作。

所以不要显示调用ExitProcess和ExitThread


4.3.3 TerminateProcess函数

调用TerminateProcess也可以终止一个进程

WINBASEAPI
BOOL
WINAPI
TerminateProcess(
    _In_ HANDLE hProcess,
    _In_ UINT uExitCode
    );

任何线程都可以调用TerminateProcess来终止另一个进程或者自己的进程。 hProcess指定了要终止的进程的句柄。其退出代码的值就是传给uExitCode的值。

被终止的进程得不到自己要被终止的通知--应用程序不能正确清理,也不能阻止它自己被强行终止。例如这种情况下进程不能将它在内存中的信息flush到磁盘上。

操作系统在进程终止以后会进行彻底清理,保证不会泄露任何操作系统资源。在进程终止后绝对不会泄漏任何东西

TerminateProcess函数是异步的,并不等到进程完全终止了才返回。为了确定进程是否终止需要使用WaitForSingleObject


4.3.4 当进程中的所有线程终止时

当进程的所有线程终止时,操作系统认为没有任何理由再保持进程的地址空间。并会终止这个进程。进程的退出代码会被设置为最后一个终止的哪个线程的退出代码。


4.3.5 当进程终止运行时

一个进程终止时,系统回依次执行以下操作。

1) 终止进程中驻留的任何线程

2)释放进程分配的所有用户对象和GDI对象,关闭所有内核对象(如果没有其他进程打开这些内核对象的句柄,那么他们会被销毁。否则引用计数器-1)

3)进程的退出代码从STILL_ACTIVE变为传给ExitProcess或TerminateProcess函数的代码

4)进程内核对象的状态变为已触发状态。

5)进程内核对象引用计数器-1


当父进程忘记关闭子进程句柄时候,子进程即使结束。其进程内核对象句柄仍然不会被销毁。此时可以获得一些统计信息,例如GetExitCodeProcess来获得一个已经终止的进程的退出代码。

WINBASEAPI
BOOL
WINAPI
GetExitCodeProcess(
    _In_ HANDLE hProcess,
    _Out_ LPDWORD lpExitCode
    );

如果被调用的进程未终止,lpExitCode会返回 STILL_ACTIVE(0x103)


重点,如果对进程的统计数据不再感兴趣应该调用CloseHandle来递减内核对象的使用计数器,并释放它。


4.4 子进程

为了执行复杂的任务而不让当前线程一直等待,可以创建一个新的进程来完成工作。父进程和子进程之间可以进行一些数据共享。

(DDE dynamic Data Exchange) OLE, 管道, 邮件槽等。共享数据最方便的方式就是使用内存映像文件(CreateFileMapping)


以阻塞方式运行子进程

以下代码创建了一个子进程,并等待子进程完成相应的工作正常结束以后,再继续当前线程的执行。

	PROCESS_INFORMATION pi;
	DWORD dwExitCode;

	//Spawn the child process.
	BOOL fSuccess = CreateProcess(..., &pi);
	if (fSuccess) {
		// Close the thread handle as soon as it is no longer needed!
		CloseHandle(pi.hThread);

		// Suspend our execution until the child has terminated.
		WaitForSingleObject(pi.hProcess, INFINITE);

		// The child process terminated; get its exit code.
		GetExitCodeProcess(pi.hProcess, &dwExitCode);

		// Close the process handle as soon as it is no longer needed.
		CloseHandle(pi.hProcess);
	}

上面的例子使用了WaitForSingleObject函数,该函数会一直等待直到hObject参数所表示的对象变为已触发。 进程对象在终止的时候会变为已触发。

WaitForSingleObject将会挂起当前线程,直到子进程终止。


一个良好的编程习惯:在不需要使用子进程的相关内核对象应该立即CloseHandle。 否则假定子进程生成了另外一个新的子进程,而自己的主线程已经退出。由于调用CreateProcess的当前进程未释放子进程的进程对象,因此自进程的内核对象不会被操作系统释放。


运行独立的子进程

大多数时候应用程序将另一个进程作为独立的进程(detached process)来启动。这就意味着一旦子进程创建,父进程就不再与其通信,或者不必等他它完成工作之后再继续自己的工作(当前进程不必挂起等待)。这时候只需要调用CloseHandle关闭子进程的进程句柄和主线程句柄。

一个例子

	PROCESS_INFORMATION pi;

	//Spawn the child process.
	BOOL fSuccess = CreateProcess(..., &pi);
	if (fSuccess) {
		// Allow the system to destroy the process & thread kernel
		// objects as soon as the child process terminates.
		// Close the thread handle as soon as it is no longer needed!
		CloseHandle(pi.hThread);
		CloseHandle(pi.hProcess);
	}

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

每个用户登录windows以后会有一个安全令牌(Security token)其后该用户启动的进程都拥有此令牌的权限。

由于许多windows用户使用Administrator登录,此用户的权限太高(可以修改系统文件)可能导致操作系统以高权限执行而已软件而破坏操作系统。


在Vista以上用户以Administrator登录出来具有高特权的安全令牌,还会具有一个筛选过的令牌(filtered token) 只有普通的标准用户权限(standard user)。

之后从第一个进程开始所有启动的进程都和筛选过的令牌相关联。因此默认运行的应用程序将无法访问受限资源。


可以在进程启动之前(进程边界, 进程已经启动以后会与筛选过的令牌相关联并且运行时不可修改)让操作系统提示用户取得提升权限的同意。也可以在快捷菜单中选择以管理员身份运行。


可以在自己的应用程序上显示一个盾牌图,会弹出一个权限提升对话框。


Windows只允许进程边界上进行权限提升(未启动以前)。 可以用一个未提升权限的进程来生成另一个提升了权限的进程,后者将包含一个com服务器,这个新进程将保持活动状态。这样未提升权限的进程就可以向已经提升权限的进程发出IPC调用,而不必启动一个新的实例再终止它自身。


4.5.1 自动提升进程的权限

windows每次启动应用程序都将自动弹框询问并提升应用程序的权限(比如安装程序)

在可执行文件中嵌入资源(RT_MANIFEST) 系统会检查

一个例子 参考blog : http://blog.csdn.net/sesiria/article/details/51939231

  
      
          
              
          
      
 

level 属性可能有3个不同的值

《Windows核心编程》读书笔记四 进程_第25张图片


也可以保存成一个  应用程序.exe.manifest的文件的外部清单

如果exe本身内嵌了一个清单,则外部清单会被忽略。

也可以自行设定应用程序默认以管理员权限运行

《Windows核心编程》读书笔记四 进程_第26张图片


4.5.2 手动提升进程的权限

CreateProcess函数没有提供提升应用程序权限的功能。 可以调用ShellExecuteEx函数

SHSTDAPI_(BOOL) ShellExecuteExW(_Inout_ SHELLEXECUTEINFOW *pExecInfo);

typedef struct _SHELLEXECUTEINFOW
{
    DWORD cbSize;               // in, required, sizeof of this structure
    ULONG fMask;                // in, SEE_MASK_XXX values
    HWND hwnd;                  // in, optional
    LPCWSTR  lpVerb;            // in, optional when unspecified the default verb is choosen
    LPCWSTR  lpFile;            // in, either this value or lpIDList must be specified
    LPCWSTR  lpParameters;      // in, optional
    LPCWSTR  lpDirectory;       // in, optional
    int nShow;                  // in, required
    HINSTANCE hInstApp;         // out when SEE_MASK_NOCLOSEPROCESS is specified
    void *lpIDList;             // in, valid when SEE_MASK_IDLIST is specified, PCIDLIST_ABSOLUTE, for use with SEE_MASK_IDLIST & SEE_MASK_INVOKEIDLIST
    LPCWSTR  lpClass;           // in, valid when SEE_MASK_CLASSNAME is specified
    HKEY hkeyClass;             // in, valid when SEE_MASK_CLASSKEY is specified
    DWORD dwHotKey;             // in, valid when SEE_MASK_HOTKEY is specified
    union                       
    {                           
        HANDLE hIcon;           // not used
#if (NTDDI_VERSION >= NTDDI_WIN2K)
        HANDLE hMonitor;        // in, valid when SEE_MASK_HMONITOR specified
#endif // (NTDDI_VERSION >= NTDDI_WIN2K)
    } DUMMYUNIONNAME;           
    HANDLE hProcess;            // out, valid when SEE_MASK_NOCLOSEPROCESS specified
} SHELLEXECUTEINFOW, *LPSHELLEXECUTEINFOW;

SHELLEXECUTEINFO结构中 lpVerb 和lpFile 为主要关注的参数。前者设定为 “runas” 后者包含可执行文件的路径。

代码如下所示

	// Initialize the structure.
	SHELLEXECUTEINFO sei = { sizeof(SHELLEXECUTEINFO) };
	// Ask for privileges elevation.
	sei.lpVerb = TEXT("runas");
	// Create a Command Prompt from which you will be able to start
	// other elevated applications.
	sei.lpFile = TEXT("cmd.exe");
	// Don't forget this parameter; otherwise, the window will e hidden.
	sei.nShow = SW_SHOWNORMAL;
	if (!ShellExecuteEx(&sei)) {
		DWORD dwStatus = GetLastError();
		if (dwStatus == ERROR_CANCELLED) {
			// the user refused to allow privileges elevation.
		}
		else if (dwStatus == ERROR_FILE_NOT_FOUND) {
			// The file defined by lpFile was not found and
			// an error message popped up.
		}
	}


但一个进程提升权限以后,每次他再调用 CreateProcess来生成另一个子进程都会获得和它的父进程一样的提升后的权限。

某些任务需要高权限的时候应该在启动该任务的界面元素盘显示一个盾牌图标。

由于任务管理器由另一个进程或者一个进程中的COM服务来执行,所以应该需要将需要管理员权限的所有任务集中到另一个应用程序中,并通过ShellExecuteEx(lpVerb 传送runas)来提升他的权限。 具体要执行的特权操作采用命令行参数传递。(SHELLEXECUTEINFO的lpParameters字段)


4.5.3 何为权限上下文

如何判断应用程序是以管理与身份运行还是以筛选令牌权限运行的

以下代码的函数GetProcessElevation返回一个提升类型并指出进程是否以管理员身份运行的BOOL值。

BOOL GetProcessElevation(TOKEN_ELEVATION_TYPE * pElevationType, BOOL * pIsAdmin) {
	HANDLE hToken = NULL;
	DWORD dwSize;

	// Get current process token
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
		return FALSE;

	BOOL bResult = FALSE;

	// Retrive elevation type information
	if (GetTokenInformation(hToken, TokenElevationType,
		pElevationType, sizeof(TOKEN_ELEVATION_TYPE), &dwSize)) {
		// Create the SID corresponding to the Administrators group
		BYTE adminSID[SECURITY_MAX_SID_SIZE];
		dwSize = sizeof(adminSID);

		CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL,
			&adminSID, &dwSize);

		if (*pElevationType == TokenElevationTypeLimited) {
			// Get handle to linked token (will have one if we are lua)
			HANDLE hUnfilteredToken = NULL;
			GetTokenInformation(hToken, TokenLinkedToken, (VOID*)
				&hUnfilteredToken, sizeof(HANDLE), &dwSize);

			// Check if this original token contains admin SID
			if (CheckTokenMembership(hUnfilteredToken, &adminSID, pIsAdmin)){
				bResult = TRUE;
			}

			// Don't forget to close the unfiltered token
			CloseHandle(hUnfilteredToken);
		}
		else {
			*pIsAdmin = IsUserAnAdmin();
			bResult = TRUE;
		}
	}
	// Don't forget to close the process token
	CloseHandle(hToken);
	return bResult;
}

TOKEN_ELEVATION_TYPE枚举类型定义

《Windows核心编程》读书笔记四 进程_第27张图片

首先获取这些值并判断使用的令牌是否被筛选过。

接下来判断用户是否是管理员。

如果没有被筛选过,直接调用IsUserAnAdmin()返回

如果被筛选过,首先获取未筛选的令牌把TokenLinkedToken传给GetTokenInformation)然后判断其是否包含管理员的Sid。(借助于CreateWellKnownSid和CheckTokenMembership)

该函数可以用来获取当前进程的令牌和权限用以控制是否显示盾牌图标。


4.5.4 枚举系统正在运行的进程

Windows在注册表中维护一个性能数据库,包含海量信息。例如RegQueryValueEx把注册表的根目录设为KEY_PERFORMANCE_DATA

但是该数据库在Win95和98上不可用, 

没有自己的函数,使用注册表函数

数据库信息布局非常复杂


为了更方便访问此数据库借助Performance Data Helper(PDH.dll)

在(Win9X中 使用ToolHelp API的Process32First 和Process32Next函数)

在WinNT中使用EnumProcess函数。

在Win2000开始Tool Help函数支持2K以上的NT内核系统。


4.5.5 Process Information示例程序 (该示例涉及较多的后续章节的知识 还用到了WinDbg)

运行结果

《Windows核心编程》读书笔记四 进程_第28张图片

除了安全描述符(SID)和访问控制列表(access control list, ACL). 系统还通过在系统访问控制列表(SACL)中新增一个名为强制标签的访问控制项(Access control entry)来为受保护的资源分配一个所谓的完整性级别(integrity level)

信任级别

《Windows核心编程》读书笔记四 进程_第29张图片

利用Process Explorer工具可以查看应用程序的完整性级别(integrity level)。请选择Select Columns -> Process Image->Integrity Level

《Windows核心编程》读书笔记四 进程_第30张图片


windows还利用完整性级别来拒绝低完整性级别的进程访问高完整性级别的用户界面。这个机制成为用户界面特权隔离(User Interface Privilege Isolation,UIPI)

为了防止完整性低的进程获取另一个完整性高的进程的信息或进行虚假输入,Windows阻止完整性低的进程通过PostMessage/SendMessage向完整性高的进程发送Windows消息. 也阻止通过挂钩子来拦截完整性较高的进程的windows消息。

可以利用WindowDump来做实验或者spy++


你可能感兴趣的:(Windows)