Режимы адресации памяти x86-64 обеспечивают гибкий доступ к памяти, позволяя легко обращаться к переменным, массивам, записям, указателям и другим сложным типам данных. Архитектура x86-64 предоставляет несколько режимов адресации:
Адресация регистров
Адресация памяти относительно счетчика команд (PC-relative memory addressing)
Косвенная адресация регистров ([reg64]
)
Косвенная адресация регистров со смещением ([reg64 + offset]
)
Косвенная адресация регистров со смещением и масштабированием ([reg64 + offset + reg64 * scale]
)
Режим адресации регистров предоставляет доступ к регистрам общего назначения. Это стандартное обращение к регистрам процессора:
mov rax, rdx
Регистры представляют самый быстрый тип памяти компьютера. Инструкции, которые используют регистры, короче и быстрее, чем те, которые обращаются к памяти. Поскольку для большинства вычислений требуется по крайней мере один регистр, режим адресации регистров популярен в ассемблере x86-64. Единственное ограничение - оба операнда должны иметь одну и ту же разрядность.
Данный режим заключается в использовании имени переменной или константы, которая определена в одном из разделов - .data, .data?, .const, .code
и т. д.
.data i32 dword 33 .code main proc mov eax, i32 ret main endp end
Для вычисления адреса переменных/констант применяется регистр RIP (Instruction Pointer), он же счетчик команд (Program Counter), отсюда и соответствующее название.
Косвенная адресация регистров (register-indirect addressing mode) означает, что регистр содержит адрес некоторого объекта. Используя данный адрес, извлекается данный объект. Чтобы указать, что мы хотим взять не значение из регистра, а значения из адреса, который хранится в регистре, название регистра помещается в квадратные скобки:
[reg64]
Где reg64 - это один из 64-разрядных регистров общего назначения: RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8, R9, R10, R11, R12, R13, R14 и R15.
Для загрузки адреса некоторого объекта может применяться инструкция lea (load effective address). Данная инструкция имеет следующий синтаксис:
lea reg64, variable
Первый операнд инструкции представляет 64-разрядный регистр общего назначения, куда загружается адрес переменной variable - второго операнда. Стоит отметить, что переменная variable может представлять любой размер, поскольку в регистр загружается именно ее адрес, а адрес в 64-разрядной архитектуре имеет длину 64 бита.
Например, определим в секции данных переменную text и загрузим ее адрес в регистр rcx:
.data text byte "hello", 0 .code main proc lea rcx, text ; загрузка в rcx адреса переменной text ret main endp end
Стоит отметить, что в C/C++ это было бы аналогично следующему коду:
char text[] = "hello"; char *RCX = &text[0];
Загруженный адрес затем можно использовать для получения данных при косвенной адресации:
.data i32 dword 39 .code main proc lea rcx, i32 ; загружаем в RCX адрес переменной i32 mov eax, [rcx] ; загружаем в EAX значение по адресу из RCX ret main endp end
Здесь в регистр RCX загружается адрес переменной i32. Затем в регистр EAX помещается значение, которое хранится по адресу из регистра RCX - то есть значение переменной i32.
Также можно наоборот - сохранять значение по определенному адресу. Например:
.data i32 dword ? .code main proc lea rcx, i32 ; загружаем в регистр RCX адрес переменной i32 mov rdx, 12 ; в регистр RDX помещаем число 12 mov [rcx], rdx ; в память по адресу [RCX] помещаем значение из RDX mov eax, i32 ; в регистр EAX помещаем значение переменной i32 - 12 ret main endp end
Здесь в секции данных определена неинициализированная переменная i32. Сохраним в эту переменную число 12. Для этого загружаем адрес этой переменной в регистр RCX
lea rcx, i32
Сохраняемое число помещаем вначале в регистр RDX
mov rdx, 12
Далее помещаем данные из регистра RDX в участок памяти, адрес которой хранится в регистре RCX, то есть по сути адрес переменной i32:
mov [rcx], rdx
Стоит отметить, что ассемблер не позволяет напрямую сохранить по адресу числовую константу, поэтому надо сначала числовую константу поместить в регистр, и затем сохранить значение этого регистра по адресу переменной.
Дополнительно для адреса можно установить смещение в байтах. В качестве смещения выступает 32-разрядное число со знаком
[reg64 ±смещение]
Например:
.data n1 dword 1 n2 dword 3 .code main proc lea rbx, n1 ; загружаем в регистр RBX адрес переменной n1 mov eax, [rbx + 4] ; в регистр EAX загружаем данные с адреса [rbx + 4], то есть переменной n2 ret main endp end
Здесь в регистр RBX помещается адрес переменной n1. Эта переменная занимает 4 байта (тип dword), соответственно адрес следующей переменной - n2 равен [RBX + 4]. И если мы возьмем данные по этому адресу, то фактически мы получим значение переменной n2.
Аналогично можно сохранять данные по адресу, используя смещение
.data n1 dword 1 n2 dword 3 .code main proc lea rbx, n1 ; загружаем в регистр RBX адрес переменной n1 mov rdx, 15 ; в регистр RDX помещаем число 15 mov [rbx + 4], rdx ; в память по адресу [RBX] помещаем значение из RDX mov eax, n2 ; в регистр EAX помещаем значение переменной n2 - 15 ret main endp end
При этом смещение также может быть отрицательным. В этом случае адрес сдвигается на указанное число байтов назад.
Использование смещений очень удобно, когда речь идет об обращении к частям каких-то сложных данных, например, массивов.
Адресация с масштабированием позволяет комбинировать два регистра плюс смещение и умножать значение индексного регистра на коэффициент масштабирования 1, 2, 4 или 8, чтобы вычислить адрес данных:
[base_reg64 + index_reg64*scale] [base_reg64 + index_reg64*scale + offset] [base_reg64 + index_reg64*scale - offset]
base_reg64
представляет любой 64-битный регистр общего назначения, index_reg64
представляет любой 64-битный регистр общего назначения,
кроме RSP, а scale
(коэффициент масштабирования) должен быть одной из констант 1, 2, 4 или 8. Дополнительно можно прибавлять или отнимать смещение.
Например, у нас есть набор некоторых данных, и нам надо в этом наборе получить какой-то определенный элемент:
.data numbers dword 11, 12, 13, 14, 15, 16, 17, 18 .code main proc lea rbx, numbers ; загружаем в регистр RBX адрес переменной numbers mov rsi, 5 ; индекс получаемого двойного слова - получаем 6 элемент mov eax, [rbx + rsi*4] ; в регистр EAX загружаем данные с адреса [rbx + rsi*4], то есть число 16 ret main endp end
Здесь данные представлены набором двойных слов numbers. Каждое число в этом наборе занимает 4 байта. Допустим, мы хотим получить 6-е число. Для этого в регистр RSI помещаем индекс нужного нам элемента - число 5. То есть, чтобы получить 6-е число, нам надо пройти 5 предыдущих чисел.
mov rsi, 5
Затем загружаем в регистр EAX значение 6-го числа из numbers, то есть число 16.
mov eax, [rbx + rsi*4]
Адрес складывается из следующих компонентов. Во-первых, в регистре RBX адрес начала набора numbers, то есть адрес первого элемента из этого набора. Чтобы дойти к адресу 6-го
элемента от адреса 1-го элемента, нам надо пройти 5 элементов, поэтому в регистре RSI число 5. Каждый элемент занимает 4 байта, соответственно, чтобы пройти от
1-го элемента до 6-го, нам надо пройти rsi*4 = 5 *4 = 20
байтов.
Как и в общем случае при косвенной адресации можно выполнять преобразования данных. Например, возьмем следующую программу:
.data n1 byte 11 n2 byte 12 n3 byte 13 n4 byte 14 .code main proc lea rbx, n1 ; загружаем в регистр RBX адрес переменной n1 mov eax, [rbx] ; в регистр EAX загружаем данные с адреса [rbx], то есть переменной n1 ret main endp end
В регистр RBX помещается адрес переменной n1, затем данные из памяти по адресу из RBX помещаются в регистр EAX. То есть мы ожидаем, что в регистре EAX в итоге будет число 11 - значение переменной n1. Однако если мы скомпилируем программу с помощью MASM, запустим и проверим содержимое регистра EAX, то мы увидим, что там не число 11:
c:\asm>ml64 hello.asm /link /entry:main Microsoft (R) Macro Assembler (x64) Version 14.36.32532.0 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: hello.asm Microsoft (R) Incremental Linker Version 14.36.32532.0 Copyright (C) Microsoft Corporation. All rights reserved. /OUT:hello.exe hello.obj /entry:main c:\asm>hello c:\asm>echo %ERRORLEVEL% 235736075 c:\asm>
Число 235736075 вряд ли равно значению переменной n1. Поскольку значение помещается в 32-разрядный регистр EAX, то по адресу из RBX выбирается 4 байта, то есть все последующие переменные - n2, n3 и n4. Но нам надо получить только значение переменной n1. Для этого мы можем получить значение в 8-разрядный регистр:
.data n1 byte 11 n2 byte 12 n3 byte 13 n4 byte 14 .code main proc lea rbx, n1 ; загружаем в регистр RBX адрес переменной n1 mov rax, 0 ; обнуляем регистр rax mov al, [rbx] ; AL = 11 (и EAX = 11) ret main endp end
Либо мы можем применить преобразование до байта:
.data n1 byte 11 n2 byte 12 n3 byte 13 n4 byte 14 .code main proc lea rbx, n1 movzx eax, byte ptr[rbx] ret main endp end
Таким образом, данные по адресу из RBX преобразуются до 1 байта, а инструкция movzx заполняется старшие разряды нулями, благодаря чему нет несоответствия по размеру между операндами.