blob: e2e9bfa2ddfcfee04d2d6015efaf8e48973e2cc5 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2025 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Symbolizes any addresses found in an EC log."""
import argparse
import os
import re
import subprocess
import sys
# Regex to find registers and their hex values
# Looks for e.g., PC: 0x1234, lr = 0xabcd, (PC 0x1234), [lr=0xabcd]
# Captures PC, LR, RA, SP, xPSR, or R<n>, A<n>, T<n> (case-insensitive) and the address.
# Example Input:
# 25-08-28 18:14:34.454 shell_uart [SP=0x801080d0, PC=0x800131f8, RA=0x800131f6]
#
# 25-09-03 17:18:24.136 Saved panic data: 0x0C (NEW)
# 25-09-03 17:18:24.136 ra = 0x8001312A
# 25-09-03 17:18:24.139 a0 = 0x00000001
# 25-09-03 17:18:24.141 a1 = 0x800369A7
# 25-09-03 17:18:24.146 a2 = 0x00000001
#
# Example Output:
# [Log Line]: 25-08-28 18:14:34.454 shell_uart [SP=0x801080d0, PC=0x800131f8, RA=0x800131f6]
# SP (0x801080d0): ['0x801080d0: ?? ??:0']
# PC (0x800131f8): ['0x800131f8: command_crash.lto_priv.0 at ??:?']
# RA (0x800131f6): ['0x800131f6: command_crash.lto_priv.0 at ??:?']
# --------------------
# [Log Line]: 25-09-03 17:18:24.136 ra = 0x8001312A
# RA (0x8001312A): ['0x8001312a: command_crash.lto_priv.0 at ??:?']
# --------------------
# [Log Line]: 25-09-03 17:18:24.139 a0 = 0x00000001
# A0 (0x00000001): ['0x00000001: ?? ??:0']
# --------------------
# [Log Line]: 25-09-03 17:18:24.141 a1 = 0x800369A7
# A1 (0x800369A7): ['0x800369a7: _cfb_font_list_start at ??:?']
# --------------------
# [Log Line]: 25-09-03 17:18:24.146 a2 = 0x00000001
# A2 (0x00000001): ['0x00000001: ?? ??:0']
#
LOG_ADDR_REGEX = re.compile(
r"""
(?P<reg>PC|LR|RA|SP|xPSR|R\d+|A\d+|T\d+|\#\d+) # Register name or frame index
\s*[:=]\s* # Separator : or =
(?P<addr>(?:0x)?[0-9a-fA-F]+) # The hexadecimal address (0x prefix is optional)
""",
re.IGNORECASE | re.VERBOSE,
)
LOG_THREAD_REGEX = re.compile(r"Thread: (.*?)\n")
ADDR2LINE_CACHE = {}
def symbolize_address(addr, elf_file, addr2line_tool):
"""
Runs addr2line to get symbol information for a given address.
Caches results.
"""
if addr in ADDR2LINE_CACHE:
return ADDR2LINE_CACHE[addr]
if not os.path.exists(elf_file):
return f"ELF file not found: {elf_file}"
try:
cmd = [
addr2line_tool,
"-e",
elf_file,
"-a",
addr,
"-f", # Show function names
"-C", # Demangle function names
"-i", # Show inlines
"-p", # Pretty print
]
result = subprocess.run(
cmd, capture_output=True, text=True, check=True, encoding="utf-8"
)
# Output is typically address followed by function/file:line on next line
output_lines = result.stdout.strip().split("\n")
if len(output_lines) >= 2:
# Combine the line after the address
symbolized = output_lines[1].strip()
ADDR2LINE_CACHE[addr] = symbolized
return symbolized
ADDR2LINE_CACHE[addr] = output_lines
return output_lines
except subprocess.CalledProcessError as e:
error_msg = f"addr2line failed for {addr}: {e.stderr.strip()}"
ADDR2LINE_CACHE[addr] = error_msg
return error_msg
except FileNotFoundError:
error_msg = f"addr2line tool not found: {addr2line_tool}"
# No point caching this per address
raise
def parse_log_line(line, elf_file, addr2line_tool):
"""
Finds all PC/LR addresses in a line and symbolizes them.
"""
thread_match = LOG_THREAD_REGEX.search(line)
if thread_match:
name = thread_match.group(1).strip()
print(f"Thread: {name}")
matches = LOG_ADDR_REGEX.finditer(line)
symbolized_info = []
found_addresses = False
for match in matches:
found_addresses = True
reg = match.group("reg").upper()
addr = match.group("addr")
try:
symbol = symbolize_address(addr, elf_file, addr2line_tool)
symbolized_info.append(f" {reg} ({addr}): {symbol}")
except FileNotFoundError as e:
# Tool not found, print error and exit early.
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if found_addresses:
if len(symbolized_info) == 1:
print(
f"[Log Line]: {line.strip()} --> {symbolized_info[0].strip()}"
)
else:
print(f"[Log Line]: {line.strip()}")
for info in symbolized_info:
print(info)
print("-" * 20)
def main():
"""
Main entry point
"""
parser = argparse.ArgumentParser(
description="Parse log files and run addr2line on PC and LR values."
)
parser.add_argument(
"--elf", required=True, help="Path to the ELF file with debug symbols."
)
parser.add_argument(
"--addr2line",
default="addr2line",
help="Path to the addr2line executable.",
)
parser.add_argument(
"log_file",
nargs="?",
type=argparse.FileType("r"),
default=sys.stdin,
help="Log file to parse (reads from stdin if not specified).",
)
args = parser.parse_args()
if not os.path.exists(args.elf):
print(f"Error: ELF file not found: {args.elf}", file=sys.stderr)
sys.exit(1)
try:
for line in args.log_file:
parse_log_line(line, args.elf, args.addr2line)
except KeyboardInterrupt:
print("\nExiting...", file=sys.stderr)
finally:
if args.log_file != sys.stdin:
args.log_file.close()
if __name__ == "__main__":
main()