Опубликован: 01.03.2016 | Уровень: для всех | Доступ: платный
Лекция 5:

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

Смотрите: в секции .rodata (данные только для чтения) создаётся массив из 4 значений. Мы обращаемся к нему как к обычному массиву, индексируя его по %eax:jump_table(,%eax,4). Но зачем перед этим стоит звёздочка? Она означает, что мы хотим перейти по адресу, содержащемуся в памяти по адресу jump_table(,%eax,4) (если бы её не было, мы бы перешли по этому адресу и начали исполнять массив jump_table как код).

Заметьте, что тут нам понадобились значения 0, 1, 3, укладывающиеся в маленький промежуток [0; 3]. Так как для значения 2 не предусмотрено особой обработки, в массиве адресов jump_table индексу 2 соответствует case_default. Перед тем, как сделать jmp, нужно обязательно убедиться, что проверяемое значение входит в наш промежуток, и если не входит - перейти на default. Если вы этого не сделаете, то, когда попадётся значение, находящееся за пределами массива, программа, в лучшем случае, получит segmentation fault, а в худшем (если рядом с этим масивом адресов в памяти окажется еще один массив адресов) код продолжит исполнение вообще непонятно где.

Теперь рассмотрим случай, когда значения для веток case находятся на большом расстоянии друг от друга. Очевидно, что способ с массивом адресов не подходит, иначе массив занимал бы большое количество памяти и содержал в основном адреса ветки default. В этом случае лучшее, что может сделать программист, - выразить switch как последовательное сравнение со всеми перечисленными значениями. Если значений довольно много, придётся применить немного логики: приблизительно прикинуть, какие ветки будут исполняться чаще всего, и отсортировать их в таком порядке в коде. Это нужно для того, чтобы наиболее часто исполняемые ветки исполнялись после маленького числа сравнений. Допустим, у нас есть варианты 5, 38, 70 и 1400, причём 70 будет появляться чаще всего:

.data
printf_format:
        .string  "%u\n "

.text
.globl main

main:
        pushl %ebp
        movl  %esp, %ebp

        movl  $70, %eax         /* получить в %eax некоторое 
                                   интересующее нас значение         */

        cmpl  $70, %eax
        je    case_70

        cmpl  $5, %eax
        je    case_5

        cmpl  $38, %eax
        je    case_38

        cmpl  $1400, %eax
        je    case_1400

case_default:
        movl  $100, %ecx
        jmp   switch_end

case_5:
        movl  $5, %ecx
        jmp   switch_end

case_38:
        movl  $15, %ecx
        jmp   switch_end

case_70:
        movl  $25, %ecx
        jmp   switch_end

case_1400:
        movl  $35, %ecx

switch_end:

        pushl %ecx

        pushl $printf_format
        call  printf

        movl  $0, %eax

        movl  %ebp, %esp
        popl  %ebp
        ret

Единственное, на что хочется обратить внимание, - на расположение ветки default: если все сравнения оказались ложными, код default выполняется автоматически.

Наконец, третий, комбинированный, вариант. Путь имеем варианты 35, 36, 37, 39, 1200, 1600 и 7000. Тогда мы видим промежуток [35; 39] и ещё три числа. Код будет выглядеть приблизительно так:

        movl  $1, %eax          /* получить в %eax некоторое 
                                   интересующее нас значение         */

        cmpl  $35, %eax
        jb    case_default

        cmpl  $39, %eax
        ja    switch_compare

        jmp   *jump_table-140(,%eax,4)

.section .rodata
        .p2align 4
jump_table:
        .long case_35
        .long case_36
        .long case_37
        .long case_default
        .long case_39
.text

switch_compare:
        cmpl  $1200, %eax
        jmp   case_1200

        cmpl  $1600, %eax
        jmp   case_1600

        cmpl  $7000, %eax
        jmp   case_7000

case_default:
        /* ... */
        jmp   switch_end

case_35:
        /* ... */
        jmp   switch_end

        ... ещё код ...
switch_end:

Заметьте, что промежуток начинается с числа 35, а не с 0. Для того, чтобы не производить вычитание 35 отдельной командой и не создавать массив, в котором от 0 до 34 идёт адреса метки default, сначала проверяется принадлежность числа промежутку [35; 39], а затем производится переход, но массив адресов считается размещённым на 35 двойных слов "ниже " в памяти (то есть, на 35 ? 4 = 140 байт). В результате получается, что адрес перехода считывается из памяти по адресу jump_table - 35*4 + %eax*4 = jump_table + (%eax - 35)*4. Выиграли одно вычитание.

