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