JezK
Edit File: libjvm.so-gdb.py
# Copyright 2016, Red Hat and individual contributors # by the @authors tag. # # This is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 2.1 of # the License, or (at your option) any later version. # # This software is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this software; if not, write to the Free # Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA # 02110-1301 USA, or see the FSF site: http://www.fsf.org. # # @authors Andrew Dinn import gdb import traceback import re from gdb.FrameDecorator import FrameDecorator # we rely on the unwinder API provided by gdb.7.10 _debug = False _warning = False def warning(*args, **kwargs): if _warning: gdb.write(*args, **kwargs) def debug(*args, **kwargs): if _debug: gdb.write(*args, **kwargs) _dump_frame = False _have_unwinder = True try: from gdb.unwinder import Unwinder gdb.write("Installing openjdk unwinder\n") except ImportError: _have_unwinder = False # We need something here; it doesn't matter what as no unwinder # will ever be instantiated. Unwinder = object def debug_write(msg): debug(msg) # pass def t(msg): debug("%s\n" % msg) # pass debug("dbg.py\n") # class providing various type conversions for gdb Value instances class Types(object): # cache some basic primitive and pointer types byte_t = gdb.lookup_type('unsigned char') char_t = gdb.lookup_type('char') int_t = gdb.lookup_type('int') long_t = gdb.lookup_type('long') void_t = gdb.lookup_type('void') bytep_t = byte_t.pointer() charp_t = char_t.pointer() intp_t = int_t.pointer() longp_t = long_t.pointer() voidp_t = void_t.pointer() codeblobp_t = gdb.lookup_type('CodeBlob').pointer() nmethodp_t = gdb.lookup_type('nmethod').pointer() ptrp_t = voidp_t.pointer() # convert Values to primitive Values @classmethod def cast_byte(cls, value): return value.cast(cls.byte_t) @classmethod def cast_int(cls, value): return value.cast(cls.int_t) @classmethod def cast_long(cls, value): return value.cast(cls.long_t) # convert Values to pointer Values @classmethod def cast_bytep(cls, value): return value.cast(cls.bytep_t) @classmethod def cast_intp(cls, value): return value.cast(cls.intp_t) @classmethod def cast_longp(cls, value): return value.cast(cls.longp_t) @classmethod def cast_voidp(cls, value): return value.cast(cls.voidp_t) @classmethod def cast_ptrp(cls, value): return value.cast(cls.ptrp_t) # cast Value to pointer type then load and return contents as Value @classmethod def load_ptr(cls, value): return cls.cast_ptrp(cls.cast_ptrp(value).dereference()) @classmethod def load_byte(cls, value): return cls.cast_bytep(value).dereference() @classmethod def load_int(cls, value): return cls.cast_intp(value).dereference() @classmethod def load_long(cls, value): return cls.cast_longp(value).dereference() # cast Value to int and return as python integer @classmethod def as_int(cls, value): return int(cls.cast_int(value)) # cast Value to long and return as python integer @classmethod def as_long(cls, value): return int(cls.cast_long(value)) # construct Value from integer x and cast to type t @classmethod def to_type(cls, x, t): return gdb.Value(x).cast(t) # construct voidp Value from integer x @classmethod def to_voidp(cls, x): return cls.to_type(x, cls.voidp_t) # construct void ** Value from integer x @classmethod def to_ptrp(cls, x): return cls.to_type(x, cls.ptrp_t) # OpenJDK specific classes which understand the layout of the code # heap and know how to translate a PC to an associated code blob and, # from there to a method object. n.b. in some cases the latter step # requires a frame base pointer bu tthat can be calculated using the # code blob and stack pointer # class encapsulating details of a specific heap class CodeHeap: # track whether we have static initialized yet class_inited = False heap_block_type = None code_blob_type = None @classmethod def class_init(cls): t("CodeHeap.class_init") if cls.class_inited: return # we can only proceed if we have the necessary heap symbols # if this excepts then we don't care cls.heap_block_type = gdb.lookup_type("HeapBlock").pointer() cls.code_blob_type = gdb.lookup_type("CodeBlob").pointer() cls.class_inited = True def __init__(self, heap): t("CodeHeap.__init__") # make sure we have static inited successfuly self.class_init() # if we got here we are ok to create a new instance self.heap = heap self.name = str(heap['_name']) self.lo = Types.as_long(heap['_memory']['_low_boundary']) self.hi = Types.as_long(heap['_memory']['_high_boundary']) self.segmap = heap['_segmap'] self.segmap_lo = self.segmap['_low'] self.segment_size = int(heap['_segment_size']) self.log2_segment_size = int(heap['_log2_segment_size']) debug("@@ heap.name = %s\n" % self.name) debug("@@ heap.lo = 0x%x\n" % self.lo) debug("@@ heap.hi = 0x%x\n" % self.hi) def inrange(self, x): debug("CodeHeap.inrange") return self.lo <= x and self.hi > x def findblob(self, pc): debug("CodeHeap.findblob") x = Types.as_long(pc) debug("@@ findblob(%s, 0x%x)\n" % (self.name, pc)) debug("@@ pc (%s) = 0x%x \n" % (str(pc.type), pc)) debug("@@ self.lo = 0x%x\n" % self.lo) debug("@@ self.hi = 0x%x\n" % self.hi) # check pc is in this heap's range t("if not self.inrange(x):") if not self.inrange(x): return None debug("@@ pc in range\n") t("segments = 0") segments = 0 debug("@@ segmap_lo (%s) = 0x%x\n" % (str(self.segmap_lo.type), self.segmap_lo)) debug("@@ self.lo = 0x%x \n" % self.lo) debug("@@ self.log2_segment_size = 0x%x \n" % self.log2_segment_size) t("offset = Types.as_long(pc - self.lo)") offset = Types.as_long(pc - self.lo) debug("@@ offset = 0x%x\n" % offset) t("shift = self.log2_segment_size") shift = self.log2_segment_size debug("@@ shift = 0x%x\n" % shift) t("segment = offset >> shift") segment = offset >> shift # segment = (offset >> self.log2_segment_size) #segment = offset >> (self.log2_segment_size & 0x31) debug("@@ segment = 0x%x\n" % segment) t("tag = (self.segmap_lo + segment).dereference() & 0xff") tag = (self.segmap_lo + segment).dereference() & 0xff # tag = Types.load_byte(self.segmap_lo + segment) & 0xff debug("@@ tag (%s) = 0x%x\n" % (str(tag.type), tag)) t("while tag > 0 and segments < 64:") while tag > 0 and segments < 64: t("segment = segment - tag") segment = segment - tag debug("@@ segment = 0x%x\n" % segment) t("tag = (self.segmap_lo + segment).dereference() & 0xff") tag = (self.segmap_lo + segment).dereference() & 0xff debug("@@ tag (%s) = 0x%x\n" % (str(tag.type), tag)) t("segments += 1") segments += 1 t("if tag != 0:") if tag != 0: t("return None") return None debug("@@ lo = 0x%x\n" % self.lo) debug("@@ segment << self.log2_segment_size = 0x%x\n" % (segment << self.log2_segment_size)) t("block_addr = self.lo + (segment << self.log2_segment_size)") block_addr = self.lo + (segment << self.log2_segment_size) debug("@@ block_addr (%s) = 0x%x\n" % (str(block_addr.type), block_addr)) t("heap_block = gdb.Value(block_addr).cast(CodeHeap.heap_block_type)") heap_block = gdb.Value(block_addr).cast(CodeHeap.heap_block_type) debug("@@ heap_block (%s) = 0x%x\n" % (str(heap_block.type), heap_block)) t("if heap_block['_header']['_used'] != 1:") if heap_block['_header']['_used'] != 1: # hmm, this is not meant to happen t("return None") return None t("blob = (heap_block + 1).cast(CodeHeap.code_blob_type)") blob = (heap_block + 1).cast(CodeHeap.code_blob_type) return blob # class encapsulating access to code cache memory # this is essentially all static as per the JVM class class CodeCache: # track whether we have successfully intiialized class_inited = False # static heap start, lo and hi bounds for code addresses lo = 0 hi = 0 heap_count = 0 heap_list = [] @classmethod def class_init(cls): t("CodeCache.class_init") if cls.class_inited: return t("if cls.lo == 0 or cls.hi == 0:") if cls.lo == 0 or cls.hi == 0: try: t("cls.lo = gdb.parse_and_eval(\"CodeCache::_low_bound\")") lo = gdb.parse_and_eval("CodeCache::_low_bound") cls.lo = Types.as_long(lo) debug("@@ CodeCache::_low_bound = 0x%x\n" % cls.lo) if cls.lo == 0: return t("cls.hi = gdb.parse_and_eval(\"CodeCache::_high_bound\")") hi = gdb.parse_and_eval("CodeCache::_high_bound") cls.hi = Types.as_long(hi) debug("@@ CodeCache::_high_bound = 0x%x\n" % cls.hi) if cls.hi == 0: return except Exception as arg: debug("@@ %s\n" % arg) cls.lo = 0 cls.hi = 0 cls.class_inited = False raise t("f cls.heap_list == []:") if cls.heap_list == []: try: t("heaps = gdb.parse_and_eval(\"CodeCache::_heaps\")") heaps = gdb.parse_and_eval("CodeCache::_heaps") debug("@@ CodeCache::_heaps (%s) = 0x%x\n" % (heaps.type, heaps)) t("len = int(heaps['_len'])") len = int(heaps['_len']) debug("@@ CodeCache::_heaps->_len = %d\n" % len) t("data = heaps['_data']") data = heaps['_data'] debug("@@ CodeCache::_heaps->_data = 0x%x\n" % data) t("for i in range(0, len):") for i in range(0, len): t("heap = CodeHeap((data + i).dereference())") heap = CodeHeap((data + i).dereference()) t("cls.heap_list.append(heap)") cls.heap_list.append(heap) t("cls.heap_count += 1") cls.heap_count += 1 except Exception as arg: debug("@@ %s\n" % arg) cls.heap_list = [] cls.heap_count = 0 cls.class_inited = False raise cls.class_inited = True @classmethod def inrange(cls, pc): t("CodeCache.inrange") # make sure we are initialized cls.class_init() # if we got here we can use the heaps x = Types.as_long(pc) t("return cls.lo <= x and cls.hi > x") return cls.lo <= x and cls.hi > x @classmethod def makestr(cls, charcnt, charptr): t("CodeCache.makestr") #res = "" #for i in range(0, charcnt): # c = (charptr + i).dereference() # res = ("%s%c" % (res, c)) #return res debug("charcnt = %d charptr = %s\n" % (charcnt, str(charptr))) return charptr.string("ascii", "ignore", charcnt) # given a PC find the associated OpenJDK code blob instance @classmethod def findblob(cls, pc): t("CodeCache.findblob") # make sure we are initialized cls.class_init() # if we got here we can use the heaps if not cls.inrange(pc): raise gdb.GdbError("dbg.findblob : address 0x%x is not in range!" % pc) for heap in cls.heap_list: try: t("blob = heap.findblob(pc)") blob = heap.findblob(pc) except Exception as arg: debug("@@ findblob excepted %s\n" % str(arg)) t("blob = None") blob = None t("if blob != None:") if blob != None: t("name=str(blob['_name'])") name=str(blob['_name']) debug("@@ blob(0x%x) -> %s\n" % (pc, name)) t("return blob") return blob t("raise gdb.GdbError") raise gdb.GdbError("dbg.findblob : no blob for inrange address 0x%x!" % pc) # abstract over some constants for stack frame layout class FrameConstants(object): class_inited = False _sender_sp_offset = 0 _interpreter_frame_sender_sp_offset = 0 _interpreter_frame_method_offset = 0 _interpreter_frame_bcp_offset = 0 @classmethod def class_init(cls): if cls.class_inited: return cls._interpreter_frame_sender_sp_offset = int(gdb.parse_and_eval("frame::interpreter_frame_sender_sp_offset")) cls._sender_sp_offset = int(gdb.parse_and_eval("frame::sender_sp_offset")) cls._interpreter_frame_method_offset = int(gdb.parse_and_eval("frame::interpreter_frame_method_offset")) cls._interpreter_frame_bcp_offset = int(gdb.parse_and_eval("frame::interpreter_frame_bcp_offset")) # only set if we got here with no errors cls.class_inited = True @classmethod def sender_sp_offset(cls): cls.class_init() return cls._sender_sp_offset @classmethod def interpreter_frame_sender_sp_offset(cls): cls.class_init() return cls._interpreter_frame_sender_sp_offset @classmethod def interpreter_frame_method_offset(cls): cls.class_init() return cls._interpreter_frame_method_offset @classmethod def interpreter_frame_bcp_offset(cls): cls.class_init() return cls._interpreter_frame_bcp_offset # class which knows how to read a compressed stream of bytes n.b. at # the moment we only need to know how to read bytes and ints class CompressedStream: # important constants for compressed stream read BitsPerByte = 8 lg_H = 6 H = 1 << lg_H L = (1 << BitsPerByte) - H MAX_i = 4 def __init__(self, data): # data is a gdb.Value of type 'byte *' self.data = data self.pos = 0 # retrieve the byte at offset pos def at(self, pos): return int(Types.load_byte(self.data + pos)) # read and return the next byte def read(self): t("CompressedStream.read()") pos = self.pos b = self.at(pos) self.pos = pos+1 return b def read_int(self): t("CompressedStream.read_int()") b0 = self.read() debug("b0 = 0x%x\n" % b0) if b0 < CompressedStream.L: return b0 return self.read_int_mb(b0) def read_signed_int(self): t("CompressedStream.read_signed_int()") return self.decode_sign(self.read_int()) def decode_sign(self, x): t("CompressedStream.decode_sign()") return (x >> 1) ^ (0 - (x & 1)) def read_int_mb(self, b): t# ("CompressedStream.read_int_mb()") sum = b pos = self.pos debug("pos = %d\n" % pos) debug("sum = 0x%x\n" % sum) lg_H_i = CompressedStream.lg_H i = 0 while (True): b_i = self.at(pos + i) debug("b_%d = %d\n" % (i, b_i)) sum += (b_i << lg_H_i) debug("sum = 0x%x\n" % sum) i += 1 if b_i < CompressedStream.L or i == CompressedStream.MAX_i: self.pos = pos + i debug("self.pos = %d\n" % self.pos) return sum lg_H_i += CompressedStream.lg_H # class which knows how to find method and bytecode # index pairs from a pc associated with a given compiled # method. n.b. there may be more than one such pair # because of inlining. class MethodBCIReader: pcdescstar_t = None class_inited = False @classmethod def class_init(cls): t("MethodBCIReader.class_init") if cls.class_inited: return # cache some useful types cls.pcdesc_p = gdb.lookup_type("PcDesc").pointer() cls.metadata_p = gdb.lookup_type("Metadata").pointer() cls.metadata_pp = cls.metadata_p.pointer() cls.method_p = gdb.lookup_type("Method").pointer() cls.class_inited = True @classmethod def as_pcdesc_p(cls, val): return Types.to_type(val, cls.pcdesc_p) def __init__(self, nmethod, method): t("MethodBCIReader.__init__") # ensure we have cached the necessary types self.class_init() # need to unpack pc scopes self.nmethod = nmethod self.method = method debug("nmethod (%s) = 0x%x\n" % (str(nmethod.type), Types.as_long(nmethod))) blob = Types.to_type(nmethod, Types.codeblobp_t); self.code_begin = Types.as_long(blob['_code_begin']) self.code_end = Types.as_long(blob['_code_end']) scopes_pcs_begin_offset = Types.as_int(nmethod['_scopes_pcs_offset']) debug("scopes_pcs_begin_offset = 0x%x\n" % scopes_pcs_begin_offset) scopes_pcs_end_offset = Types.as_int(nmethod['_dependencies_offset']) debug("scopes_pcs_end_offset = 0x%x\n" % scopes_pcs_end_offset) header_begin = Types.cast_bytep(nmethod) self.scopes_pcs_begin = self.as_pcdesc_p(header_begin + scopes_pcs_begin_offset) debug("scopes_pcs_begin (%s) = 0x%x\n" % (str(self.scopes_pcs_begin.type), Types.as_long(self.scopes_pcs_begin))) self.scopes_pcs_end = self.as_pcdesc_p(header_begin + scopes_pcs_end_offset) debug("scopes_pcs_end (%s) = 0x%x\n" % (str(self.scopes_pcs_end.type), Types.as_long(self.scopes_pcs_end))) def find_pc_desc(self, pc_off): lower = self.scopes_pcs_begin upper = self.scopes_pcs_end - 1 if lower == upper: return None # non-empty table always starts with lower as a sentinel with # offset -1 and will have at least one real offset beyond that next = lower + 1 while next < upper and next['_pc_offset'] <= pc_off: next = next + 1 # use the last known bci below this pc return next - 1 def pc_desc_to_method_bci_stack(self, pc_desc): scope_decode_offset = Types.as_int(pc_desc['_scope_decode_offset']) if scope_decode_offset == 0: return [ { 'method': self.method, 'bci': 0 } ] nmethod = self.nmethod debug("nmethod = 0x%x\n" % nmethod) debug("pc_desc = 0x%x\n" % Types.as_long(pc_desc)) base = Types.cast_bytep(nmethod) # scopes_data_offset = Types.as_int(nmethod['_scopes_data_offset']) # scopes_base = base + scopes_data_offset scopes_base = nmethod['_scopes_data_begin'] debug("scopes_base = 0x%x\n" % Types.as_long(scopes_base)) metadata_offset = Types.as_int(nmethod['_metadata_offset']) metadata_base = Types.to_type(base + metadata_offset, self.metadata_pp) debug("metadata_base = 0x%x\n" % Types.as_long(metadata_base)) scope = scopes_base + scope_decode_offset debug("scope = 0x%x\n" % Types.as_long(scope)) stream = CompressedStream(scope) debug("stream = %s\n" % stream) sender = stream.read_int() debug("sender = %s\n" % sender) # method name is actually in metadata method_idx = stream.read_int() method_md = (metadata_base + (method_idx - 1)).dereference() methodptr = Types.to_type(method_md, self.method_p) method = Method(methodptr) # bci is offset by -1 to allow range [-1, ..., MAX_UINT) bci = stream.read_int() - 1 debug("method,bci = %s,0x%x\n" % (method.get_name(), bci)) result = [ { 'method': method, 'bci': bci } ] while sender > 0: debug("\nsender = 0x%x\n" % sender) stream = CompressedStream(scopes_base + sender) sender = stream.read_int() method_idx = stream.read_int() method_md = (metadata_base + (method_idx - 1)).dereference() methodptr = Types.to_type(method_md, self.method_p) method = Method(methodptr) # bci is offset by -1 to allow range [-1, ..., MAX_UINT) bci = stream.read_int() - 1 debug("method,bci = %s,0x%x\n" % (method.get_name(), bci)) result.append( { 'method': method, 'bci': bci } ) return result def pc_to_method_bci_stack(self, pc): # need to search unpacked pc scopes if pc < self.code_begin or pc >= self.code_end: return None pc_off = pc - self.code_begin debug("\npc_off = 0x%x\n" % pc_off) pc_desc = self.find_pc_desc(pc_off) if pc_desc is None: return None return self.pc_desc_to_method_bci_stack(pc_desc) # class which knows how to read a method's line # number table, translating bytecode indices to line numbers class LineReader: # table is a gdb.Value of type 'byte *' (strictly 'u_char *' in JVM code) def __init__(self, table): t("LineReader.init") self.table = table self.translations = {} def bc_to_line(self, bci): t("LineReader.bc_to_line()") try: return self.translations[bci] except Exception as arg: line = self.compute_line(bci) if line >= 0: self.translations[bci] = line return line def compute_line(self, bci): t("LineReader.compute_line()") debug("table = 0x%x\n" % self.table) bestline = -1 self.stream = CompressedStream(self.table) self._bci = 0 self._line = 0 while self.read_pair(): nextbci = self._bci nextline = self._line if nextbci >= bci: return nextline else: bestline = nextline return bestline def read_pair(self): t("LineReader.read_pair()") next = self.stream.read() debug("next = 0x%x\n" % next) if next == 0: return False if next == 0xff: self._bci = self._bci + self.stream.read_signed_int() self._line = self._line + self.stream.read_signed_int() else: self._bci = self._bci + (next >> 3) self._line = self._line + (next & 0x7) debug("_bci = %d\n" % self._bci) debug("_line = %d\n" % self._line) return True # class to provide access to data relating to a Method object class Method(object): def __init__(self, methodptr): self.methodptr = methodptr; self.name = None const_method = self.methodptr['_constMethod'] bcbase = Types.cast_bytep(const_method + 1) bytecode_size = Types.as_int(const_method['_code_size']) lnbase = Types.cast_bytep(bcbase + bytecode_size) self.line_number_reader = LineReader(lnbase) def get_name(self): if self.name == None: self.make_name(self.methodptr) return self.name def get_klass_path(self): if self.name == None: self.make_name(self.methodptr) return self.klass_path def get_line(self, bci): if bci < 0: bci = 0 return self.line_number_reader.bc_to_line(bci) def make_name(self, methodptr): const_method = methodptr['_constMethod'] constant_pool = const_method['_constants'] constant_pool_base = Types.cast_voidp((constant_pool + 1)) klass_sym = constant_pool['_pool_holder']['_name'] klass_name = klass_sym['_body'].cast(gdb.lookup_type("char").pointer()) klass_name_length = int(klass_sym['_length']) klass_path = CodeCache.makestr(klass_name_length, klass_name) self.klass_str = klass_path.replace("/", ".") dollaridx = klass_path.find('$') if dollaridx >= 0: klass_path = klass_path[0:dollaridx] self.klass_path = klass_path + ".java" method_idx = const_method['_name_index'] method_sym = (constant_pool_base + (8 * method_idx)).cast(gdb.lookup_type("Symbol").pointer().pointer()).dereference() method_name = method_sym['_body'].cast(gdb.lookup_type("char").pointer()) method_name_length = int(method_sym['_length']) self.method_str = CodeCache.makestr(method_name_length, method_name) sig_idx = const_method['_signature_index'] sig_sym = (constant_pool_base + (8 * sig_idx)).cast(gdb.lookup_type("Symbol").pointer().pointer()).dereference() sig_name = sig_sym['_body'].cast(gdb.lookup_type("char").pointer()) sig_name_length = int(sig_sym['_length']) sig_str = CodeCache.makestr(sig_name_length, sig_name) self.sig_str = self.make_sig_str(sig_str) self.name = self.klass_str + "." + self.method_str + self.sig_str def make_sig_str(self, sig): in_sym_name = False sig_str = "" prefix="" for i, c in enumerate(sig): if c == "(": sig_str = sig_str + c elif c == ")": # ignore return type return sig_str + c elif in_sym_name == True: if c == ";": in_sym_name = False elif c == "/": sig_str = sig_str + "." else: sig_str = sig_str + c elif c == "L": sig_str = sig_str + prefix prefix = "," in_sym_name = True elif c == "B": sig_str = sig_str + prefix + "byte" prefix = "," elif c == "S": sig_str = sig_str + prefix + "short" prefix = "," elif c == "C": sig_str = sig_str + prefix + "char" prefix = "," elif c == "I": sig_str = sig_str + prefix + "int" prefix = "," elif c == "J": sig_str = sig_str + prefix + "long" prefix = "," elif c == "F": sig_str = sig_str + prefix + "float" prefix = "," elif c == "D": sig_str = sig_str + prefix + "double" prefix = "," elif c == "Z": sig_str = sig_str + prefix + "boolean" prefix = "," return sig_str # This represents a method in a JIT frame for the purposes of # display. There may be more than one method associated with # a (compiled) frame because of inlining. The MethodInfo object # associated with a JIT frame creates a list of decorators. # Interpreted and stub MethodInfo objects insert only one # decorator. Compiled MethodInfo objects insert one for the # base method and one for each inlined method found at the # frame PC address. class OpenJDKFrameDecorator(FrameDecorator): def __init__(self, base, methodname, filename, line): super(FrameDecorator, self).__init__(base) self._base = base self._methodname = methodname self._filename = filename self._line = line def function(self): try: t("OpenJDKFrameDecorator.function") return self._methodname except Exception as arg: debug("!!! function oops !!! %s\n" % arg) return None def method_name(self): return self._methodname def filename(self): try: return self._filename except Exception as arg: debug("!!! filename oops !!! %s\n" % arg) return None def line(self): try: return self._line except Exception as arg: debug("!!! line oops !!! %s\n" % arg) return None # A frame filter for OpenJDK. class OpenJDKFrameFilter(object): def __init__(self, unwinder): self.name="OpenJDK" self.enabled = False self.priority = 100 self.unwinder = unwinder self.unwinder.set_filter(self) def maybe_wrap_frame(self, frame): t("OpenJDKFrameFilter.maybe_wrap_frame") if self.unwinder is None: return [ frame ] t("unwindercache = self.unwinder.unwindercache") unwindercache = self.unwinder.unwindercache if unwindercache is None: return [ frame ] t("base = frame.inferior_frame()") base = frame.inferior_frame() t("sp = Types.as_long(base.read_register('rsp'))") sp = base.read_register('rsp') x = Types.as_long(sp) debug("@@ get info at unwindercache[0x%x]\n" % x) try: cache_entry = unwindercache[x] except Exception as arg: debug("@@ unwindercache lookup excepted %s\n" % str(arg)) # n.b. no such entry throws an exception # just ignore and use existing frame return [ frame ] try: if cache_entry is None: debug("@@ lookup found no cache_entry\n") return [ frame ] elif cache_entry.codetype == "unknown": debug("@@ lookup found unknown cache_entry\n") return [ frame ] else: debug("@@ got cache_entry for blob 0x%x at unwindercache[0x%x]\n" % (cache_entry.blob, x)) method_info = cache_entry.method_info if method_info == None: return [ frame ] else: return method_info.decorate(frame) except Exception as arg: warning("!!! maybe_wrap_frame oops !!! %s / %s\n" % (arg, traceback.format_exc())) return [ frame ] def flatten(self, list_of_lists): return iter([x for y in list_of_lists for x in y ]) def filter(self, frame_iter): # return map(self.maybe_wrap_frame, frame_iter) return self.flatten( map(self.maybe_wrap_frame, frame_iter) ) # A frame id class, as specified by the gdb unwinder API. class OpenJDKFrameId(object): def __init__(self, sp, pc): t("OpenJDKFrameId.__init__") self.sp = sp self.pc = pc # class hierarchy to record details of different code blobs # the class implements functionality required by the frame decorator class MethodInfo(object): def __init__(self, entry): self.blob = entry.blob self.pc = Types.as_long(entry.pc) self.sp = Types.as_long(entry.sp) if entry.bp is None: self.bp = 0 else: self.bp = Types.as_long(entry.bp) def decorate(self, frame): return [ frame ] # info for stub frame class StubMethodInfo(MethodInfo): def __init__(self, entry, name): super(StubMethodInfo, self).__init__(entry) self.name = name def decorate(self, frame): return [ OpenJDKFrameDecorator(frame, self.name, None, None) ] # common info for compiled, native or interpreted frame class JavaMethodInfo(MethodInfo): def __init__(self, entry): super(JavaMethodInfo, self).__init__(entry) def cache_method_info(self): methodptr = self.get_methodptr() self.method = Method(methodptr) def get_method(self): return self.method def method_name(self): return self.get_method().get_name() def filename(self): return self.get_method().get_klass_path() def decorate(self, frame): return [ OpenJDKFrameDecorator(frame, self.method_name(), self.filename(), None) ] # info for compiled frame class CompiledMethodInfo(JavaMethodInfo): def __init__(self, entry): t("CompiledMethodInfo.__init__") super(CompiledMethodInfo,self).__init__(entry) blob = self.blob nmethod = Types.to_type(blob, Types.nmethodp_t) self.methodptr = nmethod['_method'] const_method = self.methodptr['_constMethod'] bcbase = Types.cast_bytep(const_method + 1) self.code_begin = Types.as_long(blob['_code_begin']) # get bc and line number info from method self.cache_method_info() # get PC to BCI translator from the nmethod self.bytecode_index_reader = MethodBCIReader(nmethod, self.method) t("self.method_bci_stack = self.bytecode_index_reader.pc_to_method_bci_stack(self.pc)") self.method_bci_stack = self.bytecode_index_reader.pc_to_method_bci_stack(self.pc) # subclasses need to compute their method pointer def get_methodptr(self): return self.methodptr def format_method_name(self, method, is_outer): name = method.get_name() if is_outer: return ("[compiled offset=0x%x] %s" % (self.pc - self.code_begin, name)) else: return ("[inlined] %s" % name) def make_decorator(self, frame, pair, is_outer): t("make_decorator") method = pair['method'] bci = pair['bci'] methodname = self.format_method_name(method, is_outer) filename = method.get_klass_path() line = method.get_line(bci) if line < 0: line = None decorator = OpenJDKFrameDecorator(frame, methodname, filename, line) return decorator def decorate(self, frame): if self.method_bci_stack == None: return [ frame ] else: try: decorators = [] pairs = self.method_bci_stack debug("converting method_bci_stack = %s\n" % self.method_bci_stack) l = len(pairs) for i in range(l): pair = pairs[i] debug("decorating pair %s\n" % pair) decorator = self.make_decorator(frame, pair, i == (l - 1)) decorators.append(decorator) return decorators except Exception as arg: debug("!!! decorate oops %s / %s !!!\n" % (arg, traceback.format_exc())) return [ frame ] # info for native frame class NativeMethodInfo(JavaMethodInfo): def __init__(self, entry): t("NativeMethodInfo.__init__") super(NativeMethodInfo,self).__init__(entry) blob = self.blob nmethod = Types.to_type(blob, Types.nmethodp_t) self.methodptr = nmethod['_method'] self.code_begin = Types.as_long(blob['_code_begin']) # get bc and line number info from method self.cache_method_info() # subclasses need to compute their method pointer def get_methodptr(self): return self.methodptr def format_method_name(self, method): name = method.get_name() return ("[native offset=0x%x] %s" % (self.pc - self.code_begin, name)) def method_name(self): return self.format_method_name(self.method) # info for interpreted frame class InterpretedMethodInfo(JavaMethodInfo): def __init__(self, entry, bcp): super(InterpretedMethodInfo,self).__init__(entry) # interpreter frames store methodptr in slot 3 methodptr_offset = FrameConstants.interpreter_frame_method_offset() * 8 methodptr_addr = gdb.Value((self.bp + methodptr_offset) & 0xffffffffffffffff) methodptr_slot = methodptr_addr.cast(gdb.lookup_type("Method").pointer().pointer()) self.methodptr = methodptr_slot.dereference() # bytecode immediately follows const method const_method = self.methodptr['_constMethod'] bcbase = Types.cast_bytep(const_method + 1) debug("@@ bcbase = 0x%x\n" % Types.as_long(bcbase)) bcp_offset = FrameConstants.interpreter_frame_bcp_offset() * 8 if bcp is None: # interpreter frames store bytecodeptr in slot 8 bcp_addr = gdb.Value((self.bp + bcp_offset) & 0xffffffffffffffff) bcp_val = Types.cast_bytep(Types.load_ptr(bcp_addr)) else: bcp_val = Types.cast_bytep(bcp) # sanity check the register value -- it is sometimes off # when returning from a call out bcoff = Types.as_long(bcp_val - bcbase) if bcoff < 0 or bcoff >= 0x10000: # use the value in the frame slot bcp_addr = gdb.Value((self.bp + bcp_offset) & 0xffffffffffffffff) bcp_val = Types.cast_bytep(Types.load_ptr(bcp_addr)) self.bcoff = Types.as_long(bcp_val - bcbase) debug("@@ bcoff = 0x%x\n" % self.bcoff) # line number table immediately following bytecode bytecode_size = Types.as_int(const_method['_code_size']) self.is_native = (bytecode_size == 0) # n.b. data in compressed line_number_table block is u_char debug("bytecode_size = 0x%x\n" % bytecode_size) lnbase = Types.cast_bytep(bcbase + bytecode_size) debug("lnbase = 0x%x\n" % Types.as_long(lnbase)) self.line_number_reader = LineReader(lnbase) self.cache_method_info() def get_methodptr(self): return self.methodptr def format_method_name(self, method): name = method.get_name() if self.is_native: return "[native] " + name else: return ("[interpreted: bc = %d] " % self.bcoff) + name def line(self): line = self.line_number_reader.bc_to_line(self.bcoff) debug("bc_to_line(%d) = %d\n" % (self.bcoff, line)) if line < 0: line = None return line def decorate(self, frame): method = self.get_method() return [ OpenJDKFrameDecorator(frame, self.format_method_name(method), method.get_klass_path(), self.line()) ] class OpenJDKUnwinderCacheEntry(object): def __init__(self, blob, sp, pc, bp, bcp, name, codetype): t("OpenJDKUnwinderCacheEntry.__init__") self.blob = blob self.sp = sp self.pc = pc self.bp = bp self.codetype = codetype try: if codetype == "compiled": self.method_info = CompiledMethodInfo(self) elif codetype == "native": self.method_info = NativeMethodInfo(self) elif codetype == "interpreted": self.method_info = InterpretedMethodInfo(self, bcp) elif codetype == "stub": self.method_info = StubMethodInfo(self, name) else: self.method_info = None except Exception as arg: debug("!!! failed to cache info for %s frame [pc: 0x%x sp:0x%x bp 0x%x] !!!\n!!! %s !!!\n" % (codetype, pc, sp, bp, arg)) self.method_info = None # an unwinder class, an instance of which can be registered with gdb # to handle unwinding of frames. class OpenJDKUnwinder(Unwinder): def __init__(self): t("OpenJDKUnwinder.__init__") super(OpenJDKUnwinder, self).__init__("OpenJDKUnwinder") # blob name will be in format '0xHexDigits "AlphaNumSpaces"' self.matcher=re.compile('^0x[a-fA-F0-9]+ "(.*)"$') self.unwindercache = {} self.invocations = {} def set_filter(self, filter): self.filter = filter @property def enabled(self): return self._enabled # Intercept when the user runs `enable/disable unwinder` @enabled.setter def enabled(self, value): self._enabled = value if self.filter and value: self.filter.enabled = value # the method that gets called by the pyuw_sniffer def __call__(self, pending_frame): # sometimes when we call into python gdb routines # the call tries to re-establish the frame and ends # up calling the frame sniffer recursively # # so use a list keyed by thread to avoid recursive calls t("OpenJDKUnwinder.__call__") thread = gdb.selected_thread() if self.invocations.get(thread) != None: debug("!!! blocked %s !!!\n" % str(thread)) return None try: debug("!!! blocking %s !!!\n" % str(thread)) self.invocations[thread] = thread result = self.call_sub(pending_frame) debug("!!! unblocking %s !!!\n" % str(thread)) self.invocations[thread] = None return result except Exception as arg: debug("!!! __call__ oops %s !!!\n" % arg) debug("!!! unblocking %s !!!\n" % str(thread)) self.invocations[thread] = None return None def call_sub(self, pending_frame): t("OpenJDKUnwinder.__call_sub__") debug("@@ reading pending frame registers\n") pc = pending_frame.read_register('rip') debug("@@ pc = 0x%x\n" % Types.as_long(pc)) sp = pending_frame.read_register('rsp') debug("@@ sp = 0x%x\n" % Types.as_long(sp)) bp = pending_frame.read_register('rbp') debug("@@ bp = 0x%x\n" % Types.as_long(bp)) try: if not CodeCache.inrange(pc): t("not CodeCache.inrange(0x%x)\n" % pc) return None except Exception as arg: debug("@@ %s\n" % arg) return None if _dump_frame: debug_write(" pc = 0x%x\n" % Types.as_long(pc)) debug_write(" sp = 0x%x\n" % Types.as_long(sp)) debug_write(" bp = 0x%x\n" % Types.as_long(bp)) # this is a Java frame so we need to return unwind info # # the interpreter keeps the bytecode pointer (bcp) in a # register and saves it to the stack when it makes a call to # another Java method or to C++. if the current frame has a # bcp register value then we pass it in case the pending frame # is an intrepreter frame. that ensures we use the most up to # date bcp when the inferior has stopped at the top level in a # Java interpreted frame. it also saves us doing a redundant # stack read when the pending frame sits below a non-JITted # (C++) frame. n.b. if the current frame is a JITted frame # (i.e. one that we have already unwound) then rbp will not be # present. that's ok because the frame decorator can still # find the latest bcp value on the stack. bcp = pending_frame.read_register('r13') try: # convert returned value to a python int to force a check that # the register is defined. if not this will except bcp = gdb.Value(int(bcp)) debug("@@ bcp = 0x%x\n" % Types.as_long(bcp)) except Exception as arg: debug("@@ !!! call_sub oops %s !!! \n" % arg) bcp = None debug("@@ bcp = None\n") t("blob = CodeCache.findblob(pc)") blob = CodeCache.findblob(pc) t("if blob is None:") if blob is None: t("return None") return None # if the blob is an nmethod then we use the frame # size to identify the frame base otherwise we # use the value in rbp t("name = str(blob['_name'])") name = str(blob['_name']) # blob name will be in format '0xHexDigits "AlphaNumSpaces"' # and we just want the bit between the quotes m = self.matcher.match(name) if not m is None: debug("@@ m.group(1) == %s\n" % m.group(1)) name = m.group(1) if name == "nmethod": debug("@@ compiled %s\n" % name) codetype = 'compiled' # TODO -- need to check if frame is complete # i.e. if ((char *)pc - (char *)blob) > blob['_code_begin'] + blob['_frame_complete_offset'] # if not then we have not pushed a frame. # what do we do then? use SP as BP??? frame_size = blob['_frame_size'] debug("@@ frame_size = 0x%x\n" % int(frame_size)) # n.b. frame_size includes stacked rbp and rip hence the -2 bp = sp + ((frame_size - 2) * 8) debug("@@ revised bp = 0x%x\n" % Types.as_long(bp)) elif name == "native nmethod": debug("@@ native %s \n" % name) codetype = "native" elif name == "Interpreter": debug("@@ interpreted %s\n" %name) codetype = "interpreted" elif name[:4] == "Stub": debug("@@ stub %s\n" % name) codetype = "stub" else: debug("@@ unknown %s\n" % name) codetype = "unknown" # cache details of the current frame x = Types.as_long(sp) debug("@@ add %s cache entry for blob 0x%x at unwindercache[0x%x]\n" % (codetype, blob, x)) self.unwindercache[x] = OpenJDKUnwinderCacheEntry(blob, sp, pc, bp, bcp, name, codetype) t("next_bp = Types.load_long(bp)") next_bp = Types.load_long(bp) t("next_pc = Types.load_long(bp + 8)") next_pc = Types.load_long(bp + 8) # next_sp is normally just 2 words below current bp # but for interpreted frames we need to skip locals # so we pull caller_sp from the frame if codetype == "interpreted": interpreter_frame_sender_sp_offset = FrameConstants.interpreter_frame_sender_sp_offset() * 8 # interpreter frames store sender sp in slot 1 next_sp = Types.load_long(bp + interpreter_frame_sender_sp_offset) else: sender_sp_offset = FrameConstants.sender_sp_offset() * 8 next_sp = bp + sender_sp_offset # create unwind info for this frame t("frameid = OpenJDKFrameId(...)") frameid = OpenJDKFrameId(Types.to_voidp(next_sp), Types.to_type(next_pc, pc.type)) debug("@@ created frame id\n") t("unwind_info = pending_frame.create_unwind_info(frameid)") unwind_info = pending_frame.create_unwind_info(frameid) debug("@@ created unwind info\n") debug("@@ next_bp = 0x%x\n" % next_bp) debug("@@ next_pc = 0x%x\n" % next_pc) debug("@@ next_sp = 0x%x\n" % next_sp) # we must calculate pc, sp and bp. # # for now we only add the minimum of registers that we know # are valid. unwind_info.add_saved_register('rip', next_pc) unwind_info.add_saved_register('rsp', next_sp) unwind_info.add_saved_register('rbp', next_bp) if _dump_frame: debug_write("next pc = 0x%x\n" % Types.as_long(next_pc)) debug_write("next sp = 0x%x\n" % Types.as_long(next_sp)) debug_write("next bp = 0x%x\n" % Types.as_long(next_bp)) t("return unwind_info") return unwind_info # register the unwinder globally [probably really needs to be # registered solely with libjvm.so] def register_unwinder(): try: unwinder = None if _have_unwinder: unwinder = OpenJDKUnwinder() gdb.unwinder.register_unwinder(None, unwinder, replace=True) filt = OpenJDKFrameFilter(unwinder) gdb.frame_filters[filt.name] = filt except Exception as arg: traceback.format_exc() debug("!!! register_unwinder failed %s / %s !!!\n" % (arg, traceback.format_exc())) gdb.write("Failed to register OpenJDK unwinder and frame filter: %s\n" % arg) gdb.write("Is openjdk-*-dbg package installed?\n") register_unwinder()