04 мая 2025 в 18:44

Танец регистров в архитектуре x86 или как процессор управляет хаосом

Научимся дизассемблировать простые программы на Си и на их примерах изучим основные регистры архитектуры x86. Разберем каждую строчку ассемблерного кода.

Введение

В современном мире программирования и разработки программного обеспечения, понимание внутреннего устройства компьютерных систем является ключевым фактором для создания эффективных, надежных и оптимизированных приложений. Для нас, начинающих исследователей и будущих реверс-инженеров, важно хотя бы на первых порах не испугаться дизассемблированного кода. В этой статье будут рассмотрены основные регистры процессора архитектуры x86 и разобраны реальные дизассемблированные программы.

Почему важно понимать архитектуру процессора?

Архитектура процессора – это фундамент, на котором строится все программное обеспечение. Она определяет, как процессор выполняет инструкции, управляет памятью и взаимодействует с периферийными устройствами.

Для чего могут пригодиться эти знания?

  • Оптимизировать код. Зная, как процессор обрабатывает данные и выполняет инструкции, разработчики могут писать более эффективный код, который использует ресурсы процессора более рационально.
  • Отлаживать и анализировать. Понимание низкоуровневых механизмов работы программ помогает выявлять и исправлять ошибки, а также анализировать поведение программ в сложных сценариях.
  • Проводить реверс-инжиниринг. Для исследователей и специалистов по безопасности, знание архитектуры процессора необходимо для анализа и понимания работы вредоносных программ и защиты от них.

Основные регистры и их роль

Одним из ключевых элементов архитектуры процессора являются регистры. Эти быстрые ячейки памяти, расположенные непосредственно в процессоре, играют критическую роль в выполнении программ. В этой статье мы рассмотрим:

  • Общие регистры. Такие как EAX, EBX, ECX, EDX, ESP, EBP, ESI и EDI которые используются для хранения данных и выполнения операций.
  • Сегментные регистры. Регистры CS, DS, SS, ES, FS, GS – управляют доступом к различным сегментам памяти.
  • Регистры управления и состояния. Регистры EIP и EFLAGS, которые отслеживают и управляют потоком выполнения и состоянием процессора.

Компиляция и дизассемблирование

Важным аспектом разработки и анализа программ является процесс преобразования исходного кода в машинный код и наоборот. Мы изучим:

  • Компиляцию. Как компиляторы, такие как GCC, преобразуют высокоуровневый код на языках программирования в машинный код, который может быть выполнен процессором. Мы рассмотрим различные этапы компиляции и оптимизации, которые делают программы более эффективными.
  • Дизассемблирование. Обратный процесс, который позволяет анализировать машинный код и преобразовывать его в ассемблерный код. Это важно для отладки, анализа и реверс-инжиниринга. Мы рассмотрим инструменты, такие как objdump, которые облегчают этот процесс.

В этой статье постараюсь предоставить вам всестороннее понимание основных регистров процессора, их функций и взаимодействия. Эти знания являются основой для разработки и анализа программного обеспечения, и надеюсь, что они помогут вам углубить ваше понимание компьютерных систем и повысить ваши навыки в области программирования и реверс-инжиниринга.

Общие регистры процессора: от x86 до x86-64

В мире компьютерной архитектуры регистры процессора играют ключевую роль, выступая в качестве быстрых и эффективных хранилищ данных, которые активно используются в различных операциях.

Для тех, кто только начинает погружаться в изучение архитектуры x86, знакомство с основными регистрами, такими как EAX, EBX, ECX, EDX, ESP, EBP, ESI и EDI, является важным шагом. Эти регистры, каждый из которых имеет свои уникальные функции и назначения, служат для хранения данных, адресации памяти, управления потоком выполнения и выполнения других операций.

Регистры общего назначения

  • EAX (extended accumulator register). Часто используется для операций ввода-вывода, арифметических вычислений и хранения возвращаемых значений функций.
  • EBX (base register). Применяется как указатель на данные в сегменте данных и для хранения адресов.
  • ECX (count register). Традиционно используется в циклах как счетчик итераций.
  • EDX (data register). Дополняет eax в операциях умножения и деления, а также участвует в операциях ввода-вывода.

Указательные и индексные регистры

  • ESP (stack pointer register). Указывает на вершину стека и активно используется для управления вызовами функций и локальными переменными.
  • EBP (base pointer register). Служит базовым указателем для текущего стекового фрейма, облегчая доступ к локальным переменным и параметрам функции.
  • ESI (source index) и EDI (destination index). Используются в операциях, связанных с перемещением данных, таких как строковые операции, где esi указывает на источник, а edi — на место назначения.

Переход на 64-битную архитектуру

С появлением архитектуры x86-64 эти регистры получили свои 64-битные аналоги: RAX, RBX, RCX, RDX, RSP, RBP, RSI и RDI. Расширение до 64 бит позволило увеличить объем данных, с которыми можно работать, и улучшить производительность в современных вычислительных задачах. Кроме того, в x86-64 добавлены новые регистры, такие как R8-R15, что предоставляет дополнительные возможности для оптимизации и параллельной обработки данных.

Понимание и эффективное использование этих регистров — это один из важных навыков в реверс-инжиниринге.

Примечание

В этой статье мы исследуем основные регистры процессора архитектуры x86 (EAX, EBX, ECX, EDX, ESP, EBP, ESI и EDI) для упрощения. Рассмотрим примеры кода на Си и его дизассемблированное представление.

Сегментные регистры: основы управления памятью в архитектуре x86

В архитектуре x86, помимо регистров общего назначения и указателей, существует особая категория регистров, известных как сегментные регистры. Эти регистры играют ключевую роль в управлении памятью и организации доступа к различным сегментам адресного пространства. Далее кратко познакомимся с сегментными регистрами: CS, DS, SS, ES, FS и GS.

Регистр CS (code segment register)

  • Назначение CS. Регистр CS указывает на сегмент кода, который содержит исполняемые инструкции программы. Этот регистр определяет, откуда процессор должен брать инструкции для выполнения.
  • Особенности CS. В защищенном режиме работы процессора, CS также содержит информацию о привилегиях и уровне доступа к коду. Это позволяет реализовать механизмы защиты, такие как разделение между пользовательским и системным кодом.

Регистр DS (data segment register)

  • Назначение DS. Регистр DS указывает на сегмент данных, который содержит глобальные и статические переменные программы. Этот регистр определяет, откуда процессор должен брать данные для операций.
  • Использование DS. Регистр DS часто используется для доступа к данным в сегменте данных, что позволяет программам эффективно управлять и обрабатывать информацию.

Регистр SS (stack segment register)

  • Назначение SS. Регистр SS указывает на сегмент стека, который используется для хранения локальных переменных, адресов возврата и временных данных. Этот регистр определяет, где находится стек для текущей программы.
  • Особенности SS. Управление стеком критично для вызовов функций и обработки исключений, поэтому SS играет важную роль в обеспечении корректной работы программы.

Регистр ES (extra segment register)

  • Назначение ES. Регистр ES – это дополнительный сегментный регистр, который может быть использован для различных целей, в зависимости от контекста. Традиционно он используется для операций с данными, которые требуют дополнительного сегмента.
  • Использование ES. В некоторых операциях, таких как строковые операции, ES может быть использован для указания на сегмент, куда будут копироваться данные.

Регистры FS и GS (additional segment registers)

  • Назначение FS и GS. Регистр FS и GS – это дополнительные сегментные регистры, которые были добавлены для расширения возможностей управления памятью. Они могут быть использованы для различных целей, в зависимости от операционной системы и приложения.
  • Использование FS. В Windows, FS часто используется для доступа к информации о потоке, такой как указатель на блок информации потока (Thread Information Block, TIB).
  • Использование GS. В 64-битных системах, GS используется для доступа к различным системным структурам, таким как указатель на структуру данных, связанную с текущим процессором.
  • Сегментные регистры в архитектуре x86 предоставляют механизм для организации и управления памятью, обеспечивая разделение данных, кода и стека. Они играют важную роль в обеспечении безопасности и эффективности выполнения программ.
  • В современных системах, особенно в 64-битных, сегментная модель управления памятью несколько упрощена, но сегментные регистры все еще играют важную роль в обеспечении работы операционной системы и приложений.

Регистры управления и состояния: EIP и EFLAGS в архитектуре x86

В архитектуре x86, помимо регистров общего назначения и сегментных регистров, существует категория регистров, которые играют критическую роль в управлении выполнением программ и отслеживании состояния процессора. Два из наиболее важных регистров в этой категории — это EIP (extended instruction pointer) и EFLAGS (extended flags register). Давайте подробнее рассмотрим их функции и назначение.

Регистр EIP (extended instruction pointer)

Назначение EIP

Регистр EIP, также известный как указатель команд (instruction pointer), указывает на адрес следующей инструкции, которая будет выполнена процессором. Этот регистр играет ключевую роль в управлении потоком выполнения программы.

Функции EIP

  • Управление выполнением. Регистр EIP автоматически увеличивается после выполнения каждой инструкции, чтобы указывать на следующую инструкцию в последовательности.
  • Управление переходами. Когда процессор встречает инструкции перехода (например, JMP, CALL, RET), значение EIP изменяется, чтобы указать на новый адрес, с которого должно продолжиться выполнение.
  • Обработка исключений и прерываний. При возникновении исключений или прерываний, EIP сохраняется в стеке, чтобы после обработки события программа могла вернуться к выполнению с правильной точки.

Особенности EIP

  • Регистр EIP не может быть напрямую изменен программным кодом. Изменение EIP осуществляется через инструкции управления потоком, такие как JMP, CALL, RET и обработчики прерываний.

Регистр EFLAGS (extended flags register)

Назначение EFLAGS

Регистр EFLAGS – это регистр, который содержит набор флагов, отражающих текущее состояние процессора и результаты выполнения последней инструкции. Эти флаги используются для управления условными операциями и отслеживания состояния процессора.

Регистры EIP и EFLAGS являются неотъемлемой частью архитектуры x86, обеспечивая управление выполнением программ и отслеживание состояния процессора. Понимание их функций и взаимодействия с другими регистрами и инструкциями необходимо для разработки эффективного и надежного программного обеспечения. Эти регистры лежат в основе механизмов управления потоком выполнения и обработки исключений, что делает их критически важными для работы любой программы на уровне процессора.

Компиляция и дизассемблированние программ в примерах

Компиляция исходного кода примеров

Исходный код программ будет написан на языке Си. Далее код будет компилироваться с помощью GCC.

В мире разработки программного обеспечения компиляция — это ключевой процесс, который превращает исходный код, написанный на языке высокого уровня, таком как C или C++, в исполняемый машинный код, который может быть выполнен компьютером. Одним из самых популярных инструментов для этой цели является GCC.

Что такое GCC?

GCC (GNU Compiler Collection) — это набор компиляторов с открытым исходным кодом, который поддерживает множество языков программирования, включая C, C++, Objective-C, Fortran, Ada и другие. Изначально разработанный для проекта GNU, GCC стал стандартным компилятором для многих операционных систем на базе Unix, таких как Linux, и широко используется в разработке программного обеспечения по всему миру.

GCC был создан в 1987 году Ричардом Столлманом как часть проекта GNU, с целью предоставить свободный компилятор для языка C. Со временем он расширился, чтобы поддерживать другие языки и архитектуры.

Будучи частью проекта GNU, GCC распространяется под лицензией GPL (GNU General Public License), что позволяет пользователям свободно использовать, изменять и распространять его.

Основные возможности GCC

Поддержка множества языков. GCC поддерживает не только C и C++, но и другие языки, такие как Objective-C, Fortran, Ada и Go.

Кросс-компиляция. GCC позволяет компилировать программы для различных архитектур процессоров, что делает его полезным для разработки встроенных систем и других специализированных приложений.

Примечание

В наших примерах мы будем компилировать программы под архитектуру x86 для упрощения последующего анализа.

Оптимизация. GCC предлагает широкий спектр опций оптимизации, которые позволяют улучшить производительность и эффективность скомпилированного кода.

Примечание

В наших примерах будем стараться отключать оптимизацию и включать отладочную информацию для упрощения анализа.

Расширяемость. Благодаря модульной архитектуре, GCC может быть расширен для поддержки новых языков и архитектур.

Как выполняется компиляция программы с помощью GCC

Компиляция — это процесс преобразования исходного кода в исполняемый файл. Далее посмотрим основные этапы, которые выполняются при компиляции программы с использованием GCC.

1. Препроцессинг
  • На этом этапе выполняется обработка директив препроцессора, таких как #include и #define. GCC использует препроцессор cpp для этого.
  • Команда: gcc -E source.c -o source.i
2. Компиляция в ассемблер
  • Преобразует предварительно обработанный код в ассемблерный код.
  • Команда: gcc -S source.i -o source.s
3. Ассемблирование
  • Преобразует ассемблерный код в объектный файл, содержащий машинный код.
  • Команда: gcc -c source.s -o source.o
4. Компоновка
  • Связывает объектные файлы и библиотеки для создания конечного исполняемого файла.
  • Команда: gcc source.o -o executable

Простая компиляция одной командой

Для простых программ можно использовать одну команду, которая выполнит все этапы:

bash
gcc source.c -o executable 

Пример компиляции

Предположим, у вас есть файл hello.c со следующим содержимым:

c
#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
} 

Чтобы скомпилировать этот файл в исполняемый файл hello, выполните команду:

bash
gcc -m32 -O0 -g -o hello hello.c 

Мы будем использовать следующие дополнительные флаги:

  • -m32: компилирует 32-битную версию программы;
  • -O0: отключает оптимизацию;
  • -g: включает отладочную информацию

Затем вы можете запустить программу:

bash
hello.exe 

Она выведет строку:

Hello, World! 

Влияние компилятора на дальнейшее дизассемблирование

Компилятор играет критическую роль в процессе преобразования исходного кода в машинный код, и его настройки и особенности могут существенно влиять на то, как этот код будет выглядеть в дизассемблированном виде.

Далее познакомимся с ключевыми моментами, которые могут помешать нашему взаимопониманию: дизассемблированный вами код может отличаться от представленных примеров.

1. Оптимизация кода

Компиляторы, такие как GCC, предлагают различные уровни оптимизации (например, -O0, -O1, -O2, -O3, -Os), которые изменяют способ генерации машинного кода.

Без оптимизации (-O0)
  • Код генерируется так, чтобы он был максимально похож на исходный код, что облегчает отладку.
  • Переменные и инструкции сохраняются в том порядке, в котором они указаны в исходном коде.
  • Дизассемблированный код будет более читаемым и понятным, так как он будет более линеен и предсказуем.
