llvm/cross-project-tests/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py

# DExTer : Debugging Experience Tester
# ~~~~~~   ~         ~~         ~   ~~
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

import sys
import os
import platform

from dex.debugger.DebuggerBase import DebuggerBase, watch_is_active
from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR
from dex.dextIR import ProgramState, StackFrame, SourceLocation
from dex.utils.Exceptions import DebuggerException, LoadDebuggerException
from dex.utils.ReturnCode import ReturnCode

if platform.system() == "Windows":
    # Don't load on linux; _load_interface will croak before any names are used.
    from . import setup
    from . import probe_process
    from . import breakpoint


class DbgEng(DebuggerBase):
    def __init__(self, context, *args):
        self.breakpoints = []
        self.running = False
        self.finished = False
        self.step_info = None
        super(DbgEng, self).__init__(context, *args)

    def _custom_init(self):
        try:
            res = setup.setup_everything(self.context.options.executable)
            self.client = res
            self.running = True
        except Exception as e:
            raise Exception("Failed to start debuggee: {}".format(e))

    def _custom_exit(self):
        setup.cleanup(self.client)

    def _load_interface(self):
        arch = platform.architecture()[0]
        machine = platform.machine()
        if arch == "32bit" and machine == "AMD64":
            # This python process is 32 bits, but is sitting on a 64 bit machine.
            # Bad things may happen, don't support it.
            raise LoadDebuggerException(
                "Can't run Dexter dbgeng on 32 bit python in a 64 bit environment"
            )

        if platform.system() != "Windows":
            raise LoadDebuggerException("DbgEng supports Windows only")

        # Otherwise, everything was imported earlier

    @classmethod
    def get_name(cls):
        return "dbgeng"

    @classmethod
    def get_option_name(cls):
        return "dbgeng"

    @property
    def frames_below_main(self):
        return []

    @property
    def version(self):
        # I don't believe there's a well defined DbgEng version, outside of the
        # version of Windows being used.
        return "1"

    def clear_breakpoints(self):
        for x in self.breakpoints:
            x.RemoveFlags(breakpoint.BreakpointFlags.DEBUG_BREAKPOINT_ENABLED)
            self.client.Control.RemoveBreakpoint(x)

    def _add_breakpoint(self, file_, line):
        # Breakpoint setting/deleting is not supported by dbgeng at this moment
        # but is something that should be considered in the future.
        # TODO: this method is called in the DefaultController but has no effect.
        pass

    def _add_conditional_breakpoint(self, file_, line, condition):
        # breakpoint setting/deleting is not supported by dbgeng at this moment
        # but is something that should be considered in the future.
        raise NotImplementedError(
            "add_conditional_breakpoint is not yet implemented by dbgeng"
        )

    def get_triggered_breakpoint_ids(self):
        raise NotImplementedError(
            "get_triggered_breakpoint_ids is not yet implemented by dbgeng"
        )

    def delete_breakpoints(self, ids):
        # breakpoint setting/deleting is not supported by dbgeng at this moment
        # but is something that should be considered in the future.
        raise NotImplementedError(
            "delete_conditional_breakpoint is not yet implemented by dbgeng"
        )

    def launch(self, cmdline):
        assert (
            len(cmdline) == 0 and not self.context.options.target_run_args
        ), "Command lines unimplemented for dbgeng right now"
        # We are, by this point, already launched.
        self.step_info = probe_process.probe_state(self.client)

    def step(self):
        res = setup.step_once(self.client)
        if not res:
            self.finished = True
        self.step_info = res

    def go(self):
        # FIXME: running freely doesn't seem to reliably stop when back in a
        # relevant source file -- this is likely to be a problem when setting
        # breakpoints. Until that's fixed, single step instead of running
        # freely. This isn't very efficient, but at least makes progress.
        self.step()

    def _get_step_info(self, watches, step_index):
        frames = self.step_info
        state_frames = []

        # For now assume the base function is the... function, ignoring
        # inlining.
        dex_frames = []
        for i, x in enumerate(frames):
            # XXX Might be able to get columns out through
            # GetSourceEntriesByOffset, not a priority now
            loc = LocIR(path=x.source_file, lineno=x.line_no, column=0)
            new_frame = FrameIR(function=x.function_name, is_inlined=False, loc=loc)
            dex_frames.append(new_frame)

            state_frame = StackFrame(
                function=new_frame.function,
                is_inlined=new_frame.is_inlined,
                location=SourceLocation(path=x.source_file, lineno=x.line_no, column=0),
                watches={},
            )
            for expr in map(
                # Filter out watches that are not active in the current frame,
                # and then evaluate all the active watches.
                lambda watch_info, idx=i: self.evaluate_expression(
                    watch_info.expression, idx
                ),
                filter(
                    lambda watch_info, idx=i, line_no=loc.lineno, path=loc.path: watch_is_active(
                        watch_info, path, idx, line_no
                    ),
                    watches,
                ),
            ):
                state_frame.watches[expr.expression] = expr
            state_frames.append(state_frame)

        return StepIR(
            step_index=step_index,
            frames=dex_frames,
            stop_reason=StopReason.STEP,
            program_state=ProgramState(state_frames),
        )

    @property
    def is_running(self):
        return False  # We're never free-running

    @property
    def is_finished(self):
        return self.finished

    def evaluate_expression(self, expression, frame_idx=0):
        # XXX: cdb insists on using '->' to examine fields of structures,
        # as it appears to reserve '.' for other purposes.
        fixed_expr = expression.replace(".", "->")

        orig_scope_idx = self.client.Symbols.GetCurrentScopeFrameIndex()
        self.client.Symbols.SetScopeFrameByIndex(frame_idx)

        res = self.client.Control.Evaluate(fixed_expr)
        if res is not None:
            result, typename = self.client.Control.Evaluate(fixed_expr)
            could_eval = True
        else:
            result, typename = (None, None)
            could_eval = False

        self.client.Symbols.SetScopeFrameByIndex(orig_scope_idx)

        return ValueIR(
            expression=expression,
            value=str(result),
            type_name=typename,
            error_string="",
            could_evaluate=could_eval,
            is_optimized_away=False,
            is_irretrievable=not could_eval,
        )