board_inspector/extractors: extract device information

This patch extracts information on devices and put them under the
`/acrn-config/devices` node in the board XML.

The generated XML looks like the following:

  <devices>
    <bus type="system">
      <acpi_object>\_SB_</acpi_object>
      <bus id="PNP0A08" type="pci" address="0x0" description="...">
        <vendor>0x8086</vendor>
        <identifier>0x591f</identifier>
        <subsystem_vendor>0x1028</subsystem_vendor>
        <subsystem_identifier>0x07a1</subsystem_identifier>
        <class>0x060000</class>
        <acpi_object>\_SB_.PCI0</acpi_object>
        <resource type="bus_number" min="0x0" max="0x3e" len="0x3f"/>
        <resource type="io_port" min="0x0" max="0xcf7" len="0xcf8"/>
        <resource type="io_port" min="0xcf8" max="0xcf8" len="0x8"/>
        <resource type="io_port" min="0xd00" max="0xffff" len="0xf300"/>
        <resource type="memory" min="0x10000" max="0x1ffff" len="0x0"/>
        <resource type="memory" min="0xa0000" max="0xbffff" len="0x20000"/>
        <resource type="memory" min="0xc0000" max="0xc3fff" len="0x4000"/>
        <resource type="memory" min="0xc4000" max="0xc7fff" len="0x4000"/>
        ...
        <capability id="vendor_specific"/>
        <device address="0x1"> ... </device>
        ...
      <bus>
    <bus>
    <device> ... <device>
  <devices>

The hierarchy of devices are based on the hierarchy of device objects in
the ACPI namespace (which is established by interpreting the ACPI DSDT and
SSDT tables). Typically most device objects are under the predefined
`_SB_` (i.e. System Bus) object under which an object representing the PCI
root complex (`\_SB_.PCI0` in the example above) can be found. The PCI
devices attached to bus 0 are listed as children of the PCI root complex
node.

For each bus or device, the board inspector tries best to parse the
information from both ACPI device objects and PCI configuration space to
extract the following:

- the model (via `_HID` object and PCI vendor ID, device ID and class code),
- assigned resources (via `_CRS` object and PCI BARs),
- capabilities (via the PCI capability list)

v1 -> v2:
 - Fix references to undeclared modules or variables.
 - Make the ACPI extractor advanced and not enabled by default.
 - Extract the secondary I/O and memory-mapped I/O addresses of bridges.

Tracked-On: #5922
Signed-off-by: Junjie Mao <junjie.mao@intel.com>
This commit is contained in:
Junjie Mao 2021-05-03 10:25:31 +08:00 committed by wenlingz
parent 52ee5827e1
commit 0aa899271d
5 changed files with 606 additions and 0 deletions

View File

