| # Copyright 2021 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """A symbolizer based on llvm-symbolizer.""" |
| |
| import shutil |
| import subprocess |
| import threading |
| import json |
| from typing import Optional, Tuple |
| from pathlib import Path |
| from pw_symbolizer import symbolizer |
| |
| |
| class LlvmSymbolizer(symbolizer.Symbolizer): |
| """A symbolizer that wraps llvm-symbolizer.""" |
| def __init__(self, binary: Optional[Path] = None, force_legacy=False): |
| # Lets destructor return cleanly if the binary is not found. |
| self._symbolizer = None |
| if shutil.which('llvm-symbolizer') is None: |
| raise FileNotFoundError( |
| 'llvm-symbolizer not installed. Run bootstrap, or download ' |
| 'LLVM (https://github.com/llvm/llvm-project/releases/) and add ' |
| 'the tools to your system PATH') |
| |
| # Prefer JSON output as it's easier to decode. |
| if force_legacy: |
| self._json_mode = False |
| else: |
| self._json_mode = LlvmSymbolizer._is_json_compatibile() |
| |
| if binary is not None: |
| if not binary.exists(): |
| raise FileNotFoundError(binary) |
| |
| output_style = 'JSON' if self._json_mode else 'LLVM' |
| cmd = [ |
| 'llvm-symbolizer', |
| '--no-inlines', |
| '--demangle', |
| '--functions', |
| f'--output-style={output_style}', |
| '--exe', |
| str(binary), |
| ] |
| self._symbolizer = subprocess.Popen(cmd, |
| stdout=subprocess.PIPE, |
| stdin=subprocess.PIPE) |
| |
| self._lock: threading.Lock = threading.Lock() |
| |
| def __del__(self): |
| if self._symbolizer: |
| self._symbolizer.terminate() |
| self._symbolizer.wait() |
| |
| @staticmethod |
| def _is_json_compatibile() -> bool: |
| """Checks llvm-symbolizer to ensure compatibility""" |
| result = subprocess.run(('llvm-symbolizer', '--help'), |
| stdout=subprocess.PIPE, |
| stdin=subprocess.PIPE) |
| for line in result.stdout.decode().splitlines(): |
| if '--output-style' in line and 'JSON' in line: |
| return True |
| |
| return False |
| |
| @staticmethod |
| def _read_json_symbol(address, stdout) -> symbolizer.Symbol: |
| """Reads a single symbol from llvm-symbolizer's JSON output mode.""" |
| results = json.loads(stdout.readline().decode()) |
| # The symbol resolution should give us at least one symbol, even |
| # if it's largely empty. |
| assert len(results["Symbol"]) > 0 |
| |
| # Get the first symbol. |
| symbol = results["Symbol"][0] |
| |
| return symbolizer.Symbol(address=address, |
| name=symbol['FunctionName'], |
| file=symbol['FileName'], |
| line=symbol['Line']) |
| |
| @staticmethod |
| def _llvm_output_line_splitter(file_and_line: str) -> Tuple[str, int]: |
| split = file_and_line.split(':') |
| # LLVM file name output is as follows: |
| # path/to/src.c:123:1 |
| # Where the last number is the discriminator, the second to last the |
| # line number, and all leading characters the file name. For now, |
| # this class ignores discriminators. |
| line_number_str = split[-2] |
| file = ':'.join(split[:-2]) |
| |
| if not line_number_str: |
| raise ValueError(f'Bad symbol format: {file_and_line}') |
| |
| # For unknown file names, mark as blank. |
| if file.startswith('?'): |
| return ('', 0) |
| |
| return (file, int(line_number_str)) |
| |
| @staticmethod |
| def _read_llvm_symbol(address, stdout) -> symbolizer.Symbol: |
| """Reads a single symbol from llvm-symbolizer's LLVM output mode.""" |
| symbol = stdout.readline().decode().strip() |
| file_and_line = stdout.readline().decode().strip() |
| |
| # Might have gotten multiple symbol matches, drop all of the other ones. |
| # The results of a symbol are denoted by an empty newline. |
| while stdout.readline().decode() != '\n': |
| pass |
| |
| if symbol.startswith('?'): |
| return symbolizer.Symbol(address) |
| |
| file, line_number = LlvmSymbolizer._llvm_output_line_splitter( |
| file_and_line) |
| |
| return symbolizer.Symbol(address, symbol, file, line_number) |
| |
| def symbolize(self, address: int) -> symbolizer.Symbol: |
| """Symbolizes an address using the loaded ELF file.""" |
| if not self._symbolizer: |
| return symbolizer.Symbol(address=address, name='', file='', line=0) |
| |
| with self._lock: |
| if self._symbolizer.returncode is not None: |
| raise ValueError('llvm-symbolizer closed unexpectedly') |
| |
| stdin = self._symbolizer.stdin |
| stdout = self._symbolizer.stdout |
| |
| assert stdin is not None |
| assert stdout is not None |
| |
| stdin.write(f'0x{address:08X}\n'.encode()) |
| stdin.flush() |
| |
| if self._json_mode: |
| return LlvmSymbolizer._read_json_symbol(address, stdout) |
| |
| return LlvmSymbolizer._read_llvm_symbol(address, stdout) |