让我们与星光探索者一起,探索Windows吧!

再探CreateProcess函数

我们用CreateProcess函数创建一个进程,CreateProcess函数的原型如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BOOL CreateProcess(
LPCTSTR lpApplicationName, // 应用程序名称
LPTSTR lpCommandLine, // 命令行参数
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 默认进程的安全属性,可传NULL
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 默认进程的安全属性,可传NULL
BOOL bInheritHandles, // 当前进程内核对象能否被子进程继承
DWORD dwCreationFlags, // 创建进程的方式
LPVOID lpEnvironment, // 创建进程使用的环境变量,
// 传NULL表示使用父进程的
LPCSTR lpCurrentDirectory, // 创建进程的驱动器和目录,
// 传NULL表示使用父进程的
LPSTARTUPINFOA lpStartupInfo, // 传递给新进程的信息
LPPROCESS_INFORMATION lpProcessInformation // 新进程返回的信息
);

下面详细探索CreateProcess的参数意义

lpApplicationName和lpCommandLine

lpApplicationName和lpCommandLine分别指定新进程要运行的可执行文件名称和新进程的命令行字符串。注意lpCommandLine参数类型是LPTSTR,在内部CreateProcess实际上会修改我们传给他的命令行字符串,在CreateProcess函数返回前,这个字符串会被还原成原来的模样。

由于Microsoft的C/C++编译器把字符串字面量放在只读内存中,这会引起访问违规(早期版本的Microsoft的C/C++编译器把字符串放到可读可写内存中,因此不会引起访问违规)。因此我们可以把常量字符串复制到一个临时缓冲区,然后再传入lpCommandLine位置。

如果lpApplicationName不为NULL,那么Windows将尝试打开叫lpApplicationName的可执行文件。如果lpApplicationName为NULL,Windows将解析lpCommandLine的第一个token,假定这是我们想运行的可执行文件名称,如果该可执行文件无拓展名,默认为.exe。

CreateProcess按以下顺序搜索可执行文件

  1. 主调进程.exe文件所在目录

  2. 主调进程当前目录

  3. Windows系统目录,即GetSystemDirectory返回的System32文件夹

  4. Windows目录

  5. PATH环境变量列出的目录

相信各位小伙伴在第一次配置编程环境时,总是去修改PATH环境变量,这就是原因所在了。当然如果这个文件名是一个完整路径,系统就直接查找这个路径的文件

lpProcessAttributes, lpThreadAttributes, bInheritHandles

lpProcessAttributes,lpThreadAttributes用于指定创建的进程的进程对象和线程对象的安全性,可以都传NULL,这样系统就为这两个内核对象指定默认安全描述符。

dwCreationFlags

dwCreationFlags参数标识了新进程的创建方式的标志,多个标志可用按位或组合起来

部分可用标志如下:

标志 作用
DEBUG_PROCESS 父进程希望调试子进程以及子进程将来生成的所有进程。该标志想系统表明,任何一个子进程(即被调试程序,debugee)中发生的特定的事件,要通知父进程(即调试器,debuger)
DEBUG_ONLY_THIS_PROCESS 类似DEBUG_PROCESS,但是只有生成的这个子进程发生特定事件会通知父进程
CREATE_SUSPENDED 让系统在创建新进程同时挂起其主线程,这样父进程就可以修改子进程地址空间的内存等。父进程修改之后,可调用ResumeThread允许子进程执行代码
DETACHED_PROCESS 阻止一个CUI应用程序的进程访问父进程的控制台窗口,因为默认情况下子进程会继续使用父进程的控制台窗口

此外我们还可以设置创建进程的优先级类,不过大部分应用程序都不这么做

lpEnvironment

新进程使用的环境字符串,如果传入NULL,子进程将继承父进程使用的一组环境字符串

lpCurrentDirectory

设置子进程的当前驱动器和目录,如果为NULL,新进程的工作目录与生成新进程的应用程序一样。

lpStartInfo

传递给新进程的信息,详细请看STARTUPINFOA (processthreadsapi.h) - Win32 apps | Microsoft Learn

lpProcessInformation

我们需要准备一个PROCESS_INFORMATION 结构,CreateProcess函数返回时初始化这个结构的成员。

在创建一个内核对象时,系统会给此对象分配一个独一无二的标识符,没有别的内核对象会有相同的ID编号,对于线程对象也是一样的。然后,对象分配到的ID不可能是0,Windows任务管理器将进程ID 0 与 系统空闲进程(System Idle Process)关联,实际上没有这个东西,任务管理器创建这个虚构的进程目的是将其作为Idle线程的占位符,在没有别的线程正在运行时,系统就运行这个Idle进程,System Idle Process线程数量始终等于计算机CPU数量,始终代表未被真实进程使用的CPU使用率。

如果子进程想和父进程保持通信,那么最好不应该使用对方的进程句柄,而采用别的方式(如内核对象,窗口句柄)。因为子进程创建之后,系统就认为父进程与子进程不再有父子关系,并且如果此时父进程已经死掉,其他创建的进程很可能会分配到这个死掉的父进程的ID,此时这个ID对应的就不是原来那个父进程了。一旦我们使用保存的进程ID,操作的就是新进程不再是原来那个。

可调用GetCurrentProcessId获得当前进程ID,GetCurrentThreadId获得当前运行的线程ID,GetProcessId获得与指定句柄对应的进程ID,GetThreadId获得与指定句柄对应的一个线程ID。GetProcessIdOfThread可以获得其所在进程的ID

终止进程的方式

进程可以通过4种方式终止:

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

  • 进程中一个线程调用ExitProcess函数(避免这么做)

  • 另一个进程中的线程调用TerminateProcess函数(避免这么做)

  • 进程中所有线程都“自然死亡”(几乎不会发生)

主线程入口点函数返回

一个应用程序,应该保证只有主线程入口点函数返回之后,这个应用程序才终止。对于主线程入口点函数返回方式,可以保证任何C++对象被正确析构,操作系统正确释放线程栈使用内存,系统将进程的退出代码设为入口点函数返回值,系统递减进程内核对象的使用技术

进程中一个线程调用ExitProcess函数

在进程的任意一个线程调用VOID ExitProcess(UINT fuExitCode)将导致这个进程终止,并设置进程退出代码为fuExitCode,如果ExitProcess后还有别的代码,那些代码永远不会执行。

当主线程的入口点函数(WinMain,wWinMain,main,或wmain)返回时,会返回到C/C++运行库启动代码,这样会正确清理进程使用的全部C运行时资源,然后C运行库启动代码调用ExitProcess,进程退出。这就是为什么主线程的入口点函数返回,整个进程都被终止。

TerminateProcess函数

调用BOOL TerminateProcess(HANDLE hProcess, UINT fuExitCode)可终止一个进程,任何进程都可以通过这种方式终止。只有在无法通过其他方式强制进程退出时,才应使用此函数。被终止的进程得不到自己要被终止的通知,因为应用程序不能正确清理,也不能阻止他自己被强行终止。

操作系统保证进程终止后会进行彻底的清理,确保不会泄露任何操作系统资源,进程使用的所有内存都会被释放,所有打开的文件都会被关闭,所有内核对象的引用计数都将递减,所有用户对象和GDI对象会被销毁。进程在终止后绝对不会泄露任何东西。

用户账户控制UAC

对于应用程序开发人员,影响最大的技术即为用户账户控制(User Account Control, UAC)。Microsoft注意到大多数用户都喜欢使用管理员(Administrator)账户登录Windows。这个账户用户几乎可以没有限制地访问重要系统资源。用户帐户控制 是 Microsoft 的总体安全构想的基本组件。 UAC 有助于缓解恶意软件的影响。

下图表示管理员与普通用户在登录时的不同之处。

默认情况下,标准用户和管理员在标准用户的安全上下文中访问资源并运行应用。 当用户登录到计算机时,系统会为该用户创建访问令牌。 访问令牌包含有关授予用户的访问权限级别的信息,包括特定安全标识符 (SID) 和 Windows 权限。

管理员登录时,将为用户创建两个单独的访问令牌:标准用户访问令牌和管理员访问令牌。 标准用户访问令牌包含与管理员访问令牌相同的特定于用户的信息,但会删除管理 Windows 特权和 SID。 标准用户访问令牌用于启动不执行管理任务的应用 (标准用户应用) 。 然后,标准用户访问令牌用于显示桌面 (explorer.exe) 。 Explorer.exe是所有其他用户启动的进程从中继承其访问令牌的父进程。 因此,除非用户提供许可或凭据来批准应用使用完整的管理访问令牌,否则所有应用都以标准用户身份运行。

作为管理员组成员的用户可以使用标准用户访问令牌登录、浏览 Web 和阅读电子邮件。 当管理员需要执行需要管理员访问令牌的任务时,Windows 会自动提示用户进行审批。 此提示称为提升提示,可以使用本地安全策略管理单元 (Secpol.msc) 或组策略来配置其行为。 有关详细信息,请参阅 用户帐户控制安全策略设置

启用 UAC 后,标准用户的用户体验不同于管理员审批模式下管理员的用户体验。 建议的、更安全的运行 Windows 方法是使主要用户帐户成为标准用户帐户。 以标准用户身份运行有助于最大程度地提高托管环境的安全性。 使用内置的 UAC 提升组件,标准用户可以通过输入本地管理员帐户的有效凭据来轻松执行管理任务。 标准用户的默认内置 UAC 提升组件是凭据提示。

以标准用户身份运行的替代方法是在管理员审批模式下以管理员身份运行。 使用内置的 UAC 提升组件,本地管理员组的成员可以通过提供审批轻松执行管理任务。 管理员审批模式下管理员帐户的默认内置 UAC 提升组件称为同意提示。

如: