第四章进程
一、基本知识
1、 进程的定义,一个正在运行的程序的一个实例。由两部分组成,内核对象(系统保存进程统计信息的地方,用来管理进程)和地址空间(进程的所有执行体、数据段、堆栈等)。
2、 Windows有2大类应用程序。CUI(控制台用户界面)和GUI(图形用户界面),划分界限模糊。其中,关联到了VS集成环境链接器的各种开关。
3、 启动函数在进行一系列初始化(获取新进程的完整命令行的指针、环境变量指针、初始化C/C++运行库的全局变量、C运行库内存分配函数malloc和calloc及其他低级I/O历程使用的堆、调用全局和静态C++类对象的构造函数)后进入入口函数。
4、 进程实例句柄,独一无二。HMODULE和HINSTANCE是一回事吧,16位Windows除外。相关的函数就有HMOUDULEGetModuleHandle(PCTSTR pszModule),参数为NULL则是主调进程的(在DLL中调用的话,既是调用DLL的进程)可执行文件的基地址(HINSTANCE)。
5、 进程的命令行,一个新进程在创建时,都会接收一个命令行。WinMain的pszCmdline相关(忽略了执行体文件的名称的)。GetCommandLine返回的是同一块命令行内存,一般用来只读。好习惯的话,应该创建个缓冲区来。
6、 进程的环境变量,每个进程都有这么个与之关联的环境块,是在进程地址空间内分配的一个内存块。可通过GetEnvironmentStrings函数来获取完整的环境块。格式就不多说了。释放环境变量应该用BOOL FreeEnvironmentStrings(PTSTR pszEnvironmentBlock)函数。(这样就可以不管它’\0’位置怎样了) CUI程序还有专用的访问方法,通过main入口函数所接收的TCHAR* env[]参数来实现。env是一个字符串指针数组,每个指针都指向一个不同的环境变量(定义采用常规的“名称=值”的格式),NULL指针表示数组尾。
7、 用户登录Windows时,系统会创建shell进程。通过检查注册表中的两个项来获得初始的环境字符串。第一个项包含应用于系统的所有环境变量的列表:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SessionManager\Environment第二个项包含应用于当前登录用户的所有环境变量的列表:HKEY_CURRENT_USER\Environment。
8、 通过修改注册表修改环境变量后,用户需要注销并重新登录才能生效。或者给其它主窗口发送WM_SETTINGCHANGE消息。SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)TEXT("Environment"))。
9、 子进程会继承父进程的环境变量,但会受到父进程的控制(访问权限)。不过,子进程的环境块和父进程的环境块不共享一块内存。既是子进程的环境变量改变不影响父进程。
10、 环境变量的其它相关操作:GetEnvironmentVariable,ExandEnvironmentStrings,SetEnvironmentVariable。
11、 进程的错误模式,SetErrorMode函数。
12、 进程当前所在的驱动器和目录,文件路径查找使用。该信息以进程为单位进行维护(GetCurrentDirectory、SetCurrentDirectory)。可以提供多个驱动器的当前目录。GetFullPathName获取当前完整目录。
13、 系统版本,暂时过滤……
二、CreatProcess函数
1、 一个线程调用CreatProcess时,系统将创建一个进程的内核对象,初始化计数器为1。然后为新进程创建一个虚拟地址空间,并将执行体文件(和所有必要DLL)的代码及数据加载到进程的地址空间。
2、 CreatProcess在进程完全初始化之前就返回TRUE了,这意味着操作系统的加载器尚未定位到所有必要的DLL。如果说有DLL找不到或者无法初始化,那么这个进程就会终止。
CreatProcess函数的原型如下:
BOOL WINAPI CreateProcess(
__in_opt LPCTSTRlpApplicationName,
__inout_opt LPTSTRlpCommandLine,
__in_opt LPSECURITY_ATTRIBUTESlpProcessAttributes,
__in_opt LPSECURITY_ATTRIBUTESlpThreadAttributes,
__in BOOLbInheritHandles,
__in DWORDdwCreationFlags,
__in_opt LPVOIDlpEnvironment,
__in_opt LPCTSTRlpCurrentDirectory,
__in LPSTARTUPINFOlpStartupInfo,
__out LPPROCESS_INFORMATIONlpProcessInformation
);
3、 参数lpApplicationName和lpCommandLine分别代表新进程所要使用的执行体文件名称,以及要传给新进程的命令行字符串。lpCommandLine是个LPSTR变量,也就是意味着需要一个非“常量字符串”。因为在CreatProcess内部会暂时地改变命令行字符串,所以不能为只读。否则会出错。所以需要先把常量字符串拷贝到一个临时缓冲区里。
4、 如果lpApplicationName参数为NULL的时候,可以使用lpCommandLine参数来指定一个完整的命令行,供CreateProcess创建新进程。CreateProcess解析lpCommandLine字符串时,会检查字符串中的第一个标记,如果是执行体文件的名称(无扩展名默认为exe),则为按照以下顺序搜索。a、主调用进程.exe文件所在的目录;b、住调用进程的当前目录;c、Windows系统目录,既是GetSystemDirectory返回的System32子文件夹;d、Windows目录;e、PATH环境变量中列出的目录。当然,如果命令行包含一个完整的路径的时候,就只会用该完整路径来查找执行体了。
5、 lpProcessAttributes、lpThreadAttributes和bInheritHandles参数,与新进程的内核对象操作权限有关。创建新的进程,系统必须创建一个进程内核对象和一个线程内核对象(用于进程的主线程)。一般设置前两个参数为NULL的话,系统会为这两个内核对象(进程内核对象和线程内核对象)指定默认的安全描述符。
6、 dwCreationFlags参数,标志了影响新进程创建方式的标志。多个标志可以用按位OR运算符来组合。同时,还允许指定一个优先级类。不过,书上说这样做没有多大必要,而且对于大多数应用程序,都不应该这样做,说是系统会为新进程分配一个默认的优先级类。
7、 lpEnvironment参数,指向一个内存块,该内存块包含新进程要使用的环境变量字符串。大多数时候为NULL,导致子进程继承父进程使用的环境变量。
8、 lpCurrentDirectory参数,允许父进程设置子进程的当前驱动和目录。如果为NULL,则子进程的工作目录与父进程的一样。该参数必须执行一个用0来终止的字符串,同时必须在路径中指定一个盘符(驱动器号)。
9、 lpStartupInfo参数,指向STARTUPINFO结构。然后就该学习这个结构了。Windows在创建新进程的时候使用这个结构的成员。大多数应用程序都希望生成的应用程序只是使用默认值。最起码要将此结构中的所有成员初始化为0,并将bd城花园设为此结构的大小(STARTUPINFO si = { sizeof(si) };)。如果没有把结构的内容清零,则成员将包含主调用线程的堆栈上的垃圾数据,可能会造成破坏。然后把就要结合刚刚的dwCreationFlags的标志了,既是是否允许使用STARTUPINFO的某些成员。
10、 关于STARTUPINFOEX的另外一个字段lpAttributeList,还需要再继续看看。
11、 lpProcessInformation参数,指向一个PROCESS_INFORMATION结构。就是用来保存新创建的进程的基本信息的结构吧。有hProcess、hThread、dwProcessId、dwThreadId。需要注意的都是ID的时效性,既是ID是随机分配的,一般不要使用ID,用内核对象或者窗口句柄等。
三、终止进程
1、 四种方式终止:a、主线程的入口函数返回(强烈推荐);b、进程中的一个线程调用ExitProcess函数(要避免这个方式);c、另一个进程中的线程调用TerminateProcess函数(要避免这种方式);d、进程中的所有线程都“自然死亡”(很难发生)。
2、 主线程的入口函数返回,可以保证主线程的所有资源都被正确清理。a、该线程创建的任何C++对象都将由这些对象的析构函数正确销毁;b、操作系统将正确释放线程堆栈使用的内存;c、系统将进程的退出代码(在进程内核对象中维护)设为我们的入口函数的返回值;d、系统会递减进程内核对象的使用计数。
3、 ExitProcess函数,进程中的其它任何线程都会随进程一起终止。Windows Platfrom SDK文档支出,一个进程在其所有线程都终止之后才会终止。从操作系统的角度出发,这种说法是正确的。但是C/C++运行时的启动代码确保了下面这一点:一旦你的应用程序的主线程从它的入口函数返回,那么不管当前在进程中是否正在运行其他线程,都会调用Exitprocess来终止进程。不过,如果在入口函数中调用ExitThread,而不是调用ExitProcess或者简单返回,应用程序的主线程将停止执行,但只要进程中还有其他线程正在运行,进程就不会终止。ExitProcess会造成进程“当场死亡”,C++对象不会被正确析构。
4、 TerminateProcess函数,任何线程都可以调用其来终止另一个进程或者它自己的进程。应该在无法通过其它方法来强制进程退出时使用。在这种情况下,进程无法将它再内存中的任何信息转存到磁盘上。因为没有被终止的通知。不过,操作系统会在进程终止之后彻底进行清理,确保不会泄露任何操作系统资源。但TerminateProcess是异步的,返回的时候不一定马上被杀死了,需要配合类似WaitForSingleObject(该进程对象变为signaled)的函数。
5、 GetExitProcess函数,可以用来获取已经死亡的一个进程的退出代码,需要句柄,既是在该进程死亡前,需要获得其句柄。任何时候都可以调用这个函数,如果进程没有终止,则是STILLZ_ACTIVE标志,如果终止了,则返回实际的而退出代码。
四、子进程
1、 单任务同步机制,共享数据方式(用于进程通信),内存映射文件是最方便的方式。
2、 运行分离的子进程,一旦进程创建并开始执行,父进程就不再与新进程通信,或者不用等他完成工作之后才继续自己的工作。Windows资源管理器就是这样工作的。方法:创建成功后关闭新的进程以及其主线程的句柄。
五、UAC(用户账号控制)
1、 安全令牌,每当有代码视图一个受保护的安全资源时,操作系统就会要求出示这个安全令牌。
2、 经过筛选的令牌,只授予标志用户的权限。权限受限的进程无法访问需要更高权限才能访问的安全资源。
3、 可以要求操作系统提升权限,但只能再进程边界上提升。默认情况下,一个进程启动时,它会与当前登录用户的筛选令牌关联起来。要为进程授予更多的权限,需要指示Windows做这样一件事情:在进程启动之前,友好地征求最终用户提权的同意。作为最终用户,可以通过以管理员身份运行来提权。
4、 系统程序,征求对话框是蓝色横幅;有签名的程序,征求对话框是灰色的横幅;没有签名的程序,对话框则是橙色的横幅。
5、 普通用户登录的话,就需要一个over-the-shoulder登录,既是需要输入管理员信息。
6、 Windows只运行在进程边界上进行提权。一旦进程启动,再要求更多的权限就已经迟了。不过,一个未提权的进程可以生成另一个提升了权限的进程,后者将包含一个COM服务器。
7、 自动提升进程的权限,应用程序的执行体中嵌入了一种特定的资源(RT_MAIFEST),系统就会检查<trustInfo>区域,并解析其内容。其中有level属性,可以用来设置需要的权限。更改后,需要注销登录。
8、 手动提升进程的权限,需要调用ShellExecuteExitProcess函数(BOOLShellExecuteEx(LPSHELLEXECUTEINFO pExecInfo);)。而SHELLEXECUTEINFO结构中唯一有趣的字段是lpVerb和lpFile。前者必须被设为“runas”,后者必须包含使用提升后的权限来启动的一个执行体文件的路径。如果用户拒绝提权,将返回FALSE,可以通过GetLastError来使用ERROR_CANCELLED值判断。当一个进程使用提升权后的权限启动时,它每次用CreatProcess来生成另一个进程时,子进程都会获得和它的父进程一样的提升后的权限。
9、 假如一个应用程序使用一个筛选后的令牌来运行的,那么一旦视图调用CreatProcess来生成一个要求提权的执行体,这个调用就会失败,GetLastError会返回ERROR_ELEVATION_REQUIRED。
10、 何为当前权限上下文,两个问题:如何判断应用程序是否以管理员身份运行;如何判断它是以提升的权限来启动的,还是正在使用筛选的令牌运行。函数GetProcessElevation(P106)能返回提升类型和一个指出是否正在以管理员身份运行的布尔值。