Команды ассемблера
Команда mov
mov источник, назначение
Команда mov производит копирование источника в назначение. Рассмотрим примеры:
/*
* Это просто примеры использования команды mov,
* ничего толкового этот код не делает
*/
.data
some_var:
.long 0x00000072
other_var:
.long 0x00000001, 0x00000002, 0x00000003
.text
.globl main
main:
movl $0x48, %eax /* поместить число 0x00000048 в %eax */
movl $some_var, %eax /* поместить в %eax значение метки
some_var, то есть адрес числа в
памяти; например, у автора
содержимое %eax равно 0x08049589 */
movl some_var, %eax /* обратиться к содержимому переменной;
в %eax теперь 0x00000072 */
movl other_var + 4, %eax /* other_var указывает на 0x00000001
размер одного значения типа long - 4
байта; значит, other_var + 4
указывает на 0x00000002;
в %eax теперь 0x00000002 */
movl $1, %ecx /* поместить число 1 в %ecx */
movl other_var(,%ecx,4), %eax /* поместить в %eax первый
(нумерация с нуля) элемент массива
other_var, пользуясь %ecx как
индексным регистром */
movl $other_var, %ebx /* поместить в %ebx адрес массива
other_var */
movl 4(%ebx), %eax /* обратиться по адресу %ebx + 4;
в %eax снова 0x00000002 */
movl $other_var + 4, %eax /* поместить в %eax адрес, по
которому расположен 0x00000002
(адрес массива плюс 4 байта --
пропустить нулевой элемент) */
movl $0x15, (%eax) /* записать по адресу "то, что записано
в %eax " число 0x00000015 */
Внимательно следите, когда вы загружаете адрес переменной, а когда обращаетесь к значению переменной по её адресу. Например:
movl other_var + 4, %eax /* забыли знак $, в результате в %eax
находится число 0x00000002 */
movl $0x15, (%eax) /* пытаемся записать по адресу
0x00000002 - > получаем segmentation
fault */
movl 0x48, %eax /* забыли $, и пытаемся обратиться по
адресу 0x00000048 - > segmentation
fault */
Команда lea
lea - мнемоническое от англ. Load Effective Address. Синтаксис:
lea источник, назначение
Команда lea помещает адрес источника в назначение. Источник должен находиться в памяти (не может быть непосредственным значением - константой или регистром). Например:
.data
some_var:
.long 0x00000072
.text
leal 0x32, %eax /* аналогично movl $0x32, %eax */
leal some_var, %eax /* аналогично movl $some_var, %eax */
leal $0x32, %eax /* вызовет ошибку при компиляции,
так как $0x32 - непосредственное
значение */
leal $some_var, %eax /* аналогично, ошибка компиляции:
$some_var - это непосредственное
значение, адрес */
leal 4(%esp), %eax /* поместить в %eax адрес предыдущего
элемента в стеке;
фактически, %eax = %esp + 4 */
Команды для работы со стеком
Предусмотрено две специальные команды для работы со стеком: push (поместить в стек) и pop (извлечь из стека). Синтаксис:
push источник pop назначение
При описании работы стека мы уже обсуждали принцип работы команд push и pop. Важный нюанс: push и pop работают только с операндами размером 4 или 2 байта. Если вы попробуете скомпилировать что-то вроде
pushb 0x10
[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. Однако в документации Intel1Intel® 64 and IA-32 Architectures Software Developer's Manual, 4.1 Instructions (N-Z), PUSH сказано, что в стек помещается такое значение %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.