board_inspector: collect inter-device dependency in board XMLs

AML allows devices defined in an ACPI namespace to have inter-dependency,
i.e. a method defined in one device can refer to objects in other
devices. While such inter-dependency is common in device manipulation
methods, device identification and configuration methods, such as _CRS, may
depend on other devices as well.

An example we have already met is a PCS (Physical Coding Sublayer) which
calculates resource descriptors by accessing the PCI configuration space of
the accompanying Ethernet controller. Without the ACPI object describing
the PCS, a driver of the Ethernet controller may refuse to initialize.

This patch adds a preliminary dependency analyzer to detect such
inter-device dependency. The analyzer walks through the reference chains of
an object, identifying whether the referenced objects are operation fields
of a device. Depending on the result of this analysis, the board XML is
refined as follows.

  * When an object (probably a method) references such fields, the original
    object definition in host DSDT/SSDTs will be copied in the AML template
    so that they still work in VMs where the operation fields may be
    virtualized. Such objects will be referred to as "copied objects"
    hereinafter.

  * The objects that are **directly** referenced by a copied object is
    added in the AML template as well. Such objects still belong to devices
    where they are originally defined in the host ACPI namespace. Their
    definition, however, may be copied or replaced with constant values,
    depending on the dependency analysis on these objects.

  * Nodes with the "dependency" tag are added under "device" nodes in the
    board XML, allowing the configuration tools to follow the device
    dependency chain when generating vACPI tables. These nodes only
    represent direct dependencies; indirect dependencies can be inferred by
    following those direct ones.

The current implementation does not allow objects being added to AML
templates if they refer to any of the following.

  * Global objects, i.e. objects not belonging to any device. Such objects
    tend to encode system-wide information, such as the ACPI
    NVS (Non-Volatile Storage) or its fields.

  * Methods with parameters.

Objects with such references are thus being hidden from guest software,
just like how they are invisible in the current implementation.

This patch is added in v2 of the series.

v2 -> v3:
  * Also collect dependencies due to providing or consuming resources.
  * Refactor the dependency detection logic for clarity.

Tracked-On: #6287
Signed-off-by: Junjie Mao <junjie.mao@intel.com>
This commit is contained in:
Junjie Mao 2021-08-02 16:13:50 +08:00 committed by wenlingz
parent a2e9a05737
commit d6e47bcea5
4 changed files with 307 additions and 21 deletions

View File

@ -30,6 +30,14 @@ class FieldDecl(NamedDecl):
def dump(self):
print(f"{self.name}: {self.__class__.__name__}, {self.length} bits")
class OperationRegionDecl(NamedDecl):
@staticmethod
def object_type():
return 10
def __init__(self, name, tree):
super().__init__(name, tree)
class OperationFieldDecl(NamedDecl):
def __init__(self, name, length, tree):
super().__init__(name, tree)
@ -37,6 +45,7 @@ class OperationFieldDecl(NamedDecl):
self.offset = None
self.length = length
self.access_width = None
self.parent_tree = None
def set_location(self, region, offset, access_width):
self.region = region
@ -155,6 +164,13 @@ class Context:
else:
return parent
@staticmethod
def normalize_namepath(namepath):
path = namepath.lstrip("\\^")
prefix = namepath[:(len(namepath) - len(path))]
parts = '.'.join(map(lambda x: x[:4].ljust(4, '_'), path.split(".")))
return prefix + parts
def __init__(self):
self.streams = {}
self.current_stream = None
@ -266,18 +282,25 @@ class Context:
prefix_len -= 1
raise KeyError(name)
def lookup_symbol(self, name):
def lookup_symbol(self, name, scope=None):
if scope:
self.change_scope(scope)
try:
if name.startswith("\\"):
return self.__symbol_table[name]
ret = self.__symbol_table[name]
elif name.startswith("^") or name.find(".") >= 0:
realpath = self.realpath(self.__current_scope, name)
return self.__symbol_table[realpath]
ret = self.__symbol_table[realpath]
else:
return self.__lookup_symbol_in_parents(self.__symbol_table, name)
ret = self.__lookup_symbol_in_parents(self.__symbol_table, name)
except KeyError:
logging.debug(f"Cannot find definition of {name}")
raise UndefinedSymbol(name, self.get_scope())
ret = None
if scope:
self.pop_scope()
if not ret:
raise UndefinedSymbol(name, scope if scope else self.get_scope())
return ret
def has_symbol(self, name):
try:

