Команды для работы со стеком
Предусмотрено две специальные команды для работы со стеком: 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). Место размещения результата также зависит от суффикса команды. Нужно отметить, что результат умножения двух -разрядных чисел может уместиться только в -разрядном регистре результата. В следующей таблице описано, в какие регистры попадает результат при той или иной разрядности операндов.
Команда | Второй сомножитель | Результат |
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 , передать управление на метку.
Напишем программу для вычисления суммы чисел от 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;
}