Немодные вещи куда интересней нежели то, что у всех на слуху и на виду. В мире .NET, например, немодной является рефлексия, о которой знают, но не пользуются в виду преклонения перед мантрами Рихтера. Несомненно, монография «CLR via C#» — лучшее из книг о .NET, однако сам ее автор далеко не везде следует своим же рекомендациям, а потому принимающим на веру абсолютно все написанное в ней, стоит перестать выдавать чужие мысли за свои.
Рефлексия типов действительно достаточно медленная вещь, но не настолько, чтобы отказаться от ее использования вовсе. В случае PowerShell издержки на упаковку и распаковку практически незаметны глазу, поэтому за производительность шибко опасаться не приходится. При этом в некоторых случаях использование рефлексии способно существенно сократить количество кода, что в свою очередь упрощает сопровождение последнего (с чем модники категорически не согласны) и открывает доступ к интимным местам операционной системы в обход оснастке управления (WMI). С точки зрения безопасности это не очень-то и хорошо, но вот в плане системного администрирования — недурственно. Хотя у этого мнения также найдутся свои противники.
Допустим, мы все же решились на использование рефлексии: как наиболее эффективно ее применять в PowerShell? Во-первых, готовых рецептов ни у кого нет, да и вряд ли когда-то будут, ибо самая суть уже описана все в той же «CLR via C#», во-вторых, само по себе понятие «эффективность» относительно, следовательно, рефлексию можно рассматривать лишь как альтернативный вариант решения некоторых задач. В качестве примера — пусть и весьма натянутого, — рассмотрим получение сборок установленных в GAC.
В отсутствии gacutil пример вполне может заменить собой первый, запускаемый с ключом /l. Впрочем, интереснее методов-оберток могут быть только WinAPI сигнатуры, однако перебирать типы в которых они имеются ILDASM'ом или просто ковыряться в исходных кода .NET платформы не шибко заманчиво. Почему бы не доверить эту работу самому PowerShell?!
Как оно работает? Мы передаем название некоторого публичного типа, скажем Regex, функции выше, далее тип проверяется на доступность в текущем домене приложений и извлекаются данные о сигнатурах сборки, в которой этот тип определен. Можно, конечно, вывод перенаправить в XML или любой другой формат, дабы не тратиться на повторное сканирование, но это кому как нравится, да и идея здесь главным образом в том, чтобы не размениваться на поиски сигнатур вручную. А сигнатур, между тем, не просто много, а очень много; особый интерес могут вызвать DeviceIoControl (Systemd.Data.dll), а также NtQueryInformationProcess и NtQuerySystemInformation (System.dll), — и вот здесь мы вплотную подобрались к вещам совершенно немодным: чтению данным по смещениям посредством рефлекторно вызываемых методов. В процессе подбора примера ничего оригинального, кроме как вывести список модулей, загруженных системой, не надумалось, так что будем рассматривать его.
То есть, размеры структур RTL_PROCESS_MODULES и RTL_PROCESS_MODULE_INFORMATION равны 288 и 284 байт соответственно.
Здорово! Дело за малым.
Вот таким вот незатейливым способом мы извлекли интересующие нас данные, — ничего сложного. При использовании Add-Type то же заняло примерно одинаковое количество кода, разница лишь в том, что в домене приложений не было создано вспомогательной сборки. Можно ли это считать приятным бонусом или это все же пространство для злокодинг-маневра, вопрос риторический.
Рефлексия типов действительно достаточно медленная вещь, но не настолько, чтобы отказаться от ее использования вовсе. В случае 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)
alien007
03.05.2016 10:22+1Powershell он же для другого. Если сильно нужно, в таком случае проще написать библиотеку командлетов на C# или Managed C++. PowerShell это просто язык описания сценариев из готовых кирпичиков.
Чем к примеру вон та портянка с WinAPI лучше одной строчки:
[System.Diagnostics.Process]::GetCurrentProcess().Modules
svekl
03.05.2016 10:30+1о которой знают, но не пользуются
Не могу с Вами согласиться, такой мощный инструмент не может не использоваться и используется повсеместно, в том числе, в популярных фреймворках и ORM, без рефлексии реализовать такое было бы невозможно.
kekekeks
Не проще ли было сгенерировать эти самые P/Invoke и структурки к ним через DefineDynamicAssembly? Зачем по кишкам фреймворка для этого ковыряться?
GrigoriZakharov
Не проще. Некоторое время назад мной была написана обертка над DefineDynamicAssembly, позволяющая создавать структуры, перечисления, делегаты и DllImport'ы налету в рамках одной сборки домена приложений, но фокус оказался действенным лишь в PowerShell 2, в более поздних версиях у хоста начинала «ехать крыша» и он плодил кучу левых сборок. Это во-первых. Во-вторых, «ковыряния в кишках», если Вы заметили, не производятся вручную. В-третьих, не вижу смысла изобретать велосипед сызнова: если есть сигнатура в самой платформе, для чего плодить вспомогательные сборки и захламляться домен приложений? Ко всему прочему при использовании DefineDynamicAssembly без оборачивания его в отдельную, скажем, библиотеку, возрастает количество набираемого текста кода, что для кодомазахистов самое то.
kekekeks
Нет гарантии, что если она там есть сегодня, то завтра там и останется. Это не публичное API всё же.
GrigoriZakharov
Ну, знаете ли: где гарантии что сегодня есть Microsoft, а завтра её нет? Согласно этой логике получается так, что проще и вовсе ничего не делать, а просто медитировать на .NET, — вот только какая от того будет практическая ценность, неясно. Взять WinAPI, например, ZwQuerySystemInformation начиная с восьмерки выпилен и те, кто использовал именно эту функцию, а не её Nt'шного близнеца, лишь развели руками. Понимаете в чем соль? Все предусмотреть невозможно, именно поэтому человечеством не было доселе изобретено что-то совершенное и вряд ли это ему когда-то удастся. И потом, мы все живем в настоящем, рассудит же будущее.