1#!/usr/bin/env python3 2# 3# Copyright (C) 2013 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"""stack symbolizes native crash dumps.""" 18 19import collections 20import functools 21import os 22import pathlib 23import re 24import subprocess 25import symbol 26import tempfile 27import unittest 28 29import example_crashes 30 31def ConvertTrace(lines): 32 tracer = TraceConverter() 33 print("Reading symbols from", symbol.SYMBOLS_DIR) 34 tracer.ConvertTrace(lines) 35 36class TraceConverter: 37 process_info_line = re.compile(r"(pid: [0-9]+, tid: [0-9]+.*)") 38 revision_line = re.compile(r"(Revision: '(.*)')") 39 signal_line = re.compile(r"(signal [0-9]+ \(.*\).*)") 40 abort_message_line = re.compile(r"(Abort message: '.*')") 41 thread_line = re.compile(r"(.*)(--- ){15}---") 42 dalvik_jni_thread_line = re.compile("(\".*\" prio=[0-9]+ tid=[0-9]+ NATIVE.*)") 43 dalvik_native_thread_line = re.compile("(\".*\" sysTid=[0-9]+ nice=[0-9]+.*)") 44 register_line = re.compile("$a") 45 trace_line = re.compile("$a") 46 sanitizer_trace_line = re.compile("$a") 47 value_line = re.compile("$a") 48 code_line = re.compile("$a") 49 zipinfo_central_directory_line = re.compile(r"Central\s+directory\s+entry") 50 zipinfo_central_info_match = re.compile( 51 r"^\s*(\S+)$\s*offset of local header from start of archive:\s*(\d+)" 52 r".*^\s*compressed size:\s+(\d+)", re.M | re.S) 53 unreachable_line = re.compile(r"((\d+ bytes in \d+ unreachable allocations)|" 54 r"(\d+ bytes unreachable at [0-9a-f]+)|" 55 r"(referencing \d+ unreachable bytes in \d+ allocation(s)?)|" 56 r"(and \d+ similar unreachable bytes in \d+ allocation(s)?))") 57 trace_lines = [] 58 value_lines = [] 59 last_frame = -1 60 width = "{8}" 61 spacing = "" 62 apk_info = dict() 63 lib_to_path = dict() 64 mte_fault_address = None 65 mte_stack_records = [] 66 67 # We use the "file" command line tool to extract BuildId from ELF files. 68 ElfInfo = collections.namedtuple("ElfInfo", ["bitness", "build_id"]) 69 readelf_output = re.compile(r"Class:\s*ELF(?P<bitness>32|64).*" 70 r"Build ID:\s*(?P<build_id>[0-9a-f]+)", 71 flags=re.DOTALL) 72 73 def UpdateBitnessRegexes(self): 74 if symbol.ARCH_IS_32BIT: 75 self.width = "{8}" 76 self.spacing = "" 77 else: 78 self.width = "{16}" 79 self.spacing = " " 80 self.register_line = re.compile(" (([ ]*\\b(\S*)\\b +[0-9a-f]" + self.width + "){1,5}$)") 81 82 # Note that both trace and value line matching allow for variable amounts of 83 # whitespace (e.g. \t). This is because the we want to allow for the stack 84 # tool to operate on AndroidFeedback provided system logs. AndroidFeedback 85 # strips out double spaces that are found in tombsone files and logcat output. 86 # 87 # Examples of matched trace lines include lines from tombstone files like: 88 # #00 pc 001cf42e /data/data/com.my.project/lib/libmyproject.so 89 # 90 # Or lines from AndroidFeedback crash report system logs like: 91 # 03-25 00:51:05.520 I/DEBUG ( 65): #00 pc 001cf42e /data/data/com.my.project/lib/libmyproject.so 92 # Please note the spacing differences. 93 self.trace_line = re.compile( 94 r".*" # Random start stuff. 95 r"\#(?P<frame>[0-9]+)" # Frame number. 96 r"[ \t]+..[ \t]+" # (space)pc(space). 97 r"(?P<offset>[0-9a-f]" + self.width + ")[ \t]+" # Offset (hex number given without 98 # 0x prefix). 99 r"(?P<dso>\[[^\]]+\]|[^\r\n \t]*)" # Library name. 100 r"( \(offset (?P<so_offset>0x[0-9a-fA-F]+)\))?" # Offset into the file to find the start of the shared so. 101 r"(?P<symbolpresent> \((?P<symbol>.*?)\))?" # Is the symbol there? (non-greedy) 102 r"( \(BuildId: (?P<build_id>.*)\))?" # Optional build-id of the ELF file. 103 r"[ \t]*$") # End of line (to expand non-greedy match). 104 # pylint: disable-msg=C6310 105 # Sanitizer output. This is different from debuggerd output, and it is easier to handle this as 106 # its own regex. Example: 107 # 08-19 05:29:26.283 397 403 I : #0 0xb6a15237 (/system/lib/libclang_rt.asan-arm-android.so+0x4f237) 108 self.sanitizer_trace_line = re.compile( 109 r".*" # Random start stuff. 110 r"\#(?P<frame>[0-9]+)" # Frame number. 111 r"[ \t]+0x[0-9a-f]+[ \t]+" # PC, not interesting to us. 112 r"\(" # Opening paren. 113 r"(?P<dso>[^+]+)" # Library name. 114 r"\+" # '+' 115 r"0x(?P<offset>[0-9a-f]+)" # Offset (hex number given with 116 # 0x prefix). 117 r"\)") # Closing paren. 118 # pylint: disable-msg=C6310 119 # Examples of matched value lines include: 120 # bea4170c 8018e4e9 /data/data/com.my.project/lib/libmyproject.so 121 # bea4170c 8018e4e9 /data/data/com.my.project/lib/libmyproject.so (symbol) 122 # 03-25 00:51:05.530 I/DEBUG ( 65): bea4170c 8018e4e9 /data/data/com.my.project/lib/libmyproject.so 123 # Again, note the spacing differences. 124 self.value_line = re.compile(r"(.*)([0-9a-f]" + self.width + r")[ \t]+([0-9a-f]" + self.width + r")[ \t]+([^\r\n \t]*)( \((.*)\))?") 125 # Lines from 'code around' sections of the output will be matched before 126 # value lines because otheriwse the 'code around' sections will be confused as 127 # value lines. 128 # 129 # Examples include: 130 # 801cf40c ffffc4cc 00b2f2c5 00b2f1c7 00c1e1a8 131 # 03-25 00:51:05.530 I/DEBUG ( 65): 801cf40c ffffc4cc 00b2f2c5 00b2f1c7 00c1e1a8 132 self.code_line = re.compile(r"(.*)[ \t]*[a-f0-9]" + self.width + 133 r"[ \t]*[a-f0-9]" + self.width + 134 r"[ \t]*[a-f0-9]" + self.width + 135 r"[ \t]*[a-f0-9]" + self.width + 136 r"[ \t]*[a-f0-9]" + self.width + 137 r"[ \t]*[ \r\n]") # pylint: disable-msg=C6310 138 self.mte_sync_line = re.compile(r".*signal 11 \(SIGSEGV\), code 9 \(SEGV_MTESERR\), fault addr 0x(?P<address>[0-9a-f]+)") 139 self.mte_stack_record_line = re.compile(r".*stack_record fp:0x(?P<fp>[0-9a-f]+) " 140 r"tag:0x(?P<tag>[0-9a-f]+) " 141 r"pc:(?P<object>[^+]+)\+0x(?P<offset>[0-9a-f]+)" 142 r"(?: \(BuildId: (?P<buildid>[A-Za-z0-9]+)\))?") 143 144 def CleanLine(self, ln): 145 # AndroidFeedback adds zero width spaces into its crash reports. These 146 # should be removed or the regular expresssions will fail to match. 147 return ln.encode().decode(encoding='utf8', errors='ignore') 148 149 def PrintTraceLines(self, trace_lines): 150 """Print back trace.""" 151 maxlen = max(len(tl[1]) for tl in trace_lines) 152 print("\nStack Trace:") 153 print(" RELADDR " + self.spacing + "FUNCTION".ljust(maxlen) + " FILE:LINE") 154 for tl in self.trace_lines: 155 (addr, symbol_with_offset, location) = tl 156 print(" %8s %s %s" % (addr, symbol_with_offset.ljust(maxlen), location)) 157 158 def PrintValueLines(self, value_lines): 159 """Print stack data values.""" 160 maxlen = max(len(tl[2]) for tl in self.value_lines) 161 print("\nStack Data:") 162 print(" ADDR " + self.spacing + "VALUE " + "FUNCTION".ljust(maxlen) + " FILE:LINE") 163 for vl in self.value_lines: 164 (addr, value, symbol_with_offset, location) = vl 165 print(" %8s %8s %s %s" % (addr, value, symbol_with_offset.ljust(maxlen), location)) 166 167 def MatchStackRecords(self): 168 if self.mte_fault_address is None: 169 return 170 fault_tag = (self.mte_fault_address >> 56) & 0xF 171 untagged_fault_address = self.mte_fault_address & ~(0xF << 56) 172 build_id_to_lib = {} 173 record_for_lib = collections.defaultdict(lambda: collections.defaultdict(set)) 174 for lib, buildid, offset, fp, tag in self.mte_stack_records: 175 if buildid is not None: 176 if buildid not in build_id_to_lib: 177 basename = os.path.basename(lib).split("!")[-1] 178 newlib = self.GetLibraryByBuildId(symbol.SYMBOLS_DIR, basename, buildid) 179 if newlib is not None: 180 build_id_to_lib[buildid] = newlib 181 lib = newlib 182 else: 183 lib = build_id_to_lib[buildid] 184 record_for_lib[lib][offset].add((fp, tag)) 185 186 for lib, values in record_for_lib.items(): 187 records = symbol.GetStackRecordsForSet(lib, values.keys()) or [] 188 for addr, function_name, local_name, file_line, frame_offset, size, tag_offset in records: 189 if frame_offset is None or size is None or tag_offset is None: 190 continue 191 for fp, tag in values[addr]: 192 obj_offset = untagged_fault_address - fp - frame_offset 193 if tag + tag_offset == fault_tag and obj_offset < size: 194 print('') 195 print('Potentially referenced stack object:') 196 print(' %d bytes inside a variable "%s" in stack frame of function "%s"'% (obj_offset, local_name, function_name)) 197 print(' at %s' % file_line) 198 199 def PrintOutput(self, trace_lines, value_lines): 200 if self.trace_lines: 201 self.PrintTraceLines(self.trace_lines) 202 if self.value_lines: 203 self.PrintValueLines(self.value_lines) 204 if self.mte_stack_records: 205 self.MatchStackRecords() 206 207 def PrintDivider(self): 208 print("\n-----------------------------------------------------\n") 209 210 def DeleteApkTmpFiles(self): 211 for _, _, tmp_files in self.apk_info.values(): 212 for tmp_file in tmp_files.values(): 213 os.unlink(tmp_file) 214 215 def ConvertTrace(self, lines): 216 lines = [self.CleanLine(line) for line in lines] 217 try: 218 if symbol.ARCH_IS_32BIT is None: 219 symbol.SetBitness(lines) 220 self.UpdateBitnessRegexes() 221 for line in lines: 222 self.ProcessLine(line) 223 self.PrintOutput(self.trace_lines, self.value_lines) 224 finally: 225 # Delete any temporary files created while processing the lines. 226 self.DeleteApkTmpFiles() 227 228 def MatchTraceLine(self, line): 229 match = self.trace_line.match(line) 230 if match: 231 return {"frame": match.group("frame"), 232 "offset": match.group("offset"), 233 "so_offset": match.group("so_offset"), 234 "dso": match.group("dso"), 235 "symbol_present": bool(match.group("symbolpresent")), 236 "symbol_name": match.group("symbol"), 237 "build_id": match.group("build_id")} 238 match = self.sanitizer_trace_line.match(line) 239 if match: 240 return {"frame": match.group("frame"), 241 "offset": match.group("offset"), 242 "so_offset": None, 243 "dso": match.group("dso"), 244 "symbol_present": False, 245 "symbol_name": None, 246 "build_id": None} 247 return None 248 249 def ExtractLibFromApk(self, apk, shared_lib_name): 250 # Create a temporary file containing the shared library from the apk. 251 tmp_file = None 252 try: 253 tmp_fd, tmp_file = tempfile.mkstemp() 254 if subprocess.call(["unzip", "-p", apk, shared_lib_name], stdout=tmp_fd) == 0: 255 os.close(tmp_fd) 256 shared_file = tmp_file 257 tmp_file = None 258 return shared_file 259 finally: 260 if tmp_file: 261 os.close(tmp_fd) 262 os.unlink(tmp_file) 263 return None 264 265 def ProcessCentralInfo(self, offset_list, central_info): 266 match = self.zipinfo_central_info_match.search(central_info) 267 if not match: 268 raise Exception("Cannot find all info from zipinfo\n" + central_info) 269 name = match.group(1) 270 start = int(match.group(2)) 271 end = start + int(match.group(3)) 272 273 offset_list.append([name, start, end]) 274 return name, start, end 275 276 def GetLibFromApk(self, apk, offset): 277 # Convert the string to hex. 278 offset = int(offset, 16) 279 280 # Check if we already have information about this offset. 281 if apk in self.apk_info: 282 apk_full_path, offset_list, tmp_files = self.apk_info[apk] 283 for file_name, start, end in offset_list: 284 if offset >= start and offset < end: 285 if file_name in tmp_files: 286 return file_name, tmp_files[file_name] 287 tmp_file = self.ExtractLibFromApk(apk_full_path, file_name) 288 if tmp_file: 289 tmp_files[file_name] = tmp_file 290 return file_name, tmp_file 291 break 292 return None, None 293 294 if not "ANDROID_PRODUCT_OUT" in os.environ: 295 print("ANDROID_PRODUCT_OUT environment variable not set.") 296 return None, None 297 out_dir = os.environ["ANDROID_PRODUCT_OUT"] 298 if not os.path.exists(out_dir): 299 print("ANDROID_PRODUCT_OUT", out_dir, "does not exist.") 300 return None, None 301 if apk.startswith("/"): 302 apk_full_path = out_dir + apk 303 else: 304 apk_full_path = os.path.join(out_dir, apk) 305 if not os.path.exists(apk_full_path): 306 print("Cannot find apk", apk) 307 return None, None 308 309 cmd = subprocess.Popen(["zipinfo", "-v", apk_full_path], stdout=subprocess.PIPE, 310 encoding='utf8') 311 # Find the first central info marker. 312 for line in cmd.stdout: 313 if self.zipinfo_central_directory_line.search(line): 314 break 315 316 central_info = "" 317 file_name = None 318 offset_list = [] 319 for line in cmd.stdout: 320 match = self.zipinfo_central_directory_line.search(line) 321 if match: 322 cur_name, start, end = self.ProcessCentralInfo(offset_list, central_info) 323 if not file_name and offset >= start and offset < end: 324 file_name = cur_name 325 central_info = "" 326 else: 327 central_info += line 328 if central_info: 329 cur_name, start, end = self.ProcessCentralInfo(offset_list, central_info) 330 if not file_name and offset >= start and offset < end: 331 file_name = cur_name 332 333 # Make sure the offset_list is sorted, the zip file does not guarantee 334 # that the entries are in order. 335 offset_list = sorted(offset_list, key=lambda entry: entry[1]) 336 337 # Save the information from the zip. 338 tmp_files = dict() 339 self.apk_info[apk] = [apk_full_path, offset_list, tmp_files] 340 if not file_name: 341 return None, None 342 tmp_shared_lib = self.ExtractLibFromApk(apk_full_path, file_name) 343 if tmp_shared_lib: 344 tmp_files[file_name] = tmp_shared_lib 345 return file_name, tmp_shared_lib 346 return None, None 347 348 # Find all files in the symbols directory and group them by basename (without directory). 349 @functools.lru_cache(maxsize=None) 350 def GlobSymbolsDir(self, symbols_dir): 351 files_by_basename = {} 352 for path in sorted(pathlib.Path(symbols_dir).glob("**/*")): 353 if os.path.isfile(path): 354 files_by_basename.setdefault(path.name, []).append(path) 355 return files_by_basename 356 357 # Use the "file" command line tool to find the bitness and build_id of given ELF file. 358 @functools.lru_cache(maxsize=None) 359 def GetLibraryInfo(self, lib): 360 stdout = subprocess.check_output([symbol.ToolPath("llvm-readelf"), "-h", "-n", lib], text=True) 361 match = self.readelf_output.search(stdout) 362 if match: 363 return self.ElfInfo(bitness=match.group("bitness"), build_id=match.group("build_id")) 364 return None 365 366 # Search for a library with the given basename and build_id anywhere in the symbols directory. 367 @functools.lru_cache(maxsize=None) 368 def GetLibraryByBuildId(self, symbols_dir, basename, build_id): 369 for candidate in self.GlobSymbolsDir(symbols_dir).get(basename, []): 370 info = self.GetLibraryInfo(candidate) 371 if info and info.build_id == build_id: 372 return "/" + str(candidate.relative_to(symbols_dir)) 373 return None 374 375 def GetLibPath(self, lib): 376 if lib in self.lib_to_path: 377 return self.lib_to_path[lib] 378 379 lib_path = self.FindLibPath(lib) 380 self.lib_to_path[lib] = lib_path 381 return lib_path 382 383 def FindLibPath(self, lib): 384 symbol_dir = symbol.SYMBOLS_DIR 385 if os.path.isfile(symbol_dir + lib): 386 return lib 387 388 # Try and rewrite any apex files if not found in symbols. 389 # For some reason, the directory in symbols does not match 390 # the path on system. 391 # The path is com.android.<directory> on device, but 392 # com.google.android.<directory> in symbols. 393 new_lib = lib.replace("/com.android.", "/com.google.android.") 394 if os.path.isfile(symbol_dir + new_lib): 395 return new_lib 396 397 # When using atest, test paths are different between the out/ directory 398 # and device. Apply fixups. 399 if not lib.startswith("/data/local/tests/") and not lib.startswith("/data/local/tmp/"): 400 print("WARNING: Cannot find %s in symbol directory" % lib) 401 return lib 402 403 test_name = lib.rsplit("/", 1)[-1] 404 test_dir = "/data/nativetest" 405 test_dir_bitness = "" 406 if symbol.ARCH_IS_32BIT: 407 bitness = "32" 408 else: 409 bitness = "64" 410 test_dir_bitness = "64" 411 412 # Unfortunately, the location of the real symbol file is not 413 # standardized, so we need to go hunting for it. 414 415 # This is in vendor, look for the value in: 416 # /data/nativetest{64}/vendor/test_name/test_name 417 if lib.startswith("/data/local/tests/vendor/"): 418 lib_path = os.path.join(test_dir + test_dir_bitness, "vendor", test_name, test_name) 419 if os.path.isfile(symbol_dir + lib_path): 420 return lib_path 421 422 # Look for the path in: 423 # /data/nativetest{64}/test_name/test_name 424 lib_path = os.path.join(test_dir + test_dir_bitness, test_name, test_name) 425 if os.path.isfile(symbol_dir + lib_path): 426 return lib_path 427 428 # CtsXXX tests are in really non-standard locations try: 429 # /data/nativetest/{test_name} 430 lib_path = os.path.join(test_dir, test_name) 431 if os.path.isfile(symbol_dir + lib_path): 432 return lib_path 433 # Try: 434 # /data/nativetest/{test_name}{32|64} 435 lib_path += bitness 436 if os.path.isfile(symbol_dir + lib_path): 437 return lib_path 438 439 # Cannot find location, give up and return the original path 440 print("WARNING: Cannot find %s in symbol directory" % lib) 441 return lib 442 443 444 def ProcessLine(self, line): 445 ret = False 446 process_header = self.process_info_line.search(line) 447 signal_header = self.signal_line.search(line) 448 abort_message_header = self.abort_message_line.search(line) 449 thread_header = self.thread_line.search(line) 450 register_header = self.register_line.search(line) 451 revision_header = self.revision_line.search(line) 452 dalvik_jni_thread_header = self.dalvik_jni_thread_line.search(line) 453 dalvik_native_thread_header = self.dalvik_native_thread_line.search(line) 454 unreachable_header = self.unreachable_line.search(line) 455 if process_header or signal_header or abort_message_header or thread_header or \ 456 register_header or dalvik_jni_thread_header or dalvik_native_thread_header or \ 457 revision_header or unreachable_header: 458 ret = True 459 if self.trace_lines or self.value_lines or self.mte_stack_records: 460 self.PrintOutput(self.trace_lines, self.value_lines) 461 self.PrintDivider() 462 self.trace_lines = [] 463 self.value_lines = [] 464 self.mte_fault_address = None 465 self.mte_stack_records = [] 466 self.last_frame = -1 467 if self.mte_sync_line.match(line): 468 match = self.mte_sync_line.match(line) 469 self.mte_fault_address = int(match.group("address"), 16) 470 if process_header: 471 print(process_header.group(1)) 472 if signal_header: 473 print(signal_header.group(1)) 474 if abort_message_header: 475 print(abort_message_header.group(1)) 476 if register_header: 477 print(register_header.group(1)) 478 if thread_header: 479 print(thread_header.group(1)) 480 if dalvik_jni_thread_header: 481 print(dalvik_jni_thread_header.group(1)) 482 if dalvik_native_thread_header: 483 print(dalvik_native_thread_header.group(1)) 484 if revision_header: 485 print(revision_header.group(1)) 486 if unreachable_header: 487 print(unreachable_header.group(1)) 488 return True 489 trace_line_dict = self.MatchTraceLine(line) 490 if trace_line_dict is not None: 491 ret = True 492 frame = int(trace_line_dict["frame"]) 493 code_addr = trace_line_dict["offset"] 494 area = trace_line_dict["dso"] 495 so_offset = trace_line_dict["so_offset"] 496 symbol_present = trace_line_dict["symbol_present"] 497 symbol_name = trace_line_dict["symbol_name"] 498 build_id = trace_line_dict["build_id"] 499 500 if frame <= self.last_frame and (self.trace_lines or self.value_lines): 501 self.PrintOutput(self.trace_lines, self.value_lines) 502 self.PrintDivider() 503 self.trace_lines = [] 504 self.value_lines = [] 505 self.last_frame = frame 506 507 if area == "<unknown>" or area == "[heap]" or area == "[stack]": 508 self.trace_lines.append((code_addr, "", area)) 509 else: 510 # If this is an apk, it usually means that there is actually 511 # a shared so that was loaded directly out of it. In that case, 512 # extract the shared library and the name of the shared library. 513 lib = None 514 # The format of the map name: 515 # Some.apk!libshared.so 516 # or 517 # Some.apk 518 if so_offset: 519 # If it ends in apk, we are done. 520 apk = None 521 if area.endswith(".apk"): 522 apk = area 523 else: 524 index = area.rfind(".so!") 525 if index != -1: 526 # Sometimes we'll see something like: 527 # #01 pc abcd libart.so!libart.so (offset 0x134000) 528 # Remove everything after the ! and zero the offset value. 529 area = area[0:index + 3] 530 so_offset = 0 531 else: 532 index = area.rfind(".apk!") 533 if index != -1: 534 apk = area[0:index + 4] 535 if apk: 536 lib_name, lib = self.GetLibFromApk(apk, so_offset) 537 else: 538 # Sometimes we'll see something like: 539 # #01 pc abcd libart.so!libart.so 540 # Remove everything after the !. 541 index = area.rfind(".so!") 542 if index != -1: 543 area = area[0:index + 3] 544 if not lib: 545 lib = area 546 lib_name = None 547 548 if build_id: 549 # If we have the build_id, do a brute-force search of the symbols directory. 550 basename = os.path.basename(lib).split("!")[-1] 551 lib = self.GetLibraryByBuildId(symbol.SYMBOLS_DIR, basename, build_id) 552 if not lib: 553 print("WARNING: Cannot find {} with build id {} in symbols directory." 554 .format(basename, build_id)) 555 else: 556 # When using atest, test paths are different between the out/ directory 557 # and device. Apply fixups. 558 lib = self.GetLibPath(lib) 559 560 # If a calls b which further calls c and c is inlined to b, we want to 561 # display "a -> b -> c" in the stack trace instead of just "a -> c" 562 info = symbol.SymbolInformation(lib, code_addr) 563 nest_count = len(info) - 1 564 for (source_symbol, source_location, symbol_with_offset) in info: 565 if not source_symbol: 566 if symbol_present: 567 source_symbol = symbol.CallCppFilt(symbol_name) 568 else: 569 source_symbol = "<unknown>" 570 if not symbol.VERBOSE: 571 source_symbol = symbol.FormatSymbolWithoutParameters(source_symbol) 572 symbol_with_offset = symbol.FormatSymbolWithoutParameters(symbol_with_offset) 573 if not source_location: 574 source_location = area 575 if lib_name: 576 source_location += "(" + lib_name + ")" 577 if nest_count > 0: 578 nest_count = nest_count - 1 579 arrow = "v------>" 580 if not symbol.ARCH_IS_32BIT: 581 arrow = "v-------------->" 582 self.trace_lines.append((arrow, source_symbol, source_location)) 583 else: 584 if not symbol_with_offset: 585 symbol_with_offset = source_symbol 586 self.trace_lines.append((code_addr, symbol_with_offset, source_location)) 587 if self.code_line.match(line): 588 # Code lines should be ignored. If this were exluded the 'code around' 589 # sections would trigger value_line matches. 590 return ret 591 if self.value_line.match(line): 592 ret = True 593 match = self.value_line.match(line) 594 (unused_, addr, value, area, symbol_present, symbol_name) = match.groups() 595 if area == "<unknown>" or area == "[heap]" or area == "[stack]" or not area: 596 self.value_lines.append((addr, value, "", area)) 597 else: 598 info = symbol.SymbolInformation(area, value) 599 (source_symbol, source_location, object_symbol_with_offset) = info.pop() 600 # If there is no information, skip this. 601 if source_symbol or source_location or object_symbol_with_offset: 602 if not source_symbol: 603 if symbol_present: 604 source_symbol = symbol.CallCppFilt(symbol_name) 605 else: 606 source_symbol = "<unknown>" 607 if not source_location: 608 source_location = area 609 if not object_symbol_with_offset: 610 object_symbol_with_offset = source_symbol 611 self.value_lines.append((addr, 612 value, 613 object_symbol_with_offset, 614 source_location)) 615 if self.mte_stack_record_line.match(line): 616 ret = True 617 match = self.mte_stack_record_line.match(line) 618 if self.mte_fault_address is not None: 619 self.mte_stack_records.append( 620 (match.group("object"), 621 match.group("buildid"), 622 int(match.group("offset"), 16), 623 int(match.group("fp"), 16), 624 int(match.group("tag"), 16))) 625 626 return ret 627 628 629class RegisterPatternTests(unittest.TestCase): 630 def assert_register_matches(self, abi, example_crash, stupid_pattern): 631 tc = TraceConverter() 632 lines = example_crash.split('\n') 633 symbol.SetBitness(lines) 634 tc.UpdateBitnessRegexes() 635 for line in lines: 636 tc.ProcessLine(line) 637 is_register = (re.search(stupid_pattern, line) is not None) 638 matched = (tc.register_line.search(line) is not None) 639 self.assertEqual(matched, is_register, line) 640 tc.PrintOutput(tc.trace_lines, tc.value_lines) 641 642 def test_arm_registers(self): 643 self.assert_register_matches("arm", example_crashes.arm, '\\b(r0|r4|r8|ip|scr)\\b') 644 645 def test_arm64_registers(self): 646 self.assert_register_matches("arm64", example_crashes.arm64, '\\b(x0|x4|x8|x12|x16|x20|x24|x28|sp|v[1-3]?[0-9])\\b') 647 648 def test_x86_registers(self): 649 self.assert_register_matches("x86", example_crashes.x86, '\\b(eax|esi|xcs|eip)\\b') 650 651 def test_x86_64_registers(self): 652 self.assert_register_matches("x86_64", example_crashes.x86_64, '\\b(rax|rsi|r8|r12|cs|rip)\\b') 653 654 def test_riscv64_registers(self): 655 self.assert_register_matches("riscv64", example_crashes.riscv64, '\\b(gp|t2|t6|s3|s7|s11|a3|a7|sp)\\b') 656 657class LibmemunreachablePatternTests(unittest.TestCase): 658 def test_libmemunreachable(self): 659 tc = TraceConverter() 660 lines = example_crashes.libmemunreachable.split('\n') 661 662 symbol.SetBitness(lines) 663 self.assertTrue(symbol.ARCH_IS_32BIT) 664 tc.UpdateBitnessRegexes() 665 header_lines = 0 666 trace_lines = 0 667 for line in lines: 668 tc.ProcessLine(line) 669 if re.search(tc.unreachable_line, line) is not None: 670 header_lines += 1 671 if tc.MatchTraceLine(line) is not None: 672 trace_lines += 1 673 self.assertEqual(header_lines, 3) 674 self.assertEqual(trace_lines, 2) 675 tc.PrintOutput(tc.trace_lines, tc.value_lines) 676 677class LongASANStackTests(unittest.TestCase): 678 # Test that a long ASAN-style (non-padded frame numbers) stack trace is not split into two 679 # when the frame number becomes two digits. This happened before as the frame number was 680 # handled as a string and not converted to an integral. 681 def test_long_asan_crash(self): 682 tc = TraceConverter() 683 lines = example_crashes.long_asan_crash.splitlines() 684 symbol.SetBitness(lines) 685 tc.UpdateBitnessRegexes() 686 # Test by making sure trace_line_count is monotonically non-decreasing. If the stack trace 687 # is split, a separator is printed and trace_lines is flushed. 688 trace_line_count = 0 689 for line in lines: 690 tc.ProcessLine(line) 691 self.assertLessEqual(trace_line_count, len(tc.trace_lines)) 692 trace_line_count = len(tc.trace_lines) 693 # The split happened at transition of frame #9 -> #10. Make sure we have parsed (and stored) 694 # more than ten frames. 695 self.assertGreater(trace_line_count, 10) 696 tc.PrintOutput(tc.trace_lines, tc.value_lines) 697 698class ValueLinesTest(unittest.TestCase): 699 def test_value_line_skipped(self): 700 tc = TraceConverter() 701 symbol.ARCH_IS_32BIT = True 702 tc.UpdateBitnessRegexes() 703 tc.ProcessLine(" 12345678 00001000 .") 704 self.assertEqual([], tc.value_lines) 705 706if __name__ == '__main__': 707 unittest.main(verbosity=2) 708