Дополнение

По этой причине мы используем флаг -O0 для компиляции программ. Запутанный (оптимизированный) код только усложнит задачу изучения основных регистров.

С оптимизацией (-O1, -O2, -O3)
  • Компилятор переупорядочивает инструкции, удаляет неиспользуемый код и применяет различные техники для повышения производительности.
  • Это может привести к более сложному и запутанному дизассемблированному коду, так как инструкции могут быть переупорядочены, а переменные могут быть оптимизированы или удалены.
  • Например, переменные могут быть размещены в регистрах, а не в памяти, что усложняет отслеживание их значений.

2. Генерация имен функций и переменных

Компиляторы могут изменять имена функций и переменных в объектном коде, особенно в C++, где используется так называемый name mangling.

C++ name mangling
  • Имена функций и переменных изменяются для поддержки перегрузки и пространств имен.
  • Это делает дизассемблированный код менее читаемым, если не использовать инструменты для декодирования mangled names, такие как c++filt.

3. Встраивание функций (Inlining)

Компиляторы могут встраивать функции, что означает, что код функции вставляется непосредственно в место вызова, а не вызывается как отдельная функция.

Влияние на дизассемблирование
  • Встраивание функций может сделать дизассемблированный код более сложным, так как логика функции будет распределена по разным местам.
  • Это может затруднить понимание структуры программы и потока выполнения.

4. Удаление неиспользуемого кода (Dead code elimination)

Компиляторы удаляют код, который никогда не будет выполнен, что может привести к отсутствию определенных функций или блоков в дизассемблированном коде.

Влияние на дизассемблирование
  • Это может сделать дизассемблированный код менее полным, так как некоторые части исходного кода могут отсутствовать.

5. Выравнивание и размещение кода

Компиляторы могут изменять порядок размещения функций и данных в памяти, что влияет на адреса и переходы в дизассемблированном коде.

Влияние на Дизассемблирование
  • Это может привести к нелинейному потоку выполнения, что усложняет анализ.

6. Вставка проверок и обработка ошибок

Компиляторы могут добавлять проверки и обработку ошибок, которые не были явным образом указаны в исходном коде.

Влияние на дизассемблирование

  • Это может добавить дополнительные инструкции и усложнить дизассемблированный код.

Компилятор и его настройки оказывают значительное влияние на то, как программа будет представлена в дизассемблированном виде. Понимание этих влияний важно для реверс-инжиниринга и анализа программного обеспечения, так как это позволяет более точно интерпретировать и понимать машинный код.

Дизассемблирование программ

Дизассемблирование — это процесс преобразования машинного кода или объектного файла обратно в ассемблерный код, что позволяет анализировать и понимать, как работает программа на низком уровне. Одним из наиболее популярных инструментов для этой задачи в Unix-подобных системах, таких как Linux, является **objdump**, который является частью набора инструментов GNU Binutils.

Что такое objdump?

Инструмент objdump — это утилита командной строки, которая отображает информацию о файлах объектного формата, таких как исполняемые файлы, объектные файлы и библиотеки. Она может показывать различные аспекты файла, включая символы, секции, архитектурные детали и, что наиболее важно для нас, дизассемблированный ассемблерный код.

Основные функции objdump

  • Дизассемблирование. Преобразование машинного кода в ассемблерный код.
  • Отображение секций. Показ информации о секциях в объектном файле, таких как .text, .data, .bss и другие.
  • Отображение символов. Вывод списка символов (функций и переменных) и их адресов.
  • Отображение заголовков. Информация о заголовках файла, таких как архитектура, точка входа и т.д.
  • Другие Функции. Включая отображение информации о перемещениях, динамических символах и т.д.

Процесс дизассемблирования

Для дизассемблирования примеров будем использовать команду следующего вида:

bash
objdump -M intel -d example 
  • -M intel – устанавливает синтаксис Intel для ассемблера;
  • -d – дизассемблирует секции кода (секция .text).

1. Общие регистры (general-purpose registers) на практике

1.1. Регистр EAX (accumulator register)

Кратко: назначение регистра EAX

-  Основной регистр для арифметических и логических операций.

-  Часто используется для возврата значений из функций.

-  В 32-битной архитектуре: EAX (32 бита), AX (16 бит), AH и AL (по 8 бит каждый).

Пример 1 (реальный). Исходный код на C

Давайте рассмотрим пример программы на языке C, которая использует регистр EAX, и посмотрим, как этот код выглядит в дизассемблированном виде.

Создайте файл example.c со следующим содержимым:

c
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 5;
    int y = 10;
    int sum = add(x, y);
    printf("Sum is %d\n", sum);
    return 0;
} 
Фрагмент дизассемблированной программы формата pei-i386 (функции add и main)
assembly
004015c0 <_add>:
  4015c0:	55                   	push   ebp
  4015c1:	89 e5                	mov    ebp,esp
  4015c3:	8b 55 08             	mov    edx,DWORD PTR [ebp+0x8]
  4015c6:	8b 45 0c             	mov    eax,DWORD PTR [ebp+0xc]
  4015c9:	01 d0                	add    eax,edx
  4015cb:	5d                   	pop    ebp
  4015cc:	c3                   	ret    

004015cd <_main>:
  4015cd:	55                   	push   ebp
  4015ce:	89 e5                	mov    ebp,esp
  4015d0:	83 e4 f0             	and    esp,0xfffffff0
  4015d3:	83 ec 20             	sub    esp,0x20
  4015d6:	e8 d5 00 00 00       	call   4016b0 <___main>
  4015db:	c7 44 24 1c 05 00 00 	mov    DWORD PTR [esp+0x1c],0x5
  4015e2:	00 
  4015e3:	c7 44 24 18 0a 00 00 	mov    DWORD PTR [esp+0x18],0xa
  4015ea:	00 
  4015eb:	8b 44 24 18          	mov    eax,DWORD PTR [esp+0x18]
  4015ef:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  4015f3:	8b 44 24 1c          	mov    eax,DWORD PTR [esp+0x1c]
  4015f7:	89 04 24             	mov    DWORD PTR [esp],eax
  4015fa:	e8 c1 ff ff ff       	call   4015c0 <_add>
  4015ff:	89 44 24 14          	mov    DWORD PTR [esp+0x14],eax
  401603:	8b 44 24 14          	mov    eax,DWORD PTR [esp+0x14]
  401607:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  40160b:	c7 04 24 44 40 40 00 	mov    DWORD PTR [esp],0x404044
  401612:	e8 c5 0f 00 00       	call   4025dc <_printf>
  401617:	b8 00 00 00 00       	mov    eax,0x0
  40161c:	c9                   	leave  
Примечание

В разборе первого примера кода мы пройдемся по каждой строчке. В дальнейшем будем пропускать разобранные инструкции.

Объяснение дизассемблированного кода функции _add (004015c0)

Сохраняет текущее значение регистра EBP в стеке. Это стандартное начало функции, которое сохраняет указатель базы стека для вызывающей функции:

4015c0:  55   push   ebp 

Устанавливает EBP (указатель базы стека) равным текущему значению ESP (указатель стека). Это создает новый стековый фрейм для функции:

4015c1: 89 e5    mov    ebp,esp 

Загружает значение из памяти по адресу ebp + 0x8 в регистр EDX. В контексте вызова функции, это обычно первый аргумент функции:

4015c3: 8b 55 08   mov   edx,DWORD PTR [ebp+0x8] 
Примечание

-  Операция mov перемещает информацию из одного места в другое.

-  DWORD PTR – оператор размера операнда, который указывает, что операция должна быть выполнена с двойным словом (DWORD). В контексте 32-битной архитектуры x86, слово (WORD) обычно означает 16 бит, а двойное слово (DWORD) — 32 бита. Оператор ptr в ассемблере указывает, что следующий за ним операнд должен быть интерпретирован как указатель на данные определенного размера.

-  Выражение [ebp+0x8] указывает на адрес в памяти, который находится на 8 байт выше текущего значения EBP. В контексте вызова функции, это обычно соответствует первому аргументу функции.

Загружает значение из памяти по адресу ebp + 0xc в регистр EAX. Это обычно второй аргумент функции:

4015c6: 8b 45 0c   mov   eax,DWORD PTR [ebp+0xc] 

Добавляет значение из EDX к значению в EAX и сохраняет результат в EAX. Это выполняет операцию сложения двух аргументов:

4015c9: 01 d0    add    eax,edx 

Восстанавливает предыдущее значение EBP из стека. Это завершает текущий стековый фрейм и восстанавливает состояние вызывающей функции:

4015cb: 5d    pop    ebp 

Возвращает управление вызывающей функции. Это завершает выполнение текущей функции:

4015cc: c3    ret 
Объяснение дизассемблированного кода функции _main (004015cd)

Последовательность 55 89 E5 в дизассемблированном коде является стандартным началом функции в ассемблере x86, и она служит для установки нового стекового фрейма и сохранения состояния вызывающей функции. Это важная часть соглашений о вызовах, которые обеспечивают корректное взаимодействие между функциями в программе:

4015cd: 55       push   ebp
4015ce: 89 e5    mov    ebp,esp 
Примечание

К последовательности 55 89 E5 постараемся больше не возвращаться.

Выравнивает ESP по 16-байтовой границе. Это часто делается для оптимизации работы с памятью:

4015d0: 83 e4 f0   and   esp,0xfffffff0 

Выделяет 32 байта (0x20) в стеке для локальных переменных:

4015d3: 83 ec 20   sub    esp,0x20 

Вызывает функцию ___main, которая, вероятно, выполняет инициализацию среды выполнения C/C++:

4015d6: e8 d5 00 00 00    call   4016b0 <___main> 

Помещает значение 0x5 в память по адресу esp + 0x1c. Это инициализация локальной переменной:

4015db: c7 44 24 1c 05 00 00  mov    DWORD PTR [esp+0x1c],0x5 

Просто байт-заполнитель, который завершает предыдущую инструкцию:

4015e2:   00 
Примечание

Байт-заполнитель в дизассемблированном коде — это однобайтовая инструкция, которая не выполняет никаких операций и используется для выравнивания кода или заполнения пространства. В ассемблере x86 такой байт обычно представлен инструкцией nop (no operation) или просто как байт, который не изменяет состояние программы. Давайте разберем детальнее назначение этой, на первый взгляд бесполезной, инструкции:

-  Выравнивание кода: улучшение производительности процессора за счет выравнивания инструкций по определенным границам памяти. Некоторые процессоры более эффективно выполняют инструкции, когда они начинаются с определенных адресов (например, по границе 4 или 16 байт). Выравнивание кода помогает избежать ситуаций, когда инструкции пересекают эти границы, что может вызвать задержки в выполнении.

-  Заполнение пространства: заполнение пространства для достижения определенной длины или размера секции. В некоторых случаях, секции в объектных файлах должны иметь определенный размер, и компилятор может добавлять байты-заполнители, чтобы достичь этого размера. При создании патчей или модификаций кода, байты-заполнители могут использоваться для замены существующих инструкций без изменения общего размера кода.

-  Предсказание переходов: в некоторых случаях, выравнивание кода может помочь процессору лучше предсказывать переходы, что может улучшить производительность.

Помещает значение 0xA (10 в десятичной системе) в память по адресу esp + 0x18. Это инициализация другой локальной переменной:

4015e3: c7 44 24 18 0a 00 00  mov    DWORD PTR [esp+0x18],0xa 

Загружает значение из памяти по адресу esp + 0x18 в регистр EAX. Это значение равно 0xA:

4015eb: 8b 44 24 18    mov    eax,DWORD PTR [esp+0x18] 

Помещает значение из EAX в память по адресу esp + 0x4. Это подготовка второго аргумента для последующего вызова функции:

4015ef: 89 44 24 04    mov    DWORD PTR [esp+0x4],eax 
Соглашение о вызовах (Calling convention)

Почему значение из [esp+0x18] было перемещено в регистр EAX, а затем из этого регистра в память по адресу [esp+0x4]?

В архитектуре x86, особенно в 32-битных системах, часто используется соглашение о вызовах cdecl, где аргументы функции передаются через стек: перед вызовом функции, аргументы помещаются в стек в обратном порядке (справа налево). Это означает, что последний аргумент функции помещается в стек первым.

Влияние оптимизации на использование регистра EAX

Компилятор мог оптимизировать этот код и сразу переместить значение 0xA во второй аргумент функции, находящийся по адресу [esp+0x4].

Так как мы отключили оптимизацию кода при компиляции, дизассемблированный код максимально повторяет исходный код программы на Си. То есть мы сначала присвоили переменной y число 10, затем переменную y передали в качестве аргумента функции add – то же самое, если бы мы сразу передали число 10 в функцию add.

Загружает значение из памяти по адресу esp + 0x1c в EAX. Это значение равно 0x5:

4015f3: 8b 44 24 1c    mov    eax,DWORD PTR [esp+0x1c] 

Помещает значение из EAX в память по адресу ESP. Это подготовка первого аргумента для вызова функции:

4015f7: 89 04 24    mov    DWORD PTR [esp],eax 

Вызывает функцию _add, передавая два аргумента, которые были подготовлены ранее (первый: 4015f7, второй: 4015ef):

4015fa: e8 c1 ff ff ff    call   4015c0 <_add> 
Откуда в названии функции появилось нижнее подчеркивание?

Некоторые компиляторы и компоновщики могут добавлять префиксы или суффиксы к именам функций в объектном коде.

Намного интереснее обстоят дела, если скомпилировать этот исходный Си-код как С++. В игру вступает name mangling – (искажение имен) для поддержки перегрузки функций и других возможностей. Функция add имела бы примерно следующие, на первый взгляд, непонятное название: __Z3addii

Есть специальные инструменты для перевода таких имён на человеческий язык. На самом деле мы и сами легко сможем их перевести:

-  Первое нижнее подчеркивание добавил компилятор.

-  _Z — это префикс, указывающий на начало mangled name.

-  3 — длина имени функции (add имеет 3 символа).

-  add — имя функции.

-  i — тип первого параметра (int).

-  i — тип второго параметра (int).

Сохраняет возвращаемое значение из EAX в память по адресу esp + 0x14 (выделенная память для переменной sum):

4015ff: 89 44 24 14    mov   DWORD PTR [esp+0x14],eax 
Откуда результат выполнения функции в регистре EAX?

У вас мог возникнуть вопрос, каким образом результат выполнения функции оказался в регистре EAX, если мы явно не возвращали его из функции. Разберем этот момент.

