go-delve/delve · error

could not get registers

Error message

could not get registers

What it means

registers() on darwin/amd64 reads GPRs with the Mach call get_registers(thread_act, x86_thread_state64_t). Any kern_return other than KERN_SUCCESS produces the opaque 'could not get registers' error. Typically the thread act is no longer valid or the task port lost rights.

Solutions

  1. Refresh the thread list and skip/evict threads whose act is invalid (KERN_TERMINATED)
  2. Ensure delve can access the task port (same user/root, and not blocked by SIP/hardened runtime restrictions)
  3. Re-attach to the process if its task port rights were revoked (e.g. after exec)
  4. Surface the kern_return value in the error to distinguish thread-dead from permission problems

Example fix

// before
kret := C.get_registers(C.mach_port_name_t(thread.os.threadAct), &state)
if kret != C.KERN_SUCCESS {
	return nil, fmt.Errorf("could not get registers")
}
// after
kret := C.get_registers(C.mach_port_name_t(thread.os.threadAct), &state)
if kret != C.KERN_SUCCESS {
	return nil, fmt.Errorf("could not get registers for thread %d: kern_return=%d", thread.ID, kret)
}
Defensive patterns

Strategy: retry

Validate before calling

// before reading registers on darwin
if !isThreadAlive(dbp, thread.ID) {
	return nil, fmt.Errorf("thread %d no longer valid", thread.ID)
}

Type guard

func isGetRegsErr(err error) bool {
	return err != nil && strings.Contains(err.Error(), "could not get registers")
}

Try / catch

regs, err := registers(thread)
if isGetRegsErr(err) {
	// refresh thread list once, then retry
	dbp.refreshThreads()
	regs, err = registers(thread)
}
return regs, err

Prevention

When it happens

Trigger: Listing/reading registers for a thread that terminated between thread enumeration and the Mach call, after the target execed (stale acts), or when task-for-pid/thread privileges were denied by macOS hardening.

Common situations: Debuggee exit racing a 'goroutines'/'registers' command; attaching to protected processes under SIP/hardened runtime; stale thread cache after fork/exec.

Related errors


AI-assisted analysis of go-delve/delve@a23773e6c3 (2026-08-31). Data as JSON: /api/errors/a9cda590082ed43f. Report an issue: GitHub.

Appendix: source

Thrown at pkg/proc/native/registers_darwin_amd64.go:297

	case x86asm.R12:
		return r.r12, nil
	case x86asm.R13:
		return r.r13, nil
	case x86asm.R14:
		return r.r14, nil
	case x86asm.R15:
		return r.r15, nil
	}

	return 0, proc.ErrUnknownRegister
}

func registers(thread *nativeThread) (proc.Registers, error) {
	var state C.x86_thread_state64_t
	var identity C.thread_identifier_info_data_t
	kret := C.get_registers(C.mach_port_name_t(thread.os.threadAct), &state)
	if kret != C.KERN_SUCCESS {
		return nil, fmt.Errorf("could not get registers")
	}
	kret = C.get_identity(C.mach_port_name_t(thread.os.threadAct), &identity)
	if kret != C.KERN_SUCCESS {
		return nil, fmt.Errorf("could not get thread identity information")
	}
	/*
		thread_identifier_info::thread_handle contains the base of the
		thread-specific data area, which on x86 and x86_64 is the thread’s base
		address of the %gs segment. 10.9.2 xnu-2422.90.20/osfmk/kern/thread.c
		thread_info_internal() gets the value from
		machine_thread::cthread_self, which is the same value used to set the
		%gs base in xnu-2422.90.20/osfmk/i386/pcb_native.c
		act_machine_switch_pcb().
		--
		comment copied from chromium's crashpad
		https://chromium.googlesource.com/crashpad/crashpad/+/master/snapshot/mac/process_reader.cc
	*/
	regs := &Regs{

View on GitHub (pinned to a23773e6c3)