diff --git a/src/runtime/gc_globals_precise.go b/src/runtime/gc_globals_precise.go index 3cdba741..5fbaea37 100644 --- a/src/runtime/gc_globals_precise.go +++ b/src/runtime/gc_globals_precise.go @@ -1,5 +1,5 @@ -//go:build gc.conservative && !baremetal && !tinygo.wasm && !windows -// +build gc.conservative,!baremetal,!tinygo.wasm,!windows +//go:build gc.conservative && !baremetal && !darwin && !tinygo.wasm && !windows +// +build gc.conservative,!baremetal,!darwin,!tinygo.wasm,!windows package runtime diff --git a/src/runtime/os_darwin.go b/src/runtime/os_darwin.go index d485f042..1d895722 100644 --- a/src/runtime/os_darwin.go +++ b/src/runtime/os_darwin.go @@ -3,6 +3,8 @@ package runtime +import "unsafe" + const GOOS = "darwin" const ( @@ -18,3 +20,93 @@ const ( clock_REALTIME = 0 clock_MONOTONIC_RAW = 4 ) + +// https://opensource.apple.com/source/xnu/xnu-7195.141.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html +type machHeader struct { + magic uint32 + cputype uint32 + cpusubtype uint32 + filetype uint32 + ncmds uint32 + sizeofcmds uint32 + flags uint32 + reserved uint32 +} + +// Struct for the LC_SEGMENT_64 load command. +type segmentLoadCommand struct { + cmd uint32 // LC_SEGMENT_64 + cmdsize uint32 + segname [16]byte + vmaddr uintptr + vmsize uintptr + fileoff uintptr + filesize uintptr + maxprot uint32 + initprot uint32 + nsects uint32 + flags uint32 +} + +// MachO header of the currently running process. +//go:extern _mh_execute_header +var libc_mh_execute_header machHeader + +// Mark global variables. +// The MachO linker doesn't seem to provide symbols for the start and end of the +// data section. There is get_etext, get_edata, and get_end, but these are +// undocumented and don't work with ASLR (which is enabled by default). +// Therefore, read the MachO header directly. +func markGlobals() { + // Here is a useful blog post to understand the MachO file format: + // https://h3adsh0tzz.com/2020/01/macho-file-format/ + + const ( + MH_MAGIC_64 = 0xfeedfacf + LC_SEGMENT_64 = 0x19 + VM_PROT_WRITE = 0x02 + ) + + // Sanity check that we're actually looking at a MachO header. + if gcAsserts && libc_mh_execute_header.magic != MH_MAGIC_64 { + runtimePanic("gc: unexpected MachO header") + } + + // Iterate through the load commands. + // Because we're only interested in LC_SEGMENT_64 load commands, cast the + // pointer to that struct in advance. + var offset uintptr + var hasOffset bool + cmd := (*segmentLoadCommand)(unsafe.Pointer(uintptr(unsafe.Pointer(&libc_mh_execute_header)) + unsafe.Sizeof(machHeader{}))) + for i := libc_mh_execute_header.ncmds; i != 0; i-- { + if cmd.cmd == LC_SEGMENT_64 { + if cmd.fileoff == 0 && cmd.nsects != 0 { + // Detect ASLR offset by checking fileoff and nsects. This + // locates the __TEXT segment. This matches getsectiondata: + // https://opensource.apple.com/source/cctools/cctools-973.0.1/libmacho/getsecbyname.c.auto.html + offset = uintptr(unsafe.Pointer(&libc_mh_execute_header)) - cmd.vmaddr + hasOffset = true + } + if cmd.maxprot&VM_PROT_WRITE != 0 { + // Found a writable segment, which may contain Go globals. + if gcAsserts && !hasOffset { + // No ASLR offset detected. Did the __TEXT segment come + // after the __DATA segment? + // Note that when ASLR is disabled (for example, when + // running inside lldb), the offset is zero. That's why we + // need a separate hasOffset for this assert. + runtimePanic("gc: did not detect ASLR offset") + } + // Scan this segment for GC roots. + // This could be improved by only reading the memory areas + // covered by sections. That would reduce the amount of memory + // scanned a little bit (up to a single VM page). + markRoots(offset+cmd.vmaddr, offset+cmd.vmaddr+cmd.vmsize) + } + } + + // Move on to the next load command (wich may or may not be a + // LC_SEGMENT_64). + cmd = (*segmentLoadCommand)(unsafe.Pointer(uintptr(unsafe.Pointer(cmd)) + uintptr(cmd.cmdsize))) + } +}