《WINDOWS 环境下32位汇编语言程序设计》第13章 过程控制(1)

进程的概念在第12章中有所介绍,所谓进程,就是一个执行中的文件所使用的资源的总和,包括虚拟地址空间、代码、数据、对象句柄、环境变量和用来执行代码的线程等。只产生一个新的进程是很简单的,说穿了就是在一个程序中执行另一个程序而已,在DOS操作系统下编程的时候我们就对执行另一个文件使用的int 21h/4bh功能非常熟悉。

如果只为了介绍如何执行一个文件,那么就根本不需要使用单独的一章,实际上,有关进程的课题中最令人感兴趣的是如何在建立了一个进程后继续控制它,包括对它进行跟踪调试,读写其他进程的地址空间及进程的隐藏等,本章的重点就是介绍这些内容。

13.1节首先介绍一些与进程相关的周边知识,包括如何使用命令行参数和如何操作环境变量等。13.2节介绍进程的创建、终止和等待。13.3节介绍与进程调试有关的内容,包括如何枚举系统中运行的进程,读写进程的地址空间和调试API的使用等。13.4节的内容与病毒、木马等有害程序的防治有关,中间演示了这些有害程序常用的进程隐藏与创建远程线程等功能的实现,使读者对这些内容有了初步的了解。

13.1 环境变量和命令行参数

13.1.1 环境变量

1.什么是环境变量

环境变量就是在命令提示符下键入“Set”命令后列出来的内容,它的定义格式以XXX=YYY的形式来表示,其中的XXX是环境变量的名称,YYY是环境变量的值,下面的例子是笔者使用的计算机中列出的部分环境变量:

ALLUSERSPROFILE=C:\Documents and Settings\All Users
APPDATA=C:\Documents and Settings\Administrator\Application Data
BLASTER=A220 I7 D1 H7 P330 T6
SBPCI=C:\SBPCI
COMPUTERNAME=WORKGROU-86NSVP
ComSpec=C:\WINDOWS\system32\cmd.exe
SystemDrive=C:
SystemRoot=C:\WINDOWS
HOMEDRIVE=C:
ProgramFiles=C:\Program Files
HOMEPATH=\Documents and Settings\Administrator
LOGONSERVER=\\WORKGROU-86NSVP
OS=Windows_NT
Path=C:\WINDOWS\system32;C:\WINDOWS;c\tools;C:\WIN98\Twain_32\Nuscan
NUMBER_OF_PROCESSORS=1
PROCESSOR_ARCHITECTURE=x86
PROCESSOR_IDENTIFIER=x86 Family 6 Model 6 Stepping 0, GenuineIntel
PROCESSOR_LEVEL=6
PROCESSOR_REVISION=0600
PROMPT=$P$G
TEMP=C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp
TMP=C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp
USERDOMAIN=WORKGROU-86NSVP
USERNAME=Administrator
windir=C:\WINDOWS

环境变量按照用途可以分为3大类:

● 与系统运行相关的环境变量——这些变量的值与系统的正常运行息息相关。如PATH变量定义的是可执行文件的搜索路径,它直接影响系统搜寻可执行文件的位置和先后顺序;而TEMP和TMP变量将影响系统创建临时文件的位置;ComSpec变量定义命令行管理器的文件名,在DOS操作系统下,如果这个变量的定义错误,就会导致系统无法装入Command.com而挂起。

● 反映系统状态的环境变量——如NUMBER_OF_PROCESSORS,PROCESSOR_LEVEL和PROCESSOR_IDENTIFIER等是操作系统根据当前的硬件定义的;COMPUTERNAME,USERNAME和USERDOMAIN等变量是操作系统根据当前的机器名、登录用户等定义的;OS,HOMEDRIVE和ProgramFiles等则定义了操作系统的名称和安装位置。通过检测这些变量,应用程序可以了解系统的配置情况和其他一些重要信息。

● 应用程序自定义的环境变量——如上面列出的BLASTER和SBPCI是创新声卡自定义的变量。在本书例子程序的编译环境中,也定义了一些编译器和链接器使用的变量,如Include与Lib等,这些变量是应用程序根据自己的需要自定义的。

2.对环境变量进行操作

在命令提示符窗口中,可以通过“Set变量=内容”格式的命令来设置环境变量的值,也可以通过等号后面不带内容的“Set变量=”命令来删除某个环境变量。如果需要在程序中对环境变量进行操作,可以使用Win32中的以下几个API。

如果想获取一个环境变量的值,可以使用GetEnvironmentVariable函数:

invoke  GetEnvironmentVariable,lpVarName,lpBuffer,dwSize

