![]() |
Введение в программирование на ассемблере |
![]() |
Регистры процессора
Регистры процессора – быстродействующая оперативная память, физически расположенная внутри чипа. Процесс выполнения любой программы на компьютере заключается в чтении и записи информации из регистров в ОЗУ и выполнении различных операций (арифметических, логических, векторных), операндами которых являются регистры. Размер регистров, их имена и количество зависит от конкретной архитектуры процессора.
Так, процессоры 32-битной архитектуры Intel (i386) содержат следующие регистры:
- Регистры общего назначения EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP. Каждый из них имеет размер 4 байта. К младшей двухбайтовой части этих регистров также можно обратиться, опустив “Е” в названии. В свою очередь, к старшим и младшим байтам двухбайтовой части можно обратиться, заменив “X” на “H” или “L” соответственно;
- Специальный регистр EIP, содержащий адрес текущей инструкции. Имеет размер 4 байта. Значение этого регистра нельзя изменить напрямую;
- Специальный регистр EFLAGS, представляющий собой набор 1-битовых ячеек (флагов). Также имеет размер 4 байта. Как и в случае с регистром EIP, значения отдельных флагов нельзя изменять напрямую. Подробный состав этого регистра будет приведен позднее.
Состав регистров 64-битной архитектуры Intel (x86_64, amd64) представлен следующими регистрами:
- Регистры общего назначения RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP расширяют соответствующие им 32-битные регистры до 8 байт, причем регистр EAX является младшей частью регистра RAX, подобно тому как AX является младшей частью EAX;
- Дополнительные регистры общего назначения R8, R9, R10, R11, R12, R13, R14, R15. Также имеют размер 8 байт;
- Специальный регистр RIP, расширяющий EIP;
- Регистр флагов RFLAGS.
Иллюстрация расширения регистров на примере регистра RAX представлена ниже:
RAX (8 байт) |
|||||||
EAX (4 байта) |
|||||||
AX (2 байта) |
|||||||
AH (1 б.) |
AL (1 б.) |
Язык ассемблера
Существует множество реализаций компиляторов ассемблера (которые тоже называются ассемблерами), каждая из которых обладает отличительными особенностями синтаксиса и принципами работы. Однако, основная задача, которую решает любой ассемблер – трансляция мнемоник команд (т.е. их текстового представления) в двоичный код, понятный процессору. Гипотетически, программист может сразу писать код в двоичном виде, но для человека представляется проблематичным помнить конкретные коды всех команд процессора (включая их варианты, в зависимости от типа операнда).
Примером ассемблирования может быть инструкция
add eax, 1
добавляющая к значению регистра EAX единицу, и соответствующая ей инструкция
машинного кода 83 С0 01
(в шестнадцатеричном виде).
Программа на ассемблере, в общем случае, представляет собой последовательность
подобных инструкций. Каждая инструкция может иметь различные режимы работы, в
зависимости от типов операндов. Например, приведенная выше инструкция ADD
складывает значение регистра с константой, в то время как инструкция
add eax, ebx
прибавит к регистру EAX значение регистра EBX.
Полный список поддерживаемых процессором/ассемблером инструкций следует искать в соответствующей документации. В данной лабораторной работе будут приведены лишь инструкции, необходимые для совершения базовых операций.
Современные ассемблеры:
- MASM (Microsoft Assembler). Поддерживает 32- и 64-битные процессоры, поставляется вместе с Visual C++ в составе Visual Studio. Работает только на ОС Windows;
- FASM (Flat Assembler). Кроссплатформенный ассемблер с открытым исходным кодом, поддерживающий Unix-подобные ОС и Windows. Не требует линкера (компоновщика) для создания программ. Для Windows имеет графический редактор кода;
- GAS (GNU Assembler). Ассемблер, используемый проектом GNU и компилятором GCC. Поддерживает широкое множество аппаратных платформ и ОС;
- NASM (Netwide Assembler). Кроссплатформенный ассемблер с открытым исходным кодом. Поддерживает Windows и Unix-подобные ОС.
Рассмотрим инструкции, которые понадобятся для выполнения данной лабораторной работы.
add операнд1, операнд2
– выполняет арифметическое сложение двух операндов. Результат сохраняется в операнд1;sub операнд1, операнд2
– вычитает операнд2 из операнда1. Результат сохраняется в операнд1;inc операнд
– увеличивает операнд на единицу;dec операнд
– уменьшает операнд на единицу;neg операнд
– умножает операнд на -1, т.е. меняет его знак;mov операнд1, операнд2
– копирует операнд2 в операнд1;mul операнд
– умножает значение регистра EAX на значение операнда, причем операндом в данной инструкции может быть только регистр. Результат умножения (8-байтовое число) сохраняется в регистрах EDX и EAX;div операнд
– делит 8-байтовое значение регистров EDX:EAX (значение этих регистров интерпретируется как одно число) на значение операнда. Как и в случае с mul, операнд должен быть регистром. Результат вычисления сохраняется в два регистра: EDX (остаток от деления) и EAX (неполное частное).
Необходимо отметить, что в инструкциях mov, add и sub хотя бы один
из операндов должен являться регистром. Например, инструкция mov 123, 456
не
будет распознана ассемблером.
Дизассемблером называется программа, производящая операцию, обратную ассемблированию, т.е. транслирующая двоичный код в текстовое представление. Чаще всего, дизассемблеры не реализуются в виде отдельной программы, а являются частью отладчика.
Работа с памятью в ассемблере
В отличие от регистров, количество и размер которых фиксировано для конкретного процессора, оперативная память компьютера может иметь различные объемы. По этой причине, для обращения к ячейкам памяти используются не имена, а адреса, представляющие собой обычные числа (записываемые, как правило, в шестнадцатеричной системе счисления).
Для каждой программы, выполняемой на компьютере, операционная система создает виртуальное адресное пространство, начинающееся с адреса 0. Конечный адрес этого пространства зависит от многих факторов, но главным его ограничителем является разрядность процессора. В 32-битных процессорах каждый регистр может хранить до 4 байт информации, т.е. 232 различных значений. Это означает, в том числе, что такой процессор не сможет адресовать более чем 4 Гб памяти просто потому что адрес ячейки памяти не может быть длиннее 32 бит.
Адресное пространство программы разделяется на различные области, в зависимости от их функционального назначения. Например, при запуске программы, загрузчик ОС располагает код программы в определенном сегменте памяти – сегменте кода. Программа может содержать глобальные переменные и константы, и под эту информацию так же выделяется участок памяти – сегмент данных. В процессе выполнения программы она может запрашивать и освобождать память под собственные нужды, и эта память выделяется из области, называемой кучей (heap). Наконец, для хранения локальных переменных, а так же для передачи параметров в процедуры используется область памяти, называемая стеком (stack).
Программа имеет возможность обратиться к любой ячейке памяти, однако не всякая такая попытка будет успешной. Например, попытка прочитать информацию из ячейки с адресом 0 неизбежно приведёт к ошибке выполнения и остановке программы. Следующий код дает пример чтения и записи ячеек памяти в ассемблере:
mov eax, 1 ; Непосредственная адресация
mov eax, [1] ; Прямая абсолютная адресация
mov ebx, eax ; Прямая регистровая адресация
mov eax, [ebx] ; Косвенная регистровая адресация
В первой строке не производится никакого доступа к памяти: в регистр EAX записывается константа 1, и это называется непосредственной адресацией.
Во второй строке квадратные скобки вокруг единицы означают, что вместо записи в EAX значения 1, туда следует записать содержимое ячейки памяти с адресом 1. Это называется прямой абсолютной адресацией, т. к. адрес-операнд является абсолютным.
При прямой регистровой адресации в третьей строке значение не задано константой, а берётся из определенного регистра (в данном случае, EAX). В результате выполнения этой строки в регистре EBX так же окажется единица.
Наконец, последняя строка показывает, что такой синтаксис применим не только к константам, но и к регистрам. В этом случае, значение регистра EBX используется как адрес ячейки памяти, из которой требуется произвести чтение. Такой вид адресации называется косвенным регистровым. При использовании косвенной адресации, в квадратных скобках можно производить арифметические операции, имеющие своими операндами регистры и константы:
mov eax, [ebx+5]
Синтаксис объявления глобальных переменных в ассемблере имеет следующий вид:
имя_переменной тип значение
Ограничения на допустимые имена переменных зависят от конкретного ассемблера. В общем случае имя должно состоять из латинских букв и цифр, и начинаться с буквы.
Тип переменной может быть:
- db (1 байт);
- dw (2 байта);
- dd (4 байта);
- dq (8 байт).
В качестве начального значения могут быть использованы:
- Знак “?”, который означает, что переменную можно не инициализировать. Это приведёт к тому, что её значение в момент выполнения программы будет произвольным;
- Числовая константа. Десятичные числа записываются как есть, к
шестнадцатеричным числам необходимо добавлять символ
h
; - Строковая константа. Строки обрамляются двойными или одинарными кавычками. Во время выполнения переменная будет содержать ASCII-код, соответствующий константе.
Примеры объявления глобальных переменных:
db 0
a dd 0,1,2
b dd 3 dup(0)
bb db 'a','b','c' с
В первой строке под переменную a будет выделен 1 байт (ключевое слово
db), который будет инициализирован нулем. Во второй строке будет выделено 3
участка памяти по 4 байта каждый (ключевое слово dd), которые будут
последовательно инициализированы числами 0, 1 и 2. Третья строка эквивалентна
второй с тем лишь отличием, что память будет инициализирована нулями.
Конструкция n dup (x)
соответствует записи вида x, x, x, ...
из n
элементов. Наконец, последняя строка инициализирует 3 байта ASCII-кодами букв
“a”, “b” и “c” соответственно.
Имя объявленной переменной может использоваться как операнд-константа в любой инструкции программы. Значением такого операнда является адрес ячейки памяти, которая была выделена под соответсвующую переменную. Таким образом, чтобы получить доступ к значению, хранящемуся в переменной, необходимо использовать прямую (абсолютную или косвенную) адресацию:
dd 123
a
mov eax, a ; Загружаем в eax адрес переменной a
mov ebx, [a] ; Загружаем в ebx значение переменной a
Важно отметить, что при загрузке значений из памяти, размеры операндов должны совпадать. Например, следующий код вызовет ошибку компиляции:
db 0
a
mov eax, [a]
Здесь a
имеет размер 1 байт, в то время как eax
имеет размер 4 байта. Можно
предложить несколько решений этой проблемы:
- Изменить тип переменной на
dd
; - Загрузить значение в 1-байтовый регистр:
mov al, [a]
; - Загрузить из указанного адреса 4 байта, вместо одного:
mov eax, dword [a]
. Ключевое словоdword
в данном случае заставляет ассемблер считатьa
адресом 4-байтовой переменной, т.е. как будто она была объявлена с типомdd
. Данный подход заключает в себе некоторую опасность, т.к. помимо запрошенного байта по адресуa
будут прочитаны еще 3 байта:a+1
,a+2
иa+3
. Это может привести к разнообразным негативным последствиям, поэтому в общем случае использовать такой способ не рекомендуется.
Отладчик
Отладчиком называется программа, позволяющая выполнять другую программу пошагово (т.е. инструкция за инструкцией). Между выполнением отдельных инструкций отлаживаемая программа находится “на паузе”. В этот момент отладчик предоставляет возможность просмотреть состояние регистров и памяти, изменять их значения, а так же изменять непосредственно код выполняемой программы.
Как и ассемблеры, отладчики представлены различными реализациями, отличающимися функционалом и типами поддерживаемой операционной системы и исполняемых файлов. Современные отладчики:
- GDB (консольный, Unix-подобные ОС);
- LLDB (консольный, кроссплатформенный);
- OllyDbg (графический, Windows, поддерживает отладку только 32-битного кода);
- x64dbg (графический, Windows, 32- и 64-битный код).
Структура окна графического отладчика x64dbg представлена на рисунке 1.

