diff --git a/Include/internal/pycore_runtime.h b/Include/internal/pycore_runtime.h
index 7c705d1224f915..cfe2de5a0c902e 100644
--- a/Include/internal/pycore_runtime.h
+++ b/Include/internal/pycore_runtime.h
@@ -55,74 +55,81 @@ typedef struct _Py_DebugOffsets {
     uint64_t version;
     // Runtime state offset;
     struct _runtime_state {
-        off_t finalizing;
-        off_t interpreters_head;
+        uint64_t finalizing;
+        uint64_t interpreters_head;
     } runtime_state;
 
     // Interpreter state offset;
     struct _interpreter_state {
-        off_t next;
-        off_t threads_head;
-        off_t gc;
-        off_t imports_modules;
-        off_t sysdict;
-        off_t builtins;
-        off_t ceval_gil;
-        off_t gil_runtime_state_locked;
-        off_t gil_runtime_state_holder;
+        uint64_t next;
+        uint64_t threads_head;
+        uint64_t gc;
+        uint64_t imports_modules;
+        uint64_t sysdict;
+        uint64_t builtins;
+        uint64_t ceval_gil;
+        uint64_t gil_runtime_state_locked;
+        uint64_t gil_runtime_state_holder;
     } interpreter_state;
 
     // Thread state offset;
     struct _thread_state{
-        off_t prev;
-        off_t next;
-        off_t interp;
-        off_t current_frame;
-        off_t thread_id;
-        off_t native_thread_id;
+        uint64_t prev;
+        uint64_t next;
+        uint64_t interp;
+        uint64_t current_frame;
+        uint64_t thread_id;
+        uint64_t native_thread_id;
     } thread_state;
 
     // InterpreterFrame offset;
     struct _interpreter_frame {
-        off_t previous;
-        off_t executable;
-        off_t instr_ptr;
-        off_t localsplus;
-        off_t owner;
+        uint64_t previous;
+        uint64_t executable;
+        uint64_t instr_ptr;
+        uint64_t localsplus;
+        uint64_t owner;
     } interpreter_frame;
 
     // CFrame offset;
     struct _cframe {
-        off_t current_frame;
-        off_t previous;
+        uint64_t current_frame;
+        uint64_t previous;
     } cframe;
 
     // Code object offset;
     struct _code_object {
-        off_t filename;
-        off_t name;
-        off_t linetable;
-        off_t firstlineno;
-        off_t argcount;
-        off_t localsplusnames;
-        off_t localspluskinds;
-        off_t co_code_adaptive;
+        uint64_t filename;
+        uint64_t name;
+        uint64_t linetable;
+        uint64_t firstlineno;
+        uint64_t argcount;
+        uint64_t localsplusnames;
+        uint64_t localspluskinds;
+        uint64_t co_code_adaptive;
     } code_object;
 
     // PyObject offset;
     struct _pyobject {
-        off_t ob_type;
+        uint64_t ob_type;
     } pyobject;
 
     // PyTypeObject object offset;
     struct _type_object {
-        off_t tp_name;
+        uint64_t tp_name;
     } type_object;
 
     // PyTuple object offset;
     struct _tuple_object {
-        off_t ob_item;
+        uint64_t ob_item;
     } tuple_object;
+
+    // Unicode object offset;
+    struct _unicode_object {
+        uint64_t state;
+        uint64_t length;
+        size_t asciiobject_size;
+    } unicode_object;
 } _Py_DebugOffsets;
 
 /* Full Python runtime state */
diff --git a/Include/internal/pycore_runtime_init.h b/Include/internal/pycore_runtime_init.h
index be81604d653814..b0acaa193444ff 100644
--- a/Include/internal/pycore_runtime_init.h
+++ b/Include/internal/pycore_runtime_init.h
@@ -83,6 +83,11 @@ extern PyTypeObject _PyExc_MemoryError;
             .tuple_object = { \
                 .ob_item = offsetof(PyTupleObject, ob_item), \
             }, \
