Skip to content

Commit 830377b

Browse files
committedApr 21, 2026
Add symbolication tools (thanks Claude)
1 parent 2107d32 commit 830377b

3 files changed

Lines changed: 1439 additions & 0 deletions

File tree

 

‎tools/SYMBOLICATION.md‎

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
# Symbolication and Crash Analysis Tools
2+
3+
A comprehensive toolkit for analyzing and symbolicting crashes in ravynOS binaries.
4+
5+
## Tools Overview
6+
7+
### 1. `symbolicate.py` - Address to Symbol Mapping
8+
9+
Maps crash addresses to symbols in compiled binaries.
10+
11+
**Usage:**
12+
```bash
13+
# Single address
14+
./symbolicate.py /path/to/binary 0x1d9660
15+
16+
# Multiple addresses
17+
./symbolicate.py /path/to/binary 0x1d9660 0x1d9700 0x1d9750
18+
19+
# With register context
20+
./symbolicate.py /path/to/binary --rip 0x1d9660 --rax 0x8010000000000007
21+
22+
# Stack trace
23+
./symbolicate.py /path/to/binary --stack-trace 0x1d9660,0x1d9700,0x1d9750
24+
25+
# Search for symbols
26+
./symbolicate.py /path/to/binary --search syscall
27+
28+
# JSON output
29+
./symbolicate.py /path/to/binary 0x1d9660 --json
30+
```
31+
32+
**Features:**
33+
- Binary search through symbol table for fast lookup
34+
- Shows symbol name, section, and offset
35+
- Displays nearby symbols for context
36+
- Supports both exact and substring symbol search
37+
- JSON output for automation
38+
- Handles both global and local symbols
39+
40+
**Example Output:**
41+
```
42+
=== Symbolication Results ===
43+
44+
1. 0x1d9660 -> _exit (TEXT) +0x0 (entry point)
45+
Size: 0x20 bytes
46+
Offset: 0x0
47+
Context: entry point
48+
Nearby symbols:
49+
0x1d9640 syscall_wrapper
50+
0x1d9660 _exit <--
51+
0x1d9680 exit_trampoline
52+
```
53+
54+
### 2. `crash-analyzer.py` - Comprehensive Crash Analysis
55+
56+
Analyzes crash dumps and classifies crash types with recommendations.
57+
58+
**Usage:**
59+
```bash
60+
# Basic crash analysis with register state
61+
./crash-analyzer.py \
62+
--binary /path/to/binary \
63+
--rip 0x1d9660 \
64+
--rax 0x8010000000000007
65+
66+
# With full register context
67+
./crash-analyzer.py \
68+
--binary /path/to/binary \
69+
--rip 0x1d9660 \
70+
--rax 0x8010000000000007 \
71+
--rdi 0x000000010ffd0000 \
72+
--rsi 0x0000000115337c7e \
73+
--rdx 0x000000011534d833
74+
75+
# With stack trace
76+
./crash-analyzer.py \
77+
--binary /path/to/binary \
78+
--rip 0x1d9660 \
79+
--rax 0x8010000000000007 \
80+
--stack-trace 0x1d9660,0x1d9700,0x1d9750
81+
82+
# JSON output
83+
./crash-analyzer.py \
84+
--binary /path/to/binary \
85+
--rip 0x1d9660 \
86+
--rax 0x8010000000000007 \
87+
--json
88+
```
89+
90+
**Crash Types Detected:**
91+
- **Invalid Pointer Dereference** - Bit 63 set or known error patterns
92+
- **Null Pointer Dereference** - RAX = 0x0
93+
- **Syscall Failure** - At syscall instruction
94+
- **Segmentation Fault** - Unaligned or invalid memory access
95+
- **Buffer Overflow** - Stack corruption patterns
96+
- **Use After Free** - Freed memory markers
97+
- **Unknown** - Insufficient information
98+
99+
**Example Output:**
100+
```
101+
======================================================================
102+
CRASH ANALYSIS REPORT
103+
======================================================================
104+
105+
Crash Type: invalid_pointer_dereference
106+
Confidence: 95%
107+
108+
Description:
109+
RAX contains invalid pointer: 0x8010000000000007. Bit 63 set indicates
110+
kernel space or error marker.
111+
112+
Affected Symbol: dyld::recursiveBind+0x45
113+
Crash Address (RIP): 0x1d9660
114+
115+
RAX: 0x8010000000000007
116+
RDI: 0x000000010ffd0000
117+
118+
Stack Trace (3 frames):
119+
[0] 0x1d9660
120+
[1] 0x1d9700
121+
[2] 0x1d9750
122+
123+
Recommendations:
124+
1. Check dyld's binding/interposing state at this instruction
125+
2. Verify no corrupted mach-o load commands
126+
3. Review recursive dependency loading logic
127+
4. Check for malformed or misinterpreted bind/lazy-bind records
128+
129+
======================================================================
130+
```
131+
132+
### 3. `analyze-launchd-crash.py` - launchd-Specific Analysis
133+
134+
Specialized analyzer for launchd stub crashes, focusing on syscall entry points and exit paths.
135+
136+
**Usage:**
137+
```bash
138+
# Analyze launchd crash
139+
./analyze-launchd-crash.py /path/to/launchd 0x1d9660
140+
141+
# Analyze exit-related code paths
142+
./analyze-launchd-crash.py /path/to/launchd 0x1d9660 --analyze-exit
143+
144+
# Verbose output
145+
./analyze-launchd-crash.py /path/to/launchd 0x1d9660 -v
146+
```
147+
148+
**Features:**
149+
- Detects syscall instructions at crash location
150+
- Analyzes exit-related code paths
151+
- Shows disassembly context around crash
152+
- Identifies if crash is at binary entry point
153+
- Lists known syscall constants
154+
155+
**Example Output:**
156+
```
157+
=== Launchd Crash Analysis ===
158+
159+
RIP: 0x1d9660
160+
Entry Point: 0x1d9660
161+
⚠️ Crash at entry point (very early in execution)
162+
163+
Syscall Entry: YES - syscall
164+
This suggests a syscall instruction is at the crash location
165+
166+
Disassembly around crash:
167+
168+
0000000000001d50 <_start>:
169+
1d50: 55 push %rbp
170+
1d51: 48 89 e5 mov %rsp,%rbp
171+
...
172+
1d60: 0f 05 syscall
173+
1d62: ...
174+
```
175+
176+
## Practical Examples
177+
178+
### Example 1: Analyzing the launchd stub issue
179+
180+
Given the crash at `0x1d9660` in the launchd stub which is the `syscall` instruction in `_exit`:
181+
182+
```bash
183+
# First, symbolicate the address
184+
./symbolicate.py /nest/ravynos/BSD/sbin/launchd 0x1d9660
185+
186+
# Then analyze the crash with register state
187+
./crash-analyzer.py \
188+
--binary /nest/ravynos/BSD/sbin/launchd \
189+
--rip 0x1d9660 \
190+
--rax 0x2000001 # SYS_exit
191+
192+
# Get detailed launchd-specific analysis
193+
./analyze-launchd-crash.py /nest/ravynos/BSD/sbin/launchd 0x1d9660 --analyze-exit
194+
```
195+
196+
### Example 2: Analyzing dyld crash with full register state
197+
198+
From CRASH_FIX_ANALYSIS.md:
199+
200+
```bash
201+
./crash-analyzer.py \
202+
--binary /nest/ravynos/Libraries/dyld/src/dyld \
203+
--rip 0x00000001154ad2eb \
204+
--rax 0x8010000000000007 \
205+
--rdi 0x000000010ffd0000 \
206+
--stack-trace 0x00000001154ad2eb,0x0000000115337c7e,0x000000011534d833,0x000000011534ac51
207+
```
208+
209+
### Example 3: Searching for symbols
210+
211+
Find all syscall-related symbols:
212+
213+
```bash
214+
./symbolicate.py /path/to/binary --search syscall
215+
```
216+
217+
Find all exit-related symbols:
218+
219+
```bash
220+
./symbolicate.py /path/to/binary --search exit
221+
```
222+
223+
## Building and Testing
224+
225+
### Prerequisites
226+
227+
The tools require standard Unix utilities:
228+
- `nm` - For symbol table extraction
229+
- `objdump` - For disassembly
230+
- `readelf` or `otool` - For binary headers
231+
- Python 3.6+
232+
233+
Install on macOS:
234+
```bash
235+
brew install binutils
236+
```
237+
238+
Install on Linux:
239+
```bash
240+
sudo apt-get install binutils
241+
```
242+
243+
### Quick Test
244+
245+
```bash
246+
# Test symbolicate on a system binary
247+
./symbolicate.py /bin/ls 0x100001000
248+
249+
# Test on ravynOS launchd
250+
./symbolicate.py /nest/ravynos/BSD/sbin/launchd 0x1d9660
251+
```
252+
253+
## Integration with Crash Analysis Workflow
254+
255+
1. **Capture crash** - Kernel or crash reporter provides RIP, RAX, and stack trace
256+
2. **Locate binary** - Find the binary that crashed (often in /nest/ravynos/BSD/sbin/launchd or /nest/ravynos/Libraries/dyld/src/dyld)
257+
3. **Symbolicate** - Use `symbolicate.py` to map addresses to function names
258+
4. **Analyze** - Use `crash-analyzer.py` to classify the crash type
259+
5. **Investigate** - Use `analyze-launchd-crash.py` for launchd-specific patterns
260+
261+
Example workflow script:
262+
263+
```bash
264+
#!/bin/bash
265+
266+
BINARY="$1"
267+
RIP="$2"
268+
RAX="$3"
269+
270+
echo "=== Quick Crash Analysis ==="
271+
echo ""
272+
273+
echo "Step 1: Symbolicate addresses"
274+
./symbolicate.py "$BINARY" "$RIP" --context 3
275+
echo ""
276+
277+
echo "Step 2: Analyze crash"
278+
./crash-analyzer.py --binary "$BINARY" --rip "$RIP" --rax "$RAX"
279+
```
280+
281+
## Output Formats
282+
283+
### Human-Readable (Default)
284+
285+
Formatted for terminal viewing with colors and indentation.
286+
287+
### JSON Output
288+
289+
All tools support `--json` for machine-readable output:
290+
291+
```bash
292+
./symbolicate.py /path/to/binary 0x1d9660 --json
293+
./crash-analyzer.py --binary /path/to/binary --rip 0x1d9660 --json
294+
```
295+
296+
JSON structure:
297+
```json
298+
{
299+
"address": "0x1d9660",
300+
"symbol": "_exit",
301+
"section": "TEXT",
302+
"offset": "0x0",
303+
"context": "entry point"
304+
}
305+
```
306+
307+
## Troubleshooting
308+
309+
### "nm command not found"
310+
Install binutils:
311+
- macOS: `brew install binutils`
312+
- Linux: `sudo apt-get install binutils`
313+
314+
### "No symbols found"
315+
Binary may be stripped. Use debug symbols if available:
316+
```bash
317+
./symbolicate.py /path/to/binary.dSYM/Contents/Resources/DWARF/binary 0x1d9660
318+
```
319+
320+
### "Could not get disassembly"
321+
Ensure objdump is installed and binary is accessible.
322+
323+
### Very slow on large binaries
324+
The tools cache symbols after first load. Subsequent queries are fast.
325+
326+
## Advanced Usage
327+
328+
### Analyzing multiple crashes
329+
330+
Create a file `crashes.txt`:
331+
```
332+
0x1d9660 0x8010000000000007
333+
0x1d9700 0xdeadbeef
334+
0x1d9750 0x0000000115337c7e
335+
```
336+
337+
Process all:
338+
```bash
339+
while read rip rax; do
340+
./crash-analyzer.py --binary /path/to/binary --rip "$rip" --rax "$rax"
341+
done < crashes.txt
342+
```
343+
344+
### Integration with CI/CD
345+
346+
Export as JSON for processing:
347+
```bash
348+
./crash-analyzer.py \
349+
--binary /path/to/binary \
350+
--rip "$RIP" \
351+
--rax "$RAX" \
352+
--json > crash_report.json
353+
```
354+
355+
Parse the JSON in your CI pipeline to trigger alerts or create issues.
356+
357+
## Known Limitations
358+
359+
1. **Stripped binaries** - Requires debug symbols
360+
2. **ASLR** - Addresses must be relative to binary's load address
361+
3. **Indirect calls** - Can't determine exact target of indirect calls
362+
4. **Cross-binary analysis** - Each binary requires separate analysis
363+
364+
## Contributing
365+
366+
To add new crash pattern detection:
367+
368+
1. Add pattern to `CrashAnalyzer.INVALID_POINTER_MARKERS` or implement new check
369+
2. Add test case in analysis
370+
3. Update this README with example
371+
372+
## License
373+
374+
Part of ravynOS project. See main LICENSE file.
375+
376+
## See Also
377+
378+
- CRASH_FIX_ANALYSIS.md - Detailed crash analysis from development
379+
- Kernel/xnu/bsd/kern/mach_loader.c - Binary loader implementation
380+
- Libraries/dyld/src/ - Dynamic linker source
381+

‎tools/crash-analyzer.py‎

Lines changed: 481 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,481 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Comprehensive Crash Analysis Toolkit for ravynOS
4+
5+
Provides integrated analysis of crash dumps, including symbolication,
6+
register state analysis, and pattern matching against known issues.
7+
8+
Usage:
9+
./crash-analyzer.py --rip 0x1d9660 --rax 0x8010000000000007 --binary /path/to/binary
10+
./crash-analyzer.py --crash-file crash.txt --binary /path/to/binary
11+
./crash-analyzer.py --dyld-crash --rip 0x1d9660 --stack-trace 0x...,0x...,0x...
12+
"""
13+
14+
import sys
15+
import os
16+
import subprocess
17+
import re
18+
import argparse
19+
import json
20+
from dataclasses import dataclass, asdict
21+
from typing import List, Optional, Dict, Tuple
22+
from enum import Enum
23+
24+
25+
class CrashType(Enum):
26+
"""Classification of crash types"""
27+
INVALID_POINTER = "invalid_pointer_dereference"
28+
SYSCALL_FAILURE = "syscall_failure"
29+
SEGMENT_VIOLATION = "segmentation_fault"
30+
ALIGNMENT_FAULT = "alignment_fault"
31+
NULL_POINTER = "null_pointer_dereference"
32+
BUFFER_OVERFLOW = "buffer_overflow"
33+
USE_AFTER_FREE = "use_after_free"
34+
UNKNOWN = "unknown"
35+
36+
37+
@dataclass
38+
class CrashContext:
39+
"""Context information about a crash"""
40+
rip: int
41+
rax: Optional[int] = None
42+
rdi: Optional[int] = None
43+
rsi: Optional[int] = None
44+
rdx: Optional[int] = None
45+
stack_trace: List[int] = None
46+
47+
def __post_init__(self):
48+
if self.stack_trace is None:
49+
self.stack_trace = []
50+
51+
def to_dict(self) -> dict:
52+
"""Convert to dictionary"""
53+
return {
54+
'rip': hex(self.rip),
55+
'rax': hex(self.rax) if self.rax else None,
56+
'rdi': hex(self.rdi) if self.rdi else None,
57+
'rsi': hex(self.rsi) if self.rsi else None,
58+
'rdx': hex(self.rdx) if self.rdx else None,
59+
'stack_trace': [hex(addr) for addr in self.stack_trace],
60+
}
61+
62+
63+
@dataclass
64+
class CrashAnalysisResult:
65+
"""Result of crash analysis"""
66+
crash_type: CrashType
67+
confidence: float # 0.0 to 1.0
68+
description: str
69+
affected_symbol: Optional[str]
70+
affected_address: int
71+
recommendations: List[str]
72+
73+
def to_dict(self) -> dict:
74+
"""Convert to dictionary"""
75+
return {
76+
'crash_type': self.crash_type.value,
77+
'confidence': self.confidence,
78+
'description': self.description,
79+
'affected_symbol': self.affected_symbol,
80+
'affected_address': hex(self.affected_address),
81+
'recommendations': self.recommendations,
82+
}
83+
84+
85+
class CrashAnalyzer:
86+
"""Main crash analysis engine"""
87+
88+
# Known invalid pointer patterns (bit 63 typically indicates kernel/error space)
89+
INVALID_POINTER_MARKERS = [
90+
0x8010000000000000, # Bit 63 set
91+
0xffff800000000000, # Kernel space marker
92+
0xdeadbeef, # Debug pattern
93+
0xdeaddeaddeaddead, # Debug pattern
94+
0xcccccccccccccccc, # Debug pattern
95+
]
96+
97+
def __init__(self, binary_path: str, verbose: bool = False):
98+
self.binary_path = binary_path
99+
self.verbose = verbose
100+
self.symbols: Dict[int, str] = {}
101+
self.otool_path = self._find_otool()
102+
self._load_symbols()
103+
104+
def _find_otool(self) -> Optional[str]:
105+
"""Find otool in common locations"""
106+
possible_paths = [
107+
'otool',
108+
'/usr/bin/otool',
109+
'/usr/local/bin/otool',
110+
'/nest/build/Developer/Toolchains/Default.xctoolchain/usr/bin/otool',
111+
]
112+
113+
for path in possible_paths:
114+
try:
115+
result = subprocess.run([path, '-h', '--version'],
116+
capture_output=True, timeout=2)
117+
if result.returncode == 0:
118+
return path
119+
except:
120+
pass
121+
122+
return None
123+
124+
def _load_symbols(self):
125+
"""Load symbols from binary"""
126+
# Try Mach-O first
127+
if self._is_macho_binary() and self.otool_path:
128+
self._load_symbols_from_macho()
129+
else:
130+
# Fall back to ELF
131+
self._load_symbols_from_elf()
132+
133+
def _is_macho_binary(self) -> bool:
134+
"""Check if binary is Mach-O format"""
135+
try:
136+
result = subprocess.run(
137+
['file', self.binary_path],
138+
capture_output=True,
139+
text=True,
140+
timeout=10
141+
)
142+
return 'Mach-O' in result.stdout
143+
except:
144+
return False
145+
146+
def _load_symbols_from_elf(self):
147+
"""Load symbols from ELF binary using nm"""
148+
try:
149+
result = subprocess.run(
150+
['nm', '-n', self.binary_path],
151+
capture_output=True,
152+
text=True,
153+
timeout=10
154+
)
155+
156+
for line in result.stdout.strip().split('\n'):
157+
parts = line.split()
158+
if len(parts) >= 3:
159+
try:
160+
addr = int(parts[0], 16)
161+
name = ' '.join(parts[2:])
162+
self.symbols[addr] = name
163+
except (ValueError, IndexError):
164+
pass
165+
except Exception as e:
166+
if self.verbose:
167+
print(f"Warning: Could not load ELF symbols: {e}", file=sys.stderr)
168+
169+
def _load_symbols_from_macho(self):
170+
"""Load symbols from Mach-O binary using otool"""
171+
try:
172+
result = subprocess.run(
173+
[self.otool_path, '-tv', self.binary_path],
174+
capture_output=True,
175+
text=True,
176+
timeout=10
177+
)
178+
179+
current_symbol = None
180+
for line in result.stdout.strip().split('\n'):
181+
line = line.strip()
182+
183+
# Symbol definition (ends with :)
184+
if line.endswith(':') and not line[0].isdigit():
185+
current_symbol = line[:-1]
186+
187+
# Instruction line with address
188+
parts = line.split()
189+
if parts and all(c in '0123456789abcdef' for c in parts[0]):
190+
try:
191+
addr = int(parts[0], 16)
192+
if current_symbol:
193+
self.symbols[addr] = current_symbol
194+
current_symbol = None # Clear after first instruction
195+
except (ValueError, IndexError):
196+
pass
197+
except Exception as e:
198+
if self.verbose:
199+
print(f"Warning: Could not load Mach-O symbols: {e}", file=sys.stderr)
200+
201+
def analyze(self, crash_context: CrashContext) -> CrashAnalysisResult:
202+
"""Analyze crash and determine type"""
203+
204+
# Check for known invalid pointer patterns
205+
if crash_context.rax and self._is_invalid_pointer(crash_context.rax):
206+
return CrashAnalysisResult(
207+
crash_type=CrashType.INVALID_POINTER,
208+
confidence=0.95,
209+
description=f"RAX contains invalid pointer: {hex(crash_context.rax)}. "
210+
f"Bit 63 set indicates kernel space or error marker.",
211+
affected_symbol=self._find_symbol(crash_context.rip),
212+
affected_address=crash_context.rip,
213+
recommendations=[
214+
"Check dyld's binding/interposing state at this instruction",
215+
"Verify no corrupted mach-o load commands",
216+
"Review recursive dependency loading logic",
217+
"Check for malformed or misinterpreted bind/lazy-bind records",
218+
]
219+
)
220+
221+
# Check for null pointer
222+
if crash_context.rax == 0:
223+
return CrashAnalysisResult(
224+
crash_type=CrashType.NULL_POINTER,
225+
confidence=0.90,
226+
description="RAX is NULL, likely dereferencing null pointer",
227+
affected_symbol=self._find_symbol(crash_context.rip),
228+
affected_address=crash_context.rip,
229+
recommendations=[
230+
"Add null pointer checks before memory operations",
231+
"Review control flow that could result in null values",
232+
]
233+
)
234+
235+
# Check for syscall-related patterns
236+
if self._is_syscall_location(crash_context.rip):
237+
return CrashAnalysisResult(
238+
crash_type=CrashType.SYSCALL_FAILURE,
239+
confidence=0.85,
240+
description="Crash at syscall instruction. "
241+
"May indicate invalid syscall number or arguments.",
242+
affected_symbol=self._find_symbol(crash_context.rip),
243+
affected_address=crash_context.rip,
244+
recommendations=[
245+
"Check syscall number in RAX",
246+
"Verify syscall arguments in RDI, RSI, RDX",
247+
"Check if syscall is available in this kernel version",
248+
]
249+
)
250+
251+
# Generic analysis
252+
return CrashAnalysisResult(
253+
crash_type=CrashType.UNKNOWN,
254+
confidence=0.0,
255+
description="Unable to determine crash cause from available information",
256+
affected_symbol=self._find_symbol(crash_context.rip),
257+
affected_address=crash_context.rip,
258+
recommendations=[
259+
"Obtain full debug symbols",
260+
"Run under debugger to capture full register state",
261+
"Enable crash reporter diagnostics",
262+
]
263+
)
264+
265+
def _is_invalid_pointer(self, value: int) -> bool:
266+
"""Check if value looks like an invalid pointer"""
267+
# Check bit 63 (high bit)
268+
if value & (1 << 63):
269+
return True
270+
271+
# Check known patterns
272+
for pattern in self.INVALID_POINTER_MARKERS:
273+
if (value & 0xf0f0f0f0f0f0f0f0) == (pattern & 0xf0f0f0f0f0f0f0f0):
274+
return True
275+
276+
return False
277+
278+
def _find_symbol(self, address: int) -> Optional[str]:
279+
"""Find symbol containing or near address"""
280+
# Look for exact match or closest lower address
281+
closest_addr = None
282+
closest_name = None
283+
284+
for addr in sorted(self.symbols.keys()):
285+
if addr <= address:
286+
closest_addr = addr
287+
closest_name = self.symbols[addr]
288+
else:
289+
break
290+
291+
if closest_name:
292+
offset = address - closest_addr if closest_addr else 0
293+
if offset == 0:
294+
return closest_name
295+
else:
296+
return f"{closest_name}+0x{offset:x}"
297+
298+
return None
299+
300+
def _is_syscall_location(self, address: int) -> bool:
301+
"""Check if address appears to be at a syscall instruction"""
302+
try:
303+
# Try otool first (for Mach-O)
304+
if self.otool_path:
305+
result = subprocess.run(
306+
[self.otool_path, '-tv', self.binary_path],
307+
capture_output=True,
308+
text=True,
309+
timeout=10
310+
)
311+
312+
for line in result.stdout.split('\n'):
313+
if f'{address:x}' in line and 'syscall' in line.lower():
314+
return True
315+
316+
# Fall back to objdump (for ELF)
317+
result = subprocess.run(
318+
['objdump', '-d', self.binary_path],
319+
capture_output=True,
320+
text=True,
321+
timeout=10
322+
)
323+
324+
for line in result.stdout.split('\n'):
325+
if f'{address:x}' in line and 'syscall' in line.lower():
326+
return True
327+
except Exception:
328+
pass
329+
330+
return False
331+
332+
333+
def parse_register_value(value_str: str) -> int:
334+
"""Parse register value (hex or decimal)"""
335+
value_str = value_str.strip()
336+
if value_str.startswith(('0x', '0X')):
337+
return int(value_str, 16)
338+
return int(value_str, 16) # Default to hex for crash analysis
339+
340+
341+
def normalize_code_address(raw_addr: int, slide: int) -> int:
342+
"""Normalize a runtime code address to a file-relative address using slide."""
343+
if slide < 0:
344+
raise ValueError("slide must be non-negative")
345+
if raw_addr < slide:
346+
raise ValueError(f"address {hex(raw_addr)} is smaller than slide {hex(slide)}")
347+
return raw_addr - slide
348+
349+
350+
def print_analysis_report(analysis: CrashAnalysisResult, crash_context: CrashContext):
351+
"""Pretty-print crash analysis report"""
352+
print(f"\n{'='*70}")
353+
print(f" CRASH ANALYSIS REPORT")
354+
print(f"{'='*70}\n")
355+
356+
print(f"Crash Type: {analysis.crash_type.value}")
357+
print(f"Confidence: {analysis.confidence*100:.0f}%\n")
358+
359+
print(f"Description:")
360+
print(f" {analysis.description}\n")
361+
362+
print(f"Affected Symbol: {analysis.affected_symbol or 'Unknown'}")
363+
print(f"Crash Address (RIP): {hex(crash_context.rip)}\n")
364+
365+
if crash_context.rax is not None:
366+
print(f"RAX: {hex(crash_context.rax)}")
367+
if crash_context.rdi is not None:
368+
print(f"RDI: {hex(crash_context.rdi)}")
369+
if crash_context.rsi is not None:
370+
print(f"RSI: {hex(crash_context.rsi)}")
371+
if crash_context.rdx is not None:
372+
print(f"RDX: {hex(crash_context.rdx)}")
373+
374+
if crash_context.stack_trace:
375+
print(f"\nStack Trace ({len(crash_context.stack_trace)} frames):")
376+
for i, addr in enumerate(crash_context.stack_trace):
377+
print(f" [{i}] {hex(addr)}")
378+
379+
print(f"\nRecommendations:")
380+
for i, rec in enumerate(analysis.recommendations, 1):
381+
print(f" {i}. {rec}")
382+
383+
print(f"\n{'='*70}\n")
384+
385+
386+
def main():
387+
parser = argparse.ArgumentParser(
388+
description='Comprehensive crash analysis for ravynOS',
389+
formatter_class=argparse.RawDescriptionHelpFormatter,
390+
epilog='''
391+
Examples:
392+
%(prog)s --rip 0x1d9660 --rax 0x8010000000000007 --binary /path/to/binary
393+
%(prog)s --rip 0x1d9660 --stack-trace 0x...,0x...,0x... --binary /path/to/binary
394+
%(prog)s --crash-file crash.txt --binary /path/to/binary
395+
'''
396+
)
397+
398+
parser.add_argument('--binary', required=True, help='Path to the crashed binary')
399+
parser.add_argument('--rip', type=str, help='RIP register value (crash address)')
400+
parser.add_argument('--rax', type=str, help='RAX register value')
401+
parser.add_argument('--rdi', type=str, help='RDI register value')
402+
parser.add_argument('--rsi', type=str, help='RSI register value')
403+
parser.add_argument('--rdx', type=str, help='RDX register value')
404+
parser.add_argument('--stack-trace', type=str, help='Stack trace (comma-separated addresses)')
405+
parser.add_argument('--slide', type=str, help='ASLR slide to subtract from RIP/stack addresses (e.g. 0x11526d000)')
406+
parser.add_argument('--crash-file', type=str, help='Read crash context from file')
407+
parser.add_argument('--json', action='store_true', help='Output as JSON')
408+
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
409+
410+
args = parser.parse_args()
411+
412+
try:
413+
slide = parse_register_value(args.slide) if args.slide else 0
414+
415+
# Parse crash context
416+
if args.crash_file:
417+
# TODO: Parse crash file format
418+
print("Error: --crash-file not yet implemented", file=sys.stderr)
419+
return 1
420+
421+
if not args.rip:
422+
parser.print_help()
423+
return 1
424+
425+
raw_rip = parse_register_value(args.rip)
426+
427+
# Build crash context
428+
context = CrashContext(
429+
rip=normalize_code_address(raw_rip, slide),
430+
rax=parse_register_value(args.rax) if args.rax else None,
431+
rdi=parse_register_value(args.rdi) if args.rdi else None,
432+
rsi=parse_register_value(args.rsi) if args.rsi else None,
433+
rdx=parse_register_value(args.rdx) if args.rdx else None,
434+
)
435+
436+
# Parse stack trace
437+
if args.stack_trace:
438+
context.stack_trace = [
439+
normalize_code_address(parse_register_value(addr.strip()), slide)
440+
for addr in args.stack_trace.split(',')
441+
]
442+
443+
if slide and not args.json:
444+
print(f"Applied slide: {hex(slide)}")
445+
print(f"RIP normalized: {hex(raw_rip)} -> {hex(context.rip)}")
446+
447+
# Perform analysis
448+
analyzer = CrashAnalyzer(args.binary, verbose=args.verbose)
449+
result = analyzer.analyze(context)
450+
451+
if args.json:
452+
output = {
453+
'crash_context': context.to_dict(),
454+
'analysis': result.to_dict(),
455+
}
456+
print(json.dumps(output, indent=2))
457+
else:
458+
print_analysis_report(result, context)
459+
460+
return 0
461+
462+
except FileNotFoundError as e:
463+
print(f"Error: {e}", file=sys.stderr)
464+
return 1
465+
except ValueError as e:
466+
print(f"Error: Invalid register value: {e}", file=sys.stderr)
467+
return 1
468+
except Exception as e:
469+
print(f"Error: {e}", file=sys.stderr)
470+
if args.verbose:
471+
import traceback
472+
traceback.print_exc()
473+
return 1
474+
475+
476+
if __name__ == '__main__':
477+
sys.exit(main())
478+
479+
480+
481+

‎tools/symbolicate.py‎

Lines changed: 577 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,577 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Symbolication Tool for ravynOS Crash Analysis
4+
5+
This tool maps crash addresses to symbols in compiled binaries, helping identify
6+
the exact function and instruction where a crash occurred.
7+
8+
Usage:
9+
./symbolicate.py <binary_path> <address> [<address2> ...]
10+
./symbolicate.py <binary_path> --rip <address> --rax <address>
11+
./symbolicate.py <binary_path> --stack-trace <addr1>,<addr2>,<addr3>
12+
13+
Examples:
14+
./symbolicate.py /path/to/launchd 0x1d9660
15+
./symbolicate.py /path/to/dyld 0x00000001154ad2eb
16+
./symbolicate.py /path/to/dyld --rip 0x1d9660 --rax 0x8010000000000007
17+
./symbolicate.py /path/to/dyld --stack-trace 0x1d9660,0x1d9700,0x1d9750
18+
"""
19+
20+
import sys
21+
import os
22+
import subprocess
23+
import re
24+
import argparse
25+
from dataclasses import dataclass
26+
from typing import List, Optional, Dict, Tuple
27+
import json
28+
29+
@dataclass
30+
class Symbol:
31+
"""Represents a symbol in the binary"""
32+
address: int
33+
size: int
34+
name: str
35+
section: str
36+
is_function: bool
37+
38+
def contains(self, addr: int) -> bool:
39+
"""Check if address falls within this symbol's range"""
40+
return self.address <= addr < (self.address + self.size)
41+
42+
def offset(self, addr: int) -> int:
43+
"""Get offset from symbol start to given address"""
44+
return addr - self.address
45+
46+
def __repr__(self) -> str:
47+
offset_str = f"+0x{self.offset(addr):x}" if hasattr(self, 'addr') else ""
48+
return f"{self.name} ({self.section}) @ 0x{self.address:x}"
49+
50+
51+
@dataclass
52+
class SymbolicationResult:
53+
"""Result of symbolication for a single address"""
54+
address: int
55+
symbol: Optional[Symbol]
56+
context: Optional[str] # e.g., "first instruction", "middle of function"
57+
58+
def __str__(self) -> str:
59+
if not self.symbol:
60+
return f"0x{self.address:x} - Unknown symbol"
61+
62+
offset = self.offset()
63+
if offset == 0:
64+
offset_str = " (entry point)"
65+
else:
66+
offset_str = f" +0x{offset:x}"
67+
68+
return f"0x{self.address:x} -> {self.symbol.name}{offset_str} ({self.symbol.section})"
69+
70+
def offset(self) -> int:
71+
if self.symbol:
72+
return self.symbol.offset(self.address)
73+
return 0
74+
75+
76+
class Symbolizer:
77+
"""Main symbolication engine"""
78+
79+
def __init__(self, binary_path: str, verbose: bool = False):
80+
self.binary_path = binary_path
81+
self.verbose = verbose
82+
self.symbols: List[Symbol] = []
83+
self._symbol_map: Dict[int, Symbol] = {}
84+
85+
if not os.path.exists(binary_path):
86+
raise FileNotFoundError(f"Binary not found: {binary_path}")
87+
88+
self._load_symbols()
89+
90+
def _find_otool(self) -> Optional[str]:
91+
"""Find otool in common locations"""
92+
possible_paths = [
93+
'otool',
94+
'/usr/bin/otool',
95+
'/usr/local/bin/otool',
96+
'/nest/build/Developer/Toolchains/Default.xctoolchain/usr/bin/otool',
97+
]
98+
99+
for path in possible_paths:
100+
try:
101+
result = subprocess.run([path, '-h', '--version'],
102+
capture_output=True, timeout=2)
103+
if result.returncode == 0:
104+
return path
105+
except:
106+
pass
107+
108+
return None
109+
110+
def _load_symbols(self):
111+
"""Load symbols from the binary using nm or otool"""
112+
try:
113+
# First, check if this is a Mach-O binary (ravynOS/macOS)
114+
if self._is_macho_binary():
115+
self._load_symbols_from_macho()
116+
else:
117+
# Fall back to ELF (nm)
118+
self._load_symbols_from_elf()
119+
120+
except FileNotFoundError:
121+
raise RuntimeError("Symbol tools not found. Install binutils and/or otool.")
122+
except subprocess.TimeoutExpired:
123+
raise RuntimeError("Symbol loading timed out")
124+
125+
if self.verbose:
126+
print(f"Loaded {len(self.symbols)} symbols", file=sys.stderr)
127+
128+
def _is_macho_binary(self) -> bool:
129+
"""Check if binary is Mach-O format"""
130+
try:
131+
result = subprocess.run(
132+
['file', self.binary_path],
133+
capture_output=True,
134+
text=True,
135+
timeout=10
136+
)
137+
return 'Mach-O' in result.stdout
138+
except:
139+
return False
140+
141+
def _load_symbols_from_elf(self):
142+
"""Load symbols from ELF binary using nm"""
143+
result = subprocess.run(
144+
['nm', '-nSt', self.binary_path],
145+
capture_output=True,
146+
text=True,
147+
timeout=30
148+
)
149+
150+
if result.returncode != 0:
151+
if self.verbose:
152+
print(f"Warning: nm failed: {result.stderr}", file=sys.stderr)
153+
return
154+
155+
self._parse_nm_output(result.stdout)
156+
157+
def _load_symbols_from_macho(self):
158+
"""Load symbols from Mach-O binary using otool"""
159+
# Find otool
160+
otool_path = self._find_otool()
161+
if not otool_path:
162+
if self.verbose:
163+
print(f"Warning: otool not found", file=sys.stderr)
164+
return
165+
166+
# Get disassembly with symbols
167+
result = subprocess.run(
168+
[otool_path, '-tv', self.binary_path],
169+
capture_output=True,
170+
text=True,
171+
timeout=30
172+
)
173+
174+
if result.returncode != 0:
175+
if self.verbose:
176+
print(f"Warning: otool -tv failed: {result.stderr}", file=sys.stderr)
177+
return
178+
179+
self._parse_otool_output(result.stdout)
180+
181+
def _parse_nm_output(self, output: str):
182+
"""Parse nm output in format: address size type section name"""
183+
# Format from 'nm -nSt': address size type section name
184+
pattern = r'^([0-9a-f]+)\s+([0-9a-f]+)\s+(.)\s+(\S+)\s+(.+)$'
185+
186+
for line in output.strip().split('\n'):
187+
if not line.strip():
188+
continue
189+
190+
match = re.match(pattern, line)
191+
if not match:
192+
continue
193+
194+
addr_str, size_str, sym_type, section, name = match.groups()
195+
196+
try:
197+
address = int(addr_str, 16)
198+
size = int(size_str, 16)
199+
is_function = sym_type in 'tT' # lowercase = local, uppercase = global
200+
201+
symbol = Symbol(
202+
address=address,
203+
size=size,
204+
name=name,
205+
section=section,
206+
is_function=is_function
207+
)
208+
209+
self.symbols.append(symbol)
210+
if address not in self._symbol_map:
211+
self._symbol_map[address] = symbol
212+
213+
except (ValueError, IndexError) as e:
214+
if self.verbose:
215+
print(f"Warning: Failed to parse symbol line: {line}", file=sys.stderr)
216+
217+
# Sort symbols by address for binary search
218+
self.symbols.sort(key=lambda s: s.address)
219+
220+
def _parse_otool_output(self, output: str):
221+
"""Parse otool -tv output for Mach-O binaries
222+
223+
Format example:
224+
_main:
225+
0000000100000be0 pushq %rbp
226+
227+
Or with actual symbol names at the start
228+
"""
229+
current_symbol = None
230+
current_address = None
231+
max_instruction_address = None
232+
233+
for line in output.strip().split('\n'):
234+
line = line.strip()
235+
if not line:
236+
continue
237+
238+
# Check for symbol name (function definition)
239+
# Format: _functionname: or functionname:
240+
if line.endswith(':') and not line[0].isdigit():
241+
symbol_name = line[:-1]
242+
current_symbol = symbol_name
243+
current_address = None
244+
continue
245+
246+
# Parse instruction lines to get addresses
247+
# Format: 0000000100000be0 pushq %rbp
248+
parts = line.split()
249+
if len(parts) >= 2 and all(c in '0123456789abcdef' for c in parts[0]):
250+
try:
251+
address = int(parts[0], 16)
252+
if max_instruction_address is None or address > max_instruction_address:
253+
max_instruction_address = address
254+
255+
if current_symbol and current_address is None:
256+
# This is the first instruction of a symbol
257+
current_address = address
258+
259+
# Create symbol entry
260+
symbol = Symbol(
261+
address=address,
262+
size=0, # We don't know the size from otool -tv alone
263+
name=current_symbol,
264+
section='__text', # Usually in text section
265+
is_function=True
266+
)
267+
268+
self.symbols.append(symbol)
269+
if address not in self._symbol_map:
270+
self._symbol_map[address] = symbol
271+
272+
current_symbol = None
273+
274+
except (ValueError, IndexError):
275+
pass
276+
277+
# Sort symbols and infer synthetic sizes so range lookups work for Mach-O.
278+
self.symbols.sort(key=lambda s: s.address)
279+
for i in range(len(self.symbols) - 1):
280+
next_addr = self.symbols[i + 1].address
281+
curr_addr = self.symbols[i].address
282+
inferred = max(1, next_addr - curr_addr)
283+
if self.symbols[i].size == 0:
284+
self.symbols[i].size = inferred
285+
286+
# Keep the final symbol searchable for nearby addresses.
287+
if self.symbols and self.symbols[-1].size == 0:
288+
if max_instruction_address is not None and max_instruction_address >= self.symbols[-1].address:
289+
self.symbols[-1].size = (max_instruction_address - self.symbols[-1].address) + 1
290+
else:
291+
self.symbols[-1].size = 1
292+
293+
def symbolicate(self, address: int) -> SymbolicationResult:
294+
"""Find the symbol containing the given address"""
295+
296+
# Quick lookup if exact address exists
297+
if address in self._symbol_map:
298+
return SymbolicationResult(
299+
address=address,
300+
symbol=self._symbol_map[address],
301+
context="entry point"
302+
)
303+
304+
# Binary search for containing symbol
305+
symbol = self._find_containing_symbol(address)
306+
307+
if symbol:
308+
context = None
309+
offset = symbol.offset(address)
310+
311+
# Provide context based on offset
312+
if offset == 0:
313+
context = "entry point"
314+
elif offset < 32: # Typical prologue size
315+
context = "in prologue"
316+
elif offset == symbol.size - 1:
317+
context = "at exit"
318+
319+
return SymbolicationResult(
320+
address=address,
321+
symbol=symbol,
322+
context=context
323+
)
324+
325+
return SymbolicationResult(address=address, symbol=None, context=None)
326+
327+
def _find_containing_symbol(self, address: int) -> Optional[Symbol]:
328+
"""Binary search to find symbol containing address"""
329+
left, right = 0, len(self.symbols) - 1
330+
331+
while left <= right:
332+
mid = (left + right) // 2
333+
symbol = self.symbols[mid]
334+
335+
if symbol.contains(address):
336+
return symbol
337+
elif symbol.address > address:
338+
right = mid - 1
339+
else:
340+
left = mid + 1
341+
342+
# Check the symbol just before the address
343+
if right >= 0 and right < len(self.symbols):
344+
symbol = self.symbols[right]
345+
if symbol.contains(address):
346+
return symbol
347+
348+
return None
349+
350+
def symbolicate_multiple(self, addresses: List[int]) -> List[SymbolicationResult]:
351+
"""Symbolicate multiple addresses"""
352+
return [self.symbolicate(addr) for addr in addresses]
353+
354+
def find_symbol_by_name(self, name: str, exact: bool = False) -> List[Symbol]:
355+
"""Find symbols by name (substring or exact match)"""
356+
results = []
357+
for symbol in self.symbols:
358+
if exact:
359+
if symbol.name == name:
360+
results.append(symbol)
361+
else:
362+
if name.lower() in symbol.name.lower():
363+
results.append(symbol)
364+
return results
365+
366+
def get_context_around_address(self, address: int, before: int = 5, after: int = 5) -> List[Symbol]:
367+
"""Get symbols around a given address"""
368+
idx = self._find_symbol_index(address)
369+
if idx is None:
370+
return []
371+
372+
start = max(0, idx - before)
373+
end = min(len(self.symbols), idx + after + 1)
374+
return self.symbols[start:end]
375+
376+
def _find_symbol_index(self, address: int) -> Optional[int]:
377+
"""Find the index of a symbol containing or near the address"""
378+
left, right = 0, len(self.symbols) - 1
379+
380+
while left <= right:
381+
mid = (left + right) // 2
382+
symbol = self.symbols[mid]
383+
384+
if symbol.contains(address):
385+
return mid
386+
elif symbol.address > address:
387+
right = mid - 1
388+
else:
389+
left = mid + 1
390+
391+
return right if right >= 0 else None
392+
393+
394+
def parse_hex_address(addr_str: str) -> int:
395+
"""Parse address string (with or without 0x prefix)"""
396+
addr_str = addr_str.strip()
397+
if addr_str.startswith(('0x', '0X')):
398+
return int(addr_str, 16)
399+
return int(addr_str, 16)
400+
401+
402+
def format_hex_address(addr: int) -> str:
403+
"""Format address as hex string"""
404+
return f"0x{addr:x}"
405+
406+
407+
def normalize_code_address(raw_addr: int, slide: int) -> int:
408+
"""Normalize a runtime code address to a file-relative address using slide."""
409+
if slide < 0:
410+
raise ValueError("slide must be non-negative")
411+
if raw_addr < slide:
412+
raise ValueError(f"address {format_hex_address(raw_addr)} is smaller than slide {format_hex_address(slide)}")
413+
return raw_addr - slide
414+
415+
416+
def main():
417+
parser = argparse.ArgumentParser(
418+
description='Symbolicate crash addresses in ravynOS binaries',
419+
formatter_class=argparse.RawDescriptionHelpFormatter,
420+
epilog='''
421+
Examples:
422+
%(prog)s /path/to/launchd 0x1d9660
423+
%(prog)s /path/to/dyld 0x1d9660 0x1d9700 0x1d9750
424+
%(prog)s /path/to/dyld --rip 0x1d9660
425+
%(prog)s /path/to/dyld --stack-trace 0x1d9660,0x1d9700,0x1d9750
426+
%(prog)s /path/to/dyld --search syscall
427+
'''
428+
)
429+
430+
parser.add_argument('binary', help='Path to the binary to symbolicate')
431+
parser.add_argument('addresses', nargs='*', help='Addresses to symbolicate')
432+
parser.add_argument('--rip', type=str, help='RIP register value (crashed instruction pointer)')
433+
parser.add_argument('--rax', type=str, help='RAX register value (for context)')
434+
parser.add_argument('--stack-trace', type=str, help='Stack trace addresses (comma-separated)')
435+
parser.add_argument('--slide', type=str, help='ASLR slide to subtract from code addresses (e.g. 0x11526d000)')
436+
parser.add_argument('--search', type=str, help='Search for symbol by name')
437+
parser.add_argument('--context', type=int, default=5, help='Symbols to show around address (default: 5)')
438+
parser.add_argument('--json', action='store_true', help='Output as JSON')
439+
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
440+
441+
args = parser.parse_args()
442+
443+
try:
444+
slide = parse_hex_address(args.slide) if args.slide else 0
445+
446+
# Initialize symbolizer
447+
symbolizer = Symbolizer(args.binary, verbose=args.verbose)
448+
449+
# Handle search mode
450+
if args.search:
451+
print(f"\nSearching for symbols matching: {args.search}")
452+
results = symbolizer.find_symbol_by_name(args.search)
453+
if results:
454+
for symbol in results:
455+
print(f" 0x{symbol.address:x} {symbol.name} ({symbol.section}) size=0x{symbol.size:x}")
456+
else:
457+
print(" No symbols found")
458+
return 0
459+
460+
# Collect addresses to symbolicate
461+
addresses_to_symbolicate = []
462+
463+
# Add RIP if provided
464+
if args.rip:
465+
try:
466+
raw_rip = parse_hex_address(args.rip)
467+
normalized_rip = normalize_code_address(raw_rip, slide)
468+
addresses_to_symbolicate.append(normalized_rip)
469+
if slide:
470+
print(f"RIP: {args.rip} -> {format_hex_address(normalized_rip)} (slide={format_hex_address(slide)})")
471+
else:
472+
print(f"RIP: {args.rip}")
473+
except ValueError:
474+
print(f"Error: Invalid RIP address: {args.rip}", file=sys.stderr)
475+
return 1
476+
477+
# Add RAX if provided (for reference)
478+
if args.rax:
479+
try:
480+
rax_addr = parse_hex_address(args.rax)
481+
print(f"RAX: {args.rax} (invalid pointer marker)")
482+
except ValueError:
483+
print(f"Error: Invalid RAX address: {args.rax}", file=sys.stderr)
484+
485+
# Add stack trace addresses
486+
if args.stack_trace:
487+
try:
488+
for addr_str in args.stack_trace.split(','):
489+
raw_addr = parse_hex_address(addr_str.strip())
490+
addresses_to_symbolicate.append(normalize_code_address(raw_addr, slide))
491+
except ValueError as e:
492+
print(f"Error: Invalid stack trace address: {e}", file=sys.stderr)
493+
return 1
494+
495+
# Add positional arguments
496+
for addr_str in args.addresses:
497+
try:
498+
raw_addr = parse_hex_address(addr_str)
499+
addresses_to_symbolicate.append(normalize_code_address(raw_addr, slide))
500+
except ValueError:
501+
print(f"Error: Invalid address: {addr_str}", file=sys.stderr)
502+
return 1
503+
504+
if not addresses_to_symbolicate:
505+
parser.print_help()
506+
return 1
507+
508+
# Symbolicate
509+
results = symbolizer.symbolicate_multiple(addresses_to_symbolicate)
510+
511+
if args.json:
512+
# JSON output
513+
json_results = []
514+
for result in results:
515+
json_obj = {
516+
'address': format_hex_address(result.address),
517+
'symbol': result.symbol.name if result.symbol else None,
518+
'section': result.symbol.section if result.symbol else None,
519+
'offset': f"0x{result.offset():x}" if result.symbol else None,
520+
'context': result.context
521+
}
522+
json_results.append(json_obj)
523+
print(json.dumps(json_results, indent=2))
524+
else:
525+
# Human-readable output
526+
print("\n=== Symbolication Results ===\n")
527+
528+
for i, result in enumerate(results):
529+
print(f"{i+1}. {result}")
530+
531+
# Show context
532+
if result.symbol:
533+
print(f" Size: 0x{result.symbol.size:x} bytes")
534+
print(f" Offset: 0x{result.offset():x}")
535+
if result.context:
536+
print(f" Context: {result.context}")
537+
538+
# Show nearby symbols
539+
if args.context > 0:
540+
context_symbols = symbolizer.get_context_around_address(
541+
result.address,
542+
before=args.context,
543+
after=args.context
544+
)
545+
print(f" Nearby symbols:")
546+
for nearby in context_symbols:
547+
marker = " <--" if nearby == result.symbol else ""
548+
print(f" 0x{nearby.address:x} {nearby.name}{marker}")
549+
550+
print()
551+
552+
return 0
553+
554+
except FileNotFoundError as e:
555+
print(f"Error: {e}", file=sys.stderr)
556+
return 1
557+
except RuntimeError as e:
558+
print(f"Error: {e}", file=sys.stderr)
559+
return 1
560+
except Exception as e:
561+
print(f"Unexpected error: {e}", file=sys.stderr)
562+
if args.verbose:
563+
import traceback
564+
traceback.print_exc()
565+
return 1
566+
567+
568+
if __name__ == '__main__':
569+
sys.exit(main())
570+
571+
572+
573+
574+
575+
576+
577+

0 commit comments

Comments
 (0)
Please sign in to comment.