Введение в программирование на ассемблере

Регистры процессора

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

Так, процессоры 32-битной архитектуры Intel (i386) содержат следующие регистры:

Состав регистров 64-битной архитектуры Intel (x86_64, amd64) представлен следующими регистрами:

Иллюстрация расширения регистров на примере регистра RAX представлена ниже:

RAX (8 байт)

EAX (4 байта)

AX (2 байта)

AH (1 б.)

AL (1 б.)

Язык ассемблера

Существует множество реализаций компиляторов ассемблера (которые тоже называются ассемблерами), каждая из которых обладает отличительными особенностями синтаксиса и принципами работы. Однако, основная задача, которую решает любой ассемблер – трансляция мнемоник команд (т.е. их текстового представления) в двоичный код, понятный процессору. Гипотетически, программист может сразу писать код в двоичном виде, но для человека представляется проблематичным помнить конкретные коды всех команд процессора (включая их варианты, в зависимости от типа операнда).

Примером ассемблирования может быть инструкция

add eax, 1

добавляющая к значению регистра EAX единицу, и соответствующая ей инструкция машинного кода 83 С0 01 (в шестнадцатеричном виде).

Программа на ассемблере, в общем случае, представляет собой последовательность подобных инструкций. Каждая инструкция может иметь различные режимы работы, в зависимости от типов операндов. Например, приведенная выше инструкция ADD складывает значение регистра с константой, в то время как инструкция add eax, ebx прибавит к регистру EAX значение регистра EBX.

Полный список поддерживаемых процессором/ассемблером инструкций следует искать в соответствующей документации. В данной лабораторной работе будут приведены лишь инструкции, необходимые для совершения базовых операций.

Современные ассемблеры:

Рассмотрим инструкции, которые понадобятся для выполнения данной лабораторной работы.

Необходимо отметить, что в инструкциях 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]

Синтаксис объявления глобальных переменных в ассемблере имеет следующий вид:

имя_переменной   тип   значение

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

Тип переменной может быть:

В качестве начального значения могут быть использованы:

Примеры объявления глобальных переменных:

a db 0
b dd 0,1,2
bb dd 3 dup(0)
с 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” соответственно.

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

a dd 123

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

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

a db 0

mov eax, [a]

Здесь a имеет размер 1 байт, в то время как eax имеет размер 4 байта. Можно предложить несколько решений этой проблемы:

Отладчик

Отладчиком называется программа, позволяющая выполнять другую программу пошагово (т.е. инструкция за инструкцией). Между выполнением отдельных инструкций отлаживаемая программа находится “на паузе”. В этот момент отладчик предоставляет возможность просмотреть состояние регистров и памяти, изменять их значения, а так же изменять непосредственно код выполняемой программы.

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

Структура окна графического отладчика x64dbg представлена на рисунке 1.

Рисунок 1.

Здесь

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

Более детальный вид окна 2:

Рисунок 2.

Здесь изображены:

  1. Столбец, содержащий адрес инструкции в шестнадцатеричном виде;
  2. Машинный код инструкции;
  3. Дизассемблированная инструкция;
  4. Дополнительная информация о выделенной инструкции. Например, для инструкций, операнды которых содержат адреса ячеек памяти, это окно выводит содержимое соответствующих ячеек.

Как уже было сказано ранее, отладчик позволяет изменять код программы “на лету”. Для этого в области 3 необходимо выделить инструкцию, которую нужно изменить, а после этого нажать “пробел”. Во всплывающем окошке вводится новая инструкция, которая перезапишет собой текущую. Стоит отметить, что различные инструкции имеют различную длину в байтах, поэтому заменяя 1-байтовую инструкцию, скажем, 2-байтовой, отладчик перезапишет и инструкцию следующую за ней.

Отладка программы

Отладка программы начинается с загрузки ее в отладчик. В случае графических отладчиков (OllyDbg и x64dbg), это делается через меню “File”→“Open”. Сразу после запуска отладчик остановит программу на ближайшей точке останова, которая, в зависимости от его настроек, может быть

Остановленная программа может быть выполнена пошагово с заходом в процедуры (Step Into), пошагово без захода в процедуры (Step Over), выполнена до ближайшего выхода из текущей процедуры (Execute Till Return), а так же просто продолжена (Continue). В последнем случае программа будет выполняться до тех пор, пока управление не дойдет до одной из точек останова, или до завершения программы, или пока не будет остановлена вручную кнопкой Break. Изменившиеся значения регистров, стека и памяти между двумя паузами отладчик помечает красным цветом.

Во время паузы выполнения программы пользователь может:

Контрольные вопросы

  1. Что такое регистры процессора? Каковы их виды?
  2. Что такое ассемблер?
  3. Что такое отладчик?

