.

Некоторые программы могут открывать файл в двоичном или шестнадцатеричном виде. На экране 1 показан пример файла, открытого в текстовом редакторе в шестнадцатеричном коде. Серая область слева — смещение от начала файла (в шестнадцатеричном виде), в центре приведены шестнадцатеричные значения байтов файла, по 16 байт в строке. В правой части указаны видимые ASCII символы, соответствующие шестнадцатеричным значениям (неотображаемые символы показаны точками).

 

Двоичный файл, открытый в шестнадцатеричном коде
Экран 1. Двоичный файл, открытый в шестнадцатеричном коде

Нельзя ли получать такого рода данные с помощью средств PowerShell, то есть для вывода шестнадцатеричного дампа файла пользоваться простым запросом из командной строки без необходимости запускать отдельную программу?

Вывод дампа файла с помощью Get-Content

Для вывода побайтового дампа файла можно воспользоваться параметром -EncodingByte команды PowerShellGet-Content. Например, запрос

Get-ContentC:Windowsnotepad.exe -EncodingByte

позволяет получить файл программы WindowsNotepad.exe в виде массива байтов. Остается преобразовать его в шестнадцатеричный дамп, аналогичный тому, что показан на экране 1.

Одной из проблем является медленная скорость выполнения команды, даже при обработке относительно небольших файлов. Причина в том, что Get-Contentсначала считывает весь файл в память, а затем выводит его в виде массива.

Для повышения скорости у Get-Content есть параметр — ReadCount, позволяющий указать размер буфера. Например,

-ReadCount 16

предписывает Get-Content считывать единовременно по 16 байт (то есть файл считывается как серия 16-байтовых блоков). Если размер файла – величина, не кратная 16, то последний блок окажется меньше 16 байт. Для доступа к блокам используется ForEach-Object и переменная $_. Например, код PowerShell, приведенный в листинге 1, позволяет вывести файл Notepad.exe в шестнадцатеричном формате, где каждый байт представлен как пара шестнадцатеричных цифр, по 16 байт в строке.

Параметр -ReadCount команды Get-Content, несомненно, улучшает производительность, но в ходе тестирования я заметил, что многократная операция конкатенации строк (фрагмент A в листинге 1) замедляет выполнение сценария по мере увеличения размера файла. Я решил выяснить, нельзя ли повысить производительность другим способом.

Для считывания файла я решил вместо команды Get-Content использовать метод OpenRead объекта.NETFrameworkSystem.IO.File. Метод OpenRead возвращает доступный только для чтения объект FileStream. Подобно Get-Content, метод Read объекта FileStream предусматривает возможность единовременного считывания определенного количества байт файла. Однако вместо того чтобы ограничить буфер размером в 16 байт, я решил использовать буфер большего размера и проходить его шагами по 16 байт. Это позволяет избежать конкатенации строк, за исключением последних байт файла (если его размер – величина, не кратная 16 байт). Этот метод показан в листинге 2.

Сценарий, приведенный в листинге 2, открывает Notepad.exe как доступный только для чтения объект FileStream. Затем создается массив размером 64 Кбайт в качестве буфера для метода Read объекта FileStream (фрагмент A листинга 2). Метод Read возвращает байты, извлеченные из файла. Далее код проходит через буфер шагами по 16 байт. В коде используется метод Floor объекта Math для выяснения количества 16-байтных блоков в буфере (размер последнего блока, возвращаемого методом Read, может быть меньше 64 Кбайт). Применение оператора –f в сценарии, приведенном в листинге 2, позволяет вывести каждый 16-байтный срез буфера в шестнадцатеричном виде.

Если число байт, извлеченных методом Read из буфера, не кратно 16, то выражение if в первой строке фрагмента листинга 2 возвращает ненулевое значение. В этом случае код, приведенный в листинге 2, применяет конкатенацию строк, аналогично коду листинга 1, после чего выводит конечные байты файла.

Листинг 1 и листинг 2 производят одинаковые выходные данные. Однако код в листинге 2 выполняется быстрее, чем код в листинге 1, поскольку конкатенация строк для переменной $output происходит только один раз, и только если размер файла – величина, не кратная 16.

В обоих сценариях отсутствуют два момента: смещение от начала файла (серая область на экране 1) и ASCII-представление для каждого байта (16 символов справа на экране 1). Остается внести эти доработки, и сценарий полностью готов. Готовый сценарий Get-HexDump.ps1 (листинг 3) включает все компоненты (и некоторые другие усовершенствования).

Применение Get-HexDump.ps1

Синтаксис команды сценария выглядит следующим образом:

