location of custom hooks file in a repository checkout remove python environment variables such as pythonhome temporary value until it s initialized by getgdbversion below runs gdb in batch mode with the additional arguments given by args returns its stdout stderr decoded from utf8 using the replace handler batch mode exit after processing all the command files specified with xcommand nx do not execute commands from any gdbinit initialization files gh66384 redirect stdin to prevent gdb from messing with the terminal settings this is what no gdb looks like there may however be other errors that manifest this way too regex to parse gnu gdb gdb suse linux enterprise 12 7 7n 7 7 gnu gdb gdb fedora 7 9 117 fc22n 7 9 gnu gdb 6 1 1 freebsdn 6 1 gnu gdb gdb fedora 7 5 137 fc18n 7 5 hp gdb 6 7 for hp itanium 32 or 64 bit and target hpux 11iv2 and 11iv3 n 6 7 verify that gdb was built with the embedded python support enabled and verify that gdb can load our custom hooks as os security settings may disallow this without a customized gdbinit controlflow enforcement technology true if mcet fcfprotection options are found but false if fcfprotectionnone or fcfprotectionreturn is found test that the debugger can debug python def getstacktraceself sourcenone scriptnone breakpointbreakpointfn cmdsafterbreakpointnone importsitefalse ignorestderrfalse we use set breakpoint pending yes to avoid blocking with a function foo not defined make breakpoint pending on future shared library load y or n error which typically happens python is dynamically linked the breakpoints of interest are to be found in the shared library when this happens we still get function textiowrapperwrite not defined emitted to stderr each time alas initially i had evalcommandcontinue here but removed it to avoid repeated print breakpoints when traversing hierarchical data structures generate a list of commands in gdb s language commands set breakpoint pending yes break s breakpoint the tests assume that the first frame of printed backtrace will not contain program counter that is however not guaranteed by gdb therefore we need to use set print address off to make sure the counter is not there for example 0 in pyobjectprint is assumed but sometimes this can be e g 0 0x00003fffb7dd1798 in pyobjectprint set print address off run gdb as of 7 4 onwards can distinguish between the value of a variable at entry vs current value http sourceware orggdbonlinedocsgdbvariables html which leads to the selftests failing with errors like this assertionerror ventry disable this if gdbversion 7 4 commands set print entryvalues no if cmdsafterbreakpoint if cetprotection bpo32962 when python is compiled with mcet fcfprotection function arguments are unusable before running the first instruction of the function entry point the next command makes the required first step commands next commands cmdsafterbreakpoint else commands backtrace print commands use commands to generate the arguments with which to invoke gdb args evalcommands cmd for cmd in commands args args sys executable args extendsubprocess argsfrominterpreterflags if not importsite s suppresses the default import site args s if source args c source elif script args script use args to invoke gdb capturing stdout stderr out err rungdbargs pythonhashseedpythonhashseed if not ignorestderr for line in err splitlines printline filesys stderr bpo34007 sometimes some versions of the shared libraries that are part of the traceback are compiled in optimised mode and the program counter pc is not present not allowing gdb to walk the frames back when this happens the python bindings of gdb raise an exception making the test impossible to succeed if pc not saved in err raise unittest skiptestgdb cannot walk the frame object because the program counter is not present bpo40019 skip the test if gdb failed to read debug information because the python binary is optimized for pattern in frame information optimized out unable to read information on python frame gh91960 on python built with clang og gdb gets frameoptimized out for pyevalevalframedefault parameter unable to read python frame information gh104736 on python built with clang og on ppc64le pybt displays a truncated or not traceback but where logs this error message backtrace stopped frame did not save the pc gh104736 when bt command displays something like 1 0x0000000000000000 in the traceback is likely truncated or wrong if pattern in out raise unittest skiptestfpattern r found in gdb output return out def assertendswithself actual expend location of custom hooks file in a repository checkout remove python environment variables such as pythonhome temporary value until it s initialized by get_gdb_version below runs gdb in batch mode with the additional arguments given by args returns its stdout stderr decoded from utf 8 using the replace handler batch mode exit after processing all the command files specified with x command nx do not execute commands from any gdbinit initialization files gh 66384 redirect stdin to prevent gdb from messing with the terminal settings this is what no gdb looks like there may however be other errors that manifest this way too regex to parse gnu gdb gdb suse linux enterprise 12 7 7 n 7 7 gnu gdb gdb fedora 7 9 1 17 fc22 n 7 9 gnu gdb 6 1 1 freebsd n 6 1 gnu gdb gdb fedora 7 5 1 37 fc18 n 7 5 hp gdb 6 7 for hp itanium 32 or 64 bit and target hp ux 11iv2 and 11iv3 n 6 7 verify that gdb was built with the embedded python support enabled and verify that gdb can load our custom hooks as os security settings may disallow this without a customized gdbinit control flow enforcement technology true if mcet fcf protection options are found but false if fcf protection none or fcf protection return is found test that the debugger can debug python run python c source under gdb with a breakpoint support injecting commands after the breakpoint is reached returns the stdout from gdb cmds_after_breakpoint if provided a list of strings gdb commands we use set breakpoint pending yes to avoid blocking with a function foo not defined make breakpoint pending on future shared library load y or n error which typically happens python is dynamically linked the breakpoints of interest are to be found in the shared library when this happens we still get function textiowrapper_write not defined emitted to stderr each time alas initially i had eval command continue here but removed it to avoid repeated print breakpoints when traversing hierarchical data structures generate a list of commands in gdb s language the tests assume that the first frame of printed backtrace will not contain program counter that is however not guaranteed by gdb therefore we need to use set print address off to make sure the counter is not there for example 0 in pyobject_print is assumed but sometimes this can be e g 0 0x00003fffb7dd1798 in pyobject_print gdb as of 7 4 onwards can distinguish between the value of a variable at entry vs current value http sourceware org gdb onlinedocs gdb variables html which leads to the selftests failing with errors like this assertionerror v entry disable this bpo 32962 when python is compiled with mcet fcf protection function arguments are unusable before running the first instruction of the function entry point the next command makes the required first step print commands use commands to generate the arguments with which to invoke gdb s suppresses the default import site use args to invoke gdb capturing stdout stderr bpo 34007 sometimes some versions of the shared libraries that are part of the traceback are compiled in optimised mode and the program counter pc is not present not allowing gdb to walk the frames back when this happens the python bindings of gdb raise an exception making the test impossible to succeed bpo 40019 skip the test if gdb failed to read debug information because the python binary is optimized gh 91960 on python built with clang og gdb gets frame optimized out for _pyeval_evalframedefault parameter gh 104736 on python built with clang og on ppc64le py bt displays a truncated or not traceback but where logs this error message gh 104736 when bt command displays something like 1 0x0000000000000000 in the traceback is likely truncated or wrong ensure that the given actual string ends with exp_end
import os
import re
import shlex
import shutil
import subprocess
import sys
import sysconfig
import unittest
from test import support


