《Windows核心编程》第4章 进程

进程通常被定义成一个正在运行的程序的一个实例。它由两部分构成:
1) 一个内核对象,操作系统用它来管理进程;
2) 一个地址空间,其中含所有可执行文件或DLL 模块的代码和数据。

一个进程可以有一个或多个线程,所有线程都在进程的地址空间中执行其代码。每个进程至少要有一个线程来执行进程地址空间包含的代码。当系统创建进程的时候,会自动为进程创建第一个线程,称之为主线程。

对于所有要运行的线程,操作系统会轮流为每个线程调度 CPU 时间。它会采取轮询方式为每个线程都分配可运行的时间片。对于多CPU 的系统而言,操作系统会采取更加复杂的算法来为线程分配CPU 时间。

Windows 应用程序必须有一个入口点函数,应用程序开始运行时,这个函数会被调用。但是实际上操作系统在之前会调用一个C/C++ 运行时的启动函数(WinMainCRTStartup/ wWinMainCRTStartup/mainCRTStartup/wmainCRTStartup )。该函数将初始化C/C++ 运行库,使我们能调用malloc 和free 之类的函数,同时还确保了在我们的代码开始执行之前,我们声明的任何全局和静态C++ 对象都能被正确的构造。

所有C/C++ 运行库启动函数都做相同的事情,区别在于它们要处理的是ANSI 还是Unicode ,以及在初始化C 运行库后,它们调用的是哪一个入口点函数。这些启动函数所做的工作:
1) 获取指向新进程的完整命令行的一个指针;
2) 获取指向新进程的环境变量的一个指针;
3) 初始化C/C++ 运行库的全局变量;
4) 初始化C 运行库内存分配函数和其他底层I/O 例程使用的堆;
5) 调用所有全局和静态C++ 类对象的构造函数。

入口点函数返回后,启动函数将调用C 运行库函数exit ,向其传递返回值。exit 函数执行以下任务:
1) 调用_onexit 函数调用所注册的任何一个函数;
2) 调用所有全局和静态C++ 类对象的析构函数;
3) 在DEBUG 版本中,如果设置了_CRTDBG_LEAK_CHECK_DF 标志,就通过调用函数_CtrDumpMemoryLeaks 来 内存泄露报告;
4) 调用操作系统的ExitProcess 函数,向其传入入口点函数的返回值。其结果当然是操作系统杀死该进程,并设置它的退出代码。

加载到进程地址空间的每一个可执行文件或DLL 文件都被赋予了一个独一无二的实例句柄。可执行文件的实例被当做(w)WinMain 函数的第一个参数hInstanceExe 传入。在需要加载资源的函数调用中,一般都要提供此句柄的值。通常都会将hInstanceExe 这个参数保存在一个全局变量中,使其很容易被可执行文件的所有代码访问。

参数hInstanceExe 的实际值是一个内存基地址,系统将可执行文件的映像加载到进程地址空间中的这个位置。可执行文件的映像具体加载到哪一个基地址,是由链接器决定的。不同的链接器使用不同的默认基地址。

如果想知道一个可执行文件或DLL 文件被加载到进程地址空间的位置,可以使用GetModuleHandle 函数来返回一个句柄/ 基地址:
HMODULE GetModuleHandle(PCTSTR pszModule);
此外调用函数GetModuleHandlEx 也可以实现。

GetModuleHandle 的两个重要特征:1 )它只检查主调进程的地址空间;2 )如果向其传递参数NULL ,会返回进程地址空间中的可执行文件的基地址。所以即使调用GetModuleHandle (NULL )的代码是在一个DLL 文件中,返回值仍是可执行文件的基地址,而非DLL 文件的基地址。

C 运行库的启动代码开始执行一个GUI 应用程序时,会调用Windows 函数GetCommandLine 来获取进程的完整命令行,忽略可执行文件的名称,然后将指向命令行剩余部分的一个指针传给WinMain 的pszCmdLine 参数。

应用程序可以通过调用函数CommandLineToArgvW, 将任何Unicode 字符串分解成单独的标记。该函数原型为:PWSTR* CommandLineToArgvW (PWSTR pszCmdLine,// 指向命令行的字符串,可以通过调用函// 数GetCommandLineW 获得
                   int* pNumArgs); // 整数的地址,被设为命令行中实参的数目
CommandLineToArgvW 返回的是一个Unicode 字符串指针数组的地址。函数在内部分配内存,应用可以依靠系统在进程终止时释放该内存,也可以通过调用HeapFree 来自己释放它。

每个进程都有一个与它关联的环境块,这是在进程地址空间内分配的一块内存。有两种方式可以访问环境块,它们使用了不同形式的输出,需要采用不同的方法来解析。
方式1 :通过调用GetEnvironmentStrings 函数来取得完整的环境块。如果不在需要该内存块,应该调用FreeEnvironmentStrings 函数来释放它。
方式2 :这种是CUI 程序专用的,它通过应用程序main 入口点函数所接受的TCHAR* env[] 参数来实现。env 是一个字符串指针数组,每个指针都指向一个不同的环境变量(格式为“名称= 值”)。

可以使用GetEnvironmentVariable 来判断一个环境变量是否存在;如果存在,它的值是什么。对于存在可替换字符串的情况,可以使用函数 ExpandGetEnvironmentStrings 。可以使用SetEnvironmentVariable 来添加、删除一个变量,或者修改一个变量的值。

进程可以调用SetErrorMode 函数来告诉系统如何处理一些错误。默认情况下,子进程会继承父进程的错误模式标志。父进程也可以在调用CreateProcess 时指定标志参数来阻止子进程继承其错误模式。

系统在内部跟踪记录着一个进程的当前驱动器和目录。这种信息是以进程为来单位来维护的,所以如果进程的某个线程更改了当前驱动器或目录,那么对于该进程中的所有线程而言,这个信息被更改了。可以调用GetCurrentDirectory 和SetCurrentDirectory 来获取和设置其所在进程的当前驱动器和目录。

C 运行库函数_chdir 函数在内部调用SetCurrentDirectory ,但还会调用函数SetEnvironmentVariable 来添加或修改环境变量,从而使不同驱动器的当前目录得以保留。

要想得到当前系统的版本信息,可以调用函数GetVersionEx 。此外,函数VerifyVersionInfo 能对主机系统和应用程序要求的版本进行比较。

 调用CreateProcess 时,系统将创建一个进程内核对象,其初始使用计数为1 。进程内核对象不是进程本身,而是操作系统用来管理这个进程的一个数据结构。然后系统为新进程创建一个虚拟地址空间,并将可执行文件(和所有必要的DLL )的代码及数据加载到进程的地址空间中。再然后系统为新进程的主线程创建一个线程内核对象(其使用计数为1 )。当然线程内核对象也是操作系统用来管理线程的一个数据结构。这个主线程一开始就会执行C/C++ 运行时的启动函数,最终会调用应用程序的WinMain ,wWinMain ,main 或wmain 函数。如果系统成功创建了新进程和新线程,CreateProcess 函数返回TRUE 。

对于函数CreateProcess 中参数STARTUPINFO ,应该这样将其结构中所有成员初始化为0 ,并将cb 成员设为此结构大小,即:STARTUPINFO si = {sizeof(si)};
如果没有把该结构的内容清零,则成员将包含主调线程的栈上的垃圾数据。把这种垃圾数据传给CreateProcess ,有可能会造成新进程创建失败,具体取决于垃圾数据的内容。

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

可以使用GetCurrentProcessId (或GetCurrentThreadId )来得到当前进程(正在运行的线程)的ID 。可以使用GetProcessId( 或GetThreadId) 来获得与指定句柄对应的一个进程(线程)的ID 。根据一个线程句柄,可以调用GetProcessIdOfThread 来获得其所在进程的ID 。
 
如果应用程序需要与它的“创建者”通信,最好不要使用ID (因为ID 会被系统重用),而应该定义一个更持久的通信机制,比如内核对象、窗口句柄等。

进程可以通过以下四种方式终止:
l  主线程的入口点函数返回(强烈推荐的方式)
l  进程中的一个线程调用ExitProcess 函数(要避免使用)
l  另一个进程中的线程调用TerminateProcess 函数(要避免使用)
l  进程中的所有线程都“自然死亡”(这种情况几乎不会发生)

让主线程的入口点函数返回,可以确保下列操作被执行:
l  该线程创建的任何C++ 对象都将由这些对象的析构函数正确销毁
l  操作系统将正确释放线程栈使用的内存
l  系统将进程的退出代码设为入口点函数的返回值
l  系统将递减进程内核对象的使用计数

调用ExitProcess 或ExitThread 会导致进程或线程直接终止运行,再也不会返回当前函数调用。C/C++ 应用程序应避免调用这些函数,因为C/C++ 运行库可能不能执行正确清理工作。

一个进程终止时,系统会依次执行以下操作:
1) 终止进程中遗留的任何线程;
2) 释放进程分配的所有用户对象和GDI 对象;
3) 进程退出代码从STILL_ACTIVE 变为传给ExitProcess 或TerminateProcess 函数的代码;
4) 进程内核对象的状态变成已触发状态;
5) 进程内核对象的使用计数递减1 。
 
可以通过GetExitCodeProcess 来获得已经终止的一个进程的退出代码。该函数对查找进程内核对象,并从内核对象的数据结构中提取出用于标识退出代码的成员。注意,任何时候都可以调用这个函数。如果在调用时,进程还没有终止,则退出代码就是STILL_ACTIVE 标识符对应的值。

vista 以前的系统,用户用管理员账户登录时,会创建一个安全令牌,每当有代码视图访问一个受保护的安全资源时,操作系统就会使用这个安全令牌,这个令牌与新建的所有进程关联,第一个是资源管理器,后者随即将令牌拿给它的所有子进程。

在vista 中,用户使用Administrator 这样的一个账户登录时,除了与这个账户对应的安全令牌外,还会创建一个经过筛选的令牌,后者将只被授予标准用户权限,以后系统代表最终用户启动的所有新进程都会关联这个筛选令牌。

对权限提升/ 筛选的进程进行调试可能比较麻烦,但你可以遵循一条非常简单的黄金法则: 希望被调试的进程继承什么权限,就以哪种权限启动visual Studio 。
如果需要调试的是一个以标准用户身份运行的已经筛选的进程,就必须以标准用户的身份来启动VisualStudio ,每次单击它的默认快捷方式(或通过开始菜单启动) 时,都是以标准用户的什么启动它的,否则被调试的进程会从以管理员身份启动的一个Visual Studio 实例中继承提升后的权限,这并不是你所期望的。
如果需要调试的是一个以管理员身份运行的进程啊(例如,根据那个进程的manifest 文件中描述的,它可能必须以管理员身份运行),那么 visualStudio 必须同样以管理员身份启动,否则就会显示一条错误消息,指出“请求的操作需要提升权限”,而且被调试的进程根本不会启动。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值