Содержание статьи
- Циклы 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 с несколькими счетчиками
- Заключение
Для понимания происходящего тебе не помешает ознакомиться и с прошлой статьей, где мы рассмотрели теорию устройства разных видов циклов на языках высокого уровня и их отражение в дизассемблерных листингах. А сейчас настало время практики!
Циклы 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 с отключенной оптимизацией.
![Результат выполнения примера while-do Результат выполнения примера while-do](/uploads/posts/2022-08/while-do.png)
Результат компиляции должен выглядеть примерно так:
; int __cdecl main(int argc, const char **argv, const char **envp)
mainproc near; CODE XREF: __scrt_common_main_seh+107↓p
; DATA XREF: .pdаta:0000000140004018↓o
var_18= dword ptr -18h
var_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), делаем переход в начало цикла
jgshort loc_140001113
Поскольку условие расположено в конце цикла, это цикл do
:
do printf("Оператор цикла do\n");
while (--a > 0);
; возвращаем 0
xor eax, eax
; восстанавливаем стек
add rsp, 38h
retn
mainendp
Visual C++ 2022 с включенной оптимизацией
Совсем другой результат получится, если включить оптимизацию. Откомпилируем тот же самый пример с ключом /
(максимальная оптимизация: приоритет скорости) и посмотрим на результат, выданный компилятором:
; int __cdecl main(int argc, const char **argv, const char **envp)
mainproc near; CODE XREF: __scrt_common_main_seh+107↓p
; DATA XREF: .pdаta: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
jgshort loc_1400010A0
Этот цикл прямиком отображается в конструкцию языка C/C++:
var_EBX = 0xB;
do { printf("Оператор цикла do\n"); }
while (--var_EBX > 0);
; возвращаем ноль
xor eax, eax
; восстанавливаем стек
add rsp, 20h
; восстанавливаем регистр
pop rbx
retn
mainendp
C++ Builder 10 без оптимизации
Несколько иначе обрабатывает циклы компилятор Embarcadero C++ Builder 10.4. Смотри пример while-do_cb
:
; int __cdecl main(int argc, const char **argv, const char **envp)
public main
mainproc near; DATA XREF: __acrtused+29↑o
; объявляем шесть переменных
var_1C= dword ptr -1Ch
var_18= dword ptr -18h
var_14= dword ptr -14h
var_10= qword ptr -10h
var_8= dword ptr -8
var_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), то мы прыгаем назад к началу "нового цикла"
; и осуществлению очередной итерации
jgshort 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
retn
mainendp
Скачать:
Скриншоты:
Важно:
Все статьи и материал на сайте размещаются из свободных источников. Приносим свои глубочайшие извинения, если Ваша статья или материал была опубликована без Вашего на то согласия.
Напишите нам, и мы в срочном порядке примем меры.