В этом примере, как и в предыдущих, имеет смысл переставить некоторые части этого кода в начало, если вы заранее знаете, какие значения вам придётся обрабатывать чаще всего.

Пример: интерпретатор языка Brainfuck

Brainfuck - это эзотерический язык программирования, то есть язык, предназначенный не для практического применения, а придуманный как головоломка, как задача, которая заставляет программиста думать нестандартно. Команды Brainfuck управляют массивом целых чисел с неограниченным набором ячеек, есть понятие текущей ячейки.

  • Команды < и > дают возможность перемещаться по массиву на одну ячейку влево и, соответственно, вправо.
  • Команды + и - увеличивают и, соответственно, уменьшают содержимое текущей ячейки на 1.
  • Команда . выводит содержимое текущей ячейки на экран как один символ; команда , читает один символ и помещает его в текущую ячейку.
  • Команды циклов [ и ] должны всегда находиться в парах и соблюдать правила вложенности (как скобки в математических выражениях). Команда [ сравнивает значение текущей ячейки с 0: если оно равно 0, то выполняется команда, следующая за соответствующей ], если не равно, то просто выполняется следующая команда. Команда ] передаёт управление на соответствующую [.
  • Остальные символы в коде программы являются комментариями, и их следует пропускать.

В начальном состоянии все ячейки содержат значение 0, а текущей является крайняя левая ячейка.

Вот несколько программ с объяснениями:

+ >+ >+        Устанавливает первые три ячейки в 1
[-]          Обнуляет текущую ячейку
[ > > >+ < < <-]   Перемещает значение текущей ячейки в ячейку, расположенную
              "тремя шагами правее "

Интерпретация программы состоит из двух шагов: загрузка программы и собственно исполнение. Во время загрузки следует проверить корректность программы (соответствие []) и расположить код программы в памяти в удобном для выполнения виде. Для этого каждой команде присваивается номер операции, начиная с 0, - для того, чтобы можно было выполнять операции при помощи помощи перехода по массиву адресов, как в switch.

Большинство программ на Brainfuck содержат последовательности одинаковых команд < > + -, которые можно выполнять не по одной, а все сразу. Например, выполняя код +++++, можно выполнить пять раз увеличение на 1, или один раз увеличение на 5. Таким образом, довольно простыми средствами можно сильно оптимизировать выполнение программы.

Вот программы, которые вызовут ошибки загрузки:

[       No matching  '] ' found for a  '[ '
]       No matching  '[ ' found for a  '] '

А эти программы вызовут ошибки выполнения:

 <       Memory underflow
+[ >+]   Memory overflow

Исходный код:

#define BF_PROGRAM_SIZE 1024
#define BF_MEMORY_CELLS 32768
#define BF_MEMORY_SIZE BF_MEMORY_CELLS*4

#define BF_OP_LOOP_START 0
#define BF_OP_LOOP_END   1
#define BF_OP_MOVE_LEFT  2
#define BF_OP_MOVE_RIGHT 3
#define BF_OP_INC        4
#define BF_OP_DEC        5
#define BF_OP_PUTC       6
#define BF_OP_GETC       7
#define BF_OP_EXIT       8

.section .rodata
str_memory_underflow:
        .string  "Memory underflow\n "

str_memory_overflow:
        .string  "Memory overflow\n "

str_loop_start_not_found:
        .string  "No matching  '[ ' found for a  '] '\n "

str_loop_end_not_found:
        .string  "No matching  '] ' found for a  '[ '\n "

.data
bf_program_ptr:
        .long 0

bf_program_size:
        .long 0

/*
 * Программа загружается в память вот так:
 * =============================
 * код_операции, операнд,
 * код_операции, операнд,
 * код_операции, операнд, ...
 * =============================
 * И код_операции, и операнд занимают по 4 байта.
 * Таким образом, одна команда занимает в памяти 8 байт.
 *
 * Для команды [ (начало цикла) операндом является номер команды, 
 * следующий за концом цикла.
 *
 * Для команды ] (конец цикла) операндом является номер команды-начала 
 * цикла ].
 *
 * Для остальных команд ( <  > + - . ,) операнд задаёт количество 
 * повторений этой команды.  Например, для кода +++++ должен быть 
 * сгенерирован код операции BF_OP_INC с операндом 5, который при 
 * выполнении увеличит текущую ячейку на 5.
 */

.text
.globl main
main:
        pushl %ebp
        movl  %esp, %ebp

/* ******************************************* */
/* загрузка программы                          */
/* ******************************************* */

        movl  $BF_PROGRAM_SIZE, %ecx
        movl  %ecx, bf_program_size
        pushl %ecx
        call  malloc
        movl  %eax, bf_program_ptr

        movl  %eax, %ebx        /* %ebx - указатель на блок памяти, 
                                   содержащий внутреннее представление 
                                   программы                         */
        xorl  %ecx, %ecx        /* %ecx - номер текущей команды      */
        xorl  %esi, %esi        /* %esi - предыдущая команда, символ */

bf_read_loop:
        pushl %ecx
        pushl stdin
        call  fgetc
        addl  $4, %esp
        popl  %ecx

        cmpl  $-1, %eax
        je    bf_read_end

        cmpl  $ '[, %eax         /* команды, которые всегда 
                                   обрабатываются по одной: [ и ]    */
        je    bf_read_loop_start

        cmpl  $ '], %eax
        je    bf_read_loop_end

        cmpl  %esi, %eax        /* текущая команда такая же, как и 
                                   предыдущая?                       */
        jne   not_dupe

        incl  -4(%ebx,%ecx,8)   /* такая же. Но %ecx указывает на 
                                   следующую команду, поэтому 
                                   используем отрицательное смещение -4
                                                                     */
        jmp   bf_read_loop

not_dupe:                       /* другая                            */
        cmpl  $ ' <, %eax
        je    bf_read_move_left

        cmpl  $ ' >, %eax
        je    bf_read_move_right

        cmpl  $ '+, %eax
        je    bf_read_inc

        cmpl  $ '-, %eax
        je    bf_read_dec

        cmpl  $ '., %eax
        je    bf_read_putc

        cmpl  $ ',, %eax
        je    bf_read_getc

        jmp   bf_read_loop

bf_read_loop_start:
        movl  $BF_OP_LOOP_START, (%ebx,%ecx,8)
        movl  $0, 4(%ebx,%ecx,8)
        jmp   bf_read_switch_end

bf_read_loop_end:
        movl  $BF_OP_LOOP_END, (%ebx,%ecx,8)
        movl  %ecx, %edx
bf_read_loop_end_find:
        testl %edx, %edx
        jz    bf_read_loop_end_not_found
        decl  %edx
        cmpl  $0, 4(%ebx,%edx,8)
        je    bf_read_loop_end_found
        jmp   bf_read_loop_end_find
bf_read_loop_end_not_found:
        jmp   loop_start_not_found
bf_read_loop_end_found:
        leal  1(%ecx), %edi
        movl  %edi, 4(%ebx,%edx,8)
        movl  %edx, 4(%ebx,%ecx,8)
        jmp   bf_read_switch_end

bf_read_move_left:
        movl  $BF_OP_MOVE_LEFT, (%ebx,%ecx,8)
        jmp   bf_read_switch_end_1

bf_read_move_right:
        movl  $BF_OP_MOVE_RIGHT, (%ebx,%ecx,8)
        jmp   bf_read_switch_end_1

bf_read_inc:
        movl  $BF_OP_INC, (%ebx,%ecx,8)
        jmp   bf_read_switch_end_1

bf_read_dec:
        movl  $BF_OP_DEC, (%ebx,%ecx,8)
        jmp   bf_read_switch_end_1

bf_read_putc:
        movl  $BF_OP_PUTC, (%ebx,%ecx,8)
        jmp   bf_read_switch_end_1

bf_read_getc:
        movl  $BF_OP_GETC, (%ebx,%ecx,8)

bf_read_switch_end_1:
        movl  $1, 4(%ebx,%ecx,8)

bf_read_switch_end:

        movl  %eax, %esi        /* сохранить текущую команду для 
                                   сравнения                         */

        incl  %ecx

        leal  (,%ecx,8), %edx   /* блок памяти закончился?           */
        cmpl  bf_program_size, %edx
        jne   bf_read_loop

        addl  $BF_PROGRAM_SIZE, %edx  /* увеличить размер блока памяти 
                                                                     */
        movl  %edx, bf_program_size
        pushl %ecx
        pushl %edx
        pushl %ebx
        call  realloc
        addl  $8, %esp
        popl  %ecx
        movl  %eax, bf_program_ptr
        movl  %eax, %ebx

        jmp   bf_read_loop

bf_read_end:

        movl  $BF_OP_EXIT, (%ebx,%ecx,8)  /* последней добавить 
                                             команду выхода          */
        movl  $1, 4(%ebx,%ecx,8)

/*
 * Ищем незакрытые  '[ ':
 * Ищем 0 в поле операнда.  Саму команду не проверяем, так как 0 может 
 * быть операндом только у  '[ '.
 */

        xorl  %edx, %edx
1:
        cmpl  $0, 4(%ebx,%ecx,8)
        je    loop_end_not_found
        incl  %ecx
        testl %edx, %ecx
        je    2f
        jmp   1b
2:

/* ******************************************* */
/* выполнение программы                        */
/* ******************************************* */


        pushl $BF_MEMORY_SIZE   /* выделить блок памяти для памяти 
                                   программы                         */
        call  malloc
        addl  $4, %esp
        movl  %eax, %esi

        xorl  %ecx, %ecx        /* %ecx - номер текущей команды      */
        xorl  %edi, %edi        /* %edi - номер текущей ячейки памяти 
                                                                     */

interpreter_loop:
        movl  (%ebx,%ecx,8), %eax     /* %eax - команда              */
        movl  4(%ebx,%ecx,8), %edx    /* %edx - операнд              */

        jmp   *interpreter_jump_table(,%eax,4)
.section .rodata
interpreter_jump_table:
        .long bf_op_loop_start
        .long bf_op_loop_end
        .long bf_op_move_left
        .long bf_op_move_right
        .long bf_op_inc
        .long bf_op_dec
        .long bf_op_putc
        .long bf_op_getc
        .long bf_op_exit
.text

bf_op_loop_start:
        cmpl  $0, (%esi,%edi,4)
        je    bf_op_loop_start_jump
        incl  %ecx
        jmp   interpreter_loop
bf_op_loop_start_jump:
        movl  %edx, %ecx
        jmp   interpreter_loop

bf_op_loop_end:
        movl  %edx, %ecx
        jmp   interpreter_loop

bf_op_move_left:
        movl  %edi, %eax
        subl  %edx, %eax        /* если номер новой ячейки 
                                   памяти  < 0 ...                    */
        js    memory_underflow
        movl  %eax, %edi
        incl  %ecx
        jmp   interpreter_loop

bf_op_move_right:
        movl  %edi, %eax
        addl  %edx, %eax        /* если номер новой ячейки памяти 
                                   больше допустимого...             */
        cmpl  $BF_MEMORY_CELLS, %eax
        jae   memory_overflow
        movl  %eax, %edi
        incl  %ecx
        jmp   interpreter_loop

bf_op_inc:
        addl  %edx, (%esi,%edi,4)
        incl  %ecx
        jmp   interpreter_loop

bf_op_dec:
        subl  %edx, (%esi,%edi,4)
        incl  %ecx
        jmp   interpreter_loop

bf_op_putc:
        xorl  %eax, %eax
        movb  (%esi,%edi,4), %al
        pushl %ecx
        pushl %edi
        movl  %edx, %edi
        pushl stdout
        pushl %eax
bf_op_putc_loop:
        call  fputc
        decl  %edi
        testl %edi, %edi
        jne   bf_op_putc_loop
        addl  $4, %esp
        call  fflush
        addl  $4, %esp
        popl  %edi
        popl  %ecx
        incl  %ecx
        jmp   interpreter_loop

bf_op_getc:
        pushl %ecx
        pushl %edi
        movl  %edx, %edi
        pushl stdin
bf_op_getc_loop:
        call  getc
        decl  %edi
        testl %edi, %edi
        jne   bf_op_getc_loop
        addl  $4, %esp
        movl  %eax, (%esi,%edi,4)
        popl  %edi
        popl  %ecx
        incl  %ecx
        jmp   interpreter_loop

bf_op_exit:
        xorl  %eax, %eax
        jmp   interpreter_exit

/* ******************************************* */
/* обработчики ошибок                          */
/* ******************************************* */

memory_underflow:
        pushl $str_memory_underflow
        call  printf
        movl  $1, %eax
        jmp   interpreter_exit

memory_overflow:
        pushl $str_memory_overflow
        call  printf
        movl  $1, %eax
        jmp   interpreter_exit

loop_start_not_found:
        pushl $str_loop_start_not_found
        call  printf
        movl  $1, %eax
        jmp   interpreter_exit

loop_end_not_found:
        pushl $str_loop_end_not_found
        call  printf
        movl  $1, %eax

interpreter_exit:
        movl  %ebp, %esp
        popl  %ebp
        .size main, .-main
Константин Белюстин
Константин Белюстин
Украина, г. Киев
Максим Барашков
Максим Барашков
Россия, Якутск