Програмерский спецназ

Уступая пожеланиям трудящихся :), я решил посмотреть, можно ли извлечь какой-нибудь практический смысл из использования Alt-кодов. Это пересеклось с другой давно меня занимавшей темой. Представьте необитаемый остров, пещеру, а в ней много "железа", но почти нет софта. Ситуация может оказаться не такой фантастической, как кажется на первый взгляд, начиная от случайно оставшейся в деревенском чулане "двойки" с допотопным модемом и кончая системами на основе NT с умышленно стертым debug'ом, не говоря о более продвинутых средствах разработки. В общем, мы оказываемся в ситуации некоего "виртуального экстрима", когда только от наших собственных способностей и умений, как и в случае экстрима реального, зависит, сможем ли мы "выжить" на этом необитаемом виртуальном острове.

Чтобы не было недоразумений, с самого начала подчеркну, что речь идет именно о "выживании". Никаких рафинированных систем с SDK, никаких уютных комфортных квартир - тайга, сырость, даже спичек нет - разжигай свой костер только подручными средствами. Любителям комфорта и собственных привычек тут делать совершенно нечего. Как и в настоящем спецназе, здесь могут пройти лишь парни с огрубевшими мозолистыми руками и обветренными лицами; их я и приглашаю совершить со мной эту прогулку.

Здесь мы сталкиваемся с интересным вопросом: а что, собственно, считать "подручными средствами"? Понятно, что компьютер уже должен быть :). Единственным его ПО "в комплекте" является BIOS. Поэтому можно было бы потребовать, что сначала нужно создать загрузочную дискету - но для этого опять нужен компьютер с уже запущенной, пусть самой примитивной, ОС! Замкнутый круг. В принципе, "задачей спецназа" можно было бы назвать подготовку загрузочной дискеты с использованием собранного из подручных средств электромагнита :). Или, скажем, с использованием тех же проводов и прочих подручных средств перепрограммировать BIOS так, чтобы можно было вводить бинарные значения в оперативную память прямо с клавиатуры. На самом деле, это весьма забавно; возможно, когда-нибудь мы этим даже и займемся, но все же это уводит нас в область железа. А у нас спецназ програмерский, а не аппаратный.

Поэтому примем следующий постулат: доступна загрузочная дискета с минимальными возможностями. Если такой дискеты нет, задача считается неосуществимой, а для ее решения требуется вызвать "аппаратный спецназ". Минимальный загрузочный набор является ориентированным на DOS (boot-сектор, io.sys, msdos.sys, coomand.com или их аналоги) - это наиболее распространенный вариант; вероятность того, что найдется загрузочная дискета или хотя бы "форматированная под загрузку" (format /s), относительно велика. Обратите внимание, что это никак не уменьшает наши возможности: мы получаем доступ к любому IBM-совместимому компьютеру (независимо от возможно установленной на нем ОС - будь то Windows XP, Windows 2003 Server, Linux, FreeBSD или т.п.), а через него - при наличии соответствующего коммуникационного железа - мы можем (в принципе!) получить доступ к любым другим системам.

Замечание для тех, кто любит все делать своими руками и хочет создать такую загрузочную дискету. При ее создании из-под Windows 98 с помощью команды 'sys a:' на дискету записывается фиктивный command.com размером 20 Кб; его нужно удалить, а вместо него вручную скопировать настоящий размером 92 Кб из каталога Windows. В Windows Me команда sys не работает для дискет, а format не имеет опции /s - для создания загрузочной дискеты необходимо воспользоваться вкладкой "Загрузочный диск" диалога "Установка и удаление программ" панели управления. Затем, для чистоты эксперимента, можно удалить все лишние файлы, кроме перечисленных выше. В линейке Windows NT традиционную загрузочную дискету создать нельзя; там создается аварийный набор из нескольких дискет, но это другое. В этом случае используются дискеты, созданные в DOS или Windows9*/Me, или же при уже запущенной ОС используется командный интерпретатор cmd.exe. Заметим также, что ничто не мешает работать с окном DOS в уже запущенном Windows9*/Me или с полноценным, а не урезанным DOS - дискета с указанными файлами является лишь минимальным необходимым набором.

