编写一个进程

进程实例句柄

每一个运行的EXE或者DLL都有一个唯一的实例句柄,程序通常会将这个句柄作为wminmain的第一个参数hInstanceExe。

这个Handle在加载资源的时候使用,例如

HICON LoadIcon(
HINSTANCE hInstance,
PCTSTR pszIcon);

HhInstanceExe的实际值就是PE文件被装载在内存中的位置,这个位置由linker决定,windows默认加载在0x00400000,可以通过/BASE: address来修改链接位置。

GetModuleHandle返回程序的加载地址,如果传递的是NULL,返回当前程序加载地址。

如果是DLL有两种方法获得当前DLL的加载地址,一个是通过linker的预定义变量__ImageBase,另外一个是通过函数GetModuleHandleEx第一参数为GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,第二个参数为当前运行的函数地址,第三个参数就是获得地址的指针。 对应代码如下

extern "C" const IMAGE_DOS_HEADER __ImageBase;
void DumpModule() {
  // Get the base address of the running application.
  // Can be different from the running module if this code is in a DLL.
  HMODULE hModule = GetModuleHandle(NULL);
  _tprintf(TEXT("with GetModuleHandle(NULL) = 0x%x\r\n"), hModule);
  // Use the pseudo-variable __ImageBase to get
  // the address of the current module hModule/hInstance.
  _tprintf(TEXT("with __ImageBase = 0x%x\r\n"), (HINSTANCE)&__ImageBase);
  // Pass the address of the current method DumpModule
  // as parameter to GetModuleHandleEx to get the address
  // of the current module hModule/hInstance.
  hModule = NULL;
  GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCTSTR)DumpModule,
                    &hModule);
  _tprintf(TEXT("with GetModuleHandleEx = 0x%x\r\n"), hModule);
}

GetModuleHandle需要注意,它被不会返回没有被加载的EXE或者DLL地址。二是它会返回的是运行的EXE地址,如果使用GetModuleHandle(NULL)在DLL中,它返回的是EXE的加载地址。

环境变量

环境变量是一个内存块中包含一组字符串

=::=::\ ...
VarName1=VarValue1\0
VarName2=VarValue2\0

注意除了第一行之外,也可能有以=开头的行,这种不做环境变量使用

环境变量中空格,也被视为name或者value

系统环境变量存储在注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment

用户环境变量存储在HKEY_CURRENT_USER\Environment

修改环境变量后一般需要重启来刷新,但是一些进程如explorer taskmanager control panel可以通过WM_SETTINGCHANGEmessage来刷新。

SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM) TEXT("Environment"));

一般子进程的环境变量与父进程一样,但是父进程可以指定继承哪些环境变量。这里注意父子进程的环境变量是独立的

可以通过函数GetEnvironmentVariable来获得对应的环境变量的值

环境变量中有一些\%\%字符表示可以被替换,可以通过ExpandEnvironmentStrings来将它展开。

可以通过SetEnvironmentVariable来对环境变量进行添加,删除、修改

进程的错误模式

所有进程都有一些flag来关联当遇到错误,例如找不到指定文件,磁盘错误、对齐错误时应该怎么做。通过SetErrorMode来设置这些flag,UINT SetErrorMode(UINT fuErrorMode)

参数是下表中的一些组合

一盘来说子进程继承这些error flag,这也导致如果遇到一些问题,子进程可能直接失败然后退出。但是用户没有感受。

可以通过在CreateProcess设置CREATE_DEFAULT_ERROR_MODEFlag不让子进程继承父进程error mode。

进程所在的目录和驱动

当未指定完成路径的时候,通过当前驱动器和当前目录开始寻找。

进程中的一个线程可以修改当前目录或者驱动,这对进程中所有线程有效

通过GetCurrentDirectory获得当前目录,SetCurrentDirectory设置当前目录

在windows中MAX_PATH=260是默认的最长目录路径,所以在获得当前目录时,传递MAX_PATH是safe的

进程的当前目录

系统记录进程当前驱动器和目录,但是没有记录每个驱动器的当前目录。但是操作系统使用环境字符串来进行记录其他驱动的当前目录

=C:=C:\windows
=D:=D:\

创建子进程的时候不会默认继承父进程的各个驱动器的当前目录。想要子进程继承,父进程需要创建驱动器字母的环境变量,。

父进程可以获得指定驱动器的当前目录通过GetFullPathName

系统版本

通过GetVersinoEx获得系统版本相关信息

BOOL GetVersionEx(POSVERSIONINFOEX pVersionInformation);

typedef struct {
  DWORD dwOSVersionInfoSize;
  DWORD dwMajorVersion;
  DWORD dwMinorVersion;
  DWORD dwBuildNumber;
  DWORD dwPlatformId;
  TCHAR szCSDVersion[128];
  WORD wServicePackMajor;
  WORD wServicePackMinor;
  WORD wSuiteMask;
  BYTE wProductType;
  BYTE wReserved;
} OSVERSIONINFOEX, *POSVERSIONINFOEX;

