furnace-avm2 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -81,7 +81,7 @@ Contrary to that, `furnace-avm2-decompiler` works on a class level. You can incl
81
81
 
82
82
  The `-D funids` option adds a comment with method body index for each decompiled method. It can be used for debugging decompiler failures.
83
83
 
84
- You'll notice that some methods probably will not get decompiled. (The file I used in this example is quite complex.) Not every possible bytecode sequence can be directly represented in ActionScript 3, and there are some corner cases yet to be described in the decompiler. For "partially decompiled" (i.e. where there were no control flow uncertainites, but some expressions were impossible to transform to ActionScript) the relevant NF-AST code is automatically emitted. You can look at it manually with `furnace-avm2 -n`. For "failed" methods there is no generated code, but you might try to look at control flow graph (`furnace-avm2 -C`, look for emitted `method-*.dot` file) in [Graphviz](http://en.wikipedia.org/wiki/Graphviz) format to understand the logic.
84
+ You'll notice that some methods probably will not get decompiled. (The file I used in this example is quite complex.) Not every possible bytecode sequence can be directly represented in ActionScript 3, and there are some corner cases yet to be described in the decompiler. For "partially decompiled" (i.e. where there were no control flow uncertainites, but some expressions were impossible to transform to ActionScript) the relevant NF-AST code is automatically emitted. You can look at it manually with `furnace-avm2 -n`. For "failed" methods there is no generated code, but you might try to look at control flow graph (`furnace-avm2 -C graphviz`, look for emitted `method-*.dot` file) in [Graphviz](http://en.wikipedia.org/wiki/Graphviz) format to understand the logic.
85
85
 
86
86
  Programming interface
87
87
  ---------------------
@@ -15,11 +15,15 @@ require "benchmark"
15
15
 
16
16
  include Furnace
17
17
 
18
+ GRAPH_FORMATS = %w(none graphviz)
19
+
18
20
  opts = Trollop::options do
19
21
  version "furnace-avm2 #{AVM2::VERSION}"
20
22
  banner <<-EOS
21
23
  furnace-avm2 is a processing tool which operates on ActionScript3 bytecode.
22
24
 
25
+ Supported graphing formats: #{GRAPH_FORMATS.join(", ")}.
26
+
23
27
  Usage: #{__FILE__} [options]
24
28
  EOS
25
29
 
@@ -31,14 +35,14 @@ EOS
31
35
 
32
36
  opt :only, "Only operate on methods <i+>", :type => :ints, :short => '-O'
33
37
  opt :except, "Operate on all methods except <i+>", :type => :ints, :short => '-E'
34
- opt :grep, "Search <s> in method names", :type => :string, :short => '-G'
38
+ opt :grep, "Search <s> in method names", :type => :string, :short => '-g'
35
39
 
36
40
  opt :collect, "Collect failed methods instead of exiting", :default => false
37
41
  opt :sort_by_size, "Sort methods by body size", :default => false
38
42
 
39
43
  opt :disasm_before, "Disassemble methods before transforming", :default => false, :short => '-B'
40
44
  opt :disasm_after, "Disassemble methods after transforming", :default => false, :short => '-A'
41
- opt :cfg, "Emit CFG in Graphviz format for methods", :default => false, :short => '-C'
45
+ opt :cfg, "Emit CFG in specified format for methods", :type => :string, :short => '-G'
42
46
 
43
47
  opt :dce, "Eliminate dead code", :default => false
44
48
  opt :fix_names, "Remove invalid characters from names", :default => true, :short => '-q'
@@ -49,6 +53,10 @@ end
49
53
 
50
54
  Trollop::die "Stray arguments: #{ARGV}" unless ARGV.empty?
51
55
 
56
+ if opts[:cfg] && !GRAPH_FORMATS.include?(opts[:cfg])
57
+ Trollop::die "Unsupported graphing format."
58
+ end
59
+
52
60
  abc = nil
53
61
  File.open(opts[:input]) do |file|
54
62
  abc = AVM2::ABC::File.new
@@ -150,8 +158,11 @@ opts[:threads].times do
150
158
 
151
159
  if opts[:cfg]
152
160
  cfg, = body.code_to_cfg
153
- File.open("method-#{body.method_idx}.dot", "w") do |dot|
154
- dot.write cfg.to_graphviz
161
+
162
+ if opts[:cfg] == 'graphviz'
163
+ File.open("method-#{body.method_idx}.dot", "w") do |dot|
164
+ dot.write cfg.to_graphviz
165
+ end
155
166
  end
