metasm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. data/BUGS +11 -0
  2. data/CREDITS +17 -0
  3. data/README +270 -0
  4. data/TODO +114 -0
  5. data/doc/code_organisation.txt +146 -0
  6. data/doc/const_missing.txt +16 -0
  7. data/doc/core_classes.txt +75 -0
  8. data/doc/feature_list.txt +53 -0
  9. data/doc/index.txt +59 -0
  10. data/doc/install_notes.txt +170 -0
  11. data/doc/style.css +3 -0
  12. data/doc/use_cases.txt +18 -0
  13. data/lib/metasm.rb +80 -0
  14. data/lib/metasm/arm.rb +12 -0
  15. data/lib/metasm/arm/debug.rb +39 -0
  16. data/lib/metasm/arm/decode.rb +167 -0
  17. data/lib/metasm/arm/encode.rb +77 -0
  18. data/lib/metasm/arm/main.rb +75 -0
  19. data/lib/metasm/arm/opcodes.rb +177 -0
  20. data/lib/metasm/arm/parse.rb +130 -0
  21. data/lib/metasm/arm/render.rb +55 -0
  22. data/lib/metasm/compile_c.rb +1457 -0
  23. data/lib/metasm/dalvik.rb +8 -0
  24. data/lib/metasm/dalvik/decode.rb +196 -0
  25. data/lib/metasm/dalvik/main.rb +60 -0
  26. data/lib/metasm/dalvik/opcodes.rb +366 -0
  27. data/lib/metasm/decode.rb +213 -0
  28. data/lib/metasm/decompile.rb +2659 -0
  29. data/lib/metasm/disassemble.rb +2068 -0
  30. data/lib/metasm/disassemble_api.rb +1280 -0
  31. data/lib/metasm/dynldr.rb +1329 -0
  32. data/lib/metasm/encode.rb +333 -0
  33. data/lib/metasm/exe_format/a_out.rb +194 -0
  34. data/lib/metasm/exe_format/autoexe.rb +82 -0
  35. data/lib/metasm/exe_format/bflt.rb +189 -0
  36. data/lib/metasm/exe_format/coff.rb +455 -0
  37. data/lib/metasm/exe_format/coff_decode.rb +901 -0
  38. data/lib/metasm/exe_format/coff_encode.rb +1078 -0
  39. data/lib/metasm/exe_format/dex.rb +457 -0
  40. data/lib/metasm/exe_format/dol.rb +145 -0
  41. data/lib/metasm/exe_format/elf.rb +923 -0
  42. data/lib/metasm/exe_format/elf_decode.rb +979 -0
  43. data/lib/metasm/exe_format/elf_encode.rb +1375 -0
  44. data/lib/metasm/exe_format/macho.rb +827 -0
  45. data/lib/metasm/exe_format/main.rb +228 -0
  46. data/lib/metasm/exe_format/mz.rb +164 -0
  47. data/lib/metasm/exe_format/nds.rb +172 -0
  48. data/lib/metasm/exe_format/pe.rb +437 -0
  49. data/lib/metasm/exe_format/serialstruct.rb +246 -0
  50. data/lib/metasm/exe_format/shellcode.rb +114 -0
  51. data/lib/metasm/exe_format/xcoff.rb +167 -0
  52. data/lib/metasm/gui.rb +23 -0
  53. data/lib/metasm/gui/cstruct.rb +373 -0
  54. data/lib/metasm/gui/dasm_coverage.rb +199 -0
  55. data/lib/metasm/gui/dasm_decomp.rb +369 -0
  56. data/lib/metasm/gui/dasm_funcgraph.rb +103 -0
  57. data/lib/metasm/gui/dasm_graph.rb +1354 -0
  58. data/lib/metasm/gui/dasm_hex.rb +543 -0
  59. data/lib/metasm/gui/dasm_listing.rb +599 -0
  60. data/lib/metasm/gui/dasm_main.rb +906 -0
  61. data/lib/metasm/gui/dasm_opcodes.rb +291 -0
  62. data/lib/metasm/gui/debug.rb +1228 -0
  63. data/lib/metasm/gui/gtk.rb +884 -0
  64. data/lib/metasm/gui/qt.rb +495 -0
  65. data/lib/metasm/gui/win32.rb +3004 -0
  66. data/lib/metasm/gui/x11.rb +621 -0
  67. data/lib/metasm/ia32.rb +14 -0
  68. data/lib/metasm/ia32/compile_c.rb +1523 -0
  69. data/lib/metasm/ia32/debug.rb +193 -0
  70. data/lib/metasm/ia32/decode.rb +1167 -0
  71. data/lib/metasm/ia32/decompile.rb +564 -0
  72. data/lib/metasm/ia32/encode.rb +314 -0
  73. data/lib/metasm/ia32/main.rb +233 -0
  74. data/lib/metasm/ia32/opcodes.rb +872 -0
  75. data/lib/metasm/ia32/parse.rb +327 -0
  76. data/lib/metasm/ia32/render.rb +91 -0
  77. data/lib/metasm/main.rb +1193 -0
  78. data/lib/metasm/mips.rb +11 -0
  79. data/lib/metasm/mips/compile_c.rb +7 -0
  80. data/lib/metasm/mips/decode.rb +253 -0
  81. data/lib/metasm/mips/encode.rb +51 -0
  82. data/lib/metasm/mips/main.rb +72 -0
  83. data/lib/metasm/mips/opcodes.rb +443 -0
  84. data/lib/metasm/mips/parse.rb +51 -0
  85. data/lib/metasm/mips/render.rb +43 -0
  86. data/lib/metasm/os/gnu_exports.rb +270 -0
  87. data/lib/metasm/os/linux.rb +1112 -0
  88. data/lib/metasm/os/main.rb +1686 -0
  89. data/lib/metasm/os/remote.rb +527 -0
  90. data/lib/metasm/os/windows.rb +2027 -0
  91. data/lib/metasm/os/windows_exports.rb +745 -0
  92. data/lib/metasm/parse.rb +876 -0
  93. data/lib/metasm/parse_c.rb +3938 -0
  94. data/lib/metasm/pic16c/decode.rb +42 -0
  95. data/lib/metasm/pic16c/main.rb +17 -0
  96. data/lib/metasm/pic16c/opcodes.rb +68 -0
  97. data/lib/metasm/ppc.rb +11 -0
  98. data/lib/metasm/ppc/decode.rb +264 -0
  99. data/lib/metasm/ppc/decompile.rb +251 -0
  100. data/lib/metasm/ppc/encode.rb +51 -0
  101. data/lib/metasm/ppc/main.rb +129 -0
  102. data/lib/metasm/ppc/opcodes.rb +410 -0
  103. data/lib/metasm/ppc/parse.rb +52 -0
  104. data/lib/metasm/preprocessor.rb +1277 -0
  105. data/lib/metasm/render.rb +130 -0
  106. data/lib/metasm/sh4.rb +8 -0
  107. data/lib/metasm/sh4/decode.rb +336 -0
  108. data/lib/metasm/sh4/main.rb +292 -0
  109. data/lib/metasm/sh4/opcodes.rb +381 -0
  110. data/lib/metasm/x86_64.rb +12 -0
  111. data/lib/metasm/x86_64/compile_c.rb +1025 -0
  112. data/lib/metasm/x86_64/debug.rb +59 -0
  113. data/lib/metasm/x86_64/decode.rb +268 -0
  114. data/lib/metasm/x86_64/encode.rb +264 -0
  115. data/lib/metasm/x86_64/main.rb +135 -0
  116. data/lib/metasm/x86_64/opcodes.rb +118 -0
  117. data/lib/metasm/x86_64/parse.rb +68 -0
  118. data/misc/bottleneck.rb +61 -0
  119. data/misc/cheader-findpppath.rb +58 -0
  120. data/misc/hexdiff.rb +74 -0
  121. data/misc/hexdump.rb +55 -0
  122. data/misc/metasm-all.rb +13 -0
  123. data/misc/objdiff.rb +47 -0
  124. data/misc/objscan.rb +40 -0
  125. data/misc/pdfparse.rb +661 -0
  126. data/misc/ppc_pdf2oplist.rb +192 -0
  127. data/misc/tcp_proxy_hex.rb +84 -0
  128. data/misc/txt2html.rb +440 -0
  129. data/samples/a.out.rb +31 -0
  130. data/samples/asmsyntax.rb +77 -0
  131. data/samples/bindiff.rb +555 -0
  132. data/samples/compilation-steps.rb +49 -0
  133. data/samples/cparser_makestackoffset.rb +55 -0
  134. data/samples/dasm-backtrack.rb +38 -0
  135. data/samples/dasmnavig.rb +318 -0
  136. data/samples/dbg-apihook.rb +228 -0
  137. data/samples/dbghelp.rb +143 -0
  138. data/samples/disassemble-gui.rb +102 -0
  139. data/samples/disassemble.rb +133 -0
  140. data/samples/dump_upx.rb +95 -0
  141. data/samples/dynamic_ruby.rb +1929 -0
  142. data/samples/elf_list_needed.rb +46 -0
  143. data/samples/elf_listexports.rb +33 -0
  144. data/samples/elfencode.rb +25 -0
  145. data/samples/exeencode.rb +128 -0
  146. data/samples/factorize-headers-elfimports.rb +77 -0
  147. data/samples/factorize-headers-peimports.rb +109 -0
  148. data/samples/factorize-headers.rb +43 -0
  149. data/samples/gdbclient.rb +583 -0
  150. data/samples/generate_libsigs.rb +102 -0
  151. data/samples/hotfix_gtk_dbg.rb +59 -0
  152. data/samples/install_win_env.rb +78 -0
  153. data/samples/lindebug.rb +924 -0
  154. data/samples/linux_injectsyscall.rb +95 -0
  155. data/samples/machoencode.rb +31 -0
  156. data/samples/metasm-shell.rb +91 -0
  157. data/samples/pe-hook.rb +69 -0
  158. data/samples/pe-ia32-cpuid.rb +203 -0
  159. data/samples/pe-mips.rb +35 -0
  160. data/samples/pe-shutdown.rb +78 -0
  161. data/samples/pe-testrelocs.rb +51 -0
  162. data/samples/pe-testrsrc.rb +24 -0
  163. data/samples/pe_listexports.rb +31 -0
  164. data/samples/peencode.rb +19 -0
  165. data/samples/peldr.rb +494 -0
  166. data/samples/preprocess-flatten.rb +19 -0
  167. data/samples/r0trace.rb +308 -0
  168. data/samples/rubstop.rb +399 -0
  169. data/samples/scan_pt_gnu_stack.rb +54 -0
  170. data/samples/scanpeexports.rb +62 -0
  171. data/samples/shellcode-c.rb +40 -0
  172. data/samples/shellcode-dynlink.rb +146 -0
  173. data/samples/source.asm +34 -0
  174. data/samples/struct_offset.rb +47 -0
  175. data/samples/testpe.rb +32 -0
  176. data/samples/testraw.rb +45 -0
  177. data/samples/win32genloader.rb +132 -0
  178. data/samples/win32hooker-advanced.rb +169 -0
  179. data/samples/win32hooker.rb +96 -0
  180. data/samples/win32livedasm.rb +33 -0
  181. data/samples/win32remotescan.rb +133 -0
  182. data/samples/wintrace.rb +92 -0
  183. data/tests/all.rb +8 -0
  184. data/tests/dasm.rb +39 -0
  185. data/tests/dynldr.rb +35 -0
  186. data/tests/encodeddata.rb +132 -0
  187. data/tests/ia32.rb +82 -0
  188. data/tests/mips.rb +116 -0
  189. data/tests/parse_c.rb +239 -0
  190. data/tests/preprocessor.rb +269 -0
  191. data/tests/x86_64.rb +62 -0
  192. metadata +255 -0
