diff --git a/src/coredump.rs b/src/coredump.rs
index 7df1e010..e0118437 100644
--- a/src/coredump.rs
+++ b/src/coredump.rs
@@ -246,7 +246,8 @@ impl PythonCoreDump {
 
         // lets us figure out which thread has the GIL
         let config = Config::default();
-        let threadstate_address = get_threadstate_address(&python_info, &version, &config)?;
+        let threadstate_address =
+            get_threadstate_address(interpreter_address, &python_info, &version, &config)?;
         info!("found threadstate at 0x{:016x}", threadstate_address);
 
         Ok(PythonCoreDump {
diff --git a/src/python_interpreters.rs b/src/python_interpreters.rs
index 609ca7ba..bd055791 100644
--- a/src/python_interpreters.rs
+++ b/src/python_interpreters.rs
@@ -14,6 +14,7 @@ This means we can't dereference them directly.
 use crate::python_bindings::{
     v2_7_15, v3_10_0, v3_11_0, v3_12_0, v3_3_7, v3_5_5, v3_6_6, v3_7_0, v3_8_0, v3_9_5,
 };
+use crate::utils::offset_of;
 
 pub trait InterpreterState {
     type ThreadState: ThreadState;
@@ -22,6 +23,7 @@ pub trait InterpreterState {
     type ListObject: ListObject;
     type TupleObject: TupleObject;
     fn head(&self) -> *mut Self::ThreadState;
+    fn gil_locked(&self) -> Option<bool>;
     fn modules(&self) -> *mut Self::Object;
 }
 
@@ -100,10 +102,6 @@ pub trait TypeObject {
     fn flags(&self) -> usize;
 }
 
-fn offset_of<T, M>(object: *const T, member: *const M) -> usize {
-    member as usize - object as usize
-}
-
 /// This macro provides a common impl for PyThreadState/PyFrameObject/PyCodeObject traits
 /// (this code is identical across python versions, we are only abstracting the struct layouts here).
 /// String handling changes substantially between python versions, and is handled separately.
@@ -115,9 +113,13 @@ macro_rules! PythonCommonImpl {
             type StringObject = $py::$stringobject;
             type ListObject = $py::PyListObject;
             type TupleObject = $py::PyTupleObject;
+
             fn head(&self) -> *mut Self::ThreadState {
                 self.tstate_head
             }
+            fn gil_locked(&self) -> Option<bool> {
+                None
+            }
             fn modules(&self) -> *mut Self::Object {
                 self.modules
             }
@@ -415,9 +417,14 @@ impl InterpreterState for v3_12_0::PyInterpreterState {
     type StringObject = v3_12_0::PyUnicodeObject;
     type ListObject = v3_12_0::PyListObject;
     type TupleObject = v3_12_0::PyTupleObject;
+
     fn head(&self) -> *mut Self::ThreadState {
         self.threads.head
     }
+    fn gil_locked(&self) -> Option<bool> {
+        Some(self._gil.locked._value != 0)
+    }
+
     fn modules(&self) -> *mut Self::Object {
         self.imports.modules
     }
@@ -506,6 +513,9 @@ impl InterpreterState for v3_11_0::PyInterpreterState {
     fn head(&self) -> *mut Self::ThreadState {
         self.threads.head
     }
+    fn gil_locked(&self) -> Option<bool> {
+        None
+    }
     fn modules(&self) -> *mut Self::Object {
         self.modules
     }
diff --git a/src/python_process_info.rs b/src/python_process_info.rs
index 2270556c..5ecb4cc2 100644
--- a/src/python_process_info.rs
+++ b/src/python_process_info.rs
@@ -529,6 +529,7 @@ where
 }
 
 pub fn get_threadstate_address(
+    interpreter_address: usize,
     python_info: &PythonProcessInfo,
     version: &Version,
     config: &Config,
@@ -536,7 +537,16 @@ pub fn get_threadstate_address(
     let threadstate_address = match version {
         Version {
             major: 3,
-            minor: 7..=12,
+            minor: 12,
+            ..
+        } => {
+            let interp: v3_12_0::_is = Default::default();
+            let offset = crate::utils::offset_of(&interp, &interp._gil.last_holder._value);
+            interpreter_address + offset
+        }
+        Version {
+            major: 3,
+            minor: 7..=11,
             ..
         } => match python_info.get_symbol("_PyRuntime") {
             Some(&addr) => {
diff --git a/src/python_spy.rs b/src/python_spy.rs
index 531616ab..40361577 100644
--- a/src/python_spy.rs
+++ b/src/python_spy.rs
@@ -62,7 +62,8 @@ impl PythonSpy {
         info!("Found interpreter at 0x{:016x}", interpreter_address);
 
         // lets us figure out which thread has the GIL
-        let threadstate_address = get_threadstate_address(&python_info, &version, config)?;
+        let threadstate_address =
+            get_threadstate_address(interpreter_address, &python_info, &version, config)?;
 
         #[cfg(feature = "unwind")]
         let native = if config.native {
@@ -206,18 +207,19 @@ impl PythonSpy {
             None
         };
 
-        // TODO: hoist most of this code out to stack_trace.rs, and
-        // then annotate the output of that with things like native stack traces etc
-        //      have moved in gil / locals etc
-        let gil_thread_id =
-            get_gil_threadid::<I, Process>(self.threadstate_address, &self.process)?;
-
         // Get the python interpreter, and loop over all the python threads
         let interp: I = self
             .process
             .copy_struct(self.interpreter_address)
             .context("Failed to copy PyInterpreterState from process")?;
 
+        // get the threadid of the gil if appropriate
+        let gil_thread_id = if interp.gil_locked().unwrap_or(true) {
+            get_gil_threadid::<I, Process>(self.threadstate_address, &self.process)?
+        } else {
+            0
+        };
+
         let mut traces = Vec::new();
         let mut threads = interp.head();
         while !threads.is_null() {
diff --git a/src/stack_trace.rs b/src/stack_trace.rs
index dce648e9..27d44a96 100644
--- a/src/stack_trace.rs
+++ b/src/stack_trace.rs
@@ -77,7 +77,11 @@ where
     I: InterpreterState,
     P: ProcessMemory,
 {
-    let gil_thread_id = get_gil_threadid::<I, P>(threadstate_address, process)?;
+    let gil_thread_id = if interpreter.gil_locked().unwrap_or(true) {
+        get_gil_threadid::<I, P>(threadstate_address, process)?
+    } else {
+        0
+    };
 
     let mut ret = Vec::new();
     let mut threads = interpreter.head();
diff --git a/src/utils.rs b/src/utils.rs
index 2ca1374f..a354f151 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -20,3 +20,7 @@ pub fn resolve_filename(filename: &str, modulename: &str) -> Option<String> {
 
     None
 }
+
+pub fn offset_of<T, M>(object: *const T, member: *const M) -> usize {
+    member as usize - object as usize
+}