.
Некоторые программы могут открывать файл в двоичном или шестнадцатеричном виде. На экране 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).
Экран 2. Первые 720 байт файла Notepad.exe, полученные с помощью Get-HexDump.ps1 |
Одним ограничением меньше
В PowerShell не предусмотрено специальной команды для просмотра содержимого двоичных файлов. Имея в своем распоряжении сценарий Get-HexDump.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 }
$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()
# 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() }