(4)进程
进程是程序运行的一个实例,由以下两部分组成:
-
进程内核对象:操作系统用它来管理和统计进程信息;
-
进程地址空间:所有进程执行所需要的代码和数据存在这个地址空间中。
进程是惰性的,进程要做任何事都需要通过线程在其上下文环境中执行来实现。当一个进程创建后,操作系统也同时为其创建一个主线程(primary thread),主线程又会创建其他线程。当进程中所有的线程都停止时,操作系统也同时销毁该进程。
Windows中可能的进程入口函数如下:
WinMain/wWinMain是窗口进程程序的入口,相应的C运行时库入口微WinMainCRTStartup/wWinMainStartup;同理,main/wmain是控制台进程程序的入口,相应的C运行时库入口为mainCRTStartup/wmainCRTStartup。
通过编译器的连接开关/SubSystem:Console和/SubSystem:Winodws可以设定程序使用哪个子系统,事实上可以不设置这个值,Visual Studio的编译器会根据我们所提供入口函数来确定到底链接到哪个子系统。程序运行的时候,Windows操作系统的加载程序(loader)会检查执行文件镜像中的文件头,从而获得该子系统值。
Windows加载程序到进程后,即相应的CRT启动函数,这些启动函数的用途可总结如下:
-
获取只想新进程的完整命令行;
-
获得指向进程环境变量的指针;
-
初始化C/C++运行时库的全局变量。可以包含stdlib.h以访问这些变量,所有这些变量总结如下图;
-
初始化CRT内存分配函数(malloc/calloc)和CRT IO所使用的堆(heap);
-
调用所有全局和静态C++对象的构造函数。(Microsoft不推荐使用这些CRT变量,因为使用它们的代码有可能在CRT初始化前运行,最好调用相应的Windows API)
如果是控制台程序可以将入口函数
_tmain(int argc, TCHAR* argv[])
换成
_tmain(int argc, TCHAR* argv[], TCHAR* env[])
Windows会这样调用
int nMainRetVal = _tmain(argc, argv, envp);
第三个参数指向进程环境变量。
入口函数返回后,CRT启动函数将调用CRT函数exit,并将(nMainRetVal)作为参数传给它,exit函数执行以下任务:
- 调用_onexit函数所注册的任何一个函数;
- 调用所有全局和静态C++对象的析构函数;
- 在Debug版中,如果设置了_CRTDBG_LEAK_CHECK_DF标志,就通过调用_CrtDumpMemoryLeaks来生成内存泄漏报告;
- 调用Windows操作系统的ExitProcess,并传入nMainRetVal,这会导致操作系统kill该进程,并设置其对出代码。
进程实例句柄
// 连接器伪变量,指示正在运行的代码所在文件(exe/dll)被加载到应用程序总哪个位置
EXTERN_C const IMAGE_DOS_HEADER __ImageBase;
void DumpModule()
{
//返回进程可执行文件的实例句柄
HMODULE hApp=GetModuleHandle(NULL);
TCHAR szModuleName[MAX_PATH]={};
GetModuleFileName(hApp, szModuleName, MAX_PATH);
_tprintf(_T("GetModuleHandle(NULL) returned -> address:0x%x with name: %s\r\n"), hApp, szModuleName);
//当前执行代码所在文件的实例句柄
HINSTANCE hCurrentModule=(HINSTANCE)&__ImageBase;
GetModuleFileName(hCurrentModule, szModuleName, MAX_PATH);
_tprintf(_T("__ImageBase returned -> address:0x%x with name: %s\r\n"), hCurrentModule, szModuleName);
HMODULE hCurrentExecutingModule=NULL;
//获得DumpModule函数所在文件的实例句柄
DWORD r = GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCTSTR)DumpModule, &hCurrentExecutingModule);
GetModuleFileName(hCurrentExecutingModule, szModuleName, MAX_PATH);
_tprintf(_T("GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) returned -> address:0x%x with name: %s\r\n"),
hCurrentExecutingModule, szModuleName);
}
前一个进程的实例
wWinMain(
_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nShowCmd
);
hPrevInstance为前一个实例的句柄,但在Win32始终为NULL,其存在只是兼容Win16。Win16时代所有进程共享同一进程空间,为了指示程序的不同实例知道某些事已经被前一个实例做了,如注册Window Class,所以需要前一实例句柄;但Win32开始,每个进程有自己独享的进程空间,因而不再需要hPrevInstance,但为了让老代码还能运行,因此保留该参数,并永远设为NULL。
进程的命令行
分解命令行参数代码
Int nNumArgs;
PTSTR *ppArgv = CommandLineArgv(GetCommandLine(), &nNumArgs);
//use the arguments
If(*ppArgv[1] == _T('x')){
…
}
//free heap
HeapFree(GetProcessHeap(), 0, ppArgv);
进程环境变量
GetEnvironmentStrings和FreeEnvironmentStrings来操作进程环境变量
PTSTR pEnvBlock = GetEnviromentStrings();
// 注意处理=::=::以及空格
FreeEnvironmentStrings(pEnvBlock);
其他操作环境变量的函数
GetEnvironmentVariable
ExpandEnvironmentStrings
SetEnvironmentVariable
进程的关联性(亲缘性)
通常进程可以在任何CPU上执行,然而也可以强制其在可用的CPU某个子集上执行,称为进程的关联性(processor affinity),子进程将继承父进程关联性。
进程错误模式
进程当前目录
GetCurrentDirectory
SetCurrentDirctory
系统版本
GetVersion返回MS-DOS和Windows版本号,该函数的Windows版本好高字节和低字节反了
GetVersionEx返回更加详细的信息
VerifyVersionInfo可以测试系统详细的版本信息
CreateProcess函数
pszApplicationName: 如果不为NULL,则使用该参数,但必须包含完整的路径和扩展名;
pszCommandLine:如果pszApplicationName为NULL,则使用该参数,可以不包含扩展名,默认为.exe,如果不包含路径,CreateProcess将在以下目录查找:
- 主调进程的exe所在目录;
- 主调进程当前目录;
- Windows系统目录,即GetSystemDirectory返回的System32目录;
- Windows目录;
- PATH环境变量中列出的目录。
PsaProcess, psaThread, bInheritHandles可指定进程的安全描述参数和是否继承父进程所拥有的可继承内核对象。
fdwCreate包含了创建进程的标志位。
PvEnvironment包含了传给新进程的环境变量,如果为NULL则从父进程继承。
pszCUrDir允许父进程设置子进程当前驱动器和目录。
PsiStartInfo必须初始化各个成员为0,并设置好结构体大小,否则可能导致创建进程失败,即STARTUPINFO si = {sizeof(si)};
ppiProcInfo为系统返回给我们的参数,包含了被创建进程和其主线程的信息。
终止进程
终止进程有四种方式:
- 进程从main函数退出(推荐)
- 主动调用ExitProcess:显示的调用ExitProcess会导致全局,静态等C++对象析构函数不被调用,并且C运行时的许多其他清理工作也无法完成;
-
其他进程调用TerminalProcess:被kill的进程本身不会完成清理工作,但windows操作系统会保证这些清理工作,从而系统资源都会释放,也就是说进程在结束后不会遗漏任何资源:
- 进程的内存释放
- 所有打开的文件都关闭
- 所有的内核对象都减少引用数
- 所有的用户对象和GDI对象都销毁(区分内核,用户和GDI对象的标准:看其Create*或其他操作函数来自于哪个dll,kernel32.dll/user.dll/gdi.dll)
- 进程所有线程"自然死亡":几乎不会发生,除非被其他人调用了TerminalThread或者主动调用了ExitThread,进程的退出会被设为最后一个线程的退出代码。
进程终止时会做以下动作:
- 终止任何的遗留线程;
- 释放所分配的用户对象,GDI对象,关闭所引用的内核对象;
- 进程退出代码由STILL_ACTIVE变为传递给ExitProcess和TerminalProcess的参数(GetExitCodeProcess可获得该退出码);
- 进程内核对象的状态变成已触发状态;
- 进程内核对象引用计数减1(进程内核对象的寿命有可能比进程本身要长,因此其他进程就有机会获得进程终止后的退出代码,和统计信息)。
子进程
CreateProcess创建子进程后,可以通过Proces_Information来操作子进程,如果不需要的话,应立即CloseHandle来关闭子进程主线程(hThread)和子进程本身(hProcess)的句柄。WaitForSingleObject(hProcess, INFINITE)可以等待子进程结束。
进程安全
Windows Vista起引入了UAC(user account control),每次进程需要访问一个受保护或者具有更高系统完整性级别的资源时候,都需要用户的确认(即弹出UAC对话框,这点被证明很招人烦,微软在后续的Windows版本也大大降低了弹窗的频率)。即使一个用户是以管理员身份登录(大部分Windows用户都是这么干的),默认情况下,启动的进程也只是拥有一个被筛选过的安全令牌。
自动提升权限:程序可以嵌入清单资源(RT_MANIFEST)并在trustinfo段声明requestedExecutionLevel信息
手动提升权限:调用ShellExecuteEx设置lpVerb = "runas",lpFile="可执行文件"参数来申请提升权限,当然这需要用户的确认
权限上下文:
下面的函数可以用来获取权限上下文:
- OpenProcessToken
- GetTokenInformation
- CrateWellKnownSid
- CheckTokenMembership:
- IsUserAdmin:判断当前进程是否以管理员身份运行
枚举系统中的进程
两套枚举系统中进程的API:
- PDH.dll中Process32First, Process32Next
- PSAPI.dll中EnumProcesses
完整性级别(integrity level)
进程令牌还包含有进程的系统完整性级别,每个内核对象也有其相应的系统完整性级别,有了这两个信息,Windows系统就能在一个进程访问一个内核对象的时候比较其完整性级别,完整性级别低的进程无法修改和删除完整性高的对象,至于是否能读,取决于和对象相关的ACE(access control entry)的资源策略设置。
由于完整性级别比较发生在ACL(access control list)之前,所以如果其完整性级别较要访问的资源低,即使进程拥有访问资源的权限,访问也会被拒绝。例如,一个进程从网上下载和执行代码,这种设计就尤为重要。
GetProcessIntegrityLevel可以获得进程的完整新级别。