#!/usr/bin/python # # Copyright (C) 2023 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import argparse import bisect import os import subprocess import sys PROT_READ = 1 PROT_WRITE = 2 PROT_EXEC = 4 class MapEntry: start = -1 end = -1 prot = 0 offset = 0 file = "" base_addr = -1 class TraceParser: def __init__(self, tombstone, trace, symbols_dir): self.maps = {} self.tombstone = tombstone self.trace = trace self.symbols_dir = symbols_dir def parse_prot(self, prot_str): prot = 0 if prot_str[0] == "r": prot |= PROT_READ if prot_str[1] == "w": prot |= PROT_WRITE if prot_str[2] == "x": prot |= PROT_EXEC return prot def parse_tombstone_map_entry(self, line, line_number): if not line.startswith(" ") and not line.startswith("--->"): raise Exception( "Unexpected line (" + line_number + ") in maps section: " + line ) if line.startswith("--->Fault address"): return line = line[3:] # throw away indent/prefix entries = line.split(maxsplit=5) addrs = entries[0].split("-") map_entry = MapEntry() map_entry.start = int(addrs[0].replace("'", ""), 16) map_entry.end = int(addrs[1].replace("'", ""), 16) map_entry.prot = self.parse_prot(entries[1]) map_entry.offset = int(entries[2], 16) map_entry.size = int(entries[3], 16) if len(entries) >= 5: map_entry.file = entries[4] # The default base address is start map_entry.base_addr = map_entry.start # Skip PROT_NONE mappings so they do not interfere with # file mappings if map_entry.prot == 0: return self.maps[map_entry.start] = map_entry def read_maps_from_tombstone(self): with open(self.tombstone, "r") as f: maps_section_started = False line_number = 0 for line in f: line_number += 1 if maps_section_started: # Maps section ends when we hit either '---------' or end of file if line.startswith("---------"): break self.parse_tombstone_map_entry(line, line_number) else: maps_section_started = line.startswith("memory map") def calculate_base_addr_for_map_entries(self): # Ascending order of start_addr (key) is important here last_file = None current_base_addr = -1 for key in sorted(self.maps): # For now we are assuming load_bias is 0, revisit once proved otherwise # note that load_bias printed in tombstone is incorrect atm map = self.maps[key] if not map.file: continue # treat /memfd as if it was anon mapping if map.file.startswith("/memfd:"): continue if map.file != last_file: last_file = map.file current_base_addr = map.start map.base_addr = current_base_addr def addr2line(self, address, file): if not file: print("error: no file") return None p = subprocess.run( ["addr2line", "-e", self.symbols_dir + file, hex(address)], capture_output=True, text=True, ) if p.returncode != 0: # print("error: ", p.stderr) return None return p.stdout.strip() def symbolize_trace(self): with open(self.trace, "r") as f: sorted_start_addresses = sorted(self.maps.keys()) for line in f: tokens = line.split(maxsplit=2) if len(tokens) <= 2: continue msg = tokens[2] if not msg.startswith("RunGeneratedCode @"): continue address = int(msg.split("@")[1].strip(), 16) pos = bisect.bisect_right(sorted_start_addresses, address) map = self.maps[sorted_start_addresses[pos]] if address > map.end: print("%x (not maped)" % address) continue relative_addr = address - map.base_addr file_and_line = self.addr2line(relative_addr, map.file) if file_and_line: print( "%x (%s+%x) %s" % (address, map.file, relative_addr, file_and_line) ) else: print("%x (%s+%x)" % (address, map.file, relative_addr)) def parse(self): self.read_maps_from_tombstone() self.calculate_base_addr_for_map_entries() self.symbolize_trace() def get_symbol_dir(args): if args.symbols_dir: return symbols_dir product_out = os.environ.get("ANDROID_PRODUCT_OUT") if not product_out: raise Error( "--symbols_dir is not set and unable to resolve ANDROID_PRODUCT_OUT via" " environment variable" ) return product_out + "/symbols" def main(): argument_parser = argparse.ArgumentParser() argument_parser.add_argument( "trace", help="file containing berberis trace output" ) # TODO(b/232598137): Make it possible to read maps from /proc/pid/maps format as an # alternative option argument_parser.add_argument( "tombstone", help="Tombstone of the corresponding crash" ) argument_parser.add_argument( "--symbols_dir", help="Symbols dir (default is '$ANDROID_PRODUCT_OUT/symbols')", ) args = argument_parser.parse_args() parser = TraceParser(args.tombstone, args.trace, get_symbol_dir(args)) parser.parse() if __name__ == "__main__": sys.exit(main())