Module pandare.extras.procTrace

Create a graph of which processes run/ran over time.

Multiple modes supported:

1) Run on a live PANDA guest with python-based analysis - use the snake_hook plugin user@host:~/panda/panda/plugins/proc_trace$ $(python3 -m pandare.qcows x86_64) -panda snak_hook:files=graph.py root@guest:~# whoami root@guest:~# ls (qemu) quit 2) Run on a PANDA recording with python-based analysis user@host:~/panda/panda/plugins/proc_trace$ $(python3 -m pandare.qcows x86_64) -panda snak_hook:files=graph.py -replay /path/to/recording

3) Run on a live/recorded PANDA guest with C++ data collection, then render graph with python user@host:~/panda/panda/plugins/proc_trace$ $(python3 -m pandare.qcows x86_64) -panda proc_trace -plog /tmp/graph.plog [-replay …] user@host:~/panda/panda/plugins/proc_trace$ python3 graph.py /tmp/graph.plog

Expand source code
#!/usr/bin/env python3
'''
Create a graph of which processes run/ran over time.

Multiple modes supported:

1) Run on a live PANDA guest with python-based analysis - use the snake_hook plugin
    user@host:~/panda/panda/plugins/proc_trace$ $(python3 -m pandare.qcows x86_64) -panda snak_hook:files=graph.py
    root@guest:~# whoami
    root@guest:~# ls
    (qemu) quit
2) Run on a PANDA recording with python-based analysis
    user@host:~/panda/panda/plugins/proc_trace$ $(python3 -m pandare.qcows x86_64) -panda snak_hook:files=graph.py -replay /path/to/recording

3) Run on a live/recorded PANDA guest with C++ data collection, then render graph with python
    user@host:~/panda/panda/plugins/proc_trace$ $(python3 -m pandare.qcows x86_64) -panda proc_trace -plog /tmp/graph.plog [-replay ...]
    user@host:~/panda/panda/plugins/proc_trace$ python3 graph.py /tmp/graph.plog
'''

from pandare import PyPlugin

