Вот код C:
int baz(int a, int b)
{
return a * 11;
}
Это скомпилировано в следующий набор инструкций по сборке (с флагом -O2):
baz(int, int):
lea eax, [rdi+rdi*4]
lea eax, [rdi+rax*2]
ret
Инструкция lea
вычисляет эффективный адрес второго операнда (исходного операнда) и сохраняет его в первом операнде. Мне кажется, что первая инструкция должна загрузить адрес в регистр EAX, но если это так, то умножение RAX на 2 не имеет смысла во второй lea
инструкции, поэтому я делаю вывод, что эти две lea
инструкции не делают совсем то же самое.
Мне было интересно, может ли кто-нибудь прояснить, что именно здесь происходит.
Аргумент функции для a
хранится в rdi
. Нет необходимости загружать что-либо из памяти.
lea eax, [rdi+rdi*4]
не вычисляет адрес какой-либо ячейки памяти для извлечения данных. Вместо этого компилятор просто переназначает инструкцию для выполнения умножения. Он хранит a + a*4
в eax
. Назовем это значение t
.
lea eax, [rdi+rax*2]
затем эффективно сохраняет a + t*2
в eax
.
rax
также является регистром, в котором возвращается возвращаемое значение функции.
Таким образом, возвращаемое значение будет a + t*2
, то есть a + (a + a*4)*2
, то есть a + a*5*2
, то есть a*11
.
Я так понимаю, эти две инструкции делают одно и то же: сохраняют значение, вычисленное в квадратных скобках, в регистре eax. Однако не должна ли инструкция lea загружать адрес в регистр eax по определению?
@KaKkoi Он вычисляет (загружает) эффективный адрес, указанный операндом адреса. Он ничего не загружает из вычисленного эффективного адреса, например. mov
с тем же адресным операндом.
Linux использует соглашение о вызовах System V AMD64 ABI , которое передает первый целочисленный параметр в регистре RDI
и возвращаемое значение в RAX
. Здесь EAX
достаточно, потому что он возвращает 32-битное значение. Второй параметр не используется.
LEA изначально предназначался для адресных вычислений на процессорах 8086, но также используется для целочисленной арифметики с постоянным множителем, как здесь. Постоянный коэффициент кодируется с использованием значения масштаба байта SIB в кодировке инструкции. Это может быть 1,2,4 или 8.
Итак, код можно объяснить
baz(RDI, RSI): ; a, b
lea eax, [rdi+rdi*4] ; RAX = 1*a + 4*a = 5*a
lea eax, [rdi+rax*2] ; RAX = 1*a + 2*RAX = 1*a + 2*(5*a)
ret ; return RAX/EAX = 11*a
Верхняя половина RAX (64-битное значение) автоматически очищается первым LEA
, см. этот ТАК вопрос.
Понятно, но могу я спросить, как именно ЦП решает, предназначена ли инструкция lea для вычисления адреса или целого числа с использованием целочисленной арифметики с постоянным коэффициентом?
Это не так. Оба являются просто целыми значениями. Первоначально коэффициент масштабирования, вероятно, предназначался для вычисления адресов для байтов (8, фактор 1), слов (16, фактор 2), двойных слов (32, фактор 4), счетверенных слов (64, фактор 8). Это удобно для вычисления адресов в массивах, т.е. lea eax,[base+5*4]
может вычислить адрес пятого (5) элемента двойного слова (4) массива с базовым адресом «база», все в одной инструкции.
Что бы вычислить адрес шестого. (+0*4 = первое, +1*4 = второе и т. д.)
@ikegami: Спасибо за исправление. Конечно, массивы на уровне памяти основаны на нуле.
@ zx485 lea eax,[base+5*4]
- плохой пример, потому что lea
потребовалось бы больше, если бы 5
был регистром, а не константой.
Если вы используете LEA для фактических адресов, вы должны использовать 64-битный размер операнда, например lea rax, [rel base+5*4]
для режима адресации относительно RIP. Если вы не используете относительную RIP-адресацию, то нет смысла использовать LEA, если регистры не задействованы, используйте mov eax, base+5*4
(например, в «маленькой» модели кода, такой как не-PIE в Linux, где статические адреса находятся в нижних 31 битов виртуального адресного пространства, что позволяет использовать 32-битные расширения с нулевым или знаковым расширением.)
По сути, забудьте об «адресах» и посмотрите, что на самом деле делает
lea
: простая арифметика, сдвиги и сложения. И это позволяет вам выполнять их в комбинациях, которые часто требуют меньшего количества инструкций, чем эквивалентная последовательность инструкцийshl/add
, поэтому, когда вам нужна такая комбинация, вы можете оптимизировать, как это сделал компилятор здесь. Никто не заставляет вас использовать результат в качестве адреса.