Skip to content

Commit 88d093e

Browse files
alexmalyshevmeta-codesync[bot]
authored andcommitted
Abstract DeoptPatcher into CodePatcher
Summary: Making a more general abstraction that can patch arbitrary sections of code. Or at least up to 7 bytes :). The CodePatcher stores a small buffer of data to patch in plus the destination address. When a patch is triggered, it will swap what it has in its buffer with what is at the destination address. This allows the CodePatcher to undo its patch in the future with an "unpatch". A "deopt patcher" is a specific case of this, and there's now a new subclass called JumpPatcher that covers this case, but it's still more general in that it's focused on patching in jumps rather than anything specific to deopts. The 7-byte limit here is to keep the CodePatcher to a small size, currently 24 bytes. If in the future we'll want to patch in 8-byte values we can replace its vtable with a smaller enum value and shrink it further. Reviewed By: jbower-fb Differential Revision: D83591551 fbshipit-source-id: 160023a047397fb9ef21452e6914bcc49a1ee95d
1 parent ccdbc19 commit 88d093e

14 files changed

Lines changed: 346 additions & 201 deletions

cinderx/Jit/code_patcher.cpp

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
#include "cinderx/Jit/code_patcher.h"
4+
5+
#include "cinderx/Common/log.h"
6+
#include "cinderx/Common/util.h"
7+
8+
#include <array>
9+
#include <cstring>
10+
11+
namespace jit {
12+
13+
namespace {
14+
15+
static_assert(
16+
sizeof(CodePatcher) == 24,
17+
"CodePatcher should be kept small as there could be many per function");
18+
19+
// 5-byte nop - https://www.felixcloutier.com/x86/nop
20+
//
21+
// Asmjit supports multi-byte nops but for whatever reason we can't get it to
22+
// emit the 5-byte version.
23+
constexpr auto kJmpNopBytes =
24+
std::to_array<uint8_t>({0x0f, 0x1f, 0x44, 0x00, 0x00});
25+
26+
// Compute an x86-64 jump displacement operand.
27+
uint32_t jumpDisplacement(uintptr_t from, uintptr_t to) {
28+
auto disp = to - (from + kJmpNopBytes.size());
29+
JIT_CHECK(
30+
fitsInt32(disp),
31+
"Can't encode jump from {:#x} to {:#x} as relative",
32+
from,
33+
to);
34+
return static_cast<uint32_t>(disp);
35+
}
36+
37+
// Given the starting address and displacement operand of a jump instruction,
38+
// resolve it to a target address.
39+
uintptr_t resolveDisplacement(uintptr_t from, uint32_t displacement) {
40+
return from + displacement + kJmpNopBytes.size();
41+
}
42+
43+
} // namespace
44+
45+
void CodePatcher::link(uintptr_t patchpoint, std::span<const uint8_t> data) {
46+
JIT_CHECK(!isLinked(), "Trying to re-link a patcher");
47+
48+
patchpoint_ = reinterpret_cast<uint8_t*>(patchpoint);
49+
50+
JIT_CHECK(
51+
data.size() <= data_.size(),
52+
"Trying to link a patch point with {} bytes of data but only {} are "
53+
"supported",
54+
data.size(),
55+
data_.size());
56+
57+
std::memcpy(data_.data(), data.data(), data.size());
58+
data_len_ = data.size();
59+
60+
onLink();
61+
}
62+
63+
void CodePatcher::patch() {
64+
JIT_CHECK(isLinked(), "Trying to patch a patcher that isn't linked");
65+
JIT_DLOG("Patching DeoptPatchPoint at {}", static_cast<void*>(patchpoint_));
66+
67+
swap();
68+
69+
is_patched_ = true;
70+
onPatch();
71+
}
72+
73+
void CodePatcher::unpatch() {
74+
JIT_CHECK(isLinked(), "Trying to unpatch a patcher that isn't linked");
75+
JIT_DLOG("Unpatching DeoptPatchPoint at {}", static_cast<void*>(patchpoint_));
76+
77+
swap();
78+
79+
is_patched_ = false;
80+
onUnpatch();
81+
}
82+
83+
bool CodePatcher::isLinked() const {
84+
return patchpoint_ != nullptr;
85+
}
86+
87+
bool CodePatcher::isPatched() const {
88+
return is_patched_;
89+
}
90+
91+
uint8_t* CodePatcher::patchpoint() const {
92+
return patchpoint_;
93+
}
94+
95+
std::span<const uint8_t> CodePatcher::storedBytes() const {
96+
return std::span{data_.data(), data_len_};
97+
}
98+
99+
void CodePatcher::swap() {
100+
decltype(data_) temp;
101+
std::memcpy(temp.data(), patchpoint_, data_len_);
102+
std::memcpy(patchpoint_, data_.data(), data_len_);
103+
std::memcpy(data_.data(), temp.data(), data_len_);
104+
}
105+
106+
JumpPatcher::JumpPatcher() {
107+
// Initializes to a nop.
108+
std::memcpy(data_.data(), kJmpNopBytes.data(), kJmpNopBytes.size());
109+
data_len_ = kJmpNopBytes.size();
110+
}
111+
112+
void JumpPatcher::linkJump(uintptr_t patchpoint, uintptr_t jump_target) {
113+
auto disp = jumpDisplacement(patchpoint, jump_target);
114+
115+
// 32 bit relative jump - https://www.felixcloutier.com/x86/jmp
116+
std::array<uint8_t, kJmpNopBytes.size()> buf{};
117+
buf[0] = 0xe9;
118+
std::memcpy(buf.data() + 1, &disp, sizeof(uint32_t));
119+
120+
link(patchpoint, buf);
121+
}
122+
123+
uint8_t* JumpPatcher::jumpTarget() const {
124+
JIT_CHECK(
125+
isLinked(), "Can't compute jump target before JumpPatcher is linked");
126+
127+
std::span<const uint8_t> bytes = storedBytes();
128+
JIT_CHECK(
129+
bytes.size() == 5,
130+
"Must have linked a 5-byte 'jmp $DISP' instruction into a JumpPatcher");
131+
132+
uint32_t disp = 0;
133+
std::memcpy(&disp, bytes.data() + 1, bytes.size() - 1);
134+
135+
return reinterpret_cast<uint8_t*>(
136+
resolveDisplacement(reinterpret_cast<uintptr_t>(patchpoint_), disp));
137+
}
138+
139+
} // namespace jit

