Команды для работы со стеком

Предусмотрено две специальные команды для работы со стеком: push (поместить в стек) и pop (извлечь из стека). Синтаксис:

push источник

pop назначение

При описании работы стека мы уже обсуждали принцип работы команд push и pop. Важный нюанс: push и pop работают только с операндами размером 4 или 2 байта. Если вы попробуете скомпилировать что-то вроде

pushb 0x10

GCC вернёт следующее:

[user@host:~]$ gcc test.s

test.s: Assembler messages:

test.s:14: Error: suffix or operands invalid for `push '

[user@host:~]$

Согласно ABI, в Linux стек выровнен по long. Сама архитектура этого не требует, это только соглашение между программами, но не рассчитывайте, что другие библиотеки подпрограмм или операционная система захотят работать с невыровненным стеком. Что всё это значит? Если вы резервируете место в стеке, количество байт должно быть кратно размеру long, то есть 4. Например, вам нужно всего 2 байта в стеке для short, но вам всё равно придётся резервировать 4 байта, чтобы соблюдать выравнивание. А теперь примеры:

.text

pushl $0x10 /* поместить в стек число 0x10 */

pushl $0x20 /* поместить в стек число 0x20 */

popl %eax /* извлечь 0x20 из стека и записать в

%eax */

popl %ebx /* извлечь 0x10 из стека и записать в

%ebx */

pushl %eax /* странный способ сделать */

popl %ebx /* movl %eax, %ebx */

movl $0x00000010, %eax

pushl %eax /* поместить в стек содержимое %eax */

popw %ax /* извлечь 2 байта из стека и

записать в %ax */

popw %bx /* и ещё 2 байта и записать в %bx */

/* в %ax находится 0x0010, в %bx

находится 0x0000; такой код сложен

для понимания, его следует избегать

*/

pushl %eax /* поместить %eax в стек; %esp

уменьшится на 4 */

addl $4, %esp /* увеличить %esp на 4; таким образом,

стек будет приведён в исходное

состояние */

Интересный вопрос: какое значение помещает в стек вот эта команда

pushl %esp

Если ещё раз взглянуть на алгоритм работы команды push, кажется очевидным, что в данном случае она должна поместить уже уменьшенное значение %esp. Однако в документации Intel1 сказано, что в стек помещается такое значение %esp, каким оно было до выполнения команды - и она действительно работает именно так.

Арифметика

Арифметических команд в нашем распоряжении довольно много. Синтаксис:

inc операнд

dec операнд

add источник, приёмник

sub источник, приёмник

mul множитель_1

Принцип работы:

· inc: увеличивает операнд на 1.

· dec: уменьшает операнд на 1.

· add: приёмник = приёмник + источник (то есть, увеличивает приёмник на источник).

· sub: приёмник = приёмник - источник (то есть, уменьшает приёмник на источник).

Команда mul имеет только один операнд. Второй сомножитель задаётся неявно. Он находится в регистре %eax, и его размер выбирается в зависимости от суффикса команды (b, w или l). Место размещения результата также зависит от суффикса команды. Нужно отметить, что результат умножения двух Команды для работы со стеком - student2.ru -разрядных чисел может уместиться только в Команды для работы со стеком - student2.ru -разрядном регистре результата. В следующей таблице описано, в какие регистры попадает результат при той или иной разрядности операндов.



Команда Второй сомножитель Результат
mulb %al 16 бит: %ax
mulw %ax 32 бита: младшая часть в %ax, старшая в %dx
mull %eax 64 бита: младшая часть в %eax, старшая в %edx

Примеры:

.text

movl $72, %eax

incl %eax /* в %eax число 73 */

decl %eax /* в %eax число 72 */

movl $48, %eax

addl $16, %eax /* в %eax число 64 */

movb $5, %al

movb $5, %bl

mulb %bl /* в регистре %ax произведение

%al ? %bl = 25 */

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

char x, y;

x = 250;

y = 14;

x = x + y;

printf( "%d ", (int) x);

Большинство сразу скажет, что результат (250 + 14 = 264) больше, чем может поместиться в одном байте. И что же напечатает программа? 8. Давайте рассмотрим, что происходит при сложении в двоичной системе.

11111010 250

+ 00001110 + 14

---------- ---

1 00001000 264

| |

| <------ >|

8 бит

Получается, что результат занимает 9 бит, а в переменную может поместиться только 8 бит. Это называется переполнением - перенос из старшего бита результата. В Си переполнение не может быть перехвачено, но в микропроцессоре эта ситуация регистрируется, и её можно обработать. Когда происходит переполнение, устанавливается флаг cf. Команды условного перехода jc и jnc анализируют состояние этого флага. Команды условного перехода будут рассмотрены далее, здесь эта информация приводится для полноты описания команд.

movb $0, %ah /* %ah = 0 */

movb $250, %al /* %al = 250 */

addb $14, %al /* %al = %al + 14

происходит переполнение,

устанавливается флаг cf;

в %al число 8 */

jnc no_carry /* если переполнения не было, перейти

на метку */

movb $1, %ah /* %ah = 1 */

no_carry:

/* %ax = 264 = 0x0108 */

Этот код выдаёт правильную сумму в регистре %ax с учётом переполнения, если оно произошло. Попробуйте поменять числа в строках 2 и 3.

Команда lea для арифметики

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

Вспомним, как формируется адрес операнда:

смещение(база, индекс, множитель)

Вычисленный адрес будет равен база + индекс ? множитель + смещение.

Чем это нам удобно? Так мы можем получить команду с двумя операндами-источниками и одним результатом:

movl $10, %eax

movl $7, %ebx

leal 5(%eax) ,%ecx /* %ecx = %eax + 5 = 15 */

leal -3(%eax) ,%ecx /* %ecx = %eax - 3 = 7 */

leal (%eax,%ebx) ,%ecx /* %ecx = %eax + %ebx ? 1 = 17 */

leal (%eax,%ebx,2) ,%ecx /* %ecx = %eax + %ebx ? 2 = 24 */

leal 1(%eax,%ebx,2),%ecx /* %ecx = %eax + %ebx ? 2 + 1 = 25 */

leal (,%eax,8) ,%ecx /* %ecx = %eax ? 8 = 80 */

leal (%eax,%eax,2) ,%ecx /* %ecx = %eax + %eax ? 2 = %eax ? 3 = 30 */

leal (%eax,%eax,4) ,%ecx /* %ecx = %eax + %eax ? 4 = %eax ? 5 = 50 */

leal (%eax,%eax,8) ,%ecx /* %ecx = %eax + %eax ? 8 = %eax ? 9 = 90 */

Вспомните, что при сложении командой add результат записывается на место одного из слагаемых. Теперь, наверно, стало ясно главное преимущество lea в тех случаях, где её можно применить: она не перезаписывает операнды-источники. Как вы это сможете использовать, зависит только от вашей фантазии: прибавить константу к регистру и записать в другой регистр, сложить два регистра и записать в третий… Также lea можно применять для умножения регистра на 3, 5 и 9, как показано выше.

Команда loop

Синтаксис:

loop метка

Принцип работы:

· уменьшить значение регистра %ecx на 1;

· если %ecx = 0, передать управление следующей за loop команде;

· если %ecx Команды для работы со стеком - student2.ru , передать управление на метку.

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

.data

printf_format:

.string "%d\n "

.text

.globl main

main:

movl $0, %eax /* в %eax будет результат, поэтому в

начале его нужно обнулить */

movl $10, %ecx /* 10 шагов цикла */

sum:

addl %ecx, %eax /* %eax = %eax + %ecx */

loop sum

/* %eax = 55, %ecx = 0 */

/*

* следующий код выводит число в %eax на экран и завершает программу

*/

pushl %eax

pushl $printf_format

call printf

addl $8, %esp

movl $0, %eax

ret

На Си это выглядело бы так:

#include <stdio.h >

int main()

{

int eax, ecx;

eax = 0;

ecx = 10;

do

{

eax += ecx;

} while(--ecx);

printf( "%d\n ", eax);

return 0;

}

Наши рекомендации