def render_graph(procinfo, time_data, total_insns, n_cols=120, show_ranges=True, show_graph=True):
    col_size = total_insns / n_cols
    pids = set([x for x,y in time_data]) # really a list of (pid, tid) tuples
    merged = {} # pid: [(True, 100), False, 9999)

    for pid in pids:
        on_off_times = []

        off_count = 0

        for (pid2, block_c) in time_data:
            if pid2 == pid:
                # On!
                on_off_times.append((True, block_c))
            else:
                # Off
                on_off_times.append((False, block_c))

        merged[pid] = on_off_times

    # Render output: Stage 1 - PID -> procname details
    #   Count   Pid              Name/tid              Asid    First         Last
    #    297  1355   [bash find  / 1355]          3b14e000   963083  ->  7829616

    if show_ranges:
        print(" Ins.Count PID   TID  First       Last     Names")

        for (pid, tid) in sorted(procinfo, key=lambda v: procinfo[v]['count'], reverse=True):
            details = procinfo[(pid, tid)]
            names = ", ".join([x.decode() for x in details['names']])

            end = f"{details['last']:<8}" if details['last'] is not None else "N/A"
            print(f"{details['count']: >10} {pid:<5} {tid:<5}{details['first']:<8} -> {end} {names}")


    # Render output: Stage 2: ascii art
    if show_graph:
        ascii_art = {} # (pid, tid): art
        for (pid, tid), times in merged.items():
            row = ""
            pending = None
            queue = merged[(pid, tid)]
            # Consume data from pending+merged in chunks of col_size
            # e.g. col_size=10 (True, 8), (False, 1), (True, 10)
            # simplifies to {True:9, False:1} and adds (True:9) to pending

            for cur_col in range(n_cols):
                counted = 0
                on_count = 0
                off_count = 0
                #import ipdb
                while (counted < col_size and len(queue)): #or pending is not None:
                    if pending is not None:
                        (on_bool, cnt) = pending
                        pending = None
                    else:
                        old_len = len(queue)
                        (on_bool, cnt) = queue.pop(0)
                        assert(len(queue) < old_len), "pop don't happen"

                    if cnt > col_size-counted: #Hard case: count part, move remainder to pending
                        remainder = cnt - (col_size-counted)
                        cnt = col_size-counted # Maximum allowed now
                        pending = (on_bool, remainder)

                    assert(cnt <= col_size-counted) # Now it's (always) the easy case for what's left
                    if on_bool:
                        on_count += cnt
                    else:
                        off_count += cnt
                    counted += cnt

                # /while
                # Use on_count and off_count to determine how to label this cell
                density_map = " ▂▃▄▅▆▇"
                on_count / col_size

                idx = round((on_count/col_size)*(len(density_map)-1))
                if idx == 0 and on_count > 0:
                    c = '.' # If any code executed, mark it
                else:
                    c = density_map[idx]
                row += c

            ascii_art[(pid, tid)] = row

        # Render art
        print("PID  TID  | "+ "-"*(n_cols//2-4) + "HISTORY" + "-"*(n_cols//2-4) + "| NAMES")
        for (pid, tid) in sorted(ascii_art, key=lambda x: x[0]):
            row = ascii_art[(pid, tid)]
            details = procinfo[(pid, tid)]
            names = ", ".join([x.decode() for x in details['names']])
            print(f"{pid: <4} {tid: <4} |{row}| {names}")

class ProcGraph(PyPlugin):
    def __init__(self, panda):
        # Data collection
        self.procinfo = {} # PID: info
        self.time_data = [] # [(PID, #blocks)]
        self.total_insns = 0
        self.n_insns = 0
        self.last_pid = None
        self.show_ranges = not self.get_arg("hide_ranges")
        self.show_graph = not self.get_arg("hide_graph")
        self.panda = panda

        # config option: number of columns
        self.n_cols = self.get_arg("cols") or 120

        @panda.cb_start_block_exec
        def sbe(cpu, tb):
            self.n_insns += tb.icount
            self.total_insns += tb.icount

        @panda.ppp("osi", "on_task_change")
        def task_change(cpu):
            proc = panda.plugins['osi'].get_current_process(cpu)
            thread = panda.plugins['osi'].get_current_thread(cpu)

            if proc == panda.ffi.NULL:
                print(f"Warning: Unable to identify process at {self.n_insns}")
                return
            if thread == panda.ffi.NULL:
                print(f"Warning: Unable to identify thread at {self.n_insns}")
                return

            proc_key = (proc.pid, thread.tid)
            if proc_key not in self.procinfo:
                self.procinfo[proc_key] = {"names": set(), #"tids": set(),
                                      "first": self.total_insns, "last": None,
                                      "count": 0}

            name = panda.ffi.string(proc.name)  if proc.name != panda.ffi.NULL else "(error)"
            self.procinfo[proc_key]["names"].add(name)

            # Update insn count for last process and indicate it (maybe) ends at total_insns-1
            if self.last_pid:
                # count since we last ran is it's old end value, minus where it just ended
                self.procinfo[self.last_pid]["count"] += (self.total_insns-1) - self.procinfo[self.last_pid]["last"]  \
                                                if self.procinfo[self.last_pid]["last"] is not None \
                                                else (self.total_insns-1) - self.procinfo[self.last_pid]["first"]
                self.procinfo[self.last_pid]["last"] = self.total_insns-1

            self.last_pid = proc_key

            self.time_data.append((proc_key, self.n_insns))
            self.n_insns = 0

    def uninit(self):
        render_graph(self.procinfo, self.time_data, self.total_insns, n_cols=self.n_cols, show_ranges=self.show_ranges, show_graph=self.show_graph)

        # Fully reset state
        self.panda.disable_ppp("task_change")
        self.procinfo = {} # PID: info
        self.time_data = [] # [(PID, #blocks)]
        self.total_insns = 0
        self.n_insns = 0
        self.last_pid = None

if __name__ == '__main__':
    import sys
    import os
    from pandare.plog_reader import PLogReader

    if len(sys.argv) != 2:
        raise ValueError("Usage: graph.py [pandalog]")

    pandalog_path = sys.argv[1]

    if not os.path.isfile(pandalog_path):
        raise ValueError(f"Pandalog not found: {pandalog_path}")

    procinfo = {}
    time_data = []
    total_insns = None
    last_pid = None

    with PLogReader(pandalog_path) as plr:
        for msg in plr:
            if msg.HasField('proc_trace'):
                pt = msg.proc_trace
                proc_key = (pt.pid, pt.tid)
                if proc_key not in procinfo:
                    procinfo[proc_key] = {
                        "names": set(),
                        "first": pt.start_instr,
                        "last": None,
                        "count": 1 # Ensure last value shows up, even though we don't know when it ends
                    }
                name = pt.name.encode()
                procinfo[proc_key]["names"].add(name)

                if last_pid:
                    procinfo[last_pid]["count"] += pt.start_instr - (procinfo[last_pid]["last"] if procinfo[last_pid]["last"] is not None else 0)
                    procinfo[last_pid]["last"] = pt.start_instr
                    if not total_insns or pt.start_instr > total_insns:
                        total_insns = pt.start_instr

                last_pid = proc_key

                time_data.append((proc_key, pt.start_instr))

    # Now procinfo and time_data populated, can call original graph render logic
    total_insns += 1 # Ensure we count the last insn
    render_graph(procinfo, time_data, total_insns, n_cols=120)

Functions

def render_graph(procinfo, time_data, total_insns, n_cols=120, show_ranges=True, show_graph=True)
Expand source code
def render_graph(procinfo, time_data, total_insns, n_cols=120, show_ranges=True, show_graph=True):
    col_size = total_insns / n_cols
    pids = set([x for x,y in time_data]) # really a list of (pid, tid) tuples
    merged = {} # pid: [(True, 100), False, 9999)

    for pid in pids:
        on_off_times = []

        off_count = 0

        for (pid2, block_c) in time_data:
            if pid2 == pid:
                # On!
                on_off_times.append((True, block_c))
            else:
                # Off
                on_off_times.append((False, block_c))

        merged[pid] = on_off_times

    # Render output: Stage 1 - PID -> procname details
    #   Count   Pid              Name/tid              Asid    First         Last
    #    297  1355   [bash find  / 1355]          3b14e000   963083  ->  7829616

    if show_ranges:
        print(" Ins.Count PID   TID  First       Last     Names")

        for (pid, tid) in sorted(procinfo, key=lambda v: procinfo[v]['count'], reverse=True):
            details = procinfo[(pid, tid)]
            names = ", ".join([x.decode() for x in details['names']])

            end = f"{details['last']:<8}" if details['last'] is not None else "N/A"
            print(f"{details['count']: >10} {pid:<5} {tid:<5}{details['first']:<8} -> {end} {names}")


    # Render output: Stage 2: ascii art
    if show_graph:
        ascii_art = {} # (pid, tid): art
        for (pid, tid), times in merged.items():
            row = ""
            pending = None
            queue = merged[(pid, tid)]
            # Consume data from pending+merged in chunks of col_size
            # e.g. col_size=10 (True, 8), (False, 1), (True, 10)
            # simplifies to {True:9, False:1} and adds (True:9) to pending

            for cur_col in range(n_cols):
                counted = 0
                on_count = 0
                off_count = 0
                #import ipdb
                while (counted < col_size and len(queue)): #or pending is not None:
                    if pending is not None:
                        (on_bool, cnt) = pending
                        pending = None
                    else:
                        old_len = len(queue)
                        (on_bool, cnt) = queue.pop(0)
                        assert(len(queue) < old_len), "pop don't happen"

                    if cnt > col_size-counted: #Hard case: count part, move remainder to pending
                        remainder = cnt - (col_size-counted)
                        cnt = col_size-counted # Maximum allowed now
                        pending = (on_bool, remainder)

                    assert(cnt <= col_size-counted) # Now it's (always) the easy case for what's left
                    if on_bool:
                        on_count += cnt
                    else:
                        off_count += cnt
                    counted += cnt

                # /while
                # Use on_count and off_count to determine how to label this cell
                density_map = " ▂▃▄▅▆▇"
                on_count / col_size

                idx = round((on_count/col_size)*(len(density_map)-1))
                if idx == 0 and on_count > 0:
                    c = '.' # If any code executed, mark it
                else:
                    c = density_map[idx]
                row += c

            ascii_art[(pid, tid)] = row

        # Render art
        print("PID  TID  | "+ "-"*(n_cols//2-4) + "HISTORY" + "-"*(n_cols//2-4) + "| NAMES")
        for (pid, tid) in sorted(ascii_art, key=lambda x: x[0]):
            row = ascii_art[(pid, tid)]
            details = procinfo[(pid, tid)]
            names = ", ".join([x.decode() for x in details['names']])
            print(f"{pid: <4} {tid: <4} |{row}| {names}")

Classes

class ProcGraph (panda)

Base class which PyPANDA plugins should inherit. Subclasses may register callbacks using the provided panda object and use the PyPlugin APIs:

  • self.get_args or self.get_arg_bool to check argument values
  • self.ppp to interact with other PyPlugins via PPP interfaces
  • self.ppp_cb_boilerplate('cb_name') to register a ppp-style callback
  • self.ppp_run_cb('cb_name') to run a previously-registered ppp-style callback
  • @PyPlugin.ppp_export to mark a class method as ppp-exported

For more information, check out the pyplugin documentation.

Expand source code
class ProcGraph(PyPlugin):
    def __init__(self, panda):
        # Data collection
        self.procinfo = {} # PID: info
        self.time_data = [] # [(PID, #blocks)]
        self.total_insns = 0
        self.n_insns = 0
        self.last_pid = None
        self.show_ranges = not self.get_arg("hide_ranges")
        self.show_graph = not self.get_arg("hide_graph")
        self.panda = panda

        # config option: number of columns
        self.n_cols = self.get_arg("cols") or 120

        @panda.cb_start_block_exec
        def sbe(cpu, tb):
            self.n_insns += tb.icount
            self.total_insns += tb.icount

        @panda.ppp("osi", "on_task_change")
        def task_change(cpu):
            proc = panda.plugins['osi'].get_current_process(cpu)
            thread = panda.plugins['osi'].get_current_thread(cpu)

            if proc == panda.ffi.NULL:
                print(f"Warning: Unable to identify process at {self.n_insns}")
                return
            if thread == panda.ffi.NULL:
                print(f"Warning: Unable to identify thread at {self.n_insns}")
                return

            proc_key = (proc.pid, thread.tid)
            if proc_key not in self.procinfo:
                self.procinfo[proc_key] = {"names": set(), #"tids": set(),
                                      "first": self.total_insns, "last": None,
                                      "count": 0}

            name = panda.ffi.string(proc.name)  if proc.name != panda.ffi.NULL else "(error)"
            self.procinfo[proc_key]["names"].add(name)

            # Update insn count for last process and indicate it (maybe) ends at total_insns-1
            if self.last_pid:
                # count since we last ran is it's old end value, minus where it just ended
                self.procinfo[self.last_pid]["count"] += (self.total_insns-1) - self.procinfo[self.last_pid]["last"]  \
                                                if self.procinfo[self.last_pid]["last"] is not None \
                                                else (self.total_insns-1) - self.procinfo[self.last_pid]["first"]
                self.procinfo[self.last_pid]["last"] = self.total_insns-1

            self.last_pid = proc_key

            self.time_data.append((proc_key, self.n_insns))
            self.n_insns = 0

    def uninit(self):
        render_graph(self.procinfo, self.time_data, self.total_insns, n_cols=self.n_cols, show_ranges=self.show_ranges, show_graph=self.show_graph)

        # Fully reset state
        self.panda.disable_ppp("task_change")
        self.procinfo = {} # PID: info
        self.time_data = [] # [(PID, #blocks)]
        self.total_insns = 0
        self.n_insns = 0
        self.last_pid = None

Ancestors

Methods

def uninit(self)
Expand source code
def uninit(self):
    render_graph(self.procinfo, self.time_data, self.total_insns, n_cols=self.n_cols, show_ranges=self.show_ranges, show_graph=self.show_graph)

    # Fully reset state
    self.panda.disable_ppp("task_change")
    self.procinfo = {} # PID: info
    self.time_data = [] # [(PID, #blocks)]
    self.total_insns = 0
    self.n_insns = 0
    self.last_pid = None

Inherited members