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