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