В архитектуре x86 регистр EAX используется для возврата значений из функций по определенным соглашениям о вызовах (calling conventions). Это не происходит по умолчанию в смысле аппаратного обеспечения, а является частью соглашений, которые разработчики и компиляторы используют для обеспечения совместимости и предсказуемости в программах.

Соглашения о вызовах определяют, как функции передают аргументы, как используются регистры и как осуществляется возврат из функции. В архитектуре x86 существует несколько соглашений о вызовах, и одно из них — это cdecl (C declaration), которое широко используется в языках программирования, таких как C и C++.

В соглашении о вызовах cdecl, возвращаемое значение функции передается через регистр EAX. Это означает, что если функция возвращает значение, оно должно быть помещено в EAX перед выполнением инструкции ret.

Загружает значение из памяти по адресу esp + 0x14 в EAX. Это возвращаемое значение из функции _add:

401603: 8b 44 24 14    mov   eax,DWORD PTR [esp+0x14] 

Помещает значение из EAX в память по адресу esp + 0x4. Это подготовка аргумента для вызова функции printf:

401607: 89 44 24 04    mov    DWORD PTR [esp+0x4],eax 

Помещает адрес строки в память по адресу ESP. Это подготовка первого аргумента для вызова printf:

40160b: c7 04 24 44 40 40 00  mov    DWORD PTR [esp],0x404044 

Вызывает функцию printf, передавая строку и аргумент:

401612: e8 c5 0f 00 00    call   4025dc <_printf> 

Устанавливает возвращаемое значение функции main в 0, что указывает на успешное завершение программы:

401617: b8 00 00 00 00     mov    eax,0x0 

Восстанавливает стековый фрейм вызывающей функции, эквивалентно выполнению mov esp, ebp и pop ebp:

40161c: c9    leave 
Объяснение использования EAX
  • Регистр EAX используется для хранения возвращаемого значения функции add.
  • В функции add, регистр EAX загружается значением второго аргумента (b), затем к нему добавляется значение первого аргумента (a), и результат остается в EAX.
  • В функции main, EAX используется для хранения промежуточных результатов, таких как значения переменных x и y.
  • После вызова функции add, результат сохраняется в EAX и затем передается в функцию printf.
  • В конце функции main, регистр EAX устанавливается в 0 для возврата значения 0.

Этот пример демонстрирует, как регистр EAX используется для хранения возвращаемых значений и промежуточных результатов в ассемблерном коде.

Фрагмент дизассемблированной программы формата elf32-i386 (функции add и main)

Для примера рассмотрим дизассемблированный код программы формата ELF32:

00001189 <__x86.get_pc_thunk.dx>:
    1189:	8b 14 24             	mov    edx,DWORD PTR [esp]
    118c:	c3                   	ret

0000118d <add>:
    118d:	55                   	push   ebp
    118e:	89 e5                	mov    ebp,esp
    1190:	e8 6f 00 00 00       	call   1204 <__x86.get_pc_thunk.ax>
    1195:	05 5f 2e 00 00       	add    eax,0x2e5f
    119a:	8b 55 08             	mov    edx,DWORD PTR [ebp+0x8]
    119d:	8b 45 0c             	mov    eax,DWORD PTR [ebp+0xc]
    11a0:	01 d0                	add    eax,edx
    11a2:	5d                   	pop    ebp
    11a3:	c3                   	ret

000011a4 <main>:
    11a4:	8d 4c 24 04          	lea    ecx,[esp+0x4]
    11a8:	83 e4 f0             	and    esp,0xfffffff0
    11ab:	ff 71 fc             	push   DWORD PTR [ecx-0x4]
    11ae:	55                   	push   ebp
    11af:	89 e5                	mov    ebp,esp
    11b1:	53                   	push   ebx
    11b2:	51                   	push   ecx
    11b3:	83 ec 10             	sub    esp,0x10
    11b6:	e8 d5 fe ff ff       	call   1090 <__x86.get_pc_thunk.bx>
    11bb:	81 c3 39 2e 00 00    	add    ebx,0x2e39
    11c1:	c7 45 f4 05 00 00 00 	mov    DWORD PTR [ebp-0xc],0x5
    11c8:	c7 45 f0 0a 00 00 00 	mov    DWORD PTR [ebp-0x10],0xa
    11cf:	ff 75 f0             	push   DWORD PTR [ebp-0x10]
    11d2:	ff 75 f4             	push   DWORD PTR [ebp-0xc]
    11d5:	e8 b3 ff ff ff       	call   118d <add>
    11da:	83 c4 08             	add    esp,0x8
    11dd:	89 45 ec             	mov    DWORD PTR [ebp-0x14],eax
    11e0:	83 ec 08             	sub    esp,0x8
    11e3:	ff 75 ec             	push   DWORD PTR [ebp-0x14]
    11e6:	8d 83 14 e0 ff ff    	lea    eax,[ebx-0x1fec]
    11ec:	50                   	push   eax
    11ed:	e8 4e fe ff ff       	call   1040 <printf@plt>
    11f2:	83 c4 10             	add    esp,0x10
    11f5:	b8 00 00 00 00       	mov    eax,0x0
    11fa:	8d 65 f8             	lea    esp,[ebp-0x8]
    11fd:	59                   	pop    ecx
    11fe:	5b                   	pop    ebx
    11ff:	5d                   	pop    ebp
    1200:	8d 61 fc             	lea    esp,[ecx-0x4]
    1203:	c3                   	ret

00001204 <__x86.get_pc_thunk.ax>:
    1204:	8b 04 24             	mov    eax,DWORD PTR [esp]
    1207:	c3                   	ret 

Дизассемблированный код программ формата PE (предыдущий пример) и ELF32 имеет множество сходств (они были собраны из одного исходного кода), но в тоже время сильно отличаются.

Почему так много операций push?

Использование push и mov в ELF32 и PE форматах обусловлено различиями в соглашениях о вызовах, архитектурных особенностях и подходах к оптимизации. В ELF32, где часто используются соглашения о вызовах, основанные на стеке, инструкции push встречаются чаще.

1.2. Регистр EBX (base register)

Кратко: назначение регистра EBX

-  Использование регистра EBX как указателя на данные в сегменте данных.

-  Регистр EBX может служить базовым регистром для адресации.

Немного теории: использование регистра EBX как указателя на данные в сегменте данных

Когда говорят, что регистр EBX используется как указатель на данные в сегменте данных, это означает, что EBX содержит адрес, который указывает на область памяти в сегменте данных программы.

Сегмент данных (Data segment)

Сегмент данных — это область памяти, выделенная для хранения глобальных и статических переменных программы. Это включает в себя как инициализированные, так и неинициализированные данные. В архитектуре x86, сегмент данных обычно обозначается как .data и .bss в объектных файлах.

Типы данных в сегменте данных:

  • Инициализированные данные. Переменные, которым присвоены начальные значения при компиляции, например, int x = 10;.
  • Неинициализированные данные. Переменные, которые не имеют начальных значений, например, static int y;.
Регистр EBX как указатель

Назначение EBX. Регистр EBX — это один из регистров общего назначения, но он часто используется для хранения адресов, особенно в контексте адресации данных в памяти.

Когда EBX используется как указатель, он содержит адрес, который указывает на определенную ячейку памяти.

Пример 1. Доступ к глобальной переменной
assembly
.data
	myGlobalVar dd 0x12345678  ; Глобальная переменная в сегменте данных

.text
     mov ebx, offset myGlobalVar  ; Загружаем адрес myGlobalVar в ebx
     mov eax, [ebx]               ; Загружаем значение по адресу ebx в eax 

Объяснение:

  • offset myGlobalVar загружает адрес глобальной переменной myGlobalVar в EBX.
  • mov eax, [ebx] загружает значение по адресу, хранящемуся в EBX, в регистр EAX.
Пример 2. Работа с массивом
assembly
.data
     myArray dd 10, 20, 30, 40, 50  ; Массив из 5 элементов

.text
     mov ebx, offset myArray        ; Загружаем адрес начала массива в ebx
     mov eax, [ebx + 4]             ; Загружаем второй элемент массива (20) в eax
     add eax, [ebx + 8]             ; Добавляем третий элемент (30) к eax 

Объяснение:

  • EBX указывает на начало массива myArray.
  • mov eax, [ebx + 4] загружает значение второго элемента массива в EAX.
  • add eax, [ebx + 8] добавляет значение третьего элемента массива к EAX.
Пример 3. Работа с указателями и арифметикой
assembly
.data
     myVar dd 100  ; Переменная в сегменте данных

.text
     mov ebx, offset myVar          ; Загружаем адрес myVar в ebx
     mov eax, [ebx]                 ; Загружаем значение myVar в eax
     add eax, 50                    ; Добавляем 50 к значению
     mov [ebx], eax                 ; Сохраняем результат обратно в myVar 

Объяснение:

  • Регистр EBX указывает на myVar.
  • Значение myVar загружается в EAX, увеличивается на 50 и затем сохраняется обратно в myVar.

Немного теории: регистр EBX может служить базовым регистром для адресации

Когда говорят, что регистр EBX может служить базовым регистром для адресации, это означает, что EBX используется для хранения базового адреса, относительно которого вычисляются адреса других данных в памяти. Это является частью режима адресации в архитектуре x86, где регистры могут использоваться для формирования эффективного адреса данных. Давайте разберем это подробно и рассмотрим примеры.

Базовый регистр для адресации

Базовый регистр — это регистр, который содержит адрес, используемый как отправная точка для вычисления адреса других данных. Относительно этого адреса вычисляются смещения для доступа к различным элементам данных.

Роль EBX как базового регистра:

  • Регистр EBX может использоваться для хранения адреса начала структуры данных, массива или любой другой области памяти.
  • Относительно этого адреса можно вычислять смещения для доступа к конкретным элементам данных.
Пример 1. Доступ к элементам массива
assembly
.data
   myArray dd 10, 20, 30, 40, 50  ; Массив из 5 элементов

.text
   mov ebx, offset myArray        ; Загружаем адрес начала массива в ebx
   mov eax, [ebx + 4]             ; Загружаем второй элемент массива (20) в eax
   add eax, [ebx + 8]             ; Добавляем третий элемент (30) к eax 

Объяснение:

  • Регистр EBX содержит адрес начала массива myArray.
  • ebx + 4 указывает на второй элемент массива (20), который загружается в EAX.
  • ebx + 8 указывает на третий элемент массива (30), который добавляется к значению в EAX.
Пример 2. Работа со структурами
assembly
.data
   myStruct:
     .dw 100          ; Первое поле структуры
     .dw 200          ; Второе поле структуры

.text
   mov ebx, offset myStruct      ; Загружаем адрес структуры в ebx
   mov ax, [ebx]                 ; Загружаем первое поле (100) в ax
   mov bx, [ebx + 2]             ; Загружаем второе поле (200) в bx 

Объяснение:

  • Регистр EBX содержит адрес структуры myStruct.
  • Регистр EBX используется для доступа к полям структуры: первое поле по адресу EBX, второе поле по адресу ebx + 2.
Пример 3. Индексная адресация
assembly
.data
   myArray dd 5, 15, 25, 35, 45  ; Массив из 5 элементов

.text
   mov ebx, offset myArray        ; Загружаем адрес начала массива в ebx
   mov ecx, 2                     ; Устанавливаем индекс 2
   mov eax, [ebx + ecx*4]        ; Загружаем третий элемент (25) в eax 

Объяснение:

  • Регистр EBX содержит адрес начала массива.
  • Регистр ECX используется как индекс, и ebx + ecx*4 вычисляет адрес третьего элемента массива (поскольку каждый элемент занимает 4 байта).
  • Значение третьего элемента (25) загружается в EAX.

Пример 1 (реальный). Исходный код на C для демонстрации использования регистра ebx в качестве дополнительного регистра для хранения

Рассмотрим пример программы на языке C, которая использует регистр EBX, и посмотрим, как этот код выглядит в дизассемблированном виде.

Создайте файл example.c со следующим содержимым:

c
#include <stdio.h>

int calculate(int a, int b) {
    int temp = a * 2;
    return temp + b;
}

int main() {
    int x = 5;
    int y = 10;
    int result = calculate(x, y);
    printf("Result is %d\n", result);
    return 0;
} 
Фрагмент дизассемблированной программы (функции calculate и main)
assembly
004015c0 <_calculate>:
  4015c0:	55                   	push   ebp
  4015c1:	89 e5                	mov    ebp,esp
  4015c3:	83 ec 10             	sub    esp,0x10
  4015c6:	8b 45 08             	mov    eax,DWORD PTR [ebp+0x8]
  4015c9:	01 c0                	add    eax,eax
  4015cb:	89 45 fc             	mov    DWORD PTR [ebp-0x4],eax
  4015ce:	8b 55 fc             	mov    ebx,DWORD PTR [ebp-0x4]
  4015d1:	8b 45 0c             	mov    eax,DWORD PTR [ebp+0xc]
  4015d4:	01 d0                	add    eax,ebx
  4015d6:	c9                   	leave  
  4015d7:	c3                   	ret    

004015d8 <_main>:
  4015d8:	55                   	push   ebp
  4015d9:	89 e5                	mov    ebp,esp
  4015db:	83 e4 f0             	and    esp,0xfffffff0
  4015de:	83 ec 20             	sub    esp,0x20
  4015e1:	e8 da 00 00 00       	call   4016c0 <___main>
  4015e6:	c7 44 24 1c 05 00 00 	mov    DWORD PTR [esp+0x1c],0x5
  4015ed:	00 
  4015ee:	c7 44 24 18 0a 00 00 	mov    DWORD PTR [esp+0x18],0xa
  4015f5:	00 
  4015f6:	8b 44 24 18          	mov    eax,DWORD PTR [esp+0x18]
  4015fa:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  4015fe:	8b 44 24 1c          	mov    eax,DWORD PTR [esp+0x1c]
  401602:	89 04 24             	mov    DWORD PTR [esp],eax
  401605:	e8 b6 ff ff ff       	call   4015c0 <_calculate>
  40160a:	89 44 24 14          	mov    DWORD PTR [esp+0x14],eax
  40160e:	8b 44 24 14          	mov    eax,DWORD PTR [esp+0x14]
  401612:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  401616:	c7 04 24 44 40 40 00 	mov    DWORD PTR [esp],0x404044
  40161d:	e8 ca 0f 00 00       	call   4025ec <_printf>
  401622:	b8 00 00 00 00       	mov    eax,0x0
  401627:	c9                   	leave  
  401628:	c3                   	ret    
Почему мной был заменён регистр EDX на EBX

