如何通过C#获取调用执行指定EXE的程序路径与进程ID
结论
可以获取调用方进程ID与文件路径,但不存在适配所有Windows版本、所有启动场景的零权限通用方案,不同场景下的实现可靠性差异较大。
常见实现方案与适用边界
1. 父进程查询法(常规场景首选,无特殊权限要求)
不管b.exe是通过ShellExecute系列API还是.NET的Process类启动a.exe,默认情况下b.exe就是a.exe的父进程,Windows内核会在新进程的基本信息结构中记录父进程PID,可通过ntdll导出的NtQueryInformationProcessAPI直接读取,拿到PID后再查询对应进程的可执行文件路径即可。
注意:这个API属于内核层长期稳定的未公开API,从Windows 2000到最新的Windows 11均保持兼容,性能远高于WMI查询。
可直接复用的实现代码:
using System; using System.Diagnostics; using System.Runtime.InteropServices; public static class CallerProcessResolver { [DllImport("ntdll.dll")] private static extern int NtQueryInformationProcess( IntPtr processHandle, int processInformationClass, IntPtr processInformation, uint processInformationLength, out uint returnLength); private const int ProcessBasicInfoClass = 0; [StructLayout(LayoutKind.Sequential)] private struct PROCESS_BASIC_INFORMATION { public IntPtr Reserved1; public IntPtr PebBaseAddress; public IntPtr Reserved2_0; public IntPtr Reserved2_1; public IntPtr CurrentProcessId; public IntPtr ParentProcessId; } /// <summary> /// 在程序启动入口调用,获取调用方进程信息 /// </summary> /// <returns>父进程PID、父进程可执行文件路径(拿不到时返回null)</returns> public static (int parentPid, string? parentExePath) GetCallerInfo() { using var currentProcess = Process.GetCurrentProcess(); var infoStruct = new PROCESS_BASIC_INFORMATION(); IntPtr structPtr = Marshal.AllocHGlobal(Marshal.SizeOf(infoStruct)); uint returnedBytes = 0; try { Marshal.StructureToPtr(infoStruct, structPtr, false); NtQueryInformationProcess( currentProcess.Handle, ProcessBasicInfoClass, structPtr, (uint)Marshal.SizeOf(infoStruct), out returnedBytes); infoStruct = Marshal.PtrToStructure<PROCESS_BASIC_INFORMATION>(structPtr); } finally { Marshal.FreeHGlobal(structPtr); } int callerPid = infoStruct.ParentProcessId.ToInt32(); string? callerPath = null; try { using var callerProcess = Process.GetProcessById(callerPid); callerPath = callerProcess.MainModule?.FileName; } catch { // 触发异常的常见原因:父进程已退出、权限不足无法访问父进程模块信息 } return (callerPid, callerPath); } }
这个方案的局限性:
- Windows不会对父进程句柄做持有引用,如果b.exe在a.exe启动后立刻退出,对应的PID可能被系统回收复用给其他新进程,此时拿到的信息不属于真实调用方
- 如果b.exe调用启动API时刻意通过
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS属性伪造父进程,拿到的是指定的虚假父进程信息 - 如果启动过程触发UAC提权,中间会经过系统提权组件中转,此时父进程会是系统进程而非真实的b.exe
- 如果a.exe权限远低于b.exe(比如a是普通用户权限运行,b是系统服务/管理员权限运行),会因权限不足无法读取b.exe的文件路径,只能拿到PID
2. 句柄枚举法(特殊场景兜底,需要管理员权限)
如果启动链存在UAC中转、父进程伪造这类特殊情况,可以在a.exe启动后立刻枚举系统内所有进程的打开句柄,查找持有当前a.exe进程句柄、且句柄具备进程创建相关权限的进程,即为真实调用方。
这个方案可靠性更高,但必须以管理员权限运行,且枚举句柄的性能开销大,还需要过滤系统进程的默认句柄,误判率高,仅适合对可靠性要求极高、且能保证运行权限的场景。
3. 参数传递法(100%可靠,无权限要求,优先推荐)
如果a.exe和b.exe都是你自己可控的程序,完全不需要依赖系统API查询:b.exe启动a.exe时,直接通过命令行参数把自身的PID、可执行文件路径传给a.exe即可,a.exe启动时直接解析命令行就能拿到准确信息,完全不存在前面提到的父进程退出、PID复用、UAC断链、权限不足的问题。
避坑提示
- 不推荐使用WMI查询
Win32_Process的ParentProcessId字段,性能比原生API差10~100倍,且同样存在父进程退出后信息不准的问题 - 如果a.exe编译为32位程序在64位系统上运行,查询64位父进程路径时会触发SysWOW64文件系统重定向,要么关闭“首选32位”编译选项编译为AnyCPU,要么临时调用
Wow64DisableWow64FsRedirectionAPI关闭重定向后再查询路径 - 调用方查询必须放在a.exe的启动入口点第一时间执行,程序运行越久,父进程退出、PID复用的概率越高,结果可靠性越低
内容的提问来源于stack exchange,提问作者SuperBerry
相关产品推荐
相关产品推荐