@ -0,0 +1,202 @@
# Copyright (C) 2021 Intel Corporation. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
#
import logging
import lxml.etree
from acpiparser import parse_dsdt
from acpiparser.aml.interpreter import ConcreteInterpreter
from acpiparser.aml.exception import UndefinedSymbol, FutureWork
from acpiparser.rdt import *
from extractors.helpers import add_child, get_node
def parse_eisa_id(eisa_id):
chars = [
(eisa_id & 0x7c) >> 2, # Bit 6:2 of the first byte
((eisa_id & 0x3) << 3) | ((eisa_id & 0xe000) >> 13), # Bit 1:0 of the first byte and bit 7:5 of the second
(eisa_id & 0x1F00) >> 8, # Bit 4:0 of the second byte
(eisa_id & 0x00F00000) >> 20, # Bit 7:4 of the third byte
(eisa_id & 0x000F0000) >> 16, # Bit 3:0 of the third byte
(eisa_id & 0xF0000000) >> 28, # Bit 7:4 of the fourth byte
(eisa_id & 0x0F000000) >> 24, # Bit 3:0 of the fourth byte
]
if all(map(lambda x:x <= (ord('Z') - 0x40), chars[:3])):
manufacturer = ''.join(map(lambda x: chr(x + 0x40), chars[:3]))
product = ''.join(map(lambda x: "%X" % x, chars[3:6]))
revision = "%X" % chars[6]
return manufacturer + product + revision
else:
return None
predefined_nameseg = {
"_SB_": ("bus", "system"),
"_TZ_": ("thermalzone", None),
}
buses = {
"PNP0A03": "pci",
"PNP0A08": "pci",
}
def get_device_element(devices_node, namepath, hid):
assert namepath.startswith("\\")
namesegs = namepath[1:].split(".")
element = devices_node
for i,nameseg in enumerate(namesegs):
buspath = f"\\{'.'.join(namesegs[:(i+1)])}"
tag, typ = "device", None
if nameseg in predefined_nameseg.keys():
tag, typ = predefined_nameseg[nameseg]
next_element = None
for child in element:
acpi_object = get_node(child, "acpi_object")
if acpi_object is not None and acpi_object.text == buspath:
next_element = child
break
if next_element is None:
next_element = add_child(element, tag, None)
add_child(next_element, "acpi_object", buspath)
if typ:
next_element.set("type", typ)
element = next_element
if hid:
element.set("id", hid)
return element
def parse_irq(item, elem):
add_child(elem, "resource", type="irq", int=hex(item._INT))
def parse_io_port(item, elem):
add_child(elem, "resource", type="io_port", min=hex(item._MIN), max=hex(item._MAX), len=hex(item._LEN))
def parse_fixed_io_port(item, elem):
add_child(elem, "resource", type="io_port", min=hex(item._BAS), max=hex(item._BAS + item._LEN - 1), len=hex(item._LEN))
def parse_fixed_memory_range(item, elem):
add_child(elem, "resource", type="memory", min=hex(item._BAS), max=hex(item._BAS + item._LEN - 1), len=hex(item._LEN))
def parse_address_space_resource(item, elem):
if item._TYP == 0:
typ = "memory"
elif item._TYP == 1:
typ = "io_port"
elif item._TYP == 2:
typ = "bus_number"
else:
typ = "custom"
add_child(elem, "resource", type=typ, min=hex(item._MIN), max=hex(item._MAX), len=hex(item._LEN))
def parse_extended_irq(item, elem):
for irq in item._INT:
add_child(elem, "resource", type="irq", int=hex(irq))
resource_parsers = {
(0, SMALL_RESOURCE_ITEM_IRQ_FORMAT): parse_irq,
(0, SMALL_RESOURCE_ITEM_IO_PORT): parse_io_port,
(0, SMALL_RESOURCE_ITEM_FIXED_LOCATION_IO_PORT): parse_fixed_io_port,
(0, SMALL_RESOURCE_ITEM_END_TAG): (lambda x,y: None),
(1, LARGE_RESOURCE_ITEM_32BIT_FIXED_MEMORY_RANGE): parse_fixed_memory_range,
(1, LARGE_RESOURCE_ITEM_ADDRESS_SPACE_RESOURCE): parse_address_space_resource,
(1, LARGE_RESOURCE_ITEM_WORD_ADDRESS_SPACE): parse_address_space_resource,
(1, LARGE_RESOURCE_ITEM_EXTENDED_INTERRUPT): parse_extended_irq,
(1, LARGE_RESOURCE_ITEM_QWORD_ADDRESS_SPACE): parse_address_space_resource,
(1, LARGE_RESOURCE_ITEM_EXTENDED_ADDRESS_SPACE): parse_address_space_resource,
}
def fetch_device_info(devices_node, interpreter, namepath):
logging.info(f"Fetch information about device object {namepath}")
try:
# Check if an _INI method exists
try:
interpreter.interpret_method_call(namepath + "._INI")
except UndefinedSymbol:
pass
sta = None
if interpreter.context.has_symbol(f"{namepath}._STA"):
sta = interpreter.interpret_method_call(f"{namepath}._STA").get()
if sta & 0x1 == 0:
return
# Hardware ID
hid = ""
if interpreter.context.has_symbol(f"{namepath}._HID"):
hid = interpreter.interpret_method_call(f"{namepath}._HID").get()
if isinstance(hid, str):
pass
elif isinstance(hid, int):
eisa_id = parse_eisa_id(hid)
if eisa_id:
hid = eisa_id
else:
hid = hex(hid)
else:
hid = "<unknown>"
# Create the XML element for the device and create its ancestors if necessary
element = get_device_element(devices_node, namepath, hid)
if hid in buses.keys():
element.tag = "bus"
element.set("type", buses[hid])
# Address
if interpreter.context.has_symbol(f"{namepath}._ADR"):
adr = interpreter.interpret_method_call(f"{namepath}._ADR").get()
if isinstance(adr, int):
adr = hex(adr)
if len(element.xpath(f"../*[@address='{adr}']")) > 0:
logging.warning(f"{namepath} has siblings with duplicated address {adr}.")
else:
element.set("address", hex(adr) if isinstance(adr, int) else adr)
# Status
if sta is not None:
status = add_child(element, "status")
add_child(status, "present", "y" if sta & 0x1 != 0 else "n")
add_child(status, "enabled", "y" if sta & 0x2 != 0 else "n")
add_child(status, "functioning", "y" if sta & 0x8 != 0 else "n")
# Resources
if interpreter.context.has_symbol(f"{namepath}._CRS"):
data = interpreter.interpret_method_call(f"{namepath}._CRS").get()
rdt = parse_resource_data(data)
for item in rdt.items:
p = (item.type, item.name)
if p in resource_parsers.keys():
resource_parsers[p](item, element)
else:
add_child(element, "resource", type=item.__class__.__name__)
# PCI interrupt routing
if interpreter.context.has_symbol(f"{namepath}._PRT"):
interpreter.interpret_method_call(f"{namepath}._PRT")
except FutureWork:
pass
def extract(board_etree):
devices_node = get_node(board_etree, "//devices")
try:
namespace = parse_dsdt()
except Exception as e:
logging.info(f"Parse ACPI DSDT/SSDT failed: {str(e)}")
logging.info(f"Will not extract information from ACPI DSDT/SSDT")
return
interpreter = ConcreteInterpreter(namespace)
for device in sorted(namespace.devices, key=lambda x:x.name):
try:
fetch_device_info(devices_node, interpreter, device.name)
except Exception as e:
logging.info(f"Fetch information about device object {device.name} failed: {str(e)}")
advanced = True