В дизассемблированном коде функции _calculate я заменил регистр EDX на EBX для демонстрации использования этого регистра. Это никак не повлияет на программу, так как регистр EBX в ней не используется.

Почему компилятор использует регистр EDX? Поскольку EDX является временным регистром и не требует сохранения, компилятор может предпочесть его для хранения промежуточных значений или адресов, особенно если это позволяет избежать дополнительных операций сохранения и восстановления.

Компиляторы стремятся использовать регистры наиболее эффективным образом. Если EBX не является оптимальным выбором для данного фрагмента кода (например, из-за необходимости сохранения и восстановления его значения), компилятор может выбрать другой регистр, такой как EDX.

Компиляторы анализируют поток данных и зависимости, чтобы определить, какие регистры лучше всего использовать для минимизации зависимостей и повышения параллелизма выполнения инструкций.

Объяснение дизассемблированного кода функции _calculate (004015c0)

Выделяет 0x10 (16) байт в стеке для локальных переменных:

4015c3: 83 ec 10    sub    esp,0x10 

Загружает первый аргумент a функции в регистр EAX. Это значение, которое нужно обработать:

4015c6: 8b 45 08    mov    eax,DWORD PTR [ebp+0x8] 

Удваивает значение (a * 2) в EAX (умножает на 2):

4015c9: 01 c0    add    eax,eax 

Сохраняет результат в локальной переменной temp по адресу ebp-0x4:

4015cb: 89 45 fc    mov    DWORD PTR [ebp-0x4],eax 

Загружает значение из локальной переменной temp в регистр EBX:

4015ce: 8b 55 fc    mov    ebx,DWORD PTR [ebp-0x4] 

Загружает второй аргумент b функции в регистр EAX:

4015d1: 8b 45 0c    mov    eax,DWORD PTR [ebp+0xc] 

Добавляет значение из EBX к EAX, (temp + b) суммируя два значения:

4015d4: 01 d0    add    eax,ebx 

Значение сохраняется в регистре EAX и далее возвращается из функции _calculate. Далее все аналогичного предыдущему примеру.

Объяснение использования EBX
  • Регистр EBX используется для хранения промежуточного результата вычислений.
  • В функции calculate регистр EAX сначала загружается значением первого аргумента (a), затем умножается на 2. Результат сохраняется в EBX.
  • Затем к EBX добавляется второй аргумент (b), и результат сохраняется в EAX для возврата.
  • В функции main регистр EBX не используется явно, так как основная работа выполняется с другими регистрами.

Этот пример демонстрирует, как регистр EBX используется для хранения промежуточных результатов в ассемблерном коде.

Пример 2 (выдуманный). Исходный код на C для демонстрации использования регистра EBX в качестве дополнительного регистра для хранения

Создадим простую программу на языке C, которая использует регистр EBX в соответствии с его назначением — как базовый регистр для адресации данных в памяти. Затем мы рассмотрим дизассемблированный вид этой программы, чтобы увидеть, как EBX используется в машинном коде.

c
#include <stdio.h>

// Глобальный массив для демонстрации
int myArray[5] = {10, 20, 30, 40, 50};

int main() {
    int sum = 0;
    int i;

    // Используем ebx для адресации элементов массива
    for (i = 0; i < 5; i++) {
        sum += myArray[i];
    }

    printf("Sum: %d\n", sum);
    return 0;
} 
Объяснение исходного кода программы
  • Массив myArray: это глобальный массив, который инициализируется пятью целыми числами.
  • Переменная sum: используется для хранения суммы элементов массива.
  • Цикл for: проходит по всем элементам массива, добавляя каждый элемент к sum.
  • Функция printf: выводит сумму элементов массива.
Фрагмент дизассемблированной программы (цикл for)

Для демонстрации использования EBX как базового регистра, мы рассмотрим часть дизассемблированного кода, относящуюся к циклу for. Предположим, что компилятор решил использовать EBX для хранения адреса массива myArray.

assembly
mov ebx, offset myArray    ; Загружаем адрес myArray в ebx
xor eax, eax               ; Обнуляем eax (eax будет использоваться для суммы)
xor ecx, ecx               ; Обнуляем ecx (ecx будет использоваться как индекс i)

loop_start:
    cmp ecx, 5                 ; Сравниваем ecx с 5
    jge loop_end               ; Если ecx >= 5, выходим из цикла
    add eax, [ebx + ecx*4]     ; Добавляем элемент массива к eax
    inc ecx                    ; Увеличиваем индекс i
    jmp loop_start             ; Переходим к началу цикла

loop_end:
    ; Здесь eax содержит сумму элементов массива
    ; Продолжаем с вызовом printf 
Примечание

В действительности дизассемблированный код выглядит иначе.

Объяснение дизассемблированного кода цикла for

Загружает адрес массива myArray в регистр EBX. Теперь EBX служит базовым регистром для адресации элементов массива:

mov ebx, offset myArray 

Обнуляет регистр EAX, который будет использоваться для хранения суммы элементов массива:

xor eax, eax 

Обнуляет регистр ECX, который будет использоваться как индекс i в цикле:

xor ecx, ecx 

Начало цикла:

loop_start: 

Сравнивает значение ECX с 5:

cmp ecx, 5 

Если ECX больше или равно 5, переходит к метке loop_end, завершая цикл:

jge loop_end 

Добавляет значение элемента массива, на который указывает ebx + ecx*4, к eax. Здесь EBX используется как базовый регистр, а ecx*4 — как смещение для доступа к нужному элементу массива (поскольку каждый элемент занимает 4 байта):

add eax, [ebx + ecx*4] 

Увеличивает значение ECX на 1, переходя к следующему элементу массива:

inc ecx 

Переходит к началу цикла:

jmp loop_start 

Конец цикла:

loop_end: 

В этом примере регистр EBX используется как базовый регистр для адресации элементов массива myArray. Это демонстрирует, как EBX может быть эффективно использован для доступа к данным в памяти, обеспечивая гибкость и скорость в ассемблерном коде.

1.3. Регистр ECX (count register)

Кратко: назначение регистра ECX

-  Часто используется как счетчик в циклах.

-  Поддерживает операции сдвига и циклического сдвига.

Пример 1 (реальный). Исходный код на C

Рассмотрим пример программы на языке C, которая должна, теоретически, использовать регистр ECX. Создайте файл example.c со следующим содержимым:

c
#include <stdio.h>

int multiply(int a, int b) {
    int result = 0;
    for(int i = 0; i < b; i++) {
        result += a;
    }
    return result;
}

int main() {
    int x = 5;
    int y = 3;
    int product = multiply(x, y);
    printf("Result is %d\n", product);
    return 0;
} 
Фрагмент дизассемблированной программы (функции multiply и main)
assembly
004015c0 <_multiply>:
  4015c0:	55                   	push   ebp
  4015c1:	89 e5                	mov    ebp,esp
  4015c3:	83 ec 10             	sub    esp,0x10
  4015c6:	c7 45 fc 00 00 00 00 	mov    DWORD PTR [ebp-0x4],0x0
  4015cd:	c7 45 f8 00 00 00 00 	mov    DWORD PTR [ebp-0x8],0x0
  4015d4:	eb 0a                	jmp    4015e0 <_multiply+0x20>
  4015d6:	8b 45 08             	mov    eax,DWORD PTR [ebp+0x8]
  4015d9:	01 45 fc             	add    DWORD PTR [ebp-0x4],eax
  4015dc:	83 45 f8 01          	add    DWORD PTR [ebp-0x8],0x1
  4015e0:	8b 45 f8             	mov    eax,DWORD PTR [ebp-0x8]
  4015e3:	3b 45 0c             	cmp    eax,DWORD PTR [ebp+0xc]
  4015e6:	7c ee                	jl     4015d6 <_multiply+0x16>
  4015e8:	8b 45 fc             	mov    eax,DWORD PTR [ebp-0x4]
  4015eb:	c9                   	leave  
  4015ec:	c3                   	ret    

004015ed <_main>:
  4015ed:	55                   	push   ebp
  4015ee:	89 e5                	mov    ebp,esp
  4015f0:	83 e4 f0             	and    esp,0xfffffff0
  4015f3:	83 ec 20             	sub    esp,0x20
  4015f6:	e8 d5 00 00 00       	call   4016d0 <___main>
  4015fb:	c7 44 24 1c 05 00 00 	mov    DWORD PTR [esp+0x1c],0x5
  401602:	00 
  401603:	c7 44 24 18 03 00 00 	mov    DWORD PTR [esp+0x18],0x3
  40160a:	00 
  40160b:	8b 44 24 18          	mov    eax,DWORD PTR [esp+0x18]
  40160f:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  401613:	8b 44 24 1c          	mov    eax,DWORD PTR [esp+0x1c]
  401617:	89 04 24             	mov    DWORD PTR [esp],eax
  40161a:	e8 a1 ff ff ff       	call   4015c0 <_multiply>
  40161f:	89 44 24 14          	mov    DWORD PTR [esp+0x14],eax
  401623:	8b 44 24 14          	mov    eax,DWORD PTR [esp+0x14]
  401627:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  40162b:	c7 04 24 44 40 40 00 	mov    DWORD PTR [esp],0x404044
  401632:	e8 c5 0f 00 00       	call   4025fc <_printf>
  401637:	b8 00 00 00 00       	mov    eax,0x0
  40163c:	c9                   	leave  
  40163d:	c3                   	ret   
Где обещанный регистр ECX?

Компилятор решил не использовать регистр ECX. Значения переменных i, a, result перемещались через один регистр EAX.

Объяснение дизассемблированного кода функции _multiply (004015c0)

Инициализирует локальную переменную result по адресу ebp-0x4 значением 0x0. Это переменная для хранения результата:

4015c6: c7 45 fc 00 00 00 00  mov    DWORD PTR [ebp-0x4],0x0 

Инициализирует локальную переменную i по адресу ebp-0x8 значением 0x0. Это счетчик:

4015cd: c7 45 f8 00 00 00 00  mov    DWORD PTR [ebp-0x8],0x0 

Безусловный переход к метке 4015e0, которая является началом цикла:

4015d4: eb 0a    jmp    4015e0 <_multiply+0x20> 

Если условие по адресу 4015e6 истинно (i < b), то переходим в начало тела цикла по адресу 4015d6. Проверка условия выглядит так:

4015e0:	8b 45 f8             	mov    eax,DWORD PTR [ebp-0x8]
4015e3:	3b 45 0c             	cmp    eax,DWORD PTR [ebp+0xc]
4015e6:	7c ee                	jl     4015d6 <_multiply+0x16> 

Загружает первый аргумент a функции в EAX. Это значение, которое нужно умножить:

4015d6: 8b 45 08    mov    eax,DWORD PTR [ebp+0x8] 

Добавляет значение из EAX к локальной переменной result по адресу ebp-0x4. Это накапливает результат:

4015d9: 01 45 fc    add    DWORD PTR [ebp-0x4],eax 

Увеличивает счетчик i по адресу ebp-0x8 на 1:

4015dc: 83 45 f8 01    add    DWORD PTR [ebp-0x8],0x1 

Подготавливает к проверке условие в цикле for. Загружает текущее значение счетчика в EAX:

4015e0: 8b 45 f8    mov    eax,DWORD PTR [ebp-0x8] 

Сравнивает счетчик со вторым аргументом b функции:

4015e3: 3b 45 0c    cmp    eax,DWORD PTR [ebp+0xc] 

Если счетчик i меньше второго аргумента b функции (i < b), переходит к метке 4015d6, продолжая цикл до тех пор, пока выполняется условие:

4015e6: 7c ee    jl     4015d6 <_multiply+0x16> 

После выхода из цикла значение переменной result (по адресу ebp-0x4) перемещается в EAX для возврата из функции _multiply:

4015e8: 8b 45 fc    mov    eax,DWORD PTR [ebp-0x4] 

Пример 2 (выдуманный).

Напишем ещё одну простую программу на языке C, которая использует регистр ECX в соответствии с его назначением — как счетчик в циклах и для операций сдвига.

c
#include <stdio.h>

// Функция для вычисления факториала числа
unsigned int factorial(unsigned int n) {
    unsigned int result = 1;
    unsigned int i;

    // Используем ecx как счетчик в цикле
    for (i = 1; i <= n; i++) {
        result *= i;
    }

    return result;
}

int main() {
    unsigned int number = 5;
    unsigned int fact = factorial(number);
    printf("Факториал числа %u равен %u\n", number, fact);
    return 0;
} 
Объяснение исходного кода программы
  • Функция factorial: вычисляет факториал числа n с использованием цикла.
  • Переменная result: хранит текущее значение факториала.
  • Цикл for: использует переменную i в качестве счетчика, который будет отображаться на регистр ECX.
  • Функция printf: выводит результат вычисления факториала.
Фрагмент дизассемблированной программы (функция factorial)

Для демонстрации использования ECX как счетчика в цикле, мы рассмотрим часть дизассемблированного кода, относящуюся к функции factorial.

assembly
; Предполагаемый дизассемблированный код для функции factorial

factorial:
    push ebp                ; Сохраняем указатель базы стека
    mov ebp, esp            ; Устанавливаем новый указатель базы стека
    mov eax, 1              ; Инициализируем eax (result) значением 1
    mov ecx, 1              ; Инициализируем ecx (i) значением 1

loop_start:
    cmp ecx, [ebp+8]        ; Сравниваем ecx с n (аргумент функции)
    jg loop_end             ; Если ecx > n, выходим из цикла
    imul eax, ecx           ; Умножаем eax (result) на ecx (i)
    inc ecx                 ; Увеличиваем ecx (i)
    jmp loop_start          ; Переходим к началу цикла

loop_end:
    pop ebp                 ; Восстанавливаем указатель базы стека
    ret                     ; Возвращаем результат в eax

main:
    push ebp                ; Сохраняем указатель базы стека
    mov ebp, esp            ; Устанавливаем новый указатель базы стека
    mov dword ptr [ebp-4], 5 ; Инициализируем переменную number значением 5
    mov eax, [ebp-4]        ; Загружаем number в eax
    push eax                 ; Передаем number в качестве аргумента в factorial
    call factorial           ; Вызываем функцию factorial
    add esp, 4               ; Очищаем стек
    mov [ebp-8], eax         ; Сохраняем результат в fact
    mov eax, [ebp-8]         ; Загружаем fact в eax
    push eax                 ; Передаем fact в качестве аргумента в printf
    push offset format       ; Передаем строку формата в стек
    call printf              ; Вызываем printf
    add esp, 8               ; Очищаем стек
    xor eax, eax             ; Возвращаем 0
    pop ebp                  ; Восстанавливаем указатель базы стека
    ret                      ; Возвращаем управление 