156
167
 
157
168
  puts "Method #{body.method_idx}; dominators"
@@ -17,6 +17,6 @@ Gem::Specification.new do |s|
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ["lib"]
19
19
 
20
- s.add_runtime_dependency "furnace", '= 0.2.4'
20
+ s.add_runtime_dependency "furnace", '= 0.2.5'
21
21
  s.add_runtime_dependency "trollop"
22
22
  end
@@ -1,6 +1,6 @@
1
1
  module Furnace::AVM2::ABC
2
2
  class AS3AlchemyLoadFloat32 < Opcode
3
- instruction 0x3d
3
+ instruction 0x38
4
4
 
5
5
  consume 1
6
6
  produce 1
@@ -1,6 +1,6 @@
1
1
  module Furnace::AVM2::ABC
2
2
  class AS3AlchemyLoadFloat64 < Opcode
3
- instruction 0x3e
3
+ instruction 0x39
4
4
 
5
5
  consume 1
6
6
  produce 1
@@ -1,6 +1,6 @@
1
1
  module Furnace::AVM2::ABC
2
2
  class AS3AlchemyLoadInt16 < Opcode
3
- instruction 0x3b
3
+ instruction 0x36
4
4
 
5
5
  consume 1
6
6
  produce 1
@@ -1,6 +1,6 @@
1
1
  module Furnace::AVM2::ABC
2
2
  class AS3AlchemyLoadInt32 < Opcode
3
- instruction 0x3c
3
+ instruction 0x37
4
4
 
5
5
  consume 1
6
6
  produce 1
@@ -1,6 +1,6 @@
1
1
  module Furnace::AVM2::ABC
2
2
  class AS3AlchemyLoadInt8 < Opcode
3
- instruction 0x3a
3
+ instruction 0x35
4
4
 
5
5
  consume 1
6
6
  produce 1
@@ -1,6 +1,6 @@
1
1
  module Furnace::AVM2::ABC
2
2
  class AS3AlchemyStoreFloat32 < Opcode
3
- instruction 0x38
3
+ instruction 0x3d
4
4
 
5
5
  consume 2
6
6
  produce 0
@@ -1,6 +1,6 @@
1
1
  module Furnace::AVM2::ABC
2
2
  class AS3AlchemyStoreFloat64 < Opcode
3
- instruction 0x39
3
+ instruction 0x3e
4
4
 
5
5
  consume 2
6
6
  produce 0
@@ -1,6 +1,6 @@
1
1
  module Furnace::AVM2::ABC
2
2
  class AS3AlchemyStoreInt16 < Opcode
3
- instruction 0x36
3
+ instruction 0x3b
4
4
 
5
5
  consume 2
6
6
  produce 0
@@ -1,6 +1,6 @@
1
1
  module Furnace::AVM2::ABC
2
2
  class AS3AlchemyStoreInt32 < Opcode
3
- instruction 0x37
3
+ instruction 0x3c
4
4
 
5
5
  consume 2
6
6
  produce 0
@@ -1,6 +1,6 @@
1
1
  module Furnace::AVM2::ABC
2
2
  class AS3AlchemyStoreInt8 < Opcode
3
- instruction 0x35
3
+ instruction 0x3a
4
4
 
5
5
  consume 2
6
6
  produce 0
@@ -9,8 +9,11 @@ module Furnace::AVM2::Tokens
9
9
 
10
10
  if options[:environment] == :class && options[:static]
11
11
  if origin.initializer_body
12
- properties = Furnace::AVM2::Decompiler.new(
13
- origin.initializer_body, options).decompose_static_initializer
12
+ initializer_decompiler = Furnace::AVM2::Decompiler.new(
13
+ origin.initializer_body, options.merge(global_code: true))
14
+ properties = initializer_decompiler.decompose_static_initializer
15
+ static_initialization = initializer_decompiler.decompile
16
+
14
17
  options = options.merge(property_values: properties)
15
18
  end
16
19
  end
@@ -21,6 +24,11 @@ module Furnace::AVM2::Tokens
21
24
  tokens << Furnace::Code::NewlineToken.new(origin, options)
22
25
  end
23
26
 
27
+ if static_initialization && static_initialization.children.any?
28
+ tokens << static_initialization
29
+ tokens << Furnace::Code::NewlineToken.new(origin, options)
30
+ end
31
+
24
32
  if options[:environment] == :class && !options[:static]
