КулЛиб - Классная библиотека! Скачать книги бесплатно 

Введение в reverse engineering для начинающих [Денис Юричев] (pdf) читать онлайн

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


 [Настройки текста]  [Cбросить фильтры]
Введение в reverse engineering для начинающих

Денис Юричев


cbnd
c
○2013,
Денис Юричев.
Это произведение доступно по лицензии Creative Commons
«Attribution-NonCommercial-NoDerivs» («Атрибуция — Некоммерческое использование — Без
производных произведений») 3.0 Непортированная. Чтобы увидеть копию этой лицензии,
посетите http://creativecommons.org/licenses/by-nc-nd/3.0/.
Версия этого текста (12 января 2014 г.).
Возможно, более новая версии текста, а так же англоязычная версия, также доступна по ссылке
http://yurichev.com/RE-book.html
Вы также можете подписаться на мой twitter для получения информации о новых версиях этого
текста, итд: @yurichev_ru, либо подписаться на список рассылки.

Начните изучение языка ассемблера и
reverse engineering сегодня!
Автор этой книги также доступен как преподаватель (по крайней мере в 2014).

Обращайтесь:

i

ОГЛАВЛЕНИЕ

ОГЛАВЛЕНИЕ

Оглавление
Предисловие
0.1 Мини-ЧаВО1 . . . . .
0.2 Об авторе . . . . . . .
0.3 Благодарности . . . .
0.4 Краудфандинг . . . .
0.4.1 Жертвователи

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

vii
. vii
. vii
. viii
. viii
. viii

1 Паттерны компиляторов
1.1 Hello, world! . . . . . . . . . . . . . . . . . . . .
1.1.1 x86 . . . . . . . . . . . . . . . . . . . . .
1.1.2 ARM . . . . . . . . . . . . . . . . . . . . .
1.2 Стек . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.1 Для чего используется стек? . . . . . .
1.3 printf() с несколькими агрументами . . . .
1.3.1 x86 . . . . . . . . . . . . . . . . . . . . .
1.3.2 ARM: 3 аргумента в printf() . . . . .
1.3.3 ARM: 8 аргументов в printf() . . . .
1.3.4 Кстати . . . . . . . . . . . . . . . . . . . .
1.4 scanf() . . . . . . . . . . . . . . . . . . . . . . . .
1.4.1 Об указателях . . . . . . . . . . . . . . .
1.4.2 x86 . . . . . . . . . . . . . . . . . . . . .
1.4.3 ARM . . . . . . . . . . . . . . . . . . . . .
1.4.4 Глобальные переменные . . . . . . . . .
1.4.5 Проверка результата scanf() . . . . . . .
1.5 Передача параметров через стек . . . . . . . .
1.5.1 x86 . . . . . . . . . . . . . . . . . . . . .
1.5.2 ARM . . . . . . . . . . . . . . . . . . . . .
1.6 И еще немного о возвращаемых результатах .
1.7 Указатели . . . . . . . . . . . . . . . . . . . . . .
1.8 Условные переходы . . . . . . . . . . . . . . . .
1.8.1 x86 . . . . . . . . . . . . . . . . . . . . .
1.8.2 ARM . . . . . . . . . . . . . . . . . . . . .
1.9 switch()/case/default . . . . . . . . . . . . . . . .
1.9.1 Если вариантов мало . . . . . . . . . . .
1.9.2 И если много . . . . . . . . . . . . . . . .
1.10 Циклы . . . . . . . . . . . . . . . . . . . . . . . .
1.10.1 x86 . . . . . . . . . . . . . . . . . . . . .
1.10.2 ARM . . . . . . . . . . . . . . . . . . . . .
1.10.3 Еще кое-что . . . . . . . . . . . . . . . .
1.11 strlen() . . . . . . . . . . . . . . . . . . . . . . . .
1.11.1 x86 . . . . . . . . . . . . . . . . . . . . .
1.11.2 ARM . . . . . . . . . . . . . . . . . . . . .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

Часто задаваемые вопросы

ii

1
1
1
4
9
9
13
13
14
15
18
18
19
19
20
21
23
25
25
27
28
29
30
30
32
34
34
37
42
42
45
46
46
46
49

1.12 Деление на 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.12.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.12.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.12.3 Определение делителя . . . . . . . . . . . . . . . . . .
1.13 Работа с FPU . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.13.1 Простой пример . . . . . . . . . . . . . . . . . . . . . .
1.13.2 Передача чисел с плавающей запятой в аргументах
1.13.3 Пример с сравнением . . . . . . . . . . . . . . . . . .
1.14 Массивы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.14.1 Простой пример . . . . . . . . . . . . . . . . . . . . . .
1.14.2 Переполнение буфера . . . . . . . . . . . . . . . . . .
1.14.3 Защита от переполнения буфера . . . . . . . . . . .
1.14.4 Еще немного о массивах . . . . . . . . . . . . . . . .
1.14.5 Многомерные массивы . . . . . . . . . . . . . . . . .
1.15 Битовые поля . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.15.1 Проверка какого-либо бита . . . . . . . . . . . . . . .
1.15.2 Установка/сброс отдельного бита . . . . . . . . . . .
1.15.3 Сдвиги . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.15.4 Пример вычисления CRC32 . . . . . . . . . . . . . . .
1.16 Структуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.16.1 Пример SYSTEMTIME . . . . . . . . . . . . . . . . . . .
1.16.2 Выделяем место для структуры через malloc() . . . .
1.16.3 struct tm . . . . . . . . . . . . . . . . . . . . . . . . . .
1.16.4 Упаковка полей в структуре . . . . . . . . . . . . . . .
1.16.5 Вложенные структуры . . . . . . . . . . . . . . . . . .
1.16.6 Работа с битовыми полями в структуре . . . . . . . .
1.17 Объединения (union) . . . . . . . . . . . . . . . . . . . . . . .
1.17.1 Пример генератора случайных чисел . . . . . . . . .
1.18 Указатели на функции . . . . . . . . . . . . . . . . . . . . . . .
1.18.1 GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.19 SIMD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.19.1 Векторизация . . . . . . . . . . . . . . . . . . . . . . .
1.19.2 Реализация strlen() при помощи SIMD . . . . . .
1.20 64 бита . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.20.1 x86-64 . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.20.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.21 C99 restrict . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.22 Inline-функции . . . . . . . . . . . . . . . . . . . . . . . . . . .
2 Си++
2.1 Классы . . . . . . . . . . . . . . . . . . .
2.1.1 Классы . . . . . . . . . . . . . .
2.1.2 Наследование классов . . . . .
2.1.3 Инкапсуляция . . . . . . . . . .
2.1.4 Множественное наследование
2.1.5 Виртуальные методы . . . . . .
2.2 ostream . . . . . . . . . . . . . . . . . .
2.3 References . . . . . . . . . . . . . . . . .
2.4 STL . . . . . . . . . . . . . . . . . . . . .
2.4.1 std::string . . . . . . . . . . . . .
2.4.2 std::list . . . . . . . . . . . . . .
2.4.3 std::vector . . . . . . . . . . . . .
2.4.4 std::map и std::set . . . . . . . .

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

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

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

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

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

iii

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ОГЛАВЛЕНИЕ
. . . . . . 51
. . . . . . 51
. . . . . . 52
. . . . . . 54
. . . . . . 54
. . . . . . 55
. . . . . . 58
. . . . . . 60
. . . . . . 67
. . . . . . 67
. . . . . . 70
. . . . . . 73
. . . . . . 76
. . . . . . 77
. . . . . . 79
. . . . . . 79
. . . . . . 83
. . . . . . 86
. . . . . . 88
. . . . . . 91
. . . . . . 91
. . . . . . 93
. . . . . . 95
. . . . . . 99
. . . . . . 102
. . . . . . 103
. . . . . . 108
. . . . . . 108
. . . . . . 110
. . . . . . 112
. . . . . . 113
. . . . . . 114
. . . . . . 119
. . . . . . 122
. . . . . . 122
. . . . . . 128
. . . . . . 128
. . . . . . 131

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

133
133
133
137
140
142
144
147
148
148
149
155
163
170

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ОГЛАВЛЕНИЕ
179
. . . . . . 179
. . . . . . 179
. . . . . . 181
. . . . . . 181
. . . . . . 181
. . . . . . 181
. . . . . . 181
. . . . . . 182
. . . . . . 182
. . . . . . 183
. . . . . . 185
. . . . . . 185
. . . . . . 187
. . . . . . 188
. . . . . . 188
. . . . . . 190
. . . . . . 192
. . . . . . 192

4 Поиск в коде того что нужно
4.1 Связь с внешним миром . . . . . . . . . . . . . . . . . . .
4.1.1 Часто используемые ф-ции Windows API . . . . .
4.1.2 tracer: Перехват всех ф-ций в отдельном модуле
4.2 Строки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3 Вызовы assert() . . . . . . . . . . . . . . . . . . . . . . . . .
4.4 Константы . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.4.1 Magic numbers . . . . . . . . . . . . . . . . . . . . .
4.4.2 Поиск констант . . . . . . . . . . . . . . . . . . . .
4.5 Поиск нужных инструкций . . . . . . . . . . . . . . . . . .
4.6 Подозрительные паттерны кода . . . . . . . . . . . . . . .
4.6.1 Вручную написанный код на ассемблере . . . . .
4.7 Использование magic numbers для трассировки . . . . .
4.8 Прочее . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.9 Старые методы, тем не менее, интересные . . . . . . . .
4.9.1 Сравнение “снимков” памяти . . . . . . . . . . . .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

195
195
196
196
197
197
198
198
199
199
201
201
201
202
202
202

5 Специфичное для ОС
5.1 Форматы файлов . . . . . . . . . . .
5.1.1 Win32 PE . . . . . . . . . . .
5.2 Системные вызовы (syscall-ы) . . .
5.2.1 Linux . . . . . . . . . . . . .
5.2.2 Windows . . . . . . . . . . .
5.3 Windows NT: Критические секции

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

203
203
203
209
210
210
210

3 Еще кое-что
3.1 Пролог и эпилог в функции . . . . . . . . . . . . . . .
3.2 npad . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3 Представление знака в числах . . . . . . . . . . . . .
3.3.1 Переполнение integer . . . . . . . . . . . . . .
3.4 Способы передачи аргументов при вызове функций
3.4.1 cdecl . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.2 stdcall . . . . . . . . . . . . . . . . . . . . . . . .
3.4.3 fastcall . . . . . . . . . . . . . . . . . . . . . . .
3.4.4 thiscall . . . . . . . . . . . . . . . . . . . . . . .
3.4.5 x86-64 . . . . . . . . . . . . . . . . . . . . . . .
3.4.6 Возвращение переменных типа float, double .
3.5 Адресно-независимый код . . . . . . . . . . . . . . . .
3.5.1 Windows . . . . . . . . . . . . . . . . . . . . . .
3.6 Thread Local Storage . . . . . . . . . . . . . . . . . . . .
3.7 Трюк с LD_PRELOAD в Linux . . . . . . . . . . . . . . .
3.8 Itanium . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.9 Перестановка basic block-ов . . . . . . . . . . . . . . .
3.9.1 Profile-guided optimization . . . . . . . . . . .

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

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

.
.
.
.
.
.

.
.
.
.
.
.

6 Инструменты
213
6.0.1 Отладчик . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
6.0.2 Трассировка системных вызовов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
7 Еще примеры
7.1 Донглы . . . . . . . . . . . . . . . . . . . . . . .
7.1.1 Пример #1: MacOS Classic и PowerPC
7.1.2 Пример #2: SCO OpenServer . . . . .
7.1.3 Пример #3: MS-DOS . . . . . . . . . .

iv

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

.
.
.
.

214
214
214
221
228

7.2
7.3

7.4

“QR9”: Любительская криптосистема вдохновленная кубиком Рубика
SAP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.3.1 Касательно сжимания сетевого траффика в клиенте SAP . . .
7.3.2 Функции проверки пароля в SAP 6.0 . . . . . . . . . . . . . . .
Oracle RDBMS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.4.1 Таблица V$VERSION в Oracle RDBMS . . . . . . . . . . . . . . .
7.4.2 Таблица X$KSMLRU в Oracle RDBMS . . . . . . . . . . . . . . .
7.4.3 Таблица V$TIMER в Oracle RDBMS . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

ОГЛАВЛЕНИЕ
. . . . . . 234
. . . . . . 259
. . . . . . 259
. . . . . . 269
. . . . . . 272
. . . . . . 272
. . . . . . 279
. . . . . . 281

8 Прочее
285
8.1 Compiler intrinsic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
8.2 Аномалии компиляторов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
9 Что стоит почитать
9.1 Книги . . . . . . . . .
9.1.1 Windows . .
9.1.2 Си/Си++ . . .
9.1.3 x86 / x86-64
9.1.4 ARM . . . . .
9.2 Блоги . . . . . . . . .
9.2.1 Windows . .

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.

287
287
287
287
287
287
287
287

10 Задачи
10.1 Легкий уровень . . .
10.1.1 Задача 1.1 .
10.1.2 Задача 1.2 .
10.1.3 Задача 1.3 .
10.1.4 Задача 1.4 .
10.1.5 Задача 1.5 .
10.1.6 Задача 1.6 .
10.1.7 Задача 1.7 .
10.1.8 Задача 1.8 .
10.1.9 Задача 1.9 .
10.1.10Задача 1.10
10.1.11Задача 1.11
10.2 Средний уровень . .
10.2.1 Задача 2.1 .
10.2.2 Задача 2.2 .
10.2.3 Задача 2.3 .
10.2.4 Задача 2.4 .
10.2.5 Задача 2.5 .
10.2.6 Задача 2.6 .
10.2.7 Задача 2.7 .
10.3 crackme / keygenme

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

288
288
288
289
292
294
297
298
300
303
305
307
307
307
307
313
313
313
314
314
314
314

11 Ответы на задачи
11.1 Легкий уровень . .
11.1.1 Задача 1.1
11.1.2 Задача 1.2
11.1.3 Задача 1.3
11.1.4 Задача 1.4
11.1.5 Задача 1.5
11.1.6 Задача 1.6
11.1.7 Задача 1.7

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

315
315
315
315
315
316
316
317
317

.
.
.
.
.
.
.
.

v

11.1.8 Задача 1.8 .
11.1.9 Задача 1.9 .
11.1.10Задача 1.11
11.2 Средний уровень . .
11.2.1 Задача 2.1 .
11.2.2 Задача 2.2 .
11.2.3 Задача 2.3 .
11.2.4 Задача 2.4 .
11.2.5 Задача 2.5 .
11.2.6 Задача 2.6 .

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

ОГЛАВЛЕНИЕ
. . . . . . 318
. . . . . . 318
. . . . . . 318
. . . . . . 319
. . . . . . 319
. . . . . . 319
. . . . . . 319
. . . . . . 319
. . . . . . 319
. . . . . . 319

Послесловие
320
11.3 Вопросы? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320
Приложение
11.4 Общая терминология . . . . . . . . . . . . . . . . . . . . . . . . .
11.5 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11.5.1 Терминология . . . . . . . . . . . . . . . . . . . . . . . . .
11.5.2 Регистры общего пользования . . . . . . . . . . . . . . .
11.5.3 FPU-регистры . . . . . . . . . . . . . . . . . . . . . . . . .
11.5.4 SIMD-регистры . . . . . . . . . . . . . . . . . . . . . . . .
11.5.5 Отладочные регистры . . . . . . . . . . . . . . . . . . . .
11.5.6 Инструкции . . . . . . . . . . . . . . . . . . . . . . . . . .
11.6 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11.6.1 Регистры общего пользования . . . . . . . . . . . . . . .
11.6.2 Current Program Status Register (CPSR) . . . . . . . . . .
11.6.3 Регистры VPF (для чисел с плавающей точкой) и NEON

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

321
321
321
321
321
325
327
327
328
339
339
340
340

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

341

Литература

345

Глоссарий

347

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

349

vi

ГЛАВА 0. ПРЕДИСЛОВИЕ

Предисловие
Здесь (будет) немного моих заметок о reverse engineering на русском языке для начинающих, для тех кто
хочет научиться понимать создаваемый Си/Си++ компиляторами код для x86 (коего, практически, больше
всего остального) и ARM.
У термина “reverse engineering” несколько популярных значений: 1) исследование скомпилированных
программ; 2) сканирование трехмерной модели для последующего копирования; 3) восстановление структуры СУБД. Настоящий сборник заметок связан с первым значением

0.1

Мини-ЧаВО

∙ Q: Нужно ли учится понимать язык ассемблера в наше время?
A: Да: ради того чтобы понимать лучше внутреннее устройство, отлаживать код лучше и быстрее.
∙ Q: Нужно ли учиться писать на языке ассемблера в наше время?
A: Пожалуй, нет, если только не писать низкоуровневый код для ОС2 .
∙ Q: Но для написания очень оптимизированных процедур?
A: Нет, современные компиляторы Си/Си++ делают это лучше.
∙ Q: Нужно ли знать внутреннее устройство микропроцессоров?
A: Современные CPU3 очень сложные. Если вы не собираетесь писать очень оптимизированный код,
или не работаете над кодегенератором компилятора, тогда устройство CPU можно изучать только в
общих чертах 4 . В то же время, для понимания и анализа кода, достаточно только знать ISA5 , назначения регистров, т.е., “внешнюю” часть CPU, доступную для прикладного программиста.
∙ Q: И все таки зачем мне учить ассемблер?
A: В основном, для лучшего понимания происходящего во время отладки и для исследования программ без наличия исходных кодов, включая зловреды (или вредоносы) 6 .
∙ Q: Как можно найти работу reverse engineer-а?
A: На reddit посвященному RE7 время от времени бывают hiring thread (2013 Q3), посмотрите там.

0.2

Об авторе

Денис Юричев — опытный reverse engineer и программист. Также доступен как преподаватель языка ассемблера, обратной разработки (reverse engineering), Си/Си++. Может обучать удаленно через электронную
почту, Skype или иной мессенджер, либо лично, в Киеве. С его резюме можно ознакомиться здесь.
2

Операционная Система
Central processing unit
4
Очень хороший текст на эту тему: [8]
5
Instruction Set Architecture (Архитектура набора команд)
6
современные (2013) русскоязычные термины для malware
7
http://www.reddit.com/r/ReverseEngineering/
3

vii

0.3. БЛАГОДАРНОСТИ

0.3

ГЛАВА 0. ПРЕДИСЛОВИЕ

Благодарности

Андрей ”herm1t” Баранович, Слава ”Avid” Казаков, Станислав ”Beaver” Бобрицкий, Александр Лысенко, Александр ”Lstar” Черненький, Андрей Зубинский, Марк “Logxen” Купер, Shell Rocket, Arnaud Patard (rtp на
#debian-arm IRC), и всем тем на github.com кто присылал замечания и коррективы.
Было использовано множество пакетов LATEX: их авторов я также хотел бы поблагодарить.

0.4

Краудфандинг

Как выясняется, быть (техническим) писателем требует много сил и работы.
Эта книга является свободной, находится в свободном доступе, и доступна в виде исходных кодов 8
(LaTeX), и всегда будет оставаться таковой.
В мои текущие планы насчет этой книги входит добавление информации на эти темы: MIPS, Objective-C,
Visual Basic, anti-debugging tricks, Windows NT kernel debugger, OpenMP, Java, .NET, Oracle RDBMS.
Если вы хотите чтобы я продолжал свою работу и писал на эти темы, вы можете рассмотреть идею
краудфандинга.
Со способами краудфандинга можно ознакомиться на странице http://yurichev.com/crowdfunding.
html
Имена всех жертвователей будут перечислены в книге! Жертвователи также имеют право просить меня
дописывать в книгу что-то раньше чем остальное.
Почему не попробовать издаться? Потому что это техническая литература, которая, как мне кажется,
не может быть закончена или быть замороженой в бумажном виде. Такие технические справочники чемто похожи на Wikipedia или библиотеку MSDN9 , они могут развиваться бесконечно долго. Кто-то может
сесть и не отрываясь написать всё от начала до конца, опубликовать это и забыть. Как выясняется, это не
я. Каждый день меня посещают мысли вроде “это было написано плохо, можно было бы и лучше написать”,
“это плохой пример, я знаю получше”, “еще одна вещь которую я могу объяснить лушче и короче”, итд. Как
можно увидеть в истории коммитов исходников этой книги, я делаю много мелких изменений почти каждый
день: https://github.com/dennis714/RE-for-beginners/commits/master.
Так что книга наверное будет в виде “rolling release”, как говорят о дистрибутивах Linux вроде Gentoo.
Без релизов (и дедлайнов) вообще, а постепенная разработка. И я не знаю, сколько займет времени написать всё что я знаю, может быть 10 лет или больше. Конечно, это не очень удобно для читателей желающий
стабильности, но всё что я могу им предложить это файл ChangeLog, служащий как секция “что нового”. Те,
кому интересно, могут проверять его время от времени, или мой блог/twitter 10 .

0.4.1

Жертвователи

3 * аноним.

8

https://github.com/dennis714/RE-for-beginners
Microsoft Developer Network
10
http://blog.yurichev.com/ https://twitter.com/yurichev_ru
9

viii

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

Глава 1

Паттерны компиляторов
Когда я учил Си, а затем Си++, я просто писал небольшие фрагменты кода, компилировал и смотрел что
получилось на ассемблере. Так намного проще было понять. Я делал это такое количество раз, что связь
между кодом на Си/Си++ и тем что генерирует компилятор вбилась мне в подсознание достаточно глубоко,
поэтому я могу глядя на код на ассемблере сразу понимать, в общих чертах, что там было написано на Си.
Возможно это поможет кому-то еще, попробую описать некоторые примеры.

1.1

Hello, world!

Начнем с знаменитого примера из книги “The C programming Language” [14]:
#include
int main()
{
printf("hello, world");
return 0;
};

1.1.1

x86

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

(Ключ /Fa означает сгенерировать листинг на ассемблере)
Listing 1.1: MSVC 2010
CONST
SEGMENT
$SG3830 DB
’hello, world’, 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

1

1.1. HELLO, WORLD!
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
MSVC выдает листинги в Intel-овском синтаксисе. Разница между Intel-синтаксисом и AT&T будет рассмотрена немного позже.
Компилятор сгенерировал файл 1.obj, который впоследствии будет слинкован линкером в 1.exe.
В нашем случае, этот файл состоит из двух сегментов: CONST (для данных-констант) и _TEXT (для кода).
Строка “hello, world” в Си/Си++ имеет тип const char*, однако не имеет имени.
Но компилятору нужно как-то с ней работать, так что он дает ей внутреннее имя $SG3830.
Как видно, строка заканчивается нулевым байтом — это требования стандарта Си/Си++ для строк.
В сегменте кода _TEXT, находится пока только одна функция: main().
Функция main(), как и практически все функции, начинается с пролога и заканчивается эпилогом 1 .
Далее следует вызов функции printf(): CALL _printf.
Перед этим вызовом, адрес строки (или указатель на нее) с нашим приветствием при помощи инструкции PUSH помещается в стек.
После того как функция printf() возвращает управление в функцию main(), адрес строки (или указатель на нее) все еще лежит в стеке.
Так как он больше не нужен, то указатель стека (регистр ESP) корректируется.
ADD ESP, 4 означает прибавить 4 к значению в регистре ESP.
Почему 4? Так как, это 32-битный код, для передачи адреса нужно аккурат 4 байта. В x64-коде это 8
байт.
“ADD ESP, 4” эквивалентно “POP регистр”, но без использования какого-либо регистра2 .
Некоторые компиляторы, например Intel C++ Compiler, в этой же ситуации, могут вместо ADD сгенерировать POP ECX (подобное можно встретить например в коде Oracle RDBMS, им скомпилированном), что
почти то же самое, только портится значение в регистре ECX.
Возможно, компилятор применяет POP ECX потому что эта инструкция короче (1 байт против 3).
О стеке можно прочитать в соответствующем разделе (1.2).
После вызова printf(), в оригинальном коде на Си/Си++ указано return 0 — вернуть 0 в качестве
результата функции main().
В сгенерированном коде это обеспечивается инструкцией XOR EAX, EAX
XOR, на самом деле, как легко догадаться, “исключающее ИЛИ” 3 , но компиляторы часто используют
его вместо простого MOV EAX, 0 — снова потому что опкод короче (2 байта против 5).
Бывает так, что некоторые компиляторы генерируют SUB EAX, EAX, что значит, отнять значение в
EAX от значения в EAX, что в любом случае даст 0 в результате.
Самая последняя инструкция RET возвращает управление в вызывающую функцию. Обычно, это код
Си/Си++ CRT4 , который, в свою очередь, вернет управление операционной системе.
GCC
Теперь скомпилируем то же самое компилятором GCC 4.4.1 в Linux: gcc 1.c -o 1
Затем при помощи IDA5 . посмотрим как создалась функция main().
(IDA, как и MSVC, показывает код в Intel-синтаксисе).
N.B. Мы также можем заставить GCC генерировать листинги в этом формате при помощи ключей -S
-masm=intel
Listing 1.2: GCC
main

proc near

var_10

= dword ptr -10h
push
mov
and
sub
mov

ebp
ebp,
esp,
esp,
eax,

esp
0FFFFFFF0h
10h
offset aHelloWorld ; "hello, world"

1

Об этом смотрите подробнее в разделе о прологе и эпилоге функции (3.1).
Флаги процессора, впрочем, модифицируются
3
http://en.wikipedia.org/wiki/Exclusive_or
4
C runtime library
5
Interactive Disassembler
2

2

1.1. HELLO, WORLD!
mov
call
mov
leave
retn
endp

main

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
[esp+10h+var_10], eax
_printf
eax, 0

Почти то же самое. Адрес строки “hello, world” лежащей в сегменте данных, в начале сохраняется в EAX,
затем записывается в стек. А еще в прологе функции мы видим AND ESP, 0FFFFFFF0h — эта инструкция
выравнивает значение в ESP по 16-байтной границе, делая все значения в стеке также выровненными
по этой границе (процессор более эффективно работает с переменными расположенными в памяти по
адресам кратным 4 или 16)6 .
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-мире.
Listing 1.3: компилируем в GCC 4.7.3
gcc -S 1_1.c

Получим такой файл:
Listing 1.4: GCC 4.7.3
.file
"1_1.c"
.section
.rodata
.LC0:
.string "hello, world"
.text
.globl main
.type
main, @function
main:
.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:
6

Wikipedia: Выравнивание данных

3

1.1. HELLO, WORLD!

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

.size
main, .-main
.ident "GCC: (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3"
.section
.note.GNU-stack,"",@progbits

Здесь много макросов (начинающихся с точки). Они нам пока не интересны. Пока что, ради упрощения,
мы можем их игнорировать и впредь (кроме макроса .string, при помощи которого кодируется последовательность символов оканчивающихся нулем, такие же строки как в Си). И тогда получится следующее
7:
Listing 1.5: GCC 4.7.3
.LC0:
.string "hello, world"
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-синтаксисе: .
Чтобы легче понимать разницу, можно запомнить следующее: когда вы работаете с Intel-синтаксисом,
можете в уме ставить знак равенства (=) между операндами, а когда с AT&T-синтаксисом, мысленно
ставьте стрелку направо (→) 8 .
∙ AT&T: Перед именами регистров ставится знак процента (%), а перед числами знак доллара ($). Вместо
квадратных скобок применяются круглые.
∙ AT&T: К каждой инструкции добавляется специальный символ, определяющий тип данных:
– l — long (32 бита)
– w — word (16 бит)
– b — byte (8 бит)
Возвращаясь к результату компиляции: он идентичен тому, который мы посмотрели в IDA. Одна мелочь:
0FFFFFFF0h записывается как $-16. Это тоже самое: 16 в десятичной системе это 0x10 в шестнадцатеричной. -0x10 будет как раз 0xFFFFFFF0 (в рамках 32-битных чисел).
Еще: возвращаемый результат устанавливается в 0 обычной инструкцией MOV а не XOR. MOV просто
загружает значение в регистр. Её название не очень удачное (данные не перемещаются), в других архитектурах подобная инструкция обычно носит название “load” или что-то в этом роде.

1.1.2

ARM

Для экспериментов с процессором ARM, я выбрал два компилятора: популярный в embedded-среде Keil
Release 6/2013 и среду разработки Apple Xcode 4.6.3 (с компилятором LLVM-GCC 4.2 ), генерирующую код
для ARM-совместимых процессоров и SOC9 в iPod/iPhone/iPad, планшетных компьютеров для Windows 8
и Windows RT10 итаких устройствах как Raspberry Pi.
7

Кстати, для уменьшения генерации “лишних” макросов, можно использовать такой ключ GCC: -fno-asynchronous-unwind-tables
Кстати, в некоторые стандартных функциях библиотеки Си (например, memcpy(), strcpy()) также применяется расстановка
аргументов как в Intel-синтаксисе: в начале указатель в памяти на блок назначения, затем указатель на блок-источник.
9
System on Chip
10
http://en.wikipedia.org/wiki/List_of_Windows_8_and_RT_tablet_devices
8

4

1.1. HELLO, WORLD!
Неоптимизирующий Keil + Режим ARM

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

Для начала, скомпилируем наш пример в Keil:
armcc.exe --arm --c90 -O0 1.c

Компилятор armcc генерирует листинг на ассемблере в формате Intel, но он содержит некоторые высокоуровневые макросы связанные с ARM11 , а нам важнее увидеть инструкции “как есть”, так что посмотрим
скомпилированный результат в IDA.
Listing 1.6: Неоптимизирующий Keil + Режим ARM + IDA
.text:00000000
.text:00000000
.text:00000004
.text:00000008
.text:0000000C
.text:00000010

main
10
1E
15
00
10

40
0E
19
00
80

2D
8F
00
A0
BD

E9
E2
EB
E3
E8

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

STMFD
ADR
BL
MOV
LDMFD

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

DCB "hello, world",0

; DATA XREF: main+4

Вот чуть-чуть фактов о процессоре ARM, которые желательно знать. Процессор ARM имеет по крайней мере два основных режима: режим ARM и thumb. В первом (ARM) режиме доступны все инструкции
и каждая имеет размер 32 бита (или 4 байта). Во втором режиме (thumb) каждая инструкция имеет размер 16 бит (или 2 байта) 12 . Режим thumb может выглядеть привлекательнее тем, что программа на нем
может быть 1) компактнее; 2) эффективнее исполняться на микроконтроллере с 16-битной шиной данных.
Но за всё нужно платить: в режиме thumb куда меньше возможностей процессора, например, возможен
доступ только к 8-и регистрам процессора, и чтобы совершить некоторые действия, выполнимые в режиме
ARM одной инструкцией, нужны несколько thumb-инструкций. Начиная с ARMv7, имеется также поддержка
инструкций thumb-2, это thumb расширенный до поддержки куда большего числа инструкций. Распространено заблуждение что thumb-2 это смесь ARM и thumb. Это не верно. Просто thumb-2 был дополен
до более полной поддержки возможностей процессора, что теперь может легко конкурировать с режимом
ARM. Программа для процессора ARM может представлять смесь процедур скомпилированных для обоих
режимов. Основное количество приложений для iPod/iPhone/iPad скомпилировано для набора инструкций
thumb-2, потому что Xcode делает так по умолчанию.
В вышеприведененном примере можно легко увидеть что каждая инструкция имеет размер 4 байта.
Действительно, ведь мы же компилировали наш код для режима ARM а не thumb.
Самая первая инструкция, ”STMFD SP!, {R4,LR}”13 , работает как инструкция PUSH в x86, записывает значения двух регистров (R4 и LR14 ) в стек. Действительно, в выдаваемом листинге на ассемблере,
компилятор armcc, для упрощения, указывает здесь инструкцию ”PUSH {r4,lr}”. Но это не совсем точно, инструкция PUSH доступна только в режиме thumb, поэтому, во избежания путанницы, я предложил
работать в IDA.
Итак, эта инструкция записывает значения регистров R4 и LR по адресу в памяти, на который указывает
регистр SP1516 , затем уменьшает SP, чтобы он указывал на место в стеке, доступное для новых записей.
Эта инструкция, как и инструкция PUSH в режиме thumb, может сохранить в стеке одновременно несколько значений регистров, что может быть очень удобно. Кстати, такого в x86 нет. Так же следует заметить, что
STMFD — генерализация инструкции PUSH (то есть, расширяет её возможности), потому что может работать
с любым регистром а не только с SP, это тоже может быть очень удобно.
Инструкция ”ADR R0, aHelloWorld” прибавляет значение регистра PC17 к смещению, где хранится строка “hello, world” . Причем здесь PC, можно спросить? Притом, что это так называемый “адреснонезависимый код” 18 , он предназначен для исполнения будучи не привязанным к каким-либо адресам в
11

