Вывод комадлета Get-Process для восприятия не слишком удобен — просто список, в котором данные ко всему прочему отражены в стиле стран ближнего востока, то бишь справа налево, подается Microsoft как идеолгоически верный путь к пониманию сути того, что творится в процессах. Речь не о том как с помощью PowerShell палить вирусы, а об отсутствии визуального представления какой процесс какого Ерофеича родил, чай, ведь уже пятая версия PowerShell'а, а древовидного представления по-прежнему нема, да и в ближайшей перспективе, должно быть, не предвидется.

И здесь из зрительного зала раздается: «А нафига нам древовидное представление процессов в PowerShell, когда есть ProcessExplorer, на худой конец — pslist?» Во-первых, GUI для консольщика как серпом по яйцам, во-вторых, какой резон разводить зоопарк из набора сторонних утилит, когда наличие PowerShell по сути является синонимом «уже все есть»? — остается лишь творить под цвет своих фломастеров. Преамбула приобретает некий сюрреалистический оттенок, да и рискует затянуться, если продолжать в том же роде, так что готовим фломастеры…

Сюрприз!


Если кто-то из читающих раскатал губу на WMI, то может смело закатывать ее фломастером обратно, ибо речь опять-таки пойдет о рефлексии, точнее не столько о ней самой, сколько о достижении цели через нее. Кудряво сказано, но да ладно. Как подсказывает Кэп, для построения дерева процессов нужно знать такие параметры, как имя процесса, его PID, а также PPID. Последний служит отправной точной при определении отпрыска от родителя, причем получить оный в Windows можно как миниму тремя приемами дзюдо: счетчики производительности, WMI и NtQuerySystemInformation. Первый (ровно как и второй) идет лесом, так как в грубом приближении является оберткой над NtQuerySystemInformation, токмо с тормозами в придачу — показатель варьируется от начинки ПК, но это тема отдельного разговора.
Открываем Vim (или что у кого там любимое) и пишем:

Set-Variable ($$ = [Regex].Assembly.GetType(
  'Microsoft.Win32.NativeMethods'
).GetMethod('NtQuerySystemInformation')).Name $$

Итак, мы определили переменую $NtQuerySystemInformation. Теперь нужно получить указатель на структуру SYSTEM_PROCESS_INFORMATION, в Win7 x86 выглядящую так:

   +0x000 NextEntryOffset  : Uint4B
   +0x004 NumberOfThreads  : Uint4B
   +0x008 WorkingSetPrivateSize : _LARGE_INTEGER
   +0x010 HardFaultCount   : Uint4B
   +0x014 NumberOfThreadsHighWatermark : Uint4B
   +0x018 CycleTime        : Uint8B
   +0x020 CreateTime       : _LARGE_INTEGER
   +0x028 UserTime         : _LARGE_INTEGER
   +0x030 KernelTime       : _LARGE_INTEGER
   +0x038 ImageName        : _UNICODE_STRING
   +0x040 BasePriority     : Int4B
   +0x044 UniqueProcessId  : Ptr32 Void
   +0x048 InheritedFromUniqueProcessId : Ptr32 Void
   +0x04c HandleCount      : Uint4B
   +0x050 SessionId        : Uint4B
   +0x054 UniqueProcessKey : Uint4B
   +0x058 PeakVirtualSize  : Uint4B
   +0x05c VirtualSize      : Uint4B
   +0x060 PageFaultCount   : Uint4B
   +0x064 PeakWorkingSetSize : Uint4B
   +0x068 WorkingSetSize   : Uint4B
   +0x06c QuotaPeakPagedPoolUsage : Uint4B
   +0x070 QuotaPagedPoolUsage : Uint4B
   +0x074 QuotaPeakNonPagedPoolUsage : Uint4B
   +0x078 QuotaNonPagedPoolUsage : Uint4B
   +0x07c PagefileUsage    : Uint4B
   +0x080 PeakPagefileUsage : Uint4B
   +0x084 PrivatePageCount : Uint4B
   +0x088 ReadOperationCount : _LARGE_INTEGER
   +0x090 WriteOperationCount : _LARGE_INTEGER
   +0x098 OtherOperationCount : _LARGE_INTEGER
   +0x0a0 ReadTransferCount : _LARGE_INTEGER
   +0x0a8 WriteTransferCount : _LARGE_INTEGER
   +0x0b0 OtherTransferCount : _LARGE_INTEGER