+            .unicode_object = { \
+                .state = offsetof(PyUnicodeObject, _base._base.state), \
+                .length = offsetof(PyUnicodeObject, _base._base.length), \
+                .asciiobject_size = sizeof(PyASCIIObject), \
+            }, \
         }, \
         .allocators = { \
             .standard = _pymem_allocators_standard_INIT(runtime), \
diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py
new file mode 100644
index 00000000000000..86c07de507e39c
--- /dev/null
+++ b/Lib/test/test_external_inspection.py
@@ -0,0 +1,84 @@
+import unittest
+import os
+import textwrap
+import importlib
+import sys
+from test.support import os_helper, SHORT_TIMEOUT
+from test.support.script_helper import make_script
+
+import subprocess
+
+PROCESS_VM_READV_SUPPORTED = False
+
+try:
+    from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
+    from _testexternalinspection import get_stack_trace
+except ImportError:
+    unittest.skip("Test only runs when _testexternalinspection is available")
+
+def _make_test_script(script_dir, script_basename, source):
+    to_return = make_script(script_dir, script_basename, source)
+    importlib.invalidate_caches()
+    return to_return
+
+class TestGetStackTrace(unittest.TestCase):
+
+    @unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux", "Test only runs on Linux and MacOS")
+    @unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support")
+    def test_remote_stack_trace(self):
+        # Spawn a process with some realistic Python code
+        script = textwrap.dedent("""\
+            import time, sys, os
+            def bar():
+                for x in range(100):
+                    if x == 50:
+                        baz()
+            def baz():
+                foo()
+
+            def foo():
+                fifo = sys.argv[1]
+                with open(sys.argv[1], "w") as fifo:
+                    fifo.write("ready")
+                time.sleep(1000)
+
+            bar()
+            """)
+        stack_trace = None
+        with os_helper.temp_dir() as work_dir:
+            script_dir = os.path.join(work_dir, "script_pkg")
+            os.mkdir(script_dir)
+            fifo = f"{work_dir}/the_fifo"
+            os.mkfifo(fifo)
+            script_name = _make_test_script(script_dir, 'script', script)
+            try:
+                p = subprocess.Popen([sys.executable, script_name,  str(fifo)])
+                with open(fifo, "r") as fifo_file:
+                    response = fifo_file.read()
+                self.assertEqual(response, "ready")
+                stack_trace = get_stack_trace(p.pid)
+            except PermissionError:
+                self.skipTest("Insufficient permissions to read the stack trace")
+            finally:
+                os.remove(fifo)
+                p.kill()
+                p.terminate()
+                p.wait(timeout=SHORT_TIMEOUT)
+
+
+            expected_stack_trace = [
+                'foo',
+                'baz',
+                'bar',
+                '<module>'
+            ]
+            self.assertEqual(stack_trace, expected_stack_trace)
+
+    @unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux", "Test only runs on Linux and MacOS")
+    @unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support")
+    def test_self_trace(self):
+        stack_trace = get_stack_trace(os.getpid())
+        self.assertEqual(stack_trace[0], "test_self_trace")
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Modules/Setup b/Modules/Setup
index 8ad9a5aebbfcaa..cd1cf24c25d406 100644
--- a/Modules/Setup
+++ b/Modules/Setup
@@ -285,6 +285,7 @@ PYTHONPATH=$(COREPYTHONPATH)
 #_testcapi _testcapimodule.c
 #_testimportmultiple _testimportmultiple.c
 #_testmultiphase _testmultiphase.c
+#_testexternalinspection _testexternalinspection.c
 #_testsinglephase _testsinglephase.c
 
 # ---
diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in
index e98775a4808765..73b082691a3fd4 100644
--- a/Modules/Setup.stdlib.in
+++ b/Modules/Setup.stdlib.in
@@ -171,6 +171,7 @@
 @MODULE__TESTIMPORTMULTIPLE_TRUE@_testimportmultiple _testimportmultiple.c
 @MODULE__TESTMULTIPHASE_TRUE@_testmultiphase _testmultiphase.c
 @MODULE__TESTMULTIPHASE_TRUE@_testsinglephase _testsinglephase.c
+@MODULE__TESTEXTERNALINSPECTION_TRUE@_testexternalinspection _testexternalinspection.c
 @MODULE__CTYPES_TEST_TRUE@_ctypes_test _ctypes/_ctypes_test.c
 
 # Limited API template modules; must be built as shared modules.
diff --git a/Modules/_testexternalinspection.c b/Modules/_testexternalinspection.c
new file mode 100644
index 00000000000000..19ee3b8dd1428d
--- /dev/null
+++ b/Modules/_testexternalinspection.c
@@ -0,0 +1,629 @@
+#define _GNU_SOURCE
+
+#ifdef __linux__
+#    include <elf.h>
+#    include <sys/uio.h>
+#    if INTPTR_MAX == INT64_MAX
+#        define Elf_Ehdr Elf64_Ehdr
+#        define Elf_Shdr Elf64_Shdr
+#        define Elf_Phdr Elf64_Phdr
+#    else
+#        define Elf_Ehdr Elf32_Ehdr
+#        define Elf_Shdr Elf32_Shdr
+#        define Elf_Phdr Elf32_Phdr
+#    endif
+#    include <sys/mman.h>
+#endif
+
+#ifdef __APPLE__
+#    include <libproc.h>
+#    include <mach-o/fat.h>
+#    include <mach-o/loader.h>
+#    include <mach-o/nlist.h>
+#    include <mach/mach.h>
+#    include <mach/mach_vm.h>
+#    include <mach/machine.h>
+#    include <sys/mman.h>
+#    include <sys/proc.h>
+#    include <sys/sysctl.h>
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/param.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#ifndef Py_BUILD_CORE_BUILTIN
+#    define Py_BUILD_CORE_MODULE 1
+#endif
+#include "Python.h"
+#include <internal/pycore_runtime.h>
+
+#ifndef HAVE_PROCESS_VM_READV
+#    define HAVE_PROCESS_VM_READV 0
+#endif
+
+#ifdef __APPLE__
+static void*
+analyze_macho64(mach_port_t proc_ref, void* base, void* map)
+{
+    struct mach_header_64* hdr = (struct mach_header_64*)map;
+    int ncmds = hdr->ncmds;
+
+    int cmd_cnt = 0;
+    struct segment_command_64* cmd = map + sizeof(struct mach_header_64);
+
+    mach_vm_size_t size = 0;
+    mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
+    mach_vm_address_t address = (mach_vm_address_t)base;
+    vm_region_basic_info_data_64_t region_info;
+    mach_port_t object_name;
+
+    for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) {
+        if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__DATA") == 0) {
+            while (cmd->filesize != size) {
+                address += size;
+                if (mach_vm_region(
+                            proc_ref,
+                            &address,
+                            &size,
+                            VM_REGION_BASIC_INFO_64,
+                            (vm_region_info_t)&region_info,  // cppcheck-suppress [uninitvar]
+                            &count,
+                            &object_name)
+                    != KERN_SUCCESS)
+                {
+                    PyErr_SetString(PyExc_RuntimeError, "Cannot get any more VM maps.\n");
+                    return NULL;
+                }
+            }
+            base = (void*)address - cmd->vmaddr;
+
+            int nsects = cmd->nsects;
+            struct section_64* sec =
+                    (struct section_64*)((void*)cmd + sizeof(struct segment_command_64));
+            for (int j = 0; j < nsects; j++) {
+                if (strcmp(sec[j].sectname, "PyRuntime") == 0) {
+                    return base + sec[j].addr;
+                }
+            }
+            cmd_cnt++;
+        }
+
+        cmd = (struct segment_command_64*)((void*)cmd + cmd->cmdsize);
+    }
+    return NULL;
+}
+
+static void*
+analyze_macho(char* path, void* base, mach_vm_size_t size, mach_port_t proc_ref)
+{
+    int fd = open(path, O_RDONLY);
+    if (fd == -1) {
+        PyErr_Format(PyExc_RuntimeError, "Cannot open binary %s\n", path);
+        return NULL;
+    }
+
+    struct stat fs;
+    if (fstat(fd, &fs) == -1) {
+        PyErr_Format(PyExc_RuntimeError, "Cannot get size of binary %s\n", path);
+        close(fd);
+        return NULL;
+    }
+
+    void* map = mmap(0, fs.st_size, PROT_READ, MAP_SHARED, fd, 0);
+    if (map == MAP_FAILED) {
+        PyErr_Format(PyExc_RuntimeError, "Cannot map binary %s\n", path);
+        close(fd);
+        return NULL;
+    }
+
+    void* result = NULL;
+
+    struct mach_header_64* hdr = (struct mach_header_64*)map;
+    switch (hdr->magic) {
+        case MH_MAGIC:
+        case MH_CIGAM:
+        case FAT_MAGIC:
+        case FAT_CIGAM:
+            PyErr_SetString(PyExc_RuntimeError, "32-bit Mach-O binaries are not supported");
+            break;
+        case MH_MAGIC_64:
+        case MH_CIGAM_64:
+            result = analyze_macho64(proc_ref, base, map);
+            break;
+        default:
+            PyErr_SetString(PyExc_RuntimeError, "Unknown Mach-O magic");
+            break;
+    }
+
+    munmap(map, fs.st_size);
+    if (close(fd) != 0) {
+        PyErr_SetFromErrno(PyExc_OSError);
+    }
+    return result;
+}
+
+static mach_port_t
+pid_to_task(pid_t pid)
+{
+    mach_port_t task;
+    kern_return_t result;
+
+    result = task_for_pid(mach_task_self(), pid, &task);
+    if (result != KERN_SUCCESS) {
+        PyErr_Format(PyExc_PermissionError, "Cannot get task for PID %d", pid);
+        return 0;
+    }
+    return task;
+}
+
+static void*
+get_py_runtime_macos(pid_t pid)
+{
+    mach_vm_address_t address = 0;
+    mach_vm_size_t size = 0;
+    mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
+    vm_region_basic_info_data_64_t region_info;
+    mach_port_t object_name;
+
+    mach_port_t proc_ref = pid_to_task(pid);
+    if (proc_ref == 0) {
+        PyErr_SetString(PyExc_PermissionError, "Cannot get task for PID");
+        return NULL;
+    }
+
+    int match_found = 0;
+    char map_filename[MAXPATHLEN + 1];
+    void* result_address = NULL;
+    while (mach_vm_region(
+                   proc_ref,
+                   &address,
+                   &size,
+                   VM_REGION_BASIC_INFO_64,
+                   (vm_region_info_t)&region_info,
+                   &count,
+                   &object_name)
+           == KERN_SUCCESS)
+    {
+        int path_len = proc_regionfilename(pid, address, map_filename, MAXPATHLEN);
+        if (path_len == 0) {
+            address += size;
+            continue;
+        }
+
+        char* filename = strrchr(map_filename, '/');
+        if (filename != NULL) {
+            filename++;  // Move past the '/'
+        } else {
+            filename = map_filename;  // No path, use the whole string
+        }
+
+        // Check if the filename starts with "python" or "libpython"
+        if (!match_found && strncmp(filename, "python", 6) == 0) {
+            match_found = 1;
+            result_address = analyze_macho(map_filename, (void*)address, size, proc_ref);
+        }
+        if (strncmp(filename, "libpython", 9) == 0) {
+            match_found = 1;
+            result_address = analyze_macho(map_filename, (void*)address, size, proc_ref);
+            break;
+        }
+
+        address += size;
+    }
+    return result_address;
+}
+#endif
+
+#ifdef __linux__
+void*
+find_python_map_start_address(pid_t pid, char* result_filename)
+{
+    char maps_file_path[64];
+    sprintf(maps_file_path, "/proc/%d/maps", pid);
+
+    FILE* maps_file = fopen(maps_file_path, "r");
+    if (maps_file == NULL) {
+        PyErr_SetFromErrno(PyExc_OSError);
+        return NULL;
+    }
+
+    int match_found = 0;
+
+    char line[256];
+    char map_filename[PATH_MAX];
+    void* result_address = 0;
+    while (fgets(line, sizeof(line), maps_file) != NULL) {
+        unsigned long start_address = 0;
+        sscanf(line, "%lx-%*x %*s %*s %*s %*s %s", &start_address, map_filename);
+        char* filename = strrchr(map_filename, '/');
+        if (filename != NULL) {
+            filename++;  // Move past the '/'
+        } else {
+            filename = map_filename;  // No path, use the whole string
+        }
+
+        // Check if the filename starts with "python" or "libpython"
+        if (!match_found && strncmp(filename, "python", 6) == 0) {
+            match_found = 1;
+            result_address = (void*)start_address;
+            strcpy(result_filename, map_filename);
+        }
+        if (strncmp(filename, "libpython", 9) == 0) {
+            match_found = 1;
+            result_address = (void*)start_address;
+            strcpy(result_filename, map_filename);
+            break;
+        }
+    }
+
+    fclose(maps_file);
+
+    if (!match_found) {
+        map_filename[0] = '\0';
+    }
+
+    return result_address;
+}
+
+void*
+get_py_runtime_linux(pid_t pid)
+{
+    char elf_file[256];
+    void* start_address = (void*)find_python_map_start_address(pid, elf_file);
+
+    if (start_address == 0) {
+        PyErr_SetString(PyExc_RuntimeError, "No memory map associated with python or libpython found");
+        return NULL;
+    }
+
+    void* result = NULL;
+    void* file_memory = NULL;
+
+    int fd = open(elf_file, O_RDONLY);
+    if (fd < 0) {
+        PyErr_SetFromErrno(PyExc_OSError);
+        goto exit;
+    }
+
+    struct stat file_stats;
+    if (fstat(fd, &file_stats) != 0) {
+        PyErr_SetFromErrno(PyExc_OSError);
+        goto exit;
+    }
+
+    file_memory = mmap(NULL, file_stats.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+    if (file_memory == MAP_FAILED) {
+        PyErr_SetFromErrno(PyExc_OSError);
+        goto exit;
+    }
+
+    Elf_Ehdr* elf_header = (Elf_Ehdr*)file_memory;
+
+    Elf_Shdr* section_header_table = (Elf_Shdr*)(file_memory + elf_header->e_shoff);
+
+    Elf_Shdr* shstrtab_section = &section_header_table[elf_header->e_shstrndx];
+    char* shstrtab = (char*)(file_memory + shstrtab_section->sh_offset);
+
+    Elf_Shdr* py_runtime_section = NULL;
+    for (int i = 0; i < elf_header->e_shnum; i++) {
+        if (strcmp(".PyRuntime", shstrtab + section_header_table[i].sh_name) == 0) {
+            py_runtime_section = &section_header_table[i];
+            break;
+        }
+    }
+
+    Elf_Phdr* program_header_table = (Elf_Phdr*)(file_memory + elf_header->e_phoff);
+    // Find the first PT_LOAD segment
+    Elf_Phdr* first_load_segment = NULL;
+    for (int i = 0; i < elf_header->e_phnum; i++) {
+        if (program_header_table[i].p_type == PT_LOAD) {
+            first_load_segment = &program_header_table[i];
+            break;
+        }
+    }
+
+    if (py_runtime_section != NULL && first_load_segment != NULL) {
+        uintptr_t elf_load_addr = first_load_segment->p_vaddr
+                                  - (first_load_segment->p_vaddr % first_load_segment->p_align);
+        result = start_address + py_runtime_section->sh_addr - elf_load_addr;
+    }
+
+exit:
+    if (close(fd) != 0) {
+        PyErr_SetFromErrno(PyExc_OSError);
+    }
+    if (file_memory != NULL) {
+        munmap(file_memory, file_stats.st_size);
+    }
+    return result;
+}
+#endif
+
+ssize_t
+read_memory(pid_t pid, void* remote_address, size_t len, void* dst)
+{
+    ssize_t total_bytes_read = 0;
+#ifdef __linux__
+    struct iovec local[1];
+    struct iovec remote[1];
+    ssize_t result = 0;
+    ssize_t read = 0;
+
+    do {
+        local[0].iov_base = dst + result;
+        local[0].iov_len = len - result;
+        remote[0].iov_base = (void*)(remote_address + result);
+        remote[0].iov_len = len - result;
+
+        read = process_vm_readv(pid, local, 1, remote, 1, 0);
+        if (read < 0) {
+            PyErr_SetFromErrno(PyExc_OSError);
+            return -1;
+        }
+
+        result += read;
+    } while ((size_t)read != local[0].iov_len);
+    total_bytes_read = result;
+#elif defined(__APPLE__)
+    ssize_t result = -1;
+    kern_return_t kr = mach_vm_read_overwrite(
+            pid_to_task(pid),
+            (mach_vm_address_t)remote_address,
+            len,
+            (mach_vm_address_t)dst,
+            (mach_vm_size_t*)&result);
+
+    if (kr != KERN_SUCCESS) {
+        switch (kr) {
+            case KERN_PROTECTION_FAILURE:
+                PyErr_SetString(PyExc_PermissionError, "Not enough permissions to read memory");
+                break;
+            case KERN_INVALID_ARGUMENT:
+                PyErr_SetString(PyExc_PermissionError, "Invalid argument to mach_vm_read_overwrite");
+                break;
+            default:
+                PyErr_SetString(PyExc_RuntimeError, "Unknown error reading memory");
+        }
+        return -1;
+    }
+    total_bytes_read = len;
+#else
+    return -1;
+#endif
+    return total_bytes_read;
+}
+
+int
+read_string(pid_t pid, _Py_DebugOffsets* debug_offsets, void* address, char* buffer, Py_ssize_t size)
+{
+    Py_ssize_t len;
+    ssize_t bytes_read =
+            read_memory(pid, address + debug_offsets->unicode_object.length, sizeof(Py_ssize_t), &len);
+    if (bytes_read == -1) {
+        return -1;
+    }
+    if (len >= size) {
+        PyErr_SetString(PyExc_RuntimeError, "Buffer too small");
+        return -1;
+    }
+    size_t offset = debug_offsets->unicode_object.asciiobject_size;
+    bytes_read = read_memory(pid, address + offset, len, buffer);
+    if (bytes_read == -1) {
+        return -1;
+    }
+    buffer[len] = '\0';
+    return 0;
+}
+
+void*
+get_py_runtime(pid_t pid)
+{
+#if defined(__linux__)
+    return get_py_runtime_linux(pid);
+#elif defined(__APPLE__)
+    return get_py_runtime_macos(pid);
+#else
+    return NULL;
+#endif
+}
+
+static int
+parse_code_object(
+        int pid,
+        PyObject* result,
+        struct _Py_DebugOffsets* offsets,
+        void* address,
+        void** previous_frame)
+{
+    void* address_of_function_name;
+    read_memory(
+            pid,
+            (void*)(address + offsets->code_object.name),
+            sizeof(void*),
+            &address_of_function_name);
+
+    if (address_of_function_name == NULL) {
+        PyErr_SetString(PyExc_RuntimeError, "No function name found");
+        return -1;
+    }
+
+    char function_name[256];
+    if (read_string(pid, offsets, address_of_function_name, function_name, sizeof(function_name)) != 0) {
+        return -1;
+    }
+
+    PyObject* py_function_name = PyUnicode_FromString(function_name);
+    if (py_function_name == NULL) {
+        return -1;
+    }
+
+    if (PyList_Append(result, py_function_name) == -1) {
+        Py_DECREF(py_function_name);
+        return -1;
+    }
+    Py_DECREF(py_function_name);
+
+    return 0;
+}
+
+static int
+parse_frame_object(
+        int pid,
+        PyObject* result,
+        struct _Py_DebugOffsets* offsets,
+        void* address,
+        void** previous_frame)
+{
+    ssize_t bytes_read = read_memory(
+            pid,
+            (void*)(address + offsets->interpreter_frame.previous),
+            sizeof(void*),
+            previous_frame);
+    if (bytes_read == -1) {
+        return -1;
+    }
+
+    char owner;
+    bytes_read =
+            read_memory(pid, (void*)(address + offsets->interpreter_frame.owner), sizeof(char), &owner);
+    if (bytes_read < 0) {
+        return -1;
+    }
+
+    if (owner == FRAME_OWNED_BY_CSTACK) {
+        return 0;
+    }
+
+    void* address_of_code_object;
+    bytes_read = read_memory(
+            pid,
+            (void*)(address + offsets->interpreter_frame.executable),
+            sizeof(void*),
+            &address_of_code_object);
+    if (bytes_read == -1) {
+        return -1;
+    }
+
+    if (address_of_code_object == NULL) {
+        return 0;
+    }
+    return parse_code_object(pid, result, offsets, address_of_code_object, previous_frame);
+}
+
+static PyObject*
+get_stack_trace(PyObject* self, PyObject* args)
+{
+#if (!defined(__linux__) && !defined(__APPLE__)) || (defined(__linux__) && !HAVE_PROCESS_VM_READV)
+    PyErr_SetString(PyExc_RuntimeError, "get_stack_trace is not supported on this platform");
+    return NULL;
+#endif
+    int pid;
+
+    if (!PyArg_ParseTuple(args, "i", &pid)) {
+        return NULL;
+    }
+
+    void* runtime_start_address = get_py_runtime(pid);
+    if (runtime_start_address == NULL) {
+        if (!PyErr_Occurred()) {
+            PyErr_SetString(PyExc_RuntimeError, "Failed to get .PyRuntime address");
+        }
+        return NULL;
+    }
+    size_t size = sizeof(struct _Py_DebugOffsets);
+    struct _Py_DebugOffsets local_debug_offsets;
+
+    ssize_t bytes_read = read_memory(pid, runtime_start_address, size, &local_debug_offsets);
+    if (bytes_read == -1) {
+        return NULL;
+    }
+    off_t thread_state_list_head = local_debug_offsets.runtime_state.interpreters_head;
+
+    void* address_of_interpreter_state;
+    bytes_read = read_memory(
+            pid,
+            (void*)(runtime_start_address + thread_state_list_head),
+            sizeof(void*),
+            &address_of_interpreter_state);
+    if (bytes_read == -1) {
+        return NULL;
+    }
+
+    if (address_of_interpreter_state == NULL) {
+        PyErr_SetString(PyExc_RuntimeError, "No interpreter state found");
+        return NULL;
+    }
+
+    void* address_of_thread;
+    bytes_read = read_memory(
+            pid,
+            (void*)(address_of_interpreter_state + local_debug_offsets.interpreter_state.threads_head),
+            sizeof(void*),
+            &address_of_thread);
+    if (bytes_read == -1) {
+        return NULL;
+    }
+
+    PyObject* result = PyList_New(0);
+    if (result == NULL) {
+        return NULL;
+    }
+
+    // No Python frames are available for us (can happen at tear-down).
+    if (address_of_thread != NULL) {
+        void* address_of_current_frame;
+        (void)read_memory(
+                pid,
+                (void*)(address_of_thread + local_debug_offsets.thread_state.current_frame),
+                sizeof(void*),
+                &address_of_current_frame);
+        while (address_of_current_frame != NULL) {
+            if (parse_frame_object(
+                        pid,
+                        result,
+                        &local_debug_offsets,
+                        address_of_current_frame,
+                        &address_of_current_frame)
+                < 0)
+            {
+                Py_DECREF(result);
+                return NULL;
+            }
+        }
+    }
+
+    return result;
+}
+
+static PyMethodDef methods[] = {
+        {"get_stack_trace", get_stack_trace, METH_VARARGS, "Get the Python stack from a given PID"},
+        {NULL, NULL, 0, NULL},
+};
+
+static struct PyModuleDef module = {
+        .m_base = PyModuleDef_HEAD_INIT,
+        .m_name = "_testexternalinspection",
+        .m_size = -1,
+        .m_methods = methods,
+};
+
+PyMODINIT_FUNC
+PyInit__testexternalinspection(void)
+{
+    PyObject* mod = PyModule_Create(&module);
+    int rc = PyModule_AddIntConstant(mod, "PROCESS_VM_READV_SUPPORTED", HAVE_PROCESS_VM_READV);
+    if (rc < 0) {
+        Py_DECREF(mod);
+        return NULL;
+    }
+    return mod;
+}
diff --git a/Tools/build/generate_stdlib_module_names.py b/Tools/build/generate_stdlib_module_names.py
index 5dce4e042d1eb4..588dfda50658be 100644
--- a/Tools/build/generate_stdlib_module_names.py
+++ b/Tools/build/generate_stdlib_module_names.py
@@ -34,6 +34,7 @@
     '_testinternalcapi',
     '_testmultiphase',
     '_testsinglephase',
+    '_testexternalinspection',
     '_xxsubinterpreters',
     '_xxinterpchannels',
     '_xxinterpqueues',
diff --git a/configure b/configure
index ba2d49df7c65fe..65b1624900cfb2 100755
--- a/configure
+++ b/configure
@@ -661,6 +661,8 @@ MODULE__XXTESTFUZZ_FALSE
 MODULE__XXTESTFUZZ_TRUE
 MODULE_XXSUBTYPE_FALSE
 MODULE_XXSUBTYPE_TRUE
+MODULE__TESTEXTERNALINSPECTION_FALSE
+MODULE__TESTEXTERNALINSPECTION_TRUE
 MODULE__TESTMULTIPHASE_FALSE
 MODULE__TESTMULTIPHASE_TRUE
 MODULE__TESTIMPORTMULTIPLE_FALSE
@@ -17913,6 +17915,12 @@ if test "x$ac_cv_func_preadv2" = xyes
 then :
   printf "%s\n" "#define HAVE_PREADV2 1" >>confdefs.h
 
+fi
+ac_fn_c_check_func "$LINENO" "process_vm_readv" "ac_cv_func_process_vm_readv"
+if test "x$ac_cv_func_process_vm_readv" = xyes
+then :
+  printf "%s\n" "#define HAVE_PROCESS_VM_READV 1" >>confdefs.h
+
 fi
 ac_fn_c_check_func "$LINENO" "pthread_cond_timedwait_relative_np" "ac_cv_func_pthread_cond_timedwait_relative_np"
 if test "x$ac_cv_func_pthread_cond_timedwait_relative_np" = xyes
@@ -30540,6 +30548,44 @@ fi
 printf "%s\n" "$py_cv_module__testmultiphase" >&6; }
 
 
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module _testexternalinspection" >&5
+printf %s "checking for stdlib extension module _testexternalinspection... " >&6; }
+        if test "$py_cv_module__testexternalinspection" != "n/a"
+then :
+
+    if test "$TEST_MODULES" = yes
+then :
+  if true
+then :
+  py_cv_module__testexternalinspection=yes
+else $as_nop
+  py_cv_module__testexternalinspection=missing
+fi
+else $as_nop
+  py_cv_module__testexternalinspection=disabled
+fi
+
+fi
+  as_fn_append MODULE_BLOCK "MODULE__TESTEXTERNALINSPECTION_STATE=$py_cv_module__testexternalinspection$as_nl"
+  if test "x$py_cv_module__testexternalinspection" = xyes
+then :
+
+
+
+
+fi
+   if test "$py_cv_module__testexternalinspection" = yes; then
+  MODULE__TESTEXTERNALINSPECTION_TRUE=
+  MODULE__TESTEXTERNALINSPECTION_FALSE='#'
+else
+  MODULE__TESTEXTERNALINSPECTION_TRUE='#'
+  MODULE__TESTEXTERNALINSPECTION_FALSE=
+fi
+
+  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $py_cv_module__testexternalinspection" >&5
+printf "%s\n" "$py_cv_module__testexternalinspection" >&6; }
+
+
   { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for stdlib extension module xxsubtype" >&5
 printf %s "checking for stdlib extension module xxsubtype... " >&6; }
         if test "$py_cv_module_xxsubtype" != "n/a"
