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