Косвенная адресация

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

Для обращения к данным по определенному адресу применяется косвенная адресация. Ассемблер NASM позволяет задавать адрес с помощью определенных вычислений. Общую форму адресации мы видели в прошлой статье. Так, переменная представляет адрес ее значения, а чтобы получить значение по этому адресу, надо поместить адрес в квадратные скобки:

[number] ; значение переменной number

Например, загрузим в регистр адрес переменной и получим значение по этому адресу на примере программы для Linux:

global _start

section .data
num dq 12 

section .text
_start:
    mov rax, [num]     ; загружаем данные из num в rax
    add rax, 3
    mov [num], rax      ; загружаем данные из rax в num
    mov rdi, [num]      ; rdi = 15
    mov rax, 60
    syscall 

Здесь в программе сначала загружаем в регистр RAX значение переменной num:

mov rax, [num]

Изменяем значение в RAX и сохраняем его обратно в переменную:

add rax, 3
mov [num], rax

В коде для Windows все будет аналогично, только перед именем переменной применяется оператор rel:

global _start

section .data
num dq 12 

section .text
_start:
    mov rdi, [rel num]     ; загружаем данные из num в rdi
    add rdi, 3
    mov [rel num], rdi      ; загружаем данные из rdi в num
    mov rax, [rel num]      ; rax = 15
    ret    

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

Адресация в NASM

Обращение по некоторому адресу в NASM задается следующей формулой:

[base + (index * scale) + offset]

Основные компоненты этой формулы:

  • base: базовый регистр, который содержит некоторый адрес. Это может быть 64-разрядный или 32-разрядный регистр общего назначения или регистр RSP

  • index: индексный регистр, который содержит некоторый индекс относительно адреса в базовом регистре. В качестве индексного регистра также могут выступать 64-разрядный или 32-разрядный регистр общего назначения или регистр RSP

  • scale: множитель, на который умножается значение индексного регистра. Может принимать значения 1, 2, 4 или 8

  • offset: может представлять 32-разрядное значение в виде числа или имени переменной. Это может быть 64-разрядный регистр общего назначения или регистр RSP

Необязательно использовать сразу все компоненты для определения адреса. Примеры использования:

Схема

Пример

Описание

[base]

[rdx]

Только базовый регистр

[offset]

[0F3h] или [имя_переменной]

Числовое смещение (абсолютный адрес) или переменная (адрес значения переменной)

[base + offset]

[rcx + 033h]

Базовый регистр + смещение

[base + index]

[rax + ecx]

Базовый регистр + индексный регистр

[index * scale]

[rbx * 4]

Индексный регистр, умноженный на мультиприкатор

[index * scale + offset]

[rbx * 4 + 65]

Индексный регистр, умноженный на мультиприкатор, + смещение

[base + index * scale]

[rsp + rbx * 4]

Базовый регистр + индексный регистр, умноженный на мультиприкатор

[base + index * scale + offset]

[rsp + rbx * 4 + 65]

Базовый регистр + индексный регистр, умноженный на мультиприкатор, + смещение

Рассмотрим некоторые варианты.

Смещения

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

mov rax, [num]     ; загружаем в rax значение из переменной num 

К смещению мы можем прибавить числовую константу:

global _start

section .data
nums dq 12, 13, 14, 15, 16

section .text
_start:
    mov rdi, [nums + 16]      ; rdi = 14
    mov rax, 60
    syscall 

Здесь к адресу переменной nums прибавляется 16 байт: [nums + 16]. 16 байт - это 2 значения qword. А массив nums - как раз набор чисел qword. Таким образом, если просто [nums] - это адрес первого элемента в nums, то nums + 16 - это адрес 3-го элемента, то есть числа 14.

Смещение может быть отрицательным, в этом случае идет переход назад:

global _start

section .data
number dq 33
nums dq 12, 13, 14, 15, 16

section .text
_start:
    mov rdi, [nums - 8]      ; rdi = 33
    mov rax, 60
    syscall 

Здесь опять же первое смещение представляет адрес массива nums. Однако от него вычитается число 8, то есть мы как бы идет на 8 байт назад, где определена переменная number. Соответственно выражение [nums - 8] обратится к переменной number.