например, он показывает инструкции PUSH/POP отсутствующие в режиме ARM
Кстати, инструкции фиксированного размера удобны тем, что всегда можно легко узнать адрес следующей (или предыдущей)
инструкции. Эта особенность будет рассмотрена в секции о switch() (1.9.2).
13
Store Multiple Full Descending
14
Link Register
15
Stack Pointer
16
ESP, RSP в x86
17
Program Counter
18
Читайте больше об этом в соответствующем разделе (3.5)
12

5

1.1. HELLO, WORLD!
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
памяти. В опкоде инструкции ADR указывается разница между адресом этой инструкции и местом, где хранится строка. Эта разница всегда будет постоянной, вне зависимости от того, куда был загружен ОС наш
код. Поэтому всё что нужно это прибавить адрес текущей инструкции (из PC) чтобы получить текущий абсолютный адрес нашей Си-строки.
Инструкция ”BL __2printf”19 вызывает функцию printf(). Работа этой инструкции состоит из
двух фаз:
∙ записать адрес после инструкции BL (0xC) в регистр LR;
∙ затем собственно передать управление в printf(), записав адрес этой функции в регистр PC20 .
Ведь, когда функция printf() закончит работу, нужно знать, куда вернуть управление, поэтому закончив работу, всякая функция передает управление по адресу записанному в регистре LR.
В этом разница между “чистыми” RISC21 -процессорами вроде ARM и CISC22 -процессорами как x86, где
адрес возврата записывается в стек23 .
Кстати, 32-битный абсолютный адрес, либо же смещение, невозможно закодировать в 32-битной инструкции BL, в ней есть место только для 24-х бит. Так же следует отметить, что из-за того что все инструкции
в режиме ARM имеют длину 4 байта (32 бита), и инструкции могут находится только по адресам кратным
4, то последние 2 бита (всегда нулевых) можно не кодировать. В итоге имеем 26 бит, при помощи которых
можно закодировать смещение ± ≈ 32𝑀 .
Следующая инструкция ”MOV R0, #0”24 просто записывает 0 в регистр R0. Ведь наша Си-функция
возвращает 0 а возвращаемое значение всякая функция оставляет в R0.
Последняя инструкция ”LDMFD SP!, R4,PC”25 это инструкция обратная от STMFD. Она загружает
из стека значения для сохранения их в R4 и PC, увеличивая указатель стека SP. Это, в каком-то смысле,
аналог POP. N.B. Самая первая инструкция STMFD сохранила в стеке R4 и LR, а восстанавливаются во
время исполнения LDMFD регистры R4 и PC. Как я уже описывал, в регистре LR обычно сохраняется адрес
места, куда нужно всякой функции вернуть управление. Самая первая инструкция сохраняет это значение в
стеке, потому что наша функция main() позже будет сама пользоваться этим регистром, в момент вызова
printf(). А затем, в конце функции, это значение можно сразу записать в PC, таким образом, передав
управление туда, откуда была вызвана наша функция. Так как функция main() обычно самая главная в
Си/Си++, управление будет возвращено в загрузчик ОС, либо куда-то в CRT, или что-то в этом роде.
DCB — директива ассемблера, описывающая массивы байт или ASCII-строк, аналог директивы DB в
x86-ассемблере.
Неоптимизирующий Keil: Режим thumb
Скомпилируем тот же пример в Keil для режима thumb:
armcc.exe --thumb --c90 -O0 1.c

Получим (в IDA):
Listing 1.7: Неоптимизирующий Keil + Режим thumb + IDA
.text:00000000
.text:00000000
.text:00000002
.text:00000004
.text:00000008
.text:0000000A

main
10
C0
06
00
10

B5
A0
F0 2E F9
20
BD

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

PUSH
ADR
BL
MOVS
POP

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

DCB "hello, world",0

19

Branch with Link
EIP, RIP в x86
21
Reduced instruction set computing
22
Complex instruction set computing
23
Подробнее об этом будет описано в следующей главе (1.2)
24
MOVe
25
Load Multiple Full Descending
20

6

; DATA XREF: main+2

1.1. HELLO, WORLD!
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
Сразу бросаются в глаза двухбайтные (16-битные) опкоды, это, как я уже упоминал, thumb. Кроме инструкции BL. Но на самом деле, она состоит из двух 16-битных инструкций. Это потому что загрузить в PC
смещение, по которому находится функция printf(), используя так мало места в одном 16-битном опкоде, нельзя. Поэтому первая 16-битная инструкция загружает старшие 10 бит смещения, а вторая — младшие
11 бит смещения. Как я уже упоминал, все инструкции в thumb-режиме имеют длину 2 байта (или 16 бит).
Поэтому невозможна такая ситуация, когда thumb-инструкция начинается по нечетному адресу. Учитывая
сказанное, последний бит адреса можно не кодировать. Таким образом, в итоге, в thumb-инструкции BL
кодируется смещение ± ≈ 2𝑀 от текущего адреса.
Остальные инструкции в функции: PUSH и POP работают почти так же как и описанные STMFD/LDMFD,
только регистр SP здесь не указывается явно. ADR работает также как и в предыдущем примере. MOVS
записывает 0 в регистр R0 для возврата нуля.
Оптимизирующий Xcode (LLVM) + Режим ARM
Xcode 4.6.3 без включенной оптимизации выдает слишком много лишнего кода, поэтому остановимся на
той версии, где как можно меньше инструкций: -O3.
Listing 1.8: Оптимизирующий Xcode (LLVM) + Режим ARM
__text:000028C4
__text:000028C4
__text:000028C8
__text:000028CC
__text:000028D0
__text:000028D4
__text:000028D8
__text:000028DC
__text:000028E0

_hello_world
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

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

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}

DCB "Hello world!",0

Инструкции STMFD и LDMFD нам уже знакомы.
Инструкция MOV просто записывает число 0x1686 в регистр R0, это смещение указывающее на строку
“Hello world!”.
Регистр R7, по стандарту принятому в [2] это frame pointer , о нем будет рассказано позже.
Инструкция MOVT R0, #0 записывает 0 в старшие 16 бит регистра. Дело в том, что обычная инструкция
MOV в режиме ARM может записывать какое-либо значение только в младшие 16 бит регистра, ведь, больше
нельзя закодировать в ней. Помните, что в режиме ARM опкоды всех инструкций ограничены длиной в
32 бита. Конечно, это ограничение не касается перемещений между регистрами. Поэтому для записи в
старшие биты (от 16-го по 31-го включительно) существует дополнительная команда MOVT. Впрочем, здесь
её использование избыточно, потому что инструкция ”MOV R0, #0x1686” выше итак обнулила старшую
часть регистра. Возможно, это недочет компилятора.
Инструкция ”ADD R0, PC, R0” прибавляет PC к R0, для вычисления действительного адреса строки
“Hello world!”, как нам уже известно, это “адресно-независимый код”, поэтому такая корректива необходима.
Инструкция BL вызывает puts() вместо printf().
Компилятор заменил вызов printf() на puts(). Действительно, printf() с одним агрументом это
почти аналог puts().
Почти, если принять условие что в строке не будет управляющих символов printf() начинающихся
со знака процента. Тогда эффект от работы этих двух функций будет разным 26 .
Зачем компилятор заменил один вызов на другой? Потому что puts() () работает быстрее 27 .
Видимо потому, что puts() проталкивает символы в stdout не сравнивая каждый со знаком процента.
Далее уже знакомая инструкция ”MOV R0, #0”, служащая для установки в 0 возвращаемого значения
функции.
26
27

Также нужно заметить, что puts() не требует символа перевода строки ’\n’ в конце строки, поэтому его здесь нет.
http://www.ciselant.de/projects/gcc_printf/gcc_printf.html

7

1.1. HELLO, WORLD!
Оптимизирующий Xcode (LLVM) + Режим thumb-2

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

По умолчанию, Xcode 4.6.3 генерирует код для режима thumb-2, примерно в такой манере:
Listing 1.9: Оптимизирующий Xcode (LLVM) + Режим thumb-2
__text:00002B6C
__text:00002B6C
__text:00002B6E
__text:00002B72
__text:00002B74
__text:00002B78
__text:00002B7A
__text:00002B7E
__text:00002B80

_hello_world
80
41
6F
C0
78
01
00
80

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

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). Так
что мы видим здесь что инструкции MOVW, MOVT.W и BLX начинаются с 0xFx.
Одна из thumb-2 инструкций это “MOVW R0, #0x13D8” — она записывает 16-битное число в младшую часть регистра R0.
Еще “MOVT.W R0, #0” — эта инструкция работает так же как и MOVT из предыдущего примера, но
она работает в thumb-2.
Помимо прочих отличий, здесь используется инструкция BLX вместо BL. Отличие в том, что помимо
сохранения адреса возврата в регистре LR и передаче управления в функцию puts(), происходит смена
режима процессора с thumb на ARM, либо наоборот. Здесь это нужно потому что инструкция, куда ведет
переход, выглядит так (она закодирована в режиме ARM):
__symbolstub1:00003FEC _puts
__symbolstub1:00003FEC 44 F0 9F E5

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

Итак, внимательный читатель может задать справделивый вопрос: почему бы не вызывать 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”
8

1.2. СТЕК
в мнемонике инструкции).

1.2

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

Стек

Стек в компьютерных науках — это одна из наиболее фундаментальных вещей 28 .
Технически, это просто блок памяти в памяти процесса + регистр ESP или RSP в x86, либо SP в ARM,
который указывает где-то в пределах этого блока.
Часто используемые инструкции для работы со стеком это PUSH и POP (в x86 и thumb-режиме ARM).
PUSH уменьшает ESP/RSP/SP на 4 в 32-битном режиме (или на 8 в 64-битном), затем записывает по адресу
на который указывает ESP/RSP/SP содержимое своего единственного операнда.
POP это обратная операция — сначала достает из указателя стека значение и помещает его в операнд
(который очень часто является регистром) и затем увеличивает указатель стека на 4 (или 8).
В самом начале, регистр-указатель указывает на конец стека. PUSH уменьшает регистр-указатель, а
POP — увеличивает. Конец стека находится в начале блока памяти выделенного под стек. Это странно, но
это так.
В процессоре ARM, тем не менее, есть поддержка стеков растущих как в сторону уменьшения, так и в
сторону увеличения.
Например, инструкции STMFD29 /LDMFD30 , STMED31 /LDMED32 предназначены для descending-стека, т.е.,
уменьшающегося. Инструкции STMFA33 /LMDFA34 , STMEA35 /LDMEA36 предназначены для ascending-стека,
т.е., увеличивающегося.

1.2.1

Для чего используется стек?

Сохранение адреса куда должно вернуться управление после вызова функции
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

. . . но тем не менее создает нужный код:
28

http://en.wikipedia.org/wiki/Call_stack
Store Multiple Full Descending
30
Load Multiple Full Descending
31
Store Multiple Empty Descending
32
Load Multiple Empty Descending
33
Store Multiple Full Ascending
34
Load Multiple Full Ascending
35
Store Multiple Empty Ascending
36
Load Multiple Empty Ascending
29

9

1.2. СТЕК

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

?f@@YAXXZ PROC
; File c:\tmp6\ss.cpp
; Line 2
push
ebp
mov
ebp, esp
; Line 3
call
?f@@YAXXZ
; Line 4
pop
ebp
ret
0
?f@@YAXXZ ENDP

; f

; f

; f

. . . причем, если включить оптимизацию (/Ox), то будет даже интереснее, без переполнения стека, но
работать будет корректно37 :
?f@@YAXXZ PROC
; File c:\tmp6\ss.cpp
; Line 2
$LL3@f:
; Line 3
jmp
SHORT $LL3@f
?f@@YAXXZ ENDP

; f

; f

GCC 4.4.1 генерирует точно такой же код в обоих случаях, хотя и не предупреждает о проблеме.
ARM Программы для ARM также используют стек для сохранения RA38 , куда нужно вернуться, но несколько иначе. Как уже упоминалось в секции “Hello, world!” (1.1.2), RA записывается в регистр LR (link register).
Но если есть необходимость вызывать какую-то другую функцию, и использовать регистр LR еще раз, его
значение желательно сохранить. Обычно, это происходит в прологе функции, часто мы видим там инструкцию вроде “PUSH R4-R7,LR” , а в эпилоге “POP R4-R7,PC” — так сохраняются регистры, которые
будут использоваться в текущей функции, в том числе LR.
Тем не менее, если некая функция не вызывает никаких более функций, в терминологии ARM она называется leaf function39 . Как следствие, “leaf”-функция не использует регистр LR. А если эта функция небольшая, использует мало регистров, она может не использовать стек вообще. Таким образом, в ARM возможен
вызов небольших “leaf” функций не используя стек. Это может быть быстрее чем в x86, ведь внешняя память для стека не используется 40 . Либо, это может быть полезным для тех ситуаций, когда память для стека
еще не выделена либо недоступна.
Передача параметров для функции
Самый распространенный способ передачи параметров в x86 называется “cdecl”:
push arg3
push arg2
push arg1
call f
add esp, 4*3

Вызываемая функция получает свои параметры также через указатель стека.
Следовательно, так будут расположены значения в стеке перед исполнением самой первой инструкции
ф-ции f():
∙ ESP — адрес возврата
∙ ESP+4 — arg1
∙ ESP+8 — arg2
37

здесь ирония
Адрес возврата
39
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka13785.html
40
Когда-то очень давно, на PDP-11 и VAX, на инструкцию CALL (вызов других функций) могло тратиться вплоть до 50% времени,
возможно из-за работы с памятью, поэтому считалось что много небольших функций это анти-паттерн [20, Chapter 4, Part II].
38

10

1.2. СТЕК
∙ ESP+0xA — arg3

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

См.также в соответствующем разделе о других способах передачи аргументов через стек (3.4).
Важно отметить, что, в общем, никто не заставляет программистов передавать параметры именно через
стек, это не является требованием к исполняемому коду.
Вы можете делать это совершенно иначе, не используя стек вообще.
К примеру, можно выделять в куче место для аргументов, заполнять их и передавать в функцию указатель на это место через EAX. И это вполне будет работать 41 .
Однако, так традиционно сложилось, что в x86 и ARM передача аргументов происходит именно через
стек.
Кстати, вызываемая ф-ция не имеет информации, сколько аргументов было ей было передано. Ф-ции Си
с переменным количеством аргументов (как printf()) определяют их количество по спецификатором
строки формата (начинающиеся со знака %). Если написать что-то вроде
printf("%d %d %d", 1234);

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

envp
argv
argc
main

Если вы объявляете main() как main() без аргументов, они, тем не менее, присутствуют в стеке, но
не используются. Если вы объявите main() как main(int argc, char *argv[]), вы будете использовать два аргумента, а третий останется для вашей ф-ции “невидимым”. Более того, можно даже объявить
main(int argc), и это будет работать.
Хранение локальных переменных
Функция может выделить для себя некоторое место в стеке для локальных переменных просто отодвинув
указатель стека глубже к концу стека.
Это снова не является необходимым требованием. Вы можете хранить локальные переменные где угодно. Но по традиции всё сложилось так.
x86: Функция alloca()
Интересен случай с функцией alloca()42 .
Эта функция работает как malloc(), но выделяет память прямо в стеке.
Память освобождать через free() не нужно, так как эпилог функции (3.1) вернет ESP назад в изначальное состояние и выделенная память просто аyнулируется.
Интересна реализация функции alloca().
Эта функция, если упрощенно, просто сдвигает ESP вглубь стека на столько байт сколько вам нужно и
возвращает ESP в качестве указателя на выделенный блок. Попробуем:
#include
#include

41
Например, в книге Дональда Кнута “Искусство программирования”, в разделе 1.4.1 посвященном подпрограммам [15, раздел 1.4.1], мы можем прочитать о возможности располагать параметры для вызываемой подпрограммы после инструкции JMP
передающей управление подпрограмме. Кнут описывает что это было особенно удобно для компьютеров System/360.
42
В MSVC, реализацию функции можно посмотреть в файлах alloca16.asm и chkstk.asm в C:\Program Files
(x86)\Microsoft Visual Studio 10.0\VC\crt\src\intel

11

1.2. СТЕК

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

void f()
{
char *buf=(char*)alloca (600);
_snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3);
puts (buf);
};

(Функция _snprintf() работает так же как и printf(), только вместо выдачи результата в stdout
(т.е., на терминал или в консоль), записывает его в буфер buf. puts() выдает содержимое буфера buf
в stdout. Конечно, можно было бы заменить оба этих вызова на один printf(), но мне нужно проиллюстрировать использование небольшого буфера.)
MSVC Компилируем (MSVC 2010):
Listing 1.10: 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
esi
__snprintf

push
call
add

esi
_puts
esp, 28

; 00000258H

; 0000001cH

...

Единственный параметр в alloca() передается через EAX, а не как обычно через стек 43 . После вызова alloca(), ESP теперь указывает на блок в 600 байт который мы можем использовать под buf.
GCC + Синтаксис Intel А GCC 4.4.1 обходится без вызова других функций:
Listing 1.11: GCC 4.7.3
.LC0:
.string "hi! %d, %d, %d\n"
f:
push
mov
push
sub
lea
and
mov
mov
mov
mov
mov
mov
call
mov
call
mov
leave
ret

43

ebp
ebp, esp
ebx
esp, 660
ebx, [esp+39]
ebx, -16
; выровнять указатель по 16-байтной границе
DWORD PTR [esp], ebx
; s
DWORD PTR [esp+20], 3
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]

Это потому что alloca() это не сколько функция, сколько т.е. compiler intrinsic (8)

12

1.3. PRINTF() С НЕСКОЛЬКИМИ АГРУМЕНТАМИ
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
GCC + Синтаксис AT&T Посмотрим на тот же код, только в синтаксисе AT&T:
Listing 1.12: 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

Всё то же самое что и в прошлом листинге.
N.B. Например, movl $3, 20(%esp) это аналог mov DWORD PTR [esp+20], 3 в Intel-синтаксисе
— при адресации памяти в виде регистр+смещение, это записывается в AT&T синтаксисе как смещение(%регистр).
(Windows) SEH
В стеке хранятся записи SEH44 для функции (если имеются) 45 .
Защита от переполнений буфера
Здесь больше об этом (1.14.2).

1.3

printf() с несколькими агрументами

Попробуем теперь немного расширить пример Hello, world! (1.1), написав в теле функции main():
printf("a=%d; b=%d; c=%d", 1, 2, 3);

1.3.1

x86

Компилируем при помощи 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

; 00000010H

Все почти то же, за исключением того, что теперь видно, что аргументы для printf() заталкиваются
в стек в обратном порядке: самый первый аргумент заталкивается последним.
44
45

Structured Exception Handling
О SEH: классическая статья Мэтта Питрека: http://www.microsoft.com/msj/0197/Exception/Exception.aspx

13

1.3. PRINTF() С НЕСКОЛЬКИМИ АГРУМЕНТАМИ
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
Кстати, вспомним что переменные типа int в 32-битной системе, как известно, имеет ширину 32 бита,
это 4 байта.
Итак, у нас всего 4 аргумента. 4 * 4 = 16 — именно 16 байт занимают в стеке указатель на строку плюс
еще 3 числа типа int.
Когда при помощи инструкции “ADD ESP, X” корректируется указатель стека ESP после вызова какойлибо функции, зачастую можно сделать вывод о том, сколько аргументов у вызываемой функции было,
разделив X на 4.
Конечно, это относится только к cdecl-методу передачи аргументов через стек.
См.также в соответствующем разделе о способах передачи аргументов через стек (3.4).
Иногда бывает так, что подряд идут несколько вызовов разных функций, но стек корректируется только
один раз, после последнего вызова:
push a1
push a2
call ...
...
push a1
call ...
...
push a1
push a2
push a3
call ...
add esp, 24

Скомпилируем то же самое в Linux при помощи GCC 4.4.1 и посмотрим в IDA что вышло:
main

proc near

var_10
var_C
var_8
var_4

=
=
=
=

main

push
mov
and
sub
mov
mov
mov
mov
mov
call
mov
leave
retn
endp

dword
dword
dword
dword

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
_printf
eax, 0

Можно сказать, что этот короткий код созданный GCC отличается от кода MSVC только способом помещения значений в стек. Здесь GCC снова работает со стеком напрямую без PUSH/POP.

1.3.2

ARM: 3 аргумента в printf()

В ARM традиционно принята такая схема передачи аргументов в функцию: 4 первых аргумента через регистры R0-R3, а остальные — через стек. Это немного похоже на то как аргументы передаются в fastcall (3.4.3)
или win64 (3.4.5).
Неоптимизирующий Keil + Режим ARM
Listing 1.13: Неоптимизирующий Keil + Режим ARM
.text:00000014
.text:00000014
.text:00000018
.text:0000001C
.text:00000020

printf_main1
10
03
02
01

40
30
20
10

2D
A0
A0
A0

E9
E3
E3
E3

STMFD
MOV
MOV
MOV

SP!, {R4,LR}
R3, #3
R2, #2
R1, #1

14

1.3. PRINTF() С НЕСКОЛЬКИМИ АГРУМЕНТАМИ
.text:00000024 1D 0E 8F E2
.text:00000028 0D 19 00 EB
.text:0000002C 10 80 BD E8

ADR
BL
LDMFD

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
R0, aADBDCD
__2printf
SP!, {R4,PC}

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

Итак, первые 4 аргумента передаются через регистры R0-R3, по порядку: указатель на формат-строку
для printf() в R0, затем 1 в R1, 2 в R2 и 3 в R3.
Пока что, здесь нет ничего необычного.
Оптимизирующий Keil + Режим ARM
Listing 1.14: Оптимизирующий Keil + Режим ARM
.text:00000014
.text:00000014
.text:00000014
.text:00000018
.text:0000001C
.text:00000020
.text:00000024

EXPORT printf_main1
printf_main1
03
02
01
1E
CB

30
20
10
0E
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. Но в LR находится адрес места, откуда была вызвана наша функция! А следовательно, управление из printf() вернется сразу туда. Следовательно, нет нужды сохранять
LR, потому что нет нужны модифицировать LR. А нет нужды модифицировать LR, потому что нет иных вызовов функций, кроме printf(), к тому же, после этого вызова не нужно ничего здесь больше делать!
Поэтому такая оптимизация возможна.
Еще один похожий пример описан в секции “switch()/case/default” , здесь (1.9.1).
Оптимизирующий Keil + Режим thumb
Listing 1.15: Оптимизирующий Keil + Режим thumb
.text:0000000C
.text:0000000C
.text:0000000E
.text:00000010
.text:00000012
.text:00000014
.text:00000016
.text:0000001A

printf_main1
10
03
02
01
A4
06
10

B5
23
22
21
A0
F0 EB F8
BD

PUSH
MOVS
MOVS
MOVS
ADR
BL
POP

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

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

Здесь нет особых отличий от неоптимизированного варианта для режима ARM.

1.3.3

ARM: 8 аргументов в printf()

Для того, чтобы посмотреть, как остальные аргументы будут передаваться через стек, изменим пример еще
раз, увеличив количество передаваемых аргументов до 9 (строка формата printf() и 8 переменных типа
int):
void printf_main2()
{
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);
};

15

1.3. PRINTF() С НЕСКОЛЬКИМИ АГРУМЕНТАМИ
Оптимизирующий Keil: Режим ARM
.text:00000028
.text:00000028
.text:00000028
.text:00000028
.text:00000028
.text:00000028
.text:00000028 04
.text:0000002C 14
.text:00000030 08
.text:00000034 07
.text:00000038 06
.text:0000003C 05
.text:00000040 04
.text:00000044 0F
.text:00000048 04
.text:0000004C 00
.text:00000050 03
.text:00000054 02
.text:00000058 01
.text:0000005C 6E
=%d; g=%"...
.text:00000060 BC
.text:00000064 14
.text:00000068 04

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

printf_main2
var_18
var_14
var_4
E0
D0
30
20
10
00
C0
00
00
00
30
20
10
0F

2D
4D
A0
A0
A0
A0
8D
8C
A0
8D
A0
A0
A0
8F

E5
E2
E3
E3
E3
E3
E2
E8
E3
E5
E3
E3
E3
E2

18 00 EB
D0 8D E2
F0 9D E4

= -0x18
= -0x14
= -4
STR
SUB
MOV
MOV
MOV
MOV
ADD
STMIA
MOV
STR
MOV
MOV
MOV
ADR

LR, [SP,#var_4]!
SP, SP, #0x14
R3, #8
R2, #7
R1, #6
R0, #5
R12, SP, #0x18+var_14
R12, {R0-R3}
R0, #4
R0, [SP,#0x18+var_18]
R3, #3
R2, #2
R1, #1
R0, aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d; d=%d; e=%d; f

BL
ADD
LDR

__2printf
SP, SP, #0x14
PC, [SP+4+var_4],#4

Этот код можно условно разделить на несколько частей:
∙ Пролог функции:
Самая первая инструкция “STR LR, [SP,#var_4]!” сохраняет в стеке LR, ведь, нам придется
использовать этот регистр для вызова printf().
Вторая инструкция “SUB SP, SP, #0x14” уменьшает указатель стека SP, но на самом деле, эта
процедура нужна для выделения в локальном стеке места размером 0x14 (20) байт. Действительно,
нам нужно передать 5 32-битных значений через стек в printf(), каждое значение занимает 4
байта, а 5 * 4 = 20 — как раз. Остальные 4 32-битных значения будут переданы через регистры.
∙ Передача 5, 6, 7 и 8 через стек:
Затем значения 5, 6, 7 и 8 записываются в регистры R0, R1, R2 и R3 соответственно. Затем инструкция “ADD R12, SP, #0x18+var_14” записывает в регистр R12 адрес места в стеке, куда будут
помещены эти 4 значения. var_14 это макрос ассемблера, равный −0𝑥14, такие макросы создает IDA,
чтобы удобнее было показывать, как код обращается к стеку. Макросы var_?, создаваемые IDA, отражают локальные переменные в стеке. Так что, в R12 будет записано 𝑆𝑃 + 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 равен −0𝑥18, смещение будет 0, так что, значение
из регистра R0 (4) запишется туда, куда указывает SP.
∙ Передача 1, 2 и 3 через регистры:
Значения для первых трех чисел (a, b, c) (1, 2, 3 соответственно) передаются в регистрах R1, R2 и R3
перед самим вызововм printf(), а остальные 5 значений передаются через стек, и вот как:
∙ Вызов printf():
∙ Эпилог функции:
Инструкция “ADD SP, SP, #0x14” возвращает SP на прежнее место, аннулируя таким образом,
всё что было записано в стеке. Конечно, то что было записано в стек, там пока и останется, но всё это
будет многократно перезаписано во время исполнения последующих функций.
Инструкция “LDR PC, [SP+4+var_4],#4” загружает в PC сохраненное значение LR из стека, таким образом, обеспечивая выход из функции.
16

1.3. PRINTF() С НЕСКОЛЬКИМИ АГРУМЕНТАМИ
Оптимизирующий Keil: Режим thumb
.text:0000001C
.text:0000001C
.text:0000001C
.text:0000001C
.text:0000001C
.text:0000001C
.text:0000001C 00
.text:0000001E 08
.text:00000020 85
.text:00000022 04
.text:00000024 07
.text:00000026 06
.text:00000028 05
.text:0000002A 01
.text:0000002C 07
.text:0000002E 04
.text:00000030 00
.text:00000032 03
.text:00000034 02
.text:00000036 01
.text:00000038 A0
=%d; g=%"...
.text:0000003A 06
.text:0000003E
.text:0000003E
.text:0000003E 05
.text:00000040 00

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

printf_main2
var_18
var_14
var_8

= -0x18
= -0x14
= -8

B5
23
B0
93
22
21
20
AB
C3
20
90
23
22
21
A0

PUSH
MOVS
SUB
STR
MOVS
MOVS
MOVS
ADD
STMIA
MOVS
STR
MOVS
MOVS
MOVS
ADR

{LR}
R3, #8
SP, SP, #0x14
R3, [SP,#0x18+var_8]
R2, #7
R1, #6
R0, #5
R3, SP, #0x18+var_14
R3!, {R0-R2}
R0, #4
R0, [SP,#0x18+var_18]
R3, #3
R2, #2
R1, #1
R0, aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d; d=%d; e=%d; f

F0 D9 F8

BL

__2printf

ADD
POP

SP, SP, #0x14
{PC}

loc_3E
B0
BD

; CODE XREF: example13_f+16

Это почти то же самое что и в предыдущем примере, только код для thumb и значения помещаются в
стек немного иначе: в начале 8 за первый раз, затем 5, 6, 7 за второй раз и 4 за третий раз.
Оптимизирующий Xcode (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
__text:0000292C
__text:00002930
__text:00002934
__text:00002938
__text:0000293C
__text:00002940
__text:00002944
__text:00002948
__text:0000294C
__text:00002950
__text:00002954
__text:00002958

_printf_main2
var_1C
var_C
80
0D
14
70
07
00
04
00
06
05
00
0A
08
01
02
03
10
A4
07
80

40
70
D0
05
C0
00
20
00
30
10
20
10
90
10
20
30
90
05
D0
80

2D
A0
4D
01
A0
40
A0
8F
A0
A0
8D
8D
A0
A0
A0
A0
8D
00
A0
BD

E9
E1
E2
E3
E3
E3
E3
E0
E3
E3
E5
E9
E3
E3
E3
E3
E5
EB
E1
E8

= -0x1C
= -0xC
STMFD
MOV
SUB
MOV
MOV
MOVT
MOV
ADD
MOV
MOV
STR
STMFA
MOV
MOV
MOV
MOV
STR
BL
MOV
LDMFD

SP!, {R7,LR}
R7, SP
SP, SP, #0x14
R0, #0x1570
R12, #7
R0, #0
R2, #4
R0, PC, R0
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, когда это
можно было бы сделать в одном месте. Однако, у оптимизирующего компилятора могут быть свои доводы
о том, как лучше составлять инструкции друг с другом для лучшей эффективности исполнения. Процессор обычно пытается исполнять одновременно идущие друг за другом инструкции. К примеру, инструкции

17

1.4. SCANF()
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
“MOVT R0, #0” и “ADD R0, PC, R0” не могут быть исполнены одновременно, потому что обе инструкции модифицируют регистр R0. А вот инструкции “MOVT R0, #0” и “MOV R2, #4” легко можно
исполнить одновременно, потому что эффекты от их исполнения никак не конфликтуют друг с другом. Вероятно, компилятор старается генерировать код именно таким образом, конечно, там где это возможно.
Оптимизирующий Xcode (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
__text:00002BC0
__text:00002BC4
__text:00002BC8
__text:00002BCA
__text:00002BCC
__text:00002BCE
__text:00002BD2
__text:00002BD6
__text:00002BD8

_printf_main2
var_1C
var_18
var_C
80
6F
85
41
4F
C0
04
78
06
05
0D
00
4F
8E
01
02
03
CD
01
05
80

B5
46
B0
F2
F0
F2
22
44
23
21
F1
92
F0
E8
21
22
23
F8
F0
B0
BD

D8 20
07 0C
00 00

04 0E
08 09
0A 10

10 90
0A EA

= -0x1C
= -0x18
= -0xC
PUSH
MOV
SUB
MOVW
MOV.W
MOVT.W
MOVS
ADD
MOVS
MOVS
ADD.W
STR
MOV.W
STMIA.W
MOVS
MOVS
MOVS
STR.W
BLX
ADD
POP

{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]
R9, #8
LR, {R1,R3,R12}
R1, #1
R2, #2
R3, #3
R9, [SP,#0x1C+var_C]
_printf
SP, SP, #0x14
{R7,PC}

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

1.3.4

Кстати

Кстати, разница между способом передачи параметров принятая в x86 и ARM, неплохо иллюстрирует тот
важный момент, что процессору, в общем, все равно как будут передаваться параметры функций. Можно
создать гипотетический компилятор, который будет передавать их при помощи указателя на структуру с
параметрами, не пользуясь стеком вообще.

1.4

scanf()

Теперь попробуем использовать scanf().
int main()
{
int x;
printf ("Enter X:\n");
scanf ("%d", &x);
printf ("You entered %d...\n", x);
return 0;
};

Да, согласен, использовать scanf() в наши времена для того чтобы спросить у пользователя что-то:
не самая хорошая идея. Но я хотел проиллюстрировать передачу указателя на int.

18

1.4. SCANF()

1.4.1

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

Об указателях

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

1.4.2

x86

Что получаем на ассемблере компилируя MSVC 2010:
CONST
SEGMENT
$SG3831
DB
’Enter X:’, 0aH, 00H
ORG $+2
$SG3832
DB
’%d’, 00H
ORG $+1
$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
call
_printf
add
esp, 4
lea
eax, DWORD PTR _x$[ebp]
push
eax
push
OFFSET $SG3832
call
_scanf
add
esp, 8
mov
ecx, DWORD PTR _x$[ebp]
push
ecx
push
OFFSET $SG3833
call
_printf
add
esp, 8
xor
eax, eax
mov
esp, ebp
pop
ebp
ret
0
_main
ENDP
_TEXT
ENDS

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

1.4. SCANF()
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
Следующая после пролога инструкция PUSH ECX не ставит своей целью сохранить значение регистра
ECX. (Заметьте отсутствие сооветствующей инструкции POP ECX в конце функции)
Она на самом деле выделяет в стеке 4 байта для хранения x в будущем.
Доступ к x будет осуществляться при помощи объявленного макроса _x$ (он равен -4) и регистра EBP
указывающего на текущий фрейм.
Вообще, во все время исполнения функции, EBP указывает на текущий фрейм и через EBP+смещение
можно иметь доступ как к локальным переменным функции, так и аргументам функции.
Можно было бы использовать ESP, но он во время исполнения функции постоянно меняется. Так что
можно сказать что EBP это замороженное состояние ESP на момент начала исполнения функции.
У функции scanf() в нашем примере два аргумента.
Первый — указатель на строку содержащую “%d” и второй — адрес переменной x.
Вначале адрес x помещается в регистр EAX при помощи инструкции lea eax, DWORD PTR _x$[ebp].
Инструкция LEA означает load effective address, но со временем она изменила свою функцию (11.5.6).
Можно сказать что в данном случае LEA просто помещает в EAX результат суммы значения в регистре
EBP и макроса _x$.
Это тоже что и lea eax, [ebp-4].
Итак, от значения EBP отнимается 4 и помещается в EAX. Далее значение EAX заталкивается в стек и
вызывается scanf().
После этого вызывается printf(). Первый аргумент вызова которого, строка: “You entered %d...\n”.
Второй аргумент: mov ecx, [ebp-4], эта инструкция помещает в ECX не адрес переменной x, а его
значение, что там сейчас находится.
Далее значение ECX заталкивается в стек и вызывается последний printf().
Попробуем тоже самое скомпилировать в Linux при помощи GCC 4.4.1:
main

proc near

var_20
var_1C
var_4

= dword ptr -20h
= dword ptr -1Ch
= dword ptr -4

main

push
mov
and
sub
mov
call
mov
lea
mov
mov
call
mov
mov
mov
mov
call
mov
leave
retn
endp

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

GCC заменил первый вызов printf() на puts(), почему это было сделано, уже было описано раннее (1.1.2).
Далее все как и прежде — параметры заталкиваются через стек при помощи MOV.

1.4.3

ARM

Оптимизирующий Keil + Режим thumb
.text:00000042
.text:00000042
.text:00000042
.text:00000042
.text:00000042 08 B5

scanf_main
var_8

= -8
PUSH

{R3,LR}

20

1.4. SCANF()
.text:00000044
.text:00000046
.text:0000004A
.text:0000004C
.text:0000004E
.text:00000052
.text:00000054
.text:00000056
.text:0000005A
.text:0000005C

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
A9
06
69
AA
06
00
A9
06
00
08

A0
F0 D3 F8
46
A0
F0 CD F8
99
A0
F0 CB F8
20
BD

ADR
BL
MOV
ADR
BL
LDR
ADR
BL
MOVS
POP

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

Чтобы scanf() мог вернуть значение, ему нужно передать указательна переменную типа int. int — 32битное значение, для его хранения нужно только 4 байта и оно помещается в 32-битный регистр. Место для
локальной переменной x выделяется в стеке, IDA наименовала её var_8, впрочем, место для нее выделять не
обязательно, т.к., указатель стека SP уже указывает на место, свободное для использования сразу же. Так что
значение указателя SP копируется в регистр R1, и вместе с format-строкой, передается в scanf(). Позже,
при помощи инструкции LDR, это значение перемещается из стека в регистр R1, чтобы быть переданным
в printf().
Варианты скомпилированные для ARM-режима процессора, а также варианты скомпилированные при
помощи Xcode LLVM, не очень отличаются от этого, так что, мы можем пропустить их здесь.

1.4.4

Глобальные переменные

x86
А что если переменная x из предыдущего примера будет глобальной переменной а не локальной? Тогда к
ней смогут обращаться из любого другого места, а не только из тела функции. Это снова не очень хорошая
практика программирования, но ради примера мы можем себе это позволить.
_DATA
SEGMENT
COMM
_x:DWORD
$SG2456
DB
’Enter X:’, 0aH, 00H
ORG $+2
$SG2457
DB
’%d’, 00H
ORG $+1
$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
push
eax
push
OFFSET $SG2458
call
_printf
add
esp, 8
xor
eax, eax
pop
ebp
ret
0
_main
ENDP
_TEXT
ENDS

Ничего особенного, в целом. Теперь x объявлена в сегменте _DATA. Память для нее в стеке более не
выделяется. Все обращения к ней происходит не через стек, а уже напрямую. Её значение неопределено.
Это означает, что память под нее будет выделена, но ни компилятор, ни ОС не будет заботиться о том, что
там будет лежать на момент старта функции main(). В качестве домашнего задания, попробуйте объявить
большой неопределенный массив и посмотреть что там будет лежать после загрузки.
21

1.4. SCANF()
Попробуем изменить объявление этой переменной:

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

int x=10; // default value

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

SEGMENT
DD
0aH

...

Здесь уже по месту этой переменной записано 0xA с типом DD (dword = 32 бита).
Если вы откроете скомпилированный .exe-файл в IDA, то увидите что x находится аккурат в начале
сегмента _DATA, после этой переменной будут текстовые строки.
А вот если вы откроете в IDA, .exe скомплированный в прошлом примере, где значение x неопределено,
то в 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

_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

_x обозначен как ?, наряду с другими переменными не требующими инициализции. Это означает, что
при загрузке .exe в память, место под все это выделено будет. Но в самом .exe ничего этого нет. Неинициализированные переменные не занимают места в исполняемых файлах. Удобно для больших массивов,
например.
В Linux все также почти. За исключением того что если значение x не определено, то эта переменная
будет находится в сегменте _bss. В ELF46 этот сегмент имеет такие аттрибуты:
; Segment type: Uninitialized
; Segment permissions: Read/Write

Ну а если сделать статическое присвоение этой переменной какого-либо значения, например 10, то она
будет находится в сегменте _data, это сегмент с такими аттрибутами:
; Segment type: Pure data
; Segment permissions: Read/Write

ARM: Оптимизирующий Keil + Режим thumb
.text:00000000 ; Segment type: Pure code
.text:00000000
AREA .text, CODE
...
.text:00000000 main
.text:00000000
PUSH
{R4,LR}
.text:00000002
ADR
R0, aEnterX
; "Enter X:\n"
.text:00000004
BL
__2printf
.text:00000008
LDR
R1, =x
.text:0000000A
ADR
R0, aD
; "%d"
.text:0000000C
BL
__0scanf
.text:00000010
LDR
R0, =x
.text:00000012
LDR
R1, [R0]
.text:00000014
ADR
R0, aYouEnteredD___ ; "You entered %d...\n"
.text:00000016
BL
__2printf
.text:0000001A
MOVS
R0, #0
.text:0000001C
POP
{R4,PC}
...
.text:00000020 aEnterX
DCB "Enter X:",0xA,0
; DATA XREF: main+2
.text:0000002A
DCB
0
46

Формат исполняемых файлов, использующийся в Linux и некоторых других *NIX

22

1.4. SCANF()
.text:0000002B
.text:0000002C
.text:0000002C
.text:00000030
.text:00000033
.text:00000034
.text:00000047
.text:00000047
.text:00000047
...
.data:00000048
.data:00000048
.data:00000048
.data:00000048
.data:00000048
.data:00000048
.data:00000048

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
off_2C

DCB
DCD x

0
; DATA XREF: main+8
; main+10
; DATA XREF: main+A

aD

DCB "%d",0
DCB
0
aYouEnteredD___ DCB "You entered %d...",0xA,0 ; DATA XREF: main+14
DCB 0
; .text
ends

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

; DATA XREF: main+8
; main+10

ends

Итак, переменная x теперь глобальная, и она расположена, почему-то, в другом сегменте, а именно
сегменте данных (.data). Можно спросить, почему текстовые строки расположены в сегменте кода (.text)
а x нельзя было разместить тут же? Потому что эта переменная, и как следует из определения, она может меняться. И может даже быть, меняться часто. Сегмент кода нередко может быть расположен в ПЗУ
микроконтроллера (не забывайте, мы сейчас имеем дело с embedded-микроэлектроникой, где дефицит
памяти это обычное дело), а изменяемые переменные — в ОЗУ. Хранить в ОЗУ неизменяемые данные,
когда в наличии есть ПЗУ, не экономно. К тому же, сегмент данных в ОЗУ с константами нужно было бы
инициализировать перед работой, ведь, после включения ОЗУ, очевидно, она содержит в себе случайную
информацию.
Далее, мы видим, в сегменте кода, хранится указатель на переменную x (off_2C) и вообще, все операции с переменной, происходят через этот указатель. Это связано с тем что переменная x может быть
расположена где-то довольно далеко от данного участка кода, так что её адрес нужно сохранить в непосредственной близости к этому коду. Инструкция LDR в thumb-режиме может адресовать только переменные в пределах вплоть до 1020 байт от места где она находится. Эта же инструкция в ARM-режиме —
переменные в пределах ±4095 байт, таким образом, адрес глобальной переменной x нужно расположить
в непосредственной близости, ведь нет никакой гарантии, что компоновщик47 сможет разместить саму переменную где-то рядом, она может быть даже в другом чипе памяти!
Еще одна вещь: если переменную объявить как const, то компилятор Keil разместит её в сегменте .constdata.
Должно быть, впоследствии, компоновщик и этот сегмент сможет разместить в ПЗУ, вместе с сегментом кода.

1.4.5

Проверка результата scanf()

x86
Как я уже упоминал, использовать scanf() в наше время это слегка старомодно. Но если уж жизнь заставила этим заниматься, нужно хотя бы проверять, сработал ли scanf() правильно или пользователь ввел
вместо числа что-то другое, что scanf() не смог трактовать как число.
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;
};

По стандарту, scanf()48 возвращает количество успешно полученных значений.
47
48

linker в англоязычной литературе
MSDN: scanf, wscanf

23

1.4. SCANF()
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
В нашем случае, если все успешно и пользователь ввел таки некое число, scanf() вернет 1. А если
нет, то 0 или EOF.
Я добавил код проверяющий результат scanf() и в случае ошибки, он сообщает пользователю что-то
другое.
Вот, что выходит на ассемблере (MSVC 2010):
; Line 8
lea
push
push
call
add
cmp
jne
; Line 9
mov
push
push
call
add
; Line 10
jmp
$LN2@main:
; Line 11
push
call
add
$LN1@main:
; Line 13
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.
Мы проверяем его инструкцией CMP EAX, 1 (CoMPare), то есть, сравниваем значение в EAX с 1.
Следующий за инструкцией CMP: условный переход JNE. Это означает Jump if Not Equal, то есть, условный
переход если не равно.
Итак, если EAX не равен 1, то JNE заставит перейти процессор по адресу указанном в операнде 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 сравнивает два значения
и выставляет флаги процессора49 . 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 без сохранения результата.
Код созданный при помощи GCC 4.4.1 в Linux практически такой же, если не считать мелких отличий,
которые мы уже рассмотрели раннее.
49

См.также о флагах x86-процессора: http://en.wikipedia.org/wiki/FLAGS_register_(computing).

24

1.5. ПЕРЕДАЧА ПАРАМЕТРОВ ЧЕРЕЗ СТЕК
ARM: Оптимизирующий Keil + Режим thumb

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

Listing 1.16: Оптимизирующий Keil + Режим thumb
var_8

= -8
PUSH
ADR
BL
MOV
ADR
BL
CMP
BEQ
ADR
BL

{R3,LR}
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 и BEQ50 .
CMP аналогична той что в x86, она отнимает один аргумент от второго и сохраняет флаги.
BEQ совершает переход по другому адресу, если операнды при сравнении были равны, либо если результат последнего вычисления был 0, либо если флаг Z равен 1. То же что и JZ в x86.
Всё остальное просто: исполнение разветвляется на две ветки, затем они сходятся там, где в R0 записывается 0 как возвращаемое из функции значение и происходит выход из функции.

1.5

Передача параметров через стек

Как мы уже успели заметить, вызывающая функция передает аргументы для вызываемой через стек. А как
вызываемая функция имеет к ним доступ?
#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.5.1

x86

Имеем в итоге (MSVC 2010 Express):
Listing 1.17: MSVC 2010 Express
_TEXT
SEGMENT
_a$ = 8
_b$ = 12
_c$ = 16
_f
PROC
; File c:\...\1.c
; Line 4
push
ebp
50

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

(PowerPC, ARM) Branch if Equal

25

1.5. ПЕРЕДАЧА ПАРАМЕТРОВ ЧЕРЕЗ СТЕК
mov
; Line 5
mov
imul
add
; Line 6
pop
ret
_f
ENDP
_main
PROC
; Line 9
push
mov
; Line 10
push
push
push
call
add
push
push
call
add
; Line 11
xor
; Line 12
pop
ret
_main
ENDP

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

ebp, esp
eax, DWORD PTR _a$[ebp]
eax, DWORD PTR _b$[ebp]
eax, DWORD PTR _c$[ebp]
ebp
0

ebp
ebp, esp
3
2
1
_f
esp, 12
eax
OFFSET $SG2463 ; ’%d’, 0aH, 00H
_printf
esp, 8

; 0000000cH

eax, eax
ebp
0

Итак, здесь видно: в функции main() заталкиваются три числа в стек и вызывается функция f(int,int,int).
Внутри f(), доступ к аргументам, также как и к локальным переменным, происходит через макросы: _a$
= 8, но разница в том, что эти смещения со знаком плюс, таким образом если прибавить макрос _a$ к
указателю на EBP, то адресуется внешняя часть стека относительно EBP.
Далее все более-менее просто: значение a помещается в EAX. Далее EAX умножается при помощи
инструкции IMUL на то что лежит в _b, так в EAX остается произведение51 этих двух значений. Далее к
регистру EAX прибавляется то что лежит в _c. Значение из EAX никуда не нужно перекладывать, оно уже
лежит где надо. Возвращаем управление вызываемой функции — она возьмет значение из EAX и отправит
его в printf().
Скомпилируем то же в GCC 4.4.1 и посмотрим результат в IDA:
Listing 1.18: 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

main

public main
proc near

var_10
var_C
var_8

= dword ptr -10h
= dword ptr -0Ch
= dword ptr -8
push
mov
and

51

ebp
ebp,
eax,
eax,
eax,
ebp

; CODE XREF: main+20
8
0Ch
10h

esp
[ebp+arg_0]
[ebp+arg_4]
[ebp+arg_8]

; DATA XREF: _start+17

ebp
ebp, esp
esp, 0FFFFFFF0h

результат умножения

26

1.5. ПЕРЕДАЧА ПАРАМЕТРОВ ЧЕРЕЗ СТЕК
sub
mov
mov
mov
call
mov
mov
mov
call
mov
leave
retn
endp

main

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

esp, 10h
; char *
[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

Практически то же самое, если не считать мелких отличий описанных раннее.

1.5.2

ARM

Неоптимизирующий Keil + Режим ARM
.text:000000A4
.text:000000A8
.text:000000AC
...
.text:000000B0
.text:000000B0
.text:000000B4
.text:000000B8
.text:000000BC
.text:000000C0
.text:000000C4
.text:000000C8
.text:000000CC
.text:000000D0
.text:000000D4
.text:000000D8

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
BL
MOV
MOV
ADR
BL
MOV
LDMFD

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

main
10
03
02
01
F7
00
04
5A
E3
00
10

40
20
10
00
FF
40
10
0F
18
00
80

2D
A0
A0
A0
FF
A0
A0
8F
00
A0
BD

E9
E3
E3
E3
EB
E1
E1
E2
EB
E3
E8

; "%d\n"

В функции main() просто вызываются две функции, в первую (f) передается три значения.
Как я уже упоминал, первые 4 значения, в ARM обычно передаются в первых 4-х регистрах (R0-R3).
Функция f, как видно, использует три первых регистра (R0-R2) как аргументы.
Инструкция MLA (Multiply Accumulate) перемножает два первых операнда (R3 и R1), прибавляет к произведению третий операнд (R2) и помещает результат в нулевой операнд (R0), через который, по стандарту,
возвращаются значения функций.
Умножение и сложение одновременно52 (Fused multiply–add) это много где применяемая операция,
кстати, аналогичной инструкции в x86 нет, если не считать новых FMA-инструкций53 в SIMD.
Самая первая инструкция MOV R3, R0, по видимому, избыточна (можно было бы обойтись только одной инструкцией MLA), компилятор не оптимизировал её, ведь, это компиляция без оптимизации.
Инструкция BX возвращает управление по адресу записанному в LR и, если нужно, переключает режимы процессора с thumb на ARM или наоборот. Это может быть необходимым потому, что, как мы видим,
функции f неизвестно, из какого кода она будет вызываться, из ARM или thumb. Поэтому, если она будет
вызываться из кода thumb, BX не только вернет управление в вызывающую функцию, но также переключит
процессор в режим thumb. Либо не переключит, если функция вызывалась из кода для режима ARM.
Оптимизирующий Keil + Режим 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, как раз, где вызываемая функция будет его читать и использовать.
52
53

wikipedia: Умножение-сложение
https://en.wikipedia.org/wiki/FMA_instruction_set

27

1.6. И ЕЩЕ НЕМНОГО О ВОЗВРАЩАЕМЫХ РЕЗУЛЬТАТАХ
Оптимизирующий Keil + Режим thumb
.text:0000005E 48 43
.text:00000060 80 18
.text:00000062 70 47

MULS
ADDS
BX

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

R0, R1
R0, R0, R2
LR

В режиме thumb, инструкция MLA недоступна, так что компилятору пришлось сгенерировать код, делающий обе операции по отдельности. Первая инструкция MULS умножает R0 на R1 оставляя результат в R1.
Вторая (ADDS) складывает результат и R2, оставляя результат в R0.

1.6

И еще немного о возвращаемых результатах

Резльутат выполнения функции в x86 обычно возвращается54 через регистр EAX, а если результат имеет
тип байт или символ (char), то в самой младшей части EAX — AL. Если функция возвращает число с плавающей запятой, то регистр FPU ST(0) будет использован. В ARM обычно результат возвращается в регистре
R0.

Кстати, что будет если возвращаемое значение в ф-ции 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().
Там, скорее всего, будет какие-то случайное число, оставшееся от работы вашей ф-ции. Так что, код завершения программы будет псевдослучайным.

Вернемся к тому факту, что возвращемое значение остается в регистре 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;
54

См.также: MSDN: Return Values (C++)

28

1.7. УКАЗАТЕЛИ

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

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]
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

; get_some_values

Имя внутреннего макроса для передачи указателя на структуру здесь это $T3853.

1.7

Указатели

Указатели также часто используются для возврата значений из функции (вспомните случай со scanf() (1.4)).
Например, когда функции нужно вернуть сразу два значения:
void f1 (int x, int y, int *sum, int *product)
{
*sum=x+y;
*product=x*y;
};
void main()
{
int sum, product;
f1(123, 456, &sum, &product);
printf ("sum=%d, product=%d\n", sum, product);
};

Это компилируется в:
Listing 1.19: Оптимизирующий MSVC 2010
CONST
SEGMENT
$SG3863 DB
$SG3864 DB
CONST
ENDS
_TEXT
SEGMENT
_x$ = 8
_y$ = 12
_sum$ = 16
_product$ = 20
f1 PROC
mov
mov
lea
imul
mov
push
mov
mov
mov
pop
ret
f1 ENDP
_product$ = -8
_sum$ = -4
_main
PROC
sub
lea

’sum=%d, product=%d’, 0aH, 00H
’sum=%d, product=%d’, 0aH, 00H

;
;
;
;

size
size
size
size

=
=
=
=

4
4
4
4

; f1
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
; f1
; size = 4
; size = 4
esp, 8
eax, DWORD PTR _product$[esp+8]

29

1.8. УСЛОВНЫЕ ПЕРЕХОДЫ
push
lea
push
push
push
call
mov
mov
push
push
push
call

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

eax
ecx, DWORD PTR _sum$[esp+12]
ecx
456
123
f1
; f1
edx, DWORD PTR _product$[esp+24]
eax, DWORD PTR _sum$[esp+24]
edx
eax
OFFSET $SG3863
_printf

; 000001c8H
; 0000007bH

...

См.также об references в Си++: (2.3).

1.8

Условные переходы

Об условных переходах.
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’, 0aH, 00H
call
_printf
add
esp, 4
$LN3@f_signed:
mov
ecx, DWORD PTR _a$[ebp]
cmp
ecx, DWORD PTR _b$[ebp]
jne
SHORT $LN2@f_signed

30

1.8. УСЛОВНЫЕ ПЕРЕХОДЫ

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

push
OFFSET $SG739 ; ’a==b’, 0aH, 00H
call
_printf
add
esp, 4
$LN2@f_signed:
mov
edx, DWORD PTR _a$[ebp]
cmp
edx, DWORD PTR _b$[ebp]
jge
SHORT $LN4@f_signed
push
OFFSET $SG741 ; ’ab\n”, а BLGT вызывает
printf(). Следовательно, эти инструкции с суффиксом -GT, исполнятся только в том случае, если значение в R0 (там 𝑎) было больше чем значение в R4 (там 𝑏).
Далее мы увидим инструкции ADREQ и BLEQ. Они работают так же как и ADR и BL, но исполнятся только
в случае если значения при сравнении были равны. Перед ними еще один CMP (ведь вызов printf() мог
испортить состояние флагов).
Далее мы увидим LDMGEFD, эта инструкция работает так же как и LDMFD55 , но сработает только в случае
если в результате сравнения одно из значений было больше или равно второму (Greater or Equal).
Смысл инструкции “LDMGEFD SP!, {R4-R6,PC}” в том, что это как бы эпилог функции, но он сработает только если 𝑎 >= 𝑏, только тогда работа функции закончится. Но если это не так, то есть 𝑎 < 𝑏, то
исполнение дойдет до следующей инструкции “LDMFD SP!, {R4-R6,LR}”, это еще один эпилог функции, эта инструкция восстанавливает состояние регистров R4-R6, но и LR вместо PC, таким образом, пока
55

Load Multiple Full Descending

32

1.8. УСЛОВНЫЕ ПЕРЕХОДЫ
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
что не делая возврата из функции. Последние две инструкции вызывают printf() со строкой «ab)
return a;
return b;
};

x86
Несмотря на кажущуюся простоту этой функции, понять как она работает будет чуть сложнее.
Вот что выдал MSVC 2010:
Listing 1.45: MSVC 2010
PUBLIC
_d_max
_TEXT
SEGMENT
_a$ = 8
; size = 8
_b$ = 16
; size = 8
_d_max
PROC
push
ebp
mov
ebp, esp
fld
QWORD PTR _b$[ebp]
; состояние стека сейчас: ST(0) = _b
; сравниваем _b (в ST(0)) и _a, затем выталкиваем значение из стека
fcomp

QWORD PTR _a$[ebp]

; стек теперь пустой
fnstsw ax
test
ah, 5
jp
SHORT $LN1@d_max
; мы здесь если if a>b
fld
QWORD PTR _a$[ebp]
jmp
SHORT $LN2@d_max
$LN1@d_max:
fld
QWORD PTR _b$[ebp]
$LN2@d_max:
pop
ebp
ret
0
_d_max
ENDP

Итак, FLD загружает _b в регистр ST(0).
FCOMP сравнивает содержимое ST(0) с тем что лежит в _a и выставляет биты C3/C2/C0 в регистре
статуса FPU. Это 16-битный регистр отражающий текущее состояние FPU.
Итак, биты C3/C2/C0 выставлены, но, к сожалению, у процессоров до Intel P6 82 нет инструкций условного перехода, проверяющих эти биты. Возможно, так сложилось исторически (вспомните о том что FPU
когда-то был вообще отдельным чипом). А у Intel P6 появились инструкции FCOMI/FCOMIP/FUCOMI/FUCOMIP —
делающие тоже самое, только напрямую модифицирующие флаги ZF/PF/CF.
После этого, инструкция FCOMP выдергивает одно значение из стека. Это отличает её от FCOM, которая
просто сравнивает значения, оставляя стек в таком же состоянии.
FNSTSW копирует содержимое регистра статуса в AX. Биты C3/C2/C0 занимают позиции, соответственно, 14, 10, 8, в этих позициях они и остаются в регистре AX, и все они расположены в старшей части регистра
— AH.
∙ Если b>a в нашем случае, то биты C3/C2/C0 должны быть выставлены так: 0, 0, 0.
∙ Если a>b, то биты будут выставлены: 0, 0, 1.
82

Intel P6 это Pentium Pro, Pentium II, и далее

60

1.13. РАБОТА С FPU
∙ Если a=b, то биты будут выставлены так: 1, 0, 0.

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

После исполнения test ah, 5, бит C3 и C1 сбросится в ноль, на позициях 0 и 2 (внутри регистра AH)
останутся соответственно C0 и C2.
Теперь немного о parity flag83 . Еще один замечательный рудимент:

One common reason to test the parity flag actually has nothing to do with parity. The FPU
has four condition flags (C0 to C3), but they can not be tested directly, and must instead be first
copied to the flags register. When this happens, C0 is placed in the carry flag, C2 in the parity
flag and C3 in the zero flag. The C2 flag is set when e.g. incomparable floating point values
(NaN or unsupported format) are compared with the FUCOM instructions.84

Этот флаг выставляется в 1 если количество единиц в последнем результате — четно. И в 0 если —
нечетно.
Таким образом, что мы имеем, флаг PF будет выставлен в 1, если C0 и C2 оба 1 или оба 0. И тогда
сработает последующий JP (jump if PF==1). Если мы вернемся чуть назад и посмотрим значения C3/C2/C0
для разных вариантов, то увидим, что условный переход JP сработает в двух случаях: если b>a или если
a==b (ведь бит C3 уже вылетел после исполнения test ah, 5).
Дальше все просто. Если условный переход сработал, то FLD загрузит значение _b в ST(0), а если не
сработал, то загрузится _a и произойдет выход из функции.
Но это еще не все!
А теперь скомпилируем все это в MSVC 2010 с опцией /Ox
Listing 1.46: Оптимизирующий MSVC 2010
_a$ = 8
_b$ = 16
_d_max
fld
fld

; size = 8
; size = 8
PROC
QWORD PTR _b$[esp-4]
QWORD PTR _a$[esp-4]

; состояне стека сейчас: ST(0) = _a, ST(1) = _b
fcom
fnstsw
test
jne

ST(1) ; сравнить _a и ST(1) = (_b)
ax
ah, 65
; 00000041H
SHORT $LN5@d_max

; копировать содержимое ST(0) в ST(1) и вытолкнуть значение из стека,
; оставив _a на вершине
fstp
ST(1)
; состояние стека сейчас: ST(0) = _a
ret
0
$LN5@d_max:
; копировать содержимое ST(0) в ST(0) и вытолкнуть значение из стека,
; оставив _b на вершине
fstp
ST(0)
; состояние стека сейчас: ST(0) = _b
ret
_d_max

0
ENDP

FCOM отличается от FCOMP тем что просто сравнивает значения и оставляет стек в том же состоянии.
В отличие от предыдущего примера, операнды здесь в другом порядке. Поэтому и результат сравнения в
C3/C2/C0 будет другим чем раньше:
83

флаг четности

61

1.13. РАБОТА С FPU
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
∙ Если a>b в нашем случае, то биты C3/C2/C0 должны быть выставлены так: 0, 0, 0.
∙ Если b>a, то биты будут выставлены: 0, 0, 1.
∙ Если a=b, то биты будут выставлены так: 1, 0, 0.
Инструкция test ah, 65 как бы оставляет только два бита — C3 и C0. Они оба будут нулями, если
a>b: в таком случае переход JNE не сработает. Далее имеется инструкция FSTP ST(1) — эта инструкция
копирует значение ST(0) в указанный операнд и выдергивает одно значение из стека. В данном случае,
она копирует ST(0) (где сейчас лежит _a) в ST(1). После этого на вершине стека два раза лежат _a. Затем
одно значение выдергивается. После этого в ST(0) остается _a и функция завершается.
Условный переход JNE сработает в двух других случаях: если b>a или a==b. ST(0) скопируется в ST(0),
что как бы холостая операция, затем одно значение из стека вылетит и на вершине стека останется то что
до этого лежало в ST(1) (то есть, _b). И функция завершится. Эта инструкция используется здесь видимо
потому что в FPU нет инструкции которая просто выдергивает значение из стека и больше ничего.
Но и это еще не все.
GCC 4.4.1
Listing 1.47: GCC 4.4.1
d_max proc near
b
a
a_first_half
a_second_half
b_first_half
b_second_half
push
mov
sub

=
=
=
=
=
=

qword
qword
dword
dword
dword
dword

ptr -10h
ptr -8
ptr 8
ptr 0Ch
ptr 10h
ptr 14h

ebp
ebp, esp
esp, 10h

; переложим a и b в локальный стек:
mov
mov
mov
mov
mov
mov
mov
mov

eax, [ebp+a_first_half]
dword ptr [ebp+a], eax
eax, [ebp+a_second_half]
dword ptr [ebp+a+4], eax
eax, [ebp+b_first_half]
dword ptr [ebp+b], eax
eax, [ebp+b_second_half]
dword ptr [ebp+b+4], eax

; загружаем a и b в стек FPU
fld
fld

[ebp+a]
[ebp+b]

; текущее состояние стека: ST(0) - b; ST(1) - a
fxch

st(1) ; эта инструкция меняет ST(1) и ST(0) местами

; текущее состояние стека: ST(0) - a; ST(1) - b
fucompp
fnstsw
sahf
setnbe
test
jz
fld
jmp

; сравнить a и b и выдернуть из стека два значения, т.е., a и b
; записать статус FPU в AX
; загрузить состояние флагов SF, ZF, AF, PF, и CF из AH
al ; записать единицу в AL если CF=0 и ZF=0
al, al
; AL==0 ?
short loc_8048453 ; да
[ebp+a]
short locret_8048456
ax

loc_8048453:
fld
[ebp+b]
locret_8048456:

62

1.13. РАБОТА С FPU

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

leave
retn
d_max endp

FUCOMPP — это почти то же что и FCOM, только выкидывает из стека оба значения после сравнения, а
также несколько иначе реагирует на “не-числа”.
Немного о не-числах:
FPU умеет работать со специальными переменными, которые числами не являются и называются “не
числа” или NaN85 . Это бесконечность, результат деления на ноль, и так далее. Нечисла бывают “тихие” и
“сигнализирующие”. С первыми можно продолжать работать и далее, а вот если вы попытаетесь совершить
какую-то операцию с сигнализирующим нечислом, то сработает исключение.
Так вот, FCOM вызовет исключение если любой из операндов — какое-либо нечисло. FUCOM же вызовет
исключение только если один из операндов именно “сигнализирующее нечисло”.
Далее мы видим SAHF — это довольно редкая инструкция в коде не использущим FPU. 8 бит из AH
перекладываются в младшие 8 бит регистра статуса процессора в таком порядке: SF:ZF:-:AF:-:PF::CF b в нашем случае, то флаги будут выставлены так: ZF=0, PF=0, CF=0.
∙ Если ab.
Тогда в AL будет записана единица, последующий условный переход JZ взят не будет, и функция вернет
_a. В остальных случаях, функция вернет _b.
Но и это еще не конец.
GCC 4.4.1 с оптимизацией -O3
Listing 1.48: Оптимизирующий GCC 4.4.1
d_max

public d_max
proc near

arg_0
arg_8

= qword ptr
= qword ptr
push
mov
fld
fld

85
86

8
10h

ebp
ebp, esp
[ebp+arg_0] ; _a
[ebp+arg_8] ; _b

http://ru.wikipedia.org/wiki/NaN
cc это condition code

63

1.13. РАБОТА С FPU

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

; состояние стека сейчас: ST(0) = _b, ST(1) = _a
fxch
st(1)
; состояние стека сейчас: ST(0) = _a, ST(1) = _b
fucom
st(1) ; сравнить _a и _b
fnstsw ax
sahf
ja
short loc_8048448
; записать ST(0) в ST(0) (холостая операция), выкинуть значение лежащее на вершине стека, оставить _b
fstp
st
jmp
short loc_804844A
loc_8048448:
; записать _a в ST(0), выкинуть значение лежащее на вершине стека, оставить _a на вершине стека
fstp
st(1)
loc_804844A:

d_max

pop
retn
endp

ebp

Почти все что здесь есть уже описано мною, кроме одного: использование JA после SAHF. Действительно, инструкции условных переходов “больше”, “меньше”, “равно” для сравнения беззнаковых чисел (JA,
JAE, JBE, JBE, JE/JZ, JNA, JNAE, JNB, JNBE, JNE/JNZ) проверяют только флаги CF и ZF. И биты C3/C2/C0
после сравнения перекладываются в эти флаги аккурат так, чтобы перечисленные инструкции переходов
могли работать. JA сработает если CF и ZF обнулены.
Таким образом, перечисленные инструкции условного перехода можно использовать после инструкций
FNSTSW/SAHF.
Вполне возможно что биты статуса FPU C3/C2/C0 преднамерено были размещены таким образом, чтобы переноситься на базовые флаги процессора без перестановок.
ARM + Оптимизирующий Xcode (LLVM) + Режим ARM
Listing 1.49: Оптимизирующий Xcode (LLVM) + Режим ARM
VMOV
VMOV
VCMPE.F64
VMRS
VMOVGT.F64
VMOV
BX

D16, R2, R3 ; b
D17, R0, R1 ; a
D17, D16
APSR_nzcv, FPSCR
D16, D17 ; copy b to D16
R0, R1, D16
LR

Очень простой случай. Входные величины помещаются в D17 и D16 и сравниваются при помощи инструкции VCMPE. Как и в сопроцессорах x86, сопроцессор в ARM имеет свой собственный регистр статуса
и флагов, (FPSCR), потому как есть необходимость хранить специфичные для его работы флаги.
И так же как и в x86, в ARM нет инструкций условного перехода, проверяющих биты в регистре статуса
сопроцессора, так что имеется инструкция VMRS , копирующая 4 бита (N, Z, C, V) из статуса сопроцессора в
биты общего статуса (регистр APSR).
VMOVGT это аналог MOVGT, инструкция, сработающая если при сравнении один операнд был больше
чем второй (GT — Greater Than).
Если она сработает, в D16 запишется значение 𝑏, лежащее в тот момент в D17.
А если не сработает, то в D16 останется лежать значение 𝑎.
Предпоследняя инструкция VMOV подготовит то что было в D16 для возврата через пару регистров R0
и R1.
ARM + Оптимизирующий Xcode (LLVM) + Режим thumb-2
Listing 1.50: Оптимизирующий Xcode (LLVM) + Режим thumb-2
VMOV
VMOV
VCMPE.F64

D16, R2, R3 ; b
D17, R0, R1 ; a
D17, D16

64

1.13. РАБОТА С FPU
VMRS
IT GT
VMOVGT.F64
VMOV
BX

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

APSR_nzcv, FPSCR
D16, D17
R0, R1, D16
LR

Почти то же самое что и в предыдущем примере, за парой отличий. Дело в том, многие инструкции в
режиме ARM можно дополнять условием, которое если справедливо, то инструкция выполнится.
Но в режиме thumb такого нет. В 16-битных инструкций просто нет места для лишних 4 битов, при
помощи которых можно было бы закодировать условие выполнения.
Поэтому в thumb-2 добавили возможность дополнять thumb-инструкции условиями.
Здесь, в листинге сгенерированном при помощи IDA, мы видим инструкцию VMOVGT, такую же как и в
предыдущем примере.
Но в реальности, там закодирована обычная инструкция VMOV, просто IDA добавила суффикс -GT к ней,
потому что перед этой инструкцией стоит “IT GT”.
Инструкция IT определяет так называемый if-then block. После этой инструкции, можно указывать до
четырех инструкций, к которым будет добавлен суффикс условия. В нашем примере, “IT GT” означает,
что следующая за ней инструкция будет исполнена, если условие GT (Greater Than) справедливо.
Теперь более сложный пример, кстати, из “Angry Birds” (для iOS):
Listing 1.51: Angry Birds Classic
ITE NE
VMOVNE
VMOVEQ

R2, R3, D16
R2, R3, D17

ITE означает if-then-else и кодирует суффиксы для двух следующих за ней инструкций. Первая из них
исполнится, если условие закодированное в ITE (NE, not equal) будет в тот момент справедливо, а вторая
— если это условие не сработает. (Обратное условие от NE это EQ (equal)).
Еще чуть сложнее, и снова этот фрагмент из “Angry Birds”:
Listing 1.52: Angry Birds Classic
ITTTT EQ
MOVEQ
ADDEQ
POPEQ.W
POPEQ

R0, R4
SP, SP, #0x20
{R8,R10}
{R4-R7,PC}

4 символа “T” в инструкции означают что 4 следующие инструкции будут исполнены если условие соблюдается. Поэтому IDA добавила ко всем четырем инструкциям суффикс -EQ.
А если бы здесь было, например, ITEEE EQ (if-then-else-else-else), тогда суффиксы для следующих четырех инструкций были бы расставлены так:
-EQ
-NE
-NE
-NE

Еще фрагмент из “Angry Birds”:
Listing 1.53: Angry Birds Classic
CMP.W
ITTE LE
SUBLE.W
NEGLE
MOVGT

R0, #0xFFFFFFFF
R10, R0, #1
R0, R0
R10, R0

ITTE (if-then-then-else) означает что первая и вторая инструкции исполнятся, если условие LE (Less or
Equal) справедливо, а третья — если справедливо обратное условие (GT — Greater Than).
Компиляторы способны генерировать далеко не все варианты. Например, в вышеупомянутой игре “Angry
Birds” (версия classic для iOS) попадаются только такие варианты инструкции IT: IT, ITE, ITT, ITTE, ITTT,
ITTTT. Как я это узнал? В IDA можно сгенерировать листинг, так я и сделал, только в опциях я установил
так чтобы показывались 4 байта для каждого опкода. Затем, зная что старшая часть 16-битного опкода IT
это 0xBF, я сделал при помощи grep это:
65

1.13. РАБОТА С FPU

ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ

cat AngryBirdsClassic.lst | grep " BF" | grep "IT" > results.lst

Кстати, если писать на ассемблере для режима thumb-2 вручную, и дополнять инструкции суффиксами
условия, то ассемблер автоматически будет добавлять инструкцию IT с соответствующими флагами, там
где надо.
ARM + Неоптимизирующий Xcode (LLVM) + Режим ARM
Listing 1.54: Неоптимизирующий Xcode (LLVM) + Режим ARM
b
a
val_to_return
saved_R7

=
=
=
=

-0x20
-0x18
-0x10
-4

STR
MOV
SUB
BIC
VMOV
VMOV
VSTR
VSTR
VLDR
VLDR
VCMPE.F64
VMRS
BLE
VLDR
VSTR
B

R7, [SP,#saved_R7]!
R7, SP
SP, SP, #0x1C
SP, SP, #7
D16, R2, R3
D17, R0, R1
D17, [SP,#0x20+a]
D16, [SP,#0x20+b]
D16, [SP,#0x20+a]
D17, [SP,#0x20+b]
D16, D17
APSR_nzcv, FPSCR
loc_2E08
D16, [SP,#0x20+a]
D16, [SP,#0x20+val_to_return]
loc_2E10

VLDR
VSTR

D16, [SP,#0x20+b]
D16, [SP,#0x20+val_to_return]

VLDR
VMOV
MOV
LDR
BX

D16, [SP,#0x20+val_to_return]
R0, R1, D16
SP, R7
R7, [SP+0x20+b],#4
LR

loc_2E08

loc_2E10

Почти то же самое что мы уже видели, но много избыточного кода из-за хранения 𝑎 и 𝑏, а также выходного значения, в локальном стеке.
ARM + Оптимизирующий Keil + Режим thumb
Listing 1.55: Оптимизирующий Keil + Режим thumb
PUSH
MOVS
MOVS
MOVS
MOVS
BL
BCS
MOVS
MOVS
POP

{R3-R7,LR}
R4, R2
R5, R3
R6, R0
R7, R1
__aeabi_cdrcmple
loc_1C0
R0, R6
R1, R7
{R3-R7,PC}

MOVS
MOVS
POP

R0, R4
R1, R5
{R3-R7,PC}

loc_1C0

Keil не генерирует специальную инструкцию для сравнения чисел с плавающей запятой, потому что не
расчитывает на то что она будет поддерживаться, а простым сравнением побитово здесь не обойтись. Для
сравнения вызывается библиотечная функция __aeabi_cdrcmple. N.B. Результат сравнения эта функция
66

1.14. МАССИВЫ
ГЛАВА 1. ПАТТЕРНЫ КОМПИЛЯТОРОВ
оставляет в флагах, чтобы следующая за вызовом инструкция BCS (Carry set - Greater than or equal) могла
работать без дополнительного кода.

1.14

Массивы

Массив это просто набор переменных в памяти, обязательно лежащих рядом, и обязательно одного типа
87 ..

1.14.1

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

#include
int main()
{
int a[20];
int i;
for (i=0; iwidth=width;
this->height=height;
this->depth=depth;
};
void dump()
{
printf ("this is box. color=%d, width=%d, height=%d, depth=%d\n", color, width, height, depth)
;
};
};

Снова скомпилируем в MSVC 2008 с опциями /Ox и /Ob0 и посмотрим код метода box::dump():
?dump@box@@QAEXXZ PROC
; _this$ = ecx
mov
eax, DWORD PTR [ecx+12]

; box::dump, COMDAT

140

2.1. КЛАССЫ

ГЛАВА 2. СИ++

mov
edx, DWORD PTR [ecx+8]
push
eax
mov
eax, DWORD PTR [ecx+4]
mov
ecx, DWORD PTR [ecx]
push
edx
push
eax
push
ecx
; ’this is box. color=%d, width=%d, height=%d, depth=%d’, 0aH, 00H
push
OFFSET ??_C@_0DG@NCNGAADL@this?5is?5box?4?5color?$DN?$CFd?0?5width?$DN?$CFd?0@
call
_printf
add
esp, 20
; 00000014H
ret
0
?dump@box@@QAEXXZ ENDP
; box::dump

Разметка полей в классе выходит такой:
смещение
+0x0
+0x4
+0x8
+0xC

описание
int color
int width
int height
int depth

Все поля приватные и недоступные для модификации из других функций, но, зная эту разметку, сможем
ли мы создать код модифицирующий эти поля?
Для этого я добавил функцию hack_oop_encapsulation(), которая если обладает приведенным
ниже телом, то просто не скомпилируется:
void hack_oop_encapsulation(class box * o)
{
o->width=1; // that code can’t be compiled: "error C2248: ’box::width’ : cannot access private member
declared in class ’box’"
};

Тем не менее, если преобразовать тип box к типу указатель на массив int, и если модифицировать полученный массив int-ов, тогда всё получится.
void hack_oop_encapsulation(class box * o)
{
unsigned int *ptr_to_object=reinterpret_cast(o);
ptr_to_object[1]=123;
};

Код этой функции довольно прост — можно сказать, функция берет на вход указатель на массив int-ов
и записывает 123 во второй int:
?hack_oop_encapsulation@@YAXPAVbox@@@Z PROC
mov
eax, DWORD PTR _o$[esp-4]
mov
DWORD PTR [eax+4], 123
ret
0
?hack_oop_encapsulation@@YAXPAVbox@@@Z ENDP

; hack_oop_encapsulation
; 0000007bH
; hack_oop_encapsulation

Проверим, как это работает:
int main()
{
box b(1, 10, 20, 30);
b.dump();
hack_oop_encapsulation(&b);
b.dump();
return 0;
};

Запускаем:
this is box. color=1, width=10, height=20, depth=30
this is box. color=1, width=123, height=20, depth=30

141

2.1. КЛАССЫ
ГЛАВА 2. СИ++
Выходит, инкапсуляция это защита полей класса только на стадции компиляции. Компилятор Си++ не
позволит сгенерировать код прямо модифицирующий защищенные поля, тем не менее, используя грязные
трюки это вполне возможно.

2.1.4

Множественное наследование

Множественное наследование это создание класса наследующего поля и методы от двух или более классов.
Снова напишем простой пример:
#include
class box
{
public:
int width, height, depth;
box() { };
box(int width, int height, int depth)
{
this->width=width;
this->height=height;
this->depth=depth;
};
void dump()
{
printf ("this is box. width=%d, height=%d, depth=%d\n", width, height, depth);
};
int get_volume()
{
return width * height * depth;
};
};
class solid_object
{
public:
int density;
solid_object() { };
solid_object(int density)
{
this->density=density;
};
int get_density()
{
return density;
};
void dump()
{
printf ("this is solid_object. density=%d\n", density);
};
};
class solid_box: box, solid_object
{
public:
solid_box (int width, int height, int depth, int density)
{
this->width=width;
this->height=height;
this->depth=depth;
this->density=density;
};
void dump()
{
printf ("this is solid_box. width=%d, height=%d, depth=%d, density=%d\n", width, height, depth
, density);
};
int get_weight() { return get_volume() * get_density(); };
};
int main()
{
box b(10, 20, 30);
solid_object so(100);
solid_box sb(10, 20, 30, 3);

142

2.1. КЛАССЫ

ГЛАВА 2. СИ++

b.dump();
so.dump();
sb.dump();
printf ("%d\n", sb.get_weight());
return 0;
};

Снова скомпилируем в MSVC 2008 с опциями /Ox и /Ob0 и посмотрим код методов box::dump(),
solid_object::dump(), solid_box::dump():
Listing 2.5: Оптимизирующий MSVC 2008 /Ob0
?dump@box@@QAEXXZ PROC
; box::dump, COMDAT
; _this$ = ecx
mov
eax, DWORD PTR [ecx+8]
mov
edx, DWORD PTR [ecx+4]
push
eax
mov
eax, DWORD PTR [ecx]
push
edx
push
eax
; ’this is box. width=%d, height=%d, depth=%d’, 0aH, 00H
push
OFFSET ??_C@_0CM@DIKPHDFI@this?5is?5box?4?5width?$DN?$CFd?0?5height?$DN?$CFd@
call
_printf
add
esp, 16
; 00000010H
ret
0
?dump@box@@QAEXXZ ENDP
; box::dump

Listing 2.6: Оптимизирующий MSVC 2008 /Ob0
?dump@solid_object@@QAEXXZ PROC
; solid_object::dump, COMDAT
; _this$ = ecx
mov
eax, DWORD PTR [ecx]
push
eax
; ’this is solid_object. density=%d’, 0aH
push
OFFSET ??_C@_0CC@KICFJINL@this?5is?5solid_object?4?5density?$DN?$CFd@
call
_printf
add
esp, 8
ret
0
?dump@solid_object@@QAEXXZ ENDP
; solid_object::dump

Listing 2.7: Оптимизирующий MSVC 2008 /Ob0
?dump@solid_box@@QAEXXZ PROC
; solid_box::dump, COMDAT
; _this$ = ecx
mov
eax, DWORD PTR [ecx+12]
mov
edx, DWORD PTR [ecx+8]
push
eax
mov
eax, DWORD PTR [ecx+4]
mov
ecx, DWORD PTR [ecx]
push
edx
push
eax
push
ecx
; ’this is solid_box. width=%d, height=%d, depth=%d, density=%d’, 0aH
push
OFFSET ??_C@_0DO@HNCNIHNN@this?5is?5solid_box?4?5width?$DN?$CFd?0?5hei@
call
_printf
add
esp, 20
; 00000014H
ret
0
?dump@solid_box@@QAEXXZ ENDP
; solid_box::dump

Выходит, имеем такую разметку в памяти для всех трех классов:
класс box:
смещение
+0x0
+0x4
+0x8

описание
width
height
depth

класс solid_object:

143

2.1. КЛАССЫ

ГЛАВА 2. СИ++
смещение
+0x0

описание
density

Можно сказать, что разметка класса solid_box будет объедененной:
класс solid_box:
смещение
+0x0
+0x4
+0x8
+0xC

описание
width
height
depth
density

Код методов box::get_volume() и solid_object::get_density() тривиален:
Listing 2.8: Оптимизирующий MSVC 2008 /Ob0
?get_volume@box@@QAEHXZ PROC
; _this$ = ecx
mov
eax, DWORD PTR [ecx+8]
imul
eax, DWORD PTR [ecx+4]
imul
eax, DWORD PTR [ecx]
ret
0
?get_volume@box@@QAEHXZ ENDP

; box::get_volume, COMDAT

; box::get_volume

Listing 2.9: Оптимизирующий MSVC 2008 /Ob0
?get_density@solid_object@@QAEHXZ PROC
; _this$ = ecx
mov
eax, DWORD PTR [ecx]
ret
0
?get_density@solid_object@@QAEHXZ ENDP

; solid_object::get_density, COMDAT

; solid_object::get_density

А вот код метода solid_box::get_weight() куда интереснее:
Listing 2.10: Оптимизирующий MSVC 2008 /Ob0
?get_weight@solid_box@@QAEHXZ PROC
; _this$ = ecx
push
esi
mov
esi, ecx
push
edi
lea
ecx, DWORD PTR [esi+12]
call
?get_density@solid_object@@QAEHXZ
mov
ecx, esi
mov
edi, eax
call
?get_volume@box@@QAEHXZ
imul
eax, edi
pop
edi
pop
esi
ret
0
?get_weight@solid_box@@QAEHXZ ENDP

; solid_box::get_weight, COMDAT

; solid_object::get_density

; box::get_volume

; solid_box::get_weight

get_weight() просто вызывает два метода, но для get_volume() он передает просто указатель на
this, а для get_density(), он передает указатель на this сдвинутый на 12 байт (либо 0xC байт), а там,
в разметке класса solid_box, как раз начинаются поля класса solid_object.
Так, метод solid_object::get_density() будет полагать что работает с обычным классом solid_object,
а метод box::get_volume() будет работать только со своими тремя полями, полагая, что работает с
обычным экземпляром класса box.
Таким образом, можно сказать, что экземпляр класса-наследника нескольких классов представляет в
памяти просто объедененный класс, содержащий все унаследованные поля. А каждый унаследованный метод вызывается с передачей ему указателя на соответствующую часть структуры.

2.1.5

Виртуальные методы

И снова простой пример:

144

2.1. КЛАССЫ

ГЛАВА 2. СИ++

#include
class object
{
public:
int color;
object() { };
object (int color) { this->color=color; };
virtual void dump()
{
printf ("color=%d\n", color);
};
};
class box : public object
{
private:
int width, height, depth;
public:
box(int color, int width, int height, int depth)
{
this->color=color;
this->width=width;
this->height=height;
this->depth=depth;
};
void dump()
{
printf ("this is box. color=%d, width=%d, height=%d, depth=%d\n", color, width, height, depth)
;
};
};
class sphere : public object
{
private:
int radius;
public:
sphere(int color, int radius)
{
this->color=color;
this->radius=radius;
};
void dump()
{
printf ("this is sphere. color=%d, radius=%d\n", color, radius);
};
};
int main()
{
box b(1, 10, 20, 30);
sphere s(2, 40);
object *o1=&b;
object *o2=&s;
o1->dump();
o2->dump();
return 0;
};

У класса object есть виртуальный метод dump(), впоследствии заменяемый в классах-наследниках box
и sphere.
Если в какой-то среде, где неизвестно, какого типа является экземпляр класса, как в функции main()
в примере, вызывается виртуальный метод dump(), где-то должна сохраняться информация о том, какой
же метод в итоге вызвать.
Скомпилируем в MSVC 2008 с опциями /Ox и /Ob0 и посмотрим код функции main():
_s$ = -32
_b$ = -20
_main
PROC
sub
push

; size = 12
; size = 20
esp, 32
30

; 00000020H
; 0000001eH

145

2.1.КЛАССЫ

_main

push
push
push
lea
call
push
push
lea
call
mov
mov
lea
call
mov
mov
lea
call
xor
add
ret
ENDP

ГЛАВА 2. СИ++
20
10
1
ecx, DWORD PTR _b$[esp+48]
??0box@@QAE@HHHH@Z
40
2
ecx, DWORD PTR _s$[esp+40]
??0sphere@@QAE@HH@Z
eax, DWORD PTR _b$[esp+32]
edx, DWORD PTR [eax]
ecx, DWORD PTR _b$[esp+32]
edx
eax, DWORD PTR _s$[esp+32]
edx, DWORD PTR [eax]
ecx, DWORD PTR _s$[esp+32]
edx
eax, eax
esp, 32
0

; 00000014H
; 0000000aH

; box::box
; 00000028H

; sphere::sphere

; 00000020H

Указатель на функцию dump() берется откуда-то из экземпляра класса (объекта). Где мог записаться
туда адрес нового метода-функции? Только в конструкторах, больше негде: ведь в функции main() ничего
более не вызывается. 7
Посмотрим код конструктора класса box:
??_R0?AVbox@@@8 DD FLAT:??_7type_info@@6B@
DD
00H
DB
’.?AVbox@@’, 00H

; box ‘RTTI Type Descriptor’

??_R1A@?0A@EA@box@@8 DD FLAT:??_R0?AVbox@@@8
DD
01H
DD
00H
DD
0ffffffffH
DD
00H
DD
040H
DD
FLAT:??_R3box@@8

; box::‘RTTI Base Class Descriptor at (0,-1,0,64)’

??_R2box@@8 DD
DD

FLAT:??_R1A@?0A@EA@box@@8
FLAT:??_R1A@?0A@EA@object@@8

; box::‘RTTI Base Class Array’

??_R3box@@8 DD
DD
DD
DD

00H
00H
02H
FLAT:??_R2box@@8

; box::‘RTTI Class Hierarchy Descriptor’

??_R4box@@6B@ DD 00H
DD
00H
DD
00H
DD
FLAT:??_R0?AVbox@@@8
DD
FLAT:??_R3box@@8

; box::‘RTTI Complete Object Locator’

??_7box@@6B@ DD FLAT:??_R4box@@6B@
DD
FLAT:?dump@box@@UAEXXZ

; box::‘vftable’

_color$ = 8
_width$ = 12
_height$ = 16
_depth$ = 20
??0box@@QAE@HHHH@Z PROC
; _this$ = ecx
push
esi
mov
esi, ecx
call
??0object@@QAE@XZ
mov
eax, DWORD PTR _color$[esp]
mov
ecx, DWORD PTR _width$[esp]
mov
edx, DWORD PTR _height$[esp]
mov
DWORD PTR [esi+4], eax
mov
eax, DWORD PTR _depth$[esp]
mov
DWORD PTR [esi+16], eax
mov
DWORD PTR [esi], OFFSET ??_7box@@6B@
mov
DWORD PTR [esi+8], ecx

;
;
;
;
;

7

size = 4
size = 4
size = 4
size = 4
box::box, COMDAT

; object::object

Об указателях на функции читайте больше в соответствующем разделе:(1.18)

146

2.2. OSTREAM

ГЛАВА 2. СИ++

mov
DWORD PTR [esi+12], edx
mov
eax, esi
pop
esi
ret
16
??0box@@QAE@HHHH@Z ENDP

; 00000010H
; box::box

Здесь мы видим что разметка класса немного другая: в качестве первого поля имеется указатель на
некую таблицу box::‘vftable’ (название оставлено компилятором MSVC).
В этой таблице есть ссылка на таблицу с названием box::‘RTTI Complete Object Locator’ и
еще ссылка на метод box::dump(). Итак, это называется таблица виртуальных методов и RTTI8 . Таблица
виртуальных методов хранит в себе адреса методов, а RTTI хранит информацию о типах вообще. Кстати,
RTTI-таблицы это именно те таблицы, информация из которых используются при вызове dynamic_cast и
typeid в С++. Вы можете увидеть что здесь хранится даже имя класса в виде обычной строки. Так, какойнибудь метод базового класса object может вызвать виртуальный метод object::dump() что в итоге
вызовет нужный метод унаследованного класса, потому что информация о нем присутствует прямо в этой
структуре класса.
Работа с этими таблицами и поиск адреса нужного метода, занимает какое-то время процессора, возможно, поэтому считается что работа с виртуальными методами медленна.
В сгенерированном коде от GCC RTTI-таблицы устроены чуть-чуть иначе.

2.2

ostream

Начнем снова с примера типа “hello world”, на этот раз используя ostream:
#include
int main()
{
std::cout _Next, n->_Prev, n->x, n->y);
};
void dump_List_vals (struct List_node* n)
{
struct List_node* current=n;
for (;;)
{
dump_List_node (current);
current=current->_Next;
if (current==n) // end
break;
};
};
void dump_List_val (unsigned int *a)
{
#ifdef _MSC_VER
// GCC implementation doesn’t have "size" field
printf ("_Myhead=0x%p, _Mysize=%d\n", a[0], a[1]);
#endif
dump_List_vals ((struct List_node*)a[0]);
};
int main()
{
std::list l;
printf ("* empty list:\n");
dump_List_val((unsigned int*)(void*)&l);

155

2.4. STL

ГЛАВА 2. СИ++

struct a t1;
t1.x=1;
t1.y=2;
l.push_front (t1);
t1.x=3;
t1.y=4;
l.push_front (t1);
t1.x=5;
t1.y=6;
l.push_back (t1);
printf ("* 3-elements list:\n");
dump_List_val((unsigned int*)(void*)&l);
std::list::iterator tmp;
printf ("node at .begin:\n");
tmp=l.begin();
dump_List_node ((struct List_node *)*(void**)&tmp);
printf ("node at .end:\n");
tmp=l.end();
dump_List_node ((struct List_node *)*(void**)&tmp);
printf ("* let’s count from the begin:\n");
std::list::iterator it=l.begin();
printf ("1st element: %d %d\n", (*it).x, (*it).y);
it++;
printf ("2nd element: %d %d\n", (*it).x, (*it).y);
it++;
printf ("3rd element: %d %d\n", (*it).x, (*it).y);
it++;
printf ("element at .end(): %d %d\n", (*it).x, (*it).y);
printf ("* let’s count from the end:\n");
std::list::iterator it2=l.end();
printf ("element at .end(): %d %d\n", (*it2).x, (*it2).y);
it2--;
printf ("3rd element: %d %d\n", (*it2).x, (*it2).y);
it2--;
printf ("2nd element: %d %d\n", (*it2).x, (*it2).y);
it2--;
printf ("1st element: %d %d\n", (*it2).x, (*it2).y);
printf ("removing last element...\n");
l.pop_back();
dump_List_val((unsigned int*)(void*)&l);
};

GCC
Начнем с GCC.
При запуске увидим длинный вывод, будем разбирать его по частям.
* empty list:
ptr=0x0028fe90 _Next=0x0028fe90 _Prev=0x0028fe90 x=3 y=0

Видим пустой список. Не смотря на то что он пуст, имеется один элемент с мусором в переменных 𝑥 и
𝑦. Оба указателя “next” и “prev” указывают на себя:

156

2.4. STL

ГЛАВА 2. СИ++
list.begin()

Переменная
std::list

list.end()

Next
Prev
X=garbage
Y=garbage

Это тот момент, когда итераторы .begin и .end равны друг другу.
Вставим 3 элемента и список в памяти будет представлен так:
* 3-elements list:
ptr=0x000349a0 _Next=0x00034988
ptr=0x00034988 _Next=0x00034b40
ptr=0x00034b40 _Next=0x0028fe90
ptr=0x0028fe90 _Next=0x000349a0

_Prev=0x0028fe90
_Prev=0x000349a0
_Prev=0x00034988
_Prev=0x00034b40

x=3
x=1
x=5
x=5

y=4
y=2
y=6
y=6

Последний элемент всё еще на 0x0028fe90, он не будет передвинут куда-либо до самого уничтожения
списка. Он все еще содержит случайный мусор в полях 𝑥 и 𝑦 (5 и 6). Случайно совпало так, что эти значения
точно такие же как и в последнем элементе, но это не значит, что они имеют какое-то значение.
Вот как эти 3 элемента хранятся в памяти:
Переменная
std::list
list.begin()

list.end()

Next

Next

Next

Next

Prev

Prev

Prev

Prev

X=1-й элемент

X=2-й элемент

X=3-й элемент

X=мусор

Y=1-й элемент

Y=2-й элемент

Y=3-й элемент

Y=мусор

Переменная 𝑙 всегда указывает на первый элемент.
Итераторы .begin() и .end() ни на что не указывают, и вообще отсутствуют в памяти, но указатели на эти
элементы будут возвращены когда соответствующие методы будут вызваны.
Иметь элемент с “мусором” это очень популярная практика в реализации двусвязных списков. Без него,
многие операции были бы сложнее, и следовательно, медленнее.
Итератор на самом деле это просто указатель на элемент. list.begin() и list.end() просто возвращают
указатели.
node at .begin:
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
node at .end:
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034b40 x=5 y=6

Тот факт что список циркулярный, очень помогает: если иметь указатель только на первый элемент, т.е.,
тот что в переменной 𝑙, очень легко получить указатель на последний элемент, без необходимости обходить
все элементы списка. Вставка элемента в конец списка также быстра благодаря этой особенности.
157

2.4. STL
ГЛАВА 2. СИ++
operator- и operator++ просто выставляют текущее значение итератора на current_node->prev
или current_node->next. Обратные итераторы (.rbegin, .rend) работают точно также, только наоборот.
operator* на итераторе просто возвращает указатель на место в структуре, где начинается пользовательская структура, т.е., указатель на самый первый элемент структуры (𝑥).
Вставка в список и удаление очень просты: просто выделите новый элемент (или освободите) и исправьте все указатели так, чтобы они были верны.
Вот почему итератор может стать недействительным после удаления элемента: он может всё еще указывать на уже освобожденный элемент. И конечно же, информация из освобожденного элемента, на который
указывает итератор, не может использоваться более.
В реализации GCC (по крайней мере 4.8.1) не сохраняется текущая длина списка: это выливается в
медленный метод .size(): он должен пройти по всему списку считая элементы, просто потому что нет другого
способа получить эту информацию. Это означает что эта операция 𝑂(𝑛), т.е., она работает тем медленнее,
чем больше элементов в списке.
Listing 2.20: GCC 4.8.1 -O3 -fno-inline-small-functions
main

proc near
push
ebp
mov
ebp, esp
push
esi
push
ebx
and
esp, 0FFFFFFF0h
sub
esp, 20h
lea
ebx, [esp+10h]
mov
dword ptr [esp], offset s ; "* empty list:"
mov
[esp+10h], ebx
mov
[esp+14h], ebx
call
puts
mov
[esp], ebx
call
_Z13dump_List_valPj ; dump_List_val(uint *)
lea
esi, [esp+18h]
mov
[esp+4], esi
mov
[esp], ebx
mov
dword ptr [esp+18h], 1 ; X for new element
mov
dword ptr [esp+1Ch], 2 ; Y for new element
call
_ZNSt4listI1aSaIS0_EE10push_frontERKS0_ ; std::list::
push_front(a const&)
mov
[esp+4], esi
mov
[esp], ebx
mov
dword ptr [esp+18h], 3 ; X for new element
mov
dword ptr [esp+1Ch], 4 ; Y for new element
call
_ZNSt4listI1aSaIS0_EE10push_frontERKS0_ ; std::list::
push_front(a const&)
mov
dword ptr [esp], 10h
mov
dword ptr [esp+18h], 5 ; X for new element
mov
dword ptr [esp+1Ch], 6 ; Y for new element
call
_Znwj
; operator new(uint)
cmp
eax, 0FFFFFFF8h
jz
short loc_80002A6
mov
ecx, [esp+1Ch]
mov
edx, [esp+18h]
mov
[eax+0Ch], ecx
mov
[eax+8], edx

loc_80002A6:

; CODE XREF: main+86
mov
[esp+4], ebx
mov
[esp], eax
call
_ZNSt8__detail15_List_node_base7_M_hookEPS0_ ; std::__detail::_List_node_base::
_M_hook(std::__detail::_List_node_base*)
mov
dword ptr [esp], offset a3ElementsList ; "* 3-elements list:"
call
puts
mov
[esp], ebx
call
_Z13dump_List_valPj ; dump_List_val(uint *)
mov
dword ptr [esp], offset aNodeAt_begin ; "node at .begin:"
call
puts
mov
eax, [esp+10h]
mov
[esp], eax
call
_Z14dump_List_nodeP9List_node ; dump_List_node(List_node *)
mov
dword ptr [esp], offset aNodeAt_end ; "node at .end:"
call
puts
mov
[esp], ebx
call
_Z14dump_List_nodeP9List_node ; dump_List_node(List_node *)

158

2.4. STL
mov
call
mov
mov
mov
mov
mov
mov
mov
call
mov
mov
mov
mov
mov
mov
mov
call
mov
mov
mov
mov
mov
mov
mov
call
mov
mov
mov
mov
mov
mov
mov
call
mov
call
mov
mov
mov
mov
mov
mov
call
mov
mov
mov
mov
mov
mov
mov
call
mov
mov
mov
mov
mov
mov
mov
call
mov
mov
mov
mov
mov
mov
mov
call
mov
call
mov
mov
call
_M_unhook(void)
mov
call
mov

ГЛАВА 2. СИ++
dword ptr [esp], offset aLetSCountFromT ; "* let’s count from the begin:"
puts
esi, [esp+10h]
eax, [esi+0Ch]
[esp+0Ch], eax
eax, [esi+8]
dword ptr [esp+4], offset a1stElementDD ; "1st element: %d %d\n"
dword ptr [esp], 1
[esp+8], eax
__printf_chk
esi, [esi] ; operator++: get ->next pointer
eax, [esi+0Ch]
[esp+0Ch], eax
eax, [esi+8]
dword ptr [esp+4], offset a2ndElementDD ; "2nd element: %d %d\n"
dword ptr [esp], 1
[esp+8], eax
__printf_chk
esi, [esi] ; operator++: get ->next pointer
eax, [esi+0Ch]
[esp+0Ch], eax
eax, [esi+8]
dword ptr [esp+4], offset a3rdElementDD ; "3rd element: %d %d\n"
dword ptr [esp], 1
[esp+8], eax
__printf_chk
eax, [esi] ; operator++: get ->next pointer
edx, [eax+0Ch]
[esp+0Ch], edx
eax, [eax+8]
dword ptr [esp+4], offset aElementAt_endD ; "element at .end(): %d %d\n"
dword ptr [esp], 1
[esp+8], eax
__printf_chk
dword ptr [esp], offset aLetSCountFro_0 ; "* let’s count from the end:"
puts
eax, [esp+1Ch]
dword ptr [esp+4], offset aElementAt_endD ; "element at .end(): %d %d\n"
dword ptr [esp], 1
[esp+0Ch], eax
eax, [esp+18h]
[esp+8], eax
__printf_chk
esi, [esp+14h]
eax, [esi+0Ch]
[esp+0Ch], eax
eax, [esi+8]
dword ptr [esp+4], offset a3rdElementDD ; "3rd element: %d %d\n"
dword ptr [esp], 1
[esp+8], eax
__printf_chk
esi, [esi+4] ; operator--: get ->prev pointer
eax, [esi+0Ch]
[esp+0Ch], eax
eax, [esi+8]
dword ptr [esp+4], offset a2ndElementDD ; "2nd element: %d %d\n"
dword ptr [esp], 1
[esp+8], eax
__printf_chk
eax, [esi+4] ; operator--: get ->prev pointer
edx, [eax+0Ch]
[esp+0Ch], edx
eax, [eax+8]
dword ptr [esp+4], offset a1stElementDD ; "1st element: %d %d\n"
dword ptr [esp], 1
[esp+8], eax
__printf_chk
dword ptr [esp], offset aRemovingLastEl ; "removing last element..."
puts
esi, [esp+14h]
[esp], esi
_ZNSt8__detail15_List_node_base9_M_unhookEv ; std::__detail::_List_node_base::
[esp], esi
_ZdlPv
[esp], ebx

; void *
; operator delete(void *)

159

2.4. STL
call
mov
call
_M_clear(void)
lea
xor
pop
pop
pop
retn
main
endp

ГЛАВА 2. СИ++
_Z13dump_List_valPj ; dump_List_val(uint *)
[esp], ebx
_ZNSt10_List_baseI1aSaIS0_EE8_M_clearEv ; std::_List_base::
esp, [ebp-8]
eax, eax
ebx
esi
ebp

Listing 2.21: Весь вывод
* empty list:
ptr=0x0028fe90 _Next=0x0028fe90
* 3-elements list:
ptr=0x000349a0 _Next=0x00034988
ptr=0x00034988 _Next=0x00034b40
ptr=0x00034b40 _Next=0x0028fe90
ptr=0x0028fe90 _Next=0x000349a0
node at .begin:
ptr=0x000349a0 _Next=0x00034988
node at .end:
ptr=0x0028fe90 _Next=0x000349a0
* let’s count from the begin:
1st element: 3 4
2nd element: 1 2
3rd element: 5 6
element at .end(): 5 6
* let’s count from the end:
element at .end(): 5 6
3rd element: 5 6
2nd element: 1 2
1st element: 3 4
removing last element...
ptr=0x000349a0 _Next=0x00034988
ptr=0x00034988 _Next=0x0028fe90
ptr=0x0028fe90 _Next=0x000349a0

_Prev=0x0028fe90 x=3 y=0
_Prev=0x0028fe90
_Prev=0x000349a0
_Prev=0x00034988
_Prev=0x00034b40

x=3
x=1
x=5
x=5

y=4
y=2
y=6
y=6

_Prev=0x0028fe90 x=3 y=4
_Prev=0x00034b40 x=5 y=6

_Prev=0x0028fe90 x=3 y=4
_Prev=0x000349a0 x=1 y=2
_Prev=0x00034988 x=5 y=6

MSVC
Реализация MSVC (2012) точно такая же, только еще и сохраняет текущий размер списка. Это означает что
метод .size() очень быстр (𝑂(1)): просто прочитать одно значение из памяти. С другой стороны, переменная
хранящая размер должна корректироваться при каждой вставке/удалении.
Реализация MSVC также немного отлична в смысле расстановки элементов:
Переменная
std::list
list.end()

list.begin()

Next

Next

Next

Next

Prev

Prev

Prev

Prev

X=мусор

X=1-й элемент

X=2-й элемент

X=3-й элемент

Y=мусор

Y=1-й элемент

Y=2-й элемент

Y=3-й элемент

У GCC его элемент с “мусором” в самом конце списка, а у MSVC в самом начале.
160

2.4. STL

ГЛАВА 2. СИ++
Listing 2.22: MSVC 2012 /Fa2.asm /Ox /GS- /Ob1

_l$ = -16
; size = 8
_t1$ = -8
; size = 8
_main
PROC
sub
esp, 16
; 00000010H
push
ebx
push
esi
push
edi
push
0
push
0
lea
ecx, DWORD PTR _l$[esp+36]
mov
DWORD PTR _l$[esp+40], 0
; allocate first "garbage" element
call
?_Buynode0@?$_List_alloc@$0A@U?$_List_base_types@Ua@@V?
$allocator@Ua@@@std@@@std@@@std@@QAEPAU?$_List_node@Ua@@PAX@2@PAU32@0@Z ; std::_List_alloc::_Buynode0
mov
edi, DWORD PTR __imp__printf
mov
ebx, eax
push
OFFSET $SG40685 ; ’* empty list:’
mov
DWORD PTR _l$[esp+32], ebx
call
edi ; printf
lea
eax, DWORD PTR _l$[esp+32]
push
eax
call
?dump_List_val@@YAXPAI@Z
; dump_List_val
mov
esi, DWORD PTR [ebx]
add
esp, 8
lea
eax, DWORD PTR _t1$[esp+28]
push
eax
push
DWORD PTR [esi+4]
lea
ecx, DWORD PTR _l$[esp+36]
push
esi
mov
DWORD PTR _t1$[esp+40], 1 ; data for a new node
mov
DWORD PTR _t1$[esp+44], 2 ; data for a new node
; allocate new node
call
??$_Buynode@ABUa@@@?$_List_buy@Ua@@V?$allocator@Ua@@@std@@@std@@QAEPAU?
$_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ; std::_List_buy::_Buynode
mov
DWORD PTR [esi+4], eax
mov
ecx, DWORD PTR [eax+4]
mov
DWORD PTR _t1$[esp+28], 3 ; data for a new node
mov
DWORD PTR [ecx], eax
mov
esi, DWORD PTR [ebx]
lea
eax, DWORD PTR _t1$[esp+28]
push
eax
push
DWORD PTR [esi+4]
lea
ecx, DWORD PTR _l$[esp+36]
push
esi
mov
DWORD PTR _t1$[esp+44], 4 ; data for a new node
; allocate new node
call
??$_Buynode@ABUa@@@?$_List_buy@Ua@@V?$allocator@Ua@@@std@@@std@@QAEPAU?
$_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ; std::_List_buy::_Buynode
mov
DWORD PTR [esi+4], eax
mov
ecx, DWORD PTR [eax+4]
mov
DWORD PTR _t1$[esp+28], 5 ; data for a new node
mov
DWORD PTR [ecx], eax
lea
eax, DWORD PTR _t1$[esp+28]
push
eax
push
DWORD PTR [ebx+4]
lea
ecx, DWORD PTR _l$[esp+36]
push
ebx
mov
DWORD PTR _t1$[esp+44], 6 ; data for a new node
; allocate new node
call
??$_Buynode@ABUa@@@?$_List_buy@Ua@@V?$allocator@Ua@@@std@@@std@@QAEPAU?
$_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ; std::_List_buy::_Buynode
mov
DWORD PTR [ebx+4], eax
mov
ecx, DWORD PTR [eax+4]
push
OFFSET $SG40689 ; ’* 3-elements list:’
mov
DWORD PTR _l$[esp+36], 3
mov
DWORD PTR [ecx], eax
call
edi ; printf
lea
eax, DWORD PTR _l$[esp+32]
push
eax
call
?dump_List_val@@YAXPAI@Z
; dump_List_val
push
OFFSET $SG40831 ; ’node at .begin:’
call
edi ; printf
push
DWORD PTR [ebx] ; get next field of node $l$ variable points to
call
?dump_List_node@@YAXPAUList_node@@@Z
; dump_List_node

161

2.4. STL
push
call
push
call
push
call
mov
push
push
push
call
mov
push
push
push
call
mov
push
push
push
call
mov
add
push
push
push
call
push
call
push
push
push
call
mov
push
push
push
call
mov
push
push
push
call
mov
push
push
push
call
add
push
call
mov
add

ГЛАВА 2. СИ++
OFFSET $SG40835 ; ’node at .end:’
edi ; printf
ebx ; pointer to the node $l$ variable points to!
?dump_List_node@@YAXPAUList_node@@@Z
; dump_List_node
OFFSET $SG40839 ; ’* let’’s count from the begin:’
edi ; printf
esi, DWORD PTR [ebx] ; operator++: get ->next pointer
DWORD PTR [esi+12]
DWORD PTR [esi+8]
OFFSET $SG40846 ; ’1st element: %d %d’
edi ; printf
esi, DWORD PTR [esi] ; operator++: get ->next pointer
DWORD PTR [esi+12]
DWORD PTR [esi+8]
OFFSET $SG40848 ; ’2nd element: %d %d’
edi ; printf
esi, DWORD PTR [esi] ; operator++: get ->next pointer
DWORD PTR [esi+12]
DWORD PTR [esi+8]
OFFSET $SG40850 ; ’3rd element: %d %d’
edi ; printf
eax, DWORD PTR [esi] ; operator++: get ->next pointer
esp, 64
; 00000040H
DWORD PTR [eax+12]
DWORD PTR [eax+8]
OFFSET $SG40852 ; ’element at .end(): %d %d’
edi ; printf
OFFSET $SG40853 ; ’* let’’s count from the end:’
edi ; printf
DWORD PTR [ebx+12] ; use x and y fields from the node $l$ variable points to
DWORD PTR [ebx+8]
OFFSET $SG40860 ; ’element at .end(): %d %d’
edi ; printf
esi, DWORD PTR [ebx+4] ; operator--: get ->prev pointer
DWORD PTR [esi+12]
DWORD PTR [esi+8]
OFFSET $SG40862 ; ’3rd element: %d %d’
edi ; printf
esi, DWORD PTR [esi+4] ; operator--: get ->prev pointer
DWORD PTR [esi+12]
DWORD PTR [esi+8]
OFFSET $SG40864 ; ’2nd element: %d %d’
edi ; printf
eax, DWORD PTR [esi+4] ; operator--: get ->prev pointer
DWORD PTR [eax+12]
DWORD PTR [eax+8]
OFFSET $SG40866 ; ’1st element: %d %d’
edi ; printf
esp, 64
; 00000040H
OFFSET $SG40867 ; ’removing last element...’
edi ; printf
edx, DWORD PTR [ebx+4]
esp, 4

; prev=next?
; it is the only element, "garbage one"?
; if yes, do not delete it!
cmp
edx, ebx
je
SHORT $LN349@main
mov
ecx, DWORD PTR [edx+4]
mov
eax, DWORD PTR [edx]
mov
DWORD PTR [ecx], eax
mov
ecx, DWORD PTR [edx]
mov
eax, DWORD PTR [edx+4]
push
edx
mov
DWORD PTR [ecx+4], eax
call
??3@YAXPAX@Z
add
esp, 4
mov
DWORD PTR _l$[esp+32], 2
$LN349@main:
lea
eax, DWORD PTR _l$[esp+28]
push
eax
call
?dump_List_val@@YAXPAI@Z
mov
eax, DWORD PTR [ebx]
add
esp, 4
mov
DWORD PTR [ebx], ebx

; operator delete

; dump_List_val

162

2.4. STL
mov
cmp
je
$LL414@main:
mov
push
call
add
mov
cmp
jne
$LN412@main:
push
call
add
xor
pop
pop
pop
add
ret
_main
ENDP

ГЛАВА 2. СИ++
DWORD PTR [ebx+4], ebx
eax, ebx
SHORT $LN412@main
esi, DWORD PTR [eax]
eax
??3@YAXPAX@Z
esp, 4
eax, esi
esi, ebx
SHORT $LL414@main

; operator delete

ebx
??3@YAXPAX@Z
esp, 4
eax, eax
edi
esi
ebx
esp, 16
0

; operator delete

; 00000010H

В отличие от GCC, код MSVC выделяет элемент с “мусором” в самом начале ф-ции при помощи ф-ции
“Buynode”, она также используется и во время выделения остальных элементов (код GCC выделяет самый
первый элемент в локальном стеке).
Listing 2.23: Весь вывод
* empty list:
_Myhead=0x003CC258, _Mysize=0
ptr=0x003CC258 _Next=0x003CC258 _Prev=0x003CC258
* 3-elements list:
_Myhead=0x003CC258, _Mysize=3
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC2A0
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258
ptr=0x003CC270 _Next=0x003CC2A0 _Prev=0x003CC288
ptr=0x003CC2A0 _Next=0x003CC258 _Prev=0x003CC270
node at .begin:
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258
node at .end:
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC2A0
* let’s count from the begin:
1st element: 3 4
2nd element: 1 2
3rd element: 5 6
element at .end(): 6226002 4522072
* let’s count from the end:
element at .end(): 6226002 4522072
3rd element: 5 6
2nd element: 1 2
1st element: 3 4
removing last element...
_Myhead=0x003CC258, _Mysize=2
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC270
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258
ptr=0x003CC270 _Next=0x003CC258 _Prev=0x003CC288

x=6226002 y=4522072

x=6226002 y=4522072
x=3 y=4
x=1 y=2
x=5 y=6
x=3 y=4
x=6226002 y=4522072

x=6226002 y=4522072
x=3 y=4
x=1 y=2

C++11 std::forward_list
Это то же самое что и std::list, но только односвязный список, т.е., имеющий только поле “next” в каждом
элементе. Таким образом расход памяти меньше, но возможности идти по списку назад здесь нет.

2.4.3

std::vector

Я бы назвал std::vector “безопасной оболочкой (wrapper)” PODT11 массива в Си. Изнутри он очень
похож на std::string (2.4.1): он имеет указатель на буфер, указатель на конец массива и указатель на
конец буфера.
11

(C++) Plain Old Data Type

163

2.4. STL
ГЛАВА 2. СИ++
Элементы массива просто лежат в памяти впритык друг к другу, также как и в обычном массиве (1.14).
В C++11 появился метод .data() возвращающий указатель на этот буфер, это похоже на .c_str() в
std::string.
Выделенный буфер в куче может быть больше чем сам массив.
Реализации MSVC и GCC почти одинаковые, отличаются только имена полей в структуре12 , так что здесь
один исходник работающий для обоих компиляторов. И снова здесь Си-подобный код для вывода структуры std::vector:
#include
#include
#include
#include






struct vector_of_ints
{
// MSVC names:
int *Myfirst;
int *Mylast;
int *Myend;
// GCC structure is the same, names are: _M_start, _M_finish, _M_end_of_storage
};
void dump(struct vector_of_ints *in)
{
printf ("_Myfirst=0x%p, _Mylast=0x%p, _Myend=0x%p\n", in->Myfirst, in->Mylast, in->Myend);
size_t size=(in->Mylast-in->Myfirst);
size_t capacity=(in->Myend-in->Myfirst);
printf ("size=%d, capacity=%d\n", size, capacity);
for (size_t i=0; iMyfirst[i]);
};
int main()
{
std::vector c;
dump ((struct vector_of_ints*)(void*)&c);
c.push_back(1);
dump ((struct vector_of_ints*)(void*)&c);
c.push_back(2);
dump ((struct vector_of_ints*)(void*)&c);
c.push_back(3);
dump ((struct vector_of_ints*)(void*)&c);
c.push_back(4);
dump ((struct vector_of_ints*)(void*)&c);
c.reserve (6);
dump ((struct vector_of_ints*)(void*)&c);
c.push_back(5);
dump ((struct vector_of_ints*)(void*)&c);
c.push_back(6);
dump ((struct vector_of_ints*)(void*)&c);
printf ("%d\n", c.at(5)); // bounds checking
printf ("%d\n", c[8]); // operator[], no bounds checking
};

Примерный вывод программы скомпилированной в MSVC:
_Myfirst=0x00000000,
size=0, capacity=0
_Myfirst=0x0051CF48,
size=1, capacity=1
element 0: 1
_Myfirst=0x0051CF58,
size=2, capacity=2
element 0: 1
element 1: 2
_Myfirst=0x0051C278,
size=3, capacity=3
element 0: 1
element 1: 2
element 2: 3
_Myfirst=0x0051C290,
12

_Mylast=0x00000000, _Myend=0x00000000
_Mylast=0x0051CF4C, _Myend=0x0051CF4C

_Mylast=0x0051CF60, _Myend=0x0051CF60

_Mylast=0x0051C284, _Myend=0x0051C284

_Mylast=0x0051C2A0, _Myend=0x0051C2A0

внутренности GCC: http://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a01371.html

164

2.4. STL

ГЛАВА 2. СИ++

size=4, capacity=4
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0x0051B180, _Mylast=0x0051B190, _Myend=0x0051B198
size=4, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0x0051B180, _Mylast=0x0051B194, _Myend=0x0051B198
size=5, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
_Myfirst=0x0051B180, _Mylast=0x0051B198, _Myend=0x0051B198
size=6, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
element 5: 6
6
6619158

Как можно заметить, выделенного буфера в самом начале ф-ции main() пока нет. После первого
вызова push_back() буфер выделяется. И далее, после каждого вызова push_back() и длина массива и вместимость буфера (capacity) увеличиваются. Но адрес буфера также меняется, потому что вызов ф-ции push_back() перевыделяет буфер в куче каждый раз. Это дорогая операция, вот почему
очень важно предсказать размер будущего массива и зарезервировать место для него при помощи метода .reserve(). Самое последнее число это мусуор: там нет элементов массива в этом месте, вот откуда это случайное число. Это иллюстрация того факта что метод operator[] в std::vector не проверяет индекс на правильность. Метод .at() с другой стороны, проверяет, и подкидывает исключение
std::out_of_range в случае ошибки.
Давайте посмотрим код:
Listing 2.24: MSVC 2012 /GS- /Ob1
$SG52650 DB
$SG52651 DB

’%d’, 0aH, 00H
’%d’, 0aH, 00H

_this$ = -4
; size = 4
__Pos$ = 8
; size = 4
?at@?$vector@HV?$allocator@H@std@@@std@@QAEAAHI@Z PROC ; std::vector::at,
COMDAT
; _this$ = ecx
push
ebp
mov
ebp, esp
push
ecx
mov
DWORD PTR _this$[ebp], ecx
mov
eax, DWORD PTR _this$[ebp]
mov
ecx, DWORD PTR _this$[ebp]
mov
edx, DWORD PTR [eax+4]
sub
edx, DWORD PTR [ecx]
sar
edx, 2
cmp
edx, DWORD PTR __Pos$[ebp]
ja
SHORT $LN1@at
push
OFFSET ??_C@_0BM@NMJKDPPO@invalid?5vector?$DMT?$DO?5subscript?$AA@
call
DWORD PTR __imp_?_Xout_of_range@std@@YAXPBD@Z
$LN1@at:
mov
eax, DWORD PTR _this$[ebp]
mov
ecx, DWORD PTR [eax]
mov
edx, DWORD PTR __Pos$[ebp]
lea
eax, DWORD PTR [ecx+edx*4]
$LN3@at:
mov
esp, ebp
pop
ebp
ret
4
?at@?$vector@HV?$allocator@H@std@@@std@@QAEAAHI@Z ENDP ; std::vector::at

165

2.4. STL

ГЛАВА 2. СИ++

_c$ =
$T1 =
$T2 =
$T3 =
$T4 =
$T5 =
$T6 =
_main

-36
; size = 12
-24
; size = 4
-20
; size = 4
-16
; size = 4
-12
; size = 4
-8
; size = 4
-4
; size = 4
PROC
push
ebp
mov
ebp, esp
sub
esp, 36
; 00000024H
mov
DWORD PTR _c$[ebp], 0
; Myfirst
mov
DWORD PTR _c$[ebp+4], 0
; Mylast
mov
DWORD PTR _c$[ebp+8], 0
; Myend
lea
eax, DWORD PTR _c$[ebp]
push
eax
call
?dump@@YAXPAUvector_of_ints@@@Z
; dump
add
esp, 4
mov
DWORD PTR $T6[ebp], 1
lea
ecx, DWORD PTR $T6[ebp]
push
ecx
lea
ecx, DWORD PTR _c$[ebp]
call
?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector::push_back
lea
edx, DWORD PTR _c$[ebp]
push
edx
call
?dump@@YAXPAUvector_of_ints@@@Z
; dump
add
esp, 4
mov
DWORD PTR $T5[ebp], 2
lea
eax, DWORD PTR $T5[ebp]
push
eax
lea
ecx, DWORD PTR _c$[ebp]
call
?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector::push_back
lea
ecx, DWORD PTR _c$[ebp]
push
ecx
call
?dump@@YAXPAUvector_of_ints@@@Z
; dump
add
esp, 4
mov
DWORD PTR $T4[ebp], 3
lea
edx, DWORD PTR $T4[ebp]
push
edx
lea
ecx, DWORD PTR _c$[ebp]
call
?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector::push_back
lea
eax, DWORD PTR _c$[ebp]
push
eax
call
?dump@@YAXPAUvector_of_ints@@@Z
; dump
add
esp, 4
mov
DWORD PTR $T3[ebp], 4
lea
ecx, DWORD PTR $T3[ebp]
push
ecx
lea
ecx, DWORD PTR _c$[ebp]
call
?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector::push_back
lea
edx, DWORD PTR _c$[ebp]
push
edx
call
?dump@@YAXPAUvector_of_ints@@@Z
; dump
add
esp, 4
push
6
lea
ecx, DWORD PTR _c$[ebp]
call
?reserve@?$vector@HV?$allocator@H@std@@@std@@QAEXI@Z ; std::vector::reserve
lea
eax, DWORD PTR _c$[ebp]
push
eax
call
?dump@@YAXPAUvector_of_ints@@@Z
; dump
add
esp, 4
mov
DWORD PTR $T2[ebp], 5
lea
ecx, DWORD PTR $T2[ebp]
push
ecx
lea
ecx, DWORD PTR _c$[ebp]
call
?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector::push_back
lea
edx, DWORD PTR _c$[ebp]
push
edx
call
?dump@@YAXPAUvector_of_ints@@@Z
; dump

166

2.4. STL

ГЛАВА 2. СИ++

add
esp, 4
mov
DWORD PTR $T1[ebp], 6
lea
eax, DWORD PTR $T1[ebp]
push
eax
lea
ecx, DWORD PTR _c$[ebp]
call
?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector::push_back
lea
ecx, DWORD PTR _c$[ebp]
push
ecx
call
?dump@@YAXPAUvector_of_ints@@@Z
; dump
add
esp, 4
push
5
lea
ecx, DWORD PTR _c$[ebp]
call
?at@?$vector@HV?$allocator@H@std@@@std@@QAEAAHI@Z ; std::vector::at
mov
edx, DWORD PTR [eax]
push
edx
push
OFFSET $SG52650 ; ’%d’
call
DWORD PTR __imp__printf
add
esp, 8
mov
eax, 8
shl
eax, 2
mov
ecx, DWORD PTR _c$[ebp]
mov
edx, DWORD PTR [ecx+eax]
push
edx
push
OFFSET $SG52651 ; ’%d’
call
DWORD PTR __imp__printf
add
esp, 8
lea
ecx, DWORD PTR _c$[ebp]
call
?_Tidy@?$vector@HV?$allocator@H@std@@@std@@IAEXXZ ; std::vector::_Tidy
xor
eax, eax
mov
esp, ebp
pop
ebp
ret
0
_main
ENDP

Мы видим как метод .at() проверяет границы и подкидывает исключение в случае ошибки. Число,
которое выводит последний вызов printf() берется из памяти, без всяких проверок.
Читатель может спросить, почему бы не использовать переменные “size” и “capacity”, как это сделано в
std::string. Я подозреваю что это для более быстрой проверки границ. Но я не уверен.
Код генерируемый GCC почти такой же, в целом, но метод .at() вставлен прямо в код:
Listing 2.25: GCC 4.8.1 -fno-inline-small-functions -O1
main

proc near
push
ebp
mov
ebp, esp
push
edi
push
esi
push
ebx
and
esp, 0FFFFFFF0h
sub
esp, 20h
mov
dword ptr [esp+14h], 0
mov
dword ptr [esp+18h], 0
mov
dword ptr [esp+1Ch], 0
lea
eax, [esp+14h]
mov
[esp], eax
call
_Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov
dword ptr [esp+10h], 1
lea
eax, [esp+10h]
mov
[esp+4], eax
lea
eax, [esp+14h]
mov
[esp], eax
call
_ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector::
push_back(int const&)
lea
eax, [esp+14h]
mov
[esp], eax
call
_Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov
dword ptr [esp+10h], 2
lea
eax, [esp+10h]
mov
[esp+4], eax
lea
eax, [esp+14h]
mov
[esp], eax

167

2.4. STL

ГЛАВА 2. СИ++

call
_ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector::
push_back(int const&)
lea
eax, [esp+14h]
mov
[esp], eax
call
_Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov
dword ptr [esp+10h], 3
lea
eax, [esp+10h]
mov
[esp+4], eax
lea
eax, [esp+14h]
mov
[esp], eax
call
_ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector::
push_back(int const&)
lea
eax, [esp+14h]
mov
[esp], eax
call
_Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov
dword ptr [esp+10h], 4
lea
eax, [esp+10h]
mov
[esp+4], eax
lea
eax, [esp+14h]
mov
[esp], eax
call
_ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector::
push_back(int const&)
lea
eax, [esp+14h]
mov
[esp], eax
call
_Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov
ebx, [esp+14h]
mov
eax, [esp+1Ch]
sub
eax, ebx
cmp
eax, 17h
ja
short loc_80001CF
mov
edi, [esp+18h]
sub
edi, ebx
sar
edi, 2
mov
dword ptr [esp], 18h
call
_Znwj
; operator new(uint)
mov
esi, eax
test
edi, edi
jz
short loc_80001AD
lea
eax, ds:0[edi*4]
mov
[esp+8], eax
; n
mov
[esp+4], ebx
; src
mov
[esp], esi
; dest
call
memmove
loc_80001AD:
mov
test
jz
mov
call

; CODE XREF: main+F8
eax, [esp+14h]
eax, eax
short loc_80001BD
[esp], eax
; void *
_ZdlPv
; operator delete(void *)

mov
lea
mov
add
mov

; CODE XREF: main+117
[esp+14h], esi
eax, [esi+edi*4]
[esp+18h], eax
esi, 18h
[esp+1Ch], esi

loc_80001BD:

loc_80001CF:

; CODE XREF: main+DD
lea
eax, [esp+14h]
mov
[esp], eax
call
_Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov
dword ptr [esp+10h], 5
lea
eax, [esp+10h]
mov
[esp+4], eax
lea
eax, [esp+14h]
mov
[esp], eax
call
_ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector::
push_back(int const&)
lea
eax, [esp+14h]
mov
[esp], eax
call
_Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov
dword ptr [esp+10h], 6
lea
eax, [esp+10h]
mov
[esp+4], eax
lea
eax, [esp+14h]

168

2.4. STL

ГЛАВА 2. СИ++

mov
[esp], eax
call
_ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector::
push_back(int const&)
lea
eax, [esp+14h]
mov
[esp], eax
call
_Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov
eax, [esp+14h]
mov
edx, [esp+18h]
sub
edx, eax
cmp
edx, 17h
ja
short loc_8000246
mov
dword ptr [esp], offset aVector_m_range ; "vector::_M_range_check"
call
_ZSt20__throw_out_of_rangePKc ; std::__throw_out_of_range(char const*)
loc_8000246:

; CODE XREF: main+19C
mov
eax, [eax+14h]
mov
[esp+8], eax
mov
dword ptr [esp+4], offset aD ; "%d\n"
mov
dword ptr [esp], 1
call
__printf_chk
mov
eax, [esp+14h]
mov
eax, [eax+20h]
mov
[esp+8], eax
mov
dword ptr [esp+4], offset aD ; "%d\n"
mov
dword ptr [esp], 1
call
__printf_chk
mov
eax, [esp+14h]
test
eax, eax
jz
short loc_80002AC
mov
[esp], eax
; void *
call
_ZdlPv
; operator delete(void *)
jmp
short loc_80002AC
; --------------------------------------------------------------------------mov
ebx, eax
mov
edx, [esp+14h]
test
edx, edx
jz
short loc_80002A4
mov
[esp], edx
; void *
call
_ZdlPv
; operator delete(void *)
loc_80002A4:

; CODE XREF: main+1FE
mov
[esp], ebx
call
_Unwind_Resume
; --------------------------------------------------------------------------loc_80002AC:

; CODE XREF: main+1EA
; main+1F4
mov
lea
pop
pop
pop
pop

locret_80002B8:

main

eax, 0
esp, [ebp-0Ch]
ebx
esi
edi
ebp
; DATA XREF: .eh_frame:08000510
; .eh_frame:080005BC

retn
endp

Метод .reserve() точно также вставлен прямо в код main(). Он вызывает new() если буфер слишком мал для нового массива, вызывает memmove() для копирования содержимого буфера, и вызывает
delete() для освобождения старого буфера.
Посмотрим что выводит программа будучи скомпилированная GCC:
_Myfirst=0x(nil), _Mylast=0x(nil), _Myend=0x(nil)
size=0, capacity=0
_Myfirst=0x0x8257008, _Mylast=0x0x825700c, _Myend=0x0x825700c
size=1, capacity=1
element 0: 1
_Myfirst=0x0x8257018, _Mylast=0x0x8257020, _Myend=0x0x8257020
size=2, capacity=2
element 0: 1
element 1: 2
_Myfirst=0x0x8257028, _Mylast=0x0x8257034, _Myend=0x0x8257038
size=3, capacity=4

169

2.4. STL
element 0: 1
element 1: 2
element 2: 3
_Myfirst=0x0x8257028,
size=4, capacity=4
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0x0x8257040,
size=4, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0x0x8257040,
size=5, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
_Myfirst=0x0x8257040,
size=6, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
element 5: 6
6
0

ГЛАВА 2. СИ++

_Mylast=0x0x8257038, _Myend=0x0x8257038

_Mylast=0x0x8257050, _Myend=0x0x8257058

_Mylast=0x0x8257054, _Myend=0x0x8257058

_Mylast=0x0x8257058, _Myend=0x0x8257058

Мы можем заметить что буфер растет иначе чем в MSVC.
При помощи простых экспериментов становится ясно, что в реализации MSVC буфер увеличивается на
~50% каждый раз, когда он должен был увеличен, а у GCC он увеличивается на 100% каждый раз, т.е.,
удваивается.

2.4.4

std::map и std::set

Двоичное дерево это еще одна фундаментальная структура данных. Как следует из названия, это дерево,
но у каждого узла максимум 2 связи с другими узлами. Каждый узел имеет ключ и/или значение.
Обычно, именно при помощи двоичных деревьев реализуются “словари” пар ключ-значения (AKA “ассоциативные массивы”).
Двоичные деревья имеют по крайней мере три важных свойства:
∙ Все ключи всегда хранятся в отсортированном виде.
∙ Могут хранится ключи любых типов. Алгоритмы для работы с двоичными деревьями не зависят от
типа ключа, для работы им нужна только ф-ция для сравнения ключей.
∙ Поиск необходимого ключа относительно быстрый по сравнению со списками или массивами.
Очень простой пример: давайте сохраним вот эти числа в двоичном дереве: 0, 1, 2, 3, 5, 6, 9, 10, 11, 12,
20, 99, 100, 101, 107, 1001, 1010.

170

2.4. STL

ГЛАВА 2. СИ++
10

100

1

5

0

3

2

20

6

12

9

11

107

99

101

1001

1010

Все ключи меньше чем значение ключа узла, сохраняются по левой стороне. Все ключи больше чем
значение ключа узла, сохраняются по правой стороне.
Таким образом, алгоритм для поиска нужного ключа прост: если искомое значение меньше чем значение текущего узла: двигаемся влево, если больше: двигаемся вправо, останавливаемся если они равны.
Таким образом, алгоритм может искать числа, текстовые строки, итд, только при помощи ф-ции сравнения
ключей.
Все ключи имеют уникальные значения.
Учитывая это, нужно ≈ log2 𝑛 шагов для поиска ключа в сбалансированном дереве содержащем 𝑛 ключей. Это ≈ 10 шагов для ≈ 1000 ключей, или ≈ 13 шагов для ≈ 10000 ключей. Неплохо, но для этого дерево
всегда должно быть сбаланировано: т.е., ключи должны быть равномерно распределены на всех ярусах.
Операции вставки и удаления проводят дополнительную работу по обслуживанию дерева и сохранения
его в сбалансированном состоянии.
Известно несколько популярных алгоритмом балансировки, включая AVL-деревья и красно-черные деревья. Последний дополняет узел значением “цвета” для упрощения балансировки, таким образом каждый
узел может быть “красным” или “черным”.
Реализации std::map и std::set обоих GCC и MSVC используют красно-черные деревья.
std::set содержит только ключи. std::map это “расширенная” версия set: здесь имеется еще и значение (value) на каждом узле.
MSVC
#include
#include
#include
#include






// struct is not packed!
struct tree_node
{
struct tree_node *Left;
struct tree_node *Parent;
struct tree_node *Right;
char Color; // 0 - Red, 1 - Black
char Isnil;
//std::pair Myval;
unsigned int first; // called Myval in std::set
const char *second; // not present in std::set
};
struct tree_struct
{
struct tree_node *Myhead;
size_t Mysize;
};

171

2.4. STL

ГЛАВА 2. СИ++

void dump_tree_node (struct tree_node *n, bool is_set, bool traverse)
{
printf ("ptr=0x%p Left=0x%p Parent=0x%p Right=0x%p Color=%d Isnil=%d\n",
n, n->Left, n->Parent, n->Right, n->Color, n->Isnil);
if (n->Isnil==0)
{
if (is_set)
printf ("first=%d\n", n->first);
else
printf ("first=%d second=[%s]\n", n->first, n->second);
}
if (traverse)
{
if (n->Isnil==1)
dump_tree_node (n->Parent, is_set, true);
else
{
if (n->Left->Isnil==0)
dump_tree_node (n->Left, is_set, true);
if (n->Right->Isnil==0)
dump_tree_node (n->Right, is_set, true);
};
};
};
const char* ALOT_OF_TABS="\t\t\t\t\t\t\t\t\t\t\t";
void dump_as_tree (int tabs, struct tree_node *n, bool is_set)
{
if (is_set)
printf ("%d\n", n->first);
else
printf ("%d [%s]\n", n->first, n->second);
if (n->Left->Isnil==0)
{
printf ("%.*sL-------", tabs, ALOT_OF_TABS);
dump_as_tree (tabs+1, n->Left, is_set);
};
if (n->Right->Isnil==0)
{
printf ("%.*sR-------", tabs, ALOT_OF_TABS);
dump_as_tree (tabs+1, n->Right, is_set);
};
};
void dump_map_and_set(struct tree_struct *m, bool is_set)
{
printf ("ptr=0x%p, Myhead=0x%p, Mysize=%d\n", m, m->Myhead, m->Mysize);
dump_tree_node (m->Myhead, is_set, true);
printf ("As a tree:\n");
printf ("root----");
dump_as_tree (1, m->Myhead->Parent, is_set);
};
int main()
{
// map
std::map m;
m[10]="ten";
m[20]="twenty";
m[3]="three";
m[101]="one hundred one";
m[100]="one hundred";
m[12]="twelve";
m[107]="one hundred seven";
m[0]="zero";
m[1]="one";
m[6]="six";
m[99]="ninety-nine";
m[5]="five";
m[11]="eleven";
m[1001]="one thousand one";
m[1010]="one thousand ten";

172

2.4. STL

ГЛАВА 2. СИ++

m[2]="two";
m[9]="nine";
printf ("dumping m as map:\n");
dump_map_and_set ((struct tree_struct *)(void*)&m, false);
std::map::iterator it1=m.begin();
printf ("m.begin():\n");
dump_tree_node ((struct tree_node *)*(void**)&it1, false, false);
it1=m.end();
printf ("m.end():\n");
dump_tree_node ((struct tree_node *)*(void**)&it1, false, false);
// set
std::set s;
s.insert(123);
s.insert(456);
s.insert(11);
s.insert(12);
s.insert(100);
s.insert(1001);
printf ("dumping s as set:\n");
dump_map_and_set ((struct tree_struct *)(void*)&s, true);
std::set::iterator it2=s.begin();
printf ("s.begin():\n");
dump_tree_node ((struct tree_node *)*(void**)&it2, true, false);
it2=s.end();
printf ("s.end():\n");
dump_tree_node ((struct tree_node *)*(void**)&it2, true, false);
};

Listing 2.26: MSVC 2012
dumping m as map:
ptr=0x0020FE04, Myhead=0x005BB3A0, Mysize=17
ptr=0x005BB3A0 Left=0x005BB4A0 Parent=0x005BB3C0
ptr=0x005BB3C0 Left=0x005BB4C0 Parent=0x005BB3A0
first=10 second=[ten]
ptr=0x005BB4C0 Left=0x005BB4A0 Parent=0x005BB3C0
first=1 second=[one]
ptr=0x005BB4A0 Left=0x005BB3A0 Parent=0x005BB4C0
first=0 second=[zero]
ptr=0x005BB520 Left=0x005BB400 Parent=0x005BB4C0
first=5 second=[five]
ptr=0x005BB400 Left=0x005BB5A0 Parent=0x005BB520
first=3 second=[three]
ptr=0x005BB5A0 Left=0x005BB3A0 Parent=0x005BB400
first=2 second=[two]
ptr=0x005BB4E0 Left=0x005BB3A0 Parent=0x005BB520
first=6 second=[six]
ptr=0x005BB5C0 Left=0x005BB3A0 Parent=0x005BB4E0
first=9 second=[nine]
ptr=0x005BB440 Left=0x005BB3E0 Parent=0x005BB3C0
first=100 second=[one hundred]
ptr=0x005BB3E0 Left=0x005BB460 Parent=0x005BB440
first=20 second=[twenty]
ptr=0x005BB460 Left=0x005BB540 Parent=0x005BB3E0
first=12 second=[twelve]
ptr=0x005BB540 Left=0x005BB3A0 Parent=0x005BB460
first=11 second=[eleven]
ptr=0x005BB500 Left=0x005BB3A0 Parent=0x005BB3E0
first=99 second=[ninety-nine]
ptr=0x005BB480 Left=0x005BB420 Parent=0x005BB440
first=107 second=[one hundred seven]
ptr=0x005BB420 Left=0x005BB3A0 Parent=0x005BB480
first=101 second=[one hundred one]
ptr=0x005BB560 Left=0x005BB3A0 Parent=0x005BB480
first=1001 second=[one thousand one]
ptr=0x005BB580 Left=0x005BB3A0 Parent=0x005BB560
first=1010 second=[one thousand ten]
As a tree:
root----10 [ten]
L-------1 [one]
L-------0 [zero]
R-------5 [five]
L-------3 [three]

Right=0x005BB580 Color=1 Isnil=1
Right=0x005BB440 Color=1 Isnil=0
Right=0x005BB520 Color=1 Isnil=0
Right=0x005BB3A0 Color=1 Isnil=0
Right=0x005BB4E0 Color=0 Isnil=0
Right=0x005BB3A0 Color=1 Isnil=0
Right=0x005BB3A0 Color=0 Isnil=0
Right=0x005BB5C0 Color=1 Isnil=0
Right=0x005BB3A0 Color=0 Isnil=0
Right=0x005BB480 Color=1 Isnil=0
Right=0x005BB500 Color=0 Isnil=0
Right=0x005BB3A0 Color=1 Isnil=0
Right=0x005BB3A0 Color=0 Isnil=0
Right=0x005BB3A0 Color=1 Isnil=0
Right=0x005BB560 Color=0 Isnil=0
Right=0x005BB3A0 Color=1 Isnil=0
Right=0x005BB580 Color=1 Isnil=0
Right=0x005BB3A0 Color=0 Isnil=0

173

2.4. STL

ГЛАВА 2. СИ++

L-------2 [two]
R-------6 [six]
R-------9 [nine]
R-------100 [one hundred]
L-------20 [twenty]
L-------12 [twelve]
L-------11 [eleven]
R-------99 [ninety-nine]
R-------107 [one hundred seven]
L-------101 [one hundred one]
R-------1001 [one thousand one]
R-------1010 [one thousand ten]
m.begin():
ptr=0x005BB4A0 Left=0x005BB3A0 Parent=0x005BB4C0 Right=0x005BB3A0 Color=1 Isnil=0
first=0 second=[zero]
m.end():
ptr=0x005BB3A0 Left=0x005BB4A0 Parent=0x005BB3C0 Right=0x005BB580 Color=1 Isnil=1
dumping s as set:
ptr=0x0020FDFC, Myhead=0x005BB5E0, Mysize=6
ptr=0x005BB5E0 Left=0x005BB640 Parent=0x005BB600
ptr=0x005BB600 Left=0x005BB660 Parent=0x005BB5E0
first=123
ptr=0x005BB660 Left=0x005BB640 Parent=0x005BB600
first=12
ptr=0x005BB640 Left=0x005BB5E0 Parent=0x005BB660
first=11
ptr=0x005BB680 Left=0x005BB5E0 Parent=0x005BB660
first=100
ptr=0x005BB620 Left=0x005BB5E0 Parent=0x005BB600
first=456
ptr=0x005BB6A0 Left=0x005BB5E0 Parent=0x005BB620
first=1001
As a tree:
root----123
L-------12
L-------11
R-------100
R-------456
R-------1001
s.begin():
ptr=0x005BB640 Left=0x005BB5E0 Parent=0x005BB660
first=11
s.end():
ptr=0x005BB5E0 Left=0x005BB640 Parent=0x005BB600

Right=0x005BB6A0 Color=1 Isnil=1
Right=0x005BB620 Color=1 Isnil=0
Right=0x005BB680 Color=1 Isnil=0
Right=0x005BB5E0 Color=0 Isnil=0
Right=0x005BB5E0 Color=0 Isnil=0
Right=0x005BB6A0 Color=1 Isnil=0
Right=0x005BB5E0 Color=0 Isnil=0

Right=0x005BB5E0 Color=0 Isnil=0

Right=0x005BB6A0 Color=1 Isnil=1

Структура не запакована, так что оба значения типа char занимают по 4 байта.
В std::map, first и second могут быть представлены как одно значение типа std::pair. std::set
имеет только одно значение в этом месте структуры.
Текущий размер дерева всегда присутствует, как и в случае реализации std::list в MSVC (2.4.2).
Как и в случае с std::list, итераторы это просто указатели на узлы. Итератор .begin() указывает на минимальный ключ. Этот указатель нигде не сохранен (как в списках), минимальный ключ дерева
нужно находить каждый раз. operator- и operator++ перемещают указатель не текущий узел на узелпредшественник или узел-преемник, т.е., узлы содержащие предыдущий и следующий ключ. Алгоритмы
для всех этих операций описаны в [7].
Итератор .end() указывает на корневой узел, он имеет 1 в Isnil, что означает что у узла нет ключа
и/или значения. Так что его можно рассматривать как “landing zone” в HDD13 .
GCC
#include
#include
#include
#include
#include







struct map_pair
{
13

Hard disk drive

174

2.4. STL

ГЛАВА 2. СИ++

int key;
const char *value;
};
struct tree_node
{
int M_color; // 0 - Red, 1 - Black
struct tree_node *M_parent;
struct tree_node *M_left;
struct tree_node *M_right;
};
struct tree_struct
{
int M_key_compare;
struct tree_node M_header;
size_t M_node_count;
};
void dump_tree_node (struct tree_node *n, bool is_set, bool traverse, bool dump_keys_and_values)
{
printf ("ptr=0x%p M_left=0x%p M_parent=0x%p M_right=0x%p M_color=%d\n",
n, n->M_left, n->M_parent, n->M_right, n->M_color);
void *point_after_struct=((char*)n)+sizeof(struct tree_node);
if (dump_keys_and_values)
{
if (is_set)
printf ("key=%d\n", *(int*)point_after_struct);
else
{
struct map_pair *p=(struct map_pair *)point_after_struct;
printf ("key=%d value=[%s]\n", p->key, p->value);
};
};
if (traverse==false)
return;
if (n->M_left)
dump_tree_node (n->M_left, is_set, traverse, dump_keys_and_values);
if (n->M_right)
dump_tree_node (n->M_right, is_set, traverse, dump_keys_and_values);
};
const char* ALOT_OF_TABS="\t\t\t\t\t\t\t\t\t\t\t";
void dump_as_tree (int tabs, struct tree_node *n, bool is_set)
{
void *point_after_struct=((char*)n)+sizeof(struct tree_node);
if (is_set)
printf ("%d\n", *(int*)point_after_struct);
else
{
struct map_pair *p=(struct map_pair *)point_after_struct;
printf ("%d [%s]\n", p->key, p->value);
}
if (n->M_left)
{
printf ("%.*sL-------", tabs, ALOT_OF_TABS);
dump_as_tree (tabs+1, n->M_left, is_set);
};
if (n->M_right)
{
printf ("%.*sR-------", tabs, ALOT_OF_TABS);
dump_as_tree (tabs+1, n->M_right, is_set);
};
};
void dump_map_and_set(struct tree_struct *m, bool is_set)
{
printf ("ptr=0x%p, M_key_compare=0x%x, M_header=0x%p, M_node_count=%d\n",
m, m->M_key_compare, &m->M_header, m->M_node_count);

175

2.4. STL

ГЛАВА 2. СИ++

dump_tree_node (m->M_header.M_parent, is_set, true, true);
printf ("As a tree:\n");
printf ("root----");
dump_as_tree (1, m->M_header.M_parent, is_set);
};
int main()
{
// map
std::map m;
m[10]="ten";
m[20]="twenty";
m[3]="three";
m[101]="one hundred one";
m[100]="one hundred";
m[12]="twelve";
m[107]="one hundred seven";
m[0]="zero";
m[1]="one";
m[6]="six";
m[99]="ninety-nine";
m[5]="five";
m[11]="eleven";
m[1001]="one thousand one";
m[1010]="one thousand ten";
m[2]="two";
m[9]="nine";
printf ("dumping m as map:\n");
dump_map_and_set ((struct tree_struct *)(void*)&m, false);
std::map::iterator it1=m.begin();
printf ("m.begin():\n");
dump_tree_node ((struct tree_node *)*(void**)&it1, false, false, true);
it1=m.end();
printf ("m.end():\n");
dump_tree_node ((struct tree_node *)*(void**)&it1, false, false, false);
// set
std::set s;
s.insert(123);
s.insert(456);
s.insert(11);
s.insert(12);
s.insert(100);
s.insert(1001);
printf ("dumping s as set:\n");
dump_map_and_set ((struct tree_struct *)(void*)&s, true);
std::set::iterator it2=s.begin();
printf ("s.begin():\n");
dump_tree_node ((struct tree_node *)*(void**)&it2, true, false, true);
it2=s.end();
printf ("s.end():\n");
dump_tree_node ((struct tree_node *)*(void**)&it2, true, false, false);
};

Listing 2.27: GCC 4.8.1
dumping m as map:
ptr=0x0028FE3C, M_key_compare=0x402b70, M_header=0x0028FE40, M_node_count=17
ptr=0x007A4988 M_left=0x007A4C00 M_parent=0x0028FE40 M_right=0x007A4B80 M_color=1
key=10 value=[ten]
ptr=0x007A4C00 M_left=0x007A4BE0 M_parent=0x007A4988 M_right=0x007A4C60 M_color=1
key=1 value=[one]
ptr=0x007A4BE0 M_left=0x00000000 M_parent=0x007A4C00 M_right=0x00000000 M_color=1
key=0 value=[zero]
ptr=0x007A4C60 M_left=0x007A4B40 M_parent=0x007A4C00 M_right=0x007A4C20 M_color=0
key=5 value=[five]
ptr=0x007A4B40 M_left=0x007A4CE0 M_parent=0x007A4C60 M_right=0x00000000 M_color=1
key=3 value=[three]
ptr=0x007A4CE0 M_left=0x00000000 M_parent=0x007A4B40 M_right=0x00000000 M_color=0
key=2 value=[two]
ptr=0x007A4C20 M_left=0x00000000 M_parent=0x007A4C60 M_right=0x007A4D00 M_color=1

176

2.4. STL

ГЛАВА 2. СИ++

key=6 value=[six]
ptr=0x007A4D00 M_left=0x00000000 M_parent=0x007A4C20 M_right=0x00000000
key=9 value=[nine]
ptr=0x007A4B80 M_left=0x007A49A8 M_parent=0x007A4988 M_right=0x007A4BC0
key=100 value=[one hundred]
ptr=0x007A49A8 M_left=0x007A4BA0 M_parent=0x007A4B80 M_right=0x007A4C40
key=20 value=[twenty]
ptr=0x007A4BA0 M_left=0x007A4C80 M_parent=0x007A49A8 M_right=0x00000000
key=12 value=[twelve]
ptr=0x007A4C80 M_left=0x00000000 M_parent=0x007A4BA0 M_right=0x00000000
key=11 value=[eleven]
ptr=0x007A4C40 M_left=0x00000000 M_parent=0x007A49A8 M_right=0x00000000
key=99 value=[ninety-nine]
ptr=0x007A4BC0 M_left=0x007A4B60 M_parent=0x007A4B80 M_right=0x007A4CA0
key=107 value=[one hundred seven]
ptr=0x007A4B60 M_left=0x00000000 M_parent=0x007A4BC0 M_right=0x00000000
key=101 value=[one hundred one]
ptr=0x007A4CA0 M_left=0x00000000 M_parent=0x007A4BC0 M_right=0x007A4CC0
key=1001 value=[one thousand one]
ptr=0x007A4CC0 M_left=0x00000000 M_parent=0x007A4CA0 M_right=0x00000000
key=1010 value=[one thousand ten]
As a tree:
root----10 [ten]
L-------1 [one]
L-------0 [zero]
R-------5 [five]
L-------3 [three]
L-------2 [two]
R-------6 [six]
R-------9 [nine]
R-------100 [one hundred]
L-------20 [twenty]
L-------12 [twelve]
L-------11 [eleven]
R-------99 [ninety-nine]
R-------107 [one hundred seven]
L-------101 [one hundred one]
R-------1001 [one thousand one]
R-------1010 [one thousand ten]
m.begin():
ptr=0x007A4BE0 M_left=0x00000000 M_parent=0x007A4C00 M_right=0x00000000
key=0 value=[zero]
m.end():
ptr=0x0028FE40 M_left=0x007A4BE0 M_parent=0x007A4988 M_right=0x007A4CC0
dumping s as set:
ptr=0x0028FE20, M_key_compare=0x8, M_header=0x0028FE24, M_node_count=6
ptr=0x007A1E80 M_left=0x01D5D890 M_parent=0x0028FE24 M_right=0x01D5D850
key=123
ptr=0x01D5D890 M_left=0x01D5D870 M_parent=0x007A1E80 M_right=0x01D5D8B0
key=12
ptr=0x01D5D870 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000
key=11
ptr=0x01D5D8B0 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000
key=100
ptr=0x01D5D850 M_left=0x00000000 M_parent=0x007A1E80 M_right=0x01D5D8D0
key=456
ptr=0x01D5D8D0 M_left=0x00000000 M_parent=0x01D5D850 M_right=0x00000000
key=1001
As a tree:
root----123
L-------12
L-------11
R-------100
R-------456
R-------1001
s.begin():
ptr=0x01D5D870 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000
key=11
s.end():
ptr=0x0028FE24 M_left=0x01D5D870 M_parent=0x007A1E80 M_right=0x01D5D8D0

M_color=0
M_color=1
M_color=0
M_color=1
M_color=0
M_color=1
M_color=0
M_color=1
M_color=1
M_color=0

M_color=1

M_color=0

M_color=1
M_color=1
M_color=0
M_color=0
M_color=1
M_color=0

M_color=0

M_color=0

Реализация в GCC очень похожа 14 . Разница только в том что здесь нет поля Isnil, так что структура
занимает немного меньше места в памяти чем та что реализована в MSVC. Корневой узел это так же место
14

http://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.1/stl__tree_8h-source.html

177

2.4. STL
куда указывает итератор .end(), не имеющий ключа и/или значения.

178

ГЛАВА 2. СИ++

ГЛАВА 3. ЕЩЕ КОЕ-ЧТО

Глава 3

Еще кое-что
3.1

Пролог и эпилог в функции

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

ebp
ebp, esp
esp, X

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

esp, ebp
ebp
0

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

3.2

npad

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

LISTING.INC
This file contains assembler macros and is included by the files created
with the -FA compiler switch to be assembled by MASM (Microsoft Macro
Assembler).
Copyright (c) 1993-2003, Microsoft Corporation. All rights reserved.

;; non destructive nops
npad macro size

179

3.2. NPAD

ГЛАВА 3. ЕЩЕ КОЕ-ЧТО

if size eq 1
nop
else
if size eq 2
mov edi, edi
else
if size eq 3
; lea ecx, [ecx+00]
DB 8DH, 49H, 00H
else
if size eq 4
; lea esp, [esp+00]
DB 8DH, 64H, 24H, 00H
else
if size eq 5
add eax, DWORD PTR 0
else
if size eq 6
; lea ebx, [ebx+00000000]
DB 8DH, 9BH, 00H, 00H, 00H, 00H
else
if size eq 7
; lea esp, [esp+00000000]
DB 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H
else
if size eq 8
; jmp .+8; .npad 6
DB 0EBH, 06H, 8DH, 9BH, 00H, 00H, 00H, 00H
else
if size eq 9
; jmp .+9; .npad 7
DB 0EBH, 07H, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H
else
if size eq 10
; jmp .+A; .npad 7; .npad 1
DB 0EBH, 08H, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 90H
else
if size eq 11
; jmp .+B; .npad 7; .npad 2
DB 0EBH, 09H, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8BH, 0FFH
else
if size eq 12
; jmp .+C; .npad 7; .npad 3
DB 0EBH, 0AH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8DH, 49H, 00H
else
if size eq 13
; jmp .+D; .npad 7; .npad 4
DB 0EBH, 0BH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8DH, 64H, 24H, 00H
else
if size eq 14
; jmp .+E; .npad 7; .npad 5
DB 0EBH, 0CH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 05H, 00H, 00H, 00H, 00H
else
if size eq 15
; jmp .+F; .npad 7; .npad 6
DB 0EBH, 0DH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8DH, 9BH, 00H, 00H, 00H, 00H
else
%out error: unsupported npad size
.err
endif
endif
endif
endif
endif
endif
endif
endif
endif
endif
endif
endif
endif
endif
endif
endm

180

3.3. ПРЕДСТАВЛЕНИЕ ЗНАКА В ЧИСЛАХ

3.3

ГЛАВА 3. ЕЩЕ КОЕ-ЧТО

Представление знака в числах

Методов представления чисел с знаком “плюс” или “минус” несколько1 , а в x86 применяется метод “дополнительный код” или “two’scomplement”.
Разница в подходе к знаковым/беззнаковым числам, собственно, нужна потому что, например, если
представить 0xFFFFFFFE и 0x0000002 как беззнаковое, то первое число (4294967294) больше второго
(2). Если их оба представить как знаковые, то первое будет −2, которое, разумеется, меньше чем второе
(2). Вот почему инструкции для условных переходов (1.8) представлены в обоих версиях — и для знаковых
сравнений (например JG, JL) и для беззнаковых (JA, JBE).

3.3.1

Переполнение integer

Бывает так, что ошибки представления знаковых/беззнаковых могут привести к уязвимости переполнение
integer.
Например, есть некий сервис, который принимает по сети некие пакеты. В пакете есть заголовок где
указана длина пакета. Это 32-битное значение. В процессе приема пакета, сервис проверяет это значение и
сверяет, больше ли оно чем максимальный размер пакета, скажем, константа MAX_PACKET_SIZE (например, 10 килобайт), и если да, то пакет отвергается как некорректный. Сравнение знаковое. Злоумышленник
подставляет значение 0xFFFFFFFF. Это число трактуется как знаковое −1 и оно меньше чем 10000. Проверка проходит. Продолжаем дальше и копируем этот пакет куда-нибудь себе в сегмент данных. . . вызов
функции memcpy (dst, src, 0xFFFFFFFF) скорее всего, затрет много чего внутри процесса.
Немного подробнее: [3].

3.4

Способы передачи аргументов при вызове функций

3.4.1

cdecl

Этот способ передачи аргументов через стек чаще всего используется в языках Си/Си++.
Вызывающая функция заталкивает в стек аргументы в обратном порядке: сначала последний аргумент
в стек, затем предпоследний, и в самом конце — первый аргумент. Вызывающая функция должна также
затем вернуть указатель стека в нормальное состояние, после возврата вызываемой функции.
Listing 3.1: cdecl
push arg3
push arg2
push arg3
call function
add esp, 12 ; returns ESP

3.4.2

stdcall

Это почти то же что и cdecl, за исключением того что вызываемая функция сама возвращает ESP в нормальное состояние, выполнив инструкцию RET x вместо RET, где x = количество_аргументов *
sizeof(int)2 . Вызывающая функция не будет корректировать указатель стека при помощи инструкции
add esp, x.
Listing 3.2: stdcall
push
push
push
call

arg3
arg2
arg1
function

function:
... do something ...
ret 12
1
2

http://en.wikipedia.org/wiki/Signed_number_representations
Размер переменной типа int — 4 в x86-системах и 8 в x64-системах

181

3.4. СПОСОБЫ ПЕРЕДАЧИ АРГУМЕНТОВ ПРИ ВЫЗОВЕ ФУНКЦИЙ
ГЛАВА 3. ЕЩЕ КОЕ-ЧТО
Этот способ используется почти везде в системных библиотеках win32, но не в win64 (о win64 смотрите
ниже).
Функции с переменным количеством аргументов
Функции вроде printf(), должно быть, единственный случай функций в Си/Си++ с переменным количеством аргументов, но с их помощью можно легко проследить очень важную разницу между cdecl и stdcall.
Начнем с того, что компилятор знает сколько аргументов было у printf(). Однако, вызываемая функция
printf(), которая уже давно скомпилированна и находится в системной библиотеке MSVCRT.DLL (если
говорить о Windows), не знает сколько аргументов ей передали, хотя может установить их количество по
строке формата. Таким образом, если бы printf() была stdcall-функцией и возвращала указатель стека в
первоначальное состояние подсчитав количество аргументов в строке формата, это была бы потенциально
опасная ситуация, когда одна опечатка программиста могла бы вызывать неожиданные падения программы. Таким образом, для таких функций stdcall явно не подходит, а подходит cdecl.

3.4.3

fastcall

Это общее название для передачи некоторых аргументов через регистры а всех остальных — через стек.На
более старых процессорах, это работало потенциально быстрее чем cdecl/stdcall (ведь стек в памяти использовался меньше). Впрочем, на современных, намного более сложных CPU, выигрыша может не быть.
Это не стандартизированый способ, поэтому разные компиляторы делают это по-своему. Разумеется,
если у вас есть, скажем, две DLL, одна использует другую, и обе они собраны с fastcall но разными компиляторами, очень вероятно что будут проблемы.
MSVC и GCC передает первый и второй аргумент через ECX и EDX а остальные аргументы через стек.
Вызываемая функция возвращает указатель стека в первоначальное состояние.
Указатель стека должен быть возвращен в первоначальное состояние вызываемой функцией, как в случае stdcall.
Listing 3.3: fastcall
push arg3
mov edx, arg2
mov ecx, arg1
call function
function:
.. do something ..
ret 4

GCC regparm
Это в некотором роде, развитие fastcall3 . Опцией -mregparm=x можно указывать, сколько аргументов
компилятор будет передавать через регистры. Максимально 3. В этом случае будут задействованы регистры
EAX, EDX и ECX.
Разумеется, если аргументов у функции меньше трех, то будет задействована только часть регистров.
Вызывающая функция возвращает указатель стека в первоначальное состояние.
Для примера, см. (1.15.1).

3.4.4

thiscall

В С++, это передача в функцию-метод указателя this на объект.
В MSVC указатель this обычно передается в регистре ECX.
В GCC указатель this обычно передается как самый первый аргумент. Таким образом, внутри будет видно: у всех функций-методов на один аргумент больше.
Для примера, см. (2.1.1).
3

http://www.ohse.de/uwe/articles/gcc-attributes.html#func-regparm

182

3.4. СПОСОБЫ ПЕРЕДАЧИ АРГУМЕНТОВ ПРИ ВЫЗОВЕ ФУНКЦИЙ

3.4.5

ГЛАВА 3. ЕЩЕ КОЕ-ЧТО

x86-64

Windows x64
В win64 метод передачи всех параметров немного похож на fastcall. Первые 4 аргумента записываются
в регистры RCX, RDX, R8, R9, а остальные — в стек. Вызывающая функция также должна подготовить место
из 32 байт или для четырех 64-битных значений, чтобы вызываемая функция могла сохранить там первые
4 аргумента. Короткие функции могут использовать переменные прямо из регистров, но бо́льшие могут
сохранять их значения на будущее.
Вызывающая функция должна вернуть указатель стека в первоначальное состояние.
Это же соглашение используется и в системных библиотеках Windows x86-64 (вместо stdcall в win32).
Пример:
#include
void f1(int a, int b, int c, int d, int e, int f, int g)
{
printf ("%d %d %d %d %d %d %d\n", a, b, c, d, e, f, g);
};
int main()
{
f1(1,2,3,4,5,6,7);
};

Listing 3.4: MSVC 2012 /0b
$SG2937 DB
main

main

’%d %d %d %d %d %d %d’, 0aH, 00H

PROC
sub

rsp, 72

mov
mov
mov
mov
mov
mov
mov
call

DWORD PTR [rsp+48], 7
DWORD PTR [rsp+40], 6
DWORD PTR [rsp+32], 5
r9d, 4
r8d, 3
edx, 2
ecx, 1
f1

xor
add
ret
ENDP

eax, eax
rsp, 72
0

a$ = 80
b$ = 88
c$ = 96
d$ = 104
e$ = 112
f$ = 120
g$ = 128
f1
PROC
$LN3:
mov
mov
mov
mov
sub
mov
mov
mov
mov
mov
mov
mov
mov
mov
mov
mov
lea

DWORD PTR
DWORD PTR
DWORD PTR
DWORD PTR
rsp, 72

; 00000048H

; 00000048H

[rsp+32], r9d
[rsp+24], r8d
[rsp+16], edx
[rsp+8], ecx
; 00000048H

eax, DWORD PTR g$[rsp]
DWORD PTR [rsp+56], eax
eax, DWORD PTR f$[rsp]
DWORD PTR [rsp+48], eax
eax, DWORD PTR e$[rsp]
DWORD PTR [rsp+40], eax
eax, DWORD PTR d$[rsp]
DWORD PTR [rsp+32], eax
r9d, DWORD PTR c$[rsp]
r8d, DWORD PTR b$[rsp]
edx, DWORD PTR a$[rsp]
rcx, OFFSET FLAT:$SG2937

183

3.4. СПОСОБЫ ПЕРЕДАЧИ АРГУМЕНТОВ ПРИ ВЫЗОВЕ ФУНКЦИЙ

f1

call

printf

add
ret
ENDP

rsp, 72
0

ГЛАВА 3. ЕЩЕ КОЕ-ЧТО

; 00000048H

Здесь мы легко видим как 7 аргументов передаются: 4 через регистры и остальные 3 через стек. Код
пролога ф-ции f1() сохраняет аргументы в “scratch space” — место в стеке предназначенное именно для
этого. Это делается потому что компилятор может быть не уверен, достаточно ли ему будет остальных регистров для работы исключая эти 4, которые иначе будут заняты аргументами до конца исполнения ф-ции.
Выделение “scratch space” в стеке лежит на ответственности вызывающей ф-ции.
Listing 3.5: MSVC 2012 /Ox /0b
$SG2777 DB

’%d %d %d %d %d %d %d’, 0aH, 00H

a$ = 80
b$ = 88
c$ = 96
d$ = 104
e$ = 112
f$ = 120
g$ = 128
f1
PROC
$LN3:
sub

rsp, 72

f1
main

main

; 00000048H

mov
mov
mov
mov
mov
mov
mov
mov
mov
mov
lea
call

eax, DWORD PTR g$[rsp]
DWORD PTR [rsp+56], eax
eax, DWORD PTR f$[rsp]
DWORD PTR [rsp+48], eax
eax, DWORD PTR e$[rsp]
DWORD PTR [rsp+40], eax
DWORD PTR [rsp+32], r9d
r9d, r8d
r8d, edx
edx, ecx
rcx, OFFSET FLAT:$SG2777
printf

add
ret
ENDP

rsp, 72
0

; 00000048H

PROC
sub

rsp, 72

; 00000048H

mov
mov
mov
lea
lea
lea
mov
call

edx, 2
DWORD PTR [rsp+48], 7
DWORD PTR [rsp+40], 6
r9d, QWORD PTR [rdx+2]
r8d, QWORD PTR [rdx+1]
ecx, QWORD PTR [rdx-1]
DWORD PTR [rsp+32], 5
f1

xor
add
ret
ENDP

eax, eax
rsp, 72
0

; 00000048H

Если компилировать этот пример с оптимизацией, то выйдет почти то же самое, только “scratch space”
не используется, потому что незачем.
Обратите также внимание на то как MSVC 2012 оптимизирует примитивную загрузку значений в регистры используя LEA (11.5.6). Я не уверен что это того стоит, но может быть.
Linux x64
Метод передачи аргументо в Linux для x86-64 почти такой же как и в Windows, но 6 регистров используется вместо 4 (RDI, RSI, RDX, RCX, R8, R9), и здесь нет “scratch space”, но callee может сохранять значения
регистров в стеке, если нужно.
184

3.5. АДРЕСНО-НЕЗАВИСИМЫЙ КОД

ГЛАВА 3. ЕЩЕ КОЕ-ЧТО
Listing 3.6: GCC 4.7.3 -O3

.LC0:
.string "%d %d %d %d %d %d %d\n"
f1:
sub
mov
mov
mov
mov
mov
mov
mov
mov
mov
mov
xor
call
add
ret

rsp, 40
eax, DWORD PTR [rsp+48]
DWORD PTR [rsp+8], r9d
r9d, ecx
DWORD PTR [rsp], r8d
ecx, esi
r8d, edx
esi, OFFSET FLAT:.LC0
edx, edi
edi, 1
DWORD PTR [rsp+16], eax
eax, eax
__printf_chk
rsp, 40

sub
mov
mov
mov
mov
mov
mov
mov
call
add
ret

rsp, 24
r9d, 6
r8d, 5
DWORD PTR [rsp], 7
ecx, 4
edx, 3
esi, 2
edi, 1
f1
rsp, 24

main:

N.B.: здесь значения записываются в 32-битные части регистров (например EAX) а не в весь 64-битный
регистр (RAX). Это связано с тем что в x86-64, запись в младшую 32-битную часть 64-битного регистра
автоматически обнуляет старшие 32 бита. Вероятно, это сделано для упрощения портирования кода под
x86-64.

3.4.6

Возвращение переменных типа float, double

Во всех соглашениях кроме Win64, переменная типа float или double возвращается через регистр FPU
ST(0).
В Win64 переменные типа float и double возвращаются в регистре XMM0 вместо ST(0).

3.5

Адресно-независимый код

Во время анализа динамических библиотек (.so) в Linux, часто можно заметить такой шаблонный код:
Listing 3.7: libc-2.17.so x86
.text:0012D5E3 __x86_get_pc_thunk_bx proc near
.text:0012D5E3
.text:0012D5E3
mov
ebx, [esp+0]
.text:0012D5E6
retn
.text:0012D5E6 __x86_get_pc_thunk_bx endp

; CODE XREF: sub_17350+3
; sub_173CC+4 ...

...
.text:000576C0 sub_576C0

proc near

; CODE XREF: tmpfile+73

...
.text:000576C0
.text:000576C1
.text:000576C8
.text:000576C9
.text:000576CA
.text:000576CB
.text:000576D0
.text:000576D6

push
mov
push
push
push
call
add
sub

ebp
ecx, large gs:0
edi
esi
ebx
__x86_get_pc_thunk_bx
ebx, 157930h
esp, 9Ch

185

3.5. АДРЕСНО-НЕЗАВИСИМЫЙ КОД

ГЛАВА 3. ЕЩЕ КОЕ-ЧТО

...
.text:000579F0
.text:000579F6
.text:000579FA
c"
.text:00057A00
.text:00057A04
__gen_tempname\""
.text:00057A0A
.text:00057A12
.text:00057A15

lea
mov
lea

eax, (a__gen_tempname - 1AF000h)[ebx] ; "__gen_tempname"
[esp+0ACh+var_A0], eax
eax, (a__SysdepsPosix - 1AF000h)[ebx] ; "../sysdeps/posix/tempname.

mov
lea

[esp+0ACh+var_A8], eax
eax, (aInvalidKindIn_ - 1AF000h)[ebx] ; "! \"invalid KIND in

mov
mov
call

[esp+0ACh+var_A4], 14Ah
[esp+0ACh+var_AC], eax
__assert_fail

Все указатели на строки корректируются при помощи некоторой константы из регистра EBX, которая
вычисляется в начале каждой функции. Это так называемый адресно-независимый код (PIC4 ), он предназначен для исполнения будучи расположенным по любому адресу в памяти, вот почему он не содержит
никаких абсолютных адресов в памяти.
PIC был очень важен в ранних компьютерных системах и важен сейчас во встраиваемых5 , не имеющих
поддержки виртуальной памяти (все процессы расположены в одном непрерывном блоке памяти). Он до
сих пор используется в *NIX системах для динамических библиотек, потому что динамическая библиотека
может использоваться одновременно в нескольких процессах, будучи загружена в память только один раз.
Но все эти процессы могут загрузить одну и ту же динамическую библиотеку по разным адресам, вот почему
динамическая библиотека должна работать корректно не привыязываясь к абсолютным адресам.
Простой эксперимент:
#include
int global_variable=123;
int f1(int var)
{
int rt=global_variable+var;
printf ("returning %d\n", rt);
return rt;
};

Скомпилируем в GCC 4.7.3 и посмотрим итоговый файл .so в IDA:
gcc -fPIC -shared -O3 -o 1.so 1.c

Listing 3.8: GCC 4.7.3
.text:00000440
public __x86_get_pc_thunk_bx
.text:00000440 __x86_get_pc_thunk_bx proc near
; CODE XREF: _init_proc+4
.text:00000440
; deregister_tm_clones+4 ...
.text:00000440
mov
ebx, [esp+0]
.text:00000443
retn
.text:00000443 __x86_get_pc_thunk_bx endp
.text:00000570
.text:00000570
.text:00000570
.text:00000570
.text:00000570
.text:00000570
.text:00000570
.text:00000570
.text:00000570
.text:00000570
.text:00000570
.text:00000573
.text:00000577
.text:0000057C
.text:00000582
.text:00000586
.text:0000058C
.text:0000058E
.text:00000594
4
5

f1

public f1
proc near

var_1C
var_18
var_14
var_8
var_4
arg_0

=
=
=
=
=
=

dword
dword
dword
dword
dword
dword

sub
mov
call
add
mov
mov
mov
lea
add

ptr
ptr
ptr
ptr
ptr
ptr

-1Ch
-18h
-14h
-8
-4
4

esp, 1Ch
[esp+1Ch+var_8], ebx
__x86_get_pc_thunk_bx
ebx, 1A84h
[esp+1Ch+var_4], esi
eax, ds:(global_variable_ptr - 2000h)[ebx]
esi, [eax]
eax, (aReturningD - 2000h)[ebx] ; "returning %d\n"
esi, [esp+1Ch+arg_0]

Position Independent Code
embedded

186

3.5. АДРЕСНО-НЕЗАВИСИМЫЙ КОД
.text:00000598
.text:0000059C
.text:000005A3
.text:000005A7
.text:000005AC
.text:000005AE
.text:000005B2
.text:000005B6
.text:000005B9
.text:000005B9 f1

mov
mov
mov
call
mov
mov
mov
add
retn
endp

ГЛАВА 3. ЕЩЕ КОЕ-ЧТО
[esp+1Ch+var_18], eax
[esp+1Ch+var_1C], 1
[esp+1Ch+var_14], esi
___printf_chk
eax, esi
ebx, [esp+1Ch+var_8]
esi, [esp+1Ch+var_4]
esp, 1Ch

Так и есть: указатели на строку «returning %d\n» и переменную global_variable корректируются при каждом исполнении функции Функция __x86_get_pc_thunk_bx() возвращает адрес точки после вызова самой
себя (здесь: 0x57C) в EBX. Это очень простой способ получить значение указателя на текущую инструкцию
(EIP) в произвольном месте. Константа 0x1A84 связана с разницей между началом этой функции и так называемой Global Offset Table Procedure Linkage Table (GOT PLT), секцией, сразу же за Global Offset Table (GOT),
где находится указатель на global_variable. IDA показыавет смещения уже обработанными, чтобы их было
проще понимать, но на самом деле код такой:
.text:00000577
.text:0000057C
.text:00000582
.text:00000586
.text:0000058C
.text:0000058E

call
add
mov
mov
mov
lea

__x86_get_pc_thunk_bx
ebx, 1A84h
[esp+1Ch+var_4], esi
eax, [ebx-0Ch]
esi, [eax]
eax, [ebx-1A30h]

Так что, EBX указывает на секцию GOT PLT и для вычисления указателя на global_variable, которая хранится в GOT, нужно вычесть 0xC. А чтобы вычислить указатель на «returning %d\n», нужно вычесть 0x1A30.
Кстати, вот зачем в AMD64 появилась поддержка адресации относительно RIP6 , просто для упрощения
PIC-кода.
Скомпилируем тот же код на Си при помощи той же версии GCC, но для x64.
IDA упростит код на выходе убирая упоминания RIP, так что я буду использовать objdump вместо:
0000000000000720
720:
48 8b 05
727:
53
728:
89 fb
72a:
48 8d 35
731:
bf 01 00
736:
03 18
738:
31 c0
73a:
89 da
73c:
e8 df fe
741:
89 d8
743:
5b
744:
c3

:
b9 08 20 00

20 00 00 00
00 00

ff ff

mov
push
mov
lea
mov
add
xor
mov
call
mov
pop
ret

rax,QWORD PTR [rip+0x2008b9]
# 200fe0
rbx
ebx,edi
rsi,[rip+0x20]
# 751
edi,0x1
ebx,DWORD PTR [rax]
eax,eax
edx,ebx
620
eax,ebx
rbx

0x2008b9 это разница между адресом инструкции по 0x720 и global_variable, а 0x20 это разница
между инструкцией по 0x72A и строкой «returning %d\n».
Как видно, необходимость очень часто пересчитывать адреса делает исполнение немного медленнее
(хотя это и стало лучше в x64). Так что если вы заботитесь о скорости исполнения, то наверное нужно
задуматься о статической компоновке (static linking) ( [9]).

3.5.1

Windows

Такой механизм не используется в Windows DLL. Если загрузчику в Windows приходится загружать DLL в
другое место, он “патчит” DLL прямо в памяти (на местах FIXUP-ов) чтобы скорректировать все адреса. Это
приводит к тому что загруженную один раз DLL нельзя использовать одновременно в разных процессах,
желающих расположить её по разным адресам — потому что каждый загруженный в память экземпляр
DLL доводится до того чтобы работать только по этим адресам.
6

указатель инструкций в AMD64

187

3.6. THREAD LOCAL STORAGE

3.6

ГЛАВА 3. ЕЩЕ КОЕ-ЧТО

Thread Local Storage

Это область данных, отдельная для каждого треда. Каждый тред может хранить там то, что ему нужно. Один
из известных примеров, это стандартная глобальная переменная в Си errno. Несколько тредов одновременно могут вызывать функции возвращающие код ошибки в errno, поэтому глобальная переменная здесь не
будет работать корректно, для мультитредовых программ errno нужно хранить в в TLS.
В C++11 ввели модификатор thread_local показывающий что каждый тред будет иметь свою версию этой
переменной, и её можно инициализировать, и она расположена в TLS 7 :
Listing 3.9: C++11
#include
#include
thread_local int tmp=3;
int main()
{
std::cout KEY[1];
ctx->KEY[2];
ctx->KEY[3];

n = TEA_ROUNDS;
while (n-- > 0) {
sum += TEA_DELTA;
y += ((z > 5) + k1);
z += ((y > 5) + k3);
}
out[0] = cpu_to_le32(y);
out[1] = cpu_to_le32(z);
}

И вот как он был скомпилирован:
Listing 3.11: Linux Kernel 3.2.0.4 для Itanium 2 (McKinley)
0090|
0090|08
0096|80
009C|00
00A0|09
00A6|F0
00AC|44
00B0|08
00B6|00
00BC|00
00C0|05
00CC|92
00D0|08
00D6|50
00DC|F0
00E0|0A
00E6|20
00EC|00
00F0|
00F0|
00F0|09
00F6|D0
00FC|A3
0100|03
0106|B0
010C|D3
0110|0B
0116|F0
011C|00
0120|00
0126|80
012C|F1
0130|0B
0136|A0
013C|00
0140|0B
0146|60
014C|00
0150|11
0156|E0

tea_encrypt:
adds r16 = 96, r32
adds r8 = 88, r32
nop.i 0
00 21
adds r3 = 92, r32
28 00
ld4 r15 = [r34], 4
adds r32 = 100, r32;;
10 10
ld4 r19 = [r16]
42 40
mov r16 = r0
mov.i r2 = ar.lc
10 10 9E FF FF FF 7F 20
ld4 r14 = [r34]
movl r17 = 0xFFFFFFFF9E3779B9;;
01 00
nop.m 0
20 00
ld4 r21 = [r8]
mov.i ar.lc = 31
10 10
ld4 r20 = [r3];;
20 00
ld4 r18 = [r32]
nop.i 0

80
C0
00
18
20
06
98
01
08
70
F3
00
01
09
A0
01
00

80
82
04
70
88
01
00
00
CA
00
CE
00
20
2A
00
80
04

41
00
00
41
20
84
20
00
00
44
6B
00
20
00
06
20
00

00 21
42 00

80
71
70
F0
E1
F1
C8
78
00
00
51
98
B8
C0
00
48
B9
00
00
70

40
54
68
40
50
3C
6C
64
04
00
3C
4C
3C
48
04
28
24
04
00
58

22
26
52
1C
00
80
34
00
00
00
34
80
20
00
00
16
1E
00
00
00

00 20
40 80
00 20
40 40
0F 20
40 00
01 00
29 60
00 20
40 00
0F 20
40 00
01 00
40 A0

loc_F0:
add r16 = r16, r17
shladd r29 = r14, 4,
extr.u r28 = r14, 5,
add r30 = r16, r14
add r27 = r28, r20;;
xor r26 = r29, r30;;
xor r25 = r27, r26;;
add r15 = r15, r25
nop.i 0;;
nop.m 0
extr.u r24 = r15, 5,
shladd r11 = r15, 4,
add r23 = r15, r16;;
add r10 = r24, r18
nop.i 0;;
xor r9 = r10, r11;;
xor r22 = r23, r9
nop.i 0;;
nop.m 0
add r14 = r14, r22

191

r21
27;;

// ptr to ctx->KEY[2]
// ptr to ctx->KEY[0]
//
//
//
//
//
//
//
//

ptr to ctx->KEY[1]
load z
ptr to ctx->KEY[3]
r19=k2
r0 always contain zero
save lc register
load y
TEA_DELTA

//
//
//
//

r21=k0
TEA_ROUNDS is 32
r20=k1
r18=k3

// r16=sum, r17=TEA_DELTA
// r14=y, r21=k0

// r20=k1

// r15=z

27
r19

// r19=k2
// r18=k3

3.9. ПЕРЕСТАНОВКА BASIC BLOCK-ОВ
015C|A0
0160|09
0166|00
016C|20
0170|11
0176|00
017C|08

FF
20
00
08
00
00
00

FF
3C
00
AA
38
00
84

48
42
02
00
42
02
00

90 15
00 00
90 11
00 80

ГЛАВА 3. ЕЩЕ КОЕ-ЧТО
br.cloop.sptk.few loc_F0;;
st4 [r33] = r15, 4
nop.m 0
mov.i ar.lc = r2;;
st4 [r33] = r14
nop.i 0
br.ret.sptk.many b0;;

// store z
// restore lc register
// store y

Прежде всего, все инструкции IA64 сгруппированы в пачки (bundle) из трех инструкций. Каждая пачка
имеет размер 16 байт и состоит из template-кода и трех инструкций. IDA показывает пачки как 6+6+4 байт
— вы можете легко заметить эту повторяющуюся структуру.
Все 3 инструкции каждой пачки обычно исполняются одновременно, если только у какой-то инструкции
нет “стоп-бита”.
Вероятно, инженеры Intel и HP собрали статистику наиболее встречающихся шаблонных сочетаний
инструкций и решили ввести типы пачек (AKA “templates”): код пачки определяет типы инструкций в пачке.
Их всего 12. Например, нулевой тип это MII, что означает: первая инструкция это Memory (загрузка или
запись в память), вторая и третья это I (инструкция работающая с целочисленными значениями). Еще один
пример, тип 0x1d: MFB: первая инструкция это Memory (загрузка или запись в память), вторая это Float
(инструкция работающая с FPU), третья это Branch (инструкция перехода).
Если компилятор не может подобрать подходящую инструкцию в соответствующее место пачки, он может вставить NOP14 : вы можете здесь увидеть инструкции nop.i (NOP на том месте где должна была бы
находиться целочисленная инструкция) или nop.m (инструкция обращения к памяти должна была находиться здесь). Если вручную писать на ассемблере, NOP-ы могут вставляться автоматически.
И это еще не все. Пачки тоже могут быть объеденены в группы. Каждая пачка может иметь “стоп-бит”,
так что все следующие друг за другом пачки вплоть до той, что имеет стоп-бит, могут быть исполнены
одновременно. На практике, Itanium 2 может исполнять 2 пачки одновременно, таким образом, исполнять
6 инструкций одновременно.
Так что все инструкции внутри пачки и группы не могут мешать друг другу (т.е., не должны иметь data
hazard-ов). А если это так, то результаты будут непредсказуемые.
На ассемблере, каждый стоп-бит маркируется как ;; (две точки с запятой) после инструкции. Так, инструкции на [180-19c] могут быть исполнены одновременно: они не мешают друг другу. Следующая группа:
[1a0-1bc].
Мы также видим стоп-бит на 22c. Следующая инструкция на 230 также имеет стоп-бит. Это значит что
эта инструкция должна исполняться изолированно от всех остальных (как в CISC). Действительно: следующая инструкция на 236 использует результат полученный от нее (значение в регистре r10), так что они не
могут исполняться одновременно. Должно быть, компилятор не смог найти лучший способ распараллелить
инструкции, или, иными словами, загрузить CPU насколько это возможно, отсюда так много стоп-битов и
NOP-ов. Писать на ассемблере вручную это также очень трудная задача: программист должен группировать
инструкции вручную.
У программиста остается возможность добавлять стоп-биты к каждой инструкции, но это сведет на нет
всю мощность Itanium, ради которой он создавался.
Интересные примеры написания IA64-кода вручную можно найти в исходниках ядра Linux:
http://lxr.free-electrons.com/source/arch/ia64/lib/.
Еще одна вводна статья об ассемблере Itanium: [5].
Еще одна интересная особенность Itanium это speculative execution (исполнение инструкций зараннее,
когда еще не известно, нужно ли это) и бит NaT (“not a thing”), отдаленно напоминающий NaN-числа:
http://blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx.

3.9
3.9.1

Перестановка basic block-ов
Profile-guided optimization

Этот метод оптимизации кода может перемещать некоторые basic block-и в другую секцию исполняемого
бинарного файла.
14

No OPeration

192

3.9. ПЕРЕСТАНОВКА BASIC BLOCK-ОВ
ГЛАВА 3. ЕЩЕ КОЕ-ЧТО
Очевидно, в ф-ции есть места которые исполняются чаще всего (например, тела циклов) и реже всего
(например, код обработки ошибок, обработчики исключений).
Компилятор добавляет дополнительный (instrumentation) код в исполняемый файл, затем разработчик
запускает его с тестами для сбора статистики. Затем компилятор, при помощи собранной статистики, приготавливает итоговый исполняемый файл где весь редко исполняемый код перемещен в другую секцию.
В результате, весь часто исполняемый код ф-ции становится компактным, что очень важно для скорости
исполнения и кэш-памяти.
Пример из Oracle RDBMS, который скомпилирован при помощи Intel C++:
Listing 3.12: orageneric11.dll (win32)
_skgfsync

public _skgfsync
proc near

; address 0x6030D86A
db
nop
push
mov
mov
test
jz
mov
test
jnz

66h

mov
mov
mov
lea
and
mov
cmp
jnz
mov
pop
retn
endp

eax, [ebp+8]
edx, [ebp+10h]
dword ptr [eax], 0
eax, [edx+0Fh]
eax, 0FFFFFFFCh
ecx, [eax]
ecx, 45726963h
error
esp, ebp
ebp

ebp
ebp, esp
edx, [ebp+0Ch]
edx, edx
short loc_6030D884
eax, [edx+30h]
eax, 400h
__VInfreq__skgfsync

; write to log

contiune:

_skgfsync

; exit with error

...
; address 0x60B953F0
__VInfreq__skgfsync:
mov
eax, [edx]
test
eax, eax
jz
contiune
mov
ecx, [ebp+10h]
push
ecx
mov
ecx, [ebp+8]
push
edx
push
ecx
push
offset ... ; "skgfsync(se=0x%x, ctx=0x%x, iov=0x%x)\n"
push
dword ptr [edx+4]
call
dword ptr [eax] ; write to log
add
esp, 14h
jmp
contiune
; --------------------------------------------------------------------------error:
mov
mov
mov
mov
mov
mov
pop
retn
; END OF FUNCTION CHUNK

edx, [ebp+8]
dword ptr [edx], 69AAh ; 27050 "function called with invalid FIB/IOV structure"
eax, [eax]
[edx+4], eax
dword ptr [edx+8], 0FA4h ; 4004
esp, ebp
ebp
FOR _skgfsync

Расстояние между двумя адресами приведенных фрагментов кода почти 9 МБ.
Весь редко исполняемый код помещен в конце секции кода DLL-файла, среди редко исполняемых ча193

3.9. ПЕРЕСТАНОВКА BASIC BLOCK-ОВ
ГЛАВА 3. ЕЩЕ КОЕ-ЧТО
стей прочих ф-ций. Эта часть ф-ции была отмечена компилятором Intel C++ префиксом VInfreg. Мы видим
часть ф-ции которая записывает в лог-файл (вероятно, в случае ошибки или предупреждения или чего-то
в этом роде) которая наверное не исполнялась слишком часто когда разработчики Oracle собирали статистику (если вообще исполнялась). Basic block записывающий в лог-файл, в конце концов возвращает
управление в “горячую” часть ф-ции
Другая “редкая” часть это basic block возвращающий код ошибки 27050.
В ELF-файлах для Linux весь редко исполняемый код перемещается компилятором Intel C++ в другую
секцию (text.unlikely) оставляя весь “горячий” код в секции text.hot.
С точки зрения reverse engineer-а, эта информация может помочь разделить ф-цию на её основу и части
отвечающие за обработку ошибок.

194

ГЛАВА 4. ПОИСК В КОДЕ ТОГО ЧТО НУЖНО

Глава 4

Поиск в коде того что нужно
Современное ПО, в общем-то, минимализмом не отличается.
Но не потому, что программисты слишком много пишут, а потому что к исполняемым файлам обыкновенно прикомпилируют все подряд библиотеки. Если бы все вспомогательные библиотеки всегда выносили
во внешние DLL, мир был бы иным. (Еще одна причина для Си++ — STL и прочие библиотеки шаблонов.)
Таким образом, очень полезно сразу понимать, какая функция из стандартной библиотеки или болееменее известной (как Boost1 , libpng2 ), а какая — имеет отношение к тому что мы пытаемся найти в коде.
Переписывать весь код на Си/Си++, чтобы разобраться в нем, безусловно, не имеет никакого смысла.
Одна из важных задач reverse engineer-а это быстрый поиск в коде того что собственно его интересует.
Дизассемблер IDA позволяет делать поиск как минимум строк, последовательностей байт, констант.
Можно даже сделать экспорт кода в текстовый файл .lst или .asm и затем натравить на него grep, awk,
итд.
Когда вы пытаетесь понять, что делает тот или иной код, это запросто может быть какая-то опенсорсная
библиотека вроде libpng. Поэтому когда находите константы, или текстовые строки которые выглядят явно
знакомыми, всегда полезно их погуглить. А если вы найдете искомый опенсорсный проект где это используется, то тогда будет достаточно будет просто сравнить вашу функцию с ней. Это решит часть проблем.
К примеру, если программа использует какие-то XML-файлы, первым шагом может быть установление,
какая именно XML-библиотека для этого используется, ведь часто используется какая-то стандартная (или
очень известная) вместо самодельной.
К примеру, однажды я пытался разобраться как происходит компрессия/декомпрессия сетевых пакетов
в SAP 6.0. Это очень большая программа, но к ней идет подробный .PDB-файл с отладочной информацией,
и это очень удобно. Я в конце концов пришел к тому что одна из функций декомпрессирующая пакеты
называется CsDecomprLZC(). Не сильно раздумывая, я решил погуглить и оказалось что функция с таким же
названием имеется в MaxDB (это опен-сорсный проект SAP)3 .
http://www.google.com/search?q=CsDecomprLZC
Каково же было мое удивление, когда оказалось, что в MaxDB используется точно такой же алгоритм,
скорее всего, с таким же исходником.

4.1

Связь с внешним миром

Первое на что нужно обратить внимание, это какие функции из API ОС и какие функции стандартных библиотек используются.
Если программа поделена на главный исполняемый файл и группу DLL-файлов, то имена функций в
этих DLL, бывает так, могут помочь.
Если нас интересует, что именно приводит к вызову MessageBox() с определенным текстом, то первое
что можно попробовать сделать: найти в сегменте данных этот текст, найти ссылки на него, и найти, откуда
может передаться управление к интересующему нас вызову MessageBox().
1

http://www.boost.org/
http://www.libpng.org/pub/png/libpng.html
3
Больше об этом в соответствующей секции (7.3.1)
2

195

4.1. СВЯЗЬ С ВНЕШНИМ МИРОМ
ГЛАВА 4. ПОИСК В КОДЕ ТОГО ЧТО НУЖНО
Если речь идет о компьютерной игре, и нам интересно какие события в ней более-менее случайны, мы
можем найти функцию rand() или её заменитель (как алгоритм Mersenne twister), и посмотреть, из каких
мест эта функция вызывается и что самое главное: как используется результат этой функции.
Но если это не игра, а rand() используется, то также весьма любопытно, зачем. Бывают неожиданные
случаи вроде использования rand() в алгоритме для сжатия данных (для имитации шифрования): http:
//blog.yurichev.com/node/44.

4.1.1

Часто используемые ф-ции Windows API

∙ Работа с реестром (advapi32.dll): RegEnumKeyEx4 5 , RegEnumValue6 5 , RegGetValue7 5 , RegOpenKeyEx8
5 , RegQueryValueEx9 5 .
∙ Работа с текстовыми .ini-файлами (kernel32.dll): GetPrivateProfileString 10 5 .
∙ Диалоговые окна (user32.dll): MessageBox 11 5 , MessageBoxEx 12 5 , SetDlgItemText 13 5 , GetDlgItemText
14 5 .
∙ Работа с ресурсами(5.1.1): (user32.dll): LoadMenu 15 5 .
∙ Работа с TCP/IP-сетью (ws2_32.dll): WSARecv 16 , WSASend 17 .
∙ Работа с файлами (kernel32.dll): CreateFile 18 5 , ReadFile 19 , ReadFileEx 20 , WriteFile 21 , WriteFileEx 22 .
∙ Высокоуровневая работа с Internet (wininet.dll): WinHttpOpen 23 .
∙ Проверка цифровой подписи исполняемого файла (wintrust.dll): WinVerifyTrust 24 .
∙ Стандартная библиотека MSVC (при случае динамического связывания) (msvcr*.dll): assert, itoa, ltoa,
open, printf, read, strcmp, atol, atoi, fopen, fread, fwrite, memcmp, rand, strlen, strstr, strchr.

4.1.2

tracer: Перехват всех ф-ций в отдельном модуле

В tracer 6.0.1 есть INT3-брякпоинты, хотя и срабатывающие только один раз, но зато их можно установить
на все сразу ф-ции в некоей DLL.
--one-time-INT3-bp:somedll.dll!.*

Либо, поставим INT3-прерывание на все функции, имена которых начинаются с префикса xml:
--one-time-INT3-bp:somedll.dll!xml.*
4

http://msdn.microsoft.com/en-us/library/windows/desktop/ms724862(v=vs.85).aspx
Может иметь суффикс -A для ASCII-версии и -W для Unicode-версии
6
http://msdn.microsoft.com/en-us/library/windows/desktop/ms724865(v=vs.85).aspx
7
http://msdn.microsoft.com/en-us/library/windows/desktop/ms724868(v=vs.85).aspx
8
http://msdn.microsoft.com/en-us/library/windows/desktop/ms724897(v=vs.85).aspx
9
http://msdn.microsoft.com/en-us/library/windows/desktop/ms724911(v=vs.85).aspx
10
http://msdn.microsoft.com/en-us/library/windows/desktop/ms724353(v=vs.85).aspx
11
http://msdn.microsoft.com/en-us/library/ms645505(VS.85).aspx
12
http://msdn.microsoft.com/en-us/library/ms645507(v=vs.85).aspx
13
http://msdn.microsoft.com/en-us/library/ms645521(v=vs.85).aspx
14
http://msdn.microsoft.com/en-us/library/ms645489(v=vs.85).aspx
15
http://msdn.microsoft.com/en-us/library/ms647990(v=vs.85).aspx
16
http://msdn.microsoft.com/en-us/library/windows/desktop/ms741688(v=vs.85).aspx
17
http://msdn.microsoft.com/en-us/library/windows/desktop/ms742203(v=vs.85).aspx
18
http://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx
19
http://msdn.microsoft.com/en-us/library/windows/desktop/aa365467(v=vs.85).aspx
20
http://msdn.microsoft.com/en-us/library/windows/desktop/aa365468(v=vs.85).aspx
21
http://msdn.microsoft.com/en-us/library/windows/desktop/aa365747(v=vs.85).aspx
22
http://msdn.microsoft.com/en-us/library/windows/desktop/aa365748(v=vs.85).aspx
23
http://msdn.microsoft.com/en-us/library/windows/desktop/aa384098(v=vs.85).aspx
24
http://msdn.microsoft.com/library/windows/desktop/aa388208.aspx
5

196

4.2. СТРОКИ
ГЛАВА 4. ПОИСК В КОДЕ ТОГО ЧТО НУЖНО
В качестве обратной стороны медали, такие прерывания срабатывают только один раз.
Tracer покажет вызов какой-либо функции, если он случится, но только один раз. Еще один недостаток
— увидеть аргументы функции также нельзя.
Тем не менее, эта возможность очень удобна для тех ситуаций, когда вы знаете что некая программа
использует некую DLL, но не знаете какие именно функции в этой DLL. И функций много.
Например, попробуем узнать, что использует cygwin-утилита uptime:
tracer -l:uptime.exe --one-time-INT3-bp:cygwin1.dll!.*

Так мы можем увидеть все ф-ции из библиотеки cygwin1.dll, которые были вызваны хотя бы один раз,
и откуда:
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time
One-time

4.2

INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3
INT3

breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:
breakpoint:

cygwin1.dll!__main (called from uptime.exe!OEP+0x6d (0x40106d))
cygwin1.dll!_geteuid32 (called from uptime.exe!OEP+0xba3 (0x401ba3))
cygwin1.dll!_getuid32 (called from uptime.exe!OEP+0xbaa (0x401baa))
cygwin1.dll!_getegid32 (called from uptime.exe!OEP+0xcb7 (0x401cb7))
cygwin1.dll!_getgid32 (called from uptime.exe!OEP+0xcbe (0x401cbe))
cygwin1.dll!sysconf (called from uptime.exe!OEP+0x735 (0x401735))
cygwin1.dll!setlocale (called from uptime.exe!OEP+0x7b2 (0x4017b2))
cygwin1.dll!_open64 (called from uptime.exe!OEP+0x994 (0x401994))
cygwin1.dll!_lseek64 (called from uptime.exe!OEP+0x7ea (0x4017ea))
cygwin1.dll!read (called from uptime.exe!OEP+0x809 (0x401809))
cygwin1.dll!sscanf (called from uptime.exe!OEP+0x839 (0x401839))
cygwin1.dll!uname (called from uptime.exe!OEP+0x139 (0x401139))
cygwin1.dll!time (called from uptime.exe!OEP+0x22e (0x40122e))
cygwin1.dll!localtime (called from uptime.exe!OEP+0x236 (0x401236))
cygwin1.dll!sprintf (called from uptime.exe!OEP+0x25a (0x40125a))
cygwin1.dll!setutent (called from uptime.exe!OEP+0x3b1 (0x4013b1))
cygwin1.dll!getutent (called from uptime.exe!OEP+0x3c5 (0x4013c5))
cygwin1.dll!endutent (called from uptime.exe!OEP+0x3e6 (0x4013e6))
cygwin1.dll!puts (called from uptime.exe!OEP+0x4c3 (0x4014c3))

Строки

Очень сильно помогают отладочные сообщения, если они имеются. В некотором смысле, отладочные сообщения, это отчет о том, что сейчас происходит в программе. Зачастую, это printf()-подобные функции,
которые пишут куда-нибудь в лог, а бывает так что и не пишут ничего, но вызовы остались, так как эта сборка — не отладочная, а release. Если в отладочных сообщениях дампятся значения некоторых локальных
или глобальных переменных, это тоже может помочь, как минимум, узнать их имена. Например, в Oracle
RDBMS одна из таких функций: ksdwrt().
Осмысленные текстовые строки вообще очень сильно могут помочь. Дизассемблер IDA может сразу
указать, из какой функции и из какого её места используется эта строка. Попадаются и смешные случаи.
Парадоксально, но сообщения об ошибках также могут помочь найти то что нужно. В Oracle RDBMS
сигнализация об ошибках проходит при помощи вызова некоторой группы функций. Тут еще немного об
этом.
Можно довольно быстро найти, какие функции сообщают о каких ошибках, и при каких условиях. Это,
кстати, одна из причин, почему в защите софта от копирования, бывает так, что сообщение об ошибке
заменяется невнятным кодом или номером ошибки. Мало кому приятно, если взломщик быстро поймет, из
за чего именно срабатывает защита от копирования, просто по сообщению об ошибке.

4.3

Вызовы assert()

Может также помочь наличие assert() в коде: обычно этот макрос оставляет название файла-исходника,
номер строки, и условие.
Наиболее полезная информация содержится в assert-условии, по нему можно судить по именам переменных или именам полей структур. Другая полезная информация это имена файлов — по их именам
можно попытаться предположить, что там за код. Так же, по именам файлов можно опознать какую-либо
очень известную опен-сорсную библиотеку.
197

4.4. КОНСТАНТЫ

ГЛАВА 4. ПОИСК В КОДЕ ТОГО ЧТО НУЖНО
Listing 4.1: Пример информативных вызовов assert()

.text:107D4B29
.text:107D4B2D
.text:107D4B30
.text:107D4B32
.text:107D4B37
.text:107D4B3C
"...
.text:107D4B41

mov
cmp
jz
push
push
push

dx, [ecx+42h]
edx, 1
short loc_107D4B4A
1ECh
offset aWrite_c ; "write.c"
offset aTdTd_planarcon ; "td->td_planarconfig == PLANARCONFIG_CON

call

ds:_assert

mov
and
test
jz
push
push
push
call

edx, [ebp-4]
edx, 3
edx, edx
short loc_107D52E9
58h
offset aDumpmode_c ; "dumpmode.c"
offset aN30
; "(n & 3) == 0"
ds:_assert

mov
cmp
jle
push
push
push
call

cx, [eax+6]
ecx, 0Ch
short loc_107D677A
2D8h
offset aLzw_c
; "lzw.c"
offset aSpLzw_nbitsBit ; "sp->lzw_nbits to EBX
mov
edx, ecx
xor
eax, eax
mov
edi, ebx
push
ebp
; File
shr
ecx, 2
rep stosd
mov
ecx, edx
push
1
; Count
and
ecx, 3
rep stosb
; memset (buffer, 0, aligned_size)
mov
eax, [esp+38h+Str]
push
eax
; ElementSize
push
ebx
; DstBuf
call
_fread
; read file
push
ebp
; File
call
_fclose
mov
ecx, [esp+44h+password]
push
ecx
; password
push
esi
; aligned size
push
ebx
; buffer
call
crypt
; do crypt
mov
edx, [esp+50h+Filename]
add
esp, 40h
push
offset aWb
; "wb"
push
edx
; Filename
call
_fopen
mov
edi, eax
push
edi
; File
push
1
; Count
push
3
; Size
push
offset aQr9
; "QR9"
call
_fwrite
; write file signature
push
edi
; File
push
1
; Count
lea
eax, [esp+30h+Str]
push
4
; Size
push
eax
; Str
call
_fwrite
; write original file size
push
edi
; File
push
1
; Count
push
esi
; Size
push
ebx
; Str
call
_fwrite
; write crypted file
push
edi
; File
call
_fclose
push
ebx
; Memory
call
_free
add
esp, 40h
pop
edi
pop
esi
pop
ebx
pop
ebp
retn
endp

; --------------------------------------------------------------------------align 10h
; =============== S U B R O U T I N E =======================================

; int __cdecl decrypt_file(char *Filename, int, void *Src)
decrypt_file
proc near
; CODE XREF: _main+6E
Filename
arg_4
Src

= dword ptr
= dword ptr
= dword ptr
mov

4
8
0Ch

eax, [esp+Filename]

241

7.2. “QR9”: ЛЮБИТЕЛЬСКАЯ КРИПТОСИСТЕМА ВДОХНОВЛЕННАЯ КУБИКОМ РУБИКА ГЛАВА 7. ЕЩЕ ПРИМЕРЫ
.text:00541404
.text:00541405
.text:00541406
.text:00541407
.text:00541408
.text:0054140D
.text:0054140E
.text:00541413
.text:00541415
.text:00541418
.text:0054141A
.text:0054141C
.text:00541421
.text:00541426
.text:00541429
.text:0054142A
.text:0054142B
.text:0054142C
.text:0054142D
.text:0054142E
.text:0054142E
.text:0054142E
.text:0054142E
.text:00541430
.text:00541432
.text:00541433
.text:00541438
.text:00541439
.text:0054143E
.text:00541440
.text:00541442
.text:00541443
.text:00541445
.text:0054144A
.text:0054144B
.text:00541450
.text:00541451
.text:00541453
.text:00541455
.text:00541456
.text:00541457
.text:0054145C
.text:0054145D
.text:00541462
.text:00541465
.text:0054146A
.text:0054146F
.text:00541471
.text:00541473
.text:00541475
.text:00541477
.text:0054147C
.text:00541481
.text:00541484
.text:00541485
.text:00541486
.text:00541487
.text:00541488
.text:00541489
.text:00541489
.text:00541489
.text:00541489
.text:0054148D
.text:00541490
.text:00541493
.text:00541496
.text:00541497
.text:00541498
.text:00541499
.text:0054149E
.text:005414A2
.text:005414A7
.text:005414A8
.text:005414AD
.text:005414AF
.text:005414B0

push
ebx
push
ebp
push
esi
push
edi
push
offset aRb
; "rb"
push
eax
; Filename
call
_fopen
mov
esi, eax
add
esp, 8
test
esi, esi
jnz
short loc_54142E
push
offset aCannotOpenIn_0 ; "Cannot open input file!\n"
call
_printf
add
esp, 4
pop
edi
pop
esi
pop
ebp
pop
ebx
retn
; --------------------------------------------------------------------------loc_54142E:

; CODE XREF: decrypt_file+1A
push
2
; Origin
push
0
; Offset
push
esi
; File
call
_fseek
push
esi
; File
call
_ftell
push
0
; Origin
push
0
; Offset
push
esi
; File
mov
ebp, eax
call
_fseek
push
ebp
; Size
call
_malloc
push
esi
; File
mov
ebx, eax
push
1
; Count
push
ebp
; ElementSize
push
ebx
; DstBuf
call
_fread
push
esi
; File
call
_fclose
add
esp, 34h
mov
ecx, 3
mov
edi, offset aQr9_0 ; "QR9"
mov
esi, ebx
xor
edx, edx
repe cmpsb
jz
short loc_541489
push
offset aFileIsNotCrypt ; "File is not crypted!\n"
call
_printf
add
esp, 4
pop
edi
pop
esi
pop
ebp
pop
ebx
retn
; --------------------------------------------------------------------------loc_541489:
mov
mov
add
lea
push
push
push
call
mov
push
push
call
mov
push
push

; CODE XREF: decrypt_file+75
eax, [esp+10h+Src]
edi, [ebx+3]
ebp, 0FFFFFFF9h
esi, [ebx+7]
eax
; Src
ebp
; int
esi
; int
decrypt
ecx, [esp+1Ch+arg_4]
offset aWb_0
; "wb"
ecx
; Filename
_fopen
ebp, eax
ebp
; File
1
; Count

242

7.2. “QR9”: ЛЮБИТЕЛЬСКАЯ КРИПТОСИСТЕМА ВДОХНОВЛЕННАЯ КУБИКОМ РУБИКА ГЛАВА 7. ЕЩЕ ПРИМЕРЫ
.text:005414B2
.text:005414B3
.text:005414B4
.text:005414B9
.text:005414BA
.text:005414BF
.text:005414C0
.text:005414C5
.text:005414C8
.text:005414C9
.text:005414CA
.text:005414CB
.text:005414CC
.text:005414CC decrypt_file

push
push
call
push
call
push
call
add
pop
pop
pop
pop
retn
endp

edi
esi
_fwrite
ebp
_fclose
ebx
_free
esp, 2Ch
edi
esi
ebp
ebx

; Size
; Str
; File
; Memory

Все имена функций и меток даны мною в процессе анализа.
Я начал с самого верха. Вот функция, берущая на вход два имени файла и пароль.
.text:00541320
.text:00541320
.text:00541320
.text:00541320
.text:00541320
.text:00541320
.text:00541320

; int __cdecl crypt_file(int Str, char *Filename, int password)
crypt_file
proc near
Str
Filename
password

= dword ptr
= dword ptr
= dword ptr

4
8
0Ch

Открыть файл и сообщить об ошибке в случае ошибки:
.text:00541320
mov
eax, [esp+Str]
.text:00541324
push
ebp
.text:00541325
push
offset Mode
; "rb"
.text:0054132A
push
eax
; Filename
.text:0054132B
call
_fopen
; open file
.text:00541330
mov
ebp, eax
.text:00541332
add
esp, 8
.text:00541335
test
ebp, ebp
.text:00541337
jnz
short loc_541348
.text:00541339
push
offset Format
; "Cannot open input file!\n"
.text:0054133E
call
_printf
.text:00541343
add
esp, 4
.text:00541346
pop
ebp
.text:00541347
retn
.text:00541348 ; --------------------------------------------------------------------------.text:00541348
.text:00541348 loc_541348:

Узнать размер файла используя fseek()/ftell():
.text:00541348
.text:00541349
.text:0054134A
.text:0054134B
.text:0054134D
.text:0054134F

push
push
push
push
push
push

ebx
esi
edi
2
0
ebp

; Origin
; Offset
; File

; переместить текущую позицию файла на конец
.text:00541350 call
_fseek
.text:00541355 push
ebp
; File
.text:00541356 call
_ftell
; узнать текущую позицию
.text:0054135B push
0
; Origin
.text:0054135D push
0
; Offset
.text:0054135F push
ebp
; File
.text:00541360 mov
[esp+2Ch+Str], eax
; переместить текущую позицию файла на начало
.text:00541364 call
_fseek

Этот фрагмент кода вычисляет длину файла выровненную по 64-байтной границе. Это потому что этот
алгоритм шифрования работает только с блоками размерами 64 байта. Работает очень просто: разделить
длину файла на 64, забыть об остатке, прибавить 1, умножить на 64. Следующий код удаляет остаток от
деления как если бы это значение уже было разделено на 64 и добавляет 64. Это почти то же самое.
.text:00541369 mov

esi, [esp+2Ch+Str]

; сбросить в ноль младшие 6 бит

243

7.2. “QR9”: ЛЮБИТЕЛЬСКАЯ КРИПТОСИСТЕМА ВДОХНОВЛЕННАЯ КУБИКОМ РУБИКА ГЛАВА 7. ЕЩЕ ПРИМЕРЫ
.text:0054136D and

esi, 0FFFFFFC0h

; выровнять размер по 64-байтной границе
.text:00541370 add
esi, 40h

Выделить буфер с выровненным размером:
.text:00541373
.text:00541374

push
call

esi
_malloc

; Size

Вызвать memset(), т.е., очистить выделенный буфер12 .
.text:00541379
.text:0054137B
.text:0054137D
.text:0054137F
.text:00541381
.text:00541383
.text:00541384
.text:00541387
.text:00541389
.text:0054138B
.text:0054138D
.text:00541390

mov
ecx,
mov
ebx,
mov
edx,
xor
eax,
mov
edi,
push
ebp
shr
ecx,
rep stosd
mov
ecx,
push
1
and
ecx,
rep stosb

esi
eax
ecx
eax
ebx

; указатель на выделенный буфер -> to EBX

; File
2
edx
; Count
3
; memset (buffer, 0, выровненный_размер)

Чтение файла используя стандартную функцию Си fread().
.text:00541392
.text:00541396
.text:00541397
.text:00541398
.text:0054139D
.text:0054139E

mov
push
push
call
push
call

eax, [esp+38h+Str]
eax
; ElementSize
ebx
; DstBuf
_fread
; read file
ebp
; File
_fclose

Вызов crypt(). Эта функция берет на вход буфер, длину буфера (выровненную) и строку пароля.
.text:005413A3
.text:005413A7
.text:005413A8
.text:005413A9
.text:005413AA

mov
push
push
push
call

ecx, [esp+44h+password]
ecx
; password
esi
; aligned size
ebx
; buffer
crypt
; do crypt

Создать выходной файл. Кстати, разработчик забыл вставить проверку, создался ли файл успешно! Результат открытия файла, впрочем, проверяется.
.text:005413AF
.text:005413B3
.text:005413B6
.text:005413BB
.text:005413BC
.text:005413C1

mov
add
push
push
call
mov

edx, [esp+50h+Filename]
esp, 40h
offset aWb
; "wb"
edx
; Filename
_fopen
edi, eax

Теперь хэндл созданного файла в регистре EDI. Зписываем сигнатуру “QR9”.
.text:005413C3
.text:005413C4
.text:005413C6
.text:005413C8
.text:005413CD

push
push
push
push
call

edi
1
3
offset aQr9
_fwrite

;
;
;
;
;

File
Count
Size
"QR9"
write file signature

Записываем настоящую длину файла (не выровненную):
.text:005413D2
.text:005413D3
.text:005413D5
.text:005413D9
.text:005413DB
.text:005413DC

push
push
lea
push
push
call

edi
; File
1
; Count
eax, [esp+30h+Str]
4
; Size
eax
; Str
_fwrite
; write original file size

Записываем шифрованный буфер:
12

malloc() + memset() можно было бы заменить на calloc()

244

7.2. “QR9”: ЛЮБИТЕЛЬСКАЯ КРИПТОСИСТЕМА ВДОХНОВЛЕННАЯ КУБИКОМ РУБИКА ГЛАВА 7. ЕЩЕ ПРИМЕРЫ
.text:005413E1
.text:005413E2
.text:005413E4
.text:005413E5
.text:005413E6

push
push
push
push
call

edi
1
esi
ebx
_fwrite

;
;
;
;
;

File
Count
Size
Str
write crypted file

Закрыть файл и освободить выделенный буфер:
.text:005413EB
.text:005413EC
.text:005413F1
.text:005413F2
.text:005413F7
.text:005413FA
.text:005413FB
.text:005413FC
.text:005413FD
.text:005413FE
.text:005413FE crypt_file

push
call
push
call
add
pop
pop
pop
pop
retn
endp

edi
_fclose
ebx
_free
esp, 40h
edi
esi
ebx
ebp

; File
; Memory

Переписанный на Си код:
void crypt_file(char *fin, char* fout, char *pw)
{
FILE *f;
int flen, flen_aligned;
BYTE *buf;
f=fopen(fin, "rb");
if (f==NULL)
{
printf ("Cannot open input file!\n");
return;
};
fseek (f, 0, SEEK_END);
flen=ftell (f);
fseek (f, 0, SEEK_SET);
flen_aligned=(flen&0xFFFFFFC0)+0x40;
buf=(BYTE*)malloc (flen_aligned);
memset (buf, 0, flen_aligned);
fread (buf, flen, 1, f);
fclose (f);
crypt (buf, flen_aligned, pw);
f=fopen(fout, "wb");
fwrite ("QR9", 3, 1, f);
fwrite (&flen, 4, 1, f);
fwrite (buf, flen_aligned, 1, f);
fclose (f);
free (buf);
};

Процедура дешифрования почти такая же:
.text:00541400
.text:00541400
.text:00541400
.text:00541400
.text:00541400
.text:00541400
.text:00541400
.text:00541400
.text:00541404
.text:00541405
.text:00541406
.text:00541407

; int __cdecl decrypt_file(char *Filename, int, void *Src)
decrypt_file
proc near
Filename
arg_4
Src

= dword ptr
= dword ptr
= dword ptr
mov
push
push
push
push

4
8
0Ch

eax, [esp+Filename]
ebx
ebp
esi
edi

245

7.2. “QR9”: ЛЮБИТЕЛЬСКАЯ КРИПТОСИСТЕМА ВДОХНОВЛЕННАЯ КУБИКОМ РУБИКА ГЛАВА 7. ЕЩЕ ПРИМЕРЫ
.text:00541408
push
offset aRb
; "rb"
.text:0054140D
push
eax
; Filename
.text:0054140E
call
_fopen
.text:00541413
mov
esi, eax
.text:00541415
add
esp, 8
.text:00541418
test
esi, esi
.text:0054141A
jnz
short loc_54142E
.text:0054141C
push
offset aCannotOpenIn_0 ; "Cannot open input file!\n"
.text:00541421
call
_printf
.text:00541426
add
esp,4
.text:00541429
pop
edi
.text:0054142A
pop
esi
.text:0054142B
pop
ebp
.text:0054142C
pop
ebx
.text:0054142D
retn
.text:0054142E ; --------------------------------------------------------------------------.text:0054142E
.text:0054142E loc_54142E:
.text:0054142E
push
2
; Origin
.text:00541430
push
0
; Offset
.text:00541432
push
esi
; File
.text:00541433
call
_fseek
.text:00541438
push
esi
; File
.text:00541439
call
_ftell
.text:0054143E
push
0
; Origin
.text:00541440
push
0
; Offset
.text:00541442
push
esi
; File
.text:00541443
mov
ebp, eax
.text:00541445
call
_fseek
.text:0054144A
push
ebp
; Size
.text:0054144B
call
_malloc
.text:00541450
push
esi
; File
.text:00541451
mov
ebx, eax
.text:00541453
push
1
; Count
.text:00541455
push
ebp
; ElementSize
.text:00541456
push
ebx
; DstBuf
.text:00541457
call
_fread
.text:0054145C
push
esi
; File
.text:0054145D
call
_fclose

Проверяем сигнатуру (первые 3 байта):
.text:00541462
.text:00541465
.text:0054146A
.text:0054146F
.text:00541471
.text:00541473
.text:00541475

add
esp, 34h
mov
ecx, 3
mov
edi, offset aQr9_0 ; "QR9"
mov
esi, ebx
xor
edx, edx
repe cmpsb
jz
short loc_541489

Сообщить об ошибке если сигнатура отсутствует:
.text:00541477
push
offset aFileIsNotCrypt ; "File is not crypted!\n"
.text:0054147C
call
_printf
.text:00541481
add
esp, 4
.text:00541484
pop
edi
.text:00541485
pop
esi
.text:00541486
pop
ebp
.text:00541487
pop
ebx
.text:00541488
retn
.text:00541489 ; --------------------------------------------------------------------------.text:00541489
.text:00541489 loc_541489:

Вызвать decrypt().
.text:00541489
.text:0054148D
.text:00541490
.text:00541493
.text:00541496
.text:00541497
.text:00541498
.text:00541499
.text:0054149E
.text:005414A2

mov
mov
add
lea
push
push
push
call
mov
push

eax, [esp+10h+Src]
edi, [ebx+3]
ebp, 0FFFFFFF9h
esi, [ebx+7]
eax
; Src
ebp
; int
esi
; int
decrypt
ecx, [esp+1Ch+arg_4]
offset aWb_0
; "wb"

246

7.2. “QR9”: ЛЮБИТЕЛЬСКАЯ КРИПТОСИСТЕМА ВДОХНОВЛЕННАЯ КУБИКОМ РУБИКА ГЛАВА 7. ЕЩЕ ПРИМЕРЫ
.text:005414A7
.text:005414A8
.text:005414AD
.text:005414AF
.text:005414B0
.text:005414B2
.text:005414B3
.text:005414B4
.text:005414B9
.text:005414BA
.text:005414BF
.text:005414C0
.text:005414C5
.text:005414C8
.text:005414C9
.text:005414CA
.text:005414CB
.text:005414CC
.text:005414CC decrypt_file

push
call
mov
push
push
push
push
call
push
call
push
call
add
pop
pop
pop
pop
retn
endp

ecx
_fopen
ebp, eax
ebp
1
edi
esi
_fwrite
ebp
_fclose
ebx
_free
esp, 2Ch
edi
esi
ebp
ebx

; Filename

;
;
;
;

File
Count
Size
Str

; File
; Memory

Переписанный на Си код:
void decrypt_file(char *fin, char* fout, char *pw)
{
FILE *f;
int real_flen, flen;
BYTE *buf;
f=fopen(fin, "rb");
if (f==NULL)
{
printf ("Cannot open input file!\n");
return;
};
fseek (f, 0, SEEK_END);
flen=ftell (f);
fseek (f, 0, SEEK_SET);
buf=(BYTE*)malloc (flen);
fread (buf, flen, 1, f);
fclose (f);
if (memcmp (buf, "QR9", 3)!=0)
{
printf ("File is not crypted!\n");
return;
};
memcpy (&real_flen, buf+3, 4);
decrypt (buf+(3+4), flen-(3+4), pw);
f=fopen(fout, "wb");
fwrite (buf+(3+4), real_flen, 1, f);
fclose (f);
free (buf);
};

OK, посмотрим глубже.
Функция crypt():
.text:00541260
.text:00541260
.text:00541260
.text:00541260
.text:00541260
.text:00541260
.text:00541260
.text:00541261

crypt

proc near

arg_0
arg_4
arg_8

= dword ptr
= dword ptr
= dword ptr
push
mov

4
8
0Ch

ebx
ebx, [esp+4+arg_0]

247

7.2. “QR9”: ЛЮБИТЕЛЬСКАЯ КРИПТОСИСТЕМА ВДОХНОВЛЕННАЯ КУБИКОМ РУБИКА ГЛАВА 7. ЕЩЕ ПРИМЕРЫ
.text:00541265
.text:00541266
.text:00541267
.text:00541268
.text:0054126A
.text:0054126A loc_54126A:

push
push
push
xor

ebp
esi
edi
ebp, ebp

Этот фрагмент кода копирует часть входного буфера во внутренний буфер, который я поже назвал
“cube64”. Длина в регистре ECX. MOVSD означает скопировать 32-битное слово, так что, 16 32-битных слов
это как раз 64 байта.
.text:0054126A
.text:0054126E
.text:00541273
.text:00541275
.text:0054127A
.text:0054127C
.text:0054127D

mov
eax,
mov
ecx,
mov
esi,
mov
edi,
push
1
push
eax
rep movsd

[esp+10h+arg_8]
10h
ebx
; EBX is pointer within input buffer
offset cube64

Вызвать rotate_all_with_password():
.text:0054127F

call

rotate_all_with_password

Скопировать зашифрованное содержимое из “cube64” назад в буфер:
.text:00541284
.text:00541288
.text:0054128A
.text:0054128D
.text:00541290
.text:00541295
.text:0054129A
.text:0054129D
.text:0054129F

mov
eax,
mov
edi,
add
ebp,
add
esp,
mov
ecx,
mov
esi,
add
ebx,
cmp
ebp,
rep movsd

[esp+18h+arg_4]
ebx
40h
8
10h
offset cube64
40h ; add 64 to input buffer pointer
eax ; EBP contain ammount of crypted data.

Если EBP не больше чем длина во входном аргументе, тогда переходим к следующему блоку.
.text:005412A1
.text:005412A3
.text:005412A4
.text:005412A5
.text:005412A6
.text:005412A7
.text:005412A7 crypt

jl
pop
pop
pop
pop
retn
endp

short loc_54126A
edi
esi
ebp
ebx

Реконструированная функция crypt():
void crypt (BYTE *buf, int sz, char *pw)
{
int i=0;
do
{
memcpy (cube, buf+i, 8*8);
rotate_all (pw, 1);
memcpy (buf+i, cube, 8*8);
i+=64;
}
while (i=’a’ && c24)
q-=24;
int quotient=q/3;
int remainder=q % 3;
switch (remainder)

250

7.2. “QR9”: ЛЮБИТЕЛЬСКАЯ КРИПТОСИСТЕМА ВДОХНОВЛЕННАЯ КУБИКОМ РУБИКА ГЛАВА 7. ЕЩЕ ПРИМЕРЫ
{
case 0: for (int i=0; i