Задание на лабораторную работу

  1. Установить ассемблер FASM (http://flatassembler.net/) и отладчик x64dbg (http://x64dbg.com/).
  2. Написать и скомпилировать программу, соответствующую своему варианту. Простейший пример программы на FASM приведен в конце лабораторной работы.
  3. Загрузить скомпилированную программу в отладчик и убедиться в ее правильном функционировании.

Варианты заданий

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

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

Вариант 1

  1. Вычислить арифметическое выражение \(a*b+a*(c+d)+d*(-1)\). Переменные a, b, c, d в начале выполнения программы должны быть расположены в регистрах eax, ebx, ecx и edx соответственно. Численные значения можно взять произвольными.
  2. Создать неинициализированную глобальную переменную размером в 4 байта, программно инициализировать ее таким образом, чтобы в окне дампа можно было увидеть AA BB CC DD.

Вариант 2

  1. Вычислить арифметическое выражение \(a+(b^2+1)⁄(b-5)\). Переменные a и b в начале выполнения программы должны быть расположены в регистрах esi и edi соответственно. Численные значения можно взять произвольными.
  2. Создать глобальную переменную, содержащую строку “Labs” и другую глобальную переменную такого же размера. Написать код, копирующий содержимое первой переменной во вторую.

Вариант 3

  1. Вычислить арифметическое выражение \(a*a+b\). Переменные a и b в начале выполнения программы должны быть расположены в регистрах eax и ebx соответственно, а их численные значения должны быть равны 65535 и 131072.
  2. Выделить участок памяти размером в 8 байт и программно инициализировать его таким образом, чтобы в окне дампа можно было бы увидеть значения вида 00 01 00 01 ....

Вариант 4

  1. Вычислить арифметическое выражение \((a+4*(b*c)+d^3)⁄2\). Переменные a, b, c, d в начале выполнения программы должны быть расположены в регистрах eax, ebx, ecx и edx соответственно. Численные значения можно взять произвольными.
  2. Создать глобальную переменную, инициализированную 8 целыми числами (как минимум 3 четных числа) и другую глобальную переменную такого же размера. Написать код, копирующий четные числа из первой переменной во вторую.

Вариант 5

  1. В рамках одной программы вычислить арифметические выражения \(a*b*(-1)\) и \(c^2+d^3-100\). Переменные a, b, c, d в начале выполнения программы должны быть расположены в регистрах eax, ebx, ecx и edx соответственно. Численные значения можно взять произвольными.
  2. Выделить участок памяти размером в 16 байт и программно инициализировать его таким образом, чтобы в окне дампа можно было бы увидеть значения вида 01 00 01 00 ….

Вариант 6

  1. Вычислить арифметическое выражение \(a^2 ⁄ 5+b*c-a⁄b\). Переменные a, b, c в начале выполнения программы должны быть расположены в регистрах eax, edx и edi соответственно. Численные значения можно взять произвольными.
  2. Создать глобальную переменную, содержащую строку “Hello”. Написать код, модифицирующий содержимое этой переменной в слово “World” путем арифметических операций.

Вариант 7

  1. Вычислить арифметическое выражение \((a*b*c-a*c*d)⁄3\). Переменные a, b, c, d в начале выполнения программы должны быть расположены в регистрах eax, ebx, ecx и edx соответственно. Численные значения можно взять произвольными.
  2. Создать глобальную переменную, которая содержит 10 участков памяти по 2 байта и инициализировать ее произвольными числами. Поменять местами первое и последнее число, а остальные сделать отрицательными.

Вариант 8

  1. Вычислить арифметическое выражение \((a^2 * b^2) ⁄ 10 + 5\). Переменные a, b в начале выполнения программы должны быть расположены в регистрах esi и edi соответственно. Численные значения можно взять произвольными.
  2. Создать глобальную переменную, содержащую строку “ReversE” и другую глобальную переменную такого же размера. Написать код, копирующий содержимое первой переменной во вторую, инвертируя порядок символов.

Вариант 9

  1. Вычислить арифметическое выражение \((a*b)⁄(a^3 + 10)\). Переменные a, b в начале выполнения программы должны быть расположены в регистрах esi и edi соответственно. Численные значения a и b можно взять произвольными.
  2. Создать глобальную переменную, которая содержит 8 участков памяти по 4 байта и заполнить ее произвольными числами. Поменять местами числа на четных и нечетных позициях.

Вариант 10

  1. Вычислить арифметическое выражение \((a⁄b - (c+2)⁄d)*a^2\). Переменные a, b, c, d в начале выполнения программы должны быть расположены в регистрах eax, ebx, ecx и edx соответственно. Численные значения можно взять произвольными.
  2. Объявить неинициализированный участок памяти размером в 5 байт, программно инициализировать его таким образом, чтобы в окне дампа можно было увидеть FE 00 DC 11 BA.

Вариант 11

  1. Вычислить арифметическое выражение \((a-8)⁄(b+15)-2⁄3\). Переменные a, b в начале выполнения программы должны быть расположены в регистрах eax, ebx соответственно. Численные значения можно взять произвольными.
  2. Создать 2 глобальные переменные, содержащие 4 участка памяти по 1 байту и инициализировать их произвольными числами. Создать еще одну глобальную переменную, в которую программно поместить суммы элементов созданных переменных.

Вариант 12

  1. В рамках одной программы вычислить арифметические выражения \((-a)⁄(b+1)\) и \(c*(b/a) + 5\). Переменные a, b, c в начале выполнения программы должны быть расположены в регистрах eax, ebx и ecx соответственно. Численные значения можно взять произвольными.
  2. Создать 2 глобальные переменные, содержащую строки “even” и “odd” соответственно. Поменять местами буквы на четных позициях первой переменной с буквами на нечетных позициях второй.

Вариант 13

  1. Вычислить арифметическое выражение \(c+(5*a^2 + 6⁄b)-d^3\). Переменные a, b, c, d в начале выполнения программы должны быть расположены в регистрах eax, ebx, ecx и edx соответственно. Численные значения можно взять произвольными.
  2. Создать глобальную переменную, инициализированную 8 целыми числами (как минимум 4 отрицательных числа) и другую глобальную переменную такого же размера. Скопировать положительные числа из первой переменной во вторую, а на месте отрицательных расположить значение 0.

Пример программы на ассемблере FASM

format PE GUI ; указание о типе исполняемого файла (без консольного интерфейса)
entry start ; указание с какой метки начинать выполнение программы
a dd 0 ; объявление глобальной переменной

start: ; точка входа в программу

mov eax, 1 ; код программы

ret ; завершение программы