1. Архитектура ЭВМ и исполняемые файлы
Любая программа, с которой вы взаимодействуете, в конечном итоге представляет собой набор электрических сигналов, проходящих через микросхемы. Чтобы успешно заниматься обратной разработкой, искать уязвимости или оптимизировать алгоритмы, необходимо понимать, как абстрактный код, написанный программистом, превращается в эти сигналы, и как операционная система управляет этим процессом. Фундаментом для этого служит понимание архитектуры процессора и структуры исполняемых файлов.
Центральный процессор и его память
Центральный процессор (CPU) — это главное вычислительное устройство компьютера, которое умеет выполнять лишь строго ограниченный набор базовых команд: сложение, вычитание, перемещение данных и условные переходы. Процессор не понимает языки высокого уровня вроде Python или C++, он оперирует исключительно машинным кодом — последовательностью байтов, где каждый байт или группа байтов кодирует конкретную инструкцию.
Для выполнения вычислений процессору требуется память. Обращение к основной оперативной памяти (RAM) занимает время, поэтому внутри самого процессора существуют регистры — сверхбыстрые ячейки памяти микроскопического объема. Они работают на частоте самого процессора и используются для хранения промежуточных результатов вычислений, адресов памяти и флагов состояния.
В современной 64-битной архитектуре x86-64 (которую используют большинство современных ПК) регистры вмещают 64 бита (8 байт) данных. Ключевые регистры делятся на несколько групп:
* Регистры общего назначения: RAX, RBX, RCX, RDX и другие. Они используются для математических операций и хранения временных данных. Например, RAX традиционно хранит результат выполнения функции.
Указатель инструкций (Instruction Pointer): RIP. Это самый важный регистр для реверс-инженера. Он всегда содержит адрес памяти, по которому находится следующая* команда для выполнения. Изменяя значение RIP, злоумышленник может заставить программу выполнить вредоносный код.
* Регистр флагов: RFLAGS. Содержит набор логических индикаторов (флагов), которые меняются после каждой математической операции. Например, если при вычитании двух чисел получился ноль, устанавливается флаг нуля (Zero Flag). На основе этого флага процессор принимает решение, выполнять ли условный переход.
> Представьте кухню ресторана. Процессор — это шеф-повар. Оперативная память — это большой холодильник в подвале, куда нужно долго идти за продуктами. Регистры — это разделочная доска прямо под руками повара. На доске помещается мало продуктов, но работать с ними можно мгновенно.
Если мы хотим сложить два числа, процессор сначала загрузит их из «холодильника» (RAM) на «разделочную доску» (регистры), выполнит сложение, а затем отправит результат обратно.
Жизненный цикл программы: от текста к бинарному коду
Программисты пишут код в виде обычного текста. Чтобы этот текст стал понятен процессору, он должен пройти процесс трансляции, который состоит из нескольких этапов.
Сначала исходный код обрабатывается компилятором. Компилятор проверяет синтаксис и переводит конструкции языка высокого уровня в язык ассемблера — низкоуровневое, но все еще читаемое человеком представление машинных команд. Каждой инструкции ассемблера (например, MOV, ADD, JMP) соответствует конкретный машинный код.
Затем в дело вступает ассемблер (как программа-транслятор), который превращает текстовые инструкции в сырые байты, создавая объектный файл. Однако этот файл еще нельзя запустить. В нем могут быть вызовы функций, код которых находится в других файлах или системных библиотеках (например, функция вывода текста на экран).
На финальном этапе работает компоновщик (linker). Он собирает все объектные файлы воедино, находит недостающие функции в библиотеках и связывает их, формируя итоговый исполняемый файл.
!Схема преобразования исходного кода в исполняемый файл
Например, если вы написали программу на C размером 5 килобайт, после компиляции объектный файл может весить 2 килобайта. Но после работы компоновщика, который добавит в файл стандартные библиотеки для работы с вводом-выводом, итоговый .exe файл может увеличиться до 50 килобайт.
Анатомия исполняемого файла
Исполняемый файл — это не просто сплошной поток машинного кода. Если бы это было так, операционная система не знала бы, с какого места начинать выполнение, сколько памяти выделить программе и какие библиотеки ей нужны.
Для решения этой проблемы существуют стандартизированные форматы исполняемых файлов. В операционной системе Windows это формат PE (Portable Executable), а в Linux — ELF (Executable and Linkable Format). Несмотря на различия в деталях, их концептуальная структура схожа.
Исполняемый файл можно сравнить с книгой. У него есть оглавление (заголовки) и главы (секции).
Заголовки содержат метаданные: для какой архитектуры процессора скомпилирован файл, где находится точка входа (адрес первой инструкции, с которой начнется выполнение), и какие системные библиотеки требуются для работы.
Секции содержат сами данные программы. Они разделены по правам доступа, чтобы обеспечить безопасность и стабильность работы:
| Название секции | Содержимое | Права доступа в памяти |
| :--- | :--- | :--- |
| .text | Исполняемый машинный код программы. Именно здесь реверс-инженеры проводят большую часть времени, анализируя логику. | Чтение и Выполнение (Read, Execute) |
| .data | Глобальные переменные, которым задано начальное значение при написании кода (например, int score = 100;). | Чтение и Запись (Read, Write) |
| .rdata / .rodata | Данные только для чтения: константы и жестко закодированные строки (например, текст "Введите пароль:"). | Только чтение (Read) |
| .bss | Неинициализированные глобальные переменные. В файле на диске эта секция не занимает места, память под нее выделяется только при запуске. | Чтение и Запись (Read, Write) |
Разделение прав доступа критически важно для безопасности. Если злоумышленник попытается изменить машинный код программы прямо во время ее работы (записать данные в секцию .text), операционная система немедленно завершит процесс с ошибкой нарушения доступа, так как у этой секции нет прав на запись.
Виртуальная память и загрузка программы
Когда вы дважды кликаете по исполняемому файлу, операционная система передает управление загрузчику (OS Loader). Загрузчик читает заголовки файла, выделяет необходимый объем оперативной памяти и копирует туда секции файла.
Здесь вступает в игру концепция виртуального адресного пространства. Современные операционные системы не дают программам прямого доступа к физическим микросхемам оперативной памяти. Вместо этого каждой программе выделяется собственная иллюзорная (виртуальная) память.
Для 32-битных программ это пространство составляет 4 гигабайта, а для 64-битных — астрономические величины, измеряемые терабайтами. Каждая запущенная программа «думает», что она единственная в системе и владеет всей памятью.
Например, если вы запустите два экземпляра одной и той же программы, внутри отладчика вы увидите, что их машинный код находится по абсолютно одинаковому виртуальному адресу, скажем, 0x00400000. Однако в реальности процессор и операционная система на лету транслируют этот виртуальный адрес в разные физические адреса на планке RAM.
Это решает сразу две задачи:
Для усложнения работы реверс-инженерам и создателям эксплойтов была внедрена технология ASLR (Address Space Layout Randomization). Если программа скомпилирована с поддержкой ASLR, загрузчик операционной системы при каждом новом запуске будет случайным образом сдвигать базовый адрес загрузки программы.
Если вчера функция проверки пароля находилась по адресу 0x00401050, то после перезагрузки компьютера она может оказаться по адресу 0x007A1050. Это делает невозможным создание универсальных вредоносных программ, которые полагаются на жестко заданные адреса памяти, и заставляет реверс-инженеров вычислять адреса динамически, опираясь на относительные смещения.
Понимание того, как код взаимодействует с регистрами, как он упакован в секции PE/ELF файлов и как проецируется в виртуальную память — это базовый навык. Без него дизассемблированный листинг программы будет казаться бессмысленным набором символов. В дальнейшем эти знания позволят находить точки входа, анализировать алгоритмы шифрования и выявлять уязвимости, связанные с повреждением памяти.