lpVarName指向一个以0结尾的字符串,用来指定需要获取的环境变量名,lpBuffer指向用来接收变量值的缓冲区,dwSize参数指定缓冲区的大小。如果函数执行成功,返回值是返回到缓冲区中的字符数量(不包括结束的0字符);如果环境变量不存在,返回值是0;如果缓冲区太小以至于放不下环境变量内容,那么缓存区中不会返回任何内容,这时函数的返回值是需要的缓存区的大小,这就意味着,如果返回值比缓冲区的大小要大,那么必须扩大缓冲区后再次调用。

下面的代码演示了如何将PATH变量的值获取到szBuffer缓冲区中:

.data
szBuffer  db 200 dup (?)
szVarName db 'PATH',0
.code
invoke  GetEnvironmentVariable,addr szVarName,\
                            addr szBuffer,sizeof szBuffer

GetEnvironmentVariable函数只能用来获取已知名称的环境变量,如果需要枚举所有的环境变量,可以使用GetEnvironmentStrings函数。这个函数返回一个内存块指针,内存块中包含了所有的环境变量定义,通过扫描整个内存块就可以获得所有的环境变量定义。该函数没有输入参数:

invoke   GetEnvironmentStrings
mov      lpVar,eax

内存块中环境变量存放格式为“变量1=内容1”,0,“变量2=内容2”,0,……“变量N=内容N”,0,0,即每个定义字符串以0结束,然后开始下一个变量定义字符串,全部定义字符串的最后再以一个附加的0结束。

GetEnvironmentStrings函数返回的内存块是系统申请的,当不需要再使用的时候,需要将它释放,释放这个内存块并不等于删除全部环境变量,而仅是释放这份拷贝而已。释放使用的函数是FreeEnvironmentStrings:

invoke FreeEnvironmentStrings,lpVar

函数的输入参数lpVar就是GetEnvironmentStrings函数返回的内存块指针。

如果需要改变现存环境变量的值,设置新的环境变量或者删除某个环境变量,可以使用SetEnvironmentVariable函数:

invoke  SetEnvironmentVariable,lpVarName,lpValue

lpVarName指向环境变量的名称,lpValue指向一个以0结尾的字符串,用来指定环境变量的新值。

当lpVarName指定的环境变量已经存在且lpValue指向一个空串时,这个变量将被删除;如果lpValue指向的不是一个空串,那么环境变量的值将被改为这个新的字符串;如果lpVarName指定的环境变量不存在且lpValue指向的不是一个空串,那么系统将建立新的环境变量。

SetEnvironmentVariable函数的运行结果仅改变本进程的环境变量,并不会影响其他进程。比如,打开一个命令提示符窗口,在其中改变某些环境变量的设置,然后再打开另一个命令提示符窗口查看,就会发现这个新窗口中的环境变量并没有改变。

但是环境变量的值可以被子进程继承,如果在一个程序中创建了另一个进程,那么可以让这个子进程“看到”改动以后的环境变量,这就是在一个命令提示符窗口中改变了环境变量的设置,然后用命令行方式运行一些程序,改变的环境变量对这些程序的运行都有效的原因。

13.1.2 命令行参数

1.什么是命令行参数

在命令行中通过输入文件名来执行文件,在文件名后面跟的参数就是命令行参数,比如,通过Telnet连接到某个远程计算机的1234号端口,可以输入:

Telnet 192.168.0.11234

“Telnet”后面跟的“192.168.0.11234”就是命令行参数,它可以被程序获取,命令行参数是当做一个以0结尾的字符串被程序获取的。

对于窗口程序来说,命令行参数并不是必需的,因为大部分的窗口程序都在菜单中提供了“打开文件”、“选项”等功能,并不需要用户在命令行参数中指定,试想一下:如果窗口程序必须依靠命令行参数输入某些数据的话,那么用户就不能通过在任务管理器中双击图标来执行程序了,因为这样无法输入命令行参数。

但是某些情况下,命令行参数又是窗口程序的必然补充,比如,Windows中的文本文件往往被关联到记事本程序Notepad.exe上,直接双击文本文件,Windows就会自动执行Notepad.exe,并把文本文件名通过命令行参数传递给它,所以对于Notepad.exe来说,虽然已经在菜单中提供了“打开文件”功能,但也必须处理命令行参数,否则对关联文件就无法处理。

而对于控制台程序(如系统中的Ping.exe,Format.exe和Xcopy.exe等在命令行下运行的程序)来说,它们没有自己的窗口,也就无法通过菜单来选择某些功能,这时通过命令行传递各种参数就是必然的选择。

2.使用命令行参数

