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

Простейший способ определить операнд в памяти - дать имя ячейке памяти. В дальнейшем программа использует это имя в командах для ссылки на соответствующий участок памяти. При использовании ассемблера из Паскаль-программы объявление переменных обычно производится не внутри ассемблерных вставок, а в секции var процедуры или функции. Эти переменные могут быть использованы как Pascal кодом, так и кодом на языке ассемблер (рисунок 20).

program vars;

var

  IntegerVar : longint;

begin

  IntegerVar := 5;

  asm

  dec IntegerVar

  end;

  writeln(IntegerVar); { на экран будет выведено число 4 }

end.

Рисунок 20 - Использование переменных

ПОНЯТИЕ СТЕКА

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

Рассмотрим пример. В стек добавляются три элемента типа двойное слово (4 байта) в следующем порядке: 1-й, 2-й, 3-й. Получившийся стек представлен на рисунке 21. Последний добавленный элемент называется вершиной стека. Каждый новый элемент «кладется» на вершину стека. При взятии элемента из стека первым берется также элемент, являющийся вершиной. Т. е. для стека из четырех элементов порядок доступа к ним будет такой: новый, 3-й, 2-й, 1-й.

НЕ нашли? Не то? Что вы ищете?

Рисунок 21 – Стек

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

Для помещения данных в стек используется команда push. Алгоритм ее работы таков:

Уменьшить значение esp на размер операнда Записать операнд в вершину стека

Обратная ей команда это команда pop, ее алгоритм:

Загрузить в операнд содержимое вершины стека (адресуется парой esp) Увеличить содержимое esp на размер операнда

Стек часто используется для временного хранения значений регистров (рис. 22).

push eax  ; размещение eax в стеке

mov eax, x  ; некоторый фрагмент

add eax, 1234h ; кода который "портит"

mov x, eax  ; значение находящееся в eax

pop eax  ; извлечение eax из стека

Рисунок 22 - Пример размещения и извлечения регистра из стека

Другая функция стека - это организация вызовов подпрограмм и возврата из них, для этого используются команды call и ret соответственно. Пример такой программы представлен на рисунке 23.

  ...

  mov eax, 1234h ; помещение аргумента процедуры в регистр eax

  call proc1  ; вызов процедуры

  ...

proc1:  sal eax, 1  ; умножение содержимого eax на 2

  ret  ; возврат из процедуры

Рисунок 23 - Пример организации процедуры и ее вызова

Инструкция call сохраняет в стеке адрес следующей за ней команды (аналогично команде push) и осуществляет переход по адресу указанному ей в качестве операнда (аналогично команде jmp). Команда ret  в нашем случае извлекает из стека адрес (ранее помещенный туда командой call) и осуществляет переход на него. Отметим, что в данном случае мы использовали для передачи параметров регистры (eax), возвращаемое значение после выхода из процедуры также находилось в eax.

Следует обратить внимание на такой момент: процессор не поддерживает никаких механизмов для контроля правильности работы со стеком, эта функция лежит целиком на программисте. В частности если внутри процедуры будет осуществлено помещение некоторого значения в стек командой push, но симметричной команды pop выполнено не будет, то это приведет к тому, что команда ret вместо адреса возврата извлечет значение помещенное командой push и сделает попытку перехода по этому адресу. В большинстве случаев  это приведет к ошибке доступа (access violation).

Отметим, что команда ret может иметь операнд-число, представляющее собой величину на которую будет дополнительно увеличено значение esp при выполнении этой инструкции. Смысл этого действия будет пояснен ниже.

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

Рассмотрим написание процедур на ассемблере и передачу параметров. В качестве примера рассмотрим функцию сложения двух чисел (рисунок 24).

function Sum1(x, y : longint) : longint;

begin

  asm

  mov eax, x  { Поместить в регистр eax значение x }

  add eax, y  { Добавить к значению в eax y }

  mov @Result, eax  { eax содержит результат функции }

  end;

end;

Рисунок 24 - Функция сложения двух чисел

Дизассемблирование данной функции показывает, что помимо написанного нами кода компилятор добавил еще некоторое количество команд (рисунок 25). Вызов этой функции представлен на рисунке 26.

