# 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 re from gdb.FrameDecorator import FrameDecorator # we rely on the unwinder API provided by gdb.7.10 _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): gdb.write(msg) # pass def t(msg): gdb.write("%s\n" % msg) # pass # debug_write("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() 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 = "codeheap" 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_write("@@ heap.name = %s\n" % self.name) # debug_write("@@ heap.lo = 0x%x\n" % self.lo) # debug_write("@@ heap.hi = 0x%x\n" % self.hi) def inrange(self, x): # t("CodeHeap.inrange") return self.lo <= x and self.hi > x def findblob(self, pc): # t("CodeHeap.findblob") x = Types.as_long(pc) # debug_write("@@ findblob(%s, 0x%x)\n" % (self.name, pc)) # debug_write("@@ pc (%s) = 0x%x \n" % (str(pc.type), pc)) # debug_write("@@ self.lo = 0x%x\n" % self.lo) # debug_write("@@ 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_write("@@ pc in range\n") # t("segments = 0") segments = 0 # debug_write("@@ segmap_lo (%s) = 0x%x\n" % (str(self.segmap_lo.type), self.segmap_lo)) # debug_write("@@ self.lo = 0x%x \n" % self.lo) # debug_write("@@ 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_write("@@ offset = 0x%x\n" % offset) # t("shift = self.log2_segment_size") shift = self.log2_segment_size # debug_write("@@ 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_write("@@ 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_write("@@ 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_write("@@ segment = 0x%x\n" % segment) # t("tag = (self.segmap_lo + segment).dereference() & 0xff") tag = (self.segmap_lo + segment).dereference() & 0xff # debug_write("@@ 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_write("@@ lo = 0x%x\n" % self.lo) # debug_write("@@ 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_write("@@ 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_write("@@ 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 if cls.heap_list == []: try: heap = gdb.parse_and_eval("CodeCache::_heap") # debug_write("@@ CodeCache::_heap (%s) = 0x%x\n" % (heap.type, heap)) if Types.as_long(heap) == 0: raise gdb.GdbError("dbg.CodeCache.class_init : heap not ready!") lo = heap['_memory']['_low_boundary'] # debug_write("@@ lo = 0x%x\n" % lo) cls.lo = Types.as_long(lo) if Types.as_long(lo) == 0: raise gdb.GdbError("dbg.CodeCache.class_init : lo not ready!") hi = heap['_memory']['_high_boundary'] # debug_write("@@ hi = 0x%x\n" % hi) if Types.as_long(hi) == 0: raise gdb.GdbError("dbg.CodeCache.class_init : hi not ready!") cls.hi = Types.as_long(hi) codeheap = CodeHeap(heap) cls.heap_list.append(codeheap) cls.heap_count = 1 except Exception as arg: # debug_write("@@ oops %s\n" % arg) cls.heap_list = [] cls.heap_count = 0 cls.lo = 0 cls.hi = 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_write("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_write("@@ 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_write("@@ 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_bcx_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_bcx_offset = int(gdb.parse_and_eval("frame::interpreter_frame_bcx_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_bcx_offset(cls): cls.class_init() return cls._interpreter_frame_bcx_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_write("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_write("pos = %d\n" % pos) # debug_write("sum = 0x%x\n" % sum) lg_H_i = CompressedStream.lg_H i = 0 while (True): b_i = self.at(pos + i) # debug_write("b_%d = %d\n" % (i, b_i)) sum += (b_i << lg_H_i) # debug_write("sum = 0x%x\n" % sum) i += 1 if b_i < CompressedStream.L or i == CompressedStream.MAX_i: self.pos = pos + i # debug_write("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_write("nmethod (%s) = 0x%x\n" % (str(nmethod.type), Types.as_long(nmethod))) code_begin_offset = Types.as_int(nmethod['_code_offset']) code_end_offset = Types.as_int(nmethod['_data_offset']) scopes_pcs_begin_offset = Types.as_int(nmethod['_scopes_pcs_offset']) # debug_write("scopes_pcs_begin_offset = 0x%x\n" % scopes_pcs_begin_offset) scopes_pcs_end_offset = Types.as_int(nmethod['_dependencies_offset']) # debug_write("scopes_pcs_end_offset = 0x%x\n" % scopes_pcs_end_offset) header_begin = Types.cast_bytep(nmethod) self.code_begin = Types.as_long(header_begin + code_begin_offset) self.code_end = Types.as_long(header_begin + code_end_offset) self.scopes_pcs_begin = self.as_pcdesc_p(header_begin + scopes_pcs_begin_offset) # debug_write("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_write("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_write("nmethod = 0x%x\n" % nmethod) # debug_write("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 # debug_write("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_write("metadata_base = 0x%x\n" % Types.as_long(metadata_base)) scope = scopes_base + scope_decode_offset # debug_write("scope = 0x%x\n" % Types.as_long(scope)) stream = CompressedStream(scope) # debug_write("stream = %s\n" % stream) sender = stream.read_int() # debug_write("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_write("method,bci = %s,0x%x\n" % (method.get_name(), bci)) result = [ { 'method': method, 'bci': bci } ] while sender > 0: # debug_write("\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_write("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_write("\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_write("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_write("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_write("_bci = %d\n" % self._bci) # debug_write("_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__() 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: gdb.write("!!! function oops !!! %s\n" % arg) return None def method_name(self): return _methodname def filename(self): try: return self._filename except Exception as arg: gdb.write("!!! filename oops !!! %s\n" % arg) return None def line(self): try: return self._line except Exception as arg: gdb.write("!!! line oops !!! %s\n" % arg) return None # A frame filter for OpenJDK. class OpenJDKFrameFilter(object): def __init__(self, unwinder): self.name="OpenJDK" self.enabled = True self.priority = 100 self.unwinder = unwinder 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_write("@@ get info at unwindercache[0x%x]\n" % x) try: cache_entry = unwindercache[x] except Exception as arg: # n.b. no such entry throws an exception # just ignore and use existing frame return [ frame ] try: if cache_entry is None: # debug_write("@@ lookup found no cache_entry\n") return [ frame ] elif cache_entry.codetype == "unknown": # debug_write("@@ lookup found unknown cache_entry\n") return [ frame ] else: # debug_write("@@ 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: gdb.write("!!! maybe_wrap_frame oops !!! %s\n" % arg) return [ frame ] def flatten(self, list_of_lists): return [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) header_begin = Types.cast_bytep(nmethod) code_begin_offset = Types.as_int(nmethod['_code_offset']) self.code_begin = Types.as_long(header_begin + code_begin_offset) self.cache_method_info() # get PC to BCI translator from the nmethod self.bytecode_index_reader = MethodBCIReader(nmethod, self.method) 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_write("converting method_bci_stack = %s\n" % self.method_bci_stack) l = len(pairs) for i in range(l): pair = pairs[i] # debug_write("decorating pair %s\n" % pair) decorator = self.make_decorator(frame, pair, i == (l - 1)) decorators.append(decorator) return decorators except Exception as arg: gdb.write("!!! decorate oops %s !!!\n" % arg) 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'] const_method = self.methodptr['_constMethod'] bcbase = Types.cast_bytep(const_method + 1) header_begin = Types.cast_bytep(nmethod) code_begin_offset = Types.as_int(nmethod['_code_offset']) self.code_begin = Types.as_long(header_begin + code_begin_offset) 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_write("@@ bcbase = 0x%x\n" % Types.as_long(bcbase)) bcp_offset = FrameConstants.interpreter_frame_bcx_offset() * 8 if bcp is None: # interpreter frames store bytecodeptr in slot 7 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_write("@@ 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_write("bytecode_size = 0x%x\n" % bytecode_size) lnbase = Types.cast_bytep(bcbase + bytecode_size) # debug_write("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_write("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: gdb.write("!!! 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 = {} # 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_write("!!! blocked %s !!!\n" % str(thread)) return None try: # debug_write("!!! blocking %s !!!\n" % str(thread)) self.invocations[thread] = thread result = self.call_sub(pending_frame) # debug_write("!!! unblocking %s !!!\n" % str(thread)) self.invocations[thread] = None return result except Exception as arg: gdb.write("!!! __call__ oops %s !!!\n" % arg) # debug_write("!!! unblocking %s !!!\n" % str(thread)) self.invocations[thread] = None return None def call_sub(self, pending_frame): # t("OpenJDKUnwinder.__call_sub__") # debug_write("@@ reading pending frame registers\n") pc = pending_frame.read_register('rip') # debug_write("@@ pc = 0x%x\n" % Types.as_long(pc)) sp = pending_frame.read_register('rsp') # debug_write("@@ sp = 0x%x\n" % Types.as_long(sp)) bp = pending_frame.read_register('rbp') # debug_write("@@ 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_write("@@ %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_write("@@ bcp = 0x%x\n" % Types.as_long(bcp)) except Exception as arg: # debug_write("@@ !!! call_sub oops %s !!! \n" % arg) bcp = None # debug_write("@@ 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_write("@@ m.group(1) == %s\n" % m.group(1)) name = m.group(1) if name == "nmethod": # debug_write("@@ compiled %s\n" % name) codetype = 'compiled' # TODO -- need to check if frame is complete # i.e. if ((char *)pc - (char *)blob) > blob['_header_size'] + blob['_code_offset'] + 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_write("@@ 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_write("@@ revised bp = 0x%x\n" % Types.as_long(bp)) elif name == "native nmethod": # debug_write("@@ native %s \n" % name) codetype = "native" elif name == "Interpreter": # debug_write("@@ interpreted %s\n" %name) codetype = "interpreted" elif name[:4] == "Stub": # debug_write("@@ stub %s\n" % name) codetype = "stub" else: # debug_write("@@ unknown %s\n" % name) codetype = "unknown" # cache details of the current frame x = Types.as_long(sp) # debug_write("@@ 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_write("@@ created frame id\n") # t("unwind_info = pending_frame.create_unwind_info(frameid)") unwind_info = pending_frame.create_unwind_info(frameid) # debug_write("@@ created unwind info\n") # debug_write("@@ next_bp = 0x%x\n" % next_bp) # debug_write("@@ next_pc = 0x%x\n" % next_pc) # debug_write("@@ 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(): unwinder = None if _have_unwinder: unwinder = OpenJDKUnwinder() gdb.unwinder.register_unwinder(None, unwinder, replace=True) filt = OpenJDKFrameFilter(unwinder) gdb.frame_filters[filt.name] = filt register_unwinder()