Украина, г. Киев |
Команды ассемблера
Размер элементов цепочки, которые обрабатывают эти команды, зависит от использованного суффикса команды.
Команда movs выполняет копирование одного элемента из цепочки-источника в цепочку-приёмник.
Команда cmps выполняет сравнение элемента из цепочки-источника и цепочки-приёмника (фактически, как и cmp, выполняет вычитание, источник - приёмник, результат никуда не записывается, но флаги устанавливаются).
Команда scas предназначена для поиска определённого элемента в цепочке. Она сравнивает содержимое регистра %eax/%ax/%al и содержимое элемента цепочки (выполняется вычитание %eax/%ax/%al - элемент_цепочки, результат не записывается, но флаги устанавливаются). Адрес цепочки должен быть помещён в регистр %edi.
После того, как эти команды выполнили своё основное действие, они увеличивают/уменьшают индексные регистры на размер элемента цепочки. Подчеркну тот факт, что эти команды обрабатывают только один элемент цепочки. Таким образом, нужно организовать что-то вроде цикла для обработки всей цепочки. Для этих целей существуют префиксы команд:
rep repe/repz repne/repnz
Эти префиксы ставятся перед командой, например: repe scas. Префикс организовывает как бы цикл из одной команды, при этом с каждым шагом цикла значение регистра %ecx автоматически уменьшается на 1.
- rep повторяет команду, пока %ecx не равен нулю.
- repe (или repz - то же самое) повторяет команду, пока %ecx не равен нулю и установлен флаг zf. Анализируя значение регистра %ecx, можно установить точную причину выхода из цикла: если %ecx равен нулю, значит, zf всегда был установлен, и вся цепочка пройдена до конца, если %ecx больше нуля - значит, флаг zf в какой-то момент был сброшен.
- repne (или repnz - то же самое) повторяет команду, пока %ecx не равен нулю и не установлен флаг zf.
Также следует указать команды для управления флагом df:
cld std
cld (CLear Direction flag) сбрасывает флаг df.
std (SeT Direction flag) устанавливает флаг df.
Пример: memcpy
Вооружившись новыми знаниями, попробуем заново изобрести функцию memcpy(3):
.data printf_format: .string "%s\n " str_in: .string "abc123()!@!777 " .set str_in_length, .-str_in .bss str_out: .space str_in_length .text /* void *my_memcpy(void *dest, const void *src, size_t n); */ my_memcpy: pushl %ebp movl %esp, %ebp pushl %esi pushl %edi movl 8(%ebp), %edi /* цепочка-назначение */ movl 12(%ebp), %esi /* цепочка-источник */ movl 16(%ebp), %ecx /* длина */ rep movsb movl 8(%ebp), %eax /* вернуть dest */ popl %edi popl %esi movl %ebp, %esp popl %ebp ret .globl main main: pushl %ebp movl %esp, %ebp pushl $str_in_length pushl $str_in pushl $str_out call my_memcpy pushl $str_out pushl $printf_format call printf movl $0, %eax movl %ebp, %esp popl %ebp ret
Вы, наверно, будете удивлены, если я вам скажу, что эта реализация memcpy всё равно не самая быстрая. "Что ещё можно сделать? " - спросите вы. Ведь мы можем копировать данные не по одному байту, а по целых 4 байта за раз при помощи movsl. Тогда у нас получается приблизительно такой алгоритм: копируем как можно больше данных блоками по 4 байта, после этого остаётся хвостик в 0, 1, 2 или 3 байта; этот остаток можно скопировать при помощи movsb. Поэтому нашу memcpy лучше переписать вот так:
/* void *my_memcpy(void *dest, const void *src, size_t n); */ my_memcpy: pushl %ebp movl %esp, %ebp pushl %esi pushl %edi movl 8(%ebp), %edi /* цепочка-назначение */ movl 12(%ebp), %esi /* цепочка-источник */ movl 16(%ebp), %edx /* длина */ movl %edx, %ecx shrl $2, %ecx /* делить на 2^2 = 4; теперь в находится %ecx количество 4-байтных кусочков */ rep movsl movl %edx, %ecx andl $3, %ecx /* $3 == $0b11, оставить только два младших бита, то есть остаток от деления на 4 */ jz 1f /* если результат 0, пропустить цепочечную команду */ rep movsb 1: movl 8(%ebp), %eax /* вернуть dest */ popl %edi popl %esi movl %ebp, %esp popl %ebp ret
Пример: strlen
Теперь strlen: нам нужно сравнить каждый байт цепочки с 0, остановиться, когда найдём 0, и вернуть количество ненулевых байт. Как счетчик мы будем использовать регистр %ecx, который автоматически изменяют все префиксы. Но префиксы уменьшают счетчик и прекращают выполнение команды, когда %ecx равен 0. Поэтому перед цепочечной командой мы поместим в %ecx число 0xffffffff, и этот регистр будет уменьшатся в ходе выполнения цепочечной команды. Результат получится в обратном коде, поэтому мы используем команду not для инвертирования всех битов. И после этого ещё уменьшим результат на 1, так как нулевой байт тоже был посчитан.
.data printf_format: .string "%u\n " str_in: .string "abc123()!@!777 " .text /* size_t my_strlen(const char *s); */ my_strlen: pushl %ebp movl %esp, %ebp pushl %edi movl 8(%ebp), %edi /* цепочка */ movl $0xffffffff, %ecx xorl %eax, %eax /* %eax = 0 */ repne scasb notl %ecx decl %ecx movl %ecx, %eax popl %edi movl %ebp, %esp popl %ebp ret .globl main main: pushl %ebp movl %esp, %ebp pushl $str_in call my_strlen pushl %eax pushl $printf_format call printf movl $0, %eax movl %ebp, %esp popl %ebp ret
Как реализованы другие стандартные цепочечные функции, можно посмотреть, например, в исходных кодах ядра Linux в файлах /usr/src/linux/arch/x86/include/asm/string_*.h,/usr/src/linux/arch/x86/lib/{mem*,str*}. Оттуда взяты все примеры для этого раздела.
В заключение обсуждения цепочечных команд нужно сказать следующее: не следует заново изобретать стандартные функции, как мы это только что сделали. Это всего лишь пример и объяснение принципов их работы. В реальных программах используйте цепочечные команды, только когда они реально смогут помочь при нестандартной обработке цепочек, а для стандартных операций лучше вызывать библиотечные функции.
Конструкция switch
Оператор switch языка Си можно переписать на ассемблере разными способами. Рассмотрим несколько вариантов того, какими могут быть значения у case:
- значения из определённого маленького промежутка (все или почти все), например, 23, 24, 25, 27, 29, 30;
- значения, между которыми большие "расстояния " на числовой прямой, например, 5, 15, 80, 3800;
- комбинированный вариант: 35, 36, 37, 38, 39, 1200, 1600, 7000.
Рассмотрим решение для первого случая. Вспомним, что команда jmp принимает адрес не только в виде непосредственного значения (метки), но и как обращение к памяти. Значит, мы можем осуществлять переход на адрес, вычисленный в процессе выполнения. Теперь вопрос: как можно вычислить адрес? А нам не нужно ничего вычислять, мы просто поместим все адреса case-веток в массив. Пользуясь проверяемым значением как индексом массива, выбираем нужный адрес case-ветки. Таким образом, процессор всё вычислит за нас. Посмотрите на следующий код:
.data printf_format: .string "%u\n " .text .globl main main: pushl %ebp movl %esp, %ebp movl $1, %eax /* получить в %eax некоторое интересующее нас значение */ /* мы предусмотрели случаи только для 0, 1, 3, поэтому, */ cmpl $3, %eax /* если %eax больше 3 (как беззнаковое), */ ja case_default /* перейти к default */ jmp *jump_table(,%eax,4) /* перейти по адресу, содержащемуся в памяти jump_table + %eax*4 */ .section .rodata .p2align 4 jump_table: /* массив адресов */ .long case_0 /* адрес этого элемента массива: jump_table + 0 */ .long case_1 /* jump_table + 4 */ .long case_default /* jump_table + 8 */ .long case_3 /* jump_table + 12 */ .text case_0: movl $5, %ecx /* тело case-блока */ jmp switch_end /* имитация break - переход в конец switch */ case_1: movl $15, %ecx jmp switch_end case_3: movl $35, %ecx jmp switch_end case_default: movl $100, %ecx switch_end: pushl %ecx /* вывести %ecx на экран, выйти */ pushl $printf_format call printf movl $0, %eax movl %ebp, %esp popl %ebp ret
Этот код эквивалентен следующему коду на Си:
#include <stdio.h > int main() { unsigned int a, c; a = 1; switch(a) { case 0: c = 5; break; case 1: c = 15; break; case 3: c = 35; break; default: c = 100; break; } printf( "%u\n ", c); return 0; }