Windows核心编程之核心总结(第四章 进程(三))(2018.6.21)
2021-04-03 00:26
标签:代码实现 任务管理器 new %s users 重要 决定 def etl 本章节将学习以后经常用到的CreateProcess函数,听网上的人说有些面试官喜欢问这个函数的大概功能和参数作用哦,可见这个函数是十分重要滴,那我们来详细了解和测试这个函数的功能吧,有些不足的以后有实际经验再来修改和补充。说实话,我现在也只是一名大学生,到了实际开发也许才会用到这本书的内容,但我现在是作为兴趣来学这本书的,因为这本书给我的feel就是自己掌控Windows系统,这感觉太棒了。不管以后用不用的到,我觉得对我的帮助都很大。好了,闲话说到这吧,现在本章节的学习目标如下: 在了解这个创建子进程的函数之前,我们回顾一下当我们运行一个应用程序后,生成一个进程所做的事:当我们双击一个应用程序,这个程序就会被载入内存变成一个进程,也叫主调进程;系统会创建一个进程内核对象,其初始使用计数为1,而可执行文件(和所有必要的DLL文件)的代码及数据加载进进程地址空间,我们都知道进程的产生必然也会同时产生一个主线程,所以系统还创建了一个主线程内核对象,其初始使用计数也为1;当开始执行可执行文件代码时前,主线程一开始就会执行C/C++运行库的启动函数,然后做些初始化全局变量、调用构造函数等初始化工作,然后就会调用应用程序里的入口函数(WinMain,wWinMain,main或wmain函数),当执行完可执行文件和DLL文件的代码,那么这个入口函数就会返回nMainRetVal,然后传给exit函数结束进程。 接下来,详细讲讲各参数的使用,参数名我采用书本的名称,MSDN里的函数参数名和书本函数参数名有所不同。 对于pszApplicationName和pszCommandLine参数值设置的不同有以下三种情况,我们一一列举: 当然,如果pszCommandLine参数包含的是一个完整路径而不是只有一个可执行文件名,那么就直接利用这个完整路径搜索这个可执行文件了,就没必要按上面列举的6条搜索路径搜索了。那么CreateProcess函数使用pszCommandLine指向的字符串,就作为子进程的命令行字符串,子进程内部的线程可以调用GetCommandLine函数获取这个由父进程调用CreateProcess函数所传入的CreateProcess函数参数的命令行字符串。 当我运行CreateProcess.exe可执行文件时,运行结果如下图所示: 当我运行CreateProcess.exe可执行文件时,运行结果如下图所示: 当我运行CreateProcess.exe可执行文件时,运行结果如下图所示: 运行结果如下: 注意每个标志名称前面的数字(①、②、③、④...)代表优先级从低到高。好了,大概知道有这些优先级,那么我先介绍一个GetPriorityClass函数: 下面对这个函数的使用做个测试: 运行结果如下: 运行结果如下: 分析上面代码执行过程:在父进程创建好自定义的环境块,用chNewEnv来保存起来,然后作为CreateProcess函数的pvEnvironment参数传入,默认情况下父进程的环境块传入后,子进程以ANSI形式保存起来,我们必须加上个CREATE_UNICODE_ENVIRONMENT标志告诉系统我们等会创建的子进程环境块在子进程用Unicode字符形式保存。子进程通过调用GetEnvironmentStrings函数获取了整个环境块并输出在控制台窗口上。这就是父进程将一个我们自己自定义的环境变量块传递给子进程的方式。 2.STARTUPINFOEX结构体: 大多数应用程序都希望生成的应用程序只是使用默认值,因此,必须先初始化结构体成员,再将cb字段设置为对应结构体的大小,例如以下标准使用代码: 这里只简单介绍一下,因为字段太多,不可能都明白,想要使用更多流弊的属性,再查也不迟。 好了,接下来书本就有一堆理解性的内容给我们看: Windows核心编程之核心总结(第四章 进程(三))(2018.6.21) 标签:代码实现 任务管理器 new %s users 重要 决定 def etl 原文地址:http://blog.51cto.com/12731497/2131501
1.CreateProcess函数
2.实现子进程继承父进程环境变量块的方法
3.实现父进程将一个环境变量块传递给子进程的方法CreateProcess函数
回归这个CreateProcess函数,其实跟主调进程的过程差不多:当主调进程(调用CreateProcess函数的当前进程也叫父进程)的一个线程调用CreateProcess函数就创建了一个新进程,系统将创建一个新进程内核对象,其初始使用计数为1,进程内核对象实际也只是一个分配在内核区的数据结构,它也叫PCB(进程控制块),用于管理和控制进程。系统还为这个新进程创建一个进程地址空间,并将可执行文件(和所有必要的DLL文件)的代码及数据加载进新进程地址空间。然后系统还为新进程的主线程创建一个线程内核对象(其使用计数为1),和新进程内核对象一样,也是数据结构,其实也就是操作系统领域所说的TCB(线程控制块),用于管理和控制这个线程。这个主线程一开始就会调用C/C++运行库的启动函数,最终会调用应用程序的入口函数(WinMain,wWinMain,main或wmain函数),当执行完可执行文件和DLL文件的代码,那么这个入口函数就会返回nMainRetVal,然后传给exit函数结束进程。
当了解了上面的流程后,我们先放上CreateProcess函数的函数签名(函数参数很多,大概先过一遍,再慢慢深入每一个参数),再逐一剥析该函数的参数:BOOL WINAPI CreateProcess(
__in_opt LPCTSTR lpApplicationName,
__inout_opt LPTSTR lpCommandLine,
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in BOOL bInheritHandles,
__in DWORD dwCreationFlags,
__in_opt LPVOID lpEnvironment,
__in_opt LPCTSTR lpCurrentDirectory,
__in LPSTARTUPINFO lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation
);
lpApplicationName:被执行的模块的名称。这个模块可以是一个windows应用程序。也可以是其他类型的模块(例如MS-DOS或者OS/2)。
lpCommandLine:被执行的命令行参数。这个字符串的最大长度可以达到32768个字符,包括null结尾符。如果lpApplicationName是NULL,那么lpCommandLine参数中的可执行文件的名字被限定在MAX_PATH个字符之内。
lpProcessAttributes:一个指向SECURITY_ATTRIBUTES结构的指针,这个结构中,最重要的数据结构是一个安全描述符,他决定了新产生的进程对象,是否能被其他子进程继承,这个进程对象,可以被那些用户访问。
lpThreadAttributes:一个指向SECURITY_ATTRIBUTES结构体的指针。如果lpThreadAttributes=NULL,那么新线程的句柄不能够被继承。
dwCreationFlags:这个标志控制了进程的创建和优先级,例如:哪个进程先获得CPU资源。
lpEnvironment:一个指向环境变量内存块的指针。如果这个参数是NULL,那么新进程使用父进程的环境变量。
lpCurrentDirectory:进程的当前目录。
lpStartupInfo:一个指向STARTUPINFO或者STARTUPINFOEX结构的指针。
lpProcessInformation:一个指向PROCESS_INFORMATION结构的指针。
(1)pszApplicationName和pszCommandLine参数
pszApplicationName和pszCommandLine参数分别指定新进程要使用的执行体文件的名称,以及要传给新进程的命令行字符串。
注意,对于pszCommandLine参数,CreateProcess函数期望你传入的是一个非“常量字符串”的地址。在内部,CreateProcess实际上会修改你传给它的命令行字符串。但在CreateProcess返回之前,它会将这个字符串还原为原来的形式。因为如果CreateProcess函数试图修改字符串时,会引起访问违规,因为在现在版本的编译器都将常量字符串放在常量存储区,属于右值不允许修改的,这就产生矛盾了。所以,建议在调用CreateProcess之前,把常量字符串复制进一个临时缓冲区(在栈区存储),这样在内部,CreateProcess修改你传给它的命令行字符串也不会发生访问违规了。就像下面的代码一样:TCHAR szCommandLine[] = TEXT("NOTEPAD");
CreateProcess(NULL, szCommandLine, NULL, NULL,
FALSE, 0, NULL, NULL, &si, &pi);
1.如果lpApplicationName为NULL,pszCommandLine不为NULL;那么当CreateProcess函数解析pszCommandLine字符串时,它会检查第一个标记,并假定此标记是我们想运行的可执行文件的名称,如果可执行文件的名称没有扩展名,就会默认是.exe扩展名。CreateProcess函数就会按照以下顺序搜索可执行文件:1. 进程可执行文件所在目录
2. 父进程的当前目录
3. GetSystemDirectory函数获取的系统目录。
4. 16位windows系统目录。没有函数可以获得这个系统目录,但这个目录确实会被搜索。这个系统目录是System。
5. windows目录。也就是GetWindowsDirectory函数获得的目录。
6. 在PATH环境变量中列出的目录。注意,这个函数并不搜索App Paths注册表键定义的路径。如果想搜索这个目录下的目录,使用ShellExecute函数。
2.如果lpApplicationName不为NULL,lpCommandLine为NULL;那么此时,函数使用lpApplicationName指向的字符串,作为命令行字符串(后面会有测试案例证明),而若lpApplicationName包含的字符串是想要运行的可执行文件的名称(没有包含绝对路径),在这种情况下,必须指定文件扩展名,系统不会自动假定文件名有一个.exe扩展名,CreateProcess函数就会在主调进程的当前目录搜索这个文件名的可执行文件,若没有则以调用失败告终;除非lpApplicationName指向的字符串包含的是文件的绝对路径,那么就可以直接找到可执行文件了。
3.如果lpApplicationName和lpCommandLine都不为NULL,那么lpApplicationName就是可执行文件的文件名,而lpCommandLine指向的就是命令行参数。新进程可以使用GetCommandLine函数,来获取完整的命令行。控制台进程使用argc和argv参数,来分析命令行。此时argv[0]代表可执行文件的名称,作为命令行的第一个参数。
现在对第一种情况(如果lpApplicationName为NULL,pszCommandLine不为NULL)进行简单测试://CreateProcess.exe可执行文件(作为主调进程),源文件代码如下:
#include
//ChildProcess.exe可执行文件(作为子进程),源文件代码如下:
#include
小总结:我们在运行结果中其实可以看出,当运行CreateProcess.exe可执行文件时,先执行该文件的代码,当执行完后再执行ChildProcess.exe可执行文件的代码,并不是在CreateProcess函数时就开始执行ChildProcess.exe可执行文件的代码。对于这一结论,书本P43原文有说到:传入TRUE时,操作系统会创建新的子进程,但不允许子进程立即执行它的代码。
现在对第二种情况(如果lpApplicationName不为NULL,lpCommandLine为NULL)进行简单测试://CreateProcess.exe可执行文件(作为主调进程),源文件代码如下:
#include
//ChildProcess.exe可执行文件(作为子进程),源文件代码如下:
#include
小总结:看,运行结果感觉是一样,其实代码有点不一样的,我是将CreateProcess函数的lpCommandLine置NULL,而lpApplicationName为一可执行文件的绝对路径,那么,CreateProcess函数使用lpApplicationName指向的字符串,作为命令行字符串。
现在对第三种情况(如果lpApplicationName和lpCommandLine都不为NULL)进行简单测试://CreateProcess.exe可执行文件(作为主调进程),源文件代码如下:
#include
//ChildProcess.exe可执行文件(作为子进程),源文件代码如下:
#include
小总结:如果lpApplicationName和lpCommandLine都不为NULL,那么lpApplicationName就是可执行文件的文件名,而lpCommandLine指向的就是命令行参数。
(2)psaProcess,psaThread和bInheritHandles参数
前面讲过,内核对象自身在创建时我们是可以将安全属性(SECURITY_ATTRIBUTES)关联到内核对象。而主调进程的线程调用CreateProcess就创建了一个新子进程,系统必须创建一个进程内核对象和一个线程内核对象,那么由于这两个对象也是内核对象,那么我们就可以在调用CreateProcess函数时手动为这两个内核对象关联安全属性(SECURITY_ATTRIBUTES)。利用CreateProcess函数的psaProcess和psaThread参数就可以实现关联过程。我们都知道SECURITY_ATTRIBUTES结构有三个字段,分别是结构大小、内核对象句柄是否可被子进程继承(bInheritHandle字段)、安全描述符。如果这两个参数为NULL,那么系统将为这两个内核对象指定默认的安全描述符(设置为默认该进程或线程内核对象句柄不可被子进程继承和设置为默认安全描述符),也可以自己创建并初始化两个SECURITY_ATTRIBUTES结构(可以自主指定安全描述符和自主设置该进程或线程内核对象句柄可否被子进程继承),并将这两个自己创建的安全属性(SECURITYATTRIBUTES)赋予进程内核对象和线程内核对象。其实到这里我就产生了一个疑问:既然子进程创建时我们可以手动添加安全属性,那主调进程的进程内核对象和线程内核对象的安全属性谁来指定?该怎么修改?子进程继承主调进程时会不会连主调进程的进程内核对象和线程内核对象一起继承过来?这个疑问,我知识面不广、涉及不深,所以也没法告诉你们,等有经验了,再来深讨这个问题吧,哈哈O(∩∩)O。
bInheritHandles参数是关系到子进程能否继承父进程所有可继承的内核对象句柄(为TRUE可继承,反之不可继承)。注意:SECURITYATTRIBUTES结构体有一个bInheritHandle字段,而CreateProcess函数有这个bInheritHandles参数,虽然都是布尔值代表能否继承,但应用范围不同。前一个是用于父进程创建的内核对象句柄可否被子进程继承,而后一个是用于子进程能否继承父进程所有可继承的内核对象句柄,两者还是有所不同的。
(3)fdwCreate参数
fdwCreate参数标志控制了进程的创建和优先级。标志太多,我就不一一列举了。链接在此,谁敢造次:https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863(v=vs.85).aspx
比较有意思的标志是CREATE_NEW_CONSOLE:新的进程将使用一个新的控制台,而不是继承父进程的控制台。这个标志不能与DETACHED_PROCESS标志一起使用。前面我们测试第一、二个参数时,主调进程和子进程各自的线程执行的输出代码都呈现在一个控制台上,是因为CreateProcess函数的参数设置为0,那现在我们来测试下这个标志,代码如下://CreateProcess.exe可执行文件(作为主调进程),源文件代码如下:
#include
//ChildProcess.exe可执行文件(作为子进程),源文件代码如下:
#include
fdwCreate参数还允许我们指定一个优先级类,当我们不在CreateProcess函数设置优先级,那么系统会为我们的新进程分配一个默认的优先级类(NORMAL_PRIORITY_CLASS)。优先级有以下几种:
Return code
Return value
Description
①IDLE_PRIORITY_CLASS
0x00000040
Process whose threads run only when the system is idle and are preempted by the threads of any process running in a higher priority class. An example is a screen saver. The idle priority class is inherited by child processes.
②BELOW_NORMAL_PRIORITY_CLASS
0x00004000
Process that has priority above IDLE_PRIORITY_CLASS but below NORMAL_PRIORITY_CLASS.
③NORMAL_PRIORITY_CLASS
0x00000020
Process with no special scheduling needs.
④ABOVE_NORMAL_PRIORITY_CLASS
0x00008000
Process that has priority above NORMAL_PRIORITY_CLASS but below HIGH_PRIORITY_CLASS.
⑤HIGH_PRIORITY_CLASS
0x00000080
Process that performs time-critical tasks that must be executed immediately for it to run correctly. The threads of a high-priority class process preempt the threads of normal or idle priority class processes. An example is the Task List, which must respond quickly when called by the user, regardless of the load on the operating system. Use extreme care when using the high-priority class, because a high-priority class CPU-bound application can use nearly all available cycles.
⑥REALTIME_PRIORITY_CLASS
0x00000100
Process that has the highest possible priority. The threads of a real-time priority class process preempt the threads of all other processes, including operating system processes performing important tasks. For example, a real-time process that executes for more than a very brief interval can cause disk caches not to flush or cause the mouse to be unresponsive.
1.GetPriorityClass函数:获取指定进程的优先级DWORD WINAPI GetPriorityClass(
_In_ HANDLE hProcess//进程句柄
);
#include
#include
经过多个标志的切换和输出,我们可以得出以下结论:父进程的优先级若为③、④、⑤、⑥,在调用CreateProcess函数时不指定优先级则子进程的优先级默认设置为③;父进程的优先级若为①、②,在调用CreateProcess函数时不指定优先级则子进程的优先级默认设置为父进程的①、②(类似继承);若在调用CreateProcess函数指定了子进程的优先级,那么子进程的优先级与父进程自身的优先级无关,指定啥那子进程的优先级就是啥。
(4)pvEnvironment参数
每个进程(包括主调进程、子进程等)都拥有一个环境块,这个环境块是在进程地址空间分配的一块内存。而pvEnvironment参数指向一块内存,其中包含新进程要使用的环境字符串。对于这个参数的使用有两种方式,要么传一个NULL,那么将导致子进程继承其父进程使用的一组环境字符串;要么传一个环境字符串(1.可以自己定义一个环境字符串再传入pvEnvironment参数。2.通过GetEnvironmentStrings函数获取父进程的环境字符串,再传入pvEnvironment参数,但要注意如果不再需要这块内存,那么你就要调用FreeEnvironmentStrings函数来释放它,其实当为pvEnvironment参数传入NULL,CreateProcess函数内部就是这样做的。)
实例1:在子进程创建过程中改变子进程的环境变量是一个进程改变另一个进程环境变量的唯一方式。一个进程绝不能直接改变另一个进程(非子进程)的环境变量。下面代码实现子进程继承父进程环境变量的方法。#include
#include
实例2:默认情况下,子进程继承父进程环境变量内存块的一份拷贝;下面代码通过调用CreateProcess函数实现父进程(CreateProcess.exe就是父进程的可执行文件)将一个我们自己自定义的环境变量块传递给子进程(ChildProcess.exe就是子进程的可执行文件,因此,该代码的运行结果就是子进程打印从父进程继承而来的环境变量)。
这个实例我是参考https://blog.csdn.net/asce1885/article/details/5706087 精选文章后作出的测试分析,有兴趣可以去看看。//CreateProcess.exe可执行文件(作为父进程),源文件代码如下:
#include
//ChildProcess.exe可执行文件(作为子进程),源文件代码如下:
#include
运行结果如下:
(5)pszCurDir参数
pszCurDir参数允许父进程设置子进程的当前驱动器和目录。如果参数为NULL,则子进程的工作目录就是生成新进程的应用程序的当前所在目录。如果参数不为NULL,则pszCurDir必须指向一个以0结尾的字符串,并且路径必须指定一个驱动器号(D、C、E盘)。
(6)psiStartInfo参数
一个指向STARTUPINFO或者STARTUPINFOEX结构的指针。如果要设置扩展属性,那么dwCreateFlags标志中,应该包含EXTENDED_STARTUPINFO_PRESENT标志。
1.STARTUPINFO结构体:typedef struct _STARTUPINFO {
DWORD cb; //startupinfo结构体的大小
LPTSTR lpReserved;
LPTSTR lpDesktop;//次进程归那个桌面。
LPTSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;//对于控制台程序来说,一行有几个字符
DWORD dwYCountChars;// 对于控制台程序来说,有多少行。
DWORD dwFillAttribute;//对控制台程序来说,背景色和字体颜色
DWORD dwFlags;
WORD wShowWindow;//表示窗口是否显式。
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
typedef struct _STARTUPINFOEX {
STARTUPINFO StartupInfo;
PPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
} STARTUPINFOEX, *LPSTARTUPINFOEX;
STARTUPINFO info;
ZeroMemory(&info,sizeof(info));//注意:如果没有把结构的内容清零,可能会造成新进程创建的失败
info.cb=sizeof(info);
(7)ppiProcInfo参数
一个指向PROCESS_INFORMATION结构的指针,CreateProcess函数在返回之前初始化这个结构的成员。
1.PROCESS_INFORMATION结构typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;//进程句柄
HANDLE hThread;//主线程句柄
DWORD dwProcessId;//进程ID
DWORD dwThreadId;//主线程ID
} PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
系统在创建一个新的进程的时侯,系统会建立一个进程内核对象和线程内核对象,内核对象都有一个使用计数,系统会为这个对象赋以一个初始的计数1,在CreateProcess()函数返回之前,这个函数会打开线程对象和进程对象,并将每个对象的与进程相关的句柄放入到结构体PROCESS_INFORMATION中的hProcess和hThread成员中,当CreateProcess在内部打开这些对象的时候,每个对象的使用计数就变为2了,如果我们在父进程当中不需要这两个句柄就可以先将其关闭,系统就会为子进程的进程内核对象和线程内核对象的使用计数减1,当子进程终止运行的时候,系统会再将使用计数减1,至此,子进程的内核对象的使用计数变为0,这两个对象就会被释放掉。注意:必须关闭子进程和它的主线程的句柄,以避免在应用程序运行时泄漏资源。当然,当进程终止运行时,系统会自动消除这些泄漏现象,但是,当进程不再需要访问子进程和它的线程时,如果编写得较好的软件,最好显式关闭这些句柄(通过调用CloseHandle函数来关闭)。不能关闭这些句柄是开发人员最常犯的错误之一。由于某些原因,许多开发人员认为,关闭进程或线程的句柄,会促使系统撤消该进程或线程。实际情况并非如此。关闭句柄只是告诉系统,你对进程或线程的统计数据不感兴趣。进程或线程将继续运行,直到它自己终止运行。关闭进程或线程句柄不等于关闭进程或线程。
之前我们都学过内核对象有公有部分(使用计数、安全描述符)和特有部分(ID就是其中之一),当进程内核对象创建后,系统赋予该对象一个独一无二的标识号,系统中的其他任何进程内核对象都不能使用这个相同的ID号。线程内核对象的情况也一样。当一个线程内核对象创建时,该对象被赋予一个独一无二的、系统范围的ID号。为什么独一无二?因为进程ID和线程ID共享相同的号码池。Windows任务管理器将进程ID0与“System Idle Process”(系统空闲进程)相关联。CreateProcess返回之前,它会将这些ID填充到PROCESS_INFORMATION结构的dwProcessId和dwThreadId成员中。对于获取当前进程ID前面章节已经讲了,这里不再赘述,而获取当前线程ID也差不多,通过GetCurrentThreadId获取,而GetThreadId函数通过指定线程句柄来获取对应的线程ID。
文章标题:Windows核心编程之核心总结(第四章 进程(三))(2018.6.21)
文章链接:http://soscw.com/index.php/essay/71606.html