Режимы адресации. Косвенная адресация

Последнее обновление: 02.07.2023

Режимы адресации памяти 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

Для загрузки адреса некоторого объекта может применяться инструкция 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 заполняется старшие разряды нулями, благодаря чему нет несоответствия по размеру между операндами.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850