lea assembler что это

Команда LEA

Команда LEA в Ассемблере вычисляет эффективный адрес ИСТОЧНИКА и помещает его в ПРИЁМНИК. Синтаксис:

LEA ПРИЁМНИК, ИСТОЧНИК

После выполнения этой команды флаги не изменяются.

Что такое эффективный адрес

Прежде чем продолжить рассказ об инструкции LEA, напомню, что такое эффективный адрес.

Так вот, слово “эффективный” можно перевести на русский как “действенный”, “действующий”, “настоящий”. Что касается программистской терминологии, то в некоторых источниках вместо “эффективный адрес” встречается словосочетание “текущий адрес” или даже “виртуальный адрес”.

Слишком глубоко в адресацию погружаться не будем. Если вы совершенно далеки от этого, то можете изучить мою контрольную работу по этой теме университетских времён (эх, давно это было…)

БАЗА + СМЕЩЕНИЕ + ИНДЕКС

Любая из частей эффективного адреса может отсутствовать (например, необязательно указывать СМЕЩЕНИЕ или ИНДЕКС), но обязательно должна присутствовать хотя бы одна часть (например, только БАЗА).

Вычисление эффективного адреса

Ну а теперь чуть подробнее о самой команде LEA. Как уже было сказано, она выполняет вычисление адреса в Ассемблере. В итоге в ПРИЁМНИК записывается адрес памяти (точнее, только смещение).

С помощью команды LEA можно вычислить адрес переменной, которая описана сложным способом адресации (например, по базе с индексированием, что часто используется при работе с массивами и строками).

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

Команду LEA также удобно применять для определения адреса параметра, находящегося в стеке. Например, если в процедуре определяется локальный массив, то для работы с ним часто необходимо загрузить его смещение в индексный регистр (что как раз таки можно сделать командой LEA).

Оператор OFFSET позволяет определить смещение только при компиляции, и в отличие от него команда LEA может сделать это во время выполнения программы. Хотя в остальных случаях обычно вместо LEA используют MOV и OFFSET, то есть

LEA ПРИЁМНИК, ИСТОЧНИК

это то же самое, что и

MOV ПРИЁМНИК, offset ИСТОЧНИК

При этом следует помнить об указанных выше ограничениях применения оператора OFFSET.

Команда LEA в арифметических операциях

Инструкция LEA часто используется для арифметических операций, таких как умножение и сложение. Преимущество такого способа в том, что команда LEA занимает меньше места, чем команды арифметических операций. Кроме того, в отличие от последних, она не изменяет флаги. Примеры:

Обратите внимание на то, что адресацию со смещением, где используется знак умножения (*), не поддерживает эмулятор emu8086 (возможно, некоторые другие эмуляторы тоже). Поэтому в данном эмуляторе первый пример не будет работать правильно. Второй же пример (сложение), будет работать.

Напоследок, как всегда, о происхождении аббревиатуры LEA.

Источник

Система команд x86

Влияние команды на флаги и форматы команды:

Вычислить эффективный адрес операнда m и поместить результат в r16

Вычислить эффективный адрес операнда m и поместить результат в r32

Описание:

Команда LEA вычисляет эффективный адрес (смещение) операнда-источника команды (который должен быть операндом в памяти) и сохраняет его в регистре — операнде-назначении команды. Атрибут размера операнда команды определяется выбранным регистром (16- или 32-битным). Атрибут размера адреса определяется атрибутом текущего кодового сегмента. Атрибуты размера адреса и размера операнда воздействуют на операцию, выполняемую командой LEA так, как это показано в таблице 6.65.

Таблица 6.65. Воздействие атрибутов размера операнда и размера адреса на действие команды LEA

Размер операнда

Размер адреса

Выполняемая операция

Вычисляется 16-битный эффективный адрес и сохраняется в 16-битном регистре-назначении

Вычисляется 32-битный эффективный адрес его младшие 16 бит сохраняются в 16-битном регистре-назначении

Вычисляется 16-битный эффективный адрес, затем знакорасширяется и сохраняется в 32-битном регистре-назначении