Причем из всей структуры нас интересуют такие поля как NextEntryOffset, ImageName, UniqueProcessId и InheritedFromUniqueProcessId, так что для получения только этих четырех полей определять структуру в домене приложений слишком жирно — воспользуемся методами типа Marshal.
Получаем указатель:

if (($ta = [PSObject].Assembly.GetType(
  'System.Management.Automation.TypeAccelerators'
))::Get.Keys -notcontains 'Marshal') {
  $ta::Add('Marshal', [Runtime.InteropServices.Marshal])
}

$ret = 0
try {
  #задаем размер буфера минимальным значением
  $ptr = [Marshal]::AllocHGlobal(1024)
  if ($NtQuerySystemInformation.Invoke($null, (
    $par = [Object[]]@(5, $ptr, 1024, $ret)
  )) -eq 0xC0000004) { #STATUS_INFO_LENGTH_MISMATCH
    $ptr = [Marshal]::ReAllocHGlobal($ptr, [IntPtr]$par[3])
    if ($NtQuerySystemInformation.Invoke($null, (
      $par = [Object[]]@(5, $ptr, $par[3], 0)
    )) -ne 0) {
      throw New-Object InvalidOperationException('Что-то пошло не так...')
    }
  }
}
catch { $_.Exception }
finally {
  if ($ptr -ne $null) {
    [Marshal]::FreeHGlobal($ptr)
  }
}

Указатель получили, читаем данные. Стоп! А ведь ImageName — это структура UNICODE_STRING, как быть? Делаем ход конем:

$UNICODE_STRING = [Activator]::CreateInstance(
  [Object].Assembly.GetType(
    'Microsoft.Win32.Win32Native+UNICODE_STRING'
  )
)

Вот теперь мы во всеоружии и готовы «читать» указатель.

$len = [Marshal]::SizeOf($UNICODE_STRING) - 1
$tmp = $ptr
$Processes = while (($$ = [Marshal]::ReadInt32($tmp))) { #NextEntryOffset
  [Byte[]]$bytes = 0..$len | ForEach-Object {$ofb = 0x38}{
    [Marshal]::ReadByte($tmp, $ofb)
    $ofb++
  }
  #конвертируем байты в UNICODE_STRING
  $gch = [Runtime.InteropServices.GCHandle]::Alloc($bytes, 'Pinned')
  $uni = [Marshal]::PtrToStructure(
    $gch.AddrOfPinnedObject(), [Type]$UNICODE_STRING.GetType()
  )
  $gch.Free()
  
  New-Object PSObject -Property @{
    ProcessName = if ([String]::IsNullOrEmpty((
      $proc = $uni.GetType().GetField(
        'Buffer', [Reflection.BindingFlags]36
      ).GetValue($uni))
    )) { 'Idle' } else { $proc }
    PID = [Marshal]::ReadInt32($tmp, 0x44)
    PPID = [Marshal]::ReadInt32($tmp, 0x48)
  }
  $tmp = [IntPtr]($tmp.ToInt32() + $$)
}

Переменная $Processes отныне хранит массив объектов PSObject, эдакие контейнеры для нужных нам данных. Теперь, согласно женевской конвенции, остается построить само дерево.

function Get-ProcessChild {
  param(
    [Parameter(Mandatory=$true, Position=0)]
    [PSObject]$Process,
    
    [Parameter(Position=1)]
    [Int32]$Depth = 1
  )
  
  $Processes | Where-Object {
    $_.PPID -eq $Process.PID -and $_.PPID -ne 0
  } | ForEach-Object {
    "$("$([Char]32)" * 2 * $Depth)$($_.ProcessName) ($($_.PID))"
    Get-ProcessChild $_ (++$Depth)
    $Depth--
  }
}

$Processes | Where-Object {
  -not (Get-Process -Id $_.PPID -ea 0) -or $_.PPID -eq 0
} | ForEach-Object {
  "$($_.ProcessName) ($($_.PID))"
  Get-ProcessChild $_
}

