Украина, г. Киев |
Команды ассемблера
Команда 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.