Итак, нам удалось слегка оживить холодное железо, мы запустились и имеем теперь интерактивную командную оболочку с довольно ограниченными возможностями. Что дальше? Как "вытянуть" из наличного железа возможности, которые в нем могут быть запрятаны? Насколько далеко мы сможем продвинуться в этом предприятии, целиком зависит от наших собственных знаний и умений. Повторюсь еще раз, это не является нормальным программированием. К сожалению, термин "экстремальное программирование" уже используется в другом контексте - нам бы он очень подошел. Тем не менее, это и есть настоящий экстрим. Точно такой же, как лазание по вертикальным откосам скал и через таежные буреломы, несмотря на наличие ровных автострад и железных дорог - в реальном мире находятся любители таких путешествий, почему бы им не быть в мире виртуальном?

Следующим шагом, как и при выживании в реальном мире, нам нужно начать создавать простейшие, но жизненно необходимые в повседневном использовании инструменты - те, с помощью которых мы и должны выполнять свою задачу (будь то коммуникационная программа, какой-нибудь драйвер для железа или т.п.) Причем все подчинено одной главной цели - выжить и выполнить поставленную задачу (настоящий спецназ!) А это значит - ничего лишнего; экономить силы, экономить время, делать только абсолютно необходимое, выявляя самую суть и безжалостно отбрасывая все постороннее. Тут уже не до програмерских примочек, оптимизаций, шлифовки и даже (о ужас!) "правильного стиля". Все должно быть надежно и просто, как автомат Калашникова.

Как вы уже, наверное, догадались, такой инструмент мы и будем создавать с помощью так называемых Alt-кодов - встроенной в интерпретатор command.com (а также cmd.exe в Windows NT и последующих) возможности набирать бинарные значения, используя правый цифровой блок клавиатуры при нажатой клавише Alt. В самом деле, мы можем использовать команду 'copy con file.bin' в сочетании с Alt-кодами, чтобы создать двоичный файл file.bin. Хотя здесь есть и проблема.

Дело в том, что не все двоичные значения можно набрать таким способом - некоторые управляющие символы вызывают непосредственную реакцию системы, а в файл не записываются. К этим значениям относятся: 0, 3, 6, 8, 10 (0Ah), 13 (0Dh), 16 (10h), 19 (13h), 26 (1Ah), 27 (1Bh), 127 (7Fh). Причем эти значения относятся к command.com DOS (соответственно, и Windows9*). Cmd.exe от линейки Windows NT в этом отношении более "либеральна" - "запрещенными" являются лишь символы 8, 13 (0Dh), 26 (1Ah) и 127 (7Fh). К тому же, размер буфера строки в команде copy ограничен 127 байтами (в Windows 2k/Xp - 510), а это неприятно осложняет создание нормально функционирующих программ. Большие файлы набирать с помощью 'copy con' можно, но придется специально обрабатывать переходы через символы конца строки (CRLF), которые будут вставляться в файл.

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

В нашем случае это будет даже не редактор, а скорее конвертор, переводящий поток ASCII-символов из стандартного ввода в поток двоичных значений в стандартный вывод. Причем все символы, не попадающие в диапазон ASCII-символов 0..9A..Fa..f, просто игнорируются. Это позволяет форматировать вводимый текст для большего удобства восприятия (добавлять пробелы между отдельными байтами, а между строками вставлять перевод строки - CRLF). Алгоритм приведен на следующем рисунке.

Нам понадобятся три переменные размером в 1 байт: для хранения обрабатываемого байта (назовем ее place), для хранения предыдущего байта (place2) и указатель того, относится ли обрабатываемый байт к первой или второй тетраде (check). В целях экономии (нам нужно уложиться в 127 байтов!) эти переменные оставим за пределами com-файла. Тем не менее, в начале программы необходимо на всякий случай проинициализировать их нулями. Команда типа 'mov byte ptr [place],0' не годится, т.к. в этом случае в код попадает непосредственное значение 0, а это недопустимый символ. Поэтому используем косвенный способ с использованием регистра:

100: 31C0                      xor    ax,ax
102: A3nnnn                    mov    [place],ax
105: A3nnnn                    mov    [check],ax

