Введение
В современном мире программирования и разработки программного обеспечения, понимание внутреннего устройства компьютерных систем является ключевым фактором для создания эффективных, надежных и оптимизированных приложений. Для нас, начинающих исследователей и будущих реверс-инженеров, важно хотя бы на первых порах не испугаться дизассемблированного кода. В этой статье будут рассмотрены основные регистры процессора архитектуры 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
Простая компиляция одной командой
Для простых программ можно использовать одну команду, которая выполнит все этапы:
gcc source.c -o executable
Пример компиляции
Предположим, у вас есть файл hello.c
со следующим содержимым:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
Чтобы скомпилировать этот файл в исполняемый файл hello
, выполните команду:
gcc -m32 -O0 -g -o hello hello.c
Мы будем использовать следующие дополнительные флаги:
-m32
: компилирует 32-битную версию программы;-O0
: отключает оптимизацию;-g
: включает отладочную информацию
Затем вы можете запустить программу:
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
и другие. - Отображение символов. Вывод списка символов (функций и переменных) и их адресов.
- Отображение заголовков. Информация о заголовках файла, таких как архитектура, точка входа и т.д.
- Другие Функции. Включая отображение информации о перемещениях, динамических символах и т.д.
Процесс дизассемблирования
Для дизассемблирования примеров будем использовать команду следующего вида:
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
со следующим содержимым:
#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
)
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. Доступ к глобальной переменной
.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. Работа с массивом
.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. Работа с указателями и арифметикой
.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. Доступ к элементам массива
.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. Работа со структурами
.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. Индексная адресация
.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
со следующим содержимым:
#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
)
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
используется в машинном коде.
#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
.
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 со следующим содержимым:
#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
)
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
в соответствии с его назначением — как счетчик в циклах и для операций сдвига.
#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
.
; Предполагаемый дизассемблированный код для функции 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
со следующим содержимым:
#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
)
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
в EDX
(в EDX
теперь находится значение переменной 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
для копирования строки
Предположим, мы хотим скопировать строку из одного места в памяти в другое.
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
для загрузки данных
Предположим, мы хотим загрузить данные из массива в регистр.
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
для сравнения строк
Предположим, мы хотим сравнить две строки.
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
со следующим содержимым:
#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
)
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
со следующим содержимым:
#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
)
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)
Эти флаги используются для управления режимами работы процессора и защиты, и обычно не изменяются в пользовательском коде.
Примеры использования в дизассемблированном коде
Проверка на ноль
cmp eax, ebx
jz label1 ; Переход, если ZF установлен (eax == ebx)
Проверка на переполнение
add eax, ecx
jo overflow_handler ; Переход, если OF установлен (произошло переполнение)
Проверка на перенос
add eax, ecx
jc carry_handler ; Переход, если CF установлен (произошел перенос)
Управление направлением
cld ; Очистка DF (увеличение указателей)
std ; Установка DF (уменьшение указателей)
Пошаговый режим
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
, предоставляют мощные возможности для анализа объектных и исполняемых файлов, включая дизассемблирование, отображение символов и информации о секциях. Эти инструменты являются незаменимыми для отладки, реверс-инжиниринга и анализа программного обеспечения.
Итог
В совокупности, знание общих и сегментных регистров, а также регистров управления и состояния, позволяет глубже понять внутреннее устройство процессора и принципы работы программ. Компиляция и дизассемблирование – это две стороны одной медали, которые позволяют переходить от высокоуровневого кода к машинному и обратно, обеспечивая полное понимание программного обеспечения. Эти знания и навыки являются основой для уверенного реверс-инжиниринга программ.