要获取命令行参数,可以使用GetCommandLine函数,这个函数没有输入参数,返回值是一个指向命令行参数字符串的指针:

invoke GetCommandLine
mov lpCmdline,eax

获取命令行参数字符串以后,首先必须对它进行处理,比如,对于上面的Telnet程序来说,第一个参数“192.168.0.1”指定主机名,第二个参数“1234”指定端口号,但是我们得到的却是一个连在一起的字符串,所以必须扫描字符串将两个参数分开后才能使用,而且,必须通过检查参数字符串防止用户输入了错误的参数。

Win32中并没有通用的用来扫描参数字符串的函数,有一个函数CommandLineToArgvW虽然可以用来对字符串进行扫描,但这个函数仅适用于Unicode字符串,而且仅在NT系列中得到支持,在Windows 9x系列中无法使用,为了使用这个函数而将程序限制在Windows NT下运行显然是得不偿失的。

C语言为用户考虑到了这一点,在C的初始化程序将控制权交到WinMain函数之前就已经对命令行参数进行了处理,并将参数按照空格划分成不同的部分放到argv数组中,参数数量则存放在argc变量中,在程序的任何地方都可以存取这些变量;但在Win32汇编中,这些工作就需要自己来做了,笔者在本节的例子程序中提供了两个通用函数,读者可以把它们用在其他程序中。在分析这个例子之前,先来看看在命令行参数字符串中究竟可以收到哪些内容,这些内容将决定分析字符串的算法。

读者可以编写一个很简单的测试程序Test.exe,在程序中调用GetCommandLine函数并将得到的命令行参数显示出来,假如把这个Test.exe放在“C:\Program files”目录下并使用不同的方法去执行它,执行的时候带3个参数“aaa bbb ccc”,就可以发现得到的命令行参数可能是下面的样子:

(1)test aaa bbb ccc
(2)test.exe aaa bbb ccc
(3)te"st".exe aaa bbb ccc
(4)c:\program" "files\test aaa bbb ccc
(5)"C:\Program files\Test.exe"
(6)"C:\Program files\Test.exe" "C:\aaa bbb ccc"

结果(1)到(4)显示的命令行参数字符串是在命令提示符下输入同样的执行命令得到的。需要注意的有两点:

首先是可执行文件名被当做命令行参数的第一个组成部分被传递过来了,所以要使用真正由用户输入的命令行参数,必须首先将这部分过滤掉。

其次是对可执行文件名的处理,输入test或test.exe可以正常执行文件并不奇怪,奇怪的是输入te"st".exe或者c:\program""files\test这样的文件名也能执行,难道这也是合法的文件名吗?

答案是肯定的,这是因为Windows将空格当做参数的分隔符,但是长文件名中同样可以使用空格,这就产生了冲突,为了解决这个问题,Windows使用双引号来界定,假如文件名中存在空格的话,必须将空格包含在两个双引号中,但双引号并不是只能用一对,也并不一定要用在文件名的头尾,实际上它们可以在文件名的任何地方出现,只要是成对的并且所有空格都被包含在某一对双引号中就可以了,双引号本身不是文件名的组成部分,Windows在最后会自动将它们剔除,但是传递到命令行参数中的时候还是保留了输入时的样子。

结果(5)是在文件管理器中通过双击Test.exe执行时得到的命令行字符串,结果(6)是在C盘的根目录下建立了一个名为“aaa bbb ccc”的文件,并在“打开方式”中选择Test.exe后得到的命令行字符串。从这些结果中可以发现:只要由Windows来打开文件,那么Windows就会用很规范的方式传递文件名,具体就表现在文件名肯定是带全路径的,并且不管文件名中是否包含空格,头尾都会被加上一个双引号。

根据这些特征,就不难写出一个通用的分析命令行参数的子程序来:

; AnalyzeCmdLineParam.asm
; 命令行参数分析的通用子程序
; 功能:
; _argc ---> 对命令行参数进行数量统计
; _argv ---> 取某个命令行参数
;-------------------------------------------------------------------
CHAR_BLANK 	equ 20h ;定义空格
CHAR_DELI 	equ '"' ;定义分隔符
;-------------------------------------------------------------------
; 取命令行参数个数 (arg count)
; 参数个数必定大于等于 1, 参数 1 为当前执行文件名
_argc proc 
	local @dwArgc 
	
	pushad 
	mov @dwArgc, 0
	invoke GetCommandLine 
	mov esi, eax 
	cld 
	
_argc_loop:
	;忽略参数之间的空格
	lodsb 					;mov al, [esi];  esi递增
	or al, al 
	jz _argc_end 
	cmp al, CHAR_BLANK
	jz _argc_loop 
	
	;一个参数开始
	dec esi 
	inc @dwArgc 
