I was trying to get a better understanding of how Go deals with Goroutine’s stacks, and I came across a blog post1 that despite being great about describing how the whole thing works, it didn’t give great hints about how to extract the stack growing & shrink information from the Go runtime.

tl;dr: build a patched version of Go from source, then use that to build your binary

It turns out that under the runtime package, more specifically, in the runtime/stack.go file, there’s a variable once set to a non-zero value, brings the information we’re searching for:

    const (
            // stackDebug == 0: no logging
            //            == 1: logging of per-stack operations
            //            == 2: logging of per-frame operations
            //            == 3: logging of per-word updates
            //            == 4: logging of per-word reads
            stackDebug = 0

(see https://github.com/golang/go/blob/22f09ced523d907177670293061be678f42608e0/src/runtime/stack.go#L106-L112)

Now, the problem was - how could I turn that value on? i.e., how could I set stackDebug to 1 so that I could get per-stack operations being logged?

My first reaction was to go with ldflags, changing it at build time, but it turns out that that doesn’t work - being a const, it’ll not be seen during the link phase, thus, having no effect.

E.g., in the following example:

    package main

    import "fmt"

    const foo = "bar"
    var zaz = "caz"

    func main() {
            fmt.Println(foo, zaz)

we can try changing the value of those two, but we can see that only the variable (zaz) gets its value changed:

    $ go build -a -ldflags='-X main.foo=aaaaaaaa -X main.zaz=bbbbbbbb' 
    $ ./sample
    bar bbbbbbbb

Well, the solution then? Compile Go from scratch, and then build our binary with that version of Go.

First, clone go (either https://github.com/golang/go or https://go.googlesource.com/go), then apply the patch:

diff --git a/src/runtime/stack.go b/src/runtime/stack.go
index ebbe3e013d..439c1d00d1 100644
--- a/src/runtime/stack.go
+++ b/src/runtime/stack.go
@@ -109,7 +109,7 @@ const (
    //            == 2: logging of per-frame operations
    //            == 3: logging of per-word updates
    //            == 4: logging of per-word reads
-	stackDebug       = 0
+	stackDebug       = 1
    stackFromSystem  = 0 // allocate stacks from system memory instead of the heap
    stackFaultOnFree = 0 // old stacks are mapped noaccess to detect use after free
    stackPoisonCopy  = 0 // fill stack that should not be accessed with garbage, to detect bad dereferences during copy

And then, already having Go installed, use it to build the version of Go under your modified repository:

    cd ./src

Once it finishes, it’ll have the binaries available at ./bin:

    ├── go
    └── gofmt

Which can now give you stack grow & shrink info once you compile a program with it:

    stackfree 0xc00708c000 4096
    runtime: newstack sp=0xc004770350 stack=[0xc004770000, 0xc004770800]
    morebuf={pc:0x4941f9 sp:0xc004770360 lr:0x0}
    sched={pc:0x5e8379 sp:0xc004770358 lr:0x0 ctxt:0x0}
    stackalloc 4096
    stackcacherefill order=1
    allocated 0xc000c6e000
    copystack gp=0xc007102f00 [0xc004770000 0xc004770358
    0xc004770800] -> [0xc000c6e000 0xc000c6eb58 0xc000c6f000]/4096
    stackfree 0xc004770000 2048

If you’re interesting to learn more, make sure you check out the Go contributing guide! https://golang.org/doc/contribute.html

  1. https://medium.com/a-journey-with-go/go-how-does-the-goroutine-stack-size-evolve-447fc02085e5 ↩︎