Вызов абсолютного указателя в машинном коде x86

Каков "правильный" способ call абсолютный указатель в машинном коде x86? Есть ли хороший способ сделать это в одной инструкции?

Что я хочу сделать:

Я пытаюсь создать своего рода упрощенный мини-JIT (до сих пор) на основе "подпрограммы потоков". По сути, это самый короткий возможный шаг по сравнению с интерпретатором байт-кода: каждый код операции реализован как отдельная функция, поэтому каждый базовый блок байт-кодов может быть "JITted" в свою собственную свежую процедуру, которая выглядит примерно так:

{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-разрядном режиме.

Может кто-нибудь объяснить эти коды операций и как, или если я буду использовать их или других для этой цели?

(Существует также очевидный ответ использования косвенного вызова через регистр, но это похоже на "неправильный" подход - предполагая, что инструкция прямого вызова действительно существует.)

2 ответа

Все здесь относится к jmp к абсолютным адресам, и синтаксис для указания цели такой же. Вопрос касается JITing, но я также включил синтаксис NASM и AT&T, чтобы расширить сферу применения.


x86 не имеет кодировки для нормального (рядом) call или же jmp на абсолютный адрес, закодированный в инструкции (то есть прямой вызов /jmp). См. Руководство Intel по установке insn set ref для call, (См. Также вики-теги 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)

Они прекрасно собираются и связываются в позиционно-зависимом коде (не в разделяемой библиотеке или исполняемом файле PIE). Но не в x86-64 OS X, где текстовый раздел отображается выше 4 ГБ, поэтому он не может достичь низкого адреса с rel32, Укажите свой абсолютный адрес, чтобы быть в диапазоне от того, откуда вы звоните.


Но если вам нужно создать независимый от позиции код, который не знает своего собственного абсолютного адреса, или если адрес, который вам нужно вызвать, находится на расстоянии более +-2 ГБ от вызывающего абонента (возможно в 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-относительной адресации, например

NASM call [rel function_pointer]; Если вы не можете забить любой рег
AT&T call *function_pointer(%rip)


Обратите внимание, что косвенные вызовы / переходы делают ваш код потенциально уязвимым для атак Spectre, особенно если вы выполняете JIT как часть изолированной программной среды для ненадежного кода в рамках одного и того же процесса. (В этом случае только исправления ядра не защитят вас).

Косвенные скачки также будут иметь несколько худшие штрафы за неверное предсказание ветвления, чем прямые ( call rel32 ) Назначение нормального прямого call insn известен, как только он декодируется, ранее в конвейере, как только он обнаружил, что там вообще есть ветвь.

Косвенные ветви обычно хорошо предсказывают современное оборудование x86 и обычно используются для вызовов динамических библиотек / DLL. Это не страшно, но call rel32 определенно лучше

Даже прямой call все же требуется некоторое предсказание ветвлений, чтобы полностью избежать пузырей конвейера. (Предсказание необходимо перед декодированием, например, учитывая, что мы только что извлекли этот блок, какой блок должен быть извлечен на следующей стадии. Последовательность jmp next_instruction замедляется, когда у вас заканчиваются записи-предсказатели ветвлений). mov + косвенный call reg также хуже даже при совершенном предсказании ветвлений, потому что он имеет больший размер кода и больше мопов, но это довольно минимальный эффект. Если дополнительный mov Это проблема, заключающаяся в том, что вставлять код вместо его вызова - хорошая идея, если это возможно.


Интересный факт: call 0xdeadbeef будет собирать, но не ссылаться на 64-битный статический исполняемый файл в Linux, если вы не используете скрипт компоновщика для .text раздел / текстовый сегмент ближе к этому адресу. .text раздел обычно начинается в 0x400080 в статическом исполняемом файле (или динамическом исполняемом файле без PIE), то есть в низком 2 ГБ виртуального адресного пространства, где весь статический код / ​​данные живет в модели кода по умолчанию. Но 0xdeadbeef находится в старшей половине младших 32 битов (то есть в младшей 4G, но не младшей 2G), поэтому его можно представить как 32-разрядное целое число с нулевым расширением, но не 32-разрядное с расширенным знаком. А также 0x00000000deadbeef - 0x0000000000400080 не помещается в 32-разрядное целое число со знаком, которое будет правильно расширяться до 64 бит. (Часть адресного пространства вы можете достичь с отрицательным rel32 то, что оборачивается от низкого адреса, - верхние 2 ГБ 64-разрядного адресного пространства; обычно верхняя половина адресного пространства зарезервирована для использования ядром.)

Собирается нормально с 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 кодировки (медленно, не используйте)

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

В основных операционных системах используется модель плоской памяти, в которой вам никогда не нужно менять cs и это не стандартизировано, что cs значение будет использоваться для процессов пользовательского пространства. Даже если вы хотите использовать далеко jmp Вы должны выяснить, какое значение поместить в часть селектора сегмента. (Легко во время JITing: просто прочитайте текущий cs с mov eax, cs, Но трудно быть переносимым для преждевременной компиляции.)


call ptr16:64 не существует, дальние прямые кодировки существуют только для 16- и 32-битного кода. В 64-битном режиме вы можете только call с 10-байтовым m16:64 операнд памяти, как call far [rdi], Или нажмите сегмент: смещение в стеке и использовать retf,

Вы не можете сделать это только с одной инструкцией. Достойный способ сделать это с MOV + CALL:

0000000002347490: 48b83412000000000000  mov rax, 0x1234
000000000234749a: 48ffd0                call rax

Если адрес вызываемой процедуры изменяется, измените восемь байтов, начиная со смещения 2. Если адрес кода, вызывающего 0x1234, изменится, вам не нужно ничего делать, потому что адресация является абсолютной.

Другие вопросы по тегам