View File

@ -0,0 +1,150 @@
# Copyright (C) 2021 Intel Corporation. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
#
import os
import re
import logging
import lxml.etree
from pcieparser import parse_config_space
from pcieparser.header import IOBar, MemoryBar32, MemoryBar64
from extractors.helpers import add_child, get_node
PCI_ROOT_PATH = "/sys/devices/pci0000:00"
bdf_regex = re.compile(r"^([0-9a-f]{4}):([0-9a-f]{2}):([0-9a-f]{2}).([0-7]{1})$")
def collect_hostbridge_resources(bus_node):
with open("/proc/iomem", "r") as f:
for line in f.readlines():
fields = line.strip().split(" : ")
if fields[1] == "PCI Bus 0000:00":
begin, end = tuple(map(lambda x: int(f"0x{x}", base=16), fields[0].split("-")))
add_child(bus_node, "resource", type="memory", min=hex(begin), max=hex(end), len=hex(end - begin + 1))
def parse_msi(cap_node, cap_struct):
if cap_struct.multiple_message_capable > 0:
multiple_message_node = add_child(cap_node, "capability", id="multiple-message")
add_child(multiple_message_node, "count", str(1 << cap_struct.multiple_message_capable))
if cap_struct.address_64bit:
add_child(cap_node, "capability", id="64-bit address")
if cap_struct.per_vector_masking_capable:
add_child(cap_node, "capability", id="per-vector masking")
cap_parsers = {
"MSI": parse_msi,
}
def parse_device(bus_node, device_path):
device_name = os.path.basename(device_path)
if device_name == "0000:00:00.0":
device_node = bus_node
else:
m = bdf_regex.match(device_name)
device, function = int(m.group(3), base=16), int(m.group(4), base=16)
adr = hex((device << 16) + function)
device_node = get_node(bus_node, f"./device[@address='{adr}']")
if device_node is None:
device_node = add_child(bus_node, "device", None, address=adr)
cfg = parse_config_space(device_path)
# Device identifiers
vendor_id = "0x{:04x}".format(cfg.header.vendor_id)
device_id = "0x{:04x}".format(cfg.header.device_id)
class_code = "0x{:06x}".format(cfg.header.class_code)
if device_node.get("id") is None:
device_node.set("id", device_id)
add_child(device_node, "vendor", vendor_id)
add_child(device_node, "identifier", device_id)
add_child(device_node, "class", class_code)
if cfg.header.header_type == 0:
subvendor_id = "0x{:04x}".format(cfg.header.subsystem_vendor_id)
subdevice_id = "0x{:04x}".format(cfg.header.subsystem_device_id)
add_child(device_node, "subsystem_vendor", subvendor_id)
add_child(device_node, "subsystem_identifier", subdevice_id)
# BARs
idx = 0
for bar in cfg.header.bars:
resource_path = os.path.join(device_path, f"resource{idx}")
resource_type = bar.resource_type
base = bar.base
if os.path.exists(resource_path):
resource_node = get_node(device_node, f"./resource[@type = '{resource_type}' and @min = '{hex(base)}']")
if resource_node is None:
size = os.path.getsize(resource_path)
resource_node = add_child(device_node, "resource", None, type=resource_type, min=hex(base), max=hex(base + size - 1), len=hex(size))
resource_node.set("id", f"bar{idx}")
if isinstance(bar, MemoryBar32):
resource_node.set("width", "32")
resource_node.set("prefetchable", str(bar.prefetchable))
elif isinstance(bar, MemoryBar64):
resource_node.set("width", "64")
resource_node.set("prefetchable", str(bar.prefetchable))
elif bar.base != 0:
logging.error(f"Cannot detect the size of BAR {idx}")
if isinstance(bar, MemoryBar64):
idx += 2
else:
idx += 1
# Capabilities
for cap in cfg.caps:
cap_node = add_child(device_node, "capability", id=cap.name)
if cap.name in cap_parsers:
cap_parsers[cap.name](cap_node, cap)
for cap in cfg.extcaps:
cap_node = add_child(device_node, "capability", id=cap.name)
if cap.name in cap_parsers:
cap_parsers[cap.name](cap_node, cap)
# Secondary bus
if cfg.header.header_type == 1:
# According to section 3.2.5.6, PCI to PCI Bridge Architecture Specification, the I/O Limit register contains a
# value smaller than the I/O Base register if there are no I/O addresses on the secondary side.
io_base = (cfg.header.io_base_upper_16_bits << 16) | ((cfg.header.io_base >> 4) << 12)
io_end = (cfg.header.io_limit_upper_16_bits << 16) | ((cfg.header.io_limit >> 4) << 12) | 0xfff
if io_base <= io_end:
add_child(device_node, "resource", type="io_port",
min=hex(io_base), max=hex(io_end), len=hex(io_end - io_base + 1))
# According to section 3.2.5.8, PCI to PCI Bridge Architecture Specification, the Memory Limit register contains
# a value smaller than the Memory Base register if there are no memory-mapped I/O addresses on the secondary
# side.
if cfg.header.memory_base <= cfg.header.memory_limit:
memory_base = (cfg.header.memory_base >> 4) << 20
memory_end = ((cfg.header.memory_limit >> 4) << 20) | 0xfffff
add_child(device_node, "resource", type="memory",
min=hex(memory_base), max=hex(memory_end), len=hex(memory_end - memory_base + 1))
secondary_bus_node = add_child(device_node, "bus", type="pci", address=hex(cfg.header.secondary_bus_number))
return secondary_bus_node
return device_node
def enum_devices(bus_node, root_path):
device_names = sorted(filter(lambda x:bdf_regex.match(x) != None, os.listdir(root_path)))
for device_name in device_names:
p = os.path.join(root_path, device_name)
device_node = parse_device(bus_node, p)
enum_devices(device_node, p)
def extract(board_etree):
bus_node = get_node(board_etree, "//bus[@type='pci']")
if bus_node is None:
devices_node = get_node(board_etree, "//devices")
bus_node = add_child(devices_node, "bus", type="pci", address="0x0")
collect_hostbridge_resources(bus_node)
else:
# Assume there is only one device object in the ACPI DSDT that represents a PCI bridge (which should be the host
# bridge in this case). If the ACPI table does not provide an _ADR object, add the default address of the host
# bridge (i.e. bus 0).
if bus_node.get("address") is None:
bus_node.set("address", "0x0")
enum_devices(bus_node, PCI_ROOT_PATH)

