Reverse Engineering для начинающих (Понимание языка ассемблера) [Денис Юричев] (pdf) читать онлайн

-  Reverse Engineering для начинающих (Понимание языка ассемблера)  11.29 Мб (скачать pdf) (скачать pdf+fbd)  (читать)  (читать постранично) - Денис Юричев

Книга в формате pdf! Изображения и текст могут не отображаться!


Настройки текста:



____
| _ \ _____
_____ _ __ ___ ___
| |_) / _ \ \ / / _ \ '__/ __|/ _ \
| _ < __/\ V / __/ | \__ \ __/
|_| \_\___| \_/ \___|_| |___/\___|
_____
_
_
| ____|_ __
__ _(_)_ __
___ ___ _ __(_)_ __
__ _
| _| | '_ \ / _` | | '_ \ / _ \/ _ \ '__| | '_ \ / _` |
| |___| | | | (_| | | | | | __/ __/ | | | | | | (_| |
|_____|_| |_|\__, |_|_| |_|\___|\___|_| |_|_| |_|\__, |
|___/
|___/
__
/ _| ___ _ __
| |_ / _ \| '__|
| _| (_) | |
|_| \___/|_|
____
_
| __ ) ___ __ _(_)_ __ _ __
___ _ __ ___
| _ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__/ __|
| |_) | __/ (_| | | | | | | | | __/ | \__ \
|____/ \___|\__, |_|_| |_|_| |_|\___|_| |___/
|___/

i

Reverse Engineering для начинающих
(Понимание языка ассемблера)

Почему два названия? Читайте здесь: (стр. xvi).

Денис Юричев

cb a
©2013-2020, Денис Юричев.
Это произведение доступно по лицензии Creative Commons
«Attribution-ShareAlike 4.0 International» (CC BY-SA 4.0). Чтобы увидеть копию
этой лицензии, посетите
https://creativecommons.org/licenses/by-sa/4.0/.
Версия этого текста (27

ноября 2020 г.).

Самая новая версия текста (а также англоязычная версия) доступна на сайте
https://beginners.re/.

Нужны переводчики!
Возможно, вы захотите мне помочь с переводом этой работы на другие языки,
кроме английского и русского. Просто пришлите мне любой фрагмент переведенного текста (не важно, насколько короткий), и я добавлю его в исходный
код на LaTeX.
Не спрашивайте, нужно ли переводить. Просто делайте хоть что-нибудь. Я уже
перестал отвечать на емейлы вроде “что нужно сделать?”
Также, прочитайте это.
Посмотреть статистику языков можно прямо здесь: https://beginners.re/.
Скорость не важна, потому что это опен-сорсный проект все-таки. Ваше имя будет указано в числе участников проекта. Корейский, китайский и персидский
языки зарезервированы издателями. Английскую и русскую версии я делаю
сам, но английский у меня все еще ужасный, так что я буду очень признателен
за коррективы, итд. Даже мой русский несовершенный, так что я благодарен
за коррективы и русского текста!
Не стесняйтесь писать мне: .

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

Краткое оглавление
1 Образцы кода

1

2 Важные фундаментальные вещи

574

3 Более сложные примеры

604

4 Java

857

5 Поиск в коде того что нужно

910

6 Специфичное для ОС

956

7 Инструменты

1030

8 Примеры из практики

1033

9 Примеры разбора закрытых (проприетарных) форматов файлов 1189
10 Прочее

1266

11 Что стоит почитать

1288

12 Сообщества

1292

ii

Послесловие

1294

Приложение

1296

Список принятых сокращений

1335

Глоссарий

1342

Предметный указатель

1345

Оглавление
1 Образцы кода
1.1 Метод . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Некоторые базовые понятия . . . . . . . .
1.2.1 Краткое введение в CPU . . . . . . .
1.2.2 Представление чисел . . . . . . . . .
1.3 Пустая функция . . . . . . . . . . . . . . . . .
1.3.1 x86 . . . . . . . . . . . . . . . . . . . . .
1.3.2 ARM . . . . . . . . . . . . . . . . . . . . .
1.3.3 MIPS . . . . . . . . . . . . . . . . . . . . .
1.3.4 Пустые функции на практике . . .
1.4 Возврат значения . . . . . . . . . . . . . . . .
1.4.1 x86 . . . . . . . . . . . . . . . . . . . . .
1.4.2 ARM . . . . . . . . . . . . . . . . . . . . .
1.4.3 MIPS . . . . . . . . . . . . . . . . . . . . .
1.4.4 На практике . . . . . . . . . . . . . . .
1.5 Hello, world! . . . . . . . . . . . . . . . . . . . .
1.5.1 x86 . . . . . . . . . . . . . . . . . . . . .
1.5.2 x86-64 . . . . . . . . . . . . . . . . . . .
1.5.3 ARM . . . . . . . . . . . . . . . . . . . . .
1.5.4 MIPS . . . . . . . . . . . . . . . . . . . . .
1.5.5 Вывод . . . . . . . . . . . . . . . . . . .
1.5.6 Упражнения . . . . . . . . . . . . . . .
1.6 Пролог и эпилог функций . . . . . . . . . .
1.6.1 Рекурсия . . . . . . . . . . . . . . . . .
1.7 Еще кое-что о пустой ф-ции . . . . . . . .
1.8 Еще кое-что о возвращаемых значениях

iii

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

1
1
2
2
4
8
8
8
8
9
10
10
11
11
11
12
12
20
24
33
39
39
40
40
40
41

1.9 Стек . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.9.1 Почему стек растет в обратную сторону? . . . . . . . . . . . . . .
1.9.2 Для чего используется стек? . . . . . . . . . . . . . . . . . . . . . . .
1.9.3 Разметка типичного стека . . . . . . . . . . . . . . . . . . . . . . . .
1.9.4 Мусор в стеке . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.9.5 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.10 Почти пустая ф-ция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.11 printf() с несколькими аргументами . . . . . . . . . . . . . . . . . . . . .
1.11.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.11.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.11.3 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.11.4 Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.11.5 Кстати . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.12 scanf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.12.1 Простой пример . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.12.2 Классическая ошибка . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.12.3 Глобальные переменные . . . . . . . . . . . . . . . . . . . . . . . .
1.12.4 Проверка результата scanf() . . . . . . . . . . . . . . . . . . . . . .
1.12.5 Упражнение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.13 Стоит отметить: глобальные и локальные переменные . . . . . . . .
1.14 Доступ к переданным аргументам . . . . . . . . . . . . . . . . . . . . . .
1.14.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.14.2 x64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.14.3 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.14.4 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.15 Ещё о возвращаемых результатах . . . . . . . . . . . . . . . . . . . . . .
1.15.1 Попытка использовать результат функции возвращающей
void . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.15.2 Что если не использовать результат функции? . . . . . . . . .
1.15.3 Возврат структуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.16 Указатели . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.16.1 Возврат значений . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.16.2 Обменять входные значения друг с другом . . . . . . . . . . . .
1.17 Оператор GOTO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.17.1 Мертвый код . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.17.2 Упражнение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.18 Условные переходы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.18.1 Простой пример . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.18.2 Вычисление абсолютной величины . . . . . . . . . . . . . . . . .
1.18.3 Тернарный условный оператор . . . . . . . . . . . . . . . . . . . .
1.18.4 Поиск минимального и максимального значения . . . . . . . .
1.18.5 Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.18.6 Упражнение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.19 Взлом ПО . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.20 Пранк: невозможность выйти из Windows 7 . . . . . . . . . . . . . . . .
1.21 switch()/case/default . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.21.1 Если вариантов мало . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.21.2 И если много . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.21.3 Когда много case в одном блоке . . . . . . . . . . . . . . . . . . .
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

iv
41
42
43
51
51
56
56
57
57
72
80
88
89
89
89
102
103
115
129
130
130
130
133
137
141
142
143
144
145
146
146
157
158
161
162
162
162
183
186
190
196
198
198
201
201
201
217
232

1.21.4 Fall-through . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.21.5 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.22 Циклы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.22.1 Простой пример . . . . . . . . . . . . . . . . . . . . . . . . . .
1.22.2 Функция копирования блоков памяти . . . . . . . . . . .
1.22.3 Проверка условия . . . . . . . . . . . . . . . . . . . . . . . .
1.22.4 Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.22.5 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.23 Еще кое-что о строках . . . . . . . . . . . . . . . . . . . . . . . . . .
1.23.1 strlen() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.24 Замена одних арифметических инструкций на другие . . .
1.24.1 Умножение . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.24.2 Деление . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.24.3 Упражнение . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.25 Работа с FPU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.25.1 IEEE 754 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.25.2 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.25.3 ARM, MIPS, x86/x64 SIMD . . . . . . . . . . . . . . . . . . . .
1.25.4 Си/Си++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.25.5 Простой пример . . . . . . . . . . . . . . . . . . . . . . . . . .
1.25.6 Передача чисел с плавающей запятой в аргументах
1.25.7 Пример со сравнением . . . . . . . . . . . . . . . . . . . . .
1.25.8 Некоторые константы . . . . . . . . . . . . . . . . . . . . .
1.25.9 Копирование . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.25.10 Стек, калькуляторы и обратная польская запись . .
1.25.11 80 бит? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.25.12 x64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.25.13 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.26 Массивы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.26.1 Простой пример . . . . . . . . . . . . . . . . . . . . . . . . . .
1.26.2 Переполнение буфера . . . . . . . . . . . . . . . . . . . . .
1.26.3 Защита от переполнения буфера . . . . . . . . . . . . . .
1.26.4 Еще немного о массивах . . . . . . . . . . . . . . . . . . . .
1.26.5 Массив указателей на строки . . . . . . . . . . . . . . . .
1.26.6 Многомерные массивы . . . . . . . . . . . . . . . . . . . . .
1.26.7 Набор строк как двухмерный массив . . . . . . . . . . .
1.26.8 Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.26.9 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.27 Пример: ошибка в Angband . . . . . . . . . . . . . . . . . . . . . .
1.28 Работа с отдельными битами . . . . . . . . . . . . . . . . . . . . .
1.28.1 Проверка какого-либо бита . . . . . . . . . . . . . . . . . .
1.28.2 Установка и сброс отдельного бита . . . . . . . . . . . .
1.28.3 Сдвиги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.28.4 Установка и сброс отдельного бита: пример с FPU1 .
1.28.5 Подсчет выставленных бит . . . . . . . . . . . . . . . . . .
1.28.6 Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.28.7 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1 Floating-Point

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

Unit

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

v
237
240
240
240
254
257
258
261
261
261
275
275
281
282
283
283
283
283
283
284
295
298
337
337
337
337
338
338
338
338
347
355
361
361
371
381
386
386
386
389
390
395
405
405
411
429
432

1.29 Линейный конгруэнтный генератор . . . . . . . . . . . . . . . .
1.29.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.29.2 x64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.29.3 32-bit ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.29.4 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.29.5 Версия этого примера для многопоточной среды . . .
1.30 Структуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.30.1 MSVC: Пример SYSTEMTIME . . . . . . . . . . . . . . . . . .
1.30.2 Выделяем место для структуры через malloc() . . . . .
1.30.3 UNIX: struct tm . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.30.4 Упаковка полей в структуре . . . . . . . . . . . . . . . . .
1.30.5 Вложенные структуры . . . . . . . . . . . . . . . . . . . . .
1.30.6 Работа с битовыми полями в структуре . . . . . . . . .
1.30.7 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.31 Классическая ошибка с struct . . . . . . . . . . . . . . . . . . . . .
1.32 Объединения (union) . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.32.1 Пример генератора случайных чисел . . . . . . . . . . .
1.32.2 Вычисление машинного эпсилона . . . . . . . . . . . . .
1.32.3 Замена инструкции FSCALE . . . . . . . . . . . . . . . . . .
1.32.4 Быстрое вычисление квадратного корня . . . . . . . . .
1.33 Указатели на функции . . . . . . . . . . . . . . . . . . . . . . . . .
1.33.1 MSVC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.33.2 GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.33.3 Опасность указателей на ф-ции . . . . . . . . . . . . . . .
1.34 64-битные значения в 32-битной среде . . . . . . . . . . . . . .
1.34.1 Возврат 64-битного значения . . . . . . . . . . . . . . . .
1.34.2 Передача аргументов, сложение, вычитание . . . . . .
1.34.3 Умножение, деление . . . . . . . . . . . . . . . . . . . . . .
1.34.4 Сдвиг вправо . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.34.5 Конвертирование 32-битного значения в 64-битное .
1.35 Случай со структурой LARGE_INTEGER . . . . . . . . . . . . . . .
1.36 SIMD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.36.1 Векторизация . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.36.2 Реализация strlen() при помощи SIMD . . . . . . . . .
1.37 64 бита . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.37.1 x86-64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.37.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.37.3 Числа с плавающей запятой . . . . . . . . . . . . . . . . .
1.37.4 Критика 64-битной архитектуры . . . . . . . . . . . . . .
1.38 Работа с числами с плавающей запятой (SIMD) . . . . . . . . .
1.38.1 Простой пример . . . . . . . . . . . . . . . . . . . . . . . . . .
1.38.2 Передача чисел с плавающей запятой в аргументах
1.38.3 Пример со сравнением . . . . . . . . . . . . . . . . . . . . .
1.38.4 Вычисление машинного эпсилона: x64 и SIMD . . . . .
1.38.5 И снова пример генератора случайных чисел . . . . .
1.38.6 Итог . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.39 Кое-что специфичное для ARM . . . . . . . . . . . . . . . . . . . .
1.39.1 Знак номера (#) перед числом . . . . . . . . . . . . . . . .
1.39.2 Режимы адресации . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

vi
432
433
435
435
436
439
439
439
444
447
460
469
472
481
481
483
483
487
490
491
492
493
500
505
506
506
507
511
516
518
519
522
523
536
541
541
550
550
550
551
551
559
560
563
564
565
565
565
565

.
.
.
.
.

vii
567
569
571
571
573

2 Важные фундаментальные вещи
2.1 Целочисленные типы данных . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.1 Бит . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.2 Ниббл AKA nibble AKA nybble . . . . . . . . . . . . . . . . . . . . . . .
2.1.3 Байт . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.4 Wide char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.5 Знаковые целочисленные и беззнаковые . . . . . . . . . . . . . .
2.1.6 Слово (word) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.7 Регистр адреса . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.1.8 Числа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Представление знака в числах . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.1 Использование IMUL вместо MUL . . . . . . . . . . . . . . . . . . . .
2.2.2 Еще кое-что о дополнительном коде . . . . . . . . . . . . . . . . .
2.2.3 -1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3 Целочисленное переполнение (integer overflow) . . . . . . . . . . . . . .
2.4 AND . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.1 Проверка того, находится ли значение на границе 2n . . . . . .
2.4.2 Кирилличная кодировка KOI-8R . . . . . . . . . . . . . . . . . . . . .
2.5 И и ИЛИ как вычитание и сложение . . . . . . . . . . . . . . . . . . . . . .
2.5.1 Текстовые строки в ПЗУ2 ZX Spectrum . . . . . . . . . . . . . . . .
2.6 XOR (исключающее ИЛИ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.1 Логическая разница (logical difference) . . . . . . . . . . . . . . . .
2.6.2 Бытовая речь . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.3 Шифрование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.4 RAID3 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.5 Алгоритм обмена значений при помощи исключающего ИЛИ
2.6.6 Список связанный при помощи XOR . . . . . . . . . . . . . . . . . .
2.6.7 Трюк с переключением значений . . . . . . . . . . . . . . . . . . . .
2.6.8 Хэширование Зобриста / табуляционное хэширование . . . . .
2.6.9 Кстати . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6.10 AND/OR/XOR как MOV . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.7 Подсчет бит . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.8 Endianness (порядок байт) . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.8.1 Big-endian (от старшего к младшему) . . . . . . . . . . . . . . . . .
2.8.2 Little-endian (от младшего к старшему) . . . . . . . . . . . . . . . .
2.8.3 Пример . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.8.4 Bi-endian (переключаемый порядок) . . . . . . . . . . . . . . . . . .
2.8.5 Конвертирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.9 Память . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.10 CPU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.10.1 Предсказатели переходов . . . . . . . . . . . . . . . . . . . . . . . .

574
574
574
575
576
577
577
577
579
580
583
585
586
587
587
588
588
589
590
590
593
594
594
594
594
595
595
596
597
598
598
598
599
599
599
599
600
600
601
601
601

1.39.3 Загрузка констант в регистр . . . . . . . . .
1.39.4 Релоки в ARM64 . . . . . . . . . . . . . . . . . .
1.40 Кое-что специфичное для MIPS . . . . . . . . . . . .
1.40.1 Загрузка 32-битной константы в регистр
1.40.2 Книги и прочие материалы о MIPS . . . . .

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

2 Постоянное
3 Redundant

запоминающее устройство
Array of Independent Disks

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

viii
2.10.2 Зависимости между данными . . . . . . . . . . . . . . . . . . . . . 602
2.11 Хеш-функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 602
2.11.1 Как работает односторонняя функция? . . . . . . . . . . . . . . 602
3 Более сложные примеры
3.1 Двойное отрицание . . . . . . . . . . . . . . . . . . . . . .
3.2 Использование const (const correctness) . . . . . . . .
3.2.1 Пересекающиеся const-строки . . . . . . . . . .
3.3 Пример strstr() . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4 Конвертирование температуры . . . . . . . . . . . . . .
3.4.1 Целочисленные значения . . . . . . . . . . . . . .
3.4.2 Числа с плавающей запятой . . . . . . . . . . . .
3.5 Числа Фибоначчи . . . . . . . . . . . . . . . . . . . . . . . .
3.5.1 Пример #1 . . . . . . . . . . . . . . . . . . . . . . . .
3.5.2 Пример #2 . . . . . . . . . . . . . . . . . . . . . . . .
3.5.3 Итог . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.6 Пример вычисления CRC32 . . . . . . . . . . . . . . . . .
3.7 Пример вычисления адреса сети . . . . . . . . . . . . .
3.7.1 calc_network_address() . . . . . . . . . . . . . . . .
3.7.2 form_IP() . . . . . . . . . . . . . . . . . . . . . . . . . .
3.7.3 print_as_IP() . . . . . . . . . . . . . . . . . . . . . . . .
3.7.4 form_netmask() и set_bit() . . . . . . . . . . . . . .
3.7.5 Итог . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.8 Циклы: несколько итераторов . . . . . . . . . . . . . . .
3.8.1 Три итератора . . . . . . . . . . . . . . . . . . . . . .
3.8.2 Два итератора . . . . . . . . . . . . . . . . . . . . .
3.8.3 Случай Intel C++ 2011 . . . . . . . . . . . . . . . .
3.9 Duff’s device . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.9.1 Нужно ли использовать развернутые циклы?
3.10 Деление используя умножение . . . . . . . . . . . . .
3.10.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.10.2 Как это работает . . . . . . . . . . . . . . . . . . .
3.10.3 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.10.4 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.10.5 Упражнение . . . . . . . . . . . . . . . . . . . . . .
3.11 Конверсия строки в число (atoi()) . . . . . . . . . . . .
3.11.1 Простой пример . . . . . . . . . . . . . . . . . . . .
3.11.2 Немного расширенный пример . . . . . . . . .
3.11.3 Упражнение . . . . . . . . . . . . . . . . . . . . . .
3.12 Inline-функции . . . . . . . . . . . . . . . . . . . . . . . . .
3.12.1 Функции работы со строками и памятью . . .
3.13 C99 restrict . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.14 Функция abs() без переходов . . . . . . . . . . . . . . .
3.14.1 Оптимизирующий GCC 4.9.1 x64 . . . . . . . .
3.14.2 Оптимизирующий GCC 4.9 ARM64 . . . . . . . .
3.15 Функции с переменным количеством аргументов .
3.15.1 Вычисление среднего арифметического . . .
3.15.2 Случай с функцией vprintf() . . . . . . . . . . . .
3.15.3 Случай с Pin . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

604
604
605
607
608
609
609
611
615
615
619
622
623
627
629
630
632
633
634
635
635
636
638
640
644
644
644
646
646
648
649
649
649
654
657
658
659
669
672
672
673
674
674
679
681

3.15.4 Эксплуатация строки формата . . . . . . . . . . . . . . . . . . . .
3.16 Обрезка строк . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.16.1 x64: Оптимизирующий MSVC 2013 . . . . . . . . . . . . . . . . . .
3.16.2 x64: Неоптимизирующий GCC 4.9.1 . . . . . . . . . . . . . . . . .
3.16.3 x64: Оптимизирующий GCC 4.9.1 . . . . . . . . . . . . . . . . . . .
3.16.4 ARM64: Неоптимизирующий GCC (Linaro) 4.9 . . . . . . . . . . .
3.16.5 ARM64: Оптимизирующий GCC (Linaro) 4.9 . . . . . . . . . . . . .
3.16.6 ARM: Оптимизирующий Keil 6/2013 (Режим ARM) . . . . . . . .
3.16.7 ARM: Оптимизирующий Keil 6/2013 (Режим Thumb) . . . . . . .
3.16.8 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.17 Функция toupper() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.17.1 x64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.17.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.17.3 Используя битовые операции . . . . . . . . . . . . . . . . . . . . .
3.17.4 Итог . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.18 Обфускация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.18.1 Текстовые строки . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.18.2 Исполняемый код . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.18.3 Виртуальная машина / псевдо-код . . . . . . . . . . . . . . . . . .
3.18.4 Еще кое-что . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.18.5 Упражнение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.19 Си++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.19.1 Классы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.19.2 ostream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.19.3 References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.19.4 STL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.19.5 Память . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.20 Отрицательные индексы массивов . . . . . . . . . . . . . . . . . . . . . .
3.20.1 Адресация строки с конца . . . . . . . . . . . . . . . . . . . . . . . .
3.20.2 Адресация некоторого блока с конца . . . . . . . . . . . . . . . .
3.20.3 Массивы начинающиеся с 1 . . . . . . . . . . . . . . . . . . . . . . .
3.21 Больше об указателях . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.21.1 Работа с адресами вместо указателей . . . . . . . . . . . . . . .
3.21.2 Передача значений как указателей; тэггированные объединения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.21.3 Издевательство над указателями в ядре Windows . . . . . . .
3.21.4 Нулевые указатели . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.21.5 Массив как аргумент функции . . . . . . . . . . . . . . . . . . . . .
3.21.6 Указатель на функцию . . . . . . . . . . . . . . . . . . . . . . . . . .
3.21.7 Указатель на функцию: защита от копирования . . . . . . . . .
3.21.8 Указатель на ф-цию: частая ошибка (или опечатка) . . . . . .
3.21.9 Указатель как идентификатор объекта . . . . . . . . . . . . . .
3.22 Оптимизации циклов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.22.1 Странная оптимизация циклов . . . . . . . . . . . . . . . . . . . .
3.22.2 Еще одна оптимизация циклов . . . . . . . . . . . . . . . . . . . .
3.23 Еще о структурах . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.23.1 Иногда вместо массива можно использовать структуру в Си
3.23.2 Безразмерный массив в структуре Си . . . . . . . . . . . . . . . .
3.23.3 Версия структуры в Си . . . . . . . . . . . . . . . . . . . . . . . . . .
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

ix
681
683
684
686
687
689
690
691
692
693
694
695
697
699
700
700
700
701
704
704
704
704
704
726
728
729
774
775
775
776
776
779
780
783
784
790
796
797
798
799
800
801
801
803
806
806
807
809

x
3.23.4 Файл с рекордами в игре «Block out» и примитивная сериализация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.24 memmove() и memcpy() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.24.1 Анти-отладочный прием . . . . . . . . . . . . . . . . . . . . . . . . .
3.25 setjmp/longjmp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.26 Другие нездоровые хаки связанные со стеком . . . . . . . . . . . . . .
3.26.1 Доступ к аргументам и локальным переменным вызывающей ф-ции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.26.2 Возврат строки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.27 OpenMP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.27.1 MSVC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.27.2 GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.28 Еще одна heisenbug-а . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.29 Случай с забытым return . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.30 Домашнее задание: больше об указателях на ф-ции и объединениях (union) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.31 Windows 16-bit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.31.1 Пример#1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.31.2 Пример #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.31.3 Пример #3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.31.4 Пример #4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.31.5 Пример #5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.31.6 Пример #6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4 Java
4.1 Java . . . . . . . . . . . . . . . . . . . . . . . .
4.1.1 Введение . . . . . . . . . . . . . . . .
4.1.2 Возврат значения . . . . . . . . . .
4.1.3 Простая вычисляющая функция
4.1.4 Модель памяти в JVM4 . . . . . . .
4.1.5 Простой вызов функций . . . . . .
4.1.6 Вызов beep() . . . . . . . . . . . . . .
4.1.7 Линейный конгруэнтный ГПСЧ5 .
4.1.8 Условные переходы . . . . . . . . .
4.1.9 Передача аргументов . . . . . . . .
4.1.10 Битовые поля . . . . . . . . . . . .
4.1.11 Циклы . . . . . . . . . . . . . . . . .
4.1.12 switch() . . . . . . . . . . . . . . . . .
4.1.13 Массивы . . . . . . . . . . . . . . . .
4.1.14 Строки . . . . . . . . . . . . . . . . .
4.1.15 Исключения . . . . . . . . . . . . .
4.1.16 Классы . . . . . . . . . . . . . . . . .
4.1.17 Простейшая модификация . . .
4.1.18 Итоги . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

811
817
818
818
822
822
824
826
829
832
834
835
840
841
842
842
843
844
848
852
857
857
857
858
864
867
868
870
871
872
875
876
878
880
882
893
896
900
903
908

5 Поиск в коде того что нужно
910
5.1 Идентификация исполняемых файлов . . . . . . . . . . . . . . . . . . . . 911
4 Java

Virtual Machine
псевдослучайных чисел

5 Генератор

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

5.1.1 Microsoft Visual C++ . . . . . . . . . . . . . . . . . . . . . .
5.1.2 GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.3 Intel Fortran . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.4 Watcom, OpenWatcom . . . . . . . . . . . . . . . . . . . . .
5.1.5 Borland . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.6 Другие известные DLL . . . . . . . . . . . . . . . . . . . .
5.2 Связь с внешним миром (на уровне функции) . . . . . . . .
5.3 Связь с внешним миром (win32) . . . . . . . . . . . . . . . . . .
5.3.1 Часто используемые функции Windows API . . . . . .
5.3.2 Расширение триального периода . . . . . . . . . . . . .
5.3.3 Удаление nag-окна . . . . . . . . . . . . . . . . . . . . . .
5.3.4 tracer: Перехват всех функций в отдельном модуле
5.4 Строки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.4.1 Текстовые строки . . . . . . . . . . . . . . . . . . . . . . .
5.4.2 Поиск строк в бинарном файле . . . . . . . . . . . . . .
5.4.3 Сообщения об ошибках и отладочные сообщения .
5.4.4 Подозрительные магические строки . . . . . . . . . .
5.5 Вызовы assert() . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.6 Константы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.6.1 Магические числа . . . . . . . . . . . . . . . . . . . . . . .
5.6.2 Специфические константы . . . . . . . . . . . . . . . . .
5.6.3 Поиск констант . . . . . . . . . . . . . . . . . . . . . . . . .
5.7 Поиск нужных инструкций . . . . . . . . . . . . . . . . . . . . .
5.8 Подозрительные паттерны кода . . . . . . . . . . . . . . . . .
5.8.1 Инструкции XOR . . . . . . . . . . . . . . . . . . . . . . . .
5.8.2 Вручную написанный код на ассемблере . . . . . . .
5.9 Использование magic numbers для трассировки . . . . . . .
5.10 Циклы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.10.1 Некоторые паттерны в бинарных файлах . . . . . .
5.10.2 Сравнение «снимков» памяти . . . . . . . . . . . . . .
5.11 Определение ISA6 . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.11.1 Неверно дизассемблированный код . . . . . . . . . .
5.11.2 Корректно дизассемблированный код . . . . . . . .
5.12 Прочее . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.12.1 Общая идея . . . . . . . . . . . . . . . . . . . . . . . . . .
5.12.2 Порядок функций в бинарном коде . . . . . . . . . .
5.12.3 Крохотные функции . . . . . . . . . . . . . . . . . . . . .
5.12.4 Си++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.12.5 Намеренный сбой . . . . . . . . . . . . . . . . . . . . . .
6 Специфичное для ОС
6.1 Способы передачи аргументов при вызове функций
6.1.1 cdecl . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.1.2 stdcall . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.1.3 fastcall . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.1.4 thiscall . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.1.5 x86-64 . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.1.6 Возвращение переменных типа float, double .
6 Instruction

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

xi
911
912
912
912
912
914
914
914
915
916
916
916
917
917
924
925
926
926
927
928
930
931
931
933
933
933
934
935
936
945
947
947
954
954
954
954
955
955
955

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

956
956
956
956
958
959
960
964

Set Architecture (Архитектура набора команд)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

6.2
6.3

6.4

6.5

6.1.7 Модификация аргументов . . . . . . . . . . . . . . . . .
6.1.8 Указатель на аргумент функции . . . . . . . . . . . . .
Thread Local Storage . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2.1 Вернемся к линейному конгруэнтному генератору
Системные вызовы (syscall-ы) . . . . . . . . . . . . . . . . . . .
6.3.1 Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.3.2 Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.4.1 Адресно-независимый код . . . . . . . . . . . . . . . . .
6.4.2 Трюк с LD_PRELOAD в Linux . . . . . . . . . . . . . . . . .
Windows NT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.5.1 CRT (win32) . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.5.2 Win32 PE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.5.3 Windows SEH . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.5.4 Windows NT: Критические секции . . . . . . . . . . . .

7 Инструменты
7.1 Дизассемблеры . . . . . . . . . . . .
7.1.1 IDA . . . . . . . . . . . . . . . .
7.2 Отладчики . . . . . . . . . . . . . . .
7.2.1 OllyDbg . . . . . . . . . . . . . .
7.2.2 GDB . . . . . . . . . . . . . . . .
7.2.3 tracer . . . . . . . . . . . . . . .
7.3 Трассировка системных вызовов
7.4 Декомпиляторы . . . . . . . . . . . .
7.5 Прочие инструменты . . . . . . . .
7.5.1 Калькуляторы . . . . . . . . .
7.6 Чего-то здесь недостает? . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

xii
. 964
. 965
. 967
. 968
. 974
. 974
. 975
. 975
. 975
. 979
. 982
. 982
. 987
. 998
.1027

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

1030
.1030
.1030
.1030
.1030
.1030
.1030
.1031
.1031
.1032
.1032
.1032

8 Примеры из практики
8.1 Шутка с Маджонгом (Windows 7) . . . . . . . . . . .
8.2 Шутка с task manager (Windows Vista) . . . . . . . .
8.2.1 Использование LEA для загрузки значений
8.3 Шутка с игрой Color Lines . . . . . . . . . . . . . . . .
8.4 Сапёр (Windows XP) . . . . . . . . . . . . . . . . . . . .
8.4.1 Автоматический поиск массива . . . . . . . .
8.4.2 Упражнения . . . . . . . . . . . . . . . . . . . . .
8.5 Хакаем часы в Windows . . . . . . . . . . . . . . . . . .
8.6 Донглы . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.6.1 Пример #1: MacOS Classic и PowerPC . . . .
8.6.2 Пример #2: SCO OpenServer . . . . . . . . . .
8.6.3 Пример #3: MS-DOS . . . . . . . . . . . . . . . .
8.7 Случай с зашифрованной БД #1 . . . . . . . . . . . .
8.7.1 Base64 и энтропия . . . . . . . . . . . . . . . . .
8.7.2 Данные сжаты? . . . . . . . . . . . . . . . . . . .
8.7.3 Данные зашифрованы? . . . . . . . . . . . . .
8.7.4 CryptoPP . . . . . . . . . . . . . . . . . . . . . . . .
8.7.5 Режим обратной связи по шифротексту . .
8.7.6 Инициализирующий вектор . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

1033
.1034
.1036
.1040
.1042
.1045
.1052
.1053
.1053
.1063
.1063
.1073
.1086
.1093
.1093
.1096
.1096
.1097
.1100
.1103

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

8.7.7 Структура буфера . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.7.8 Шум в конце . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.7.9 Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.7.10 Post Scriptum: перебор всех IV7 . . . . . . . . . . . . . . . . . .
8.8 Разгон майнера биткоинов Cointerra . . . . . . . . . . . . . . . . . . . .
8.9 Взлом простого шифровальщика исполняемого кода . . . . . . . .
8.9.1 Еще идеи для рассмотрения . . . . . . . . . . . . . . . . . . . . .
8.10 SAP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.10.1 Касательно сжимания сетевого траффика в клиенте SAP
8.10.2 Функции проверки пароля в SAP 6.0 . . . . . . . . . . . . . . .
8.11 Oracle RDBMS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.11.1 Таблица V$VERSION в Oracle RDBMS . . . . . . . . . . . . . . . .
8.11.2 Таблица X$KSMLRU в Oracle RDBMS . . . . . . . . . . . . . . . .
8.11.3 Таблица V$TIMER в Oracle RDBMS . . . . . . . . . . . . . . . . .
8.12 Вручную написанный на ассемблере код . . . . . . . . . . . . . . . .
8.12.1 Тестовый файл EICAR . . . . . . . . . . . . . . . . . . . . . . . . .
8.13 Демо . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.13.1 10 PRINT CHR$(205.5+RND(1)); : GOTO 10 . . . . . . . . . . . .
8.13.2 Множество Мандельброта . . . . . . . . . . . . . . . . . . . . . .
8.14 Как я переписывал 100 килобайт x86-кода на чистый Си . . . . .
8.15 ”Прикуп” в игре ”Марьяж” . . . . . . . . . . . . . . . . . . . . . . . . . .
8.15.1 Упражнение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.16 Другие примеры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

xiii
.1104
.1107
.1108
.1108
.1109
.1115
.1121
.1121
.1121
.1136
.1141
.1141
.1152
.1154
.1158
.1158
.1160
.1160
.1164
.1177
.1178
.1188
.1188

9 Примеры разбора закрытых (проприетарных) форматов файлов 1189
9.1 Примитивное XOR-шифрование . . . . . . . . . . . . . . . . . . . . . . . . .1189
9.1.1 Простейшее XOR-шифрование . . . . . . . . . . . . . . . . . . . . . .1189
9.1.2 Norton Guide: простейшее однобайтное XOR-шифрование . . .1191
9.1.3 Простейшее четырехбайтное XOR-шифрование . . . . . . . . . .1195
9.1.4 Простое шифрование используя XOR-маску . . . . . . . . . . . .1199
9.1.5 Простое шифрование используя XOR-маску, второй случай . .1208
9.1.6 Домашнее задание . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1215
9.2 Информационная энтропия . . . . . . . . . . . . . . . . . . . . . . . . . . . .1215
9.2.1 Анализирование энтропии в Mathematica . . . . . . . . . . . . . .1216
9.2.2 Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1228
9.2.3 Инструменты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1228
9.2.4 Кое-что о примитивном шифровании как XOR . . . . . . . . . . .1228
9.2.5 Еще об энтропии исполняемого кода . . . . . . . . . . . . . . . . .1228
9.2.6 ГПСЧ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1229
9.2.7 Еще примеры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1229
9.2.8 Энтропия различных файлов . . . . . . . . . . . . . . . . . . . . . . .1229
9.2.9 Понижение уровня энтропии . . . . . . . . . . . . . . . . . . . . . . .1231
9.3 Файл сохранения состояния в игре Millenium . . . . . . . . . . . . . . . .1232
9.4 Файл с индексами в программе fortune . . . . . . . . . . . . . . . . . . . .1239
9.4.1 Хакинг . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1245
9.4.2 Файлы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1246
9.5 Oracle RDBMS: .SYM-файлы . . . . . . . . . . . . . . . . . . . . . . . . . . . .1247
9.6 Oracle RDBMS: .MSB-файлы . . . . . . . . . . . . . . . . . . . . . . . . . . . .1259
7 Initialization

Vector

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

xiv
9.6.1 Вывод . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1265
9.7 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1265
9.8 Дальнейшее чтение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1265
10 Прочее
10.1 Модификация исполняемых файлов . . . . . .
10.1.1 x86-код . . . . . . . . . . . . . . . . . . . . . .
10.2 Статистика количества аргументов функций
10.3 Compiler intrinsic . . . . . . . . . . . . . . . . . . . .
10.4 Аномалии компиляторов . . . . . . . . . . . . . .
10.4.1 Oracle RDBMS 11.2 and Intel C++ 10.1 .
10.4.2 MSVC 6.0 . . . . . . . . . . . . . . . . . . . . .
10.4.3 ftol2() в MSVC 2012 . . . . . . . . . . . . . .
10.4.4 Итог . . . . . . . . . . . . . . . . . . . . . . . .
10.5 Itanium . . . . . . . . . . . . . . . . . . . . . . . . . .
10.6 Модель памяти в 8086 . . . . . . . . . . . . . . . .
10.7 Перестановка basic block-ов . . . . . . . . . . . .
10.7.1 Profile-guided optimization . . . . . . . . .
10.8 Мой опыт с Hex-Rays 2.2.0 . . . . . . . . . . . . .
10.8.1 Ошибки . . . . . . . . . . . . . . . . . . . . .
10.8.2 Странности . . . . . . . . . . . . . . . . . . .
10.8.3 Безмолвие . . . . . . . . . . . . . . . . . . .
10.8.4 Запятая . . . . . . . . . . . . . . . . . . . . .
10.8.5 Типы данных . . . . . . . . . . . . . . . . . .
10.8.6 Длинные и запутанные выражения . .
10.8.7 Правила де Моргана и декомпиляция .
10.8.8 Мой план . . . . . . . . . . . . . . . . . . . .
10.8.9 Итог . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

1266
.1266
.1266
.1267
.1268
.1268
.1268
.1269
.1269
.1271
.1271
.1274
.1276
.1276
.1278
.1278
.1280
.1282
.1283
.1284
.1285
.1285
.1287
.1287

11 Что стоит почитать
11.1 Книги и прочие материалы
11.1.1 Reverse Engineering .
11.1.2 Windows . . . . . . . . .
11.1.3 Си/Си++ . . . . . . . . .
11.1.4 x86 / x86-64 . . . . . . .
11.1.5 ARM . . . . . . . . . . . .
11.1.6 Язык ассемблера . . .
11.1.7 Java . . . . . . . . . . . .
11.1.8 UNIX . . . . . . . . . . . .
11.1.9 Программирование .
11.1.10 Криптография . . . .
11.1.11 Что-то попроще . . .

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

1288
.1288
.1288
.1288
.1289
.1289
.1290
.1290
.1290
.1290
.1290
.1290
.1291

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

12 Сообщества

1292

Послесловие

1294

12.1 Вопросы? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1294

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

xv

Приложение
.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . .
.1.1 Терминология . . . . . . . . . . . . . . .
.1.2 Регистры общего пользования . . .
.1.3 Регистры FPU . . . . . . . . . . . . . . .
.1.4 Регистры SIMD . . . . . . . . . . . . . .
.1.5 Отладочные регистры . . . . . . . . .
.1.6 Инструкции . . . . . . . . . . . . . . . .
.1.7 npad . . . . . . . . . . . . . . . . . . . . .
.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . .
.2.1 Терминология . . . . . . . . . . . . . . .
.2.2 Версии . . . . . . . . . . . . . . . . . . . .
.2.3 32-битный ARM (AArch32) . . . . . . .
.2.4 64-битный ARM (AArch64) . . . . . . .
.2.5 Инструкции . . . . . . . . . . . . . . . .
.3 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . .
.3.1 Регистры . . . . . . . . . . . . . . . . . .
.3.2 Инструкции . . . . . . . . . . . . . . . .
.4 Некоторые библиотечные функции GCC .
.5 Некоторые библиотечные функции MSVC
.6 Cheatsheets . . . . . . . . . . . . . . . . . . . . .
.6.1 IDA . . . . . . . . . . . . . . . . . . . . . .
.6.2 OllyDbg . . . . . . . . . . . . . . . . . . .
.6.3 MSVC . . . . . . . . . . . . . . . . . . . . .
.6.4 GCC . . . . . . . . . . . . . . . . . . . . . .
.6.5 GDB . . . . . . . . . . . . . . . . . . . . . .

Список принятых сокращений

1296
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.1296
.1296
.1296
.1301
.1304
.1304
.1305
.1322
.1324
.1324
.1325
.1325
.1327
.1327
.1328
.1328
.1329
.1330
.1330
.1331
.1331
.1331
.1332
.1332
.1332

1335

Глоссарий

1342

Предметный указатель

1345

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

xvi

Предисловие
Почему два названия?
В 2014-2018 книга называлась “Reverse Engineering для начинающих”, но я всегда подозревал что это слишком сужает аудиторию.
Люди от инфобезопасности знают о “reverse engineering”, но я от них редко
слышу слово “ассемблер”.
Точно также, термин “reverse engineering” слишком незнакомый для общей
аудитории программистов, но они знают про “ассемблер”.
В июле 2018, для эксперимента, я заменил название на “Assembly Language for
Beginners” и запостил ссылку на сайт Hacker News8 , и книгу приняли, в общем,
хорошо.
Так что, пусть так и будет, у книги будет два названия.
Хотя, я поменял второе название на “Understanding Assembly Language” (“Понимание языка ассемблера”), потому что кто-то уже написал книгу “Assembly
Language for Beginners”. Также, люди говорят что “для начинающих” уже звучит немного саркастично для книги объемом в ~1000 страниц.
Книги отличаются только названием, именем файла (UAL-XX.pdf и RE4B-XX.pdf),
URL-ом и парой первых страниц.

О reverse engineering
У термина «reverse engineering» несколько популярных значений: 1) исследование скомпилированных программ; 2) сканирование трехмерной модели для
последующего копирования; 3) восстановление структуры СУБД.
Настоящая книга связана с первым значением.

Желательные знания перед началом чтения
Очень желательно базовое знание ЯП9 Си. Рекомендуемые материалы: 11.1.3
(стр. 1289).

Упражнения и задачи
…все перемещены на отдельный сайт: http://challenges.re.

Отзывы об этой книге
https://beginners.re/#praise.
8 https://news.ycombinator.com/item?id=17549050
9 Язык

Программирования

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

xvii

Университеты
Эта книга рекомендуется по крайне мере в этих университетах: https://beginners.
re/#uni.

Благодарности
Тем, кто много помогал мне отвечая на массу вопросов: SkullC0DEr.
Тем, кто присылал замечания об ошибках и неточностях: Александр Лысенко,
Федерико Рамондино, Марк Уилсон, Разихова Мейрамгуль Кайратовна, Анатолий Прокофьев, Костя Бегунец, Валентин “netch” Нечаев, Александр Плахов,
Артем Метла, Александр Ястребов, Влад Головкин10 , Евгений Прошин, Александр Мясников, Алексей Третьяков, Олег Песков, Zhu Ruijin, Changmin Heo,
Vitor Vidal, Stijn Crevits, Jean-Gregoire Foulon11 , Ben L., Etienne Khan, Norbert Szetei12 ,
Marc Remy, Michael Hansen, Derk Barten, The Renaissance13 , Hugo Chan, Emil
Mursalimov, Tanner Hoke, Tan90909090@GitHub, Ole Petter Orhagen, Sourav Punoriyar,
Vitor Oliveira, Alexis Ehret, Maxim Shlochiski, Greg Paton, Pierrick Lebourgeois, Abdullah
Alomair..
Просто помогали разными способами: Андрей Зубинский, Arnaud Patard (rtp
на #debian-arm IRC), noshadow на #gcc IRC, Александр Автаев, Mohsen Mostafa
Jokar, Пётр Советов, Миша “tiphareth” Вербицкий.
Переводчикам на китайский язык: Antiy Labs (antiy.cn), Archer.
Переводчику на корейский язык: Byungho Min.
Переводчику на голландский язык: Cedric Sambre (AKA Midas).
Переводчикам на испанский язык: Diego Boy, Luis Alberto Espinosa Calvo, Fernando
Guida, Diogo Mussi, Patricio Galdames, Emiliano Estevarena.
Переводчикам на португальский язык: Thales Stevan de A. Gois, Diogo Mussi, Luiz
Filipe, Primo David Santini.
Переводчикам на итальянский язык: Federico Ramondino14 , Paolo Stivanin15 , twyK,
Fabrizio Bertone, Matteo Sticco, Marco Negro16 , bluepulsar.
Переводчикам на французский язык: Florent Besnard17 , Marc Remy18 , Baudouin
Landais, Téo Dacquet19 , BlueSkeye@GitHub20 .
10 goto-vlad@github
11 https://github.com/pixjuan
12 https://github.com/73696e65
13 https://github.com/TheRenaissance
14 https://github.com/pinkrab
15 https://github.com/paolostivanin
16 https://github.com/Internaut401
17 https://github.com/besnardf
18 https://github.com/mremy
19 https://github.com/T30rix
20 https://github.com/BlueSkeye

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

xviii
Переводчикам на немецкий язык: Dennis Siekmeier21 , Julius Angres22 , Dirk Loser23 ,
Clemens Tamme, Philipp Schweinzer.
Переводчикам на польский язык: Kateryna Rozanova, Aleksander Mistewicz, Wiktoria
Lewicka, Marcin Sokołowski.
Переводчикам на японский язык: shmz@github24 ,4ryuJP@github25 .
Корректорам: Владимир Ботов, Андрей Бражук, Марк “Logxen” Купер, Yuan
Jochen Kang, Mal Malakov, Lewis Porter, Jarle Thorsen, Hong Xie.
Васил Колев26 сделал очень много исправлений и указал на многие ошибки.
И ещё всем тем на github.com кто присылал замечания и исправления.
Было использовано множество пакетов LATEX. Их авторов я также хотел бы поблагодарить.
Жертвователи
Тем, кто поддерживал меня во время написания этой книги:
2 * Oleg Vygovsky (50+100 UAH), Daniel Bilar ($50), James Truscott ($4.5), Luis
Rocha ($63), Joris van de Vis ($127), Richard S Shultz ($20), Jang Minchang ($20),
Shade Atlas (5 AUD), Yao Xiao ($10), Pawel Szczur (40 CHF), Justin Simms ($20),
Shawn the R0ck ($27), Ki Chan Ahn ($50), Triop AB (100 SEK), Ange Albertini (e10+50),
Sergey Lukianov (300 RUR), Ludvig Gislason (200 SEK), Gérard Labadie (e40), Sergey
Volchkov (10 AUD), Vankayala Vigneswararao ($50), Philippe Teuwen ($4), Martin
Haeberli ($10), Victor Cazacov (e5), Tobias Sturzenegger (10 CHF), Sonny Thai ($15),
Bayna AlZaabi ($75), Redfive B.V. (e25), Joona Oskari Heikkilä (e5), Marshall Bishop
($50), Nicolas Werner (e12), Jeremy Brown ($100), Alexandre Borges ($25), Vladimir
Dikovski (e50), Jiarui Hong (100.00 SEK), Jim Di (500 RUR), Tan Vincent ($30), Sri
Harsha Kandrakota (10 AUD), Pillay Harish (10 SGD), Timur Valiev (230 RUR), Carlos
Garcia Prado (e10), Salikov Alexander (500 RUR), Oliver Whitehouse (30 GBP), Katy
Moe ($14), Maxim Dyakonov ($3), Sebastian Aguilera (e20), Hans-Martin Münch (e15),
Jarle Thorsen (100 NOK), Vitaly Osipov ($100), Yuri Romanov (1000 RUR), Aliaksandr
Autayeu (e10), Tudor Azoitei ($40), Z0vsky (e10), Yu Dai ($10), Anonymous ($15),
Vladislav Chelnokov ($25), Nenad Noveljic ($50), Ryan Smith ($25), Andreas Schommer
(e5), Nikolay Gavrilov ($300), Ernesto Bonev Reynoso ($30).
Огромное спасибо каждому!

mini-ЧаВО
Q: Эта книга проще/легче других?
21 https://github.com/DSiekmeier
22 https://github.com/JAngres
23 https://github.com/PolymathMonkey
24 https://github.com/shmz
25 https://github.com/4ryuJP
26 https://vasil.ludost.net/

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

xix
A: Нет, примерно на таком же уровне, как и остальные книги посвященные
этой теме.
Q: Мне страшно начинать читать эту книгу, здесь более 1000 страниц. ”... для
начинающих” в названии звучит слегка саркастично.
A: Основная часть книги это масса разных листингов. И эта книга действительно для начинающих, тут многого (пока) не хватает.
Q: Что необходимо знать перед чтением книги?
A: Желательно иметь базовое понимание Си/Си++.
Q: Должен ли я изучать сразу x86/x64/ARM и MIPS? Это не многовато?
A: Для начала, вы можете читать только о x86/x64, пропуская/пролистывая части о ARM/MIPS.
Q: Возможно ли купить русскую/английскую бумажную книгу?
A: К сожалению нет, пока ни один издатель не заинтересовался в издании русской или английской версии. А пока вы можете распечатать/переплести её в
вашем любимом копи-шопе/копи-центре. https://yurichev.com/news/20200222_
printed_RE4B/.
Q: Существует ли версия epub/mobi?
A: Книга очень сильно завязана на специфические для TeX/LaTeX хаки, поэтому
преобразование в HTML (epub/mobi это набор HTML) легким не будет.
Q: Зачем в наше время нужно изучать язык ассемблера?
A: Если вы не разработчик ОС27 , вам наверное не нужно писать на ассемблере:
современные компиляторы (2010-ые) оптимизируют код намного лучше человека 28 .
К тому же, современные CPU29 это крайне сложные устройства и знание ассемблера вряд ли поможет узнать их внутренности.
Но все-таки остается по крайней мере две области, где знание ассемблера
может хорошо помочь: 1) исследование malware (зловредов) с целью анализа; 2) лучшее понимание вашего скомпилированного кода в процессе отладки.
Таким образом, эта книга предназначена для тех, кто хочет скорее понимать
ассемблер, нежели писать на нем, и вот почему здесь масса примеров, связанных с результатами работы компиляторов.
Q: Я кликнул на ссылку внутри PDF-документа, как теперь вернуться назад?
A: В Adobe Acrobat Reader нажмите сочетание Alt+LeftArrow. В Evince кликните
на “Save. Демо-версия может поставляться с отключенным пунктом меню,
но даже если кракер разрешит этот пункт, будет вызываться пустая функция,
в которой полезного кода нет.
IDA маркирует такие функции именами вроде nullsub_00, nullsub_01, итд.

1.4. Возврат значения
Еще одна простейшая функция это та, что возвращает некоторую константу:
Вот, например:
Листинг 1.8: Код на Си/Си++
int f()
{
return 123;
};

Скомпилируем её.

1.4.1. x86
И вот что делает оптимизирующий GCC:
Листинг 1.9: Оптимизирующий GCC/MSVC (вывод на ассемблере)
f:
mov
ret

eax, 123

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

11

1.4.2. ARM
А что насчет ARM?
Листинг 1.10: Оптимизирующий Keil 6/2013 (Режим ARM) ASM Output
f

PROC
MOV
BX
ENDP

r0,#0x7b ; 123
lr

ARM использует регистр R0 для возврата значений, так что здесь 123 помещается в R0.
Нужно отметить, что название инструкции MOV в x86 и ARM сбивает с толку.
На самом деле, данные не перемещаются, а скорее копируются.

1.4.3. MIPS
Вывод на ассемблере в GCC показывает регистры по номерам:
Листинг 1.11: Оптимизирующий GCC 4.4.5 (вывод на ассемблере)
j
li

$31
$2,123

# 0x7b

…а IDA— по псевдоименам:
Листинг 1.12: Оптимизирующий GCC 4.4.5 (IDA)
jr
li

$ra
$v0, 0x7B

Так что регистр $2 (или $V0) используется для возврата значений. LI это “Load
Immediate”, и это эквивалент MOV в MIPS.
Другая инструкция это инструкция перехода (J или JR), которая возвращает
управление в вызывающую ф-цию.
Но почему инструкция загрузки (LI) и инструкция перехода (J или JR) поменяны
местами? Это артефакт RISC и называется он “branch delay slot”.
На самом деле, нам не нужно вникать в эти детали. Нужно просто запомнить: в
MIPS инструкция после инструкции перехода исполняется перед инструкцией
перехода.
Таким образом, инструкция перехода всегда поменяна местами с той, которая
должна быть исполнена перед ней.

1.4.4. На практике
На практике крайне часто встречаются ф-ции, которые возвращают 1 (true)
или 0 (false).
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

12
Самые маленькие утилиты UNIX, /bin/true и /bin/false возвращают 0 и 1 соответственно, как код возврата (ноль как код возврата обычно означает успех, не
ноль означает ошибку).

1.5. Hello, world!
Продолжим, используя знаменитый пример из книги [Брайан Керниган, Деннис Ритчи, Язык программирования Си, второе издание, (1988, 2009)]:
Листинг 1.13: код на Си/Си++
#include
int main()
{
printf("hello, world\n");
return 0;
}

1.5.1. x86
MSVC
Компилируем в MSVC 2010:
cl 1.cpp /Fa1.asm

(Ключ /Fa означает сгенерировать листинг на ассемблере)
Листинг 1.14: MSVC 2010
CONST
SEGMENT
$SG3830 DB
'hello, world', 0AH, 00H
CONST
ENDS
PUBLIC _main
EXTRN
_printf:PROC
; Function compile flags: /Odtp
_TEXT
SEGMENT
_main
PROC
push
ebp
mov
ebp, esp
push
OFFSET $SG3830
call
_printf
add
esp, 4
xor
eax, eax
pop
ebp
ret
0
_main
ENDP
_TEXT
ENDS

MSVC выдает листинги в синтаксисе Intel. Разница между синтаксисом Intel и
AT&T будет рассмотрена немного позже:
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

13
Компилятор сгенерировал файл 1.obj, который впоследствии будет слинкован линкером в 1.exe. В нашем случае этот файл состоит из двух сегментов:
CONST (для данных-констант) и _TEXT (для кода).
Строка hello, world в Си/Си++имеет тип const char[][Bjarne Stroustrup, The
C++ Programming Language, 4th Edition, (2013)p176, 7.3.2], однако не имеет
имени. Но компилятору нужно как-то с ней работать, поэтому он дает ей внутреннее имя $SG3830.
Поэтому пример можно было бы переписать вот так:
#include
const char $SG3830[]="hello, world\n";
int main()
{
printf($SG3830);
return 0;
}

Вернемся к листингу на ассемблере. Как видно, строка заканчивается нулевым
байтом — это требования стандарта Си/Си++для строк. Больше о строках в
Си/Си++: 5.4.1 (стр. 917).
В сегменте кода _TEXT находится пока только одна функция: main(). Функция
main(), как и практически все функции, начинается с пролога и заканчивается
эпилогом 16 .
Далее следует вызов функции printf(): CALL _printf. Перед этим вызовом
адрес строки (или указатель на неё) с нашим приветствием (“Hello, world!”)
при помощи инструкции PUSH помещается в стек.
После того, как функция printf() возвращает управление в функцию main(),
адрес строки (или указатель на неё) всё ещё лежит в стеке. Так как он больше
не нужен, то указатель стека (регистр ESP) корректируется.
ADD ESP, 4 означает прибавить 4 к значению в регистре ESP.
Почему 4? Так как это 32-битный код, для передачи адреса нужно 4 байта. В
x64-коде это 8 байт.
ADD ESP, 4 эквивалентно POP регистр, но без использования какого-либо регистра17 .
Некоторые компиляторы, например, Intel C++ Compiler, в этой же ситуации могут вместо ADD сгенерировать POP ECX (подобное можно встретить, например,
в коде Oracle RDBMS, им скомпилированном), что почти то же самое, только
портится значение в регистре ECX. Возможно, компилятор применяет POP ECX,
потому что эта инструкция короче (1 байт у POP против 3 у ADD).
Вот пример использования POP вместо ADD из Oracle RDBMS:
16 Об

этом смотрите подробнее в разделе о прологе и эпилоге функции (1.6 (стр. 40)).
процессора, впрочем, модифицируются

17 Флаги

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

14
Листинг 1.15: Oracle RDBMS 10.2 Linux (файл app.o)
.text:0800029A
.text:0800029B
.text:080002A0

push
call
pop

ebx
qksfroChild
ecx

Впрочем, MSVC был замечен в подобном же.
Листинг 1.16: MineSweeper из Windows 7 32-bit
.text:0102106F
.text:01021071
.text:01021077

push
call
pop

0
ds:time
ecx

После вызова printf() в оригинальном коде на Си/Си++указано return 0 —
вернуть 0 в качестве результата функции main().
В сгенерированном коде это обеспечивается инструкцией
XOR EAX, EAX.
XOR, как легко догадаться — «исключающее ИЛИ»18 , но компиляторы часто
используют его вместо простого MOV EAX, 0 — снова потому, что опкод короче
(2 байта у XOR против 5 у MOV).
Некоторые компиляторы генерируют SUB EAX, EAX, что значит отнять значение в EAX от значения в EAX, что в любом случае даст 0 в результате.
Самая последняя инструкция RET возвращает управление в вызывающую функцию. Обычно это код Си/Си++CRT19 , который, в свою очередь, вернёт управление в ОС.
GCC
Теперь скомпилируем то же самое компилятором GCC 4.4.1 в Linux: gcc 1.c
-o 1. Затем при помощи IDA посмотрим как скомпилировалась функция main().
IDA, как и MSVC, показывает код в синтаксисе Intel20 .
Листинг 1.17: код в IDA
main

proc near

var_10

= dword ptr −10h
push
mov
and
sub
mov
mov
call

ebp
ebp, esp
esp, 0FFFFFFF0h
esp, 10h
eax, offset aHelloWorld ; "hello, world\n"
[esp+10h+var_10], eax
_printf

18 wikipedia
19 C

Runtime library
также можем заставить GCC генерировать листинги в этом формате при помощи ключей
-S -masm=intel.
20 Мы

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

15
mov
leave
retn
endp

main

eax, 0

Почти то же самое. Адрес строки hello, world, лежащей в сегменте данных,
вначале сохраняется в EAX, затем записывается в стек. А ещё в прологе функции мы видим AND ESP, 0FFFFFFF0h — эта инструкция выравнивает значение
в ESP по 16-байтной границе, делая все значения в стеке также выровненными по этой границе (процессор более эффективно работает с переменными,
расположенными в памяти по адресам кратным 4 или 16).
SUB ESP, 10h выделяет в стеке 16 байт. Хотя, как будет видно далее, здесь
достаточно только 4.
Это происходит потому, что количество выделяемого места в локальном стеке
тоже выровнено по 16-байтной границе.
Адрес строки (или указатель на строку) затем записывается прямо в стек без
помощи инструкции PUSH. var_10 одновременно и локальная переменная и аргумент для printf(). Подробнее об этом будет ниже.
Затем вызывается printf().
В отличие от MSVC, GCC в компиляции без включенной оптимизации генерирует MOV EAX, 0 вместо более короткого опкода.
Последняя инструкция LEAVE — это аналог команд MOV ESP, EBP и POP EBP —
то есть возврат указателя стека и регистра EBP в первоначальное состояние.
Это необходимо, т.к. в начале функции мы модифицировали регистры ESP и
EBP
(при помощи MOV EBP, ESP / AND ESP, …).
GCC: Синтаксис AT&T
Попробуем посмотреть, как выглядит то же самое в синтаксисе AT&T языка
ассемблера. Этот синтаксис больше распространен в UNIX-мире.
Листинг 1.18: компилируем в GCC 4.7.3
gcc −S 1_1.c

Получим такой файл:
Листинг 1.19: GCC 4.7.3
.file
"1_1.c"
.section
.rodata
.LC0:
.string "hello, world\n"
.text
.globl main
.type
main, @function
main:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

16
.LFB0:
.cfi_startproc
pushl
%ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, −8
movl
%esp, %ebp
.cfi_def_cfa_register 5
andl
$−16, %esp
subl
$16, %esp
movl
$.LC0, (%esp)
call
printf
movl
$0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size
main, .−main
.ident "GCC: (Ubuntu/Linaro 4.7.3−1ubuntu1) 4.7.3"
.section
.note.GNU−stack,"",@progbits

Здесь много макросов (начинающихся с точки). Они нам пока не интересны.
Пока что, ради упрощения, мы можем их игнорировать (кроме макроса .string,
при помощи которого кодируется последовательность символов, оканчивающихся нулем — такие же строки как в Си). И тогда получится следующее 21 :
Листинг 1.20: GCC 4.7.3
.LC0:
.string "hello, world\n"
main:
pushl
movl
andl
subl
movl
call
movl
leave
ret

%ebp
%esp, %ebp
$−16, %esp
$16, %esp
$.LC0, (%esp)
printf
$0, %eax

Основные отличия синтаксиса Intel и AT&T следующие:
• Операнды записываются наоборот.
В Intel-синтаксисе:
.
В AT&T-синтаксисе:
.
21 Кстати, для уменьшения генерации «лишних» макросов, можно использовать такой ключ GCC:
-fno-asynchronous-unwind-tables

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

17
Чтобы легче понимать разницу, можно запомнить следующее: когда вы
работаете с синтаксисом Intel — можете в уме ставить знак равенства
(=) между операндами, а когда с синтаксисом AT&T — мысленно ставьте
стрелку направо (→) 22 .
• AT&T: Перед именами регистров ставится символ процента (%), а перед
числами символ доллара ($). Вместо квадратных скобок используются круглые.
• AT&T: К каждой инструкции добавляется специальный символ, определяющий тип данных:
– q — quad (64 бита)
– l — long (32 бита)
– w — word (16 бит)
– b — byte (8 бит)
Возвращаясь к результату компиляции: он идентичен тому, который мы посмотрели в IDA. Одна мелочь: 0FFFFFFF0h записывается как $-16. Это то же
самое: 16 в десятичной системе это 0x10 в шестнадцатеричной. -0x10 будет
как раз 0xFFFFFFF0 (в рамках 32-битных чисел).
Возвращаемый результат устанавливается в 0 обычной инструкцией MOV, а не
XOR. MOV просто загружает значение в регистр. Её название не очень удачное
(данные не перемещаются, а копируются). В других архитектурах подобная
инструкция обычно носит название «LOAD» или «STORE» или что-то в этом роде.
Коррекция (патчинг) строки (Win32)
Мы можем легко найти строку “hello, world” в исполняемом файле при помощи
Hiew:

Рис. 1.1: Hiew
Можем перевести наше сообщение на испанский язык:
22 Кстати, в некоторых стандартных функциях библиотеки Си (например, memcpy(), strcpy()) также применяется расстановка аргументов как в синтаксисе Intel: вначале указатель в памяти на
блок назначения, затем указатель на блок-источник.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

18

Рис. 1.2: Hiew
Испанский текст на 1 байт короче английского, так что добавляем в конце байт
0x0A (\n) и нулевой байт.
Работает.
Что если мы хотим вставить более длинное сообщение? После оригинального
текста на английском есть какие-то нулевые байты. Трудно сказать, можно ли
их перезаписывать: они могут где-то использоваться в CRT-коде, а может и нет.
Так или иначе, вы можете их перезаписывать, только если вы действительно
знаете, что делаете.
Коррекция строки (Linux x64)
Попробуем пропатчить исполняемый файл для Linux x64 используя rada.re:
Листинг 1.21: Сессия в rada.re
dennis@bigbox ~/tmp % gcc hw.c
dennis@bigbox ~/tmp % radare2 a.out
−− SHALL WE PLAY A GAME?
[0x00400430]> / hello
Searching 5 bytes from 0x00400000 to 0x00601040: 68 65 6c 6c 6f
Searching 5 bytes in [0x400000−0x601040]
hits: 1
0x004005c4 hit0_0 .HHhello, world;0.
[0x00400430]> s 0x004005c4
[0x004005c4]> px
− offset −
0 1
0x004005c4 6865
0x004005d4 011b
0x004005e4 7c00
0x004005f4 a400
0x00400604 0c01
0x00400614 0178
0x00400624 1c00
0x00400634 0000

2 3
6c6c
033b
0000
0000
0000
1001
0000
0000

4 5
6f2c
3000
5cfe
6cff
1400
1b0c
08fe
1400

6 7
2077
0000
ffff
ffff
0000
0708
ffff
0000

8 9
6f72
0500
4c00
c400
0000
9001
2a00
0000

A B
6c64
0000
0000
0000
0000
0710
0000
0000

C D
0000
1cfe
52ff
dcff
017a
1400
0000
017a

E F
0000
ffff
ffff
ffff
5200
0000
0000
5200

0123456789ABCDEF
hello, world....
...;0...........
|...\...L...R...
....l...........
.............zR.
.x..............
........∗.......
.............zR.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

19
0x00400644
0x00400654
0x00400664
0x00400674
0x00400684
0x00400694
0x004006a4
0x004006b4

0178
1c00
0e18
0000
1500
0800
6500
0e20

1001
0000
4a0f
0000
0000
0000
0000
8d04

1b0c
98fd
0b77
1c00
0041
4400
0042
420e

0708
ffff
0880
0000
0e10
0000
0e10
288c

9001
3000
003f
4400
8602
6400
8f02
0548

0000
0000
1a3b
0000
430d
0000
420e
0e30

2400
000e
2a33
a6fe
0650
a0fe
188e
8606

0000
1046
2422
ffff
0c07
ffff
0345
480e

.x..........$...
........0......F
..J..w...?.;∗3$"
........D.......
.....A....C..P..
....D...d.......
e....B....B....E
. ..B.(..H.0..H.

[0x004005c4]> oo+
File a.out reopened in read−write mode
[0x004005c4]> w hola, mundo\x00
[0x004005c4]> q
dennis@bigbox ~/tmp % ./a.out
hola, mundo

Что я здесь делаю: ищу строку «hello» используя команду /, я затем я выставляю курсор (seek в терминах rada.re) на этот адрес. Потом я хочу удостовериться, что это действительно нужное место: px выводит байты по этому адресу.
oo+ переключает rada.re в режим чтения-записи. w записывает ASCII-строку на
месте курсора (seek). Нужно отметить \00 в конце — это нулевой байт. q заканчивает работу.
Это реальная история взлома ПО
Некое ПО обрабатывало изображения, и когда не было зарегистрированно, оно
добавляло водяные знаки, вроде “This image was processed by evaluation version
of [software name]”, поперек картинки. Мы попробовали от балды: нашли эту
строку в исполняемом файле и забили пробелами. Водяные знаки пропали. Технически, они продолжали добавляться. При помощи соответствующих ф-ций
Qt, надпись продолжала добавляться в итоговое изображение. Но добавление
пробелов не меняло само изображение...
Локализация ПО во времена MS-DOS
Описанный способ был очень распространен для перевода ПО под MS-DOS на
русский язык в 1980-е и 1990-е. Эта техника доступна даже для тех, кто вовсе не разбирается в машинном коде и форматах исполняемых файлов. Новая
строка не должна быть длиннее старой, потому что имеется риск затереть
какую-то другую переменную или код. Русские слова и предложения обычно
немного длиннее английских, так что локализованное ПО содержало массу
странных акронимов и труднопонятных сокращений.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

20

Рис. 1.3: Русифицированный Norton Commander 5.51
Вероятно, так было и с другими языками в других странах.
В строках в Delphi, длина строки также должна быть поправлена, если нужно.

1.5.2. x86-64
MSVC: x86-64
Попробуем также 64-битный MSVC:
Листинг 1.22: MSVC 2012 x64
$SG2989 DB
main

main

PROC
sub
lea
call
xor
add
ret
ENDP

'hello, world', 0AH, 00H

rsp, 40
rcx, OFFSET FLAT:$SG2989
printf
eax, eax
rsp, 40
0

В x86-64 все регистры были расширены до 64-х бит и теперь имеют префикс R-.
Чтобы поменьше задействовать стек (иными словами, поменьше обращаться к
кэшу и внешней памяти), уже давно имелся довольно популярный метод передачи аргументов функции через регистры (fastcall) 6.1.3 (стр. 958). Т.е. часть
аргументов функции передается через регистры и часть —через стек. В Win64
первые 4 аргумента функции передаются через регистры RCX, RDX, R8, R9. Это
мы здесь и видим: указатель на строку в printf() теперь передается не через
стек, а через регистр RCX. Указатели теперь 64-битные, так что они передаются через 64-битные части регистров (имеющие префикс R-). Но для обратной
совместимости можно обращаться и к нижним 32 битам регистров используя
префикс E-. Вот как выглядит регистр RAX/EAX/AX/AL в x86-64:
7-й

6-й

Номер байта:
5-й 4-й 3-й 2-й 1-й 0-й
RAXx64
EAX
AX
AH AL

Функция main() возвращает значение типа int, который в Си/Си++, надо полагать, для лучшей совместимости и переносимости, оставили 32-битным. Вот
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

21
почему в конце функции main() обнуляется не RAX, а EAX, т.е. 32-битная часть
регистра. Также видно, что 40 байт выделяются в локальном стеке. Это «shadow
space» которое мы будем рассматривать позже: 1.14.2 (стр. 135).
GCC: x86-64
Попробуем GCC в 64-битном Linux:
Листинг 1.23: GCC 4.4.6 x64
.string "hello,
main:
sub
mov
xor
call
xor
add
ret

world\n"
rsp, 8
edi, OFFSET FLAT:.LC0 ; "hello, world\n"
eax, eax ; количество переданных векторных регистров
printf
eax, eax
rsp, 8

В Linux, *BSD и Mac OS X для x86-64 также принят способ передачи аргументов
функции через регистры [Michael Matz, Jan Hubicka, Andreas Jaeger, Mark Mitchell,
System V Application Binary Interface. AMD64 Architecture Processor Supplement,
(2013)] 23 .
6 первых аргументов передаются через регистры RDI, RSI, RDX, RCX, R8, R9, а
остальные — через стек.
Так что указатель на строку передается через EDI (32-битную часть регистра).
Но почему не через 64-битную часть, RDI?
Важно запомнить, что в 64-битном режиме все инструкции MOV, записывающие
что-либо в младшую 32-битную часть регистра, обнуляют старшие 32-бита (это
можно найти в документации от Intel: 11.1.4 (стр. 1289)). То есть, инструкция
MOV EAX, 011223344h корректно запишет это значение в RAX, старшие биты
сбросятся в ноль.
Если посмотреть в IDA скомпилированный объектный файл (.o), увидим также
опкоды всех инструкций 24 :
Листинг 1.24: GCC 4.4.6 x64
.text:00000000004004D0
.text:00000000004004D0
.text:00000000004004D4
world\n"
.text:00000000004004D9
.text:00000000004004DB
.text:00000000004004E0
.text:00000000004004E2
.text:00000000004004E6
.text:00000000004004E6

48 83 EC 08
BF E8 05 40 00
31
E8
31
48
C3

C0
D8 FE FF FF
C0
83 C4 08

main
sub
mov
xor
call
xor
add
retn
main

proc near
rsp, 8
edi, offset format ; "hello,
eax, eax
_printf
eax, eax
rsp, 8
endp

23 Также доступно здесь: https://software.intel.com/sites/default/files/article/402129/
mpx-linux64-abi.pdf
24 Это нужно задать в Options → Disassembly → Number of opcode bytes

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

22
Как видно, инструкция, записывающая в EDI по адресу 0x4004D4, занимает 5
байт. Та же инструкция, записывающая 64-битное значение в RDI, занимает 7
байт. Возможно, GCC решил немного сэкономить. К тому же, вероятно, он уверен, что сегмент данных, где хранится строка, никогда не будет расположен
в адресах выше 4GiB.
Здесь мы также видим обнуление регистра EAX перед вызовом printf(). Это
делается потому что по упомянутому выше стандарту передачи аргументов
в *NIX для x86-64 в EAX передается количество задействованных векторных
регистров.
Коррекция (патчинг) адреса (Win64)
Если наш пример скомпилирован в MSVC 2013 используя опцию /MD (подразумевая меньший исполняемый файл из-за внешнего связывания файла MSVCR*.DLL),
ф-ция main() идет первой, и её легко найти:

Рис. 1.4: Hiew
В качестве эксперимента, мы можем инкрементировать адрес на 1:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

23

Рис. 1.5: Hiew
Hiew показывает строку «ello, world». И когда мы запускаем исполняемый файл,
именно эта строка и выводится.
Выбор другой строки из исполняемого файла (Linux x64)
Исполняемый файл, если скомпилировать используя GCC 5.4.0 на Linux x64,
имеет множество других строк: в основном, это имена импортированных фций и имена библиотек.
Запускаю objdump, чтобы посмотреть содержимое всех секций скомпилированного файла:
% objdump −s a.out
a.out:

file format elf64−x86−64

Contents of section .interp:
400238 2f6c6962 36342f6c 642d6c69 6e75782d
400248 7838362d 36342e73 6f2e3200
Contents of section .note.ABI−tag:
400254 04000000 10000000 01000000 474e5500
400264 00000000 02000000 06000000 20000000
Contents of section .note.gnu.build−id:

/lib64/ld−linux−
x86−64.so.2.
............GNU.
............ ...

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

24
400274 04000000 14000000 03000000 474e5500
400284 fe461178 5bb710b4 bbf2aca8 5ec1ec10
400294 cf3f7ae4

............GNU.
.F.x[.......^...
.?z.

...

Не проблема передать адрес текстовой строки «/lib64/ld-linux-x86-64.so.2» в
вызов printf():
#include
int main()
{
printf(0x400238);
return 0;
}

Трудно поверить, но этот код печатает вышеуказанную строку.
Измените адрес на 0x400260, и напечатается строка «GNU». Адрес точен для
конкретной версии GCC, GNU toolset, итд. На вашей системе, исполняемый
файл может быть немного другой, и все адреса тоже будут другими. Также,
добавление/удаление кода из исходных кодов, скорее всего, сдвинет все адреса вперед или назад.

1.5.3. ARM
Для экспериментов с процессором ARM было использовано несколько компиляторов:
• Популярный в embedded-среде Keil Release 6/2013.
• Apple Xcode 4.6.3 с компилятором LLVM-GCC 4.2

25

.

• GCC 4.9 (Linaro) (для ARM64), доступный в виде исполняемого файла для
win32 на http://www.linaro.org/projects/armv8/.
Везде в этой книге, если не указано иное, идет речь о 32-битном ARM (включая
режимы Thumb и Thumb-2). Когда речь идет о 64-битном ARM, он называется
здесь ARM64.
Неоптимизирующий Keil 6/2013 (Режим ARM)
Для начала скомпилируем наш пример в Keil:
armcc.exe −−arm −−c90 −O0 1.c

Компилятор armcc генерирует листинг на ассемблере в формате Intel. Этот листинг содержит некоторые высокоуровневые макросы, связанные с ARM 26 , а
25 Это действительно так: Apple Xcode 4.6.3 использует опен-сорсный GCC как компилятор переднего плана и кодогенератор LLVM
26 например, он показывает инструкции PUSH/POP, отсутствующие в режиме ARM

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

25
нам важнее увидеть инструкции «как есть», так что посмотрим скомпилированный результат в IDA.
Листинг 1.25: Неоптимизирующий Keil 6/2013 (Режим ARM) IDA
.text:00000000
.text:00000000
.text:00000004
.text:00000008
.text:0000000C
.text:00000010

10
1E
15
00
10

40
0E
19
00
80

2D
8F
00
A0
BD

E9
E2
EB
E3
E8

main
STMFD
ADR
BL
MOV
LDMFD

.text:000001EC 68 65 6C 6C+aHelloWorld
main+4

SP!, {R4,LR}
R0, aHelloWorld ; "hello, world"
__2printf
R0, #0
SP!, {R4,PC}
DCB "hello, world",0

; DATA XREF:

В вышеприведённом примере можно легко увидеть, что каждая инструкция
имеет размер 4 байта. Действительно, ведь мы же компилировали наш код
для режима ARM, а не Thumb.
Самая первая инструкция, STMFD SP!, {R4,LR}27 , работает как инструкция PUSH
в x86, записывая значения двух регистров (R4 и LR) в стек. Действительно, в
выдаваемом листинге на ассемблере компилятор armcc для упрощения указывает здесь инструкцию PUSH {r4,lr}. Но это не совсем точно, инструкция
PUSH доступна только в режиме Thumb, поэтому, во избежание путаницы, я
предложил работать в IDA.
Итак, эта инструкция уменьшает SP29 , чтобы он указывал на место в стеке,
свободное для записи новых значений, затем записывает значения регистров
R4 и LR по адресу в памяти, на который указывает измененный регистр SP.
Эта инструкция, как и инструкция PUSH в режиме Thumb, может сохранить
в стеке одновременно несколько значений регистров, что может быть очень
удобно. Кстати, такого в x86 нет. Также следует заметить, что STMFD — генерализация инструкции PUSH (то есть расширяет её возможности), потому что
может работать с любым регистром, а не только с SP. Другими словами, STMFD
можно использовать для записи набора регистров в указанном месте памяти.
Инструкция ADR R0, aHelloWorld прибавляет или отнимает значение регистра
PC30 к смещению, где хранится строка hello, world. Причем здесь PC, можно
спросить? Притом, что это так называемый «адресно-независимый код» 31 . Он
предназначен для исполнения будучи не привязанным к каким-либо адресам
в памяти. Другими словами, это относительная от PC адресация. В опкоде инструкции ADR указывается разница между адресом этой инструкции и местом,
где хранится строка. Эта разница всегда будет постоянной, вне зависимости
от того, куда был загружен ОС наш код. Поэтому всё, что нужно — это прибавить адрес текущей инструкции (из PC), чтобы получить текущий абсолютный
адрес нашей Си-строки.
27 STMFD28
29 указатель

стека. SP/ESP/RSP в x86/x64. SP в ARM.
Counter. IP/EIP/RIP в x86/64. PC в ARM.
31 Читайте больше об этом в соответствующем разделе (6.4.1 (стр. 975))

30 Program

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

26
Инструкция BL __2printf32 вызывает функцию printf(). Работа этой инструкции состоит из двух фаз:
• записать адрес после инструкции BL (0xC) в регистр LR;
• передать управление в printf(), записав адрес этой функции в регистр
PC.
Ведь когда функция printf() закончит работу, нужно знать, куда вернуть
управление, поэтому закончив работу, всякая функция передает управление
по адресу, записанному в регистре LR.
В этом разница между «чистыми» RISC-процессорами вроде ARM и CISC33 -процессорами
как x86, где адрес возврата обычно записывается в стек (1.9 (стр. 41)).
Кстати, 32-битный абсолютный адрес (либо смещение) невозможно закодировать в 32-битной инструкции BL, в ней есть место только для 24-х бит. Поскольку все инструкции в режиме ARM имеют длину 4 байта (32 бита) и инструкции
могут находится только по адресам кратным 4, то последние 2 бита (всегда
нулевых) можно не кодировать. В итоге имеем 26 бит, при помощи которых
можно закодировать current_P C ± ≈ 32M .
Следующая инструкция MOV R0, #034 просто записывает 0 в регистр R0. Ведь
наша Си-функция возвращает 0, а возвращаемое значение всякая функция
оставляет в R0.
Последняя инструкция LDMFD SP!, R4,PC35 . Она загружает из стека (или любого другого места в памяти) значения для сохранения их в R4 и PC, увеличивая
указатель стека SP. Здесь она работает как аналог POP.
N.B. Самая первая инструкция STMFD сохранила в стеке R4 и LR, а восстанавливаются во время исполнения LDMFD регистры R4 и PC.
Как мы уже знаем, в регистре LR обычно сохраняется адрес места, куда нужно
всякой функции вернуть управление. Самая первая инструкция сохраняет это
значение в стеке, потому что наша функция main() позже будет сама пользоваться этим регистром в момент вызова printf(). А затем, в конце функции,
это значение можно сразу записать прямо в PC, таким образом, передав управление туда, откуда была вызвана наша функция.
Так как функция main() обычно самая главная в Си/Си++, управление будет
возвращено в загрузчик ОС, либо куда-то в CRT или что-то в этом роде.
Всё это позволяет избавиться от инструкции BX LR в самом конце функции.
DCB — директива ассемблера, описывающая массивы байт или ASCII-строк, аналог директивы DB в x86-ассемблере.
Неоптимизирующий Keil 6/2013 (Режим Thumb)
Скомпилируем тот же пример в Keil для режима Thumb:
32 Branch

with Link
Instruction Set Computing
34 Означает MOVe
35 LDMFD36 — это инструкция, обратная STMFD
33 Complex

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

27
armcc.exe −−thumb −−c90 −O0 1.c

Получим (в IDA):
Листинг 1.26: Неоптимизирующий Keil 6/2013 (Режим Thumb) + IDA
.text:00000000
.text:00000000
.text:00000002
.text:00000004
.text:00000008
.text:0000000A

10
C0
06
00
10

B5
A0
F0 2E F9
20
BD

main
PUSH
ADR
BL
MOVS
POP

{R4,LR}
R0, aHelloWorld ; "hello, world"
__2printf
R0, #0
{R4,PC}

.text:00000304 68 65 6C 6C+aHelloWorld
main+2

DCB "hello, world",0

; DATA XREF:

Сразу бросаются в глаза двухбайтные (16-битные) опкоды — это, как уже было
отмечено, Thumb.
Кроме инструкции BL. Но на самом деле она состоит из двух 16-битных инструкций. Это потому что в одном 16-битном опкоде слишком мало места для
задания смещения, по которому находится функция printf(). Так что первая
16-битная инструкция загружает старшие 10 бит смещения, а вторая — младшие 11 бит смещения.
Как уже было упомянуто, все инструкции в Thumb-режиме имеют длину 2 байта (или 16 бит). Поэтому невозможна такая ситуация, когда Thumb-инструкция
начинается по нечетному адресу.
Учитывая сказанное, последний бит адреса можно не кодировать. Таким образом, в Thumb-инструкции BL можно закодировать адрес current_P C ± ≈ 2M .
Остальные инструкции в функции (PUSH и POP) здесь работают почти так же,
как и описанные STMFD/LDMFD, только регистр SP здесь не указывается явно.
ADR работает так же, как и в предыдущем примере. MOVS записывает 0 в регистр R0 для возврата нуля.
Оптимизирующий Xcode 4.6.3 (LLVM) (Режим ARM)
Xcode 4.6.3 без включенной оптимизации выдает слишком много лишнего кода, поэтому включим оптимизацию компилятора (ключ -O3), потому что там
меньше инструкций.
Листинг 1.27: Оптимизирующий Xcode 4.6.3 (LLVM) (Режим ARM)
__text:000028C4
__text:000028C4
__text:000028C8
__text:000028CC
__text:000028D0
__text:000028D4
__text:000028D8
__text:000028DC
__text:000028E0

80
86
0D
00
00
C3
00
80

40
06
70
00
00
05
00
80

2D
01
A0
40
8F
00
A0
BD

E9
E3
E1
E3
E0
EB
E3
E8

_hello_world
STMFD
MOV
MOV
MOVT
ADD
BL
MOV
LDMFD

SP!, {R7,LR}
R0, #0x1686
R7, SP
R0, #0
R0, PC, R0
_puts
R0, #0
SP!, {R7,PC}

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

28
__cstring:00003F62 48 65 6C 6C+aHelloWorld_0

DCB "Hello world!",0

Инструкции STMFD и LDMFD нам уже знакомы.
Инструкция MOV просто записывает число 0x1686 в регистр R0 — это смещение,
указывающее на строку «Hello world!».
Регистр R7 (по стандарту, принятому в [iOS ABI Function Call Guide, (2010)]37 )
это frame pointer, о нем будет рассказано позже.
Инструкция MOVT R0, #0 (MOVe Top) записывает 0 в старшие 16 бит регистра.
Дело в том, что обычная инструкция MOV в режиме ARM может записывать
какое-либо значение только в младшие 16 бит регистра, ведь в ней нельзя
закодировать больше. Помните, что в режиме ARM опкоды всех инструкций
ограничены длиной в 32 бита. Конечно, это ограничение не касается перемещений данных между регистрами.
Поэтому для записи в старшие биты (с 16-го по 31-й включительно) существует
дополнительная команда MOVT. Впрочем, здесь её использование избыточно,
потому что инструкция MOV R0, #0x1686 выше и так обнулила старшую часть
регистра. Возможно, это недочет компилятора.
Инструкция ADD R0, PC, R0 прибавляет PC к R0 для вычисления действительного адреса строки «Hello world!». Как нам уже известно, это «адресно-независимый
код», поэтому такая корректива необходима.
Инструкция BL вызывает puts() вместо printf().
LLVM заменил вызов printf() на puts(). Действительно, printf() с одним аргументом это почти аналог puts().
Почти, если принять условие, что в строке не будет управляющих символов
printf(), начинающихся со знака процента. Тогда эффект от работы этих двух
функций будет разным 38 .
Зачем компилятор заменил один вызов на другой? Наверное потому что puts()
работает быстрее 39 . Видимо потому что puts() проталкивает символы в stdout
не сравнивая каждый со знаком процента.
Далее уже знакомая инструкция MOV R0, #0, служащая для установки в 0 возвращаемого значения функции.
Оптимизирующий Xcode 4.6.3 (LLVM) (Режим Thumb-2)
По умолчанию Xcode 4.6.3 генерирует код для режима Thumb-2 примерно в
такой манере:
37 Также
доступно здесь: http://developer.apple.com/library/ios/documentation/Xcode/
Conceptual/iPhoneOSABIReference/iPhoneOSABIReference.pdf
38 Также нужно заметить, что puts() не требует символа перевода строки ‘\n’ в конце строки,
поэтому его здесь нет.
39 ciselant.de/projects/gcc_printf/gcc_printf.html

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

29
Листинг 1.28: Оптимизирующий Xcode 4.6.3 (LLVM) (Режим Thumb-2)
__text:00002B6C
__text:00002B6C
__text:00002B6E
__text:00002B72
__text:00002B74
__text:00002B78
__text:00002B7A
__text:00002B7E
__text:00002B80

80
41
6F
C0
78
01
00
80

B5
F2 D8 30
46
F2 00 00
44
F0 38 EA
20
BD

_hello_world
PUSH
MOVW
MOV
MOVT.W
ADD
BLX
MOVS
POP

{R7,LR}
R0, #0x13D8
R7, SP
R0, #0
R0, PC
_puts
R0, #0
{R7,PC}

...
__cstring:00003E70 48 65 6C 6C 6F 20+aHelloWorld

DCB "Hello world!",0xA,0

Инструкции BL и BLX в Thumb, как мы помним, кодируются как пара 16-битных
инструкций, а в Thumb-2 эти суррогатные опкоды расширены так, что новые
инструкции кодируются здесь как 32-битные инструкции. Это можно заметить
по тому что опкоды Thumb-2 инструкций всегда начинаются с 0xFx либо с 0xEx.
Но в листинге IDA байты опкода переставлены местами. Это из-за того, что
в процессоре ARM инструкции кодируются так: в начале последний байт, потом первый (для Thumb и Thumb-2 режима), либо, (для инструкций в режиме
ARM) в начале четвертый байт, затем третий, второй и первый (т.е. другой
endianness).
Вот так байты следуют в листингах IDA:
• для режимов ARM и ARM64: 4-3-2-1;
• для режима Thumb: 2-1;
• для пары 16-битных инструкций в режиме Thumb-2: 2-1-4-3.
Так что мы видим здесь что инструкции MOVW, MOVT.W и BLX начинаются с 0xFx.
Одна из Thumb-2 инструкций это MOVW R0, #0x13D8 — она записывает 16-битное
число в младшую часть регистра R0, очищая старшие биты.
Ещё MOVT.W R0, #0 — эта инструкция работает так же, как и MOVT из предыдущего примера, но она работает в Thumb-2.
Помимо прочих отличий, здесь используется инструкция BLX вместо BL. Отличие в том, что помимо сохранения адреса возврата в регистре LR и передаче управления в функцию puts(), происходит смена режима процессора
с Thumb/Thumb-2 на режим ARM (либо назад). Здесь это нужно потому, что
инструкция, куда ведет переход, выглядит так (она закодирована в режиме
ARM):
__symbolstub1:00003FEC _puts
__symbolstub1:00003FEC 44 F0 9F E5

; CODE XREF: _hello_world+E
LDR PC, =__imp__puts

Это просто переход на место, где записан адрес puts() в секции импортов.
Итак, внимательный читатель может задать справедливый вопрос: почему бы

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

30
не вызывать puts() сразу в том же месте кода, где он нужен? Но это не очень
выгодно из-за экономии места и вот почему.
Практически любая программа использует внешние динамические библиотеки
(будь то DLL в Windows, .so в *NIX либо .dylib в Mac OS X). В динамических библиотеках находятся часто используемые библиотечные функции, в том числе
стандартная функция Си puts().
В исполняемом бинарном файле (Windows PE .exe, ELF либо Mach-O) имеется
секция импортов, список символов (функций либо глобальных переменных)
импортируемых из внешних модулей, а также названия самих модулей. Загрузчик ОС загружает необходимые модули и, перебирая импортируемые символы в основном модуле, проставляет правильные адреса каждого символа. В
нашем случае, __imp__puts это 32-битная переменная, куда загрузчик ОС запишет правильный адрес этой же функции во внешней библиотеке. Так что
инструкция LDR просто берет 32-битное значение из этой переменной, и, записывая его в регистр PC, просто передает туда управление. Чтобы уменьшить
время работы загрузчика ОС, нужно чтобы ему пришлось записать адрес каждого символа только один раз, в соответствующее, выделенное для них, место.
К тому же, как мы уже убедились, нельзя одной инструкцией загрузить в регистр 32-битное число без обращений к памяти. Так что наиболее оптимально
выделить отдельную функцию, работающую в режиме ARM, чья единственная
цель — передавать управление дальше, в динамическую библиотеку. И затем
ссылаться на эту короткую функцию из одной инструкции (так называемую
thunk-функцию) из Thumb-кода.
Кстати, в предыдущем примере (скомпилированном для режима ARM), переход при помощи инструкции BL ведет на такую же thunk-функцию, однако режим процессора не переключается (отсюда отсутствие «X» в мнемонике инструкции).
Еще о thunk-функциях
Thunk-функции трудновато понять, скорее всего, из-за путаницы в терминах.
Проще всего представлять их как адаптеры-переходники из одного типа разъемов в другой. Например, адаптер, позволяющий вставить в американскую розетку британскую вилку, или наоборот. Thunk-функции также иногда называются wrapper-ами. Wrap в английском языке это обертывать, завертывать. Вот
еще несколько описаний этих функций:
“A piece of coding which provides an address:”, according to
P. Z. Ingerman, who invented thunks in 1961 as a way of binding
actual parameters to their formal definitions in Algol-60 procedure
calls. If a procedure is called with an expression in the place of a
formal parameter, the compiler generates a thunk which computes
the expression and leaves the address of the result in some standard
location.


Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

31
Microsoft and IBM have both defined, in their Intel-based systems,
a “16-bit environment” (with bletcherous segment registers and 64K
address limits) and a “32-bit environment” (with flat addressing and
semi-real memory management). The two environments can both be
running on the same computer and OS (thanks to what is called, in the
Microsoft world, WOW which stands for Windows On Windows). MS and
IBM have both decided that the process of getting from 16- to 32-bit
and vice versa is called a “thunk”; for Windows 95, there is even a tool,
THUNK.EXE, called a “thunk compiler”.
( The Jargon File )
Еще один пример мы можем найти в библиотеке LAPACK — (“Linear Algebra
PACKage”) написанная на FORTRAN. Разработчики на Си/Си++также хотят использовать LAPACK, но переписывать её на Си/Си++, а затем поддерживать
несколько версий, это безумие. Так что имеются короткие функции на Си вызываемые из Си/Си++-среды, которые, в свою очередь, вызывают функции на
FORTRAN, и почти ничего больше не делают:
double Blas_Dot_Prod(const LaVectorDouble &dx, const LaVectorDouble &dy)
{
assert(dx.size()==dy.size());
integer n = dx.size();
integer incx = dx.inc(), incy = dy.inc();
return F77NAME(ddot)(&n, &dx(0), &incx, &dy(0), &incy);
}

Такие ф-ции еще называют “wrappers” (т.е., “обертка”).
ARM64
GCC
Компилируем пример в GCC 4.8.1 для ARM64:
Листинг 1.29: Неоптимизирующий GCC 4.8.1 + objdump
1
2
3
4
5
6
7
8
9
10
11
12

0000000000400590 :
400590:
a9bf7bfd
400594:
910003fd
400598:
90000000
40059c:
91192000
4005a0:
97ffffa0
4005a4:
52800000
4005a8:
a8c17bfd
4005ac:
d65f03c0

stp
mov
adrp
add
bl
mov
ldp
ret

x29, x30, [sp,#−16]!
x29, sp
x0, 400000
x0, x0, #0x648
400420
w0, #0x0 // #0
x29, x30, [sp],#16

...

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

32
13
14

Contents of section .rodata:
400640 01000200 00000000 48656c6c 6f210a00

........Hello!..

В ARM64 нет режима Thumb и Thumb-2, только ARM, так что тут только 32битные инструкции.
Регистров тут в 2 раза больше: .2.4 (стр. 1327). 64-битные регистры теперь
имеют префикс X-, а их 32-битные части — W-.
Инструкция STP (Store Pair) сохраняет в стеке сразу два регистра: X29 и X30.
Конечно, эта инструкция может сохранять эту пару где угодно в памяти, но
здесь указан регистр SP, так что пара сохраняется именно в стеке.
Регистры в ARM64 64-битные, каждый имеет длину в 8 байт, так что для хранения двух регистров нужно именно 16 байт.
Восклицательный знак (“!”) после операнда означает, что сначала от SP будет
отнято 16 и только затем значения из пары регистров будут записаны в стек.
Это называется pre-index. Больше о разнице между post-index и pre-index описано здесь: 1.39.2 (стр. 565).
Таким образом, в терминах более знакомого всем процессора x86, первая инструкция — это просто аналог пары инструкций PUSH X29 и PUSH X30. X29 в
ARM64 используется как FP40 , а X30 как LR, поэтому они сохраняются в прологе функции и восстанавливаются в эпилоге.
Вторая инструкция копирует SP в X29 (или FP). Это нужно для установки стекового фрейма функции.
Инструкции ADRP и ADD нужны для формирования адреса строки «Hello!» в регистре X0, ведь первый аргумент функции передается через этот регистр. Но в
ARM нет инструкций, при помощи которых можно записать в регистр длинное
число (потому что сама длина инструкции ограничена 4-я байтами. Больше об
этом здесь: 1.39.3 (стр. 567)). Так что нужно использовать несколько инструкций. Первая инструкция (ADRP) записывает в X0 адрес 4-килобайтной страницы
где находится строка, а вторая (ADD) просто прибавляет к этому адресу остаток. Читайте больше об этом: 1.39.4 (стр. 569).
0x400000 + 0x648 = 0x400648, и мы видим, что в секции данных .rodata по
этому адресу как раз находится наша Си-строка «Hello!».
Затем при помощи инструкции BL вызывается puts(). Это уже рассматривалось ранее: 1.5.3 (стр. 28).
Инструкция MOV записывает 0 в W0. W0 это младшие 32 бита 64-битного регистра X0:
Старшие 32 бита

младшие 32 бита
X0
W0

А результат функции возвращается через X0, и main() возвращает 0, так что
вот так готовится возвращаемый результат.
40 Frame

Pointer

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

33
Почему именно 32-битная часть? Потому что в ARM64, как и в x86-64, тип int
оставили 32-битным, для лучшей совместимости.
Следовательно, раз уж функция возвращает 32-битный int, то нужно заполнить только 32 младших бита регистра X0.
Для того, чтобы удостовериться в этом, немного отредактируем этот пример
и перекомпилируем его.
Теперь main() возвращает 64-битное значение:
Листинг 1.30: main() возвращающая значение типа uint64_t
#include
#include
uint64_t main()
{
printf ("Hello!\n");
return 0;
}

Результат точно такой же, только MOV в той строке теперь выглядит так:
Листинг 1.31: Неоптимизирующий GCC 4.8.1 + objdump
4005a4:

d2800000

mov

x0, #0x0

// #0

Далее при помощи инструкции LDP (Load Pair) восстанавливаются регистры X29
и X30.
Восклицательного знака после инструкции нет. Это означает, что сначала значения достаются из стека, и только потом SP увеличивается на 16.
Это называется post-index.
В ARM64 есть новая инструкция: RET. Она работает так же как и BX LR, но там
добавлен специальный бит, подсказывающий процессору, что это именно выход из функции, а не просто переход, чтобы процессор мог более оптимально
исполнять эту инструкцию.
Из-за простоты этой функции оптимизирующий GCC генерирует точно такой
же код.

1.5.4. MIPS
О «глобальном указателе» («global pointer»)
«Глобальный указатель» («global pointer») — это важная концепция в MIPS. Как
мы уже возможно знаем, каждая инструкция в MIPS имеет размер 32 бита, поэтому невозможно закодировать 32-битный адрес внутри одной инструкции.
Вместо этого нужно использовать пару инструкций (как это сделал GCC для
загрузки адреса текстовой строки в нашем примере). С другой стороны, используя только одну инструкцию, возможно загружать данные по адресам в

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

34
пределах register − 32768...register + 32767, потому что 16 бит знакового смещения можно закодировать в одной инструкции). Так мы можем выделить какойто регистр для этих целей и ещё выделить буфер в 64KiB для самых часто
используемых данных. Выделенный регистр называется «глобальный указатель» («global pointer») и он указывает на середину области 64KiB. Эта область
обычно содержит глобальные переменные и адреса импортированных функций вроде printf(), потому что разработчики GCC решили, что получение адреса функции должно быть как можно более быстрой операцией, исполняющейся за одну инструкцию вместо двух. В ELF-файле эта 64KiB-область находится частично в секции .sbss («small BSS41 ») для неинициализированных данных и в секции .sdata («small data») для инициализированных данных. Это значит что программист может выбирать, к чему нужен как можно более быстрый
доступ, и затем расположить это в секциях .sdata/.sbss. Некоторые программисты «старой школы» могут вспомнить модель памяти в MS-DOS 10.6 (стр. 1274)
или в менеджерах памяти вроде XMS/EMS, где вся память делилась на блоки
по 64KiB.
Эта концепция применяется не только в MIPS. По крайней мере PowerPC также
использует эту технику.
Оптимизирующий GCC
Рассмотрим следующий пример, иллюстрирующий концепцию «глобального
указателя».
Листинг 1.32: Оптимизирующий GCC 4.4.5 (вывод на ассемблере)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

$LC0:
; \000 это ноль в восьмеричной системе:
.ascii "Hello, world!\012\000"
main:
; пролог функции
; установить GP:
lui
$28,%hi(__gnu_local_gp)
addiu
$sp,$sp,−32
addiu
$28,$28,%lo(__gnu_local_gp)
; сохранить RA в локальном стеке:
sw
$31,28($sp)
; загрузить адрес функции puts() из GP в $25:
lw
$25,%call16(puts)($28)
; загрузить адрес текстовой строки в $4 ($a0):
lui
$4,%hi($LC0)
; перейти на puts(), сохранив адрес возврата в link-регистре:
jalr
$25
addiu
$4,$4,%lo($LC0) ; branch delay slot
; восстановить RA:
lw
$31,28($sp)
; скопировать 0 из $zero в $v0:
move
$2,$0
; вернуть управление сделав переход по адресу в RA:
j
$31
41 Block

Started by Symbol

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

35
25
26

; эпилог функции:
addiu
$sp,$sp,32 ; branch delay slot + освободить стек от локальных
переменных

Как видно, регистр $GP в прологе функции выставляется в середину этой области. Регистр RA сохраняется в локальном стеке. Здесь также используется
puts() вместо printf(). Адрес функции puts() загружается в $25 инструкцией LW («Load Word»). Затем адрес текстовой строки загружается в $4 парой
инструкций LUI («Load Upper Immediate») и ADDIU («Add Immediate Unsigned
Word»). LUI устанавливает старшие 16 бит регистра (поэтому в имени инструкции присутствует «upper») и ADDIU прибавляет младшие 16 бит к адресу. ADDIU
следует за JALR (помните о branch delay slots?). Регистр $4 также называется
$A0, который используется для передачи первого аргумента функции 42 . JALR
(«Jump and Link Register») делает переход по адресу в регистре $25 (там адрес puts()) при этом сохраняя адрес следующей инструкции (LW) в RA. Это так
же как и в ARM. И ещё одна важная вещь: адрес сохраняемый в RA это адрес
не следующей инструкции (потому что это delay slot и исполняется перед инструкцией перехода), а инструкции после неё (после delay slot). Таким образом
во время исполнения JALR в RA записывается P C + 8. В нашем случае это адрес
инструкции LW следующей после ADDIU.
LW («Load Word») в строке 20 восстанавливает RA из локального стека (эта инструкция скорее часть эпилога функции).
MOVE в строке 22 копирует значение из регистра $0 ($ZERO) в $2 ($V0).
В MIPS есть константный регистр, всегда содержащий ноль. Должно быть, разработчики MIPS решили, что 0 это самая востребованная константа в программировании, так что пусть будет использоваться регистр $0, всякий раз, когда
будет нужен 0. Другой интересный факт: в MIPS нет инструкции, копирующей
значения из регистра в регистр. На самом деле, MOVE DST, SRC это ADD DST,
SRC, $ZERO (DST = SRC + 0), которая делает тоже самое. Очевидно, разработчики MIPS хотели сделать как можно более компактную таблицу опкодов. Это
не значит, что сложение происходит во время каждой инструкции MOVE. Скорее всего, эти псевдоинструкции оптимизируются в CPU и АЛУ43 никогда не
используется.
J в строке 24 делает переход по адресу в RA, и это работает как выход из
функции. ADDIU после J на самом деле исполняется перед J (помните о branch
delay slots?) и это часть эпилога функции.
Вот листинг сгенерированный IDA. Каждый регистр имеет свой псевдоним:
Листинг 1.33: Оптимизирующий GCC 4.4.5 (IDA)
1
2
3
4
5
6

.text:00000000 main:
.text:00000000
.text:00000000 var_10
.text:00000000 var_4
.text:00000000
; пролог функции
42 Таблица

= −0x10
= −4

регистров в MIPS доступна в приложении .3.1 (стр. 1328)
устройство

43 Арифметико-логическое

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

36
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

; установить GP:
.text:00000000
lui
$gp, (__gnu_local_gp >> 16)
.text:00000004
addiu
$sp, −0x20
.text:00000008
la
$gp, (__gnu_local_gp & 0xFFFF)
; сохранить RA в локальном стеке:
.text:0000000C
sw
$ra, 0x20+var_4($sp)
; сохранить GP в локальном стеке:
; по какой-то причине, этой инструкции не было в ассемблерном выводе в GCC:
.text:00000010
sw
$gp, 0x20+var_10($sp)
; загрузить адрес функции puts() из GP в $t9:
.text:00000014
lw
$t9, (puts & 0xFFFF)($gp)
; сформировать адрес текстовой строки в $a0:
.text:00000018
lui
$a0, ($LC0 >> 16) # "Hello, world!"
; перейти на puts(), сохранив адрес возврата в link-регистре:
.text:0000001C
jalr
$t9
.text:00000020
la
$a0, ($LC0 & 0xFFFF) # "Hello,
world!"
; восстановить RA:
.text:00000024
lw
$ra, 0x20+var_4($sp)
; скопировать 0 из $zero в $v0:
.text:00000028
move
$v0, $zero
; вернуть управление сделав переход по адресу в RA:
.text:0000002C
jr
$ra
; эпилог функции:
.text:00000030
addiu
$sp, 0x20

Инструкция в строке 15 сохраняет GP в локальном стеке. Эта инструкция мистическим образом отсутствует в листинге от GCC, может быть из-за ошибки в
самом GCC44 . Значение GP должно быть сохранено, потому что всякая функция
может работать со своим собственным окном данных размером 64KiB. Регистр,
содержащий адрес функции puts() называется $T9, потому что регистры с
префиксом T- называются «temporaries» и их содержимое можно не сохранять.
Неоптимизирующий GCC
Неоптимизирующий GCC более многословный.
Листинг 1.34: Неоптимизирующий GCC 4.4.5 (вывод на ассемблере)
1
2
3
4
5
6
7
8
9
10
11
12

$LC0:
.ascii "Hello, world!\012\000"
main:
; пролог функции
; сохранить RA ($31) и FP в стеке:
addiu
$sp,$sp,−32
sw
$31,28($sp)
sw
$fp,24($sp)
; установить FP (указатель стекового фрейма):
move
$fp,$sp
; установить GP:
lui
$28,%hi(__gnu_local_gp)
44 Очевидно, функция вывода листингов не так критична для пользователей GCC, поэтому там
вполне могут быть неисправленные косметические ошибки.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

37
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

addiu
$28,$28,%lo(__gnu_local_gp)
; загрузить адрес текстовой строки:
lui
$2,%hi($LC0)
addiu
$4,$2,%lo($LC0)
; загрузить адрес функции puts() используя GP:
lw
$2,%call16(puts)($28)
nop
; вызвать puts():
move
$25,$2
jalr
$25
nop ; branch delay slot
; восстановить GP из локального стека:
lw
$28,16($fp)
; установить регистр $2 ($V0) в ноль:
move
$2,$0
; эпилог функции.
; восстановить SP:
move
$sp,$fp
; восстановить RA:
lw
$31,28($sp)
; восстановить FP:
lw
$fp,24($sp)
addiu
$sp,$sp,32
; переход на RA:
j
$31
nop ; branch delay slot

Мы видим, что регистр FP используется как указатель на фрейм стека. Мы также видим 3 NOP-а. Второй и третий следуют за инструкциями перехода. Видимо, компилятор GCC всегда добавляет NOP-ы (из-за branch delay slots) после
инструкций переходов и затем, если включена оптимизация, от них может избавляться. Так что они остались здесь.
Вот также листинг от IDA:
Листинг 1.35: Неоптимизирующий GCC 4.4.5 (IDA)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

.text:00000000 main:
.text:00000000
.text:00000000 var_10
= −0x10
.text:00000000 var_8
= −8
.text:00000000 var_4
= −4
.text:00000000
; пролог функции
; сохранить RA и FP в стеке:
.text:00000000
addiu
$sp, −0x20
.text:00000004
sw
$ra, 0x20+var_4($sp)
.text:00000008
sw
$fp, 0x20+var_8($sp)
; установить FP (указатель стекового фрейма):
.text:0000000C
move
$fp, $sp
; установить GP:
.text:00000010
la
$gp, __gnu_local_gp
.text:00000018
sw
$gp, 0x20+var_10($sp)
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

38
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

; загрузить адрес текстовой строки:
.text:0000001C
lui
$v0, (aHelloWorld >> 16) # "Hello,
world!"
.text:00000020
addiu
$a0, $v0, (aHelloWorld & 0xFFFF) #
"Hello, world!"
; загрузить адрес функции puts() используя GP:
.text:00000024
lw
$v0, (puts & 0xFFFF)($gp)
.text:00000028
or
$at, $zero ; NOP
; вызвать puts():
.text:0000002C
move
$t9, $v0
.text:00000030
jalr
$t9
.text:00000034
or
$at, $zero ; NOP
; восстановить GP из локального стека:
.text:00000038
lw
$gp, 0x20+var_10($fp)
; установить регистр $2 ($V0) в ноль:
.text:0000003C
move
$v0, $zero
; эпилог функции.
; восстановить SP:
.text:00000040
move
$sp, $fp
; восстановить RA:
.text:00000044
lw
$ra, 0x20+var_4($sp)
; восстановить FP:
.text:00000048
lw
$fp, 0x20+var_8($sp)
.text:0000004C
addiu
$sp, 0x20
; переход на RA:
.text:00000050
jr
$ra
.text:00000054
or
$at, $zero ; NOP

Интересно что IDA распознала пару инструкций LUI/ADDIU и собрала их в одну псевдоинструкцию LA («Load Address») в строке 15. Мы также видим, что
размер этой псевдоинструкции 8 байт! Это псевдоинструкция (или макрос),
потому что это не настоящая инструкция MIPS, а скорее просто удобное имя
для пары инструкций.
Ещё кое что: IDA не распознала NOP-инструкции в строках 22, 26 и 41.
Это OR $AT, $ZERO. По своей сути это инструкция, применяющая операцию
ИЛИ к содержимому регистра $AT с нулем, что, конечно же, холостая операция.
MIPS, как и многие другие ISA, не имеет отдельной NOP-инструкции.
Роль стекового фрейма в этом примере
Адрес текстовой строки передается в регистре. Так зачем устанавливать локальный стек? Причина в том, что значения регистров RA и GP должны быть
сохранены где-то (потому что вызывается printf()) и для этого используется
локальный стек.
Если бы это была leaf function, тогда можно было бы избавиться от пролога и
эпилога функции. Например: 1.4.3 (стр. 11).
Оптимизирующий GCC: загрузим в GDB

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

39
Листинг 1.36: пример сессии в GDB
root@debian−mips:~# gcc hw.c −O3 −o hw
root@debian−mips:~# gdb hw
GNU gdb (GDB) 7.0.1−debian
...
Reading symbols from /root/hw...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x400654
(gdb) run
Starting program: /root/hw
Breakpoint 1, 0x00400654 in main ()
(gdb) set step−mode on
(gdb) disas
Dump of assembler code for function main:
0x00400640 :
lui
gp,0x42
0x00400644 :
addiu
sp,sp,−32
0x00400648 :
addiu
gp,gp,−30624
0x0040064c :
sw
ra,28(sp)
gp,16(sp)
0x00400650 :
sw
0x00400654 :
lw
t9,−32716(gp)
0x00400658 :
lui
a0,0x40
0x0040065c :
jalr
t9
0x00400660 :
addiu
a0,a0,2080
0x00400664 :
lw
ra,28(sp)
0x00400668 :
move
v0,zero
0x0040066c :
jr
ra
0x00400670 :
addiu
sp,sp,32
End of assembler dump.
(gdb) s
0x00400658 in main ()
(gdb) s
0x0040065c in main ()
(gdb) s
0x2ab2de60 in printf () from /lib/libc.so.6
(gdb) x/s $a0
0x400820:
"hello, world"
(gdb)

1.5.5. Вывод
Основная разница между кодом x86/ARM и x64/ARM64 в том, что указатель на
строку теперь 64-битный. Действительно, ведь для того современные CPU и
стали 64-битными, потому что подешевела память, её теперь можно поставить
в компьютер намного больше, и чтобы её адресовать, 32-х бит уже недостаточно. Поэтому все указатели теперь 64-битные.

1.5.6. Упражнения
• http://challenges.re/48
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

40
• http://challenges.re/49

1.6. Пролог и эпилог функций
Пролог функции это инструкции в самом начале функции. Как правило, это
что-то вроде такого фрагмента кода:
push
mov
sub

ebp
ebp, esp
esp, X

Эти инструкции делают следующее: сохраняют значение регистра EBP на будущее, выставляют EBP равным ESP, затем подготавливают место в стеке для
хранения локальных переменных.
EBP сохраняет свое значение на протяжении всей функции, он будет использоваться здесь для доступа к локальным переменным и аргументам. Можно
было бы использовать и ESP, но он постоянно меняется и это не очень удобно.
Эпилог функции аннулирует выделенное место в стеке, восстанавливает значение EBP на старое и возвращает управление в вызывающую функцию:
mov
pop
ret

esp, ebp
ebp
0

Пролог и эпилог функции обычно находятся в дизассемблерах для отделения
функций друг от друга.

1.6.1. Рекурсия
Наличие эпилога и пролога может несколько ухудшить эффективность рекурсии.
Больше о рекурсии в этой книге: 3.5.3 (стр. 622).

1.7. Еще кое-что о пустой ф-ции
Вернемся к примеру с пустой ф-цией 1.3 (стр. 8). Теперь, когда мы уже знаем о
прологе и эпилоге ф-ции, вот эта пустая ф-ция 1.1 (стр. 8) скомпилированная
неоптимизирующим GCC:
Листинг 1.37: Неоптимизирующий GCC 8.2 x64 (вывод на ассемблере)
f:
push
mov
nop
pop
ret

rbp
rbp, rsp
rbp

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

41
Это RET, но пролог и эпилог ф-ции, вероятно, не был соптимизирован и остался
как есть. NOP это, похоже, артефакт компилятора. Так или иначе, единственная
здесь рабочая инструкция это RET. Остальные инструкции могут быть убраны
(или соптимизированы).

1.8. Еще кое-что о возвращаемых значениях
Теперь, когда мы уже знаем о прологе и эпилоге ф-ции, попробуем скомпилировать пример, возвращающий значение (1.4 (стр. 10), 1.8 (стр. 10)), используя
неоптимизирующий GCC:
Листинг 1.38: Неоптимизирующий GCC 8.2 x64 (вывод на ассемблере)
f:
push
mov
mov
pop
ret

rbp
rbp, rsp
eax, 123
rbp

Рабочие здесь инструкции это MOV и RET, остальные – пролог и эпилог.

1.9. Стек
Стек в компьютерных науках — это одна из наиболее фундаментальных структур данных 45 . AKA46 LIFO47 .
Технически это просто блок памяти в памяти процесса + регистр ESP в x86 или
RSP в x64, либо SP в ARM, который указывает где-то в пределах этого блока.
Часто используемые инструкции для работы со стеком — это PUSH и POP (в x86
и Thumb-режиме ARM). PUSH уменьшает ESP/RSP/SP на 4 в 32-битном режиме
(или на 8 в 64-битном), затем записывает по адресу, на который указывает
ESP/RSP/SP, содержимое своего единственного операнда.
POP это обратная операция — сначала достает из указателя стека значение и
помещает его в операнд (который очень часто является регистром) и затем
увеличивает указатель стека на 4 (или 8).
В самом начале регистр-указатель указывает на конец стека. Конец стека находится в начале блока памяти, выделенного под стек. Это странно, но это так.
PUSH уменьшает регистр-указатель, а POP — увеличивает.
В процессоре ARM, тем не менее, есть поддержка стеков, растущих как в сторону уменьшения, так и в сторону увеличения.
Например, инструкции STMFD/LDMFD, STMED48 /LDMED49 предназначены для descending45 wikipedia.org/wiki/Call_stack
46

Also Known As — Также известный как
In First Out (последним вошел, первым вышел)
48 Store Multiple Empty Descending (инструкция ARM)
49 Load Multiple Empty Descending (инструкция ARM)
47 Last

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

42
стека (растет назад, начиная с высоких адресов в сторону низких).
Инструкции STMFA50 /LDMFA51 , STMEA52 /LDMEA53 предназначены для ascendingстека (растет вперед, начиная с низких адресов в сторону высоких).

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

Вершина стека

Куча

Стэк

В [D. M. Ritchie and K. Thompson, The UNIX Time Sharing System, (1974)]54 можно
прочитать:
The user-core part of an image is divided into three logical
segments. The program text segment begins at location 0 in the virtual
address space. During execution, this segment is write-protected and
a single copy of it is shared among all processes executing the
same program. At the first 8K byte boundary above the program text
segment in the virtual address space begins a nonshared, writable
data segment, the size of which may be extended by a system call.
Starting at the highest address in the virtual address space is a stack
segment, which automatically grows downward as the hardware’s
stack pointer fluctuates.
Это немного напоминает как некоторые студенты пишут два конспекта в одной тетрадке: первый конспект начинается обычным образом, второй пишется
с конца, перевернув тетрадку. Конспекты могут встретиться где-то посредине,
в случае недостатка свободного места.
50 Store

Multiple Full Ascending (инструкция ARM)
Multiple Full Ascending (инструкция ARM)
52 Store Multiple Empty Ascending (инструкция ARM)
53 Load Multiple Empty Ascending (инструкция ARM)
54 Также доступно здесь: URL
51 Load

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

43

1.9.2. Для чего используется стек?
Сохранение адреса возврата управления
x86
При вызове другой функции через CALL сначала в стек записывается адрес,
указывающий на место после инструкции CALL, затем делается безусловный
переход (почти как JMP) на адрес, указанный в операнде.
CALL — это аналог пары инструкций PUSH address_after_call / JMP.
RET вытаскивает из стека значение и передает управление по этому адресу —
это аналог пары инструкций POP tmp / JMP tmp.
Крайне легко устроить переполнение стека, запустив бесконечную рекурсию:
void f()
{
f();
};

MSVC 2008 предупреждает о проблеме:
c:\tmp6>cl ss.cpp /Fass.asm
Microsoft (R) 32−bit C/C++ Optimizing Compiler Version 15.00.21022.08 for ⤦
Ç 80x86
Copyright (C) Microsoft Corporation. All rights reserved.
ss.cpp
c:\tmp6\ss.cpp(4) : warning C4717: 'f' : recursive on all control paths, ⤦
Ç function will cause runtime stack overflow

…но, тем не менее, создает нужный код:
?f@@YAXXZ PROC
; Line 2
push
mov
; Line 3
call
; Line 4
pop
ret
?f@@YAXXZ ENDP

; f
ebp
ebp, esp
?f@@YAXXZ

; f

ebp
0
; f

…причем, если включить оптимизацию (/Ox), то будет даже интереснее, без
переполнения стека, но работать будет корректно55 :
?f@@YAXXZ PROC
; Line 2
$LL3@f:
55 здесь

; f

ирония

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

44
; Line 3
jmp
?f@@YAXXZ ENDP

SHORT $LL3@f
; f

GCC 4.4.1 генерирует точно такой же код в обоих случаях, хотя и не предупреждает о проблеме.
ARM
Программы для ARM также используют стек для сохранения RA, куда нужно
вернуться, но несколько иначе. Как уже упоминалось в секции «Hello, world!» (1.5.3
(стр. 24)), RA записывается в регистр LR (link register). Но если есть необходимость вызывать какую-то другую функцию и использовать регистр LR ещё раз,
его значение желательно сохранить.
Обычно это происходит в прологе функции, часто мы видим там инструкцию
вроде PUSH {R4-R7,LR}, а в эпилоге POP {R4-R7,PC} — так сохраняются регистры, которые будут использоваться в текущей функции, в том числе LR.
Тем не менее, если некая функция не вызывает никаких более функций, в терминологии RISC она называется leaf function56 . Как следствие, «leaf»-функция
не сохраняет регистр LR (потому что не изменяет его). А если эта функция
небольшая, использует мало регистров, она может не использовать стек вообще. Таким образом, в ARM возможен вызов небольших leaf-функций не используя стек. Это может быть быстрее чем в старых x86, ведь внешняя память для
стека не используется 57 . Либо это может быть полезным для тех ситуаций,
когда память для стека ещё не выделена, либо недоступна,
Некоторые примеры таких функций: 1.14.3 (стр. 138), 1.14.3 (стр. 139), 1.282
(стр. 403), 1.298 (стр. 425), 1.28.5 (стр. 425), 1.192 (стр. 272), 1.190 (стр. 270),
1.209 (стр. 292).
Передача параметров функции
Самый распространенный способ передачи параметров в x86 называется «cdecl»:
push arg3
push arg2
push arg1
call f
add esp, 12 ; 4*3=12

Вызываемая функция получает свои параметры также через указатель стека.
Следовательно, так расположены значения в стеке перед исполнением самой
первой инструкции функции f():
56 infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka13785.html
57 Когда-то,

очень давно, на PDP-11 и VAX на инструкцию CALL (вызов других функций) могло тратиться вплоть до 50% времени (возможно из-за работы с памятью), поэтому считалось, что много
небольших функций это анти-паттерн [Eric S. Raymond, The Art of UNIX Programming, (2003)Chapter
4, Part II].

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

45
ESP
ESP+4
ESP+8
ESP+0xC


адрес возврата
аргумент#1, маркируется в IDA как arg_0
аргумент#2, маркируется в IDA как arg_4
аргумент#3, маркируется в IDA как arg_8


См. также в соответствующем разделе о других способах передачи аргументов через стек (6.1 (стр. 956)).
Кстати, вызываемая функция не имеет информации о количестве переданных ей аргументов. Функции Си с переменным количеством аргументов (как
printf()) могут определять их количество по спецификаторам строки формата (начинающиеся со знака %).
Если написать что-то вроде:
printf("%d %d %d", 1234);

printf() выведет 1234, затем ещё два случайных числа58 , которые волею случая оказались в стеке рядом.
Вот почему не так уж и важно, как объявлять функцию main():
как main(), main(int argc, char *argv[])
либо main(int argc, char *argv[], char *envp[]).
В реальности, CRT-код вызывает main() примерно так:
push
push
push
call
...

envp
argv
argc
main

Если вы объявляете main() без аргументов, они, тем не менее, присутствуют в
стеке, но не используются. Если вы объявите main() как main(int argc, char
*argv[]), вы можете использовать два первых аргумента, а третий останется
для вашей функции «невидимым». Более того, можно даже объявить main(int
argc), и это будет работать.
Альтернативные способы передачи аргументов
Важно отметить, что, в общем, никто не заставляет программистов передавать параметры именно через стек, это не является требованием к исполняемому коду. Вы можете делать это совершенно иначе, не используя стек вообще.
В каком-то смысле, популярный метод среди начинающих использовать язык
ассемблера, это передавать аргументы в глобальных переменных, например:
Листинг 1.39: Код на ассемблере
58 В

строгом смысле, они не случайны, скорее, непредсказуемы: 1.9.4 (стр. 51)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

46
...
mov
mov
call

X, 123
Y, 456
do_something

...
X
Y

dd
dd

?
?

do_something proc near
; take X
; take Y
; do something
retn
do_something endp

Но у этого метода есть очевидный недостаток: ф-ция do_something() не сможет вызвать саму себя рекурсивно (либо, через какую-то стороннюю ф-цию),
потому что тогда придется затереть свои собственные аргументы. Та же история с локальными переменными: если хранить их в глобальных переменных,
ф-ция не сможет вызывать сама себя. К тому же, этот метод не безопасный
для мультитредовой среды59 . Способ хранения подобной информации в стеке
заметно всё упрощает — он может хранить столько аргументов ф-ций и/или
значений вообще, сколько в нем есть места.
В [Donald E. Knuth, The Art of Computer Programming, Volume 1, 3rd ed., (1997),
189] можно прочитать про еще более странные схемы передачи аргументов,
которые были очень удобны на IBM System/360.
В MS-DOS был метод передачи аргументов через регистры, например, этот
фрагмент кода для древней 16-битной MS-DOS выводит “Hello, world!”:
mov
mov
int

dx, msg
ah, 9
21h

; адрес сообщения
; 9 означает ф-цию "вывод строки"
; DOS "syscall"

mov
int

ah, 4ch
21h

; ф-ция "закончить программу"
; DOS "syscall"

msg

db 'Hello, World!\$'

Это очень похоже на метод 6.1.3 (стр. 958). И еще на метод вызовов сисколлов
в Linux (6.3.1 (стр. 974)) и Windows.
Если ф-ция в MS-DOS возвращает булево значение (т.е., один бит, обычно сигнализирующий об ошибке), часто использовался флаг CF.
Например:
59 При корректной реализации, каждый тред будет иметь свой собственный стек со своими
аргументами/переменными.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

47
mov ah, 3ch
; создать файл
lea dx, filename
mov cl, 1
int 21h
jc error
mov file_handle, ax
...
error:
...

В случае ошибки, флаг CF будет выставлен. Иначе, хэндл только что созданного файла возвращается в AX.
Этот метод до сих пор используется программистами на ассемблере. В исходных кодах Windows Research Kernel (который очень похож на Windows 2003) мы
можем найти такое
(файл base/ntos/ke/i386/cpu.asm):
public
Get386Stepping

Get386Stepping
proc

call
jnc
mov
ret

MultiplyTest
short G3s00
ax, 0

; Perform multiplication test
; if nc, muttest is ok

call
jnc
mov
ret

Check386B0
short G3s05
ax, 100h

; Check for B0 stepping
; if nc, it's B1/later
; It is B0/earlier stepping

call
jc
mov
ret

Check386D1
short G3s10
ax, 301h

; Check for D1 stepping
; if c, it is NOT D1
; It is D1/later stepping

mov
ret

ax, 101h

; assume it is B1 stepping

G3s00:

G3s05:

G3s10:

...
MultiplyTest

mlt00:

xor
push
call
pop
jc
loop
clc

proc
cx,cx
cx
Multiply
cx
short mltx
mlt00

; 64K times is a nice round number
; does this chip's multiply work?
; if c, No, exit
; if nc, YEs, loop to try again

mltx:
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

48
ret
MultiplyTest

endp

Хранение локальных переменных
Функция может выделить для себя некоторое место в стеке для локальных
переменных, просто отодвинув указатель стека глубже к концу стека.
Это очень быстро вне зависимости от количества локальных переменных. Хранить локальные переменные в стеке не является необходимым требованием.
Вы можете хранить локальные переменные где угодно. Но по традиции всё
сложилось так.
x86: Функция alloca()
Интересен случай с функцией alloca() 60 . Эта функция работает как malloc(),
но выделяет память прямо в стеке. Память освобождать через free() не нужно, так как эпилог функции (1.6 (стр. 40)) вернет ESP в изначальное состояние
и выделенная память просто выкидывается. Интересна реализация функции
alloca(). Эта функция, если упрощенно, просто сдвигает ESP вглубь стека на
столько байт, делая так, что ESP указывает на выделенный блок.
Попробуем:
#ifdef __GNUC__
#include // GCC
#else
#include // MSVC
#endif
#include
void f()
{
char ∗buf=(char∗)alloca (600);
#ifdef __GNUC__
snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3); // GCC
#else
_snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3); // MSVC
#endif
puts (buf);
};

Функция _snprintf() работает так же, как и printf(), только вместо выдачи
результата в stdout (т.е. на терминал или в консоль), записывает его в буфер
buf. Функция puts() выдает содержимое буфера buf в stdout. Конечно, можно
было бы заменить оба этих вызова на один printf(), но здесь нужно проиллюстрировать использование небольшого буфера.
60 В MSVC, реализацию функции можно посмотреть в файлах alloca16.asm и chkstk.asm в
C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\crt\src\intel

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

49
MSVC
Компилируем (MSVC 2010):
Листинг 1.40: MSVC 2010
...
mov
call
mov

eax, 600 ; 00000258H
__alloca_probe_16
esi, esp

push
push
push
push
push
push
call

3
2
1
OFFSET $SG2672
600
; 00000258H
esi
__snprintf

push
call
add

esi
_puts
esp, 28

...

Единственный параметр в alloca() передается через EAX, а не как обычно
через стек 61 .
GCC + Синтаксис Intel
А GCC 4.4.1 обходится без вызова других функций:
Листинг 1.41: GCC 4.7.3
.LC0:
.string "hi! %d, %d, %d\n"
f:
push
mov
push
sub
lea
and
границе
mov
mov

ebp
ebp,
ebx
esp,
ebx,
ebx,

esp
660
[esp+39]
−16

DWORD PTR [esp], ebx
DWORD PTR [esp+20], 3

; выровнять указатель по 16-байтной
; s

61 Это потому, что alloca() — это не сколько функция, сколько т.н. compiler intrinsic (10.3
(стр. 1268)) Одна из причин, почему здесь нужна именно функция, а не несколько инструкций
прямо в коде в том, что в реализации функции alloca() от MSVC62 есть также код, читающий из
только что выделенной памяти, чтобы ОС подключила физическую память к этому региону VM63 .
После вызова alloca() ESP указывает на блок в 600 байт, который мы можем использовать под
buf.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

50
mov
mov
mov
mov
call
mov
call
mov
leave
ret

DWORD PTR [esp+16], 2
DWORD PTR [esp+12], 1
DWORD PTR [esp+8], OFFSET FLAT:.LC0 ; "hi! %d, %d, %d\n"
DWORD PTR [esp+4], 600
; maxlen
_snprintf
DWORD PTR [esp], ebx
; s
puts
ebx, DWORD PTR [ebp−4]

GCC + Синтаксис AT&T
Посмотрим на тот же код, только в синтаксисе AT&T:
Листинг 1.42: GCC 4.7.3
.LC0:
.string "hi! %d, %d, %d\n"
f:
pushl
movl
pushl
subl
leal
andl
movl
movl
movl
movl
movl
movl
call
movl
call
movl
leave
ret

%ebp
%esp, %ebp
%ebx
$660, %esp
39(%esp), %ebx
$−16, %ebx
%ebx, (%esp)
$3, 20(%esp)
$2, 16(%esp)
$1, 12(%esp)
$.LC0, 8(%esp)
$600, 4(%esp)
_snprintf
%ebx, (%esp)
puts
−4(%ebp), %ebx

Всё то же самое, что и в прошлом листинге.
Кстати, movl $3, 20(%esp) — это аналог mov DWORD PTR [esp+20], 3 в синтаксисе Intel. Адресация памяти в виде регистр+смещение записывается в синтаксисе AT&T как смещение(%регистр).
(Windows) SEH
В стеке хранятся записи SEH64 для функции (если они присутствуют). Читайте
больше о нем здесь: (6.5.3 (стр. 998)).
64 Structured

Exception Handling

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

51
Защита от переполнений буфера
Здесь больше об этом (1.26.2 (стр. 347)).
Автоматическое освобождение данных в стеке
Возможно, причина хранения локальных переменных и SEH-записей в стеке в
том, что после выхода из функции, всё эти данные освобождаются автоматически, используя только одну инструкцию корректирования указателя стека
(часто это ADD). Аргументы функций, можно сказать, тоже освобождаются автоматически в конце функции. А всё что хранится в куче (heap) нужно освобождать явно.

1.9.3. Разметка типичного стека
Разметка типичного стека в 32-битной среде перед исполнением самой первой
инструкции функции выглядит так:

ESP-0xC
ESP-8
ESP-4
ESP
ESP+4
ESP+8
ESP+0xC



локальная переменная#2, маркируется в IDA как var_8
локальная переменная#1, маркируется в IDA как var_4
сохраненное значениеEBP
Адрес возврата
аргумент#1, маркируется в IDA как arg_0
аргумент#2, маркируется в IDA как arg_4
аргумент#3, маркируется в IDA как arg_8


1.9.4. Мусор в стеке
When one says that something seems
random, what one usually means in practice
is that one cannot see any regularities in it.
Stephen Wolfram, A New Kind of Science.

Часто в этой книге говорится о «шуме» или «мусоре» в стеке или памяти. Откуда он берется? Это то, что осталось там после исполнения предыдущих функций.
Короткий пример:
#include
void f1()
{
int a=1, b=2, c=3;
};
void f2()
{
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

52
int a, b, c;
printf ("%d, %d, %d\n", a, b, c);
};
int main()
{
f1();
f2();
};

Компилируем…
Листинг 1.43: Неоптимизирующий MSVC 2010
$SG2752 DB

'%d, %d, %d', 0aH, 00H

_c$ = −12
_b$ = −8
_a$ = −4
_f1
PROC
push
mov
sub
mov
mov
mov
mov
pop
ret
_f1
ENDP

; size = 4
; size = 4
; size = 4

_c$ = −12
_b$ = −8
_a$ = −4
_f2
PROC
push
mov
sub
mov
push
mov
push
mov
push
push
call
add
mov
pop
ret
_f2
ENDP
_main

PROC
push

ebp
ebp, esp
esp, 12
DWORD PTR _a$[ebp], 1
DWORD PTR _b$[ebp], 2
DWORD PTR _c$[ebp], 3
esp, ebp
ebp
0

; size = 4
; size = 4
; size = 4
ebp
ebp, esp
esp, 12
eax, DWORD PTR _c$[ebp]
eax
ecx, DWORD PTR _b$[ebp]
ecx
edx, DWORD PTR _a$[ebp]
edx
OFFSET $SG2752 ; '%d, %d, %d'
DWORD PTR __imp__printf
esp, 16
esp, ebp
ebp
0

ebp

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

53

_main

mov
call
call
xor
pop
ret
ENDP

ebp, esp
_f1
_f2
eax, eax
ebp
0

Компилятор поворчит немного…
c:\Polygon\c>cl st.c /Fast.asm /MD
Microsoft (R) 32−bit C/C++ Optimizing Compiler Version 16.00.40219.01 for ⤦
Ç 80x86
Copyright (C) Microsoft Corporation. All rights reserved.
st.c
c:\polygon\c\st.c(11) : warning C4700: uninitialized local variable 'c' ⤦
Ç used
c:\polygon\c\st.c(11) : warning C4700: uninitialized local variable 'b' ⤦
Ç used
c:\polygon\c\st.c(11) : warning C4700: uninitialized local variable 'a' ⤦
Ç used
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out:st.exe
st.obj

Но когда мы запускаем…
c:\Polygon\c>st
1, 2, 3

Ох. Вот это странно. Мы ведь не устанавливали значения никаких переменных
в f2(). Эти значения — это «привидения», которые всё ещё в стеке.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

54
Загрузим пример в OllyDbg:

Рис. 1.6: OllyDbg: f1()
Когда f1() заполняет переменные a, b и c они сохраняются по адресу 0x1FF860,
итд.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

55
А когда исполняется f2():

Рис. 1.7: OllyDbg: f2()
... a, b и c в функции f2() находятся по тем же адресам! Пока никто не перезаписал их, так что они здесь в нетронутом виде. Для создания такой странной
ситуации несколько функций должны исполняться друг за другом и SP должен
быть одинаковым при входе в функции, т.е. у функций должно быть равное
количество аргументов). Тогда локальные переменные будут расположены в
том же месте стека. Подводя итоги, все значения в стеке (да и памяти вообще)
это значения оставшиеся от исполнения предыдущих функций. Строго говоря,
они не случайны, они скорее непредсказуемы. А как иначе? Можно было бы
очищать части стека перед исполнением каждой функции, но это слишком
много лишней (и ненужной) работы.
MSVC 2013
Этот пример был скомпилирован в MSVC 2010. Но один читатель этой книги
сделал попытку скомпилировать пример в MSVC 2013, запустил и увидел 3
числа в обратном порядке:
c:\Polygon\c>st
3, 2, 1

Почему? Я также попробовал скомпилировать этот пример в MSVC 2013 и увидел это:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

56
Листинг 1.44: MSVC 2013
_a$ = −12
_b$ = −8
_c$ = −4
_f2
PROC

; size = 4
; size = 4
; size = 4

...
_f2

ENDP

_c$ = −12
_b$ = −8
_a$ = −4
_f1
PROC

; size = 4
; size = 4
; size = 4

...
_f1

ENDP

В отличии от MSVC 2010, MSVC 2013 разместил переменные a/b/c в функции
f2() в обратном порядке. И это полностью корректно, потому что в стандартах
Си/Си++нет правила, в каком порядке локальные переменные должны быть
размещены в локальном стеке, если вообще. Разница есть из-за того что MSVC
2010 делает это одним способом, а в MSVC 2013, вероятно, что-то немного
изменили во внутренностях компилятора, так что он ведет себя слегка иначе.

1.9.5. Упражнения
• http://challenges.re/51
• http://challenges.re/52

1.10. Почти пустая ф-ция
Это фрагмент реального кода найденного в Boolector65 :
// forward declaration. the function is residing in some other module:
int boolector_main (int argc, char ∗∗argv);
// executable
int main (int argc, char ∗∗argv)
{
return boolector_main (argc, argv);
}

Зачем кому-то это делать? Не ясно, но можно предположить что boolector_main()
может быть скомпилирована в виде DLL или динамической библиотеки, и может вызываться во время тестов. Конечно, тесты могут подготовить переменные argc/argv так же, как это сделал бы CRT.
65 https://boolector.github.io/

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

57
Интересно, как это компилируется:
Листинг 1.45: Неоптимизирующий GCC 8.2 x64 (вывод на ассемблере)
main:
push
mov
sub
mov
mov
mov
mov
mov
mov
call
leave
ret

rbp
rbp, rsp
rsp, 16
DWORD PTR −4[rbp], edi
QWORD PTR −16[rbp], rsi
rdx, QWORD PTR −16[rbp]
eax, DWORD PTR −4[rbp]
rsi, rdx
edi, eax
boolector_main

Здесь у нас: пролог, ненужная (не соптимизированная) перетасовка двух аргументов, CALL, эпилог, RET. Посмотрим на оптимизированную версию:
Листинг 1.46: Оптимизирующий GCC 8.2 x64 (вывод на ассемблере)
main:
jmp

boolector_main

Вот так просто: стек/регистры остаются как есть, а ф-ция boolector_main()
имеет тот же набор аргументов. Так что всё что нужно, это просто передать
управление по другому адресу.
Это близко к thunk function.
Кое-что посложнее мы увидим позже: 1.11.2 (стр. 73), 1.21.1 (стр. 203).

1.11. printf() с несколькими аргументами
Попробуем теперь немного расширить пример Hello, world! (1.5 (стр. 12)), написав в теле функции main():
#include
int main()
{
printf("a=%d; b=%d; c=%d", 1, 2, 3);
return 0;
};

1.11.1. x86
x86: 3 целочисленных аргумента
MSVC

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

58
Компилируем при помощи MSVC 2010 Express, и в итоге получим:
$SG3830 DB

'a=%d; b=%d; c=%d', 00H

...
push
push
push
push
call
add

3
2
1
OFFSET $SG3830
_printf
esp, 16

Всё почти то же, за исключением того, что теперь видно, что аргументы для
printf() заталкиваются в стек в обратном порядке: самый первый аргумент
заталкивается последним.
Кстати, вспомним, что переменные типа int в 32-битной системе, как известно,
имеют ширину 32 бита, это 4 байта.
Итак, у нас всего 4 аргумента. 4 ∗ 4 = 16 — именно 16 байт занимают в стеке
указатель на строку плюс ещё 3 числа типа int.
Когда при помощи инструкции ADD ESP, X корректируется указатель стека
ESP после вызова какой-либо функции, зачастую можно сделать вывод о том,
сколько аргументов у вызываемой функции было, разделив X на 4.
Конечно, это относится только к cdecl-методу передачи аргументов через стек,
и только для 32-битной среды.
См. также в соответствующем разделе о способах передачи аргументов через
стек (6.1 (стр. 956)).
Иногда бывает так, что подряд идут несколько вызовов разных функций, но
стек корректируется только один раз, после последнего вызова:
push a1
push a2
call ...
...
push a1
call ...
...
push a1
push a2
push a3
call ...
add esp, 24

Вот пример из реальной жизни:
Листинг 1.47: x86
.text:100113E7
.text:100113E9
.text:100113EE

push
call
call

3
sub_100018B0 ; берет один аргумент (3)
sub_100019D0 ; не имеет аргументов вообще

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

59
.text:100113F3
.text:100113F8
.text:100113FA
.text:100113FF

call
push
call
add

sub_10006A90 ; не имеет аргументов вообще
1
sub_100018B0 ; берет один аргумент (1)
esp, 8
; выбрасывает из стека два аргумента

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

60
MSVC и OllyDbg
Попробуем этот же пример в OllyDbg. Это один из наиболее популярных win32отладчиков пользовательского режима. Мы можем компилировать наш пример в MSVC 2012 с опцией /MD что означает линковать с библиотекой MSVCR*.DLL,
чтобы импортируемые функции были хорошо видны в отладчике.
Затем загружаем исполняемый файл в OllyDbg. Самая первая точка останова в
ntdll.dll, нажмите F9 (запустить). Вторая точка останова в CRT-коде. Теперь
мы должны найти функцию main().
Найдите этот код, прокрутив окно кода до самого верха (MSVC располагает
функцию main() в самом начале секции кода):

Рис. 1.8: OllyDbg: самое начало функции main()
Кликните на инструкции PUSH EBP, нажмите F2 (установка точки останова) и
нажмите F9 (запустить). Нам нужно произвести все эти манипуляции, чтобы
пропустить CRT-код, потому что нам он пока не интересен.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

61
Нажмите F8 (сделать шаг, не входя в функцию) 6 раз, т.е. пропустить 6 инструкций:

Рис. 1.9: OllyDbg: перед исполнением printf()
Теперь PC указывает на инструкцию CALL printf. OllyDbg, как и другие отладчики, подсвечивает регистры со значениями, которые изменились. Поэтому
каждый раз когда мы нажимаем F8, EIP изменяется и его значение подсвечивается красным. ESP также меняется, потому что значения заталкиваются в
стек.
Где находятся эти значения в стеке? Посмотрите на правое нижнее окно в
отладчике:

Рис. 1.10: OllyDbg: стек с сохраненными значениями (красная рамка добавлена
в графическом редакторе)
Здесь видно 3 столбца: адрес в стеке, значение в стеке и ещё дополнительный
комментарий от OllyDbg. OllyDbg может находить указатели на ASCII-строки в
стеке, так что он показывает здесь printf()-строку.
Можно кликнуть правой кнопкой мыши на строке формата, кликнуть на «Follow
in dump» и строка формата появится в окне слева внизу, где всегда виден

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

62
какой-либо участок памяти. Эти значения в памяти можно редактировать. Можно изменить саму строку формата, и тогда результат работы нашего примера
будет другой. В данном случае пользы от этого немного, но для упражнения
это полезно, чтобы начать чувствовать как тут всё работает.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

63
Нажмите F8 (сделать шаг, не входя в функцию).
В консоли мы видим вывод:
a=1; b=2; c=3

Посмотрим как изменились регистры и состояние стека:

Рис. 1.11: OllyDbg после исполнения printf()
Регистр EAX теперь содержит 0xD (13). Всё верно: printf() возвращает количество выведенных символов. Значение EIP изменилось. Действительно, теперь
здесь адрес инструкции после CALL printf. Значения регистров ECX и EDX также изменились. Очевидно, внутренности функции printf() используют их для
каких-то своих нужд.
Очень важно то, что значение ESP не изменилось. И аргументы-значения в стеке также! Мы ясно видим здесь и строку формата и соответствующие ей 3
значения, они всё ещё здесь. Действительно, по соглашению вызовов cdecl,
вызываемая функция не возвращает ESP назад. Это должна делать вызывающая функция (caller).

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

64
Нажмите F8 снова, чтобы исполнилась инструкция ADD ESP, 10:

Рис. 1.12: OllyDbg: после исполнения инструкции ADD ESP, 10
ESP изменился, но значения всё ещё в стеке! Конечно, никому не нужно заполнять эти значения нулями или что-то в этом роде. Всё что выше указателя
стека (SP) это шум или мусор и не имеет особой ценности. Было бы очень затратно по времени очищать ненужные элементы стека, к тому же, никому это
и не нужно.
GCC
Скомпилируем то же самое в Linux при помощи GCC 4.4.1 и посмотрим на результат в IDA:
main

proc near

var_10
var_C
var_8
var_4

=
=
=
=

dword
dword
dword
dword

push
mov
and
sub
mov
mov
mov
mov
mov

ptr
ptr
ptr
ptr

−10h
−0Ch
−8
−4

ebp
ebp, esp
esp, 0FFFFFFF0h
esp, 10h
eax, offset aADBDCD ; "a=%d; b=%d; c=%d"
[esp+10h+var_4], 3
[esp+10h+var_8], 2
[esp+10h+var_C], 1
[esp+10h+var_10], eax

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

65
call
mov
leave
retn
endp

main

_printf
eax, 0

Можно сказать, что этот короткий код, созданный GCC, отличается от кода
MSVC только способом помещения значений в стек. Здесь GCC снова работает
со стеком напрямую без PUSH/POP.
GCC и GDB
Попробуем также этот пример и в GDB66 в Linux.
-g означает генерировать отладочную информацию в выходном исполняемом
файле.
$ gcc 1.c −g −o 1
$ gdb 1
GNU gdb (GDB) 7.6.1−ubuntu
...
Reading symbols from /home/dennis/polygon/1...done.

Листинг 1.48: установим точку останова на printf()
(gdb) b printf
Breakpoint 1 at 0x80482f0

Запускаем. У нас нет исходного кода функции, поэтому GDB не может его показать.
(gdb) run
Starting program: /home/dennis/polygon/1
Breakpoint 1, __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
29
printf.c: No such file or directory.

Выдать 10 элементов стека. Левый столбец — это адрес в стеке.
(gdb) x/10w $esp
0xbffff11c:
0x0804844a
0xbffff12c:
0x00000003
0xbffff13c:
0xb7e29905

0x080484f0
0x08048460
0x00000001

0x00000001
0x00000000

0x00000002
0x00000000

Самый первый элемент это RA (0x0804844a). Мы можем удостовериться в этом,
дизассемблируя память по этому адресу:
66 GNU

Debugger

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

66
(gdb) x/5i 0x0804844a
0x804844a : mov
$0x0,%eax
0x804844f : leave
0x8048450 : ret
0x8048451:
xchg
%ax,%ax
0x8048453:
xchg
%ax,%ax

Две инструкции XCHG это холостые инструкции, аналогичные NOP.
Второй элемент (0x080484f0) это адрес строки формата:
(gdb) x/s 0x080484f0
0x80484f0:
"a=%d; b=%d; c=%d"

Остальные 3 элемента (1, 2, 3) это аргументы функции printf(). Остальные
элементы это может быть и мусор в стеке, но могут быть и значения от других
функций, их локальные переменные, итд. Пока что мы можем игнорировать
их.
Исполняем «finish». Это значит исполнять все инструкции до самого конца
функции. В данном случае это означает исполнять до завершения printf().
(gdb) finish
Run till exit from #0 __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at ⤦
Ç printf.c:29
main () at 1.c:6
6
return 0;
Value returned is $2 = 13

GDB показывает, что вернула printf() в EAX (13). Это, так же как и в примере
с OllyDbg, количество напечатанных символов.
А ещё мы видим «return 0;» и что это выражение находится в файле 1.c в строке 6. Действительно, файл 1.c лежит в текущем директории и GDB находит
там эту строку. Как GDB знает, какая строка Си-кода сейчас исполняется? Компилятор, генерируя отладочную информацию, также сохраняет информацию
о соответствии строк в исходном коде и адресов инструкций. GDB это всё-таки
отладчик уровня исходных текстов.
Посмотрим регистры. 13 в EAX:
(gdb) info registers
eax
0xd
13
ecx
0x0
0
edx
0x0
0
ebx
0xb7fc0000
esp
0xbffff120
ebp
0xbffff138
esi
0x0
0
edi
0x0
0
eip
0x804844a
...

−1208221696
0xbffff120
0xbffff138

0x804844a

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

67
Попробуем дизассемблировать текущие инструкции. Стрелка указывает на инструкцию, которая будет исполнена следующей.
(gdb) disas
Dump of assembler code for function main:
0x0804841d :
push
%ebp
0x0804841e :
mov
%esp,%ebp
0x08048420 :
and
$0xfffffff0,%esp
0x08048423 :
sub
$0x10,%esp
0x08048426 :
movl
$0x3,0xc(%esp)
0x0804842e :
movl
$0x2,0x8(%esp)
0x08048436 :
movl
$0x1,0x4(%esp)
0x0804843e :
movl
$0x80484f0,(%esp)
0x08048445 :
call
0x80482f0
=> 0x0804844a :
mov
$0x0,%eax
0x0804844f :
leave
0x08048450 :
ret
End of assembler dump.

По умолчанию GDB показывает дизассемблированный листинг в формате AT&T.
Но можно также переключиться в формат Intel:
(gdb) set disassembly−flavor intel
(gdb) disas
Dump of assembler code for function main:
0x0804841d :
push
ebp
0x0804841e :
mov
ebp,esp
0x08048420 :
and
esp,0xfffffff0
0x08048423 :
sub
esp,0x10
0x08048426 :
mov
DWORD PTR [esp+0xc],0x3
0x0804842e :
mov
DWORD PTR [esp+0x8],0x2
0x08048436 :
mov
DWORD PTR [esp+0x4],0x1
0x0804843e :
mov
DWORD PTR [esp],0x80484f0
0x08048445 :
call
0x80482f0
=> 0x0804844a :
mov
eax,0x0
0x0804844f :
leave
0x08048450 :
ret
End of assembler dump.

Исполняем следующую строку Си/Си++-кода. GDB покажет закрывающуюся
скобку, означая, что это конец блока в функции.
(gdb) step
7
};

Посмотрим регистры после исполнения инструкции MOV EAX, 0. EAX здесь уже
действительно ноль.
(gdb) info registers
eax
0x0
0
ecx
0x0
0
edx
0x0
0
ebx
0xb7fc0000

−1208221696

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

68
esp
ebp
esi
edi
eip
...

0xbffff120
0xbffff138
0x0
0
0x0
0
0x804844f

0xbffff120
0xbffff138

0x804844f

x64: 8 целочисленных аргументов
Для того чтобы посмотреть, как остальные аргументы будут передаваться через стек, изменим пример ещё раз, увеличив количество передаваемых аргументов до 9 (строка формата printf() и 8 переменных типа int):
#include
int main()
{
printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n", 1, 2, 3,⤦
Ç 4, 5, 6, 7, 8);
return 0;
};

MSVC
Как уже было сказано ранее, первые 4 аргумента в Win64 передаются в регистрах RCX, RDX, R8, R9, а остальные — через стек. Здесь мы это и видим. Впрочем,
инструкция PUSH не используется, вместо неё при помощи MOV значения сразу
записываются в стек.
Листинг 1.49: MSVC 2012 x64
$SG2923 DB
main

'a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d', 0aH, 00H

PROC
sub

rsp, 88

mov
mov
mov
mov
mov
mov
mov
mov
lea
call

DWORD PTR [rsp+64], 8
DWORD PTR [rsp+56], 7
DWORD PTR [rsp+48], 6
DWORD PTR [rsp+40], 5
DWORD PTR [rsp+32], 4
r9d, 3
r8d, 2
edx, 1
rcx, OFFSET FLAT:$SG2923
printf

; возврат 0
xor
eax, eax
add

rsp, 88

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

69
main
_TEXT
END

ret
ENDP
ENDS

0

Наблюдательный читатель может спросить, почему для значений типа int отводится 8 байт, ведь нужно только 4? Да, это нужно запомнить: для значений
всех типов более коротких чем 64-бита, отводится 8 байт. Это сделано для
удобства: так всегда легко рассчитать адрес того или иного аргумента. К тому же, все они расположены по выровненным адресам в памяти. В 32-битных
средах точно также: для всех типов резервируется 4 байта в стеке.
GCC
В *NIX-системах для x86-64 ситуация похожая, вот только первые 6 аргументов передаются через RDI, RSI, RDX, RCX, R8, R9. Остальные — через стек. GCC
генерирует код, записывающий указатель на строку в EDI вместо RDI — это
мы уже рассмотрели чуть раньше: 1.5.2 (стр. 22).
Почему перед вызовом printf() очищается регистр EAX мы уже рассмотрели
ранее 1.5.2 (стр. 22).
Листинг 1.50: Оптимизирующий GCC 4.4.6 x64
.LC0:
.string "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n"
main:
sub

rsp, 40

mov
mov
mov
mov
mov
mov
xor
mov
mov
mov
call

r9d, 5
r8d, 4
ecx, 3
edx, 2
esi, 1
edi, OFFSET FLAT:.LC0
eax, eax ; количество переданных векторных регистров
DWORD PTR [rsp+16], 8
DWORD PTR [rsp+8], 7
DWORD PTR [rsp], 6
printf

; возврат 0
xor
add
ret

eax, eax
rsp, 40

GCC + GDB

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

70
Попробуем этот пример в GDB.
$ gcc −g 2.c −o 2
$ gdb 2
GNU gdb (GDB) 7.6.1−ubuntu
...
Reading symbols from /home/dennis/polygon/2...done.

Листинг 1.51: ставим точку останова на printf(), запускаем
(gdb) b printf
Breakpoint 1 at 0x400410
(gdb) run
Starting program: /home/dennis/polygon/2
Breakpoint 1, __printf (format=0x400628 "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d⤦
Ç ; g=%d; h=%d\n") at printf.c:29
29
printf.c: No such file or directory.

В регистрах RSI/RDX/RCX/R8/R9 всё предсказуемо. А RIP содержит адрес самой
первой инструкции функции printf().
(gdb) info registers
rax
0x0
0
rbx
0x0
0
rcx
0x3
3
rdx
0x2
2
rsi
0x1
1
rdi
0x400628 4195880
rbp
0x7fffffffdf60
rsp
0x7fffffffdf38
r8
0x4
4
r9
0x5
5
r10
0x7fffffffdce0
r11
0x7ffff7a65f60
r12
0x400440 4195392
r13
0x7fffffffe040
r14
0x0
0
r15
0x0
0
rip
0x7ffff7a65f60
...

0x7fffffffdf60
0x7fffffffdf38

140737488346336
140737348263776
140737488347200

0x7ffff7a65f60

Листинг 1.52: смотрим на строку формата
(gdb) x/s $rdi
0x400628:

"a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n"

Дампим стек на этот раз с командой x/g — g означает giant words, т.е. 64битные слова.
(gdb) x/10g $rsp
0x7fffffffdf38: 0x0000000000400576

0x0000000000000006

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

71
0x7fffffffdf48:
0x7fffffffdf58:
0x7fffffffdf68:
0x7fffffffdf78:

0x0000000000000007
0x0000000000000000
0x00007ffff7a33de5
0x00007fffffffe048

0x00007fff00000008
0x0000000000000000
0x0000000000000000
0x0000000100000000

Самый первый элемент стека, как и в прошлый раз, это RA. Через стек также
передаются 3 значения: 6, 7, 8. Видно, что 8 передается с неочищенной старшей 32-битной частью: 0x00007fff00000008. Это нормально, ведь передаются
числа типа int, а они 32-битные. Так что в старшей части регистра или памяти
стека остался «случайный мусор».
GDB показывает всю функцию main(), если попытаться посмотреть, куда вернется управление после исполнения printf().
(gdb) set disassembly−flavor intel
(gdb) disas 0x0000000000400576
Dump of assembler code for function main:
0x000000000040052d :
push
rbp
0x000000000040052e :
mov
rbp,rsp
0x0000000000400531 :
sub
rsp,0x20
0x0000000000400535 :
mov
DWORD PTR [rsp+0x10],0x8
0x000000000040053d :
mov
DWORD PTR [rsp+0x8],0x7
0x0000000000400545 :
mov
DWORD PTR [rsp],0x6
0x000000000040054c :
mov
r9d,0x5
0x0000000000400552 :
mov
r8d,0x4
0x0000000000400558 :
mov
ecx,0x3
0x000000000040055d :
mov
edx,0x2
0x0000000000400562 :
mov
esi,0x1
0x0000000000400567 :
mov
edi,0x400628
0x000000000040056c :
mov
eax,0x0
0x0000000000400571 :
call
0x400410
0x0000000000400576 :
mov
eax,0x0
0x000000000040057b :
leave
0x000000000040057c :
ret
End of assembler dump.

Заканчиваем исполнение printf(), исполняем инструкцию обнуляющую EAX,
удостоверяемся что в регистре EAX именно ноль. RIP указывает сейчас на инструкцию LEAVE, т.е. предпоследнюю в функции main().
(gdb) finish
Run till exit from #0 __printf (format=0x400628 "a=%d; b=%d; c=%d; d=%d; e⤦
Ç =%d; f=%d; g=%d; h=%d\n") at printf.c:29
a=1; b=2; c=3; d=4; e=5; f=6; g=7; h=8
main () at 2.c:6
6
return 0;
Value returned is $1 = 39
(gdb) next
7
};
(gdb) info registers
rax
0x0
0
rbx
0x0
0
rcx
0x26
38
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

72
rdx
rsi
rdi
rbp
rsp
r8
r9
r10
r11
r12
r13
r14
r15
rip
...

0x7ffff7dd59f0
140737351866864
0x7fffffd9
2147483609
0x0
0
0x7fffffffdf60
0x7fffffffdf60
0x7fffffffdf40
0x7fffffffdf40
0x7ffff7dd26a0
140737351853728
0x7ffff7a60134
140737348239668
0x7fffffffd5b0
140737488344496
0x7ffff7a95900
140737348458752
0x400440 4195392
0x7fffffffe040
140737488347200
0x0
0
0x0
0
0x40057b 0x40057b

1.11.2. ARM
ARM: 3 целочисленных аргумента
В ARM традиционно принята такая схема передачи аргументов в функцию: 4
первых аргумента через регистры R0-R3; а остальные — через стек. Это немного похоже на то, как аргументы передаются в fastcall (6.1.3 (стр. 958)) или
win64 (6.1.5 (стр. 960)).
32-битный ARM

Неоптимизирующий Keil 6/2013 (Режим ARM)
Листинг 1.53: Неоптимизирующий Keil 6/2013 (Режим ARM)
.text:00000000
.text:00000000
.text:00000004
.text:00000008
.text:0000000C
.text:00000010
.text:00000014
.text:00000018
.text:0000001C

main
10 40
03 30
02 20
01 10
08 00
06 00
00 00
10 80

2D
A0
A0
A0
8F
00
A0
BD

E9
E3
E3
E3
E2
EB
E3
E8

STMFD
MOV
MOV
MOV
ADR
BL
MOV
LDMFD

SP!, {R4,LR}
R3, #3
R2, #2
R1, #1
R0, aADBDCD
__2printf
R0, #0
SP!, {R4,PC}

; "a=%d; b=%d; c=%d"
; return 0

Итак, первые 4 аргумента передаются через регистры R0-R3, по порядку: указатель на формат-строку для printf() в R0, затем 1 в R1, 2 в R2 и 3 в R3.
Инструкция на 0x18 записывает 0 в R0 — это выражение в Си return 0. Пока что
здесь нет ничего необычного. Оптимизирующий Keil 6/2013 генерирует точно
такой же код.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

73
Оптимизирующий Keil 6/2013 (Режим Thumb)
Листинг 1.54: Оптимизирующий Keil 6/2013 (Режим Thumb)
.text:00000000
.text:00000000
.text:00000002
.text:00000004
.text:00000006
.text:00000008
.text:0000000A
.text:0000000E
.text:00000010

main
10 B5
03 23
02 22
01 21
02 A0
00 F0 0D F8
00 20
10 BD

PUSH
MOVS
MOVS
MOVS
ADR
BL
MOVS
POP

{R4,LR}
R3, #3
R2, #2
R1, #1
R0, aADBDCD
__2printf
R0, #0
{R4,PC}

; "a=%d; b=%d; c=%d"

Здесь нет особых отличий от неоптимизированного варианта для режима ARM.
Оптимизирующий Keil 6/2013 (Режим ARM) + убираем return
Немного переделаем пример, убрав return 0:
#include
void main()
{
printf("a=%d; b=%d; c=%d", 1, 2, 3);
};

Результат получится необычным:
Листинг 1.55: Оптимизирующий Keil 6/2013 (Режим ARM)
.text:00000014
.text:00000014
.text:00000018
.text:0000001C
.text:00000020
.text:00000024

main
03 30
02 20
01 10
1E 0E
CB 18

A0
A0
A0
8F
00

E3
E3
E3
E2
EA

MOV
MOV
MOV
ADR
B

R3, #3
R2, #2
R1, #1
R0, aADBDCD
__2printf

; "a=%d; b=%d; c=%d\n"

Это оптимизированная версия (-O3) для режима ARM, и здесь мы видим последнюю инструкцию B вместо привычной нам BL. Отличия между этой оптимизированной версией и предыдущей, скомпилированной без оптимизации, ещё и
в том, что здесь нет пролога и эпилога функции (инструкций, сохраняющих
состояние регистров R0 и LR). Инструкция B просто переходит на другой адрес, без манипуляций с регистром LR, то есть это аналог JMP в x86. Почему это
работает нормально? Потому что этот код эквивалентен предыдущему.
Основных причин две: 1) стек не модифицируется, как и указатель стека SP; 2)
вызов функции printf() последний, после него ничего не происходит. Функция printf(), отработав, просто возвращает управление по адресу, записанному в LR.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

74
Но в LR находится адрес места, откуда была вызвана наша функция! А следовательно, управление из printf() вернется сразу туда.
Значит нет нужды сохранять LR, потому что нет нужды модифицировать LR. А
нет нужды модифицировать LR, потому что нет иных вызовов функций, кроме
printf(), к тому же, после этого вызова не нужно ничего здесь больше делать!
Поэтому такая оптимизация возможна.
Эта оптимизация часто используется в функциях, где последнее выражение —
это вызов другой функции.
Ещё один похожий пример описан здесь: 1.21.1 (стр. 204).
ARM64

Неоптимизирующий GCC (Linaro) 4.9
Листинг 1.56: Неоптимизирующий GCC (Linaro) 4.9
.LC1:
.string "a=%d; b=%d; c=%d"
f2:
; сохранить FP и LR в стековом фрейме:
stp
x29, x30, [sp, −16]!
; установить стековый фрейм (FP=SP):
add
x29, sp, 0
adrp
x0, .LC1
add
x0, x0, :lo12:.LC1
mov
w1, 1
mov
w2, 2
mov
w3, 3
bl
printf
mov
w0, 0
; восстановить FP и LR
ldp
x29, x30, [sp], 16
ret

Итак, первая инструкция STP (Store Pair) сохраняет FP (X29) и LR (X30) в стеке.
Вторая инструкция ADD X29, SP, 0 формирует стековый фрейм. Это просто
запись значения SP в X29.
Далее уже знакомая пара инструкций ADRP/ADD формирует указатель на строку.
lo12 означает младшие 12 бит, т.е., линкер запишет младшие 12 бит адреса
метки LC1 в опкод инструкции ADD. 1, 2 и 3 это 32-битные значения типа int,
так что они загружаются в 32-битные части регистров 67
Оптимизирующий GCC (Linaro) 4.9 генерирует почти такой же код.
67 Изменение 1 на 1L изменит эту константу на 64-битную и она будет загружаться в 64-битный
регистр. См. о целочисленных константах/литералах: 1, 2.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

75
ARM: 8 целочисленных аргументов
Снова воспользуемся примером с 9-ю аргументами из предыдущей секции:
1.11.1 (стр. 68).
#include
int main()
{
printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n", 1, 2, 3,⤦
Ç 4, 5, 6, 7, 8);
return 0;
};

Оптимизирующий Keil 6/2013: Режим ARM
.text:00000028
main
.text:00000028
.text:00000028
var_18 = −0x18
.text:00000028
var_14 = −0x14
.text:00000028
var_4 = −4
.text:00000028
.text:00000028 04 E0 2D E5 STR
LR, [SP,#var_4]!
.text:0000002C 14 D0 4D E2 SUB
SP, SP, #0x14
.text:00000030 08 30 A0 E3 MOV
R3, #8
.text:00000034 07 20 A0 E3 MOV
R2, #7
.text:00000038 06 10 A0 E3 MOV
R1, #6
.text:0000003C 05 00 A0 E3 MOV
R0, #5
.text:00000040 04 C0 8D E2 ADD
R12, SP, #0x18+var_14
.text:00000044 0F 00 8C E8 STMIA R12, {R0−R3}
.text:00000048 04 00 A0 E3 MOV
R0, #4
.text:0000004C 00 00 8D E5 STR
R0, [SP,#0x18+var_18]
.text:00000050 03 30 A0 E3 MOV
R3, #3
.text:00000054 02 20 A0 E3 MOV
R2, #2
.text:00000058 01 10 A0 E3 MOV
R1, #1
.text:0000005C 6E 0F 8F E2 ADR
R0, aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d;
d=%d; e=%d; f=%d; g=%"...
.text:00000060 BC 18 00 EB BL
__2printf
.text:00000064 14 D0 8D E2 ADD
SP, SP, #0x14
.text:00000068 04 F0 9D E4 LDR
PC, [SP+4+var_4],#4

Этот код можно условно разделить на несколько частей:
• Пролог функции:
Самая первая инструкция STR LR, [SP,#var_4]! сохраняет в стеке LR, ведь
нам придется использовать этот регистр для вызова printf(). Восклицательный знак в конце означает pre-index. Это значит, что в начале SP
должно быть уменьшено на 4, затем по адресу в SP должно быть записано
значение LR.
Это аналог знакомой в x86 инструкции PUSH. Читайте больше об этом:
1.39.2 (стр. 565).
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

76
Вторая инструкция SUB SP, SP, #0x14 уменьшает указатель стека SP, но,
на самом деле, эта процедура нужна для выделения в локальном стеке
места размером 0x14 (20) байт. Действительно, нам нужно передать 5
32-битных значений через стек в printf(). Каждое значение занимает
4 байта, все вместе — 5 ∗ 4 = 20. Остальные 4 32-битных значения будут
переданы через регистры.
• Передача 5, 6, 7 и 8 через стек: они записываются в регистры R0, R1, R2 и
R3 соответственно.
Затем инструкция ADD R12, SP, #0x18+var_14 записывает в регистр R12
адрес места в стеке, куда будут помещены эти 4 значения. var_14 — это
макрос ассемблера, равный -0x14. Такие макросы создает IDA, чтобы удобнее было показывать, как код обращается к стеку.
Макросы var_?, создаваемые IDA, отражают локальные переменные в стеке. Так что в R12 будет записано SP+4.
Следующая инструкция STMIA R12, R0-R3 записывает содержимое регистров R0-R3 по адресу в памяти, на который указывает R12.
Инструкция STMIA означает Store Multiple Increment After.
Increment After означает, что R12 будет увеличиваться на 4 после записи
каждого значения регистра.
• Передача 4 через стек: 4 записывается в R0, затем инструкция STR R0,
[SP,#0x18+var_18] записывает его в стек. var_18 равен -0x18, смещение
будет 0, так что значение из регистра R0 (4) запишется туда, куда указывает SP.
• Передача 1, 2 и 3 через регистры:
Значения для первых трех чисел (a, b, c) (1, 2, 3 соответственно) передаются в регистрах R1, R2 и R3 перед самим вызовом printf().
• Вызов printf().
• Эпилог функции:
Инструкция ADD SP, SP, #0x14 возвращает SP на прежнее место, аннулируя таким образом всё, что было записано в стек. Конечно, то что было
записано в стек, там пока и останется, но всё это будет многократно перезаписано во время исполнения последующих функций.
Инструкция LDR PC, [SP+4+var_4],#4 загружает в PC сохраненное значение LR из стека, обеспечивая таким образом выход из функции.
Здесь нет восклицательного знака — действительно, сначала PC загружается из места, куда указывает SP (4 + var_4 = 4 + (−4) = 0, так что эта
инструкция аналогична LDR PC, [SP],#4), затем SP увеличивается на 4.
Это называется post-index68 . Почему IDA показывает инструкцию именно
так? Потому что она хочет показать разметку стека и тот факт, что var_4
выделена в локальном стеке именно для сохраненного значения LR. Эта
инструкция в каком-то смысле аналогична POP PC в x86 69 .
68 Читайте
69 В

больше об этом: 1.39.2 (стр. 565).
x86 невозможно установить значение IP/EIP/RIP используя POP, но будем надеяться, вы

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

77
Оптимизирующий Keil 6/2013: Режим Thumb
.text:0000001C
printf_main2
.text:0000001C
.text:0000001C
var_18 = −0x18
.text:0000001C
var_14 = −0x14
.text:0000001C
var_8 = −8
.text:0000001C
.text:0000001C 00 B5
PUSH
{LR}
.text:0000001E 08 23
MOVS
R3, #8
.text:00000020 85 B0
SUB
SP, SP, #0x14
.text:00000022 04 93
STR
R3, [SP,#0x18+var_8]
.text:00000024 07 22
MOVS
R2, #7
.text:00000026 06 21
MOVS
R1, #6
.text:00000028 05 20
MOVS
R0, #5
.text:0000002A 01 AB
ADD
R3, SP, #0x18+var_14
.text:0000002C 07 C3
STMIA
R3!, {R0−R2}
.text:0000002E 04 20
MOVS
R0, #4
.text:00000030 00 90
STR
R0, [SP,#0x18+var_18]
.text:00000032 03 23
MOVS
R3, #3
.text:00000034 02 22
MOVS
R2, #2
.text:00000036 01 21
MOVS
R1, #1
.text:00000038 A0 A0
ADR
R0, aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d;
d=%d; e=%d; f=%d; g=%"...
.text:0000003A 06 F0 D9 F8 BL
__2printf
.text:0000003E
.text:0000003E
loc_3E
; CODE XREF: example13_f+16
.text:0000003E 05 B0
ADD
SP, SP, #0x14
.text:00000040 00 BD
POP
{PC}

Это почти то же самое что и в предыдущем примере, только код для Thumb и
значения помещаются в стек немного иначе: сначала 8 за первый раз, затем 5,
6, 7 за второй раз и 4 за третий раз.
Оптимизирующий Xcode 4.6.3 (LLVM): Режим ARM
__text:0000290C
__text:0000290C
__text:0000290C
__text:0000290C
__text:0000290C
__text:0000290C
__text:00002910
__text:00002914
__text:00002918
__text:0000291C
__text:00002920
__text:00002924
__text:00002928

_printf_main2
var_1C = −0x1C
var_C = −0xC
80
0D
14
70
07
00
04
00

40
70
D0
05
C0
00
20
00

2D
A0
4D
01
A0
40
A0
8F

E9
E1
E2
E3
E3
E3
E3
E0

STMFD
MOV
SUB
MOV
MOV
MOVT
MOV
ADD

SP!, {R7,LR}
R7, SP
SP, SP, #0x14
R0, #0x1570
R12, #7
R0, #0
R2, #4
R0, PC, R0

поняли аналогию.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

78
__text:0000292C
__text:00002930
__text:00002934
__text:00002938
__text:0000293C
__text:00002940
__text:00002944
__text:00002948
__text:0000294C
__text:00002950
__text:00002954
__text:00002958

06
05
00
0A
08
01
02
03
10
A4
07
80

30
10
20
10
90
10
20
30
90
05
D0
80

A0
A0
8D
8D
A0
A0
A0
A0
8D
00
A0
BD

E3
E3
E5
E9
E3
E3
E3
E3
E5
EB
E1
E8

MOV
MOV
STR
STMFA
MOV
MOV
MOV
MOV
STR
BL
MOV
LDMFD

R3, #6
R1, #5
R2, [SP,#0x1C+var_1C]
SP, {R1,R3,R12}
R9, #8
R1, #1
R2, #2
R3, #3
R9, [SP,#0x1C+var_C]
_printf
SP, R7
SP!, {R7,PC}

Почти то же самое, что мы уже видели, за исключением того, что STMFA (Store
Multiple Full Ascending) — это синоним инструкции STMIB (Store Multiple Increment
Before). Эта инструкция увеличивает SP и только затем записывает в память
значение очередного регистра, но не наоборот.
Далее бросается в глаза то, что инструкции как будто бы расположены случайно. Например, значение в регистре R0 подготавливается в трех местах, по
адресам 0x2918, 0x2920 и 0x2928, когда это можно было бы сделать в одном
месте. Однако, у оптимизирующего компилятора могут быть свои доводы о
том, как лучше составлять инструкции друг с другом для лучшей эффективности исполнения. Процессор обычно пытается исполнять одновременно идущие друг за другом инструкции. К примеру, инструкции MOVT R0, #0 и ADD R0,
PC, R0 не могут быть исполнены одновременно, потому что обе инструкции
модифицируют регистр R0. А вот инструкции MOVT R0, #0 и MOV R2, #4 легко
можно исполнить одновременно, потому что эффекты от их исполнения никак
не конфликтуют друг с другом. Вероятно, компилятор старается генерировать
код именно таким образом там, где это возможно.
Оптимизирующий Xcode 4.6.3 (LLVM): Режим Thumb-2
__text:00002BA0
__text:00002BA0
__text:00002BA0
__text:00002BA0
__text:00002BA0
__text:00002BA0
__text:00002BA0
__text:00002BA2
__text:00002BA4
__text:00002BA6
__text:00002BAA
__text:00002BAE
__text:00002BB2
__text:00002BB4
__text:00002BB6
__text:00002BB8
__text:00002BBA
__text:00002BBE

_printf_main2
var_1C = −0x1C
var_18 = −0x18
var_C = −0xC
80
6F
85
41
4F
C0
04
78
06
05
0D
00

B5
46
B0
F2
F0
F2
22
44
23
21
F1
92

D8 20
07 0C
00 00

04 0E

PUSH
MOV
SUB
MOVW
MOV.W
MOVT.W
MOVS
ADD
MOVS
MOVS
ADD.W
STR

{R7,LR}
R7, SP
SP, SP, #0x14
R0, #0x12D8
R12, #7
R0, #0
R2, #4
R0, PC ; char *
R3, #6
R1, #5
LR, SP, #0x1C+var_18
R2, [SP,#0x1C+var_1C]

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

79
__text:00002BC0
__text:00002BC4
__text:00002BC8
__text:00002BCA
__text:00002BCC
__text:00002BCE
__text:00002BD2
__text:00002BD6
__text:00002BD8

4F
8E
01
02
03
CD
01
05
80

F0
E8
21
22
23
F8
F0
B0
BD

08 09
0A 10

10 90
0A EA

MOV.W
STMIA.W
MOVS
MOVS
MOVS
STR.W
BLX
ADD
POP

R9, #8
LR, {R1,R3,R12}
R1, #1
R2, #2
R3, #3
R9, [SP,#0x1C+var_C]
_printf
SP, SP, #0x14
{R7,PC}

Почти то же самое, что и в предыдущем примере, лишь за тем исключением,
что здесь используются Thumb/Thumb-2-инструкции.
ARM64

Неоптимизирующий GCC (Linaro) 4.9
Листинг 1.57: Неоптимизирующий GCC (Linaro) 4.9
.LC2:
.string "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n"
f3:
; выделить больше места в стеке:
sub
sp, sp, #32
; сохранить FP и LR в стековом фрейме:
stp
x29, x30, [sp,16]
; установить указатель фрейма (FP=SP+16):
add
x29, sp, 16
adrp
x0, .LC2 ; "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n"
add
x0, x0, :lo12:.LC2
mov
w1, 8
; 9-й аргумент
str
w1, [sp]
; сохранить 9-й аргумент в стеке
mov
w1, 1
mov
w2, 2
mov
w3, 3
mov
w4, 4
mov
w5, 5
mov
w6, 6
mov
w7, 7
bl
printf
sub
sp, x29, #16
; восстановить FP и LR
ldp
x29, x30, [sp,16]
add
sp, sp, 32
ret

Первые 8 аргументов передаются в X- или W-регистрах: [Procedure Call Standard
for the ARM 64-bit Architecture (AArch64), (2013)]70 . Указатель на строку требует
70 Также
доступно здесь:
IHI0055B_aapcs64.pdf

http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

80
64-битного регистра, так что он передается в X0. Все остальные значения имеют 32-битный тип int, так что они записываются в 32-битные части регистров
(W-). Девятый аргумент (8) передается через стек. Действительно, невозможно передать большое количество аргументов в регистрах, потому что количество регистров ограничено.
Оптимизирующий GCC (Linaro) 4.9 генерирует почти такой же код.

1.11.3. MIPS
3 целочисленных аргумента
Оптимизирующий GCC 4.4.5
Главное отличие от примера «Hello, world!» в том, что здесь на самом деле
вызывается printf() вместо puts() и ещё три аргумента передаются в регистрах $5…$7 (или $A0…$A2). Вот почему эти регистры имеют префикс A-. Это
значит, что они используются для передачи аргументов.
Листинг 1.58: Оптимизирующий GCC 4.4.5 (вывод на ассемблере)
$LC0:
.ascii "a=%d; b=%d; c=%d\000"
main:
; пролог функции:
lui
$28,%hi(__gnu_local_gp)
addiu
$sp,$sp,−32
addiu
$28,$28,%lo(__gnu_local_gp)
sw
$31,28($sp)
; загрузить адрес printf():
lw
$25,%call16(printf)($28)
; загрузить адрес текстовой строки и установить первый аргумент printf():
lui
$4,%hi($LC0)
addiu
$4,$4,%lo($LC0)
; установить второй аргумент printf():
li
$5,1
# 0x1
; установить третий аргумент printf():
li
$6,2
# 0x2
; вызов printf():
jalr
$25
; установить четвертый аргумент printf() (branch delay slot):
li
$7,3
# 0x3
; эпилог функции:
lw
$31,28($sp)
; установить возвращаемое значение в 0:
move
$2,$0
; возврат
j
$31
addiu
$sp,$sp,32 ; branch delay slot

Листинг 1.59: Оптимизирующий GCC 4.4.5 (IDA)
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

81
.text:00000000 main:
.text:00000000
.text:00000000 var_10
= −0x10
.text:00000000 var_4
= −4
.text:00000000
; пролог функции:
.text:00000000
lui
$gp, (__gnu_local_gp >> 16)
.text:00000004
addiu
$sp, −0x20
.text:00000008
la
$gp, (__gnu_local_gp & 0xFFFF)
.text:0000000C
sw
$ra, 0x20+var_4($sp)
.text:00000010
sw
$gp, 0x20+var_10($sp)
; загрузить адрес printf():
.text:00000014
lw
$t9, (printf & 0xFFFF)($gp)
; загрузить адрес текстовой строки и установить первый аргумент printf():
.text:00000018
la
$a0, $LC0
# "a=%d; b=%d; c=%d"
; установить второй аргумент printf():
.text:00000020
li
$a1, 1
; установить третий аргумент printf():
.text:00000024
li
$a2, 2
; вызов printf():
.text:00000028
jalr
$t9
; установить четвертый аргумент printf() (branch delay slot):
.text:0000002C
li
$a3, 3
; эпилог функции:
.text:00000030
lw
$ra, 0x20+var_4($sp)
; установить возвращаемое значение в 0:
.text:00000034
move
$v0, $zero
; возврат
.text:00000038
jr
$ra
.text:0000003C
addiu
$sp, 0x20 ; branch delay slot

IDA объединила пару инструкций LUI и ADDIU в одну псевдоинструкцию LA. Вот
почему здесь нет инструкции по адресу 0x1C: потому что LA занимает 8 байт.
Неоптимизирующий GCC 4.4.5
Неоптимизирующий GCC более многословен:
Листинг 1.60: Неоптимизирующий GCC 4.4.5 (вывод на ассемблере)
$LC0:
.ascii "a=%d; b=%d; c=%d\000"
main:
; пролог функции:
addiu
$sp,$sp,−32
sw
$31,28($sp)
sw
$fp,24($sp)
move
$fp,$sp
lui
$28,%hi(__gnu_local_gp)
addiu
$28,$28,%lo(__gnu_local_gp)
; загрузить адрес текстовой строки:
lui
$2,%hi($LC0)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

82
;
;
;
;
;

;

addiu
$2,$2,%lo($LC0)
установить первый аргумент printf():
move
$4,$2
установить второй аргумент printf():
li
$5,1
# 0x1
установить третий аргумент printf():
li
$6,2
# 0x2
установить четвертый аргумент printf():
li
$7,3
# 0x3
получить адрес printf():
lw
$2,%call16(printf)($28)
nop
вызов printf():
move
$25,$2
jalr
$25
nop

; эпилог функции:
lw
$28,16($fp)
; установить возвращаемое значение в 0:
move
$2,$0
move
$sp,$fp
lw
$31,28($sp)
lw
$fp,24($sp)
addiu
$sp,$sp,32
; возврат
j
$31
nop

Листинг 1.61: Неоптимизирующий GCC 4.4.5 (IDA)
.text:00000000 main:
.text:00000000
.text:00000000 var_10
= −0x10
.text:00000000 var_8
= −8
.text:00000000 var_4
= −4
.text:00000000
; пролог функции:
.text:00000000
addiu
$sp,
.text:00000004
sw
$ra,
.text:00000008
sw
$fp,
.text:0000000C
move
$fp,
.text:00000010
la
$gp,
.text:00000018
sw
$gp,
; загрузить адрес текстовой строки:
.text:0000001C
la
$v0,
; установить первый аргумент printf():
.text:00000024
move
$a0,
; установить второй аргумент printf():
.text:00000028
li
$a1,
; установить третий аргумент printf():
.text:0000002C
li
$a2,
; установить четвертый аргумент printf():
.text:00000030
li
$a3,

−0x20
0x20+var_4($sp)
0x20+var_8($sp)
$sp
__gnu_local_gp
0x20+var_10($sp)
aADBDCD

# "a=%d; b=%d; c=%d"

$v0
1
2
3

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

83
; получить адрес printf():
.text:00000034
lw
$v0, (printf & 0xFFFF)($gp)
.text:00000038
or
$at, $zero
; вызов printf():
.text:0000003C
move
$t9, $v0
.text:00000040
jalr
$t9
.text:00000044
or
$at, $zero ; NOP
; эпилог функции:
.text:00000048
lw
$gp, 0x20+var_10($fp)
; установить возвращаемое значение в 0:
.text:0000004C
move
$v0, $zero
.text:00000050
move
$sp, $fp
.text:00000054
lw
$ra, 0x20+var_4($sp)
.text:00000058
lw
$fp, 0x20+var_8($sp)
.text:0000005C
addiu
$sp, 0x20
; возврат
.text:00000060
jr
$ra
.text:00000064
or
$at, $zero ; NOP

8 целочисленных аргументов
Снова воспользуемся примером с 9-ю аргументами из предыдущей секции:
1.11.1 (стр. 68).
#include
int main()
{
printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n", 1, 2, 3,⤦
Ç 4, 5, 6, 7, 8);
return 0;
};

Оптимизирующий GCC 4.4.5
Только 4 первых аргумента передаются в регистрах $A0 …$A3, так что остальные передаются через стек.
Это соглашение о вызовах O32 (самое популярное в мире MIPS). Другие соглашения о вызовах, или вручную написанный код на ассемблере, могут наделять
регистры другими функциями.
SW означает «Store Word» (записать слово из регистра в память). В MIPS нет инструкции для записи значения в память, так что для этого используется пара
инструкций (LI/SW).
Листинг 1.62: Оптимизирующий GCC 4.4.5 (вывод на ассемблере)
$LC0:
.ascii

"a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\012\000"

main:
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

84
; пролог функции:
lui
$28,%hi(__gnu_local_gp)
addiu
$sp,$sp,−56
addiu
$28,$28,%lo(__gnu_local_gp)
sw
$31,52($sp)
; передать 5-й аргумент в стеке:
li
$2,4
# 0x4
sw
$2,16($sp)
; передать 6-й аргумент в стеке:
li
$2,5
# 0x5
sw
$2,20($sp)
; передать 7-й аргумент в стеке:
li
$2,6
# 0x6
sw
$2,24($sp)
; передать 8-й аргумент в стеке:
li
$2,7
# 0x7
lw
$25,%call16(printf)($28)
sw
$2,28($sp)
; передать 1-й аргумент в $a0:
lui
$4,%hi($LC0)
; передать 9-й аргумент в стеке:
li
$2,8
# 0x8
sw
$2,32($sp)
addiu
$4,$4,%lo($LC0)
; передать 2-й аргумент в $a1:
li
$5,1
# 0x1
; передать 3-й аргумент в $a2:
li
$6,2
# 0x2
; вызов printf():
jalr
$25
; передать 4-й аргумент в $a3 (branch delay slot):
li
$7,3
# 0x3
; эпилог функции:
lw
$31,52($sp)
; установить возвращаемое значение в 0:
move
$2,$0
; возврат
j
$31
addiu
$sp,$sp,56 ; branch delay slot

Листинг 1.63: Оптимизирующий GCC 4.4.5 (IDA)
.text:00000000 main:
.text:00000000
.text:00000000 var_28
.text:00000000 var_24
.text:00000000 var_20
.text:00000000 var_1C
.text:00000000 var_18
.text:00000000 var_10
.text:00000000 var_4
.text:00000000
; пролог функции:

=
=
=
=
=
=
=

−0x28
−0x24
−0x20
−0x1C
−0x18
−0x10
−4

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

85
.text:00000000
lui
$gp,
.text:00000004
addiu
$sp,
.text:00000008
la
$gp,
.text:0000000C
sw
$ra,
.text:00000010
sw
$gp,
; передать 5-й аргумент в стеке:
.text:00000014
li
$v0,
.text:00000018
sw
$v0,
; передать 6-й аргумент в стеке:
.text:0000001C
li
$v0,
.text:00000020
sw
$v0,
; передать 7-й аргумент в стеке:
.text:00000024
li
$v0,
.text:00000028
sw
$v0,
; передать 8-й аргумент в стеке:
.text:0000002C
li
$v0,
.text:00000030
lw
$t9,
.text:00000034
sw
$v0,
; готовить 1-й аргумент в $a0:
.text:00000038
lui
$a0,
c=%d; d=%d; e=%d; f=%d; g=%"...
; передать 9-й аргумент в стеке:
.text:0000003C
li
$v0,
.text:00000040
sw
$v0,
; передать 1-й аргумент в $a0:
.text:00000044
la
$a0,
c=%d; d=%d; e=%d; f=%d; g=%"...
; передать 2-й аргумент в $a1:
.text:00000048
li
$a1,
; передать 3-й аргумент в $a2:
.text:0000004C
li
$a2,
; вызов printf():
.text:00000050
jalr
$t9
; передать 4-й аргумент в $a3 (branch delay
.text:00000054
li
$a3,
; эпилог функции:
.text:00000058
lw
$ra,
; установить возвращаемое значение в 0:
.text:0000005C
move
$v0,
; возврат
.text:00000060
jr
$ra
.text:00000064
addiu
$sp,

(__gnu_local_gp >> 16)
−0x38
(__gnu_local_gp & 0xFFFF)
0x38+var_4($sp)
0x38+var_10($sp)
4
0x38+var_28($sp)
5
0x38+var_24($sp)
6
0x38+var_20($sp)
7
(printf & 0xFFFF)($gp)
0x38+var_1C($sp)
($LC0 >> 16)

# "a=%d; b=%d;

8
0x38+var_18($sp)
($LC0 & 0xFFFF)

# "a=%d; b=%d;

1
2

slot):
3
0x38+var_4($sp)
$zero

0x38 ; branch delay slot

Неоптимизирующий GCC 4.4.5
Неоптимизирующий GCC более многословен:
Листинг 1.64: Неоптимизирующий GCC 4.4.5 (вывод на ассемблере)
$LC0:
.ascii "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\012\000"
main:
; пролог функции:
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

86

;

;

;

;

;

;
;
;
;
;

;
;

;

addiu
$sp,$sp,−56
sw
$31,52($sp)
sw
$fp,48($sp)
move
$fp,$sp
lui
$28,%hi(__gnu_local_gp)
addiu
$28,$28,%lo(__gnu_local_gp)
lui
$2,%hi($LC0)
addiu
$2,$2,%lo($LC0)
передать 5-й аргумент в стеке:
li
$3,4
# 0x4
sw
$3,16($sp)
передать 6-й аргумент в стеке:
li
$3,5
# 0x5
sw
$3,20($sp)
передать 7-й аргумент в стеке:
li
$3,6
# 0x6
sw
$3,24($sp)
передать 8-й аргумент в стеке:
li
$3,7
# 0x7
sw
$3,28($sp)
передать 9-й аргумент в стеке:
li
$3,8
# 0x8
sw
$3,32($sp)
передать 1-й аргумент в $a0:
move
$4,$2
передать 2-й аргумент в $a1:
li
$5,1
# 0x1
передать 3-й аргумент в $a2:
li
$6,2
# 0x2
передать 4-й аргумент в $a3:
li
$7,3
# 0x3
вызов printf():
lw
$2,%call16(printf)($28)
nop
move
$25,$2
jalr
$25
nop
эпилог функции:
lw
$28,40($fp)
установить возвращаемое значение в 0:
move
$2,$0
move
$sp,$fp
lw
$31,52($sp)
lw
$fp,48($sp)
addiu
$sp,$sp,56
возврат
j
$31
nop

Листинг 1.65: Неоптимизирующий GCC 4.4.5 (IDA)
.text:00000000 main:
.text:00000000
.text:00000000 var_28

= −0x28

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

87
.text:00000000 var_24
= −0x24
.text:00000000 var_20
= −0x20
.text:00000000 var_1C
= −0x1C
.text:00000000 var_18
= −0x18
.text:00000000 var_10
= −0x10
.text:00000000 var_8
= −8
.text:00000000 var_4
= −4
.text:00000000
; пролог функции:
.text:00000000
addiu
$sp,
.text:00000004
sw
$ra,
.text:00000008
sw
$fp,
.text:0000000C
move
$fp,
.text:00000010
la
$gp,
.text:00000018
sw
$gp,
.text:0000001C
la
$v0,
c=%d; d=%d; e=%d; f=%d; g=%"...
; передать 5-й аргумент в стеке:
.text:00000024
li
$v1,
.text:00000028
sw
$v1,
; передать 6-й аргумент в стеке:
.text:0000002C
li
$v1,
.text:00000030
sw
$v1,
; передать 7-й аргумент в стеке:
.text:00000034
li
$v1,
.text:00000038
sw
$v1,
; передать 8-й аргумент в стеке:
.text:0000003C
li
$v1,
.text:00000040
sw
$v1,
; передать 9-й аргумент в стеке:
.text:00000044
li
$v1,
.text:00000048
sw
$v1,
; передать 1-й аргумент в $a0:
.text:0000004C
move
$a0,
; передать 2-й аргумент в $a1:
.text:00000050
li
$a1,
; передать 3-й аргумент в $a2:
.text:00000054
li
$a2,
; передать 4-й аргумент в $a3:
.text:00000058
li
$a3,
; вызов printf():
.text:0000005C
lw
$v0,
.text:00000060
or
$at,
.text:00000064
move
$t9,
.text:00000068
jalr
$t9
.text:0000006C
or
$at,
; эпилог функции:
.text:00000070
lw
$gp,
; установить возвращаемое значение в 0:
.text:00000074
move
$v0,
.text:00000078
move
$sp,
.text:0000007C
lw
$ra,
.text:00000080
lw
$fp,
.text:00000084
addiu
$sp,

−0x38
0x38+var_4($sp)
0x38+var_8($sp)
$sp
__gnu_local_gp
0x38+var_10($sp)
aADBDCDDDEDFDGD # "a=%d; b=%d;
4
0x38+var_28($sp)
5
0x38+var_24($sp)
6
0x38+var_20($sp)
7
0x38+var_1C($sp)
8
0x38+var_18($sp)
$v0
1
2
3
(printf & 0xFFFF)($gp)
$zero
$v0
$zero ; NOP
0x38+var_10($fp)
$zero
$fp
0x38+var_4($sp)
0x38+var_8($sp)
0x38

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

88
; возврат
.text:00000088
.text:0000008C

jr
or

$ra
$at, $zero ; NOP

1.11.4. Вывод
Вот примерный скелет вызова функции:
Листинг 1.66: x86
...
PUSH третий аргумент
PUSH второй аргумент
PUSH первый аргумент
CALL функция
; модифицировать указатель стека (если нужно)

Листинг 1.67: x64 (MSVC)
MOV RCX, первый аргумент
MOV RDX, второй аргумент
MOV R8, третий аргумент
MOV R9, 4-й аргумент
...
PUSH 5-й, 6-й аргумент, и т.д. (если нужно)
CALL функция
; модифицировать указатель стека (если нужно)

Листинг 1.68: x64 (GCC)
MOV RDI, первый аргумент
MOV RSI, второй аргумент
MOV RDX, третий аргумент
MOV RCX, 4-й аргумент
MOV R8, 5-й аргумент
MOV R9, 6-й аргумент
...
PUSH 7-й, 8-й аргумент, и т.д. (если нужно)
CALL функция
; модифицировать указатель стека (если нужно)

Листинг 1.69: ARM
MOV R0, первый аргумент
MOV R1, второй аргумент
MOV R2, третий аргумент
MOV R3, 4-й аргумент
; передать 5-й, 6-й аргумент, и т.д., в стеке (если нужно)
BL функция
; модифицировать указатель стека (если нужно)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

89
Листинг 1.70: ARM64
MOV X0, первый аргумент
MOV X1, второй аргумент
MOV X2, третий аргумент
MOV X3, 4-й аргумент
MOV X4, 5-й аргумент
MOV X5, 6-й аргумент
MOV X6, 7-й аргумент
MOV X7, 8-й аргумент
; передать 9-й, 10-й аргумент, и т.д., в стеке (если нужно)
BL функция
; модифицировать указатель стека (если нужно)

Листинг 1.71: MIPS (соглашение о вызовах O32)
LI \$4, первый аргумент ; AKA $A0
LI \$5, второй аргумент ; AKA $A1
LI \$6, третий аргумент ; AKA $A2
LI \$7, 4-й аргумент ; AKA $A3
; передать 5-й, 6-й аргумент, и т.д., в стеке (если нужно)
LW temp_reg, адрес функции
JALR temp_reg

1.11.5. Кстати
Кстати, разница между способом передачи параметров принятая в x86, x64,
fastcall, ARM и MIPS неплохо иллюстрирует тот важный момент, что процессору, в общем, всё равно, как будут передаваться параметры функций. Можно
создать компилятор, который будет передавать их при помощи указателя на
структуру с параметрами, не пользуясь стеком вообще.
Регистры $A0…$A3 в MIPS так названы только для удобства (это соглашение
о вызовах O32). Программисты могут использовать любые другие регистры
(может быть, только кроме $ZERO) для передачи данных или любое другое
соглашение о вызовах.
CPU не знает о соглашениях о вызовах вообще.
Можно также вспомнить, что начинающие программисты на ассемблере передают параметры в другие функции обычно через регистры, без всякого явного
порядка, или даже через глобальные переменные. И всё это нормально работает.

1.12. scanf()
Теперь попробуем использовать scanf().

1.12.1. Простой пример

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

90
#include
int main()
{
int x;
printf ("Enter X:\n");
scanf ("%d", &x);
printf ("You entered %d...\n", x);
return 0;
};

Использовать scanf() в наши времена для того, чтобы спросить у пользователя что-то — не самая хорошая идея. Но так мы проиллюстрируем передачу
указателя на переменную типа int.
Об указателях
Это одна из фундаментальных вещей в программировании. Часто большой массив, структуру или объект передавать в другую функцию путем копирования
данных невыгодно, а передать адрес массива, структуры или объекта куда
проще. Например, если вы собираетесь вывести в консоль текстовую строку,
достаточно только передать её адрес в ядро ОС.
К тому же, если вызываемая функция (callee) должна изменить что-то в этом
большом массиве или структуре, то возвращать её полностью так же абсурдно. Так что самое простое, что можно сделать, это передать в функцию-callee
адрес массива или структуры, и пусть callee что-то там изменит.
Указатель в Си/Си++— это просто адрес какого-либо места в памяти.
В x86 адрес представляется в виде 32-битного числа (т.е. занимает 4 байта), а
в x86-64 как 64-битное число (занимает 8 байт). Кстати, отсюда негодование
некоторых людей, связанное с переходом на x86-64 — на этой архитектуре
все указатели занимают в 2 раза больше места, в том числе и в “дорогой” кэшпамяти.
При некотором упорстве можно работать только с безтиповыми указателями
(void*), например, стандартная функция Си memcpy(), копирующая блок из одного места памяти в другое принимает на вход 2 указателя типа void*, потому
что нельзя заранее предугадать, какого типа блок вы собираетесь копировать.
Для копирования тип данных не важен, важен только размер блока.
Также указатели широко используются, когда функции нужно вернуть более
одного значения (мы ещё вернемся к этому в будущем (3.21 (стр. 779)) ).
Функция scanf()—это как раз такой случай.
Помимо того, что этой функции нужно показать, сколько значений было прочитано успешно, ей ещё и нужно вернуть сами значения.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

91
Тип указателя в Си/Си++нужен только для проверки типов на стадии компиляции.
Внутри, в скомпилированном коде, никакой информации о типах указателей
нет вообще.
x86
MSVC
Что получаем на ассемблере, компилируя в MSVC 2010:
CONST
SEGMENT
$SG3831
DB
'Enter X:', 0aH, 00H
$SG3832
DB
'%d', 00H
$SG3833
DB
'You entered %d...', 0aH, 00H
CONST
ENDS
PUBLIC
_main
EXTRN
_scanf:PROC
EXTRN
_printf:PROC
; Function compile flags: /Odtp
_TEXT
SEGMENT
_x$ = −4
; size = 4
_main
PROC
push
ebp
mov
ebp, esp
push
ecx
push
OFFSET $SG3831 ; 'Enter X:'
call
_printf
add
esp, 4
lea
eax, DWORD PTR _x$[ebp]
push
eax
push
OFFSET $SG3832 ; '%d'
call
_scanf
add
esp, 8
mov
ecx, DWORD PTR _x$[ebp]
push
ecx
push
OFFSET $SG3833 ; 'You entered %d...'
call
_printf
add
esp, 8
; возврат 0
xor
eax, eax
mov
esp, ebp
pop
ebp
ret
0
_main
ENDP
_TEXT
ENDS

Переменная x является локальной.
По стандарту Си/Си++она доступна только из этой же функции и нигде более. Так получилось, что локальные переменные располагаются в стеке. МоЕсли вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

92
жет быть, можно было бы использовать и другие варианты, но в x86 это традиционно так.
Следующая после пролога инструкция PUSH ECX не ставит своей целью сохранить значение регистра ECX. (Заметьте отсутствие соответствующей инструкции POP ECX в конце функции).
Она на самом деле выделяет в стеке 4 байта для хранения x в будущем.
Доступ к x будет осуществляться при помощи объявленного макроса _x$ (он
равен -4) и регистра EBP указывающего на текущий фрейм.
Во всё время исполнения функции EBP указывает на текущий фрейм и через
EBP+смещение можно получить доступ как к локальным переменным функции,
так и аргументам функции.
Можно было бы использовать ESP, но он во время исполнения функции часто
меняется, а это не удобно. Так что можно сказать, что EBP это замороженное
состояние ESP на момент начала исполнения функции.
Разметка типичного стекового фрейма в 32-битной среде:

EBP-8
EBP-4
EBP
EBP+4
EBP+8
EBP+0xC
EBP+0x10



локальная переменная #2, маркируется в IDA как var_8
локальная переменная #1, маркируется в IDA как var_4
сохраненное значение EBP
адрес возврата
аргумент#1, маркируется в IDA как arg_0
аргумент#2, маркируется в IDA как arg_4
аргумент#3, маркируется в IDA как arg_8


У функции scanf() в нашем примере два аргумента.
Первый — указатель на строку, содержащую %d и второй — адрес переменной
x.
Вначале адрес x помещается в регистр EAX при помощи инструкции lea eax,
DWORD PTR _x$[ebp].
Инструкция LEA означает load effective address, и часто используется для формирования адреса чего-либо (.1.6 (стр. 1308)).
Можно сказать, что в данном случае LEA просто помещает в EAX результат
суммы значения в регистре EBP и макроса _x$.
Это тоже что и lea eax, [ebp-4].
Итак, от значения EBP отнимается 4 и помещается в EAX. Далее значение EAX
заталкивается в стек и вызывается scanf().
После этого вызывается printf(). Первый аргумент вызова строка: You entered
%d...\n.
Второй аргумент: mov ecx, [ebp-4]. Эта инструкция помещает в ECX не адрес
переменной x, а её значение.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

93
Далее значение ECX заталкивается в стек и вызывается printf().

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

94
MSVC + OllyDbg
Попробуем этот же пример в OllyDbg. Загружаем, нажимаем F8 (сделать шаг,
не входя в функцию) до тех пор, пока не окажемся в своем исполняемом файле,
а не в ntdll.dll. Прокручиваем вверх до тех пор, пока не найдем main(). Щелкаем на первой инструкции (PUSH EBP), нажимаем F2 (set a breakpoint), затем
F9 (Run) и точка останова срабатывает на начале main().
Трассируем до того места, где готовится адрес переменной x:

Рис. 1.13: OllyDbg: вычисляется адрес локальной переменной
На EAX в окне регистров можно нажать правой кнопкой и далее выбрать «Follow
in stack». Этот адрес покажется в окне стека.
Смотрите, это переменная в локальном стеке. Там дорисована красная стрелка. И там сейчас какой-то мусор (0x6E494714). Адрес этого элемента стека сейчас, при помощи PUSH запишется в этот же стек рядом. Трассируем при помощи F8 вплоть до конца исполнения scanf(). А пока scanf() исполняется, в
консольном окне, вводим, например, 123:
Enter X:
123

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

95
Вот тут scanf() отработал:

Рис. 1.14: OllyDbg: scanf() исполнилась
scanf() вернул 1 в EAX, что означает, что он успешно прочитал одно значение.
В наблюдаемом нами элементе стека теперь 0x7B (123).

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

96
Чуть позже это значение копируется из стека в регистр ECX и передается в
printf():

Рис. 1.15: OllyDbg: готовим значение для передачи в printf()
GCC
Попробуем тоже самое скомпилировать в Linux при помощи GCC 4.4.1:
main

proc near

var_20
var_1C
var_4

= dword ptr −20h
= dword ptr −1Ch
= dword ptr −4
push
mov
and
sub
mov
call
mov
lea
mov
mov
call
mov
mov
mov
mov
call
mov

ebp
ebp, esp
esp, 0FFFFFFF0h
esp, 20h
[esp+20h+var_20], offset aEnterX ; "Enter X:"
_puts
eax, offset aD ; "%d"
edx, [esp+20h+var_4]
[esp+20h+var_1C], edx
[esp+20h+var_20], eax
___isoc99_scanf
edx, [esp+20h+var_4]
eax, offset aYouEnteredD___ ; "You entered %d...\n"
[esp+20h+var_1C], edx
[esp+20h+var_20], eax
_printf
eax, 0

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

97
leave
retn
endp

main

GCC заменил первый вызов printf() на puts(). Почему это было сделано, уже
было описано ранее (1.5.3 (стр. 28)).
Далее всё как и прежде — параметры заталкиваются через стек при помощи
MOV.
Кстати
Этот простой пример иллюстрирует то обстоятельство, что компилятор преобразует список выражений в Си/Си++-блоке просто в последовательный набор
инструкций. Между выражениями в Си/Си++ничего нет, и в итоговом машинном коде между ними тоже ничего нет, управление переходит от одной инструкции к следующей за ней.
x64
Всё то же самое, только используются регистры вместо стека для передачи
аргументов функций.
MSVC
Листинг 1.72: MSVC 2012 x64
_DATA
$SG1289
$SG1291
$SG1292
_DATA

SEGMENT
DB
'Enter X:', 0aH, 00H
DB
'%d', 00H
DB
'You entered %d...', 0aH, 00H
ENDS

_TEXT
SEGMENT
x$ = 32
main
PROC
$LN3:
sub
lea
call
lea
lea
call
mov
lea
call

rsp, 56
rcx, OFFSET FLAT:$SG1289 ; 'Enter X:'
printf
rdx, QWORD PTR x$[rsp]
rcx, OFFSET FLAT:$SG1291 ; '%d'
scanf
edx, DWORD PTR x$[rsp]
rcx, OFFSET FLAT:$SG1292 ; 'You entered %d...'
printf

; возврат 0
xor
eax, eax
add
rsp, 56
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

98
main
_TEXT

ret
ENDP
ENDS

0

GCC
Листинг 1.73: Оптимизирующий GCC 4.4.6 x64
.LC0:
.string "Enter X:"
.LC1:
.string "%d"
.LC2:
.string "You entered %d...\n"
main:
sub
mov
call
lea
mov
xor
call
mov
mov
xor
call

rsp, 24
edi, OFFSET FLAT:.LC0 ; "Enter X:"
puts
rsi, [rsp+12]
edi, OFFSET FLAT:.LC1 ; "%d"
eax, eax
__isoc99_scanf
esi, DWORD PTR [rsp+12]
edi, OFFSET FLAT:.LC2 ; "You entered %d...\n"
eax, eax
printf

; возврат 0
xor
eax, eax
add
rsp, 24
ret

ARM
Оптимизирующий Keil 6/2013 (Режим Thumb)
.text:00000042
.text:00000042
.text:00000042
.text:00000042
.text:00000042
.text:00000044
.text:00000046
.text:0000004A
.text:0000004C
.text:0000004E
.text:00000052
.text:00000054
%d...\n"

scanf_main
var_8
08
A9
06
69
AA
06
00
A9

B5
A0
F0 D3 F8
46
A0
F0 CD F8
99
A0

PUSH
ADR
BL
MOV
ADR
BL
LDR
ADR

= −8
{R3,LR}
R0, aEnterX ; "Enter X:\n"
__2printf
R1, SP
R0, aD ; "%d"
__0scanf
R1, [SP,#8+var_8]
R0, aYouEnteredD___ ; "You entered

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

99
.text:00000056 06 F0 CB F8
.text:0000005A 00 20
.text:0000005C 08 BD

BL
MOVS
POP

__2printf
R0, #0
{R3,PC}

Чтобы scanf() мог вернуть значение, ему нужно передать указатель на переменную типа int. int — 32-битное значение, для его хранения нужно только 4
байта, и оно помещается в 32-битный регистр.
Место для локальной переменной x выделяется в стеке, IDA наименовала её
var_8. Впрочем, место для неё выделять не обязательно, т.к. указатель стека
SP уже указывает на место, свободное для использования. Так что значение
указателя SP копируется в регистр R1, и вместе с format-строкой, передается
в scanf().
Инструкции PUSH/POP в ARM работают иначе, чем в x86 (тут всё наоборот). Это
синонимы инструкций STM/STMDB/LDM/LDMIA. И инструкция PUSH в начале записывает в стек значение, затем вычитает 4 из SP. POP в начале прибавляет
4 к SP, затем читает значение из стека. Так что после PUSH, SP указывает на
неиспользуемое место в стеке. Его и использует scanf(), а затем и printf().
LDMIA означает Load Multiple Registers Increment address After each transfer. STMDB
означает Store Multiple Registers Decrement address Before each transfer.
Позже, при помощи инструкции LDR, это значение перемещается из стека в
регистр R1, чтобы быть переданным в printf().
ARM64
Листинг 1.74: Неоптимизирующий GCC 4.9.1 ARM64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

.LC0:
.string "Enter X:"
.LC1:
.string "%d"
.LC2:
.string "You entered %d...\n"
scanf_main:
; вычесть 32 из SP, затем сохранить FP и LR в стековом фрейме:
stp
x29, x30, [sp, −32]!
; установить стековый фрейм (FP=SP)
add
x29, sp, 0
; загрузить указатель на строку "Enter X:"
adrp
x0, .LC0
add
x0, x0, :lo12:.LC0
; X0=указатель на строку "Enter X:"
; вывести её:
bl
puts
; загрузить указатель на строку "%d":
adrp
x0, .LC1
add
x0, x0, :lo12:.LC1
; найти место в стековом фрейме для переменной "x" (X1=FP+28):
add
x1, x29, 28

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

100
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

; X1=адрес переменной "x"
; передать адрес в scanf() и вызвать её:
bl
__isoc99_scanf
; загрузить 32-битное значение из переменной в стековом фрейме:
ldr
w1, [x29,28]
; W1=x
; загрузить указатель на строку "You entered %d...\n"
; printf() возьмет текстовую строку из X0 и переменную "x" из X1 (или W1)
adrp
x0, .LC2
add
x0, x0, :lo12:.LC2
bl
printf
; возврат 0
mov
w0, 0
; восстановить FP и LR, затем прибавить 32 к SP:
ldp
x29, x30, [sp], 32
ret

Под стековый фрейм выделяется 32 байта, что больше чем нужно. Может быть,
это связано с выравниваем по границе памяти? Самая интересная часть — это
поиск места под переменную x в стековом фрейме (строка 22). Почему 28?
Почему-то, компилятор решил расположить эту переменную в конце стекового фрейма, а не в начале. Адрес потом передается в scanf(), которая просто
сохраняет значение, введенное пользователем, в памяти по этому адресу. Это
32-битное значение типа int. Значение загружается в строке 27 и затем передается в printf().
MIPS
Для переменной x выделено место в стеке, и к нему будут производиться обращения как $sp + 24.
Её адрес передается в scanf(), а значение прочитанное от пользователя загружается используя инструкцию LW («Load Word» — загрузить слово) и затем
оно передается в printf().
Листинг 1.75: Оптимизирующий GCC 4.4.5 (вывод на ассемблере)
$LC0:
.ascii

"Enter X:\000"

.ascii

"%d\000"

$LC1:
$LC2:
.ascii "You entered %d...\012\000"
main:
; пролог функции:
lui
$28,%hi(__gnu_local_gp)
addiu
$sp,$sp,−40
addiu
$28,$28,%lo(__gnu_local_gp)
sw
$31,36($sp)
; вызов puts():
lw
$25,%call16(puts)($28)
lui
$4,%hi($LC0)
jalr
$25
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

101
addiu
$4,$4,%lo($LC0) ; branch delay slot
; вызов scanf():
lw
$28,16($sp)
lui
$4,%hi($LC1)
lw
$25,%call16(__isoc99_scanf)($28)
; установить второй аргумент для scanf(), $a1=$sp+24:
addiu
$5,$sp,24
jalr
$25
addiu
$4,$4,%lo($LC1) ; branch delay slot
; вызов printf():
lw
$28,16($sp)
; установить второй аргумент для printf(),
; загрузить слово по адресу $sp+24:
lw
$5,24($sp)
lw
$25,%call16(printf)($28)
lui
$4,%hi($LC2)
jalr
$25
addiu
$4,$4,%lo($LC2) ; branch delay slot
; эпилог функции:
lw
$31,36($sp)
; установить возвращаемое значение в 0:
move
$2,$0
; возврат:
j
$31
addiu
$sp,$sp,40
; branch delay slot

IDA показывает разметку стека следующим образом:
Листинг 1.76: Оптимизирующий GCC 4.4.5 (IDA)
.text:00000000 main:
.text:00000000
.text:00000000 var_18
.text:00000000 var_10
.text:00000000 var_4
.text:00000000
; пролог функции:
.text:00000000
.text:00000004
.text:00000008
.text:0000000C
.text:00000010
; вызов puts():
.text:00000014
.text:00000018
.text:0000001C
.text:00000020
delay slot
; вызов scanf():
.text:00000024
.text:00000028
.text:0000002C

= −0x18
= −0x10
= −4

lui
addiu
la
sw
sw

$gp,
$sp,
$gp,
$ra,
$gp,

(__gnu_local_gp >> 16)
−0x28
(__gnu_local_gp & 0xFFFF)
0x28+var_4($sp)
0x28+var_18($sp)

lw
lui
jalr
la

$t9, (puts & 0xFFFF)($gp)
$a0, ($LC0 >> 16) # "Enter X:"
$t9
$a0, ($LC0 & 0xFFFF) # "Enter X:" ; branch

lw
lui
lw

$gp, 0x28+var_18($sp)
$a0, ($LC1 >> 16) # "%d"
$t9, (__isoc99_scanf & 0xFFFF)($gp)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

102
; установить второй аргумент для scanf(), $a1=$sp+24:
.text:00000030
addiu
$a1, $sp, 0x28+var_10
.text:00000034
jalr
$t9 ; branch delay slot
.text:00000038
la
$a0, ($LC1 & 0xFFFF) # "%d"
; вызов printf():
.text:0000003C
lw
$gp, 0x28+var_18($sp)
; установить второй аргумент для printf(),
; загрузить слово по адресу $sp+24:
.text:00000040
lw
$a1, 0x28+var_10($sp)
.text:00000044
lw
$t9, (printf & 0xFFFF)($gp)
.text:00000048
lui
$a0, ($LC2 >> 16) # "You entered %d...\n"
.text:0000004C
jalr
$t9
.text:00000050
la
$a0, ($LC2 & 0xFFFF) # "You entered %d...\n"
; branch delay slot
; эпилог функции:
.text:00000054
lw
$ra, 0x28+var_4($sp)
; установить возвращаемое значение в 0:
.text:00000058
move
$v0, $zero
; возврат:
.text:0000005C
jr
$ra
.text:00000060
addiu
$sp, 0x28 ; branch delay slot

1.12.2. Классическая ошибка
Это очень популярная ошибка (и/или опечатка) — передать значение x вместо
указателя на x:
#include
int main()
{
int x;
printf ("Enter X:\n");
scanf ("%d", x); // BUG
printf ("You entered %d...\n", x);
return 0;
};

Что тут происходит? x не инициализирована и содержит случайный шум из
локального стека. Когда вызывается scanf(), ф-ция берет строку от пользователя, парсит её, получает число и пытается записать в x, считая его как адрес
в памяти. Но там случайный шум, так что scanf() будет пытаться писать по
случайному адресу. Скорее всего, процесс упадет.
Интересно что некоторые CRT-библиотеки, в отладочной сборке, заполняют
только что выделенную память визуально различимыми числами вроде 0xCCCCCCCC
или 0x0BADF00D, итд. В этом случае, x может содержать 0xCCCCCCCC, и scanf()
попытается писать по адресу 0xCCCCCCCC. Если вы заметите что что-то в вашем процессе пытается писать по адресу 0xCCCCCCCC, вы поймете, что неиниЕсли вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

103
циализированная переменная (или указатель) используется без инициализации. Это лучше, чем если только что выделенная память очищается нулевыми
байтами.

1.12.3. Глобальные переменные
А что если переменная x из предыдущего примера будет глобальной переменной, а не локальной? Тогда к ней смогут обращаться из любого другого
места, а не только из тела функции. Глобальные переменные считаются антипаттерном, но ради примера мы можем себе это позволить.
#include
// теперь x это глобальная переменная
int x;
int main()
{
printf ("Enter X:\n");
scanf ("%d", &x);
printf ("You entered %d...\n", x);
return 0;
};

MSVC: x86
_DATA
SEGMENT
COMM
_x:DWORD
$SG2456
DB
'Enter X:', 0aH, 00H
$SG2457
DB
'%d', 00H
$SG2458
DB
'You entered %d...', 0aH, 00H
_DATA
ENDS
PUBLIC
_main
EXTRN
_scanf:PROC
EXTRN
_printf:PROC
; Function compile flags: /Odtp
_TEXT
SEGMENT
_main
PROC
push
ebp
mov
ebp, esp
push
OFFSET $SG2456
call
_printf
add
esp, 4
push
OFFSET _x
push
OFFSET $SG2457
call
_scanf
add
esp, 8
mov
eax, DWORD PTR _x

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

104
push
eax
push
OFFSET $SG2458
call
_printf
add
esp, 8
xor
eax, eax
pop
ebp
ret
0
_main
ENDP
_TEXT
ENDS

В целом ничего особенного. Теперь x объявлена в сегменте _DATA. Память для
неё в стеке более не выделяется. Все обращения к ней происходит не через
стек, а уже напрямую. Неинициализированные глобальные переменные не занимают места в исполняемом файле (и действительно, зачем в исполняемом
файле нужно выделять место под изначально нулевые переменные?), но тогда, когда к этому месту в памяти кто-то обратится, ОС подставит туда блок,
состоящий из нулей71 .
Попробуем изменить объявление этой переменной:
int x=10; // значение по умолчанию

Выйдет в итоге:
_DATA
_x

SEGMENT
DD
0aH

...

Здесь уже по месту этой переменной записано 0xA с типом DD (dword = 32
бита).
Если вы откроете скомпилированный .exe-файл в IDA, то увидите, что x находится в начале сегмента _DATA, после этой переменной будут текстовые строки.
А вот если вы откроете в IDA.exe скомпилированный в прошлом примере, где
значение x не определено, то вы увидите:
Листинг 1.77: IDA
.data:0040FA80
.data:0040FA80
.data:0040FA84
.data:0040FA84
.data:0040FA88
.data:0040FA88
.data:0040FA8C
.data:0040FA8C
.data:0040FA8C
.data:0040FA90
.data:0040FA90
.data:0040FA94
71 Так

_x

dd ?

dword_40FA84

dd ?

dword_40FA88

dd ?

; LPVOID lpMem
lpMem

dd ?

dword_40FA90

dd ?

dword_40FA94

dd ?

;
;
;
;
;
;

DATA XREF: _main+10
_main+22
DATA XREF: _memset+1E
unknown_libname_1+28
DATA XREF: ___sbh_find_block+5
___sbh_free_block+2BC

;
;
;
;
;

DATA XREF: ___sbh_find_block+B
___sbh_free_block+2CA
DATA XREF: _V6_HeapAlloc+13
__calloc_impl+72
DATA XREF: ___sbh_free_block+2FE

работает VM

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

105
_x обозначен как ?, наряду с другими переменными не требующими инициализации. Это означает, что при загрузке .exe в память, место под всё это выделено будет и будет заполнено нулевыми байтами [ISO/IEC 9899:TC3 (C C99
standard), (2007)6.7.8p10]. Но в самом .exe ничего этого нет. Неинициализированные переменные не занимают места в исполняемых файлах. Это удобно
для больших массивов, например.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

106
MSVC: x86 + OllyDbg
Тут даже проще:

Рис. 1.16: OllyDbg: после исполнения scanf()
Переменная хранится в сегменте данных. Кстати, после исполнения инструкции PUSH (заталкивающей адрес x) адрес появится в стеке, и на этом элементе
можно нажать правой кнопкой, выбрать «Follow in dump». И в окне памяти слева появится эта переменная.
После того как в консоли введем 123, здесь появится 0x7B.
Почему самый первый байт это 7B? По логике вещей, здесь должно было бы
быть 00 00 00 7B. Это называется endianness, и в x86 принят формат littleendian. Это означает, что в начале записывается самый младший байт, а заканчивается самым старшим байтом. Больше об этом: 2.8 (стр. 599).
Позже из этого места в памяти 32-битное значение загружается в EAX и передается в printf().
Адрес переменной x в памяти 0x00C53394.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

107
В OllyDbg мы можем посмотреть карту памяти процесса (Alt-M) и увидим, что
этот адрес внутри PE-сегмента .data нашей программы:

Рис. 1.17: OllyDbg: карта памяти процесса
GCC: x86
В Linux всё почти также. За исключением того, что если значение x не определено, то эта переменная будет находиться в сегменте _bss. В ELF72 этот
сегмент имеет такие атрибуты:
; Segment type: Uninitialized
; Segment permissions: Read/Write

Ну а если сделать статическое присвоение этой переменной какого-либо значения, например, 10, то она будет находиться в сегменте _data, это сегмент с
такими атрибутами:
; Segment type: Pure data
; Segment permissions: Read/Write
72 Executable and Linkable Format: Формат исполняемых файлов, использующийся в Linux и некоторых других *NIX

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

108
MSVC: x64
Листинг 1.78: MSVC 2012 x64
_DATA
COMM
$SG2924
$SG2925
$SG2926
_DATA

SEGMENT
x:DWORD
DB
'Enter X:', 0aH, 00H
DB
'%d', 00H
DB
'You entered %d...', 0aH, 00H
ENDS

_TEXT
main
$LN3:

SEGMENT
PROC
sub

rsp, 40

lea
call
lea
lea
call
mov
lea
call

rcx, OFFSET FLAT:$SG2924 ; 'Enter X:'
printf
rdx, OFFSET FLAT:x
rcx, OFFSET FLAT:$SG2925 ; '%d'
scanf
edx, DWORD PTR x
rcx, OFFSET FLAT:$SG2926 ; 'You entered %d...'
printf

; возврат 0
xor
eax, eax

main
_TEXT

add
ret
ENDP
ENDS

rsp, 40
0

Почти такой же код как и в x86. Обратите внимание что для scanf() адрес
переменной x передается при помощи инструкции LEA, а во второй printf()
передается само значение переменной при помощи MOV. DWORD PTR — это часть
языка ассемблера (не имеющая отношения к машинным кодам) показывающая,
что тип переменной в памяти именно 32-битный, и инструкция MOV должна
быть здесь закодирована соответственно.
ARM: Оптимизирующий Keil 6/2013 (Режим Thumb)
Листинг 1.79: IDA
.text:00000000 ; Segment type: Pure code
.text:00000000
AREA .text, CODE
...
.text:00000000 main
.text:00000000
PUSH
{R4,LR}
.text:00000002
ADR
R0, aEnterX
.text:00000004
BL
__2printf
.text:00000008
LDR
R1, =x
.text:0000000A
ADR
R0, aD

; "Enter X:\n"

; "%d"

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

109
.text:0000000C
.text:00000010
.text:00000012
.text:00000014
.text:00000016
.text:0000001A
.text:0000001C
...
.text:00000020
.text:0000002A
.text:0000002B
.text:0000002C
.text:0000002C
.text:00000030
.text:00000033
.text:00000034
main+14
.text:00000047
.text:00000047
.text:00000047
...
.data:00000048
.data:00000048
.data:00000048
.data:00000048
.data:00000048
.data:00000048
.data:00000048

BL
LDR
LDR
ADR
BL
MOVS
POP

__0scanf
R0, =x
R1, [R0]
R0, aYouEnteredD___ ; "You entered %d...\n"
__2printf
R0, #0
{R4,PC}

aEnterX DCB "Enter X:",0xA,0 ; DATA XREF: main+2
DCB
0
DCB
0
off_2C DCD x
; DATA XREF: main+8
; main+10
aD
DCB "%d",0
; DATA XREF: main+A
DCB
0
aYouEnteredD___ DCB "You entered %d...",0xA,0 ; DATA XREF:
DCB 0
; .text ends

; Segment type: Pure data
AREA .data, DATA
; ORG 0x48
EXPORT x
x
DCD 0xA

; DATA XREF: main+8
; main+10

; .data ends

Итак, переменная x теперь глобальная, и она расположена, почему-то, в другом сегменте, а именно сегменте данных (.data). Можно спросить, почему текстовые строки расположены в сегменте кода (.text), а x нельзя было разместить тут же?
Потому что эта переменная, и как следует из определения, она может меняться. И может быть, меняться часто.
Ну а текстовые строки имеют тип констант, они не будут меняться, поэтому
они располагаются в сегменте .text.
Сегмент кода иногда может быть расположен в ПЗУ микроконтроллера (не
забывайте, мы сейчас имеем дело с встраиваемой (embedded) микроэлектроникой, где дефицит памяти — обычное дело), а изменяемые переменные — в
ОЗУ.
Хранить в ОЗУ неизменяемые данные, когда в наличии есть ПЗУ, не экономно.
К тому же, сегмент данных в ОЗУ с константами нужно инициализировать перед работой, ведь, после включения ОЗУ, очевидно, она содержит в себе случайную информацию.
Далее мы видим в сегменте кода хранится указатель на переменную x (off_2C)
и все операции с переменной происходят через этот указатель.
Это связано с тем, что переменная x может быть расположена где-то довольно
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

110
далеко от данного участка кода, так что её адрес нужно сохранить в непосредственной близости к этому коду.
Инструкция LDR в Thumb-режиме может адресовать только переменные в пределах вплоть до 1020 байт от своего местоположения.
Эта же инструкция в ARM-режиме — переменные в пределах ±4095 байт.
Таким образом, адрес глобальной переменной x нужно расположить в непосредственной близости, ведь нет никакой гарантии, что компоновщик73 сможет разместить саму переменную где-то рядом, она может быть даже в другом чипе памяти!
Ещё одна вещь: если переменную объявить, как const, то компилятор Keil разместит её в сегменте .constdata.
Должно быть, впоследствии компоновщик и этот сегмент сможет разместить
в ПЗУ вместе с сегментом кода.
ARM64
Листинг 1.80: Неоптимизирующий GCC 4.9.1 ARM64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

.comm

x,4,4

.LC0:
.string "Enter X:"
.LC1:
.string "%d"
.LC2:
.string "You entered %d...\n"
f5:
; сохранить FP и LR в стековом фрейме:
stp
x29, x30, [sp, −16]!
; установить стековый фрейм (FP=SP)
add
x29, sp, 0
; загрузить указатель на строку "Enter X:":
adrp
x0, .LC0
add
x0, x0, :lo12:.LC0
bl
puts
; загрузить указатель на строку "%d":
adrp
x0, .LC1
add
x0, x0, :lo12:.LC1
; сформировать адрес глобальной переменной x:
adrp
x1, x
add
x1, x1, :lo12:x
bl
__isoc99_scanf
; снова сформировать адрес глобальной переменной x:
adrp
x0, x
add
x0, x0, :lo12:x
; загрузить значение из памяти по этому адресу:
ldr
w1, [x0]
; загрузить указатель на строку "You entered %d...\n":
adrp
x0, .LC2
73 linker

в англоязычной литературе

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

111
31
32
33
34
35
36
37

add
x0, x0, :lo12:.LC2
bl
printf
; возврат 0
mov
w0, 0
; восстановить FP и LR:
ldp
x29, x30, [sp], 16
ret

Теперь x это глобальная переменная, и её адрес вычисляется при помощи пары
инструкций ADRP/ADD (строки 21 и 25).
MIPS
Неинициализированная глобальная переменная
Так что теперь переменная x глобальная. Сделаем исполняемый файл вместо
объектного и загрузим его в IDA. IDA показывает присутствие переменной x в
ELF-секции .sbss (помните о «Global Pointer»? 1.5.4 (стр. 33)), так как переменная не инициализируется в самом начале.
Листинг 1.81: Оптимизирующий GCC 4.4.5 (IDA)
.text:004006C0 main:
.text:004006C0
.text:004006C0 var_10
.text:004006C0 var_4
.text:004006C0
; пролог функции:
.text:004006C0
.text:004006C4
.text:004006C8
.text:004006CC
.text:004006D0
; вызов puts():
.text:004006D4
.text:004006D8
.text:004006DC
.text:004006E0
slot
; вызов scanf():
.text:004006E4
.text:004006E8
.text:004006EC
; подготовить адрес x:
.text:004006F0
.text:004006F4
.text:004006F8
; вызов printf():
.text:004006FC
.text:00400700
; взять адрес x:
.text:00400704
.text:00400708

= −0x10
= −4

lui
addiu
li
sw
sw

$gp,
$sp,
$gp,
$ra,
$gp,

0x42
−0x20
0x418940
0x20+var_4($sp)
0x20+var_10($sp)

la
lui
jalr
la

$t9, puts
$a0, 0x40
$t9 ; puts
$a0, aEnterX

lw
lui
la

$gp, 0x20+var_10($sp)
$a0, 0x40
$t9, __isoc99_scanf

la
jalr
la

$a1, x
$t9 ; __isoc99_scanf
$a0, aD
# "%d" ; branch delay slot

lw
lui

$gp, 0x20+var_10($sp)
$a0, 0x40

la
la

$v0, x
$t9, printf

# "Enter X:" ; branch delay

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

112
; загрузить значение из переменной "x" и передать его в printf() в $a1:
.text:0040070C
lw
$a1, (x − 0x41099C)($v0)
.text:00400710
jalr
$t9 ; printf
.text:00400714
la
$a0, aYouEnteredD___ # "You entered %d...\n"
; branch delay slot
; эпилог функции:
.text:00400718
lw
$ra, 0x20+var_4($sp)
.text:0040071C
move
$v0, $zero
.text:00400720
jr
$ra
.text:00400724
addiu
$sp, 0x20 ; branch delay slot
...
.sbss:0041099C # Segment type: Uninitialized
.sbss:0041099C
.sbss
.sbss:0041099C
.globl x
.sbss:0041099C x:
.space 4
.sbss:0041099C

IDA уменьшает количество информации, так что сделаем также листинг используя objdump и добавим туда свои комментарии:
Листинг 1.82: Оптимизирующий GCC 4.4.5 (objdump)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

004006c0 :
; пролог функции:
4006c0: 3c1c0042 lui
gp,0x42
4006c4: 27bdffe0 addiu
sp,sp,−32
4006c8: 279c8940 addiu
gp,gp,−30400
4006cc: afbf001c sw
ra,28(sp)
4006d0: afbc0010 sw
gp,16(sp)
; вызов puts():
4006d4: 8f998034 lw
t9,−32716(gp)
4006d8: 3c040040 lui
a0,0x40
4006dc: 0320f809 jalr
t9
4006e0: 248408f0 addiu
a0,a0,2288 ; branch delay slot
; вызов scanf():
4006e4: 8fbc0010 lw
gp,16(sp)
4006e8: 3c040040 lui
a0,0x40
4006ec: 8f998038 lw
t9,−32712(gp)
; подготовить адрес x:
4006f0: 8f858044 lw
a1,−32700(gp)
4006f4: 0320f809 jalr
t9
4006f8: 248408fc addiu
a0,a0,2300 ; branch delay slot
; вызов printf():
4006fc: 8fbc0010 lw
gp,16(sp)
400700: 3c040040 lui
a0,0x40
; взять адрес x:
400704: 8f828044 lw
v0,−32700(gp)
400708: 8f99803c lw
t9,−32708(gp)
; загрузить значение из переменной "x" и передать его в printf() в $a1:
40070c: 8c450000 lw
a1,0(v0)
400710: 0320f809 jalr
t9
400714: 24840900 addiu
a0,a0,2304 ; branch delay slot
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

113
31
32
33
34
35
36
37
38

; эпилог функции:
400718: 8fbf001c lw
ra,28(sp)
40071c: 00001021 move
v0,zero
400720: 03e00008 jr
ra
400724: 27bd0020 addiu
sp,sp,32
; branch delay slot
; набор NOP-ов для выравнивания начала следующей ф-ции по 16-байтной границе:
400728: 00200825 move
at,at
40072c: 00200825 move
at,at

Теперь мы видим, как адрес переменной x берется из буфера 64KiB, используя
GP и прибавление к нему отрицательного смещения (строка 18).
И даже более того: адреса трех внешних функций, используемых в нашем примере (puts(), scanf(), printf()) также берутся из буфера 64KiB используя GP
(строки 9, 16 и 26).
GP указывает на середину буфера, так что такие смещения могут нам подсказать, что адреса всех трех функций, а также адрес переменной x расположены
где-то в самом начале буфера. Действительно, ведь наш пример крохотный.
Ещё нужно отметить что функция заканчивается двумя NOP-ами (MOVE $AT,$AT —
это холостая инструкция), чтобы выровнять начало следующей функции по
16-байтной границе.
Инициализированная глобальная переменная
Немного изменим наш пример и сделаем, чтобы у x было значение по умолчанию:
int x=10; // значение по умолчанию

Теперь IDA показывает что переменная x располагается в секции .data:
Листинг 1.83: Оптимизирующий GCC 4.4.5 (IDA)
.text:004006A0 main:
.text:004006A0
.text:004006A0 var_10
.text:004006A0 var_8
.text:004006A0 var_4
.text:004006A0
.text:004006A0
.text:004006A4
.text:004006A8
.text:004006AC
.text:004006B0
.text:004006B4
.text:004006B8
.text:004006BC
.text:004006C0
.text:004006C4
.text:004006C8
; подготовить старшую

= −0x10
= −8
= −4
lui
$gp, 0x42
addiu
$sp, −0x20
li
$gp, 0x418930
sw
$ra, 0x20+var_4($sp)
sw
$s0, 0x20+var_8($sp)
sw
$gp, 0x20+var_10($sp)
la
$t9, puts
lui
$a0, 0x40
jalr
$t9 ; puts
la
$a0, aEnterX
# "Enter X:"
lw
$gp, 0x20+var_10($sp)
часть адреса x:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

114
.text:004006CC
lui
$s0, 0x41
.text:004006D0
la
$t9, __isoc99_scanf
.text:004006D4
lui
$a0, 0x40
; прибавить младшую часть адреса x:
.text:004006D8
addiu
$a1, $s0, (x − 0x410000)
; теперь адрес x в $a1.
.text:004006DC
jalr
$t9 ; __isoc99_scanf
.text:004006E0
la
$a0, aD
# "%d"
.text:004006E4
lw
$gp, 0x20+var_10($sp)
; загрузить слово из памяти:
.text:004006E8
lw
$a1, x
; значение x теперь в $a1.
.text:004006EC
la
$t9, printf
.text:004006F0
lui
$a0, 0x40
.text:004006F4
jalr
$t9 ; printf
.text:004006F8
la
$a0, aYouEnteredD___ # "You entered %d...\n"
.text:004006FC
lw
$ra, 0x20+var_4($sp)
.text:00400700
move
$v0, $zero
.text:00400704
lw
$s0, 0x20+var_8($sp)
.text:00400708
jr
$ra
.text:0040070C
addiu
$sp, 0x20
...
.data:00410920
.data:00410920 x:

.globl x
.word 0xA

Почему не .sdata? Может быть, нужно было указать какую-то опцию в GCC? Тем
не менее, x теперь в .data, а это уже общая память и мы можем посмотреть как
происходит работа с переменными там.
Адрес переменной должен быть сформирован парой инструкций. В нашем случае это LUI («Load Upper Immediate» — загрузить старшие 16 бит) и ADDIU («Add
Immediate Unsigned Word» — прибавить значение). Вот так же листинг сгенерированный objdump-ом для лучшего рассмотрения:
Листинг 1.84: Оптимизирующий GCC 4.4.5 (objdump)
004006a0 :
4006a0: 3c1c0042 lui
gp,0x42
4006a4: 27bdffe0 addiu
sp,sp,−32
4006a8: 279c8930 addiu
gp,gp,−30416
4006ac: afbf001c sw
ra,28(sp)
4006b0: afb00018 sw
s0,24(sp)
4006b4: afbc0010 sw
gp,16(sp)
4006b8: 8f998034 lw
t9,−32716(gp)
4006bc: 3c040040 lui
a0,0x40
4006c0: 0320f809 jalr
t9
4006c4: 248408d0 addiu
a0,a0,2256
4006c8: 8fbc0010 lw
gp,16(sp)
; подготовить старшую часть адреса x:
4006cc: 3c100041 lui
s0,0x41
4006d0: 8f998038 lw
t9,−32712(gp)
4006d4: 3c040040 lui
a0,0x40
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

115
; прибавить младшую часть адреса x:
4006d8: 26050920 addiu
a1,s0,2336
; теперь адрес x в $a1.
4006dc: 0320f809 jalr
t9
4006e0: 248408dc addiu
a0,a0,2268
4006e4: 8fbc0010 lw
gp,16(sp)
; старшая часть адреса x всё еще в $s0.
; прибавить младшую часть к ней и загрузить слово из памяти:
4006e8: 8e050920 lw
a1,2336(s0)
; значение x теперь в $a1.
4006ec: 8f99803c lw
t9,−32708(gp)
4006f0: 3c040040 lui
a0,0x40
4006f4: 0320f809 jalr
t9
4006f8: 248408e0 addiu
a0,a0,2272
4006fc: 8fbf001c lw
ra,28(sp)
400700: 00001021 move
v0,zero
400704: 8fb00018 lw
s0,24(sp)
400708: 03e00008 jr
ra
40070c: 27bd0020 addiu
sp,sp,32

Адрес формируется используя LUI и ADDIU, но старшая часть адреса всё ещё в
регистре $S0, и можно закодировать смещение в инструкции LW («Load Word»),
так что одной LW достаточно для загрузки значения из переменной и передачи
его в printf(). Регистры хранящие временные данные имеют префикс T-, но
здесь есть также регистры с префиксом S-, содержимое которых должно быть
сохранено в других функциях (т.е. «saved»).
Вот почему $S0 был установлен по адресу 0x4006cc и затем был использован
по адресу 0x4006e8 после вызова scanf().
Функция scanf() не изменяет это значение.

1.12.4. Проверка результата scanf()
Как уже было упомянуто, использовать scanf() в наше время слегка старомодно. Но если уж пришлось, нужно хотя бы проверять, сработал ли scanf()
правильно или пользователь ввел вместо числа что-то другое, что scanf() не
смог трактовать как число.
#include
int main()
{
int x;
printf ("Enter X:\n");
if (scanf ("%d", &x)==1)
printf ("You entered %d...\n", x);
else
printf ("What you entered? Huh?\n");
return 0;
};
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

116

По стандарту,scanf()74 возвращает количество успешно полученных значений.
В нашем случае, если всё успешно и пользователь ввел таки некое число, scanf()
вернет 1. А если нет, то 0 (или EOF75 ).
Добавим код, проверяющий результат scanf() и в случае ошибки он сообщает
пользователю что-то другое.
Это работает предсказуемо:
C:\...>ex3.exe
Enter X:
123
You entered 123...
C:\...>ex3.exe
Enter X:
ouch
What you entered? Huh?

MSVC: x86
Вот что выходит на ассемблере (MSVC 2010):
lea
push
push
call
add
cmp
jne
mov
push
push
call
add
jmp
$LN2@main:
push
call
add
$LN1@main:
xor

eax, DWORD PTR _x$[ebp]
eax
OFFSET $SG3833 ; '%d', 00H
_scanf
esp, 8
eax, 1
SHORT $LN2@main
ecx, DWORD PTR _x$[ebp]
ecx
OFFSET $SG3834 ; 'You entered %d...', 0aH, 00H
_printf
esp, 8
SHORT $LN1@main
OFFSET $SG3836 ; 'What you entered? Huh?', 0aH, 00H
_printf
esp, 4
eax, eax

Для того чтобы вызывающая функция имела доступ к результату вызываемой
функции, вызываемая функция (в нашем случае scanf()) оставляет это значение в регистре EAX.
74 scanf,
75 End

wscanf: MSDN
of File (конец файла)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

117
Мы проверяем его инструкцией CMP EAX, 1 (CoMPare), то есть сравниваем значение в EAX с 1.
Следующий за инструкцией CMP: условный переход JNE. Это означает Jump if
Not Equal, то есть условный переход если не равно.
Итак, если EAX не равен 1, то JNE заставит CPU перейти по адресу указанном в
операнде JNE, у нас это $LN2@main. Передав управление по этому адресу, CPU
начнет исполнять вызов printf() с аргументом What you entered? Huh?. Но
если всё нормально, перехода не случится и исполнится другой printf() с
двумя аргументами:
'You entered %d...' и значением переменной x.
Для того чтобы после этого вызова не исполнился сразу второй вызов printf(),
после него есть инструкция JMP, безусловный переход, который отправит процессор на место после второго printf() и перед инструкцией XOR EAX, EAX,
которая реализует return 0.
Итак, можно сказать, что в подавляющих случаях сравнение какой-либо переменной с чем-то другим происходит при помощи пары инструкций CMP и Jcc,
где cc это condition code. CMP сравнивает два значения и выставляет флаги
процессора76 . Jcc проверяет нужные ему флаги и выполняет переход по указанному адресу (или не выполняет).
Но на самом деле, как это не парадоксально поначалу звучит, CMP это почти
то же самое что и инструкция SUB, которая отнимает числа одно от другого.
Все арифметические инструкции также выставляют флаги в соответствии с
результатом, не только CMP. Если мы сравним 1 и 1, от единицы отнимется единица, получится 0, и выставится флаг ZF (zero flag), означающий, что последний полученный результат был 0. Ни при каких других значениях EAX, флаг
ZF не может быть выставлен, кроме тех, когда операнды равны друг другу.
Инструкция JNE проверяет только флаг ZF, и совершает переход только если
флаг не поднят. Фактически, JNE это синоним инструкции JNZ (Jump if Not Zero).
Ассемблер транслирует обе инструкции в один и тот же опкод. Таким образом,
можно CMP заменить на SUB и всё будет работать также, но разница в том, что
SUB всё-таки испортит значение в первом операнде. CMP это SUB без сохранения результата, но изменяющая флаги.
MSVC: x86: IDA
Наверное, уже пора делать первые попытки анализа кода в IDA. Кстати, начинающим полезно компилировать в MSVC с ключом /MD, что означает, что все
эти стандартные функции не будут скомпонованы с исполняемым файлом, а
будут импортироваться из файла MSVCR*.DLL. Так будет легче увидеть, где какая стандартная функция используется.
Анализируя код в IDA, очень полезно делать пометки для себя (и других). Например, разбирая этот пример, мы сразу видим, что JNZ срабатывает в случае
ошибки. Можно навести курсор на эту метку, нажать «n» и переименовать метку в «error». Ещё одну метку — в «exit». Вот как у меня получилось в итоге:
76 См.

также о флагах x86-процессора: wikipedia.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

118
.text:00401000
.text:00401000
.text:00401000
.text:00401000
.text:00401000
.text:00401000
.text:00401000
.text:00401000
.text:00401001
.text:00401003
.text:00401004
.text:00401009
.text:0040100F
.text:00401012
.text:00401015
.text:00401016
.text:0040101B
.text:00401021
.text:00401024
.text:00401027
.text:00401029
.text:0040102C
.text:0040102D
.text:00401032
.text:00401038
.text:0040103B
.text:0040103D
.text:0040103D
.text:0040103D
.text:00401042
.text:00401048
.text:0040104B
.text:0040104B
.text:0040104B
.text:0040104D
.text:0040104F
.text:00401050
.text:00401050

_main proc near
var_4
argc
argv
envp

=
=
=
=

dword
dword
dword
dword

push
mov
push
push
call
add
lea
push
push
call
add
cmp
jnz
mov
push
push
call
add
jmp
error: ; CODE
push
call
add

ptr −4
ptr 8
ptr 0Ch
ptr 10h
ebp
ebp, esp
ecx
offset Format ; "Enter X:\n"
ds:printf
esp, 4
eax, [ebp+var_4]
eax
offset aD ; "%d"
ds:scanf
esp, 8
eax, 1
short error
ecx, [ebp+var_4]
ecx
offset aYou ; "You entered %d...\n"
ds:printf
esp, 8
short exit
XREF: _main+27
offset aWhat ; "What you entered? Huh?\n"
ds:printf
esp, 4

exit: ; CODE XREF: _main+3B
xor
eax, eax
mov
esp, ebp
pop
ebp
retn
_main endp

Так понимать код становится чуть легче. Впрочем, меру нужно знать во всем
и комментировать каждую инструкцию не стоит.
В IDA также можно скрывать части функций: нужно выделить скрываемую
часть, нажать Ctrl-«–» на цифровой клавиатуре и ввести текст.
Скроем две части и придумаем им названия:
.text:00401000 _text segment para public 'CODE' use32
.text:00401000
assume cs:_text
.text:00401000
;org 401000h
.text:00401000 ; ask for X
.text:00401012 ; get X
.text:00401024
cmp eax, 1

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

119
.text:00401027
.text:00401029
.text:0040103B
.text:0040103D
.text:0040103D
.text:0040103D
.text:00401042
.text:00401048
.text:0040104B
.text:0040104B
.text:0040104B
.text:0040104D
.text:0040104F
.text:00401050
.text:00401050

jnz short error
; print result
jmp short exit
error: ; CODE XREF: _main+27
push offset aWhat ; "What you entered? Huh?\n"
call ds:printf
add esp, 4
exit: ; CODE XREF: _main+3B
xor eax, eax
mov esp, ebp
pop ebp
retn
_main endp

Раскрывать скрытые части функций можно при помощи Ctrl-«+» на цифровой
клавиатуре.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

120
Нажав «пробел», мы увидим, как IDA может представить функцию в виде графа:

Рис. 1.18: Отображение функции в IDA в виде графа
После каждого условного перехода видны две стрелки: зеленая и красная. Зеленая ведет к тому блоку, который исполнится если переход сработает, а красная — если не сработает.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

121
В этом режиме также можно сворачивать узлы и давать им названия («group
nodes»). Сделаем это для трех блоков:

Рис. 1.19: Отображение в IDA в виде графа с тремя свернутыми блоками
Всё это очень полезно делать. Вообще, очень важная часть работы реверсера (да и любого исследователя) состоит в том, чтобы уменьшать количество
имеющейся информации.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

122
MSVC: x86 + OllyDbg
Попробуем в OllyDbg немного хакнуть программу и сделать вид, что scanf()
срабатывает всегда без ошибок. Когда в scanf() передается адрес локальной
переменной, изначально в этой переменной находится некий мусор. В данном
случае это 0x6E494714:

Рис. 1.20: OllyDbg: передача адреса переменной в scanf()

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

123
Когда scanf() запускается, вводим в консоли что-то непохожее на число, например «asdasd». scanf() заканчивается с 0 в EAX, что означает, что произошла ошибка.
Вместе с этим мы можем посмотреть на локальную переменную в стеке — она
не изменилась. Действительно, ведь что туда записала бы функция scanf()?
Она не делала ничего кроме возвращения нуля. Попробуем ещё немного «хакнуть» нашу программу. Щелкнем правой кнопкой на EAX, там, в числе опций,
будет также «Set to 1». Это нам и нужно.
В EAX теперь 1, последующая проверка пройдет как надо, и printf() выведет
значение переменной из стека.
Запускаем (F9) и видим в консоли следующее:
Листинг 1.85: консоль
Enter X:
asdasd
You entered 1850296084...

Действительно, 1850296084 это десятичное представление числа в стеке (0x6E494714)!

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

124
MSVC: x86 + Hiew
Это ещё может быть и простым примером исправления исполняемого файла.
Мы можем попробовать исправить его таким образом, что программа всегда
будет выводить числа, вне зависимости от ввода.
Исполняемый файл скомпилирован с импортированием функций из MSVCR*.DLL
(т.е. с опцией /MD)77 , поэтому мы можем отыскать функцию main() в самом
начале секции .text. Откроем исполняемый файл в Hiew, найдем самое начало
секции .text (Enter, F8, F6, Enter, Enter).
Мы увидим следующее:

Рис. 1.21: Hiew: функция main()
Hiew находит ASCIIZ78 -строки и показывает их, также как и имена импортируемых функций.
77 то,

что ещё называют «dynamic linking»
Zero (ASCII-строка заканчивающаяся нулем )

78 ASCII

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

125
Переведите курсор на адрес .00401027 (с инструкцией JNZ, которую мы хотим
заблокировать), нажмите F3, затем наберите «9090» (что означает два NOP-а):

Рис. 1.22: Hiew: замена JNZ на два NOP-а
Затем F9 (update). Теперь исполняемый файл записан на диск. Он будет вести
себя так, как нам надо.
Два NOP-а, возможно, не так эстетично, как могло бы быть. Другой способ изменить инструкцию это записать 0 во второй байт опкода (смещение перехода),
так что JNZ всегда будет переходить на следующую инструкцию.
Можно изменить и наоборот: первый байт заменить на EB, второй байт (смещение перехода) не трогать. Получится всегда срабатывающий безусловный
переход. Теперь сообщение об ошибке будет выдаваться всегда, даже если
мы ввели число.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

126
MSVC: x64
Так как здесь мы работаем с переменными типа int, а они в x86-64 остались
32-битными, то мы здесь видим, как продолжают использоваться регистры с
префиксом E-. Но для работы с указателями, конечно, используются 64-битные
части регистров с префиксом R-.
Листинг 1.86: MSVC 2012 x64
_DATA
$SG2924
$SG2926
$SG2927
$SG2929
_DATA

SEGMENT
DB
DB
DB
DB
ENDS

'Enter X:', 0aH, 00H
'%d', 00H
'You entered %d...', 0aH, 00H
'What you entered? Huh?', 0aH, 00H

_TEXT
SEGMENT
x$ = 32
main
PROC
$LN5:
sub
rsp, 56
lea
rcx, OFFSET FLAT:$SG2924
call
printf
lea
rdx, QWORD PTR x$[rsp]
lea
rcx, OFFSET FLAT:$SG2926
call
scanf
cmp
eax, 1
jne
SHORT $LN2@main
mov
edx, DWORD PTR x$[rsp]
lea
rcx, OFFSET FLAT:$SG2927
call
printf
jmp
SHORT $LN1@main
$LN2@main:
lea
rcx, OFFSET FLAT:$SG2929
call
printf
$LN1@main:
; возврат 0
xor
eax, eax
add
rsp, 56
ret
0
main
ENDP
_TEXT
ENDS
END

; 'Enter X:'

; '%d'

; 'You entered %d...'

; 'What you entered? Huh?'

ARM
ARM: Оптимизирующий Keil 6/2013 (Режим Thumb)
Листинг 1.87: Оптимизирующий Keil 6/2013 (Режим Thumb)
var_8

= −8
PUSH

{R3,LR}

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

127
ADR
BL
MOV
ADR
BL
CMP
BEQ
ADR
BL

R0, aEnterX
; "Enter X:\n"
__2printf
R1, SP
R0, aD
; "%d"
__0scanf
R0, #1
loc_1E
R0, aWhatYouEntered ; "What you entered? Huh?\n"
__2printf

MOVS
POP

R0, #0
{R3,PC}

LDR
ADR
BL
B

; CODE XREF: main+12
R1, [SP,#8+var_8]
R0, aYouEnteredD___ ; "You entered %d...\n"
__2printf
loc_1A

loc_1A

; CODE XREF: main+26

loc_1E

Здесь для нас есть новые инструкции: CMP и BEQ79 .
CMP аналогична той что в x86: она отнимает один аргумент от второго и сохраняет флаги.
BEQ совершает переход по другому адресу, если операнды при сравнении были равны, либо если результат последнего вычисления был 0, либо если флаг
Z равен 1. То же что и JZ в x86.
Всё остальное просто: исполнение разветвляется на две ветки, затем они сходятся там, где в R0 записывается 0 как возвращаемое из функции значение и
происходит выход из функции.
ARM64
Листинг 1.88: Неоптимизирующий GCC 4.9.1 ARM64
1
2
3
4
5
6
7
8
9
10
11
12
13
14

.LC0:
.string "Enter X:"
.LC1:
.string "%d"
.LC2:
.string "You entered %d...\n"
.LC3:
.string "What you entered? Huh?"
f6:
; сохранить FP и LR в стековом фрейме:
stp
x29, x30, [sp, −32]!
; установить стековый фрейм (FP=SP)
add
x29, sp, 0
; загрузить указатель на строку "Enter X:"
79 (PowerPC,

ARM) Branch if Equal

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

128
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

adrp
x0, .LC0
add
x0, x0, :lo12:.LC0
bl
puts
; загрузить указатель на строку "%d":
adrp
x0, .LC1
add
x0, x0, :lo12:.LC1
; вычислить адрес переменной x в локальном стеке
add
x1, x29, 28
bl
__isoc99_scanf
; scanf() возвращает результат в W0.
; проверяем его:
cmp
w0, 1
; BNE это Branch if Not Equal (переход, если не равно)
; так что если W00, произойдет переход на L2
bne
.L2
; в этот момент W0=1, означая, что ошибки не было
; загрузить значение x из локального стека
ldr
w1, [x29,28]
; загрузить указатель на строку "You entered %d...\n":
adrp
x0, .LC2
add
x0, x0, :lo12:.LC2
bl
printf
; пропустить код, печатающий строку "What you entered? Huh?":
b
.L3
.L2:
; загрузить указатель на строку "What you entered? Huh?":
adrp
x0, .LC3
add
x0, x0, :lo12:.LC3
bl
puts
.L3:
; возврат 0
mov
w0, 0
; восстановить FP и LR:
ldp
x29, x30, [sp], 32
ret

Исполнение здесь разветвляется, используя пару инструкций CMP/BNE (Branch
if Not Equal: переход если не равно).
MIPS
Листинг 1.89: Оптимизирующий GCC 4.4.5 (IDA)
.text:004006A0
.text:004006A0
.text:004006A0
.text:004006A0
.text:004006A0
.text:004006A0
.text:004006A0
.text:004006A4
.text:004006A8
.text:004006AC

main:
var_18
var_10
var_4

= −0x18
= −0x10
= −4
lui
addiu
li
sw

$gp,
$sp,
$gp,
$ra,

0x42
−0x28
0x418960
0x28+var_4($sp)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

129
.text:004006B0
.text:004006B4
.text:004006B8
.text:004006BC
.text:004006C0
.text:004006C4
.text:004006C8
.text:004006CC
.text:004006D0
.text:004006D4
.text:004006D8
.text:004006DC
.text:004006E0
.text:004006E4
.text:004006E8
.text:004006EC
.text:004006F0
.text:004006F4
.text:004006F8
Huh?"
.text:004006FC
.text:00400700
.text:00400704
.text:00400708

sw
la
lui
jalr
la
lw
lui
la
la
jalr
addiu
li
lw
beq
or
la
lui
jalr
la

$gp, 0x28+var_18($sp)
$t9, puts
$a0, 0x40
$t9 ; puts
$a0, aEnterX
# "Enter X:"
$gp, 0x28+var_18($sp)
$a0, 0x40
$t9, __isoc99_scanf
$a0, aD
# "%d"
$t9 ; __isoc99_scanf
$a1, $sp, 0x28+var_10 # branch delay slot
$v1, 1
$gp, 0x28+var_18($sp)
$v0, $v1, loc_40070C
$at, $zero
# branch delay slot, NOP
$t9, puts
$a0, 0x40
$t9 ; puts
$a0, aWhatYouEntered # "What you entered?

lw
move
jr
addiu

$ra, 0x28+var_4($sp)
$v0, $zero
$ra
$sp, 0x28

.text:0040070C loc_40070C:
.text:0040070C
la
.text:00400710
lw
.text:00400714
lui
.text:00400718
jalr
.text:0040071C
la
%d...\n"
.text:00400720
lw
.text:00400724
move
.text:00400728
jr
.text:0040072C
addiu

$t9, printf
$a1, 0x28+var_10($sp)
$a0, 0x40
$t9 ; printf
$a0, aYouEnteredD___ # "You entered
$ra, 0x28+var_4($sp)
$v0, $zero
$ra
$sp, 0x28

scanf() возвращает результат своей работы в регистре $V0 и он проверяется
по адресу 0x004006E4 сравнивая значения в $V0 и $V1 (1 записан в $V1 ранее,
на 0x004006DC). BEQ означает «Branch Equal» (переход если равно). Если значения равны (т.е. в случае успеха), произойдет переход по адресу 0x0040070C.
Упражнение
Как мы можем увидеть, инструкцию JNE/JNZ можно вполне заменить на JE/JZ
или наоборот (или BNE на BEQ и наоборот). Но при этом ещё нужно переставить
базовые блоки местами. Попробуйте сделать это в каком-нибудь примере.

1.12.5. Упражнение
• http://challenges.re/53

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

130

1.13. Стоит отметить: глобальные и локальные переменные
Теперь вы знаете, что глобальные переменные обнуляются в начале в ОС (1.12.3
(стр. 105), [ISO/IEC 9899:TC3 (C C99 standard), (2007)6.7.8p10]), а локальные –
нет (1.9.4 (стр. 51)).
Иногда, у вас есть глобальная переменная, которую вы забыли проинициализировать, и исполнение вашей программы зависит от того факта, что в начале
исполнения там ноль. Потом вы редактируете вашу программу и перемещаете глобальную переменную внутрь ф-ции, делая её локальной. Она не будет
более инициализироваться в ноль, и это может в итоге приводить к труднонаходимым ошибкам.

1.14. Доступ к переданным аргументам
Как мы уже успели заметить, вызывающая функция передает аргументы для
вызываемой через стек. А как вызываемая функция получает к ним доступ?
Листинг 1.90: простой пример
#include
int f (int a, int b, int c)
{
return a∗b+c;
};
int main()
{
printf ("%d\n", f(1, 2, 3));
return 0;
};

1.14.1. x86
MSVC
Рассмотрим пример, скомпилированный в (MSVC 2010 Express):
Листинг 1.91: MSVC 2010 Express
_TEXT
SEGMENT
_a$ = 8
_b$ = 12
_c$ = 16
_f
PROC
push
mov
mov
imul

; size = 4
; size = 4
; size = 4
ebp
ebp, esp
eax, DWORD PTR _a$[ebp]
eax, DWORD PTR _b$[ebp]

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

131
add
pop
ret
ENDP

_f
_main

_main

eax, DWORD PTR _c$[ebp]
ebp
0

PROC
push
ebp
mov
ebp, esp
push
3 ; третий аргумент
push
2 ; второй аргумент
push
1 ; первый аргумент
call
_f
add
esp, 12
push
eax
push
OFFSET $SG2463 ; '%d', 0aH, 00H
call
_printf
add
esp, 8
; возврат 0
xor
eax, eax
pop
ebp
ret
0
ENDP

Итак, здесь видно: в функции main() заталкиваются три числа в стек и вызывается функция
f(int,int,int).
Внутри f() доступ к аргументам, также как и к локальным переменным, происходит через макросы: _a$ = 8, но разница в том, что эти смещения со знаком
плюс, таким образом если прибавить макрос _a$ к указателю на EBP, то адресуется внешняя часть фрейма стека относительно EBP.
Далее всё более-менее просто: значение a помещается в EAX. Далее EAX умножается при помощи инструкции IMUL на то, что лежит в _b, и в EAX остается
произведение этих двух значений.
Далее к регистру EAX прибавляется то, что лежит в _c.
Значение из EAX никуда не нужно перекладывать, оно уже лежит где надо.
Возвращаем управление вызывающей функции — она возьмет значение из EAX
и отправит его в printf().
MSVC + OllyDbg
Проиллюстрируем всё это в OllyDbg. Когда мы протрассируем до первой инструкции в f(), которая использует какой-то из аргументов (первый), мы увидим, что EBP указывает на фрейм стека. Он выделен красным прямоугольником.
Самый первый элемент фрейма стека — это сохраненное значение EBP, затем
RA. Третий элемент это первый аргумент функции, затем второй аргумент и
третий.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

132
Для доступа к первому аргументу функции нужно прибавить к EBP 8 (2 32битных слова).
OllyDbg в курсе этого, так что он добавил комментарии к элементам стека вроде «RETURN from» и «Arg1 = …», итд.
N.B.: аргументы функции являются членами фрейма стека вызывающей функции, а не текущей. Поэтому OllyDbg отметил элементы «Arg» как члены другого
фрейма стека.

Рис. 1.23: OllyDbg: внутри функции f()
GCC
Скомпилируем то же в GCC 4.4.1 и посмотрим результат в IDA:
Листинг 1.92: GCC 4.4.1
f

public f
proc near

arg_0
arg_4
arg_8

= dword ptr
= dword ptr
= dword ptr

f

push
mov
mov
imul
add
pop
retn
endp

ebp
ebp,
eax,
eax,
eax,
ebp

8
0Ch
10h

esp
[ebp+arg_0] ; первый аргумент
[ebp+arg_4] ; второй аргумент
[ebp+arg_8] ; третий аргумент

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

133
main

public main
proc near

var_10
var_C
var_8

= dword ptr −10h
= dword ptr −0Ch
= dword ptr −8

main

push
mov
and
sub
mov
mov
mov
call
mov
mov
mov
call
mov
leave
retn
endp

ebp
ebp, esp
esp, 0FFFFFFF0h
esp, 10h
[esp+10h+var_8], 3 ; третий аргумент
[esp+10h+var_C], 2 ; второй аргумент
[esp+10h+var_10], 1 ; первый аргумент
f
edx, offset aD ; "%d\n"
[esp+10h+var_C], eax
[esp+10h+var_10], edx
_printf
eax, 0

Практически то же самое, если не считать мелких отличий описанных ранее.
После вызова обоих функций указатель стека не возвращается назад, потому
что предпоследняя инструкция LEAVE (.1.6 (стр. 1308)) делает это за один раз,
в конце исполнения.

1.14.2. x64
В x86-64 всё немного иначе, здесь аргументы функции (4 или 6) передаются
через регистры, а callee из читает их из регистров, а не из стека.
MSVC
Оптимизирующий MSVC:
Листинг 1.93: Оптимизирующий MSVC 2012 x64
$SG2997 DB
main

PROC
sub
mov
lea
lea
call
lea
mov
call
xor

'%d', 0aH, 00H

rsp, 40
edx, 2
r8d, QWORD PTR [rdx+1] ; R8D=3
ecx, QWORD PTR [rdx−1] ; ECX=1
f
rcx, OFFSET FLAT:$SG2997 ; '%d'
edx, eax
printf
eax, eax

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

134

main
f

add
ret
ENDP

rsp, 40
0

PROC
; ECX - первый аргумент
; EDX - второй аргумент
; R8D - третий аргумент
imul
ecx, edx
lea
eax, DWORD PTR [r8+rcx]
ret
0
ENDP

f

Как видно, очень компактная функция f() берет аргументы прямо из регистров.
Инструкция LEA используется здесь для сложения чисел. Должно быть компилятор посчитал, что это будет эффективнее использования ADD.
В самой main() LEA также используется для подготовки первого и третьего
аргумента: должно быть, компилятор решил, что LEA будет работать здесь
быстрее, чем загрузка значения в регистр при помощи MOV.
Попробуем посмотреть вывод неоптимизирующего MSVC:
Листинг 1.94: MSVC 2012 x64
f

proc near

; область "shadow":
arg_0
= dword ptr
arg_8
= dword ptr
arg_10
= dword ptr

8
10h
18h

; ECX - первый аргумент
; EDX - второй аргумент
; R8D - третий аргумент
mov
[rsp+arg_10], r8d
mov
[rsp+arg_8], edx
mov
[rsp+arg_0], ecx
mov
eax, [rsp+arg_0]
imul
eax, [rsp+arg_8]
add
eax, [rsp+arg_10]
retn
endp

f
main

proc near
sub
rsp, 28h
mov
r8d, 3 ; третий
mov
edx, 2 ; второй
mov
ecx, 1 ; первый
call
f
mov
edx, eax
lea
rcx, $SG2931
call
printf

аргумент
аргумент
аргумент

; "%d\n"

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

135
; возврат 0
xor
eax, eax
add
rsp, 28h
retn
endp

main

Немного путанее: все 3 аргумента из регистров зачем-то сохраняются в стеке.
Это называется «shadow space» 80 : каждая функция в Win64 может (хотя и не
обязана) сохранять значения 4-х регистров там.
Это делается по крайней мере из-за двух причин: 1) в большой функции отвести целый регистр (а тем более 4 регистра) для входного аргумента слишком
расточительно, так что к нему будет обращение через стек;
2) отладчик всегда знает, где найти аргументы функции в момент останова 81 .
Так что, какие-то большие функции могут сохранять входные аргументы в
«shadow space» для использования в будущем, а небольшие функции, как наша, могут этого и не делать.
Место в стеке для «shadow space» выделяет именно caller.
GCC
Оптимизирующий GCC также делает понятный код:
Листинг 1.95: Оптимизирующий GCC 4.4.6 x64
f:
; EDI - первый аргумент
; ESI - второй аргумент
; EDX - третий аргумент
imul
esi, edi
lea
eax, [rdx+rsi]
ret
main:
sub
mov
mov
mov
call
mov
mov
xor
call
xor
add
ret

rsp, 8
edx, 3
esi, 2
edi, 1
f
edi, OFFSET FLAT:.LC0 ; "%d\n"
esi, eax
eax, eax ; количество переданных векторных регистров
printf
eax, eax
rsp, 8

80 MSDN
81 MSDN

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

136
Неоптимизирующий GCC:
Листинг 1.96: GCC 4.4.6 x64
f:
; EDI - первый аргумент
; ESI - второй аргумент
; EDX - третий аргумент
push
rbp
mov
rbp, rsp
mov
DWORD PTR [rbp−4], edi
mov
DWORD PTR [rbp−8], esi
mov
DWORD PTR [rbp−12], edx
mov
eax, DWORD PTR [rbp−4]
imul
eax, DWORD PTR [rbp−8]
add
eax, DWORD PTR [rbp−12]
leave
ret
main:
push
mov
mov
mov
mov
call
mov
mov
mov
mov
mov
call
mov
leave
ret

rbp
rbp, rsp
edx, 3
esi, 2
edi, 1
f
edx, eax
eax, OFFSET FLAT:.LC0 ; "%d\n"
esi, edx
rdi, rax
eax, 0 ; количество переданных векторных регистров
printf
eax, 0

В соглашении о вызовах System V *NIX ([Michael Matz, Jan Hubicka, Andreas Jaeger,
Mark Mitchell, System V Application Binary Interface. AMD64 Architecture Processor
Supplement, (2013)] 82 ) нет «shadow space», но callee тоже иногда должен сохранять где-то аргументы, потому что, опять же, регистров может и не хватить
на все действия. Что мы здесь и видим.
GCC: uint64_t вместо int
Наш пример работал с 32-битным int, поэтому использовались 32-битные части
регистров с префиксом E-.
Его можно немного переделать, чтобы он заработал с 64-битными значениями:
#include
#include
82 Также доступно здесь: https://software.intel.com/sites/default/files/article/402129/
mpx-linux64-abi.pdf

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

137
uint64_t f (uint64_t a, uint64_t b, uint64_t c)
{
return a∗b+c;
};
int main()
{
printf ("%lld\n", f(0x1122334455667788,
0x1111111122222222,
0x3333333344444444));
return 0;
};

Листинг 1.97: Оптимизирующий GCC 4.4.6 x64
f

proc near
imul
rsi, rdi
lea
rax, [rdx+rsi]
retn
endp

f
main

main

proc near
sub
rsp, 8
mov
rdx, 3333333344444444h ; третий аргумент
mov
rsi, 1111111122222222h ; второй аргумент
mov
rdi, 1122334455667788h ; первый аргумент
call
f
mov
edi, offset format ; "%lld\n"
mov
rsi, rax
xor
eax, eax ; количество переданных векторных регистров
call
_printf
xor
eax, eax
add
rsp, 8
retn
endp

Собствено, всё то же самое, только используются регистры целиком, с префиксом R-.

1.14.3. ARM
Неоптимизирующий Keil 6/2013 (Режим ARM)
.text:000000A4
.text:000000A8
.text:000000AC
...
.text:000000B0
.text:000000B0
.text:000000B4
.text:000000B8
.text:000000BC

00 30 A0 E1
93 21 20 E0
1E FF 2F E1

MOV
MLA
BX

R3, R0
R0, R3, R1, R2
LR

STMFD
MOV
MOV
MOV

SP!, {R4,LR}
R2, #3
R1, #2
R0, #1

main
10
03
02
01

40
20
10
00

2D
A0
A0
A0

E9
E3
E3
E3

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

138
.text:000000C0
.text:000000C4
.text:000000C8
.text:000000CC
.text:000000D0
.text:000000D4
.text:000000D8

F7
00
04
5A
E3
00
10

FF
40
10
0F
18
00
80

FF
A0
A0
8F
00
A0
BD

EB
E1
E1
E2
EB
E3
E8

BL
MOV
MOV
ADR
BL
MOV
LDMFD

f
R4, R0
R1, R4
R0, aD_0
__2printf
R0, #0
SP!, {R4,PC}

; "%d\n"

В функции main() просто вызываются две функции, в первую (f()) передается
три значения. Как уже было упомянуто, первые 4 значения в ARM обычно передаются в первых 4-х регистрах (R0-R3). Функция f(), как видно, использует
три первых регистра (R0-R2) как аргументы.
Инструкция MLA (Multiply Accumulate) перемножает два первых операнда (R3
и R1), прибавляет к произведению третий операнд (R2) и помещает результат
в нулевой регистр (R0), через который, по стандарту, возвращаются значения
функций.
Умножение и сложение одновременно (Fused multiply–add) это часто применяемая операция. Кстати, аналогичной инструкции в x86 не было до появления
FMA-инструкций в SIMD 83 .
Самая первая инструкция MOV R3, R0, по-видимому, избыточна (можно было
бы обойтись только одной инструкцией MLA). Компилятор не оптимизировал
её, ведь, это компиляция без оптимизации.
Инструкция BX возвращает управление по адресу, записанному в LR и, если
нужно, переключает режимы процессора с Thumb на ARM или наоборот. Это
может быть необходимым потому, что, как мы видим, функции f() неизвестно,
из какого кода она будет вызываться, из ARM или Thumb. Поэтому, если она
будет вызываться из кода Thumb, BX не только возвращает управление в вызывающую функцию, но также переключает процессор в режим Thumb. Либо
не переключит, если функция вызывалась из кода для режима ARM: [ARM(R)
Architecture Reference Manual, ARMv7-A and ARMv7-R edition, (2012)A2.3.2].
Оптимизирующий Keil 6/2013 (Режим ARM)
.text:00000098
f
.text:00000098 91 20 20 E0
.text:0000009C 1E FF 2F E1

MLA
BX

R0, R1, R0, R2
LR

А вот и функция f(), скомпилированная компилятором Keil в режиме полной
оптимизации (-O3). Инструкция MOV была оптимизирована: теперь MLA использует все входящие регистры и помещает результат в R0, где вызываемая функция будет его читать и использовать.
Оптимизирующий Keil 6/2013 (Режим Thumb)
83 wikipedia

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

139
.text:0000005E 48 43
.text:00000060 80 18
.text:00000062 70 47

MULS
ADDS
BX

R0, R1
R0, R0, R2
LR

В режиме Thumb инструкция MLA недоступна, так что компилятору пришлось
сгенерировать код, делающий обе операции по отдельности.
Первая инструкция MULS умножает R0 на R1, оставляя результат в R0. Вторая
(ADDS) складывает результат и R2, оставляя результат в R0.
ARM64
Оптимизирующий GCC (Linaro) 4.9
Тут всё просто. MADD это просто инструкция, производящая умножение и сложение одновременно (как MLA, которую мы уже видели). Все 3 аргумента передаются в 32-битных частях X-регистров. Действительно, типы аргументов это
32-битные int’ы. Результат возвращается в W0.
Листинг 1.98: Оптимизирующий GCC (Linaro) 4.9
f:
madd
ret

w0, w0, w1, w2

main:
; сохранить FP и LR в стековом фрейме:
stp
x29, x30, [sp, −16]!
mov
w2, 3
mov
w1, 2
add
x29, sp, 0
mov
w0, 1
bl
f
mov
w1, w0
adrp
x0, .LC7
add
x0, x0, :lo12:.LC7
bl
printf
; возврат 0
mov
w0, 0
; восстановить FP и LR
ldp
x29, x30, [sp], 16
ret
.LC7:
.string "%d\n"

Также расширим все типы данных до 64-битных uint64_t и попробуем:
#include
#include
uint64_t f (uint64_t a, uint64_t b, uint64_t c)
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

140
{
return a∗b+c;
};
int main()
{
printf ("%lld\n", f(0x1122334455667788,
0x1111111122222222,
0x3333333344444444));
return 0;
};
f:
madd
ret

x0, x0, x1, x2

mov
adrp
stp
movk
add
movk
add
movk
bl
mov
ldp
ret

x1, 13396
x0, .LC8
x29, x30, [sp, −16]!
x1, 0x27d0, lsl 16
x0, x0, :lo12:.LC8
x1, 0x122, lsl 32
x29, sp, 0
x1, 0x58be, lsl 48
printf
w0, 0
x29, x30, [sp], 16

main:

.LC8:
.string "%lld\n"

Функция f() точно такая же, только теперь используются полные части 64битных X-регистров. Длинные 64-битные значения загружаются в регистры по
частям, это описано здесь: 1.39.3 (стр. 567).
Неоптимизирующий GCC (Linaro) 4.9
Неоптимизирующий компилятор выдает немного лишнего кода:
f:
sub
str
str
str
ldr
ldr
mul
ldr
add
add
ret

sp,
w0,
w1,
w2,
w1,
w0,
w1,
w0,
w0,
sp,

sp, #16
[sp,12]
[sp,8]
[sp,4]
[sp,12]
[sp,8]
w1, w0
[sp,4]
w1, w0
sp, 16

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

141
Код сохраняет входные аргументы в локальном стеке на случай если кому-то
(или чему-то) в этой функции понадобится использовать регистры W0...W2, перезаписывая оригинальные аргументы функции, которые могут понадобится
в будущем. Это называется Register Save Area. [Procedure Call Standard for the
ARM 64-bit Architecture (AArch64), (2013)]84 . Вызываемая функция не обязана
сохранять их. Это то же что и «Shadow Space»: 1.14.2 (стр. 135).
Почему оптимизирующий GCC 4.9 убрал этот, сохраняющий аргументы, код?
Потому что он провел дополнительную работу по оптимизации и сделал вывод,
что аргументы функции не понадобятся в будущем и регистры W0...W2 также
не будут использоваться.
Также мы видим пару инструкций MUL/ADD вместо одной MADD.

1.14.4. MIPS
Листинг 1.99: Оптимизирующий GCC 4.4.5
.text:00000000 f:
; $a0=a
; $a1=b
; $a2=c
.text:00000000
mult
$a1,
.text:00000004
mflo
$v0
.text:00000008
jr
$ra
.text:0000000C
addu
$v0,
; результат в $v0 во время выхода
.text:00000010 main:
.text:00000010
.text:00000010 var_10 = −0x10
.text:00000010 var_4
= −4
.text:00000010
.text:00000010
lui
$gp,
.text:00000014
addiu
$sp,
.text:00000018
la
$gp,
.text:0000001C
sw
$ra,
.text:00000020
sw
$gp,
; установить c:
.text:00000024
li
$a2,
; установить a:
.text:00000028
li
$a0,
.text:0000002C
jal
f
; установить b:
.text:00000030
li
$a1,
; результат сейчас в $v0
.text:00000034
lw
$gp,
.text:00000038
lui
$a0,
.text:0000003C
lw
$t9,
.text:00000040
la
$a0,
.text:00000044
jalr
$t9
84 Также

доступно здесь:
IHI0055B_aapcs64.pdf

$a0

$a2, $v0

; branch delay slot

(__gnu_local_gp >> 16)
−0x20
(__gnu_local_gp & 0xFFFF)
0x20+var_4($sp)
0x20+var_10($sp)
3
1

2

; branch delay slot

0x20+var_10($sp)
($LC0 >> 16)
(printf & 0xFFFF)($gp)
($LC0 & 0xFFFF)

http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

142
; взять результат ф-ции f() и передать его
; как второй аргумент в printf():
.text:00000048
move
$a1, $v0
; branch delay slot
.text:0000004C
lw
$ra, 0x20+var_4($sp)
.text:00000050
move
$v0, $zero
.text:00000054
jr
$ra
.text:00000058
addiu
$sp, 0x20
; branch delay slot

Первые 4 аргумента функции передаются в четырех регистрах с префиксами
A-.
В MIPS есть два специальных регистра: HI и LO, которые выставляются в 64битный результат умножения во время исполнения инструкции MULT.
К регистрам можно обращаться только используя инструкции MFLO и MFHI. Здесь
MFLO берет младшую часть результата умножения и записывает в $V0. Так что
старшая 32-битная часть результата игнорируется (содержимое регистра HI
не используется). Действительно, мы ведь работаем с 32-битным типом int.
И наконец, ADDU («Add Unsigned» — добавить беззнаковое) прибавляет значение третьего аргумента к результату.
В MIPS есть две разных инструкции сложения: ADD и ADDU. На самом деле, дело не в знаковых числах, а в исключениях: ADD может вызвать исключение во
время переполнения. Это иногда полезно85 и поддерживается, например, в ЯП
Ada.
ADDU не вызывает исключения во время переполнения. А так как Си/Си++не
поддерживает всё это, мы видим здесь ADDU вместо ADD.
32-битный результат оставляется в $V0.
В main() есть новая для нас инструкция: JAL («Jump and Link»). Разница между
JAL и JALR в том, что относительное смещение кодируется в первой инструкции, а JALR переходит по абсолютному адресу, записанному в регистр («Jump
and Link Register»).
Обе функции f() и main() расположены в одном объектном файле, так что
относительный адрес f() известен и фиксирован.

1.15. Ещё о возвращаемых результатах
Результат выполнения функции в x86 обычно возвращается 86 через регистр
EAX, а если результат имеет тип байт или символ (char), то в самой младшей
части EAX — AL. Если функция возвращает число с плавающей запятой, то будет использован регистр FPU ST(0). В ARM обычно результат возвращается в
регистре R0.
85 http://blog.regehr.org/archives/1154
86 См.

также: MSDN: Return Values (C++): MSDN

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

143

1.15.1. Попытка использовать результат функции возвращающей void
Кстати, что будет, если возвращаемое значение в функции main() объявлять
не как int, а как void? Т.н. startup-код вызывает main() примерно так:
push
push
push
call
push
call

envp
argv
argc
main
eax
exit

Иными словами:
exit(main(argc,argv,envp));

Если вы объявите main() как void, и ничего не будете возвращать явно (при
помощи выражения return), то в единственный аргумент exit() попадет то, что
лежало в регистре EAX на момент выхода из main(). Там, скорее всего, будет
какие-то случайное число, оставшееся от работы вашей функции. Так что код
завершения программы будет псевдослучайным.
Мы можем это проиллюстрировать. Заметьте, что у функции main() тип возвращаемого значения именно void:
#include
void main()
{
printf ("Hello, world!\n");
};

Скомпилируем в Linux.
GCC 4.8.1 заменила printf() на puts() (мы видели это прежде: 1.5.3 (стр. 28)),
но это нормально, потому что puts() возвращает количество выведенных символов, так же как и printf(). Обратите внимание на то, что EAX не обнуляется
перед выходом из main(). Это значит что EAX перед выходом из main() содержит то, что puts() оставляет там.
Листинг 1.100: GCC 4.8.1
.LC0:
.string "Hello, world!"
main:
push
mov
and
sub
mov
call
leave
ret

ebp
ebp, esp
esp, −16
esp, 16
DWORD PTR [esp], OFFSET FLAT:.LC0
puts

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

144
Напишем небольшой скрипт на bash, показывающий статус возврата («exit
status» или «exit code»):
Листинг 1.101: tst.sh
#!/bin/sh
./hello_world
echo $?

И запустим:
$ tst.sh
Hello, world!
14

14 это как раз количество выведенных символов. Количество выведенных символов проскальзывает из printf() через EAX/RAX в «exit code».
Кстати, когда в Hex-Rays мы разбираем C++ код, нередко можно наткнуться
на ф-цию, которая заканчивается деструктором какого-либо класса:
...
call
mov
pop
pop
mov
add
retn

??1CString@@QAE@XZ ; CString::~CString(void)
ecx, [esp+30h+var_C]
edi
ebx
large fs:0, ecx
esp, 28h

По стандарту С++ деструкторы ничего не возвращают, но когда Hex-Rays об
этом не знает и думает, что и деструктор и эта ф-ция по умолчанию возвращает int, то на выходе получается такой код:
...
return CString::~CString(&Str);
}

1.15.2. Что если не использовать результат функции?
printf() возвращает количество успешно выведенных символов, но результат
работы этой функции редко используется на практике.
Можно даже явно вызывать функции, чей смысл именно в возвращаемых значениях, но явно не использовать их:
int f()
{
// пропускаем первые 3 случайных значения:
rand();
rand();
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

145
rand();
// и используем 4-е:
return rand();
};

Результат работы rand() остается в EAX во всех четырех случаях. Но в первых
трех случаях значение, лежащее в EAX, просто не используется.

1.15.3. Возврат структуры
Вернемся к тому факту, что возвращаемое значение остается в регистре EAX.
Вот почему старые компиляторы Си не способны создавать функции, возвращающие нечто большее, нежели помещается в один регистр (обычно тип int),
а когда нужно, приходится возвращать через указатели, указываемые в аргументах. Так что, как правило, если функция должна вернуть несколько значений, она возвращает только одно, а остальные — через указатели. Хотя позже
и стало возможным, вернуть, скажем, целую структуру, но этот метод до сих
пор не очень популярен. Если функция должна вернуть структуру, вызывающая функция должна сама, скрыто и прозрачно для программиста, выделить
место и передать указатель на него в качестве первого аргумента. Это почти
то же самое что и сделать это вручную, но компилятор прячет это.
Небольшой пример:
struct s
{
int a;
int b;
int c;
};
struct s get_some_values (int a)
{
struct s rt;
rt.a=a+1;
rt.b=a+2;
rt.c=a+3;
return rt;
};

…получим (MSVC 2010 /Ox):
$T3853 = 8
; size = 4
_a$ = 12
; size = 4
?get_some_values@@YA?AUs@@H@Z PROC
mov
ecx, DWORD PTR _a$[esp−4]
mov
eax, DWORD PTR $T3853[esp−4]
lea
edx, DWORD PTR [ecx+1]
mov
DWORD PTR [eax], edx
lea
edx, DWORD PTR [ecx+2]

; get_some_values

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

146
add
ecx, 3
mov
DWORD PTR [eax+4], edx
mov
DWORD PTR [eax+8], ecx
ret
0
?get_some_values@@YA?AUs@@H@Z ENDP

; get_some_values

$T3853 это имя внутреннего макроса для передачи указателя на структуру.
Этот пример можно даже переписать, используя расширения C99:
struct s
{
int a;
int b;
int c;
};
struct s get_some_values (int a)
{
return (struct s){.a=a+1, .b=a+2, .c=a+3};
};

Листинг 1.102: GCC 4.8.1
_get_some_values proc near
ptr_to_struct
a

= dword ptr
= dword ptr

mov
mov
lea
mov
lea
add
mov
mov
retn
_get_some_values endp

4
8

edx, [esp+a]
eax, [esp+ptr_to_struct]
ecx, [edx+1]
[eax], ecx
ecx, [edx+2]
edx, 3
[eax+4], ecx
[eax+8], edx

Как видно, функция просто заполняет поля в структуре, выделенной вызывающей функцией. Как если бы передавался просто указатель на структуру. Так
что никаких проблем с эффективностью нет.

1.16. Указатели
1.16.1. Возврат значений
Указатели также часто используются для возврата значений из функции (вспомните случай со scanf() (1.12 (стр. 89))).
Например, когда функции нужно вернуть сразу два значения.
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

147
Пример с глобальными переменными
#include
void f1 (int x, int y, int ∗sum, int ∗product)
{
∗sum=x+y;
∗product=x∗y;
};
int sum, product;
void main()
{
f1(123, 456, &sum, &product);
printf ("sum=%d, product=%d\n", sum, product);
};

Это компилируется в:
Листинг 1.103: Оптимизирующий MSVC 2010 (/Ob0)
COMM
_product:DWORD
COMM
_sum:DWORD
$SG2803 DB
'sum=%d, product=%d', 0aH, 00H
_x$ = 8
_y$ = 12
_sum$ = 16
_product$ = 20
_f1
PROC
mov
mov
lea
imul
mov
push
mov
mov
mov
pop
ret
_f1
ENDP
_main

PROC
push
push
push
push
call
mov
mov
push
push

;
;
;
;

size
size
size
size

=
=
=
=

4
4
4
4

ecx, DWORD PTR _y$[esp−4]
eax, DWORD PTR _x$[esp−4]
edx, DWORD PTR [eax+ecx]
eax, ecx
ecx, DWORD PTR _product$[esp−4]
esi
esi, DWORD PTR _sum$[esp]
DWORD PTR [esi], edx
DWORD PTR [ecx], eax
esi
0

OFFSET _product
OFFSET _sum
456
; 000001c8H
123
; 0000007bH
_f1
eax, DWORD PTR _product
ecx, DWORD PTR _sum
eax
ecx

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

148

_main

push
call
add
xor
ret
ENDP

OFFSET $SG2803
DWORD PTR __imp__printf
esp, 28
eax, eax
0

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

149
Посмотрим это в OllyDbg:

Рис. 1.24: OllyDbg: передаются адреса двух глобальных переменных в f1()
В начале адреса обоих глобальных переменных передаются в f1(). Можно нажать «Follow in dump» на элементе стека и в окне слева увидим место в сегменте данных, выделенное для двух переменных.
Эти переменные обнулены, потому что по стандарту неинициализированные
данные (BSS) обнуляются перед началом исполнения: [ISO/IEC 9899:TC3 (C C99
standard), (2007)6.7.8p10].

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

150
И они находятся в сегменте данных, о чем можно удостовериться, нажав Alt-M
и увидев карту памяти:

Рис. 1.25: OllyDbg: карта памяти

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

151
Трассируем (F7) до начала исполнения f1():

Рис. 1.26: OllyDbg: начало работы f1()
В стеке видны значения 456 (0x1C8) и 123 (0x7B), а также адреса двух глобальных переменных.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

152
Трассируем до конца f1(). Мы видим в окне слева, как результаты вычисления
появились в глобальных переменных:

Рис. 1.27: OllyDbg: f1() заканчивает работу

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

153
Теперь из глобальных переменных значения загружаются в регистры для передачи в printf():

Рис. 1.28: OllyDbg: адреса глобальных переменных передаются в printf()
Пример с локальными переменными
Немного переделаем пример:
Листинг 1.104: теперь переменные локальные
void main()
{
int sum, product; // теперь эти переменные для этой ф-ции --локальные
f1(123, 456, &sum, &product);
printf ("sum=%d, product=%d\n", sum, product);
};

Код функции f1() не изменится. Изменится только main():
Листинг 1.105: Оптимизирующий MSVC 2010 (/Ob0)
_product$ = −8
_sum$ = −4
_main
PROC
; Line 10
sub
; Line 13
lea
push
lea
push

; size = 4
; size = 4

esp, 8
eax, DWORD PTR _product$[esp+8]
eax
ecx, DWORD PTR _sum$[esp+12]
ecx

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

154
push
push
call
; Line 14
mov
mov
push
push
push
call
; Line 15
xor
add
ret

456
123
_f1

; 000001c8H
; 0000007bH

edx, DWORD PTR _product$[esp+24]
eax, DWORD PTR _sum$[esp+24]
edx
eax
OFFSET $SG2803
DWORD PTR __imp__printf
eax, eax
esp, 36
0

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

155
Снова посмотрим в OllyDbg. Адреса локальных переменных в стеке это 0x2EF854
и 0x2EF858. Видно, как они заталкиваются в стек:

Рис. 1.29: OllyDbg: адреса локальных переменных заталкиваются в стек

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

156
Начало работы f1(). В стеке по адресам 0x2EF854 и 0x2EF858 пока находится
случайный мусор:

Рис. 1.30: OllyDbg: f1() начинает работу

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

157
Конец работы f1():

Рис. 1.31: OllyDbg: f1() заканчивает работу
В стеке по адресам 0x2EF854 и 0x2EF858 теперь находятся значения 0xDB18 и
0x243, это результаты работы f1().
Вывод
f1() может одинаково хорошо возвращать результаты работы в любые места
памяти. В этом суть и удобство указателей. Кстати, references в Си++работают
точно так же. Читайте больше об этом: (3.19.3 (стр. 728)).

1.16.2. Обменять входные значения друг с другом
Вот так:
#include
#include
void swap_bytes (unsigned char∗ first, unsigned char∗ second)
{
unsigned char tmp1;
unsigned char tmp2;
tmp1=∗first;
tmp2=∗second;
∗first=tmp2;
∗second=tmp1;
};

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

158
int main()
{
// копируем строку в кучу, чтобы у нас была возможность эту самую
строку модифицировать
char ∗s=strdup("string");
// меняем 2-й и 3-й символы
swap_bytes (s+1, s+2);
printf ("%s\n", s);
};

Как видим, байты загружаются в младшие 8-битные части регистров ECX и EBX
используя MOVZX (так что старшие части регистров очищаются), затем байты
записываются назад в другом порядке.
Листинг 1.106: Optimizing GCC 5.4
swap_bytes:
push
mov
mov
movzx
movzx
mov
mov
pop
ret

ebx
edx,
eax,
ecx,
ebx,
BYTE
BYTE
ebx

DWORD PTR [esp+8]
DWORD PTR [esp+12]
BYTE PTR [edx]
BYTE PTR [eax]
PTR [edx], bl
PTR [eax], cl

Адреса обоих байтов берутся из аргументов и во время исполнения ф-ции находятся в регистрах EDX и EAX.
Так что исопльзуем указатели — вероятно, без них нет способа решить эту
задачу лучше.

1.17. Оператор GOTO
Оператор GOTO считается анти-паттерном, см: [Edgar Dijkstra, Go To Statement
Considered Harmful (1968)87 ]. Но тем не менее, его можно использовать в разумных пределах, см: [Donald E. Knuth, Structured Programming with go to Statements
(1974)88 ] 89 .
Вот простейший пример:
#include
int main()
{
87 http://yurichev.com/mirrors/Dijkstra68.pdf
88 http://yurichev.com/mirrors/KnuthStructuredProgrammingGoTo.pdf
89 В

[Денис Юричев, Заметки о языке программирования Си/Си++] также есть примеры.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

159
printf ("begin\n");
goto exit;
printf ("skip me!\n");
exit:
printf ("end\n");
};

Вот что мы получаем в MSVC 2012:
Листинг 1.107: MSVC 2012
$SG2934 DB
$SG2936 DB
$SG2937 DB
_main

PROC
push
mov
push
call
add
jmp
push
call
add

'begin', 0aH, 00H
'skip me!', 0aH, 00H
'end', 0aH, 00H

ebp
ebp, esp
OFFSET $SG2934 ; 'begin'
_printf
esp, 4
SHORT $exit$3
OFFSET $SG2936 ; 'skip me!'
_printf
esp, 4

$exit$3:

_main

push
call
add
xor
pop
ret
ENDP

OFFSET $SG2937 ; 'end'
_printf
esp, 4
eax, eax
ebp
0

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

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

160
Это также может быть простым упражнением на модификацию кода.
Откроем исполняемый файл в Hiew:

Рис. 1.32: Hiew

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

161
Поместите курсор по адресу JMP (0x410), нажмите F3 (редактирование), нажмите два нуля, так что опкод становится EB 00:

Рис. 1.33: Hiew
Второй байт опкода JMP это относительное смещение от перехода. 0 означает
место прямо после текущей инструкции. Теперь JMP не будет пропускать следующий вызов printf(). Нажмите F9 (запись) и выйдите. Теперь мы запускаем
исполняемый файл и видим это:
Листинг 1.108: Результат
C:\...>goto.exe
begin
skip me!
end

Подобного же эффекта можно достичь, если заменить инструкцию JMP на две
инструкции NOP. NOP имеет опкод 0x90 и длину в 1 байт, так что нужно 2 инструкции для замены.

1.17.1. Мертвый код
Вызов второго printf() также называется «мертвым кодом» («dead code») в
терминах компиляторов. Это значит, что он никогда не будет исполнен. Так
что если вы компилируете этот пример с оптимизацией, компилятор удаляет
«мертвый код» не оставляя следа:
Листинг 1.109: Оптимизирующий MSVC 2012
$SG2981 DB

'begin', 0aH, 00H

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

162
$SG2983 DB
$SG2984 DB
_main

PROC
push
call
push

'skip me!', 0aH, 00H
'end', 0aH, 00H

OFFSET $SG2981 ; 'begin'
_printf
OFFSET $SG2984 ; 'end'

$exit$4:

_main

call
add
xor
ret
ENDP

_printf
esp, 8
eax, eax
0

Впрочем, строку «skip me!» компилятор убрать забыл.

1.17.2. Упражнение
Попробуйте добиться того же самого в вашем любимом компиляторе и отладчике.

1.18. Условные переходы
1.18.1. Простой пример
#include
void f_signed (int a, int b)
{
if (a>b)
printf ("a>b\n");
if (a==b)
printf ("a==b\n");
if (ab)
printf ("a>b\n");
if (a==b)
printf ("a==b\n");
if (ab\n, а BLGT вызывает printf(). Следовательно, эти
инструкции с суффиксом -GT исполнятся только в том случае, если значение
в R0 (там a) было больше, чем значение в R4 (там b).
Далее мы увидим инструкции ADREQ и BLEQ. Они работают так же, как и ADR и
BL, но исполнятся только если значения при последнем сравнении были равны.
Перед ними расположен ещё один CMP, потому что вызов printf() мог испортить состояние флагов.
Далее мы увидим LDMGEFD. Эта инструкция работает так же, как и LDMFD90 , но
сработает только если в результате сравнения одно из значений было больше
или равно второму (Greater or Equal). Смысл инструкции LDMGEFD SP!, {R4-R6,PC}
в том, что это как бы эпилог функции, но он сработает только если a >= b, только тогда работа функции закончится.
Но если это не так, то есть a < b, то исполнение дойдет до следующей инструкции LDMFD SP!, {R4-R6,LR}. Это ещё один эпилог функции. Эта инструкция
восстанавливает состояние регистров R4-R6, но и LR вместо PC, таким образом, пока что, не делая возврата из функции.
Последние две инструкции вызывают printf() со строкой «ab)
beq
.L20
; Branch if Equal (переход, если равно) (a==b)
bge
.L15
; Branch if Greater than or Equal (переход, если
больше или равно) (a>=b) (здесь это невозможно)
; ab)
cmp
w19, w1
beq
.L26
; Branch if Equal (переход, если равно) (a==b)
.L23:
bcc
.L27
; Branch if Carry Clear (если нет переноса)(если
меньше, чем) (a 16) # "a>b"
.text:00000040
addiu
$a0, $v0, (unk_230 & 0xFFFF) # "a>b"
.text:00000044
lw
$v0, (puts & 0xFFFF)($gp)
.text:00000048
or
$at, $zero ; NOP
.text:0000004C
move
$t9, $v0
.text:00000050
jalr
$t9
.text:00000054
or
$at, $zero ; branch delay slot, NOP
.text:00000058
lw
$gp, 0x20+var_10($fp)
.text:0000005C
.text:0000005C loc_5C:
# CODE XREF: f_signed+34
.text:0000005C
lw
$v1, 0x20+arg_0($fp)
.text:00000060
lw
$v0, 0x20+arg_4($fp)
.text:00000064
or
$at, $zero ; NOP
; проверить a==b, перейти на loc_90, если это не так:
.text:00000068
bne
$v1, $v0, loc_90
.text:0000006C
or
$at, $zero ; branch delay slot, NOP
; условие верно, вывести "a==b" и закончить:
.text:00000070
lui
$v0, (aAB >> 16) # "a==b"
.text:00000074
addiu
$a0, $v0, (aAB & 0xFFFF) # "a==b"
.text:00000078
lw
$v0, (puts & 0xFFFF)($gp)
.text:0000007C
or
$at, $zero ; NOP
.text:00000080
move
$t9, $v0
.text:00000084
jalr
$t9
.text:00000088
or
$at, $zero ; branch delay slot, NOP
.text:0000008C
lw
$gp, 0x20+var_10($fp)
.text:00000090
.text:00000090 loc_90:
# CODE XREF: f_signed+68
.text:00000090
lw
$v1, 0x20+arg_0($fp)
.text:00000094
lw
$v0, 0x20+arg_4($fp)
.text:00000098
or
$at, $zero ; NOP
; проверить условие $v1 16) # "a_

Посмотрим внутри исходного кода CRT компилятора Borland C++ 3.1, файл
c0.asm:
; _checknull()

check for null pointer zapping copyright message

...
; Check for null pointers before exit
__checknull

PROC
PUBLIC

DIST
__checknull

IF

LDATA EQ false
__TINY__
push
si
push
di
mov
es, cs:DGROUP@@
xor
ax, ax
mov
si, ax
mov
cx, lgth_CopyRight
ComputeChecksum label
near
add
al, es:[si]
adc
ah, 0
inc
si
loop
ComputeChecksum
sub
ax, CheckSum
jz
@@SumOK
mov
cx, lgth_NullCheck
mov
dx, offset DGROUP: NullCheck
call
ErrorDisplay
@@SumOK:
pop
di
pop
si
ENDIF
ENDIF
IFNDEF

_DATA

SEGMENT

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

792
; Magic symbol used by the debug info to locate the data segment
public DATASEG@
DATASEG@
label
byte
; The CopyRight string must NOT be moved or changed without
; changing the null pointer check logic
CopyRight
lgth_CopyRight

db
db
equ

4 dup(0)
'Borland C++ − Copyright 1991 Borland Intl.',0
$ − CopyRight

IF
LDATA EQ false
IFNDEF __TINY__
CheckSum
equ
00D5Ch
NullCheck
db
'Null pointer assignment', 13, 10
lgth_NullCheck equ
$ − NullCheck
ENDIF
ENDIF
...

Модель памяти в MS-DOS крайне странная (10.6 (стр. 1274)), и, вероятно, её и
не нужно изучать, если только вы не фанат ретрокомпьютинга или ретрогейминга. Одну только вещь можно держать в памяти, это то, что сегмент памяти
(включая сегмент данных) в MS-DOS это место где хранится код или данные,
но в отличие от “серьезных” ОС, он начинается с нулевого адреса.
И в Borland C++ CRT, сегмент данных начинается с 4-х нулевых байт и строки
копирайта “Borland C++ - Copyright 1991 Borland Intl.”. Целостность 4-х нулевых байт и текстовой строки проверяется в конце, и если что-то нарушено,
выводится сообщение об ошибке.
Но зачем? Запись по нулевому указателю это распространенная ошибка в Си/Си++, и если вы делаете это в *NIX или Windows, ваше приложение упадет. В
MS-DOS нет защиты памяти, так что это приходится проверять в CRT во время
выхода, пост-фактум. Если вы видите это сообщение, значит ваша программа
в каком-то месте что-то записала по нулевому адресу.
Наша программа это сделала. И вот почему число 1234 было прочитано корректно: потому что оно было записано на месте первых 4-х байт. Контрольная
сумма во время выхода неверна (потому что наше число там осталось), так что
сообщение было выведено.
Прав ли я? Я переписал программу для проверки моих предположений:
#include
int main()
{
int ∗ptr=NULL;
∗ptr=1234;
printf ("Now let's read at NULL\n");
printf ("%d\n", ∗ptr);
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

793
∗ptr=0; // psst, cover our tracks!
};

Программа исполняется без ошибки во время выхода.
Хотя и метод предупреждать о записи по нулевому указателю имел смысл в
MS-DOS, вероятно, это всё может использоваться и сегодня, на маломощных
MCU без защиты памяти и/или MMU40 .
Почему кому-то может понадобиться писать по нулевому адресу?
Но почему трезвомыслящему программисту может понадобиться записывать
что-то по нулевому адресу? Это может быть сделано случайно, например, указатель должен быть инициализирован и указывать на только что выделенный
блок в памяти, а затем должен быть передан в какую-то ф-цию, возвращающую данные через указатель.
int ∗ptr=NULL;
... мы забыли выделить память и инициализировать ptr
strcpy (ptr, buf); // strcpy() завершает работу молча, потому что в MS-DOS
нет защиты памяти

И даже хуже:
int ∗ptr=malloc(1000);
... мы забыли проверить, действительно ли память была выделена: это же MS−⤦
Ç DOS и у тогдашних компьютеров было мало памяти,
... и нехватка памяти была обычной ситуацией.
... если malloc() вернул NULL, тогда ptr будет тоже NULL.
strcpy (ptr, buf); // strcpy() завершает работу молча, потому что в MS-DOS
нет защиты памяти

Писать по нулевому адресу намеренно
Вот пример из dmalloc41 , портабельный (переносимый) способ сгенерировать
core dump, в отсутствии иных способов:
3.4 Generating a Core File on Errors
====================================
If the `error−abort' debug token has been enabled, when the library
detects any problems with the heap memory, it will immediately attempt
to dump a core file. ∗Note Debug Tokens::. Core files are a complete
copy of the program and it's state and can be used by a debugger to see
specifically what is going on when the error occurred. ∗Note Using
With a Debugger::. By default, the low, medium, and high arguments to
40 Memory

Management Unit

41 http://dmalloc.com/

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

794
the library utility enable the `error−abort' token. You can disable
this feature by entering `dmalloc −m error−abort' (−m for minus) to
remove the `error−abort' token and your program will just log errors
and continue. You can also use the `error−dump' token which tries to
dump core when it sees an error but still continue running. ∗Note
Debug Tokens::.
When a program dumps core, the system writes the program and all of
its memory to a file on disk usually named `core'. If your program is
called `foo' then your system may dump core as `foo.core'. If you are
not getting a `core' file, make sure that your program has not changed
to a new directory meaning that it may have written the core file in a
different location. Also insure that your program has write privileges
over the directory that it is in otherwise it will not be able to dump
a core file. Core dumps are often security problems since they contain
all program memory so systems often block their being produced. You
will want to check your user and system's core dump size ulimit
settings.
The library by default uses the `abort' function to dump core which
may or may not work depending on your operating system. If the
following program does not dump core then this may be the problem. See
`KILL_PROCESS' definition in `settings.dist'.
main()
{
abort();
}
If `abort' does work then you may want to try the following setting
in `settings.dist'. This code tries to generate a segmentation fault
by dereferencing a `NULL' pointer.
#define KILL_PROCESS

{ int ∗_int_p = 0L; ∗_int_p = 1; }

NULL в Си/Си++
NULL в C/C++ это просто макрос, который часто определяют так:
#define NULL

((void∗)0)

( libio.h file )
void* это тип данных, отражающий тот факт, что это указатель, но на значение
неизвестного типа (void).
NULL обычно используется чтобы показать отсутствие объекта. Например, у
вас есть односвязный список, и каждый узел имеет значение (или указатель
на значение) и указатель вроде next. Чтобы показать, что следующего узла
нет, в поле next записывается 0. (Остальные решения просто хуже.) Вероятно, вы можете использовать какую-то крайне экзотическую среду, где можно
выделить память по нулевому адресу. Как вы будете показывать отсутствие
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

795
следующего узла? Какой-нибудь magic number? Может быть -1? Или дополнительным битом?
В Википедии мы можем найти это:
In fact, quite contrary to the zero page’s original preferential use,
some modern operating systems such as FreeBSD, Linux and Microsoft
Windows[2] actually make the zero page inaccessible to trap uses of
NULL pointers.
( https://en.wikipedia.org/wiki/Zero_page )
Нулевой указатель на ф-цию
Можно вызывать ф-ции по их адресу. Например, я компилирую это при помощи
MSVC 2010 и запускаю в Windows 7:
#include
#include
int main()
{
printf ("0x%x\n", &MessageBoxA);
};

Результат 0x7578feae, и он не меняется и после того, как я запустил это несколько раз, потому что user32.dll (где находится ф-ция MessageBoxA) всегда загружается по одному и тому же адресу. И потому что ASLR42 не включено (тогда
результат был бы всё время разным).
Вызовем ф-цию MessageBoxA() по адресу:
#include
#include
typedef int (∗msgboxtype)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption,
Ç UINT uType);
int main()
{
msgboxtype msgboxaddr=0x7578feae;
// заставить загрузиться DLL в память процесса,
// т.к., наш код не использует никакую ф-цию из user32.dll,
// и DLL не импортируется
LoadLibrary ("user32.dll");
msgboxaddr(NULL, "Hello, world!", "hello", MB_OK);
};
42 Address

Space Layout Randomization

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!



796
Странно выглядит, но работает в Windows 7 x86.
Это часто используется в шеллкодах, потому что оттуда трудно вызывать фции из DLL по их именам. А ASLR это контрмера.
И вот теперь что по-настоящему странно выглядит, некоторые программисты
на Си для встраиваемых (embedded) систем, могут быть знакомы с таким кодом:
int reset()
{
void (∗foo)(void) = 0;
foo();
};

Кому понадобится вызывать ф-цию по адресу 0? Это портабельный способ перейти на нулевой адрес. Множество маломощных микроконтроллеров не имеют защиты памяти или MMU, и после сброса, они просто начинают исполнять
код по нулевому адресу, где может быть записан инициализирующий код. Так
что переход по нулевому адресу это способ сброса. Можно использовать и
inline-ассемблер, но если это неудобно, тогда можно использовать этот портабельный метод.
Это даже корректно компилируется при помощи GCC 4.8.4 на Linux x64:
reset:
sub
xor
call
add
ret

rsp, 8
eax, eax
rax
rsp, 8

То обстоятельство, что указатель стека сдвинут, это не проблема: инициализирующий код в микроконтроллерах обычно полностью игнорирует состояние
регистров и памяти и загружает всё “с чистого листа”.
И конечно, этот код упадет в *NIX или Windows, из-за защиты памяти, и даже
если бы её не было, по нулевому адресу нет никакого кода.
В GCC даже есть нестандартное расширение, позволяющее перейти по определенному адресу, вместо того чтобы вызывать ф-цию: http://gcc.gnu.org/
onlinedocs/gcc/Labels-as-Values.html.

3.21.5. Массив как аргумент функции
Кто-то может спросить, какая разница между объявлением аргумента ф-ции
как массива и как указателя?
Как видно, разницы вообще нет:
void write_something1(int a[16])
{
a[5]=0;
};
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

797
void write_something2(int ∗a)
{
a[5]=0;
};
int f()
{
int a[16];
write_something1(a);
write_something2(a);
};

Оптимизирующий GCC 4.8.4:
write_something1:
mov
DWORD PTR [rdi+20], 0
ret
write_something2:
mov
DWORD PTR [rdi+20], 0
ret

Но вы можете объявлять массив вместо указателя для самодокументации, если размер массива известен зараннее и определен. И может быть, какой-нибудь
инструмент для статического анализа выявит возможное переполнение буфера. Или такие инструменты есть уже сегодня?
Некоторые люди, включая Линуса Торвальдса, критикуют эту возможность Си/Си++: https://lkml.org/lkml/2015/9/3/428.
В стандарте C99 имеется также ключевое слово static [ISO/IEC 9899:TC3 (C C99
standard), (2007) 6.7.5.3]:
If the keyword static also appears within the [ and ] of the array
type derivation, then for each call to the function, the value of
the corresponding actual argument shall provide access to the first
element of an array with at least as many elements as specified by
the size expression.

3.21.6. Указатель на функцию
Имя ф-ции в Си/Си++ без скобок, как “printf” это указатель на ф-цию типа void
(*)(). Попробуем прочитать содержимое ф-ции и пропатчить его:
#include
#include
void print_something ()
{
printf ("we are in %s()\n", __FUNCTION__);
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

798
};
int main()
{
print_something();
printf ("first 3 bytes: %x %x %x...\n",
∗(unsigned char∗)print_something,
∗((unsigned char∗)print_something+1),
∗((unsigned char∗)print_something+2));
∗(unsigned char∗)print_something=0xC3; // RET's opcode
printf ("going to call patched print_something():\n");
print_something();
printf ("it must exit at this point\n");
};

При запуске видно что первые 3 байта ф-ции это 55 89 e5. Действительно,
это опкоды инструкций PUSH EBP и MOV EBP, ESP (это опкоды x86). Но потом
процесс падает, потому что секция text доступна только для чтения.
Мы можем перекомпилировать наш пример и сделать так, чтобы секция text
была доступна для записи 43 :
gcc −−static −g −Wl,−−omagic −o example example.c

Это работает!
we are in print_something()
first 3 bytes: 55 89 e5...
going to call patched print_something():
it must exit at this point

3.21.7. Указатель на функцию: защита от копирования
Взломщик может найти ф-цию, проверяющую защиту и возвращать true или
false. Он(а) может вписать там XOR EAX,EAX / RETN или MOV EAX, 1 / RETN.
Может ли проверить целостность ф-ции? Оказывается, сделать это легко.
Судя по objdump, первые 3 байта ф-ции check_protection() это 0x55 0x89 0xE5
(учитывая, что это неоптимизирующий GCC):
#include
#include
int check_protection()
{
// do something
return 0;
// or return 1;
43 http://stackoverflow.com/questions/27581279/make-text-segment-writable-elf

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

799
};
int main()
{
if (check_protection()==0)
{
printf ("no protection installed\n");
exit(0);
};
// ...and then, at some very important point...
if (∗(((unsigned char∗)check_protection)+0) != 0x55)
{
printf ("1st byte has been altered\n");
// do something mean, add watermark, etc
};
if (∗(((unsigned char∗)check_protection)+1) != 0x89)
{
printf ("2nd byte has been altered\n");
// do something mean, add watermark, etc
};
if (∗(((unsigned char∗)check_protection)+2) != 0xe5)
{
printf ("3rd byte has been altered\n");
// do something mean, add watermark, etc
};
};
0000054d :
54d:
55
54e:
89 e5
550:
e8 b7 00 00 00
555:
05 7f 1a 00 00
55a:
b8 00 00 00 00
55f:
5d
560:
c3

push
mov
call
add
mov
pop
ret

%ebp
%esp,%ebp
60c
$0x1a7f,%eax
$0x0,%eax
%ebp

Если кто-то пропатчит начало ф-ции check_protection(), ваша программа может совершить что-то подлое, например, внезапно закончить работу. Чтобы
разобраться с таким трюком, взломщик может установить брякпоинт на чтение памяти, по адресу начала ф-ции. (В tracer-е для этого есть опция BPMx.)

3.21.8. Указатель на ф-цию: частая ошибка (или опечатка)
Печально известная ошибка/опечатка:
int expired()
{
// check license key, current date/time, etc
};
int main()
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

800
{
if (expired) // must be expired() here
{
print ("expired\n");
exit(0);
}
else
{
// do something
};
};

Т.к. имя ф-ции само по себе трактуется как указатель на ф-цию, или её адрес,
выражение if(function_name) работает как if(true).
К сожалению, компилятор с Си/Си++не выдает предупреждение об этом.

3.21.9. Указатель как идентификатор объекта
В ассемблере и Си нет возможностей ООП, но там вполне можно писать код в
стиле ООП (просто относитесь к структуре, как к объекту).
Интересно что, иногда, указатель на объект (или его адрес) называется идентификатором (в смысле сокрытия данных/инкапсуляции).
Например, LoadLibrary(), судя по MSDN44 , возвращает “handle” модуля 45 . Затем вы передаете этот “handle” в другую ф-цию вроде GetProcAddress(). Но
на самом деле, LoadLibrary() возвращает указатель на DLL-файл загруженный
(mapped) в памяти 46 . Вы можете прочитать два байта по адресу возвращенному LoadLibrary(), и это будет “MZ” (первые два байта любого файла типа
.EXE/.DLL в Windows).
Очевидно, Microsoft “скрывает” этот факт для обеспечения лучшей совместимости в будущем. Также, типы данных HMODULE и HINSTANCE имели другой
смысл в 16-битной Windows.
Возможно, это причина, почему printf() имеет модификатор “%p”, который
используется для вывода указателей (32-битные целочисленные на 32-битных
архитектурах, 64-битные на 64-битных, итд) в шестнадцатеричной форме. Адрес структуры сохраненный в отладочном протоколе может помочь в поисках
такого же в том же протоколе.
Вот например из исходного кода SQLite:
...
struct Pager {
sqlite3_vfs ∗pVfs;
44 Microsoft

/∗ OS functions to use for IO ∗/

Developer Network

45 https://msdn.microsoft.com/ru-ru/library/windows/desktop/ms684175(v=vs.85).aspx
46 https://blogs.msdn.microsoft.com/oldnewthing/20041025-00/?p=37483

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

801
u8 exclusiveMode;
Ç ∗/
u8 journalMode;
u8 useJournal;
u8 noSync;

/∗ Boolean. True if locking_mode==EXCLUSIVE ⤦
/∗ One of the PAGER_JOURNALMODE_∗ values ∗/
/∗ Use a rollback journal on this file ∗/
/∗ Do not sync the journal if true ∗/

....
static int pagerLockDb(Pager ∗pPager, int eLock){
int rc = SQLITE_OK;
assert( eLock==SHARED_LOCK || eLock==RESERVED_LOCK || eLock==⤦
Ç EXCLUSIVE_LOCK );
if( pPager−>eLockeLock==UNKNOWN_LOCK ){
rc = sqlite3OsLock(pPager−>fd, eLock);
if( rc==SQLITE_OK && (pPager−>eLock!=UNKNOWN_LOCK||eLock==⤦
Ç EXCLUSIVE_LOCK) ){
pPager−>eLock = (u8)eLock;
IOTRACE(("LOCK %p %d\n", pPager, eLock))
}
}
return rc;
}
...
PAGER_INCR(sqlite3_pager_readdb_count);
PAGER_INCR(pPager−>nRead);
IOTRACE(("PGIN %p %d\n", pPager, pgno));
PAGERTRACE(("FETCH %d page %d hash(%08x)\n",
PAGERID(pPager), pgno, pager_pagehash(pPg)));
...

3.22. Оптимизации циклов
3.22.1. Странная оптимизация циклов
Это самая простая (из всех возможных) реализация memcpy():
void memcpy (unsigned char∗ dst, unsigned char∗ src, size_t cnt)
{
size_t i;
for (i=0; ib=2;
strcpy (s−>s, STRING);
f(s);
};

Если коротко, это работает, потому что в Си нет проверок границ массивов. К
любому массиву относятся так, будто он бесконечный.
Проблема: после выделения, полный размер выделенного блока для структуры неизвестен (хотя известен менеджеру памяти), так что вы не можете заменить строку бо́ льшей строкой. Но вы бы смогли делать это, если бы поле было
определено как что-то вроде s[MAX_NAME].
Другими словами, вы имеете структуру плюс массив (или строку) спаянных
вместе в одном выделенном блоке памяти. Другая проблема еще в том, что вы
не можете объявить два таких массива в одной структуре, или объявить еще
одно поле после такого массива.
Более старые компиляторы требуют объявить массив хотя бы с одним элементом: s[1], более новые позволяют определять его как массив с переменной
длиной: s[]. В стандарте C99 это также называется flexible array member.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

809
Читайте об этом больше в документации GCC47 , в документации MSDN48 .
Деннис Ритчи (один из создателей Си) называет этот трюк «unwarranted chumminess
with the C implementation» (вероятно, подтверждая хакерскую природу трюка).
Вам это может нравиться, или нет, вы можете использовать это или нет: но
это еще одна демонстрация того, как структуры располагаются в памяти, вот
почему я написал об этом.

3.23.3. Версия структуры в Си
Многие программисты под Windows видели это в MSDN:
SizeOfStruct
The size of the structure, in bytes. This member must be set to sizeof(⤦
Ç SYMBOL_INFO).

( https://msdn.microsoft.com/en-us/library/windows/desktop/ms680686(v=
vs.85).aspx )
Некоторые структуры вроде SYMBOL_INFO действительно начинаются с такого
поля. Почему? Это что-то вроде версии структуры.
Представьте, что у вас есть ф-ция, рисующая круги. Она берет один аргумент
- указатель на структуру с двумя полями: X, Y и радиус. И затем цветные дисплеи наводнили рынок, где-то в 80-х. И вы хотите добавить аргумент цвет в
ф-цию. Но, скажем так, вы не можете добавить еще один аргумент в нее (множество ПО используют ваше API49 и его нельзя перекомпилировать). И если
какое-то старое ПО использует ваше API с цветным дисплеем, пусть ваша фция рисует круг в цветах по умолчанию (черный и белый).
Позже вы добавляете еще одну возможность: круг может быть закрашен, и
можно выбирать тип заливки.
Вот одно из решений проблемы:
#include
struct ver1
{
size_t SizeOfStruct;
int coord_X;
int coord_Y;
int radius;
};
struct ver2
{
size_t SizeOfStruct;
int coord_X;
int coord_Y;
47 https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html
48 https://msdn.microsoft.com/en-us/library/b6fae073.aspx
49 Application

Programming Interface

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

810
int radius;
int color;
};
struct ver3
{
size_t SizeOfStruct;
int coord_X;
int coord_Y;
int radius;
int color;
int fill_brush_type; // 0 - не заливать круг
};
void draw_circle(struct ver3 ∗s) // здесь используется самая последняя версия
структуры
{
// мы полагаем что в SizeOfStruct всегда присутствуют поля coord_X и
coord_Y
printf ("Собираемся рисовать круг на %d:%d\n", s−>coord_X, s−>⤦
Ç coord_Y);
if (s−>SizeOfStruct>=sizeof(int)∗5)
{
// это минимум ver2, поле цвета присутствует
printf ("Собираемся установить цвет %d\n", s−>color);
}
if (s−>SizeOfStruct>=sizeof(int)∗6)
{
// это минимум ver3, присутствует поле с типом заливки
printf ("Мы собираемся залить его используя тип заливки %d\⤦
Ç n", s−>fill_brush_type);
}
};
// раннее ПО
void call_as_ver1()
{
struct ver1 s;
s.SizeOfStruct=sizeof(s);
s.coord_X=123;
s.coord_Y=456;
s.radius=10;
printf ("∗∗ %s()\n", __FUNCTION__);
draw_circle(&s);
};
// следующая версия
void call_as_ver2()
{
struct ver2 s;
s.SizeOfStruct=sizeof(s);
s.coord_X=123;
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

811
s.coord_Y=456;
s.radius=10;
s.color=1;
printf ("∗∗ %s()\n", __FUNCTION__);
draw_circle(&s);
};
// самая поздняя, наиболее расширенная версия
void call_as_ver3()
{
struct ver3 s;
s.SizeOfStruct=sizeof(s);
s.coord_X=123;
s.coord_Y=456;
s.radius=10;
s.color=1;
s.fill_brush_type=3;
printf ("∗∗ %s()\n", __FUNCTION__);
draw_circle(&s);
};
int main()
{
call_as_ver1();
call_as_ver2();
call_as_ver3();
};

Другими словами, поле SizeOfStruct берет на себя роль поля версия структуры.
Это может быть перечисляемый тип (1, 2, 3, итд.), но установка поля SizeOfStruct
равным sizeof(struct...), это лучше защищено от ошибок: в вызываемом коде мы
просто пишем s.SizeOfStruct=sizeof(...).
В Си++ эта проблема решается наследованием (3.19.1 (стр. 713)). Просто расширяете ваш базовый класс (назовем его Circle), и затем вам нужен ColoredCircle,
а потом FilledColoredCircle, и так далее. Текущая версия объекта (или более
точно, текущий тип) будет определяться при помощи RTTI в Си++.
Так что если вы где-то в MSDN видите SizeOfStruct — вероятно, эта структура
уже расширялась в прошлом, как минимум один раз.

3.23.4. Файл с рекордами в игре «Block out» и примитивная
сериализация
Многие видеоигры имеют файл с рекордами, иногда называемый «Зал славы».
Древняя игра «Block out»50 (трехмерный тетрис из 1989) не исключение, вот
что мы можем увидеть в конце:
50 http://www.bestoldgames.net/eng/old-games/blockout.php

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

812

Рис. 3.4: Таблица рекордов
Мы можем увидеть, что после того как мы всякий раз добаяем свое имя, этот
файл меняется: BLSCORE.DAT.
% xxd −g 1 BLSCORE.DAT
00000000:
00000010:
00000020:
00000030:
00000040:
00000050:
00000060:
00000070:
00000080:
00000090:
000000a0:
000000b0:
000000c0:
000000d0:
000000e0:
000000f0:
00000100:

0a
00
2e
2d
00
4a
33
65
30
00
69
32
2e
38
00
2e
2d

00
30
2e
32
46
61
2d
2e
31
00
6c
37
2e
00
30
2e
32

58
33
2e
30
01
6d
32
2e
38
00
2e
2d
00
54
33
2e
30

65
2d
2e
31
00
65
37
2e
00
30
2e
32
7b
6f
2d
2e
31

6e
32
2e
38
00
73
2d
00
4d
33
2e
30
00
6d
32
2e
38

69
37
2e
00
30
2e
32
ea
69
2d
2e
31
00
2e
37
2e
00

61
2d
00
4a
33
2e
30
00
6b
32
2e
38
00
2e
2d
00

2e
32
61
6f
2d
2e
31
00
65
37
2e
00
30
2e
32
77

2e
30
01
68
32
2e
38
00
2e
2d
00
4d
33
2e
30
00

2e
31
00
6e
37
2e
00
30
2e
32
ac
61
2d
2e
31
00

2e
38
00
2e
2d
00
43
33
2e
30
00
72
32
2e
38
00

2e
00
30
2e
32
44
68
2d
2e
31
00
79
37
2e
00
30

00
50
33
2e
30
01
61
32
2e
38
00
2e
2d
00
42
33

df
61
2d
2e
31
00
72
37
2e
00
30
2e
32
77
6f
2d

01
75
32
2e
38
00
6c
2d
00
50
33
2e
30
00
62
32

00
6c
37
2e
00
30
69
32
b5
68
2d
2e
31
00
2e
37

..Xenia.........
.03−27−2018.Paul
.......a...03−27
−2018.John......
.F...03−27−2018.
James......D...0
3−27−2018.Charli
e........03−27−2
018.Mike........
...03−27−2018.Ph
il...........03−
27−2018.Mary....
...{...03−27−201
8.Tom........w..
.03−27−2018.Bob.
.......w...03−27
−2018.

Все записи и так хорошо видны. Самый первый байт, вероятно, это количество
записей. Второй это 0, и, на самом деле, число записей может быть 16-битным
значением, которое простирается на 2 байта.
После имени «Xenia» мы видим байты 0xDF и 0x01. У Xenia 479 очков, и это
именно 0x1DF в шестнадцатеричной системе. Так что значение рекорда, вероятно, 16-битное целочисленное, а может и 32-битное: после каждого по два
нулевых байта.
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

813
Подумаем теперь о том факте, что и элементы массива, и элементы структуры
всегда располагаются в памяти друг к другу впритык. Это позволяет там
записывать весь массив/структуру в файл используя простую ф-цию write() или
fwrite(), а затем восстанавливать его используя read() или fread(), настолько
всё просто. Это то, что сейчас называется сериализацией.
Чтение
Напишем программу на Си для чтения файла рекордов:
#include
#include
#include
#include

struct entry
{
char name[11]; // включая терминирующий ноль
uint32_t score;
char date[11]; // включая терминирующий ноль
} __attribute__ ((aligned (1),packed));
struct highscore_file
{
uint8_t count;
uint8_t unknown;
struct entry entries[10];
} __attribute__ ((aligned (1), packed));
struct highscore_file file;
int main(int argc, char∗ argv[])
{
FILE∗ f=fopen(argv[1], "rb");
assert (f!=NULL);
size_t got=fread(&file, 1, sizeof(struct highscore_file), f);
assert (got==sizeof(struct highscore_file));
fclose(f);
for (int i=0; i `|h|e|l|l|o|...`

memcpy(), которая копирует 32-битные или 64-битные слова за раз, или даже
SIMD, здесь очевидно не сработают, потому как нужно использовать ф-цию
копирования работающую побайтово.
Теперь даже более сложный пример, вставьте 2 байта впереди строки:
`|h|e|l|l|o|...` −> `|.|.|h|e|l|l|o|...`

Теперь даже ф-ция работающая побайтово не сработает, нужно копировать
байты с конца.
Это тот редкий случай, когда x86 флаг DF нужно выставлять перед инструкцией REP MOVSB: DF определяет направление, и теперь мы должны двигаться
назад.
Обычная процедура memmove() работает примерно так: 1) если источник ниже назначения, копируем вперед; 2) если источник над назначением, копируем назад.
Это memmove() из uClibc:
void ∗memmove(void ∗dest, const
{
int eax, ecx, esi, edi;
__asm__ __volatile__(
"
movl
"
cmpl
"
je

void ∗src, size_t n)

%%eax, %%edi\n"
%%esi, %%eax\n"
2f\n" /∗ (optional) src == dest −> NOP ∗/

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

818
"
jb
1f\n" /∗ src > dest −> simple copy ∗/
"
leal
−1(%%esi,%%ecx), %%esi\n"
"
leal
−1(%%eax,%%ecx), %%edi\n"
"
std\n"
"1:
rep; movsb\n"
"
cld\n"
"2:\n"
: "=&c" (ecx), "=&S" (esi), "=&a" (eax), "=&D" (edi)
: "0" (n), "1" (src), "2" (dest)
: "memory"
);
return (void∗)eax;
}

В первом случае, REP MOVSB вызывается со сброшенным флагом DF. Во втором,
DF в начале выставляется, но потом сбрасывается.
Более сложный алгоритм имеет такую часть:
«если разница между источником и назначением больше чем ширина слова, копируем используя слова нежели байты, и используем побитовое копирование
для копирования невыровненных частей».
Так происходит в неоптимизированной части на Си в Glibc 2.24.
Учитывая всё это, memmove() может работать медленнее, чем memcpy(). Но
некоторые люди, включая Линуса Торвальдса, спорят53 что memcpy() должна быть синонимом memmove(), а последняя ф-ция должна в начале проверять, пересекаются ли буферы или нет, и затем вести себя как memcpy() или
memmove(). Все же, в наше время, проверка на пересекающиеся буферы это
очень дешевая операция.

3.24.1. Анти-отладочный прием
Я слышал об анти-отладочном приеме, где всё что вам нужно для падения
процесса это выставить DF: следующий вызов memcpy() приведет к падению,
потому что будет копировать назад. Но я не могу это проверить: похоже, все
процедуры копирования сбрасывают/выставляют DF, как им надо. С другой стороны, memmove() из uClibc, код которой я цитировал здесь, не имеет явного
сброса DF (он подразумевает, что DF всегда сброшен?), так что он может и
упасть.

3.25. setjmp/longjmp
setjmp/longjmp это механизм в Си, очень похожий на throw/catch в Си++ и других высокоуровневых ЯП. Вот пример из zlib:
...
53 https://bugzilla.redhat.com/show_bug.cgi?id=638477#c132

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

819
/∗ return if bits() or decode() tries to read past available input ∗/
if (setjmp(s.env) != 0)
/∗ if came back here via longjmp(),⤦
Ç ∗/
err = 2;
/∗ then skip decomp(), return ⤦
Ç error ∗/
else
err = decomp(&s); /∗ decompress ∗/
...
/∗ load at least need bits into val ∗/
val = s−>bitbuf;
while (s−>bitcnt < need) {
if (s−>left == 0) {
s−>left = s−>infun(s−>inhow, &(s−>in));
if (s−>left == 0) longjmp(s−>env, 1); /∗ out of input ∗/
...
if (s−>left == 0) {
s−>left = s−>infun(s−>inhow, &(s−>in));
if (s−>left == 0) longjmp(s−>env, 1); /∗ out of input ∗/

( zlib/contrib/blast/blast.c )
Вызов setjmp() сохраняет текущие PC, SP и другие регистры в структуре env,
затем возвращает 0.
В слуае ошибки, longjmp() телепортирует вас в место точно после вызова
setjmp(), как если бы вызов setjmp() вернул ненулевое значение (которое
было передано в longjmp()). Это напоминает нам сисколл fork() в UNIX.
Посмотрим в более дистиллированный пример:
#include
#include
jmp_buf env;
void f2()
{
printf ("%s() begin\n", __FUNCTION__);
// something odd happened here
longjmp (env, 1234);
printf ("%s() end\n", __FUNCTION__);
};
void f1()
{
printf ("%s() begin\n", __FUNCTION__);
f2();
printf ("%s() end\n", __FUNCTION__);
};

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

820
int main()
{
int err=setjmp(env);
if (err==0)
{
f1();
}
else
{
printf ("Error %d\n", err);
};
};

Если запустим, то увидим:
f1() begin
f2() begin
Error 1234

Структура jmp_buf обычно недокументирована, чтобы сохранить прямую совместимость.
Посмотрим, как setjmp() реализован в MSVC 2013 x64:
...
; RCX = address of jmp_buf
mov
mov
mov
mov
mov
mov
mov
mov
mov
lea
mov
mov
mov
stmxcsr
fnstcw
movdqa
movdqa
movdqa
movdqa
movdqa
movdqa
movdqa
movdqa
movdqa
movdqa

[rcx], rax
[rcx+8], rbx
[rcx+18h], rbp
[rcx+20h], rsi
[rcx+28h], rdi
[rcx+30h], r12
[rcx+38h], r13
[rcx+40h], r14
[rcx+48h], r15
r8, [rsp+arg_0]
[rcx+10h], r8
r8, [rsp+0]
; get saved RA from stack
[rcx+50h], r8
; save it
dword ptr [rcx+58h]
word ptr [rcx+5Ch]
xmmword ptr [rcx+60h], xmm6
xmmword ptr [rcx+70h], xmm7
xmmword ptr [rcx+80h], xmm8
xmmword ptr [rcx+90h], xmm9
xmmword ptr [rcx+0A0h], xmm10
xmmword ptr [rcx+0B0h], xmm11
xmmword ptr [rcx+0C0h], xmm12
xmmword ptr [rcx+0D0h], xmm13
xmmword ptr [rcx+0E0h], xmm14
xmmword ptr [rcx+0F0h], xmm15

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

821
retn

Она просто заполняет структуру jmp_buf текущими значениями почти всех регистров. Также, текущее значение RA берется из стека и сохраняется в jmp_buf:
В будущем, оно будет испоьзовано как новое значение PC.
Теперь longjmp():
...
; RCX = address of jmp_buf
mov
mov
mov
mov
mov
mov
mov
mov
ldmxcsr
fnclex
fldcw
movdqa
movdqa
movdqa
movdqa
movdqa
movdqa
movdqa
movdqa
movdqa
movdqa
mov
mov
mov
jmp

rax, rdx
rbx, [rcx+8]
rsi, [rcx+20h]
rdi, [rcx+28h]
r12, [rcx+30h]
r13, [rcx+38h]
r14, [rcx+40h]
r15, [rcx+48h]
dword ptr [rcx+58h]
word ptr [rcx+5Ch]
xmm6, xmmword ptr [rcx+60h]
xmm7, xmmword ptr [rcx+70h]
xmm8, xmmword ptr [rcx+80h]
xmm9, xmmword ptr [rcx+90h]
xmm10, xmmword ptr [rcx+0A0h]
xmm11, xmmword ptr [rcx+0B0h]
xmm12, xmmword ptr [rcx+0C0h]
xmm13, xmmword ptr [rcx+0D0h]
xmm14, xmmword ptr [rcx+0E0h]
xmm15, xmmword ptr [rcx+0F0h]
rdx, [rcx+50h] ; get PC (RIP)
rbp, [rcx+18h]
rsp, [rcx+10h]
rdx
; jump to saved PC

...

Она просто восстанавливает (почти) все регистры, берет из структуры RA и
переходит туда. Эффект такой же, как если бы setjmp() вернула управление в
вызывающую ф-цию. Также, RAX выставляется такой же, как и второй аргумент
longjmp(). Это работает, как если бы setjmp() вернуло ненулевое значение в
самом начале.
Как побочный эффект восстановления SP, все значения в стеке, которые были
установлены и использованы между вызовами setjmp() и longjmp(), просто выкидываются. Они больше не будут использоваться. Следовательно, longjmp()
обычно делает переход назад 54 .
54 Впрочем,

существуют люди, которые используют всё это для куда более сложных вещей, вклю-

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

822
Это подразумевает, что в отличии от механизма throw/catch в Си++, память не
будет освобождаться, деструкторы не будут вызываться, итд. Следовательно,
эта техника иногда опасна. Тем не менее, всё это довольно популярно, до сих
пор. Это все еще используется в Oracle RDBMS.
Это также имеет неожиданный побочный эффект: если некий буфер был перезаписан внутри ф-ции (может даже из-за удаленной атаки), и ф-ция хочет
сообщить об ошибке, и вызывает longjmp(), перезаписанная часть стека становится просто неиспользованной.
В качестве упражнения, попробуйте понять, почему не все регистры сохраняются. Почему пропускаются регистры XMM0-XMM5 и другие?

3.26. Другие нездоровые хаки связанные со стеком
3.26.1. Доступ к аргументам и локальным переменным вызывающей ф-ции
Из основ Си/Си++мы знаем, что иметь доступ к аргументам ф-ции или её локальным переменным — невозможно.
Тем не менее, при помощи грязных хаков это возможно. Например:
#include
void f(char ∗text)
{
// распечатать стек
int ∗tmp=&text;
for (int i=0; itm_mon, t−>tm_mday,
t−>tm_hour, t−>tm_min, t−>tm_sec);
MessageBox (NULL, strbuf, "caption", MB_OK);
return 0;
};
WinMain

proc near

var_4
var_2

= word ptr −4
= word ptr −2

push
bp
mov
bp, sp
push
ax
push
ax
xor
ax, ax
call
time_
mov
[bp+var_4], ax
; low part of UNIX time
mov
[bp+var_2], dx
; high part of UNIX time
lea
ax, [bp+var_4]
; take a pointer of high part
call
localtime_
mov
bx, ax
; t
push
word ptr [bx]
; second
push
word ptr [bx+2] ; minute
push
word ptr [bx+4] ; hour
push
word ptr [bx+6] ; day
push
word ptr [bx+8] ; month
mov
ax, [bx+0Ah]
; year
add
ax, 1900
push
ax
mov
ax, offset a04d02d02d02d02 ;
"%04d-%02d-%02d %02d:%02d:%02d"
push
ax
mov
ax, offset strbuf
push
ax
call
sprintf_
add
sp, 10h
xor
ax, ax
; NULL
push
ax
push
ds
mov
ax, offset strbuf
push
ax
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

854

WinMain

push
mov
push
xor
push
call
xor
mov
pop
retn
endp

ds
ax, offset aCaption ; "caption"
ax
ax, ax
; MB_OK
ax
MESSAGEBOX
ax, ax
sp, bp
bp
0Ah

Время в формате UNIX это 32-битное значение, так что оно возвращается в паре регистров DX:AX и сохраняется в двух локальных 16-битных переменных.
Потом указатель на эту пару передается в функцию localtime(). Функция
localtime() имеет структуру struct tm расположенную у себя где-то внутри,
так что только указатель на нее возвращается. Кстати, это также означает,
что функцию нельзя вызывать еще раз, пока её результаты не были использованы.
Для функций time() и localtime() используется Watcom-соглашение о вызовах: первые четыре аргумента передаются через регистры AX, DX, BX и CX,
а остальные аргументы через стек. Функции, использующие это соглашение,
маркируется символом подчеркивания в конце имени.
Для вызова функции sprintf() используется обычное соглашение cdecl (6.1.1
(стр. 956)) вместо PASCAL или Watcom, так что аргументы передаются привычным образом.
Глобальные переменные
Это тот же пример, только переменные теперь глобальные:
#include
#include
#include
char strbuf[256];
struct tm ∗t;
time_t unix_time;
int PASCAL WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow )
{
unix_time=time(NULL);
t=localtime (&unix_time);
sprintf (strbuf, "%04d−%02d−%02d %02d:%02d:%02d", t−>tm_year+1900, ⤦
Ç t−>tm_mon, t−>tm_mday,
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

855
t−>tm_hour, t−>tm_min, t−>tm_sec);
MessageBox (NULL, strbuf, "caption", MB_OK);
return 0;
};
unix_time_low
unix_time_high
t

dw 0
dw 0
dw 0

WinMain

proc near
push
bp
mov
bp, sp
xor
ax, ax
call
time_
mov
unix_time_low, ax
mov
unix_time_high, dx
mov
ax, offset unix_time_low
call
localtime_
mov
bx, ax
mov
t, ax
; will not be used in future...
push
word ptr [bx]
; seconds
push
word ptr [bx+2]
; minutes
push
word ptr [bx+4]
; hour
push
word ptr [bx+6]
; day
push
word ptr [bx+8]
; month
mov
ax, [bx+0Ah]
; year
add
ax, 1900
push
ax
mov
ax, offset a04d02d02d02d02 ;
"%04d-%02d-%02d %02d:%02d:%02d"
push
ax
mov
ax, offset strbuf
push
ax
call
sprintf_
add
sp, 10h
xor
ax, ax
; NULL
push
ax
push
ds
mov
ax, offset strbuf
push
ax
push
ds
mov
ax, offset aCaption ; "caption"
push
ax
xor
ax, ax
; MB_OK
push
ax
call
MESSAGEBOX
xor
ax, ax
; return 0
pop
bp
retn
0Ah
WinMain
endp

t не будет использоваться, но компилятор создал код, записывающий в эту
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

856
переменную.
Потому что он не уверен, может быть это значение будет прочитано где-то в
другом модуле.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

Глава 4

Java
4.1. Java
4.1.1. Введение
Есть немало известных декомпиляторов для Java (или для JVM-байткода вообще) 1 .
Причина в том что декомпиляция JVM-байткода проще чем низкоуровневого
x86-кода:
• Здесь намного больше информации о типах.
• Модель памяти в JVM более строгая и очерченная.
• Java-компилятор не делает никаких оптимизаций (это делает JVM JIT2 во
время исполнения), так что байткод в class-файлах легко читаем.
Когда знания JVM-байткода могут быть полезны?
• Мелкая/несложная работа по патчингу class-файлов без необходимости
снова компилировать результаты декомпилятора.
• Анализ обфусцированного кода.
• Анализ кода сгенерированного более новым Java-компилятором, для которого еще пока нет обновленного декомпилятора.
• Создание вашего собственного обфускатора.
• Создание кодегенератора компилятора (back-end), создающего код для
JVM (как Scala, Clojure, итд 3 ).
Начнем с простых фрагментов кода.
Если не указано иное, везде используется JDK 1.7.
1 Например,

JAD: http://varaneckas.com/jad/
compilation
3 Полный список: http://en.wikipedia.org/wiki/List_of_JVM_languages
2 Just-In-Time

857

858
Эта команда использовалась везде для декомпиляции class-файлов:
javap -c -verbose.
Эта книга использовалась мною для подготовки всех примеров: [Tim Lindholm,
Frank Yellin, Gilad Bracha, Alex Buckley, The Java(R) Virtual Machine Specification /
Java SE 7 Edition] 4 .

4.1.2. Возврат значения
Наверное, самая простая из всех возможных функций на Java это та, что возвращает некоторое значение.
О, и мы не должны забывать, что в Java нет «свободных» функций в общем
смысле, это «методы».
Каждый метод принадлежит какому-то классу, так что невозможно объявить
метод вне какого-либо класса.
Но мы все равно будем называть их «функциями», для простоты.
public class ret
{
public static int main(String[] args)
{
return 0;
}
}

Компилируем это:
javac ret.java

…и декомпилирую используя стандартную утилиту в Java:
javap −c −verbose ret.class

И получаем:
Листинг 4.1: JDK 1.7 (excerpt)
public static int main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iconst_0
1: ireturn

Разработчики Java решили, что 0 это самая используемая константа в программировании, так что здесь есть отдельная однобайтная инструкция iconst_0,
заталкивающая 0 в стек 5 .
4 Также доступно здесь: https://docs.oracle.com/javase/specs/jvms/se7/jvms7.pdf; http://
docs.oracle.com/javase/specs/jvms/se7/html/
5 Так же как и в MIPS, где для нулевой константы имеется отдельный регистр: 1.5.4 (стр. 35).

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

859
Здесь есть также iconst_1 (заталкивающая 1), iconst_2, итд, вплоть до iconst_5.
Есть также iconst_m1 заталкивающая -1.
Стек также используется в JVM для передачи данных в вызывающие ф-ции, и
также для возврата значений. Так что iconst_0 заталкивает 0 в стек. ireturn
возвращает целочисленное значение (i в названии означает integer) из TOS6 .
Немного перепишем наш пример, теперь возвращаем 1234:
public class ret
{
public static int main(String[] args)
{
return 1234;
}
}

…получаем:
Листинг 4.2: JDK 1.7 (excerpt)
public static int main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: sipush
1234
3: ireturn

sipush (short integer) заталкивает значение 1234 в стек. short в имени означает,
что 16-битное значение будет заталкиваться в стек.
Число 1234 действительно помещается в 16-битное значение.
Как насчет бо́ льших значений?
public class ret
{
public static int main(String[] args)
{
return 12345678;
}
}

Листинг 4.3: Constant pool
...
#2 = Integer

12345678

...
public static int main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
6 Top

of Stack (вершина стека)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

860
0: ldc
2: ireturn

#2

// int 12345678

Невозможно закодировать 32-битное число в опкоде какой-либо JVM-инструкции,
разработчики не оставили такой возможности.
Так что 32-битное число 12345678 сохранено в так называемом «constant pool»
(пул констант), который, так скажем, является библиотекой наиболее используемых констант (включая строки, объекты, итд).
Этот способ передачи констант не уникален для JVM.
MIPS, ARM и прочие RISC-процессоры не могут кодировать 32-битные числа в
32-битных опкодах, так что код для RISC-процессоров (включая MIPS и ARM)
должен конструировать значения в несколько шагов, или держать их в сегменте данных: 1.39.3 (стр. 567), 1.40.1 (стр. 571).
Код для MIPS также традиционно имеет пул констант, называемый «literal pool»,
это сегменты с названиями «.lit4» (для хранения 32-битных чисел с плавающей
точкой одинарной точности) и «.lit8»(для хранения 64-битных чисел с плавающей точкой двойной точности).
Попробуем некоторые другие типы данных!
Boolean:
public class ret
{
public static boolean main(String[] args)
{
return true;
}
}
public static boolean main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iconst_1
1: ireturn

Этот JVM-байткод не отличается от того, что возвращает целочисленную 1.
32-битные слоты данных в стеке также используются для булевых значений,
как в Си/Си++.
Но нельзя использовать возвращаемое значение булевого типа как целочисленное и наоборот — информация о типах сохраняется в class-файлах и проверяется при запуске.
Та же история с 16-битным short:
public class ret
{
public static short main(String[] args)
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

861
{
return 1234;
}
}
public static short main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: sipush
1234
3: ireturn

…и char!
public class ret
{
public static char main(String[] args)
{
return 'A';
}
}
public static char main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: bipush
65
2: ireturn

bipush означает «push byte».
Нужно сказать, что char в Java, это 16-битный символ в кодировке UTF-16, и он
эквивалентен short, но ASCII-код символа «A» это 65, и можно воспользоваться
инструкцией для передачи байта в стек.
Попробуем также byte:
public class retc
{
public static byte main(String[] args)
{
return 123;
}
}
public static byte main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: bipush
123
2: ireturn

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

862
Кто-то может спросить, зачем заморачиваться использованием 16-битного типа short, который внутри все равно 32-битный integer?
Зачем использовать тип данных char, если это то же самое что и тип short?
Ответ прост: для контроля типов данных и читабельности исходников.
char может быть эквивалентом short, но мы быстро понимаем, что это ячейка
для символа в кодировке UTF-16, а не для какого-то другого целочисленного
значения.
Когда используем short, мы можем показать всем, что диапазон этой переменной ограничен 16-ю битами.
Очень хорошая идея использовать тип boolean где нужно, вместо int для тех
же целей, как это было в Си.
В Java есть также 64-битный целочисленный тип:
public class ret3
{
public static long main(String[] args)
{
return 1234567890123456789L;
}
}

Листинг 4.4: Constant pool
...
#2 = Long

1234567890123456789l

...
public static long main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc2_w
#2
// long 1234567890123456789l
3: lreturn

64-битное число также хранится в пуле констант, ldc2_w загружает его и lreturn
(long return) возвращает его.
Инструкция ldc2_w также используется для загрузки чисел с плавающей точкой двойной точности (которые также занимают 64 бита) из пула констант:
public class ret
{
public static double main(String[] args)
{
return 123.456d;
}
}

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

863
Листинг 4.5: Constant pool
...
#2 = Double

123.456d

...
public static double main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc2_w
#2
// double 123.456d
3: dreturn

dreturn означает «return double».
И наконец, числа с плавающей точкой одинарной точности:
public class ret
{
public static float main(String[] args)
{
return 123.456f;
}
}

Листинг 4.6: Constant pool
...
#2 = Float

123.456f

...
public static float main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: ldc
#2
// float 123.456f
2: freturn

Используемая здесь инструкция ldc та же, что и для загрузки 32-битных целочисленных чисел из пула констант.
freturn означает «return float».
А что насчет тех случаев, когда функция ничего не возвращает?
public class ret
{
public static void main(String[] args)
{
return;
}
}

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

864
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return

Это означает, что инструкция return используется для возврата управления
без возврата какого-либо значения.
Зная все это, по последней инструкции очень легко определить тип возвращаемого значения функции (или метода).

4.1.3. Простая вычисляющая функция
Продолжим с простой вычисляющей функцией.
public class calc
{
public static int half(int a)
{
return a/2;
}
}

Это тот случай, когда используется инструкция iconst_2:
public static int half(int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iload_0
1: iconst_2
2: idiv
3: ireturn

iload_0 Берет нулевой аргумент функции и заталкивает его в стек. iconst_2
заталкивает в стек 2.
Вот как выглядит стек после исполнения этих двух инструкций:
+−−−+
TOS −>| 2 |
+−−−+
| a |
+−−−+

idiv просто берет два значения на вершине стека (TOS), делит одно на другое
и оставляет результат на вершине (TOS):
+−−−−−−−−+
TOS −>| result |
+−−−−−−−−+
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

865
ireturn берет его и возвращает.
Продолжим с числами с плавающей запятой, двойной точности:
public class calc
{
public static double half_double(double a)
{
return a/2.0;
}
}

Листинг 4.7: Constant pool
...
#2 = Double

2.0d

...
public static double half_double(double);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: dload_0
1: ldc2_w
#2
4: ddiv
5: dreturn

// double 2.0d

Почти то же самое, но инструкция ldc2_w используется для загрузки константы 2.0 из пула констант.
Также, все три инструкции имеют префикс d, что означает, что они работают
с переменными типа double.
Теперь перейдем к функции с двумя аргументами:
public class calc
{
public static int sum(int a, int b)
{
return a+b;
}
}
public static int sum(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn

iload_0 загружает первый аргумент функции (a), iload_1 — второй (b).
Вот так выглядит стек после исполнения обоих инструкций:
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

866
+−−−+
TOS −>| b |
+−−−+
| a |
+−−−+

iadd складывает два значения и оставляет результат на TOS:
+−−−−−−−−+
TOS −>| result |
+−−−−−−−−+

Расширим этот пример до типа данных long:
public static long lsum(long a, long b)
{
return a+b;
}

…получим:
public static long lsum(long, long);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=4, args_size=2
0: lload_0
1: lload_2
2: ladd
3: lreturn

Вторая инструкция lload берет второй аргумент из второго слота.
Это потому что 64-битное значение long занимает ровно два 32-битных слота.
Немного более сложный пример:
public class calc
{
public static int mult_add(int a, int b, int c)
{
return a∗b+c;
}
}
public static int mult_add(int, int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=3
0: iload_0
1: iload_1
2: imul
3: iload_2
4: iadd
5: ireturn
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

867
Первый шаг это умножение. Произведение остается на TOS:
+−−−−−−−−−+
TOS −>| product |
+−−−−−−−−−+

iload_2 загружает третий аргумент (c) в стек:
+−−−−−−−−−+
TOS −>|
c
|
+−−−−−−−−−+
| product |
+−−−−−−−−−+

Теперь инструкция iadd может сложить два значения.

4.1.4. Модель памяти в JVM
x86 и другие низкоуровневые среды используют стек для передачи аргументов и как хранилище локальных переменных. JVM устроена немного иначе.
Тут есть:
• Массив локальных переменных (LVA7 ).
Используется как хранилище для аргументов функций и локальных переменных.
Инструкции вроде iload_0 загружают значения оттуда. istore записывает значения туда.
В начале идут аргументы функции: начиная с 0, или с 1 (если нулевой
аргумент занят указателем this.
Затем располагаются локальные переменные.
Каждый слот имеет размер 32 бита.
Следовательно, значения типов long и double занимают два слота.
• Стек операндов (или просто «стек»).
Используется для вычислений и для передачи аргументов во время вызова других функций.
В отличие от низкоуровневых сред вроде x86, здесь невозможно работать
со стеком без использования инструкций, которые явно заталкивают или
выталкивают значения туда/оттуда.
• Куча (heap). Используется как хранилище для объектов и массивов.
Эти 3 области изолированы друг от друга.
7 (Java)

Local Variable Array (массив локальных переменных)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

868

4.1.5. Простой вызов функций
Math.random() возвращает псевдослучайное число в пределах [0.0 …1.0), но
представим, по какой-то причине, нам нужна функция, возвращающая число
в пределах [0.0 …0.5):
public class HalfRandom
{
public static double f()
{
return Math.random()/2;
}
}

Листинг 4.8: Constant pool
...
#2 = Methodref
#3 = Double
...
#12
...
#18
#19
#22
#23

#18.#19
2.0d

//

java/lang/Math.random:()D

= Utf8

()D

=
=
=
=

#22
// java/lang/Math
#23:#12
// random:()D
java/lang/Math
random

Class
NameAndType
Utf8
Utf8

public static double f();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=0, args_size=0
0: invokestatic #2
3: ldc2_w
#3
6: ddiv
7: dreturn

// Method java/lang/Math.random:()D
// double 2.0d

invokestatic вызывает функцию Math.random() и оставляет результат на TOS.
Затем результат делится на 2.0 и возвращается.
Но как закодировано имя функции?
Оно закодировано в пуле констант используя выражение Methodref.
Оно определяет имена класса и метода.
Первое поле Methodref указывает на выражение Class, которое, в свою очередь, указывает на обычную текстовую строку («java/lang/Math»).
Второе выражение Methodref указывает на выражение NameAndType, которое
также имеет две ссылки на строки.
Первая строка это «random», это имя метода.
Вторая строка это «()D», которая кодирует тип функции. Это означает, что
возвращаемый тип — double (отсюда D в строке).
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

869
Благодаря этому 1) JVM проверяет корректность типов данных; 2) Java-декомпиляторы
могут восстанавливать типы данных из class-файлов.
Наконец попробуем пример «Hello, world!»:
public class HelloWorld
{
public static void main(String[] args)
{
System.out.println("Hello, World");
}
}

Листинг 4.9: Constant pool
...
#2
Ç
#3
#4
Ç
...
#16
#17
#18
#19
#20
...
#23
#24
#25
#26
#27
#28
...

= Fieldref
#16.#17
/PrintStream;
= String
#18
= Methodref
#19.#20
Ljava/lang/String;)V

//

java/lang/System.out:Ljava/io⤦

//
//

Hello, World
java/io/PrintStream.println:(⤦

=
=
=
=
=

Class
NameAndType
Utf8
Class
NameAndType

#23
#24:#25
Hello, World
#26
#27:#28

//
//

java/lang/System
out:Ljava/io/PrintStream;

//
//

java/io/PrintStream
println:(Ljava/lang/String;)V

=
=
=
=
=
=

Utf8
Utf8
Utf8
Utf8
Utf8
Utf8

java/lang/System
out
Ljava/io/PrintStream;
java/io/PrintStream
println
(Ljava/lang/String;)V

public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic
#2
// Field java/lang/System.out:Ljava/io/⤦
Ç PrintStream;
3: ldc
#3
// String Hello, World
5: invokevirtual #4
// Method java/io/PrintStream.println:(⤦
Ç Ljava/lang/String;)V
8: return

ldc по смещению 3 берет указатель (или адрес) на строку «Hello, World» в пуле
констант и заталкивает его в стек.
В мире Java это называется reference, но это скорее указатель или просто адрес
8
.


разнице между указателями и reference в С++: 3.19.3 (стр. 728).

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

870
Уже знакомая нам инструкция invokevirtual берет информацию о функции
(или методе) println из пула констант и вызывает её.
Как мы можем знать, есть несколько методов println, каждый предназначен
для каждого типа данных.
В нашем случае, используется та версия println, которая для типа данных
String.
Что насчет первой инструкции getstatic?
Эта инструкция берет reference (или адрес) поля объекта System.out и заталкивает его в стек.
Это значение работает как указатель this для метода println.
Таким образом, внутри, метод println берет на вход два аргумента: 1) this, т.е.
указатель на объект 9 ; 2) адрес строки «Hello, World».
Действительно, println() вызывается как метод в рамках инициализированного объекта System.out.
Для удобства, утилита javap пишет всю эту информацию в комментариях.

4.1.6. Вызов beep()
Вот простейший вызов двух функций без аргументов:
public static void main(String[] args)
{
java.awt.Toolkit.getDefaultToolkit().beep();
};
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: invokestatic #2
// Method java/awt/Toolkit.⤦
Ç getDefaultToolkit:()Ljava/awt/Toolkit;
3: invokevirtual #3
// Method java/awt/Toolkit.beep:()V
6: return

Первая invokestatic по смещению 0 вызывает
java.awt.Toolkit.getDefaultToolkit(), которая возвращает
reference (указатель) на объект класса Toolkit.
Инструкция invokevirtual по смещению 3 вызывает метод beep() этого класса.
9 Или

«экземпляр класса» в некоторой русскоязычной литературе.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

871

4.1.7. Линейный конгруэнтный ГПСЧ
Попробуем простой генератор псевдослучайных чисел, который мы однажды
уже рассматривали в этой книге (1.29 (стр. 432)):
public class LCG
{
public static int rand_state;
public void my_srand (int init)
{
rand_state=init;
}
public static int RNG_a=1664525;
public static int RNG_c=1013904223;
public int my_rand ()
{
rand_state=rand_state∗RNG_a;
rand_state=rand_state+RNG_c;
return rand_state & 0x7fff;
}
}

Здесь пара полей класса, которые инициализируются в начале. Но как?
В выводе javap мы можем найти конструктор класса:
static {};
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc
#5
// int 1664525
2: putstatic
#3
// Field RNG_a:I
5: ldc
#6
// int 1013904223
7: putstatic
#4
// Field RNG_c:I
10: return

Так инициализируются переменные.
RNG_a занимает третий слот в классе и RNG_c — четвертый, и putstatic записывает туда константы.
Функция my_srand() просто записывает входное значение в
rand_state:
public void my_srand(int);
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: iload_1
1: putstatic
#2
// Field rand_state:I
4: return

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

872
iload_1 берет входное значение и заталкивает его в стек. Но почему не iload_0?
Это потому что эта функция может использовать поля класса, а переменная
this также передается в эту функцию как нулевой аргумент.
Поле rand_state занимает второй слот в классе, так что putstatic копирует
переменную из TOS во второй слот.
Теперь my_rand():
public int my_rand();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic
#2
// Field rand_state:I
3: getstatic
#3
// Field RNG_a:I
6: imul
7: putstatic
#2
// Field rand_state:I
10: getstatic
#2
// Field rand_state:I
13: getstatic
#4
// Field RNG_c:I
16: iadd
17: putstatic
#2
// Field rand_state:I
20: getstatic
#2
// Field rand_state:I
23: sipush
32767
26: iand
27: ireturn

Она просто загружает все переменные из полей объекта, производит с ними
операции и обновляет значение rand_state, используя инструкцию putstatic.
По смещению 20, значение rand_state перезагружается снова (это потому что
оно было выброшено из стека перед этим, инструкцией putstatic).
Это выглядит как неэффективный код, но можете быть уверенными, JVM обычно достаточно хорош, чтобы хорошо оптимизировать подобные вещи.

4.1.8. Условные переходы
Перейдем к условным переходам.
public class abs
{
public static int abs(int a)
{
if (ab)
return b;
return a;
}

Получаем:
public static int min(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: if_icmple
7
5: iload_1
6: ireturn
7: iload_0
8: ireturn

if_icmple выталкивает два значения и сравнивает их.
Если второе меньше первого (или равно), происходит переход на смещение 7.
Когда мы определяем функцию max() …
public static int max (int a, int b)
{
if (a>b)
return a;
return b;
}

…итоговый код точно такой же, только последние инструкции iload (на смещениях 5 и 7) поменяны местами:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

874
public static int max(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: if_icmple
7
5: iload_0
6: ireturn
7: iload_1
8: ireturn

Более сложный пример:
public class cond
{
public static void f(int i)
{
if (i100)
System.out.print(">100");
if (i==0)
System.out.print("==0");
}
}
public static void f(int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iload_0
1: bipush
100
3: if_icmpge
14
6: getstatic
#2
//
Ç PrintStream;
9: ldc
#3
//
11: invokevirtual #4
//
Ç Ljava/lang/String;)V
14: iload_0
15: bipush
100
17: if_icmpne
28
20: getstatic
#2
//
Ç PrintStream;
23: ldc
#5
//
25: invokevirtual #4
//
Ç Ljava/lang/String;)V
28: iload_0
29: bipush
100
31: if_icmple
42

Field java/lang/System.out:Ljava/io/⤦
String 100
// Method java/io/PrintStream.print:(⤦

// Field java/lang/System.out:Ljava/io/⤦
// String ==0
// Method java/io/PrintStream.print:(⤦

if_icmpge Выталкивает два значения и сравнивает их.
Если второй больше первого, или равен первому, происходит переход на смещение 14.
if_icmpne и if_icmple работают одинаково, но используются разные условия.
По смещению 43 есть также инструкция ifne.
Название неудачное, её было бы лучше назвать ifnz (переход если переменная на TOS не равна нулю).
И вот что она делает: производит переход на смещение 54, если входное значение не ноль.
Если ноль, управление передается на смещение 46, где выводится строка «==0».
N.B.: В JVM нет беззнаковых типов данных, так что инструкции сравнения работают только со знаковыми целочисленными значениями.

4.1.9. Передача аргументов
Теперь расширим пример min()/max():
public class minmax
{
public static int min (int a, int b)
{
if (a>b)
return b;
return a;
}
public static int max (int a, int b)
{
if (a>b)
return a;
return b;
}
public static void main(String[] args)
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

876
{
int a=123, b=456;
int max_value=max(a, b);
int min_value=min(a, b);
System.out.println(min_value);
System.out.println(max_value);
}
}

Вот код функции main():
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: bipush
123
2: istore_1
3: sipush
456
6: istore_2
7: iload_1
8: iload_2
9: invokestatic #2
// Method max:(II)I
12: istore_3
13: iload_1
14: iload_2
15: invokestatic #3
// Method min:(II)I
18: istore
4
20: getstatic
#4
// Field java/lang/System.out:Ljava/io/⤦
Ç PrintStream;
23: iload
4
25: invokevirtual #5
// Method java/io/PrintStream.println:(I⤦
Ç )V
28: getstatic
#4
// Field java/lang/System.out:Ljava/io/⤦
Ç PrintStream;
31: iload_3
32: invokevirtual #5
// Method java/io/PrintStream.println:(I⤦
Ç )V
35: return

В другую функцию аргументы передаются в стеке, а возвращаемое значение
остается на TOS.

4.1.10. Битовые поля
Все побитовые операции работают также, как и в любой другой ISA:
public static int set (int a, int b)
{
return a | 1ExceptionAddress);
42 Также

доступно здесь: http://www.microsoft.com/msj/0197/Exception/Exception.aspx

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1001
if (ExceptionRecord−>ExceptionCode==0xE1223344)
{
printf ("That's for us\n");
// yes, we "handled" the exception
return ExceptionContinueExecution;
}
else if (ExceptionRecord−>ExceptionCode==EXCEPTION_ACCESS_VIOLATION⤦
Ç )
{
printf ("ContextRecord−>Eax=0x%08X\n", ContextRecord−>Eax);
// will it be possible to 'fix' it?
printf ("Trying to fix wrong pointer address\n");
ContextRecord−>Eax=(DWORD)&new_value;
// yes, we "handled" the exception
return ExceptionContinueExecution;
}
else
{
printf ("We do not handle this\n");
// someone else's problem
return ExceptionContinueSearch;
};
}
int main()
{
DWORD handler = (DWORD)except_handler; // take a pointer to our
handler
// install exception handler
__asm
{
record:
push
handler
push
FS:[0]
mov
FS:[0],ESP
}

// make EXCEPTION_REGISTRATION
// address of handler function
// address of previous handler
// add new EXECEPTION_REGISTRATION

RaiseException (0xE1223344, 0, 0, NULL);
// now do something very bad
int∗ ptr=NULL;
int val=0;
val=∗ptr;
printf ("val=%d\n", val);
// deinstall exception handler
__asm
{
record
mov
eax,[ESP]
mov
FS:[0], EAX
add
esp, 8
off stack
}

// remove our EXECEPTION_REGISTRATION
// get pointer to previous record
// install previous record
// clean our EXECEPTION_REGISTRATION

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1002
return 0;
}

Сегментный регистр FS: в win32 указывает на TIB. Самый первый элемент TIB
это указатель на последний обработчик в цепочке. Мы сохраняем его в стеке и записываем туда адрес своего обработчика. Эта структура называется
_EXCEPTION_REGISTRATION, это простейший односвязный список, и эти элементы хранятся прямо в стеке.
Листинг 6.22: MSVC/VC/crt/src/exsup.inc
_EXCEPTION_REGISTRATION struc
prev
dd
?
handler dd
?
_EXCEPTION_REGISTRATION ends

Так что каждое поле «handler» указывает на обработчик, а каждое поле «prev»
указывает на предыдущую структуру в цепочке обработчиков. Самая последняя структура имеет 0xFFFFFFFF (-1) в поле «prev».
Стек

TIB



+0: __except_list

FS:0

+4: …

Prev=0xFFFFFFFF

+8: …

Handle

функцияобработчик


Prev
функцияобработчик

Handle

Prev

функцияобработчик

Handle


После инсталляции своего обработчика, вызываем RaiseException()43 . Это пользовательские исключения. Обработчик проверяет код.
Если код 0xE1223344, то он возвращает ExceptionContinueExecution, что сигнализирует системе что обработчик скорректировал состояние CPU (обычно
это регистры EIP/ESP) и что ОС может возобновить исполнение треда. Если вы
немного измените код так что обработчик будет возвращать ExceptionContinueSearch,
то ОС будет вызывать остальные обработчики в цепочке, и вряд ли найдется
43 MSDN

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1003
тот, кто обработает ваше исключение, ведь информации о нем (вернее, его коде) ни у кого нет. Вы увидите стандартное окно Windows о падении процесса.
Какова разница между системными исключениями и пользовательскими? Вот
системные:
как определен в WinBase.h
EXCEPTION_ACCESS_VIOLATION
EXCEPTION_DATATYPE_MISALIGNMENT
EXCEPTION_BREAKPOINT
EXCEPTION_SINGLE_STEP
EXCEPTION_ARRAY_BOUNDS_EXCEEDED
EXCEPTION_FLT_DENORMAL_OPERAND
EXCEPTION_FLT_DIVIDE_BY_ZERO
EXCEPTION_FLT_INEXACT_RESULT
EXCEPTION_FLT_INVALID_OPERATION
EXCEPTION_FLT_OVERFLOW
EXCEPTION_FLT_STACK_CHECK
EXCEPTION_FLT_UNDERFLOW
EXCEPTION_INT_DIVIDE_BY_ZERO
EXCEPTION_INT_OVERFLOW
EXCEPTION_PRIV_INSTRUCTION
EXCEPTION_IN_PAGE_ERROR
EXCEPTION_ILLEGAL_INSTRUCTION
EXCEPTION_NONCONTINUABLE_EXCEPTION
EXCEPTION_STACK_OVERFLOW
EXCEPTION_INVALID_DISPOSITION
EXCEPTION_GUARD_PAGE
EXCEPTION_INVALID_HANDLE
EXCEPTION_POSSIBLE_DEADLOCK
CONTROL_C_EXIT

как определен в ntstatus.h
STATUS_ACCESS_VIOLATION
STATUS_DATATYPE_MISALIGNMENT
STATUS_BREAKPOINT
STATUS_SINGLE_STEP
STATUS_ARRAY_BOUNDS_EXCEEDED
STATUS_FLOAT_DENORMAL_OPERAND
STATUS_FLOAT_DIVIDE_BY_ZERO
STATUS_FLOAT_INEXACT_RESULT
STATUS_FLOAT_INVALID_OPERATION
STATUS_FLOAT_OVERFLOW
STATUS_FLOAT_STACK_CHECK
STATUS_FLOAT_UNDERFLOW
STATUS_INTEGER_DIVIDE_BY_ZERO
STATUS_INTEGER_OVERFLOW
STATUS_PRIVILEGED_INSTRUCTION
STATUS_IN_PAGE_ERROR
STATUS_ILLEGAL_INSTRUCTION
STATUS_NONCONTINUABLE_EXCEPTION
STATUS_STACK_OVERFLOW
STATUS_INVALID_DISPOSITION
STATUS_GUARD_PAGE_VIOLATION
STATUS_INVALID_HANDLE
STATUS_POSSIBLE_DEADLOCK
STATUS_CONTROL_C_EXIT

как число
0xC0000005
0x80000002
0x80000003
0x80000004
0xC000008C
0xC000008D
0xC000008E
0xC000008F
0xC0000090
0xC0000091
0xC0000092
0xC0000093
0xC0000094
0xC0000095
0xC0000096
0xC0000006
0xC000001D
0xC0000025
0xC00000FD
0xC0000026
0x80000001
0xC0000008
0xC0000194
0xC000013A

Так определяется код:
31

29

S

28

U 0

16

27

15

Facility code

0

Error code

S это код статуса: 11 — ошибка; 10 — предупреждение; 01 — информация; 00
— успех. U —- является ли этот код пользовательским, а не системным.
Вот почему мы выбрали 0xE1223344 — E16 (11102 ) 0xE (1110b) означает, что
это 1) пользовательское исключение; 2) ошибка. Хотя, если быть честным, этот
пример нормально работает и без этих старших бит.
Далее мы пытаемся прочитать значение из памяти по адресу 0. Конечно, в
win32 по этому адресу обычно ничего нет, и сработает исключение. Однако,
первый обработчик, который будет заниматься этим делом — ваш, и он узнает
об этом первым, проверяя код на соответствие с константной EXCEPTION_ACCESS_VIOLATION.
А если заглянуть в то что получилось на ассемблере, то можно увидеть, что
код читающий из памяти по адресу 0, выглядит так:
Листинг 6.23: MSVC 2010

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1004
...
xor
mov
push
push
call
add
...

eax, eax
eax, DWORD PTR [eax] ; exception will occur here
eax
OFFSET msg
_printf
esp, 8

Возможно ли «на лету» исправить ошибку и предложить программе исполняться далее? Да, наш обработчик может изменить значение в EAX и предложить
ОС исполнить эту же инструкцию еще раз. Что мы и делаем. printf() напечатает 1234, потому что после работы нашего обработчика, EAX будет не 0, а
будет содержать адрес глобальной переменной new_value. Программа будет
исполняться далее.
Собственно, вот что происходит: срабатывает защита менеджера памяти в
CPU, он останавливает работу треда, отыскивает в ядре Windows обработчик
исключений, тот, в свою очередь, начинает вызывать обработчики из цепочки
SEH, по одному.
Мы компилируем это всё в MSVC 2010, но конечно же, нет никакой гарантии
что для указателя будет использован именно регистр EAX.
Этот трюк с подменой адреса эффектно выглядит, и мы рассматриваем его
здесь для наглядной иллюстрации работы SEH.
Тем не менее, трудно припомнить, применяется ли где-то подобное на практике для исправления ошибок «на лету».
Почему SEH-записи хранятся именно в стеке а не в каком-то другом месте? Возможно, потому что ОС не нужно заботиться об освобождении этой информации, эти записи просто пропадают как ненужные когда функция заканчивает
работу.
Это чем-то похоже на alloca(): (1.9.2 (стр. 48)).
Теперь вспомним MSVC
Должно быть, программистам Microsoft были нужны исключения в Си, но не
в Си++(для использования в ядре Windows NT, которое написано на Си), так
что они добавили нестандартное расширение Си в MSVC 44 . Оно не связано с
исключениями в Си++.
__try
{
...
}
__except(filter code)
{
handler code
}
44 MSDN

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1005
Блок «finally» может присутствовать вместо код обработчика:
__try
{
...
}
__finally
{
...
}

Код-фильтр — это выражение, отвечающее на вопрос, соответствует ли код
этого обработчика к поднятому исключению. Если ваш код слишком большой
и не помещается в одно выражение, отдельная функция-фильтр может быть
определена.
Таких конструкций много в ядре Windows. Вот несколько примеров оттуда (WRK):
Листинг 6.24: WRK-v1.2/base/ntos/ob/obwait.c
try {
KeReleaseMutant( (PKMUTANT)SignalObject,
MUTANT_INCREMENT,
FALSE,
TRUE );
} except((GetExceptionCode () == STATUS_ABANDONED ||
GetExceptionCode () == STATUS_MUTANT_NOT_OWNED)?
EXCEPTION_EXECUTE_HANDLER :
EXCEPTION_CONTINUE_SEARCH) {
Status = GetExceptionCode();
goto WaitExit;
}

Листинг 6.25: WRK-v1.2/base/ntos/cache/cachesub.c
try {
RtlCopyBytes( (PVOID)((PCHAR)CacheBuffer + PageOffset),
UserBuffer,
MorePages ?
(PAGE_SIZE − PageOffset) :
(ReceivedLength − PageOffset) );
} except( CcCopyReadExceptionFilter( GetExceptionInformation(),
&Status ) ) {

Вот пример кода-фильтра:
Листинг 6.26: WRK-v1.2/base/ntos/cache/copysup.c
LONG
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1006
CcCopyReadExceptionFilter(
IN PEXCEPTION_POINTERS ExceptionPointer,
IN PNTSTATUS ExceptionCode
)
/∗++
Routine Description:
This routine serves as an exception filter and has the special job of
extracting the "real" I/O error when Mm raises STATUS_IN_PAGE_ERROR
beneath us.
Arguments:
ExceptionPointer − A pointer to the exception record that contains
the real Io Status.
ExceptionCode − A pointer to an NTSTATUS that is to receive the real
status.
Return Value:
EXCEPTION_EXECUTE_HANDLER
−−∗/
{
∗ExceptionCode = ExceptionPointer−>ExceptionRecord−>ExceptionCode;
if ( (∗ExceptionCode == STATUS_IN_PAGE_ERROR) &&
(ExceptionPointer−>ExceptionRecord−>NumberParameters >= 3) ) {
∗ExceptionCode = (NTSTATUS) ExceptionPointer−>ExceptionRecord−>⤦
Ç ExceptionInformation[2];
}
ASSERT( !NT_SUCCESS(∗ExceptionCode) );
return EXCEPTION_EXECUTE_HANDLER;
}

Внутри, SEH это расширение исключений поддерживаемых OS.
Но функция обработчик теперь или _except_handler3 (для SEH3) или _except_handler4
(для SEH4). Код обработчика от MSVC, расположен в его библиотеках, или же в
msvcr*.dll. Очень важно понимать, что SEH это специфичное для MSVC. Другие
win32-компиляторы могут предлагать что-то совершенно другое.
SEH3
SEH3 имеет _except_handler3 как функцию-обработчик, и расширяет структуЕсли вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1007
ру
_EXCEPTION_REGISTRATION добавляя указатель на scope table и переменную previous
try level. SEH4 расширяет scope table добавляя еще 4 значения связанных с защитой от переполнения буфера.
scope table это таблица, состоящая из указателей на код фильтра и обработчика, для каждого уровня вложенности try/except.
TIB

Stack


+0: __except_list

FS:0

+4: …

Prev=0xFFFFFFFF

+8: …

Handle

функцияобработчик


таблица scope
0xFFFFFFFF (-1)
информация для
первого блока
try/except

Prev



функция-фильтр
функцияобработчик/finallyобработчик

функцияобработчик

Handle

Prev
_except_handler3

Handle
0
информация для
второго блока
try/except

таблица scope

функция-фильтр
функцияобработчик/finallyобработчик

предыдущий уровень try
EBP


1
информация для
третьего блока
try/except

функция-фильтр
функцияобработчик/finallyобработчик
… остальные
элементы …

И снова, очень важно понимать, что OS заботится только о полях prev/handle,
и больше ничего. Это работа функции _except_handler3 читать другие поля,
читать scope table и решать, какой обработчик исполнять и когда.
Исходный код функции _except_handler3 закрыт. Хотя, Sanos OS, имеющая
слой совместимости с win32, имеет некоторые функции написанные заново,

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1008
которые в каком-то смысле эквивалентны тем что в Windows 45 . Другие попытки реализации имеются в Wine46 и ReactOS47 .
Если указатель filter ноль, handler указывает на код finally .
Во время исполнения, значение previous try level в стеке меняется, чтобы функция _except_handler3 знала о текущем уровне вложенности, чтобы знать, какой элемент таблицы scope table использовать.
SEH3: пример с одним блоком try/except
#include
#include
#include
int main()
{
int∗ p = NULL;
__try
{
printf("hello #1!\n");
∗p = 13;
// causes an access violation exception;
printf("hello #2!\n");
}
__except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("access violation, can't recover\n");
}
}

Листинг 6.27: MSVC 2003
$SG74605
$SG74606
$SG74608
_DATA

DB
DB
DB
ENDS

'hello #1!', 0aH, 00H
'hello #2!', 0aH, 00H
'access violation, can''t recover', 0aH, 00H

; scope table:
CONST
SEGMENT
$T74622
DD
0ffffffffH
DD
FLAT:$L74617
DD
FLAT:$L74618

; previous try level
; filter
; handler

CONST
ENDS
_TEXT
SEGMENT
$T74621 = −32 ; size = 4
_p$ = −28
; size = 4
45 https://code.google.com/p/sanos/source/browse/src/win32/msvcrt/except.c
46 GitHub
47 http://doxygen.reactos.org/d4/df2/lib_2sdk_2crt_2except_2except_8c_source.html

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1009
__$SEHRec$ = −24 ; size = 24
_main
PROC NEAR
push
ebp
mov
ebp, esp
push
−1
; previous try level
push
OFFSET FLAT:$T74622
; scope table
push
OFFSET FLAT:__except_handler3
; handler
mov
eax, DWORD PTR fs:__except_list
push
eax
; prev
mov
DWORD PTR fs:__except_list, esp
add
esp, −16
; 3 registers to be saved:
push
ebx
push
esi
push
edi
mov
DWORD PTR __$SEHRec$[ebp], esp
mov
DWORD PTR _p$[ebp], 0
mov
DWORD PTR __$SEHRec$[ebp+20], 0 ; previous try level
push
OFFSET FLAT:$SG74605 ; 'hello #1!'
call
_printf
add
esp, 4
mov
eax, DWORD PTR _p$[ebp]
mov
DWORD PTR [eax], 13
push
OFFSET FLAT:$SG74606 ; 'hello #2!'
call
_printf
add
esp, 4
mov
DWORD PTR __$SEHRec$[ebp+20], −1 ; previous try level
jmp
SHORT $L74616
; filter code:
$L74617:
$L74627:
mov
ecx, DWORD PTR __$SEHRec$[ebp+4]
mov
edx, DWORD PTR [ecx]
mov
eax, DWORD PTR [edx]
mov
DWORD PTR $T74621[ebp], eax
mov
eax, DWORD PTR $T74621[ebp]
sub
eax, −1073741819; c0000005H
neg
eax
sbb
eax, eax
inc
eax
$L74619:
$L74626:
ret
0
; handler code:
$L74618:
mov
esp, DWORD PTR __$SEHRec$[ebp]
push
OFFSET FLAT:$SG74608 ; 'access violation, can''t recover'
call
_printf
add
esp, 4
mov
DWORD PTR __$SEHRec$[ebp+20], −1 ; setting previous try level back
to -1
$L74616:
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1010
xor
mov
mov
pop
pop
pop
mov
pop
ret
_main
_TEXT
END

eax, eax
ecx, DWORD PTR __$SEHRec$[ebp+8]
DWORD PTR fs:__except_list, ecx
edi
esi
ebx
esp, ebp
ebp
0
ENDP
ENDS

Здесь мы видим, как структура SEH конструируется в стеке. Scope table расположена в сегменте CONST — действительно, эти поля не будут меняться. Интересно, как меняется переменная previous try level. Исходное значение 0xFFFFFFFF
(−1). Момент, когда тело try открывается, обозначен инструкцией, записывающей 0 в эту переменную. В момент, когда тело try закрывается, −1 возвращается в нее назад. Мы также видим адреса кода фильтра и обработчика. Так
мы можем легко увидеть структуру конструкций try/except в функции.
Так как код инициализации SEH-структур в прологе функций может быть общим для нескольких функций, иногда компилятор вставляет в прологе вызов
функции SEH_prolog(), которая всё это делает. А код для деинициализации
SEH в функции SEH_epilog().
Запустим этот пример в tracer:
tracer.exe −l:2.exe −−dump−seh

Листинг 6.28: tracer.exe output
EXCEPTION_ACCESS_VIOLATION at 2.exe!main+0x44 (0x401054) ⤦
Ç ExceptionInformation[0]=1
EAX=0x00000000 EBX=0x7efde000 ECX=0x0040cbc8 EDX=0x0008e3c8
ESI=0x00001db1 EDI=0x00000000 EBP=0x0018feac ESP=0x0018fe80
EIP=0x00401054
FLAGS=AF IF RF
∗ SEH frame at 0x18fe9c prev=0x18ff78 handler=0x401204 (2.exe!⤦
Ç _except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=−1, filter=0x401070 (2.exe!main+0⤦
Ç x60) handler=0x401088 (2.exe!main+0x78)
∗ SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x401204 (2.exe!⤦
Ç _except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=−1, filter=0x401531 (2.exe!⤦
Ç mainCRTStartup+0x18d) handler=0x401545 (2.exe!mainCRTStartup+0x1a1)
∗ SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll!⤦
Ç __except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header:
GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1011
EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=−2, filter=0x771f74d0 (ntdll.dll!⤦
Ç ___safe_se_handler_table+0x20) handler=0x771f90eb (ntdll.dll!⤦
Ç _TppTerminateProcess@4+0x43)
∗ SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdll.dll!⤦
Ç _FinalExceptionHandler@16)

Мы видим, что цепочка SEH состоит из 4-х обработчиков.
Первые два расположены в нашем примере. Два? Но ведь мы же сделали только один? Да, второй был установлен в CRT-функции _mainCRTStartup(), и судя
по всему, он обрабатывает как минимум исключения связанные с FPU. Его код
можно посмотреть в инсталляции MSVC: crt/src/winxfltr.c.
Третий это SEH4 в ntdll.dll, и четвертый это обработчик, не имеющий отношения к MSVC, расположенный в ntdll.dll, имеющий «говорящее» название функции.
Как видно, в цепочке присутствуют обработчики трех типов: один не связан
с MSVC вообще (последний) и два связанных с MSVC: SEH3 и SEH4.
SEH3: пример с двумя блоками try/except
#include
#include
#include
int filter_user_exceptions (unsigned int code, struct _EXCEPTION_POINTERS ∗⤦
Ç ep)
{
printf("in filter. code=0x%08X\n", code);
if (code == 0x112233)
{
printf("yes, that is our exception\n");
return EXCEPTION_EXECUTE_HANDLER;
}
else
{
printf("not our exception\n");
return EXCEPTION_CONTINUE_SEARCH;
};
}
int main()
{
int∗ p = NULL;
__try
{
__try
{
printf ("hello!\n");
RaiseException (0x112233, 0, 0, NULL);
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1012
printf ("0x112233 raised. now let's crash\n");
∗p = 13;
// causes an access violation exception;
}
__except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("access violation, can't recover\n");
}
}
__except(filter_user_exceptions(GetExceptionCode(), ⤦
Ç GetExceptionInformation()))
{
// the filter_user_exceptions() function answering to the question
// "is this exception belongs to this block?"
// if yes, do the follow:
printf("user exception caught\n");
}
}

Теперь здесь два блока try. Так что scope table теперь содержит два элемента,
один элемент на каждый блок. Previous try level меняется вместе с тем, как
исполнение доходит до очередного try-блока, либо выходит из него.
Листинг 6.29: MSVC 2003
$SG74606
$SG74608
$SG74610
$SG74617
$SG74619
$SG74621
$SG74623

DB
DB
DB
DB
DB
DB
DB

'in filter. code=0x%08X', 0aH, 00H
'yes, that is our exception', 0aH, 00H
'not our exception', 0aH, 00H
'hello!', 0aH, 00H
'0x112233 raised. now let''s crash', 0aH, 00H
'access violation, can''t recover', 0aH, 00H
'user exception caught', 0aH, 00H

_code$ = 8
; size = 4
_ep$ = 12
; size = 4
_filter_user_exceptions PROC NEAR
push
ebp
mov
ebp, esp
mov
eax, DWORD PTR _code$[ebp]
push
eax
push
OFFSET FLAT:$SG74606 ; 'in filter. code=0x%08X'
call
_printf
add
esp, 8
cmp
DWORD PTR _code$[ebp], 1122867; 00112233H
jne
SHORT $L74607
push
OFFSET FLAT:$SG74608 ; 'yes, that is our exception'
call
_printf
add
esp, 4
mov
eax, 1
jmp
SHORT $L74605
$L74607:
push
OFFSET FLAT:$SG74610 ; 'not our exception'
call
_printf
add
esp, 4
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1013
xor
eax, eax
$L74605:
pop
ebp
ret
0
_filter_user_exceptions ENDP
; scope table:
CONST
SEGMENT
$T74644
DD
0ffffffffH
DD
FLAT:$L74634
DD
FLAT:$L74635
DD
00H
DD
FLAT:$L74638
DD
FLAT:$L74639
CONST
ENDS

;
;
;
;
;
;

previous try level for outer block
outer block filter
outer block handler
previous try level for inner block
inner block filter
inner block handler

$T74643 = −36
; size = 4
$T74642 = −32
; size = 4
_p$ = −28
; size = 4
__$SEHRec$ = −24 ; size = 24
_main
PROC NEAR
push
ebp
mov
ebp, esp
push
−1 ; previous try level
push
OFFSET FLAT:$T74644
push
OFFSET FLAT:__except_handler3
mov
eax, DWORD PTR fs:__except_list
push
eax
mov
DWORD PTR fs:__except_list, esp
add
esp, −20
push
ebx
push
esi
push
edi
mov
DWORD PTR __$SEHRec$[ebp], esp
mov
DWORD PTR _p$[ebp], 0
mov
DWORD PTR __$SEHRec$[ebp+20], 0 ; outer try block entered. set
previous try level to 0
mov
DWORD PTR __$SEHRec$[ebp+20], 1 ; inner try block entered. set
previous try level to 1
push
OFFSET FLAT:$SG74617 ; 'hello!'
call
_printf
add
esp, 4
push
0
push
0
push
0
push
1122867
; 00112233H
call
DWORD PTR __imp__RaiseException@16
push
OFFSET FLAT:$SG74619 ; '0x112233 raised. now let''s crash'
call
_printf
add
esp, 4
mov
eax, DWORD PTR _p$[ebp]
mov
DWORD PTR [eax], 13
mov
DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set
previous try level back to 0
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1014
jmp

SHORT $L74615

; inner block filter:
$L74638:
$L74650:
mov
ecx, DWORD PTR __$SEHRec$[ebp+4]
mov
edx, DWORD PTR [ecx]
mov
eax, DWORD PTR [edx]
mov
DWORD PTR $T74643[ebp], eax
mov
eax, DWORD PTR $T74643[ebp]
sub
eax, −1073741819; c0000005H
neg
eax
sbb
eax, eax
inc
eax
$L74640:
$L74648:
ret
0
; inner block handler:
$L74639:
mov
esp, DWORD PTR __$SEHRec$[ebp]
push
OFFSET FLAT:$SG74621 ; 'access violation, can''t recover'
call
_printf
add
esp, 4
mov
DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set
previous try level back to 0
$L74615:
mov
DWORD PTR __$SEHRec$[ebp+20], −1 ; outer try block exited, set
previous try level back to -1
jmp
SHORT $L74633
; outer block filter:
$L74634:
$L74651:
mov
ecx, DWORD PTR __$SEHRec$[ebp+4]
mov
edx, DWORD PTR [ecx]
mov
eax, DWORD PTR [edx]
mov
DWORD PTR $T74642[ebp], eax
mov
ecx, DWORD PTR __$SEHRec$[ebp+4]
push
ecx
mov
edx, DWORD PTR $T74642[ebp]
push
edx
call
_filter_user_exceptions
add
esp, 8
$L74636:
$L74649:
ret
0
; outer block handler:
$L74635:
mov
esp, DWORD PTR __$SEHRec$[ebp]
push
OFFSET FLAT:$SG74623 ; 'user exception caught'
call
_printf
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1015
add
esp, 4
mov
DWORD PTR __$SEHRec$[ebp+20], −1 ; both try blocks exited. set
previous try level back to -1
$L74633:
xor
eax, eax
mov
ecx, DWORD PTR __$SEHRec$[ebp+8]
mov
DWORD PTR fs:__except_list, ecx
pop
edi
pop
esi
pop
ebx
mov
esp, ebp
pop
ebp
ret
0
_main
ENDP

Если установить точку останова на функцию printf() вызываемую из обработчика, мы можем увидеть, что добавился еще один SEH-обработчик. Наверное,
это еще какая-то дополнительная механика, скрытая внутри процесса обработки исключений. Тут мы также видим scope table состоящую из двух элементов.
tracer.exe −l:3.exe bpx=3.exe!printf −−dump−seh

Листинг 6.30: tracer.exe output
(0) 3.exe!printf
EAX=0x0000001b EBX=0x00000000 ECX=0x0040cc58 EDX=0x0008e3c8
ESI=0x00000000 EDI=0x00000000 EBP=0x0018f840 ESP=0x0018f838
EIP=0x004011b6
FLAGS=PF ZF IF
∗ SEH frame at 0x18f88c prev=0x18fe9c handler=0x771db4ad (ntdll.dll!⤦
Ç ExecuteHandler2@20+0x3a)
∗ SEH frame at 0x18fe9c prev=0x18ff78 handler=0x4012e0 (3.exe!⤦
Ç _except_handler3)
SEH3 frame. previous trylevel=1
scopetable entry[0]. previous try level=−1, filter=0x401120 (3.exe!main+0⤦
Ç xb0) handler=0x40113b (3.exe!main+0xcb)
scopetable entry[1]. previous try level=0, filter=0x4010e8 (3.exe!main+0x78⤦
Ç ) handler=0x401100 (3.exe!main+0x90)
∗ SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x4012e0 (3.exe!⤦
Ç _except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=−1, filter=0x40160d (3.exe!⤦
Ç mainCRTStartup+0x18d) handler=0x401621 (3.exe!mainCRTStartup+0x1a1)
∗ SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll!⤦
Ç __except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header:
GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=−2, filter=0x771f74d0 (ntdll.dll!⤦
Ç ___safe_se_handler_table+0x20) handler=0x771f90eb (ntdll.dll!⤦
Ç _TppTerminateProcess@4+0x43)
∗ SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdll.dll!⤦
Ç _FinalExceptionHandler@16)
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1016

SEH4
Во время атаки переполнения буфера (1.26.2 (стр. 347)) адрес scope table может быть перезаписан, так что начиная с MSVC 2005, SEH3 был дополнен защитой от переполнения буфера, до SEH4. Указатель на scope table теперь проXOR-ен с security cookie.
Scope table расширена, теперь имеет заголовок, содержащий 2 указателя на
security cookies. Каждый элемент имеет смещение внутри стека на другое значение: это адрес фрейма (EBP) также про-XOR-еный с security_cookie расположенный в стеке. Это значение будет прочитано во время обработки исключения и проверено на правильность.
Security cookie в стеке случайное каждый раз, так что атакующий, как мы надеемся, не может предсказать его.
Изначальное значение previous try level это −2 в SEH4 вместо −1.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1017
TIB

Stack


+0: __except_list

FS:0

+4: …

Prev=0xFFFFFFFF

+8: …

Handle

функцияобработчик


таблица scope
смещение GS Cookie

Prev

смещение GS Cookie
XOR

информация для
первого блока
try/except

информация для
второго блока
try/except

функцияобработчик

Handle


смещение EH Cookie

Prev

смещение EH Cookie
XOR

Handle

_except_handler4

0xFFFFFFFF (-1)

таблица scope
⊕security_cookie

функция-фильтр

предыдущий уровень try

функцияобработчик/finallyобработчик

EBP

0

EBP⊕security_cookie



функция-фильтр



функцияобработчик/finallyобработчик
1
информация для
третьего блока
try/except

функция-фильтр
функцияобработчик/finallyобработчик
… остальные
элементы …

Оба примера скомпилированные в MSVC 2012 с SEH4:
Листинг 6.31: MSVC 2012: one try block example
$SG85485 DB
$SG85486 DB
$SG85488 DB

'hello #1!', 0aH, 00H
'hello #2!', 0aH, 00H
'access violation, can''t recover', 0aH, 00H

; scope table:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1018
xdata$x
__sehtable$_main
DD
DD
DD
DD
DD
DD
xdata$x

SEGMENT
DD 0fffffffeH
00H
0ffffffccH
00H
0fffffffeH
FLAT:$LN12@main
FLAT:$LN8@main
ENDS

;
;
;
;
;
;
;

GS Cookie Offset
GS Cookie XOR Offset
EH Cookie Offset
EH Cookie XOR Offset
previous try level
filter
handler

$T2 = −36
; size = 4
_p$ = −32
; size = 4
tv68 = −28
; size = 4
__$SEHRec$ = −24 ; size = 24
_main
PROC
push
ebp
mov
ebp, esp
push
−2
push
OFFSET __sehtable$_main
push
OFFSET __except_handler4
mov
eax, DWORD PTR fs:0
push
eax
add
esp, −20
push
ebx
push
esi
push
edi
mov
eax, DWORD PTR ___security_cookie
xor
DWORD PTR __$SEHRec$[ebp+16], eax ; xored pointer to scope table
xor
eax, ebp
push
eax
; ebp ^ security_cookie
lea
eax, DWORD PTR __$SEHRec$[ebp+8] ;
pointer to VC_EXCEPTION_REGISTRATION_RECORD
mov
DWORD PTR fs:0, eax
mov
DWORD PTR __$SEHRec$[ebp], esp
mov
DWORD PTR _p$[ebp], 0
mov
DWORD PTR __$SEHRec$[ebp+20], 0 ; previous try level
push
OFFSET $SG85485 ; 'hello #1!'
call
_printf
add
esp, 4
mov
eax, DWORD PTR _p$[ebp]
mov
DWORD PTR [eax], 13
push
OFFSET $SG85486 ; 'hello #2!'
call
_printf
add
esp, 4
mov
DWORD PTR __$SEHRec$[ebp+20], −2 ; previous try level
jmp
SHORT $LN6@main
; filter:
$LN7@main:
$LN12@main:
mov
ecx, DWORD PTR __$SEHRec$[ebp+4]
mov
edx, DWORD PTR [ecx]
mov
eax, DWORD PTR [edx]
mov
DWORD PTR $T2[ebp], eax
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1019
cmp
DWORD PTR $T2[ebp], −1073741819 ; c0000005H
jne
SHORT $LN4@main
mov
DWORD PTR tv68[ebp], 1
jmp
SHORT $LN5@main
$LN4@main:
mov
DWORD PTR tv68[ebp], 0
$LN5@main:
mov
eax, DWORD PTR tv68[ebp]
$LN9@main:
$LN11@main:
ret
0
; handler:
$LN8@main:
mov
esp, DWORD PTR __$SEHRec$[ebp]
push
OFFSET $SG85488 ; 'access violation, can''t recover'
call
_printf
add
esp, 4
mov
DWORD PTR __$SEHRec$[ebp+20], −2 ; previous try level
$LN6@main:
xor
eax, eax
mov
ecx, DWORD PTR __$SEHRec$[ebp+8]
mov
DWORD PTR fs:0, ecx
pop
ecx
pop
edi
pop
esi
pop
ebx
mov
esp, ebp
pop
ebp
ret
0
_main
ENDP

Листинг 6.32: MSVC 2012: two try blocks example
$SG85486
$SG85488
$SG85490
$SG85497
$SG85499
$SG85501
$SG85503

DB
DB
DB
DB
DB
DB
DB

'in filter. code=0x%08X', 0aH, 00H
'yes, that is our exception', 0aH, 00H
'not our exception', 0aH, 00H
'hello!', 0aH, 00H
'0x112233 raised. now let''s crash', 0aH, 00H
'access violation, can''t recover', 0aH, 00H
'user exception caught', 0aH, 00H

xdata$x
SEGMENT
__sehtable$_main DD 0fffffffeH
DD
00H
DD
0ffffffc8H
DD
00H
DD
0fffffffeH
DD
FLAT:$LN19@main
DD
FLAT:$LN9@main
DD
00H
DD
FLAT:$LN18@main
DD
FLAT:$LN13@main
xdata$x
ENDS

;
;
;
;
;
;
;
;
;
;

GS Cookie Offset
GS Cookie XOR Offset
EH Cookie Offset
EH Cookie Offset
previous try level for outer block
outer block filter
outer block handler
previous try level for inner block
inner block filter
inner block handler

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1020
$T2 = −40
; size = 4
$T3 = −36
; size = 4
_p$ = −32
; size = 4
tv72 = −28
; size = 4
__$SEHRec$ = −24 ; size = 24
_main
PROC
push
ebp
mov
ebp, esp
push
−2 ; initial previous try level
push
OFFSET __sehtable$_main
push
OFFSET __except_handler4
mov
eax, DWORD PTR fs:0
push
eax ; prev
add
esp, −24
push
ebx
push
esi
push
edi
mov
eax, DWORD PTR ___security_cookie
xor
DWORD PTR __$SEHRec$[ebp+16], eax
; xored pointer to scope
table
xor
eax, ebp
; ebp ^ security_cookie
push
eax
lea
eax, DWORD PTR __$SEHRec$[ebp+8]
;
pointer to VC_EXCEPTION_REGISTRATION_RECORD
mov
DWORD PTR fs:0, eax
mov
DWORD PTR __$SEHRec$[ebp], esp
mov
DWORD PTR _p$[ebp], 0
mov
DWORD PTR __$SEHRec$[ebp+20], 0 ; entering outer try block,
setting previous try level=0
mov
DWORD PTR __$SEHRec$[ebp+20], 1 ; entering inner try block,
setting previous try level=1
push
OFFSET $SG85497 ; 'hello!'
call
_printf
add
esp, 4
push
0
push
0
push
0
push
1122867 ; 00112233H
call
DWORD PTR __imp__RaiseException@16
push
OFFSET $SG85499 ; '0x112233 raised. now let''s crash'
call
_printf
add
esp, 4
mov
eax, DWORD PTR _p$[ebp]
mov
DWORD PTR [eax], 13
mov
DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, set
previous try level back to 0
jmp
SHORT $LN2@main
; inner block filter:
$LN12@main:
$LN18@main:
mov
ecx, DWORD PTR __$SEHRec$[ebp+4]
mov
edx, DWORD PTR [ecx]
mov
eax, DWORD PTR [edx]
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1021
mov
DWORD PTR $T3[ebp], eax
cmp
DWORD PTR $T3[ebp], −1073741819 ; c0000005H
jne
SHORT $LN5@main
mov
DWORD PTR tv72[ebp], 1
jmp
SHORT $LN6@main
$LN5@main:
mov
DWORD PTR tv72[ebp], 0
$LN6@main:
mov
eax, DWORD PTR tv72[ebp]
$LN14@main:
$LN16@main:
ret
0
; inner block handler:
$LN13@main:
mov
esp, DWORD PTR __$SEHRec$[ebp]
push
OFFSET $SG85501 ; 'access violation, can''t recover'
call
_printf
add
esp, 4
mov
DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, setting
previous try level back to 0
$LN2@main:
mov
DWORD PTR __$SEHRec$[ebp+20], −2 ; exiting both blocks, setting
previous try level back to -2
jmp
SHORT $LN7@main
; outer block filter:
$LN8@main:
$LN19@main:
mov
ecx, DWORD PTR __$SEHRec$[ebp+4]
mov
edx, DWORD PTR [ecx]
mov
eax, DWORD PTR [edx]
mov
DWORD PTR $T2[ebp], eax
mov
ecx, DWORD PTR __$SEHRec$[ebp+4]
push
ecx
mov
edx, DWORD PTR $T2[ebp]
push
edx
call
_filter_user_exceptions
add
esp, 8
$LN10@main:
$LN17@main:
ret
0
; outer block handler:
$LN9@main:
mov
esp, DWORD PTR __$SEHRec$[ebp]
push
OFFSET $SG85503 ; 'user exception caught'
call
_printf
add
esp, 4
mov
DWORD PTR __$SEHRec$[ebp+20], −2 ; exiting both blocks, setting
previous try level back to -2
$LN7@main:
xor
eax, eax
mov
ecx, DWORD PTR __$SEHRec$[ebp+8]
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1022
mov
pop
pop
pop
pop
mov
pop
ret
_main

DWORD PTR fs:0, ecx
ecx
edi
esi
ebx
esp, ebp
ebp
0
ENDP

_code$ = 8 ; size = 4
_ep$ = 12
; size = 4
_filter_user_exceptions PROC
push
ebp
mov
ebp, esp
mov
eax, DWORD PTR _code$[ebp]
push
eax
push
OFFSET $SG85486 ; 'in filter. code=0x%08X'
call
_printf
add
esp, 8
cmp
DWORD PTR _code$[ebp], 1122867 ; 00112233H
jne
SHORT $LN2@filter_use
push
OFFSET $SG85488 ; 'yes, that is our exception'
call
_printf
add
esp, 4
mov
eax, 1
jmp
SHORT $LN3@filter_use
jmp
SHORT $LN3@filter_use
$LN2@filter_use:
push
OFFSET $SG85490 ; 'not our exception'
call
_printf
add
esp, 4
xor
eax, eax
$LN3@filter_use:
pop
ebp
ret
0
_filter_user_exceptions ENDP

Вот значение cookies: Cookie Offset это разница между адресом записанного в стеке значения EBP и значения EBP ⊕ security_cookie в стеке. Cookie XOR
Offset это дополнительная разница между значением EBP ⊕ security_cookie и
тем что записано в стеке. Если это уравнение не верно, то процесс остановится из-за разрушения стека:
security_cookie ⊕ (CookieXOROf f set + address_of _saved_EBP ) ==
stack[address_of _saved_EBP + CookieOf f set]
Если Cookie Offset равно −2, это значит, что оно не присутствует.
Windows x64
Как видно, это не самая быстрая штука, устанавливать SEH-структуры в каждом прологе функции. Еще одна проблема производительности — это менять
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1023
переменную previous try level много раз в течении исполнении функции. Так
что в x64 всё сильно изменилось, теперь все указатели на try-блоки, функции фильтров и обработчиков, теперь записаны в другом PE-сегменте .pdata,
откуда обработчик исключений ОС берет всю информацию.
Вот два примера из предыдущей секции, скомпилированных для x64:
Листинг 6.33: MSVC 2012
$SG86276 DB
$SG86277 DB
$SG86279 DB

'hello #1!', 0aH, 00H
'hello #2!', 0aH, 00H
'access violation, can''t recover', 0aH, 00H

pdata
SEGMENT
$pdata$main DD imagerel $LN9
DD
imagerel $LN9+61
DD
imagerel $unwind$main
pdata
ENDS
pdata
SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
DD
imagerel main$filt$0+32
DD
imagerel $unwind$main$filt$0
pdata
ENDS
xdata
SEGMENT
$unwind$main DD 020609H
DD
030023206H
DD
imagerel __C_specific_handler
DD
01H
DD
imagerel $LN9+8
DD
imagerel $LN9+40
DD
imagerel main$filt$0
DD
imagerel $LN9+40
$unwind$main$filt$0 DD 020601H
DD
050023206H
xdata
ENDS
_TEXT
main
$LN9:

SEGMENT
PROC

push
sub
xor
lea
call
mov
lea
call
jmp
$LN6@main:
lea
recover'
call
npad
$LN8@main:
xor

rbx
rsp, 32
ebx, ebx
rcx, OFFSET FLAT:$SG86276 ; 'hello #1!'
printf
DWORD PTR [rbx], 13
rcx, OFFSET FLAT:$SG86277 ; 'hello #2!'
printf
SHORT $LN8@main
rcx, OFFSET FLAT:$SG86279 ; 'access violation, can''t
printf
1 ; align next label
eax, eax

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1024

main
_TEXT

add
pop
ret
ENDP
ENDS

rsp, 32
rbx
0

text$x SEGMENT
main$filt$0 PROC
push
rbp
sub
rsp, 32
mov
rbp, rdx
$LN5@main$filt$:
mov
rax, QWORD PTR [rcx]
xor
ecx, ecx
cmp
DWORD PTR [rax], −1073741819; c0000005H
sete
cl
mov
eax, ecx
$LN7@main$filt$:
add
rsp, 32
pop
rbp
ret
0
int
3
main$filt$0 ENDP
text$x ENDS

Листинг 6.34: MSVC 2012
$SG86277
$SG86279
$SG86281
$SG86288
$SG86290
$SG86292
$SG86294

DB
DB
DB
DB
DB
DB
DB

'in filter. code=0x%08X', 0aH, 00H
'yes, that is our exception', 0aH, 00H
'not our exception', 0aH, 00H
'hello!', 0aH, 00H
'0x112233 raised. now let''s crash', 0aH, 00H
'access violation, can''t recover', 0aH, 00H
'user exception caught', 0aH, 00H

pdata
SEGMENT
$pdata$filter_user_exceptions DD imagerel $LN6
DD
imagerel $LN6+73
DD
imagerel $unwind$filter_user_exceptions
$pdata$main DD imagerel $LN14
DD
imagerel $LN14+95
DD
imagerel $unwind$main
pdata
ENDS
pdata
SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
DD
imagerel main$filt$0+32
DD
imagerel $unwind$main$filt$0
$pdata$main$filt$1 DD imagerel main$filt$1
DD
imagerel main$filt$1+30
DD
imagerel $unwind$main$filt$1
pdata
ENDS
xdata
SEGMENT
$unwind$filter_user_exceptions DD 020601H
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1025
DD
030023206H
$unwind$main DD 020609H
DD
030023206H
DD
imagerel __C_specific_handler
DD
02H
DD
imagerel $LN14+8
DD
imagerel $LN14+59
DD
imagerel main$filt$0
DD
imagerel $LN14+59
DD
imagerel $LN14+8
DD
imagerel $LN14+74
DD
imagerel main$filt$1
DD
imagerel $LN14+74
$unwind$main$filt$0 DD 020601H
DD
050023206H
$unwind$main$filt$1 DD 020601H
DD
050023206H
xdata
ENDS
_TEXT
main
$LN14:

SEGMENT
PROC

push
sub
xor
lea
call
xor
xor
xor
mov
call
lea
crash'
call
mov
jmp
$LN11@main:
lea
recover'
call
npad
$LN13@main:
jmp
$LN7@main:
lea
call
npad
$LN9@main:
xor
add
pop
ret
main
ENDP

rbx
rsp, 32
ebx, ebx
rcx, OFFSET FLAT:$SG86288 ; 'hello!'
printf
r9d, r9d
r8d, r8d
edx, edx
ecx, 1122867 ; 00112233H
QWORD PTR __imp_RaiseException
rcx, OFFSET FLAT:$SG86290 ; '0x112233 raised. now let''s
printf
DWORD PTR [rbx], 13
SHORT $LN13@main
rcx, OFFSET FLAT:$SG86292 ; 'access violation, can''t
printf
1 ; align next label
SHORT $LN9@main
rcx, OFFSET FLAT:$SG86294 ; 'user exception caught'
printf
1 ; align next label
eax, eax
rsp, 32
rbx
0

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1026
text$x SEGMENT
main$filt$0 PROC
push
rbp
sub
rsp, 32
mov
rbp, rdx
$LN10@main$filt$:
mov
rax, QWORD PTR [rcx]
xor
ecx, ecx
cmp
DWORD PTR [rax], −1073741819; c0000005H
sete
cl
mov
eax, ecx
$LN12@main$filt$:
add
rsp, 32
pop
rbp
ret
0
int
3
main$filt$0 ENDP
main$filt$1 PROC
push
rbp
sub
rsp, 32
mov
rbp, rdx
$LN6@main$filt$:
mov
rax, QWORD PTR [rcx]
mov
rdx, rcx
mov
ecx, DWORD PTR [rax]
call
filter_user_exceptions
npad
1 ; align next label
$LN8@main$filt$:
add
rsp, 32
pop
rbp
ret
0
int
3
main$filt$1 ENDP
text$x ENDS
_TEXT
SEGMENT
code$ = 48
ep$ = 56
filter_user_exceptions PROC
$LN6:
push
rbx
sub
rsp, 32
mov
ebx, ecx
mov
edx, ecx
lea
rcx, OFFSET FLAT:$SG86277 ; 'in filter. code=0x%08X'
call
printf
cmp
ebx, 1122867; 00112233H
jne
SHORT $LN2@filter_use
lea
rcx, OFFSET FLAT:$SG86279 ; 'yes, that is our exception'
call
printf
mov
eax, 1

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1027
add
rsp, 32
pop
rbx
ret
0
$LN2@filter_use:
lea
rcx, OFFSET FLAT:$SG86281 ; 'not our exception'
call
printf
xor
eax, eax
add
rsp, 32
pop
rbx
ret
0
filter_user_exceptions ENDP
_TEXT
ENDS

Смотрите [Igor Skochinsky, Compiler Internals: Exceptions and RTTI, (2012)]
более детального описания.

48

для

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

SEH

[Matt Pietrek, A Crash Course on the Depths of Win32™ Structured Exception Handling,
(1997)]49 , [Igor Skochinsky, Compiler Internals: Exceptions and RTTI, (2012)] 50 .

6.5.4. Windows NT: Критические секции
Критические секции в любой ОС очень важны в мультитредовой среде, используются в основном для обеспечения гарантии что только один тред будет
иметь доступ к данным в один момент времени, блокируя остальные треды и
прерывания.
Вот как структура CRITICAL_SECTION объявлена в линейке OS Windows NT:
Листинг 6.35: (Windows Research Kernel v1.2) public/sdk/inc/nturtl.h
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread;

// from the thread's ClientId->UniqueThread

48 Также доступно здесь: http://yurichev.com/mirrors/RE/Recon-2012-Skochinsky-Compiler-Internals.
pdf
49 Также доступно здесь: http://www.microsoft.com/msj/0197/Exception/Exception.aspx
50 Также доступно здесь: http://yurichev.com/mirrors/RE/Recon-2012-Skochinsky-Compiler-Internals.
pdf

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1028
HANDLE LockSemaphore;
ULONG_PTR SpinCount;
// force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, ∗PRTL_CRITICAL_SECTION;

Вот как работает функция EnterCriticalSection():
Листинг 6.36: Windows 2008/ntdll.dll/x86 (begin)
_RtlEnterCriticalSection@4
var_C
var_8
var_4
arg_0

=
=
=
=

dword
dword
dword
dword

ptr −0Ch
ptr −8
ptr −4
ptr 8

mov
edi, edi
push
ebp
mov
ebp, esp
sub
esp, 0Ch
push
esi
push
edi
mov
edi, [ebp+arg_0]
lea
esi, [edi+4] ; LockCount
mov
eax, esi
lock btr dword ptr [eax], 0
jnb
wait ; jump if CF=0
loc_7DE922DD:
mov
mov
mov
mov
pop
xor
pop
mov
pop
retn

eax, large fs:18h
ecx, [eax+24h]
[edi+0Ch], ecx
dword ptr [edi+8], 1
edi
eax, eax
esi
esp, ebp
ebp
4

... skipped

Самая важная инструкция в этом фрагменте кода — это BTR (с префиксом
LOCK): нулевой бит сохраняется в флаге CF и очищается в памяти . Это атомарная операция, блокирующая доступ всех остальных процессоров к этому
значению в памяти (обратите внимание на префикс LOCK перед инструкцией
BTR.
Если бит в LockCount является 1, хорошо, сбросить его и вернуться из функции:
мы в критической секции . Если нет — критическая секция уже занята другим
тредом, тогда ждем .
Ожидание там сделано через вызов WaitForSingleObject().
А вот как работает функция LeaveCriticalSection():
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1029
Листинг 6.37: Windows 2008/ntdll.dll/x86 (begin)
_RtlLeaveCriticalSection@4 proc near
arg_0

= dword ptr

8

mov
edi, edi
push
ebp
mov
ebp, esp
push
esi
mov
esi, [ebp+arg_0]
add
dword ptr [esi+8], 0FFFFFFFFh ; RecursionCount
jnz
short loc_7DE922B2
push
ebx
push
edi
lea
edi, [esi+4]
; LockCount
mov
dword ptr [esi+0Ch], 0
mov
ebx, 1
mov
eax, edi
lock xadd [eax], ebx
inc
ebx
cmp
ebx, 0FFFFFFFFh
jnz
loc_7DEA8EB7
loc_7DE922B0:
pop
pop

edi
ebx

xor
pop
pop
retn

eax, eax
esi
ebp
4

loc_7DE922B2:

... skipped

XADD это «обменять и прибавить». В данном случае, это значит прибавить 1 к
значению в LockCount, при этом сохранить изначальное значение LockCount
в регистре EBX. Впрочем, значение в EBX позже инкрементируется при помощи последующей инструкции INC EBX, и оно также будет равно обновленному
значению LockCount.
Эта операция также атомарная, потому что также имеет префикс LOCK, что
означает, что другие CPU или ядра CPU в системе не будут иметь доступа к
этой ячейке памяти .
Префикс LOCK очень важен: два треда, каждый из которых работает на разных
CPU или ядрах CPU, могут попытаться одновременно войти в критическую секцию, одновременно модифицируя значение в памяти, и это может привести к
непредсказуемым результатам.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

Глава 7

Инструменты
7.1. Дизассемблеры
7.1.1. IDA
Старая бесплатная версия доступна для скачивания 1 .
Краткий справочник горячих клавиш: .6.1 (стр. 1331)

7.2. Отладчики
7.2.1. OllyDbg
Очень популярный отладчик пользовательской среды win32: ollydbg.de.
Краткий справочник горячих клавиш: .6.2 (стр. 1331)

7.2.2. GDB
Не очень популярный отладчик у реверсеров, тем не менее, крайне удобный.
Некоторые команды: .6.5 (стр. 1332).

7.2.3. tracer
Автор часто использует tracer

2

вместо отладчика.

Со временем, автор этих строк отказался использовать отладчик, потому что
всё что ему нужно от него это иногда подсмотреть какие-либо аргументы какойлибо функции во время исполнения или состояние регистров в определенном
1 hex-rays.com/products/ida/support/download_freeware.shtml
2 yurichev.com

1030

1031
месте. Каждый раз загружать отладчик для этого это слишком, поэтому родилась очень простая утилита tracer. Она консольная, запускается из командной
строки, позволяет перехватывать исполнение функций, ставить точки останова на произвольные места, смотреть состояние регистров, модифицировать
их, итд.
Но для учебы очень полезно трассировать код руками в отладчике, наблюдать как меняются значения регистров (например, как минимум классический
SoftICE, OllyDbg, WinDbg подсвечивают измененные регистры), флагов, данные,
менять их самому, смотреть реакцию, итд.

7.3. Трассировка системных вызовов
strace / dtruss
Позволяет показать, какие системные вызовы (syscalls(6.3 (стр. 974))) прямо
сейчас вызывает процесс.
Например:
# strace df −h
...
access("/etc/ld.so.nohwcap", F_OK)
= −1 ENOENT (No such file or ⤦
Ç directory)
open("/lib/i386−linux−gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF⤦
Ç \1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220\232\1\0004\0\0\0"..., ⤦
Ç 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1770984, ...}) = 0
mmap2(NULL, 1780508, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) ⤦
Ç = 0xb75b3000

В Mac OS X для этого же имеется dtruss.
В Cygwin также есть strace, впрочем, насколько известно, он показывает результаты только для .exe-файлов скомпилированных для среды самого cygwin.

7.4. Декомпиляторы
Пока существует только один публично доступный декомпилятор в Си высокого качества: Hex-Rays: hex-rays.com/products/decompiler/
Чиайте больше о нем: 10.8 (стр. 1278).

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1032

7.5. Прочие инструменты
• Microsoft Visual Studio Express3 : Усеченная бесплатная версия Visual Studio,
пригодная для простых экспериментов. Некоторые полезные опции: .6.3
(стр. 1332).
• Hiew4 : для мелкой модификации кода в исполняемых файлах.
• binary grep: небольшая утилита для поиска констант (либо просто последовательности байт) в большом количестве файлов, включая неисполняемые: GitHub. В rada.re имеется также rafind2 для тех же целей.

7.5.1. Калькуляторы
Хороший калькулятор для нужд реверс-инженера должен поддерживать как
минимум десятичную, шестнадцатеричную и двоичную системы счисления, а
также многие важные операции как “исключающее ИЛИ” и сдвиги.
• В IDA есть встроенный калькулятор (“?”).
• В rada.re есть rax2.
• https://yurichev.com/progcalc/
• Стандартный калькулятор в Windows имеет режим программистского калькулятора.

7.6. Чего-то здесь недостает?
Если вы знаете о хорошем инструменте, которого не хватает здесь в этом списке, пожалуйста сообщите мне об этом:
.

3 visualstudio.com/en-US/products/visual-studio-express-vs
4 hiew.ru

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

Глава 8

Примеры из практики
Вместо эпиграфа:
Питер Сейбел: Как вы читаете исходный код? Ведь непросто
читать даже то, что написано на известном вам языке программирования.
Дональд Кнут: Но это действительно того стоит, если говорить о том, что выстраивается в вашей голове. Как я читаю код?
Когда-то была машина под названием Bunker Ramo 300, и кто-то
мне однажды сказал, что компилятор Фортрана для этой машины
работает чрезвычайно быстро, но никто не понимает почему. Я заполучил копию его исходного кода. У меня не было руководства
по этому компьютеру, поэтому я даже не был уверен, какой это
был машинный язык.
Но я взялся за это, посчитав интересной задачей. Я нашел
BEGIN и начал разбираться. В кодах операций есть ряд двухбуквенных мнемоник, поэтому я мог начать анализировать: “Возможно, это инструкция загрузки, а это, возможно, инструкция перехода”. Кроме того, я знал, что это компилятор Фортрана, и иногда
он обращался к седьмой колонке перфокарты - там он мог определить, комментарий это или нет.
Спустя три часа я кое-что понял об этом компьютере. Затем
обнаружил огромные таблицы ветвлений. То есть это была своего
рода головоломка, и я продолжал рисовать небольшие схемы, как
разведчик, пытающийся разгадать секретный шифр. Но я знал,
что программа работает, и знал, что это компилятор Фортрана —
это не был шифр, в том смысле что программа не была написана
с сознательной целью запутать. Все дело было в коде, поскольку
у меня не было руководства по компьютеру.
В конце концов мне удалось выяснить, почему компилятор работал так быстро. К сожалению, дело было не в гениальных алгоритмах - просто там применялись методы неструктурированного

1033

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

8.1. Шутка с Маджонгом (Windows 7)
Маджонг это замечательная игра, однако, можем ли мы усложнить её, запретив пункт меню Hint (подсказка)?
В Windows 7, я могу найти файлы Mahjong.dll и Mahjong.exe в:
C:\Windows\winsxs\
x86_microsoft-windows-s..inboxgames-shanghai_31bf3856ad364e35_6.1.7600.16385_none\
c07a51d9507d9398.
Также, файл Mahjong.exe.mui в:
C:\Windows\winsxs\
x86_microsoft-windows-s..-shanghai.resources_31bf3856ad364e35_6.1.7600.16385_en-us
_c430954533c66bf3
ив
C:\Windows\winsxs\
x86_microsoft-windows-s..-shanghai.resources_31bf3856ad364e35_6.1.7600.16385_ru-ru
_0d51acf984cb679a.
Я использую англоязычную Windows, но с поддержкой русского языка, так что
тут могут быть файлы ресурсов для двух языков. Открыв файл Mahjong.exe.mui
в Resource Hacker, можно увидеть определения меню:
Листинг 8.1: Ресурсы меню в Mahjong.exe.mui
103 MENU
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
{
POPUP "&Game"
{
MENUITEM "&New Game\tF2", 40000
MENUITEM SEPARATOR
MENUITEM "&Undo\tCtrl+Z", 40001
MENUITEM "&Hint\tH", 40002
MENUITEM SEPARATOR
MENUITEM "&Statistics\tF4", 40003
MENUITEM "&Options\tF5", 40004
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1035
MENUITEM "Change &Appearance\tF7",
MENUITEM SEPARATOR
MENUITEM "E&xit", 40006

40005

}
POPUP "&Help"
{
MENUITEM "&View Help\tF1", 40015
MENUITEM "&About Mahjong Titans", 40016
MENUITEM SEPARATOR
MENUITEM "Get &More Games Online", 40020
}
}

Подменю Hint имеет код 40002. Открываю Mahjong.exe в IDA и ищу значение
40002.
(Я пишу это в ноябре 2019. Почему-то, IDA не может выкачать PDB-файлы с
серверов Microsoft. Может быть, Windows 7 больше не поддерживается? Так
или иначе, имен ф-ций я не смог установить...)
Листинг 8.2: Mahjong.exe
.text:010205C8 6A 03
.text:010205CA 85 FF
.text:010205CC 5B

push
test
pop

3
edi, edi
ebx

push
push
call
cmp
mov
jnz

edi
; uIDEnableItem
hmenu ; hMenu
esi
; EnableMenuItem
[ebp+arg_0], 1
edi, 40002
short loc_1020651 ; переход должен

push
push
push
call
push
push
push
call
jmp

0
; uEnable
edi
; uIDEnableItem
hMenu ; hMenu
esi
; EnableMenuItem
0
; uEnable
edi
; uIDEnableItem
hmenu ; hMenu
esi
; EnableMenuItem
short loc_102066B

...
.text:01020625 57
.text:01020626 FF
.text:0102062C FF
.text:0102062E 83
.text:01020632 BF
.text:01020637 75
быть всегда
.text:01020639 6A
.text:0102063B 57
.text:0102063C FF
.text:01020642 FF
.text:01020644 6A
.text:01020646 57
.text:01020647 FF
.text:0102064D FF
.text:0102064F EB
.text:01020651
.text:01020651
.text:01020651 53
.text:01020652 57
.text:01020653 FF
.text:01020659 FF
.text:0102065B 53
.text:0102065C 57
.text:0102065D FF
.text:01020663 FF

35 C8 97 08 01
D6
7D 08 01
42 9C 00 00
18
00
35 B4 8B 08 01
D6
00
35 C8 97 08 01
D6
1A

loc_1020651:
push
ebx
push
edi
35 B4 8B 08 01 push
hMenu
D6
call
esi
push
ebx
push
edi
35 C8 97 08 01 push
hmenu
D6
call
esi

;
;
;
;
;
;
;
;
;

CODE XREF: sub_1020581+B6
3
uIDEnableItem
hMenu
EnableMenuItem
3
uIDEnableItem
hMenu
EnableMenuItem

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1036
Эта часть кода разрешает или запрещает пункт меню Hint.
И согласно MSDN1 :
MF_DISABLED | MF_GRAYED = 3 и MF_ENABLED = 0.
Я думаю, эта ф-ция разрешает или запрещает несколько пунктов меню (Hint,
Undo, итд), исходя из значения в arg_0. Потому что при старте, когда пользователь выбирает тип игры, пункты Hint и Undo запрещены. Они разрешены,
когда игра начинается.
Так что я изменяю в файле Mahjong.exe по адресу 0x01020637 байт 0x75 на
0xEB, делая так, что переход JNZ всегда будет работать. Эффект в том, что фция всегда будет вызываться как EnableMenuItem(..., ..., 3). Теперь подменю Hint всё время запрещено.
Также, почему-то, ф-ция EnableMenuItem() вызывается дважды, для hMenu и
для hmenu. Может быть, в программе два меню, и может быть, они переключаются?
В качестве домашней работы, попробуйте запретить подменю Undo, чтобы сделать игру еще труднее.

8.2. Шутка с task manager (Windows Vista)
Посмотрим, сможем ли мы немного хакнуть Task Manager, чтобы он находил
больше ядер в CPU, чем присутствует.
В начале задумаемся, откуда Task Manager знает количество ядер?
В win32 имеется функция GetSystemInfo(), при помощи которой можно узнать.
Но она не импортируется в taskmgr.exe. Есть еще одна в NTAPI, NtQuerySystemInformation(),
которая используется в taskmgr.exe в ряде мест.
Чтобы узнать количество ядер, нужно вызвать эту функцию с константной
SystemBasicInformation в первом аргументе (а это ноль
2

).

Второй аргумент должен указывать на буфер, который примет всю информацию.
Так что нам нужно найти все вызовы функции
NtQuerySystemInformation(0, ?, ?, ?).
Откроем taskmgr.exe в IDA. Что всегда хорошо с исполняемыми файлами от
Microsoft, это то что IDA может скачать соответствующий PDB-файл именно
для этого файла и добавить все имена функций.
Видимо, Task Manager написан на Си++и некоторые функции и классы имеют
говорящие за себя имена.
1 https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enablemenuitem
2 MSDN

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1037
Тут есть классы CAdapter, CNetPage, CPerfPage, CProcInfo, CProcPage, CSvcPage,
CTaskPage, CUserPage. Должно быть, каждый класс соответствует каждой вкладке в Task Manager.
Пройдемся по всем вызовам и добавим комментарий с числом, передающимся
как первый аргумент.
В некоторых местах напишем «not zero», потому что значение в тех местах
однозначно не ноль, но что-то другое (больше об этом во второй части главы).
А мы все-таки ищем ноль передаваемый как аргумент.

Рис. 8.1: IDA: вызовы функции NtQuerySystemInformation()
Да, имена действительно говорящие сами за себя.
Когда мы внимательно изучим каждое место, где вызывается
NtQuerySystemInformation(0, ?, ?, ?), то быстро найдем то что нужно в функции InitPerfInfo():
Листинг 8.3: taskmgr.exe (Windows Vista)
.text:10000B4B3
xor
.text:10000B4B6
lea
.text:10000B4BB
xor
.text:10000B4BD
lea
.text:10000B4C1
mov
.text:10000B4C4
call
.text:10000B4CA
xor
.text:10000B4CC
cmp
.text:10000B4CE
jge
.text:10000B4D0
.text:10000B4D0 loc_10000B4D0:
InitPerfInfo(void)+97

r9d, r9d
rdx, [rsp+0C78h+var_C58] ; buffer
ecx, ecx
ebp, [r9+40h]
r8d, ebp
cs:__imp_NtQuerySystemInformation ; 0
ebx, ebx
eax, ebx
short loc_10000B4D7
; CODE XREF:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1038
.text:10000B4D0
;
InitPerfInfo(void)+AF
.text:10000B4D0
xor
al, al
.text:10000B4D2
jmp
loc_10000B5EA
.text:10000B4D7 ;
--------------------------------------------------------------------------.text:10000B4D7
.text:10000B4D7 loc_10000B4D7:
; CODE XREF:
InitPerfInfo(void)+36
.text:10000B4D7
mov
eax, [rsp+0C78h+var_C50]
.text:10000B4DB
mov
esi, ebx
.text:10000B4DD
mov
r12d, 3E80h
.text:10000B4E3
mov
cs:?g_PageSize@@3KA, eax ; ulong g_PageSize
.text:10000B4E9
shr
eax, 0Ah
.text:10000B4EC
lea
r13, __ImageBase
.text:10000B4F3
imul
eax, [rsp+0C78h+var_C4C]
.text:10000B4F8
cmp
[rsp+0C78h+var_C20], bpl
.text:10000B4FD
mov
cs:?g_MEMMax@@3_JA, rax ; __int64 g_MEMMax
.text:10000B504
movzx
eax, [rsp+0C78h+var_C20] ; number of CPUs
.text:10000B509
cmova
eax, ebp
.text:10000B50C
cmp
al, bl
.text:10000B50E
mov
cs:?g_cProcessors@@3EA, al ;
uchar g_cProcessors

g_cProcessors это глобальная переменная и это имя присвоено IDA в соответствии с PDB-файлом, скачанным с сервера символов Microsoft.
Байт берется из var_C20. И var_C58 передается в
NtQuerySystemInformation() как указатель на принимающий буфер. Разница
между 0xC20 и 0xC58 это 0x38 (56). Посмотрим на формат структуры, который
можно найти в MSDN:
typedef struct _SYSTEM_BASIC_INFORMATION {
BYTE Reserved1[24];
PVOID Reserved2[4];
CCHAR NumberOfProcessors;
} SYSTEM_BASIC_INFORMATION;

Это система x64, так что каждый PVOID занимает здесь 8 байт.
Так что все reserved-поля занимают 24 + 4 ∗ 8 = 56.
О да, это значит, что var_C20 в локальном стеке это именно поле NumberOfProcessors
структуры SYSTEM_BASIC_INFORMATION.
Проверим нашу догадку. Скопируем taskmgr.exe из C:\Windows\System32 в
какую-нибудь другую папку (чтобы Windows Resource Protection не пыталась
восстанавливать измененный taskmgr.exe).
Откроем его в Hiew и найдем это место:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1039

Рис. 8.2: Hiew: найдем это место
Заменим инструкцию MOVZX на нашу.
Сделаем вид что у нас 64 ядра процессора. Добавим дополнительную инструкцию NOP (потому что наша инструкция короче чем та что там сейчас):

Рис. 8.3: Hiew: меняем инструкцию
И это работает! Конечно же, данные в графиках неправильные. Иногда, Task
Manager даже показывает общую загрузку CPU более 100%.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1040

Рис. 8.4: Обманутый Windows Task Manager
Самое большое число, при котором Task Manager не падает, это 64.
Должно быть, Task Manager в Windows Vista не тестировался на компьютерах
с большим количеством ядер.
И, наверное, там есть внутри какие-то статичные структуры данных, ограниченные до 64-х ядер.

8.2.1. Использование LEA для загрузки значений
Иногда, LEA используется в taskmgr.exe вместо MOV для установки первого аргумента
NtQuerySystemInformation():
Листинг 8.4: taskmgr.exe (Windows Vista)
xor
div
lea
lea
mov
mov

r9d, r9d
dword ptr [rsp+4C8h+WndClass.lpfnWndProc]
rdx, [rsp+4C8h+VersionInformation]
ecx, [r9+2]
; put 2 to ECX
r8d, 138h
ebx, eax

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1041
; ECX=SystemPerformanceInformation
call
cs:__imp_NtQuerySystemInformation ; 2
...
mov
r8d, 30h
lea
r9, [rsp+298h+var_268]
lea
rdx, [rsp+298h+var_258]
lea
ecx, [r8−2Dh]
; put 3 to ECX
; ECX=SystemTimeOfDayInformation
call
cs:__imp_NtQuerySystemInformation ; not zero
...
mov
rbp, [rsi+8]
mov
r8d, 20h
lea
r9, [rsp+98h+arg_0]
lea
rdx, [rsp+98h+var_78]
lea
ecx, [r8+2Fh]
; put 0x4F to ECX
mov
[rsp+98h+var_60], ebx
mov
[rsp+98h+var_68], rbp
; ECX=SystemSuperfetchInformation
call
cs:__imp_NtQuerySystemInformation ; not zero

Должно быть, MSVC сделал так, потому что код инструкции LEA короче чем MOV
REG, 5 (было бы 5 байт вместо 4).
LEA со смещением в пределах −128..127 (смещение будет занимать 1 байт в опкоде) с 32-битными регистрами даже еще короче (из-за отсутствия REX-префикса)
— 3 байта.
Еще один пример подобного: 6.1.5 (стр. 962).

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1042

8.3. Шутка с игрой Color Lines
Это очень популярная игра с большим количеством реализаций. Возьмем одну
из них, с названием BallTriX, от 1997, доступную бесплатно на https://archive.
org/details/BallTriX_1020 3 . Вот как она выглядит:

Рис. 8.5: Обычный вид игры

3 Или
на https://web.archive.org/web/20141110053442/http://www.download-central.ws/
Win32/Games/B/BallTriX/ или http://www.benya.com/balltrix/.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1043
Посмотрим, сможем ли мы найти генератор псевдослучайных чисел и и сделать с ним одну шутку.
IDA быстро распознает стандартную функцию _rand в balltrix.exe по адресу 0x00403DA0. IDA также показывает, что она вызывается только из одного
места:
.text:00402C9C
CODE XREF:
.text:00402C9C
.text:00402C9C
.text:00402C9C
.text:00402C9C
.text:00402C9C
.text:00402C9D
.text:00402C9F
.text:00402CA0
.text:00402CA1
.text:00402CA2
.text:00402CA7
.text:00402CAE
.text:00402CB4
.text:00402CB9
.text:00402CBA
.text:00402CBC
.text:00402CC2
.text:00402CC7
.text:00402CC8
.text:00402CCB
.text:00402CD1
.text:00402CD6
.text:00402CDB
.text:00402CDC
.text:00402CDD
.text:00402CDE
.text:00402CDF
.text:00402CDF

sub_402C9C
sub_402ACA+52

proc near

;
; sub_402ACA+64 ...

arg_0

= dword ptr

sub_402C9C

push
mov
push
push
push
mov
imul
add
mov
cdq
idiv
mov
call
cdq
idiv
mov
mov
jmp
pop
pop
pop
leave
retn
endp

ebp
ebp,
ebx
esi
edi
eax,
eax,
eax,
ecx,

8

esp

dword_40D430
dword_40D440
dword_40D5C8
32000

ecx
dword_40D440, edx
_rand
[ebp+arg_0]
dword_40D430, edx
eax, dword_40D430
$+5
edi
esi
ebx

Назовем её «random». Пока не будем концентрироваться на самом коде функции.
Эта функция вызывается из трех мест.
Вот первые два:
.text:00402B16
.text:00402B1B
.text:00402B1C
.text:00402B21
.text:00402B24
.text:00402B25
.text:00402B28
.text:00402B2D
.text:00402B2E
.text:00402B33

mov
push
call
add
inc
mov
mov
push
call
add

eax, dword_40C03C ; 10 here
eax
random
esp, 4
eax
[ebp+var_C], eax
eax, dword_40C040 ; 10 here
eax
random
esp, 4

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1044
Вот третье:
.text:00402BBB
.text:00402BC0
.text:00402BC1
.text:00402BC6
.text:00402BC9

mov
push
call
add
inc

eax, dword_40C058 ; 5 here
eax
random
esp, 4
eax

Так что у функции только один аргумент. 10 передается в первых двух случаях
и 5 в третьем.
Мы также можем заметить, что размер доски 10*10 и здесь 5 возможных цветов. Это оно! Стандартная функция rand() возвращает число в пределах 0..0x7FFF
и это неудобно, так что многие программисты пишут свою функцию, возвращающую случайное число в некоторых заданных пределах. В нашем случае,
предел это 0..n − 1 и n передается как единственный аргумент в функцию. Мы
можем быстро проверить это в отладчике.
Сделаем так, чтобы третий вызов функции всегда возвращал ноль. В начале
заменим три инструкции (PUSH/CALL/ADD) на NOPs. Затем добавим инструкцию
XOR EAX, EAX, для очистки регистра EAX.
.00402BB8:
.00402BBB:
.00402BC0:
.00402BC2:
.00402BC3:
.00402BC4:
.00402BC5:
.00402BC6:
.00402BC7:
.00402BC8:
.00402BC9:
.00402BCA:
.00402BCD:
.00402BD0:

83C410
A158C04000
31C0
90
90
90
90
90
90
90
40
8B4DF8
8D0C49
8B15F4D54000

add
mov
xor
nop
nop
nop
nop
nop
nop
nop
inc
mov
lea
mov

esp,010
eax,[00040C058]
eax,eax

eax
ecx,[ebp][−8]
ecx,[ecx][ecx]∗2
edx,[00040D5F4]

Что мы сделали, это заменили вызов функции random() на код, всегда возвращающий ноль.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1045
Теперь запустим:

Рис. 8.6: Шутка сработала
О да, это работает4 .
Но почему аргументы функции random() это глобальные переменные? Это просто потому что в настройках игры можно изменять размер доски, так что эти
параметры не фиксированы. 10 и 5 это просто значения по умолчанию.

8.4. Сапёр (Windows XP)
Для тех, кто не очень хорошо играет в Сапёра (Minesweeper), можно попробовать найти все скрытые мины в отладчике.
Как мы знаем, Сапёр располагает мины случайным образом, так что там должен быть генератор случайных чисел или вызов стандартной функции Си rand().
Вот что хорошо в реверсинге продуктов от Microsoft, так это то что часто есть
PDB-файл со всеми символами (имена функций, и т.д.).
Когда мы загружаем winmine.exe в IDA, она скачивает PDB файл именно для
этого исполняемого файла и добавляет все имена.
И вот оно, только один вызов rand() в этой функции:
.text:01003940 ; __stdcall Rnd(x)
.text:01003940 _Rnd@4
proc near
StartGame()+53
.text:01003940
.text:01003940

; CODE XREF:
; StartGame()+61

4 Автор этой книги однажды сделал это как шутку для его сотрудников, в надежде что они
перестанут играть. Надежды не оправдались.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1046
.text:01003940 arg_0
.text:01003940
.text:01003940
.text:01003946
.text:01003947
.text:0100394B
.text:0100394D
.text:0100394D _Rnd@4

= dword ptr
call
cdq
idiv
mov
retn
endp

4

ds:__imp__rand
[esp+arg_0]
eax, edx
4

Так её назвала IDA и это было имя данное ей разработчиками Сапёра.
Функция очень простая:
int Rnd(int limit)
{
return rand() % limit;
};

(В PDB-файле не было имени «limit»; это мы назвали этот аргумент так, вручную.)
Так что она возвращает случайное число в пределах от нуля до заданного предела.
Rnd() вызывается только из одного места, это функция с названием StartGame(),
и как видно, это именно тот код, что расставляет мины:
.text:010036C7
.text:010036CD
.text:010036D2
.text:010036D8
.text:010036DA
.text:010036DB
.text:010036E0
.text:010036E1
.text:010036E3
.text:010036E6
.text:010036EE
.text:010036F0
.text:010036F3
.text:010036FA
.text:010036FD
.text:01003703

push
call
push
mov
inc
call
inc
mov
shl
test
jnz
shl
lea
or
dec
jnz

_xBoxMac
_Rnd@4
; Rnd(x)
_yBoxMac
esi, eax
esi
_Rnd@4
; Rnd(x)
eax
ecx, eax
ecx, 5
; ECX=ECX*32
_rgBlk[ecx+esi], 80h
short loc_10036C7
eax, 5
; EAX=EAX*32
eax, _rgBlk[eax+esi]
byte ptr [eax], 80h
_cBombStart
short loc_10036C7

Сапёр позволяет задать размеры доски, так что X (xBoxMac) и Y (yBoxMac) это
глобальные переменные.
Они передаются в Rnd() и генерируются случайные координаты. Мина устанавливается инструкцией OR на 0x010036FA. И если она уже была установлена до
этого (это возможно, если пара функций Rnd() сгенерирует пару, которая уже
была сгенерирована), тогда TEST и JNZ на 0x010036E6 перейдет на повторную
генерацию пары.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1047
cBombStart это глобальная переменная, содержащая количество мин. Так что
это цикл.
Ширина двухмерного массива это 32 (мы можем это вывести, глядя на инструкцию SHL, которая умножает одну из координат на 32).
Размер глобального массива rgBlk можно легко узнать по разнице между меткой rgBlk в сегменте данных и следующей известной меткой. Это 0x360 (864):
.data:01005340 _rgBlk
MainWndProc(x,x,x,x)+574
.data:01005340
.data:010056A0 _Preferences
FixMenus()+2
...

db 360h dup(?)

; DATA XREF:

dd ?

; DisplayBlk(x,x)+23
; DATA XREF:

864/32 = 27.
Так что размер массива 27∗32? Это близко к тому что мы знаем: если попытаемся установить размер доски в установках Сапёра на 100 ∗ 100, то он установит
размер 24∗30. Так что это максимальный размер доски здесь. И размер массива
фиксирован для доски любого размера.
Посмотрим на всё это в OllyDbg. Запустим Сапёр, присоединим (attach) OllyDbg
к нему и увидим содержимое памяти по адресу где массив rgBlk (0x01005340)
5

.

Так что у нас выходит такой дамп памяти массива:
Address
01005340
01005350
01005360
01005370
01005380
01005390
010053A0
010053B0
010053C0
010053D0
010053E0
010053F0
01005400
01005410
01005420
01005430
01005440
01005450
01005460
01005470
01005480
01005490

Hex dump
10 10 10
0F 0F 0F
10 0F 0F
0F 0F 0F
10 0F 0F
0F 0F 0F
10 0F 0F
0F 0F 0F
10 0F 0F
0F 0F 0F
10 0F 0F
0F 0F 0F
10 0F 0F
0F 0F 0F
10 8F 0F
0F 0F 0F
10 8F 0F
0F 0F 0F
10 0F 0F
0F 0F 0F
10 10 10
0F 0F 0F

10|10
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
8F|0F
0F|0F
0F|8F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
10|10
0F|0F

10
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
8F
0F
10
0F

10
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
8F
0F
0F
0F
8F
0F
0F
0F
10
0F

10|10
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|8F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
10|10
0F|0F

10
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
8F
0F
8F
0F
10
0F

10
0F
10
0F
10
0F
10
0F
10
0F
10
0F
10
0F
10
0F
10
0F
10
0F
10
0F

0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F
0F|0F

0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F

0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F
0F

0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|
0F|

5 Все адреса здесь для Сапёра под Windows XP SP3 English. Они могут отличаться для других
сервис-паков.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1048
010054A0
010054B0
010054C0

0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

OllyDbg, как и любой другой шестнадцатеричный редактор, показывает 16
байт на строку. Так что каждая 32-байтная строка массива занимает ровно
2 строки.
Это уровень для начинающих (доска 9*9).
Тут еще какая-то квадратная структура, заметная визуально (байты 0x10).
Нажмем «Run» в OllyDbg чтобы разморозить процесс Сапёра, потом нажмем в
случайное место окна Сапёра, попадаемся на мине, но теперь видны все мины:

Рис. 8.7: Мины
Сравнивая места с минами и дамп, мы можем обнаружить что 0x10 это граница,
0x0F — пустой блок, 0x8F — мина. Вероятно 0x10 это т.н., sentinel value.
Теперь добавим комментариев и также заключим все байты 0x8F в квадратные
скобки:
border:
01005340
01005350
line #1:
01005360
01005370
line #2:
01005380
01005390
line #3:
010053A0
010053B0
line #4:

10 10 10 10 10 10 10 10 10 10 10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
10 0F 0F 0F 0F 0F 0F 0F[8F]0F 10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1049
010053C0
010053D0
line #5:
010053E0
010053F0
line #6:
01005400
01005410
line #7:
01005420
01005430
line #8:
01005440
01005450
line #9:
01005460
01005470
border:
01005480
01005490

10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
10 0F 0F[8F]0F 0F[8F]0F 0F 0F 10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
10[8F]0F 0F[8F]0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
10[8F]0F 0F 0F 0F[8F]0F 0F[8F]10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
10 0F 0F 0F 0F[8F]0F 0F 0F[8F]10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
10 10 10 10 10 10 10 10 10 10 10 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

Теперь уберем все байты связанные с границами (0x10) и всё что за ними:
0F 0F 0F 0F 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F[8F]0F
0F 0F 0F 0F 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F
0F 0F[8F]0F 0F[8F]0F 0F 0F
[8F]0F 0F[8F]0F 0F 0F 0F 0F
[8F]0F 0F 0F 0F[8F]0F 0F[8F]
0F 0F 0F 0F[8F]0F 0F 0F[8F]

Да, это всё мины, теперь это очень хорошо видно, в сравнении со скриншотом.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1050
Вот что интересно, это то что мы можем модифицировать массив прямо в
OllyDbg.
Уберем все мины заменив все байты 0x8F на 0x0F, и вот что получится в Сапёре:

Рис. 8.8: Все мины убраны в отладчике
Также уберем их все и добавим их в первом ряду:

Рис. 8.9: Мины, установленные в отладчике
Отладчик не очень удобен для подсматривания (а это была наша изначальная
цель), так что напишем маленькую утилиту для показа содержимого доски:
// Windows XP MineSweeper cheater
// written by dennis(a)yurichev.com for http://beginners.re/ book
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1051
#include
#include
#include
int main (int argc, char ∗ argv[])
{
int i, j;
HANDLE h;
DWORD PID, address, rd;
BYTE board[27][32];
if (argc!=3)
{
printf ("Usage: %s \n", argv[0]);
return 0;
};
assert (argv[1]!=NULL);
assert (argv[2]!=NULL);
assert (sscanf (argv[1], "%d", &PID)==1);
assert (sscanf (argv[2], "%x", &address)==1);
h=OpenProcess (PROCESS_VM_OPERATION | PROCESS_VM_READ | ⤦
Ç PROCESS_VM_WRITE, FALSE, PID);
if (h==NULL)
{
DWORD e=GetLastError();
printf ("OpenProcess error: %08X\n", e);
return 0;
};
if (ReadProcessMemory (h, (LPVOID)address, board, sizeof(board), &⤦
Ç rd)!=TRUE)
{
printf ("ReadProcessMemory() failed\n");
return 0;
};
for (i=1; iclockRadius, scale, 100);
lppt = rCircleTable + pos;
SetROP2(hDC, patMode);
SelectObject(hDC, hPen);
LineTo( hDC,
np−>clockCenter.x + MulDiv(lppt−>x, radius, 8000),
np−>clockCenter.y + MulDiv(lppt−>y, radius, 8000) );
}

Теперь всё ясно: координаты были предвычислены, как если бы циферблат
был размером 2 ⋅ 8000, а затем он масштабируется до радиуса текущего циферблата используя ф-цию MulDiv().

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1063
Структура POINT10 это структура из двух 32-битных значений, первое это x,
второе это y.

8.6. Донглы
Автор этих строк иногда делал замену донглам или «эмуляторы донглов» и
здесь немного примеров, как это происходит.
Об одном неописанном здесь случае с Rockey и Z3 вы также можете прочитать
здесь : http://yurichev.com/tmp/SAT_SMT_DRAFT.pdf.

8.6.1. Пример #1: MacOS Classic и PowerPC
Вот пример программы для MacOS Classic 11 , для PowerPC. Компания, разработавшая этот продукт, давно исчезла, так что (легальный) пользователь боялся
того что донгла может сломаться.
Если запустить программу без подключенной донглы, можно увидеть окно с
надписью
”Invalid Security Device”. Мне повезло потому что этот текст можно было легко
найти внутри исполняемого файла.
Представим, что мы не знакомы ни с Mac OS Classic, ни с PowerPC, но всё-таки
попробуем.
IDA открывает исполняемый файл легко, показывая его тип как
”PEF (Mac OS or Be OS executable)” (действительно, это стандартный тип файлов в Mac OS Classic).
В поисках текстовой строки с сообщением об ошибке, мы попадаем на этот
фрагмент кода:
...
seg000:000C87FC
seg000:000C8800
seg000:000C8804
seg000:000C8808
seg000:000C880C
seg000:000C8810

38
48
60
54
40
80

60
03
00
60
82
62

00
93
00
06
00
9F

01
41
00
3F
40
D8

li
bl
nop
clrlwi.
bne
lwz

%r3, 1
check1
%r0, %r3, 24
OK
%r3, TC_aInvalidSecurityDevice

...

Да, это код PowerPC. Это очень типичный процессор для RISC 1990-х. Каждая
инструкция занимает 4 байта (как и в MIPS и ARM) и их имена немного похожи
на имена инструкций MIPS.
10 https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx
11 MacOS

перед тем как перейти на UNIX

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1064
check1() это имя которое мы дадим этой функции немного позже. BL это инструкция Branch Link т.е. предназначенная для вызова подпрограмм. Самое
важное место — это инструкция BNE, срабатывающая, если проверка наличия
донглы прошла успешно, либо не срабатывающая в случае ошибки: и тогда
адрес текстовой строки с сообщением об ошибке будет загружен в регистр r3
для последующей передачи в функцию отображения диалогового окна.
Из [Steve Zucker, SunSoft and Kari Karhi, IBM, SYSTEM V APPLICATION BINARY INTERFACE:
PowerPC Processor Supplement, (1995)]12 мы узнаем, что регистр r3 используется
для возврата значений (и еще r4 если значение 64-битное).
Еще одна, пока что неизвестная инструкция CLRLWI. Из [PowerPC(tm) Microprocessor
Family: The Programming Environments for 32-Bit Microprocessors, (2000)]13 мы
узнаем, что эта инструкция одновременно и очищает и загружает. В нашем
случае, она очищает 24 старших бита из значения в r3 и записывает всё это в
r0, так что это аналог MOVZX в x86 (1.23.1 (стр. 263)), но также устанавливает
флаги, так что BNE может проверить их потом.
Посмотрим внутрь check1():
seg000:00101B40
seg000:00101B40
seg000:00101B40
seg000:00101B40
seg000:00101B40
seg000:00101B40
seg000:00101B44
seg000:00101B48
seg000:00101B4C
seg000:00101B50
seg000:00101B54
seg000:00101B58
seg000:00101B5C
seg000:00101B60
seg000:00101B60

check1: # CODE XREF: seg000:00063E7Cp
# sub_64070+160p ...
.set arg_8,
7C
90
94
48
60
80
38
7C
4E

08
01
21
01
00
01
21
08
80

02
00
FF
6B
00
00
00
03
00

A6
08
C0
39
00
48
40
A6
20

8

mflr
%r0
stw
%r0, arg_8(%sp)
stwu
%sp, −0x40(%sp)
bl
check2
nop
lwz
%r0, 0x40+arg_8(%sp)
addi
%sp, %sp, 0x40
mtlr
%r0
blr
# End of function check1

Как можно увидеть в IDA, эта функция вызывается из многих мест в программе,
но только значение в регистре r3 проверяется сразу после каждого вызова.
Всё что эта функция делает это только вызывает другую функцию, так что
это thunk function: здесь присутствует и пролог функции и эпилог, но регистр
r3 не трогается, так что checkl() возвращает то, что возвращает check2().
BLR14 это похоже возврат из функции, но так как IDA делает всю разметку
функций автоматически, наверное, мы можем пока не интересоваться этим.
Так как это типичный RISC, похоже, подпрограммы вызываются, используя link
register, точно как в ARM.
Функция check2() более сложная:
seg000:00118684

check2: # CODE XREF: check1+Cp

12 Также

доступно здесь: http://yurichev.com/mirrors/PowerPC/elfspec_ppc.pdf
доступно здесь: http://yurichev.com/mirrors/PowerPC/6xx_pem.pdf
14 (PowerPC) Branch to Link Register
13 Также

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1065
seg000:00118684
seg000:00118684
seg000:00118684
seg000:00118684
seg000:00118684
seg000:00118684
seg000:00118684
seg000:00118684
seg000:00118688
seg000:0011868C
seg000:00118690
seg000:00118690
seg000:00118694
seg000:00118698
seg000:0011869C
seg000:001186A0
seg000:001186A4
seg000:001186A8
seg000:001186AC
seg000:001186B0
seg000:001186B4
seg000:001186B8
seg000:001186B8
seg000:001186B8
seg000:001186BC
seg000:001186C0
seg000:001186C4
seg000:001186C4
seg000:001186C4
seg000:001186C8
seg000:001186CC
seg000:001186D0
seg000:001186D4
seg000:001186D8
seg000:001186DC
seg000:001186E0
seg000:001186E0
seg000:001186E0
seg000:001186E4
seg000:001186E8
seg000:001186EC
seg000:001186F0
seg000:001186F4
seg000:001186F8
seg000:001186F8
seg000:001186F8
seg000:001186FC
seg000:00118700
seg000:00118704
seg000:00118708
seg000:00118708
seg000:00118708
seg000:0011870C

.set
.set
.set
.set
.set
93 E1 FF FC
7C 08 02 A6
83 E2 95 A8
93
93
7C
90
54
28
94
40
38
48

C1
A1
7D
01
60
00
21
82
60
00

FF
FF
1B
00
06
00
FF
00
00
00

F8
F4
78
08
3E
01
B0
0C
01
6C

var_18, −0x18
var_C, −0xC
var_8, −8
var_4, −4
arg_8, 8

stw
%r31, var_4(%sp)
mflr
%r0
lwz
%r31, off_1485E8 # dword_24B704
.using dword_24B704, %r31
stw
%r30, var_8(%sp)
stw
%r29, var_C(%sp)
mr
%r29, %r3
stw
%r0, arg_8(%sp)
clrlwi %r0, %r3, 24
cmplwi %r0, 1
stwu
%sp, −0x50(%sp)
bne
loc_1186B8
li
%r3, 1
b
exit

48 00 03 D5
60 00 00 00
3B C0 00 00

loc_1186B8: # CODE XREF: check2+28j
bl
sub_118A8C
nop
li
%r30, 0

57
41
38
80
48
60
48

C0
82
61
9F
00
00
00

06
00
00
00
C0
00
00

3F
18
38
00
55
00
1C

skip:
clrlwi.
beq
addi
lwz
bl
nop
b

80
38
38
48
60
3B

BF
81
60
00
00
C0

00
00
08
BF
00
00

00
38
C2
99
00
01

loc_1186E0: # CODE XREF: check2+44j
lwz
%r5, dword_24B704
addi
%r4, %sp, 0x50+var_18
li
%r3, 0x1234
bl
.RBEFINDFIRST
nop
li
%r30, 1

54
41
38
48

60
82
60
00

04
00
00
00

3F
0C
00
1C

loc_1186F8: # CODE XREF: check2+58j
clrlwi. %r0, %r3, 16
beq
must_jump
li
%r3, 0
# error
b
exit

7F A3 EB 78
48 00 00 31

must_jump: # CODE XREF: check2+78j
mr
%r3, %r29
bl
check3

# CODE XREF: check2+94j
%r0, %r30, 24
loc_1186E0
%r3, %sp, 0x50+var_18
%r4, dword_24B704
.RBEFINDNEXT
loc_1186F8

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1066
seg000:00118710
seg000:00118714
seg000:00118718
seg000:0011871C
seg000:00118720
seg000:00118720
seg000:00118720
seg000:00118720
seg000:00118724
seg000:00118728
seg000:0011872C
seg000:00118730
seg000:00118734
seg000:00118738
seg000:00118738

60
54
41
38

00
60
82
60

00
06
FF
00

00
3F
AC
01

nop
clrlwi. %r0, %r3, 24
beq
skip
li
%r3, 1
exit:

80
38
83
7C
83
83
4E

01
21
E1
08
C1
A1
80

00
00
FF
03
FF
FF
00

58
50
FC
A6
F8
F4
20

# CODE XREF: check2+30j
# check2+80j
%r0, 0x50+arg_8(%sp)
%sp, %sp, 0x50
%r31, var_4(%sp)
%r0
%r30, var_8(%sp)
%r29, var_C(%sp)

lwz
addi
lwz
mtlr
lwz
lwz
blr
# End of function check2

Снова повезло: имена некоторых функций оставлены в исполняемом файле (в
символах в отладочной секции? Трудно сказать до тех пор, пока мы не знакомы
с этим форматом файлов, может быть это что-то вроде PE-экспортов (6.5.2))?
Как например .RBEFINDNEXT() and .RBEFINDFIRST().
В итоге, эти функции вызывают другие функции с именами вроде .GetNextDeviceViaUSB(),
.USBSendPKT(), так что они явно работают с каким-то USB-устройством.
Тут даже есть функция с названием .GetNextEve3Device() — звучит знакомо,
в 1990-х годах была донгла Sentinel Eve3 для ADB-порта (присутствующих на
Макинтошах).
В начале посмотрим на то как устанавливается регистр r3 одновременно игнорируя всё остальное. Мы знаем, что «хорошее» значение в r3 должно быть не
нулевым, а нулевой r3 приведет к выводу диалогового окна с сообщением об
ошибке.
В функции имеются две инструкции li %r3, 1 и одна li %r3, 0 (Load Immediate,
т.е. загрузить значение в регистр). Самая первая инструкция находится на
0x001186B0 — и честно говоря, трудно заранее понять, что это означает.
А вот то что мы видим дальше понять проще: вызывается .RBEFINDFIRST() и
в случае ошибки, 0 будет записан в r3 и мы перейдем на exit, а иначе будет
вызвана функция check3() — если и она будет выполнена с ошибкой, будет
вызвана .RBEFINDNEXT() вероятно, для поиска другого USB-устройства.
N.B.: clrlwi. %r0, %r3, 16 это аналог того что мы уже видели, но она очищает 16 старших бит, т.е.,
.RBEFINDFIRST() вероятно возвращает 16-битное значение.
B (означает branch) — безусловный переход.
BEQ это обратная инструкция от BNE.
Посмотрим на check3():
seg000:0011873C
seg000:0011873C
seg000:0011873C

check3: # CODE XREF: check2+88p
.set var_18, −0x18

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1067
seg000:0011873C
seg000:0011873C
seg000:0011873C
seg000:0011873C
seg000:0011873C
seg000:0011873C
seg000:00118740
seg000:00118744
seg000:00118748
seg000:0011874C
seg000:00118750
seg000:00118750
seg000:00118754
seg000:00118758
seg000:0011875C
seg000:00118760
seg000:00118764
seg000:00118768
seg000:0011876C
seg000:00118770
seg000:00118774
seg000:00118778
seg000:0011877C
seg000:00118780
seg000:00118784
seg000:00118784
seg000:00118784
seg000:00118788
seg000:0011878C
seg000:00118790
seg000:00118794
seg000:00118798
seg000:00118798
seg000:00118798
seg000:0011879C
seg000:001187A0
seg000:001187A4
seg000:001187A8
seg000:001187AC
seg000:001187B0
seg000:001187B4
seg000:001187B8
seg000:001187BC
seg000:001187C0
seg000:001187C0
seg000:001187C0
seg000:001187C4
seg000:001187C8
seg000:001187CC
seg000:001187D0
seg000:001187D4
seg000:001187D4
seg000:001187D4

.set
.set
.set
.set

var_C, −0xC
var_8, −8
var_4, −4
arg_8, 8

93
7C
38
93
83

E1
08
A0
C1
C2

FF
02
00
FF
95

FC
A6
00
F8
A8

stw
%r31, var_4(%sp)
mflr
%r0
li
%r5, 0
stw
%r30, var_8(%sp)
lwz
%r30, off_1485E8 # dword_24B704
.using dword_24B704, %r30
stw
%r29, var_C(%sp)
addi
%r29, %r3, 0
li
%r3, 0
stw
%r0, arg_8(%sp)
stwu
%sp, −0x50(%sp)
lwz
%r6, dword_24B704
addi
%r4, %sp, 0x50+var_18
bl
.RBEREAD
nop
clrlwi. %r0, %r3, 16
beq
loc_118784
li
%r3, 0
b
exit

93
3B
38
90
94
80
38
48
60
54
41
38
48

A1
A3
60
01
21
DE
81
00
00
60
82
60
00

FF
00
00
00
FF
00
00
C0
00
04
00
00
02

F4
00
00
08
B0
00
38
5D
00
3F
0C
00
F0

A0
28
41
38
48

01
00
82
60
00

00
04
00
00
02

38
B2
0C
00
DC

loc_118784: # CODE XREF: check3+3Cj
lhz
%r0, 0x50+var_18(%sp)
cmplwi %r0, 0x1100
beq
loc_118798
li
%r3, 0
b
exit

80
38
38
38
48
60
54
41
38
48

DE
81
60
A0
00
00
60
82
60
00

00
00
00
00
C0
00
04
00
00
02

00
38
01
00
21
00
3F
0C
00
B4

loc_118798: # CODE XREF: check3+50j
lwz
%r6, dword_24B704
addi
%r4, %sp, 0x50+var_18
li
%r3, 1
li
%r5, 0
bl
.RBEREAD
nop
clrlwi. %r0, %r3, 16
beq
loc_1187C0
li
%r3, 0
b
exit

A0
28
41
38
48

01
00
82
60
00

00
06
00
00
02

38
4B
0C
00
A0

loc_1187C0: # CODE XREF: check3+78j
lhz
%r0, 0x50+var_18(%sp)
cmplwi %r0, 0x09AB
beq
loc_1187D4
li
%r3, 0
b
exit

4B F9 F3 D9

loc_1187D4: # CODE XREF: check3+8Cj
bl
sub_B7BAC

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1068
seg000:001187D8
seg000:001187DC
seg000:001187E0
seg000:001187E4
seg000:001187E8
seg000:001187EC
seg000:001187F0
seg000:001187F4
seg000:001187F8
seg000:001187F8
seg000:001187F8
seg000:001187FC
seg000:00118800
seg000:00118804
seg000:00118804
seg000:00118804
seg000:00118808
seg000:0011880C
seg000:00118810
seg000:00118814
seg000:00118818
seg000:0011881C
seg000:00118820
seg000:00118824
seg000:00118828
seg000:0011882C
seg000:0011882C
seg000:0011882C
seg000:00118830
seg000:00118834
seg000:00118838
seg000:0011883C
seg000:00118840
seg000:00118840
seg000:00118840
seg000:00118844
seg000:00118848
seg000:00118848
seg000:00118848
seg000:0011884C
seg000:00118850
seg000:00118854
seg000:00118858
seg000:0011885C
seg000:00118860
seg000:00118864
seg000:00118868
seg000:0011886C
seg000:00118870
seg000:00118870
seg000:00118870
seg000:00118874
seg000:00118878

60
54
2C
41
40
2C
40
48

00
60
00
82
80
00
80
00

00
06
00
01
00
00
00
01

00
3E
05
00
10
04
58
8C

nop
clrlwi
cmpwi
beq
bge
cmpwi
bge
b

%r0, %r3, 24
%r0, 5
loc_1188E4
loc_1187F8
%r0, 4
loc_118848
loc_118980

2C 00 00 0B
41 82 00 08
48 00 01 80

loc_1187F8: # CODE XREF: check3+ACj
cmpwi
%r0, 0xB
beq
loc_118804
b
loc_118980

80
38
38
38
48
60
54
41
38
48

DE
81
60
A0
00
00
60
82
60
00

00
00
00
00
BF
00
04
00
00
02

00
38
08
00
B5
00
3F
0C
00
48

loc_118804: # CODE XREF: check3+C0j
lwz
%r6, dword_24B704
addi
%r4, %sp, 0x50+var_18
li
%r3, 8
li
%r5, 0
bl
.RBEREAD
nop
clrlwi. %r0, %r3, 16
beq
loc_11882C
li
%r3, 0
b
exit

A0
28
41
38
48

01
00
82
60
00

00
11
00
00
02

38
30
0C
00
34

loc_11882C: # CODE XREF: check3+E4j
lhz
%r0, 0x50+var_18(%sp)
cmplwi %r0, 0xFEA0
beq
loc_118840
li
%r3, 0
b
exit

38 60 00 01
48 00 02 2C

loc_118840: # CODE XREF: check3+F8j
li
%r3, 1
b
exit

80
38
38
38
48
60
54
41
38
48

loc_118848: # CODE XREF: check3+B4j
lwz
%r6, dword_24B704
addi
%r4, %sp, 0x50+var_18
li
%r3, 0xA
li
%r5, 0
bl
.RBEREAD
nop
clrlwi. %r0, %r3, 16
beq
loc_118870
li
%r3, 0
b
exit

DE
81
60
A0
00
00
60
82
60
00

00
00
00
00
BF
00
04
00
00
02

00
38
0A
00
71
00
3F
0C
00
04

A0 01 00 38
28 00 03 F3
41 82 00 0C

loc_118870: # CODE XREF: check3+128j
lhz
%r0, 0x50+var_18(%sp)
cmplwi %r0, 0xA6E1
beq
loc_118884

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1069
seg000:0011887C
seg000:00118880
seg000:00118884
seg000:00118884
seg000:00118884
seg000:00118888
seg000:0011888C
seg000:00118890
seg000:00118894
seg000:00118898
seg000:00118898
seg000:00118898
seg000:0011889C
seg000:001188A0
seg000:001188A4
seg000:001188A8
seg000:001188AC
seg000:001188B0
seg000:001188B4
seg000:001188B8
seg000:001188BC
seg000:001188C0
seg000:001188C0
seg000:001188C0
seg000:001188C4
seg000:001188C8
seg000:001188CC
seg000:001188D0
seg000:001188D4
seg000:001188D4
seg000:001188D4
seg000:001188D8
seg000:001188DC
seg000:001188E0
seg000:001188E4
seg000:001188E4
seg000:001188E4
seg000:001188E8
seg000:001188EC
seg000:001188F0
seg000:001188F4
seg000:001188F8
seg000:001188FC
seg000:00118900
seg000:00118904
seg000:00118908
seg000:0011890C
seg000:0011890C
seg000:0011890C
seg000:00118910
seg000:00118914
seg000:00118918
seg000:0011891C

38 60 00 00
48 00 01 F0

li
b

%r3, 0
exit

57
28
40
38
48

BF
1F
82
60
00

06
00
00
00
01

3E
02
0C
01
DC

loc_118884: # CODE XREF: check3+13Cj
clrlwi %r31, %r29, 24
cmplwi %r31, 2
bne
loc_118898
li
%r3, 1
b
exit

80
38
38
38
48
60
54
41
38
48

DE
81
60
A0
00
00
60
82
60
00

00
00
00
00
BF
00
04
00
00
01

00
38
0B
00
21
00
3F
0C
00
B4

loc_118898: # CODE XREF: check3+150j
lwz
%r6, dword_24B704
addi
%r4, %sp, 0x50+var_18
li
%r3, 0xB
li
%r5, 0
bl
.RBEREAD
nop
clrlwi. %r0, %r3, 16
beq
loc_1188C0
li
%r3, 0
b
exit

A0
28
41
38
48

01
00
82
60
00

00
23
00
00
01

38
1C
0C
00
A0

loc_1188C0: # CODE XREF: check3+178j
lhz
%r0, 0x50+var_18(%sp)
cmplwi %r0, 0x1C20
beq
loc_1188D4
li
%r3, 0
b
exit

28
40
38
48

1F
82
60
00

00
01
00
01

03
94
01
90

loc_1188D4: # CODE XREF: check3+18Cj
cmplwi %r31, 3
bne
error
li
%r3, 1
b
exit

80
38
38
38
48
60
54
41
38
48

DE
81
60
A0
00
00
60
82
60
00

00
00
00
00
BE
00
04
00
00
01

00
38
0C
00
D5
00
3F
0C
00
68

loc_1188E4: # CODE XREF: check3+A8j
lwz
%r6, dword_24B704
addi
%r4, %sp, 0x50+var_18
li
%r3, 0xC
li
%r5, 0
bl
.RBEREAD
nop
clrlwi. %r0, %r3, 16
beq
loc_11890C
li
%r3, 0
b
exit

A0
28
41
38
48

01
00
82
60
00

00
1F
00
00
01

38
40
0C
00
54

loc_11890C: # CODE XREF: check3+1C4j
lhz
%r0, 0x50+var_18(%sp)
cmplwi %r0, 0x40FF
beq
loc_118920
li
%r3, 0
b
exit

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1070
seg000:00118920
seg000:00118920
seg000:00118920
seg000:00118924
seg000:00118928
seg000:0011892C
seg000:00118930
seg000:00118934
seg000:00118934
seg000:00118934
seg000:00118938
seg000:0011893C
seg000:00118940
seg000:00118944
seg000:00118948
seg000:0011894C
seg000:00118950
seg000:00118954
seg000:00118958
seg000:0011895C
seg000:0011895C
seg000:0011895C
seg000:00118960
seg000:00118964
seg000:00118968
seg000:0011896C
seg000:00118970
seg000:00118970
seg000:00118970
seg000:00118974
seg000:00118978
seg000:0011897C
seg000:00118980
seg000:00118980
seg000:00118980
seg000:00118980
seg000:00118984
seg000:00118988
seg000:0011898C
seg000:00118990
seg000:00118994
seg000:00118998
seg000:0011899C
seg000:001189A0
seg000:001189A4
seg000:001189A8
seg000:001189AC
seg000:001189AC
seg000:001189AC
seg000:001189B0
seg000:001189B4
seg000:001189B8
seg000:001189BC

57
28
40
38
48

BF
1F
82
60
00

06
00
00
00
01

3E
02
0C
01
40

loc_118920: # CODE XREF: check3+1D8j
clrlwi %r31, %r29, 24
cmplwi %r31, 2
bne
loc_118934
li
%r3, 1
b
exit

80
38
38
38
48
60
54
41
38
48

DE
81
60
A0
00
00
60
82
60
00

00
00
00
00
BE
00
04
00
00
01

00
38
0D
00
85
00
3F
0C
00
18

loc_118934: # CODE XREF: check3+1ECj
lwz
%r6, dword_24B704
addi
%r4, %sp, 0x50+var_18
li
%r3, 0xD
li
%r5, 0
bl
.RBEREAD
nop
clrlwi. %r0, %r3, 16
beq
loc_11895C
li
%r3, 0
b
exit

A0
28
41
38
48

01
00
82
60
00

00
07
00
00
01

38
CF
0C
00
04

loc_11895C: # CODE XREF: check3+214j
lhz
%r0, 0x50+var_18(%sp)
cmplwi %r0, 0xFC7
beq
loc_118970
li
%r3, 0
b
exit

28
40
38
48

1F
82
60
00

00
00
00
00

03
F8
01
F4

loc_118970: # CODE XREF: check3+228j
cmplwi %r31, 3
bne
error
li
%r3, 1
b
exit

80
38
3B
38
38
48
60
54
41
38
48

DE
81
E0
60
A0
00
00
60
82
60
00

00
00
00
00
00
BE
00
04
00
00
00

00
38
00
04
00
35
00
3F
0C
00
C8

loc_118980: # CODE XREF: check3+B8j
# check3+C4j
lwz
%r6, dword_24B704
addi
%r4, %sp, 0x50+var_18
li
%r31, 0
li
%r3, 4
li
%r5, 0
bl
.RBEREAD
nop
clrlwi. %r0, %r3, 16
beq
loc_1189AC
li
%r3, 0
b
exit

A0
28
40
3B
48

01
00
82
E0
00

00
1D
00
00
00

38
6A
0C
01
14

loc_1189AC: # CODE XREF: check3+264j
lhz
%r0, 0x50+var_18(%sp)
cmplwi %r0, 0xAED0
bne
loc_1189C0
li
%r31, 1
b
loc_1189D0

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1071
seg000:001189C0
seg000:001189C0
seg000:001189C0
seg000:001189C4
seg000:001189C8
seg000:001189CC
seg000:001189D0
seg000:001189D0
seg000:001189D0
seg000:001189D0
seg000:001189D4
seg000:001189D8
seg000:001189DC
seg000:001189E0
seg000:001189E4
seg000:001189E8
seg000:001189EC
seg000:001189F0
seg000:001189F0
seg000:001189F0
seg000:001189F4
seg000:001189F8
seg000:001189F8
seg000:001189F8
seg000:001189FC
seg000:00118A00
seg000:00118A04
seg000:00118A08
seg000:00118A0C
seg000:00118A10
seg000:00118A14
seg000:00118A18
seg000:00118A1C
seg000:00118A20
seg000:00118A20
seg000:00118A20
seg000:00118A24
seg000:00118A28
seg000:00118A2C
seg000:00118A30
seg000:00118A34
seg000:00118A34
seg000:00118A34
seg000:00118A38
seg000:00118A3C
seg000:00118A40
seg000:00118A44
seg000:00118A44
seg000:00118A44
seg000:00118A44
seg000:00118A48
seg000:00118A4C
seg000:00118A50

28
41
38
48

57
28
40
57
41
48
60
48

00
82
60
00

A0
00
82
E0
82
00
00
00

18
00
00
00

06
00
00
06
00
4C
00
00

28
0C
00
A4

loc_1189C0: # CODE XREF: check3+278j
cmplwi %r0, 0x2818
beq
loc_1189D0
li
%r3, 0
b
exit

3E
02
20
3F
10
69
00
84

loc_1189D0: # CODE XREF: check3+280j
# check3+288j
clrlwi %r0, %r29, 24
cmplwi %r0, 2
bne
loc_1189F8
clrlwi. %r0, %r31, 24
beq
good2
bl
sub_11D64C
nop
b
exit

38 60 00 01
48 00 00 7C

good2:
li
b

# CODE XREF: check3+2A4j
%r3, 1
exit

80
38
38
38
48
60
54
41
38
48

DE
81
60
A0
00
00
60
82
60
00

00
00
00
00
BD
00
04
00
00
00

00
38
05
00
C1
00
3F
0C
00
54

loc_1189F8: # CODE XREF: check3+29Cj
lwz
%r6, dword_24B704
addi
%r4, %sp, 0x50+var_18
li
%r3, 5
li
%r5, 0
bl
.RBEREAD
nop
clrlwi. %r0, %r3, 16
beq
loc_118A20
li
%r3, 0
b
exit

A0
28
40
3B
48

01
00
82
E0
00

00
11
00
00
00

38
D3
0C
01
14

loc_118A20: # CODE XREF: check3+2D8j
lhz
%r0, 0x50+var_18(%sp)
cmplwi %r0, 0xD300
bne
loc_118A34
li
%r31, 1
b
good1

28
41
38
48

00
82
60
00

1A
00
00
00

EB
0C
00
30

loc_118A34: # CODE XREF: check3+2ECj
cmplwi %r0, 0xEBA1
beq
good1
li
%r3, 0
b
exit
good1:

57
28
40
57

A0
00
82
E0

06
00
00
06

3E
03
20
3F

# CODE XREF: check3+2F4j
# check3+2FCj
clrlwi %r0, %r29, 24
cmplwi %r0, 3
bne
error
clrlwi. %r0, %r31, 24

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1072
seg000:00118A54
seg000:00118A58
seg000:00118A5C
seg000:00118A60
seg000:00118A64
seg000:00118A64
seg000:00118A64
seg000:00118A68
seg000:00118A6C
seg000:00118A6C
seg000:00118A6C
seg000:00118A6C
seg000:00118A70
seg000:00118A70
seg000:00118A70
seg000:00118A70
seg000:00118A74
seg000:00118A78
seg000:00118A7C
seg000:00118A80
seg000:00118A84
seg000:00118A88
seg000:00118A88

41
48
60
48

82
00
00
00

00
4B
00
00

10
F5
00
10

beq
bl
nop
b

38 60 00 01
48 00 00 08

good:
li
b

# CODE XREF: check3+318j
%r3, 1
exit

error:

# CODE XREF: check3+19Cj
# check3+238j ...
%r3, 0

38 60 00 00

li
exit:

80
38
83
7C
83
83
4E

01
21
E1
08
C1
A1
80

00
00
FF
03
FF
FF
00

58
50
FC
A6
F8
F4
20

good
sub_11D64C
exit

# CODE XREF: check3+44j
# check3+58j ...
%r0, 0x50+arg_8(%sp)
%sp, %sp, 0x50
%r31, var_4(%sp)
%r0
%r30, var_8(%sp)
%r29, var_C(%sp)

lwz
addi
lwz
mtlr
lwz
lwz
blr
# End of function check3

Здесь много вызовов .RBEREAD(). Эта функция, должно быть, читает какие-то
значения из донглы, которые потом сравниваются здесь при помощи CMPLWI.
Мы также видим в регистр r3 записывается перед каждым вызовом .RBEREAD()
одно из этих значений: 0, 1, 8, 0xA, 0xB, 0xC, 0xD, 4, 5. Вероятно адрес в памяти
или что-то в этом роде?
Да, действительно, если погуглить имена этих функций, можно легко найти
документацию к Sentinel Eve3!
Hаверное, уже и не нужно изучать остальные инструкции PowerPC: всё что делает эта функция это просто вызывает .RBEREAD(), сравнивает его результаты
с константами и возвращает 1 если результат сравнения положительный или
0 в другом случае.
Всё ясно: check1() должна всегда возвращать 1 или иное ненулевое значение. Но так как мы не очень уверены в своих знаниях инструкций PowerPC,
будем осторожны и пропатчим переходы в check2 на адресах 0x001186FC и
0x00118718.
На 0x001186FC мы записываем байты 0x48 и 0 таким образом превращая инструкцию BEQ в инструкцию B (безусловный переход): мы можем заметить этот
опкод прямо в коде даже без обращения к [PowerPC(tm) Microprocessor Family:
The Programming Environments for 32-Bit Microprocessors, (2000)]15 .
На 0x00118718 мы записываем байт 0x60 и еще 3 нулевых байта, таким образом
превращая её в инструкцию NOP: Этот опкод мы также можем подсмотреть
прямо в коде.
15 Также

доступно здесь: http://yurichev.com/mirrors/PowerPC/6xx_pem.pdf

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1073
И всё заработало без подключенной донглы.
Резюмируя, такие простые модификации можно делать в IDA даже с минимальными знаниями ассемблера.

8.6.2. Пример #2: SCO OpenServer
Древняя программа для SCO OpenServer от 1997 разработанная давно исчезнувшей компанией.
Специальный драйвер донглы инсталлируется в системе, он содержит такие
текстовые строки: «Copyright 1989, Rainbow Technologies, Inc., Irvine, CA» и «Sentinel
Integrated Driver Ver. 3.0 ».
После инсталляции драйвера, в /dev появляются такие устройства:
/dev/rbsl8
/dev/rbsl9
/dev/rbsl10

Без подключенной донглы, программа сообщает об ошибке, но сообщение об
ошибке не удается найти в исполняемых файлах.
Еще раз спасибо IDA, она легко загружает исполняемые файлы формата COFF
использующиеся в SCO OpenServer.
Попробуем также поискать строку «rbsl», и действительно, её можно найти в
таком фрагменте кода:
.text:00022AB8
.text:00022AB8
.text:00022AB8
.text:00022AB8
.text:00022AB8
.text:00022AB8
.text:00022AB8
.text:00022AB8
.text:00022AB9
.text:00022ABB
.text:00022ABE
.text:00022ABF
.text:00022AC4
.text:00022AC5
.text:00022AC8
.text:00022AC9
.text:00022ACA
.text:00022ACF
.text:00022AD2
.text:00022AD7
.text:00022ADD
.text:00022ADE
.text:00022AE1
.text:00022AE4
.text:00022AE9

SSQC

public SSQC
proc near ; CODE XREF: SSQ+7p

var_44 = byte ptr −44h
var_29 = byte ptr −29h
arg_0 = dword ptr 8
push
mov
sub
push
mov
push
mov
push
push
call
add
cmp
jnz
inc
mov
movsx
cmp
jz

ebp
ebp, esp
esp, 44h
edi
edi, offset unk_4035D0
esi
esi, [ebp+arg_0]
ebx
esi
strlen
esp, 4
eax, 2
loc_22BA4
esi
al, [esi−1]
eax, al
eax, '3'
loc_22B84

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1074
.text:00022AEF
cmp
eax, '4'
.text:00022AF4
jz
loc_22B94
.text:00022AFA
cmp
eax, '5'
.text:00022AFF
jnz
short loc_22B6B
.text:00022B01
movsx
ebx, byte ptr [esi]
.text:00022B04
sub
ebx, '0'
.text:00022B07
mov
eax, 7
.text:00022B0C
add
eax, ebx
.text:00022B0E
push
eax
.text:00022B0F
lea
eax, [ebp+var_44]
.text:00022B12
push
offset aDevSlD ; "/dev/sl%d"
.text:00022B17
push
eax
.text:00022B18
call
nl_sprintf
.text:00022B1D
push
0
; int
.text:00022B1F
push
offset aDevRbsl8 ; char *
.text:00022B24
call
_access
.text:00022B29
add
esp, 14h
.text:00022B2C
cmp
eax, 0FFFFFFFFh
.text:00022B31
jz
short loc_22B48
.text:00022B33
lea
eax, [ebx+7]
.text:00022B36
push
eax
.text:00022B37
lea
eax, [ebp+var_44]
.text:00022B3A
push
offset aDevRbslD ; "/dev/rbsl%d"
.text:00022B3F
push
eax
.text:00022B40
call
nl_sprintf
.text:00022B45
add
esp, 0Ch
.text:00022B48
.text:00022B48 loc_22B48: ; CODE XREF: SSQC+79j
.text:00022B48
mov
edx, [edi]
.text:00022B4A
test
edx, edx
.text:00022B4C
jle
short loc_22B57
.text:00022B4E
push
edx
; int
.text:00022B4F
call
_close
.text:00022B54
add
esp, 4
.text:00022B57
.text:00022B57 loc_22B57: ; CODE XREF: SSQC+94j
.text:00022B57
push
2
; int
.text:00022B59
lea
eax, [ebp+var_44]
.text:00022B5C
push
eax
; char *
.text:00022B5D
call
_open
.text:00022B62
add
esp, 8
.text:00022B65
test
eax, eax
.text:00022B67
mov
[edi], eax
.text:00022B69
jge
short loc_22B78
.text:00022B6B
.text:00022B6B loc_22B6B: ; CODE XREF: SSQC+47j
.text:00022B6B
mov
eax, 0FFFFFFFFh
.text:00022B70
pop
ebx
.text:00022B71
pop
esi
.text:00022B72
pop
edi
.text:00022B73
mov
esp, ebp
.text:00022B75
pop
ebp
.text:00022B76
retn

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1075
.text:00022B78
.text:00022B78
.text:00022B78
.text:00022B79
.text:00022B7A
.text:00022B7B
.text:00022B7D
.text:00022B7F
.text:00022B80
.text:00022B84
.text:00022B84
.text:00022B84
.text:00022B86
.text:00022B87
.text:00022B88
.text:00022B89
.text:00022B8E
.text:00022B90
.text:00022B92
.text:00022B93
.text:00022B94
.text:00022B94
.text:00022B94
.text:00022B96
.text:00022B97
.text:00022B98
.text:00022B99
.text:00022B9E
.text:00022BA0
.text:00022BA2
.text:00022BA3
.text:00022BA4
.text:00022BA4
.text:00022BA4
.text:00022BAB
.text:00022BAC
.text:00022BAD
.text:00022BB4
.text:00022BB5
.text:00022BB8
.text:00022BBD
.text:00022BBE
.text:00022BC3
.text:00022BC6
.text:00022BC7
.text:00022BCC
.text:00022BCF
.text:00022BD4
.text:00022BD6
.text:00022BDA
.text:00022BDA
.text:00022BDA
.text:00022BDD

loc_22B78: ; CODE XREF: SSQC+B1j
pop
ebx
pop
esi
pop
edi
xor
eax, eax
mov
esp, ebp
pop
ebp
retn
loc_22B84: ; CODE XREF: SSQC+31j
mov
al, [esi]
pop
ebx
pop
esi
pop
edi
mov
ds:byte_407224, al
mov
esp, ebp
xor
eax, eax
pop
ebp
retn
loc_22B94: ; CODE XREF: SSQC+3Cj
mov
al, [esi]
pop
ebx
pop
esi
pop
edi
mov
ds:byte_407225, al
mov
esp, ebp
xor
eax, eax
pop
ebp
retn
loc_22BA4: ; CODE XREF: SSQC+1Fj
movsx
eax, ds:byte_407225
push
esi
push
eax
movsx
eax, ds:byte_407224
push
eax
lea
eax, [ebp+var_44]
push
offset a46CCS
; "46%c%c%s"
push
eax
call
nl_sprintf
lea
eax, [ebp+var_44]
push
eax
call
strlen
add
esp, 18h
cmp
eax, 1Bh
jle
short loc_22BDA
mov
[ebp+var_29], 0
loc_22BDA: ; CODE XREF: SSQC+11Cj
lea
eax, [ebp+var_44]
push
eax

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1076
.text:00022BDE
.text:00022BE3
.text:00022BE4
.text:00022BE7
.text:00022BE8
.text:00022BEA
.text:00022BEB
.text:00022BF0
.text:00022BF3
.text:00022BF4
.text:00022BF5
.text:00022BF6
.text:00022BF8
.text:00022BF9
.text:00022BFA
.text:00022BFA SSQC

call
strlen
push
eax
; unsigned int
lea
eax, [ebp+var_44]
push
eax
; void *
mov
eax, [edi]
push
eax
; int
call
_write
add
esp, 10h
pop
ebx
pop
esi
pop
edi
mov
esp, ebp
pop
ebp
retn
db 0Eh dup(90h)
endp

Действительно, должна же как-то программа обмениваться информацией с
драйвером.
Единственное место где вызывается функция SSQC() это thunk function:
.text:0000DBE8
.text:0000DBE8 SSQ
.text:0000DBE8
.text:0000DBE8
.text:0000DBE8 arg_0
.text:0000DBE8
.text:0000DBE8
.text:0000DBE9
.text:0000DBEB
.text:0000DBEE
.text:0000DBEF
.text:0000DBF4
.text:0000DBF7
.text:0000DBF9
.text:0000DBFA
.text:0000DBFB SSQ

public SSQ
proc near ; CODE XREF: sys_info+A9p
; sys_info+CBp ...
= dword ptr
push
mov
mov
push
call
add
mov
pop
retn
endp

ebp
ebp,
edx,
edx
SSQC
esp,
esp,
ebp

8

esp
[ebp+arg_0]

4
ebp

А вот SSQ() может вызываться по крайней мере из двух разных функций.
Одна из них:
.data:0040169C _51_52_53
DATA XREF: init_sys+392r
.data:0040169C
.data:0040169C
CONTINUE: "
.data:004016A0
.data:004016A4
.data:004016A8

dd offset aPressAnyKeyT_0 ;
; sys_info+A1r
; "PRESS ANY KEY TO
dd offset a51
dd offset a52
dd offset a53

; "51"
; "52"
; "53"

...

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1077
.data:004016B8 _3C_or_3E
sys_info:loc_D67Br
.data:004016B8
.data:004016BC

dd offset a3c

; DATA XREF:

dd offset a3e

; "3C"
; "3E"

; эти имена мы сами дали этим меткам:
.data:004016C0 answers1
dd 6B05h
sys_info+E7r
.data:004016C4
dd 3D87h
.data:004016C8 answers2
dd 3Ch
sys_info+F2r
.data:004016CC
dd 832h
.data:004016D0 _C_and_B
db 0Ch
sys_info+BAr
.data:004016D0
.data:004016D1 byte_4016D1
db 0Bh
DATA XREF: sys_info+FDr
.data:004016D2
db
0

; DATA XREF:
; DATA XREF:
; DATA XREF:
; sys_info:OKr
;

...
.text:0000D652
xor
eax, eax
.text:0000D654
mov
al, ds:ctl_port
.text:0000D659
mov
ecx, _51_52_53[eax∗4]
.text:0000D660
push
ecx
.text:0000D661
call
SSQ
.text:0000D666
add
esp, 4
.text:0000D669
cmp
eax, 0FFFFFFFFh
.text:0000D66E
jz
short loc_D6D1
.text:0000D670
xor
ebx, ebx
.text:0000D672
mov
al, _C_and_B
.text:0000D677
test
al, al
.text:0000D679
jz
short loc_D6C0
.text:0000D67B
.text:0000D67B loc_D67B: ; CODE XREF: sys_info+106j
.text:0000D67B
mov
eax, _3C_or_3E[ebx∗4]
.text:0000D682
push
eax
.text:0000D683
call
SSQ
.text:0000D688
push
offset a4g
; "4G"
.text:0000D68D
call
SSQ
.text:0000D692
push
offset a0123456789 ; "0123456789"
.text:0000D697
call
SSQ
.text:0000D69C
add
esp, 0Ch
.text:0000D69F
mov
edx, answers1[ebx∗4]
.text:0000D6A6
cmp
eax, edx
.text:0000D6A8
jz
short OK
.text:0000D6AA
mov
ecx, answers2[ebx∗4]
.text:0000D6B1
cmp
eax, ecx
.text:0000D6B3
jz
short OK
.text:0000D6B5
mov
al, byte_4016D1[ebx]
.text:0000D6BB
inc
ebx
.text:0000D6BC
test
al, al
.text:0000D6BE
jnz
short loc_D67B
.text:0000D6C0

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1078
.text:0000D6C0
.text:0000D6C0
.text:0000D6C6
.text:0000D6C8
.text:0000D6CD
.text:0000D6CF
.text:0000D6D1
.text:0000D6D1
.text:0000D6D1
.text:0000D6D1
.text:0000D6D4
.text:0000D6D5
.text:0000D6D8
.text:0000D6DB
.text:0000D6E1
.text:0000D6E1
.text:0000D6E1
.text:0000D6E1
.text:0000D6E2
.text:0000D6E3
.text:0000D6E5
.text:0000D6E6
.text:0000D6E8
.text:0000D6E8
.text:0000D6E8
.text:0000D6EE
.text:0000D6EF
.text:0000D6F0
.text:0000D6F5
.text:0000D6F7
.text:0000D6F8
.text:0000D6F8

loc_D6C0: ; CODE XREF: sys_info+C1j
inc
ds:ctl_port
xor
eax, eax
mov
al, ds:ctl_port
cmp
eax, edi
jle
short loc_D652
loc_D6D1: ; CODE XREF: sys_info+98j
; sys_info+B6j
mov
edx, [ebp+var_8]
inc
edx
mov
[ebp+var_8], edx
cmp
edx, 3
jle
loc_D641
loc_D6E1: ; CODE XREF: sys_info+16j
; sys_info+51j ...
pop
ebx
pop
edi
mov
esp, ebp
pop
ebp
retn
OK:
; CODE XREF: sys_info+F0j
; sys_info+FBj
mov
al, _C_and_B[ebx]
pop
ebx
pop
edi
mov
ds:ctl_model, al
mov
esp, ebp
pop
ebp
retn
sys_info
endp

«3C» и «3E» — это звучит знакомо: когда-то была донгла Sentinel Pro от Rainbow
без памяти, предоставляющая только одну секретную крипто-хеширующую
функцию.
О том, что такое хэш-функция, было описано здесь: 2.11 (стр. 602).
Но вернемся к нашей программе. Так что программа может только проверить
подключена ли донгла или нет. Никакой больше информации в такую донглу
без памяти записать нельзя. Двухсимвольные коды — это команды (можно увидеть, как они обрабатываются в функции SSQC()) а все остальные строки хешируются внутри донглы превращаясь в 16-битное число. Алгоритм был секретный, так что нельзя было написать замену драйверу или сделать электронную
копию донглы идеально эмулирующую алгоритм. С другой стороны, всегда
можно было перехватить все обращения к ней и найти те константы, с которыми сравнивается результат хеширования. Но надо сказать, вполне возможно
создать устойчивую защиту от копирования базирующуюся на секретной хешфункции: пусть она шифрует все файлы с которыми ваша программа работает.
Но вернемся к нашему коду.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1079
Коды 51/52/53 используются для выбора номера принтеровского LPT-порта.
3x/4x используются для выбора «family» так донглы Sentinel Pro можно отличать друг от друга: ведь более одной донглы может быть подключено к LPTпорту.
Единственная строка, передающаяся в хеш-функцию это
”0123456789”. Затем результат сравнивается с несколькими правильными значениями.
Если результат правилен, 0xC или 0xB будет записано в глобальную переменную ctl_model.
Еще одна строка для хеширования: ”PRESS ANY KEY TO CONTINUE: ”, но результат не проверяется. Трудно сказать, зачем это, может быть по ошибке 16 .
Давайте посмотрим, где проверяется значение глобальной переменной ctl_model.
Одно из таких мест:
.text:0000D708 prep_sys proc near ; CODE XREF: init_sys+46Ap
.text:0000D708
.text:0000D708 var_14
= dword ptr −14h
.text:0000D708 var_10
= byte ptr −10h
.text:0000D708 var_8
= dword ptr −8
.text:0000D708 var_2
= word ptr −2
.text:0000D708
.text:0000D708
push
ebp
.text:0000D709
mov
eax, ds:net_env
.text:0000D70E
mov
ebp, esp
.text:0000D710
sub
esp, 1Ch
.text:0000D713
test
eax, eax
.text:0000D715
jnz
short loc_D734
.text:0000D717
mov
al, ds:ctl_model
.text:0000D71C
test
al, al
.text:0000D71E
jnz
short loc_D77E
.text:0000D720
mov
[ebp+var_8], offset aIeCvulnvvOkgT_ ;
"Ie-cvulnvV\\\bOKG]T_"
.text:0000D727
mov
edx, 7
.text:0000D72C
jmp
loc_D7E7
...
.text:0000D7E7 loc_D7E7: ; CODE XREF: prep_sys+24j
.text:0000D7E7
; prep_sys+33j
.text:0000D7E7
push
edx
.text:0000D7E8
mov
edx, [ebp+var_8]
.text:0000D7EB
push
20h
.text:0000D7ED
push
edx
.text:0000D7EE
push
16h
.text:0000D7F0
call
err_warn
.text:0000D7F5
push
offset station_sem
.text:0000D7FA
call
ClosSem
.text:0000D7FF
call
startup_err
16 Это

очень странное чувство: находить ошибки в столь древнем ПО.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1080
Если оно 0, шифрованное сообщение об ошибке будет передано в функцию
дешифрования, и оно будет показано.
Функция дешифровки сообщений об ошибке похоже применяет простой xoring:
.text:0000A43C err_warn
prep_sys+E8p
.text:0000A43C
.text:0000A43C
.text:0000A43C var_55
.text:0000A43C var_54
.text:0000A43C arg_0
.text:0000A43C arg_4
.text:0000A43C arg_8
.text:0000A43C arg_C
.text:0000A43C
.text:0000A43C
.text:0000A43D
.text:0000A43F
.text:0000A442
.text:0000A443
.text:0000A446
.text:0000A448
.text:0000A44A
.text:0000A44B
.text:0000A44D
.text:0000A450
.text:0000A453
.text:0000A453 loc_A453:
err_warn+28j
.text:0000A453
.text:0000A455
.text:0000A458
.text:0000A45A
.text:0000A45D
.text:0000A45E
.text:0000A460
.text:0000A464
.text:0000A466
.text:0000A466 loc_A466:
err_warn+Fj
.text:0000A466
.text:0000A46B
.text:0000A46E
.text:0000A473
.text:0000A475
.text:0000A478
.text:0000A479
.text:0000A47E
.text:0000A481
.text:0000A481 loc_A481:
err_warn+72j
.text:0000A481
.text:0000A483
.text:0000A485

proc near

; CODE XREF:
; prep_sys2+2Fp ...

=
=
=
=
=
=

byte ptr −55h
byte ptr −54h
dword ptr 8
dword ptr 0Ch
dword ptr 10h
dword ptr 14h

push
mov
sub
push
mov
xor
test
push
jle
mov
mov

ebp
ebp, esp
esp, 54h
edi
ecx, [ebp+arg_8]
edi, edi
ecx, ecx
esi
short loc_A466
esi, [ebp+arg_C] ; key
edx, [ebp+arg_4] ; string
; CODE XREF:

xor
mov
xor
add
inc
cmp
mov
jl

eax, eax
al, [edx+edi]
eax, esi
esi, 3
edi
edi, ecx
[ebp+edi+var_55], al
short loc_A453
; CODE XREF:

mov
mov
cmp
jnz
lea
push
call
add

[ebp+edi+var_54], 0
eax, [ebp+arg_0]
eax, 18h
short loc_A49C
eax, [ebp+var_54]
eax
status_line
esp, 4
; CODE XREF:

push
push
lea

50h
0
eax, [ebp+var_54]

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1081
.text:0000A488
.text:0000A489
.text:0000A48E
.text:0000A493
.text:0000A496
.text:0000A497
.text:0000A498
.text:0000A49A
.text:0000A49B
.text:0000A49C
.text:0000A49C loc_A49C:
err_warn+37j
.text:0000A49C
.text:0000A49E
.text:0000A4A1
.text:0000A4A4
.text:0000A4A5
.text:0000A4A6
.text:0000A4AB
.text:0000A4AE
.text:0000A4AE err_warn

push
call
call
add
pop
pop
mov
pop
retn

eax
memset
pcv_refresh
esp, 0Ch
esi
edi
esp, ebp
ebp

; CODE XREF:
push
lea
mov
push
push
call
add
jmp
endp

0
eax, [ebp+var_54]
edx, [ebp+arg_0]
edx
eax
pcv_lputs
esp, 0Ch
short loc_A481

Вот почему не получилось найти сообщение об ошибке в исполняемых файлах,
потому что оно было зашифровано, это очень популярная практика.
Еще один вызов хеширующей функции передает строку «offln» и сравнивает
результат с константами 0xFE81 и 0x12A9. Если результат не сходится, происходит работа с какой-то функцией timer() (может быть для ожидания плохо
подключенной донглы и нового запроса?), затем дешифрует еще одно сообщение об ошибке и выводит его.
.text:0000DA55 loc_DA55:
sync_sys+24Cj
.text:0000DA55
.text:0000DA5A
.text:0000DA5F
.text:0000DA62
.text:0000DA64
.text:0000DA66
.text:0000DA69
.text:0000DA6B
.text:0000DA71
.text:0000DA77
.text:0000DA7D
.text:0000DA83
.text:0000DA83 loc_DA83:
sync_sys+201j
.text:0000DA83
.text:0000DA85
.text:0000DA88
.text:0000DA8A
.text:0000DA90
.text:0000DA96
.text:0000DA99

; CODE XREF:
push
call
add
mov
mov
cmp
jnz
cmp
jz
cmp
jz

offset aOffln
; "offln"
SSQ
esp, 4
dl, [ebx]
esi, eax
dl, 0Bh
short loc_DA83
esi, 0FE81h
OK
esi, 0FFFFF8EFh
OK
; CODE XREF:

mov
cmp
jnz
cmp
jz
cmp
jz

cl, [ebx]
cl, 0Ch
short loc_DA9F
esi, 12A9h
OK
esi, 0FFFFFFF5h
OK

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1082
.text:0000DA9F
.text:0000DA9F loc_DA9F:
sync_sys+220j
.text:0000DA9F
.text:0000DAA2
.text:0000DAA4
.text:0000DAA6
.text:0000DAA8
.text:0000DAAD
.text:0000DAB0
.text:0000DAB0 loc_DAB0:
sync_sys+23Cj
.text:0000DAB0
.text:0000DAB1
.text:0000DAB4
.text:0000DAB6
.text:0000DABB
.text:0000DABD

; CODE XREF:
mov
test
jz
push
call
add

eax, [ebp+var_18]
eax, eax
short loc_DAB0
24h
timer
esp, 4
; CODE XREF:

inc
cmp
jle
mov
test
jz

edi
edi, 3
short loc_DA55
eax, ds:net_env
eax, eax
short error

...
.text:0000DAF7 error:
sync_sys+255j
.text:0000DAF7
.text:0000DAF7
mov
Ç encrypted_error_message2
.text:0000DAFE
mov
.text:0000DB05
jmp

; CODE XREF:
; sync_sys+274j ...
[ebp+var_8], offset ⤦
[ebp+var_C], 17h ; decrypting key
decrypt_end_print_message

...
; это имя мы сами дали этой метке:
.text:0000D9B6 decrypt_end_print_message:
; CODE XREF:
sync_sys+29Dj
.text:0000D9B6
; sync_sys+2ABj
.text:0000D9B6
mov
eax, [ebp+var_18]
.text:0000D9B9
test
eax, eax
.text:0000D9BB
jnz
short loc_D9FB
.text:0000D9BD
mov
edx, [ebp+var_C] ; key
.text:0000D9C0
mov
ecx, [ebp+var_8] ; string
.text:0000D9C3
push
edx
.text:0000D9C4
push
20h
.text:0000D9C6
push
ecx
.text:0000D9C7
push
18h
.text:0000D9C9
call
err_warn
.text:0000D9CE
push
0Fh
.text:0000D9D0
push
190h
.text:0000D9D5
call
sound
.text:0000D9DA
mov
[ebp+var_18], 1
.text:0000D9E1
add
esp, 18h
.text:0000D9E4
call
pcv_kbhit
.text:0000D9E9
test
eax, eax
.text:0000D9EB
jz
short loc_D9FB

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1083
...
; это имя мы сами дали этой метке:
.data:00401736 encrypted_error_message2 db 74h, 72h, 78h, 43h, 48h, 6, 5Ah,⤦
Ç 49h, 4Ch, 2 dup(47h)
.data:00401736
db 51h, 4Fh, 47h, 61h, 20h, 22h, 3Ch, 24h, ⤦
Ç 33h, 36h, 76h
.data:00401736
db 3Ah, 33h, 31h, 0Ch, 0, 0Bh, 1Fh, 7, 1Eh, ⤦
Ç 1Ah

Заставить работать программу без донглы довольно просто: просто пропатчить все места после инструкций CMP где происходят соответствующие сравнения.
Еще одна возможность — это написать свой драйвер для SCO OpenServer, содержащий таблицу возможных вопросов и ответов, все те что имеются в программе.
Дешифровка сообщений об ошибке
Кстати, мы также можем дешифровать все сообщения об ошибке. Алгоритм,
находящийся в функции err_warn() действительно, крайне прост:
Листинг 8.5: Функция дешифровки
.text:0000A44D
.text:0000A450
.text:0000A453 loc_A453:
.text:0000A453
.text:0000A455
дешифровки
.text:0000A458
.text:0000A45A
следующего байта
.text:0000A45D
.text:0000A45E
.text:0000A460
.text:0000A464

mov
mov

esi, [ebp+arg_C] ; key
edx, [ebp+arg_4] ; string

xor
mov

eax, eax
al, [edx+edi] ; загружаем байт для

xor
add

eax, esi
esi, 3

inc
cmp
mov
jl

edi
edi, ecx
[ebp+edi+var_55], al
short loc_A453

; дешифруем его
; изменяем ключ для

Как видно, не только сама строка поступает на вход, но также и ключ для
дешифровки:
.text:0000DAF7 error:
sync_sys+255j
.text:0000DAF7
.text:0000DAF7
mov
Ç encrypted_error_message2
.text:0000DAFE
mov
.text:0000DB05
jmp

; CODE XREF:
; sync_sys+274j ...
[ebp+var_8], offset ⤦
[ebp+var_C], 17h ; decrypting key
decrypt_end_print_message

...
; это имя мы сами дали этой метке:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1084
.text:0000D9B6 decrypt_end_print_message:
; CODE XREF:
sync_sys+29Dj
.text:0000D9B6
; sync_sys+2ABj
.text:0000D9B6
mov
eax, [ebp+var_18]
.text:0000D9B9
test
eax, eax
.text:0000D9BB
jnz
short loc_D9FB
.text:0000D9BD
mov
edx, [ebp+var_C] ; key
.text:0000D9C0
mov
ecx, [ebp+var_8] ; string
.text:0000D9C3
push
edx
.text:0000D9C4
push
20h
.text:0000D9C6
push
ecx
.text:0000D9C7
push
18h
.text:0000D9C9
call
err_warn

Алгоритм это очень простой xoring: каждый байт XOR-ится с ключом, но ключ
увеличивается на 3 после обработки каждого байта.
Напишем небольшой скрипт на Python для проверки наших догадок:
Листинг 8.6: Python 3.x
#!/usr/bin/python
import sys
msg=[0x74, 0x72, 0x78, 0x43, 0x48, 0x6, 0x5A, 0x49, 0x4C, 0x47, 0x47,
0x51, 0x4F, 0x47, 0x61, 0x20, 0x22, 0x3C, 0x24, 0x33, 0x36, 0x76,
0x3A, 0x33, 0x31, 0x0C, 0x0, 0x0B, 0x1F, 0x7, 0x1E, 0x1A]
key=0x17
tmp=key
for i in msg:
sys.stdout.write ("%c" % (i^tmp))
tmp=tmp+3
sys.stdout.flush()

И он выводит: «check security device connection». Так что да, это дешифрованное сообщение.
Здесь есть также и другие сообщения, с соответствующими ключами. Но надо
сказать, их можно дешифровать и без ключей. В начале, мы можем заметить,
что ключ — это просто байт. Это потому что самая важная часть функции дешифровки (XOR) оперирует байтами. Ключ находится в регистре ESI, но только
младшие 8 бит (т.е. байт) регистра используются. Следовательно, ключ может
быть больше 255, но его значение будет округляться.
И как следствие, мы можем попробовать обычный перебор всех ключей в диапазоне 0..255. Мы также можем пропускать сообщения содержащие непечатные символы.
Листинг 8.7: Python 3.x
#!/usr/bin/python
import sys, curses.ascii
msgs=[
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1085
[0x74, 0x72, 0x78, 0x43, 0x48, 0x6, 0x5A, 0x49, 0x4C, 0x47, 0x47,
0x51, 0x4F, 0x47, 0x61, 0x20, 0x22, 0x3C, 0x24, 0x33, 0x36, 0x76,
0x3A, 0x33, 0x31, 0x0C, 0x0, 0x0B, 0x1F, 0x7, 0x1E, 0x1A],
[0x49, 0x65, 0x2D, 0x63, 0x76, 0x75, 0x6C, 0x6E, 0x76, 0x56, 0x5C,
8, 0x4F, 0x4B, 0x47, 0x5D, 0x54, 0x5F, 0x1D, 0x26, 0x2C, 0x33,
0x27, 0x28, 0x6F, 0x72, 0x75, 0x78, 0x7B, 0x7E, 0x41, 0x44],
[0x45, 0x61, 0x31, 0x67, 0x72, 0x79, 0x68, 0x52, 0x4A, 0x52, 0x50,
0x0C, 0x4B, 0x57, 0x43, 0x51, 0x58, 0x5B, 0x61, 0x37, 0x33, 0x2B,
0x39, 0x39, 0x3C, 0x38, 0x79, 0x3A, 0x30, 0x17, 0x0B, 0x0C],
[0x40, 0x64, 0x79, 0x75, 0x7F, 0x6F, 0x0, 0x4C, 0x40, 0x9, 0x4D, 0x5A,
0x46, 0x5D, 0x57, 0x49, 0x57, 0x3B, 0x21, 0x23, 0x6A, 0x38, 0x23,
0x36, 0x24, 0x2A, 0x7C, 0x3A, 0x1A, 0x6, 0x0D, 0x0E, 0x0A, 0x14,
0x10],
[0x72, 0x7C, 0x72, 0x79, 0x76, 0x0,
0x50, 0x43, 0x4A, 0x59, 0x5D, 0x5B, 0x41, 0x41, 0x1B, 0x5A,
0x24, 0x32, 0x2E, 0x29, 0x28, 0x70, 0x20, 0x22, 0x38, 0x28, 0x36,
0x0D, 0x0B, 0x48, 0x4B, 0x4E]]

def is_string_printable(s):
return all(list(map(lambda x: curses.ascii.isprint(x), s)))
cnt=1
for msg in msgs:
print ("message #%d" % cnt)
for key in range(0,256):
result=[]
tmp=key
for i in msg:
result.append (i^tmp)
tmp=tmp+3
if is_string_printable (result):
print ("key=", key, "value=", "".join(list(map(chr,⤦
Ç result))))
cnt=cnt+1

И мы получим:
Листинг 8.8: Results
message #1
key= 20 value= `eb^h%|``hudw|_af{n~f%ljmSbnwlpk
key= 21 value= ajc]i"}cawtgv{^bgto}g"millcmvkqh
key= 22 value= bkd\j#rbbvsfuz!cduh|d#bhomdlujni
key= 23 value= check security device connection
key= 24 value= lifbl!pd|tqhsx#ejwjbb!`nQofbshlo
message #2
key= 7 value= No security device found
key= 8 value= An#rbbvsVuz!cduhld#ghtme?!#!'!#!
message #3
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1086
key= 7 value= BkugasLkvw&fgpgag^uvcrwml.`mwhj
key= 10 value= Ol!td`tMhwx'efwfbf!tubuvnm!anvok
key= 11 value= No security device station found
key= 12 value= In#rjbvsnuz!{duhdd#r{`whho#gPtme
message #4
key= 14 value= Number of authorized users exceeded
key= 15 value= Ovlmdq!hg#`juknuhydk!vrbsp!Zy`dbefe
message #5
key= 17 value= check security device station
key= 18 value= `ijbh!td`tmhwx'efwfbf!tubuVnm!'!

Тут есть какой-то мусор, но мы можем быстро отыскать сообщения на английском языке!
Кстати, так как алгоритм использует простой XOR, та же функция может использоваться и для шифрования сообщения. Если нужно, мы можем зашифровать наши собственные сообщения, и пропатчить программу вставив их.

8.6.3. Пример #3: MS-DOS
Еще одна очень старая программа для MS-DOS от 1995 также разработанная
давно исчезнувшей компанией.
Во времена перед DOS-экстендерами, всё ПО для MS-DOS рассчитывалось на
процессоры 8086 или 80286, так что в своей массе весь код был 16-битным.
16-битный код в основном такой же, какой вы уже видели в этой книге, но все
регистры 16-битные, и доступно меньше инструкций.
Среда MS-DOS не могла иметь никаких драйверов, и ПО работало с «голым»
железом через порты, так что здесь вы можете увидеть инструкции OUT/IN,
которые в наше время присутствуют в основном только в драйверах (в современных OS нельзя обращаться на прямую к портам из user mode).
Учитывая это, ПО для MS-DOS должно работать с донглой обращаясь к принтерному LPT-порту напрямую. Так что мы можем просто поискать эти инструкции.
И да, вот они:
seg030:0034
seg030:0034
seg030:0034
seg030:0034
seg030:0034
seg030:0034
seg030:0035
seg030:0037
seg030:003B
seg030:003E
seg030:003F
seg030:0040
seg030:0040

out_port proc far ; CODE XREF: sent_pro+22p
; sent_pro+2Ap ...
arg_0
55
8B EC
8B 16 7E E7
8A 46 06
EE
5D
CB

= byte ptr

push
mov
mov
mov
out
pop
retf
out_port endp

bp
bp,
dx,
al,
dx,
bp

6

sp
_out_port ; 0x378
[bp+arg_0]
al

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1087
(Все имена меток в этом примере даны мною).
Функция out_port() вызывается только из одной функции:
seg030:0041
seg030:0041
seg030:0041
seg030:0041
seg030:0041
seg030:0041
seg030:0041
seg030:0045
seg030:0046
seg030:0047
seg030:004B
seg030:004C
seg030:004E
seg030:0051
seg030:0054
seg030:0056
seg030:0059
seg030:005C
seg030:005E
seg030:005F
seg030:0062
seg030:0063
seg030:0066
seg030:0067
seg030:006A
seg030:006B
seg030:006E
seg030:006F
seg030:0071
seg030:0073
seg030:0073
seg030:0073
seg030:0074
seg030:0074
seg030:0074
seg030:0078
seg030:007A
seg030:007D
seg030:007E
seg030:0081
seg030:0082
seg030:0085
seg030:0086
seg030:0089
seg030:008A
seg030:008D
seg030:008E
seg030:0091
seg030:0092
seg030:0095

sent_pro proc far ; CODE XREF: check_dongle+34p
var_3
var_2
arg_0
C8
56
57
8B
EC
8A
80
80
8A
88
80
8A
EE
68
0E
E8
59
68
0E
E8
59
33
EB

04 00 00

16 82 E7
D8
E3
CB
C3
46
E3
C3

FE
04
FD
1F

FF 00
CE FF
D3 00
C6 FF
F6
01

enter
push
push
mov
in
mov
and
or
mov
mov
and
mov
out
push
push
call
pop
push
push
call
pop
xor
jmp

4, 0
si
di
dx, _in_port_1 ; 0x37A
al, dx
bl, al
bl, 0FEh
bl, 4
al, bl
[bp+var_3], al
bl, 1Fh
al, bl
dx, al
0FFh
cs
near ptr out_port
cx
0D3h
cs
near ptr out_port
cx
si, si
short loc_359D4

loc_359D3: ; CODE XREF: sent_pro+37j
inc
si

46

81
7C
68
0E
E8
59
68
0E
E8
59
68
0E
E8
59
68
0E

= byte ptr −3
= word ptr −2
= dword ptr 6

FE 96 00
F9
C3 00
B3 FF
C7 00
AB FF
D3 00
A3 FF
C3 00

loc_359D4: ; CODE XREF: sent_pro+30j
cmp
si, 96h
jl
short loc_359D3
push
0C3h
push
cs
call
near ptr out_port
pop
cx
push
0C7h
push
cs
call
near ptr out_port
pop
cx
push
0D3h
push
cs
call
near ptr out_port
pop
cx
push
0C3h
push
cs

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1088
seg030:0096
seg030:0099
seg030:009A
seg030:009D
seg030:009E
seg030:00A1
seg030:00A2
seg030:00A5
seg030:00A6
seg030:00A9
seg030:00AA
seg030:00AD
seg030:00AF
seg030:00AF
seg030:00AF
seg030:00B2
seg030:00B2
seg030:00B2
seg030:00B4
seg030:00B8
seg030:00B9
seg030:00BB
seg030:00BD
seg030:00C0
seg030:00C0
seg030:00C0
seg030:00C5
seg030:00C7
seg030:00CA
seg030:00CC
seg030:00CC
seg030:00CC
seg030:00CF
seg030:00D0
seg030:00D3
seg030:00D4
seg030:00D7
seg030:00D7
seg030:00D7
seg030:00D8
seg030:00DB
seg030:00DC
seg030:00DF
seg030:00E0
seg030:00E3
seg030:00E4
seg030:00E7
seg030:00E9
seg030:00EC
seg030:00ED
seg030:00EF
seg030:00EF
seg030:00EF

E8
59
68
0E
E8
59
68
0E
E8
59
BF
EB

9B FF
C7 00
93 FF
D3 00
8B FF
FF FF
40

call
pop
push
push
call
pop
push
push
call
pop
mov
jmp

near ptr out_port
cx
0C7h
cs
near ptr out_port
cx
0D3h
cs
near ptr out_port
cx
di, 0FFFFh
short loc_35A4F

BE 04 00

loc_35A0F: ; CODE XREF: sent_pro+BDj
mov
si, 4

D1
8B
EC
A8
75
83

80
03
CF 01

loc_35A12: ; CODE XREF: sent_pro+ACj
shl
di, 1
mov
dx, _in_port_2 ; 0x379
in
al, dx
test
al, 80h
jnz
short loc_35A20
or
di, 1

F7
74
68
EB

46 FE 08+
05
D7 00
0B

loc_35A20: ; CODE XREF: sent_pro+7Aj
test
[bp+var_2], 8
jz
short loc_35A2C
push
0D7h ; '+'
jmp
short loc_35A37

E7
16 80 E7

68 C3 00
0E
E8 61 FF
59
68 C7 00

loc_35A2C: ; CODE XREF: sent_pro+84j
push
0C3h
push
cs
call
near ptr out_port
pop
cx
push
0C7h

0E
E8
59
68
0E
E8
59
8B
D1
89
4E
75

C3

loc_35A37: ; CODE XREF: sent_pro+89j
push
cs
call
near ptr out_port
pop
cx
push
0D3h
push
cs
call
near ptr out_port
pop
cx
mov
ax, [bp+var_2]
shl
ax, 1
mov
[bp+var_2], ax
dec
si
jnz
short loc_35A12

C4 5E 06

loc_35A4F: ; CODE XREF: sent_pro+6Cj
les
bx, [bp+arg_0]

59 FF
D3 00
51 FF
46 FE
E0
46 FE

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1089
seg030:00F2
seg030:00F5
seg030:00F8
seg030:00F9
seg030:00FC
seg030:00FE
seg030:0100
seg030:0103
seg030:0104
seg030:0107
seg030:0108
seg030:010C
seg030:010D
seg030:010F
seg030:0112
seg030:0114
seg030:0115
seg030:0116
seg030:0118
seg030:011B
seg030:011D
seg030:0120
seg030:0123
seg030:0125
seg030:0125
seg030:0125
seg030:0128
seg030:0128
seg030:0128
seg030:012B
seg030:012D
seg030:0130
seg030:0130
seg030:0130
seg030:0134
seg030:0136
seg030:0137
seg030:0139
seg030:013A
seg030:013B
seg030:013C
seg030:013C

FF
26
98
89
0B
75
68
0E
E8
59
8B
EC
8A
80
8A
EE
EC
8A
F6
74
8A
80
EB

46 06
8A 07
46 FE
C0
AF
FF 00
2D FF
16 82 E7
C8
E1 5F
C1

C8
C1 20
08
5E FD
E3 DF
03

inc
mov
cbw
mov
or
jnz
push
push
call
pop
mov
in
mov
and
mov
out
in
mov
test
jz
mov
and
jmp

word ptr [bp+arg_0]
al, es:[bx]
[bp+var_2], ax
ax, ax
short loc_35A0F
0FFh
cs
near ptr out_port
cx
dx, _in_port_1 ; 0x37A
al, dx
cl, al
cl, 5Fh
al, cl
dx, al
al, dx
cl, al
cl, 20h
short loc_35A85
bl, [bp+var_3]
bl, 0DFh
short loc_35A88

8A 5E FD

loc_35A85: ; CODE XREF: sent_pro+DAj
mov
bl, [bp+var_3]

F6 C1 80
74 03
80 E3 7F

loc_35A88: ; CODE XREF: sent_pro+E2j
test
cl, 80h
jz
short loc_35A90
and
bl, 7Fh

8B 16 82 E7
8A C3
EE
8B C7
5F
5E
C9
CB

loc_35A90: ; CODE XREF: sent_pro+EAj
mov
dx, _in_port_1 ; 0x37A
mov
al, bl
out
dx, al
mov
ax, di
pop
di
pop
si
leave
retf
sent_pro endp

Это также «хеширующая» донгла Sentinel Pro как и в предыдущем примере.
Это заметно по тому что текстовые строки передаются и здесь, 16-битные значения также возвращаются и сравниваются с другими.
Так вот как происходит работа с Sentinel Pro через порты. Адрес выходного
порта обычно 0x378, т.е. принтерного порта, данные для него во времена перед USB отправлялись прямо сюда. Порт однонаправленный, потому что когда
его разрабатывали, никто не мог предположить, что кому-то понадобится по-

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1090
лучать информацию из принтера 17 . Единственный способ получить информацию из принтера это регистр статуса на порту 0x379, он содержит такие биты
как «paper out», «ack», «busy» — так принтер может сигнализировать о том,
что он готов или нет, и о том, есть ли в нем бумага. Так что донгла возвращает
информацию через какой-то из этих бит, по одному биту на каждой итерации.
_in_port_2 содержит адрес статуса (0x379) и _in_port_1 содержит адрес управляющего регистра (0x37A).
Судя по всему, донгла возвращает информацию только через флаг «busy» на
seg030:00B9: каждый бит записывается в регистре DI позже возвращаемый в
самом конце функции.
Что означают все эти отсылаемые в выходной порт байты? Трудно сказать. Возможно, команды донглы. Но честно говоря, нам и не обязательно знать: нашу
задачу можно легко решить и без этих знаний.
Вот функция проверки донглы:
00000000
00000000
00000019
0000001B

struct_0
field_0
_A
struct_0

struc ; (sizeof=0x1B)
db 25 dup(?)
dw ?
ends

dseg:3CBC 61 63 72 75+_Q
dseg:3CBC 6E 00 00 00+

; string(C)

struct_0
; DATA XREF: check_dongle+2Eo

... skipped ...
dseg:3E00
dseg:3E1B
dseg:3E36
dseg:3E51
dseg:3E6C
dseg:3E87
dseg:3EA2
dseg:3EBD

63
64
63
70
63
63
64
63

seg030:0145
seg030:0145
seg030:0145
seg030:0145
seg030:0145
seg030:0145
seg030:0149
seg030:014A
seg030:014D
seg030:014F
seg030:0154
seg030:0155
seg030:0156

6F
6F
61
61
6F
6C
69
6F

66
67
74
70
6B
6F
72
70

66+
00+
00+
65+
65+
63+
00+
79+

struct_0
struct_0
struct_0
struct_0
struct_0
struct_0
struct_0
struct_0

check_dongle proc far ; CODE XREF: sub_3771D+3EP
var_6 = dword ptr −6
var_2 = word ptr −2
C8
56
66
6A
9A
52
50
66

06 00 00
6A 00
00
C1 18 00+

58

enter
push
push
push
call
push
push
pop

6, 0
si
large 0
0
_biostime
dx
ax
eax

; newtime
; cmd

17 Если учитывать только Centronics и не учитывать последующий стандарт IEEE 1284 — в нем из
принтера можно получать информацию.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1091
seg030:0158
seg030:015B
seg030:015F
seg030:0164
seg030:0166
seg030:0168
seg030:0169
seg030:016A
seg030:016D
seg030:016E
seg030:0170
seg030:0173
seg030:0176
seg030:0177
seg030:0178
seg030:0179
seg030:017C
seg030:017F
seg030:0182
seg030:0184
seg030:0187
seg030:018B
seg030:018F
seg030:0192
seg030:0197
seg030:0199
seg030:019C
seg030:01A0
seg030:01A3
seg030:01A5
seg030:01A8
seg030:01AA
seg030:01AA
seg030:01AA
seg030:01AA
seg030:01AC
seg030:01AC
seg030:01AC
seg030:01AD
seg030:01AE
seg030:01AE

83
66
66
7E
6A
90
0E
E8
59
8B
6B
05
1E
50
0E
E8
83
89
8B
6B
66
66
66
66
8B
6B
8B
3B
74
B8
EB

C4 06
89 46 FA
3B 06 D8+
44
14

52 00
F0
C0 1B
BC 3C

C5
C4
46
C6
C0
0F
8B
03
89
DE
DB
87
46
05
01
02

33 C0

5E
C9
CB

add
mov
cmp
jle
push
nop
push
call
pop
mov
imul
add
push
push
push
call
add
mov
mov
imul
movsx
mov
add
mov
mov
imul
mov
cmp
jz
mov
jmp

FE
04
FE
12
BF C0
56 FA
D0
16 D8+
1B
D5 3C
FE
00

sp, 6
[bp+var_6], eax
eax, _expiration
short loc_35B0A
14h
cs
near ptr get_rand
cx
si, ax
ax, 1Bh
ax, offset _Q
ds
ax
cs
near ptr sent_pro
sp, 4
[bp+var_2], ax
ax, si
ax, 18
eax, ax
edx, [bp+var_6]
edx, eax
_expiration, edx
bx, si
bx, 27
ax, _Q._A[bx]
ax, [bp+var_2]
short loc_35B0A
ax, 1
short loc_35B0C

loc_35B0A: ; CODE XREF: check_dongle+1Fj
; check_dongle+5Ej
xor
ax, ax
loc_35B0C: ; CODE XREF: check_dongle+63j
pop
si
leave
retf
check_dongle endp

А так как эта функция может вызываться слишком часто, например, перед
выполнением каждой важной возможности ПО, а обращение к донгле вообщето медленное (и из-за медленного принтерного порта, и из-за медленного MCU
в донгле), так что они, наверное, добавили возможность пропускать проверку
донглы слишком часто, используя текущее время в функции biostime().
Функция get_rand() использует стандартную функцию Си:
seg030:01BF
seg030:01BF
seg030:01BF
seg030:01BF

get_rand proc far ; CODE XREF: check_dongle+25p
arg_0

= word ptr

6

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1092
seg030:01BF
seg030:01C0
seg030:01C2
seg030:01C7
seg030:01CB
seg030:01D0
seg030:01D4
seg030:01DA
seg030:01DC
seg030:01DF
seg030:01E0
seg030:01E0

55
8B
9A
66
66
66
66
66
66
5D
CB

EC
3D
0F
0F
0F
BB
99
F7

21
BF
BF
AF
00
FB

push
mov
00+
call
C0
movsx
56+
movsx
C2
imul
80+
mov
cdq
idiv
pop
retf
get_rand endp

bp
bp, sp
_rand
eax, ax
edx, [bp+arg_0]
eax, edx
ebx, 8000h
ebx
bp

Так что текстовая строка выбирается случайно, отправляется в донглу и результат хеширования сверяется с корректным значением.
Текстовые строки, похоже, составлялись так же случайно, во время разработки ПО.
И вот как вызывается главная процедура проверки донглы:
seg033:087B 9A 45 01 96+
call
check_dongle
seg033:0880 0B C0
or
ax, ax
seg033:0882 74 62
jz
short OK
seg033:0884 83 3E 60 42+
cmp
word_620E0, 0
seg033:0889 75 5B
jnz
short OK
seg033:088B FF 06 60 42
inc
word_620E0
seg033:088F 1E
push
ds
seg033:0890 68 22 44
push
offset aTrupcRequiresA ;
"This Software Requires a Software Lock\n"
seg033:0893 1E
push
ds
seg033:0894 68 60 E9
push
offset byte_6C7E0 ; dest
seg033:0897 9A 79 65 00+
call
_strcpy
seg033:089C 83 C4 08
add
sp, 8
seg033:089F 1E
push
ds
seg033:08A0 68 42 44
push
offset aPleaseContactA ; "Please Contact
..."
seg033:08A3 1E
push
ds
seg033:08A4 68 60 E9
push
offset byte_6C7E0 ; dest
seg033:08A7 9A CD 64 00+
call
_strcat

Заставить работать программу без донглы очень просто: просто заставить
функцию check_dongle() возвращать всегда 0.
Например, вставив такой код в самом её начале:
mov ax,0
retf

Наблюдательный читатель может заметить, что функция Си strcpy() имеет 2
аргумента, но здесь мы видим, что передается 4:
seg033:088F 1E
push
seg033:0890 68 22 44
push
"This Software Requires a Software Lock\n"

ds
offset aTrupcRequiresA ;

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1093
seg033:0893
seg033:0894
seg033:0897
seg033:089C

1E
68 60 E9
9A 79 65 00+
83 C4 08

push
push
call
add

ds
offset byte_6C7E0 ; dest
_strcpy
sp, 8

Это связано с моделью памяти в MS-DOS. Об этом больше читайте здесь: 10.6
(стр. 1274).
Так что, strcpy(), как и любая другая функция принимающая указатель (-и) в
аргументах, работает с 16-битными парами.
Вернемся к нашему примеру. DS сейчас указывает на сегмент данных размещенный в исполняемом файле, там, где хранится текстовая строка.
В функции sent_pro() каждый байт строки загружается на
seg030:00EF: инструкция LES загружает из переданного аргумента пару ES:BX
одновременно. MOV на seg030:00F5 загружает байт из памяти, на который указывает пара ES:BX.

8.7. Случай с зашифрованной БД #1
(Эта часть впервые появилась в моем блоге 26-Aug-2015. Обсуждение: https:
//news.ycombinator.com/item?id=10128684.)

8.7.1. Base64 и энтропия
Мне достался XML-файл, содержащий некоторые зашифрованные данные. Вероятно, что-то связанное с заказми и/или с информацией о клиентах.

1
yjmxhXUbhB/5MV45chPsXZWAJwIh1S0aD9lFn3XuJMSxJ3/E+⤦
Ç UE3hsnH

2
0KGe/wnypFBjsy+U0C2P9fC5nDZP3XDZLMPCRaiBw9OjIk6Tu5U⤦
Ç =

3
mqkXfdzvQKvEArdzh+zD9oETVGBFvcTBLs2ph1b5bYddExzp

4
FCx6JhIDqnESyT3HAepyE1BJ3cJd7wCk+APCRUeuNtZdpCvQ2MR/7⤦
Ç kLXtfUHuA==

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1094
...

Файл доступен здесь.
Это явно данные закодированные в base64, потому что все строки состоят из
латинских символов, цифр, и символов плюс (+) и слэш (/). Могут быть еще
два выравнивающих символа (=), но они никогда не встречаются в середине
строки. Зная эти свойства base64, такие строки легко распозновать.
Попробуем декодировать эти блоки и вычислить их энтропии (9.2 (стр. 1215))
при помощи Wolfram Mathematica:
In[]:= ListOfBase64Strings =
Map[First[#[[3]]] &, Cases[Import["encrypted.xml"], XMLElement["Data", _,⤦
Ç _], Infinity]];
In[]:= BinaryStrings =
Map[ImportString[#, {"Base64", "String"}] &, ListOfBase64Strings];
In[]:= Entropies = Map[N[Entropy[2, #]] &, BinaryStrings];
In[]:= Variance[Entropies]
Out[]= 0.0238614

Разброс (variance) низкий. Это означает, что значения энтропии не очень отличаются друг от друга. Это видно на графике:
In[]:= ListPlot[Entropies]

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1095
Большинство значений между 5.0 и 5.4. Это свидетельство того что данные
сжаты и/или зашифрованы.
Чтобы понять разброс (variance), подсчитаем энтропии всех строк в книге Конана Дойля The Hound of the Baskervilles:
In[]:= BaskervillesLines = Import["http://www.gutenberg.org/cache/epub⤦
Ç /2852/pg2852.txt", "List"];
In[]:= EntropiesT = Map[N[Entropy[2, #]] &, BaskervillesLines];
In[]:= Variance[EntropiesT]
Out[]= 2.73883
In[]:= ListPlot[EntropiesT]

Большинство значений находится вокруг 4, но есть также ме́ ньшие значения,
и они повлияли на конечное значение разброса.
Вероятно, самые короткие строки имеют ме́ ньшую энтропию, попробуем короткую строку из книги Конан Дойля:
In[]:= Entropy[2, "Yes, sir."] // N
Out[]= 2.9477

Попробуем еще ме́ ньшую:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1096
In[]:= Entropy[2, "Yes"] // N
Out[]= 1.58496
In[]:= Entropy[2, "No"] // N
Out[]= 1.

8.7.2. Данные сжаты?
ОК, наши данные сжаты и/или зашифрованы. Сжаты ли? Почти все компрессоры данных помещают некоторый заголовок в начале, сигнатуру или что-то
вроде этого. Как видим, здесь ничего такого нет в начале каждого блока. Все
еще возможно что это какой-то самодельный компрессор, но они очень редки. С другой стороны, самодельные криптоалгоритмы попадаются часто, потому что их куда легче заставить работать. Даже примитивные криптосистемы
без ключей, как memfrob()18 и ROT13 нормально работают без ошибок. А чтобы написать свой компрессор с нуля, используя только фантазию и воображение, так что он будет работать без ошибок, это серьезная задача. Некоторые
программисты реализуют ф-ции сжатия данных по учебникам, но это также
редкость. Наиболее популярные способы это: 1) просто взять опен-сорсную
библиотеку вроде zlib; 2) скопипастить что-то откуда-то. Опен-сорсные алгоритмы сжатия данных обычно добавляют какой-то заголовок, и точно так же
делают алгоритмы с сайтов вроде http://www.codeproject.com/.

8.7.3. Данные зашифрованы?
Основные алгоритмы шифрования обрабатывают данные блоками. DES — по 8
байт, AES — по 16 байт. Если входной буфер не делится без остатка на длину
блока, он дополняется нулями (или еще чем-то), так что зашифрованные данные будут выровнены по размеру блока этого алгоритма шифрования. Это не
наш случай.
Используя Wolfram Mathematica, я проанализировал длины блоков:
In[]:=
Out[]=
44 −>
30 −>

Counts[Map[StringLength[#] &, BinaryStrings]]
1858, 38 −> 1235, 36 −> 699, 46 −> 1151, 40 −> 1784,
1558, 50 −> 366, 34 −> 291, 32 −> 74, 56 −> 15, 48 −> 716,
13, 52 −> 156, 54 −> 71, 60 −> 3, 58 −> 6, 28 −> 4|>

1858 блоков имеют длину 42 байта, 1235 блоков имеют длину 38 байт, итд.
Я сделал график:
ListPlot[Counts[Map[StringLength[#] &, BinaryStrings]]]
18 http://linux.die.net/man/3/memfrob

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1097

Так что большинство блоков имеют размер между ~36 и ~48. Вот еще что стоит
отметить: длины всех блоков четные. Нет ни одного блока с нечетной длиной.
Хотя, существуют потоковые шифры, которые работают на уровне байт, или
даже на уровне бит.

8.7.4. CryptoPP
Программа, при помощи которой можно листать зашифрованную базу написана на C# и код на .NET сильно обфусцирован. Тем не менее, имеется DLL
с кодом для x86, который, после краткого рассмотрения, имеет части из популярной опен-сорсной библиотеки CryptoPP! (Я просто нашел внутри строки
«CryptoPP».) Теперь легко найти все ф-ции внутри DLL, потому что библиотека
CryptoPP опен-сорсная.
Библиотека CryptoPP имеет множество ф-ций шифрования, включая AES (AKA
Rijndael). Современные x86-процессоры имеют AES-инструкции вроде AESENC,
AESDEC и AESKEYGENASSIST 19 . Они не производят полного шифрования/дешифрования, но они делают бо́ льшую часть работы. И новые версии CryptoPP используют их. Например, здесь: 1, 2. К моему удивлению, во время дешифрования, инструкция, AESENC исполняется, а AESDEC — нет (я это проверил при помощи моей утилиты tracer, но можно использовать любой отладчик). Я проверил,
поддерживает ли мой процессор AES-инструкции. Некоторые процессоры Intel
i3 не поддерживают. И если нет, библиотека CryptoPP применяет ф-ции AES ре19 https://en.wikipedia.org/wiki/AES_instruction_set

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1098
ализованные старым способом 20 . Но мой процессор поддерживает их. Почему
AESDEC не исполняется? Почему программа использует шифрование AES чтобы
дешифровать БД?
ОК, найти ф-цию шифрования блока это не проблема. Она называется
CryptoPP::Rijndael::Enc::ProcessAndXorBlock: src, и она может вызывать другую
ф-цию:
Rijndael::Enc::AdvancedProcessBlocks() src, которая, в свою очередь, может вызывать две ф-ции ( AESNI_Enc_Block and AESNI_Enc_4_Blocks ) которые имеют
инструкции AESENC.
Так что, судя по внутренностям CryptoPP,
CryptoPP::Rijndael::Enc::ProcessAndXorBlock() шифрует один 16-байтный блок. Попробуем установить брякпоинт на ней и посмотрим, что происходит во время
дешифрования. Я снова использую мою простую утилиту tracer. Сейчас программа должна дешифровать первый блок. О, и кстати, вот первый блок сконвертированный из кодировки base64 в шестнадцатеричный вид, будем держать его под рукой:
00000000: CA 39 B1 85 75 1B 84 1F
Ç ..]
00000010: 95 80 27 02 21 D5 2D 1A
Ç .
00000020: B1 27 7F 84 FE 41 37 86

F9 31 5E 39 72 13 EC 5D

.9..u....1^9r⤦

0F D9 45 9F 75 EE 24 C4

..'.!.−...E.u.$⤦

C9 C0

.'...A7...

А еще вот аргументы ф-ции из исходных файлов CryptoPP:
size_t Rijndael::Enc::AdvancedProcessBlocks(const byte ∗inBlocks, const ⤦
Ç byte ∗xorBlocks, byte ∗outBlocks, size_t length, word32 flags);

Так что у него 5 аргументов. Возможные флаги это:
enum {BT_InBlockIsCounter=1, BT_DontIncrementInOutPointers=2, BT_XorInput⤦
Ç =4, BT_ReverseDirection=8, BT_AllowParallel=16} ⤦
Ç FlagsForAdvancedProcessBlocks;

ОК, запускаем tracer на ф-ции ProcessAndXorBlock():
... tracer.exe −l:filename.exe bpf=filename.exe!0x4339a0,args:5,dump_args:0⤦
Ç x10
Warning: no tracer.cfg file.
PID=1984|New process software.exe
no module registered with image base 0x77320000
no module registered with image base 0x76e20000
no module registered with image base 0x77320000
no module registered with image base 0x77220000
Warning: unknown (to us) INT3 breakpoint at ntdll.dll!⤦
Ç LdrVerifyImageMatchesChecksum+0x96c (0x776c103b)
(0) software.exe!0x4339a0(0x38b920, 0x0, 0x38b978, 0x10, 0x0) (called from ⤦
Ç software.exe!.text+0x33c0d (0x13e4c0d))
20 https://github.com/mmoss/cryptopp/blob/2772f7b57182b31a41659b48d5f35a7b6cedd34d/src/
rijndael.cpp#L355

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1099
Argument 1/5
0038B920: 01 00 00 00 FF FF FF FF−79 C1 69 0B 67 C1 04 7D "........y.i.g⤦
Ç ..}"
Argument 3/5
0038B978: CD CD CD CD CD CD CD CD−CD CD CD CD CD CD CD CD ⤦
Ç "................"
(0) software.exe!0x4339a0() −> 0x0
Argument 3/5 difference
00000000: C7 39 4E 7B 33 1B D6 1F−B8 31 10 39 39 13 A5 5D ".9N⤦
Ç {3....1.99..]"
(0) software.exe!0x4339a0(0x38a828, 0x38a838, 0x38bb40, 0x0, 0x8) (called ⤦
Ç from software.exe!.text+0x3a407 (0x13eb407))
Argument 1/5
0038A828: 95 80 27 02 21 D5 2D 1A−0F D9 45 9F 75 EE 24 C4 "..'.!.−...E.u.$⤦
Ç ."
Argument 2/5
0038A838: B1 27 7F 84 FE 41 37 86−C9 C0 00 CD CD CD CD CD ".'...A7⤦
Ç ........."
Argument 3/5
0038BB40: CD CD CD CD CD CD CD CD−CD CD CD CD CD CD CD CD ⤦
Ç "................"
(0) software.exe!0x4339a0() −> 0x0
(0) software.exe!0x4339a0(0x38b920, 0x38a828, 0x38bb30, 0x10, 0x0) (called ⤦
Ç from software.exe!.text+0x33c0d (0x13e4c0d))
Argument 1/5
0038B920: CA 39 B1 85 75 1B 84 1F−F9 31 5E 39 72 13 EC 5D ".9..u....1^9r⤦
Ç ..]"
Argument 2/5
0038A828: 95 80 27 02 21 D5 2D 1A−0F D9 45 9F 75 EE 24 C4 "..'.!.−...E.u.$⤦
Ç ."
Argument 3/5
0038BB30: CD CD CD CD CD CD CD CD−CD CD CD CD CD CD CD CD ⤦
Ç "................"
(0) software.exe!0x4339a0() −> 0x0
Argument 3/5 difference
00000000: 45 00 20 00 4A 00 4F 00−48 00 4E 00 53 00 00 00 "E. .J.O.H.N.S⤦
Ç ..."
(0) software.exe!0x4339a0(0x38b920, 0x0, 0x38b978, 0x10, 0x0) (called from ⤦
Ç software.exe!.text+0x33c0d (0x13e4c0d))
Argument 1/5
0038B920: 95 80 27 02 21 D5 2D 1A−0F D9 45 9F 75 EE 24 C4 "..'.!.−...E.u.$⤦
Ç ."
Argument 3/5
0038B978: 95 80 27 02 21 D5 2D 1A−0F D9 45 9F 75 EE 24 C4 "..'.!.−...E.u.$⤦
Ç ."
(0) software.exe!0x4339a0() −> 0x0
Argument 3/5 difference
00000000: B1 27 7F E4 9F 01 E3 81−CF C6 12 FB B9 7C F1 BC ⤦
Ç ".'...........|.."
PID=1984|Process software.exe exited. ExitCode=0 (0x0)

Тут мы можем увидеть входы в ф-цию ProcessAndXorBlock(), и выходы из нее.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1100
Это вывод из ф-ции во время первого вызова:
00000000: C7 39 4E 7B 33 1B D6 1F−B8 31 10 39 39 13 A5 5D ".9N⤦
Ç {3....1.99..]"

Затем ф-ция ProcessAndXorBlock() вызывается с блоком нулевого размера,
но с флагом 8 (BT_ReverseDirection).
Второй вызов:
00000000: 45 00 20 00 4A 00 4F 00−48 00 4E 00 53 00 00 00 "E. .J.O.H.N.S⤦
Ç ..."

Ох, тут есть знакомая нам строка!
Третий вызов:
00000000: B1 27 7F E4 9F 01 E3 81−CF C6 12 FB B9 7C F1 BC ⤦
Ç ".'...........|.."

Первый вывод очень похож на первые 16 байт зашифрованного буфера.
Вывод первого вызова ProcessAndXorBlock():
00000000: C7 39 4E 7B 33 1B D6 1F−B8 31 10 39 39 13 A5 5D ".9N⤦
Ç {3....1.99..]"

Первые 16 байт зашифрованного буфера:
00000000: CA 39 B1 85 75 1B 84 1F F9 31 5E 39 72 13 EC 5D

.9..u....1^9r..]

Тут слишком много одинаковых байт! Как так получается, что результат шифрования AES может быть очень похож на шифрованный буфер в то время как
это не шифрование, а скорее дешифрование?

8.7.5. Режим обратной связи по шифротексту
Ответ это CFB21 : в этом режиме, алгоритм AES используются не как алгоритм
шифрования, а как устройство для генерации случайных данных с криптографической стойкостью. Само шифрование производится используя простую
операцию XOR.
Вот алгоритм шифрования (иллюстрации взяты из Wikipedia):
21 Режим

обратной связи по шифротексту (Cipher Feedback)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1101

И дешифрования:

Посмотрим: операция шифрования в AES генерирует 16 байт (или 128 бит) случайных данных, которые можно использовать во время применения операции
XOR, но кто заставляет нас использовать все 16 байт? Если на последней итерации у нас 1 байт данных, давайте про-XOR-им 1 байт данных с 1 байтом
сгенерированных случайных данных? Это приводит к важному свойству режима CFB: данные не нужно выравнивать, данные произвольного размера могут
быть зашифрованы и дешифрованы.
О, и вот почему все шифрованные блоки не выровнены. И вот почему инструкция AESDEC никогда не вызывается.
Давайте попробуем дешифровать первый блок вручную, используя Питон. Режим CFB также использует IV, как seed для CSPRNG22 . В нашем случае, IV это
блок, который шифруется на первой итерации:
22 Криптографически стойкий генератор псевдослучайных чисел (cryptographically secure
pseudorandom number generator)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1102
0038B920: 01 00 00 00 FF FF FF FF−79 C1 69 0B 67 C1 04 7D "........y.i.g⤦
Ç ..}"

О, и нам нужно также восстановить ключ шифрования. В DLL есть AESKEYGENASSIST,
и она вызывается, и используется в ф-ции
Rijndael::Base::UncheckedSetKey(): src. Её легко найти в IDA и установить брякпойнт. Посмотрим:
... tracer.exe −l:filename.exe bpf=filename.exe!0x435c30,args:3,dump_args:0⤦
Ç x10
Warning: no tracer.cfg file.
PID=2068|New process software.exe
no module registered with image base 0x77320000
no module registered with image base 0x76e20000
no module registered with image base 0x77320000
no module registered with image base 0x77220000
Warning: unknown (to us) INT3 breakpoint at ntdll.dll!⤦
Ç LdrVerifyImageMatchesChecksum+0x96c (0x776c103b)
(0) software.exe!0x435c30(0x15e8000, 0x10, 0x14f808) (called from software.⤦
Ç exe!.text+0x22fa1 (0x13d3fa1))
Argument 1/3
015E8000: CD C5 7E AD 28 5F 6D E1−CE 8F CC 29 B1 21 88 8E "..~.(_m....)⤦
Ç .!.."
Argument 3/3
0014F808: 38 82 58 01 C8 B9 46 00−01 D1 3C 01 00 F8 14 00 "8.X...F⤦
Ç ... removed».
Немного поэкспериментировав с этой функцией, мы быстро понимаем, что проблема именно в ней. Она вызывается из функции chckpass() — одна из функций
проверяющих пароль.
В начале, давайте убедимся, что мы на верном пути:
Запускаем tracer:
tracer64.exe −a:disp+work.exe bpf=disp+work.exe!chckpass,args:3,unicode
PID=2236|TID=2248|(0) disp+work.exe!chckpass (0x202c770, L"Brewered1

Ç
", 0x41) (called from 0x1402f1060 (disp+work.⤦
Ç exe!usrexist+0x3c0))
PID=2236|TID=2248|(0) disp+work.exe!chckpass −> 0x35

Функции вызываются так: syssigni() -> DyISigni() -> dychkusr() -> usrexist() ->
chckpass().
Число 0x35 возвращается из chckpass() в этом месте:
.text:00000001402ED567 loc_1402ED567:
chckpass+B4
.text:00000001402ED567
.text:00000001402ED56A
.text:00000001402ED56F
.text:00000001402ED572
.text:00000001402ED578
.text:00000001402ED57B
.text:00000001402ED581
usr02_readonly
.text:00000001402ED583
.text:00000001402ED586
Ç password_attempt_limit_exceeded
.text:00000001402ED58B
.text:00000001402ED58D
.text:00000001402ED58F
.text:00000001402ED594
.text:00000001402ED598

; CODE XREF:
mov
call
cmp
jz
cmp
jz
xor

rcx, rbx
; usr02
password_idle_check
eax, 33h
loc_1402EDB4E
eax, 36h
loc_1402EDB3D
edx, edx
;

mov
call

rcx, rbx


test
jz
mov
add
pop

al, al
short loc_1402ED5A0
eax, 35h
rsp, 60h
r14

; usr02

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1140
.text:00000001402ED59A
.text:00000001402ED59C
.text:00000001402ED59D
.text:00000001402ED59E
.text:00000001402ED59F

pop
pop
pop
pop
retn

r12
rdi
rsi
rbx

Отлично, давайте проверим:
tracer64.exe −a:disp+work.exe bpf=disp+work.exe!⤦
Ç password_attempt_limit_exceeded,args:4,unicode,rt:0
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded (0⤦
Ç x202c770, 0, 0x257758, 0) (called from 0x1402ed58b (disp+work.exe!⤦
Ç chckpass+0xeb))
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded −> 1
PID=2744|TID=360|We modify return value (EAX/RAX) of this function to 0
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded (0⤦
Ç x202c770, 0, 0, 0) (called from 0x1402e9794 (disp+work.exe!chngpass+0⤦
Ç xe4))
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded −> 1
PID=2744|TID=360|We modify return value (EAX/RAX) of this function to 0

Великолепно! Теперь мы можем успешно залогиниться.
Кстати, мы можем сделать вид что вообще забыли пароль, заставляя chckpass()
всегда возвращать ноль, и этого достаточно для отключения проверки пароля:
tracer64.exe −a:disp+work.exe bpf=disp+work.exe!chckpass,args:3,unicode,rt⤦
Ç :0
PID=2744|TID=360|(0) disp+work.exe!chckpass (0x202c770, L"bogus

Ç
", 0x41) (called from 0x1402f1060 (disp+work.⤦
Ç exe!usrexist+0x3c0))
PID=2744|TID=360|(0) disp+work.exe!chckpass −> 0x35
PID=2744|TID=360|We modify return value (EAX/RAX) of this function to 0

Что еще можно сказать, бегло анализируя функцию
password_attempt_limit_exceeded(), это то, что в начале можно увидеть следующий вызов:
lea
call
test
jz
movzx
cmp
jz
cmp
jz
cmp
jnz

rcx, aLoginFailed_us ; "login/failed_user_auto_unlock"
sapgparam
rax, rax
short loc_1402E19DE
eax, word ptr [rax]
ax, 'N'
short loc_1402E19D4
ax, 'n'
short loc_1402E19D4
ax, '0'
short loc_1402E19DE

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1141
Очевидно, функция sapgparam() используется чтобы узнать значение какойлибо переменной конфигурации. Эта функция может вызываться из 1768 разных мест.
Вероятно, при помощи этой информации, мы можем легко находить те места
кода, на которые влияют определенные переменные конфигурации.
Замечательно! Имена функций очень понятны, куда понятнее чем в Oracle
RDBMS.
По всей видимости, процесс disp+work весь написан на Си++. Должно быть,
он был переписан не так давно?

8.11. Oracle RDBMS
8.11.1. Таблица V$VERSION в Oracle RDBMS
Oracle RDBMS 11.2 это очень большая программа, основной модуль oracle.exe
содержит около 124 тысячи функций. Для сравнения, ядро Windows 7 x64 (ntoskrnl.exe)
— около 11 тысяч функций, а ядро Linux 3.9.8 (с драйверами по умолчанию) —
31 тысяч функций.
Начнем с одного простого вопроса. Откуда Oracle RDBMS берет информацию,
когда мы в SQL*Plus пишем вот такой вот простой запрос:
SQL> select ∗ from V$VERSION;

И получаем:
BANNER
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 − Production
PL/SQL Release 11.2.0.1.0 − Production
CORE
11.2.0.1.0
Production
TNS for 32−bit Windows: Version 11.2.0.1.0 − Production
NLSRTL Version 11.2.0.1.0 − Production

Начнем. Где в самом Oracle RDBMS мы можем найти строку V$VERSION?
Для win32-версии, эта строка имеется в файле oracle.exe, это легко увидеть.
Но мы также можем использовать объектные (.o) файлы от версии Oracle RDBMS
для Linux, потому что в них сохраняются имена функций и глобальных переменных, а в oracle.exe для win32 этого нет.
Итак, строка V$VERSION имеется в файле kqf.o, в самой главной Oracle-библиотеке
libserver11.a.
Ссылка на эту текстовую строку имеется в таблице kqfviw, размещенной в
этом же файле kqf.o:
Листинг 8.10: kqf.o
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1142
.rodata:0800C4A0 kqfviw dd 0Bh
; DATA XREF: kqfchk:loc_8003A6D
.rodata:0800C4A0
; kqfgbn+34
.rodata:0800C4A4
dd offset _2__STRING_10102_0 ; "GV$WAITSTAT"
.rodata:0800C4A8
dd 4
.rodata:0800C4AC
dd offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C4B0
dd 3
.rodata:0800C4B4
dd 0
.rodata:0800C4B8
dd 195h
.rodata:0800C4BC
dd 4
.rodata:0800C4C0
dd 0
.rodata:0800C4C4
dd 0FFFFC1CBh
.rodata:0800C4C8
dd 3
.rodata:0800C4CC
dd 0
.rodata:0800C4D0
dd 0Ah
.rodata:0800C4D4
dd offset _2__STRING_10104_0 ; "V$WAITSTAT"
.rodata:0800C4D8
dd 4
.rodata:0800C4DC
dd offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C4E0
dd 3
.rodata:0800C4E4
dd 0
.rodata:0800C4E8
dd 4Eh
.rodata:0800C4EC
dd 3
.rodata:0800C4F0
dd 0
.rodata:0800C4F4
dd 0FFFFC003h
.rodata:0800C4F8
dd 4
.rodata:0800C4FC
dd 0
.rodata:0800C500
dd 5
.rodata:0800C504
dd offset _2__STRING_10105_0 ; "GV$BH"
.rodata:0800C508
dd 4
.rodata:0800C50C
dd offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C510
dd 3
.rodata:0800C514
dd 0
.rodata:0800C518
dd 269h
.rodata:0800C51C
dd 15h
.rodata:0800C520
dd 0
.rodata:0800C524
dd 0FFFFC1EDh
.rodata:0800C528
dd 8
.rodata:0800C52C
dd 0
.rodata:0800C530
dd 4
.rodata:0800C534
dd offset _2__STRING_10106_0 ; "V$BH"
.rodata:0800C538
dd 4
.rodata:0800C53C
dd offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C540
dd 3
.rodata:0800C544
dd 0
.rodata:0800C548
dd 0F5h
.rodata:0800C54C
dd 14h
.rodata:0800C550
dd 0
.rodata:0800C554
dd 0FFFFC1EEh
.rodata:0800C558
dd 5
.rodata:0800C55C
dd 0

Кстати, нередко, при изучении внутренностей Oracle RDBMS, появляется вопрос, почему имена функций и глобальных переменных такие странные. Вероятно, дело в том, что Oracle RDBMS очень старый продукт сам по себе и писался
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1143
на Си еще в 1980-х.
А в те времена стандарт Си гарантировал поддержку имен переменных длиной только до шести символов включительно: «6 significant initial characters in
an external identifier»36
Вероятно, таблица kqfviw содержащая в себе многие (а может даже и все)
view с префиксом V$, это служебные view (fixed views), присутствующие всегда. Бегло оценив цикличность данных, мы легко видим, что в каждом элементе таблицы kqfviw 12 32-битных полей. В IDA легко создать структуру из
12-и элементов и применить её ко всем элементам таблицы. Для версии Oracle
RDBMS 11.2, здесь 1023 элемента в таблице, то есть, здесь описываются 1023
всех возможных fixed view. Позже, мы еще вернемся к этому числу.
Как видно, мы не очень много можем узнать чисел в этих полях. Самое первое
поле всегда равно длине строки-названия view (без терминирующего ноля).
Это справедливо для всех элементов. Но эта информация не очень полезна.
Мы также знаем, что информацию обо всех fixed views можно получить из fixed
view под названием V$FIXED_VIEW_DEFINITION (кстати, информация для этого
view также берется из таблиц kqfviw и kqfvip). Между прочим, там тоже 1023
элемента. Совпадение? Нет.
SQL> select ∗ from V$FIXED_VIEW_DEFINITION where view_name='V$VERSION';
VIEW_NAME
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
VIEW_DEFINITION
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
V$VERSION
select BANNER from GV$VERSION where inst_id = USERENV('Instance')

Итак, V$VERSION это как бы thunk view для другого, с названием GV$VERSION,
который, в свою очередь:
SQL> select ∗ from V$FIXED_VIEW_DEFINITION where view_name='GV$VERSION';
VIEW_NAME
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
VIEW_DEFINITION
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
GV$VERSION
select inst_id, banner from x$version

Таблицы с префиксом X$ в Oracle RDBMS— это также служебные таблицы, они
не документированы, не могут изменятся пользователем, и обновляются динамически.
Попробуем поискать текст
36 Draft

ANSI C Standard (ANSI X3J11/88-090) (May 13, 1988) (yurichev.com)

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1144
select BANNER from GV$VERSION where inst_id =
USERENV('Instance')

... в файле kqf.o и находим ссылку на него в таблице kqfvip:
Листинг 8.11: kqf.o
.rodata:080185A0 kqfvip dd offset _2__STRING_11126_0 ; DATA XREF: kqfgvcn+18
.rodata:080185A0
; kqfgvt+F
.rodata:080185A0
;
"select inst_id,decode(indx,1,'data bloc"...
.rodata:080185A4
dd offset kqfv459_c_0
.rodata:080185A8
dd 0
.rodata:080185AC
dd 0
...
.rodata:08019570
dd offset _2__STRING_11378_0 ;
"select BANNER from GV$VERSION where in"...
.rodata:08019574
dd offset kqfv133_c_0
.rodata:08019578
dd 0
.rodata:0801957C
dd 0
.rodata:08019580
dd offset _2__STRING_11379_0 ;
"select inst_id,decode(bitand(cfflg,1),0"...
.rodata:08019584
dd offset kqfv403_c_0
.rodata:08019588
dd 0
.rodata:0801958C
dd 0
.rodata:08019590
dd offset _2__STRING_11380_0 ;
"select STATUS , NAME, IS_RECOVERY_DEST"...
.rodata:08019594
dd offset kqfv199_c_0

Таблица, по всей видимости, имеет 4 поля в каждом элементе. Кстати, здесь
так же 1023 элемента, уже знакомое нам число.
Второе поле указывает на другую таблицу, содержащую поля этого fixed view.
Для V$VERSION, эта таблица только из двух элементов, первый это 6 и второй
это строка BANNER (число 6 это длина строки) и далее терминирующий элемент
содержащий 0 и нулевую Си-строку:
Листинг 8.12: kqf.o
.rodata:080BBAC4 kqfv133_c_0 dd 6
; DATA XREF: .rodata:08019574
.rodata:080BBAC8
dd offset _2__STRING_5017_0 ; "BANNER"
.rodata:080BBACC
dd 0
.rodata:080BBAD0
dd offset _2__STRING_0_0

Объединив данные из таблиц kqfviw и kqfvip, мы получим SQL-запросы, которые исполняются, когда пользователь хочет получить информацию из какоголибо fixed view.
Напишем программу oracle tables37 , которая собирает всю эту информацию из
объектных файлов от Oracle RDBMS под Linux.
37 yurichev.com

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1145
Для V$VERSION, мы можем найти следующее:
Листинг 8.13: Результат работы oracle tables
kqfviw_element.viewname: [V$VERSION] ?: 0x3 0x43 0x1 0xffffc085 0x4
kqfvip_element.statement: [select BANNER from GV$VERSION where inst_id = ⤦
Ç USERENV('Instance')]
kqfvip_element.params:
[BANNER]

И:
Листинг 8.14: Результат работы oracle tables
kqfviw_element.viewname: [GV$VERSION] ?: 0x3 0x26 0x2 0xffffc192 0x1
kqfvip_element.statement: [select inst_id, banner from x$version]
kqfvip_element.params:
[INST_ID] [BANNER]

Fixed view GV$VERSION отличается от V$VERSION тем, что содержит еще и поле
отражающее идентификатор instance.
Но так или иначе, мы теперь упираемся в таблицу X$VERSION. Как и прочие
X$-таблицы, она не документирована, однако, мы можем оттуда что-то прочитать:
SQL> select ∗ from x$version;
ADDR
INDX
INST_ID
−−−−−−−− −−−−−−−−−− −−−−−−−−−−
BANNER
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
0DBAF574
0
1
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 − Production
...

Эта таблица содержит дополнительные поля вроде ADDR и INDX.
Бегло листая содержимое файла kqf.o в IDA мы можем увидеть еще одну таблицу где есть ссылка на строку X$VERSION, это kqftab:
Листинг 8.15: kqf.o
.rodata:0803CAC0
.rodata:0803CAC4
.rodata:0803CAC8
.rodata:0803CACC
.rodata:0803CAD0
.rodata:0803CAD4
.rodata:0803CAD8
.rodata:0803CADC
.rodata:0803CAE0
.rodata:0803CAE4

dd
dd
dd
dd
dd
dd
dd
dd
dd
dd

9
; element number 0x1f6
offset _2__STRING_13113_0 ; "X$VERSION"
4
offset _2__STRING_13114_0 ; "kqvt"
4
4
0
4
0Ch
0FFFFC075h

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1146
.rodata:0803CAE8
.rodata:0803CAEC
.rodata:0803CAF0
.rodata:0803CAF4
.rodata:0803CAF8
.rodata:0803CAFC
.rodata:0803CB00
.rodata:0803CB04
.rodata:0803CB08
.rodata:0803CB0C
.rodata:0803CB10
.rodata:0803CB14
.rodata:0803CB18
.rodata:0803CB1C

dd
dd
dd
dd
dd
dd
dd
dd
dd
dd
dd
dd
dd
dd

3
0
7
offset _2__STRING_13115_0 ; "X$KQFSZ"
5
offset _2__STRING_13116_0 ; "kqfsz"
1
38h
0
7
0
0FFFFC09Dh
2
0

Здесь очень много ссылок на названия X$-таблиц, вероятно, на все те что имеются в Oracle RDBMS этой версии.
Но мы снова упираемся в то что не имеем достаточно информации. Не ясно,
что означает строка kqvt.
Вообще, префикс kq может означать kernel и query.
v, может быть, version, а t — type?
Сказать трудно.
Таблицу с очень похожим названием мы можем найти в kqf.o:
Листинг 8.16: kqf.o
.rodata:0808C360 kqvt_c_0
Ç 0, 0, 4, 0, 0>
.rodata:0808C360
.rodata:08042680
.rodata:0808C360
.rodata:0808C384
Ç 0, 0, 0, 4, 0, 0> ;
"INDX"
.rodata:0808C3A8
Ç 0, 0, 0, 4, 0, 0> ;
"INST_ID"
.rodata:0808C3CC
Ç 0, 0, 0, 50h, 0, 0>
"BANNER"
.rodata:0808C3F0
Ç 0, 0, 0, 0>

kqftap_param select ∗ from V$TIMER;
HSECS
−−−−−−−−−−
27295167

Листинг 8.23: вывод tracer
TID=2428|(0) oracle.exe!_ksugtm (0x0, 0xd76c5f0) (called from oracle.exe!⤦
Ç __VInfreq__qerfxFetch+0xfad (0x56bb6d5))
Argument 2/2
0D76C5F0: 38 C9
"8.

Ç "
TID=2428|(0) oracle.exe!_ksugtm () −> 0x4 (0x4)
Argument 2/2 difference
00000000: D1 7C A0 01
".|..

Ç "
TID=2428|(0) oracle.exe!_ksugtm (0x0, 0xd76c5f0) (called from oracle.exe!⤦
Ç __VInfreq__qerfxFetch+0xfad (0x56bb6d5))
Argument 2/2

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1157
0D76C5F0: 38 C9
"8.
Ç "
TID=2428|(0) oracle.exe!_ksugtm () −> 0x4 (0x4)
Argument 2/2 difference
00000000: 1E 7D A0 01
".}..
Ç "
TID=2428|(0) oracle.exe!_ksugtm (0x0, 0xd76c5f0) (called from oracle.exe!⤦
Ç __VInfreq__qerfxFetch+0xfad (0x56bb6d5))
Argument 2/2
0D76C5F0: 38 C9
"8.
Ç "
TID=2428|(0) oracle.exe!_ksugtm () −> 0x4 (0x4)
Argument 2/2 difference
00000000: BF 7D A0 01
".}..
Ç "









Действительно — значение то, что мы видим в SQL*Plus, и оно возвращается
через второй аргумент.
Посмотрим, что в функции slgcs() (Linux x86):
slgcs

proc near

var_4
arg_0

= dword ptr −4
= dword ptr 8

slgcs

push
mov
push
mov
mov
call
pop
nop
mov
mov
call
push
push
push
push
call
mov
add
mov
pop
retn
endp

ebp
ebp, esp
esi
[ebp+var_4], ebx
eax, [ebp+arg_0]
$+5
ebx
; PIC mode
ebx, offset _GLOBAL_OFFSET_TABLE_
dword ptr [eax], 0
sltrgatime64
; PIC mode
0
0Ah
edx
eax
__udivdi3
; PIC mode
ebx, [ebp+var_4]
esp, 10h
esp, ebp
ebp

(это просто вызов sltrgatime64() и деление его результата на 10 (3.10 (стр. 644)))
И в win32-версии:
_slgcs

proc near

; CODE XREF: _dbgefgHtElResetCount+15

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1158
; _dbgerRunActions+1528

_slgcs

db
nop
push
mov
mov
mov
call
mov
mov
mul
shr
mov
mov
pop
retn
endp

66h
ebp
ebp, esp
eax, [ebp+8]
dword ptr [eax], 0
ds:__imp__GetTickCount@0 ; GetTickCount()
edx, eax
eax, 0CCCCCCCDh
edx
edx, 3
eax, edx
esp, ebp
ebp

Это просто результат GetTickCount()

41

поделенный на 10 (3.10 (стр. 644)).

Вуаля! Вот почему в win32-версии и версии Linux x86 разные результаты, потому что они получаются разными системными функциями ОС.
Drain по-английски дренаж, отток, водосток. Таким образом, возможно имеется ввиду подключение определенного столбца системной таблице к функции.
Добавим поддержку таблицы kqfd_tab_registry_0 в oracle tables42 , теперь мы
можем видеть, при помощи каких функций, столбцы в системных таблицах
подключаются к значениям, например:
[X$KSUTM] [kqfd_OPN_ksutm_c] [kqfd_tabl_fetch] [NULL] [NULL] [⤦
Ç kqfd_DRN_ksutm_c]
[X$KSUSGIF] [kqfd_OPN_ksusg_c] [kqfd_tabl_fetch] [NULL] [NULL] [⤦
Ç kqfd_DRN_ksusg_c]

OPN, возможно, open, а DRN, вероятно, означает drain.

8.12. Вручную написанный на ассемблере код
8.12.1. Тестовый файл EICAR
Этот .COM-файл предназначен для тестирования антивирусов, его можно запустить в MS-DOS и он выведет такую строку: «EICAR-STANDARD-ANTIVIRUS-TESTFILE!».
Он примечателен тем, что он полностью состоит только из печатных ASCIIсимволов, следовательно, его можно набрать в любом текстовом редакторе:
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR−STANDARD−ANTIVIRUS−TEST−FILE!$H+H∗
41 MSDN
42 yurichev.com

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1159
Попробуем его разобрать:
; изначальное состояние: SP=0FFFEh, SS:[SP]=0
0100 58
pop
ax
; AX=0, SP=0
0101 35 4F 21
xor
ax, 214Fh
; AX = 214Fh and SP = 0
0104 50
push
ax
; AX = 214Fh, SP = FFFEh and SS:[FFFE] = 214Fh
0105 25 40 41
and
ax, 4140h
; AX = 140h, SP = FFFEh and SS:[FFFE] = 214Fh
0108 50
push
ax
; AX = 140h, SP = FFFCh, SS:[FFFC] = 140h and SS:[FFFE] = 214Fh
0109 5B
pop
bx
; AX = 140h, BX = 140h, SP = FFFEh and SS:[FFFE] = 214Fh
010A 34 5C
xor
al, 5Ch
; AX = 11Ch, BX = 140h, SP = FFFEh and SS:[FFFE] = 214Fh
010C 50
push
ax
010D 5A
pop
dx
; AX = 11Ch, BX = 140h, DX = 11Ch, SP = FFFEh and SS:[FFFE] = 214Fh
010E 58
pop
ax
; AX = 214Fh, BX = 140h, DX = 11Ch and SP = 0
010F 35 34 28
xor
ax, 2834h
; AX = 97Bh, BX = 140h, DX = 11Ch and SP = 0
0112 50
push
ax
0113 5E
pop
si
; AX = 97Bh, BX = 140h, DX = 11Ch, SI = 97Bh and SP = 0
0114 29 37
sub
[bx], si
0116 43
inc
bx
0117 43
inc
bx
0118 29 37
sub
[bx], si
011A 7D 24
jge
short near ptr word_10140
011C 45 49 43 ... db 'EICAR−STANDARD−ANTIVIRUS−TEST−FILE!$'
0140 48 2B
word_10140 dw 2B48h ; CD 21 (INT 21) будет здесь
0142 48 2A
dw 2A48h ; CD 20 (INT 20) будет здесь
0144 0D
db 0Dh
0145 0A
db 0Ah

Добавим везде комментарии, показывающие состояние регистров и стека после каждой инструкции.
Собственно, все эти инструкции нужны только для того чтобы исполнить следующий код:
B4
BA
CD
CD

09
1C 01
21
20

MOV
MOV
INT
INT

AH, 9
DX, 11Ch
21h
20h

INT 21h с функцией 9 (переданной в AH) просто выводит строку, адрес которой
передан в DS:DX. Кстати, строка должна быть завершена символом ’$’. Надо
полагать, это наследие CP/M и эта функция в DOS осталась для совместимости.
INT 20h возвращает управление в DOS.
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1160
Но, как видно, далеко не все опкоды этих инструкций печатные. Так что основная часть EICAR-файла это:
• подготовка нужных значений регистров (AH и DX);
• подготовка в памяти опкодов для INT 21 и INT 20;
• исполнение INT 21 и INT 20.
Кстати, подобная техника широко используется для создания шеллкодов, где
нужно создать x86-код, который будет нужно передать в виде текстовой строки.
Здесь также список всех x86-инструкций с печатаемыми опкодами: .1.6 (стр. 1321).

8.13. Демо
Демо (или демомейкинг) были великолепным упражнением в математике, программировании компьютерной графики и очень плотному программированию
на ассемблере вручную.

8.13.1. 10 PRINT CHR$(205.5+RND(1)); : GOTO 10
Все примеры здесь для .COM-файлов под MS-DOS.
В [Nick Montfort et al, 10 PRINT CHR$(205.5+RND(1)); : GOTO 10, (The MIT Press:2012)]
43
можно прочитать об одном из простейших генераторов случайных лабиринтов. Он просто бесконечно и случайно печатает символ слэша или обратный
слэш, выдавая в итоге что-то вроде:
43 Также

доступно здесь: http://trope-tank.mit.edu/10_PRINT_121114.pdf

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1161

Здесь несколько известных реализаций для 16-битного x86.
Версия 42-х байт от Trixter
Листинг взят с его сайта44 , но комментарии — автора.
00000000: B001
mov
al,1
; установить видеорежим 40x25
00000002: CD10
int
010
00000004: 30FF
xor
bh,bh
; установить видеостраницу для
вызова int 10h
00000006: B9D007
mov
cx,007D0
; вывод 2000 символов
00000009: 31C0
xor
ax,ax
0000000B: 9C
pushf
; сохранить флаги
; узнать случайное число из чипа таймера
0000000C: FA
cli
; запретить прерывания
0000000D: E643
out
043,al
; записать 0 в порт 43h
; прочитать 16-битное значение из порта 40h
0000000F: E440
in
al,040
00000011: 88C4
mov
ah,al
00000013: E440
in
al,040
00000015: 9D
popf
; разрешить прерывания
возвращая значение флага IF
00000016: 86C4
xchg
ah,al
; здесь мы имеем псевдослучайное 16-битное значение
00000018: D1E8
shr
ax,1
0000001A: D1E8
shr
ax,1
; в CF сейчас находится второй бит из значения
0000001C: B05C
mov
al,05C ;'́
; если CF=1, пропускаем следующую инструкцию
0000001E: 7202
jc
000000022
44 http://trixter.oldskool.org/2012/12/17/maze-generation-in-thirteen-bytes/

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1162
; если CF=0, загружаем
00000020: B02F
; вывод символа
00000022: B40E
00000024: CD10
00000026: E2E1
00000028: CD20

в регистр AL другой символ
mov
al,02F ;'/'
mov
int
loop
int

ah,00E
010
000000009 ; цикл в 2000 раз
020
; возврат в DOS

Псевдослучайное число на самом деле это время, прошедшее со старта системы, получаемое из чипа таймера 8253, это значение увеличивается на единицу
18.2 раза в секунду.
Записывая ноль в порт 43h, мы имеем ввиду что команда это «выбрать счетчик
0», ”counter latch”, ”двоичный счетчик” (а не значение BCD).
Прерывания снова разрешаются при помощи инструкции POPF, которая также
возвращает флаг IF.
Инструкцию IN нельзя использовать с другими регистрами кроме AL, поэтому
здесь перетасовка.
Моя попытка укоротить версию Trixter: 27 байт
Мы можем сказать, что мы используем таймер не для того чтобы получить
точное время, но псевдослучайное число, так что мы можем не тратить время
(и код) на запрещение прерываний. Еще можно сказать, что так как мы берем
бит из младшей 8-битной части, то мы можем считывать только её.
Немного укоротим код и выходит 27 байт:
00000000:
00000003:
00000005:
00000007:
00000009:
0000000B:
0000000D:
0000000F:
00000011:
; вывести
00000013:
00000015:
00000017:
; выход в
00000019:

B9D007
mov
31C0
xor
E643
out
E440
in
D1E8
shr
D1E8
shr
B05C
mov
7202
jc
B02F
mov
символ на экран
B40E
mov
CD10
int
E2EA
loop
DOS
CD20
int

cx,007D0 ;
ax,ax
;
043,al
al,040
;
ax,1
;
ax,1
al,05C
;
000000013
al,02F
;

вывести только 2000 символов
команда чипу таймера
читать 8 бит из таймера
переместить второй бит в флаг CF
подготовить '\'
подготовить '/'

ah,00E
010
000000003
020

Использование случайного мусора в памяти как источника случайных
чисел
Так как это MS-DOS, защиты памяти здесь нет вовсе, так что мы можем читать
с какого угодно адреса. И даже более того: простая инструкция LODSB будет
читать байт по адресу DS:SI, но это не проблема если правильные значения
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1163
не установлены в регистры, пусть она читает 1) случайные байты; 2) из случайного места в памяти!
Так что на странице Trixter-а45 можно найти предложение использовать LODSB
без всякой инициализации.
Есть также предложение использовать инструкцию SCASB вместо, потому что
она выставляет флаги в соответствии с прочитанным значением.
Еще одна идея насчет минимизации кода — это использовать прерывание DOS
INT 29h которое просто печатает символ на экране из регистра AL.
Это то что сделал Peter Ferrie

46

:

Листинг 8.24: Peter Ferrie: 10 байт
; AL в этом месте имеет случайное значение
00000000: AE
scasb
; CF устанавливается по результату вычитания случайного байта памяти из AL.
; так что он здесь случаен, в каком-то смысле
00000001: D6
setalc
; AL выставляется в 0xFF если CF=1 или в 0 если наоборот
00000002: 242D
and
al,02D ;'-'
; AL здесь 0x2D либо 0
00000004: 042F
add
al,02F ;'/'
; AL здесь 0x5C либо 0x2F
00000006: CD29
int
029
; вывести AL на экране
00000008: EBF6
jmps
000000000 ; бесконечный цикл

Так что можно избавиться и от условных переходов. ASCII-код обратного слэша
(«\») это 0x5C и 0x2F для слэша («/»).
Так что нам нужно конвертировать один (псевдослучайный) бит из флага CF в
значение 0x5C или 0x2F.
Это делается легко: применяя операцию «И» ко всем битам в AL (где все 8 бит
либо выставлены, либо сброшены) с 0x2D мы имеем просто 0 или 0x2D.
Прибавляя значение 0x2F к этому значению, мы получаем 0x5C или 0x2F. И
просто выводим это на экран.
Вывод
Также стоит отметить, что результат может быть разным в эмуляторе DOSBox,
Windows NT и даже MS-DOS, из-за разных условий: чип таймера может эмулироваться по-разному, изначальные значения регистров также могут быть разными.

45 http://trixter.oldskool.org/2012/12/17/maze-generation-in-thirteen-bytes/
46 http://pferrie.host22.com/misc/10print.htm

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1164

8.13.2. Множество Мандельброта
You know, if you magnify the coastline, it still
looks like a coastline, and a lot of other
things have this property. Nature has
recursive algorithms that it uses to generate
clouds and Swiss cheese and things like that.
Дональд Кнут, интервью (1993)

Множество Мандельброта это фрактал, характерное свойство которого это самоподобие.
При увеличении картинки, вы видите, что этот характерный узор повторяется
бесконечно.
Вот демо47 написанное автором по имени «Sir_Lagsalot» в 2009, рисующее множество Мандельброта, и это программа для x86 с размером файла всего 64
байта. Там только 30 16-битных x86-инструкций.
Вот что она рисует:

Попробуем разобраться, как она работает.
47 Можно

скачать здесь,

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1165
Теория
Немного о комплексных числах
Комплексное число состоит из двух чисел (вещественная (Re) и мнимая (Im).
Комплексная плоскость — это двухмерная плоскость, где любое комплексное
число может быть расположено: вещественная часть — это одна координата
и мнимая — вторая.
Некоторые базовые правила, которые нам понадобятся:
• Сложение: (a + bi) + (c + di) = (a + c) + (b + d)i
Другими словами:
Re(sum) = Re(a) + Re(b)
Im(sum) = Im(a) + Im(b)
• Умножение: (a + bi)(c + di) = (ac − bd) + (bc + ad)i
Другими словами:
Re(product) = Re(a) ⋅ Re(c) − Re(b) ⋅ Re(d)
Im(product) = Im(b) ⋅ Im(c) + Im(a) ⋅ Im(d)
• Возведение в квадрат: (a + bi)2 = (a + bi)(a + bi) = (a2 − b2 ) + (2ab)i
Другими словами:
Re(square) = Re(a)2 − Im(a)2
Im(square) = 2 ⋅ Re(a) ⋅ Im(a)
Как нарисовать множество Мандельброта
Множество Мандельброта — это набор точек, для которых рекурсивное соотношение zn+1 = zn 2 +c (где z и c это комплексные числа и c это начальное значение)
не стремится к бесконечности.
Простым русским языком:
• Перечисляем все точки на экране.
• Проверяем, является ли эта точка в множестве Мандельброта.
• Вот как проверить:
– Представим точку как комплексное число.
– Возведем в квадрат.
– Прибавим значение точки в самом начале.
– Вышло за пределы? Прерываемся, если да.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1166
– Передвигаем точку в новое место, координаты которого только что
вычислили.
– Повторять всё это некое разумное количество итераций.
• Двигающаяся точка в итоге не вышла за пределы? Тогда рисуем точку.
• Двигающаяся точка в итоге вышла за пределы?
– (Для черно-белого изображения) ничего не рисуем.
– (Для цветного изображения) преобразуем количество итераций в какойнибудь цвет. Так что цвет будет показывать, с какой скоростью точка
вышла за пределы.
Вот алгоритмы для комплексных и обычных целочисленных чисел (на языке,
отдаленно напоминающем Python):
Листинг 8.25: Для комплексных чисел
def check_if_is_in_set(P):
P_start=P
iterations=0
while True:
if (P>bounds):
break
P=P^2+P_start
if iterations > max_iterations:
break
iterations++
return iterations
# черно-белое
for each point on screen P:
if check_if_is_in_set (P) < max_iterations:
нарисовать точку
# цветное
for each point on screen P:
iterations = if check_if_is_in_set (P)
преобразовать количество итераций в цвет
нарисовать цветную точку

Целочисленная версия, это версия где все операции над комплексными числами заменены на операции с целочисленными, в соответствии с изложенными
ранее правилами.
Листинг 8.26: Для целочисленных чисел
def check_if_is_in_set(X, Y):
X_start=X
Y_start=Y
iterations=0

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1167
while True:
if (X^2 + Y^2 > bounds):
break
new_X=X^2 − Y^2 + X_start
new_Y=2∗X∗Y + Y_start
if iterations > max_iterations:
break
iterations++
return iterations
# черно-белое
for X = min_X to max_X:
for Y = min_Y to max_Y:
if check_if_is_in_set (X,Y) < max_iterations:
нарисовать точку на X, Y
# цветное
for X = min_X to max_X:
for Y = min_Y to max_Y:
iterations = if check_if_is_in_set (X,Y)
преобразовать количество итераций в цвет
нарисовать цветную точку на X,Y

Вот также исходный текст на C#, который есть в статье в Wikipedia48 , но мы
немного изменим его, чтобы он выдавал количество итераций, вместо некоторого символа 49 :
using
using
using
using

System;
System.Collections.Generic;
System.Linq;
System.Text;

namespace Mnoj
{
class Program
{
static void Main(string[] args)
{
double realCoord, imagCoord;
double realTemp, imagTemp, realTemp2, arg;
int iterations;
for (imagCoord = 1.2; imagCoord >= −1.2; imagCoord −= 0.05)
{
for (realCoord = −0.6; realCoord
|
|
|
|
|
|
v
X=0, Y=199 X=319, Y=199

; переключиться в графический видеорежим VGA 320*200*256
mov al,13h
int 10h
; в самом начале BX равен 0
; в самом начале DI равен 0xFFFE
; DS:BX (или DS:0) указывает на Program Segment Prefix в этот момент
; ... первые 4 байта которого этого CD 20 FF 9F
les ax,[bx]
; ES:AX=9FFF:20CD
FillLoop:
; установить DX в 0. CWD работает так: DX:AX = sign_extend(AX).
; AX здесь 0x20CD (в начале) или меньше 320 (когда вернемся после цикла),
; так что DX всегда будет 0.
cwd
mov ax,di
; AX это текущий указатель внутри VGA-буфера
; разделить текущий указатель на 320
mov cx,320
div cx
; DX (start_X) - остаток (столбец: 0..319); AX - результат (строка: 0..199)
sub ax,100
; AX=AX-100, так что AX (start_Y) сейчас в пределах -100..99
; DX в пределах 0..319 или 0x0000..0x013F
dec dh
; DX сейчас в пределах 0xFF00..0x003F (-256..63)
xor bx,bx
xor si,si
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1172
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

; BX (temp_X)=0; SI (temp_Y)=0
; получить максимальное количество итераций
; CX всё еще 320 здесь, так что это будет максимальным количеством итераций
MandelLoop:
mov bp,si
; BP = temp_Y
imul si,bx
; SI = temp_X*temp_Y
add si,si
; SI = SI*2 = (temp_X*temp_Y)*2
imul bx,bx
; BX = BX^2 = temp_X^2
jo MandelBreak ; переполнение?
imul bp,bp
; BP = BP^2 = temp_Y^2
jo MandelBreak ; переполнение?
add bx,bp
; BX = BX+BP = temp_X^2 + temp_Y^2
jo MandelBreak ; переполнение?
sub bx,bp
; BX = BX-BP = temp_X^2 + temp_Y^2 - temp_Y^2 = temp_X^2
sub bx,bp
; BX = BX-BP = temp_X^2 - temp_Y^2
; скорректировать масштаб:
sar bx,6
; BX=BX/64
add bx,dx
; BX=BX+start_X
; здесь temp_X = temp_X^2 - temp_Y^2 + start_X
sar si,6
; SI=SI/64
add si,ax
; SI=SI+start_Y
; здесь temp_Y = (temp_X*temp_Y)*2 + start_Y
loop MandelLoop
MandelBreak:
; CX=итерации
xchg ax,cx
; AX=итерации. записать AL в VGA-буфер на ES:[DI]
stosb
; stosb также инкрементирует DI, так что DI теперь указывает на следующую
точку в VGA-буфере
; всегда переходим, так что это вечный цикл
jmp FillLoop

Алгоритм:
• Переключаемся в режим VGA 320*200 256 цветов. 320 ∗ 200 = 64000 (0xFA00).
Каждый пиксель кодируется одним байтом, так что размер буфера 0xFA00
байт.
Он адресуется здесь при помощи пары регистров ES:DI.
ES должен быть здесь 0xA000, потому что это сегментный адрес видеобуфера, но запись числа 0xA000 в ES потребует по крайней мере 4 байта
(PUSH 0A000h / POP ES). О 16-битной модели памяти в MS-DOS, читайте
больше тут: 10.6 (стр. 1274).
Учитывая, что BX здесь 0, и Program Segment Prefix находится по нулевому
адресу, 2-байтная инструкция LES AX,[BX] запишет 0x20CD в AX и 0x9FFF
в ES.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1173
Так что программа начнет рисовать на 16 пикселей (или байт) перед видеобуфером.
Но это MS-DOS, здесь нет защиты памяти, так что запись происходит в
самый конец обычной памяти, а там, как правило, ничего важного нет.
Вот почему вы видите красную полосу шириной 16 пикселей справа. Вся
картинка сдвинута налево на 16 пикселей. Это цена экономии 2-х байт.
• Вечный цикл, обрабатывающий каждый пиксель. Наверное, самый общий
метод обойти все точки на экране это два цикла: один для X-координаты,
второй для Y-координаты.
Но тогда вам придется перемножать координаты для поиска байта в видеобуфере VGA. Автор этого демо решил сделать наоборот: перебирать
все байты в видеобуфере при помощи одного цикла вместо двух и затем
получать координаты текущей точки при помощи деления.
В итоге координаты такие: X в пределах −256..63 и Y в пределах −100..99.
Вы можете увидеть на скриншоте что картинка как бы сдвинута в правую
часть экрана. Это потому что самая большая черная дыра в форме сердца
обычно появляется на координатах 0,0 и они здесь сдвинуты вправо.
Мог ли автор просто отнять 160 от X, чтобы получилось значение в пределах −160..159? Да, но инструкция SUB DX, 160 занимает 4 байта, тогда как
DEC DH — 2 байта (которая отнимает 0x100 (256) от DX). Так что картинка
сдвинута ценой экономии еще 2-х байт.
– Проверить, является ли текущая точка внутри множества Мандельброта. Алгоритм такой же, как и описанный здесь.
– Цикл организуется инструкцией LOOP, которая использует регистр CX
как счетчик. Автор мог бы установить число итераций на какое-то
число, но не сделал этого: потому что 320 уже находится в CX (было установлено на строке 35), и это итак подходящее число как число
максимальных итераций.
Мы здесь экономим немного места, не загружая другое значение в
регистр CX.
– Здесь используется IMUL вместо MUL, потому что мы работаем со знаковыми значениями: помните, что координаты 0,0 должны быть гдето рядом с центром экрана.
Тоже самое и с SAR (арифметический сдвиг для знаковых значений):
она используется вместо SHR.
– Еще одна идея — это упростить проверку пределов. Нам бы пришлось
проверять пару координат, т.е. две переменных. Что делает автор
это трижды проверяет на переполнение: две операции возведения в
квадрат и одно прибавление. Действительно, мы ведь используем 16битные регистры, содержащие знаковые значения в пределах -32768..32767,
так что если любая из координат больше чем 32767 в процессе умножения, точка однозначно вышла за пределы, и мы переходим на метку MandelBreak.
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1174
– Здесь также имеется деление на 64 (при помощи инструкции SAR). 64
задает масштаб.
Попробуйте увеличить значение и вы получите более увеличенную
картинку, или уменьшить для меньшей.
• Мы находимся на метке MandelBreak, есть только две возможности попасть сюда: цикл закончился с CX=0 (точка внутри множества Мандельброта ); или потому что произошло переполнение (CX все еще содержит
какое-то значение). Записываем 8-битную часть CX (CL) в видеобуфер. Палитра по умолчанию грубая, тем не менее, 0 это черный: поэтому видим
черные дыры в местах где точки внутри множества Мандельброта.
Палитру можно инициализировать в начале программы, но не забывайте,
это всего лишь программа на 64 байта!
• Программа работает в вечном цикле, потому что дополнительная проверка, где остановится, или пользовательский интерфейс, это дополнительные инструкции.
Еще оптимизационные трюки:
• 1-байтная CWD используется здесь для обнуления DX вместо двухбайтной
XOR DX, DX или даже трехбайтной MOV DX, 0.
• 1-байтная XCHG AX, CX используется вместо двухбайтной MOV AX,CX. Текущее значение в AX все равно уже не нужно.
• DI (позиция в видеобуфере) не инициализирована, и будет 0xFFFE в начале 50 . Это нормально, потому что программа работает бесконечно для всех
DI в пределах 0..0xFFFF, и пользователь не может увидеть, что работала
началась за экраном (последний пиксель видеобуфера 320*200 имеет адрес 0xF9FF).
Так что некоторая часть работы на самом деле происходит за экраном.
А иначе понадобятся дополнительные инструкции для установки DI в 0;
добавить проверку на конец буфера.
Моя «исправленная» версия
Листинг 8.28: Моя «исправленная» версия
1
2
3
4
5
6
7
8
9
10

org 100h
mov al,13h
int 10h
; установить палитру
mov dx, 3c8h
mov al, 0
out dx, al
mov cx, 100h
inc dx
50 Больше

о состояниях регистров на старте: https://code.google.com/p/corkami/wiki/
InitialValues#DOS

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1175
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

l00:
mov al, cl
shl ax, 2
out dx, al ; красный
out dx, al ; зеленый
out dx, al ; синий
loop l00
push 0a000h
pop es
xor di, di
FillLoop:
cwd
mov ax,di
mov cx,320
div cx
sub ax,100
sub dx,160
xor bx,bx
xor si,si
MandelLoop:
mov bp,si
imul si,bx
add si,si
imul bx,bx
jo MandelBreak
imul bp,bp
jo MandelBreak
add bx,bp
jo MandelBreak
sub bx,bp
sub bx,bp
sar
add
sar
add

bx,6
bx,dx
si,6
si,ax

loop MandelLoop
MandelBreak:
xchg ax,cx
stosb
cmp di, 0FA00h
jb FillLoop
; дождаться нажатия любой клавиши
xor ax,ax
int 16h

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1176
64
65
66
67
68

; установить текстовый видеорежим
mov ax, 3
int 10h
; выход
int 20h

Автор сих строк попытался исправить все эти странности: теперь палитра плавная черно-белая, видеобуфер на правильном месте (строки 19..20), картинка рисуется в центре экрана (строка 30), программа в итоге заканчивается
и ждет, пока пользователь нажмет какую-нибудь клавишу (строки 58..68).
Но теперь она намного больше: 105 байт (или 54 инструкции)
51

.

Рис. 8.20: Моя «исправленная» версия
Смотрите также: маленькая программа на Си печатающая множество Мандельброта в ASCII: https://people.sc.fsu.edu/~jburkardt/c_src/mandelbrot_
ascii/mandelbrot_ascii.html
https://miyuki.github.io/2017/10/04/gcc-archaeology-1.html.
51 Можете

поэкспериментировать и сами: скачайте DosBox и NASM и компилируйте так:
nasm file.asm -fbin -o file.com

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1177

8.14. Как я переписывал 100 килобайт x86-кода
на чистый Си
То была DLL-ка с секцией кода 100 килобайт, она брала на вход многоканальный сигнал и выдавала другой многоканальный сигнал. Там много всего было
связано с обработкой сигналов. Внутри было очень много FPU-кода. Написано
по-олдскульному, так, как писали в то время, когда передача параметров через аргументы ф-ций была дорогой, и потому использовалось много глобальных переменных и массивов, почти всё хранилось в них, а ф-ции, напротив,
имели сравнительно мало аргументов, если вообще. Функции большие, их было около ста.
Тесты были, много.
Проблема была в том, что функции слишком большие и Hex-Rays неизменно
выдавал немного неверный код. Нужно было очень внимательно всё чистить
вручную. В процессе работы, я нашел в нем каких-то ошибок: 10.8.
Все 100 ф-ций декомпилировать сразу нельзя — где-то будут ошибки, тесты
не пройдут, и где вы будете искать эти ошибки? Приходится переписывать по
чуть-чуть.
В DLL-ке есть некая корневая ф-ция, скажем, ProcessMain(). Я переписываю её
на Си при помощи Hex-Rays, она запускается из обычного .exe-процесса. Все фции из DLL-ки, которые вызываются далее, у меня вызывались через указатели
на ф-ции. DLL-ка загружена, и пока они все там.
ASLR отключил, и DLL-ка каждый раз грузится по одному и тому же адресу,
потому и адреса всех ф-ций одни и те же. Важно, что и адреса глобальных
массивов тоже одни и те же
Затем переписываю ф-ции, вызывающиеся непосредственно из ProcessMain(),
затем еще ниже, итд. Таким образом, ф-ции я постепенно перетаскивал из DLL
в свою .exe. Каждый раз тестируя.
Много раз бывало и так — ф-ция слишком большая, например, несколько килобайт x86-кода, и после декомпиляции в Си, там что-то косячит, и неизвестно
где. Из IDA я экспортировал её листинг в текст на ассемблере и компилировал
при помощи обычного ассемблера (ML в MSVC). Она компилируется в .obj-файл
и прикомпилируется к главной .exe, и пока всё ОК. Затем я делил эту ф-цию
на более мелкие, здорово пригодился (когда бы еще?) опыт написания программ на чистом ассемблере в середине 90-х (руки до сих пор помнят). Если
всё работает, более мелкие ф-ции постепенно переписывал на Си при помощи
Hex-Rays, в то время как ”головная” ф-ция более высокого уровня всё еще на
ассемблере.
Интересно, что было много глобальных массивов, но границы между ними были сильно размыты. Но я вижу что есть какой-то большой кусок в секции .data,
где лежит всё подряд. Дошел до стадии, когда на Си переписано уже всё, а
все обращения к массивам происходят по адресам внутри секции .data в подгружаемой DLL-ке, впрочем, там почти не было констант. Затем, чтобы совсем
отказаться от DLL-ки, я сделал большой глобальный ”кусок” уже у себя на Си,

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1178
и вся работа с массивами шла через мой ”кусок”, при том, что все массивы всё
еще не были отделены друг от друга.
Вот реальный фрагмент оттуда, как было в начале. Значение — это адрес в
.data-секции в DLL-ке:
int
int
int
int
...

∗a_val511=0x1002B588;
∗a_val483=0x1002B590;
∗a_val481=0x1002B5B8;
∗a_val515=0x1002B6E4;

И все обращения происходят через указатели.
Потом я сделал ”кусок”:
char lump[0x1000000];
/∗ 0x1002B588
/∗ 0x1002B590
/∗ 0x1002B5B8
/∗ 0x1002B6E4
...

∗/int
∗/int
∗/int
∗/int

∗a_val511=(int∗)&lump[0x2B588];
∗a_val483=(int∗)&lump[0x2B590];
∗a_val481=(int∗)&lump[0x2B5B8];
∗a_val515=(int∗)&lump[0x2B6E4];

DLL-ку теперь можно было наконец-то отцепить и разбираться с границами
массивов. Этот процесс я хотел немного автоматизировать и использовал для
этого Pin. Я написал утилиту, которая показывала, по каким адресам в глобальном ”куске” были обращения из каждого адреса. Точнее, в каких пределах?
Так стало проще видеть границы массивов.
”На войне все средства хороши”, так что я доходил и до того, что использовал
Mathematica и Z3 для сокращения слишком длинных выражений (Hex-Rays не
всё может оптимизировать):
https://github.com/DennisYurichev/SAT_SMT_by_example/blob/master/proofs/
simplify_EN.tex.
Очень хорошим тестом было пересобрать всё под Linux при помощи GCC и заставить работать — как всегда, это было нелегко. Плюс, чтобы работало корректно и под x86 и под x64.

8.15. ”Прикуп” в игре ”Марьяж”
Знал бы прикуп — жил бы в Сочи.
Поговорка.

”Марьяж” — старая и довольно популярная версия игры в ”Преферанс” под
DOS.
Играют три игрока, каждому раздается по 10 карт, остальные 2 остаются в т.н.
”прикупе”. Начинаются торги, во время которых ”прикуп” скрыт. Он открывается после того, как один из игроков сделает ”заказ”.
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1179
Знание карт в ”прикупе” обычно имеет решающее преимущество.
Вот так в игре выглядит состояние ”торгов”, и ”прикуп” посредине, скрытый:

Рис. 8.21: ”Торги”
Попробуем ”подсмотреть” карты в ”прикупе” в этой игре.
Для начала — что мы знаем? Игра под DOS, датируется 1997-м годом. IDA показывает имена стандартных функций вроде @GetImage$q7Integert1t1t1m3Any
— это ”манглинг” типичный для Borland Pascal, что позволяет сделать вывод,
что сама игра написана на Паскале и скомпилирована Borland Pascal-ем.
Файлов около 10-и и некоторые имеют текстовую строку в заголовке ”Marriage
Image Library” — должно быть, это библиотеки спрайтов.
В IDA можно увидеть что используется функция @PutImage$q7Integert1m3Any4Word,
которая, собственно, рисует некий спрайт на экране. Она вызывается по крайней мере из 8-и мест. Чтобы узнать что происходит в каждом из этих 8-и мест,
мы можем блокировать работу каждой функции и смотреть, что будет происходить. Например, первая ф-ция имеет адрес seg002:062E, и она заканчивается
инструкцией retf 0Eh на seg002:102A. Это означает, что метод вызовов ф-ций
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1180
в Borland Pascal под DOS схож с stdcall — вызываемая ф-ция должна сама возвращать стек в состояние до того как началась передача аргументов. В самом
начале этой ф-ции вписываем инструкцию ”retf 0eh”, либо 3 байта: CA 0E 00.
Запускаем ”Марьяж” и внешне вроде бы ничего не изменилось.
Переходим ко второй ф-ции, которая активно использует @PutImage$q7Integert1m3Any4Word.
Она находится по адресу seg008:0AB5 и заканчивается инструкцией retf 0Ah.
Вписываем эту инструкцию в самом начале и запускаем:

Рис. 8.22: Карт нет
Карт не видно вообще. И видимо, эта функция их отображает, мы её заблокировали, и теперь карт не видно. Назовем эту ф-цию в IDA draw_card(). Помимо @PutImage$q7Integert1m3Any4Word, в этой ф-ции вызываются также ф-ции
@SetColor$q4Word, @SetFillStyle$q4Wordt1,
@Bar$q7Integert1t1t1, @OutTextXY$q7Integert16String.
Сама ф-ция draw_cards() (её название мы дали ей сами только что) вызывается из 4-х мест. Попробуем точно также ”блокировать” каждую ф-цию.
Когда я ”блокирую” вторую, по адресу seg008:0DF3 и запускаю программу, вижу такое:
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1181

Рис. 8.23: Все карты кроме карт игрока
Видны все карты, кроме карт игрока. Видимо, эта функция рисует карты игрока.
Я переименовываю её в IDA в draw_players_cards().
Четвертая ф-ция, вызывающая draw_cards(), находится по адресу seg008:16B3,
и когда я её ”блокирую”, я вижу в игре такое:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1182

Рис. 8.24: ”Прикупа” нет
Все карты есть, кроме ”прикупа”. Более того, эта ф-ция вызывает только draw_cards(),
и только 2 раза. Видимо эта ф-ция и отображает карты ”прикупа”. Будем рассматривать её внимательнее.
seg008:16B3
seg008:16B3
seg008:16B3
seg008:16B3
seg008:16B3
seg008:16B3
seg008:16B3
seg008:16B3
seg008:16B7
seg008:16BA
seg008:16BC
seg008:16BF
seg008:16C2
seg008:16C5
seg008:16C7
seg008:16CA

draw_prikup

proc far

var_E
var_C
arg_0

= word ptr −0Eh
= word ptr −0Ch
= byte ptr 6
enter
mov
xor
imul
mov
mov
xor
imul
mov

; CODE XREF: seg010:00B0
; sub_15098+6

0Eh, 0
al, byte_2C0EA
ah, ah
ax, 23h
[bp+var_C], ax
al, byte_2C0EB
ah, ah
ax, 0Ah
[bp+var_E], ax

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1183
seg008:16CD
cmp
[bp+arg_0], 0
seg008:16D1
jnz
short loc_1334A
seg008:16D3
cmp
byte_2BB08, 0
seg008:16D8
jz
short loc_13356
seg008:16DA
seg008:16DA loc_1334A:
; CODE XREF:
draw_prikup+1E
seg008:16DA
mov
al, byte ptr word_32084
seg008:16DD
mov
byte_293AD, al
seg008:16E0
mov
al, byte ptr word_32086
seg008:16E3
mov
byte_293AC, al
seg008:16E6
seg008:16E6 loc_13356:
; CODE XREF:
draw_prikup+25
seg008:16E6
mov
al, byte_293AC
seg008:16E9
xor
ah, ah
seg008:16EB
push
ax
seg008:16EC
mov
al, byte_293AD
seg008:16EF
xor
ah, ah
seg008:16F1
push
ax
seg008:16F2
push
[bp+var_C]
seg008:16F5
push
[bp+var_E]
seg008:16F8
cmp
[bp+arg_0], 0
seg008:16FC
jnz
short loc_13379
seg008:16FE
cmp
byte_2BB08, 0
seg008:1703
jnz
short loc_13379
seg008:1705
mov
al, 0
seg008:1707
jmp
short loc_1337B
seg008:1709 ;
--------------------------------------------------------------------------seg008:1709
seg008:1709 loc_13379:
; CODE XREF:
draw_prikup+49
seg008:1709
; draw_prikup+50
seg008:1709
mov
al, 1
seg008:170B
seg008:170B loc_1337B:
; CODE XREF:
draw_prikup+54
seg008:170B
push
ax
seg008:170C
push
cs
seg008:170D
call
near ptr draw_card
seg008:1710
mov
al, byte_2C0EA
seg008:1713
xor
ah, ah
seg008:1715
mov
si, ax
seg008:1717
shl
ax, 1
seg008:1719
add
ax, si
seg008:171B
add
ax, [bp+var_C]
seg008:171E
mov
[bp+var_C], ax
seg008:1721
cmp
[bp+arg_0], 0
seg008:1725
jnz
short loc_1339E
seg008:1727
cmp
byte_2BB08, 0
seg008:172C
jz
short loc_133AA
seg008:172E
seg008:172E loc_1339E:
; CODE XREF:
draw_prikup+72
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1184
seg008:172E
mov
al, byte ptr word_32088
seg008:1731
mov
byte_293AD, al
seg008:1734
mov
al, byte ptr word_3208A
seg008:1737
mov
byte_293AC, al
seg008:173A
seg008:173A loc_133AA:
; CODE XREF:
draw_prikup+79
seg008:173A
mov
al, byte_293AC
seg008:173D
xor
ah, ah
seg008:173F
push
ax
seg008:1740
mov
al, byte_293AD
seg008:1743
xor
ah, ah
seg008:1745
push
ax
seg008:1746
push
[bp+var_C]
seg008:1749
push
[bp+var_E]
seg008:174C
cmp
[bp+arg_0], 0
seg008:1750
jnz
short loc_133CD
seg008:1752
cmp
byte_2BB08, 0
seg008:1757
jnz
short loc_133CD
seg008:1759
mov
al, 0
seg008:175B
jmp
short loc_133CF
seg008:175D ;
--------------------------------------------------------------------------seg008:175D
seg008:175D loc_133CD:
; CODE XREF:
draw_prikup+9D
seg008:175D
; draw_prikup+A4
seg008:175D
mov
al, 1
seg008:175F
seg008:175F loc_133CF:
; CODE XREF:
draw_prikup+A8
seg008:175F
push
ax
seg008:1760
push
cs
seg008:1761
call
near ptr draw_card ; prikup #2
seg008:1764
leave
seg008:1765
retf
2
seg008:1765 draw_prikup
endp

Интересно посмотреть, как именно вызывается draw_prikup(). У нее только
один аргумент.
Иногда она вызывается с аргументом 1:
...
seg010:084C
seg010:084E
...

push
call

1
draw_prikup

А иногда с аргументом 0, причем вот в таком контексте, где уже есть другая
знакомая функция:
seg010:0067
seg010:0069
seg010:006C
seg010:006D

push
mov
push
call

1
al, byte_31F41
ax
sub_12FDC

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1185
seg010:0072
seg010:0074
seg010:0077
seg010:0078
seg010:007D
seg010:007F
seg010:0082
seg010:0083
seg010:0088
seg010:008A
seg010:008D
seg010:008E
seg010:0093
seg010:0095
seg010:0098
seg010:0099
seg010:009E
seg010:00A0
seg010:00A3
seg010:00A4
seg010:00A9
seg010:00AE
seg010:00B0
seg010:00B5

push
mov
push
call
push
mov
push
call
push
mov
push
call
push
mov
push
call
push
mov
push
call
call
push
call
mov

1
al, byte_31F41
ax
draw_players_cards
2
al, byte_31F42
ax
sub_12FDC
2
al, byte_31F42
ax
draw_players_cards
3
al, byte_31F43
ax
sub_12FDC
3
al, byte_31F43
ax
draw_players_cards
sub_1257A
0
draw_prikup
byte_2BB95, 0

Так что единственный аргумент у draw_prikup() может быть или 0 или 1, т.е.,
это, возможно, булевый тип. На что он влияет внутри самой ф-ции? При ближайшем рассмотрении видно, что входящий 0 или 1 передается в draw_card(),
т.е., у последней тоже есть булевый аргумент. Помимо всего прочего, если передается 1, то по адресам seg008:16DA и seg008:172E копируются несколько
байт из одной группы глобальных переменных в другую.
Эксперимент: здесь 4 раза сравнивается единственный аргумент с 0 и далее
следует JNZ. Что если сравнение будет происходить с 1, и, таким образом, работа функции draw_prikup() будет обратной? Патчим и запускаем:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1186

Рис. 8.25: ”Прикуп” открыт
”Прикуп” открыт, но когда я делаю ”заказ”, и, по логике вещей, ”прикуп” теперь должен стать открытым, он наоборот становится закрытым:

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1187

Рис. 8.26: ”Прикуп” закрыт
Всё ясно: если аргумент draw_prikup() нулевой, то карты рисуются рубашкой
вверх, если 1, то открытые. Этот же аргумент передается в draw_card() — эта
ф-ция может рисовать и открытые и закрытые карты.
Пропатчить ”Марьяж” теперь легко, достаточно исправить все условные переходы так, как будто бы в ф-цию всегда приходит 1 в аргументе и тогда ”прикуп” всегда будет открыт.
Но что за байты копируются в seg008:16DA и seg008:172E? Я попробовал забить инструкции копирования MOV NOP-ами — ”прикуп” вообще перестал отображаться.
Тогда я сделал так, чтобы всегда записывалась 1:
...
00004B5A:
00004B5C:
00004B5D:
00004B60:
00004B62:
00004B63:

B001
90
A26D08
B001
90
A26C08

mov
nop
mov
mov
nop
mov

al,1
[0086D],al
al,1
[0086C],al

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1188
...

Тогда ”прикуп” отображается как два пиковых туза. А если первый байт — 2, а
второй — 1, получается трефовый туз. Видимо так и кодируется масть карты,
а затем и сама карта. А draw_card() затем считывает эту информацию из пары
глобальных переменных. А копируется она тоже из глобальных переменных,
где собственно и находится состояние карт у игроков и в прикупе после случайной тасовки. Но нельзя забывать, что если мы сделаем так, что в ”прикупе”
всегда будет 2 пиковых туза, это будет только на экране так отображаться, а
в памяти состояние карт останется таким же, как и после тасовки.
Всё понятно: автор решил сделать одну ф-цию для отрисовки и закрытого и
открытого прикупа, поэтому нам, можно сказать, повезло. Могло быть труднее:
в самом начале рисовались бы просто две рубашки карт, а открытый прикуп
только потом.
Я также пробовал сделать шутку-пранк: во время торгов одна карта ”прикупа” открыта, а вторая закрыта, а после ”заказа”, наоборот, первая закрыта, а
вторая открывается. В качестве упражнения, вы можете попробовать сделать
так.
Еще кое-что: чтобы сделать прикуп открытым, ведь можно же найти место где
вызывается draw_prikup() и поменять 0 на 1. Можно, только это место не в
головой marriage.exe, а в marriage.000, а это DOS-овский оверлей (начинается
с сигнатуры ”FBOV”).
В качестве упражнения, можно попробовать подсматривать состояние всех
карт, и у обоих игроков. Для этого нужно отладчиком смотреть состояние глобальной памяти рядом с тем, откуда считываются обе карты прикупа.
Файлы:
оригинальная версия: http://beginners.re/examples/marriage/original.zip,
пропатченная мною версия: http://beginners.re/examples/marriage/patched.
zip (все 4 условных перехода после cmp [bp+arg_0], 0 заменены на JMP).

8.15.1. Упражнение
Бытовали слухи, что сама программа жульничает, “подглядывая” в карты соперниковлюдей. Это возможно, если алгоритмы, определяющие лучший ход, будут использовать информацию из карт соперника. Тогда будет видно, что происходят обращения к этим глобальным переменным из этих мест. Либо же этого
не будет видно, если эти обращения происходят только из ф-ции генерации
случайных карт и ф-ций их отрисовки.

8.16. Другие примеры
Здесь также был пример с Z3 и ручной декомпиляцией. Он перемещен сюда:
https://sat-smt.codes.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

Глава 9

Примеры разбора закрытых
(проприетарных) форматов
файлов
9.1. Примитивное XOR-шифрование
В русскоязычной литературе также используется термин гаммирование.

9.1.1. Простейшее XOR-шифрование
Однажды я видел ПО, где все отладочные сообщения были зашифрованы используя XOR со значением 3. Иными словами, 2 младших бита каждого символа
были переключены.
“Hello, world” становилось “Kfool/#tlqog”:
Листинг 9.1: Python
#!/usr/bin/python
msg="Hello, world!"
print "".join(map(lambda x: chr(ord(x)^3), msg))

Это интересное шифрование (или даже обфускация), потому что оно имеет
два важных свойства: 1) одна ф-ция для шифрования/дешифрования, просто
вызовите её еще раз; 2) символы на выходе печатаемые, так что вся строка
может быть использована в исходном коде без специальных (escaping) символов.
Второе свойство использует тот факт что все печатаемые символы расположены в рядах: 0x2x-0x7x, и когда вы меняете два младших бита, символ переме-

1189

1190
щается на 1 или 3 символа влево или вправо, но никогда не перемещается в
другой (может быть, непечатаемый) ряд:

Рис. 9.1: 7-битная ASCII-таблица в Emacs
…с единственным исключением символа 0x7F.
Например, давайте зашифруем символы в пределах A-Z:
#!/usr/bin/python
msg="@ABCDEFGHIJKLMNO"
print "".join(map(lambda x: chr(ord(x)^3), msg))

Результат: CBA@GFEDKJIHONML.
Это как если символы “@” и “C” были поменены местами, и так же и “B” и “a”.
Так или иначе, это интересный пример использующий свойства XOR, нежели
шифрование: тот самый эффект сохранения печатаемости может быть достигнут переключая любой из младших 4-х бит, в любой последовательности.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1191

9.1.2. Norton Guide: простейшее однобайтное XOR-шифрование
Norton Guide был популярен во времена MS-DOS, это была резидентная программа, работающая как гипертекстовый справочник.
Базы данных Norton Guide это файлы с расширением .ng, содержимое которых
выглядит как зашифрованное:

Рис. 9.2: Очень типичный вид
Почему мы думаем, что зашифрованное а не сжатое?
Мы видим, как слишком часто попадается байт 0x1A (который выглядит как
«→»), в сжатом файле такого не было бы никогда.
Во-вторых, мы видим длинные части состоящие только из латинских букв, они
выглядят как строки на незнакомом языке.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1192
Из-за того, что байт 0x1A слишком часто встречается, мы можем попробовать
расшифровать файл, полагая что он зашифрован простейшим XOR-шифрованием.
Применяем XOR с константой 0x1A к каждому байту в Hiew и мы можем видеть
знакомые текстовые строки на английском:

Рис. 9.3: Hiew применение XOR с 0x1A
XOR-шифрование с одним константным байтом это самый простой способ шифрования, который, тем не менее, иногда встречается.
Теперь понятно почему байт 0x1A так часто встречался: потому что в файле
очень много нулевых байт и в зашифрованном виде они везде были заменены
на 0x1A.
Но эта константа могла быть другой.
В таком случае, можно было бы попробовать перебрать все 256 комбинаций, и
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1193
посмотреть содержимое «на глаз», а 256 — это совсем немного.
Больше о формате файлов Norton Guide: http://www.davep.org/norton-guides/
file-format/.
Энтропия
Очень важное свойство подобного примитивного шифрования в том, что информационная энтропия зашифрованного/дешифрованного блока точно такая
же. Вот мой анализ в Wolfram Mathematica 10.
Листинг 9.2: Wolfram Mathematica 10
In[1]:= input = BinaryReadList["X86.NG"];
In[2]:= Entropy[2, input] // N
Out[2]= 5.62724
In[3]:= decrypted = Map[BitXor[#, 16^^1A] &, input];
In[4]:= Export["X86_decrypted.NG", decrypted, "Binary"];
In[5]:= Entropy[2, decrypted] // N
Out[5]= 5.62724
In[6]:= Entropy[2, ExampleData[{"Text", "ShakespearesSonnets"}]] // N
Out[6]= 4.42366

Что мы здесь делаем это загружаем файл, вычисляем его энтропию, дешифруем его, сохраняем, снова вычисляем энтропию (точно такая же!).
Mathematica дает возможность анализировать некоторые хорошо известные
англоязычные тексты.
Так что мы вычисляем энтропию сонетов Шейкспира, и она близка к энтропии
анализируемого нами файла.
Анализируемый нами файл состоит из предложений на английском языке, которые близки к языку Шейкспира.
И применение побайтового XOR к тексту на английском языке не меняет энтропию.
Хотя, это не будет справедливо когда файл зашифрован при помощи XOR шаблоном длиннее одного байта.
Файл, который мы анализировали, можно скачать здесь: http://beginners.
re/examples/norton_guide/X86.NG.
Еще кое-что о базе энтропии

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1194
Wolfram Mathematica вычисляет энтропию с базой e (основание натурального
логарифма), а утилита UNIX ent 1 использует базу 2.
Так что мы явно указываем базу 2 в команде Entropy, чтобы Mathematica давала те же результаты, что и утилита ent.

1 http://www.fourmilab.ch/random/

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1195

9.1.3. Простейшее четырехбайтное XOR-шифрование
Если при XOR-шифровании применялся шаблон длиннее байта, например, 4байтный, то его также легко увидеть.
Например, вот начало файла kernel32.dll (32-битная версия из Windows Server
2008):

Рис. 9.4: Оригинальный файл

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1196
Вот он же, но «зашифрованный» 4-байтным ключом:

Рис. 9.5: «Зашифрованный» файл
Очень легко увидеть повторяющиеся 4 символа.
Ведь в заголовке PE-файла много длинных нулевых областей, из-за которых
ключ становится видным.

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1197
Вот начало PE-заголовка в 16-ричном виде:

Рис. 9.6: PE-заголовок

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1198
И вот он же, «зашифрованный»:

Рис. 9.7: «Зашифрованный» PE-заголовок
Легко увидеть визуально, что ключ это следующие 4 байта: 8C 61 D2 63. Используя эту информацию, довольно легко расшифровать весь файл.
Таким образом, важно помнить эти свойства PE-файлов: 1) в PE-заголовке много нулевых областей; 2) все PE-секции дополняются нулями до границы страницы (4096 байт), так что после всех секций обычно имеются длинные нулевые
области.
Некоторые другие форматы файлов могут также иметь длинные нулевые области.
Это очень типично для файлов, используемых научным и инженерным ПО.
Для тех, кто самостоятельно хочет изучить эти файлы, то их можно скачать
Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1199
здесь:
http://beginners.re/examples/XOR_4byte/.
Упражнение
• http://challenges.re/50

9.1.4. Простое шифрование используя XOR-маску
Я нашел одну старую игру в стиле interactive fiction в архиве if-archive2 :
The New Castle v3.5 − Text/Adventure Game
in the style of the original Infocom (tm)
type games, Zork, Collosal Cave (Adventure),
etc. Can you solve the mystery of the
abandoned castle?
Shareware from Software Customization.
Software Customization [ASP] Version 3.5 Feb. 2000

Можно скачать здесь: https://beginners.re/current-tree/ff/XOR/mask_1/files/
newcastle.tgz.
Там внутри есть файл (с названием castle.dbf ), который явно зашифрован, но
не настоящим криптоалгоритмом, и он не сжат, это что-то куда проще. Я бы
даже не стал измерять уровень энтропии (9.2 (стр. 1215)) этого файла, потому
что я итак уверен, что он низкий. Вот как он выглядит в Midnight Commander:

Рис. 9.8: Зашифрованный файл в Midnight Commander
2 http://www.ifarchive.org/

Если вы заметили опечатку, ошибку или имеете какие-то либо соображения,
пожелания, пожалуйста, напишите мне: . Спасибо!

1200
Зашифрованный файл можно скачать здесь: https://beginners.re/current-tree/
ff/XOR/mask_1/files/castle.dbf.bz2.
Можно ли расшифровать его без доступа к программе, используя просто этот
файл?
Тут явно просматривается повторяющаяся строка. Если использовалось простое шифрование с XOR-маской, такие повторяющиеся строки это явное свидетельство, потому что, вероятно, тут были длинные лакуны с нулевыми байтами, которые, в свою очередь, присутствуют во мноигих исполняемых файлах,
и в остальных бинарных файлах.
Вот дамп начала этого файла используя утилиту xxd из UNIX:
...
0000030:
0000040:
0000050:
0000060:
0000070:

09
7a
75
50
1c

61
11
50
2e
3c

0d
0f
02
28
37

63
72
4a
72
5d

0f
6e
31
24
27

77
03
71
4b
5a

14
05
31
38
1c

69
7d
33
21
7c

75
7d
5c
4c
6a

62
63
27
09
10

67
7e
08
37
14

76
77
5c
38
68

01
66
51
3b
77

7e
1e
74
51
08

1d
7a
3e
41
6d

61
02
39
2d
1a

.a.c.w.iubgv.~.a
z..rn..}}c~wf.z.
uP.J1q13\'.\Qt>9
P.(r$K8!L.78;QA−
.