Write "5" in to the R9 register mov(r9, imm(0x5)) # Write "3" in to the RAX register mov(rax, imm(0x3)) # Add RAX and R9. Result will end up in RAX add(rax, r9) # Return from this function ret end # Allocate some executable memory jit_buffer = Fisk::Helpers.jitbuffer 4096 # Assemble the code and write to the JIT buffer fisk.write_to jit_buffer p jit_buffer.to_function([], Fiddle::TYPE_INT).call $ ruby add_two_numbers.rb 8 Code Output Write our code Allocate executable memory Write the bytes to executable memory Point the CPU at our bytes!
memory jit_buffer = Fisk::Helpers.jitbuffer 4096 # Assemble the code and write to the JIT buffer fisk.write_to jit_buffer func = jit_buffer.to_function([], Fiddle::TYPE_INT) define_method :add, &func p add De fi ne a method!
3 end fisk = Fisk.new fisk.asm do # Copy 5 to RAX mov(rax, imm(5)) # Copy 3 to RSI mov(rsi, imm(3)) # Add RAX and RSI add(rax, rsi) # Return from this function ret end 🤔
[] iseq = RubyVM::InstructionSequence.of(method(:add)) instructions = iseq.to_a.last instructions.each do |insn| next unless insn.is_a?(Array) case insn in [:putobject, v] p PUSH: v stack << v in [:opt_plus, v] left = stack.shift right = stack.shift p POP: [left, right] p PUSH: left + right stack << (left + right) in [:leave] p RETURN: stack.shift end end $ ruby test.rb {:PUSH=>5} {:PUSH=>3} {:POP=>[5, 3]} {:PUSH=>8} {:RETURN=>8} Stack Object!
def initialize @depth = 0 end # Push on the stack. Returns the location # to write a temporary variable def push reg = REGISTERS.fetch(@depth) @depth += 1 reg end # Pop off the stack. Returns the location # to read the temporary variable def pop @depth -= 1 REGISTERS.fetch(@depth) end end stack = Stack.new location = stack.push write(location, temporary_value) Max Depth: 2
unless insn.is_a?(Array) case insn in [:putobject, v] location = stack.push write(location, v) # Write the value in [:opt_plus, v] left = stack.pop right = stack.pop # Add left and right result = add(left, right) # Get the location to push location = stack.push write(location, result) in [:leave] result = stack.pop # Write the result to the return location write(return_location, result) # Then return ret end end Where should I write? Where should I read? Where should I write? Where should I read?
Fisk.new instructions.each do |insn| next unless insn.is_a?(Array) case insn in [:putobject, v] location = stack.push # Write the value fisk.mov(location, fisk.imm(v)) in [:opt_plus, v] left = stack.pop right = stack.pop # Add left and right fisk.add(left, right) # Get the location to push location = stack.push fisk.mov(location, left) in [:leave] result = stack.pop # Write the result to the return location fisk.mov(fisk.rax, result) # Then return fisk.ret end end fisk.write_to($stdout) Code $ ruby test.rb | ndisasm -b 64 - 00000000 49C7C105000000 mov r9,0x5 00000007 49C7C203000000 mov r10,0x3 0000000E 4D01CA add r10,r9 00000011 4D89D1 mov r9,r10 00000014 4C89C8 mov rax,r9 00000017 C3 ret Output Write to the “stack” Add values Write to the “stack” Write to return location Print machine code
do |insn| next unless insn.is_a?(Array) case insn in [:putobject, v] location = stack.push # Write the value fisk.mov(location, fisk.imm(v)) in [:opt_plus, v] left = stack.pop right = stack.pop # Add left and right fisk.add(left, right) # Get the location to push location = stack.push fisk.mov(location, left) in [:leave] result = stack.pop # Write the result to the return location fisk.mov(fisk.rax, result) # Then return fisk.ret end end fisk.write_to($stdout) Code $ ruby test.rb | ndisasm -b 64 - 00000000 49C7C105000000 mov r9,0x5 00000007 49C7C203000000 mov r10,0x3 0000000E 4D01CA add r10,r9 00000011 4D89D1 mov r9,r10 00000014 4C89C8 mov rax,r9 00000017 C3 ret Output CPU State Register Value R9 R10 RAX 8 8 8
do |insn| next unless insn.is_a?(Array) case insn in [:putobject, v] location = stack.push # Write the value fisk.mov(location, fisk.imm(v)) in [:opt_plus, v] left = stack.pop right = stack.pop # Add left and right fisk.add(right, left) # push stack.push in [:leave] result = stack.pop # Write the result to the return location fisk.mov(fisk.rax, result) # Then return fisk.ret end end fisk.write_to($stdout) Code $ ruby test.rb | ndisasm -b 64 - 00000000 49C7C105000000 mov r9,0x5 00000007 49C7C203000000 mov r10,0x3 0000000E 4D01D1 add r9,r10 00000011 4C89C8 mov rax,r9 00000014 C3 ret Output CPU State Register Value R9 R10 RAX 3 8 8
[R9, R10] REGISTERS = [RAX, R9] def initialize @depth = 0 end # Push on the stack. Returns the location # to write a temporary variable def push reg = REGISTERS.fetch(@depth) @depth += 1 reg end # Pop off the stack. Returns the location # to read the temporary variable def pop @depth -= 1 REGISTERS.fetch(@depth) end end Last value in RAX
|insn| next unless insn.is_a?(Array) case insn in [:putobject, v] location = stack.push # Write the value fisk.mov(location, fisk.imm(v)) in [:opt_plus, v] left = stack.pop right = stack.pop # Add left and right fisk.add(right, left) # Get the location to push location = stack.push #fisk.mov(location, left) in [:leave] result = stack.pop # The last value is already in RAX # fisk.mov(fisk.rax, result) # Then return fisk.ret end end fisk.write_to($stdout) Code $ ruby test.rb | ndisasm -b 64 - 00000000 48C7C005000000 mov rax,0x5 00000007 49C7C103000000 mov r9,0x3 0000000E 4C01C8 add rax,r9 00000011 C3 ret Output CPU State Register Value R9 R10 RAX 8 8
iseq.body return Qundef unless jit_enabled body.total_calls += 1 if body.total_calls >= call_threshold compile_iseq(iseq) end if body.jit_func body.jit_func.call else Qundef end end
iseq.body return Qundef unless jit_enabled body.total_calls += 1 if body.total_calls >= call_threshold compile_iseq(iseq) end if body.jit_func body.jit_func.call else Qundef end end
= method(:cool_method) # Get the ISeq Object iseq = RubyVM::InstructionSequence.of(m) # Unwrap the iseq pointer from the T_DATA iseq_ptr = RData.data(Fiddle.dlwrap(iseq)) # Read the JIT function iseq_t = RbISeqT.new(iseq_ptr) p iseq_t.body.jit_func # => 0 test.rb struct RData { /** Basic part, including flags and class. */ struct RBasic basic; RUBY_DATA_FUNC dmark; RUBY_DATA_FUNC dfree; /** Pointer to the actual C level struct that you want to wrap. */ void *data; }; Ruby Internals RData
= method(:cool_method) # Get the ISeq Object iseq = RubyVM::InstructionSequence.of(m) # Unwrap the iseq pointer from the T_DATA iseq_ptr = RData.data(Fiddle.dlwrap(iseq)) # Read the JIT function iseq_t = RbISeqT.new(iseq_ptr) p iseq_t.body.jit_func # => 0 test.rb struct rb_iseq_constant_body { /* [SNIP] */ #if USE_MJIT /* The following fields are MJIT related info. */ VALUE (*jit_func)(struct rb_execution_context_struct *, struct rb_control_frame_struct *); /* function pointer for loaded native code */ long unsigned total_calls; /* number of total calls with `mjit_exec()` */ struct rb_mjit_unit *jit_unit; #endif }; Ruby Internals
function fisk = Fisk.new fisk.asm do # Pop the current stack frame add(rsi, imm(RbControlFrameStruct.byte_size)) mov(m64(rdi, RbExecutionContextT.offsetof("cfp")), rsi) # Return 42 mov(rax, imm((42 << 1) | 1)) ret end buf = Fisk::Helpers.jitbuffer 1024 fisk.write_to(buf) # Assign the JIT function iseq_t.body.jit_func = buf.memory.to_i p cool_method