Прыжки для 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, о которой я не знаю?


person AlexJ136    schedule 22.04.2015    source источник
comment
Хм, возможно, вы переоцениваете необходимость генерировать более 2 гигабайт кода. Преимущество джиттера в том, что вы всегда можете сказать, что вам нужно вернуться к непрямому переходу, например jmp rax.   -  person Hans Passant    schedule 22.04.2015
comment
@HansPassant Это хороший момент. На данный момент моя цель — просто реализовать простейшую работающую вещь, а о производительности беспокоиться потом.   -  person AlexJ136    schedule 22.04.2015
comment
Также связано: Обработка вызовов удаленных встроенных функций в JIT/ re: выделение блоков рядом друг с другом с помощью mmap с адресом подсказки, поэтому вы может использовать прямую кодировку call или jmp rel32.   -  person Peter Cordes    schedule 12.04.2019


Ответы (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.

Это то, чего вы пытаетесь достичь?

person Neitsa    schedule 22.04.2015
comment
Спасибо. выполнение 'mov rax, code_block', 'push rax', 'ret' дает эффект, который я ищу. В идеале я бы хотел что-то, что не должно касаться стека, но пока этого будет достаточно. - person AlexJ136; 24.04.2015
comment
Однако будьте осторожны: современные процессоры Intel отслеживают адреса возврата для прогнозирования целевых ветвей. Ручная отправка другого адреса, а затем использование ret для перехода к нему мешает предсказателю и, вероятно, приведет к снижению производительности. Дополнительную информацию см. в руководстве по оптимизации. - person Martin Törnwall; 26.04.2015
comment
@ AlexJ136: Это почти максимально неэффективно по сравнению с call rax или jmp rax. x86-64 имеет регистровый косвенный вызов и jmp, используйте их! push/ret намного хуже, потому что он разбивает стек предиктора адреса возврата, не сопоставляя call и ret. (call next — это особый случай, и он не используется в RAS на большинстве процессоров blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0, так что это оставляет его несбалансированным для будущих доходов, а также заставляет этот ret делать неверные прогнозы.) Использование call/ pop узнать свой адрес тоже безумие в x86-64; Вот почему у нас есть RIP-относительный LEA. - person Peter Cordes; 12.04.2019

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

Даже если вы mmap каждый блок разделяете, весьма вероятно, что вы не получите два соседних (в смысле потока управления) блока, расстояние между которыми превышает ±2 ГБ. При этом есть несколько веских причин не отображать каждый блок отдельно. Во-первых, минимальной единицей размещения 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 и использовать переход для регистрации.

person Martin Törnwall    schedule 23.04.2015
comment
Эффекты iTLB были бы более актуальной проблемой, если только вы не говорите только о крошечных базовых блоках. Локальность i-cache находится в 64-байтных чанках (или 128-байтных с учетом предварительной выборки смежных строк L2), а short-jmp — rel8 -128..+127 байтовых смещений. Но да, movabs/jmp *rax — очевидный выбор. См. также Обработка вызовов удаленных встроенных функций в JIT - person Peter Cordes; 12.04.2019