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/>.
19 if sys
.version_info
< (3, 0):
21 ERROR: ScratchABit requires Python 3.3 or higher. You are running it with
22 following Python version:
26 Try running it as 'python3 %s' and/or install suitable version.
28 """ % (sys
.version
, sys
.argv
[0]))
40 from scratchabit
import engine
43 from picotui
.widgets
import *
44 from picotui
import editorext
as editor
45 from picotui
.screen
import Screen
46 from picotui
.editorext
import Viewer
47 from picotui
.menu
import *
48 from picotui
.dialogs
import *
50 from scratchabit
import utils
51 from scratchabit
import help
52 from scratchabit
import saveload
53 from scratchabit
import actions
54 from scratchabit
import uiprefs
61 MENU_ADD_TO_FUNC
= 2002
62 MENU_WRITE_ALL_HTML
= 2003
67 def set_show_bytes(self
, show_bytes
):
68 self
.show_bytes
= show_bytes
71 sz
+= show_bytes
* 2 + 1
72 engine
.DisasmObj
.LEADER_SIZE
= sz
81 print("%08x %s" % (p
.cmd
.ea
, p
.cmd
.disasm
))
82 p
.cmd
.ea
+= p
.cmd
.size
86 class DisasmViewer(editor
.EditorExt
):
88 def __init__(self
, *args
):
89 super().__init
__(*args
)
93 self
.def_color
= C_PAIR(C_CYAN
, C_BLUE
)
95 def set_model(self
, model
):
97 self
.set_lines(model
.lines())
98 # Invalidate top_line. Assuming goto_*() will be called
100 self
.top_line
= sys
.maxsize
102 def show_line(self
, l
, i
):
103 show_bytes
= APP
.show_bytes
105 if not isinstance(l
, str):
110 b
= self
.model
.AS
.get_bytes(l
.ea
, l
.size
)
111 bin
= str(binascii
.hexlify(b
[:show_bytes
]), "ascii")
112 if l
.size
> show_bytes
:
114 res
+= idaapi
.fillstr(bin
, show_bytes
* 2 + 1)
115 res
+= l
.indent
+ l
.render()
118 engine
.Label
: C_PAIR(C_GREEN
, C_BLUE
),
119 engine
.AreaWrapper
: C_PAIR(C_YELLOW
, C_BLUE
),
120 engine
.FunctionWrapper
: C_PAIR(C_B_YELLOW
, C_BLUE
),
121 engine
.Xref
: C_PAIR(C_MAGENTA
, C_BLUE
),
122 engine
.Unknown
: C_PAIR(C_WHITE
, C_BLUE
),
123 engine
.Data
: C_PAIR(C_MAGENTA
, C_BLUE
),
124 engine
.String
: C_PAIR(C_B_MAGENTA
, C_BLUE
),
125 engine
.Fill
: C_PAIR(C_B_BLUE
, C_BLUE
),
127 c
= COLOR_MAP
.get(type(l
), self
.def_color
)
129 super().show_line(res
, i
)
133 def handle_input(self
, key
):
135 return super().handle_input(key
)
136 except Exception as ex
:
137 self
.show_exception(ex
)
141 def goto_addr(self
, to_addr
, col
=None, from_addr
=None):
143 self
.show_status("No address-like value to go to")
146 if isinstance(to_addr
, tuple):
147 to_addr
, subno
= to_addr
148 adj_addr
= self
.model
.AS
.adjust_addr_reverse(to_addr
)
150 self
.show_status("Unknown address: 0x%x" % to_addr
)
154 # If we can position cursor within current screen, do that,
156 no
= self
.model
.addr2line_no(to_addr
, subno
)
158 if self
.line_visible(no
):
159 self
.goto_line(no
, col
=col
)
160 if from_addr
is not None:
161 self
.addr_stack
.append(from_addr
)
164 # Otherwise, re-render model around needed address, and redraw screen
166 model
= engine
.render_partial_around(to_addr
, 0, HEIGHT
* 2)
167 self
.show_status("Rendering time: %fs" % (time
.time() - t
))
169 self
.show_status("Unknown address: 0x%x" % to_addr
)
171 self
.set_model(model
)
173 no
= self
.model
.addr2line_no(to_addr
, subno
)
175 if from_addr
is not None:
176 self
.addr_stack
.append(from_addr
)
177 if not self
.goto_line(no
, col
=col
):
178 # Need to redraw always, because we changed underlying model
181 self
.show_status("Unknown address: %x" % to_addr
)
183 def update_model(self
, stay_on_real
=False):
184 """Re-render model and update screen in such way that cursor stayed
185 on the same line (as far as possible).
186 stay_on_real == False - try to stay on same relative line no. for
188 stay_on_real == True - try to stay on the line which contains real
189 bytes for the current address (use this if you know that cursor
190 stayed on such line before the update).
192 addr
, subno
= self
.cur_addr_subno()
194 model
= engine
.render_partial_around(addr
, subno
, HEIGHT
* 2)
195 self
.show_status("Rendering time: %fs" % (time
.time() - t
))
196 self
.set_model(model
)
198 self
.cur_line
= model
.target_addr_lineno_real
200 self
.cur_line
= model
.target_addr_lineno
201 self
.top_line
= self
.cur_line
- self
.row
202 #log.debug("update_model: addr=%x, row=%d, cur_line=%d, top_line=%d" % (addr, self.row, self.cur_line, self.top_line))
205 def handle_cursor_keys(self
, key
):
207 if super().handle_cursor_keys(key
):
208 if self
.cur_line
== cl
:
210 #log.debug("handle_cursor_keys: cur: %d, total: %d", self.cur_line, self.total_lines)
211 if self
.cur_line
<= HEIGHT
or self
.total_lines
- self
.cur_line
<= HEIGHT
:
212 log
.debug("handle_cursor_keys: triggering update")
220 line
= self
.get_cur_line()
223 # Address of the next line. It may be the same address as the
224 # current line, as several lines may "belong" to the same address,
225 # (virtual lines like headers, etc.)
226 def next_line_addr_subno(self
):
228 l
= self
.content
[self
.cur_line
+ 1]
229 return (l
.ea
, l
.subno
)
233 # Return next address following the current line. May need to skip
236 addr
= self
.cur_addr()
237 n
= self
.cur_line
+ 1
239 while self
.content
[n
].ea
== addr
:
241 return self
.content
[n
].ea
245 def cur_addr_subno(self
):
246 line
= self
.get_cur_line()
247 return (line
.ea
, line
.subno
)
249 def cur_operand_no(self
, line
):
250 col
= self
.col
- engine
.DisasmObj
.LEADER_SIZE
- len(line
.indent
)
251 #self.show_status("Enter pressed: %s, %s" % (col, line))
252 for i
, pos
in enumerate(line
.arg_pos
):
253 if pos
[0] <= col
<= pos
[1]:
257 def analyze_status(self
, cnt
):
258 self
.show_status("Analyzing (%d insts so far)" % cnt
)
260 def expect_flags(self
, fl
, allowed_flags
):
261 if fl
not in allowed_flags
:
262 self
.show_status("Undefine first (u key)")
267 def show_exception(self
, e
):
268 log
.exception("Exception processing user command")
273 self
.dialog_box(L
, T
, W
, H
)
274 v
= Viewer(L
+ 1, T
+ 1, W
- 2, H
- 2)
277 "Exception occurred processing the command. Press Esc to continue.",
278 "Recommended action is saving database, quitting and comparing",
279 "database files with backup copies for possibility of data loss",
280 "or corruption. The exception was also logged to scratchabit.log.",
281 "Please report way to reproduce it to",
282 "https://github.com/pfalcon/ScratchABit/issues",
284 ] + traceback
.format_exc().splitlines())
289 def resolve_expr(self
, expr
):
291 if expr
[0].isdigit():
294 words
= expr
.split("+", 1)
298 addend
= int(words
[1], 0)
301 to_addr
= self
.model
.AS
.resolve_label(words
[0])
304 return to_addr
+ addend
307 def require_non_func(self
, fl
):
308 if fl
& ~
(self
.model
.AS
.FUNC | self
.model
.AS
.ALT_CODE
) != self
.model
.AS
.CODE
:
309 self
.show_status("Code required")
311 if fl
& self
.model
.AS
.FUNC
:
312 self
.show_status("Already a function")
320 def action_goto(self
):
321 d
= Dialog(4, 4, title
="Go to")
322 d
.add(1, 1, WLabel("Label/addr:"))
323 entry
= WAutoComplete(20, "", self
.model
.AS
.get_label_list())
325 entry
.finish_dialog
= ACTION_OK
327 d
.add(1, 2, WLabel("Press Down to auto-complete"))
332 value
= entry
.get_text()
333 if '0' <= value
[0] <= '9':
336 addr
= self
.model
.AS
.resolve_label(value
)
337 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
340 def action_make_ascii(self
):
341 addr
= self
.cur_addr()
342 fl
= self
.model
.AS
.get_flags(addr
)
343 if not self
.expect_flags(fl
, (self
.model
.AS
.DATA
, self
.model
.AS
.UNK
)):
348 b
= self
.model
.AS
.get_byte(addr
)
349 fl
= self
.model
.AS
.get_flags(addr
)
350 if not (0x20 <= b
<= 0x7e or b
in (0x0a, 0x0d)):
354 if fl
not in (self
.model
.AS
.UNK
, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
):
357 if c
< '0' or c
in string
.punctuation
:
363 self
.model
.AS
.set_flags(self
.cur_addr(), sz
, self
.model
.AS
.STR
, self
.model
.AS
.DATA_CONT
)
364 self
.model
.AS
.make_unique_label(self
.cur_addr(), label
)
368 def handle_edit_key(self
, key
):
369 if key
in ACTION_MAP
:
370 return ACTION_MAP
[key
](self
)
372 line
= self
.get_cur_line()
373 if key
== editor
.KEY_ENTER
:
374 line
= self
.get_cur_line()
375 log
.info("Enter pressed: %s" % line
)
376 op_no
= self
.cur_operand_no(line
)
377 self
.show_status("Enter pressed: %s, %s" % (self
.col
, op_no
))
379 # No longer try to jump only to addresses in args, parse
380 # textual representation below
381 if False and isinstance(line
, engine
.DisasmObj
):
384 to_addr
= o
.get_addr()
386 o
= line
.get_operand_addr()
388 to_addr
= o
.get_addr()
390 pos
= self
.col
- line
.LEADER_SIZE
- len(line
.indent
)
391 word
= utils
.get_word_at_pos(line
.cache
, pos
)
392 self
.show_status("Enter pressed: %s, %s, %s" % (self
.col
, op_no
, word
))
393 to_addr
= self
.resolve_expr(word
)
395 self
.show_status("Unknown address: %s" % word
)
397 self
.goto_addr(to_addr
, from_addr
=self
.cur_addr_subno())
398 elif key
== editor
.KEY_ESC
:
400 self
.show_status("Returning")
401 self
.goto_addr(self
.addr_stack
.pop())
404 if self
.model
.AS
.changed
:
405 res
= DConfirmation("There're unsaved changes. Quit?").result()
407 return editor
.KEY_QUIT
409 elif key
== b
"\x1b[5;5~": # Ctrl+PgUp
410 self
.goto_addr(self
.model
.AS
.min_addr(), from_addr
=line
.ea
)
411 elif key
== b
"\x1b[6;5~": # Ctrl+PgDn
412 self
.goto_addr(self
.model
.AS
.max_addr(), from_addr
=line
.ea
)
414 addr
= self
.cur_addr()
415 self
.show_status("Analyzing at %x" % addr
)
416 engine
.add_entrypoint(addr
, False)
417 engine
.analyze(self
.analyze_status
)
421 addr
= self
.cur_addr()
422 self
.show_status("Analyzing at %x" % addr
)
423 self
.model
.AS
.make_alt_code(addr
)
424 engine
.add_entrypoint(addr
, False)
425 engine
.analyze(self
.analyze_status
)
429 addr
= self
.cur_addr()
430 fl
= self
.model
.AS
.get_flags(addr
, 0xff)
431 if not self
.require_non_func(fl
):
433 self
.show_status("Retracing as a function...")
434 self
.model
.AS
.make_label("fun_", addr
)
435 engine
.add_entrypoint(addr
, True)
436 engine
.analyze(self
.analyze_status
)
438 self
.show_status("Retraced as a function")
440 elif key
== MENU_ADD_TO_FUNC
:
441 addr
= self
.cur_addr()
442 if actions
.add_code_to_func(APP
, addr
):
446 addr
= self
.cur_addr()
447 fl
= self
.model
.AS
.get_flags(addr
)
448 if not self
.expect_flags(fl
, (self
.model
.AS
.DATA
, self
.model
.AS
.UNK
)):
450 if fl
== self
.model
.AS
.UNK
:
451 self
.model
.AS
.set_flags(addr
, 1, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
)
453 sz
= self
.model
.AS
.get_unit_size(addr
)
454 self
.model
.undefine_unit(addr
)
457 self
.model
.AS
.set_flags(addr
, sz
, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
)
460 addr
= self
.cur_addr()
461 fl
= self
.model
.AS
.get_flags(addr
)
462 if not self
.expect_flags(fl
, (self
.model
.AS
.UNK
,)):
465 off
, area
= self
.model
.AS
.addr2area(self
.cur_addr())
466 # Don't cross area boundaries with filler
467 remaining
= area
[engine
.END
] - addr
+ 1
471 fl
= self
.model
.AS
.get_flags(addr
)
472 except engine
.InvalidAddrException
:
474 if fl
!= self
.model
.AS
.UNK
:
476 b
= self
.model
.AS
.get_byte(addr
)
477 if b
not in (0, 0xff):
478 self
.show_status("Filler must consist of 0x00 or 0xff")
484 self
.model
.AS
.make_filler(self
.cur_addr(), sz
)
488 addr
= self
.cur_addr()
489 self
.model
.undefine_unit(addr
)
493 op_no
= self
.cur_operand_no(self
.get_cur_line())
495 addr
= self
.cur_addr()
496 subtype
= self
.model
.AS
.get_arg_prop(addr
, op_no
, "subtype")
497 if subtype
!= engine
.IMM_ADDR
:
499 engine
.IMM_UHEX
: engine
.IMM_UDEC
,
500 engine
.IMM_UDEC
: engine
.IMM_UHEX
,
502 self
.model
.AS
.set_arg_prop(addr
, op_no
, "subtype", next_subtype
[subtype
])
504 self
.show_status("Changed arg #%d to %s" % (op_no
, next_subtype
[subtype
]))
506 addr
= self
.cur_addr()
507 line
= self
.get_cur_line()
508 o
= line
.get_operand_addr()
510 self
.show_status("Cannot convert operand to offset")
512 if o
.type != idaapi
.o_imm
or not self
.model
.AS
.is_valid_addr(o
.get_addr()):
513 self
.show_status("Cannot convert operand to offset: #%s: %s" % (o
.n
, o
.type))
516 if self
.model
.AS
.get_arg_prop(addr
, o
.n
, "subtype") == engine
.IMM_ADDR
:
517 self
.model
.AS
.unmake_arg_offset(addr
, o
.n
, o
.get_addr())
519 self
.model
.AS
.make_arg_offset(addr
, o
.n
, o
.get_addr())
520 self
.update_model(True)
522 addr
= self
.cur_addr()
523 comment
= self
.model
.AS
.get_comment(addr
) or ""
524 res
= DMultiEntry(60, 5, comment
.split("\n"), title
="Comment:").result()
525 if res
!= ACTION_CANCEL
:
526 res
= "\n".join(res
).rstrip("\n")
527 self
.model
.AS
.set_comment(addr
, res
)
532 addr
= self
.cur_addr()
533 label
= self
.model
.AS
.get_label(addr
)
534 def_label
= self
.model
.AS
.get_default_label(addr
)
535 s
= label
or def_label
537 res
= DTextEntry(30, s
, title
="New label:").result()
543 if self
.model
.AS
.label_exists(res
):
545 self
.show_status("Duplicate label")
547 self
.model
.AS
.set_label(addr
, res
)
549 # If it's new label, we need to add it to model
555 elif key
== editor
.KEY_F1
:
559 self
.show_status("Saving...")
561 saveload
.save_state(project_dir
)
562 timer
= time
.time() - timer
563 log
.info("Saved database in %fs", timer
)
564 self
.model
.AS
.changed
= False
565 self
.show_status("Saved in %fs" % timer
)
566 elif key
== b
"\x11": # ^Q
567 class IssueList(WListBox
):
568 def render_line(self
, l
):
570 d
= Dialog(4, 4, title
="Problems list")
571 lw
= IssueList(40, 16, self
.model
.AS
.get_issues())
572 lw
.finish_dialog
= ACTION_OK
577 val
= lw
.get_cur_line()
579 self
.goto_addr(val
[0], from_addr
=self
.cur_addr())
582 off
, area
= self
.model
.AS
.addr2area(self
.cur_addr())
583 props
= area
[engine
.PROPS
]
584 percent
= 100 * off
/ (area
[engine
.END
] - area
[engine
.START
] + 1)
585 func
= self
.model
.AS
.lookup_func(self
.cur_addr())
586 func
= self
.model
.AS
.get_label(func
.start
) if func
else None
587 status
= "Area: 0x%x %s (%s): %.1f%%, func: %s" % (
588 area
[engine
.START
], props
.get("name", "noname"), props
["access"], percent
, func
590 subarea
= self
.model
.AS
.lookup_subarea(self
.cur_addr())
592 status
+= ", subarea: " + subarea
[2]
593 self
.show_status(status
)
595 from scratchabit
import memmap
596 addr
= memmap
.show(self
.model
.AS
, self
.cur_addr())
598 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
601 out_fname
= "out.lst"
602 actions
.write_disasm_all(APP
, out_fname
, feedback_obj
=self
)
603 self
.show_status("Disassembly listing written: " + out_fname
)
604 elif key
== MENU_WRITE_ALL_HTML
:
605 out_fname
= "out.html"
606 with
open(out_fname
, "w") as f
:
608 m
= actions
.HTMLSaveModel(f
, self
)
609 m
.aspace
= self
.model
.AS
610 engine
.render_partial(m
, 0, 0, 10000000)
612 self
.show_status("Disassembly HTML listing written: " + out_fname
)
613 elif key
== b
"\x17": # Ctrl+W
614 outfile
= actions
.write_func_by_addr(APP
, self
.cur_addr(), feedback_obj
=self
)
616 self
.show_status("Wrote file: %s" % outfile
)
617 elif key
== b
"\x15": # Ctrl+U
619 addr
= self
.cur_addr()
620 flags
= self
.model
.AS
.get_flags(addr
)
621 if flags
== self
.model
.AS
.UNK
:
622 # If already on undefined, skip the current stride of them,
623 # as they indeed go in batches.
625 flags
= self
.model
.AS
.get_flags(addr
)
626 if flags
!= self
.model
.AS
.UNK
:
628 addr
= self
.model
.AS
.next_addr(addr
)
634 flags
= self
.model
.AS
.get_flags(addr
)
635 if flags
== self
.model
.AS
.UNK
:
636 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
638 addr
= self
.model
.AS
.next_addr(addr
)
643 self
.show_status("There're no further undefined strides")
645 elif key
== b
"\x06": # Ctrl+F
647 addr
= self
.cur_addr()
648 flags
= self
.model
.AS
.get_flags(addr
, ~ADDRESS_SPACE
.ALT_CODE
)
649 if flags
== self
.model
.AS
.CODE
:
650 # If already on non-func code, skip the current stride of it,
651 # as it indeed go in batches.
653 flags
= self
.model
.AS
.get_flags(addr
, ~ADDRESS_SPACE
.ALT_CODE
)
654 self
.show_status("fl=%x" % flags
)
655 if flags
not in (self
.model
.AS
.CODE
, self
.model
.AS
.CODE_CONT
):
657 addr
= self
.model
.AS
.next_addr(addr
)
663 flags
= self
.model
.AS
.get_flags(addr
, ~ADDRESS_SPACE
.ALT_CODE
)
664 if flags
== self
.model
.AS
.CODE
:
665 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
667 addr
= self
.model
.AS
.next_addr(addr
)
672 self
.show_status("There're no further non-function code strides")
674 elif key
in (b
"/", b
"?"): # "/" and Shift+"/"
676 class FoundException(Exception): pass
678 class TextSearchModel(engine
.Model
):
679 def __init__(self
, substr
, ctrl
, this_addr
, this_subno
):
683 self
.this_addr
= this_addr
684 self
.this_subno
= this_subno
686 def add_object(self
, addr
, line
):
687 super().add_object(addr
, line
)
688 # Skip virtual lines before the line from which we started
689 if addr
== self
.this_addr
and line
.subno
< self
.this_subno
:
692 idx
= txt
.find(self
.search
)
694 raise FoundException((addr
, line
.subno
), idx
+ line
.LEADER_SIZE
+ len(line
.indent
))
695 if self
.cnt
% 256 == 0:
696 self
.ctrl
.show_status("Searching: 0x%x" % addr
)
698 # Don't accumulate lines
703 d
= Dialog(4, 4, title
="Text Search")
704 d
.add(1, 1, WLabel("Search for:"))
705 entry
= WTextEntry(20, self
.search_str
)
706 entry
.finish_dialog
= ACTION_OK
710 self
.search_str
= entry
.get_text()
711 if res
!= ACTION_OK
or not self
.search_str
:
713 addr
, subno
= self
.cur_addr_subno()
715 addr
, subno
= self
.next_line_addr_subno()
718 engine
.render_from(TextSearchModel(self
.search_str
, self
, addr
, subno
), addr
, 10000000)
719 except FoundException
as res
:
720 self
.goto_addr(res
.args
[0], col
=res
.args
[1], from_addr
=self
.cur_addr())
722 self
.show_status("Not found: " + self
.search_str
)
724 elif key
== MENU_PREFS
:
727 elif key
== MENU_PLUGIN
:
728 res
= DTextEntry(30, "", title
="Plugin module name:").result()
731 self
.show_status("Running '%s' plugin..." % res
)
734 self
.show_status("Plugin '%s' ran successfully" % res
)
736 self
.show_status("Unbound key: " + repr(key
))
740 b
"g": DisasmViewer
.action_goto
,
741 b
"a": DisasmViewer
.action_make_ascii
,
749 def filter_config_line(l
):
750 l
= re
.sub(r
"#.*$", "", l
)
754 def load_symbols(fname
):
755 with
open(fname
) as f
:
757 l
= filter_config_line(l
)
760 m
= re
.search(r
"\b([A-Za-z_$.][A-Za-z0-9_$.]*)\s*=\s*((0x)?[0-9A-Fa-f]+)", l
)
763 ENTRYPOINTS
.append((m
.group(1), int(m
.group(2), 0)))
765 print("Warning: cannot parse entrypoint info from: %r" % l
)
768 # Allow undescores to separate digit groups
770 return int(s
.replace("_", ""), 0)
773 def parse_range(arg
):
777 m
= re
.match(r
"(.+?)\s*\(\s*(.+?)\s*\)", arg
)
778 start
= str2int(m
.group(1))
779 end
= start
+ str2int(m
.group(2)) - 1
781 m
= re
.match(r
"(.+)\s*-\s*(.+)", arg
)
782 start
= str2int(m
.group(1))
783 end
= str2int(m
.group(2))
787 def parse_entrypoints(f
):
789 l
= filter_config_line(l
)
794 m
= re
.match(r
'load "(.+?)"', l
)
796 load_symbols(m
.group(1))
798 label
, addr
= [v
.strip() for v
in l
.split("=")]
799 ENTRYPOINTS
.append((label
, str2int(addr
)))
802 def parse_subareas(f
):
805 l
= filter_config_line(l
)
812 assert len(args
) == 2
813 start
, end
= parse_range(args
[1])
814 engine
.ADDRESS_SPACE
.add_subarea(start
, end
, args
[0])
815 engine
.ADDRESS_SPACE
.finish_subareas()
819 def load_target_file(loader
, fname
):
820 print("Loading %s..." % fname
)
822 entry
= loader
.load(engine
.ADDRESS_SPACE
, fname
)
823 log
.info("Loaded %s in %fs, entrypoint: %s", fname
, time
.time() - timer
, hex(entry
) if entry
is not None else None)
824 if entry
is not None:
825 ENTRYPOINTS
.append(("_ENTRY_", entry
))
828 def parse_disasm_def(fname
):
830 with
open(fname
) as f
:
832 l
= filter_config_line(l
)
842 print("Processing section: %s" % section
)
843 if section
== "entrypoints":
844 l
= parse_entrypoints(f
)
845 elif section
== "subareas":
846 l
= parse_subareas(f
)
848 assert 0, "Unknown section: " + section
855 if l
.startswith("load"):
857 if args
[2][0] in string
.digits
:
858 addr
= str2int(args
[2])
859 print("Loading %s @0x%x" % (args
[1], addr
))
860 engine
.ADDRESS_SPACE
.load_content(open(args
[1], "rb"), addr
)
862 print("Loading %s (%s plugin)" % (args
[1], args
[2]))
863 loader
= __import__(args
[2])
864 load_target_file(loader
, args
[1])
865 elif l
.startswith("cpu "):
867 CPU_PLUGIN
= __import__(args
[1])
868 if hasattr(CPU_PLUGIN
, "arch_id"):
869 engine
.set_arch_id(CPU_PLUGIN
.arch_id
)
870 print("Loading CPU plugin %s" % (args
[1]))
871 elif l
.startswith("show bytes "):
873 APP
.show_bytes
= int(args
[2])
874 elif l
.startswith("area "):
876 assert len(args
) == 4
877 start
, end
= parse_range(args
[2])
878 a
= engine
.ADDRESS_SPACE
.add_area(start
, end
, {"name": args
[1], "access": args
[3].upper()})
879 print("Adding area: %s" % engine
.str_area(a
))
881 assert 0, "Unknown directive: " + l
887 self
.screen_size
= Screen
.screen_size()
888 self
.e
= DisasmViewer(1, 2, self
.screen_size
[0] - 2, self
.screen_size
[1] - 4)
890 menu_file
= WMenuBox([
891 ("Save (Shift+s)", b
"S"),
892 ("Write disasm (Shift+w)", b
"W"),
893 ("Write disasm in HTML", MENU_WRITE_ALL_HTML
),
894 ("Write function (Ctrl+w)", b
"\x17"),
897 menu_goto
= WMenuBox([
898 ("Follow (Enter)", KEY_ENTER
), ("Return (Esc)", KEY_ESC
),
899 ("Goto... (g)", b
"g"), ("Search disasm... (/)", b
"/"),
900 ("Search next (Shift+/)", b
"?"), ("Next undefined (Ctrl+u)", b
"\x15"),
901 ("Next non-function code (Ctrl+f)", b
"\x06"),
903 menu_edit
= WMenuBox([
904 ("Undefined (u)", b
"u"),
906 ("Alt code (Shift+c)", b
"C"),
908 ("ASCII String (a)", b
"a"), ("Filler (f)", b
"f"), ("Make label (n)", b
"n"),
909 ("Mark function start (F)", b
"F"), ("Add code to function", MENU_ADD_TO_FUNC
),
910 ("Number/Address (o)", b
"o"), ("Hex/dec (h)", b
"h"),
912 menu_analysis
= WMenuBox([
913 ("Info (whereami) (i)", b
"i"), ("Memory map (Shift+i)", b
"I"),
914 ("Issue list (Ctrl+q)", b
"\x11"),
915 ("Run plugin...", MENU_PLUGIN
),
916 ("Preferences...", MENU_PREFS
),
918 menu_help
= WMenuBox([
919 ("Help (F1)", KEY_F1
), ("About...", "about"),
921 self
.menu_bar
= WMenuBar([
922 ("File", menu_file
), ("Goto", menu_goto
), ("Edit", menu_edit
),
923 ("Analysis", menu_analysis
), ("Help", menu_help
)
925 self
.menu_bar
.permanent
= True
927 def redraw(self
, allow_cursor
=True):
928 self
.menu_bar
.redraw()
929 self
.e
.attr_color(C_B_WHITE
, C_BLUE
)
930 self
.e
.draw_box(0, 1, self
.screen_size
[0], self
.screen_size
[1] - 2)
938 key
= self
.e
.get_input()
939 if isinstance(key
, list):
941 if self
.menu_bar
.inside(x
, y
):
942 self
.menu_bar
.focus
= True
944 if self
.menu_bar
.focus
:
945 res
= self
.menu_bar
.handle_input(key
)
946 if res
== ACTION_CANCEL
:
947 self
.menu_bar
.focus
= False
948 elif res
is not None and res
is not True:
950 res
= self
.e
.handle_input(res
)
951 if res
is not None and res
is not True:
955 self
.menu_bar
.focus
= True
956 self
.menu_bar
.redraw()
959 res
= self
.e
.handle_input(key
)
961 if res
is not None and res
is not True:
965 def call_script(script
, progress_func
=None):
966 mod
= __import__(script
)
967 main_f
= getattr(mod
, "main", None)
970 # A script might have queues some entrypoints, etc.
972 engine
.analyze(progress_func
)
975 if __name__
== "__main__":
977 argp
= argparse
.ArgumentParser(description
="ScratchABit interactive disassembler")
978 argp
.add_argument("file", help="Input file (binary or disassembly .def)")
979 argp
.add_argument("--script", action
="append", help="Run script from file after loading environment")
980 argp
.add_argument("--save", action
="store_true", help="Save DB after --script and quit; don't show UI")
981 args
= argp
.parse_args()
983 # Plugin dirs are relative to the dir where scratchabit.py resides.
984 # sys.path[0] below provide absolute path of this dir, resolved for
986 plugin_dirs
= ["plugins", "plugins/cpu", "plugins/loader"]
987 for d
in plugin_dirs
:
988 sys
.path
.append(os
.path
.join(sys
.path
[0], d
))
989 log
.basicConfig(filename
="scratchabit.log", format
='%(asctime)s %(message)s', level
=log
.DEBUG
)
990 log
.info("=" * 30 + " Started " + "=" * 30)
992 if args
.file.endswith(".def"):
993 parse_disasm_def(args
.file)
994 project_name
= args
.file.rsplit(".", 1)[0]
996 import default_plugins
997 for loader_id
in default_plugins
.loaders
:
998 loader
= __import__(loader_id
)
999 arch_id
= loader
.detect(args
.file)
1003 print("Error: file '%s' not recognized by default loaders" % args
.file)
1005 if arch_id
not in default_plugins
.cpus
:
1006 print("Error: no plugin for CPU '%s' as detected for file '%s'" % (arch_id
, args
.file))
1009 engine
.set_arch_id(arch_id
)
1010 load_target_file(loader
, args
.file)
1011 CPU_PLUGIN
= __import__(default_plugins
.cpus
[arch_id
])
1012 project_name
= args
.file
1014 p
= CPU_PLUGIN
.PROCESSOR_ENTRY()
1015 if hasattr(p
, "config"):
1017 engine
.set_processor(p
)
1018 if hasattr(p
, "help_text"):
1019 help.set_cpu_help(p
.help_text
)
1021 APP
.aspace
= engine
.ADDRESS_SPACE
1023 engine
.ADDRESS_SPACE
.is_loading
= True
1025 # Calc various offset based on show_bytes value
1026 APP
.set_show_bytes(APP
.show_bytes
)
1028 # Strip suffix if any from def filename
1029 project_dir
= project_name
+ ".scratchabit"
1031 if saveload
.save_exists(project_dir
):
1033 saveload
.load_state(project_dir
)
1034 log
.info("Loaded database in %fs", time
.time() - timer
)
1037 for label
, addr
in ENTRYPOINTS
:
1038 if engine
.arch_id
== "arm_32" and addr
& 1:
1040 engine
.ADDRESS_SPACE
.make_alt_code(addr
)
1041 if engine
.ADDRESS_SPACE
.is_exec(addr
):
1042 engine
.add_entrypoint(addr
)
1043 engine
.ADDRESS_SPACE
.make_unique_label(addr
, label
)
1045 sys
.stdout
.write("Performing initial analysis... %d\r" % cnt
)
1046 engine
.analyze(_progress
)
1047 log
.info("Performed initial analysis in %fs", time
.time() - timer
)
1050 #engine.print_address_map()
1054 sys
.stdout
.write("Performing analysis after running script(s)... %d\r" % cnt
)
1055 for script
in args
.script
:
1056 call_script(script
, _progress
)
1059 saveload
.save_state(project_dir
)
1064 if os
.path
.exists(project_dir
+ "/session.addr_stack"):
1065 addr_stack
= saveload
.load_addr_stack(project_dir
)
1067 show_addr
= addr_stack
.pop()
1068 log
.info("Loaded saved address stack, last address: 0x%x", show_addr
)
1069 if show_addr
is None:
1071 show_addr
= ENTRYPOINTS
[0][1]
1072 if engine
.arch_id
== "arm_32":
1075 show_addr
= engine
.ADDRESS_SPACE
.min_addr()
1077 log
.info("Starting UI")
1080 #_model = engine.render()
1081 _model
= engine
.render_partial_around(show_addr
, 0, HEIGHT
* 2)
1082 print("Rendering time: %fs" % (time
.time() - t
))
1083 #print(_model.lines())
1086 engine
.ADDRESS_SPACE
.is_loading
= False
1087 engine
.ADDRESS_SPACE
.changed
= False
1091 Screen
.enable_mouse()
1092 main_screen
= MainScreen()
1093 APP
.main_screen
= main_screen
1095 main_screen
.e
.set_model(_model
)
1096 main_screen
.e
.addr_stack
= addr_stack
1097 main_screen
.e
.goto_addr(show_addr
)
1098 Screen
.set_screen_redraw(main_screen
.redraw
)
1099 main_screen
.redraw()
1100 main_screen
.e
.show_status("Press F1 for help, F9 for menus")
1103 log
.exception("Unhandled exception")
1106 Screen
.goto(0, main_screen
.screen_size
[1])
1108 Screen
.disable_mouse()
1111 saveload
.save_session(project_dir
, main_screen
.e
)