_argc_loop1:
	lodsb 
	or al, al 
	cmp al, CHAR_BLANK 
	jz _argc_loop 			;参数结束
	cmp al, CHAR_DELI 	
	jnz _argc_loop1 		;继续处理参数内容
 
	;如果一个参数中的一部分有空格,则用 " " 包括
@@:
	lodsb 
	or al, al 
	jz _argc_loop1
_argc_end:
	popad 
	mov eax, @dwArgc 
	ret 
_argc endp 



;--------------------------------------------------------------------
; 取指定位置的命令行参数
; argv 0 = 执行文件名
; argv 1 = 参数1 ...
;--------------------------------------------------------------------
_argv proc _dwArgv, _lpReturn, _dwSize 
	local @dwArgv, @dwFlag 
	
	pushad 
	inc _dwArgv 
	mov @dwArgv, 0
	mov edi, _lpReturn 
	
	invoke GetCommandLine 
	mov esi, eax 
	cld 
	
_argv_loop:
	;忽略参数之间的空格
	lodsb 
	or al, al 
	jz _argv_end 
	cmp al, CHAR_BLANK 
	jz _argv_loop 
	
	;一个参数开始
	;如果和要求的参数符合,则开始复制到返回缓冲区
	dec esi 
	inc @dwArgv 
	mov @dwFlag, FALSE 
	mov eax, _dwArgv 
	cmp eax, @dwArgv 
	jnz @F 
	mov @dwFlag, TRUE 
@@:
_argv_loop1:
	lodsb 
	or al, al 
	jz _argv_end 
	cmp al, CHAR_BLANK 
	jz _argv_loop 			;参数结束
	cmp al, CHAR_DELI 
	jz _argv_loop2 
	cmp _dwSize, 1 
	jle @F 
	cmp @dwFlag, TRUE 
	jne @F 
	stosb 
	dec _dwSize 
@@:
	jmp _argv_loop1 		;继续处理参数内容
 
_argv_loop2:
	lodsb 
	or al, al 
	jz _argv_end 
	cmp al, CHAR_DELI 
	jz _argv_loop1 
	cmp _dwSize, 1 
	jle @F 
	cmp @dwFlag, TRUE 
	jne @F 
	stosb 
	dec _dwSize 
@@:
	jmp _argv_loop2 
	
_argv_end:
	xor al, al 
	stosb 
	popad 
	ret 
_argv endp 

这两个通用子程序被存放在_CmdLine.asm文件中,读者可以在其他的程序中用include语句将它包含使用,其中函数_argc返回命令行参数的个数,当执行文件时没有附带参数的时候,函数的返回值一般是1,这时获取的命令行字符串中仅有一个组成部分——那就是可执行文件名;_argv函数则将指定编号的参数返回到一个缓冲区中,读者可以这样使用:

invoke _argv,dwArgv,lpReturn,dwSize

其中dwArgv参数指定要获取的参数编号,0表示获取字符串中的第一个组成部分(一般是文件名),1表示获取第二个组成部分,也就是在文件名后面输入的第1个参数,以此类推,函数对返回的字符串已经做了处理,丢弃了中间或者两端的所有双引号。lpReturn指向用来接收参数字符串的缓冲区,dwSize指定了缓冲区的大小。

这里有一个使用这两个函数的例子,源代码在所附光盘的Chapter13\CmdLine目录中,其中CmdLine.asm的内容如下:

; CmdLine.asm
; 命令行参数的获取和分析例子
;--------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Cmdline.asm
; Link /subsystem:windows Cmdline.obj
.386
.model flat,stdcall 
option casemap:none 
 
; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 

; 数据段
.data?
szBuffer1	byte 4096 dup(?)
szBuffer2 	byte 4096 dup(?)
szOutput 	byte 8192 dup(?)

.const 
szCaption 	byte '命令行参数',0
szFormat1 	byte '可执行文件名称:',0dh,0ah,'%s',0dh,0ah,0ah
			byte '参数总数:%d',0dh,0ah,0
szFormat2 	byte '参数[%d]:%s',0dh,0ah,0
 
; 代码段
.code 
include AnalyzeCmdLineParam.asm 

;main函数
main proc 
	invoke GetModuleFileName, NULL, offset szBuffer1, sizeof szBuffer1 
	invoke _argc 
	mov ebx, eax 
	invoke wsprintf, addr szOutput, addr szFormat1, addr szBuffer1, eax 
	
	xor esi, esi 
	.while esi < ebx 
		invoke _argv, esi, addr szBuffer2, sizeof szBuffer2 
		invoke wsprintf, addr szBuffer1, addr szFormat2, esi, addr szBuffer2 
		invoke lstrcat, addr szOutput, addr szBuffer1 
		inc esi 
	.endw 
	invoke MessageBox, NULL, addr szOutput, addr szCaption, MB_OK 
	invoke ExitProcess, 0
