Skip to content

Commit 43ae576

Browse files
committed
[WIP] Added code generation for the x86 architecture (issue #41).
1 parent be788bc commit 43ae576

File tree

6 files changed

+985
-0
lines changed

6 files changed

+985
-0
lines changed

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,7 @@ group :development do
3030
gem 'stackprof', require: false, platform: :mri
3131

3232
gem 'command_kit-completion', '~> 0.2', require: false
33+
34+
# codegen dependencies:
35+
gem 'nokogiri', '~> 1.4', require: false
3336
end

Rakefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,7 @@ file 'vendor/isa/arm64-sysregs.xml' => %w[vendor/cache/SysReg_xml_v86A-2020-06.t
7878
sh 'tar', '-C', 'vendor/cache', '--strip-components=1', '-xzf', 'vendor/cache/SysReg_xml_v86A-2020-06.tar.gz', 'SysReg_xml_v86A-2020-06/enc_index.xml'
7979
sh 'mv', 'vendor/cache/enc_index.xml', 'vendor/isa/arm64-sysregs.xml'
8080
end
81+
82+
namespace :codegen do
83+
task(:x86 => 'vendor/isa/x86.xml') { ruby 'codegen/x86.rb' }
84+
end

codegen/x86.rb

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
require 'erb'
2+
require 'fileutils'
3+
4+
require_relative 'x86/isa'
5+
6+
module CodeGen
7+
module X86
8+
# The root directory of the project.
9+
ROOT = File.expand_path(File.join(__dir__,'..'))
10+
11+
# The `lib/` directory of the project.
12+
LIB_DIR = File.join(ROOT,'lib')
13+
14+
# The `lib/ronin/asm/x86/` directory.
15+
DIR = File.join(LIB_DIR,'ronin','asm','x86')
16+
17+
# The `lib/ronin/asm/x86/instructions` directory.
18+
INSTRUCTIONS_DIR = File.join(DIR,'instructions')
19+
20+
#
21+
# Represents an instruction file within
22+
# `lib/ronin/asm/x86/instructions/`.
23+
#
24+
class InstructionFile
25+
26+
# The path to the `instruction.rb.erb` template file.
27+
TEMPLATE_PATH = File.join(__dir__,'x86','instruction.rb.erb')
28+
29+
# The parsed ERB template.
30+
TEMPLATE = ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-')
31+
32+
# The x86 instruction metadata.
33+
#
34+
# @return [XML::Instruction]
35+
attr_reader :instruction
36+
37+
#
38+
# Initializes the instruction file.
39+
#
40+
# @param [XML::Instruction] instruction
41+
# The x86 instruction metadata object.
42+
#
43+
def initialize(instruction)
44+
@instruction = instruction
45+
end
46+
47+
#
48+
# Generates the Ruby file for the instruction.
49+
#
50+
# @param [XML::Instruction] instruction
51+
# The x86 instruction metadata object.
52+
#
53+
# @see #generate
54+
#
55+
def self.generate(instruction)
56+
new(instruction).generate
57+
end
58+
59+
#
60+
# The base name for the instruction Ruby file.
61+
#
62+
# @return [String]
63+
#
64+
def filename
65+
"#{@instruction.name}.rb"
66+
end
67+
68+
#
69+
# The absolute path for the instruction Ruby file.
70+
#
71+
# @return [String]
72+
#
73+
def path
74+
File.join(INSTRUCTIONS_DIR,filename)
75+
end
76+
77+
#
78+
# Generates the instruction Ruby file.
79+
#
80+
def generate
81+
File.write(path,TEMPLATE.result(binding))
82+
end
83+
84+
private
85+
86+
#
87+
# Outputs either `if` or `elsif` depending on whether the `index` is `0`
88+
# or greater than `0`.
89+
#
90+
# @param [Integer] index
91+
# @return ["if", "elsif"]
92+
#
93+
def if_elsif(index)
94+
if index == 0 then 'if'
95+
else 'elsif'
96+
end
97+
end
98+
99+
#
100+
# Builds a boolean expression that matches the operand types in an
101+
# `@operands` array.
102+
#
103+
# @param [Array<Operands>] operands
104+
# @return [String]
105+
#
106+
def operands_match_expression(operands)
107+
if operands.empty?
108+
"@operands.empty?"
109+
else
110+
[
111+
"@operands.length == #{operands.length}",
112+
*operands.map.with_index { |operand,index|
113+
"@operands[#{index}].type == #{operand.type.inspect}"
114+
}
115+
].join(' && ')
116+
end
117+
end
118+
119+
#
120+
# Converts an operand index back into Ruby code.
121+
#
122+
# @param [ISA::OperandIndex] object
123+
# @return [String]
124+
#
125+
def operand_index(operand_index) = "@operands[#{operand_index.to_i}]"
126+
127+
#
128+
# Formats a hex byte.
129+
#
130+
# @param [Integer] byte
131+
# @return [String]
132+
#
133+
def hex_byte(byte) = format("0x%.2x", byte)
134+
135+
#
136+
# Formats an integer as binary.
137+
#
138+
# @param [Integer] integer
139+
# @param [Integer, nil] bits
140+
# @return [String]
141+
def binary(integer, bits: ) = format("0b%.#{bits}b", integer)
142+
143+
#
144+
# Formats an argument for an Assembler method call.
145+
#
146+
# @param [ISA::OperandIndex, Integer] object
147+
# @return [String]
148+
#
149+
def value_arg(object)
150+
case object
151+
when ISA::OperandIndex then operand_index(object)
152+
else object.inspect
153+
end
154+
end
155+
156+
#
157+
# @param [Array<CodeOffset, DataOffset, Immediate, ModRM, Opcode,
158+
# Prefix, RegisterByte, VEX, EVEX>] encoding
159+
# @return [String]
160+
#
161+
def encoder_method_call(encoding)
162+
case encoding
163+
when ISA::Encoding::CodeOffset
164+
"encoder.write_code_offset(#{value_arg(encoding.value)}, #{encoding.size.inspect})"
165+
when ISA::Encoding::DataOffset
166+
"encoder.write_data_offset(#{value_arg(encoding.value)}, #{encoding.size.inspect})"
167+
when ISA::Encoding::Immediate
168+
"encoder.write_immediate(#{value_arg(encoding.value)}, #{encoding.size.inspect})"
169+
when ISA::Encoding::ModRM
170+
"encoder.write_modrm(#{value_arg(encoding.mode)}, #{value_arg(encoding.reg)}, #{value_arg(encoding.rm)}, @operands)"
171+
when ISA::Encoding::Opcode
172+
"encoder.write_opcode(#{hex_byte(encoding.byte)}, #{value_arg(encoding.addend)})"
173+
when ISA::Encoding::Prefix
174+
"encoder.write_prefix(#{hex_byte(encoding.byte)}, #{encoding.mandatory.inspect})"
175+
when ISA::Encoding::RegisterByte
176+
"encoder.write_register_byte(#{value_arg(encoding.register)}, #{value_arg(encoding.payload)})"
177+
when ISA::Encoding::VEX
178+
"encoder.write_vex(#{encoding.type.inspect}, #{encoding.w.inspect}, #{encoding.l.inspect}, #{binary(encoding.m_mmmm, bits: 5)}, #{binary(encoding.pp, bits: 2)}, #{encoding.r.inspect}, #{encoding.x.inspect}, #{encoding.b.inspect}, #{value_arg(encoding.vvvv)})"
179+
when ISA::Encoding::EVEX
180+
"encoder.write_evex(#{binary(encoding.mmm, bits: 3)}, #{binary(encoding.pp, bits: 2)}, #{encoding.w.inspect}, #{value_arg(encoding.ll)}, #{value_arg(encoding.vvvv)}, #{encoding.v.inspect}, #{binary(encoding.rr, bits: 2)}, #{encoding.B.inspect}, #{encoding.x.inspect}, #{value_arg(encoding.b)}, #{value_arg(encoding.aaa)}, #{value_arg(encoding.z)}, #{encoding.disp8xN.inspect})"
181+
end
182+
end
183+
184+
end
185+
186+
#
187+
# Represents the `lib/ronin/asm/x86/instructions.rb` file.
188+
#
189+
class InstructionsFile
190+
191+
# The path to the `instructions.rb.erb` template file.
192+
TEMPLATE_PATH = File.join(__dir__,'x86','instructions.rb.erb')
193+
194+
# The parsed ERB template.
195+
TEMPLATE = ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-')
196+
197+
# The x86 instruction metadata objects.
198+
#
199+
# @return [Array<XML::Instruction>]
200+
attr_reader :instructions
201+
202+
#
203+
# Initializes the instructions file object.
204+
#
205+
# @param [Array<XML::Instruction>] instructions
206+
# The x86 instruction metadata objects.
207+
#
208+
def initialize(instructions)
209+
@instructions = instructions
210+
end
211+
212+
def self.generate(instructions)
213+
new(instructions).generate
214+
end
215+
216+
#
217+
# The absolute path for the `lib/ronin/asm/x86/instructions.rb` file.
218+
#
219+
# @return [String]
220+
#
221+
def path
222+
File.join(DIR,'instructions.rb')
223+
end
224+
225+
#
226+
# Generates the `lib/ronin/asm/x86/instructions.rb` file.
227+
#
228+
def generate
229+
File.write(path,TEMPLATE.result(binding))
230+
end
231+
232+
end
233+
end
234+
end
235+
236+
if $0 == __FILE__
237+
instructions = CodeGen::X86::ISA.load
238+
239+
# Ensure `lib/ronin/asm/x86/instructions/` exists.
240+
FileUtils.mkdir_p(CodeGen::X86::INSTRUCTIONS_DIR)
241+
242+
# Generate the individual instruction Ruby files within
243+
# `lib/ronin/asm/x86/instructions/`.
244+
instructions.each do |instruction|
245+
CodeGen::X86::InstructionFile.generate(instruction)
246+
end
247+
248+
# Generate the `lib/ronin/asm/x86/instructions.rb` file.
249+
CodeGen::X86::InstructionsFile.generate(instructions)
250+
end

codegen/x86/instruction.rb.erb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
#
3+
# ronin-asm - A Ruby DSL for crafting Assembly programs and shellcode.
4+
#
5+
# Copyright (c) 2007-<%= Date.today.year %> Hal Brodigan (postmodern.mod3 at gmail.com)
6+
#
7+
# ronin-asm is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU Lesser General Public License as published
9+
# by the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# ronin-asm is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Lesser General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Lesser General Public License
18+
# along with ronin-asm. If not, see <https://www.gnu.org/licenses/>.
19+
#
20+
21+
#
22+
# NOTE: this file was automatically generated. Do not edit!
23+
#
24+
25+
require_relative '../instruction'
26+
27+
module Ronin
28+
module ASM
29+
module Arch
30+
module X86
31+
module Instructions
32+
#
33+
# <%= @instruction.summary %>
34+
#
35+
class <%= @instruction.name.upcase %> < Instruction
36+
37+
#
38+
# Initializes the `<%= @instruction.name %>` instruction.
39+
#
40+
# @param [Array<Operand>] operands
41+
# The operands for the instruction.
42+
#
43+
def initialize(*operands)
44+
super(:<%= @instruction.name %>,*operands)
45+
end
46+
47+
#
48+
# Encodes the `<%= @instruction.name %>` instruction.
49+
#
50+
# @param [Encoder] encoder
51+
#
52+
# @api private
53+
#
54+
def encode(encoder)
55+
<%- @instruction.forms.each.with_index do |instruction_form,index| -%>
56+
<%= if_elsif(index) %> <%= operands_match_expression(instruction_form.operands) %>
57+
<%- instruction_form.encodings.first.each do |encoding| -%>
58+
<%= encoder_method_call(encoding) %>
59+
<%- end -%>
60+
<%- end -%>
61+
else
62+
raise(ArgumentError,"invalid operands given for instruction: #{@name} #{@operands.map(&:type).join(', ')}")
63+
end
64+
end
65+
66+
end
67+
end
68+
end
69+
end
70+
end
71+
end

codegen/x86/instructions.rb.erb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
#
3+
# ronin-asm - A Ruby DSL for crafting Assembly programs and shellcode.
4+
#
5+
# Copyright (c) 2007-<%= Date.today.year %> Hal Brodigan (postmodern.mod3 at gmail.com)
6+
#
7+
# ronin-asm is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU Lesser General Public License as published
9+
# by the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# ronin-asm is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Lesser General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Lesser General Public License
18+
# along with ronin-asm. If not, see <https://www.gnu.org/licenses/>.
19+
#
20+
21+
module Ronin
22+
module ASM
23+
module Arch
24+
module X86
25+
module Instructions
26+
<%- @instructions.each do |instruction| -%>
27+
autoload :<%= instruction.name.upcase %>, 'ronin/asm/arch/x86/instructions/<%= instruction.name %>'
28+
<%- end -%>
29+
30+
<%- @instructions.each do |instruction| -%>
31+
def <%= instruction.name %>(*operands) = Instructions::<%= instruction.name.upcase %>.new(*operands)
32+
<%- end -%>
33+
end
34+
end
35+
end
36+
end
37+
end

0 commit comments

Comments
 (0)