0%

Windows核心编程 - 进程

最近在学习 Win32编程,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习Win32开发的童鞋们提供一份参考。

本系列博文均根据学习《Windows核心编程》一书总结而来;

运行环境:

  • 操作系统: Windows 10家庭版
  • 编译器:Visual Studio 2019

进程

操作系统如何创建进程内核对象,以方便管理每个进程。使用相关的内核对象来对进程进行操作,进程的不同属性;

进程通常被定义为一个正常运行的程序的实例,它由两部分组成:

  • 一个是操作系统用来管理进程的内核对象。内核对象也是用来存放关于进程的统计信息的地方
  • 另一个是地址空间,它包含所有可执行模块或DLL模块的代码和数据。它好包含动态分配的空间。

进程是不活泼的,若要使得进程完成某个操作,它必须拥有一个在它环境中的运行的线程,该线程负责执行包含在进程的地址空间中的代码。实际上,一个进程可以包含多个线程,所有这些线程都“同时”执行进程地址空间中的代码。为此,每个线程都有它自己的一组CPU寄存器和它自己的对战。每个进程至少拥有一个线程

如果没有线程来执行进程的地址空间中的代码,那么进程就会自动被系统撤销

如果我们要使得这些线程同时运行,操作系统就要为每个线程安排一定的CPU时间。它通过一种循环方式为线程提供时间片(量程),造成一种假象,仿佛所有的线程都是同时运行;

当创建一个进程的时候,系统会自动创建它的一个线程,成为主线程,该线程可以创建其他线程,而这些线程又可以创建更多线程

第一个Windows程序

如果我们使用VC++来创建应用程序时,我们需要将子系统修改成:windows,手动编译的链接开关是:SUBSYSTEM:WINDOWS;

Windows程序必须拥有一个在应用程序启动运行时调用的进入点函数,可以使用的进入口函数有四个:

1
2
3
4
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,int nCmdShow );
int WINAPI wWinMain(HINSTANCE hinstance, HINSTANCE hPrevlnst, PWSTR lpsCmdLine, int nCmdShow);
int __cdecl main(int argc,char *argv[],char*envp[]);
int __cdecl wmain(int argc,wchar_t *argv[],wchar_t *envp[]);

操作系统实际上并不会调用我们编写的入口点函数,它会调用C/C++运行期库;

ASCII和UNiCODE字符的入口点:

连接器是要查看代码中存在以上四个函数中的哪一个,确定可执行程序是哪个子系统,并且确定可执行程序中应当嵌入哪个C++启动器,所有的C/C++运行期启动函数的作用是相同的。差别在于它们究竟是ASCII还是Unicode;

启动函数功能如下:

  • 检索指向新进程的完整命令行的指针
  • 检索指向新进程的环境变量的指针
  • 对C/C++运行期的全局变量进行初始化
  • 对C运行期内存单位分配函数和其他底层输入输出例程使用的内存栈进行初始化
  • 为所有全局和静态C++类对象调用构造函数

进程实例句柄

加载到进程地址空间的每个可执行文件或DLL文件都被赋予了一个独一无二的实例句柄。

可执行文件的映像加载到的基地址是由链接器决定的。VC++链接使用的默认基地址是0x00400000。可以改变应用程序加载到的基地址,方式是使用VC链接程序中的/BASE:address链接程序开关;

我们可以使用GetModuleHandle函数返回可执行文件或DLL文件加载到进程的地址空间时所使用的句柄/基地址:

1
2
3
4
5
HMODULE
WINAPI
GetModuleHandleA(
_In_opt_ LPCSTR lpModuleName
);

进程命令行

当一个新进程创建时,它需要传递一个命令行。该命令行用不为空,至少用于创建新进程的可执行文件的名字是命令行第一个标记;它主要用于检索进程的命令行,跳过可执行文件的名字,并将指向命令行其余部分的指针传递给WinMain的pszCmdLine参数;

我们也可以获得一个指向进程的完整命令行的指针,使用GetCommandLine函数:

1
2
3
4
5
6
WINBASEAPI
LPSTR
WINAPI
GetCommandLineA(
VOID
);

进程的环境变量

每个进程都有一个与它相关的环境快。环境快是进程的地址空间中分配的一个内存块。每个环境快都包含一组字符串;

进程错误模式

与每个进程相关的是一组标志,用于告诉系统,进程对严重的错误应该如何反映:

1
2
3
4
5
6
WINBASEAPI
UINT
WINAPI
SetErrorMode(
_In_ UINT uMode
);

默认情况下,子进程继承父进程的错误模式标志

进程的当前驱动器和目录

调用以下函数,线程能够获得设置它的进程的当前目录和驱动器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DWORD
WINAPI
GetCurrentDirectoryW(
_In_ DWORD nBufferLength,
_Out_writes_to_opt_(nBufferLength,return + 1) LPWSTR lpBuffer
);