View File

@ -0,0 +1,123 @@
# Copyright (C) 2021 Intel Corporation. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
#
import os
import logging
from extractors.helpers import get_node
PCI_IDS_PATHS = [
"/usr/share/misc/pci.ids",
"/usr/share/hwdata/pci.ids",
]
class PCI_IDs:
def __init__(self, f):
names = {}
vendor_id = None
device_id = None
classes = {}
class_id = None
for line in f.readlines():
line = line.strip("\n")
if line == "" or line.startswith("#"):
continue
if line.startswith("\t\t"):
if device_id is not None:
parts = line.strip().split(" ")
subvendor_id, subdevice_id = tuple(map(lambda x: int(x, base=16), parts[0].split(" ")))
desc = " ".join(parts[1:])
names[(vendor_id, device_id, subvendor_id, subdevice_id)] = desc
elif line.startswith("\t"):
if vendor_id is not None:
parts = line.strip().split(" ")
device_id = int(parts[0], base=16)
desc = " ".join(parts[1:])
names[(vendor_id, device_id)] = desc
elif class_id is not None:
parts = line.strip().split(" ")
subclass_id = int(parts[0], base=16)
desc = " ".join(parts[1:])
classes[(class_id, subclass_id)] = desc
elif line.startswith("C"):
parts = line.strip().split(" ")
class_id = int(parts[0][2:], base=16)
vendor_id = None
device_id = None
desc = " ".join(parts[1:])
classes[(class_id,)] = desc
else:
parts = line.strip().split(" ")
vendor_id = int(parts[0], base=16)
device_id = None
class_id = None
desc = " ".join(parts[1:])
names[(vendor_id,)] = desc
self.__names = names
self.__classes = classes
def lookup(self, vendor_id, device_id, subsystem_vendor_id, subsystem_device_id, class_code):
acc = []
# Class
if class_code:
class_id = (class_code >> 16)
subclass_id = ((class_code >> 8) & 0xFF)
if (class_id, subclass_id) in self.__classes.keys():
acc.append(self.__classes[(class_id, subclass_id)] + ":")
elif (class_id,) in self.__classes.keys():
acc.append(self.__classes[(class_id,)] + ":")
# Vendor
if vendor_id:
if (vendor_id,) in self.__names.keys():
acc.append(self.__names[(vendor_id,)])
# Device
if vendor_id and device_id:
if (vendor_id, device_id) in self.__names.keys():
acc.append(self.__names[(vendor_id, device_id)])
return " ".join(acc)
def lookup_pci_device(element, ids):
vendor_id = get_node(element, "vendor/text()")
device_id = get_node(element, "identifier/text()")
subsystem_vendor_id = get_node(element, "subsystem_vendor/text()")
subsystem_device_id = get_node(element, "subsystem_identifier/text()")
class_code = get_node(element, "class/text()")
args = [vendor_id, device_id, subsystem_vendor_id, subsystem_device_id, class_code]
desc = ids.lookup(*list(map(lambda x: int(x, base=16) if x else None, args)))
if desc:
element.set("description", desc)
def lookup_pci_devices(board_etree):
# Lookup names of PCI devices from pci.ids if possible
pci_id_path = None
for path in PCI_IDS_PATHS:
if os.path.exists(path):
pci_id_path = path
if pci_id_path:
with open(pci_id_path, "r") as f:
ids = PCI_IDs(f)
devices = board_etree.xpath("//device")
for device in devices:
lookup_pci_device(device, ids)
buses = board_etree.xpath("//bus")
for bus in buses:
lookup_pci_device(bus, ids)
else:
logging.info(f"Cannot find pci.ids under /usr/share. PCI device names will not be available.")
def extract(board_etree):
lookup_pci_devices(board_etree)

