diff --git a/Makefile b/Makefile index 3f6dce01e..8566f87b3 100644 --- a/Makefile +++ b/Makefile @@ -122,7 +122,7 @@ test-deps: TEST_INTEGRATION_BINARY_DIRS := tracer processmanager/ebpf support interpreter/golabels/integrationtests -pprof-execs: pprof_1_23 pprof_1_24 pprof_1_24_cgo +pprof-execs: pprof_1_23 pprof_1_24 pprof_1_24_cgo pprof_1_24_cgo_pie pprof_1_23: CGO_ENABLED=0 GOTOOLCHAIN=go1.23.7 go test -C ./interpreter/golabels/integrationtests/pprof -c -trimpath -tags $(GO_TAGS),nocgo,integration -o ./../$@ @@ -133,6 +133,9 @@ pprof_1_24: pprof_1_24_cgo: CGO_ENABLED=1 GOTOOLCHAIN=go1.24.6 go test -C ./interpreter/golabels/integrationtests/pprof -c -ldflags '-extldflags "-static"' -trimpath -tags $(GO_TAGS),withcgo,integration -o ./../$@ +pprof_1_24_cgo_pie: + CGO_ENABLED=1 GOTOOLCHAIN=go1.24.6 go test -C ./interpreter/golabels/integrationtests/pprof -c -ldflags '-extldflags "-static"' -trimpath -buildmode=pie -tags $(GO_TAGS),withcgo,integration -o ./../$@ + integration-test-binaries: generate ebpf pprof-execs $(foreach test_name, $(TEST_INTEGRATION_BINARY_DIRS), \ (go test -ldflags='-extldflags=-static' -trimpath -c \ diff --git a/interpreter/golabels/integrationtests/golabels_integration_test.go b/interpreter/golabels/integrationtests/golabels_integration_test.go index 8935b8274..e55dac75a 100644 --- a/interpreter/golabels/integrationtests/golabels_integration_test.go +++ b/interpreter/golabels/integrationtests/golabels_integration_test.go @@ -31,6 +31,9 @@ var ( //go:embed pprof_1_24_cgo pprof_1_24_cgo []byte + + //go:embed pprof_1_24_cgo_pie + pprof_1_24_cgo_pie []byte ) type mockIntervals struct{} @@ -51,9 +54,10 @@ func Test_Golabels(t *testing.T) { tests := map[string]struct { bin []byte }{ - "pprof_1_23": {bin: pprof_1_23}, - "pprof_1_24": {bin: pprof_1_24}, - "pprof_1_24_cgo": {bin: pprof_1_24_cgo}, + "pprof_1_23": {bin: pprof_1_23}, + "pprof_1_24": {bin: pprof_1_24}, + "pprof_1_24_cgo": {bin: pprof_1_24_cgo}, + "pprof_1_24_cgo_pie": {bin: pprof_1_24_cgo_pie}, } for name, tc := range tests { diff --git a/interpreter/golabels/tls_amd64.go b/interpreter/golabels/tls_amd64.go index 2d4e88d36..67e22a5ee 100644 --- a/interpreter/golabels/tls_amd64.go +++ b/interpreter/golabels/tls_amd64.go @@ -7,6 +7,8 @@ package golabels // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabe import ( log "github.com/sirupsen/logrus" + "go.opentelemetry.io/ebpf-profiler/asm/amd" + e "go.opentelemetry.io/ebpf-profiler/asm/expression" "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" "go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo" "golang.org/x/arch/x86/x86asm" @@ -25,24 +27,48 @@ func extractTLSGOffset(f *pfelf.File) (int32, error) { // Dump of assembler code for function runtime.stackcheck: // 0x0000000000470080 <+0>: mov %fs:0xfffffffffffffff8,%rax + // Binaries built with -buildmode=pie have a different assembly code for stackcheck with 2 movs: + // 0x00000000007ec320 <+0>: mov $0xfffffffffffffff8,%rcx + // 0x00000000007ec327 <+7>: mov %fs:(%rcx),%rax sym, err := pclntab.LookupSymbol("runtime.stackcheck") if err != nil { return 0, err } - b, err := f.VirtualMemory(int64(sym.Address), 10, 10) - if err != nil { - return 0, err - } - i, err := x86asm.Decode(b, 64) + sz := int(min(sym.Size, 128)) + code, err := f.VirtualMemory(int64(sym.Address), sz, sz) if err != nil { return 0, err } - if i.Op == x86asm.MOV { - mem, ok := i.Args[1].(x86asm.Mem) - if ok { + + offset := e.NewImmediateCapture("offset") + it := amd.NewInterpreterWithCode(code) + for { + op, err := it.Step() + if err != nil { + break + } + if op.Op != x86asm.MOV { + continue + } + mem, ok := op.Args[1].(x86asm.Mem) + if !ok || mem.Segment != x86asm.FS { + continue + } + // If the base is 0, it means the offset is directly in the register: + // 0x0000000000470080 <+0>: mov %fs:0xfffffffffffffff8,%rax + if mem.Base == 0 { return int32(mem.Disp), nil } + // Otherwise, the offset is in the register: + // 0x00000000007ec320 <+0>: mov $0xfffffffffffffff8,%rcx + // 0x00000000007ec327 <+7>: mov %fs:(%rcx),%rax + // Check if the register value was set with an immediate value in a previous instruction + // and if so, use that value as the offset. + actual := it.Regs.GetX86(mem.Base) + if actual.Match(offset) { + return int32(offset.CapturedValue()), nil + } } log.Warnf("Failed to decode stackcheck symbol, Go label collection might not work") return -8, nil diff --git a/interpreter/golabels/tls_arm64.go b/interpreter/golabels/tls_arm64.go index e4338b1c5..6e3902cae 100644 --- a/interpreter/golabels/tls_arm64.go +++ b/interpreter/golabels/tls_arm64.go @@ -7,6 +7,7 @@ package golabels // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabe import ( log "github.com/sirupsen/logrus" + "go.opentelemetry.io/ebpf-profiler/armhelpers" "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" "golang.org/x/arch/arm64/arm64asm" ) @@ -21,6 +22,17 @@ import ( // 0x000000000007f270 <+16>: mov x27, #0x30 // #48 // 0x000000000007f274 <+20>: ldr x28, [x0, x27] // 0x000000000007f278 <+24>: ret +// +// And, when compiled with -buildmode=pie: +// +// 0x00000000000c2290 <+0>: adrp x27, 0x2ca000 +// 0x00000000000c2294 <+4>: ldrsb x0, [x27, #1766] +// 0x00000000000c2298 <+8>: cbz x0, 0xc22ac +// 0x00000000000c229c <+12>: mrs x0, tpidr_el0 +// 0x00000000000c22a0 <+16>: movz x27, #0x0, lsl #16 +// 0x00000000000c22a4 <+20>: movk x27, #0x10 +// 0x00000000000c22a8 <+24>: ldr x28, [x0, x27] +// 0x00000000000c22ac <+28>: ret func extractTLSGOffset(f *pfelf.File) (int32, error) { iscgo, err := f.IsCgoEnabled() if err != nil || !iscgo { @@ -47,11 +59,23 @@ func extractTLSGOffset(f *pfelf.File) (int32, error) { if err != nil { return 0, err } - if i.Op == arm64asm.MOV { + switch i.Op { + case arm64asm.MOV: imm, ok := i.Args[1].(arm64asm.Imm64) if ok { return int32(imm.Imm), nil } + case arm64asm.MOVK: + // when compiled with -buildmode=pie, mov instruction is split into two instructions: movz and movk + // movz is used to zero the register and set bits 16-31, while movk is used to set the lower 16 bits: + // movz x27, #0x0, lsl #16 + // movk x27, #0x10 + // For now, we'll just decode the immediate value from the movk instruction since the one from the movz + // instruction seems to always be 0. + imm, ok := armhelpers.DecodeImmediate(i.Args[1]) + if ok { + return int32(imm), nil + } } } log.Warnf("Failed to decode load_g symbol, Go label collection might not work with CGO frames")