@@ -31152,6 +31198,10 @@ if test -z "${MODULE__TESTMULTIPHASE_TRUE}" && test -z "${MODULE__TESTMULTIPHASE
   as_fn_error $? "conditional \"MODULE__TESTMULTIPHASE\" was never defined.
 Usually this means the macro was only invoked conditionally." "$LINENO" 5
 fi
+if test -z "${MODULE__TESTEXTERNALINSPECTION_TRUE}" && test -z "${MODULE__TESTEXTERNALINSPECTION_FALSE}"; then
+  as_fn_error $? "conditional \"MODULE__TESTEXTERNALINSPECTION\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
 if test -z "${MODULE_XXSUBTYPE_TRUE}" && test -z "${MODULE_XXSUBTYPE_FALSE}"; then
   as_fn_error $? "conditional \"MODULE_XXSUBTYPE\" was never defined.
 Usually this means the macro was only invoked conditionally." "$LINENO" 5
diff --git a/configure.ac b/configure.ac
index b39af7422c4c7c..ad3d6b528a6209 100644
--- a/configure.ac
+++ b/configure.ac
@@ -4840,7 +4840,7 @@ AC_CHECK_FUNCS([ \
   mknod mknodat mktime mmap mremap nice openat opendir pathconf pause pipe \
   pipe2 plock poll posix_fadvise posix_fallocate posix_openpt posix_spawn posix_spawnp \
   posix_spawn_file_actions_addclosefrom_np \
-  pread preadv preadv2 pthread_cond_timedwait_relative_np pthread_condattr_setclock pthread_init \
+  pread preadv preadv2 process_vm_readv pthread_cond_timedwait_relative_np pthread_condattr_setclock pthread_init \
   pthread_kill ptsname ptsname_r pwrite pwritev pwritev2 readlink readlinkat readv realpath renameat \
   rtpSpawn sched_get_priority_max sched_rr_get_interval sched_setaffinity \
   sched_setparam sched_setscheduler sem_clockwait sem_getvalue sem_open \
@@ -7457,6 +7457,7 @@ PY_STDLIB_MOD([_testinternalcapi], [test "$TEST_MODULES" = yes])
 PY_STDLIB_MOD([_testbuffer], [test "$TEST_MODULES" = yes])
 PY_STDLIB_MOD([_testimportmultiple], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
 PY_STDLIB_MOD([_testmultiphase], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
+PY_STDLIB_MOD([_testexternalinspection], [test "$TEST_MODULES" = yes])
 PY_STDLIB_MOD([xxsubtype], [test "$TEST_MODULES" = yes])
 PY_STDLIB_MOD([_xxtestfuzz], [test "$TEST_MODULES" = yes])
 PY_STDLIB_MOD([_ctypes_test],
diff --git a/pyconfig.h.in b/pyconfig.h.in
index 2b4bb1a2b52866..8fc68f463b34d0 100644
--- a/pyconfig.h.in
+++ b/pyconfig.h.in
@@ -933,6 +933,9 @@
 /* Define to 1 if you have the <process.h> header file. */
 #undef HAVE_PROCESS_H
 
+/* Define to 1 if you have the `process_vm_readv' function. */
+#undef HAVE_PROCESS_VM_READV
+
 /* Define if your compiler supports function prototype */
 #undef HAVE_PROTOTYPES