Примечание

В действительности дизассемблированный код выглядит иначе.

Объяснение дизассемблированного кода функции factorial
  • mov ecx, 1: инициализирует счетчик ECX значением 1.
  • loop_start: метка начала цикла.
  • cmp ecx, [ebp+8]: сравнивает значение ECX с аргументом функции n.
  • jg loop_end: если ECX больше n, выходит из цикла.
  • imul eax, ecx: умножает текущее значение result (хранится в EAX) на ECX.
  • inc ecx: увеличивает значение ECX на 1.
  • jmp loop_start: переходит к началу цикла.

В этом примере, ECX используется как счетчик в цикле для вычисления факториала. Это демонстрирует, как ECX может быть эффективно использован для управления итерациями цикла, что является типичным применением этого регистра в ассемблерном коде.

1.4. Регистр EDX (data register)

Кратко: назначение регистра EDX

-  Дополняет регистр EAX в некоторых операциях, таких как умножение и деление.

-  Используется для хранения старшей части результата в операциях умножения и деления.

Пример 1 (реальный). Исходный код на C для демонстрации использования регистр EDX для выполнения операции арифметического сдвига

Рассмотрим пример программы на языке C, которая использует регистр EDX, и посмотрим, как этот код выглядит в дизассемблированном виде.

Создайте файл example.c со следующим содержимым:

c
#include <stdio.h>

int compute(int a, int b) {
    int temp = a * b;
    int result = temp / 2;
    return result;
}

int main() {
    int x = 6;
    int y = 3;
    int res = compute(x, y);
    printf("Result is %d\n", res);
    return 0;
} 
Фрагмент дизассемблированной программы (функции compute и main)
assembly
004015c0 <_compute>:
  4015c0:	55                   	push   ebp
  4015c1:	89 e5                	mov    ebp,esp
  4015c3:	83 ec 10             	sub    esp,0x10
  4015c6:	8b 45 08             	mov    eax,DWORD PTR [ebp+0x8]
  4015c9:	0f af 45 0c          	imul   eax,DWORD PTR [ebp+0xc]
  4015cd:	89 45 fc             	mov    DWORD PTR [ebp-0x4],eax
  4015d0:	8b 45 fc             	mov    eax,DWORD PTR [ebp-0x4]
  4015d3:	89 c2                	mov    edx,eax
  4015d5:	c1 ea 1f             	shr    edx,0x1f
  4015d8:	01 d0                	add    eax,edx
  4015da:	d1 f8                	sar    eax,1
  4015dc:	89 45 f8             	mov    DWORD PTR [ebp-0x8],eax
  4015df:	8b 45 f8             	mov    eax,DWORD PTR [ebp-0x8]
  4015e2:	c9                   	leave  
  4015e3:	c3                   	ret    

004015e4 <_main>:
  4015e4:	55                   	push   ebp
  4015e5:	89 e5                	mov    ebp,esp
  4015e7:	83 e4 f0             	and    esp,0xfffffff0
  4015ea:	83 ec 20             	sub    esp,0x20
  4015ed:	e8 de 00 00 00       	call   4016d0 <___main>
  4015f2:	c7 44 24 1c 06 00 00 	mov    DWORD PTR [esp+0x1c],0x6
  4015f9:	00 
  4015fa:	c7 44 24 18 03 00 00 	mov    DWORD PTR [esp+0x18],0x3
  401601:	00 
  401602:	8b 44 24 18          	mov    eax,DWORD PTR [esp+0x18]
  401606:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  40160a:	8b 44 24 1c          	mov    eax,DWORD PTR [esp+0x1c]
  40160e:	89 04 24             	mov    DWORD PTR [esp],eax
  401611:	e8 aa ff ff ff       	call   4015c0 <_compute>
  401616:	89 44 24 14          	mov    DWORD PTR [esp+0x14],eax
  40161a:	8b 44 24 14          	mov    eax,DWORD PTR [esp+0x14]
  40161e:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  401622:	c7 04 24 44 40 40 00 	mov    DWORD PTR [esp],0x404044
  401629:	e8 ce 0f 00 00       	call   4025fc <_printf>
  40162e:	b8 00 00 00 00       	mov    eax,0x0
  401633:	c9                   	leave  
  401634:	c3                   	ret     
Объяснение дизассемблированного кода функции _compute (004015c0)

Выделяет 0x10 (16) байт в стеке для локальных переменных:

4015c3: 83 ec 10    sub    esp,0x10 

Загружает первый аргумент a функции в регистр EAX:

4015c6: 8b 45 08    mov    eax,DWORD PTR [ebp+0x8] 

Выполняет умножение EAX на второй аргумент b функции (DWORD PTR [ebp+0xc]), сохраняя результат в EAX. Это целочисленное умножение:

4015c9: 0f af 45 0c    imul   eax,DWORD PTR [ebp+0xc] 
Примечание

Команда imul в ассемблере x86 используется для выполнения умножения со знаком. Она позволяет умножать целые числа со знаком и может работать с различным количеством операндов.

Синтаксис: imul <операнд> - умножает значение в регистре AX, EAX или RAX (в зависимости от разрядности) на операнд. Результат сохраняется в паре регистров DX:AX, EDX:EAX или RDX:RAX для 16, 32 и 64 бит соответственно.

Синтаксис: imul <операнд1>, <операнд2> - умножает операнд1 на операнд2 и сохраняет результат в операнд1. Операнды могут быть регистрами или ячейками памяти.

Синтаксис: imul <операнд1>, <операнд2>, <непосредственное значение> - умножает операнд2 на непосредственное значение и сохраняет результат в операнд1

Сохраняет результат умножения в локальной переменной temp по адресу ebp-0x4:

4015cd: 89 45 fc    mov    DWORD PTR [ebp-0x4],eax 

Загружает значение из локальной переменной temp в EAX:

4015d0: 8b 45 fc    mov    eax,DWORD PTR [ebp-0x4] 

Копирует значение из EAX в EDXEDX теперь находится значение переменной temp)

4015d3: 89 c2    mov    edx,eax 

Выполняет арифметический сдвиг вправо EDX на 31 бит. Это используется для получения знакового бита числа (теперь в EDX хранится 0 или 1):

4015d5: c1 ea 1f    shr    edx,0x1f 
Что означает получение знакового бита числа?

Знаковый бит — это старший бит (самый левый) в двоичном представлении числа, который указывает на его знак: 0 – число положительное или ноль, 1 – отрицательное.

Что происходит при сдвиге вправо командой shr на 31 бит? Старший бит сдвигается на место младшего, остальные биты теряются, а освобождённые биты слева – заполняются нулями.

Добавляет значение EDX к EAX. Это часть алгоритма для вычисления среднего значения с учетом знака:

4015d8: 01 d0    add    eax,edx 

Выполняет арифметический сдвиг вправо EAX на 1 бит, что эквивалентно делению на 2 с учетом знака:

4015da: d1 f8    sar    eax,1 
Арифметический сдвиг

Операция sar выполняет арифметический сдвиг вправо, где каждый бит числа сдвигается вправо на указанное количество позиций. При этом самый старший бит (знаковый бит) сохраняется, что позволяет сохранить знак числа.

Сохраняет результат в локальной переменной result по адресу ebp-0x8:

4015dc: 89 45 f8    mov    DWORD PTR [ebp-0x8],eax 

Загружает результат обратно в eax для возврата из функции:

4015df: 8b 45 f8    mov    eax,DWORD PTR [ebp-0x8] 
Объяснение дизассемблированного кода функции _main (004015e4)

Выделяет 0x20 (32) байта в стеке для локальных переменных:

4015ea: 83 ec 20    sub    esp,0x20 

Устанавливает значение 0x6 по адресу esp+0x1c для переменной x:

4015f2: c7 44 24 1c 0x06000000  mov    DWORD PTR [esp+0x1c],0x6 

Устанавливает значение 0x3 по адресу esp+0x18 для переменной y:

4015fa: c7 44 24 18 0x03000000  mov    DWORD PTR [esp+0x18],0x3 

Загружает значение 0x3 в EAX:

401602: 8b 44 24 18    mov    eax,DWORD PTR [esp+0x18] 

Сохраняет значение 0x3 по адресу esp+0x4. Это второй аргумент b для функции _compute:

401606: 89 44 24 04    mov    DWORD PTR [esp+0x4],eax 

Загружает значение 0x6 в EAX:

40160a: 8b 44 24 1c    mov    eax,DWORD PTR [esp+0x1c] 

Сохраняет значение 0x6 в стеке как первый аргумент a для функции _compute:

40160e: 89 04 24    mov    DWORD PTR [esp],eax 

Вызывает функцию _compute с аргументами 0x6 и 0x3:

401611: e8 aa ff ff ff    call   4015c0 <_compute> 

Сохраняет результат вызова _compute по адресу esp+0x14:

401616: 89 44 24 14    mov    DWORD PTR [esp+0x14],eax 

Загружает результат в EAX:

40161a: 8b 44 24 14    mov    eax,DWORD PTR [esp+0x14] 

Сохраняет результат по адресу esp+0x4. Это второй аргумент для printf:

40161e: 89 44 24 04    mov    DWORD PTR [esp+0x4],eax 

Устанавливает первый аргумент для printf – это строка формата:

401622: c7 04 24 44 40 40 00  mov    DWORD PTR [esp],0x404044 

Вызывает функцию printf с переданными аргументами:

401629: e8 ce 0f 00 00    call   4025fc <_printf> 
Объяснение использования EDX
  • Регистр EDX используется в операции деления. В архитектуре x86, когда выполняется деление, EDX содержит старшую часть делимого, а EAX — младшую.

Этот пример демонстрирует, как регистр EDX используется в операциях деления в ассемблерном коде.

1.5. Регистр ESI (source index)

Кратко: назначение регистра ESI

-  Регистр ESI используется как указатель на источник данных в операциях с памятью, таких как строковые операции.

Немного теории: регистр ESI используется как указатель на источник данных в операциях с памятью, таких как строковые операции

Регистр ESI (source index): в архитектуре x86 является одним из регистров общего назначения, но он также имеет специальное назначение в строковых операциях.

Строковые операции в ассемблере x86 предназначены для обработки блоков данных, которые могут быть строками символов или просто последовательностями байтов. Эти операции автоматически увеличивают или уменьшают регистры-указатели в зависимости от флага направления (DF).

В 32-битном режиме регистр esi используется как указатель на источник данных в строковых операциях, таких как movs, cmps, scas, lods и stos.

В 64-битном режиме, аналогичную роль выполняет регистр RSI.

Команды movs, cmps, scas, lods и stos в ассемблере x86 являются частью группы строковых операций (string instructions). Эти команды предназначены для обработки блоков данных, таких как массивы или строки, и часто используются в циклах для выполнения операций над последовательностями байтов, слов или двойных слов.

Операция movs (move string)

Операция movs копирует данные из одного места в памяти в другое.

Обычно используется без явных операндов, так как работает с регистрами ESI (или SI в 16-битном режиме) и EDI (или DI в 16-битном режиме), которые указывают на источник и приемник соответственно.

Работает следующим образом: копирует байт, слово или двойное слово из адреса, на который указывает ESI, в адрес, на который указывает EDI. После выполнения операции, регистры ESI и EDI автоматически увеличиваются или уменьшаются (в зависимости от флага направления DF) на размер операнда (1, 2 или 4 байта).

Пример: movsb (перемещение байта), movsw (перемещение слова), movsd (перемещение двойного слова).

Операция cmps (compare string)

Операция cmps сравнивает два блока данных в памяти. Тоже работает с регистрами ESI (источник) и EDI (приемник).

Работает следующим образом: сравнивает байт, слово или двойное слово из адреса, на который указывает ESI, с байтом, словом или двойным словом из адреса, на который указывает EDI. Устанавливает флаги в соответствии с результатом сравнения. После выполнения операции, регистры ESI и EDI автоматически увеличиваются или уменьшаются на размер операнда.

Пример: cmpsb (сравнение байта), cmpsw (сравнение слова), cmpsd (сравнение двойного слова).

Операция scas (scan string)

Операция scas ищет определенное значение в блоке данных.

Работает следующим образом: взаимодействует с регистром EDI, который указывает на начало блока данных, и регистром AL/AX/EAX (в зависимости от размера операнда), который содержит значение для поиска. Сравнивает значение в регистре AL/AX/EAX с байтом, словом или двойным словом из адреса, на который указывает EDI. Далее устанавливает флаги в соответствии с результатом сравнения. После выполнения операции, регистр EDI автоматически увеличивается или уменьшается на размер операнда.

Пример: scasb (поиск байта), scasw (поиск слова), scasd (поиск двойного слова).

Операция lods (load string)

Операция lods загружает данные из памяти в регистр. Работает с регистром ESI, который указывает на источник данных, и регистром AL/AX/EAX (в зависимости от размера операнда), в который загружаются данные.

Работает следующим образом: загружает байт, слово или двойное слово из адреса, на который указывает ESI, в регистр AL/AX/EAX. После выполнения операции, регистр ESI автоматически увеличивается или уменьшается на размер операнда.

Пример: lodsb (загрузка байта), lodsw (загрузка слова), lodsd (загрузка двойного слова).

Операция stos (store string)

Операция stos сохраняет данные из регистра в память. Работает с регистром EDI, который указывает на приемник данных, и регистром AL/AX/EAX (в зависимости от размера операнда), из которого берутся данные.

Работает следующим образом: сохраняет байт, слово или двойное слово из регистра AL/AX/EAX в адрес, на который указывает EDI. После выполнения операции, регистр EDI автоматически увеличивается или уменьшается на размер операнда.

Пример: stosb (сохранение байта), stosw (сохранение слова), stosd (сохранение двойного слова).

Пример 1. Использование movs для копирования строки

Предположим, мы хотим скопировать строку из одного места в памяти в другое.

assembly
section .data
    source db 'Hello, World!', 0
    dest db 14 dup (0)

section .text
    global _start

_start:
    lea esi, [source]    ; Загружаем адрес source в esi
    lea edi, [dest]      ; Загружаем адрес dest в edi
    mov ecx, 13          ; Устанавливаем счетчик для 13 символов (без учета нулевого символа)

copy_loop:
    movsb                ; Копируем байт из [esi] в [edi], увеличиваем esi и edi
    dec ecx              ; Уменьшаем счетчик
    jnz copy_loop        ; Повторяем, если счетчик не равен нулю

    ; Здесь строка скопирована из source в dest 

