pw_cpu_exception_cortex_m: Symbolize PC and LR

Updates the exception analyzer to support symbolization of the PC and LR
registers to improve exception debugability.

Change-Id: I7baa9b1029801082d0f019234186740326488dfb
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/71741
Pigweed-Auto-Submit: Armando Montanez <amontanez@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_cpu_exception_cortex_m/docs.rst b/pw_cpu_exception_cortex_m/docs.rst
index b1972a6..a744a77 100644
--- a/pw_cpu_exception_cortex_m/docs.rst
+++ b/pw_cpu_exception_cortex_m/docs.rst
@@ -192,4 +192,44 @@
 ================
 This module's included Python exception analyzer tooling provides snapshot
 integration via a ``process_snapshot()`` function that produces a multi-line
-dump from a serialized snapshot proto.
+dump from a serialized snapshot proto, for example:
+
+.. code-block::
+
+  Exception caused by a usage fault.
+
+  Active Crash Fault Status Register (CFSR) fields:
+  UNDEFINSTR  Undefined Instruction UsageFault.
+      The processor has attempted to execute an undefined
+      instruction. When this bit is set to 1, the PC value stacked
+      for the exception return points to the undefined instruction.
+      An undefined instruction is an instruction that the processor
+      cannot decode.
+
+  All registers:
+  pc         0x0800e1c4 example::Service::Crash(_example_service_CrashRequest const&, _pw_protobuf_Empty&) (src/example_service/service.cc:131)
+  lr         0x0800e141 example::Service::Crash(_example_service_CrashRequest const&, _pw_protobuf_Empty&) (src/example_service/service.cc:128)
+  psr        0x81000000
+  msp        0x20040fd8
+  psp        0x20001488
+  exc_return 0xffffffed
+  cfsr       0x00010000
+  mmfar      0xe000ed34
+  bfar       0xe000ed38
+  icsr       0x00000803
+  hfsr       0x40000000
+  shcsr      0x00000000
+  control    0x00000000
+  r0         0xe03f7847
+  r1         0x714083dc
+  r2         0x0b36dc49
+  r3         0x7fbfbe1a
+  r4         0xc36e8efb
+  r5         0x69a14b13
+  r6         0x0ec35eaa
+  r7         0xa5df5543
+  r8         0xc892b931
+  r9         0xa2372c94
+  r10        0xbd15c968
+  r11        0x759b95ab
+  r12        0x00000000
diff --git a/pw_cpu_exception_cortex_m/py/BUILD.gn b/pw_cpu_exception_cortex_m/py/BUILD.gn
index f840c7b..9de835e 100644
--- a/pw_cpu_exception_cortex_m/py/BUILD.gn
+++ b/pw_cpu_exception_cortex_m/py/BUILD.gn
@@ -32,6 +32,7 @@
   python_deps = [
     "$dir_pw_cli/py",
     "$dir_pw_protobuf_compiler/py",
+    "$dir_pw_symbolizer/py",
     "..:cpu_state_protos.python",
   ]
   pylintrc = "$dir_pigweed/.pylintrc"
diff --git a/pw_cpu_exception_cortex_m/py/exception_analyzer_test.py b/pw_cpu_exception_cortex_m/py/exception_analyzer_test.py
index cae1f26..e12ee6b 100644
--- a/pw_cpu_exception_cortex_m/py/exception_analyzer_test.py
+++ b/pw_cpu_exception_cortex_m/py/exception_analyzer_test.py
@@ -17,6 +17,7 @@
 import unittest
 from pw_cpu_exception_cortex_m import exception_analyzer, cortex_m_constants
 from pw_cpu_exception_cortex_m_protos import cpu_state_pb2
+import pw_symbolizer
 
 # pylint: disable=protected-access
 
@@ -163,6 +164,25 @@
         ))
         self.assertEqual(cpu_state_info.dump_registers(), expected_dump)
 
+    def test_symbolization(self):
+        """Ensure certain registers are symbolized."""
+        cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
+        known_symbols = (
+            pw_symbolizer.Symbol(0x0800A200, 'foo()', 'src/foo.c', 41),
+            pw_symbolizer.Symbol(0x08000004, 'boot_entry()',
+                                 'src/vector_table.c', 5),
+        )
+        symbolizer = pw_symbolizer.FakeSymbolizer(known_symbols)
+        cpu_state_proto.pc = 0x0800A200
+        cpu_state_proto.lr = 0x08000004
+        cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
+            cpu_state_proto, symbolizer)
+        expected_dump = '\n'.join((
+            'pc         0x0800a200 foo() (src/foo.c:41)',
+            'lr         0x08000004 boot_entry() (src/vector_table.c:5)',
+        ))
+        self.assertEqual(cpu_state_info.dump_registers(), expected_dump)
+
     def test_dump_no_cfsr(self):
         """Validate basic CPU state dump."""
         cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