Здесь
- Окно регистров. Позволяет просматривать и изменять текущие значения регистров процессора;
- Окно дизассемблированного кода программы. Выводит машинный код отлаживаемой программы (в шестнадцатеричном виде) и результат его дизассемблирования. Подсвечивает инструкцию, которая будет выполнена на следующем шаге;
- Окно дампа памяти. Позволяет просматривать и изменять содержимое оперативной памяти компьютера;
- Окно стека. Выполняет ту же роль, что и окно дампа для стека – особой области памяти, активно используемой в процессе выполнения программы.
Более детальный вид окна 2:

Здесь изображены:
- Столбец, содержащий адрес инструкции в шестнадцатеричном виде;
- Машинный код инструкции;
- Дизассемблированная инструкция;
- Дополнительная информация о выделенной инструкции. Например, для инструкций, операнды которых содержат адреса ячеек памяти, это окно выводит содержимое соответствующих ячеек.
Как уже было сказано ранее, отладчик позволяет изменять код программы “на лету”. Для этого в области 3 необходимо выделить инструкцию, которую нужно изменить, а после этого нажать “пробел”. Во всплывающем окошке вводится новая инструкция, которая перезапишет собой текущую. Стоит отметить, что различные инструкции имеют различную длину в байтах, поэтому заменяя 1-байтовую инструкцию, скажем, 2-байтовой, отладчик перезапишет и инструкцию следующую за ней.
Отладка программы
Отладка программы начинается с загрузки ее в отладчик. В случае графических отладчиков (OllyDbg и x64dbg), это делается через меню “File”→“Open”. Сразу после запуска отладчик остановит программу на ближайшей точке останова, которая, в зависимости от его настроек, может быть
- непосредственно точкой входа (entry point) в программу, т.е. самой первой ее инструкцией;
- обработчиком исключительной ситуации (exception), возникшей при запуске программы. Примером такой ошибки может быть отсутствие необходимой DLL-библиотеки;
- Системным событием. Эти точки останова активизируются в зависимости от настроек отладчика. Примером такой точки останова может быть загрузка очередной DLL-библиотеки в адресное пространство процесса.
Остановленная программа может быть выполнена пошагово с заходом в процедуры (Step Into), пошагово без захода в процедуры (Step Over), выполнена до ближайшего выхода из текущей процедуры (Execute Till Return), а так же просто продолжена (Continue). В последнем случае программа будет выполняться до тех пор, пока управление не дойдет до одной из точек останова, или до завершения программы, или пока не будет остановлена вручную кнопкой Break. Изменившиеся значения регистров, стека и памяти между двумя паузами отладчик помечает красным цветом.
Во время паузы выполнения программы пользователь может:
- Изменять значения регистров общего назначения и регистра FLAGS;
- Изменять значения ячеек памяти (как в окне дампа, так и в окне стека);
- Изменять код выполняемой программы в окне CPU.
Контрольные вопросы
- Что такое регистры процессора? Каковы их виды?
- Что такое ассемблер?
- Что такое отладчик?
Задание на лабораторную работу
- Установить ассемблер FASM (http://flatassembler.net/) и отладчик x64dbg (http://x64dbg.com/).
- Написать и скомпилировать программу, соответствующую своему варианту. Простейший пример программы на FASM приведен в конце лабораторной работы.
- Загрузить скомпилированную программу в отладчик и убедиться в ее правильном функционировании.
Варианты заданий
В формулировках ниже слова “программно инициализировать переменную” означают, что необходимо написать код, заполняющий эти переменные в соответствии с заданием.
В заданиях, требующих произвести действия над значениями, удовлетворяющими какому-либо условию, не требуется применять инструкции ветвления и организовывать циклы.
Вариант 1
- Вычислить арифметическое выражение \(a*b+a*(c+d)+d*(-1)\). Переменные
a
,b
,c
,d
в начале выполнения программы должны быть расположены в регистрахeax
,ebx
,ecx
иedx
соответственно. Численные значения можно взять произвольными. - Создать неинициализированную глобальную переменную размером в 4 байта,
программно инициализировать ее таким образом, чтобы в окне дампа можно было
увидеть
AA BB CC DD
.
Вариант 2
- Вычислить арифметическое выражение \(a+(b^2+1)⁄(b-5)\). Переменные
a
иb
в начале выполнения программы должны быть расположены в регистрахesi
иedi
соответственно. Численные значения можно взять произвольными. - Создать глобальную переменную, содержащую строку “Labs” и другую глобальную переменную такого же размера. Написать код, копирующий содержимое первой переменной во вторую.
Вариант 3
- Вычислить арифметическое выражение \(a*a+b\). Переменные
a
иb
в начале выполнения программы должны быть расположены в регистрахeax
иebx
соответственно, а их численные значения должны быть равны65535
и131072
. - Выделить участок памяти размером в 8 байт и программно инициализировать
его таким образом, чтобы в окне дампа можно было бы увидеть значения вида
00 01 00 01 ...
.
Вариант 4
- Вычислить арифметическое выражение \((a+4*(b*c)+d^3)⁄2\). Переменные
a
,b
,c
,d
в начале выполнения программы должны быть расположены в регистрахeax
,ebx
,ecx
иedx
соответственно. Численные значения можно взять произвольными. - Создать глобальную переменную, инициализированную 8 целыми числами (как минимум 3 четных числа) и другую глобальную переменную такого же размера. Написать код, копирующий четные числа из первой переменной во вторую.
Вариант 5
- В рамках одной программы вычислить арифметические выражения \(a*b*(-1)\) и
\(c^2+d^3-100\). Переменные
a
,b
,c
,d
в начале выполнения программы должны быть расположены в регистрахeax
,ebx
,ecx
иedx
соответственно. Численные значения можно взять произвольными. - Выделить участок памяти размером в 16 байт и программно инициализировать
его таким образом, чтобы в окне дампа можно было бы увидеть значения вида
01 00 01 00 …
.
Вариант 6
- Вычислить арифметическое выражение \(a^2 ⁄ 5+b*c-a⁄b\). Переменные
a
,b
,c
в начале выполнения программы должны быть расположены в регистрахeax
,edx
иedi
соответственно. Численные значения можно взять произвольными. - Создать глобальную переменную, содержащую строку “Hello”. Написать код, модифицирующий содержимое этой переменной в слово “World” путем арифметических операций.
Вариант 7
- Вычислить арифметическое выражение \((a*b*c-a*c*d)⁄3\). Переменные
a
,b
,c
,d
в начале выполнения программы должны быть расположены в регистрахeax
,ebx
,ecx
иedx
соответственно. Численные значения можно взять произвольными. - Создать глобальную переменную, которая содержит 10 участков памяти по 2 байта и инициализировать ее произвольными числами. Поменять местами первое и последнее число, а остальные сделать отрицательными.
Вариант 8
- Вычислить арифметическое выражение \((a^2 * b^2) ⁄ 10 + 5\). Переменные
a
,b
в начале выполнения программы должны быть расположены в регистрахesi
иedi
соответственно. Численные значения можно взять произвольными. - Создать глобальную переменную, содержащую строку “ReversE” и другую глобальную переменную такого же размера. Написать код, копирующий содержимое первой переменной во вторую, инвертируя порядок символов.
Вариант 9
- Вычислить арифметическое выражение \((a*b)⁄(a^3 + 10)\). Переменные
a
,b
в начале выполнения программы должны быть расположены в регистрахesi
иedi
соответственно. Численные значенияa
иb
можно взять произвольными. - Создать глобальную переменную, которая содержит 8 участков памяти по 4 байта и заполнить ее произвольными числами. Поменять местами числа на четных и нечетных позициях.
Вариант 10
- Вычислить арифметическое выражение \((a⁄b - (c+2)⁄d)*a^2\). Переменные
a
,b
,c
,d
в начале выполнения программы должны быть расположены в регистрахeax
,ebx
,ecx
иedx
соответственно. Численные значения можно взять произвольными. - Объявить неинициализированный участок памяти размером в 5 байт, программно
инициализировать его таким образом, чтобы в окне дампа можно было увидеть
FE 00 DC 11 BA
.
Вариант 11
- Вычислить арифметическое выражение \((a-8)⁄(b+15)-2⁄3\). Переменные
a
,b
в начале выполнения программы должны быть расположены в регистрахeax
,ebx
соответственно. Численные значения можно взять произвольными. - Создать 2 глобальные переменные, содержащие 4 участка памяти по 1 байту и инициализировать их произвольными числами. Создать еще одну глобальную переменную, в которую программно поместить суммы элементов созданных переменных.
Вариант 12
- В рамках одной программы вычислить арифметические выражения \((-a)⁄(b+1)\) и
\(c*(b/a) + 5\). Переменные
a
,b
,c
в начале выполнения программы должны быть расположены в регистрахeax
,ebx
иecx
соответственно. Численные значения можно взять произвольными. - Создать 2 глобальные переменные, содержащую строки “even” и “odd” соответственно. Поменять местами буквы на четных позициях первой переменной с буквами на нечетных позициях второй.
Вариант 13
- Вычислить арифметическое выражение \(c+(5*a^2 + 6⁄b)-d^3\). Переменные
a
,b
,c
,d
в начале выполнения программы должны быть расположены в регистрахeax
,ebx
,ecx
иedx
соответственно. Численные значения можно взять произвольными. - Создать глобальную переменную, инициализированную 8 целыми числами (как минимум 4 отрицательных числа) и другую глобальную переменную такого же размера. Скопировать положительные числа из первой переменной во вторую, а на месте отрицательных расположить значение 0.
Пример программы на ассемблере FASM
format PE GUI ; указание о типе исполняемого файла (без консольного интерфейса)
entry start ; указание с какой метки начинать выполнение программы
dd 0 ; объявление глобальной переменной
a
start: ; точка входа в программу
mov eax, 1 ; код программы
ret ; завершение программы
