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/>.
28 from scratchabit
import engine
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
49 MENU_ADD_TO_FUNC
= 2002
61 print("%08x %s" % (p
.cmd
.ea
, p
.cmd
.disasm
))
62 p
.cmd
.ea
+= p
.cmd
.size
66 class DisasmViewer(editor
.EditorExt
):
68 def __init__(self
, *args
):
69 super().__init
__(*args
)
73 self
.def_color
= C_PAIR(C_CYAN
, C_BLUE
)
75 def set_model(self
, model
):
77 self
.set_lines(model
.lines())
78 # Invalidate top_line. Assuming goto_*() will be called
80 self
.top_line
= sys
.maxsize
82 def show_line(self
, l
, i
):
85 if not isinstance(l
, str):
90 b
= self
.model
.AS
.get_bytes(l
.ea
, l
.size
)
91 bin
= str(binascii
.hexlify(b
[:show_bytes
]), "ascii")
92 if l
.size
> show_bytes
:
94 res
+= idaapi
.fillstr(bin
, show_bytes
* 2 + 1)
95 res
+= l
.indent
+ l
.render()
98 engine
.Label
: C_PAIR(C_GREEN
, C_BLUE
),
99 engine
.AreaWrapper
: C_PAIR(C_YELLOW
, C_BLUE
),
100 engine
.FunctionWrapper
: C_PAIR(C_B_YELLOW
, C_BLUE
),
101 engine
.Xref
: C_PAIR(C_MAGENTA
, C_BLUE
),
102 engine
.Unknown
: C_PAIR(C_WHITE
, C_BLUE
),
103 engine
.Data
: C_PAIR(C_MAGENTA
, C_BLUE
),
104 engine
.String
: C_PAIR(C_B_MAGENTA
, C_BLUE
),
105 engine
.Fill
: C_PAIR(C_B_BLUE
, C_BLUE
),
107 c
= COLOR_MAP
.get(type(l
), self
.def_color
)
109 super().show_line(res
, i
)
113 def handle_input(self
, key
):
115 return super().handle_input(key
)
116 except Exception as ex
:
117 self
.show_exception(ex
)
121 def goto_addr(self
, to_addr
, col
=None, from_addr
=None):
123 self
.show_status("No address-like value to go to")
126 if isinstance(to_addr
, tuple):
127 to_addr
, subno
= to_addr
128 adj_addr
= self
.model
.AS
.adjust_addr_reverse(to_addr
)
130 self
.show_status("Unknown address: 0x%x" % to_addr
)
134 # If we can position cursor within current screen, do that,
136 no
= self
.model
.addr2line_no(to_addr
, subno
)
138 if self
.line_visible(no
):
139 self
.goto_line(no
, col
=col
)
140 if from_addr
is not None:
141 self
.addr_stack
.append(from_addr
)
144 # Otherwise, re-render model around needed address, and redraw screen
146 model
= engine
.render_partial_around(to_addr
, 0, HEIGHT
* 2)
147 self
.show_status("Rendering time: %fs" % (time
.time() - t
))
149 self
.show_status("Unknown address: 0x%x" % to_addr
)
151 self
.set_model(model
)
153 no
= self
.model
.addr2line_no(to_addr
, subno
)
155 if from_addr
is not None:
156 self
.addr_stack
.append(from_addr
)
157 if not self
.goto_line(no
, col
=col
):
158 # Need to redraw always, because we changed underlying model
161 self
.show_status("Unknown address: %x" % to_addr
)
163 def update_model(self
, stay_on_real
=False):
164 """Re-render model and update screen in such way that cursor stayed
165 on the same line (as far as possible).
166 stay_on_real == False - try to stay on same relative line no. for
168 stay_on_real == True - try to stay on the line which contains real
169 bytes for the current address (use this if you know that cursor
170 stayed on such line before the update).
172 addr
, subno
= self
.cur_addr_subno()
174 model
= engine
.render_partial_around(addr
, subno
, HEIGHT
* 2)
175 self
.show_status("Rendering time: %fs" % (time
.time() - t
))
176 self
.set_model(model
)
178 self
.cur_line
= model
.target_addr_lineno_real
180 self
.cur_line
= model
.target_addr_lineno
181 self
.top_line
= self
.cur_line
- self
.row
182 #log.debug("update_model: addr=%x, row=%d, cur_line=%d, top_line=%d" % (addr, self.row, self.cur_line, self.top_line))
185 def handle_cursor_keys(self
, key
):
187 if super().handle_cursor_keys(key
):
188 if self
.cur_line
== cl
:
190 #log.debug("handle_cursor_keys: cur: %d, total: %d", self.cur_line, self.total_lines)
191 if self
.cur_line
<= HEIGHT
or self
.total_lines
- self
.cur_line
<= HEIGHT
:
192 log
.debug("handle_cursor_keys: triggering update")
200 line
= self
.get_cur_line()
203 # Address of the next line. It may be the same address as the
204 # current line, as several lines may "belong" to the same address,
205 # (virtual lines like headers, etc.)
206 def next_line_addr_subno(self
):
208 l
= self
.content
[self
.cur_line
+ 1]
209 return (l
.ea
, l
.subno
)
213 # Return next address following the current line. May need to skip
216 addr
= self
.cur_addr()
217 n
= self
.cur_line
+ 1
219 while self
.content
[n
].ea
== addr
:
221 return self
.content
[n
].ea
225 def cur_addr_subno(self
):
226 line
= self
.get_cur_line()
227 return (line
.ea
, line
.subno
)
229 def cur_operand_no(self
, line
):
230 col
= self
.col
- engine
.DisasmObj
.LEADER_SIZE
- len(line
.indent
)
231 #self.show_status("Enter pressed: %s, %s" % (col, line))
232 for i
, pos
in enumerate(line
.arg_pos
):
233 if pos
[0] <= col
<= pos
[1]:
237 def analyze_status(self
, cnt
):
238 self
.show_status("Analyzing (%d insts so far)" % cnt
)
240 def expect_flags(self
, fl
, allowed_flags
):
241 if fl
not in allowed_flags
:
242 self
.show_status("Undefine first (u key)")
247 def show_exception(self
, e
):
248 log
.exception("Exception processing user command")
253 self
.dialog_box(L
, T
, W
, H
)
254 v
= Viewer(L
+ 1, T
+ 1, W
- 2, H
- 2)
257 "Exception occured processing the command. Press Esc to continue.",
258 "Recommended action is saving database, quitting and comparing",
259 "database files with backup copies for possibility of data loss",
260 "or corruption. The exception was also logged to scratchabit.log.",
261 "Please report way to reproduce it to",
262 "https://github.com/pfalcon/ScratchABit/issues",
264 ] + traceback
.format_exc().splitlines())
269 def resolve_expr(self
, expr
):
271 if expr
[0].isdigit():
274 words
= expr
.split("+", 1)
278 addend
= int(words
[1], 0)
281 to_addr
= self
.model
.AS
.resolve_label(words
[0])
284 return to_addr
+ addend
287 def require_non_func(self
, fl
):
288 if fl
& 0x7f != self
.model
.AS
.CODE
:
289 self
.show_status("Code required")
291 if fl
& self
.model
.AS
.FUNC
:
292 self
.show_status("Already a function")
297 def handle_edit_key(self
, key
):
298 line
= self
.get_cur_line()
299 if key
== editor
.KEY_ENTER
:
300 line
= self
.get_cur_line()
301 log
.info("Enter pressed: %s" % line
)
302 op_no
= self
.cur_operand_no(line
)
303 self
.show_status("Enter pressed: %s, %s" % (self
.col
, op_no
))
305 # No longer try to jump only to addresses in args, parse
306 # textual representation below
307 if False and isinstance(line
, engine
.DisasmObj
):
310 to_addr
= o
.get_addr()
312 o
= line
.get_operand_addr()
314 to_addr
= o
.get_addr()
316 pos
= self
.col
- line
.LEADER_SIZE
- len(line
.indent
)
317 word
= utils
.get_word_at_pos(line
.cache
, pos
)
318 self
.show_status("Enter pressed: %s, %s, %s" % (self
.col
, op_no
, word
))
319 to_addr
= self
.resolve_expr(word
)
321 self
.show_status("Unknown address: %s" % word
)
323 self
.goto_addr(to_addr
, from_addr
=self
.cur_addr_subno())
324 elif key
== editor
.KEY_ESC
:
326 self
.show_status("Returning")
327 self
.goto_addr(self
.addr_stack
.pop())
330 if self
.model
.AS
.changed
:
331 res
= DConfirmation("There're unsaved changes. Quit?").result()
333 return editor
.KEY_QUIT
335 elif key
== b
"\x1b[5;5~": # Ctrl+PgUp
336 self
.goto_addr(self
.model
.AS
.min_addr(), from_addr
=line
.ea
)
337 elif key
== b
"\x1b[6;5~": # Ctrl+PgDn
338 self
.goto_addr(self
.model
.AS
.max_addr(), from_addr
=line
.ea
)
340 addr
= self
.cur_addr()
341 self
.show_status("Analyzing at %x" % addr
)
342 engine
.add_entrypoint(addr
, False)
343 engine
.analyze(self
.analyze_status
)
347 addr
= self
.cur_addr()
348 fl
= self
.model
.AS
.get_flags(addr
, 0xff)
349 if not self
.require_non_func(fl
):
351 self
.show_status("Retracing as a function...")
352 self
.model
.AS
.make_label("fun_", addr
)
353 engine
.add_entrypoint(addr
, True)
354 engine
.analyze(self
.analyze_status
)
356 self
.show_status("Retraced as a function")
358 elif key
== MENU_ADD_TO_FUNC
:
359 addr
= self
.cur_addr()
360 if actions
.add_code_to_func(APP
, addr
):
364 addr
= self
.cur_addr()
365 fl
= self
.model
.AS
.get_flags(addr
)
366 if not self
.expect_flags(fl
, (self
.model
.AS
.DATA
, self
.model
.AS
.UNK
)):
368 if fl
== self
.model
.AS
.UNK
:
369 self
.model
.AS
.set_flags(addr
, 1, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
)
371 sz
= self
.model
.AS
.get_unit_size(addr
)
372 self
.model
.undefine_unit(addr
)
375 self
.model
.AS
.set_flags(addr
, sz
, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
)
378 addr
= self
.cur_addr()
379 fl
= self
.model
.AS
.get_flags(addr
)
380 if not self
.expect_flags(fl
, (self
.model
.AS
.DATA
, self
.model
.AS
.UNK
)):
385 b
= self
.model
.AS
.get_byte(addr
)
386 fl
= self
.model
.AS
.get_flags(addr
)
387 if not (0x20 <= b
<= 0x7e or b
in (0x0a, 0x0d)):
391 if fl
not in (self
.model
.AS
.UNK
, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
):
394 if c
< '0' or c
in string
.punctuation
:
400 self
.model
.AS
.set_flags(self
.cur_addr(), sz
, self
.model
.AS
.STR
, self
.model
.AS
.DATA_CONT
)
401 self
.model
.AS
.make_unique_label(self
.cur_addr(), label
)
404 addr
= self
.cur_addr()
405 fl
= self
.model
.AS
.get_flags(addr
)
406 if not self
.expect_flags(fl
, (self
.model
.AS
.UNK
,)):
409 off
, area
= self
.model
.AS
.addr2area(self
.cur_addr())
410 # Don't cross area boundaries with filler
411 remaining
= area
[engine
.END
] - addr
+ 1
415 fl
= self
.model
.AS
.get_flags(addr
)
416 except engine
.InvalidAddrException
:
418 if fl
!= self
.model
.AS
.UNK
:
420 b
= self
.model
.AS
.get_byte(addr
)
421 if b
not in (0, 0xff):
422 self
.show_status("Filler must consist of 0x00 or 0xff")
428 self
.model
.AS
.make_filler(self
.cur_addr(), sz
)
432 addr
= self
.cur_addr()
433 self
.model
.undefine_unit(addr
)
437 op_no
= self
.cur_operand_no(self
.get_cur_line())
439 addr
= self
.cur_addr()
440 subtype
= self
.model
.AS
.get_arg_prop(addr
, op_no
, "subtype")
441 if subtype
!= engine
.IMM_ADDR
:
443 engine
.IMM_UHEX
: engine
.IMM_UDEC
,
444 engine
.IMM_UDEC
: engine
.IMM_UHEX
,
446 self
.model
.AS
.set_arg_prop(addr
, op_no
, "subtype", next_subtype
[subtype
])
448 self
.show_status("Changed arg #%d to %s" % (op_no
, next_subtype
[subtype
]))
450 addr
= self
.cur_addr()
451 line
= self
.get_cur_line()
452 o
= line
.get_operand_addr()
454 self
.show_status("Cannot convert operand to offset")
456 if o
.type != idaapi
.o_imm
or not self
.model
.AS
.is_valid_addr(o
.get_addr()):
457 self
.show_status("Cannot convert operand to offset: #%s: %s" % (o
.n
, o
.type))
460 if self
.model
.AS
.get_arg_prop(addr
, o
.n
, "subtype") == engine
.IMM_ADDR
:
461 self
.model
.AS
.unmake_arg_offset(addr
, o
.n
, o
.get_addr())
463 self
.model
.AS
.make_arg_offset(addr
, o
.n
, o
.get_addr())
464 self
.update_model(True)
466 addr
= self
.cur_addr()
467 comment
= self
.model
.AS
.get_comment(addr
) or ""
468 res
= DMultiEntry(60, 5, comment
.split("\n"), title
="Comment:").result()
469 if res
!= ACTION_CANCEL
:
470 res
= "\n".join(res
).rstrip("\n")
471 self
.model
.AS
.set_comment(addr
, res
)
476 addr
= self
.cur_addr()
477 label
= self
.model
.AS
.get_label(addr
)
478 def_label
= self
.model
.AS
.get_default_label(addr
)
479 s
= label
or def_label
481 res
= DTextEntry(30, s
, title
="New label:").result()
487 if self
.model
.AS
.label_exists(res
):
489 self
.show_status("Duplicate label")
491 self
.model
.AS
.set_label(addr
, res
)
493 # If it's new label, we need to add it to model
499 d
= Dialog(4, 4, title
="Go to")
500 d
.add(1, 1, WLabel("Label/addr:"))
501 entry
= WAutoComplete(20, "", self
.model
.AS
.get_label_list())
503 entry
.finish_dialog
= ACTION_OK
505 d
.add(1, 2, WLabel("Press Down to auto-complete"))
510 value
= entry
.get_text()
511 if '0' <= value
[0] <= '9':
514 addr
= self
.model
.AS
.resolve_label(value
)
515 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
517 elif key
== editor
.KEY_F1
:
521 self
.show_status("Saving...")
522 saveload
.save_state(project_dir
)
523 self
.model
.AS
.changed
= False
524 self
.show_status("Saved.")
525 elif key
== b
"\x11": # ^Q
526 class IssueList(WListBox
):
527 def render_line(self
, l
):
529 d
= Dialog(4, 4, title
="Problems list")
530 lw
= IssueList(40, 16, self
.model
.AS
.get_issues())
531 lw
.finish_dialog
= ACTION_OK
536 val
= lw
.get_cur_line()
538 self
.goto_addr(val
[0], from_addr
=self
.cur_addr())
541 off
, area
= self
.model
.AS
.addr2area(self
.cur_addr())
542 props
= area
[engine
.PROPS
]
543 percent
= 100 * off
/ (area
[engine
.END
] - area
[engine
.START
] + 1)
544 func
= self
.model
.AS
.lookup_func(self
.cur_addr())
545 func
= self
.model
.AS
.get_label(func
.start
) if func
else None
546 status
= "Area: 0x%x %s (%s): %.1f%%, func: %s" % (
547 area
[engine
.START
], props
.get("name", "noname"), props
["access"], percent
, func
549 subarea
= self
.model
.AS
.lookup_subarea(self
.cur_addr())
551 status
+= ", subarea: " + subarea
[2]
552 self
.show_status(status
)
554 from scratchabit
import memmap
555 addr
= memmap
.show(self
.model
.AS
, self
.cur_addr())
557 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
560 out_fname
= "out.lst"
561 with
open(out_fname
, "w") as f
:
562 engine
.render_partial(actions
.TextSaveModel(f
, self
), 0, 0, 10000000)
563 self
.show_status("Disassembly listing written: " + out_fname
)
564 elif key
== b
"\x17": # Ctrl+W
565 outfile
= actions
.write_func_by_addr(APP
, self
.cur_addr(), feedback_obj
=self
)
567 self
.show_status("Wrote file: %s" % outfile
)
568 elif key
== b
"\x15": # Ctrl+U
570 addr
= self
.cur_addr()
571 flags
= self
.model
.AS
.get_flags(addr
)
572 if flags
== self
.model
.AS
.UNK
:
573 # If already on undefined, skip the current stride of them,
574 # as they indeed go in batches.
576 flags
= self
.model
.AS
.get_flags(addr
)
577 if flags
!= self
.model
.AS
.UNK
:
579 addr
= self
.model
.AS
.next_addr(addr
)
585 flags
= self
.model
.AS
.get_flags(addr
)
586 if flags
== self
.model
.AS
.UNK
:
587 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
589 addr
= self
.model
.AS
.next_addr(addr
)
594 self
.show_status("There're no further undefined strides")
596 elif key
== b
"\x06": # Ctrl+F
598 addr
= self
.cur_addr()
599 flags
= self
.model
.AS
.get_flags(addr
, 0xff)
600 if flags
== self
.model
.AS
.CODE
:
601 # If already on non-func code, skip the current stride of it,
602 # as it indeed go in batches.
604 flags
= self
.model
.AS
.get_flags(addr
, 0xff)
605 self
.show_status("fl=%x" % flags
)
606 if flags
not in (self
.model
.AS
.CODE
, self
.model
.AS
.CODE_CONT
):
608 addr
= self
.model
.AS
.next_addr(addr
)
614 flags
= self
.model
.AS
.get_flags(addr
, 0xff)
615 if flags
== self
.model
.AS
.CODE
:
616 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
618 addr
= self
.model
.AS
.next_addr(addr
)
623 self
.show_status("There're no further non-function code strides")
625 elif key
in (b
"/", b
"?"): # "/" and Shift+"/"
627 class FoundException(Exception): pass
629 class TextSearchModel(engine
.Model
):
630 def __init__(self
, substr
, ctrl
, this_addr
, this_subno
):
634 self
.this_addr
= this_addr
635 self
.this_subno
= this_subno
637 def add_line(self
, addr
, line
):
638 super().add_line(addr
, line
)
639 # Skip virtual lines before the line from which we started
640 if addr
== self
.this_addr
and line
.subno
< self
.this_subno
:
643 idx
= txt
.find(self
.search
)
645 raise FoundException((addr
, line
.subno
), idx
+ line
.LEADER_SIZE
+ len(line
.indent
))
646 if self
.cnt
% 256 == 0:
647 self
.ctrl
.show_status("Searching: 0x%x" % addr
)
649 # Don't accumulate lines
654 d
= Dialog(4, 4, title
="Text Search")
655 d
.add(1, 1, WLabel("Search for:"))
656 entry
= WTextEntry(20, self
.search_str
)
657 entry
.finish_dialog
= ACTION_OK
661 self
.search_str
= entry
.get_text()
662 if res
!= ACTION_OK
or not self
.search_str
:
664 addr
, subno
= self
.cur_addr_subno()
666 addr
, subno
= self
.next_line_addr_subno()
669 engine
.render_from(TextSearchModel(self
.search_str
, self
, addr
, subno
), addr
, 10000000)
670 except FoundException
as res
:
671 self
.goto_addr(res
.args
[0], col
=res
.args
[1], from_addr
=self
.cur_addr())
673 self
.show_status("Not found: " + self
.search_str
)
675 elif key
== MENU_PREFS
:
678 elif key
== MENU_PLUGIN
:
679 res
= DTextEntry(30, "", title
="Plugin module name:").result()
682 self
.show_status("Running '%s' plugin..." % res
)
685 self
.show_status("Plugin '%s' ran successfully" % res
)
687 self
.show_status("Unbound key: " + repr(key
))
694 def filter_config_line(l
):
695 l
= re
.sub(r
"#.*$", "", l
)
699 def load_symbols(fname
):
700 with
open(fname
) as f
:
702 l
= filter_config_line(l
)
705 m
= re
.search(r
"\b([A-Za-z_$.][A-Za-z0-9_$.]*)\s*=\s*((0x)?[0-9A-Fa-f]+)", l
)
708 ENTRYPOINTS
.append((m
.group(1), int(m
.group(2), 0)))
710 print("Warning: cannot parse entrypoint info from: %r" % l
)
713 # Allow undescores to separate digit groups
715 return int(s
.replace("_", ""), 0)
718 def parse_range(arg
):
722 m
= re
.match(r
"(.+?)\s*\(\s*(.+?)\s*\)", arg
)
723 start
= str2int(m
.group(1))
724 end
= start
+ str2int(m
.group(2)) - 1
726 m
= re
.match(r
"(.+)\s*-\s*(.+)", arg
)
727 start
= str2int(m
.group(1))
728 end
= str2int(m
.group(2))
732 def parse_entrypoints(f
):
734 l
= filter_config_line(l
)
739 m
= re
.match(r
'load "(.+?)"', l
)
741 load_symbols(m
.group(1))
743 label
, addr
= [v
.strip() for v
in l
.split("=")]
744 ENTRYPOINTS
.append((label
, int(addr
, 0)))
747 def parse_subareas(f
):
750 l
= filter_config_line(l
)
757 assert len(args
) == 2
758 start
, end
= parse_range(args
[1])
759 engine
.ADDRESS_SPACE
.add_subarea(start
, end
, args
[0])
760 engine
.ADDRESS_SPACE
.finish_subareas()
764 def load_target_file(loader
, fname
):
765 entry
= loader
.load(engine
.ADDRESS_SPACE
, fname
)
766 log
.info("Loaded %s, entrypoint: %s", fname
, hex(entry
) if entry
is not None else None)
767 if entry
is not None:
768 ENTRYPOINTS
.append(("_ENTRY_", entry
))
771 def parse_disasm_def(fname
):
774 with
open(fname
) as f
:
776 l
= filter_config_line(l
)
786 print("Processing section: %s" % section
)
787 if section
== "entrypoints":
788 l
= parse_entrypoints(f
)
789 elif section
== "subareas":
790 l
= parse_subareas(f
)
792 assert 0, "Unknown section: " + section
799 if l
.startswith("load"):
801 if args
[2][0] in string
.digits
:
802 addr
= int(args
[2], 0)
803 print("Loading %s @0x%x" % (args
[1], addr
))
804 engine
.ADDRESS_SPACE
.load_content(open(args
[1], "rb"), addr
)
806 print("Loading %s (%s plugin)" % (args
[1], args
[2]))
807 loader
= __import__(args
[2])
808 load_target_file(loader
, args
[1])
809 elif l
.startswith("cpu "):
811 CPU_PLUGIN
= __import__(args
[1])
812 print("Loading CPU plugin %s" % (args
[1]))
813 elif l
.startswith("show bytes "):
815 show_bytes
= int(args
[2])
816 elif l
.startswith("area "):
818 assert len(args
) == 4
819 start
, end
= parse_range(args
[2])
820 a
= engine
.ADDRESS_SPACE
.add_area(start
, end
, {"name": args
[1], "access": args
[3].upper()})
821 print("Adding area: %s" % engine
.str_area(a
))
823 assert 0, "Unknown directive: " + l
829 self
.screen_size
= Screen
.screen_size()
830 self
.e
= DisasmViewer(1, 2, self
.screen_size
[0] - 2, self
.screen_size
[1] - 4)
832 menu_file
= WMenuBox([
833 ("Save (Shift+s)", b
"S"), ("Write disasm (Shift+w)", b
"W"),
834 ("Write function (Ctrl+w)", b
"\x17"),
837 menu_goto
= WMenuBox([
838 ("Follow (Enter)", KEY_ENTER
), ("Return (Esc)", KEY_ESC
),
839 ("Goto... (g)", b
"g"), ("Search disasm... (/)", b
"/"),
840 ("Search next (Shift+/)", b
"?"), ("Next undefined (Ctrl+u)", b
"\x15"),
841 ("Next non-function code (Ctrl+f)", b
"\x06"),
843 menu_edit
= WMenuBox([
844 ("Undefined (u)", b
"u"), ("Code (c)", b
"c"), ("Data (d)", b
"d"),
845 ("ASCII String (a)", b
"a"), ("Filler (f)", b
"f"), ("Make label (n)", b
"n"),
846 ("Mark function start (F)", b
"F"), ("Add code to function", MENU_ADD_TO_FUNC
),
847 ("Number/Address (o)", b
"o"), ("Hex/dec (h)", b
"h"),
849 menu_analysis
= WMenuBox([
850 ("Info (whereami) (i)", b
"i"), ("Memory map (Shift+i)", b
"I"),
851 ("Run plugin...", MENU_PLUGIN
),
852 ("Preferences...", MENU_PREFS
),
854 menu_help
= WMenuBox([
855 ("Help (F1)", KEY_F1
), ("About...", "about"),
857 self
.menu_bar
= WMenuBar([
858 ("File", menu_file
), ("Goto", menu_goto
), ("Edit", menu_edit
),
859 ("Analysis", menu_analysis
), ("Help", menu_help
)
861 self
.menu_bar
.permanent
= True
863 def redraw(self
, allow_cursor
=True):
864 self
.menu_bar
.redraw()
865 self
.e
.attr_color(C_B_WHITE
, C_BLUE
)
866 self
.e
.draw_box(0, 1, self
.screen_size
[0], self
.screen_size
[1] - 2)
874 key
= self
.e
.get_input()
875 if isinstance(key
, list):
877 if self
.menu_bar
.inside(x
, y
):
878 self
.menu_bar
.focus
= True
880 if self
.menu_bar
.focus
:
881 res
= self
.menu_bar
.handle_input(key
)
882 if res
== ACTION_CANCEL
:
883 self
.menu_bar
.focus
= False
884 elif res
is not None and res
is not True:
886 res
= self
.e
.handle_input(res
)
887 if res
is not None and res
is not True:
891 self
.menu_bar
.focus
= True
892 self
.menu_bar
.redraw()
895 res
= self
.e
.handle_input(key
)
897 if res
is not None and res
is not True:
901 def call_script(script
):
902 mod
= __import__(script
)
903 main_f
= getattr(mod
, "main", None)
908 if __name__
== "__main__":
910 argp
= argparse
.ArgumentParser(description
="ScratchABit interactive disassembler")
911 argp
.add_argument("file", help="Input file (binary or disassembly .def)")
912 argp
.add_argument("--script", action
="append", help="Run script from file after loading environment")
913 argp
.add_argument("--save", action
="store_true", help="Save after --script and quit; don't show UI")
914 args
= argp
.parse_args()
916 # Plugin dirs are relative to the dir where scratchabit.py resides.
917 # sys.path[0] below provide absolute path of this dir, resolved for
919 plugin_dirs
= ["plugins", "plugins/cpu", "plugins/loader"]
920 for d
in plugin_dirs
:
921 sys
.path
.append(os
.path
.join(sys
.path
[0], d
))
922 log
.basicConfig(filename
="scratchabit.log", format
='%(asctime)s %(message)s', level
=log
.DEBUG
)
925 if args
.file.endswith(".def"):
926 parse_disasm_def(args
.file)
927 project_name
= args
.file.rsplit(".", 1)[0]
929 import default_plugins
930 for loader_id
in default_plugins
.loaders
:
931 loader
= __import__(loader_id
)
932 arch_id
= loader
.detect(args
.file)
936 print("Error: file '%s' not recognized by default loaders" % args
.file)
938 if arch_id
not in default_plugins
.cpus
:
939 print("Error: no plugin for CPU '%s' as detected for file '%s'" % (arch_id
, args
.file))
941 load_target_file(loader
, args
.file)
942 CPU_PLUGIN
= __import__(default_plugins
.cpus
[arch_id
])
943 project_name
= args
.file
945 p
= CPU_PLUGIN
.PROCESSOR_ENTRY()
946 if hasattr(p
, "config"):
948 engine
.set_processor(p
)
949 if hasattr(p
, "help_text"):
950 help.set_cpu_help(p
.help_text
)
952 APP
.aspace
= engine
.ADDRESS_SPACE
954 engine
.ADDRESS_SPACE
.is_loading
= True
956 engine
.DisasmObj
.LEADER_SIZE
= 8 + 1
958 engine
.DisasmObj
.LEADER_SIZE
+= show_bytes
* 2 + 1
960 # Strip suffix if any from def filename
961 project_dir
= project_name
+ ".scratchabit"
963 if saveload
.save_exists(project_dir
):
964 saveload
.load_state(project_dir
)
966 for label
, addr
in ENTRYPOINTS
:
967 if engine
.ADDRESS_SPACE
.is_exec(addr
):
968 engine
.add_entrypoint(addr
)
969 engine
.ADDRESS_SPACE
.make_unique_label(addr
, label
)
971 sys
.stdout
.write("Performing initial analysis... %d\r" % cnt
)
972 engine
.analyze(_progress
)
975 #engine.print_address_map()
978 for script
in args
.script
:
981 saveload
.save_state(project_dir
)
985 if os
.path
.exists(project_dir
+ "/session.addr_stack"):
986 addr_stack
= saveload
.load_addr_stack(project_dir
)
988 show_addr
= addr_stack
.pop()
991 show_addr
= ENTRYPOINTS
[0][1]
993 show_addr
= engine
.ADDRESS_SPACE
.min_addr()
996 #_model = engine.render()
997 _model
= engine
.render_partial_around(show_addr
, 0, HEIGHT
* 2)
998 print("Rendering time: %fs" % (time
.time() - t
))
999 #print(_model.lines())
1002 engine
.ADDRESS_SPACE
.is_loading
= False
1003 engine
.ADDRESS_SPACE
.changed
= False
1007 Screen
.enable_mouse()
1008 main_screen
= MainScreen()
1009 APP
.main_screen
= main_screen
1011 main_screen
.e
.set_model(_model
)
1012 main_screen
.e
.addr_stack
= addr_stack
1013 main_screen
.e
.goto_addr(show_addr
)
1014 Screen
.set_screen_redraw(main_screen
.redraw
)
1015 main_screen
.redraw()
1016 main_screen
.e
.show_status("Press F1 for help, F9 for menus")
1019 log
.exception("Unhandled exception")
1022 Screen
.goto(0, main_screen
.screen_size
[1])
1024 Screen
.disable_mouse()
1027 saveload
.save_session(project_dir
, main_screen
.e
)