diff --git a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/exception_analyzer.py b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/exception_analyzer.py
index 1fe3f9e..a6cb5cb 100644
--- a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/exception_analyzer.py
+++ b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/exception_analyzer.py
@@ -13,17 +13,27 @@
 # the License.
 """Tools to analyze Cortex-M CPU state context captured during an exception."""
 
-from typing import Tuple
+from typing import Optional, Tuple
 
 from pw_cpu_exception_cortex_m import cortex_m_constants
 from pw_cpu_exception_cortex_m_protos import cpu_state_pb2
+import pw_symbolizer
+
+# These registers are symbolized when dumped.
+_SYMBOLIZED_REGISTERS = ('pc', 'lr', 'bfar', 'mmfar', 'msp', 'psp', 'r0', 'r1',
+                         'r2', 'r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10',
+                         'r11', 'r12')
 
 
 class CortexMExceptionAnalyzer:
     """This class provides helper functions to dump a ArmV7mCpuState proto."""
-    def __init__(self, cpu_state):
+    def __init__(self,
+                 cpu_state,
+                 symbolizer: Optional[pw_symbolizer.Symbolizer] = None):
         self._cpu_state = cpu_state
-        self._active_cfsr_fields = None
+        self._symbolizer = symbolizer
+        self._active_cfsr_fields: Optional[Tuple[cortex_m_constants.BitField,
+                                                 ...]] = None
 
     def active_cfsr_fields(self) -> Tuple[cortex_m_constants.BitField, ...]:
         """Returns a list of BitFields for each active CFSR flag."""
@@ -115,11 +125,16 @@
     def dump_registers(self) -> str:
         """Dumps all captured CPU registers as a multi-line string."""
         registers = []
-        # TODO(amontanez): Do fancier decode of some registers like PC and LR.
         for field in self._cpu_state.DESCRIPTOR.fields:
             if self._cpu_state.HasField(field.name):
                 register_value = getattr(self._cpu_state, field.name)
-                registers.append(f'{field.name:<10} 0x{register_value:08x}')
+                register_str = f'{field.name:<10} 0x{register_value:08x}'
+                if (self._symbolizer is not None
+                        and field.name in _SYMBOLIZED_REGISTERS):
+                    symbol = self._symbolizer.symbolize(register_value)
+                    if symbol.name:
+                        register_str += f' {symbol}'
+                registers.append(register_str)
         return '\n'.join(registers)
 
     def dump_active_active_cfsr_fields(self) -> str:
@@ -153,7 +168,9 @@
         return '\n'.join(dump)
 
 
-def process_snapshot(serialized_snapshot: bytes) -> str:
+def process_snapshot(
+        serialized_snapshot: bytes,
+        symbolizer: Optional[pw_symbolizer.Symbolizer] = None) -> str:
     """Returns the stringified result of a SnapshotCpuState message run though
     a CortexMExceptionAnalyzer.
     """
@@ -161,6 +178,8 @@
     snapshot.ParseFromString(serialized_snapshot)
 
     if snapshot.HasField('armv7m_cpu_state'):
-        return f'{CortexMExceptionAnalyzer(snapshot.armv7m_cpu_state)}\n'
+        state_analyzer = CortexMExceptionAnalyzer(snapshot.armv7m_cpu_state,
+                                                  symbolizer)
+        return f'{state_analyzer}\n'
 
     return ''
diff --git a/pw_snapshot/py/pw_snapshot/processor.py b/pw_snapshot/py/pw_snapshot/processor.py
index 3375bed..dbcaf4a 100644
--- a/pw_snapshot/py/pw_snapshot/processor.py
+++ b/pw_snapshot/py/pw_snapshot/processor.py
@@ -74,7 +74,7 @@
         symbolizer = LlvmSymbolizer()
 
     cortex_m_cpu_state = pw_cpu_exception_cortex_m.process_snapshot(
-        serialized_snapshot)
+        serialized_snapshot, symbolizer)
     if cortex_m_cpu_state:
         output.append(cortex_m_cpu_state)