第四章 进程
本章内容
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是一个字符串指针数