class Fisk

Constants

RetryRequest
VERSION

Public Class Methods

new() { |self| ... } click to toggle source
# File lib/fisk.rb, line 420
def initialize
  @instructions = []
  @labels = {}
  # A set of temp registers recorded as we see them (not at allocation time)
  @temp_registers = Set.new
  yield self if block_given?
end

Public Instance Methods

asm(buf = StringIO.new(''.b), metadata: {}) click to toggle source

Instance eval's a given block and writes encoded instructions to buf. For example:

fisk = Fisk.new
fisk.asm do
  mov r9, imm64(32)
end
# File lib/fisk.rb, line 624
def asm buf = StringIO.new(''.b), metadata: {}, &block
  instance_eval(&block)
  write_to buf, metadata: metadata
  buf
end
assign_registers(list, local: false) click to toggle source

Assign registers to any temporary registers. Only registers in list will be used when selecting register assignments

# File lib/fisk.rb, line 524
def assign_registers list, local: false
  temp_registers = @temp_registers

  # This mutates the temp registers, setting their end_point based on
  # the CFG
  self.cfg unless local

  temp_registers.each do |reg|
    unless reg.end_point
      raise Errors::UnreleasedRegisterError, "Register #{reg.name} hasn't been released"
    end
  end

  temp_registers = temp_registers.sort_by(&:start_point)

  active         = []
  free_registers = list.reverse
  register_count = list.length

  temp_registers.each do |temp_reg|
    # expire old intervals
    active, dead = active.sort_by(&:end_point).partition do |j|
      j.end_point >= temp_reg.start_point
    end

    # Add unused registers back to the free register list
    dead.each { |tr| free_registers << tr.register }

    if active.length == register_count
      raise NotImplementedError, "Register spilled"
    end

    temp_reg.register = free_registers.pop
    active << temp_reg
  end
end
basic_blocks() click to toggle source

Return a list of basic blocks for the instructions

# File lib/fisk.rb, line 448
def basic_blocks
  cfg.blocks
end
cfg() click to toggle source

Return a cfg for these instructions. The CFG includes connected basic blocks as well as any temporary registers

# File lib/fisk.rb, line 454
def cfg
  CFG.build @instructions
end
comment(message) click to toggle source

Insert a comment at the current position in the instructions.

# File lib/fisk.rb, line 511
def comment message
  @instructions << Comment.new(message)
  self
end
gen_with_insn(insns, params) click to toggle source
# File lib/fisk.rb, line 685
  def gen_with_insn insns, params
    forms = insns.forms.find_all do |insn|
      if insn.operands.length == params.length
        params.zip(insn.operands).all? { |want_op, have_op|
          want_op.works?(have_op.type)
        }
      else
        false
      end
    end

    if forms.length == 0
      valid_forms = insns.forms.map { |form|
        "    #{insns.name} #{form.operands.map(&:type).join(", ")}"
      }.join "\n"
      msg = <<~eostr
      Couldn't find instruction #{insns.name} #{params.map(&:type).join(", ")}
      Valid forms:
      #{valid_forms}
      eostr
      raise NotImplementedError, msg
    end

    form = forms.first

    insn = nil

    params.each do |param|
      if param.unresolved?
        if insns.name =~ /^J/ # I hope all jump instructions start with J!
          insn = UnresolvedJumpInstruction.new(insns, form, params.first)
        else
          # If it's not a jump instruction, assume unresolved RIP relative 😬
          insn = UnresolvedRIPInstruction.new(insns, form, params)
        end
      end

      if param.temp_register?
        if param.end_point
          raise Errors::UseAfterInvalidationError, "Register #{param.name} used after release"
        end
        @temp_registers << param

        param.start_point ||= @instructions.length
      end
    end

    insn ||= Instruction.new(insns, form, params)

    @instructions << insn

    self
  end
imm(val) click to toggle source

Create a signed immediate value of the right width

# File lib/fisk.rb, line 573
def imm val
  if val >= -0x7F - 1 && val <= 0x7F
    imm8 val
  elsif val >=  -0x7FFF - 1 && val <= 0x7FFF
    imm16 val
  elsif val >=  -0x7FFFFFFF - 1 && val <= 0x7FFFFFFF
    imm32 val
  elsif val >=  -0x7FFFFFFFFFFFFFFF - 1 && val <= 0x7FFFFFFFFFFFFFFF
    imm64 val
  else
    raise ArgumentError, "#{val} is larger than a 64 bit int"
  end
end
label(name) click to toggle source

Create a label to be used with jump instructions. For example:

fisk.jmp(fisk.label(:foo))
fisk.int(lit(3))
fisk.put_label(:foo)
# File lib/fisk.rb, line 464
def label name
  UnknownLabel.new(name)
end
lazy(&block) click to toggle source

Record a block that will be called during instruction encoding. The block will be yielded the current position of the buffer.

For example:

patch_position = nil

fisk.nop
fisk.lazy { |pos| patch_location = pos }
fisk.jmp(fisk.label(:foo))
fisk.lazy { |_|
  fisk.mov(fisk.r8, fisk.imm64(patch_location))
}
fisk.write_to(buf)

The first lazy block will be yielded the position of the buffer immediately after encoding the `nop` instruction and before encoding the `jmp` instruction. The second lazy block will be yielded to after the `jmp` instruction has been encoded. This gives you a chance to write the buffer position from certain points in to your assembly

# File lib/fisk.rb, line 498
def lazy &block
  @instructions << Lazy.new(block)
  self
end
lit(val) click to toggle source
# File lib/fisk.rb, line 612
def lit val
  Lit.new val
end
m(x, displacement = 0) click to toggle source
# File lib/fisk.rb, line 224
def m x, displacement = 0
  M.new x, displacement
end
m64(x, displacement = 0) click to toggle source
# File lib/fisk.rb, line 220
def m64 x, displacement = 0
  M64.new x, displacement
end
make_label(name)
Alias for: put_label
moffs64(val) click to toggle source
# File lib/fisk.rb, line 568
def moffs64 val
  MOffs64.new val
end
put_label(name) click to toggle source

Insert a label named name at the current position in the instructions.

# File lib/fisk.rb, line 504
def put_label name
  @instructions << Label.new(name)
  self
end
Also aliased as: make_label
register(name = "temp") click to toggle source

Allocate and return a new register. These registers will be replaced with real registers when `assign_registers` is called.

# File lib/fisk.rb, line 518
def register name = "temp"
  Registers::Temp.new name, "r64"
end
rel32(val) click to toggle source
# File lib/fisk.rb, line 608
def rel32 val
  Rel32.new val
end
rel8(val) click to toggle source
# File lib/fisk.rb, line 604
def rel8 val
  Rel8.new val
end
release_all_registers() click to toggle source

Releases all registers that haven't already been released

# File lib/fisk.rb, line 440
def release_all_registers
  @temp_registers.each do |reg|
    next if reg.end_point
    release_register reg
  end
end
release_register(reg) click to toggle source

Mark a temporary register as “done being used” at this point in the instructions. Using the register after passing the register to this method results in undefined behavior.

# File lib/fisk.rb, line 431
def release_register reg
  if reg.end_point
    raise Errors::AlreadyReleasedError, "register #{reg.name} already released at #{reg.end_point}"
  end

  reg.end_point = (@instructions.length - 1)
end
rip(displacement = 0) click to toggle source
# File lib/fisk.rb, line 216
def rip displacement = 0
  Registers::Rip.new(displacement)
end
to_binary(metadata: {}) click to toggle source

Encodes all instructions and returns a binary string with the encoded instructions.

# File lib/fisk.rb, line 632
def to_binary(metadata: {})
  io = StringIO.new ''.b
  write_to io, metadata: metadata
  io.string
end
uimm(val) click to toggle source

Create an unsigned immediate value of the right width

# File lib/fisk.rb, line 588
def uimm val
  if val < 0
    raise ArgumentError, "#{val} is negative"
  elsif val <= 0xFF
    imm8 val
  elsif val <= 0xFFFF
    imm16 val
  elsif val <= 0xFFFFFFFF
    imm32 val
  elsif val <= 0xFFFFFFFFFFFFFFFF
    imm64 val
  else
    raise ArgumentError, "#{val} is too large for a 64 bit int"
  end
end
write_to(buffer, metadata: {}) click to toggle source

Encode all instructions and write them to buffer. buffer should be an IO object.

# File lib/fisk.rb, line 642
def write_to buffer, metadata: {}
  labels = {}
  comments = {}
  unresolved = []
  instructions = @instructions.dup
  backup = @instructions.dup
  @instructions.clear

  while insn = instructions.shift
    if insn.label?
      labels[insn.name] = buffer.pos
    elsif insn.comment?
      comments.update({buffer.pos => insn.message}) { |_, *lines| lines.join($/) }
    elsif insn.lazy?
      insn.encode buffer, labels
      instructions.unshift(*@instructions)
      @instructions.clear
    else
      if insn.retry?
        retry_req = RetryRequest.new(insn, buffer.pos)
        unresolved << retry_req
      end

      write_instruction insn, buffer, labels
    end
  end

  metadata[:comments] = comments
  @instructions = backup

  return if unresolved.empty?

  pos = buffer.pos
  unresolved.each do |req|
    insn = req.insn
    buffer.seek req.io_seek_pos, IO::SEEK_SET
    insn.encode buffer, labels
  end
  buffer.seek pos, IO::SEEK_SET

  buffer
end

Private Instance Methods

write_instruction(insn, buffer, labels) click to toggle source
# File lib/fisk.rb, line 741
def write_instruction insn, buffer, labels
  insn.encode buffer, labels
end