Рефлексия в PowerShell

Немодные вещи куда интересней нежели то, что у всех на слуху и на виду. В мире .NET, например, немодной является рефлексия, о которой знают, но не пользуются в виду преклонения перед мантрами Рихтера. Несомненно, монография «CLR via C#» — лучшее из книг о .NET, однако сам ее автор далеко не везде следует своим же рекомендациям, а потому принимающим на веру абсолютно все написанное в ней, стоит перестать выдавать чужие мысли за свои.
Рефлексия типов действительно достаточно медленная вещь, но не настолько, чтобы отказаться от ее использования вовсе. В случае PowerShell издержки на упаковку и распаковку практически незаметны глазу, поэтому за производительность шибко опасаться не приходится. При этом в некоторых случаях использование рефлексии способно существенно сократить количество кода, что в свою очередь упрощает сопровождение последнего (с чем модники категорически не согласны) и открывает доступ к интимным местам операционной системы в обход оснастке управления (WMI). С точки зрения безопасности это не очень-то и хорошо, но вот в плане системного администрирования — недурственно. Хотя у этого мнения также найдутся свои противники.
Допустим, мы все же решились на использование рефлексии: как наиболее эффективно ее применять в PowerShell? Во-первых, готовых рецептов ни у кого нет, да и вряд ли когда-то будут, ибо самая суть уже описана все в той же «CLR via C#», во-вторых, само по себе понятие «эффективность» относительно, следовательно, рефлексию можно рассматривать лишь как альтернативный вариант решения некоторых задач. В качестве примера — пусть и весьма натянутого, — рассмотрим получение сборок установленных в GAC.

#requires -version 2

$al = New-Object Collections.ArrayList
[Object].Assembly.GetType(
  'Microsoft.Win32.Fusion'
).GetMethod('ReadCache').Invoke($null, @(
  [Collections.ArrayList]$al, $null, [UInt32]2
))
$al


В отсутствии gacutil пример вполне может заменить собой первый, запускаемый с ключом /l. Впрочем, интереснее методов-оберток могут быть только WinAPI сигнатуры, однако перебирать типы в которых они имеются ILDASM’ом или просто ковыряться в исходных кода .NET платформы не шибко заманчиво. Почему бы не доверить эту работу самому PowerShell?!

