• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright (C) 2024 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 functools
20import pathlib
21import subprocess
22import re
23from pathlib import Path
24from typing import Callable, Dict, Optional, Tuple
25
26import etm_types as etm
27from simpleperf_report_lib import ReportLib
28from simpleperf_utils import bytes_to_str, BinaryFinder, EtmContext, log_exit, ReadElf, Objdump, ToolFinder
29
30
31class Tracer:
32    def __init__(self, lib: ReportLib, binary_finder: BinaryFinder, objdump: Objdump) -> None:
33        self.abort = False
34
35        self.last_timestamp: Optional[int] = None
36        self.lost_decoding = False
37
38        self.context = EtmContext()
39
40        self.instructions = 0
41        self.cycles = 0
42
43        self.lib = lib
44        self.binary_finder = binary_finder
45        self.objdump = objdump
46
47        self.disassembly: Dict[str, Dict[int, str]] = {}
48
49    def __call__(self, trace_id: int, elem: etm.GenericTraceElement) -> None:
50        if self.abort:
51            return
52
53        try:
54            self.process(trace_id, elem)
55        except Exception as e:
56            self.abort = True
57            raise e
58
59    def reset_trace(self) -> None:
60        self.context.clear()
61        self.lost_decoding = False
62        self.last_timestamp = None
63
64    def process(self, trace_id: int, elem: etm.GenericTraceElement) -> None:
65        if elem.elem_type == etm.ElemType.TRACE_ON:
66            self.reset_trace()
67            return
68
69        elif elem.elem_type == etm.ElemType.NO_SYNC:
70            print("NO_SYNC: trace is lost, possibly due to overflow.")
71            self.reset_trace()
72            return
73
74        elif elem.elem_type == etm.ElemType.PE_CONTEXT:
75            if self.context.update(elem.context):
76                print("New Context: ", end='')
77                self.context.print()
78                if self.context.tid:
79                    process = self.lib.GetThread(self.context.tid)
80                    if process:
81                        print(f"PID: {process[0]}, TID: {process[1]}, comm: {process[2]}")
82            return
83
84        elif elem.elem_type == etm.ElemType.ADDR_NACC:
85            if not self.lost_decoding:
86                self.lost_decoding = True
87                mapped = self.lib.ConvertETMAddressToVaddrInFile(trace_id, elem.st_addr)
88                if mapped:
89                    print(f'ADDR_NACC: path {mapped[0]} cannot be decoded!')
90                else:
91                    print(f'ADDR_NACC: trace address {hex(elem.st_addr)} is not mapped!')
92            return
93
94        elif elem.elem_type == etm.ElemType.EXCEPTION:
95            print(f'Exception: "{elem.exception_type()}" ({elem.exception_number})!' +
96                  (f" (Excepted return: {hex(elem.en_addr)})" if elem.excep_ret_addr else ""))
97            if elem.exception_number == 3 and elem.excep_ret_addr:
98                # For traps, output the instruction that it trapped on; it is usual to return to a
99                # different address, to skip the trapping instruction.
100                mapped = self.lib.ConvertETMAddressToVaddrInFile(trace_id, elem.en_addr)
101                if mapped:
102                    print("Trapped on:")
103                    start_path, start_offset = mapped
104                    b = str(self.find_binary(start_path))
105                    self.print_disassembly(b, start_offset, start_offset)
106                else:
107                    print(f"Trapped on unmapped address {hex(elem.en_addr)}!")
108            return
109
110        elif elem.elem_type == etm.ElemType.TIMESTAMP:
111            if self.last_timestamp != elem.timestamp:
112                self.last_timestamp = elem.timestamp
113                print(f'Current timestamp: {elem.timestamp}')
114            return
115
116        elif elem.elem_type == etm.ElemType.CYCLE_COUNT and elem.has_cc:
117            print("Cycles: ", elem.cycle_count)
118            self.cycles += elem.cycle_count
119            return
120
121        elif elem.elem_type != etm.ElemType.INSTR_RANGE:
122            return
123
124        self.lost_decoding = False
125        self.instructions += elem.num_instr_range
126        start_path, start_offset = self.lib.ConvertETMAddressToVaddrInFile(
127            trace_id, elem.st_addr) or ("", 0)
128        end_path, end_offset = self.lib.ConvertETMAddressToVaddrInFile(
129            trace_id, elem.en_addr - elem.last_instr_sz) or ("", 0)
130
131        error_messages = []
132        if not start_path:
133            error_messages.append(f"Couldn't determine start path for address {elem.st_addr}!")
134        if not end_path:
135            error_messages.append(
136                f"Couldn't determine start path for address {elem.en_addr - elem.last_instr_sz}!")
137        if error_messages:
138            raise RuntimeError(' '.join(error_messages))
139
140        if start_path == '[kernel.kallsyms]':
141            start_path = 'vmlinux'
142
143        cpu = (trace_id - 0x10) // 2
144        print(f'CPU{cpu} {start_path}: {hex(start_offset)} -> {hex(end_offset)}')
145        b = str(self.find_binary(start_path))
146        self.print_disassembly(b, start_offset, end_offset)
147        if not elem.last_instr_cond and not elem.last_instr_exec:
148            raise RuntimeError(f'Wrong binary! Unconditional branch at {hex(end_offset)}'
149                               f' in {start_path} was not taken!')
150
151    @functools.lru_cache
152    def find_binary(self, path: str) -> Optional[Path]:
153        # binary_finder.find_binary opens the binary to check if it is an ELF, and runs readelf on
154        # it to ensure that the build ids match. This is too much to do in our hot loop, therefore
155        # its result should be cached.
156        buildid = self.lib.GetBuildIdForPath(path)
157        return self.binary_finder.find_binary(path, buildid)
158
159    def print_disassembly(self, path: str, start: int, end: int) -> None:
160        disassembly = self.disassemble(path)
161        if not disassembly:
162            log_exit(f"Failed to disassemble '{path}'!")
163
164        for i in range(start, end + 4, 4):
165            print(disassembly[i])
166
167    def disassemble(self, path: str) -> Dict[int, str]:
168        if path in self.disassembly:
169            return self.disassembly[path]
170
171        dso_info = self.objdump.get_dso_info(path, None)
172        self.disassembly[path] = self.objdump.disassemble_whole(dso_info)
173        return self.disassembly[path]
174
175
176def get_args() -> argparse.Namespace:
177    parser = argparse.ArgumentParser(description='Generate instruction trace from ETM data.')
178    parser.add_argument('-i', '--record_file', nargs=1, default=['perf.data'], help="""
179                        Set profiling data file to process.""")
180    parser.add_argument('--binary_cache', nargs=1, default=["binary_cache"], help="""
181                        Set path to the binary cache.""")
182    parser.add_argument('--ndk_path', nargs=1, help='Find tools in the ndk path.')
183    return parser.parse_args()
184
185
186def main() -> None:
187    args = get_args()
188
189    binary_cache_path = args.binary_cache[0]
190    if not pathlib.Path(binary_cache_path).is_dir():
191        log_exit(f"Binary cache '{binary_cache_path}' is not a directory!")
192        return
193
194    ndk_path = args.ndk_path[0] if args.ndk_path else None
195
196    lib = ReportLib()
197    try:
198        lib.SetRecordFile(args.record_file[0])
199        lib.SetSymfs(binary_cache_path)
200        lib.SetLogSeverity('error')
201
202        binary_finder = BinaryFinder(binary_cache_path, ReadElf(ndk_path))
203        objdump = Objdump(ndk_path, binary_finder)
204
205        callback = Tracer(lib, binary_finder, objdump)
206
207        lib.SetETMCallback(callback)
208        while not callback.abort and lib.GetNextSample():
209            pass
210
211        if callback.cycles:
212            print("Total cycles:", callback.cycles)
213        print("Total decoded instructions:", callback.instructions)
214
215    finally:
216        lib.Close()
217
218
219if __name__ == '__main__':
220    main()
221