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 fl
= self
.model
.AS
.get_flags(addr
, 0xff)
411 if not self
.require_non_func(fl
):
413 self
.show_status("Retracing as a function...")
414 self
.model
.AS
.make_label("fun_", addr
)
415 engine
.add_entrypoint(addr
, True)
416 engine
.analyze(self
.analyze_status
)
418 self
.show_status("Retraced as a function")
420 elif key
== MENU_ADD_TO_FUNC
:
421 addr
= self
.cur_addr()
422 if actions
.add_code_to_func(APP
, addr
):
426 addr
= self
.cur_addr()
427 fl
= self
.model
.AS
.get_flags(addr
)
428 if not self
.expect_flags(fl
, (self
.model
.AS
.DATA
, self
.model
.AS
.UNK
)):
430 if fl
== self
.model
.AS
.UNK
:
431 self
.model
.AS
.set_flags(addr
, 1, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
)
433 sz
= self
.model
.AS
.get_unit_size(addr
)
434 self
.model
.undefine_unit(addr
)
437 self
.model
.AS
.set_flags(addr
, sz
, self
.model
.AS
.DATA
, self
.model
.AS
.DATA_CONT
)
440 addr
= self
.cur_addr()
441 fl
= self
.model
.AS
.get_flags(addr
)
442 if not self
.expect_flags(fl
, (self
.model
.AS
.UNK
,)):
445 off
, area
= self
.model
.AS
.addr2area(self
.cur_addr())
446 # Don't cross area boundaries with filler
447 remaining
= area
[engine
.END
] - addr
+ 1
451 fl
= self
.model
.AS
.get_flags(addr
)
452 except engine
.InvalidAddrException
:
454 if fl
!= self
.model
.AS
.UNK
:
456 b
= self
.model
.AS
.get_byte(addr
)
457 if b
not in (0, 0xff):
458 self
.show_status("Filler must consist of 0x00 or 0xff")
464 self
.model
.AS
.make_filler(self
.cur_addr(), sz
)
468 addr
= self
.cur_addr()
469 self
.model
.undefine_unit(addr
)
473 op_no
= self
.cur_operand_no(self
.get_cur_line())
475 addr
= self
.cur_addr()
476 subtype
= self
.model
.AS
.get_arg_prop(addr
, op_no
, "subtype")
477 if subtype
!= engine
.IMM_ADDR
:
479 engine
.IMM_UHEX
: engine
.IMM_UDEC
,
480 engine
.IMM_UDEC
: engine
.IMM_UHEX
,
482 self
.model
.AS
.set_arg_prop(addr
, op_no
, "subtype", next_subtype
[subtype
])
484 self
.show_status("Changed arg #%d to %s" % (op_no
, next_subtype
[subtype
]))
486 addr
= self
.cur_addr()
487 line
= self
.get_cur_line()
488 o
= line
.get_operand_addr()
490 self
.show_status("Cannot convert operand to offset")
492 if o
.type != idaapi
.o_imm
or not self
.model
.AS
.is_valid_addr(o
.get_addr()):
493 self
.show_status("Cannot convert operand to offset: #%s: %s" % (o
.n
, o
.type))
496 if self
.model
.AS
.get_arg_prop(addr
, o
.n
, "subtype") == engine
.IMM_ADDR
:
497 self
.model
.AS
.unmake_arg_offset(addr
, o
.n
, o
.get_addr())
499 self
.model
.AS
.make_arg_offset(addr
, o
.n
, o
.get_addr())
500 self
.update_model(True)
502 addr
= self
.cur_addr()
503 comment
= self
.model
.AS
.get_comment(addr
) or ""
504 res
= DMultiEntry(60, 5, comment
.split("\n"), title
="Comment:").result()
505 if res
!= ACTION_CANCEL
:
506 res
= "\n".join(res
).rstrip("\n")
507 self
.model
.AS
.set_comment(addr
, res
)
512 addr
= self
.cur_addr()
513 label
= self
.model
.AS
.get_label(addr
)
514 def_label
= self
.model
.AS
.get_default_label(addr
)
515 s
= label
or def_label
517 res
= DTextEntry(30, s
, title
="New label:").result()
523 if self
.model
.AS
.label_exists(res
):
525 self
.show_status("Duplicate label")
527 self
.model
.AS
.set_label(addr
, res
)
529 # If it's new label, we need to add it to model
535 elif key
== editor
.KEY_F1
:
539 self
.show_status("Saving...")
540 saveload
.save_state(project_dir
)
541 self
.model
.AS
.changed
= False
542 self
.show_status("Saved.")
543 elif key
== b
"\x11": # ^Q
544 class IssueList(WListBox
):
545 def render_line(self
, l
):
547 d
= Dialog(4, 4, title
="Problems list")
548 lw
= IssueList(40, 16, self
.model
.AS
.get_issues())
549 lw
.finish_dialog
= ACTION_OK
554 val
= lw
.get_cur_line()
556 self
.goto_addr(val
[0], from_addr
=self
.cur_addr())
559 off
, area
= self
.model
.AS
.addr2area(self
.cur_addr())
560 props
= area
[engine
.PROPS
]
561 percent
= 100 * off
/ (area
[engine
.END
] - area
[engine
.START
] + 1)
562 func
= self
.model
.AS
.lookup_func(self
.cur_addr())
563 func
= self
.model
.AS
.get_label(func
.start
) if func
else None
564 status
= "Area: 0x%x %s (%s): %.1f%%, func: %s" % (
565 area
[engine
.START
], props
.get("name", "noname"), props
["access"], percent
, func
567 subarea
= self
.model
.AS
.lookup_subarea(self
.cur_addr())
569 status
+= ", subarea: " + subarea
[2]
570 self
.show_status(status
)
572 from scratchabit
import memmap
573 addr
= memmap
.show(self
.model
.AS
, self
.cur_addr())
575 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
578 out_fname
= "out.lst"
579 with
open(out_fname
, "w") as f
:
580 engine
.render_partial(actions
.TextSaveModel(f
, self
), 0, 0, 10000000)
581 self
.show_status("Disassembly listing written: " + out_fname
)
582 elif key
== MENU_WRITE_ALL_HTML
:
583 out_fname
= "out.html"
584 with
open(out_fname
, "w") as f
:
586 m
= actions
.HTMLSaveModel(f
, self
)
587 m
.aspace
= self
.model
.AS
588 engine
.render_partial(m
, 0, 0, 10000000)
590 self
.show_status("Disassembly HTML listing written: " + out_fname
)
591 elif key
== b
"\x17": # Ctrl+W
592 outfile
= actions
.write_func_by_addr(APP
, self
.cur_addr(), feedback_obj
=self
)
594 self
.show_status("Wrote file: %s" % outfile
)
595 elif key
== b
"\x15": # Ctrl+U
597 addr
= self
.cur_addr()
598 flags
= self
.model
.AS
.get_flags(addr
)
599 if flags
== self
.model
.AS
.UNK
:
600 # If already on undefined, skip the current stride of them,
601 # as they indeed go in batches.
603 flags
= self
.model
.AS
.get_flags(addr
)
604 if flags
!= self
.model
.AS
.UNK
:
606 addr
= self
.model
.AS
.next_addr(addr
)
612 flags
= self
.model
.AS
.get_flags(addr
)
613 if flags
== self
.model
.AS
.UNK
:
614 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
616 addr
= self
.model
.AS
.next_addr(addr
)
621 self
.show_status("There're no further undefined strides")
623 elif key
== b
"\x06": # Ctrl+F
625 addr
= self
.cur_addr()
626 flags
= self
.model
.AS
.get_flags(addr
, ~ADDRESS_SPACE
.ALT_CODE
)
627 if flags
== self
.model
.AS
.CODE
:
628 # If already on non-func code, skip the current stride of it,
629 # as it indeed go in batches.
631 flags
= self
.model
.AS
.get_flags(addr
, ~ADDRESS_SPACE
.ALT_CODE
)
632 self
.show_status("fl=%x" % flags
)
633 if flags
not in (self
.model
.AS
.CODE
, self
.model
.AS
.CODE_CONT
):
635 addr
= self
.model
.AS
.next_addr(addr
)
641 flags
= self
.model
.AS
.get_flags(addr
, ~ADDRESS_SPACE
.ALT_CODE
)
642 if flags
== self
.model
.AS
.CODE
:
643 self
.goto_addr(addr
, from_addr
=self
.cur_addr())
645 addr
= self
.model
.AS
.next_addr(addr
)
650 self
.show_status("There're no further non-function code strides")
652 elif key
in (b
"/", b
"?"): # "/" and Shift+"/"
654 class FoundException(Exception): pass
656 class TextSearchModel(engine
.Model
):
657 def __init__(self
, substr
, ctrl
, this_addr
, this_subno
):
661 self
.this_addr
= this_addr
662 self
.this_subno
= this_subno
664 def add_object(self
, addr
, line
):
665 super().add_object(addr
, line
)
666 # Skip virtual lines before the line from which we started
667 if addr
== self
.this_addr
and line
.subno
< self
.this_subno
:
670 idx
= txt
.find(self
.search
)
672 raise FoundException((addr
, line
.subno
), idx
+ line
.LEADER_SIZE
+ len(line
.indent
))
673 if self
.cnt
% 256 == 0:
674 self
.ctrl
.show_status("Searching: 0x%x" % addr
)
676 # Don't accumulate lines
681 d
= Dialog(4, 4, title
="Text Search")
682 d
.add(1, 1, WLabel("Search for:"))
683 entry
= WTextEntry(20, self
.search_str
)
684 entry
.finish_dialog
= ACTION_OK
688 self
.search_str
= entry
.get_text()
689 if res
!= ACTION_OK
or not self
.search_str
:
691 addr
, subno
= self
.cur_addr_subno()
693 addr
, subno
= self
.next_line_addr_subno()
696 engine
.render_from(TextSearchModel(self
.search_str
, self
, addr
, subno
), addr
, 10000000)
697 except FoundException
as res
:
698 self
.goto_addr(res
.args
[0], col
=res
.args
[1], from_addr
=self
.cur_addr())
700 self
.show_status("Not found: " + self
.search_str
)
702 elif key
== MENU_PREFS
:
705 elif key
== MENU_PLUGIN
:
706 res
= DTextEntry(30, "", title
="Plugin module name:").result()
709 self
.show_status("Running '%s' plugin..." % res
)
712 self
.show_status("Plugin '%s' ran successfully" % res
)
714 self
.show_status("Unbound key: " + repr(key
))
718 b
"g": DisasmViewer
.action_goto
,
719 b
"a": DisasmViewer
.action_make_ascii
,
727 def filter_config_line(l
):
728 l
= re
.sub(r
"#.*$", "", l
)
732 def load_symbols(fname
):
733 with
open(fname
) as f
:
735 l
= filter_config_line(l
)
738 m
= re
.search(r
"\b([A-Za-z_$.][A-Za-z0-9_$.]*)\s*=\s*((0x)?[0-9A-Fa-f]+)", l
)
741 ENTRYPOINTS
.append((m
.group(1), int(m
.group(2), 0)))
743 print("Warning: cannot parse entrypoint info from: %r" % l
)
746 # Allow undescores to separate digit groups
748 return int(s
.replace("_", ""), 0)
751 def parse_range(arg
):
755 m
= re
.match(r
"(.+?)\s*\(\s*(.+?)\s*\)", arg
)
756 start
= str2int(m
.group(1))
757 end
= start
+ str2int(m
.group(2)) - 1
759 m
= re
.match(r
"(.+)\s*-\s*(.+)", arg
)
760 start
= str2int(m
.group(1))
761 end
= str2int(m
.group(2))
765 def parse_entrypoints(f
):
767 l
= filter_config_line(l
)
772 m
= re
.match(r
'load "(.+?)"', l
)
774 load_symbols(m
.group(1))
776 label
, addr
= [v
.strip() for v
in l
.split("=")]
777 ENTRYPOINTS
.append((label
, int(addr
, 0)))
780 def parse_subareas(f
):
783 l
= filter_config_line(l
)
790 assert len(args
) == 2
791 start
, end
= parse_range(args
[1])
792 engine
.ADDRESS_SPACE
.add_subarea(start
, end
, args
[0])
793 engine
.ADDRESS_SPACE
.finish_subareas()
797 def load_target_file(loader
, fname
):
798 entry
= loader
.load(engine
.ADDRESS_SPACE
, fname
)
799 log
.info("Loaded %s, entrypoint: %s", fname
, hex(entry
) if entry
is not None else None)
800 if entry
is not None:
801 ENTRYPOINTS
.append(("_ENTRY_", entry
))
804 def parse_disasm_def(fname
):
806 with
open(fname
) as f
:
808 l
= filter_config_line(l
)
818 print("Processing section: %s" % section
)
819 if section
== "entrypoints":
820 l
= parse_entrypoints(f
)
821 elif section
== "subareas":
822 l
= parse_subareas(f
)
824 assert 0, "Unknown section: " + section
831 if l
.startswith("load"):
833 if args
[2][0] in string
.digits
:
834 addr
= int(args
[2], 0)
835 print("Loading %s @0x%x" % (args
[1], addr
))
836 engine
.ADDRESS_SPACE
.load_content(open(args
[1], "rb"), addr
)
838 print("Loading %s (%s plugin)" % (args
[1], args
[2]))
839 loader
= __import__(args
[2])
840 load_target_file(loader
, args
[1])
841 elif l
.startswith("cpu "):
843 CPU_PLUGIN
= __import__(args
[1])
844 if hasattr(CPU_PLUGIN
, "arch_id"):
845 engine
.set_arch_id(CPU_PLUGIN
.arch_id
)
846 print("Loading CPU plugin %s" % (args
[1]))
847 elif l
.startswith("show bytes "):
849 APP
.show_bytes
= int(args
[2])
850 elif l
.startswith("area "):
852 assert len(args
) == 4
853 start
, end
= parse_range(args
[2])
854 a
= engine
.ADDRESS_SPACE
.add_area(start
, end
, {"name": args
[1], "access": args
[3].upper()})
855 print("Adding area: %s" % engine
.str_area(a
))
857 assert 0, "Unknown directive: " + l
863 self
.screen_size
= Screen
.screen_size()
864 self
.e
= DisasmViewer(1, 2, self
.screen_size
[0] - 2, self
.screen_size
[1] - 4)
866 menu_file
= WMenuBox([
867 ("Save (Shift+s)", b
"S"),
868 ("Write disasm (Shift+w)", b
"W"),
869 ("Write disasm in HTML", MENU_WRITE_ALL_HTML
),
870 ("Write function (Ctrl+w)", b
"\x17"),
873 menu_goto
= WMenuBox([
874 ("Follow (Enter)", KEY_ENTER
), ("Return (Esc)", KEY_ESC
),
875 ("Goto... (g)", b
"g"), ("Search disasm... (/)", b
"/"),
876 ("Search next (Shift+/)", b
"?"), ("Next undefined (Ctrl+u)", b
"\x15"),
877 ("Next non-function code (Ctrl+f)", b
"\x06"),
879 menu_edit
= WMenuBox([
880 ("Undefined (u)", b
"u"), ("Code (c)", b
"c"), ("Data (d)", b
"d"),
881 ("ASCII String (a)", b
"a"), ("Filler (f)", b
"f"), ("Make label (n)", b
"n"),
882 ("Mark function start (F)", b
"F"), ("Add code to function", MENU_ADD_TO_FUNC
),
883 ("Number/Address (o)", b
"o"), ("Hex/dec (h)", b
"h"),
885 menu_analysis
= WMenuBox([
886 ("Info (whereami) (i)", b
"i"), ("Memory map (Shift+i)", b
"I"),
887 ("Run plugin...", MENU_PLUGIN
),
888 ("Preferences...", MENU_PREFS
),
890 menu_help
= WMenuBox([
891 ("Help (F1)", KEY_F1
), ("About...", "about"),
893 self
.menu_bar
= WMenuBar([
894 ("File", menu_file
), ("Goto", menu_goto
), ("Edit", menu_edit
),
895 ("Analysis", menu_analysis
), ("Help", menu_help
)
897 self
.menu_bar
.permanent
= True
899 def redraw(self
, allow_cursor
=True):
900 self
.menu_bar
.redraw()
901 self
.e
.attr_color(C_B_WHITE
, C_BLUE
)
902 self
.e
.draw_box(0, 1, self
.screen_size
[0], self
.screen_size
[1] - 2)
910 key
= self
.e
.get_input()
911 if isinstance(key
, list):
913 if self
.menu_bar
.inside(x
, y
):
914 self
.menu_bar
.focus
= True
916 if self
.menu_bar
.focus
:
917 res
= self
.menu_bar
.handle_input(key
)
918 if res
== ACTION_CANCEL
:
919 self
.menu_bar
.focus
= False
920 elif res
is not None and res
is not True:
922 res
= self
.e
.handle_input(res
)
923 if res
is not None and res
is not True:
927 self
.menu_bar
.focus
= True
928 self
.menu_bar
.redraw()
931 res
= self
.e
.handle_input(key
)
933 if res
is not None and res
is not True:
937 def call_script(script
):
938 mod
= __import__(script
)
939 main_f
= getattr(mod
, "main", None)
944 if __name__
== "__main__":
946 argp
= argparse
.ArgumentParser(description
="ScratchABit interactive disassembler")
947 argp
.add_argument("file", help="Input file (binary or disassembly .def)")
948 argp
.add_argument("--script", action
="append", help="Run script from file after loading environment")
949 argp
.add_argument("--save", action
="store_true", help="Save after --script and quit; don't show UI")
950 args
= argp
.parse_args()
952 # Plugin dirs are relative to the dir where scratchabit.py resides.
953 # sys.path[0] below provide absolute path of this dir, resolved for
955 plugin_dirs
= ["plugins", "plugins/cpu", "plugins/loader"]
956 for d
in plugin_dirs
:
957 sys
.path
.append(os
.path
.join(sys
.path
[0], d
))
958 log
.basicConfig(filename
="scratchabit.log", format
='%(asctime)s %(message)s', level
=log
.DEBUG
)
961 if args
.file.endswith(".def"):
962 parse_disasm_def(args
.file)
963 project_name
= args
.file.rsplit(".", 1)[0]
965 import default_plugins
966 for loader_id
in default_plugins
.loaders
:
967 loader
= __import__(loader_id
)
968 arch_id
= loader
.detect(args
.file)
972 print("Error: file '%s' not recognized by default loaders" % args
.file)
974 if arch_id
not in default_plugins
.cpus
:
975 print("Error: no plugin for CPU '%s' as detected for file '%s'" % (arch_id
, args
.file))
978 engine
.set_arch_id(arch_id
)
979 load_target_file(loader
, args
.file)
980 CPU_PLUGIN
= __import__(default_plugins
.cpus
[arch_id
])
981 project_name
= args
.file
983 p
= CPU_PLUGIN
.PROCESSOR_ENTRY()
984 if hasattr(p
, "config"):
986 engine
.set_processor(p
)
987 if hasattr(p
, "help_text"):
988 help.set_cpu_help(p
.help_text
)
990 APP
.aspace
= engine
.ADDRESS_SPACE
992 engine
.ADDRESS_SPACE
.is_loading
= True
994 # Calc various offset based on show_bytes value
995 APP
.set_show_bytes(APP
.show_bytes
)
997 # Strip suffix if any from def filename
998 project_dir
= project_name
+ ".scratchabit"
1000 if saveload
.save_exists(project_dir
):
1001 saveload
.load_state(project_dir
)
1003 for label
, addr
in ENTRYPOINTS
:
1004 addr
&= engine
.code_addr_mask
1005 if engine
.ADDRESS_SPACE
.is_exec(addr
):
1006 engine
.add_entrypoint(addr
)
1007 engine
.ADDRESS_SPACE
.make_unique_label(addr
, label
)
1009 sys
.stdout
.write("Performing initial analysis... %d\r" % cnt
)
1010 engine
.analyze(_progress
)
1013 #engine.print_address_map()
1016 for script
in args
.script
:
1019 saveload
.save_state(project_dir
)
1023 if os
.path
.exists(project_dir
+ "/session.addr_stack"):
1024 addr_stack
= saveload
.load_addr_stack(project_dir
)
1026 show_addr
= addr_stack
.pop()
1029 show_addr
= ENTRYPOINTS
[0][1]
1031 show_addr
= engine
.ADDRESS_SPACE
.min_addr()
1034 #_model = engine.render()
1035 _model
= engine
.render_partial_around(show_addr
, 0, HEIGHT
* 2)
1036 print("Rendering time: %fs" % (time
.time() - t
))
1037 #print(_model.lines())
1040 engine
.ADDRESS_SPACE
.is_loading
= False
1041 engine
.ADDRESS_SPACE
.changed
= False
1045 Screen
.enable_mouse()
1046 main_screen
= MainScreen()
1047 APP
.main_screen
= main_screen
1049 main_screen
.e
.set_model(_model
)
1050 main_screen
.e
.addr_stack
= addr_stack
1051 main_screen
.e
.goto_addr(show_addr
)
1052 Screen
.set_screen_redraw(main_screen
.redraw
)
1053 main_screen
.redraw()
1054 main_screen
.e
.show_status("Press F1 for help, F9 for menus")
1057 log
.exception("Unhandled exception")
1060 Screen
.goto(0, main_screen
.screen_size
[1])
1062 Screen
.disable_mouse()
1065 saveload
.save_session(project_dir
, main_screen
.e
)