xref: /aosp_15_r20/development/scripts/stack_core.py (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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