Index: tools/grokdump.py |
diff --git a/tools/grokdump.py b/tools/grokdump.py |
index a5a2ae08a879b11447352a262e02222da4e7933b..8178b2f0cfef2c032208002d2ebd74eb48c587d5 100755 |
--- a/tools/grokdump.py |
+++ b/tools/grokdump.py |
@@ -27,7 +27,9 @@ |
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+import BaseHTTPServer |
import bisect |
+import cgi |
import cmd |
import codecs |
import ctypes |
@@ -37,10 +39,15 @@ import mmap |
import optparse |
import os |
import re |
-import struct |
import sys |
import types |
+import urllib |
+import urlparse |
import v8heapconst |
+import webbrowser |
+ |
+PORT_NUMBER = 8081 |
+ |
USAGE="""usage: %prog [OPTIONS] [DUMP-FILE] |
@@ -701,6 +708,20 @@ class MinidumpReader(object): |
reader.FormatIntPtr(word)) |
self.ForEachMemoryRegion(search_inside_region) |
+ def FindWordList(self, word): |
+ aligned_res = [] |
+ unaligned_res = [] |
+ def search_inside_region(reader, start, size, location): |
+ for loc in xrange(location, location + size - self.PointerSize()): |
+ if reader._ReadWord(loc) == word: |
+ slot = start + (loc - location) |
+ if slot % self.PointerSize() == 0: |
+ aligned_res.append(slot) |
+ else: |
+ unaligned_res.append(slot) |
+ self.ForEachMemoryRegion(search_inside_region) |
+ return (aligned_res, unaligned_res) |
+ |
def FindLocation(self, address): |
offset = 0 |
if self.memory_list64 is not None: |
@@ -930,8 +951,9 @@ class HeapObject(object): |
def SmiField(self, offset): |
field_value = self.heap.reader.ReadUIntPtr(self.address + offset) |
- assert (field_value & 1) == 0 |
- return field_value / 2 |
+ if (field_value & 1) == 0: |
+ return field_value / 2 |
+ return None |
class Map(HeapObject): |
@@ -1348,7 +1370,9 @@ class JSFunction(HeapObject): |
if not self.shared.script.Is(Script): return source |
script_source = self.shared.script.source |
if not script_source.Is(String): return source |
- return script_source.GetChars()[start:end] |
+ if start and end: |
+ source = script_source.GetChars()[start:end] |
+ return source |
class SharedFunctionInfo(HeapObject): |
@@ -1382,7 +1406,10 @@ class SharedFunctionInfo(HeapObject): |
else: |
start_position_and_type = \ |
self.SmiField(self.StartPositionAndTypeOffset()) |
- self.start_position = start_position_and_type >> 2 |
+ if start_position_and_type: |
+ self.start_position = start_position_and_type >> 2 |
+ else: |
+ self.start_position = None |
self.end_position = \ |
self.SmiField(self.EndPositionOffset()) |
@@ -1561,6 +1588,79 @@ class KnownMap(HeapObject): |
return "<%s>" % self.known_name |
+COMMENT_RE = re.compile(r"^C (0x[0-9a-fA-F]+) (.*)$") |
+PAGEADDRESS_RE = re.compile( |
+ r"^P (mappage|pointerpage|datapage) (0x[0-9a-fA-F]+)$") |
+ |
+ |
+class InspectionInfo(object): |
+ def __init__(self, minidump_name, reader): |
+ self.comment_file = minidump_name + ".comments" |
+ self.address_comments = {} |
+ self.page_address = {} |
+ if os.path.exists(self.comment_file): |
+ with open(self.comment_file, "r") as f: |
+ lines = f.readlines() |
+ f.close() |
+ |
+ for l in lines: |
+ m = COMMENT_RE.match(l) |
+ if m: |
+ self.address_comments[int(m.group(1), 0)] = m.group(2) |
+ m = PAGEADDRESS_RE.match(l) |
+ if m: |
+ self.page_address[m.group(1)] = int(m.group(2), 0) |
+ self.reader = reader |
+ self.styles = {} |
+ self.color_addresses() |
+ return |
+ |
+ def get_page_address(self, page_kind): |
+ return self.page_address.get(page_kind, 0) |
+ |
+ def save_page_address(self, page_kind, address): |
+ with open(self.comment_file, "a") as f: |
+ f.write("P %s 0x%x\n" % (page_kind, address)) |
+ f.close() |
+ |
+ def color_addresses(self): |
+ # Color all stack addresses. |
+ exception_thread = self.reader.thread_map[self.reader.exception.thread_id] |
+ stack_top = self.reader.ExceptionSP() |
+ stack_bottom = exception_thread.stack.start + \ |
+ exception_thread.stack.memory.data_size |
+ frame_pointer = self.reader.ExceptionFP() |
+ self.styles[frame_pointer] = "frame" |
+ for slot in xrange(stack_top, stack_bottom, self.reader.PointerSize()): |
+ self.styles[slot] = "stackaddress" |
+ for slot in xrange(stack_top, stack_bottom, self.reader.PointerSize()): |
+ maybe_address = self.reader.ReadUIntPtr(slot) |
+ self.styles[maybe_address] = "stackval" |
+ if slot == frame_pointer: |
+ self.styles[slot] = "frame" |
+ frame_pointer = maybe_address |
+ self.styles[self.reader.ExceptionIP()] = "pc" |
+ |
+ def get_style_class(self, address): |
+ return self.styles.get(address, None) |
+ |
+ def get_style_class_string(self, address): |
+ style = self.get_style_class(address) |
+ if style != None: |
+ return " class=\"%s\" " % style |
+ else: |
+ return "" |
+ |
+ def set_comment(self, address, comment): |
+ self.address_comments[address] = comment |
+ with open(self.comment_file, "a") as f: |
+ f.write("C 0x%x %s\n" % (address, comment)) |
+ f.close() |
+ |
+ def get_comment(self, address): |
+ return self.address_comments.get(address, "") |
+ |
+ |
class InspectionPadawan(object): |
"""The padawan can improve annotations by sensing well-known objects.""" |
def __init__(self, reader, heap): |
@@ -1653,6 +1753,1033 @@ class InspectionPadawan(object): |
self.reader.FormatIntPtr(self.known_first_data_page), |
self.reader.FormatIntPtr(self.known_first_pointer_page)) |
+WEB_HEADER = """ |
+<!DOCTYPE html> |
+<html> |
+<head> |
+<meta content="text/html; charset=utf-8" http-equiv="content-type"> |
+<style media="screen" type="text/css"> |
+ |
+.code { |
+ font-family: monospace; |
+} |
+ |
+.dmptable { |
+ border-collapse : collapse; |
+ border-spacing : 0px; |
+} |
+ |
+.codedump { |
+ border-collapse : collapse; |
+ border-spacing : 0px; |
+} |
+ |
+.addrcomments { |
+ border : 0px; |
+} |
+ |
+.register { |
+ padding-right : 1em; |
+} |
+ |
+.header { |
+ clear : both; |
+} |
+ |
+.header .navigation { |
+ float : left; |
+} |
+ |
+.header .dumpname { |
+ float : right; |
+} |
+ |
+tr.highlight-line { |
+ background-color : yellow; |
+} |
+ |
+.highlight { |
+ background-color : magenta; |
+} |
+ |
+tr.inexact-highlight-line { |
+ background-color : pink; |
+} |
+ |
+input { |
+ background-color: inherit; |
+ border: 1px solid LightGray; |
+} |
+ |
+.dumpcomments { |
+ border : 1px solid LightGray; |
+ width : 32em; |
+} |
+ |
+.regions td { |
+ padding:0 15px 0 15px; |
+} |
+ |
+.stackframe td { |
+ background-color : cyan; |
+} |
+ |
+.stackaddress { |
+ background-color : LightGray; |
+} |
+ |
+.stackval { |
+ background-color : LightCyan; |
+} |
+ |
+.frame { |
+ background-color : cyan; |
+} |
+ |
+.commentinput { |
+ width : 20em; |
+} |
+ |
+a.nodump:visited { |
+ color : black; |
+ text-decoration : none; |
+} |
+ |
+a.nodump:link { |
+ color : black; |
+ text-decoration : none; |
+} |
+ |
+a:visited { |
+ color : blueviolet; |
+} |
+ |
+a:link { |
+ color : blue; |
+} |
+ |
+.disasmcomment { |
+ color : DarkGreen; |
+} |
+ |
+</style> |
+ |
+<script type="application/javascript"> |
+ |
+var address_str = "address-"; |
+var address_len = address_str.length; |
+ |
+function comment() { |
+ var s = event.srcElement.id; |
+ var index = s.indexOf(address_str); |
+ if (index >= 0) { |
+ send_comment(s.substring(index + address_len), event.srcElement.value); |
+ } |
+} |
+ |
+function send_comment(address, comment) { |
+ xmlhttp = new XMLHttpRequest(); |
+ address = encodeURIComponent(address) |
+ comment = encodeURIComponent(comment) |
+ xmlhttp.open("GET", |
+ "setcomment?%(query_dump)s&address=" + address + |
+ "&comment=" + comment, true); |
+ xmlhttp.send(); |
+} |
+ |
+var dump_str = "dump-"; |
+var dump_len = dump_str.length; |
+ |
+function dump_comment() { |
+ var s = event.srcElement.id; |
+ var index = s.indexOf(dump_str); |
+ if (index >= 0) { |
+ send_dump_desc(s.substring(index + dump_len), event.srcElement.value); |
+ } |
+} |
+ |
+function send_dump_desc(name, desc) { |
+ xmlhttp = new XMLHttpRequest(); |
+ name = encodeURIComponent(name) |
+ desc = encodeURIComponent(desc) |
+ xmlhttp.open("GET", |
+ "setdumpdesc?dump=" + name + |
+ "&description=" + desc, true); |
+ xmlhttp.send(); |
+} |
+ |
+function onpage(kind, address) { |
+ xmlhttp = new XMLHttpRequest(); |
+ kind = encodeURIComponent(kind) |
+ address = encodeURIComponent(address) |
+ xmlhttp.onreadystatechange = function() { |
+ if (xmlhttp.readyState==4 && xmlhttp.status==200) { |
+ location.reload(true) |
+ } |
+ }; |
+ xmlhttp.open("GET", |
+ "setpageaddress?%(query_dump)s&kind=" + kind + |
+ "&address=" + address); |
+ xmlhttp.send(); |
+} |
+ |
+</script> |
+ |
+<title>Dump %(dump_name)s</title> |
+</head> |
+ |
+<body> |
+ <div class="header"> |
+ <form class="navigation" action="search.html"> |
+ <a href="summary.html?%(query_dump)s">Context info</a> |
+ <a href="info.html?%(query_dump)s">Dump info</a> |
+ <a href="modules.html?%(query_dump)s">Modules</a> |
+ |
+ <input type="search" name="val"> |
+ <input type="submit" name="search" value="Search"> |
+ <input type="hidden" name="dump" value="%(dump_name)s"> |
+ </form> |
+ <form class="navigation" action="disasm.html#highlight"> |
+ |
+ |
+ |
+ <input type="search" name="val"> |
+ <input type="submit" name="disasm" value="Disasm"> |
+ |
+ |
+ |
+ <a href="dumps.html">Dumps...</a> |
+ </form> |
+ </div> |
+ <br> |
+ <hr> |
+""" |
+ |
+ |
+WEB_FOOTER = """ |
+</body> |
+</html> |
+""" |
+ |
+ |
+class WebParameterError(Exception): |
+ def __init__(self, message): |
+ Exception.__init__(self, message) |
+ |
+ |
+class InspectionWebHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
+ def formatter(self, query_components): |
+ name = query_components.get("dump", [None])[0] |
+ return self.server.get_dump_formatter(name) |
+ |
+ def send_success_html_headers(self): |
+ self.send_response(200) |
+ self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") |
+ self.send_header("Pragma", "no-cache") |
+ self.send_header("Expires", "0") |
+ self.send_header('Content-type','text/html') |
+ self.end_headers() |
+ return |
+ |
+ def do_GET(self): |
+ try: |
+ parsedurl = urlparse.urlparse(self.path) |
+ query_components = urlparse.parse_qs(parsedurl.query) |
+ if parsedurl.path == "/dumps.html": |
+ self.send_success_html_headers() |
+ self.server.output_dumps(self.wfile) |
+ elif parsedurl.path == "/summary.html": |
+ self.send_success_html_headers() |
+ self.formatter(query_components).output_summary(self.wfile) |
+ elif parsedurl.path == "/info.html": |
+ self.send_success_html_headers() |
+ self.formatter(query_components).output_info(self.wfile) |
+ elif parsedurl.path == "/modules.html": |
+ self.send_success_html_headers() |
+ self.formatter(query_components).output_modules(self.wfile) |
+ elif parsedurl.path == "/search.html": |
+ address = query_components.get("val", []) |
+ if len(address) != 1: |
+ self.send_error(404, "Invalid params") |
+ return |
+ self.send_success_html_headers() |
+ self.formatter(query_components).output_search_res( |
+ self.wfile, address[0]) |
+ elif parsedurl.path == "/disasm.html": |
+ address = query_components.get("val", []) |
+ exact = query_components.get("exact", ["on"]) |
+ if len(address) != 1: |
+ self.send_error(404, "Invalid params") |
+ return |
+ self.send_success_html_headers() |
+ self.formatter(query_components).output_disasm( |
+ self.wfile, address[0], exact[0]) |
+ elif parsedurl.path == "/data.html": |
+ address = query_components.get("val", []) |
+ datakind = query_components.get("type", ["address"]) |
+ if len(address) == 1 and len(datakind) == 1: |
+ self.send_success_html_headers() |
+ self.formatter(query_components).output_data( |
+ self.wfile, address[0], datakind[0]) |
+ else: |
+ self.send_error(404,'Invalid params') |
+ elif parsedurl.path == "/setdumpdesc": |
+ name = query_components.get("dump", [""]) |
+ description = query_components.get("description", [""]) |
+ if len(name) == 1 and len(description) == 1: |
+ name = name[0] |
+ description = description[0] |
+ if self.server.set_dump_desc(name, description): |
+ self.send_success_html_headers() |
+ self.wfile.write("OK") |
+ return |
+ self.send_error(404,'Invalid params') |
+ elif parsedurl.path == "/setcomment": |
+ address = query_components.get("address", []) |
+ comment = query_components.get("comment", [""]) |
+ if len(address) == 1 and len(comment) == 1: |
+ address = address[0] |
+ comment = comment[0] |
+ self.formatter(query_components).set_comment(address, comment) |
+ self.send_success_html_headers() |
+ self.wfile.write("OK") |
+ else: |
+ self.send_error(404,'Invalid params') |
+ elif parsedurl.path == "/setpageaddress": |
+ kind = query_components.get("kind", []) |
+ address = query_components.get("address", [""]) |
+ if len(kind) == 1 and len(address) == 1: |
+ kind = kind[0] |
+ address = address[0] |
+ self.formatter(query_components).set_page_address(kind, address) |
+ self.send_success_html_headers() |
+ self.wfile.write("OK") |
+ else: |
+ self.send_error(404,'Invalid params') |
+ else: |
+ self.send_error(404,'File Not Found: %s' % self.path) |
+ |
+ except IOError: |
+ self.send_error(404,'File Not Found: %s' % self.path) |
+ |
+ except WebParameterError as e: |
+ self.send_error(404, 'Web parameter error: %s' % e.message) |
+ |
+ |
+HTML_REG_FORMAT = "<span class=\"register\"><b>%s</b>: %s</span>\n" |
+ |
+ |
+class InspectionWebFormatter(object): |
+ CONTEXT_FULL = 0 |
+ CONTEXT_SHORT = 1 |
+ |
+ def __init__(self, switches, minidump_name, http_server): |
+ self.dumpfilename = os.path.split(minidump_name)[1] |
+ self.encfilename = urllib.urlencode({ 'dump' : self.dumpfilename }) |
+ self.reader = MinidumpReader(switches, minidump_name) |
+ self.server = http_server |
+ |
+ # Set up the heap |
+ exception_thread = self.reader.thread_map[self.reader.exception.thread_id] |
+ stack_top = self.reader.ExceptionSP() |
+ stack_bottom = exception_thread.stack.start + \ |
+ exception_thread.stack.memory.data_size |
+ stack_map = {self.reader.ExceptionIP(): -1} |
+ for slot in xrange(stack_top, stack_bottom, self.reader.PointerSize()): |
+ maybe_address = self.reader.ReadUIntPtr(slot) |
+ if not maybe_address in stack_map: |
+ stack_map[maybe_address] = slot |
+ self.heap = V8Heap(self.reader, stack_map) |
+ |
+ self.padawan = InspectionPadawan(self.reader, self.heap) |
+ self.comments = InspectionInfo(minidump_name, self.reader) |
+ self.padawan.known_first_data_page = ( |
+ self.comments.get_page_address("datapage")) |
+ self.padawan.known_first_map_page = ( |
+ self.comments.get_page_address("mappage")) |
+ self.padawan.known_first_pointer_page = ( |
+ self.comments.get_page_address("pointerpage")) |
+ |
+ def set_comment(self, straddress, comment): |
+ try: |
+ address = int(straddress, 0) |
+ self.comments.set_comment(address, comment) |
+ except ValueError: |
+ print "Invalid address" |
+ |
+ def set_page_address(self, kind, straddress): |
+ try: |
+ address = int(straddress, 0) |
+ if kind == "datapage": |
+ self.padawan.known_first_data_page = address |
+ elif kind == "mappage": |
+ self.padawan.known_first_map_page = address |
+ elif kind == "pointerpage": |
+ self.padawan.known_first_pointer_page = address |
+ self.comments.save_page_address(kind, address) |
+ except ValueError: |
+ print "Invalid address" |
+ |
+ def td_from_address(self, f, address): |
+ f.write("<td %s>" % self.comments.get_style_class_string(address)) |
+ |
+ def format_address(self, maybeaddress, straddress = None): |
+ if maybeaddress is None: |
+ return "not in dump" |
+ else: |
+ if straddress is None: |
+ straddress = "0x" + self.reader.FormatIntPtr(maybeaddress) |
+ style_class = "" |
+ if not self.reader.IsValidAddress(maybeaddress): |
+ style_class = " class=\"nodump\"" |
+ return ("<a %s href=\"search.html?%s&val=%s\">%s</a>" % |
+ (style_class, self.encfilename, straddress, straddress)) |
+ |
+ def output_header(self, f): |
+ f.write(WEB_HEADER % |
+ { "query_dump" : self.encfilename, |
+ "dump_name" : cgi.escape(self.dumpfilename) }) |
+ |
+ def output_footer(self, f): |
+ f.write(WEB_FOOTER) |
+ |
+ MAX_CONTEXT_STACK = 4096 |
+ |
+ def output_summary(self, f): |
+ self.output_header(f) |
+ f.write('<div class="code">') |
+ self.output_context(f, InspectionWebFormatter.CONTEXT_SHORT) |
+ self.output_disasm_pc(f) |
+ |
+ # Output stack |
+ exception_thread = self.reader.thread_map[self.reader.exception.thread_id] |
+ stack_bottom = exception_thread.stack.start + \ |
+ min(exception_thread.stack.memory.data_size, self.MAX_CONTEXT_STACK) |
+ stack_top = self.reader.ExceptionSP() |
+ self.output_words(f, stack_top - 16, stack_bottom, stack_top, "Stack") |
+ |
+ f.write('</div>') |
+ self.output_footer(f) |
+ return |
+ |
+ def output_info(self, f): |
+ self.output_header(f) |
+ f.write("<h3>Dump info</h3>\n") |
+ f.write("Description: ") |
+ self.server.output_dump_desc_field(f, self.dumpfilename) |
+ f.write("<br>\n") |
+ f.write("Filename: ") |
+ f.write("<span class=\"code\">%s</span><br>\n" % (self.dumpfilename)) |
+ dt = datetime.datetime.fromtimestamp(self.reader.header.time_date_stampt) |
+ f.write("Timestamp: %s<br>\n" % dt.strftime('%Y-%m-%d %H:%M:%S')) |
+ self.output_context(f, InspectionWebFormatter.CONTEXT_FULL) |
+ self.output_address_ranges(f) |
+ self.output_footer(f) |
+ return |
+ |
+ def output_address_ranges(self, f): |
+ regions = {} |
+ def print_region(_reader, start, size, _location): |
+ regions[start] = size |
+ self.reader.ForEachMemoryRegion(print_region) |
+ f.write("<h3>Available memory regions</h3>\n") |
+ f.write('<div class="code">') |
+ f.write("<table class=\"regions\">\n") |
+ f.write("<thead><tr>") |
+ f.write("<th>Start address</th>") |
+ f.write("<th>End address</th>") |
+ f.write("<th>Number of bytes</th>") |
+ f.write("</tr></thead>\n") |
+ for start in sorted(regions): |
+ size = regions[start] |
+ f.write("<tr>") |
+ f.write("<td>%s</td>" % self.format_address(start)) |
+ f.write("<td> %s</td>" % self.format_address(start + size)) |
+ f.write("<td> %d</td>" % size) |
+ f.write("</tr>\n") |
+ f.write("</table>\n") |
+ f.write('</div>') |
+ return |
+ |
+ def output_module_details(self, f, module): |
+ f.write("<b>%s</b>" % GetModuleName(self.reader, module)) |
+ file_version = GetVersionString(module.version_info.dwFileVersionMS, |
+ module.version_info.dwFileVersionLS) |
+ product_version = GetVersionString(module.version_info.dwProductVersionMS, |
+ module.version_info.dwProductVersionLS) |
+ f.write("<br> \n") |
+ f.write("base: %s" % self.reader.FormatIntPtr(module.base_of_image)) |
+ f.write("<br> \n") |
+ f.write(" end: %s" % self.reader.FormatIntPtr(module.base_of_image + |
+ module.size_of_image)) |
+ f.write("<br> \n") |
+ f.write(" file version: %s" % file_version) |
+ f.write("<br> \n") |
+ f.write(" product version: %s" % product_version) |
+ f.write("<br> \n") |
+ time_date_stamp = datetime.datetime.fromtimestamp(module.time_date_stamp) |
+ f.write(" timestamp: %s" % time_date_stamp) |
+ f.write("<br>\n"); |
+ |
+ def output_modules(self, f): |
+ self.output_header(f) |
+ f.write('<div class="code">') |
+ for module in self.reader.module_list.modules: |
+ self.output_module_details(f, module) |
+ f.write("</div>") |
+ self.output_footer(f) |
+ return |
+ |
+ def output_context(self, f, details): |
+ exception_thread = self.reader.thread_map[self.reader.exception.thread_id] |
+ f.write("<h3>Exception context</h3>") |
+ f.write('<div class="code">\n') |
+ f.write("Thread id: %d" % exception_thread.id) |
+ f.write(" Exception code: %08X\n" % |
+ self.reader.exception.exception.code) |
+ if details == InspectionWebFormatter.CONTEXT_FULL: |
+ if self.reader.exception.exception.parameter_count > 0: |
+ f.write(" Exception parameters: \n") |
+ for i in xrange(0, self.reader.exception.exception.parameter_count): |
+ f.write("%08x" % self.reader.exception.exception.information[i]) |
+ f.write("<br><br>\n") |
+ |
+ for r in CONTEXT_FOR_ARCH[self.reader.arch]: |
+ f.write(HTML_REG_FORMAT % |
+ (r, self.format_address(self.reader.Register(r)))) |
+ # TODO(vitalyr): decode eflags. |
+ if self.reader.arch == MD_CPU_ARCHITECTURE_ARM: |
+ f.write("<b>cpsr</b>: %s" % bin(self.reader.exception_context.cpsr)[2:]) |
+ else: |
+ f.write("<b>eflags</b>: %s" % |
+ bin(self.reader.exception_context.eflags)[2:]) |
+ f.write('</div>\n') |
+ return |
+ |
+ def align_down(self, a, size): |
+ alignment_correction = a % size |
+ return a - alignment_correction |
+ |
+ def align_up(self, a, size): |
+ alignment_correction = (size - 1) - ((a + size - 1) % size) |
+ return a + alignment_correction |
+ |
+ def format_object(self, address): |
+ heap_object = self.padawan.SenseObject(address) |
+ return cgi.escape(str(heap_object or "")) |
+ |
+ def output_data(self, f, straddress, datakind): |
+ try: |
+ self.output_header(f) |
+ address = int(straddress, 0) |
+ if not self.reader.IsValidAddress(address): |
+ f.write("<h3>Address 0x%x not found in the dump.</h3>" % address) |
+ return |
+ region = self.reader.FindRegion(address) |
+ if datakind == "address": |
+ self.output_words(f, region[0], region[0] + region[1], address, "Dump") |
+ elif datakind == "ascii": |
+ self.output_ascii(f, region[0], region[0] + region[1], address) |
+ self.output_footer(f) |
+ |
+ except ValueError: |
+ f.write("<h3>Unrecognized address format \"%s\".</h3>" % straddress) |
+ return |
+ |
+ def output_words(self, f, start_address, end_address, |
+ highlight_address, desc): |
+ region = self.reader.FindRegion(highlight_address) |
+ if region is None: |
+ f.write("<h3>Address 0x%x not found in the dump.</h3>\n" % |
+ (highlight_address)) |
+ return |
+ size = self.heap.PointerSize() |
+ start_address = self.align_down(start_address, size) |
+ low = self.align_down(region[0], size) |
+ high = self.align_up(region[0] + region[1], size) |
+ if start_address < low: |
+ start_address = low |
+ end_address = self.align_up(end_address, size) |
+ if end_address > high: |
+ end_address = high |
+ |
+ expand = "" |
+ if start_address != low or end_address != high: |
+ expand = ("(<a href=\"data.html?%s&val=0x%x#highlight\">" |
+ " more..." |
+ " </a>)" % |
+ (self.encfilename, highlight_address)) |
+ |
+ f.write("<h3>%s 0x%x - 0x%x, " |
+ "highlighting <a href=\"#highlight\">0x%x</a> %s</h3>\n" % |
+ (desc, start_address, end_address, highlight_address, expand)) |
+ f.write('<div class="code">') |
+ f.write("<table class=\"codedump\">\n") |
+ |
+ for slot in xrange(start_address, end_address, size): |
+ heap_object = "" |
+ maybe_address = None |
+ end_region = region[0] + region[1] |
+ if slot < region[0] or slot + size > end_region: |
+ straddress = "0x" |
+ for i in xrange(end_region, slot + size): |
+ straddress += "??" |
+ for i in reversed( |
+ xrange(max(slot, region[0]), min(slot + size, end_region))): |
+ straddress += "%02x" % self.reader.ReadU8(i) |
+ for i in xrange(slot, region[0]): |
+ straddress += "??" |
+ else: |
+ maybe_address = self.reader.ReadUIntPtr(slot) |
+ straddress = self.format_address(maybe_address) |
+ if maybe_address: |
+ heap_object = self.format_object(maybe_address) |
+ |
+ address_fmt = "%s </td>\n" |
+ if slot == highlight_address: |
+ f.write("<tr class=\"highlight-line\">\n") |
+ address_fmt = "<a id=\"highlight\"></a>%s </td>\n" |
+ elif slot < highlight_address and highlight_address < slot + size: |
+ f.write("<tr class=\"inexact-highlight-line\">\n") |
+ address_fmt = "<a id=\"highlight\"></a>%s </td>\n" |
+ else: |
+ f.write("<tr>\n") |
+ |
+ f.write(" <td>") |
+ self.output_comment_box(f, "da-", slot) |
+ f.write("</td>\n") |
+ f.write(" ") |
+ self.td_from_address(f, slot) |
+ f.write(address_fmt % self.format_address(slot)) |
+ f.write(" ") |
+ self.td_from_address(f, maybe_address) |
+ f.write(": %s </td>\n" % straddress) |
+ f.write(" <td>") |
+ if maybe_address != None: |
+ self.output_comment_box( |
+ f, "sv-" + self.reader.FormatIntPtr(slot), maybe_address) |
+ f.write(" </td>\n") |
+ f.write(" <td>%s</td>\n" % (heap_object or '')) |
+ f.write("</tr>\n") |
+ f.write("</table>\n") |
+ f.write("</div>") |
+ return |
+ |
+ def output_ascii(self, f, start_address, end_address, highlight_address): |
+ region = self.reader.FindRegion(highlight_address) |
+ if region is None: |
+ f.write("<h3>Address %x not found in the dump.</h3>" % |
+ highlight_address) |
+ return |
+ if start_address < region[0]: |
+ start_address = region[0] |
+ if end_address > region[0] + region[1]: |
+ end_address = region[0] + region[1] |
+ |
+ expand = "" |
+ if start_address != region[0] or end_address != region[0] + region[1]: |
+ link = ("data.html?%s&val=0x%x&type=ascii#highlight" % |
+ (self.encfilename, highlight_address)) |
+ expand = "(<a href=\"%s\">more...</a>)" % link |
+ |
+ f.write("<h3>ASCII dump 0x%x - 0x%x, highlighting 0x%x %s</h3>" % |
+ (start_address, end_address, highlight_address, expand)) |
+ |
+ line_width = 64 |
+ |
+ f.write('<div class="code">') |
+ |
+ start = self.align_down(start_address, line_width) |
+ |
+ for address in xrange(start, end_address): |
+ if address % 64 == 0: |
+ if address != start: |
+ f.write("<br>") |
+ f.write("0x%08x: " % address) |
+ if address < start_address: |
+ f.write(" ") |
+ else: |
+ if address == highlight_address: |
+ f.write("<span class=\"highlight\">") |
+ code = self.reader.ReadU8(address) |
+ if code < 127 and code >= 32: |
+ f.write("&#") |
+ f.write(str(code)) |
+ f.write(";") |
+ else: |
+ f.write("·") |
+ if address == highlight_address: |
+ f.write("</span>") |
+ f.write("</div>") |
+ return |
+ |
+ def output_disasm(self, f, straddress, strexact): |
+ try: |
+ self.output_header(f) |
+ address = int(straddress, 0) |
+ if not self.reader.IsValidAddress(address): |
+ f.write("<h3>Address 0x%x not found in the dump.</h3>" % address) |
+ return |
+ region = self.reader.FindRegion(address) |
+ self.output_disasm_range( |
+ f, region[0], region[0] + region[1], address, strexact == "on") |
+ self.output_footer(f) |
+ except ValueError: |
+ f.write("<h3>Unrecognized address format \"%s\".</h3>" % straddress) |
+ return |
+ |
+ def output_disasm_range( |
+ self, f, start_address, end_address, highlight_address, exact): |
+ region = self.reader.FindRegion(highlight_address) |
+ if start_address < region[0]: |
+ start_address = region[0] |
+ if end_address > region[0] + region[1]: |
+ end_address = region[0] + region[1] |
+ count = end_address - start_address |
+ lines = self.reader.GetDisasmLines(start_address, count) |
+ found = False |
+ if exact: |
+ for line in lines: |
+ if line[0] + start_address == highlight_address: |
+ found = True |
+ break |
+ if not found: |
+ start_address = highlight_address |
+ count = end_address - start_address |
+ lines = self.reader.GetDisasmLines(highlight_address, count) |
+ expand = "" |
+ if start_address != region[0] or end_address != region[0] + region[1]: |
+ exactness = "" |
+ if exact and not found and end_address == region[0] + region[1]: |
+ exactness = "&exact=off" |
+ expand = ("(<a href=\"disasm.html?%s%s" |
+ "&val=0x%x#highlight\">more...</a>)" % |
+ (self.encfilename, exactness, highlight_address)) |
+ |
+ f.write("<h3>Disassembling 0x%x - 0x%x, highlighting 0x%x %s</h3>" % |
+ (start_address, end_address, highlight_address, expand)) |
+ f.write('<div class="code">') |
+ f.write("<table class=\"codedump\">\n"); |
+ for i in xrange(0, len(lines)): |
+ line = lines[i] |
+ next_address = count |
+ if i + 1 < len(lines): |
+ next_line = lines[i + 1] |
+ next_address = next_line[0] |
+ self.format_disasm_line( |
+ f, start_address, line, next_address, highlight_address) |
+ f.write("</table>\n") |
+ f.write("</div>") |
+ return |
+ |
+ def annotate_disasm_addresses(self, line): |
+ extra = [] |
+ for m in ADDRESS_RE.finditer(line): |
+ maybe_address = int(m.group(0), 16) |
+ formatted_address = self.format_address(maybe_address, m.group(0)) |
+ line = line.replace(m.group(0), formatted_address) |
+ object_info = self.padawan.SenseObject(maybe_address) |
+ if not object_info: |
+ continue |
+ extra.append(cgi.escape(str(object_info))) |
+ if len(extra) == 0: |
+ return line |
+ return ("%s <span class=\"disasmcomment\">;; %s</span>" % |
+ (line, ", ".join(extra))) |
+ |
+ def format_disasm_line( |
+ self, f, start, line, next_address, highlight_address): |
+ line_address = start + line[0] |
+ address_fmt = " <td>%s</td>\n" |
+ if line_address == highlight_address: |
+ f.write("<tr class=\"highlight-line\">\n") |
+ address_fmt = " <td><a id=\"highlight\">%s</a></td>\n" |
+ elif (line_address < highlight_address and |
+ highlight_address < next_address + start): |
+ f.write("<tr class=\"inexact-highlight-line\">\n") |
+ address_fmt = " <td><a id=\"highlight\">%s</a></td>\n" |
+ else: |
+ f.write("<tr>\n") |
+ num_bytes = next_address - line[0] |
+ stack_slot = self.heap.stack_map.get(line_address) |
+ marker = "" |
+ if stack_slot: |
+ marker = "=>" |
+ op_offset = 3 * num_bytes - 1 |
+ |
+ code = line[1] |
+ # Compute the actual call target which the disassembler is too stupid |
+ # to figure out (it adds the call offset to the disassembly offset rather |
+ # than the absolute instruction address). |
+ if self.heap.reader.arch == MD_CPU_ARCHITECTURE_X86: |
+ if code.startswith("e8"): |
+ words = code.split() |
+ if len(words) > 6 and words[5] == "call": |
+ offset = int(words[4] + words[3] + words[2] + words[1], 16) |
+ target = (line_address + offset + 5) & 0xFFFFFFFF |
+ code = code.replace(words[6], "0x%08x" % target) |
+ # TODO(jkummerow): port this hack to ARM and x64. |
+ |
+ opcodes = code[:op_offset] |
+ code = self.annotate_disasm_addresses(code[op_offset:]) |
+ f.write(" <td>") |
+ self.output_comment_box(f, "codel-", line_address) |
+ f.write("</td>\n") |
+ f.write(address_fmt % marker) |
+ f.write(" ") |
+ self.td_from_address(f, line_address) |
+ f.write("%s (+0x%x)</td>\n" % |
+ (self.format_address(line_address), line[0])) |
+ f.write(" <td>: %s </td>\n" % opcodes) |
+ f.write(" <td>%s</td>\n" % code) |
+ f.write("</tr>\n") |
+ |
+ def output_comment_box(self, f, prefix, address): |
+ f.write("<input type=\"text\" class=\"commentinput\" " |
+ "id=\"%s-address-0x%s\" onchange=\"comment()\" value=\"%s\">" % |
+ (prefix, |
+ self.reader.FormatIntPtr(address), |
+ cgi.escape(self.comments.get_comment(address)) or "")) |
+ |
+ MAX_FOUND_RESULTS = 100 |
+ |
+ def output_find_results(self, f, results): |
+ f.write("Addresses") |
+ toomany = len(results) > self.MAX_FOUND_RESULTS |
+ if toomany: |
+ f.write("(found %i results, displaying only first %i)" % |
+ (len(results), self.MAX_FOUND_RESULTS)) |
+ f.write(": \n") |
+ results = sorted(results) |
+ results = results[:min(len(results), self.MAX_FOUND_RESULTS)] |
+ for address in results: |
+ f.write("<span %s>%s</span>\n" % |
+ (self.comments.get_style_class_string(address), |
+ self.format_address(address))) |
+ if toomany: |
+ f.write("...\n") |
+ |
+ |
+ def output_page_info(self, f, page_kind, page_address, my_page_address): |
+ if my_page_address == page_address and page_address != 0: |
+ f.write("Marked first %s page.\n" % page_kind) |
+ else: |
+ f.write("<span id=\"%spage\" style=\"display:none\">" % page_kind) |
+ f.write("Marked first %s page." % page_kind) |
+ f.write("</span>\n") |
+ f.write("<button onclick=\"onpage('%spage', '0x%x')\">" % |
+ (page_kind, my_page_address)) |
+ f.write("Mark as first %s page</button>\n" % page_kind) |
+ return |
+ |
+ def output_search_res(self, f, straddress): |
+ try: |
+ self.output_header(f) |
+ f.write("<h3>Search results for %s</h3>" % straddress) |
+ |
+ address = int(straddress, 0) |
+ |
+ f.write("Comment: ") |
+ self.output_comment_box(f, "search-", address) |
+ f.write("<br>\n") |
+ |
+ page_address = address & ~self.heap.PageAlignmentMask() |
+ |
+ f.write("Page info: \n") |
+ self.output_page_info(f, "data", self.padawan.known_first_data_page, \ |
+ page_address) |
+ self.output_page_info(f, "map", self.padawan.known_first_map_page, \ |
+ page_address) |
+ self.output_page_info(f, "pointer", \ |
+ self.padawan.known_first_pointer_page, \ |
+ page_address) |
+ |
+ if not self.reader.IsValidAddress(address): |
+ f.write("<h3>The contents at address %s not found in the dump.</h3>" % \ |
+ straddress) |
+ else: |
+ # Print as words |
+ self.output_words(f, address - 8, address + 32, address, "Dump") |
+ |
+ # Print as ASCII |
+ f.write("<hr>\n") |
+ self.output_ascii(f, address, address + 256, address) |
+ |
+ # Print as code |
+ f.write("<hr>\n") |
+ self.output_disasm_range(f, address - 16, address + 16, address, True) |
+ |
+ aligned_res, unaligned_res = self.reader.FindWordList(address) |
+ |
+ if len(aligned_res) > 0: |
+ f.write("<h3>Occurrences of 0x%x at aligned addresses</h3>\n" % |
+ address) |
+ self.output_find_results(f, aligned_res) |
+ |
+ if len(unaligned_res) > 0: |
+ f.write("<h3>Occurrences of 0x%x at unaligned addresses</h3>\n" % \ |
+ address) |
+ self.output_find_results(f, unaligned_res) |
+ |
+ if len(aligned_res) + len(unaligned_res) == 0: |
+ f.write("<h3>No occurences of 0x%x found in the dump</h3>\n" % address) |
+ |
+ self.output_footer(f) |
+ |
+ except ValueError: |
+ f.write("<h3>Unrecognized address format \"%s\".</h3>" % straddress) |
+ return |
+ |
+ def output_disasm_pc(self, f): |
+ address = self.reader.ExceptionIP() |
+ if not self.reader.IsValidAddress(address): |
+ return |
+ self.output_disasm_range(f, address - 16, address + 16, address, True) |
+ |
+ |
+WEB_DUMPS_HEADER = """ |
+<!DOCTYPE html> |
+<html> |
+<head> |
+<meta content="text/html; charset=utf-8" http-equiv="content-type"> |
+<style media="screen" type="text/css"> |
+ |
+.dumplist { |
+ border-collapse : collapse; |
+ border-spacing : 0px; |
+ font-family: monospace; |
+} |
+ |
+.dumpcomments { |
+ border : 1px solid LightGray; |
+ width : 32em; |
+} |
+ |
+</style> |
+ |
+<script type="application/javascript"> |
+ |
+var dump_str = "dump-"; |
+var dump_len = dump_str.length; |
+ |
+function dump_comment() { |
+ var s = event.srcElement.id; |
+ var index = s.indexOf(dump_str); |
+ if (index >= 0) { |
+ send_dump_desc(s.substring(index + dump_len), event.srcElement.value); |
+ } |
+} |
+ |
+function send_dump_desc(name, desc) { |
+ xmlhttp = new XMLHttpRequest(); |
+ name = encodeURIComponent(name) |
+ desc = encodeURIComponent(desc) |
+ xmlhttp.open("GET", |
+ "setdumpdesc?dump=" + name + |
+ "&description=" + desc, true); |
+ xmlhttp.send(); |
+} |
+ |
+</script> |
+ |
+<title>Dump list</title> |
+</head> |
+ |
+<body> |
+""" |
+ |
+WEB_DUMPS_FOOTER = """ |
+</body> |
+</html> |
+""" |
+ |
+DUMP_FILE_RE = re.compile(r"[-_0-9a-zA-Z][-\._0-9a-zA-Z]*\.dmp$") |
+ |
+ |
+class InspectionWebServer(BaseHTTPServer.HTTPServer): |
+ def __init__(self, port_number, switches, minidump_name): |
+ BaseHTTPServer.HTTPServer.__init__( |
+ self, ('', port_number), InspectionWebHandler) |
+ splitpath = os.path.split(minidump_name) |
+ self.dumppath = splitpath[0] |
+ self.dumpfilename = splitpath[1] |
+ self.default_formatter = InspectionWebFormatter( |
+ switches, minidump_name, self) |
+ self.formatters = { self.dumpfilename : self.default_formatter } |
+ self.switches = switches |
+ |
+ def output_dump_desc_field(self, f, name): |
+ try: |
+ descfile = open(os.path.join(self.dumppath, name + ".desc"), "r") |
+ desc = descfile.readline() |
+ descfile.close() |
+ except IOError: |
+ desc = "" |
+ f.write("<input type=\"text\" class=\"dumpcomments\" " |
+ "id=\"dump-%s\" onchange=\"dump_comment()\" value=\"%s\">\n" % |
+ (cgi.escape(name), desc)) |
+ |
+ def set_dump_desc(self, name, description): |
+ if not DUMP_FILE_RE.match(name): |
+ return False |
+ fname = os.path.join(self.dumppath, name) |
+ if not os.path.isfile(fname): |
+ return False |
+ fname = fname + ".desc" |
+ descfile = open(fname, "w") |
+ descfile.write(description) |
+ descfile.close() |
+ return True |
+ |
+ def get_dump_formatter(self, name): |
+ if name is None: |
+ return self.default_formatter |
+ else: |
+ if not DUMP_FILE_RE.match(name): |
+ raise WebParameterError("Invalid name '%s'" % name) |
+ formatter = self.formatters.get(name, None) |
+ if formatter is None: |
+ try: |
+ formatter = InspectionWebFormatter( |
+ self.switches, os.path.join(self.dumppath, name), self) |
+ self.formatters[name] = formatter |
+ except IOError: |
+ raise WebParameterError("Could not open dump '%s'" % name) |
+ return formatter |
+ |
+ def output_dumps(self, f): |
+ f.write(WEB_DUMPS_HEADER) |
+ f.write("<h3>List of available dumps</h3>") |
+ f.write("<table class=\"dumplist\">\n") |
+ f.write("<thead><tr>") |
+ f.write("<th>Name</th>") |
+ f.write("<th>File time</th>") |
+ f.write("<th>Comment</th>") |
+ f.write("</tr></thead>") |
+ dumps_by_time = {} |
+ for fname in os.listdir(self.dumppath): |
+ if DUMP_FILE_RE.match(fname): |
+ mtime = os.stat(os.path.join(self.dumppath, fname)).st_mtime |
+ fnames = dumps_by_time.get(mtime, []) |
+ fnames.append(fname) |
+ dumps_by_time[mtime] = fnames |
+ |
+ for mtime in sorted(dumps_by_time, reverse=True): |
+ fnames = dumps_by_time[mtime] |
+ for fname in fnames: |
+ f.write("<tr>\n") |
+ f.write("<td><a href=\"summary.html?%s\">%s</a></td>\n" % ( |
+ (urllib.urlencode({ 'dump' : fname }), fname))) |
+ f.write("<td> ") |
+ f.write(datetime.datetime.fromtimestamp(mtime)) |
+ f.write("</td>") |
+ f.write("<td> ") |
+ self.output_dump_desc_field(f, fname) |
+ f.write("</td>") |
+ f.write("</tr>\n") |
+ f.write("</table>\n") |
+ f.write(WEB_DUMPS_FOOTER) |
+ return |
class InspectionShell(cmd.Cmd): |
def __init__(self, reader, heap): |
@@ -1996,6 +3123,8 @@ if __name__ == "__main__": |
parser = optparse.OptionParser(USAGE) |
parser.add_option("-s", "--shell", dest="shell", action="store_true", |
help="start an interactive inspector shell") |
+ parser.add_option("-w", "--web", dest="web", action="store_true", |
+ help="start a web server on localhost:%i" % PORT_NUMBER) |
parser.add_option("-c", "--command", dest="command", default="", |
help="run an interactive inspector shell command and exit") |
parser.add_option("-f", "--full", dest="full", action="store_true", |
@@ -2014,4 +3143,14 @@ if __name__ == "__main__": |
if len(args) != 1: |
parser.print_help() |
sys.exit(1) |
- AnalyzeMinidump(options, args[0]) |
+ if options.web: |
+ try: |
+ server = InspectionWebServer(PORT_NUMBER, options, args[0]) |
+ print 'Started httpserver on port ' , PORT_NUMBER |
+ webbrowser.open('http://localhost:%i/summary.html' % PORT_NUMBER) |
+ server.serve_forever() |
+ except KeyboardInterrupt: |
+ print '^C received, shutting down the web server' |
+ server.socket.close() |
+ else: |
+ AnalyzeMinidump(options, args[0]) |