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