WINBASEAPI
BOOL
WINAPI
SetCurrentDirectoryW(
_In_ LPCWSTR lpPathName
);

进程当前目录

如果父进程创建了一个它想传递给子进程的环境快,子进程的环境快不会继承父进程的当前目录,相反,子进程的当前目录将默认为每个驱动器的根目录。如果想子进程继承父进程的当前目录,该父进程必须创建这些驱动器名的环境变量;

1
2
3
4
5
6
7
8
DWORD
WINAPI
GetFullPathNameA(
_In_ LPCSTR lpFileName,
_In_ DWORD nBufferLength,
_Out_writes_to_opt_(nBufferLength,return + 1) LPSTR lpBuffer,
_Outptr_opt_ LPSTR* lpFilePart
);

系统版本

1
2
3
4
5
BOOL
WINAPI
GetVersionExA(
_Inout_ LPOSVERSIONINFOA lpVersionInformation
);

GetVersionEx必须传入一个OSVERSIONINFOW结构;

CreateProcess函数

可以使用CreateProcess函数创建一个进程:

1
2
3
4
5
6
7
8
9
10
11
BOOL CreateProcess(
LPCTSTR lpApplicationName, // name of executable module
LPTSTR lpCommandLine, // command line string
LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
BOOL bInheritHandles, // handle inheritance option
DWORD dwCreationFlags, // creation flags
LPVOID lpEnvironment, // new environment block
LPCTSTR lpCurrentDirectory, // current directory name
LPSTARTUPINFO lpStartupInfo, // startup information
LPPROCESS_INFORMATION lpProcessInformation // process information);

当一个线程系统CreateProcess时,系统就会自动创建一个进程内核对象,其初始使用计数1,该进程内核对象不是进程本身,二十操作系统管理进程时使用的一个较小的数据结构。系统会为新进程创建一个虚拟地址空间,并将可执行文件或任何必要的DLL文件的代码和数据加载到该进程的地址空间中。

lpApplicationName和lpCommandLine

lpApplicationName和lpCommandLine参数分别用于设定新进程要使用的可执行文件的名字和传递给新进程的命令行参数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
int main()
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
TCHAR szApplicationName[] = TEXT("C:\\Windows\\SysWOW64\\notepad.exe");
if (!CreateProcess(
szApplicationName, //对象名称
NULL, //进程命令行
NULL, //不继承进程句柄
NULL, //不继承线程句柄
FALSE, //不继承句柄
0, //创建标志
NULL, //使用父进程环境变量
NULL, //使用父进程目录作为当前目录
&si, //进程的详细信息
&pi //
)
)
{
printf("CreateChildProcess Error = %d\n", GetLastError());
return FALSE;
}
return 0;
}

值得注意是的是,如果我们给szApplicationName参数传递为NULL,并且使用lpCommandLine也可以调用系统环境变量中的一些程序,比如可以直接创建CMD;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
int main()
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
TCHAR szApplicationName[] = TEXT("cmd.exe");
if (!CreateProcess(
NULL, //对象名称
szApplicationName, //进程命令行
NULL, //不继承进程句柄
NULL, //不继承线程句柄
FALSE, //不继承句柄
0, //创建标志
NULL, //使用父进程环境变量
NULL, //使用父进程目录作为当前目录
&si, //进程的详细信息
&pi //
)

)
{
printf("CreateChildProcess Error = %d\n", GetLastError());
return FALSE;
}
return 0;
}

psaProcess和psaThread和bInheritHandles

若要创建一个新进程,系统必须创建一个进程内核对象和一个线程内核对象(用于进程的主线程)。可以使用psaProcess和psaPthread参数分别设定进程对象和线程对象需要的安全属性;一般传递为NULL,但是可以使用SECURITY_ATTRIBUTES结构对它们进行初始化;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
int main()
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
SECURITY_ATTRIBUTES sPthread;
SECURITY_ATTRIBUTES sPProcess;
sPProcess.bInheritHandle = TRUE; //继承
sPProcess.nLength = sizeof(sPProcess);
sPthread.bInheritHandle = FALSE; //不继承
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
TCHAR szApplicationName[] = TEXT("cmd.exe");
if (!CreateProcess(
NULL, //对象名称
szApplicationName, //进程命令行
&sPProcess, //不继承进程句柄
&sPthread, //不继承线程句柄
FALSE, //不继承句柄
0, //创建标志
NULL, //使用父进程环境变量
NULL, //使用父进程目录作为当前目录
&si, //进程的详细信息
&pi //
)

)
{
printf("CreateChildProcess Error = %d\n", GetLastError());
return FALSE;
}
return 0;
}

fdwCreate

fdwCreate参数用于标识标志,以便于规定如何创建新进程。

