本文概述:
(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;
}
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;
}
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;
}
环境变量
系统会为进程设置初始环境变量,其来源包括3个部分:内置环境变量、用户环境变量和系统环境变量。
子进程可以继承父进程的环境变量。
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;
}
C#
由于Windows API使用起来较为复杂,业务开发时通常使用更高级的编程语言或程序库,例如C#。
使用C#创建新进程的核心是ProcessStartInfo类,其主要属性如下图所示:
创建新进程的程序示例如下:
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();
}
}
}
业务开发场景
VS程序调试小技巧
以QT为例,在Visual Stusio中开发QT桌面软件时,如果没有指定QT依赖库路径,调试时会报错。
大多数开发者的做法是:将QT依赖库的路径直接添加到PATH系统变量(或PATH用户变量)中。
以上做法可行,但存在若干不足:(1)系统对PATH环境变量的长度有限制,随意增加PATH变量长度存在风险;(2)无法应对多版本QT的开发调试。
最佳做法:在Visual Stusio中针对单个项目设置环境变量,如下图所示:
平台切换最佳方案
在某些业务场景下,需要在程序1中启动程序2,并且希望在程序2启动后判断自己是否由程序1启动(众所周知,程序2也可以由用户双击启动),然后根据启动方式做不同的操作。
为此,我们可以借助命令行参数进行判断:
(1)程序1启动程序2之前,先生成一个不会重复的标识字符串(例如GUID),将该字符串写到本地文件(约定文件位置);
(2)程序1启动程序2时,将该字符串以命令行参数的方式传递给新进程;
(3)新进程启动后,获取命令行参数,并读取本地文件里的标识字符串,将两者进行对比,相同则说明该进程是由程序1启动的,不同则不是。