main endp 
end main 

程序很简单,首先调用GetModuleFileName函数获取可执行文件的文件名,这是为了方便读者和参数中获取的文件名做个对比,然后程序调用_argc函数获得参数数量,并根据这个数量循环获取每个参数。编译链接后输入命令:

cmdline aaa bbb "ccc ddd" eee

程序会显示出如图13.1所示的消息框。

图13.1 命令行参数例子的运行结果

可见,函数正确划分了命令行参数字符串中的各个参数。需要说明的是,为了能够在某个参数中使用空格,函数同样规定可以将参数中的空格用双引号包含,所以参数字符串中的"ccc ddd"被解释为一个参数并丢弃了两端的双引号。

13.2 执行可执行文件

13.2.1 方法一:Shell调用

Win32中可以通过ShellExecute和WinExec函数来执行另一个可执行文件,本节介绍这两个函数的用法。WinExec函数的使用方法是:

invoke  WinExec,lpCmdLine,dwCmdShow

lpCmdLine参数指向一个以0结尾的字符串,这个字符串中包含可执行文件加上命令行参数,如果被执行的文件会显示一个窗口,那么函数可以在dwCmdShow参数中指定窗口的显示方式,这个参数的定义同ShowWindow函数中的dwCmdShow参数。

【完整代码测试笔记】

; ShellCall.asm
; 命令行参数的获取和分析例子
;--------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff ShellCall.asm
; Link /subsystem:windows ShellCall.obj
.386
.model flat,stdcall 
option casemap:none 
 
; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 

; 数据段
.const 
szExeName	byte 'CmdLine', 0
 
; 代码段
.code 
;main函数
main proc 
	invoke WinExec, offset szExeName, 1
	invoke ExitProcess, 0
main endp 
end main 

运行结果:

如果文件被成功执行,那么函数返回一个大于31的值。使用WinExec函数执行文件和在Windows“开始”菜单的“运行”中键入命令在效果上是一样的。

另一个函数ShellExecute的功能相对比较多一点,函数的语法是:

invoke ShellExecute,hWnd,lpOperation,lpFile,lpParam,\
            lpDirectory,dwCmdShow

这个函数既可以用来执行一个可执行文件,也可以指定一个数据文件名让Windows自动查找关联到这个数据文件的可执行文件,并执行这个可执行文件来处理指定的数据文件,数据文件名会以命令行参数的方式传递给可执行文件。函数的参数定义如下。

● hWnd——用来指定被执行文件显示的窗口所属的父窗口。

● lpFile——用来指定文件名,文件名既可以是可执行文件也可以是数据文件。

● lpOperation——指向一个表示执行方式的字符串,字符串可以是下列取值:

■ “open”——文件被打开,这时lpFile指定的文件名可以是可执行文件、目录名或数据文件名。如果lpOperation参数为空,函数默认执行open操作。

■ “print”——文件被打印,这时lpFile指定的文件名必须是数据文件。如果指定的是可执行文件,那么函数当做“open”操作。

■ “explore”——浏览lpFile参数中指定的目录。

● lpParameters——当lpFile参数指定了一个可执行文件,本参数用来指定命令行参数。

如果lpFile参数指定的是数据文件,那么本参数必须是NULL。

● lpDirectory——执行或打开文件时使用的默认目录。

● dwCmdShow——如果函数执行了一个可执行文件,这个参数指定窗口的打开方式。

如果文件被成功执行,那么函数返回一个大于31的值。这里是几个使用ShellExecute函数的例子:

【完整的代码示例】

; ShellCall2.asm
; 命令行参数的获取和分析例子
;--------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff ShellCall2.asm
; Link /subsystem:windows ShellCall2.obj
.386
.model flat,stdcall 
option casemap:none 
 
;include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 
include 	c:/masm32/include/shell32.inc 
includelib 	c:/masm32/lib/shell32.lib 


;数据段
.const 
szFileName	byte 'Text', 0
szCmdline 	byte 'aaa bbb ccc', 0
szEmail 	byte 'mailto:luoyunbin@hotmail.com', 0
szWebPage 	byte 'https://www.baidu.com', 0
szHelp 		byte 'Win32asm.chm', 0