pEnvironment

pEnvironment参数用于指向包含新进程要使用的环境字符串的内存块;大部分时间为NULL;

也可以使用GetEnvironmentStrings获取进程正在使用的环境字符串数据块的地址;

pszCurDir

pszCurDir参数允许父进程设置子进程的当前驱动器和目录,这个参数必须是以’\0’结尾的字符串;

psiStartInfo

psiStartInfo参数用于指向一个STARTUPINFO结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _STARTUPINFO { 
DWORD cb;
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;

cb参数表示结构体的大小,这个参数必须设置,大部分应用程序要求的仅仅是默认值;

ppiProcInfo

ppiProcInfo参数用于指向你必须指定的PROCESS_INFORMATION结构,CreateProcess在返回之前要对该结构的成员进行初始化;

1
2
3
4
5
6
typedef struct _PROCESS_INFORMATION { 
HANDLE hProcess; //进程句柄
HANDLE hThread; //线程句柄
DWORD dwProcessId; //进程ID
DWORD dwThreadId; //线程ID
} PROCESS_INFORMATION;

创建新进程可使系统简历一个进程内核对象和一个线程内核对象;关闭一个进程必须关闭它所有的句柄;

终止进程的运行

若要终止进程的运行,可以调用以下方法:

  • 主线程的进入点函数返回;
  • 进程中的一个线程调用ExitProcess函数;
  • 另一个进程中的线程调用TerminateProcess函数;
  • 进程中的所有线程自行终止运行;

主线程的进入点函数返回

始终都应该这样设计程序,即当主线程的进入点函数返回时,它的进程才能终止运行;

让主线程的进入点函数返回,可以确保以下列操作:

  • 该线程创建的任何C++对象都能将使用它们的狗在函数正确的撤销
  • 操作系统能正确的释放该线程的堆栈使用的内存
  • 系统将进程的推出代码设置为进入函数点的返回值
  • 系统将进程的内核对象的返回值-1

ExitProcess函数

当进程中一个线程调用ExitProcess函数时,进程便终止运行;

1
VOID ExitProcess(  UINT uExitCode   // exit code for all threads);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <windows.h>
#include <stdio.h>

class CSomeObj
{
public:
CSomeObj();
~CSomeObj();

private:

};

CSomeObj::CSomeObj()
{
printf("Constructor\r\n");
}

CSomeObj::~CSomeObj()
{
printf("Destructor\r\n");
}
CSomeObj g_GlocalObj;
int main(void)
{
CSomeObj LocalObj;
ExitProcess(0); //再次调用ExitProcess,构造函数将失去作用
printf("我不会被打印\n");
return 0;
}

TerMinateProcess

调用TerminateProcess函数也可以终止进程;

1
2
3
BOOL TerminateProcess(  
HANDLE hProcess, // handle to the process
UINT uExitCode // exit code for the process);

任意线程都可以调用TerminateProcess来终止另一个进程或自己的进程运行;

进程终止时情况

当进程终止时,下列操作将启动:

  • 进程中剩余的所有线程全部终止运行,(线程依附进程,进程是最小的单位执行体),每个进程都至少拥有一个线程,如果线程全部消亡,进程就没有存在的意思;
  • 进程指定的所有用户对象和GDI全部被释放,所有内核对象都关闭;
  • 进程的推出代码将从STILL_ACTIVE改为传递给ExitProcess或TerminateProcess的代码
  • 进程内核对象的状态变为收到通知的状态。系统中的其他线程挂起,直到进程终止运行;
  • 进程内核对象的使用计数递减1

当进程终止运行时,系统能够自动确定它的内核对象的使用计数。如果使用计数降为 0,那么没有其他进程拥有该对象打开的句柄,当进程被撤消时,对象也被撤消。

可以调用GetExitCodeProcess来获取当前已经撤销的进程的退出代码:

1
2
BOOL GetExitCodeProcess(  HANDLE hProcess,     // handle to the process
LPDWORD lpExitCode // termination status);

子进程

如果需要另一个代码块来执行操作,通过调用函数或子例程,当调用一个函数时,在函数返货之前,代码将无法继续操作。大多数情况下,需要实施这种单步任务;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <Windows.h>
#include <stdio.h>


int main()
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
DWORD dwExitCode;
TCHAR szCmdLineName[] = TEXT("NOTEPAD.EXE");
BOOL fSuccess = CreateProcess(NULL, szCmdLineName, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
if (fSuccess)
{
CloseHandle(pi.hProcess);
WaitForSingleObject(pi.hProcess,INFINITE);
GetExitCodeProcess(pi.hProcess, &dwExitCode);
CloseHandle(pi.hThread);
}
return 0;
}

枚举进程

枚举进程可以使用Procee32First和Process32Next函数;