1#!/usr/bin/python 2# 3# Copyright (C) 2023 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18import argparse 19import bisect 20import os 21import subprocess 22import sys 23 24PROT_READ = 1 25PROT_WRITE = 2 26PROT_EXEC = 4 27 28 29class MapEntry: 30 start = -1 31 end = -1 32 prot = 0 33 offset = 0 34 file = "" 35 base_addr = -1 36 37 38class TraceParser: 39 40 def __init__(self, tombstone, trace, symbols_dir): 41 self.maps = {} 42 self.tombstone = tombstone 43 self.trace = trace 44 self.symbols_dir = symbols_dir 45 46 def parse_prot(self, prot_str): 47 prot = 0 48 if prot_str[0] == "r": 49 prot |= PROT_READ 50 if prot_str[1] == "w": 51 prot |= PROT_WRITE 52 if prot_str[2] == "x": 53 prot |= PROT_EXEC 54 55 return prot 56 57 def parse_tombstone_map_entry(self, line, line_number): 58 if not line.startswith(" ") and not line.startswith("--->"): 59 raise Exception( 60 "Unexpected line (" + line_number + ") in maps section: " + line 61 ) 62 63 if line.startswith("--->Fault address"): 64 return 65 66 line = line[3:] # throw away indent/prefix 67 68 entries = line.split(maxsplit=5) 69 70 addrs = entries[0].split("-") 71 map_entry = MapEntry() 72 map_entry.start = int(addrs[0].replace("'", ""), 16) 73 map_entry.end = int(addrs[1].replace("'", ""), 16) 74 map_entry.prot = self.parse_prot(entries[1]) 75 map_entry.offset = int(entries[2], 16) 76 map_entry.size = int(entries[3], 16) 77 if len(entries) >= 5: 78 map_entry.file = entries[4] 79 80 # The default base address is start 81 map_entry.base_addr = map_entry.start 82 83 # Skip PROT_NONE mappings so they do not interfere with 84 # file mappings 85 if map_entry.prot == 0: 86 return 87 88 self.maps[map_entry.start] = map_entry 89 90 def read_maps_from_tombstone(self): 91 with open(self.tombstone, "r") as f: 92 maps_section_started = False 93 line_number = 0 94 for line in f: 95 line_number += 1 96 if maps_section_started: 97 # Maps section ends when we hit either '---------' or end of file 98 if line.startswith("---------"): 99 break 100 self.parse_tombstone_map_entry(line, line_number) 101 else: 102 maps_section_started = line.startswith("memory map") 103 104 def calculate_base_addr_for_map_entries(self): 105 # Ascending order of start_addr (key) is important here 106 last_file = None 107 current_base_addr = -1 108 for key in sorted(self.maps): 109 # For now we are assuming load_bias is 0, revisit once proved otherwise 110 # note that load_bias printed in tombstone is incorrect atm 111 map = self.maps[key] 112 if not map.file: 113 continue 114 115 # treat /memfd as if it was anon mapping 116 if map.file.startswith("/memfd:"): 117 continue 118 119 if map.file != last_file: 120 last_file = map.file 121 current_base_addr = map.start 122 123 map.base_addr = current_base_addr 124 125 def addr2line(self, address, file): 126 if not file: 127 print("error: no file") 128 return None 129 130 p = subprocess.run( 131 ["addr2line", "-e", self.symbols_dir + file, hex(address)], 132 capture_output=True, 133 text=True, 134 ) 135 if p.returncode != 0: 136 # print("error: ", p.stderr) 137 return None 138 return p.stdout.strip() 139 140 def symbolize_trace(self): 141 with open(self.trace, "r") as f: 142 sorted_start_addresses = sorted(self.maps.keys()) 143 144 for line in f: 145 tokens = line.split(maxsplit=2) 146 if len(tokens) <= 2: 147 continue 148 msg = tokens[2] 149 if not msg.startswith("RunGeneratedCode @"): 150 continue 151 152 address = int(msg.split("@")[1].strip(), 16) 153 154 pos = bisect.bisect_right(sorted_start_addresses, address) 155 map = self.maps[sorted_start_addresses[pos]] 156 157 if address > map.end: 158 print("%x (not maped)" % address) 159 continue 160 161 relative_addr = address - map.base_addr 162 163 file_and_line = self.addr2line(relative_addr, map.file) 164 if file_and_line: 165 print( 166 "%x (%s+%x) %s" % (address, map.file, relative_addr, file_and_line) 167 ) 168 else: 169 print("%x (%s+%x)" % (address, map.file, relative_addr)) 170 171 def parse(self): 172 self.read_maps_from_tombstone() 173 self.calculate_base_addr_for_map_entries() 174 self.symbolize_trace() 175 176 177def get_symbol_dir(args): 178 if args.symbols_dir: 179 return symbols_dir 180 181 product_out = os.environ.get("ANDROID_PRODUCT_OUT") 182 if not product_out: 183 raise Error( 184 "--symbols_dir is not set and unable to resolve ANDROID_PRODUCT_OUT via" 185 " environment variable" 186 ) 187 188 return product_out + "/symbols" 189 190 191def main(): 192 argument_parser = argparse.ArgumentParser() 193 argument_parser.add_argument( 194 "trace", help="file containing berberis trace output" 195 ) 196 # TODO(b/232598137): Make it possible to read maps from /proc/pid/maps format as an 197 # alternative option 198 argument_parser.add_argument( 199 "tombstone", help="Tombstone of the corresponding crash" 200 ) 201 argument_parser.add_argument( 202 "--symbols_dir", 203 help="Symbols dir (default is '$ANDROID_PRODUCT_OUT/symbols')", 204 ) 205 206 args = argument_parser.parse_args() 207 208 parser = TraceParser(args.tombstone, args.trace, get_symbol_dir(args)) 209 parser.parse() 210 211 212if __name__ == "__main__": 213 sys.exit(main()) 214 215