;代码段
.code 
;main函数
main proc 
	invoke ShellExecute, 0, 0, addr szFileName, addr szCmdline, 0, SW_SHOWNORMAL 
	invoke ShellExecute, 0, 0, addr szEmail, 0, 0, SW_SHOW 
	invoke ShellExecute, 0, 0, addr szWebPage, 0, 0, SW_SHOW 
	invoke ShellExecute, 0, 0, addr szHelp, 0, 0, SW_SHOW 
	invoke ExitProcess, 0
main endp 
end main 

例子(1)执行Test.exe文件,并将命令行参数“aaa bbb ccc”传递给它。例子(2)打开默认的邮件收发程序并显示一个“新建邮件”窗口,指定的邮件地址会被自动地填入到收件人一栏中。例子(3)打开浏览器并自动打开网站https://www.baidu.com。例子(4)会运行chm帮助文件阅读器hh.exe,并自动打开Win32asm.chm帮助文件。

13.2.2 方法二:创建进程

用ShellExecute和WinExec函数来执行文件是很方便的,调用这两个函数从某种意义上讲,相当于创建了新的进程,但是函数返回以后,这些新建的进程却脱离了我们的控制,我们无法知道它们在什么时候结束,也无法去强制结束它们。要对进程进行后续的控制,必须使用函数CreateProcess来创建进程。

当一个进程被创建的时候,系统进行以下的操作:

● 系统为进程创建一个内核对象,并将它的初始计数设置为1,与线程对象类似,进程对象只是一个比较小的数据结构,它包含进程的一些统计信息。进程对象可以通过进程句柄来引用。

● 系统为进程创建一个虚拟地址空间,并将可执行文件装载到这个地址空间中。系统同时处理可执行文件的导入表,将导入表中登记的所有dll文件装入。每个dll文件被装入的时候,DLL的入口函数被执行,如果入口函数返回初始化失败信息的话,进程的初始化失败。可执行文件本身和所有的dll文件都被看做是单独的模块,都被分配了一个实例句柄(实例句柄在数值上等于模块装入到地址空间中的线性地址)。

● 系统为进程建立一个主线程,主线程将从可执行文件的入口地址开始执行。

对于线程来说,Windows为系统中的每个线程分配一个线程句柄和线程ID以便区分它们,同样,对于进程来说,每个进程也对应一个进程句柄和一个进程ID。

当某个进程创建了一个新的进程的时候,被创建的进程称为“子进程”,创建它的进程称为“父进程”,子进程可以从父进程那里继承环境变量以及其他一些对象,在子进程中可以继续创建“孙进程”。

创建进程使用CreateProcess函数。下面是一个使用这个函数的例子程序,程序显示如图13.2所示的对话框,允许用户输入需要执行的文件名(或者通过“浏览”按钮选择文件名)和传递给文件的命令行参数,当文件开始执行时,程序将“浏览”按钮与文件名输入框等子窗口灰化,在子进程结束以后再恢复它们。程序也可以通过“终止”按钮强制结束子进程的运行(“执行”按钮在子进程开始执行后被改为“终止”按钮)。

图13.2 建立进程的例子程序

程序的源代码在所附光盘的Chapter13\Process目录中,其中的Process.rc文件定义了上面所示的对话框:

//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include                    <resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN                1000
#define DLG_MAIN                1000
#define IDC_FILE                1001
#define IDC_CMDLINE             1002
#define IDC_BROWSE              1003
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN     ICON               "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 111, 104, 201, 57
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "执行文件"
FONT 9, "宋体"
{
LTEXT "文件名", -1, 7, 8, 25, 8
EDITTEXT IDC_FILE, 35, 5, 160, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP
LTEXT "命令行", -1, 7, 25, 25, 8
EDITTEXT IDC_CMDLINE, 35, 22, 160, 12, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP
PUSHBUTTON "浏览(&B)", IDC_BROWSE, 115, 38, 40, 14
PUSHBUTTON "执行(&E)", IDOK, 155, 38, 40, 14, WS_DISABLED | WS_TABSTOP
}

汇编源文件Process.asm的内容如下:

; Process.asm    --------   创建另一个进程,并且等待它的结束。
;--------------------------------------------------------------------
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Process.asm
; rc Process.rc
; Link /subsystem:windows Process.obj Process.res
.386
.model flat,stdcall 
option casemap:none 
 
; include 文件定义
include 	c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 
include 	c:/masm32/include/comdlg32.inc 
includelib 	c:/masm32/lib/comdlg32.lib 

; equ 等值定义
ICO_MAIN 	equ 1000
DLG_MAIN 	equ 1000
IDC_FILE 	equ 1001
IDC_CMDLINE equ 1002
IDC_BROWSE  equ 1003
 
