1// Copyright 2014 Google Inc. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Package symbolizer provides a routine to populate a profile with 16// symbol, file and line number information. It relies on the 17// addr2liner and demangle packages to do the actual work. 18package symbolizer 19 20import ( 21 "fmt" 22 "io" 23 "net/http" 24 "net/url" 25 "path/filepath" 26 "strings" 27 28 "github.com/google/pprof/internal/binutils" 29 "github.com/google/pprof/internal/plugin" 30 "github.com/google/pprof/internal/symbolz" 31 "github.com/google/pprof/profile" 32 "github.com/ianlancetaylor/demangle" 33) 34 35// Symbolizer implements the plugin.Symbolize interface. 36type Symbolizer struct { 37 Obj plugin.ObjTool 38 UI plugin.UI 39 Transport http.RoundTripper 40} 41 42// test taps for dependency injection 43var symbolzSymbolize = symbolz.Symbolize 44var localSymbolize = doLocalSymbolize 45var demangleFunction = Demangle 46 47// Symbolize attempts to symbolize profile p. First uses binutils on 48// local binaries; if the source is a URL it attempts to get any 49// missed entries using symbolz. 50func (s *Symbolizer) Symbolize(mode string, sources plugin.MappingSources, p *profile.Profile) error { 51 remote, local, fast, force, demanglerMode := true, true, false, false, "" 52 for _, o := range strings.Split(strings.ToLower(mode), ":") { 53 switch o { 54 case "": 55 continue 56 case "none", "no": 57 return nil 58 case "local": 59 remote, local = false, true 60 case "fastlocal": 61 remote, local, fast = false, true, true 62 case "remote": 63 remote, local = true, false 64 case "force": 65 force = true 66 default: 67 switch d := strings.TrimPrefix(o, "demangle="); d { 68 case "full", "none", "templates": 69 demanglerMode = d 70 force = true 71 continue 72 case "default": 73 continue 74 } 75 s.UI.PrintErr("ignoring unrecognized symbolization option: " + mode) 76 s.UI.PrintErr("expecting -symbolize=[local|fastlocal|remote|none][:force][:demangle=[none|full|templates|default]") 77 } 78 } 79 80 var err error 81 if local { 82 // Symbolize locally using binutils. 83 if err = localSymbolize(p, fast, force, s.Obj, s.UI); err != nil { 84 s.UI.PrintErr("local symbolization: " + err.Error()) 85 } 86 } 87 if remote { 88 post := func(source, post string) ([]byte, error) { 89 return postURL(source, post, s.Transport) 90 } 91 if err = symbolzSymbolize(p, force, sources, post, s.UI); err != nil { 92 return err // Ran out of options. 93 } 94 } 95 96 demangleFunction(p, force, demanglerMode) 97 return nil 98} 99 100// postURL issues a POST to a URL over HTTP. 101func postURL(source, post string, tr http.RoundTripper) ([]byte, error) { 102 client := &http.Client{ 103 Transport: tr, 104 } 105 resp, err := client.Post(source, "application/octet-stream", strings.NewReader(post)) 106 if err != nil { 107 return nil, fmt.Errorf("http post %s: %v", source, err) 108 } 109 defer resp.Body.Close() 110 if resp.StatusCode != http.StatusOK { 111 return nil, fmt.Errorf("http post %s: %v", source, statusCodeError(resp)) 112 } 113 return io.ReadAll(resp.Body) 114} 115 116func statusCodeError(resp *http.Response) error { 117 if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") { 118 // error is from pprof endpoint 119 if body, err := io.ReadAll(resp.Body); err == nil { 120 return fmt.Errorf("server response: %s - %s", resp.Status, body) 121 } 122 } 123 return fmt.Errorf("server response: %s", resp.Status) 124} 125 126// doLocalSymbolize adds symbol and line number information to all locations 127// in a profile. mode enables some options to control 128// symbolization. 129func doLocalSymbolize(prof *profile.Profile, fast, force bool, obj plugin.ObjTool, ui plugin.UI) error { 130 if fast { 131 if bu, ok := obj.(*binutils.Binutils); ok { 132 bu.SetFastSymbolization(true) 133 } 134 } 135 136 functions := map[profile.Function]*profile.Function{} 137 addFunction := func(f *profile.Function) *profile.Function { 138 if fp := functions[*f]; fp != nil { 139 return fp 140 } 141 functions[*f] = f 142 f.ID = uint64(len(prof.Function)) + 1 143 prof.Function = append(prof.Function, f) 144 return f 145 } 146 147 missingBinaries := false 148 mappingLocs := map[*profile.Mapping][]*profile.Location{} 149 for _, l := range prof.Location { 150 mappingLocs[l.Mapping] = append(mappingLocs[l.Mapping], l) 151 } 152 for midx, m := range prof.Mapping { 153 locs := mappingLocs[m] 154 if len(locs) == 0 { 155 // The mapping is dangling and has no locations pointing to it. 156 continue 157 } 158 // Do not attempt to re-symbolize a mapping that has already been symbolized. 159 if !force && (m.HasFunctions || m.HasFilenames || m.HasLineNumbers) { 160 continue 161 } 162 if m.File == "" { 163 if midx == 0 { 164 ui.PrintErr("Main binary filename not available.") 165 continue 166 } 167 missingBinaries = true 168 continue 169 } 170 if m.Unsymbolizable() { 171 // Skip well-known system mappings 172 continue 173 } 174 if m.BuildID == "" { 175 if u, err := url.Parse(m.File); err == nil && u.IsAbs() && strings.Contains(strings.ToLower(u.Scheme), "http") { 176 // Skip mappings pointing to a source URL 177 continue 178 } 179 } 180 181 name := filepath.Base(m.File) 182 if m.BuildID != "" { 183 name += fmt.Sprintf(" (build ID %s)", m.BuildID) 184 } 185 f, err := obj.Open(m.File, m.Start, m.Limit, m.Offset, m.KernelRelocationSymbol) 186 if err != nil { 187 ui.PrintErr("Local symbolization failed for ", name, ": ", err) 188 missingBinaries = true 189 continue 190 } 191 if fid := f.BuildID(); m.BuildID != "" && fid != "" && fid != m.BuildID { 192 ui.PrintErr("Local symbolization failed for ", name, ": build ID mismatch") 193 f.Close() 194 continue 195 } 196 symbolizeOneMapping(m, locs, f, addFunction) 197 f.Close() 198 } 199 200 if missingBinaries { 201 ui.PrintErr("Some binary filenames not available. Symbolization may be incomplete.\n" + 202 "Try setting PPROF_BINARY_PATH to the search path for local binaries.") 203 } 204 return nil 205} 206 207func symbolizeOneMapping(m *profile.Mapping, locs []*profile.Location, obj plugin.ObjFile, addFunction func(*profile.Function) *profile.Function) { 208 for _, l := range locs { 209 stack, err := obj.SourceLine(l.Address) 210 if err != nil || len(stack) == 0 { 211 // No answers from addr2line. 212 continue 213 } 214 215 l.Line = make([]profile.Line, len(stack)) 216 l.IsFolded = false 217 for i, frame := range stack { 218 if frame.Func != "" { 219 m.HasFunctions = true 220 } 221 if frame.File != "" { 222 m.HasFilenames = true 223 } 224 if frame.Line != 0 { 225 m.HasLineNumbers = true 226 } 227 f := addFunction(&profile.Function{ 228 Name: frame.Func, 229 SystemName: frame.Func, 230 Filename: frame.File, 231 }) 232 l.Line[i] = profile.Line{ 233 Function: f, 234 Line: int64(frame.Line), 235 Column: int64(frame.Column), 236 } 237 } 238 239 if len(stack) > 0 { 240 m.HasInlineFrames = true 241 } 242 } 243} 244 245// Demangle updates the function names in a profile with demangled C++ 246// names, simplified according to demanglerMode. If force is set, 247// overwrite any names that appear already demangled. 248func Demangle(prof *profile.Profile, force bool, demanglerMode string) { 249 if force { 250 // Remove the current demangled names to force demangling 251 for _, f := range prof.Function { 252 if f.Name != "" && f.SystemName != "" { 253 f.Name = f.SystemName 254 } 255 } 256 } 257 258 options := demanglerModeToOptions(demanglerMode) 259 for _, fn := range prof.Function { 260 demangleSingleFunction(fn, options) 261 } 262} 263 264func demanglerModeToOptions(demanglerMode string) []demangle.Option { 265 switch demanglerMode { 266 case "": // demangled, simplified: no parameters, no templates, no return type 267 return []demangle.Option{demangle.NoParams, demangle.NoEnclosingParams, demangle.NoTemplateParams} 268 case "templates": // demangled, simplified: no parameters, no return type 269 return []demangle.Option{demangle.NoParams, demangle.NoEnclosingParams} 270 case "full": 271 return []demangle.Option{demangle.NoClones} 272 case "none": // no demangling 273 return []demangle.Option{} 274 } 275 276 panic(fmt.Sprintf("unknown demanglerMode %s", demanglerMode)) 277} 278 279func demangleSingleFunction(fn *profile.Function, options []demangle.Option) { 280 if fn.Name != "" && fn.SystemName != fn.Name { 281 return // Already demangled. 282 } 283 // Copy the options because they may be updated by the call. 284 o := make([]demangle.Option, len(options)) 285 copy(o, options) 286 if demangled := demangle.Filter(fn.SystemName, o...); demangled != fn.SystemName { 287 fn.Name = demangled 288 return 289 } 290 // Could not demangle. Apply heuristics in case the name is 291 // already demangled. 292 name := fn.SystemName 293 if looksLikeDemangledCPlusPlus(name) { 294 for _, o := range options { 295 switch o { 296 case demangle.NoParams: 297 name = removeMatching(name, '(', ')') 298 case demangle.NoTemplateParams: 299 name = removeMatching(name, '<', '>') 300 } 301 } 302 } 303 fn.Name = name 304} 305 306// looksLikeDemangledCPlusPlus is a heuristic to decide if a name is 307// the result of demangling C++. If so, further heuristics will be 308// applied to simplify the name. 309func looksLikeDemangledCPlusPlus(demangled string) bool { 310 // Skip java names of the form "class.<init>". 311 if strings.Contains(demangled, ".<") { 312 return false 313 } 314 // Skip Go names of the form "foo.(*Bar[...]).Method". 315 if strings.Contains(demangled, "]).") { 316 return false 317 } 318 return strings.ContainsAny(demangled, "<>[]") || strings.Contains(demangled, "::") 319} 320 321// removeMatching removes nested instances of start..end from name. 322func removeMatching(name string, start, end byte) string { 323 s := string(start) + string(end) 324 var nesting, first, current int 325 for index := strings.IndexAny(name[current:], s); index != -1; index = strings.IndexAny(name[current:], s) { 326 switch current += index; name[current] { 327 case start: 328 nesting++ 329 if nesting == 1 { 330 first = current 331 } 332 case end: 333 nesting-- 334 switch { 335 case nesting < 0: 336 return name // Mismatch, abort 337 case nesting == 0: 338 name = name[:first] + name[current+1:] 339 current = first - 1 340 } 341 } 342 current++ 343 } 344 return name 345} 346