可以通过VerifyVersionInfo来比较系统版本是否符合

BOOL VerifyVersionInfo(
    POSVERSIONINFOEX pVersionInformation,
    DWORD dwTypeMask,
    DWORDLONG dwlConditionMask)

使用这个函数必须初始化OSVERSIONINFOEX结构体,初始化它的dwOSVERSINOINFOSIZE成员为结构体的大小,之后初始化需要比较的成员。通过dwTypeMask指定初始化了哪一个版本编号。通过dwlConditionMask,描述如何进行比较

dwlConditionMask由复杂的bit位组成,可以通过VER_SET_CONDITION宏来设置。

VER_SET_CONDITION(
    DWORDLONG dwlConditionMask,
    ULONG dwTypeBitMask,
    ULONG dwConditionMask)

dwlConditionMask是需要设置的变量,dwTypeBitMask是你想比较的结构体中的哪一个,如果比较多个成员需要多次使用VER_SET_CONDITION,每次dwTypeBitMask指向一个成员。dwConditionMask指向如何比较。VER_EQUAL, VER_GREATER, VER_GREATER_EQUAL, VER_LESS, or VER_LESS_EQUAL。对于VER_NT_WORKSTATION成员也可以使用这个进行比较。但是对于VER_SUITENAME成员需要使用VER_AND VER_OR进行比较

设置完成之后可以使用VerifyVersionInfo进行比较,如果返回非0值符合要求,如果返回0版本不符合要求或者函数设置有错误。可以通过GetLastError,如果获得ERROR_OLD_WIN_VERSION,函数设置成功但是系统不符合要求。

下面这段程序判断是不是Windows Vista:

// Prepare the OSVERSIONINFOEX structure to indicate Windows Vista.
OSVERSIONINFOEX osver = {0};
osver.dwOSVersionInfoSize = sizeof(osver);
osver.dwMajorVersion = 6;
osver.dwMinorVersion = 0;
osver.dwPlatformId = VER_PLATFORM_WIN32_NT;
// Prepare the condition mask.
DWORDLONG dwlConditionMask = 0; // You MUST initialize this to 0.
VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_EQUAL);
VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, VER_EQUAL);
VER_SET_CONDITION(dwlConditionMask, VER_PLATFORMID, VER_EQUAL);
// Perform the version test.
if (VerifyVersionInfo(&osver,
                      VER_MAJORVERSION | VER_MINORVERSION | VER_PLATFORMID,
                      dwlConditionMask)) {
  // The host system is Windows Vista exactly.
} else {
  // The host system is NOT Windows Vista.
}

CreateProcess

BOOL CreateProcess(
    PCTSTR pszApplicationName,
    PTSTR pszCommandLine,
    PSECURITY_ATTRIBUTES psaProcess,
    PSECURITY_ATTRIBUTES psaThread,
    BOOL bInheritHandles,
    DWORD fdwCreate,
    PVOID pvEnvironment,
    PCTSTR pszCurDir,
    PSTARTUPINFO psiStartInfo,
    PPROCESS_INFORMATION ppiProcInfo)

系统创建一个进程内核对象 with usage count 1,进程内核对象不是进程本身,而是一个小型的数据结构存储了进程相关信息。然后创建虚拟内存空间加载EXE和需要的DLL

系统然后创建一个Thread kernel object with usage count 1,作为进程的初始线程。线程执行linker设置的entry point 最终call main 函数。如果系统成功创建进程,和初始线程,返回TRUE。

CreateProcess 在进程完全初始化之前就会返回True.这个时候系统lader没有加载所有的所需DLL,如果DLL不能被加载的话,子进程会退出同时父进程不会发现问题

pszApplicationName and pszCommandLine

