Немодные вещи куда интересней нежели то, что у всех на слуху и на виду. В мире .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 то же заняло примерно одинаковое количество кода, разница лишь в том, что в домене приложений не было создано вспомогательной сборки. Можно ли это считать приятным бонусом или это все же пространство для злокодинг-маневра, вопрос риторический.

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


  1. kekekeks
    03.05.2016 00:08
    +1

    Не проще ли было сгенерировать эти самые P/Invoke и структурки к ним через DefineDynamicAssembly? Зачем по кишкам фреймворка для этого ковыряться?


    1. GrigoriZakharov
      03.05.2016 07:14
      +1

      Не проще. Некоторое время назад мной была написана обертка над DefineDynamicAssembly, позволяющая создавать структуры, перечисления, делегаты и DllImport'ы налету в рамках одной сборки домена приложений, но фокус оказался действенным лишь в PowerShell 2, в более поздних версиях у хоста начинала «ехать крыша» и он плодил кучу левых сборок. Это во-первых. Во-вторых, «ковыряния в кишках», если Вы заметили, не производятся вручную. В-третьих, не вижу смысла изобретать велосипед сызнова: если есть сигнатура в самой платформе, для чего плодить вспомогательные сборки и захламляться домен приложений? Ко всему прочему при использовании DefineDynamicAssembly без оборачивания его в отдельную, скажем, библиотеку, возрастает количество набираемого текста кода, что для кодомазахистов самое то.


      1. kekekeks
        03.05.2016 12:03
        +1

        . В-третьих, не вижу смысла изобретать велосипед сызнова: если есть сигнатура в самой платформ

        Нет гарантии, что если она там есть сегодня, то завтра там и останется. Это не публичное API всё же.


        1. GrigoriZakharov
          03.05.2016 20:10

          Ну, знаете ли: где гарантии что сегодня есть Microsoft, а завтра её нет? Согласно этой логике получается так, что проще и вовсе ничего не делать, а просто медитировать на .NET, — вот только какая от того будет практическая ценность, неясно. Взять WinAPI, например, ZwQuerySystemInformation начиная с восьмерки выпилен и те, кто использовал именно эту функцию, а не её Nt'шного близнеца, лишь развели руками. Понимаете в чем соль? Все предусмотреть невозможно, именно поэтому человечеством не было доселе изобретено что-то совершенное и вряд ли это ему когда-то удастся. И потом, мы все живем в настоящем, рассудит же будущее.


  1. alien007
    03.05.2016 10:22
    +1

    Powershell он же для другого. Если сильно нужно, в таком случае проще написать библиотеку командлетов на C# или Managed C++. PowerShell это просто язык описания сценариев из готовых кирпичиков.
    Чем к примеру вон та портянка с WinAPI лучше одной строчки:
    [System.Diagnostics.Process]::GetCurrentProcess().Modules


  1. svekl
    03.05.2016 10:30
    +1

    о которой знают, но не пользуются

    Не могу с Вами согласиться, такой мощный инструмент не может не использоваться и используется повсеместно, в том числе, в популярных фреймворках и ORM, без рефлексии реализовать такое было бы невозможно.