После запуска получим в хосте древовидное представление процессов. Собственно, на этом вечерний эротический сеанс показ окончен, можно расходиться.
Полный код
#function Get-ProcessTree {
  <#
    .NOTES
        Вместо методов расширений .Where и .ForEach используются
        одноименные командлеты в целях совместимости с PS -lt v5
  #>
  begin {
    Set-Variable ($$ = [Regex].Assembly.GetType(
      'Microsoft.Win32.NativeMethods'
    ).GetMethod('NtQuerySystemInformation')).Name $$
    
    $UNICODE_STRING = [Activator]::CreateInstance(
      [Object].Assembly.GetType(
        'Microsoft.Win32.Win32Native+UNICODE_STRING'
      )
    )
    
    function Get-ProcessChild {
      param(
        [Parameter(Mandatory=$true, Position=0)]
        [PSObject]$Process,
        
        [Parameter(Position=1)]
        [Int32]$Depth = 1
      )
      
      $Processes | Where-Object {
        $_.PPID -eq $Process.PID -and $_.PPID -ne 0
      } | ForEach-Object {
        "$("$([Char]32)" * 2 * $Depth)$($_.ProcessName) ($($_.PID))"
        Get-ProcessChild $_ (++$Depth)
        $Depth--
      }
    }
    
    if (($ta = [PSObject].Assembly.GetType(
      'System.Management.Automation.TypeAccelerators'
    ))::Get.Keys -notcontains 'Marshal') {
      $ta::Add('Marshal', [Runtime.InteropServices.Marshal])
    }
  }
  process {
    try {
      $ret = 0
      $ptr = [Marshal]::AllocHGlobal(1024)
      
      if ($NtQuerySystemInformation.Invoke($null, (
        $par = [Object[]]@(5, $ptr, 1024, $ret)
      )) -eq 0xC0000004) { #STATUS_INFO_LENGTH_MISMATCH
        $ptr = [Marshal]::ReAllocHGlobal($ptr, [IntPtr]$par[3])
        if (($nts = $NtQuerySystemInformation.Invoke($null, (
          $par = [Object[]]@(5, $ptr, $par[3], 0)
        ))) -ne 0) {
          throw New-Object InvalidOperationException(
            'NTSTATUS: 0x{0:X}' -f $nts
          )
        }
      }
      
      $len = [Marshal]::SizeOf($UNICODE_STRING) - 1
      $tmp = $ptr
      $Processes = while (($$ = [Marshal]::ReadInt32($tmp))) {
        [Byte[]]$bytes = 0..$len | ForEach-Object {$ofb = 0x38}{
          [Marshal]::ReadByte($tmp, $ofb)
          $ofb++
        }
        
        $gch = [Runtime.InteropServices.GCHandle]::Alloc($bytes, 'Pinned')
        $uni = [Marshal]::PtrToStructure(
          $gch.AddrOfPinnedObject(), [Type]$UNICODE_STRING.GetType()
        )
        $gch.Free()
        
        New-Object PSObject -Property @{
          ProcessName = if ([String]::IsNullOrEmpty((
            $proc = $uni.GetType().GetField(
              'Buffer', [Reflection.BindingFlags]36
            ).GetValue($uni))
          )) { 'Idle' } else { $proc }
          PID = [Marshal]::ReadInt32($tmp, 0x44)
          PPID = [Marshal]::ReadInt32($tmp, 0x48)
        }
        $tmp = [IntPtr]($tmp.ToInt32() + $$)
      }
    }
    catch { $_.Exception }
    finally {
      if ($ptr -ne $null) {
        [Marshal]::FreeHGlobal($ptr)
      }
    }
  }
  end {
    if ($Processes -eq $null) {
      break
    }
    
    $Processes | Where-Object {
      -not (Get-Process -Id $_.PPID -ea 0) -or $_.PPID -eq 0
    } | ForEach-Object {
      "$($_.ProcessName) ($($_.PID))"
      Get-ProcessChild $_
    }
    
    [void]$ta::Remove('Marshal')
  }
#}

Поделиться с друзьями
-->

Комментарии (8)


  1. rbobot
    03.06.2016 20:32

    Григорий, не останавливайтесь! Очень интересно ходить по вашим следам.


  1. AAT666
    03.06.2016 23:47

    ISE вылетает с ошибкой «System.AccessViolationException»… $Processes не получает данные, позже поразбираюсь. Так-то интересно! Мерси.


  1. AAT666
    04.06.2016 08:39

    Ок, все работает. Сорри!.. но до такого уровня мне — как пешком до Луны…


  1. maydjin
    04.06.2016 11:35

    Не хочу показаться занозой в заднице. Но, это попытка сделать вывод как в ps -H?

    Кстати в git bash сия утилитка имеет место быть, только нет щас винды под рукой что бы проверить, умеет ли она там в этот флаг.


  1. pak-nikolai
    05.06.2016 09:21

    выдает ошибку «Переполнение в результате выполнения арифметической операции.»
    win 10, powerhsell 5.0, CLR 4

    этот код делает то же самое?

    Function Show-ProcessTree
    {            
    [CmdletBinding()]            
    Param()            
        Begin {            
            # Identify top level processes            
            # They have either an identified processID that doesn't exist anymore            
            # Or they don't have a Parentprocess ID at all            
            $allprocess  = Get-WmiObject -Class Win32_process            
            $uniquetop  = ($allprocess).ParentProcessID | Sort-Object -Unique            
            $existingtop =  ($uniquetop | ForEach-Object -Process {$allprocess | Where ProcessId -EQ $_}).ProcessID            
            $nonexistent = (Compare-Object -ReferenceObject $uniquetop -DifferenceObject $existingtop).InPutObject            
            $topprocess = ($allprocess | ForEach-Object -Process {            
                if ($_.ProcessID -eq $_.ParentProcessID){  $_.ProcessID  }            
                if ($_.ParentProcessID -in $nonexistent) {  $_.ProcessID  }            
            })            
            # Sub functions            
            # Function that indents to a level i            
            function Indent {            
                Param([Int]$i)            
                $Global:Indent = $null            
                For ($x=1; $x -le $i; $x++)            
                {            
                    $Global:Indent += [char]9            
                }            
            }            
            Function Get-ChildProcessesById {            
            Param($ID)            
                # use $allprocess variable instead of Get-WmiObject -Class Win32_process to speed up            
                $allprocess | Where { $_.ParentProcessID -eq $ID} | ForEach-Object {            
                    Indent $i            
                    '{0}{1} {2}' -f $Indent,$_.ProcessID,($_.Name -split "\.")[0]            
                    $i++            
                    Get-ChildProcessesById -ID $_.ProcessID            
                    $i--            
                }            
            } # end of function            
        }            
        Process {            
            $topprocess | ForEach-Object {            
                '{0} {1}' -f $_,(Get-Process -Id $_).ProcessName            
                # Avoid processID 0 because parentProcessId = processID            
                if ($_ -ne 0 )            
                {            
                    $i = 1            
                    Get-ChildProcessesById -ID $_            
                }            
            }            
        }             
        End {}            
    }
    
    Clear-Host             
    Show-ProcessTree            
    Start-Sleep -Seconds 1   
    


    взято отсюда https://p0w3rsh3ll.wordpress.com/2012/10/12/show-processtree/ используется WMI


    1. AAT666
      05.06.2016 09:49

      Это для x86.
      На х64 тоже получал такую ошибку.


      1. pak-nikolai
        05.06.2016 10:16

        обычно сервера на 64 битах. вариант с WMI хоть в продакшене можно использовать, правда не представляю зачем
        код приведенный выше в комменте выдает вот такую картинку
        image

        обычно:

        • ищешь процесс от юзера
        • следишь за загрузкой процессов (например 20 юзеров стартуют процесс, и нужно оследить чтобы не запустился 21ый, или высвобождать по какомуто условию, или смотреть кто сколько жрет памяти)
        • отслеживаешь запуск и завершение от юзера
        • производишь какието действия от появления процесса.

        Но вот искать процесс запущенный другим процессом, да еще глазами в списке на терминале с 60 пользователями?!

        Не прощще ли сделать фильтрацию в запросе?


        1. AAT666
          05.06.2016 10:43

          Да я-то с Вами согласен, уважаемый. Но такие претензии, все же, лучше автору предъявлять — а я лишь просто сообщил, как запустить скрипт.

          … и к тому же, автор в начале статьи все разъяснил — как и почему он создал этот скрипт. Да и положа руку на сердце, стоит признать — очень увлекательно у него это получилось! За что ему огромный респект, однозначно!