В листинге слева приведено смещение с начала com-файла (в hex со смещения 100h), далее - машинный код, а затем с отступом - ассемблерная мнемоника для удобства восприятия. Здесь мы инициализируем сразу по 2 байта. Вместо 'nnnn' мы потом подставим действительные адреса этих переменных.

Далее нам нужно ввести 1 байт данных из стандартного ввода. Для этого используется функция 3Fh чтения из файла прерывания DOS 21h. В регистре AH должен содержаться номер функции (3Fh), в регистре BX - дескриптор файла (для стандартного ввода - 0), в регистре CX - число читаемых байтов (в нашем случае 1), DS:DX адресует буфер, куда должны читаться данные. В данном случае, нельзя использовать инструкцию 'mov cx,1', т.к. получаем код 'B9 01 00' - появляется запрещенный нулевой байт.

108: B43F                      mov    ah,3F
10A: 31DB                      xor    bx,bx
10C: 31C9                      xor    cx,cx
10E: 41                        inc    cx
10F: BAnnnn                    mov    dx,offset place
112: CD21                      int    21

Дальше начинаются проверки введенного значения, здесь же осуществляется и основное преобразование вводимых значений. Для начала проверим состояние флага переноса (CF) после функции файлового чтения - если он установлен, произошла ошибка чтения, выходим из программы:

114: 72nn                  (1) jb     EXIT

В регистре AX возвращается число прочитанных байтов. Если прочитано 0 байт, файл закончился - также выходим.

116: 85C0                      test   ax,ax
118: 74nn                  (2) jz     EXIT

Далее загружаем сохраненный в переменной 'place' байт в регистр AH для анализа.

11A: 8A26nnnn                  mov    ah,byte ptr [place]
11E: 80FC30                    cmp    ah,30
121: 72E5                  (3) jb     108
123: 80FC39                    cmp    ah,39
126: 7705                  (4) jnbe   12D
128: 80EC30                    sub    ah,30
12B: EBnn                  (5) jmp    NUMBER
12D: 80FC41                    cmp    ah,41
130: 72D6                  (6) jb     108
132: 80FC46                    cmp    ah,46
135: 7705                  (7) jnbe   13C
137: 80EC37                    sub    ah,37
13A: EBnn                  (8) jmp    NUMBER
13C: 80FC61                    cmp    ah,61
13F: 72C7                  (9) jb     108
141: 80FC66                    cmp    ah,66
144: 77C2                      jnbe   108
146: 80EC57                    sub    ah,57

Логика исполнения должна быть ясна из рисунка. Небольшие ссылки вперед ("перескакивание через инструкцию") вычислены сразу; часто использующаяся ссылка назад - на ввод очередного символа, соответствующий код расположен по смещению 108h. Остальные ссылки вперед помечены метками NUMBER и FIRST (далее); они для удобства обозначены также и на рисунке. Начало следующего участка кода соответствует метке NUMBER: мы получили смещение (149h), теперь можно вычислить все ранее сделанные ссылки на этот адрес.

149: 90                        nop
14A: 8A1Ennnn                  mov    bl,byte ptr [check]
14E: 84DB                      test   bl,bl
150: 74nn                      jz     FIRST
152: F6DC                      neg    ah
154: 2826nnnn                  sub    byte ptr [place2],ah

Этот код может показаться несколько странным. Зачем нужен nop, когда мы и так стараемся вместить код в ограниченные 127 байтов? Если вы посмотрите на сделанные ранее ссылки вперед и подсчитаете соответствующие смещения (в частности, для инструкции по адресу 13Ah), при ссылке на адрес 149h вы получите инструкцию 'EB 0D', а 0Dh у нас "запрещенный" символ. Поэтому ближайший адрес, на который мы можем ссылаться, - 14Ah, а по адресу 149h придется оставить пустую инструкцию. Далее, если введенный ASCII-символ соответствует первой тетраде выходного байта, в переменной check будет 0. Но использовать инструкцию типа 'cmp byte ptr [check],0' нельзя, поскольку в код опять попадает 0 в виде непосредственного значения. Поэтому используем косвенный метод через копирование значения в регистр BL и последующим test.