Объяснение:

  • ESI указывает на источник данных (source).
  • EDI указывает на место назначения (dest).
  • movsb копирует байт из [esi] в [edi] и увеличивает оба регистра.
  • Цикл продолжается, пока ECX не станет равным нулю.
Пример 2. Использование lods для загрузки данных

Предположим, мы хотим загрузить данные из массива в регистр.

assembly
section .data
    array db 1, 2, 3, 4, 5

section .text
    global _start

_start:
    lea esi, [array]     ; Загружаем адрес array в esi
    mov ecx, 5           ; Устанавливаем счетчик для 5 элементов

load_loop:
    lodsb                ; Загружаем байт из [esi] в al, увеличиваем esi
    ; Здесь можно выполнить операции с загруженным байтом
    dec ecx              ; Уменьшаем счетчик
    jnz load_loop        ; Повторяем, если счетчик не равен нулю

    ; Здесь данные из массива загружены в регистр al 

Объяснение:

  • ESI указывает на источник данных (array).
  • lodsb загружает байт из [esi] в AL и увеличивает ESI.
  • Цикл продолжается, пока ECX не станет равным нулю.
Пример 3: Использование cmps для сравнения строк

Предположим, мы хотим сравнить две строки.

assembly
section .data
    string1 db 'Hello', 0
    string2 db 'Hello', 0

section .text
    global _start

_start:
    lea esi, [string1]   ; Загружаем адрес string1 в esi
    lea edi, [string2]   ; Загружаем адрес string2 в edi
    mov ecx, 5           ; Устанавливаем счетчик для 5 символов

compare_loop:
    cmpsb                ; Сравниваем байты из [esi] и [edi], увеличиваем esi и edi
    jne not_equal        ; Если не равны, переходим к not_equal
    dec ecx              ; Уменьшаем счетчик
    jnz compare_loop     ; Повторяем, если счетчик не равен нулю

    ; Строки равны
    jmp end_program

not_equal:
    ; Строки не равны

end_program:
    ; Завершение программы 

Объяснение:

  • ESI указывает на источник данных (string1).
  • EDI указывает на место назначения (string2).
  • cmpsb сравнивает байты из [esi] и [edi] и увеличивает оба регистра.
  • Цикл продолжается, пока ECX не станет равным нулю.

Регистр ESI (или RSI в 64-битном режиме) играет ключевую роль в операциях с памятью, особенно в строковых операциях, где он используется как указатель на источник данных. Это позволяет эффективно управлять данными в памяти, выполняя такие операции, как копирование, сравнение и загрузка данных.

Пример 1 (реальный). Исходный код на C

004015c0 <_process_data>:
  4015c0:	55                   	push   ebp
  4015c1:	89 e5                	mov    ebp,esp
  4015c3:	83 ec 28             	sub    esp,0x28
  4015c6:	c7 45 f4 00 00 00 00 	mov    DWORD PTR [ebp-0xc],0x0
  4015cd:	eb 2c                	jmp    4015fb <_process_data+0x3b>
  4015cf:	8b 45 f4             	mov    eax,DWORD PTR [ebp-0xc]
  4015d2:	8d 14 85 00 00 00 00 	lea    edx,[eax*4+0x0]
  4015d9:	8b 45 08             	mov    eax,DWORD PTR [ebp+0x8]
  4015dc:	01 d0                	add    eax,edx
  4015de:	8b 00                	mov    eax,DWORD PTR [eax]
  4015e0:	89 44 24 08          	mov    DWORD PTR [esp+0x8],eax
  4015e4:	8b 45 f4             	mov    eax,DWORD PTR [ebp-0xc]
  4015e7:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  4015eb:	c7 04 24 44 40 40 00 	mov    DWORD PTR [esp],0x404044
  4015f2:	e8 25 10 00 00       	call   40261c <_printf>
  4015f7:	83 45 f4 01          	add    DWORD PTR [ebp-0xc],0x1
  4015fb:	8b 45 f4             	mov    eax,DWORD PTR [ebp-0xc]
  4015fe:	3b 45 0c             	cmp    eax,DWORD PTR [ebp+0xc]
  401601:	7c cc                	jl     4015cf <_process_data+0xf>
  401603:	90                   	nop
  401604:	c9                   	leave  
  401605:	c3                   	ret    

00401606 <_main>:
  401606:	55                   	push   ebp
  401607:	89 e5                	mov    ebp,esp
  401609:	83 e4 f0             	and    esp,0xfffffff0
  40160c:	83 ec 30             	sub    esp,0x30
  40160f:	e8 dc 00 00 00       	call   4016f0 <___main>
  401614:	c7 44 24 18 0a 00 00 	mov    DWORD PTR [esp+0x18],0xa
  40161b:	00 
  40161c:	c7 44 24 1c 14 00 00 	mov    DWORD PTR [esp+0x1c],0x14
  401623:	00 
  401624:	c7 44 24 20 1e 00 00 	mov    DWORD PTR [esp+0x20],0x1e
  40162b:	00 
  40162c:	c7 44 24 24 28 00 00 	mov    DWORD PTR [esp+0x24],0x28
  401633:	00 
  401634:	c7 44 24 28 32 00 00 	mov    DWORD PTR [esp+0x28],0x32
  40163b:	00 
  40163c:	c7 44 24 2c 05 00 00 	mov    DWORD PTR [esp+0x2c],0x5
  401643:	00 
  401644:	8b 44 24 2c          	mov    eax,DWORD PTR [esp+0x2c]
  401648:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  40164c:	8d 44 24 18          	lea    eax,[esp+0x18]
  401650:	89 04 24             	mov    DWORD PTR [esp],eax
  401653:	e8 68 ff ff ff       	call   4015c0 <_process_data>
  401658:	b8 00 00 00 00       	mov    eax,0x0
  40165d:	c9                   	leave  
  40165e:	c3                   	ret    
  40165f:	90                   	nop 

Рассмотрим пример программы на языке C, которая неявно использует регистр ESI, и посмотрим, как этот код выглядит в дизассемблированном виде.

Создайте файл example.c со следующим содержимым:

c
#include <stdio.h>

void process_data(int *data, int length) {
    for(int i = 0; i < length; i++) {
        printf("Data[%d] = %d\n", i, data[i]);
    }
}

int main() {
    int myData[5] = {10, 20, 30, 40, 50};
    int length = 5;
    process_data(myData, length);
    return 0;
} 
Фрагмент дизассемблированной программы (функции process_data и main)
assembly
004015c0 <_process_data>:
  4015c0:	55                   	push   ebp
  4015c1:	89 e5                	mov    ebp,esp
  4015c3:	83 ec 28             	sub    esp,0x28
  4015c6:	c7 45 f4 00 00 00 00 	mov    DWORD PTR [ebp-0xc],0x0
  4015cd:	eb 2c                	jmp    4015fb <_process_data+0x3b>
  4015cf:	8b 45 f4             	mov    eax,DWORD PTR [ebp-0xc]
  4015d2:	8d 14 85 00 00 00 00 	lea    edx,[eax*4+0x0]
  4015d9:	8b 45 08             	mov    eax,DWORD PTR [ebp+0x8]
  4015dc:	01 d0                	add    eax,edx
  4015de:	8b 00                	mov    eax,DWORD PTR [eax]
  4015e0:	89 44 24 08          	mov    DWORD PTR [esp+0x8],eax
  4015e4:	8b 45 f4             	mov    eax,DWORD PTR [ebp-0xc]
  4015e7:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  4015eb:	c7 04 24 44 40 40 00 	mov    DWORD PTR [esp],0x404044
  4015f2:	e8 25 10 00 00       	call   40261c <_printf>
  4015f7:	83 45 f4 01          	add    DWORD PTR [ebp-0xc],0x1
  4015fb:	8b 45 f4             	mov    eax,DWORD PTR [ebp-0xc]
  4015fe:	3b 45 0c             	cmp    eax,DWORD PTR [ebp+0xc]
  401601:	7c cc                	jl     4015cf <_process_data+0xf>
  401603:	90                   	nop
  401604:	c9                   	leave  
  401605:	c3                   	ret    

00401606 <_main>:
  401606:	55                   	push   ebp
  401607:	89 e5                	mov    ebp,esp
  401609:	83 e4 f0             	and    esp,0xfffffff0
  40160c:	83 ec 30             	sub    esp,0x30
  40160f:	e8 dc 00 00 00       	call   4016f0 <___main>
  401614:	c7 44 24 18 0a 00 00 	mov    DWORD PTR [esp+0x18],0xa
  40161b:	00 
  40161c:	c7 44 24 1c 14 00 00 	mov    DWORD PTR [esp+0x1c],0x14
  401623:	00 
  401624:	c7 44 24 20 1e 00 00 	mov    DWORD PTR [esp+0x20],0x1e
  40162b:	00 
  40162c:	c7 44 24 24 28 00 00 	mov    DWORD PTR [esp+0x24],0x28
  401633:	00 
  401634:	c7 44 24 28 32 00 00 	mov    DWORD PTR [esp+0x28],0x32
  40163b:	00 
  40163c:	c7 44 24 2c 05 00 00 	mov    DWORD PTR [esp+0x2c],0x5
  401643:	00 
  401644:	8b 44 24 2c          	mov    eax,DWORD PTR [esp+0x2c]
  401648:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  40164c:	8d 44 24 18          	lea    eax,[esp+0x18]
  401650:	89 04 24             	mov    DWORD PTR [esp],eax
  401653:	e8 68 ff ff ff       	call   4015c0 <_process_data>
  401658:	b8 00 00 00 00       	mov    eax,0x0
  40165d:	c9                   	leave  
  40165e:	c3                   	ret    
  40165f:	90                   	nop 
Объяснение дизассемблированного кода функции _process_data (004015c0)

Выделяет 0x28 (40) байт в стеке для локальных переменных:

4015c3: 83 ec 28    sub    esp,0x28 

Инициализирует локальную переменную по адресу ebp-0xc значением 0x0. Это, вероятно, счетчик или индекс:

4015c6: c7 45 f4 00 00 00 00  mov    DWORD PTR [ebp-0xc],0x0 

Безусловный переход к метке 4015fb. Это начало цикла:

4015cd: eb 2c    jmp    4015fb <_process_data+0x3b> 

Загружает значение счетчика из ebp-0xc в регистр EAX:

4015cf: 8b 45 f4    mov    eax,DWORD PTR [ebp-0xc] 

Вычисляет адрес в памяти, используя eax * 4. Это может быть связано с индексацией массива:

4015d2: 8d 14 85 00 00 00 00  lea    edx,[eax*4+0x0] 

Загружает аргумент функции (по адресу ebp+0x8) в EAX. Это указатель на массив:

4015d9: 8b 45 08    mov    eax,DWORD PTR [ebp+0x8] 

Добавляет значение EDX к EAX, вычисляя адрес элемента в массиве:

4015dc: 01 d0    add    eax,edx 

Загружает значение из вычисленного адреса в EAX:

4015de: 8b 00    mov    eax,DWORD PTR [eax] 

Сохраняет значение в стеке для передачи в функцию printf:

4015e0: 89 44 24 08    mov    DWORD PTR [esp+0x8],eax 

Загружает значение счетчика в EAX:

4015e4: 8b 45 f4    mov    eax,DWORD PTR [ebp-0xc] 

Сохраняет значение счетчика в стеке для передачи в printf:

4015e7: 89 44 24 04    mov    DWORD PTR [esp+0x4],eax 

Устанавливает первый аргумент для printf – это, строка формата:

4015eb: c7 04 24 44 40 40 00  mov    DWORD PTR [esp],0x404044 

Вызывает функцию printf с переданными аргументами:

4015f2: e8 25 10 00 00    call   40261c <_printf> 

Увеличивает значение счетчика на 1:

4015f7: 83 45 f4 01    add    DWORD PTR [ebp-0xc],0x1 

Загружает обновленное значение счетчика в EAX:

4015fb: 8b 45 f4    mov    eax,DWORD PTR [ebp-0xc] 

Сравнивает значение счетчика с аргументом функции (по адресу ebp+0xc):

4015fe: 3b 45 0c    cmp    eax,DWORD PTR [ebp+0xc] 

Если счетчик меньше аргумента, переходит к метке 4015cf, продолжая цикл:

401601: 7c cc    jl     4015cf <_process_data+0xf> 
Объяснение дизассемблированного кода функции _main (00401606)

Выделяет 0x30 (48) байт в стеке для локальных переменных:

40160c: 83 ec 30    sub    esp,0x30 

Устанавливает значение 0xa (10) по адресу esp+0x18:

401614: c7 44 24 18 0a 00 00  mov    DWORD PTR [esp+0x18],0xa 

Устанавливает значение 0x14 (20) по адресу esp+0x1c:

40161c: c7 44 24 1c 14 00 00  mov    DWORD PTR [esp+0x1c],0x14 

Устанавливает значение 0x1e (30) по адресу esp+0x20:

401624: c7 44 24 20 1e 00 00  mov    DWORD PTR [esp+0x20],0x1e 

Устанавливает значение 0x28 (40) по адресу esp+0x24:

40162c: c7 44 24 24 28 00 00  mov    DWORD PTR [esp+0x24],0x28 

Устанавливает значение 0x32 (50) по адресу esp+0x28:

401634: c7 44 24 28 32 00 00  mov    DWORD PTR [esp+0x28],0x32 

Устанавливает значение 0x5 (5) по адресу esp+0x2c:

40163c: c7 44 24 2c 05 00 00  mov    DWORD PTR [esp+0x2c],0x5 

Загружает значение 0x5 в EAX:

401644: 8b 44 24 2c    mov    eax,DWORD PTR [esp+0x2c] 

Сохраняет значение 0x5 по адресу esp+0x4. Это аргумент для функции _process_data:

401648: 89 44 24 04    mov    DWORD PTR [esp+0x4],eax 

Загружает адрес esp+0x18 в EAX. Это указатель на массив:

40164c: 8d 44 24 18    lea    eax,[esp+0x18] 

Сохраняет адрес esp+0x18 в стеке как первый аргумент для функции _process_data:

401650: 89 04 24    mov    DWORD PTR [esp],eax 

Вызывает функцию _process_data с аргументами, которые были установлены в стеке:

401653: e8 68 ff ff ff    call   4015c0 <_process_data> 
Объяснение использования ESI
  • В функции process_data, регистр ESI не используется явно. Однако, в более сложных программах, ESI может использоваться для хранения указателей или других данных.

Этот пример демонстрирует, что в данной программе регистр ESI не используется. В ассемблере регистры ESI и EDI часто используются для работы с данными, такими как указатели на источник и место назначения в строковых операциях. В более сложных программах, особенно в тех, которые выполняют операции с памятью, ESI может играть важную роль.