@@ -0,0 +1,103 @@
1
+ # This file is part of Metasm, the Ruby assembly manipulation suite
2
+ # Copyright (C) 2006-2009 Yoann GUILLOT
3
+ #
4
+ # Licence is LGPL, see LICENCE in the top-level directory
5
+
6
+ require 'metasm/gui/dasm_graph'
7
+
8
+ module Metasm
9
+ module Gui
10
+ class FuncGraphViewWidget < GraphViewWidget
11
+ # :full / :from / :to / :both
12
+ # :from = graph of functions called by addr
13
+ # :to = graph of functions calling addr
14
+ attr_accessor :graph_mode
15
+ def initialize(*a)
16
+ super(*a)
17
+ @graph_mode = :full
18
+ end
19
+
20
+ def build_ctx(ctx)
21
+ addr = @curcontext.root_addrs[0]
22
+ case @graph_mode
23
+ when :full
24
+ g = @dasm.function_graph
25
+ when :from
26
+ g = @dasm.function_graph_from(addr)
27
+ when :to
28
+ g = @dasm.function_graph_to(addr)
29
+ when :both
30
+ # merge from+to
31
+ g = @dasm.function_graph_to(addr)
32
+ @dasm.function_graph_from(addr).each { |k, v|
33
+ g[k] ||= v
34
+ g[k] |= v
35
+ }
36
+ end
37
+ g = {addr => []} if not g or g.empty?
38
+
39
+ # create boxes
40
+ (g.keys + g.values).flatten.uniq.each { |a|
41
+ # box text
42
+ txt = @dasm.get_label_at(a)
43
+ txt ||= Expression[a].to_s
44
+ b = ctx.new_box a, :addresses => [a], :line_text_col => [], :line_address => [a]
45
+ b[:line_text_col] << [[txt, :label]]
46
+ b.w = txt.length * @font_width + 2
47
+ b.h = @font_height
48
+ }
49
+
50
+ # link boxes
51
+ g.each { |f, tl| tl.each { |t| ctx.link_boxes(f, t) } }
52
+
53
+ end
54
+
55
+ def doubleclick(x, y)
56
+ if find_box_xy(x, y) and @hl_word and @zoom >= 0.90 and @zoom <= 1.1
57
+ @mousemove_origin = nil
58
+ @parent_widget.focus_addr(@hl_word, :graph)
59
+ else
60
+ super(x, y)
61
+ end
62
+ end
63
+
64
+ def get_cursor_pos
65
+ [@curcontext.root_addrs[0], @graph_mode]
66
+ end
67
+
68
+ def set_cursor_pos(p)
69
+ addr, m = p
70
+ focus_addr(addr, m)
71
+ @caret_x = 0
72
+ update_caret
73
+ end
74
+
75
+ def focus_addr(addr, mode=@graph_mode)
76
+ if mode == false
77
+ # simply center the view on addr in the current graph
78
+ raise 'fu' if not b = @curcontext.box.find { |b_| b_[:line_address][0] == addr }
79
+ @caret_box, @caret_x, @caret_y = b, 0, 0
80
+ @curcontext.view_x += (width/2 / @zoom - width/2)
81
+ @curcontext.view_y += (height/2 / @zoom - height/2)
82
+ @zoom = 1.0
83
+
84
+ focus_xy(b.x, b.y)
85
+ update_caret
86
+ return
87
+ end
88
+
89
+ return if not addr = @dasm.normalize(addr)
90
+ if not @dasm.function[addr]
91
+ return if not addr = @dasm.find_function_start(addr)
92
+ end
93
+ return true if @curcontext.root_addrs == [addr] and @graph_mode == mode
94
+ @graph_mode = mode
95
+ @curcontext = Graph.new('fu')
96
+ @curcontext.root_addrs = [addr]
97
+ @want_focus_addr = addr
98
+ gui_update
99
+ true
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,1354 @@
1
+ # This file is part of Metasm, the Ruby assembly manipulation suite
2
+ # Copyright (C) 2006-2009 Yoann GUILLOT
3
+ #
4
+ # Licence is LGPL, see LICENCE in the top-level directory
5
+
6
+ module Metasm
7
+ module Gui
8
+ class Graph
9
+ # one box, has a text, an id, and a list of other boxes to/from
10
+ class Box
11
+ attr_accessor :id, :x, :y, :w, :h
12
+ attr_accessor :to, :from # other boxes linked (arrays)
13
+ attr_accessor :content
14
+ attr_accessor :direct_to
15
+ def initialize(id, content=nil)
16
+ @id = id
17
+ @x = @y = @w = @h = 0
18
+ @to, @from = [], []
19
+ @content = content
20
+ end
21
+ def [](a) @content[a] end
22
+ #def inspect ; puts caller ; "#{Expression[@id] rescue @id.inspect}" end
23
+ end
24
+
25
+ # TODO
26
+ class MergedBox
27
+ attr_accessor :id, :text, :x, :y, :w, :h
28
+ attr_accessor :to, :from
29
+ end
30
+
31
+ attr_accessor :id, :box, :root_addrs, :view_x, :view_y, :keep_split
32
+ def initialize(id)
33
+ @id = id
34
+ @root_addrs = []
35
+ @view_x = @view_y = -0xfff_ffff
36
+ clear
37
+ end
38
+
39
+ # empty @box
40
+ def clear
41
+ @box = []
42
+ end
43
+
44
+ # link the two boxes (by id)
45
+ def link_boxes(id1, id2)
46
+ raise "unknown index 1 #{id1}" if not b1 = @box.find { |b| b.id == id1 }
47
+ raise "unknown index 2 #{id2}" if not b2 = @box.find { |b| b.id == id2 }
48
+ b1.to |= [b2]
49
+ b2.from |= [b1]
50
+ end
51
+
52
+ # creates a new box, ensures id is not already taken
53
+ def new_box(id, content=nil)
54
+ raise "duplicate id #{id}" if @box.find { |b| b.id == id }
55
+ b = Box.new(id, content)
56
+ @box << b
57
+ b
58
+ end
59
+
60
+ # place boxes in a good-looking layout
61
+ def auto_arrange_init(list=@box)
62
+ # groups is an array of box groups
63
+ # all groups are centered on the origin
64
+ @groups = list.map { |b|
65
+ b.x = -b.w/2
66
+ b.y = -b.h/2
67
+ g = Box.new(nil, [b])
68
+ g.x = b.x - 8
69
+ g.y = b.y - 9
70
+ g.w = b.w + 16
71
+ g.h = b.h + 18
72
+ g
73
+ }
74
+
75
+ # init group.to/from
76
+ # must always point to something that is in the 'groups' array
77
+ # no self references
78
+ # a box is in one and only one group in 'groups'
79
+ @groups.each { |g|
80
+ g.to = g.content.first.to.map { |t| next if not t = list.index(t) ; @groups[t] }.compact - [g]
81
+ g.from = g.content.first.from.map { |f| next if not f = list.index(f) ; @groups[f] }.compact - [g]
82
+ }
83
+
84
+ # walk from a box, fork at each multiple to, chop links to a previous box (loops etc)
85
+ @madetree = false
86
+ end
87
+
88
+ # gives a text representation of the current graph state
89
+ def dump_layout(groups=@groups)
90
+ groups.map { |g| "#{groups.index(g)} -> #{g.to.map { |t| groups.index(t) }.sort.inspect}" }
91
+ end
92
+
93
+ def auto_arrange_step
94
+ # TODO fix
95
+ # 0->[1, 2] 1->[3] 2->[3, 4] 3->[] 4->[1]
96
+ # push 0 jz l3 push 1 jz l4 push 2 l3: push 3 l4: hlt
97
+ # and more generally all non-looping graphs where this algo creates backward links
98
+
99
+ groups = @groups
100
+ return if groups.length <= 1
101
+
102
+ maketree = lambda { |roots|
103
+ next if @madetree
104
+ @madetree = true
105
+
106
+ maxdepth = {} # max arc count to reach this box from graph start (excl loop)
107
+
108
+ trim = lambda { |g, from|
109
+ # unlink g from (part of) its from
110
+ from.each { |gg| gg.to.delete g }
111
+ g.from -= from
112
+ }
113
+
114
+ walk = lambda { |g|
115
+ # score
116
+ parentdepth = g.from.map { |gg| maxdepth[gg] }
117
+ if parentdepth.empty?
118
+ # root
119
+ maxdepth[g] = 0
120
+ elsif parentdepth.include? nil
121
+ # not farthest parent found / loop
122
+ next
123
+ # elsif maxdepth[g] => ?
124
+ else
125
+ maxdepth[g] = parentdepth.max + 1
126
+ end
127
+ g.to.each { |gg| walk[gg] }
128
+ }
129
+
130
+ roots.each { |g| trim[g, g.from] unless g.from.empty? }
131
+ roots.each { |g| walk[g] }
132
+
133
+ # handle loops now (unmarked nodes)
134
+ while unmarked = groups - maxdepth.keys and not unmarked.empty?
135
+ if g = unmarked.find { |g_| g_.from.find { |gg| maxdepth[gg] } }
136
+ # loop head
137
+ trim[g, g.from.find_all { |gg| not maxdepth[gg] }] # XXX not quite sure for this
138
+ walk[g]
139
+ else
140
+ # disconnected subgraph
141
+ g = unmarked.find { |g_| g_.from.empty? } || unmarked.first
142
+ trim[g, g.from]
143
+ maxdepth[g] = 0
144
+ walk[g]
145
+ end
146
+ end
147
+ }
148
+
149
+ # concat all ary boxes into its 1st element, remove trailing groups from 'groups'
150
+ # updates from/to
151
+ merge_groups = lambda { |ary|
152
+ bg = Box.new(nil, [])
153
+ bg.x, bg.y = ary.map { |g| g.x }.min, ary.map { |g| g.y }.min
154
+ bg.w, bg.h = ary.map { |g| g.x+g.w }.max - bg.x, ary.map { |g| g.y+g.h }.max - bg.y
155
+ ary.each { |g|
156
+ bg.content.concat g.content
157
+ bg.to |= g.to
158
+ bg.from |= g.from
159
+ }
160
+ bg.to -= ary
161
+ bg.to.each { |t| t.from = t.from - ary + [bg] }
162
+ bg.from -= ary
163
+ bg.from.each { |f| f.to = f.to - ary + [bg] }
164
+ idx = ary.map { |g| groups.index(g) }.min
165
+ groups = @groups = groups - ary
166
+ groups.insert(idx, bg)
167
+ bg
168
+ }
169
+
170
+ # move all boxes within group of dx, dy
171
+ move_group = lambda { |g, dx, dy|
172
+ g.content.each { |b| b.x += dx ; b.y += dy }
173
+ g.x += dx ; g.y += dy
174
+ }
175
+
176
+ align_hz = lambda { |ary|
177
+ # if we have one of the block much bigger than the others, put it on the far right
178
+ big = ary.sort_by { |g| g.h }.last
179
+ if (ary-[big]).all? { |g| g.h < big.h/3 }
180
+ ary -= [big]
181
+ else
182
+ big = nil
183
+ end
184
+ nx = ary.map { |g| g.w }.inject(0) { |a, b| a+b } / -2
185
+ nx *= 2 if big and ary.length == 1 # just put the parent on the separation of the 2 child
186
+ ary.each { |g|
187
+ move_group[g, nx-g.x, 0]
188
+ nx += g.w
189
+ }
190
+ move_group[big, nx-big.x, 0] if big
191
+ }
192
+ align_vt = lambda { |ary|
193
+ ny = ary.map { |g| g.h }.inject(0) { |a, b| a+b } / -2
194
+ ary.each { |g|
195
+ move_group[g, 0, ny-g.y]
196
+ ny += g.h
197
+ }
198
+ }
199
+
200
+ # scan groups for a column pattern (head has 1 'to' which from == [head])
201
+ group_columns = lambda {
202
+ groups.find { |g|
203
+ next if g.from.length == 1 and g.from.first.to.length == 1
204
+ ary = [g]
205
+ ary << (g = g.to.first) while g.to.length == 1 and g.to.first.from.length == 1
206
+ next if ary.length <= 1
207
+ align_vt[ary]
208
+ merge_groups[ary]
209
+ true
210
+ }
211
+ }
212
+
213
+ # scan groups for a line pattern (multiple groups with same to & same from)
214
+ group_lines = lambda { |strict|
215
+ if groups.all? { |g1| g1.from.empty? and g1.to.empty? }
216
+ # disjoint subgraphs
217
+ align_hz[groups]
218
+ merge_groups[groups]
219
+ next true
220
+ end
221
+
222
+ groups.find { |g1|
223
+ ary = g1.from.map { |gg| gg.to }.flatten.uniq.find_all { |gg|
224
+ gg != g1 and
225
+ (gg.from - g1.from).empty? and (g1.from - gg.from).empty? and
226
+ (strict ? ((gg.to - g1.to).empty? and (g1.to - gg.to).empty?) : (g1.to & gg.to).first)
227
+ }
228
+ ary = g1.to.map { |gg| gg.from }.flatten.uniq.find_all { |gg|
229
+ gg != g1 and
230
+ (gg.to - g1.to).empty? and (g1.to - gg.to).empty? and
231
+ (strict ? ((gg.from - g1.from).empty? and (g1.from - gg.from).empty?) : (g1.from & gg.from).first)
232
+ } if ary.empty?
233
+ next if ary.empty?
234
+ ary << g1
235
+ dy = 16*ary.map { |g| g.to.length + g.from.length }.inject { |a, b| a+b }
236
+ ary.each { |g| g.h += dy ; g.y -= dy/2 }
237
+ align_hz[ary]
238
+ if ary.first.to.empty? # shrink graph if highly dissymetric and to.empty?
239
+ ah = ary.map { |g| g.h }.max
240
+ ary.each { |g|
241
+ move_group[g, 0, (g.h-ah)/2] # move up
242
+ next if not p = ary[ary.index(g)-1]
243
+ y = [g.y, p.y].min # shrink width
244
+ h = [g.h, p.h].min
245
+ xp = p.content.map { |b| b.x+b.w if b.y+b.h+8 >= y and b.y-8 <= y+h }.compact.max || p.x+p.w/2
246
+ xg = g.content.map { |b| b.x if b.y+b.h+8 >= y and b.y-8 <= y+h }.compact.min || g.x+g.w/2
247
+ dx = xg-xp-24
248
+ next if dx <= 0
249
+ ary.each { |gg|
250
+ dx = -dx if gg == g
251
+ move_group[gg, dx/2, 0]
252
+ }
253
+ if p.x+p.w > ary.last.x+ary.last.w or ary.first.x > g.x # fix broken centerism
254
+ x = [g.x, ary.first.x].min
255
+ xm = [p.x+p.w, ary.last.x+ary.last.w].max
256
+ ary.each { |gg| move_group[gg, (x+xm)/-2, 0] }
257
+ end
258
+ }
259
+ end
260
+ merge_groups[ary]
261
+ true
262
+ }
263
+ }
264
+
265
+ group_inv_if = {}
266
+
267
+ # scan groups for a if/then pattern (1 -> 2 -> 3 & 1 -> 3)
268
+ group_ifthen = lambda { |strict|
269
+ groups.reverse.find { |g|
270
+ next if not g2 = g.to.find { |g2_| (g2_.to.length == 1 and g.to.include?(g2_.to.first)) or
271
+ (not strict and g2_.to.empty?) }
272
+ next if strict and g2.from != [g] or g.to.length != 2
273
+ g2.h += 16 ; g2.y -= 8
274
+ align_vt[[g, g2]]
275
+ dx = -g2.x+8
276
+ dx -= g2.w+16 if group_inv_if[g]
277
+ move_group[g2, dx, 0]
278
+ merge_groups[[g, g2]]
279
+ true
280
+ }
281
+ }
282
+
283
+ # if (a || b) c;
284
+ # the 'else' case handles '&& else', and && is two if/then nested
285
+ group_or = lambda { |strict|
286
+ groups.find { |g|
287
+ next if g.to.length != 2
288
+ g2 = g.to[0]
289
+ g2 = g.to[1] if not g2.to.include? g.to[1]
290
+ thn = (g.to & g2.to).first
291
+ next if g2.to.length != 2 or not thn or thn.to.length != 1
292
+ els = (g2.to - [thn]).first
293
+ if thn.to == [els]
294
+ els = nil
295
+ elsif els.to != thn.to
296
+ next if strict
297
+ align_vt[[g, g2]]
298
+ merge_groups[[g, g2]]
299
+ break true
300
+ else
301
+ align_hz[[thn, els]]
302
+ thn = merge_groups[[thn, els]]
303
+ end
304
+ thn.h += 16 ; thn.y -= 8
305
+ align_vt[[g, g2, thn]]
306
+ move_group[g2, -g2.x, 0]
307
+ move_group[thn, thn.x-8, 0] if not els
308
+ merge_groups[[g, g2, thn]]
309
+ true
310
+ }
311
+ }
312
+
313
+
314
+ # loop with exit 1 -> 2, 3 & 2 -> 1
315
+ group_loop = lambda {
316
+ groups.find { |g|
317
+ next if not g2 = g.to.sort_by { |g2_| g2_.h }.find { |g2_| g2_.to == [g] or (g2_.to.empty? and g2_.from == [g]) }
318
+ g2.h += 16
319
+ align_vt[[g, g2]]
320
+ move_group[g2, g2.x-8, 0]
321
+ merge_groups[[g, g2]]
322
+ true
323
+ }
324
+ }
325
+
326
+ # same single from or to
327
+ group_halflines = lambda {
328
+ ary = nil
329
+ if groups.find { |g| ary = g.from.find_all { |gg| gg.to == [g] } and ary.length > 1 } or
330
+ groups.find { |g| ary = g.to.find_all { |gg| gg.from == [g] } and ary.length > 1 }
331
+ align_hz[ary]
332
+ merge_groups[ary]
333
+ true
334
+ end
335
+ }
336
+
337
+
338
+ # unknown pattern, group as we can..
339
+ group_other = lambda {
340
+ puts 'graph arrange: unknown configuration', dump_layout
341
+ g1 = groups.find_all { |g| g.from.empty? }
342
+ g1 << groups[rand(groups.length)] if g1.empty?
343
+ g2 = g1.map { |g| g.to }.flatten.uniq - g1
344
+ align_vt[g1]
345
+ g1 = merge_groups[g1]
346
+ g1.w += 128 ; g1.x -= 64
347
+ next if g2.empty?
348
+ align_vt[g2]
349
+ g2 = merge_groups[g2]
350
+ g2.w += 128 ; g2.x -= 64
351
+
352
+ align_hz[[g1, g2]]
353
+ merge_groups[[g1, g2]]
354
+ true
355
+ }
356
+
357
+ # check constructs with multiple blocks with to to end block (a la break;)
358
+ ign_break = lambda {
359
+ can_reach = lambda { |b1, b2, term|
360
+ next if b1 == term
361
+ done = [term]
362
+ todo = b1.to.dup
363
+ while t = todo.pop
364
+ next if done.include? t
365
+ done << t
366
+ break true if t == b2
367
+ todo.concat t.to
368
+ end
369
+ }
370
+ can_reach_unidir = lambda { |b1, b2, term| can_reach[b1, b2, term] and not can_reach[b2, b1, term] }
371
+ groups.find { |g|
372
+ f2 = nil
373
+ if (g.from.length > 2 and f3 = g.from.find { |f| f.to == [g] } and f1 = g.from.find { |f|
374
+ f2 = g.from.find { |ff| can_reach_unidir[ff, f3, g] and can_reach_unidir[f, ff, g] }}) or
375
+ (g.to.length > 2 and f3 = g.to.find { |f| f.from == [g] } and f1 = g.to.find { |f|
376
+ f2 = g.to.find { |ff| can_reach_unidir[f3, ff, g] and can_reach_unidir[ff, f, g] }})
377
+ group_inv_if[f1] = true
378
+ if f3.to == [g]
379
+ g.from.delete f2
380
+ f2.to.delete g
381
+ else
382
+ g.to.delete f2
383
+ f2.from.delete g
384
+ end
385
+ true
386
+ end
387
+ }
388
+ }
389
+
390
+ # walk graph from roots, cut backward links
391
+ trim_graph = lambda {
392
+ next true if ign_break[]
393
+ g1 = groups.find_all { |g| g.from.empty? }
394
+ g1 << groups.first if g1.empty?
395
+ cntpre = groups.inject(0) { |cntpre_, g| cntpre_ + g.to.length }
396
+ g1.each { |g| maketree[[g]] }
397
+ cntpost = groups.inject(0) { |cntpre_, g| cntpre_ + g.to.length }
398
+ true if cntpre != cntpost
399
+ }
400
+
401
+ # known, clean patterns
402
+ group_clean = lambda {
403
+ group_columns[] or group_lines[true] or group_ifthen[true] or group_loop[] or group_or[true]
404
+ }
405
+ # approximations
406
+ group_unclean = lambda {
407
+ group_lines[false] or group_or[false] or group_halflines[] or group_ifthen[false] or group_other[]
408
+ }
409
+
410
+ group_clean[] or trim_graph[] or group_unclean[]
411
+ end
412
+
413
+ # the boxes have been almost put in place, here we soften a little the result & arrange some qwirks
414
+ def auto_arrange_post
415
+ # entrypoint should be above other boxes, same for exitpoints
416
+ @box.each { |b|
417
+ if b.from == []
418
+ chld = b.to
419
+ chld = @box - [b] if not @box.find { |bb| bb != b and bb.from == [] }
420
+ chld.each { |t| b.y = t.y - b.h - 16 if t.y < b.y+b.h }
421
+ end
422
+ if b.to == []
423
+ chld = b.from
424
+ chld = @box - [b] if not @box.find { |bb| bb != b and bb.to == [] }
425
+ chld.each { |f| b.y = f.y + f.h + 16 if f.y+f.h > b.y }
426
+ end
427
+ }
428
+
429
+ boxxy = @box.sort_by { |bb| bb.y }
430
+ # fill gaps that we created
431
+ @box.each { |b|
432
+ bottom = b.y+b.h
433
+ next if not follower = boxxy.find { |bb| bb.y+bb.h > bottom }
434
+
435
+ # preserve line[] constructs margins
436
+ gap = follower.y-16*follower.from.length - (bottom+16*b.to.length)
437
+ next if gap <= 0
438
+
439
+ @box.each { |bb|
440
+ if bb.y+bb.h <= bottom
441
+ bb.y += gap/2
442
+ else
443
+ bb.y -= gap/2
444
+ end
445
+ }
446
+ boxxy = @box.sort_by { |bb| bb.y }
447
+ }
448
+
449
+ @box[0,0].each { |b|
450
+ # TODO elastic positionning (ignore up arrows ?) & collision detection (box/box + box/arrow)
451
+ f = b.from[0]
452
+ t = b.to[0]
453
+ if b.to.length == 1 and b.from.length == 1 and b.y+b.h<t.y and b.y>f.y+f.h
454
+ wx = (t.x+t.w/2 + f.x+f.w/2)/2 - b.w/2
455
+ wy = (t.y + f.y+f.h)/2 - b.h/2
456
+ b.x += (wx-b.x)/5
457
+ b.y += (wy-b.y)/5
458
+ end
459
+ }
460
+
461
+ end
462
+
463
+ def auto_arrange_boxes
464
+ auto_arrange_init
465
+ nil while @groups.length > 1 and auto_arrange_step
466
+ @groups = []
467
+ auto_arrange_post
468
+ end
469
+ end
470
+
471
+
472
+
473
+
474
+
475
+ class GraphViewWidget < DrawableWidget
476
+ attr_accessor :dasm, :caret_box, :curcontext, :zoom, :margin
477
+ # bool, specifies if we should display addresses before instrs
478
+ attr_accessor :show_addresses
479
+
480
+ def initialize_widget(dasm, parent_widget)
481
+ @dasm = dasm
482
+ @parent_widget = parent_widget
483
+
484
+ @show_addresses = false
485
+
486
+ @caret_box = nil
487
+ @selected_boxes = []
488
+ @shown_boxes = []
489
+ @mousemove_origin = @mousemove_origin_ctrl = nil
490
+ @curcontext = Graph.new(nil)
491
+ @margin = 8
492
+ @zoom = 1.0
493
+ @default_color_association = { :background => :paleblue, :hlbox_bg => :palegrey, :box_bg => :white,
494
+ :text => :black, :arrow_hl => :red, :comment => :darkblue, :address => :darkblue,
495
+ :instruction => :black, :label => :darkgreen, :caret => :black, :hl_word => :palered,
496
+ :cursorline_bg => :paleyellow, :arrow_cond => :darkgreen, :arrow_uncond => :darkblue,
497
+ :arrow_direct => :darkred }
498
+ # @othergraphs = ? (to keep user-specified formatting)
499
+ end
500
+
501
+ def resized(w, h)
502
+ redraw
503
+ end
504
+
505
+ def find_box_xy(x, y)
506
+ x = @curcontext.view_x+x/@zoom
507
+ y = @curcontext.view_y+y/@zoom
508
+ @shown_boxes.to_a.reverse.find { |b| b.x <= x and b.x+b.w > x and b.y <= y-1 and b.y+b.h > y+1 }
509
+ end
510
+
511
+ def mouse_wheel_ctrl(dir, x, y)
512
+ case dir
513
+ when :up
514
+ if @zoom < 100
515
+ oldzoom = @zoom
516
+ @zoom *= 1.1
517
+ @zoom = 1.0 if (@zoom-1.0).abs < 0.05
518
+ @curcontext.view_x += (x / oldzoom - x / @zoom)
519
+ @curcontext.view_y += (y / oldzoom - y / @zoom)
520
+ end
521
+ when :down
522
+ if @zoom > 1.0/100
523
+ oldzoom = @zoom
524
+ @zoom /= 1.1
525
+ @zoom = 1.0 if (@zoom-1.0).abs < 0.05
526
+ @curcontext.view_x += (x / oldzoom - x / @zoom)
527
+ @curcontext.view_y += (y / oldzoom - y / @zoom)
528
+ end
529
+ end
530
+ redraw
531
+ end
532
+
533
+ def mouse_wheel(dir, x, y)
534
+ case dir
535
+ when :up; @curcontext.view_y -= height/4 / @zoom
536
+ when :down; @curcontext.view_y += height/4 / @zoom
537
+ end
538
+ redraw
539
+ end
540
+
541
+ def mousemove(x, y)
542
+ return if not @mousemove_origin
543
+
544
+ dx = (x - @mousemove_origin[0])/@zoom
545
+ dy = (y - @mousemove_origin[1])/@zoom
546
+ @mousemove_origin = [x, y]
547
+ if @selected_boxes.empty?
548
+ @curcontext.view_x -= dx ; @curcontext.view_y -= dy
549
+ else
550
+ @selected_boxes.each { |b| b.x += dx ; b.y += dy }
551
+ end
552
+ redraw
553
+ end
554
+
555
+ def mouserelease(x, y)
556
+ mousemove(x, y)
557
+ @mousemove_origin = nil
558
+
559
+ if @mousemove_origin_ctrl
560
+ x1 = @curcontext.view_x + @mousemove_origin_ctrl[0]/@zoom
561
+ x2 = x1 + (x - @mousemove_origin_ctrl[0])/@zoom
562
+ x1, x2 = x2, x1 if x1 > x2
563
+ y1 = @curcontext.view_y + @mousemove_origin_ctrl[1]/@zoom
564
+ y2 = y1 + (y - @mousemove_origin_ctrl[1])/@zoom
565
+ y1, y2 = y2, y1 if y1 > y2
566
+ @selected_boxes |= @curcontext.box.find_all { |b| b.x >= x1 and b.x + b.w <= x2 and b.y >= y1 and b.y + b.h <= y2 }
567
+ redraw
568
+ @mousemove_origin_ctrl = nil
569
+ end
570
+ end
571
+
572
+ def click_ctrl(x, y)
573
+ if b = find_box_xy(x, y)
574
+ if @selected_boxes.include? b
575
+ @selected_boxes.delete b
576
+ else
577
+ @selected_boxes << b
578
+ end
579
+ redraw
580
+ else
581
+ @mousemove_origin_ctrl = [x, y]
582
+ end
583
+ end
584
+
585
+ def click(x, y)
586
+ @mousemove_origin = [x, y]
587
+ if b = find_box_xy(x, y)
588
+ @selected_boxes = [b] if not @selected_boxes.include? b
589
+ @caret_box = b
590
+ @caret_x = (@curcontext.view_x+x/@zoom-b.x-1).to_i / @font_width
591
+ @caret_y = (@curcontext.view_y+y/@zoom-b.y-1).to_i / @font_height
592
+ update_caret
593
+ else
594
+ @selected_boxes = []
595
+ @caret_box = nil
596
+ end
597
+ redraw
598
+ end
599
+
600
+ # if the target is a call to a subfunction, open a new window with the graph of this function (popup)
601
+ def rightclick(x, y)
602
+ if b = find_box_xy(x, y) and @zoom >= 0.90 and @zoom <= 1.1
603
+ click(x, y)
604
+ @mousemove_origin = nil
605
+ @parent_widget.clone_window(@hl_word, :graph)
606
+ end
607
+ end
608
+
609
+ def doubleclick(x, y)
610
+ if b = find_box_xy(x, y)
611
+ @mousemove_origin = nil
612
+ if @hl_word and @zoom >= 0.90 and @zoom <= 1.1
613
+ @parent_widget.focus_addr(@hl_word)
614
+ else
615
+ @parent_widget.focus_addr b[:addresses].first
616
+ end
617
+ elsif doubleclick_check_arrow(x, y)
618
+ elsif @zoom == 1.0
619
+ zoom_all
620
+ else
621
+ @curcontext.view_x += (x/@zoom - x)
622
+ @curcontext.view_y += (y/@zoom - y)
623
+ @zoom = 1.0
624
+ end
625
+ redraw
626
+ end
627
+
628
+ # check if the user clicked on the beginning/end of an arrow, if so focus on the other end
629
+ def doubleclick_check_arrow(x, y)
630
+ return if @margin*@zoom < 2
631
+ x = @curcontext.view_x+x/@zoom
632
+ y = @curcontext.view_y+y/@zoom
633
+ sx = nil
634
+ if bt = @shown_boxes.to_a.reverse.find { |b|
635
+ y >= b.y+b.h-1 and y <= b.y+b.h-1+@margin+2 and
636
+ sx = b.x+b.w/2 - b.to.length/2 * @margin/2 and
637
+ x >= sx-@margin/2 and x <= sx+b.to.length*@margin/2 # should be margin/4, but add a little comfort margin
638
+ }
639
+ idx = (x-sx+@margin/4).to_i / (@margin/2)
640
+ idx = 0 if idx < 0
641
+ idx = bt.to.length-1 if idx >= bt.to.length
642
+ if bt.to[idx]
643
+ if @parent_widget
644
+ @parent_widget.focus_addr bt.to[idx][:line_address][0]
645
+ else
646
+ focus_xy(bt.to[idx].x, bt.to[idx].y)
647
+ end
648
+ end
649
+ true
650
+ elsif bf = @shown_boxes.to_a.reverse.find { |b|
651
+ y >= b.y-@margin-2 and y <= b.y and
652
+ sx = b.x+b.w/2 - b.from.length/2 * @margin/2 and
653
+ x >= sx-@margin/2 and x <= sx+b.from.length*@margin/2
654
+ }
655
+ idx = (x-sx+@margin/4).to_i / (@margin/2)
656
+ idx = 0 if idx < 0
657
+ idx = bf.from.length-1 if idx >= bf.from.length
658
+ if bf.from[idx]
659
+ if @parent_widget
660
+ @parent_widget.focus_addr bf.from[idx][:line_address][-1]
661
+ else
662
+ focus_xy(bt.from[idx].x, bt.from[idx].y)
663
+ end
664
+ end
665
+ true
666
+ end
667
+ end
668
+
669
+ # update the zoom & view_xy to show the whole graph in the window
670
+ def zoom_all
671
+ minx = @curcontext.box.map { |b| b.x }.min.to_i - 10
672
+ miny = @curcontext.box.map { |b| b.y }.min.to_i - 10
673
+ maxx = @curcontext.box.map { |b| b.x + b.w }.max.to_i + 10
674
+ maxy = @curcontext.box.map { |b| b.y + b.h }.max.to_i + 10
675
+ @zoom = [width.to_f/(maxx-minx), height.to_f/(maxy-miny)].min
676
+ @zoom = 1.0 if @zoom > 1.0 or (@zoom-1.0).abs < 0.1
677
+ @curcontext.view_x = minx + (maxx-minx-width/@zoom)/2
678
+ @curcontext.view_y = miny + (maxy-miny-height/@zoom)/2
679
+ redraw
680
+ end
681
+
682
+ def paint
683
+ update_graph if @want_update_graph
684
+ if @want_focus_addr and @curcontext.box.find { |b_| b_[:line_address].index(@want_focus_addr) }
685
+ focus_addr(@want_focus_addr, false)
686
+ @want_focus_addr = nil
687
+ #zoom_all
688
+ end
689
+
690
+ @curcontext.box.each { |b|
691
+ # reorder arrows so that endings do not overlap
692
+ b.to = b.to.sort_by { |bt| bt.x+bt.w/2 }
693
+ b.from = b.from.sort_by { |bt| bt.x+bt.w/2 }
694
+ }
695
+ # arrows drawn first to stay under the boxes
696
+ # XXX precalc ?
697
+ @curcontext.box.each { |b|
698
+ b.to.each { |bt| paint_arrow(b, bt) }
699
+ }
700
+
701
+ @shown_boxes = []
702
+ w_w, w_h = width, height
703
+ @curcontext.box.each { |b|
704
+ next if b.x >= @curcontext.view_x+w_w/@zoom or b.y >= @curcontext.view_y+w_h/@zoom or b.x+b.w <= @curcontext.view_x or b.y+b.h <= @curcontext.view_y
705
+ @shown_boxes << b
706
+ paint_box(b)
707
+ }
708
+ end
709
+
710
+ def set_color_arrow(b1, b2)
711
+ if b1 == @caret_box or b2 == @caret_box
712
+ draw_color :arrow_hl
713
+ elsif b1.to.length == 1
714
+ draw_color :arrow_uncond
715
+ elsif b1.direct_to == b2.id
716
+ draw_color :arrow_direct
717
+ else
718
+ draw_color :arrow_cond
719
+ end
720
+ end
721
+
722
+ def paint_arrow(b1, b2)
723
+ x1, y1 = b1.x+b1.w/2-@curcontext.view_x, b1.y+b1.h-@curcontext.view_y
724
+ x2, y2 = b2.x+b2.w/2-@curcontext.view_x, b2.y-1-@curcontext.view_y
725
+ x1o, x2o = x1, x2
726
+ margin = @margin
727
+ x1 += (-(b1.to.length-1)/2 + b1.to.index(b2)) * margin/2
728
+ x2 += (-(b2.from.length-1)/2 + b2.from.index(b1)) * margin/2
729
+ return if (y1+margin < 0 and y2 < 0) or (y1 > height/@zoom and y2-margin > height/@zoom) # just clip on y
730
+ margin, x1, y1, x2, y2, b1w, b2w, x1o, x2o = [margin, x1, y1, x2, y2, b1.w, b2.w, x1o, x2o].map { |v| v*@zoom }
731
+
732
+
733
+ # XXX gtk wraps coords around 0x8000
734
+ if x1.abs > 0x7000 ; y1 /= x1.abs/0x7000 ; x1 /= x1.abs/0x7000 ; end
735
+ if y1.abs > 0x7000 ; x1 /= y1.abs/0x7000 ; y1 /= y1.abs/0x7000 ; end
736
+ if x2.abs > 0x7000 ; y2 /= x2.abs/0x7000 ; x2 /= x2.abs/0x7000 ; end
737
+ if y2.abs > 0x7000 ; x2 /= y2.abs/0x7000 ; y2 /= y2.abs/0x7000 ; end
738
+
739
+ # straighten vertical arrows if possible
740
+ if y2 > y1 and (x1-x2).abs <= margin
741
+ if b1.to.length == 1
742
+ x1 = x2
743
+ elsif b2.from.length == 1
744
+ x2 = x1
745
+ end
746
+ end
747
+
748
+ set_color_arrow(b1, b2)
749
+ if margin > 1
750
+ # draw arrow tip
751
+ draw_line(x1, y1, x1, y1+margin)
752
+ draw_line(x2, y2-margin+1, x2, y2)
753
+ draw_line(x2-margin/2, y2-margin/2, x2, y2)
754
+ draw_line(x2+margin/2, y2-margin/2, x2, y2)
755
+ y1 += margin
756
+ y2 -= margin-1
757
+ end
758
+ if y2+margin >= y1-margin-1
759
+ # straight vertical down arrow
760
+ draw_line(x1, y1, x2, y2) if x1 != y1 or x2 != y2
761
+
762
+ # else arrow up, need to sneak around boxes
763
+ elsif x1o-b1w/2-margin >= x2o+b2w/2+margin # z
764
+ draw_line(x1, y1, x1o-b1w/2-margin, y1)
765
+ draw_line(x1o-b1w/2-margin, y1, x2o+b2w/2+margin, y2)
766
+ draw_line(x2o+b2w/2+margin, y2, x2, y2)
767
+ draw_line(x1, y1+1, x1o-b1w/2-margin, y1+1) # double
768
+ draw_line(x1o-b1w/2-margin+1, y1, x2o+b2w/2+margin+1, y2)
769
+ draw_line(x2o+b2w/2+margin, y2+1, x2, y2+1)
770
+ elsif x1+b1w/2+margin <= x2-b2w/2-margin # invert z
771
+ draw_line(x1, y1, x1o+b1w/2+margin, y1)
772
+ draw_line(x1o+b1w/2+margin, y1, x2o-b2w/2-margin, y2)
773
+ draw_line(x2o-b2w/2-margin, y2, x2, y2)
774
+ draw_line(x1, y1+1, x1+b1w/2+margin, y1+1) # double
775
+ draw_line(x1o+b1w/2+margin+1, y1, x2o-b2w/2-margin+1, y2)
776
+ draw_line(x2o-b2w/2-margin, y2+1, x2, y2+1)
777
+ else # turn around
778
+ x = (x1 <= x2 ? [x1o-b1w/2-margin, x2o-b2w/2-margin].min : [x1o+b1w/2+margin, x2o+b2w/2+margin].max)
779
+ draw_line(x1, y1, x, y1)
780
+ draw_line(x, y1, x, y2)
781
+ draw_line(x, y2, x2, y2)
782
+ draw_line(x1, y1+1, x, y1+1) # double
783
+ draw_line(x+1, y1, x+1, y2)
784
+ draw_line(x, y2+1, x2, y2+1)
785
+ end
786
+ end
787
+
788
+ def set_color_boxshadow(b)
789
+ draw_color :black
790
+ end
791
+
792
+ def set_color_box(b)
793
+ if @selected_boxes.include? b
794
+ draw_color :hlbox_bg
795
+ else
796
+ draw_color :box_bg
797
+ end
798
+ end
799
+
800
+ def paint_box(b)
801
+ set_color_boxshadow(b)
802
+ draw_rectangle((b.x-@curcontext.view_x+3)*@zoom, (b.y-@curcontext.view_y+4)*@zoom, b.w*@zoom, b.h*@zoom)
803
+ set_color_box(b)
804
+ draw_rectangle((b.x-@curcontext.view_x)*@zoom, (b.y-@curcontext.view_y+1)*@zoom, b.w*@zoom, b.h*@zoom)
805
+
806
+ # current text position
807
+ x = (b.x - @curcontext.view_x + 1)*@zoom
808
+ y = (b.y - @curcontext.view_y + 1)*@zoom
809
+ w_w = (b.x - @curcontext.view_x + b.w - @font_width)*@zoom
810
+ w_h = (b.y - @curcontext.view_y + b.h - @font_height)*@zoom
811
+
812
+ if @parent_widget and @parent_widget.bg_color_callback
813
+ ly = 0
814
+ b[:line_address].each { |a|
815
+ if c = @parent_widget.bg_color_callback[a]
816
+ draw_rectangle_color(c, (b.x-@curcontext.view_x)*@zoom, (1+b.y-@curcontext.view_y+ly*@font_height)*@zoom, b.w*@zoom, (@font_height*@zoom).ceil)
817
+ end
818
+ ly += 1
819
+ }
820
+ end
821
+
822
+ if @caret_box == b
823
+ draw_rectangle_color(:cursorline_bg, (b.x-@curcontext.view_x)*@zoom, (1+b.y-@curcontext.view_y+@caret_y*@font_height)*@zoom, b.w*@zoom, @font_height*@zoom)
824
+ end
825
+
826
+ return if @zoom < 0.99 or @zoom > 1.1
827
+ # TODO dynamic font size ?
828
+
829
+ # renders a string at current cursor position with a color
830
+ # must not include newline
831
+ render = lambda { |str, color|
832
+ # function ends when we write under the bottom of the listing
833
+ next if y >= w_h+2 or x >= w_w
834
+ if @hl_word
835
+ stmp = str
836
+ pre_x = 0
837
+ while stmp =~ /^(.*?)(\b#{Regexp.escape @hl_word}\b)/
838
+ s1, s2 = $1, $2
839
+ pre_x += s1.length * @font_width
840
+ hl_x = s2.length * @font_width
841
+ draw_rectangle_color(:hl_word, x+pre_x, y, hl_x, @font_height*@zoom)
842
+ pre_x += hl_x
843
+ stmp = stmp[s1.length+s2.length..-1]
844
+ end
845
+ end
846
+ draw_string_color(color, x, y, str)
847
+ x += str.length * @font_width
848
+ }
849
+
850
+ b[:line_text_col].each { |list|
851
+ list.each { |s, c| render[s, c] }
852
+ x = (b.x - @curcontext.view_x + 1)*@zoom
853
+ y += @font_height*@zoom
854
+ }
855
+
856
+ if b == @caret_box and focus?
857
+ cx = (b.x - @curcontext.view_x + 1 + @caret_x*@font_width)*@zoom
858
+ cy = (b.y - @curcontext.view_y + 1 + @caret_y*@font_height)*@zoom
859
+ draw_line_color(:caret, cx, cy, cx, cy+(@font_height-1)*@zoom)
860
+ end
861
+ end
862
+
863
+ def gui_update
864
+ @want_update_graph = true
865
+ redraw
866
+ end
867
+
868
+ #
869
+ # rebuild the code flow graph from @curcontext.roots
870
+ # recalc the boxes w/h
871
+ #
872
+ def update_graph
873
+ @want_update_graph = false
874
+
875
+ ctx = @curcontext
876
+
877
+ boxcnt = ctx.box.length
878
+ arrcnt = ctx.box.inject(0) { |s, b| s + b.to.length + b.from.length }
879
+ ctx.clear
880
+
881
+ build_ctx(ctx)
882
+
883
+ ctx.auto_arrange_boxes
884
+
885
+ return if ctx != @curcontext
886
+
887
+ if boxcnt != ctx.box.length or arrcnt != ctx.box.inject(0) { |s, b| s + b.to.length + b.from.length }
888
+ zoom_all
889
+ elsif @caret_box # update @caret_box with a box at the same place
890
+ bx = @caret_box.x + @caret_box.w/2
891
+ by = @caret_box.y + @caret_box.h/2
892
+ @caret_box = ctx.box.find { |cb| cb.x < bx and cb.x+cb.w > bx and cb.y < by and cb.y+cb.h > by }
893
+ end
894
+ end
895
+
896
+ def load_dotfile(path)
897
+ @want_update_graph = false
898
+ @curcontext.clear
899
+ boxes = {}
900
+ new_box = lambda { |text|
901
+ b = @curcontext.new_box(text, :line_text_col => [[[text, :text]]])
902
+ b.w = text.length * @font_width
903
+ b.h = @font_height
904
+ b
905
+ }
906
+ max = File.size(path)
907
+ i = 0
908
+ File.open(path) { |fd|
909
+ while l = fd.gets
910
+ case l.strip
911
+ when /^"?(\w+)"?\s*->\s*"?(\w+)"?;?$/
912
+ b1 = boxes[$1] ||= new_box[$1]
913
+ b2 = boxes[$2] ||= new_box[$2]
914
+ b1.to |= [b2]
915
+ b2.from |= [b1]
916
+ end
917
+ $stderr.printf("%.02f\r" % (fd.pos*100.0/max)) if (i += 1) & 0xff == 0
918
+ end
919
+ }
920
+ p boxes.length
921
+ redraw
922
+ rescue Interrupt
923
+ p boxes.length
924
+ end
925
+
926
+ # create the graph objects in ctx
927
+ def build_ctx(ctx)
928
+ # graph : block -> following blocks in same function
929
+ block_rel = {}
930
+
931
+ todo = ctx.root_addrs.dup
932
+ done = [:default, Expression::Unknown]
933
+ while a = todo.shift
934
+ a = @dasm.normalize a
935
+ next if done.include? a
936
+ done << a
937
+ next if not di = @dasm.di_at(a)
938
+ if not di.block_head?
939
+ block_rel[di.block.address] = [a]
940
+ @dasm.split_block(a)
941
+ end
942
+ block_rel[a] = []
943
+ di.block.each_to_samefunc(@dasm) { |t|
944
+ t = @dasm.normalize t
945
+ next if not @dasm.di_at(t)
946
+ todo << t
947
+ block_rel[a] << t
948
+ }
949
+ block_rel[a].uniq!
950
+ end
951
+
952
+ # populate boxes
953
+ addr2box = {}
954
+ todo = ctx.root_addrs.dup
955
+ todo.delete_if { |t| not @dasm.di_at(t) } # undefined func start
956
+ done = []
957
+ while a = todo.shift
958
+ next if done.include? a
959
+ done << a
960
+ if not ctx.keep_split.to_a.include?(a) and from = block_rel.keys.find_all { |ba| block_rel[ba].include? a } and
961
+ from.length == 1 and block_rel[from.first].length == 1 and
962
+ addr2box[from.first] and lst = @dasm.decoded[from.first].block.list.last and
963
+ lst.next_addr == a and (not lst.opcode.props[:saveip] or lst.block.to_subfuncret)
964
+ box = addr2box[from.first]
965
+ else
966
+ box = ctx.new_box a, :addresses => [], :line_text_col => [], :line_address => []
967
+ end
968
+ @dasm.decoded[a].block.list.each { |di_|
969
+ box[:addresses] << di_.address
970
+ addr2box[di_.address] = box
971
+ }
972
+ todo.concat block_rel[a]
973
+ end
974
+
975
+ # link boxes
976
+ ctx.box.each { |b|
977
+ next if not di = @dasm.decoded[b[:addresses].last]
978
+ a = di.block.address
979
+ next if not block_rel[a]
980
+ block_rel[a].each { |t|
981
+ ctx.link_boxes(b.id, t)
982
+ b.direct_to = t if t == di.next_addr
983
+ }
984
+ }
985
+
986
+ # calc box dimensions/text
987
+ ctx.box.each { |b|
988
+ colstr = []
989
+ curaddr = nil
990
+ line = 0
991
+ render = lambda { |str, col| colstr << [str, col] }
992
+ nl = lambda {
993
+ b[:line_address][line] = curaddr
994
+ b[:line_text_col][line] = colstr
995
+ colstr = []
996
+ line += 1
997
+ }
998
+ b[:addresses].each { |addr|
999
+ curaddr = addr
1000
+ if di = @dasm.di_at(curaddr)
1001
+ if di.block_head?
1002
+ # render dump_block_header, add a few colors
1003
+ b_header = '' ; @dasm.dump_block_header(di.block) { |l| b_header << l ; b_header << ?\n if b_header[-1] != ?\n }
1004
+ b_header.strip.each_line { |l| l.chomp!
1005
+ col = :comment
1006
+ col = :label if l[0, 2] != '//' and l[-1] == ?:
1007
+ render[l, col]
1008
+ nl[]
1009
+ }
1010
+ end
1011
+ render["#{Expression[curaddr]} ", :address] if @show_addresses
1012
+ render[di.instruction.to_s.ljust(di.comment ? 24 : 0), :instruction]
1013
+ render[' ; ' + di.comment.join(' ')[0, 64], :comment] if di.comment
1014
+ nl[]
1015
+ else
1016
+ # TODO real data display (dwords, xrefs, strings..)
1017
+ if label = @dasm.get_label_at(curaddr)
1018
+ render[label + ' ', :label]
1019
+ end
1020
+ s = @dasm.get_section_at(curaddr)
1021
+ render['db '+((s and s[0].data.length > s[0].ptr) ? Expression[s[0].read(1)[0]].to_s : '?'), :text]
1022
+ nl[]
1023
+ end
1024
+ }
1025
+ b.w = b[:line_text_col].map { |strc| strc.map { |s, c| s }.join.length }.max.to_i * @font_width + 2
1026
+ b.w += 1 if b.w % 2 == 0 # ensure boxes have odd width -> vertical arrows are straight
1027
+ b.h = line * @font_height
1028
+ }
1029
+ end
1030
+
1031
+ def keypress_ctrl(key)
1032
+ case key
1033
+ when ?F
1034
+ @parent_widget.inputbox('text to search in curview (regex)', :text => @hl_word) { |pat|
1035
+ re = /#{pat}/i
1036
+ list = [['addr', 'instr']]
1037
+ @curcontext.box.each { |b|
1038
+ b[:line_text_col].zip(b[:line_address]) { |l, a|
1039
+ str = l.map { |s, c| s }.join
1040
+ list << [Expression[a], str] if str =~ re
1041
+ }
1042
+ }
1043
+ @parent_widget.list_bghilight("search result for /#{pat}/i", list) { |i| @parent_widget.focus_addr i[0] }
1044
+ }
1045
+ else return false
1046
+ end
1047
+ true
1048
+ end
1049
+
1050
+ def keypress(key)
1051
+ case key
1052
+ when :left
1053
+ if @caret_box
1054
+ if @caret_x > 0
1055
+ @caret_x -= 1
1056
+ update_caret
1057
+ elsif b = @curcontext.box.sort_by { |b_| -b_.x }.find { |b_| b_.x < @caret_box.x and
1058
+ b_.y < @caret_box.y+@caret_y*@font_height and
1059
+ b_.y+b_.h > @caret_box.y+(@caret_y+1)*@font_height }
1060
+ @caret_x = (b.w/@font_width).to_i
1061
+ @caret_y += ((@caret_box.y-b.y)/@font_height).to_i
1062
+ @caret_box = b
1063
+ update_caret
1064
+ redraw
1065
+ else
1066
+ @curcontext.view_x -= 20/@zoom
1067
+ redraw
1068
+ end
1069
+ else
1070
+ @curcontext.view_x -= 20/@zoom
1071
+ redraw
1072
+ end
1073
+ when :up
1074
+ if @caret_box
1075
+ if @caret_y > 0
1076
+ @caret_y -= 1
1077
+ update_caret
1078
+ elsif b = @curcontext.box.sort_by { |b_| -b_.y }.find { |b_| b_.y < @caret_box.y and
1079
+ b_.x < @caret_box.x+@caret_x*@font_width and
1080
+ b_.x+b_.w > @caret_box.x+(@caret_x+1)*@font_width }
1081
+ @caret_x += ((@caret_box.x-b.x)/@font_width).to_i
1082
+ @caret_y = b[:line_address].length-1
1083
+ @caret_box = b
1084
+ update_caret
1085
+ redraw
1086
+ else
1087
+ @curcontext.view_y -= 20/@zoom
1088
+ redraw
1089
+ end
1090
+ else
1091
+ @curcontext.view_y -= 20/@zoom
1092
+ redraw
1093
+ end
1094
+ when :right
1095
+ if @caret_box
1096
+ if @caret_x <= @caret_box[:line_text_col].map { |s| s.map { |ss, cc| ss }.join.length }.max
1097
+ @caret_x += 1
1098
+ update_caret
1099
+ elsif b = @curcontext.box.sort_by { |b_| b_.x }.find { |b_| b_.x > @caret_box.x and
1100
+ b_.y < @caret_box.y+@caret_y*@font_height and
1101
+ b_.y+b_.h > @caret_box.y+(@caret_y+1)*@font_height }
1102
+ @caret_x = 0
1103
+ @caret_y += ((@caret_box.y-b.y)/@font_height).to_i
1104
+ @caret_box = b
1105
+ update_caret
1106
+ redraw
1107
+ else
1108
+ @curcontext.view_x += 20/@zoom
1109
+ redraw
1110
+ end
1111
+ else
1112
+ @curcontext.view_x += 20/@zoom
1113
+ redraw
1114
+ end
1115
+ when :down
1116
+ if @caret_box
1117
+ if @caret_y < @caret_box[:line_address].length-1
1118
+ @caret_y += 1
1119
+ update_caret
1120
+ elsif b = @curcontext.box.sort_by { |b_| b_.y }.find { |b_| b_.y > @caret_box.y and
1121
+ b_.x < @caret_box.x+@caret_x*@font_width and
1122
+ b_.x+b_.w > @caret_box.x+(@caret_x+1)*@font_width }
1123
+ @caret_x += ((@caret_box.x-b.x)/@font_width).to_i
1124
+ @caret_y = 0
1125
+ @caret_box = b
1126
+ update_caret
1127
+ redraw
1128
+ else
1129
+ @curcontext.view_y += 20/@zoom
1130
+ redraw
1131
+ end
1132
+ else
1133
+ @curcontext.view_y += 20/@zoom
1134
+ redraw
1135
+ end
1136
+ when :pgup
1137
+ if @caret_box
1138
+ @caret_y = 0
1139
+ update_caret
1140
+ else
1141
+ @curcontext.view_y -= height/4/@zoom
1142
+ redraw
1143
+ end
1144
+ when :pgdown
1145
+ if @caret_box
1146
+ @caret_y = @caret_box[:line_address].length-1
1147
+ update_caret
1148
+ else
1149
+ @curcontext.view_y += height/4/@zoom
1150
+ redraw
1151
+ end
1152
+ when :home
1153
+ if @caret_box
1154
+ @caret_x = 0
1155
+ update_caret
1156
+ else
1157
+ @curcontext.view_x = @curcontext.box.map { |b_| b_.x }.min-10
1158
+ @curcontext.view_y = @curcontext.box.map { |b_| b_.y }.min-10
1159
+ redraw
1160
+ end
1161
+ when :end
1162
+ if @caret_box
1163
+ @caret_x = @caret_box[:line_text_col][@caret_y].to_a.map { |ss, cc| ss }.join.length
1164
+ update_caret
1165
+ else
1166
+ @curcontext.view_x = [@curcontext.box.map { |b_| b_.x+b_.w }.max-width/@zoom+10, @curcontext.box.map { |b_| b_.x }.min-10].max
1167
+ @curcontext.view_y = [@curcontext.box.map { |b_| b_.y+b_.h }.max-height/@zoom+10, @curcontext.box.map { |b_| b_.y }.min-10].max
1168
+ redraw
1169
+ end
1170
+
1171
+ when :delete
1172
+ @selected_boxes.each { |b_|
1173
+ @curcontext.box.delete b_
1174
+ b_.from.each { |bb| bb.to.delete b_ }
1175
+ b_.to.each { |bb| bb.from.delete b_ }
1176
+ }
1177
+ redraw
1178
+
1179
+ when ?a
1180
+ puts 'autoarrange'
1181
+ @curcontext.auto_arrange_boxes
1182
+ redraw
1183
+ puts 'autoarrange done'
1184
+ when ?u
1185
+ gui_update
1186
+
1187
+ when ?R
1188
+ load __FILE__
1189
+ when ?S # reset
1190
+ @curcontext.auto_arrange_init(@selected_boxes.empty? ? @curcontext.box : @selected_boxes)
1191
+ puts 'reset', @curcontext.dump_layout, ''
1192
+ zoom_all
1193
+ redraw
1194
+ when ?T # step auto_arrange
1195
+ @curcontext.auto_arrange_step
1196
+ puts @curcontext.dump_layout, ''
1197
+ zoom_all
1198
+ redraw
1199
+ when ?L # post auto_arrange
1200
+ @curcontext.auto_arrange_post
1201
+ zoom_all
1202
+ redraw
1203
+ when ?V # shrink
1204
+ @selected_boxes.each { |b_|
1205
+ dx = (b_.from + b_.to).map { |bb| bb.x+bb.w/2 - b_.x-b_.w/2 }
1206
+ dx = dx.inject(0) { |s, xx| s+xx }/dx.length
1207
+ b_.x += dx
1208
+ }
1209
+ redraw
1210
+ when ?I # create arbitrary boxes/links
1211
+ if @selected_boxes.empty?
1212
+ @fakebox ||= 0
1213
+ b = @curcontext.new_box "id_#@fakebox",
1214
+ :addresses => [], :line_address => [],
1215
+ :line_text_col => [[[" blublu #@fakebox", :text]]]
1216
+ b.w = @font_width * 15
1217
+ b.h = @font_height * 2
1218
+ b.x = rand(200) - 100
1219
+ b.y = rand(200) - 100
1220
+
1221
+ @fakebox += 1
1222
+ else
1223
+ b1, *bl = @selected_boxes
1224
+ bl = [b1] if bl.empty? # loop
1225
+ bl.each { |b2|
1226
+ if b1.to.include? b2
1227
+ b1.to.delete b2
1228
+ b2.from.delete b1
1229
+ else
1230
+ b1.to << b2
1231
+ b2.from << b1
1232
+ end
1233
+ }
1234
+ end
1235
+ redraw
1236
+
1237
+ when ?1 # (numeric) zoom to 1:1
1238
+ if @zoom == 1.0
1239
+ zoom_all
1240
+ else
1241
+ @curcontext.view_x += (width/2 / @zoom - width/2)
1242
+ @curcontext.view_y += (height/2 / @zoom - height/2)
1243
+ @zoom = 1.0
1244
+ end
1245
+ redraw
1246
+ when :insert # split curbox at @caret_y
1247
+ if @caret_box and a = @caret_box[:line_address][@caret_y] and @dasm.decoded[a]
1248
+ @dasm.split_block(a)
1249
+ @curcontext.keep_split ||= []
1250
+ @curcontext.keep_split |= [a]
1251
+ gui_update
1252
+ focus_addr a
1253
+ end
1254
+ else return false
1255
+ end
1256
+ true
1257
+ end
1258
+
1259
+ # find a suitable array of graph roots, walking up from a block (function start/entrypoint)
1260
+ def dasm_find_roots(addr)
1261
+ todo = [addr]
1262
+ done = []
1263
+ roots = []
1264
+ default_root = nil
1265
+ while a = todo.shift
1266
+ next if not di = @dasm.di_at(a)
1267
+ b = di.block
1268
+ a = b.address
1269
+ if done.include? a
1270
+ default_root ||= a
1271
+ next
1272
+ end
1273
+ done << a
1274
+ newf = []
1275
+ b.each_from_samefunc(@dasm) { |f| newf << f }
1276
+ if newf.empty?
1277
+ roots << b.address
1278
+ else
1279
+ todo.concat newf
1280
+ end
1281
+ end
1282
+ roots << default_root if roots.empty? and default_root
1283
+
1284
+ roots
1285
+ end
1286
+
1287
+ def set_cursor_pos(p)
1288
+ addr, x = p
1289
+ focus_addr(addr)
1290
+ @caret_x = x
1291
+ update_caret
1292
+ end
1293
+
1294
+ def get_cursor_pos
1295
+ [current_address, @caret_x]
1296
+ end
1297
+
1298
+ # focus on addr
1299
+ # addr may be a dasm label, dasm address, dasm address in string form (eg "0DEADBEEFh")
1300
+ # addr must point to a decodedinstruction
1301
+ # if the addr is not found in curcontext, the code flow is walked up until a function
1302
+ # start or an entrypoint is found, then the graph is created from there
1303
+ # will call gui_update then
1304
+ def focus_addr(addr, can_update_context=true)
1305
+ return if @parent_widget and not addr = @parent_widget.normalize(addr)
1306
+ return if not @dasm.di_at(addr)
1307
+
1308
+ # move window / change curcontext
1309
+ if b = @curcontext.box.find { |b_| b_[:line_address].index(addr) }
1310
+ @caret_box, @caret_x, @caret_y = b, 0, b[:line_address].rindex(addr)
1311
+ @curcontext.view_x += (width/2 / @zoom - width/2)
1312
+ @curcontext.view_y += (height/2 / @zoom - height/2)
1313
+ @zoom = 1.0
1314
+
1315
+ focus_xy(b.x, b.y + @caret_y*@font_height)
1316
+ update_caret
1317
+ elsif can_update_context
1318
+ @curcontext = Graph.new 'testic'
1319
+ @curcontext.root_addrs = dasm_find_roots(addr)
1320
+ @want_focus_addr = addr
1321
+ gui_update
1322
+ else
1323
+ return
1324
+ end
1325
+ true
1326
+ end
1327
+
1328
+ def focus_xy(x, y)
1329
+ if not @curcontext.view_x or @curcontext.view_x*@zoom + width*3/4 < x or @curcontext.view_x*@zoom > x
1330
+ @curcontext.view_x = (x - width/5)/@zoom
1331
+ redraw
1332
+ end
1333
+ if not @curcontext.view_y or @curcontext.view_y*@zoom + height*3/4 < y or @curcontext.view_y*@zoom > y
1334
+ @curcontext.view_y = (y - height/5)/@zoom
1335
+ redraw
1336
+ end
1337
+ end
1338
+
1339
+ # hint that the caret moved
1340
+ # redraw, change the hilighted word
1341
+ def update_caret
1342
+ return if not @caret_box or not @caret_x or not l = @caret_box[:line_text_col][@caret_y]
1343
+ l = l.map { |s, c| s }.join
1344
+ @parent_widget.focus_changed_callback[] if @parent_widget and @parent_widget.focus_changed_callback and @oldcaret_y != @caret_y
1345
+ update_hl_word(l, @caret_x)
1346
+ redraw
1347
+ end
1348
+
1349
+ def current_address
1350
+ @caret_box ? @caret_box[:line_address][@caret_y] : @curcontext.root_addrs.first
1351
+ end
1352
+ end
1353
+ end
1354
+ end