cinderx/Jit/code_patcher.h

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
#pragma once
4+
5+
#include <cstdint>
6+
#include <span>
7+
8+
namespace jit {
9+
10+
// A CodePatcher is used by the runtime to overwrite parts of compiled code.
11+
// Often times this is used to patch in a jump to a deopt exit when an invariant
12+
// that the compiled code relies on is invalidated. It is intended to be used in
13+
// conjunction with the DeoptPatchpoint HIR instruction.
14+
//
15+
// Using a CodePatcher looks roughly like:
16+
// 1. Allocate a CodePatcher.
17+
//
18+
// 2. Allocate a DeoptPatchpoint HIR instruction linked to the CodePatcher
19+
// from (1) and insert it into the appropriate point in the HIR
20+
// instruction stream.
21+
//
22+
// 3. Link the CodePatcher from (1) to the appropriate address in the
23+
// generated code after code generation is complete.
24+
//
25+
// A CodePatcher is only valid for as long as the compiled code to which it is
26+
// linked is alive, so care must be taken not to call `patch()` after the code
27+
// has been destroyed.
28+
class CodePatcher {
29+
public:
30+
virtual ~CodePatcher() = default;
31+
32+
// Link the patcher to a specific location in generated code. This is
33+
// intended to be called by the JIT after code has been generated but before
34+
// it is active.
35+
//
36+
// `patchpoint` contains the address of the first byte of the patchpoint.
37+
// `data` contains the bytes that will be written on patching.
38+
void link(uintptr_t patchpoint, std::span<const uint8_t> data);
39+
40+
// Overwrite the patchpoint.
41+
//
42+
// The patcher must be linked before this can be called.
43+
void patch();
44+
45+
// Revert the patchpoint back to a nop.
46+
//
47+
// The patcher must be linked before this can be called.
48+
void unpatch();
49+
50+
// Check if the patcher has been linked.
51+
bool isLinked() const;
52+
53+
// Check if the patcher is currently patched.
54+
bool isPatched() const;
55+
56+
// Get where in the code to patch. Will be nullptr before the patcher is
57+
// linked.
58+
uint8_t* patchpoint() const;
59+
60+
// Get the bytes that are stored within the patcher right now.
61+
//
62+
// This either contains the bytes that will be patched in, or the bytes that
63+
// were there originally. The former is injected with patch(), the latter can
64+
// be put back in with unpatch().
65+
std::span<const uint8_t> storedBytes() const;
66+
67+
protected:
68+
// Callback to execute after linking (e.g. subscribing to changes).
69+
virtual void onLink() {}
70+
71+
// Callback to execute after patching (e.g. cleaning up the patcher).
72+
virtual void onPatch() {}
73+
74+
// Callback to execute after unpatching.
75+
virtual void onUnpatch() {}
76+
77+
// Swap data between this object and the actual patchpoint.
78+
void swap();
79+
80+
// Where in the code we should patch.
81+
uint8_t* patchpoint_{nullptr};
82+
83+
// Data that's written into the patch point. This is swapped with what's
84+
// already there, so that this can continuously patch and unpatch.
85+
//
86+
// The size of the array here is the total capacity, not necessarily all of it
87+
// will be patched.
88+
std::array<uint8_t, 7> data_{};
89+
90+
// Actual length of the data buffer above, from 0 to 7 bytes.
91+
uint8_t data_len_ : 7 {0};
92+
93+
// Whether patch() has been called and a corresponding unpatch() has not yet
94+
// been called..
95+
bool is_patched_ : 1 {false};
96+
};
97+
98+
// Subclass of a CodePatcher that is intended for patching in jumps.
99+
class JumpPatcher : public CodePatcher {
100+
public:
101+
JumpPatcher();
102+
~JumpPatcher() override = default;
103+
104+
// Specific form of link() for handling jumps.
105+
//
106+
// NB: The distance between the patchpoint and the jump target must fit into a
107+
// signed 32-bit int.
108+
void linkJump(uintptr_t patchpoint, uintptr_t jump_target);
109+
110+
// Get the jump target of this patcher.
111+
uint8_t* jumpTarget() const;
112+
};
113+
114+
} // namespace jit

