Прыжки для JIT (x86 64)


Я пишу JIT-компилятор на C для x86_64 linux.

В настоящее время идея заключается в том, чтобы сгенерировать некоторый байт-код в буфере исполняемой памяти (например, полученный с помощью вызова mmap) и перейти к нему с помощью указателя функции.

Я хотел бы иметь возможность связать несколько блоков исполняемой памяти вместе так, чтобы они могли прыгать между собой, используя только собственные инструкции.

В идеале указатель уровня C на исполняемый блок может быть записан в другой блок как абсолютный перейти адрес примерно так:

unsigned char *code_1 = { 0xAB, 0xCD, ... };
void *exec_block_1 = mmap(code1, ... );
write_bytecode(code_1, code_block_1);
...
unsigned char *code_2 = { 0xAB, 0xCD, ... , exec_block_1, ... };
void *exec_block_2 = mmap(code2, ... );
write_bytecode(code_2, exec_block_2); // bytecode contains code_block_1 as a jump
                                      // address so that the code in the second block
                                      // can jump to the code in the first block

Однако я нахожу ограничения x86_64 довольно серьезным препятствием здесь. В x86_64 невозможно перейти к абсолютному 64-разрядному адресу, так как все доступные 64-разрядные операции перехода относятся к указателю инструкции. Это означает, что я не могу использовать C-указатель в качестве цели перехода для сгенерированного кода.

Существует ли решение этой проблемы, которое позволит мне связать блоки вместе описанным выше способом? Возможно, инструкция x86_64 о чем я не знаю?

2 3

2 ответа:

Хм, Я не уверен, что ясно понял ваш вопрос, и если это правильный ответ. это довольно запутанный способ достичь этого:

    ;instr              ; opcodes [op size] (comment)
    call next           ; e8 00 00 00 00 [4] (call to get current location)
next:
    pop rax             ; 58 [1]  (next label address in rax)
    add rax, 12h        ; 48 83 c0 12 [4] (adjust rax to fall on landing label)
    push rax            ; 50 [1]  (push adjusted value)
    mov rax, code_block ; 48 b8 XX XX XX XX XX XX XX XX [10] (load target address)
    push rax            ; 50 [1] (push to ret to code_block)
    ret                 ; c3 [1] (go to code_block)
landing:    
    nop
    nop

e8 00 00 00 00 просто там, чтобы получить текущий указатель на вершине стека. Затем код настраивает rax, чтобы позже упасть на посадочную метку. Вам нужно будет заменить XXmov rax, code_block) виртуальным адресом code block. В качестве вызова используется инструкция ret. Когда вызывающий объект возвращается, код должен упасть на landing.

Это что-то вроде того, что ты делаешь? пытаешься чего-то добиться?

Если вы знаете адреса блоков в момент, когда вы посылаете команды перехода, вы можете просто проверить, соответствует ли расстояние в байтах от адреса команды перехода до адреса целевого блока 32-битному знаковому смещению семейства инструкций jXX.

Даже если вы mmap каждый блок отдельно, шансы довольно велики, что вы не получите два соседних (В смысле потока управления) блока, которые находятся более чем на ±2GiB друг от друга. То существо есть несколько веских причин не сопоставлять каждый блок отдельно таким образом. Во-первых, минимальная единица выделения mmap - это (почти по определению) страница, которая, вероятно, составляет не менее 4 КБ. Это означает, что неиспользуемое пространство после кода для каждого блока тратится впустую. Во-вторых, более плотная упаковка базовых блоков увеличивает использование кэша команд и вероятность того, что более короткое кодирование перехода будет допустимым.

Возможно, инструкция x86_64, которую я не в курсе?

Кстати, есть инструкция для загрузки 64-битного непосредственного в rax. Набор инструментов GNU ссылается на него как movabs:

0000000000000000 <.text>:
   0:   49 b8 ff ff ff ff ff    movabs rax,0x7fffffffffffffff
   7:   ff ff 7f
Поэтому, если вы действительно хотите, вы можете просто загрузить указатель в rax и использовать прыжок для регистрации.