1.6. Регистр EDI (destination index)

Кратко: назначение регистра EDI

-  Используется как указатель на место назначения в операциях с памятью.

Пример 1 (реальный). Исходный код на C

Рассмотрим пример программы на языке C, которая могла бы использовать регистр EDI, и посмотрим, как этот код выглядит в дизассемблированном виде.

Создайте файл example.c со следующим содержимым:

c
#include <stdio.h>
#include <string.h>

void copy_data(char *source, char *destination, int length) {
    for(int i = 0; i < length; i++) {
        destination[i] = source[i];
    }
}

int main() {
    char src[] = "Hello, World!";
    char dest[50];
    int len = strlen(src) + 1; // +1 для завершающего нулевого байта
    copy_data(src, dest, len);
    printf("Copied string: %s\n", dest);
    return 0;
} 
Фрагмент дизассемблированной программы (функции copy_data и main)
assembly
004015c0 <_copy_data>:
  4015c0:	55                   	push   ebp
  4015c1:	89 e5                	mov    ebp,esp
  4015c3:	83 ec 10             	sub    esp,0x10
  4015c6:	c7 45 fc 00 00 00 00 	mov    DWORD PTR [ebp-0x4],0x0
  4015cd:	eb 19                	jmp    4015e8 <_copy_data+0x28>
  4015cf:	8b 55 fc             	mov    edx,DWORD PTR [ebp-0x4]
  4015d2:	8b 45 08             	mov    eax,DWORD PTR [ebp+0x8]
  4015d5:	01 d0                	add    eax,edx
  4015d7:	8b 4d fc             	mov    ecx,DWORD PTR [ebp-0x4]
  4015da:	8b 55 0c             	mov    edx,DWORD PTR [ebp+0xc]
  4015dd:	01 ca                	add    edx,ecx
  4015df:	0f b6 00             	movzx  eax,BYTE PTR [eax]
  4015e2:	88 02                	mov    BYTE PTR [edx],al
  4015e4:	83 45 fc 01          	add    DWORD PTR [ebp-0x4],0x1
  4015e8:	8b 45 fc             	mov    eax,DWORD PTR [ebp-0x4]
  4015eb:	3b 45 10             	cmp    eax,DWORD PTR [ebp+0x10]
  4015ee:	7c df                	jl     4015cf <_copy_data+0xf>
  4015f0:	90                   	nop
  4015f1:	c9                   	leave  
  4015f2:	c3                   	ret    

004015f3 <_main>:
  4015f3:	55                   	push   ebp
  4015f4:	89 e5                	mov    ebp,esp
  4015f6:	83 e4 f0             	and    esp,0xfffffff0
  4015f9:	83 ec 60             	sub    esp,0x60
  4015fc:	e8 ff 00 00 00       	call   401700 <___main>
  401601:	c7 44 24 4e 48 65 6c 	mov    DWORD PTR [esp+0x4e],0x6c6c6548
  401608:	6c 
  401609:	c7 44 24 52 6f 2c 20 	mov    DWORD PTR [esp+0x52],0x57202c6f
  401610:	57 
  401611:	c7 44 24 56 6f 72 6c 	mov    DWORD PTR [esp+0x56],0x646c726f
  401618:	64 
  401619:	66 c7 44 24 5a 21 00 	mov    WORD PTR [esp+0x5a],0x21
  401620:	8d 44 24 4e          	lea    eax,[esp+0x4e]
  401624:	89 04 24             	mov    DWORD PTR [esp],eax
  401627:	e8 f0 0f 00 00       	call   40261c <_strlen>
  40162c:	83 c0 01             	add    eax,0x1
  40162f:	89 44 24 5c          	mov    DWORD PTR [esp+0x5c],eax
  401633:	8b 44 24 5c          	mov    eax,DWORD PTR [esp+0x5c]
  401637:	89 44 24 08          	mov    DWORD PTR [esp+0x8],eax
  40163b:	8d 44 24 1c          	lea    eax,[esp+0x1c]
  40163f:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  401643:	8d 44 24 4e          	lea    eax,[esp+0x4e]
  401647:	89 04 24             	mov    DWORD PTR [esp],eax
  40164a:	e8 71 ff ff ff       	call   4015c0 <_copy_data>
  40164f:	8d 44 24 1c          	lea    eax,[esp+0x1c]
  401653:	89 44 24 04          	mov    DWORD PTR [esp+0x4],eax
  401657:	c7 04 24 44 40 40 00 	mov    DWORD PTR [esp],0x404044
  40165e:	e8 c9 0f 00 00       	call   40262c <_printf>
  401663:	b8 00 00 00 00       	mov    eax,0x0
  401668:	c9                   	leave  
  401669:	c3                   	ret     
Объяснение дизассемблированного кода функции _copy_data (004015c0)

Выделяет 0x10 (16) байт в стеке для локальных переменных:

4015c3: 83 ec 10    sub    esp,0x10 

Инициализирует локальную переменную по адресу ebp-0x4 значением 0x0. Это счетчик или индекс:

4015c6: c7 45 fc 0x00000000    mov    DWORD PTR [ebp-0x4],0x0 

Безусловный переход к метке 4015e8, которая находится в конце цикла:

4015cd: eb 19    jmp    4015e8 <_copy_data+0x28> 

Загружает значение счетчика в EDX:

4015cf: 8b 55 fc    mov    edx,DWORD PTR [ebp-0x4] 

Загружает первый аргумент функции в EAX. Это указатель на исходный массив:

4015d2: 8b 45 08    mov    eax,DWORD PTR [ebp+0x8] 

Добавляет значение EDX к EAX, вычисляя адрес элемента в исходном массиве:

4015d5: 01 d0    add    eax,edx 

Загружает значение счетчика в ECX:

4015d7: 8b 4d fc    mov    ecx,DWORD PTR [ebp-0x4] 

Загружает второй аргумент функции в EDX. Это указатель на целевой массив:

4015da: 8b 55 0c    mov    edx,DWORD PTR [ebp+0xc] 

Добавляет значение ECX к EDX, вычисляя адрес элемента в целевом массиве:

4015dd: 01 ca     add    edx,ecx 

Загружает байт из исходного массива в EAX, расширяя его до 32 бит:

4015df: 0f b6 00     movzx  eax,BYTE PTR [eax] 

Сохраняет байт из AL в целевой массив:

4015e2: 88 02     mov    BYTE PTR [edx],al 

Увеличивает счетчик на 1:

4015e4: 83 45 fc 01    add    DWORD PTR [ebp-0x4],0x1 

Загружает текущее значение счетчика в EAX:

4015e8: 8b 45 fc     mov    eax,DWORD PTR [ebp-0x4] 

Сравнивает счетчик с третьим аргументом функции:

4015eb: 3b 45 10     cmp    eax,DWORD PTR [ebp+0x10] 

Если счетчик меньше третьего аргумента, переходит к метке 4015cf, продолжая цикл:

4015ee: 7c df    jl     4015cf <_copy_data+0xf> 
Объяснение дизассемблированного кода функции _main (004015f3)

Устанавливает значение 0x6c6c6548 по адресу esp+0x4e. Это часть строки Hello, World!:

401601: c7 44 24 4e 0x48656c6c  mov    DWORD PTR [esp+0x4e],0x6c6c6548 
Разбор значения 0x6c6c6548

Строка 0x6c6c6548 – это шестнадцатеричное значение, которое можно разбить на байты: 6c 6c 65 48

В формате little-endian (который используется в большинстве современных процессоров x86), байты хранятся в обратном порядке. Поэтому, когда мы интерпретируем это значение как строку в ASCII, мы должны читать его справа налево:

-  48 – это ASCII для символа 'H'

-  65 – это ASCII для символа 'e'

-  6c – это ASCII для символа 'l'

-  6c – это ASCII для символа 'l'

Таким образом, правильная интерпретация 0x6c6c6548 как строки в ASCII – это "lleH".

Поскольку в формате little-endian байты хранятся в обратном порядке, чтобы получить правильную строку, мы должны читать байты в обратном порядке: 48 65 6c 6c – это "Hell".

Строка 0x6c6c6548 в формате little-endian интерпретируется как строка "lleH". Если мы читаем байты в обратном порядке, то получаем "Hell", что является частью строки "Hello, World!".

Таким образом, 0x6c6c6548 соответствует первой части строки "Hello, World!", а именно "Hell".

Почему для адресации используется регистр ESP, а не EBP?

В ассемблере x86 адресация относительно регистров ESP (указатель стека) и EBP (базовый указатель) используется для доступа к данным в стеке. Выбор между ESP и EBP зависит от контекста и соглашений о вызовах, используемых в программе.

Адресация относительно EBP. Регистр EBP обычно используется как базовый указатель для доступа к локальным переменным и аргументам функции. Это стандартный подход в соглашениях о вызовах, таких как cdecl и stdcall, где EBP устанавливается в начале функции и используется для доступа к элементам стека относительно него.

Адресация относительно ESP. Регистр ESP указывает на вершину стека и изменяется при каждом push и pop вызове функции или возврате из функции. Использование ESP для адресации может быть менее распространено, так как его значение часто меняется, что делает код более сложным для чтения и анализа. Однако, в некоторых случаях, особенно в оптимизированном коде, компиляторы могут использовать ESP для адресации, если это позволяет избежать лишних операций с EBP.

Возможные причины использования ESP в нашем примере:

-  Оптимизация. Компилятор мог решить использовать ESP для адресации, чтобы избежать дополнительных операций по сохранению и восстановлению EBP. Это может быть частью стратегии оптимизации, направленной на уменьшение количества инструкций.

-  Контекст выполнения. В некоторых случаях, особенно в начале функции или после вызова другой функции, регистр ESP может быть в состоянии, которое позволяет использовать его для адресации без необходимости установки EBP.

-  Соглашение о вызовах. Определенные соглашения о вызовах или специфические для компилятора оптимизации могут предписывать использование ESP для адресации в определенных ситуациях.

Использование ESP для адресации в нашем примере, вероятно, связано с оптимизацией или специфическим контекстом выполнения. Компиляторы иногда принимают решения, которые могут показаться неочевидными, но которые на самом деле направлены на улучшение производительности или уменьшение размера кода.

Следующий байт, вероятно, является частью последовательности байтов, составляющих строку, и был выделен отдельно, возможно, из-за особенностей выравнивания или форматирования в дизассемблированном выводе:

401608: 6c 

Устанавливает значение 0x57202c6f по адресу esp+0x52. Это строка "o, W":

401609: c7 44 24 52 0x6f2c2057  mov    DWORD PTR [esp+0x52],0x57202c6f 

Устанавливает значение 0x646c726f по адресу esp+0x56. Это часть строки "orld":

401611: c7 44 24 56 0x6f726c64  mov    DWORD PTR [esp+0x56],0x646c726f 

Устанавливает значение 0x21 (символ '!') по адресу esp+0x5a:

401619: 66 c7 44 24 5a 0x2100    mov    WORD PTR [esp+0x5a],0x21 
Префикс размера операнда – 66

В дизассемблированном коде, когда вы видите, что инструкция начинается с префикса, такого как 66, это может указывать на использование префикса размера операнда (operand-size prefix).

В архитектуре x86 префикс 66 изменяет размер операнда инструкции. В 32-битном режиме он обычно используется для переключения между 16-битными и 32-битными операндами. В 32-битном коде префикс 66 может использоваться для выполнения 16-битной операции, когда по умолчанию ожидается 32-битная операция.

В 32-битном коде, если вы хотите выполнить 16-битную операцию, вы должны явно указать это с помощью префикса 66. Это может быть необходимо для совместимости или для работы с данными, которые имеют размер 16 бит.

В некоторых случаях, использование 16-битных операций может быть более эффективным или необходимым для работы с определенными типами данных или структурами.

В редких случаях, префикс 66 может появляться из-за ошибок компиляции или особенностей компилятора. Однако, в большинстве случаев, это сделано намеренно.

Ссылается на первый символ, то есть загружает адрес строки "Hello, World!" в EAX:

40161c: 8d 44 24 4e     lea    eax,[esp+0x4e] 
Чем отличается инструкция lea от mov?

Инструкция lea в ассемблере x86 используется для загрузки эффективного адреса в регистр.

Инструкция lea (load effective address, пер. загрузить эффективный адрес) – вычисляет адрес операнда-источника и помещает этот адрес в регистр-приёмник, не обращаясь к данным по этому адресу. Обычно используется для доступа к локальным переменным или данным в стековом фрейме.

Инструкция lea eax, [esp+0x4e] выполняет следующие действия: Вычисляет адрес, который находится на 0x4e байт выше текущего значения регистра ESP. Далее помещает вычисленный адрес в регистр eax (важно отметить, что инструкция lea не обращается к данным по этому адресу, а только вычисляет и загружает сам адрес).

Операция lea часто используется для вычисления адресов, которые затем могут быть использованы для доступа к данным. Это может быть полезно для выполнения арифметических операций с указателями без фактического доступа к данным.

Операция lea позволяет выполнять сложные вычисления с адресами, такие как умножение на константу или сложение с другими регистрами.

Поскольку lea не обращается к памяти, она может быть более эффективной, чем другие инструкции, которые выполняют аналогичные вычисления и доступ к памяти.

Сохраняет адрес строки в стеке как аргумент для функции _strlen:

401620: 89 04 24     mov    DWORD PTR [esp],eax 

Вызывает функцию _strlen для вычисления длины строки, результат сохраняется в регистре EAX:

401623: e8 f0 0f 00 00    call   40261c <_strlen> 

Увеличивает длину строки на 1, чтобы учесть символ завершения строки:

40162c: 83 c0 01     add    eax,0x1 

Сохраняет длину строки по адресу esp+0x5c:

40162f: 89 44 24 5c     mov    DWORD PTR [esp+0x5c],eax 

Загружает длину строки в EAX:

401633: 8b 44 24 5c     mov    eax,DWORD PTR [esp+0x5c] 

Сохраняет длину строки по адресу esp+0x8:

401637: 89 44 24 08    mov    DWORD PTR [esp+0x8],eax 

Загружает адрес буфера для копирования строки в EAX:

40163b: 8d 44 24 1c    lea    eax,[esp+0x1c] 

Сохраняет адрес буфера в стеке как второй аргумент для функции _copy_data:

40163f: 89 44 24 04    mov    DWORD PTR [esp+0x4],eax 

Загружает адрес исходной строки в EAX:

401643: 8d 44 24 4e    lea    eax,[esp+0x4e] 

Сохраняет адрес исходной строки в стеке как первый аргумент для функции _copy_data:

