Source code for libdebug.utils.elf_utils
#
# This file is part of libdebug Python library (https://github.com/libdebug/libdebug).
# Copyright (c) 2023-2024 Gabriele Digregorio, Roberto Alessandro Bertolini. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for details.
#
import functools
from pathlib import Path
import requests
from elftools.elf.elffile import ELFFile
from libdebug.cffi.debug_sym_cffi import ffi
from libdebug.cffi.debug_sym_cffi import lib as lib_sym
from libdebug.liblog import liblog
from libdebug.utils.libcontext import libcontext
DEBUGINFOD_PATH: Path = Path.home() / ".cache" / "debuginfod_client"
LOCAL_DEBUG_PATH: Path = Path("/usr/lib/debug/.build-id/")
URL_BASE: str = "https://debuginfod.elfutils.org/buildid/{}/debuginfo"
def _download_debuginfod(buildid: str, debuginfod_path: Path) -> None:
"""Downloads the debuginfo file corresponding to the specified buildid.
Args:
buildid (str): The buildid of the debuginfo file.
debuginfod_path (Path): The output directory.
"""
try:
url = URL_BASE.format(buildid)
r = requests.get(url, allow_redirects=True, timeout=1)
if r.ok:
debuginfod_path.parent.mkdir(parents=True, exist_ok=True)
with debuginfod_path.open("wb") as f:
f.write(r.content)
except Exception as e:
liblog.debugger(f"Exception {e} occurred while downloading debuginfod symbols")
@functools.cache
def _debuginfod(buildid: str) -> Path:
"""Returns the path to the debuginfo file corresponding to the specified buildid.
Args:
buildid (str): The buildid of the debuginfo file.
Returns:
debuginfod_path (Path): The path to the debuginfo file corresponding to the specified buildid.
"""
debuginfod_path = Path.home() / ".cache" / "debuginfod_client" / buildid / "debuginfo"
if not debuginfod_path.exists():
_download_debuginfod(buildid, debuginfod_path)
return debuginfod_path
@functools.cache
def _collect_external_info(path: str) -> dict[str, tuple[int, int]]:
"""Returns a dictionary containing the symbols taken from the external debuginfo file.
Args:
path (str): The path to the ELF file.
Returns:
symbols (dict): A dictionary containing the symbols of the specified external debuginfo file.
"""
symbols = {}
c_file_path = ffi.new("char[]", path.encode("utf-8"))
head = lib_sym.collect_external_symbols(c_file_path, libcontext.sym_lvl)
if head != ffi.NULL:
cursor = head
while cursor != ffi.NULL:
symbol_name = ffi.string(cursor.name).decode("utf-8")
symbols[symbol_name] = (cursor.low_pc, cursor.high_pc)
cursor = cursor.next
lib_sym.free_symbol_info(head)
return symbols
@functools.cache
def _parse_elf_file(path: str, debug_info_level: int) -> tuple[dict[str, tuple[int, int]], str | None, str | None]:
"""Returns a dictionary containing the symbols of the specified ELF file and the buildid.
Args:
path (str): The path to the ELF file.
debug_info_level (int): The debug info level.
Returns:
symbols (dict): A dictionary containing the symbols of the specified ELF file.
buildid (str): The buildid of the specified ELF file.
debug_file_path (str): The path to the external debuginfo file corresponding.
"""
symbols = {}
buildid = None
debug_file_path = None
c_file_path = ffi.new("char[]", path.encode("utf-8"))
head = lib_sym.read_elf_info(c_file_path, debug_info_level)
if head != ffi.NULL:
cursor = head
while cursor != ffi.NULL:
symbol_name = ffi.string(cursor.name).decode("utf-8")
symbols[symbol_name] = (cursor.low_pc, cursor.high_pc)
cursor = cursor.next
lib_sym.free_symbol_info(head)
if debug_info_level > 2:
buildid = lib_sym.get_build_id()
buildid = ffi.string(buildid).decode("utf-8") if buildid != ffi.NULL else None
debug_file_path = lib_sym.get_debug_file()
debug_file_path = ffi.string(debug_file_path).decode("utf-8") if debug_file_path != ffi.NULL else None
return symbols, buildid, debug_file_path
[docs]
@functools.cache
def resolve_symbol(path: str, symbol: str) -> int:
"""Returns the address of the specified symbol in the specified ELF file.
Args:
path (str): The path to the ELF file.
symbol (str): The symbol whose address should be returned.
Returns:
int: The address of the specified symbol in the specified ELF file.
"""
if libcontext.sym_lvl == 0:
raise Exception(
"Symbol resolution is disabled. Please enable it by setting the sym_lvl libcontext parameter to a value greater than 0.",
)
# Retrieve the symbols from the SymbolTableSection
symbols, buildid, debug_file = _parse_elf_file(path, libcontext.sym_lvl)
if symbol in symbols:
return symbols[symbol][0]
# Retrieve the symbols from the external debuginfo file
if buildid and debug_file and libcontext.sym_lvl > 2:
folder = buildid[:2]
absolute_debug_path_str = str((LOCAL_DEBUG_PATH / folder / debug_file).resolve())
symbols = _collect_external_info(absolute_debug_path_str)
if symbol in symbols:
return symbols[symbol][0]
# Retrieve the symbols from debuginfod
if buildid and libcontext.sym_lvl > 4:
absolute_debug_path = _debuginfod(buildid)
if absolute_debug_path.exists():
symbols = _collect_external_info(str(absolute_debug_path))
if symbol in symbols:
return symbols[symbol][0]
# Symbol not found
raise ValueError(f"Symbol {symbol} not found in {path}. Please specify a valid symbol.")
[docs]
@functools.cache
def resolve_address(path: str, address: int) -> str:
"""Returns the symbol corresponding to the specified address in the specified ELF file.
Args:
path (str): The path to the ELF file.
address (int): The address whose symbol should be returned.
Returns:
str: The symbol corresponding to the specified address in the specified ELF file.
"""
if libcontext.sym_lvl == 0:
return hex(address)
# Retrieve the symbols from the SymbolTableSection
symbols, buildid, debug_file = _parse_elf_file(path, libcontext.sym_lvl)
for symbol, (symbol_start, symbol_end) in symbols.items():
if symbol_start <= address < symbol_end:
return f"{symbol}+{address-symbol_start:x}"
# Retrieve the symbols from the external debuginfo file
if buildid and debug_file and libcontext.sym_lvl > 2:
folder = buildid[:2]
absolute_debug_path_str = str((LOCAL_DEBUG_PATH / folder / debug_file).resolve())
symbols = _collect_external_info(absolute_debug_path_str)
for symbol, (symbol_start, symbol_end) in symbols.items():
if symbol_start <= address < symbol_end:
return f"{symbol}+{address-symbol_start:x}"
# Retrieve the symbols from debuginfod
if buildid and libcontext.sym_lvl > 4:
absolute_debug_path = _debuginfod(buildid)
if absolute_debug_path.exists():
symbols = _collect_external_info(str(absolute_debug_path))
for symbol, (symbol_start, symbol_end) in symbols.items():
if symbol_start <= address < symbol_end:
return f"{symbol}+{address-symbol_start:x}"
# Address not found
raise ValueError(f"Address {hex(address)} not found in {path}. Please specify a valid address.")
[docs]
@functools.cache
def is_pie(path: str) -> bool:
"""Returns True if the specified ELF file is position independent, False otherwise.
Args:
path (str): The path to the ELF file.
Returns:
bool: True if the specified ELF file is position independent, False otherwise.
"""
with Path(path).open("rb") as elf_file:
elf = ELFFile(elf_file)
return elf.header.e_type == "ET_DYN"
[docs]
@functools.cache
def get_entry_point(path: str) -> int:
"""Returns the entry point of the specified ELF file.
Args:
path (str): The path to the ELF file.
Returns:
int: The entry point of the specified ELF file.
"""
with Path(path).open("rb") as elf_file:
elf = ELFFile(elf_file)
return elf.header.e_entry