25
33
  tokens << ConstructorToken.new(origin, options)
26
34
  end
@@ -15,15 +15,15 @@ module Furnace::AVM2
15
15
  def initialize(body, options)
16
16
  @body, @method, @options = body, body.method, options.dup
17
17
  @closure = @options.delete(:closure)
18
+
19
+ @scopes = []
20
+ @metascopes = []
21
+ @catch_scopes = {}
18
22
  end
19
23
 
20
24
  def decompile
21
25
  begin
22
26
  @locals = Set.new([0]) + (1..@method.param_count).to_a
23
- @scopes = []
24
- @metascopes = []
25
-
26
- @catch_scopes = {}
27
27
 
28
28
  @closure_locals = Set.new
29
29
 
@@ -34,7 +34,9 @@ module Furnace::AVM2
34
34
 
35
35
  @global_slots = @options[:global_slots] || {}
36
36
 
37
- stmt_block @body.code_to_nf,
37
+ @scopes << :this if @options[:global_code]
38
+
39
+ stmt_block (@nf || @body.code_to_nf),
38
40
  function: !@options[:global_code],
39
41
  closure: @closure
40
42
 
@@ -76,8 +78,13 @@ module Furnace::AVM2
76
78
 
77
79
  def decompose_static_initializer
78
80
  properties = {}
81
+ matches = []
82
+
83
+ @nf = @body.code_to_nf
84
+
85
+ StaticProperty.find_all(@nf.children) do |match, captures|
86
+ matches.push match
79
87
 
80
- StaticProperty.find_all(@body.code_to_nf.children) do |match, captures|
81
88
  begin
82
89
  token = handle_expression(captures[:value])
83
90
  rescue ExpressionNotRecognized => e
@@ -87,6 +94,8 @@ module Furnace::AVM2
87
94
  properties[captures[:property]] = token
88
95
  end
89
96
 
97
+ @nf.children -= matches
98
+
90
99
  properties
91
100
  end
92
101
 
@@ -339,6 +348,17 @@ module Furnace::AVM2
339
348
  end
340
349
  end
341
350
 
351
+ def stmt_with(opcode, nodes)
352
+ object, scope = opcode.children
353
+
354
+ @scopes << :with
355
+ nodes << token(WithToken,
356
+ expr(object),
357
+ stmt_block(scope))
358
+ ensure
359
+ @scopes.pop
360
+ end
361
+
342
362
  # Expressions
343
363
 
344
364
  def handle_expression(opcode)
@@ -821,7 +841,7 @@ module Furnace::AVM2
821
841
  ],
822
842
  [
823
843
  either[
824
- [:get_scope_object, 0],
844
+ [:get_scope_object, capture(:scope)],
825
845
  [:get_global_scope]
826
846
  ],
827
847
  capture(:multiname)
@@ -830,6 +850,10 @@ module Furnace::AVM2
830
850
  capture_rest(:arguments)]
831
851
  end
832
852
 
853
+ def pseudo_global_scope?(scope)
854
+ [:this, :with].include?(@scopes[scope || 0])
855
+ end
856
+
833
857
  def expr_get_lex(opcode)
834
858
  multiname, = opcode.children
835
859
  get_name(nil, multiname)
@@ -837,6 +861,7 @@ module Furnace::AVM2
837
861
 
838
862
  def expr_get_property(opcode)
839
863
  if captures = PropertyGlobal.match(opcode)
864
+ return if !captures[:multiname] && !pseudo_global_scope?(captures[:scope])
840
865
  get_name(nil, captures[:multiname])
841
866
  else
842
867
  subject, multiname, = opcode.children
@@ -860,6 +885,7 @@ module Furnace::AVM2
860
885
 
861
886
  def expr_set_property(opcode)
862
887
  if captures = PropertyGlobal.match(opcode)