cinderx/Jit/codegen/autogen.cpp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
#include "cinderx/Jit/codegen/autogen.h"
44

55
#include "cinderx/Common/util.h"
6+
#include "cinderx/Jit/code_patcher.h"
67
#include "cinderx/Jit/codegen/gen_asm_utils.h"
78
#include "cinderx/Jit/codegen/x86_64.h"
8-
#include "cinderx/Jit/deopt_patcher.h"
99
#include "cinderx/Jit/frame.h"
1010
#include "cinderx/Jit/generators_rt.h"
1111
#include "cinderx/Jit/jit_rt.h"
@@ -260,10 +260,15 @@ void TranslateGuard(Environ* env, const Instruction* instr) {
260260
void TranslateDeoptPatchpoint(Environ* env, const Instruction* instr) {
261261
auto as = env->as;
262262

263-
// Generate patchpoint
263+
auto patcher =
264+
reinterpret_cast<JumpPatcher*>(instr->getInput(0)->getMemoryAddress());
265+
266+
// Generate patchpoint by writing in an appropriately sized nop. As a future
267+
// optimization, we may be able to avoid reserving space for the patchpoint if
268+
// we can prove that the following bytes are not the target of a jump.
264269
auto patchpoint_label = as->newLabel();
265270
as->bind(patchpoint_label);
266-
for (uint8_t byte : kJmpNopBytes) {
271+
for (uint8_t byte : patcher->storedBytes()) {
267272
as->db(byte);
268273
}
269274

@@ -276,8 +281,6 @@ void TranslateDeoptPatchpoint(Environ* env, const Instruction* instr) {
276281

277282
// The runtime will link the patcher to the appropriate point in the code
278283
// once code generation has completed.
279-
auto patcher =
280-
reinterpret_cast<DeoptPatcher*>(instr->getInput(0)->getMemoryAddress());
281284
env->pending_deopt_patchers.emplace_back(
282285
patcher, patchpoint_label, deopt_label);
283286
}

cinderx/Jit/codegen/environ.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ struct Environ {
5353
std::vector<DeoptExit> deopt_exits;
5454

5555
struct PendingDeoptPatcher {
56-
PendingDeoptPatcher(DeoptPatcher* p, asmjit::Label pp, asmjit::Label de)
56+
PendingDeoptPatcher(JumpPatcher* p, asmjit::Label pp, asmjit::Label de)
5757
: patcher(p), patchpoint(pp), deopt_exit(de) {}
58-
DeoptPatcher* patcher;
58+
JumpPatcher* patcher;
5959

6060
// Location of the patchpoint
6161
asmjit::Label patchpoint;

cinderx/Jit/codegen/gen_asm.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,7 @@ void NativeGenerator::linkDeoptPatchers(const asmjit::CodeHolder& code) {
14221422
for (const auto& udp : env_.pending_deopt_patchers) {
14231423
uint64_t patchpoint = base + code.labelOffsetFromBase(udp.patchpoint);
14241424
uint64_t deopt_exit = base + code.labelOffsetFromBase(udp.deopt_exit);
1425-
udp.patcher->link(patchpoint, deopt_exit);
1425+
udp.patcher->linkJump(patchpoint, deopt_exit);
14261426

14271427
// Register patcher with the runtime if it is type-based.
14281428
if (auto typed_patcher = dynamic_cast<TypeDeoptPatcher*>(udp.patcher)) {
@@ -1433,8 +1433,8 @@ void NativeGenerator::linkDeoptPatchers(const asmjit::CodeHolder& code) {
14331433
// Any patchers that aren't linked at this point are pointing to patch points
14341434
// that were optimized out. It's safe to delete them.
14351435
std::erase_if(
1436-
const_cast<hir::Function*>(func_)->deopt_patchers,
1437-
[](std::unique_ptr<DeoptPatcher>& patcher) {
1436+
const_cast<hir::Function*>(func_)->code_patchers,
1437+
[](std::unique_ptr<CodePatcher>& patcher) {
14381438
return !patcher->isLinked();
14391439
});
14401440
}

cinderx/Jit/compiled_function.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ void CompiledFunction::setCompileTime(std::chrono::nanoseconds time) {
5858
compile_time_ = time;
5959
}
6060

61-
void CompiledFunction::setDeoptPatchers(
62-
std::vector<std::unique_ptr<DeoptPatcher>>&& deopt_patchers) {
63-
deopt_patchers_ = std::move(deopt_patchers);
61+
void CompiledFunction::setCodePatchers(
62+
std::vector<std::unique_ptr<CodePatcher>>&& code_patchers) {
63+
code_patchers_ = std::move(code_patchers);
6464
}
6565

6666
void CompiledFunction::setHirFunc(std::unique_ptr<hir::Function>&& irfunc) {

cinderx/Jit/compiled_function.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ bool isJitCompiled(const PyFunctionObject* func);
5454
#ifdef __cplusplus
5555

5656
#include "cinderx/Common/util.h"
57+
#include "cinderx/Jit/code_patcher.h"
5758
#include "cinderx/Jit/code_runtime.h"
58-
#include "cinderx/Jit/deopt_patcher.h"
5959
#include "cinderx/Jit/hir/hir.h"
6060

6161
#include <chrono>
@@ -130,8 +130,8 @@ class CompiledFunction {
130130
std::chrono::nanoseconds compileTime() const;
131131
void setCompileTime(std::chrono::nanoseconds time);
132132

133-
void setDeoptPatchers(
134-
std::vector<std::unique_ptr<DeoptPatcher>>&& deopt_patchers);
133+
void setCodePatchers(
134+
std::vector<std::unique_ptr<CodePatcher>>&& code_patchers);
135135

136136
void setHirFunc(std::unique_ptr<hir::Function>&& irfunc);
137137

@@ -154,8 +154,8 @@ class CompiledFunction {
154154
std::chrono::nanoseconds compile_time_;
155155
hir::Function::InlineFunctionStats inline_function_stats_;
156156
hir::OpcodeCounts hir_opcode_counts_;
157-
// All the deopt patchers pointing to patch points in this function.
158-
std::vector<std::unique_ptr<DeoptPatcher>> deopt_patchers_;
157+
// All the code patchers pointing to patch points in this function.
158+
std::vector<std::unique_ptr<CodePatcher>> code_patchers_;
159159
std::unique_ptr<hir::Function> irfunc_;
160160
CodeRuntime* runtime_;
161161
};

cinderx/Jit/compiler.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ std::unique_ptr<CompiledFunction> Compiler::Compile(
253253
hir_opcode_counts,
254254
code_runtime);
255255
compiled_func->setCompileTime(compile_time);
256-
compiled_func->setDeoptPatchers(std::move(irfunc->deopt_patchers));
256+
compiled_func->setCodePatchers(std::move(irfunc->code_patchers));
257257
if (getConfig().log.debug) {
258258
irfunc->setCompilationPhaseTimer(nullptr);
259259
compiled_func->setHirFunc(std::move(irfunc));

0 commit comments

Comments
 (0)