Что такое test в ассемблере
Команда TEST
Команда TEST выполняет логическое И между всеми битами двух операндов. Результат никуда не записывается, команда влияет только на флаги (то есть первый операнд не изменяется). Синтаксис:
TEST ЧИСЛО1, ЧИСЛО2
В зависимости от результата могут быть изменены флаги ZF, SF, PF. Инструкция TEST всегда сбрасывает флаги OF и CF.
ЧИСЛО1 может быть одним из следующих:
ЧИСЛО2 может быть одним из следующих:
С учётом ограничений, которые были описаны выше, комбинации ЧИСЛО1-ЧИСЛО2 могут быть следующими:
Таблица истинности для логического И приведена здесь.
Отличия, разумеется, есть. Об одном уже было сказано: команда AND записывает результат операции логического И в первый операнд, а команда TEST никуда не записывает результат, а только изменяет флаги (в зависимости от результата).
Если команда AND наиболее часто используется для сброса определённых битов числа, то команда TEST обычно используется для проверки битов и совместно с командами условного перехода.
Кроме того, с помощью инструкции TEST можно определить состояние сразу нескольких битов числа.
Допустим, мы хотим узнать, сброшены ли нулевой и третий биты числа в регистре AL. Тогда можно использовать такую команду с битовой маской, где установлены 3-й и 0-й биты:
А теперь несколько примеров, которые показывают, как работает этот код.
То есть флаг нуля ZF будет установлен только в том случае, если оба бита (0-й и 3-й) сброшены.
Здесь мы проверяем, является ли число в регистре АХ чётным или нечётным. И в зависимости от результата переходим к той или иной метке.
Инструкции JNZ и JZ изучим как-нибудь в другой раз. Но если коротко, то JZ выполняет переход, если результат равен нулю, а JNZ выполняет переход, если результат НЕ равен нулю.
Что такое test в ассемблере
Методики условных вычислений на самом нижнем (двоичном) уровне основаны на четырех основных операциях двоичной алгебры: И, ИЛИ, ИСКЛЮЧАЮЩЕЕ ИЛИ и НЕ. Эти операции положены в основу работы логических схем компьютера, а также его программного обеспечения.
В системе команд процессоров семейства x 86 предусмотрены команды AND, OR, XOR, NOT, TEST и ВT, выполняющие перечисленные выше булевы операции между байтами, словами и двойными словами (табл.1).
Таблица 1. Логические команды процессора.
Выполняет операцию логического И между двумя операндами
Выполняет операцию логического ИЛИ между двумя операндами
Выполняет операцию исключающего ИЛИ между двумя операндами
Выполняет операцию логического отрицание (НЕ) единственного операнда
Выполняет операцию логического И между двумя операндами, устанавливает соответствующие флаги состояния процессора, но результат операции не записывается вместо операнда получателя данных
Копирует бит операнда получателя, номер n которого задан в исходном операнде, во флаг переноса ( CF ), а затем, в зависимости от команды, тестирует, инвертирует, сбрасывает или устанавливает этот же бит операнда получателя
8.2. Флаги состояния процессора.
Каждая команда, рассмотренная на этой лекции, влияет на состояние флагов процессора. Ранее вы узнали что, что после выполнения арифметических и логических команд процессор устанавливает соответствующее значение флагов нуля (ZF), переноса (CF), знака (ZF) и др., как описано ниже.
· Флаг нуля (Zero flag, или ZF) устанавливается, если при выполнении арифметической или логической операции получается число, равное нулю (т.е. все биты результата равны 0).
· Флаг переноса (Carry flag, или CF) устанавливается в случае, если при выполнении беззнаковой арифметической операции получается число, разрядность которого превышает разрядность выделенного для него поля результата.
· Флаг знака (Sign flag, или SF) устанавливается, если при выполнении арифметической или логической операции получается отрицательное число (т.е. старший бит результата равен 1).
· Флаг переполнения (Overflow flag, или OF) устанавливается в случае, если при выполнении арифметической операции со знаком получается число, разрядность которого превышает разрядность выделенного для него поля результата.
· Флаг четности (Parity flag, или PF) устанавливается в случае, если в результате выполнения арифметической или логической операции получается число, содержащее четное количество единичных битов.
Команда AND выполняет операцию логического И между соответствующими парами битов операндов команды и помещает результат на место операнда получателя данных:
AND получатель, источник
Существуют следующие варианты команды AND:
Команда AND может работать с 8-, 16- или 32-разрядными операндами, причем длина у обоих операндов должна быть одинаковой. При выполнении операции поразрядного логического И значение результата будет равно 1 только в том случае, если оба бита пары равны 1. В табл. 2 приведена таблица истинности для операции логического И.
Таблица 2. Таблица истинности для операции логического И.
Для выполнения этой операции можно воспользоваться двумя командами:
В данном случае полезная информация находится в четырех младших битах числа, а значения четырех старших битов для нас не имеет особого значения. В результате маскирования мы выделяем значение отдельных битов числа и помещаем их в регистр AL.
Флаги. Команда AND всегда сбрасывает флаги переполнения (OF) и переноса (CF). Кроме того, она устанавливает значения флагов знака (SF), нуля (ZF) и четности (PF) в соответствии со значением результата.
Команда OR выполняет операцию логического ИЛИ между соответствующими парами битов операндов команды и помещает результат на место операнда получателя данных:
OR получатель, источник
В команде OR используются аналогичные команде AND типы операндов:
Команда OR может работать с 8-, 16- или 32-разрядными операндами, причем длина у обоих операндов должна быть одинаковой. При выполнении операции поразрядного логического ИЛИ значение результата будет равно 1, если хотя бы один из битов пары операндов равен 1. В табл. 3 приведена таблица истинности для операции логического ИЛИ.
Таблица 3. Таблица истинности для операции логического ИЛИ.
Команда OR обычно используется для установки в единицу отдельных битов двоичного числа (например, флагов состояния процессора) по заданной маске. Если бит маски равен 0, значение соответствующего разряда числа не изменяется, а если равен 1 — то устанавливается в 1. В качестве примера на рис. 8.2 показано, как можно установить четыре младших бита 8-разрядного двоичного числа, выбрав в качестве маски число 0Fh. Значение старших битов числа при это не меняется.
С помощью команды OR можно преобразовать двоичное число, значение которого находится в диапазоне от 0 до 9 в ASCII-строке. Для этого нужно установить в единицу биты 4 и 5. Например, если в регистре AL находится число 05h, то чтобы преобразовать его в соответствующий ASCII-код, нужно выполнить операцию OR регистра AL с числом 30h. В результате получится число 35h, которое соответствует ASCII-коду цифры 5 (рис. 8.3).
Рис. 8.3. Преобразование двоичного числа в ASCII-код
с помощью команды OR.
На языке ассемблера подобное преобразование можно записать так:
Флаги. Команда OR всегда сбрасывает флаги переполнения (OF) и переноса (CF). Кроме того, она устанавливает значения флагов знака (SF), нуля (ZF) и четности (PF) в соответствии со значением результата. Например, с помощью команды OR можно определить, какое значение находится в регистре (отрицательное, положительное или нуль). Для этого вначале нужно выполнить команду OR, указав в качестве операндов один и тот же регистр, например:
а затем – проанализировать значение флагов, как показано в табл. 4.
Таблица 4. Определение значения числа по флагам состояния процессора.
Команда XOR выполняет операцию ИСКЛЮЧАЮЩЕГО ИЛИ между соответствующими парами битов операндов команды и помещает результат на место операнда получателя данных:
XOR получатель, источник
В команде XOR используются аналогичные командам AND и OR типы операндов:
Команда XOR может работать с 8-, 16- или 32-разрядными операндами, причем длина у обоих операндов должна быть одинаковой. При выполнении операции поразрядного ИСКЛЮЧАЮЩЕГО ИЛИ значение результата будет равно 1, если значения битов пары операндов различны, и 0 — если значения битов равны. В табл. 5 приведена таблица истинности для операции логического ИСКЛЮЧАЮЩЕГО ИЛИ.
Таблица 6.5. Таблица истинности для операции ИСКЛЮЧАЮЩЕГО ИЛИ.
Как следует из таблицы, при выполнении операции ИСКЛЮЧАЮЩЕГО ИЛИ с нулевым битом получается исходное значение бита, а с единичным битом — значение исходного бита инвертируется.
Таблица 6. Демонстрация свойства реверсивности операции ИСКЛЮЧАЮЩЕГО ИЛИ.
Флаги. Команда XOR всегда сбрасывает флаги переполнения (OF) и переноса (CF). Кроме того, она устанавливает значения флагов знака (SF), нуля (ZF) и четности (PF) в соответствии со значением результата.
Проверка флага четности (PF). Флаг четности позволяет узнать, какое количество единичных битов (четное или нечетное) содержится в младшем байте результата выполнения логической или арифметической команды. Если этот флаг установлен, значит, в результате получилось четное количество единичных битов, а если сброшен, то нечетное. Количество единичных битов можно проверить, не меняя значения результата. Для этого сначала нужно выполнить команду XOR с нулевым значением (т.е. с числом, все биты которого равны нулю), а затем проверить флаг четности:
В отладчиках часто для обозначения четного количества единиц в полученном результате используется аббревиатура РЕ (т.е. Parity Even), а для нечетного — РО (т.е. Parity Odd).
Четность в 16-разрядных словах. Выше мы уже говорили о том, что флаг четности PF устанавливается в зависимости от количества единиц, содержащихся в младших восьми разрядах результата. Для выполнения контроля по четности 16-разрядных операндов, нужно выполнить команду XOR между старшим и младшим байтами этого числа:
Таким образом, 16-разрядный операнд разбивается на 2 группы по 8 битов. При выполнении команды XOR единичные биты, находящиеся в соответствующих позициях двух 8-разрядных операндов, не будут учитываться, поскольку соответствующий бит результата равен нулю. Эта команда удаляет из результата любые пересекающиеся единичные биты двух 8-разрядных операндов и добавляет в результат непересекающиеся единичные биты. Следовательно, четность полученного нами 8-разрядного операнда будет такой же, как и четность исходного 16-разрядного числа.
А если нам нужно оценить четность 32-разрядного числа? Тогда, пронумеровав его байты, соответственно, В0, В1, В2 и В3, четность можно определить по следующей формуле: В0 XOR В1, XOR B 2 XOR В3.
Команда NOT позволяет выполнить инверсию всех битов операнда, в результате чего получается обратный код числа. В команде допускаются следующие типы операндов:
Например, обратный код числа F 0 h равен 0 Fh :
not al ; AL = 00001111b
Флаги. Команда NOT не изменяет флаги процессора.
Команда TEST выполняет операцию поразрядного логического И между соответствующими парами битов операндов и, в зависимости от полученного результата, устанавливает флаги состояния процессора. При этом, в отличие от команды AND, значение операнда получателя данных не изменяется. В команде TEST используются аналогичные команде AND типы операндов. Обычно команда TEST применяется для анализа значения отдельных битов числа по маске.
Пример: тестирование нескольких битов. С помощью команды TEST можно определить состояние сразу нескольких битов числа. Предположим, мы хотим узнать, установлен ли нулевой и третий биты регистра AL. Для этого можно воспользоваться такой командой:
test al,00001001b ; Тестируем биты 0 и 3
Как показано в приведенных ниже примерах, флаг нуля ZF будет установлен только в том случае, если все тестируемые биты сброшены:
Флаги. Команда TEST всегда сбрасывает флаги переполнения (OF) и переноса (CF). Кроме того, она устанавливает значения флагов знака (SF), нуля (ZF) и четности (PF) в соответствии со значением результата выполнения операции логического И (как и команда AND).
Команда СМР вычитает исходный операнд из операнда получателя данных и, в зависимости от полученного результата, устанавливает флаги состояния процессора. При этом, в отличие от команды SUB, значение операнда получателя данных не изменяется.
СМР получатель, источник
В команде СМР используются аналогичные команде AND типы операндов.
Флаги. Команда СМР изменяет состояние следующих флагов: CF (флаг переноса), ZF (флаг нуля), SF (флаг знака), OF (флаг переполнения), AF (флаг служебного переноса), PF (флаг четности). Они устанавливаются в зависимости от значения, которое было бы получено в результате применения команды SUB. Например, как показано в табл. 7, после выполнения команды СМР, по состоянию флагов нуля (ZF) и переноса (CF) можно судить о величинах сравниваемых между собой беззнаковых операндов.
Таблица 7. Состояние флагов после сравнения беззнаковых операндов с помощью команды СМР.
Как работает инструкция «Test»
Исходя из описания, она используется для проверки битов. Т.е, в случае
Ошибка компиляции «error: parser: инструкция ожидается»
задание: напишем программу, работающую по алгоритму: (a) вывести приглашение, типа «Введите.
Программа «Жизнь», работает не так, как описано в книге
Всем привет! Написал прогу «жизнь» взятую из книги зубкова. Писать старался самостоятельно, поэтому.
Как исправить ошибку «warning LNK4089: all references to «winmm.dll» discarded by /OPT:REF»
В данном коде выдает ошибку «warning LNK4089: all references to «winmm.dll» discarded by /OPT:REF».
Добавлено через 4 минуты
Даны числа «x» и «z», если их сумма кратна 3, то вывести «1», если нет, то 0
Даны числа «x» и «z», если их сумма кратна 3, то вывести «1», если нет, то 0.
В словах, которые имеют окончание «ing», сделать замену «ing» на «ed»
Задан текст. Группы символов, разделённые пробелами (одним или несколькими) и не содержащими.
Почему не работает функция «Нажмите клавишу.»
Вычитал,что данные строчки кода,вызывают функцию «Нажмите клавишу. «,но почему то ничего не.
Записки программиста
Шпаргалка по основным инструкциям ассемблера x86/x64
В прошлой статье мы написали наше первое hello world приложение на асме, научились его компилировать и отлаживать, а также узнали, как делать системные вызовы в Linux. Сегодня же мы познакомимся непосредственно с ассемблерными инструкциями, понятием регистров, стека и вот этого всего. Ассемблеры для архитектур x86 (a.k.a i386) и x64 (a.k.a amd64) очень похожи, в связи с чем нет смысла рассматривать их в отдельных статьях. Притом акцент я постараюсь делать на x64, попутно отмечая отличия от x86, если они есть. Далее предполагается, что вы уже знаете, например, чем стек отличается от кучи, и объяснять такие вещи не требуется.
Регистры общего назначения
Регистр — это небольшой (обычно 4 или 8 байт) кусочек памяти в процессоре с чрезвычайно большой скоростью доступа. Регистры делятся на регистры специального назначения и регистры общего назначения. Нас сейчас интересуют регистры общего назначения. Как можно догадаться по названию, программа может использовать эти регистры под свои нужды, как ей вздумается.
На x86 доступно восемь 32-х битных регистров общего назначения — eax, ebx, ecx, edx, esp, ebp, esi и edi. Регистры не имеют заданного наперед типа, то есть, они могут трактоваться как знаковые или беззнаковые целые числа, указатели, булевы значения, ASCII-коды символов, и так далее. Несмотря на то, что в теории эти регистры можно использовать как угодно, на практике обычно каждый регистр используется определенным образом. Так, esp указывает на вершину стека, ecx играет роль счетчика, а в eax записывается результат выполнения операции или процедуры. Существуют 16-и битные регистры ax, bx, cx, dx, sp, bp, si и di, представляющие собой 16 младших бит соответствующих 32-х битных регистров. Также доступны и 8-и битовые регистры ah, al, bh, bl, ch, cl, dh и dl, которые представляют собой старшие и младшие байты регистров ax, bx, cx и dx соответственно.
Рассмотрим пример. Допустим, выполняются следующие три инструкции:
Значения регистров после записи в eax значения 0 x AABBCCDD:
Значения после записи в регистр al значения 0 x EE:
Значения регистров после записи в ax числа 0 x 1234:
Как видите, ничего сложного.
На x64 размер регистров был увеличен до 64-х бит. Соответствующие регистры получили название rax, rbx, и так далее. Кроме того, регистров общего назначения стало шестнадцать вместо восьми. Дополнительные регистры получили названия r8, r9, …, r15. Соответствующие им регистры, которые представляют младшие 32, 16 и 8 бит, получили название r8d, r8w, r8b, и по аналогии для регистров r9-r15. Кроме того, появились регистры, представляющие собой младшие 8 бит регистров rsi, rdi, rbp и rsp — sil, dil, bpl и spl соответственно.
Про адресацию
Как уже отмечалось, регистры могут трактоваться, как указатели на данные в памяти. Для разыменования таких указателей используется специальный синтаксис:
Эта запись означает «прочитай 8 байт по адресу, записанному в регистре rsp, и сохрани их в регистр rax». При запуске программы rsp указывает на вершину стека, где хранится число аргументов, переданных программе (argc), указатели на эти аргументы, а также переменные окружения и кое-какая другая информация. Таким образом, в результате выполнения приведенной выше инструкции (разумеется, при условии, что перед ней не выполнялось каких-либо других инструкций) в rax будет записано количество аргументов, с которыми была запущена программа.
В одной команде можно указывать адрес и смешение (как положительное, так и отрицательное) относительно него:
Эта запись означает «возьми rsp, прибавь к нему 8, прочитай 8 байт по получившемуся адресу и положи их в rax». Таким образом, в rax будет записан адрес строки, представляющей собой первый аргумент программы, то есть, имя исполняемого файла.
При работе с массивами бывает удобно обращаться к элементу с определенным индексом. Соответствующий синтаксис:
Читается так: «посчитай rcx*8 + rsp + 16, и поменяй местами 8 байт (размер регистра) по получившемуся адресу и значение регистра rax». Другими словами, rsp и 16 все так же играют роль смещения, rcx играет роль индекса в массиве, а 8 — это размер элемента массива. При использовании данного синтаксиса допустимыми размерами элемента являются только 1, 2, 4 и 8. Если требуется какой-то другой размер, можно использовать инструкции умножения, бинарного сдвига и прочие, которые мы рассмотрим далее.
Наконец, следующий код тоже валиден:
.data
msg :
. ascii «Hello, world!\n»
. text
В смысле, что можно не указывать регистр со смещением или вообще какие-либо регистры. В результате выполнения этого кода в регистры al и ah будет записан ASCII-код буквы H, или 0 x 48.
В этом контексте хотелось бы упомянуть еще одну полезную ассемблерную инструкцию:
Инструкция lea очень удобна, так как позволяет сразу выполнить умножение и несколько сложений.
Fun fact! На x64 в байткоде инструкций никогда не используются 64-х битовые смещения. В отличие от x86, инструкции часто оперируют не абсолютными адресами, а адресами относительно адреса самой инструкции, что позволяет обращаться к ближайшим +/- 2 Гб оперативной памяти. Соответствующий синтаксис:
Как видите, «относительный» mov еще и на один байт короче! Что это за регистр такой rip мы узнаем чуть ниже.
Для записи же полного 64-х битового значения в регистр предусмотрена специальная инструкция:
Другими словами, процессоры x64 так же экономно кодируют инструкции, как и процессоры x86, и в наше время нет особо смысла использовать процессоры x86 в системах, имеющих пару гигабайт оперативной памяти или меньше (мобильные устройства, холодильники, микроволновки, и так далее). Скорее всего, процессоры x64 будут даже более эффективны за счет большего числа доступных регистров и большего размера этих регистров.
Арифметические операции
Рассмотрим основные арифметические операции:
# инкремент: rax = rax + 1 = 124
inc % rax
Здесь и далее операндами могут быть не только регистры, но и участки памяти или константы. Но оба операнда не могут быть участками памяти. Это правило применимо ко всем инструкциям ассемблера x86/x64, по крайней мере, из рассмотренных в данной статье.
В данном примере инструкция mul умножает al на cl, и сохраняет результат умножения в пару регистров al и ah. Таким образом, ax примет значение 0 x 12C или 300 в десятичной нотации. В худшем случае для сохранения результата перемножения двух N-байтовых значений может потребоваться до 2*N байт. В зависимости от размера операнда результат сохраняется в al:ah, ax:dx, eax:edx или rax:rdx. Притом в качестве множителей всегда используется первый из этих регистров и переданный инструкции аргумент.
Знаковое умножение производится точно так же при помощи инструкции imul. Кроме того, существуют варианты imul с двумя и тремя аргументами:
Инструкции div и idiv производят действия, обратные mul и imul. Например:
# rax = rdx:rax / rcx = 3
# rdx = rdx:rax % rcx = 87
div % rcx
Как видите, был получен результат целочисленного деления, а также остаток от деления.
Это далеко не все арифметические инструкции. Например, есть еще adc (сложение с учетом флага переноса), sbb (вычитание с учетом займа), а также соответствующие им инструкции, выставляющие и очищающие соответствующие флаги (ctc, clc), и многие другие. Но они распространены намного меньше, и потому в рамках данной статьи не рассматриваются.
Логические и битовые операции
Как уже отмечалось, особой типизации в ассемблере x86/x64 не предусмотрено. Поэтому не стоит удивляться, что в нем нет отдельных инструкций для выполнения булевых операций и отдельных для выполнения битовых операций. Вместо этого есть один набор инструкций, работающих с битами, а уж как интерпретировать результат — решает конкретная программа.
Так, например, выглядит вычисление простейшего логического выражения:
Заметьте, что здесь мы использовали по одному младшему биту в каждом из 64-х битовых регистров. Таким образом, в старших битах образуется мусор, который мы обнуляем последней командой.
Еще одна полезная инструкция — это xor (исключающее или). В логических выражениях xor используется нечасто, однако с его помощью часто происходит обнуление регистров. Если посмотреть на опкоды инструкций, то становится понятно, почему:
Как видите, инструкции xor и inc кодируются всего лишь тремя байтами каждая, в то время, как делающая то же самое инструкция mov занимает целых семь байт. Каждый отдельный случай, конечно, лучше бенчмаркать отдельно, но общее эвристическое правило такое — чем короче код, тем больше его помещается в кэши процессора, тем быстрее он работает.
В данном контексте также следует вспомнить инструкции побитового сдвига, тестирования битов (bit test) и сканирования битов (bit scan):
Еще есть битовые сдвиги со знаком (sal, sar), циклические сдвиги с флагом переноса (rcl, rcr), а также сдвиги двойной точности (shld, shrd). Но используются они не так уж часто, да и утомишься перечислять вообще все инструкции. Поэтому их изучение я оставляю вам в качестве домашнего задания.
Условные выражения и циклы
Выше несколько раз упоминались какие-то там флаги, например, флаг переноса. Под флагами понимаются биты специального регистра eflags / rflags (название на x86 и x64 соответственно). Напрямую обращаться к этому регистру при помощи инструкций mov, add и подобных нельзя, но он изменяется и используется различными инструкциями косвенно. Например, уже упомянутый флаг переноса (carry flag, CF) хранится в нулевом бите eflags / rflags и используется, например, в той же инструкции bt. Еще из часто используемых флагов можно назвать zero flag (ZF, 6-ой бит), sign flag (SF, 7-ой бит), direction flag (DF, 10-ый бит) и overflow flag (OF, 11-ый бит).
В результате значение rax будет равно единице, так как первая инструкция inс будет пропущена. Заметьте, что адрес перехода также может быть записан в регистре:
Впрочем, на практике такого кода лучше избегать, так как он ломает предсказание переходов и потому менее эффективен.
Условные переходы обычно осуществляются при помощи инструкции cmp, которая сравнивает два своих операнда и выставляет соответствующие флаги, за которой следует инструкция из семейства je, jg и подобных:
je 1f # перейти, если равны (equal)
jl 1f # перейти, если знаково меньше (less)
jb 1f # перейти, если беззнаково меньше (below)
jg 1f # перейти, если знаково больше (greater)
ja 1f # перейти, если беззнаково больше (above)
Существует также инструкции jne (перейти, если не равны), jle (перейти, если знаково меньше или равны), jna (перейти, если беззнаково не больше) и подобные. Принцип их именования, надеюсь, очевиден. Вместо je / jne часто пишут jz / jnz, так как инструкции je / jne просто проверяют значение ZF. Также есть инструкции, проверяющие другие флаги — js, jo и jp, но на практике они используются редко. Все эти инструкции вместе взятые обычно называют jcc. То есть, вместо конкретных условий пишутся две буквы «c», от «condition». Здесь можно найти хорошую сводную таблицу по всем инструкциям jcc и тому, какие флаги они проверяют.
Помимо cmp также часто используют инструкцию test:
Fun fact! Интересно, что cmp и test в душе являются теми же sub и and, только не изменяют своих операндов. Это знание можно использовать для одновременного выполнения sub или and и условного перехода, без дополнительных инструкций cmp или test.
Еще из инструкций, связанных с условными переходами, можно отметить следующие.
Инструкция jrcxz осуществляет переход только в том случае, если значение регистра rcx равно нулю.
Инструкции семейства cmovcc (conditional move) работают как mov, но только при выполнении заданного условия, по аналогии с jcc.
Инструкции setcc присваивают однобайтовому регистру или байту в памяти значение 1, если заданное условие выполняется, и 0 иначе.
Сравнить rax с заданным куском памяти. Если равны, выставить ZF и сохранить по указанному адресу значение указанного регистра, в данном примере rcx. Иначе очистить ZF и загрузить значение из памяти в rax. Также оба операнда могут быть регистрами.
Инструкция cmpxchg8b главным образом нужна в x86. Она работает аналогично cmpxchg, только производит compare and swap сразу 8-и байт. Регистры edx:eax используются для сравнения, а регистры ecx:ebx хранят то, что мы хотим записать. Инструкция cmpxchg16b по тому же принципу производит compare and swap сразу 16-и байт на x64.
Важно! Примите во внимание, что без префикса lock все эти compare and swap инструкции не атомарны.
Не нужно быть семи пядей во лбу, чтобы изобразить при помощи этих инструкций конструкцию if-then-else или циклы for / while, поэтому двигаемся дальше.
«Строковые» операции
Рассмотрим следующий кусок кода:
В регистры rsi и rdi кладутся адреса двух строк. Командой cld очищается флаг направления (DF). Инструкция, выполняющая обратное действие, называется std. Затем в дело вступает инструкция cmpsb. Она сравнивает байты (%rsi) и (%rdi) и выставляет флаги в соответствии с результатом сравнения. Затем, если DF = 0, rsi и rdi увеличиваются на единицу (количество байт в том, что мы сравнивали), иначе — уменьшаются. Аналогичные инструкции cmpsw, cmpsl и cmpsq сравнивают слова, длинные слова и четверные слова соответственно.
Инструкции cmps интересны тем, что могут использоваться с префиксом rep, repe (repz) и repne (repnz). Например:
Префикс rep повторяет инструкцию заданное в регистре rcx количество раз. Префиксы repz и repnz делают то же самое, но только после каждого выполнения инструкции дополнительно проверяется ZF. Цикл прерывается, если ZF = 0 в случае c repz и если ZF = 1 в случае с repnz. Таким образом, приведенный выше код проверяет равенство двух буферов одинакового размера.
Аналогичные инструкции movs перекладывает данные из буфера, адрес которого указан в rsi, в буфер, адрес которого указан в rdi (легко запомнить — rsi значит source, rdi значит destination). Инструкции stos заполняет буфер по адресу из регистра rdi байтами из регистра rax (или eax, или ax, или al, в зависимости от конкретной инструкции). Инструкции lods делают обратное действие — копируют байты по указанному в rsi адресу в регистр rax. Наконец, инструкции scas ищут байты из регистра rax (или соответствующих регистров меньшего размера) в буфере, адрес которого указан в rdi. Как и cmps, все эти инструкции работают с префиксами rep, repz и repnz.
Работа со стеком и процедуры
Со стеком все очень просто. Инструкция push кладет свой аргумент на стек, а инструкция pop извлекает значение со стека. Например, если временно забыть про инструкцию xchg, то поменять местами значение двух регистров можно так:
Существуют инструкции, помещающие на стек и извлекающие с него регистр rflags / eflags:
А так, к примеру, можно получить значение флага CF:
На x86 также существуют инструкции pusha и popa, сохраняющие на стеке и восстанавливающие с него значения всех регистров. В x64 этих инструкций больше нет. Видимо, потому что регистров стало больше и сами регистры теперь длиннее — сохранять и восстанавливать их все стало сильно дороже.
Процедуры, как правило, «создаются» при помощи инструкций call и ret. Инструкция call кладет на стек адрес следующей инструкции и передает управление по указанному в аргументе адресу. Инструкция ret читает со стека адрес возврата и передает по нему управление. Например:
# выход из процедуры
ret
Как правило, возвращаемое значение передается в регистре rax или, если его размера не достаточно, записывается в структуру, адрес которой передается в качестве аргумента. К вопросу о передаче аргументов. Соглашений о вызовах существует великое множество. В одних все аргументы всегда передаются через стек (отдельный вопрос — в каком порядке) и за очистку стека от аргументов отвечает сама процедура, в других часть аргументов передается через регистры, а часть через стек, и за очистку стека от аргументов отвечает вызывающая сторона, плюс множество вариантов посередине, с отдельными правилами касательно выравнивания аргументов на стеке, передачи this, если это ООП язык, и так далее. В общем случае для произвольно взятой архитектуры, компилятора и языка программирования соглашение о вызовах может быть вообще каким угодно.
Для примера рассмотрим ассемблерный код, сгенерированный CLang 3.8 для простой программки на языке C под x64. Так выглядит одна из процедур:
# типичный пролог процедуры
# регистр rsp не изменяется, так как процедура не вызывает никаких
# других процедур
400950: 55 push %rbp
400951: 48 89 e5 mov %rsp,%rbp
# типичный эпилог
4009a3: 5d pop %rbp
4009a4: c3 retq
Как видите, два аргумента были переданы процедуре через регистры rdi и rsi. По всей видимости, используется конвенция под названием System V AMD64 ABI. Утверждается, что это стандарт де-факто под x64 на *nix системах. Я не вижу смысла пересказывать описание этой конвенции здесь, заинтересованные читатели могут ознакомиться с полным описанием по приведенной ссылке.
Заключение
Еще интересный топик, оставшийся за кадром — это атомарные операции, барьеры памяти, спинлоки и вот это все. Например, compare and swap часто реализуется просто как инструкция cmpxchg с префиксом lock. По аналогии реализуется атомарный инкремент, декремент, и прочее. Увы, все это тянет на тему для отдельной статьи.
В качестве источников дополнительной информации можно рекомендовать книгу Modern X86 Assembly Language Programming, и, конечно же, мануалы от Intel. Также довольно неплоха книга x86 Assembly на wikibooks.org.
Из онлайн-справочников по ассемблерным инструкциям стоит обратить внимание на следующие:
А знаете ли вы ассемблер, и если да, то находите ли это знание полезным?