📄 013.txt
字号:
1. 创建进程
创建进程需要为新进程窗口的外观指定一些属性,就像使用Shell调用方式执行文件时的dwCmdShow参数一样,这些属性通过一个STARTUPINFO结构来指定:
STARTUPINFO STRUCT
cb DWORD ? ;结构的长度
lpReserved DWORD ? ;保留字段
lpDesktop DWORD ? ;NT下使用,指定桌面名称
lpTitle DWORD ? ;控制台程序使用,指定控制台窗口标题
dwX DWORD ? ;当新进程使用CW_USEDEFAULT参数创建
dwY DWORD ? ;窗口的时候将使用这些位置和大小属性
dwXSize DWORD ?
dwYSize DWORD ?
dwXCountChars DWORD ? ;控制台程序使用,指定控制台窗口行数
dwYCountChars DWORD ?
dwFillAttribute DWORD ? ;控制台程序使用,指定控制台窗口背景色
dwFlags DWORD ? ;标志
wShowWindow WORD ? ;窗口的显示方式
cbReserved2 WORD ?
lpReserved2 DWORD ?
hStdInput DWORD ? ;控制台程序使用:几个标准句柄
hStdOutput DWORD ?
hStdError DWORD ?
STARTUPINFO ENDS
在需要定制新进程的窗口的时候,才需要手工填写STARTUPINFO结构(比如需要将控制台程序的输入和输出重新定位时,可以改写hStdInput和hStdOutput字段),在大部分情况下,并不需要新进程的窗口有什么特殊之处,这时只要使用GetStartupInfo获取当前进程的STARTUPINFO并使用它的默认值就可以了:
.data?
stStartUp STARTUPINFO
.code
invoke GetStartupInfo,addr stStartUp
获取STARTUPINFO结构以后,就可以把它用在创建进程的函数CreateProcess中:
invoke CreateProcess,lpApplicationName,lpCommandLine,\
lpProcessAttributes,lpThreadAttributes,bInheritHandles,\
dwCreationFlags,lpEnvironment,lpCurrentDirectory,\
lpStartupInfo,lpProcessInformation
函数的各个参数定义如下。
● lpApplicationName——指向一个以0结尾的字符串,用来指定可执行文件名,如果这个参数指定为NULL,那么文件名可以在lpCommandLine参数指定的命令行参数中包括。
● lpCommandLine—指向一个以0结尾的字符串,用来指定命令行参数,如果lpApplicationName参数为NULL,那么命令行字符串的第一个组成部分用来指定可执行文件名;如果两个参数都不为空,那么lpApplicationName用做文件名,lpCommandLine用做命令行参数。
● lpProcessAttributes——指向一个SECURITY_ATTRIBUTES结构,用来指定新进程的安全属性,如果进程句柄不需要被其他子进程继承,可以在这里使用NULL。
● lpThreadAttributes——指向一个SECURITY_ATTRIBUTES结构,用来指定新进程的安全属性,如果进程句柄不需要被其他线程继承,可以在这里使用NULL。
● bInheritHandles——指定当前进程的句柄是否可以被新进程继承,如果指定TRUE,那么可以继承,一般在这里使用FALSE。
● dwCreationFlags——创建标志,指定新进程的优先级以及其他标志,这个参数类似于CreateThread函数中的同名参数,它可以是一些标志的组合,下面列出了一些常用的标志:
■ CREATE_NEW_CONSOLE——如果新进程是控制台程序,那么为它新建一个控制台窗口,而不是使用父进程的控制台窗口。
■ CREATE_SUSPENDED—新建进程的主线程一开始处于挂起状态,需要以后用 ResumeThread函数来恢复它的执行。
■ DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS——调试进程,相关内容在13.3一节的进程调试中会有详细介绍,如果同时指定DEBUG_ONLY_THIS_PROCESS标志,那么被调试的进程仅是被创建的子进程,否则子进程创建的“孙进程”也在被调试之列。
■ HIGH_PRIORITY_CLASS,IDLE_PRIORITY_CLASS,NORMAL_PRIORITY_CLASS和REALTIME_PRIORITY_CLASS——用来指定新进程的优先级。
● lpEnvironment——指向新进程的环境变量块,如果这个参数指定为NULL,表示让Windows拷贝当前进程的环境块当做子进程的环境块,如果程序需要将修改过的环境块传递给子进程,可以设置这个参数。
● lpCurrentDirectory——指向一个路径字符串,用来指定子进程的当前驱动器和当前目录,如果指定为NULL,子进程将引用父进程的当前路径。
● lpStartupInfo——指向前面介绍的STARTUPINFO结构。
● lpProcessInformation——指向一个PROCESS_INFORMATION结构,这个结构用来供函数返回新建进程的相关信息。
如果函数执行成功,返回值是非0值,否则函数返回0。新建进程的句柄在哪里呢?这些句柄就在lpProcessInformation参数指向的PROCESS_INFORMATION结构中。结构定义为:
PROCESS_INFORMATION STRUCT
hProcess DWORD ? ;进程句柄
hThread DWORD ? ;进程的主线程句柄
dwProcessId DWORD ? ;进程ID
dwThreadId DWORD ? ;进程的主线程ID
PROCESS_INFORMATION ENDS
新进程被创建的时候其主线程也同时被创建,主线程句柄也常常会被用到,所以函数要返回的值不仅仅是进程句柄,因此,单靠函数的返回值是无法返回足够的信息的,这就是CreateProcess函数用PROCESS_INFORMATION结构来返回信息的原因。同样,可以通过检测PROCESS_INFORMATION结构是否被填写来判断函数是否执行成功。
理解了这些参数的含义,就会发现CreateProcess函数的使用其实是很简单的,因为大部分参数都可以用默认值。例子程序中用下面的代码来创建新进程,读者可以看到大部分的参数都使用默认的NULL:
invoke lstrcpy,addr @szBuffer,addr szFileName
.if szCmdLine
invoke lstrcat,addr @szBuffer,addr szBlank
invoke lstrcat,addr @szBuffer,addr szCmdLine
.endif
invoke GetStartupInfo,addr stStartUp
invoke CreateProcess,NULL,addr @szBuffer,NULL,NULL,NULL,\
NORMAL_PRIORITY_CLASS,NULL,NULL,addr stStartUp,addr stProcInfo
例子代码中将lpApplicationName参数设置为NULL,并将文件名szfileName和命令行参数szCmdLine合成一个字符串存放在@szBuffer中,然后一并在lpCommandLine参数中指定,为什么不直接使用两个字符串呢,就像下面的代码一样:
invoke CreateProcess,addr szFileName,addr szCmdLine,NULL,NULL,NULL,\
NORMAL_PRIORITY_CLASS,NULL,NULL,addr stStartUp,addr stProcInfo
这是因为,这种用法在某种情况下可能引起错误。来做下面的实验。
首先将例子程序中的CreateProcess改成分开使用文件名和参数字符串,然后用这个程序去执行13.1.2一节中的Cmdline.exe程序,并尝试输入不同的内容就可以发现,当不指定命令行参数的时候,运行结果如图13.3左图所示:Windows会自动将文件名当做命令行参数的第一个组成部分传递给子进程,一切正常。
图13.3 CreateProcess中的命令行
但是指定了命令行以后,问题就出来了,右图是输入参数“aaa bbb ccc”时的结果,也就是说,当指定了命令行以后,Windows就不会自动在前面加上文件名了,假如被执行的文件将命令行中的第一项当做文件名来看待并将它忽略的话,就会丢失一个参数,遗憾的是,几乎所有的程序都是这样做的!读者可以通过这种方法试将一个文本文件名传递给Windows自带的Notepad.exe,结果就是Notepad.exe把它给丢弃了。
为了避免这个错误,程序需要将文件名添加到命令行字符串的前面,但这样的话,指定lpApplicationName参数也就变得多此一举了,因为这时不用指定这个参数函数也可以正常执行。
2. 结束进程
要结束一个进程的执行,可以使用ExitProcess函数。对于我们来说,这个函数是最熟悉的,因为在所有的例子程序中都用它来结束程序的执行:
invoke ExitProcess,dwExitCode
与线程结束时有个退出码类似,进程结束时也可以指定一个退出码,dwExitCode就用来指定进程的退出码。
ExitProcess函数只能用来结束当前进程,不能用于结束其他进程,包括当前进程创建的子进程,因为它并没有参数可以用来输入进程句柄。如果需要结束其他进程的执行,可以使用TerminateProcess函数:
invoke TerminateProcess,hProcess,dwExitCode
hProcess参数用来指定需要结束的进程的句柄,dwExitCode用来指定进程的退出码。
TerminateProcess函数不是一个推荐使用的函数,一般仅在很极端的情况下使用(如任务管理器用来结束停止响应的进程),因为它将目标进程无条件结束,被结束的进程根本没有机会进行扫尾工作,同时,目标进程使用的dll文件也不会收到结束通知,所以极有可能造成数据丢失。
当进程被结束的时候,系统做下面的工作:
(1)进程创建或打开的所有对象句柄被关闭。
(2)进程中的所有线程被终止。
(3)进程及进程中所有线程的状态被改为置位状态,以便让WaitForSingleObject函数正确检测。
(4)进程对象中的退出码字段从STILL_ACTIVE被改为指定的退出码。
当一个进程被结束的时候,并不影响它创建的子进程,进程对象也不会马上从内存中删除,因为可能其他进程还需要通过进程句柄检测进程状态,直到使用CloseHandle函数将进程句柄关闭以后,进程对象才真正被删除。
大家还记得在DOS下编写批处理文件的时候使用的ERRORLEVEL吗?批处理中可以通过检测ERRORLEVEL来执行不同的逻辑,这个ERRORLEVEL就是命令行窗口中上次执行的可执行程序返回的退出码。Win32中窗口程序的退出码是无法做这个用途了,但它也可以用来在程序退出后向父进程传递简单的状态信息。
要检测进程的退出码,可以使用GetExitCodeProcess函数:
invoke GetExitCodeProcess,hProcess,lpExitCode
hProcess参数指定被检测进程的进程句柄,lpExitCode指向一个双字变量,用来接收函数返回的退出码。如果执行成功,函数返回非0值并将退出码返回到lpExitCode指定的变量中,如果执行失败函数返回0。如果被检测的进程没有结束,那么返回到lpExitCode中的是STILL_ACTIVE。
通过检测子进程的退出码是否是STILL_ACTIVE,就可以得知子进程是否已经结束,但如果需要在父进程中等待子进程结束时,就没有必要在一个循环中不停地检测退出码。在上一章中介绍的WaitForSingleObject函数也可以用于等待进程结束,在程序中只要如下使用就可以了:
invoke WaitForSingleObject,hProcess,dwMilliseconds
如果超时参数dwMilliseconds指定INFINITE,表示在子进程结束前函数不会返回。
当不再使用进程句柄的时候,不要忘记关闭PROCESS_INFORMATION结构中返回的进程句柄和主线程句柄,关闭这两个句柄使用CloseHandle函数。
13.3 进 程 调 试
在DOS操作系统下,一个程序可以读写系统中的所有内存,所以可以方便地修改任何地方的代码和数据,不管这些代码和数据是不是自己所有的,另外,程序可以自由存取所有的寄存器,自由设置所有的中断,所以程序可以通过设置单步中断或断点中断来跟踪代码的执行。这些功能可以归结为对一个进程进行调试。
在Windows操作系统中,不同进程之间的地址空间是隔离的,要用指令直接存取其他进程地址空间中的代码和数据是不可能的,用户程序也没有权限去截获中断,甚至连在自己的代码段中写数据都是不合法的,那么在Windows中还可以实现类似DOS中的调试功能吗?答案是肯定的,但必须通过专用的API函数来完成,本节要讨论的就是这方面的内容。
13.3.1 获取运行中的进程句柄
要对进程进行某种操作,就必须首先知道该进程的进程句柄或者进程ID,否则一切无从谈起,对于程序自己创建的子进程来说,CreateProcess函数返回了进程句柄和进程ID,但如果需要调试系统中已经运行的进程,那就必须首先获取它们的句柄才行。
Win32中并没有直接获取其他进程句柄的函数,但如果知道进程ID,可以由此得到进程句柄,所以可以首先通过某种途径获取进程ID。
1. 从窗口句柄获取进程句柄
获取进程ID的方法之一是使用GetWindowThreadProcessId函数,这个函数可以从一个窗口句柄获得创建该窗口的进程的进程ID,而通过FindWindow函数得到窗口句柄是很简单的,所以GetWindowThreadProcessId函数的用途相当广泛。该函数的用法是:
invoke GetWindowThreadProcessId,hWnd,lpdwProcessId
其中hWnd参数指定需要用来获取进程ID的窗口句柄,lpdwProcessId指向一个双字变量,函数在这里返回创建窗口的进程ID,函数的返回值是目标进程中创建该窗口的线程的线程句柄(一个有用的副产品!)。
得到了进程ID以后,就可以通过OpenProcess函数来获取该进程的句柄了:
invoke OpenProcess,dwDesiredAccess,bInheritHandle,dwProcessId
.if eax
mov hProcess,eax
.endif
函数的参数定义如下。
● dwDesiredAccess——指定需要对该进程进行的操作,要对目标进程进行某种操作,必须指定操作代码,但是在Windows NT操作系统中,对其他进程操作需要有相应的权限,如需要结束目标进程就必须有PROCESS_TERMINATE权限才行,当权限不够的时候,打开进程的操作就会失败。一般来说,除了系统进程以外,可以对其他进程进行任何操作,操作码可以是以下取值的组合:
■ PROCESS_ALL_ACCESS——等于下面全部操作码的组合。
■ PROCESS_CREATE_THREAD——允许创建远程线程。
■ PROCESS_DUP_HANDLE——允许进程句柄被复制。
■ PROCESS_QUERY_INFORMATION——允许使用GetExitCodeProcess函数查询进程的退出码或使用GetPriorityClass函数查询进程的优先级。
■ PROCESS_SET_INFORMATION——允许使用SetPriorityClass函数设置进程的优先级。
■ PROCESS_TERMINATE——允许终止进程。
■ PROCESS_VM_OPERATION—允许使用WriteProcessMemory函数或VirtualProtectEx函数修改进程的地址空间。
■ PROCESS_VM_READ——允许对进程的地址空间进行读操作。
■ PROCESS_VM_WRITE——允许对进程的地址空间进行写操作。
● bInheritHandle——指明返回的进程句柄是否可以被当前进程的子进程继承,如果参数指定为TRUE,则句柄可以被继承。
● dwProcessId——指定目标进程的进程ID。
如果函数执行成功,返回值是被打开的进程句柄。如果函数执行失败则返回NULL。一般打开失败的原因是由权限不够引起的。当完成对目标进程的操作以后,必须使用CloseHandle将获得的句柄关闭。
2. 从快照函数获取进程句柄
使用GetWindowThreadProcessId获取进程ID的先决条件是进程必须创建了窗口,对于在后台运行的没有窗口的进程该如何处理呢?这就要通过枚举系统中运行的进程来解决了,这个功能可以由CreateToolhelp32Snapshot函数来实现。
通过CreateToolhelp32Snapshot函数可以获得一个进程的列表,可以从列表中得到进程的ID、进程对应的可执行文件名和创建该进程的父进程ID等数据,这个函数支持Windows 9x系列和Windows 2000及以上的系统,不支持Windows NT 4.0(幸好使用NT 4.0的机会已经不多了)。
CreateToolhelp32Snapshot函数的名称比较奇怪,“Snapshot”是快照的意思,难道函数和拍快速成像照片有某种联系吗?没有联系,“快照”只是函数执行方式的一种形像的比喻罢了。就像自然界中的生命循环一样,系统中的进程也是生生不息的,随时都可能有进程被结束,也随时会有新的进程诞生。“快照”保留了函数被调用时的进程列表,在以后读取“快照”数据的过程中如果有进程创建或结束,就不会影响“快照”中的列表,就好比我们照了一张照片后走人,照片还是可以留下来慢慢地看。
所附光盘的Chapter13\ProcessList目录中的例子演示了快照函数的使用方法,程序显示了如图13.4所示的列表框,用户可以选择列表框中的某个进程,并且使用“终止”按钮将它结束。左图是程序在Windows 2000下的运行结果,右图是程序在Windows 98下运行的结果,Windows 2000下的文件名是不带路径的,可见函数在不同操作系统下得到的可执行文件名的表现方式稍微有些不同。
图13.4 “快照”例子的运行界面
目录中的ProcessList.rc文件定义了图13.4所示的对话框。
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN 1000
#define DLG_MAIN 1000
#define IDC_PROCESS 1001
#define IDC_REFRESH 1002
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN ICON "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 76, 95, 190, 108
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "进程列表"
FONT 9, "宋体"
{
LISTBOX IDC_PROCESS, 8, 5, 173, 86, LBS_STANDARD | LBS_SORT
PUSHBUTTON "刷新(&R)", IDC_REFRESH, 87, 90, 45, 14
DEFPUSHBUTTON "终止(&T)", IDOK, 137, 90, 45, 14, BS_DEFPUSHBUTTON | WS_DISABLED
| WS_TABSTOP
}
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
汇编源文件ProcessList.asm的内容如下:
.386
.model flat, stdcall
option casemap :none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -