Windows系统编程——创建新进程

原文地址:李浩的博客 lihaohello.top


本文概述:
(1)简要介绍Windows程序的启动过程;
(2)详细介绍在控制台程序、窗口程序中获取命令行、环境变量和当前目录的方法;
(3)以Windows API和C#为例,编写创建新进程、设置并获取命令行等数据的完整代码;
(4)介绍环境变量、命令行在VS程序调试、业务开发平台切换中的实际应用。


程序启动过程

1、关键概念:启动函数、入口函数。
2、入口函数有4种(main、wmain、WinMain、wWinMain),main、wmain针对**控制台程序,WinMain、wWinMain针对窗口程序,具体使用哪种,由程序的子系统类别**确定,子系统类别可以在Visual Studio中设置;如果没有设置,以上述第一个出现的函数作为入口函数。w开头的入口函数用于Unicode编码的项目。
3、链接器根据入口函数种类确定启动函数,启动函数先进行必要的环境设置(包括设置命令行、环境变量等),再调用入口函数。
总结:启动程序时,程序被加载到内存,启动函数执行,先进行环境设置,然后调用入口函数,同时将命令行等参数以实参方式传递给入口函数。入口函数对开发者可见,因此开发者可以获取这些参数。

进程“四大件”

基地址

加载资源时需要使用进程基地址,获取方式如下:

#include <windows.h>
#include <tchar.h>

extern "C" const IMAGE_DOS_HEADER __ImageBase;

void test() {
    // GetModuleHandle(NULL)获得当前可执行文件的基地址,而非DLL文件的基地址
	HMODULE hModule = GetModuleHandle(nullptr);
    
	HINSTANCE hInst = (HINSTANCE)&__ImageBase;
	HMODULE hModule2 = nullptr;
    
    // 获取当前函数test所在DLL的基地址
	GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCTSTR)test, &hModule2);
}

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPWSTR    lpCmdLine,
	_In_ int       nCmdShow)
{
    TCHAR moduleName[256];
	GetModuleFileName(GetModuleHandle(nullptr), moduleName, 256);
	test();
	return 0;
}

命令行

1、直接获取入口函数参数(控制台程序)

int _tmain(int argc, TCHAR** argv) {
	_tprintf(_T("argc=%d\n"), argc);
	for (size_t i = 0; i < argc; i++)
		_tprintf(_T("argv[%d]=%s\n"), i, argv[i]);
	system("pause");
	return 0;
}

image.png
2、直接获取入口函数参数(窗口程序)

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPTSTR    lpCmdLine,
	_In_ int       nCmdShow)
{
	MessageBox(NULL, lpCmdLine, _T("温馨提示"), MB_OK);
	return 0;
}

image.png
3、通过函数获取(控制台程序、窗口程序)
当前,开发复杂项目时通常使用既有框架(MFC、QT等),有些框架对入口函数进行了封装,开发者无法直接获取入口函数实参,因此只能通过这种方式获取命令行等参数。

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPTSTR    lpCmdLine,
	_In_ int       nCmdShow)
{
	int nNumArgv;
	PWSTR* ppArgv = CommandLineToArgvW(GetCommandLineW(), &nNumArgv);
	for (size_t i = 0; i < nNumArgv; i++) {
		TCHAR info[256];
		swprintf_s(info, _T("argv[%d]=%s"), i, ppArgv[i]);
		MessageBox(NULL, info, _T("温馨提示"), MB_OK);
	}
	return 0;
}

int _tmain(int argc, TCHAR** argv) {
	int nNumArgv;
	PWSTR* ppArgv = CommandLineToArgvW(GetCommandLineW(), &nNumArgv);
	for (size_t i = 0; i < nNumArgv; i++)
		wprintf(L"argv[%d]=%s\n", i, ppArgv[i]);

	system("pause");
	return 0;
}

image.png

环境变量

系统会为进程设置初始环境变量,其来源包括3个部分:内置环境变量、用户环境变量和系统环境变量。
子进程可以继承父进程的环境变量。
image.png
1、在控制台程序中通过实参获取环境变量

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) {
	int i = 0;
    // envp最后一个是nullptr
	while (envp[i++] != nullptr) {
		wprintf(L"%s\n", envp[i]);
	}

	system("pause");
	return 0;
}

