Repository navigation
Expand file tree
/
Copy patharm-stack
More file actions
executable file
·2331 lines (1951 loc) · 97.8 KB
/
Copy patharm-stack
File metadata and controls
executable file
·2331 lines (1951 loc) · 97.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
ARM Stack Usage Analyzer
A comprehensive tool for analyzing stack usage in ARM embedded projects.
This tool parses object files and stack usage information to provide detailed
analysis of function call graphs and stack consumption patterns.
Originally inspired by avstack.pl, this implementation provides enhanced
visualization, filtering capabilities, and better error handling.
Requirements:
- ARM GCC toolchain (arm-none-eabi-objdump)
- Project compiled with -fstack-usage flag
- Object files (.o or .c.obj) and stack usage files (.su)
Author: wizath
GitHub: https://github.com/wizath
License: MIT
Version: 1.13
"""
import sys
import os
import re
import subprocess
import glob
import json
import logging
from collections import defaultdict, namedtuple
from typing import Dict, Set, List, Optional
import argparse
from pathlib import Path
from datetime import datetime
# Version information
__version__ = "1.13"
__author__ = "wizath"
__github__ = "https://github.com/wizath"
class Colors:
"""ANSI color codes for enhanced terminal output"""
HEADER = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
END = '\033[0m'
# Tool configuration constants
DEFAULT_OBJDUMP = "arm-none-eabi-objdump"
CALL_COST = 4 # ARM function call overhead in bytes
DEFAULT_STACK_MARGIN = 1.5 # Safety margin for thread stack recommendations
SUBPROCESS_TIMEOUT = 60 # Timeout for subprocess calls in seconds
MAX_UNRESOLVED_DISPLAY = 20 # Maximum unresolved functions to display
MAX_FUNCTION_NAME_LEN = 39 # Maximum function name length before truncation
# RTOS thread/task creation patterns for auto-detection
# Format: (function_pattern, entry_func_arg_position, stack_size_arg_position)
RTOS_THREAD_PATTERNS = {
'FreeRTOS': [
(r'xTaskCreate\s*\(\s*(\w+)', 0, None),
(r'xTaskCreateStatic\s*\(\s*(\w+)', 0, None),
],
'Zephyr': [
(r'k_thread_create\s*\([^,]+,\s*[^,]+,\s*[^,]+,\s*(\w+)', 0, None),
(r'K_THREAD_DEFINE\s*\(\s*\w+\s*,\s*\d+\s*,\s*(\w+)', 0, None),
],
'CMSIS-RTOS': [
(r'osThreadNew\s*\(\s*(\w+)', 0, None),
(r'osThreadCreate\s*\([^,]+,\s*(\w+)', 0, None),
],
'ThreadX': [
(r'tx_thread_create\s*\([^,]+,\s*[^,]+,\s*(\w+)', 0, None),
],
'Azure-RTOS': [
(r'tx_thread_create\s*\([^,]+,\s*[^,]+,\s*(\w+)', 0, None),
],
'embOS': [
(r'OS_TASK_Create\s*\([^,]+,\s*[^,]+,\s*(\w+)', 0, None),
(r'OS_TASK_CreateEx\s*\([^,]+,\s*[^,]+,\s*(\w+)', 0, None),
],
'uC/OS': [
(r'OSTaskCreate\s*\(\s*(\w+)', 0, None),
(r'OSTaskCreateExt\s*\(\s*(\w+)', 0, None),
],
'POSIX': [
(r'pthread_create\s*\([^,]+,\s*[^,]+,\s*(\w+)', 0, None),
],
}
# Data structure for function information
FunctionInfo = namedtuple('FunctionInfo', [
'name', # Function name (cleaned)
'cost', # Total stack cost including callees
'frame_size', # Local frame size from .su file
'height', # Call chain depth
'is_recursive', # Whether function is recursive
'is_root', # Whether function is a root (entry point)
'is_unbounded', # Whether function has unbounded stack usage
'unbounded_reason', # Reason for unbounded stack (recursion, function_pointer, etc.)
'code_size', # Function code size in flash (bytes)
'data_size', # Initialized data (.data) per source file
'bss_size', # Uninitialized data (.bss) per source file
'has_heap_alloc' # Whether function calls malloc/free etc.
])
# Heap allocation function patterns to detect
HEAP_ALLOC_FUNCTIONS = {
'malloc', 'free', 'calloc', 'realloc', # Standard C
'pvPortMalloc', 'vPortFree', # FreeRTOS
'k_malloc', 'k_free', # Zephyr
'mem_malloc', 'mem_free', # lwIP
'OS_MemGet', 'OS_MemPut', # uC/OS
}
# Data structure for detected thread/task information
ThreadInfo = namedtuple('ThreadInfo', [
'entry_func', # Thread entry function name
'rtos', # Detected RTOS type
'source_file', # Source file where thread was created
'stack_usage', # Calculated stack usage
'recommended', # Recommended stack size (with margin)
'is_unbounded' # Whether stack usage is unbounded
])
class AnalysisData:
"""Shared data container for analysis results between components."""
def __init__(self):
# Core data structures for analysis
self.frame_size: Dict[str, int] = {}
self.call_graph: Dict[str, Set[str]] = defaultdict(set)
self.total_cost: Dict[str, int] = {}
self.call_depth: Dict[str, int] = {}
self.visited: Dict[str, str] = {}
self.has_caller: Set[str] = set()
self.unresolved: Set[str] = set()
# Symbol resolution tables
self.addresses: Dict[int, str] = {}
self.global_name: Dict[str, str] = {}
self.ambiguous: Set[str] = set()
# Function pointer detection
self.has_function_pointer: Set[str] = set()
self.rtl_available: bool = False
# Code size tracking (flash usage)
self.code_size: Dict[str, int] = {}
self.elf_code_size: Dict[str, int] = {}
self.elf_file: Optional[str] = None
# Static allocation tracking (RAM usage)
self.data_size: Dict[str, int] = {}
self.bss_size: Dict[str, int] = {}
self.static_size: Dict[str, int] = {}
self.elf_data_size: Dict[str, int] = {}
self.elf_bss_size: Dict[str, int] = {}
self.func_static_vars: Dict[str, List[tuple]] = {}
# Memory region tracking (symbol -> section name)
self.symbol_sections: Dict[str, str] = {} # symbol -> section name
self.section_sizes: Dict[str, int] = {} # section -> total size
self.section_symbols: Dict[str, Dict[str, int]] = {} # section -> {symbol: size}
# Heap allocation detection
self.has_heap_alloc: Set[str] = set()
# Git repository information
self.git_info: Dict[str, str] = {}
# Thread/task detection
self.detected_threads: List[ThreadInfo] = []
class ObjectFileParser:
"""Handles parsing of object files, .su files, and ELF binaries."""
def __init__(self, build_dir: Path, objdump_path: str, data: AnalysisData):
self.build_dir = build_dir
self.objdump_path = objdump_path
self.data = data
self.logger = logging.getLogger(__name__)
def find_object_files(self) -> List[str]:
"""Locate all relevant object files in the build directory."""
patterns = ["**/*.o", "**/*.c.obj"]
obj_files = []
for pattern in patterns:
obj_files.extend(glob.glob(str(self.build_dir / pattern), recursive=True))
filtered = [
f for f in obj_files
if "CMakeFiles/CompilerIdC" not in f
and "CMakeFiles/CompilerIdCXX" not in f
]
self.logger.info(f" Found {len(filtered)} object files")
return sorted(filtered)
def parse_disassembly(self, obj_file: str) -> None:
"""Parse object file disassembly to extract call graph."""
try:
result = subprocess.run(
[self.objdump_path, "-dr", obj_file],
capture_output=True, text=True, check=True,
timeout=SUBPROCESS_TIMEOUT
)
current_function = None
for line in result.stdout.splitlines():
line = line.strip()
func_match = re.match(r'^([0-9a-fA-F]+) <(.*)>:', line)
if func_match:
addr, name = func_match.groups()
current_function = f"{name}@{obj_file}"
self.data.call_graph[current_function] = set()
if name in self.data.global_name:
self.data.ambiguous.add(name)
self.data.global_name[name] = current_function
addr = addr.lstrip('0') or '0'
self.data.addresses[f"{addr}@{obj_file}"] = current_function
call_match = re.search(r': R_[A-Za-z0-9_]+_CALL\s+(.+)', line)
if call_match and current_function:
target = call_match.group(1)
if target in HEAP_ALLOC_FUNCTIONS:
self.data.has_heap_alloc.add(current_function)
if target == ".text":
target = f"@{obj_file}"
elif target.startswith(".text+0x"):
offset = target[9:]
target = f"{offset}@{obj_file}"
self.data.call_graph[current_function].add(target)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
print(f"{Colors.RED}Error disassembling {obj_file}: {e}{Colors.END}")
sys.exit(1)
def parse_su_file(self, obj_file: str) -> None:
"""Parse stack usage file to extract frame sizes."""
if obj_file.endswith('.o'):
su_file = obj_file[:-2] + '.su'
elif obj_file.endswith('.c.obj'):
su_file = obj_file[:-6] + '.c.su'
else:
return
try:
with open(su_file, 'r') as f:
for line in f:
match = re.match(r'.*:(\w+)\s+(\d+)\s+(\w+)', line)
if match:
func_name, size, qualifier = match.groups()
func_full_name = f"{func_name}@{obj_file}"
self.data.frame_size[func_full_name] = int(size) + CALL_COST
except FileNotFoundError:
pass
def parse_static_sizes(self, obj_file: str) -> None:
"""Parse object file to extract .data and .bss sizes."""
nm_path = self.objdump_path.replace('objdump', 'nm')
try:
result = subprocess.run(
[nm_path, '--print-size', obj_file],
capture_output=True, text=True, check=True,
timeout=SUBPROCESS_TIMEOUT
)
total_data = 0
total_bss = 0
for line in result.stdout.splitlines():
parts = line.strip().split()
if len(parts) >= 4:
size_hex, sym_type, sym_name = parts[1], parts[2], parts[3]
try:
size = int(size_hex, 16)
except ValueError:
continue
if sym_type in ('D', 'd'):
total_data += size
var_type = 'data'
elif sym_type in ('B', 'b'):
total_bss += size
var_type = 'bss'
else:
continue
if '.' in sym_name:
func_name = sym_name.split('.')[0]
var_name = sym_name.split('.', 1)[1]
if func_name not in self.data.func_static_vars:
self.data.func_static_vars[func_name] = []
self.data.func_static_vars[func_name].append((var_name, size, var_type))
if total_data > 0:
self.data.data_size[obj_file] = total_data
if total_bss > 0:
self.data.bss_size[obj_file] = total_bss
if total_data + total_bss > 0:
self.data.static_size[obj_file] = total_data + total_bss
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
pass
def parse_elf(self, elf_file: str) -> None:
"""Parse ELF file for accurate linked sizes."""
self.data.elf_file = elf_file
nm_path = self.objdump_path.replace('objdump', 'nm')
self.logger.info(f"Parsing ELF file: {elf_file}")
try:
result = subprocess.run(
[nm_path, '--print-size', elf_file],
capture_output=True, text=True, check=True,
timeout=SUBPROCESS_TIMEOUT
)
for line in result.stdout.splitlines():
parts = line.strip().split()
if len(parts) >= 4:
size_hex, sym_type, name = parts[1], parts[2], parts[3]
try:
size = int(size_hex, 16)
except ValueError:
continue
if sym_type in ('T', 't'):
self.data.elf_code_size[name] = size
elif sym_type in ('D', 'd'):
self.data.elf_data_size[name] = size
elif sym_type in ('B', 'b'):
self.data.elf_bss_size[name] = size
self.logger.info(f" Code: {len(self.data.elf_code_size)} functions")
self.logger.info(f" .data: {len(self.data.elf_data_size)} symbols")
self.logger.info(f" .bss: {len(self.data.elf_bss_size)} symbols")
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as e:
self.logger.warning(f"Failed to parse ELF: {e}")
# Also parse sections for memory region detection
self._parse_elf_sections(elf_file)
def _parse_elf_sections(self, elf_file: str) -> None:
"""Parse ELF sections to detect memory regions (SRAM vs HyperRAM etc)."""
try:
# Use objdump -h to get section headers
result = subprocess.run(
[self.objdump_path, '-h', elf_file],
capture_output=True, text=True, check=True,
timeout=SUBPROCESS_TIMEOUT
)
for line in result.stdout.splitlines():
# Format: "idx name size vma lma file_off algn flags"
# Example: " 3 .data 00001234 20000000 08010000 00010000 2**2"
parts = line.split()
if len(parts) >= 6 and parts[0].isdigit():
section_name = parts[1]
try:
size = int(parts[2], 16)
vma = int(parts[3], 16)
except ValueError:
continue
if size > 0:
self.data.section_sizes[section_name] = size
# Now get symbol-to-section mapping using nm -S
nm_path = self.objdump_path.replace('objdump', 'nm')
result = subprocess.run(
[nm_path, '-S', '--format=sysv', elf_file],
capture_output=True, text=True, check=True,
timeout=SUBPROCESS_TIMEOUT
)
for line in result.stdout.splitlines():
# SysV format: "name |address |class|type|size|line|section"
if '|' in line:
parts = [p.strip() for p in line.split('|')]
if len(parts) >= 7:
name, size_str, section = parts[0], parts[4], parts[6]
if name and section and section != '':
self.data.symbol_sections[name] = section
# Parse size and group by section
try:
size = int(size_str, 16) if size_str else 0
if size > 0:
if section not in self.data.section_symbols:
self.data.section_symbols[section] = {}
self.data.section_symbols[section][name] = size
except ValueError:
pass
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
pass
def parse_map_file(self, map_file: str) -> None:
"""Parse linker map file for detailed memory region info."""
self.logger.info(f"Parsing map file: {map_file}")
try:
with open(map_file, 'r') as f:
content = f.read()
# Parse memory configuration section for section-to-region mapping
# This is informational - actual usage comes from ELF sections
self.logger.info(" Parsed map file for memory layout info")
except FileNotFoundError:
self.logger.warning(f"Map file not found: {map_file}")
except Exception as e:
self.logger.warning(f"Failed to parse map file: {e}")
class CallGraphAnalyzer:
"""Handles call graph analysis and stack cost calculation."""
def __init__(self, data: AnalysisData, show_warnings: bool = True):
self.data = data
self.show_warnings = show_warnings
self.logger = logging.getLogger(__name__)
def resolve_symbols(self) -> None:
"""Resolve function references across object files."""
for func_name, callees in list(self.data.call_graph.items()):
resolved_callees = set()
for callee in callees:
if callee.startswith('@'):
resolved_callees.add(callee)
elif '@' in callee:
addr_key = callee
if addr_key in self.data.addresses:
resolved_callees.add(self.data.addresses[addr_key])
else:
self.data.unresolved.add(callee)
elif callee in self.data.global_name:
resolved_callees.add(self.data.global_name[callee])
else:
self.data.unresolved.add(callee)
self.data.call_graph[func_name] = resolved_callees
def trace_call_graph(self) -> None:
"""Calculate stack costs through DFS traversal."""
self.logger.info("Analyzing call graph...")
def trace(func_name: str) -> None:
if func_name in self.data.visited:
if self.data.visited[func_name] == "?":
self.data.visited[func_name] = "R"
self.data.total_cost[func_name] = float('inf')
self.data.call_depth[func_name] = float('inf')
return
self.data.visited[func_name] = "?"
if func_name in self.data.has_function_pointer:
self.data.visited[func_name] = "P"
self.data.total_cost[func_name] = float('inf')
self.data.call_depth[func_name] = float('inf')
return
max_depth = 0
max_frame = 0
if func_name in self.data.call_graph:
for callee in self.data.call_graph[func_name]:
self.data.has_caller.add(callee)
trace(callee)
callee_cost = self.data.total_cost.get(callee, 0)
callee_depth = self.data.call_depth.get(callee, 0)
if callee_cost == float('inf') or callee_depth == float('inf'):
self.data.visited[func_name] = "U"
self.data.total_cost[func_name] = float('inf')
self.data.call_depth[func_name] = float('inf')
return
max_frame = max(max_frame, callee_cost)
max_depth = max(max_depth, callee_depth)
self.data.call_depth[func_name] = max_depth + 1
self.data.total_cost[func_name] = max_frame + self.data.frame_size.get(func_name, 0)
if self.data.visited[func_name] == "?":
self.data.visited[func_name] = " "
for func_name in self.data.call_graph:
trace(func_name)
class StackAnalyzer:
"""
Main orchestrator for ARM stack usage analysis.
Coordinates the parsing, analysis, and reporting components:
- ObjectFileParser: Parses object files, .su files, and ELF binaries
- CallGraphAnalyzer: Resolves symbols and calculates stack costs
- Report generation methods for various output formats
"""
def __init__(
self,
build_dir: str,
threshold: int = 10,
show_warnings: bool = True,
objdump_path: str = DEFAULT_OBJDUMP,
stack_margin: float = DEFAULT_STACK_MARGIN):
"""
Initialize the stack analyzer.
Args:
build_dir: Path to build directory containing object files
threshold: Minimum stack usage threshold for reporting (bytes)
show_warnings: Whether to display symbol resolution warnings
objdump_path: Path to objdump executable
stack_margin: Safety margin multiplier for thread stack recommendations
"""
self.build_dir = Path(build_dir)
self.threshold = threshold
self.show_warnings = show_warnings
self.objdump_path = objdump_path
self.stack_margin = stack_margin
self.logger = logging.getLogger(__name__)
# Shared data container
self._data = AnalysisData()
# Component classes
self._parser = ObjectFileParser(self.build_dir, objdump_path, self._data)
self._analyzer = CallGraphAnalyzer(self._data, show_warnings)
# Expose data attributes for backward compatibility
self.frame_size = self._data.frame_size
self.call_graph = self._data.call_graph
self.total_cost = self._data.total_cost
self.call_depth = self._data.call_depth
self.visited = self._data.visited
self.has_caller = self._data.has_caller
self.unresolved = self._data.unresolved
self.addresses = self._data.addresses
self.global_name = self._data.global_name
self.ambiguous = self._data.ambiguous
self.has_function_pointer = self._data.has_function_pointer
self.rtl_available = self._data.rtl_available
self.code_size = self._data.code_size
self.elf_code_size = self._data.elf_code_size
self.elf_file = self._data.elf_file
self.data_size = self._data.data_size
self.bss_size = self._data.bss_size
self.static_size = self._data.static_size
# ELF-based static sizes (accurate after linking)
self.elf_data_size = self._data.elf_data_size
self.elf_bss_size = self._data.elf_bss_size
self.func_static_vars = self._data.func_static_vars
self.has_heap_alloc = self._data.has_heap_alloc
self.git_info = self._data.git_info
self.detected_threads = self._data.detected_threads
self.symbol_sections = self._data.symbol_sections
self.section_sizes = self._data.section_sizes
self.section_symbols = self._data.section_symbols
def find_object_files(self) -> List[str]:
"""Locate all relevant object files in the build directory."""
if not self.build_dir.exists():
raise FileNotFoundError(f"Build directory '{self.build_dir}' does not exist")
return self._parser.find_object_files()
def _find_rtl_files(self, obj_files: List[str]) -> List[str]:
"""
Find RTL dump files corresponding to object files.
RTL files are generated with -fdump-rtl-dfinish and have extensions like:
- .c.270r.dfinish (GCC 5.x)
- .c.271r.dfinish (GCC 6.x)
- .c.272r.dfinish (GCC 7.x+)
Args:
obj_files: List of object file paths
Returns:
List of RTL dump file paths
"""
rtl_files = []
for obj_file in obj_files:
# Get base name for RTL file search
if obj_file.endswith('.o'):
base_name = obj_file[:-2] # Remove .o
elif obj_file.endswith('.c.obj'):
base_name = obj_file[:-6] # Remove .c.obj
else:
continue
# Search for RTL dump files with various extensions
rtl_pattern = f"{base_name}.c.*r.dfinish"
matching_files = glob.glob(rtl_pattern)
if matching_files:
# Use the first matching RTL file (should be only one)
rtl_files.append(matching_files[0])
return rtl_files
def _detect_git_info(self) -> None:
"""
Detect git repository information from current working directory.
Extracts:
- Git commit hash (full and short)
- Current branch name
- Repository status (clean/dirty)
- Remote origin URL (if available)
"""
try:
# Check if we're in a git repository
result = subprocess.run(['git', 'rev-parse', '--is-inside-work-tree'],
capture_output=True, text=True, cwd=os.getcwd(),
timeout=SUBPROCESS_TIMEOUT)
if result.returncode != 0:
self.logger.debug("Not in a git repository")
return
# Get commit hash (full)
result = subprocess.run(['git', 'rev-parse', 'HEAD'],
capture_output=True, text=True, cwd=os.getcwd(),
timeout=SUBPROCESS_TIMEOUT)
if result.returncode == 0:
self.git_info['commit_hash'] = result.stdout.strip()
self.git_info['commit_hash_short'] = result.stdout.strip()[:8]
# Get current branch name
result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
capture_output=True, text=True, cwd=os.getcwd(),
timeout=SUBPROCESS_TIMEOUT)
if result.returncode == 0:
self.git_info['branch'] = result.stdout.strip()
# Check if repository is clean
result = subprocess.run(['git', 'status', '--porcelain'],
capture_output=True, text=True, cwd=os.getcwd(),
timeout=SUBPROCESS_TIMEOUT)
if result.returncode == 0:
self.git_info['is_clean'] = len(result.stdout.strip()) == 0
self.git_info['status'] = 'clean' if self.git_info['is_clean'] else 'dirty'
# Get remote origin URL (if available)
result = subprocess.run(['git', 'remote', 'get-url', 'origin'],
capture_output=True, text=True, cwd=os.getcwd(),
timeout=SUBPROCESS_TIMEOUT)
if result.returncode == 0:
self.git_info['remote_url'] = result.stdout.strip()
# Get commit message (first line)
result = subprocess.run(['git', 'log', '-1', '--pretty=format:%s'],
capture_output=True, text=True, cwd=os.getcwd(),
timeout=SUBPROCESS_TIMEOUT)
if result.returncode == 0:
self.git_info['commit_message'] = result.stdout.strip()
# Get commit author and date
result = subprocess.run(['git', 'log', '-1', '--pretty=format:%an'],
capture_output=True, text=True, cwd=os.getcwd(),
timeout=SUBPROCESS_TIMEOUT)
if result.returncode == 0:
self.git_info['commit_author'] = result.stdout.strip()
result = subprocess.run(['git', 'log', '-1', '--pretty=format:%ci'],
capture_output=True, text=True, cwd=os.getcwd(),
timeout=SUBPROCESS_TIMEOUT)
if result.returncode == 0:
self.git_info['commit_date'] = result.stdout.strip()
if self.git_info:
branch = self.git_info.get('branch', 'unknown')
commit = self.git_info.get('commit_hash_short', 'unknown')
self.logger.info(f" ✓ Git repository detected: {branch}@{commit}")
if not self.git_info.get('is_clean', True):
self.logger.info(" ⚠ Repository has uncommitted changes")
except FileNotFoundError:
self.logger.debug("Git not available")
except Exception as e:
if self.show_warnings:
self.logger.warning(f"⚠️ Error detecting git info: {e}")
def check_prerequisites(self) -> None:
"""
Verify that all required tools and files are available for analysis.
Checks for:
- ARM objdump tool availability
- Build directory existence
- Presence of object files and stack usage files
Returns:
List of object files to process
Raises:
SystemExit: If prerequisites are not met
"""
self.logger.info("🔍 Checking prerequisites...")
# Detect git repository information
self._detect_git_info()
# Verify ARM toolchain availability
try:
subprocess.run([self.objdump_path, "--version"], capture_output=True, check=True,
timeout=SUBPROCESS_TIMEOUT)
self.logger.info(f" ✓ {self.objdump_path} found")
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
error_msg = (f"{Colors.RED}❌ Error: {self.objdump_path} not found or timed out. "
f"Please install ARM GCC toolchain or specify correct path with --objdump.{Colors.END}")
print(error_msg)
sys.exit(1)
# Verify build directory
if not self.build_dir.exists():
print(f"{Colors.RED}❌ Error: Build directory '{self.build_dir}' does not exist.{Colors.END}")
print(f"{Colors.YELLOW}💡 Please run cmake and build the project first.{Colors.END}")
sys.exit(1)
# Locate object files
obj_files = self.find_object_files()
self.logger.info(f" ✓ Found {len(obj_files)} object files")
# Verify stack usage files exist
su_files = []
for obj_file in obj_files:
# Handle both .o and .c.obj extensions
if obj_file.endswith('.o'):
su_file = obj_file[:-2] + '.su'
elif obj_file.endswith('.c.obj'):
su_file = obj_file[:-6] + '.c.su'
else:
continue
if os.path.exists(su_file):
su_files.append(su_file)
if not su_files:
print(f"{Colors.RED}❌ Error: No stack usage files (.su) found.{Colors.END}")
print(f"{Colors.YELLOW}💡 Please compile with -fstack-usage flag to generate .su files.{Colors.END}")
print(f"{Colors.YELLOW}💡 Add '-fstack-usage' to your CMakeLists.txt target_compile_options.{Colors.END}")
sys.exit(1)
self.logger.info(f" ✓ Found {len(su_files)} stack usage files (from {len(obj_files)} object files)")
# Check for RTL dump files for function pointer detection
rtl_files = self._find_rtl_files(obj_files)
if rtl_files:
self.rtl_available = True
self.logger.info(f" ✓ Found {len(rtl_files)} RTL dump files for function pointer detection")
else:
self.logger.info(" ⚠ No RTL dump files found. Function pointer detection disabled.")
self.logger.info(" 💡 Compile with -fdump-rtl-dfinish for enhanced analysis.")
return obj_files
def parse_object_files(self, obj_files: List[str]) -> None:
"""
Parse all object files to extract call graph and stack usage information.
Args:
obj_files: List of object file paths to process
"""
self.logger.info(f"Parsing {len(obj_files)} object files...")
for i, obj_file in enumerate(obj_files, 1):
if i % 50 == 0 or i == len(obj_files):
self.logger.debug(f"Progress: {i}/{len(obj_files)} files processed")
self._parser.parse_disassembly(obj_file)
self._parser.parse_su_file(obj_file)
self._parse_code_sizes(obj_file)
self._parser.parse_static_sizes(obj_file)
if self._data.rtl_available:
self._parse_rtl_files(obj_files)
self.logger.info("Parsing complete")
def _parse_disassembly(self, obj_file: str) -> None:
"""
Parse objdump disassembly output to build function call graph.
Extracts:
- Function definitions and their addresses
- Function call relationships via relocation entries
- Symbol address mappings for resolution
Args:
obj_file: Path to object file to disassemble
Raises:
SystemExit: If disassembly fails
"""
try:
result = subprocess.run([self.objdump_path, "-dr", obj_file],
capture_output=True, text=True, check=True,
timeout=SUBPROCESS_TIMEOUT)
current_function = None
for line in result.stdout.splitlines():
line = line.strip()
# Match function definition: "00000000 <function_name>:"
func_match = re.match(r'^([0-9a-fA-F]+) <(.*)>:', line)
if func_match:
addr, name = func_match.groups()
current_function = f"{name}@{obj_file}"
self.call_graph[current_function] = set()
# Track ambiguous function names across object files
if name in self.global_name:
self.ambiguous.add(name)
self.global_name[name] = current_function
# Store address mapping for symbol resolution
addr = addr.lstrip('0') or '0'
self.addresses[f"{addr}@{obj_file}"] = current_function
# Match function call: ": R_ARM_CALL target"
call_match = re.search(r': R_[A-Za-z0-9_]+_CALL\s+(.+)', line)
if call_match and current_function:
target = call_match.group(1)
# Detect heap allocation function calls
if target in HEAP_ALLOC_FUNCTIONS:
self.has_heap_alloc.add(current_function)
# Handle different call target formats
if target == ".text":
target = f"@{obj_file}"
elif target.startswith(".text+0x"):
offset = target[9:] # Remove ".text+0x" prefix
target = f"{offset}@{obj_file}"
self.call_graph[current_function].add(target)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
print(f"{Colors.RED}❌ Error disassembling {obj_file}: {e}{Colors.END}")
sys.exit(1)
def _parse_su_file(self, obj_file: str) -> None:
"""
Parse stack usage (.su) file to extract function frame sizes.
The .su file format is: "path:line:column:function_name size qualifier"
Frame sizes are adjusted by adding the ARM call overhead cost.
Args:
obj_file: Object file path (corresponding .su file will be located)
"""
# Handle both .o and .c.obj extensions
if obj_file.endswith('.o'):
su_file = obj_file[:-2] + '.su'
elif obj_file.endswith('.c.obj'):
su_file = obj_file[:-6] + '.c.su'
else:
return
try:
with open(su_file, 'r', encoding='utf-8') as f:
for line in f:
# Parse: "/path/file.c:137:20:function_name size qualifier"
match = re.search(r':([^:\t ]+)[\t ]+([0-9]+)[\t ]+', line)
if match:
func_name, size = match.groups()
# Add ARM function call overhead to frame size
self.frame_size[f"{func_name}@{obj_file}"] = int(size) + CALL_COST
except FileNotFoundError:
# Stack usage files may not exist for assembly files or system libraries
pass
def _parse_code_sizes(self, obj_file: str) -> None:
"""
REMOVED: Object file code sizes are meaningless (include dead code).
Use --elf option to get accurate linked code sizes.
"""
# Object file sizes are misleading - skip entirely
# Users must provide --elf for code size statistics
pass
def parse_elf_code_sizes(self, elf_file: str) -> None:
"""Parse ELF file for accurate linked code and static sizes."""
self._parser.parse_elf(elf_file)
def parse_map_file(self, map_file: str) -> None:
"""Parse linker map file for memory region details."""
self._parser.parse_map_file(map_file)
def _parse_static_sizes(self, obj_file: str) -> None:
"""
Parse object file to extract static allocation sizes using nm.
Uses arm-none-eabi-nm to get .data and .bss symbol sizes from object files.
Separates initialized (.data) from uninitialized (.bss) data.
Also detects per-function static variables (named like func.varname).
Args:
obj_file: Object file path to analyze
"""
nm_path = self.objdump_path.replace('objdump', 'nm')
try:
result = subprocess.run(
[nm_path, '--print-size', obj_file],
capture_output=True, text=True, check=True,
timeout=SUBPROCESS_TIMEOUT
)
total_data = 0
total_bss = 0
for line in result.stdout.splitlines():
# Format: "address size type name"
# D/d = initialized data (.data), B/b = uninitialized data (.bss)
parts = line.strip().split()
if len(parts) >= 4:
size_hex, sym_type, sym_name = parts[1], parts[2], parts[3]
try:
size = int(size_hex, 16)
except ValueError:
continue
# Separate .data (D/d) from .bss (B/b)
if sym_type in ('D', 'd'):
total_data += size
var_type = 'data'
elif sym_type in ('B', 'b'):
total_bss += size
var_type = 'bss'
else:
continue
# Detect per-function static variables (pattern: func.varname or func.N)
if '.' in sym_name:
func_name = sym_name.split('.')[0]
var_name = sym_name.split('.', 1)[1]
if func_name not in self.func_static_vars:
self.func_static_vars[func_name] = []
self.func_static_vars[func_name].append((var_name, size, var_type))
if total_data > 0:
self.data_size[obj_file] = total_data
if total_bss > 0:
self.bss_size[obj_file] = total_bss
if total_data + total_bss > 0:
self.static_size[obj_file] = total_data + total_bss
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
pass
except FileNotFoundError:
pass
def detect_thread_entries(self) -> None:
"""
Detect RTOS thread/task entry functions by scanning source files.
Searches for common RTOS task creation patterns (FreeRTOS, Zephyr, etc.)
and extracts the thread entry function names for stack analysis.
"""
self.logger.info("🔍 Detecting RTOS thread entry points...")
# Find all C source files in build directory and parent
source_dirs = [self.build_dir]
if self.build_dir.parent != self.build_dir:
source_dirs.append(self.build_dir.parent)
source_files = []
for src_dir in source_dirs:
source_files.extend(glob.glob(str(src_dir / "**/*.c"), recursive=True))