[编辑:4/2021 - 新答案...]
由于 Visual Studio 调试器最近发生了变化,我原来的答案在调试时停止正常工作。为了解决这个问题,我提供了一种完全不同的方法。原始答案的文本包含在底部。
1. Just the code, please...
要确定 .NET 应用程序是否在 GUI 模式下运行:
[DllImport("kernel32.dll")] static extern IntPtr GetModuleHandleW(IntPtr _);
public static bool IsGui
{
get
{
var p = GetModuleHandleW(default);
return Marshal.ReadInt16(p, Marshal.ReadInt32(p, 0x3C) + 0x5C) == 2;
}
}
这会检查Subsystem
值在PE标头 https://en.wikipedia.org/wiki/Portable_Executable。对于控制台应用程序,该值将为3
代替2
.
2. Discussion
如a中所述相关问题 https://stackoverflow.com/questions/30890104/determine-whether-assembly-is-a-gui-application,最可靠的指标GUI vs. console是个 ”Subsystem
“ 字段中PE标头 https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#windows-subsystem可执行映像的。下面的C#enum
列出允许的(记录的)值:
public enum Subsystem : ushort
{
Unknown /**/ = 0x0000,
Native /**/ = 0x0001,
WindowsGui /**/ = 0x0002,
WindowsCui /**/ = 0x0003,
OS2Cui /**/ = 0x0005,
PosixCui /**/ = 0x0007,
NativeWindows /**/ = 0x0008,
WindowsCEGui /**/ = 0x0009,
EfiApplication /**/ = 0x000A,
EfiBootServiceDriver /**/ = 0x000B,
EfiRuntimeDriver /**/ = 0x000C,
EfiRom /**/ = 0x000D,
Xbox /**/ = 0x000E,
WindowsBootApplication /**/ = 0x0010,
};
尽管该代码(在另一个答案中)很简单,但我们这里的情况可以大大简化。由于我们只对自己正在运行的进程(必须加载)特别感兴趣,因此您不必打开任何文件或从磁盘读取来获取子系统价值。我们的可执行映像保证已经映射到内存中。通过调用以下函数可以轻松检索任何加载的文件图像的基地址GetModuleHandleW https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew功能:
[DllImport("kernel32.dll")]
static extern IntPtr GetModuleHandleW(IntPtr lpModuleName);
尽管我们可能会为此函数提供文件名,但事情变得更容易,而且我们不必这样做。通过null
,或者在这种情况下,default(IntPtr.Zero)
(这与IntPtr.Zero
),返回当前进程的虚拟内存映像的基地址。这消除了必须获取入口组件及其的额外步骤(前面提到过)Location
属性等。闲话少说,这里是新的简化代码:
static Subsystem GetSubsystem()
{
var p = GetModuleHandleW(default); // VM base address of mapped PE image
p += Marshal.ReadInt32(p, 0x3C); // RVA of COFF/PE within DOS header
return (Subsystem)Marshal.ReadInt16(p + 0x5C); // PE offset to 'Subsystem' word
}
public static bool IsGui => GetSubsystem() == Subsystem.WindowsGui;
public static bool IsConsole => GetSubsystem() == Subsystem.WindowsCui;
【新答案正式结束】
3. Bonus Discussion
出于 .NET 的目的,Subsystem
也许是最——or only— 中的有用信息PE标头。但根据您对细节的容忍度,可能还有其他宝贵的花絮,并且可以轻松使用刚刚描述的技术来检索其他有趣的数据。
显然,通过改变最终的场偏移(0x5C
)之前使用过,您可以访问 COFF 或 PE 标头中的其他字段。下一个片段说明了这一点Subsystem
(如上所述)加上三个附加字段及其各自的偏移量。
注意:为了减少混乱,enum
以下使用的声明可以找到here https://pastebin.com/pZL3Y7vM
var p = GetModuleHandleW(default); // PE image VM mapped base address
p += Marshal.ReadInt32(p, 0x3C); // RVA of COFF/PE within DOS header
var subsys = (Subsystem)Marshal.ReadInt16(p + 0x005C); // (same as before)
var machine = (ImageFileMachine)Marshal.ReadInt16(p + 0x0004); // new
var imgType = (ImageFileCharacteristics)Marshal.ReadInt16(p + 0x0016); // new
var dllFlags = (DllCharacteristics)Marshal.ReadInt16(p + 0x005E); // new
// ... etc.
为了改进访问非托管内存中的多个字段时的情况,必须定义一个覆盖struct
。这允许使用 C# 进行直接、自然的托管访问。对于运行示例,我将相邻的 COFF 和 PE 标头合并到以下 C# 中struct
定义,并且只包含我们认为有趣的四个字段:
[StructLayout(LayoutKind.Explicit)]
struct COFF_PE
{
[FieldOffset(0x04)] public ImageFileMachine MachineType;
[FieldOffset(0x16)] public ImageFileCharacteristics Characteristics;
[FieldOffset(0x5C)] public Subsystem Subsystem;
[FieldOffset(0x5E)] public DllCharacteristics DllCharacteristics;
};
注意:可以找到此结构的更完整版本,没有省略的字段here https://pastebin.com/EU1QJJ7i
任何互操作struct
例如,必须在运行时正确设置,并且有很多选项可以做到这一点。理想情况下,通常最好强加struct
覆盖”in-situ“直接在非托管内存上,这样就不需要进行内存复制。但是,为了避免进一步延长此处的讨论,我将展示一种涉及复制的更简单的方法。
var p = GetModuleHandleW(default);
var _pe = Marshal.PtrToStructure<COFF_PE>(p + Marshal.ReadInt32(p, 0x3C));
Trace.WriteLine($@"
MachineType: {_pe.MachineType}
Characteristics: {_pe.Characteristics}
Subsystem: {_pe.Subsystem}
DllCharacteristics: {_pe.DllCharacteristics}");
4. Output of the demo code
这是当console程序正在运行...
MachineType: Amd64
Characteristics: ExecutableImage, LargeAddressAware
Subsystem: WindowsCui (3)
DllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware
...相比GUI(WPF)应用程序:
MachineType: Amd64
Characteristics: ExecutableImage, LargeAddressAware
Subsystem: WindowsGui (2)
DllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware
[旧:2012 年的原始答案...]
要确定 .NET 应用程序是否在 GUI 模式下运行:
bool is_console_app = Console.OpenStandardInput(1) != Stream.Null;