; 数据段
.data?
hInstance 	dword ?
hWinMain 	dword ?
szFileName 	byte MAX_PATH dup(?)
szCmdLine 	byte MAX_PATH dup(?)
stStartUp 	STARTUPINFO <?>
stProcInfo  PROCESS_INFORMATION <?>

.const 
szFileExt 	byte '可执行文件(*.exe;*.com)',0,'*.exe;*.com',0,0
szErrExec 	byte '无法执行文件!',0
szStart 	byte '执行(&E)',0
szStop 		byte '终止(&T)',0
szBlank 	byte ' ',0

; 代码段
.code
_ProcExec proc uses ebx esi edi _lParam 
	local @szBuffer[MAX_PATH*2]:byte 
 
	;设置按钮状态以及其它准备工作
	invoke GetDlgItem, hWinMain, IDC_FILE 
	invoke EnableWindow, eax, FALSE 
	invoke GetDlgItem, hWinMain, IDC_CMDLINE 
	invoke EnableWindow, eax, FALSE 
	invoke GetDlgItem, hWinMain, IDC_BROWSE 
	invoke EnableWindow, eax, FALSE 
	invoke SetDlgItemText, hWinMain, IDOK, addr szStop 
	invoke GetDlgItemText, hWinMain, IDC_FILE, addr szFileName, sizeof szFileName 
	invoke GetDlgItemText, hWinMain, IDC_CMDLINE, addr szCmdLine, sizeof szCmdLine 
	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 
	.if eax 
	;等待进程结束
		invoke WaitForSingleObject,stProcInfo.hProcess, INFINITE 
		invoke CloseHandle, stProcInfo.hProcess 
		invoke CloseHandle, stProcInfo.hThread 
	.else 
		invoke MessageBox, hWinMain, addr szErrExec, NULL, MB_OK or MB_ICONWARNING 
	.endif 
 
	;恢复按钮状态
	invoke RtlZeroMemory, addr stProcInfo, sizeof stProcInfo 
	invoke GetDlgItem, hWinMain, IDC_FILE 
	invoke EnableWindow, eax, TRUE 
	invoke GetDlgItem, hWinMain, IDC_CMDLINE 
	invoke EnableWindow, eax, TRUE 
	invoke GetDlgItem, hWinMain, IDC_BROWSE 
	invoke EnableWindow, eax, TRUE 
	invoke SetDlgItemText, hWinMain, IDOK, addr szStart 
	ret 
_ProcExec endp 

_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam 
		local @dwThreadID 
		local @stOF:OPENFILENAME 
		
		mov eax, wMsg 
		.if eax == WM_COMMAND 
			mov eax, wParam 
			.if ax == IDOK 
				.if stProcInfo.hProcess 
					invoke TerminateProcess, stProcInfo.hProcess, -1
				.else 
					invoke CreateThread, NULL, 0, offset _ProcExec, NULL, \
							NULL, addr @dwThreadID 
					invoke CloseHandle, eax 
				.endif 
			.elseif ax == IDC_BROWSE 
			;浏览打开的文件
			invoke RtlZeroMemory, addr @stOF, sizeof @stOF 
			mov @stOF.lStructSize, sizeof @stOF 
			push hWinMain 
			pop @stOF.hwndOwner 
			mov @stOF.lpstrFilter, offset szFileExt 
			mov @stOF.lpstrFile, offset szFileName 
			mov @stOF.nMaxFile, MAX_PATH
			mov @stOF.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST 
			invoke GetOpenFileName, addr @stOF 
			.if eax 
				invoke SetDlgItemText, hWnd, IDC_FILE, addr szFileName 
			.endif 
		.elseif ax == IDC_FILE 
			invoke GetWindowTextLength, lParam 
			mov ebx, eax 
			invoke GetDlgItem, hWnd, IDOK 
			invoke EnableWindow, eax, ebx 
		.endif 
	.elseif eax == WM_CLOSE 
		invoke EndDialog, hWnd, NULL 
	.elseif eax == WM_INITDIALOG 
		push hWnd 
		pop hWinMain 
		invoke SendDlgItemMessage, hWnd, IDC_FILE, EM_LIMITTEXT, MAX_PATH, 0
		invoke SendDlgItemMessage, hWnd, IDC_CMDLINE, EM_LIMITTEXT, MAX_PATH, 0
	.else 
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 
	
;main函数
main proc 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, eax, DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke ExitProcess, 0
main endp 
end main 

编译运行测试:

当按下“浏览”按钮(IDC_BROWSE)的时候,程序在WM_COMMAND消息中显示一个“打开文件”通用对话框并让用户选择可执行文件。

