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