chikaku

且听风吟

永远是深夜有多好。
github
email

Go Runtime Black Magic

In the Go Program Startup, it was mentioned that the pointer to the data structure of the currently running (scheduled as running) goroutine, *g, is stored on TLS. You can retrieve *g using a bit of assembly (of course, it is just a uintptr, not a typed pointer) to access runtime goroutine data. Moreover, g/p/m data is related, allowing for many other operations.

In fact, there are many similar repos now, such as goid and pid, that use this method. However, similar libraries exhibit a phenomenon where we can see various *_go1.13.go, *_go1.14.go, *_go1.15.go, etc., files in the repo's code. The reason is that the runtime data structures may change across versions, and the offset of a field may differ for different versions. It is necessary to write a structure consistent with the runtime for each version and then use unsafe.Offsetof to obtain the offset.

While using reflection, I thought there should be a place in the Go ELF that stores the offset information of these data structures. After researching Go ELF-related materials, I found that Go saves debuginfo in DWARF format, and the standard library provides the corresponding interface debug/dwarf. However, there are some issues: first, the binaries compiled during go test do not seem to include debuginfo, leading to errors when reading dwarf; second, reading dwarf from ELF compiled on Darwin reports a bad magic number, only usable happily on Linux.

The subsequent steps are relatively simple. By obtaining the binary path through os.Args[0], reading out dwarf, and looking up symbol information based on structure names and field names, you can read the offset of any field of any data structure at runtime (including runtime's g/p/m and other non-exported fields of non-exported structures). Now, we have the magic to obtain runtime data!

For specific implementations, you can check the tools-go repository under the rt package.

Usage#

Here are some tools implemented in the rt package.

Offset of Any Field in Any Structure rt.Offsetof#

Similar to unsafe.Offsetof, the structure name must be written correctly here. If it is a third-party library structure, it can be looked up through the symbol table.

// Get the offset of the goid field of the runtime g
offset, _ := rt.Offsetof("runtime.g", "goid")
fmt.Println(offset)
// 152

Current Goroutine Stack Size rt.GoStackSize#

Since there are two fields on g that identify the stack range, the stack size can be obtained through the difference.

func main() {
    done := make(chan int)
    go func() { done <- demo() }()
    <-done
}

func demo() int {
    size, _ := rt.GoStackSize()
    fmt.Println("init stack size", size)

    rec(10000, size)
    return 0
}

func rec(n, size0 int) {
    size, _ := rt.GoStackSize()
    if size != size0 {
        fmt.Println("stack size")
    }

    if n > 0 { rec(n-1, size) }
}

From the output, we can see the initial stack size is 4K, and each subsequent morestack doubles it.

init stack size 4096
stack size 8192
stack size 16384
stack size 32768
stack size 65536
stack size 131072
stack size 262144
stack size 524288
stack size 1048576
stack size 2097152

Total Number of Timers rt.NumTimers#

Since all timers are registered on p, you can get the number of timers for a single p through p.numTimers. Moreover, the runtime conveniently provides a global variable allp, which can be linked using linkname to traverse all p!

Of course, this method may not yield completely accurate data due to timer state modifications and delayed deletions, but it can estimate a relative value, which is still very useful during stress testing.

func main() {
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)
    time.AfterFunc(time.Minute, nil)

    cnt, _ := rt.NumTimers()
    fmt.Println("timer count", cnt)
    // timer count 5
}

Notes ⚠️⚠️⚠️#

  • The rt package is currently only available on Linux.
  • Needs to be built! The binaries compiled directly with go run or go test do not include debuginfo and will report errors decoding dwarf section info at offset. The repository internally pre-compiles a binary to read dwarf during testing.
  • Danger! This library directly manipulates *g and allp (although it also links the locks for use~~ but it feels even more dangerous~~). It is uncertain what impact this may have on the runtime, so it should be tried in testing and stress testing environments.
  • Performance: The runtime will read the binary once, and there may be some potentially time-consuming symbol traversals in the methods for obtaining structure field offsets.
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.