1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
|
require 'ruby_vm/mjit/assembler'
require 'ruby_vm/mjit/block'
require 'ruby_vm/mjit/block_stub'
require 'ruby_vm/mjit/branch_stub'
require 'ruby_vm/mjit/code_block'
require 'ruby_vm/mjit/context'
require 'ruby_vm/mjit/exit_compiler'
require 'ruby_vm/mjit/insn_compiler'
require 'ruby_vm/mjit/instruction'
require 'ruby_vm/mjit/invariants'
require 'ruby_vm/mjit/jit_state'
module RubyVM::MJIT
# Compilation status
KeepCompiling = :KeepCompiling
CantCompile = :CantCompile
EndBlock = :EndBlock
# Ruby constants
Qtrue = Fiddle::Qtrue
Qfalse = Fiddle::Qfalse
Qnil = Fiddle::Qnil
Qundef = Fiddle::Qundef
# Callee-saved registers
# TODO: support using r12/r13 here
EC = :r14
CFP = :r15
SP = :rbx
# Scratch registers: rax, rcx
class Compiler
attr_accessor :write_pos
def self.decode_insn(encoded)
INSNS.fetch(C.rb_vm_insn_decode(encoded))
end
# @param mem_block [Integer] JIT buffer address
# @param mem_size [Integer] JIT buffer size
def initialize(mem_block, mem_size)
@cb = CodeBlock.new(mem_block: mem_block, mem_size: mem_size / 2)
@ocb = CodeBlock.new(mem_block: mem_block + mem_size / 2, mem_size: mem_size / 2, outlined: true)
@exit_compiler = ExitCompiler.new
@insn_compiler = InsnCompiler.new(@ocb, @exit_compiler)
@leave_exit = Assembler.new.then do |asm|
@exit_compiler.compile_leave_exit(asm)
@ocb.write(asm)
end
end
# Compile an ISEQ from its entry point.
# @param iseq `RubyVM::MJIT::CPointer::Struct_rb_iseq_t`
# @param cfp `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t`
def compile(iseq, cfp)
# TODO: Support has_opt
return if iseq.body.param.flags.has_opt
asm = Assembler.new
asm.comment("Block: #{iseq.body.location.label}@#{C.rb_iseq_path(iseq)}:#{iseq.body.location.first_lineno}")
compile_prologue(asm)
compile_block(asm, jit: JITState.new(iseq:, cfp:))
iseq.body.jit_func = @cb.write(asm)
rescue Exception => e
$stderr.puts e.full_message # TODO: check verbose
end
# Continue compilation from a block stub.
# @param block_stub [RubyVM::MJIT::BlockStub]
# @param cfp `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t`
# @return [Integer] The starting address of the compiled block stub
def block_stub_hit(block_stub, cfp)
# Update cfp->pc for `jit.at_current_insn?`
cfp.pc = block_stub.pc
# Prepare the jump target
new_asm = Assembler.new.tap do |asm|
jit = JITState.new(iseq: block_stub.iseq, cfp:)
compile_block(asm, jit:, pc: block_stub.pc, ctx: block_stub.ctx)
end
# Rewrite the block stub
if @cb.write_addr == block_stub.end_addr
# If the block stub's jump is the last code, overwrite the jump with the new code.
@cb.set_write_addr(block_stub.start_addr)
@cb.write(new_asm)
else
# If the block stub's jump is old code, change the jump target to the new code.
new_addr = @cb.write(new_asm)
@cb.with_write_addr(block_stub.start_addr) do
asm = Assembler.new
block_stub.change_block.call(asm, new_addr)
@cb.write(asm)
end
new_addr
end
end
# Compile a branch stub.
# @param branch_stub [RubyVM::MJIT::BranchStub]
# @param cfp `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t`
# @param branch_target_p [TrueClass,FalseClass]
# @return [Integer] The starting address of the compiled branch stub
def branch_stub_hit(branch_stub, cfp, branch_target_p)
# Update cfp->pc for `jit.at_current_insn?`
pc = branch_target_p ? branch_stub.branch_target_pc : branch_stub.fallthrough_pc
cfp.pc = pc
# Prepare the jump target
new_asm = Assembler.new.tap do |asm|
jit = JITState.new(iseq: branch_stub.iseq, cfp:)
compile_block(asm, jit:, pc:, ctx: branch_stub.ctx.dup)
end
# Rewrite the branch stub
if @cb.write_addr == branch_stub.end_addr
# If the branch stub's jump is the last code, overwrite the jump with the new code.
@cb.set_write_addr(branch_stub.start_addr)
Assembler.new.tap do |branch_asm|
if branch_target_p
branch_stub.branch_target_next.call(branch_asm)
else
branch_stub.fallthrough_next.call(branch_asm)
end
@cb.write(branch_asm)
end
# Compile a fallthrough over the jump
if branch_target_p
branch_stub.branch_target_addr = @cb.write(new_asm)
else
branch_stub.fallthrough_addr = @cb.write(new_asm)
end
else
# Otherwise, just prepare the new code somewhere
if branch_target_p
unless @cb.include?(branch_stub.branch_target_addr)
branch_stub.branch_target_addr = @cb.write(new_asm)
end
else
unless @cb.include?(branch_stub.fallthrough_addr)
branch_stub.fallthrough_addr = @cb.write(new_asm)
end
end
# Update jump destinations
branch_asm = Assembler.new
if branch_stub.end_addr == branch_stub.branch_target_addr # branch_target_next has been used
branch_stub.branch_target_next.call(branch_asm)
elsif branch_stub.end_addr == branch_stub.fallthrough_addr # fallthrough_next has been used
branch_stub.fallthrough_next.call(branch_asm)
else
branch_stub.neither_next.call(branch_asm)
end
@cb.with_write_addr(branch_stub.start_addr) do
@cb.write(branch_asm)
end
end
if branch_target_p
branch_stub.branch_target_addr
else
branch_stub.fallthrough_addr
end
end
private
# Callee-saved: rbx, rsp, rbp, r12, r13, r14, r15
# Caller-saved: rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11
#
# @param asm [RubyVM::MJIT::Assembler]
def compile_prologue(asm)
asm.comment('MJIT entry point')
# Save callee-saved registers used by JITed code
asm.push(CFP)
asm.push(EC)
asm.push(SP)
# Move arguments EC and CFP to dedicated registers
asm.mov(EC, :rdi)
asm.mov(CFP, :rsi)
# Load sp to a dedicated register
asm.mov(SP, [CFP, C.rb_control_frame_t.offsetof(:sp)]) # rbx = cfp->sp
# Setup cfp->jit_return
asm.mov(:rax, @leave_exit)
asm.mov([CFP, C.rb_control_frame_t.offsetof(:jit_return)], :rax)
end
# @param asm [RubyVM::MJIT::Assembler]
def compile_block(asm, jit:, pc: jit.iseq.body.iseq_encoded.to_i, ctx: Context.new)
# Mark the block start address and prepare an exit code storage
jit.block = Block.new(pc:)
asm.block(jit.block)
# Compile each insn
iseq = jit.iseq
index = (pc - iseq.body.iseq_encoded.to_i) / C.VALUE.size
while index < iseq.body.iseq_size
insn = self.class.decode_insn(iseq.body.iseq_encoded[index])
jit.pc = (iseq.body.iseq_encoded + index).to_i
case status = @insn_compiler.compile(jit, ctx, asm, insn)
when KeepCompiling
index += insn.len
when EndBlock
# TODO: pad nops if entry exit exists
break
when CantCompile
@exit_compiler.compile_side_exit(jit, ctx, asm)
break
else
raise "compiling #{insn.name} returned unexpected status: #{status.inspect}"
end
end
end
end
end
|