Вычисленное значение второй (в данном случае) тетрады необходимо добавить к переменной (place2), содержащей значение сохраненной первой тетрады. К несчастью, опкод инструкции add равен 0, поэтому нам придется вообще от нее отказаться, заменив ее двумя инструкциями neg и sub. Думаю, с этим разобрались.

Далее идет вывод одного байта в стандартный вывод. Используется функция 40h файлового вывода прерывания DOS 21h. В регистре AH, как обычно, должен быть номер функции (40h), в BX - дескриптор файла (на этот раз стандартный вывод, т.е. 1), в CX - число выводимых байтов (1), DS:DX адресует буфер для вывода (у нас - адрес переменной place2). Для избежания появления нулевых байтов в коде придется применить те же хитрости с регистрами и inc, которые мы использовали при вводе. В случае ошибки (установленный бит CF при возврате из функции) сразу завершаем программу, чтобы не нагородить лишнего.

158: B440                      mov    ah,40
15A: 31DB                      xor    bx,bx
15C: 43                        inc    bx
15D: 89D9                      mov    cx,bx
15F: BAnnnn                    mov    dx,offset place2
162: CD21                      int    21
164: 72nn                      jb     EXIT

Записав очередной байт, нужно указать в переменной check, что следующий вводимый байт ASCII-кода будет представлять первую тетраду выводимого байта; для этого обнуляем эту переменную. Как обычно, это делается опосредованно через регистр:

166: 30C0                      xor    al,al
168: A2nnnn                    mov    [check],al
16B: EB9B                      jmp    108

В этом месте должен размещаться код для обработки случая первой тетрады (метка FIRST). Но сюда ссылается команда по адресу 150h, и если сделать ссылку на адрес 16Dh, получится инструкция '74 1B', а 1Bh у нас тоже "запрещен". Придется сослаться на следующий адрес 16Eh, а по адресу 16Dh опять поставить nop:

16D: 90                        nop
16E: B104                      mov    cl,04
170: D2E4                      shl    ah,cl
172: 8826nnnn                  mov    byte ptr [place2],ah
176: FE0Ennnn                  dec    byte ptr [check]
17A: EB8C                      jmp    108

Инструкция по смещению 176 делает отметку в переменной check, что следующий вводимый байт будет соответствовать второй тетраде выходного байта. Вообще-то, check представляет собой простую логическую переменную, и решение делается на основе проверки ее на предмет 0 - не 0. Можно было бы использовать инструкцию 'inc byte ptr [check]', но, к несчастью, при этом в коде появляется запрещенный символ 06. Приходится вместо inc использовать dec; в нашем случае это совершенно не играет роли. Вот и все - здесь конец программы, инструкция возврата и метка EXIT:

17C: C3                        ret

Программа заняла 7Dh = 125 байтов. Переменные, как мы уже говорили, можно разместить и за пределами файла, "виртуально"; т.е. можно было бы использовать адрес 17Dh. Но, как вы понимаете :), сочинение такого опуса и его подгонка заняла не один час, и для облегчения себе жизни, чтобы не пересчитывать каждый раз хотя бы адреса переменных, я отвел им места "с запасом" - для place, place2 и check соответственно 180h, 181h и 182h. Теперь можно вставить эти адреса, а также вычислить смещения и подставить их вместо 'nnnn' в инструкции. Окончательно мы получим такой код:

31 C0 A3 80 ¦ 01 A3 82 01 ¦ B4 3F 31 DB ¦ 31 C9 41 BA
80 01 CD 21 ¦ 72 66 85 C0 ¦ 74 62 8A 26 ¦ 80 01 80 FC
30 72 E5 80 ¦ FC 39 77 05 ¦ 80 EC 30 EB ¦ 1D 80 FC 41
72 D6 80 FC ¦ 46 77 05 80 ¦ EC 37 EB 0E ¦ 80 FC 61 72
C7 80 FC 66 ¦ 77 C2 80 EC ¦ 57 90 8A 1E ¦ 82 01 84 DB
74 1C F6 DC ¦ 28 26 81 01 ¦ B4 40 31 DB ¦ 43 89 D9 BA
81 01 CD 21 ¦ 72 16 30 C0 ¦ A2 82 01 EB ¦ 9B 90 B1 04
D2 E4 88 26 ¦ 81 01 FE 0E ¦ 82 01 EB 8C ¦ C3

