1+ # Export VU (DVP) overlay data: binary data (.bin) and symbols (.sym)
2+ # @category ghidra-emotionengine
3+ # @runtime PyGhidra
4+
5+ import typing
6+ if typing .TYPE_CHECKING :
7+ from ghidra .ghidra_builtins import *
8+
9+ import os
10+ import re
11+ from collections import defaultdict
12+ from ghidra .program .model .symbol import SourceType
13+ import jpype
14+
15+ # Constants from DvpOverlayTable.java
16+ VU1_TEXT_ADDRESS = 0x11008000
17+ DVP_OVERLAY_PREFIX = ".DVP.overlay.."
18+ VU_INSTRUCTION_SIZE = 8 # VU instructions are 8 bytes (64-bit)
19+
20+ class OverlayData :
21+ """Container for overlay block data grouped by filename hash."""
22+
23+ def __init__ (self , filename_hash ):
24+ self .filename_hash = filename_hash
25+ self .symbols = [] # List of (vu_address, symbol_name) tuples
26+ self .blocks = [] # List of (vaddr, block) tuples for binary export
27+
28+ def add_symbol (self , vu_address , symbol_name ):
29+ """Add a symbol to this overlay."""
30+ self .symbols .append ((vu_address , symbol_name ))
31+
32+ def add_block (self , vaddr , block ):
33+ """Add a memory block to this overlay."""
34+ self .blocks .append ((vaddr , block ))
35+
36+ def get_sorted_symbols (self ):
37+ """Get symbols sorted by address."""
38+ return sorted (self .symbols , key = lambda x : x [0 ])
39+
40+ def get_sorted_blocks (self ):
41+ """Get blocks sorted by vaddr."""
42+ return sorted (self .blocks , key = lambda x : x [0 ])
43+
44+ def parse_overlay_block_name (block_name ):
45+ """
46+ Parse DVP overlay block name to extract components.
47+
48+ Format: .DVP.overlay..<overlay_name_offset>.<filename_hash>.<vaddr>.<line_of_split>.<counter>
49+ Example: .DVP.overlay..0x0000.a1b2c3d4.0x0100.0042.1
50+
51+ Returns:
52+ dict with keys: overlay_name_offset, filename_hash, vaddr, line_of_split, counter
53+ or None if parsing fails
54+ """
55+ if not block_name .startswith (DVP_OVERLAY_PREFIX ):
56+ return None
57+
58+ # Remove prefix and split by dots
59+ remainder = block_name [len (DVP_OVERLAY_PREFIX ):]
60+ parts = remainder .split ('.' )
61+
62+ if len (parts ) < 5 :
63+ return None
64+
65+ try :
66+ return {
67+ 'overlay_name_offset' : parts [0 ], # e.g., "0x0000"
68+ 'filename_hash' : parts [1 ], # e.g., "a1b2c3d4"
69+ 'vaddr' : parts [2 ], # e.g., "0x0100"
70+ 'line_of_split' : parts [3 ], # e.g., "0042"
71+ 'counter' : parts [4 ] # e.g., "1"
72+ }
73+ except (IndexError , ValueError ):
74+ return None
75+
76+ def convert_vu_address (address_offset ):
77+ """
78+ Convert Ghidra address to VU instruction address.
79+
80+ Subtracts VU1_TEXT_ADDRESS base and divides by 8 (instruction word size).
81+
82+ Args:
83+ address_offset: The address offset as a long
84+
85+ Returns:
86+ VU instruction address as integer
87+ """
88+ relative_offset = address_offset - VU1_TEXT_ADDRESS
89+ vu_address = relative_offset // VU_INSTRUCTION_SIZE
90+ return int (vu_address )
91+
92+ def collect_overlay_data (memory , symbol_table ):
93+ """
94+ Collect all overlay data from memory blocks.
95+
96+ Returns:
97+ dict: filename_hash -> OverlayData mapping
98+ """
99+ overlays = defaultdict (lambda : OverlayData (None ))
100+ overlay_blocks_found = 0
101+ total_symbols = 0
102+
103+ # Iterate through all memory blocks
104+ for block in memory .blocks :
105+ block_name = block .name
106+
107+ # Check if this is a DVP overlay block
108+ if not block_name .startswith (DVP_OVERLAY_PREFIX ):
109+ continue
110+
111+ # Parse the block name
112+ parsed = parse_overlay_block_name (block_name )
113+ if parsed is None :
114+ printerr (f"Warning: Could not parse overlay block name: { block_name } " )
115+ continue
116+
117+ filename_hash = parsed ['filename_hash' ]
118+ vaddr = parsed ['vaddr' ]
119+ overlay_blocks_found += 1
120+
121+ println (f"Processing overlay block: { block_name } " )
122+ println (f" Filename hash: { filename_hash } " )
123+
124+ # Initialize overlay data if needed
125+ if overlays [filename_hash ].filename_hash is None :
126+ overlays [filename_hash ].filename_hash = filename_hash
127+
128+ # Parse vaddr for sorting blocks
129+ try :
130+ vaddr_int = int (vaddr , 16 ) if vaddr .startswith ('0x' ) else int (vaddr , 16 )
131+ except ValueError :
132+ vaddr_int = 0
133+
134+ # Add block for binary export
135+ overlays [filename_hash ].add_block (vaddr_int , block )
136+
137+ # Collect symbols from this block
138+ block_start = block .start
139+ block_end = block .end
140+ block_symbol_count = 0
141+
142+ symbol_iter = symbol_table .getSymbolIterator (block_start , True )
143+ for symbol in symbol_iter :
144+ symbol_addr = symbol .address
145+
146+ # Check if symbol is within this block
147+ if symbol_addr .compareTo (block_end ) > 0 :
148+ break
149+ if symbol_addr .compareTo (block_start ) < 0 :
150+ continue
151+
152+ # Skip default symbols and VU temporary symbols
153+ if symbol .source == SourceType .DEFAULT :
154+ continue
155+
156+ symbol_name = symbol .name
157+ if re .match (r'\.?vu\.\d+' , symbol_name ):
158+ continue
159+
160+ # Convert address and add symbol
161+ vu_address = convert_vu_address (symbol_addr .offset )
162+ overlays [filename_hash ].add_symbol (vu_address , symbol_name )
163+ block_symbol_count += 1
164+ total_symbols += 1
165+
166+ println (f" Found { block_symbol_count } symbols" )
167+
168+ return dict (overlays ), overlay_blocks_found , total_symbols
169+
170+ def export_symbol_file (overlay_data , output_path ):
171+ """
172+ Export symbols to a .sym file.
173+
174+ Args:
175+ overlay_data: OverlayData instance
176+ output_path: Directory path for output
177+
178+ Returns:
179+ bool: True if successful
180+ """
181+ filename = f"{ overlay_data .filename_hash } .sym"
182+ filepath = os .path .join (output_path , filename )
183+
184+ try :
185+ symbols = overlay_data .get_sorted_symbols ()
186+ with open (filepath , 'w' , encoding = 'utf-8' ) as f :
187+ for vu_address , symbol_name in symbols :
188+ # Format: 4 hex digits (no 0x prefix), space, symbol name
189+ f .write (f"{ vu_address :04x} { symbol_name } \n " )
190+
191+ println (f"Wrote { len (symbols )} symbols to: { filename } " )
192+ return True
193+
194+ except Exception as e :
195+ printerr (f"Error writing symbol file { filename } : { str (e )} " )
196+ return False
197+
198+ def export_binary_file (overlay_data , output_path ):
199+ """
200+ Export raw binary data to a .bin file.
201+
202+ Args:
203+ overlay_data: OverlayData instance
204+ output_path: Directory path for output
205+
206+ Returns:
207+ bool: True if successful
208+ """
209+ filename = f"{ overlay_data .filename_hash } .bin"
210+ filepath = os .path .join (output_path , filename )
211+
212+ try :
213+ blocks = overlay_data .get_sorted_blocks ()
214+ total_bytes = 0
215+
216+ with open (filepath , 'wb' ) as f :
217+ for vaddr_int , block in blocks :
218+ # Get block size and create byte array
219+ block_size = int (block .size )
220+ byte_array = jpype .JByte [block_size ]
221+
222+ # Read bytes from the block
223+ bytes_read = block .getBytes (block .start , byte_array )
224+
225+ # Convert Java bytes (signed) to Python bytes (unsigned)
226+ python_bytes = bytes ([(b & 0xFF ) for b in byte_array [:bytes_read ]])
227+
228+ # Write to file
229+ f .write (python_bytes )
230+ total_bytes += bytes_read
231+
232+ println (f"Wrote { total_bytes } bytes to: { filename } " )
233+ return True
234+
235+ except Exception as e :
236+ printerr (f"Error writing binary file { filename } : { str (e )} " )
237+ import traceback
238+ printerr (traceback .format_exc ())
239+ return False
240+
241+ def export_dvp_overlays ():
242+ """Main function to export DVP overlay data."""
243+
244+ # Validate that a program is loaded
245+ if currentProgram is None :
246+ printerr ("No program is currently loaded" )
247+ return
248+
249+ println ("Exporting DVP overlay data..." )
250+ println ("-" * 60 )
251+
252+ # Get output directory from user
253+ try :
254+ output_dir = askDirectory ("Select Output Directory for DVP Overlay Files" , "Select" )
255+ except :
256+ println ("No directory selected, export cancelled" )
257+ return
258+
259+ if output_dir is None :
260+ println ("No directory selected, export cancelled" )
261+ return
262+
263+ output_path = output_dir .absolutePath
264+ println (f"Output directory: { output_path } " )
265+ println ("-" * 60 )
266+
267+ # Collect overlay data
268+ memory = currentProgram .memory
269+ symbol_table = currentProgram .symbolTable
270+
271+ overlays , overlay_blocks_found , total_symbols = collect_overlay_data (memory , symbol_table )
272+
273+ println ("-" * 60 )
274+ println (f"Found { overlay_blocks_found } DVP overlay blocks" )
275+ println (f"Total symbols collected: { total_symbols } " )
276+ println (f"Unique overlays (by filename hash): { len (overlays )} " )
277+ println ("-" * 60 )
278+
279+ if overlay_blocks_found == 0 :
280+ println ("No DVP overlay blocks found in program" )
281+ return
282+
283+ # Export files for each overlay
284+ sym_files_written = 0
285+ bin_files_written = 0
286+
287+ for filename_hash , overlay_data in overlays .items ():
288+ # Export symbol file
289+ if export_symbol_file (overlay_data , output_path ):
290+ sym_files_written += 1
291+
292+ # Export binary file
293+ if export_binary_file (overlay_data , output_path ):
294+ bin_files_written += 1
295+
296+ # Print summary
297+ println ("-" * 60 )
298+ println ("Export complete!" )
299+ println (f" Symbol files (.sym): { sym_files_written } " )
300+ println (f" Binary files (.bin): { bin_files_written } " )
301+ println (f" Total symbols exported: { total_symbols } " )
302+ println (f" Output directory: { output_path } " )
303+ println ("-" * 60 )
304+
305+ # Run the export
306+ export_dvp_overlays ()
0 commit comments