View File

@ -531,6 +531,7 @@ def DefField_hook_post(context, tree):
sym = context.lookup_symbol(name)
assert isinstance(sym, OperationFieldDecl)
sym.set_location(region_name, bit_offset, access_width)
sym.parent_tree = tree
bit_offset += length
elif field.label == "ReservedField":
length = field.FieldLength.value
@ -580,7 +581,7 @@ def DefMethod_hook_post(context, tree):
context.register_symbol(sym)
def DefOpRegion_hook_named(context, tree, name):
sym = NamedDecl(name, tree)
sym = OperationRegionDecl(name, tree)
context.register_symbol(sym)
def DefPowerRes_hook_named(context, tree, name):

View File

@ -19,7 +19,12 @@ from acpiparser.rdt import *
from extractors.helpers import add_child, get_node
device_objects = defaultdict(lambda: {})
device_objects = defaultdict(lambda: {}) # device_path -> object_name -> tree
device_deps = defaultdict(lambda: defaultdict(lambda: set())) # device_path -> dep_type -> {device_path}
DEP_TYPE_USES = "uses"
DEP_TYPE_USED_BY = "is used by"
DEP_TYPE_CONSUMES = "consumes resources from"
DEP_TYPE_PROVIDES = "provides resources to"
def parse_eisa_id(eisa_id):
chars = [
@ -117,13 +122,262 @@ resource_parsers = {
(1, LARGE_RESOURCE_ITEM_EXTENDED_ADDRESS_SPACE): parse_address_space_resource,
}
def add_object_to_device(context, device_path, obj_name, result):
if not obj_name in device_objects[device_path].keys():
tree = builder.build_value(result)
if tree:
device_objects[device_path][obj_name] = builder.DefName(obj_name, tree)
class CollectDependencyVisitor(Visitor):
class AnalysisResult:
def __init__(self):
self.direct = defaultdict(lambda: list()) # device path -> [decls]
self.all = defaultdict(lambda: list()) # device path -> [decls]
def add_direct_dep(self, scope_decl, decl):
scope_name = scope_decl.name if scope_decl else "global"
if decl not in self.direct[scope_name]:
self.direct[scope_name].append(decl)
if decl not in self.all[scope_name]:
self.all[scope_name].append(decl)
def add_indirect_dep(self, scope_decl, decl):
scope_name = scope_decl.name if scope_decl else "global"
if decl not in self.all[scope_name]:
self.all[scope_name].append(decl)
def __str__(self):
formatter = lambda pair: "{}: {}".format(pair[0], list(map(lambda decl: decl.name, pair[1])))
direct_deps = ", ".join(map(formatter, self.direct.items()))
all_deps = ", ".join(map(formatter, self.all.items()))
return f"direct deps = {{ {direct_deps} }}; all deps = {{ {all_deps} }}"
def __init__(self, interpreter):
super().__init__(Direction.TOPDOWN)
self.interpreter = interpreter
self.context = interpreter.context
# namepath -> Boolean: a cache of operation regions whose exposure has been determine previously
self.op_region_exposure = {}
def __is_exposed_opregion(self, op_region_decl):
try:
# Check the cache first. If the given region is not checked previously (which will cause a KeyError
# exception), the 'except' clause will do the actual work.
return self.op_region_exposure[op_region_decl.name]
except KeyError:
# If an operation region is exposed to a VM, we cannot assume any field in that region to have constant
# values as the VM may be able to change it at runtime. Thus, such operation regions shall be exposed
# unchanged in the vACPI.
#
# An operation region is considered to be exposed if all of the following conditions are true.
#
# 1. It belongs to a device (i.e. global operation regions such as ACPI NVS are not exposed)
#
# 2. It is a system memory region that resides in any of the resources declared for the device; or it is
# within the PCI configuration space.
#
# FIXME: The BAR-mapped MMIO regions are also exposed to VMs but not yet considered in the following
# logic. This needs to be considered later.
op_region_is_exposed = False
op_region_type = op_region_decl.tree.RegionSpace.value
if op_region_type == 0x00: # System memory
self.interpreter.context.change_scope(op_region_decl.tree.scope)
region_base = self.interpreter.interpret(op_region_decl.tree.RegionOffset).get()
region_length = self.interpreter.interpret(op_region_decl.tree.RegionLen).get()
self.interpreter.context.pop_scope()
device_decl = self.__find_device_of_object(op_region_decl)
if device_decl and self.context.has_symbol(f"{device_decl.name}._CRS"):
crs_object = self.interpreter.interpret_method_call(f"{device_decl.name}._CRS")
resources = parse_resource_data(crs_object.get())
for item in filter(lambda x: x.type == 1, resources.items):
if item.name == LARGE_RESOURCE_ITEM_32BIT_FIXED_MEMORY_RANGE:
if item._BAS <= region_base and region_base + region_length - 1 <= item._BAS + item._LEN - 1:
op_region_is_exposed = True
break
elif item.name in [LARGE_RESOURCE_ITEM_ADDRESS_SPACE_RESOURCE,
LARGE_RESOURCE_ITEM_WORD_ADDRESS_SPACE,
LARGE_RESOURCE_ITEM_QWORD_ADDRESS_SPACE,
LARGE_RESOURCE_ITEM_EXTENDED_ADDRESS_SPACE]:
if item._MIN <= region_base and region_base + region_length - 1 <= item._MAX:
op_region_is_exposed = True
break
elif op_region_type == 0x02: # PCI configuration space is always exposed
op_region_is_exposed = True
self.op_region_exposure[op_region_decl.name] = op_region_is_exposed
return op_region_is_exposed
def is_exposed_field(self, field_decl):
if isinstance(field_decl.region, str):
return self.__is_exposed_opregion(self.context.lookup_symbol(field_decl.region))
else:
logging.warning(f"{device_path}.{obj_name}: will not added to vACPI due to unrecognized type: {result.__class__.__name__}")
# Indexed fields are typically accessed using I/O ports which are not exposed to VMs in general.
return False
def analyze(self, scope, obj_name):
self.result = self.AnalysisResult()
self.tree_under_analysis = self.context.lookup_symbol(obj_name, scope).tree
self.to_visit = set([self.tree_under_analysis])
self.current_tree = None
visited = set()
while self.to_visit:
self.current_tree = self.to_visit.pop()
if self.current_tree not in visited:
self.visit(self.current_tree)
visited.add(self.current_tree)
return self.result
def __find_device_of_object(self, decl):
scope_decl = decl
try:
while not isinstance(scope_decl, context.DeviceDecl):
scope_decl = self.context.lookup_symbol(self.context.parent(scope_decl.name))
except UndefinedSymbol:
scope_decl = None
return scope_decl
def __add_dependency(self, decl):
scope_decl = decl
try:
while not isinstance(scope_decl, context.DeviceDecl):
scope_decl = self.context.lookup_symbol(self.context.parent(scope_decl.name))
if scope_decl.tree == self.current_tree:
# There is no need to record any dependency if ``decl`` is declared within the scope of the current
# visiting one (e.g. a local variable in a method).
return
except UndefinedSymbol:
scope_decl = None
if self.current_tree == self.tree_under_analysis:
self.result.add_direct_dep(scope_decl, decl)
else:
self.result.add_indirect_dep(scope_decl, decl)
def NameString(self, tree):
self.context.change_scope(tree.scope)
try:
decl = self.context.lookup_symbol(tree.value)
if isinstance(decl, context.OperationFieldDecl) and self.current_tree == self.tree_under_analysis:
if self.is_exposed_field(decl):
op_region_decl = self.context.lookup_symbol(decl.region)
self.__add_dependency(op_region_decl)
self.to_visit.add(op_region_decl.tree)
elif isinstance(decl, context.MethodDecl):
self.to_visit.add(decl.tree)
# Do not record the object under analysis as a dependency
if decl.tree != self.tree_under_analysis:
self.__add_dependency(decl)
except UndefinedSymbol:
pass
self.context.pop_scope()
def add_object_to_device(interpreter, device_path, obj_name, result):
def aux(device_path, obj_name, result):
# This is the main function that recursively scans dependent object definitions and include either their
# original definition or calculated values into the AML template. The algorithm is as follows:
#
# 1. Collect the objects that are used (either directly or indirectly) by the given object.
#
# 2. Determine how this object should go to the AML template by the following rules.
#
# a. If the object depends on any global object (i.e. not in the scope of any device), the object will not
# be put to the AML template at all. Up to now we are not aware of any safe way to expose the object to
# VMs as global objects can be operation fields within arbitrary memory-mapped regions.
#
# b. If the object depends on any operation field that is exposed to a VM, the object will be copied as is
# in the AML template.
#
# c. Otherwise, it will be replaced with the current evaluated value as it is unlikely to change due to
# guest software activities.
#
# Operation regions and its fields, when necessary, are always copied as is.
#
# Dependency among devices are also collected along the way.
if not obj_name in device_objects[device_path].keys():
visitor = CollectDependencyVisitor(interpreter)
deps = visitor.analyze(device_path, obj_name)
copy_object = False
if deps.all:
# If the object refers to any operation region directly or indirectly, it is generally necessary to copy
# the original definition of the object.
for dev, decls in deps.all.items():
if next(filter(lambda x: isinstance(x, context.OperationFieldDecl) and visitor.is_exposed_field(x), decls), None):
copy_object = True
break
evaluated = (result != None)
need_global = ("global" in deps.all.keys())
formatter = lambda x: '+' if x else '-'
logging.info(f"{device_path}.{obj_name}: Evaluated{formatter(evaluated)} Copy{formatter(copy_object)} NeedGlobal{formatter(need_global)}")
if result == None or copy_object:
if need_global:
global_objs = ', '.join(map(lambda x: x.name, deps.all["global"]))
raise NotImplementedError(f"{device_path}.{obj_name}: references to global objects: {global_objs}")
# Add directly referred objects first
for peer_device, peer_decls in deps.direct.items():
if peer_device == "global":
peer_device = device_path
for peer_decl in peer_decls:
peer_obj_name = peer_decl.name[-4:]
if isinstance(peer_decl, context.OperationRegionDecl):
aux(peer_device, peer_obj_name, None)
elif isinstance(peer_decl, context.OperationFieldDecl):
op_region_name = peer_decl.region
# Assume an operation region has at most one DefField object defining its fields
device_objects[peer_device][f"{op_region_name}_fields"] = peer_decl.parent_tree
else:
if isinstance(peer_decl, context.MethodDecl) and peer_decl.nargs > 0:
raise NotImplementedError(f"{peer_decl.name}: copy of methods with arguments is not supported")
value = interpreter.interpret_method_call(peer_decl.name)
aux(peer_device, peer_obj_name, value)
# If decl is of another device, declare decl as an external symbol in the template of
# device_path so that the template can be parsed on its own
if peer_device != device_path:
device_objects[device_path][peer_decl.name] = builder.DefExternal(
peer_decl.name,
peer_decl.object_type(),
peer_decl.nargs if isinstance(peer_decl, context.MethodDecl) else 0)
device_deps[device_path][DEP_TYPE_USES].add(peer_device)
device_deps[peer_device][DEP_TYPE_USED_BY].add(device_path)
decl = interpreter.context.lookup_symbol(obj_name, device_path)
device_objects[device_path][obj_name] = decl.tree
else:
tree = builder.build_value(result)
if tree:
device_objects[device_path][obj_name] = builder.DefName(obj_name, tree)
else:
raise NotImplementedError(f"{device_path}.{obj_name}: unrecognized type: {result.__class__.__name__}")
# The main routine that collects dependent objects recursively
try:
aux(device_path, obj_name, result)
# A device also depends on resource providers. If the given object is a resource template, scan for the encoded
# resource sources.
if obj_name == "_CRS":
namespace = interpreter.context
rdt = parse_resource_data(result.get())
for item in rdt.items:
source = getattr(item, "resource_source", None)
if source:
source = source.decode("ascii")
try:
peer_device = namespace.lookup_symbol(namespace.normalize_namepath(source), device_path).name
device_deps[device_path][DEP_TYPE_CONSUMES].add(peer_device)
device_deps[peer_device][DEP_TYPE_PROVIDES].add(device_path)
except:
pass
except NotImplementedError as e:
logging.info(f"{device_path}.{obj_name}: will not be added to vACPI, reason: {str(e)}")
def fetch_device_info(devices_node, interpreter, namepath):
logging.info(f"Fetch information about device object {namepath}")
@ -141,7 +395,7 @@ def fetch_device_info(devices_node, interpreter, namepath):
sta = result.get()
if sta & 0x1 == 0:
return
add_object_to_device(interpreter.context, namepath, "_STA", result)
add_object_to_device(interpreter, namepath, "_STA", result)
# Hardware ID
hid = ""
@ -158,7 +412,7 @@ def fetch_device_info(devices_node, interpreter, namepath):
hid = hex(hid)
else:
hid = "<unknown>"
add_object_to_device(interpreter.context, namepath, "_HID", result)
add_object_to_device(interpreter, namepath, "_HID", result)
# Compatible ID
cids = []
@ -193,14 +447,14 @@ def fetch_device_info(devices_node, interpreter, namepath):
result = interpreter.interpret_method_call(f"{namepath}._UID")
uid = result.get()
add_child(element, "acpi_uid", str(uid))
add_object_to_device(interpreter.context, namepath, "_UID", result)
add_object_to_device(interpreter, namepath, "_UID", result)
# Description
if interpreter.context.has_symbol(f"{namepath}._STR"):
result = interpreter.interpret_method_call(f"{namepath}._STR")
desc = result.get().decode(encoding="utf-16").strip("\00")
element.set("description", desc)
add_object_to_device(interpreter.context, namepath, "_STR", result)
add_object_to_device(interpreter, namepath, "_STR", result)
# Address
if interpreter.context.has_symbol(f"{namepath}._ADR"):
@ -212,7 +466,7 @@ def fetch_device_info(devices_node, interpreter, namepath):
logging.info(f"{namepath} has siblings with duplicated address {adr}.")
else:
element.set("address", hex(adr) if isinstance(adr, int) else adr)
add_object_to_device(interpreter.context, namepath, "_ADR", result)
add_object_to_device(interpreter, namepath, "_ADR", result)
# Status
if sta is not None:
@ -235,7 +489,7 @@ def fetch_device_info(devices_node, interpreter, namepath):
else:
add_child(element, "resource", type=item.__class__.__name__, id=f"res{idx}")
add_object_to_device(interpreter.context, namepath, "_CRS", result)
add_object_to_device(interpreter, namepath, "_CRS", result)
# PCI interrupt routing
if interpreter.context.has_symbol(f"{namepath}._PRT"):
@ -304,4 +558,12 @@ def extract(board_etree):
builder.TermList(*list(objs.values())))
add_child(element, "aml_template", visitor.generate(tree).hex())
for dev, deps in device_deps.items():
element = get_node(devices_node, f"//device[acpi_object='{dev}']")
if element is not None:
for kind, targets in deps.items():
for target in targets:
if dev != target:
add_child(element, "dependency", target, type=kind)
advanced = True

View File

@ -24,7 +24,7 @@ def getkey(child):
tags = ["vendor", "identifier", "subsystem_vendor", "subsystem_identifier", "class",
"acpi_object", "compatible_id", "acpi_uid", "aml_template", "status",
"resource", "capability", "interrupt_pin_routing", "bus", "device"]
"resource", "capability", "interrupt_pin_routing", "dependency", "bus", "device"]
if child.tag == "resource":
return (tags.index(child.tag), child.get("type"), resource_subkey(child))