Но это еще не все - надо все это перевести в десятичную систему, и если вы проделаете это самостоятельно, то поймете, насколько ценный инструмент мы на самом деле создаем :). Но раз уж я проделал эту работу, не стану жадничать и приведу готовый вариант:

049 192 163 128 | 001 163 130 001 | 180 063 049 219 | 049 201 065 186
128 001 205 033 | 114 102 133 192 | 116 098 138 038 | 128 001 128 252
048 114 229 128 | 252 057 119 005 | 128 236 048 235 | 029 128 252 065
114 214 128 252 | 070 119 005 128 | 236 055 235 014 | 128 252 097 114
199 128 252 102 | 119 194 128 236 | 087 144 138 030 | 130 001 132 219
116 028 246 220 | 040 038 129 001 | 180 064 049 219 | 067 137 217 186
129 001 205 033 | 114 022 048 192 | 162 130 001 235 | 155 144 177 004
210 228 136 038 | 129 001 254 014 | 130 001 235 140 | 195

Это не избавляет вас от необходимости вручную набивать числа. Если кто-то успел позабыть, как это делается ;), я напомню. В консольном окне (либо после загрузки с подготовленной дискеты) набирается команда:

copy con txt2bin.com

Затем вводятся все приведенные выше числа (десятичные), при вводе каждого числа нажимается и удерживается клавиша Alt. В случае ошибки можно просто стереть символ клавишей "Backspace" и ввести заново. Набрав все, нажимаем Ctrl+Z (или F6) и Enter - файл готов. Программа в принципе может работать и в интерактивном режиме, хотя это не очень удобно. Основное ее предназначение - использование в перенаправлениях, типа:

txt2bin < input.txt > output.bin

В качестве редактора для создания файла input.txt используется все та же внутренняя команда оболочки copy:

copy con input.txt

Только на этот раз мы набираем не Alt-коды, а нормальный текст, вернее, текстовое представление 16-ричного кода. Непременно нужно создать тестовый файл со всеми возможными 16-ричными значениями и подать его на вход нашего редактора, чтобы проверить, что все работает нормально:

00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
. . . и т.д. до
f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff

Эта программа далеко не шедевр, возможно, в ней есть большие и малые ляпусы, тем не менее, она удовлетворяет главному требованию - она работает, и работает универсально. Я проверял ее для DOS 6.22, Windows 98 SE/Me/2000 Server/XP Pro/2003 Server, а также загрузочных дискет, созданных Windows 98 и Me. Во всех случаях при наборе Alt-кодов возражений со стороны интерпретатора не поступало :), а сама полученная программа успешно конвертировала тестовый файл со всеми возможными значениями в соответствующий бинарный файл. Кстати, если вы владеете слепой печатью на правом цифровом блоке, можно поспорить, каким образом программа набирается быстрее - с помощью Alt-кодов или традиционным образом, через написание исходного текста на ассемблере с последующим построением (если брать суммарное время, боюсь, сравнение окажется не в пользу ассемблера).

Еще одно замечание по поводу возможности ошибок, трудности программирования в машинных кодах и т.д. и т.п. В ходе подготовки материала я набирал этот числовой блок в Alt-кодах раз двадцать, и ни разу не сделал ни единой ошибки. Между тем в ассемблерных проектах, даже коротких, практически всегда хоть мелкая, но найдется какая-нибудь ошибка. Так что в мире программирования гуляет под видом истин множество предрассудков; развеять их могли бы люди, имеющие собственный опыт (например, создания больших проектов прямо в машинных кодах. Но у кого такой опыт есть? Ведь это считается безумием!) Но если бы мы ограничились тестированием лишь с небольшим пробным файлом, это было бы так, забавой, не более. Кто знает, может, там прячется какая-нибудь хитрая ошибка? Поэтому нужно преобразовать в ASCII-представление какой-нибудь рабочий бинарный файл, затем конвертировать его с помощью нашей утилиты и посмотреть, будет ли исходный файл работать после такого двойного преобразования. Я приведу готовый листинг утилиты, созданной с помощью MASM32, для преобразования бинарных файлов в текстовое 16-ричное представление. Не привожу полного описания, т.к. здесь нет ничего особенного; к тому же в исходном тексте есть комментарии.

.386
.model flat,stdcall
option casemap:none
include masm32includewindows.inc
include masm32includeuser32.inc
include masm32includekernel32.inc
include masm32includecomdlg32.inc
includelib masm32libuser32.lib
includelib masm32libkernel32.lib
includelib masm32libcomdlg32.lib

.data
f	OPENFILENAME <76,0,0,offset msk,0,0,1,offset file1,256,0,0,
		offset file2,offset src,
	OFN_FILEMUSTEXIST OR OFN_HIDEREADONLY OR OFN_PATHMUSTEXIST,
		0,0,0,0,0,0>
ttl	db "Bit2txt",0
err1	db "Opening source file failed",0
err2	db "File reading failed",0
err3	db "File writing failed",0
err4	db "Opening destination file failed",0
msk	db "All files",0,"*.*",0,0
src	db "Select source file",0
dst	db "Select destination file",0
defext	db "txt",0

.data?
hf1	dword ?		; дескрипторы файлов
hf2	dword ?
read	dword ?		; число прочитанных байтов
wrtn	dword ?		; число записанных байтов
file1	db 256 dup(?)	; буфер для имени и пути файла
file2	db 256 dup(?)	; каталог файла-источника
file3	db 256 dup(?)	; каталог файла-назначения
buf	db 16 dup(?)	; буфер чтения
buf2	db 50 dup(?)	; буфер записи

.code
start:
	invoke GetWindowsDirectory,offset file2,256
	invoke GetCurrentDirectory,256,offset file3
	invoke GetOpenFileName,offset f
	.if eax==0	; Файл не выбран
		jmp stop
	.else
		invoke CreateFile,offset file1,GENERIC_READ,0,0,
 			OPEN_EXISTING,0,0
		mov hf1,eax
		.if eax==INVALID_HANDLE_VALUE
			invoke MessageBox,0,offset err1,
 				offset ttl,MB_ICONERROR
			jmp stop
		.endif
; Первый (бинарный) файл открыт - выбираем второй (для записи ASCII)
		lea eax,f
		assume eax:ptr OPENFILENAME
		mov [eax].lpstrTitle,offset dst
 		; поместить в текущий каталог
		mov [eax].lpstrInitialDir,offset file3	
 		; расширение по умолчаню - txt
		mov [eax].lpstrDefExt,offset defext	
		; флаги для чтения и записи различны
		mov [eax].Flags,OFN_OVERWRITEPROMPT OR OFN_HIDEREADONLY 
 			OR OFN_PATHMUSTEXIST OR OFN_EXTENSIONDIFFERENT
		assume eax:nothing
		invoke GetSaveFileName,offset f
		.if eax!=0	; Выбран или создан файл для записи
			invoke CreateFile,offset file1,GENERIC_WRITE,
				0,0,CREATE_ALWAYS,0,0
			mov hf2,eax
			.if eax!=INVALID_HANDLE_VALUE
			; успешно открыты оба файла; 
			; начинаем цикл чтения-записи (bin->ASCII)
m0:
	invoke ReadFile,hf1,offset buf,16,offset read,0
	; каждый раз читаем строку в 16 байтов
	.if eax==0	; ошибка чтения - выход
		invoke MessageBox,0,offset err2,offset ttl,MB_ICONERROR
		jmp m4
	.endif
	mov ebx,read	 
	cmp ebx,0	; конец файла (прочитано 0 байт) -
	je m4		; выход из цикла
	inc ebx
	shl ebx,1	; в EBX вычисляем число байтов, которые нужно будет
	add ebx,read ; записать = (число прочитанных байтов * 3 + 2)
			; (на каждый двоичный байт - 2 байта ASCII-кода +
			; 1 пробел + CRLF в конце строки)
	cld
	lea esi,buf	; буфер чтения <= 16
	lea edi,buf2	; буфер записи <= 50
	mov cl,4	; для сдвига