888
+ return if !captures[:multiname] && !pseudo_global_scope?(captures[:scope])
863
889
  token(AssignmentToken, [
864
890
  get_name(nil, captures[:multiname]),
865
891
  parenthesize(expr(*captures[:arguments]))
@@ -893,6 +919,7 @@ module Furnace::AVM2
893
919
 
894
920
  def expr_do_property(opcode, klass, has_args)
895
921
  if captures = PropertyGlobal.match(opcode)
922
+ return if !captures[:multiname] && !pseudo_global_scope?(captures[:scope])
896
923
  token(klass, [
897
924
  get_name(nil, captures[:multiname]),
898
925
  (token(ArgumentsToken, exprs(captures[:arguments])) if has_args)
@@ -1118,13 +1145,18 @@ module Furnace::AVM2
1118
1145
  origin = multiname.metadata[:origin]
1119
1146
  case origin.kind
1120
1147
  when :QName, :QNameA, :Multiname, :MultinameA
1148
+ prefix = nil
1149
+ if [:QNameA, :MultinameA].include? origin.kind
1150
+ prefix = '@'
1151
+ end
1152
+
1121
1153
  if subject
1122
1154
  token(AccessToken, [
1123
1155
  parenthesize(subject),
1124
- token(PropertyNameToken, origin.name)
1156
+ token(PropertyNameToken, "#{prefix}#{origin.name}")
1125
1157
  ])
1126
1158
  else
1127
- token(PropertyNameToken, origin.name)
1159
+ token(PropertyNameToken, "#{prefix}#{origin.name}")
1128
1160
  end
1129
1161
  when :MultinameL, :MultinameLA
1130
1162
  if subject
@@ -0,0 +1,9 @@
1
+ require_relative 'control_flow_token'
2
+
3
+ module Furnace::AVM2::Tokens
4
+ class WithToken < ControlFlowToken
5
+ def keyword
6
+ 'with'
7
+ end
8
+ end
9
+ end
@@ -235,9 +235,9 @@ module Furnace::AVM2
235
235
  log nesting, "exit point (second guess): #{exit_point.inspect}"
236
236
  end
237
237
 
238
- if exit_point.nil?
238
+ if exit_point.nil? || @dom[exit_point].include?(stopgap)
239
239
  exit_point = stopgap
240
- log nesting, "exit point (last restort): stopgap #{stopgap.inspect}"
240
+ log nesting, "exit point (third guess): stopgap #{stopgap.inspect}"
241
241
  end
242
242
 
243
243
  # Flatten the one-element sets.
@@ -305,7 +305,11 @@ module Furnace::AVM2
305
305
  node
306
306
  ])
307
307
 
308
- block = exit_point
308
+ if reachable?(exit_point, [ block ])
309
+ block = exit_point
310
+ else
311
+ block = nil
312
+ end
309
313
  elsif @loops.include?(block) && !@postcond_heads.include?(block)
310
314
  # we're trapped in a strange loop
311
315
  if block.insns.first == block.cti &&
@@ -548,6 +552,37 @@ module Furnace::AVM2
548
552
  end
549
553
  end
550
554
 
555
+ # Check if a block is reachable from sources.
556
+ def reachable?(block, sources)
557
+ worklist = sources.to_set
558
+ visited = Set[]
559
+
560
+ while worklist.any?
561
+ node = worklist.first
562
+ worklist.delete node
563
+
564
+ return true if node == block
565
+
566
+ visited.add node
567
+
568
+ node.targets.each do |target|
569
+ # Skip visited nodes.
570
+ if visited.include?(target)
571
+ next
572
+ end
573
+
574
+ # Skip back edges.
575
+ if @dom[node].include?(target)
576
+ next
577
+ end
578
+
579
+ worklist.add target
580
+ end
581
+ end
582
+
583
+ false
584
+ end
585
+
551
586
  # Find a set of merge points for a set of partially diverged
552
587
  # paths beginning from `heads'.
553
588
  # E.g. here:
@@ -178,6 +178,117 @@ module Furnace::AVM2
178
178
  ])
179
179
  end
180
180
  end
