Содержание статьи
Фундаментальные основы хакерства
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.
Ссылки на другие статьи из этого цикла ищи на странице автора.
Сегодняшняя статья — продолжение предыдущей, если ее не прочитать, разобраться в коде будет трудновато. Что ж, продолжим наш заплыв в океан условий, ветвлений, отношений и компромиссов.
Идентификация тернарного оператора
Конструкция a=(
языка C в общем случае транслируется так: IF (
, однако результат компиляции обеих конструкций, вопреки распространенному мнению, не всегда идентичен. Оператор «?:
значительно легче поддается оптимизации, чем ветвление IF
. Покажем это на следующем примере:
#include <iostream>int main() { int a = -2; int b = 2; a = (a > 0) ? 1 : -1; // Тернарный оператор
if (b > 0) // Ветвление
b = 1; else b = -1; std::cout << "a + b = " << a + b;}
Если пропустить эту программу сквозь компилятор Microsoft 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:ExceptionDir↓o; Четыре локальные переменныеvar_18 = dword ptr -18hvar_14 = dword ptr -14hvar_10 = dword ptr -10hvar_C = dword ptr -0Ch; Резервируем место для локальных переменных sub rsp, 38h; Инициализация локальных переменных:; var_14 присваиваем -2, компилятор выразил отрицательное число; сверхбольшим положительным mov [rsp+38h+var_14], 0FFFFFFFEh; var_18 присваиваем 2, здесь все прямолинейно mov [rsp+38h+var_18], 2; Сравниваем переменную var_14 с 0 cmp [rsp+38h+var_14], 0; Переход по метке, если в результате предыдущего сравнения первый операнд меньше второго или равен ему jle short loc_140001025 ; В нашем случае все сходится, делаем прыжок mov [rsp+38h+var_10], 1 jmp short loc_14000102D; --------------------------------------loc_140001025: ; CODE XREF: main+19↑j; Делаем переход сюда mov [rsp+38h+var_10], 0FFFFFFFFh ; var_10 присваиваем -1loc_14000102D: ; CODE XREF: main+23↑j mov eax, [rsp+38h+var_10] mov [rsp+38h+var_14], eax ; var_14 = -1; Сравниваем var_18 с 0, припоминаем или прокручиваем экран дизассемблера вверх,; в var_18 помещено значение 2 cmp [rsp+38h+var_18], 0; Переход по метке, если в результате предыдущего сравнения первый операнд меньше второго или равен ему jle short loc_140001046 ; В данном случае карты не сходятся, поэтому продолжаем выполнение mov [rsp+38h+var_18], 1 ; Переменной var_18 присваиваем значение 1 jmp short loc_14000104E ; Безусловный переход; --------------------------------------loc_140001046: ; CODE XREF: main+3A↑j mov [rsp+38h+var_18], 0FFFFFFFFhloc_14000104E: ; CODE XREF: main+44↑j; Прыгаем сюда mov eax, [rsp+38h+var_18] ; EAX = 1 mov ecx, [rsp+38h+var_14] ; ECX = -1 add ecx, eax ; -1 + 1 mov eax, ecx mov [rsp+38h+var_C], eax ; Переменной var_C присваиваем сумму — значение 0; Готовим параметры lea rdx, _Val ; "a + b = " mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout ; _Ostr; Вызываем оператор << для вывода строки call std::operator<<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,char const *) mov ecx, [rsp+38h+var_C] mov edx, ecx mov rcx, rax; Вызываем оператор << для вывода числа call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(int) xor eax, eax ; Возвращаем 0 add rsp, 38h ; Восстанавливаем стек retnmain endp
Во дела! Компилятор оба условных оператора скомпилил в одинаковый код! Теперь переведем компилятор в режим максимальной оптимизации с приоритетом по скорости — /
, произведем компиляцию, откроем результат в дизассемблере и посмотрим на результат:
main proc near sub rsp, 28h mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout ; _Ostr call std::operator<<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,char const *) mov rcx, rax xor edx, edx call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(int) xor eax, eax add rsp, 28h retnmain endp
Что за чертовщина?! Здесь не осталось никаких условностей! Даже если включить режим оптимизации с приоритетом размера — /
, ничего не изменится. Компилятор заоптимизировал все что можно!
А что нам продемонстрирует C++Builder? Сначала без оптимизации:
; int __cdecl main(int argc, const char **argv, const char **envp) public mainmain proc near ; DATA XREF: __acrtused+29↑ovar_20 = qword ptr -20hvar_18 = dword ptr -18hvar_14 = dword ptr -14hvar_10 = qword ptr -10hvar_8 = dword ptr -8var_4 = dword ptr -4; Открываем кадр стека push rbp; Выделяем память под локальные переменные sub rsp, 40h; Запоминаем адрес дна стека lea rbp, [rsp+40h] mov eax, 0FFFFFFFFh ; EAX = -1 mov r8d, 1 ; R8D = 1; Инициализация локальных переменных mov [rbp+var_4], 0 mov [rbp+var_8], ecx mov [rbp+var_10], rdx mov [rbp+var_14], 0FFFFFFFEh ; var_14 = -2 mov [rbp+var_18], 2 ; var_18 = 2 mov ecx, [rbp+var_14] ; ECX = -2; Сравниваем ECX с 0 cmp ecx, 0
Ага! Используется команда условной пересылки (за подробностями обратись к предыдущей статье): G значит Greater. Другими словами, данные копируются, если первый операнд больше второго (в таком случае флаги ZF
и SF
равны 0). В нашем случае условие не выполняется, так как -2 < 0, поэтому значение из регистра R8D
в регистр EAX
не копируется.
cmovg eax, r8d mov [rbp+var_14], eax ; var_14 = -1; Сравнение var_18 с 0 cmp [rbp+var_18], 0; Переход по метке, если в результате предыдущего сравнения первый операнд меньше второго или равен ему jle short loc_4013ED ; В данном случае карты не сходятся (2 > 0), поэтому продолжаем выполнение mov [rbp+var_18], 1 ; var_18 = 1 jmp short loc_4013F4 ; Безусловный прыжок; --------------------------------------loc_4013ED: ; CODE XREF: main+42↑j mov [rbp+var_18], 0FFFFFFFFhloc_4013F4: ; CODE XREF: main+4B↑j; Безусловный прыжок сюда; Подготовка параметров для вызова оператора вывода lea rcx, a008_free_list lea rdx, aAB ; "a + b = "; Вывод строки оператором << call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,char const*) mov r8d, [rbp+var_14] ; R8D = -1 add r8d, [rbp+var_18] ; R8D = -1 + 1; Готовим параметры для вызова оператора вывода mov rcx, rax mov edx, r8d; Вывод числа (суммы) оператором << call _ZNSolsEi ; std::ostream::operator<<(int) mov [rbp+var_4], 0 mov [rbp+var_20], rax; Возвращаем 0 mov eax, [rbp+var_4]; Восстанавливаем стек add rsp, 40h; Закрываем кадр стека pop rbp retnmain endp
Было интересно увидеть результат работы C++Builder, в особенности использование команды условной пересылки, которая позволила уменьшить количество условных переходов с двух до одного. За подробностями обратись к подготовленному Visual C++ неоптимизированному дизассемблерному листингу, чтобы увидеть оба условных перехода в действии.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»