diff --git a/tools/global_bss_check.py b/tools/global_bss_check.py new file mode 100644 index 00000000..30e18b9d --- /dev/null +++ b/tools/global_bss_check.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +import colorama +colorama.init() + +import argparse +import os +import re +import collections + +regex_fileDataEntry = re.compile(r"^\s+(?P
[^\s]+)\s+(?P0x[^\s]+)\s+(?P0x[^\s]+)\s+(?P[^\s]+)$") +regex_bssEntry = re.compile(r"^\s+(?P0x[^\s]+)\s+(?P[^\s]+)$") +regex_label = re.compile(r"^(?PL[0-9A-F]{8})$") + +VarInfo = collections.namedtuple("VarInfo", ["file", "vram"]) + +def parseMapFile(mapPath: str): + with open(mapPath) as f: + mapData = f.read() + startIndex = mapData.find(".makerom") + mapData = mapData[startIndex:] + # print(len(mapData)) + + symbolsDict = collections.OrderedDict() + + inFile = False + currentFile = "" + + mapLines = mapData.split("\n") + for line in mapLines: + if inFile: + if line.startswith(" "): + entryMatch = regex_bssEntry.search(line) + + # Find variable + if entryMatch is not None: + varName = entryMatch["name"] + varVram = int(entryMatch["vram"], 16) + + # Filter out jump table labels + labelMatch = regex_label.search(varName) + if labelMatch is None: + symbolsDict[varName] = VarInfo( currentFile, varVram ) + # print( symbolsDict[varName] ) + + else: + inFile = False + else: + if line.startswith(" .bss "): + inFile = False + entryMatch = regex_fileDataEntry.search(line) + + # Find file + if entryMatch is not None: + name = "/".join(entryMatch["name"].split("/")[1:]) + + # mapfile only contains .o files, so just strip the last character to replace it + # we assume all the .c files are in the src folder, and all others are .s (true for OoT/MM) + if name.split("/")[0] == "src": + name = name[:-1] + "c" + else: + name = name[:-1] + "s" + + size = int(entryMatch["size"], 16) + vram = int(entryMatch["vram"], 16) + + if size > 0: + inFile = True + currentFile = name + + return symbolsDict + + +Compared = collections.namedtuple("Compared", [ "buildAddress", "buildFile", "expectedAddress", "expectedFile", "diff"]) + +def compareMapFiles(mapFileBuild: str, mapFileExpected: str): + badFiles = set() + missingFiles = set() + + print("Build mapfile: " + mapFileBuild, file=os.sys.stderr) + print("Expected mapfile: " + mapFileExpected, file=os.sys.stderr) + print("", file=os.sys.stderr) + + if not os.path.exists(mapFileBuild): + print(f"{colorama.Fore.LIGHTRED_EX}error{colorama.Fore.RESET}: mapfile not found at {mapFileBuild}. Did you enter the correct path?", file=os.sys.stderr) + exit(1) + + if not os.path.exists(mapFileExpected): + print(f"{colorama.Fore.LIGHTRED_EX}error{colorama.Fore.RESET}: expected mapfile not found at {mapFileExpected}. Is 'expected' missing or in a different folder?", file=os.sys.stderr) + exit(1) + + buildMap = parseMapFile(mapFileBuild) + expectedMap = parseMapFile(mapFileExpected) + + comparedDict = collections.OrderedDict() + + for symbol in buildMap: + if symbol in expectedMap: + comparedDict[symbol] = Compared( buildMap[symbol].vram, buildMap[symbol].file, expectedMap[symbol].vram, expectedMap[symbol].file, buildMap[symbol].vram - expectedMap[symbol].vram ) + if comparedDict[symbol].diff != 0: + badFiles.add(buildMap[symbol].file) + + else: + missingFiles.add(buildMap[symbol].file) + comparedDict[symbol] = Compared( buildMap[symbol].vram, buildMap[symbol].file, -1, "", "Unknown" ) + + for symbol in expectedMap: + if not symbol in buildMap: + missingFiles.add(expectedMap[symbol].file) + comparedDict[symbol] = Compared( -1, "", expectedMap[symbol].vram, expectedMap[symbol].file, "Unknown" ) + + return badFiles, missingFiles, comparedDict + + +def printCsv(badFiles, missingFiles, comparedDict, printAll = True): + print("Symbol Name,Build Address,Build File,Expected Address,Expected File,Difference,GOOD/BAD/MISSING") + + # If it's bad or missing, don't need to do anything special. + # If it's good, check for if it's in a file with bad or missing stuff, and check if print all is on. If none of these, print it. + + for symbol in comparedDict: + symbolInfo = comparedDict[symbol] + symbolGood = colorama.Fore.RED + "BAD" + colorama.Fore.RESET + if type(symbolInfo.diff) != int: + symbolGood = colorama.Fore.YELLOW + "MISSING" + colorama.Fore.RESET + print(f"{symbol},{symbolInfo.buildAddress:X},{symbolInfo.buildFile},{symbolInfo.expectedAddress:X},{symbolInfo.expectedFile},{symbolInfo.diff},{symbolGood}") + continue + + if symbolInfo.diff == 0: + symbolGood = colorama.Fore.GREEN + "GOOD" + colorama.Fore.RESET + if (not symbolInfo.buildFile in badFiles and not symbolInfo.expectedFile in badFiles) and (not symbolInfo.buildFile in badFiles and not symbolInfo.expectedFile in badFiles) and not printAll: + continue + + if symbolInfo.buildFile != symbolInfo.expectedFile: + symbolGood += colorama.Fore.CYAN + " MOVED" + colorama.Fore.RESET + print(f"{symbol},{symbolInfo.buildAddress:X},{symbolInfo.buildFile},{symbolInfo.expectedAddress:X},{symbolInfo.expectedFile},{symbolInfo.diff:X},{symbolGood}") + + +def main(): + description = "Check that globally visible bss has not been reordered." + epilog = """\ + N.B. Since this script reads the map files, it can only see globally visible bss; in-function static bss must be examined with other tools. + """ + + parser = argparse.ArgumentParser(description=description, epilog=epilog, formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument("mapFile", help="Path to a map file.") + parser.add_argument("mapFileExpected", help="Path to the expected map file. Optional, default is 'expected/mapFile'.", nargs="?", default="") + parser.add_argument("-a", "--print-all", help="Print all bss, not just non-matching.", action="store_true") + parser.add_argument("-n", "--no-fun-allowed", help="Remove amusing messages.", action="store_true") + args = parser.parse_args() + + if args.mapFileExpected == "": + args.mapFileExpected = os.path.join("expected", args.mapFile) + + badFiles, missingFiles, comparedDict = compareMapFiles(args.mapFile, args.mapFileExpected) + printCsv(badFiles, missingFiles, comparedDict, args.print_all) + + if len(badFiles) + len(missingFiles) != 0: + print("", file=os.sys.stderr) + + if len(badFiles) != 0: + print(colorama.Fore.RED + " BAD" + colorama.Style.RESET_ALL) + + for file in badFiles: + print(f"bss reordering in {file}", file=os.sys.stderr) + print("", file=os.sys.stderr) + + if not args.no_fun_allowed: + print(colorama.Fore.LIGHTWHITE_EX + + " BSS is REORDERED!!\n" + " Oh! MY GOD!!" + + colorama.Style.RESET_ALL, file=os.sys.stderr) + print("", file=os.sys.stderr) + + if len(missingFiles) != 0: + print(colorama.Fore.YELLOW + " MISSING" + colorama.Style.RESET_ALL) + + for file in missingFiles: + print(f"Symbols missing from {file}", file=os.sys.stderr) + print("", file=os.sys.stderr) + + if not args.no_fun_allowed: + print(colorama.Fore.LIGHTWHITE_EX + " Error, should (not) be in here " + colorama.Style.RESET_ALL, file=os.sys.stderr) + print("", file=os.sys.stderr) + + print("Some files appear to be missing symbols. Have they been renamed or declared as static? You may need to remake 'expected'", file=os.sys.stderr) + + return 1 + + print("", file=os.sys.stderr) + print(colorama.Fore.GREEN + " GOOD" + colorama.Style.RESET_ALL, file=os.sys.stderr) + + if args.no_fun_allowed: + return 0 + + print("\n" + colorama.Fore.LIGHTWHITE_EX + + colorama.Back.RED + " " + colorama.Back.RESET + "\n" + + colorama.Back.RED + " CONGRATURATIONS! " + colorama.Back.RESET + "\n" + + colorama.Back.RED + " All Global BSS is correct. " + colorama.Back.RESET + "\n" + + colorama.Back.RED + " THANK YOU! " + colorama.Back.RESET + "\n" + + colorama.Back.RED + " You are great decomper! " + colorama.Back.RESET + "\n" + + colorama.Back.RED + " " + colorama.Style.RESET_ALL , file=os.sys.stderr) + + return 0 + +if __name__ == "__main__": + ret = main() + exit(ret)