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