potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

C# .NET Core 3.1 NtQuerySystemInformation関数でSystemProcessInformationを取得するコード

2021/3/10:この記事は別のブログで投稿した記事を移動したものです。

C# (.NET Core 3.1)でNtQuerySystemInformation関数を用いてSystemProcessInformationの情報を取得するコードの覚書です。動作確認環境はMicrosoft Visual Studio Community 2019 Version 16.5.2です。

途中で以下の操作も行っています。

  • DllImportによる遅延バインディング
  • MarshalAs(UnmanagedType.ByValArray, SizeConst = ...)
  • Marshal.PtrToStructureによる固定長配列を含む構造体の扱い
  • Marshal.PtrToStringUniによるポインタから文字列の作成
  • System.Diagnostics.DebuggerDisplayによるデバッガーへの情報表示
  • GCHandleMarshal.PtrToStructureによるCスタイル可変長配列の読み込み

Program.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    class Program
    {
        private static class NativeMethods
        {
            [DllImport("ntdll.dll", SetLastError = false, ExactSpelling = true)]
            public static extern int NtQuerySystemInformation(
                int SystemInformationClass,
                byte[] SystemInformation,
                uint SystemInformationLength,
                out uint ReturnLength);
        }

        private enum SYSTEM_INFORMATION_CLASS : int
        {
            // SystemBasicInformation = 0,
            // SystemPerformanceInformation = 2,
            // SystemTimeOfDayInformation = 3,
            SystemProcessInformation = 5,
            // SystemProcessorPerformanceInformation = 8,
            // SystemInterruptInformation = 23,
            // SystemExceptionInformation = 33,
            // SystemRegistryQuotaInformation = 37,
            // SystemLookasideInformation = 45,
            // SystemCodeIntegrityInformation = 103,
            // SystemPolicyInformation = 134,
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct UNICODE_STRING
        {
            public ushort Length;
            public ushort MaximumLength;
            public IntPtr Buffer;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct SYSTEM_PROCESS_INFORMATION
        {
            public uint NextEntryOffset;
            public uint NumberOfThreads;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)]
            public byte[] Reserved1;
            public UNICODE_STRING ImageName;
            public int BasePriority;
            public IntPtr UniqueProcessId;
            public IntPtr Reserved2;
            public uint HandleCount;
            public uint SessionId;
            public IntPtr Reserved3;
            public UIntPtr PeakVirtualSize;
            public UIntPtr VirtualSize;
            public uint Reserved4;
            public UIntPtr PeakWorkingSetSize;
            public UIntPtr WorkingSetSize;
            public IntPtr Reserved5;
            public UIntPtr QuotaPagedPoolUsage;
            public IntPtr Reserved6;
            public UIntPtr QuotaNonPagedPoolUsage;
            public UIntPtr PagefileUsage;
            public UIntPtr PeakPagefileUsage;
            public UIntPtr PrivatePageCount;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
            public long[] Reserved7;
        }

        /// <summary>
        /// SYSTEM_PROCESS_INFORMATION構造体のデータを保持するクラスです。
        /// </summary>
        [DebuggerDisplay("{ImageName} ({UniqueProcessId})")]
        private class SystemProcessInformation
        {
            public readonly uint NumberOfThreads;
            public readonly string ImageName;
            public readonly int BasePriority;
            public readonly IntPtr UniqueProcessId;
            public readonly uint HandleCount;
            public readonly uint SessionId;
            public readonly UIntPtr PeakVirtualSize;
            public readonly UIntPtr VirtualSize;
            public readonly UIntPtr PeakWorkingSetSize;
            public readonly UIntPtr WorkingSetSize;
            public readonly UIntPtr QuotaPagedPoolUsage;
            public readonly UIntPtr QuotaNonPagedPoolUsage;
            public readonly UIntPtr PagefileUsage;
            public readonly UIntPtr PeakPagefileUsage;
            public readonly UIntPtr PrivatePageCount;

            public SystemProcessInformation()
            {
            }

            public SystemProcessInformation(ref SYSTEM_PROCESS_INFORMATION info)
            {
                NumberOfThreads = info.NumberOfThreads;
                ImageName = Marshal.PtrToStringUni(info.ImageName.Buffer);
                BasePriority = info.BasePriority;
                UniqueProcessId = info.UniqueProcessId;
                HandleCount = info.HandleCount;
                SessionId = info.SessionId;
                PeakVirtualSize = info.PeakVirtualSize;
                VirtualSize = info.VirtualSize;
                PeakWorkingSetSize = info.PeakWorkingSetSize;
                WorkingSetSize = info.WorkingSetSize;
                QuotaPagedPoolUsage = info.QuotaPagedPoolUsage;
                QuotaNonPagedPoolUsage = info.QuotaNonPagedPoolUsage;
                PagefileUsage = info.PagefileUsage;
                PeakPagefileUsage = info.PeakPagefileUsage;
                PrivatePageCount = info.PrivatePageCount;
            }
        }

        static void Main()
        {
            const int STATUS_INFO_LENGTH_MISMATCH = unchecked((int)0xC0000004);

            // SystemProcessInformationのすべての情報を取得します。
            var buffer = Array.Empty<byte>();
            for (; ; )
            {
                var status = NativeMethods.NtQuerySystemInformation(
                        (int)SYSTEM_INFORMATION_CLASS.SystemProcessInformation,
                        buffer, (uint)buffer.LongLength, out var bufferLength);
                if (status != STATUS_INFO_LENGTH_MISMATCH)
                {
                    if (status == 0)
                        break;
                    throw new NTStatusException(status);
                }
                Array.Resize(ref buffer, (int)bufferLength);
            }

            // SystemProcessInformationの情報を解析します。
            var infos = new List<SystemProcessInformation>();
            var bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
            try
            {
                var address = bufferHandle.AddrOfPinnedObject();
                for (; ; )
                {
                    // GCHandleの解放後はSYSTEM_PROCESS_INFORMATIONのImageNameの値が無効となるため、
                    // 文字列をメモリから変数に保持するクラスへ変換します。
                    var info = Marshal.PtrToStructure<SYSTEM_PROCESS_INFORMATION>(address);
                    infos.Add(new SystemProcessInformation(ref info));
                    if (info.NextEntryOffset == 0)
                        break;
                    address = IntPtr.Add(address, (int)info.NextEntryOffset);
                }
            }
            finally
            {
                bufferHandle.Free();
            }

            // TODO: ここでSystemProcessInformationの内容を処理します。
            foreach (var info in infos)
            {
                Console.WriteLine($"{info.ImageName}, {info.UniqueProcessId}");
            }
        }
    }
}

NTStatusUtility.cs

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    public static partial class NTStatusUtility
    {
        public static bool IsSuccess(int status) => status >= 0;
        public static bool IsInformation(int status) => (status >> 30) == 1;
        public static bool IsWarning(int status) => (status >> 30) == 2;
        public static bool IsError(int status) => (status >> 30) == 3;

        public static int HResultFromNTStatus(int status)
        {
            const int FACILITY_NT_BIT = 0x10000000;
            return status | FACILITY_NT_BIT;
        }
    }

    public sealed class NTStatusException : COMException
    {
        public NTStatusException()
            : base()
        {
            status = 0;
        }

        public NTStatusException(int status)
            : base(null, NTStatusUtility.HResultFromNTStatus(status))
        {
            status = status;
        }

        public NTStatusException(string message, int status)
            : base(message, NTStatusUtility.HResultFromNTStatus(status))
        {
            status = status;
        }

        public NTStatusException(string message, Exception inner)
            : base(message, inner)
        {
            status = 0;
        }

        public NTStatusException(string message)
            : base(message)
        {
            status = 0;
        }

        private int status;
        public int Status => status;
    }
}