Для обращения к данным по определенному адресу применяется косвенная адресация. Ассемблер 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 задается следующей формулой:
[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. Этой инструкции в качестве первого операнда передается регистр,
в который надо загрузить адрес. Второй операнд представляет адрес, который, как выше было рассмотрено, опять же можем складываться на основе формулы [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