# This file is part of Metasm, the Ruby assembly manipulation suite # Copyright (C) 2006-2009 Yoann GUILLOT # # Licence is LGPL, see LICENCE in the top-level directory module Metasm module Gui class Graph # one box, has a text, an id, and a list of other boxes to/from class Box attr_accessor :id, :x, :y, :w, :h attr_accessor :to, :from # other boxes linked (arrays) attr_accessor :content attr_accessor :direct_to def initialize(id, content=nil) @id = id @x = @y = @w = @h = 0 @to, @from = [], [] @content = content end def [](a) @content[a] end #def inspect ; puts caller ; "#{Expression[@id] rescue @id.inspect}" end end # TODO class MergedBox attr_accessor :id, :text, :x, :y, :w, :h attr_accessor :to, :from end attr_accessor :id, :box, :root_addrs, :view_x, :view_y, :keep_split def initialize(id) @id = id @root_addrs = [] @view_x = @view_y = -0xfff_ffff clear end # empty @box def clear @box = [] end # link the two boxes (by id) def link_boxes(id1, id2) raise "unknown index 1 #{id1}" if not b1 = @box.find { |b| b.id == id1 } raise "unknown index 2 #{id2}" if not b2 = @box.find { |b| b.id == id2 } b1.to |= [b2] b2.from |= [b1] end # creates a new box, ensures id is not already taken def new_box(id, content=nil) raise "duplicate id #{id}" if @box.find { |b| b.id == id } b = Box.new(id, content) @box << b b end # place boxes in a good-looking layout def auto_arrange_init(list=@box) # groups is an array of box groups # all groups are centered on the origin @groups = list.map { |b| b.x = -b.w/2 b.y = -b.h/2 g = Box.new(nil, [b]) g.x = b.x - 8 g.y = b.y - 9 g.w = b.w + 16 g.h = b.h + 18 g } # init group.to/from # must always point to something that is in the 'groups' array # no self references # a box is in one and only one group in 'groups' @groups.each { |g| g.to = g.content.first.to.map { |t| next if not t = list.index(t) ; @groups[t] }.compact - [g] g.from = g.content.first.from.map { |f| next if not f = list.index(f) ; @groups[f] }.compact - [g] } # walk from a box, fork at each multiple to, chop links to a previous box (loops etc) @madetree = false end # gives a text representation of the current graph state def dump_layout(groups=@groups) groups.map { |g| "#{groups.index(g)} -> #{g.to.map { |t| groups.index(t) }.sort.inspect}" } end def auto_arrange_step # TODO fix # 0->[1, 2] 1->[3] 2->[3, 4] 3->[] 4->[1] # push 0 jz l3 push 1 jz l4 push 2 l3: push 3 l4: hlt # and more generally all non-looping graphs where this algo creates backward links groups = @groups return if groups.length <= 1 maketree = lambda { |roots| next if @madetree @madetree = true maxdepth = {} # max arc count to reach this box from graph start (excl loop) trim = lambda { |g, from| # unlink g from (part of) its from from.each { |gg| gg.to.delete g } g.from -= from } walk = lambda { |g| # score parentdepth = g.from.map { |gg| maxdepth[gg] } if parentdepth.empty? # root maxdepth[g] = 0 elsif parentdepth.include? nil # not farthest parent found / loop next # elsif maxdepth[g] => ? else maxdepth[g] = parentdepth.max + 1 end g.to.each { |gg| walk[gg] } } roots.each { |g| trim[g, g.from] unless g.from.empty? } roots.each { |g| walk[g] } # handle loops now (unmarked nodes) while unmarked = groups - maxdepth.keys and not unmarked.empty? if g = unmarked.find { |g_| g_.from.find { |gg| maxdepth[gg] } } # loop head trim[g, g.from.find_all { |gg| not maxdepth[gg] }] # XXX not quite sure for this walk[g] else # disconnected subgraph g = unmarked.find { |g_| g_.from.empty? } || unmarked.first trim[g, g.from] maxdepth[g] = 0 walk[g] end end } # concat all ary boxes into its 1st element, remove trailing groups from 'groups' # updates from/to merge_groups = lambda { |ary| bg = Box.new(nil, []) bg.x, bg.y = ary.map { |g| g.x }.min, ary.map { |g| g.y }.min bg.w, bg.h = ary.map { |g| g.x+g.w }.max - bg.x, ary.map { |g| g.y+g.h }.max - bg.y ary.each { |g| bg.content.concat g.content bg.to |= g.to bg.from |= g.from } bg.to -= ary bg.to.each { |t| t.from = t.from - ary + [bg] } bg.from -= ary bg.from.each { |f| f.to = f.to - ary + [bg] } idx = ary.map { |g| groups.index(g) }.min groups = @groups = groups - ary groups.insert(idx, bg) bg } # move all boxes within group of dx, dy move_group = lambda { |g, dx, dy| g.content.each { |b| b.x += dx ; b.y += dy } g.x += dx ; g.y += dy } align_hz = lambda { |ary| # if we have one of the block much bigger than the others, put it on the far right big = ary.sort_by { |g| g.h }.last if (ary-[big]).all? { |g| g.h < big.h/3 } ary -= [big] else big = nil end nx = ary.map { |g| g.w }.inject(0) { |a, b| a+b } / -2 nx *= 2 if big and ary.length == 1 # just put the parent on the separation of the 2 child ary.each { |g| move_group[g, nx-g.x, 0] nx += g.w } move_group[big, nx-big.x, 0] if big } align_vt = lambda { |ary| ny = ary.map { |g| g.h }.inject(0) { |a, b| a+b } / -2 ary.each { |g| move_group[g, 0, ny-g.y] ny += g.h } } # scan groups for a column pattern (head has 1 'to' which from == [head]) group_columns = lambda { groups.find { |g| next if g.from.length == 1 and g.from.first.to.length == 1 ary = [g] ary << (g = g.to.first) while g.to.length == 1 and g.to.first.from.length == 1 next if ary.length <= 1 align_vt[ary] merge_groups[ary] true } } # scan groups for a line pattern (multiple groups with same to & same from) group_lines = lambda { |strict| if groups.all? { |g1| g1.from.empty? and g1.to.empty? } # disjoint subgraphs align_hz[groups] merge_groups[groups] next true end groups.find { |g1| ary = g1.from.map { |gg| gg.to }.flatten.uniq.find_all { |gg| gg != g1 and (gg.from - g1.from).empty? and (g1.from - gg.from).empty? and (strict ? ((gg.to - g1.to).empty? and (g1.to - gg.to).empty?) : (g1.to & gg.to).first) } ary = g1.to.map { |gg| gg.from }.flatten.uniq.find_all { |gg| gg != g1 and (gg.to - g1.to).empty? and (g1.to - gg.to).empty? and (strict ? ((gg.from - g1.from).empty? and (g1.from - gg.from).empty?) : (g1.from & gg.from).first) } if ary.empty? next if ary.empty? ary << g1 dy = 16*ary.map { |g| g.to.length + g.from.length }.inject { |a, b| a+b } ary.each { |g| g.h += dy ; g.y -= dy/2 } align_hz[ary] if ary.first.to.empty? # shrink graph if highly dissymetric and to.empty? ah = ary.map { |g| g.h }.max ary.each { |g| move_group[g, 0, (g.h-ah)/2] # move up next if not p = ary[ary.index(g)-1] y = [g.y, p.y].min # shrink width h = [g.h, p.h].min 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 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 dx = xg-xp-24 next if dx <= 0 ary.each { |gg| dx = -dx if gg == g move_group[gg, dx/2, 0] } if p.x+p.w > ary.last.x+ary.last.w or ary.first.x > g.x # fix broken centerism x = [g.x, ary.first.x].min xm = [p.x+p.w, ary.last.x+ary.last.w].max ary.each { |gg| move_group[gg, (x+xm)/-2, 0] } end } end merge_groups[ary] true } } group_inv_if = {} # scan groups for a if/then pattern (1 -> 2 -> 3 & 1 -> 3) group_ifthen = lambda { |strict| groups.reverse.find { |g| next if not g2 = g.to.find { |g2_| (g2_.to.length == 1 and g.to.include?(g2_.to.first)) or (not strict and g2_.to.empty?) } next if strict and g2.from != [g] or g.to.length != 2 g2.h += 16 ; g2.y -= 8 align_vt[[g, g2]] dx = -g2.x+8 dx -= g2.w+16 if group_inv_if[g] move_group[g2, dx, 0] merge_groups[[g, g2]] true } } # if (a || b) c; # the 'else' case handles '&& else', and && is two if/then nested group_or = lambda { |strict| groups.find { |g| next if g.to.length != 2 g2 = g.to[0] g2 = g.to[1] if not g2.to.include? g.to[1] thn = (g.to & g2.to).first next if g2.to.length != 2 or not thn or thn.to.length != 1 els = (g2.to - [thn]).first if thn.to == [els] els = nil elsif els.to != thn.to next if strict align_vt[[g, g2]] merge_groups[[g, g2]] break true else align_hz[[thn, els]] thn = merge_groups[[thn, els]] end thn.h += 16 ; thn.y -= 8 align_vt[[g, g2, thn]] move_group[g2, -g2.x, 0] move_group[thn, thn.x-8, 0] if not els merge_groups[[g, g2, thn]] true } } # loop with exit 1 -> 2, 3 & 2 -> 1 group_loop = lambda { groups.find { |g| next if not g2 = g.to.sort_by { |g2_| g2_.h }.find { |g2_| g2_.to == [g] or (g2_.to.empty? and g2_.from == [g]) } g2.h += 16 align_vt[[g, g2]] move_group[g2, g2.x-8, 0] merge_groups[[g, g2]] true } } # same single from or to group_halflines = lambda { ary = nil if groups.find { |g| ary = g.from.find_all { |gg| gg.to == [g] } and ary.length > 1 } or groups.find { |g| ary = g.to.find_all { |gg| gg.from == [g] } and ary.length > 1 } align_hz[ary] merge_groups[ary] true end } # unknown pattern, group as we can.. group_other = lambda { puts 'graph arrange: unknown configuration', dump_layout g1 = groups.find_all { |g| g.from.empty? } g1 << groups[rand(groups.length)] if g1.empty? g2 = g1.map { |g| g.to }.flatten.uniq - g1 align_vt[g1] g1 = merge_groups[g1] g1.w += 128 ; g1.x -= 64 next if g2.empty? align_vt[g2] g2 = merge_groups[g2] g2.w += 128 ; g2.x -= 64 align_hz[[g1, g2]] merge_groups[[g1, g2]] true } # check constructs with multiple blocks with to to end block (a la break;) ign_break = lambda { can_reach = lambda { |b1, b2, term| next if b1 == term done = [term] todo = b1.to.dup while t = todo.pop next if done.include? t done << t break true if t == b2 todo.concat t.to end } can_reach_unidir = lambda { |b1, b2, term| can_reach[b1, b2, term] and not can_reach[b2, b1, term] } groups.find { |g| f2 = nil if (g.from.length > 2 and f3 = g.from.find { |f| f.to == [g] } and f1 = g.from.find { |f| f2 = g.from.find { |ff| can_reach_unidir[ff, f3, g] and can_reach_unidir[f, ff, g] }}) or (g.to.length > 2 and f3 = g.to.find { |f| f.from == [g] } and f1 = g.to.find { |f| f2 = g.to.find { |ff| can_reach_unidir[f3, ff, g] and can_reach_unidir[ff, f, g] }}) group_inv_if[f1] = true if f3.to == [g] g.from.delete f2 f2.to.delete g else g.to.delete f2 f2.from.delete g end true end } } # walk graph from roots, cut backward links trim_graph = lambda { next true if ign_break[] g1 = groups.find_all { |g| g.from.empty? } g1 << groups.first if g1.empty? cntpre = groups.inject(0) { |cntpre_, g| cntpre_ + g.to.length } g1.each { |g| maketree[[g]] } cntpost = groups.inject(0) { |cntpre_, g| cntpre_ + g.to.length } true if cntpre != cntpost } # known, clean patterns group_clean = lambda { group_columns[] or group_lines[true] or group_ifthen[true] or group_loop[] or group_or[true] } # approximations group_unclean = lambda { group_lines[false] or group_or[false] or group_halflines[] or group_ifthen[false] or group_other[] } group_clean[] or trim_graph[] or group_unclean[] end # the boxes have been almost put in place, here we soften a little the result & arrange some qwirks def auto_arrange_post # entrypoint should be above other boxes, same for exitpoints @box.each { |b| if b.from == [] chld = b.to chld = @box - [b] if not @box.find { |bb| bb != b and bb.from == [] } chld.each { |t| b.y = t.y - b.h - 16 if t.y < b.y+b.h } end if b.to == [] chld = b.from chld = @box - [b] if not @box.find { |bb| bb != b and bb.to == [] } chld.each { |f| b.y = f.y + f.h + 16 if f.y+f.h > b.y } end } boxxy = @box.sort_by { |bb| bb.y } # fill gaps that we created @box.each { |b| bottom = b.y+b.h next if not follower = boxxy.find { |bb| bb.y+bb.h > bottom } # preserve line[] constructs margins gap = follower.y-16*follower.from.length - (bottom+16*b.to.length) next if gap <= 0 @box.each { |bb| if bb.y+bb.h <= bottom bb.y += gap/2 else bb.y -= gap/2 end } boxxy = @box.sort_by { |bb| bb.y } } @box[0,0].each { |b| # TODO elastic positionning (ignore up arrows ?) & collision detection (box/box + box/arrow) f = b.from[0] t = b.to[0] if b.to.length == 1 and b.from.length == 1 and b.y+b.hf.y+f.h wx = (t.x+t.w/2 + f.x+f.w/2)/2 - b.w/2 wy = (t.y + f.y+f.h)/2 - b.h/2 b.x += (wx-b.x)/5 b.y += (wy-b.y)/5 end } end def auto_arrange_boxes auto_arrange_init nil while @groups.length > 1 and auto_arrange_step @groups = [] auto_arrange_post end end class GraphViewWidget < DrawableWidget attr_accessor :dasm, :caret_box, :curcontext, :zoom, :margin # bool, specifies if we should display addresses before instrs attr_accessor :show_addresses def initialize_widget(dasm, parent_widget) @dasm = dasm @parent_widget = parent_widget @show_addresses = false @caret_box = nil @selected_boxes = [] @shown_boxes = [] @mousemove_origin = @mousemove_origin_ctrl = nil @curcontext = Graph.new(nil) @margin = 8 @zoom = 1.0 @default_color_association = { :background => :paleblue, :hlbox_bg => :palegrey, :box_bg => :white, :text => :black, :arrow_hl => :red, :comment => :darkblue, :address => :darkblue, :instruction => :black, :label => :darkgreen, :caret => :black, :hl_word => :palered, :cursorline_bg => :paleyellow, :arrow_cond => :darkgreen, :arrow_uncond => :darkblue, :arrow_direct => :darkred } # @othergraphs = ? (to keep user-specified formatting) end def resized(w, h) redraw end def find_box_xy(x, y) x = @curcontext.view_x+x/@zoom y = @curcontext.view_y+y/@zoom @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 } end def mouse_wheel_ctrl(dir, x, y) case dir when :up if @zoom < 100 oldzoom = @zoom @zoom *= 1.1 @zoom = 1.0 if (@zoom-1.0).abs < 0.05 @curcontext.view_x += (x / oldzoom - x / @zoom) @curcontext.view_y += (y / oldzoom - y / @zoom) end when :down if @zoom > 1.0/100 oldzoom = @zoom @zoom /= 1.1 @zoom = 1.0 if (@zoom-1.0).abs < 0.05 @curcontext.view_x += (x / oldzoom - x / @zoom) @curcontext.view_y += (y / oldzoom - y / @zoom) end end redraw end def mouse_wheel(dir, x, y) case dir when :up; @curcontext.view_y -= height/4 / @zoom when :down; @curcontext.view_y += height/4 / @zoom end redraw end def mousemove(x, y) return if not @mousemove_origin dx = (x - @mousemove_origin[0])/@zoom dy = (y - @mousemove_origin[1])/@zoom @mousemove_origin = [x, y] if @selected_boxes.empty? @curcontext.view_x -= dx ; @curcontext.view_y -= dy else @selected_boxes.each { |b| b.x += dx ; b.y += dy } end redraw end def mouserelease(x, y) mousemove(x, y) @mousemove_origin = nil if @mousemove_origin_ctrl x1 = @curcontext.view_x + @mousemove_origin_ctrl[0]/@zoom x2 = x1 + (x - @mousemove_origin_ctrl[0])/@zoom x1, x2 = x2, x1 if x1 > x2 y1 = @curcontext.view_y + @mousemove_origin_ctrl[1]/@zoom y2 = y1 + (y - @mousemove_origin_ctrl[1])/@zoom y1, y2 = y2, y1 if y1 > y2 @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 } redraw @mousemove_origin_ctrl = nil end end def click_ctrl(x, y) if b = find_box_xy(x, y) if @selected_boxes.include? b @selected_boxes.delete b else @selected_boxes << b end redraw else @mousemove_origin_ctrl = [x, y] end end def click(x, y) @mousemove_origin = [x, y] if b = find_box_xy(x, y) @selected_boxes = [b] if not @selected_boxes.include? b @caret_box = b @caret_x = (@curcontext.view_x+x/@zoom-b.x-1).to_i / @font_width @caret_y = (@curcontext.view_y+y/@zoom-b.y-1).to_i / @font_height update_caret else @selected_boxes = [] @caret_box = nil end redraw end # if the target is a call to a subfunction, open a new window with the graph of this function (popup) def rightclick(x, y) if b = find_box_xy(x, y) and @zoom >= 0.90 and @zoom <= 1.1 click(x, y) @mousemove_origin = nil @parent_widget.clone_window(@hl_word, :graph) end end def doubleclick(x, y) if b = find_box_xy(x, y) @mousemove_origin = nil if @hl_word and @zoom >= 0.90 and @zoom <= 1.1 @parent_widget.focus_addr(@hl_word) else @parent_widget.focus_addr b[:addresses].first end elsif doubleclick_check_arrow(x, y) elsif @zoom == 1.0 zoom_all else @curcontext.view_x += (x/@zoom - x) @curcontext.view_y += (y/@zoom - y) @zoom = 1.0 end redraw end # check if the user clicked on the beginning/end of an arrow, if so focus on the other end def doubleclick_check_arrow(x, y) return if @margin*@zoom < 2 x = @curcontext.view_x+x/@zoom y = @curcontext.view_y+y/@zoom sx = nil if bt = @shown_boxes.to_a.reverse.find { |b| y >= b.y+b.h-1 and y <= b.y+b.h-1+@margin+2 and sx = b.x+b.w/2 - b.to.length/2 * @margin/2 and x >= sx-@margin/2 and x <= sx+b.to.length*@margin/2 # should be margin/4, but add a little comfort margin } idx = (x-sx+@margin/4).to_i / (@margin/2) idx = 0 if idx < 0 idx = bt.to.length-1 if idx >= bt.to.length if bt.to[idx] if @parent_widget @parent_widget.focus_addr bt.to[idx][:line_address][0] else focus_xy(bt.to[idx].x, bt.to[idx].y) end end true elsif bf = @shown_boxes.to_a.reverse.find { |b| y >= b.y-@margin-2 and y <= b.y and sx = b.x+b.w/2 - b.from.length/2 * @margin/2 and x >= sx-@margin/2 and x <= sx+b.from.length*@margin/2 } idx = (x-sx+@margin/4).to_i / (@margin/2) idx = 0 if idx < 0 idx = bf.from.length-1 if idx >= bf.from.length if bf.from[idx] if @parent_widget @parent_widget.focus_addr bf.from[idx][:line_address][-1] else focus_xy(bt.from[idx].x, bt.from[idx].y) end end true end end # update the zoom & view_xy to show the whole graph in the window def zoom_all minx = @curcontext.box.map { |b| b.x }.min.to_i - 10 miny = @curcontext.box.map { |b| b.y }.min.to_i - 10 maxx = @curcontext.box.map { |b| b.x + b.w }.max.to_i + 10 maxy = @curcontext.box.map { |b| b.y + b.h }.max.to_i + 10 @zoom = [width.to_f/(maxx-minx), height.to_f/(maxy-miny)].min @zoom = 1.0 if @zoom > 1.0 or (@zoom-1.0).abs < 0.1 @curcontext.view_x = minx + (maxx-minx-width/@zoom)/2 @curcontext.view_y = miny + (maxy-miny-height/@zoom)/2 redraw end def paint update_graph if @want_update_graph if @want_focus_addr and @curcontext.box.find { |b_| b_[:line_address].index(@want_focus_addr) } focus_addr(@want_focus_addr, false) @want_focus_addr = nil #zoom_all end @curcontext.box.each { |b| # reorder arrows so that endings do not overlap b.to = b.to.sort_by { |bt| bt.x+bt.w/2 } b.from = b.from.sort_by { |bt| bt.x+bt.w/2 } } # arrows drawn first to stay under the boxes # XXX precalc ? @curcontext.box.each { |b| b.to.each { |bt| paint_arrow(b, bt) } } @shown_boxes = [] w_w, w_h = width, height @curcontext.box.each { |b| 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 @shown_boxes << b paint_box(b) } end def set_color_arrow(b1, b2) if b1 == @caret_box or b2 == @caret_box draw_color :arrow_hl elsif b1.to.length == 1 draw_color :arrow_uncond elsif b1.direct_to == b2.id draw_color :arrow_direct else draw_color :arrow_cond end end def paint_arrow(b1, b2) x1, y1 = b1.x+b1.w/2-@curcontext.view_x, b1.y+b1.h-@curcontext.view_y x2, y2 = b2.x+b2.w/2-@curcontext.view_x, b2.y-1-@curcontext.view_y x1o, x2o = x1, x2 margin = @margin x1 += (-(b1.to.length-1)/2 + b1.to.index(b2)) * margin/2 x2 += (-(b2.from.length-1)/2 + b2.from.index(b1)) * margin/2 return if (y1+margin < 0 and y2 < 0) or (y1 > height/@zoom and y2-margin > height/@zoom) # just clip on y margin, x1, y1, x2, y2, b1w, b2w, x1o, x2o = [margin, x1, y1, x2, y2, b1.w, b2.w, x1o, x2o].map { |v| v*@zoom } # XXX gtk wraps coords around 0x8000 if x1.abs > 0x7000 ; y1 /= x1.abs/0x7000 ; x1 /= x1.abs/0x7000 ; end if y1.abs > 0x7000 ; x1 /= y1.abs/0x7000 ; y1 /= y1.abs/0x7000 ; end if x2.abs > 0x7000 ; y2 /= x2.abs/0x7000 ; x2 /= x2.abs/0x7000 ; end if y2.abs > 0x7000 ; x2 /= y2.abs/0x7000 ; y2 /= y2.abs/0x7000 ; end # straighten vertical arrows if possible if y2 > y1 and (x1-x2).abs <= margin if b1.to.length == 1 x1 = x2 elsif b2.from.length == 1 x2 = x1 end end set_color_arrow(b1, b2) if margin > 1 # draw arrow tip draw_line(x1, y1, x1, y1+margin) draw_line(x2, y2-margin+1, x2, y2) draw_line(x2-margin/2, y2-margin/2, x2, y2) draw_line(x2+margin/2, y2-margin/2, x2, y2) y1 += margin y2 -= margin-1 end if y2+margin >= y1-margin-1 # straight vertical down arrow draw_line(x1, y1, x2, y2) if x1 != y1 or x2 != y2 # else arrow up, need to sneak around boxes elsif x1o-b1w/2-margin >= x2o+b2w/2+margin # z draw_line(x1, y1, x1o-b1w/2-margin, y1) draw_line(x1o-b1w/2-margin, y1, x2o+b2w/2+margin, y2) draw_line(x2o+b2w/2+margin, y2, x2, y2) draw_line(x1, y1+1, x1o-b1w/2-margin, y1+1) # double draw_line(x1o-b1w/2-margin+1, y1, x2o+b2w/2+margin+1, y2) draw_line(x2o+b2w/2+margin, y2+1, x2, y2+1) elsif x1+b1w/2+margin <= x2-b2w/2-margin # invert z draw_line(x1, y1, x1o+b1w/2+margin, y1) draw_line(x1o+b1w/2+margin, y1, x2o-b2w/2-margin, y2) draw_line(x2o-b2w/2-margin, y2, x2, y2) draw_line(x1, y1+1, x1+b1w/2+margin, y1+1) # double draw_line(x1o+b1w/2+margin+1, y1, x2o-b2w/2-margin+1, y2) draw_line(x2o-b2w/2-margin, y2+1, x2, y2+1) else # turn around x = (x1 <= x2 ? [x1o-b1w/2-margin, x2o-b2w/2-margin].min : [x1o+b1w/2+margin, x2o+b2w/2+margin].max) draw_line(x1, y1, x, y1) draw_line(x, y1, x, y2) draw_line(x, y2, x2, y2) draw_line(x1, y1+1, x, y1+1) # double draw_line(x+1, y1, x+1, y2) draw_line(x, y2+1, x2, y2+1) end end def set_color_boxshadow(b) draw_color :black end def set_color_box(b) if @selected_boxes.include? b draw_color :hlbox_bg else draw_color :box_bg end end def paint_box(b) set_color_boxshadow(b) draw_rectangle((b.x-@curcontext.view_x+3)*@zoom, (b.y-@curcontext.view_y+4)*@zoom, b.w*@zoom, b.h*@zoom) set_color_box(b) draw_rectangle((b.x-@curcontext.view_x)*@zoom, (b.y-@curcontext.view_y+1)*@zoom, b.w*@zoom, b.h*@zoom) # current text position x = (b.x - @curcontext.view_x + 1)*@zoom y = (b.y - @curcontext.view_y + 1)*@zoom w_w = (b.x - @curcontext.view_x + b.w - @font_width)*@zoom w_h = (b.y - @curcontext.view_y + b.h - @font_height)*@zoom if @parent_widget and @parent_widget.bg_color_callback ly = 0 b[:line_address].each { |a| if c = @parent_widget.bg_color_callback[a] 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) end ly += 1 } end if @caret_box == b 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) end return if @zoom < 0.99 or @zoom > 1.1 # TODO dynamic font size ? # renders a string at current cursor position with a color # must not include newline render = lambda { |str, color| # function ends when we write under the bottom of the listing next if y >= w_h+2 or x >= w_w if @hl_word stmp = str pre_x = 0 while stmp =~ /^(.*?)(\b#{Regexp.escape @hl_word}\b)/ s1, s2 = $1, $2 pre_x += s1.length * @font_width hl_x = s2.length * @font_width draw_rectangle_color(:hl_word, x+pre_x, y, hl_x, @font_height*@zoom) pre_x += hl_x stmp = stmp[s1.length+s2.length..-1] end end draw_string_color(color, x, y, str) x += str.length * @font_width } b[:line_text_col].each { |list| list.each { |s, c| render[s, c] } x = (b.x - @curcontext.view_x + 1)*@zoom y += @font_height*@zoom } if b == @caret_box and focus? cx = (b.x - @curcontext.view_x + 1 + @caret_x*@font_width)*@zoom cy = (b.y - @curcontext.view_y + 1 + @caret_y*@font_height)*@zoom draw_line_color(:caret, cx, cy, cx, cy+(@font_height-1)*@zoom) end end def gui_update @want_update_graph = true redraw end # # rebuild the code flow graph from @curcontext.roots # recalc the boxes w/h # def update_graph @want_update_graph = false ctx = @curcontext boxcnt = ctx.box.length arrcnt = ctx.box.inject(0) { |s, b| s + b.to.length + b.from.length } ctx.clear build_ctx(ctx) ctx.auto_arrange_boxes return if ctx != @curcontext if boxcnt != ctx.box.length or arrcnt != ctx.box.inject(0) { |s, b| s + b.to.length + b.from.length } zoom_all elsif @caret_box # update @caret_box with a box at the same place bx = @caret_box.x + @caret_box.w/2 by = @caret_box.y + @caret_box.h/2 @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 } end end def load_dotfile(path) @want_update_graph = false @curcontext.clear boxes = {} new_box = lambda { |text| b = @curcontext.new_box(text, :line_text_col => [[[text, :text]]]) b.w = text.length * @font_width b.h = @font_height b } max = File.size(path) i = 0 File.open(path) { |fd| while l = fd.gets case l.strip when /^"?(\w+)"?\s*->\s*"?(\w+)"?;?$/ b1 = boxes[$1] ||= new_box[$1] b2 = boxes[$2] ||= new_box[$2] b1.to |= [b2] b2.from |= [b1] end $stderr.printf("%.02f\r" % (fd.pos*100.0/max)) if (i += 1) & 0xff == 0 end } p boxes.length redraw rescue Interrupt p boxes.length end # create the graph objects in ctx def build_ctx(ctx) # graph : block -> following blocks in same function block_rel = {} todo = ctx.root_addrs.dup done = [:default, Expression::Unknown] while a = todo.shift a = @dasm.normalize a next if done.include? a done << a next if not di = @dasm.di_at(a) if not di.block_head? block_rel[di.block.address] = [a] @dasm.split_block(a) end block_rel[a] = [] di.block.each_to_samefunc(@dasm) { |t| t = @dasm.normalize t next if not @dasm.di_at(t) todo << t block_rel[a] << t } block_rel[a].uniq! end # populate boxes addr2box = {} todo = ctx.root_addrs.dup todo.delete_if { |t| not @dasm.di_at(t) } # undefined func start done = [] while a = todo.shift next if done.include? a done << a if not ctx.keep_split.to_a.include?(a) and from = block_rel.keys.find_all { |ba| block_rel[ba].include? a } and from.length == 1 and block_rel[from.first].length == 1 and addr2box[from.first] and lst = @dasm.decoded[from.first].block.list.last and lst.next_addr == a and (not lst.opcode.props[:saveip] or lst.block.to_subfuncret) box = addr2box[from.first] else box = ctx.new_box a, :addresses => [], :line_text_col => [], :line_address => [] end @dasm.decoded[a].block.list.each { |di_| box[:addresses] << di_.address addr2box[di_.address] = box } todo.concat block_rel[a] end # link boxes ctx.box.each { |b| next if not di = @dasm.decoded[b[:addresses].last] a = di.block.address next if not block_rel[a] block_rel[a].each { |t| ctx.link_boxes(b.id, t) b.direct_to = t if t == di.next_addr } } # calc box dimensions/text ctx.box.each { |b| colstr = [] curaddr = nil line = 0 render = lambda { |str, col| colstr << [str, col] } nl = lambda { b[:line_address][line] = curaddr b[:line_text_col][line] = colstr colstr = [] line += 1 } b[:addresses].each { |addr| curaddr = addr if di = @dasm.di_at(curaddr) if di.block_head? # render dump_block_header, add a few colors b_header = '' ; @dasm.dump_block_header(di.block) { |l| b_header << l ; b_header << ?\n if b_header[-1] != ?\n } b_header.strip.each_line { |l| l.chomp! col = :comment col = :label if l[0, 2] != '//' and l[-1] == ?: render[l, col] nl[] } end render["#{Expression[curaddr]} ", :address] if @show_addresses render[di.instruction.to_s.ljust(di.comment ? 24 : 0), :instruction] render[' ; ' + di.comment.join(' ')[0, 64], :comment] if di.comment nl[] else # TODO real data display (dwords, xrefs, strings..) if label = @dasm.get_label_at(curaddr) render[label + ' ', :label] end s = @dasm.get_section_at(curaddr) render['db '+((s and s[0].data.length > s[0].ptr) ? Expression[s[0].read(1)[0]].to_s : '?'), :text] nl[] end } b.w = b[:line_text_col].map { |strc| strc.map { |s, c| s }.join.length }.max.to_i * @font_width + 2 b.w += 1 if b.w % 2 == 0 # ensure boxes have odd width -> vertical arrows are straight b.h = line * @font_height } end def keypress_ctrl(key) case key when ?F @parent_widget.inputbox('text to search in curview (regex)', :text => @hl_word) { |pat| re = /#{pat}/i list = [['addr', 'instr']] @curcontext.box.each { |b| b[:line_text_col].zip(b[:line_address]) { |l, a| str = l.map { |s, c| s }.join list << [Expression[a], str] if str =~ re } } @parent_widget.list_bghilight("search result for /#{pat}/i", list) { |i| @parent_widget.focus_addr i[0] } } else return false end true end def keypress(key) case key when :left if @caret_box if @caret_x > 0 @caret_x -= 1 update_caret elsif b = @curcontext.box.sort_by { |b_| -b_.x }.find { |b_| b_.x < @caret_box.x and b_.y < @caret_box.y+@caret_y*@font_height and b_.y+b_.h > @caret_box.y+(@caret_y+1)*@font_height } @caret_x = (b.w/@font_width).to_i @caret_y += ((@caret_box.y-b.y)/@font_height).to_i @caret_box = b update_caret redraw else @curcontext.view_x -= 20/@zoom redraw end else @curcontext.view_x -= 20/@zoom redraw end when :up if @caret_box if @caret_y > 0 @caret_y -= 1 update_caret elsif b = @curcontext.box.sort_by { |b_| -b_.y }.find { |b_| b_.y < @caret_box.y and b_.x < @caret_box.x+@caret_x*@font_width and b_.x+b_.w > @caret_box.x+(@caret_x+1)*@font_width } @caret_x += ((@caret_box.x-b.x)/@font_width).to_i @caret_y = b[:line_address].length-1 @caret_box = b update_caret redraw else @curcontext.view_y -= 20/@zoom redraw end else @curcontext.view_y -= 20/@zoom redraw end when :right if @caret_box if @caret_x <= @caret_box[:line_text_col].map { |s| s.map { |ss, cc| ss }.join.length }.max @caret_x += 1 update_caret elsif b = @curcontext.box.sort_by { |b_| b_.x }.find { |b_| b_.x > @caret_box.x and b_.y < @caret_box.y+@caret_y*@font_height and b_.y+b_.h > @caret_box.y+(@caret_y+1)*@font_height } @caret_x = 0 @caret_y += ((@caret_box.y-b.y)/@font_height).to_i @caret_box = b update_caret redraw else @curcontext.view_x += 20/@zoom redraw end else @curcontext.view_x += 20/@zoom redraw end when :down if @caret_box if @caret_y < @caret_box[:line_address].length-1 @caret_y += 1 update_caret elsif b = @curcontext.box.sort_by { |b_| b_.y }.find { |b_| b_.y > @caret_box.y and b_.x < @caret_box.x+@caret_x*@font_width and b_.x+b_.w > @caret_box.x+(@caret_x+1)*@font_width } @caret_x += ((@caret_box.x-b.x)/@font_width).to_i @caret_y = 0 @caret_box = b update_caret redraw else @curcontext.view_y += 20/@zoom redraw end else @curcontext.view_y += 20/@zoom redraw end when :pgup if @caret_box @caret_y = 0 update_caret else @curcontext.view_y -= height/4/@zoom redraw end when :pgdown if @caret_box @caret_y = @caret_box[:line_address].length-1 update_caret else @curcontext.view_y += height/4/@zoom redraw end when :home if @caret_box @caret_x = 0 update_caret else @curcontext.view_x = @curcontext.box.map { |b_| b_.x }.min-10 @curcontext.view_y = @curcontext.box.map { |b_| b_.y }.min-10 redraw end when :end if @caret_box @caret_x = @caret_box[:line_text_col][@caret_y].to_a.map { |ss, cc| ss }.join.length update_caret else @curcontext.view_x = [@curcontext.box.map { |b_| b_.x+b_.w }.max-width/@zoom+10, @curcontext.box.map { |b_| b_.x }.min-10].max @curcontext.view_y = [@curcontext.box.map { |b_| b_.y+b_.h }.max-height/@zoom+10, @curcontext.box.map { |b_| b_.y }.min-10].max redraw end when :delete @selected_boxes.each { |b_| @curcontext.box.delete b_ b_.from.each { |bb| bb.to.delete b_ } b_.to.each { |bb| bb.from.delete b_ } } redraw when ?a puts 'autoarrange' @curcontext.auto_arrange_boxes redraw puts 'autoarrange done' when ?u gui_update when ?R load __FILE__ when ?S # reset @curcontext.auto_arrange_init(@selected_boxes.empty? ? @curcontext.box : @selected_boxes) puts 'reset', @curcontext.dump_layout, '' zoom_all redraw when ?T # step auto_arrange @curcontext.auto_arrange_step puts @curcontext.dump_layout, '' zoom_all redraw when ?L # post auto_arrange @curcontext.auto_arrange_post zoom_all redraw when ?V # shrink @selected_boxes.each { |b_| dx = (b_.from + b_.to).map { |bb| bb.x+bb.w/2 - b_.x-b_.w/2 } dx = dx.inject(0) { |s, xx| s+xx }/dx.length b_.x += dx } redraw when ?I # create arbitrary boxes/links if @selected_boxes.empty? @fakebox ||= 0 b = @curcontext.new_box "id_#@fakebox", :addresses => [], :line_address => [], :line_text_col => [[[" blublu #@fakebox", :text]]] b.w = @font_width * 15 b.h = @font_height * 2 b.x = rand(200) - 100 b.y = rand(200) - 100 @fakebox += 1 else b1, *bl = @selected_boxes bl = [b1] if bl.empty? # loop bl.each { |b2| if b1.to.include? b2 b1.to.delete b2 b2.from.delete b1 else b1.to << b2 b2.from << b1 end } end redraw when ?1 # (numeric) zoom to 1:1 if @zoom == 1.0 zoom_all else @curcontext.view_x += (width/2 / @zoom - width/2) @curcontext.view_y += (height/2 / @zoom - height/2) @zoom = 1.0 end redraw when :insert # split curbox at @caret_y if @caret_box and a = @caret_box[:line_address][@caret_y] and @dasm.decoded[a] @dasm.split_block(a) @curcontext.keep_split ||= [] @curcontext.keep_split |= [a] gui_update focus_addr a end else return false end true end # find a suitable array of graph roots, walking up from a block (function start/entrypoint) def dasm_find_roots(addr) todo = [addr] done = [] roots = [] default_root = nil while a = todo.shift next if not di = @dasm.di_at(a) b = di.block a = b.address if done.include? a default_root ||= a next end done << a newf = [] b.each_from_samefunc(@dasm) { |f| newf << f } if newf.empty? roots << b.address else todo.concat newf end end roots << default_root if roots.empty? and default_root roots end def set_cursor_pos(p) addr, x = p focus_addr(addr) @caret_x = x update_caret end def get_cursor_pos [current_address, @caret_x] end # focus on addr # addr may be a dasm label, dasm address, dasm address in string form (eg "0DEADBEEFh") # addr must point to a decodedinstruction # if the addr is not found in curcontext, the code flow is walked up until a function # start or an entrypoint is found, then the graph is created from there # will call gui_update then def focus_addr(addr, can_update_context=true) return if @parent_widget and not addr = @parent_widget.normalize(addr) return if not @dasm.di_at(addr) # move window / change curcontext if b = @curcontext.box.find { |b_| b_[:line_address].index(addr) } @caret_box, @caret_x, @caret_y = b, 0, b[:line_address].rindex(addr) @curcontext.view_x += (width/2 / @zoom - width/2) @curcontext.view_y += (height/2 / @zoom - height/2) @zoom = 1.0 focus_xy(b.x, b.y + @caret_y*@font_height) update_caret elsif can_update_context @curcontext = Graph.new 'testic' @curcontext.root_addrs = dasm_find_roots(addr) @want_focus_addr = addr gui_update else return end true end def focus_xy(x, y) if not @curcontext.view_x or @curcontext.view_x*@zoom + width*3/4 < x or @curcontext.view_x*@zoom > x @curcontext.view_x = (x - width/5)/@zoom redraw end if not @curcontext.view_y or @curcontext.view_y*@zoom + height*3/4 < y or @curcontext.view_y*@zoom > y @curcontext.view_y = (y - height/5)/@zoom redraw end end # hint that the caret moved # redraw, change the hilighted word def update_caret return if not @caret_box or not @caret_x or not l = @caret_box[:line_text_col][@caret_y] l = l.map { |s, c| s }.join @parent_widget.focus_changed_callback[] if @parent_widget and @parent_widget.focus_changed_callback and @oldcaret_y != @caret_y update_hl_word(l, @caret_x) redraw end def current_address @caret_box ? @caret_box[:line_address][@caret_y] : @curcontext.root_addrs.first end end end end