function Find-Pinvoke {
  <#
    .EXAMPLE
        PS C:\> Find-Pinvoke Regex
  #>
  param(
    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [String]$TypeName
  )
  
  begin {
    if (($base = $TypeName -as [Type]) -eq $null) {
      Write-Warning "В текущем домене приложений указанный тип не нйден."
      break
    }
  }
  process {
    foreach ($type in $base.Assembly.GetTypes()) {
      $type.GetMethods([Reflection.BindingFlags]60) | % {
        if (($_.Attributes -band 0x2000) -eq 0x2000) {
          $sig = [Reflection.CustomAttributeData]::GetCustomAttributes(
            $_ # данные о pinvoke методе
          ) | ? {$_.ToString() -cmatch 'DllImportAttribute'}
          New-Object PSObject -Property @{
            Module     = if (![IO.Path]::HasExtension(
              ($$ = $sig.ConstructorArguments[0].Value)
            )) { "$($$).dll" } else { $$ }
            EntryPoint = ($sig.NamedArguments | ? {
              $_.MemberInfo.Name -eq 'EntryPoint'
            }).TypedValue.Value
            MethodName = $_.Name
            Attributes = $_.Attributes
            TypeName   = $type.FullName
            Signature  = $_.ToString() -replace '(\S+)\s+(.*)', '$2 as $1'
            DllImport  = $sig
          } | Select-Object Module, EntryPoint, TypeName, MethodName, `
          Attributes, Signature, DllImport
        }
      }
    } #foreach
  }
  end {}
}


Как оно работает? Мы передаем название некоторого публичного типа, скажем Regex, функции выше, далее тип проверяется на доступность в текущем домене приложений и извлекаются данные о сигнатурах сборки, в которой этот тип определен. Можно, конечно, вывод перенаправить в XML или любой другой формат, дабы не тратиться на повторное сканирование, но это кому как нравится, да и идея здесь главным образом в том, чтобы не размениваться на поиски сигнатур вручную. А сигнатур, между тем, не просто много, а очень много; особый интерес могут вызвать DeviceIoControl (Systemd.Data.dll), а также NtQueryInformationProcess и NtQuerySystemInformation (System.dll), — и вот здесь мы вплотную подобрались к вещам совершенно немодным: чтению данным по смещениям посредством рефлекторно вызываемых методов. В процессе подбора примера ничего оригинального, кроме как вывести список модулей, загруженных системой, не надумалось, так что будем рассматривать его.

PS C:\> Invoke-Debugger
...
0.000> dt ole32!_rtl_process_modules /r
   +0x000 NumberOfModules  : Uint4B
   +0x004 Modules          : [1] _RTL_PROCESS_MODULE_INFORMATION
      +0x000 Section          : Ptr32 Void
      +0x004 MappedBase       : Ptr32 Void
      +0x008 ImageBase        : Ptr32 Void
      +0x00c ImageSize        : Uint4B
      +0x010 Flags            : Uint4B
      +0x014 LoadOrderIndex   : Uint2B
      +0x016 InitOrderIndex   : Uint2B
      +0x018 LoadCount        : Uint2B
      +0x01a OffsetToFileName : Uint2B
      +0x01c FullPathName     : [256] UChar
0.000> ?? sizeof(ole32!_rtl_process_modules)
unsigned int 0x120


То есть, размеры структур RTL_PROCESS_MODULES и RTL_PROCESS_MODULE_INFORMATION равны 288 и 284 байт соответственно.

0.000> dt ole32!_system_information_class
...
   SystemModuleInformation = 0n11
...


Здорово! Дело за малым.

#function Get-LoadedModules {
  begin {
    # акселератор типа Marshal
    if (($$ = [PSObject].Assembly.GetType(
      'System.Management.Automation.TypeAccelerators'
    ))::Get.Keys -notcontains 'Marshal') {
      [void]$$::Add('Marshal', [Runtime.InteropServices.Marshal])
    }

    $NtQuerySystemInformation = [Regex].Assembly.GetType(
      'Microsoft.Win32.NativeMethods'
    ).GetMethod('NtQuerySystemInformation')
    $ret = 0
  }
  process {
    try { # устанавливаем истинный размер буфера
      $ptr = [Marshal]::AllocHGlobal(1024)
      if ($NtQuerySystemInformation.Invoke($null, (
        $par = [Object[]]@(11, $ptr, 1024, $ret)
      )) -ne 0) {
        $ptr = [Marshal]::ReAllocHGlobal($ptr, [IntPtr]$par[3])
        if ($NtQuerySystemInformation.Invoke($null, @(11, $ptr, $par[3], 0)) -ne 0) {
          throw New-Object InvalidOperationException('Что-то пошло не так...')
        }
      }

      # считываем интересующую нас информацию относительно смещений
      0..([Marshal]::ReadInt32($ptr) - 1) | % {$i = 12}{
        New-Object PSObject -Property @{
          Address = '0x{0:X}' -f [Marshal]::ReadInt32($ptr, $i)
          Size = [Marshal]::ReadInt32($ptr, $i + 4)
          Name = [IO.Path]::GetFileName(([Marshal]::PtrToStringAnsi(
            [IntPtr]($ptr.ToInt64() + $i + 20), 256
          )).Split("`0")[0])
        }
        $i += 284 # переходим к следующей структуре
      } | Select-Object Name, Address, Size | Format-Table -AutoSize
    }
    catch {
      $_.Exception
    }
    finally {
      if ($ptr) { [Marshal]::FreeHGlobal($ptr) }
    }
  }
  end {
    [void]$$::Remove('Marshal') # удаляем акселератор
  }
#}


Вот таким вот незатейливым способом мы извлекли интересующие нас данные, — ничего сложного. При использовании Add-Type то же заняло примерно одинаковое количество кода, разница лишь в том, что в домене приложений не было создано вспомогательной сборки. Можно ли это считать приятным бонусом или это все же пространство для злокодинг-маневра, вопрос риторический.

© Habrahabr.ru