In Go Program Startup, it was mentioned that the pointer to the data structure of the currently running coroutine, *g
, is stored in TLS. You can use a little bit of assembly code to retrieve *g
(of course, it is only a uintptr, not a typed pointer) to access runtime coroutine data. The g/p/m data is related, and many other things can be done.
In fact, there are many similar repositories now, such as goid and pid, which use this method. However, similar libraries have a phenomenon that we can see various files like *_go1.13.go, *_go1.14.go, *_go1.15.go in the code of the repository. The reason is that the runtime data structure of each version may be modified, and the offset of a field corresponding to different versions will be different. Therefore, a structure consistent with runtime needs to be written for each version, and then the offset can be obtained using unsafe.Offsetof.
When using reflection, I thought that there should be a place in Go ELF that stores the offset information of these data structures. After searching for information related to Go ELF, I found that Go saves debuginfo in DWARF format, and the standard library provides the corresponding interface debug/dwarf. However, there are some problems. First, the binary compiled during go test does not seem to include debuginfo, which causes errors when reading dwarf. Second, reading dwarf of the ELF compiled under Darwin will report a bad magic number. It can only be used happily on Linux.
Subsequently, it became relatively simple. By using os.Args[0]
to obtain the binary path, the dwarf can be read, and the symbol information can be searched based on the struct name and field name, so that the offset of any field of any data structure can be read at runtime (including the g/p/m of runtime and non-exported fields of non-exported structures). Now, we have the magic to obtain runtime data!
For specific implementation, please refer to the rt package in the tools-go repository.
Usage#
Below are some tools implemented by the rt package.
Offset of Any Field of Any Structure rt.Offsetof#
Similar to unsafe.Offsetof, the struct name must be written correctly here. If it is a third-party library structure, it can be searched 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
Stack Size of Current Coroutine rt.GoStackSize#
Because there are two fields on g that indicate the range of the stack, the stack size can be obtained by 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) }
}
The initial stack size is 4K, and subsequent morestack doubles the size each time.
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, the number of timers for a single p can be obtained through p.numTimers. Moreover, the runtime just happens to provide a global variable allp, which can be traversed by linking with linkname!
Of course, due to the modification of timer status and delayed deletion, the data may not be completely accurate, but it can estimate a relative value, which is very useful for load 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
- Build required! The binary compiled directly with go run or go test does not include debuginfo, which will cause an error "decoding dwarf section info at offset". The repository pre-compiles a binary for reading dwarf during testing.
- Dangerous! This library directly operates on *g and allp (although the lock is also linked and used
but it feels even more dangerous). It is uncertain what impact it will have on the runtime. It can be tried in a test environment and under load testing. - Performance The runtime reads the binary once, and there may be some symbol traversals that may be time-consuming in the method of obtaining struct fields.