View File

@ -0,0 +1,41 @@
# Copyright (C) 2021 Intel Corporation. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
#
import lxml.etree
def getkey(child):
def resource_subkey(node):
typ = node.get("type")
if typ in ["memory", "io_port"]:
return int(node.get("min"), base=16)
elif typ == "irq":
return int(node.get("int"), base=16)
else:
return 0
def device_subkey(node):
adr = node.get("address")
if adr is not None:
return int(adr, base=16)
else:
return 0xFFFFFFFF
tags = ["vendor", "identifier", "subsystem_vendor", "subsystem_identifier", "class", "acpi_object", "status", "resource", "capability", "bus", "device"]
if child.tag == "resource":
return (tags.index(child.tag), child.get("type"), resource_subkey(child))
elif child.tag == "device":
return (tags.index(child.tag), device_subkey(child))
else:
return (tags.index(child.tag),)
def extract(board_etree):
# Sort children of bus and device nodes
bus_nodes = board_etree.xpath("//bus")
for bus_node in bus_nodes:
bus_node[:] = sorted(bus_node, key=getkey)
device_nodes = board_etree.xpath("//device")
for device_node in device_nodes:
device_node[:] = sorted(device_node, key=getkey)

View File

@ -0,0 +1,90 @@
# Copyright (C) 2021 Intel Corporation. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
#
import ctypes
import copy
import lib.cdata as cdata
class ExtendedCapability:
# Capability names from PCI Express Base Specification, mostly Table 9-23
_cap_names_ = {
0x01: "Advanced Error Reporting",
0x02: "Virtual Channel",
0x03: "Device Serial Number",
0x04: "Power Budgeting",
0x05: "Root Complex Link Declaration",
0x06: "Root Complex Internal Link Control",
0x07: "Root Complex Event Collector Endpoint Association",
0x08: "Multi-Function Virtual Channel",
0x09: "Virtual Channel",
0x0a: "RCRB Header",
0x0b: "Vendor-Specific Extended",
0x0c: "Configuration Access Correlation",
0x0d: "ACS",
0x0e: "ARI",
0x0f: "ATS",
0x10: "SR-IOV",
0x11: "MR-IOV",
0x12: "Multicast",
0x13: "PRI",
0x15: "Resizable BAR",
0x16: "DPA",
0x17: "TPH Requester",
0x18: "LTR",
0x19: "Secondary PCI Express",
0x1a: "PMUX",
0x1b: "PASID",
0x1c: "LNR",
0x1d: "DPC",
0x1e: "L1 PM Substates",
0x1f: "TPM",
0x20: "M-PCIe",
0x21: "FRS Queueing",
0x22: "Readiness Time Reporting",
0x23: "Designated Vendor-Specific",
0x24: "VF Resizable BAR",
0x25: "Data Link Feature",
0x26: "Physical Layer 16.0 GT/s",
0x27: "Lane Margining at the Receiver",
0x28: "Hierarchy ID",
0x29: "NPEM",
0x2a: "Physical Layer 32.0 GT/s",
0x2b: "Alternate Protocol",
0x2c: "SFI",
}
@property
def name(self):
if self.id in self._cap_names_.keys():
return self._cap_names_[self.id]
else:
return f"Reserved Extended ({hex(self.id)})"
class ExtendedCapabilityListRegister(cdata.Struct, ExtendedCapability):
_pack_ = 1
_fields_ = [
('id', ctypes.c_uint32, 16),
('version', ctypes.c_uint32, 4),
('next_cap_ptr_raw', ctypes.c_uint32, 12),
]
@property
def next_cap_ptr(self):
return self.next_cap_ptr_raw & 0xffc
# Module API
def extended_capabilities(data):
buf = ctypes.create_string_buffer(data, len(data))
cap_ptr = 0x100
acc = list()
while cap_ptr != 0:
caplist = ExtendedCapabilityListRegister.from_buffer_copy(buf, cap_ptr)
if caplist.id != 0:
acc.append(caplist)
cap_ptr = caplist.next_cap_ptr
return acc