2、通过GetEnvironmentStrings()获取环境变量块(适用控制台程序、窗口程序,但需要编码解析环境块)

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) {
	PTSTR pEnvBlock = GetEnvironmentStrings();
	system("pause");
	return 0;
}

3、实用函数

  • GetEnviromentVariable()、ExpandEnviromentStrings():根据环境变量名查询环境变量值。
  • SetEnviromentVariable():设置环境变量值。

当前目录

系统内部会跟踪每个进程的当前驱动器和当前目录,如果不提供文件的完整路径名,进程将在当前驱动器的当前目录查找文件。
如果采用D:ReadMe.txt的方式指定文件路径,默认查找D盘的当前路径,在该路径下找文件;如果没有当前目录,则在D盘根目录下找文件。

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) {
	DWORD num = GetCurrentDirectory(0, nullptr);
	PTSTR info = new TCHAR[num];
	GetCurrentDirectory(num, info);
	_tprintf(_T("%s"), info);
	delete[] info;

	system("pause");
	return 0;
}

创建新进程的方式

Windows API

在Windows操作系统上,Windows API是创建进程的最底层方式;C#是上层封装。
创建新进程的函数是CreateProcess,其原型如下:

BOOL CreateProcessW(
    // 应用程序路径,一般不适用该参数,设置为NULL
    [in, optional]      LPCWSTR               lpApplicationName,
    // 进程命令行:空格分隔,第一个为应用程序路径
    [in, out, optional] LPWSTR                lpCommandLine,

    // 指定进程权限和线程权限,以及本进程是否继承父进程的句柄
    [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
    [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
    [in]                BOOL                  bInheritHandles,

    // 指定进程创建标识符
    [in]                DWORD                 dwCreationFlags,

    // 指定本进程的环境变量,为NULL则继承父进程的环境
    [in, optional]      LPVOID                lpEnvironment,

    // 指定本进程的当前目录
    [in, optional]      LPCWSTR               lpCurrentDirectory,

    // 进程创建的其他信息
    [in]                LPSTARTUPINFOW        lpStartupInfo,

    // 返回本进程及其主线程的句柄和ID
    [out]               LPPROCESS_INFORMATION lpProcessInformation
);

本文重点关注程序名、命令行和环境变量这三个参数,封装创建进程并设置这三个参数的函数,如下:

#include <Windows.h>
#include <stdio.h>
#include <tchar.h>

/// @brief 封装创建进程的函数
/// @param lpApplicationName 程序名 
/// @param numCmds 命令行数量(不包含程序名)
/// @param cmds 命令行字符串数组
/// @param numEnvs 环境变量数量
/// @param envNames 环境变量名字符串数组
/// @param envValues 环境变量值字符串数组
/// @return 成功返回TRUE,失败返回FALSE
BOOL CreateProcessWithCmdAndEnviroment(LPCWSTR lpApplicationName,
	DWORD numCmds, WCHAR* cmds[],
	DWORD numEnvs, WCHAR* envNames[], WCHAR* envValues[]) {
	// 计算总的命令行字符串长度
	int num = 0;
	num += (wcslen(lpApplicationName) + 3);
	for (size_t i = 0; i < numCmds; i++)
		num += (wcslen(cmds[i]) + 1);
	num++;

	// 组装命令行字符串
	WCHAR* cmd = new WCHAR[num];
	wcscpy_s(cmd, num, L"\"");	// 用引号包裹程序路径,应对路径中包含空格的情况
	wcscat_s(cmd, num, lpApplicationName);
	wcscat_s(cmd, num, L"\"");
	for (size_t i = 0; i < numCmds; i++) {
		wcscat_s(cmd, num, L" ");
		wcscat_s(cmd, num, cmds[i]);
	}

	for (size_t i = 0; i < numEnvs; i++)
		SetEnvironmentVariable(envNames[i], envValues[i]);

	STARTUPINFO startupInfo = { sizeof(startupInfo) };
	PROCESS_INFORMATION processInfo;
	BOOL flag = CreateProcess(nullptr, cmd, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startupInfo, &processInfo);
	delete[] cmd;

	return flag;
}

int _tmain() {
	// 设置命令行参数
	WCHAR cmd1[] = L"hello";
	WCHAR cmd2[] = L"world";
	WCHAR* cmds[] = { cmd1,cmd2 };

	// 设置环境变量参数
	WCHAR envName1[] = L"env1";
	WCHAR envName2[] = L"env2";
	WCHAR envValue1[] = L"envValue1";
	WCHAR envValue2[] = L"envValue2";
	WCHAR* envNames[] = { envName1,envName2 };
	WCHAR* envValues[] = { envValue1,envValue2 };

	CreateProcessWithCmdAndEnviroment(L"E:\\Desktop\\WindowsViaC\\Debug\\LHTest.exe", 2, cmds, 2, envNames, envValues);
	return 0;
}

为验证该函数的正确性,编写一个测试程序,用于打印命令行参数和环境变量,如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <tchar.h>

/// @brief 打印指定名称的环境变量
/// @param name 环境变量名
void PrintEnviromentVariable(PCTSTR name) {
	PTSTR value = NULL;
	DWORD res = GetEnvironmentVariable(name, value, 0);
	if (res != 0) {
		DWORD size = res * sizeof(TCHAR);
		value = (PTSTR)malloc(size);
		GetEnvironmentVariable(name, value, size);
		_tprintf(_T("%s=%s\n"), name, value);
		free(value);
	}
	else
		_tprintf(_T("'%s'=<unknown value>\n"), name);
}

int _tmain() {
	// 打印环境变量名
	wprintf(L"环境变量:\n");
	PrintEnviromentVariable(_T("env1"));
	PrintEnviromentVariable(_T("env2"));

	// 打印命令行
	wprintf(L"\n命令行:\n");
	int nNumArgv;
	PWSTR* ppArgv = CommandLineToArgvW(GetCommandLineW(), &nNumArgv);
	for (size_t i = 0; i < nNumArgv; i++)
		wprintf(L"argv[%d]=%s\n", i, ppArgv[i]);

	system("pause");
	return 0;
}

image.png

C#

由于Windows API使用起来较为复杂,业务开发时通常使用更高级的编程语言或程序库,例如C#。
使用C#创建新进程的核心是ProcessStartInfo类,其主要属性如下图所示:
image.png
创建新进程的程序示例如下:

using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System;
using System.Data;
using System.Diagnostics;
using System.IO;

namespace Test {
    class Program {
        static void Main(string[] args) {
            ProcessStartInfo process = new ProcessStartInfo();
            process.UseShellExecute = false;
            process.FileName = @"LHTest.exe";
            process.Arguments = string.Format("{0} {1}", "C#", "Windows");
            process.Environment.Add("env1", "This is env1");
            process.Environment.Add("env2", "This is env2");
            Process.Start(process);

            Console.ReadLine();
        }
    }
}

image.png

业务开发场景

VS程序调试小技巧

以QT为例,在Visual Stusio中开发QT桌面软件时,如果没有指定QT依赖库路径,调试时会报错。
大多数开发者的做法是:将QT依赖库的路径直接添加到PATH系统变量(或PATH用户变量)中。
以上做法可行,但存在若干不足:(1)系统对PATH环境变量的长度有限制,随意增加PATH变量长度存在风险;(2)无法应对多版本QT的开发调试。
最佳做法:在Visual Stusio中针对单个项目设置环境变量,如下图所示:
image.png

平台切换最佳方案

在某些业务场景下,需要在程序1中启动程序2,并且希望在程序2启动后判断自己是否由程序1启动(众所周知,程序2也可以由用户双击启动),然后根据启动方式做不同的操作。
为此,我们可以借助命令行参数进行判断:
(1)程序1启动程序2之前,先生成一个不会重复的标识字符串(例如GUID),将该字符串写到本地文件(约定文件位置);
(2)程序1启动程序2时,将该字符串以命令行参数的方式传递给新进程;
(3)新进程启动后,获取命令行参数,并读取本地文件里的标识字符串,将两者进行对比,相同则说明该进程是由程序1启动的,不同则不是。


原文地址:李浩的博客 lihaohello.top

  • 14
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值