181
+
182
+ def on_begin(node)
183
+ # Fold (with)'s
184
+ with_begin = node.children.index do |child|
185
+ child.type == :push_with
186
+ end
187
+ with_end = nil
188
+
189
+ if with_begin
190
+ nesting = 0
191
+ node.children[with_begin..-1].each_with_index do |child, index|
192
+ if child.type == :push_with || child.type == :push_scope
193
+ nesting += 1
194
+ elsif child.type == :pop_scope
195
+ nesting -= 1
196
+ if nesting == 0
197
+ with_end = with_begin + index
198
+ break
199
+ end
200
+ end
201
+ end
202
+
203
+ if nesting == 0
204
+ with_scope, = node.children[with_begin].children
205
+ with_content = node.children.slice (with_begin + 1)..(with_end - 1)
206
+
207
+ with_node = AST::Node.new(:with, [
208
+ with_scope,
209
+ AST::Node.new(:begin,
210
+ with_content
211
+ )
212
+ ])
213
+
214
+ node.children.slice! with_begin..with_end
215
+ node.children.insert with_begin, with_node
216
+ end
217
+ end
218
+
219
+ # Remove obviously dead code
220
+ first_ctn = node.children.index do |child|
221
+ [:return_void, :return_value, :break, :continue, :throw].include? child.type
222
+ end
223
+ return unless first_ctn
224
+
225
+ node.children.slice! (first_ctn + 1)..-1
226
+ end
227
+
228
+ OptimizedSwitchSeed = AST::Matcher.new do
229
+ [:ternary,
230
+ [:===, capture(:case_value),
231
+ [:get_local, capture(:local_index)]],
232
+ [:integer, capture(:case_index)],
233
+ capture(:nested)]
234
+ end
235
+
236
+ OptimizedSwitchNested = AST::Matcher.new do
237
+ either[
238
+ [:ternary,
239
+ [:===, capture(:case_value),
240
+ [:get_local, backref(:local_index)]],
241
+ [:integer, capture(:case_index)],
242
+ capture(:nested)],
243
+ [:integer, capture(:default_index)]
244
+ ]
245
+ end
246
+
247
+ NumericCase = AST::Matcher.new do
248
+ [:case, [:integer, capture(:index)]]
249
+ end
250
+
251
+ def on_switch(node)
252
+ condition, body = node.children
253
+
254
+ if captures = OptimizedSwitchSeed.match(condition)
255
+ mapping = { captures[:case_index] => captures[:case_value] }
256
+ while captures = OptimizedSwitchNested.match(captures[:nested], captures)
257
+ break if captures[:default_index]
258
+ mapping[captures[:case_index]] = captures[:case_value]
259
+ end
260
+
261
+ return if captures.nil?
262
+
263
+ case_mapping = {}
264
+
265
+ body.children.each do |child|
266
+ if case_captures = NumericCase.match(child)
267
+ case_index = case_captures[:index]
268
+ if captures[:default_index] == case_index
269
+ case_mapping[child] = nil
270
+ elsif mapping.has_key?(case_index)
271
+ case_mapping[child] = mapping[case_index]
272
+ else
273
+ # fallback
274
+ return
275
+ end
276
+ end
277
+ end
278
+
279
+ # At this point, we are sure that this switch can be transformed.
280
+
281
+ node.children[0] = AST::Node.new(:get_local, [ captures[:local_index] ])
282
+
283
+ case_mapping.each do |child, value|
284
+ if value.nil?
285
+ body.children.delete child
286
+ else
287
+ child.children[0] = value
288
+ end
289
+ end
290
+ end
291
+ end
181
292
  end
182
293
  end
183
294
  end
@@ -1,5 +1,5 @@
1
1
  module Furnace
2
2
  module AVM2
3
- VERSION = "1.0.1"
3
+ VERSION = "1.0.2"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: furnace-avm2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-06-22 00:00:00.000000000 Z
12
+ date: 2012-07-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: furnace
@@ -18,7 +18,7 @@ dependencies:
18
18
  requirements:
19
19
  - - '='
20
20
  - !ruby/object:Gem::Version
21
- version: 0.2.4
21
+ version: 0.2.5
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - '='
28
28
  - !ruby/object:Gem::Version
29
- version: 0.2.4
29
+ version: 0.2.5
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: trollop
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -344,6 +344,7 @@ files:
344
344
  - lib/furnace-avm2/source/implementation_tokens/unary_post_operator_token.rb
345
345
  - lib/furnace-avm2/source/implementation_tokens/variable_name_token.rb
346
346
  - lib/furnace-avm2/source/implementation_tokens/while_token.rb
347
+ - lib/furnace-avm2/source/implementation_tokens/with_token.rb
347
348
  - lib/furnace-avm2/source/implementation_tokens/xml_literal_token.rb
348
349
  - lib/furnace-avm2/transform.rb
349
350
  - lib/furnace-avm2/transform/ast_build.rb
@@ -377,9 +378,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
377
378
  version: '0'
378
379
  requirements: []
379
380
  rubyforge_project:
380
- rubygems_version: 1.8.23
381
+ rubygems_version: 1.8.24
381
382
  signing_key:
382
383
  specification_version: 3
383
384
  summary: AVM2 analysis framework based on Furnace
384
385
  test_files: []
385
- has_rdoc: