├── tests ├── __init__.py ├── sample_c_app │ ├── bye.c │ ├── makefile │ └── hello.c ├── test_example.py ├── test_string_stream.py ├── test_gdbescapes.py ├── test_gdbcontroller.py ├── test_gdbmiparser.py └── response_samples.txt ├── docs ├── README.md ├── CHANGELOG.md └── api │ ├── iomanager.md │ ├── gdbmiparser.md │ └── gdbcontroller.md ├── mkdoc_requirements.in ├── pygdbmi ├── __init__.py ├── py.typed ├── constants.py ├── printcolor.py ├── StringStream.py ├── gdbcontroller.py ├── gdbescapes.py ├── gdbmiparser.py └── IoManager.py ├── .flake8 ├── .gitignore ├── mypy.ini ├── MANIFEST.in ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── tests.yml ├── mkdocs.yml ├── LICENSE ├── mkdoc_requirements.txt ├── setup.py ├── example.py ├── noxfile.py ├── CHANGELOG.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /docs/api/iomanager.md: -------------------------------------------------------------------------------- 1 | ::: pygdbmi.IoManager -------------------------------------------------------------------------------- /docs/api/gdbmiparser.md: -------------------------------------------------------------------------------- 1 | ::: pygdbmi.gdbmiparser -------------------------------------------------------------------------------- /docs/api/gdbcontroller.md: -------------------------------------------------------------------------------- 1 | ::: pygdbmi.gdbcontroller -------------------------------------------------------------------------------- /mkdoc_requirements.in: -------------------------------------------------------------------------------- 1 | . 2 | mkdocstrings[python] 3 | mkdocs 4 | mkdocs-material 5 | pygments -------------------------------------------------------------------------------- /tests/sample_c_app/bye.c: -------------------------------------------------------------------------------- 1 | #include 2 | void bye() 3 | { 4 | printf("Bye\n"); 5 | } -------------------------------------------------------------------------------- /tests/sample_c_app/makefile: -------------------------------------------------------------------------------- 1 | hello: hello.c bye.c 2 | gcc hello.c bye.c -g -o pygdbmiapp.a 3 | -------------------------------------------------------------------------------- /pygdbmi/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.11.0.0" 2 | 3 | __all__ = [ 4 | "IoManager", 5 | "gdbcontroller", 6 | "gdbmiparser", 7 | ] 8 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | # line length, whitespace before ':', line break before binary operator 4 | ignore = E501, E203, W503 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | a.out 2 | *.a 3 | build 4 | dist 5 | .DS_Store 6 | __pycache__ 7 | *egg* 8 | __pycache__ 9 | *.dSYM 10 | .mypy_cache/ 11 | venv 12 | site -------------------------------------------------------------------------------- /tests/test_example.py: -------------------------------------------------------------------------------- 1 | import example 2 | 3 | 4 | def test_example() -> None: 5 | """Test `example.py` in the root of the repo""" 6 | example.main() 7 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.7 3 | show_error_codes = True 4 | warn_redundant_casts = True 5 | strict_equality = True 6 | disallow_untyped_defs = True 7 | -------------------------------------------------------------------------------- /pygdbmi/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. This package uses inline types. 2 | # https://mypy.readthedocs.io/en/latest/installed_packages.html#making-pep-561-compatible-packages -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include LICENSE 3 | 4 | graft pygdbmi 5 | 6 | exclude example.py 7 | exclude .flake8 8 | exclude noxfile.py 9 | exclude mkdocs.yml 10 | exclude __pycache__ 11 | exclude mypy.ini 12 | exclude *.in 13 | exclude *.txt 14 | exclude *.pyc 15 | exclude *.sw[po] 16 | exclude *~ 17 | 18 | prune tests 19 | prune docs 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | - [] I have added an entry to `CHANGELOG.md` 3 | 4 | ## Summary of changes 5 | 6 | ## Test plan 7 | 8 | Tested by running 9 | ``` 10 | # command(s) to exercise these changes 11 | ``` 12 | -------------------------------------------------------------------------------- /pygdbmi/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | __all__ = [ 5 | "DEFAULT_GDB_TIMEOUT_SEC", 6 | "DEFAULT_TIME_TO_CHECK_FOR_ADDITIONAL_OUTPUT_SEC", 7 | "GdbTimeoutError", 8 | "USING_WINDOWS", 9 | ] 10 | 11 | 12 | DEFAULT_GDB_TIMEOUT_SEC = 1 13 | DEFAULT_TIME_TO_CHECK_FOR_ADDITIONAL_OUTPUT_SEC = 0.2 14 | USING_WINDOWS = os.name == "nt" 15 | 16 | 17 | class GdbTimeoutError(ValueError): 18 | """Raised when no response is recieved from gdb after the timeout has been triggered""" 19 | 20 | pass 21 | -------------------------------------------------------------------------------- /tests/sample_c_app/hello.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | struct my_type_t { 4 | int a; 5 | float b; 6 | struct { 7 | size_t c; 8 | double d; 9 | }; 10 | }; 11 | 12 | /* Forward declaration */ 13 | void bye(); 14 | 15 | int main() 16 | { 17 | printf("Hello world\n"); 18 | printf(" leading spaces should be preserved. So should trailing spaces. \n"); 19 | struct my_type_t myvar= { 20 | .a = 1, 21 | .b = 1.2, 22 | .c = 4, 23 | .d = 6.7 24 | }; 25 | int i = 0; 26 | for(i = 0; i < 2; i++){ 27 | printf("i = %d\n", i); 28 | } 29 | bye(); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pygdbmi 2 | site_description: Parse gdb machine interface output with Python 3 | 4 | theme: 5 | name: "material" 6 | repo_name: cs01/pygdbmi 7 | repo_url: https://github.com/cs01/pygdbmi 8 | 9 | nav: 10 | - Home: "README.md" 11 | - Api: 12 | - "gdbmiparser": "api/gdbmiparser.md" 13 | - "gdbcontroller": "api/gdbcontroller.md" 14 | - "iomanager": "api/iomanager.md" 15 | - Changelog: "CHANGELOG.md" 16 | 17 | markdown_extensions: 18 | - admonition # note blocks, warning blocks -- https://github.com/mkdocs/mkdocs/issues/1659 19 | - codehilite 20 | 21 | plugins: 22 | - mkdocstrings 23 | - search 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | # Steps to reproduce the behavior 12 | 13 | **Expected behavior** 14 | # A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Please complete the following information:** 20 | * OS: 21 | * pygdbmi version (`pip freeze` output): 22 | 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /tests/test_string_stream.py: -------------------------------------------------------------------------------- 1 | from pygdbmi.StringStream import StringStream 2 | 3 | 4 | def test_string_stream() -> None: 5 | """Tests the StringStream API""" 6 | raw_text = 'abc- "d" ""ef"" g' 7 | stream = StringStream(raw_text) 8 | assert stream.index == 0 9 | assert stream.len == len(raw_text) 10 | 11 | buf = stream.read(1) 12 | assert buf == "a" 13 | assert stream.index == 1 14 | 15 | stream.seek(-1) 16 | assert stream.index == 0 17 | 18 | buf = stream.advance_past_chars(['"']) 19 | buf = stream.advance_past_string_with_gdb_escapes() 20 | assert buf == "d" 21 | 22 | buf = stream.advance_past_chars(['"']) 23 | buf = stream.advance_past_chars(['"']) 24 | buf = stream.advance_past_string_with_gdb_escapes() 25 | assert buf == "ef" 26 | 27 | # read way past end to test it gracefully returns the 28 | # remainder of the string without failing 29 | buf = stream.read(50) 30 | assert buf == '" g' 31 | -------------------------------------------------------------------------------- /pygdbmi/printcolor.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any 3 | 4 | 5 | __all__ = [ 6 | "fmt_cyan", 7 | "fmt_green", 8 | "print_cyan", 9 | "print_green", 10 | "print_red", 11 | ] 12 | 13 | 14 | USING_WINDOWS = os.name == "nt" 15 | 16 | 17 | def print_red(x: Any) -> None: 18 | if USING_WINDOWS: 19 | print(x) 20 | else: 21 | print(f"\033[91m {x}\033[00m") 22 | 23 | 24 | def print_green(x: Any) -> None: 25 | if USING_WINDOWS: 26 | print(x) 27 | else: 28 | print(f"\033[92m {x}\033[00m") 29 | 30 | 31 | def print_cyan(x: Any) -> None: 32 | if USING_WINDOWS: 33 | print(x) 34 | else: 35 | print(f"\033[96m {x}\033[00m") 36 | 37 | 38 | def fmt_green(x: Any) -> str: 39 | if USING_WINDOWS: 40 | return x 41 | else: 42 | return f"\033[92m {x}\033[00m" 43 | 44 | 45 | def fmt_cyan(x: Any) -> str: 46 | if USING_WINDOWS: 47 | return x 48 | else: 49 | return f"\033[96m {x}\033[00m" 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Chad Smith gmail.com> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /mkdoc_requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.10 3 | # To update, run: 4 | # 5 | # pip-compile mkdoc_requirements.in 6 | # 7 | click==8.1.3 8 | # via mkdocs 9 | ghp-import==2.1.0 10 | # via mkdocs 11 | griffe==0.22.0 12 | # via mkdocstrings-python 13 | importlib-metadata==4.12.0 14 | # via mkdocs 15 | jinja2==3.1.2 16 | # via 17 | # mkdocs 18 | # mkdocs-material 19 | # mkdocstrings 20 | markdown==3.3.7 21 | # via 22 | # mkdocs 23 | # mkdocs-autorefs 24 | # mkdocs-material 25 | # mkdocstrings 26 | # pymdown-extensions 27 | markupsafe==2.1.1 28 | # via 29 | # jinja2 30 | # mkdocstrings 31 | mergedeep==1.3.4 32 | # via mkdocs 33 | mkdocs==1.3.1 34 | # via 35 | # -r mkdoc_requirements.in 36 | # mkdocs-autorefs 37 | # mkdocs-material 38 | # mkdocstrings 39 | mkdocs-autorefs==0.4.1 40 | # via mkdocstrings 41 | mkdocs-material==8.3.9 42 | # via -r mkdoc_requirements.in 43 | mkdocs-material-extensions==1.0.3 44 | # via mkdocs-material 45 | mkdocstrings[python]==0.19.0 46 | # via 47 | # -r mkdoc_requirements.in 48 | # mkdocstrings-python 49 | mkdocstrings-python==0.7.1 50 | # via mkdocstrings 51 | packaging==21.3 52 | # via mkdocs 53 | . 54 | # via -r mkdoc_requirements.in 55 | pygments==2.12.0 56 | # via 57 | # -r mkdoc_requirements.in 58 | # mkdocs-material 59 | pymdown-extensions==9.5 60 | # via 61 | # mkdocs-material 62 | # mkdocstrings 63 | pyparsing==3.0.9 64 | # via packaging 65 | python-dateutil==2.8.2 66 | # via ghp-import 67 | pyyaml==6.0 68 | # via 69 | # mkdocs 70 | # pyyaml-env-tag 71 | pyyaml-env-tag==0.1 72 | # via mkdocs 73 | six==1.16.0 74 | # via python-dateutil 75 | watchdog==2.1.9 76 | # via mkdocs 77 | zipp==3.8.1 78 | # via importlib-metadata 79 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import re 5 | from codecs import open 6 | 7 | from setuptools import find_packages, setup # type: ignore 8 | 9 | 10 | EXCLUDE_FROM_PACKAGES = ["tests"] 11 | CURDIR = os.path.abspath(os.path.dirname(__file__)) 12 | README = open("README.md", encoding="utf-8").read() 13 | 14 | with open("pygdbmi/__init__.py") as fd: 15 | matches = re.search( 16 | r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE 17 | ) 18 | version = "0.0.0.0" 19 | if matches: 20 | version = matches.group(1) 21 | 22 | 23 | setup( 24 | name="pygdbmi", 25 | version=version, 26 | author="Chad Smith", 27 | author_email="grassfedcode@gmail.com", 28 | description="Parse gdb machine interface output with Python", 29 | long_description=README, 30 | long_description_content_type="text/markdown", 31 | url="https://github.com/cs01/pygdbmi", 32 | license="MIT", 33 | packages=find_packages(exclude=EXCLUDE_FROM_PACKAGES), 34 | include_package_data=True, 35 | keywords=["gdb", "python", "machine-interface", "parse", "frontend"], 36 | scripts=[], 37 | entry_points={}, 38 | zip_safe=False, 39 | classifiers=[ 40 | "Intended Audience :: Developers", 41 | "License :: OSI Approved :: MIT License", 42 | "Operating System :: OS Independent", 43 | "Programming Language :: Python", 44 | "Programming Language :: Python :: 3", 45 | # If modifying the list of supported versions, also update the versions pygdbmi is tested 46 | # with, see noxfile.py and .github/workflows/tests.yml. 47 | "Programming Language :: Python :: 3.7", 48 | "Programming Language :: Python :: 3.8", 49 | "Programming Language :: Python :: 3.9", 50 | "Programming Language :: Python :: 3.10", 51 | "Programming Language :: Python :: Implementation :: PyPy", 52 | ], 53 | ) 54 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions 2 | 3 | name: Tests 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - master 10 | release: 11 | 12 | jobs: 13 | run_tests: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | # Keep the version here in sync with the ones used in noxfile.py 19 | python-version: ["3.7", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install nox 31 | - name: Install gdb ubuntu 32 | run: | 33 | sudo apt-get install gdb 34 | - name: Execute Tests 35 | run: | 36 | nox --non-interactive --session tests-${{ matrix.python-version }} 37 | 38 | lint: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v2 42 | - name: Set up Python 43 | uses: actions/setup-python@v2 44 | with: 45 | python-version: "3.10" 46 | - name: Install dependencies 47 | run: | 48 | python -m pip install --upgrade pip 49 | pip install nox 50 | - name: Lint 51 | run: | 52 | nox --non-interactive --session lint 53 | 54 | docs: 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | - name: Set up Python 59 | uses: actions/setup-python@v2 60 | with: 61 | python-version: "3.10" 62 | - name: Install dependencies 63 | run: | 64 | python -m pip install --upgrade pip 65 | pip install nox 66 | - name: Verify Docs 67 | run: | 68 | nox --non-interactive --session docs 69 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Run with `python -m example` 5 | """ 6 | import os 7 | import shutil 8 | import subprocess 9 | import sys 10 | 11 | from pygdbmi.gdbcontroller import GdbController 12 | 13 | 14 | SAMPLE_C_CODE_DIR = os.path.join( 15 | os.path.dirname(os.path.realpath(__file__)), "tests", "sample_c_app" 16 | ) 17 | SAMPLE_C_BINARY = os.path.join(SAMPLE_C_CODE_DIR, "pygdbmiapp.a") 18 | PYTHON3 = sys.version_info.major == 3 19 | USING_WINDOWS = os.name == "nt" 20 | 21 | if USING_WINDOWS: 22 | SAMPLE_C_BINARY = SAMPLE_C_BINARY.replace("\\", "/") 23 | MAKE_CMD = "mingw32-make.exe" 24 | else: 25 | MAKE_CMD = "make" 26 | 27 | 28 | def main() -> None: 29 | """Build and debug an application programatically 30 | 31 | For a list of GDB MI commands, see https://www.sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI.html 32 | """ 33 | 34 | # Build C program 35 | if not shutil.which(MAKE_CMD): 36 | print( 37 | 'Could not find executable "%s". Ensure it is installed and on your $PATH.' 38 | % MAKE_CMD 39 | ) 40 | exit(1) 41 | subprocess.check_output([MAKE_CMD, "-C", SAMPLE_C_CODE_DIR, "--quiet"]) 42 | 43 | # Initialize object that manages gdb subprocess 44 | gdbmi = GdbController() 45 | 46 | # Send gdb commands. Gdb machine interface commands are easier to script around, 47 | # hence the name "machine interface". 48 | # Responses are automatically printed as they are received if verbose is True. 49 | # Responses are returned after writing, by default. 50 | 51 | # Load the file 52 | responses = gdbmi.write("-file-exec-and-symbols %s" % SAMPLE_C_BINARY) 53 | # Get list of source files used to compile the binary 54 | responses = gdbmi.write("-file-list-exec-source-files") 55 | # Add breakpoint 56 | responses = gdbmi.write("-break-insert main") 57 | # Run 58 | responses = gdbmi.write("-exec-run") 59 | responses = gdbmi.write("-exec-next") 60 | responses = gdbmi.write("-exec-next") 61 | responses = gdbmi.write("-exec-continue") # noqa: F841 62 | 63 | # gdbmi.gdb_process will be None because the gdb subprocess (and its inferior 64 | # program) will be terminated 65 | gdbmi.exit() 66 | 67 | 68 | if __name__ == "__main__": 69 | main() 70 | -------------------------------------------------------------------------------- /pygdbmi/StringStream.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pygdbmi.gdbescapes import advance_past_string_with_gdb_escapes 4 | 5 | 6 | __all__ = ["StringStream"] 7 | 8 | 9 | class StringStream: 10 | """A simple class to hold text so that when passed 11 | between functions, the object is passed by reference 12 | and memory does not need to be repeatedly allocated for the string. 13 | 14 | This class was written here to avoid adding a dependency 15 | to the project. 16 | """ 17 | 18 | def __init__(self, raw_text: str, debug: bool = False) -> None: 19 | self.raw_text = raw_text 20 | self.index = 0 21 | self.len = len(raw_text) 22 | 23 | def read(self, count: int) -> str: 24 | """Read count characters starting at self.index, 25 | and return those characters as a string 26 | """ 27 | new_index = self.index + count 28 | if new_index > self.len: 29 | buf = self.raw_text[self.index :] # return to the end, don't fail 30 | else: 31 | buf = self.raw_text[self.index : new_index] 32 | self.index = new_index 33 | 34 | return buf 35 | 36 | def seek(self, offset: int) -> None: 37 | """Advance the index of this StringStream by offset characters""" 38 | self.index = self.index + offset 39 | 40 | def advance_past_chars(self, chars: List[str]) -> str: 41 | """Advance the index past specific chars 42 | Args chars (list): list of characters to advance past 43 | 44 | Return substring that was advanced past 45 | """ 46 | start_index = self.index 47 | while True: 48 | current_char = self.raw_text[self.index] 49 | self.index += 1 50 | if current_char in chars: 51 | break 52 | 53 | elif self.index == self.len: 54 | break 55 | 56 | return self.raw_text[start_index : self.index - 1] 57 | 58 | def advance_past_string_with_gdb_escapes(self) -> str: 59 | """Advance the index past a quoted string until the end quote is reached, and 60 | return the string (after unescaping it) 61 | 62 | Must be called only after encountering a quote character. 63 | """ 64 | assert self.index > 0 and self.raw_text[self.index - 1] == '"', ( 65 | "advance_past_string_with_gdb_escapes called not at the start of a string " 66 | f"(at index {self.index} of text {self.raw_text!r}, " 67 | f"remaining string {self.raw_text[self.index:]!r})" 68 | ) 69 | 70 | unescaped_str, self.index = advance_past_string_with_gdb_escapes( 71 | self.raw_text, start=self.index 72 | ) 73 | return unescaped_str 74 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import shutil 3 | from pathlib import Path 4 | 5 | import nox 6 | from nox.sessions import Session 7 | 8 | 9 | nox.options.sessions = ["tests", "lint", "docs"] 10 | nox.options.reuse_existing_virtualenvs = True 11 | 12 | 13 | # Run tests with (at least) the oldest and newest versions we support. 14 | # If these are modified, also modify .github/workflows/tests.yml and the list of supported versions 15 | # in setup.py. 16 | @nox.session(python=["3.7", "3.10"]) 17 | def tests(session: Session) -> None: 18 | session.install(".", "pytest") 19 | session.run("pytest", *session.posargs) 20 | 21 | 22 | LINTED_PATHS = { 23 | str(p.resolve()) 24 | for p in itertools.chain( 25 | # All top-level Python files. 26 | Path(".").glob("*.py"), 27 | # Plus Python files in these specified directories. 28 | *(Path(d).glob("**/*.py") for d in ("pygdbmi", "tests")) 29 | ) 30 | } 31 | 32 | ISORT_OPTIONS = ["--profile", "black", "--lines-after-imports", "2"] 33 | 34 | 35 | # `format` is a builtin so the function is named differently. 36 | @nox.session(name="format") 37 | def format_(session: Session) -> None: 38 | """Re-format all Python source files or, if positionals are passed, only the 39 | specified files.""" 40 | files = LINTED_PATHS 41 | if session.posargs: 42 | # Only use positional arguments which are linted files. 43 | files = files & {str(Path(f).resolve()) for f in session.posargs} 44 | 45 | session.install("isort", "black", "flake8", "mypy", "check-manifest") 46 | session.run("isort", *ISORT_OPTIONS, *files) 47 | session.run("black", *files) 48 | 49 | 50 | @nox.session() 51 | def lint(session: Session) -> None: 52 | session.install( 53 | # Packages needed as they are used directly. 54 | "black", 55 | "check-manifest", 56 | "flake8", 57 | "isort", 58 | "mypy", 59 | # Packages needed to provide types for mypy. 60 | "nox", 61 | "pytest", 62 | ) 63 | session.run("isort", "--check-only", *ISORT_OPTIONS, *LINTED_PATHS) 64 | session.run("black", "--check", *LINTED_PATHS) 65 | session.run("flake8", *LINTED_PATHS) 66 | session.run("mypy", *LINTED_PATHS) 67 | session.run("check-manifest") 68 | session.run("python", "setup.py", "check", "--metadata", "--strict") 69 | 70 | 71 | def install_mkdoc_dependencies(session: Session) -> None: 72 | session.install("-r", "mkdoc_requirements.txt") 73 | 74 | 75 | @nox.session 76 | def docs(session: Session) -> None: 77 | install_mkdoc_dependencies(session) 78 | session.run("mkdocs", "build") 79 | 80 | 81 | @nox.session 82 | def serve_docs(session: Session) -> None: 83 | install_mkdoc_dependencies(session) 84 | session.run("mkdocs", "serve") 85 | 86 | 87 | @nox.session 88 | def publish_docs(session: Session) -> None: 89 | install_mkdoc_dependencies(session) 90 | session.run("mkdocs", "gh-deploy") 91 | 92 | 93 | @nox.session(python="3.7") 94 | def build(session: Session) -> None: 95 | session.install("setuptools", "wheel", "twine") 96 | shutil.rmtree("dist", ignore_errors=True) 97 | shutil.rmtree("build", ignore_errors=True) 98 | session.run("python", "setup.py", "--quiet", "sdist", "bdist_wheel") 99 | session.run("twine", "check", "dist/*") 100 | 101 | 102 | @nox.session(python="3.7") 103 | def publish(session: Session) -> None: 104 | build(session) 105 | print("REMINDER: Has the changelog been updated?") 106 | session.run("python", "-m", "twine", "upload", "dist/*") 107 | publish_docs(session) 108 | -------------------------------------------------------------------------------- /tests/test_gdbescapes.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Optional 3 | 4 | import pytest 5 | 6 | from pygdbmi.gdbescapes import advance_past_string_with_gdb_escapes, unescape 7 | 8 | 9 | # Split a Unicode character into its UTF-8 bytes and encode each one as a 3-digit 10 | # oct char prefixed with a "\". 11 | # This is the opposite of what the gdbescapes module does. 12 | GDB_ESCAPED_PIZZA = "".join(rf"\{c:03o}" for c in "\N{SLICE OF PIZZA}".encode()) 13 | # Similar but for a simple space. 14 | # This character was chosen because, in octal, it's shorter than three digits, so we 15 | # can check that unescape_gdb_mi_string handles the initial `0` correctly. 16 | # Note that a space would usually not be escaped by GDB itself, but it's fine if it 17 | # is. 18 | GDB_ESCAPED_SPACE = rf"\{ord(' '):03o}" 19 | 20 | 21 | @pytest.mark.parametrize( 22 | "input_str, expected", 23 | [ 24 | (r"a", "a"), 25 | (r"hello world", "hello world"), 26 | (r"hello\nworld", "hello\nworld"), 27 | (r"quote: <\">", 'quote: <">'), 28 | # UTF-8 text encoded as a sequence of octal characters. 29 | (GDB_ESCAPED_PIZZA, "\N{SLICE OF PIZZA}"), 30 | # Similar but for a simple space. 31 | (GDB_ESCAPED_SPACE, " "), 32 | # Several escapes in the same string. 33 | ( 34 | ( 35 | rf"\tmultiple\nescapes\tin\"the\'same\"string\"foo" 36 | rf"{GDB_ESCAPED_SPACE}bar{GDB_ESCAPED_PIZZA}" 37 | ), 38 | '\tmultiple\nescapes\tin"the\'same"string"foo bar\N{SLICE OF PIZZA}', 39 | ), 40 | # An octal sequence that is not valid UTF-8 doesn't get changes, see #64. 41 | (r"254 '\376'", r"254 '\376'"), 42 | ], 43 | ) 44 | def test_unescape(input_str: str, expected: str) -> None: 45 | """Test the unescape function""" 46 | assert unescape(input_str) == expected 47 | 48 | 49 | @pytest.mark.parametrize( 50 | "input_str, exc_message", 51 | [ 52 | (r'"', "Unescaped quote found"), 53 | (r'"x', "Unescaped quote found"), 54 | (r'a"', "Unescaped quote found"), 55 | (r'a"x', "Unescaped quote found"), 56 | (r'a"x"foo', "Unescaped quote found"), 57 | (r"\777", "Invalid octal number"), 58 | (r"\400", "Invalid octal number"), 59 | (r"\X", "Invalid escape character"), 60 | (r"\1", "Invalid escape character"), 61 | (r"\11", "Invalid escape character"), 62 | ], 63 | ) 64 | def test_bad_string(input_str: str, exc_message: str) -> None: 65 | """Test the unescape function with invalid inputs""" 66 | with pytest.raises(ValueError, match=re.escape(exc_message)): 67 | unescape(input_str) 68 | 69 | 70 | @pytest.mark.parametrize( 71 | "input_escaped_str, expected_unescaped_str, expected_after_str, start", 72 | [ 73 | (r'a"', "a", "", None), 74 | (r'a"bc', "a", "bc", None), 75 | (r'"a"', "a", "", 1), 76 | (r'"a"bc', "a", "bc", 1), 77 | (r'x="a"', "a", "", 3), 78 | (r'x="a"bc', "a", "bc", 3), 79 | # Escaped quotes. 80 | (r'\""', '"', "", None), 81 | (r'"\""', '"', "", 1), 82 | (r'"\"",foo', '"', ",foo", 1), 83 | (r'x="\""', '"', "", 3), 84 | (r'"\"hello\"world\""', '"hello"world"', "", 1), 85 | # Other escapes. 86 | (r'\n"', "\n", "", None), 87 | (r'"\n"', "\n", "", 1), 88 | (r'"\n",foo', "\n", ",foo", 1), 89 | (r'x="\n"', "\n", "", 3), 90 | (r'"\nhello\nworld\n"', "\nhello\nworld\n", "", 1), 91 | ( 92 | rf'"I want a {GDB_ESCAPED_PIZZA}"something else', 93 | "I want a \N{SLICE OF PIZZA}", 94 | "something else", 95 | 1, 96 | ), 97 | ], 98 | ) 99 | def test_advance_past_string_with_gdb_escapes( 100 | input_escaped_str: str, 101 | expected_unescaped_str: str, 102 | expected_after_str: str, 103 | start: Optional[int], 104 | ) -> None: 105 | """Test the advance_past_string_with_gdb_escapes function""" 106 | kwargs = {} 107 | if start is not None: 108 | kwargs["start"] = start 109 | 110 | actual_unescaped_str, after_quote_index = advance_past_string_with_gdb_escapes( 111 | input_escaped_str, **kwargs 112 | ) 113 | assert actual_unescaped_str == expected_unescaped_str 114 | actual_after_str = input_escaped_str[after_quote_index:] 115 | assert actual_after_str == expected_after_str 116 | 117 | 118 | @pytest.mark.parametrize( 119 | "input_str", 120 | [ 121 | r"", 122 | r"\"", 123 | r"a\"", 124 | r"\"a", 125 | r"a", 126 | r"a\"b", 127 | ], 128 | ) 129 | def test_advance_past_string_with_gdb_escapes_raises(input_str: str) -> None: 130 | """Test the advance_past_string_with_gdb_escapes function with invalid input""" 131 | with pytest.raises(ValueError, match=r"Missing closing quote"): 132 | advance_past_string_with_gdb_escapes(input_str) 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # pygdbmi release history 2 | 3 | ## <0.11.0.1>.dev0 4 | 5 | - *Replace this line with new entries* 6 | 7 | ## 0.11.0.0 8 | 9 | **Breaking changes** 10 | 11 | - Removed `pygdbmi.IoManager.make_non_blocking` from the public API; it's unrelated and was not meant to be public 12 | 13 | Other changes 14 | 15 | - Fixed a bug where notifications without a payload were not recognized as such 16 | - Invalid octal sequences produced by GDB are left unchanged instead of causing a `UnicodeDecodeError` (#64) 17 | - Fix a crash on Windows by waiting for the GDB process to exit in `GdbController.exit` 18 | - Added type annotations to the whole public API 19 | - Updated the examples in `README.md` to use the current API and show the results printed by this version of pygdbmi (#69) 20 | 21 | Internal changes 22 | 23 | - Update and freeze dependencies for documentation generation 24 | - Refactored the code to parse MI records to decrease the number of regex matches to perform 25 | - Added `__all__` to all modules, which means that star imports (like `from pygdbmi.gdbmiparser import *`) will not pollute the namespace with modules used by pygdbmi itself 26 | - Added `nox -s format` to re-format the source code using the correct options 27 | - Reformatted all imports with `isort`, and use it as part of `nox -s lint` and `nox -s format` 28 | - Converted tests to use pytest's test structure rather than the unittest-based one 29 | - Added mypy configuration to detect more problems and to force all code to be annotated 30 | - Added a test for `example.py` 31 | - Replaced uses of `distutils.spawn.find_executable`, which is deprecated, with `shutil.which` 32 | - Ran [`pyupgrade`](https://github.com/asottile/pyupgrade) (with option `--py37-plus`) on the codebase to convert to Python 3.7 idioms 33 | - Excluded some common backup and cache files from `MANIFEST.in` to prevent unwanted files to be included which causes `check-manifest` to fail 34 | - Fix `.flake8` to not cause errors with some versions of the `flake8` tool 35 | 36 | ## 0.10.0.2 37 | 38 | - Strings containing escapes are now unescaped, both for messages in error records, which were previously mangled (#57), and textual records, which were previously left escaped (#58) 39 | - Dropped support for Python 3.6 and added explicit support for Python 3.9 and 3.10. 40 | 41 | ## 0.10.0.1 42 | 43 | - Fix bug with `time_to_check_for_additional_output_sec`, as it was not being used when passed to `GdbController` 44 | 45 | ## 0.10.0.0 46 | 47 | **Breaking Changes** 48 | 49 | - Drop support for Python 3.5 50 | - Update `GdbController()` API. New API is `GdbController(command: Optional[List[str]], time_to_check_for_additional_output_sec: Optional[int])`. 51 | - `GdbController.verify_valid_gdb_subprocess()` was removed 52 | - Remove `NoGdbProcessError` error 53 | 54 | Other Changes 55 | 56 | - Add new `IoManager` class to handle more generic use-cases 57 | - [dev] use pytest for testing 58 | - gdb mi parsing remains unchanged 59 | 60 | ## 0.9.0.3 61 | 62 | - Drop support for 2.7, 3.4 63 | - Add support for 3.7, 3.8 64 | - Add `py.typed` file so mypy can enforce type hints on `pygdbmi` 65 | - Do not log in StringStream (#36) 66 | - Updates to build and CI tests (use nox) 67 | - Use mkdocs and mkdocstrings 68 | - Doc updates 69 | 70 | ## 0.9.0.2 71 | 72 | - More doc updates 73 | 74 | ## 0.9.0.1 75 | 76 | - Update docs 77 | 78 | ## 0.9.0.0 79 | 80 | - Stop buffering output 81 | - Use logger in GdbController; modify `verbose` arguments. 82 | - Remove support for Python 3.3 83 | 84 | ## 0.8.4.0 85 | 86 | - Add method `get_subprocess_cmd` to view the gdb command run in the shell 87 | 88 | ## 0.8.3.0 89 | 90 | - Improve reading gdb responses on unix (performance, bugfix) (@mouuff) 91 | 92 | ## 0.8.2.0 93 | 94 | - Add support for [record and replay (rr) gdb supplement](http://rr-project.org/) 95 | 96 | ## 0.8.1.1 97 | 98 | - Discard unexpected text from gdb 99 | 100 | ## 0.8.1.0 101 | 102 | - Add native Windows support 103 | 104 | ## 0.8.0.0 105 | 106 | - Make parsing more efficient when gdb outputs large strings 107 | - Add new methods to GdbController class: `spawn_new_gdb_subprocess`, `send_signal_to_gdb`, and `interrupt_gdb` 108 | 109 | ## 0.7.4.5 110 | 111 | - Update setup.py 112 | 113 | ## 0.7.4.4 114 | 115 | - Fix windows ctypes import (#23, @rudolfwalter) 116 | 117 | ## 0.7.4.3 118 | 119 | - Workaround gdb bug with repeated dictionary keys 120 | 121 | ## 0.7.4.2 122 | 123 | - Improved buffering of incomplete gdb mi output (@trapito) 124 | - Remove support of Python 3.2 125 | 126 | ## 0.7.4.1 127 | 128 | - Preserve leading and trailing spaces in gdb/mi output (plus unit tests) 129 | - Add unit test for buffering of gdb/mi output 130 | - Documentation updates 131 | - Refactoring 132 | 133 | ## 0.7.4.0 134 | 135 | - Add more exception types (`NoGdbProcessError`, `GdbTimeoutError`) 136 | - Add logic fixes for Windows (@johncf) 137 | - Use codecs.open() to open the readme.rst, to prevent locale related bugs (@mariusmue) 138 | 139 | ## 0.7.3.3 140 | 141 | - Add alternate pipe implementation for Windows 142 | 143 | ## 0.7.3.2 144 | 145 | - Replace `epoll` with `select` for osx compatibility (@felipesere) 146 | 147 | ## 0.7.3.1 148 | 149 | - Fix README 150 | 151 | ## 0.7.3.0 152 | 153 | - Add support for gdb/mi (optional) tokens (@mariusmue) 154 | -------------------------------------------------------------------------------- /pygdbmi/gdbcontroller.py: -------------------------------------------------------------------------------- 1 | """This module defines the `GdbController` class 2 | which runs gdb as a subprocess and can write to it and read from it to get 3 | structured output. 4 | """ 5 | 6 | import logging 7 | import shutil 8 | import subprocess 9 | from typing import Dict, List, Optional, Union 10 | 11 | from pygdbmi.constants import ( 12 | DEFAULT_GDB_TIMEOUT_SEC, 13 | DEFAULT_TIME_TO_CHECK_FOR_ADDITIONAL_OUTPUT_SEC, 14 | ) 15 | from pygdbmi.IoManager import IoManager 16 | 17 | 18 | __all__ = ["GdbController"] 19 | 20 | 21 | DEFAULT_GDB_LAUNCH_COMMAND = ["gdb", "--nx", "--quiet", "--interpreter=mi3"] 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class GdbController: 26 | def __init__( 27 | self, 28 | command: Optional[List[str]] = None, 29 | time_to_check_for_additional_output_sec: float = DEFAULT_TIME_TO_CHECK_FOR_ADDITIONAL_OUTPUT_SEC, 30 | ) -> None: 31 | """ 32 | Run gdb as a subprocess. Send commands and receive structured output. 33 | Create new object, along with a gdb subprocess 34 | 35 | Args: 36 | command: Command to run in shell to spawn new gdb subprocess 37 | time_to_check_for_additional_output_sec: When parsing responses, wait this amout of time before exiting (exits before timeout is reached to save time). If <= 0, full timeout time is used. 38 | Returns: 39 | New GdbController object 40 | """ 41 | 42 | if command is None: 43 | command = DEFAULT_GDB_LAUNCH_COMMAND 44 | 45 | if not any([("--interpreter=mi" in c) for c in command]): 46 | logger.warning( 47 | "Adding `--interpreter=mi3` (or similar) is recommended to get structured output. " 48 | + "See https://sourceware.org/gdb/onlinedocs/gdb/Mode-Options.html#Mode-Options." 49 | ) 50 | self.abs_gdb_path = None # abs path to gdb executable 51 | self.command: List[str] = command 52 | self.time_to_check_for_additional_output_sec = ( 53 | time_to_check_for_additional_output_sec 54 | ) 55 | self.gdb_process: Optional[subprocess.Popen] = None 56 | self._allow_overwrite_timeout_times = ( 57 | self.time_to_check_for_additional_output_sec > 0 58 | ) 59 | gdb_path = command[0] 60 | if not gdb_path: 61 | raise ValueError("a valid path to gdb must be specified") 62 | 63 | else: 64 | abs_gdb_path = shutil.which(gdb_path) 65 | if abs_gdb_path is None: 66 | raise ValueError( 67 | 'gdb executable could not be resolved from "%s"' % gdb_path 68 | ) 69 | 70 | else: 71 | self.abs_gdb_path = abs_gdb_path 72 | 73 | self.spawn_new_gdb_subprocess() 74 | 75 | def spawn_new_gdb_subprocess(self) -> int: 76 | """Spawn a new gdb subprocess with the arguments supplied to the object 77 | during initialization. If gdb subprocess already exists, terminate it before 78 | spanwing a new one. 79 | Return int: gdb process id 80 | """ 81 | if self.gdb_process: 82 | logger.debug( 83 | "Killing current gdb subprocess (pid %d)" % self.gdb_process.pid 84 | ) 85 | self.exit() 86 | 87 | logger.debug(f'Launching gdb: {" ".join(self.command)}') 88 | 89 | # Use pipes to the standard streams 90 | self.gdb_process = subprocess.Popen( 91 | self.command, 92 | shell=False, 93 | stdout=subprocess.PIPE, 94 | stdin=subprocess.PIPE, 95 | stderr=subprocess.PIPE, 96 | bufsize=0, 97 | ) 98 | 99 | assert self.gdb_process.stdin is not None 100 | assert self.gdb_process.stdout is not None 101 | self.io_manager = IoManager( 102 | self.gdb_process.stdin, 103 | self.gdb_process.stdout, 104 | self.gdb_process.stderr, 105 | self.time_to_check_for_additional_output_sec, 106 | ) 107 | return self.gdb_process.pid 108 | 109 | def get_gdb_response( 110 | self, 111 | timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, 112 | raise_error_on_timeout: bool = True, 113 | ) -> List[Dict]: 114 | """Get gdb response. See IoManager.get_gdb_response() for details""" 115 | return self.io_manager.get_gdb_response(timeout_sec, raise_error_on_timeout) 116 | 117 | def write( 118 | self, 119 | mi_cmd_to_write: Union[str, List[str]], 120 | timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, 121 | raise_error_on_timeout: bool = True, 122 | read_response: bool = True, 123 | ) -> List[Dict]: 124 | """Write command to gdb. See IoManager.write() for details""" 125 | return self.io_manager.write( 126 | mi_cmd_to_write, timeout_sec, raise_error_on_timeout, read_response 127 | ) 128 | 129 | def exit(self) -> None: 130 | """Terminate gdb process""" 131 | if self.gdb_process: 132 | self.gdb_process.terminate() 133 | self.gdb_process.wait() 134 | self.gdb_process.communicate() 135 | self.gdb_process = None 136 | return None 137 | -------------------------------------------------------------------------------- /tests/test_gdbcontroller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Unit tests 4 | 5 | Run from top level directory: ./tests/test_app.py 6 | """ 7 | 8 | import os 9 | import random 10 | import shutil 11 | import subprocess 12 | 13 | import pytest 14 | 15 | from pygdbmi.constants import USING_WINDOWS, GdbTimeoutError 16 | from pygdbmi.gdbcontroller import GdbController 17 | 18 | 19 | if USING_WINDOWS: 20 | MAKE_CMD = "mingw32-make.exe" 21 | else: 22 | MAKE_CMD = "make" 23 | 24 | 25 | def _get_c_program(makefile_target_name: str, binary_name: str) -> str: 26 | """build c program and return path to binary""" 27 | if not shutil.which(MAKE_CMD): 28 | raise AssertionError( 29 | 'Could not find executable "%s". Ensure it is installed and on your $PATH.' 30 | % MAKE_CMD 31 | ) 32 | 33 | SAMPLE_C_CODE_DIR = os.path.join( 34 | os.path.dirname(os.path.realpath(__file__)), "sample_c_app" 35 | ) 36 | binary_path = os.path.join(SAMPLE_C_CODE_DIR, binary_name) 37 | # Build C program 38 | subprocess.check_output( 39 | [MAKE_CMD, makefile_target_name, "-C", SAMPLE_C_CODE_DIR, "--quiet"] 40 | ) 41 | return binary_path 42 | 43 | 44 | def test_controller() -> None: 45 | """Build a simple C program, then run it with GdbController and verify the output is parsed 46 | as expected""" 47 | 48 | # Initialize object that manages gdb subprocess 49 | gdbmi = GdbController() 50 | 51 | c_hello_world_binary = _get_c_program("hello", "pygdbmiapp.a") 52 | 53 | if USING_WINDOWS: 54 | c_hello_world_binary = c_hello_world_binary.replace("\\", "/") 55 | # Load the binary and its symbols in the gdb subprocess 56 | responses = gdbmi.write( 57 | "-file-exec-and-symbols %s" % c_hello_world_binary, timeout_sec=1 58 | ) 59 | 60 | # Verify output was parsed into a list of responses 61 | assert len(responses) != 0 62 | response = responses[0] 63 | assert set(response.keys()) == {"message", "type", "payload", "stream", "token"} 64 | 65 | assert response["message"] == "thread-group-added" 66 | assert response["type"] == "notify" 67 | assert response["payload"] == {"id": "i1"} 68 | assert response["stream"] == "stdout" 69 | assert response["token"] is None 70 | 71 | responses = gdbmi.write(["-file-list-exec-source-files", "-break-insert main"]) 72 | assert len(responses) != 0 73 | 74 | responses = gdbmi.write(["-exec-run", "-exec-continue"], timeout_sec=3) 75 | 76 | # Test GdbTimeoutError exception 77 | with pytest.raises(GdbTimeoutError): 78 | gdbmi.get_gdb_response(timeout_sec=0) 79 | 80 | # Close gdb subprocess 81 | gdbmi.exit() 82 | assert gdbmi.gdb_process is None 83 | 84 | # Test NoGdbProcessError exception 85 | got_no_process_exception = False 86 | try: 87 | responses = gdbmi.write("-file-exec-and-symbols %s" % c_hello_world_binary) 88 | except OSError: 89 | got_no_process_exception = True 90 | assert got_no_process_exception is True 91 | 92 | # Respawn and test signal handling 93 | gdbmi.spawn_new_gdb_subprocess() 94 | responses = gdbmi.write( 95 | "-file-exec-and-symbols %s" % c_hello_world_binary, timeout_sec=1 96 | ) 97 | responses = gdbmi.write(["-break-insert main", "-exec-run"]) 98 | 99 | 100 | @pytest.mark.skip() 101 | def test_controller_buffer_randomized() -> None: 102 | """ 103 | The following code reads a sample gdb mi stream randomly to ensure partial 104 | output is read and that the buffer is working as expected on all streams. 105 | """ 106 | # Note that this code, since it was written, broke even furthere. For instance, some of the 107 | # attributes accessed by this test don't exist any more (see the `type: ignore[attr-defined]` 108 | # comments). 109 | 110 | test_directory = os.path.dirname(os.path.abspath(__file__)) 111 | datafile_path = "%s/response_samples.txt" % (test_directory) 112 | 113 | gdbmi = GdbController() 114 | for stream in gdbmi._incomplete_output.keys(): # type: ignore[attr-defined] 115 | responses = [] 116 | with open(datafile_path, "rb") as f: 117 | while True: 118 | n = random.randint(1, 100) 119 | # read random number of bytes to simulate incomplete responses 120 | gdb_mi_simulated_output = f.read(n) 121 | if gdb_mi_simulated_output == b"": 122 | break # EOF 123 | 124 | # let the controller try to parse this additional raw gdb output 125 | responses += gdbmi._get_responses_list(gdb_mi_simulated_output, stream) # type: ignore[attr-defined] 126 | assert len(responses) == 141 127 | 128 | # spot check a few 129 | assert responses[0] == { 130 | "message": None, 131 | "type": "console", 132 | "payload": "0x00007fe2c5c58920 in __nanosleep_nocancel () at ../sysdeps/unix/syscall-template.S:81\\n", 133 | "stream": stream, 134 | } 135 | if not USING_WINDOWS: 136 | # can't get this to pass in windows 137 | assert responses[71] == { 138 | "stream": stream, 139 | "message": "done", 140 | "type": "result", 141 | "payload": None, 142 | "token": None, 143 | } 144 | assert responses[82] == { 145 | "message": None, 146 | "type": "output", 147 | "payload": "The inferior program printed this! Can you still parse it?", 148 | "stream": stream, 149 | } 150 | assert responses[137] == { 151 | "stream": stream, 152 | "message": "thread-group-exited", 153 | "type": "notify", 154 | "payload": {"exit-code": "0", "id": "i1"}, 155 | "token": None, 156 | } 157 | assert responses[138] == { 158 | "stream": stream, 159 | "message": "thread-group-started", 160 | "type": "notify", 161 | "payload": {"pid": "48337", "id": "i1"}, 162 | "token": None, 163 | } 164 | assert responses[139] == { 165 | "stream": stream, 166 | "message": "tsv-created", 167 | "type": "notify", 168 | "payload": {"name": "trace_timestamp", "initial": "0"}, 169 | "token": None, 170 | } 171 | assert responses[140] == { 172 | "stream": stream, 173 | "message": "tsv-created", 174 | "type": "notify", 175 | "payload": {"name": "trace_timestamp", "initial": "0"}, 176 | "token": None, 177 | } 178 | 179 | for stream in gdbmi._incomplete_output.keys(): # type: ignore[attr-defined] 180 | assert gdbmi._incomplete_output[stream] is None # type: ignore[attr-defined] 181 | -------------------------------------------------------------------------------- /tests/test_gdbmiparser.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from typing import Any, Dict 3 | 4 | import pytest 5 | 6 | from pygdbmi.gdbmiparser import parse_response 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "response, expected_dict", 11 | [ 12 | # Test basic types. 13 | ( 14 | "^done", 15 | {"type": "result", "payload": None, "message": "done", "token": None}, 16 | ), 17 | ( 18 | '~"done"', 19 | {"type": "console", "payload": "done", "message": None}, 20 | ), 21 | ( 22 | '@"done"', 23 | {"type": "target", "payload": "done", "message": None}, 24 | ), 25 | ( 26 | '&"done"', 27 | {"type": "log", "payload": "done", "message": None}, 28 | ), 29 | ( 30 | "done", 31 | {"type": "output", "payload": "done", "message": None}, 32 | ), 33 | # Test escape sequences, 34 | ( 35 | '~""', 36 | {"type": "console", "payload": "", "message": None}, 37 | ), 38 | ( 39 | r'~"\b\f\n\r\t\""', 40 | {"type": "console", "payload": '\b\f\n\r\t"', "message": None}, 41 | ), 42 | ( 43 | '@""', 44 | {"type": "target", "payload": "", "message": None}, 45 | ), 46 | ( 47 | r'@"\b\f\n\r\t\""', 48 | {"type": "target", "payload": '\b\f\n\r\t"', "message": None}, 49 | ), 50 | ('&""', {"type": "log", "payload": "", "message": None}), 51 | ( 52 | r'&"\b\f\n\r\t\""', 53 | {"type": "log", "payload": '\b\f\n\r\t"', "message": None}, 54 | ), 55 | # Test that an escaped backslash gets captured. 56 | ( 57 | r'&"\\"', 58 | {"type": "log", "payload": "\\", "message": None}, 59 | ), 60 | # Test that a dictionary with repeated keys (a gdb bug) is gracefully worked-around by pygdbmi 61 | # See https://sourceware.org/bugzilla/show_bug.cgi?id=22217 62 | # and https://github.com/cs01/pygdbmi/issues/19 63 | ( 64 | '^done,thread-ids={thread-id="3",thread-id="2",thread-id="1"}, current-thread-id="1",number-of-threads="3"', 65 | { 66 | "type": "result", 67 | "payload": { 68 | "thread-ids": {"thread-id": ["3", "2", "1"]}, 69 | "current-thread-id": "1", 70 | "number-of-threads": "3", 71 | }, 72 | "message": "done", 73 | "token": None, 74 | }, 75 | ), 76 | # Test errors. 77 | ( 78 | r'^error,msg="some message"', 79 | { 80 | "type": "result", 81 | "message": "error", 82 | "payload": {"msg": "some message"}, 83 | "token": None, 84 | }, 85 | ), 86 | ( 87 | r'^error,msg="some message",code="undefined-command"', 88 | { 89 | "type": "result", 90 | "message": "error", 91 | "payload": {"msg": "some message", "code": "undefined-command"}, 92 | "token": None, 93 | }, 94 | ), 95 | ( 96 | r'^error,msg="message\twith\nescapes"', 97 | { 98 | "type": "result", 99 | "message": "error", 100 | "payload": {"msg": "message\twith\nescapes"}, 101 | "token": None, 102 | }, 103 | ), 104 | ( 105 | r'^error,msg="This is a double quote: <\">"', 106 | { 107 | "type": "result", 108 | "message": "error", 109 | "payload": {"msg": 'This is a double quote: <">'}, 110 | "token": None, 111 | }, 112 | ), 113 | ( 114 | r'^error,msg="This is a double quote: <\">",code="undefined-command"', 115 | { 116 | "type": "result", 117 | "message": "error", 118 | "payload": { 119 | "msg": 'This is a double quote: <">', 120 | "code": "undefined-command", 121 | }, 122 | "token": None, 123 | }, 124 | ), 125 | # Test a real world dictionary. 126 | ( 127 | '=breakpoint-modified,bkpt={number="1",empty_arr=[],type="breakpoint",disp="keep",enabled="y",addr="0x000000000040059c",func="main",file="hello.c",fullname="/home/git/pygdbmi/tests/sample_c_app/hello.c",line="9",thread-groups=["i1"],times="1",original-location="hello.c:9"}', 128 | { 129 | "message": "breakpoint-modified", 130 | "payload": { 131 | "bkpt": { 132 | "addr": "0x000000000040059c", 133 | "disp": "keep", 134 | "enabled": "y", 135 | "file": "hello.c", 136 | "fullname": "/home/git/pygdbmi/tests/sample_c_app/hello.c", 137 | "func": "main", 138 | "line": "9", 139 | "number": "1", 140 | "empty_arr": [], 141 | "original-location": "hello.c:9", 142 | "thread-groups": ["i1"], 143 | "times": "1", 144 | "type": "breakpoint", 145 | } 146 | }, 147 | "type": "notify", 148 | "token": None, 149 | }, 150 | ), 151 | # Test records with token. 152 | ( 153 | "1342^done", 154 | {"type": "result", "payload": None, "message": "done", "token": 1342}, 155 | ), 156 | # Test extra characters at end of dictionary are discarded (issue #30). 157 | ( 158 | '=event,name="gdb"discardme', 159 | { 160 | "type": "notify", 161 | "payload": {"name": "gdb"}, 162 | "message": "event", 163 | "token": None, 164 | }, 165 | ), 166 | # Test async records status changes. 167 | ( 168 | '*running,thread-id="all"', 169 | { 170 | "type": "notify", 171 | "payload": {"thread-id": "all"}, 172 | "message": "running", 173 | "token": None, 174 | }, 175 | ), 176 | ( 177 | "*stopped", 178 | { 179 | "type": "notify", 180 | "payload": None, 181 | "message": "stopped", 182 | "token": None, 183 | }, 184 | ), 185 | ], 186 | ) 187 | def test_parser(response: str, expected_dict: Dict[str, Any]) -> None: 188 | """Test that the parser returns dictionaries from gdb mi strings as expected""" 189 | assert parse_response(response) == expected_dict 190 | 191 | 192 | def _get_test_input(n_repetitions: int) -> str: 193 | data = ", ".join( 194 | ['"/a/path/to/parse/' + str(i) + '"' for i in range(n_repetitions)] 195 | ) 196 | return "=test-message,test-data=[" + data + "]" 197 | 198 | 199 | def _get_avg_time_to_parse(input_str: str, num_runs: int) -> float: 200 | avg_time = 0.0 201 | for _ in range(num_runs): 202 | t0 = time() 203 | parse_response(input_str) 204 | t1 = time() 205 | time_to_run = t1 - t0 206 | avg_time += time_to_run / num_runs 207 | return avg_time 208 | 209 | 210 | def test_performance_big_o() -> None: 211 | num_runs = 2 212 | 213 | large_input_len = 100000 214 | 215 | single_input = _get_test_input(1) 216 | large_input = _get_test_input(large_input_len) 217 | 218 | t_small = _get_avg_time_to_parse(single_input, num_runs) or 0.0001 219 | t_large = _get_avg_time_to_parse(large_input, num_runs) 220 | bigo_n = (t_large / large_input_len) / t_small 221 | assert bigo_n < 1 # with old parser, this was over 3 222 | -------------------------------------------------------------------------------- /pygdbmi/gdbescapes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for unescaping strings produced by GDB MI. 3 | """ 4 | 5 | import re 6 | from typing import Iterator, Tuple 7 | 8 | 9 | __all__ = [ 10 | "advance_past_string_with_gdb_escapes", 11 | "unescape", 12 | ] 13 | 14 | 15 | def unescape(escaped_str: str) -> str: 16 | """Unescape a string escaped by GDB in MI mode. 17 | 18 | Args: 19 | escaped_str: String to unescape (without initial and final double quote). 20 | 21 | Returns: 22 | The strings with escape codes transformed into normal characters. 23 | """ 24 | unescaped_str, after_string_index = _unescape_internal( 25 | escaped_str, expect_closing_quote=False 26 | ) 27 | assert after_string_index == -1, ( 28 | f"after_string_index is {after_string_index} but it was " 29 | "expected to be -1 as expect_closing_quote is set to False" 30 | ) 31 | return unescaped_str 32 | 33 | 34 | def advance_past_string_with_gdb_escapes( 35 | escaped_str: str, *, start: int = 0 36 | ) -> Tuple[str, int]: 37 | """Unescape a string escaped by GDB in MI mode, and find the double quote 38 | terminating it. 39 | 40 | Args: 41 | escaped_str: String to unescape (without initial double quote). 42 | start: the position in escaped_str at which to start unescaping the string 43 | 44 | Returns: 45 | A tuple containing the unescaped string and the index in escaped_str just after 46 | the escape string (that is, escaped_str[start-1] is always the closing double 47 | quote and escaped_str[start:] is the portion of escaped_str after the escaped 48 | string). 49 | """ 50 | return _unescape_internal(escaped_str, expect_closing_quote=True, start=start) 51 | 52 | 53 | # Regular expression matching both escapes and unescaped quotes in GDB MI escaped 54 | # strings. 55 | _ESCAPES_RE = re.compile( 56 | r""" 57 | # Match all text before an escape or quote so it can be preserved as is. 58 | (?P 59 | .*? 60 | ) 61 | # Match either an escape or an unescaped quote. 62 | ( 63 | ( 64 | # All escapes start with a backslash... 65 | \\ 66 | # ... and are followed by either a 3-digit octal number or a single 67 | # character for common escapes. See _GDB_MI_NON_OCTAL_ESCAPES for valid 68 | # ones. 69 | ( 70 | # Instead of matching a single octal escape we match multiple ones in a 71 | # row. 72 | # This is because a single Unicode character can be encoded as multiple 73 | # escape sequences so, if we decoded the escape sequences one at a time, 74 | # the resulting string would not be valid until all the bytes are 75 | # converted. 76 | # This could also be solved by converting the input string into bytes 77 | # but that's much slower for long strings. 78 | (?P 79 | # First octal number without backslash which we matched earlier. 80 | [0-7]{3} 81 | # Addional (and optional) octal numbers, including a backslash. 82 | ( 83 | \\ 84 | [0-7]{3} 85 | )* 86 | ) 87 | | 88 | (?P.) 89 | ) 90 | ) 91 | | 92 | # Match an unescaped quote. 93 | # If expect_closing_quote is true, then this means the string is finished. 94 | # If false, then the quote should have been escaped. 95 | (?P") 96 | ) 97 | """, 98 | flags=re.VERBOSE, 99 | ) 100 | 101 | # Map from single character escape codes allowed in GDB MI strings to the corresponding 102 | # unescaped value. 103 | _NON_OCTAL_ESCAPES = { 104 | "'": "'", 105 | "\\": "\\", 106 | "a": "\a", 107 | "b": "\b", 108 | "e": "\033", 109 | "f": "\f", 110 | "n": "\n", 111 | "r": "\r", 112 | "t": "\t", 113 | '"': '"', 114 | } 115 | 116 | 117 | def _unescape_internal( 118 | escaped_str: str, *, expect_closing_quote: bool, start: int = 0 119 | ) -> Tuple[str, int]: 120 | """Common code for unescaping strings escaped by GDB in MI mode. 121 | 122 | MI-mode escapes are similar to standard Python escapes but: 123 | * "\\e" is a valid escape. 124 | * "\\NNN" escapes use numbers represented in octal format. 125 | For instance, "\\040" encodes character 0o40, that is character 32 in decimal, 126 | that is a space. 127 | 128 | For details, see printchar in gdb/utils.c in the binutils-gdb repo. 129 | 130 | Args: 131 | escaped_str: String to unescape 132 | expect_closing_quote: If true the closing quote must be in escaped_str[start:]. 133 | Otherwise, no unescaped quote is allowed. 134 | start: the position in escaped_str at which to start unescaping the string. 135 | 136 | Returns: 137 | A tuple containing the unescaped string and the index in escaped_str just after 138 | the escape string, or -1 if expect_closing_quote is False. 139 | """ 140 | # The _ESCAPES_RE expression only matches escapes or unescaped quotes, plus the 141 | # preeeding part of the escaped string. 142 | # This variable tracks the end of the last match so the portion of escaped_str after 143 | # that is not lost. 144 | unmatched_start_index = start 145 | 146 | # Was the closing quote found? 147 | # This can be true only if expect_closing_quote is true. 148 | found_closing_quote = False 149 | 150 | unescaped_parts = [] 151 | for match in _ESCAPES_RE.finditer(escaped_str, pos=start): 152 | # Text before the match (and after any previous match). 153 | unescaped_parts.append(match["before"]) 154 | 155 | escaped_octal = match["escaped_octal"] 156 | escaped_char = match["escaped_char"] 157 | unescaped_quote = match["unescaped_quote"] 158 | 159 | _, unmatched_start_index = match.span() 160 | 161 | if escaped_octal is not None: 162 | # We found one or more octal escapes. These are in the form "NNN" or, for 163 | # multiple characters in a row, "NNN\NNN\NNN[...]". 164 | # escaped_octal is guaranteed to be in the correct format by _ESCAPES_RE. 165 | octal_sequence_bytes = bytearray() 166 | # Strip the backslashes and iterate over the octal codes 3 by 3. 167 | for octal_number in _split_n_chars(escaped_octal.replace("\\", ""), 3): 168 | # Convert the 3 digits into a single byte. 169 | try: 170 | octal_sequence_bytes.append(int(octal_number, base=8)) 171 | except ValueError as exc: 172 | raise ValueError( 173 | f"Invalid octal number {octal_number!r} in {escaped_str!r}" 174 | ) from exc 175 | try: 176 | replaced = octal_sequence_bytes.decode("utf-8") 177 | except UnicodeDecodeError: 178 | # GDB should never generate invalid sequences but, according to #64, 179 | # it can do that on Windows. In this case we just keep the sequence 180 | # unchanged. 181 | replaced = f"\\{escaped_octal}" 182 | 183 | elif escaped_char is not None: 184 | # We found a single escaped character. 185 | try: 186 | replaced = _NON_OCTAL_ESCAPES[escaped_char] 187 | except KeyError as exc: 188 | raise ValueError( 189 | f"Invalid escape character {escaped_char!r} in {escaped_str!r}" 190 | ) from exc 191 | 192 | elif unescaped_quote: 193 | # We found an unescaped quote. 194 | if not expect_closing_quote: 195 | raise ValueError(f"Unescaped quote found in {escaped_str!r}") 196 | 197 | # This is the ending quote, so stop processing. 198 | found_closing_quote = True 199 | break 200 | 201 | else: 202 | raise AssertionError( 203 | f"This code should not be reached for string {escaped_str!r}" 204 | ) 205 | 206 | unescaped_parts.append(replaced) 207 | 208 | if not found_closing_quote: 209 | if expect_closing_quote: 210 | raise ValueError(f"Missing closing quote in {escaped_str!r}") 211 | 212 | # Don't drop the part of the escaped string after the last escape. 213 | unescaped_parts.append(escaped_str[unmatched_start_index:]) 214 | # With expect_closing_quote being false, the whole string must always be matched 215 | # so unmatched_start_index is not useful so we set it to -1. 216 | # (We could set it to len(unmatched_start_index) as well but we would not get 217 | # any benefit from having it set to a correct value.) 218 | unmatched_start_index = -1 219 | 220 | return "".join(unescaped_parts), unmatched_start_index 221 | 222 | 223 | def _split_n_chars(s: str, n: int) -> Iterator[str]: 224 | """Iterates over string s `n` characters at a time""" 225 | for i in range(0, len(s), n): 226 | yield s[i : i + n] 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | pygdbmi - Get Structured Output from GDB's Machine Interface 3 |

4 | 5 |

6 | 7 | 8 | Test status 9 | 10 | 11 | PyPI version 12 | 13 |

14 | 15 | **Documentation** [https://cs01.github.io/pygdbmi](https://cs01.github.io/pygdbmi) 16 | 17 | **Source Code** [https://github.com/cs01/pygdbmi](https://github.com/cs01/pygdbmi) 18 | 19 | --- 20 | 21 | Python (**py**) [**gdb**](https://www.gnu.org/software/gdb/) machine interface [(**mi**)](https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI.html) 22 | 23 | > GDB/MI is a line based machine oriented text interface to GDB and is activated by specifying using the --interpreter command line option (see Mode Options). It is specifically intended to support the development of systems which use the debugger as just one small component of a larger system. 24 | 25 | ## What's in the box? 26 | 27 | 1. A function to parse gdb machine interface string output and return structured data types (Python dicts) that are JSON serializable. Useful for writing the backend to a gdb frontend. For example, [gdbgui](https://github.com/cs01/gdbgui) uses pygdbmi on the backend. 28 | 2. A Python class to control and interact with gdb as a subprocess 29 | 30 | To get [machine interface](https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI.html) output from gdb, run gdb with the `--interpreter=mi2` flag like so: 31 | 32 | ``` 33 | gdb --interpreter=mi2 34 | ``` 35 | 36 | ## Installation 37 | 38 | pip install pygdbmi 39 | 40 | ## Compatibility 41 | 42 | ### Operating Systems 43 | 44 | Cross platform support for Linux, macOS and Windows 45 | 46 | - Linux/Unix 47 | 48 | Ubuntu 14.04 and 16.04 have been tested to work. Other versions likely work as well. 49 | 50 | - macOS 51 | 52 | Note: the error `please check gdb is codesigned - see taskgated(8)` can be fixed by codesigning gdb with [these instructions](http://andresabino.com/2015/04/14/codesign-gdb-on-mac-os-x-yosemite-10-10-2/). If the error is not fixed, please [create an issue in github](https://github.com/cs01/pygdbmi/issues). 53 | 54 | - Windows 55 | 56 | Windows 10 has been tested to work with MinGW and cygwin. 57 | 58 | ### gdb versions 59 | 60 | - gdb 7.6+ has been tested. Older versions may work as well. 61 | 62 | ## Examples 63 | 64 | gdb mi defines a syntax for its output that is suitable for machine readability and scripting: [example output](https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Simple-Examples.html#GDB_002fMI-Simple-Examples): 65 | 66 | ``` 67 | -> -break-insert main 68 | <- ^done,bkpt={number="1",type="breakpoint",disp="keep", 69 | enabled="y",addr="0x08048564",func="main",file="myprog.c", 70 | fullname="/home/myprog.c",line="68",thread-groups=["i1"], 71 | times="0"} 72 | <- (gdb) 73 | ``` 74 | 75 | Use `pygdbmi.gdbmiparser.parse_response` to turn that string output into a JSON serializable dictionary 76 | 77 | ```python 78 | from pygdbmi import gdbmiparser 79 | from pprint import pprint 80 | response = gdbmiparser.parse_response('^done,bkpt={number="1",type="breakpoint",disp="keep", enabled="y",addr="0x08048564",func="main",file="myprog.c",fullname="/home/myprog.c",line="68",thread-groups=["i1"],times="0"') 81 | pprint(response) 82 | pprint(response) 83 | # Prints: 84 | # {'message': 'done', 85 | # 'payload': {'bkpt': {'addr': '0x08048564', 86 | # 'disp': 'keep', 87 | # 'enabled': 'y', 88 | # 'file': 'myprog.c', 89 | # 'fullname': '/home/myprog.c', 90 | # 'func': 'main', 91 | # 'line': '68', 92 | # 'number': '1', 93 | # 'thread-groups': ['i1'], 94 | # 'times': '0', 95 | # 'type': 'breakpoint'}}, 96 | # 'token': None, 97 | # 'type': 'result'} 98 | ``` 99 | 100 | ## Programmatic Control Over gdb 101 | 102 | But how do you get the gdb output into Python in the first place? If you want, `pygdbmi` also has a class to control gdb as subprocess. You can write commands, and get structured output back: 103 | 104 | ```python 105 | from pygdbmi.gdbcontroller import GdbController 106 | from pprint import pprint 107 | 108 | # Start gdb process 109 | gdbmi = GdbController() 110 | print(gdbmi.command) # print actual command run as subprocess 111 | # Load binary a.out and get structured response 112 | response = gdbmi.write('-file-exec-file a.out') 113 | pprint(response) 114 | # Prints: 115 | # [{'message': 'thread-group-added', 116 | # 'payload': {'id': 'i1'}, 117 | # 'stream': 'stdout', 118 | # 'token': None, 119 | # 'type': 'notify'}, 120 | # {'message': 'done', 121 | # 'payload': None, 122 | # 'stream': 'stdout', 123 | # 'token': None, 124 | # 'type': 'result'}] 125 | ``` 126 | 127 | Now do whatever you want with gdb. All gdb commands, as well as gdb [machine interface commands](<(https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Input-Syntax.html#GDB_002fMI-Input-Syntax)>) are acceptable. gdb mi commands give better structured output that is machine readable, rather than gdb console output. mi commands begin with a `-`. 128 | 129 | ```python 130 | response = gdbmi.write('-break-insert main') # machine interface (MI) commands start with a '-' 131 | response = gdbmi.write('break main') # normal gdb commands work too, but the return value is slightly different 132 | response = gdbmi.write('-exec-run') 133 | response = gdbmi.write('run') 134 | response = gdbmi.write('-exec-next', timeout_sec=0.1) # the wait time can be modified from the default of 1 second 135 | response = gdbmi.write('next') 136 | response = gdbmi.write('next', raise_error_on_timeout=False) 137 | response = gdbmi.write('next', raise_error_on_timeout=True, timeout_sec=0.01) 138 | response = gdbmi.write('-exec-continue') 139 | response = gdbmi.send_signal_to_gdb('SIGKILL') # name of signal is okay 140 | response = gdbmi.send_signal_to_gdb(2) # value of signal is okay too 141 | response = gdbmi.interrupt_gdb() # sends SIGINT to gdb 142 | response = gdbmi.write('continue') 143 | response = gdbmi.exit() 144 | ``` 145 | 146 | ## Parsed Output Format 147 | 148 | Each parsed gdb response consists of a list of dictionaries. Each dictionary has keys `message`, `payload`, `token`, and `type`. 149 | 150 | - `message` contains a textual message from gdb, which is not always present. When missing, this is `None`. 151 | - `payload` contains the content of gdb's output, which can contain any of the following: `dictionary`, `list`, `string`. This too is not always present, and can be `None` depending on the response. 152 | - `token` If an input command was prefixed with a (optional) token then the corresponding output for that command will also be prefixed by that same token. This field is only present for pygdbmi output types `nofity` and `result`. When missing, this is `None`. 153 | 154 | The `type` is defined based on gdb's various [mi output record types](<(https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Output-Records.html#GDB_002fMI-Output-Records)>), and can be 155 | 156 | - `result` - the result of a gdb command, such as `done`, `running`, `error`, etc. 157 | - `notify` - additional async changes that have occurred, such as breakpoint modified 158 | - `console` - textual responses to cli commands 159 | - `log` - debugging messages from gdb's internals 160 | - `output` - output from target 161 | - `target` - output from remote target 162 | - `done` - when gdb has finished its output 163 | 164 | ## Contributing 165 | 166 | Documentation fixes, bug fixes, performance improvements, and functional improvements are welcome. You may want to create an issue before beginning work to make sure I am interested in merging it to the master branch. 167 | 168 | pygdbmi uses [nox](https://github.com/theacodes/nox) for automation. 169 | 170 | See available tasks with 171 | 172 | ``` 173 | nox -l 174 | ``` 175 | 176 | Run tests and lint with 177 | 178 | ``` 179 | nox -s tests 180 | nox -s lint 181 | ``` 182 | 183 | Positional arguments passed to `nox -s tests` are passed directly to `pytest`. For instance, to run only the parse tests use 184 | 185 | ``` 186 | nox -s tests -- tests/test_gdbmiparser.py 187 | ``` 188 | 189 | See [`pytest`'s documentation](https://docs.pytest.org/) for more details on how to run tests. 190 | 191 | To format code using the correct settings use 192 | 193 | ``` 194 | nox -s format 195 | ``` 196 | 197 | Or, to format only specified files, use 198 | 199 | ``` 200 | nox -s format -- example.py pygdbmi/IoManager.py 201 | ``` 202 | 203 | ### Making a release 204 | 205 | Only maintainers of the [pygdbmi package on PyPi](https://pypi.org/project/pygdbmi/) can make a release. 206 | 207 | In the following steps, replace these strings with the correct values: 208 | 209 | - `` is the name of the remote for the main pygdbmi repository (for instance, `origin`) 210 | - `` is the version number chosen in step 2. 211 | 212 | To make a release: 213 | 214 | 1. Checkout the `master` branch and pull from the main repository with `git pull master` 215 | 2. Decide the version number for the new release; we follow 216 | [Semantic Versioning](https://semver.org/) but prefixing the version with `0.`: given a version 217 | number _0.SECOND.THIRD.FOURTH_, increment the: 218 | - _SECOND_ component when you make incompatible API changes 219 | - _THIRD_ component when you add functionality in a backwards compatible manner 220 | - _FOURTH_ component when you make backwards compatible bug fixes 221 | 3. Update `CHANGELOG.md` to list the chosen version number instead of `## dev` 222 | 4. Update `__version__` in `pygdbmi/__init__.py` to the chosen version number 223 | 5. Create a branch, for instance using `git checkout -b before-release-` 224 | 6. Commit your changes, for instance using `git commit -a -m 'Bump version to for release'` 225 | 7. Check that the docs look fine by serving them locally with `nox -s serve_docs` 226 | 8. Push the branch, for instance with `git push --set-upstream before-release-` 227 | 9. If tests pass on the PR you created, you can merge into `master` 228 | 10. Go to the [new release page](https://github.com/cs01/pygdbmi/releases/new) and prepare the 229 | release: 230 | - Add a tag in the form `v` (for example `v0.1.2.3`) 231 | - Set the title to `pygdbmi v` (for example `pygdbmi v0.1.2.3`) 232 | - Copy and paste the section for the new release only from `CHANGELOG.md` excluding the line 233 | with the version number 234 | - Press “Publish release” 235 | 10. Publish the release to PyPI with `nox -s publish` 236 | 11. Publish the docs with `nox -s publish_docs` 237 | 11. Verify that the [PyPi page for pygdbmi](https://pypi.org/project/pygdbmi/) looks correct 238 | 12. Verify that the [published docs](https://cs01.github.io/pygdbmi/) look correct 239 | 13. Prepare for changes for the next release by adding something like this above the previous 240 | entries in `CHANGELOG.md` (where `` is `` with the last digit increaded 241 | by 1): 242 | 243 | ``` 244 | ## .dev0 245 | 246 | - *Replace this line with new entries* 247 | ``` 248 | 249 | 14. Create a branch for the changes with `git checkout -b after-release-` 250 | 15. Commit the change with `git commit -m 'Prepare for work on the next release' CHANGELOG.md` 251 | 16. Push the branch with `git push --set-upstream after-release-` 252 | 17. If tests pass, merge into `master` 253 | 254 | ## Similar projects 255 | 256 | - [tsgdbmi](https://github.com/Guyutongxue/tsgdbmi) A port of pygdbmi to TypeScript 257 | - [danielzfranklin/gdbmi](https://github.com/danielzfranklin/gdbmi) A port of pygdbmi to Rust 258 | 259 | ## Projects Using pygdbmi 260 | 261 | - [gdbgui](https://github.com/cs01/gdbgui) implements a browser-based frontend to gdb, using pygdbmi on the backend 262 | - [PINCE](https://github.com/korcankaraokcu/PINCE) is a gdb frontend that aims to provide a reverse engineering tool and a reusable library focused on games. It uses pygdbmi to parse gdb/mi based output for some functions 263 | - [avatar²](https://github.com/avatartwo/avatar2) is an orchestration framework for reversing and analysing firmware of embedded devices. It utilizes pygdbmi for internal communication to different analysis targets. 264 | - [UDB](https://undo.io/udb) is a proprietary time-travel debugger for C and C++ based on GDB. It uses pygdbmi in its extensive test suite to parse the debugger's output. 265 | - [pwndbg-gui](https://github.com/AlEscher/pwndbg-gui) is a user-friendly graphical interface for [pwndbg](https://github.com/pwndbg/pwndbg), a tool that simplifies exploit development and reverse engineering with GDB. It uses pygdbmi to interact with GDB and get structured responses. 266 | - Know of another project? Create a PR and add it here. 267 | 268 | ## Authors 269 | 270 | - [Chad Smith](https://github.com/cs01) (main author and creator). 271 | - [Marco Barisione](http://www.barisione.org/) (co-maintainer). 272 | - [The community](https://github.com/cs01/pygdbmi/graphs/contributors). Thanks especially to @mariusmue, @bobthekingofegypt, @mouuff, and @felipesere. 273 | -------------------------------------------------------------------------------- /pygdbmi/gdbmiparser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python parser for gdb's machine interface interpreter. 3 | 4 | Parses string output from gdb with the `--interpreter=mi2` flag into 5 | structured objects. 6 | 7 | See more at https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI.html#GDB_002fMI 8 | """ 9 | 10 | import functools 11 | import logging 12 | import re 13 | from typing import Any, Callable, Dict, List, Match, Optional, Pattern, Tuple, Union 14 | 15 | from pygdbmi.gdbescapes import unescape 16 | from pygdbmi.printcolor import fmt_green 17 | from pygdbmi.StringStream import StringStream 18 | 19 | 20 | __all__ = [ 21 | "parse_response", 22 | "response_is_finished", 23 | ] 24 | 25 | 26 | _DEBUG = False 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | def _setup_logger(logger: logging.Logger, debug: bool) -> None: 31 | logger.propagate = False 32 | 33 | handler = logging.StreamHandler() 34 | handler.setFormatter( 35 | logging.Formatter("[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s") 36 | ) 37 | if debug: 38 | level = logging.DEBUG 39 | else: 40 | level = logging.ERROR 41 | 42 | logger.setLevel(level) 43 | logger.addHandler(handler) 44 | 45 | 46 | _setup_logger(logger, _DEBUG) 47 | 48 | 49 | def parse_response(gdb_mi_text: str) -> Dict: 50 | """Parse gdb mi text and turn it into a dictionary. 51 | 52 | See https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Stream-Records.html#GDB_002fMI-Stream-Records 53 | for details on types of gdb mi output. 54 | 55 | Args: 56 | gdb_mi_text: String output from gdb 57 | 58 | Returns: 59 | dictionary with keys "type", "message", "payload", "token" 60 | """ 61 | stream = StringStream(gdb_mi_text, debug=_DEBUG) 62 | 63 | for pattern, parser in _GDB_MI_PATTERNS_AND_PARSERS: 64 | match = pattern.match(gdb_mi_text) 65 | if match is not None: 66 | return parser(match, stream) 67 | 68 | # This was not gdb mi output, so it must have just been printed by 69 | # the inferior program that's being debugged 70 | return { 71 | "type": "output", 72 | "message": None, 73 | "payload": gdb_mi_text, 74 | } 75 | 76 | 77 | def response_is_finished(gdb_mi_text: str) -> bool: 78 | """Return true if the gdb mi response is ending 79 | 80 | Args: 81 | gdb_mi_text: String output from gdb 82 | 83 | Returns: 84 | True if gdb response is finished 85 | """ 86 | return _GDB_MI_RESPONSE_FINISHED_RE.match(gdb_mi_text) is not None 87 | 88 | 89 | # ======================================================================== 90 | # All functions and variables below are used internally to parse mi output 91 | # ======================================================================== 92 | 93 | 94 | def _parse_mi_notify(match: Match, stream: StringStream) -> Dict: 95 | """Parser function for matches against a notify record. 96 | 97 | See _GDB_MI_PATTERNS_AND_PARSERS for details.""" 98 | message = match["message"] 99 | logger.debug("parsed message") 100 | logger.debug("%s", fmt_green(message)) 101 | 102 | return { 103 | "type": "notify", 104 | "message": message.strip(), 105 | "payload": _extract_payload(match, stream), 106 | "token": _extract_token(match), 107 | } 108 | 109 | 110 | def _parse_mi_result(match: Match, stream: StringStream) -> Dict: 111 | """Parser function for matches against a result record. 112 | 113 | See _GDB_MI_PATTERNS_AND_PARSERS for details.""" 114 | return { 115 | "type": "result", 116 | "message": match["message"], 117 | "payload": _extract_payload(match, stream), 118 | "token": _extract_token(match), 119 | } 120 | 121 | 122 | def _parse_mi_output(match: Match, stream: StringStream, output_type: str) -> Dict: 123 | """Parser function for matches against a console, log or target record. 124 | 125 | The record type must be specified in output_type. 126 | 127 | See _GDB_MI_PATTERNS_AND_PARSERS for details.""" 128 | return { 129 | "type": output_type, 130 | "message": None, 131 | "payload": unescape(match["payload"]), 132 | } 133 | 134 | 135 | def _parse_mi_finished(match: Match, stream: StringStream) -> Dict: 136 | """Parser function for matches against a finished record. 137 | 138 | See _GDB_MI_PATTERNS_AND_PARSERS for details.""" 139 | return { 140 | "type": "done", 141 | "message": None, 142 | "payload": None, 143 | } 144 | 145 | 146 | def _extract_token(match: Match) -> Optional[int]: 147 | """Extract a token from a match against a regular expression which included 148 | _GDB_MI_COMPONENT_TOKEN.""" 149 | token = match["token"] 150 | return int(token) if token is not None else None 151 | 152 | 153 | def _extract_payload(match: Match, stream: StringStream) -> Optional[Dict]: 154 | """Extract a token from a match against a regular expression which included 155 | _GDB_MI_COMPONENT_PAYLOAD.""" 156 | if match["payload"] is None: 157 | return None 158 | 159 | stream.advance_past_chars([","]) 160 | return _parse_dict(stream) 161 | 162 | 163 | # A regular expression matching a response finished record. 164 | _GDB_MI_RESPONSE_FINISHED_RE = re.compile(r"^\(gdb\)\s*$") 165 | 166 | # Regular expression identifying a token in a MI record. 167 | _GDB_MI_COMPONENT_TOKEN = r"(?P\d+)?" 168 | # Regular expression identifying a payload in a MI record. 169 | _GDB_MI_COMPONENT_PAYLOAD = r"(?P,.*)?" 170 | 171 | # The type of the functions which parse MI records as used by 172 | # _GDB_MI_PATTERNS_AND_PARSERS. 173 | _PARSER_FUNCTION = Callable[[Match, StringStream], Dict] 174 | 175 | # A list where each item is a tuple of: 176 | # - A compiled regular expression matching a MI record. 177 | # - A function which is called if the regex matched with the match and a StringStream. 178 | # It must return a dictionary with details on the MI record.. 179 | # 180 | # For more details on the MI , see 181 | # https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Stream-Records.html#GDB_002fMI-Stream-Records 182 | # 183 | # The order matters as items are iterated in ordered and that stops once a match is 184 | # found. 185 | _GDB_MI_PATTERNS_AND_PARSERS: List[Tuple[Pattern, _PARSER_FUNCTION]] = [ 186 | # https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Result-Records.html#GDB_002fMI-Result-Records 187 | # In addition to a number of out-of-band notifications, 188 | # the response to a gdb/mi command includes one of the following result indications: 189 | # done, running, connected, error, exit 190 | ( 191 | re.compile( 192 | rf"^{_GDB_MI_COMPONENT_TOKEN}\^(?P\S+?){_GDB_MI_COMPONENT_PAYLOAD}$" 193 | ), 194 | _parse_mi_result, 195 | ), 196 | # https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Async-Records.html#GDB_002fMI-Async-Records 197 | # Async records are used to notify the gdb/mi client of additional 198 | # changes that have occurred. Those changes can either be a consequence 199 | # of gdb/mi commands (e.g., a breakpoint modified) or a result of target activity 200 | # (e.g., target stopped). 201 | ( 202 | re.compile( 203 | rf"^{_GDB_MI_COMPONENT_TOKEN}[*=](?P\S+?){_GDB_MI_COMPONENT_PAYLOAD}$" 204 | ), 205 | _parse_mi_notify, 206 | ), 207 | # https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Stream-Records.html#GDB_002fMI-Stream-Records 208 | # "~" string-output 209 | # The console output stream contains text that should be displayed 210 | # in the CLI console window. It contains the textual responses to CLI commands. 211 | ( 212 | re.compile(r'~"(?P.*)"', re.DOTALL), 213 | functools.partial(_parse_mi_output, output_type="console"), 214 | ), 215 | # https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Stream-Records.html#GDB_002fMI-Stream-Records 216 | # "&" string-output 217 | # The log stream contains debugging messages being produced by gdb's internals. 218 | ( 219 | re.compile(r'&"(?P.*)"', re.DOTALL), 220 | functools.partial(_parse_mi_output, output_type="log"), 221 | ), 222 | # https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Stream-Records.html#GDB_002fMI-Stream-Records 223 | # "@" string-output 224 | # The target output stream contains any textual output from the 225 | # running target. This is only present when GDB's event loop is truly asynchronous, 226 | # which is currently only the case for remote targets. 227 | ( 228 | re.compile(r'@"(?P.*)"', re.DOTALL), 229 | functools.partial(_parse_mi_output, output_type="target"), 230 | ), 231 | ( 232 | _GDB_MI_RESPONSE_FINISHED_RE, 233 | _parse_mi_finished, 234 | ), 235 | ] 236 | 237 | 238 | _WHITESPACE = [" ", "\t", "\r", "\n"] 239 | 240 | _GDB_MI_CHAR_DICT_START = "{" 241 | _GDB_MI_CHAR_ARRAY_START = "[" 242 | _GDB_MI_CHAR_STRING_START = '"' 243 | _GDB_MI_VALUE_START_CHARS = [ 244 | _GDB_MI_CHAR_DICT_START, 245 | _GDB_MI_CHAR_ARRAY_START, 246 | _GDB_MI_CHAR_STRING_START, 247 | ] 248 | 249 | 250 | def _parse_dict(stream: StringStream) -> Dict: 251 | """Parse dictionary, with optional starting character '{' 252 | return (tuple): 253 | Number of characters parsed from to_parse 254 | Parsed dictionary 255 | """ 256 | obj: Dict[str, Union[str, list, dict]] = {} 257 | 258 | logger.debug("%s", fmt_green("parsing dict")) 259 | 260 | while True: 261 | c = stream.read(1) 262 | if c in _WHITESPACE: 263 | pass 264 | elif c in ["{", ","]: 265 | pass 266 | elif c in ["}", ""]: 267 | # end of object, exit loop 268 | break 269 | 270 | else: 271 | stream.seek(-1) 272 | key, val = _parse_key_val(stream) 273 | if key in obj: 274 | # This is a gdb bug. We should never get repeated keys in a dict! 275 | # See https://sourceware.org/bugzilla/show_bug.cgi?id=22217 276 | # and https://github.com/cs01/pygdbmi/issues/19 277 | # Example: 278 | # thread-ids={thread-id="1",thread-id="2"} 279 | # Results in: 280 | # thread-ids: {{'thread-id': ['1', '2']}} 281 | # Rather than the lossy 282 | # thread-ids: {'thread-id': 2} # '1' got overwritten! 283 | if isinstance(obj[key], list): 284 | obj[key].append(val) # type: ignore 285 | else: 286 | obj[key] = [obj[key], val] 287 | else: 288 | obj[key] = val 289 | 290 | look_ahead_for_garbage = True 291 | c = stream.read(1) 292 | while look_ahead_for_garbage: 293 | if c in ["}", ",", ""]: 294 | look_ahead_for_garbage = False 295 | else: 296 | # got some garbage text, skip it. for example: 297 | # name="gdb"gargage # skip over 'garbage' 298 | # name="gdb"\n # skip over '\n' 299 | logger.debug("skipping unexpected charcter: " + c) 300 | c = stream.read(1) 301 | stream.seek(-1) 302 | 303 | logger.debug("parsed dict") 304 | logger.debug("%s", fmt_green(obj)) 305 | return obj 306 | 307 | 308 | def _parse_key_val(stream: StringStream) -> Tuple[str, Union[str, List, Dict]]: 309 | """Parse key, value combination 310 | return (tuple): 311 | Parsed key (string) 312 | Parsed value (either a string, array, or dict) 313 | """ 314 | 315 | logger.debug("parsing key/val") 316 | key = _parse_key(stream) 317 | val = _parse_val(stream) 318 | 319 | logger.debug("parsed key/val") 320 | logger.debug("%s", fmt_green(key)) 321 | logger.debug("%s", fmt_green(val)) 322 | 323 | return key, val 324 | 325 | 326 | def _parse_key(stream: StringStream) -> str: 327 | """Parse key, value combination 328 | returns : 329 | Parsed key (string) 330 | """ 331 | logger.debug("parsing key") 332 | 333 | key = stream.advance_past_chars(["="]) 334 | 335 | logger.debug("parsed key:") 336 | logger.debug("%s", fmt_green(key)) 337 | return key 338 | 339 | 340 | def _parse_val(stream: StringStream) -> Union[str, List, Dict]: 341 | """Parse value from string 342 | returns: 343 | Parsed value (either a string, array, or dict) 344 | """ 345 | 346 | logger.debug("parsing value") 347 | 348 | val: Any 349 | 350 | while True: 351 | c = stream.read(1) 352 | 353 | if c == "{": 354 | # Start object 355 | val = _parse_dict(stream) 356 | break 357 | 358 | elif c == "[": 359 | # Start of an array 360 | val = _parse_array(stream) 361 | break 362 | 363 | elif c == '"': 364 | # Start of a string 365 | val = stream.advance_past_string_with_gdb_escapes() 366 | break 367 | 368 | elif _DEBUG: 369 | raise ValueError("unexpected character: %s" % c) 370 | 371 | else: 372 | logger.warn(f'unexpected character: "{c}" ({ord(c)}). Continuing.') 373 | val = "" # this will be overwritten if there are more characters to be read 374 | 375 | logger.debug("parsed value:") 376 | logger.debug("%s", fmt_green(val)) 377 | 378 | return val 379 | 380 | 381 | def _parse_array(stream: StringStream) -> list: 382 | """Parse an array, stream should be passed the initial [ 383 | returns: 384 | Parsed array 385 | """ 386 | 387 | logger.debug("parsing array") 388 | arr = [] 389 | while True: 390 | c = stream.read(1) 391 | 392 | if c in _GDB_MI_VALUE_START_CHARS: 393 | stream.seek(-1) 394 | val = _parse_val(stream) 395 | arr.append(val) 396 | elif c in _WHITESPACE: 397 | pass 398 | elif c == ",": 399 | pass 400 | elif c == "]": 401 | # Stop when this array has finished. Note 402 | # that elements of this array can be also be arrays. 403 | break 404 | 405 | logger.debug("parsed array:") 406 | logger.debug("%s", fmt_green(arr)) 407 | return arr 408 | -------------------------------------------------------------------------------- /pygdbmi/IoManager.py: -------------------------------------------------------------------------------- 1 | """This module defines the `IoManager` class 2 | which manages I/O for file objects connected to an existing gdb process 3 | or pty. 4 | """ 5 | import logging 6 | import os 7 | import select 8 | import time 9 | from pprint import pformat 10 | from typing import IO, Any, Dict, List, Optional, Tuple, Union 11 | 12 | from pygdbmi import gdbmiparser 13 | from pygdbmi.constants import ( 14 | DEFAULT_GDB_TIMEOUT_SEC, 15 | DEFAULT_TIME_TO_CHECK_FOR_ADDITIONAL_OUTPUT_SEC, 16 | USING_WINDOWS, 17 | GdbTimeoutError, 18 | ) 19 | 20 | 21 | if USING_WINDOWS: 22 | import msvcrt 23 | from ctypes import POINTER, WinError, byref, windll, wintypes # type: ignore 24 | from ctypes.wintypes import BOOL, DWORD, HANDLE 25 | else: 26 | import fcntl 27 | 28 | 29 | __all__ = ["IoManager"] 30 | 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | 35 | class IoManager: 36 | def __init__( 37 | self, 38 | stdin: IO[bytes], 39 | stdout: IO[bytes], 40 | stderr: Optional[IO[bytes]], 41 | time_to_check_for_additional_output_sec: float = DEFAULT_TIME_TO_CHECK_FOR_ADDITIONAL_OUTPUT_SEC, 42 | ) -> None: 43 | """ 44 | Manage I/O for file objects created before calling this class 45 | This can be useful if the gdb process is managed elsewhere, or if a 46 | pty is used. 47 | """ 48 | 49 | self.stdin = stdin 50 | self.stdout = stdout 51 | self.stderr = stderr 52 | 53 | self.stdin_fileno = self.stdin.fileno() 54 | self.stdout_fileno = self.stdout.fileno() 55 | self.stderr_fileno = self.stderr.fileno() if self.stderr else -1 56 | 57 | self.read_list: List[int] = [] 58 | if self.stdout: 59 | self.read_list.append(self.stdout_fileno) 60 | self.write_list = [self.stdin_fileno] 61 | 62 | self._incomplete_output: Dict[str, Any] = {"stdout": None, "stderr": None} 63 | self.time_to_check_for_additional_output_sec = ( 64 | time_to_check_for_additional_output_sec 65 | ) 66 | self._allow_overwrite_timeout_times = ( 67 | self.time_to_check_for_additional_output_sec > 0 68 | ) 69 | _make_non_blocking(self.stdout) 70 | if self.stderr: 71 | _make_non_blocking(self.stderr) 72 | 73 | def get_gdb_response( 74 | self, 75 | timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, 76 | raise_error_on_timeout: bool = True, 77 | ) -> List[Dict]: 78 | """Get response from GDB, and block while doing so. If GDB does not have any response ready to be read 79 | by timeout_sec, an exception is raised. 80 | 81 | Args: 82 | timeout_sec: Maximum time to wait for reponse. Must be >= 0. Will return after 83 | raise_error_on_timeout: Whether an exception should be raised if no response was found after timeout_sec 84 | 85 | Returns: 86 | List of parsed GDB responses, returned from gdbmiparser.parse_response, with the 87 | additional key 'stream' which is either 'stdout' or 'stderr' 88 | 89 | Raises: 90 | GdbTimeoutError: if response is not received within timeout_sec 91 | ValueError: if select returned unexpected file number 92 | """ 93 | 94 | if timeout_sec < 0: 95 | logger.warning("timeout_sec was negative, replacing with 0") 96 | timeout_sec = 0 97 | 98 | if USING_WINDOWS: 99 | retval = self._get_responses_windows(timeout_sec) 100 | else: 101 | retval = self._get_responses_unix(timeout_sec) 102 | 103 | if not retval and raise_error_on_timeout: 104 | raise GdbTimeoutError( 105 | "Did not get response from gdb after %s seconds" % timeout_sec 106 | ) 107 | 108 | else: 109 | return retval 110 | 111 | def _get_responses_windows(self, timeout_sec: float) -> List[Dict]: 112 | """Get responses on windows. Assume no support for select and use a while loop.""" 113 | timeout_time_sec = time.time() + timeout_sec 114 | responses = [] 115 | while True: 116 | responses_list = [] 117 | try: 118 | self.stdout.flush() 119 | raw_output = self.stdout.readline().replace(b"\r", b"\n") 120 | responses_list = self._get_responses_list(raw_output, "stdout") 121 | except OSError: 122 | pass 123 | 124 | if self.stderr is not None: 125 | try: 126 | self.stderr.flush() 127 | raw_output = self.stderr.readline().replace(b"\r", b"\n") 128 | responses_list += self._get_responses_list(raw_output, "stderr") 129 | except OSError: 130 | pass 131 | 132 | responses += responses_list 133 | if timeout_sec == 0: 134 | break 135 | elif responses_list and self._allow_overwrite_timeout_times: 136 | timeout_time_sec = min( 137 | time.time() + self.time_to_check_for_additional_output_sec, 138 | timeout_time_sec, 139 | ) 140 | elif time.time() > timeout_time_sec: 141 | break 142 | 143 | return responses 144 | 145 | def _get_responses_unix(self, timeout_sec: float) -> List[Dict]: 146 | """Get responses on unix-like system. Use select to wait for output.""" 147 | timeout_time_sec = time.time() + timeout_sec 148 | responses = [] 149 | while True: 150 | select_timeout = timeout_time_sec - time.time() 151 | if select_timeout <= 0: 152 | select_timeout = 0 153 | events, _, _ = select.select(self.read_list, [], [], select_timeout) 154 | responses_list = None # to avoid infinite loop if using Python 2 155 | for fileno in events: 156 | # new data is ready to read 157 | if fileno == self.stdout_fileno: 158 | self.stdout.flush() 159 | raw_output = self.stdout.read() 160 | stream = "stdout" 161 | 162 | elif fileno == self.stderr_fileno: 163 | assert self.stderr is not None 164 | self.stderr.flush() 165 | raw_output = self.stderr.read() 166 | stream = "stderr" 167 | 168 | else: 169 | raise ValueError( 170 | "Developer error. Got unexpected file number %d" % fileno 171 | ) 172 | responses_list = self._get_responses_list(raw_output, stream) 173 | responses += responses_list 174 | 175 | if timeout_sec == 0: # just exit immediately 176 | break 177 | 178 | elif responses_list and self._allow_overwrite_timeout_times: 179 | # update timeout time to potentially be closer to now to avoid lengthy wait times when nothing is being output by gdb 180 | timeout_time_sec = min( 181 | time.time() + self.time_to_check_for_additional_output_sec, 182 | timeout_time_sec, 183 | ) 184 | 185 | elif time.time() > timeout_time_sec: 186 | break 187 | 188 | return responses 189 | 190 | def _get_responses_list( 191 | self, raw_output: bytes, stream: str 192 | ) -> List[Dict[Any, Any]]: 193 | """Get parsed response list from string output 194 | Args: 195 | raw_output (unicode): gdb output to parse 196 | stream (str): either stdout or stderr 197 | """ 198 | responses: List[Dict[Any, Any]] = [] 199 | 200 | (_new_output, self._incomplete_output[stream],) = _buffer_incomplete_responses( 201 | raw_output, self._incomplete_output.get(stream) 202 | ) 203 | 204 | if not _new_output: 205 | return responses 206 | 207 | response_list = list( 208 | filter(lambda x: x, _new_output.decode(errors="replace").split("\n")) 209 | ) # remove blank lines 210 | 211 | # parse each response from gdb into a dict, and store in a list 212 | for response in response_list: 213 | if gdbmiparser.response_is_finished(response): 214 | pass 215 | else: 216 | parsed_response = gdbmiparser.parse_response(response) 217 | parsed_response["stream"] = stream 218 | 219 | logger.debug("%s", pformat(parsed_response)) 220 | 221 | responses.append(parsed_response) 222 | 223 | return responses 224 | 225 | def write( 226 | self, 227 | mi_cmd_to_write: Union[str, List[str]], 228 | timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, 229 | raise_error_on_timeout: bool = True, 230 | read_response: bool = True, 231 | ) -> List[Dict]: 232 | """Write to gdb process. Block while parsing responses from gdb for a maximum of timeout_sec. 233 | 234 | Args: 235 | mi_cmd_to_write: String to write to gdb. If list, it is joined by newlines. 236 | timeout_sec: Maximum number of seconds to wait for response before exiting. Must be >= 0. 237 | raise_error_on_timeout: If read_response is True, raise error if no response is received 238 | read_response: Block and read response. If there is a separate thread running, this can be false, and the reading thread read the output. 239 | Returns: 240 | List of parsed gdb responses if read_response is True, otherwise [] 241 | Raises: 242 | TypeError: if mi_cmd_to_write is not valid 243 | """ 244 | # self.verify_valid_gdb_subprocess() 245 | if timeout_sec < 0: 246 | logger.warning("timeout_sec was negative, replacing with 0") 247 | timeout_sec = 0 248 | 249 | # Ensure proper type of the mi command 250 | if isinstance(mi_cmd_to_write, str): 251 | mi_cmd_to_write_str = mi_cmd_to_write 252 | elif isinstance(mi_cmd_to_write, list): 253 | mi_cmd_to_write_str = "\n".join(mi_cmd_to_write) 254 | else: 255 | raise TypeError( 256 | "The gdb mi command must a be str or list. Got " 257 | + str(type(mi_cmd_to_write)) 258 | ) 259 | 260 | logger.debug("writing: %s", mi_cmd_to_write) 261 | 262 | if not mi_cmd_to_write_str.endswith("\n"): 263 | mi_cmd_to_write_nl = mi_cmd_to_write_str + "\n" 264 | else: 265 | mi_cmd_to_write_nl = mi_cmd_to_write_str 266 | 267 | if USING_WINDOWS: 268 | # select not implemented in windows for pipes 269 | # assume it's always ready 270 | outputready = [self.stdin_fileno] 271 | else: 272 | _, outputready, _ = select.select([], self.write_list, [], timeout_sec) 273 | for fileno in outputready: 274 | if fileno == self.stdin_fileno: 275 | # ready to write 276 | self.stdin.write(mi_cmd_to_write_nl.encode()) # type: ignore 277 | # must flush, otherwise gdb won't realize there is data 278 | # to evaluate, and we won't get a response 279 | self.stdin.flush() # type: ignore 280 | else: 281 | logger.error("got unexpected fileno %d" % fileno) 282 | 283 | if read_response is True: 284 | return self.get_gdb_response( 285 | timeout_sec=timeout_sec, raise_error_on_timeout=raise_error_on_timeout 286 | ) 287 | 288 | else: 289 | return [] 290 | 291 | 292 | def _buffer_incomplete_responses( 293 | raw_output: Optional[bytes], buf: Optional[bytes] 294 | ) -> Tuple[Optional[bytes], Optional[bytes]]: 295 | """It is possible for some of gdb's output to be read before it completely finished its response. 296 | In that case, a partial mi response was read, which cannot be parsed into structured data. 297 | We want to ALWAYS parse complete mi records. To do this, we store a buffer of gdb's 298 | output if the output did not end in a newline. 299 | 300 | Args: 301 | raw_output: Contents of the gdb mi output 302 | buf (str): Buffered gdb response from the past. This is incomplete and needs to be prepended to 303 | gdb's next output. 304 | 305 | Returns: 306 | (raw_output, buf) 307 | """ 308 | 309 | if raw_output: 310 | if buf: 311 | # concatenate buffer and new output 312 | raw_output = b"".join([buf, raw_output]) 313 | buf = None 314 | 315 | if b"\n" not in raw_output: 316 | # newline was not found, so assume output is incomplete and store in buffer 317 | buf = raw_output 318 | raw_output = None 319 | 320 | elif not raw_output.endswith(b"\n"): 321 | # raw output doesn't end in a newline, so store everything after the last newline (if anything) 322 | # in the buffer, and parse everything before it 323 | remainder_offset = raw_output.rindex(b"\n") + 1 324 | buf = raw_output[remainder_offset:] 325 | raw_output = raw_output[:remainder_offset] 326 | 327 | return (raw_output, buf) 328 | 329 | 330 | def _make_non_blocking(file_obj: IO) -> None: 331 | """make file object non-blocking 332 | Windows doesn't have the fcntl module, but someone on 333 | stack overflow supplied this code as an answer, and it works 334 | http://stackoverflow.com/a/34504971/2893090""" 335 | 336 | if USING_WINDOWS: 337 | LPDWORD = POINTER(DWORD) 338 | PIPE_NOWAIT = wintypes.DWORD(0x00000001) 339 | 340 | SetNamedPipeHandleState = windll.kernel32.SetNamedPipeHandleState 341 | SetNamedPipeHandleState.argtypes = [HANDLE, LPDWORD, LPDWORD, LPDWORD] 342 | SetNamedPipeHandleState.restype = BOOL 343 | 344 | h = msvcrt.get_osfhandle(file_obj.fileno()) # type: ignore 345 | 346 | res = windll.kernel32.SetNamedPipeHandleState(h, byref(PIPE_NOWAIT), None, None) 347 | if res == 0: 348 | raise ValueError(WinError()) 349 | 350 | else: 351 | # Set the file status flag (F_SETFL) on the pipes to be non-blocking 352 | # so we can attempt to read from a pipe with no new data without locking 353 | # the program up 354 | fcntl.fcntl(file_obj, fcntl.F_SETFL, os.O_NONBLOCK) 355 | -------------------------------------------------------------------------------- /tests/response_samples.txt: -------------------------------------------------------------------------------- 1 | ~"0x00007fe2c5c58920 in __nanosleep_nocancel () at ../sysdeps/unix/syscall-template.S:81\n" 2 | &"81\t../sysdeps/unix/syscall-template.S: No such file or directory.\n" 3 | &"attach 48337\n" 4 | ~"Attaching to process 48337\n" 5 | =breakpoint-modified,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c54940",func="__opendir",file="../sysdeps/posix/opendir.c",fullname="/build/eglibc-MjiXCM/eglibc-2.19/dirent/../sysdeps/posix/opendir.c",line="159",thread-groups=["i1"],times="1",original-location="opendir"} 6 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="10",original-location="write"} 7 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="11",original-location="write"} 8 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="12",original-location="write"} 9 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="13",original-location="write"} 10 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="14",original-location="write"} 11 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="15",original-location="write"} 12 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="16",original-location="write"} 13 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="17",original-location="write"} 14 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="18",original-location="write"} 15 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="19",original-location="write"} 16 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="1",original-location="write"} 17 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="20",original-location="write"} 18 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="21",original-location="write"} 19 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="22",original-location="write"} 20 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="23",original-location="write"} 21 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="24",original-location="write"} 22 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="25",original-location="write"} 23 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="26",original-location="write"} 24 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="27",original-location="write"} 25 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="28",original-location="write"} 26 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="29",original-location="write"} 27 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="2",original-location="write"} 28 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="30",original-location="write"} 29 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="31",original-location="write"} 30 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="32",original-location="write"} 31 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="33",original-location="write"} 32 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="34",original-location="write"} 33 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="35",original-location="write"} 34 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="36",original-location="write"} 35 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="37",original-location="write"} 36 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="38",original-location="write"} 37 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="39",original-location="write"} 38 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="3",original-location="write"} 39 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="40",original-location="write"} 40 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="41",original-location="write"} 41 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="42",original-location="write"} 42 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="43",original-location="write"} 43 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="44",original-location="write"} 44 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="45",original-location="write"} 45 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="46",original-location="write"} 46 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="47",original-location="write"} 47 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="48",original-location="write"} 48 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="49",original-location="write"} 49 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="4",original-location="write"} 50 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="50",original-location="write"} 51 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="51",original-location="write"} 52 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="52",original-location="write"} 53 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="53",original-location="write"} 54 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="54",original-location="write"} 55 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="55",original-location="write"} 56 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="56",original-location="write"} 57 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="57",original-location="write"} 58 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="58",original-location="write"} 59 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="59",original-location="write"} 60 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="5",original-location="write"} 61 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="60",original-location="write"} 62 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="61",original-location="write"} 63 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="62",original-location="write"} 64 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="63",original-location="write"} 65 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="64",original-location="write"} 66 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="65",original-location="write"} 67 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="6",original-location="write"} 68 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="7",original-location="write"} 69 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="8",original-location="write"} 70 | =breakpoint-modified,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="9",original-location="write"} 71 | &"detach\n" 72 | ^done 73 | ^done,bkpt={number="1",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c54940",func="__opendir",file="../sysdeps/posix/opendir.c",fullname="/build/eglibc-MjiXCM/eglibc-2.19/dirent/../sysdeps/posix/opendir.c",line="159",thread-groups=["i1"],times="0",original-location="opendir"} 74 | ^done,bkpt={number="2",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82f70",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="0",original-location="write"} 75 | ^done,bkpt={number="3",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c82d20",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="0",original-location="open"} 76 | ^done,bkpt={number="4",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c92d60",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/socket/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="0",original-location="sendto"} 77 | ^done,bkpt={number="5",type="breakpoint",disp="keep",enabled="y",addr="0x00007fe2c5c92b90",file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/socket/../sysdeps/unix/syscall-template.S",line="81",thread-groups=["i1"],times="0",original-location="recvfrom"} 78 | ~"done.\n" 79 | ^done,wpt={number="6",exp="*0x40000001c538"} 80 | ^done,wpt={number="7",exp="*0x40000001f000"} 81 | ^done,wpt={number="8",exp="*0x400000026020"} 82 | ^done,wpt={number="9",exp="*0x40000002c538"} 83 | The inferior program printed this! Can you still parse it? 84 | ^error,msg="Function \"iemMemStoreDataU128AlignedSseJmp\" not defined." 85 | ^error,msg="Function \"iemMemStoreDataU16Jmp\" not defined." 86 | ^error,msg="Function \"iemMemStoreDataU16\" not defined." 87 | ^error,msg="Function \"iemMemStoreDataU32Jmp\" not defined." 88 | ^error,msg="Function \"iemMemStoreDataU32\" not defined." 89 | ^error,msg="Function \"iemMemStoreDataU64Jmp\" not defined." 90 | ^error,msg="Function \"iemMemStoreDataU64\" not defined." 91 | ^error,msg="Function \"iemMemStoreDataU8Jmp\" not defined." 92 | ^error,msg="Function \"iemMemStoreDataU8\" not defined." 93 | ^error,msg="Function \"iemMemStoreDataXdtr\" not defined." 94 | ^error,msg="Function \"__pthread_create_2_1\" not defined." 95 | ^error,msg="The program is not being run." 96 | =library-loaded,id="/lib64/ld-linux-x86-64.so.2",target-name="/lib64/ld-linux-x86-64.so.2",host-name="/lib64/ld-linux-x86-64.so.2",symbols-loaded="0",thread-group="i1" 97 | =library-loaded,id="/lib/x86_64-linux-gnu/libacl.so.1",target-name="/lib/x86_64-linux-gnu/libacl.so.1",host-name="/lib/x86_64-linux-gnu/libacl.so.1",symbols-loaded="0",thread-group="i1" 98 | =library-loaded,id="/lib/x86_64-linux-gnu/libattr.so.1",target-name="/lib/x86_64-linux-gnu/libattr.so.1",host-name="/lib/x86_64-linux-gnu/libattr.so.1",symbols-loaded="0",thread-group="i1" 99 | =library-loaded,id="/lib/x86_64-linux-gnu/libc.so.6",target-name="/lib/x86_64-linux-gnu/libc.so.6",host-name="/lib/x86_64-linux-gnu/libc.so.6",symbols-loaded="0",thread-group="i1" 100 | =library-loaded,id="/lib/x86_64-linux-gnu/libdl.so.2",target-name="/lib/x86_64-linux-gnu/libdl.so.2",host-name="/lib/x86_64-linux-gnu/libdl.so.2",symbols-loaded="0",thread-group="i1" 101 | =library-loaded,id="/lib/x86_64-linux-gnu/libpcre.so.3",target-name="/lib/x86_64-linux-gnu/libpcre.so.3",host-name="/lib/x86_64-linux-gnu/libpcre.so.3",symbols-loaded="0",thread-group="i1" 102 | =library-loaded,id="/lib/x86_64-linux-gnu/libselinux.so.1",target-name="/lib/x86_64-linux-gnu/libselinux.so.1",host-name="/lib/x86_64-linux-gnu/libselinux.so.1",symbols-loaded="0",thread-group="i1" 103 | =library-loaded,id="/opt/vmfuzz-gdb-injector/gdb_injector.so",target-name="/opt/vmfuzz-gdb-injector/gdb_injector.so",host-name="/opt/vmfuzz-gdb-injector/gdb_injector.so",symbols-loaded="0",thread-group="i1" 104 | ~"Loaded symbols for /lib64/ld-linux-x86-64.so.2\n" 105 | ~"Loaded symbols for /lib/x86_64-linux-gnu/libacl.so.1\n" 106 | ~"Loaded symbols for /lib/x86_64-linux-gnu/libattr.so.1\n" 107 | ~"Loaded symbols for /lib/x86_64-linux-gnu/libc.so.6\n" 108 | ~"Loaded symbols for /lib/x86_64-linux-gnu/libdl.so.2\n" 109 | ~"Loaded symbols for /lib/x86_64-linux-gnu/libpcre.so.3\n" 110 | ~"Loaded symbols for /lib/x86_64-linux-gnu/libselinux.so.1\n" 111 | ~"Loaded symbols for /opt/vmfuzz-gdb-injector/gdb_injector.so\n" 112 | ~"(no debugging symbols found)...done.\n" 113 | ~"Reading symbols from /bin/ls..." 114 | ~"Reading symbols from /lib64/ld-linux-x86-64.so.2..." 115 | ~"Reading symbols from /lib/x86_64-linux-gnu/libacl.so.1..." 116 | ~"Reading symbols from /lib/x86_64-linux-gnu/libattr.so.1..." 117 | ~"Reading symbols from /lib/x86_64-linux-gnu/libc.so.6..." 118 | ~"Reading symbols from /lib/x86_64-linux-gnu/libdl.so.2..." 119 | ~"Reading symbols from /lib/x86_64-linux-gnu/libpcre.so.3..." 120 | ~"Reading symbols from /lib/x86_64-linux-gnu/libselinux.so.1..." 121 | ~"Reading symbols from /opt/vmfuzz-gdb-injector/gdb_injector.so..." 122 | ~"Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/ld-2.19.so..." 123 | ~"Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libc-2.19.so..." 124 | ~"Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libdl-2.19.so..." 125 | ^running 126 | *running,thread-id="1" 127 | *running,thread-id="all" 128 | *stopped,frame={addr="0x00007fe2c5c58920",func="__nanosleep_nocancel",args=[],file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/posix/../sysdeps/unix/syscall-template.S",line="81"},thread-id="1",stopped-threads="all",core="1" 129 | *stopped,reason="breakpoint-hit",disp="keep",bkptno="1",frame={addr="0x00007fe2c5c54940",func="__opendir",args=[{name="name",value="0x236c320 \".\""}],file="../sysdeps/posix/opendir.c",fullname="/build/eglibc-MjiXCM/eglibc-2.19/dirent/../sysdeps/posix/opendir.c",line="159"},thread-id="1",stopped-threads="all",core="1" 130 | *stopped,reason="breakpoint-hit",disp="keep",bkptno="2",frame={addr="0x00007fe2c5c82f70",func="write",args=[],file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81"},thread-id="1",stopped-threads="all",core="0" 131 | *stopped,reason="breakpoint-hit",disp="keep",bkptno="2",frame={addr="0x00007fe2c5c82f70",func="write",args=[],file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/io/../sysdeps/unix/syscall-template.S",line="81"},thread-id="1",stopped-threads="all",core="1" 132 | *stopped,reason="exited-normally" 133 | *stopped,reason="signal-received",signal-name="SIGUSR1",signal-meaning="User defined signal 1",frame={addr="0x00007fe2c5c58920",func="__nanosleep_nocancel",args=[],file="../sysdeps/unix/syscall-template.S",fullname="/build/eglibc-MjiXCM/eglibc-2.19/posix/../sysdeps/unix/syscall-template.S",line="81"},thread-id="1",stopped-threads="all",core="1" 134 | &"The program is not being run.\n" 135 | =thread-created,id="1",group-id="i1" 136 | =thread-exited,id="1",group-id="i1" 137 | =thread-group-added,id="i1" 138 | =thread-group-exited,id="i1",exit-code="0" 139 | =thread-group-started,id="i1",pid="48337" 140 | =tsv-created,name="trace_timestamp",initial="0"\n 141 | =tsv-created,name="trace_timestamp",initial="0"\\n 142 | --------------------------------------------------------------------------------