# -*- coding: utf-8 -*-
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
from typing import Iterator, Callable, Dict, List
from crash.exceptions import InvalidArgumentError, ArgumentTypeError
from crash.exceptions import UnexpectedGDBTypeError
from crash.util import array_size, struct_has_member
from crash.util.symbols import Types, Symvals, SymbolCallbacks
from crash.types.list import list_for_each_entry
import gdb
PF_EXITING = 0x4
types = Types(['struct task_struct', 'struct mm_struct', 'atomic_long_t'])
symvals = Symvals(['init_task', 'init_mm'])
# This is pretty painful. These are all #defines so none of them end
# up with symbols in the kernel. The best approximation we have is
# task_state_array which doesn't include all of them. All we can do
# is make some assumptions based on the changes upstream. This will
# be fragile.
[docs]class TaskStateFlags:
"""
A class to contain state related to discovering task flag values.
Not meant to be instantiated.
The initial values below are overridden once symbols are available to
resolve them properly.
"""
TASK_RUNNING = 0
TASK_FLAG_UNINITIALIZED = -1
TASK_INTERRUPTIBLE: int = TASK_FLAG_UNINITIALIZED
TASK_UNINTERRUPTIBLE: int = TASK_FLAG_UNINITIALIZED
TASK_STOPPED: int = TASK_FLAG_UNINITIALIZED
EXIT_ZOMBIE: int = TASK_FLAG_UNINITIALIZED
TASK_DEAD: int = TASK_FLAG_UNINITIALIZED
EXIT_DEAD: int = TASK_FLAG_UNINITIALIZED
TASK_SWAPPING: int = TASK_FLAG_UNINITIALIZED
TASK_TRACING_STOPPED: int = TASK_FLAG_UNINITIALIZED
TASK_WAKEKILL: int = TASK_FLAG_UNINITIALIZED
TASK_WAKING: int = TASK_FLAG_UNINITIALIZED
TASK_PARKED: int = TASK_FLAG_UNINITIALIZED
__TASK_IDLE: int = TASK_FLAG_UNINITIALIZED
TASK_EXCLUSIVE: int = TASK_FLAG_UNINITIALIZED
TASK_NOLOAD: int = TASK_FLAG_UNINITIALIZED
TASK_NEW: int = TASK_FLAG_UNINITIALIZED
TASK_IDLE: int = TASK_FLAG_UNINITIALIZED
def __init__(self) -> None:
raise NotImplementedError("This class is not meant to be instantiated")
[docs] @classmethod
def has_flag(cls, flagname: str) -> bool:
v = getattr(cls, flagname)
return v != cls.TASK_FLAG_UNINITIALIZED
[docs] @classmethod
def task_state_flags_callback(cls, symbol: gdb.Symbol) -> None:
# pylint: disable=unused-argument
"""
Detect which task flags this kernel uses.
Meant to be used as a SymbolCallback.
Different kernels use different task flags or even different values
for the same flags. This method tries to determine the flags for
the kernel.
Args:
symbol: The ``task_state_array`` symbol.
"""
task_state_array = symbol.value()
count = array_size(task_state_array)
bit = 0
for i in range(count):
state = task_state_array[i].string()
state_strings = {
'(running)' : 'TASK_RUNNING',
'(sleeping)' : 'TASK_INTERRUPTIBLE',
'(disk sleep)' : 'TASK_UNINTERRUPTIBLE',
'(stopped)' : 'TASK_STOPPED',
'(zombie)' : 'EXIT_ZOMBIE',
'x (dead)' : 'TASK_DEAD',
'X (dead)' : 'EXIT_DEAD',
'(swapping)' : 'TASK_SWAPPING',
'(tracing stop)' : 'TASK_TRACING_STOPPED',
'(wakekill)' : 'TASK_WAKEKILL',
'(waking)' : 'TASK_WAKING',
'(parked)' : 'TASK_PARKED',
'(idle)' : '__TASK_IDLE',
}
for key in state_strings:
if key in state:
setattr(cls, state_strings[key], bit)
if bit == 0:
bit = 1
else:
bit <<= 1
# Linux 4.14 re-introduced TASK_PARKED into task_state_array
# which renumbered some bits
if cls.has_flag('TASK_PARKED') and not cls.has_flag('TASK_DEAD'):
newbits = cls.TASK_PARKED << 1
cls.TASK_DEAD = newbits
cls.TASK_WAKEKILL = newbits << 1
cls.TASK_WAKING = newbits << 2
cls.TASK_NOLOAD = newbits << 3
cls.TASK_NEW = newbits << 4
assert cls.TASK_PARKED == 0x0040
assert cls.TASK_DEAD == 0x0080
assert cls.TASK_WAKEKILL == 0x0100
assert cls.TASK_WAKING == 0x0200
# Linux 3.14 removed several elements from task_state_array
# so we'll have to make some assumptions.
# TASK_NOLOAD wasn't introduced until 4.2 and wasn't added
# to task_state_array until v4.14. There's no way to
# detect whether the use of the flag is valid for a particular
# kernel release.
elif cls.has_flag('EXIT_DEAD'):
if cls.EXIT_ZOMBIE > cls.EXIT_DEAD:
newbits = cls.EXIT_ZOMBIE << 1
else:
newbits = cls.EXIT_DEAD << 1
cls.TASK_DEAD = newbits
cls.TASK_WAKEKILL = newbits << 1
cls.TASK_WAKING = newbits << 2
cls.TASK_PARKED = newbits << 3
cls.TASK_NOLOAD = newbits << 4
cls.TASK_NEW = newbits << 5
assert cls.TASK_DEAD == 0x0040
assert cls.TASK_WAKEKILL == 0x0080
assert cls.TASK_WAKING == 0x0100
assert cls.TASK_PARKED == 0x0200
else:
assert cls.TASK_DEAD == 64
assert cls.TASK_WAKEKILL == 128
assert cls.TASK_WAKING == 256
assert cls.TASK_PARKED == 512
if cls.has_flag('TASK_NOLOAD'):
assert cls.TASK_NOLOAD == 1024
cls.TASK_IDLE = cls.TASK_NOLOAD | cls.TASK_UNINTERRUPTIBLE
assert cls.TASK_IDLE == 1026
if cls.has_flag('TASK_NEW'):
assert cls.TASK_NEW == 2048
cls._check_state_bits()
@classmethod
def _check_state_bits(cls) -> None:
required = [
'TASK_RUNNING',
'TASK_INTERRUPTIBLE',
'TASK_UNINTERRUPTIBLE',
'EXIT_ZOMBIE',
'TASK_STOPPED',
]
missing = []
for bit in required:
if not cls.has_flag(bit):
missing.append(bit)
if missing:
raise RuntimeError("Missing required task states: {}"
.format(",".join(missing)))
symbol_cbs = SymbolCallbacks([('task_state_array',
TaskStateFlags.task_state_flags_callback)])
TF = TaskStateFlags
[docs]class LinuxTask:
"""
A wrapper class for ``struct task_struct``. There will be typically
one of these allocated for every task discovered in the debugging
environment.
Args:
task_struct: The task to wrap. The value must be of type
``struct task_struct``.
Attributes:
task_struct (:obj:`gdb.Value`): The task being wrapped. The value
is of type ``struct task_struct``.
active (:obj:`bool`): Whether this task is active
cpu (:obj:`int`): The CPU number the task was using
regs: The registers associated with this task, if active
thread_info (:obj:`gdb.Value`): The architecture-specific
``struct thread_info`` for this task. The value will be of
type ``struct thread_info``.
thread (:obj:`gdb.InferiorThread`): The GDB representation of the
thread.
mem_valid (:obj:`bool`): Whether the memory statistics are currently
valid.
rss (:obj:`int`): The size of the resident memory for this task.
total_vm (:obj:`int`): The total size of the vm space for this task.
pgd_addr (:obj:`int`): The address of the top of the page table tree.
Raises:
:obj:`.ArgumentTypeError`: task_struct was not a :obj:`gdb.Value`.
:obj:`.UnexpectedGDBTypeError`: task_struct was not of type
``struct task_struct``.
:obj:`.InvalidArgumentError`: The cpu number was not ``None`` or an
:obj:`int`.
"""
_valid = False
_task_state_has_exit_state = None
_anon_file_rss_fields: List[str] = list()
# Version-specific hooks -- these will be None here but we'll raise a
# NotImplementedError if any of them aren't found.
_get_rss: Callable[['LinuxTask'], int]
_get_last_run: Callable[['LinuxTask'], int]
def __init__(self, task_struct: gdb.Value) -> None:
self._init_task_types(task_struct)
if not isinstance(task_struct, gdb.Value):
raise ArgumentTypeError('task_struct', task_struct, gdb.Value)
if not (task_struct.type == types.task_struct_type or
task_struct.type == types.task_struct_type.pointer()):
raise UnexpectedGDBTypeError('task_struct', task_struct,
types.task_struct_type)
self.task_struct = task_struct
self.active = False
self.cpu = -1
self.regs: Dict[str, int] = dict()
self.thread_info: gdb.Value
self.thread: gdb.InferiorThread
# mem data
self.mem_valid = False
self.rss = 0
self.total_vm = 0
self.pgd_addr = 0
@classmethod
def _init_task_types(cls, task: gdb.Value) -> None:
if not cls._valid:
t = types.task_struct_type
if task.type != t:
raise UnexpectedGDBTypeError('task', task, t)
# Using a type within the same context makes things a *lot* faster
# This works around a shortcoming in gdb. A type lookup and
# a type resolved from a symbol will be different structures
# within gdb. Equality requires a deep comparison rather than
# a simple pointer comparison.
types.override('struct task_struct', task.type)
fields = types.task_struct_type.fields()
cls._task_state_has_exit_state = 'exit_state' in fields
cls._pick_get_rss()
cls._pick_last_run()
cls._valid = True
[docs] def set_active(self, cpu: int, regs: Dict[str, int]) -> None:
"""
Set this task as active in the debugging environment
Args:
cpu: Which CPU this task was using
regs: The registers associated with this task
Raises:
:obj:`.InvalidArgumentError`: The cpu was not a valid integer.
"""
if not (isinstance(cpu, int) and cpu >= 0):
raise InvalidArgumentError("cpu must be integer >= 0")
self.active = True
self.cpu = cpu
self.regs = regs
[docs] def attach_thread(self, thread: gdb.InferiorThread) -> None:
"""
Associate a gdb thread with this task
Args:
thread: The gdb thread to associate with this task
"""
if not isinstance(thread, gdb.InferiorThread):
raise TypeError("Expected gdb.InferiorThread")
self.thread = thread
[docs] def set_thread_info(self, thread_info: gdb.Value) -> None:
"""
Set the thread info for this task
The thread info structure is architecture specific. This method
allows the architecture code to assign its thread info structure
to this task.
Args:
thread_info: The ``struct thread_info`` to be associated with
this task. The value must be of type ``struct thread_info``.
"""
self.thread_info = thread_info
[docs] def get_thread_info(self) -> gdb.Value:
"""
Get the thread info for this task
The thread info structure is architecture specific and so this
method abstracts its retreival.
Returns:
:obj:`gdb.Value`: The struct thread_info associated with this
task. The type of the value is ``struct thread_info``.
"""
return self.thread_info
[docs] def get_last_cpu(self) -> int:
"""
Returns the last cpu this task was scheduled to execute on
Returns:
:obj:`int`: The last cpu this task was scheduled to execute on
"""
if struct_has_member(self.task_struct, 'cpu'):
cpu = self.task_struct['cpu']
else:
cpu = self.thread_info['cpu']
return int(cpu)
# Hrm. This seems broken since we're combining flags from
# two fields.
[docs] def task_state(self) -> int:
"""
Return the task state flags for this task *(possibly broken due to
combining flags from ``state`` and ``exit_state``)*.
Returns:
:obj:`int`: The state flags for this task.
"""
state = int(self.task_struct['state'])
if self._task_state_has_exit_state:
state |= int(self.task_struct['exit_state'])
return state
[docs] def maybe_dead(self) -> bool:
"""
Returns whether this task is dead
Returns:
:obj:`bool`: Whether this task is dead
"""
state = self.task_state()
known = TF.TASK_INTERRUPTIBLE
known |= TF.TASK_UNINTERRUPTIBLE
known |= TF.EXIT_ZOMBIE
known |= TF.TASK_STOPPED
if TF.has_flag('TASK_SWAPPING'):
known |= TF.TASK_SWAPPING
return (state & known) == 0
[docs] def task_flags(self) -> int:
"""
Returns the flags for this task
Returns:
:obj:`int`: The flags for this task
"""
return int(self.task_struct['flags'])
[docs] def is_exiting(self) -> bool:
"""
Returns whether a task is exiting
Returns:
:obj:`bool`: Whether the task is exiting
"""
return (self.task_flags() & PF_EXITING) != 0
[docs] def is_zombie(self) -> bool:
"""
Returns whether a task is in Zombie state
Returns:
:obj:`bool`: Whether the task is in zombie state
"""
return (self.task_state() & TF.EXIT_ZOMBIE) != 0
[docs] def is_thread_group_leader(self) -> bool:
"""
Returns whether a task is a thread group leader
Returns:
:obj:`bool`: Whether the task is a thread group leader
"""
return int(self.task_struct['exit_signal']) >= 0
[docs] def update_mem_usage(self) -> None:
"""
Update the memory usage for this task
Tasks are created initially without their memory statistics. This
method explicitly updates them.
"""
if self.mem_valid:
return
if self.is_zombie() or self.is_exiting():
return
mm = self.task_struct['mm']
if not mm:
self.mem_valid = True
return
self.rss = self.get_rss()
self.total_vm = int(mm['total_vm'])
self.pgd_addr = int(mm['pgd'])
self.mem_valid = True
[docs] def task_name(self, brackets: bool = False) -> str:
"""
Returns the ``comm`` field of this task
Args:
brackets: If this task is a kernel thread, surround the name
in square brackets
Returns:
:obj:`str`: The ``comm`` field of this task a python string
"""
name = self.task_struct['comm'].string()
if brackets and self.is_kernel_task():
return f"[{name}]"
return name
[docs] def task_pid(self) -> int:
"""
Returns the pid of this task
Returns:
:obj:`int`: The pid of this task
"""
return int(self.task_struct['pid'])
[docs] def parent_pid(self) -> int:
"""
Returns the pid of this task's parent
Returns:
:obj:`int`: The pid of this task's parent
"""
return int(self.task_struct['parent']['pid'])
[docs] def task_address(self) -> int:
"""
Returns the address of the task_struct for this task
Returns:
:obj:`int`: The address of the task_struct
"""
return int(self.task_struct.address)
[docs] def is_kernel_task(self) -> bool:
if self.task_struct['pid'] == 0:
return True
if self.is_zombie() or self.is_exiting():
return False
mm = self.task_struct['mm']
if mm == 0:
return True
if symvals.init_mm and mm == symvals.init_mm.address:
return True
return False
[docs] @classmethod
def set_get_stack_pointer(cls, fn: Callable[[gdb.Value], int]) -> None:
"""
Set the stack pointer callback for this architecture
The callback must accept a :obj:`gdb.Value` of type
``struct thread`` and return a :obj:`int` containing the address
of the stack pointer.
Args:
fn: The callback to use. It will be used by all tasks.
"""
setattr(cls, '_get_stack_pointer_fn', fn)
[docs] def get_stack_pointer(self) -> int:
"""
Get the stack pointer for this task
Returns:
:obj:`int`: The address of the stack pointer for this task.
Raises:
:obj:`NotImplementedError`: The architecture hasn't provided
a stack pointer callback.
"""
try:
fn = getattr(self, '_get_stack_pointer_fn')
except AttributeError:
raise NotImplementedError("Architecture hasn't provided stack pointer callback")
return fn(self.task_struct['thread'])
def _get_rss_field(self) -> int:
return int(self.task_struct['mm']['rss'].value())
def _get__rss_field(self) -> int:
return int(self.task_struct['mm']['_rss'].value())
def _get_rss_stat_field(self) -> int:
stat = self.task_struct['mm']['rss_stat']['count']
rss = 0
for i in range(array_size(stat)):
rss += int(stat[i]['counter'])
return rss
def _get_anon_file_rss_fields(self) -> int:
mm = self.task_struct['mm']
rss = 0
for name in self._anon_file_rss_fields:
if mm[name].type == types.atomic_long_t_type:
rss += int(mm[name]['counter'])
else:
rss += int(mm[name])
return rss
# The Pythonic way to do this is by generating the LinuxTask class
# dynamically. We may do that eventually, but for now we can just
# select the proper function and assign it to the class.
@classmethod
def _pick_get_rss(cls) -> None:
if struct_has_member(types.mm_struct_type, 'rss'):
cls._get_rss = cls._get_rss_field
elif struct_has_member(types.mm_struct_type, '_rss'):
cls._get_rss = cls._get__rss_field
elif struct_has_member(types.mm_struct_type, 'rss_stat'):
cls._get_rss = cls._get_rss_stat_field
else:
if struct_has_member(types.mm_struct_type, '_file_rss'):
cls._anon_file_rss_fields.append('_file_rss')
if struct_has_member(types.mm_struct_type, '_anon_rss'):
cls._anon_file_rss_fields.append('_anon_rss')
cls._get_rss = cls._get_anon_file_rss_fields
if not cls._anon_file_rss_fields:
raise RuntimeError("No method to retrieve RSS from task found.")
def __get_rss(self) -> int:
raise NotImplementedError("_get_rss not implemented")
def _last_run__last_run(self) -> int:
return int(self.task_struct['last_run'])
def _last_run__timestamp(self) -> int:
return int(self.task_struct['timestamp'])
def _last_run__last_arrival(self) -> int:
return int(self.task_struct['sched_info']['last_arrival'])
@classmethod
def _pick_last_run(cls) -> None:
fields = types.task_struct_type.keys()
if ('sched_info' in fields and
'last_arrival' in types.task_struct_type['sched_info'].type.keys()):
cls._get_last_run = cls._last_run__last_arrival
elif 'last_run' in fields:
cls._get_last_run = cls._last_run__last_run
elif 'timestamp' in fields:
cls._get_last_run = cls._last_run__timestamp
else:
raise RuntimeError("No method to retrieve last run from task found.")
[docs] def last_run(self) -> int:
"""
The timestamp of when this task was last run
Returns:
:obj:`int`: The timestamp of when this task was last run
"""
return self._get_last_run()
[docs]def for_each_thread_group_leader() -> Iterator[gdb.Value]:
"""
Iterate the task list and yield each thread group leader
Yields:
:obj:`gdb.Value`: The next task on the list. The value is of
type ``struct task_struct``.
"""
task_list = symvals.init_task['tasks']
for task in list_for_each_entry(task_list, symvals.init_task.type,
'tasks', include_head=True):
yield task
[docs]def for_each_thread_in_group(task: gdb.Value) -> Iterator[gdb.Value]:
"""
Iterate a thread group leader's thread list and
yield each struct task_struct
Args:
task: The task_struct that is the thread group leader. The value
must be of type ``struct task_struct``.
Yields:
:obj:`gdb.Value`: The next task on the list. The value is of type
``struct task_struct``.
"""
thread_list = task['thread_group']
for thread in list_for_each_entry(thread_list, symvals.init_task.type,
'thread_group'):
yield thread
[docs]def for_each_all_tasks() -> Iterator[gdb.Value]:
"""
Iterate the task list and yield each task including any associated
thread tasks
Yields:
:obj:`gdb.Value`: The next task on the list. The value is of type
``struct task_struct``.
"""
for leader in for_each_thread_group_leader():
yield leader
for task in for_each_thread_in_group(leader):
yield task