├── .gitignore ├── LICENSE ├── PyExt.sln ├── README.rst ├── appveyor.yml ├── include ├── PyBoolObject.h ├── PyByteArrayObject.h ├── PyCellObject.h ├── PyCodeObject.h ├── PyComplexObject.h ├── PyDictKeysObject.h ├── PyDictObject.h ├── PyFloatObject.h ├── PyFrame.h ├── PyFrameObject.h ├── PyFunctionObject.h ├── PyIntObject.h ├── PyInterpreterFrame.h ├── PyInterpreterState.h ├── PyListObject.h ├── PyLongObject.h ├── PyMemberDef.h ├── PyNoneObject.h ├── PyNotImplementedObject.h ├── PyObject.h ├── PySetObject.h ├── PyStringObject.h ├── PyStringValue.h ├── PyThreadState.h ├── PyTupleObject.h ├── PyTypeObject.h ├── PyUnicodeObject.h ├── PyVarObject.h ├── RemoteType.h ├── globals.h ├── pyextpublic.h └── utils │ ├── ScopeExit.h │ └── lossless_cast.h ├── src ├── ExtHelpers.cpp ├── ExtHelpers.h ├── PyExt.def ├── PyExt.rc ├── PyExt.vcxproj ├── PyExt.vcxproj.filters ├── PyFrame.cpp ├── PyInterpreterFrame.cpp ├── PyInterpreterState.cpp ├── PyMemberDef.cpp ├── PyThreadState.cpp ├── RemoteType.cpp ├── extension.cpp ├── extension.h ├── fieldAsPyObject.h ├── globals.cpp ├── init.wdb ├── objects │ ├── PyBoolObject.cpp │ ├── PyByteArrayObject.cpp │ ├── PyCellObject.cpp │ ├── PyCodeObject.cpp │ ├── PyComplexObject.cpp │ ├── PyDictKeysObject.cpp │ ├── PyDictObject.cpp │ ├── PyFloatObject.cpp │ ├── PyFrameObject.cpp │ ├── PyFunctionObject.cpp │ ├── PyIntObject.cpp │ ├── PyListObject.cpp │ ├── PyLongObject.cpp │ ├── PyNoneObject.cpp │ ├── PyNotImplementedObject.cpp │ ├── PyObject.cpp │ ├── PyObjectMake.cpp │ ├── PySetObject.cpp │ ├── PyStringObject.cpp │ ├── PyStringValue.cpp │ ├── PyTupleObject.cpp │ ├── PyTypeObject.cpp │ ├── PyUnicodeObject.cpp │ └── PyVarObject.cpp ├── pch.cpp ├── pch.h ├── pysetautointerpreterstate.cpp ├── pystack.cpp └── pysymfix.cpp └── test ├── PyExtTest ├── FibonacciTest.cpp ├── LocalsplusTest.cpp ├── ObjectDetailsTest.cpp ├── ObjectTypesTest.cpp ├── PyExtTest.vcxproj ├── PyExtTest.vcxproj.filters ├── PythonDumpFile.cpp ├── PythonDumpFile.h ├── ScopeExitTest.cpp ├── TestConfigData.cpp ├── TestConfigData.h ├── catch.hpp └── main.cpp └── scripts ├── break.py ├── fibonacci_test.py ├── localsplus_test.py ├── object_details.py ├── object_types.py ├── python_installations.py ├── run_all_tests.py └── win32debug.py /.gitignore: -------------------------------------------------------------------------------- 1 | # User files. 2 | *.*proj.user 3 | .vs/ 4 | 5 | # Build artifacts. 6 | [Dd]ebug/ 7 | [Rr]elease/ 8 | x64/ 9 | x86/ 10 | *.aps 11 | 12 | # Debug artifacts. 13 | /*.pdb/ 14 | /pingme.txt 15 | 16 | # Python artifacts. 17 | *.pyc 18 | 19 | # Unit test artifacts. 20 | /test/scripts/*.dmp 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sean D. Cline 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PyExt.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.6 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PyExt", "src\PyExt.vcxproj", "{EEBC73BA-27F2-4483-8639-54A108B77594}" 7 | EndProject 8 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PyExtTest", "test\PyExtTest\PyExtTest.vcxproj", "{AE750E22-5A8E-401E-A843-B9D56B09A015}" 9 | ProjectSection(ProjectDependencies) = postProject 10 | {EEBC73BA-27F2-4483-8639-54A108B77594} = {EEBC73BA-27F2-4483-8639-54A108B77594} 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Win32 = Debug|Win32 16 | Debug|x64 = Debug|x64 17 | Release|Win32 = Release|Win32 18 | Release|x64 = Release|x64 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {EEBC73BA-27F2-4483-8639-54A108B77594}.Debug|Win32.ActiveCfg = Debug|Win32 22 | {EEBC73BA-27F2-4483-8639-54A108B77594}.Debug|Win32.Build.0 = Debug|Win32 23 | {EEBC73BA-27F2-4483-8639-54A108B77594}.Debug|x64.ActiveCfg = Debug|x64 24 | {EEBC73BA-27F2-4483-8639-54A108B77594}.Debug|x64.Build.0 = Debug|x64 25 | {EEBC73BA-27F2-4483-8639-54A108B77594}.Release|Win32.ActiveCfg = Release|Win32 26 | {EEBC73BA-27F2-4483-8639-54A108B77594}.Release|Win32.Build.0 = Release|Win32 27 | {EEBC73BA-27F2-4483-8639-54A108B77594}.Release|x64.ActiveCfg = Release|x64 28 | {EEBC73BA-27F2-4483-8639-54A108B77594}.Release|x64.Build.0 = Release|x64 29 | {AE750E22-5A8E-401E-A843-B9D56B09A015}.Debug|Win32.ActiveCfg = Debug|Win32 30 | {AE750E22-5A8E-401E-A843-B9D56B09A015}.Debug|Win32.Build.0 = Debug|Win32 31 | {AE750E22-5A8E-401E-A843-B9D56B09A015}.Debug|x64.ActiveCfg = Debug|x64 32 | {AE750E22-5A8E-401E-A843-B9D56B09A015}.Debug|x64.Build.0 = Debug|x64 33 | {AE750E22-5A8E-401E-A843-B9D56B09A015}.Release|Win32.ActiveCfg = Release|Win32 34 | {AE750E22-5A8E-401E-A843-B9D56B09A015}.Release|Win32.Build.0 = Release|Win32 35 | {AE750E22-5A8E-401E-A843-B9D56B09A015}.Release|x64.ActiveCfg = Release|x64 36 | {AE750E22-5A8E-401E-A843-B9D56B09A015}.Release|x64.Build.0 = Release|x64 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | WinDbg Extensions for Python 3 | ============================ 4 | .. image:: https://ci.appveyor.com/api/projects/status/f4osp2swvm1l25ct/branch/master?svg=true 5 | :alt: Build Status 6 | :target: https://ci.appveyor.com/project/SeanCline/pyext/branch/master 7 | 8 | This debugger extension provides visualizations for Python objects and stacktraces when debugging the CPython interpreter. It helps with live debugging and post-mortem analysis of dump files. 9 | 10 | The goal of this project is to provide a similar debugging experience in WinDbg/CDB/NTSD as `already exists in GDB `_. 11 | 12 | Currently, the extension is tested against 32bit and 64bit builds of Python versions 2.7, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13. 13 | 14 | Installation 15 | ============ 16 | - Build from source or download binaries from the Releases page 17 | - Copy `pyext.dll` of the appropriate bitness into `\\winext` 18 | - Ensure you have Microsoft Visual C++ 2017 Redistributable installed 19 | 20 | Extension Commands 21 | ================== 22 | 23 | !pystack 24 | -------- 25 | Displays the Python callstack for the current thread. 26 | 27 | Example usage: 28 | ^^^^^^^^^^^^^^ 29 | .. code-block:: 30 | 31 | 0:000> !pystack 32 | Thread 0: 33 | File "C:\Python36\lib\threading.py", line 1072, in _wait_for_tstate_lock 34 | File "C:\Python36\lib\threading.py", line 1056, in join 35 | File "scripts\win32debug.py", line 148, in _launch_and_wait 36 | File "scripts\win32debug.py", line 175, in dump_process 37 | File ".\fibonacci_test.py", line 18, in recursive_fib 38 | File ".\fibonacci_test.py", line 18, in recursive_fib 39 | File ".\fibonacci_test.py", line 18, in recursive_fib 40 | File ".\fibonacci_test.py", line 28, in 41 | 42 | Use `~*e!pystack` to display the Python stack for all threads. 43 | 44 | !pyobj 45 | ------ 46 | Displays the reference count, type, and value of a Python object, using similar formatting to Python's builtin `repr()` function. 47 | 48 | Example usage: 49 | ^^^^^^^^^^^^^^ 50 | .. code-block:: 51 | 52 | 0:000> !pyobj autoInterpreterState->tstate_head->frame->f_code 53 | PyCodeObject at address: 000001fc`b6a87f60 54 | RefCount: 2 55 | Type: code 56 | Repr: 57 | 58 | .. code-block:: 59 | 60 | 0:000> !pyobj autoInterpreterState->tstate_head->frame->f_globals 61 | PyDictObject at address: 000001fc`b6ba6bd0 62 | RefCount: 15 63 | Type: dict 64 | Repr: { 65 | '__name__': 'win32debug', 66 | '__doc__': 'Wrappers around various Win32 APIs debugging.', 67 | # ... 68 | } 69 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.1.{build} 2 | 3 | image: Visual Studio 2017 4 | 5 | shallow_clone: true 6 | 7 | install: 8 | - choco install -y windows-sdk-10-version-1903-windbg 9 | - choco install -y --no-progress --params "Include_symbols=1" python310 10 | - choco install -y --no-progress --params "Include_symbols=1" python311 11 | - choco install -y --no-progress --params "Include_symbols=1" python312 12 | - choco install -y --no-progress --params "Include_symbols=1" python313 13 | 14 | platform: 15 | - Win32 16 | - x64 17 | 18 | configuration: 19 | - Debug 20 | - Release 21 | 22 | build: 23 | project: PyExt.sln 24 | 25 | test_script: 26 | - cd test\scripts 27 | - py -3 run_all_tests.py %APPVEYOR_BUILD_FOLDER%/%PLATFORM%/%CONFIGURATION%/PyExtTest.exe 28 | 29 | # Package up build artifacts. 30 | after_build: 31 | - 7z a PyExt-%PLATFORM%-%CONFIGURATION%.zip .\*\*\pyext.dll .\*\*\pyext.pdb 32 | 33 | artifacts: 34 | - path: PyExt-$(Platform)-$(Configuration).zip 35 | name: PyExt-$(Platform)-$(Configuration) 36 | 37 | # Deploy build artifacts to GitHub Releases 38 | deploy: 39 | - provider: GitHub 40 | release: PyExt-v$(appveyor_build_version) 41 | description: 'AppVeyor deploy of PyExt v$(appveyor_build_version)' 42 | auth_token: 43 | secure: FLeKshd/j9QXbwrM1rCA+tNGkFz8DnN/2Xr/m66oHzv2Qzm9GMb4Bj5UT4GNC5IQ 44 | artifact: PyExt-$(Platform)-$(Configuration).zip 45 | draft: true 46 | prerelease: true 47 | on: 48 | branch: master # release from master branch only 49 | appveyor_repo_tag: true # deploy on tag push only -------------------------------------------------------------------------------- /include/PyBoolObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include 5 | #include 6 | 7 | namespace PyExt::Remote { 8 | 9 | /// Represents a PyBoolObject in the debuggee's address space. 10 | class PYEXT_PUBLIC PyBoolObject : public PyObject 11 | { 12 | 13 | public: // Construction/Destruction. 14 | explicit PyBoolObject(Offset objectAddress); 15 | 16 | public: // Members. 17 | auto boolValue() const -> bool; 18 | auto repr(bool pretty = true) const -> std::string override; 19 | 20 | }; 21 | 22 | } -------------------------------------------------------------------------------- /include/PyByteArrayObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyVarObject.h" 4 | #include "PyStringValue.h" 5 | #include 6 | #include 7 | 8 | namespace PyExt::Remote { 9 | 10 | /// Represents a PyByteArrayObject in the debuggee's address space. 11 | class PYEXT_PUBLIC PyByteArrayObject : public PyVarObject, public PyStringValue 12 | { 13 | 14 | public: // Construction/Destruction. 15 | explicit PyByteArrayObject(Offset objectAddress); 16 | 17 | public: // Members. 18 | auto arrayValue() const -> std::vector; 19 | auto stringLength() const -> SSize; 20 | auto stringValue() const -> std::string override; 21 | auto repr(bool pretty = true) const -> std::string override; 22 | 23 | }; 24 | 25 | } -------------------------------------------------------------------------------- /include/PyCellObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include 5 | 6 | namespace PyExt::Remote { 7 | 8 | /// Represents a PyBoolObject in the debuggee's address space. 9 | class PYEXT_PUBLIC PyCellObject : public PyObject 10 | { 11 | 12 | public: // Construction/Destruction. 13 | explicit PyCellObject(Offset objectAddress); 14 | 15 | public: // Members. 16 | auto objectReference() const -> std::unique_ptr; 17 | auto repr(bool pretty = true) const -> std::string override; 18 | 19 | }; 20 | 21 | } -------------------------------------------------------------------------------- /include/PyCodeObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include 5 | #include 6 | #include 7 | 8 | namespace PyExt::Remote { 9 | 10 | /// Represents a PyCodeObject in the debuggee's address space. 11 | class PYEXT_PUBLIC PyCodeObject : public PyObject 12 | { 13 | 14 | public: // Construction/Destruction. 15 | explicit PyCodeObject(Offset objectAddress); 16 | 17 | public: 18 | // Members. 19 | auto numberOfLocals() const -> int; 20 | auto firstLineNumber() const -> int; 21 | auto lineNumberFromInstructionOffset(int instruction) const -> int; 22 | auto lineNumberFromPrevInstruction(int instruction) const -> int; 23 | auto varNames() const -> std::vector; 24 | auto freeVars() const -> std::vector; 25 | auto cellVars() const -> std::vector; 26 | auto localsplusNames() const -> std::vector; 27 | auto filename() const -> std::string; 28 | auto name() const -> std::string; 29 | auto lineNumberTableOld() const -> std::vector; 30 | auto lineNumberTableNew() const -> std::vector; 31 | auto repr(bool pretty = true) const -> std::string override; 32 | 33 | protected: 34 | // Helpers. 35 | auto lineNumberFromInstructionOffsetOld(int instruction, const std::vector &lnotab) const -> int; 36 | auto lineNumberFromInstructionOffsetNew(int instruction, const std::vector &linetable) const -> int; 37 | auto readStringTuple(std::string name) const -> std::vector; 38 | auto readVarint(std::vector::const_iterator &index) const -> unsigned int; 39 | auto readSvarint(std::vector::const_iterator &index) const -> int; 40 | }; 41 | 42 | } -------------------------------------------------------------------------------- /include/PyComplexObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include 5 | #include 6 | #include 7 | 8 | namespace PyExt::Remote { 9 | 10 | /// Represents a PyComplexObject in the debuggee's address space. 11 | class PYEXT_PUBLIC PyComplexObject : public PyObject 12 | { 13 | 14 | public: // Construction/Destruction. 15 | explicit PyComplexObject(Offset objectAddress); 16 | 17 | public: // Members. 18 | auto complexValue() const -> std::complex; 19 | auto repr(bool pretty = true) const -> std::string override; 20 | 21 | }; 22 | 23 | } -------------------------------------------------------------------------------- /include/PyDictKeysObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RemoteType.h" 4 | 5 | namespace PyExt::Remote { 6 | 7 | /// Represents a PyDictKeysObject in the debuggee's address space. 8 | class PYEXT_PUBLIC PyDictKeysObject : private RemoteType 9 | { 10 | 11 | public: // Construction/Destruction. 12 | explicit PyDictKeysObject(Offset objectAddress); 13 | 14 | public: // Members. 15 | auto getEntriesTable() -> ExtRemoteTyped; 16 | auto getEntriesTableSize() -> RemoteType::SSize; 17 | using RemoteType::remoteType; 18 | 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /include/PyDictObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace PyExt::Remote { 11 | 12 | class PyDictKeysObject; 13 | 14 | /// Common interface for PyDictObject and PyManagedDict 15 | class PYEXT_PUBLIC PyDict 16 | { 17 | 18 | public: // Construction/Destruction. 19 | virtual ~PyDict(); 20 | 21 | public: // Members. 22 | virtual auto pairValues() const->std::vector, std::unique_ptr>> = 0; 23 | virtual auto repr(bool pretty = true) const->std::string; 24 | 25 | }; 26 | 27 | 28 | class PYEXT_PUBLIC PyManagedDict : public PyDict 29 | { 30 | 31 | public: // Construction/Destruction. 32 | explicit PyManagedDict(RemoteType::Offset keysPtr, RemoteType::Offset valuesPtr); 33 | 34 | public: // Members. 35 | auto pairValues() const->std::vector, std::unique_ptr>> override; 36 | 37 | private: 38 | RemoteType::Offset keysPtr; 39 | RemoteType::Offset valuesPtr; 40 | 41 | }; 42 | 43 | 44 | /// Represents a PyDictObject in the debuggee's address space. 45 | class PYEXT_PUBLIC PyDictObject : public PyObject, public PyDict 46 | { 47 | 48 | public: // Construction/Destruction. 49 | explicit PyDictObject(Offset objectAddress); 50 | 51 | public: // Members. 52 | auto pairValues() const -> std::vector, std::unique_ptr>> override; 53 | auto repr(bool pretty = true) const -> std::string override; 54 | 55 | }; 56 | 57 | } -------------------------------------------------------------------------------- /include/PyFloatObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include 5 | #include 6 | 7 | namespace PyExt::Remote { 8 | 9 | /// Represents a PyFloatObject in the debuggee's address space. 10 | class PYEXT_PUBLIC PyFloatObject : public PyObject 11 | { 12 | 13 | public: // Construction/Destruction. 14 | explicit PyFloatObject(Offset objectAddress); 15 | 16 | public: // Members. 17 | auto floatValue() const -> double; 18 | auto repr(bool pretty = true) const -> std::string override; 19 | 20 | }; 21 | 22 | } -------------------------------------------------------------------------------- /include/PyFrame.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pyextpublic.h" 4 | 5 | namespace PyExt::Remote { 6 | 7 | class Offset; 8 | class PyDictObject; 9 | class PyCodeObject; 10 | class PyFunctionObject; 11 | class PyObject; 12 | 13 | /// Common interface for PyFrameObject and PyInterpreterFrame 14 | class PYEXT_PUBLIC PyFrame 15 | { 16 | public: 17 | virtual ~PyFrame(); 18 | 19 | public: // Members of the remote type. 20 | virtual auto locals() const -> std::unique_ptr = 0; 21 | virtual auto localsplus() const -> std::vector>> = 0; 22 | virtual auto globals() const -> std::unique_ptr = 0; 23 | virtual auto code() const -> std::unique_ptr = 0; 24 | virtual auto previous() const -> std::unique_ptr = 0; 25 | virtual auto currentLineNumber() const -> int = 0; 26 | virtual auto details() const -> std::string; 27 | }; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /include/PyFrameObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyFrame.h" 4 | #include "PyVarObject.h" 5 | #include 6 | #include 7 | 8 | namespace PyExt::Remote { 9 | 10 | // Forward declarations. 11 | class PyDictObject; 12 | class PyCodeObject; 13 | class PyFunctionObject; 14 | 15 | /// Represents a PyFrameObject in the debuggee's address space. 16 | class PYEXT_PUBLIC PyFrameObject : public PyVarObject, public PyFrame 17 | { 18 | 19 | public: // Construction/Destruction. 20 | explicit PyFrameObject(Offset objectAddress); 21 | 22 | public: // Members. 23 | auto locals() const -> std::unique_ptr override; 24 | auto localsplus() const -> std::vector>> override; 25 | auto globals() const -> std::unique_ptr override; 26 | auto code() const -> std::unique_ptr override; 27 | auto previous() const->std::unique_ptr override; 28 | auto back() const -> std::unique_ptr; 29 | auto trace() const -> std::unique_ptr; 30 | auto lastInstruction() const -> int; 31 | auto currentLineNumber() const -> int override; 32 | auto repr(bool pretty = true) const -> std::string override; 33 | auto details() const -> std::string override; 34 | 35 | }; 36 | 37 | } -------------------------------------------------------------------------------- /include/PyFunctionObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include 5 | #include 6 | 7 | namespace PyExt::Remote { 8 | 9 | // Forward declarations. 10 | class PyCodeObject; 11 | class PyDictObject; 12 | class PyTupleObject; 13 | class PyDictObject; 14 | class PyListObject; 15 | class PyStringValue; 16 | 17 | /// Represents a PyFunctionObject in the debuggee's address space. 18 | class PYEXT_PUBLIC PyFunctionObject : public PyObject 19 | { 20 | 21 | public: // Construction/Destruction. 22 | explicit PyFunctionObject(Offset objectAddress); 23 | 24 | public: // Members. 25 | auto code() const -> std::unique_ptr; 26 | auto globals() const -> std::unique_ptr; 27 | auto defaults() const -> std::unique_ptr; 28 | auto kwdefaults() const -> std::unique_ptr; 29 | auto closure() const -> std::unique_ptr; 30 | auto doc() const -> std::unique_ptr; 31 | auto name() const -> std::unique_ptr; 32 | auto dict() const -> std::unique_ptr override; 33 | auto weakreflist() const -> std::unique_ptr; 34 | auto module() const -> std::unique_ptr; 35 | auto annotations() const -> std::unique_ptr; 36 | auto qualname() const -> std::unique_ptr; 37 | auto repr(bool pretty = true) const -> std::string override; 38 | 39 | }; 40 | 41 | } -------------------------------------------------------------------------------- /include/PyIntObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include 5 | #include 6 | 7 | namespace PyExt::Remote { 8 | 9 | /// Represents a PyIntObject in the debuggee's address space. 10 | class PYEXT_PUBLIC PyIntObject : public PyObject 11 | { 12 | 13 | public: // Construction/Destruction. 14 | explicit PyIntObject(Offset objectAddress); 15 | 16 | public: // Members. 17 | auto intValue() const -> std::int32_t; 18 | auto repr(bool pretty = true) const -> std::string override; 19 | 20 | }; 21 | 22 | } -------------------------------------------------------------------------------- /include/PyInterpreterFrame.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyFrame.h" 4 | #include "RemoteType.h" 5 | #include "pyextpublic.h" 6 | 7 | #include 8 | 9 | class ExtRemoteTyped; 10 | 11 | namespace PyExt::Remote { 12 | 13 | class PyDictObject; 14 | class PyCodeObject; 15 | class PyFrameObject; 16 | class PyFunctionObject; 17 | class PyObject; 18 | 19 | /// Python 3.11 and later 20 | /// @see https://github.com/python/cpython/blob/master/include/internal/pycore_frame.h 21 | class PYEXT_PUBLIC PyInterpreterFrame : public RemoteType, public PyFrame 22 | { 23 | public: 24 | explicit PyInterpreterFrame(const RemoteType& remoteType); 25 | 26 | public: // Members of the remote type. 27 | using RemoteType::offset; 28 | auto locals() const->std::unique_ptr override; 29 | auto localsplus() const->std::vector>> override; 30 | auto globals() const->std::unique_ptr override; 31 | auto code() const->std::unique_ptr override; 32 | auto previous() const->std::unique_ptr override; 33 | auto prevInstruction() const -> int; 34 | auto currentLineNumber() const -> int override; 35 | }; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /include/PyInterpreterState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RemoteType.h" 4 | #include "pyextpublic.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class ExtRemoteTyped; 12 | 13 | namespace PyExt::Remote { 14 | 15 | class PyDictObject; 16 | class PyThreadState; 17 | 18 | /// Represents a PyInterpreterState instance in the debuggee's address space. 19 | /// @see https://github.com/python/cpython/blob/master/Include/pystate.h 20 | class PYEXT_PUBLIC PyInterpreterState : private RemoteType 21 | { 22 | public: // Contruction/Destruction. 23 | explicit PyInterpreterState(const RemoteType& remoteType); 24 | ~PyInterpreterState(); 25 | 26 | /// Returns the Python's global interpreter state instance. 27 | static auto makeAutoInterpreterState() -> std::unique_ptr; 28 | 29 | /// Returns a range of all interpreter states in the process, starting with the autoInterpreterState. 30 | static auto allInterpreterStates() -> std::vector; //< TODO: Return generator 31 | 32 | /// Returns the PyThreadState associated with a thread id or None if no such thread exists. 33 | static auto findThreadStateBySystemThreadId(std::uint64_t systemThreadId) -> std::optional; 34 | 35 | /// Provide a way to manually specify the interpreter state used by makeAutoInterpreterState(). 36 | static void setAutoInterpreterStateExpression(const std::string& expression); 37 | 38 | 39 | public: // Members of the remote type. 40 | auto next() const -> std::unique_ptr; 41 | auto tstate_head() const -> std::unique_ptr; 42 | 43 | public: // Utility functions around the members. 44 | /// Returns a range of all the threads in this interpreter. 45 | auto allThreadStates() const -> std::vector; //< TODO: Return generator 46 | 47 | private: 48 | #pragma warning (push) 49 | #pragma warning (disable: 4251) //< Hide warnings about exporting private symbols. 50 | static std::string autoInterpreterStateExpressionOverride; 51 | #pragma warning (pop) 52 | }; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /include/PyListObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyVarObject.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace PyExt::Remote { 10 | 11 | /// Represents a PyListObject in the debuggee's address space. 12 | class PYEXT_PUBLIC PyListObject : public PyVarObject 13 | { 14 | 15 | public: // Construction/Destruction. 16 | explicit PyListObject(Offset objectAddress); 17 | 18 | public: // Members. 19 | auto numItems() const -> SSize; 20 | auto at(SSize index) const -> std::unique_ptr; 21 | auto listValue() const -> std::vector>; 22 | auto repr(bool pretty = true) const -> std::string override; 23 | 24 | }; 25 | 26 | } -------------------------------------------------------------------------------- /include/PyLongObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | 5 | #include 6 | #include 7 | 8 | namespace PyExt::Remote { 9 | 10 | /// Represents a PyLongObject in the debuggee's address space. 11 | class PYEXT_PUBLIC PyLongObject : public PyObject 12 | { 13 | 14 | public: // Construction/Destruction. 15 | explicit PyLongObject(Offset objectAddress, const bool isBool = false); 16 | 17 | public: // Members. 18 | auto repr(bool pretty = true) const -> std::string override; 19 | 20 | private: 21 | const bool isBool; 22 | 23 | }; 24 | 25 | } -------------------------------------------------------------------------------- /include/PyMemberDef.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RemoteType.h" 4 | #include "pyextpublic.h" 5 | 6 | namespace PyExt::Remote { 7 | 8 | class PYEXT_PUBLIC PyMemberDef 9 | { 10 | 11 | public: // Constants. 12 | static const int T_SHORT = 0; 13 | static const int T_INT = 1; 14 | static const int T_LONG = 2; 15 | static const int T_FLOAT = 3; 16 | static const int T_DOUBLE = 4; 17 | static const int T_STRING = 5; 18 | static const int T_OBJECT = 6; 19 | static const int T_CHAR = 7; 20 | static const int T_BYTE = 8; 21 | static const int T_UBYTE = 9; 22 | static const int T_USHORT = 10; 23 | static const int T_UINT = 11; 24 | static const int T_ULONG = 12; 25 | static const int T_STRING_INPLACE = 13; 26 | static const int T_BOOL = 14; 27 | static const int T_OBJECT_EX = 16; 28 | static const int T_LONGLONG = 17; 29 | static const int T_ULONGLONG = 18; 30 | static const int T_PYSSIZET = 19; 31 | static const int T_NONE = 20; 32 | 33 | public: // Construction/Destruction. 34 | virtual ~PyMemberDef(); 35 | 36 | public: // Members of the remote type. 37 | virtual auto name() const -> std::string = 0; 38 | virtual auto type() const -> int = 0; 39 | virtual auto offset() const -> RemoteType::SSize = 0; 40 | 41 | }; 42 | 43 | 44 | class PYEXT_PUBLIC PyMemberDefAuto : private RemoteType, public PyMemberDef 45 | { 46 | 47 | public: // Construction/Destruction. 48 | explicit PyMemberDefAuto(Offset objectAddress); 49 | 50 | public: // Members of the remote type. 51 | auto name() const -> std::string override; 52 | auto type() const -> int override; 53 | auto offset() const -> SSize override; 54 | 55 | }; 56 | 57 | 58 | class PYEXT_PUBLIC PyMemberDefManual : public PyMemberDef 59 | { 60 | 61 | public: // Construction/Destruction. 62 | explicit PyMemberDefManual(RemoteType::Offset objectAddress); 63 | 64 | public: // Members of the remote type. 65 | auto name() const -> std::string override; 66 | auto type() const -> int override; 67 | auto offset() const -> RemoteType::SSize override; 68 | 69 | private: 70 | RemoteType::Offset objectAddress; 71 | 72 | }; 73 | 74 | } 75 | -------------------------------------------------------------------------------- /include/PyNoneObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include 5 | 6 | namespace PyExt::Remote { 7 | 8 | /// Represents an object of type NoneType. The most boring of the Python types. 9 | class PYEXT_PUBLIC PyNoneObject : public PyObject 10 | { 11 | 12 | public: // Construction/Destruction. 13 | explicit PyNoneObject(Offset objectAddress); 14 | 15 | public: // Members. 16 | auto repr(bool pretty = true) const -> std::string override; 17 | 18 | }; 19 | 20 | } -------------------------------------------------------------------------------- /include/PyNotImplementedObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include 5 | 6 | namespace PyExt::Remote { 7 | 8 | /// Represents an object of type NoneType. The most boring of the Python types. 9 | class PYEXT_PUBLIC PyNotImplementedObject : public PyObject 10 | { 11 | 12 | public: // Construction/Destruction. 13 | explicit PyNotImplementedObject(Offset objectAddress); 14 | 15 | public: // Members. 16 | auto repr(bool pretty = true) const -> std::string override; 17 | 18 | }; 19 | 20 | } -------------------------------------------------------------------------------- /include/PyObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pyextpublic.h" 4 | #include "RemoteType.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | class ExtRemoteTyped; 12 | 13 | namespace PyExt::Remote { 14 | 15 | class PyTypeObject; //< Forward Declaration. 16 | class PyDict; //< Forward Declaration. 17 | 18 | /// Represents a PyObject in the debuggee's address space. Base class for all types of PyObject. 19 | class PYEXT_PUBLIC PyObject : private RemoteType 20 | { 21 | 22 | public: // Typedefs. 23 | using RemoteType::Offset; 24 | using RemoteType::SSize; 25 | 26 | public: // Construction/Destruction. 27 | explicit PyObject(Offset objectAddress, const std::string& symbolName = "PyObject"); 28 | virtual ~PyObject(); 29 | 30 | // Polymorphic constructor. Creates the most-derived PyObject it can. 31 | static auto make(PyObject::Offset remoteAddress) -> std::unique_ptr; 32 | // Constructor by type name. Necessary to get the base type repr for types derived from built-in types. 33 | static auto make(PyObject::Offset remoteAddress, const std::string& typeName) -> std::unique_ptr; 34 | using RemoteType::readOffsetArray; 35 | 36 | public: // Members. 37 | using RemoteType::offset; 38 | using RemoteType::symbolName; 39 | auto refCount() const -> SSize; 40 | auto type() const -> PyTypeObject; 41 | auto slots() const -> std::vector>>; 42 | auto managedDict() const -> std::unique_ptr; 43 | virtual auto dict() const -> std::unique_ptr; 44 | virtual auto repr(bool pretty = true) const -> std::string; 45 | virtual auto details() const -> std::string; 46 | 47 | protected: // Helpers for more derived classes. 48 | /// Returns a field by name in the `ob_base` member. 49 | auto baseField(const std::string& fieldName) const -> ExtRemoteTyped; 50 | using RemoteType::remoteType; 51 | 52 | }; 53 | 54 | } -------------------------------------------------------------------------------- /include/PySetObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace PyExt::Remote { 10 | 11 | /// Represents a PySetObject in the debuggee's address space. 12 | class PYEXT_PUBLIC PySetObject : public PyObject 13 | { 14 | 15 | public: // Construction/Destruction. 16 | explicit PySetObject(Offset objectAddress); 17 | 18 | public: // Members. 19 | auto numItems() const -> SSize; 20 | auto listValue() const -> std::vector>; 21 | auto repr(bool pretty = true) const -> std::string override; 22 | 23 | }; 24 | 25 | } -------------------------------------------------------------------------------- /include/PyStringObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyVarObject.h" 4 | #include "PyStringValue.h" 5 | #include 6 | 7 | namespace PyExt::Remote { 8 | 9 | /// Represents a PyStringObject or a PyBytesObject in the debuggee's address space. 10 | class PYEXT_PUBLIC PyBaseStringObject : public PyVarObject, public PyStringValue 11 | { 12 | 13 | public: // Construction/Destruction. 14 | explicit PyBaseStringObject(Offset objectAddress, const std::string& typeName); 15 | 16 | public: // Members. 17 | auto stringLength() const -> SSize; 18 | auto stringValue() const -> std::string override; 19 | virtual auto repr(bool pretty = true) const -> std::string override; 20 | 21 | }; 22 | 23 | // Python3 has a PyBytesObject that takes the place of PyStringObject. 24 | // It shares the same layout as Python2's PyStringObject but has a different symbol name. 25 | 26 | /// Represents a Python3 PyBytesObject in the debuggee's address space. 27 | class PYEXT_PUBLIC PyBytesObject : public PyBaseStringObject { 28 | public: 29 | explicit PyBytesObject(Offset objectAddress); 30 | auto repr(bool pretty = true) const -> std::string override; 31 | }; 32 | 33 | 34 | /// Represents a Python2 PyStringObject in the debuggee's address space. 35 | class PYEXT_PUBLIC PyStringObject : public PyBaseStringObject 36 | { 37 | public: 38 | explicit PyStringObject(Offset objectAddress); 39 | }; 40 | 41 | } -------------------------------------------------------------------------------- /include/PyStringValue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pyextpublic.h" 4 | 5 | #include 6 | #include 7 | 8 | namespace PyExt::Remote { 9 | 10 | // Interface for objects that can provide a string representation of their value. 11 | // Intended for PyStringObject and PyUnicodeObject. 12 | class PYEXT_PUBLIC PyStringValue 13 | { 14 | 15 | public: // Construction/Destruction. 16 | virtual ~PyStringValue() = default; 17 | 18 | public: // Members. 19 | virtual auto stringValue() const -> std::string = 0; 20 | 21 | protected: // Helpers forimplementors of PyStringValue. 22 | enum class QuoteType { Single, Double }; 23 | static auto escapeAndQuoteString(const std::string& str, QuoteType quoteType = QuoteType::Single) -> std::string; 24 | }; 25 | 26 | } -------------------------------------------------------------------------------- /include/PyThreadState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RemoteType.h" 4 | #include "pyextpublic.h" 5 | 6 | #include 7 | 8 | class ExtRemoteTyped; 9 | 10 | namespace PyExt::Remote { 11 | 12 | class PyFrame; 13 | class PyFrameObject; 14 | class PyInterpreterFrame; 15 | 16 | /// Represents a PyInterpreterState instance in the debuggee's address space. 17 | /// @see https://github.com/python/cpython/blob/master/Include/pystate.h 18 | class PYEXT_PUBLIC PyThreadState : private RemoteType 19 | { 20 | public: 21 | explicit PyThreadState(const RemoteType& remoteType); 22 | ~PyThreadState(); 23 | 24 | public: // Members of the remote type. 25 | auto next() const -> std::unique_ptr; 26 | auto currentFrame() const -> std::unique_ptr; 27 | auto tracing() const -> long; 28 | auto thread_id() const -> long; 29 | 30 | public: // Utility functions around the members. 31 | /// Returns a range of all the frames in this threadState. 32 | auto allFrames() const -> std::vector>; //< TODO: Return generator 33 | }; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /include/PyTupleObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyVarObject.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace PyExt::Remote { 10 | 11 | /// Represents a PyTupleObject in the debuggee's address space. 12 | class PYEXT_PUBLIC PyTupleObject : public PyVarObject 13 | { 14 | 15 | public: // Construction/Destruction. 16 | explicit PyTupleObject(Offset objectAddress); 17 | 18 | public: // Members. 19 | auto numItems() const -> SSize; 20 | auto at(SSize index) const -> std::unique_ptr; 21 | auto listValue() const -> std::vector>; 22 | auto repr(bool pretty = true) const -> std::string override; 23 | 24 | }; 25 | 26 | } -------------------------------------------------------------------------------- /include/PyTypeObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyVarObject.h" 4 | #include "PyMemberDef.h" 5 | #include "PyTupleObject.h" 6 | #include "PyDictObject.h" 7 | #include 8 | #include 9 | 10 | namespace PyExt::Remote { 11 | 12 | /// Represents a PyTypeObject in the debuggee's address space. 13 | class PYEXT_PUBLIC PyTypeObject : public PyVarObject 14 | { 15 | public: // Statics. 16 | static auto builtinTypes() -> const std::vector&; 17 | 18 | public: // Construction/Destruction. 19 | explicit PyTypeObject(Offset objectAddress); 20 | 21 | public: // Members. 22 | auto name() const -> std::string; 23 | auto basicSize() const -> SSize; 24 | auto itemSize() const -> SSize; 25 | auto documentation() const -> std::string; 26 | auto members() const -> std::vector>; 27 | auto isManagedDict() const -> bool; 28 | auto getStaticBuiltinIndex() const -> SSize; 29 | auto hasInlineValues() const -> bool; 30 | auto dictOffset() const -> SSize; 31 | auto mro() const -> std::unique_ptr; 32 | auto isPython2() const -> bool; 33 | auto dict() const -> std::unique_ptr override; 34 | auto repr(bool pretty = true) const -> std::string override; 35 | 36 | }; 37 | 38 | } -------------------------------------------------------------------------------- /include/PyUnicodeObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include "PyStringValue.h" 5 | #include 6 | 7 | namespace PyExt::Remote { 8 | 9 | /// Represents a PyAscii/[Compact]UnicodeObject in the debuggee's address space. 10 | class PYEXT_PUBLIC PyUnicodeObject : public PyObject, public PyStringValue 11 | { 12 | 13 | public: // Construction/Destruction. 14 | explicit PyUnicodeObject(Offset objectAddress); 15 | 16 | public: // Enums. 17 | /// SSTATE. 18 | enum InterningState { NotInterned, InternedMortal, InternedImortal }; 19 | 20 | /// PyUnicode_Kind. 21 | enum class Kind { Wchar, OneByte, TwoByte, FourByte }; 22 | 23 | public: // String state. 24 | auto interningState() const -> InterningState; 25 | auto kind() const -> Kind; 26 | auto isCompact() const -> bool; 27 | auto isAscii() const -> bool; 28 | auto isReady() const -> bool; 29 | 30 | public: // Members. 31 | auto stringLength() const -> SSize; 32 | auto stringValue() const -> std::string override; 33 | auto repr(bool pretty = true) const -> std::string override; 34 | 35 | }; 36 | 37 | } -------------------------------------------------------------------------------- /include/PyVarObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | #include 5 | 6 | namespace PyExt::Remote { 7 | 8 | /// Base class for all variable-sized objects. 9 | /// Represents a PyVarObject (objects with a dynamic allocation) in the debuggee's address space. 10 | class PYEXT_PUBLIC PyVarObject : public PyObject 11 | { 12 | 13 | public: // Construction/Destruction. 14 | explicit PyVarObject(Offset objectAddress, const std::string& typeName = "PyVarObject"); 15 | 16 | public: // Members. 17 | SSize size() const; 18 | 19 | }; 20 | 21 | } -------------------------------------------------------------------------------- /include/RemoteType.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pyextpublic.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | 10 | class ExtRemoteTyped; 11 | 12 | namespace PyExt::Remote { 13 | 14 | /// Represents an instance of a type in the debuggee's address space. 15 | class PYEXT_PUBLIC RemoteType 16 | { 17 | 18 | public: // Typedefs. 19 | using Offset = std::uint64_t; 20 | using SSize = std::int64_t; 21 | 22 | public: // Construction/Destruction. 23 | explicit RemoteType(const std::string& objectExpression); 24 | explicit RemoteType(Offset objectAddress, const std::string& symbolName); 25 | explicit RemoteType(ExtRemoteTyped remoteType); 26 | virtual ~RemoteType(); 27 | 28 | public: // Copy/Move. 29 | RemoteType(const RemoteType&); 30 | RemoteType& operator=(const RemoteType&); 31 | RemoteType(RemoteType&&); 32 | RemoteType& operator=(RemoteType&&); 33 | 34 | public: // Methods. 35 | auto offset() const -> Offset; 36 | auto symbolName() const -> std::string; 37 | // necessary for x86 because offsets in remote address space are only 32 Bit 38 | static auto readOffsetArray(/*const*/ ExtRemoteTyped& remoteArray, std::uint64_t numElements) -> std::vector; 39 | 40 | protected: // Helpers for more derived classes. 41 | /// Access to the instance's memory in the debuggee. 42 | auto remoteType() const -> ExtRemoteTyped&; 43 | 44 | private: // Data. 45 | #pragma warning (push) 46 | #pragma warning (disable: 4251) //< Hide warnings about exporting private symbols. 47 | std::string symbolName_; 48 | std::shared_ptr remoteType_; 49 | #pragma warning (pop) 50 | 51 | }; 52 | 53 | } -------------------------------------------------------------------------------- /include/globals.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pyextpublic.h" 4 | #include 5 | 6 | namespace PyExt { 7 | 8 | // These functions are provided for testability. 9 | // They allow the test harness to initialize/uninitialize the global extension instance 10 | // as would typically happen when calling an extension command. 11 | PYEXT_PUBLIC auto InitializeGlobalsForTest(IDebugClient* pClient) -> void; 12 | PYEXT_PUBLIC auto UninitializeGlobalsForTest() -> void; 13 | 14 | } -------------------------------------------------------------------------------- /include/pyextpublic.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Export symbols when building pyext.dll. Import linking against pyext.dll 4 | #ifdef PYEXT_DLL 5 | # define PYEXT_PUBLIC __declspec(dllexport) 6 | #else 7 | # define PYEXT_PUBLIC __declspec(dllimport) 8 | #endif 9 | -------------------------------------------------------------------------------- /include/utils/ScopeExit.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace utils { 6 | 7 | template 8 | class ScopeExit 9 | { 10 | 11 | public: 12 | ScopeExit(Invokable&& f) 13 | : f_{ std::forward(f) }, 14 | armed_{ true } 15 | { } 16 | 17 | ~ScopeExit() 18 | { 19 | if (armed_) 20 | f_(); 21 | } 22 | 23 | ScopeExit(ScopeExit&& other) 24 | : f_{ std::move(other.f_) }, 25 | armed_{ true } 26 | { 27 | other.reset(); 28 | }; 29 | 30 | ScopeExit& operator=(ScopeExit&& other) 31 | { 32 | f_ = std::move(other.f_); 33 | other.reset(); 34 | } 35 | 36 | void set(Invokable&& f) 37 | { 38 | f_ = std::forward(f); 39 | armed_ = true; 40 | } 41 | 42 | void reset() 43 | { 44 | armed_ = false; 45 | } 46 | 47 | 48 | private: // Non-copyable. 49 | ScopeExit(const ScopeExit&) = delete; 50 | ScopeExit& operator=(const ScopeExit&) = delete; 51 | 52 | private: 53 | Invokable f_; 54 | bool armed_; 55 | 56 | }; 57 | 58 | 59 | template 60 | ScopeExit makeScopeExit(Invokable&& f) { 61 | return ScopeExit(std::forward(f)); 62 | }; 63 | 64 | } -------------------------------------------------------------------------------- /include/utils/lossless_cast.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace utils { 6 | 7 | /// Converts from one type to another. If equality is cannot preserved an overflow_error is raised. 8 | template 9 | auto lossless_cast(Source value) -> Target 10 | { 11 | auto to = static_cast(value); 12 | 13 | if (static_cast(to) != value) 14 | throw std::overflow_error("lossless_cast could not represent value in target type."); 15 | 16 | return to; 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/ExtHelpers.cpp: -------------------------------------------------------------------------------- 1 | #include "ExtHelpers.h" 2 | 3 | #include 4 | #include 5 | using namespace std; 6 | 7 | 8 | namespace utils { 9 | 10 | auto getPointerSize() -> int 11 | { 12 | string objExpression = "sizeof(void*)"s; 13 | ExtRemoteTyped remoteObj(objExpression.c_str()); 14 | return utils::readIntegral(remoteObj); 15 | } 16 | 17 | 18 | auto escapeDml(const string& str) -> string 19 | { 20 | std::string buffer; 21 | buffer.reserve(str.size()); 22 | for (auto ch : str) { 23 | switch (ch) { 24 | case '&': buffer += "&"; break; 25 | case '\"': buffer += """; break; 26 | // case '\'': buffer += "'"; break; no DML special character?! 27 | case '<': buffer += "<"; break; 28 | case '>': buffer += ">"; break; 29 | default: buffer += ch; break; 30 | } 31 | } 32 | return buffer; 33 | } 34 | 35 | 36 | auto link(const string& text, const string& cmd, const string& alt) -> string 37 | { 38 | ostringstream oss; 39 | oss << "" << escapeDml(text) << ""; 43 | return oss.str(); 44 | } 45 | 46 | 47 | auto getFullSymbolName(const string& symbolName) -> string 48 | { 49 | ExtBuffer buffer; 50 | g_Ext->FindFirstModule("python???", &buffer, 0); 51 | return buffer.GetBuffer() + "!"s + symbolName; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/ExtHelpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | using namespace std::literals::string_literals; 9 | 10 | namespace utils { 11 | 12 | /// Reads the correct number of bytes from an ExtRemoteTyped and casts it to the provided integral type. 13 | template 14 | auto readIntegral(/*const*/ ExtRemoteTyped& remoteData) -> Integral 15 | { 16 | bool isSigned = std::is_signed_v; 17 | auto size = remoteData.GetTypeSize(); 18 | switch (size) { 19 | case 1: 20 | return isSigned ? static_cast(remoteData.GetChar()) : static_cast(remoteData.GetUchar()); 21 | case 2: 22 | return isSigned ? static_cast(remoteData.GetShort()) : static_cast(remoteData.GetUshort()); 23 | case 4: 24 | return isSigned ? static_cast(remoteData.GetLong()) : static_cast(remoteData.GetUlong()); 25 | case 8: 26 | return isSigned ? static_cast(remoteData.GetLong64()) : static_cast(remoteData.GetUlong64()); 27 | } 28 | 29 | g_Ext->ThrowInterrupt(); 30 | g_Ext->ThrowRemote(E_INVALIDARG, "Invalid ExtRemoteTyped size for integral read."); 31 | } 32 | 33 | 34 | // Reads an array of elements pointed to by an ExtRemoteData. 35 | template 36 | auto readArray(/*const*/ ExtRemoteTyped& remoteArray, std::uint64_t numElements) -> std::vector 37 | { 38 | if (numElements == 0) 39 | return { }; 40 | 41 | auto remoteData = remoteArray.Dereference(); 42 | const auto remoteSize = remoteData.GetTypeSize(); 43 | if (remoteSize != sizeof(ElemType)) { 44 | g_Ext->ThrowRemote(E_INVALIDARG, "sizeof(ElemType) does not match size type size for ExtRemoteTyped array read."); 45 | } 46 | 47 | const auto arrayExtent = numElements * remoteSize; 48 | // ExtRemoteData only supports arrays with 32bit extents, so we have to narrow it. 49 | if (numElements > std::numeric_limits::max()) { 50 | g_Ext->ThrowRemote(E_BOUNDS, "Could not read array. Size too large."); 51 | } 52 | 53 | std::vector buffer(numElements); 54 | remoteData.ReadBuffer(buffer.data(), static_cast(arrayExtent)); 55 | return buffer; 56 | } 57 | 58 | 59 | template 60 | auto ignoreExtensionError(T&& function) -> void 61 | { 62 | ExtCaptureOutputA ignoreOut; 63 | ignoreOut.Start(); 64 | try { 65 | function(); 66 | } catch (ExtException&) { } 67 | } 68 | 69 | 70 | auto getPointerSize() -> int; 71 | auto escapeDml(const std::string& str) -> std::string; 72 | auto link(const std::string& text, const std::string& cmd, const std::string& alt = ""s) -> std::string; 73 | auto getFullSymbolName(const std::string& symbolName) -> std::string; 74 | 75 | } -------------------------------------------------------------------------------- /src/PyExt.def: -------------------------------------------------------------------------------- 1 | LIBRARY 2 | EXPORTS 3 | 4 | ; EngExtCpp exports. 5 | DebugExtensionInitialize 6 | DebugExtensionUninitialize 7 | DebugExtensionNotify 8 | KnownStructOutputEx 9 | help 10 | 11 | ; Extension commands. 12 | pyobj 13 | pystack 14 | pysymfix 15 | pysetautointerpreterstate 16 | pyinterpreterframe 17 | -------------------------------------------------------------------------------- /src/PyExt.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanCline/PyExt/9877ce0f2f79889985f13d1a980383c024862e2e/src/PyExt.rc -------------------------------------------------------------------------------- /src/PyExt.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 15.0 23 | {EEBC73BA-27F2-4483-8639-54A108B77594} 24 | PyExt 25 | 10.0.17763.0 26 | 10.0.10240.0 27 | 28 | 29 | 30 | $(SolutionDir)$(Platform)\$(Configuration)\ 31 | $(Platform)\$(Configuration)\ 32 | 33 | 34 | DynamicLibrary 35 | v141 36 | 37 | 38 | true 39 | 40 | 41 | false 42 | true 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | $(ProjectName.ToLower()) 55 | 56 | 57 | 58 | Level4 59 | true 60 | $(SolutionDir)\include;$(WindowsSdkDir)\Debuggers\inc;%(AdditionalIncludeDirectories) 61 | Caret 62 | Use 63 | pch.h 64 | pch.h 65 | stdcpp17 66 | PYEXT_DLL;NOMINMAX;%(PreprocessorDefinitions) 67 | 68 | 69 | Windows 70 | dbgeng.lib 71 | $(WindowsSdkDir)\Debuggers\lib\$(PlatformTarget) 72 | $(ProjectName).def 73 | 74 | 75 | 76 | 77 | Disabled 78 | 79 | 80 | 81 | 82 | MaxSpeed 83 | true 84 | true 85 | true 86 | 87 | 88 | true 89 | true 90 | UseLinkTimeCodeGeneration 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | Create 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 5040;4838;4267;4245 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | $(FrameworkSdkDir)Debuggers/$(PlatformTarget)/windbg.exe 189 | WindowsLocalDebugger 190 | -c ".load $(TargetPath); $$>< $(ProjectDir)/init.wdb" -z "$(SolutionDir)/test/scripts/object_types.dmp" 191 | 192 | -------------------------------------------------------------------------------- /src/PyExt.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | {644d828f-7d56-440a-a72b-2328b59eebf9} 18 | 19 | 20 | {5da2ca2a-ccb3-4b5c-acbe-3fc465e14af6} 21 | 22 | 23 | {59f4fa16-82cf-46e3-8162-863e9d05d686} 24 | 25 | 26 | 27 | 28 | Source Files 29 | 30 | 31 | Source Files 32 | 33 | 34 | Source Files 35 | 36 | 37 | Source Files\objects 38 | 39 | 40 | Source Files\objects 41 | 42 | 43 | Source Files\objects 44 | 45 | 46 | Source Files\objects 47 | 48 | 49 | Source Files\objects 50 | 51 | 52 | Source Files\objects 53 | 54 | 55 | Source Files\objects 56 | 57 | 58 | Source Files\objects 59 | 60 | 61 | Source Files\objects 62 | 63 | 64 | Source Files\objects 65 | 66 | 67 | Source Files\objects 68 | 69 | 70 | Source Files\objects 71 | 72 | 73 | Source Files\objects 74 | 75 | 76 | Source Files\objects 77 | 78 | 79 | Source Files\objects 80 | 81 | 82 | Source Files 83 | 84 | 85 | Source Files\objects 86 | 87 | 88 | Source Files\objects 89 | 90 | 91 | Source Files\objects 92 | 93 | 94 | Source Files\objects 95 | 96 | 97 | Source Files\objects 98 | 99 | 100 | Source Files\objects 101 | 102 | 103 | Source Files 104 | 105 | 106 | Source Files 107 | 108 | 109 | Source Files\objects 110 | 111 | 112 | Source Files 113 | 114 | 115 | Source Files 116 | 117 | 118 | Source Files 119 | 120 | 121 | Source Files 122 | 123 | 124 | Source Files 125 | 126 | 127 | Source Files 128 | 129 | 130 | Source Files 131 | 132 | 133 | Source Files 134 | 135 | 136 | Source Files 137 | 138 | 139 | Source Files 140 | 141 | 142 | Source Files 143 | 144 | 145 | Source Files\objects 146 | 147 | 148 | 149 | 150 | Header Files 151 | 152 | 153 | Header Files\objects 154 | 155 | 156 | Header Files\objects 157 | 158 | 159 | Header Files\objects 160 | 161 | 162 | Header Files\objects 163 | 164 | 165 | Header Files\objects 166 | 167 | 168 | Header Files\objects 169 | 170 | 171 | Header Files\objects 172 | 173 | 174 | Header Files\objects 175 | 176 | 177 | Header Files\objects 178 | 179 | 180 | Header Files\objects 181 | 182 | 183 | Header Files\objects 184 | 185 | 186 | Header Files\objects 187 | 188 | 189 | Header Files\objects 190 | 191 | 192 | Header Files\objects 193 | 194 | 195 | Header Files\objects 196 | 197 | 198 | Header Files 199 | 200 | 201 | Header Files\objects 202 | 203 | 204 | Header Files 205 | 206 | 207 | Header Files\utils 208 | 209 | 210 | Header Files 211 | 212 | 213 | Header Files\utils 214 | 215 | 216 | Header Files\objects 217 | 218 | 219 | Header Files\objects 220 | 221 | 222 | Header Files\objects 223 | 224 | 225 | Header Files 226 | 227 | 228 | Header Files 229 | 230 | 231 | Header Files\objects 232 | 233 | 234 | Header Files 235 | 236 | 237 | Header Files 238 | 239 | 240 | Header Files 241 | 242 | 243 | Header Files\objects 244 | 245 | 246 | Header Files 247 | 248 | 249 | Header Files 250 | 251 | 252 | Header Files 253 | 254 | 255 | Header Files 256 | 257 | 258 | Header Files\objects 259 | 260 | 261 | 262 | 263 | Resource Files 264 | 265 | 266 | 267 | 268 | Source Files 269 | 270 | 271 | -------------------------------------------------------------------------------- /src/PyFrame.cpp: -------------------------------------------------------------------------------- 1 | #include "PyFrame.h" 2 | 3 | #include "PyObject.h" 4 | 5 | #include 6 | #include 7 | using namespace std; 8 | 9 | namespace PyExt::Remote { 10 | 11 | PyFrame::~PyFrame() 12 | { 13 | } 14 | 15 | 16 | auto PyFrame::details() const -> string 17 | { 18 | const auto elementSeparator = "\n"; 19 | const auto indentation = "\t"; 20 | 21 | ostringstream oss; 22 | oss << "localsplus: {" << elementSeparator; 23 | 24 | for (auto const& pairValue : localsplus()) { 25 | auto const& key = pairValue.first; 26 | auto const& value = pairValue.second; 27 | if (value != nullptr) 28 | oss << indentation << key << ": " << value->repr(true) << ',' << elementSeparator; 29 | } 30 | 31 | oss << '}'; 32 | return oss.str(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/PyInterpreterFrame.cpp: -------------------------------------------------------------------------------- 1 | #include "PyInterpreterFrame.h" 2 | 3 | 4 | #include "PyObject.h" 5 | #include "PyCodeObject.h" 6 | #include "PyDictObject.h" 7 | #include "PyFrameObject.h" 8 | 9 | #include "fieldAsPyObject.h" 10 | #include "ExtHelpers.h" 11 | 12 | #include 13 | 14 | #include 15 | using namespace std; 16 | 17 | namespace PyExt::Remote { 18 | 19 | PyInterpreterFrame::PyInterpreterFrame(const RemoteType& remoteType) 20 | : RemoteType(remoteType) 21 | { 22 | } 23 | 24 | 25 | auto PyInterpreterFrame::locals() const -> unique_ptr 26 | { 27 | // Note: The CPython code comments indicate that this isn't always a dict object. In practice, it seems to be. 28 | return utils::fieldAsPyObject(remoteType(), "f_locals"); 29 | } 30 | 31 | auto PyInterpreterFrame::localsplus() const -> vector>> 32 | { 33 | auto codeObject = code(); 34 | if (codeObject == nullptr) 35 | return {}; 36 | 37 | vector names = codeObject->localsplusNames(); 38 | auto numLocalsplus = names.size(); 39 | if (numLocalsplus == 0) 40 | return {}; 41 | 42 | auto f_localsplus = remoteType().Field("localsplus"); 43 | auto pyObjAddrs = readOffsetArray(f_localsplus, numLocalsplus); 44 | vector>> localsplus(numLocalsplus); 45 | for (size_t i = 0; i < numLocalsplus; ++i) { 46 | auto addr = pyObjAddrs.at(i); 47 | auto objPtr = addr ? PyObject::make(addr) : nullptr; 48 | localsplus[i] = make_pair(names.at(i), move(objPtr)); 49 | } 50 | return localsplus; 51 | } 52 | 53 | auto PyInterpreterFrame::globals() const -> unique_ptr 54 | { 55 | return utils::fieldAsPyObject(remoteType(), "f_globals"); 56 | } 57 | 58 | 59 | auto PyInterpreterFrame::code() const -> unique_ptr 60 | { 61 | auto code = utils::fieldAsPyObject(remoteType(), "f_executable"); 62 | if (code != nullptr) 63 | return code; // Python 3.13+ 64 | return utils::fieldAsPyObject(remoteType(), "f_code"); 65 | } 66 | 67 | 68 | auto PyInterpreterFrame::previous() const -> unique_ptr 69 | { 70 | auto previous = remoteType().Field("previous"); 71 | if (previous.GetPtr() == 0) 72 | return { }; 73 | 74 | auto ownerRaw = previous.Field("owner"); 75 | auto owner = utils::readIntegral(ownerRaw); 76 | if (owner == 3) { // FRAME_OWNED_BY_CSTACK 77 | // see https://github.com/python/cpython/blob/3bd942f106aa36c261a2d90104c027026b2a8fb6/Python/traceback.c#L979-L982 78 | previous = previous.Field("previous"); 79 | if (previous.GetPtr() == 0) 80 | return { }; 81 | 82 | ownerRaw = previous.Field("owner"); 83 | owner = utils::readIntegral(ownerRaw); 84 | if (owner == 3) 85 | throw runtime_error("Cannot have more than one shim frame in a row."); 86 | } 87 | 88 | return make_unique(RemoteType(previous)); 89 | } 90 | 91 | 92 | auto PyInterpreterFrame::prevInstruction() const -> int 93 | { 94 | auto instrPtr = remoteType().HasField("instr_ptr") 95 | ? remoteType().Field("instr_ptr") // Python 3.13+ 96 | : remoteType().Field("prev_instr"); 97 | return utils::readIntegral(instrPtr); 98 | } 99 | 100 | 101 | auto PyInterpreterFrame::currentLineNumber() const -> int 102 | { 103 | auto codeObject = code(); 104 | 105 | // Do a lookup into the code object's line number table (co_linetable). 106 | return codeObject->lineNumberFromPrevInstruction(prevInstruction()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/PyInterpreterState.cpp: -------------------------------------------------------------------------------- 1 | #include "PyInterpreterState.h" 2 | #include "PyThreadState.h" 3 | #include "PyFrameObject.h" 4 | #include "PyDictObject.h" 5 | 6 | #include "fieldAsPyObject.h" 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | using namespace std; 15 | 16 | namespace PyExt::Remote { 17 | 18 | PyInterpreterState::PyInterpreterState(const RemoteType& remoteType) 19 | : RemoteType(remoteType) 20 | { 21 | } 22 | 23 | 24 | PyInterpreterState::~PyInterpreterState() 25 | { 26 | } 27 | 28 | 29 | auto PyInterpreterState::makeAutoInterpreterState() -> unique_ptr 30 | { 31 | string errorMessage; // Build a string of everything that went wrong. 32 | 33 | // Capture any errors that occured. Only print them when none of the fallbacks were successful. 34 | ExtCaptureOutputA errorOutput; 35 | errorOutput.Start(); 36 | 37 | // See if a autoInterpreterState offset has been manually provided. 38 | if (!autoInterpreterStateExpressionOverride.empty()) { 39 | try { 40 | return make_unique(RemoteType(autoInterpreterStateExpressionOverride)); 41 | } catch (ExtException& ex) { 42 | errorMessage += ex.GetMessage() + "\n"s; 43 | }; 44 | } 45 | 46 | // In Python 3.7, the autoInterpreterState has moved into the gilstate. Try it first. 47 | try { 48 | return make_unique(RemoteType("_PyRuntime.gilstate.autoInterpreterState"s)); 49 | } catch (ExtException& ex) { 50 | errorMessage += ex.GetMessage() + "\n"s; 51 | }; 52 | 53 | // Fall back on the pre-3.7 global autoInterpreterState. 54 | try { 55 | return make_unique(RemoteType("autoInterpreterState"s)); 56 | } catch (ExtException& ex) { 57 | errorMessage += ex.GetMessage() + "\n"s; 58 | }; 59 | 60 | // All fallbacks failed. Report the error. 61 | throw runtime_error("Could not find autoInterpreterState.\n"s + errorMessage + "\n"s + errorOutput.GetTextNonNull()); 62 | } 63 | 64 | 65 | auto PyInterpreterState::allInterpreterStates() -> std::vector 66 | { 67 | vector states; 68 | for (auto state = makeAutoInterpreterState(); state != nullptr; state = state->next()) { 69 | states.push_back(*state); 70 | } 71 | return states; 72 | } 73 | 74 | 75 | auto PyInterpreterState::findThreadStateBySystemThreadId(uint64_t systemThreadId) -> optional 76 | { 77 | for (auto istate = makeAutoInterpreterState(); istate != nullptr; istate = istate->next()) { 78 | for (auto tstate = istate->tstate_head(); tstate != nullptr; tstate = tstate->next()) { 79 | if (tstate->thread_id() == systemThreadId) { 80 | return move(*tstate); 81 | } 82 | } 83 | } 84 | 85 | return { }; 86 | } 87 | 88 | string PyInterpreterState::autoInterpreterStateExpressionOverride; 89 | void PyInterpreterState::setAutoInterpreterStateExpression(const string& expression) 90 | { 91 | // TODO: Consider validating the expression here rather than only in makeAutoInterpreterState. 92 | autoInterpreterStateExpressionOverride = expression; 93 | } 94 | 95 | 96 | auto PyInterpreterState::next() const -> std::unique_ptr 97 | { 98 | auto next = remoteType().Field("next"); 99 | if (next.GetPtr() == 0) 100 | return { }; 101 | 102 | return make_unique(RemoteType(next)); 103 | } 104 | 105 | 106 | auto PyInterpreterState::tstate_head() const -> unique_ptr 107 | { 108 | if (remoteType().HasField("tstate_head")) { 109 | // Old, pre-3.11 location of tstate_head. 110 | return make_unique(RemoteType(remoteType().Field("tstate_head"))); 111 | } else { 112 | // New, 3.11+ location of the head thread. 113 | return make_unique(RemoteType(remoteType().Field("threads").Field("head"))); 114 | } 115 | } 116 | 117 | 118 | auto PyInterpreterState::allThreadStates() const -> std::vector 119 | { 120 | vector states; 121 | for (auto state = tstate_head(); state != nullptr; state = state->next()) { 122 | states.push_back(*state); 123 | } 124 | return states; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/PyMemberDef.cpp: -------------------------------------------------------------------------------- 1 | #include "PyMemberDef.h" 2 | 3 | #include "ExtHelpers.h" 4 | 5 | using namespace std; 6 | 7 | namespace PyExt::Remote { 8 | 9 | PyMemberDef::~PyMemberDef() 10 | { 11 | } 12 | 13 | 14 | PyMemberDefAuto::PyMemberDefAuto(Offset objectAddress) 15 | : RemoteType(objectAddress, "PyMemberDef") 16 | { 17 | } 18 | 19 | 20 | auto PyMemberDefAuto::name() const -> string 21 | { 22 | ExtBuffer buff; 23 | remoteType().Field("name").Dereference().GetString(&buff); 24 | return buff.GetBuffer(); 25 | } 26 | 27 | 28 | auto PyMemberDefAuto::type() const -> int 29 | { 30 | auto type_ = remoteType().Field("type"); 31 | return utils::readIntegral(type_); 32 | } 33 | 34 | 35 | auto PyMemberDefAuto::offset() const -> SSize 36 | { 37 | auto offset_ = remoteType().Field("offset"); 38 | return utils::readIntegral(offset_); 39 | } 40 | 41 | 42 | PyMemberDefManual::PyMemberDefManual(RemoteType::Offset objectAddress) 43 | : objectAddress(objectAddress) 44 | { 45 | } 46 | 47 | 48 | auto PyMemberDefManual::name() const -> string 49 | { 50 | ExtBuffer buff; 51 | ExtRemoteTyped("(char**)@$extin", objectAddress).Dereference().Dereference().GetString(&buff); 52 | return buff.GetBuffer(); 53 | } 54 | 55 | 56 | auto PyMemberDefManual::type() const -> int 57 | { 58 | auto addr = objectAddress + utils::getPointerSize(); 59 | auto type_ = ExtRemoteTyped("(int*)@$extin", addr).Dereference(); 60 | return utils::readIntegral(type_); 61 | } 62 | 63 | 64 | auto PyMemberDefManual::offset() const -> RemoteType::SSize 65 | { 66 | auto addr = objectAddress + utils::getPointerSize() * 2; 67 | auto offset_ = ExtRemoteTyped("(Py_ssize_t*)@$extin", addr).Dereference(); 68 | return utils::readIntegral(offset_); 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /src/PyThreadState.cpp: -------------------------------------------------------------------------------- 1 | #include "PyThreadState.h" 2 | #include "PyFrame.h" 3 | #include "PyFrameObject.h" 4 | #include "PyInterpreterFrame.h" 5 | 6 | #include "fieldAsPyObject.h" 7 | #include "ExtHelpers.h" 8 | 9 | #include 10 | 11 | #include 12 | using namespace std; 13 | 14 | namespace PyExt::Remote { 15 | 16 | PyThreadState::PyThreadState(const RemoteType& remoteType) 17 | : RemoteType(remoteType) 18 | { 19 | } 20 | 21 | 22 | PyThreadState::~PyThreadState() 23 | { 24 | } 25 | 26 | 27 | auto PyThreadState::next() const -> std::unique_ptr 28 | { 29 | auto next = remoteType().Field("next"); 30 | if (next.GetPtr() == 0) 31 | return { }; 32 | 33 | return make_unique(RemoteType(next)); 34 | } 35 | 36 | 37 | auto PyThreadState::currentFrame() const -> std::unique_ptr 38 | { 39 | auto frameObject = utils::fieldAsPyObject(remoteType(), "frame"); 40 | if (frameObject != nullptr) 41 | return frameObject; // Python < 3.11 42 | 43 | auto frameContainer = remoteType(); // Python 3.13+ 44 | if (remoteType().HasField("cframe")) { 45 | // Python 3.11, 3.12 46 | frameContainer = remoteType().Field("cframe"); 47 | if (frameContainer.GetPtr() == 0) 48 | return { }; 49 | } 50 | 51 | auto frame = frameContainer.Field("current_frame"); 52 | if (frame.GetPtr() == 0) 53 | return { }; 54 | return make_unique(RemoteType(frame)); 55 | } 56 | 57 | 58 | auto PyThreadState::tracing() const -> long 59 | { 60 | auto field = remoteType().Field("tracing"); 61 | return utils::readIntegral(field); 62 | } 63 | 64 | 65 | auto PyThreadState::thread_id() const -> long 66 | { 67 | auto field = remoteType().Field("thread_id"); 68 | return utils::readIntegral(field); 69 | } 70 | 71 | 72 | auto PyThreadState::allFrames() const -> vector> 73 | { 74 | vector> frames; 75 | shared_ptr f; 76 | f = currentFrame(); 77 | for (; f != nullptr; f = f->previous()) { 78 | frames.push_back(f); 79 | } 80 | return frames; 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /src/RemoteType.cpp: -------------------------------------------------------------------------------- 1 | #include "RemoteType.h" 2 | 3 | #include "ExtHelpers.h" 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | using namespace std; 10 | 11 | namespace PyExt::Remote { 12 | 13 | RemoteType::RemoteType(const string& objectExpression) 14 | : remoteType_(make_shared(objectExpression.c_str())) 15 | { 16 | } 17 | 18 | RemoteType::RemoteType(Offset objectAddress, const std::string& symbolName) 19 | : symbolName_(symbolName), 20 | remoteType_(make_shared(symbolName.c_str(), objectAddress, true)) 21 | { 22 | } 23 | 24 | RemoteType::RemoteType(ExtRemoteTyped remoteType) 25 | : remoteType_(make_shared(std::move(remoteType))) 26 | { 27 | } 28 | 29 | RemoteType::~RemoteType() = default; 30 | RemoteType::RemoteType(const RemoteType&) = default; 31 | RemoteType& RemoteType::operator=(const RemoteType&) = default; 32 | RemoteType::RemoteType(RemoteType&&) = default; 33 | RemoteType& RemoteType::operator=(RemoteType&&) = default; 34 | 35 | 36 | auto RemoteType::offset() const -> Offset 37 | { 38 | return remoteType().GetPtr(); 39 | } 40 | 41 | 42 | auto RemoteType::symbolName() const -> std::string 43 | { 44 | return symbolName_.empty() ? remoteType().GetTypeName() : symbolName_; 45 | } 46 | 47 | 48 | auto RemoteType::remoteType() const -> ExtRemoteTyped& 49 | { 50 | return *remoteType_; 51 | } 52 | 53 | 54 | auto RemoteType::readOffsetArray(/*const*/ ExtRemoteTyped& remoteArray, std::uint64_t numElements) -> vector 55 | { 56 | auto ptrSize = utils::getPointerSize(); 57 | switch (ptrSize) { 58 | case 4: { 59 | // x86 - 32 Bit Python 60 | auto ptrs = utils::readArray(remoteArray, numElements); 61 | vector offsets(begin(ptrs), end(ptrs)); 62 | return offsets; 63 | } 64 | case 8: 65 | // x64 - 64 Bit Python 66 | return utils::readArray(remoteArray, numElements); 67 | } 68 | 69 | g_Ext->ThrowInterrupt(); 70 | g_Ext->ThrowRemote(E_INVALIDARG, "Unsupported pointer size."); 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/extension.cpp: -------------------------------------------------------------------------------- 1 | #include "extension.h" 2 | 3 | #include "PyObject.h" 4 | #include "PyVarObject.h" 5 | #include "PyTypeObject.h" 6 | #include "PyInterpreterFrame.h" 7 | #include "RemoteType.h" 8 | #include "PyInterpreterState.h" 9 | using namespace PyExt::Remote; 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | using namespace std; 16 | 17 | namespace PyExt { 18 | 19 | EXT_CLASS::EXT_CLASS() 20 | { 21 | // Set up our known struct handlers. 22 | // Windbg uses these to know how to pretty-print types. 23 | auto handler = static_cast(&EXT_CLASS::KnownStructObjectHandler); 24 | static ExtKnownStruct knownstructs[] = { 25 | { "PyVarObject", handler, true }, 26 | { "PyObject", handler, true }, 27 | { "_object", handler, true }, 28 | { "PyTypeObject", handler, true }, 29 | { "_typeobject", handler, true }, 30 | { "PyFrameObject", handler, true }, 31 | { "_frame", handler, true }, 32 | { "_dictobject", handler, true }, 33 | { "PyDictObject", handler, true }, 34 | { "_setobject", handler, true }, 35 | { "PySetObject", handler, true }, 36 | { "PyCodeObject", handler, true }, 37 | { nullptr, nullptr, false } 38 | }; 39 | 40 | m_KnownStructs = knownstructs; 41 | } 42 | 43 | 44 | void EXT_CLASS::KnownStructObjectHandler(_In_ PCSTR /*TypeName*/, _In_ ULONG Flags, _In_ ULONG64 Offset) 45 | { 46 | if (Flags == DEBUG_KNOWN_STRUCT_GET_SINGLE_LINE_OUTPUT) 47 | { 48 | auto pyObj = PyObject::make(Offset); 49 | 50 | AppendString("Type: %s ", pyObj->type().name().c_str()); 51 | 52 | auto repr = pyObj->repr(false); 53 | if (!repr.empty()) { 54 | if (repr.size() > m_AppendBufferChars 55 | || (ULONG_PTR)(m_AppendAt - m_AppendBuffer) > m_AppendBufferChars - repr.size()) { 56 | auto message = "offset()); 57 | AppendBufferString(message.c_str()); 58 | } else { 59 | AppendBufferString(repr.c_str()); 60 | } 61 | } 62 | } 63 | } 64 | 65 | 66 | auto EXT_CLASS::printDml(const string& content) -> void 67 | { 68 | // There is a limit for single output. It seems impossible to know the exact value 69 | // of the limit in advance, so we take the value from the documentation. 70 | // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/dbgeng/nf-dbgeng-idebugcontrol4-controlledoutputvalistwide#remarks 71 | 72 | if (content.size() <= chunkSize) { 73 | Dml("%s", content.c_str()); 74 | } else { 75 | for (size_t chunkOffset = 0; chunkOffset < content.size(); chunkOffset += chunkSize) { 76 | Dml("%s", content.substr(chunkOffset, chunkSize).c_str()); 77 | } 78 | } 79 | Out("\n"); 80 | } 81 | 82 | 83 | EXT_COMMAND(pyobj, "Prints information about a Python object", "{;s;PyObject address}") 84 | { 85 | ensureSymbolsLoaded(); 86 | 87 | auto offset = evalOffset(GetUnnamedArgStr(0)); 88 | auto pyObj = PyObject::make(offset); 89 | 90 | Out("%s at address: %y\n", pyObj->symbolName().c_str(), pyObj->offset()); 91 | Out("\tRefCount: %s\n", to_string(pyObj->refCount()).c_str()); 92 | Out("\tType: %s\n", pyObj->type().name().c_str()); 93 | 94 | // Print the size if its a PyVarObject. 95 | auto pyVarObj = dynamic_cast(pyObj.get()); 96 | if (pyVarObj != nullptr) 97 | Out("\tSize: %d\n", pyVarObj->size()); 98 | 99 | auto repr = pyObj->repr(true); 100 | if (!repr.empty()) { 101 | Out("\tRepr: "); 102 | printDml(repr); 103 | } 104 | 105 | auto details = pyObj->details(); 106 | if (!details.empty()) { 107 | Out("\tDetails:\n"); 108 | printDml(details); 109 | } 110 | } 111 | 112 | 113 | EXT_COMMAND(pyinterpreterframe, "Prints information about a Python interpreter frame", "{;s;Frame address}") 114 | { 115 | ensureSymbolsLoaded(); 116 | 117 | auto offset = evalOffset(GetUnnamedArgStr(0)); 118 | auto frame = make_unique(RemoteType(offset, "_PyInterpreterFrame")); 119 | 120 | auto details = frame->details(); 121 | Out("\tDetails:\n"); 122 | printDml(details); 123 | } 124 | 125 | 126 | auto EXT_CLASS::ensureSymbolsLoaded() -> void 127 | { 128 | // Hide the massive wall o'text `GetSymbolTypeId` prints when it fails to resolve a symbol. 129 | // We'll print our own if it fails... 130 | ExtCaptureOutputA ignoreOut; 131 | ignoreOut.Start(); 132 | 133 | // See if the symbol can be found. 134 | try { 135 | // If we can construct the AutoInterpreterState, assume symbols are good. 136 | PyInterpreterState::makeAutoInterpreterState(); 137 | return; 138 | } catch (...) { } 139 | 140 | // See if triggering a reload and retrying helps matters. 141 | m_Symbols->Reload("/f python*"); 142 | 143 | // Try again now that symbols are reloaded. 144 | try { 145 | // If we can construct the AutoInterpreterState, assume symbols are good. 146 | PyInterpreterState::makeAutoInterpreterState(); 147 | return; 148 | } 149 | catch (...) { } 150 | 151 | // No luck finding the interpreter state symbol. Print a message to the user. 152 | 153 | ignoreOut.Delete(); 154 | Err("\n\n"); 155 | Err("*************************************************************************\n"); 156 | Err("*** ERROR: Python symbols could not be loaded. ***\n"); 157 | Err("*** Install the debugging symbols for your version of Python##.dll ***\n"); 158 | Err("*** and add them to the symbol path. ***\n"); 159 | Err("*** ***\n"); 160 | Err("*** Alternatively, you run the command !pysymfix to add the Python ***\n"); 161 | Err("*** symbol server created for use with this extension ***\n"); 162 | Err("*************************************************************************\n"); 163 | Err("\n\n"); 164 | // Don't bother throwing. If there really is a symbol issue, it will be caught later on. 165 | } 166 | 167 | 168 | auto EXT_CLASS::evalOffset(const string& arg) -> UINT64 169 | { 170 | // First, see if the arg can be parsed as an expression. 171 | try { 172 | string objExpression = "(void*)("s + arg + ")"s; 173 | ExtRemoteTyped remoteObj(objExpression.c_str()); 174 | return remoteObj.GetPtr(); 175 | } catch (ExtException&) { 176 | // Fall back on evaluating it as a number. 177 | return g_Ext->EvalExprU64(arg.c_str()); 178 | } 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /src/extension.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | 7 | namespace PyExt { 8 | 9 | // engextcpp uses this class as the basis for the entire extension. 10 | // It's instantiated once in globals.cpp and a global pointer to it is held by engextcpp. 11 | class EXT_CLASS : public ExtExtension 12 | { 13 | 14 | public: 15 | explicit EXT_CLASS(); 16 | 17 | public: // Commands. 18 | EXT_COMMAND_METHOD(pyobj); 19 | EXT_COMMAND_METHOD(pystack); 20 | EXT_COMMAND_METHOD(pysymfix); 21 | EXT_COMMAND_METHOD(pysetautointerpreterstate); 22 | EXT_COMMAND_METHOD(pyinterpreterframe); 23 | 24 | public: // Known structs. 25 | auto KnownStructObjectHandler(_In_ PCSTR TypeName, _In_ ULONG Flags, _In_ ULONG64 Offset) -> void; 26 | 27 | private: // Helper methods. 28 | static const size_t chunkSize = 16000; 29 | 30 | /// Prints a DML text, splitting it into chunks if needed. 31 | auto printDml(const std::string& content) -> void; 32 | 33 | /// Evaluates an expression as a pointer and returns the result as an offset in the debuggee's address space. 34 | auto evalOffset(const std::string& arg) -> UINT64; 35 | 36 | /// Prints an error message to the user when Python symbols cannot be loaded. 37 | auto ensureSymbolsLoaded() -> void; 38 | }; 39 | 40 | } -------------------------------------------------------------------------------- /src/fieldAsPyObject.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "PyObject.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace utils { 10 | 11 | // A helper for derived Py*Objects to construct Py*Objects from their fields. 12 | template 13 | auto fieldAsPyObject(ExtRemoteTyped& remoteType, const std::string& fieldName) -> std::unique_ptr 14 | { 15 | if (!remoteType.HasField(fieldName.c_str())) 16 | return {}; 17 | 18 | auto fieldPtr = remoteType.Field(fieldName.c_str()).GetPtr(); 19 | if (fieldPtr == 0) 20 | return {}; 21 | 22 | auto objPtr = PyExt::Remote::PyObject::make(fieldPtr); 23 | auto derivedObjPtr = dynamic_cast(objPtr.get()); 24 | 25 | // If the dynamic_cast worked, transfer ownership. 26 | if (derivedObjPtr != nullptr) 27 | objPtr.release(); 28 | 29 | return std::unique_ptr(derivedObjPtr); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/globals.cpp: -------------------------------------------------------------------------------- 1 | #include "globals.h" 2 | #include "extension.h" 3 | #include "pyextpublic.h" 4 | using namespace PyExt; 5 | 6 | #include 7 | 8 | // Instantiate EngExtCpp's globals. 9 | // This must appear in only one translation unit. 10 | EXT_DECLARE_GLOBALS(); 11 | 12 | namespace PyExt { 13 | 14 | /// Initializes the extension as calls from DbgEng.dll would. 15 | /// Intended to be called by the test harness to set up the global state. 16 | PYEXT_PUBLIC void InitializeGlobalsForTest(IDebugClient* pClient) 17 | { 18 | g_Ext = g_ExtInstancePtr; 19 | g_Ext->Query(pClient); 20 | } 21 | 22 | 23 | /// Intended to be called by the test harness to reset the global state. 24 | PYEXT_PUBLIC void UninitializeGlobalsForTest() 25 | { 26 | g_Ext->Release(); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/init.wdb: -------------------------------------------------------------------------------- 1 | .sympath "srv*c:\symbols*http://pythonsymbols.sdcline.com/symbols;srv*c:\symbols*http://msdl.microsoft.com/download/symbols"; 2 | .reload; 3 | 4 | ~0s; 5 | kv; 6 | $$ .frame 0n22; 7 | $$ dv /t /v; 8 | $$ ??f; 9 | $$ !pyobj f->f_globals; 10 | $$ !pyobj f->f_locals; 11 | 12 | ~*e!pystack; -------------------------------------------------------------------------------- /src/objects/PyBoolObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyBoolObject.h" 2 | 3 | #include "../ExtHelpers.h" 4 | 5 | #include 6 | #include 7 | using namespace std; 8 | 9 | 10 | namespace PyExt::Remote { 11 | 12 | PyBoolObject::PyBoolObject(Offset objectAddress) 13 | : PyObject(objectAddress, "PyBoolObject") 14 | { 15 | } 16 | 17 | 18 | auto PyBoolObject::boolValue() const -> bool 19 | { 20 | auto ival = remoteType().Field("ob_ival"); 21 | return utils::readIntegral(ival); 22 | } 23 | 24 | 25 | auto PyBoolObject::repr(bool /*pretty*/) const -> string 26 | { 27 | return boolValue() ? "True" : "False"; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/objects/PyByteArrayObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyByteArrayObject.h" 2 | #include "PyStringValue.h" 3 | 4 | #include "utils/lossless_cast.h" 5 | #include "../ExtHelpers.h" 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | using namespace std; 13 | 14 | 15 | namespace PyExt::Remote { 16 | 17 | PyByteArrayObject::PyByteArrayObject(Offset objectAddress) 18 | : PyVarObject(objectAddress, "PyByteArrayObject") 19 | { 20 | } 21 | 22 | 23 | auto PyByteArrayObject::stringLength() const -> SSize 24 | { 25 | auto len = size(); 26 | return (len < 0) ? 0 : len; 27 | } 28 | 29 | 30 | auto PyByteArrayObject::stringValue() const -> string 31 | { 32 | auto byteVec = arrayValue(); 33 | return { begin(byteVec), end(byteVec) }; 34 | } 35 | 36 | 37 | auto PyByteArrayObject::arrayValue() const -> std::vector 38 | { 39 | auto len = stringLength(); 40 | if (len <= 0) 41 | return { }; 42 | 43 | vector buff(utils::lossless_cast(len), '\0'); 44 | auto bytesField = remoteType().Field("ob_bytes"); 45 | bytesField.Dereference().ReadBuffer(buff.data(), static_cast(buff.size())); 46 | return buff; 47 | } 48 | 49 | 50 | auto PyByteArrayObject::repr(bool pretty) const -> string 51 | { 52 | string repr = "bytearray(b"s + escapeAndQuoteString(stringValue()) + ")"s; 53 | return pretty ? utils::escapeDml(repr) : repr; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/objects/PyCellObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyCellObject.h" 2 | 3 | #include "PyTypeObject.h" 4 | 5 | #include "../ExtHelpers.h" 6 | 7 | #include 8 | using namespace std; 9 | 10 | 11 | namespace PyExt::Remote { 12 | 13 | PyCellObject::PyCellObject(Offset objectAddress) 14 | : PyObject(objectAddress, "PyCellObject") 15 | { 16 | } 17 | 18 | 19 | auto PyCellObject::objectReference() const -> unique_ptr 20 | { 21 | auto objPtr = remoteType().Field("ob_ref").GetPtr(); 22 | return make(objPtr); 23 | } 24 | 25 | 26 | auto PyCellObject::repr(bool pretty) const -> string 27 | { 28 | string repr = PyObject::repr(pretty); 29 | return repr + ": " + objectReference()->repr(pretty); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/objects/PyCodeObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyCodeObject.h" 2 | 3 | #include "PyStringValue.h" 4 | #include "PyTupleObject.h" 5 | 6 | #include "utils/lossless_cast.h" 7 | #include "../fieldAsPyObject.h" 8 | #include "../ExtHelpers.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | using namespace std; 17 | 18 | 19 | namespace PyExt::Remote { 20 | 21 | PyCodeObject::PyCodeObject(Offset objectAddress) 22 | : PyObject(objectAddress, "PyCodeObject") 23 | { 24 | } 25 | 26 | 27 | auto PyCodeObject::numberOfLocals() const -> int 28 | { 29 | auto nlocals = remoteType().Field("co_nlocals"); 30 | return utils::readIntegral(nlocals); 31 | } 32 | 33 | 34 | auto PyCodeObject::firstLineNumber() const -> int 35 | { 36 | auto firstlineno = remoteType().Field("co_firstlineno"); 37 | return utils::readIntegral(firstlineno); 38 | } 39 | 40 | 41 | auto PyCodeObject::lineNumberFromInstructionOffset(int instruction) const -> int 42 | { 43 | // TODO: Consider caching this table in an ordered container. 44 | auto lnotab = lineNumberTableNew(); 45 | if (lnotab.empty()) { 46 | // Python 3.9 and below 47 | lnotab = lineNumberTableOld(); 48 | if (!lnotab.empty()) { 49 | return lineNumberFromInstructionOffsetOld(instruction, lnotab); 50 | } 51 | } 52 | else { 53 | // Python 3.10 54 | // We have to multiply instruction with the size of a code unit 55 | // https://github.com/python/cpython/blob/v3.10.0/Python/ceval.c#L5485 56 | return lineNumberFromInstructionOffsetNew(instruction * 2, lnotab); 57 | } 58 | 59 | return firstLineNumber(); 60 | } 61 | 62 | 63 | 64 | auto PyCodeObject::lineNumberFromInstructionOffsetOld(int instruction, const vector &lnotab) const -> int 65 | { 66 | // Python 3.9 and below 67 | // This code is explained in the CPython codebase in Objects/lnotab_notes.txt 68 | int lineno = 0; 69 | int addr = 0; 70 | auto last = end(lnotab); 71 | for (auto it = begin(lnotab); it != last; ++it) { 72 | auto addr_incr = *it++; 73 | 74 | if (it == last) { 75 | assert(false && "co_lnotab had an odd number of elements."); 76 | break; //< For now, just return the line number we've calculated so far. 77 | } 78 | 79 | auto line_incr = *it; 80 | 81 | addr += addr_incr; 82 | if (addr > instruction) 83 | break; 84 | 85 | if (line_incr >= 0x80) 86 | lineno -= 0x100; 87 | 88 | lineno += line_incr; 89 | } 90 | 91 | return firstLineNumber() + lineno; 92 | } 93 | 94 | 95 | auto PyCodeObject::lineNumberFromInstructionOffsetNew(int instruction, const vector& linetable) const -> int 96 | { 97 | // Python 3.10 98 | // This code is based on co_lines(), which can be found in the CPython codebase in Objects/lnotab_notes.txt 99 | int line = firstLineNumber(); 100 | int addr = 0; 101 | auto last = end(linetable); 102 | for (auto it = begin(linetable); it != last; ++it) { 103 | auto sdelta = *it++; 104 | 105 | if (it == last) { 106 | assert(false && "co_linetable had an odd number of elements."); 107 | break; //< For now, just return the line number we've calculated so far. 108 | } 109 | 110 | auto ldelta = static_cast(*it); 111 | 112 | addr += sdelta; 113 | if (ldelta == -128) // no line number, treated as delta of zero 114 | ldelta = 0; 115 | line += ldelta; 116 | if (addr > instruction) 117 | break; 118 | } 119 | 120 | return line; 121 | } 122 | 123 | 124 | auto PyCodeObject::lineNumberFromPrevInstruction(int instruction) const -> int 125 | { 126 | // Python 3.11 and above, see Objects/locations.md 127 | auto codeAdaptivePtr = remoteType().Field("co_code_adaptive").GetPointerTo(); 128 | auto firstInstruction = utils::readIntegral(codeAdaptivePtr); 129 | instruction -= firstInstruction; 130 | 131 | int line = firstLineNumber(); 132 | int addr = 0; 133 | auto linetable = lineNumberTableNew(); 134 | auto last = end(linetable); 135 | for (auto it = begin(linetable); it != last;) { 136 | auto byte = *it++; 137 | auto length = (byte & 7) + 1; 138 | addr += length * 2; 139 | auto code = (byte >> 3) & 15; 140 | 141 | if (code <= 9) { 142 | // short form: 2 bytes, no line delta 143 | it++; 144 | } else if (code >= 10 && code <= 12) { 145 | // one line form: 3 bytes, line delta = code - 10 146 | it += 2; 147 | line += code - 10; 148 | } else if (code == 13) { 149 | // no column info 150 | line += readSvarint(it); // start line 151 | } else if (code == 14) { 152 | // long form 153 | line += readSvarint(it); // start line 154 | readVarint(it); // end line 155 | readVarint(it); // start column 156 | readVarint(it); // end column 157 | } else if (code == 15) { 158 | // no location 159 | } else { 160 | assert(false && "unexpected code in co_linetable."); 161 | break; //< For now, just return the line number we've calculated so far. 162 | } 163 | 164 | if (addr > instruction) 165 | break; 166 | } 167 | 168 | return line; 169 | } 170 | 171 | 172 | auto PyCodeObject::varNames() const -> vector 173 | { 174 | return readStringTuple("co_varnames"); 175 | } 176 | 177 | 178 | auto PyCodeObject::freeVars() const -> vector 179 | { 180 | return readStringTuple("co_freevars"); 181 | } 182 | 183 | 184 | auto PyCodeObject::cellVars() const -> vector 185 | { 186 | return readStringTuple("co_cellvars"); 187 | } 188 | 189 | 190 | auto PyCodeObject::localsplusNames() const -> vector 191 | { 192 | return readStringTuple("co_localsplusnames"); 193 | } 194 | 195 | 196 | auto PyCodeObject::filename() const -> string 197 | { 198 | auto filenameStr = utils::fieldAsPyObject(remoteType(), "co_filename"); 199 | if (filenameStr == nullptr) 200 | return { }; 201 | 202 | return filenameStr->stringValue(); 203 | } 204 | 205 | 206 | auto PyCodeObject::name() const -> string 207 | { 208 | auto nameStr = utils::fieldAsPyObject(remoteType(), "co_name"); 209 | if (nameStr == nullptr) 210 | return { }; 211 | 212 | return nameStr->stringValue(); 213 | } 214 | 215 | 216 | auto PyCodeObject::lineNumberTableOld() const -> vector 217 | { 218 | auto codeStr = utils::fieldAsPyObject(remoteType(), "co_lnotab"); 219 | if (codeStr == nullptr) 220 | return { }; 221 | 222 | auto tableString = codeStr->stringValue(); 223 | return vector(begin(tableString), end(tableString)); 224 | } 225 | 226 | auto PyCodeObject::lineNumberTableNew() const -> vector 227 | { 228 | auto codeStr = utils::fieldAsPyObject(remoteType(), "co_linetable"); 229 | if (codeStr == nullptr) 230 | return { }; 231 | 232 | auto tableString = codeStr->stringValue(); 233 | return vector(begin(tableString), end(tableString)); 234 | } 235 | 236 | 237 | auto PyCodeObject::repr(bool pretty) const -> string 238 | { 239 | string repr = ""; 240 | if (pretty) 241 | return utils::link(repr, "!pyobj 0n"s + to_string(offset())); 242 | return repr; 243 | } 244 | 245 | 246 | auto PyCodeObject::readStringTuple(string name) const -> vector 247 | { 248 | auto tuplePtr = utils::fieldAsPyObject(remoteType(), name); 249 | if (tuplePtr == nullptr) 250 | return { }; 251 | 252 | auto count = utils::lossless_cast(tuplePtr->numItems()); 253 | vector values(count); 254 | 255 | for (size_t i = 0; i < count; ++i) { 256 | auto pyVarName = tuplePtr->at(i); 257 | values[i] = pyVarName->repr(); 258 | } 259 | 260 | return values; 261 | } 262 | 263 | 264 | auto PyCodeObject::readVarint(std::vector::const_iterator& it) const -> unsigned int 265 | { 266 | auto ret = 0; 267 | uint8_t byte; 268 | auto shift = 0; 269 | do { 270 | byte = *it++; 271 | ret += (byte & 63) << shift; 272 | shift += 6; 273 | } while ((byte & 64) != 0); 274 | return ret; 275 | } 276 | 277 | 278 | auto PyCodeObject::readSvarint(std::vector::const_iterator& it) const -> int 279 | { 280 | auto varint = readVarint(it); 281 | auto svarint = static_cast(varint >> 1); 282 | if ((varint & 1) != 0) 283 | svarint = -svarint; 284 | return svarint; 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/objects/PyComplexObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyComplexObject.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | using namespace std; 8 | 9 | namespace PyExt::Remote { 10 | 11 | PyComplexObject::PyComplexObject(Offset objectAddress) 12 | : PyObject(objectAddress, "PyComplexObject") 13 | { 14 | } 15 | 16 | 17 | auto PyComplexObject::complexValue() const -> complex 18 | { 19 | auto cval = remoteType().Field("cval"); 20 | return { cval.Field("real").GetDouble(), cval.Field("imag").GetDouble() }; 21 | } 22 | 23 | 24 | auto PyComplexObject::repr(bool /*pretty*/) const -> string 25 | { 26 | auto c = complexValue(); 27 | string operation = (c.imag() < 0) ? "-" : "+"; 28 | return "(" + to_string(c.real()) + operation + to_string(fabs(c.imag())) + "j)"; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/objects/PyDictKeysObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyDictKeysObject.h" 2 | 3 | #include "PyDictObject.h" 4 | #include "../ExtHelpers.h" 5 | 6 | 7 | namespace PyExt::Remote { 8 | 9 | // We use "_dictkeysobject" because "PyDictKeysObject" is not available in Python < 3.11 10 | PyDictKeysObject::PyDictKeysObject(Offset objectAddress) 11 | : RemoteType(objectAddress, "_dictkeysobject") 12 | { 13 | } 14 | 15 | 16 | auto PyDictKeysObject::getEntriesTable() -> ExtRemoteTyped 17 | { 18 | auto obj = remoteType(); 19 | 20 | // Python <= 3.5 stores a pointer to the entries table in `ma_keys->dk_entries`. 21 | if (obj.HasField("dk_entries")) 22 | return obj.Field("dk_entries"); 23 | 24 | // Python >= 3.6 uses a "compact" layout where the entries appear after the `ma_keys->dk_indices` table. 25 | PyObject::SSize size; 26 | if (obj.HasField("dk_size")) { 27 | auto sizeField = obj.Field("dk_size"); 28 | size = utils::readIntegral(sizeField); 29 | } 30 | else { 31 | // Python >= 3.11 stores log2 of size 32 | auto log2sizeField = obj.Field("dk_log2_size"); 33 | auto log2size = utils::readIntegral(log2sizeField); 34 | size = static_cast(1) << log2size; 35 | } 36 | auto pointerSize = utils::getPointerSize(); 37 | 38 | int indexSize = 0; 39 | if (size <= 0xff) { 40 | indexSize = 1; 41 | } 42 | else if (size <= 0xffff) { 43 | indexSize = 2; 44 | } 45 | else if (size <= 0xffffffff) { 46 | indexSize = 4; 47 | } 48 | else { 49 | indexSize = pointerSize; 50 | } 51 | 52 | auto indicies = obj.Field("dk_indices"); // 3.6 and 3.7 both have an indicies field. 53 | ExtRemoteTyped indiciesPtr; 54 | if (indicies.HasField("as_1")) { 55 | // Python 3.6 accesses dk_indicies though a union. 56 | indiciesPtr = indicies.Field("as_1").GetPointerTo(); 57 | } 58 | else { 59 | // Python 3.7 accesses it as a char[]. 60 | indiciesPtr = indicies.GetPointerTo(); 61 | } 62 | 63 | auto entriesPtr = indiciesPtr.GetPtr() + (size * indexSize); 64 | if (obj.HasField("dk_kind")) { // Python >= 3.11 65 | auto dk_kind = obj.Field("dk_kind"); 66 | auto kind = utils::readIntegral(dk_kind); 67 | if (kind != 0) 68 | return ExtRemoteTyped("PyDictUnicodeEntry", entriesPtr, true); 69 | } 70 | return ExtRemoteTyped("PyDictKeyEntry", entriesPtr, true); 71 | } 72 | 73 | 74 | auto PyDictKeysObject::getEntriesTableSize() -> RemoteType::SSize 75 | { 76 | auto obj = remoteType(); 77 | 78 | // Python 3.5 79 | if (!obj.HasField("dk_nentries")) { 80 | auto sizeField = obj.Field("dk_size"); 81 | return utils::readIntegral(sizeField); 82 | } 83 | 84 | // Python >= 3.6 85 | auto numEntriesField = obj.Field("dk_nentries"); 86 | return utils::readIntegral(numEntriesField); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/objects/PyDictObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyDictObject.h" 2 | 3 | #include "../ExtHelpers.h" 4 | #include "PyDictKeysObject.h" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | using namespace std; 16 | 17 | 18 | namespace { 19 | using PyExt::Remote::PyObject; 20 | using PyExt::Remote::PyDictKeysObject; 21 | 22 | auto getEntriesTable(ExtRemoteTyped dictObj) -> ExtRemoteTyped 23 | { 24 | // Python 2 stores a pointer to the entries table right in `ma_table`. 25 | if (dictObj.HasField("ma_table")) 26 | return dictObj.Field("ma_table"); 27 | 28 | // Python 3 adds another layer of abstraction and stores a PyDictKeysObject in `ma_keys`. 29 | auto keys = dictObj.Field("ma_keys"); 30 | auto dictKeysObj = make_unique(keys.GetPtr()); 31 | return dictKeysObj->getEntriesTable(); 32 | } 33 | 34 | 35 | auto getEntriesTableSize(ExtRemoteTyped dictObj) -> PyObject::SSize 36 | { 37 | // Python 2. 38 | if (dictObj.HasField("ma_mask")) { 39 | // Mask is table size (a power of 2) minus 1. 40 | auto mask = dictObj.Field("ma_mask"); 41 | return utils::readIntegral(mask) + 1; 42 | } 43 | 44 | // Python 3. 45 | auto keys = dictObj.Field("ma_keys"); 46 | auto dictKeysObj = make_unique(keys.GetPtr()); 47 | return dictKeysObj->getEntriesTableSize(); 48 | } 49 | 50 | 51 | auto getIsCombined(ExtRemoteTyped dictObj) -> bool 52 | { 53 | // Python 2 tables always act like Python 3 "combined" tables. 54 | if (dictObj.HasField("ma_mask")) { 55 | return true; 56 | } 57 | 58 | // Python 3. 59 | auto valuesPtr = dictObj.Field("ma_values").GetPtr(); 60 | return (valuesPtr == 0); 61 | } 62 | 63 | } 64 | 65 | namespace PyExt::Remote { 66 | 67 | PyDict::~PyDict() 68 | { 69 | } 70 | 71 | 72 | auto PyDict::repr(bool pretty) const -> string 73 | { 74 | const auto elementSeparator = (pretty) ? "\n" : " "; //< Use a newline when pretty-print is on. 75 | const auto indentation = (pretty) ? "\t" : ""; //< Indent only when pretty is on. 76 | 77 | ostringstream oss; 78 | oss << '{' << elementSeparator; 79 | 80 | for (auto& pairValue : pairValues()) { //< TODO: Structured bindings. for (auto&& [key, value] : pairValues) { 81 | auto& key = pairValue.first; 82 | auto& value = pairValue.second; 83 | oss << indentation << key->repr(pretty) << ": " << value->repr(pretty) << ',' << elementSeparator; 84 | } 85 | 86 | oss << '}'; 87 | return oss.str(); 88 | } 89 | 90 | 91 | PyManagedDict::PyManagedDict(RemoteType::Offset keysPtr, RemoteType::Offset valuesPtr) 92 | : keysPtr(keysPtr), valuesPtr(valuesPtr) 93 | { 94 | } 95 | 96 | 97 | auto PyManagedDict::pairValues() const -> vector, unique_ptr>> 98 | { 99 | vector, unique_ptr>> pairs; 100 | 101 | auto keys = make_unique(keysPtr); 102 | auto table = keys->getEntriesTable(); 103 | auto tableSize = keys->getEntriesTableSize(); 104 | auto nextValue = valuesPtr; 105 | auto ptrSize = utils::getPointerSize(); 106 | 107 | for (auto i = 0; i < tableSize; ++i, nextValue += ptrSize) { 108 | auto dictEntry = table.ArrayElement(i); 109 | 110 | auto keyPtr = dictEntry.Field("me_key").GetPtr(); 111 | auto valuePtr = ExtRemoteTyped("(PyObject**)@$extin", nextValue).Dereference().GetPtr(); 112 | 113 | if (keyPtr == 0 || valuePtr == 0) //< The hash bucket might be empty. 114 | continue; 115 | 116 | auto key = PyObject::make(keyPtr); 117 | auto value = PyObject::make(valuePtr); 118 | pairs.push_back(make_pair(move(key), move(value))); 119 | } 120 | 121 | return pairs; 122 | } 123 | 124 | 125 | PyDictObject::PyDictObject(Offset objectAddress) 126 | : PyObject(objectAddress, "PyDictObject") 127 | { 128 | } 129 | 130 | 131 | auto PyDictObject::pairValues() const -> vector, unique_ptr>> 132 | { 133 | vector, unique_ptr>> pairs; 134 | 135 | auto table = getEntriesTable(remoteType()); 136 | const auto tableSize = getEntriesTableSize(remoteType()); 137 | const bool isCombined = getIsCombined(remoteType()); 138 | 139 | optional valuesArray; 140 | if (!isCombined) { 141 | valuesArray = remoteType().Field("ma_values"); 142 | utils::ignoreExtensionError([&] { 143 | // Python >= 3.11 144 | // Find full symbol name because there may be a name collision leading to truncated type info. 145 | valuesArray = ExtRemoteTyped(utils::getFullSymbolName("_dictvalues").c_str(), valuesArray->GetPtr(), true); 146 | valuesArray = valuesArray->Field("values"); 147 | }); 148 | } 149 | 150 | for (SSize i = 0; i < tableSize; ++i) { 151 | auto dictEntry = table.ArrayElement(i); 152 | 153 | Offset keyPtr = dictEntry.Field("me_key").GetPtr(); 154 | Offset valuePtr = 0; 155 | if (isCombined) { 156 | valuePtr = dictEntry.Field("me_value").GetPtr(); 157 | } else { 158 | valuePtr = valuesArray->ArrayElement(i).GetPtr(); 159 | } 160 | 161 | if (keyPtr == 0 || valuePtr == 0) //< The hash bucket might be empty. 162 | continue; 163 | 164 | auto key = PyObject::make(keyPtr); 165 | auto value = PyObject::make(valuePtr); 166 | pairs.push_back(make_pair(move(key), move(value))); 167 | } 168 | 169 | return pairs; 170 | } 171 | 172 | 173 | auto PyDictObject::repr(bool pretty) const -> string 174 | { 175 | return PyDict::repr(pretty); 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /src/objects/PyFloatObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyFloatObject.h" 2 | 3 | #include 4 | #include 5 | using namespace std; 6 | 7 | namespace PyExt::Remote { 8 | 9 | PyFloatObject::PyFloatObject(Offset objectAddress) 10 | : PyObject(objectAddress, "PyFloatObject") 11 | { 12 | } 13 | 14 | 15 | auto PyFloatObject::floatValue() const -> double 16 | { 17 | auto fval = remoteType().Field("ob_fval"); 18 | return fval.GetDouble(); 19 | } 20 | 21 | 22 | auto PyFloatObject::repr(bool /*pretty*/) const -> string 23 | { 24 | return to_string(floatValue()); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/objects/PyFrameObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyFrameObject.h" 2 | 3 | 4 | #include "PyCodeObject.h" 5 | #include "PyDictObject.h" 6 | #include "PyFunctionObject.h" 7 | #include "../fieldAsPyObject.h" 8 | #include "../ExtHelpers.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | using namespace std; 15 | 16 | namespace PyExt::Remote { 17 | 18 | PyFrameObject::PyFrameObject(Offset objectAddress) 19 | : PyVarObject(objectAddress, "_frame") 20 | { 21 | } 22 | 23 | 24 | auto PyFrameObject::locals() const -> unique_ptr 25 | { 26 | // Note: The CPython code comments indicate that this isn't always a dict object. In practice, it seems to be. 27 | return utils::fieldAsPyObject(remoteType(), "f_locals"); 28 | } 29 | 30 | auto PyFrameObject::localsplus() const -> vector>> 31 | { 32 | auto codeObject = code(); 33 | if (codeObject == nullptr) 34 | return {}; 35 | 36 | vector varNames = codeObject->varNames(); 37 | vector cellVars = codeObject->cellVars(); 38 | vector freeVars = codeObject->freeVars(); 39 | auto numLocalsplus = varNames.size() + cellVars.size() + freeVars.size(); 40 | if (numLocalsplus == 0) 41 | return {}; 42 | 43 | vector names; 44 | names.reserve(numLocalsplus); 45 | names.insert(names.end(), varNames.begin(), varNames.end()); 46 | names.insert(names.end(), cellVars.begin(), cellVars.end()); 47 | names.insert(names.end(), freeVars.begin(), freeVars.end()); 48 | 49 | auto f_localsplus = remoteType().Field("f_localsplus"); 50 | auto pyObjAddrs = readOffsetArray(f_localsplus, numLocalsplus); 51 | vector>> localsplus(numLocalsplus); 52 | for (size_t i = 0; i < numLocalsplus; ++i) { 53 | auto addr = pyObjAddrs.at(i); 54 | auto objPtr = addr ? make(addr) : nullptr; 55 | localsplus[i] = make_pair(names.at(i), move(objPtr)); 56 | } 57 | return localsplus; 58 | } 59 | 60 | auto PyFrameObject::globals() const -> unique_ptr 61 | { 62 | return utils::fieldAsPyObject(remoteType(), "f_globals"); 63 | } 64 | 65 | 66 | auto PyFrameObject::code() const -> unique_ptr 67 | { 68 | return utils::fieldAsPyObject(remoteType(), "f_code"); 69 | } 70 | 71 | 72 | auto PyFrameObject::previous() const -> unique_ptr 73 | { 74 | return back(); 75 | } 76 | 77 | 78 | auto PyFrameObject::back() const -> unique_ptr 79 | { 80 | return utils::fieldAsPyObject(remoteType(), "f_back"); 81 | } 82 | 83 | 84 | auto PyFrameObject::trace() const -> unique_ptr 85 | { 86 | return utils::fieldAsPyObject(remoteType(), "f_trace"); 87 | } 88 | 89 | 90 | auto PyFrameObject::lastInstruction() const -> int 91 | { 92 | auto lasti = remoteType().Field("f_lasti"); 93 | return utils::readIntegral(lasti); 94 | } 95 | 96 | 97 | auto PyFrameObject::currentLineNumber() const -> int 98 | { 99 | // When tracing is enabled, we can use the acccurately updated f_lineno field. 100 | auto traceFunction = trace(); 101 | auto codeObject = code(); 102 | if (traceFunction != nullptr || codeObject == nullptr) { 103 | auto lineno = remoteType().Field("f_lineno"); 104 | return utils::readIntegral(lineno); 105 | } 106 | 107 | // Otherwise, we need to do a lookup into the code object's line number table (co_linetable resp. co_lnotab). 108 | return codeObject->lineNumberFromInstructionOffset(lastInstruction()); 109 | } 110 | 111 | 112 | auto PyFrameObject::repr(bool pretty) const -> string 113 | { 114 | string repr = ""; 115 | if (pretty) 116 | return utils::link(repr, "!pyobj 0n"s + to_string(offset())); 117 | return repr; 118 | } 119 | 120 | 121 | auto PyFrameObject::details() const -> string 122 | { 123 | return PyFrame::details(); 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /src/objects/PyFunctionObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyFunctionObject.h" 2 | 3 | #include "PyCodeObject.h" 4 | #include "PyDictObject.h" 5 | #include "PyTupleObject.h" 6 | #include "PyDictObject.h" 7 | #include "PyListObject.h" 8 | #include "PyStringValue.h" 9 | #include "../fieldAsPyObject.h" 10 | #include "../ExtHelpers.h" 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | using namespace std; 17 | 18 | namespace PyExt::Remote { 19 | 20 | PyFunctionObject::PyFunctionObject(Offset objectAddress) 21 | : PyObject(objectAddress, "PyFunctionObject") 22 | { 23 | } 24 | 25 | 26 | auto PyFunctionObject::code() const -> unique_ptr 27 | { 28 | return utils::fieldAsPyObject(remoteType(), "func_code"); 29 | } 30 | 31 | 32 | auto PyFunctionObject::globals() const -> unique_ptr 33 | { 34 | return utils::fieldAsPyObject(remoteType(), "func_globals"); 35 | } 36 | 37 | 38 | auto PyFunctionObject::defaults() const -> unique_ptr 39 | { 40 | return utils::fieldAsPyObject(remoteType(), "func_defaults"); 41 | } 42 | 43 | 44 | auto PyFunctionObject::kwdefaults() const -> unique_ptr 45 | { 46 | return utils::fieldAsPyObject(remoteType(), "func_kwdefaults"); 47 | } 48 | 49 | 50 | auto PyFunctionObject::closure() const -> unique_ptr 51 | { 52 | return utils::fieldAsPyObject(remoteType(), "func_closure"); 53 | } 54 | 55 | 56 | auto PyFunctionObject::doc() const -> unique_ptr 57 | { 58 | return utils::fieldAsPyObject(remoteType(), "func_doc"); 59 | } 60 | 61 | 62 | auto PyFunctionObject::name() const -> unique_ptr 63 | { 64 | return utils::fieldAsPyObject(remoteType(), "func_name"); 65 | } 66 | 67 | 68 | auto PyFunctionObject::dict() const -> unique_ptr 69 | { 70 | return utils::fieldAsPyObject(remoteType(), "func_dict"); 71 | } 72 | 73 | 74 | auto PyFunctionObject::weakreflist() const -> unique_ptr 75 | { 76 | return utils::fieldAsPyObject(remoteType(), "func_weakreflist"); 77 | } 78 | 79 | 80 | auto PyFunctionObject::module() const -> unique_ptr 81 | { 82 | return utils::fieldAsPyObject(remoteType(), "func_module"); 83 | } 84 | 85 | 86 | auto PyFunctionObject::annotations() const -> unique_ptr 87 | { 88 | return utils::fieldAsPyObject(remoteType(), "func_annotations"); 89 | } 90 | 91 | 92 | auto PyFunctionObject::qualname() const -> unique_ptr 93 | { 94 | return utils::fieldAsPyObject(remoteType(), "func_qualname"); 95 | } 96 | 97 | 98 | auto PyFunctionObject::repr(bool pretty) const -> string 99 | { 100 | auto nameObject = name(); 101 | string repr; 102 | if (nameObject == nullptr) 103 | repr = ""; 104 | else 105 | repr = "stringValue() + ">"; 106 | if (pretty) 107 | return utils::link(repr, "!pyobj 0n"s + to_string(offset())); 108 | return repr; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/objects/PyIntObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyIntObject.h" 2 | 3 | #include "../ExtHelpers.h" 4 | 5 | #include 6 | #include 7 | using namespace std; 8 | 9 | namespace PyExt::Remote { 10 | 11 | PyIntObject::PyIntObject(Offset objectAddress) 12 | : PyObject(objectAddress, "PyIntObject") 13 | { 14 | } 15 | 16 | 17 | auto PyIntObject::intValue() const -> int32_t 18 | { 19 | auto ival = remoteType().Field("ob_ival"); 20 | return utils::readIntegral(ival); 21 | } 22 | 23 | 24 | auto PyIntObject::repr(bool /*pretty*/) const -> string 25 | { 26 | return to_string(intValue()); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/objects/PyListObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyListObject.h" 2 | 3 | #include "utils/lossless_cast.h" 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | using namespace std; 14 | 15 | namespace PyExt::Remote { 16 | 17 | PyListObject::PyListObject(Offset objectAddress) 18 | : PyVarObject(objectAddress, "PyListObject") 19 | { 20 | } 21 | 22 | 23 | auto PyListObject::numItems() const -> SSize 24 | { 25 | return PyVarObject::size(); 26 | } 27 | 28 | 29 | auto PyListObject::at(SSize index) const -> unique_ptr 30 | { 31 | if (index < 0 || index >= numItems()) 32 | throw out_of_range("PyListObject::at index out of range."); 33 | 34 | auto obj = remoteType(); 35 | auto itemPtr = obj.Field("ob_item").ArrayElement(index).GetPtr(); 36 | return PyObject::make(itemPtr); 37 | } 38 | 39 | 40 | auto PyListObject::listValue() const -> vector> 41 | { 42 | auto count = utils::lossless_cast(numItems()); 43 | vector> values(count); 44 | 45 | for (size_t i = 0; i < count; ++i) { 46 | values[i] = at(i); 47 | } 48 | 49 | return values; 50 | } 51 | 52 | 53 | auto PyListObject::repr(bool pretty) const -> string 54 | { 55 | ostringstream oss; 56 | oss << "[ "; 57 | 58 | auto count = numItems(); 59 | for (SSize i = 0; i < count; ++i) { 60 | auto elem = at(i); 61 | if (elem != nullptr) 62 | oss << elem->repr(pretty); 63 | 64 | if (i + 1 < count) 65 | oss << ", "; 66 | } 67 | 68 | oss << " ]"; 69 | return oss.str(); 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /src/objects/PyLongObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyLongObject.h" 2 | 3 | #include "PyVarObject.h" 4 | 5 | #include "../ExtHelpers.h" 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | using namespace std; 14 | 15 | namespace PyExt::Remote { 16 | 17 | PyLongObject::PyLongObject(Offset objectAddress, const bool isBool) 18 | : PyObject(objectAddress, "PyLongObject"), isBool(isBool) 19 | { 20 | } 21 | 22 | 23 | auto PyLongObject::repr(bool /*pretty*/) const -> string 24 | { 25 | ExtRemoteTyped digits; 26 | SSize digitCount; 27 | SSize sign; 28 | auto isCompact = false; 29 | 30 | if (remoteType().HasField("long_value")) { 31 | // Python 3.12+ 32 | auto longValue = remoteType().Field("long_value"); 33 | digits = longValue.Field("ob_digit"); 34 | 35 | auto tagRaw = longValue.Field("lv_tag"); 36 | auto tag = utils::readIntegral(tagRaw); 37 | digitCount = tag >> 3; // _PyLong_NON_SIZE_BITS 38 | sign = 1 - (tag & 3); // _PyLong_SIGN_MASK 39 | isCompact = tag < (2 << 3); // _PyLong_NON_SIZE_BITS 40 | } else { 41 | digits = remoteType().Field("ob_digit"); 42 | auto varObject = PyVarObject(offset()); 43 | sign = varObject.size(); 44 | digitCount = abs(sign); 45 | } 46 | 47 | auto firstDigit = digits.ArrayElement(0); 48 | auto firstDigitValue = utils::readIntegral(firstDigit); 49 | 50 | if (isBool) 51 | return firstDigitValue == 1 ? "True"s : "False"s; 52 | 53 | if (isCompact) 54 | return to_string(sign * (SSize)firstDigitValue); 55 | 56 | const auto bytesPerDigit = firstDigit.GetTypeSize(); 57 | 58 | // Set up our constants based on how CPython was compiled. 59 | // See: https://github.com/python/cpython/blob/master/Include/longintrepr.h 60 | int64_t SHIFT = 0, DECIMAL_BASE = 0, DECIMAL_SHIFT = 0; 61 | if (bytesPerDigit == 4) { 62 | SHIFT = 30; 63 | DECIMAL_SHIFT = 9; 64 | DECIMAL_BASE = 1000000000; 65 | } else if (bytesPerDigit == 2) { 66 | SHIFT = 15; 67 | DECIMAL_SHIFT = 4; 68 | DECIMAL_BASE = 10000; 69 | } else { 70 | throw runtime_error("Unexpected PyLong bytes per digit of " + to_string(bytesPerDigit)); 71 | } 72 | //const auto BASE = static_cast(1) << SHIFT; 73 | //const auto MASK = BASE - 1; 74 | 75 | // Convert from BASE to DECIMAL_BASE and store the result in `buff`. 76 | vector buff; 77 | for (int64_t i = digitCount - 1; i >= 0; --i) { 78 | auto hiElement = digits.ArrayElement(i); 79 | auto hi = utils::readIntegral(hiElement); 80 | for (auto& buffDigit : buff) { 81 | uint64_t z = buffDigit << SHIFT | hi; 82 | hi = z / DECIMAL_BASE; 83 | buffDigit = z - hi * DECIMAL_BASE; 84 | } 85 | 86 | while (hi) { 87 | buff.push_back(hi % DECIMAL_BASE); 88 | hi /= DECIMAL_BASE; 89 | } 90 | } 91 | 92 | // If the buffer is empty, use 0. 93 | if (buff.empty()) 94 | buff.push_back(0); 95 | 96 | // Convert `buff` from DECIMAL_BASE to base 10 and store the characters in `out`. 97 | string out; 98 | for (size_t i = 0; i < buff.size() - 1; i++) { 99 | auto rem = buff[i]; 100 | for (int64_t j = 0; j < DECIMAL_SHIFT; ++j) { 101 | out.push_back('0' + rem % 10); 102 | rem /= 10; 103 | } 104 | } 105 | 106 | // Consume the last digit. 107 | auto rem = buff.back(); 108 | do { 109 | out.push_back('0' + rem % 10); 110 | rem /= 10; 111 | } while (rem != 0); 112 | 113 | // Append a negative sign if needed. 114 | if (sign < 0) 115 | out.push_back('-'); 116 | 117 | reverse(out.begin(), out.end()); 118 | return out; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/objects/PyNoneObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyNoneObject.h" 2 | 3 | #include 4 | #include 5 | using namespace std; 6 | 7 | namespace PyExt::Remote { 8 | 9 | PyNoneObject::PyNoneObject(Offset objectAddress) 10 | : PyObject(objectAddress) 11 | { 12 | } 13 | 14 | 15 | auto PyNoneObject::repr(bool /*pretty*/) const -> string 16 | { 17 | return "None"; 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/objects/PyNotImplementedObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyNotImplementedObject.h" 2 | 3 | #include 4 | #include 5 | using namespace std; 6 | 7 | namespace PyExt::Remote { 8 | 9 | PyNotImplementedObject::PyNotImplementedObject(Offset objectAddress) 10 | : PyObject(objectAddress) 11 | { 12 | } 13 | 14 | 15 | auto PyNotImplementedObject::repr(bool /*pretty*/) const -> string 16 | { 17 | return "NotImplemented"; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/objects/PyObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyObject.h" 2 | 3 | #include "PyTypeObject.h" 4 | #include "PyVarObject.h" 5 | #include "PyDictObject.h" 6 | #include "PyDictKeysObject.h" 7 | #include "../ExtHelpers.h" 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | using namespace std; 17 | 18 | namespace PyExt::Remote { 19 | 20 | PyObject::PyObject(Offset objectAddress, const string& symbolName /*= "PyObject"*/) 21 | : RemoteType(objectAddress, symbolName) 22 | { 23 | } 24 | 25 | 26 | PyObject::~PyObject() 27 | { 28 | } 29 | 30 | 31 | auto PyObject::refCount() const -> SSize 32 | { 33 | auto refcnt = baseField("ob_refcnt"); 34 | return utils::readIntegral(refcnt); 35 | } 36 | 37 | 38 | auto PyObject::type() const -> PyTypeObject 39 | { 40 | return PyTypeObject(baseField("ob_type").GetPtr()); 41 | } 42 | 43 | 44 | auto PyObject::slots() const -> vector>> 45 | { 46 | vector>> slots; 47 | 48 | // slots of the type itself and all parents 49 | for (auto const& typeObj : type().mro()->listValue()) { 50 | auto members = PyTypeObject(typeObj->offset()).members(); 51 | for (auto const& memberDef : members) { 52 | // TODO: handle other types than T_OBJECT_EX 53 | if (memberDef->type() == PyMemberDef::T_OBJECT_EX) { 54 | auto objPtr = ExtRemoteTyped("(PyObject**)@$extin", offset() + memberDef->offset()); 55 | auto addr = objPtr.Dereference().GetPtr(); 56 | auto value = addr ? make(objPtr.Dereference().GetPtr()) : nullptr; 57 | slots.push_back(make_pair(memberDef->name(), move(value))); 58 | } 59 | } 60 | } 61 | return slots; 62 | } 63 | 64 | 65 | auto PyObject::managedDict() const -> unique_ptr 66 | { 67 | // Python >= 3.11, see PyObject_GenericGetDict 68 | if (!type().isManagedDict()) 69 | return { }; 70 | 71 | auto pointerSize = utils::getPointerSize(); 72 | 73 | Offset dictPtr = 0; 74 | utils::ignoreExtensionError([&] { 75 | // Python >= 3.13 76 | auto managedDictPtr = offset() - 3 * pointerSize; 77 | dictPtr = ExtRemoteTyped("PyManagedDictPointer", managedDictPtr, true).Field("dict").GetPtr(); 78 | }); 79 | if (dictPtr) 80 | return make_unique(dictPtr); 81 | 82 | Offset valuesPtr; 83 | if (type().hasInlineValues()) { 84 | // Python >= 3.13 85 | auto dictValues = ExtRemoteTyped("(_dictvalues*)((PyObject*)(@$extin)+1)", offset()); 86 | valuesPtr = dictValues.Field("values").GetPtr(); 87 | } else { 88 | optional dictOrValues; 89 | utils::ignoreExtensionError([&] { 90 | // Python 3.12 91 | auto dictOrValuesPtr = offset() - 3 * pointerSize; 92 | dictOrValues = ExtRemoteTyped("PyDictOrValues", dictOrValuesPtr, true); 93 | }); 94 | 95 | if (dictOrValues) { 96 | valuesPtr = dictOrValues->Field("values").GetPtr(); 97 | if (valuesPtr & 1) { 98 | valuesPtr += 1; 99 | } else { 100 | auto dictPtr = dictOrValues->Field("dict").GetPtr(); 101 | return dictPtr ? make_unique(dictPtr) : nullptr; 102 | } 103 | } else { 104 | auto valuesPtrPtr = offset() - 4 * pointerSize; 105 | valuesPtr = ExtRemoteTyped("(PyObject***)@$extin", valuesPtrPtr).Dereference().GetPtr(); 106 | if (valuesPtr == 0) { 107 | auto dictPtr = ExtRemoteTyped("(PyDictObject**)@$extin", valuesPtrPtr + pointerSize).Dereference().GetPtr(); 108 | return dictPtr ? make_unique(dictPtr) : nullptr; 109 | } 110 | } 111 | } 112 | 113 | auto ht = ExtRemoteTyped("PyHeapTypeObject", type().offset(), true); 114 | auto cachedKeys = ht.Field("ht_cached_keys"); 115 | return make_unique(cachedKeys.GetPtr(), valuesPtr); 116 | } 117 | 118 | 119 | auto PyObject::dict() const -> unique_ptr 120 | { 121 | auto managedDict_ = managedDict(); 122 | if (managedDict_ != nullptr) 123 | return managedDict_; 124 | 125 | // see https://docs.python.org/3.10/c-api/typeobj.html#c.PyTypeObject.tp_dictoffset 126 | auto dictOffset_ = type().dictOffset(); 127 | if (dictOffset_ == 0) 128 | return nullptr; 129 | if (dictOffset_ < 0) { 130 | auto thisAsVar = PyVarObject(this->offset()); 131 | auto obSize = thisAsVar.size(); 132 | if (obSize < 0) 133 | obSize = -obSize; 134 | dictOffset_ += type().basicSize() + obSize * type().itemSize(); 135 | // alignment 136 | auto ptrSize = utils::getPointerSize(); 137 | dictOffset_ = (dictOffset_ + (ptrSize - 1)) & ~(ptrSize - 1); 138 | } 139 | auto dictPtr = ExtRemoteTyped("(PyDictObject**)@$extin", offset() + dictOffset_); 140 | auto dictAddr = dictPtr.Dereference().GetPtr(); 141 | return dictAddr ? make_unique(dictAddr) : nullptr; 142 | } 143 | 144 | 145 | auto PyObject::repr(bool pretty) const -> string 146 | { 147 | string repr = "<" + type().name() + " object>"; 148 | if (pretty) 149 | return utils::link(repr, "!pyobj 0n"s + to_string(offset())); 150 | return repr; 151 | } 152 | 153 | 154 | auto PyObject::details() const -> string 155 | { 156 | const auto sectionSeparator = "\n"; 157 | const auto elementSeparator = "\n"; 158 | const auto indentation = "\t"; 159 | ostringstream oss; 160 | bool empty = true; 161 | 162 | // repr of built-in base types (not for built-in types itself) 163 | auto const& types = PyTypeObject::builtinTypes(); 164 | if (std::find(begin(types), end(types), type().name()) == types.end()) { 165 | for (auto const& typeObj : type().mro()->listValue()) { 166 | auto const typeName = PyTypeObject(typeObj->offset()).name(); 167 | if (find(begin(types), end(types), typeName) != end(types)) { 168 | if (!empty) 169 | oss << sectionSeparator; 170 | else 171 | empty = false; 172 | oss << typeName << " repr: " << make(offset(), typeName)->repr(); 173 | } 174 | } 175 | } 176 | 177 | // __slots__ (including base types) 178 | auto slots_ = slots(); 179 | if (slots_.size() > 0) { 180 | if (!empty) 181 | oss << sectionSeparator; 182 | else 183 | empty = false; 184 | oss << "slots: {" << elementSeparator; 185 | for (auto const& pairValue : slots()) { 186 | auto const& name = pairValue.first; 187 | auto const& value = pairValue.second; 188 | if (value != nullptr) 189 | oss << indentation << name << ": " << value->repr(true) << ',' << elementSeparator; 190 | } 191 | oss << '}'; 192 | } 193 | 194 | // __dict__ 195 | auto dictPtr = dict(); 196 | if (dictPtr != nullptr) { 197 | if (!empty) 198 | oss << sectionSeparator; 199 | else 200 | empty = false; 201 | oss << "dict: " << dictPtr->repr(true); 202 | } 203 | 204 | return oss.str(); 205 | } 206 | 207 | 208 | auto PyObject::baseField(const string & fieldName) const -> ExtRemoteTyped 209 | { 210 | ExtRemoteTyped obj = remoteType(); 211 | 212 | // Python3 tucks the base members away in a struct named ob_base. 213 | while (obj.HasField("ob_base") && !obj.HasField(fieldName.c_str())) { 214 | // Drill down into the ob_base member until we hit the end. 215 | obj = obj.Field("ob_base"); 216 | } 217 | 218 | return obj.Field(fieldName.c_str()); 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /src/objects/PyObjectMake.cpp: -------------------------------------------------------------------------------- 1 | #include "PyObject.h" 2 | #include "PyVarObject.h" 3 | #include "PyTypeObject.h" 4 | #include "PyStringObject.h" 5 | #include "PyUnicodeObject.h" 6 | #include "PyByteArrayObject.h" 7 | #include "PyListObject.h" 8 | #include "PyTupleObject.h" 9 | #include "PySetObject.h" 10 | #include "PyDictObject.h" 11 | #include "PyIntObject.h" 12 | #include "PyLongObject.h" 13 | #include "PyFloatObject.h" 14 | #include "PyBoolObject.h" 15 | #include "PyComplexObject.h" 16 | #include "PyFrameObject.h" 17 | #include "PyCodeObject.h" 18 | #include "PyFunctionObject.h" 19 | #include "PyCellObject.h" 20 | #include "PyNoneObject.h" 21 | #include "PyNotImplementedObject.h" 22 | 23 | #include 24 | using namespace std; 25 | 26 | namespace PyExt::Remote { 27 | 28 | auto PyObject::make(PyObject::Offset remoteAddress) -> unique_ptr 29 | { 30 | // Get the type of this object. 31 | const auto typeObj = PyObject(remoteAddress).type(); 32 | const auto typeName = typeObj.name(); 33 | 34 | return make(remoteAddress, typeName); 35 | } 36 | 37 | 38 | auto PyObject::make(PyObject::Offset remoteAddress, const std::string& typeName) -> unique_ptr 39 | { 40 | // TODO: Turn this into a map to factory functions. 41 | if (typeName == "type") { 42 | return make_unique(remoteAddress); 43 | } else if (typeName == "str") { 44 | const auto typeObj = PyObject(remoteAddress).type(); 45 | if (typeObj.isPython2()) { 46 | return make_unique(remoteAddress); 47 | } else { 48 | return make_unique(remoteAddress); 49 | } 50 | } else if (typeName == "bytes") { 51 | return make_unique(remoteAddress); 52 | } else if (typeName == "bytearray") { 53 | return make_unique(remoteAddress); 54 | } else if (typeName == "list") { 55 | return make_unique(remoteAddress); 56 | } else if (typeName == "tuple") { 57 | return make_unique(remoteAddress); 58 | } else if (typeName == "set") { 59 | return make_unique(remoteAddress); 60 | } else if (typeName == "dict") { 61 | return make_unique(remoteAddress); 62 | } else if (typeName == "int") { 63 | const auto typeObj = PyObject(remoteAddress).type(); 64 | if (typeObj.isPython2()) { 65 | return make_unique(remoteAddress); 66 | } else { 67 | return make_unique(remoteAddress); 68 | } 69 | } else if (typeName == "long") { 70 | return make_unique(remoteAddress); 71 | } else if (typeName == "float") { 72 | return make_unique(remoteAddress); 73 | } else if (typeName == "bool") { 74 | const auto typeObj = PyObject(remoteAddress).type(); 75 | if (typeObj.isPython2()) { 76 | return make_unique(remoteAddress); 77 | } else { 78 | return make_unique(remoteAddress, true); 79 | } 80 | } else if (typeName == "complex") { 81 | return make_unique(remoteAddress); 82 | } else if (typeName == "frame") { 83 | return make_unique(remoteAddress); 84 | } else if (typeName == "code") { 85 | return make_unique(remoteAddress); 86 | } else if (typeName == "function") { 87 | return make_unique(remoteAddress); 88 | } else if (typeName == "cell") { 89 | return make_unique(remoteAddress); 90 | } else if (typeName == "NoneType") { 91 | return make_unique(remoteAddress); 92 | } else if (typeName == "NotImplementedType") { 93 | return make_unique(remoteAddress); 94 | } else { 95 | return make_unique(remoteAddress); 96 | } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/objects/PySetObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PySetObject.h" 2 | 3 | #include "../ExtHelpers.h" 4 | 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | using namespace std; 15 | 16 | namespace { 17 | PyExt::Remote::PyObject::Offset getDummyPtr() 18 | { 19 | // Dummy is a special pointer used to represent a removed element, 20 | // distinguished from a never-inserted, NULL element. 21 | try { 22 | // Python 3.4+ exports the dummy with a helpful name. 23 | return (ExtRemoteTyped("_PySet_Dummy").GetPtr()); 24 | } catch (ExtRemoteException&) { /*...*/ } 25 | 26 | try { 27 | // Python 3.3 exports a single python##!dummy symbol we can use. 28 | return (ExtRemoteTyped("!dummy").GetPtr()); 29 | } catch (ExtRemoteException&) { /*...*/ } 30 | 31 | // Python 2.7 and earlier have `static PyObject *dummy` variables in multiple translation units. 32 | // We have no easy way of knowing which is used by PySetObject. 33 | // As it is, Python 2.7 dumps will display b'' for removed items. 34 | return 0; //< TODO: Treat all python*!dummy symbols as dummies. 35 | } 36 | } 37 | 38 | 39 | namespace PyExt::Remote { 40 | 41 | PySetObject::PySetObject(Offset objectAddress) 42 | : PyObject(objectAddress, "PySetObject") 43 | { 44 | } 45 | 46 | 47 | auto PySetObject::numItems() const -> SSize 48 | { 49 | auto usedField = remoteType().Field("used"); 50 | return utils::readIntegral(usedField); 51 | } 52 | 53 | 54 | auto PySetObject::listValue() const -> vector> 55 | { 56 | vector> values; 57 | 58 | auto maskField = remoteType().Field("mask"); 59 | auto tableSize = utils::readIntegral(maskField) + 1; 60 | 61 | // Dummie entries are used as placeholders for removed elements. 62 | auto dummyPtr = getDummyPtr(); 63 | 64 | for (SSize i = 0; i < tableSize; ++i) { 65 | auto entry = remoteType().Field("table").ArrayElement(i); 66 | auto keyPtr = entry.Field("key").GetPtr(); 67 | if (keyPtr == 0 || keyPtr == dummyPtr) 68 | continue; 69 | 70 | auto key = PyObject::make(keyPtr); 71 | values.push_back(move(key)); 72 | } 73 | 74 | return values; 75 | } 76 | 77 | 78 | auto PySetObject::repr(bool pretty) const -> string 79 | { 80 | const auto elementSeparator = (pretty) ? "\n" : " "; //< Use a newline when pretty-print is on. 81 | const auto indentation = (pretty) ? "\t" : ""; //< Indent only when pretty is on. 82 | 83 | ostringstream oss; 84 | oss << '{' << elementSeparator; 85 | 86 | for (auto& value : listValue()) { 87 | oss << indentation << value->repr(pretty) << ',' << elementSeparator; 88 | } 89 | 90 | oss << elementSeparator << '}'; 91 | return oss.str(); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/objects/PyStringObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyStringObject.h" 2 | 3 | #include "utils/lossless_cast.h" 4 | #include "../ExtHelpers.h" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | using namespace std; 11 | 12 | namespace PyExt::Remote { 13 | 14 | PyBaseStringObject::PyBaseStringObject(Offset objectAddress, const std::string& typeName) 15 | : PyVarObject(objectAddress, typeName) 16 | { 17 | } 18 | 19 | 20 | auto PyBaseStringObject::stringLength() const -> SSize 21 | { 22 | auto len = size(); 23 | 24 | // Don't let the length go negative. Python enforces this invariant. 25 | if (len < 0) 26 | throw runtime_error("Negative size in PyBaseStringObject."); 27 | 28 | return len; 29 | } 30 | 31 | 32 | auto PyBaseStringObject::stringValue() const -> string 33 | { 34 | // String and Bytes objects in Python can embed \0's so we read `len` bytes from the debuggee 35 | // instead of stopping at the first \0. 36 | const auto len = stringLength(); 37 | if (len == 0) 38 | return ""s; 39 | 40 | string buffer(utils::lossless_cast(len), '\0'); 41 | auto sval = remoteType().Field("ob_sval"); 42 | sval.Dereference().ReadBuffer(buffer.data(), utils::lossless_cast(buffer.size())); 43 | return buffer; 44 | } 45 | 46 | 47 | auto PyBaseStringObject::repr(bool pretty) const -> string 48 | { 49 | string repr = escapeAndQuoteString(stringValue()); 50 | return pretty ? utils::escapeDml(repr) : repr; 51 | } 52 | 53 | 54 | 55 | PyBytesObject::PyBytesObject(Offset objectAddress) 56 | : PyBaseStringObject(objectAddress, "PyBytesObject") 57 | { 58 | } 59 | 60 | 61 | auto PyBytesObject::repr(bool pretty) const -> string 62 | { 63 | return "b" + PyBaseStringObject::repr(pretty); 64 | } 65 | 66 | 67 | PyStringObject::PyStringObject(Offset objectAddress) 68 | : PyBaseStringObject(objectAddress, "PyStringObject") 69 | { 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/objects/PyStringValue.cpp: -------------------------------------------------------------------------------- 1 | #include "PyStringValue.h" 2 | 3 | #include 4 | using namespace std; 5 | 6 | namespace { 7 | 8 | constexpr auto hexAlphabet = "0123456789ABCDEF"; 9 | 10 | auto charToHex(char c) -> string { 11 | string s(2, '\0'); 12 | s[0] = hexAlphabet[(c & 0x0F)]; 13 | s[1] = hexAlphabet[(c >> 4) & 0x0F]; 14 | return s; 15 | } 16 | 17 | 18 | auto escapeCharacter(char c) -> string { 19 | // Return known escape sequences. 20 | switch (c) { 21 | default: break; 22 | case '\\': return "\\\\"; //< Backslash(\) 23 | case '\'': return "\\'"; //< Single quote (') 24 | case '"': return "\\\""; //< Double quote (") 25 | case '\a': return "\\a"; //< ASCII Bell(BEL) 26 | case '\b': return "\\b"; //< ASCII Backspace(BS) 27 | case '\f': return "\\f"; //< ASCII Formfeed(FF) 28 | case '\n': return "\\n"; //< ASCII Linefeed(LF) 29 | case '\r': return "\\r"; //< ASCII Carriage Return(CR) 30 | case '\t': return "\\t"; //< ASCII Horizontal Tab(TAB) 31 | case '\v': return "\\v"; //< ASCII Vertical Tab(VT) 32 | } 33 | 34 | // Escape nonprintalbles with a hex code. 35 | if (!isprint(static_cast(c))) { 36 | return "\\x"s + charToHex(c); 37 | } 38 | 39 | // Otherwise, it doesn't need escaped. 40 | return { c }; 41 | } 42 | 43 | } 44 | 45 | namespace PyExt::Remote { 46 | 47 | auto PyStringValue::escapeAndQuoteString(const string& in, QuoteType quoteType) -> string 48 | { 49 | string out; 50 | out.reserve(in.length() + 2); 51 | 52 | out += (quoteType == QuoteType::Double) ? '"' : '\''; 53 | for (auto c : in) { 54 | if ((quoteType == QuoteType::Single && c == '"') 55 | || (quoteType == QuoteType::Double && c == '\'')) 56 | { 57 | out += c; //< Don't escape the other kind of quote. 58 | } else { 59 | out += escapeCharacter(c); 60 | } 61 | } 62 | out += (quoteType == QuoteType::Double) ? '"' : '\''; 63 | 64 | return out; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/objects/PyTupleObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyTupleObject.h" 2 | 3 | #include "utils/lossless_cast.h" 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | using namespace std; 14 | 15 | namespace PyExt::Remote { 16 | 17 | PyTupleObject::PyTupleObject(Offset objectAddress) 18 | : PyVarObject(objectAddress, "PyTupleObject") 19 | { 20 | } 21 | 22 | 23 | auto PyTupleObject::numItems() const -> SSize 24 | { 25 | return PyVarObject::size(); 26 | } 27 | 28 | 29 | auto PyTupleObject::at(SSize index) const -> unique_ptr 30 | { 31 | if (index < 0 || index >= numItems()) 32 | throw out_of_range("PyListObject::at index out of range."); 33 | 34 | auto obj = remoteType(); 35 | auto itemPtr = obj.Field("ob_item").ArrayElement(index).GetPtr(); 36 | return PyObject::make(itemPtr); 37 | } 38 | 39 | 40 | auto PyTupleObject::listValue() const -> vector> 41 | { 42 | auto count = utils::lossless_cast(numItems()); 43 | vector> values(count); 44 | 45 | for (size_t i = 0; i < count; ++i) { 46 | values[i] = at(i); 47 | } 48 | 49 | return values; 50 | } 51 | 52 | 53 | auto PyTupleObject::repr(bool pretty) const -> string 54 | { 55 | ostringstream oss; 56 | oss << "("; 57 | 58 | auto count = numItems(); 59 | for (SSize i = 0; i < count; ++i) { 60 | auto elem = at(i); 61 | if (elem != nullptr) 62 | oss << elem->repr(pretty); 63 | 64 | if (i + 1 < count) 65 | oss << ", "; 66 | } 67 | 68 | oss << ")"; 69 | return oss.str(); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/objects/PyTypeObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyTypeObject.h" 2 | 3 | #include "../fieldAsPyObject.h" 4 | #include "../ExtHelpers.h" 5 | 6 | #include 7 | #include 8 | using namespace std; 9 | 10 | namespace PyExt::Remote { 11 | auto PyTypeObject::builtinTypes() -> const std::vector& 12 | { 13 | static const std::vector types { 14 | "type", 15 | "str", 16 | "bytes", 17 | "bytearray", 18 | "tuple", 19 | "set", 20 | "dict", 21 | "int", 22 | "long", 23 | "float", 24 | "bool", 25 | "complex", 26 | "frame", 27 | "code", 28 | "function", 29 | "cell", 30 | "NoneType", 31 | "NotImplementedType", 32 | }; 33 | 34 | return types; 35 | } 36 | 37 | PyTypeObject::PyTypeObject(Offset objectAddress) 38 | : PyVarObject(objectAddress, "PyTypeObject") 39 | { 40 | } 41 | 42 | 43 | auto PyTypeObject::name() const -> string 44 | { 45 | ExtBuffer buff; 46 | remoteType().Field("tp_name").Dereference().GetString(&buff); 47 | return buff.GetBuffer(); 48 | } 49 | 50 | 51 | auto PyTypeObject::basicSize() const -> SSize 52 | { 53 | auto basicSize = remoteType().Field("tp_basicsize"); 54 | return utils::readIntegral(basicSize); 55 | } 56 | 57 | 58 | auto PyTypeObject::itemSize() const -> SSize 59 | { 60 | auto itemSize = remoteType().Field("tp_itemsize"); 61 | return utils::readIntegral(itemSize); 62 | } 63 | 64 | 65 | auto PyTypeObject::documentation() const -> string 66 | { 67 | ExtBuffer buff; 68 | auto doc = remoteType().Field("ob_type").Field("tp_doc"); 69 | if (doc.GetPtr() == 0) 70 | return {}; 71 | 72 | doc.Dereference().GetString(&buff); 73 | return buff.GetBuffer(); 74 | } 75 | 76 | 77 | auto PyTypeObject::members() const -> vector> 78 | { 79 | vector> members; 80 | auto tp_members = remoteType().Field("tp_members"); 81 | auto ptr = tp_members.GetPtr(); 82 | if (ptr != 0) { 83 | auto isKnownType = false; 84 | utils::ignoreExtensionError([&] { 85 | tp_members = ExtRemoteTyped("PyMemberDef", ptr, true); // necessary for Python >= 3.8 86 | isKnownType = true; 87 | }); 88 | if (isKnownType) { 89 | for (int i = 0; ; ++i) { 90 | auto elem = tp_members.ArrayElement(i); 91 | if (elem.Field("name").GetPtr() == 0) 92 | break; 93 | members.push_back(make_unique(elem.GetPointerTo().GetPtr())); 94 | } 95 | } else { 96 | // The symbol "PyMemberDef" is not available in Python 3.11. 97 | // (In Python 3.12 it is available again...) 98 | auto ptrSize = utils::getPointerSize(); 99 | auto memberDefSize = 5 * ptrSize; 100 | while (ExtRemoteTyped("(void**)@$extin", ptr).Dereference().GetPtr() != 0) { 101 | members.push_back(make_unique(ptr)); 102 | ptr += memberDefSize; 103 | } 104 | } 105 | } 106 | return members; 107 | } 108 | 109 | 110 | auto PyTypeObject::isManagedDict() const -> bool 111 | { 112 | if (isPython2()) // in Python 2 this bit has another meaning 113 | return false; 114 | auto flags_ = remoteType().Field("tp_flags"); 115 | auto flags = utils::readIntegral(flags_); 116 | return flags & (1 << 4); // Py_TPFLAGS_MANAGED_DICT 117 | } 118 | 119 | 120 | auto PyTypeObject::getStaticBuiltinIndex() const -> SSize 121 | { 122 | if (isPython2()) 123 | return -1; 124 | auto type = remoteType(); 125 | auto flagsRaw = type.Field("tp_flags"); 126 | auto flags = utils::readIntegral(flagsRaw); 127 | if (flags & (1 << 1)) { // _Py_TPFLAGS_STATIC_BUILTIN 128 | // Python >= 3.12 129 | auto subclassesRaw = type.Field("tp_subclasses"); 130 | auto subclasses = utils::readIntegral(subclassesRaw); 131 | return subclasses - 1; 132 | } 133 | return -1; 134 | } 135 | 136 | 137 | auto PyTypeObject::hasInlineValues() const -> bool 138 | { 139 | if (isPython2()) 140 | return false; 141 | auto flagsRaw = remoteType().Field("tp_flags"); 142 | auto flags = utils::readIntegral(flagsRaw); 143 | return flags & (1 << 2); // Py_TPFLAGS_INLINE_VALUES 144 | } 145 | 146 | 147 | auto PyTypeObject::dictOffset() const -> SSize 148 | { 149 | auto dictOffset = remoteType().Field("tp_dictoffset"); 150 | return utils::readIntegral(dictOffset); 151 | } 152 | 153 | 154 | auto PyTypeObject::mro() const -> unique_ptr 155 | { 156 | return utils::fieldAsPyObject(remoteType(), "tp_mro"); 157 | } 158 | 159 | 160 | // This isn't really the best place for this method, but it's useful for factories to know 161 | // which type to construct when a Python3 type differs from a Python 2 type of the same name. 162 | auto PyTypeObject::isPython2() const -> bool 163 | { 164 | return !remoteType().HasField("ob_base"); 165 | } 166 | 167 | 168 | auto PyTypeObject::dict() const -> unique_ptr 169 | { 170 | Offset dictAddr; 171 | auto builtinIndex = getStaticBuiltinIndex(); 172 | if (builtinIndex != -1) { 173 | auto builtins = ExtRemoteTyped("_PyRuntime.interpreters.main->types.builtins"); // TODO: Support multiple interpreters 174 | if (builtins.HasField("initialized")) // Python >= 3.13 175 | builtins = builtins.Field("initialized"); 176 | auto builtinState = builtins.ArrayElement(builtinIndex); 177 | dictAddr = builtinState.Field("tp_dict").GetPtr(); 178 | } else { 179 | dictAddr = remoteType().Field("tp_dict").GetPtr(); 180 | } 181 | return make_unique(dictAddr); 182 | } 183 | 184 | 185 | auto PyTypeObject::repr(bool pretty) const -> string 186 | { 187 | string repr = ""; 188 | if (pretty) 189 | return utils::link(repr, "!pyobj 0n"s + to_string(offset())); 190 | return repr; 191 | } 192 | 193 | } -------------------------------------------------------------------------------- /src/objects/PyUnicodeObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyUnicodeObject.h" 2 | #include "PyStringValue.h" 3 | 4 | #include "utils/lossless_cast.h" 5 | #include "../ExtHelpers.h" 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | using namespace std; 15 | 16 | namespace { 17 | // Python3 strings follow an c-style "inheritance" heirarchy where the base struct is stored 18 | // as the first member of the derived with a name of `_base`. 19 | // From most to least derived, this looks like: 20 | // PyUnicodeObject -> PyCompactUnicodeObject -> PyASCIIObject. 21 | // 22 | // This function returns a field regardless of which `_base` it's held in. 23 | auto getField(ExtRemoteTyped obj, const string& fieldName) -> ExtRemoteTyped 24 | { 25 | // Follow the _base until we find a field or there are no more bases. 26 | while (!obj.HasField(fieldName.c_str()) && obj.HasField("_base")) { 27 | obj = obj.Field("_base"); 28 | } 29 | 30 | return obj.Field(fieldName.c_str()); 31 | } 32 | 33 | auto hasField(ExtRemoteTyped obj, const string& fieldName) -> bool 34 | { 35 | // Follow the _base until we find a field or there are no more bases. 36 | while (!obj.HasField(fieldName.c_str()) && obj.HasField("_base")) { 37 | obj = obj.Field("_base"); 38 | } 39 | 40 | return obj.HasField("fieldName.c_str()"); 41 | } 42 | } 43 | 44 | namespace PyExt::Remote { 45 | 46 | PyUnicodeObject::PyUnicodeObject(Offset objectAddress) 47 | : PyObject(objectAddress, "PyASCIIObject") 48 | { 49 | // We start off with the base type of PyASCIIObject. 50 | // Up-cast ourselved to the most derived type possible. 51 | if (isCompact()) { 52 | if (isAscii()) { 53 | remoteType().Set("PyASCIIObject", objectAddress, true); 54 | } else { 55 | remoteType().Set("PyCompactUnicodeObject", objectAddress, true); 56 | } 57 | } else { 58 | remoteType().Set("PyUnicodeObject", objectAddress, true); 59 | } 60 | } 61 | 62 | 63 | auto PyUnicodeObject::interningState() const -> InterningState 64 | { 65 | auto internedField = getField(remoteType(), "state").Field("interned"); 66 | return utils::readIntegral(internedField); 67 | } 68 | 69 | 70 | auto PyUnicodeObject::kind() const -> Kind 71 | { 72 | auto kindField = getField(remoteType(), "state").Field("kind"); 73 | return utils::readIntegral(kindField); 74 | } 75 | 76 | 77 | auto PyUnicodeObject::isCompact() const -> bool 78 | { 79 | auto compactField = getField(remoteType(), "state").Field("compact"); 80 | return utils::readIntegral(compactField); 81 | } 82 | 83 | 84 | auto PyUnicodeObject::isAscii() const -> bool 85 | { 86 | auto asciiField = getField(remoteType(), "state").Field("ascii"); 87 | return utils::readIntegral(asciiField); 88 | } 89 | 90 | 91 | auto PyUnicodeObject::isReady() const -> bool 92 | { 93 | if (!hasField(remoteType(), "state")) 94 | return true; //< All strings are "ready" in Python 3.12 95 | 96 | auto readyField = getField(remoteType(), "state").Field("ready"); 97 | return utils::readIntegral(readyField); 98 | } 99 | 100 | 101 | auto PyUnicodeObject::stringLength() const -> SSize 102 | { 103 | if (!isReady()) 104 | return 0; //< TODO: Consider supporting non-ready strings. 105 | 106 | auto lengthField = getField(remoteType(), "length"); 107 | return utils::readIntegral(lengthField); 108 | } 109 | 110 | 111 | namespace { 112 | // For now, these are dumb conversions. They will clip anything outside the bounds of a char. 113 | // I'm not too concerned with this since the debugger APIs we're calling don't display unicode 114 | // particularly well anyway. 115 | string wstring_to_string(wstring str) 116 | { 117 | return { begin(str), end(str) }; //< TODO: Convert. 118 | } 119 | 120 | string utf8_to_string(string str) 121 | { 122 | return str; //< TODO: Convert. 123 | } 124 | 125 | string utf16_to_string(u16string str) 126 | { 127 | return { begin(str), end(str) }; //< TODO: Convert. 128 | } 129 | 130 | string utf32_to_string(u32string str) 131 | { 132 | return { begin(str), end(str) }; //< TODO: Convert. 133 | } 134 | } 135 | 136 | 137 | auto PyUnicodeObject::stringValue() const -> string 138 | { 139 | const auto length = utils::lossless_cast(stringLength()); 140 | if (length == 0) 141 | return { }; 142 | 143 | if (hasField(remoteType(), "wstr")) { 144 | // Python <=3.11 had a wstr member in _base that could point directly to the string data. 145 | auto wstrField = getField(remoteType(), "wstr"); 146 | if (wstrField.GetPtr() != 0) { 147 | auto buffer = utils::readArray(wstrField, length); 148 | 149 | if (isAscii()) { 150 | return { begin(buffer), end(buffer) }; 151 | } 152 | else { 153 | return wstring_to_string({ begin(buffer), end(buffer) }); 154 | } 155 | } 156 | } else { 157 | Offset dataPtr = 0; 158 | if (isCompact()) { 159 | dataPtr = offset() + remoteType().Dereference().GetTypeSize(); 160 | } else { 161 | dataPtr = remoteType().Field("data").Field("any").GetPtr(); 162 | } 163 | 164 | switch (kind()) { 165 | case Kind::Wchar: 166 | { 167 | ExtRemoteTyped data("wchar_t", dataPtr, true); 168 | auto buffer = utils::readArray(data, length); 169 | return { begin(buffer), end(buffer) }; 170 | } break; 171 | case Kind::OneByte: 172 | { 173 | ExtRemoteTyped data("Py_UCS1", dataPtr, true); 174 | auto buffer = utils::readArray(data, length); 175 | return utf8_to_string({ begin(buffer), end(buffer) }); 176 | } break; 177 | case Kind::TwoByte: 178 | { 179 | ExtRemoteTyped data("Py_UCS2", dataPtr, true); 180 | auto buffer = utils::readArray(data, length); 181 | return utf16_to_string({ begin(buffer), end(buffer) }); 182 | } break; 183 | case Kind::FourByte: 184 | { 185 | ExtRemoteTyped data("Py_UCS4", dataPtr, true); 186 | auto buffer = utils::readArray(data, length); 187 | return utf16_to_string({ begin(buffer), end(buffer) }); 188 | } break; 189 | default: 190 | { 191 | throw runtime_error("Unexpected `kind` when decoding string value."); 192 | } break; 193 | } 194 | } 195 | } 196 | 197 | 198 | auto PyUnicodeObject::repr(bool pretty) const -> string 199 | { 200 | string repr = isReady() ? escapeAndQuoteString(stringValue()) : ""; 201 | return pretty ? utils::escapeDml(repr) : repr; 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /src/objects/PyVarObject.cpp: -------------------------------------------------------------------------------- 1 | #include "PyVarObject.h" 2 | 3 | #include "../ExtHelpers.h" 4 | 5 | #include 6 | 7 | #include 8 | using namespace std; 9 | 10 | namespace PyExt::Remote { 11 | 12 | PyVarObject::PyVarObject(Offset objectAddress, const string& typeName /*= "PyVarObject"*/) 13 | : PyObject(objectAddress, typeName) 14 | { 15 | } 16 | 17 | 18 | auto PyVarObject::size() const -> SSize 19 | { 20 | auto sizeField = baseField("ob_size"); 21 | return utils::readIntegral(sizeField); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/pch.cpp: -------------------------------------------------------------------------------- 1 | #include "pch.h" 2 | -------------------------------------------------------------------------------- /src/pch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Disable warnings from the Debugging Tools for Windows headers. 4 | #pragma warning( push ) 5 | #pragma warning( disable : 4838 ) 6 | #pragma warning( disable : 5040 ) 7 | #include 8 | #pragma warning( pop ) 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include -------------------------------------------------------------------------------- /src/pysetautointerpreterstate.cpp: -------------------------------------------------------------------------------- 1 | #include "extension.h" 2 | 3 | #include "PyInterpreterState.h" 4 | using namespace PyExt::Remote; 5 | 6 | #include "ExtHelpers.h" 7 | 8 | #include 9 | 10 | #include 11 | #include 12 | using namespace std; 13 | 14 | 15 | namespace PyExt { 16 | 17 | EXT_COMMAND(pysetautointerpreterstate, "Manually provide the address of the PyInterpreterState struct. Use only when PyExt fails to detect it automatically.", "{;x,o;PyInterpreterState Expression}") 18 | { 19 | ensureSymbolsLoaded(); 20 | 21 | string objExpression = (m_NumUnnamedArgs < 1) ? "" : GetUnnamedArgStr(0); 22 | 23 | // No autointerpreterstate expression provided. Revert to the default. 24 | if (objExpression.empty()) { 25 | Out("Using default AutoInterpreterState.\n"); 26 | PyInterpreterState::setAutoInterpreterStateExpression(""); 27 | return; 28 | } 29 | 30 | // Validate the expression before passing it onto PyInterpreterState. 31 | try { 32 | ExtRemoteTyped remoteObj(objExpression.c_str()); 33 | } catch (ExtException&) { 34 | Out("PySetAutoInterpreterState failed to evaluate expression: %s\n", objExpression.c_str()); 35 | throw; 36 | } 37 | 38 | // If we got here then the provided expression is at least valid. 39 | PyInterpreterState::setAutoInterpreterStateExpression(objExpression); 40 | Out("AutoInterpreterState set to: %s\n", objExpression.c_str()); 41 | 42 | Out("\n"); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/pystack.cpp: -------------------------------------------------------------------------------- 1 | #include "extension.h" 2 | 3 | #include "PyFrameObject.h" 4 | #include "PyInterpreterFrame.h" 5 | #include "PyDictObject.h" 6 | #include "PyCodeObject.h" 7 | #include "PyInterpreterState.h" 8 | #include "PyThreadState.h" 9 | using namespace PyExt::Remote; 10 | 11 | #include "ExtHelpers.h" 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | using namespace std; 23 | 24 | 25 | namespace { 26 | 27 | auto getCurrentThreadId(IDebugSystemObjects* pSystem) -> uint64_t 28 | { 29 | ULONG currentThreadSystemId = 0; 30 | HRESULT hr = pSystem->GetCurrentThreadId(¤tThreadSystemId); 31 | if (FAILED(hr)) 32 | throw runtime_error("Failed to retreive current thread id."); 33 | return currentThreadSystemId; 34 | } 35 | 36 | 37 | auto getCurrentThreadSystemId(IDebugSystemObjects* pSystem) -> uint64_t 38 | { 39 | ULONG currentThreadSystemId = 0; 40 | HRESULT hr = pSystem->GetCurrentThreadSystemId(¤tThreadSystemId); 41 | if (FAILED(hr)) 42 | throw runtime_error("Failed to retreive current thread system id."); 43 | return currentThreadSystemId; 44 | } 45 | 46 | 47 | auto frameToString(const PyFrame& frame) -> string 48 | { 49 | ostringstream oss; 50 | 51 | auto codeObject = frame.code(); 52 | if (codeObject == nullptr) 53 | throw runtime_error("Warning: PyFrameObject is missing PyCodeObject."); 54 | 55 | auto filename = codeObject->filename(); 56 | oss << "File \"" << utils::link(filename, ".open " + filename, "Open source code.") 57 | << "\", line " << frame.currentLineNumber() 58 | << ", in " << utils::link(codeObject->name(), "!pyobj 0n" + to_string(codeObject->offset()), "Inspect PyCodeObject."); 59 | 60 | return oss.str(); 61 | } 62 | 63 | 64 | auto frameToCommandString(const PyFrame& frame) -> string 65 | { 66 | ostringstream oss; 67 | 68 | auto* interpreterFrame = dynamic_cast(&frame); 69 | if (interpreterFrame) { 70 | // 3.11+ 71 | oss << utils::link("[Frame]", "!pyinterpreterframe 0n"s + to_string(interpreterFrame->offset()), "Inspect interpreter frame (including localsplus).") << " "; 72 | } else { 73 | // <3.11 74 | auto& frameObject = dynamic_cast(frame); 75 | oss << utils::link("[Frame]", "!pyobj 0n"s + to_string(frameObject.offset()), "Inspect frame object (including localsplus).") << " "; 76 | } 77 | 78 | auto locals = frame.locals(); 79 | if (locals != nullptr && locals->offset() != 0) 80 | oss << utils::link("[Locals]", "!pyobj 0n"s + to_string(locals->offset()), "Inspect this frame's locals.") << " "; 81 | 82 | auto globals = frame.globals(); 83 | if (globals != nullptr && globals->offset() != 0) 84 | oss << utils::link("[Globals]", "!pyobj 0n"s + to_string(globals->offset()), "Inspect this frame's captured globals.") << " "; 85 | 86 | return oss.str(); 87 | } 88 | 89 | } 90 | 91 | namespace PyExt { 92 | 93 | EXT_COMMAND(pystack, "Prints the Python stack for the current thread, or starting at a provided PyFrameObject", "{;s,o;PyFrameObject address}") 94 | { 95 | ensureSymbolsLoaded(); 96 | 97 | try { 98 | unique_ptr frame; 99 | 100 | // Either start at the user-provided PyFrameObject, or find the top frame of the current thread, if one exists. 101 | if (m_NumUnnamedArgs < 1) { 102 | // Print the thread header. 103 | auto threadHeader = "Thread " + to_string(getCurrentThreadId(m_System)) + ":"; 104 | Out("%s\n", threadHeader.c_str()); 105 | 106 | auto threadSystemId = getCurrentThreadSystemId(m_System); 107 | auto threadState = PyInterpreterState::findThreadStateBySystemThreadId(threadSystemId); 108 | if (!threadState.has_value()) 109 | throw runtime_error("Thread does not contain any Python frames."); 110 | 111 | frame = threadState->currentFrame(); 112 | } else { 113 | // Print info about the user-provided PyFrameObject as a header. 114 | auto frameOffset = evalOffset(GetUnnamedArgStr(0)); 115 | Out("Stack trace starting at (PyFrameObject*)(%y):\n", frameOffset); 116 | 117 | frame = make_unique(frameOffset); 118 | } 119 | 120 | if (frame == nullptr) 121 | throw runtime_error("Could not find PyFrameObject or PyInterpreterFrame."); 122 | 123 | // Print each frame. 124 | for (; frame != nullptr; frame = frame->previous()) { 125 | auto frameStr = frameToString(*frame); 126 | Dml("\t%s\n", frameStr.c_str()); 127 | 128 | auto frameCommandStr = frameToCommandString(*frame); 129 | Dml("\t\t%s\n", frameCommandStr.c_str()); 130 | } 131 | } catch (exception& ex) { 132 | Warn("\t%s\n", ex.what()); 133 | if (!HasFullMemBasic()) { 134 | Err("\tThe dump file does not contain enough data for PyExt to work properly.\n"); 135 | Err("\tCapture a dump with full memory to ensure PyExt can reconstruct callstacks.\n"); 136 | } 137 | } 138 | 139 | Out("\n"); 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/pysymfix.cpp: -------------------------------------------------------------------------------- 1 | #include "extension.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | using namespace std; 9 | 10 | 11 | namespace { 12 | 13 | auto getSymbolPath(IDebugSymbols* pSymbols) -> string 14 | { 15 | // Get the size we'll need first. 16 | ULONG pathSize = 0; 17 | HRESULT hr = pSymbols->GetSymbolPath(nullptr, 0, &pathSize); 18 | if (FAILED(hr)) 19 | throw runtime_error("GetSymbolPath failed to get path size with hr=" + hr); 20 | 21 | // Now get the buffer. 22 | string buff(pathSize, '\0'); 23 | hr = pSymbols->GetSymbolPath(buff.data(), pathSize, &pathSize); 24 | if (FAILED(hr)) 25 | throw runtime_error("GetSymbolPath failed to get path size with hr=" + hr); 26 | 27 | assert(pathSize == buff.size() && "Unexpected symbol path size after second call to GetSymbolPath."); 28 | buff.erase(buff.find('\0')); //< Trim off any extra from our string. 29 | 30 | return buff; 31 | } 32 | 33 | 34 | auto setSymbolPath(IDebugSymbols* pSymbols, const string& path) -> void 35 | { 36 | HRESULT hr = pSymbols->SetSymbolPath(path.c_str()); 37 | if (FAILED(hr)) 38 | throw runtime_error("SetSymbolPath failed with hr=" + hr); 39 | } 40 | 41 | string concatenatePaths(const string& path1, const string& path2, char delim = ';') 42 | { 43 | auto cattedPath = path1; 44 | if (!cattedPath.empty() && cattedPath.back() != delim) { 45 | cattedPath += delim; 46 | } 47 | cattedPath += path2; 48 | return cattedPath; 49 | } 50 | 51 | } 52 | 53 | 54 | namespace PyExt { 55 | 56 | EXT_COMMAND(pysymfix, "Adds python symbols to the symbol path.", "") 57 | { 58 | auto sympath = getSymbolPath(m_Symbols); 59 | Out("Current symbol path: %s\n", sympath.c_str()); 60 | 61 | if (sympath.find("pythonsymbols.sdcline.com/symbols") == string::npos) { 62 | Out("Adding symbol server to path...\n"); 63 | const auto newPath = concatenatePaths(sympath, "srv*http://pythonsymbols.sdcline.com/symbols"); 64 | setSymbolPath(m_Symbols, newPath); 65 | Out("New symbol path: %s\n", getSymbolPath(m_Symbols).c_str()); 66 | } else { 67 | Out("Python symbol server already in path.\n"); 68 | } 69 | 70 | Out("Loading symbols...\n"); 71 | ensureSymbolsLoaded(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /test/PyExtTest/FibonacciTest.cpp: -------------------------------------------------------------------------------- 1 | #include "Catch.hpp" 2 | 3 | #include "PythonDumpFile.h" 4 | #include "TestConfigData.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | using namespace PyExt::Remote; 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | TEST_CASE("fibonacci_test.py has the expected line numbers.", "[integration][fibonacci_test]") 21 | { 22 | auto dump = PythonDumpFile(TestConfigData::instance().fibonaciiDumpFileNameOrDefault()); 23 | 24 | // Set up pyext.dll so it thinks DbgEng is calling into it. 25 | PyExt::InitializeGlobalsForTest(dump.pClient.Get()); 26 | auto cleanup = utils::makeScopeExit(PyExt::UninitializeGlobalsForTest); 27 | 28 | auto frames = dump.getMainThreadFrames(); 29 | REQUIRE(frames.size() > 90); 30 | 31 | SECTION("Bottom frame is the module.") 32 | { 33 | auto bottomFrame = frames.back(); 34 | REQUIRE(bottomFrame->currentLineNumber() == 28); 35 | 36 | auto codeObj = bottomFrame->code(); 37 | REQUIRE(codeObj != nullptr); 38 | REQUIRE(codeObj->name() == ""); 39 | REQUIRE(codeObj->filename().find("fibonacci_test.py") != std::string::npos); 40 | 41 | // Expected to be similar to: 42 | std::regex expectedRegex(R"()"); 43 | REQUIRE(regex_match(codeObj->repr(false), expectedRegex)); 44 | // Expected to be similar to: "<code object, file ".\fibonacci_test.py", line 9>" 45 | expectedRegex = R"(<code object, file "[^&]*fibonacci_test.py", line \d+>)"; 46 | REQUIRE(regex_match(codeObj->repr(true), expectedRegex)); 47 | } 48 | 49 | SECTION("The next several frames are in function recursive_fib.") 50 | { 51 | auto numFibFrames = std::count_if(begin(frames), end(frames), [](auto frame) { 52 | return frame->code()->name() == "recursive_fib" && frame->currentLineNumber() == 24; 53 | }); 54 | 55 | REQUIRE(numFibFrames > 90); 56 | } 57 | 58 | SECTION("The top frame in recursive_fib is the one that triggered the dump.") 59 | { 60 | auto topFrameInFib = std::find_if(begin(frames), end(frames), [](auto frame) { 61 | return frame->code()->name() == "recursive_fib"; 62 | }); 63 | 64 | REQUIRE((*topFrameInFib)->currentLineNumber() == 18); 65 | REQUIRE((*(topFrameInFib - 1))->code()->name() == "dump_process"); 66 | } 67 | } -------------------------------------------------------------------------------- /test/PyExtTest/LocalsplusTest.cpp: -------------------------------------------------------------------------------- 1 | #include "Catch.hpp" 2 | 3 | #include "PythonDumpFile.h" 4 | #include "TestConfigData.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | using namespace PyExt::Remote; 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | using namespace std; 21 | 22 | TEST_CASE("localsplus_test.py has the expected frames with localsplus.", "[integration][localsplus_test]") 23 | { 24 | auto dump = PythonDumpFile(TestConfigData::instance().localsplusDumpFileNameOrDefault()); 25 | 26 | // Set up pyext.dll so it thinks DbgEng is calling into it. 27 | PyExt::InitializeGlobalsForTest(dump.pClient.Get()); 28 | auto cleanup = utils::makeScopeExit(PyExt::UninitializeGlobalsForTest); 29 | 30 | auto frames = dump.getMainThreadFrames(); 31 | REQUIRE(frames.size() > 6); 32 | 33 | vector> expectations{ 34 | { 35 | "f_cellvar", 36 | 37 | R"(localsplus: \{\n)" 38 | R"(\t'self': <A object>,\n)" 39 | R"(\t'param1': 'test',\n)" 40 | R"(\t'param2': \[ 1, 2, 3 \],\n)" 41 | R"(\t'local1': 5,\n)" 42 | R"(\t'f_cellfreevar': <function f_cellfreevar>,\n)" 43 | R"(\t'cell1': <cell object>: \{\n)" 44 | R"(\t1: 2,\n)" 45 | R"(\t3: 4,\n)" 46 | R"(\},\n\})" 47 | }, 48 | { 49 | "f_cellfreevar", 50 | 51 | R"(localsplus: \{\n)" 52 | R"(\t'param': 7,\n)" 53 | R"(\t'local2': 6,\n)" 54 | R"(\t'f_freevar': <function f_freevar>,\n)" 55 | R"(\t'cell2': <cell object>: 9,\n)" 56 | R"(\t'cell1': <cell object>: \{\n)" 57 | R"(\t1: 2,\n)" 58 | R"(\t3: 4,\n)" 59 | R"(\},\n\})" 60 | }, 61 | { 62 | "f_freevar", 63 | 64 | R"(localsplus: \{\n)" 65 | R"(\t'x': 9,\n)" 66 | R"(\t'cell2': <cell object>: 9,\n)" 67 | R"(\})" 68 | }, 69 | }; 70 | 71 | for (auto& exp : expectations) { 72 | auto& name = exp[0]; 73 | auto& details = exp[1]; 74 | 75 | SECTION("Localsplus for frame in method " + name + "().") 76 | { 77 | auto frame = std::find_if(begin(frames), end(frames), [&name](auto frame) { 78 | return frame->code()->name() == name; 79 | }); 80 | 81 | std::regex expectedRegex(details); 82 | REQUIRE(regex_match((*frame)->details(), expectedRegex)); 83 | } 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /test/PyExtTest/ObjectDetailsTest.cpp: -------------------------------------------------------------------------------- 1 | #include "Catch.hpp" 2 | 3 | #include "PythonDumpFile.h" 4 | #include "TestConfigData.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | using namespace PyExt::Remote; 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | using namespace std; 21 | 22 | namespace { 23 | /// Finds a value in the given dict for a given key. 24 | template 25 | auto findValueByKey(RangeT& pairRange, const string& key) -> PyObject& { 26 | auto it = find_if(begin(pairRange), end(pairRange), [&](const auto& keyValuePair) { 27 | const auto* keyObj = dynamic_cast(keyValuePair.first.get()); 28 | return keyObj != nullptr && keyObj->stringValue() == key; 29 | }); 30 | 31 | if (it == end(pairRange)) 32 | throw runtime_error("Value not found for key=" + key); 33 | return *it->second; 34 | } 35 | } 36 | 37 | 38 | TEST_CASE("object_details.py has a stack frame with expected locals.", "[integration][object_details]") 39 | { 40 | auto dump = PythonDumpFile(TestConfigData::instance().objectDetailsDumpFileNameOrDefault()); 41 | 42 | // Set up pyext.dll so it thinks DbgEng is calling into it. 43 | PyExt::InitializeGlobalsForTest(dump.pClient.Get()); 44 | auto cleanup = utils::makeScopeExit(PyExt::UninitializeGlobalsForTest); 45 | 46 | auto frames = dump.getMainThreadFrames(); 47 | auto bottomFrame = frames.back(); 48 | REQUIRE(bottomFrame->code()->name() == ""); 49 | 50 | auto locals = bottomFrame->locals(); 51 | REQUIRE(locals != nullptr); 52 | 53 | auto localPairs = locals->pairValues(); 54 | REQUIRE(localPairs.size() >= 14); 55 | 56 | 57 | vector> expectations{ 58 | // Regex only necessary due to Python 2 (dicts not sorted) 59 | { "d" , "D" , R"(dict: \{\n\t'(d1': 1,\n\t'd2': 2,|d2': 2,\n\t'd1': 1,)\n\})" }, 60 | { "s" , "S" , R"(slots: \{\n\tslot1: 1,\n\tslot2: 2,\n\})" }, 61 | { "dsubd" , "DsubD" , R"(dict: \{\n(\t('d1': 1|'d2': 2|'d3': 3),\n){3}\})" }, 62 | { "ssubs" , "SsubS" , R"(slots: \{\n\tslot3: 3,\n\tslot1: 1,\n\tslot2: 2,\n\})" }, 63 | { "dsubs" , "DsubS" , R"(slots: \{\n\tslot1: 1,\n\tslot2: 2,\n\}\ndict: \{\n\t'd3': 3,\n\})" }, 64 | { "ssubd" , "SsubD" , R"(slots: \{\n\tslot3: 3,\n\}\ndict: \{\n(\t('d1': 1|'d2': 2),\n){2}\})" }, 65 | { "ssubds" , "SsubDS" , R"(slots: \{\n\tslot3: 5,\n\tslot1: 3,\n\tslot2: 4,\n\}\ndict: \{\n(\t('d1': 1|'d2': 2),\n){2}\})" }, 66 | { "negDictOffset", "NegDictOffset" , R"(tuple repr: \(1, 2, 3\)\ndict: \{\n\t'attr': 'test',\n\})" }, 67 | { "manDictRes" , "ManagedDictResolved", R"(dict: \{(\n\t'a\d+': \d+,){32}\n\})" }, 68 | }; 69 | 70 | for (auto& objExp : expectations) { 71 | auto& name = objExp[0]; 72 | auto& expectedType = objExp[1]; 73 | auto& expectedDetails = objExp[2]; 74 | 75 | SECTION("Details of " + name + " object") 76 | { 77 | auto& obj = findValueByKey(localPairs, name); 78 | REQUIRE(obj.type().name() == expectedType); 79 | std::regex expectedRegex(expectedDetails); 80 | REQUIRE(regex_match(obj.details(), expectedRegex)); 81 | } 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /test/PyExtTest/PyExtTest.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 15.0 23 | {AE750E22-5A8E-401E-A843-B9D56B09A015} 24 | PyExtTest 25 | 10.0.17763.0 26 | 27 | 28 | 29 | Application 30 | true 31 | v141 32 | 33 | 34 | 35 | 36 | Application 37 | false 38 | v141 39 | true 40 | 41 | 42 | 43 | 44 | Application 45 | true 46 | 47 | 48 | v141 49 | 50 | 51 | Application 52 | false 53 | v141 54 | true 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | $(SolutionDir)$(Platform)\$(Configuration)\ 78 | $(Platform)\$(Configuration)\ 79 | $(SolutionDir)\include;$(WindowsSdkDir)\Debuggers\inc;$(IncludePath) 80 | 81 | 82 | $(SolutionDir)$(Platform)\$(Configuration)\ 83 | $(Platform)\$(Configuration)\ 84 | $(SolutionDir)\include;$(WindowsSdkDir)\Debuggers\inc;$(IncludePath) 85 | 86 | 87 | $(SolutionDir)\include;$(WindowsSdkDir)\Debuggers\inc;$(IncludePath) 88 | 89 | 90 | $(SolutionDir)\include;$(WindowsSdkDir)\Debuggers\inc;$(IncludePath) 91 | 92 | 93 | 94 | Level4 95 | Disabled 96 | true 97 | Caret 98 | stdcpp17 99 | NOMINMAX 100 | 101 | 102 | $(TargetDir)\pyext.lib;dbgeng.lib;%(AdditionalDependencies) 103 | Console 104 | 105 | 106 | echo Copying Windows SDK debugger DLLs to target directory... 107 | cd $(TargetDir) 108 | IF NOT EXIST dbgeng.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbgeng.dll" .\ 109 | IF NOT EXIST dbgcore.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbgcore.dll" .\ 110 | IF NOT EXIST dbghelp.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbghelp.dll" .\ 111 | IF NOT EXIST DbgModel.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\DbgModel.dll" .\ 112 | IF NOT EXIST symsrv.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\symsrv.dll" .\ 113 | 114 | Copying dbghelp and symsrv from the Debugging Tools for Windows package since the one that ships in system32 can't download symbols from a remote server... 115 | 116 | 117 | 118 | 119 | Level4 120 | Disabled 121 | true 122 | Caret 123 | stdcpplatest 124 | NOMINMAX 125 | 126 | 127 | $(TargetDir)\pyext.lib;dbgeng.lib;%(AdditionalDependencies) 128 | Console 129 | 130 | 131 | echo Copying Windows SDK debugger DLLs to target directory... 132 | cd $(TargetDir) 133 | IF NOT EXIST dbgeng.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbgeng.dll" .\ 134 | IF NOT EXIST dbgcore.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbgcore.dll" .\ 135 | IF NOT EXIST dbghelp.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbghelp.dll" .\ 136 | IF NOT EXIST DbgModel.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\DbgModel.dll" .\ 137 | IF NOT EXIST symsrv.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\symsrv.dll" .\ 138 | 139 | Copying dbghelp and symsrv from the Debugging Tools for Windows package since the one that ships in system32 can't download symbols from a remote server... 140 | 141 | 142 | 143 | 144 | Level4 145 | MaxSpeed 146 | true 147 | true 148 | true 149 | Caret 150 | stdcpplatest 151 | NOMINMAX 152 | 153 | 154 | true 155 | true 156 | $(TargetDir)\pyext.lib;dbgeng.lib;%(AdditionalDependencies) 157 | Console 158 | 159 | 160 | echo Copying Windows SDK debugger DLLs to target directory... 161 | cd $(TargetDir) 162 | IF NOT EXIST dbgeng.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbgeng.dll" .\ 163 | IF NOT EXIST dbgcore.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbgcore.dll" .\ 164 | IF NOT EXIST dbghelp.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbghelp.dll" .\ 165 | IF NOT EXIST DbgModel.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\DbgModel.dll" .\ 166 | IF NOT EXIST symsrv.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\symsrv.dll" .\ 167 | 168 | Copying dbghelp and symsrv from the Debugging Tools for Windows package since the one that ships in system32 can't download symbols from a remote server... 169 | 170 | 171 | 172 | 173 | Level4 174 | MaxSpeed 175 | true 176 | true 177 | true 178 | Caret 179 | stdcpplatest 180 | NOMINMAX 181 | 182 | 183 | true 184 | true 185 | $(TargetDir)\pyext.lib;dbgeng.lib;%(AdditionalDependencies) 186 | Console 187 | 188 | 189 | echo Copying Windows SDK debugger DLLs to target directory... 190 | cd $(TargetDir) 191 | IF NOT EXIST dbgeng.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbgeng.dll" .\ 192 | IF NOT EXIST dbgcore.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbgcore.dll" .\ 193 | IF NOT EXIST dbghelp.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\dbghelp.dll" .\ 194 | IF NOT EXIST DbgModel.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\DbgModel.dll" .\ 195 | IF NOT EXIST symsrv.dll copy "$(FrameworkSdkDir)Debuggers\$(PlatformTarget)\symsrv.dll" .\ 196 | 197 | Copying dbghelp and symsrv from the Debugging Tools for Windows package since the one that ships in system32 can't download symbols from a remote server... 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | {eebc73ba-27f2-4483-8639-54a108b77594} 213 | false 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /test/PyExtTest/PyExtTest.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Source Files 20 | 21 | 22 | Source Files 23 | 24 | 25 | Source Files 26 | 27 | 28 | Source Files 29 | 30 | 31 | Source Files 32 | 33 | 34 | Source Files 35 | 36 | 37 | Source Files 38 | 39 | 40 | Source Files 41 | 42 | 43 | 44 | 45 | Header Files 46 | 47 | 48 | Header Files 49 | 50 | 51 | -------------------------------------------------------------------------------- /test/PyExtTest/PythonDumpFile.cpp: -------------------------------------------------------------------------------- 1 | #include "PythonDumpFile.h" 2 | #include "TestConfigData.h" 3 | 4 | #include 5 | #include 6 | #include 7 | using namespace PyExt::Remote; 8 | 9 | #include 10 | #include 11 | using Microsoft::WRL::ComPtr; 12 | 13 | #include 14 | #include 15 | #include 16 | using namespace std; 17 | 18 | namespace { 19 | string hresult_to_string(HRESULT hr) 20 | { 21 | return to_string(hr); 22 | } 23 | } 24 | 25 | PythonDumpFile::PythonDumpFile(const std::string& dumpFilename) 26 | { 27 | createDebugInterfaces(); 28 | openDumpFile(dumpFilename); 29 | setSymbolPath(TestConfigData::instance().symbolPathOrDefault()); 30 | loadPyExt(); 31 | } 32 | 33 | 34 | PythonDumpFile::~PythonDumpFile() 35 | { 36 | } 37 | 38 | 39 | auto PythonDumpFile::getMainThreadFrames() const -> std::vector> 40 | { 41 | auto interpState = PyInterpreterState::makeAutoInterpreterState(); 42 | auto threads = interpState->allThreadStates(); 43 | if (threads.empty()) 44 | throw std::runtime_error("No threads in process."); 45 | 46 | return threads.back().allFrames(); 47 | } 48 | 49 | 50 | auto PythonDumpFile::createDebugInterfaces() -> void 51 | { 52 | HRESULT hr = DebugCreate(__uuidof(IDebugClient), reinterpret_cast(pClient.GetAddressOf())); 53 | if (FAILED(hr)) 54 | throw runtime_error("Failed to create IDebugClient. hr=" + hresult_to_string(hr)); 55 | 56 | if (FAILED(pClient.As(&pControl))) 57 | throw runtime_error("Failed to query IDebugControl. hr=" + hresult_to_string(hr)); 58 | 59 | if (FAILED(pClient.As(&pSymbols))) 60 | throw runtime_error("Failed to query IDebugSymbols2. hr=" + hresult_to_string(hr)); 61 | } 62 | 63 | 64 | auto PythonDumpFile::setSymbolPath(const string& symbolPath) -> void 65 | { 66 | HRESULT hr = pSymbols->SetSymbolPath(symbolPath.c_str()); 67 | if (FAILED(hr)) 68 | throw runtime_error("Failed to SetSymbolPath. hr=" + hresult_to_string(hr)); 69 | 70 | pSymbols->Reload(""); 71 | pSymbols->Reload("/f python*"); 72 | } 73 | 74 | 75 | auto PythonDumpFile::openDumpFile(const std::string & dumpFilename) -> void 76 | { 77 | HRESULT hr = pClient->OpenDumpFile(dumpFilename.c_str()); 78 | if (FAILED(hr)) 79 | throw runtime_error("OpenDumpFile failed with dumpFilename=" + dumpFilename); 80 | 81 | // The debug session isn't completely open until we call WaitForEvent. 82 | hr = pControl->WaitForEvent(DEBUG_WAIT_DEFAULT, 3000 /*ms*/); 83 | if (FAILED(hr) || hr == S_FALSE) 84 | throw runtime_error("Failed to WaitForEvent. hr=" + hresult_to_string(hr)); 85 | } 86 | 87 | 88 | auto PythonDumpFile::loadPyExt() -> void 89 | { 90 | // Manually call ::LoadLibrary to use the typical dll search order since 91 | // IDebugControl::AddExtension doesn't look everywhere for the extension. 92 | auto pyExtModule = ::LoadLibrary("pyext"); 93 | if (pyExtModule == 0) 94 | throw runtime_error("Could not load pyext.dll"); 95 | 96 | // Once the extension dll is loaded, tell DbgEng to initialize it. 97 | constexpr auto bufferSize = 1024; 98 | string pyExtFile(bufferSize, '\0'); 99 | auto nSize = ::GetModuleFileName(pyExtModule, pyExtFile.data(), bufferSize); 100 | if (nSize == bufferSize) 101 | throw runtime_error("GetModuleFileName buffer too small."); 102 | 103 | pyExtFile.resize(nSize); 104 | 105 | ULONG64 pyExtHandle = 0; 106 | HRESULT hr = pControl->AddExtension(pyExtFile.data(), 0, &pyExtHandle); 107 | if (FAILED(hr)) 108 | throw runtime_error("Failed to load PyExt extension. hr=" + hresult_to_string(hr)); 109 | } 110 | -------------------------------------------------------------------------------- /test/PyExtTest/PythonDumpFile.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // Forward declarations. 9 | struct IDebugClient; 10 | struct IDebugControl; 11 | struct IDebugSymbols2; 12 | 13 | namespace PyExt::Remote { 14 | class PyFrame; 15 | } 16 | 17 | class PythonDumpFile { 18 | 19 | public: // Construction/Destruction. 20 | PythonDumpFile(const std::string& dumpFilename); 21 | ~PythonDumpFile(); 22 | 23 | public: 24 | auto getMainThreadFrames() const -> std::vector>; 25 | 26 | public: // Debug interfaces. 27 | Microsoft::WRL::ComPtr pClient; 28 | Microsoft::WRL::ComPtr pControl; 29 | Microsoft::WRL::ComPtr pSymbols; 30 | 31 | private: // Helpers. 32 | auto createDebugInterfaces() -> void; 33 | auto setSymbolPath(const std::string& symbolPath) -> void; 34 | auto openDumpFile(const std::string & dumpFilename) -> void; 35 | auto loadPyExt() -> void; 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /test/PyExtTest/ScopeExitTest.cpp: -------------------------------------------------------------------------------- 1 | #include "Catch.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | SCENARIO("ScopeExit calls its invokable when destroyed", "[ScopeExit]") 9 | { 10 | GIVEN("A ScopeExit constructed using its factory") 11 | { 12 | int numTimesCalled = 0; 13 | 14 | WHEN("it goes out of scope") 15 | { 16 | // Introduce a scope so ScopeExit's destructor is called. 17 | { 18 | auto se = utils::makeScopeExit([&numTimesCalled] { 19 | ++numTimesCalled; 20 | }); 21 | } 22 | 23 | THEN("its invokable is called once.") 24 | { 25 | REQUIRE(numTimesCalled == 1); 26 | } 27 | } 28 | } 29 | } 30 | 31 | 32 | SCENARIO("ScopeExit does not call its invokable when reset", "[ScopeExit]") 33 | { 34 | GIVEN("A ScopeExit constructed using its factory") 35 | { 36 | int numTimesCalled = 0; 37 | 38 | WHEN("it goes out of scope after being reset") 39 | { 40 | // Introduce a scope so ScopeExit's destructor is called. 41 | { 42 | auto se = utils::makeScopeExit([&numTimesCalled] { 43 | ++numTimesCalled; 44 | }); 45 | 46 | se.reset(); 47 | } 48 | 49 | THEN("its invokable is not called.") 50 | { 51 | REQUIRE(numTimesCalled == 0); 52 | } 53 | } 54 | } 55 | } 56 | 57 | 58 | SCENARIO("ScopeExit does not call its invokable when moved", "[ScopeExit]") 59 | { 60 | 61 | GIVEN("A ScopeExit constructed with a lambda") 62 | { 63 | int numTimesCalled = 0; 64 | auto se = utils::makeScopeExit([&numTimesCalled] { 65 | ++numTimesCalled; 66 | }); 67 | 68 | WHEN("it hasn't been destroyed yet") 69 | { 70 | THEN("its invokable has not yet been called.") 71 | { 72 | REQUIRE(numTimesCalled == 0); 73 | } 74 | } 75 | 76 | WHEN("it is move assigned to another scope_exit") 77 | { 78 | auto movedSe = std::move(se); 79 | 80 | THEN("its invokable has not yet been called.") 81 | { 82 | REQUIRE(numTimesCalled == 0); 83 | } 84 | } 85 | 86 | WHEN("another ScopeExit is move constructed from it") 87 | { 88 | auto movedSe{ std::move(se) }; 89 | 90 | THEN("its invokable has not yet been called.") 91 | { 92 | REQUIRE(numTimesCalled == 0); 93 | } 94 | } 95 | } 96 | } 97 | 98 | 99 | SCENARIO("ScopeExit does not call its invokable when its invokable is changed", "[ScopeExit]") 100 | { 101 | GIVEN("A ScopeExit constructed a with type-erased function") 102 | { 103 | int numTimesCalled = 0; 104 | auto se = utils::ScopeExit>([&numTimesCalled] { 105 | ++numTimesCalled; 106 | }); 107 | 108 | WHEN("its function is changed out for another of the same type") 109 | { 110 | std::function countCalls2 = [&numTimesCalled] { 111 | ++numTimesCalled; 112 | }; 113 | 114 | se.set(std::move(countCalls2)); 115 | 116 | THEN("neither the previous, nor the new function are called.") 117 | { 118 | REQUIRE(numTimesCalled == 0); 119 | } 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /test/PyExtTest/TestConfigData.cpp: -------------------------------------------------------------------------------- 1 | #include "TestConfigData.h" 2 | 3 | auto TestConfigData::instance() -> TestConfigData& 4 | { 5 | static TestConfigData config; 6 | return config; 7 | } 8 | -------------------------------------------------------------------------------- /test/PyExtTest/TestConfigData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct TestConfigData { 6 | 7 | static auto instance()->TestConfigData&; 8 | 9 | std::string objectTypesDumpFileName; 10 | auto objectTypesDumpFileNameOrDefault() const -> std::string 11 | { 12 | return objectTypesDumpFileName.empty() ? "object_types.dmp" : objectTypesDumpFileName; 13 | } 14 | 15 | std::string fibonacciDumpFileName; 16 | auto fibonaciiDumpFileNameOrDefault() const -> std::string 17 | { 18 | return fibonacciDumpFileName.empty() ? "fibonacci_test.dmp" : fibonacciDumpFileName; 19 | } 20 | 21 | std::string objectDetailsDumpFileName; 22 | auto objectDetailsDumpFileNameOrDefault() const -> std::string 23 | { 24 | return objectDetailsDumpFileName.empty() ? "object_details.dmp" : objectDetailsDumpFileName; 25 | } 26 | 27 | std::string localsplusDumpFileName; 28 | auto localsplusDumpFileNameOrDefault() const -> std::string 29 | { 30 | return localsplusDumpFileName.empty() ? "localsplus_test.dmp" : localsplusDumpFileName; 31 | } 32 | 33 | std::string symbolPath; 34 | auto symbolPathOrDefault() const -> std::string 35 | { 36 | return symbolPath.empty() ? "srv*c:\\symbols\\*http://pythonsymbols.sdcline.com/symbols/" : symbolPath; 37 | } 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /test/PyExtTest/main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_RUNNER 2 | #include "catch.hpp" 3 | 4 | #include "TestConfigData.h" 5 | 6 | // Disable warnings from the Debugging Tools for Windows headers. 7 | #pragma warning( push ) 8 | #pragma warning( disable : 4838 ) 9 | #pragma warning( disable : 5040 ) 10 | #include 11 | #pragma warning( pop ) 12 | 13 | // Provide a pretty-printer for the ExtException used by EngExtCpp. 14 | CATCH_TRANSLATE_EXCEPTION(ExtException& ex) 15 | { 16 | return ex.GetMessage(); 17 | } 18 | 19 | 20 | // Provide our own main as explained here: 21 | // https://github.com/catchorg/Catch2/blob/master/docs/own-main.md 22 | // This lets us parse a couple extra command line arguments. 23 | int main(int argc, char* argv[]) 24 | { 25 | auto& configData = TestConfigData::instance(); 26 | 27 | // Add our own command line argments. 28 | using namespace Catch::clara; 29 | Catch::Session session; 30 | auto cli = session.cli(); 31 | 32 | cli |= Opt(configData.objectTypesDumpFileName, "objectTypesDumpFileName") 33 | ["--object-types-dump-file"] 34 | ("The dump file to run tests object - types test against."); 35 | 36 | cli |= Opt(configData.fibonacciDumpFileName, "fibonaciiDumpFileName") 37 | ["--fibonacci-dump-file"] 38 | ("The dump file to run tests fibonacci tests against."); 39 | 40 | cli |= Opt(configData.objectDetailsDumpFileName, "objectDetailsDumpFileName") 41 | ["--object-details-dump-file"] 42 | ("The dump file to run tests object - details test against."); 43 | 44 | cli |= Opt(configData.localsplusDumpFileName, "localsplusDumpFileName") 45 | ["--localsplus-dump-file"] 46 | ("The dump file to run tests localsplus tests against."); 47 | 48 | cli |= Opt(configData.symbolPath, "symbolPath") 49 | ["--symbol-path"] 50 | ("Location to look for Python symbols. (PDB files)"); 51 | 52 | // Send our command line changes back into Catch. 53 | session.cli(cli); 54 | 55 | int returnCode = session.applyCommandLine(argc, argv); 56 | if (returnCode != 0) // Indicates a command line error 57 | return returnCode; 58 | 59 | // Run the tests! 60 | return session.run(); 61 | } -------------------------------------------------------------------------------- /test/scripts/break.py: -------------------------------------------------------------------------------- 1 | import win32debug 2 | 3 | # If we are being run directly, test out some debug functionality. 4 | if __name__ == "__main__": 5 | if win32debug.is_debugger_present(): 6 | win32debug.output_debug_string("A debugger is attached!\n") 7 | else: 8 | win32debug.output_debug_string("No debugger attached.\n") 9 | 10 | win32debug.output_debug_string("About to trigger break.\n") 11 | win32debug.debug_break() 12 | -------------------------------------------------------------------------------- /test/scripts/fibonacci_test.py: -------------------------------------------------------------------------------- 1 | """This test exercises PyExt's stack frame logic. 2 | 3 | It is intentionally a bad implementation since the test suite uses it to make 4 | assertions about the number of frames on the stack, and the line numbers of 5 | each frame. 6 | 7 | Note: When modifying this file, changes to line numbers may require the tests 8 | to be updated. 9 | """ 10 | 11 | import win32debug 12 | 13 | def recursive_fib(n): 14 | """Returns the nth fibonacci number, calulated recursively.""" 15 | 16 | # Write a highest point of the call stack. 17 | if n == 1: 18 | win32debug.dump_process("fibonacci_test.dmp") # Asserted line 18. 19 | exit(0) 20 | 21 | if n < 2: 22 | return n 23 | else: 24 | return recursive_fib(n-1) + recursive_fib(n-2) # Asserted line 24. 25 | 26 | 27 | if __name__ == '__main__': 28 | print(recursive_fib(500)) # Asserted line 28. -------------------------------------------------------------------------------- /test/scripts/localsplus_test.py: -------------------------------------------------------------------------------- 1 | import win32debug, sys, os 2 | 3 | 4 | class A(object): 5 | def __init__(self, a): 6 | self.a = a 7 | 8 | def f_cellvar(self, param1, param2): 9 | local1 = 5 10 | # c is cell variable because it is used in f_cellfreevar 11 | cell1 = {1: 2, 3: 4} 12 | 13 | def f_cellfreevar(param): 14 | # cell1 is free variable because it is defined outside f_cellfreevar 15 | # cell2 is cell variable because it is used in f_freevar 16 | cell2 = param + cell1[1] 17 | local2 = 6 18 | 19 | def f_freevar(): 20 | x = cell2 21 | win32debug.dump_process("localsplus_test.dmp") 22 | 23 | try: 24 | f_freevar() 25 | except Exception as e: 26 | # e is not initialized (null in localsplus) when creating the dump! 27 | pass 28 | 29 | assert 'cell2' in f_cellfreevar.__code__.co_cellvars 30 | assert 'cell1' in f_cellfreevar.__code__.co_freevars 31 | assert f_cellfreevar.__code__.co_nlocals == 4 # param, local2, f_freevar, e 32 | f_cellfreevar(7) 33 | 34 | 35 | def main(a): 36 | a.f_cellvar("test", [1,2,3]) 37 | 38 | 39 | main(A(1)) 40 | -------------------------------------------------------------------------------- /test/scripts/object_details.py: -------------------------------------------------------------------------------- 1 | import win32debug, sys, os 2 | 3 | 4 | class D(object): 5 | """dict""" 6 | def __init__(self, d1, d2): 7 | self.d0 = 0 8 | self.d1 = d1 9 | self.d2 = d2 10 | del self.d0 # d0 is still in the object's dict but has no value 11 | 12 | def f(self): 13 | self.d_uninitialized = 1 14 | 15 | 16 | class S(object): 17 | """slots""" 18 | __slots__ = 'slot1', 'slot2', 'slot_uninitialized' 19 | 20 | def __init__(self, s1, s2): 21 | self.slot1 = s1 22 | self.slot2 = s2 23 | 24 | 25 | class DsubD(D): 26 | """dict, parent dict""" 27 | def __init__(self, d1, d2, d3): 28 | D.__init__(self, d1, d2) 29 | self.d3 = d3 30 | 31 | 32 | class SsubS(S): 33 | """slots, parent slots""" 34 | __slots__ = 'slot3' 35 | 36 | def __init__(self, s1, s2, s3): 37 | S.__init__(self, s1, s2) 38 | self.slot3 = s3 39 | 40 | 41 | class DsubS(S): 42 | """dict, parent slots""" 43 | def __init__(self, s1, s2, d3): 44 | S.__init__(self, s1, s2) 45 | self.d3 = d3 46 | 47 | 48 | class SsubD(D): 49 | """slots, parent dict""" 50 | __slots__ = 'slot3' 51 | 52 | def __init__(self, d1, d2, s3): 53 | D.__init__(self, d1, d2) 54 | self.slot3 = s3 55 | 56 | 57 | class SsubDS(D, S): 58 | """slots, parents dict and slots""" 59 | __slots__ = 'slot3' 60 | 61 | def __init__(self, d1, d2, s1, s2, s3): 62 | D.__init__(self, d1, d2) 63 | S.__init__(self, s1, s2) 64 | self.slot3 = s3 65 | 66 | 67 | class NegDictOffset(tuple): 68 | """inheriting from tuple leads to a negative tp_dictoffset""" 69 | 70 | def __init__(self, tupleValue): 71 | self.attr = 'test' 72 | 73 | 74 | class ManagedDictResolved(object): 75 | """managed dict has two modes: 76 | - only values 77 | - fully resolved PyDictObject 78 | 79 | We are trying to trigger the second one with this class. 80 | """ 81 | def __init__(self): 82 | # Observation: The more attributes an object has, the more likely the dict is not stored 83 | # as values only but as PyDictObject, which triggers another branch in the code. 84 | for i in range(32): 85 | setattr(self, 'a%s' % i, i) 86 | 87 | 88 | d = D(1, 2) 89 | s = S(1, 2) 90 | dsubd = DsubD(1, 2, 3) 91 | ssubs = SsubS(1, 2, 3) 92 | dsubs = DsubS(1, 2, 3) 93 | ssubd = SsubD(1, 2, 3) 94 | ssubds = SsubDS(1, 2, 3, 4, 5) 95 | negDictOffset = NegDictOffset((1, 2, 3)) 96 | manDictRes = ManagedDictResolved() 97 | win32debug.dump_process("object_details.dmp") 98 | -------------------------------------------------------------------------------- /test/scripts/object_types.py: -------------------------------------------------------------------------------- 1 | import win32debug, sys, os 2 | 3 | string_obj = "TestString123" 4 | big_string_obj = string_obj * 100 5 | bytes_obj = b"TestBytes123" # Python 2: str, Python 3: bytes 6 | unicode_obj = u"TestUnicode" # Python 2: unicode, Python 3: str 7 | byte_array_object = bytearray(b'TestBytearray123') 8 | int_obj = int(1) 9 | long_obj = 123456789012345678901234567890123456789012345678901234567890 10 | all_long_obj = (0, -123456, 123456, 987654321987654321987654321, -987654321987654321987654321) 11 | float_obj = 3.1415 12 | complex_obj = complex(1.5, -2.25) 13 | bool_true_obj = True 14 | bool_false_obj = False 15 | none_obj = None 16 | type_obj = dict 17 | not_implemented_obj = NotImplemented 18 | 19 | def test_function(x): 20 | """Some DocString""" 21 | return x*x 22 | 23 | func_obj = test_function 24 | 25 | list_obj = [string_obj, int_obj, long_obj] 26 | 27 | tuple_obj = (string_obj, int_obj, long_obj) 28 | 29 | set_obj = { string_obj, int_obj, long_obj } 30 | 31 | dict_obj = { 32 | "string_obj": string_obj, 33 | "int_obj": int_obj, 34 | "long_obj": long_obj, 35 | } 36 | 37 | win32debug.dump_process("object_types.dmp") 38 | -------------------------------------------------------------------------------- /test/scripts/python_installations.py: -------------------------------------------------------------------------------- 1 | """A small function for listing installed Python instances on a Windows machine as defined by PEP 514.""" 2 | import os, winreg, collections 3 | 4 | # https://www.python.org/dev/peps/pep-0514/ 5 | 6 | def _enum_keys(key): 7 | nsub, nval, modified = winreg.QueryInfoKey(key) 8 | for i in range(nsub): 9 | yield winreg.EnumKey(key, i) 10 | 11 | def _get_value(key, value_name): 12 | try: 13 | value, type = winreg.QueryValueEx(key, value_name) 14 | return value 15 | except FileNotFoundError: 16 | return None 17 | 18 | """A simple data-only object that represents a Python installation on disk.""" 19 | PythonInstallation = collections.namedtuple("PythonInstallation", 20 | "company name support_url version sys_version sys_arch exec_path") 21 | 22 | def _create_python_installation(company, tag, tag_key): 23 | name = _get_value(tag_key, "DisplayName") or ("Python " + tag) 24 | url = _get_value(tag_key, "SupportUrl") or "http://www.python.org/" 25 | version = _get_value(tag_key, "Version") or tag[:3] 26 | sys_version = _get_value(tag_key, "SysVersion") or tag[:3] 27 | sys_arch = _get_value(tag_key, "SysArchitecture") or None 28 | 29 | exec_path = None 30 | try: 31 | with winreg.OpenKey(tag_key, "InstallPath") as ip_key: 32 | exec_path = (_get_value(ip_key, "ExecutablePath") 33 | or os.path.join(_get_value(ip_key, None), "python.exe")) 34 | except FileNotFoundError: 35 | pass 36 | 37 | return PythonInstallation(company, name, url, version, sys_version, 38 | sys_arch, exec_path) 39 | 40 | def get_python_installations(): 41 | """Returns a list of python executables on the machine.""" 42 | installations = set() 43 | search_keys = [ 44 | (winreg.HKEY_CURRENT_USER, r"Software\Python", 0), 45 | (winreg.HKEY_LOCAL_MACHINE, r"Software\Python", winreg.KEY_WOW64_64KEY), 46 | (winreg.HKEY_LOCAL_MACHINE, r"Software\Python", winreg.KEY_WOW64_32KEY), 47 | ] 48 | 49 | for hive, key, flags in search_keys: 50 | try: 51 | with winreg.OpenKeyEx(hive, key, access=winreg.KEY_READ | flags) as root_key: 52 | for company in _enum_keys(root_key): 53 | with winreg.OpenKey(root_key, company) as company_key: 54 | for tag in _enum_keys(company_key): 55 | with winreg.OpenKey(company_key, tag) as tag_key: 56 | installations.add(_create_python_installation(company, tag, tag_key)) 57 | except FileNotFoundError: 58 | continue 59 | 60 | return installations 61 | 62 | if __name__ == "__main__": 63 | print("\n".join(repr(x) for x in get_python_installations())) -------------------------------------------------------------------------------- /test/scripts/run_all_tests.py: -------------------------------------------------------------------------------- 1 | """Runs the PyExtTest project against all installed python instances.""" 2 | import sys, subprocess, python_installations 3 | 4 | if __name__ == '__main__': 5 | num_failed_tests = 0 6 | for installation in python_installations.get_python_installations(): 7 | # Skip versions with no executable. 8 | if installation.exec_path == None: 9 | print("Skipping (no executable)", installation, end="\n\n", flush=True) 10 | continue 11 | 12 | # Only test against official CPython installations. 13 | if installation.company != "PythonCore": 14 | print("Skipping (PythonCore)", installation, end="\n\n", flush=True) 15 | continue 16 | 17 | # Also skip versions before 2.7 since they don't have symbols. 18 | version = tuple(int(n) for n in installation.sys_version.split(".")) 19 | if version < (2, 7): 20 | print("Skipping (too old)", installation, end="\n\n", flush=True) 21 | continue 22 | 23 | # Create the dump files. 24 | print("Creating dump files with python executable:", installation.exec_path, flush=True) 25 | subprocess.check_call([installation.exec_path, "object_types.py"]) 26 | subprocess.check_call([installation.exec_path, "fibonacci_test.py"]) 27 | subprocess.check_call([installation.exec_path, "object_details.py"]) 28 | subprocess.check_call([installation.exec_path, "localsplus_test.py"]) 29 | 30 | # Run the tests against the dump files. 31 | print("Running tests with python executable:", installation.exec_path, flush=True) 32 | py_ext_test_exe = sys.argv[1] if len(sys.argv) > 1 else "../../x64/Debug/PyExtTest.exe" 33 | num_failed_tests += subprocess.call(py_ext_test_exe) 34 | 35 | sys.exit(num_failed_tests) 36 | -------------------------------------------------------------------------------- /test/scripts/win32debug.py: -------------------------------------------------------------------------------- 1 | """Wrappers around various Win32 APIs debugging. 2 | Allows writing dump files, triggering debug breaks, detecting whether a 3 | debugger is attached and printing messages to the debugger. 4 | """ 5 | 6 | import os, sys, ctypes, msvcrt, threading 7 | 8 | 9 | class MiniDumpType: 10 | """MiniDump flags as defined on `MSDN `_""" 11 | 12 | Normal = 0x00000000 13 | WithDataSegs = 0x00000001 14 | WithFullMemory = 0x00000002 15 | WithHandleData = 0x00000004 16 | FilterMemory = 0x00000008 17 | ScanMemory = 0x00000010 18 | WithUnloadedModules = 0x00000020 19 | WithIndirectlyReferencedMemory = 0x00000040 20 | FilterModulePaths = 0x00000080 21 | WithProcessThreadData = 0x00000100 22 | WithPrivateReadWriteMemory = 0x00000200 23 | WithoutOptionalData = 0x00000400 24 | WithFullMemoryInfo = 0x00000800 25 | WithThreadInfo = 0x00001000 26 | WithCodeSegs = 0x00002000 27 | WithoutAuxiliaryState = 0x00004000 28 | WithFullAuxiliaryState = 0x00008000 29 | WithPrivateWriteCopyMemory = 0x00010000 30 | IgnoreInaccessibleMemory = 0x00020000 31 | WithTokenInformation = 0x00040000 32 | WithModuleHeaders = 0x00080000 33 | FilterTriage = 0x00100000 34 | ValidTypeFlags = 0x001fffff 35 | 36 | 37 | class StandardAccessRights: 38 | """Defines the access rights common to most types of Win32 handle.""" 39 | """``_""" 40 | 41 | DELETE = 0x00010000 42 | """Required to delete the object.""" 43 | 44 | READ_CONTROL = 0x00020000 45 | """Required to read information in the security descriptor for the object, not including the information in the SACL.""" 46 | """To read or write the SACL, you must request the ACCESS_SYSTEM_SECURITY access right. For more information, see SACL Access Right.""" 47 | 48 | SYNCHRONIZE = 0x00100000 49 | """The right to use the object for synchronization. This enables a thread to wait until the object is in the signaled state.""" 50 | 51 | WRITE_DAC = 0x00040000 52 | """Required to modify the DACL in the security descriptor for the object.""" 53 | 54 | WRITE_OWNER = 0x00080000 55 | """Required to change the owner in the security descriptor for the object.""" 56 | 57 | STANDARD_RIGHTS_ALL = DELETE | READ_CONTROL | WRITE_DAC | WRITE_OWNER | SYNCHRONIZE 58 | """Combines DELETE, READ_CONTROL, WRITE_DAC, WRITE_OWNER, and SYNCHRONIZE access.""" 59 | 60 | STANDARD_RIGHTS_REQUIRED = DELETE | READ_CONTROL | WRITE_DAC | WRITE_OWNER 61 | """Combines DELETE, READ_CONTROL, WRITE_DAC, and WRITE_OWNER access.""" 62 | 63 | STANDARD_RIGHTS_EXECUTE = READ_CONTROL 64 | STANDARD_RIGHTS_READ = READ_CONTROL 65 | STANDARD_RIGHTS_WRITE = READ_CONTROL 66 | 67 | 68 | class ProcessAccessRights: 69 | """Defines the access rights specific to Win32 process handles.""" 70 | """``_""" 71 | 72 | PROCESS_CREATE_PROCESS = 0x0080 73 | """Required to create a process.""" 74 | 75 | PROCESS_CREATE_THREAD = 0x0002 76 | """Required to create a thread.""" 77 | 78 | PROCESS_DUP_HANDLE = 0x0040 79 | """Required to duplicate a handle using DuplicateHandle.""" 80 | 81 | PROCESS_QUERY_INFORMATION = 0x0400 82 | """Required to retrieve certain information about a process, such as its token, exit code, and priority class = see OpenProcessToken).""" 83 | 84 | PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 85 | """Required to retrieve certain information about a process. (GetExitCodeProcess, GetPriorityClass, IsProcessInJob, QueryFullProcessImageName).""" 86 | 87 | PROCESS_SET_INFORMATION = 0x0200 88 | """Required to set certain information about a process, such as its priority class = see SetPriorityClass).""" 89 | 90 | PROCESS_SET_QUOTA = 0x0100 91 | """Required to set memory limits using SetProcessWorkingSetSize.""" 92 | 93 | PROCESS_SUSPEND_RESUME = 0x0800 94 | """Required to suspend or resume a process.""" 95 | 96 | PROCESS_TERMINATE = 0x0001 97 | """Required to terminate a process using TerminateProcess.""" 98 | 99 | PROCESS_VM_OPERATION = 0x0008 100 | """Required to perform an operation on the address space of a process = see VirtualProtectEx and WriteProcessMemory).""" 101 | 102 | PROCESS_VM_READ = 0x0010 103 | """Required to read memory in a process using ReadProcessMemory.""" 104 | 105 | PROCESS_VM_WRITE = 0x0020 106 | """Required to write to memory in a process using WriteProcessMemory.""" 107 | 108 | SYNCHRONIZE = 0x00100000 109 | """Required to wait for the process to terminate using the wait functions.""" 110 | 111 | PROCESS_ALL_ACCESS = StandardAccessRights.STANDARD_RIGHTS_REQUIRED | StandardAccessRights.SYNCHRONIZE | 0xFFF 112 | """All possible access rights for a process object.""" 113 | 114 | 115 | class Win32Error(Exception): 116 | """Signifies an error when calling a Win32 function. Includes the function name and value of GetLastError().""" 117 | def __init__(self, function_name, last_error=None): 118 | if last_error == None: 119 | last_error = ctypes.GetLastError() 120 | 121 | self.function_name, self.last_error = function_name, last_error 122 | super(Win32Error, self).__init__(str(function_name) + " failed with GetLastError=" + str(last_error)) 123 | 124 | 125 | def _mini_dump_write_dump(dump_file_name, process_id, dump_type): 126 | """Wraps dbghelp.dll's MiniDumpWriteDump.""" 127 | 128 | # TODO: Determine the rights needed based on dump_type. 129 | process_rights = ProcessAccessRights.PROCESS_QUERY_INFORMATION | ProcessAccessRights.PROCESS_VM_READ 130 | process_handle = ctypes.windll.kernel32.OpenProcess(process_rights, False, process_id) 131 | if process_handle == 0: 132 | raise Win32Error("OpenProcess") 133 | 134 | with open(dump_file_name, "w+") as dump_file: 135 | dump_file_handle = msvcrt.get_osfhandle(dump_file.fileno()) 136 | 137 | succeeded = ctypes.windll.dbghelp.MiniDumpWriteDump(process_handle, process_id, dump_file_handle, dump_type, None, None, None) 138 | if not succeeded: 139 | raise Win32Error("MiniDumpWriteDump") 140 | 141 | 142 | def _launch_and_wait(target, args=()): 143 | """Runs a function on another thread, waits for it to finish, and returns the result or raises.""" 144 | def closure(): 145 | try: 146 | closure.result = target(*args) 147 | except Exception as ex: 148 | closure.exception = ex 149 | 150 | closure.result = closure.exception = None 151 | t = threading.Thread(target=closure) 152 | t.start() 153 | t.join() 154 | 155 | if closure.exception != None: 156 | raise closure.exception 157 | 158 | return closure.result 159 | 160 | 161 | def _generate_default_dump_filename(): 162 | """Generates a descriptive dump file name if one isn't provided.""" 163 | main_filename = os.path.realpath(sys.argv[0]) if sys.argv[0] else "dump" 164 | python_ver_str = ".".join(str(x) for x in sys.version_info[:3]) 165 | base_filename_str = os.path.splitext(os.path.basename(main_filename))[0] 166 | bittess_str = 'x64' if sys.maxsize > 2**32 else 'x86' 167 | return '.'.join([base_filename_str, python_ver_str, bittess_str, "dmp"]) 168 | 169 | 170 | def dump_process(dump_file_name=None, process_id=None): 171 | """Writes a dump of a given pid (or the current process if not specified) to a file.""" 172 | 173 | # Default args. 174 | process_id = process_id or ctypes.windll.kernel32.GetCurrentProcessId() 175 | dump_file_name = dump_file_name or _generate_default_dump_filename() 176 | 177 | dump_type = MiniDumpType.WithFullMemory | MiniDumpType.WithFullMemoryInfo | MiniDumpType.WithThreadInfo 178 | 179 | # Do the dump from a separate thread so the current thread's stack is captured properly. 180 | _launch_and_wait(_mini_dump_write_dump, (dump_file_name, process_id, dump_type)) 181 | 182 | 183 | def _to_w_string(string_object): 184 | """Returns the object unchanged if it is already unicode (wide-string), otherwise, converts to unicode.""" 185 | return string_object if (sys.version_info.major != 2) else unicode(string_object) 186 | 187 | 188 | def is_debugger_present(): 189 | """Returns true when the current process has a debugger attached.""" 190 | return ctypes.windll.kernel32.IsDebuggerPresent() != 0 191 | 192 | 193 | def output_debug_string(debug_string): 194 | """Prints a string in any attached debuggers or tools that intercept debug strings, such as DbgView.exe""" 195 | ctypes.windll.kernel32.OutputDebugStringW(_to_w_string(debug_string)) 196 | 197 | 198 | def debug_break(): 199 | """Triggers a breakpoint in an attached debugger, or halts the process if a debugger is not attached.""" 200 | ctypes.windll.kernel32.DebugBreak() 201 | 202 | 203 | # If we are being run directly, write a dump of the current process. 204 | if __name__ == "__main__": 205 | dump_process(__file__ + ".dmp") 206 | --------------------------------------------------------------------------------