├── .github └── workflows │ └── goreleaser.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── elf ├── elf.go ├── elfinfo.go └── goresym.go ├── go.mod ├── go.sum ├── main.go ├── parse.go └── proc ├── proc.go └── snapshot.go /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up environment variables 21 | run: | 22 | echo "VERSION=$(git describe --tags $(git rev-list --tags --max-count=1))" >> $GITHUB_ENV 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v2 26 | with: 27 | go-version: '1.21.0' 28 | 29 | - name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v4 31 | with: 32 | version: latest 33 | args: release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | 5 | builds: 6 | - id: go-spy 7 | binary: go-spy 8 | env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | goarch: 13 | - amd64 14 | 15 | checksum: 16 | name_template: 'checksums.txt' 17 | 18 | release: 19 | prerelease: auto 20 | 21 | snapshot: 22 | name_template: "{{ .Tag }}-next" 23 | 24 | changelog: 25 | sort: asc 26 | filters: 27 | exclude: 28 | - '^docs:' 29 | - '^test:' 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ./gray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-spy 2 | 3 | Dump goroutines from a running process. 4 | 5 | # Usage 6 | 7 | ``` 8 | $ sudo go-spy $(pidof containerd) 9 | 10 | ... 11 | -- Goroutine 16: waiting 12 | 0x5569d73833f6 runtime.gopark+214 13 | 0x5569d73b3317 time.Sleep+311 14 | 0x5569d7b85b7f github.com/containerd/containerd/runtime/restart/monitor.(*monitor).run+63 15 | 0x5569d7b846ca github.com/containerd/containerd/runtime/restart/monitor.init.0.func1.1+42 16 | 0x5569d73b6781 runtime.goexit+1 17 | -- Goroutine 26: waiting 18 | 0x5569d73833f6 runtime.gopark+214 19 | 0x5569d7393d1c runtime.selectgo+1980 20 | 0x5569d7c077d6 github.com/containerd/containerd/services/events.(*service).Subscribe+310 21 | 0x5569d7a56930 github.com/containerd/containerd/api/services/events/v1._Events_Subscribe_Handler+208 22 | 0x5569d85cfb5a github.com/containerd/containerd/services/server.streamNamespaceInterceptor+250 23 | 0x5569d85c697a github.com/grpc-ecosystem/go-grpc-middleware.ChainStreamServer.func1.1.1+58 24 | 0x5569d85c9429 github.com/grpc-ecosystem/go-grpc-prometheus.(*ServerMetrics).StreamServerInterceptor.func1+265 25 | 0x5569d85c697a github.com/grpc-ecosystem/go-grpc-middleware.ChainStreamServer.func1.1.1+58 26 | 0x5569d85cdfb3 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.StreamServerInterceptor.func1+1651 27 | 0x5569d85c697a github.com/grpc-ecosystem/go-grpc-middleware.ChainStreamServer.func1.1.1+58 28 | 0x5569d85c681e github.com/grpc-ecosystem/go-grpc-middleware.ChainStreamServer.func1+190 29 | 0x5569d7a012e6 google.golang.org/grpc.(*Server).processStreamingRPC+4550 30 | 0x5569d7a02c85 google.golang.org/grpc.(*Server).handleStream+2533 31 | 0x5569d79fb678 google.golang.org/grpc.(*Server).serveStreams.func1.2+152 32 | 0x5569d73b6781 runtime.goexit+1 33 | ... 34 | ``` 35 | 36 | No limitation for the binary: 37 | 38 | ``` 39 | $ sudo file -L $(which containerd) 40 | /usr/bin/containerd: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0c71c3183f9c22b27edfdb71ebcdc3735eea9228, for GNU/Linux 3.2.0, stripped 41 | ``` 42 | 43 | # Known issues 44 | 45 | 1. Bad optimization may lead to OOM if binary and/or memory usage is big. 46 | 2. Memory peeking may fail when goroutines are spawned frequently. 47 | 3. Multi-version support is bad and not tested at all. Go1.18 ~ 1.21 should be working, but no guarantee either. 48 | -------------------------------------------------------------------------------- /elf/elf.go: -------------------------------------------------------------------------------- 1 | package elf 2 | 3 | import "fmt" 4 | 5 | type ELF struct { 6 | filename string 7 | } 8 | 9 | func GetFromPid(pid int) *ELF { 10 | return &ELF{filename: fmt.Sprintf("/proc/%d/exe", pid)} 11 | } 12 | -------------------------------------------------------------------------------- /elf/elfinfo.go: -------------------------------------------------------------------------------- 1 | package elf 2 | 3 | import ( 4 | "debug/elf" 5 | "os" 6 | "slices" 7 | ) 8 | 9 | type Symbol struct { 10 | Offset uint64 11 | Name string 12 | } 13 | 14 | type Symbols []Symbol // sorted 15 | 16 | type G struct { 17 | Size uint64 18 | Offsets map[string]uint64 19 | } 20 | 21 | func (g *G) FieldOffset(name string) uint64 { 22 | return g.Offsets[name] 23 | } 24 | 25 | type ELFInfo struct { 26 | GoVersion string 27 | Symbols 28 | GProto, AllgsProto *Proto 29 | 30 | UnrealRuntimeGoexitOffset uint64 31 | } 32 | 33 | func (e ELFInfo) LookupSymbol(offset uint64) Symbol { 34 | idx, _ := slices.BinarySearchFunc(e.Symbols, offset, func(x Symbol, offset uint64) int { 35 | if x.Offset > offset { 36 | return 1 37 | } else if x.Offset < offset { 38 | return -1 39 | } 40 | return 0 41 | }) 42 | if idx == len(e.Symbols) { 43 | return e.Symbols[idx-1] 44 | } 45 | if e.Symbols[idx].Offset == offset { 46 | return e.Symbols[idx] 47 | } 48 | if idx == 0 { 49 | return e.Symbols[0] 50 | } 51 | return e.Symbols[idx-1] 52 | 53 | } 54 | 55 | func (e *ELF) Parse() (elfInfo *ELFInfo, err error) { 56 | metadata, err := recoverMetadata(e.filename) 57 | if err != nil { 58 | return 59 | } 60 | elfInfo = &ELFInfo{GoVersion: metadata.Version} 61 | f, err := os.Open(e.filename) 62 | if err != nil { 63 | return 64 | } 65 | defer f.Close() 66 | elfFile, err := elf.NewFile(f) 67 | if err != nil { 68 | return 69 | } 70 | textSection := elfFile.Section(".text") 71 | for _, f := range metadata.Functions { 72 | if f.FullName == "runtime.goexit" { 73 | elfInfo.UnrealRuntimeGoexitOffset = f.Start - textSection.Addr + textSection.Offset 74 | } 75 | elfInfo.Symbols = append(elfInfo.Symbols, 76 | Symbol{ 77 | Offset: f.Start - textSection.Addr + textSection.Offset, 78 | Name: f.FullName, 79 | }) 80 | } 81 | slices.SortFunc(elfInfo.Symbols, 82 | func(a, b Symbol) int { 83 | if a.Offset < b.Offset { 84 | return -1 85 | } else if a.Offset > b.Offset { 86 | return 1 87 | } 88 | return 0 89 | }) 90 | elfInfo.GProto = &Proto{ 91 | Fields: map[string]*Field{ 92 | "goid": &Field{152, 8}, 93 | "atomicstatus": &Field{144, 4}, 94 | "stack.lo": &Field{0, 8}, 95 | "stack.hi": &Field{8, 8}, 96 | "sched.pc": &Field{56 + 8, 8}, 97 | "sched.bp": &Field{56 + 48, 8}, 98 | }, 99 | } 100 | elfInfo.AllgsProto = &Proto{ 101 | Fields: map[string]*Field{ 102 | "array": &Field{0, 8}, 103 | "len": &Field{8, 8}, 104 | }, 105 | } 106 | return 107 | } 108 | 109 | func (e *ELFInfo) AdjustOffset(offset uint64) { 110 | for idx, symbol := range e.Symbols { 111 | e.Symbols[idx].Offset = symbol.Offset + offset 112 | } 113 | } 114 | 115 | type Proto struct { 116 | Fields map[string]*Field 117 | } 118 | 119 | type Field struct { 120 | Offset uint64 121 | Size uint64 122 | } 123 | 124 | func (p *Proto) GetField(name string) (*Field, bool) { 125 | field, ok := p.Fields[name] 126 | return field, ok 127 | } 128 | -------------------------------------------------------------------------------- /elf/goresym.go: -------------------------------------------------------------------------------- 1 | /*Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.*/ 2 | package elf 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "os" 8 | 9 | // we copy the go src directly, then change every include to github.com/jschwinger233/go-spy/elf/ 10 | // this is required since we're using internal files. Our modifications are directly inside the copied source 11 | 12 | "github.com/mandiant/GoReSym/objfile" 13 | ) 14 | 15 | type FuncMetadata struct { 16 | Start uint64 17 | End uint64 18 | FullName string 19 | } 20 | 21 | type ExtractMetadata struct { 22 | Version string 23 | Functions []FuncMetadata 24 | } 25 | 26 | func recoverMetadata(fileName string) (metadata ExtractMetadata, err error) { 27 | extractMetadata := ExtractMetadata{} 28 | 29 | file, err := objfile.Open(fileName) 30 | if err != nil { 31 | return ExtractMetadata{}, fmt.Errorf("invalid file: %w", err) 32 | } 33 | 34 | fileData, fileDataErr := os.ReadFile(fileName) 35 | if fileDataErr == nil { 36 | // GOVERSION 37 | if extractMetadata.Version == "" { 38 | idx := bytes.Index(fileData, []byte{0x67, 0x6F, 0x31, 0x2E}) 39 | if idx != -1 && len(fileData[idx:]) > 10 { 40 | extractMetadata.Version = "go1." 41 | ver := fileData[idx+4 : idx+10] 42 | for i, c := range ver { 43 | // the string is _not_ null terminated, nor length delimited. So, filter till first non-numeric ascii 44 | nextIsNumeric := (i+1) < len(ver) && ver[i+1] >= 0x30 && ver[i+1] <= 0x39 45 | 46 | // careful not to end with a . at the end 47 | if (c >= 0x30 && c <= 0x39 && c != ' ') || (c == '.' && nextIsNumeric) { 48 | extractMetadata.Version += string([]byte{c}) 49 | } else { 50 | break 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | tabs, err := file.PCLineTable("", 0, 0) 58 | if err != nil { 59 | return ExtractMetadata{}, fmt.Errorf("failed to read pclntab: %w", err) 60 | } 61 | 62 | if len(tabs) == 0 { 63 | return ExtractMetadata{}, fmt.Errorf("no pclntab candidates found") 64 | } 65 | 66 | var finalTab *objfile.PclntabCandidate = &tabs[0] 67 | for idx, tab := range tabs { 68 | foundMainMain, foundRuntimeGoexit := false, false 69 | for _, elem := range tab.ParsedPclntab.Funcs { 70 | if elem.Name == "main.main" { 71 | foundMainMain = true 72 | continue 73 | } 74 | if elem.Name == "runtime.goexit" { 75 | foundRuntimeGoexit = true 76 | } 77 | } 78 | if foundMainMain && foundRuntimeGoexit { 79 | finalTab = &tabs[idx] 80 | break 81 | } 82 | } 83 | 84 | for _, elem := range finalTab.ParsedPclntab.Funcs { 85 | extractMetadata.Functions = append(extractMetadata.Functions, FuncMetadata{ 86 | Start: elem.Entry, 87 | End: elem.End, 88 | FullName: elem.Name, 89 | }) 90 | } 91 | 92 | return extractMetadata, nil 93 | } 94 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jschwinger233/go-spy 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/mandiant/GoReSym v1.7.2-0.20231128194149-d75fbb44e72d 7 | github.com/prometheus/procfs v0.12.0 8 | ) 9 | 10 | require ( 11 | github.com/elliotchance/orderedmap v1.5.1 // indirect 12 | golang.org/x/arch v0.6.0 // indirect 13 | golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect 14 | golang.org/x/sys v0.12.0 // indirect 15 | rsc.io/binaryregexp v0.2.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/elliotchance/orderedmap v1.5.1 h1:G1X4PYlljzimbdQ3RXmtIZiQ9d6aRQ3sH1nzjq5mECE= 4 | github.com/elliotchance/orderedmap v1.5.1/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= 5 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 6 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 7 | github.com/mandiant/GoReSym v1.7.2-0.20231128194149-d75fbb44e72d h1:EfxH/MACKEHkdMjl7wDijXRaN1VFu8bXjKGhPi+dk2U= 8 | github.com/mandiant/GoReSym v1.7.2-0.20231128194149-d75fbb44e72d/go.mod h1:C9Cyl7dQTqNzm5KyrZ78bS54bzwMd1fu3cWDWULCzLc= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 12 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 15 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 16 | golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= 17 | golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 18 | golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= 19 | golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 20 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 21 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 24 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= 26 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 27 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strconv" 8 | 9 | "github.com/jschwinger233/go-spy/elf" 10 | "github.com/jschwinger233/go-spy/proc" 11 | ) 12 | 13 | func help() string { 14 | return "Usage: go-spy " 15 | } 16 | 17 | func main() { 18 | if len(os.Args) != 2 { 19 | log.Fatal(help()) 20 | } 21 | pid, err := strconv.Atoi(os.Args[1]) 22 | if err != nil { 23 | log.Fatal(help()) 24 | } 25 | 26 | fmt.Printf("Parsing elf\n") 27 | elfInfo, err := elf.GetFromPid(pid).Parse() 28 | if err != nil { 29 | log.Fatalf("Error parsing ELF: %v", err) 30 | } 31 | 32 | fmt.Printf("Taking snapshot\n") 33 | snapshot, err := proc.Get(pid).Snapshot() 34 | if err != nil { 35 | log.Fatalf("Error taking snapshot: %v", err) 36 | } 37 | 38 | fmt.Printf("Parsing goroutines\n") 39 | goroutines, err := parseGoroutines(elfInfo, snapshot) 40 | if err != nil { 41 | log.Fatalf("Error parsing snapshot: %v", err) 42 | } 43 | 44 | initAddr := snapshot.InitAddr() 45 | 46 | var realRuntimeGoExitOffset uint64 47 | for frame := goroutines[0].Frame(); frame.Bp != 0; frame = frame.Next(snapshot) { 48 | realRuntimeGoExitOffset = frame.Pc(snapshot) - initAddr - 1 49 | } 50 | elfInfo.AdjustOffset(realRuntimeGoExitOffset - elfInfo.UnrealRuntimeGoexitOffset) 51 | 52 | for _, goroutine := range goroutines { 53 | if goroutine.Status == Dead { 54 | continue 55 | } 56 | fmt.Printf("-- Goroutine %d: %s\n", goroutine.Goid, goroutine.StatusName()) 57 | printSymbol(goroutine.Pc, initAddr, elfInfo) 58 | for frame := goroutine.Frame(); frame.Bp != 0; frame = frame.Next(snapshot) { 59 | printSymbol(frame.Pc(snapshot), initAddr, elfInfo) 60 | } 61 | } 62 | } 63 | 64 | func printSymbol(pc, initAddr uint64, elfInfo *elf.ELFInfo) { 65 | symbol := elfInfo.LookupSymbol(pc - initAddr) 66 | fmt.Printf(" 0x%x %s+%d\n", pc, symbol.Name, pc-initAddr-symbol.Offset) 67 | } 68 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/jschwinger233/go-spy/elf" 10 | "github.com/jschwinger233/go-spy/proc" 11 | ) 12 | 13 | type GoroutineStatus = uint32 14 | 15 | const ( 16 | Runnable GoroutineStatus = iota + 1 17 | Running 18 | Syscall 19 | Waiting 20 | Moribund 21 | Dead 22 | Enqueue 23 | Copystack 24 | Preempt 25 | ) 26 | 27 | type Allgs struct { 28 | Gs []*Goroutine 29 | Len uint64 30 | } 31 | 32 | type Goroutine struct { 33 | Goid uint64 34 | Status uint32 35 | StackLo uint64 36 | StackHi uint64 37 | Pc, Bp uint64 38 | } 39 | 40 | func (g *Goroutine) Validate(idx uint64) (err error) { 41 | if idx == 0 && g.Goid != 1 { 42 | return fmt.Errorf("goid (%d) != 1", g.Goid) 43 | } 44 | if g.Status < Runnable || g.Status > Preempt { 45 | return fmt.Errorf("status (%d) < 1 || > 9", g.Status) 46 | } 47 | if g.Status != Dead && (g.StackLo == 0 || g.StackHi == 0) { 48 | return fmt.Errorf("allgs.array[%d].stack.lo (%#x) or stack.hi (%#x) == 0", idx, g.StackLo, g.StackHi) 49 | } 50 | if g.StackHi < g.StackLo || (g.StackHi-g.StackLo)%1024 != 0 { 51 | return fmt.Errorf("(allgs.array[%d].stack.hi (%#x) - stack.lo (%#x)) % 1024 != 0", idx, g.StackHi, g.StackLo) 52 | } 53 | return 54 | } 55 | func (g *Goroutine) StatusName() string { 56 | switch g.Status { 57 | case Runnable: 58 | return "runnable" 59 | case Running: 60 | return "running" 61 | case Syscall: 62 | return "syscall" 63 | case Waiting: 64 | return "waiting" 65 | case Moribund: 66 | return "moribund" 67 | case Dead: 68 | return "dead" 69 | case Enqueue: 70 | return "enqueue" 71 | case Copystack: 72 | return "copystack" 73 | case Preempt: 74 | return "preempty" 75 | } 76 | return "unknown" 77 | } 78 | 79 | func (g *Goroutine) Frame() *Frame { 80 | return &Frame{g.Bp} 81 | } 82 | 83 | type Frame struct { 84 | Bp uint64 85 | } 86 | 87 | func (f *Frame) Next(snapshot *proc.Snapshot) *Frame { 88 | return &Frame{Bytes(snapshot.X(f.Bp, 8)).ToUint64()} 89 | } 90 | 91 | func (f *Frame) Pc(snapshot *proc.Snapshot) uint64 { 92 | return Bytes(snapshot.X(f.Bp+8, 8)).ToUint64() 93 | } 94 | 95 | func parseGoroutines(ei *elf.ELFInfo, snapshot *proc.Snapshot) (goroutines []*Goroutine, err error) { 96 | allgs, err := searchAllgs(ei, snapshot) 97 | if err != nil { 98 | return 99 | } 100 | return allgs.Gs, nil 101 | } 102 | 103 | func searchAllgs(ei *elf.ELFInfo, snapshot *proc.Snapshot) (allgs *Allgs, err error) { 104 | for _, piece := range append(snapshot.Texts, snapshot.Others...) { 105 | for addr := piece.Start; addr < piece.Start+piece.Size; addr += 8 { 106 | allgsPointer := &Pointer{ 107 | addr: addr, 108 | proto: ei.AllgsProto, 109 | snapshot: snapshot, 110 | } 111 | if allgs, err = derefAllgs(allgsPointer, ei); err != nil { 112 | continue 113 | } 114 | return 115 | } 116 | } 117 | return allgs, errors.New("allgs not found") 118 | } 119 | 120 | type Pointer struct { 121 | addr uint64 122 | proto *elf.Proto 123 | snapshot *proc.Snapshot 124 | } 125 | 126 | func (p *Pointer) Field(name string) Bytes { 127 | field, found := p.proto.GetField(name) 128 | if !found { 129 | // Should never happen, so panic. 130 | log.Fatalf("field %s not found", name) 131 | } 132 | return p.snapshot.X(p.addr+field.Offset, field.Size) 133 | } 134 | 135 | func (p *Pointer) Index(i, size uint64) Bytes { 136 | return p.snapshot.X(p.addr+i*size, size) 137 | } 138 | 139 | type Bytes []byte 140 | 141 | func (b Bytes) ToUint64() uint64 { 142 | return binary.LittleEndian.Uint64(b) 143 | } 144 | 145 | func (b Bytes) ToUint32() uint32 { 146 | return binary.LittleEndian.Uint32(b) 147 | } 148 | 149 | func derefAllgs(allgsPointer *Pointer, ei *elf.ELFInfo) (allgs *Allgs, err error) { 150 | allgs = &Allgs{ 151 | Len: allgsPointer.Field("len").ToUint64(), 152 | } 153 | if allgs.Len < 3 { 154 | return nil, fmt.Errorf("allgs.len (%d) < 3", allgs.Len) 155 | } 156 | arrayPointer := &Pointer{ 157 | addr: allgsPointer.Field("array").ToUint64(), 158 | snapshot: allgsPointer.snapshot, 159 | } 160 | if arrayPointer.addr == 0 { 161 | return nil, fmt.Errorf("allgs.array == 0") 162 | } 163 | for i := uint64(0); i < allgs.Len; i++ { 164 | gPointer := &Pointer{ 165 | addr: arrayPointer.Index(i, 8).ToUint64(), 166 | proto: ei.GProto, 167 | snapshot: allgsPointer.snapshot, 168 | } 169 | g := &Goroutine{ 170 | Goid: gPointer.Field("goid").ToUint64(), 171 | Status: gPointer.Field("atomicstatus").ToUint32(), 172 | StackLo: gPointer.Field("stack.lo").ToUint64(), 173 | StackHi: gPointer.Field("stack.hi").ToUint64(), 174 | Pc: gPointer.Field("sched.pc").ToUint64(), 175 | Bp: gPointer.Field("sched.bp").ToUint64(), 176 | } 177 | if err = g.Validate(i); err != nil { 178 | return 179 | } 180 | allgs.Gs = append(allgs.Gs, g) 181 | } 182 | return allgs, nil 183 | } 184 | -------------------------------------------------------------------------------- /proc/proc.go: -------------------------------------------------------------------------------- 1 | package proc 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | type Proc struct { 10 | pid int 11 | memFile *os.File 12 | } 13 | 14 | func Get(pid int) *Proc { 15 | memFile, err := os.Open(fmt.Sprintf("/proc/%d/mem", pid)) 16 | if err != nil { 17 | log.Fatalf("Failed to open /proc/%d/mem: %v", pid, err) 18 | } 19 | return &Proc{ 20 | pid: pid, 21 | memFile: memFile, 22 | } 23 | } 24 | 25 | func (proc *Proc) ReadMemory(start, size uint64) (data []byte, err error) { 26 | data = make([]byte, size) 27 | _, err = proc.memFile.ReadAt(data, int64(start)) 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /proc/snapshot.go: -------------------------------------------------------------------------------- 1 | package proc 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/prometheus/procfs" 7 | ) 8 | 9 | type MemoryPiece struct { 10 | Start uint64 11 | Size uint64 12 | Data []byte 13 | } 14 | 15 | func newMemoryPiece(proc *Proc, procMap *procfs.ProcMap) (piece *MemoryPiece, err error) { 16 | piece = &MemoryPiece{} 17 | piece.Start = uint64(procMap.StartAddr) 18 | piece.Size = uint64(procMap.EndAddr) - uint64(procMap.StartAddr) 19 | piece.Data, err = proc.ReadMemory(piece.Start, piece.Size) 20 | return 21 | } 22 | 23 | type Snapshot struct { 24 | Texts, Others, Heaps []*MemoryPiece 25 | } 26 | 27 | func (proc *Proc) Snapshot() (snapshot *Snapshot, err error) { 28 | fs, err := procfs.NewDefaultFS() 29 | if err != nil { 30 | return 31 | } 32 | p, err := fs.Proc(proc.pid) 33 | if err != nil { 34 | return 35 | } 36 | procMaps, err := p.ProcMaps() 37 | if err != nil { 38 | return 39 | } 40 | snapshot = &Snapshot{} 41 | for _, procMap := range procMaps { 42 | if procMap.Perms.Read && strings.HasPrefix(procMap.Pathname, "/") && len(snapshot.Others) == 0 { 43 | piece, err := newMemoryPiece(proc, procMap) 44 | if err != nil { 45 | return nil, err 46 | } 47 | snapshot.Texts = append(snapshot.Texts, piece) 48 | } else if procMap.Perms.Read && procMap.Pathname == "" && procMap.StartAddr >= 0xc000000000 && procMap.StartAddr < 0xd000000000 { 49 | piece, err := newMemoryPiece(proc, procMap) 50 | if err != nil { 51 | return nil, err 52 | } 53 | snapshot.Heaps = append(snapshot.Heaps, piece) 54 | } else if procMap.Perms.Read && len(snapshot.Texts) > 0 { 55 | if len(snapshot.Others) > 0 { 56 | if lastOther := snapshot.Others[len(snapshot.Others)-1]; lastOther.Start+lastOther.Size != uint64(procMap.StartAddr) { 57 | continue 58 | } 59 | } else { 60 | if lastText := snapshot.Texts[len(snapshot.Texts)-1]; lastText.Start+lastText.Size != uint64(procMap.StartAddr) { 61 | continue 62 | } 63 | } 64 | piece, err := newMemoryPiece(proc, procMap) 65 | if err != nil { 66 | return nil, err 67 | } 68 | snapshot.Others = append(snapshot.Others, piece) 69 | } 70 | } 71 | 72 | return 73 | } 74 | 75 | func (s *Snapshot) InitAddr() uint64 { 76 | return s.Texts[0].Start 77 | } 78 | 79 | func (s *Snapshot) X(addr, size uint64) (data []byte) { 80 | data = make([]byte, size) 81 | for _, piece := range append(s.Texts, append(s.Others, s.Heaps...)...) { 82 | if addr >= piece.Start && addr < piece.Start+piece.Size { 83 | data = piece.Data[addr-piece.Start : addr-piece.Start+size] 84 | break 85 | } 86 | } 87 | return 88 | } 89 | --------------------------------------------------------------------------------