1#!/usr/bin/env python3 2# SPDX-License-Identifier: BSD-3-Clause 3# Copyright(c) 2016 Neil Horman <nhorman@tuxdriver.com> 4 5# ------------------------------------------------------------------------- 6# 7# Utility to dump PMD_INFO_STRING support from an object file 8# 9# ------------------------------------------------------------------------- 10import json 11import os 12import platform 13import sys 14import argparse 15from elftools.common.exceptions import ELFError 16from elftools.common.py3compat import byte2int 17from elftools.elf.elffile import ELFFile 18 19 20# For running from development directory. It should take precedence over the 21# installed pyelftools. 22sys.path.insert(0, '.') 23 24raw_output = False 25pcidb = None 26 27# =========================================== 28 29class Vendor: 30 """ 31 Class for vendors. This is the top level class 32 for the devices belong to a specific vendor. 33 self.devices is the device dictionary 34 subdevices are in each device. 35 """ 36 37 def __init__(self, vendorStr): 38 """ 39 Class initializes with the raw line from pci.ids 40 Parsing takes place inside __init__ 41 """ 42 self.ID = vendorStr.split()[0] 43 self.name = vendorStr.replace("%s " % self.ID, "").rstrip() 44 self.devices = {} 45 46 def addDevice(self, deviceStr): 47 """ 48 Adds a device to self.devices 49 takes the raw line from pci.ids 50 """ 51 s = deviceStr.strip() 52 devID = s.split()[0] 53 if devID in self.devices: 54 pass 55 else: 56 self.devices[devID] = Device(deviceStr) 57 58 def report(self): 59 print(self.ID, self.name) 60 for id, dev in self.devices.items(): 61 dev.report() 62 63 def find_device(self, devid): 64 # convert to a hex string and remove 0x 65 devid = hex(devid)[2:] 66 try: 67 return self.devices[devid] 68 except: 69 return Device("%s Unknown Device" % devid) 70 71 72class Device: 73 74 def __init__(self, deviceStr): 75 """ 76 Class for each device. 77 Each vendor has its own devices dictionary. 78 """ 79 s = deviceStr.strip() 80 self.ID = s.split()[0] 81 self.name = s.replace("%s " % self.ID, "") 82 self.subdevices = {} 83 84 def report(self): 85 print("\t%s\t%s" % (self.ID, self.name)) 86 for subID, subdev in self.subdevices.items(): 87 subdev.report() 88 89 def addSubDevice(self, subDeviceStr): 90 """ 91 Adds a subvendor, subdevice to device. 92 Uses raw line from pci.ids 93 """ 94 s = subDeviceStr.strip() 95 spl = s.split() 96 subVendorID = spl[0] 97 subDeviceID = spl[1] 98 subDeviceName = s.split(" ")[-1] 99 devID = "%s:%s" % (subVendorID, subDeviceID) 100 self.subdevices[devID] = SubDevice( 101 subVendorID, subDeviceID, subDeviceName) 102 103 def find_subid(self, subven, subdev): 104 subven = hex(subven)[2:] 105 subdev = hex(subdev)[2:] 106 devid = "%s:%s" % (subven, subdev) 107 108 try: 109 return self.subdevices[devid] 110 except: 111 if (subven == "ffff" and subdev == "ffff"): 112 return SubDevice("ffff", "ffff", "(All Subdevices)") 113 return SubDevice(subven, subdev, "(Unknown Subdevice)") 114 115 116class SubDevice: 117 """ 118 Class for subdevices. 119 """ 120 121 def __init__(self, vendor, device, name): 122 """ 123 Class initializes with vendorid, deviceid and name 124 """ 125 self.vendorID = vendor 126 self.deviceID = device 127 self.name = name 128 129 def report(self): 130 print("\t\t%s\t%s\t%s" % (self.vendorID, self.deviceID, self.name)) 131 132 133class PCIIds: 134 """ 135 Top class for all pci.ids entries. 136 All queries will be asked to this class. 137 PCIIds.vendors["0e11"].devices["0046"].\ 138 subdevices["0e11:4091"].name = "Smart Array 6i" 139 """ 140 141 def __init__(self, filename): 142 """ 143 Prepares the directories. 144 Checks local data file. 145 Tries to load from local, if not found, downloads from web 146 """ 147 self.version = "" 148 self.date = "" 149 self.vendors = {} 150 self.contents = None 151 self.readLocal(filename) 152 self.parse() 153 154 def reportVendors(self): 155 """Reports the vendors 156 """ 157 for vid, v in self.vendors.items(): 158 print(v.ID, v.name) 159 160 def report(self, vendor=None): 161 """ 162 Reports everything for all vendors or a specific vendor 163 PCIIds.report() reports everything 164 PCIIDs.report("0e11") reports only "Compaq Computer Corporation" 165 """ 166 if vendor is not None: 167 self.vendors[vendor].report() 168 else: 169 for vID, v in self.vendors.items(): 170 v.report() 171 172 def find_vendor(self, vid): 173 # convert vid to a hex string and remove the 0x 174 vid = hex(vid)[2:] 175 176 try: 177 return self.vendors[vid] 178 except: 179 return Vendor("%s Unknown Vendor" % (vid)) 180 181 def findDate(self, content): 182 for l in content: 183 if l.find("Date:") > -1: 184 return l.split()[-2].replace("-", "") 185 return None 186 187 def parse(self): 188 if not self.contents: 189 print("data/%s-pci.ids not found" % self.date) 190 else: 191 vendorID = "" 192 deviceID = "" 193 for l in self.contents: 194 if l[0] == "#": 195 continue 196 elif not l.strip(): 197 continue 198 else: 199 if l.find("\t\t") == 0: 200 self.vendors[vendorID].devices[ 201 deviceID].addSubDevice(l) 202 elif l.find("\t") == 0: 203 deviceID = l.strip().split()[0] 204 self.vendors[vendorID].addDevice(l) 205 else: 206 vendorID = l.split()[0] 207 self.vendors[vendorID] = Vendor(l) 208 209 def readLocal(self, filename): 210 """ 211 Reads the local file 212 """ 213 with open(filename, 'r', encoding='utf-8') as f: 214 self.contents = f.readlines() 215 self.date = self.findDate(self.contents) 216 217 def loadLocal(self): 218 """ 219 Loads database from local. If there is no file, 220 it creates a new one from web 221 """ 222 self.date = idsfile[0].split("/")[1].split("-")[0] 223 self.readLocal() 224 225 226# ======================================= 227 228def search_file(filename, search_path): 229 """ Given a search path, find file with requested name """ 230 for path in search_path.split(':'): 231 candidate = os.path.join(path, filename) 232 if os.path.exists(candidate): 233 return os.path.abspath(candidate) 234 return None 235 236 237class ReadElf(object): 238 """ display_* methods are used to emit output into the output stream 239 """ 240 241 def __init__(self, file, output): 242 """ file: 243 stream object with the ELF file to read 244 245 output: 246 output stream to write to 247 """ 248 self.elffile = ELFFile(file) 249 self.output = output 250 251 # Lazily initialized if a debug dump is requested 252 self._dwarfinfo = None 253 254 self._versioninfo = None 255 256 def _section_from_spec(self, spec): 257 """ Retrieve a section given a "spec" (either number or name). 258 Return None if no such section exists in the file. 259 """ 260 try: 261 num = int(spec) 262 if num < self.elffile.num_sections(): 263 return self.elffile.get_section(num) 264 return None 265 except ValueError: 266 # Not a number. Must be a name then 267 section = self.elffile.get_section_by_name(force_unicode(spec)) 268 if section is None: 269 # No match with a unicode name. 270 # Some versions of pyelftools (<= 0.23) store internal strings 271 # as bytes. Try again with the name encoded as bytes. 272 section = self.elffile.get_section_by_name(force_bytes(spec)) 273 return section 274 275 def pretty_print_pmdinfo(self, pmdinfo): 276 global pcidb 277 278 for i in pmdinfo["pci_ids"]: 279 vendor = pcidb.find_vendor(i[0]) 280 device = vendor.find_device(i[1]) 281 subdev = device.find_subid(i[2], i[3]) 282 print("%s (%s) : %s (%s) %s" % 283 (vendor.name, vendor.ID, device.name, 284 device.ID, subdev.name)) 285 286 def parse_pmd_info_string(self, mystring): 287 global raw_output 288 global pcidb 289 290 optional_pmd_info = [ 291 {'id': 'params', 'tag': 'PMD PARAMETERS'}, 292 {'id': 'kmod', 'tag': 'PMD KMOD DEPENDENCIES'} 293 ] 294 295 i = mystring.index("=") 296 mystring = mystring[i + 2:] 297 pmdinfo = json.loads(mystring) 298 299 if raw_output: 300 print(json.dumps(pmdinfo)) 301 return 302 303 print("PMD NAME: " + pmdinfo["name"]) 304 for i in optional_pmd_info: 305 try: 306 print("%s: %s" % (i['tag'], pmdinfo[i['id']])) 307 except KeyError: 308 continue 309 310 if pmdinfo["pci_ids"]: 311 print("PMD HW SUPPORT:") 312 if pcidb is not None: 313 self.pretty_print_pmdinfo(pmdinfo) 314 else: 315 print("VENDOR\t DEVICE\t SUBVENDOR\t SUBDEVICE") 316 for i in pmdinfo["pci_ids"]: 317 print("0x%04x\t 0x%04x\t 0x%04x\t\t 0x%04x" % 318 (i[0], i[1], i[2], i[3])) 319 320 print("") 321 322 def display_pmd_info_strings(self, section_spec): 323 """ Display a strings dump of a section. section_spec is either a 324 section number or a name. 325 """ 326 section = self._section_from_spec(section_spec) 327 if section is None: 328 return 329 330 data = section.data() 331 dataptr = 0 332 333 while dataptr < len(data): 334 while (dataptr < len(data) and 335 not 32 <= byte2int(data[dataptr]) <= 127): 336 dataptr += 1 337 338 if dataptr >= len(data): 339 break 340 341 endptr = dataptr 342 while endptr < len(data) and byte2int(data[endptr]) != 0: 343 endptr += 1 344 345 # pyelftools may return byte-strings, force decode them 346 mystring = force_unicode(data[dataptr:endptr]) 347 rc = mystring.find("PMD_INFO_STRING") 348 if rc != -1: 349 self.parse_pmd_info_string(mystring[rc:]) 350 351 dataptr = endptr 352 353 def find_librte_eal(self, section): 354 for tag in section.iter_tags(): 355 # pyelftools may return byte-strings, force decode them 356 if force_unicode(tag.entry.d_tag) == 'DT_NEEDED': 357 if "librte_eal" in force_unicode(tag.needed): 358 return force_unicode(tag.needed) 359 return None 360 361 def search_for_autoload_path(self): 362 scanelf = self 363 scanfile = None 364 library = None 365 366 section = self._section_from_spec(".dynamic") 367 try: 368 eallib = self.find_librte_eal(section) 369 if eallib is not None: 370 ldlibpath = os.environ.get('LD_LIBRARY_PATH') 371 if ldlibpath is None: 372 ldlibpath = "" 373 dtr = self.get_dt_runpath(section) 374 library = search_file(eallib, 375 dtr + ":" + ldlibpath + 376 ":/usr/lib64:/lib64:/usr/lib:/lib") 377 if library is None: 378 return (None, None) 379 if not raw_output: 380 print("Scanning for autoload path in %s" % library) 381 scanfile = open(library, 'rb') 382 scanelf = ReadElf(scanfile, sys.stdout) 383 except AttributeError: 384 # Not a dynamic binary 385 pass 386 except ELFError: 387 scanfile.close() 388 return (None, None) 389 390 section = scanelf._section_from_spec(".rodata") 391 if section is None: 392 if scanfile is not None: 393 scanfile.close() 394 return (None, None) 395 396 data = section.data() 397 dataptr = 0 398 399 while dataptr < len(data): 400 while (dataptr < len(data) and 401 not 32 <= byte2int(data[dataptr]) <= 127): 402 dataptr += 1 403 404 if dataptr >= len(data): 405 break 406 407 endptr = dataptr 408 while endptr < len(data) and byte2int(data[endptr]) != 0: 409 endptr += 1 410 411 # pyelftools may return byte-strings, force decode them 412 mystring = force_unicode(data[dataptr:endptr]) 413 rc = mystring.find("DPDK_PLUGIN_PATH") 414 if rc != -1: 415 rc = mystring.find("=") 416 return (mystring[rc + 1:], library) 417 418 dataptr = endptr 419 if scanfile is not None: 420 scanfile.close() 421 return (None, None) 422 423 def get_dt_runpath(self, dynsec): 424 for tag in dynsec.iter_tags(): 425 # pyelftools may return byte-strings, force decode them 426 if force_unicode(tag.entry.d_tag) == 'DT_RUNPATH': 427 return force_unicode(tag.runpath) 428 return "" 429 430 def process_dt_needed_entries(self): 431 """ Look to see if there are any DT_NEEDED entries in the binary 432 And process those if there are 433 """ 434 runpath = "" 435 ldlibpath = os.environ.get('LD_LIBRARY_PATH') 436 if ldlibpath is None: 437 ldlibpath = "" 438 439 dynsec = self._section_from_spec(".dynamic") 440 try: 441 runpath = self.get_dt_runpath(dynsec) 442 except AttributeError: 443 # dynsec is None, just return 444 return 445 446 for tag in dynsec.iter_tags(): 447 # pyelftools may return byte-strings, force decode them 448 if force_unicode(tag.entry.d_tag) == 'DT_NEEDED': 449 if 'librte_' in force_unicode(tag.needed): 450 library = search_file(force_unicode(tag.needed), 451 runpath + ":" + ldlibpath + 452 ":/usr/lib64:/lib64:/usr/lib:/lib") 453 if library is not None: 454 with open(library, 'rb') as file: 455 try: 456 libelf = ReadElf(file, sys.stdout) 457 except ELFError: 458 print("%s is no an ELF file" % library) 459 continue 460 libelf.process_dt_needed_entries() 461 libelf.display_pmd_info_strings(".rodata") 462 file.close() 463 464 465# compat: remove force_unicode & force_bytes when pyelftools<=0.23 support is 466# dropped. 467def force_unicode(s): 468 if hasattr(s, 'decode') and callable(s.decode): 469 s = s.decode('latin-1') # same encoding used in pyelftools py3compat 470 return s 471 472 473def force_bytes(s): 474 if hasattr(s, 'encode') and callable(s.encode): 475 s = s.encode('latin-1') # same encoding used in pyelftools py3compat 476 return s 477 478 479def scan_autoload_path(autoload_path): 480 global raw_output 481 482 if not os.path.exists(autoload_path): 483 return 484 485 try: 486 dirs = os.listdir(autoload_path) 487 except OSError: 488 # Couldn't read the directory, give up 489 return 490 491 for d in dirs: 492 dpath = os.path.join(autoload_path, d) 493 if os.path.isdir(dpath): 494 scan_autoload_path(dpath) 495 if os.path.isfile(dpath): 496 try: 497 file = open(dpath, 'rb') 498 readelf = ReadElf(file, sys.stdout) 499 except ELFError: 500 # this is likely not an elf file, skip it 501 continue 502 except IOError: 503 # No permission to read the file, skip it 504 continue 505 506 if not raw_output: 507 print("Hw Support for library %s" % d) 508 readelf.display_pmd_info_strings(".rodata") 509 file.close() 510 511 512def scan_for_autoload_pmds(dpdk_path): 513 """ 514 search the specified application or path for a pmd autoload path 515 then scan said path for pmds and report hw support 516 """ 517 global raw_output 518 519 if not os.path.isfile(dpdk_path): 520 if not raw_output: 521 print("Must specify a file name") 522 return 523 524 file = open(dpdk_path, 'rb') 525 try: 526 readelf = ReadElf(file, sys.stdout) 527 except ElfError: 528 if not raw_output: 529 print("Unable to parse %s" % file) 530 return 531 532 (autoload_path, scannedfile) = readelf.search_for_autoload_path() 533 if not autoload_path: 534 if not raw_output: 535 print("No autoload path configured in %s" % dpdk_path) 536 return 537 if not raw_output: 538 if scannedfile is None: 539 scannedfile = dpdk_path 540 print("Found autoload path %s in %s" % (autoload_path, scannedfile)) 541 542 file.close() 543 if not raw_output: 544 print("Discovered Autoload HW Support:") 545 scan_autoload_path(autoload_path) 546 return 547 548 549def main(stream=None): 550 global raw_output 551 global pcidb 552 553 pcifile_default = "./pci.ids" # For unknown OS's assume local file 554 if platform.system() == 'Linux': 555 # hwdata is the legacy location, misc is supported going forward 556 pcifile_default = "/usr/share/misc/pci.ids" 557 if not os.path.exists(pcifile_default): 558 pcifile_default = "/usr/share/hwdata/pci.ids" 559 elif platform.system() == 'FreeBSD': 560 pcifile_default = "/usr/local/share/pciids/pci.ids" 561 if not os.path.exists(pcifile_default): 562 pcifile_default = "/usr/share/misc/pci_vendors" 563 564 parser = argparse.ArgumentParser( 565 usage='usage: %(prog)s [-hrtp] [-d <pci id file>] elf_file', 566 description="Dump pmd hardware support info") 567 group = parser.add_mutually_exclusive_group() 568 group.add_argument('-r', '--raw', 569 action='store_true', dest='raw_output', 570 help='dump raw json strings') 571 group.add_argument("-t", "--table", dest="tblout", 572 help="output information on hw support as a hex table", 573 action='store_true') 574 parser.add_argument("-d", "--pcidb", dest="pcifile", 575 help="specify a pci database to get vendor names from", 576 default=pcifile_default, metavar="FILE") 577 parser.add_argument("-p", "--plugindir", dest="pdir", 578 help="scan dpdk for autoload plugins", 579 action='store_true') 580 parser.add_argument("elf_file", help="driver shared object file") 581 args = parser.parse_args() 582 583 if args.raw_output: 584 raw_output = True 585 586 if args.tblout: 587 args.pcifile = None 588 589 if args.pcifile: 590 pcidb = PCIIds(args.pcifile) 591 if pcidb is None: 592 print("Pci DB file not found") 593 exit(1) 594 595 if args.pdir: 596 exit(scan_for_autoload_pmds(args.elf_file)) 597 598 ldlibpath = os.environ.get('LD_LIBRARY_PATH') 599 if ldlibpath is None: 600 ldlibpath = "" 601 602 if os.path.exists(args.elf_file): 603 myelffile = args.elf_file 604 else: 605 myelffile = search_file(args.elf_file, 606 ldlibpath + ":/usr/lib64:/lib64:/usr/lib:/lib") 607 608 if myelffile is None: 609 print("File not found") 610 sys.exit(1) 611 612 with open(myelffile, 'rb') as file: 613 try: 614 readelf = ReadElf(file, sys.stdout) 615 readelf.process_dt_needed_entries() 616 readelf.display_pmd_info_strings(".rodata") 617 sys.exit(0) 618 619 except ELFError as ex: 620 sys.stderr.write('ELF error: %s\n' % ex) 621 sys.exit(1) 622 623 624# ------------------------------------------------------------------------- 625if __name__ == '__main__': 626 main() 627