Get-HexDump.ps1 [-Path] [-UnprintableChar ] [-BufferSize ]

Параметр -Path указывает имя файла. Поскольку это первый позиционный параметр сценария, само имя ключа-Path необязательно. Групповые символы не допускаются: сценарий может обрабатывать единовременно только один файл.

Параметр -UnprintableChar задает символ, используемый для вывода непечатаемых символов ASCII (от 32 до 126). По умолчанию используется точка (.), но с помощью этого параметра можно указать другой символ. Для вывода пробела укажите символ пробела в одинарных (' ') или двойных («") кавычках.

Параметр -BufferSize позволяет указать размер буфера, используемого сценарием для считывания содержимого файла. Размер буфера по умолчанию – 65 536 байт (64 Кбайт). Значение параметра -BufferSize должно быть кратно 16.

Для каждого 16-байтного блока файла Get-HexDump.ps1 выводит строку, содержащую следующее:

  • смещение от начала файла в шестнадцатеричном виде;
  • шестнадцатеричные значения для каждого из 16 байт;
  • ASCII-символьное представление каждого печатаемого байта
  • замещающий символ (точка или заданное значение параметра –UnprintableChar) для каждого непечатаемого байта.

Перед открытием файла сценарий проводит проверки, в ходе которых:

  • подтверждается существование файла;
  • подтверждается, что размер файла составляет менее 4 Гбайт (смещение доходит только до 0xFFFFFFFF);
  • подтверждается, что запрашиваемый размер буфера – значение, кратное 16.

Для управления ошибками сценарий использует блоки try/catch/finally. Основная часть кода заключена внутри блока try. Если происходит неустранимая ошибка, то блок catch выводит объект-ошибку. Блок finally закрывает файл независимо от наличия ошибки. В сценарии также используется команда Write-Progress для визуализации хода его выполнения. Это особенно полезно при перенаправлении выходного результата сценария другой команде или файлу.

Можно поэкспериментировать с параметром –BufferSize для оценки влияния размера буфера на скорость выполнения сценария. Здесь пригодится команда Measure-Command. Следует отметить, что значительное увеличение буфера не сопровождается резким увеличением производительности, поскольку PowerShell по-прежнему выводит отформатированную строку каждые 16 байт. Маленький буфер (на моем компьютере – около 6 Кбайт) замедляет работу сценария. Индикатор выполнения (отображаемый с использованием Write-Progress) обновляется после каждого считывания буфера и движется медленнее, показывая растущую процентную долю выполненной операции, по мере увеличения размера буфера. Опытным путем удалось выйти на оптимальный размер буфера по умолчанию – 64 Кбайт.

На экране 2 показан пример выходных данных сценария Get-HexDump.ps1 – первые 720 (то есть 45 x 16) байт файла C:WindowsNotepad.exe в системе Windows 7 x64 ServicePack 1 (SP1).

 

Первые 720 байт файла Notepad.exe, полученные с помощью Get-HexDump.ps1
Экран 2. Первые 720 байт файла Notepad.exe, полученные с помощью Get-HexDump.ps1

Одним ограничением меньше

В PowerShell не предусмотрено специальной команды для просмотра содержимого двоичных файлов. Имея в своем распоряжении сценарий Get-HexDump.ps1, вы перестанете ощущать это ограничение.

Листинг 1. GetHex1.ps1

Get-Content»C:Windowsnotepad.exe«-Encoding Byte `
-ReadCount 16 | ForEach-Object {
$output =»«
foreach ( $byte in $_ ) {
#BEGIN CALLOUT A
$output +=»{0:X2} «-f $byte
#END CALLOUT A
}
$output
}

Листинг 2. GetHex2.ps1

$bufferSize = 65536
$stream = [System.IO.File]::OpenRead(
»C:Windowsnotepad.exe«)
while ( $stream.Position -lt $stream.Length ) {
#BEGIN CALLOUT A
$buffer = new-object Byte[] $bufferSize
$bytesRead = $stream.Read($buffer, 0, $bufferSize)
#END CALLOUT A
for ( $line = 0; $line -lt [Math]::Floor($bytesRead /
16); $line++ ) {
$slice = $buffer[($line * 16)..(($line * 16) + 15)]
((»{0:X2} {1:X2} {2:X2} {3:X2} {4:X2} {5:X2} «) +
(»{6:X2} {7:X2} {8:X2} {9:X2} {10:X2} {11:X2} «) +
(»{12:X2} {13:X2} {14:X2} {15:X2} «)) -f $slice
}
#BEGIN CALLOUT B
if ( $bytesRead % 16 -ne 0 ) {
$slice = $buffer[($line * 16)..($bytesRead — 1)]
$output =»«
foreach ( $byte in $slice ) {
$output +=»{0:X2} «-f $byte
}
$output
#END CALLOUT B
}
}
$stream.Close()

Листинг 3. Get-HexDump.ps1

# Get-HexDump.ps1
# Written by Bill Stewart (bstewart@iname.com)
#requires -version 2
<#
. SYNOPSIS
Outputs the contents of a file as a hex dump.
. DESCRIPTION
Outputs the contents of a file as a hex dump. This is useful for viewing the content of a binary file. Characters outside the range of standard printable ASCII range are output using a dot (.) by default. Use the -UnprintableChar parameter to specify a different character.
. PARAMETER Path
Specifies the path to a file. Wildcards are not permitted. The parameter name (»Path«) is optional.
. PARAMETER UnprintableChar
Specifies the character to use for output of characters in the file that are outside of the standard printable ASCII character range. The default value is a dot (».«).
. PARAMETER BufferSize
Specifies the buffer size to use. The file will be read this many bytes at a time. This parameter must be a multiple of 16. The default is 65536 (64KB).
#>
param(
[parameter(Position=0,Mandatory=$TRUE)]
[String] $Path,
[Char] $UnprintableChar =».«,
[UInt32] $BufferSize = 65536
)
if ( -not (test-path -literalpath $Path) ) {
write-error»Path '$Path' not found.«-category ObjectNotFound
exit
}
$item = get-item -literalpath $Path -force
if ( -not ($? -and ($item -is [System.IO.FileInfo])) ) {
write-error»'$Path' is not a file in the file system.«-category InvalidType
exit
}
if ( $item.Length -gt [UInt32]::MaxValue ) {
write-error»'$Path' is too large.«-category OpenError
exit
}
# The file will be output in 16-byte lines.
$bytesPerLine = 16
if ( $BufferSize % $bytesPerLine -ne 0 ) {
write-error»-BufferSize parameter must be a multiple of $bytesPerLine.«-category InvalidArgument
exit
}
# Keep track of our position within the file.
[UInt32] $fileOffset = 0
try {
$stream = [System.IO.File]::OpenRead($item.FullName)
while ( $stream.Position -lt $stream.Length ) {
# Read $BufferSize bytes into $buffer, returning $bytesRead.
$buffer = new-object Byte[] $BufferSize
$bytesRead = $stream.Read($buffer, 0, $BufferSize)
# Step through buffer $bytesPerLine bytes at a time.
for ( $line = 0; $line -lt [Math]::Floor($bytesRead / $bytesPerLine); $line++ ) {
# Grab 16-byte buffer slice, and created formatted string.
$slice = $buffer[($line * $bytesPerLine)..(($line * $bytesPerLine) + $bytesPerLine — 1)]
$hexOutput =»{0:X8} {01:X2} {02:X2} {03:X2} {04:X2} {05:X2} {06:X2} {07:X2} {08:X2} {09:X2} {10:X2} {11:X2} {12:X2} {13:X2} {14:X2} {15:X2} {16:X2} «-f (, $fileOffset + $slice)
$charOutput =»«
# Create ASCII printable character output.
foreach ( $byte in $slice ) {
if ( ($byte -ge 32) -and ($byte -le 126) ) {
$charOutput += [Char] $byte
}
else {
$charOutput += $UnprintableChar
}
}
»{0} {1}«-f $hexOutput, $charOutput
$fileOffset += $bytesPerLine
}
# Process bytes from end of file when file size not multiple of $bytesPerLine.
if ( $bytesRead % $bytesPerLine -ne 0 ) {
$slice = $buffer[($line * $bytesPerLine)..($bytesRead — 1)]
$hexOutput =»{0:X8} «-f $fileOffset
$charOutput =»«
foreach ( $byte in $slice ) {
$hexOutput +=»{0:X2} «-f $byte
if ( ($byte -ge 32) -and ($byte -le 126) ) {
$charOutput += [Char] $byte
}
else {
$charOutput += $UnprintableChar
}
}
# PadRight needed to align the output.
»{0} {1}«-f $hexOutput.PadRight(58), $charOutput
}
write-progress -activity»Get-HexDump.ps1«`
-status (»Dumping file '{0}'" -f $item.FullName) `
-percentcomplete (($fileOffset / $stream.Length) * 100)
}
}
catch [System.Management.Automation.MethodInvocationException] {
throw $_
}
finally {
$stream.Close()
}