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
|
#!/usr/bin/env python3
# Compares used inlines for a TU in the decomp compared to the official debug maps.
# Checks for missing inline usages, fake inline usages, and inlines being the wrong size.
# NOTE: You must mark the TU as "Matching" in configure.py, even if it doesn't match! Otherwise this script won't work properly.
# To use this script, pass the name of the TU as the only argument. Examples:
# ./tools/utilities/debug_map_diff.py d_a_andsw0
# ./tools/utilities/debug_map_diff.py d_a_npc_fa1
from pathlib import Path
import re
import subprocess
import argparse
from collections import defaultdict
arg_parse = argparse.ArgumentParser()
arg_parse.add_argument("object_name", help="Name of the object to compare, e.g. d_a_bridge or d_a_npc_fa1")
args = arg_parse.parse_args()
target_object_name: str = args.object_name
assert not re.search(r"[/\\.]", target_object_name), "The object name should not contain slashes or dots"
debug_maps_root_path = Path("orig") / "D44J01" / "files" / "maps"
decomp_root_path = Path(".")
retcode = subprocess.call(["python", "configure.py", "--version", "D44J01", "--debug", "--map", "--non-matching"], cwd=decomp_root_path)
assert retcode == 0, "Failed to configure"
all_ninja_outputs = []
for ninja_target in subprocess.check_output(["ninja", "-t", "targets", "all"]).decode("utf-8").splitlines():
ninja_output, ninja_rule = ninja_target.split(":", 1)
all_ninja_outputs.append(ninja_output)
target_map_path_dol = debug_maps_root_path / "frameworkD.map"
target_map_path_rel = debug_maps_root_path / f"{target_object_name}D.map"
if target_map_path_rel.exists():
target_is_rel = True
target_map_path = target_map_path_rel
else:
assert target_map_path_dol.exists()
target_is_rel = False
target_map_path = target_map_path_dol
del target_map_path_dol
del target_map_path_rel
base_map_path_dol = decomp_root_path / "build" / "D44J01" / "framework.elf.MAP"
base_map_path_rel = decomp_root_path / "build" / "D44J01" / target_object_name / f"{target_object_name}.plf.MAP"
if base_map_path_rel.as_posix() in all_ninja_outputs:
base_is_rel = True
base_map_path = base_map_path_rel
else:
assert base_map_path_dol.as_posix() in all_ninja_outputs
base_is_rel = False
base_map_path = base_map_path_dol
del base_map_path_dol
del base_map_path_rel
retcode = subprocess.call(["ninja", base_map_path.relative_to(decomp_root_path)], cwd=decomp_root_path)
assert retcode == 0, "Ninja build call failed"
class Symbol:
def __init__(self, name: str, size: int, sym_type: str | None = None, linkage: str | None = None, stripped: bool | None = None, align: int | None = None):
self.name = name
self.size = size
self.sym_type = sym_type
self.linkage = linkage
self.stripped = stripped
self.align = align
def __repr__(self):
return f"Symbol(name={self.name}, size={self.size}, sym_type={self.sym_type}, linkage={self.linkage}, stripped={self.stripped})"
def get_symbols_from_linker_map(map_contents: str, missing_tree_and_stripped=False):
map_lines = map_contents.splitlines()
object_name_to_ext = {}
def remove_object_ext(object_name: str):
object_name, object_ext = object_name.split(".", 1)
if object_name in object_name_to_ext:
# Check to be sure we never have two objects with the same name but different extensions
assert object_name_to_ext[object_name] == object_ext
else:
object_name_to_ext[object_name] = object_ext
return object_name
object_name_to_symbol_name_to_type_and_linkage = defaultdict(dict)
localstatic_counters = defaultdict(lambda: defaultdict(int))
unref_dupe_symbol_names_to_object_name_to_type_linkage = {}
if not missing_tree_and_stripped:
line = map_lines.pop(0)
assert line == "Link map of _prolog" or line == "Link map of __start"
curr_unref_dupe_symbol_name = None
while True:
line = map_lines.pop(0)
if line == "":
# End of link map
break
unref_dupe_symbol_match = re.search(r"^ +(\d+)\] >>> UNREFERENCED DUPLICATE (\S+)$", line)
if unref_dupe_symbol_match:
link_map_depth = int(unref_dupe_symbol_match.group(1))
curr_unref_dupe_symbol_name = unref_dupe_symbol_match.group(2)
assert curr_unref_dupe_symbol_name not in unref_dupe_symbol_names_to_object_name_to_type_linkage
unref_dupe_symbol_names_to_object_name_to_type_linkage[curr_unref_dupe_symbol_name] = {}
continue
unref_dupe_linkage_match = re.search(r"^ +(\d+)\] >>> \((func|object),(weak)\) found in (\S+) $", line)
if unref_dupe_linkage_match:
assert curr_unref_dupe_symbol_name is not None
link_map_depth = int(unref_dupe_linkage_match.group(1))
symbol_type = unref_dupe_linkage_match.group(2)
linkage = unref_dupe_linkage_match.group(3)
object_name = remove_object_ext(unref_dupe_linkage_match.group(4))
# A symbol shouldn't be duplicates multiple times in the same object, but it's possible for multiple objects to have the exact same name.
# e.g. getRaw__Q37JGadget6binary10TParseDataCFv is in 5 different TUs named control.o
# Not really any way to detect this, so just ignore it and check that the types all match.
# assert object_name not in unref_dupe_symbol_names_to_object_name_to_type_linkage[curr_unref_dupe_symbol_name]
if object_name in unref_dupe_symbol_names_to_object_name_to_type_linkage[curr_unref_dupe_symbol_name]:
assert (symbol_type, linkage) == unref_dupe_symbol_names_to_object_name_to_type_linkage[curr_unref_dupe_symbol_name][object_name]
unref_dupe_symbol_names_to_object_name_to_type_linkage[curr_unref_dupe_symbol_name][object_name] = (symbol_type, linkage)
continue
else:
curr_unref_dupe_symbol_name = None
if line.startswith(">>> SYMBOL NOT FOUND: "):
continue
linker_symbol_match = re.search(r"^ +(\d+)\] (\S+) found as linker generated symbol$", line)
if linker_symbol_match:
continue
normal_symbol_match = re.search(r"^ +(\d+)\] (\S+) \((section|func|object|notype),(global|local|weak)\) found in (\S+) $", line)
assert normal_symbol_match is not None, f"Unknown line: {repr(line)}"
link_map_depth = int(normal_symbol_match.group(1))
symbol_name = normal_symbol_match.group(2)
symbol_type = normal_symbol_match.group(3)
linkage = normal_symbol_match.group(4)
object_name = remove_object_ext(normal_symbol_match.group(5))
if localstatic_match := re.search(r"^([^\s\$]+)\$\d+$", symbol_name):
localstatic_name = localstatic_match.group(1)
localstatic_counters[object_name][localstatic_name] += 1
symbol_name = f"{localstatic_name}${localstatic_counters[object_name][localstatic_name]}"
assert symbol_name not in object_name_to_symbol_name_to_type_and_linkage[object_name], f"Duplicate symbol: {repr(symbol_name)}"
object_name_to_symbol_name_to_type_and_linkage[object_name][symbol_name] = (symbol_type, linkage)
symbols: dict[str, dict[str, Symbol]] = defaultdict(dict)
localstatic_counters = defaultdict(lambda: defaultdict(int))
unref_dupe_symbol_names_already_added = set()
for line in map_lines:
symbol_entry_match = re.search(r"^ ([0-9a-f]{8}|UNUSED ) ([0-9a-f]{6}) ([0-9a-f]{8}|\.{8})(?: +(\d+))? (.+?)(?: \(entry of [^)]+\))? \t?(\S+)", line, re.IGNORECASE)
if symbol_entry_match:
symbol_offset = symbol_entry_match.group(1)
symbol_size = symbol_entry_match.group(2)
symbol_size = int(symbol_size, 16)
symbol_address = symbol_entry_match.group(3)
if symbol_offset == "UNUSED ":
assert symbol_address == "........"
if symbol_address == "........":
assert symbol_offset == "UNUSED "
symbol_address = None
stripped = True
else:
symbol_address = int(symbol_address, 16)
stripped = False
symbol_align = symbol_entry_match.group(4)
if symbol_align is not None:
symbol_align = int(symbol_align)
symbol_name = symbol_entry_match.group(5)
object_name = remove_object_ext(symbol_entry_match.group(6))
if symbol_name.startswith(".") or symbol_name in ["extab", "extabindex"]:
# e.g. Section symbol (.text) or pool symbol (...data)
continue
if re.search(r"^@\d+$", symbol_name):
continue
if localstatic_match := re.search(r"^([^\s\$]+)\$\d+$", symbol_name):
localstatic_name = localstatic_match.group(1)
localstatic_counters[object_name][localstatic_name] += 1
symbol_name = f"{localstatic_name}${localstatic_counters[object_name][localstatic_name]}"
if symbol_name in object_name_to_symbol_name_to_type_and_linkage[object_name]:
symbol_type, linkage = object_name_to_symbol_name_to_type_and_linkage[object_name][symbol_name]
else:
assert stripped or missing_tree_and_stripped, f"Symbol {repr(symbol_name)} is missing linkage information in object {repr(object_name)}"
symbol_type = linkage = None
symbols[object_name][symbol_name] = Symbol(symbol_name, symbol_size, sym_type=symbol_type, linkage=linkage, stripped=stripped, align=symbol_align)
if symbol_name in unref_dupe_symbol_names_to_object_name_to_type_linkage and symbol_name not in unref_dupe_symbol_names_already_added:
for other_object_name in unref_dupe_symbol_names_to_object_name_to_type_linkage[symbol_name]:
symbol_type, linkage = unref_dupe_symbol_names_to_object_name_to_type_linkage[symbol_name][other_object_name]
assert stripped == False, "Shouldn't reach a stripped duplicate here"
symbols[other_object_name][symbol_name] = Symbol(symbol_name, symbol_size, sym_type=symbol_type, linkage=linkage, stripped=stripped, align=symbol_align)
unref_dupe_symbol_names_already_added.add(symbol_name)
return symbols
def is_debug_only_symbol(symbol_name: str):
if "__11JORMContextF" in symbol_name:
return True
return False
target_missing_tree_and_stripped = not target_is_rel
all_target_symbols = get_symbols_from_linker_map(target_map_path.read_text(), missing_tree_and_stripped=target_missing_tree_and_stripped)
target_symbols = all_target_symbols[target_object_name]
if len(target_symbols) == 0:
raise Exception("Failed to find object matching the given name (check for typos)")
all_base_symbols = get_symbols_from_linker_map(base_map_path.read_text())
base_symbols = all_base_symbols[target_object_name]
if len(base_symbols) == 0:
raise Exception("Failed to find object matching the given name (check for typos)")
print(len(target_symbols), len(base_symbols))
target_symbol_names_in_previous_objects = set()
if target_missing_tree_and_stripped:
# This handles the logic for checking if a symbol already appeared earlier on in framework.map.
# Note: I'm not sure if the logic here is 100% accurate for edge cases since it just relies on dict insertion order.
# Might need to add more robust logic later that keeps track of symbol section, address, or line number within the map file...?
for object_name, symbols in all_target_symbols.items():
if object_name == target_object_name:
break
for symbol_name, symbol in symbols.items():
target_symbol_names_in_previous_objects.add(symbol.name)
symbol_size_diffs = []
total_missing = 0
total_fake = 0
total_maybe_fake = 0
total_right_size = 0
total_wrong_size = 0
total_wrong_linkage = 0
total_wrong_align = 0
for symbol_name, target_symbol in target_symbols.items():
if target_symbol.size == 0:
continue
if symbol_name not in base_symbols:
base_size = 0
else:
base_size = base_symbols[symbol_name].size
size_diff = abs(target_symbol.size - base_size)
ratio = size_diff / target_symbol.size
if symbol_name in base_symbols and size_diff != 0:
total_wrong_size += 1
symbol_size_diffs.append((symbol_name, target_symbol.size, base_size, ratio))
symbol_size_diffs.sort(key=lambda x: x[-1])
for symbol_name, target_size, base_size, ratio in symbol_size_diffs:
prefix = ""
size_diff = abs(target_size - base_size)
if base_size == 0:
# Print missing inlines last so they're obvious
continue
elif size_diff == 0:
prefix = "GOOD: "
total_right_size += 1
elif ratio < 0.05 and size_diff <= 0x10:
prefix = "CLOSE: "
else:
prefix = "WRONG: "
print(prefix + symbol_name, "0x%X" % target_size, "0x%X" % base_size, ratio)
for symbol_name, target_symbol in target_symbols.items():
if target_symbol.size == 0:
continue
if symbol_name not in base_symbols:
continue
base_symbol = base_symbols[symbol_name]
wrong_linkage = False
if target_symbol.linkage is None and base_symbol.sym_type == "object":
# The official framework.map for main.dol doesn't include linkage, but we can guess it based off of certain symbol name prefixes.
if base_symbol.name.startswith("l_") and base_symbol.linkage != "local":
wrong_linkage = True
target_linkage = "local"
elif base_symbol.name.startswith("g_") and base_symbol.linkage != "global":
wrong_linkage = True
target_linkage = "global"
elif target_symbol.linkage is not None and target_symbol.linkage != base_symbol.linkage:
wrong_linkage = True
target_linkage = target_symbol.linkage
if wrong_linkage:
total_wrong_linkage += 1
print(f"LINKAGE: {symbol_name} (should be {target_linkage}, is {base_symbol.linkage})")
for symbol_name, target_symbol in target_symbols.items():
if target_symbol.align is None:
continue
if symbol_name not in base_symbols:
continue
base_symbol = base_symbols[symbol_name]
if target_symbol.align != base_symbol.align:
total_wrong_align += 1
print(f"ALIGN: {symbol_name} (should be {target_symbol.align}, is {base_symbol.align})")
maybe_fake_symbols = []
fake_symbols = []
for symbol_name, base_symbol in base_symbols.items():
if symbol_name in target_symbols:
continue
if target_missing_tree_and_stripped and (base_symbol.stripped or symbol_name in target_symbol_names_in_previous_objects):
maybe_fake_symbols.append(base_symbol)
else:
fake_symbols.append(base_symbol)
for base_symbol in maybe_fake_symbols:
print("FAKE?:", base_symbol.name, "0x%X" % base_symbol.size)
total_maybe_fake += 1
for base_symbol in fake_symbols:
print("FAKE:", base_symbol.name, "0x%X" % base_symbol.size)
total_fake += 1
for symbol_name, target_size, base_size, ratio in symbol_size_diffs:
prefix = ""
if base_size == 0:
if is_debug_only_symbol(symbol_name):
continue
prefix = "MISSING: "
total_missing += 1
print(prefix + symbol_name, "0x%X" % target_size)
print("==================================================")
print(f"Total right size: {total_right_size}")
print(f"Total wrong size: {total_wrong_size}")
print(f"Total wrong linkage: {total_wrong_linkage}")
print(f"Total wrong alignment: {total_wrong_align}")
print(f"Total maybe fake: {total_maybe_fake}")
print(f"Total fake: {total_fake}")
print(f"Total missing: {total_missing}")
|