ebitengine / purego

Apache License 2.0
2.27k stars 70 forks source link

Support for __dso_handle #114

Open craig65535 opened 1 year ago

craig65535 commented 1 year ago

Hello, I thought I would try using purego to interface with macOS's unified logging framework. However I've hit a snag.

The unified logger uses a set of macros like os_log_debug that ultimately call _os_log_internal. This is exported by libSystem.B.dylib - so far so good. However, the first argument to that function is &__dso_handle, which is defined in C headers as

extern struct mach_header __dso_handle;

I'm not sure how to resolve this symbol. It's not exported by libSystem.B.dylib, but rather it's specific to the running executable. I don't understand this fully, but I've read that it's actually dynamically generated by the linker.

Is there a way to reference this symbol with purego, or is this not possible?

TotallyGamerJet commented 1 year ago

Isn't it possible to just use Dlsym(lib, "__dso_handle")?

Note that it returns a pointer to the value.

craig65535 commented 1 year ago

That returns an error (symbol not found). It's not in libSystem.B.dylib. As I said I'm not 100% familiar with this, but I think it's something the linker generates - a dynamic executable would have its own __dso_handle.

TotallyGamerJet commented 1 year ago

You are correct that it's unique to each dynamic object according to this StackOverflow post. I'm not sure how to reference it in Go. I am not familiar with this API either so more research needs to be done

craig65535 commented 1 year ago

Something that would help here is the ability to lookup a given symbol/C-style global variable in the current process. But I don't know how feasible that is without full cgo.

TotallyGamerJet commented 1 year ago

How would you do this with Cgo?

craig65535 commented 1 year ago

unsafe.Pointer(&C.__dso_handle)

TotallyGamerJet commented 1 year ago

So, cgo outputs Go code that links to the C symbol.

// Code generated by cmd/cgo; DO NOT EDIT.

//line /Users/jarrettkuklis/Documents/GolandProjects/purego/test/main.go:1:1
package main

//#include <os/log.h>
import _ "unsafe"
import (
    "fmt"
    "unsafe"
)

func main() {
    fmt.Printf("%x\n", unsafe.Pointer(&( /*line :11:37*/*_Cvar___dso_handle /*line :11:50*/)))
}

// --- _cgo_types.go ---
type _Ctype_struct_mach_header struct {
    magic       _Ctype_uint32_t
    cputype     _Ctype_int32_t
    cpusubtype  _Ctype_int32_t
    filetype    _Ctype_uint32_t
    ncmds       _Ctype_uint32_t
    sizeofcmds  _Ctype_uint32_t
    flags       _Ctype_uint32_t
}
//go:linkname __cgo___dso_handle __dso_handle
//go:cgo_import_static __dso_handle
var __cgo___dso_handle byte
var _Cvar___dso_handle *_Ctype_struct_mach_header = (*_Ctype_struct_mach_header)(unsafe.Pointer(&__cgo___dso_handle))

If you go ahead and copy this into a plan Go code you'll get something like this.

package main

import (
    "fmt"
    "unsafe"
)

type mach_header struct {
    magic      uint32
    cputype    int32
    cpusubtype int32
    filetype   uint32
    ncmds      uint32
    sizeofcmds uint32
    flags      uint32
}

//go:linkname __cgo___dso_handle __dso_handle
//go:cgo_import_static __dso_handle
var __cgo___dso_handle byte
var _Cvar___dso_handle *mach_header = (*mach_header)(unsafe.Pointer(&__cgo___dso_handle))

func main() {
    fmt.Printf("%x\n", unsafe.Pointer(_Cvar___dso_handle))
}

Although this doesn't compile because the //go:cgo_import_static is only available in Cgo. The cgo docs state that it's only used for -linkmode=external. We want the Go linker so we set it to internal and replace the comment with //go:cgo_import_dynamic but the single symbol version of this is not accessible either without cgo. So then I replaced it with the three argument version (//go:cgo_import_dynamic __dso_handle __dso_handle "").

Now it finally compiles but points to some really low address (0x680000) when Cgo version points to a higher address (0x104280000). And when you try to dereference the go version it will SIGSEGV. I think this is an issue with how //go:cgo_import_dynamic is implemented because on amd64 it doesn't work at all _Cvar___dso_handle points to nil.

TotallyGamerJet commented 3 weeks ago

@craig65535

I have had some time to look into this. The reason trying to load it with purego doesn't work is because _dso_handle symbol is add by the macOS C linker and Go's linker doesn't add it.

However, the data for this symbol is still able to be obtained since it is just the information about the currently running macho file. This information can be grabbed using just the stdlib:

package main

import (
    "debug/macho"
    "fmt"
    "log"
    "os"
    "structs"
)

// https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h
type Mach_header struct {
    _          structs.HostLayout // ensures it matches system layout
    Magic      uint32
    Cputype    int32
    Cpusubtype int32
    Filetype   uint32
    Ncmds      uint32
    Sizeofcmds uint32
    Flags      uint32
    _          uint32 // reserved
}

func main() {
    dir, err := os.Executable()
    if err != nil {
        log.Fatal(err)
    }
    ex, err := macho.Open(dir) // or OpenFat if it is a fat macho
    if err != nil {
        log.Fatal(err)
    }
    header := Mach_header{
        Magic:      ex.FileHeader.Magic,
        Cputype:    int32(ex.FileHeader.Cpu),
        Cpusubtype: int32(ex.FileHeader.SubCpu),
        Filetype:   uint32(ex.FileHeader.Type),
        Ncmds:      ex.FileHeader.Ncmd,
        Sizeofcmds: ex.FileHeader.Cmdsz,
        Flags:      ex.FileHeader.Flags,
    }
    fmt.Println(header)
}

I checked and this results in the exact same data as the _dso_handle symbol. You can then use &header as the argument to _os_log_internal. I believe this solves your issue. If so, please close this issue. Thank you.

craig65535 commented 1 week ago

@TotallyGamerJet I missed your update at first - thank you! I will try this and reply back here.