Опубликован: 01.03.2016 | Доступ: свободный | Студентов: 584 / 62 | Длительность: 03:55:00
Лекция 5:

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

Команда 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

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

Команда Второй сомножитель Результат
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.