; function Sum1(x, y : longint) : longint;

; begin

  push  ebp

  mov  ebp, esp

  sub  esp, 4

; mov eax, x  { Поместить в регистр eax значение x }

  mov  eax,[ebp+0Ch]

; add eax, y  { Добавить к значению в eax y }

  add  eax,[ebp+8]

; mov @Result, eax  { eax содержит результат функции }

  mov  [ebp-4],eax

; end;

  mov  eax,[ebp-4]

  leave

  ret  8

Рисунок 25 - Дизассемблированная функция Sum1

; y := Sum1(1, 2);

  push  1

  push  2

  call  Sum1

  mov  [403488h],eax

Рисунок 26 - Дизассемблированный вызов функции Sum1

Поясним основные моменты. При передаче параметров в процедуру или функцию они помещаются в стек командой push, кроме этого туда записывается адрес возврата командой call. Во время вызова функции регистр ebp сначала сохраняется в стеке, затем в регистр ebp помещается указатель на стек. Далее указатель вершины стека esp уменьшается на 4, таким образом, в стеке резервируются 4 байта для временного размещения переменной @Result. Далее выполняется код собственно функции, при этом видно, что аргументы функции адресуются относительно регистра ebp.

При возврате из процедуры необходимо восстановить значение ebp и указателя вершины стека. Это делается командой leave, которая копирует ebp в esp (mov esp, ebp), а затем восстанавливает значение в ebp из стека (pop ebp). После этих действий состояние стека восстанавливается в то состояние, в котором он находился на момент входа в функцию. Перед возвратом из процедуры осталось выполнить лишь одно действие - очистку стека от аргументов функции (согласно соглашению о вызовах процедур языка Pascal это должна делать вызываемая процедура). Это делает все та же команда ret, операнд которой имеет смысл числа байт удаляемых из стека. Возвращаемое функцией значение помещается по соглашению в регистр eax.

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

Обратим внимание, что при передаче параметров по значению (без использования ключевого слова var) в стеке передаются сами значения переменных. Если же передача идет по ссылке (со словом var), то в стеке передается адрес переменных. Процедура сложения двух целых чисел при передаче параметров по ссылке представлена на рисунке 27.

function Sum2(var x, y : longint) : longint;

asm

  mov  eax, x  { eax содержит АДРЕС! переменной x  }

  mov  eax,[eax]  { eax получает значение переменной x }

  mov  ecx, y  { eсx содержит АДРЕС! переменной y  }

  mov  ecx,[ecx]  { ecx получает значение переменной y }

  add  eax, ecx  { eax = eax + ecx }

end;

Рисунок 27 - Передача аргументов по ссылке

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

Взаимодействие программы на языке Ассемблера с операционной системой

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

Для получения доступа к некоторым возможностям операционной системы программа должна осуществить вызов некоторых ее функций. Способ взаимодействия с операционной системой или программный интерфейс (API, Application Programming Interface) индивидуален для различных типов операционных систем.

Чаще всего используется два способа осуществления вызовов операционной системы со стороны приложения: вызов процедуры с помощью команды call, либо с помощью механизма прерываний. Прерывание (interrupt) – это подпрограмма, адрес которой находится в специальной таблице векторов (адресов) прерываний.

Например, вывод строки текста на экран в системе MS-DOS (эта операционная система работает в 16-битном режиме!) выглядит как вызов функции 9 прерывания 21:

mov  ah, 9  ; номер функции DOS в AH

mov  dx, offset message  ; адрес строки message  в DX

int  21h  ; вызов системной функции DOS

В тоже время вывод окна сообщений (MessageBox) в системе Windows выглядит так:

push  MB_OK  ; специальная числовая константа,

  ; задающая набор кнопок в окне

push  offset title  ; в стек кладем адрес строки title

push  offset message  ; в стек кладем адрес строки message

push  0  ; параметр функции, говорящий о том,

  ; что окно сообщений не связано

  ; с другими окнами

call  MessageBox  ; вызов системной функции Windows

Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16