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
50 MENU_WRITE_ALL_HTML
= 2003
55 def set_show_bytes(self
, show_bytes
):
56 self
.show_bytes
= show_bytes
59 sz
+= show_bytes
* 2 + 1
60 engine
.DisasmObj
.LEADER_SIZE
= sz
69 print("%08x %s" % (p
.cmd
.ea
, p
.cmd
.disasm
))
70 p
.cmd
.ea
+= p
.cmd
.size
74 class DisasmViewer(editor
.EditorExt
):
76 def __init__(self
, *args
):
77 super().__init
__(*args
)
81 self
.def_color
= C_PAIR(C_CYAN
, C_BLUE
)
83 def set_model(self
, model
):
85 self
.set_lines(model
.lines())
86 # Invalidate top_line. Assuming goto_*() will be called
88 self
.top_line
= sys
.maxsize
90 def show_line(self
, l
, i
):
91 show_bytes
= APP
.show_bytes
93 if not isinstance(l
, str):
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
:
102 res
+= idaapi
.fillstr(bin
, show_bytes
* 2 + 1)
103 res
+= l
.indent
+ l
.render()
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
)
117 super().show_line(res
, i
)
121 def handle_input(self
, key
):
123 return super().handle_input(key
)
124 except Exception as ex
:
125 self
.show_exception(ex
)
129 def goto_addr(self
, to_addr
, col
=None, from_addr
=None):
131 self
.show_status("No address-like value to go to")
134 if isinstance(to_addr
, tuple):
135 to_addr
, subno
= to_addr
136 adj_addr
= self
.model
.AS
.adjust_addr_reverse(to_addr
)
138 self
.show_status("Unknown address: 0x%x" % to_addr
)
142 # If we can position cursor within current screen, do that,
144 no
= self
.model
.addr2line_no(to_addr
, subno
)
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
)
152 # Otherwise, re-render model around needed address, and redraw screen
154 model
= engine
.render_partial_around(to_addr
, 0, HEIGHT
* 2)
155 self
.show_status("Rendering time: %fs" % (time
.time() - t
))
157 self
.show_status("Unknown address: 0x%x" % to_addr
)
159 self
.set_model(model
)
161 no
= self
.model
.addr2line_no(to_addr
, subno
)
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
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
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()
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
)
186 self
.cur_line
= model
.target_addr_lineno_real
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))
193 def handle_cursor_keys(self
, key
):
195 if super().handle_cursor_keys(key
):
196 if self
.cur_line
== cl
:
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")
208 line
= self
.get_cur_line()
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
):
216 l
= self
.content
[self
.cur_line
+ 1]
217 return (l
.ea
, l
.subno
)
221 # Return next address following the current line. May need to skip
224 addr
= self
.cur_addr()
225 n
= self
.cur_line
+ 1
227 while self
.content
[n
].ea
== addr
:
229 return self
.content
[n
].ea
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]:
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)")
255 def show_exception(self
, e
):
256 log
.exception("Exception processing user command")
261 self
.dialog_box(L
, T
, W
, H
)
262 v
= Viewer(L
+ 1, T
+ 1, W
- 2, H
- 2)
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())
277 def resolve_expr(self
, expr
):
279 if expr
[0].isdigit():
282 words
= expr
.split("+", 1)
286 addend
= int(words
[1], 0)
289 to_addr
= self
.model
.AS
.resolve_label(words
[0])
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")
299 if fl
& self
.model
.AS
.FUNC
:
300 self
.show_status("Already a function")
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())
313 entry
.finish_dialog
= ACTION_OK
315 d
.add(1, 2, WLabel("Press Down to auto-complete"))
320 value
= entry
.get_text()
321 if '0' <= value
[0] <= '9':
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
)):
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)):
342 if fl
not in (self
.model
.AS
.UNK
, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
):
345 if c
< '0' or c
in string
.punctuation
:
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
)
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
))
367 # No longer try to jump only to addresses in args, parse
368 # textual representation below
369 if False and isinstance(line
, engine
.DisasmObj
):
372 to_addr
= o
.get_addr()
374 o
= line
.get_operand_addr()
376 to_addr
= o
.get_addr()
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
)
383 self
.show_status("Unknown address: %s" % word
)
385 self
.goto_addr(to_addr
, from_addr
=self
.cur_addr_subno())
386 elif key
== editor
.KEY_ESC
:
388 self
.show_status("Returning")
389 self
.goto_addr(self
.addr_stack
.pop())
392 if self
.model
.AS
.changed
:
393 res
= DConfirmation("There're unsaved changes. Quit?").result()
395 return editor
.KEY_QUIT
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
)
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
)
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
)
417 addr
= self
.cur_addr()
418 fl
= self
.model
.AS
.get_flags(addr
, 0xff)
419 if not self
.require_non_func(fl
):
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
)
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
):
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
)):
438 if fl
== self
.model
.AS
.UNK
:
439 self
.model
.AS
.set_flags(addr
, 1, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
)
441 sz
= self
.model
.AS
.get_unit_size(addr
)
442 self
.model
.undefine_unit(addr
)
445 self
.model
.AS
.set_flags(addr
, sz
, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
)
448 addr
= self
.cur_addr()
449 fl
= self
.model
.AS
.get_flags(addr
)
450 if not self
.expect_flags(fl
, (self
.model
.AS
.UNK
,)):
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
459 fl
= self
.model
.AS
.get_flags(addr
)
460 except engine
.InvalidAddrException
:
462 if fl
!= self
.model
.AS
.UNK
:
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")
472 self
.model
.AS
.make_filler(self
.cur_addr(), sz
)
476 addr
= self
.cur_addr()
477 self
.model
.undefine_unit(addr
)
481 op_no
= self
.cur_operand_no(self
.get_cur_line())
483 addr
= self
.cur_addr()
484 subtype
= self
.model
.AS
.get_arg_prop(addr
, op_no
, "subtype")
485 if subtype
!= engine
.IMM_ADDR
:
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
])
492 self
.show_status("Changed arg #%d to %s" % (op_no
, next_subtype
[subtype
]))
494 addr
= self
.cur_addr()
495 line
= self
.get_cur_line()
496 o
= line
.get_operand_addr()
498 self
.show_status("Cannot convert operand to offset")
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))
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())
507 self
.model
.AS
.make_arg_offset(addr
, o
.n
, o
.get_addr())
508 self
.update_model(True)
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
)
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
525 res
= DTextEntry(30, s
, title
="New label:").result()
531 if self
.model
.AS
.label_exists(res
):
533 self
.show_status("Duplicate label")
535 self
.model
.AS
.set_label(addr
, res
)
537 # If it's new label, we need to add it to model
543 elif key
== editor
.KEY_F1
:
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
):
555 d
= Dialog(4, 4, title
="Problems list")
556 lw
= IssueList(40, 16, self
.model
.AS
.get_issues())
557 lw
.finish_dialog
= ACTION_OK
562 val
= lw
.get_cur_line()
564 self
.goto_addr(val
[0], from_addr
=self
.cur_addr())
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())
577 status
+= ", subarea: " + subarea
[2]
578 self
.show_status(status
)
580 from scratchabit
import memmap
581 addr
= memmap
.show(self
.model
.AS
, self
.cur_addr())
583 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
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
:
594 m
= actions
.HTMLSaveModel(f
, self
)
595 m
.aspace
= self
.model
.AS
596 engine
.render_partial(m
, 0, 0, 10000000)
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
)
602 self
.show_status("Wrote file: %s" % outfile
)
603 elif key
== b
"\x15": # Ctrl+U
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.
611 flags
= self
.model
.AS
.get_flags(addr
)
612 if flags
!= self
.model
.AS
.UNK
:
614 addr
= self
.model
.AS
.next_addr(addr
)
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())
624 addr
= self
.model
.AS
.next_addr(addr
)
629 self
.show_status("There're no further undefined strides")
631 elif key
== b
"\x06": # Ctrl+F
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.
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
):
643 addr
= self
.model
.AS
.next_addr(addr
)
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())
653 addr
= self
.model
.AS
.next_addr(addr
)
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
):
669 self
.this_addr
= this_addr
670 self
.this_subno
= this_subno
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
:
678 idx
= txt
.find(self
.search
)
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
)
684 # Don't accumulate lines
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
696 self
.search_str
= entry
.get_text()
697 if res
!= ACTION_OK
or not self
.search_str
:
699 addr
, subno
= self
.cur_addr_subno()
701 addr
, subno
= self
.next_line_addr_subno()
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())
708 self
.show_status("Not found: " + self
.search_str
)
710 elif key
== MENU_PREFS
:
713 elif key
== MENU_PLUGIN
:
714 res
= DTextEntry(30, "", title
="Plugin module name:").result()
717 self
.show_status("Running '%s' plugin..." % res
)
720 self
.show_status("Plugin '%s' ran successfully" % res
)
722 self
.show_status("Unbound key: " + repr(key
))
726 b
"g": DisasmViewer
.action_goto
,
727 b
"a": DisasmViewer
.action_make_ascii
,
735 def filter_config_line(l
):
736 l
= re
.sub(r
"#.*$", "", l
)
740 def load_symbols(fname
):
741 with
open(fname
) as f
:
743 l
= filter_config_line(l
)
746 m
= re
.search(r
"\b([A-Za-z_$.][A-Za-z0-9_$.]*)\s*=\s*((0x)?[0-9A-Fa-f]+)", l
)
749 ENTRYPOINTS
.append((m
.group(1), int(m
.group(2), 0)))
751 print("Warning: cannot parse entrypoint info from: %r" % l
)
754 # Allow undescores to separate digit groups
756 return int(s
.replace("_", ""), 0)
759 def parse_range(arg
):
763 m
= re
.match(r
"(.+?)\s*\(\s*(.+?)\s*\)", arg
)
764 start
= str2int(m
.group(1))
765 end
= start
+ str2int(m
.group(2)) - 1
767 m
= re
.match(r
"(.+)\s*-\s*(.+)", arg
)
768 start
= str2int(m
.group(1))
769 end
= str2int(m
.group(2))
773 def parse_entrypoints(f
):
775 l
= filter_config_line(l
)
780 m
= re
.match(r
'load "(.+?)"', l
)
782 load_symbols(m
.group(1))
784 label
, addr
= [v
.strip() for v
in l
.split("=")]
785 ENTRYPOINTS
.append((label
, int(addr
, 0)))
788 def parse_subareas(f
):
791 l
= filter_config_line(l
)
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()
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
):
814 with
open(fname
) as f
:
816 l
= filter_config_line(l
)
826 print("Processing section: %s" % section
)
827 if section
== "entrypoints":
828 l
= parse_entrypoints(f
)
829 elif section
== "subareas":
830 l
= parse_subareas(f
)
832 assert 0, "Unknown section: " + section
839 if l
.startswith("load"):
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
)
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 "):
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 "):
857 APP
.show_bytes
= int(args
[2])
858 elif l
.startswith("area "):
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
))
865 assert 0, "Unknown directive: " + l
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"),
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"),
890 ("Alt code (Shift+c)", b
"C"),
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)
921 key
= self
.e
.get_input()
922 if isinstance(key
, list):
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:
938 self
.menu_bar
.focus
= True
939 self
.menu_bar
.redraw()
942 res
= self
.e
.handle_input(key
)
944 if res
is not None and res
is not True:
948 def call_script(script
, progress_func
=None):
949 mod
= __import__(script
)
950 main_f
= getattr(mod
, "main", None)
953 # A script might have queues some entrypoints, etc.
955 engine
.analyze(progress_func
)
958 if __name__
== "__main__":
960 argp
= argparse
.ArgumentParser(description
="ScratchABit interactive disassembler")
961 argp
.add_argument("file", help="Input file (binary or disassembly .def)")
962 argp
.add_argument("--script", action
="append", help="Run script from file after loading environment")
963 argp
.add_argument("--save", action
="store_true", help="Save after --script and quit; don't show UI")
964 args
= argp
.parse_args()
966 # Plugin dirs are relative to the dir where scratchabit.py resides.
967 # sys.path[0] below provide absolute path of this dir, resolved for
969 plugin_dirs
= ["plugins", "plugins/cpu", "plugins/loader"]
970 for d
in plugin_dirs
:
971 sys
.path
.append(os
.path
.join(sys
.path
[0], d
))
972 log
.basicConfig(filename
="scratchabit.log", format
='%(asctime)s %(message)s', level
=log
.DEBUG
)
975 if args
.file.endswith(".def"):
976 parse_disasm_def(args
.file)
977 project_name
= args
.file.rsplit(".", 1)[0]
979 import default_plugins
980 for loader_id
in default_plugins
.loaders
:
981 loader
= __import__(loader_id
)
982 arch_id
= loader
.detect(args
.file)
986 print("Error: file '%s' not recognized by default loaders" % args
.file)
988 if arch_id
not in default_plugins
.cpus
:
989 print("Error: no plugin for CPU '%s' as detected for file '%s'" % (arch_id
, args
.file))
992 engine
.set_arch_id(arch_id
)
993 load_target_file(loader
, args
.file)
994 CPU_PLUGIN
= __import__(default_plugins
.cpus
[arch_id
])
995 project_name
= args
.file
997 p
= CPU_PLUGIN
.PROCESSOR_ENTRY()
998 if hasattr(p
, "config"):
1000 engine
.set_processor(p
)
1001 if hasattr(p
, "help_text"):
1002 help.set_cpu_help(p
.help_text
)
1004 APP
.aspace
= engine
.ADDRESS_SPACE
1006 engine
.ADDRESS_SPACE
.is_loading
= True
1008 # Calc various offset based on show_bytes value
1009 APP
.set_show_bytes(APP
.show_bytes
)
1011 # Strip suffix if any from def filename
1012 project_dir
= project_name
+ ".scratchabit"
1014 if saveload
.save_exists(project_dir
):
1015 saveload
.load_state(project_dir
)
1017 for label
, addr
in ENTRYPOINTS
:
1018 if engine
.arch_id
== "arm_32" and addr
& 1:
1020 engine
.ADDRESS_SPACE
.make_alt_code(addr
)
1021 if engine
.ADDRESS_SPACE
.is_exec(addr
):
1022 engine
.add_entrypoint(addr
)
1023 engine
.ADDRESS_SPACE
.make_unique_label(addr
, label
)
1025 sys
.stdout
.write("Performing initial analysis... %d\r" % cnt
)
1026 engine
.analyze(_progress
)
1029 #engine.print_address_map()
1033 sys
.stdout
.write("Performing analysis after running script(s)... %d\r" % cnt
)
1034 for script
in args
.script
:
1035 call_script(script
, _progress
)
1038 saveload
.save_state(project_dir
)
1042 if os
.path
.exists(project_dir
+ "/session.addr_stack"):
1043 addr_stack
= saveload
.load_addr_stack(project_dir
)
1045 show_addr
= addr_stack
.pop()
1048 show_addr
= ENTRYPOINTS
[0][1]
1049 if engine
.arch_id
== "arm_32":
1052 show_addr
= engine
.ADDRESS_SPACE
.min_addr()
1055 #_model = engine.render()
1056 _model
= engine
.render_partial_around(show_addr
, 0, HEIGHT
* 2)
1057 print("Rendering time: %fs" % (time
.time() - t
))
1058 #print(_model.lines())
1061 engine
.ADDRESS_SPACE
.is_loading
= False
1062 engine
.ADDRESS_SPACE
.changed
= False
1066 Screen
.enable_mouse()
1067 main_screen
= MainScreen()
1068 APP
.main_screen
= main_screen
1070 main_screen
.e
.set_model(_model
)
1071 main_screen
.e
.addr_stack
= addr_stack
1072 main_screen
.e
.goto_addr(show_addr
)
1073 Screen
.set_screen_redraw(main_screen
.redraw
)
1074 main_screen
.redraw()
1075 main_screen
.e
.show_status("Press F1 for help, F9 for menus")
1078 log
.exception("Unhandled exception")
1081 Screen
.goto(0, main_screen
.screen_size
[1])
1083 Screen
.disable_mouse()
1086 saveload
.save_session(project_dir
, main_screen
.e
)