Но при использовании смещений стоит учитывать ограничения - значение смещение должно занимать не больше 32 бит.

Базовый регистр

Базовый регистр - это регистр, который хранит некоторый адрес. Например, мы можем загрузить в регистр адрес переменной и через базовый регистр обращаться к этой переменной. Так, возьмем следующую программу для Linux:

global _start

section .data
nums dq 12, 13, 14, 15, 16

section .text
_start:
    mov rbx, nums   ; в rbx - адрес переменной nums
    mov rdi, [rbx]  ; rdi = 12
    mov rax, 60
    syscall 

Здесь помещаем адрес переменной nums в регистр RBX. То есть регистр RBX будет базовым регистром. Затем обращаемся по адресу в RBX и хранящееся там значение помещаем в регистр RDI:

mov rdi, [rbx]

Это была программа для Linux. На Windows при получении данных добавляется оператор преобразования:

global _start

section .data
nums dq 12, 13, 14, 15, 16

section .text
_start:
    mov rbx, nums   ; в rbx - адрес переменной nums
    mov rax, qword [rbx]    ; преобразуем в qword
    ret    

Базовый регистр плюс смещение

К значению в базовом регистре можно добавлять смещение:

global _start

section .data
nums dq 12, 13, 14, 15, 16

section .text
_start:
    mov rbx, nums   ; в rbx - адрес переменной nums
    mov rdi, [rbx + 8]    ; к адресу в rbx прибавляем 8 байт - rdi = 13
    mov rax, 60
    syscall 

Здесь применяется смещение в 8 байт, которое прибавляется к адресу в rbx. То есть мы обращаемся к адресу второго числа в массиве nums - числа 13.

Причем мы можем в качестве смещения использовать и имя переменной:

global _start

section .data
nums dq 12, 13, 14, 15, 16

section .text
_start:
    mov rbx, 16   ; в rbx - число 16
    mov rdi, [nums + rbx]    ; к адресу в rbx прибавляем адрес переменной nums - rdi = 14
    mov rax, 60
    syscall 

Здесь базовый регистр фактически не содержит никакого адреса и равен 16. Выражение [nums + rbx] прибавляет к адресу переменной nums это число 16. Таким образом, мы получим адрес третьего числа в массиве nums. Аналогично можно добавить еще числовое смещение:

mov rdi, [nums + rbx + 8]    ; rdi = 15

В Windows при использовании имени переменной в качестве смещения все несколько сложнее (с числовыми смещениями никаких проблем нет). Так, при обращении по адресу переменной компоновщики требуют использовать адресацию относительно регистра RIP с помощью оператора rel:

mov rdi, [rel nums] 

Однако NASM не допускает сочетание адресации относительно RIP и использование базового/индексного регистров:

mov rax, qword [rel nums + rbx]  ; НЕ работает

В этом случае нам придется несколько изменить логику программы, чтобы имя переменной и базовый регистр не сочетались в одном выражении (например, загружать тот же адрес переменной в базовый регистр и далее с ним производить манипуляции. Либо можно поиграться с флагами компоновщика. Например, для компоновщика ld из GCC добавить флаг --default-image-base-low:

ld hello.o -o hello.exe --default-image-base-low

А при использовании компоновщика link от Microsoft добавить флаг /LARGEADDRESSAWARE:NO:

link hello.o /entry:_start /subsystem:console /out:hello.exe  /LARGEADDRESSAWARE:NO

Другие компоновщики могут потребовать других настроек. Однако использованные настройки могут влиять на другие аспекты выполнения программы.

Индексный регистр

Индексный регистр позволяет задать индекс элемента внутри некоторой структуры данных. Пример программы на Linux:

global _start

section .data
nums db 12, 13, 14, 15, 16

section .text
_start:
    mov rbx, nums   ; rbx - базовый регистр, хранит адрес переменной nums
    mov rsi, 2      ; rsi - индексный регистр, хранит индекс элемента массива nums
    movzx rdi, byte [rbx + rsi]    ; к адресу в rbx прибавляем 2 байтв- rdi = 14
    mov rax, 60
    syscall 

Здесь регистр rbx является базовым и хранит адрес набора nums - массива байт. Регистр rsi является индексным и хранит индекс элемента в этом наборе, который мы хотим получить. Выражение [rbx + rsi] фактически прибавит к значению в rbx значение из rsi. Так, как rsi в примере выше хранит 2, то таким образом мы обратимся к числу 14.

Аналогичный пример на Windows:

global _start

section .data
nums db 12, 13, 14, 15, 16  ; массив байт

section .text
_start:
    mov rbx, nums   ; rbx - базовый регистр, хранит адрес переменной nums
    mov rsi, 2      ; rsi - индексный регистр, хранит индекс элемента массива nums
    movzx rax, byte [rbx + rsi]    ; к адресу в rbx прибавляем 2 байт - rax = 14
    ret    

В примере выше было все относительно просто: у нас массив байт, если надо обратиться к последующему элементу, прибавляем 1. Если надо обратиться к элементу, который находится через две позиции, прибавляем два. Так как размер каждого элемента - 1 байт. Однако при работе с данными большей разрядности такой подход естественно не сработает. И в этом случае мы можем умножать индексный регистр на мультипликатор - размер элемента. В качестве мультипликатора можно применять числа 1, 2, 4 и 8 - размеры типов данных. Например, используем мультипликатор для обращения к элементами массива четверных слов:

global _start

section .data
nums dq 112, 113, 114, 115, 116  ; массив чисел qword

section .text
_start:
    mov rbx, nums   ; rbx - базовый регистр, хранит адрес переменной nums
    mov rsi, 2      ; rsi - индексный регистр, хранит индекс элемента массива nums
    mov rdi, qword [rbx + rsi * 8]    ; к адресу в rbx прибавляем (2 * 8) байт - rdi = 114
    mov rax, 60
    syscall 

При необходимости также можно прибавить смещение:

global _start

section .data
nums dq 112, 113, 114, 115, 116  ; массив чисел qword

section .text
_start:
    mov rbx, nums   ; rbx - базовый регистр, хранит адрес переменной nums
    mov rsi, 2      ; rsi - индексный регистр, хранит индекс элемента массива nums
    mov rdi, qword [rbx + rsi * 8 + 16]    ; к адресу в rbx прибавляем (2 * 8 + 16) байт - rdi = 116
    mov rax, 60
    syscall 

Индексный регистр можно комбинировать со смещением в виде имени переменной. Пример программы на Linux:

global _start

section .data
nums dq 112, 113, 114, 115, 116  ; массив чисел qword

section .text
_start:
    mov rsi, 1     ; rsi - индексный регистр, хранит индекс элемента массива nums
    mov rdi, qword [nums + rsi * 8]    ; к адресу nums прибавляем (1 * 8) байт - rdi = 113
    mov rax, 60
    syscall 

Выражение [nums + rsi * 8] получает адрес, который находится от начала nums на rsi * 8 байт, что эквивалентно адресу числа 113.

Инструкция lea

В качестве альтернативы для загрузки адреса переменной можно использовать инструкцию lea. Этой инструкции в качестве первого операнда передается регистр, в который надо загрузить адрес. Второй операнд представляет адрес, который, как выше было рассмотрено, опять же можем складываться на основе формулы [base + index * scale + offset]. Пример на Linux:

global _start

section .data
nums dq 112, 113, 114, 115, 116  ; массив чисел qword

section .text
_start:
    lea rbx, [nums]   ; в rbx - адрес переменной nums
    mov rdi, qword [rbx]  ; rdi = 112
    mov rax, 60
    syscall 

Так, в примере выше загружаем адрес переменной nums.

Аналогичный пример на Windows:

global _start

section .data
nums dq 112, 113, 114, 115, 116  ; массив чисел qword

section .text
_start:
    lea rbx, [rel nums]   ; в rbx - адрес переменной nums
    mov rax, qword [rbx]  ; rax = 112
    ret    

Причем инструкция lea позволяет выполнять динамические вычисления даже вне зависимости от адресов:

global _start

section .text
_start:
    mov rdi, 8
    mov rcx, 5
    
    lea rdi, [rdi + rcx * 2]   ; в rdi = 8 + 5*2 = 18
    mov rax, 60
    syscall 
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850