Вычисляется 32-битный эффективный адрес и сохраняется в 32-битном регистре-назначении

Операция:

IF OperandSize = 16 AND AddressSize = 16

DEST = EffectiveAddress(SRC); (* 16- битный адрес *)

ELSE IF OperandSize = 16 AND AddressSize = 32 THEN

temp = EffectiveAddress(SRC); (* 32- битный адрес *)

DEST = temp[0..15]; (* 16- битный адрес *)

ELSE IF OperandSize = 32 AND AddressSize = 16 THEN

temp = EffectiveAddress(SRC); (* 16- битный адрес *)

DEST = ZeroExtend(temp); (* 32- битный адрес *)

ELSE IF OperandSize = 32 AND AddressSize = 32 THEN

DEST = EffectiveAddress(SRC); (* 32- битный адрес *)

Особые ситуации защищенного режима:

#UD, если второй операнд не является операндом в памяти.

Особые ситуации режима реальной адресации:

#UD, если второй операнд не является операндом в памяти.

Особые ситуации режима V86:

#UD, если второй операнд не является операндом в памяти.

Замечание:

Различные ассемблеры могут по разному интерпретировать команду в зависимости от атрибута размера и символической ссылки второго операнда.

Источник

What’s the purpose of the LEA instruction?

For me, it just seems like a funky MOV. What’s its purpose and when should I use it?

lea assembler что это

17 Answers 17

As others have pointed out, LEA (load effective address) is often used as a «trick» to do certain computations, but that’s not its primary purpose. The x86 instruction set was designed to support high-level languages like Pascal and C, where arrays—especially arrays of ints or small structs—are common. Consider, for example, a struct representing (x, y) coordinates:

Now imagine a statement like:

From the «Zen of Assembly» by Abrash:

What does that give us? Two things that ADD doesn’t provide:

And LEA does not alter the flags.

Other usecase is handy in loops: the difference between LEA EAX, [ EAX + 1 ] and INC EAX is that the latter changes EFLAGS but the former does not; this preserves CMP state.

Despite all the explanations, LEA is an arithmetic operation:

It’s just that its name is extremelly stupid for a shift+add operation. The reason for that was already explained in the top rated answers (i.e. it was designed to directly map high level memory references).

Maybe just another thing about LEA instruction. You can also use LEA for fast multiplying registers by 3, 5 or 9.

lea assembler что это

lea is an abbreviation of «load effective address». It loads the address of the location reference by the source operand to the destination operand. For instance, you could use it to:

to move ebx pointer eax items further (in a 64-bit/element array) with a single instruction. Basically, you benefit from complex addressing modes supported by x86 architecture to manipulate pointers efficiently.

lea assembler что это

The biggest reason that you use LEA over a MOV is if you need to perform arithmetic on the registers that you are using to calculate the address. Effectively, you can perform what amounts to pointer arithmetic on several of the registers in combination effectively for «free.»

What’s really confusing about it is that you typically write an LEA just like a MOV but you aren’t actually dereferencing the memory. In other words:

This will move the effective address EBX * 8 into EAX, not what is found in that location. As you can see, also, it is possible to multiply by factors of two (scaling) while a MOV is limited to adding/subtracting.

lea assembler что это

The 8086 has a large family of instructions that accept a register operand and an effective address, perform some computations to compute the offset part of that effective address, and perform some operation involving the register and the memory referred to by the computed address. It was fairly simple to have one of the instructions in that family behave as above except for skipping that actual memory operation. Thus, the instructions:

were implemented almost identically internally. The difference is a skipped step. Both instructions work something like:

If one wanted to use something like stosw to store data to a BP-relative address, being able to say

was more convenient than:

lea assembler что это

The LEA (Load Effective Address) instruction is a way of obtaining the address which arises from any of the Intel processor’s memory addressing modes.

That is to say, if we have a data move like this:

it moves the contents of the designated memory location into the target register.

LEA is not a specific arithmetic instruction; it is a way of intercepting the effective address arising from any one of the processor’s memory addressing modes.

