1
0
Fork 0
mirror of https://github.com/zeldaret/oot.git synced 2025-02-11 16:09:09 +00:00
oot/sym_info.py
cadmic be1f9e75f0
Read static symbols from .mdebug in sym_info.py (#2460)
* Patch .mdebug for data_with_rodata objects

* Read static symbols from .mdebug in sym_info.py

* Add ability to print all symbols

* Add license

* Fix bug when missing .mdebug section

* /patch_data_with_rodata_mdebug.py license + nitpicks
2025-02-09 18:16:18 -05:00

335 lines
9.3 KiB
Python
Executable file

#!/usr/bin/env python3
# SPDX-FileCopyrightText: © 2025 ZeldaRET
# SPDX-License-Identifier: CC0-1.0
import argparse
import bisect
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
import struct
import sys
import elftools.elf.elffile
import mapfile_parser
@dataclass
class MdebugSymbolicHeader:
magic: int
vstamp: int
ilineMax: int
cbLine: int
cbLineOffset: int
idnMax: int
cbDnOffset: int
ipdMax: int
cbPdOffset: int
isymMax: int
cbSymOffset: int
ioptMax: int
cbOptOffset: int
iauxMax: int
cbAuxOffset: int
issMax: int
cbSsOffset: int
issExtMax: int
cbSsExtOffset: int
ifdMax: int
cbFdOffset: int
crfd: int
cbRfdOffset: int
iextMax: int
cbExtOffset: int
@dataclass
class MdebugFileDescriptor:
adr: int
rss: int
issBase: int
cbSs: int
isymBase: int
csym: int
ilineBase: int
cline: int
ioptBase: int
copt: int
ipdFirst: int
cpd: int
iauxBase: int
caux: int
rfdBase: int
crfd: int
bitfield: int
cbLineOffset: int
cbLine: int
@dataclass
class MdebugSymbol:
iss: int
value: int
st: int
sc: int
index: int
@dataclass
class LocalSymbol:
name: str
address: int
def read_mdebug_symbolic_header(f, offset: int) -> MdebugSymbolicHeader:
f.seek(offset)
data = f.read(96)
return MdebugSymbolicHeader(*struct.unpack(">2H23I", data))
def read_mdebug_file_descriptor(f, offset: int) -> MdebugFileDescriptor:
f.seek(offset)
data = f.read(72)
return MdebugFileDescriptor(*struct.unpack(">I2iI6iHh4iI2I", data))
def read_mdebug_symbol(f, offset: int) -> MdebugSymbol:
f.seek(offset)
data = f.read(12)
word0, word1, word2 = struct.unpack(">III", data)
return MdebugSymbol(
word0, word1, (word2 >> 26) & 0x3F, (word2 >> 21) & 0x1F, word2 & 0xFFFFF
)
def read_mdebug_string(f, offset: int) -> str:
f.seek(offset)
data = bytearray()
while True:
char = f.read(1)[0]
if char == 0:
break
data.append(char)
return data.decode("ascii")
def read_local_symbols_from_mdebug(elf_path: Path) -> list[LocalSymbol]:
local_symbols = []
with open(elf_path, "r+b") as f:
elf = elftools.elf.elffile.ELFFile(f)
mdebug_offset = 0
for section in elf.iter_sections():
if section.name == ".mdebug":
mdebug_offset = section["sh_offset"]
break
if mdebug_offset == 0:
print(f"No .mdebug section found in '{elf_path}'")
return []
symbolic_header = read_mdebug_symbolic_header(f, mdebug_offset)
for fd_num in range(symbolic_header.ifdMax):
fd = read_mdebug_file_descriptor(
f, symbolic_header.cbFdOffset + fd_num * 72
)
for sym_num in range(fd.isymBase, fd.isymBase + fd.csym):
sym = read_mdebug_symbol(f, symbolic_header.cbSymOffset + sym_num * 12)
if sym.st == 2: # stStatic
if not (
sym.sc == 2 or sym.sc == 3 or sym.sc == 15
): # scData, scBss, scRData
continue
sym_name = read_mdebug_string(
f, symbolic_header.cbSsOffset + fd.issBase + sym.iss
)
# EGCS mangles names of internal variables, and seemingly ":V" is for in-function static variables
if "." in sym_name:
continue
if ":" in sym_name:
sym_name, rest = sym_name.split(":", 1)
if not rest.startswith("V"):
continue
local_symbols.append(LocalSymbol(sym_name, sym.value))
elif sym.st == 14: # stStaticProc
sym_name = read_mdebug_string(
f, symbolic_header.cbSsOffset + fd.issBase + sym.iss
)
local_symbols.append(LocalSymbol(sym_name, sym.value))
return local_symbols
def merge_local_symbols(
map_file: mapfile_parser.mapfile.MapFile, local_symbols: list[LocalSymbol]
):
local_symbols.sort(key=lambda s: s.address)
for segment in map_file:
for file in segment:
# TODO: handle segmented addresses?
if file.vram < 0x80000000:
continue
start_address = file.vram
end_address = file.vram + file.size
start_index = bisect.bisect_left(
local_symbols, start_address, key=lambda s: s.address
)
end_index = bisect.bisect_left(
local_symbols, end_address, key=lambda s: s.address
)
if start_index == end_index:
continue
symbols = file.copySymbolList()
for sym in local_symbols[start_index:end_index]:
if file.vrom is None:
vrom = None
else:
vrom = sym.address - start_address + file.vrom
symbols.append(
mapfile_parser.mapfile.Symbol(
sym.name, sym.address, None, vrom, None
)
)
symbols.sort(key=lambda s: s.vram)
# Recompute symbol sizes
for i in range(len(symbols)):
if i == len(symbols) - 1:
symbols[i].size = end_address - symbols[i].vram
else:
symbols[i].size = symbols[i + 1].vram - symbols[i].vram
file.setSymbolList(symbols)
def find_symbols_by_name(
map_file: mapfile_parser.mapfile.MapFile, sym_name: str
) -> list[mapfile_parser.mapfile.FoundSymbolInfo]:
infos = []
for segment in map_file:
for file in segment:
for sym in file:
if sym.name == sym_name:
infos.append(mapfile_parser.mapfile.FoundSymbolInfo(file, sym))
return infos
def print_map_file(map_file: mapfile_parser.mapfile.MapFile):
for segment in map_file:
print(f"{segment.name}")
for file in segment:
# Ignore debug sections
if (
file.sectionType in (".pdr", ".line", ".gnu.attributes")
or file.sectionType.startswith(".debug")
or file.sectionType.startswith(".mdebug")
):
continue
print(f" {file.asStr()}")
for sym in file:
vram_str = f"{sym.vram:08X}"
if sym.vrom is None:
vrom_str = " "
else:
vrom_str = f"{sym.vrom:06X}"
print(f" {vram_str} {vrom_str} {sym.name}")
def sym_info_main():
parser = argparse.ArgumentParser(
description="Display various information about symbol or addresses."
)
parser.add_argument(
"symname",
nargs="?",
help="symbol name or VROM/VRAM address to lookup. If not given, all symbols will be printed.",
)
parser.add_argument(
"-e",
"--expected",
dest="use_expected",
action="store_true",
help="use the map file and elf in expected/build/ instead of build/",
)
parser.add_argument(
"-v",
"--version",
dest="oot_version",
help="which version should be processed (default: gc-eu-mq-dbg)",
default="gc-eu-mq-dbg",
)
args = parser.parse_args()
BUILTMAP = Path("build") / args.oot_version / f"oot-{args.oot_version}.map"
BUILTELF = Path("build") / args.oot_version / f"oot-{args.oot_version}.elf"
map_path = BUILTMAP
elf_path = BUILTELF
if args.use_expected:
map_path = "expected" / BUILTMAP
elf_path = "expected" / BUILTELF
if not map_path.exists():
print(f"Could not find map_file at '{map_path}'")
sys.exit(1)
map_file = mapfile_parser.mapfile.MapFile()
map_file.readMapFile(map_path)
if elf_path.exists():
local_symbols = read_local_symbols_from_mdebug(elf_path)
merge_local_symbols(map_file, local_symbols)
else:
print(
f"Could not find ELF file at '{elf_path}', local symbols will not be available"
)
sym_name = args.symname
if sym_name is None:
print_map_file(map_file)
sys.exit(0)
infos: list[mapfile_parser.mapfile.FoundSymbolInfo] = []
possible_files: list[mapfile_parser.mapfile.File] = []
try:
address = int(sym_name, 0)
if address >= 0x01000000:
info, possible_files = map_file.findSymbolByVram(address)
if info is not None:
infos = [info]
else:
info, possible_files = map_file.findSymbolByVrom(address)
if info is not None:
infos = [info]
except ValueError:
infos = find_symbols_by_name(map_file, sym_name)
if not infos:
print(f"'{sym_name}' not found in map file '{map_path}'")
if len(possible_files) > 0:
print("But it may be a local symbol of either of the following files:")
for f in possible_files:
print(f" {f.asStr()})")
sys.exit(1)
for info in infos:
print(info.getAsStrPlusOffset(sym_name))
if __name__ == "__main__":
sym_info_main()