401647: 89 04 24     mov    DWORD PTR [esp],eax 

Вызывает функцию _copy_data для копирования строки:

40164a: e8 71 ff ff ff    call   4015c0 <_copy_data> 

В этом примере регистр EDI не используется явно. В ассемблере регистр EDI часто используется для хранения указателя на место назначения в операциях с памятью, таких как строковые операции. В более сложных программах, особенно в тех, которые выполняют операции с памятью, EDI может играть важную роль.

1.7. Регистр EBP (base pointer)

Кратко: назначение регистра EBP

-  Используется как указатель на базу текущего стекового фрейма. Для примера такой базы в реальном мире можно привести «Высоту над уровнем моря». Только вместо расстояния по вертикали от объекта до среднего уровня поверхности моря указывается смещение адреса. То есть это нулевая точка, от которой мы начинаем отсчёт.

-  Обычно указывает на начало текущего стекового фрейма.

1.8. Регистр ESP (stack pointer)

Кратко: назначение регистра ESP

-  Указывает на вершину стека.

-  Автоматически изменяется при операциях push и pop.

Регистр ESP (stack pointer) в архитектуре x86 играет центральную роль в управлении стеком, выполняя операции по выделению и освобождению памяти для локальных переменных, аргументов функций и адресов возврата.

Немного теории: назначение и использование регистра ESP

Указатель стека

Регистр ESP всегда указывает на вершину стека, которая является самым нижним адресом в стеке (в контексте убывающего стека, как в x86).

Он автоматически изменяется при выполнении операций со стеком, таких как push, pop, вызовы функций и возвраты из них.

Операции со стеком
  • Операция push: уменьшает значение ESP на размер операнда (например, 4 байта для push eax) и сохраняет значение в стеке.
  • Операция pop: извлекает значение из стека и увеличивает ESP на размер операнда.
  • Операция call: помещает адрес возврата в стек и уменьшает ESP.
  • Операция ret: извлекает адрес возврата из стека и увеличивает ESP.
Доступ к данным в стеке

Хотя EBP обычно используется для доступа к локальным переменным и аргументам, ESP также может использоваться для доступа к данным в стеке, особенно в оптимизированном коде или в начале/конце функций.

Регистр ESP является динамическим и постоянно изменяется при выполнении операций со стеком. Он используется для управления стеком и доступа к данным в стеке, особенно в тех случаях, когда EBP не используется или не подходит. В дизассемблированном коде ESP, как вы видели в предыдущих примерах, часто встречается в операциях, связанных с вызовом функций, передачей аргументов и управлением локальными переменными.

2. Сегментные регистры (segment registers)

2.1. Регистр CS (code segment)

Кратко: назначение регистра CS

-  Указывает на сегмент кода, содержащий исполняемые инструкции.

Когда вы занимаетесь реверс-инжинирингом, важно понимать, как различные регистры используются в ассемблере. Регистр CS (code segment) в архитектуре x86 является сегментным регистром, который указывает на сегмент кода, содержащий исполняемые инструкции.

В современных 32- и 64-битных операционных системах, работающих в защищенном режиме, прямой доступ к сегментным регистрам, таким CS, ограничен, и они управляются операционной системой.

Однако, вы можете увидеть использование CS в определенных контекстах, например, при переключении между режимами процессора или в некоторых низкоуровневых операциях.

2.2. Регистр DS (data segment)

Кратко: назначение регистра DS

-  Указывает на сегмент данных, содержащий глобальные и статические переменные.

Регистр DS (data segment) в архитектуре x86 является сегментным регистром, который указывает на сегмент данных, содержащий переменные и другие данные, используемые программой. В современных 32- и 64-битных операционных системах, работающих в защищенном режиме, прямой доступ к сегментным регистрам, таким как DS, ограничен, и они управляются операционной системой.

2.3. Регистр SS (stack segment)

Кратко: назначение регистра SS

-  Указывает на сегмент стека, содержащий стековые данные.

Регистр SS (stack Segment) в архитектуре x86 является сегментным регистром, который указывает на сегмент стека, используемый для хранения локальных переменных, адресов возврата и других временных данных.

В современных 32- и 64-битных операционных системах, работающих в защищенном режиме, прямой доступ к сегментным регистрам, таким как SS, ограничен, и они управляются операционной системой.

2.4. Регистры ES, FS, GS (extra segment registers)

Кратко: назначение регистров ES, FS, GS

-  Дополнительные сегментные регистры, которые могут использоваться для различных целей, таких как указание на дополнительные сегменты данных.

В архитектуре x86 сегментные регистры ES, FS и GS используются для указания на дополнительные сегменты данных. В современных 32- и 64-битных операционных системах, работающих в защищенном режиме, прямой доступ к этим сегментным регистрам ограничен, и они управляются операционной системой. Однако, они могут использоваться в определенных контекстах, например, для специальных целей, таких как хранение информации о потоке или для доступа к специфическим данным.

3. Регистры управления и состояния

3.1. Регистр EIP (instruction pointer)

Кратко: назначение регистра EIP

-  Указывает на следующую инструкцию, которая будет выполнена.

-  Программно не доступен напрямую; изменяется при выполнении инструкций управления потоком.

Когда вы занимаетесь реверс-инжинирингом, важно понимать, как различные регистры используются в ассемблере. Регистр EIP (instruction pointer) в архитектуре x86 является специальным регистром, который указывает на адрес следующей инструкции, которая будет выполнена процессором.

В отличие от других регистров общего назначения, таких как EAX, EBX и т.д., прямой доступ к EIP из программного кода ограничен, и он управляется неявно при выполнении инструкций, таких как call, ret, jmp и условных переходах.

3.2. Регистр EFLAGS (flags register)

Кратко: назначение регистра EFLAGS

-  Содержит флаги, которые отражают состояние процессора и результаты операций.

В архитектуре x86 регистр EFLAGS (и его 64-битный аналог RFLAGS) содержит набор флагов, которые отслеживают состояние процессора и результаты выполнения арифметических и логических операций. Эти флаги используются для управления потоком выполнения программы и для принятия решений на основе результатов операций. Рассмотрим основные флаги, которые можно встретить в дизассемблированном коде.

Основные флаги в регистре EFLAGS

Флаги состояния (status flags)
Флаг CF (carry flag)

-  Устанавливается, если при выполнении арифметической операции произошел перенос из старшего бита.

-  Пример: при сложении 0xFFFFFFFF + 1 установится CF, так как произошел перенос.

Флаг PF (parity flag)

-  Устанавливается, если младший байт результата имеет четное количество единичных битов.

-  Используется для проверки четности.

Флаг AF (auxiliary carry flag)

-  Устанавливается, если при выполнении операции произошел перенос из бита 3 в бит 4.

-  Используется для двоично-десятичной арифметики (BCD).

Флаг ZF (zero flag)

-  Устанавливается, если результат операции равен нулю.

-  Пример: при вычитании 5 - 5 установится ZF.

Флаг SF (sign flag)

-  Устанавливается в соответствии со знаком результата (1 для отрицательных чисел, 0 для положительных).

-  В дополнительном коде SF отражает знак результата.

Флаг OF (overflow flag)

-  Устанавливается, если при выполнении операции произошло переполнение для чисел со знаком.

-  Пример: при сложении 0x7FFFFFFF + 1 установится OF, так как результат выходит за пределы диапазона для 32-битных чисел со знаком.

Флаги управления (control flags)
Флаг DF (direction flag)

-  Управляет направлением обработки строк в строковых операциях.

-  Если DF установлен, указатели ESI/EDI или RSI/RDI уменьшаются после каждой операции.

-  Если DF сброшен, указатели увеличиваются.

Флаг IF (interrupt enable flag)

-  Управляет обработкой прерываний.

-  Если IF установлен, процессор обрабатывает внешние прерывания.

Флаг TF (trap flag)

-  Включает пошаговый режим выполнения (трассировку).

-  Если TF установлен, после каждой инструкции процессор генерирует исключение, что позволяет отлаживать программу.

Системные флаги (system flags)

Эти флаги используются для управления режимами работы процессора и защиты, и обычно не изменяются в пользовательском коде.

Примеры использования в дизассемблированном коде

Проверка на ноль
assembly
cmp eax, ebx
jz  label1          ; Переход, если ZF установлен (eax == ebx) 
Проверка на переполнение
assembly
add eax, ecx
jo  overflow_handler ; Переход, если OF установлен (произошло переполнение) 
Проверка на перенос
assembly
add eax, ecx
jc  carry_handler    ; Переход, если CF установлен (произошел перенос) 
Управление направлением
assembly
cld                  ; Очистка DF (увеличение указателей)
std                  ; Установка DF (уменьшение указателей) 
Пошаговый режим
assembly
pushfd               ; Сохраняем текущие флаги
pop eax              ; Загружаем флаги в eax
or  eax, 0x100       ; Устанавливаем TF (Trap Flag)
push eax             ; Сохраняем обратно в стек флагов
popfd                ; Загружаем флаги обратно 

Флаги в регистре EFLAGS играют ключевую роль в управлении потоком выполнения программы и в обработке результатов операций. Они позволяют программистам и компиляторам принимать решения на основе состояния процессора, таких как равенство, переполнение, перенос и другие условия.

4. Другие регистры

4.1. Control registers (CR0, CR2, CR3, CR4)

Кратко

Управляют различными аспектами работы процессора, такими как управление памятью и защита.

В архитектуре x86 управляющие регистры, такие как CR0, CR2, CR3 и CR4, играют ключевую роль в управлении работой процессора и операционной системы. Эти регистры управляют такими аспектами, как включение защищенного режима, управление памятью, кэширование и другие.

Прямой доступ к управляющим регистрам из программного кода на языке высокого уровня, таком как C, ограничен и обычно требует привилегий уровня ядра (kernel mode). Однако, в некоторых случаях, например, при написании драйверов или встроенного кода, может потребоваться доступ к этим регистрам.

4.2. Debug registers (DR0-DR7)

Кратко

Используются для отладки, позволяя устанавливать точки останова и контролировать выполнение программы.

В архитектуре x86 отладочные регистры DR0-DR7 используются для управления аппаратными точками останова (breakpoints) и другими отладочными функциями. Эти регистры позволяют процессору отслеживать доступ к определенным адресам памяти или выполнение инструкций, что полезно для отладки и анализа программ.

Прямой доступ к отладочным регистрам из программного кода на языке высокого уровня, таком как C, ограничен и обычно требует привилегий уровня ядра (kernel mode). Это связано с тем, что неправильное использование отладочных регистров может привести к нестабильной работе системы или уязвимостям безопасности.

4.3. MMX registers (MM0-MM7)

Кратко

Используются для операций SIMD (Single Instruction, Multiple Data) в MMX технологии.

Регистры MMX (MM0-MM7) были введены в архитектуру x86 для ускорения мультимедийных операций, таких как обработка изображений, аудио и видео. Эти регистры имеют ширину 64 бита и могут использоваться для выполнения операций с упакованными целыми числами.

4.4. XMM registers (XMM0-XMM7)

Кратко

Используются для операций SSE (Streaming SIMD Extensions).

Регистры XMM (XMM0-XMM7 в 32-битной архитектуре и XMM0-XMM15 в 64-битной) используются для выполнения операций с упакованными данными, такими как векторные вычисления, обработка изображений и аудио. Эти регистры имеют ширину 128 бит и могут использоваться для выполнения операций с упакованными целыми числами и числами с плавающей запятой.

Заключение

В этой статье мы глубоко погрузились в ключевые аспекты архитектуры процессора и инструменты, которые позволяют разработчикам и исследователям понимать и управлять работой программ на низком уровне. Понимание этих концепций является фундаментальным для тех, кто стремится к мастерству в области системного программирования, реверс-инжиниринга и оптимизации производительности.

Общие регистры процессора: основные инструменты вычислений

Мы начали с изучения общих регистров процессора, таких как EAX, EBX, ECX, EDX, ESP, EBP, ESI и EDI в архитектуре x86, а также их 64-битные аналоги в x86-64. Эти регистры служат основными инструментами для выполнения арифметических операций, хранения данных и управления потоком выполнения программы. Понимание их роли и взаимодействия позволяет разработчикам писать более эффективный код и оптимизировать использование ресурсов процессора.

Сегментные регистры: организация памяти и ее защита

Далее мы рассмотрели сегментные регистры (CS, DS, SS, ES, FS, GS), которые играют критическую роль в управлении памятью и обеспечении безопасности в архитектуре x86. Эти регистры определяют, как программы обращаются к различным сегментам памяти, таким как код, данные и стек. Осознание их функций позволяет лучше понять, как операционные системы и приложения организуют и защищают память, что особенно важно в контексте многозадачности и многопоточности.

Регистры управления и состояния: контроль и мониторинг

Мы также обсудили регистры управления и состояния, такие как EIP и EFLAGS. Регистр EIP, или указатель инструкций, управляет потоком выполнения, указывая на следующую инструкцию для выполнения. Регистр EFLAGS отслеживает состояние процессора и результаты выполнения инструкций, что позволяет управлять условными операциями и обработкой исключений. Эти регистры являются неотъемлемой частью механизмов управления и мониторинга, обеспечивая надежное и предсказуемое выполнение программ.

Компиляция и дизассемблирование: путь от исходного к машинному коду

Центральное место в нашем обсуждении заняли процессы компиляции и дизассемблирования.

Компиляция – это преобразование исходного кода, написанного на языках высокого уровня, в машинный код, который может быть выполнен процессором. Мы рассмотрели роль компилятора GCC и его возможности, включая оптимизацию и поддержку различных языков программирования. Понимание того, как компилятор трансформирует код, позволяет разработчикам писать более эффективные программы и использовать расширенные возможности компилятора для оптимизации.

С другой стороны, дизассемблирование – это процесс обратного преобразования машинного кода в ассемблерный код, что позволяет анализировать и понимать, как работает программа на низком уровне. Инструменты, такие как objdump, предоставляют мощные возможности для анализа объектных и исполняемых файлов, включая дизассемблирование, отображение символов и информации о секциях. Эти инструменты являются незаменимыми для отладки, реверс-инжиниринга и анализа программного обеспечения.

Итог

В совокупности, знание общих и сегментных регистров, а также регистров управления и состояния, позволяет глубже понять внутреннее устройство процессора и принципы работы программ. Компиляция и дизассемблирование – это две стороны одной медали, которые позволяют переходить от высокоуровневого кода к машинному и обратно, обеспечивая полное понимание программного обеспечения. Эти знания и навыки являются основой для уверенного реверс-инжиниринга программ.