m1:
	lodsb
	mov ah,al
	and ah,0fh	; младшая тетрада
	cmp ah,10	; преобразуем значение в ASCII-код:
	jb m2		; если < 10, добавить лишь 30h;
	add ah,7	; если >=10, добавить 37h (A-F)
m2:	
	add ah,30h
	shr al,cl	; теперь старшая тетрада - 
	cmp al,10	; все аналогично
	jb m3
	add al,7
m3:
	add al,30h
	stosw		; сохранить сразу 2 байта (AH+AL=AX)
	mov al,20h	; добавить пробел для форматирования
	stosb
	dec read	; прочитанный буфер закончен?
	jnz m1		; нет - продолжить преобразование
	mov ax,0a0dh	; да - добавить CRLF для форматирования
	stosw		; (начинаем новую строку в 16 байтов)
	invoke WriteFile,hf2,offset buf2,ebx,offset wrtn,0
	; записать подготовленный буфер buf2
	.if eax==0 || ebx!=wrtn	; ошибка записи
		invoke MessageBox,0,offset err3,offset ttl,MB_ICONERROR
		jmp m4
	.endif
	jmp m0 ; прочитать очередные 16 (или сколько осталось) байтов
m4:
			.else	; CreateFile==INVALID_HANDLE_VALUE
	invoke MessageBox,0,offset err4,offset ttl,MB_ICONERROR
			.endif	; CreateFile
			invoke CloseHandle,hf2
		.endif	; GetSaveFileName
		invoke CloseHandle,hf1
	.endif	; GetOpenFileName
stop:
	invoke ExitProcess,0
end start

Это небольшое приложение со стандартным графическим интерфейсом, если можно так его назвать: в первом стандартном диалоговом окне открытия файла надо указать исходный бинарный файл, во втором - указать имя нового (или существующего, если он будет переписываться) файла, в который будет осуществлена запись преобразованного в текстовое шестнадцатеричное представление содержимого первого файла.

Первой "жертвой" теста, как водится, стал calc.exe. После "двойного преобразования" он работал великолепно. Но интересно ведь попробовать какой-нибудь большой файл, этак в несколько мегов. После некоторых поисков мне удалось на своей системе найти такой файл, им оказался исполняемый модуль AcrobatReader 6.0 (AcroRd32.exe) в 7,3 Мб. Преобразованный в текстовый вид, он занял 22,8 Мб. Представляете, подать такое примитивной com-программке размером в 125 байт? Даже страшно как-то - вдруг задавит своей массой! Тем не менее, я проделал и это. Конечно, пришлось подождать пару-другую минут (что ж вы хотите :)). И полученный после такого преобразования монстр запустился! Правда, отсутствовало меню и еще что-то по мелочи. Но это я отношу уже к тому, что файл был вырван из контекста сложной системы, перенесен в какой-то новый каталог, а там была куча записей в реестре и т.п. В общем, если бы была лажа с преобразованием, файл не запустился бы вообще.

Поэтому можно считать, что первый инструмент в борьбе за виртуальное выживание у нас уже есть. Я не думаю, что надо заучивать эти 125 байтов наизусть, как "Отче наш". Уловив общую суть, а главное, просмотрев этот путь и узнав, где какие подвохи попадаются, можно при необходимости его воспроизвести. А дальше уже дело собственных знаний. С помощью этого инструмента можно создавать все те вещи, которые создаются с помощью debug'а (и о которых я начал писать в статьях "Приложение Windows голыми руками" и "Dll в машинных кодах"). А это значит, что можно практически "с нуля" создавать любые нужные приложения (при необходимости используя наш принцип спецназа: используем простые утилиты для создания более сложных, а последние - для создания еще более сложных и т.д. - до выполнения боевой задачи). Фактически, это равносильно созданию этих утилит прямо в Alt-кодах. Более "голый" способ может быть разве что создание загрузочной дискеты с помощью самодельного электромагнита :). Лично я покопаюсь еще в этом направлении, уж больно интересные вещи можно здесь обнаружить.

  [C] Roustem

© 2014-2017 Сергей Воробьев

0.02