# -*- coding: utf-8 -*-
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
from typing import Dict, Union, List, Tuple
from crash.util import array_size, struct_has_member
from crash.util.symbols import Types, Symvals, MinimalSymvals
from crash.util.symbols import MinimalSymbolCallbacks, SymbolCallbacks
from crash.types.list import list_for_each_entry
from crash.types.module import for_each_module
from crash.exceptions import DelayedAttributeError, InvalidArgumentError
from crash.types.page import Page
from crash.types.cpu import highest_possible_cpu_nr
import gdb
SymbolOrValue = Union[gdb.Value, gdb.Symbol]
[docs]class PerCPUError(TypeError):
"""The passed object does not respond to a percpu pointer."""
_fmt = "{} does not correspond to a percpu pointer."
def __init__(self, var: SymbolOrValue) -> None:
super().__init__(self._fmt.format(var))
types = Types(['void *', 'char *', 'struct pcpu_chunk',
'struct percpu_counter'])
symvals = Symvals(['__per_cpu_offset', 'pcpu_base_addr', 'pcpu_slot',
'pcpu_nr_slots', 'pcpu_group_offsets'])
msymvals = MinimalSymvals(['__per_cpu_start', '__per_cpu_end'])
[docs]class PerCPUState:
"""
Per-cpus come in a few forms:
- "Array" of objects
- "Array" of pointers to objects
- Pointers to either of those
If we want to get the typing right, we need to recognize each one
and figure out what type to pass back. We do want to dereference
pointer to a percpu but we don't want to dereference a percpu
pointer.
"""
_dynamic_offset_cache: List[Tuple[int, int]] = list()
_static_ranges: Dict[int, int] = dict()
_module_ranges: Dict[int, int] = dict()
_last_cpu = -1
_nr_cpus = 0
[docs] @classmethod
# pylint: disable=unused-argument
def setup_per_cpu_size(cls, unused: gdb.Symbol) -> None:
try:
size = msymvals['__per_cpu_end'] - msymvals['__per_cpu_start']
except DelayedAttributeError:
pass
cls._static_ranges[0] = size
if msymvals['__per_cpu_start'] != 0:
cls._static_ranges[msymvals['__per_cpu_start']] = size
try:
# This is only an optimization so we don't return NR_CPUS values
# when there are far fewer CPUs on the system.
cls._last_cpu = highest_possible_cpu_nr()
except DelayedAttributeError:
pass
[docs] @classmethod
# pylint: disable=unused-argument
def setup_nr_cpus(cls, unused: gdb.Symbol) -> None:
cls._nr_cpus = array_size(symvals['__per_cpu_offset'])
if cls._last_cpu == -1:
cls._last_cpu = cls._nr_cpus
[docs] @classmethod
# pylint: disable=unused-argument
def setup_module_ranges(cls, unused: gdb.Symbol) -> None:
for module in for_each_module():
start = int(module['percpu'])
if start == 0:
continue
size = int(module['percpu_size'])
cls._module_ranges[start] = size
def _add_to_offset_cache(self, base: int, start: int, end: int) -> None:
self._dynamic_offset_cache.append((base + start, base + end))
[docs] @classmethod
def dump_ranges(cls) -> None:
"""
Dump all percpu ranges to stdout
"""
for (start, size) in cls._static_ranges.items():
print(f"static start={start:#x}, size={size:#x}")
for (start, size) in cls._module_ranges.items():
print(f"module start={start:#x}, size={size:#x}")
for (start, end) in cls._dynamic_offset_cache:
print(f"dynamic start={start:#x}, end={end:#x}")
def _setup_dynamic_offset_cache_area_map(self, chunk: gdb.Value) -> None:
used_is_negative = None
chunk_base = int(chunk["base_addr"]) - int(symvals.pcpu_base_addr)
off = 0
start = None
_map = chunk['map']
map_used = int(chunk['map_used'])
# Prior to 3.14 commit 723ad1d90b56 ("percpu: store offsets
# instead of lengths in ->map[]"), negative values in map
# meant the area is used, and the absolute value is area size.
# After the commit, the value is area offset for unused, and
# offset | 1 for used (all offsets have to be even). The value
# at index 'map_used' is a 'sentry' which is the total size |
# 1. There is no easy indication of whether kernel includes
# the commit, unless we want to rely on version numbers and
# risk breakage in case of backport to older version. Instead
# employ a heuristic which scans the first chunk, and if no
# negative value is found, assume the kernel includes the
# commit.
if used_is_negative is None:
used_is_negative = False
for i in range(map_used):
val = int(_map[i])
if val < 0:
used_is_negative = True
break
if used_is_negative:
for i in range(map_used):
val = int(_map[i])
if val < 0:
if start is None:
start = off
else:
if start is not None:
self._add_to_offset_cache(chunk_base, start, off)
start = None
off += abs(val)
if start is not None:
self._add_to_offset_cache(chunk_base, start, off)
else:
for i in range(map_used):
off = int(_map[i])
if off & 1 == 1:
off -= 1
if start is None:
start = off
else:
if start is not None:
self._add_to_offset_cache(chunk_base, start, off)
start = None
if start is not None:
off = int(_map[map_used]) - 1
self._add_to_offset_cache(chunk_base, start, off)
def _setup_dynamic_offset_cache_bitmap(self, chunk: gdb.Value) -> None:
size_in_bytes = int(chunk['nr_pages']) * Page.PAGE_SIZE
chunk_base = int(chunk["base_addr"]) - int(symvals.pcpu_base_addr)
self._add_to_offset_cache(chunk_base, 0, size_in_bytes)
def _setup_dynamic_offset_cache(self) -> None:
# TODO: interval tree would be more efficient, but this adds no 3rd
# party module dependency...
use_area_map = struct_has_member(types.pcpu_chunk_type, 'map')
for slot in range(symvals.pcpu_nr_slots):
for chunk in list_for_each_entry(symvals.pcpu_slot[slot],
types.pcpu_chunk_type, 'list'):
if use_area_map:
self._setup_dynamic_offset_cache_area_map(chunk)
else:
self._setup_dynamic_offset_cache_bitmap(chunk)
def _is_percpu_var_dynamic(self, var: int) -> bool:
try:
if not self._dynamic_offset_cache:
self._setup_dynamic_offset_cache()
# TODO: we could sort the list...
for (start, end) in self._dynamic_offset_cache:
if start <= var < end:
return True
except DelayedAttributeError:
# This can happen with the testcases or in kernels prior to 2.6.30
pass
return False
# The resolved percpu address
def _is_static_percpu_address(self, addr: int) -> bool:
for start in self._static_ranges:
size = self._static_ranges[start]
for cpu in range(0, self._last_cpu):
offset = int(symvals['__per_cpu_offset'][cpu]) + start
if offset <= addr < offset + size:
return True
return False
# The percpu virtual address
[docs] def is_static_percpu_var(self, addr: int) -> bool:
"""
Returns whether the provided address is within the bounds of
the percpu static ranges
Args:
addr: The address to query
Returns:
:obj:`bool`: Whether this address belongs to a static range
"""
for start in self._static_ranges:
size = self._static_ranges[start]
if start <= addr < start + size:
return True
return False
# The percpu range should start at offset 0 but gdb relocation
# treats 0 as a special value indicating it should just be after
# the previous section. It's possible to override this while
# loading debuginfo but not when debuginfo is embedded.
def _relocated_offset(self, var: gdb.Value) -> int:
addr = int(var)
start = msymvals['__per_cpu_start']
size = self._static_ranges[start]
if start <= addr < start + size:
return addr - start
return addr
[docs] def is_module_percpu_var(self, addr: int) -> bool:
"""
Returns whether the provided value or symbol falls within
any of the percpu ranges for modules
Args:
addr: The address to query
Returns:
:obj:`bool`: Whether this address belongs to a module range
"""
for start in self._module_ranges:
size = self._module_ranges[start]
if start <= addr < start + size:
return True
return False
[docs] def is_percpu_var(self, var: SymbolOrValue) -> bool:
"""
Returns whether the provided value or symbol falls within
any of the percpu ranges
Args:
var: The symbol or value to query
Returns:
:obj:`bool`: Whether the value belongs to any percpu range
"""
if isinstance(var, gdb.Symbol):
var = var.value().address
ivar = int(var)
if self.is_static_percpu_var(ivar):
return True
if self.is_module_percpu_var(ivar):
return True
if self._is_percpu_var_dynamic(ivar):
return True
return False
def _resolve_percpu_var(self, symvar: SymbolOrValue) -> gdb.Value:
orig_var = symvar
if isinstance(symvar, gdb.Symbol):
var = symvar.value()
else:
var = symvar
if not isinstance(var, gdb.Value):
raise InvalidArgumentError("Argument must be gdb.Symbol or gdb.Value")
if var.type.code == gdb.TYPE_CODE_PTR:
# The percpu contains pointers
if var.address is not None and self.is_percpu_var(var.address):
var = var.address
# Pointer to a percpu
elif self.is_percpu_var(var):
if var.type != types.void_p_type:
var = var.dereference().address
assert self.is_percpu_var(var)
else:
raise PerCPUError(orig_var)
# object is a percpu
elif self.is_percpu_var(var.address):
var = var.address
else:
raise PerCPUError(orig_var)
return var
def _get_percpu_var(self, symvar: SymbolOrValue, cpu: int) -> gdb.Value:
if isinstance(symvar, (gdb.Symbol, gdb.MinSymbol)):
var = symvar.value()
else:
var = symvar
if not isinstance(var, gdb.Value):
raise InvalidArgumentError("Argument must be gdb.Symbol or gdb.Value")
if cpu < 0:
raise ValueError("cpu must be >= 0")
addr = symvals['__per_cpu_offset'][cpu]
if addr > 0:
addr += self._relocated_offset(var)
val = gdb.Value(addr).cast(var.type)
if var.type != types.void_p_type:
val = val.dereference()
return val
[docs] def get_percpu_var(self, var: SymbolOrValue, cpu: int) -> gdb.Value:
"""
Retrieve a per-cpu variable for one or all CPUs
Args:
var: The symbol or value to use to resolve the percpu location
cpu: The cpu for which to return the per-cpu value.
Returns:
:obj:`gdb.Value`: The value corresponding to the specified CPU.
The value is of the same type passed via var.
Raises:
:obj:`.InvalidArgumentError`: var is not :obj:`gdb.Symbol` or
:obj:`gdb.Value`
:obj:`.PerCPUError`: var does not fall into any percpu range
:obj:`ValueError`: cpu is less than ``0``
"""
var = self._resolve_percpu_var(var)
return self._get_percpu_var(var, cpu)
[docs] def get_percpu_vars(self, var: SymbolOrValue,
nr_cpus: int = None) -> Dict[int, gdb.Value]:
"""
Retrieve a per-cpu variable for all CPUs
Args:
var: The symbol or value to use to resolve the percpu location
nr_cpus (optional): The number of CPUs for which to return results
``None`` (or unspecified) will use the highest possible
CPU count.
Returns:
:obj:`dict`(:obj:`int`, :obj:`gdb.Value`): The values corresponding
to every CPU in a dictionary indexed by CPU number. The type of the
:obj:`gdb.Value` used as the :obj:`dict` value is the same type as
the :obj:`gdb.Value` or :obj:`gdb.Symbol` passed as var.
Raises:
:obj:`.InvalidArgumentError`: var is not :obj:`gdb.Symbol` or
:obj:`gdb.Value`
:obj:`.PerCPUError`: var does not fall into any percpu range
:obj:`ValueError`: nr_cpus is <= ``0``
"""
if nr_cpus is None:
nr_cpus = self._last_cpu
if nr_cpus <= 0:
raise ValueError("nr_cpus must be > 0")
vals = dict()
var = self._resolve_percpu_var(var)
for cpu in range(0, nr_cpus):
vals[cpu] = self._get_percpu_var(var, cpu)
return vals
msym_cbs = MinimalSymbolCallbacks([('__per_cpu_start',
PerCPUState.setup_per_cpu_size),
('__per_cpu_end',
PerCPUState.setup_per_cpu_size)])
symbol_cbs = SymbolCallbacks([('__per_cpu_offset', PerCPUState.setup_nr_cpus),
('modules', PerCPUState.setup_module_ranges)])
_state = PerCPUState()
[docs]def is_percpu_var(var: SymbolOrValue) -> bool:
"""
Returns whether the provided value or symbol falls within
any of the percpu ranges
Args:
var: The symbol or value to query
Returns:
:obj:`bool`: Whether the value belongs to any percpu range
"""
return _state.is_percpu_var(var)
[docs]def get_percpu_var(var: SymbolOrValue, cpu: int) -> gdb.Value:
"""
Retrieve a per-cpu variable for a single CPU
Args:
var: The symbol or value to use to resolve the percpu location
cpu: The cpu for which to return the per-cpu value.
Returns:
:obj:`gdb.Value`: The value corresponding to the specified CPU.
The value is of the same type passed via var.
Raises:
:obj:`.InvalidArgumentError`: var is not :obj:`gdb.Symbol`
or :obj:`gdb.Value`
:obj:`.PerCPUError`: var does not fall into any percpu range
:obj:`ValueError`: cpu is less than ``0``
"""
return _state.get_percpu_var(var, cpu)
[docs]def get_percpu_vars(var: SymbolOrValue,
nr_cpus: int = None) -> Dict[int, gdb.Value]:
"""
Retrieve a per-cpu variable for all CPUs
Args:
var: The symbol or value to use to resolve the percpu location.
nr_cpus (optional): The number of CPUs for which to return results.
``None`` (or unspecified) will use the highest possible
CPU count.
Returns:
:obj:`dict`(:obj:`int`, :obj:`gdb.Value`): The values corresponding
to every CPU in a dictionary indexed by CPU number. The type of the
:obj:`gdb.Value` used as the :obj:`dict` value is the same type as
the :obj:`gdb.Value` or :obj:`gdb.Symbol` passed as var.
Raises:
:obj:`.InvalidArgumentError`: var is not :obj:`gdb.Symbol`
or :obj:`gdb.Value`
:obj:`.PerCPUError`: var does not fall into any percpu range
:obj:`ValueError`: nr_cpus is <= ``0``
"""
return _state.get_percpu_vars(var, nr_cpus)
[docs]def percpu_counter_sum(var: SymbolOrValue) -> int:
"""
Returns the sum of a percpu counter
Args:
var: The percpu counter to sum. The value must be of type
``struct percpu_counter``.
Returns:
:obj:`int`: the sum of all components of the percpu counter
"""
if isinstance(var, gdb.Symbol):
var = var.value()
if not (var.type == types.percpu_counter_type or
(var.type.code == gdb.TYPE_CODE_PTR and
var.type.target() == types.percpu_counter_type)):
raise InvalidArgumentError("var must be gdb.Symbol or gdb.Value describing `{}' not `{}'"
.format(types.percpu_counter_type, var.type))
total = int(var['count'])
v = get_percpu_vars(var['counters'])
for cpu in v:
total += int(v[cpu])
return total