GDB_PROGRAM = shutil.which('gdb') or 'gdb'


CHECKOUT_HOOK_PATH = os.path.join(os.path.dirname(sys.executable),
                                  'python-gdb.py')

SAMPLE_SCRIPT = os.path.join(os.path.dirname(__file__), 'gdb_sample.py')
BREAKPOINT_FN = 'builtin_id'

PYTHONHASHSEED = '123'


def clean_environment():
    
    return {name: value for name, value in os.environ.items()
            if not name.startswith('PYTHON')}



GDB_VERSION = (0, 0)

def run_gdb(*args, exitcode=0, check=True, **env_vars):
    
    env = clean_environment()
    if env_vars:
        env.update(env_vars)

    cmd = [GDB_PROGRAM,
           
           
           '--batch',
            
            
           '-nx']
    if GDB_VERSION >= (7, 4):
        cmd.extend(('--init-eval-command',
                    f'add-auto-load-safe-path {CHECKOUT_HOOK_PATH}'))
    cmd.extend(args)

    proc = subprocess.run(
        cmd,
        
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        encoding="utf8", errors="backslashreplace",
        env=env)

    stdout = proc.stdout
    stderr = proc.stderr
    if check and proc.returncode != exitcode:
        cmd_text = shlex.join(cmd)
        raise Exception(f"{cmd_text} failed with exit code {proc.returncode}, "
                        f"expected exit code {exitcode}:\n"
                        f"stdout={stdout!r}\n"
                        f"stderr={stderr!r}")

    return (stdout, stderr)


def get_gdb_version():
    try:
        stdout, stderr = run_gdb('--version')
    except OSError as exc:
        
        
        raise unittest.SkipTest(f"Couldn't find gdb program on the path: {exc}")

    
    
    
    
    
    
    match = re.search(r"^(?:GNU|HP) gdb.*?\b(\d+)\.(\d+)", stdout)
    if match is None:
        raise Exception("unable to parse gdb version: %r" % stdout)
    version_text = stdout
    major = int(match.group(1))
    minor = int(match.group(2))
    version = (major, minor)
    return (version_text, version)

GDB_VERSION_TEXT, GDB_VERSION = get_gdb_version()
if GDB_VERSION < (7, 0):
    raise unittest.SkipTest(
        f"gdb versions before 7.0 didn't support python embedding. "
        f"Saw gdb version {GDB_VERSION[0]}.{GDB_VERSION[1]}:\n"
        f"{GDB_VERSION_TEXT}")


def check_usable_gdb():
    
    
    
    stdout, stderr = run_gdb(
        '--eval-command=python import sys; print(sys.version_info)',
        '--args', sys.executable,
        check=False)

    if "auto-loading has been declined" in stderr:
        raise unittest.SkipTest(
            f"gdb security settings prevent use of custom hooks; "
            f"stderr: {stderr!r}")

    if not stdout:
        raise unittest.SkipTest(
            f"gdb not built with embedded python support; "
            f"stderr: {stderr!r}")

    if "major=2" in stdout:
        raise unittest.SkipTest("gdb built with Python 2")

check_usable_gdb()



def cet_protection():
    cflags = sysconfig.get_config_var('CFLAGS')
    if not cflags:
        return False
    flags = cflags.split()
    
    
    return (('-mcet' in flags)
            and any((flag.startswith('-fcf-protection')
                     and not flag.endswith(("=none", "=return")))
                    for flag in flags))
CET_PROTECTION = cet_protection()


def setup_module():
    if support.verbose:
        print(f"gdb version {GDB_VERSION[0]}.{GDB_VERSION[1]}:")
        for line in GDB_VERSION_TEXT.splitlines():
            print(" " * 4 + line)
        print(f"    path: {GDB_PROGRAM}")
        print()


class DebuggerTests(unittest.TestCase):

    

    def get_stack_trace(self, source=None, script=None,
                        breakpoint=BREAKPOINT_FN,
                        cmds_after_breakpoint=None,
                        import_site=False,
                        ignore_stderr=False):
        
        
        
        
        
        
        
        
        

        
        
        

        
        commands = [
            'set breakpoint pending yes',
            'break %s' % breakpoint,

            
            
            
            
            
            
            
            
            'set print address off',

            'run',
        ]

        
        
        
        
        
        
        if GDB_VERSION >= (7, 4):
            commands += ['set print entry-values no']

        if cmds_after_breakpoint:
            if CET_PROTECTION:
                
                
                
                
                commands += ['next']
            commands += cmds_after_breakpoint
        else:
            commands += ['backtrace']

        

        
        args = ['--eval-command=%s' % cmd for cmd in commands]
        args += ["--args",
                 sys.executable]
        args.extend(subprocess._args_from_interpreter_flags())

        if not import_site:
            
            args += ["-S"]

        if source:
            args += ["-c", source]
        elif script:
            args += [script]

        
        out, err = run_gdb(*args, PYTHONHASHSEED=PYTHONHASHSEED)

        if not ignore_stderr:
            for line in err.splitlines():
                print(line, file=sys.stderr)

        
        
        
        
        
        if "PC not saved" in err:
            raise unittest.SkipTest("gdb cannot walk the frame object"
                                    " because the Program Counter is"
                                    " not present")

        
        
        for pattern in (
            '(frame information optimized out)',
            'Unable to read information on python frame',

            
            
            '(unable to read python frame information)',

            
            
            
            'Backtrace stopped: frame did not save the PC',

            
            
            
            ' ?? ()',
        ):
            if pattern in out:
                raise unittest.SkipTest(f"{pattern!r} found in gdb output")

        return out

    def assertEndsWith(self, actual, exp_end):
        
        self.assertTrue(actual.endswith(exp_end),
                        msg='%r did not end with %r' % (actual, exp_end))

    def assertMultilineMatches(self, actual, pattern):
        m = re.match(pattern, actual, re.DOTALL)
        if not m:
            self.fail(msg='%r did not match %r' % (actual, pattern))