For instance, we can use LEA on just a simple direct address. No arithmetic is involved at all:

This is valid; we can test it at the Linux prompt:

Here, there is no addition of a scaled value, and no offset. Zero is moved into EAX. We could do that using MOV with an immediate operand also.

This is the reason why people who think that the brackets in LEA are superfluous are severely mistaken; the brackets are not LEA syntax but are part of the addressing mode.

LEA is real at the hardware level. The generated instruction encodes the actual addressing mode and the processor carries it out to the point of calculating the address. Then it moves that address to the destination instead of generating a memory reference. (Since the address calculation of an addressing mode in any other instruction has no effect on CPU flags, LEA has no effect on CPU flags.)

Contrast with loading the value from address zero:

Of course, this LEA encoding is longer than moving an immediate zero into EAX :

There is no reason for LEA to exclude this possibility though just because there is a shorter alternative; it’s just combining in an orthogonal way with the available addressing modes.

lea assembler что это

As the existing answers mentioned, LEA has the advantages of performing memory addressing arithmetic without accessing memory, saving the arithmetic result to a different register instead of the simple form of add instruction. The real underlying performance benefit is that modern processor has a separate LEA ALU unit and port for effective address generation (including LEA and other memory reference address), this means the arithmetic operation in LEA and other normal arithmetic operation in ALU could be done in parallel in one core.

Check this article of Haswell architecture for some details about LEA unit: http://www.realworldtech.com/haswell-cpu/4/

lea assembler что это

The LEA instruction can be used to avoid time consuming calculations of effective addresses by the CPU. If an address is used repeatedly it is more effective to store it in a register instead of calculating the effective address every time it is used.

It seems that lots of answers already complete, I’d like to add one more example code for showing how the lea and move instruction work differently when they have the same expression format.

To make a long story short, lea instruction and mov instructions both can be used with the parentheses enclosing the src operand of the instructions. When they are enclosed with the (), the expression in the () is calculated in the same way; however, two instructions will interpret the calculated value in the src operand in a different way.

Whether the expression is used with the lea or mov, the src value is calculated as below.

D ( Rb, Ri, S ) => (Reg[Rb]+S*Reg[Ri]+ D)

However, when it is used with the mov instruction, it tries to access the value pointed to by the address generated by the above expression and store it to the destination.

In contrast of it, when the lea instruction is executed with the above expression, it loads the generated value as it is to the destination.

The below code executes the lea instruction and mov instruction with the same parameter. However, to catch the difference, I added a user-level signal handler to catch the segmentation fault caused by accessing a wrong address as a result of mov instruction.

Example code

Execution result

lea assembler что это

lea assembler что это

Here is an example.

LEA : just an «arithmetic» instruction..

MOV transfers data between operands but lea is just calculating

lea assembler что это

All normal «calculating» instructions like adding multiplication, exclusive or set the status flags like zero, sign. If you use a complicated address, AX xor:= mem[0x333 +BX + 8*CX] the flags are set according to the xor operation.

Now you may want to use the address multiple times. Loading such an addres into a register is never intended to set status flags and luckily it doesn’t. The phrase «load effective address» makes the programmer aware of that. That is where the weird expression comes from.

It is clear that once the processor is capable of using the complicated address to process its content, it is capable of calculating it for other purposes. Indeed it can be used to perform a transformation x in one instruction. This is a general rule in assembly programming: Use the instructions however it rocks your boat. The only thing that counts is whether the particular transformation embodied by the instruction is useful for you.

have the same effect on AX but not on the status flags. (This is ciasdis notation.)

LEA vs MOV (reply to the original question)

LDS & LES (optional further reading)

These are handy instructions for 16-bit mode, be it 16-bit real mode or 16-bit protected mode.

Under 32-bit mode, there is no need in these instructions since OSes set all segment bases to zero (flat memory model), so there is no need to load segment registers. We just use 32-bit pointers, not 48.

Источник

MS-DOS и TASM 2.0. Часть 18. Ещё раз об указателе.

lea assembler что это

Указатель в программировании.

