Невозможно отучить людей изучать самые ненужные предметы.
Введение в CSS
Преимущества стилей
Добавления стилей
Типы носителей
Базовый синтаксис
Значения стилевых свойств
Селекторы тегов
Классы
CSS3
Надо знать обо всем понемножку, но все о немногом.
Идентификаторы
Контекстные селекторы
Соседние селекторы
Дочерние селекторы
Селекторы атрибутов
Универсальный селектор
Псевдоклассы
Псевдоэлементы
Кто умеет, тот делает. Кто не умеет, тот учит. Кто не умеет учить - становится деканом. (Т. Мартин)
Группирование
Наследование
Каскадирование
Валидация
Идентификаторы и классы
Написание эффективного кода
Вёрстка
Изображения
Текст
Цвет
Линии и рамки
Углы
Списки
Ссылки
Дизайны сайтов
Формы
Таблицы
CSS3
HTML5
Блог для вебмастеров
Новости мира Интернет
Сайтостроение
Ремонт и советы
Все новости
Справочник от А до Я
HTML, CSS, JavaScript
Афоризмы о учёбе
Статьи об афоризмах
Все Афоризмы
Помогли мы вам |
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.
Ссылки на другие статьи из этого цикла ищи на странице автора.
Для закрепления пройденного в прошлой статье материала рассмотрим несколько живых примеров. Начнем с самого простого — идентификации циклов while/do:
int main(){
int a = 0;
while (a++ < 10)printf("Оператор цикла whilen");
do {printf("Оператор цикла don");
} while (--a 0);}
Откомпилируем этот код с помощью 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: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 (
.
; Начало тела цикла:; заносим ссылку на строку "Оператор цикла whilen"lea
rcx, _Format
; _Format; выводим на консольcall
printf; безусловный переход, направленный назад, на метку loc_1400010EC - в начало цикла,; в область подготовки переменных для проверкиjmp
short loc_1400010EC
Между loc_1400010EC
и jmp
есть только одно условие выхода из цикла: jge
. Значит, исходный код цикла выглядел так:
Далее следует начало цикла с постусловием. Однако на данном этапе мы этого еще не знаем, хотя и можем догадаться благодаря наличию перекрестной ссылки, направленной вниз.
loc_140001113:; CODE XREF: main+23↑j
; main+4E↓j
Ага, никакого условия в начале цикла не присутствует, значит, это цикл с условием в конце или в середине.
lea
rcx, byte_140002278 ; _Format; печатаем в консолиcall
printf; тело цикла; загружаем в EAX значение переменной var_18 ("a")mov
eax, [rsp+38h+var_18]; уменьшаем значение в EAX на 1dec
eax; возвращаем значение из EAX в переменную "a" - var_18mov
[rsp+38h+var_18], eax; сравниваем переменную "a" с нулемcmp
[rsp+38h+var_18], 0; Если (a 0), делаем переход в начало циклаjgshort loc_140001113
Поскольку условие расположено в конце цикла, это цикл do
:
while (--a 0);
; возвращаем 0
xor eax, eax
; восстанавливаем стек
add rsp, 38h
retn
mainendp
Совсем другой результат получится, если включить оптимизацию. Откомпилируем тот же самый пример с ключом /
(максимальная оптимизация: приоритет скорости) и посмотрим на результат, выданный компилятором:
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, 0Ahnop
dword ptr [rax+rax+00h]; Судя по следующей перекрестной ссылке, направленной вниз, это цикл!loc_140001080:; CODE XREF: main+20↓j; заносим в регистр RCX ссылку на строку "Оператор цикла whilen"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++ дает следующую инструкцию:
do {
printf("Оператор цикла whilen");
var_RBX--;} while (var_RBX 0);
Вполне красивый и оптимальный цикл с одной переменной.
; Этот код выполняется после завершения предыдущего цикла.mov
ebx, 0Bhnop
word ptr [rax+rax+00000000h]; Перекрестная ссылка, направленная вниз, говорит нам о том, что это начало циклаloc_1400010A0:; CODE XREF: main+40↓j; Предусловия нет, значит, это цикл do; заносим в регистр RCX ссылку на строку "Оператор цикла don"lea
rcx, byte_140002278 ; _Format; выводим строку на терминалcall
printf; уменьшаем значение, загруженное в EBX, на единицуdec
ebx; проверяем EBX на равенство нулюtest
ebx, ebx; Продолжаем выполнение цикла, пока EBX 0jgshort loc_1400010A0
Этот цикл прямиком отображается в конструкцию языка C/C++:
do { printf("Оператор цикла don"); }while (--var_EBX 0);
; возвращаем ноль
xor eax, eax
; восстанавливаем стек
add rsp, 20h
; восстанавливаем регистр
pop rbx
retn
mainendp
Несколько иначе обрабатывает циклы компилятор Embarcadero C++ Builder 10.4. Смотри пример while-do_cb
:
public mainmainproc 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; сохраняем в стеке RBPpush
rbp; резервируем память для локальных переменныхsub
rsp, 40h; помещаем в RBP указатель на дно стекаlea
rbp, [rsp+40h]; инициализируем переменные:; в var_4 записывает 0, вероятно это переменная a из исходного кодаmov
[rbp+var_4], 0mov
[rbp+var_8], ecxmov
[rbp+var_10], rdx; еще одна переменная, изначально равная нулю, возьмем на заметкуmov
[rbp+var_14], 0; Ниже перекрестная ссылка, направленная вниз, значит, это начало какого-то циклаloc_40141F:; CODE XREF: main+3E↓j; в начале цикла условие не обнаружено, видимо, цикл с постусловием,; хотя не будем спешить с выводами; в регистр EAX копируем значение из переменной var_14mov
eax, [rbp+var_14]; копирование EAX в ECXmov
ecx, eax; увеличиваем значение в регистре ECX на 1add
ecx, 1; увеличенное значение из регистра ECX копируем в переменную var_14,; из которой берется значение для счетчика в начале итерацииmov
[rbp+var_14], ecx; сравнение не увеличенного значения с 0хАcmp
eax, 0Ah; если это значение больше или равно константе,; тогда выполняем прыжок за пределы цикла в область старших адресовjge
short loc_401440; в случае продолжения выполнения помещаем ссылку на строку в регистр; и выводим ее на консольlea
rcx, aOperatorIklaWh ; "Оператор цикла whilen"call
printf; зачем-то сохраняем текущее значение регистра EAX в переменной var_18...mov
[rbp+var_18], eax; ... и выполняем безусловный переход в начало циклаjmp
short loc_40141F
Вот так‑то C++ Builder оптимизировал код! Начальный цикл с предусловием выполнения он превратил в бесконечный цикл с условием выхода посередине (за подробностями обратись к прошлой статье)! Как мы можем декомпилировать этот цикл? Напрашивается такой вариант:
do {
int var_EAX = var_14;
int var_ECX = var_EAX;
var_ECX++;
var_14 = var_ECX;
if (var_EAX = 0xA) break;
printf("Оператор цикла whilen");} 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 ; "Оператор цикла don"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 превратил изначальный цикл с постусловием? В целом, никаких изменений он не внес, оставив все на своих местах. И декомпилированный листинг этого цикла должен выглядеть примерно так:
do{
int var_EAX = var_14;
var_EAX--;
var_14 = var_EAX;
printf("Оператор цикла whilen");} while (var_EAX 0);
; В ином случае, когда (EAX <= 0), пропускаем переход
; и продолжаем выполнение кода программы
mov [rbp+var_4], 0
; Возвращаем ноль
mov eax, [rbp+var_4]
; Восстанавливаем стек
add rsp, 40h
; Восстанавливаем регистр
pop rbp
retn
mainendp
|
|