当按下IDOK按钮的时候,如果有进程在执行中(hProcess不为0),表示现在按下的是“终止”按钮,这时程序调用TerminateProcess函数强制结束进程;如果没有进程在执行,表示按下的是“执行”按钮,程序创建一个新线程_ProcExec子程序,在这个子程序中完成创建新进程和等待它结束的工作。

函数CreateProcess在子进程创建以后是马上返回的,但是程序需要等待子进程结束,为了在等待的过程中主线程还能够处理对话框消息,程序在这里使用一个新的线程来完成创建和等待子进程的工作。

在线程函数_ProcExec中,程序灰化“文件名”输入框、“命令行”输入框和“浏览”按钮,并将文件名和命令行参数获取到缓冲区szFileName和szCmdLine中,接下来调用CreateProcess函数创建进程。

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参数也就变得多此一举了,因为这时不用指定这个参数函数也可以正常执行。(不排除新版本的Windows会修正这个错误的可能,但是为了避免在当前版本的Windows中出现错误,建议读者用本书例子中的方法来创建进程)

2.结束进程

要结束一个进程的执行,可以使用ExitProcess函数。对于我们来说,这个函数是最熟悉的,因为在所有的例子程序中都用它来结束程序的执行:

invoke  ExitProcess,dwExitCode

与线程结束时有个退出码类似,进程结束时也可以指定一个退出码,dwExitCode就用来指定进程的退出码。

ExitProcess函数只能用来结束当前进程,不能用于结束其他进程,包括当前进程创建的子进程,因为它并没有参数可以用来输入进程句柄。如果需要结束其他进程的执行,可以使用TerminateProcess函数:

invoke  TerminateProcess,hProcess,dwExitCode

hProcess参数用来指定需要结束的进程的句柄,dwExitCode用来指定进程的退出码。

TerminateProcess函数不是一个推荐使用的函数,一般仅在很极端的情况下使用(如任务管理器用来结束停止响应的进程),因为它将目标进程无条件结束,被结束的进程根本没有机会进行扫尾工作,同时,目标进程使用的dll文件也不会收到结束通知,所以极有可能造成数据丢失。

当进程被结束的时候,系统将做下面的工作:

(1)进程创建或打开的所有对象句柄被关闭。

(2)进程中的所有线程被终止。

(3)进程及进程中所有线程的状态被改为置位状态,以便让WaitForSingleObject函数正确检测。

(4)进程对象中的退出码字段从STILL_ACTIVE被改为指定的退出码。

大家还记得在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,表示在子进程结束前函数不会返回。

当一个进程被结束的时候,并不影响它创建的子进程,进程对象也不会马上从内存中删除,因为可能其他进程还需要通过进程句柄检测进程状态,直到使用CloseHandle函数将进程句柄关闭以后,进程对象才真正被删除。所以,当不再需要进程句柄的时候,不要忘记关闭PROCESS_INFORMATION结构中返回的进程句柄和主线程句柄。

于2024年4月-2025年9月期间,研究团队在贵州习水国家级自然保护区制定39条样线,涵盖灌木林、常绿阔叶林、针叶林、常绿落叶阔叶混交林、针阔混交林等不同植被类型,每条样线分春夏秋冬4个季节采集样品,用真菌采集软件记录经纬度、海拔、采集地点、时间、生境等信息,使用佳能相机(R6 mark Ⅱ)对大型真菌进行拍照,并采集标本,标本存放于贵州省生物研究所大型真菌标本馆(HGAMF)。 通过形态学初步鉴定,结合分子生物学最终鉴定,参考已]报道的中国毒蘑菇名录开展毒蘑菇的认定。 调查到保护区内有毒真菌7目25科64种,导致中毒的主要类型有急性肾衰竭型、神经精神型和胃肠炎型。最终形成贵州习水国家级自然保护区大型有毒真菌图片数据集,它由以下2个部分组成。 (1)附件1包含78张原始照片(.JPG),照片名字包括了大型有毒真菌的拉丁名和中文名,若无中文名的直接用拉丁名。 (2)附件2是一个压缩文件,包含了2张工作表,其中一张表是大型有毒真菌39条样线的信息,另一张表是大型有毒真菌的中毒类型。 照片采用佳能相机R6 mark Ⅱ拍摄,物种鉴定通过多种文献核实,并经两以上专家鉴定确认。该数据集可为研究地及周边的普通人识别有毒大型真菌提供参考,通过及时的图片对比,能有效避免误采误食大型有毒真菌,同时为因误食大型真菌可能引发的身体损伤进行了总结,能为患者及时治疗提供参考。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值