plugins/cpu/any_capstone.py: Rename to _any_capstone.py to avoid confusion.
[ScratchABit.git] / ScratchABit.py
blob1ba0218ae8a3e296915770b73745c4651e572d86
1 #!/usr/bin/env python3
2 # ScratchABit - interactive disassembler
4 # Copyright (c) 2015 Paul Sokolovsky
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 import sys
19 import os
20 import os.path
21 import time
22 import re
23 import string
24 import binascii
25 import logging as log
26 import argparse
28 from scratchabit import engine
29 import idaapi
31 from picotui.widgets import *
32 from picotui import editorext as editor
33 from picotui.screen import Screen
34 from picotui.editorext import Viewer
35 from picotui.menu import *
36 from picotui.dialogs import *
38 from scratchabit import utils
39 from scratchabit import help
40 from scratchabit import saveload
41 from scratchabit import actions
42 from scratchabit import uiprefs
45 HEIGHT = 21
47 MENU_PREFS = 2000
48 MENU_PLUGIN = 2001
49 MENU_ADD_TO_FUNC = 2002
50 MENU_WRITE_ALL_HTML = 2003
53 class AppClass:
55 def set_show_bytes(self, show_bytes):
56 self.show_bytes = show_bytes
57 sz = 8 + 1
58 if APP.show_bytes:
59 sz += show_bytes * 2 + 1
60 engine.DisasmObj.LEADER_SIZE = sz
63 APP = AppClass()
66 def disasm_one(p):
67 insnsz = p.ana()
68 p.out()
69 print("%08x %s" % (p.cmd.ea, p.cmd.disasm))
70 p.cmd.ea += p.cmd.size
71 p.cmd.size = 0
74 class DisasmViewer(editor.EditorExt):
76 def __init__(self, *args):
77 super().__init__(*args)
78 self.model = None
79 self.addr_stack = []
80 self.search_str = ""
81 self.def_color = C_PAIR(C_CYAN, C_BLUE)
83 def set_model(self, model):
84 self.model = model
85 self.set_lines(model.lines())
86 # Invalidate top_line. Assuming goto_*() will be called
87 # after set_model().
88 self.top_line = sys.maxsize
90 def show_line(self, l, i):
91 show_bytes = APP.show_bytes
92 res = l
93 if not isinstance(l, str):
94 res = "%08x " % l.ea
95 if show_bytes > 0:
96 bin = ""
97 if not l.virtual:
98 b = self.model.AS.get_bytes(l.ea, l.size)
99 bin = str(binascii.hexlify(b[:show_bytes]), "ascii")
100 if l.size > show_bytes:
101 bin += "+"
102 res += idaapi.fillstr(bin, show_bytes * 2 + 1)
103 res += l.indent + l.render()
105 COLOR_MAP = {
106 engine.Label: C_PAIR(C_GREEN, C_BLUE),
107 engine.AreaWrapper: C_PAIR(C_YELLOW, C_BLUE),
108 engine.FunctionWrapper: C_PAIR(C_B_YELLOW, C_BLUE),
109 engine.Xref: C_PAIR(C_MAGENTA, C_BLUE),
110 engine.Unknown: C_PAIR(C_WHITE, C_BLUE),
111 engine.Data: C_PAIR(C_MAGENTA, C_BLUE),
112 engine.String: C_PAIR(C_B_MAGENTA, C_BLUE),
113 engine.Fill: C_PAIR(C_B_BLUE, C_BLUE),
115 c = COLOR_MAP.get(type(l), self.def_color)
116 self.attr_color(c)
117 super().show_line(res, i)
118 self.attr_reset()
121 def handle_input(self, key):
122 try:
123 return super().handle_input(key)
124 except Exception as ex:
125 self.show_exception(ex)
126 return None
129 def goto_addr(self, to_addr, col=None, from_addr=None):
130 if to_addr is None:
131 self.show_status("No address-like value to go to")
132 return
133 subno = -1
134 if isinstance(to_addr, tuple):
135 to_addr, subno = to_addr
136 adj_addr = self.model.AS.adjust_addr_reverse(to_addr)
137 if adj_addr is None:
138 self.show_status("Unknown address: 0x%x" % to_addr)
139 return
140 to_addr = adj_addr
142 # If we can position cursor within current screen, do that,
143 # to avoid jumpy UI
144 no = self.model.addr2line_no(to_addr, subno)
145 if no is not None:
146 if self.line_visible(no):
147 self.goto_line(no, col=col)
148 if from_addr is not None:
149 self.addr_stack.append(from_addr)
150 return
152 # Otherwise, re-render model around needed address, and redraw screen
153 t = time.time()
154 model = engine.render_partial_around(to_addr, 0, HEIGHT * 2)
155 self.show_status("Rendering time: %fs" % (time.time() - t))
156 if not model:
157 self.show_status("Unknown address: 0x%x" % to_addr)
158 return
159 self.set_model(model)
161 no = self.model.addr2line_no(to_addr, subno)
162 if no is not None:
163 if from_addr is not None:
164 self.addr_stack.append(from_addr)
165 if not self.goto_line(no, col=col):
166 # Need to redraw always, because we changed underlying model
167 self.redraw()
168 else:
169 self.show_status("Unknown address: %x" % to_addr)
171 def update_model(self, stay_on_real=False):
172 """Re-render model and update screen in such way that cursor stayed
173 on the same line (as far as possible).
174 stay_on_real == False - try to stay on same relative line no. for
175 the current address.
176 stay_on_real == True - try to stay on the line which contains real
177 bytes for the current address (use this if you know that cursor
178 stayed on such line before the update).
180 addr, subno = self.cur_addr_subno()
181 t = time.time()
182 model = engine.render_partial_around(addr, subno, HEIGHT * 2)
183 self.show_status("Rendering time: %fs" % (time.time() - t))
184 self.set_model(model)
185 if stay_on_real:
186 self.cur_line = model.target_addr_lineno_real
187 else:
188 self.cur_line = model.target_addr_lineno
189 self.top_line = self.cur_line - self.row
190 #log.debug("update_model: addr=%x, row=%d, cur_line=%d, top_line=%d" % (addr, self.row, self.cur_line, self.top_line))
191 self.redraw()
193 def handle_cursor_keys(self, key):
194 cl = self.cur_line
195 if super().handle_cursor_keys(key):
196 if self.cur_line == cl:
197 return True
198 #log.debug("handle_cursor_keys: cur: %d, total: %d", self.cur_line, self.total_lines)
199 if self.cur_line <= HEIGHT or self.total_lines - self.cur_line <= HEIGHT:
200 log.debug("handle_cursor_keys: triggering update")
201 self.update_model()
203 return True
204 else:
205 return False
207 def cur_addr(self):
208 line = self.get_cur_line()
209 return line.ea
211 # Address of the next line. It may be the same address as the
212 # current line, as several lines may "belong" to the same address,
213 # (virtual lines like headers, etc.)
214 def next_line_addr_subno(self):
215 try:
216 l = self.content[self.cur_line + 1]
217 return (l.ea, l.subno)
218 except:
219 return None
221 # Return next address following the current line. May need to skip
222 # few virtual lines.
223 def next_addr(self):
224 addr = self.cur_addr()
225 n = self.cur_line + 1
226 try:
227 while self.content[n].ea == addr:
228 n += 1
229 return self.content[n].ea
230 except:
231 return None
233 def cur_addr_subno(self):
234 line = self.get_cur_line()
235 return (line.ea, line.subno)
237 def cur_operand_no(self, line):
238 col = self.col - engine.DisasmObj.LEADER_SIZE - len(line.indent)
239 #self.show_status("Enter pressed: %s, %s" % (col, line))
240 for i, pos in enumerate(line.arg_pos):
241 if pos[0] <= col <= pos[1]:
242 return i
243 return -1
245 def analyze_status(self, cnt):
246 self.show_status("Analyzing (%d insts so far)" % cnt)
248 def expect_flags(self, fl, allowed_flags):
249 if fl not in allowed_flags:
250 self.show_status("Undefine first (u key)")
251 return False
252 return True
255 def show_exception(self, e):
256 log.exception("Exception processing user command")
257 L = 5
258 T = 2
259 W = 70
260 H = 20
261 self.dialog_box(L, T, W, H)
262 v = Viewer(L + 1, T + 1, W - 2, H - 2)
263 import traceback
264 v.set_lines([
265 "Exception occurred processing the command. Press Esc to continue.",
266 "Recommended action is saving database, quitting and comparing",
267 "database files with backup copies for possibility of data loss",
268 "or corruption. The exception was also logged to scratchabit.log.",
269 "Please report way to reproduce it to",
270 "https://github.com/pfalcon/ScratchABit/issues",
272 ] + traceback.format_exc().splitlines())
273 v.loop()
274 self.redraw()
277 def resolve_expr(self, expr):
278 if expr:
279 if expr[0].isdigit():
280 return int(expr, 0)
281 else:
282 words = expr.split("+", 1)
283 addend = 0
284 if len(words) > 1:
285 try:
286 addend = int(words[1], 0)
287 except:
288 pass
289 to_addr = self.model.AS.resolve_label(words[0])
290 if to_addr is None:
291 return
292 return to_addr + addend
295 def require_non_func(self, fl):
296 if fl & ~(self.model.AS.FUNC | self.model.AS.ALT_CODE) != self.model.AS.CODE:
297 self.show_status("Code required")
298 return False
299 if fl & self.model.AS.FUNC:
300 self.show_status("Already a function")
301 return False
302 return True
305 # UI action handlers
308 def action_goto(self):
309 d = Dialog(4, 4, title="Go to")
310 d.add(1, 1, WLabel("Label/addr:"))
311 entry = WAutoComplete(20, "", self.model.AS.get_label_list())
312 entry.popup_h = 12
313 entry.finish_dialog = ACTION_OK
314 d.add(13, 1, entry)
315 d.add(1, 2, WLabel("Press Down to auto-complete"))
316 res = d.loop()
317 self.redraw()
319 if res == ACTION_OK:
320 value = entry.get_text()
321 if '0' <= value[0] <= '9':
322 addr = int(value, 0)
323 else:
324 addr = self.model.AS.resolve_label(value)
325 self.goto_addr(addr, from_addr=self.cur_addr())
328 def action_make_ascii(self):
329 addr = self.cur_addr()
330 fl = self.model.AS.get_flags(addr)
331 if not self.expect_flags(fl, (self.model.AS.DATA, self.model.AS.UNK)):
332 return
333 sz = 0
334 label = "s_"
335 while True:
336 b = self.model.AS.get_byte(addr)
337 fl = self.model.AS.get_flags(addr)
338 if not (0x20 <= b <= 0x7e or b in (0x0a, 0x0d)):
339 if b == 0:
340 sz += 1
341 break
342 if fl not in (self.model.AS.UNK, self.model.AS.DATA, self.model.AS.DATA_CONT):
343 break
344 c = chr(b)
345 if c < '0' or c in string.punctuation:
346 c = '_'
347 label += c
348 addr += 1
349 sz += 1
350 if sz > 0:
351 self.model.AS.set_flags(self.cur_addr(), sz, self.model.AS.STR, self.model.AS.DATA_CONT)
352 self.model.AS.make_unique_label(self.cur_addr(), label)
353 self.update_model()
356 def handle_edit_key(self, key):
357 if key in ACTION_MAP:
358 return ACTION_MAP[key](self)
360 line = self.get_cur_line()
361 if key == editor.KEY_ENTER:
362 line = self.get_cur_line()
363 log.info("Enter pressed: %s" % line)
364 op_no = self.cur_operand_no(line)
365 self.show_status("Enter pressed: %s, %s" % (self.col, op_no))
366 to_addr = None
367 # No longer try to jump only to addresses in args, parse
368 # textual representation below
369 if False and isinstance(line, engine.DisasmObj):
370 if op_no >= 0:
371 o = line[op_no]
372 to_addr = o.get_addr()
373 if to_addr is None:
374 o = line.get_operand_addr()
375 if o:
376 to_addr = o.get_addr()
377 if to_addr is None:
378 pos = self.col - line.LEADER_SIZE - len(line.indent)
379 word = utils.get_word_at_pos(line.cache, pos)
380 self.show_status("Enter pressed: %s, %s, %s" % (self.col, op_no, word))
381 to_addr = self.resolve_expr(word)
382 if to_addr is None:
383 self.show_status("Unknown address: %s" % word)
384 return
385 self.goto_addr(to_addr, from_addr=self.cur_addr_subno())
386 elif key == editor.KEY_ESC:
387 if self.addr_stack:
388 self.show_status("Returning")
389 self.goto_addr(self.addr_stack.pop())
390 elif key == b"q":
391 res = ACTION_OK
392 if self.model.AS.changed:
393 res = DConfirmation("There're unsaved changes. Quit?").result()
394 if res == ACTION_OK:
395 return editor.KEY_QUIT
396 self.redraw()
397 elif key == b"\x1b[5;5~": # Ctrl+PgUp
398 self.goto_addr(self.model.AS.min_addr(), from_addr=line.ea)
399 elif key == b"\x1b[6;5~": # Ctrl+PgDn
400 self.goto_addr(self.model.AS.max_addr(), from_addr=line.ea)
401 elif key == b"c":
402 addr = self.cur_addr()
403 self.show_status("Analyzing at %x" % addr)
404 engine.add_entrypoint(addr, False)
405 engine.analyze(self.analyze_status)
406 self.update_model()
408 elif key == b"C":
409 addr = self.cur_addr()
410 self.show_status("Analyzing at %x" % addr)
411 self.model.AS.make_alt_code(addr)
412 engine.add_entrypoint(addr, False)
413 engine.analyze(self.analyze_status)
414 self.update_model()
416 elif key == b"F":
417 addr = self.cur_addr()
418 fl = self.model.AS.get_flags(addr, 0xff)
419 if not self.require_non_func(fl):
420 return
421 self.show_status("Retracing as a function...")
422 self.model.AS.make_label("fun_", addr)
423 engine.add_entrypoint(addr, True)
424 engine.analyze(self.analyze_status)
425 self.update_model()
426 self.show_status("Retraced as a function")
428 elif key == MENU_ADD_TO_FUNC:
429 addr = self.cur_addr()
430 if actions.add_code_to_func(APP, addr):
431 self.update_model()
433 elif key == b"d":
434 addr = self.cur_addr()
435 fl = self.model.AS.get_flags(addr)
436 if not self.expect_flags(fl, (self.model.AS.DATA, self.model.AS.UNK)):
437 return
438 if fl == self.model.AS.UNK:
439 self.model.AS.set_flags(addr, 1, self.model.AS.DATA, self.model.AS.DATA_CONT)
440 else:
441 sz = self.model.AS.get_unit_size(addr)
442 self.model.undefine_unit(addr)
443 sz *= 2
444 if sz > 4: sz = 1
445 self.model.AS.set_flags(addr, sz, self.model.AS.DATA, self.model.AS.DATA_CONT)
446 self.update_model()
447 elif key == b"f":
448 addr = self.cur_addr()
449 fl = self.model.AS.get_flags(addr)
450 if not self.expect_flags(fl, (self.model.AS.UNK,)):
451 return
453 off, area = self.model.AS.addr2area(self.cur_addr())
454 # Don't cross area boundaries with filler
455 remaining = area[engine.END] - addr + 1
456 sz = 0
457 while remaining:
458 try:
459 fl = self.model.AS.get_flags(addr)
460 except engine.InvalidAddrException:
461 break
462 if fl != self.model.AS.UNK:
463 break
464 b = self.model.AS.get_byte(addr)
465 if b not in (0, 0xff):
466 self.show_status("Filler must consist of 0x00 or 0xff")
467 return
468 sz += 1
469 addr += 1
470 remaining -= 1
471 if sz > 0:
472 self.model.AS.make_filler(self.cur_addr(), sz)
473 self.update_model()
475 elif key == b"u":
476 addr = self.cur_addr()
477 self.model.undefine_unit(addr)
478 self.update_model()
480 elif key == b"h":
481 op_no = self.cur_operand_no(self.get_cur_line())
482 if op_no >= 0:
483 addr = self.cur_addr()
484 subtype = self.model.AS.get_arg_prop(addr, op_no, "subtype")
485 if subtype != engine.IMM_ADDR:
486 next_subtype = {
487 engine.IMM_UHEX: engine.IMM_UDEC,
488 engine.IMM_UDEC: engine.IMM_UHEX,
490 self.model.AS.set_arg_prop(addr, op_no, "subtype", next_subtype[subtype])
491 self.redraw()
492 self.show_status("Changed arg #%d to %s" % (op_no, next_subtype[subtype]))
493 elif key == b"o":
494 addr = self.cur_addr()
495 line = self.get_cur_line()
496 o = line.get_operand_addr()
497 if not o:
498 self.show_status("Cannot convert operand to offset")
499 return
500 if o.type != idaapi.o_imm or not self.model.AS.is_valid_addr(o.get_addr()):
501 self.show_status("Cannot convert operand to offset: #%s: %s" % (o.n, o.type))
502 return
504 if self.model.AS.get_arg_prop(addr, o.n, "subtype") == engine.IMM_ADDR:
505 self.model.AS.unmake_arg_offset(addr, o.n, o.get_addr())
506 else:
507 self.model.AS.make_arg_offset(addr, o.n, o.get_addr())
508 self.update_model(True)
509 elif key == b";":
510 addr = self.cur_addr()
511 comment = self.model.AS.get_comment(addr) or ""
512 res = DMultiEntry(60, 5, comment.split("\n"), title="Comment:").result()
513 if res != ACTION_CANCEL:
514 res = "\n".join(res).rstrip("\n")
515 self.model.AS.set_comment(addr, res)
516 self.update_model()
517 else:
518 self.redraw()
519 elif key == b"n":
520 addr = self.cur_addr()
521 label = self.model.AS.get_label(addr)
522 def_label = self.model.AS.get_default_label(addr)
523 s = label or def_label
524 while True:
525 res = DTextEntry(30, s, title="New label:").result()
526 if not res:
527 break
528 if res == def_label:
529 res = addr
530 else:
531 if self.model.AS.label_exists(res):
532 s = res
533 self.show_status("Duplicate label")
534 continue
535 self.model.AS.set_label(addr, res)
536 if not label:
537 # If it's new label, we need to add it to model
538 self.update_model()
539 return
540 break
541 self.redraw()
543 elif key == editor.KEY_F1:
544 help.help(self)
545 self.redraw()
546 elif key == b"S":
547 self.show_status("Saving...")
548 saveload.save_state(project_dir)
549 self.model.AS.changed = False
550 self.show_status("Saved.")
551 elif key == b"\x11": # ^Q
552 class IssueList(WListBox):
553 def render_line(self, l):
554 return "%08x %s" % l
555 d = Dialog(4, 4, title="Problems list")
556 lw = IssueList(40, 16, self.model.AS.get_issues())
557 lw.finish_dialog = ACTION_OK
558 d.add(1, 1, lw)
559 res = d.loop()
560 self.redraw()
561 if res == ACTION_OK:
562 val = lw.get_cur_line()
563 if val:
564 self.goto_addr(val[0], from_addr=self.cur_addr())
566 elif key == b"i":
567 off, area = self.model.AS.addr2area(self.cur_addr())
568 props = area[engine.PROPS]
569 percent = 100 * off / (area[engine.END] - area[engine.START] + 1)
570 func = self.model.AS.lookup_func(self.cur_addr())
571 func = self.model.AS.get_label(func.start) if func else None
572 status = "Area: 0x%x %s (%s): %.1f%%, func: %s" % (
573 area[engine.START], props.get("name", "noname"), props["access"], percent, func
575 subarea = self.model.AS.lookup_subarea(self.cur_addr())
576 if subarea:
577 status += ", subarea: " + subarea[2]
578 self.show_status(status)
579 elif key == b"I":
580 from scratchabit import memmap
581 addr = memmap.show(self.model.AS, self.cur_addr())
582 if addr is not None:
583 self.goto_addr(addr, from_addr=self.cur_addr())
584 self.redraw()
585 elif key == b"W":
586 out_fname = "out.lst"
587 with open(out_fname, "w") as f:
588 engine.render_partial(actions.TextSaveModel(f, self), 0, 0, 10000000)
589 self.show_status("Disassembly listing written: " + out_fname)
590 elif key == MENU_WRITE_ALL_HTML:
591 out_fname = "out.html"
592 with open(out_fname, "w") as f:
593 f.write("<pre>\n")
594 m = actions.HTMLSaveModel(f, self)
595 m.aspace = self.model.AS
596 engine.render_partial(m, 0, 0, 10000000)
597 f.write("</pre>\n")
598 self.show_status("Disassembly HTML listing written: " + out_fname)
599 elif key == b"\x17": # Ctrl+W
600 outfile = actions.write_func_by_addr(APP, self.cur_addr(), feedback_obj=self)
601 if outfile:
602 self.show_status("Wrote file: %s" % outfile)
603 elif key == b"\x15": # Ctrl+U
604 # Next undefined
605 addr = self.cur_addr()
606 flags = self.model.AS.get_flags(addr)
607 if flags == self.model.AS.UNK:
608 # If already on undefined, skip the current stride of them,
609 # as they indeed go in batches.
610 while True:
611 flags = self.model.AS.get_flags(addr)
612 if flags != self.model.AS.UNK:
613 break
614 addr = self.model.AS.next_addr(addr)
615 if addr is None:
616 break
618 if addr is not None:
619 while True:
620 flags = self.model.AS.get_flags(addr)
621 if flags == self.model.AS.UNK:
622 self.goto_addr(addr, from_addr=self.cur_addr())
623 break
624 addr = self.model.AS.next_addr(addr)
625 if addr is None:
626 break
628 if addr is None:
629 self.show_status("There're no further undefined strides")
631 elif key == b"\x06": # Ctrl+F
632 # Next non-function
633 addr = self.cur_addr()
634 flags = self.model.AS.get_flags(addr, ~ADDRESS_SPACE.ALT_CODE)
635 if flags == self.model.AS.CODE:
636 # If already on non-func code, skip the current stride of it,
637 # as it indeed go in batches.
638 while True:
639 flags = self.model.AS.get_flags(addr, ~ADDRESS_SPACE.ALT_CODE)
640 self.show_status("fl=%x" % flags)
641 if flags not in (self.model.AS.CODE, self.model.AS.CODE_CONT):
642 break
643 addr = self.model.AS.next_addr(addr)
644 if addr is None:
645 break
647 if addr is not None:
648 while True:
649 flags = self.model.AS.get_flags(addr, ~ADDRESS_SPACE.ALT_CODE)
650 if flags == self.model.AS.CODE:
651 self.goto_addr(addr, from_addr=self.cur_addr())
652 break
653 addr = self.model.AS.next_addr(addr)
654 if addr is None:
655 break
657 if addr is None:
658 self.show_status("There're no further non-function code strides")
660 elif key in (b"/", b"?"): # "/" and Shift+"/"
662 class FoundException(Exception): pass
664 class TextSearchModel(engine.Model):
665 def __init__(self, substr, ctrl, this_addr, this_subno):
666 super().__init__()
667 self.search = substr
668 self.ctrl = ctrl
669 self.this_addr = this_addr
670 self.this_subno = this_subno
671 self.cnt = 0
672 def add_object(self, addr, line):
673 super().add_object(addr, line)
674 # Skip virtual lines before the line from which we started
675 if addr == self.this_addr and line.subno < self.this_subno:
676 return
677 txt = line.render()
678 idx = txt.find(self.search)
679 if idx != -1:
680 raise FoundException((addr, line.subno), idx + line.LEADER_SIZE + len(line.indent))
681 if self.cnt % 256 == 0:
682 self.ctrl.show_status("Searching: 0x%x" % addr)
683 self.cnt += 1
684 # Don't accumulate lines
685 self._lines = []
686 self._addr2line = {}
688 if key == b"/":
689 d = Dialog(4, 4, title="Text Search")
690 d.add(1, 1, WLabel("Search for:"))
691 entry = WTextEntry(20, self.search_str)
692 entry.finish_dialog = ACTION_OK
693 d.add(13, 1, entry)
694 res = d.loop()
695 self.redraw()
696 self.search_str = entry.get_text()
697 if res != ACTION_OK or not self.search_str:
698 return
699 addr, subno = self.cur_addr_subno()
700 else:
701 addr, subno = self.next_line_addr_subno()
703 try:
704 engine.render_from(TextSearchModel(self.search_str, self, addr, subno), addr, 10000000)
705 except FoundException as res:
706 self.goto_addr(res.args[0], col=res.args[1], from_addr=self.cur_addr())
707 else:
708 self.show_status("Not found: " + self.search_str)
710 elif key == MENU_PREFS:
711 uiprefs.handle(APP)
713 elif key == MENU_PLUGIN:
714 res = DTextEntry(30, "", title="Plugin module name:").result()
715 self.redraw()
716 if res:
717 self.show_status("Running '%s' plugin..." % res)
718 call_script(res)
719 self.update_model()
720 self.show_status("Plugin '%s' ran successfully" % res)
721 else:
722 self.show_status("Unbound key: " + repr(key))
725 ACTION_MAP = {
726 b"g": DisasmViewer.action_goto,
727 b"a": DisasmViewer.action_make_ascii,
731 CPU_PLUGIN = None
732 ENTRYPOINTS = []
733 APP.show_bytes = 4
735 def filter_config_line(l):
736 l = re.sub(r"#.*$", "", l)
737 l = l.strip()
738 return l
740 def load_symbols(fname):
741 with open(fname) as f:
742 for l in f:
743 l = filter_config_line(l)
744 if not l:
745 continue
746 m = re.search(r"\b([A-Za-z_$.][A-Za-z0-9_$.]*)\s*=\s*((0x)?[0-9A-Fa-f]+)", l)
747 if m:
748 #print(m.groups())
749 ENTRYPOINTS.append((m.group(1), int(m.group(2), 0)))
750 else:
751 print("Warning: cannot parse entrypoint info from: %r" % l)
754 # Allow undescores to separate digit groups
755 def str2int(s):
756 return int(s.replace("_", ""), 0)
759 def parse_range(arg):
760 # name start(len)
761 # name start-end
762 if "(" in arg:
763 m = re.match(r"(.+?)\s*\(\s*(.+?)\s*\)", arg)
764 start = str2int(m.group(1))
765 end = start + str2int(m.group(2)) - 1
766 else:
767 m = re.match(r"(.+)\s*-\s*(.+)", arg)
768 start = str2int(m.group(1))
769 end = str2int(m.group(2))
770 return start, end
773 def parse_entrypoints(f):
774 for l in f:
775 l = filter_config_line(l)
776 if not l:
777 continue
778 if l[0] == "[":
779 return l
780 m = re.match(r'load "(.+?)"', l)
781 if m:
782 load_symbols(m.group(1))
783 else:
784 label, addr = [v.strip() for v in l.split("=")]
785 ENTRYPOINTS.append((label, int(addr, 0)))
786 return ""
788 def parse_subareas(f):
789 subareas = []
790 for l in f:
791 l = filter_config_line(l)
792 if not l:
793 continue
794 if l[0] == "[":
795 return l
797 args = l.split()
798 assert len(args) == 2
799 start, end = parse_range(args[1])
800 engine.ADDRESS_SPACE.add_subarea(start, end, args[0])
801 engine.ADDRESS_SPACE.finish_subareas()
802 return ""
805 def load_target_file(loader, fname):
806 entry = loader.load(engine.ADDRESS_SPACE, fname)
807 log.info("Loaded %s, entrypoint: %s", fname, hex(entry) if entry is not None else None)
808 if entry is not None:
809 ENTRYPOINTS.append(("_ENTRY_", entry))
812 def parse_disasm_def(fname):
813 global CPU_PLUGIN
814 with open(fname) as f:
815 for l in f:
816 l = filter_config_line(l)
817 if not l:
818 continue
819 #print(l)
820 while True:
821 if not l:
822 #return
823 break
824 if l[0] == "[":
825 section = l[1:-1]
826 print("Processing section: %s" % section)
827 if section == "entrypoints":
828 l = parse_entrypoints(f)
829 elif section == "subareas":
830 l = parse_subareas(f)
831 else:
832 assert 0, "Unknown section: " + section
833 else:
834 break
836 if not l:
837 break
839 if l.startswith("load"):
840 args = l.split()
841 if args[2][0] in string.digits:
842 addr = int(args[2], 0)
843 print("Loading %s @0x%x" % (args[1], addr))
844 engine.ADDRESS_SPACE.load_content(open(args[1], "rb"), addr)
845 else:
846 print("Loading %s (%s plugin)" % (args[1], args[2]))
847 loader = __import__(args[2])
848 load_target_file(loader, args[1])
849 elif l.startswith("cpu "):
850 args = l.split()
851 CPU_PLUGIN = __import__(args[1])
852 if hasattr(CPU_PLUGIN, "arch_id"):
853 engine.set_arch_id(CPU_PLUGIN.arch_id)
854 print("Loading CPU plugin %s" % (args[1]))
855 elif l.startswith("show bytes "):
856 args = l.split()
857 APP.show_bytes = int(args[2])
858 elif l.startswith("area "):
859 args = l.split()
860 assert len(args) == 4
861 start, end = parse_range(args[2])
862 a = engine.ADDRESS_SPACE.add_area(start, end, {"name": args[1], "access": args[3].upper()})
863 print("Adding area: %s" % engine.str_area(a))
864 else:
865 assert 0, "Unknown directive: " + l
868 class MainScreen:
870 def __init__(self):
871 self.screen_size = Screen.screen_size()
872 self.e = DisasmViewer(1, 2, self.screen_size[0] - 2, self.screen_size[1] - 4)
874 menu_file = WMenuBox([
875 ("Save (Shift+s)", b"S"),
876 ("Write disasm (Shift+w)", b"W"),
877 ("Write disasm in HTML", MENU_WRITE_ALL_HTML),
878 ("Write function (Ctrl+w)", b"\x17"),
879 ("Quit (q)", b"q")
881 menu_goto = WMenuBox([
882 ("Follow (Enter)", KEY_ENTER), ("Return (Esc)", KEY_ESC),
883 ("Goto... (g)", b"g"), ("Search disasm... (/)", b"/"),
884 ("Search next (Shift+/)", b"?"), ("Next undefined (Ctrl+u)", b"\x15"),
885 ("Next non-function code (Ctrl+f)", b"\x06"),
887 menu_edit = WMenuBox([
888 ("Undefined (u)", b"u"),
889 ("Code (c)", b"c"),
890 ("Alt code (Shift+c)", b"C"),
891 ("Data (d)", b"d"),
892 ("ASCII String (a)", b"a"), ("Filler (f)", b"f"), ("Make label (n)", b"n"),
893 ("Mark function start (F)", b"F"), ("Add code to function", MENU_ADD_TO_FUNC),
894 ("Number/Address (o)", b"o"), ("Hex/dec (h)", b"h"),
896 menu_analysis = WMenuBox([
897 ("Info (whereami) (i)", b"i"), ("Memory map (Shift+i)", b"I"),
898 ("Run plugin...", MENU_PLUGIN),
899 ("Preferences...", MENU_PREFS),
901 menu_help = WMenuBox([
902 ("Help (F1)", KEY_F1), ("About...", "about"),
904 self.menu_bar = WMenuBar([
905 ("File", menu_file), ("Goto", menu_goto), ("Edit", menu_edit),
906 ("Analysis", menu_analysis), ("Help", menu_help)
908 self.menu_bar.permanent = True
910 def redraw(self, allow_cursor=True):
911 self.menu_bar.redraw()
912 self.e.attr_color(C_B_WHITE, C_BLUE)
913 self.e.draw_box(0, 1, self.screen_size[0], self.screen_size[1] - 2)
914 self.e.attr_reset()
915 self.e.redraw()
916 if allow_cursor:
917 self.e.cursor(True)
919 def loop(self):
920 while 1:
921 key = self.e.get_input()
922 if isinstance(key, list):
923 x, y = key
924 if self.menu_bar.inside(x, y):
925 self.menu_bar.focus = True
927 if self.menu_bar.focus:
928 res = self.menu_bar.handle_input(key)
929 if res == ACTION_CANCEL:
930 self.menu_bar.focus = False
931 elif res is not None and res is not True:
933 res = self.e.handle_input(res)
934 if res is not None and res is not True:
935 return res
936 else:
937 if key == KEY_F9:
938 self.menu_bar.focus = True
939 self.menu_bar.redraw()
940 continue
942 res = self.e.handle_input(key)
944 if res is not None and res is not True:
945 return res
948 def call_script(script):
949 mod = __import__(script)
950 main_f = getattr(mod, "main", None)
951 if main_f:
952 main_f(APP)
955 if __name__ == "__main__":
957 argp = argparse.ArgumentParser(description="ScratchABit interactive disassembler")
958 argp.add_argument("file", help="Input file (binary or disassembly .def)")
959 argp.add_argument("--script", action="append", help="Run script from file after loading environment")
960 argp.add_argument("--save", action="store_true", help="Save after --script and quit; don't show UI")
961 args = argp.parse_args()
963 # Plugin dirs are relative to the dir where scratchabit.py resides.
964 # sys.path[0] below provide absolute path of this dir, resolved for
965 # symlinks.
966 plugin_dirs = ["plugins", "plugins/cpu", "plugins/loader"]
967 for d in plugin_dirs:
968 sys.path.append(os.path.join(sys.path[0], d))
969 log.basicConfig(filename="scratchabit.log", format='%(asctime)s %(message)s', level=log.DEBUG)
970 log.info("Started")
972 if args.file.endswith(".def"):
973 parse_disasm_def(args.file)
974 project_name = args.file.rsplit(".", 1)[0]
975 else:
976 import default_plugins
977 for loader_id in default_plugins.loaders:
978 loader = __import__(loader_id)
979 arch_id = loader.detect(args.file)
980 if arch_id:
981 break
982 if not arch_id:
983 print("Error: file '%s' not recognized by default loaders" % args.file)
984 sys.exit(1)
985 if arch_id not in default_plugins.cpus:
986 print("Error: no plugin for CPU '%s' as detected for file '%s'" % (arch_id, args.file))
987 sys.exit(1)
989 engine.set_arch_id(arch_id)
990 load_target_file(loader, args.file)
991 CPU_PLUGIN = __import__(default_plugins.cpus[arch_id])
992 project_name = args.file
994 p = CPU_PLUGIN.PROCESSOR_ENTRY()
995 if hasattr(p, "config"):
996 p.config()
997 engine.set_processor(p)
998 if hasattr(p, "help_text"):
999 help.set_cpu_help(p.help_text)
1000 APP.cpu_plugin = p
1001 APP.aspace = engine.ADDRESS_SPACE
1002 APP.is_ui = False
1003 engine.ADDRESS_SPACE.is_loading = True
1005 # Calc various offset based on show_bytes value
1006 APP.set_show_bytes(APP.show_bytes)
1008 # Strip suffix if any from def filename
1009 project_dir = project_name + ".scratchabit"
1011 if saveload.save_exists(project_dir):
1012 saveload.load_state(project_dir)
1013 else:
1014 for label, addr in ENTRYPOINTS:
1015 if engine.arch_id == "arm_32" and addr & 1:
1016 addr &= ~1
1017 engine.ADDRESS_SPACE.make_alt_code(addr)
1018 if engine.ADDRESS_SPACE.is_exec(addr):
1019 engine.add_entrypoint(addr)
1020 engine.ADDRESS_SPACE.make_unique_label(addr, label)
1021 def _progress(cnt):
1022 sys.stdout.write("Performing initial analysis... %d\r" % cnt)
1023 engine.analyze(_progress)
1024 print()
1026 #engine.print_address_map()
1028 if args.script:
1029 for script in args.script:
1030 call_script(script)
1031 if args.save:
1032 saveload.save_state(project_dir)
1033 sys.exit()
1035 addr_stack = []
1036 if os.path.exists(project_dir + "/session.addr_stack"):
1037 addr_stack = saveload.load_addr_stack(project_dir)
1038 print(addr_stack)
1039 show_addr = addr_stack.pop()
1040 else:
1041 if ENTRYPOINTS:
1042 show_addr = ENTRYPOINTS[0][1]
1043 if engine.arch_id == "arm_32":
1044 show_addr &= ~1
1045 else:
1046 show_addr = engine.ADDRESS_SPACE.min_addr()
1048 t = time.time()
1049 #_model = engine.render()
1050 _model = engine.render_partial_around(show_addr, 0, HEIGHT * 2)
1051 print("Rendering time: %fs" % (time.time() - t))
1052 #print(_model.lines())
1053 #sys.exit()
1055 engine.ADDRESS_SPACE.is_loading = False
1056 engine.ADDRESS_SPACE.changed = False
1057 Screen.init_tty()
1058 try:
1059 Screen.cls()
1060 Screen.enable_mouse()
1061 main_screen = MainScreen()
1062 APP.main_screen = main_screen
1063 APP.is_ui = True
1064 main_screen.e.set_model(_model)
1065 main_screen.e.addr_stack = addr_stack
1066 main_screen.e.goto_addr(show_addr)
1067 Screen.set_screen_redraw(main_screen.redraw)
1068 main_screen.redraw()
1069 main_screen.e.show_status("Press F1 for help, F9 for menus")
1070 main_screen.loop()
1071 except:
1072 log.exception("Unhandled exception")
1073 raise
1074 finally:
1075 Screen.goto(0, main_screen.screen_size[1])
1076 Screen.cursor(True)
1077 Screen.disable_mouse()
1078 Screen.deinit_tty()
1079 Screen.wr("\n\n")
1080 saveload.save_session(project_dir, main_screen.e)