|
| 1 | +#!/usr/bin/env python |
| 2 | +# |
| 3 | +# display live code coverage of DACMAN Colecovision game running in MAME |
| 4 | +# drawn as Hilbert curve |
| 5 | +# |
| 6 | +# $ mame -v coleco -video soft -cart /path/to/DACMAN.ROM -window -nomax -resolution 560x432 -debugger gdbstub -debug |
| 7 | +# $ ./dacman_live_hilbert /path/to/DACMAN.ROM |
| 8 | +# |
| 9 | + |
| 10 | +import os |
| 11 | +import sys |
| 12 | +import math |
| 13 | +import time |
| 14 | +import struct |
| 15 | +import platform |
| 16 | +from collections import defaultdict |
| 17 | + |
| 18 | +import binaryninja |
| 19 | +from binaryninja.binaryview import BinaryViewType |
| 20 | + |
| 21 | +from PIL import Image, ImageDraw |
| 22 | + |
| 23 | +# globals |
| 24 | +n = None |
| 25 | +draw = None |
| 26 | + |
| 27 | +#------------------------------------------------------------------------------ |
| 28 | +# Hilbert curve mapping algorithms from: |
| 29 | +# https://en.wikipedia.org/wiki/Hilbert_curve |
| 30 | +#------------------------------------------------------------------------------ |
| 31 | + |
| 32 | +def rot(n, x, y, rx, ry): |
| 33 | + if ry == 0: |
| 34 | + if rx == 1: |
| 35 | + x = n-1 - x; |
| 36 | + y = n-1 - y; |
| 37 | + |
| 38 | + (y,x) = (x,y) |
| 39 | + |
| 40 | + return (x,y) |
| 41 | + |
| 42 | +def d2xy(n, d): |
| 43 | + (x,y,t) = (0,0,d) |
| 44 | + |
| 45 | + level = 1 |
| 46 | + while level<n: |
| 47 | + rx = 1 & (t//2) |
| 48 | + ry = 1 & (t ^ rx) |
| 49 | + (x, y) = rot(level, x, y, rx, ry) |
| 50 | + x += level * rx |
| 51 | + y += level * ry |
| 52 | + t //= 4 |
| 53 | + level *= 2 |
| 54 | + |
| 55 | + return (x,y) |
| 56 | + |
| 57 | +def xy2d(n, x, y): |
| 58 | + (rx,ry,s,d)=(0,0,0,0) |
| 59 | + |
| 60 | + s = n//2 |
| 61 | + while s > 0: |
| 62 | + rx = int((x & s) > 0) |
| 63 | + ry = int((y & s) > 0) |
| 64 | + d += s * s * ((3 * rx) ^ ry) |
| 65 | + (x, y) = rot(n, x, y, rx, ry) |
| 66 | + s //= 2 |
| 67 | + |
| 68 | + return d |
| 69 | + |
| 70 | +#------------------------------------------------------------------------------ |
| 71 | +# Hilbert curve drawing helpers |
| 72 | +#------------------------------------------------------------------------------ |
| 73 | + |
| 74 | +# trace a Hilbert region by "wall following" |
| 75 | +def wall_follower(d0, d1): |
| 76 | + global n |
| 77 | + |
| 78 | + def ok(x, y): |
| 79 | + if x<0 or y<0: return False |
| 80 | + d = xy2d(n**2, x, y) |
| 81 | + #print('is %d within %d,%d' % (d, d0, d1)) |
| 82 | + return d>=0 and d>=d0 and d<d1 |
| 83 | + |
| 84 | + # move left until stop |
| 85 | + (x,y) = d2xy(n**2, d0) |
| 86 | + while 1: |
| 87 | + if x == 0: break |
| 88 | + if not ok(x-1,y): break |
| 89 | + x = x-1 |
| 90 | + |
| 91 | + start = (x,y) |
| 92 | + trace = [start] |
| 93 | + direction = 'down' |
| 94 | + |
| 95 | + tendencies = ['right', 'down', 'left', 'up'] |
| 96 | + |
| 97 | + while 1: |
| 98 | + #print('at (%d,%d) heading %s' % (x,y,direction)) |
| 99 | + |
| 100 | + tendency = tendencies[(tendencies.index(direction)+1) % 4] |
| 101 | + |
| 102 | + xmod = {'right':1, 'down':0, 'left':-1, 'up':0} |
| 103 | + ymod = {'right':0, 'down':-1, 'left':0, 'up':1} |
| 104 | + |
| 105 | + moved = False |
| 106 | + |
| 107 | + # case A: we can turn right |
| 108 | + x_try = x+xmod[tendency] |
| 109 | + y_try = y+ymod[tendency] |
| 110 | + if ok(x_try, y_try): |
| 111 | + direction = tendency |
| 112 | + (x,y) = (x_try, y_try) |
| 113 | + moved = True |
| 114 | + else: |
| 115 | + # case B: we can continue in current direction |
| 116 | + x_try = x+xmod[direction] |
| 117 | + y_try = y+ymod[direction] |
| 118 | + if ok(x_try, y_try): |
| 119 | + (x,y) = (x_try, y_try) |
| 120 | + moved = True |
| 121 | + else: |
| 122 | + # case C: we can't continue! ah! |
| 123 | + direction = tendencies[(tendencies.index(direction)-1)%4] |
| 124 | + |
| 125 | + if moved: |
| 126 | + trace.append((x,y)) |
| 127 | + |
| 128 | + if (x,y) == start: |
| 129 | + break |
| 130 | + |
| 131 | + return trace |
| 132 | + |
| 133 | +# [start, stop) |
| 134 | +def draw_hilbert(start, stop, color='#ffffff'): |
| 135 | + global n |
| 136 | + global draw |
| 137 | + |
| 138 | + pts = [d2xy(n, x) for x in range(start, stop)] |
| 139 | + lines = zip(pts[:-1], pts[1:]) |
| 140 | + for line in lines: |
| 141 | + ((x1,y1),(x2,y2)) = line |
| 142 | + #print('drawing line (%d,%d) -> (%d,%d)' % (x1,y1,x2,y2)) |
| 143 | + draw.line((x1,y1,x2,y2), width=1, fill=color) |
| 144 | + |
| 145 | +def draw_region(start, stop, color1='#00ff00', color2=None): |
| 146 | + global draw |
| 147 | + trace = wall_follower(start, stop) |
| 148 | + draw.polygon(trace, outline=color1, fill=color2) |
| 149 | + |
| 150 | +#------------------------------------------------------------------------------ |
| 151 | +# main() |
| 152 | +#------------------------------------------------------------------------------ |
| 153 | + |
| 154 | +if __name__ == '__main__': |
| 155 | + # analyze functions |
| 156 | + fpath = sys.argv[1] |
| 157 | + bv = BinaryViewType.get_view_of_file(fpath) |
| 158 | + bv.update_analysis_and_wait() |
| 159 | + lowest = None |
| 160 | + highest = None |
| 161 | + addr2func = {} |
| 162 | + for f in bv.functions: |
| 163 | + addr_start = f.start |
| 164 | + addr_end = f.start + f.total_bytes |
| 165 | + |
| 166 | + if lowest==None or addr_start < lowest: |
| 167 | + lowest = addr_start |
| 168 | + if highest==None or addr_end >= highest: |
| 169 | + highest = addr_end |
| 170 | + |
| 171 | + addr2func[addr_start] = f |
| 172 | + |
| 173 | + print('lowest address: 0x%04X' % lowest) |
| 174 | + print('highest address: 0x%04X' % highest) |
| 175 | + |
| 176 | + # launch debugger, set breakpoints |
| 177 | + from debugger import DebugAdapter, gdblike |
| 178 | + adapter = gdblike.connect_sense('localhost', 23946) |
| 179 | + for addr in addr2func: |
| 180 | + print('setting breakpoint at %04X: %s' % (addr, addr2func[addr].symbol.full_name)) |
| 181 | + adapter.breakpoint_set(addr) |
| 182 | + |
| 183 | + # calculate image size |
| 184 | + pixels = 1 |
| 185 | + while pixels < (highest-lowest): |
| 186 | + pixels *= 4 |
| 187 | + n = int(math.sqrt(pixels)) |
| 188 | + print('n:', n) |
| 189 | + img = Image.new('RGB', (n,n)) |
| 190 | + draw = ImageDraw.Draw(img) |
| 191 | + |
| 192 | + # intialize pygame |
| 193 | + import pygame |
| 194 | + from pygame.locals import * |
| 195 | + pygame.init() |
| 196 | + surface = pygame.display.set_mode((4*n, 4*n), RESIZABLE) |
| 197 | + pygame.display.set_caption('DACMAN code coverage') |
| 198 | + |
| 199 | + # palette is "tab20" from matplotlib |
| 200 | + palette_i = 0 |
| 201 | + palette = [ |
| 202 | + '#1F77B4', '#AEC7E8', '#FF7F0E', '#FFBB78', '#2CA02C', '#98DF8A', '#D62728', '#FF9896', |
| 203 | + '#9467BD', '#C5B0D5', '#8C564B', '#C49C94', '#E377C2', '#F7B6D2', '#7F7F7F', '#C7C7C7', |
| 204 | + '#BCBD22', '#DBDB8D', '#17BECF', '#9EDAE5' |
| 205 | + ] |
| 206 | + |
| 207 | + print('reading to rock, press any key!') |
| 208 | + input() |
| 209 | + |
| 210 | + while 1: |
| 211 | + # process pygame events |
| 212 | + for event in pygame.event.get(): |
| 213 | + if event.type == QUIT: |
| 214 | + pygame.quit() |
| 215 | + sys.exit() |
| 216 | + |
| 217 | + # wait for breakpoint, clear it |
| 218 | + (reason, data) = adapter.go() |
| 219 | + assert reason in [DebugAdapter.STOP_REASON.BREAKPOINT, DebugAdapter.STOP_REASON.SINGLE_STEP] |
| 220 | + pc = adapter.reg_read('pc') |
| 221 | + f = addr2func[pc] |
| 222 | + print('%s()' % f.symbol.full_name) |
| 223 | + adapter.breakpoint_clear(pc) |
| 224 | + |
| 225 | + # draw function |
| 226 | + addr_start = f.start |
| 227 | + addr_end = f.start + f.total_bytes |
| 228 | + if addr_end - addr_start < 4: |
| 229 | + continue |
| 230 | + print('drawing %s [0x%04X, 0x%04X)' % (f.symbol.full_name, addr_start, addr_end)) |
| 231 | + draw_region(addr_start - lowest, addr_end - lowest, None, palette[palette_i]) |
| 232 | + palette_i = (palette_i+1) % len(palette) |
| 233 | + |
| 234 | + # drawing to pygame |
| 235 | + raw_str = img.tobytes('raw', 'RGB') |
| 236 | + img_surface = pygame.image.fromstring(raw_str, (n, n), 'RGB') |
| 237 | + img_surface = pygame.transform.scale(img_surface, (4*n, 4*n)) |
| 238 | + surface.blit(img_surface, (0,0)) |
| 239 | + pygame.display.update() |
| 240 | + |
| 241 | + #time.sleep(.1) |
0 commit comments