Содержание статьи
- Циклы while/do
- Visual C++ 2022 с отключенной оптимизацией
- Visual C++ 2022 с включенной оптимизацией
- C++Builder 10 без оптимизации
- C++Builder 10 с оптимизацией
- Delphi 10
- Циклы for
- Visual C++ 2022 без оптимизации
- Visual C++ 2022 с применением оптимизации
- C++Builder 10
- Циклы с условием в середине
- Идентификация break
- Идентификация continue
- Циклы for с несколькими счетчиками
- Заключение
Для понимания происходящего тебе не помешает ознакомиться и с прошлой статьей, где мы рассмотрели теорию устройства разных видов циклов на языках высокого уровня и их отражение в дизассемблерных листингах. А сейчас настало время практики!
Фундаментальные основы хакерства
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.
Ссылки на другие статьи из этого цикла ищи на странице автора.
Циклы while/do
Visual C++ 2022 с отключенной оптимизацией
Для закрепления пройденного в прошлой статье материала рассмотрим несколько живых примеров. Начнем с самого простого — идентификации циклов while/do:
#include <stdio.h>int main(){ int a = 0; while (a++ < 10) printf("Оператор цикла while\n"); do { printf("Оператор цикла do\n"); } while (--a > 0);}
Откомпилируем этот код с помощью Visual C++ 2022 с отключенной оптимизацией.
Результат компиляции должен выглядеть примерно так:
; int __cdecl main(int argc, const char **argv, const char **envp)main proc near ; CODE XREF: __scrt_common_main_seh+107↓p ; DATA XREF: .pdata:0000000140004018↓ovar_18 = dword ptr -18hvar_14 = dword ptr -14h; Резервируем память для двух локальных переменных,; Только откуда взялась вторая? sub rsp, 38h; Заносим в переменную var_18 значение 0; Следовательно, это переменная a mov [rsp+38h+var_18], 0
Ниже следует перекрестная ссылка — loc_1400010EC
, направленная вниз. Это говорит нам о том, что перед нами начало цикла. Поскольку перекрестная ссылка направлена вниз, то переход, ссылающийся на этот адрес, будет направлен вверх!
loc_1400010EC: ; CODE XREF: main+31↓j; Загружаем в EAX значение переменной a (var_18) mov eax, [rsp+38h+var_18]; Загружаем в var_14 значение переменной a, вот, мы нашли,; где используется вторая переменная mov [rsp+38h+var_14], eax; Зачем-то снова загружаем то же значение в регистр EAX mov eax, [rsp+38h+var_18]; Увеличение значения в регистре EAX на 1 inc eax; Загружаем значение из регистра EAX в переменную var_18 ("a") mov [rsp+38h+var_18], eax; Сравниваем старое (до обновления) значение переменной a,; ранее сохраненное в var_14, с числом 0xA cmp [rsp+38h+var_14], 0Ah
Если (var_14 >
), делаем прыжок «вперед», непосредственно за инструкцию безусловного перехода, направленного «назад». Если выполняется прыжок «назад», значит, это цикл, а поскольку условие выхода из цикла проверяется в его начале, то это цикл с предусловием.
Для его отображения на цикл while
необходимо инвертировать условие выхода из цикла на условие продолжения цикла, другими словами, исправить >
на <
.
Сделав это, мы получаем: while (
.
jge short loc_140001113; Начало тела цикла:; заносим ссылку на строку "Оператор цикла while\n" lea rcx, _Format ; _Format; Выводим на консоль call printf; Безусловный переход, направленный назад, на метку loc_1400010EC — в начало цикла,; в область подготовки переменных для проверки jmp short loc_1400010EC
Между loc_1400010EC
и jmp
есть только одно условие выхода из цикла: jge
. Значит, исходный код цикла выглядел так:
while (a++ < 0xA) printf("Оператор цикла while\n");
Далее следует начало цикла с постусловием. Однако на данном этапе мы этого еще не знаем, хотя и можем догадаться благодаря наличию перекрестной ссылки, направленной вниз.
loc_140001113: ; CODE XREF: main+23↑j ; main+4E↓j
Ага, никакого условия в начале цикла не присутствует, значит, это цикл с условием в конце или в середине.
; Заносим ссылку на строку "Оператор цикла do\n" lea rcx, byte_140002278 ; _Format; Печатаем в консоли call printf; Тело цикла; Загружаем в EAX значение переменной var_18 ("a") mov eax, [rsp+38h+var_18]; Уменьшаем значение в EAX на 1 dec eax; Возвращаем значение из EAX в переменную a — var_18 mov [rsp+38h+var_18], eax; Сравниваем переменную a с нулем cmp [rsp+38h+var_18], 0; Если (a > 0), делаем переход в начало цикла jg short loc_140001113
Поскольку условие расположено в конце цикла, это цикл do
:
do printf("Оператор цикла do\n");while (--a > 0);
; Возвращаем 0 xor eax, eax; Восстанавливаем стек add rsp, 38h retnmain endp
Visual C++ 2022 с включенной оптимизацией
Совсем другой результат получится, если включить оптимизацию. Откомпилируем тот же самый пример с ключом /
(максимальная оптимизация: приоритет скорости) и посмотрим на результат, выданный компилятором:
; int __cdecl main(int argc, const char **argv, const char **envp)main proc near ; CODE XREF: __scrt_common_main_seh+107↓p ; DATA XREF: .pdata:000000014000400C↓o; Сохраняем регистр в стеке push rbx; Подготавливаем стек, ни одной локальной переменной не объявлено sub rsp, 20h; В EBX кладем число 0xA. Для чего, пока неясно mov ebx, 0Ah nop dword ptr [rax+rax+00h]; Судя по следующей перекрестной ссылке, направленной вниз, это цикл!loc_140001080: ; CODE XREF: main+20↓j; Заносим в регистр RCX ссылку на строку "Оператор цикла while\n" lea rcx, _Format ; _Format; Выводим строку на терминал call printf; Если это тело цикла, то где же предусловие?!; Вычитаем из RBX число 1 sub rbx, 1; Получается, что число 0xA, помещенное в EBX ранее, было начальным значением
Инструкция SUB
подобно CMP
изменяет состояние флага нуля. Если в результате вычитания получается 0, флаг нуля возводится в единицу. Следующая инструкция совершает прыжок назад, когда флаг не возведен, то есть в результате вычитания регистр RBX
не стал равен нулю.
jnz short loc_140001080
Компилятор в порыве оптимизации превратил неэффективный цикл с предусловием в более компактный и быстрый цикл с постусловием. Имел ли он на это право? А почему нет?! Проанализировав код, компилятор понял, что этот цикл выполняется по крайней мере один раз. Следовательно, скорректировав условие продолжения, его проверку можно вынести в конец цикла.
Также в исходном тексте был инкремент счетчика цикла от нуля до 0xA
, а в подготовленном транслятором коде мы видим обратный эффект: декремент счетчика от 0xA
до нуля. Таким образом, компилятор while ((
заменил do
.
Причем, что интересно, он не сравнивал переменную цикла с константой, а поместил константу в регистр и уменьшал его до тех пор, пока тот не стал равен нулю! Зачем? А затем, что так короче, да и работает быстрее.
Хорошо, но как нам декомпилировать этот цикл? Непосредственное отображение на язык C/C++ дает следующую инструкцию:
var_RBX = 0xA;do { printf("Оператор цикла while\n"); var_RBX--;} while (var_RBX > 0);
Вполне красивый и оптимальный цикл с одной переменной.
; Значение 0xB помещаем в регистр EBX. Это подготовка к следующему циклу; Этот код выполняется после завершения предыдущего цикла mov ebx, 0Bh nop word ptr [rax+rax+00000000h]; Перекрестная ссылка, направленная вниз, говорит нам о том, что это начало циклаloc_1400010A0: ; CODE XREF: main+40↓j; Предусловия нет, значит, это цикл do; Заносим в регистр RCX ссылку на строку "Оператор цикла do\n" lea rcx, byte_140002278 ; _Format; Выводим строку на терминал call printf; Уменьшаем значение, загруженное в EBX, на единицу dec ebx; Проверяем EBX на равенство нулю test ebx, ebx; Продолжаем выполнение цикла, пока EBX > 0 jg short loc_1400010A0
Этот цикл прямиком отображается в конструкцию языка C/C++:
var_EBX = 0xB;do { printf("Оператор цикла do\n"); }while (--var_EBX > 0);
; Возвращаем ноль xor eax, eax; Восстанавливаем стек add rsp, 20h; Восстанавливаем регистр pop rbx retnmain endp
C++Builder 10 без оптимизации
Несколько иначе обрабатывает циклы компилятор Embarcadero C++Builder 10.4. Смотри пример while-do_cb
:
; int __cdecl main(int argc, const char **argv, const char **envp) public mainmain proc near ; DATA XREF: __acrtused+29↑o; Объявляем шесть переменныхvar_1C = dword ptr -1Chvar_18 = dword ptr -18hvar_14 = dword ptr -14hvar_10 = qword ptr -10hvar_8 = dword ptr -8var_4 = dword ptr -4; Сохраняем в стеке RBP push rbp; Резервируем память для локальных переменных sub rsp, 40h; Помещаем в RBP указатель на дно стека lea rbp, [rsp+40h]; Инициализируем переменные:; в var_4 записывает 0, вероятно, это переменная a из исходного кода mov [rbp+var_4], 0 mov [rbp+var_8], ecx mov [rbp+var_10], rdx; Еще одна переменная, изначально равная нулю, возьмем на заметку mov [rbp+var_14], 0; Ниже перекрестная ссылка, направленная вниз, значит, это начало какого-то циклаloc_40141F: ; CODE XREF: main+3E↓j; В начале цикла условие не обнаружено, видимо, цикл с постусловием,; хотя не будем спешить с выводами; В регистр EAX копируем значение из переменной var_14 mov eax, [rbp+var_14]; Копирование EAX в ECX mov ecx, eax; Увеличиваем значение в регистре ECX на 1 add ecx, 1; Увеличенное значение из регистра ECX копируем в переменную var_14,; из которой берется значение для счетчика в начале итерации mov [rbp+var_14], ecx; Сравнение неувеличенного значения с 0хА cmp eax, 0Ah; Если это значение больше константы или равно ей,; выполняем прыжок за пределы цикла в область старших адресов jge short loc_401440; В случае продолжения выполнения помещаем ссылку на строку в регистр; и выводим ее на консоль lea rcx, aOperatorIklaWh ; "Оператор цикла while\n" call printf; Зачем-то сохраняем текущее значение регистра EAX в переменной var_18... mov [rbp+var_18], eax; ... и выполняем безусловный переход в начало цикла jmp short loc_40141F
Вот так‑то C++Builder оптимизировал код! Начальный цикл с предусловием выполнения он превратил в бесконечный цикл с условием выхода посередине (за подробностями обратись к прошлой статье)! Как мы можем декомпилировать этот цикл? Напрашивается такой вариант:
int var_14 = 0; do { int var_EAX = var_14; int var_ECX = var_EAX; var_ECX++; var_14 = var_ECX; if (var_EAX >= 0xA) break; printf("Оператор цикла while\n");} while (TRUE);
Этот вариант кардинально отличается от первоначального, и я очень сомневаюсь, что в лучшую сторону! Что ж, издержки производства...
; --------------------------------loc_401440: ; CODE XREF: main+2D↑j; Сюда происходит переход при выходе из предыдущего цикла; Как мы знаем, эта инструкция только переводит управление через себя jmp short $+2; --------------------------------loc_401442: ; CODE XREF: main:loc_401440↑j ; main+5D↓j; Новый цикл!; Как видим, он начинается с вывода строки, нет условия, значит, цикл с постусловием lea rcx, aOperatorIklaDo ; "Оператор цикла do\n" call printf
Проматываем дизассемблерный листинг вверх, чтобы вспомнить, какое значение находится в регистре EAX
. Значит, в этом месте программы значение в регистре EAX
равно 0хА
. Записываем это значение в переменную var_1C
(непонятно, для каких целей, ведь в будущем она не используется). Выходит, локальную переменную a
исходной программы представляет регистровая переменная EAX
.
mov [rbp+var_1C], eax; Записываем в регистр EAX значение переменной var_14; А в ней содержится значение на 1 больше, чем в EAX! То есть 0xB mov eax, [rbp+var_14]; Какой хитрый C++Builder!; Вместо реального вычитания он прибавляет к значению в EAX -1 add eax, 0FFFFFFFFh; Присваивает результат переменной var_14 mov [rbp+var_14], eax; И сравнивает уменьшенное значение с нулем cmp eax, 0; Если (EAX > 0), то мы прыгаем назад к началу «нового цикла»; и осуществлению очередной итерации jg short loc_401442
Во что C++Builder превратил изначальный цикл с постусловием? В целом никаких изменений он не внес, оставив все на своих местах. И декомпилированный листинг этого цикла должен выглядеть примерно так:
var var_14 = 0xB;do{ int var_EAX = var_14; var_EAX--; var_14 = var_EAX; printf("Оператор цикла while\n");} while (var_EAX > 0);
; В ином случае, когда (EAX <= 0), пропускаем переход; и продолжаем выполнение кода программы mov [rbp+var_4], 0; Возвращаем ноль mov eax, [rbp+var_4]; Восстанавливаем стек add rsp, 40h; Восстанавливаем регистр pop rbp retnmain endp
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»