Вызов абсолютного указателя в машинный код x86
Какой «правильный» способ call
абсолютный указатель на машинный код x86? Есть ли хороший способ сделать это в одной инструкции?
Что я хочу сделать:
Я пытаюсь создать своего рода упрощенную мини-JIT (по-прежнему) на основе «подпрограммы». Это в основном кратчайший шаг от интерпретатора байт-кода: каждый код операции реализован как отдельная функция, поэтому каждый базовый блок байт-кодов может быть «JITted» в новой процедуре, которая выглядит примерно так:
- 8086 на DOSBox: ошибка с инструкцией idiv?
- Как: pow (real, real) в x86
- Печать целого числа в виде строки с синтаксисом AT & T с системными вызовами Linux вместо printf
- x86 Расчет AX с учетом AH и AL?
- об ассемблере CF (Carry) и OF (Overflow)
{prologue} call {opcode procedure 1} call {opcode procedure 2} call {opcode procedure 3} ...etc {epilogue}
Таким образом, идея состоит в том, что фактический машинный код для каждого блока может быть просто вставлен из шаблона (при необходимости увеличивая среднюю часть), и единственный бит, который должен быть «динамически» обработан, – это копирование указателей функций для каждого кода операции в правильные места как часть каждой команды вызова.
Проблема, с которой я сталкиваюсь, – это понять, что использовать для call ...
часть шаблона. x86, похоже, не настроен с учетом этого вида использования и не поддерживает относительные и косвенные вызовы.
Похоже, что я могу использовать либо FF 15 EFBEADDE
либо 2E FF 15 EFBEADDE
чтобы гипотетически вызвать функцию DEADBEEF
(в основном они были обнаружены, помещая материал в ассемблер и дизассемблер и видя, что дает достоверные результаты, а не понимает, что они делают), но Я недостаточно разбираюсь в том, что касается сегментов, привилегий и связанной информации, чтобы увидеть разницу или как они будут отличаться от более часто встречающейся инструкции call
. Руководство по архитектуре Intel также предполагает, что они действительны только в 32-битном режиме и «недействительны» в 64-битном режиме.
Может ли кто-нибудь объяснить эти коды операций и как, или, если, я буду использовать их или другие для этой цели?
(Существует также очевидный ответ на использование косвенного вызова через регистр, но это похоже на «неправильный» подход – если на самом деле существует инструкция прямого вызова.)
- Можно ли сообщить предсказателю ветви, насколько вероятно, что он должен следовать за веткой?
- SIMD, подписанный с неподписанным умножением для 64-разрядных * 64-бит до 128 бит
- Загружает и сохраняет только инструкции, которые переупорядочиваются?
- получить длину строки в inline GNU Assembler
- Снижается ли производительность при выполнении циклов, чей счетчик uop не кратен ширине процессора?
- Эффективная функция сравнения целого числа
- x86-32 / x86-64 многоугольный fragment машинного кода, который обнаруживает 64-битный режим во время выполнения?
- Есть ли у блокировки xchg то же поведение, что и mfence?
Все здесь применимо к jmp
к абсолютным адресам, и синтаксис для задания цели одинаковый. Вопрос задает вопрос о JITing, но я также включил синтаксис NASM и AT & T для расширения сферы действия.
x86 не имеет кодировки для обычного (ближнего) call
или jmp
для абсолютного адреса, закодированного в инструкции (т. е. прямого вызова / jmp). См. Инструкцию Intel по установке INN для ввода call
. (См. Также вики-tags x86 для других ссылок на документы и руководства.) Большинство компьютерных архитектур используют относительные кодировки для обычных прыжков, таких как x86, BTW.
Лучшим вариантом (если вы можете сделать код, зависящий от позиции, который знает свой собственный адрес), является использование обычного call rel32
, прямого кодирования прямого вызова E8 rel32
, где поле rel32
является target - end_of_call_insn
(двоичное целое двоичного дополнения 2).
См. Как работает $ в NASM? на примере ручной кодировки команды call
; делая это, в то время как JITing должен быть таким же простым.
В синтаксисе AT & T: call 0x1234567
В синтаксисе NASM: call 0x1234567
Также работает с именованным символом с абсолютным адресом (например, созданный с помощью equ
или .set
)
Они собираются и соединяются очень хорошо в зависимости от положения кода (а не для общего lib или исполняемого файла PIE). Но не в x86-64 OS X, где текстовый раздел отображается выше 4GiB, поэтому он не может достичь низкого адреса с rel32
. Выберите свой абсолютный адрес в диапазоне от того, от кого вы его вызываете.
Но если вам нужно сделать независимый от позиции код , который не знает свой собственный абсолютный адрес, или если адрес, который вам нужно позвонить, больше, чем + -2GiB от вызывающего (возможно в 64-битном, но лучше разместить код достаточно близко), вы должны использовать регистр-косвенный call
; use any register you like as a scratch mov eax, 0xdeadbeef ; 5 byte mov r32, imm32 ; or mov rax, 0x7fffdeadbeef ; for addresses that don't fit in 32 bits call rax ; 2 byte FF D0
Или синтаксис AT & T
mov $0xdeadbeef, %eax # movabs $0x7fffdeadbeef, %rax # mov r64, imm64 call *%rax
Если вам действительно нужно избегать изменения каких-либо регистров, возможно, сохранить абсолютный адрес в качестве константы в памяти и использовать косвенный косвенный call
с режимом адресации, ориентированным на RIP, например
call [rel function_pointer]
NASM call [rel function_pointer]
; Если вы не можете
AT & T call *function_pointer(%rip)
Обратите внимание, что косвенные вызовы / переходы делают ваш код потенциально уязвимым для атак Spectre , особенно если вы JITing как часть изолированной программы для ненадежного кода в рамках одного и того же процесса. (В этом случае патчи ядра не будут защищать вас).
Косвенные прыжки также будут иметь несколько худшие штрафные санкции за неверный outlook, чем прямой ( call rel32
) . Назначение обычного прямого call
insn известно, как только оно декодируется, ранее в конвейере, как только обнаруживается, что там есть ветка вообще.
Непрямые ветви обычно хорошо предсказывают современные аппаратные средства x86 и обычно используются для вызовов динамических библиотек / библиотек DLL. Это не страшно, но call rel32
определенно лучше.
Однако даже прямой call
требует некоторого предсказания ветвления, чтобы избежать пузырьков трубопровода. (Перед декодированием требуется предсказание, например, учитывая, что мы просто извлекли этот блок, какой блок должен jmp next_instruction
следующий этап выборки. Последовательность jmp next_instruction
замедляется, когда у вас заканчиваются записи ветви-предсказателя ). mov
+ косвенный call reg
также хуже даже при идеальном предсказании ветвления, потому что это больший размер кода и больше uops, но это довольно минимальный эффект. Если дополнительный mov
является проблемой, вложение кода вместо его вызова является хорошей идеей, если это возможно.
call 0xdeadbeef
факт: call 0xdeadbeef
будет собираться, но не связываться с 64-битным статическим исполняемым файлом в Linux , если только вы не используете скрипт компоновщика, чтобы поместить сегмент .text
/ text ближе к этому адресу. Раздел .text
обычно начинается с 0x400080
в статическом исполняемом файле (или динамическом исполняемом файле , отличном от PIE ), то есть в низком виртуальном адресном пространстве 2GiB, где все статические коды / данные живут в модели кода по умолчанию. Но 0xdeadbeef
находится в верхней половине низких 32 бит (т. 0xdeadbeef
низком 4G, но не в низком 2G), поэтому он может быть представлен как 32-разрядное целое число с расширенным нулем, но не 32-битное расширенное расширение. И 0x00000000deadbeef - 0x0000000000400080
не помещается в 32-битное целое число, которое будет правильно распространено до 64 бит. (Часть адресного пространства, с которым вы можете связаться с отрицательным rel32
который обтекает с низкого адреса, является верхним 2GiB 64-разрядного адресного пространства, обычно верхняя половина адресного пространства зарезервирована для использования kernelм.)
Он собирает ok с yasm -felf64 -gdwarf2 foo.asm
, а objdump -drwC -Mintel
показывает:
foo.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <.text>: 0: e8 00 00 00 00 call 0x5 1: R_X86_64_PC32 *ABS*+0xdeadbeeb
Но когда ld
пытается связать его с статическим исполняемым файлом, где .text начинается с 0000000000400080
, ld -o foo foo.o
говорит foo.o:/tmp//foo.asm:1:(.text+0x1): relocation truncated to fit: R_X86_64_PC32 against '*ABS*'
.
В 32-битном кодовом call 0xdeadbeef
собирает и ссылки просто отлично, потому что rel32
может достигать где угодно из любого места. Относительное смещение не должно быть расширено до 64 бит, это просто 32-битное двоичное добавление, которое может обернуться вокруг или нет.
Прямые кодировки кодов (медленные, не используемые)
В записях руководства для call
и jmp
вы можете заметить, что существуют кодировки с абсолютными целевыми адресами, закодированными прямо в инструкции. Но они существуют только для «дальнего» call
/ jmp
которые также устанавливают CS
в новый селектор сегмента кода, который медленный (см. Руководства Agner Fog) .
CALL ptr16:32
(«Звонок далеко, абсолютный, адрес, заданный в операнде») имеет 6-байтовый сегмент: смещение, закодированное прямо в инструкции, вместо того, чтобы загружать его как данные из местоположения, заданного нормальным режимом адресации. Таким образом, это прямой вызов абсолютного адреса.
Дальний call
также подталкивает CS: EIP в качестве обратного адреса вместо EIP, поэтому он даже не совместим с обычным (ближним) call
который только подталкивает EIP. Это не проблема для jmp ptr16:32
, просто медлительность и выяснение того, что поставить для части сегмента.
Изменение CS обычно полезно только для перехода от 32 до 64-битного режима или наоборот. Обычно это будут только ядра, хотя вы можете сделать это в пользовательском пространстве в большинстве обычных ОС, которые сохраняют дескрипторы сегментов 32 и 64 бита в GDT. Тем не менее, это был бы глупый компьютерный трюк, чем что-то полезное. (64-разрядные ядра возвращаются в 32-разрядное пользовательское пространство с помощью iret
или, возможно, с sysexit
. Большинство ОС используют только один jmp один раз во время загрузки, чтобы переключиться на 64-битный сегмент кода в режиме ядра.)
Mainstream OSes использует модель с плоской памятью, где вам никогда не нужно менять cs
, и не стандартизировано, какое значение cs
будет использоваться для процессов пользовательского пространства. Даже если вы хотите использовать jmp
, вам нужно выяснить, какое значение следует добавить в селектор сегмента. (Легко, когда JITing: просто прочитайте текущие cs
с помощью mov eax, cs
. Но трудно быть переносимым для компиляции в режиме времени).
call ptr16:64
не существует, далекие прямые кодировки существуют только для 16 и 32-битного кода. В 64-битном режиме вы можете call
только с 10-байтовым m16:64
памяти m16:64
, например, call far [rdi]
. Или нажмите сегмент: смещение в стеке и используйте retf
.
Вы не можете сделать это только с одной инструкцией. Достойный способ сделать это с MOV + CALL:
0000000002347490: 48b83412000000000000 mov rax, 0x1234 000000000234749a: 48ffd0 call rax
Если адрес процедуры для вызова изменений, измените восемь байтов, начиная со смещения 2. Если адрес кода, вызывающего 0x1234, изменится, вам не нужно ничего делать, потому что адресация является абсолютной.