Skip to content

Commit ab8e393

Browse files
committed
Add support for uploading .gopclntab section
This change adds support for uploading the .gopclntab section to SymbolUploader. This is disabled by default, and can be enabled by using the `--upload-gopclntab` CLI flag.
1 parent 3e30c33 commit ab8e393

File tree

10 files changed

+264
-22
lines changed

10 files changed

+264
-22
lines changed

.github/workflows/build.yml

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ jobs:
8282
go install gotest.tools/gotestsum@v1.12.0
8383
- name: Tests
8484
run: |
85+
make test-deps
8586
gotestsum --junitfile gotestsum-report.xml -- ./... -v -race -coverprofile=coverage.txt -covermode=atomic
8687
8788
build:

Makefile

+9-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ lint:
1919
linter-version:
2020
@echo $(GOLANGCI_LINT_VERSION)
2121

22-
test:
22+
TESTDATA_DIRS:= \
23+
reporter/testdata
24+
25+
test-deps:
26+
$(foreach testdata_dir, $(TESTDATA_DIRS), \
27+
($(MAKE) -C "$(testdata_dir)") || exit ; \
28+
)
29+
30+
test: test-deps
2331
go test -v -race ./...
2432

2533
check-copyrights:

cli_flags.go

+9
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type arguments struct {
5454
environment string
5555
uploadSymbols bool
5656
uploadDynamicSymbols bool
57+
uploadGoPCLnTab bool
5758
uploadSymbolsDryRun bool
5859
tags string
5960
timeline bool
@@ -248,6 +249,14 @@ func parseArgs() (*arguments, error) {
248249
Sources: cli.EnvVars("DD_HOST_PROFILING_EXPERIMENTAL_UPLOAD_DYNAMIC_SYMBOLS"),
249250
Destination: &args.uploadDynamicSymbols,
250251
},
252+
&cli.BoolFlag{
253+
Name: "upload-gopclntab",
254+
Usage: "Enable gopcnltab upload.",
255+
Value: false,
256+
Hidden: true,
257+
Sources: cli.EnvVars("DD_HOST_PROFILING_EXPERIMENTAL_UPLOAD_GOPCLNTAB"),
258+
Destination: &args.uploadGoPCLnTab,
259+
},
251260
&cli.BoolFlag{
252261
Name: "upload-symbols-dry-run",
253262
Value: false,

main.go

+1
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ func mainWithExitCode() exitCode {
210210
SymbolUploaderConfig: reporter.SymbolUploaderConfig{
211211
Enabled: args.uploadSymbols,
212212
UploadDynamicSymbols: args.uploadDynamicSymbols,
213+
UploadGoPCLnTab: args.uploadGoPCLnTab,
213214
DryRun: args.uploadSymbolsDryRun,
214215
APIKey: args.apiKey,
215216
APPKey: args.appKey,

reporter/config.go

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ type SymbolUploaderConfig struct {
5555
Enabled bool
5656
// UploadDynamicSymbols defines whether the agent should upload dynamic symbols to the backend.
5757
UploadDynamicSymbols bool
58+
// UploadGoPCLnTab defines whether the agent should upload GoPCLnTab section for Go binaries to the backend.
59+
UploadGoPCLnTab bool
5860
// DryRun defines whether the agent should upload debug symbols to the backend in dry-run mode.
5961
DryRun bool
6062
// DataDog API key

reporter/symbol_uploader.go

+131-21
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,24 @@ import (
3232
"go.opentelemetry.io/ebpf-profiler/libpf"
3333
"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
3434
"go.opentelemetry.io/ebpf-profiler/libpf/readatbuf"
35+
"go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo"
3536
"go.opentelemetry.io/ebpf-profiler/process"
3637
)
3738

38-
const uploadCacheSize = 16384
39-
const uploadQueueSize = 1000
40-
const uploadWorkerCount = 10
39+
const (
40+
uploadCacheSize = 16384
41+
uploadQueueSize = 1000
42+
uploadWorkerCount = 10
4143

42-
const sourceMapEndpoint = "/api/v2/srcmap"
44+
sourceMapEndpoint = "/api/v2/srcmap"
4345

44-
const symbolCopyTimeout = 10 * time.Second
45-
const uploadTimeout = 15 * time.Second
46+
symbolCopyTimeout = 10 * time.Second
47+
uploadTimeout = 15 * time.Second
4648

47-
const buildIDSectionName = ".note.gnu.build-id"
49+
buildIDSectionName = ".note.gnu.build-id"
50+
51+
maxBytesGoPclntab = 128 * 1024 * 1024
52+
)
4853

4954
var debugStrSectionNames = []string{".debug_str", ".zdebug_str", ".debug_str.dwo"}
5055
var debugInfoSectionNames = []string{".debug_info", ".zdebug_info"}
@@ -57,13 +62,16 @@ type uploadData struct {
5762
opener process.FileOpener
5863
}
5964

65+
type goPCLnTabData []byte
66+
6067
type DatadogSymbolUploader struct {
6168
ddAPIKey string
6269
ddAPPKey string
6370
intakeURL string
6471
version string
6572
dryRun bool
6673
uploadDynamicSymbols bool
74+
uploadGoPCLnTab bool
6775
workerCount int
6876

6977
uploadCache *lru.SyncedLRU[libpf.FileID, struct{}]
@@ -112,6 +120,7 @@ func NewDatadogSymbolUploader(cfg SymbolUploaderConfig) (*DatadogSymbolUploader,
112120
version: cfg.Version,
113121
dryRun: cfg.DryRun,
114122
uploadDynamicSymbols: cfg.UploadDynamicSymbols,
123+
uploadGoPCLnTab: cfg.UploadGoPCLnTab,
115124
workerCount: uploadWorkerCount,
116125
client: &http.Client{Timeout: uploadTimeout},
117126
uploadCache: uploadCache,
@@ -191,7 +200,10 @@ func (d *DatadogSymbolUploader) upload(ctx context.Context, uploadData uploadDat
191200
}
192201
defer elfWrapper.Close()
193202

194-
debugElf, symbolSource := elfWrapper.findSymbols()
203+
debugElf, symbolSource, goPCLnTabInfo := elfWrapper.findSymbols()
204+
if goPCLnTabInfo != nil {
205+
log.Infof("Found GoPCLnTab symbols in %s", filePath)
206+
}
195207
if debugElf == nil {
196208
log.Debugf("Skipping symbol upload for executable %s: no debug symbols found", filePath)
197209
return false
@@ -225,7 +237,7 @@ func (d *DatadogSymbolUploader) upload(ctx context.Context, uploadData uploadDat
225237
return true
226238
}
227239

228-
err = d.handleSymbols(ctx, symbolPath, e)
240+
err = d.handleSymbols(ctx, symbolPath, e, goPCLnTabInfo)
229241
if err != nil {
230242
log.Errorf("Failed to handle symbols: %v for executable: %s", err, e)
231243
return false
@@ -316,7 +328,7 @@ func (e *executableMetadata) String() string {
316328
}
317329

318330
func (d *DatadogSymbolUploader) handleSymbols(ctx context.Context, symbolPath string,
319-
e *executableMetadata) error {
331+
e *executableMetadata, goPCLnTabInfo goPCLnTabData) error {
320332
symbolFile, err := os.CreateTemp("", "objcopy-debug")
321333
if err != nil {
322334
return fmt.Errorf("failed to create temp file to extract symbols: %w", err)
@@ -326,9 +338,16 @@ func (d *DatadogSymbolUploader) handleSymbols(ctx context.Context, symbolPath st
326338

327339
ctx, cancel := context.WithTimeout(ctx, symbolCopyTimeout)
328340
defer cancel()
329-
err = d.copySymbols(ctx, symbolPath, symbolFile.Name())
330-
if err != nil {
331-
return fmt.Errorf("failed to copy symbols: %w", err)
341+
if goPCLnTabInfo != nil {
342+
err = copySymbolsAndGoPCLnTab(ctx, symbolPath, symbolFile.Name(), goPCLnTabInfo)
343+
if err != nil {
344+
return fmt.Errorf("failed to copy GoPCLnTab: %w", err)
345+
}
346+
} else {
347+
err = copySymbols(ctx, symbolPath, symbolFile.Name())
348+
if err != nil {
349+
return fmt.Errorf("failed to copy symbols: %w", err)
350+
}
332351
}
333352

334353
err = d.uploadSymbols(ctx, symbolFile, e)
@@ -339,7 +358,38 @@ func (d *DatadogSymbolUploader) handleSymbols(ctx context.Context, symbolPath st
339358
return nil
340359
}
341360

342-
func (d *DatadogSymbolUploader) copySymbols(ctx context.Context, inputPath, outputPath string) error {
361+
func copySymbolsAndGoPCLnTab(ctx context.Context, inputPath, outputPath string,
362+
goPCLnTabInfo goPCLnTabData) error {
363+
gopclntabFile, err := os.CreateTemp("", "gopclntab")
364+
if err != nil {
365+
return fmt.Errorf("failed to create temp file to extract GoPCLnTab: %w", err)
366+
}
367+
defer os.Remove(gopclntabFile.Name())
368+
defer gopclntabFile.Close()
369+
370+
_, err = gopclntabFile.Write(goPCLnTabInfo)
371+
if err != nil {
372+
return fmt.Errorf("failed to write GoPCLnTab: %w", err)
373+
}
374+
375+
args := []string{
376+
"--only-keep-debug",
377+
"--remove-section=.gdb_index",
378+
"--remove-section=.gopclntab",
379+
"--remove-section=.data.rel.ro.gopclntab",
380+
"--add-section", ".gopclntab=" + gopclntabFile.Name(),
381+
"--set-section-flags", ".gopclntab=readonly",
382+
inputPath,
383+
outputPath,
384+
}
385+
_, err = exec.CommandContext(ctx, "objcopy", args...).Output()
386+
if err != nil {
387+
return fmt.Errorf("failed to extract debug symbols: %w", cleanCmdError(err))
388+
}
389+
return nil
390+
}
391+
392+
func copySymbols(ctx context.Context, inputPath, outputPath string) error {
343393
args := []string{
344394
"--only-keep-debug",
345395
"--remove-section=.gdb_index",
@@ -487,6 +537,56 @@ func HasDWARFData(f *pfelf.File) bool {
487537
return len(f.Progs) == 0 && hasBuildID && hasDebugStr
488538
}
489539

540+
func findGoPCLnTab(ef *pfelf.File) (goPCLnTabData, error) {
541+
var err error
542+
var data []byte
543+
544+
if s := ef.Section(".gopclntab"); s != nil {
545+
if data, err = s.Data(maxBytesGoPclntab); err != nil {
546+
return nil, fmt.Errorf("failed to load .gopclntab: %w", err)
547+
}
548+
} else if s := ef.Section(".data.rel.ro.gopclntab"); s != nil {
549+
if data, err = s.Data(maxBytesGoPclntab); err != nil {
550+
return nil, fmt.Errorf("failed to load .data.rel.ro.gopclntab: %w", err)
551+
}
552+
} else if s := ef.Section(".go.buildinfo"); s != nil {
553+
symtab, err := ef.ReadSymbols()
554+
if err != nil {
555+
// It seems the Go binary was stripped. So we use the heuristic approach
556+
// to get the stack deltas.
557+
if data, err = elfunwindinfo.SearchGoPclntab(ef); err != nil {
558+
return nil, fmt.Errorf("failed to search .gopclntab: %w", err)
559+
}
560+
} else {
561+
start, err := symtab.LookupSymbolAddress("runtime.pclntab")
562+
if err != nil {
563+
return nil, fmt.Errorf("failed to load .gopclntab via symbols: %w", err)
564+
}
565+
end, err := symtab.LookupSymbolAddress("runtime.epclntab")
566+
if err != nil {
567+
return nil, fmt.Errorf("failed to load .gopclntab via symbols: %w", err)
568+
}
569+
if start >= end {
570+
return nil, fmt.Errorf("invalid .gopclntab symbols: %v-%v", start, end)
571+
}
572+
data = make([]byte, end-start)
573+
if _, err := ef.ReadVirtualMemory(data, int64(start)); err != nil {
574+
return nil, fmt.Errorf("failed to load .gopclntab via symbols: %w", err)
575+
}
576+
}
577+
}
578+
579+
if data == nil {
580+
return nil, nil
581+
}
582+
583+
if len(data) < 16 {
584+
return nil, fmt.Errorf(".gopclntab is too short (%v)", len(data))
585+
}
586+
587+
return data, nil
588+
}
589+
490590
type elfWrapper struct {
491591
reader process.ReadAtCloser
492592
elfFile *pfelf.File
@@ -523,9 +623,19 @@ func openELF(filePath string, opener process.FileOpener) (*elfWrapper, error) {
523623

524624
// findSymbols attempts to find a symbol source for the elf file, it returns an elfWrapper around the elf file
525625
// with symbols if found, or nil if no symbols were found.
526-
func (e *elfWrapper) findSymbols() (*elfWrapper, SymbolSource) {
626+
func (e *elfWrapper) findSymbols() (*elfWrapper, SymbolSource, goPCLnTabData) {
627+
// Check if the elf file has a GoPCLnTab
628+
goPCLnTabInfo, err := findGoPCLnTab(e.elfFile)
629+
if err == nil {
630+
if goPCLnTabInfo != nil {
631+
return e, GoPCLnTab, goPCLnTabInfo
632+
}
633+
} else {
634+
log.Warnf("Failed to find .gopclntab in %s: %v", e.filePath, err)
635+
}
636+
527637
if HasDWARFData(e.elfFile) {
528-
return e, DebugInfo
638+
return e, DebugInfo, nil
529639
}
530640

531641
log.Debugf("No debug symbols found in %s", e.filePath)
@@ -538,7 +648,7 @@ func (e *elfWrapper) findSymbols() (*elfWrapper, SymbolSource) {
538648
debugElf := e.findDebugSymbolsWithBuildID()
539649
if debugElf != nil {
540650
if HasDWARFData(debugElf.elfFile) {
541-
return debugElf, DebugInfo
651+
return debugElf, DebugInfo, nil
542652
}
543653
debugElf.Close()
544654
log.Debugf("No debug symbols found in buildID link file %s", debugElf.filePath)
@@ -548,23 +658,23 @@ func (e *elfWrapper) findSymbols() (*elfWrapper, SymbolSource) {
548658
debugElf = e.findDebugSymbolsWithDebugLink()
549659
if debugElf != nil {
550660
if HasDWARFData(debugElf.elfFile) {
551-
return debugElf, DebugInfo
661+
return debugElf, DebugInfo, nil
552662
}
553663
log.Debugf("No debug symbols found in debug link file %s", debugElf.filePath)
554664
debugElf.Close()
555665
}
556666

557667
// Check if initial elf file has a symbol table
558668
if e.elfFile.Section(".symtab") != nil {
559-
return e, SymbolTable
669+
return e, SymbolTable, nil
560670
}
561671

562672
// Check if initial elf file has a dynamic symbol table
563673
if e.elfFile.Section(".dynsym") != nil {
564-
return e, DynamicSymbolTable
674+
return e, DynamicSymbolTable, nil
565675
}
566676

567-
return nil, None
677+
return nil, None, nil
568678
}
569679

570680
func (e *elfWrapper) findDebugSymbolsWithBuildID() *elfWrapper {

0 commit comments

Comments
 (0)