([[Windows Via C/C++,Fifth Edition .pdf#page=100&selection=82,0,91,74|Windows Via C/C++,Fifth Edition , p.100]]) The pszApplicationName and pszCommandLine parameters specify the name of the executable file the new process will use and the command-line string that will be passed to the new proces

ApplicationName和pszCommandLine分别是新建进程使用的名字,和传递的参数

pszCommandLine

注意到命令行是PTSTR,这意味者不是一个const值。

([[Windows Via C/C++,Fifth Edition .pdf#page=100&selection=111,0,118,7|Windows Via C/C++,Fifth Edition , p.100]]) CreateProcess actually does modify the command-line string that you pass to it. But before CreateProcess returns

如果传递一个const的值,可能会导致Acces 错误

[!PDF|] [[Windows Via C/C++,Fifth Edition .pdf#page=100&selection=124,0,130,32|Windows Via C/C++,Fifth Edition , p.100]] STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; CreateProcess(NULL, TEXT(“NOTEPAD”), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

最好的方法就是,复制一个copy出来。

[!PDF|] [[Windows Via C/C++,Fifth Edition .pdf#page=100&selection=150,0,154,40|Windows Via C/C++,Fifth Edition , p.100]] STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; TCHAR szCommandLine[] = TEXT(“NOTEPAD”); CreateProcess(NULL, szCommandLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

但是如果传递的String是ANSI,就不会有问题。因为在WINDOWS内部,已经帮你处理为Unicode。

CreateProcess 认为 pszCommandLine的 第一个TOken是EXE可执行文件的名字,如果不带.exe后缀,会自动加上EXE。

搜索EXE顺序

  1. 当前运行程序的目录

  2. Current Dir

  3. Windows 系统目录 = GetSystemDirectory 返回值

  4. Windows Dir

  5. PATH环境变量

如果使用的是绝对路径,不会去搜索目录

当找到EXE路径后,Windows创建一个新进程,将EXE映射到内存空间中。然后系统CALL Linker 设置的entry point。 像之前说的,将pszCmd在exe 名字后的地址,作为 WinMain’s pszCmdLine parameter

上述这些都发生在pszApplicationName为NULL。如果传递了pszApplicationName,除非使用绝对路径,系统会在current directory中寻找,同时系统不会帮你加上.exe后缀。 如果在current directory找不到,直接failed。

[!PDF|] [[Windows Via C/C++,Fifth Edition .pdf#page=100&selection=150,0,154,40|Windows Via C/C++,Fifth Edition , p.101]] Even if you specify a filename in the pszApplicationName parameter, however, CreateProcess passes the contents of the pszCommandLine parameter to the new process as its command line

如果使用了applicationName,系统始终会通过applicationName来作为执行的EXE,但是它仍然会传递pszCommandLine作为参数Check这里可能就有一个点,如果EDR通过argv[0]来查看进程,可能就会有问题,或者修改PEB?

[[Windows Via C/C++,Fifth Edition .pdf#page=101&selection=155,50,159,9&color=yellow|Windows Via C/C++,Fifth Edition , p.101]]

psaProcess, psaThread, and bInheritHandles

在新检查创建的时候,有一个Process Kernel Object , 和 Thread Kernel Object 创建。你可以设置它们两个的安全描述符。 psaProcess和PsaTHread就是的。如果传递NULL,系统给它们默认的安全描述符。

另外也可以通过psa,来决定这两个Handle,是否会被之后的创建的子进程继承。 详见KernelObject[[012 KernelObjet#^b88c9a]]

bInheritHandles绝对,创建的子进程能否继承父进程的句柄。

下面的代码,ProcessA 创建了ProcessB,其中psaProcess设置了可继承,psaThread设置了不可继承。 然后创建了ProcessC,设置了bInheritHandles为True。 这个时候,ProcessC就可以继承进程B的Process 句柄,不能继承B的Thread句柄。

fdwCreate

fdwCreate指定进程如何被创建,可以使用OR来进行组合。

  • DEBUG_PROCESS 告诉系统父进程想要调试子进程,以及子进程派生的所有进程。当子进程发生Events时,通知父进程

  • DEBUG_ONLY_THIS_PROCESS 与DEBUG_PROCESS 类似,但是只在最近的子进程发生Events时通知,孙进程不会进行通知

  • CREATE_SUSPENDED 新进程被创建但是,主线程被不会执行。可以在这是修改子进程内存,或者添加JOB、修改主线程优先级。当完成修改后,使用ResumeThread 函数恢复

  • DETACHED_PROCESS 阻止子进程将输出发送的父进程的命令行窗口。如果指定这个FALG,新进程需要发送输出到新的控制台窗口。,通过 AllocConsole创建它自己的Console

  • CREATE_NEW_CONSOLE 告诉系统为进进程创建一个新窗口。如果 CREATE_NEW_CONSOLE 和 DETACHED_PROCESS 同时声明,会产生错误

  • CREATE_NO_WINDOW 告诉系统不要为新进程创建任何控制台窗口,可以使用这个FLAG,创建一个进程没有用户界面。

  • CREATE_NEW_PROCESS_GROUP 修改当用户按 Ctrl + C 或者 CTRL + Break 时,通知的进程组。通过这个FLAG,可以创建一个新的进程LIST,当用户Ctrl + C时,只通知它一个

  • CREATE_DEFAULT_ERROR_MODE 子进程不继承父进程的[[#进程的错误模式 | ErrorMode]]

  • CREATE_SEPARATE_WOW_VDM 当运行16bite的程序时,新建一个VDM(Virtual DOS Machine)来运行。默认情况下所有16bit程序在一个虚拟环境中运行。优势是如果程序Crash,其他的VDM不会受到影响,劣势就是吃内存

  • CREATE_SHARED_WOW_VDM 默认情况下所有16bit程序在一个VDM中运行。但是如果修改 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\WOW的key DefaultSeparate 为YES,然后使用这个FALG,程序就会在 system’s shared VDM(??) 中运行。

如果想要检查一个32bit程序是否在64bit下运行,call IsWow64Process ,将Process Hanlde 作为第一个参数,如果返回Ture,就是的。

  • CREATE_UNICODE_ENVIRONMENT 子进程 environment block 包含 Unicode 字符,默认情况下 environment block 包含 ANSI 字符

  • CREATE_FORCEDOS 强制系统运行MS_DOS程序在16bits OS/2 程序中 ??

  • CREATE_BREAKAWAY_FROM_JOB JOB中的一个进程,spawn一个新进程 不关联JOB

  • EXTENDED_STARTUPINFO_PRESENT STARTUP-INFOEX 结构体,通过psiStartInfo 参宿传递

fdwCreate同样运行你定义进程的优先级,一般这件事系统自己做。

优先级影响进程中的线程与其他进程中的线程如何安排。

pvEnvironment

指向新进程使用的环境字符串,大部分情况下传递NULL,继承父进程的环境。 PVOID GetEnvironmentStrings(); 返回调用进程的环境字符串,可以将它的返回值作为pvEnvironment参数。当pvEnvironment为NULL时,系统就是这么做的。 当不需要这块内存时,使用BOOL FreeEnvironmentStrings(PTSTR pszEnvironmentBlock);

pszCurDir

设置子进程的当前Drive和路径。如果为NULL,和父进程一样。

如果不为NULL,指向以0结尾的目录,目录必须包含Drive。

psiStartInfo

指向STARTUPINFO或者STARTUPINFOEX结构体

    typedef struct _STARTUPINFOA {
    DWORD   cb;
    LPSTR   lpReserved;
    LPSTR   lpDesktop;
    LPSTR   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;
} STARTUPINFOA, *LPSTARTUPINFOA;

typedef struct _STARTUPINFOEXA {
    STARTUPINFOA StartupInfo;
    LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;

在大部分情况下,使用默认的结构体来创建子进程。设置cb为当前结构体的size,然后其他的设置为0.

STARTUPINFO si = { sizeof(si) }; 
CreateProcess(..., &si, ...);

注意必须要设置为0 ,不然会将栈上的数据传给它,这就不能保证新进程一定创建成功了。

下表描述了所有的成员,有的成员只在创建窗口是有用,有的只在创建Console时有用。

成员 Windows 、Console、Both 作用
cb Both 记录StartUPInnfo的字节数,起到版本识别的作用。当Windows在将来扩展了StartUPINFO时,通过size来确认是哪个结构体。目前值只能为 sizeof(STARTUPINFO) 或者 sizeof(STARTUPINFOEX)
lpReserved Both 保留字段,必须从为NULL
lpDesktop Both 指定新进程需要关联的桌面名称,如果名称不存在。使用默认的桌面属性和这个名称创建一个新桌面。 大多数情况下为NULL新检查关联当前桌面
lpTitle Console 控制台的Title名称,如果为NULL,使用EXE文件名称
dwX dwY Both 以像素为单位执行程序窗口放置在屏幕上的位置。 只用当子进程创建第一个个重叠的窗口使用CW_USEDEFAULT 作为 CreateWindow的x参数是才会使用? 如果是控制台,相对于父进程左上角的位置
dwXSize dwYSize Both 进程的宽和高,只有当子进程创建它第一个重叠窗口使用 CW_USEDEFAULT 作为 CreateWindow 的 nWidth时才被使用 对于console指定宽和高
dwXCountChars dwYCountChars Console 对于子进程控制台窗口,通过字符指定宽和高
dwFillAttribute Console 指定文字和背景颜色
dwFlags Both 一系列的Flag,标识startupinfo那些成员会被使用
wShowWindow Window 指定进程的主窗口如何出现。在ShowWindows函数中将使用wShowWindow而不是nCmdShow. 在后续的调用中wShowWindow的值 只会在 SW_SHOW-DEFAULT 被传递给ShowWindow时使用。 注意:只有dwFlags 设置了 STARTF_USESHOWWINDOW ,wShowWindow 才会被使用
cbReserved2 Both 保留必须设置为0
lpReserved2 Both 保留必须设置为0 这两个参数在C中使用_dospawn来创建新进程时使用,详细看VC/crt/src\dospawn.c 和 ioinit.c
hStdInput hStdOutput hStdError Console 指定Console输入和输出的Hanle。 默认情况下 hStdInput 指向键盘 hStdOutput和hStdError 指向Console 当你需要重定向输入输出时使用