1#!/usr/bin/env python3 2# SPDX-License-Identifier: BSD-3-Clause 3# Copyright(c) 2016 Neil Horman <nhorman@tuxdriver.com> 4# Copyright(c) 2022 Robin Jarry 5# pylint: disable=invalid-name 6 7r""" 8Utility to dump PMD_INFO_STRING support from DPDK binaries. 9 10This script prints JSON output to be interpreted by other tools. Here are some 11examples with jq: 12 13Get the complete info for a given driver: 14 15 %(prog)s dpdk-testpmd | \ 16 jq '.[] | select(.name == "cnxk_nix_inl")' 17 18Get only the required kernel modules for a given driver: 19 20 %(prog)s dpdk-testpmd | \ 21 jq '.[] | select(.name == "net_i40e").kmod' 22 23Get only the required kernel modules for a given device: 24 25 %(prog)s dpdk-testpmd | \ 26 jq '.[] | select(.pci_ids[]? | .vendor == "15b3" and .device == "1013").kmod' 27""" 28 29import argparse 30import json 31import logging 32import os 33import re 34import string 35import subprocess 36import sys 37from pathlib import Path 38from typing import Iterable, Iterator, List, Union 39 40import elftools 41from elftools.elf.elffile import ELFError, ELFFile 42 43 44# ---------------------------------------------------------------------------- 45def main() -> int: # pylint: disable=missing-docstring 46 try: 47 args = parse_args() 48 logging.basicConfig( 49 stream=sys.stderr, 50 format="%(levelname)s: %(message)s", 51 level={ 52 0: logging.ERROR, 53 1: logging.WARNING, 54 }.get(args.verbose, logging.DEBUG), 55 ) 56 info = parse_pmdinfo(args.elf_files, args.search_plugins) 57 print(json.dumps(info, indent=2)) 58 except BrokenPipeError: 59 pass 60 except KeyboardInterrupt: 61 return 1 62 except Exception as e: # pylint: disable=broad-except 63 logging.error("%s", e) 64 return 1 65 return 0 66 67 68# ---------------------------------------------------------------------------- 69def parse_args() -> argparse.Namespace: 70 """ 71 Parse command line arguments. 72 """ 73 parser = argparse.ArgumentParser( 74 description=__doc__, 75 formatter_class=argparse.RawDescriptionHelpFormatter, 76 ) 77 parser.add_argument( 78 "-p", 79 "--search-plugins", 80 action="store_true", 81 help=""" 82 In addition of ELF_FILEs and their linked dynamic libraries, also scan 83 the DPDK plugins path. 84 """, 85 ) 86 parser.add_argument( 87 "-v", 88 "--verbose", 89 action="count", 90 default=0, 91 help=""" 92 Display warnings due to linked libraries not found or ELF/JSON parsing 93 errors in these libraries. Use twice to show debug messages. 94 """, 95 ) 96 parser.add_argument( 97 "elf_files", 98 metavar="ELF_FILE", 99 nargs="+", 100 type=existing_file, 101 help=""" 102 DPDK application binary or dynamic library. 103 """, 104 ) 105 return parser.parse_args() 106 107 108# ---------------------------------------------------------------------------- 109def parse_pmdinfo(paths: Iterable[Path], search_plugins: bool) -> List[dict]: 110 """ 111 Extract DPDK PMD info JSON strings from an ELF file. 112 113 :returns: 114 A list of DPDK drivers info dictionaries. 115 """ 116 binaries = set(paths) 117 for p in paths: 118 binaries.update(get_needed_libs(p)) 119 if search_plugins: 120 # cast to list to avoid errors with update while iterating 121 binaries.update(list(get_plugin_libs(binaries))) 122 123 drivers = [] 124 125 for b in binaries: 126 logging.debug("analyzing %s", b) 127 try: 128 for s in get_elf_strings(b, ".rodata", "PMD_INFO_STRING="): 129 try: 130 info = json.loads(s) 131 scrub_pci_ids(info) 132 drivers.append(info) 133 except ValueError as e: 134 # invalid JSON, should never happen 135 logging.warning("%s: %s", b, e) 136 except ELFError as e: 137 # only happens for discovered plugins that are not ELF 138 logging.debug("%s: cannot parse ELF: %s", b, e) 139 140 return drivers 141 142 143# ---------------------------------------------------------------------------- 144PCI_FIELDS = ("vendor", "device", "subsystem_vendor", "subsystem_device") 145 146 147def scrub_pci_ids(info: dict): 148 """ 149 Convert numerical ids to hex strings. 150 Strip empty pci_ids lists. 151 Strip wildcard 0xFFFF ids. 152 """ 153 pci_ids = [] 154 for pci_fields in info.pop("pci_ids"): 155 pci = {} 156 for name, value in zip(PCI_FIELDS, pci_fields): 157 if value != 0xFFFF: 158 pci[name] = f"{value:04x}" 159 if pci: 160 pci_ids.append(pci) 161 if pci_ids: 162 info["pci_ids"] = pci_ids 163 164 165# ---------------------------------------------------------------------------- 166def get_plugin_libs(binaries: Iterable[Path]) -> Iterator[Path]: 167 """ 168 Look into the provided binaries for DPDK_PLUGIN_PATH and scan the path 169 for files. 170 """ 171 for b in binaries: 172 for p in get_elf_strings(b, ".rodata", "DPDK_PLUGIN_PATH="): 173 plugin_path = p.strip() 174 logging.debug("discovering plugins in %s", plugin_path) 175 for root, _, files in os.walk(plugin_path): 176 for f in files: 177 yield Path(root) / f 178 # no need to search in other binaries. 179 return 180 181 182# ---------------------------------------------------------------------------- 183def existing_file(value: str) -> Path: 184 """ 185 Argparse type= callback to ensure an argument points to a valid file path. 186 """ 187 path = Path(value) 188 if not path.is_file(): 189 raise argparse.ArgumentTypeError(f"{value}: No such file") 190 return path 191 192 193# ---------------------------------------------------------------------------- 194PRINTABLE_BYTES = frozenset(string.printable.encode("ascii")) 195 196 197def find_strings(buf: bytes, prefix: str) -> Iterator[str]: 198 """ 199 Extract strings of printable ASCII characters from a bytes buffer. 200 """ 201 view = memoryview(buf) 202 start = None 203 204 for i, b in enumerate(view): 205 if start is None and b in PRINTABLE_BYTES: 206 # mark beginning of string 207 start = i 208 continue 209 if start is not None: 210 if b in PRINTABLE_BYTES: 211 # string not finished 212 continue 213 if b == 0: 214 # end of string 215 s = view[start:i].tobytes().decode("ascii") 216 if s.startswith(prefix): 217 yield s[len(prefix):] 218 # There can be byte sequences where a non-printable byte 219 # follows a printable one. Ignore that. 220 start = None 221 222 223# ---------------------------------------------------------------------------- 224def elftools_version(): 225 """ 226 Extract pyelftools version as a tuple of integers for easy comparison. 227 """ 228 version = getattr(elftools, "__version__", "") 229 match = re.match(r"^(\d+)\.(\d+).*$", str(version)) 230 if not match: 231 # cannot determine version, hope for the best 232 return (0, 24) 233 return (int(match[1]), int(match[2])) 234 235 236ELFTOOLS_VERSION = elftools_version() 237 238 239def from_elftools(s: Union[bytes, str]) -> str: 240 """ 241 Earlier versions of pyelftools (< 0.24) return bytes encoded with "latin-1" 242 instead of python strings. 243 """ 244 if isinstance(s, bytes): 245 return s.decode("latin-1") 246 return s 247 248 249def to_elftools(s: str) -> Union[bytes, str]: 250 """ 251 Earlier versions of pyelftools (< 0.24) assume that ELF section and tags 252 are bytes encoded with "latin-1" instead of python strings. 253 """ 254 if ELFTOOLS_VERSION < (0, 24): 255 return s.encode("latin-1") 256 return s 257 258 259# ---------------------------------------------------------------------------- 260def get_elf_strings(path: Path, section: str, prefix: str) -> Iterator[str]: 261 """ 262 Extract strings from a named ELF section in a file. 263 """ 264 with path.open("rb") as f: 265 elf = ELFFile(f) 266 sec = elf.get_section_by_name(to_elftools(section)) 267 if not sec: 268 return 269 yield from find_strings(sec.data(), prefix) 270 271 272# ---------------------------------------------------------------------------- 273LDD_LIB_RE = re.compile( 274 r""" 275 ^ # beginning of line 276 \t # tab 277 (\S+) # lib name 278 \s+=>\s+ 279 (/\S+) # lib path 280 \s+ 281 \(0x[0-9A-Fa-f]+\) # address 282 \s* 283 $ # end of line 284 """, 285 re.MULTILINE | re.VERBOSE, 286) 287 288 289def get_needed_libs(path: Path) -> Iterator[Path]: 290 """ 291 Extract the dynamic library dependencies from an ELF executable. 292 """ 293 with subprocess.Popen( 294 ["ldd", str(path)], stdout=subprocess.PIPE, stderr=subprocess.PIPE 295 ) as proc: 296 out, err = proc.communicate() 297 if proc.returncode != 0: 298 err = err.decode("utf-8").splitlines()[-1].strip() 299 raise Exception(f"cannot read ELF file: {err}") 300 for match in LDD_LIB_RE.finditer(out.decode("utf-8")): 301 libname, libpath = match.groups() 302 if libname.startswith("librte_"): 303 libpath = Path(libpath) 304 if libpath.is_file(): 305 yield libpath.resolve() 306 307 308# ---------------------------------------------------------------------------- 309if __name__ == "__main__": 310 sys.exit(main()) 311