В статье MS-DOS и TASM 2.0. Часть 9. Указатель просто и понятно было рассмотрено, что такое указатель в программировании (pointer). Сейчас мы перейдём к вопросу практического использования указателя. Ещё раз напомним, что указатель в ассемблере — более широкое понятие, чем в Си и С++, где указатель определён как переменная, значением которой является адрес ячейки памяти. Указатель — не только переменная. Указатель в программировании на ассемблере — адрес определённой ячейки памяти. Жёсткой привязки к понятию «переменной» нет.

Преимущество указателя — простая возможность обращаться к определённой части исполняемого кода либо данных, избегая их дублирования. Например, один раз написав код функции, мы можем обращаться к нему неоднократно, осуществляя вызов указанной функции. Кстати, вызов функции — это переход исполнения кода по указателю, который для удобства «обозвали» понятным для человека названием (ну, например, «MyBestFunc»).

Указатель в программировании используется также для получения и передачи входных-выходных значений функций. С этим применением мы встретимся при Windows программировании.

Указатель — адрес ячейки памяти.

Указатель в программировании на ассемблере — адрес ячейки памяти, содержащей определённые последовательности цифр — блоки кода и данных. Блоки кода и данных называются значениями указателя, например: строка, массив, структура, функция, переменная, константа.

lea assembler что это Указатель — адрес ячейки памяти, содержащей блоки кода и данных.

Указателю можно присвоить условное обозначение (const_a, const_b, my_mass_1, MY_STRUCT_1, BitMask, my_prnt_func), определив тип данных или кода, на которые он указывает (db, dd, STRUC, RECORD, proc). Практически мы уже проделывали эти операции, создавая исходники наших простейших программ:

Работа с указателями.

Основная проблема начинающего программиста — это различие между указателем и значением, расположенным по адресу памяти, на которую указывает указатель.

Получение значения, на которое указывает указатель в Си и C++ называется «разыменование указателя». Это понятие можно употреблять и в ассемблере.

Получать адрес ячейки памяти (значение указателя) при программировании на ассемблере можно двумя способами, назовём их «статический» и «динамический». Реализовано это с помощью инструкций OFFSET и LEA.

При «статическом» способе указатель будет вычислен во время компиляции. Он не меняется и не вычисляется в процессе выполнения программы. В очень упрощённой форме: его значение равно смещению в байтах от начала программы, спроецированной в память, плюс адрес в оперативной памяти — «точка входа», адрес загрузки программы в память, который задаётся во время компиляции и записывается в заголовке (в начале) файла. Таким образом полученное значение фактически является константой.

OFFSET (Offset — смещение).

Получить «статическим способом» указатель в программировании на ассемблере можно с помощью оператора OFFSET (оператор — команда компилятору — подпрограмме, которая собирает исполняемый файл из исходного кода, написанного языком программирование). Offset возвращает значение метки в памяти. Меткой является любое именованное значение кода и данных. Например, имя переменной, константы или массива (именованное обозначение блока данных). Имя функции — фактически также является меткой (именованным обозначением блока кода).

LEA (Load Effective Address — загрузить эффективный адрес).

При «динамическом» способе адрес вычисляется в процессе исполнения программы. Для этого используется команда LEA (Load Effective Address).

lea операнд1, операнд2

Операнд 1 — это регистр-приёмник (ax, bx, dx и т.д.), куда будет перемещён эффективный адрес (указатель) ячейки памяти, в которой расположен операнд2.

Для начала, необходимо усвоить, что результат оба способа дают одинаковый, но иногда компилятор не имеет возможности определить указатель во время сборки программы. Например, при выделении динамической памяти (столкнёмся в 32 битном Windows программировании), не известно, по какому адресу она будет выделена. Если по этому адресу у нас будет находиться структура, то определить указатель на поля структуры возможно с использованием команды LEA.

Источник

Записки программиста

Шпаргалка по основным инструкциям ассемблера 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.

Из онлайн-справочников по ассемблерным инструкциям стоит обратить внимание на следующие:

А знаете ли вы ассемблер, и если да, то находите ли это знание полезным?

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *