Pull to refresh

Comments 11

UFO just landed and posted this here
Я планировал эту заметку как самодостаточную и раскрывающую ответ ровно на тот вопрос, который часто задаётся. Однако, если есть какие-то смежные темы, оставшиеся непонятными, то я могу попробовать описать их или тут, в обсуждении, или же в более развёрнутом виде, если потребуется.
можете описать механизм rip-адресации в ia64?
Сразу уточняющий вопрос — под ia64 подразумевается Intel Itanium? Если да, то в нём формата инструкции для IP-relative адресации не предусмотрено. Для написания position independent code на этой архитектуре можно просто загрузить значение ip в любой регистр общего назначения, а затем использовать его с нужным смещением.

Или всё-таки имелась в виду архитектура Intel 64?
Тогда сперва, я думаю, стоит объяснить, зачем RIP-relative адресация вообще нужна. А нужна она для эффективной поддержки т.н. position-independent code (PIC), т.е. машинного кода, работоспособность которого не зависит от его положения в памяти.

Чаще всего необходимость в PIC возникает для разделяемых библиотек. При раздельной компиляции каждой библиотеки необходимо где-то разместить данные, с которыми она работает; использующий их код будет использовать константы-смещения для их адресации. При загрузке нескольких независимых библиотек может произойти конфликт: две или более библиотек будут хотеть разместить данные по одним и тем же адресам.

Для решения этой проблемы неободимо ввести уровень косвенности: «плавающую» адресацию данных. Можно при загрузке каждой библиотеки высчитывать адреса данных заново и переписывать их прямо в коде. Можно выбрать и использовать некоторый регистр, значение которого будет определять начало блока данных для конкретной библиотеки, и изменять только его значение при переходах между ними.

Наверное, самым изящным решением является выбор указателя текущей инструкции (RIP, EIP, IP, PC, IC…) в качестве такого регистра. На этапе компиляции известно «расстояние» между инструкцией, использующей некоторую переменную, и положением самой переменной в памяти. Самое важное — эта величина не меняется, где бы в памяти мы не разместили библиотеку в будущем. Поэтому смещение относительно счётчика инструкций можно как константу зашить прямо в машинный код инструкции. При условии, что кодировка набора команд это позволяет.

Изначально Intel IA-32 не имела такой тип адресации. Смещения можно было указывать только относительно регистров общего назначения. Кроме того, не было возможности загрузить значение EIP прямо в регистр — это можно было сделать только через операции над памятью, использующие стек, и вызов подпроцедуры, помещающей EIP на стек.

call read_eip // eip -> eax

<...>

read_eip: movl (%esp), %eax
ret

(Кстати, это тот самый случай, когда попытка inline-оптимизации рушит всё веселье!)

Конечно, это всё жутко неэффективно и коряво. Поэтому в компиляторах для 32-битного режима IA-32 по умолчанию режим PIC выключен.

С приходом Intel 64 в набор инструкций был добавлен новый способ адресации (вместо неиспользуемого в 64-битном режиме 32-битного смещения при использовании байта SIB). Для него идущая после байта ModRM 32-битная константа трактуется как смещение со знаком относительно RIP.

Возможно, следующий пример поможет проиллюстрировать сказанное выше. В нём я ассемблирую программу из единственной инструкции, использующей RIP-relative адресацию операнда в памяти, и вывожу результат дизассемблирования.

user@host:~$ cat 1.s 
movq %rax, 0xaabb(%rip)

user@host:~$ as 1.s 
user@host:~$ objdump -d a.out 

a.out:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
   0:   48 89 05 bb aa 00 00    mov    %rax,0xaabb(%rip)        # 0xaac2


Здесь в результирующем машинном коде 0x48 — это REX-префикс, 0x89-часть опкода, а 0x05 — байт ModRM, кодирующий, в том числе, тот факт, что используется RIP-relative адресация, и что последующие четыре байта определяют смещение (0x0000aabb) относительно адреса инструкции, следующей за текущей.

Отмечу следующие интересные моменты.

0. Практически все архитектуры поддерживают адресацию относительно текущей команды для инструкций переходов, т.е. jmp, branch, call. Не следует путать их с rip-relative адресацией — в них не происходит обращений к памяти.
1. Поддержка адресации относительно указателя текущей инструкции существует в других архитектурах, например, в ARM, VAX, 6809.
2. В английской Википедии есть исчерпывающая статья по типам адресации: en.wikipedia.org/wiki/Addressing_mode#PC-relative_2.

Гриша, про PIC (и ABI?) давай лучше отдельный пост напиши?
О, привет ☺
Если будут силы и время — попробую. Про ABI так можно целый цикл заметок сделать — очень уж интересный вопрос.
Спасибо) Даже больше, чем было нужно.
Добавлю как разработчик UEFI — аппаратной поддержки PIC для x86 очень не хватает. Для x64 такой код может быть сгенерирован компилятором, а для x86 его приходится либо писать на ассемблере, когда нельзя сделать его исполняемым на месте, либо вычислять реальный базовый адрес и патчить relocation table.
Тем не менее, ИМХО, опять же, в псевдографике выглядит понятнее =)
Sign up to leave a comment.