diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 000000000..f887740fe --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,38 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:best-practices", + "helpers:pinGitHubActionDigestsToSemver" + ], + "packageRules": [ + { + "groupName": "Go dependencies", + "matchManagers": ["gomod"], + "schedule": ["before 8am every weekday"], + "automerge": true + }, + { + "groupName": "Docker related dependencies", + "matchManagers": ["buildpacks", "devcontainer", "docker-compose", "dockerfile"], + "schedule": ["before 8am every weekday"], + "automerge": true + }, + { + "groupName": "GitHub Actions", + "matchManagers": ["github-actions"], + "schedule": ["before 8am every weekday"], + "automerge": true + }, + { + "groupName": "Rust dependencies", + "matchManagers": ["cargo"], + "schedule": ["before 8am every weekday"] + } + ], + "ignoreDeps": [ + "golangci/golangci-lint-action" + ], + "labels": [ + "dependencies" + ] +} diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml new file mode 100644 index 000000000..a71d8fd8a --- /dev/null +++ b/.github/workflows/auto-tag.yml @@ -0,0 +1,36 @@ +name: Monthly Tagging +on: + workflow_dispatch: # Allows manual triggering of the workflow + schedule: + # Run every month on the 3rd day at 08:15 AM. + - cron: '15 8 3 * *' + +permissions: + contents: read + +jobs: + create-monthly-tag: + permissions: + contents: write # required for pushing git tags + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Configure Git + run: | + git config user.name "${GITHUB_ACTOR}" + git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + + - name: Get current year and week number + id: date + run: | + echo "tag_name=v0.0.$(date +%G%V)" >> "$GITHUB_OUTPUT" + + - name: Create and push tag + run: | + TAG_NAME="${{ steps.date.outputs.tag_name }}" + # Create an annotated tag on the latest commit of the current branch (main) + git tag -a $TAG_NAME -m "$TAG_NAME" + # Push the newly created tag to the remote repository + git push origin $TAG_NAME diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8cc89cb3d..a978b1d6c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,6 +8,8 @@ on: schedule: - cron: "21 6 * * 1" +permissions: read-all + jobs: analyze: name: Analyze Go (${{ matrix.target_arch }}) @@ -18,13 +20,13 @@ jobs: target_arch: [amd64, arm64] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up environment uses: ./.github/workflows/env - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: languages: go @@ -33,7 +35,7 @@ jobs: make TARGET_ARCH=${{ matrix.target_arch }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: category: "/language:Go" timeout-minutes: 10 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 61f1eeda3..4db8e6bda 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -4,6 +4,8 @@ on: branches: - main pull_request: +permissions: + contents: read jobs: codespell: runs-on: ubuntu-latest @@ -11,6 +13,6 @@ jobs: - name: Install codespell run: sudo apt-get install codespell - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Codespell run: make codespell diff --git a/.github/workflows/env/action.yml b/.github/workflows/env/action.yml index 3b15d1408..30d06d29b 100644 --- a/.github/workflows/env/action.yml +++ b/.github/workflows/env/action.yml @@ -1,5 +1,12 @@ name: Common environment setup +inputs: + skip_rust: + description: 'Set to true to skip installing Rust toolchains' + required: false + type: boolean + default: false + runs: using: composite steps: @@ -31,12 +38,13 @@ runs: libc6-arm64-cross qemu-user-binfmt libc6:arm64 \ musl-dev:amd64 musl-dev:arm64 musl-tools binutils-aarch64-linux-gnu - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: go.mod cache-dependency-path: go.sum id: go - name: Install Rust + if: ${{ inputs.skip_rust == false }} uses: dtolnay/rust-toolchain@stable with: targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 00d9703d5..783d3c130 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: fossas/fossa-action@93a52ecf7c3ac7eb40f5de77fd69b1a19524de94 # v1.5.0 + - uses: fossas/fossa-action@3ebcea1862c6ffbd5cf1b4d0bd6b3fe7bd6f2cac # v1.7.0 with: api-key: ${{secrets.FOSSA_API_KEY}} team: OpenTelemetry diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index c505340e6..0cecf8015 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -23,7 +23,7 @@ jobs: with: persist-credentials: false - - uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif results_format: sarif @@ -42,6 +42,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 with: sarif_file: results.sarif diff --git a/.github/workflows/push-docker-image.yml b/.github/workflows/push-docker-image.yml index 6a3aa750a..03449e27e 100644 --- a/.github/workflows/push-docker-image.yml +++ b/.github/workflows/push-docker-image.yml @@ -6,28 +6,31 @@ on: paths: - "Dockerfile" +permissions: + contents: read + jobs: build-and-push: runs-on: ubuntu-latest if: github.repository == 'open-telemetry/opentelemetry-ebpf-profiler' steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Set current timestamp tag id: tag run: | echo "tag=$(date +%Y%m%d%H%M)" >> $GITHUB_OUTPUT - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: push: true file: Dockerfile diff --git a/.github/workflows/unit-test-on-pull-request.yml b/.github/workflows/unit-test-on-pull-request.yml index eefe66f76..f6270f9ac 100644 --- a/.github/workflows/unit-test-on-pull-request.yml +++ b/.github/workflows/unit-test-on-pull-request.yml @@ -6,13 +6,16 @@ on: pull_request: branches: ["**"] +permissions: + contents: read + jobs: legal: name: Check licenses of dependencies runs-on: ubuntu-24.04 steps: - name: Clone code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up environment uses: ./.github/workflows/env - name: Check for changes in licenses of dependencies @@ -31,24 +34,20 @@ jobs: target_arch: [amd64, arm64] steps: - name: Clone code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up environment uses: ./.github/workflows/env - name: Get linter version id: linter-version run: (echo -n "version="; make linter-version) >> "$GITHUB_OUTPUT" - - name: golangci-lint - uses: golangci/golangci-lint-action@v6 + - name: linter env: GOARCH: ${{ matrix.target-arch }} CGO_ENABLED: 1 - with: - version: ${{ steps.linter-version.outputs.version }} - - name: Lint eBPF code run: | sudo apt update sudo apt install -y clang-format-17 - make lint -C support/ebpf + make lint test: name: Test (${{ matrix.target_arch }}) @@ -58,47 +57,54 @@ jobs: target_arch: [amd64, arm64] steps: - name: Clone code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up environment uses: ./.github/workflows/env + with: + skip_rust: true - name: Cache coredump modules - uses: actions/cache@v4 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: tools/coredump/modulecache key: coredumps-${{ matrix.target_arch }}-${{ hashFiles('tools/coredump/testdata/*/*.json') }} restore-keys: | coredumps-${{ matrix.target_arch }} coredumps- - - name: Direct Rust test - run: make rust-tests - name: Tests run: make test TARGET_ARCH=${{ matrix.target_arch }} + rust-test: + name: Rust Tests (${{ matrix.target_arch }}) + runs-on: ubuntu-24.04 + strategy: + matrix: + target_arch: [amd64, arm64] + steps: + - name: Clone code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up environment + uses: ./.github/workflows/env + - name: Tests + run: make rust-tests + check-binary-blobs: - name: Check for differences in the eBPF and Rust binary blobs + name: Check for differences in the eBPF binary blobs runs-on: ubuntu-24.04 - container: otel/opentelemetry-ebpf-profiler-dev:latest + container: otel/opentelemetry-ebpf-profiler-dev:latest@sha256:acce547f366150eb25392e1aff270df430ef6b759baeb4292999116018e70e6e defaults: run: shell: bash --login {0} steps: - name: Clone code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Hash binary blobs run: | sha256sum support/ebpf/tracer.ebpf.release.* > binary-blobs.hash - sha256sum target/x86_64-unknown-linux-musl/release/libsymblib_capi.a >> binary-blobs.hash - sha256sum target/aarch64-unknown-linux-musl/release/libsymblib_capi.a >> binary-blobs.hash - name: Rebuild eBPF blobs run: | rm support/ebpf/tracer.ebpf.release.* make amd64 -C support/ebpf make arm64 -C support/ebpf - - name: Rebuild Rust blobs - run: | - rm -rf target/ - make rust-components TARGET_ARCH=amd64 - make rust-components TARGET_ARCH=arm64 - name: Check for differences run: | if ! sha256sum --check binary-blobs.hash; then @@ -115,13 +121,13 @@ jobs: target_arch: [amd64, arm64] steps: - name: Clone code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up environment uses: ./.github/workflows/env - name: Prepare integration test binaries for qemu tests run: make integration-test-binaries TARGET_ARCH=${{ matrix.target_arch }} - name: Upload integration test binaries - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: integration-test-binaries-${{ matrix.target_arch }} path: support/*.test @@ -138,7 +144,6 @@ jobs: # https://github.com/cilium/ci-kernels/pkgs/container/ci-kernels/versions?filters%5Bversion_type%5D=tagged # AMD64 - - { target_arch: amd64, kernel: 4.19.314 } - { target_arch: amd64, kernel: 5.4.276 } - { target_arch: amd64, kernel: 5.10.217 } - { target_arch: amd64, kernel: 5.15.159 } @@ -155,7 +160,7 @@ jobs: - { target_arch: arm64, kernel: 6.12.16 } steps: - name: Clone code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install dependencies run: | sudo apt-get update -y @@ -167,7 +172,7 @@ jobs: go install github.com/florianl/bluebox@v0.0.1 sudo mv ~/go/bin/bluebox /usr/local/bin/. - name: Fetch integration test binaries - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: { name: "integration-test-binaries-${{ matrix.target_arch }}" } - name: Fetch precompiled kernel run: | diff --git a/.gitignore b/.gitignore index f2cfd7c8c..8e11b42e0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,3 @@ ebpf-profiler ci-kernels # Ignore target directory target/* -# But not these specific paths -!target/x86_64-unknown-linux-musl/release/libsymblib_capi.a -!target/aarch64-unknown-linux-musl/release/libsymblib_capi.a diff --git a/.golangci.yml b/.golangci.yml index ec90a4a9e..b1eca4bcc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,41 +1,27 @@ +version: "2" + +formatters: + enable: + - gofmt + - goimports + settings: + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: + - github.com/open-telemetry/opentelemetry-ebpf-profiler + run: timeout: 10m build-tags: - integration - linux -issues: - exclude-dirs: - - artifacts - - build-targets - - design - - docker-images - - docs - - etc - - experiments - - infrastructure - - legal - - libpf-rs - - mocks - - pf-code-indexing-service/cibackend/gomock_* - - pf-debug-metadata-service/dmsbackend/gomock_* - - pf-host-agent/support/ci-kernels - - pf-storage-backend/storagebackend/gomock_* - - scratch - - systemtests/benchmarks/_outdata - - target - - virt-tests - - vm-images - - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - # Don't complain about integer overflows - - text: "G115:" - linters: - - gosec - linters: - enable-all: true + default: all disable: # Disabled because of # - too many non-sensical warnings @@ -50,13 +36,14 @@ linters: - dupword - durationcheck # might be worth fixing - err113 + - errcheck - errorlint # might be worth fixing - exhaustive - exhaustruct - forbidigo - forcetypeassert # might be worth fixing - funlen - - gci # might be worth fixing + - funcorder - gochecknoglobals - gochecknoinits - gocognit @@ -64,8 +51,8 @@ linters: - gocyclo - godot - godox # complains about TODO etc - - gofumpt - gomoddirectives + - gosmopolitan - inamedparam - interfacebloat - ireturn @@ -81,6 +68,7 @@ linters: - paralleltest - protogetter - sqlclosecheck # might be worth fixing + - staticcheck - tagalign - tagliatelle - testableexamples # might be worth fixing @@ -94,45 +82,65 @@ linters: # we don't want to change code to Go 1.22+ yet - intrange - copyloopvar - - tenv -linters-settings: - goconst: - min-len: 2 - min-occurrences: 2 - gocritic: - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - disabled-checks: - - dupImport # https://github.com/go-critic/go-critic/issues/845 - - ifElseChain - - whyNoLint - - sloppyReassign - - uncheckedInlineErr # Experimental rule with high false positive rate. - gocyclo: - min-complexity: 15 - govet: - enable-all: true - disable: - - fieldalignment - settings: - printf: # analyzer name, run `go tool vet help` to see all analyzers - funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer - - debug,debugf,debugln - - error,errorf,errorln - - fatal,fatalf,fataln - - info,infof,infoln - - log,logf,logln - - warn,warnf,warnln - - print,printf,println,sprint,sprintf,sprintln,fprint,fprintf,fprintln - lll: - line-length: 100 - tab-width: 4 - misspell: - locale: US - ignore-words: - - rela + exclusions: + paths: + - design-docs + - doc + - legal + - target + + settings: + goconst: + min-len: 2 + min-occurrences: 2 + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - dupImport # https://github.com/go-critic/go-critic/issues/845 + - ifElseChain + - whyNoLint + - sloppyReassign + - uncheckedInlineErr # experimental rule with high false positive rate. + - importShadow # shadow of imported package + gocyclo: + min-complexity: 15 + gosec: + excludes: + - G103 # unsafe calls should be audited + - G115 # integer overflow + - G204 # subprocess launched with variable + - G301 # directory permissions + - G302 # file permissions + - G304 # potential file inclusion via variable + govet: + enable-all: true + disable: + - fieldalignment + - unsafeptr + settings: + printf: # analyzer name, run `go tool vet help` to see all analyzers + funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer + - debug,debugf,debugln + - error,errorf,errorln + - fatal,fatalf,fataln + - info,infof,infoln + - log,logf,logln + - warn,warnf,warnln + - print,printf,println,sprint,sprintf,sprintln,fprint,fprintf,fprintln + lll: + line-length: 100 + tab-width: 4 + misspell: + locale: US + ignore-rules: + - rela + revive: + rules: + - name: unexported-naming + disabled: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4dfd6eaa5..170b20076 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ slack channel for discussions and questions. ## Pre-requisites -- Linux (4.19+ for x86-64, 5.5+ for ARM64) with eBPF enabled (the profiler currently only runs on Linux) +- Linux (5.4+ for x86-64, 5.5+ for ARM64) with eBPF enabled (the profiler currently only runs on Linux) - Go as specified in [go.mod](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/main/go.mod) - docker - Rust as specified in [Cargo.toml](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/main/Cargo.toml) @@ -134,12 +134,6 @@ Any [Maintainer] can merge the PR once the above criteria have been met. ## Approvers and Maintainers -### Approvers - -- [Florian Lehner](https://github.com/florianl), Elastic -- [Joel Höner](https://github.com/athre0z) -- [Tim Rühsen](https://github.com/rockdaboot), Elastic - ### Maintainers - [Christos Kalkanis](https://github.com/christos68k), Elastic @@ -147,6 +141,17 @@ Any [Maintainer] can merge the PR once the above criteria have been met. - [Felix Geisendörfer](https://github.com/felixge), Datadog - [Timo Teräs](https://github.com/fabled) +For more information about the maintainer role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#maintainer). + +### Approvers + +- [Florian Lehner](https://github.com/florianl), Elastic +- [Joel Höner](https://github.com/athre0z) +- [Tim Rühsen](https://github.com/rockdaboot), Elastic +- [Damien Mathieu](https://github.com/dmathieu), Elastic + +For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#approver). + ### Become an Approver or a Maintainer See the [community membership document in OpenTelemetry community diff --git a/Cargo.lock b/Cargo.lock index 381f47901..ed9bbe0d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,10 @@ version = 3 [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "cpp_demangle" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8227005286ec39567949b33df9896bcadfa6051bccca2488129f108ca23119" +checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" dependencies = [ "cfg-if", ] @@ -137,9 +137,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys", @@ -153,9 +153,9 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fixedbitset" @@ -165,9 +165,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.30" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -183,11 +183,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + [[package]] name = "gimli" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e1d97fbe9722ba9bbd0c97051c2956e726562b61f86a25a4360398a40edfc9" +checksum = "93563d740bc9ef04104f9ed6f86f1e3275c2cdafb95664e26584b9ca807a8ffe" dependencies = [ "fallible-iterator", "stable_deref_trait", @@ -233,6 +245,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "jobserver" version = "0.1.31" @@ -244,15 +262,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "log" @@ -262,9 +280,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "lru" -version = "0.12.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" [[package]] name = "memchr" @@ -274,20 +292,20 @@ checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memmap2" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -298,9 +316,9 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "object" -version = "0.36.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "03fd943161069e1768b4b3d050890ba48730e590f57e56d4aa04e7e090e61b4a" dependencies = [ "memchr", ] @@ -339,18 +357,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.84" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.12.6" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ "bytes", "prost-derive", @@ -358,11 +376,10 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.6" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" dependencies = [ - "bytes", "heck", "itertools", "log", @@ -379,9 +396,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.6" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", "itertools", @@ -392,9 +409,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.6" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" dependencies = [ "prost", ] @@ -408,6 +425,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "regex" version = "1.10.6" @@ -439,15 +462,15 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustix" -version = "0.38.34" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags", "errno", @@ -456,11 +479,49 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -469,9 +530,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "stable_deref_trait" @@ -508,15 +569,16 @@ name = "symblib-capi" version = "0.0.0" dependencies = [ "fallible-iterator", + "serde_json", "symblib", "thiserror", ] [[package]] name = "syn" -version = "2.0.77" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -525,30 +587,31 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", + "getrandom", + "once_cell", "rustix", "windows-sys", ] [[package]] name = "thiserror" -version = "1.0.61" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -573,6 +636,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -646,11 +718,20 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "zstd" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ "zstd-safe", ] diff --git a/Cargo.toml b/Cargo.toml index 30565f389..d86a1fff7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,18 +42,18 @@ fallible-iterator = "0.3" flate2 = "1" memmap2 = "0.9.0" native-tls = "0.2" -prost = "0.12.1" -prost-build = "0.12.1" +prost = "0.14.0" +prost-build = "0.14.0" rustc-demangle = "0.1" serde_json = "1" sha2 = "0.10" tempfile = "3" -thiserror = "1" +thiserror = "2" zstd = "0.13.0" zydis = "4.1.1" [workspace.dependencies.gimli] -version = "0.30.0" +version = "0.32.0" default-features = false features = ["std", "endian-reader", "fallible-iterator"] @@ -63,11 +63,11 @@ default-features = false features = ["std"] [workspace.dependencies.lru] -version = "0.12.0" +version = "0.16.0" default-features = false [workspace.dependencies.object] -version = "0.36.0" +version = "0.37.0" default-features = false features = ["std", "read_core", "elf", "macho", "unaligned"] diff --git a/Dockerfile b/Dockerfile index 5c2f0562e..71a273340 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:testing-20241223-slim +FROM debian:testing-20241223-slim@sha256:2ed89b1e8012d945cfcc111fa1dc11a628edaa24b9af5d63d6935b5ee35d3377 WORKDIR /agent diff --git a/LICENSES/github.com/jsimonetti/rtnetlink/LICENSE.md b/LICENSES/github.com/mdlayher/kobject/LICENSE.md similarity index 96% rename from LICENSES/github.com/jsimonetti/rtnetlink/LICENSE.md rename to LICENSES/github.com/mdlayher/kobject/LICENSE.md index 9c073eb9a..ffcdf89c9 100644 --- a/LICENSES/github.com/jsimonetti/rtnetlink/LICENSE.md +++ b/LICENSES/github.com/mdlayher/kobject/LICENSE.md @@ -1,7 +1,7 @@ MIT License =========== -Copyright (C) 2016 Jeroen Simonetti +Copyright (C) 2017 Matt Layher Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/LICENSES/golang.org/x/arch/arm64/arm64asm/LICENSE b/LICENSES/golang.org/x/arch/LICENSE similarity index 100% rename from LICENSES/golang.org/x/arch/arm64/arm64asm/LICENSE rename to LICENSES/golang.org/x/arch/LICENSE diff --git a/Makefile b/Makefile index 80ad4b6a4..e1870e60d 100644 --- a/Makefile +++ b/Makefile @@ -16,13 +16,10 @@ endif # Valid values are: amd64, arm64. TARGET_ARCH ?= $(NATIVE_ARCH) - -ifeq ($(NATIVE_ARCH),$(TARGET_ARCH)) -ARCH_PREFIX := -else ifeq ($(TARGET_ARCH),arm64) -ARCH_PREFIX := aarch64-linux-gnu- +ifeq ($(TARGET_ARCH),arm64) +ARCH_PREFIX := aarch64 else ifeq ($(TARGET_ARCH),amd64) -ARCH_PREFIX := x86_64-linux-gnu- +ARCH_PREFIX := x86_64 else $(error Unsupported architecture: $(TARGET_ARCH)) endif @@ -30,8 +27,8 @@ endif export TARGET_ARCH export CGO_ENABLED = 1 export GOARCH = $(TARGET_ARCH) -export CC = $(ARCH_PREFIX)gcc -export OBJCOPY = $(ARCH_PREFIX)objcopy +export CC = $(ARCH_PREFIX)-linux-gnu-gcc +export OBJCOPY = $(ARCH_PREFIX)-linux-gnu-objcopy BRANCH = $(shell git rev-parse --abbrev-ref HEAD | tr -d '-' | tr '[:upper:]' '[:lower:]') COMMIT_SHORT_SHA = $(shell git rev-parse --short=8 HEAD) @@ -46,7 +43,7 @@ LDFLAGS := -X go.opentelemetry.io/ebpf-profiler/vc.version=$(VERSION) \ -extldflags=-static GO_TAGS := osusergo,netgo -EBPF_FLAGS := +EBPF_FLAGS := GO_FLAGS := -buildvcs=false -ldflags="$(LDFLAGS)" @@ -74,31 +71,22 @@ generate: ebpf: generate $(MAKE) $(EBPF_FLAGS) -C support/ebpf -ebpf-profiler: generate ebpf rust-components +ebpf-profiler: generate ebpf go build $(GO_FLAGS) -tags $(GO_TAGS) rust-targets: -ifeq ($(TARGET_ARCH),arm64) - rustup target add aarch64-unknown-linux-musl -else ifeq ($(TARGET_ARCH),amd64) - rustup target add x86_64-unknown-linux-musl -endif + rustup target add $(ARCH_PREFIX)-unknown-linux-musl rust-components: rust-targets -ifeq ($(TARGET_ARCH),arm64) - RUSTFLAGS="--remap-path-prefix $(PWD)=/" cargo build --lib --release --target aarch64-unknown-linux-musl -else ifeq ($(TARGET_ARCH),amd64) - RUSTFLAGS="--remap-path-prefix $(PWD)=/" cargo build --lib --release --target x86_64-unknown-linux-musl -endif + RUSTFLAGS="--remap-path-prefix $(PWD)=/" cargo build --lib --release --target $(ARCH_PREFIX)-unknown-linux-musl rust-tests: rust-targets cargo test -GOLANGCI_LINT_VERSION = "v1.64.5" +GOLANGCI_LINT_VERSION = "v2.1.6" lint: generate vanity-import-check $(MAKE) lint -C support/ebpf - go run github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) version - go run github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) run + docker run --rm -t -v $$(pwd):/app -w /app golangci/golangci-lint:$(GOLANGCI_LINT_VERSION) sh -c "golangci-lint version && golangci-lint config verify && golangci-lint run --max-issues-per-linter -1 --max-same-issues -1" format-ebpf: $(MAKE) format -C support/ebpf @@ -117,6 +105,11 @@ vanity-import-fix: $(PORTO) test: generate ebpf test-deps go test $(GO_FLAGS) -tags $(GO_TAGS) ./... +# This target isn't called from CI, it doesn't work for cross compile (ie TARGET_ARCH=arm64 on +# amd64) and the CI kernel tests run them already. Useful for local testing. +sudo-golabels-test: integration-test-binaries + (cd support && sudo ./interpreter_golabels_test.test -test.v) + TESTDATA_DIRS:= \ nativeunwind/elfunwindinfo/testdata \ libpf/pfelf/testdata \ @@ -127,9 +120,19 @@ test-deps: ($(MAKE) -C "$(testdata_dir)") || exit ; \ ) -TEST_INTEGRATION_BINARY_DIRS := tracer processmanager/ebpf support +TEST_INTEGRATION_BINARY_DIRS := tracer processmanager/ebpf support interpreter/golabels/test + +# These binaries are named ".test" to get included into bluebox initramfs +support/golbls_1_23.test: ./interpreter/golabels/test/main.go + CGO_ENABLED=0 GOTOOLCHAIN=go1.23.7 go build -tags $(GO_TAGS),nocgo -o $@ $< + +support/golbls_1_24.test: ./interpreter/golabels/test/main.go + CGO_ENABLED=0 GOTOOLCHAIN=go1.24.1 go build -tags $(GO_TAGS),nocgo -o $@ $< + +support/golbls_cgo.test: ./interpreter/golabels/test/main-cgo.go + GOTOOLCHAIN=go1.24.1 go build -ldflags '-extldflags "-static"' -tags $(GO_TAGS),usecgo -o $@ $< -integration-test-binaries: generate ebpf +integration-test-binaries: generate ebpf rust-components support/golbls_1_23.test support/golbls_1_24.test support/golbls_cgo.test $(foreach test_name, $(TEST_INTEGRATION_BINARY_DIRS), \ (go test -ldflags='-extldflags=-static' -trimpath -c \ -tags $(GO_TAGS),static_build,integration \ diff --git a/README.md b/README.md index 03499ec90..3c3e689fd 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,10 @@ The resulting binary will be named in the current directory. ## Other OSes Since the profiler is Linux-only, macOS and Windows users need to set up a Linux VM to build and run the agent. Ensure the appropriate architecture is specified if using cross-compilation. Use the same make targets as above after the Linux environment is configured in the VM. +## Supported Linux kernel version + +[7ddc23ea](https://github.com/open-telemetry/opentelemetry-ebpf-profiler/commit/7ddc23ea135a2e00fffc17850ab90534e9b63108) is the last commit with support for 4.19. Changes after this commit may require a minimal Linux kernel version of 5.4. + ## Alternative Build (Without Docker) You can build the agent without Docker by directly installing the dependencies listed in the Dockerfile. Once dependencies are set up, simply run: ```sh @@ -352,9 +356,10 @@ traces user-land will simply read and then clear this map on a timer. The BPF components are responsible for notifying user-land about new and exiting processes. An event about a new process is produced when we first interrupt it with the unwinders. Events about exiting processes are created with a -`sched_process_exit` probe. In both cases the BPF code sends a perf event to +`sched_process_free` tracepoint. In both cases the BPF code sends a perf event to notify user-land. We also re-report a PID if we detect execution in previously -unknown memory region to prompt re-scan of the mappings. +unknown memory region to prompt re-scan of the mappings. Finally, the profiler +can also profile processes whose main thread exits, leaving other threads running. ### Network protocol diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 000000000..633474854 --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,22 @@ +# Versioning Policy +This document outlines the versioning strategy for the OpenTelemetry eBPF Profiler project. + +# Development Status +This project is currently under active development. As such, users should be aware that significant changes, including breaking API modifications, may occur at any time. + +# Automatic Version Tagging +Automatic version tags are generated on a monthly basis to reflect ongoing development progress. + +# Versioning Scheme +This project adheres to [Semantic Versioning 2](https://semver.org/spec/v2.0.0.html). + +Major Version Zero (0.y.z): While the project is in its initial development phase, the major version will remain 0. Anything MAY change at any time. The public API SHOULD NOT be considered stable. Users are advised to exercise caution and expect potential breaking changes without prior notice during this phase. + +# Automatic Tag Format +The format for automatically generated tags currently follows v0.0.x, where x represents the year followed by the week number. + +## Example: + +- `v0.0.202501` would indicate the first week of 2025. + +- `v0.0.202515` would indicate the fifteenth week of 2025. diff --git a/asm/amd/insn.go b/asm/amd/insn.go new file mode 100644 index 000000000..0c0da32fc --- /dev/null +++ b/asm/amd/insn.go @@ -0,0 +1,89 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package amd // import "go.opentelemetry.io/ebpf-profiler/asm/amd" +import ( + "bytes" + + "go.opentelemetry.io/ebpf-profiler/libpf" + "golang.org/x/arch/x86/x86asm" +) + +// https://www.felixcloutier.com/x86/endbr64 +var opcodeEndBr64 = []byte{0xf3, 0x0f, 0x1e, 0xfa} + +// DecodeSkippable decodes an instruction that we don't care much about and are going to skip, +// as golang.org/x/arch/x86/x86asm fails to decode it. +// The second returned argument is the size of the decoded instruction to skip. +func DecodeSkippable(code []byte) (ok bool, size int) { + switch { + case bytes.HasPrefix(code, opcodeEndBr64): + return true, len(opcodeEndBr64) + default: + return false, 0 + } +} + +// FindExternalJump decodes every instruction in the sym function and searches for +// a relative jump outside itself - to an address not covered by the sym. +// FindExternalJump returns the destination address of the relative jump outside the function or 0. +func FindExternalJump(code []byte, f *libpf.Symbol) (libpf.Address, error) { + var ( + err error + inst x86asm.Inst + rip = int64(f.Address) + ) + for len(code) > 0 { + if ok, l := DecodeSkippable(code); ok { + inst = x86asm.Inst{Op: x86asm.NOP, Len: l} + } else { + inst, err = x86asm.Decode(code, 64) + if err != nil { + return 0, err + } + } + rip += int64(inst.Len) + code = code[inst.Len:] + if !isJump(inst.Op) { + continue + } + if rel, ok := inst.Args[0].(x86asm.Rel); !ok { + continue + } else { + dst := rip + int64(rel) + if dst >= int64(f.Address) && dst < int64(f.Address)+int64(f.Size) { + continue + } + return libpf.Address(dst), nil + } + } + return 0, nil +} + +func isJump(op x86asm.Op) bool { + switch op { + case x86asm.JA, + x86asm.JAE, + x86asm.JB, + x86asm.JBE, + x86asm.JCXZ, + x86asm.JE, + x86asm.JECXZ, + x86asm.JG, + x86asm.JGE, + x86asm.JL, + x86asm.JLE, + x86asm.JMP, + x86asm.JNE, + x86asm.JNO, + x86asm.JNP, + x86asm.JNS, + x86asm.JO, + x86asm.JP, + x86asm.JRCXZ, + x86asm.JS: + return true + default: + return false + } +} diff --git a/asm/amd/insn_test.go b/asm/amd/insn_test.go new file mode 100644 index 000000000..3dd3708f4 --- /dev/null +++ b/asm/amd/insn_test.go @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package amd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEndBr64(t *testing.T) { + res, n := DecodeSkippable([]byte{0xF3, 0x0F, 0x1E, 0xFA}) + assert.True(t, res) + assert.Equal(t, 4, n) + + res, _ = DecodeSkippable([]byte{}) + assert.False(t, res) +} diff --git a/asm/amd/regs_state.go b/asm/amd/regs_state.go new file mode 100644 index 000000000..92b36ee39 --- /dev/null +++ b/asm/amd/regs_state.go @@ -0,0 +1,66 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package amd // import "go.opentelemetry.io/ebpf-profiler/asm/amd" + +import "golang.org/x/arch/x86/x86asm" + +// regIndex returns index into RegsState.regs +func regIndex(reg x86asm.Reg) int { + switch reg { + case x86asm.RAX, x86asm.EAX: + return 1 + case x86asm.RBX, x86asm.EBX: + return 2 + case x86asm.RCX, x86asm.ECX: + return 3 + case x86asm.RDX, x86asm.EDX: + return 4 + case x86asm.RDI, x86asm.EDI: + return 5 + case x86asm.RSI, x86asm.ESI: + return 6 + case x86asm.RBP, x86asm.EBP: + return 7 + case x86asm.R8, x86asm.R8L: + return 8 + case x86asm.R9, x86asm.R9L: + return 9 + case x86asm.R10, x86asm.R10L: + return 10 + case x86asm.R11, x86asm.R11L: + return 11 + case x86asm.R12, x86asm.R12L: + return 12 + case x86asm.R13, x86asm.R13L: + return 13 + case x86asm.R14, x86asm.R14L: + return 14 + case x86asm.R15, x86asm.R15L: + return 15 + case x86asm.RSP, x86asm.ESP: + return 16 + case x86asm.RIP: + return 17 + default: + return 0 + } +} + +type RegsState struct { + regs [18]regState +} + +func (r *RegsState) Set(reg x86asm.Reg, value, loadedFrom uint64) { + r.regs[regIndex(reg)].Value = value + r.regs[regIndex(reg)].LoadedFrom = loadedFrom +} + +func (r *RegsState) Get(reg x86asm.Reg) (value, loadedFrom uint64) { + return r.regs[regIndex(reg)].Value, r.regs[regIndex(reg)].LoadedFrom +} + +type regState struct { + LoadedFrom uint64 + Value uint64 +} diff --git a/collector/factory_test.go b/collector/factory_test.go index 2f0e30a34..7e8878525 100644 --- a/collector/factory_test.go +++ b/collector/factory_test.go @@ -36,10 +36,11 @@ func TestCreateProfilesReceiver(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { t.Parallel() - - _, err := createProfilesReceiver( + typ, err := component.NewType("ProfilesReceiver") + require.NoError(t, err) + _, err = createProfilesReceiver( context.Background(), - receivertest.NewNopSettings(), + receivertest.NewNopSettings(typ), tt.config, consumertest.NewNop(), ) diff --git a/collector/internal/controller.go b/collector/internal/controller.go index 17662a552..b5597e29e 100644 --- a/collector/internal/controller.go +++ b/collector/internal/controller.go @@ -12,6 +12,7 @@ import ( "go.opentelemetry.io/ebpf-profiler/internal/controller" "go.opentelemetry.io/ebpf-profiler/reporter" "go.opentelemetry.io/ebpf-profiler/times" + "go.opentelemetry.io/ebpf-profiler/vc" ) // Controller is a bridge between the Collector's [receiverprofiles.Profiles] @@ -26,6 +27,8 @@ func NewController(cfg *controller.Config, cfg.MonitorInterval, cfg.ProbabilisticInterval) rep, err := reporter.NewCollector(&reporter.Config{ + Name: "otelcol-ebpf-profiler", + Version: vc.Version(), MaxRPCMsgSize: 32 << 20, // 32 MiB MaxGRPCRetries: 5, GRPCOperationTimeout: intervals.GRPCOperationTimeout(), @@ -34,8 +37,7 @@ func NewController(cfg *controller.Config, ReportInterval: intervals.ReportInterval(), ExecutablesCacheElements: 16384, // Next step: Calculate FramesCacheElements from numCores and samplingRate. - FramesCacheElements: 65536, - CGroupCacheElements: 1024, + FramesCacheElements: 131072, SamplesPerSecond: cfg.SamplesPerSecond, }, nextConsumer) if err != nil { diff --git a/design-docs/00002-custom-labels/README.md b/design-docs/00002-custom-labels/README.md new file mode 100644 index 000000000..ce753b9b7 --- /dev/null +++ b/design-docs/00002-custom-labels/README.md @@ -0,0 +1,40 @@ +Custom Labels +============= + +# Meta + +- **Author(s)**: Tommy Reilly +- **Start Date**: 2025-05-15 +- **Goal End Date**: 2025-06-15 +- **Primary Reviewers**: Florian Lehner, Timo Teräs, Brennan Vincent + +# Problem + +Sometimes understanding performance issues is hard because there's no way to dissect hotspots by attributes that aren't visible in the program structure. For instance in a database that uses a generic query execution path to execute all queries you may want to see how much CPU cycles are on behalf of internal queries vs external queries, or you might want to see which user is doing the most queries. This requires attaching metadata to each sample. In Go this is typically done with pprof labels and pprof data can be split out by different values of these labels (example: https://www.polarsignals.com/blog/posts/2021/04/13/demystifying-pprof-labels-with-go). + +In addition to pprof labels more examples of where custom labels could be used (out of the box only pprof labels are supported, these theoretical use cases are only intended to help understand the design space better). + +- Trace IDs for supporting queries of CPU resources used by a particular traceid +- Runtime metadata like "goid" so that CPU resources associated by a particular Goroutine can be discerned +- Arbitrary application/workload specific metadata like user, client or query + +This design doc describes how we can surface Go pprof labels in the OTel profiler and lays the groundwork for doing similar things for other languages but how languages besides Go are supported is beyond the scope of this document. + +# Success criteria + +- Any native language unwinder should be able to add custom labels to each sample, ie it should not be Go specific even if Go is the initial target +- Custom labels should have its own trace type for enable/disable purposes even though it is technically not an unwinder +- When disabled custom labels has little to no impact on performance or memory usage of the profiler +- Custom labels should be limited so that even if a program has thousands of eligible labels the number supported is reasonably small (mostly enforced by eBPF itself) +- Custom labels should be short and have fixed memory overhead +- The custom labels should be made available to the reporter backend but otherwise it should be left up to implementors what to do with them + +# Scope + +The initial proposal will only deal with Go pprof labels which are just string/string key/value pairs, more custom labels for Go or other languages may be added in the future. The initial proposal is to get up to 10 labels in best effort fashion, if any eBPF errors occur there may be fewer labels and there is no proposed mechanism for deciding which labels to grab. Even though the OTel proto allows arbitrary types for the value the initial implementation will be scoped to just strings. + +# Proposed Solution + +The solution we propose is to add support for 10 64 byte custom labels associated with each sample with 16 bytes for the label key and 48 bytes for the label value. These will be stored in the Trace struct with the stack frame information for each sample so each Trace will be 640 bytes larger than before. + +In Go 1.23 and lower labels are stored in a map so its non-deterministic which labels are read from the program, in Go 1.24+ the labels are stored in a list sorted by their keys so it will be first come first serve which labels are extracted. If the labels key or value is larger than 16/48 bytes they will be truncated. No effort is made to validate the strings from a UTF8 perspective. diff --git a/go.mod b/go.mod index 8ed40ee08..a33942bed 100644 --- a/go.mod +++ b/go.mod @@ -3,61 +3,64 @@ module go.opentelemetry.io/ebpf-profiler go 1.23.6 require ( - github.com/aws/aws-sdk-go-v2 v1.30.5 - github.com/aws/aws-sdk-go-v2/config v1.27.35 - github.com/aws/aws-sdk-go-v2/service/s3 v1.62.0 + github.com/aws/aws-sdk-go-v2 v1.36.5 + github.com/aws/aws-sdk-go-v2/config v1.29.17 + github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 github.com/cespare/xxhash/v2 v2.3.0 - github.com/cilium/ebpf v0.16.0 + github.com/cilium/ebpf v0.19.0 github.com/elastic/go-freelru v0.16.0 - github.com/elastic/go-perf v0.0.0-20241016160959-1342461adb4a + github.com/elastic/go-perf v0.0.0-20241029065020-30bec95324b8 github.com/google/uuid v1.6.0 - github.com/jsimonetti/rtnetlink v1.4.2 - github.com/klauspost/compress v1.17.9 + github.com/klauspost/compress v1.18.0 + github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d github.com/minio/sha256-simd v1.0.1 github.com/peterbourgon/ff/v3 v3.4.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 - github.com/tklauser/numcpus v0.8.0 + github.com/tklauser/numcpus v0.10.0 github.com/zeebo/xxh3 v1.0.2 - go.opentelemetry.io/collector/component v0.116.0 - go.opentelemetry.io/collector/consumer/consumertest v0.116.0 - go.opentelemetry.io/collector/consumer/xconsumer v0.116.0 - go.opentelemetry.io/collector/pdata v1.22.0 - go.opentelemetry.io/collector/pdata/pprofile v0.116.0 - go.opentelemetry.io/collector/receiver v0.116.0 - go.opentelemetry.io/collector/receiver/receivertest v0.116.0 - go.opentelemetry.io/collector/receiver/xreceiver v0.116.0 - go.opentelemetry.io/otel v1.35.0 - go.opentelemetry.io/otel/metric v1.35.0 - golang.org/x/arch v0.10.0 - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 - golang.org/x/sync v0.12.0 - golang.org/x/sys v0.31.0 - google.golang.org/grpc v1.69.2 + go.opentelemetry.io/collector/component v1.35.0 + go.opentelemetry.io/collector/consumer/consumertest v0.129.0 + go.opentelemetry.io/collector/consumer/xconsumer v0.129.0 + go.opentelemetry.io/collector/pdata v1.35.0 + go.opentelemetry.io/collector/pdata/pprofile v0.129.0 + go.opentelemetry.io/collector/receiver v1.35.0 + go.opentelemetry.io/collector/receiver/receivertest v0.129.0 + go.opentelemetry.io/collector/receiver/xreceiver v0.129.0 + go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/metric v1.37.0 + golang.org/x/arch v0.18.0 + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b + golang.org/x/mod v0.25.0 + golang.org/x/sync v0.15.0 + golang.org/x/sys v0.33.0 + google.golang.org/grpc v1.73.0 ) require ( - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.33 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.22.8 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.8 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.30.8 // indirect - github.com/aws/smithy-go v1.20.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect + github.com/aws/smithy-go v1.22.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/josharian/native v1.1.0 // indirect + github.com/jsimonetti/rtnetlink/v2 v2.0.3 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/mdlayher/netlink v1.7.2 // indirect @@ -66,19 +69,22 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/collector/component/componenttest v0.116.0 // indirect - go.opentelemetry.io/collector/config/configtelemetry v0.116.0 // indirect - go.opentelemetry.io/collector/consumer v1.22.0 // indirect - go.opentelemetry.io/collector/consumer/consumererror v0.116.0 // indirect - go.opentelemetry.io/collector/pipeline v0.116.0 // indirect - go.opentelemetry.io/otel/sdk v1.32.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/collector/component/componenttest v0.129.0 // indirect + go.opentelemetry.io/collector/consumer v1.35.0 // indirect + go.opentelemetry.io/collector/consumer/consumererror v0.129.0 // indirect + go.opentelemetry.io/collector/featuregate v1.35.0 // indirect + go.opentelemetry.io/collector/internal/telemetry v0.129.0 // indirect + go.opentelemetry.io/collector/pipeline v0.129.0 // indirect + go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect + go.opentelemetry.io/otel/log v0.12.2 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/text v0.23.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect - google.golang.org/protobuf v1.36.1 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/text v0.24.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 75db7bf02..32aea7ea3 100644 --- a/go.sum +++ b/go.sum @@ -1,84 +1,105 @@ -github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= -github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw= -github.com/aws/aws-sdk-go-v2/config v1.27.35 h1:jeFgiWYNV0vrgdZqB4kZBjYNdy0IKkwrAjr2fwpHIig= -github.com/aws/aws-sdk-go-v2/config v1.27.35/go.mod h1:qnpEvTq8ZfjrCqmJGRfWZuF+lGZ/vG8LK2K0L/TY1gQ= -github.com/aws/aws-sdk-go-v2/credentials v1.17.33 h1:lBHAQQznENv0gLHAZ73ONiTSkCtr8q3pSqWrpbBBZz0= -github.com/aws/aws-sdk-go-v2/credentials v1.17.33/go.mod h1:MBuqCUOT3ChfLuxNDGyra67eskx7ge9e3YKYBce7wpI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 h1:Roo69qTpfu8OlJ2Tb7pAYVuF0CpuUMB0IYWwYP/4DZM= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17/go.mod h1:NcWPxQzGM1USQggaTVwz6VpqMZPX1CvDJLDh6jnOCa4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 h1:FLMkfEiRjhgeDTCjjLoc3URo/TBkgeQbocA78lfkzSI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19/go.mod h1:Vx+GucNSsdhaxs3aZIKfSUjKVGsxN25nX2SRcdhuw08= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 h1:u+EfGmksnJc/x5tq3A+OD7LrMbSSR/5TrKLvkdy/fhY= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17/go.mod h1:VaMx6302JHax2vHJWgRo+5n9zvbacs3bLU/23DNQrTY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.62.0 h1:rd/aA3iDq1q7YsL5sc4dEwChutH7OZF9Ihfst6pXQzI= -github.com/aws/aws-sdk-go-v2/service/s3 v1.62.0/go.mod h1:5FmD/Dqq57gP+XwaUnd5WFPipAuzrf0HmupX27Gvjvc= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.8 h1:JRwuL+S1Qe1owZQoxblV7ORgRf2o0SrtzDVIbaVCdQ0= -github.com/aws/aws-sdk-go-v2/service/sso v1.22.8/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.8 h1:+HpGETD9463PFSj7lX5+eq7aLDs85QUIA+NBkeAsscA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.8/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.8 h1:bAi+4p5EKnni+jrfcAhb7iHFQ24bthOAV9t0taf3DCE= -github.com/aws/aws-sdk-go-v2/service/sts v1.30.8/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= -github.com/aws/smithy-go v1.20.4 h1:2HK1zBdPgRbjFOHlfeQZfpC4r72MOb9bZkiFwggKO+4= -github.com/aws/smithy-go v1.20.4/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0= +github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= +github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0= +github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U= +github.com/aws/aws-sdk-go-v2/service/s3 v1.80.3 h1:jBOwbbIQlfZG079E0YEnfipULNr7wnXbG2gwJyG9hrc= +github.com/aws/aws-sdk-go-v2/service/s3 v1.80.3/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.81.0 h1:1GmCadhKR3J2sMVKs2bAYq9VnwYeCqfRyZzD4RASGlA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.81.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.82.0 h1:JubM8CGDDFaAOmBrd8CRYNr49ZNgEAiLwGwgNMdS0nw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.82.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 h1:5Y75q0RPQoAbieyOuGLhjV9P3txvYgXv2lg0UwJOfmE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= +github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw= +github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= -github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= +github.com/cilium/ebpf v0.18.0 h1:OsSwqS4y+gQHxaKgg2U/+Fev834kdnsQbtzRnbVC6Gs= +github.com/cilium/ebpf v0.18.0/go.mod h1:vmsAT73y4lW2b4peE+qcOqw6MxvWQdC+LiU5gd/xyo4= +github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao= +github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elastic/go-freelru v0.16.0 h1:gG2HJ1WXN2tNl5/p40JS/l59HjvjRhjyAa+oFTRArYs= github.com/elastic/go-freelru v0.16.0/go.mod h1:bSdWT4M0lW79K8QbX6XY2heQYSCqD7THoYf82pT/H3I= -github.com/elastic/go-perf v0.0.0-20241016160959-1342461adb4a h1:ymmtaN4bVCmKKeu4XEf6JEWNZKRXPMng1zjpKd+8rCU= -github.com/elastic/go-perf v0.0.0-20241016160959-1342461adb4a/go.mod h1:Nt+pnRYvf0POC+7pXsrv8ubsEOSsaipJP0zlz1Ms1RM= +github.com/elastic/go-perf v0.0.0-20241029065020-30bec95324b8 h1:FD01NjsTes0RxZVQ22ebNYJA4KDdInVnR9cn1hmaMwA= +github.com/elastic/go-perf v0.0.0-20241029065020-30bec95324b8/go.mod h1:Nt+pnRYvf0POC+7pXsrv8ubsEOSsaipJP0zlz1Ms1RM= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= +github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90= -github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM= -github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= -github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= +github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= +github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= +github.com/jsimonetti/rtnetlink/v2 v2.0.3 h1:Jcp7GTnTPepoUAJ9+LhTa7ZiebvNS56T1GtlEUaPNFE= +github.com/jsimonetti/rtnetlink/v2 v2.0.3/go.mod h1:atIkksp/9fqtf6rpAw45JnttnP2gtuH9X88WPfWfS9A= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d h1:JmrZTpS0GAyMV4ZQVVH/AS0Y6r2PbnYNSRUuRX+HOLA= +github.com/mdlayher/kobject v0.0.0-20200520190114-19ca17470d7d/go.mod h1:+SexPO1ZvdbbWUdUnyXEWv3+4NwHZjKhxOmQqHY4Pqc= +github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= +github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= @@ -103,8 +124,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= -github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -113,81 +134,136 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/collector/component v0.116.0 h1:SQE1YeVfYCN7bw1n4hknUwJE5U/1qJL552sDhAdSlaA= -go.opentelemetry.io/collector/component v0.116.0/go.mod h1:MYgXFZWDTq0uPgF1mkLSFibtpNqksRVAOrmihckOQEs= -go.opentelemetry.io/collector/component/componenttest v0.116.0 h1:UIcnx4Rrs/oDRYSAZNHRMUiYs2FBlwgV5Nc0oMYfR6A= -go.opentelemetry.io/collector/component/componenttest v0.116.0/go.mod h1:W40HaKPHdBFMVI7zzHE7dhdWC+CgAnAC9SmWetFBATY= -go.opentelemetry.io/collector/config/configtelemetry v0.116.0 h1:Vl49VCHQwBOeMswDpFwcl2HD8e9y94xlrfII3SR2VeQ= -go.opentelemetry.io/collector/config/configtelemetry v0.116.0/go.mod h1:SlBEwQg0qly75rXZ6W1Ig8jN25KBVBkFIIAUI1GiAAE= -go.opentelemetry.io/collector/consumer v1.22.0 h1:QmfnNizyNZFt0uK3GG/EoT5h6PvZJ0dgVTc5hFEc1l0= -go.opentelemetry.io/collector/consumer v1.22.0/go.mod h1:tiz2khNceFAPokxxfzAuFfIpShBasMT2AL2Sbc7+m0I= -go.opentelemetry.io/collector/consumer/consumererror v0.116.0 h1:GRPnuvwxUeHKVTRzy35di8OFlxypY4YWrK+1nWMsExM= -go.opentelemetry.io/collector/consumer/consumererror v0.116.0/go.mod h1:OvQvQ2V7sHT4Vz+1/4mwdEajWZNoFUsY1NhOM8rGvXo= -go.opentelemetry.io/collector/consumer/consumertest v0.116.0 h1:pIVR7FtQMNAzfxBUSMEIC2dX5Lfo3O9ZBfx+sAwrrrM= -go.opentelemetry.io/collector/consumer/consumertest v0.116.0/go.mod h1:cV3cNDiPnls5JdhnOJJFVlclrClg9kPs04cXgYP9Gmk= -go.opentelemetry.io/collector/consumer/xconsumer v0.116.0 h1:ZrWvq7HumB0jRYmS2ztZ3hhXRNpUVBWPKMbPhsVGmZM= -go.opentelemetry.io/collector/consumer/xconsumer v0.116.0/go.mod h1:C+VFMk8vLzPun6XK8aMts6h4RaDjmzXHCPaiOxzRQzQ= -go.opentelemetry.io/collector/pdata v1.22.0 h1:3yhjL46NLdTMoP8rkkcE9B0pzjf2973crn0KKhX5UrI= -go.opentelemetry.io/collector/pdata v1.22.0/go.mod h1:nLLf6uDg8Kn5g3WNZwGyu8+kf77SwOqQvMTb5AXEbEY= -go.opentelemetry.io/collector/pdata/pprofile v0.116.0 h1:iE6lqkO7Hi6lTIIml1RI7yQ55CKqW12R2qHinwF5Zuk= -go.opentelemetry.io/collector/pdata/pprofile v0.116.0/go.mod h1:xQiPpjzIiXRFb+1fPxUy/3ygEZgo0Bu/xmLKOWu8vMQ= -go.opentelemetry.io/collector/pdata/testdata v0.116.0 h1:zmn1zpeX2BvzL6vt2dBF4OuAyFF2ml/OXcqflNgFiP0= -go.opentelemetry.io/collector/pdata/testdata v0.116.0/go.mod h1:ytWzICFN4XTDP6o65B4+Ed52JGdqgk9B8CpLHCeCpMo= -go.opentelemetry.io/collector/pipeline v0.116.0 h1:o8eKEuWEszmRpfShy7ElBoQ3Jo6kCi9ucm3yRgdNb9s= -go.opentelemetry.io/collector/pipeline v0.116.0/go.mod h1:qE3DmoB05AW0C3lmPvdxZqd/H4po84NPzd5MrqgtL74= -go.opentelemetry.io/collector/receiver v0.116.0 h1:voiBluWLwe4lbyLVwxloK6CudqqszWF+bgYKHuxnETU= -go.opentelemetry.io/collector/receiver v0.116.0/go.mod h1:zb6m8l+knUuN62ASCDqQPIm9punK8PEX1mFrF/yzMI8= -go.opentelemetry.io/collector/receiver/receivertest v0.116.0 h1:ZF4QVcots0OUiutblkyPR02pc+g7v1QaJSFW8tOzHoQ= -go.opentelemetry.io/collector/receiver/receivertest v0.116.0/go.mod h1:7GGvtHhW3o6457/wGtSWXJtCtlW6VGFUZSlf6wboNTw= -go.opentelemetry.io/collector/receiver/xreceiver v0.116.0 h1:Kc+ixqgMjU2sHhzNrFn5TttVNiJlJwTLL3sQrM9uH6s= -go.opentelemetry.io/collector/receiver/xreceiver v0.116.0/go.mod h1:H2YGSNFoMbWMIDvB8tzkReHSVqvogihjtet+ppHfYv8= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= -go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= -go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= -go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/collector/component v1.34.0 h1:YONg7FaZ5zZbj5cLdARvwtMNuZHunuyxw2fWe5fcWqc= +go.opentelemetry.io/collector/component v1.34.0/go.mod h1:GvolsSVZskXuyfQdwYacqeBSZe/1tg4RJ0YK55KSvDA= +go.opentelemetry.io/collector/component v1.35.0 h1:JpvBukEcEUvJ/TInF1KYpXtWEP+C7iYkxCHKjI0o7BQ= +go.opentelemetry.io/collector/component v1.35.0/go.mod h1:hU/ieWPxWbMAacODCSqem5ZaN6QH9W5GWiZ3MtXVuwc= +go.opentelemetry.io/collector/component/componenttest v0.128.0 h1:MGNh5lQQ0Qmz2SmNwOqLJYaWMDkMLYj/51wjMzTBR34= +go.opentelemetry.io/collector/component/componenttest v0.128.0/go.mod h1:hALNxcacqOaX/Gm/dE7sNOxAEFj41SbRqtvF57Yd6gs= +go.opentelemetry.io/collector/component/componenttest v0.129.0 h1:gpKkZGCRPu3Yn0U2co09bMvhs17yLFb59oV8Gl9mmRI= +go.opentelemetry.io/collector/component/componenttest v0.129.0/go.mod h1:JR9k34Qvd/pap6sYkPr5QqdHpTn66A5lYeYwhenKBAM= +go.opentelemetry.io/collector/consumer v1.34.0 h1:oBhHH6mgViOGhVDPozE+sUdt7jFBo2Hh32lsSr2L3Tc= +go.opentelemetry.io/collector/consumer v1.34.0/go.mod h1:DVMCb56ZBlPNcmo0lSJKn3rp18oyZQCedRE4GKIMI+Q= +go.opentelemetry.io/collector/consumer v1.35.0 h1:mgS42yh1maXBIE65IT4//iOA89BE+7xSUzV8czyevHg= +go.opentelemetry.io/collector/consumer v1.35.0/go.mod h1:9sSPX0hDHaHqzR2uSmfLOuFK9v3e9K3HRQ+fydAjOWs= +go.opentelemetry.io/collector/consumer/consumererror v0.128.0 h1:3htkWoHwXZ801ORmGeORdcMGqJHEbwdjaWhIj4LNbxw= +go.opentelemetry.io/collector/consumer/consumererror v0.128.0/go.mod h1:v3eUnvuIBSV2yBWiWoZELV1jki7HFMttWeBF311XIU0= +go.opentelemetry.io/collector/consumer/consumererror v0.129.0 h1:ud92OBWwqQlHjjx9cB48XhXU/Lz5QSAnXUAErsNHHME= +go.opentelemetry.io/collector/consumer/consumererror v0.129.0/go.mod h1:wtg7mcOkncUO/oZQUfHYoTPiVgMT4yrEKeskFv9dUJg= +go.opentelemetry.io/collector/consumer/consumertest v0.128.0 h1:x50GB0I/QvU3sQuNCap5z/P2cnq2yHoRJ/8awkiT87w= +go.opentelemetry.io/collector/consumer/consumertest v0.128.0/go.mod h1:Wb3IAbMY/DOIwJPy81PuBiW2GnKoNIz4THE7wfJwovE= +go.opentelemetry.io/collector/consumer/consumertest v0.129.0 h1:kRmrAgVvPxH5c/rTaOYAzyy0YrrYhQpBNkuqtDRrgeU= +go.opentelemetry.io/collector/consumer/consumertest v0.129.0/go.mod h1:JgJKms1+v/CuAjkPH+ceTnKeDgUUGTQV4snGu5wTEHY= +go.opentelemetry.io/collector/consumer/xconsumer v0.128.0 h1:4E+KTdCjkRS3SIw0bsv5kpv9XFXHf8x9YiPEuxBVEHY= +go.opentelemetry.io/collector/consumer/xconsumer v0.128.0/go.mod h1:OmzilL/qbjCzPMHay+WEA7/cPe5xuX7Jbj5WPIpqaMo= +go.opentelemetry.io/collector/consumer/xconsumer v0.129.0 h1:bRyJ9TGWwnrUnB5oQGTjPhxpVRbkIVeugmvks22bJ4A= +go.opentelemetry.io/collector/consumer/xconsumer v0.129.0/go.mod h1:pbe5ZyPJrtzdt/RRI0LqfT1GVBiJLbtkDKx3SBRTiTY= +go.opentelemetry.io/collector/featuregate v1.34.0 h1:zqDHpEYy1UeudrfUCvlcJL2t13dXywrC6lwpNZ5DrCU= +go.opentelemetry.io/collector/featuregate v1.34.0/go.mod h1:Y/KsHbvREENKvvN9RlpiWk/IGBK+CATBYzIIpU7nccc= +go.opentelemetry.io/collector/featuregate v1.35.0 h1:c/XRtA35odgxVc4VgOF/PTIk7ajw1wYdQ6QI562gzd4= +go.opentelemetry.io/collector/featuregate v1.35.0/go.mod h1:Y/KsHbvREENKvvN9RlpiWk/IGBK+CATBYzIIpU7nccc= +go.opentelemetry.io/collector/internal/telemetry v0.128.0 h1:ySEYWoY7J8DAYdlw2xlF0w+ODQi3AhYj7TRNflsCbx8= +go.opentelemetry.io/collector/internal/telemetry v0.128.0/go.mod h1:572B/iJqjauv3aT+zcwnlNWBPqM7+KqrYGSUuOAStrM= +go.opentelemetry.io/collector/internal/telemetry v0.129.0 h1:jkzRpIyMxMGdAzVOcBe8aRNrbP7eUrMq6cxEHe0sbzA= +go.opentelemetry.io/collector/internal/telemetry v0.129.0/go.mod h1:riAPlR2LZBV7VEx4LicOKebg3N1Ja3izzkv5fl1Lhiw= +go.opentelemetry.io/collector/pdata v1.34.0 h1:2vwYftckXe7pWxI9mfSo+tw3wqdGNrYpMbDx/5q6rw8= +go.opentelemetry.io/collector/pdata v1.34.0/go.mod h1:StPHMFkhLBellRWrULq0DNjv4znCDJZP6La4UuC+JHI= +go.opentelemetry.io/collector/pdata v1.35.0 h1:ck6WO6hCNjepADY/p9sT9/rLECTLO5ukYTumKzsqB/E= +go.opentelemetry.io/collector/pdata v1.35.0/go.mod h1:pttpb089864qG1k0DMeXLgwwTFLk+o3fAW9I6MF9tzw= +go.opentelemetry.io/collector/pdata/pprofile v0.128.0 h1:6DEtzs/liqv/ukz2EHbC5OMaj2V6K2pzuj/LaRg2YmY= +go.opentelemetry.io/collector/pdata/pprofile v0.128.0/go.mod h1:bVVRpz+zKFf1UCCRUFqy8LvnO3tHlXKkdqW2d+Wi/iA= +go.opentelemetry.io/collector/pdata/pprofile v0.129.0 h1:DgZTvjOGmyZRx7Or80hz8XbEaGwHPkIh2SX1A5eXttQ= +go.opentelemetry.io/collector/pdata/pprofile v0.129.0/go.mod h1:uUBZxqJNOk6QIMvbx30qom//uD4hXJ1K/l3qysijMLE= +go.opentelemetry.io/collector/pdata/testdata v0.128.0 h1:5xcsMtyzvb18AnS2skVtWreQP1nl6G3PiXaylKCZ6pA= +go.opentelemetry.io/collector/pdata/testdata v0.128.0/go.mod h1:9/VYVgzv3JMuIyo19KsT3FwkVyxbh3Eg5QlabQEUczA= +go.opentelemetry.io/collector/pipeline v0.128.0 h1:WgNXdFbyf/QRLy5XbO/jtPQosWrSWX/TEnSYpJq8bgI= +go.opentelemetry.io/collector/pipeline v0.128.0/go.mod h1:TO02zju/K6E+oFIOdi372Wk0MXd+Szy72zcTsFQwXl4= +go.opentelemetry.io/collector/pipeline v0.129.0 h1:Mp7RuKLizLQJ0381eJqKQ0zpgkFlhTE9cHidpJQIvMU= +go.opentelemetry.io/collector/pipeline v0.129.0/go.mod h1:TO02zju/K6E+oFIOdi372Wk0MXd+Szy72zcTsFQwXl4= +go.opentelemetry.io/collector/receiver v1.34.0 h1:un6iRBXZBz1zacEmyfDCVDDaBY4GP3TQLwor894fywg= +go.opentelemetry.io/collector/receiver v1.34.0/go.mod h1:4J9xhbXJiI/rYlvlMTskXRGbwFeczJiCkW5R2YfTe88= +go.opentelemetry.io/collector/receiver v1.35.0 h1:JOLa0cHLi6cKU+qsBWXkAWLnd5MoHdh8GaUJ97jWguY= +go.opentelemetry.io/collector/receiver v1.35.0/go.mod h1:y1y8DNoP54RsiucXP/qeRuCErBLc1gyvFjO+GIIn91s= +go.opentelemetry.io/collector/receiver/receivertest v0.128.0 h1:lIWzLAeo7EssZ9d1t6CJvCN2imoM4Fj+9HSbEZ54fBw= +go.opentelemetry.io/collector/receiver/receivertest v0.128.0/go.mod h1:1aX38R6cYe2nfw5rYW6dbHwjtUjs8z2MxrfHbXBddx8= +go.opentelemetry.io/collector/receiver/receivertest v0.129.0 h1:abzNSUJXrtPwRqDM1R+BWs0uzYN2g7YZa7t6nyeLu3s= +go.opentelemetry.io/collector/receiver/receivertest v0.129.0/go.mod h1:hcn7bZ0gfcQYW00GKfEbhwVDsPhOAKALtxK67dywjYA= +go.opentelemetry.io/collector/receiver/xreceiver v0.128.0 h1:1C5vlkyacvuGJkn5J78FpnuF3AlVYMmA0TGF3LPfcZI= +go.opentelemetry.io/collector/receiver/xreceiver v0.128.0/go.mod h1:kut2p3qChyX8K/qhsokae1vgLQAn53i2J5ddsvxJ81s= +go.opentelemetry.io/collector/receiver/xreceiver v0.129.0 h1:jQSsDPLbnX8tWDNz0a495ACoA4vVe/FlPEIftPdVtmU= +go.opentelemetry.io/collector/receiver/xreceiver v0.129.0/go.mod h1:5vzmNL4Mv2q3xlvw2ypg1d1WWWut9i5bUcphXNbQNN4= +go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 h1:u2E32P7j1a/gRgZDWhIXC+Shd4rLg70mnE7QLI/Ssnw= +go.opentelemetry.io/contrib/bridges/otelzap v0.11.0/go.mod h1:pJPCLM8gzX4ASqLlyAXjHBEYxgbOQJ/9bidWxD6PEPQ= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc= +go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E= +go.opentelemetry.io/otel/log/logtest v0.0.0-20250526142609-aa5bd0e64989 h1:4JF7oY9CcHrPGfBLijDcXZyCzGckVEyOjuat5ktmQRg= +go.opentelemetry.io/otel/log/logtest v0.0.0-20250526142609-aa5bd0e64989/go.mod h1:NToOxLDCS1tXDSB2dIj44H9xGPOpKr0csIN+gnuihv4= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= -golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= +golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -196,12 +272,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= -google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= -google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/host/host.go b/host/host.go index a47d981cd..192830690 100644 --- a/host/host.go +++ b/host/host.go @@ -50,6 +50,7 @@ type Trace struct { Comm string ProcessName string ExecutablePath string + ContainerID string Frames []Frame Hash TraceHash KTime times.KTime @@ -61,4 +62,5 @@ type Trace struct { APMTransactionID libpf.APMTransactionID CPU int EnvVars map[string]string + CustomLabels map[string]string } diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 7d823352e..bb3b0e8ae 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -137,8 +137,8 @@ func (c *Controller) Start(ctx context.Context) error { return fmt.Errorf("failed to attach scheduler monitor: %w", err) } - // This log line is used in our system tests to verify if that the agent has started. So if you - // change this log line update also the system test. + // This log line is used in our system tests to verify if that the agent has started. + // So if you change this log line update also the system test. log.Printf("Attached sched monitor") if err := startTraceHandling(ctx, c.reporter, intervals, trc, diff --git a/internal/helpers/address.go b/internal/helpers/address.go deleted file mode 100644 index 1721c9c52..000000000 --- a/internal/helpers/address.go +++ /dev/null @@ -1,138 +0,0 @@ -package helpers // import "go.opentelemetry.io/ebpf-profiler/internal/helpers" - -import ( - "errors" - "fmt" - "net" - "os" - - "github.com/jsimonetti/rtnetlink" - log "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" -) - -func addressFamily(ip net.IP) (uint8, error) { - if ip.To4() != nil { - return unix.AF_INET, nil - } - if len(ip) == net.IPv6len { - return unix.AF_INET6, nil - } - return 0, fmt.Errorf("invalid IP address: %v", ip) -} - -func resolveDestination(domain string) ([]net.IP, error) { - dstIPs, err := net.LookupIP(domain) - if err == nil { - return dstIPs, nil - } - - // domain seems not to be a DNS value. - // Try to interpret it as IP. - host, _, err := net.SplitHostPort(domain) - if err != nil { - return []net.IP{}, err - } - return net.LookupIP(host) -} - -// getSourceIPAddress returns the source IP address for the traffic destined to the specified -// domain. -func getSourceIPAddress(domain string) (net.IP, error) { - conn, err := rtnetlink.Dial(nil) - if err != nil { - return nil, errors.New("unable to open netlink connection") - } - defer conn.Close() - - dstIPs, err := resolveDestination(domain) - if err != nil { - return nil, fmt.Errorf("unable to resolve %s: %v", domain, err) - } - if len(dstIPs) == 0 { - return nil, fmt.Errorf("unable to resolve %s: no IP address", domain) - } - - var srcIP net.IP - var lastError error - found := false - - // We might get multiple IP addresses, check all of them as some may not be routable (like an - // IPv6 address on an IPv4 network). - for _, ip := range dstIPs { - addressFamily, err := addressFamily(ip) - if err != nil { - return nil, fmt.Errorf("unable to get address family for %s: %v", ip.String(), err) - } - - req := &rtnetlink.RouteMessage{ - Family: addressFamily, - Table: unix.RT_TABLE_MAIN, - Attributes: rtnetlink.RouteAttributes{ - Dst: ip, - }, - } - - routes, err := conn.Route.Get(req) - if err != nil { - lastError = fmt.Errorf("unable to get route to %s (%s): %v", domain, ip.String(), err) - continue - } - - if len(routes) == 0 { - continue - } - if len(routes) > 1 { - // More than 1 route! - // This doesn't look like this should ever happen (even in the presence of overlapping - // routes with same metric, this will return a single route). - // May be a leaky abstraction/artifact from the way the netlink API works? - // Regardless, this seems ok to ignore, but log just in case. - log.Warnf("Found multiple (%d) routes to %v; first 2 routes: %#v and %#v", - len(routes), domain, routes[0], routes[1]) - } - - // Sanity-check the result, in case the source address is left uninitialized - if len(routes[0].Attributes.Src) == 0 { - lastError = fmt.Errorf( - "unable to get route to %s (%s): no source IP address", domain, ip.String()) - continue - } - - srcIP = routes[0].Attributes.Src - found = true - break - } - - if !found { - return nil, fmt.Errorf("no route found to %s: %v", domain, lastError) - } - - log.Debugf("Traffic to %v is routed from %v", domain, srcIP.String()) - return srcIP, nil -} - -// GetHostnameAndSourceIP returns the hostname and source IP address for the traffic destined to -// the specified domain. -func GetHostnameAndSourceIP(domain string) (hostname, sourceIP string, err error) { - err = runInRootNS(func() error { - var joinedErr error - - if name, hostnameErr := os.Hostname(); hostnameErr == nil { - hostname = name - } else { - joinedErr = fmt.Errorf("failed to get hostname: %v", hostnameErr) - } - - if srcIP, ipErr := getSourceIPAddress(domain); ipErr == nil { - sourceIP = srcIP.String() - } else { - joinedErr = errors.Join(joinedErr, - fmt.Errorf("failed to get source IP: %v", ipErr)) - } - - return joinedErr - }) - - return hostname, sourceIP, err -} diff --git a/internal/helpers/kernel.go b/internal/helpers/kernel.go deleted file mode 100644 index 19b4c548b..000000000 --- a/internal/helpers/kernel.go +++ /dev/null @@ -1,16 +0,0 @@ -package helpers // import "go.opentelemetry.io/ebpf-profiler/internal/helpers" - -import ( - "fmt" - - "go.opentelemetry.io/ebpf-profiler/tracer" -) - -// GetKernelVersion returns the current version of the kernel -func GetKernelVersion() (string, error) { - major, minor, patch, err := tracer.GetCurrentKernelVersion() - if err != nil { - return "", err - } - return fmt.Sprintf("%d.%d.%d", major, minor, patch), nil -} diff --git a/internal/helpers/namespaces.go b/internal/helpers/namespaces.go deleted file mode 100644 index b72ede198..000000000 --- a/internal/helpers/namespaces.go +++ /dev/null @@ -1,120 +0,0 @@ -package helpers // import "go.opentelemetry.io/ebpf-profiler/internal/helpers" - -import ( - "errors" - "fmt" - "runtime" - "sync" - "syscall" - - log "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" -) - -// runInRootNS executes fetcher in the root namespace. -func runInRootNS(fetcher func() error) error { - // We need to call the `setns` syscall to extract information (network route, hostname) from - // different namespaces. - // However, `setns` doesn't know about goroutines, it operates on OS threads. - // Therefore, the below code needs to take extra steps to make sure no other code (outside of - // this function) will execute in a different namespace. - // - // To do this, we use `runtime.LockOSThread()`, which we call from a separate goroutine. - // runtime.LockOSThread() ensures that the thread executing the goroutine will be terminated - // when the goroutine exits, which makes it impossible for the entered namespaces to be used in - // a different context than the below code. - // - // It would be doable without a goroutine, by saving and restoring the namespaces before calling - // runtime.UnlockOSThread(), but error handling makes things complicated and unsafe/dangerous. - // The below implementation is always safe to run even in the presence of errors. - // - // The only downside is that calling this function comes at the cost of sacrificing an OS - // thread, which will likely force the Go runtime to launch a new thread later. This should be - // acceptable if it doesn't happen too often. - - // Error result of the below goroutine. May contain multiple combined errors. - var errResult error - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - // Before entering a different namespace, lock the current goroutine to a thread. - // Note that we do *not* call runtime.UnlockOSThread(): this ensures the current thread - // will exit after the goroutine finishes, which makes it impossible for other - // goroutines to enter a different namespace. - runtime.LockOSThread() - - // Try to enter root namespaces. If that fails, continue anyway as we might be able to - // gather some metadata. - utsFD, netFD := tryEnterRootNamespaces() - - // Any errors were already logged by the above function. - if utsFD != -1 { - defer unix.Close(utsFD) - } - if netFD != -1 { - defer unix.Close(netFD) - } - - if utsFD == -1 || netFD == -1 { - log.Warnf("Missing capabilities to enter root namespace, fetching information from " + - "current process namespaces") - } - - errResult = fetcher() - }() - - wg.Wait() - - return errResult -} - -// tryEnterRootNamespaces tries to enter PID 1's UTS and network namespaces. -// It returns the file descriptor associated to each, or -1 if the namespace cannot be entered. -func tryEnterRootNamespaces() (utsFD, netFD int) { - netFD, err := enterNamespace(1, "net") - if err != nil { - log.Errorf( - "Unable to enter root network namespace, host metadata may be incorrect: %v", err) - netFD = -1 - } - - utsFD, err = enterNamespace(1, "uts") - if err != nil { - log.Errorf("Unable to enter root UTS namespace, host metadata may be incorrect: %v", err) - utsFD = -1 - } - - return utsFD, netFD -} - -// enterNamespace enters a new namespace of the specified type, inherited from the provided PID. -// The returned file descriptor must be closed with unix.Close(). -// Note that this function affects the OS thread calling this function, which will likely impact -// more than one goroutine unless you also use runtime.LockOSThread. -func enterNamespace(pid int, nsType string) (int, error) { - var nsTypeInt int - switch nsType { - case "net": - nsTypeInt = syscall.CLONE_NEWNET - case "uts": - nsTypeInt = syscall.CLONE_NEWUTS - default: - return -1, fmt.Errorf("unsupported namespace type: %s", nsType) - } - - path := fmt.Sprintf("/proc/%d/ns/%s", pid, nsType) - fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC, 0) - if err != nil { - return -1, err - } - - err = unix.Setns(fd, nsTypeInt) - if err != nil { - // Close namespace and return the error - return -1, errors.Join(err, unix.Close(fd)) - } - - return fd, nil -} diff --git a/interpreter/apmint/socket.go b/interpreter/apmint/socket.go index f986fb4eb..0bd26391b 100644 --- a/interpreter/apmint/socket.go +++ b/interpreter/apmint/socket.go @@ -11,6 +11,7 @@ import ( "fmt" "io/fs" "os" + "path" "path/filepath" "strconv" "strings" @@ -46,7 +47,7 @@ func openAPMAgentSocket(pid libpf.PID, socketPath string) (*apmAgentSocket, erro } // Prepend root system to ensure that this also works with containerized apps. - socketPath = fmt.Sprintf("/proc/%d/root/%s", pid, socketPath) + socketPath = path.Join("/proc", strconv.Itoa(int(pid)), "root", socketPath) // Read effective UID/GID of the APM agent process. euid, egid, err := readProcessOwner(pid) diff --git a/interpreter/dotnet/dotnet.go b/interpreter/dotnet/dotnet.go index 7ef7c85e9..fe703c392 100644 --- a/interpreter/dotnet/dotnet.go +++ b/interpreter/dotnet/dotnet.go @@ -126,7 +126,7 @@ var ( dotnetRegex = regexp.MustCompile(`/(\d+)\.(\d+).(\d+)/libcoreclr.so$`) // The FileID used for Dotnet stub frames. Same FileID as in other interpreters. - stubsFileID = libpf.NewFileID(0x578b, 0x1d) + stubsFileID = libpf.NewStubFileID(libpf.DotnetFrame) // compiler check to make sure the needed interfaces are satisfied _ interpreter.Data = &dotnetData{} diff --git a/interpreter/go/go.go b/interpreter/go/go.go new file mode 100644 index 000000000..c7cb1cbce --- /dev/null +++ b/interpreter/go/go.go @@ -0,0 +1,138 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package golang // import "go.opentelemetry.io/ebpf-profiler/interpreter/go" + +import ( + "fmt" + "hash/fnv" + "sync/atomic" + "unique" + + "go.opentelemetry.io/ebpf-profiler/host" + "go.opentelemetry.io/ebpf-profiler/interpreter" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/metrics" + "go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo" + "go.opentelemetry.io/ebpf-profiler/remotememory" + "go.opentelemetry.io/ebpf-profiler/reporter" + "go.opentelemetry.io/ebpf-profiler/successfailurecounter" +) + +var ( + // compiler check to make sure the needed interfaces are satisfied + _ interpreter.Data = &goData{} + _ interpreter.Instance = &goInstance{} +) + +type goData struct { + refs atomic.Int32 + + pclntab *elfunwindinfo.Gopclntab +} + +type goInstance struct { + interpreter.InstanceStubs + + // Go symbolization metrics + successCount atomic.Uint64 + failCount atomic.Uint64 + + d *goData +} + +func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) ( + interpreter.Data, error) { + ef, err := info.GetELF() + if err != nil { + return nil, err + } + if !ef.IsGolang() { + return nil, nil + } + + pclntab, err := elfunwindinfo.NewGopclntab(ef) + if pclntab == nil { + return nil, err + } + + g := &goData{pclntab: pclntab} + g.refs.Store(1) + return g, nil +} + +func (g *goData) unref() { + if g.refs.Add(-1) == 0 { + _ = g.pclntab.Close() + } +} + +func (g *goData) Attach(_ interpreter.EbpfHandler, _ libpf.PID, + _ libpf.Address, _ remotememory.RemoteMemory) (interpreter.Instance, error) { + g.refs.Add(1) + return &goInstance{d: g}, nil +} + +func (g *goData) Unload(_ interpreter.EbpfHandler) { + g.unref() +} + +func (g *goInstance) GetAndResetMetrics() ([]metrics.Metric, error) { + return []metrics.Metric{ + { + ID: metrics.IDGoSymbolizationSuccess, + Value: metrics.MetricValue(g.successCount.Swap(0)), + }, + { + ID: metrics.IDGoSymbolizationFailure, + Value: metrics.MetricValue(g.failCount.Swap(0)), + }, + }, nil +} + +func (g *goInstance) Detach(_ interpreter.EbpfHandler, _ libpf.PID) error { + g.d.unref() + return nil +} + +func intern(str string) string { + return unique.Make(str).Value() +} + +func (g *goInstance) Symbolize(symbolReporter reporter.SymbolReporter, frame *host.Frame, + trace *libpf.Trace) error { + if !frame.Type.IsInterpType(libpf.Native) { + return interpreter.ErrMismatchInterpreterType + } + sfCounter := successfailurecounter.New(&g.successCount, &g.failCount) + defer sfCounter.DefaultToFailure() + + sourceFile, lineNo, fn := g.d.pclntab.Symbolize(uintptr(frame.Lineno)) + if fn == "" { + return fmt.Errorf("failed to symbolize 0x%x", frame.Lineno) + } + + // The fnv hash Write() method calls cannot fail, so it's safe to ignore the errors. + h := fnv.New128a() + _, _ = h.Write([]byte(frame.File.StringNoQuotes())) + _, _ = h.Write([]byte(fn)) + _, _ = h.Write([]byte(sourceFile)) + fileID, err := libpf.FileIDFromBytes(h.Sum(nil)) + if err != nil { + return fmt.Errorf("failed to create a file ID: %v", err) + } + + frameID := libpf.NewFrameID(fileID, libpf.AddressOrLineno(lineNo)) + + trace.AppendFrameID(libpf.GoFrame, frameID) + + symbolReporter.FrameMetadata(&reporter.FrameMetadataArgs{ + FrameID: frameID, + FunctionName: intern(fn), + SourceFile: intern(sourceFile), + SourceLine: libpf.SourceLineno(lineNo), + }) + + sfCounter.ReportSuccess() + return nil +} diff --git a/interpreter/go/go_test.go b/interpreter/go/go_test.go new file mode 100644 index 000000000..a5e0bfb16 --- /dev/null +++ b/interpreter/go/go_test.go @@ -0,0 +1,110 @@ +package golang + +import ( + "os" + "runtime" + "strings" + "testing" + + "go.opentelemetry.io/ebpf-profiler/host" + "go.opentelemetry.io/ebpf-profiler/interpreter" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + "go.opentelemetry.io/ebpf-profiler/process" + "go.opentelemetry.io/ebpf-profiler/remotememory" + "go.opentelemetry.io/ebpf-profiler/reporter" + "go.opentelemetry.io/ebpf-profiler/util" +) + +// mockReporter implements reporter.SymbolReporter for testing +type mockReporter struct { + b *testing.B + frameMetadata map[libpf.FrameID]*reporter.FrameMetadataArgs +} + +func newMockReporter(b *testing.B) *mockReporter { + return &mockReporter{ + b: b, + frameMetadata: make(map[libpf.FrameID]*reporter.FrameMetadataArgs), + } +} + +func (m *mockReporter) CompareFunctionName(fn string) { + if len(m.frameMetadata) != 1 { + m.b.Fatalf("Expected a single entry but got %d", len(m.frameMetadata)) + } + for _, v := range m.frameMetadata { + // The returned anonymous function has the suffic 'func1'. + // Therefore check only for a matching prefix. + if !strings.HasPrefix(v.FunctionName, fn) { + m.b.Fatalf("Expected '%s()' but got '%s()'", fn, v.FunctionName) + } + } +} + +func (m *mockReporter) FrameMetadata(args *reporter.FrameMetadataArgs) { + m.frameMetadata[args.FrameID] = args +} + +func (m *mockReporter) ExecutableMetadata(args *reporter.ExecutableMetadataArgs) { + // Not used in this test +} + +func (m *mockReporter) FrameKnown(frameID libpf.FrameID) bool { + _, exists := m.frameMetadata[frameID] + return exists +} + +func (m *mockReporter) ExecutableKnown(fileID libpf.FileID) bool { + return false +} + +func BenchmarkGolang(b *testing.B) { + pc, _, _, ok := runtime.Caller(1) + if !ok { + b.Fatal("Failed to get PC from runtime") + } + fn := runtime.FuncForPC(pc) + exec, err := os.Executable() + if err != nil { + b.Fatalf("Failed to get the executable: %v", err) + } + + libpfPID := libpf.PID(os.Getpid()) + pid := process.New(libpfPID, libpfPID) + + elfRef := pfelf.NewReference(exec, pid) + hostFileID, err := host.FileIDFromBytes([]byte{0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55}) + if err != nil { + b.Fatalf("Failed to create hostID: %v", err) + } + loaderInfo := interpreter.NewLoaderInfo(hostFileID, elfRef, []util.Range{}) + rm := remotememory.NewProcessVirtualMemory(libpfPID) + symReporter := newMockReporter(b) + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + gD, err := Loader(nil, loaderInfo) + if err != nil { + b.Fatalf("Failed to create loader: %v", err) + } + + gI, err := gD.Attach(nil, libpfPID, 0x0, rm) + if err != nil { + b.Fatalf("Failed to create instance: %v", err) + } + + trace := libpf.Trace{} + + if err := gI.Symbolize(symReporter, &host.Frame{ + File: hostFileID, + Lineno: libpf.AddressOrLineno(pc), + Type: libpf.FrameType(libpf.Native), + }, &trace); err != nil { + b.Fatalf("Failed to symbolize 0x%x: %v", pc, err) + } + + symReporter.CompareFunctionName(fn.Name()) + } +} diff --git a/interpreter/golabels/golabels.go b/interpreter/golabels/golabels.go new file mode 100644 index 000000000..971399c24 --- /dev/null +++ b/interpreter/golabels/golabels.go @@ -0,0 +1,73 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package golabels // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabels" + +import ( + "fmt" + "go/version" + "unsafe" + + log "github.com/sirupsen/logrus" + + "go.opentelemetry.io/ebpf-profiler/interpreter" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/remotememory" +) + +// #include "../../support/ebpf/types.h" +import "C" + +type data struct { + goVersion string + offsets C.GoLabelsOffsets + interpreter.InstanceStubs +} + +func (d *data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, + _ libpf.Address, _ remotememory.RemoteMemory) (interpreter.Instance, error) { + if err := ebpf.UpdateProcData(libpf.GoLabels, pid, unsafe.Pointer(&d.offsets)); err != nil { + return nil, err + } + + return d, nil +} + +func (d *data) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { + return ebpf.DeleteProcData(libpf.GoLabels, pid) +} + +func (d *data) Unload(_ interpreter.EbpfHandler) {} + +func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { + file, err := info.GetELF() + if err != nil { + return nil, err + } + goVersion, err := file.GoVersion() + if err != nil { + return nil, err + } + if goVersion == "" { + log.Debugf("file %s is not a Go binary", info.FileName()) + return nil, nil + } + + if version.Compare(goVersion, "go1.25") >= 0 { + return nil, fmt.Errorf("unsupported Go version %s (need >= 1.13 and <= 1.24)", goVersion) + } + + log.Debugf("file %s detected as go version %s", info.FileName(), goVersion) + + offsets := getOffsets(goVersion) + tlsOffset, err := extractTLSGOffset(file) + if err != nil { + return nil, fmt.Errorf("failed to extract TLS offset: %w", err) + } + offsets.tls_offset = C.s32(tlsOffset) + + return &data{ + goVersion: goVersion, + offsets: offsets, + }, nil +} diff --git a/interpreter/golabels/runtime_data.go b/interpreter/golabels/runtime_data.go new file mode 100644 index 000000000..fd6b2ae3f --- /dev/null +++ b/interpreter/golabels/runtime_data.go @@ -0,0 +1,51 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package golabels // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabels" + +// #include "../../support/ebpf/types.h" +import "C" +import ( + "go/version" +) + +// Offsets come from DWARF debug information, use tools/gooffsets to extract them. +// However since DWARF information can be stripped we record them here. +// TODO: Should we look for DWARF information to support new versions +// automatically when available? +func getOffsets(vers string) C.GoLabelsOffsets { + offsets := C.GoLabelsOffsets{ + // https://github.com/golang/go/blob/80e2e474b8d9124d03b744f/src/runtime/runtime2.go#L410 + m_offset: 48, + // https://github.com/golang/go/blob/80e2e474b8d9124d03b744f/src/runtime/runtime2.go#L541 + curg: 192, + // https://github.com/golang/go/blob/80e2e474b8d9124d03b744f/src/runtime/runtime2.go#L483 + labels: 0, + // https://github.com/golang/go/blob/6885bad7dd86880be6929c0/src/runtime/map.go#L112 + hmap_count: 0, + // https://github.com/golang/go/blob/6885bad7dd86880be6929c0/src/runtime/map.go#L114 + hmap_log2_bucket_count: 0, + // https://github.com/golang/go/blob/6885bad7dd86880be6929c0/src/runtime/map.go#L118 + hmap_buckets: 0, + } + + // Version enforcement takes place in the Loader function. + if version.Compare(vers, "go1.24") >= 0 { + offsets.labels = 352 + return offsets + } + + // These are the same for all versions but we have to leave them zero for 1.24+ detection. + offsets.hmap_log2_bucket_count = 9 + offsets.hmap_buckets = 16 + if version.Compare(vers, "go1.23") >= 0 { + offsets.labels = 352 + } else if version.Compare(vers, "go1.21") >= 0 { + offsets.labels = 344 + } else if version.Compare(vers, "go1.17") >= 0 { + offsets.labels = 360 + } else { + offsets.labels = 344 + } + return offsets +} diff --git a/interpreter/golabels/test/main-cgo.go b/interpreter/golabels/test/main-cgo.go new file mode 100644 index 000000000..3950f58df --- /dev/null +++ b/interpreter/golabels/test/main-cgo.go @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:build usecgo +// +build usecgo + +package main + +/* +#include + +void cgofunc() { + volatile int counter = 0; + while (counter < 1000000) { + counter++; + } +} +*/ +import "C" + +import ( + "context" + "fmt" + "math/rand" + "os" + "runtime/pprof" + "time" +) + +func randomString2(n int) string { + letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + s := make([]rune, n) + for i := range s { + s[i] = letters[rand.Intn(len(letters))] + } + return string(s) +} + +// This is a normal main program that when go build will be statically linked, this is required +// to work with qemu/bluebox testing harness. A statically linked go test built binary doesn't +// work with the go labels extractor ebpf program, not sure yet if this is a bug. +func main() { + // If first isn't subtest then we're running via bluebox init and should just exit. + if len(os.Args) != 3 || os.Args[1] != "-subtest" { + fmt.Println("PASS") + return + } + cookie := os.Args[2] + labels := pprof.Labels( + "l1"+cookie, "label1"+randomString2(16), + "l2"+cookie, "label2"+randomString2(24), + "l3"+cookie, "label3"+randomString2(48)) + lastUpdate := time.Now() + pprof.Do(context.TODO(), labels, func(context.Context) { + //nolint:revive + for time.Since(lastUpdate) < 10*time.Second { + // CPU go burr on purpose. + C.cgofunc() + } + }) + fmt.Println("PASS") +} diff --git a/interpreter/golabels/test/main.go b/interpreter/golabels/test/main.go new file mode 100644 index 000000000..4683bba38 --- /dev/null +++ b/interpreter/golabels/test/main.go @@ -0,0 +1,50 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +//go:build nocgo +// +build nocgo + +package main + +import ( + "context" + "fmt" + "math/rand" + "os" + "runtime/pprof" + "time" +) + +//nolint:gosec +func randomString(n int) string { + letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + s := make([]rune, n) + for i := range s { + s[i] = letters[rand.Intn(len(letters))] + } + return string(s) +} + +// This is a normal main program that when go build will be statically linked, this is required +// to work with qemu/bluebox testing harness. A statically linked go test built binary doesn't +// work with the go labels extractor ebpf program, not sure yet if this is a bug. +func main() { + // If first isn't subtest then we're running via bluebox init and should just exit. + if len(os.Args) != 3 || os.Args[1] != "-subtest" { + fmt.Println("PASS") + return + } + cookie := os.Args[2] + labels := pprof.Labels( + "l1"+cookie, "label1"+randomString(16), + "l2"+cookie, "label2"+randomString(24), + "l3"+cookie, "label3"+randomString(48)) + lastUpdate := time.Now() + pprof.Do(context.TODO(), labels, func(context.Context) { + //nolint:revive + for time.Since(lastUpdate) < 10*time.Second { + // CPU go burr on purpose. + } + }) + fmt.Println("PASS") +} diff --git a/interpreter/golabels/test/main_test.go b/interpreter/golabels/test/main_test.go new file mode 100644 index 000000000..050cdd6f9 --- /dev/null +++ b/interpreter/golabels/test/main_test.go @@ -0,0 +1,71 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "context" + "os" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/ebpf-profiler/testutils" + tracertypes "go.opentelemetry.io/ebpf-profiler/tracer/types" +) + +func TestGoLabels(t *testing.T) { + if !testutils.IsRoot() { + t.Skip("root privileges required") + } + + r := &testutils.MockReporter{} + enabledTracers, _ := tracertypes.Parse("") + enabledTracers.Enable(tracertypes.Labels) + traceCh, _ := testutils.StartTracer(context.Background(), t, enabledTracers, r) + for _, tc := range [][]string{ + {"./golbls_1_23.test", "123"}, + {"./golbls_1_24.test", "124"}, + {"./golbls_cgo.test", "cgo"}, + } { + t.Run(tc[0], func(t *testing.T) { + // Use a separate exe for getting labels as the bpf code doesn't seem to work with + // go test static binaries at the moment, not clear if that's a problem with the bpf + // code or a bug/fact of life for static go binaries and getting g from TLS. + cookie := tc[1] + cmd := exec.Command(tc[0], "-subtest", cookie) + err := cmd.Start() + require.NoError(t, err) + + for trace := range traceCh { + if trace == nil { + continue + } + if len(trace.CustomLabels) > 0 { + hits := 0 + for k, v := range trace.CustomLabels { + switch k { + case "l1" + cookie: + require.Len(t, v, 22) + require.True(t, strings.HasPrefix(v, "label1")) + hits++ + case "l2" + cookie: + require.Len(t, v, 30) + require.True(t, strings.HasPrefix(v, "label2")) + hits++ + case "l3" + cookie: + require.Len(t, v, 47) + require.True(t, strings.HasPrefix(v, "label3")) + hits++ + } + } + if hits == 3 { + break + } + } + } + _ = cmd.Process.Signal(os.Kill) + _ = cmd.Wait() + }) + } +} diff --git a/interpreter/golabels/tls_amd64.go b/interpreter/golabels/tls_amd64.go new file mode 100644 index 000000000..f5de769a8 --- /dev/null +++ b/interpreter/golabels/tls_amd64.go @@ -0,0 +1,48 @@ +//go:build amd64 + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package golabels // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabels" + +import ( + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + "golang.org/x/arch/x86/x86asm" +) + +// Most normal amd64 Go binaries use -8 as offset into TLS space for +// storing the current g but "static" binaries it ends up as -80. There +// may be dynamic relocating going on so just read it from a known +// symbol if possible. +func extractTLSGOffset(f *pfelf.File) (int32, error) { + syms, err := f.ReadSymbols() + if err != nil { + return 0, err + } + // Dump of assembler code for function runtime.stackcheck: + // 0x0000000000470080 <+0>: mov %fs:0xfffffffffffffff8,%rax + sym, err := syms.LookupSymbol("runtime.stackcheck.abi0") + if err != nil { + // Binary must be stripped, hope default is correct and warn. + log.Warnf("Failed to find stackcheck symbol, Go labels might not work: %v", err) + return -8, nil + } + b, err := f.VirtualMemory(int64(sym.Address), 10, 10) + if err != nil { + return 0, err + } + + i, err := x86asm.Decode(b, 64) + if err != nil { + return 0, err + } + if i.Op == x86asm.MOV { + mem, ok := i.Args[1].(x86asm.Mem) + if ok { + return int32(mem.Disp), 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 new file mode 100644 index 000000000..e4338b1c5 --- /dev/null +++ b/interpreter/golabels/tls_arm64.go @@ -0,0 +1,59 @@ +//go:build arm64 + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package golabels // import "go.opentelemetry.io/ebpf-profiler/interpreter/golabels" + +import ( + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + "golang.org/x/arch/arm64/arm64asm" +) + +// https://github.com/golang/go/blob/6885bad7dd86880be/src/runtime/tls_arm64.s#L11 +// +// Get's compiled into: +// 0x000000000007f260 <+0>: adrp x27, 0x1c2000 +// 0x000000000007f264 <+4>: ldrsb x0, [x27, #284] +// 0x000000000007f268 <+8>: cbz x0, 0x7f278 +// 0x000000000007f26c <+12>: mrs x0, tpidr_el0 +// 0x000000000007f270 <+16>: mov x27, #0x30 // #48 +// 0x000000000007f274 <+20>: ldr x28, [x0, x27] +// 0x000000000007f278 <+24>: ret +func extractTLSGOffset(f *pfelf.File) (int32, error) { + iscgo, err := f.IsCgoEnabled() + if err != nil || !iscgo { + return 0, err + } + + syms, err := f.ReadSymbols() + if err != nil { + return 0, err + } + sym, err := syms.LookupSymbol("runtime.load_g.abi0") + if err != nil { + // Binary must be stripped, just warn and return 0 and we'll rely on r28. + log.Warnf("Failed to find load_g symbol in cgo enabled Go binary "+ + "label collection in CGO frames may not work: %v", err) + return 0, nil + } + b, err := f.VirtualMemory(int64(sym.Address), 32, 32) + if err != nil { + return 0, err + } + for ; len(b) > 0; b = b[4:] { + i, err := arm64asm.Decode(b) + if err != nil { + return 0, err + } + if i.Op == arm64asm.MOV { + imm, ok := i.Args[1].(arm64asm.Imm64) + if ok { + return int32(imm.Imm), nil + } + } + } + log.Warnf("Failed to decode load_g symbol, Go label collection might not work with CGO frames") + return 0, nil +} diff --git a/interpreter/hotspot/hotspot.go b/interpreter/hotspot/hotspot.go index 3b8505b8f..efc3a1377 100644 --- a/interpreter/hotspot/hotspot.go +++ b/interpreter/hotspot/hotspot.go @@ -123,7 +123,7 @@ var ( hiddenClassMask = "+" // The FileID used for intrinsic stub frames - hotspotStubsFileID = libpf.NewFileID(0x578b, 0x1d) + hotspotStubsFileID = libpf.NewStubFileID(libpf.HotSpotFrame) _ interpreter.Data = &hotspotData{} _ interpreter.Instance = &hotspotInstance{} diff --git a/interpreter/hotspot/method.go b/interpreter/hotspot/method.go index fb499f2a4..389943f62 100644 --- a/interpreter/hotspot/method.go +++ b/interpreter/hotspot/method.go @@ -13,8 +13,6 @@ import ( ) // Constants for the JVM internals that have never changed -// -//nolint:golint,stylecheck,revive const ConstMethod_has_linenumber_table = 0x0001 // hotspotMethod contains symbolization information for one Java method. It caches diff --git a/interpreter/loaderinfo.go b/interpreter/loaderinfo.go index 093569585..5847d565b 100644 --- a/interpreter/loaderinfo.go +++ b/interpreter/loaderinfo.go @@ -9,6 +9,7 @@ import ( "go.opentelemetry.io/ebpf-profiler/host" "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + "go.opentelemetry.io/ebpf-profiler/process" "go.opentelemetry.io/ebpf-profiler/util" ) @@ -68,3 +69,13 @@ func (i *LoaderInfo) FileName() string { func (i *LoaderInfo) Gaps() []util.Range { return i.gaps } + +// ExtractAsFile returns a filename referring to the ELF executable, extracting +// it from a backing archive if needed. +func (i *LoaderInfo) ExtractAsFile() (string, error) { + if pr, ok := i.elfRef.ELFOpener.(process.Process); ok { + return pr.ExtractAsFile(i.FileName()) + } + return "", fmt.Errorf("unable to open main executable '%v' due to wrong interface type", + i.FileName()) +} diff --git a/interpreter/nodev8/v8.go b/interpreter/nodev8/v8.go index 89554f2c7..28bfa4ac6 100644 --- a/interpreter/nodev8/v8.go +++ b/interpreter/nodev8/v8.go @@ -225,10 +225,10 @@ const ( var ( // regex for the interpreter executable or shared library - v8Regex = regexp.MustCompile(`^(?:.*/)?node(\d+)?$|^(?:.*/)libnode\.so(\.\d+)?$`) + v8Regex = regexp.MustCompile(`^(?:.*/)?(?:node|nsolid)(\d+)?$|^(?:.*/)libnode\.so(\.\d+)?$`) // The FileID used for V8 stub frames - v8StubsFileID = libpf.NewFileID(0x578b, 0x1d) + v8StubsFileID = libpf.NewStubFileID(libpf.V8Frame) // the source file entry for unknown code blobs unknownSource = &v8Source{fileName: interpreter.UnknownSourceFile} @@ -352,6 +352,7 @@ type v8Data struct { WeakFixedArray uint16 `name:"WeakFixedArray__WEAK_FIXED_ARRAY_TYPE"` TrustedByteArray uint16 `name:"TrustedByteArray__TRUSTED_BYTE_ARRAY_TYPE" zero:""` TrustedFixedArray uint16 `name:"TrustedFixedArray__TRUSTED_FIXED_ARRAY_TYPE" zero:""` + TrustedWeakFixedArray uint16 `name:"TrustedFixedArray__TRUSTED_WEAK_FIXED_ARRAY_TYPE" zero:""` ProtectedFixedArray uint16 `name:"ProtectedFixedArray__PROTECTED_FIXED_ARRAY_TYPE" zero:""` JSFunction uint16 `name:"JSFunction__JS_FUNCTION_TYPE"` Map uint16 `name:"Map__MAP_TYPE"` @@ -468,7 +469,8 @@ type v8Data struct { // class DeoptimizationLiteralArray introduced in V8 9.8.23 // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/10.2.154.1/src/objects/code.h#1090 DeoptimizationLiteralArray struct { - WeakFixedArray bool + WeakFixedArray bool + TrustedWeakFixedArray bool } // https://chromium.googlesource.com/v8/v8.git/+/refs/tags/9.2.230.1/src/objects/script.tq#18 @@ -1292,7 +1294,9 @@ func (i *v8Instance) readCode(taggedPtr libpf.Address, cookie uint32, sfi *v8SFI // The first numSFI entries of literal array are the pointers for // inlined function's SFI structures expectedTag := vms.Type.FixedArray - if vms.DeoptimizationLiteralArray.WeakFixedArray { + if vms.DeoptimizationLiteralArray.TrustedWeakFixedArray { + expectedTag = vms.Type.TrustedWeakFixedArray + } else if vms.DeoptimizationLiteralArray.WeakFixedArray { expectedTag = vms.Type.WeakFixedArray } literalArrayPtr := npsr.Ptr(deoptimizationData, @@ -1880,7 +1884,7 @@ func (d *v8Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, _ libpf.Add func (d *v8Data) Unload(_ interpreter.EbpfHandler) { } -func (d *v8Data) readIntrospectionData(ef *pfelf.File, syms libpf.SymbolFinder) error { +func (d *v8Data) readIntrospectionData(ef *pfelf.File) error { // Read the variables from the pfelf.File so we avoid failures if the process // exists during extraction of the introspection data. rm := ef.GetRemoteMemory() @@ -1917,7 +1921,7 @@ func (d *v8Data) readIntrospectionData(ef *pfelf.File, syms libpf.SymbolFinder) if memberVal.Kind() == reflect.Bool { s = "v8dbg_parent_" + className + "__" + memberName } - addr, err := syms.LookupSymbolAddress(libpf.SymbolName(s)) + addr, err := ef.LookupSymbolAddress(libpf.SymbolName(s)) if err != nil { log.Debugf("V8: %s = not found", s) if classType.Name == "FrameType" { @@ -2101,6 +2105,14 @@ func (d *v8Data) readIntrospectionData(ef *pfelf.File, syms libpf.SymbolFinder) // so we can probably get away with just hardcoding it for now. vms.SharedFunctionInfo.FunctionData = 8 } + if d.version >= v8Ver(12, 5, 0) { + // This changed in f6c936e836b4d8ffafe790bcc3586f2ba5ffcf74 + vms.DeoptimizationLiteralArray.TrustedWeakFixedArray = true + } else if d.version >= v8Ver(11, 9, 0) { + // This had been WeakFixedArray for a very long time, + // but we lost the metadata in 0698c376801dcde939850b7ad0b55c7459c83f4d. + vms.DeoptimizationLiteralArray.WeakFixedArray = true + } for i := 0; i < vmVal.NumField(); i++ { classVal := vmVal.Field(i) @@ -2134,18 +2146,14 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr var vers [3]uint32 for i, sym := range []string{"major", "minor", "build"} { - var addr libpf.SymbolValue - var raw [4]byte // Resolve and read "v8::internal::Versions::XXXXXX_E" + var val []byte sym = fmt.Sprintf("_ZN2v88internal7Version6%s_E", sym) - addr, err = ef.LookupSymbolAddress(libpf.SymbolName(sym)) - if err == nil { - _, err = ef.ReadVirtualMemory(raw[:], int64(addr)) - } + _, val, err = ef.SymbolData(libpf.SymbolName(sym), 4) if err != nil { - return nil, fmt.Errorf("symbol '%s': %v", sym, err) + return nil, fmt.Errorf("unable to read '%s': %v", sym, err) } - vers[i] = npsr.Uint32(raw[:], 0) + vers[i] = npsr.Uint32(val, 0) } version := vers[0]*0x1000000 + vers[1]*0x10000 + vers[2] @@ -2155,19 +2163,11 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr vers[0], vers[1], vers[2]) } - var syms libpf.SymbolFinder - syms, err = ef.ReadDynamicSymbols() - if err != nil { - // Dynamic section does not exists for core dumps. Use the pfelf as - // symbol finder then. - syms = ef - } - d := &v8Data{ version: version, } - addr, err := syms.LookupSymbolAddress("_ZN2v88internal8Snapshot19DefaultSnapshotBlobEv") + addr, err := ef.LookupSymbolAddress("_ZN2v88internal8Snapshot19DefaultSnapshotBlobEv") if err == nil { // If there is a big stack delta soon after v8::internal::Snapshot::DefaultSnapshotBlob() // assume it is the V8 snapshot data. @@ -2180,7 +2180,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr } } - sym, err := syms.LookupSymbol("_ZN2v88internal11interpreter9Bytecodes14kBytecodeSizesE") + sym, err := ef.LookupSymbol("_ZN2v88internal11interpreter9Bytecodes14kBytecodeSizesE") if err == nil && sym.Size%3 == 0 && sym.Size < 3*256 { // Symbol v8::internal::interpreter::Bytecodes::kBytecodeSizes: // static const uint8_t Bytecodes::kBytecodeSizes[3][kBytecodeCount]; @@ -2203,7 +2203,7 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr } // load introspection data - if err = d.readIntrospectionData(ef, syms); err != nil { + if err = d.readIntrospectionData(ef); err != nil { return nil, err } diff --git a/interpreter/nodev8/v8_test.go b/interpreter/nodev8/v8_test.go new file mode 100644 index 000000000..65d68d785 --- /dev/null +++ b/interpreter/nodev8/v8_test.go @@ -0,0 +1,43 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package nodev8 // import "go.opentelemetry.io/ebpf-profiler/interpreter/nodev8" + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRegexs(t *testing.T) { + shouldMatch := []string{ + "node", + "node8", + "./node", + "/foo/bar/node", + "./foo/bar/node", + "nsolid", + "nsolid8", + "./nsolid", + "/foo/bar/nsolid", + "./foo/bar/nsolid", + "./libnode.so", + "/lib/libnode.so.12", + } + for _, s := range shouldMatch { + assert.True(t, v8Regex.MatchString(s), "regex %s should match %s", + v8Regex.String(), s) + } + + shouldNotMatch := []string{ + "node-foo", + "./nsolid-bar", + "/lib/libnodetest.so", + "/lib/libnode.so.1.2.3.4.5", + "node-nsolid", + } + for _, s := range shouldNotMatch { + assert.False(t, v8Regex.MatchString(s), "regex %s should not match %s", + v8Regex.String(), s) + } +} diff --git a/interpreter/perl/data.go b/interpreter/perl/data.go index 6a76081a5..41fb09dfc 100644 --- a/interpreter/perl/data.go +++ b/interpreter/perl/data.go @@ -24,7 +24,6 @@ type perlData struct { // vmStructs reflects the Perl internal class names and the offsets of named field // The struct names are based on the Perl C "struct name", the alternate typedef seen // mostly in code is in parenthesis. - //nolint:golint,stylecheck,revive vmStructs struct { // interpreter struct (PerlInterpreter) is defined in intrpvar.h via macro trickery // https://github.com/Perl/perl5/blob/v5.32.0/intrpvar.h diff --git a/interpreter/perl/perl.go b/interpreter/perl/perl.go index f3f7108ef..7228aa25b 100644 --- a/interpreter/perl/perl.go +++ b/interpreter/perl/perl.go @@ -52,7 +52,6 @@ import ( "go.opentelemetry.io/ebpf-profiler/interpreter" ) -//nolint:golint,stylecheck,revive const ( // Scalar Value types (SVt) // https://github.com/Perl/perl5/blob/v5.32.0/sv.h#L132-L166 diff --git a/interpreter/php/instance.go b/interpreter/php/instance.go index fa619f314..57538a6e6 100644 --- a/interpreter/php/instance.go +++ b/interpreter/php/instance.go @@ -24,7 +24,6 @@ import ( "go.opentelemetry.io/ebpf-profiler/util" ) -//nolint:golint,stylecheck,revive const ( // zend_function.type definitions from PHP sources ZEND_USER_FUNCTION = (1 << 1) diff --git a/interpreter/php/opcache.go b/interpreter/php/opcache.go index 5e5107643..6c9617af5 100644 --- a/interpreter/php/opcache.go +++ b/interpreter/php/opcache.go @@ -311,18 +311,14 @@ func getOpcacheJITInfo(ef *pfelf.File) (dasmBuf, dasmSize libpf.Address, err err // Note: zend_jit_unprotect was chosen because it immediately calls mprotect with // dasm_buf as the first parameter, which should be in a register for both x86-64 // and ARM64. - zendJit, err := ef.LookupSymbolAddress("zend_jit_unprotect") - if err != nil { - return 0, 0, err - } // We should only need 64 bytes, since this should be early in the instruction sequence. - code := make([]byte, 64) - if _, err = ef.ReadVirtualMemory(code, int64(zendJit)); err != nil { - return 0, 0, err + sym, code, err := ef.SymbolData("zend_jit_unprotect", 64) + if err != nil { + return 0, 0, fmt.Errorf("unable to read 'zend_jit_unprotect': %w", err) } - dasmBufPtr, dasmSizePtr, err := retrieveJITBufferPtrWrapper(code, zendJit) + dasmBufPtr, dasmSizePtr, err := retrieveJITBufferPtrWrapper(code, sym.Address) if err != nil { return 0, 0, fmt.Errorf("failed to extract DASM pointers: %w", err) } diff --git a/interpreter/php/php.go b/interpreter/php/php.go index 5b6206adb..45d3bc9c3 100644 --- a/interpreter/php/php.go +++ b/interpreter/php/php.go @@ -22,7 +22,6 @@ import ( "go.opentelemetry.io/ebpf-profiler/support" ) -//nolint:golint,stylecheck,revive const ( // This is used to check if the VM mode is the default one // From https://github.com/php/php-src/blob/PHP-8.0/Zend/zend_vm_opcodes.h#L29 @@ -71,7 +70,6 @@ type phpData struct { rtAddr libpf.Address // vmStructs reflects the PHP internal class names and the offsets of named field - //nolint:golint,stylecheck,revive vmStructs struct { // https://github.com/php/php-src/blob/PHP-7.4/Zend/zend_globals.h#L135 zend_executor_globals struct { @@ -204,21 +202,16 @@ func recoverExecuteExJumpLabelAddress(ef *pfelf.File) (libpf.SymbolValue, error) // executor function, has been such at least since PHP7.0. This is guaranteed // to be the vm executor function in PHP JIT'd code, since the JIT is (currently) // inoperable with overridden execute_ex's - executeExAddr, err := ef.LookupSymbolAddress("execute_ex") - if err != nil { - return libpf.SymbolValueInvalid, - fmt.Errorf("could not find execute_ex: %w", err) - } // The address we care about varies from being 47 bytes in to about 107 bytes in, - // so we'll read 128 bytes. This might need to be adjusted up in future. - code := make([]byte, 128) - if _, err = ef.ReadVirtualMemory(code, int64(executeExAddr)); err != nil { + // so we'll cap at 128 bytes. This might need to be adjusted up in future. + sym, code, err := ef.SymbolData("execute_ex", 128) + if err != nil { return libpf.SymbolValueInvalid, - fmt.Errorf("could not read from executeExAddr: %w", err) + fmt.Errorf("unable to read 'execute_ex': %w", err) } - returnAddress, err := retrieveExecuteExJumpLabelAddressWrapper(code, executeExAddr) + returnAddress, err := retrieveExecuteExJumpLabelAddressWrapper(code, sym.Address) if err != nil { return libpf.SymbolValueInvalid, fmt.Errorf("reading the return address from execute_ex failed (%w)", @@ -235,23 +228,17 @@ func determineVMKind(ef *pfelf.File) (uint, error) { // This is a publicly exposed function in PHP that returns the VM type // This has been implemented in PHP since at least 7.2 - vmKindAddr, err := ef.LookupSymbolAddress("zend_vm_kind") - if err != nil { - return 0, fmt.Errorf("zend_vm_kind not found: %w", err) - } // We should only need around 32 bytes here, since this function should be // really short (e.g a mov and a ret). - code := make([]byte, 32) - if _, err = ef.ReadVirtualMemory(code, int64(vmKindAddr)); err != nil { - return 0, fmt.Errorf("could not read from zend_vm_kind: %w", err) + _, code, err := ef.SymbolData("zend_vm_kind", 64) + if err != nil { + return 0, fmt.Errorf("unable to read 'zend_vm_kind': %w", err) } - vmKind, err := retrieveZendVMKindWrapper(code) if err != nil { return 0, fmt.Errorf("an error occurred decoding zend_vm_kind: %w", err) } - return vmKind, nil } diff --git a/interpreter/python/decode.go b/interpreter/python/decode.go index 9412aa594..820ab0ffc 100644 --- a/interpreter/python/decode.go +++ b/interpreter/python/decode.go @@ -4,16 +4,22 @@ package python // import "go.opentelemetry.io/ebpf-profiler/interpreter/python" import ( - ah "go.opentelemetry.io/ebpf-profiler/armhelpers" - aa "golang.org/x/arch/arm64/arm64asm" + "errors" + "fmt" + "runtime" + ah "go.opentelemetry.io/ebpf-profiler/armhelpers" + "go.opentelemetry.io/ebpf-profiler/asm/amd" "go.opentelemetry.io/ebpf-profiler/libpf" + aa "golang.org/x/arch/arm64/arm64asm" + "golang.org/x/arch/x86/x86asm" ) -// decodeStubArgumentWrapperARM64 disassembles arm64 code and decodes the assumed value +// decodeStubArgumentARM64 disassembles arm64 code and decodes the assumed value // of requested argument. -func decodeStubArgumentWrapperARM64(code []byte, argNumber uint8, _, +func decodeStubArgumentARM64(code []byte, addrBase libpf.SymbolValue) libpf.SymbolValue { + const argNumber uint8 = 0 // The concept is to track the latest load offset for all X0..X30 registers. // These registers are used as the function arguments. Once the first branch // instruction (function call/tail jump) is found, the state of the requested @@ -100,3 +106,96 @@ func decodeStubArgumentWrapperARM64(code []byte, argNumber uint8, _, return libpf.SymbolValueInvalid } + +func decodeStubArgumentAMD64(code []byte, codeAddress, memoryBase uint64) ( + libpf.SymbolValue, error) { + targetRegister := x86asm.RDI + + instructionOffset := 0 + regs := amd.RegsState{} + + for instructionOffset < len(code) { + rem := code[instructionOffset:] + if ok, insnLen := amd.DecodeSkippable(rem); ok { + instructionOffset += insnLen + continue + } + + inst, err := x86asm.Decode(rem, 64) + if err != nil { + return 0, fmt.Errorf("failed to decode instruction at 0x%x : %w", + instructionOffset, err) + } + instructionOffset += inst.Len + regs.Set(x86asm.RIP, codeAddress+uint64(instructionOffset), 0) + + if inst.Op == x86asm.CALL || inst.Op == x86asm.JMP { + value, loadedFrom := regs.Get(targetRegister) + if loadedFrom != 0 { + return libpf.SymbolValue(loadedFrom), nil + } + return libpf.SymbolValue(value), nil + } + + if (inst.Op == x86asm.LEA || inst.Op == x86asm.MOV) && inst.Args[0] != nil { + if reg, ok := inst.Args[0].(x86asm.Reg); ok { + var value uint64 + var loadedFrom uint64 + + switch src := inst.Args[1].(type) { + case x86asm.Imm: + value = uint64(src) + case x86asm.Mem: + baseAddr, _ := regs.Get(src.Base) + displacement := uint64(src.Disp) + + if inst.Op == x86asm.MOV { + value = memoryBase + loadedFrom = baseAddr + displacement + if src.Index != 0 { + indexValue, _ := regs.Get(src.Index) + loadedFrom += indexValue * uint64(src.Scale) + } + } else if inst.Op == x86asm.LEA { + value = baseAddr + displacement + if src.Index != 0 { + indexValue, _ := regs.Get(src.Index) + value += indexValue * uint64(src.Scale) + } + } + + case x86asm.Reg: + value, _ = regs.Get(src) + } + + regs.Set(reg, value, loadedFrom) + } + } + + if inst.Op == x86asm.ADD && inst.Args[0] != nil && inst.Args[1] != nil { + if reg, ok0 := inst.Args[0].(x86asm.Reg); ok0 { + if _, ok1 := inst.Args[1].(x86asm.Mem); ok1 { + oldValue, _ := regs.Get(reg) + value := oldValue + memoryBase + regs.Set(reg, value, 0) + } + } + } + } + return 0, errors.New("no call/jump instructions found") +} + +func decodeStubArgumentWrapper( + code []byte, + codeAddress libpf.SymbolValue, + memoryBase libpf.SymbolValue, +) (libpf.SymbolValue, error) { + switch runtime.GOARCH { + case "arm64": + return decodeStubArgumentARM64(code, memoryBase), nil + case "amd64": + return decodeStubArgumentAMD64(code, uint64(codeAddress), uint64(memoryBase)) + default: + return libpf.SymbolValueInvalid, fmt.Errorf("unsupported arch %s", runtime.GOARCH) + } +} diff --git a/interpreter/python/decode_amd64.c b/interpreter/python/decode_amd64.c deleted file mode 100644 index 1cf7c2dd8..000000000 --- a/interpreter/python/decode_amd64.c +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -//go:build amd64 - -#include "../../zydis/Zydis.h" -#include "decode_amd64.h" - -#include - -// decode_stub_argument() will decode instructions from given code blob until an assignment -// for the given argument register is found. The value loaded is then determined from the -// opcode. A call/jump instruction will terminate the finding as we are finding the argument -// to first function call (or tail call). -// Currently the following addressing schemes for the assignment are supported: -// 1) Loading virtual address with immediate value. This happens for non-PIC globals. -// 2) Loading RIP-relative virtual address. Happens for PIC/PIE globals. -// 3) Loading via pointer + displacement. Happens when the main state is given as argument, -// and the value is loaded from it. In this case 'memory_base' should be the address of -// the global state variable. -uint64_t decode_stub_argument(const uint8_t* code, size_t codesz, uint8_t argument_no, - uint64_t rip_base, uint64_t memory_base) { - ZydisDecoder decoder; - ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64); - - // Argument number to x86_64 calling convention register mapping. - ZydisRegister target_register64, target_register32; - switch (argument_no) { - case 0: - target_register64 = ZYDIS_REGISTER_RDI; - target_register32 = ZYDIS_REGISTER_EDI; - break; - case 1: - target_register64 = ZYDIS_REGISTER_RSI; - target_register32 = ZYDIS_REGISTER_ESI; - break; - case 2: - target_register64 = ZYDIS_REGISTER_RDX; - target_register32 = ZYDIS_REGISTER_EDX; - break; - default: - return 0; - } - - // Iterate instructions - ZydisDecodedInstruction instr; - ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; - ZyanUSize instruction_offset = 0; - while (ZYAN_SUCCESS(ZydisDecoderDecodeFull(&decoder, code + instruction_offset, - codesz - instruction_offset, &instr, operands))) { - instruction_offset += instr.length; - if (instr.mnemonic == ZYDIS_MNEMONIC_CALL || - instr.mnemonic == ZYDIS_MNEMONIC_JMP) { - // Unexpected call/jmp indicating end of stub code - return 0; - } - if (!(instr.mnemonic == ZYDIS_MNEMONIC_LEA || - instr.mnemonic == ZYDIS_MNEMONIC_MOV) || - operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER || - (operands[0].reg.value != target_register64 && - operands[0].reg.value != target_register32)) { - // Only "LEA/MOV target_reg, ..." meaningful - continue; - } - if (operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE) { - // MOV target_reg, immediate - return operands[1].imm.value.u; - } - if (operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY && - operands[1].mem.disp.has_displacement) { - if (operands[1].mem.base == ZYDIS_REGISTER_RIP) { - // MOV/LEA target_reg, [RIP + XXXX] - return rip_base + instruction_offset + operands[1].mem.disp.value; - } else if (memory_base) { - // MOV/LEA target_reg, [REG + XXXX] - return memory_base + operands[1].mem.disp.value; - } - continue; - } - } - - return 0; -} diff --git a/interpreter/python/decode_amd64.go b/interpreter/python/decode_amd64.go deleted file mode 100644 index 55080f246..000000000 --- a/interpreter/python/decode_amd64.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build amd64 - -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package python // import "go.opentelemetry.io/ebpf-profiler/interpreter/python" - -import ( - "unsafe" - - "go.opentelemetry.io/ebpf-profiler/libpf" - _ "go.opentelemetry.io/ebpf-profiler/zydis" // links Zydis -) - -// #cgo CFLAGS: -g -Wall -// #include "decode_amd64.h" -// #include "../../support/ebpf/types.h" -import "C" - -func decodeStubArgumentWrapperX64(code []byte, argNumber uint8, symbolValue, - addrBase libpf.SymbolValue) libpf.SymbolValue { - return libpf.SymbolValue(C.decode_stub_argument( - (*C.uint8_t)(unsafe.Pointer(&code[0])), C.size_t(len(code)), - C.uint8_t(argNumber), C.uint64_t(symbolValue), C.uint64_t(addrBase))) -} - -func decodeStubArgumentWrapper(code []byte, argNumber uint8, symbolValue, - addrBase libpf.SymbolValue) libpf.SymbolValue { - return decodeStubArgumentWrapperX64(code, argNumber, symbolValue, addrBase) -} diff --git a/interpreter/python/decode_amd64.h b/interpreter/python/decode_amd64.h deleted file mode 100644 index ffa9e737a..000000000 --- a/interpreter/python/decode_amd64.h +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -//go:build amd64 - -#ifndef __PYTHON_DECODE_X86_64__ -#define __PYTHON_DECODE_X86_64__ - -#include - -uint64_t decode_stub_argument(const uint8_t* code, size_t codesz, uint8_t argument_no, uint64_t rip_base, uint64_t memory_base); - -#endif diff --git a/interpreter/python/decode_arm64.go b/interpreter/python/decode_arm64.go deleted file mode 100644 index a2461af11..000000000 --- a/interpreter/python/decode_arm64.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build arm64 - -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package python // import "go.opentelemetry.io/ebpf-profiler/interpreter/python" - -import ( - "go.opentelemetry.io/ebpf-profiler/libpf" -) - -func decodeStubArgumentWrapper(code []byte, argNumber uint8, symbolValue, - addrBase libpf.SymbolValue) libpf.SymbolValue { - return decodeStubArgumentWrapperARM64(code, argNumber, symbolValue, addrBase) -} diff --git a/interpreter/python/decode_test.go b/interpreter/python/decode_test.go index 96ae9dabb..e2cec192d 100644 --- a/interpreter/python/decode_test.go +++ b/interpreter/python/decode_test.go @@ -1,34 +1,36 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +//nolint:lll package python import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/ebpf-profiler/libpf" ) func TestAnalyzeArm64Stubs(t *testing.T) { - val := decodeStubArgumentWrapperARM64( + val := decodeStubArgumentARM64( []byte{ 0x40, 0x0a, 0x00, 0x90, 0x01, 0xd4, 0x43, 0xf9, 0x22, 0x60, 0x17, 0x91, 0x40, 0x00, 0x40, 0xf9, 0xa2, 0xff, 0xff, 0x17}, - 0, 0, 0) + 0) assert.Equal(t, libpf.SymbolValue(1496), val, "PyEval_ReleaseLock stub test") - val = decodeStubArgumentWrapperARM64( + val = decodeStubArgumentARM64( []byte{ 0x80, 0x12, 0x00, 0xb0, 0x02, 0xd4, 0x43, 0xf9, 0x41, 0xf4, 0x42, 0xf9, 0x61, 0x00, 0x00, 0xb4, 0x40, 0xc0, 0x17, 0x91, 0xad, 0xe4, 0xfe, 0x17}, - 0, 0, 0) + 0) assert.Equal(t, libpf.SymbolValue(1520), val, "PyGILState_GetThisThreadState test") // Python 3.10.12 on ARM64 Nix - val = decodeStubArgumentWrapperARM64( + val = decodeStubArgumentARM64( []byte{ 0x40, 0x1a, 0x00, 0xd0, // adrp x0, 0xffffa0eff000 0x00, 0xa0, 0x46, 0xf9, // ldr x0, [x0, #3392] @@ -39,6 +41,259 @@ func TestAnalyzeArm64Stubs(t *testing.T) { 0x00, 0x00, 0x80, 0xd2, // mov x0, #0x0 0xc0, 0x03, 0x5f, 0xd6, // ret }, - 0, 0, 0) + 0) assert.Equal(t, libpf.SymbolValue(604), val, "PyGILState_GetThisThreadState test") } + +func BenchmarkDecodeAmd64(b *testing.B) { + for i := 0; i < b.N; i++ { + code := []byte{ + 0xf3, 0x0f, 0x1e, 0xfa, // 1bbba0: endbr64 + 0x48, 0x83, 0x3d, 0x74, 0x90, 0x1e, 0x00, // 1bbba4: cmp QWORD PTR [rip+0x1e9074],0x0 # 3a4c20 <_PyRuntime+0x240> + 0x00, // 1bbbab: + 0x74, 0x0b, // 1bbbac: je 1bbbb9 + 0x8b, 0x3d, 0x78, 0x90, 0x1e, 0x00, // 1bbbae: mov edi,DWORD PTR [rip+0x1e9078] # 3a4c2c <_PyRuntime+0x24c> + 0xe9, 0xe7, 0xea, 0xe9, 0xff, // 1bbbb4: jmp 5a6a0 + } + rip := uint64(0x1bbba0) + val, _ := decodeStubArgumentAMD64( + code, + rip, + 0, + ) + if val != 0x3a4c2c { + b.Fail() + } + } +} + +func TestAmd64DecodeStub(t *testing.T) { + testdata := []struct { + name string + code []byte + rip uint64 + expected uint64 + expectedError string + }{ + { + name: "3.10.16 gcc12 enable-optimizations disable-shared", + code: []byte{ + 0xf3, 0x0f, 0x1e, 0xfa, // 1bbba0: endbr64 + 0x48, 0x83, 0x3d, 0x74, 0x90, 0x1e, 0x00, // 1bbba4: cmp QWORD PTR [rip+0x1e9074],0x0 # 3a4c20 <_PyRuntime+0x240> + 0x00, // 1bbbab: + 0x74, 0x0b, // 1bbbac: je 1bbbb9 + 0x8b, 0x3d, 0x78, 0x90, 0x1e, 0x00, // 1bbbae: mov edi,DWORD PTR [rip+0x1e9078] # 3a4c2c <_PyRuntime+0x24c> + 0xe9, 0xe7, 0xea, 0xe9, 0xff, // 1bbbb4: jmp 5a6a0 + }, + rip: 0x1bbba0, + expected: 0x3a4c2c, + }, + { + name: "3.10.16 gcc12 disable-optimizations disable-shared", + code: []byte{ + 0xf3, 0x0f, 0x1e, 0xfa, // 172e50: endbr64 + 0x48, 0x83, 0x3d, 0x04, 0xef, 0x24, 0x00, // 172e54: cmp QWORD PTR [rip+0x24ef04],0x0 # 3c1d60 <_PyRuntime+0x240> + 0x00, // 172e5b: + 0x74, 0x12, // 172e5c: je 172e70 + 0x48, 0x8d, 0x3d, 0x03, 0xef, 0x24, 0x00, // 172e5e: lea rdi,[rip+0x24ef03] # 3c1d68 <_PyRuntime+0x248> + 0xe9, 0x86, 0x1e, 0x01, 0x00, // 172e65: jmp 184cf0 + }, + rip: 0x172e50, + expected: 0x3c1d68, + }, + { + name: "3.10.16 clang16 disable-optimizations enabled-shared", + code: []byte{ + 0x48, 0x8b, 0x05, 0x99, 0x70, 0x16, 0x00, // 1adc90: mov rax,QWORD PTR [rip+0x167099] # 314d30 <_PyRuntime@@Base-0x33668> + 0x48, 0x83, 0xb8, 0x40, 0x02, 0x00, 0x00, // 1adc97: cmp QWORD PTR [rax+0x240],0x0 + 0x00, // 1adc9e: + 0x74, 0x11, // 1adc9f: je 1adcb2 + 0xbf, 0x48, 0x02, 0x00, 0x00, // 1adca1: mov edi,0x248 + 0x48, 0x03, 0x3d, 0x83, 0x70, 0x16, 0x00, // 1adca6: add rdi,QWORD PTR [rip+0x167083] # 314d30 <_PyRuntime@@Base-0x33668> + 0xe9, 0x2e, 0x41, 0xeb, 0xff, // 1adcad: jmp 61de0 + }, + rip: 0x1adc90, + expected: 0x248, + }, + { + name: "3.12.8 gcc12 disable-optimizations enabled-shared", + code: []byte{ + 0xf3, 0x0f, 0x1e, 0xfa, // 2e25d0: endbr64 + 0x48, 0x8b, 0x05, 0x25, 0x27, 0x27, 0x00, // 2e25d4: mov rax,QWORD PTR [rip+0x272725] # 554d00 <_PyRuntime@@Base-0x1004e0> + 0x53, // 2e25db: push rbx + 0x48, 0x8d, 0x98, 0x08, 0x06, 0x00, 0x00, // 2e25dc: lea rbx,[rax+0x608] + 0x48, 0x89, 0xdf, // 2e25e3: mov rdi,rbx + 0xe8, 0x95, 0x78, 0xe2, 0xff, // 2e25e6: call 109e80 + }, + rip: 0x2e25d0, + expected: 0x608, + }, + { + name: "3.10.16 clang18 enable-optimizations enabled-shared", + code: []byte{ + 0x48, 0x8b, 0x05, 0xd9, 0x80, 0x31, 0x00, // cac50: mov rax,QWORD PTR [rip+0x3180d9] # 3e2d30 <_PyRuntime@@Base-0x32c28> + 0x48, 0x83, 0xb8, 0x40, 0x02, 0x00, 0x00, // cac57: cmp QWORD PTR [rax+0x240],0x0 + 0x00, // cac5e: + 0x74, 0x0b, // cac5f: je cac6c + 0x8b, 0xb8, 0x4c, 0x02, 0x00, 0x00, // cac61: mov edi,DWORD PTR [rax+0x24c] + 0xe9, 0x24, 0x55, 0xf9, 0xff, // cac67: jmp 60190 + }, + rip: 0xcac50, + expected: 0x24c, + }, + { + name: "3.10.16 clang18 enable-optimizations disable-shared", + code: []byte{ + 0x48, 0x83, 0x3d, 0x98, 0xc5, 0x36, 0x00, // 92000: cmp QWORD PTR [rip+0x36c598],0x0 # 3fe5a0 <_PyRuntime+0x240> + 0x00, // 92007: + 0x74, 0x0b, // 92008: je 92015 + 0x8b, 0x3d, 0x9c, 0xc5, 0x36, 0x00, // 9200a: mov edi,DWORD PTR [rip+0x36c59c] # 3fe5ac <_PyRuntime+0x24c> + 0xe9, 0x4b, 0x70, 0xfc, 0xff, // 92010: jmp 59060 + }, + rip: 0x92000, + expected: 0x3fe5ac, + }, + { + name: "3.10.16 clang16 disable-optimizations disable-shared", + code: []byte{ + 0x48, 0x8d, 0x05, 0x69, 0x19, 0x21, 0x00, // 129bc0: lea rax,[rip+0x211969] # 33b530 <_PyRuntime> + 0x48, 0x83, 0xb8, 0x40, 0x02, 0x00, 0x00, // 129bc7: cmp QWORD PTR [rax+0x240],0x0 + 0x00, // 129bce: + 0x74, 0x11, // 129bcf: je 129be2 + 0xbf, 0x48, 0x02, 0x00, 0x00, // 129bd1: mov edi,0x248 + 0x48, 0x03, 0x3d, 0x53, 0x03, 0x1e, 0x00, // 129bd6: add rdi,QWORD PTR [rip+0x1e0353] # 309f30 <_DYNAMIC+0x328> + 0xe9, 0x8e, 0xec, 0x00, 0x00, // 129bdd: jmp 138870 + }, + rip: 0x129bc0, + expected: 0x248, + }, + { + name: "3.12.8 clang16 disable-optimizations disable-shared", + code: []byte{ + 0x53, // 2a20d0: push rbx + 0xbb, 0x08, 0x06, 0x00, 0x00, // 2a20d1: mov ebx,0x608 + 0x48, 0x03, 0x1d, 0x0b, 0x1e, 0x25, 0x00, // 2a20d6: add rbx,QWORD PTR [rip+0x251e0b] # 4f3ee8 <_DYNAMIC+0x368> + 0x48, 0x89, 0xdf, // 2a20dd: mov rdi,rbx + 0xe8, 0x7b, 0x41, 0x01, 0x00, // 2a20e0: call 2b6260 + }, + rip: 0x2a20d0, + expected: 0x608, + }, + { + name: "3.10.16 clang16 disable-optimizations enabled-shared", + code: []byte{ + 0xf3, 0x0f, 0x1e, 0xfa, // 2079c0: endbr64 + 0x48, 0x8b, 0x05, 0x65, 0x03, 0x18, 0x00, // 2079c4: mov rax,QWORD PTR [rip+0x180365] # 387d30 <_PyRuntime@@Base-0x34950> + 0x48, 0x83, 0xb8, 0x40, 0x02, 0x00, 0x00, // 2079cb: cmp QWORD PTR [rax+0x240],0x0 + 0x00, // 2079d2: + 0x74, 0x13, // 2079d3: je 2079e8 + 0x48, 0x8d, 0xb8, 0x48, 0x02, 0x00, 0x00, // 2079d5: lea rdi,[rax+0x248] + 0xe9, 0x8f, 0x1f, 0xe6, 0xff, // 2079dc: jmp 69970 + }, + rip: 0x2079c0, + expected: 0x248, + }, + { + name: "3.12.8 gcc12 disable-optimizations disable-shared", + code: []byte{ + 0xf3, 0x0f, 0x1e, 0xfa, // 2eb960: endbr64 + 0x53, // 2eb964: push rbx + 0x48, 0x8d, 0x1d, 0xbc, 0x21, 0x37, 0x00, // 2eb965: lea rbx,[rip+0x3721bc] # 65db28 <_PyRuntime+0x608> + 0x48, 0x89, 0xdf, // 2eb96c: mov rdi,rbx + 0xe8, 0x0c, 0x7f, 0x01, 0x00, // 2eb96f: call 303880 + }, + rip: 0x2eb960, + expected: 0x65db28, + }, + { + name: "3.10.16 gcc12 enable-optimizations enabled-shared", + code: []byte{ + 0xf3, 0x0f, 0x1e, 0xfa, // 1c03c0: endbr64 + 0x48, 0x8b, 0x05, 0x5d, 0x69, 0x1b, 0x00, // 1c03c4: mov rax,QWORD PTR [rip+0x1b695d] # 376d28 <_PyRuntime@@Base-0x32758> + 0x48, 0x83, 0xb8, 0x40, 0x02, 0x00, 0x00, // 1c03cb: cmp QWORD PTR [rax+0x240],0x0 + 0x00, // 1c03d2: + 0x74, 0x0b, // 1c03d3: je 1c03e0 + 0x8b, 0xb8, 0x4c, 0x02, 0x00, 0x00, // 1c03d5: mov edi,DWORD PTR [rax+0x24c] + 0xe9, 0x10, 0xb4, 0xe9, 0xff, // 1c03db: jmp 5b7f0 + }, + rip: 0x1c03c0, + expected: 0x24c, + }, + { + name: "empty code", + code: nil, + expectedError: "no call/jump instructions found", + }, + { + name: "no call/jump instructions found", + code: []byte{ + 0x48, 0xC7, 0xC7, 0xEF, 0xEF, 0xEF, 0x00, // mov rdi, 0xefefef + }, + expectedError: "no call/jump instructions found", + }, + { + name: "bad instruction", + code: []byte{ + 0x48, 0xC7, 0xC7, 0xEF, 0xEF, 0xEF, 0x00, // mov rdi, 0xefefef + 0xea, // :shrug: + }, + expectedError: "failed to decode instruction at 0x7", + }, + { + name: "synthetic mov scale index", + code: []byte{ + 0x48, 0xC7, 0xC0, 0xCA, 0xCA, 0x00, 0x00, // mov rax, 0xcaca + 0xBB, 0x00, 0x00, 0x00, 0x5E, // mov ebx, 0x5e000000 + 0x67, 0x48, 0x8B, 0x7C, 0x43, 0x05, // mov rdi, qword ptr [ebx + eax*2 + 5] + 0xEB, 0x00, // jmp 0x14 + }, + expected: 0xCACA*2 + 0x5E000000 + 5, + }, + { + name: "synthetic lea scale index", + code: []byte{ + 0x48, 0xC7, 0xC0, 0xFE, 0xCA, 0x00, 0x00, // mov rax, 0xcafe + 0xBB, 0x00, 0x00, 0x00, 0x6E, // mov ebx, 0x6e000000 + 0x67, 0x48, 0x8D, 0x7C, 0x43, 0x07, // lea rdi, [ebx + eax*2 + 7] + 0xE8, 0xFB, 0xFF, 0xFF, 0xFF, // call 0x12 + }, + expected: 0xCAFE*2 + 0x6E000000 + 7, + }, + { + name: "synthetic lea edi, ... scale index", + code: []byte{ + 0xB8, 0xEF, 0x00, 0x00, 0x00, // mov eax, 0xef + 0xBB, 0x2A, 0x00, 0x00, 0x00, // mov ebx, 0x2a + 0x67, 0x8D, 0x7C, 0x43, 0x07, // lea edi, [ebx + eax*2 + 7] + 0xEB, 0xEF, // jmp 0 + }, + expected: 0xEF*2 + 0x2a + 7, + }, + } + + for _, td := range testdata { + t.Run(td.name, func(t *testing.T) { + val, err := decodeStubArgumentAMD64( + td.code, + td.rip, + 0, // NULL pointer as mem + ) + if td.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), td.expectedError) + } else { + require.NoError(t, err) + } + assert.Equal(t, td.expected, uint64(val)) + }) + } +} + +func FuzzDecodeAmd(f *testing.F) { + f.Fuzz(func(_ *testing.T, code []byte, rip uint64) { + _, err := decodeStubArgumentAMD64(code, rip, 0) + if err != nil { + return + } + }) +} diff --git a/interpreter/python/python.go b/interpreter/python/python.go index e5db89604..0d1c491ce 100644 --- a/interpreter/python/python.go +++ b/interpreter/python/python.go @@ -6,6 +6,7 @@ package python // import "go.opentelemetry.io/ebpf-profiler/interpreter/python" import ( "bytes" "debug/elf" + "encoding/hex" "errors" "fmt" "hash/fnv" @@ -18,6 +19,8 @@ import ( "unsafe" log "github.com/sirupsen/logrus" + "go.opentelemetry.io/ebpf-profiler/asm/amd" + "go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo" "github.com/elastic/go-freelru" @@ -655,33 +658,31 @@ func (d *pythonData) readIntrospectionData(ef *pfelf.File, symbol libpf.SymbolNa // decodeStub will resolve a given symbol, extract the code for it, and analyze // the code to resolve specified argument parameter to the first jump/call. -func decodeStub(ef *pfelf.File, addrBase libpf.SymbolValue, symbolName libpf.SymbolName, - argNumber uint8) libpf.SymbolValue { - symbolValue, err := ef.LookupSymbolAddress(symbolName) +func decodeStub(ef *pfelf.File, memoryBase libpf.SymbolValue, + symbolName libpf.SymbolName) (libpf.SymbolValue, error) { + // Read and decode the code for the symbol + sym, code, err := ef.SymbolData(symbolName, 64) if err != nil { - return libpf.SymbolValueInvalid + return libpf.SymbolValueInvalid, fmt.Errorf("unable to read '%s': %v", + symbolName, err) } - - code := make([]byte, 64) - if _, err := ef.ReadVirtualMemory(code, int64(symbolValue)); err != nil { - return libpf.SymbolValueInvalid - } - - value := decodeStubArgumentWrapper(code, argNumber, symbolValue, addrBase) + value, err := decodeStubArgumentWrapper(code, sym.Address, memoryBase) // Sanity check the value range and alignment - if value%4 != 0 { - return libpf.SymbolValueInvalid + if err != nil || value%4 != 0 { + return libpf.SymbolValueInvalid, fmt.Errorf("decode stub %s 0x%x %s failed (0x%x): %v", + symbolName, sym.Address, hex.Dump(code), value, err) } // If base symbol (_PyRuntime) is not provided, accept any found value. - if addrBase == 0 && value != 0 { - return value + if memoryBase == 0 && value != 0 { + return value, nil } // Check that the found value is within reasonable distance from the given symbol. - if value > addrBase && value < addrBase+4096 { - return value + if value > memoryBase && value < memoryBase+4096 { + return value, nil } - return libpf.SymbolValueInvalid + return libpf.SymbolValueInvalid, fmt.Errorf("decode stub %s 0x%x %s failed (0x%x)", + symbolName, sym.Address, hex.Dump(code), value) } func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) { @@ -736,9 +737,9 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr } // Calls first: PyThread_tss_get(autoTSSKey) - autoTLSKey = decodeStub(ef, pyruntimeAddr, "PyGILState_GetThisThreadState", 0) + autoTLSKey, err = decodeStub(ef, pyruntimeAddr, "PyGILState_GetThisThreadState") if autoTLSKey == libpf.SymbolValueInvalid { - return nil, errors.New("unable to resolve autoTLSKey") + return nil, fmt.Errorf("unable to resolve autoTLSKey %v", err) } if version >= pythonVer(3, 7) && autoTLSKey%8 == 0 { // On Python 3.7+, the call is to PyThread_tss_get, but can get optimized to @@ -753,19 +754,9 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr autoTLSKey += 4 } - // The Python main interpreter loop history in CPython git is: - // - //nolint:lll - // 87af12bff33 v3.11 2022-02-15 _PyEval_EvalFrameDefault(PyThreadState*,_PyInterpreterFrame*,int) - // ae0a2b75625 v3.10 2021-06-25 _PyEval_EvalFrameDefault(PyThreadState*,_interpreter_frame*,int) - // 0b72b23fb0c v3.9 2020-03-12 _PyEval_EvalFrameDefault(PyThreadState*,PyFrameObject*,int) - // 3cebf938727 v3.6 2016-09-05 _PyEval_EvalFrameDefault(PyFrameObject*,int) - // 49fd7fa4431 v3.0 2006-04-21 PyEval_EvalFrameEx(PyFrameObject*,int) - interpRanges, err := info.GetSymbolAsRanges("_PyEval_EvalFrameDefault") + interpRanges, err := findInterpreterRanges(info, ef) if err != nil { - if interpRanges, err = info.GetSymbolAsRanges("PyEval_EvalFrameEx"); err != nil { - return nil, err - } + return nil, err } pd := &pythonData{ @@ -840,3 +831,65 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr return pd, nil } + +func findInterpreterRanges(info *interpreter.LoaderInfo, ef *pfelf.File, +) (interpRanges []util.Range, err error) { + // The Python main interpreter loop history in CPython git is: + // + //nolint:lll + // 87af12bff33 v3.11 2022-02-15 _PyEval_EvalFrameDefault(PyThreadState*,_PyInterpreterFrame*,int) + // ae0a2b75625 v3.10 2021-06-25 _PyEval_EvalFrameDefault(PyThreadState*,_interpreter_frame*,int) + // 0b72b23fb0c v3.9 2020-03-12 _PyEval_EvalFrameDefault(PyThreadState*,PyFrameObject*,int) + // 3cebf938727 v3.6 2016-09-05 _PyEval_EvalFrameDefault(PyFrameObject*,int) + // 49fd7fa4431 v3.0 2006-04-21 PyEval_EvalFrameEx(PyFrameObject*,int) + var interp *libpf.Symbol + var code []byte + const maxCodeSize = 128 * 1024 // observed ~65k in the wild + if interp, code, err = ef.SymbolData("_PyEval_EvalFrameDefault", maxCodeSize); err != nil { + interp, code, err = ef.SymbolData("PyEval_EvalFrameEx", maxCodeSize) + } + if err != nil { + return nil, errors.New("no _PyEval_EvalFrameDefault/PyEval_EvalFrameEx symbol found") + } + interpRanges = make([]util.Range, 0, 2) + interpRanges = append(interpRanges, util.Range{ + Start: uint64(interp.Address), + End: uint64(interp.Address) + interp.Size, + }) + coldRange, err := findColdRange(ef, code, interp) + if err != nil { + log.WithError(err).Warnf("failed to recover python ranges %s", + info.FileName()) + } + if coldRange != (util.Range{}) { + interpRanges = append(interpRanges, coldRange) + } + return interpRanges, nil +} + +// findColdRange finds a relative jump from the _PyEval_EvalFrameDefault outside itself +// (to _PyEval_EvalFrameDefault.cold symbol) and then recovers the range of the .cold +// symbol using an instance of elfunwindinfo.EhFrameTable. +// findColdRange returns the util.Range of the `.cold` symbol or an empty util.Range +// https://github.com/open-telemetry/opentelemetry-ebpf-profiler/issues/416 +func findColdRange(ef *pfelf.File, code []byte, interp *libpf.Symbol) (util.Range, error) { + if ef.Machine != elf.EM_X86_64 { + return util.Range{}, nil + } + dst, err := amd.FindExternalJump(code, interp) + if err != nil || dst == 0 { + return util.Range{}, err + } + t, err := elfunwindinfo.NewEhFrameTable(ef) + if err != nil { + return util.Range{}, err + } + fde, err := t.LookupFDE(dst) + if err != nil { + return util.Range{}, err + } + return util.Range{ + Start: uint64(fde.PCBegin), + End: uint64(fde.PCBegin + fde.PCRange), + }, nil +} diff --git a/interpreter/ruby/ruby.go b/interpreter/ruby/ruby.go index 66f04b201..ac12c18f6 100644 --- a/interpreter/ruby/ruby.go +++ b/interpreter/ruby/ruby.go @@ -12,6 +12,7 @@ import ( "regexp" "runtime" "strconv" + "strings" "sync" "sync/atomic" "unsafe" @@ -94,7 +95,6 @@ type rubyData struct { version uint32 // vmStructs reflects the Ruby internal names and offsets of named fields. - //nolint:golint,stylecheck,revive vmStructs struct { // rb_execution_context_struct // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L843 @@ -785,19 +785,16 @@ func (r *rubyInstance) GetAndResetMetrics() ([]metrics.Metric, error) { // determineRubyVersion looks for the symbol ruby_version and extracts version // information from its value. func determineRubyVersion(ef *pfelf.File) (uint32, error) { - sym, err := ef.LookupSymbol("ruby_version") + _, memory, err := ef.SymbolData("ruby_version", 64) if err != nil { - return 0, fmt.Errorf("symbol ruby_version not found: %v", err) + return 0, fmt.Errorf("unable to read 'ruby_version': %v", err) } - memory := make([]byte, 5) - if _, err := ef.ReadVirtualMemory(memory, int64(sym.Address)); err != nil { - return 0, fmt.Errorf("failed to read process memory at 0x%x:%v", - sym.Address, err) + versionString := strings.TrimRight(unsafe.String(unsafe.SliceData(memory), len(memory)), "\x00") + matches := rubyVersionRegex.FindStringSubmatch(versionString) + if len(matches) < 3 { + return 0, fmt.Errorf("failed to parse version string: '%s'", versionString) } - - matches := rubyVersionRegex.FindStringSubmatch(string(memory)) - major, _ := strconv.Atoi(matches[1]) minor, _ := strconv.Atoi(matches[2]) release, _ := strconv.Atoi(matches[3]) diff --git a/kallsyms/kallsyms.go b/kallsyms/kallsyms.go new file mode 100644 index 000000000..943fb2f78 --- /dev/null +++ b/kallsyms/kallsyms.go @@ -0,0 +1,672 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package kallsyms provides functionality for reading /proc/kallsyms +// and using it to symbolize kernel addresses. +package kallsyms // import "go.opentelemetry.io/ebpf-profiler/kallsyms" + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "hash/fnv" + "io" + "os" + "path" + "slices" + "sort" + "strconv" + "strings" + "sync/atomic" + "time" + "unsafe" + + "github.com/mdlayher/kobject" + log "github.com/sirupsen/logrus" + + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + "go.opentelemetry.io/ebpf-profiler/stringutil" +) + +// Kernel is the internal name for "module" containing the built-in symbols +const Kernel = "vmlinux" + +// pointerBits is the number of bits for pointer. Used to validate data +// from the kernel kallsyms file. +const pointerBits = int(unsafe.Sizeof(libpf.Address(0)) * 8) + +// sysModule is the sysfs path for module metadata +const sysModule = "/sys/module" + +var ErrSymbolPermissions = errors.New("unable to read kallsyms addresses - check capabilities") + +var ErrNoModule = errors.New("module not found") + +var ErrNoSymbol = errors.New("symbol not found") + +var ErrModuleStub = errors.New("symbols are not available yet - retry later") + +// symbol is the per-symbol structure. The size should be minimal as +// a typical installation has 100k-200k kernel symbols. +type symbol struct { + // offset is the symbol offset from the Module start address + offset uint32 + // index is the offset to the symbol name within the Module names slice + index uint32 +} + +// Module contains symbols and metadata for one kernel module. +type Module struct { + start libpf.Address + end libpf.Address + mtime int64 + stub bool + + buildID string + fileID libpf.FileID + names []byte + symbols []symbol +} + +// Symbolizer provides the main API for reading, updating and querying +// the kernel symbols. +type Symbolizer struct { + modules atomic.Value + + reloadModules chan bool +} + +// NewSymbolizer creates and returns a new kallsyms symbolizer and loads +// the initial 'kallsymbols'. +func NewSymbolizer() (*Symbolizer, error) { + s := &Symbolizer{ + reloadModules: make(chan bool, 1), + } + if err := s.loadKallsyms(); err != nil { + return nil, err + } + return s, nil +} + +// addName appends the 'name' to the module's string slice, and returns +// an index suitable for storing in the `symbol` struct. +func (m *Module) addName(name string) uint32 { + index := len(m.names) + l := len(name) + // Cap the length to 255 bytes so it fits a byte. Longest seen + // symbol so far is 83 bytes. + if l > 255 { + l = 255 + } + m.names = append(m.names, byte(l)) + m.names = append(m.names, unsafe.Slice(unsafe.StringData(name), l)...) + return uint32(index) +} + +// setStub makes this module a stub entry for given module name. +func (m *Module) setStub(name string) { + m.names = make([]byte, 0, len(name)) + m.addName(name) + m.stub = true +} + +// bytesAt recovers a []byte representation of the string at `index` +// received from previous `addName` call. +func (m *Module) bytesAt(index uint32) []byte { + i := int(index) + l := int(m.names[i]) + return m.names[i+1 : i+1+l] +} + +// stringAt recovers the string at `index` received from previous `addName` call. +func (m *Module) stringAt(index uint32) string { + return stringutil.ByteSlice2String(m.bytesAt(index)) +} + +// parseSysfsUint reads a kernel module specific attribute from sysfs. +func parseSysfsUint(mod, knob string) (uint64, error) { + text, err := os.ReadFile(path.Join(sysModule, mod, knob)) + if err != nil { + return 0, err + } + return strconv.ParseUint(strings.Trim(stringutil.ByteSlice2String(text), "\n"), 0, pointerBits) +} + +// loadModuleMetadata is the function to load module bounds and fileID data. +// Overridable for the test suite. Returns true if the metadata was loaded +// successfully. +var loadModuleMetadata = func(m *Module, name string, oldMtime int64) bool { + if name == "bpf" { + // Kernel reports the BPF JIT symbols as part of 'bpf' module. + // There is no metadata available. + return true + } + + // Determine notes location and module size + notesFile := "/sys/kernel/notes" + if name != Kernel { + info, err := os.Stat(path.Join(sysModule, name)) + if err != nil { + return false + } + m.mtime = info.ModTime().UnixMilli() + if m.mtime == oldMtime { + return false + } + + notesFile = path.Join(sysModule, name, "notes/.note.gnu.build-id") + addr, err := parseSysfsUint(name, "sections/.text") + if err != nil { + return false + } + size, err := parseSysfsUint(name, "coresize") + if err != nil { + return false + } + m.start = libpf.Address(addr) + m.end = m.start + libpf.Address(size) + } else { + // No need to reload kernel symbols + if m.mtime == 1 { + return false + } + m.mtime = 1 + } + + // Require at least 16 bytes of BuildID to ensure there is enough entropy for a FileID. + // 16 bytes could happen when --build-id=md5 is passed to `ld`. This would imply a custom + // kernel. + var err error + m.buildID, err = pfelf.GetBuildIDFromNotesFile(notesFile) + if err == nil && len(m.buildID) >= 16 { + m.fileID = libpf.FileIDFromKernelBuildID(m.buildID) + } + return true +} + +// finish will finalize the Module. The 'symbols' slice is sorted, and +// a fallback fileID is synthesized if buildID is not available. +func (m *Module) finish() { + if m.end == 0 || m.end == ^libpf.Address(0) { + // Synthesize the end address at last symbol rounded up to page size + // because it could not be reliably determined. + lastSymbol := m.start + libpf.Address(m.symbols[len(m.symbols)-1].offset) + m.end = (lastSymbol + 4095) & ^libpf.Address(4095) + } + + sort.Slice(m.symbols, func(i, j int) bool { + return m.symbols[i].offset >= m.symbols[j].offset + }) + + // Synthesize fileID if it was not available via /sys + if m.fileID.Compare(libpf.FileID{}) == 0 && len(m.symbols) > 0 { + // Hash exports and their normalized addresses. + h := fnv.New128a() + + h.Write(m.bytesAt(0)) // module name + size := uint64(m.end - m.start) + h.Write(libpf.SliceFrom(&size)) + + for _, sym := range m.symbols { + h.Write(m.bytesAt(sym.index)) + addr := uint64(sym.offset) + h.Write(libpf.SliceFrom(&addr)) + } + + var hash [16]byte + fileID, err := libpf.FileIDFromBytes(h.Sum(hash[:0])) + if err != nil { + panic("kernel module fallback fileID construction is broken") + } + + log.Debugf("Fallback module ID for module %s is '%s' (num syms: %d)", + m.Name(), fileID.Base64(), len(m.symbols)) + } +} + +func (m *Module) Name() string { + return m.stringAt(0) +} + +func (m *Module) Start() libpf.Address { + return m.start +} + +func (m *Module) End() libpf.Address { + return m.end +} + +func (m *Module) BuildID() string { + return m.buildID +} + +func (m *Module) FileID() libpf.FileID { + return m.fileID +} + +// LookupSymbolByAddress resolves the `pc` address to the function and offset from it. +// On error, an empty string with zero offset is returned. +func (m *Module) LookupSymbolByAddress(pc libpf.Address) (funcName string, offset uint, err error) { + if m.stub { + return "", 0, ErrModuleStub + } + pcOffs := uint32(pc - m.start) + symIdx := sort.Search(len(m.symbols), func(i int) bool { + return pcOffs >= m.symbols[i].offset + }) + if symIdx >= len(m.symbols) { + return "", 0, ErrNoSymbol + } + sym := &m.symbols[symIdx] + symName := m.stringAt(sym.index) + return symName, uint(pcOffs - sym.offset), nil +} + +// LookupSymbol finds a symbol with 'name' from the Module. +func (m *Module) LookupSymbol(name string) (libpf.Address, error) { + for _, sym := range m.symbols { + if m.stringAt(sym.index) == name { + return m.start + libpf.Address(sym.offset), nil + } + } + return 0, ErrNoSymbol +} + +// LookupSymbolsByPrefix finds all symbols with the given prefix in from the Module. +func (m *Module) LookupSymbolsByPrefix(prefix string) []*libpf.Symbol { + res := make([]*libpf.Symbol, 0, 8) + for _, sym := range m.symbols { + symName := m.stringAt(sym.index) + if strings.HasPrefix(symName, prefix) { + symAddr := m.start + libpf.Address(sym.offset) + res = append(res, &libpf.Symbol{ + Name: libpf.SymbolName(symName), + Address: libpf.SymbolValue(symAddr), + }) + } + } + return res +} + +// updateSymbolsFrom parses /proc/kallsyms format data from the reader 'r'. +// If possible the data from previous reads is re-used to avoid allocations. +// The Symbolizer internal state is updated only if the input data is parsed +// successfully. +func (s *Symbolizer) updateSymbolsFrom(r io.Reader) error { + var mod *Module + var curName string + var syms []symbol + var names []byte + + noSymbols := true + modules, _ := s.modules.Load().([]Module) + + // The kallsyms symbol order is the following: + // 1. kernel symbols (from compressed kallsyms) + // 2. kernel arch symbols (if any) + // 3. module symbols (grouped by module from all loaded modules) + // 4. module symbols ftrace cloned from __init section + // (all __init symbols ftrace traced during module load) + // 5. bpf module symbols (dynamically generated from JITted bpf programs) + // + // We load the per-module symbols from group #3 in one go. We also generally + // do not care about the symbols in group #4 as they are only the __init + // symbols after they have been freed. Trying to use these symbols is + // problematic: + // 1. the symbol data is normally not present at all + // 2. they are used during init only (getting traces with them is unlikely) + // 3. after the __init data is freed, the same VMA range can be reused for + // another newly loaded module. deciding afterwards if it was the now + // released __init symbol or the newly loaded module code is non-trivial. + // 4. loading these symbols means we would have potentially overlapping symbols. + // + // For the above reasons, it is better to just ignore these ftrace cloned + // __init symbols. This is done with the 'seen' set to avoid loading symbols + // for a module if has been already processed. + seen := make(libpf.Set[string]) + + // Allocate buffers which should be able to hold the symbol data + // from the vmlinux main image (or large modules on reloads) without + // resizing based on normal distribution kernel. These are later + // cloned to the exact size needed, so these are stack allocated. + + // The modules (typical systems have 200-300) + mods := make([]Module, 0, 400) + if len(modules) == 0 { + // - 2.5MB for symbol names + // - 100k symbols + names = make([]byte, 0, 3*1024*1024) + syms = make([]symbol, 0, 128*1024) + } else { + // - 0.5MB for symbol names (e.g. i915 needs 400k) + // - 64k symbols (e.g. i915 has 12k symbols) + names = make([]byte, 0, 512*1024) + syms = make([]symbol, 0, 64*1024) + + // Copy the static symbols here. The kallsyms often starts + // with symbols not within kernel .text, and the logic below + // would not correctly detect already seen kernel symbols. + for _, mod := range modules { + if mod.Name() == Kernel { + mods = append(mods, mod) + seen[Kernel] = libpf.Void{} + break + } + } + } + + for scanner := bufio.NewScanner(r); scanner.Scan(); { + // Avoid heap allocation by not using scanner.Text(). + // NOTE: The underlying bytes will change with the next call to scanner.Scan(), + // so make sure to not keep any references after the end of the loop iteration. + line := stringutil.ByteSlice2String(scanner.Bytes()) + + // Avoid heap allocations here - do not use strings.FieldsN() + var fields [4]string + nFields := stringutil.FieldsN(line, fields[:]) + if nFields < 3 { + return fmt.Errorf("unexpected line in kallsyms: '%s'", line) + } + + // Skip non-text symbols, see 'man nm'. + // Special case for 'etext', which can be of type `D` (data) in some kernels. + if strings.IndexByte("TtVvWw", fields[1][0]) == -1 && fields[2] != "_etext" { + continue + } + + address, err := strconv.ParseUint(fields[0], 16, pointerBits) + if err != nil { + return fmt.Errorf("failed to parse address value: '%s'", fields[0]) + } + if address != 0 { + noSymbols = false + } + + moduleName := Kernel + if fields[3] != "" { + moduleName = fields[3] + if moduleName[0] != '[' && moduleName[len(moduleName)-1] != ']' { + return fmt.Errorf("failed to parse module name: '%s'", moduleName) + } + moduleName = moduleName[1 : len(moduleName)-1] + } + + if curName != moduleName { + if curName == Kernel && noSymbols { + return ErrSymbolPermissions + } + if mod != nil && len(mod.symbols) > 0 { + // Update the working buffers from potentially reallocated + // slices to avoid continuous reallocations. + names = mod.names[0:0] + syms = mod.symbols[0:0] + // Clone a copy of the data to the module so that it does not + // overlap with the working buffer, and is sized exactly the + // needed size. + mod.names = bytes.Clone(mod.names) + mod.symbols = slices.Clone(mod.symbols) + mod.finish() + // Update seen map with the cloned module name string so + // it does not get overwritten later on. + seen[mod.Name()] = libpf.Void{} + } + mod = nil + + if _, ok := seen[moduleName]; !ok { + var oldMod *Module + var oldMtime int64 + newMod := Module{ + end: ^libpf.Address(0), + symbols: syms[0:0], + names: names[0:0], + } + if moduleName != "bpf" { + oldMod, _ = getModuleByAddress(modules, libpf.Address(address)) + if oldMod != nil && !oldMod.stub && oldMod.Name() == moduleName { + oldMtime = oldMod.mtime + } else { + oldMod = nil + } + } + if loadModuleMetadata(&newMod, moduleName, oldMtime) { + // Module metadata was updated. Parse this module symbols. + mods = append(mods, newMod) + mod = &mods[len(mods)-1] + mod.addName(moduleName) + curName = mod.Name() + } else if oldMod != nil { + // Reuse the existing module data if any. + mods = append(mods, *oldMod) + curName = oldMod.Name() + } + } + } + + if mod == nil { + continue + } + + switch fields[2] { + case "_stext", "_text": + if mod.start == 0 { + mod.start = libpf.Address(address) + } + case "_etext": + if mod.end == ^libpf.Address(0) { + mod.end = libpf.Address(address) + } + case "_sinittext", "_einittext": + default: + if mod.start == 0 { + mod.start = libpf.Address(address) + } + if addr := libpf.Address(address); addr >= mod.start && addr < mod.end { + // Add symbol to the module symbols + mod.symbols = append(mod.symbols, symbol{ + offset: uint32(addr - mod.start), + index: mod.addName(fields[2]), + }) + } + } + } + if mod != nil { + mod.finish() + } + if noSymbols { + return ErrSymbolPermissions + } + + sort.Slice(mods, func(i, j int) bool { + return mods[i].start >= mods[j].start + }) + // Heap allocate the exact amount needed. This also makes the initial + // buffer stack allocated. + s.modules.Store(slices.Clone(mods)) + return nil +} + +// loadKallsyms will reload kernel symbols. This function can run concurrently with +// module and symbol lookups. The reload result is visible atomically after success. +func (s *Symbolizer) loadKallsyms() error { + file, err := os.Open("/proc/kallsyms") + if err != nil { + return fmt.Errorf("unable to open kallsyms: %v", err) + } + defer file.Close() + + return s.updateSymbolsFrom(file) +} + +var nonsyfsModules = libpf.Set[string]{ + Kernel: libpf.Void{}, + "bpf": libpf.Void{}, +} + +// loadModules will reload module metadata. +func (s *Symbolizer) loadModules() (bool, error) { + dir, err := os.Open(sysModule) + if err != nil { + return false, err + } + defer dir.Close() + + needReloadSymbols := false + modules, _ := s.modules.Load().([]Module) + mods := make([]Module, 0, 400) + + // Copy the modules not present in sysfs + for _, mod := range modules { + if _, ok := nonsyfsModules[mod.Name()]; ok { + mods = append(mods, mod) + } + } + + // Scan sysfs for current module listing and its metadata + for { + dirEntries, err := dir.ReadDir(64) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return false, err + } + for _, dirEnt := range dirEntries { + if !dirEnt.IsDir() { + continue + } + + moduleName := dirEnt.Name() + curMod := Module{} + if !loadModuleMetadata(&curMod, moduleName, 0) { + // sysfs contains directories also for statically built + // kernel modules. Ignore these. + continue + } + + oldMod, _ := getModuleByAddress(modules, curMod.start) + if oldMod != nil && oldMod.Name() == moduleName && oldMod.mtime == curMod.mtime { + // Reuse the old module + mods = append(mods, *oldMod) + } else { + // Create a stub module without symbols + curMod.setStub(moduleName) + mods = append(mods, curMod) + needReloadSymbols = true + } + } + } + + sort.Slice(mods, func(i, j int) bool { + return mods[i].start >= mods[j].start + }) + // Heap allocate the exact amount needed. This also makes the initial + // buffer stack allocated. + s.modules.Store(slices.Clone(mods)) + + return needReloadSymbols, nil +} + +// reloadWorker is the goroutine handling the reloads of the kallsyms. +func (s *Symbolizer) reloadWorker(ctx context.Context, kobjectClient *kobject.Client) { + noTimeout := make(<-chan time.Time) + nextKallsymsReload := noTimeout + nextModulesReload := noTimeout + for { + select { + case <-s.reloadModules: + // Just trigger reloading of modules with small delay to batch + // potentially multiple module loads. + if nextModulesReload == noTimeout { + nextModulesReload = time.After(100 * time.Millisecond) + } + case <-nextModulesReload: + if reloadSymbols, err := s.loadModules(); err == nil { + log.Debugf("Kernel modules metadata reloaded, new symbols: %v", reloadSymbols) + nextModulesReload = noTimeout + if reloadSymbols && nextKallsymsReload == noTimeout { + nextKallsymsReload = time.After(time.Minute) + } + } else { + log.Warnf("Failed to reload kernel modules metadata: %v", err) + nextModulesReload = time.After(10 * time.Second) + } + case <-nextKallsymsReload: + if err := s.loadKallsyms(); err == nil { + log.Debugf("Kernel symbols reloaded") + nextKallsymsReload = noTimeout + } else { + log.Warnf("Failed to reload kernel symbols: %v", err) + nextKallsymsReload = time.After(time.Minute) + } + case <-ctx.Done(): + // Terminate also the kobject poller thread + _ = kobjectClient.Close() + return + } + } +} + +// pollKobjectClient listens for kernel kobject events to reload kallsyms when needed. +func (s *Symbolizer) pollKobjectClient(kobjectClient *kobject.Client) { + for { + event, err := kobjectClient.Receive() + if err != nil { + return + } + if event.Subsystem == "module" { + log.Debugf("Kernel modules changed") + // Notify worker thread without blocking + select { + case s.reloadModules <- true: + default: + } + } + } +} + +// Reload will trigger asynchronous update of modules and symbols. +func (s *Symbolizer) StartMonitor(ctx context.Context) error { + kobjectClient, err := kobject.New() + if err != nil { + return fmt.Errorf("failed to create kobject netlink socket: %v", err) + } + go s.reloadWorker(ctx, kobjectClient) + go s.pollKobjectClient(kobjectClient) + return nil +} + +// getModuleByAddress is a helper to find a Module from the sorted 'modules' +// slice matching the address 'pc'. +func getModuleByAddress(modules []Module, pc libpf.Address) (*Module, error) { + modIdx := sort.Search(len(modules), func(i int) bool { + return pc >= modules[i].start + }) + if modIdx >= len(modules) { + return nil, ErrNoModule + } + m := &modules[modIdx] + if pc < m.start || pc >= m.end { + return nil, ErrNoModule + } + return m, nil +} + +// GetModuleByAddress finds the Module containing the address 'pc'. +func (s *Symbolizer) GetModuleByAddress(pc libpf.Address) (*Module, error) { + return getModuleByAddress(s.modules.Load().([]Module), pc) +} + +// GetModuleByAddress finds the Module containing the module 'module'. +func (s *Symbolizer) GetModuleByName(module string) (*Module, error) { + modules := s.modules.Load().([]Module) + for i := range modules { + kmod := &modules[i] + if kmod.Name() == module { + return kmod, nil + } + } + return nil, ErrNoModule +} diff --git a/kallsyms/kallsyms_test.go b/kallsyms/kallsyms_test.go new file mode 100644 index 000000000..dda0975fb --- /dev/null +++ b/kallsyms/kallsyms_test.go @@ -0,0 +1,105 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package kallsyms + +import ( + "strings" + "testing" + + "go.opentelemetry.io/ebpf-profiler/libpf" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func assertSymbol(t *testing.T, s *Symbolizer, pc libpf.Address, + eModName, eFuncName string, eOffset uint) { + kmod, err := s.GetModuleByAddress(pc) + if assert.NoError(t, err) && assert.Equal(t, kmod.Name(), eModName) { + funcName, offset, err := kmod.LookupSymbolByAddress(pc) + if assert.NoError(t, err) { + assert.Equal(t, eFuncName, funcName) + assert.Equal(t, eOffset, offset) + } + } +} + +func TestKallSyms(t *testing.T) { + // override the metadata loading to avoid mixing data from running system + loadModuleMetadata = func(_ *Module, _ string, _ int64) bool { return true } + + s := &Symbolizer{} + + err := s.updateSymbolsFrom(strings.NewReader(`0000000000000000 t pvh_start_xen +0000000000000000 T _stext +0000000000000000 T _text +0000000000000000 T startup_64 +0000000000000000 T __pfx___startup_64 +0000000000000000 T _etext`)) + assert.Equal(t, ErrSymbolPermissions, err) + + err = s.updateSymbolsFrom(strings.NewReader(`0000000000000000 A __per_cpu_start +0000000000001000 A cpu_debug_store +0000000000002000 A irq_stack_backing_store +ffffffffb5000000 t pvh_start_xen +ffffffffb5000000 T _stext +ffffffffb5000000 T _text +ffffffffb5000123 T startup_64 +ffffffffb5000180 T __pfx___startup_64 +ffffffffb5000190 T __startup_64 +ffffffffb5000460 T __pfx_startup_64_setup_gdt_idt +ffffffffb5000470 T startup_64_setup_gdt_idt +ffffffffb5001000 T __pfx___traceiter_initcall_level +ffffffffb6000000 T _etext +ffffffffc03cc610 t perf_trace_xfs_attr_list_class [xfs] +ffffffffc03cc770 t perf_trace_xfs_perag_class [xfs] +ffffffffc03cc8b0 t perf_trace_xfs_inodegc_worker [xfs] +ffffffffc03cc9d0 t perf_trace_xfs_fs_class [xfs] +ffffffffc03ccb20 t perf_trace_xfs_inodegc_shrinker_scan [xfs]`)) + require.NoError(t, err) + + _, err = s.GetModuleByName("foo") + assert.Equal(t, err, ErrNoModule) + + _, err = s.GetModuleByAddress(0x1010) + assert.Equal(t, err, ErrNoModule) + + _, err = s.GetModuleByAddress(0xffffffffffff0000) + assert.Equal(t, err, ErrNoModule) + + assertSymbol(t, s, 0xffffffffb5000470, Kernel, "startup_64_setup_gdt_idt", 0) + assertSymbol(t, s, 0xffffffffc03cc610, "xfs", "perf_trace_xfs_attr_list_class", 0) + assertSymbol(t, s, 0xffffffffc03cc610+1, "xfs", "perf_trace_xfs_attr_list_class", 1) + + err = s.updateSymbolsFrom(strings.NewReader(`0000000000000000 A __per_cpu_start +0000000000001000 A cpu_debug_store +0000000000002000 A irq_stack_backing_store +ffffffffb5000000 t pvh_start_xen +ffffffffb5000000 T _stext +ffffffffb5000000 T _text +ffffffffb5000123 T startup_64 +ffffffffb5000180 T __pfx___startup_64 +ffffffffb5000190 T __startup_64 +ffffffffb5000460 T __pfx_startup_64_setup_gdt_idt +ffffffffb5000470 T startup_64_setup_gdt_idt +ffffffffb5001000 T __pfx___traceiter_initcall_level +ffffffffb6000000 T _etext +ffffffffc13cc610 t perf_trace_xfs_attr_list_class [xfs] +ffffffffc13cc770 t perf_trace_xfs_perag_class [xfs] +ffffffffc13cc8b0 t perf_trace_xfs_inodegc_worker [xfs] +ffffffffc13cc9d0 t perf_trace_xfs_fs_class [xfs] +ffffffffc13ccb20 t perf_trace_xfs_inodegc_shrinker_scan [xfs] +ffffffffc1400000 t foo [foo] +ffffffffc13fcb20 t init_xfs_fs [xfs]`)) + require.NoError(t, err) + + _, err = s.GetModuleByAddress(0xffffffffc03cc610 + 1) + assert.Equal(t, ErrNoModule, err) + + _, err = s.GetModuleByAddress(0xffffffffc13fcb20) + assert.Equal(t, ErrNoModule, err) + + assertSymbol(t, s, 0xffffffffb5000470, "vmlinux", "startup_64_setup_gdt_idt", 0) + assertSymbol(t, s, 0xffffffffc13cc610+1, "xfs", "perf_trace_xfs_attr_list_class", 1) +} diff --git a/libpf/cgroupv2.go b/libpf/cgroupv2.go deleted file mode 100644 index aa5d952f4..000000000 --- a/libpf/cgroupv2.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package libpf // import "go.opentelemetry.io/ebpf-profiler/libpf" - -import ( - "bufio" - "fmt" - "os" - "regexp" - - lru "github.com/elastic/go-freelru" - log "github.com/sirupsen/logrus" -) - -var ( - cgroupv2PathPattern = regexp.MustCompile(`0:.*?:(.*)`) -) - -// LookupCgroupv2 returns the cgroupv2 ID for pid. -func LookupCgroupv2(cgrouplru *lru.SyncedLRU[PID, string], pid PID) (string, error) { - id, ok := cgrouplru.Get(pid) - if ok { - return id, nil - } - - // Slow path - f, err := os.Open(fmt.Sprintf("/proc/%d/cgroup", pid)) - if err != nil { - return "", err - } - defer f.Close() - - var genericCgroupv2 string - scanner := bufio.NewScanner(f) - buf := make([]byte, 512) - // Providing a predefined buffer overrides the internal buffer that Scanner uses (4096 bytes). - // We can do that and also set a maximum allocation size on the following call. - // With a maximum of 4096 characters path in the kernel, 8192 should be fine here. We don't - // expect lines in /proc//cgroup to be longer than that. - scanner.Buffer(buf, 8192) - var pathParts []string - for scanner.Scan() { - line := scanner.Text() - pathParts = cgroupv2PathPattern.FindStringSubmatch(line) - if pathParts == nil { - log.Debugf("Could not extract cgroupv2 path from line: %s", line) - continue - } - genericCgroupv2 = pathParts[1] - break - } - - // Cache the cgroupv2 information. - // To avoid busy lookups, also empty cgroupv2 information is cached. - cgrouplru.Add(pid, genericCgroupv2) - - return genericCgroupv2, nil -} diff --git a/libpf/fileid.go b/libpf/fileid.go index f58a07a92..19bd15272 100644 --- a/libpf/fileid.go +++ b/libpf/fileid.go @@ -34,6 +34,11 @@ func NewFileID(hi, lo uint64) FileID { return FileID{basehash.New128(hi, lo)} } +// NewStubFileID returns a FrameType specific stub FileID. +func NewStubFileID(typ FrameType) FileID { + return FileID{basehash.New128(0x578b, uint64(0x1d00|typ))} +} + // FileIDFromBytes parses a byte slice into the internal data representation for a file ID. func FileIDFromBytes(b []byte) (FileID, error) { // We need to check for nil since byte slice fields in protobuf messages can be optional. diff --git a/libpf/frametype.go b/libpf/frametype.go index 30ec130d0..5da613078 100644 --- a/libpf/frametype.go +++ b/libpf/frametype.go @@ -49,6 +49,8 @@ const ( V8Frame FrameType = support.FrameMarkerV8 // DotnetFrame identifies the Dotnet interpreter frames. DotnetFrame FrameType = support.FrameMarkerDotnet + // GoFrame identifies Go frames. + GoFrame FrameType = support.FrameMarkerGo // AbortFrame identifies frames that report that further unwinding was aborted due to an error. AbortFrame FrameType = support.FrameMarkerAbort ) diff --git a/libpf/interpretertype.go b/libpf/interpretertype.go index d6be81372..86b22feb6 100644 --- a/libpf/interpretertype.go +++ b/libpf/interpretertype.go @@ -31,6 +31,8 @@ const ( V8 InterpreterType = support.FrameMarkerV8 // Dotnet identifies the Dotnet interpreter. Dotnet InterpreterType = support.FrameMarkerDotnet + // Go identifies Go code. + Go InterpreterType = support.FrameMarkerGo ) // Pseudo-interpreters without a corresponding frame type. @@ -40,6 +42,9 @@ const ( // APMInt identifies the pseudo-interpreter for the APM integration. APMInt InterpreterType = 0x100 + + // Go identifies the pseudo-interpreter for Go custom labels support. + GoLabels InterpreterType = 0x101 ) // Frame converts the interpreter type into the corresponding frame type. @@ -65,6 +70,7 @@ var interpreterTypeToString = map[InterpreterType]string{ V8: "v8js", Dotnet: "dotnet", APMInt: "apm-integration", + Go: "go", } var stringToInterpreterType = make(map[string]InterpreterType, len(interpreterTypeToString)) diff --git a/libpf/pfelf/file.go b/libpf/pfelf/file.go index 75d8e4c6e..b75c60429 100644 --- a/libpf/pfelf/file.go +++ b/libpf/pfelf/file.go @@ -27,7 +27,6 @@ import ( "fmt" "hash/crc32" "io" - "os" "path/filepath" "runtime/debug" "sort" @@ -35,7 +34,7 @@ import ( "unsafe" "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/libpf/readatbuf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf/internal/mmap" "go.opentelemetry.io/ebpf-profiler/remotememory" ) @@ -50,11 +49,14 @@ const ( maxBytesLargeSection = 16 * 1024 * 1024 ) -// ErrSymbolNotFound is returned when requested symbol was not found -var ErrSymbolNotFound = errors.New("symbol not found") +// List of public errors. +var ( + // ErrSymbolNotFound is returned when the requested symbol was not found. + ErrSymbolNotFound = errors.New("symbol not found") -// ErrNotELF is returned when the file is not an ELF -var ErrNotELF = errors.New("not an ELF file") + // ErrNotELF is returned when the file is not an ELF file. + ErrNotELF = errors.New("not an ELF file") +) // File represents an open ELF file type File struct { @@ -67,6 +69,9 @@ type File struct { // ehFrame is a pointer to the PT_GNU_EH_FRAME segment of the ELF ehFrame *Prog + // loadData is a slice of pointers to the PT_LOAD data segments of the ELF. + loadData []*Prog + // ROData is a slice of pointers to the read-only data segments of the ELF // These are sorted so that segments marked as "read" appear before those // marked as "read-execute" @@ -127,6 +132,9 @@ type File struct { } var _ libpf.SymbolFinder = &File{} +var _ io.ReaderAt = &File{} +var _ io.ReaderAt = &Section{} +var _ io.ReaderAt = &Prog{} // sysvHashHeader is the ELF DT_HASH section header type sysvHashHeader struct { @@ -154,31 +162,20 @@ type Prog struct { type Section struct { elf.SectionHeader - // Embed ReaderAt for ReadAt method. - io.ReaderAt - - // Do not embed SectionReader directly, or as public member. We can't - // return the same copy to multiple callers, otherwise they corrupt - // each other's reader file position. - sr *io.SectionReader + // elfReader is the same ReadAt as used for the File + elfReader io.ReaderAt } // Open opens the named file using os.Open and prepares it for use as an ELF binary. func Open(name string) (*File, error) { - f, err := os.Open(name) + f, err := mmap.Open(name) if err != nil { return nil, err } - // Wrap it in a cacher as we often do short reads - buffered, err := readatbuf.New(f, 1024, 4) + ff, err := newFile(f, f, 0, false) if err != nil { - return nil, err - } - - ff, err := newFile(buffered, f, 0, false) - if err != nil { - f.Close() + _ = f.Close() return nil, err } return ff, nil @@ -190,7 +187,7 @@ func (f *File) Close() (err error) { err = f.closer.Close() f.closer = nil } - return + return err } // NewFile creates a new ELF file object that borrows the given reader. @@ -198,7 +195,8 @@ func NewFile(r io.ReaderAt, loadAddress uint64, hasMusl bool) (*File, error) { return newFile(r, nil, loadAddress, hasMusl) } -func newFile(r io.ReaderAt, closer io.Closer, loadAddress uint64, hasMusl bool) (*File, error) { +func newFile(r io.ReaderAt, closer io.Closer, + loadAddress uint64, hasMusl bool) (*File, error) { f := &File{ elfReader: r, InsideCore: loadAddress != 0, @@ -236,6 +234,8 @@ func newFile(r io.ReaderAt, closer io.Closer, loadAddress uint64, hasMusl bool) f.Progs = make([]Prog, hdr.Phnum) virtualBase := ^uint64(0) + numROData := 0 + numLoad := 0 for i, ph := range progs { p := &f.Progs[i] p.ProgHeader = elf.ProgHeader{ @@ -254,12 +254,24 @@ func newFile(r io.ReaderAt, closer io.Closer, loadAddress uint64, hasMusl bool) if p.Vaddr < virtualBase { virtualBase = p.Vaddr } - andFlags := p.Flags & (elf.PF_R | elf.PF_W | elf.PF_X) - if andFlags == elf.PF_R || andFlags == (elf.PF_R|elf.PF_X) { + if p.isRoData() { + numROData++ + } + numLoad++ + } + } + f.loadData = make([]*Prog, 0, numLoad) + f.ROData = make([]*Prog, 0, numROData) + for i := range progs { + p := &f.Progs[i] + if p.Type == elf.PT_LOAD { + f.loadData = append(f.loadData, p) + if p.isRoData() { f.ROData = append(f.ROData, p) } } } + if loadAddress != 0 { // Calculate the bias for coredump files f.bias = libpf.Address(loadAddress - virtualBase) @@ -335,6 +347,25 @@ func getString(section []byte, start int) (string, bool) { return string(section[start : start+slen]), true } +// NoMmapCloser is a no-op io.Closer which is returned from Take() when +// the File is not memory mapped. +type NoMmapCloser libpf.Void + +// Close implements io.Closer interface. +func (_ NoMmapCloser) Close() error { + return nil +} + +// Take takes a reference on the backing mmapped data. This allows callers to +// keep slices returned by Section.Data() and Prog.Data() after File has been +// GCd. The returned Close() will release the reference on data. +func (f *File) Take() io.Closer { + if mapping, ok := f.elfReader.(*mmap.ReaderAt); ok { + return mapping.Take() + } + return NoMmapCloser{} +} + // LoadSections loads the ELF file sections func (f *File) LoadSections() error { if f.InsideCore { @@ -379,16 +410,11 @@ func (f *File) LoadSections() error { Entsize: sh.Entsize, FileSize: sh.Size, } - s.sr = io.NewSectionReader(f.elfReader, int64(s.Offset), int64(s.FileSize)) - s.ReaderAt = s.sr + s.elfReader = f.elfReader } // Load the section name string table strsh := f.Sections[hdr.Shstrndx] - if strsh.FileSize >= 1024*1024 { - return fmt.Errorf("section headers string table too large (%d)", - strsh.FileSize) - } strtab, err := strsh.Data(maxBytesLargeSection) if err != nil { return err @@ -424,20 +450,67 @@ func (f *File) Section(name string) *Section { return nil } +// findVirtualAddressProg determines the Prog header containing the virtual address. +func (f *File) findVirtualAddressProg(addr uint64) *Prog { + // Search for the Program header that contains the start address. + for _, ph := range f.loadData { + if addr >= ph.Vaddr && addr < ph.Vaddr+ph.Memsz { + return ph + } + } + return nil +} + +// VirtualMemory returns a slice for the request data at a virtual address. +// The slice may point to mmapped data or be a newly allocated slice. +// The maxSize is the limit for allocating the memory from heap. +func (f *File) VirtualMemory(addr int64, sz, maxSize int) ([]byte, error) { + if sz == 0 { + return nil, nil + } + if ph := f.findVirtualAddressProg(uint64(addr)); ph != nil { + offset := addr - int64(ph.Vaddr) + if offset+int64(sz) <= int64(ph.Filesz) { + if mapping, ok := ph.elfReader.(*mmap.ReaderAt); ok { + return mapping.Subslice(int(ph.Off)+int(offset), sz) + } + } + if sz > maxSize { + return nil, fmt.Errorf("virtual memory area too large (%d) to copy", sz) + } + buf := make([]byte, sz) + n, err := ph.ReadAt(buf, offset) + return buf[:n], err + } + return nil, fmt.Errorf("no matching segment for 0x%x", uint64(addr)) +} + +// SymbolData returns the data associated with given dynamic symbol. +// The backing mmapped data is returned if possible, otherwise a maximum of +// maxCopy bytes of the symbol data will read to newly allocated buffer. +func (f *File) SymbolData(name libpf.SymbolName, maxCopy int) (*libpf.Symbol, []byte, error) { + sym, err := f.LookupSymbol(name) + if err != nil { + return nil, nil, err + } + symSize := int(sym.Size) + if symSize > maxCopy { + // Truncate read size if not memory mapped data. + if _, ok := f.elfReader.(*mmap.ReaderAt); !ok { + symSize = maxCopy + } + } + data, err := f.VirtualMemory(int64(sym.Address), symSize, maxCopy) + return sym, data, err +} + // ReadVirtualMemory reads bytes from given virtual address func (f *File) ReadVirtualMemory(p []byte, addr int64) (int, error) { if len(p) == 0 { return 0, nil } - for _, ph := range f.Progs { - // Search for the Program header that contains the start address. - // ReadVirtualMemory() supports ReadAt() style indication of reading - // less bytes then requested, so addr+len(p) can be an address beyond - // the segment and ReadAt() will give short read. - if ph.Type == elf.PT_LOAD && uint64(addr) >= ph.Vaddr && - uint64(addr) < ph.Vaddr+ph.Memsz { - return ph.ReadAt(p, addr-int64(ph.Vaddr)) - } + if ph := f.findVirtualAddressProg(uint64(addr)); ph != nil { + return ph.ReadAt(p, addr-int64(ph.Vaddr)) } return 0, fmt.Errorf("no matching segment for 0x%x", uint64(addr)) } @@ -514,6 +587,21 @@ func (f *File) GoVersion() (string, error) { return bi.GoVersion, nil } +func (f *File) IsCgoEnabled() (bool, error) { + _, err := f.GoVersion() + if err != nil { + return false, err + } + for _, kv := range f.goBuildInfo.Settings { + if kv.Key == "CGO_ENABLED" { + return kv.Value == "1", nil + } + } + // On some platforms GCO_ENABLED might be missing b/c they don't support + // CGO at all. + return false, nil +} + // DebuglinkFileName returns the debug file linked by .gnu_debuglink if any func (f *File) DebuglinkFileName(elfFilePath string, elfOpener ELFOpener) string { if f.debuglinkChecked { @@ -521,7 +609,7 @@ func (f *File) DebuglinkFileName(elfFilePath string, elfOpener ELFOpener) string } file, path := f.OpenDebugLink(elfFilePath, elfOpener) if file != nil { - file.Close() + _ = file.Close() } return path } @@ -651,7 +739,7 @@ func (f *File) OpenDebugLink(elfFilePath string, elfOpener ELFOpener) ( } fileCRC32, err := debugELF.CRC32() if err != nil || fileCRC32 != linkCRC32 { - debugELF.Close() + _ = debugELF.Close() continue } f.debuglinkPath = debugFile @@ -670,6 +758,15 @@ func (f *File) CRC32() (int32, error) { return int32(h.Sum32()), nil } +// isRoData determine if this program header is read-only data. +func (ph *Prog) isRoData() bool { + if ph.Type != elf.PT_LOAD { + return false + } + andFlags := ph.Flags & (elf.PF_R | elf.PF_W | elf.PF_X) + return andFlags == elf.PF_R || andFlags == (elf.PF_R|elf.PF_X) +} + // ReadAt implements the io.ReaderAt interface func (ph *Prog) ReadAt(p []byte, off int64) (n int, err error) { // First load as much as possible from the disk @@ -712,6 +809,11 @@ func (ph *Prog) Open() io.ReadSeeker { // Data loads the whole program header referenced data, and returns it as slice. func (ph *Prog) Data(maxSize uint) ([]byte, error) { + if mapping, ok := ph.elfReader.(*mmap.ReaderAt); ok { + return mapping.Subslice(int(ph.Off), int(ph.Filesz)) + } + + // Fallback option if the file is not mmaped. if ph.Filesz > uint64(maxSize) { return nil, fmt.Errorf("segment size %d is too large", ph.Filesz) } @@ -729,11 +831,34 @@ func (ph *Prog) DataReader(maxSize uint) (io.Reader, error) { return bytes.NewReader(p), nil } +// ReadAt implements the io.ReaderAt interface +func (sh *Section) ReadAt(p []byte, off int64) (n int, err error) { + if off < 0 || uint64(off) >= sh.FileSize { + return 0, io.EOF + } + truncated := false + if uint64(off)+uint64(len(p)) > sh.FileSize { + p = p[:sh.FileSize-uint64(off)] + truncated = true + } + n, err = sh.elfReader.ReadAt(p, off+int64(sh.Offset)) + if err == nil && truncated { + err = io.EOF + } + return n, err +} + // Data loads the whole section header referenced data, and returns it as a slice. func (sh *Section) Data(maxSize uint) ([]byte, error) { if sh.Flags&elf.SHF_COMPRESSED != 0 { return nil, errors.New("compressed sections not supported") } + + if mapping, ok := sh.elfReader.(*mmap.ReaderAt); ok { + return mapping.Subslice(int(sh.Offset), int(sh.FileSize)) + } + + // Fallback option if the file is not mmaped. if sh.FileSize > uint64(maxSize) { return nil, fmt.Errorf("section size %d is too large", sh.FileSize) } @@ -765,14 +890,14 @@ func (f *File) readAndMatchSymbol(n uint32, name libpf.SymbolName) (libpf.Symbol f.symbolsAddr+int64(n)*symSz); err != nil { return libpf.Symbol{}, false } - slen := len(name) + 1 - sname := make([]byte, slen) - if _, err := f.ReadVirtualMemory(sname, f.stringsAddr+int64(sym.Name)); err != nil { + slen := len(name) + sname, err := f.VirtualMemory(f.stringsAddr+int64(sym.Name), slen+1, maxBytesSmallSection) + if err != nil { return libpf.Symbol{}, false } // Verify that name matches - if sname[slen-1] != 0 || libpf.SymbolName(sname[:slen-1]) != name { + if sname[slen] != 0 || unsafe.String(unsafe.SliceData(sname), slen) != string(name) { return libpf.Symbol{}, false } @@ -908,43 +1033,48 @@ func (f *File) LookupSymbolAddress(symbol libpf.SymbolName) (libpf.SymbolValue, return s.Address, nil } -// loadSymbolTable reads given symbol table -func (f *File) loadSymbolTable(name string) (*libpf.SymbolMap, error) { +// visitSymbolTable visits all symbols in the given symbol table. +func (f *File) visitSymbolTable(name string, visitor func(libpf.Symbol)) error { symTab := f.Section(name) if symTab == nil { - return nil, fmt.Errorf("failed to read %v: section not present", name) + return fmt.Errorf("failed to read %v: section not present", name) } if symTab.Link >= uint32(len(f.Sections)) { - return nil, fmt.Errorf("failed to read %v strtab: link %v out of range", + return fmt.Errorf("failed to read %v strtab: link %v out of range", name, symTab.Link) } strTab := f.Sections[symTab.Link] strs, err := strTab.Data(maxBytesLargeSection) if err != nil { - return nil, fmt.Errorf("failed to read %v: %v", strTab.Name, err) + return fmt.Errorf("failed to read %v: %v", strTab.Name, err) } syms, err := symTab.Data(maxBytesLargeSection) if err != nil { - return nil, fmt.Errorf("failed to read %v: %v", name, err) + return fmt.Errorf("failed to read %v: %v", name, err) } - symMap := libpf.SymbolMap{} symSz := int(unsafe.Sizeof(elf.Sym64{})) for i := 0; i < len(syms); i += symSz { sym := (*elf.Sym64)(unsafe.Pointer(&syms[i])) - name, ok := getString(strs, int(sym.Name)) - if !ok { - continue + if name, ok := getString(strs, int(sym.Name)); ok { + visitor(libpf.Symbol{ + Name: libpf.SymbolName(name), + Address: libpf.SymbolValue(sym.Value), + Size: sym.Size, + }) } - symMap.Add(libpf.Symbol{ - Name: libpf.SymbolName(name), - Address: libpf.SymbolValue(sym.Value), - Size: sym.Size, - }) } - symMap.Finalize() + return nil +} - return &symMap, nil +// loadSymbolTable reads given symbol table +func (f *File) loadSymbolTable(name string) (*libpf.SymbolMap, error) { + symMap := &libpf.SymbolMap{} + if err := f.visitSymbolTable(name, func(s libpf.Symbol) { symMap.Add(s) }); err != nil { + return nil, err + } + symMap.Finalize() + return symMap, nil } // ReadSymbols reads the full dynamic symbol table from the ELF @@ -957,6 +1087,11 @@ func (f *File) ReadDynamicSymbols() (*libpf.SymbolMap, error) { return f.loadSymbolTable(".dynsym") } +// VisitDynamicSymbols iterates through the dynamic symbol table +func (f *File) VisitDynamicSymbols(visitor func(libpf.Symbol)) error { + return f.visitSymbolTable(".dynsym", visitor) +} + // DynString returns the strings listed for the given tag in the file's dynamic // program header. func (f *File) DynString(tag elf.DynTag) ([]string, error) { diff --git a/libpf/pfelf/internal/mmap/mmap.go b/libpf/pfelf/internal/mmap/mmap.go new file mode 100644 index 000000000..0c69c5759 --- /dev/null +++ b/libpf/pfelf/internal/mmap/mmap.go @@ -0,0 +1,124 @@ +// Package mmap is inspired by golang.org/x/exp/mmap with +// additional functionality. +package mmap // import "go.opentelemetry.io/ebpf-profiler/libpf/pfelf/internal/mmap" + +import ( + "errors" + "fmt" + "io" + "os" + "runtime" + "sync/atomic" + "syscall" + "unsafe" +) + +// ReaderAt reads a memory-mapped file. +// +// Like any io.ReaderAt, clients can execute parallel ReadAt calls, but it is +// not safe to call Close and reading methods concurrently. +type ReaderAt struct { + // refCount is the number of references + refCount atomic.Int32 + + data []byte +} + +// Take takes a reference on the data +func (r *ReaderAt) Take() io.Closer { + r.refCount.Add(1) + return r +} + +// Close closes the reader. +func (r *ReaderAt) Close() error { + // Drop reference + if r.refCount.Add(-1) > 0 { + return nil + } + // No more references - unmap data + if r.data == nil { + return nil + } else if len(r.data) == 0 { + r.data = nil + return nil + } + data := r.data + r.data = nil + runtime.SetFinalizer(r, nil) + return syscall.Munmap(data) +} + +// Len returns the length of the underlying memory-mapped file. +func (r *ReaderAt) Len() int { + return len(r.data) +} + +// At returns the byte at index i. +func (r *ReaderAt) At(i int) byte { + return r.data[i] +} + +// ReadAt implements the io.ReaderAt interface. +func (r *ReaderAt) ReadAt(p []byte, off int64) (int, error) { + if r.data == nil { + return 0, errors.New("mmap: closed") + } + if off < 0 || int64(len(r.data)) < off { + return 0, fmt.Errorf("mmap: invalid ReadAt offset %d", off) + } + n := copy(p, r.data[off:]) + if n < len(p) { + return n, io.EOF + } + return n, nil +} + +// Subslice returns a subset of the mmaped backed data. +func (r *ReaderAt) Subslice(offset, length int) ([]byte, error) { + if offset+length > r.Len() { + return nil, fmt.Errorf("requested data %x-%x exceeds %x: %w", + offset, offset+length, r.Len(), io.EOF) + } + return unsafe.Slice((*byte)(unsafe.Pointer(&r.data[offset])), length), nil +} + +// Open memory-maps the named file for reading. +func Open(filename string) (*ReaderAt, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + fi, err := f.Stat() + if err != nil { + return nil, err + } + + size := fi.Size() + if size == 0 { + // Treat (size == 0) as a special case, avoiding the syscall, since + // "man 2 mmap" says "the length... must be greater than 0". + // + // As we do not call syscall.Mmap, there is no need to call + // runtime.SetFinalizer to enforce a balancing syscall.Munmap. + return &ReaderAt{ + data: make([]byte, 0), + }, nil + } + if size < 0 { + return nil, fmt.Errorf("mmap: file %q has negative size", filename) + } + if size != int64(int(size)) { + return nil, fmt.Errorf("mmap: file %q is too large", filename) + } + + data, err := syscall.Mmap(int(f.Fd()), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED) + if err != nil { + return nil, err + } + r := &ReaderAt{data: data} + r.refCount.Store(1) + runtime.SetFinalizer(r, (*ReaderAt).Close) + return r, nil +} diff --git a/libpf/pfelf/internal/mmap/mmap_test.go b/libpf/pfelf/internal/mmap/mmap_test.go new file mode 100644 index 000000000..dccc95dde --- /dev/null +++ b/libpf/pfelf/internal/mmap/mmap_test.go @@ -0,0 +1,42 @@ +package mmap_test + +import ( + "fmt" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf/internal/mmap" +) + +func TestMmap_Subslice(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), t.Name()+".testfile") + require.NoError(t, err) + defer os.Remove(f.Name()) + + // Write some testData into the file. + testData := []byte("data-for-the-test") + fmt.Fprintf(f, "%s", testData) + + mf, err := mmap.Open(f.Name()) + require.NoError(t, err) + defer mf.Close() + + t.Run("invalid subslice", func(t *testing.T) { + // Try to access data out of scope from the data + // in the backing file. + _, err := mf.Subslice(1024, 1024) + assert.ErrorIs(t, err, io.EOF) + }) + + t.Run("valid subslice", func(t *testing.T) { + // Try to access data out within the scope of + // len(testData). + res, err := mf.Subslice(9, 8) + if assert.NoError(t, err) { + assert.Equal(t, res, testData[9:]) + } + }) +} diff --git a/libpf/pfelf/pfelf_test.go b/libpf/pfelf/pfelf_test.go index 4d5b4231c..1e9addc57 100644 --- a/libpf/pfelf/pfelf_test.go +++ b/libpf/pfelf/pfelf_test.go @@ -69,7 +69,7 @@ func TestGetBuildIDError(t *testing.T) { buildID, err := pfelf.GetBuildID(elfFile) if assert.ErrorIs(t, pfelf.ErrNoBuildID, err) { - assert.Equal(t, "", buildID) + assert.Empty(t, buildID) } } @@ -83,7 +83,7 @@ func TestGetDebugLinkError(t *testing.T) { debugLink, _, err := pfelf.GetDebugLink(elfFile) if assert.ErrorIs(t, pfelf.ErrNoDebugLink, err) { - assert.Equal(t, "", debugLink) + assert.Empty(t, debugLink) } } diff --git a/libpf/pfelf/reference.go b/libpf/pfelf/reference.go index 5a40f2fbd..852aacd2a 100644 --- a/libpf/pfelf/reference.go +++ b/libpf/pfelf/reference.go @@ -41,7 +41,7 @@ func (ref *Reference) GetELF() (*File, error) { // Close closes the File if it has been opened earlier. func (ref *Reference) Close() { if ref.elfFile != nil { - ref.elfFile.Close() + _ = ref.elfFile.Close() ref.elfFile = nil } } diff --git a/libpf/pid.go b/libpf/pid.go index 168a2d161..4752e6aba 100644 --- a/libpf/pid.go +++ b/libpf/pid.go @@ -1,8 +1,27 @@ package libpf // import "go.opentelemetry.io/ebpf-profiler/libpf" +import ( + "fmt" +) + // PID represent Unix Process ID (pid_t) type PID uint32 func (p PID) Hash32() uint32 { return uint32(p) } + +// PIDTID encodes a process id and a thread id +type PIDTID uint64 + +func (pt PIDTID) PID() PID { + return PID(pt >> 32) +} + +func (pt PIDTID) TID() PID { + return PID(pt & 0xFFFFFFFF) +} + +func (pt PIDTID) String() string { + return fmt.Sprintf("PID: %v TID: %v", pt.PID(), pt.TID()) +} diff --git a/libpf/trace.go b/libpf/trace.go index e18d454c8..dc997eb56 100644 --- a/libpf/trace.go +++ b/libpf/trace.go @@ -14,6 +14,7 @@ type Trace struct { MappingEnd []Address MappingFileOffsets []uint64 Hash TraceHash + CustomLabels map[string]string } // AppendFrame appends a frame to the columnar frame array without mapping information. diff --git a/maccess/maccess_amd64.go b/maccess/maccess_amd64.go index c701c8a4f..2787b1b91 100644 --- a/maccess/maccess_amd64.go +++ b/maccess/maccess_amd64.go @@ -18,6 +18,9 @@ func CopyFromUserNoFaultIsPatched(codeblob []byte, if len(codeblob) == 0 { return false, errors.New("empty code blob") } + if newCheckFuncAddr == 0 { + return false, errors.New("nmi_uaccess_okay function not found") + } for i := 0; i < len(codeblob); { idx, offset := getRelativeOffset(codeblob[i:]) diff --git a/main.go b/main.go index ddc387858..089d4d3ba 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,6 @@ import ( "golang.org/x/sys/unix" "go.opentelemetry.io/ebpf-profiler/internal/controller" - "go.opentelemetry.io/ebpf-profiler/internal/helpers" "go.opentelemetry.io/ebpf-profiler/reporter" "go.opentelemetry.io/ebpf-profiler/times" "go.opentelemetry.io/ebpf-profiler/vc" @@ -102,20 +101,9 @@ func mainWithExitCode() exitCode { intervals := times.New(cfg.ReporterInterval, cfg.MonitorInterval, cfg.ProbabilisticInterval) - kernelVersion, err := helpers.GetKernelVersion() - if err != nil { - log.Error(err) - return exitFailure - } - - // hostname and sourceIP will be populated from the root namespace. - hostname, sourceIP, err := helpers.GetHostnameAndSourceIP(cfg.CollAgentAddr) - if err != nil { - log.Error(err) - return exitFailure - } - rep, err := reporter.NewOTLP(&reporter.Config{ + Name: os.Args[0], + Version: vc.Version(), CollAgentAddr: cfg.CollAgentAddr, DisableTLS: cfg.DisableTLS, MaxRPCMsgSize: 32 << 20, // 32 MiB @@ -126,12 +114,8 @@ func mainWithExitCode() exitCode { ReportInterval: intervals.ReportInterval(), ExecutablesCacheElements: 16384, // Next step: Calculate FramesCacheElements from numCores and samplingRate. - FramesCacheElements: 65536, - CGroupCacheElements: 1024, + FramesCacheElements: 131072, SamplesPerSecond: cfg.SamplesPerSecond, - KernelVersion: kernelVersion, - HostName: hostname, - IPAddress: sourceIP, }) if err != nil { log.Error(err) diff --git a/metrics/ids.go b/metrics/ids.go index 86b80960b..cafa022a9 100644 --- a/metrics/ids.go +++ b/metrics/ids.go @@ -635,6 +635,18 @@ const ( // Number of parsing errors seen during processing /proc//maps IDErrProcParse = 275 + // Number of successfully symbolized Go frames + IDGoSymbolizationSuccess = 276 + + // Number of Go frames that failed symbolization + IDGoSymbolizationFailure = 277 + + // Number of attempts to read Go custom labels + IDUnwindGoLabelsAttempts = 278 + + // Number of failures reading Go custom labels + IDUnwindGoLabelsFailures = 279 + // max number of ID values, keep this as *last entry* - IDMax = 276 + IDMax = 280 ) diff --git a/metrics/metrics.go b/metrics/metrics.go index 1ec2c1b46..ec4776cbf 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -83,10 +83,11 @@ func init() { } } -// report converts and reports collected metrics via OTel metrics -func report() { +// report converts and reports collected metrics via OTel metrics. +// Allow for report to be overridden in the test. +var report = func() { ctx := context.Background() - for i := 0; i < nMetrics; i++ { + for i := range nMetrics { metric := metricsBuffer[i] switch typ := metricTypes[metric.ID]; typ { case MetricTypeCounter: diff --git a/metrics/metrics.json b/metrics/metrics.json index 5ebc749e5..c5534b760 100644 --- a/metrics/metrics.json +++ b/metrics/metrics.json @@ -1987,5 +1987,33 @@ "name": "ErrProcParse", "field": "agent.errors.proc_parse", "id": 275 + }, + { + "description": "Number of successfully symbolized Go frames", + "type": "counter", + "name": "GoSymbolizationSuccess", + "field": "agent.go.symbolization.successes", + "id": 276 + }, + { + "description": "Number of Go frames that failed symbolization", + "type": "counter", + "name": "GoSymbolizationFailure", + "field": "agent.go.symbolization.failures", + "id": 277 + }, + { + "description": "Number of attempts to read Go custom labels", + "type": "counter", + "name": "UnwindGoLabelsAttempts", + "field": "bpf.golabels.attempts", + "id": 278 + }, + { + "description": "Number of failures reading Go custom labels", + "type": "counter", + "name": "UnwindGoLabelsFailures", + "field": "bpf.golabels.errors", + "id": 279 } ] diff --git a/metrics/metrics_test.go b/metrics/metrics_test.go index b8be5a225..dde648c53 100644 --- a/metrics/metrics_test.go +++ b/metrics/metrics_test.go @@ -11,6 +11,9 @@ import ( // TestMetrics func TestMetrics(t *testing.T) { + // NOP report to avoid timing-based interference. + report = func() {} + inputMetrics := []Metric{ {IDELFInfoCacheHit, MetricValue(33)}, {IDELFInfoCacheMiss, MetricValue(55)}, diff --git a/nativeunwind/elfunwindinfo/elfehframe.go b/nativeunwind/elfunwindinfo/elfehframe.go index a80be83f1..c5378aea4 100644 --- a/nativeunwind/elfunwindinfo/elfehframe.go +++ b/nativeunwind/elfunwindinfo/elfehframe.go @@ -17,6 +17,7 @@ import ( "go.opentelemetry.io/ebpf-profiler/libpf/hash" "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" + "go.opentelemetry.io/ebpf-profiler/support" ) const ( @@ -37,8 +38,10 @@ var errEmptyEntry = errors.New("FDE/CIE empty") // ehframeHooks interface provides hooks for filtering and debugging eh_frame parsing type ehframeHooks interface { + // fdeUnsorted is called if FDE entries from unsorted area are found. + fdeUnsorted() // fdeHook is called for each FDE. Returns false if the FDE should be filtered out. - fdeHook(cie *cieInfo, fde *fdeInfo) bool + fdeHook(cie *cieInfo, fde *fdeInfo, deltas *sdtypes.StackDeltaArray) bool // deltaHook is called for each stack delta found deltaHook(ip uintptr, regs *vmRegs, delta sdtypes.StackDelta) // golangHook is called if .gopclntab is found to report its coverage @@ -95,7 +98,6 @@ const ( // The subset needed for normal .eh_frame handling type expressionOpcode uint8 -//nolint:deadcode,varcheck const ( opDeref expressionOpcode = 0x06 opConstU expressionOpcode = 0x10 @@ -122,7 +124,6 @@ type dwarfExpression struct { // https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/dwarfext.html type encoding uint8 -//nolint:deadcode,varcheck const ( encFormatNative encoding = 0x00 encFormatLeb128 encoding = 0x01 @@ -367,11 +368,9 @@ type cieInfo struct { // fdeInfo contains one Frame Description Entry (FDE) type fdeInfo struct { - len uint64 ciePos uint64 ipLen uintptr ipStart uintptr - sorted bool } const ( @@ -856,7 +855,7 @@ func (regs *vmRegs) getUnwindInfo(allowGenericRegisters bool) sdtypes.UnwindInfo default: panic(fmt.Sprintf("architecture %d is not supported", regs.arch)) } - if !allowGenericRegisters && info.Opcode == sdtypes.UnwindOpcodeBaseReg { + if !allowGenericRegisters && info.Opcode == support.UnwindOpcodeBaseReg { return sdtypes.UnwindInfoInvalid } return info @@ -884,29 +883,25 @@ func isSignalTrampoline(efCode *pfelf.File, fde *fdeInfo) bool { if fde.ipLen != uintptr(len(sigretCode)) { return false } - fdeCode := make([]byte, len(sigretCode)) - if _, err := efCode.ReadVirtualMemory(fdeCode, int64(fde.ipStart)); err != nil { + fdeCode, err := efCode.VirtualMemory(int64(fde.ipStart), len(sigretCode), 64) + if err != nil { return false } return bytes.Equal(fdeCode, sigretCode) } -// parseFDE reads and processes one Frame Description Entry and returns the size of -// the CIE/FDE entry, and amends the intervals to deltas table. -// The FDE format is described in: -// http://dwarfstd.org/doc/DWARF5.pdf §6.4.1 -// https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html -func (ee *elfExtractor) parseFDE(r *reader, ef *pfelf.File, ipStart uintptr, - cieCache *lru.LRU[uint64, *cieInfo], sorted bool) (size uintptr, err error) { +// parses first fields of FDE, specifically PC Begin, PC Range +func parsesFDEHeader(r *reader, efm elf.Machine, ipStart uintptr, + cieCache *lru.LRU[uint64, *cieInfo]) (fdeLen uint64, fde fdeInfo, info *cieInfo, err error) { // Parse FDE header fdeID := r.pos - fde := fdeInfo{sorted: sorted} - fde.len, fde.ciePos, err = r.parseHDR(false) + fde = fdeInfo{} + fdeLen, fde.ciePos, err = r.parseHDR(false) if err != nil { // parseHDR returns unconditionally the CIE/FDE entry length. // Also return the size here. This is to allow walkFDEs to use // this function and skip CIEs. - return uintptr(fde.len), err + return fdeLen, fde, nil, err } // Calculate CIE location, and get and cache the CIE data @@ -917,61 +912,77 @@ func (ee *elfExtractor) parseFDE(r *reader, ef *pfelf.File, ipStart uintptr, cie = &cieInfo{} if err = cr.parseCIE(cie); err != nil { - return 0, fmt.Errorf("CIE %#x failed: %v", fde.ciePos, err) + return 0, fde, nil, fmt.Errorf("CIE %#x failed: %v", fde.ciePos, err) } // initialize vmRegs from initialState - these can be used by restore // opcode during initial CIE run - cie.initialState = newVMRegs(ef.Machine) + cie.initialState = newVMRegs(efm) // Run CIE initial opcodes st := state{ cie: cie, - cur: newVMRegs(ef.Machine), + cur: newVMRegs(efm), } if err = st.step(&cr); err != nil { - return 0, err + return 0, fde, nil, err } if !cr.isValid() { - return 0, fmt.Errorf("CIE %x parsing failed", fde.ciePos) + return 0, fde, nil, fmt.Errorf("CIE %x parsing failed", fde.ciePos) } cie.initialState = st.cur cieCache.Add(fde.ciePos, cie) } // Parse rest of FDE structure (CIE dependent part) - st := state{cie: cie, cur: cie.initialState} - fde.ipStart, err = r.ptr(st.cie.enc) + + fde.ipStart, err = r.ptr(cie.enc) if err != nil { - return 0, err + return 0, fde, nil, err } if ipStart != 0 && fde.ipStart != ipStart { - return 0, fmt.Errorf( + return 0, fde, nil, fmt.Errorf( "FDE ipStart (%x) not matching search table FDE ipStart (%x)", fde.ipStart, ipStart) } - if st.cie.enc&encIndirect != 0 { - fde.ipLen, err = r.ptr(st.cie.enc) + if cie.enc&encIndirect != 0 { + fde.ipLen, err = r.ptr(cie.enc) } else { - fde.ipLen, err = r.ptr(st.cie.enc & (encFormatMask | encSignedMask)) + fde.ipLen, err = r.ptr(cie.enc & (encFormatMask | encSignedMask)) } if err != nil { - return 0, err + return 0, fde, nil, err } - if st.cie.hasAugmentation { + if cie.hasAugmentation { r.pos += uintptr(r.uleb()) } if !r.isValid() { - return 0, fmt.Errorf("FDE %x not valid after header", fdeID) + return 0, fde, nil, fmt.Errorf("FDE %x not valid after header", fdeID) + } + return fdeLen, fde, cie, nil +} + +// parseFDE reads and processes one Frame Description Entry and returns the size of +// the CIE/FDE entry, and amends the intervals to deltas table. +// The FDE format is described in: +// http://dwarfstd.org/doc/DWARF5.pdf §6.4.1 +// https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html +func (ee *elfExtractor) parseFDE(r *reader, ef *pfelf.File, ipStart uintptr, + cieCache *lru.LRU[uint64, *cieInfo], sorted bool) (size uintptr, err error) { + // Parse FDE header + fdeID := r.pos + fdeLen, fde, cie, err := parsesFDEHeader(r, ef.Machine, ipStart, cieCache) + if err != nil { + return uintptr(fdeLen), err } + st := state{cie: cie, cur: cie.initialState} // Process the FDE opcodes - if !ee.hooks.fdeHook(st.cie, &fde) { - return uintptr(fde.len), nil + if !ee.hooks.fdeHook(st.cie, &fde, ee.deltas) { + return uintptr(fdeLen), nil } st.loc = fde.ipStart - if st.cie.isSignalHandler || isSignalTrampoline(ee.file, &fde) { delta := sdtypes.StackDelta{ Address: uint64(st.loc), @@ -994,6 +1005,7 @@ func (ee *elfExtractor) parseFDE(r *reader, ef *pfelf.File, ipStart uintptr, } ee.hooks.deltaHook(ip, &st.cur, delta) ee.deltas.AddEx(delta, sorted) + sorted = true hint = sdtypes.UnwindHintNone } @@ -1009,20 +1021,15 @@ func (ee *elfExtractor) parseFDE(r *reader, ef *pfelf.File, ipStart uintptr, } } - info := sdtypes.UnwindInfoInvalid - if ef.Entry == uint64(fde.ipStart+fde.ipLen) { - info = sdtypes.UnwindInfoStop - } - // Add end-of-function stop delta. This might later get removed if there is // another function starting on this address. ee.deltas.AddEx(sdtypes.StackDelta{ Address: uint64(fde.ipStart + fde.ipLen), Hints: sdtypes.UnwindHintGap, - Info: info, + Info: sdtypes.UnwindInfoInvalid, }, sorted) - return uintptr(fde.len), nil + return uintptr(fdeLen), nil } // elfRegion is a reference to a region within an ELF file. Such a region reference can be @@ -1160,51 +1167,23 @@ func findEhSections(ef *pfelf.File) ( // walkBinSearchTable parses FDEs by following all references in the binary search table in the // `.eh_frame_hdr` section. -func (ee *elfExtractor) walkBinSearchTable(parsedFile *pfelf.File, ehFrameHdrSec *elfRegion, +func (ee *elfExtractor) walkBinSearchTable(ef *pfelf.File, ehFrameHdrSec *elfRegion, ehFrameSec *elfRegion) error { - h := (*ehFrameHdr)(unsafe.Pointer(&ehFrameHdrSec.data[0])) - - // Skip header, which is immediately followed by the binary search table. The header was - // already previously validated in `validateEhFrameHdr`. - r := ehFrameHdrSec.reader(unsafe.Sizeof(*h), false) - - if _, err := r.ptr(h.ehFramePtrEnc); err != nil { - return err - } - fdeCount, err := r.ptr(h.fdeCountEnc) + t, err := newEhFrameTableFromSections(ehFrameHdrSec, ehFrameSec, ef.Machine) if err != nil { return err } - - cieCache, err := lru.New[uint64, *cieInfo](cieCacheSize, hashUint64) - if err != nil { - return err - } - - // Walk the IP search table and dump each FDE found via it - for f := uintptr(0); f < fdeCount; f++ { - ipStart, err := r.ptr(h.tableEnc) - if err != nil { - return err - } - - fdeAddr, err := r.ptr(h.tableEnc) - if err != nil { - return err + r := t.entryAt(0) + for f := uintptr(0); f < t.fdeCount; f++ { + ipStart, fr, entryErr := t.decodeEntry(&r) + if entryErr != nil { + return entryErr } - - if fdeAddr < ehFrameSec.vaddr { - return fmt.Errorf("FDE %#x before section start %#x", - fdeAddr, ehFrameSec.vaddr) - } - - fr := ehFrameSec.reader(fdeAddr-ehFrameSec.vaddr, false) - _, err = ee.parseFDE(&fr, parsedFile, ipStart, cieCache, true) + _, err = ee.parseFDE(&fr, ef, ipStart, t.cieCache, f > 0) if err != nil && !errors.Is(err, errEmptyEntry) { return fmt.Errorf("failed to parse FDE: %v", err) } } - return nil } @@ -1217,6 +1196,8 @@ func (ee *elfExtractor) walkFDEs(ef *pfelf.File, ehFrameSec *elfRegion, debugFra return err } + ee.hooks.fdeUnsorted() + // Walk the section, and process each FDE it contains var entryLen uintptr for f := uintptr(0); f < uintptr(len(ehFrameSec.data)); f += entryLen { diff --git a/nativeunwind/elfunwindinfo/elfehframe_aarch64.go b/nativeunwind/elfunwindinfo/elfehframe_aarch64.go index abced91f0..0d9c85260 100644 --- a/nativeunwind/elfunwindinfo/elfehframe_aarch64.go +++ b/nativeunwind/elfunwindinfo/elfehframe_aarch64.go @@ -8,13 +8,15 @@ package elfunwindinfo // import "go.opentelemetry.io/ebpf-profiler/nativeunwind/ // can be taken into account regardless of the target build platform. import ( + "bytes" "debug/elf" "fmt" sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" + "go.opentelemetry.io/ebpf-profiler/support" + "golang.org/x/arch/arm64/arm64asm" ) -//nolint:deadcode,varcheck const ( // Aarch64 ABI armRegX0 uleb128 = 0 @@ -117,10 +119,10 @@ func (regs *vmRegs) getUnwindInfoARM() sdtypes.UnwindInfo { // are used for CFA. switch regs.cfa.reg { case armRegFP: - info.Opcode = sdtypes.UnwindOpcodeBaseFP + info.Opcode = support.UnwindOpcodeBaseFP info.Param = int32(regs.cfa.off) case armRegSP: - info.Opcode = sdtypes.UnwindOpcodeBaseSP + info.Opcode = support.UnwindOpcodeBaseSP info.Param = int32(regs.cfa.off) } @@ -139,7 +141,7 @@ func (regs *vmRegs) getUnwindInfoARM() sdtypes.UnwindInfo { // thus, the assumption - use UnwindOpcodeBaseLR to instruct native stack // unwinder to load RA from link register // This is either prolog or epilog sequence, read RA from link register. - info.FPOpcode = sdtypes.UnwindOpcodeBaseLR + info.FPOpcode = support.UnwindOpcodeBaseLR info.FPParam = 0 case regCFA: if regs.cfa.off != 0 { @@ -160,3 +162,39 @@ func (regs *vmRegs) getUnwindInfoARM() sdtypes.UnwindInfo { return info } + +func detectEntryARM(code []byte) int { + // Refer to test cases for the seen assembly dumps. + // Both, on GLIBC and MUSL there is no FDE for the entry code. This code tries + // to match both. The main difference is that glibc uses BL (Branch with Link) + // or a proper function call to maintain frame, and musl uses B (Branch) or + // a jump so the entry is not seen on traces. + + // Match the prolog for clearing LR/FP + if len(code) < 32 || + !bytes.Equal(code[:8], []byte{0x1d, 0x00, 0x80, 0xd2, 0x1e, 0x00, 0x80, 0xd2}) { + return 0 + } + + // Search for the second B or BL + numBranch := 0 + for pos := 8; pos < len(code); pos += 4 { + inst, err := arm64asm.Decode(code[pos:]) + if err != nil { + return 0 + } + switch inst.Op { + case arm64asm.ADD, arm64asm.ADRP, arm64asm.AND, arm64asm.LDR, + arm64asm.MOV, arm64asm.MOVK, arm64asm.MOVZ: + // nop, allowed instruction + case arm64asm.B, arm64asm.BL: + numBranch++ + if numBranch == 2 { + return pos + 4 + } + default: + return 0 + } + } + return 0 +} diff --git a/nativeunwind/elfunwindinfo/elfehframe_test.go b/nativeunwind/elfunwindinfo/elfehframe_test.go index 5aef1c960..2335e3f37 100644 --- a/nativeunwind/elfunwindinfo/elfehframe_test.go +++ b/nativeunwind/elfunwindinfo/elfehframe_test.go @@ -8,6 +8,7 @@ import ( "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" + "go.opentelemetry.io/ebpf-profiler/support" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,9 +20,12 @@ type ehtester struct { found int } -func (e *ehtester) fdeHook(cie *cieInfo, fde *fdeInfo) bool { - e.t.Logf("FDE len %d, ciePos %x, ip %x...%x, ipLen %d (enc %x, cf %d, df %d, ra %d)", - fde.len, fde.ciePos, fde.ipStart, fde.ipStart+fde.ipLen, fde.ipLen, +func (e *ehtester) fdeUnsorted() { +} + +func (e *ehtester) fdeHook(cie *cieInfo, fde *fdeInfo, _ *sdtypes.StackDeltaArray) bool { + e.t.Logf("FDE ciePos %x, ip %x...%x, ipLen %d (enc %x, cf %d, df %d, ra %d)", + fde.ciePos, fde.ipStart, fde.ipStart+fde.ipLen, fde.ipLen, cie.enc, cie.codeAlign, cie.dataAlign, cie.regRA) e.t.Logf(" LOC CFA rbp ra") return true @@ -48,18 +52,18 @@ func genDelta(opcode uint8, cfa, rbp int32) sdtypes.UnwindInfo { Param: cfa, } if rbp != 0 { - res.FPOpcode = sdtypes.UnwindOpcodeBaseCFA + res.FPOpcode = support.UnwindOpcodeBaseCFA res.FPParam = -rbp } return res } func deltaRSP(cfa, rbp int32) sdtypes.UnwindInfo { - return genDelta(sdtypes.UnwindOpcodeBaseSP, cfa, rbp) + return genDelta(support.UnwindOpcodeBaseSP, cfa, rbp) } func deltaRBP(cfa, rbp int32) sdtypes.UnwindInfo { - return genDelta(sdtypes.UnwindOpcodeBaseFP, cfa, rbp) + return genDelta(support.UnwindOpcodeBaseFP, cfa, rbp) } func TestEhFrame(t *testing.T) { diff --git a/nativeunwind/elfunwindinfo/elfehframe_x86.go b/nativeunwind/elfunwindinfo/elfehframe_x86.go index 4da9a4f65..0fc8e12ce 100644 --- a/nativeunwind/elfunwindinfo/elfehframe_x86.go +++ b/nativeunwind/elfunwindinfo/elfehframe_x86.go @@ -8,13 +8,15 @@ package elfunwindinfo // import "go.opentelemetry.io/ebpf-profiler/nativeunwind/ // can be taken into account regardless of the target build platform. import ( + "bytes" "debug/elf" "fmt" sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" + "go.opentelemetry.io/ebpf-profiler/support" + "golang.org/x/arch/x86/x86asm" ) -//nolint:deadcode,varcheck const ( // x86_64 abi (https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf, page 57) x86RegRAX uleb128 = 0 @@ -116,13 +118,13 @@ func (regs *vmRegs) getUnwindInfoX86() sdtypes.UnwindInfo { case regCFA: // Check that RBP is between CFA and stack top if regs.cfa.reg != x86RegRSP || (regs.fp.off < 0 && regs.fp.off >= -regs.cfa.off) { - info.FPOpcode = sdtypes.UnwindOpcodeBaseCFA + info.FPOpcode = support.UnwindOpcodeBaseCFA info.FPParam = int32(regs.fp.off) } case regExprReg: // expression: RBP+offrbp if r, _, offrbp, _ := splitOff(regs.fp.off); uleb128(r) == x86RegRBP { - info.FPOpcode = sdtypes.UnwindOpcodeBaseFP + info.FPOpcode = support.UnwindOpcodeBaseFP info.FPParam = int32(offrbp) } } @@ -130,11 +132,11 @@ func (regs *vmRegs) getUnwindInfoX86() sdtypes.UnwindInfo { // Determine unwind info for stack pointer switch regs.cfa.reg { case x86RegRBP: - info.Opcode = sdtypes.UnwindOpcodeBaseFP + info.Opcode = support.UnwindOpcodeBaseFP info.Param = int32(regs.cfa.off) case x86RegRSP: if regs.cfa.off != 0 { - info.Opcode = sdtypes.UnwindOpcodeBaseSP + info.Opcode = support.UnwindOpcodeBaseSP info.Param = int32(regs.cfa.off) } case x86RegRAX, x86RegR9, x86RegR11, x86RegR15: @@ -142,26 +144,57 @@ func (regs *vmRegs) getUnwindInfoX86() sdtypes.UnwindInfo { // as the CFA directly. These function do not call other code that would // trash the register, so allow these for libcrypto. if regs.cfa.off%8 == 0 { - info.Opcode = sdtypes.UnwindOpcodeBaseReg + info.Opcode = support.UnwindOpcodeBaseReg info.Param = int32(regs.cfa.reg) + int32(regs.cfa.off)<<1 } case regExprPLT: - info.Opcode = sdtypes.UnwindOpcodeCommand - info.Param = sdtypes.UnwindCommandPLT + info.Opcode = support.UnwindOpcodeCommand + info.Param = support.UnwindCommandPLT case regExprRegDeref: reg, _, off, off2 := splitOff(regs.cfa.off) if param, ok := sdtypes.PackDerefParam(int32(off), int32(off2)); ok { switch uleb128(reg) { case x86RegRBP: // GCC SSE vectorized functions - info.Opcode = sdtypes.UnwindOpcodeBaseFP | sdtypes.UnwindOpcodeFlagDeref + info.Opcode = support.UnwindOpcodeBaseFP | support.UnwindOpcodeFlagDeref info.Param = param case x86RegRSP: // OpenSSL assembly using SSE/AVX - info.Opcode = sdtypes.UnwindOpcodeBaseSP | sdtypes.UnwindOpcodeFlagDeref + info.Opcode = support.UnwindOpcodeBaseSP | support.UnwindOpcodeFlagDeref info.Param = param } } } return info } + +func detectEntryX86(code []byte) int { + // Refer to test cases for the actual assembly code seen. + // On glibc, the entry has FDE. No fixup is needed. + // On musl, the entry has no FDE, or possibly has an FDE covering part of it. + // Detect the musl case and return entry. + + // Match the assembly exactly except the LEA call offset + if len(code) < 32 || + !bytes.Equal(code[:9], []byte{0x48, 0x31, 0xed, 0x48, 0x89, 0xe7, 0x48, 0x8d, 0x35}) || + !bytes.Equal(code[13:22], []byte{0x48, 0x83, 0xe4, 0xf0, 0xe8, 0x00, 0x00, 0x00, 0x00}) { + return 0 + } + + // Decode the second portion and allow whitelisted opcodes finding the JMP + for pos := 22; pos < len(code); { + inst, err := x86asm.Decode(code[pos:], 64) + if err != nil { + return 0 + } + switch inst.Op { + case x86asm.MOV, x86asm.LEA, x86asm.XOR: + pos += inst.Len + case x86asm.JMP: + return pos + inst.Len + default: + return 0 + } + } + return 0 +} diff --git a/nativeunwind/elfunwindinfo/elfehframetable.go b/nativeunwind/elfunwindinfo/elfehframetable.go new file mode 100644 index 000000000..5a72c67bd --- /dev/null +++ b/nativeunwind/elfunwindinfo/elfehframetable.go @@ -0,0 +1,157 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package elfunwindinfo // import "go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo" + +import ( + "debug/elf" + "errors" + "fmt" + "sort" + "unsafe" + + lru "github.com/elastic/go-freelru" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" +) + +type FDE struct { + PCBegin uintptr + PCRange uintptr +} + +type EhFrameTable struct { + fdeCount uintptr + tableStartPos uintptr + tableEntrySize int + tableEnc encoding + ehFrameHdrSec *elfRegion + ehFrameSec *elfRegion + efm elf.Machine + + // cieCache holds the CIEs decoded so far. This is the only piece that is + // not concurrent safe, and could be made into a sync lru if needed. + cieCache *lru.LRU[uint64, *cieInfo] +} + +// NewEhFrameTable creates a new EhFrameTable from the given pfelf.File. +// The returned EhFrameTable is not concurrent safe. +func NewEhFrameTable(ef *pfelf.File) (*EhFrameTable, error) { + ehFrameHdrSec, ehFrameSec, err := findEhSections(ef) + if err != nil { + return nil, fmt.Errorf("failed to get EH sections: %w", err) + } + if ehFrameSec == nil { + return nil, errors.New(".eh_frame not found") + } + if ehFrameHdrSec == nil { + return nil, errors.New(".eh_frame_hdr not found") + } + return newEhFrameTableFromSections(ehFrameHdrSec, ehFrameSec, ef.Machine) +} + +// LookupFDE performs a binary search in .eh_frame_hdr for an FDE covering the given addr. +func (e *EhFrameTable) LookupFDE(addr libpf.Address) (FDE, error) { + idx := sort.Search(e.count(), func(idx int) bool { + r := e.entryAt(idx) + ipStart, _ := r.ptr(e.tableEnc) // ignoring error, check bounds later + return ipStart > uintptr(addr) + }) + if idx <= 0 { + return FDE{}, errors.New("FDE not found") + } + ipStart, fr, entryErr := e.decodeEntryAt(idx - 1) + if entryErr != nil { + return FDE{}, entryErr + } + _, fde, _, err := parsesFDEHeader(&fr, e.efm, ipStart, e.cieCache) + if err != nil { + return FDE{}, err + } + if uintptr(addr) < fde.ipStart || uintptr(addr) >= fde.ipStart+fde.ipLen { + return FDE{}, errors.New("FDE not found") + } + + return FDE{ + PCBegin: fde.ipStart, + PCRange: fde.ipLen, + }, nil +} + +func newEhFrameTableFromSections(ehFrameHdrSec *elfRegion, + ehFrameSec *elfRegion, efm elf.Machine, +) (hp *EhFrameTable, err error) { + hdr := (*ehFrameHdr)(unsafe.Pointer(&ehFrameHdrSec.data[0])) + + r := ehFrameHdrSec.reader(unsafe.Sizeof(ehFrameHdr{}), false) + if _, err = r.ptr(hdr.ehFramePtrEnc); err != nil { + return nil, err + } + fdeCount, err := r.ptr(hdr.fdeCountEnc) + if err != nil { + return nil, err + } + cieCache, err := lru.New[uint64, *cieInfo](cieCacheSize, hashUint64) + if err != nil { + return nil, err + } + return &EhFrameTable{ + fdeCount: fdeCount, + tableStartPos: r.pos, + tableEntrySize: formatLen(hdr.tableEnc) * 2, + tableEnc: hdr.tableEnc, + ehFrameHdrSec: ehFrameHdrSec, + ehFrameSec: ehFrameSec, + efm: efm, + cieCache: cieCache, + }, nil +} + +// returns FDE count +func (e *EhFrameTable) count() int { + return int(e.fdeCount) +} + +// entryAt returns a reader for the binary search table at given index. +func (e *EhFrameTable) entryAt(idx int) reader { + return e.ehFrameHdrSec.reader(e.tableStartPos+uintptr(e.tableEntrySize*idx), false) +} + +// decodeEntry decodes one entry of the binary search table from the reader. +func (e *EhFrameTable) decodeEntry(r *reader) (ipStart uintptr, fr reader, err error) { + ipStart, err = r.ptr(e.tableEnc) + if err != nil { + return 0, reader{}, err + } + var fdeAddr uintptr + fdeAddr, err = r.ptr(e.tableEnc) + if err != nil { + return 0, reader{}, err + } + if fdeAddr < e.ehFrameSec.vaddr { + return 0, reader{}, fmt.Errorf("FDE %#x before section start %#x", + fdeAddr, e.ehFrameSec.vaddr) + } + fr = e.ehFrameSec.reader(fdeAddr-e.ehFrameSec.vaddr, false) + return ipStart, fr, err +} + +// decodeEntryAt decodes the entry from given index. +func (e *EhFrameTable) decodeEntryAt(idx int) (ipStart uintptr, fr reader, err error) { + r := e.entryAt(idx) + return e.decodeEntry(&r) +} + +// formatLen returns the length of a field encoded with enc encoding. +func formatLen(enc encoding) int { + switch enc & encFormatMask { + case encFormatData2: + return 2 + case encFormatData4: + return 4 + case encFormatData8, encFormatNative: + return 8 + default: + return 0 + } +} diff --git a/nativeunwind/elfunwindinfo/elfehframetable_test.go b/nativeunwind/elfunwindinfo/elfehframetable_test.go new file mode 100644 index 000000000..1abd9f5e7 --- /dev/null +++ b/nativeunwind/elfunwindinfo/elfehframetable_test.go @@ -0,0 +1,70 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package elfunwindinfo + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/ebpf-profiler/libpf" +) + +func TestLookupFDE(t *testing.T) { + checks := []struct { + at uintptr + expected FDE + }{ + {at: 0x0, expected: FDE{}}, + {at: 0x840, expected: FDE{}}, + {at: 0x850, expected: FDE{PCBegin: 0x850, PCRange: 0x10}}, + {at: 0x855, expected: FDE{PCBegin: 0x850, PCRange: 0x10}}, + {at: 0x859, expected: FDE{PCBegin: 0x850, PCRange: 0x10}}, + {at: 0x860, expected: FDE{PCBegin: 0x860, PCRange: 0x68}}, + {at: 0x865, expected: FDE{PCBegin: 0x860, PCRange: 0x68}}, + {at: 0x8c7, expected: FDE{PCBegin: 0x860, PCRange: 0x68}}, + {at: 0x8c8, expected: FDE{}}, + {at: 0x8c9, expected: FDE{}}, + {at: 0x8cf, expected: FDE{}}, + {at: 0x8d0, expected: FDE{PCBegin: 0x8d0, PCRange: 0x11f}}, + {at: 0x8d3, expected: FDE{PCBegin: 0x8d0, PCRange: 0x11f}}, + {at: 0x9ee, expected: FDE{PCBegin: 0x8d0, PCRange: 0x11f}}, + {at: 0x9ef, expected: FDE{}}, + {at: 0x9f0, expected: FDE{PCBegin: 0x9f0, PCRange: 0x2b}}, + {at: 0x9f1, expected: FDE{PCBegin: 0x9f0, PCRange: 0x2b}}, + {at: 0xa1a, expected: FDE{PCBegin: 0x9f0, PCRange: 0x2b}}, + {at: 0xa1b, expected: FDE{}}, + {at: 0xa1c, expected: FDE{}}, + {at: 0xb1f, expected: FDE{}}, + {at: 0xb20, expected: FDE{PCBegin: 0xb20, PCRange: 0x65}}, + {at: 0xb32, expected: FDE{PCBegin: 0xb20, PCRange: 0x65}}, + {at: 0xb84, expected: FDE{PCBegin: 0xb20, PCRange: 0x65}}, + {at: 0xb85, expected: FDE{}}, + {at: 0xb90, expected: FDE{PCBegin: 0xb90, PCRange: 0x2}}, + {at: 0xb91, expected: FDE{PCBegin: 0xb90, PCRange: 0x2}}, + {at: 0xb92, expected: FDE{}}, + {at: 0xb93, expected: FDE{}}, + {at: 0x1000, expected: FDE{}}, + {at: 0xcafe000, expected: FDE{}}, + } + elf, err := getUsrBinPfelf() + require.NoError(t, err) + t.Cleanup(func() { + err = elf.Close() + require.NoError(t, err) + }) + e, err := NewEhFrameTable(elf) + require.NoError(t, err) + for _, check := range checks { + t.Run(fmt.Sprintf("%x", check.at), func(t *testing.T) { + actual, err := e.LookupFDE(libpf.Address(check.at)) + if check.expected == (FDE{}) { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, check.expected, actual) + } + }) + } +} diff --git a/nativeunwind/elfunwindinfo/elfgopclntab.go b/nativeunwind/elfunwindinfo/elfgopclntab.go index a0ac5b9fe..03cf1727d 100644 --- a/nativeunwind/elfunwindinfo/elfgopclntab.go +++ b/nativeunwind/elfunwindinfo/elfgopclntab.go @@ -11,11 +11,15 @@ import ( "bytes" "debug/elf" "fmt" + "go/version" + "io" + "sort" + "strings" "unsafe" - log "github.com/sirupsen/logrus" "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" + "go.opentelemetry.io/ebpf-profiler/support" ) // Go runtime functions for which we should not attempt to unwind further @@ -29,7 +33,8 @@ var goFunctionsStopDelta = map[string]*sdtypes.UnwindInfo{ "runtime.systemstack": &sdtypes.UnwindInfoStop, // signal return frame - "runtime.sigreturn": &sdtypes.UnwindInfoSignal, + "runtime.sigreturn": &sdtypes.UnwindInfoSignal, + "runtime.sigreturn__sigaction": &sdtypes.UnwindInfoSignal, } const ( @@ -37,16 +42,31 @@ const ( // often huge. Host agent binaries have about 32M .rodata, so allow for more. maxBytesGoPclntab = 128 * 1024 * 1024 - // pclntabHeader magic identifying Go version - magicGo1_2 = 0xfffffffb - magicGo1_16 = 0xfffffffa - magicGo1_18 = 0xfffffff0 - magicGo1_20 = 0xfffffff1 + // internally used gopclntab version + goInvalid = 0 + go1_2 = 2 + go1_16 = 16 + go1_18 = 18 + go1_20 = 20 ) +func goMagicToVersion(magic uint32) uint8 { + // pclntab header magic bytes identifying Go version + switch magic { + case 0xfffffffb: // Go 1.2 + return go1_2 + case 0xfffffffa: // Go 1.16 + return go1_16 + case 0xfffffff0: // Go 1.18 + return go1_18 + case 0xfffffff1: // Go 1.20 + return go1_20 + default: + return goInvalid + } +} + // pclntabHeader is the Golang pclntab header structure -// -//nolint:structcheck type pclntabHeader struct { // magic is one of the magicGo1_xx constants identifying the version magic uint32 @@ -62,8 +82,6 @@ type pclntabHeader struct { // pclntabHeader116 is the Golang pclntab header structure starting Go 1.16 // structural definition of this is found in go/src/runtime/symtab.go as pcHeader -// -//nolint:structcheck type pclntabHeader116 struct { pclntabHeader nfiles uint @@ -76,8 +94,6 @@ type pclntabHeader116 struct { // pclntabHeader118 is the Golang pclntab header structure starting Go 1.18 // structural definition of this is found in go/src/runtime/symtab.go as pcHeader -// -//nolint:structcheck type pclntabHeader118 struct { pclntabHeader nfiles uint @@ -90,30 +106,22 @@ type pclntabHeader118 struct { } // pclntabFuncMap is the Golang function symbol table map entry -// -//nolint:structcheck type pclntabFuncMap struct { - pc uint64 - funcOff uint64 + pc uintptr + funcOff uintptr } -// pclntabFunc is the Golang function definition (struct _func in the spec) as before Go 1.18. -// -//nolint:structcheck -type pclntabFunc struct { - startPc uint64 - nameOff, argsSize, frameSize int32 - pcspOff, pcfileOff, pclnOff int32 - nfuncData, npcData int32 +// pclntabFuncMap118 is the Golang function symbol table map entry for Go 1.18+. +type pclntabFuncMap118 struct { + pc uint32 + funcOff uint32 } -// pclntabFunc118 is the Golang function definition (struct _func in the spec) -// starting with Go 1.18. -// see: go/src/runtime/runtime2.go (struct _func) -// -//nolint:structcheck -type pclntabFunc118 struct { - entryoff uint32 // start pc, as offset from pcHeader.textStart +// pclntabFunc is the common portion of the Golang function definition. +type pclntabFunc struct { + // The actual data is preceded with the function start PC value, which is + // a pointer (pre-Go1.18) or fixed 32-bit offset from .text start (Go1.18+). + // startPc uintptr | uint32 nameOff, argsSize, frameSize int32 pcspOff, pcfileOff, pclnOff int32 nfuncData, npcData int32 @@ -136,11 +144,6 @@ func PclntabHeaderSize() int { return int(unsafe.Sizeof(pclntabHeader{})) } -// IsGo118orNewer returns true if magic matches with the Go 1.18 or newer. -func IsGo118orNewer(magic uint32) bool { - return magic == magicGo1_18 || magic == magicGo1_20 -} - // pclntabHeaderSignature returns a byte slice that can be // used to verify if some bytes represent a valid pclntab header. func pclntabHeaderSignature(arch elf.Machine) []byte { @@ -217,68 +220,20 @@ func getInt32(data []byte, offset int) int { return int(*(*int32)(unsafe.Pointer(&data[offset]))) } -// getString returns a zero terminated string from the data slice at given offset as []byte -func getString(data []byte, offset int) []byte { +// getString returns a string from the data slice at given offset. +func getString(data []byte, offset int) string { if offset < 0 || offset > len(data) { - return nil + return "" } zeroIdx := bytes.IndexByte(data[offset:], 0) if zeroIdx < 0 { - return nil + return "" } - return data[offset : offset+zeroIdx] + return unsafe.String(unsafe.SliceData(data[offset:]), zeroIdx) } -const ( - strategyUnknown = iota - strategyFramePointer - strategyDeltasWithoutRBP - strategyDeltasWithRBP -) - -// noFPSourceSuffixes lists the go runtime source files that call assembly code -// which trashes RBP. These source files need to use explicit SP delta so that -// RBP can be recovered, and be then further used for frame pointer based unwinding. -// This lists the most notable problem cases from Go runtime. -// TODO(tteras) Go Runtime files calling internal.bytealg.Index* may need to be added here. -var noFPSourceSuffixes = [][]byte{ - []byte("/src/crypto/sha1/sha1.go"), - []byte("/src/crypto/sha256/sha256.go"), - []byte("/src/crypto/sha512/sha512.go"), - []byte("/src/crypto/elliptic/p256_asm.go"), - []byte("golang.org/x/crypto/curve25519/curve25519_amd64.go"), - []byte("golang.org/x/crypto/chacha20poly1305/chacha20poly1305_amd64.go"), -} - -// getSourceFileStrategy categorizes sourceFile's unwinding strategy based on its name -func getSourceFileStrategy(arch elf.Machine, sourceFile []byte) int { - switch arch { - case elf.EM_X86_64: - // Most of the assembly code needs explicit SP delta as they do not - // create stack frame. Do not recover RBP as it is not modified. - if bytes.HasSuffix(sourceFile, []byte(".s")) { - return strategyDeltasWithoutRBP - } - - // Check for the Go source files needing SP delta unwinding to recover RBP - for _, suffix := range noFPSourceSuffixes { - if bytes.HasSuffix(sourceFile, suffix) { - return strategyDeltasWithRBP - } - } - case elf.EM_AARCH64: - // Assume all code has frame pointers as the code generated by Golang compiler - // for ARM64 supports frame pointers even for asm code. Frame pointers - // get omitted only for leaf, no arg functions. - return strategyFramePointer - } - - // Use frame pointer for others - return strategyFramePointer -} - -// SearchGoPclntab uses heuristic to find the gopclntab from RO data. -func SearchGoPclntab(ef *pfelf.File) ([]byte, error) { +// searchGoPclntab uses heuristic to find the gopclntab from RO data. +func searchGoPclntab(ef *pfelf.File) ([]byte, error) { // The sections headers are not available for coredump testing, because they are // not inside any PT_LOAD segment. And in the case ofwhere they might be available // because of alignment they are likely not usable, e.g. the musl C-library will @@ -322,8 +277,7 @@ func SearchGoPclntab(ef *pfelf.File) ([]byte, error) { // location as the .gopclntab base. Otherwise, continue just search // for next candidate location. hdr := (*pclntabHeader)(unsafe.Pointer(&data[i])) - switch hdr.magic { - case magicGo1_20, magicGo1_18, magicGo1_16, magicGo1_2: + if goMagicToVersion(hdr.magic) != goInvalid { return data[i:], nil } } @@ -332,22 +286,16 @@ func SearchGoPclntab(ef *pfelf.File) ([]byte, error) { return nil, nil } -// Parse Golang .gopclntab spdelta tables and try to produce minified intervals -// by using large frame pointer ranges when possible -func (ee *elfExtractor) parseGoPclntab() error { - var err error - var data []byte - - ef := ee.file - +// extractGoPclntab extracts the .gopclntab data from a given pfelf.File. +func extractGoPclntab(ef *pfelf.File) (data []byte, err error) { if ef.InsideCore { // Section tables not available. Use heuristic. Ignore errors as // this might not be a Go binary. - data, _ = SearchGoPclntab(ef) + data, _ = searchGoPclntab(ef) } else if s := ef.Section(".gopclntab"); s != nil { // Load the .gopclntab via section if available. if data, err = s.Data(maxBytesGoPclntab); err != nil { - return fmt.Errorf("failed to load .gopclntab section: %v", err) + return nil, fmt.Errorf("failed to load .gopclntab section: %v", err) } } else if s := ef.Section(".go.buildinfo"); s != nil { // This looks like Go binary. Lookup the runtime.pclntab symbols, @@ -358,294 +306,299 @@ func (ee *elfExtractor) parseGoPclntab() error { if err != nil { // It seems the Go binary was stripped. So we use the heuristic approach // to get the stack deltas. - if data, err = SearchGoPclntab(ef); err != nil { - return fmt.Errorf("failed to search .gopclntab: %v", err) + if data, err = searchGoPclntab(ef); err != nil { + return nil, fmt.Errorf("failed to search .gopclntab: %v", err) } } else { start, err := symtab.LookupSymbolAddress("runtime.pclntab") if err != nil { - return fmt.Errorf("failed to load .gopclntab via symbols: %v", err) + return nil, fmt.Errorf("failed to load .gopclntab via symbols: %v", err) } end, err := symtab.LookupSymbolAddress("runtime.epclntab") if err != nil { - return fmt.Errorf("failed to load .gopclntab via symbols: %v", err) + return nil, fmt.Errorf("failed to load .gopclntab via symbols: %v", err) } if start >= end { - return fmt.Errorf("invalid .gopclntab symbols: %v-%v", start, end) + return nil, fmt.Errorf("invalid .gopclntab symbols: %v-%v", start, end) } - data = make([]byte, end-start) - if _, err := ef.ReadVirtualMemory(data, int64(start)); err != nil { - return fmt.Errorf("failed to load .gopclntab via symbols: %v", err) + data, err = ef.VirtualMemory(int64(start), int(end-start), maxBytesGoPclntab) + if err != nil { + return nil, fmt.Errorf("failed to load .gopclntab via symbols: %v", err) } } } + return data, nil +} + +// Gopclntab is the API for extracting data from .gopclntab +type Gopclntab struct { + dataRef io.Closer + data []byte + textStart uintptr + numFuncs int + + version uint8 + quantum uint8 + ptrSize uint8 + funSize uint8 + funcMapSize uint8 + + // These are read-only byte slices to various areas within .gopclntab + // (subslices of data []byte). Since 'data' a slice returned by pfelf.File + // it can be allocated or mmapped read-only data. To keep memory usage + // and GC stress minimal the returned strings (symbol and file names) refer + // to this data directly (via unsafe.String). + functab, funcdata, funcnametab, filetab, pctab, cutab []byte +} + +// NewGopclntab parses and returns the parsed data for further operations. +func NewGopclntab(ef *pfelf.File) (*Gopclntab, error) { + data, err := extractGoPclntab(ef) if data == nil { - return nil + return nil, err } - var textStart uintptr hdrSize := uintptr(PclntabHeaderSize()) - mapSize := unsafe.Sizeof(pclntabFuncMap{}) - funSize := unsafe.Sizeof(pclntabFunc{}) dataLen := uintptr(len(data)) if dataLen < hdrSize { - return fmt.Errorf(".gopclntab is too short (%v)", len(data)) + return nil, fmt.Errorf(".gopclntab is too short (%v)", len(data)) } - var functab, funcdata, funcnametab, filetab, pctab, cutab []byte - hdr := (*pclntabHeader)(unsafe.Pointer(&data[0])) - fieldSize := uintptr(hdr.ptrSize) - switch hdr.magic { - case magicGo1_2: - functabEnd := int(hdrSize + uintptr(hdr.numFuncs)*mapSize + uintptr(hdr.ptrSize)) + g := &Gopclntab{ + data: data, + version: goMagicToVersion(hdr.magic), + quantum: hdr.quantum, + ptrSize: hdr.ptrSize, + funSize: hdr.ptrSize + uint8(unsafe.Sizeof(pclntabFunc{})), + funcMapSize: hdr.ptrSize * 2, + numFuncs: int(hdr.numFuncs), + } + if g.version == goInvalid || hdr.pad != 0 || hdr.ptrSize != 8 { + return nil, fmt.Errorf(".gopclntab header: %x, %x, %x", hdr.magic, hdr.pad, hdr.ptrSize) + } + + switch g.version { + case go1_2: + functabEnd := int(hdrSize) + g.numFuncs*int(g.funcMapSize) + int(hdr.ptrSize) filetabOffset := getInt32(data, functabEnd) numSourceFiles := getInt32(data, filetabOffset) if filetabOffset == 0 || numSourceFiles == 0 { - return fmt.Errorf(".gopclntab corrupt (filetab 0x%x, nfiles %d)", + return nil, fmt.Errorf(".gopclntab corrupt (filetab 0x%x, nfiles %d)", filetabOffset, numSourceFiles) } - functab = data[hdrSize:filetabOffset] - cutab = data[filetabOffset:] - pctab = data - funcnametab = data - funcdata = data - filetab = data - case magicGo1_16: + g.functab = data[hdrSize:filetabOffset] + g.cutab = data[filetabOffset:] + g.pctab = data + g.funcnametab = data + g.funcdata = data + g.filetab = data + case go1_16: hdrSize = unsafe.Sizeof(pclntabHeader116{}) if dataLen < hdrSize { - return fmt.Errorf(".gopclntab is too short (%v)", len(data)) + return nil, fmt.Errorf(".gopclntab is too short (%v)", len(data)) } hdr116 := (*pclntabHeader116)(unsafe.Pointer(&data[0])) if dataLen < hdr116.funcnameOffset || dataLen < hdr116.cuOffset || dataLen < hdr116.filetabOffset || dataLen < hdr116.pctabOffset || dataLen < hdr116.pclnOffset { - return fmt.Errorf(".gopclntab is corrupt (%x, %x, %x, %x, %x)", + return nil, fmt.Errorf(".gopclntab is corrupt (%x, %x, %x, %x, %x)", hdr116.funcnameOffset, hdr116.cuOffset, hdr116.filetabOffset, hdr116.pctabOffset, hdr116.pclnOffset) } - funcnametab = data[hdr116.funcnameOffset:] - cutab = data[hdr116.cuOffset:] - filetab = data[hdr116.filetabOffset:] - pctab = data[hdr116.pctabOffset:] - functab = data[hdr116.pclnOffset:] - funcdata = functab - case magicGo1_18, magicGo1_20: + g.funcnametab = data[hdr116.funcnameOffset:] + g.cutab = data[hdr116.cuOffset:] + g.filetab = data[hdr116.filetabOffset:] + g.pctab = data[hdr116.pctabOffset:] + g.functab = data[hdr116.pclnOffset:] + g.funcdata = g.functab + case go1_18, go1_20: hdrSize = unsafe.Sizeof(pclntabHeader118{}) if dataLen < hdrSize { - return fmt.Errorf(".gopclntab is too short (%v)", dataLen) + return nil, fmt.Errorf(".gopclntab is too short (%v)", dataLen) } hdr118 := (*pclntabHeader118)(unsafe.Pointer(&data[0])) if dataLen < hdr118.funcnameOffset || dataLen < hdr118.cuOffset || dataLen < hdr118.filetabOffset || dataLen < hdr118.pctabOffset || dataLen < hdr118.pclnOffset { - return fmt.Errorf(".gopclntab is corrupt (%x, %x, %x, %x, %x)", + return nil, fmt.Errorf(".gopclntab is corrupt (%x, %x, %x, %x, %x)", hdr118.funcnameOffset, hdr118.cuOffset, hdr118.filetabOffset, hdr118.pctabOffset, hdr118.pclnOffset) } - funcnametab = data[hdr118.funcnameOffset:] - cutab = data[hdr118.cuOffset:] - filetab = data[hdr118.filetabOffset:] - pctab = data[hdr118.pctabOffset:] - functab = data[hdr118.pclnOffset:] - funcdata = functab - textStart = hdr118.textStart - funSize = unsafe.Sizeof(pclntabFunc118{}) + g.funcnametab = data[hdr118.funcnameOffset:] + g.cutab = data[hdr118.cuOffset:] + g.filetab = data[hdr118.filetabOffset:] + g.pctab = data[hdr118.pctabOffset:] + g.functab = data[hdr118.pclnOffset:] + g.funcdata = g.functab + g.textStart = hdr118.textStart // With the change of the type of the first field of _func in Go 1.18, this // value is now hard coded. // //nolint:lll // See https://github.com/golang/go/blob/6df0957060b1315db4fd6a359eefc3ee92fcc198/src/debug/gosym/pclntab.go#L376-L382 - fieldSize = uintptr(4) - mapSize = fieldSize * 2 - default: - return fmt.Errorf(".gopclntab format (0x%x) not supported", hdr.magic) + g.funcMapSize = 2 * 4 + g.funSize = 4 + uint8(unsafe.Sizeof(pclntabFunc{})) } - if hdr.pad != 0 || hdr.ptrSize != 8 { - return fmt.Errorf(".gopclntab header: %x, %x", hdr.pad, hdr.ptrSize) + g.dataRef = ef.Take() + + return g, nil +} + +// Close releases the pfelf Data reference taken. +func (g *Gopclntab) Close() error { + return g.dataRef.Close() +} + +// getFuncMapEntry returns the entry at 'index' from the gopclntab function lookup map. +func (g *Gopclntab) getFuncMapEntry(index int) (pc, funcOff uintptr) { + if g.version >= go1_18 { + //nolint:lll + // See: https://github.com/golang/go/blob/6df0957060b1315db4fd6a359eefc3ee92fcc198/src/debug/gosym/pclntab.go#L401-L413 + fmap := (*pclntabFuncMap118)(unsafe.Pointer(&g.functab[index*int(g.funcMapSize)])) + return g.textStart + uintptr(fmap.pc), uintptr(fmap.funcOff) + } else { + fmap := (*pclntabFuncMap)(unsafe.Pointer(&g.functab[index*int(g.funcMapSize)])) + return fmap.pc, fmap.funcOff } +} - // Go uses frame-pointers by default since Go 1.7, but unfortunately - // it is not necessarily available when in code from non-Golang source - // files, such as the assembly, of the Go runtime. - // Since Golang binaries are huge statically compiled executables and - // would fill up our precious kernel delta maps fast, the strategy is to - // create deltastack maps for non-Go source files only, and otherwise - // cover the vast majority with "use frame pointer" stack delta. - sourceStrategy := make(map[int]int) +// getFunc returns the gopclntab function data and its start address. +func (g *Gopclntab) getFunc(funcOff uintptr) (uintptr, *pclntabFunc) { + // Get the function data + if uintptr(len(g.funcdata)) < funcOff+uintptr(g.funSize) { + return 0, nil + } + var pc uintptr + if g.version >= go1_18 { + pc = g.textStart + uintptr(*(*uint32)(unsafe.Pointer(&g.funcdata[funcOff]))) + funcOff += 4 + } else { + pc = *(*uintptr)(unsafe.Pointer(&g.funcdata[funcOff])) + funcOff += uintptr(g.ptrSize) + } + return pc, (*pclntabFunc)(unsafe.Pointer(&g.funcdata[funcOff])) +} - // Get target machine architecture for the ELF file - arch := ef.Machine +// getPcval returns the pcval table at given offset with 'startPc' as the pc start value. +func (g *Gopclntab) getPcval(offs int32, startPc uint) pcval { + return newPcval(g.pctab[int(offs):], startPc, g.quantum) +} - fmap := &pclntabFuncMap{} - fun := &pclntabFunc{} - // Iterate the golang PC to function lookup table (sorted by PC) - for i := uint64(0); i < hdr.numFuncs; i++ { - if IsGo118orNewer(hdr.magic) { - //nolint:lll - // See: https://github.com/golang/go/blob/6df0957060b1315db4fd6a359eefc3ee92fcc198/src/debug/gosym/pclntab.go#L401-L413 - *fmap = pclntabFuncMap{} - funcIdx := uintptr(i) * 2 * fieldSize - fmap.pc = uint64(*(*uint32)(unsafe.Pointer(&functab[funcIdx]))) - fmap.funcOff = uint64(*(*uint32)(unsafe.Pointer(&functab[funcIdx+fieldSize]))) - fmap.pc += uint64(textStart) - } else { - fmap = (*pclntabFuncMap)(unsafe.Pointer(&functab[uintptr(i)*mapSize])) - } - // Get the function data - if uintptr(len(funcdata)) < uintptr(fmap.funcOff)+funSize { - return fmt.Errorf(".gopclntab func %v descriptor is invalid", i) - } - if IsGo118orNewer(hdr.magic) { - tmp := (*pclntabFunc118)(unsafe.Pointer(&funcdata[fmap.funcOff])) - *fun = pclntabFunc{ - startPc: uint64(textStart) + uint64(tmp.entryoff), - nameOff: tmp.nameOff, - argsSize: tmp.argsSize, - frameSize: tmp.argsSize, - pcspOff: tmp.pcspOff, - pcfileOff: tmp.pcfileOff, - pclnOff: tmp.pclnOff, - nfuncData: tmp.nfuncData, - npcData: tmp.npcData, - } - } else { - fun = (*pclntabFunc)(unsafe.Pointer(&funcdata[fmap.funcOff])) - } - // First, check for functions with special handling. - funcName := getString(funcnametab, int(fun.nameOff)) - if info, found := goFunctionsStopDelta[string(funcName)]; found { - ee.deltas.Add(sdtypes.StackDelta{ - Address: fun.startPc, - Info: *info, - }) - continue +// mapPcval steps the given pcval table until matching PC is found and returns the value. +func (g *Gopclntab) mapPcval(offs int32, startPc, pc uint) (int32, bool) { + p := g.getPcval(offs, startPc) + for pc >= p.pcEnd { + if ok := p.step(); !ok { + return 0, false } + } + return p.val, true +} - // Use source file to determine strategy if possible, and default - // to using frame pointers in the unlikely case of no file info - strategy := strategyFramePointer - if fun.pcfileOff != 0 { - p := newPcval(pctab[fun.pcfileOff:], uint(fun.startPc), hdr.quantum) - fileIndex := int(p.val) - if hdr.magic == magicGo1_16 || IsGo118orNewer(hdr.magic) { - fileIndex += int(fun.npcData) - } +// Symbolize returns the file, line and function information for given PC +func (g *Gopclntab) Symbolize(pc uintptr) (sourceFile string, line uint, funcName string) { + index := sort.Search(g.numFuncs, func(i int) bool { + funcPc, _ := g.getFuncMapEntry(i) + return funcPc > pc + }) - 1 + if index < 0 { + return "", 0, "" + } - // Determine strategy - strategy = sourceStrategy[fileIndex] - if strategy == strategyUnknown { - sourceFile := getString(filetab, getInt32(cutab, 4*fileIndex)) - strategy = getSourceFileStrategy(arch, sourceFile) - sourceStrategy[fileIndex] = strategy - } - } + mapPc, funcOff := g.getFuncMapEntry(index) + funcPc, fun := g.getFunc(funcOff) + if fun == nil || mapPc != funcPc { + return "", 0, "" + } - switch arch { - case elf.EM_X86_64: - if err := parseX86pclntabFunc(ee.deltas, fun, dataLen, pctab, strategy, i, - hdr.quantum); err != nil { - return err - } - case elf.EM_AARCH64: - if err := parseArm64pclntabFunc(ee.deltas, fun, dataLen, pctab, i, - hdr.quantum); err != nil { - return err + funcName = getString(g.funcnametab, int(fun.nameOff)) + if fun.pcfileOff != 0 { + if fileIndex, ok := g.mapPcval(fun.pcfileOff, uint(funcPc), uint(pc)); ok { + if g.version >= go1_16 { + fileIndex += fun.npcData } + sourceFile = getString(g.filetab, getInt32(g.cutab, 4*int(fileIndex))) } } - - // Filter out .gopclntab info from other sources - var start, end uintptr - if IsGo118orNewer(hdr.magic) { - //nolint:lll - // https://github.com/golang/go/blob/6df0957060b1315db4fd6a359eefc3ee92fcc198/src/debug/gosym/pclntab.go#L440-L450 - start = uintptr(*(*uint32)(unsafe.Pointer(&functab[0]))) - start += textStart - // From go12symtab document, reason for indexing beyond hdr.numFuncs: - // "The final pcN value is the address just beyond func(N-1), so that the binary - // search can distinguish between a pc inside func(N-1) and a pc outside the text - // segment." - end = uintptr(*(*uint32)(unsafe.Pointer(&functab[uintptr(hdr.numFuncs)*mapSize]))) - end += textStart - } else { - start = *(*uintptr)(unsafe.Pointer(&functab[0])) - end = *(*uintptr)(unsafe.Pointer(&functab[uintptr(hdr.numFuncs)*mapSize])) + if fun.pclnOff != 0 { + lineNo, _ := g.mapPcval(fun.pclnOff, uint(funcPc), uint(pc)) + line = uint(lineNo) } - ee.hooks.golangHook(start, end) + return sourceFile, line, funcName +} - // Add end of code indicator - ee.deltas.Add(sdtypes.StackDelta{ - Address: uint64(end), - Info: sdtypes.UnwindInfoInvalid, - }) +type strategy int - return nil +const ( + strategyUnknown strategy = iota + strategyFramePointer + strategyDeltasWithFrame + strategyDeltasWithoutFrame +) + +// noFPSourceSuffixes lists the go runtime source files that call assembly code +// which trashes RBP. These source files need to use explicit SP delta so that +// RBP can be recovered, and be then further used for frame pointer based unwinding. +// This lists the most notable problem cases from Go runtime. +// TODO(tteras) Go Runtime files calling internal.bytealg.Index* may need to be added here. +var noFPSourceSuffixes = []string{ + "/src/crypto/sha1/sha1.go", + "/src/crypto/sha256/sha256.go", + "/src/crypto/sha512/sha512.go", + "/src/crypto/elliptic/p256_asm.go", + "golang.org/x/crypto/curve25519/curve25519_amd64.go", + "golang.org/x/crypto/chacha20poly1305/chacha20poly1305_amd64.go", } -// parseX86pclntabFunc extracts interval information from x86_64 based pclntabFunc. -func parseX86pclntabFunc(deltas *sdtypes.StackDeltaArray, fun *pclntabFunc, dataLen uintptr, - pctab []byte, strategy int, i uint64, quantum uint8) error { - switch { - case strategy == strategyFramePointer: - // Use stack frame-pointer delta - deltas.Add(sdtypes.StackDelta{ - Address: fun.startPc, - Info: sdtypes.UnwindInfoFramePointerX64, - }) - return nil - case fun.pcspOff != 0: - // Generate stack deltas as the information is available - if dataLen < uintptr(fun.pcspOff) { - return fmt.Errorf(".gopclntab func %v pcscOff (%d) is invalid", - i, fun.pcspOff) +// getSourceFileStrategy categorizes sourceFile's unwinding strategy based on its name +func getSourceFileStrategy(arch elf.Machine, sourceFile string, defaultStrategy strategy) strategy { + switch arch { + case elf.EM_X86_64: + // Most of the assembly code needs explicit SP delta as they do not + // create stack frame. Do not recover RBP as it is not modified. + if strings.HasSuffix(sourceFile, ".s") { + return strategyDeltasWithoutFrame } - - p := newPcval(pctab[fun.pcspOff:], uint(fun.startPc), quantum) - hints := sdtypes.UnwindHintKeep - for ok := true; ok; ok = p.step() { - info := sdtypes.UnwindInfo{ - Opcode: sdtypes.UnwindOpcodeBaseSP, - Param: p.val + 8, - } - if strategy == strategyDeltasWithRBP && info.Param >= 16 { - info.FPOpcode = sdtypes.UnwindOpcodeBaseCFA - info.FPParam = -16 + // Check for the Go source files needing SP delta unwinding to recover RBP + for _, suffix := range noFPSourceSuffixes { + if strings.HasSuffix(sourceFile, suffix) { + return strategyDeltasWithFrame } - deltas.Add(sdtypes.StackDelta{ - Address: uint64(p.pcStart), - Hints: hints, - Info: info, - }) - hints = sdtypes.UnwindHintNone } + return defaultStrategy + default: + return defaultStrategy } - log.Debugf("Unhandled .gopclntab func at %d", i) - return nil } -// parseArm64pclntabFunc extracts interval information from ARM64 based pclntabFunc. -func parseArm64pclntabFunc(deltas *sdtypes.StackDeltaArray, fun *pclntabFunc, - dataLen uintptr, pctab []byte, i uint64, quantum uint8) error { - if fun.pcspOff == 0 { - // Some CGO functions don't have PCSP info: skip them. - return nil - } - if dataLen < uintptr(fun.pcspOff) { - return fmt.Errorf(".gopclntab func %v pcspOff = %d is invalid", i, fun.pcspOff) +// parseX86pclntabFunc extracts interval information from x86_64 based pclntabFunc. +func parseX86pclntabFunc(deltas *sdtypes.StackDeltaArray, p pcval, s strategy) error { + hints := sdtypes.UnwindHintKeep + for ok := true; ok; ok = p.step() { + info := sdtypes.UnwindInfo{ + Opcode: support.UnwindOpcodeBaseSP, + Param: p.val + 8, + } + if s == strategyDeltasWithFrame && info.Param >= 16 { + info.FPOpcode = support.UnwindOpcodeBaseCFA + info.FPParam = -16 + } + deltas.Add(sdtypes.StackDelta{ + Address: uint64(p.pcStart), + Hints: hints, + Info: info, + }) + hints = sdtypes.UnwindHintNone } + return nil +} - // On ARM64, frame pointers are not properly kept when the Go runtime copies the stack during - // `runtime.morestack` calls: all old frame pointers are set to 0. - // - // https://github.com/golang/go/blob/c318f191/src/runtime/stack.go#L676 - // - // We thus need to unwind with stack delta offsets. - +// parseArm64pclntabFunc extracts interval information from ARM64 based pclntabFunc. +func parseArm64pclntabFunc(deltas *sdtypes.StackDeltaArray, p pcval, s strategy) error { hint := sdtypes.UnwindHintKeep - p := newPcval(pctab[fun.pcspOff:], uint(fun.startPc), quantum) for ok := true; ok; ok = p.step() { var info sdtypes.UnwindInfo if p.val == 0 { @@ -655,11 +608,13 @@ func parseArm64pclntabFunc(deltas *sdtypes.StackDeltaArray, fun *pclntabFunc, // Regular basic block in the function body: unwind via SP. info = sdtypes.UnwindInfo{ // Unwind via SP offset. - Opcode: sdtypes.UnwindOpcodeBaseSP, + Opcode: support.UnwindOpcodeBaseSP, Param: p.val, + } + if s == strategyDeltasWithFrame { // On ARM64, the previous LR value is stored to top-of-stack. - FPOpcode: sdtypes.UnwindOpcodeBaseSP, - FPParam: 0, + info.FPOpcode = support.UnwindOpcodeBaseSP + info.FPParam = 0 } } @@ -674,3 +629,127 @@ func parseArm64pclntabFunc(deltas *sdtypes.StackDeltaArray, fun *pclntabFunc, return nil } + +// Parse Golang .gopclntab spdelta tables and try to produce minified intervals +// by using large frame pointer ranges when possible +func (ee *elfExtractor) parseGoPclntab() error { + g, err := NewGopclntab(ee.file) + if g == nil || err != nil { + return err + } + defer g.Close() + + // Go uses frame-pointers by default since Go 1.7, but unfortunately + // it is not necessarily available when in code from non-Golang source + // files, such as the assembly, of the Go runtime. + // Since Golang binaries are huge statically compiled executables and + // would fill up our precious kernel delta maps fast, the strategy is to + // create deltastack maps for non-Go source files only, and otherwise + // cover the vast majority with "use frame pointer" stack delta. + sourceStrategy := make(map[int]strategy) + + // Get target machine architecture for the ELF file + arch := ee.file.Machine + defaultStrategy := strategyFramePointer + var parsePclntab func(deltas *sdtypes.StackDeltaArray, p pcval, s strategy) error + + switch arch { + case elf.EM_X86_64: + parsePclntab = parseX86pclntabFunc + case elf.EM_AARCH64: + parsePclntab = parseArm64pclntabFunc + // Go 1.20 and earlier did not maintain frame pointers properly on arm64. + // This was fixed for Go 1.21 and later in: + // https://github.com/golang/go/commit/a41a29ad19c25c3475a65b7265fcad870d954c2a + switch g.version { + case go1_2, go1_16, go1_18: + // Magic indicates old Go with broken arm64 frame pointers + defaultStrategy = strategyDeltasWithFrame + case go1_20: + // Ambiguous regarding if frame pointer is kept correctly. + // Take the slow path of resolving Go version. + goVer, err := ee.file.GoVersion() + if err != nil || version.Compare(goVer, "go1.21rc1") < 0 { + defaultStrategy = strategyDeltasWithFrame + } + } + default: + return fmt.Errorf("unsupported ELF architecture (%x)", arch) + } + + // Iterate the golang PC to function lookup table (sorted by PC) + for i := 0; i < g.numFuncs; i++ { + mapPc, funcOff := g.getFuncMapEntry(i) + funcPc, fun := g.getFunc(funcOff) + if fun == nil || mapPc != funcPc { + return fmt.Errorf(".gopclntab func %v descriptor is invalid (pc %x/%x)", + i, mapPc, funcPc) + } + + // First, check for functions with special handling. + funcName := getString(g.funcnametab, int(fun.nameOff)) + if info, found := goFunctionsStopDelta[funcName]; found { + ee.deltas.Add(sdtypes.StackDelta{ + Address: uint64(funcPc), + Info: *info, + }) + continue + } + + // Use source file to determine strategy if possible, and default + // to using frame pointers in the unlikely case of no file info + fileStrategy := defaultStrategy + if fun.pcfileOff != 0 { + p := g.getPcval(fun.pcfileOff, uint(funcPc)) + fileIndex := int(p.val) + if g.version >= go1_16 { + fileIndex += int(fun.npcData) + } + + // Determine strategy + fileStrategy = sourceStrategy[fileIndex] + if fileStrategy == strategyUnknown { + sourceFile := getString(g.filetab, getInt32(g.cutab, 4*fileIndex)) + fileStrategy = getSourceFileStrategy(arch, sourceFile, defaultStrategy) + sourceStrategy[fileIndex] = fileStrategy + } + } + + if fileStrategy == strategyFramePointer { + // Use stack frame-pointer delta + ee.deltas.Add(sdtypes.StackDelta{ + Address: uint64(funcPc), + Info: sdtypes.UnwindInfoFramePointer, + }) + continue + } + + if fun.pcspOff == 0 { + // Some functions don't have PCSP info: skip them. + continue + } + + // Generate stack deltas as the information is available + if len(g.pctab) < int(fun.pcspOff) { + return fmt.Errorf(".gopclntab func %v pcscOff (%d) is invalid", + i, fun.pcspOff) + } + p := g.getPcval(fun.pcspOff, uint(funcPc)) + if err := parsePclntab(ee.deltas, p, fileStrategy); err != nil { + return err + } + } + + // Filter out .gopclntab info from other sources + start, _ := g.getFuncMapEntry(0) + end, _ := g.getFuncMapEntry(g.numFuncs) + ee.hooks.golangHook(start, end) + + // Add end of code indicator + ee.deltas.Add(sdtypes.StackDelta{ + Address: uint64(end), + Info: sdtypes.UnwindInfoInvalid, + }) + + return nil +} diff --git a/nativeunwind/elfunwindinfo/elfgopclntab_test.go b/nativeunwind/elfunwindinfo/elfgopclntab_test.go index 308698265..84b372528 100644 --- a/nativeunwind/elfunwindinfo/elfgopclntab_test.go +++ b/nativeunwind/elfunwindinfo/elfgopclntab_test.go @@ -53,16 +53,16 @@ func TestPcvalInvalid(_ *testing.T) { // Some strategy tests func TestGoStrategy(t *testing.T) { res := []struct { - file string - strategy int + file string + result strategy }{ {"foo.go", strategyFramePointer}, - {"foo.s", strategyDeltasWithoutRBP}, - {"go/src/crypto/elliptic/p256_asm.go", strategyDeltasWithRBP}, + {"foo.s", strategyDeltasWithoutFrame}, + {"go/src/crypto/elliptic/p256_asm.go", strategyDeltasWithFrame}, } for _, x := range res { - s := getSourceFileStrategy(elf.EM_X86_64, []byte(x.file)) - assert.Equal(t, x.strategy, s) + s := getSourceFileStrategy(elf.EM_X86_64, x.file, strategyFramePointer) + assert.Equal(t, x.result, s) } } diff --git a/nativeunwind/elfunwindinfo/stackdeltaextraction.go b/nativeunwind/elfunwindinfo/stackdeltaextraction.go index c1cf8758e..324c89081 100644 --- a/nativeunwind/elfunwindinfo/stackdeltaextraction.go +++ b/nativeunwind/elfunwindinfo/stackdeltaextraction.go @@ -27,6 +27,13 @@ type extractionFilter struct { // should be excluded from .eh_frame extraction. start, end uintptr + // entryStart and entryEnd contain the virtual address for the entry + // stub code with synthesized stack deltas. + entryStart, entryEnd uintptr + + // entryPending is true if the entry stub stack delta has not been added. + entryPending bool + // ehFrames is true if .eh_frame stack deltas are found ehFrames bool @@ -39,23 +46,47 @@ type extractionFilter struct { var _ ehframeHooks = &extractionFilter{} +// addEntryDeltas generates the entry stub stack deltas. +func (f *extractionFilter) addEntryDeltas(deltas *sdtypes.StackDeltaArray) { + deltas.AddEx(sdtypes.StackDelta{ + Address: uint64(f.entryStart), + Hints: sdtypes.UnwindHintKeep, + Info: sdtypes.UnwindInfoStop, + }, !f.unsortedFrames) + deltas.Add(sdtypes.StackDelta{ + Address: uint64(f.entryEnd), + Info: sdtypes.UnwindInfoInvalid, + }) + f.ehFrames = true + f.entryPending = false +} + +func (f *extractionFilter) fdeUnsorted() { + f.unsortedFrames = true +} + // fdeHook filters out .eh_frame data that is superseded by .gopclntab data -func (f *extractionFilter) fdeHook(_ *cieInfo, fde *fdeInfo) bool { - if !fde.sorted { - // Seems .debug_frame sometimes has broken FDEs for zero address - if fde.ipStart == 0 { - return false - } - f.unsortedFrames = true +func (f *extractionFilter) fdeHook(_ *cieInfo, fde *fdeInfo, deltas *sdtypes.StackDeltaArray) bool { + // Drop FDEs inside the gopclntab area + if f.start <= fde.ipStart && fde.ipStart+fde.ipLen <= f.end { + return false } - // Parse functions outside the gopclntab area - if fde.ipStart < f.start || fde.ipStart > f.end { - // This is here to set the flag only when we have collected at least - // one stack delta from the relevant source. - f.ehFrames = true - return true + // Seems .debug_frame sometimes has broken FDEs for zero address + if f.unsortedFrames && fde.ipStart == 0 { + return false } - return false + // Insert entry stub deltas to their sorted position. + if f.entryPending && fde.ipStart >= f.entryStart { + f.addEntryDeltas(deltas) + } + // Drop FDEs overlapping with the detected entry stub. + if fde.ipStart+fde.ipLen > f.entryStart && f.entryEnd >= fde.ipStart { + return false + } + // This is here to set the flag only when we have collected at least + // one stack delta from the relevant source. + f.ehFrames = true + return true } // deltaHook is a stub to satisfy ehframeHooks interface @@ -85,16 +116,14 @@ type elfExtractor struct { allowGenericRegs bool } -func (ee *elfExtractor) extractDebugDeltas() error { - var err error - +func (ee *elfExtractor) extractDebugDeltas() (err error) { // Attempt finding the associated debug information file with .debug_frame, // but ignore errors if it's not available; many production systems // do not intentionally have debug packages installed. debugELF, _ := ee.file.OpenDebugLink(ee.ref.FileName(), ee.ref) if debugELF != nil { err = ee.parseDebugFrame(debugELF) - debugELF.Close() + _ = debugELF.Close() } return err } @@ -115,6 +144,32 @@ func Extract(filename string, interval *sdtypes.IntervalData) error { return ExtractELF(elfRef, interval) } +// detectEntryCode matches machine code for known entry stubs, and detects its length. +func detectEntryCode(machine elf.Machine, code []byte) int { + switch machine { + case elf.EM_X86_64: + return detectEntryX86(code) + case elf.EM_AARCH64: + return detectEntryARM(code) + default: + return 0 + } +} + +// detectEntry loads the entry stub from the ELF DSO entry and matches it. +func detectEntry(ef *pfelf.File) int { + if ef.Entry == 0 { + return 0 + } + + // Typically 52-80 bytes, allow for a bit of variance + code, err := ef.VirtualMemory(int64(ef.Entry), 128, 128) + if err != nil { + return 0 + } + return detectEntryCode(ef.Machine, code) +} + // ExtractELF takes a pfelf.Reference and provides the stack delta // intervals for it in the interval parameter. func ExtractELF(elfRef *pfelf.Reference, interval *sdtypes.IntervalData) error { @@ -122,7 +177,13 @@ func ExtractELF(elfRef *pfelf.Reference, interval *sdtypes.IntervalData) error { if err != nil { return err } + return extractFile(elfFile, elfRef, interval) +} +// extractFile extracts the elfFile stack deltas and uses the optional elfRef to resolve +// debug link references if needed. +func extractFile(elfFile *pfelf.File, elfRef *pfelf.Reference, + interval *sdtypes.IntervalData) (err error) { // Parse the stack deltas from the ELF filter := extractionFilter{} deltas := sdtypes.StackDeltaArray{} @@ -134,6 +195,12 @@ func ExtractELF(elfRef *pfelf.Reference, interval *sdtypes.IntervalData) error { allowGenericRegs: isLibCrypto(elfFile), } + if entryLength := detectEntry(elfFile); entryLength != 0 { + filter.entryStart = uintptr(elfFile.Entry) + filter.entryEnd = filter.entryStart + uintptr(entryLength) + filter.entryPending = true + } + if err = ee.parseGoPclntab(); err != nil { return fmt.Errorf("failure to parse golang stack deltas: %v", err) } @@ -143,13 +210,16 @@ func ExtractELF(elfRef *pfelf.Reference, interval *sdtypes.IntervalData) error { if err = ee.parseDebugFrame(elfFile); err != nil { return fmt.Errorf("failure to parse debug_frame stack deltas: %v", err) } - if len(deltas) < numIntervalsToOmitDebugLink { + if ee.ref != nil && len(deltas) < numIntervalsToOmitDebugLink { // There is only few stack deltas. See if we find the .gnu_debuglink // debug information for additional .debug_frame stack deltas. if err = ee.extractDebugDeltas(); err != nil { return fmt.Errorf("failure to parse debug stack deltas: %v", err) } } + if filter.entryPending { + filter.addEntryDeltas(ee.deltas) + } // If multiple sources were merged, sort them. if filter.unsortedFrames || (filter.ehFrames && filter.golangFrames) { @@ -157,9 +227,12 @@ func ExtractELF(elfRef *pfelf.Reference, interval *sdtypes.IntervalData) error { if deltas[i].Address != deltas[j].Address { return deltas[i].Address < deltas[j].Address } - // Make sure that the potential duplicate stop delta is sorted - // after the real delta. - return deltas[i].Info.Opcode < deltas[j].Info.Opcode + // Make sure that the potential duplicate "invalid" delta is sorted + // after the real delta so the proper delta is removed in next stage. + if deltas[i].Info.Opcode != deltas[j].Info.Opcode { + return deltas[i].Info.Opcode < deltas[j].Info.Opcode + } + return deltas[i].Info.Param < deltas[j].Info.Param }) maxDelta := 0 diff --git a/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go b/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go index 04738ce83..961f67833 100644 --- a/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go +++ b/nativeunwind/elfunwindinfo/stackdeltaextraction_test.go @@ -4,13 +4,15 @@ package elfunwindinfo import ( + "bytes" + "debug/elf" "encoding/base64" - "os" "testing" - sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" - + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" + sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" ) // Base64-encoded data from /usr/bin/volname on a stock debian box, the smallest @@ -134,26 +136,166 @@ var firstDeltas = sdtypes.StackDeltaArray{ {Address: 0x8e7, Info: deltaRSP(80, 16)}, } -func TestExtractStackDeltasFromFilename(t *testing.T) { +func getUsrBinPfelf() (*pfelf.File, error) { buffer, err := base64.StdEncoding.DecodeString(usrBinVolname) + if err != nil { + return nil, err + } + return pfelf.NewFile(bytes.NewReader(buffer), 0, false) +} + +func TestExtractStackDeltasFromFilename(t *testing.T) { + elf, err := getUsrBinPfelf() require.NoError(t, err) - // Write the executable file to a temporary file, and the symbol - // file, too. - exeFile, err := os.CreateTemp("/tmp", "dwarf_extract_elf_") - require.NoError(t, err) - defer exeFile.Close() - _, err = exeFile.Write(buffer) - require.NoError(t, err) - err = exeFile.Sync() - require.NoError(t, err) - defer os.Remove(exeFile.Name()) - filename := exeFile.Name() var data sdtypes.IntervalData - err = Extract(filename, &data) + err = extractFile(elf, nil, &data) require.NoError(t, err) for _, delta := range data.Deltas { t.Logf("%#v", delta) } require.Equal(t, data.Deltas[:len(firstDeltas)], firstDeltas) } + +func TestEntryDetection(t *testing.T) { + testCases := map[string]struct { + machine elf.Machine + code []byte + len int + }{ + "musl 1.2.5 / x86_64": { + machine: elf.EM_X86_64, + code: []byte{ + // 1. assembly code from crt_arch.h (no FDE at all): + // 48 31 ed xor %rbp,%rbp + // 48 89 e7 mov %rsp,%rdi + // 48 8d 35 b2 c2 00 00 lea 0xc2b2(%rip),%rsi + // 48 83 e4 f0 and $0xfffffffffffffff0,%rsp + // e8 00 00 00 00 call 0x4587 + // 2. followed with C code from [r]crt1.c (maybe with FDE): + // 8b 37 mov (%rdi),%esi + // 48 8d 57 08 lea 0x8(%rdi),%rdx + // 4c 8d 05 d0 62 00 00 lea 0x62d0(%rip),%r8 + // 45 31 c9 xor %r9d,%r9d + // 48 8d 0d 62 fa ff ff lea -0x59e(%rip),%rcx + // 48 8d 3d 8b fa ff ff lea -0x575(%rip),%rdi + // e9 76 fa ff ff jmp 0x4020 <__libc_start_main@plt> + 0x48, 0x31, 0xed, 0x48, 0x89, 0xe7, 0x48, 0x8d, + 0x35, 0xb2, 0xc2, 0x00, 0x00, 0x48, 0x83, 0xe4, + 0xf0, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x37, + 0x48, 0x8d, 0x57, 0x08, 0x4c, 0x8d, 0x05, 0xd0, + 0x62, 0x00, 0x00, 0x45, 0x31, 0xc9, 0x48, 0x8d, + 0x0d, 0x62, 0xfa, 0xff, 0xff, 0x48, 0x8d, 0x3d, + 0x8b, 0xfa, 0xff, 0xff, 0xe9, 0x76, 0xfa, 0xff, + 0xff, + }, + len: 57, + }, + "musl 1.2.5 / arm64": { + machine: elf.EM_AARCH64, + code: []byte{ + // 1. assembly code from crt_arch.h (no FDE): + // mov x29, #0x0 + // mov x30, #0x0 + // mov x0, sp + // adrp x1, 0x1f000 + // add x1, x1, #0x7d0 + // and sp, x0, #0xfffffffffffffff0 + // b 0x413c + // 2. followed with C code from [r]crt1.c (no FDE): + // mov x2, x0 + // mov x5, #0x0 + // adrp x4, 0x1f000 + // ldr x4, [x4, #3928] + // ldr x1, [x2], #8 + // adrp x3, 0x1f000 + // ldr x3, [x3, #4080] + // adrp x0, 0x1f000 + // ldr x0, [x0, #4072] + // b 0x35a0 <__libc_start_main@plt> + 0x1d, 0x00, 0x80, 0xd2, 0x1e, 0x00, 0x80, 0xd2, + 0xe0, 0x03, 0x00, 0x91, 0xc1, 0x00, 0x00, 0xf0, + 0x21, 0x40, 0x1f, 0x91, 0x1f, 0xec, 0x9c, 0x92, + 0x01, 0x00, 0x00, 0x14, 0xe2, 0x03, 0x00, 0xaa, + 0x05, 0x00, 0x80, 0xd2, 0xc3, 0x00, 0x00, 0xf0, + 0x84, 0xac, 0x47, 0xf9, 0x41, 0x84, 0x40, 0xf8, + 0xc3, 0x00, 0x00, 0xf0, 0x63, 0xf8, 0x47, 0xf9, + 0xc0, 0x00, 0x00, 0xf0, 0x00, 0xf4, 0x47, 0xf9, + 0x10, 0xfd, 0xff, 0x17, + }, + len: 68, + }, + "glibc 2.31 / arm64": { + machine: elf.EM_AARCH64, + code: []byte{ + // mov x29, #0x0 + // mov x30, #0x0 + // mov x5, x0 + // ldr x1, [sp] + // add x2, sp, #0x8 + // mov x6, sp + // adrp x0, 0x11000 + // ldr x0, [x0, #4064] + // adrp x3, 0x11000 + // ldr x3, [x3, #4056] + // adrp x4, 0x11000 + // ldr x4, [x4, #4008] + // bl 0xa90 <__libc_start_main@plt> + // bl 0xae0 + 0x1d, 0x00, 0x80, 0xd2, 0x1e, 0x00, 0x80, 0xd2, + 0xe5, 0x03, 0x00, 0xaa, 0xe1, 0x03, 0x40, 0xf9, + 0xe2, 0x23, 0x00, 0x91, 0xe6, 0x03, 0x00, 0x91, + 0x80, 0x00, 0x00, 0xb0, 0x00, 0xf0, 0x47, 0xf9, + 0x83, 0x00, 0x00, 0xb0, 0x63, 0xec, 0x47, 0xf9, + 0x84, 0x00, 0x00, 0xb0, 0x84, 0xd4, 0x47, 0xf9, + 0xab, 0xff, 0xff, 0x97, 0xbe, 0xff, 0xff, 0x97, + }, + len: 56, + }, + "glibc 2.35 / arm64": { + machine: elf.EM_AARCH64, + code: []byte{ + // mov x29, #0x0 + // mov x30, #0x0 + // mov x5, x0 + // ldr x1, [sp] + // add x2, sp, #0x8 + // mov x6, sp + // movz x0, #0x0, lsl #48 + // movk x0, #0x0, lsl #32 + // movk x0, #0xb9, lsl #16 + // movk x0, #0x1f90 + // movz x3, #0x0, lsl #48 + // movk x3, #0x0, lsl #32 + // movk x3, #0x236, lsl #16 + // movk x3, #0x65d0 + // movz x4, #0x0, lsl #48 + // movk x4, #0x0, lsl #32 + // movk x4, #0x236, lsl #16 + // movk x4, #0x6650 + // bl 0xb614e0 <__libc_start_main@plt> + // bl 0xb61460 + 0x1d, 0x00, 0x80, 0xd2, 0x1e, 0x00, 0x80, 0xd2, + 0xe5, 0x03, 0x00, 0xaa, 0xe1, 0x03, 0x40, 0xf9, + 0xe2, 0x23, 0x00, 0x91, 0xe6, 0x03, 0x00, 0x91, + 0x00, 0x00, 0xe0, 0xd2, 0x00, 0x00, 0xc0, 0xf2, + 0x20, 0x17, 0xa0, 0xf2, 0x00, 0xf2, 0x83, 0xf2, + 0x03, 0x00, 0xe0, 0xd2, 0x03, 0x00, 0xc0, 0xf2, + 0xc3, 0x46, 0xa0, 0xf2, 0x03, 0xba, 0x8c, 0xf2, + 0x04, 0x00, 0xe0, 0xd2, 0x04, 0x00, 0xc0, 0xf2, + 0xc4, 0x46, 0xa0, 0xf2, 0x04, 0xca, 0x8c, 0xf2, + 0x7d, 0x1c, 0xff, 0x97, 0x5c, 0x1c, 0xff, 0x97, + }, + len: 80, + }, + } + + for name, test := range testCases { + name := name + test := test + t.Run(name, func(t *testing.T) { + entryLen := detectEntryCode(test.machine, test.code) + assert.Equal(t, test.len, entryLen) + }) + } +} diff --git a/nativeunwind/stackdeltatypes/stackdeltatypes.go b/nativeunwind/stackdeltatypes/stackdeltatypes.go index d91e3d3dd..7b9f2722e 100644 --- a/nativeunwind/stackdeltatypes/stackdeltatypes.go +++ b/nativeunwind/stackdeltatypes/stackdeltatypes.go @@ -6,8 +6,9 @@ // stack delta information that is used in all relevant packages. package stackdeltatypes // import "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" -// #include "../../support/ebpf/stackdeltatypes.h" -import "C" +import ( + "go.opentelemetry.io/ebpf-profiler/support" +) const ( // ABI is the current binary compatibility version. It is incremented @@ -19,25 +20,6 @@ const ( // in order to keep the created STOP stack delta between functions MinimumGap = 15 - // UnwindOpcodes from the C header file - UnwindOpcodeCommand uint8 = C.UNWIND_OPCODE_COMMAND - UnwindOpcodeBaseCFA uint8 = C.UNWIND_OPCODE_BASE_CFA - UnwindOpcodeBaseSP uint8 = C.UNWIND_OPCODE_BASE_SP - UnwindOpcodeBaseFP uint8 = C.UNWIND_OPCODE_BASE_FP - UnwindOpcodeBaseLR uint8 = C.UNWIND_OPCODE_BASE_LR - UnwindOpcodeBaseReg uint8 = C.UNWIND_OPCODE_BASE_REG - UnwindOpcodeFlagDeref uint8 = C.UNWIND_OPCODEF_DEREF - - // UnwindCommands from the C header file - UnwindCommandInvalid int32 = C.UNWIND_COMMAND_INVALID - UnwindCommandStop int32 = C.UNWIND_COMMAND_STOP - UnwindCommandPLT int32 = C.UNWIND_COMMAND_PLT - UnwindCommandSignal int32 = C.UNWIND_COMMAND_SIGNAL - - // UnwindDeref handling from the C header file - UnwindDerefMask int32 = C.UNWIND_DEREF_MASK - UnwindDerefMultiplier int32 = C.UNWIND_DEREF_MULTIPLIER - // UnwindHintNone indicates that no flags are set. UnwindHintNone uint8 = 0 // UnwindHintKeep flags important intervals that should not be removed @@ -55,26 +37,27 @@ type UnwindInfo struct { } // UnwindInfoInvalid is the stack delta info indicating invalid or unsupported PC. -var UnwindInfoInvalid = UnwindInfo{Opcode: UnwindOpcodeCommand, Param: UnwindCommandInvalid} +var UnwindInfoInvalid = UnwindInfo{Opcode: support.UnwindOpcodeCommand, + Param: support.UnwindCommandInvalid} // UnwindInfoStop is the stack delta info indicating root function of a stack. -var UnwindInfoStop = UnwindInfo{Opcode: UnwindOpcodeCommand, Param: UnwindCommandStop} +var UnwindInfoStop = UnwindInfo{Opcode: support.UnwindOpcodeCommand, + Param: support.UnwindCommandStop} // UnwindInfoSignal is the stack delta info indicating signal return frame. -var UnwindInfoSignal = UnwindInfo{Opcode: UnwindOpcodeCommand, Param: UnwindCommandSignal} - -// UnwindInfoFramePointerX64 contains the description to unwind a x86-64 frame pointer frame. -var UnwindInfoFramePointerX64 = UnwindInfo{ - Opcode: UnwindOpcodeBaseFP, - Param: 16, - FPOpcode: UnwindOpcodeBaseCFA, - FPParam: -16, +var UnwindInfoSignal = UnwindInfo{Opcode: support.UnwindOpcodeCommand, + Param: support.UnwindCommandSignal} + +// UnwindInfoFramePointer contains the description to unwind a frame pointer frame. +var UnwindInfoFramePointer = UnwindInfo{ + Opcode: support.UnwindOpcodeCommand, + Param: support.UnwindCommandFramePointer, } // UnwindInfoLR contains the description to unwind ARM64 function without a frame (LR only) var UnwindInfoLR = UnwindInfo{ - Opcode: UnwindOpcodeBaseSP, - FPOpcode: UnwindOpcodeBaseLR, + Opcode: support.UnwindOpcodeBaseSP, + FPOpcode: support.UnwindOpcodeBaseLR, } // StackDelta defines the start address for the delta interval, along with @@ -100,12 +83,12 @@ type IntervalData struct { // AddEx adds a new stack delta to the array. func (deltas *StackDeltaArray) AddEx(delta StackDelta, sorted bool) { num := len(*deltas) - if delta.Info.Opcode == UnwindOpcodeCommand { + if delta.Info.Opcode == support.UnwindOpcodeCommand { // FP information is invalid/unused for command opcodes. // But DWARF info often leaves bogus data there, so resetting it // reduces the number of unique Info contents generated. - delta.Info.FPOpcode = UnwindOpcodeCommand - delta.Info.FPParam = UnwindCommandInvalid + delta.Info.FPOpcode = support.UnwindOpcodeCommand + delta.Info.FPParam = support.UnwindCommandInvalid } if num > 0 && sorted { prev := &(*deltas)[num-1] @@ -140,13 +123,15 @@ func (deltas *StackDeltaArray) Add(delta StackDelta) { // PackDerefParam compresses pre- and post-dereference parameters to single value func PackDerefParam(preDeref, postDeref int32) (int32, bool) { - if postDeref < 0 || postDeref > 0x20 || postDeref%UnwindDerefMultiplier != 0 { + if postDeref < 0 || postDeref > 0x20 || + postDeref%support.UnwindDerefMultiplier != 0 { return 0, false } - return preDeref + postDeref/UnwindDerefMultiplier, true + return preDeref + postDeref/support.UnwindDerefMultiplier, true } // UnpackDerefParam splits the pre- and post-dereference parameters from single value func UnpackDerefParam(param int32) (preDeref, postDeref int32) { - return param &^ UnwindDerefMask, (param & UnwindDerefMask) * UnwindDerefMultiplier + return param &^ support.UnwindDerefMask, + (param & support.UnwindDerefMask) * support.UnwindDerefMultiplier } diff --git a/proc/proc.go b/proc/proc.go deleted file mode 100644 index b178e92f9..000000000 --- a/proc/proc.go +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// Package proc provides functionality for retrieving kallsyms, modules and -// executable mappings via /proc. -package proc // import "go.opentelemetry.io/ebpf-profiler/proc" - -import ( - "bufio" - "errors" - "fmt" - "os" - "strconv" - "strings" - - log "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" - - "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/stringutil" -) - -const defaultMountPoint = "/proc" - -// GetKallsyms returns SymbolMap for kernel symbols from /proc/kallsyms. -func GetKallsyms(kallsymsPath string) (*libpf.SymbolMap, error) { - var address uint64 - var symbol string - - // As an example, the Debian 6.10.11 kernel has ~180k text symbols. - symmap := libpf.NewSymbolMap(200 * 1024) - noSymbols := true - - file, err := os.Open(kallsymsPath) - if err != nil { - return nil, fmt.Errorf("unable to open %s: %v", kallsymsPath, err) - } - defer file.Close() - - var scanner = bufio.NewScanner(file) - for scanner.Scan() { - // Avoid heap allocation by not using scanner.Text(). - // NOTE: The underlying bytes will change with the next call to scanner.Scan(), - // so make sure to not keep any references after the end of the loop iteration. - line := stringutil.ByteSlice2String(scanner.Bytes()) - - // Avoid heap allocations here - do not use strings.FieldsN() - var fields [4]string - nFields := stringutil.FieldsN(line, fields[:]) - - if nFields < 3 { - return nil, fmt.Errorf("unexpected line in kallsyms: '%s'", line) - } - - // Skip non-text symbols, see 'man nm'. - // Special case for 'etext', which can be of type `D` (data) in some kernels. - if strings.IndexByte("TtVvWwA", fields[1][0]) == -1 && fields[2] != "_etext" { - continue - } - - if address, err = strconv.ParseUint(fields[0], 16, 64); err != nil { - return nil, fmt.Errorf("failed to parse address value: '%s'", fields[0]) - } - - if address != 0 { - noSymbols = false - } - - symbol = strings.Clone(fields[2]) - - symmap.Add(libpf.Symbol{ - Name: libpf.SymbolName(symbol), - Address: libpf.SymbolValue(address), - }) - } - symmap.Finalize() - - if noSymbols { - return nil, errors.New( - "all addresses from kallsyms are zero - check process permissions") - } - - return symmap, nil -} - -// GetKernelModules returns SymbolMap for kernel modules from /proc/modules. -func GetKernelModules(modulesPath string, - kernelSymbols *libpf.SymbolMap) (*libpf.SymbolMap, error) { - symmap := libpf.SymbolMap{} - - file, err := os.Open(modulesPath) - if err != nil { - return nil, fmt.Errorf("unable to open %s: %v", modulesPath, err) - } - defer file.Close() - - stext, err := kernelSymbols.LookupSymbol("_stext") - if err != nil { - return nil, fmt.Errorf("unable to find kernel text section start: %v", err) - } - etext, err := kernelSymbols.LookupSymbol("_etext") - if err != nil { - return nil, fmt.Errorf("unable to find kernel text section end: %v", err) - } - log.Debugf("Found KERNEL TEXT at %x-%x", stext.Address, etext.Address) - symmap.Add(libpf.Symbol{ - Name: "vmlinux", - Address: stext.Address, - Size: uint64(etext.Address - stext.Address), - }) - - modules, err := parseKernelModules(bufio.NewScanner(file)) - if err != nil { - return nil, fmt.Errorf("failed to parse kernel modules: %v", err) - } - - for _, kmod := range modules { - symmap.Add(libpf.Symbol{ - Name: libpf.SymbolName(kmod.name), - Address: libpf.SymbolValue(kmod.address), - Size: kmod.size, - }) - } - - symmap.Finalize() - - return &symmap, nil -} - -func parseKernelModules(scanner *bufio.Scanner) ([]kernelModule, error) { - var ( - modules []kernelModule - atLeastOneValidAddress = false - count = 0 - ) - - for scanner.Scan() { - line := scanner.Text() - - count++ - - kmod, err := parseKernelModuleLine(line) - if err != nil { - return nil, fmt.Errorf("failed to parse kernel module line '%s': %v", line, err) - } - if kmod.address == 0 { - continue - } - atLeastOneValidAddress = true - - modules = append(modules, kmod) - } - - if count > 0 && !atLeastOneValidAddress { - return nil, errors.New("addresses from all modules is zero - check process permissions") - } - - return modules, nil -} - -type kernelModule struct { - name string - size uint64 - address uint64 -} - -func parseKernelModuleLine(line string) (kernelModule, error) { - // The format is: "name size refcount dependencies state address" - // The string is split into 7 parts as after address there can be an optional string. - parts := strings.SplitN(line, " ", 7) - if len(parts) < 6 { - return kernelModule{}, fmt.Errorf("unexpected line in modules: '%s'", line) - } - - size, err := parseSize(parts[1]) - if err != nil { - return kernelModule{}, err - } - - address, err := parseAddress(parts[5]) - if err != nil { - return kernelModule{}, err - } - - return kernelModule{ - name: parts[0], - size: size, - address: address, - }, nil -} - -func parseAddress(addressStr string) (uint64, error) { - address, err := strconv.ParseUint(strings.TrimPrefix(addressStr, "0x"), 16, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse address '%s' as hex value: %v", - addressStr, err) - } - - return address, nil -} - -func parseSize(sizeStr string) (uint64, error) { - size, err := strconv.ParseUint(sizeStr, 10, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse size int value: %q: %v", sizeStr, err) - } - - return size, nil -} - -// IsPIDLive checks if a PID belongs to a live process. It will never produce a false negative but -// may produce a false positive (e.g. due to permissions) in which case an error will also be -// returned. -func IsPIDLive(pid libpf.PID) (bool, error) { - // A kill syscall with a 0 signal is documented to still do the check - // whether the process exists: https://linux.die.net/man/2/kill - err := unix.Kill(int(pid), 0) - if err == nil { - return true, nil - } - - var errno unix.Errno - if errors.As(err, &errno) { - switch errno { - case unix.ESRCH: - return false, nil - case unix.EPERM: - // continue with procfs fallback - default: - return true, err - } - } - - path := fmt.Sprintf("%s/%d/maps", defaultMountPoint, pid) - _, err = os.Stat(path) - - if err != nil && os.IsNotExist(err) { - return false, nil - } - - return true, err -} diff --git a/proc/proc_test.go b/proc/proc_test.go deleted file mode 100644 index 5d4bbc5a5..000000000 --- a/proc/proc_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package proc - -import ( - "bufio" - "bytes" - "testing" - - "go.opentelemetry.io/ebpf-profiler/libpf" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func assertSymbol(t *testing.T, symmap *libpf.SymbolMap, name libpf.SymbolName, - expectedAddress libpf.SymbolValue) { - sym, err := symmap.LookupSymbol(name) - require.NoError(t, err) - assert.Equal(t, expectedAddress, sym.Address) -} - -func TestParseKallSyms(t *testing.T) { - // Check parsing as if we were non-root - symmap, err := GetKallsyms("testdata/kallsyms_0") - require.Error(t, err) - require.Nil(t, symmap) - - // Check parsing invalid file - symmap, err = GetKallsyms("testdata/kallsyms_invalid") - require.Error(t, err) - require.Nil(t, symmap) - - // Happy case - symmap, err = GetKallsyms("testdata/kallsyms") - require.NoError(t, err) - require.NotNil(t, symmap) - - assertSymbol(t, symmap, "cpu_tss_rw", 0x6000) - assertSymbol(t, symmap, "hid_add_device", 0xffffffffc033e550) -} - -func TestParseKernelModules(t *testing.T) { - content := []byte(`i40e 589824 - - Live 0xffffffffc0321000 -mpt3sas 405504 - - Live 0xffffffffc02ab000 -ahci 45056 - - Live 0xffffffffc0294000 -libahci 49152 - - Live 0xffffffffc027f000 -sp5100_tco 12288 - - Live 0xffffffffc0274000 -watchdog 40960 - - Live 0xffffffffc025f000 -k10temp 12288 - - Live 0xffffffffc0254000`) - - kmods, err := parseKernelModules(bufio.NewScanner(bytes.NewReader(content))) - require.NoError(t, err) - - require.Len(t, kmods, 7) - require.Equal(t, []kernelModule{ - { - name: "i40e", - size: 589824, - address: 0xffffffffc0321000, - }, - { - name: "mpt3sas", - size: 405504, - address: 0xffffffffc02ab000, - }, - { - name: "ahci", - size: 45056, - address: 0xffffffffc0294000, - }, - { - name: "libahci", - size: 49152, - address: 0xffffffffc027f000, - }, - { - name: "sp5100_tco", - size: 12288, - address: 0xffffffffc0274000, - }, - { - name: "watchdog", - size: 40960, - address: 0xffffffffc025f000, - }, - { - name: "k10temp", - size: 12288, - address: 0xffffffffc0254000, - }, - }, kmods) -} - -func TestParseKernelModuleLine(t *testing.T) { - tests := map[string]struct { - line string - expected kernelModule - }{ - "i40e": { - line: "i40e 589824 - - Live 0xffffffffc0364000", - expected: kernelModule{ - name: "i40e", - size: 589824, - address: 0xffffffffc0364000, - }, - }, - "nvidia": { - line: "nvidia_drm 102400 2 - Live 0xffffffffc11c0000 (POE)", - expected: kernelModule{ - name: "nvidia_drm", - size: 102400, - address: 0xffffffffc11c0000, - }, - }, - } - - for name, test := range tests { - name := name - test := test - t.Run(name, func(t *testing.T) { - kmod, err := parseKernelModuleLine(test.line) - require.NoError(t, err) - require.Equal(t, test.expected, kmod) - }) - } -} diff --git a/proc/testdata/kallsyms b/proc/testdata/kallsyms deleted file mode 100644 index 6b6f67365..000000000 --- a/proc/testdata/kallsyms +++ /dev/null @@ -1,13 +0,0 @@ -0000000000000000 A fixed_percpu_data -0000000000000000 A __per_cpu_start -0000000000001000 A cpu_debug_store -0000000000002000 A irq_stack_backing_store -0000000000006000 A cpu_tss_rw -ffffffffc0346f40 t hidraw_connect [hid] -ffffffffc0340470 t hidinput_find_field [hid] -ffffffffc033d150 t hid_parse_report [hid] -ffffffffc033f9a0 t hid_open_report [hid] -ffffffffc03459b0 t hid_quirks_init [hid] -ffffffffc0345720 t hid_ignore [hid] -ffffffffc033e550 t hid_add_device [hid] -ffffffffc0346e20 t hidraw_report_event [hid] diff --git a/proc/testdata/kallsyms_0 b/proc/testdata/kallsyms_0 deleted file mode 100644 index c4e26e86d..000000000 --- a/proc/testdata/kallsyms_0 +++ /dev/null @@ -1,5 +0,0 @@ -0000000000000000 A fixed_percpu_data -0000000000000000 A __per_cpu_start -0000000000000000 A cpu_debug_store -0000000000000000 A irq_stack_backing_store -0000000000000000 A cpu_tss_rw diff --git a/proc/testdata/kallsyms_invalid b/proc/testdata/kallsyms_invalid deleted file mode 100644 index 1b9c31a72..000000000 --- a/proc/testdata/kallsyms_invalid +++ /dev/null @@ -1,5 +0,0 @@ -0000000000000000 A fixed_percpu_data -0000000000000000 A __per_cpu_start -0000000000001000 A cpu_debug_store -0000000000002000 irq_stack_backing_store -0000000000006000 A cpu_tss_rw diff --git a/process/coredump.go b/process/coredump.go index d868f5c82..9da3f9c5f 100644 --- a/process/coredump.go +++ b/process/coredump.go @@ -23,30 +23,30 @@ import ( ) const ( - // maxNotesSection the maximum section size for notes + // maxNotesSection is the maximum section size for notes. maxNotesSection = 16 * 1024 * 1024 ) -// CoredumpProcess implements Process interface to ELF coredumps +// CoredumpProcess implements Process interface to ELF coredumps. type CoredumpProcess struct { *pfelf.File - // files contains coredump's files by name + // files contains coredump's files by name. files map[string]*CoredumpFile - // pid the original PID of the coredump + // pid is the original PID from which the coredump was generated. pid libpf.PID - // machineData contains the parsed machine data + // machineData contains the parsed machine data. machineData MachineData - // mappings contains the parsed mappings + // mappings contains the parsed mappings. mappings []Mapping - // threadInfo contains the parsed thread info + // threadInfo contains the parsed thread info. threadInfo []ThreadInfo - // execPhdrPtr points to the main executable's program headers + // execPhdrPtr points to the main executable's program headers. execPhdrPtr libpf.Address // hasMusl is set if musl c-library is detected in this coredump. This @@ -57,27 +57,27 @@ type CoredumpProcess struct { var _ Process = &CoredumpProcess{} -// CoredumpMapping describes a file backed mapping in a coredump +// CoredumpMapping describes a file backed mapping in a coredump. type CoredumpMapping struct { - // Corresponding PT_LOAD segment + // Prog points to the corresponding PT_LOAD segment. Prog *pfelf.Prog - // File is the backing file for this mapping + // File is the backing file for this mapping. File *CoredumpFile - // FileOffset is the offset in the original backing file + // FileOffset is the offset in the original backing file. FileOffset uint64 } -// CoredumpFile contains information about a file mapped into a coredump +// CoredumpFile contains information about a file mapped into a coredump. type CoredumpFile struct { - // parent is the Coredump inside which this file is + // parent is the Coredump inside which this file is. parent *CoredumpProcess - // inode is the synthesized inode for this file + // inode is the synthesized inode for this file. inode uint64 - // Name is the mapped file's name + // Name is the mapped file's name. Name string - // Mappings contains mappings regarding this file + // Mappings contains mappings regarding this file. Mappings []CoredumpMapping - // Base is the virtual address where this file is loaded + // Base is the virtual address where this file is loaded. Base uint64 } @@ -86,7 +86,6 @@ type Note64 struct { Namesz, Descsz, Type uint32 } -//nolint:revive,stylecheck const ( NAMESPACE_CORE = "CORE\x00" NAMESPACE_LINUX = "LINUX\x00" @@ -127,10 +126,10 @@ func OpenCoredump(name string) (*CoredumpProcess, error) { // It's the value of a map indexed with mapping virtual address, and contains the data // needed to associate data from different coredump data structures to proper internals. type vaddrMappings struct { - // prog is the ELF PT_LOAD Program header for this virtual address + // prog is the ELF PT_LOAD Program header for this virtual address. prog *pfelf.Prog - // mappingIndex is the mapping's index in processState.Mappings + // mappingIndex is the mapping's index in processState.Mappings. mappingIndex int } @@ -194,7 +193,7 @@ func OpenCoredumpFile(f *pfelf.File) (*CoredumpProcess, error) { break } - // Parse the note if we are interested in it (skip others) + // Parse the note if we are interested in it (skip others). name := string(nameBytes) ty := elf.NType(note.Type) if name == NAMESPACE_CORE { @@ -247,38 +246,38 @@ func (cd *CoredumpProcess) MainExecutable() string { return "" } -// PID implements the Process interface +// PID implements the Process interface. func (cd *CoredumpProcess) PID() libpf.PID { return cd.pid } -// GetMachineData implements the Process interface +// GetMachineData implements the Process interface. func (cd *CoredumpProcess) GetMachineData() MachineData { return cd.machineData } -// GetMappings implements the Process interface +// GetMappings implements the Process interface. func (cd *CoredumpProcess) GetMappings() ([]Mapping, uint32, error) { return cd.mappings, 0, nil } -// GetThreadInfo implements the Process interface +// GetThreadInfo implements the Process interface. func (cd *CoredumpProcess) GetThreads() ([]ThreadInfo, error) { return cd.threadInfo, nil } -// OpenMappingFile implements the Process interface +// OpenMappingFile implements the Process interface. func (cd *CoredumpProcess) OpenMappingFile(_ *Mapping) (ReadAtCloser, error) { - // No filesystem level backing file in coredumps + // Coredumps do not contain the original backing files. return nil, errors.New("coredump does not support opening backing file") } -// GetMappingFileLastModified implements the Process interface +// GetMappingFileLastModified implements the Process interface. func (cd *CoredumpProcess) GetMappingFileLastModified(_ *Mapping) int64 { return 0 } -// CalculateMappingFileID implements the Process interface +// CalculateMappingFileID implements the Process interface. func (cd *CoredumpProcess) CalculateMappingFileID(m *Mapping) (libpf.FileID, error) { // It is not possible to calculate the real FileID as the section headers // are likely missing. So just return a synthesized FileID. @@ -291,7 +290,7 @@ func (cd *CoredumpProcess) CalculateMappingFileID(m *Mapping) (libpf.FileID, err return libpf.FileIDFromBytes(h.Sum(nil)) } -// OpenELF implements the ELFOpener and Process interfaces +// OpenELF implements the ELFOpener and Process interfaces. func (cd *CoredumpProcess) OpenELF(path string) (*pfelf.File, error) { // Fallback to directly returning the data from coredump. This comes with caveats: // @@ -312,7 +311,13 @@ func (cd *CoredumpProcess) OpenELF(path string) (*pfelf.File, error) { return nil, fmt.Errorf("ELF file `%s` not found", path) } -// getFile returns (creating if needed) a matching CoredumpFile for given file name +// ExtractAsFile implements the Process interface. +func (cd *CoredumpProcess) ExtractAsFile(_ string) (string, error) { + // Coredumps do not contain the original backing files. + return "", errors.New("coredump does not support opening backing file") +} + +// getFile returns (creating if needed) a matching CoredumpFile for given file name. func (cd *CoredumpProcess) getFile(name string) *CoredumpFile { if cf, ok := cd.files[name]; ok { return cf @@ -329,18 +334,18 @@ func (cd *CoredumpProcess) getFile(name string) *CoredumpFile { return cf } -// FileMappingHeader64 is the header for CORE/NT_FILE note +// FileMappingHeader64 is the header for CORE/NT_FILE note. type FileMappingHeader64 struct { Entries uint64 PageSize uint64 } -// FileMappingEntry64 is the per-mapping data header in CORE/NT_FILE note +// FileMappingEntry64 is the per-mapping data header in CORE/NT_FILE note. type FileMappingEntry64 struct { Start, End, FileOffset uint64 } -// parseMappings processes CORE/NT_FILE note with description of memory mappings +// parseMappings processes a CORE/NT_FILE note with the description of memory mappings. func (cd *CoredumpProcess) parseMappings(desc []byte, vaddrToMappings map[uint64]vaddrMappings) error { hdrSize := uint64(unsafe.Sizeof(FileMappingHeader64{})) @@ -386,7 +391,7 @@ func (cd *CoredumpProcess) parseMappings(desc []byte, mapping := &cd.mappings[m.mappingIndex] mapping.Path = cf.Name mapping.FileOffset = entry.FileOffset * hdr.PageSize - // Synthesize non-zero device and inode indicating this is a filebacked mapping + // Synthesize non-zero device and inode indicating this is a filebacked mapping. mapping.Device = 1 mapping.Inode = cf.inode } @@ -395,7 +400,7 @@ func (cd *CoredumpProcess) parseMappings(desc []byte, return nil } -// parseAuxVector processes CORE/NT_AUXV note +// parseAuxVector processes a CORE/NT_AUXV note. func (cd *CoredumpProcess) parseAuxVector(desc []byte, vaddrToMappings map[uint64]vaddrMappings) { for i := 0; i+16 <= len(desc); i += 16 { value := binary.LittleEndian.Uint64(desc[i+8:]) @@ -423,7 +428,7 @@ func (cd *CoredumpProcess) parseAuxVector(desc []byte, vaddrToMappings map[uint6 } } -// PrpsInfo64 is the 64-bit NT_PRPSINFO note header +// PrpsInfo64 is the 64-bit NT_PRPSINFO note header. type PrpsInfo64 struct { State uint8 Sname uint8 @@ -441,7 +446,7 @@ type PrpsInfo64 struct { Args [80]byte } -// parseProcessInfo processes CORE/NT_PRPSINFO note +// parseProcessInfo processes a CORE/NT_PRPSINFO note. func (cd *CoredumpProcess) parseProcessInfo(desc []byte) error { if len(desc) == int(unsafe.Sizeof(PrpsInfo64{})) { info := (*PrpsInfo64)(unsafe.Pointer(&desc[0])) @@ -451,7 +456,7 @@ func (cd *CoredumpProcess) parseProcessInfo(desc []byte) error { return fmt.Errorf("unsupported NT_PRPSINFO size: %d", len(desc)) } -// parseProcessStatus processes CORE/NT_PRSTATUS note +// parseProcessStatus processes a CORE/NT_PRSTATUS note. func (cd *CoredumpProcess) parseProcessStatus(desc []byte) error { // The corresponding struct definition can be found here: // https://github.com/torvalds/linux/blob/49d766f3a0e4/include/linux/elfcore.h#L48 diff --git a/process/process.go b/process/process.go index 04c22ed6a..087be29c2 100644 --- a/process/process.go +++ b/process/process.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "os" + "path" "strconv" "strings" "sync" @@ -24,12 +25,17 @@ import ( "go.opentelemetry.io/ebpf-profiler/stringutil" ) +// GetMappings returns this error when no mappings can be extracted. +var ErrNoMappings = errors.New("no mappings") + // systemProcess provides an implementation of the Process interface for a // process that is currently running on this machine. type systemProcess struct { pid libpf.PID + tid libpf.PID - remoteMemory remotememory.RemoteMemory + mainThreadExit bool + remoteMemory remotememory.RemoteMemory fileToMapping map[string]*Mapping } @@ -53,9 +59,10 @@ func init() { } // New returns an object with Process interface accessing it -func New(pid libpf.PID) Process { +func New(pid, tid libpf.PID) Process { return &systemProcess{ pid: pid, + tid: tid, remoteMemory: remotememory.NewProcessVirtualMemory(pid), } } @@ -165,9 +172,10 @@ func parseMappings(mapsFile io.Reader) ([]Mapping, uint32, error) { path = VdsoPathName device = 0 inode = vdsoInode - } else if path != "" { - // Ignore [vsyscall] and similar executable kernel - // pages we don't care about + } else if path == "" { + // This is an anonymous mapping, keep it + } else { + // Ignore other mappings that are invalid, non-existent or are special pseudo-files continue } } else { @@ -229,20 +237,48 @@ func (sp *systemProcess) GetMappings() ([]Mapping, uint32, error) { defer mapsFile.Close() mappings, numParseErrors, err := parseMappings(mapsFile) - if err == nil { - fileToMapping := make(map[string]*Mapping, len(mappings)) - for idx := range mappings { - m := &mappings[idx] - if m.Inode == 0 { - // Ignore mappings that are invalid, - // non-existent or are special pseudo-files. - continue - } - fileToMapping[m.Path] = m + if err != nil { + return mappings, numParseErrors, err + } + + if len(mappings) == 0 { + // We could test for main thread exit here by checking for zombie state + // in /proc/sp.pid/stat but it's simpler to assume that this is the case + // and try extracting mappings for a different thread. Since we stopped + // processing /proc at agent startup, it's not possible that the agent + // will sample a process without mappings + log.Debugf("PID: %v main thread exit", sp.pid) + sp.mainThreadExit = true + + if sp.pid == sp.tid { + return mappings, numParseErrors, ErrNoMappings + } + + log.Debugf("TID: %v extracting mappings", sp.tid) + mapsFileAlt, err := os.Open(fmt.Sprintf("/proc/%d/task/%d/maps", sp.pid, sp.tid)) + // On all errors resulting from trying to get mappings from a different thread, + // return ErrNoMappings which will keep the PID tracked in processmanager and + // allow for a future iteration to try extracting mappings from a different thread. + // This is done to deal with race conditions triggered by thread exits (we do not want + // the agent to unload process metadata when a thread exits but the process is still + // alive). + if err != nil { + return mappings, numParseErrors, ErrNoMappings } - sp.fileToMapping = fileToMapping + defer mapsFileAlt.Close() + mappings, numParseErrors, err = parseMappings(mapsFileAlt) + if err != nil || len(mappings) == 0 { + return mappings, numParseErrors, ErrNoMappings + } + } + + fileToMapping := make(map[string]*Mapping, len(mappings)) + for idx := range mappings { + m := &mappings[idx] + fileToMapping[m.Path] = m } - return mappings, numParseErrors, err + sp.fileToMapping = fileToMapping + return mappings, numParseErrors, nil } func (sp *systemProcess) GetThreads() ([]ThreadInfo, error) { @@ -271,6 +307,13 @@ func (sp *systemProcess) getMappingFile(m *Mapping) string { if m.IsAnonymous() || m.IsVDSO() { return "" } + if sp.mainThreadExit { + // Neither /proc/sp.pid/map_files nor /proc/sp.pid/task/sp.tid/map_files + // nor /proc/sp.pid/root exist if main thread has exited, so we use the + // mapping path directly under the sp.tid root. + rootPath := fmt.Sprintf("/proc/%v/task/%v/root", sp.pid, sp.tid) + return path.Join(rootPath, m.Path) + } return fmt.Sprintf("/proc/%v/map_files/%x-%x", sp.pid, m.Vaddr, m.Vaddr+m.Length) } @@ -330,5 +373,9 @@ func (sp *systemProcess) OpenELF(file string) (*pfelf.File, error) { } // Fall back to opening the file using the process specific root - return pfelf.Open(fmt.Sprintf("/proc/%v/root/%s", sp.pid, file)) + return pfelf.Open(path.Join("/proc", strconv.Itoa(int(sp.pid)), "root", file)) +} + +func (sp *systemProcess) ExtractAsFile(file string) (string, error) { + return path.Join("/proc", strconv.Itoa(int(sp.pid)), "root", file), nil } diff --git a/process/process_test.go b/process/process_test.go index 9e5689db7..dce05e41b 100644 --- a/process/process_test.go +++ b/process/process_test.go @@ -26,7 +26,8 @@ var testMappings = `55fe82710000-55fe8273c000 r--p 00000000 fd:01 1068432 7f63c8eef000-7f63c8fdf000 r-xp 0001c000 1fd:01 7f63c8eef000-7f63c8fdf000 r-xp 0001c000 1fd.01 1075944 7f63c8eef000-7f63c8fdf000 r- 0001c000 1fd:01 1075944 -7f63c8eef000 r-xp 0001c000 1fd:01 1075944` +7f63c8eef000 r-xp 0001c000 1fd:01 1075944 +7f8b929f0000-7f8b92a00000 r-xp 00000000 00:00 0 ` func TestParseMappings(t *testing.T) { mappings, numParseErrors, err := parseMappings(strings.NewReader(testMappings)) @@ -98,12 +99,22 @@ func TestParseMappings(t *testing.T) { FileOffset: 114688, Path: "/tmp/usr_lib_x86_64-linux-gnu_libopensc.so.6.0.0", }, + { + Vaddr: 0x7f8b929f0000, + Device: 0x0, + Flags: elf.PF_R + elf.PF_X, + Inode: 0, + Length: 0x10000, + FileOffset: 0, + Path: "", + }, } assert.Equal(t, expected, mappings) } func TestNewPIDOfSelf(t *testing.T) { - pr := New(libpf.PID(os.Getpid())) + pid := libpf.PID(os.Getpid()) + pr := New(pid, pid) assert.NotNil(t, pr) mappings, numParseErrors, err := pr.GetMappings() diff --git a/process/types.go b/process/types.go index ffe9e59c3..7ca146b72 100644 --- a/process/types.go +++ b/process/types.go @@ -16,27 +16,27 @@ import ( "go.opentelemetry.io/ebpf-profiler/util" ) -// VdsoPathName is the path to use for VDSO mappings +// VdsoPathName is the path to use for VDSO mappings. const VdsoPathName = "linux-vdso.1.so" -// vdsoInode is the synthesized inode number for VDSO mappings +// vdsoInode is the synthesized inode number for VDSO mappings. const vdsoInode = 50 -// Mapping contains information about a memory mapping +// Mapping contains information about a memory mapping. type Mapping struct { - // Vaddr is the virtual memory start for this mapping + // Vaddr is the virtual memory start for this mapping. Vaddr uint64 - // Length is the length of the mapping + // Length is the length of the mapping. Length uint64 - // Flags contains the mapping flags and permissions + // Flags contains the mapping flags and permissions. Flags elf.ProgFlag - // FileOffset contains for file backed mappings the offset from the file start + // FileOffset contains for file backed mappings the offset from the file start. FileOffset uint64 - // Device holds the device ID where the file is located + // Device holds the device ID where the file is located. Device uint64 - // Inode holds the mapped file's inode number + // Inode holds the mapped file's inode number. Inode uint64 - // Path contains the file name for file backed mappings + // Path contains the file name for file backed mappings. Path string } @@ -63,19 +63,19 @@ func (m *Mapping) GetOnDiskFileIdentifier() util.OnDiskFileIdentifier { } } -// ThreadInfo contains the information about a thread CPU state needed for unwinding +// ThreadInfo contains the information about a thread CPU state needed for unwinding. type ThreadInfo struct { - // TPBase contains the Thread Pointer Base value + // TPBase contains the Thread Pointer Base value. TPBase uint64 - // GPRegs contains the CPU state (registers) for the thread + // GPRegs contains the CPU state (registers) for the thread. GPRegs []byte - // LWP is the Light Weight Process ID (thread ID) + // LWP is the Light Weight Process ID (thread ID). LWP uint32 } -// MachineData contains machine specific information about the process +// MachineData contains machine specific information about the process. type MachineData struct { - // Machine is the Process Machine type + // Machine is the Process Machine type. Machine elf.Machine // CodePACMask contains the PAC mask for code pointers. ARM64 specific, otherwise 0. CodePACMask uint64 @@ -83,7 +83,7 @@ type MachineData struct { DataPACMask uint64 } -// ReadAtCloser interfaces implements io.ReaderAt and io.Closer +// ReadAtCloser combines the io.ReaderAt and io.Closer interfaces. type ReadAtCloser interface { io.ReaderAt io.Closer @@ -94,31 +94,38 @@ type ReadAtCloser interface { // from different goroutines. As an exception the ELFOpener and the returned // GetRemoteMemory object are safe for concurrent use. type Process interface { - // PID returns the process identifier + // PID returns the process identifier. PID() libpf.PID - // GetMachineData reads machine specific data from the target process + // GetMachineData reads machine specific data from the target process. GetMachineData() MachineData - // GetMappings reads and parses process memory mappings + // GetMappings reads and parses process memory mappings. GetMappings() ([]Mapping, uint32, error) - // GetThreads reads the process thread states + // GetThreads reads the process thread states. GetThreads() ([]ThreadInfo, error) - // GetRemoteMemory returns a remote memory reader accessing the target process + // GetRemoteMemory returns a remote memory reader accessing the target process. GetRemoteMemory() remotememory.RemoteMemory - // OpenMappingFile returns ReadAtCloser accessing the backing file of the mapping + // OpenMappingFile returns ReadAtCloser accessing the backing file of the mapping. OpenMappingFile(*Mapping) (ReadAtCloser, error) // GetMappingFileLastModifed returns the timestamp when the backing file was last modified - // or zero if an error occurs or mapping file is not accessible via filesystem + // or zero if an error occurs or mapping file is not accessible via filesystem. GetMappingFileLastModified(*Mapping) int64 - // CalculateMappingFileID calculates FileID of the backing file + // CalculateMappingFileID calculates FileID of the backing file. CalculateMappingFileID(*Mapping) (libpf.FileID, error) + // ExtractAsFile returns a filename suitable for opening the given file from + // the target process namespace. This is a last resort method to access the file + // when the ReaderAt interface from OpenMappingFile is not sufficient. The returned + // filename may refer to /proc or be a temporarily file, and it must not be modified + // or deleted. + ExtractAsFile(string) (string, error) + io.Closer pfelf.ELFOpener diff --git a/processmanager/ebpf/ebpf.go b/processmanager/ebpf/ebpf.go index 71308f989..e68b2c572 100644 --- a/processmanager/ebpf/ebpf.go +++ b/processmanager/ebpf/ebpf.go @@ -42,8 +42,6 @@ const ( ) // EbpfHandler provides the functionality to interact with eBPF maps. -// -//nolint:revive type EbpfHandler interface { // Embed interpreter.EbpfHandler as subset of this interface. interpreter.EbpfHandler @@ -103,6 +101,7 @@ type ebpfMapsImpl struct { rubyProcs *cebpf.Map v8Procs *cebpf.Map apmIntProcs *cebpf.Map + goLabelsProcs *cebpf.Map // Stackdelta and process related eBPF maps exeIDToStackDeltaMaps []*cebpf.Map @@ -205,6 +204,11 @@ func LoadMaps(ctx context.Context, maps map[string]*cebpf.Map) (EbpfHandler, err } impl.apmIntProcs = apmIntProcs + goLabelsProcs, ok := maps["go_labels_procs"] + if !ok { + log.Fatalf("Map go_labels_procs is not available") + } + impl.goLabelsProcs = goLabelsProcs impl.stackDeltaPageToInfo, ok = maps["stack_delta_page_to_info"] if !ok { log.Fatalf("Map stack_delta_page_to_info is not available") @@ -253,29 +257,47 @@ func LoadMaps(ctx context.Context, maps map[string]*cebpf.Map) (EbpfHandler, err // UpdateInterpreterOffsets adds the given moduleRanges to the eBPF map interpreterOffsets. func (impl *ebpfMapsImpl) UpdateInterpreterOffsets(ebpfProgIndex uint16, fileID host.FileID, offsetRanges []util.Range) error { - if offsetRanges == nil { - return errors.New("offsetRanges is nil") - } - for _, offsetRange := range offsetRanges { - // The keys of this map are executable-id-and-offset-into-text entries, and - // the offset_range associated with them gives the precise area in that page - // where the main interpreter loop is located. This is required to unwind - // nicely from native code into interpreted code. - key := uint64(fileID) - value := C.OffsetRange{ - lower_offset: C.u64(offsetRange.Start), - upper_offset: C.u64(offsetRange.End), - program_index: C.u16(ebpfProgIndex), - } - if err := impl.interpreterOffsets.Update(unsafe.Pointer(&key), unsafe.Pointer(&value), - cebpf.UpdateAny); err != nil { - log.Fatalf("Failed to place interpreter range in map: %v", err) - } + key, value, err := InterpreterOffsetKeyValue(ebpfProgIndex, fileID, offsetRanges) + if err != nil { + return err + } + if err := impl.interpreterOffsets.Update(unsafe.Pointer(&key), unsafe.Pointer(&value), + cebpf.UpdateAny); err != nil { + log.Fatalf("Failed to place interpreter range in map: %v", err) } return nil } +func InterpreterOffsetKeyValue(ebpfProgIndex uint16, fileID host.FileID, + offsetRanges []util.Range) (key uint64, value C.OffsetRange, err error) { + rLen := len(offsetRanges) + if rLen < 1 || rLen > 2 { + return 0, C.OffsetRange{}, fmt.Errorf("invalid ranges %v", offsetRanges) + } + // The keys of this map are executable-id-and-offset-into-text entries, and + // the offset_range associated with them gives the precise area in that page + // where the main interpreter loop is located. This is required to unwind + // nicely from native code into interpreted code. + key = uint64(fileID) + first := offsetRanges[0] + value = C.OffsetRange{ + lower_offset1: C.u64(first.Start), + upper_offset1: C.u64(first.End), + program_index: C.u16(ebpfProgIndex), + } + if len(offsetRanges) == 2 { + // Fields {lower,upper}_offset2 may be used to specify an optional second range + // of an interpreter function. This may be useful if the interpreter function + // consists of two non-contiguous memory ranges, which may happen due to Hot/Cold + // split compiler optimization + second := offsetRanges[1] + value.lower_offset2 = C.u64(second.Start) + value.upper_offset2 = C.u64(second.End) + } + return key, value, nil +} + // getInterpreterTypeMap returns the eBPF map for the given typ // or an error if typ is not supported. func (impl *ebpfMapsImpl) getInterpreterTypeMap(typ libpf.InterpreterType) (*cebpf.Map, error) { @@ -296,6 +318,8 @@ func (impl *ebpfMapsImpl) getInterpreterTypeMap(typ libpf.InterpreterType) (*ceb return impl.v8Procs, nil case libpf.APMInt: return impl.apmIntProcs, nil + case libpf.GoLabels: + return impl.goLabelsProcs, nil default: return nil, fmt.Errorf("type %d is not (yet) supported", typ) } @@ -793,8 +817,6 @@ func (impl *ebpfMapsImpl) DeletePidPageMappingInfoBatch(pid libpf.PID, prefixes // LookupPidPageInformation returns the fileID and bias for a given pid and page combination from // the eBPF map pid_page_to_mapping_info. // So far this function is used only in tests. -// -//nolint:deadcode func (impl *ebpfMapsImpl) LookupPidPageInformation(pid uint32, page uint64) (host.FileID, uint64, error) { // pid_page_to_mapping_info is a LPM trie and expects the pid and page diff --git a/processmanager/execinfomanager/manager.go b/processmanager/execinfomanager/manager.go index e0d79148a..bee2fee7e 100644 --- a/processmanager/execinfomanager/manager.go +++ b/processmanager/execinfomanager/manager.go @@ -19,6 +19,8 @@ import ( "go.opentelemetry.io/ebpf-profiler/interpreter" "go.opentelemetry.io/ebpf-profiler/interpreter/apmint" "go.opentelemetry.io/ebpf-profiler/interpreter/dotnet" + golang "go.opentelemetry.io/ebpf-profiler/interpreter/go" + "go.opentelemetry.io/ebpf-profiler/interpreter/golabels" "go.opentelemetry.io/ebpf-profiler/interpreter/hotspot" "go.opentelemetry.io/ebpf-profiler/interpreter/nodev8" "go.opentelemetry.io/ebpf-profiler/interpreter/perl" @@ -124,8 +126,14 @@ func NewExecutableInfoManager( if includeTracers.Has(types.DotnetTracer) { interpreterLoaders = append(interpreterLoaders, dotnet.Loader) } + if includeTracers.Has(types.GoTracer) { + interpreterLoaders = append(interpreterLoaders, golang.Loader) + } interpreterLoaders = append(interpreterLoaders, apmint.Loader) + if includeTracers.Has(types.Labels) { + interpreterLoaders = append(interpreterLoaders, golabels.Loader) + } deferredFileIDs, err := lru.NewSynced[host.FileID, libpf.Void](deferredFileIDSize, func(id host.FileID) uint32 { return uint32(id) }) @@ -446,7 +454,7 @@ func (state *executableInfoManagerState) loadDeltas( // Zero means no merging happened. Only small differences for address and the CFA delta // are considered, in order to limit the amount of unique combinations generated. func calculateMergeOpcode(delta, nextDelta sdtypes.StackDelta) uint8 { - if delta.Info.Opcode == sdtypes.UnwindOpcodeCommand { + if delta.Info.Opcode == support.UnwindOpcodeCommand { return 0 } addrDiff := nextDelta.Address - delta.Address @@ -474,7 +482,7 @@ func calculateMergeOpcode(delta, nextDelta sdtypes.StackDelta) uint8 { func (state *executableInfoManagerState) getUnwindInfoIndex( info sdtypes.UnwindInfo, ) (uint16, error) { - if info.Opcode == sdtypes.UnwindOpcodeCommand { + if info.Opcode == support.UnwindOpcodeCommand { return uint16(info.Param) | support.DeltaCommandFlag, nil } diff --git a/processmanager/helpers.go b/processmanager/helpers.go index 6f4b70224..db3de2890 100644 --- a/processmanager/helpers.go +++ b/processmanager/helpers.go @@ -4,6 +4,12 @@ package processmanager // import "go.opentelemetry.io/ebpf-profiler/processmanager" import ( + "bufio" + "fmt" + "io" + "os" + "regexp" + lru "github.com/elastic/go-freelru" log "github.com/sirupsen/logrus" @@ -11,6 +17,11 @@ import ( "go.opentelemetry.io/ebpf-profiler/libpf" ) +//nolint:lll +var ( + cgroupv2ContainerIDPattern = regexp.MustCompile(`0:.*?:.*?([0-9a-fA-F]{64})(?:\.scope)?(?:/[a-z]+)?$`) +) + type lruFileIDMapper struct { cache *lru.SyncedLRU[host.FileID, libpf.FileID] } @@ -79,3 +90,36 @@ type FileIDMapper interface { // Set adds a mapping from the 64-bit file ID to the 128-bit file ID. Set(pre host.FileID, post libpf.FileID) } + +func parseContainerID(cgroupFile io.Reader) string { + scanner := bufio.NewScanner(cgroupFile) + buf := make([]byte, 512) + // Providing a predefined buffer overrides the internal buffer that Scanner uses (4096 bytes). + // We can do that and also set a maximum allocation size on the following call. + // With a maximum of 4096 characters path in the kernel, 8192 should be fine here. We don't + // expect lines in /proc//cgroup to be longer than that. + scanner.Buffer(buf, 8192) + var pathParts []string + for scanner.Scan() { + line := scanner.Text() + pathParts = cgroupv2ContainerIDPattern.FindStringSubmatch(line) + if pathParts == nil { + log.Debugf("Could not extract cgroupv2 path from line: %s", line) + continue + } + return pathParts[1] + } + + // No containerID could be extracted + return "" +} + +// extractContainerID returns the containerID for pid if cgroup v2 is used. +func extractContainerID(pid libpf.PID) (string, error) { + cgroupFile, err := os.Open(fmt.Sprintf("/proc/%d/cgroup", pid)) + if err != nil { + return "", err + } + + return parseContainerID(cgroupFile), nil +} diff --git a/processmanager/helpers_test.go b/processmanager/helpers_test.go new file mode 100644 index 000000000..8510ca96d --- /dev/null +++ b/processmanager/helpers_test.go @@ -0,0 +1,56 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package processmanager // import "go.opentelemetry.io/ebpf-profiler/processmanager" + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +//nolint:lll +func TestExtractContainerID(t *testing.T) { + tests := []struct { + line string + expectedContainerID string + }{ + { + line: "0::/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podf6f2d169_f2ae_4afa-95ed_06ff2ed6b288.slice/cri-containerd-b4d6d161c62525d726fa394b27df30e14f8ea5646313ada576b390de70cfc8cc.scope", + expectedContainerID: "b4d6d161c62525d726fa394b27df30e14f8ea5646313ada576b390de70cfc8cc", + }, + { + line: "0::/kubepods/besteffort/pod05e102bf-8744-4942-a241-9b6f07983a53/f52a212505a606972cf8614c3cb856539e71b77ecae33436c5ac442232fbacf8", + expectedContainerID: "f52a212505a606972cf8614c3cb856539e71b77ecae33436c5ac442232fbacf8", + }, + { + line: "0::/kubepods/besteffort/pod897277d4-5e6f-4999-a976-b8340e8d075e/crio-a4d6b686848a610472a2eed3ae20d4d64b6b4819feb9fdfc7fd7854deaf59ef3", + expectedContainerID: "a4d6b686848a610472a2eed3ae20d4d64b6b4819feb9fdfc7fd7854deaf59ef3", + }, + { + line: "0::/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod4c9f1974_5c46_44c2_b42f_3bbf0e98eef9.slice/cri-containerd-bacb920470900725e0aa7d914fee5eb0854315448b024b6b8420ad8429c607ba.scope", + expectedContainerID: "bacb920470900725e0aa7d914fee5eb0854315448b024b6b8420ad8429c607ba", + }, + { + line: "0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.gnome.Terminal.slice/vte-spawn-868f9513-eee8-457d-8e36-1b37ae8ae622.scope", + }, + { + line: "0::/../../user.slice/user-501.slice/session-3.scope", + }, + { + line: "0::/system.slice/docker-b1eba9dfaeba29d8b80532a574a03ea3cac29384327f339c26da13649e2120df.scope/init", + expectedContainerID: "b1eba9dfaeba29d8b80532a574a03ea3cac29384327f339c26da13649e2120df", + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.expectedContainerID, func(t *testing.T) { + reader := bytes.NewReader([]byte(tc.line)) + + gotContainerID := parseContainerID(reader) + assert.Equal(t, tc.expectedContainerID, gotContainerID) + }) + } +} diff --git a/processmanager/manager.go b/processmanager/manager.go index fa3e083ee..d3ed86b0c 100644 --- a/processmanager/manager.go +++ b/processmanager/manager.go @@ -219,9 +219,10 @@ func (pm *ProcessManager) ConvertTrace(trace *host.Trace) (newTrace *libpf.Trace traceLen := len(trace.Frames) newTrace = &libpf.Trace{ - Files: make([]libpf.FileID, 0, traceLen), - Linenos: make([]libpf.AddressOrLineno, 0, traceLen), - FrameTypes: make([]libpf.FrameType, 0, traceLen), + Files: make([]libpf.FileID, 0, traceLen), + Linenos: make([]libpf.AddressOrLineno, 0, traceLen), + FrameTypes: make([]libpf.FrameType, 0, traceLen), + CustomLabels: trace.CustomLabels, } for i := 0; i < traceLen; i++ { @@ -269,6 +270,12 @@ func (pm *ProcessManager) ConvertTrace(trace *host.Trace) (newTrace *libpf.Trace } } + // Attempt symbolization of native frames. It is best effort and + // provides non-symbolized frames if no native symbolizer is active. + if err := pm.symbolizeFrame(i, trace, newTrace); err == nil { + continue + } + fileID, ok := pm.FileIDMapper.Get(frame.File) if !ok { log.Debugf( @@ -299,7 +306,9 @@ func (pm *ProcessManager) ConvertTrace(trace *host.Trace) (newTrace *libpf.Trace func (pm *ProcessManager) MaybeNotifyAPMAgent( rawTrace *host.Trace, umTraceHash libpf.TraceHash, count uint16) string { + pm.mu.RLock() pidInterp, ok := pm.interpreters[rawTrace.PID] + pm.mu.RUnlock() if !ok { return "" } diff --git a/processmanager/manager_test.go b/processmanager/manager_test.go index 1ca36f26e..932f97ca3 100644 --- a/processmanager/manager_test.go +++ b/processmanager/manager_test.go @@ -27,6 +27,7 @@ import ( pmebpf "go.opentelemetry.io/ebpf-profiler/processmanager/ebpf" "go.opentelemetry.io/ebpf-profiler/remotememory" "go.opentelemetry.io/ebpf-profiler/reporter" + "go.opentelemetry.io/ebpf-profiler/support" tracertypes "go.opentelemetry.io/ebpf-profiler/tracer/types" "go.opentelemetry.io/ebpf-profiler/traceutil" "go.opentelemetry.io/ebpf-profiler/util" @@ -76,6 +77,10 @@ func (d *dummyProcess) OpenELF(name string) (*pfelf.File, error) { return pfelf.Open(name) } +func (d *dummyProcess) ExtractAsFile(name string) (string, error) { + return name, nil +} + func (d *dummyProcess) Close() error { return nil } @@ -98,7 +103,7 @@ func (d *dummyStackDeltaProvider) GetIntervalStructuresForFile(_ host.FileID, data := int32(8 * r.IntN(42)) result.Deltas.Add(sdtypes.StackDelta{ Address: uint64(addr), - Info: sdtypes.UnwindInfo{Opcode: sdtypes.UnwindOpcodeBaseSP, Param: data}, + Info: sdtypes.UnwindInfo{Opcode: support.UnwindOpcodeBaseSP, Param: data}, }) } return nil @@ -128,7 +133,7 @@ func generateDummyFiles(t *testing.T, num int) []string { content := []byte(tmpfile.Name()) _, err = tmpfile.Write(content) require.NoError(t, err) - tmpfile.Close() + _ = tmpfile.Close() require.NoError(t, err) files = append(files, tmpfile.Name()) } diff --git a/processmanager/processinfo.go b/processmanager/processinfo.go index fa8dcf169..4a4c0d5a0 100644 --- a/processmanager/processinfo.go +++ b/processmanager/processinfo.go @@ -21,13 +21,13 @@ import ( "time" log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" "go.opentelemetry.io/ebpf-profiler/host" "go.opentelemetry.io/ebpf-profiler/interpreter" "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" "go.opentelemetry.io/ebpf-profiler/lpm" - "go.opentelemetry.io/ebpf-profiler/proc" "go.opentelemetry.io/ebpf-profiler/process" eim "go.opentelemetry.io/ebpf-profiler/processmanager/execinfomanager" "go.opentelemetry.io/ebpf-profiler/reporter" @@ -37,6 +37,40 @@ import ( "go.opentelemetry.io/ebpf-profiler/util" ) +// isPIDLive checks if a PID belongs to a live process. It will never produce a false negative but +// may produce a false positive (e.g. due to permissions) in which case an error will also be +// returned. +func isPIDLive(pid libpf.PID) (bool, error) { + // Check first with the kill syscall which is the fastest route. + // A kill syscall with a 0 signal is documented to still do the check + // whether the process exists: https://linux.die.net/man/2/kill + err := unix.Kill(int(pid), 0) + if err == nil { + return true, nil + } + + var errno unix.Errno + if errors.As(err, &errno) { + switch errno { + case unix.ESRCH: + return false, nil + case unix.EPERM: + // It seems that in some rare cases this check can fail with + // a permission error. Fallback to a procfs check. + default: + return true, err + } + } + + path := fmt.Sprintf("/proc/%d/maps", pid) + _, err = os.Stat(path) + if err != nil && os.IsNotExist(err) { + return false, nil + } + + return true, err +} + // assignTSDInfo updates the TSDInfo for the Interpreters on given PID. // Caller must hold pm.mu write lock. func (pm *ProcessManager) assignTSDInfo(pid libpf.PID, tsdInfo *tpbase.TSDInfo) { @@ -86,7 +120,7 @@ func (pm *ProcessManager) updatePidInformation(pid libpf.PID, m *Mapping) (bool, // allocate the embedded map for this process. var processName string exePath, _ := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid)) - if name, err := os.ReadFile(fmt.Sprintf("/prod/%d/comm", pid)); err == nil { + if name, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid)); err == nil { processName = string(name) } @@ -111,10 +145,16 @@ func (pm *ProcessManager) updatePidInformation(pid libpf.PID, m *Mapping) (bool, } } + containerID, err := extractContainerID(pid) + if err != nil { + log.Debugf("Failed extracting containerID for %d: %v", pid, err) + } + info = &processInfo{ meta: ProcessMeta{ Name: processName, Executable: exePath, + ContainerID: containerID, EnvVariables: envVarMap}, mappings: make(map[libpf.Address]*Mapping), mappingsByFileID: make(map[host.FileID]map[libpf.Address]*Mapping), @@ -514,7 +554,7 @@ func (pm *ProcessManager) synchronizeMappings(pr process.Process, for _, instance := range pm.interpreters[pid] { err := instance.SynchronizeMappings(pm.ebpf, pm.reporter, pr, mappings) if err != nil { - if alive, _ := proc.IsPIDLive(pid); alive { + if alive, _ := isPIDLive(pid); alive { log.Errorf("Failed to handle new anonymous mapping for PID %d: %v", pid, err) } else { log.Debugf("Failed to handle new anonymous mapping for PID %d: process exited", @@ -614,9 +654,16 @@ func (pm *ProcessManager) SynchronizeProcess(pr process.Process) { return } + if errors.Is(err, process.ErrNoMappings) { + // When no mappings can be extracted but the process is still alive, + // do not trigger a process exit to avoid unloading process metadata. + // As it's likely that a future iteration can extract mappings from a + // different thread in the process, notify eBPF to enable further notifications. + pm.ebpf.RemoveReportedPID(pid) + return + } + // All other errors imply that the process has exited. - // Clean up, and notify eBPF. - pm.processPIDExit(pid) if os.IsNotExist(err) { // Since listing /proc and opening files in there later is inherently racy, // we expect to lose the race sometimes and thus expect to hit os.IsNotExist. @@ -626,22 +673,7 @@ func (pm *ProcessManager) SynchronizeProcess(pr process.Process) { // return ESRCH. Handle it as if the process did not exist. pm.mappingStats.errProcESRCH.Add(1) } - return - } - if len(mappings) == 0 { - // Valid process without any (executable) mappings. All cases are - // handled as process exit. Possible causes and reasoning: - // 1. It is a kernel worker process. The eBPF does not send events from these, - // but we can see kernel threads here during startup when tracer walks - // /proc and tries to synchronize all PIDs it sees. - // The PID should not exist anywhere, but we can still double check and - // make sure the PID is not tracked. - // 2. It is a normal process executing, but we just sampled it when the kernel - // execve() is rebuilding the mappings and nothing is currently mapped. - // In this case we can handle it as process exit because everything about - // the process is changing: all mappings, comm, etc. If execve fails, we - // reaped it early. If execve succeeds, we will get new synchronization - // request soon, and handle it as a new process event. + // Clean up, and notify eBPF. pm.processPIDExit(pid) return } @@ -673,7 +705,7 @@ func (pm *ProcessManager) CleanupPIDs() { pm.mu.RLock() for pid := range pm.pidToProcessInfo { - if live, _ := proc.IsPIDLive(pid); !live { + if live, _ := isPIDLive(pid); !live { deadPids = append(deadPids, pid) } } @@ -744,6 +776,7 @@ func (pm *ProcessManager) ProcessedUntil(traceCaptureKTime times.KTime) { continue } + log.Debugf("PID %v deleted", pid) delete(pm.pidToProcessInfo, pid) for _, instance := range pm.interpreters[pid] { diff --git a/processmanager/synthdeltas.go b/processmanager/synthdeltas.go index ab09ac6fe..2d05d7476 100644 --- a/processmanager/synthdeltas.go +++ b/processmanager/synthdeltas.go @@ -12,6 +12,7 @@ import ( "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" + "go.opentelemetry.io/ebpf-profiler/support" ) // regFP is the arm64 frame-pointer register (x29) number @@ -36,13 +37,7 @@ func createVDSOSyntheticRecordNone(_ *pfelf.File) sdtypes.IntervalData { func createVDSOSyntheticRecordArm64(ef *pfelf.File) sdtypes.IntervalData { deltas := sdtypes.StackDeltaArray{} deltas = append(deltas, sdtypes.StackDelta{Address: 0, Info: sdtypes.UnwindInfoLR}) - - symbols, err := ef.ReadDynamicSymbols() - if err != nil { - return sdtypes.IntervalData{} - } - - symbols.VisitAll(func(sym libpf.Symbol) { + _ = ef.VisitDynamicSymbols(func(sym libpf.Symbol) { addr := uint64(sym.Address) if sym.Name == "__kernel_rt_sigreturn" { deltas = append( @@ -54,7 +49,7 @@ func createVDSOSyntheticRecordArm64(ef *pfelf.File) sdtypes.IntervalData { } // Determine if LR is on stack code := make([]byte, sym.Size) - if _, err = ef.ReadVirtualMemory(code, int64(sym.Address)); err != nil { + if _, err := ef.ReadVirtualMemory(code, int64(sym.Address)); err != nil { return } @@ -99,9 +94,9 @@ func createVDSOSyntheticRecordArm64(ef *pfelf.File) sdtypes.IntervalData { sdtypes.StackDelta{ Address: addr + frameStart, Info: sdtypes.UnwindInfo{ - Opcode: sdtypes.UnwindOpcodeBaseFP, + Opcode: support.UnwindOpcodeBaseFP, Param: int32(frameSize), - FPOpcode: sdtypes.UnwindOpcodeBaseFP, + FPOpcode: support.UnwindOpcodeBaseFP, FPParam: 8, }, }, diff --git a/processmanager/synthdeltas_test.go b/processmanager/synthdeltas_test.go index 053acff88..0fa5b00cc 100644 --- a/processmanager/synthdeltas_test.go +++ b/processmanager/synthdeltas_test.go @@ -8,15 +8,16 @@ import ( "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" sdtypes "go.opentelemetry.io/ebpf-profiler/nativeunwind/stackdeltatypes" + "go.opentelemetry.io/ebpf-profiler/support" "github.com/stretchr/testify/require" ) func TestVDSOArm64(t *testing.T) { frameSize16 := sdtypes.UnwindInfo{ - Opcode: sdtypes.UnwindOpcodeBaseFP, + Opcode: support.UnwindOpcodeBaseFP, Param: 16, - FPOpcode: sdtypes.UnwindOpcodeBaseFP, + FPOpcode: support.UnwindOpcodeBaseFP, FPParam: 8, } diff --git a/processmanager/types.go b/processmanager/types.go index b088752bf..9807aaae8 100644 --- a/processmanager/types.go +++ b/processmanager/types.go @@ -147,6 +147,8 @@ type ProcessMeta struct { Executable string // process env vars from /proc/PID/environ EnvVariables map[string]string + // container ID retrieved from /proc/PID/cgroup + ContainerID string } // processInfo contains information about the executable mappings diff --git a/reporter/base_reporter.go b/reporter/base_reporter.go index f618e0023..baa017fd8 100644 --- a/reporter/base_reporter.go +++ b/reporter/base_reporter.go @@ -34,11 +34,8 @@ type baseReporter struct { // pdata holds the generator for the data being exported. pdata *pdata.Pdata - // cgroupv2ID caches PID to container ID information for cgroupv2 containers. - cgroupv2ID *lru.SyncedLRU[libpf.PID, string] - // traceEvents stores reported trace events (trace metadata with frames and counts) - traceEvents xsync.RWMutex[map[libpf.Origin]samples.KeyToEventMapping] + traceEvents xsync.RWMutex[samples.TraceEventsTree] // hostmetadata stores metadata that is sent out with every request. hostmetadata *lru.SyncedLRU[string, string] @@ -73,13 +70,7 @@ func (b *baseReporter) ExecutableKnown(fileID libpf.FileID) bool { } func (b *baseReporter) FrameKnown(frameID libpf.FrameID) bool { - known := false - if frameMapLock, exists := b.pdata.Frames.GetAndRefresh(frameID.FileID(), - pdata.FramesCacheLifetime); exists { - frameMap := frameMapLock.RLock() - defer frameMapLock.RUnlock(&frameMap) - _, known = (*frameMap)[frameID.AddressOrLine()] - } + _, known := b.pdata.Frames.GetAndRefresh(frameID, pdata.FrameMapLifetime) return known } @@ -102,12 +93,7 @@ func (b *baseReporter) ReportTraceEvent(trace *libpf.Trace, meta *samples.TraceE extraMeta = b.cfg.ExtraSampleAttrProd.CollectExtraSampleMeta(trace, meta) } - containerID, err := libpf.LookupCgroupv2(b.cgroupv2ID, meta.PID) - if err != nil { - log.Debugf("Failed to get a cgroupv2 ID as container ID for PID %d: %v", - meta.PID, err) - } - + containerID := meta.ContainerID key := samples.TraceAndMetaKey{ Hash: trace.Hash, Comm: meta.Comm, @@ -116,20 +102,30 @@ func (b *baseReporter) ReportTraceEvent(trace *libpf.Trace, meta *samples.TraceE ApmServiceName: meta.APMServiceName, ContainerID: containerID, Pid: int64(meta.PID), + Tid: int64(meta.TID), ExtraMeta: extraMeta, } - traceEventsMap := b.traceEvents.WLock() - defer b.traceEvents.WUnlock(&traceEventsMap) + eventsTree := b.traceEvents.WLock() + defer b.traceEvents.WUnlock(&eventsTree) - if events, exists := (*traceEventsMap)[meta.Origin][key]; exists { + if _, exists := (*eventsTree)[samples.ContainerID(containerID)]; !exists { + (*eventsTree)[samples.ContainerID(containerID)] = + make(map[libpf.Origin]samples.KeyToEventMapping) + } + + if _, exists := (*eventsTree)[samples.ContainerID(containerID)][meta.Origin]; !exists { + (*eventsTree)[samples.ContainerID(containerID)][meta.Origin] = + make(samples.KeyToEventMapping) + } + + if events, exists := (*eventsTree)[samples.ContainerID(containerID)][meta.Origin][key]; exists { events.Timestamps = append(events.Timestamps, uint64(meta.Timestamp)) events.OffTimes = append(events.OffTimes, meta.OffTime) - (*traceEventsMap)[meta.Origin][key] = events + (*eventsTree)[samples.ContainerID(containerID)][meta.Origin][key] = events return nil } - - (*traceEventsMap)[meta.Origin][key] = &samples.TraceEvents{ + (*eventsTree)[samples.ContainerID(containerID)][meta.Origin][key] = &samples.TraceEvents{ Files: trace.Files, Linenos: trace.Linenos, FrameTypes: trace.FrameTypes, @@ -144,43 +140,19 @@ func (b *baseReporter) ReportTraceEvent(trace *libpf.Trace, meta *samples.TraceE } func (b *baseReporter) FrameMetadata(args *FrameMetadataArgs) { - fileID := args.FrameID.FileID() - addressOrLine := args.FrameID.AddressOrLine() - log.Debugf("FrameMetadata [%x] %v+%v at %v:%v", - fileID, args.FunctionName, args.FunctionOffset, + args.FrameID.FileID(), args.FunctionName, args.FunctionOffset, args.SourceFile, args.SourceLine) - - if frameMapLock, exists := b.pdata.Frames.GetAndRefresh(fileID, - pdata.FramesCacheLifetime); exists { - frameMap := frameMapLock.WLock() - defer frameMapLock.WUnlock(&frameMap) - - sourceFile := args.SourceFile - if sourceFile == "" { - // The new SourceFile may be empty, and we don't want to overwrite - // an existing filePath with it. - if s, exists := (*frameMap)[addressOrLine]; exists { - sourceFile = s.FilePath - } - } - - (*frameMap)[addressOrLine] = samples.SourceInfo{ - LineNumber: args.SourceLine, - FilePath: sourceFile, - FunctionOffset: args.FunctionOffset, - FunctionName: args.FunctionName, - } - return - } - - v := make(map[libpf.AddressOrLineno]samples.SourceInfo) - v[addressOrLine] = samples.SourceInfo{ + si := samples.SourceInfo{ LineNumber: args.SourceLine, FilePath: args.SourceFile, FunctionOffset: args.FunctionOffset, FunctionName: args.FunctionName, } - mu := xsync.NewRWMutex(v) - b.pdata.Frames.Add(fileID, &mu) + if si.FilePath == "" { + if oldsi, exists := b.pdata.Frames.Get(args.FrameID); exists { + si.FilePath = oldsi.FilePath + } + } + b.pdata.Frames.Add(args.FrameID, si) } diff --git a/reporter/collector_reporter.go b/reporter/collector_reporter.go index e986c60ce..6983bd0ec 100644 --- a/reporter/collector_reporter.go +++ b/reporter/collector_reporter.go @@ -5,8 +5,6 @@ package reporter // import "go.opentelemetry.io/ebpf-profiler/reporter" import ( "context" - "maps" - "time" lru "github.com/elastic/go-freelru" log "github.com/sirupsen/logrus" @@ -16,7 +14,6 @@ import ( "go.opentelemetry.io/ebpf-profiler/libpf/xsync" "go.opentelemetry.io/ebpf-profiler/reporter/internal/pdata" "go.opentelemetry.io/ebpf-profiler/reporter/samples" - "go.opentelemetry.io/ebpf-profiler/support" ) // Assert that we implement the full Reporter interface. @@ -31,14 +28,6 @@ type CollectorReporter struct { // NewCollector builds a new CollectorReporter func NewCollector(cfg *Config, nextConsumer xconsumer.Profiles) (*CollectorReporter, error) { - cgroupv2ID, err := lru.NewSynced[libpf.PID, string](cfg.CGroupCacheElements, - func(pid libpf.PID) uint32 { return uint32(pid) }) - if err != nil { - return nil, err - } - // Set a lifetime to reduce the risk of invalid data in case of PID reuse. - cgroupv2ID.SetLifetime(90 * time.Second) - // Next step: Dynamically configure the size of this LRU. // Currently, we use the length of the JSON array in // hostmetadata/hostmetadata.json. @@ -57,11 +46,7 @@ func NewCollector(cfg *Config, nextConsumer xconsumer.Profiles) (*CollectorRepor return nil, err } - originsMap := make(map[libpf.Origin]samples.KeyToEventMapping, 2) - for _, origin := range []libpf.Origin{support.TraceOriginSampling, - support.TraceOriginOffCPU} { - originsMap[origin] = make(samples.KeyToEventMapping) - } + tree := make(samples.TraceEventsTree) return &CollectorReporter{ baseReporter: &baseReporter{ @@ -69,8 +54,7 @@ func NewCollector(cfg *Config, nextConsumer xconsumer.Profiles) (*CollectorRepor name: cfg.Name, version: cfg.Version, pdata: data, - cgroupv2ID: cgroupv2ID, - traceEvents: xsync.NewRWMutex(originsMap), + traceEvents: xsync.NewRWMutex(tree), hostmetadata: hostmetadata, runLoop: &runLoop{ stopSignal: make(chan libpf.Void), @@ -91,7 +75,6 @@ func (r *CollectorReporter) Start(ctx context.Context) error { }, func() { // Allow the GC to purge expired entries to avoid memory leaks. r.pdata.Purge() - r.cgroupv2ID.PurgeExpired() }) // When Stop() is called and a signal to 'stop' is received, then: @@ -107,16 +90,18 @@ func (r *CollectorReporter) Start(ctx context.Context) error { // reportProfile creates and sends out a profile. func (r *CollectorReporter) reportProfile(ctx context.Context) error { - traceEvents := r.traceEvents.WLock() - events := make(map[libpf.Origin]samples.KeyToEventMapping, 2) - for _, origin := range []libpf.Origin{support.TraceOriginSampling, - support.TraceOriginOffCPU} { - events[origin] = maps.Clone((*traceEvents)[origin]) - clear((*traceEvents)[origin]) + traceEventsPtr := r.traceEvents.WLock() + reportedEvents := (*traceEventsPtr) + newEvents := make(samples.TraceEventsTree) + *traceEventsPtr = newEvents + r.traceEvents.WUnlock(&traceEventsPtr) + + profiles, err := r.pdata.Generate(reportedEvents, r.name, r.version) + if err != nil { + log.Errorf("pdata: %v", err) + return nil } - r.traceEvents.WUnlock(&traceEvents) - profiles := r.pdata.Generate(events) if profiles.SampleCount() == 0 { log.Debugf("Skip sending profile with no samples") return nil diff --git a/reporter/collector_reporter_test.go b/reporter/collector_reporter_test.go index d5807d465..7a8b2d2e8 100644 --- a/reporter/collector_reporter_test.go +++ b/reporter/collector_reporter_test.go @@ -54,7 +54,6 @@ func TestCollectorReporterReportTraceEvent(t *testing.T) { r, err := NewCollector(&Config{ ExecutablesCacheElements: 1, FramesCacheElements: 1, - CGroupCacheElements: 1, }, next) require.NoError(t, err) if err := r.ReportTraceEvent(tt.trace, tt.meta); err != nil && diff --git a/reporter/config.go b/reporter/config.go index 09c2665ea..ae05eb57d 100644 --- a/reporter/config.go +++ b/reporter/config.go @@ -30,18 +30,8 @@ type Config struct { ExecutablesCacheElements uint32 // FramesCacheElements defines the item capacity of the frames cache. FramesCacheElements uint32 - // CGroupCacheElements defines the item capacity of the cgroup cache. - CGroupCacheElements uint32 // samplesPerSecond defines the number of samples per second. SamplesPerSecond int - // HostID is the host ID to be sent to the collection agent. - HostID uint64 - // KernelVersion is the kernel version of the host. - KernelVersion string - // HostName is the name of the host. - HostName string - // IPAddress is the IP address of the host. - IPAddress string // Number of connection attempts to the collector after which we give up retrying. MaxGRPCRetries uint32 diff --git a/reporter/internal/pdata/generate.go b/reporter/internal/pdata/generate.go index 0e55366bc..6133eddc9 100644 --- a/reporter/internal/pdata/generate.go +++ b/reporter/internal/pdata/generate.go @@ -4,9 +4,9 @@ package pdata // import "go.opentelemetry.io/ebpf-profiler/reporter/internal/pdata" import ( - "crypto/rand" + "fmt" + "math" "path/filepath" - "slices" "time" log "github.com/sirupsen/logrus" @@ -14,7 +14,7 @@ import ( "go.opentelemetry.io/collector/pdata/pprofile" "go.opentelemetry.io/otel/attribute" - semconv "go.opentelemetry.io/otel/semconv/v1.30.0" + semconv "go.opentelemetry.io/otel/semconv/v1.34.0" "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/reporter/samples" @@ -24,88 +24,122 @@ import ( const ( ExecutableCacheLifetime = 1 * time.Hour FramesCacheLifetime = 1 * time.Hour + FrameMapLifetime = 1 * time.Hour ) // Generate generates a pdata request out of internal profiles data, to be // exported. -func (p *Pdata) Generate(events map[libpf.Origin]samples.KeyToEventMapping) pprofile.Profiles { +func (p *Pdata) Generate(tree samples.TraceEventsTree, + agentName, agentVersion string) (pprofile.Profiles, error) { profiles := pprofile.NewProfiles() - rp := profiles.ResourceProfiles().AppendEmpty() - sp := rp.ScopeProfiles().AppendEmpty() - for _, origin := range []libpf.Origin{support.TraceOriginSampling, - support.TraceOriginOffCPU} { - if len(events[origin]) == 0 { - // Do not append empty profiles, if there - // is not profiling data for this origin. + dic := profiles.ProfilesDictionary() + + // Temporary helpers that will build the various tables in ProfilesDictionary. + stringSet := make(OrderedSet[string], 64) + funcSet := make(OrderedSet[funcInfo], 64) + mappingSet := make(OrderedSet[libpf.FileID], 64) + locationSet := make(OrderedSet[locationInfo], 64) + + // By specification, the first element should be empty. + stringSet.Add("") + funcSet.Add(funcInfo{nameIdx: stringSet.Add(""), fileNameIdx: stringSet.Add("")}) + + for containerID, originToEvents := range tree { + if len(originToEvents) == 0 { continue } - prof := sp.Profiles().AppendEmpty() - prof.SetProfileID(pprofile.ProfileID(mkProfileID())) - p.setProfile(origin, events[origin], prof) + + rp := profiles.ResourceProfiles().AppendEmpty() + rp.Resource().Attributes().PutStr(string(semconv.ContainerIDKey), + string(containerID)) + rp.SetSchemaUrl(semconv.SchemaURL) + + sp := rp.ScopeProfiles().AppendEmpty() + sp.Scope().SetName(agentName) + sp.Scope().SetVersion(agentVersion) + sp.SetSchemaUrl(semconv.SchemaURL) + + for _, origin := range []libpf.Origin{ + support.TraceOriginSampling, + support.TraceOriginOffCPU, + } { + if len(originToEvents[origin]) == 0 { + // Do not append empty profiles. + continue + } + + prof := sp.Profiles().AppendEmpty() + if err := p.setProfile(dic, + stringSet, funcSet, mappingSet, locationSet, + origin, originToEvents[origin], prof); err != nil { + return profiles, err + } + } } - return profiles -} -// mkProfileID creates a random profile ID. -func mkProfileID() []byte { - profileID := make([]byte, 16) - _, err := rand.Read(profileID) - if err != nil { - return []byte("opentelemetry-ebpf-profiler") + // Populate the ProfilesDictionary tables. + funcTable := dic.FunctionTable() + funcTable.EnsureCapacity(len(funcSet)) + for range funcSet { + funcTable.AppendEmpty() + } + for v, idx := range funcSet { + f := funcTable.At(int(idx)) + f.SetNameStrindex(v.nameIdx) + f.SetFilenameStrindex(v.fileNameIdx) + } + + stringTable := dic.StringTable() + stringTable.EnsureCapacity(len(stringSet)) + for _, val := range stringSet.ToSlice() { + stringTable.Append(val) } - return profileID + + return profiles, nil } // setProfile sets the data an OTLP profile with all collected samples up to // this moment. func (p *Pdata) setProfile( + dic pprofile.ProfilesDictionary, + stringSet OrderedSet[string], + funcSet OrderedSet[funcInfo], + mappingSet OrderedSet[libpf.FileID], + locationSet OrderedSet[locationInfo], origin libpf.Origin, events map[samples.TraceAndMetaKey]*samples.TraceEvents, profile pprofile.Profile, -) { - // stringMap is a temporary helper that will build the StringTable. - // By specification, the first element should be empty. - stringMap := make(map[string]int32) - stringMap[""] = 0 - - // funcMap is a temporary helper that will build the Function array - // in profile and make sure information is deduplicated. - funcMap := make(map[samples.FuncInfo]int32) - funcMap[samples.FuncInfo{Name: "", FileName: ""}] = 0 - +) error { st := profile.SampleType().AppendEmpty() switch origin { case support.TraceOriginSampling: - st.SetTypeStrindex(getStringMapIndex(stringMap, "samples")) - st.SetUnitStrindex(getStringMapIndex(stringMap, "count")) - + profile.SetPeriod(1e9 / int64(p.samplesPerSecond)) pt := profile.PeriodType() - pt.SetTypeStrindex(getStringMapIndex(stringMap, "cpu")) - pt.SetUnitStrindex(getStringMapIndex(stringMap, "nanoseconds")) + pt.SetTypeStrindex(stringSet.Add("cpu")) + pt.SetUnitStrindex(stringSet.Add("nanoseconds")) - profile.SetPeriod(1e9 / int64(p.samplesPerSecond)) + st.SetTypeStrindex(stringSet.Add("samples")) + st.SetUnitStrindex(stringSet.Add("count")) case support.TraceOriginOffCPU: - st.SetTypeStrindex(getStringMapIndex(stringMap, "events")) - st.SetUnitStrindex(getStringMapIndex(stringMap, "nanoseconds")) + st.SetTypeStrindex(stringSet.Add("events")) + st.SetUnitStrindex(stringSet.Add("nanoseconds")) default: - log.Errorf("Generating profile for unsupported origin %d", origin) - return + // Should never happen + return fmt.Errorf("generating profile for unsupported origin %d", origin) } - // Temporary lookup to reference existing Mappings. - fileIDtoMapping := make(map[libpf.FileID]int32) + attrMgr := samples.NewAttrTableManager(dic.AttributeTable()) - attrMgr := samples.NewAttrTableManager(profile.AttributeTable()) - var locationIndex int32 - var startTS, endTS pcommon.Timestamp + locationIndex := int32(profile.LocationIndices().Len()) + startTS, endTS := uint64(math.MaxUint64), uint64(0) for traceKey, traceInfo := range events { sample := profile.Sample().AppendEmpty() sample.SetLocationsStartIndex(locationIndex) - slices.Sort(traceInfo.Timestamps) - startTS = pcommon.Timestamp(traceInfo.Timestamps[0]) - endTS = pcommon.Timestamp(traceInfo.Timestamps[len(traceInfo.Timestamps)-1]) - + for _, ts := range traceInfo.Timestamps { + startTS = min(startTS, ts) + endTS = max(endTS, ts) + } sample.TimestampsUnixNano().FromRaw(traceInfo.Timestamps) switch origin { @@ -117,99 +151,106 @@ func (p *Pdata) setProfile( // Walk every frame of the trace. for i := range traceInfo.FrameTypes { - loc := profile.LocationTable().AppendEmpty() - loc.SetAddress(uint64(traceInfo.Linenos[i])) - attrMgr.AppendOptionalString(loc.AttributeIndices(), - semconv.ProfileFrameTypeKey, traceInfo.FrameTypes[i].String()) - + locInfo := locationInfo{ + address: uint64(traceInfo.Linenos[i]), + frameType: traceInfo.FrameTypes[i].String(), + } switch frameKind := traceInfo.FrameTypes[i]; frameKind { case libpf.NativeFrame: // As native frames are resolved in the backend, we use Mapping to // report these frames. - - var locationMappingIndex int32 - if tmpMappingIndex, exists := fileIDtoMapping[traceInfo.Files[i]]; exists { - locationMappingIndex = tmpMappingIndex - } else { - idx := int32(len(fileIDtoMapping)) - fileIDtoMapping[traceInfo.Files[i]] = idx - locationMappingIndex = idx - + locationMappingIndex, exists := mappingSet.AddWithCheck(traceInfo.Files[i]) + if !exists { ei, exists := p.Executables.GetAndRefresh(traceInfo.Files[i], ExecutableCacheLifetime) - // Next step: Select a proper default value, // if the name of the executable is not known yet. - var fileName = "UNKNOWN" + fileName := "UNKNOWN" if exists { fileName = ei.FileName } - mapping := profile.MappingTable().AppendEmpty() + mapping := dic.MappingTable().AppendEmpty() mapping.SetMemoryStart(uint64(traceInfo.MappingStarts[i])) mapping.SetMemoryLimit(uint64(traceInfo.MappingEnds[i])) mapping.SetFileOffset(traceInfo.MappingFileOffsets[i]) - mapping.SetFilenameStrindex(getStringMapIndex(stringMap, fileName)) + mapping.SetFilenameStrindex(stringSet.Add(fileName)) // Once SemConv and its Go package is released with the new // semantic convention for build_id, replace these hard coded // strings. attrMgr.AppendOptionalString(mapping.AttributeIndices(), - semconv.ProcessExecutableBuildIDGnuKey, + semconv.ProcessExecutableBuildIDGNUKey, ei.GnuBuildID) attrMgr.AppendOptionalString(mapping.AttributeIndices(), semconv.ProcessExecutableBuildIDHtlhashKey, traceInfo.Files[i].StringNoQuotes()) } - loc.SetMappingIndex(locationMappingIndex) + locInfo.mappingIndex = locationMappingIndex case libpf.AbortFrame: // Next step: Figure out how the OTLP protocol // could handle artificial frames, like AbortFrame, // that are not originated from a native or interpreted // program. default: - // Store interpreted frame information as a Line message: - line := loc.Line().AppendEmpty() - - fileIDInfoLock, exists := p.Frames.GetAndRefresh(traceInfo.Files[i], - FramesCacheLifetime) - if !exists { + // Store interpreted frame information as a Line message + locInfo.hasLine = true + if si, exists := p.Frames.GetAndRefresh( + libpf.NewFrameID(traceInfo.Files[i], traceInfo.Linenos[i]), + FramesCacheLifetime); exists { + locInfo.lineNumber = int64(si.LineNumber) + fi := funcInfo{ + nameIdx: stringSet.Add(si.FunctionName), + fileNameIdx: stringSet.Add(si.FilePath), + } + locInfo.functionIndex = funcSet.Add(fi) + } else { // At this point, we do not have enough information for the frame. // Therefore, we report a dummy entry and use the interpreter as filename. - line.SetFunctionIndex(createFunctionEntry(funcMap, - "UNREPORTED", frameKind.String())) - } else { - fileIDInfo := fileIDInfoLock.RLock() - if si, exists := (*fileIDInfo)[traceInfo.Linenos[i]]; exists { - line.SetLine(int64(si.LineNumber)) - - line.SetFunctionIndex(createFunctionEntry(funcMap, - si.FunctionName, si.FilePath)) - } else { - // At this point, we do not have enough information for the frame. - // Therefore, we report a dummy entry and use the interpreter as filename. - // To differentiate this case from the case where no information about - // the file ID is available at all, we use a different name for reported - // function. - line.SetFunctionIndex(createFunctionEntry(funcMap, - "UNRESOLVED", frameKind.String())) + // To differentiate this case from the case where no information about + // the file ID is available at all, we use a different name for reported + // function. + fi := funcInfo{ + nameIdx: stringSet.Add("UNRESOLVED"), + fileNameIdx: stringSet.Add(frameKind.String()), } - fileIDInfoLock.RUnlock(&fileIDInfo) + locInfo.functionIndex = funcSet.Add(fi) } - // To be compliant with the protocol, generate a dummy mapping entry. - loc.SetMappingIndex(getDummyMappingIndex(fileIDtoMapping, stringMap, - attrMgr, profile, traceInfo.Files[i])) + idx, exists := mappingSet.AddWithCheck(traceInfo.Files[i]) + locInfo.mappingIndex = idx + if !exists { + // To be compliant with the protocol, generate a dummy mapping entry. + mapping := dic.MappingTable().AppendEmpty() + mapping.SetFilenameStrindex(stringSet.Add("")) + attrMgr.AppendOptionalString(mapping.AttributeIndices(), + semconv.ProcessExecutableBuildIDHtlhashKey, + traceInfo.Files[i].StringNoQuotes()) + } + } // End frame type switch + + idx, exists := locationSet.AddWithCheck(locInfo) + if !exists { + // Add a new Location to the dictionary + loc := dic.LocationTable().AppendEmpty() + loc.SetAddress(locInfo.address) + loc.SetMappingIndex(locInfo.mappingIndex) + if locInfo.hasLine { + line := loc.Line().AppendEmpty() + line.SetLine(locInfo.lineNumber) + line.SetFunctionIndex(locInfo.functionIndex) + } + attrMgr.AppendOptionalString(loc.AttributeIndices(), + semconv.ProfileFrameTypeKey, locInfo.frameType) } - } + profile.LocationIndices().Append(idx) + } // End per-frame processing exeName := traceKey.ExecutablePath if exeName != "" { _, exeName = filepath.Split(exeName) } - attrMgr.AppendOptionalString(sample.AttributeIndices(), - semconv.ContainerIDKey, traceKey.ContainerID) attrMgr.AppendOptionalString(sample.AttributeIndices(), semconv.ThreadNameKey, traceKey.Comm) @@ -222,11 +263,13 @@ func (p *Pdata) setProfile( semconv.ServiceNameKey, traceKey.ApmServiceName) attrMgr.AppendInt(sample.AttributeIndices(), semconv.ProcessPIDKey, traceKey.Pid) + attrMgr.AppendInt(sample.AttributeIndices(), + semconv.ThreadIDKey, traceKey.Tid) for key, value := range traceInfo.EnvVars { attrMgr.AppendOptionalString( sample.AttributeIndices(), - attribute.Key("env."+key), + attribute.Key("process.environment_variable."+key), value) } @@ -237,87 +280,12 @@ func (p *Pdata) setProfile( sample.SetLocationsLength(int32(len(traceInfo.FrameTypes))) locationIndex += sample.LocationsLength() - } - log.Debugf("Reporting OTLP profile with %d samples", profile.Sample().Len()) - - // Populate the deduplicated functions into profile. - funcTable := profile.FunctionTable() - funcTable.EnsureCapacity(len(funcMap)) - for range funcMap { - funcTable.AppendEmpty() - } - for v, idx := range funcMap { - f := funcTable.At(int(idx)) - f.SetNameStrindex(getStringMapIndex(stringMap, v.Name)) - f.SetFilenameStrindex(getStringMapIndex(stringMap, v.FileName)) - } - - // When ranging over stringMap, the order will be according to the - // hash value of the key. To get the correct order for profile.StringTable, - // put the values in stringMap, in the correct array order. - stringTable := make([]string, len(stringMap)) - for v, idx := range stringMap { - stringTable[idx] = v - } - - for _, v := range stringTable { - profile.StringTable().Append(v) - } - - // profile.LocationIndices is not optional, and we only write elements into - // profile.Location that at least one sample references. - for i := int32(0); i < int32(profile.LocationTable().Len()); i++ { - profile.LocationIndices().Append(i) - } - - profile.SetDuration(endTS - startTS) - profile.SetStartTime(startTS) -} - -// getStringMapIndex inserts or looks up the index for value in stringMap. -func getStringMapIndex(stringMap map[string]int32, value string) int32 { - if idx, exists := stringMap[value]; exists { - return idx - } - - idx := int32(len(stringMap)) - stringMap[value] = idx - - return idx -} - -// createFunctionEntry adds a new function and returns its reference index. -func createFunctionEntry(funcMap map[samples.FuncInfo]int32, - name string, fileName string) int32 { - key := samples.FuncInfo{ - Name: name, - FileName: fileName, - } - if idx, exists := funcMap[key]; exists { - return idx - } - - idx := int32(len(funcMap)) - funcMap[key] = idx + } // End sample processing - return idx -} - -// getDummyMappingIndex inserts or looks up an entry for interpreted FileIDs. -func getDummyMappingIndex(fileIDtoMapping map[libpf.FileID]int32, - stringMap map[string]int32, attrMgr *samples.AttrTableManager, profile pprofile.Profile, - fileID libpf.FileID) int32 { - if mappingIndex, exists := fileIDtoMapping[fileID]; exists { - return mappingIndex - } + log.Debugf("Reporting OTLP profile with %d samples", profile.Sample().Len()) - locationMappingIndex := int32(len(fileIDtoMapping)) - fileIDtoMapping[fileID] = locationMappingIndex + profile.SetDuration(pcommon.Timestamp(endTS - startTS)) + profile.SetStartTime(pcommon.Timestamp(startTS)) - mapping := profile.MappingTable().AppendEmpty() - mapping.SetFilenameStrindex(getStringMapIndex(stringMap, "")) - attrMgr.AppendOptionalString(mapping.AttributeIndices(), - semconv.ProcessExecutableBuildIDHtlhashKey, - fileID.StringNoQuotes()) - return locationMappingIndex + return nil } diff --git a/reporter/internal/pdata/generate_test.go b/reporter/internal/pdata/generate_test.go index e7f60ee85..a990f2a7f 100644 --- a/reporter/internal/pdata/generate_test.go +++ b/reporter/internal/pdata/generate_test.go @@ -5,164 +5,96 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pprofile" + semconv "go.opentelemetry.io/otel/semconv/v1.34.0" + "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/libpf/xsync" "go.opentelemetry.io/ebpf-profiler/reporter/samples" "go.opentelemetry.io/ebpf-profiler/support" ) -func TestGetStringMapIndex(t *testing.T) { - for _, tt := range []struct { - name string - stringMap map[string]int32 - value string - - wantStringMap map[string]int32 - wantIndex int32 - }{ - { - name: "with a value not yet in the string map", - stringMap: map[string]int32{}, - value: "test", - - wantIndex: 0, - wantStringMap: map[string]int32{"test": 0}, - }, - { - name: "with a value already in the string map", - stringMap: map[string]int32{"test": 42}, - value: "test", - - wantIndex: 42, - wantStringMap: map[string]int32{"test": 42}, - }, - } { - t.Run(tt.name, func(t *testing.T) { - stringMap := tt.stringMap - - i := getStringMapIndex(stringMap, tt.value) - assert.Equal(t, tt.wantIndex, i) - assert.Equal(t, tt.wantStringMap, stringMap) - }) - } -} - -func TestCreateFunctionEntry(t *testing.T) { - for _, tt := range []struct { - name string - funcMap map[samples.FuncInfo]int32 - funcName string - fileName string - - wantIndex int32 - wantFuncMap map[samples.FuncInfo]int32 - }{ - { - name: "with ane entry not yet in the func map", - funcMap: map[samples.FuncInfo]int32{}, - funcName: "my_method", - fileName: "/tmp", - - wantIndex: 0, - wantFuncMap: map[samples.FuncInfo]int32{ - {Name: "my_method", FileName: "/tmp"}: 0, - }, - }, - { - name: "with ane entry already in the func map", - funcMap: map[samples.FuncInfo]int32{ - {Name: "my_method", FileName: "/tmp"}: 42, - }, - funcName: "my_method", - fileName: "/tmp", - - wantIndex: 42, - wantFuncMap: map[samples.FuncInfo]int32{ - {Name: "my_method", FileName: "/tmp"}: 42, - }, - }, - } { - t.Run(tt.name, func(t *testing.T) { - funcMap := tt.funcMap - - i := createFunctionEntry(funcMap, tt.funcName, tt.fileName) - assert.Equal(t, tt.wantIndex, i) - assert.Equal(t, tt.wantFuncMap, funcMap) - }) - } -} - func TestGetDummyMappingIndex(t *testing.T) { for _, tt := range []struct { - name string - fileIDToMapping map[libpf.FileID]int32 - stringMap map[string]int32 - fileID libpf.FileID - - wantIndex int32 - wantFileIDToMapping map[libpf.FileID]int32 - wantMappingTable []int32 - wantStringMap map[string]int32 + name string + mappingSet OrderedSet[libpf.FileID] + stringSet OrderedSet[string] + fileID libpf.FileID + + wantIndex int32 + wantMappingSet OrderedSet[libpf.FileID] + wantMappingTable []int32 + wantStringSet OrderedSet[string] }{ { name: "with an index already in the file id mapping", - fileIDToMapping: map[libpf.FileID]int32{ + mappingSet: OrderedSet[libpf.FileID]{ libpf.UnsymbolizedFileID: 42, }, - fileID: libpf.UnsymbolizedFileID, - + fileID: libpf.UnsymbolizedFileID, wantIndex: 42, + wantMappingSet: OrderedSet[libpf.FileID]{ + libpf.UnsymbolizedFileID: 42, + }, }, { - name: "with an index not yet in the file id mapping", - fileIDToMapping: map[libpf.FileID]int32{}, - stringMap: map[string]int32{}, - fileID: libpf.UnsymbolizedFileID, + name: "with an index not yet in the file id mapping", + mappingSet: OrderedSet[libpf.FileID]{}, + stringSet: OrderedSet[string]{}, + fileID: libpf.UnsymbolizedFileID, wantIndex: 0, - wantFileIDToMapping: map[libpf.FileID]int32{ + wantMappingSet: OrderedSet[libpf.FileID]{ libpf.UnsymbolizedFileID: 0, }, wantMappingTable: []int32{0}, - wantStringMap: map[string]int32{"": 0}, + wantStringSet: OrderedSet[string]{"": 0}, }, { name: "with an index not yet in the file id mapping and a filename in the string table", - fileIDToMapping: map[libpf.FileID]int32{}, - stringMap: map[string]int32{"": 42}, - fileID: libpf.UnsymbolizedFileID, + mappingSet: OrderedSet[libpf.FileID]{}, + stringSet: OrderedSet[string]{"": 42}, + fileID: libpf.UnsymbolizedFileID, wantIndex: 0, - wantFileIDToMapping: map[libpf.FileID]int32{ + wantMappingSet: OrderedSet[libpf.FileID]{ libpf.UnsymbolizedFileID: 0, }, wantMappingTable: []int32{42}, - wantStringMap: map[string]int32{"": 42}, + wantStringSet: OrderedSet[string]{"": 42}, }, } { t.Run(tt.name, func(t *testing.T) { - fitm := tt.fileIDToMapping - stringMap := tt.stringMap - profile := pprofile.NewProfile() - mgr := samples.NewAttrTableManager(profile.AttributeTable()) + mappingSet := tt.mappingSet + stringSet := tt.stringSet + dic := pprofile.NewProfilesDictionary() + mgr := samples.NewAttrTableManager(dic.AttributeTable()) + + idx, exists := mappingSet.AddWithCheck(tt.fileID) + if !exists { + mapping := dic.MappingTable().AppendEmpty() + mapping.SetFilenameStrindex(stringSet.Add("")) + mgr.AppendOptionalString(mapping.AttributeIndices(), + semconv.ProcessExecutableBuildIDHtlhashKey, + tt.fileID.StringNoQuotes()) + } - i := getDummyMappingIndex(fitm, stringMap, mgr, profile, tt.fileID) - assert.Equal(t, tt.wantIndex, i) - assert.Equal(t, tt.fileIDToMapping, fitm) - assert.Equal(t, tt.wantStringMap, stringMap) + assert.Equal(t, tt.wantIndex, idx) + assert.Equal(t, tt.wantMappingSet, mappingSet) + assert.Equal(t, tt.wantStringSet, stringSet) - require.Equal(t, len(tt.wantMappingTable), profile.MappingTable().Len()) + require.Equal(t, len(tt.wantMappingTable), dic.MappingTable().Len()) for i, v := range tt.wantMappingTable { - mapp := profile.MappingTable().At(i) + mapp := dic.MappingTable().At(i) assert.Equal(t, v, mapp.FilenameStrindex()) } }) } } +//nolint:lll func TestFunctionTableOrder(t *testing.T) { for _, tt := range []struct { name string @@ -170,16 +102,19 @@ func TestFunctionTableOrder(t *testing.T) { frames map[libpf.FileID]map[libpf.AddressOrLineno]samples.SourceInfo events map[libpf.Origin]samples.KeyToEventMapping - wantFunctionTable []string + wantFunctionTable []string + expectedResourceProfiles int }{ { - name: "with no executables", - executables: map[libpf.FileID]samples.ExecInfo{}, - frames: map[libpf.FileID]map[libpf.AddressOrLineno]samples.SourceInfo{}, - events: map[libpf.Origin]samples.KeyToEventMapping{}, - wantFunctionTable: []string{""}, + name: "no events", + executables: map[libpf.FileID]samples.ExecInfo{}, + frames: map[libpf.FileID]map[libpf.AddressOrLineno]samples.SourceInfo{}, + events: map[libpf.Origin]samples.KeyToEventMapping{}, + wantFunctionTable: []string{""}, + expectedResourceProfiles: 0, }, { - name: "single executable", + name: "single executable", + expectedResourceProfiles: 1, executables: map[libpf.FileID]samples.ExecInfo{ libpf.NewFileID(2, 3): {}, }, @@ -249,29 +184,72 @@ func TestFunctionTableOrder(t *testing.T) { t.Run(tt.name, func(t *testing.T) { d, err := New(100, 100, 100, nil) require.NoError(t, err) - for k, v := range tt.frames { - frames := xsync.NewRWMutex[map[libpf.AddressOrLineno]samples.SourceInfo](v) - d.Frames.Add(k, &frames) + for fileID, addrWithSourceInfos := range tt.frames { + for addr, si := range addrWithSourceInfos { + d.Frames.Add(libpf.NewFrameID(fileID, addr), si) + } } for k, v := range tt.executables { d.Executables.Add(k, v) } - res := d.Generate(tt.events) - expectedProfiles := len(tt.events) - require.Equal(t, 1, res.ResourceProfiles().Len()) + tree := make(samples.TraceEventsTree) + tree[""] = tt.events + res, _ := d.Generate(tree, tt.name, "version") + require.Equal(t, tt.expectedResourceProfiles, res.ResourceProfiles().Len()) + if tt.expectedResourceProfiles == 0 { + // Do not check elements of ResourceProfile if there is no expected + // ResourceProfile. + return + } require.Equal(t, 1, res.ResourceProfiles().At(0).ScopeProfiles().Len()) + expectedProfiles := len(tt.events) require.Equal(t, expectedProfiles, res.ResourceProfiles(). At(0).ScopeProfiles(). At(0).Profiles().Len()) if expectedProfiles == 0 { return } - p := res.ResourceProfiles().At(0).ScopeProfiles().At(0).Profiles().At(0) - require.Equal(t, len(tt.wantFunctionTable), p.FunctionTable().Len()) - for i := 0; i < p.FunctionTable().Len(); i++ { - funcName := p.StringTable().At(int(p.FunctionTable().At(i).NameStrindex())) + dic := res.ProfilesDictionary() + require.Equal(t, len(tt.wantFunctionTable), dic.FunctionTable().Len()) + for i := 0; i < dic.FunctionTable().Len(); i++ { + funcName := dic.StringTable().At(int(dic.FunctionTable().At(i).NameStrindex())) assert.Equal(t, tt.wantFunctionTable[i], funcName) } }) } } + +func TestProfileDuration(t *testing.T) { + for _, tt := range []struct { + name string + events map[libpf.Origin]samples.KeyToEventMapping + }{ + { + name: "profile duration", + events: map[libpf.Origin]samples.KeyToEventMapping{ + support.TraceOriginSampling: map[samples.TraceAndMetaKey]*samples.TraceEvents{ + {Pid: 1}: { + Timestamps: []uint64{2, 1, 3, 4, 7}, + }, + {Pid: 2}: { + Timestamps: []uint64{8}, + }, + }, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + d, err := New(100, 100, 100, nil) + require.NoError(t, err) + + tree := make(samples.TraceEventsTree) + tree[""] = tt.events + res, err := d.Generate(tree, tt.name, "version") + require.NoError(t, err) + + profile := res.ResourceProfiles().At(0).ScopeProfiles().At(0).Profiles().At(0) + require.Equal(t, pcommon.Timestamp(7), profile.Duration()) + require.Equal(t, pcommon.Timestamp(1), profile.StartTime()) + }) + } +} diff --git a/reporter/internal/pdata/helper.go b/reporter/internal/pdata/helper.go new file mode 100644 index 000000000..8dbb6a5e3 --- /dev/null +++ b/reporter/internal/pdata/helper.go @@ -0,0 +1,47 @@ +package pdata // import "go.opentelemetry.io/ebpf-profiler/reporter/internal/pdata" + +// OrderedSet is a set that keeps order of insertion. +type OrderedSet[T comparable] map[T]int32 + +// Add adds an element to the set and returns its index. +func (os OrderedSet[T]) Add(key T) int32 { + idx, _ := os.AddWithCheck(key) + return idx +} + +// AddWithCheck adds an element to the set, returns its index and presence state. +func (os OrderedSet[T]) AddWithCheck(key T) (int32, bool) { + if idx, exists := os[key]; exists { + return idx, true + } + + idx := int32(len(os)) + os[key] = idx + return idx, false +} + +// ToSlice returns the elements of the set as a slice, in insertion order. +func (os OrderedSet[T]) ToSlice() []T { + ret := make([]T, len(os)) + for key, idx := range os { + ret[idx] = key + } + + return ret +} + +// locationInfo is a helper used to deduplicate Locations. +type locationInfo struct { + address uint64 + mappingIndex int32 + frameType string + hasLine bool + lineNumber int64 + functionIndex int32 +} + +// funcInfo is a helper to construct profile.Function messages. +type funcInfo struct { + nameIdx int32 + fileNameIdx int32 +} diff --git a/reporter/internal/pdata/helper_test.go b/reporter/internal/pdata/helper_test.go new file mode 100644 index 000000000..9b7b80a22 --- /dev/null +++ b/reporter/internal/pdata/helper_test.go @@ -0,0 +1,54 @@ +package pdata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOrderedSet(t *testing.T) { + for _, tt := range []struct { + name string + set OrderedSet[string] + key string + + wantSet OrderedSet[string] + wantIndex int32 + wantExists bool + }{ + { + name: "with a value not yet in the string map", + set: OrderedSet[string]{}, + key: "foo", + + wantIndex: 0, + wantSet: OrderedSet[string]{"foo": 0}, + wantExists: false, + }, + { + name: "with a duplicate value already in the string map", + set: OrderedSet[string]{"foo": 0, "bar": 1}, + key: "bar", + + wantIndex: 1, + wantSet: OrderedSet[string]{"foo": 0, "bar": 1}, + wantExists: true, + }, + { + name: "with a non-duplicate value already in the string map", + set: OrderedSet[string]{"foo": 0}, + key: "baz", + + wantIndex: 1, + wantSet: OrderedSet[string]{"foo": 0, "baz": 1}, + wantExists: false, + }, + } { + t.Run(tt.name, func(t *testing.T) { + i, exists := tt.set.AddWithCheck(tt.key) + assert.Equal(t, tt.wantIndex, i) + assert.Equal(t, tt.wantSet, tt.set) + assert.Equal(t, tt.wantExists, exists) + }) + } +} diff --git a/reporter/internal/pdata/pdata.go b/reporter/internal/pdata/pdata.go index 5d67ff58f..43caefd8d 100644 --- a/reporter/internal/pdata/pdata.go +++ b/reporter/internal/pdata/pdata.go @@ -7,7 +7,6 @@ import ( lru "github.com/elastic/go-freelru" "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/libpf/xsync" "go.opentelemetry.io/ebpf-profiler/reporter/samples" ) @@ -21,10 +20,7 @@ type Pdata struct { Executables *lru.SyncedLRU[libpf.FileID, samples.ExecInfo] // Frames maps frame information to its source location. - Frames *lru.SyncedLRU[ - libpf.FileID, - *xsync.RWMutex[map[libpf.AddressOrLineno]samples.SourceInfo], - ] + Frames *lru.SyncedLRU[libpf.FrameID, samples.SourceInfo] // ExtraSampleAttrProd is an optional hook point for adding custom // attributes to samples. @@ -40,9 +36,8 @@ func New(samplesPerSecond int, executablesCacheElements, framesCacheElements uin } executables.SetLifetime(ExecutableCacheLifetime) // Allow GC to clean stale items. - frames, err := lru.NewSynced[libpf.FileID, - *xsync.RWMutex[map[libpf.AddressOrLineno]samples.SourceInfo]]( - framesCacheElements, libpf.FileID.Hash32) + frames, err := + lru.NewSynced[libpf.FrameID, samples.SourceInfo](framesCacheElements, libpf.FrameID.Hash32) if err != nil { return nil, err } diff --git a/reporter/otlp_reporter.go b/reporter/otlp_reporter.go index a0d005573..54e9f5d11 100644 --- a/reporter/otlp_reporter.go +++ b/reporter/otlp_reporter.go @@ -6,45 +6,31 @@ package reporter // import "go.opentelemetry.io/ebpf-profiler/reporter" import ( "context" "crypto/tls" - "maps" - "strconv" "time" lru "github.com/elastic/go-freelru" log "github.com/sirupsen/logrus" - "go.opentelemetry.io/collector/pdata/pprofile" "go.opentelemetry.io/collector/pdata/pprofile/pprofileotlp" - semconv "go.opentelemetry.io/otel/semconv/v1.30.0" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/encoding/gzip" "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/libpf/xsync" "go.opentelemetry.io/ebpf-profiler/reporter/internal/pdata" "go.opentelemetry.io/ebpf-profiler/reporter/samples" - "go.opentelemetry.io/ebpf-profiler/support" ) // Assert that we implement the full Reporter interface. var _ Reporter = (*OTLPReporter)(nil) +var gzipOption = grpc.UseCompressor(gzip.Name) + // OTLPReporter receives and transforms information to be OTLP/profiles compliant. type OTLPReporter struct { *baseReporter - // hostID is the unique identifier of the host. - hostID string - - // kernelVersion is the version of the kernel. - kernelVersion string - - // hostName is the name of the host. - hostName string - - // ipAddress is the IP address of the host. - ipAddress string - // client for the connection to the receiver. client pprofileotlp.GRPCClient @@ -58,14 +44,6 @@ type OTLPReporter struct { // NewOTLP returns a new instance of OTLPReporter func NewOTLP(cfg *Config) (*OTLPReporter, error) { - cgroupv2ID, err := lru.NewSynced[libpf.PID, string](cfg.CGroupCacheElements, - func(pid libpf.PID) uint32 { return uint32(pid) }) - if err != nil { - return nil, err - } - // Set a lifetime to reduce risk of invalid data in case of PID reuse. - cgroupv2ID.SetLifetime(90 * time.Second) - // Next step: Dynamically configure the size of this LRU. // Currently, we use the length of the JSON array in // hostmetadata/hostmetadata.json. @@ -84,11 +62,7 @@ func NewOTLP(cfg *Config) (*OTLPReporter, error) { return nil, err } - originsMap := make(map[libpf.Origin]samples.KeyToEventMapping, 2) - for _, origin := range []libpf.Origin{support.TraceOriginSampling, - support.TraceOriginOffCPU} { - originsMap[origin] = make(samples.KeyToEventMapping) - } + eventsTree := make(samples.TraceEventsTree) return &OTLPReporter{ baseReporter: &baseReporter{ @@ -96,17 +70,12 @@ func NewOTLP(cfg *Config) (*OTLPReporter, error) { name: cfg.Name, version: cfg.Version, pdata: data, - cgroupv2ID: cgroupv2ID, - traceEvents: xsync.NewRWMutex(originsMap), + traceEvents: xsync.NewRWMutex(eventsTree), hostmetadata: hostmetadata, runLoop: &runLoop{ stopSignal: make(chan libpf.Void), }, }, - kernelVersion: cfg.KernelVersion, - hostName: cfg.HostName, - ipAddress: cfg.IPAddress, - hostID: strconv.FormatUint(cfg.HostID, 10), pkgGRPCOperationTimeout: cfg.GRPCOperationTimeout, client: nil, }, nil @@ -135,7 +104,6 @@ func (r *OTLPReporter) Start(ctx context.Context) error { }, func() { // Allow the GC to purge expired entries to avoid memory leaks. r.pdata.Purge() - r.cgroupv2ID.PurgeExpired() }) // When Stop() is called and a signal to 'stop' is received, then: @@ -154,55 +122,30 @@ func (r *OTLPReporter) Start(ctx context.Context) error { // reportOTLPProfile creates and sends out an OTLP profile. func (r *OTLPReporter) reportOTLPProfile(ctx context.Context) error { - traceEvents := r.traceEvents.WLock() - events := make(map[libpf.Origin]samples.KeyToEventMapping, 2) - for _, origin := range []libpf.Origin{support.TraceOriginSampling, - support.TraceOriginOffCPU} { - events[origin] = maps.Clone((*traceEvents)[origin]) - clear((*traceEvents)[origin]) - } - r.traceEvents.WUnlock(&traceEvents) + traceEventsPtr := r.traceEvents.WLock() + reportedEvents := (*traceEventsPtr) + newEvents := make(samples.TraceEventsTree) + *traceEventsPtr = newEvents + r.traceEvents.WUnlock(&traceEventsPtr) - profiles := r.pdata.Generate(events) - for i := 0; i < profiles.ResourceProfiles().Len(); i++ { - r.setResource(profiles.ResourceProfiles().At(i)) + profiles, err := r.pdata.Generate(reportedEvents, r.name, r.version) + if err != nil { + log.Errorf("pdata: %v", err) + return nil } - if profiles.SampleCount() == 0 { log.Debugf("Skip sending of OTLP profile with no samples") return nil } + req := pprofileotlp.NewExportRequestFromProfiles(profiles) reqCtx, ctxCancel := context.WithTimeout(ctx, r.pkgGRPCOperationTimeout) defer ctxCancel() - _, err := r.client.Export(reqCtx, req) + _, err = r.client.Export(reqCtx, req, gzipOption) return err } -// setResource sets the resource information of the origin of the profiles. -// Next step: maybe extend this information with go.opentelemetry.io/otel/sdk/resource. -func (r *OTLPReporter) setResource(rp pprofile.ResourceProfiles) { - keys := r.hostmetadata.Keys() - attrs := rp.Resource().Attributes() - - // Add hostmedata to the attributes. - for _, k := range keys { - if v, ok := r.hostmetadata.Get(k); ok { - attrs.PutStr(k, v) - } - } - - // Add event specific attributes. - // These attributes are also included in the host metadata, but with different names/keys. - // That makes our hostmetadata attributes incompatible with OTEL collectors. - attrs.PutStr(string(semconv.HostIDKey), r.hostID) - attrs.PutStr(string(semconv.HostIPKey), r.ipAddress) - attrs.PutStr(string(semconv.HostNameKey), r.hostName) - attrs.PutStr(string(semconv.ServiceVersionKey), r.version) - attrs.PutStr("os.kernel", r.kernelVersion) -} - // waitGrpcEndpoint waits until the gRPC connection is established. func waitGrpcEndpoint(ctx context.Context, cfg *Config) (*grpc.ClientConn, error) { // Sleep with a fixed backoff time added of +/- 20% jitter diff --git a/reporter/samples/attrmgr_test.go b/reporter/samples/attrmgr_test.go index bd03376b4..85e23338a 100644 --- a/reporter/samples/attrmgr_test.go +++ b/reporter/samples/attrmgr_test.go @@ -12,7 +12,7 @@ import ( "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pprofile" "go.opentelemetry.io/ebpf-profiler/libpf" - semconv "go.opentelemetry.io/otel/semconv/v1.30.0" + semconv "go.opentelemetry.io/otel/semconv/v1.34.0" ) type attributeStruct struct { diff --git a/reporter/samples/samples.go b/reporter/samples/samples.go index 466c5ff67..e2f2ed617 100644 --- a/reporter/samples/samples.go +++ b/reporter/samples/samples.go @@ -11,6 +11,7 @@ type TraceEventMeta struct { ProcessName string ExecutablePath string APMServiceName string + ContainerID string PID, TID libpf.PID CPU int Origin libpf.Origin @@ -39,9 +40,10 @@ type TraceAndMetaKey struct { // comm and apmServiceName are provided by the eBPF programs Comm string ApmServiceName string - // containerID is annotated based on PID information + // ContainerID is annotated based on PID information ContainerID string Pid int64 + Tid int64 // Process name is retrieved from /proc/PID/comm ProcessName string // Executable path is retrieved from /proc/PID/exe @@ -52,6 +54,13 @@ type TraceAndMetaKey struct { ExtraMeta any } +// TraceEventsTree stores samples and their related metadata in a tree-like +// structure optimized for the OTel Profiling protocol representation. +type TraceEventsTree map[ContainerID]map[libpf.Origin]KeyToEventMapping + +// ContainerID represents an extracted key from /proc//cgroup. +type ContainerID string + // KeyToEventMapping supports temporary mapping traces to additional information. type KeyToEventMapping map[TraceAndMetaKey]*TraceEvents @@ -76,9 +85,3 @@ type SourceInfo struct { FunctionName string FilePath string } - -// FuncInfo is a helper to construct profile.Function messages. -type FuncInfo struct { - Name string - FileName string -} diff --git a/rust-crates/symblib-capi/Cargo.toml b/rust-crates/symblib-capi/Cargo.toml index 0ab380bb5..161e93e14 100644 --- a/rust-crates/symblib-capi/Cargo.toml +++ b/rust-crates/symblib-capi/Cargo.toml @@ -6,10 +6,13 @@ rust-version.workspace = true license.workspace = true [lib] -crate-type = ["staticlib", "cdylib"] +crate-type = ["staticlib"] [dependencies] symblib.path = "../symblib" fallible-iterator.workspace = true thiserror.workspace = true + +[build-dependencies] +serde_json = "1.0.140" diff --git a/rust-crates/symblib-capi/build.rs b/rust-crates/symblib-capi/build.rs new file mode 100644 index 000000000..1adf94adf --- /dev/null +++ b/rust-crates/symblib-capi/build.rs @@ -0,0 +1,72 @@ +use std::{env, path::PathBuf, process::Command}; + +fn main() { + // Fetch the cargo build manifest. + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let output = Command::new("cargo") + .args(&["metadata", "--format-version=1", "--no-deps"]) + .current_dir(&manifest_dir) + .output() + .expect("Failed to execute cargo metadata"); + + if !output.status.success() { + println!("cargo:warning=Failed to get cargo metadata"); + return; + } + + let metadata: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("Failed to parse cargo metadata"); + + let pkg_name = env::var("CARGO_PKG_NAME").unwrap(); + let packages = metadata["packages"].as_array().unwrap(); + let current_package = packages + .iter() + .find(|p| p["name"].as_str().unwrap() == pkg_name) + .expect("Could not find current package in metadata"); + + let targets = current_package["targets"].as_array().unwrap(); + let has_staticlib_target = targets.iter().any(|t| { + let kinds = t["kind"].as_array().unwrap(); + kinds.iter().any(|k| k.as_str().unwrap() == "staticlib") + }); + + if !has_staticlib_target { + return; + } + + let target = match env::var("TARGET") { + Ok(t) => t, + Err(_) => return, + }; + + if !target.contains("-linux-musl") { + return; + } + + let out_dir = env::var("OUT_DIR").unwrap(); + + // Get the target-libdir for the specified target + // $(shell rustc --target $(RUST_TARGET) --print target-libdir)/self-contained/libunwind.a + let output = Command::new("rustc") + .args(&["--target", &target, "--print", "target-libdir"]) + .output() + .expect("failed to execute rustc"); + + if output.status.success() { + let target_libdir_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let libunwind_path = PathBuf::from(target_libdir_str) + .join("self-contained") + .join("libunwind.a"); + + if libunwind_path.exists() { + std::fs::copy(libunwind_path, format!("{}/libunwind.a", out_dir)).unwrap(); + + println!("cargo:rustc-link-search=native={}", out_dir); + println!("cargo:rustc-link-lib=static=unwind"); + } else { + println!("cargo:warning={:?} does not exist", libunwind_path); + } + } else { + println!("cargo:warning=failed to identify target-libdir for libunwind.a"); + } +} diff --git a/rust-crates/symblib/src/gosym/raw/regions.rs b/rust-crates/symblib/src/gosym/raw/regions.rs index af8800bdb..9f93634bf 100644 --- a/rust-crates/symblib/src/gosym/raw/regions.rs +++ b/rust-crates/symblib/src/gosym/raw/regions.rs @@ -153,12 +153,12 @@ impl<'obj> FuncTable<'obj> { mod tests { use super::*; use crate::gosym::GoRuntimeInfo; - use crate::tests::testdata; + use crate::tests::go_testdata; #[test] fn test_func_by_addr() -> Result<()> { - for test_file in ["go-1.20.14", "go-1.22.12", "go-1.24.0"] { - let obj = objfile::File::load(&testdata(test_file))?; + for test_file in go_testdata() { + let obj = objfile::File::load(&test_file)?; let obj = obj.parse()?; let runtime_info = GoRuntimeInfo::open(&obj)?; diff --git a/rust-crates/symblib/src/lib.rs b/rust-crates/symblib/src/lib.rs index c49136931..333b1a29c 100644 --- a/rust-crates/symblib/src/lib.rs +++ b/rust-crates/symblib/src/lib.rs @@ -64,4 +64,13 @@ mod tests { .join("testdata") .join(name) } + + // Get go testdata binaries + pub fn go_testdata() -> Vec { + vec![ + testdata("go-1.20.14"), + testdata("go-1.22.12"), + testdata("go-1.24.0"), + ] + } } diff --git a/rust-crates/symblib/src/symbconv/go.rs b/rust-crates/symblib/src/symbconv/go.rs index a5ac90d3f..636647faf 100644 --- a/rust-crates/symblib/src/symbconv/go.rs +++ b/rust-crates/symblib/src/symbconv/go.rs @@ -22,6 +22,9 @@ pub enum Error { #[error("visitor returned an error: {0}")] Visitor(#[source] AnyError), + + #[error("line mapping failed with error: {0}")] + GoSymbolBadLineMapping(#[source] AnyError), } /// Go symbol extraction statistics. @@ -55,6 +58,25 @@ impl<'obj> super::RangeExtractor for Extractor<'obj> { } } +fn find_func_end(func: &gosym::Func<'_, '_>) -> Result> { + let mut end: Option = None; + let mut iter = func.line_mapping()?; + loop { + match iter.next()? { + Some((rng, _)) => { + if let Some(e) = end { + if rng.end < e { + continue; + } + } + end = Some(rng.end) + } + None => break, + } + } + Ok(end) +} + fn extract_ranges(obj: &objfile::Reader<'_>, visitor: super::RangeVisitor<'_>) -> Result { let mut stats = Stats::default(); @@ -68,17 +90,29 @@ fn extract_ranges(obj: &objfile::Reader<'_>, visitor: super::RangeVisitor<'_>) - let mut func_iter = go.funcs()?; while let Some(func) = func_iter.next()? { - // Infer end of function from line tables. - let Some(end) = func.line_mapping()?.map(|(rng, _)| Ok(rng.end)).max()? else { - debug!( - "WARN: unable to determine end of function ({})", - func.name()? - ); - stats.funcs_skipped += 1; - continue; + let end = match find_func_end(&func) { + Ok(Some(e)) => e, + Ok(None) => { + debug!( + "WARN: unable to determine end of function ({})", + func.name()? + ); + stats.funcs_skipped += 1; + continue; + } + Err(Error::Gosym(gosym::Error::BadLineNumber)) => { + // skip function once we hit a BadLineNumber error + debug!("WARN: bad line number of function ({})", func.name()?); + stats.funcs_skipped += 1; + continue; + } + Err(e) => { + return Err(Error::GoSymbolBadLineMapping(e.into())); + } }; let length = end.saturating_sub(func.start_addr()); + if length == 0 { debug!("WARN: zero function length ({})", func.name()?); stats.funcs_skipped += 1; @@ -111,3 +145,46 @@ fn extract_ranges(obj: &objfile::Reader<'_>, visitor: super::RangeVisitor<'_>) - Ok(stats) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::go_testdata; + + #[test] + fn test_extract_ranges() -> Result<(), AnyError> { + for test_file in go_testdata() { + let obj = objfile::File::load(&test_file)?; + let obj = obj.parse()?; + + let mut ranges = Vec::new(); + let mut visitor = |range: symbfile::Range| -> Result<(), AnyError> { + ranges.push(range); + Ok(()) + }; + extract_ranges(&obj, &mut visitor)?; + + // Verify we got some ranges + assert!(!ranges.is_empty()); + + // Verify ranges contains the main function + assert!( + ranges.iter().any(|range| range.func == "main.main" + && range.file.is_some() + && range.file.as_ref().unwrap().ends_with("main.go")), + "main.main not found in {:?}", + test_file + ); + + // Verify ranges are valid + for range in ranges { + // Basic validity checks + assert!(range.elf_va > 0); + assert!(range.length > 0); + assert!(!range.func.is_empty()); + assert!(range.file.is_some()); + } + } + Ok(()) + } +} diff --git a/stringutil/stringutil.go b/stringutil/stringutil.go index 06a199882..3be749285 100644 --- a/stringutil/stringutil.go +++ b/stringutil/stringutil.go @@ -82,5 +82,5 @@ func SplitN(s, sep string, f []string) int { // Be aware that the byte slice and the string share the same memory - which makes // the string mutable. func ByteSlice2String(b []byte) string { - return *(*string)(unsafe.Pointer(&b)) + return unsafe.String(unsafe.SliceData(b), len(b)) } diff --git a/support/README.md b/support/README.md index 181646b4a..5cb0bcbdf 100644 --- a/support/README.md +++ b/support/README.md @@ -4,11 +4,10 @@ translation. ## Testing eBPF code on different kernel version Via the following commands, you can run the eBPF loading tests on kernel version -4.9.198, 4.19.81 or 5.4.5 respectively. +5.4.276 or 6.12.16 respectively. ``` -$ ./run-tests.sh 4.9.198 -$ ./run-tests.sh 4.19.81 -$ ./run-tests.sh 5.4.5 +$ ./run-tests.sh 5.4.276 +$ ./run-tests.sh 6.12.16 ``` The script loads the provided eBPF code into the kernel in a virtual environment so that it does not affect your local environment. @@ -24,7 +23,7 @@ The tests are built on top of the following dependencies. Make sure you have the ## Test a Custom Kernel Image By default `run-tests.sh` takes only the kernel version as argument. The script looks for the kernel image with the specified version in `ci-kernels`. As an alternative one can provide a directory to look for this kernel image via `KERN_DIR`. ``` - $ KERN_DIR=my-other-kernels/ ./run-tests.sh 5.4.31 + $ KERN_DIR=my-other-kernels/ ./run-tests.sh 5.4.276 ``` ## Manually Debugging a Custom Kernel Image @@ -41,7 +40,7 @@ $ git clone -q https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git "${ ``` 3. Start the virtual environment for debugging with gdb: ``` -$ ${tmp_virtme}/virtme-run --kimg ci-kernels/linux-5.4.31.bz \ +$ ${tmp_virtme}/virtme-run --kimg ci-kernels/linux-5.4.276.bz \ --memory 4096M \ --pwd \ --script-sh "mount -t bpf bpf /sys/fs/bpf ; ./support.test -test.v" \ @@ -54,9 +53,9 @@ $ gdb # Attach gdb to the running qemu process in the same directory: (gdb) target remote localhost:1234 # Load source code: -(gdb) directory ./ci-kernels/_build/linux-5.4.31 +(gdb) directory ./ci-kernels/_build/linux-5.4.276 # Load symbols for debugging: -(gdb) sym ./ci-kernels/_build/linux-5.4.31/vmlinux +(gdb) sym ./ci-kernels/_build/linux-5.4.276/vmlinux # Set breakpoint at entry of eBPF verifier: (gdb) break do_check Breakpoint 1 at 0xffffffff81184460: file kernel/bpf/verifier.c, line 4105. diff --git a/support/ebpf/bpfdefs.h b/support/ebpf/bpfdefs.h index 533bf8c77..edd41239b 100644 --- a/support/ebpf/bpfdefs.h +++ b/support/ebpf/bpfdefs.h @@ -8,6 +8,7 @@ // tools/coredump uses CGO to build the eBPF code. Provide here the glue to // dispatch the BPF API to helpers implemented in ebpfhelpers.go. #define SEC(NAME) + #define EBPF_INLINE #define printt(fmt, ...) bpf_log(fmt, ##__VA_ARGS__) #define DEBUG_PRINT(fmt, ...) bpf_log(fmt, ##__VA_ARGS__) @@ -141,7 +142,10 @@ static long (*bpf_probe_read_kernel)(void *dst, int size, const void *unsafe_ptr #define SEC(name) \ _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wignored-attributes\"") \ __attribute__((section(name), used)) _Pragma("GCC diagnostic pop") + #define EBPF_INLINE __attribute__((__always_inline__)) #endif // !TESTING_COREDUMP +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) + #endif // OPTI_BPFDEFS_H diff --git a/support/ebpf/dotnet_tracer.ebpf.c b/support/ebpf/dotnet_tracer.ebpf.c index c9155120e..5edd3aea2 100644 --- a/support/ebpf/dotnet_tracer.ebpf.c +++ b/support/ebpf/dotnet_tracer.ebpf.c @@ -44,7 +44,7 @@ bpf_map_def SEC("maps") dotnet_procs = { // currently not Garbage Collected by the runtime. Though, we have submitted also an enhancement // request to fix the nibble map format to something sane, and this might get implemented. // see: https://github.com/dotnet/runtime/issues/93550 -static inline __attribute__((__always_inline__)) ErrorCode +static EBPF_INLINE ErrorCode dotnet_find_code_start(PerCPURecord *record, DotnetProcInfo *vi, u64 pc, u64 *code_start) { // This is an ebpf optimized implementation of EEJitManager::FindMethodCode() @@ -152,7 +152,7 @@ dotnet_find_code_start(PerCPURecord *record, DotnetProcInfo *vi, u64 pc, u64 *co } // Record a Dotnet frame -static inline __attribute__((__always_inline__)) ErrorCode +static EBPF_INLINE ErrorCode push_dotnet(Trace *trace, u64 code_header_ptr, u64 pc_offset, bool return_address) { return _push_with_return_address( @@ -160,7 +160,7 @@ push_dotnet(Trace *trace, u64 code_header_ptr, u64 pc_offset, bool return_addres } // Unwind one dotnet frame -static inline __attribute__((__always_inline__)) ErrorCode +static EBPF_INLINE ErrorCode unwind_one_dotnet_frame(PerCPURecord *record, DotnetProcInfo *vi, bool top) { UnwindState *state = &record->state; @@ -264,7 +264,7 @@ unwind_one_dotnet_frame(PerCPURecord *record, DotnetProcInfo *vi, bool top) // unwind_dotnet is the entry point for tracing when invoked from the native tracer // or interpreter dispatcher. It does not reset the trace object and will append the // dotnet stack frames to the trace object for the current CPU. -static inline __attribute__((__always_inline__)) int unwind_dotnet(struct pt_regs *ctx) +static EBPF_INLINE int unwind_dotnet(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); if (!record) { diff --git a/support/ebpf/extmaps.h b/support/ebpf/extmaps.h index 6cd347f19..f7157c6c0 100644 --- a/support/ebpf/extmaps.h +++ b/support/ebpf/extmaps.h @@ -18,6 +18,7 @@ extern bpf_map_def inhibit_events; extern bpf_map_def interpreter_offsets; extern bpf_map_def system_config; extern bpf_map_def trace_events; +extern bpf_map_def go_labels_procs; #if defined(TESTING_COREDUMP) diff --git a/support/ebpf/frametypes.h b/support/ebpf/frametypes.h index ef2bc1a57..ff87c15cb 100644 --- a/support/ebpf/frametypes.h +++ b/support/ebpf/frametypes.h @@ -33,6 +33,8 @@ #define FRAME_MARKER_PHP_JIT 0x9 // Indicates a Dotnet frame #define FRAME_MARKER_DOTNET 0xA +// Indicates a Go frame +#define FRAME_MARKER_GO 0xB // Indicates a frame containing information about a critical unwinding error // that caused further unwinding to be aborted. diff --git a/support/ebpf/go_labels.ebpf.c b/support/ebpf/go_labels.ebpf.c new file mode 100644 index 000000000..c0897905f --- /dev/null +++ b/support/ebpf/go_labels.ebpf.c @@ -0,0 +1,192 @@ +// This file contains the code for extracting custom labels from Go runtime. + +#include "bpfdefs.h" +#include "kernel.h" +#include "tracemgmt.h" +#include "tsd.h" +#include "types.h" + +static EBPF_INLINE bool +get_go_custom_labels_from_slice(struct pt_regs *ctx, PerCPURecord *record, void *labels_slice_ptr) +{ + // https://github.com/golang/go/blob/80e2e474/src/runtime/pprof/label.go#L20 + struct GoSlice labels_slice; + if (bpf_probe_read_user(&labels_slice, sizeof(struct GoSlice), labels_slice_ptr)) { + DEBUG_PRINT("cl: failed to read value for labels slice (%lx)", (unsigned long)labels_slice_ptr); + return false; + } + + CustomLabelsArray *out = &record->trace.custom_labels; + // len is number of pairs, ie its a vector of key/val structs. + u8 num_to_read = MIN(labels_slice.len, MAX_CUSTOM_LABELS); + if (bpf_probe_read_user( + &record->labels, sizeof(struct GoString) * 2 * num_to_read, labels_slice.array)) { + DEBUG_PRINT( + "cl: failed to read strings from labels slice (%lx)", (unsigned long)labels_slice.array); + return false; + } + + for (u8 i = 0; i < MAX_CUSTOM_LABELS; i++) { + if (i >= labels_slice.len) + break; + CustomLabel *lbl = &out->labels[i]; + u8 klen = MIN(record->labels[i * 2].len, CUSTOM_LABEL_MAX_KEY_LEN - 1); + if (bpf_probe_read_user(lbl->key, klen, record->labels[i * 2].str)) { + DEBUG_PRINT( + "cl: failed to read key for custom label (%lx)", (unsigned long)record->labels[i * 2].str); + return false; + } + u8 vlen = MIN(record->labels[i * 2 + 1].len, CUSTOM_LABEL_MAX_VAL_LEN - 1); + if (bpf_probe_read_user(lbl->val, vlen, record->labels[i * 2 + 1].str)) { + DEBUG_PRINT( + "cl: failed to read key for custom label (%lx)", + (unsigned long)record->labels[i * 2 + 1].str); + return false; + } + } + out->len = num_to_read; + + return true; +} + +// https://github.com/golang/go/blob/6885bad7dd86880be6929c02085/src/internal/abi/map.go#L12 +#define GO_MAP_BUCKET_SIZE 8 + +static EBPF_INLINE bool get_go_custom_labels_from_map( + struct pt_regs *ctx, PerCPURecord *record, void *labels_map_ptr_ptr, GoLabelsOffsets *offs) +{ + void *labels_map_ptr; + if (bpf_probe_read_user(&labels_map_ptr, sizeof(labels_map_ptr), labels_map_ptr_ptr)) { + DEBUG_PRINT( + "cl: failed to read value for labels_map_ptr (%lx)", (unsigned long)labels_map_ptr_ptr); + return false; + } + + u64 labels_count = 0; + if (bpf_probe_read_user(&labels_count, sizeof(labels_count), labels_map_ptr + offs->hmap_count)) { + DEBUG_PRINT("cl: failed to read value for labels_count"); + return false; + } + if (labels_count == 0) { + DEBUG_PRINT("cl: no labels"); + return false; + } + + unsigned char log_2_bucket_count; + if (bpf_probe_read_user( + &log_2_bucket_count, + sizeof(log_2_bucket_count), + labels_map_ptr + offs->hmap_log2_bucket_count)) { + DEBUG_PRINT("cl: failed to read value for bucket_count"); + return false; + } + void *label_buckets; + if (bpf_probe_read_user( + &label_buckets, sizeof(label_buckets), labels_map_ptr + offs->hmap_buckets)) { + DEBUG_PRINT("cl: failed to read value for label_buckets"); + return false; + } + + CustomLabelsArray *out = &record->trace.custom_labels; + // If the map has more than 16 buckets we just don't support it, pprof maps are typically + // small and if its a problem upgrading to Go 1.24+ is a potential solution. + u8 bucket_count = 1 << log_2_bucket_count; + for (u8 b = 0; b < 16; b++) { + if (b >= bucket_count) + break; + GoMapBucket *map_value = &record->goMapBucket; + if (bpf_probe_read_user( + map_value, sizeof(GoMapBucket), label_buckets + (b * sizeof(GoMapBucket)))) { + return false; + } + + for (u8 i = 0; i < GO_MAP_BUCKET_SIZE; i++) { + if (out->len >= MAX_CUSTOM_LABELS) + return true; + CustomLabel *lbl = &out->labels[out->len]; + char tophash = map_value->tophash[i]; + char *kstr = map_value->keys[i].str; + unsigned klen = map_value->keys[i].len; + char *vstr = map_value->values[i].str; + unsigned vlen = map_value->values[i].len; + if (tophash != 0 && kstr != NULL) { + if (bpf_probe_read_user(lbl->key, MIN(klen, CUSTOM_LABEL_MAX_KEY_LEN - 1), kstr)) { + DEBUG_PRINT("cl: failed to read key for custom label (%lx)", (unsigned long)kstr); + return false; + } + if (bpf_probe_read_user(lbl->val, MIN(vlen, CUSTOM_LABEL_MAX_VAL_LEN - 1), vstr)) { + DEBUG_PRINT("cl: failed to read value for custom label"); + return false; + } + out->len++; + } + } + } + + return true; +} + +// Go processes store the current goroutine in thread local store. From there +// this reads the g (aka goroutine) struct, then the m (the actual operating +// system thread) of that goroutine, and finally curg (current goroutine). This +// chain is necessary because getg().m.curg points to the current user g +// assigned to the thread (curg == getg() when not on the system stack). curg +// may be nil if there is no user g, such as when running in the scheduler. If +// curg is nil, then g is either a system stack (called g0) or a signal handler +// g (gsignal). Neither one will ever have label. +static EBPF_INLINE bool +get_go_custom_labels(struct pt_regs *ctx, PerCPURecord *record, GoLabelsOffsets *offs) +{ + size_t curg_ptr_addr; + if (bpf_probe_read_user( + &curg_ptr_addr, + sizeof(void *), + (void *)(record->customLabelsState.go_m_ptr + offs->curg))) { + DEBUG_PRINT("cl: failed to read value for m_ptr->curg"); + return false; + } + + void *labels_ptr; + if (bpf_probe_read_user(&labels_ptr, sizeof(void *), (void *)(curg_ptr_addr + offs->labels))) { + DEBUG_PRINT( + "cl: failed to read value for curg->labels (%lx->%lx)", + (unsigned long)curg_ptr_addr, + (unsigned long)offs->labels); + return false; + } + + if (offs->hmap_buckets == 0) { + // go 1.24+ labels is a slice + return get_go_custom_labels_from_slice(ctx, record, labels_ptr); + } + + // go 1.23- labels is a map + return get_go_custom_labels_from_map(ctx, record, labels_ptr, offs); +} + +// go_labels is the entrypoint for extracting custom labels from Go runtime. +static EBPF_INLINE int go_labels(struct pt_regs *ctx) +{ + PerCPURecord *record = get_per_cpu_record(); + if (!record) + return -1; + + u32 pid = record->trace.pid; + GoLabelsOffsets *offsets = bpf_map_lookup_elem(&go_labels_procs, &pid); + if (!offsets) { + DEBUG_PRINT("cl: no offsets, %d not recognized as a go binary", pid); + return -1; + } + DEBUG_PRINT( + "cl: go offsets found, %d recognized as a go binary: m_ptr: %lx", + pid, + (unsigned long)record->customLabelsState.go_m_ptr); + bool success = get_go_custom_labels(ctx, record, offsets); + if (!success) { + increment_metric(metricID_UnwindGoLabelsFailures); + } + + send_trace(ctx, &record->trace); + return 0; +} +MULTI_USE_FUNC(go_labels) diff --git a/support/ebpf/hotspot_tracer.ebpf.c b/support/ebpf/hotspot_tracer.ebpf.c index 8094bcf55..8121f286f 100644 --- a/support/ebpf/hotspot_tracer.ebpf.c +++ b/support/ebpf/hotspot_tracer.ebpf.c @@ -101,22 +101,20 @@ bpf_map_def SEC("maps") hotspot_procs = { }; // Record a HotSpot frame -static inline __attribute__((__always_inline__)) ErrorCode -push_hotspot(Trace *trace, u64 file, u64 line, bool return_address) +static EBPF_INLINE ErrorCode push_hotspot(Trace *trace, u64 file, u64 line, bool return_address) { return _push_with_return_address(trace, file, line, FRAME_MARKER_HOTSPOT, return_address); } // calc_line merges the three values to be encoded in a frame 'line' -static inline __attribute__((__always_inline__)) u64 -calc_line(u8 subtype, u32 pc_or_bci, u32 ptr_check) +static EBPF_INLINE u64 calc_line(u8 subtype, u32 pc_or_bci, u32 ptr_check) { return ((u64)subtype << 60) | ((u64)pc_or_bci << 32) | (u64)ptr_check; } #ifdef __x86_64__ // hotspot_addr_in_codecache checks if given address belongs to the JVM JIT code cache -__attribute__((always_inline)) inline static bool hotspot_addr_in_codecache(u32 pid, u64 addr) +static EBPF_INLINE bool hotspot_addr_in_codecache(u32 pid, u64 addr) { PIDPage key = {}; key.prefixLen = BIT_WIDTH_PID + BIT_WIDTH_PAGE; @@ -140,8 +138,7 @@ __attribute__((always_inline)) inline static bool hotspot_addr_in_codecache(u32 // hotspot_find_codeblob maps a given PC to the CodeBlob* that describes the // JIT information regarding the method (or stub) this PC belongs to. This uses // information from the PidPageMapping for the PC. -static inline __attribute__((__always_inline__)) u64 -hotspot_find_codeblob(const UnwindState *state, const HotspotProcInfo *ji) +static EBPF_INLINE u64 hotspot_find_codeblob(const UnwindState *state, const HotspotProcInfo *ji) { unsigned long segment, codeblob, segmap_start; u8 tag; @@ -191,7 +188,7 @@ hotspot_find_codeblob(const UnwindState *state, const HotspotProcInfo *ji) return codeblob; } -__attribute__((always_inline)) inline static ErrorCode +static EBPF_INLINE ErrorCode hotspot_handle_vtable_chunks(HotspotUnwindInfo *ui, HotspotUnwindAction *action) { DEBUG_PRINT("jvm: -> unwind vtable"); @@ -209,7 +206,7 @@ hotspot_handle_vtable_chunks(HotspotUnwindInfo *ui, HotspotUnwindAction *action) return ERR_OK; } -__attribute__((always_inline)) inline static ErrorCode hotspot_handle_interpreter( +static EBPF_INLINE ErrorCode hotspot_handle_interpreter( UnwindState *state, Trace *trace, HotspotUnwindInfo *ui, @@ -307,12 +304,12 @@ __attribute__((always_inline)) inline static ErrorCode hotspot_handle_interprete } #if defined(__x86_64__) -__attribute__((always_inline)) inline static void breadcrumb_fixup(HotspotUnwindInfo *ui) +static EBPF_INLINE void breadcrumb_fixup(HotspotUnwindInfo *ui) { // Nothing to do: breadcrumbs are not a thing on X86. } #elif defined(__aarch64__) -__attribute__((always_inline)) inline static void breadcrumb_fixup(HotspotUnwindInfo *ui) +static EBPF_INLINE void breadcrumb_fixup(HotspotUnwindInfo *ui) { // On ARM64, for some calls, the JVM pushes "breadcrumbs" onto the stack to make unwinding // easier for them. In the process, they unfortunately make it harder for us, since we have @@ -344,7 +341,7 @@ __attribute__((always_inline)) inline static void breadcrumb_fixup(HotspotUnwind #endif #if defined(__x86_64__) -__attribute__((always_inline)) inline static ErrorCode +static EBPF_INLINE ErrorCode hotspot_handle_prologue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, HotspotUnwindAction *action) { // In the prologue code. It generally consists of stack 'banging' (check for stack @@ -364,7 +361,7 @@ hotspot_handle_prologue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, HotspotU return ERR_OK; } #elif defined(__aarch64__) -__attribute__((always_inline)) inline static ErrorCode +static EBPF_INLINE ErrorCode hotspot_handle_prologue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, HotspotUnwindAction *action) { // On ARM64, the prologue consists of various assembly snippets, most of which we aren't really @@ -414,7 +411,7 @@ hotspot_handle_prologue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, HotspotU #endif #if defined(__x86_64__) -__attribute__((always_inline)) inline static bool +static EBPF_INLINE bool hotspot_handle_epilogue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, HotspotUnwindAction *action) { // On X86, use a heuristic to catch the likely spots of the epilogue. @@ -477,7 +474,7 @@ hotspot_handle_epilogue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, HotspotU return true; } #elif defined(__aarch64__) -__attribute__((always_inline)) inline static bool +static EBPF_INLINE bool hotspot_handle_epilogue(const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, HotspotUnwindAction *action) { // On ARM64, the epilogue code is generated roughly like this: @@ -575,7 +572,7 @@ pattern_found:; } #endif -__attribute__((always_inline)) inline static ErrorCode hotspot_handle_nmethod( +static EBPF_INLINE ErrorCode hotspot_handle_nmethod( const CodeBlobInfo *cbi, Trace *trace, HotspotUnwindInfo *ui, @@ -683,7 +680,7 @@ __attribute__((always_inline)) inline static ErrorCode hotspot_handle_nmethod( #endif } -__attribute__((always_inline)) inline static ErrorCode +static EBPF_INLINE ErrorCode hotspot_handle_stub_fallback(const CodeBlobInfo *cbi, HotspotUnwindAction *action) { DEBUG_PRINT("jvm: -> unwind stub fallback path"); @@ -699,7 +696,7 @@ hotspot_handle_stub_fallback(const CodeBlobInfo *cbi, HotspotUnwindAction *actio return ERR_OK; } -__attribute__((always_inline)) inline static ErrorCode hotspot_handle_stub( +static EBPF_INLINE ErrorCode hotspot_handle_stub( const UnwindState *state, const CodeBlobInfo *cbi, HotspotUnwindInfo *ui, @@ -733,7 +730,7 @@ __attribute__((always_inline)) inline static ErrorCode hotspot_handle_stub( #endif } -__attribute__((always_inline)) inline static ErrorCode hotspot_execute_unwind_action( +static EBPF_INLINE ErrorCode hotspot_execute_unwind_action( CodeBlobInfo *cbi, HotspotUnwindAction action, HotspotUnwindInfo *ui, @@ -796,7 +793,7 @@ __attribute__((always_inline)) inline static ErrorCode hotspot_execute_unwind_ac } // Reads information from the CodeBlob for the current PC location from the JVM process. -__attribute__((always_inline)) inline static ErrorCode hotspot_read_codeblob( +static EBPF_INLINE ErrorCode hotspot_read_codeblob( const UnwindState *state, const HotspotProcInfo *ji, HotspotUnwindScratchSpace *scratch, @@ -873,7 +870,7 @@ __attribute__((always_inline)) inline static ErrorCode hotspot_read_codeblob( } // hotspot_unwind_one_frame fully unwinds one HotSpot frame -static ErrorCode +static EBPF_INLINE ErrorCode hotspot_unwind_one_frame(PerCPURecord *record, HotspotProcInfo *ji, bool maybe_topmost) { UnwindState *state = &record->state; @@ -925,7 +922,7 @@ hotspot_unwind_one_frame(PerCPURecord *record, HotspotProcInfo *ji, bool maybe_t // unwind_hotspot is the entry point for tracing when invoked from the native tracer // and it recursive unwinds all HotSpot frames and then jumps back to unwind further // native frames that follow. -static inline __attribute__((__always_inline__)) int unwind_hotspot(struct pt_regs *ctx) +static EBPF_INLINE int unwind_hotspot(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); if (!record) diff --git a/support/ebpf/integration_test.ebpf.c b/support/ebpf/integration_test.ebpf.c index 327850c9e..f79db7960 100644 --- a/support/ebpf/integration_test.ebpf.c +++ b/support/ebpf/integration_test.ebpf.c @@ -8,8 +8,7 @@ extern bpf_map_def kernel_stackmap; -static inline __attribute__((__always_inline__)) void -send_sample_traces(void *ctx, u64 pid, s32 kstack) +static EBPF_INLINE void send_sample_traces(void *ctx, u64 pid, s32 kstack) { // Use the per CPU record for trace storage: it's too big for stack. PerCPURecord *record = get_pristine_per_cpu_record(); diff --git a/support/ebpf/interpreter_dispatcher.ebpf.c b/support/ebpf/interpreter_dispatcher.ebpf.c index 934855681..f3929ef2f 100644 --- a/support/ebpf/interpreter_dispatcher.ebpf.c +++ b/support/ebpf/interpreter_dispatcher.ebpf.c @@ -3,6 +3,7 @@ // perf event and will call the appropriate tracer for a given process #include "bpfdefs.h" +#include "kernel.h" #include "tracemgmt.h" #include "tsd.h" #include "types.h" @@ -66,14 +67,14 @@ bpf_map_def SEC("maps") reported_pids = { // // User space code will periodically iterate through the map and process each entry. // Additionally, each time eBPF code writes a value into the map, user space is notified -// through event_send_trigger (which uses maps/report_events). As key we use the PID of -// the process and as value always true. When sizing this map, we are thinking about -// the maximum number of unique PIDs that could generate events we're interested in -// (process new, process exit, unknown PC) within a map monitor/processing interval, +// through event_send_trigger (which uses maps/report_events). As key we use the PID/TID +// of the process/thread and as value always true. When sizing this map, we are thinking +// about the maximum number of unique PIDs that could generate events we're interested in +// (process new, thread group exit, unknown PC) within a map monitor/processing interval, // that we would like to support. bpf_map_def SEC("maps") pid_events = { .type = BPF_MAP_TYPE_HASH, - .key_size = sizeof(u32), + .key_size = sizeof(u64), .value_size = sizeof(bool), .max_entries = 65536, }; @@ -124,7 +125,74 @@ bpf_map_def SEC("maps") apm_int_procs = { .max_entries = 128, }; -static inline __attribute__((__always_inline__)) void maybe_add_apm_info(Trace *trace) +bpf_map_def SEC("maps") go_labels_procs = { + .type = BPF_MAP_TYPE_HASH, + .key_size = sizeof(pid_t), + .value_size = sizeof(GoLabelsOffsets), + .max_entries = 128, +}; + +static EBPF_INLINE void *get_m_ptr(struct GoLabelsOffsets *offs, UnwindState *state) +{ + u64 g_addr = 0; + void *tls_base = NULL; + if (tsd_get_base(&tls_base) < 0) { + DEBUG_PRINT("cl: failed to get tsd base; can't read m_ptr"); + return NULL; + } + DEBUG_PRINT( + "cl: read tsd_base at 0x%lx, g offset: %d", (unsigned long)tls_base, offs->tls_offset); + + if (offs->tls_offset == 0) { +#if defined(__aarch64__) + // On aarch64 for !iscgo programs the g is only stored in r28 register. + g_addr = state->r28; +#elif defined(__x86_64__) + DEBUG_PRINT("cl: TLS offset for g pointer missing for amd64"); + return NULL; +#endif + } + + if (g_addr == 0) { + if (bpf_probe_read_user(&g_addr, sizeof(void *), (void *)((s64)tls_base + offs->tls_offset))) { + DEBUG_PRINT("cl: failed to read g_addr, tls_base(%lx)", (unsigned long)tls_base); + return NULL; + } + } + + DEBUG_PRINT("cl: reading m_ptr_addr at 0x%lx + 0x%x", (unsigned long)g_addr, offs->m_offset); + void *m_ptr_addr; + if (bpf_probe_read_user(&m_ptr_addr, sizeof(void *), (void *)(g_addr + offs->m_offset))) { + DEBUG_PRINT("cl: failed m_ptr_addr"); + return NULL; + } + DEBUG_PRINT("cl: m_ptr_addr 0x%lx", (unsigned long)m_ptr_addr); + return m_ptr_addr; +} + +static EBPF_INLINE void maybe_add_go_custom_labels(struct pt_regs *ctx, PerCPURecord *record) +{ + u32 pid = record->trace.pid; + GoLabelsOffsets *offsets = bpf_map_lookup_elem(&go_labels_procs, &pid); + if (!offsets) { + DEBUG_PRINT("cl: no offsets, %d not recognized as a go binary", pid); + return; + } + + void *m_ptr_addr = get_m_ptr(offsets, &record->state); + if (!m_ptr_addr) { + return; + } + record->customLabelsState.go_m_ptr = m_ptr_addr; + + DEBUG_PRINT("cl: trace is within a process with Go custom labels enabled"); + increment_metric(metricID_UnwindGoLabelsAttempts); + // The Go label extraction code is too big to fit in the UNWIND_STOP program, so + // it is tail_call'd. + tail_call(ctx, PROG_GO_LABELS); +} + +static EBPF_INLINE void maybe_add_apm_info(Trace *trace) { u32 pid = trace->pid; // verifier needs this to be on stack on 4.15 kernel ApmIntProcInfo *proc = bpf_map_lookup_elem(&apm_int_procs, &pid); @@ -174,7 +242,7 @@ static inline __attribute__((__always_inline__)) void maybe_add_apm_info(Trace * } // unwind_stop is the tail call destination for PROG_UNWIND_STOP. -static inline __attribute__((__always_inline__)) int unwind_stop(struct pt_regs *ctx) +static EBPF_INLINE int unwind_stop(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); if (!record) @@ -205,7 +273,8 @@ static inline __attribute__((__always_inline__)) int unwind_stop(struct pt_regs // No Error break; case metricID_UnwindNativeErrWrongTextSection:; - if (report_pid(ctx, trace->pid, record->ratelimitAction)) { + u64 pid_tgid = (u64)trace->pid << 32 | trace->tid; + if (report_pid(ctx, pid_tgid, record->ratelimitAction)) { increment_metric(metricID_NumUnknownPC); } // Fallthrough to report the error @@ -235,6 +304,9 @@ static inline __attribute__((__always_inline__)) int unwind_stop(struct pt_regs } // TEMPORARY HACK END + // Must be last since it may not return (it will call send_trace). + maybe_add_go_custom_labels(ctx, record); + send_trace(ctx, trace); return 0; diff --git a/support/ebpf/kernel.h b/support/ebpf/kernel.h index 874de0904..cf239ed3b 100644 --- a/support/ebpf/kernel.h +++ b/support/ebpf/kernel.h @@ -47,11 +47,13 @@ _Static_assert(sizeof(uintptr_t) == 8, "bad uintptr_t size"); _Static_assert(sizeof(size_t) == 8, "bad size_t size"); // Define bool type (emulates stdbool.h). +#if __STDC_VERSION__ < 202311L typedef _Bool bool; -#ifndef __bool_true_false_are_defined - #define true 1 - #define false 0 - #define __bool_true_false_are_defined 1 + #ifndef __bool_true_false_are_defined + #define true 1 + #define false 0 + #define __bool_true_false_are_defined 1 + #endif #endif // Go defines `NULL` in `cgo-builtin-prolog`, so we have to check whether diff --git a/support/ebpf/native_stack_trace.ebpf.c b/support/ebpf/native_stack_trace.ebpf.c index 62c6d5c63..651f0623e 100644 --- a/support/ebpf/native_stack_trace.ebpf.c +++ b/support/ebpf/native_stack_trace.ebpf.c @@ -82,8 +82,7 @@ bpf_map_def SEC("maps") kernel_stackmap = { }; // Record a native frame -static inline __attribute__((__always_inline__)) ErrorCode -push_native(Trace *trace, u64 file, u64 line, bool return_address) +static EBPF_INLINE ErrorCode push_native(Trace *trace, u64 file, u64 line, bool return_address) { return _push_with_return_address(trace, file, line, FRAME_MARKER_NATIVE, return_address); } @@ -92,8 +91,7 @@ push_native(Trace *trace, u64 file, u64 line, bool return_address) // step, built in a way to update the value of *lo and *hi. This function will be called repeatedly // (since we cannot do loops). The return value signals whether the bsearch came to an end / found // the right element or whether it needs to continue. -static inline __attribute__((__always_inline__)) bool -bsearch_step(void *inner_map, u32 *lo, u32 *hi, u16 page_offset) +static EBPF_INLINE bool bsearch_step(void *inner_map, u32 *lo, u32 *hi, u16 page_offset) { u32 pivot = (*lo + *hi) >> 1; StackDelta *delta = bpf_map_lookup_elem(inner_map, &pivot); @@ -110,7 +108,7 @@ bsearch_step(void *inner_map, u32 *lo, u32 *hi, u16 page_offset) } // Get the outer map based on the number of stack delta entries. -static inline __attribute__((__always_inline__)) void *get_stack_delta_map(int mapID) +static EBPF_INLINE void *get_stack_delta_map(int mapID) { switch (mapID) { case 8: return &exe_id_to_8_stack_deltas; @@ -134,7 +132,7 @@ static inline __attribute__((__always_inline__)) void *get_stack_delta_map(int m } // Get the stack offset of the given instruction. -static ErrorCode get_stack_delta(UnwindState *state, int *addrDiff, u32 *unwindInfo) +static EBPF_INLINE ErrorCode get_stack_delta(UnwindState *state, int *addrDiff, u32 *unwindInfo) { u64 exe_id = state->text_section_id; @@ -254,8 +252,7 @@ static ErrorCode get_stack_delta(UnwindState *state, int *addrDiff, u32 *unwindI // BASE + param // 3. When UNWIND_OPCODEF_DEREF is set: // *(BASE + preDeref) + postDeref -static inline __attribute__((__always_inline__)) u64 -unwind_register_address(UnwindState *state, u64 cfa, u8 opcode, s32 param) +static EBPF_INLINE u64 unwind_register_address(UnwindState *state, u64 cfa, u8 opcode, s32 param) { unsigned long addr, val; s32 preDeref = param, postDeref = 0; @@ -357,7 +354,8 @@ unwind_register_address(UnwindState *state, u64 cfa, u8 opcode, s32 param) // is marked with UNWIND_COMMAND_STOP which marks entry points (main function, // thread spawn function, signal handlers, ...). #if defined(__x86_64__) -static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, UnwindState *state, bool *stop) +static EBPF_INLINE ErrorCode +unwind_one_frame(u64 pid, u32 frame_idx, UnwindState *state, bool *stop) { *stop = false; @@ -403,6 +401,11 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, UnwindState *state, bo DEBUG_PRINT("signal frame"); goto frame_ok; case UNWIND_COMMAND_STOP: *stop = true; return ERR_OK; + case UNWIND_COMMAND_FRAME_POINTER: + if (!unwinder_unwind_frame_pointer(state)) { + goto err_native_pc_read; + } + goto frame_ok; default: return ERR_UNREACHABLE; } } else { @@ -446,7 +449,8 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, UnwindState *state, bo return ERR_OK; } #elif defined(__aarch64__) -static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *state, bool *stop) +static EBPF_INLINE ErrorCode +unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *state, bool *stop) { *stop = false; @@ -480,11 +484,17 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *st state->fp = rt_regs[29]; state->lr = normalize_pac_ptr(rt_regs[30]); state->r22 = rt_regs[22]; + state->r28 = rt_regs[28]; state->return_address = false; state->lr_invalid = false; DEBUG_PRINT("signal frame"); goto frame_ok; case UNWIND_COMMAND_STOP: *stop = true; return ERR_OK; + case UNWIND_COMMAND_FRAME_POINTER: + if (!unwinder_unwind_frame_pointer(state)) { + goto err_native_pc_read; + } + goto frame_ok; default: return ERR_UNREACHABLE; } } @@ -570,7 +580,7 @@ static ErrorCode unwind_one_frame(u64 pid, u32 frame_idx, struct UnwindState *st #endif // unwind_native is the tail call destination for PROG_UNWIND_NATIVE. -static inline __attribute__((__always_inline__)) int unwind_native(struct pt_regs *ctx) +static EBPF_INLINE int unwind_native(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); if (!record) diff --git a/support/ebpf/perl_tracer.ebpf.c b/support/ebpf/perl_tracer.ebpf.c index 0f47c86d3..3c91f9e15 100644 --- a/support/ebpf/perl_tracer.ebpf.c +++ b/support/ebpf/perl_tracer.ebpf.c @@ -75,8 +75,7 @@ bpf_map_def SEC("maps") perl_procs = { }; // Record a Perl frame -static inline __attribute__((__always_inline__)) ErrorCode -push_perl(Trace *trace, u64 file, u64 line) +static EBPF_INLINE ErrorCode push_perl(Trace *trace, u64 file, u64 line) { DEBUG_PRINT("Pushing perl frame cop=0x%lx, cv=0x%lx", (unsigned long)file, (unsigned long)line); return _push(trace, file, line, FRAME_MARKER_PERL); @@ -86,8 +85,7 @@ push_perl(Trace *trace, u64 file, u64 line) // EGV to be reported for HA. This basically maps the internal code value, to its // canonical symbol name. This mapping is done in EBPF because it seems the CV* // can get undefined once it goes out of scope, but the EGV should be more permanent. -static inline __attribute__((__always_inline__)) void * -resolve_cv_egv(const PerlProcInfo *perlinfo, const void *cv) +static EBPF_INLINE void *resolve_cv_egv(const PerlProcInfo *perlinfo, const void *cv) { // First check the CV's type u32 cv_flags; @@ -165,7 +163,7 @@ resolve_cv_egv(const PerlProcInfo *perlinfo, const void *cv) return 0; } -static inline __attribute__((__always_inline__)) int +static EBPF_INLINE int process_perl_frame(PerCPURecord *record, const PerlProcInfo *perlinfo, const void *cx) { Trace *trace = &record->trace; @@ -259,8 +257,7 @@ process_perl_frame(PerCPURecord *record, const PerlProcInfo *perlinfo, const voi return PROG_UNWIND_PERL; } -static inline __attribute__((__always_inline__)) void -prepare_perl_stack(PerCPURecord *record, const PerlProcInfo *perlinfo) +static EBPF_INLINE void prepare_perl_stack(PerCPURecord *record, const PerlProcInfo *perlinfo) { const void *si = record->perlUnwindState.stackinfo; // cxstack contains the base of the current context stack which is an array of PERL_CONTEXT @@ -282,8 +279,7 @@ prepare_perl_stack(PerCPURecord *record, const PerlProcInfo *perlinfo) record->perlUnwindState.cxcur = cxstack + cxix * perlinfo->context_sizeof; } -static inline __attribute__((__always_inline__)) int -walk_perl_stack(PerCPURecord *record, const PerlProcInfo *perlinfo) +static EBPF_INLINE int walk_perl_stack(PerCPURecord *record, const PerlProcInfo *perlinfo) { const void *si = record->perlUnwindState.stackinfo; @@ -363,7 +359,7 @@ walk_perl_stack(PerCPURecord *record, const PerlProcInfo *perlinfo) // unwind_perl is the entry point for tracing when invoked from the native tracer // or interpreter dispatcher. It does not reset the trace object and will append the // Perl stack frames to the trace object for the current CPU. -static inline __attribute__((__always_inline__)) int unwind_perl(struct pt_regs *ctx) +static EBPF_INLINE int unwind_perl(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); if (!record) { diff --git a/support/ebpf/php_tracer.ebpf.c b/support/ebpf/php_tracer.ebpf.c index ff19aacf0..241c3b07f 100644 --- a/support/ebpf/php_tracer.ebpf.c +++ b/support/ebpf/php_tracer.ebpf.c @@ -26,20 +26,19 @@ bpf_map_def SEC("maps") php_procs = { }; // Record a PHP frame -static inline __attribute__((__always_inline__)) ErrorCode -push_php(Trace *trace, u64 file, u64 line, bool is_jitted) +static EBPF_INLINE ErrorCode push_php(Trace *trace, u64 file, u64 line, bool is_jitted) { int frame_type = is_jitted ? FRAME_MARKER_PHP_JIT : FRAME_MARKER_PHP; return _push(trace, file, line, frame_type); } // Record a PHP call for which no function object is available -static inline __attribute__((__always_inline__)) ErrorCode push_unknown_php(Trace *trace) +static EBPF_INLINE ErrorCode push_unknown_php(Trace *trace) { return _push(trace, UNKNOWN_FILE, FUNC_TYPE_UNKNOWN, FRAME_MARKER_PHP); } -static inline __attribute__((__always_inline__)) int process_php_frame( +static EBPF_INLINE int process_php_frame( PerCPURecord *record, PHPProcInfo *phpinfo, bool is_jitted, @@ -118,8 +117,7 @@ static inline __attribute__((__always_inline__)) int process_php_frame( return metricID_UnwindPHPFrames; } -static inline __attribute__((__always_inline__)) int -walk_php_stack(PerCPURecord *record, PHPProcInfo *phpinfo, bool is_jitted) +static EBPF_INLINE int walk_php_stack(PerCPURecord *record, PHPProcInfo *phpinfo, bool is_jitted) { const void *execute_data = record->phpUnwindState.zend_execute_data; bool mixed_traces = get_next_unwinder_after_interpreter(record) != PROG_UNWIND_STOP; @@ -195,7 +193,7 @@ walk_php_stack(PerCPURecord *record, PHPProcInfo *phpinfo, bool is_jitted) } // unwind_php is the tail call destination for PROG_UNWIND_PHP. -static inline __attribute__((__always_inline__)) int unwind_php(struct pt_regs *ctx) +static EBPF_INLINE int unwind_php(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); if (!record) diff --git a/support/ebpf/python_tracer.ebpf.c b/support/ebpf/python_tracer.ebpf.c index 2a3fbc082..3ead41502 100644 --- a/support/ebpf/python_tracer.ebpf.c +++ b/support/ebpf/python_tracer.ebpf.c @@ -25,18 +25,17 @@ bpf_map_def SEC("maps") py_procs = { }; // Record a Python frame -static inline __attribute__((__always_inline__)) ErrorCode -push_python(Trace *trace, u64 file, u64 line) +static EBPF_INLINE ErrorCode push_python(Trace *trace, u64 file, u64 line) { return _push(trace, file, line, FRAME_MARKER_PYTHON); } -static inline __attribute__((__always_inline__)) u64 py_encode_lineno(u32 object_id, u32 f_lasti) +static EBPF_INLINE u64 py_encode_lineno(u32 object_id, u32 f_lasti) { return (object_id | (((u64)f_lasti) << 32)); } -static inline __attribute__((__always_inline__)) ErrorCode process_python_frame( +static EBPF_INLINE ErrorCode process_python_frame( PerCPURecord *record, const PyProcInfo *pyinfo, void **py_frameobjectptr, @@ -156,7 +155,7 @@ static inline __attribute__((__always_inline__)) ErrorCode process_python_frame( return ERR_OK; } -static inline __attribute__((__always_inline__)) ErrorCode +static EBPF_INLINE ErrorCode walk_python_stack(PerCPURecord *record, const PyProcInfo *pyinfo, int *unwinder) { void *py_frame = record->pythonUnwindState.py_frame; @@ -194,7 +193,7 @@ walk_python_stack(PerCPURecord *record, const PyProcInfo *pyinfo, int *unwinder) // // Python sets the thread_state using pthread_setspecific with the key // stored in a global variable autoTLSkey. -static inline __attribute__((__always_inline__)) ErrorCode get_PyThreadState( +static EBPF_INLINE ErrorCode get_PyThreadState( const PyProcInfo *pyinfo, void *tsd_base, void *autoTLSkeyAddr, void **thread_state) { int key; @@ -212,8 +211,7 @@ static inline __attribute__((__always_inline__)) ErrorCode get_PyThreadState( return ERR_OK; } -static inline __attribute__((__always_inline__)) ErrorCode -get_PyFrame(const PyProcInfo *pyinfo, void **frame) +static EBPF_INLINE ErrorCode get_PyFrame(const PyProcInfo *pyinfo, void **frame) { void *tsd_base; if (tsd_get_base(&tsd_base)) { @@ -281,7 +279,7 @@ get_PyFrame(const PyProcInfo *pyinfo, void **frame) // unwind_python is the entry point for tracing when invoked from the native tracer // or interpreter dispatcher. It does not reset the trace object and will append the // Python stack frames to the trace object for the current CPU. -static inline __attribute__((__always_inline__)) int unwind_python(struct pt_regs *ctx) +static EBPF_INLINE int unwind_python(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); if (!record) diff --git a/support/ebpf/ruby_tracer.ebpf.c b/support/ebpf/ruby_tracer.ebpf.c index a00ab59b1..c37a5ba45 100644 --- a/support/ebpf/ruby_tracer.ebpf.c +++ b/support/ebpf/ruby_tracer.ebpf.c @@ -25,8 +25,7 @@ bpf_map_def SEC("maps") ruby_procs = { #define RUBY_FRAME_FLAG_LAMBDA 0x0100 // Record a Ruby frame -static inline __attribute__((__always_inline__)) ErrorCode -push_ruby(Trace *trace, u64 file, u64 line) +static EBPF_INLINE ErrorCode push_ruby(Trace *trace, u64 file, u64 line) { return _push(trace, file, line, FRAME_MARKER_RUBY); } @@ -50,7 +49,7 @@ push_ruby(Trace *trace, u64 file, u64 line) // // [2] rb_iseq_struct // https://github.com/ruby/ruby/blob/5445e0435260b449decf2ac16f9d09bae3cafe72/vm_core.h#L456 -static inline __attribute__((__always_inline__)) ErrorCode walk_ruby_stack( +static EBPF_INLINE ErrorCode walk_ruby_stack( PerCPURecord *record, const RubyProcInfo *rubyinfo, const void *current_ctx_addr, @@ -227,7 +226,7 @@ static inline __attribute__((__always_inline__)) ErrorCode walk_ruby_stack( } // unwind_ruby is the tail call destination for PROG_UNWIND_RUBY. -static inline __attribute__((__always_inline__)) int unwind_ruby(struct pt_regs *ctx) +static EBPF_INLINE int unwind_ruby(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); if (!record) diff --git a/support/ebpf/sched_monitor.ebpf.c b/support/ebpf/sched_monitor.ebpf.c index a8d6b98c1..335daf657 100644 --- a/support/ebpf/sched_monitor.ebpf.c +++ b/support/ebpf/sched_monitor.ebpf.c @@ -6,21 +6,20 @@ #include "types.h" -// tracepoint__sched_process_exit is a tracepoint attached to the scheduler that stops processes. -// Every time a processes stops this hook is triggered. -SEC("tracepoint/sched/sched_process_exit") -int tracepoint__sched_process_exit(void *ctx) -{ - u64 pid_tgid = bpf_get_current_pid_tgid(); - u32 pid = (u32)(pid_tgid >> 32); - u32 tid = (u32)(pid_tgid & 0xFFFFFFFF); +// See /sys/kernel/debug/tracing/events/sched/sched_process_free/format +// for struct layout. +struct sched_process_free_ctx { + unsigned char skip[24]; + pid_t pid; + int prio; +}; - if (pid != tid) { - // Only if the thread group ID matched with the PID the process itself exits. If they don't - // match only a thread of the process stopped and we do not need to report this PID to - // userspace for further processing. - goto exit; - } +// tracepoint__sched_process_free is a tracepoint attached to the scheduler that frees processes. +// Every time a processes exits this hook is triggered. +SEC("tracepoint/sched/sched_process_free") +int tracepoint__sched_process_free(struct sched_process_free_ctx *ctx) +{ + u32 pid = ctx->pid; if (!bpf_map_lookup_elem(&reported_pids, &pid) && !pid_information_exists(ctx, pid)) { // Only report PIDs that we explicitly track. This avoids sending kernel worker PIDs @@ -28,7 +27,7 @@ int tracepoint__sched_process_exit(void *ctx) goto exit; } - if (report_pid(ctx, pid, RATELIMIT_ACTION_RESET)) { + if (report_pid(ctx, (u64)pid << 32 | pid, RATELIMIT_ACTION_RESET)) { increment_metric(metricID_NumProcExit); } exit: diff --git a/support/ebpf/stackdeltatypes.h b/support/ebpf/stackdeltatypes.h index c0509d86e..3c9a6a422 100644 --- a/support/ebpf/stackdeltatypes.h +++ b/support/ebpf/stackdeltatypes.h @@ -17,13 +17,15 @@ #define UNWIND_OPCODEF_DEREF 0x80 // Unsupported or no value for the register -#define UNWIND_COMMAND_INVALID 0 +#define UNWIND_COMMAND_INVALID 0 // For CFA: stop unwinding, this function is a stack root function -#define UNWIND_COMMAND_STOP 1 +#define UNWIND_COMMAND_STOP 1 // Unwind a PLT entry -#define UNWIND_COMMAND_PLT 2 +#define UNWIND_COMMAND_PLT 2 // Unwind a signal frame -#define UNWIND_COMMAND_SIGNAL 3 +#define UNWIND_COMMAND_SIGNAL 3 +// Unwind using standard frame pointer +#define UNWIND_COMMAND_FRAME_POINTER 4 // If opcode has UNWIND_OPCODEF_DEREF set, the lowest bits of 'param' are used // as second adder as post-deref operation. This contains the mask for that. diff --git a/support/ebpf/tracemgmt.h b/support/ebpf/tracemgmt.h index cc2816ac9..cdfdf63de 100644 --- a/support/ebpf/tracemgmt.h +++ b/support/ebpf/tracemgmt.h @@ -13,19 +13,19 @@ // for a given function. #define MULTI_USE_FUNC(func_name) \ SEC("perf_event/" #func_name) \ - int perf_##func_name(struct pt_regs *ctx) \ + static int EBPF_INLINE perf_##func_name(struct pt_regs *ctx) \ { \ return func_name(ctx); \ } \ \ SEC("kprobe/" #func_name) \ - int kprobe_##func_name(struct pt_regs *ctx) \ + static int EBPF_INLINE kprobe_##func_name(struct pt_regs *ctx) \ { \ return func_name(ctx); \ } // increment_metric increments the value of the given metricID by 1 -static inline __attribute__((__always_inline__)) void increment_metric(u32 metricID) +static inline EBPF_INLINE void increment_metric(u32 metricID) { u64 *count = bpf_map_lookup_elem(&metrics, &metricID); if (count) { @@ -38,7 +38,7 @@ static inline __attribute__((__always_inline__)) void increment_metric(u32 metri // Send immediate notifications for event triggers to Go. // Notifications for GENERIC_PID and TRACES_FOR_SYMBOLIZATION will be // automatically inhibited until HA resets the type. -static inline void event_send_trigger(struct pt_regs *ctx, u32 event_type) +static inline EBPF_INLINE void event_send_trigger(struct pt_regs *ctx, u32 event_type) { int inhibit_key = event_type; bool inhibit_value = true; @@ -74,7 +74,7 @@ static inline void event_send_trigger(struct pt_regs *ctx, u32 event_type) struct bpf_perf_event_data; // pid_information_exists checks if the given pid exists in pid_page_to_mapping_info or not. -static inline __attribute__((__always_inline__)) bool pid_information_exists(void *ctx, int pid) +static inline EBPF_INLINE bool pid_information_exists(void *ctx, int pid) { PIDPage key = {}; key.prefixLen = BIT_WIDTH_PID + BIT_WIDTH_PAGE; @@ -93,8 +93,7 @@ static inline __attribute__((__always_inline__)) bool pid_information_exists(voi // pid_event_ratelimit determines if the PID event should be inhibited or not // based on rate limiting rules. -static inline __attribute__((__always_inline__)) bool -pid_event_ratelimit(u32 pid, int ratelimit_action) +static inline EBPF_INLINE bool pid_event_ratelimit(u32 pid, int ratelimit_action) { const u8 default_max_attempts = 8; // 25 seconds const u8 fast_max_attempts = 4; // 1.6 seconds @@ -158,24 +157,24 @@ pid_event_ratelimit(u32 pid, int ratelimit_action) // If inhibit is true, PID will first be checked against maps/reported_pids // and reporting aborted if PID has been recently reported. // Returns true if the PID was successfully reported to user space. -static inline __attribute__((__always_inline__)) bool -report_pid(void *ctx, int pid, int ratelimit_action) +static inline EBPF_INLINE bool report_pid(void *ctx, u64 pid_tgid, int ratelimit_action) { - u32 key = (u32)pid; + u32 pid = pid_tgid >> 32; if (pid_event_ratelimit(pid, ratelimit_action)) { return false; } bool value = true; - int errNo = bpf_map_update_elem(&pid_events, &key, &value, BPF_ANY); + int errNo = bpf_map_update_elem(&pid_events, &pid_tgid, &value, BPF_ANY); if (errNo != 0) { - DEBUG_PRINT("Failed to update pid_events with PID %d: %d", pid, errNo); + __attribute__((unused)) u32 tid = pid_tgid & 0xFFFFFFFF; + DEBUG_PRINT("Failed to update pid_events with PID %d TID: %d: %d", pid, tid, errNo); increment_metric(metricID_PIDEventsErr); return false; } - if (ratelimit_action == RATELIMIT_ACTION_RESET || errNo != 0) { - bpf_map_delete_elem(&reported_pids, &key); + if (ratelimit_action == RATELIMIT_ACTION_RESET) { + bpf_map_delete_elem(&reported_pids, &pid); } // Notify userspace that there is a PID waiting to be processed. @@ -191,7 +190,7 @@ report_pid(void *ctx, int pid, int ratelimit_action) // The return value of get_per_cpu_record() can never be NULL and return value checks only exist // to pass the verifier. If the implementation of get_per_cpu_record() is changed so that NULL can // be returned, also add an error metric. -static inline PerCPURecord *get_per_cpu_record(void) +static inline EBPF_INLINE PerCPURecord *get_per_cpu_record(void) { int key0 = 0; return bpf_map_lookup_elem(&per_cpu_records, &key0); @@ -201,7 +200,7 @@ static inline PerCPURecord *get_per_cpu_record(void) // The return value of get_pristine_per_cpu_record() can never be NULL and return value checks // only exist to pass the verifier. If the implementation of get_pristine_per_cpu_record() is // changed so that NULL can be returned, also add an error metric. -static inline PerCPURecord *get_pristine_per_cpu_record() +static inline EBPF_INLINE PerCPURecord *get_pristine_per_cpu_record() { PerCPURecord *record = get_per_cpu_record(); if (!record) @@ -215,6 +214,7 @@ static inline PerCPURecord *get_pristine_per_cpu_record() #elif defined(__aarch64__) record->state.lr = 0; record->state.r22 = 0; + record->state.r28 = 0; record->state.lr_invalid = false; #endif record->state.return_address = false; @@ -229,6 +229,7 @@ static inline PerCPURecord *get_pristine_per_cpu_record() record->unwindersDone = 0; record->tailCalls = 0; record->ratelimitAction = RATELIMIT_ACTION_DEFAULT; + record->customLabelsState.go_m_ptr = NULL; Trace *trace = &record->trace; trace->kernel_stack_id = -1; @@ -240,13 +241,21 @@ static inline PerCPURecord *get_pristine_per_cpu_record() trace->apm_trace_id.as_int.lo = 0; trace->apm_transaction_id.as_int = 0; + trace->custom_labels.len = 0; + u64 *labels_space = (u64 *)&trace->custom_labels.labels; + // I'm not sure this is necessary since we only increment len after + // we successfully write the label. +#pragma unroll + for (int i = 0; i < sizeof(CustomLabel) * MAX_CUSTOM_LABELS / 8; i++) { + labels_space[i] = 0; + } + return record; } // unwinder_is_done checks if a given unwinder program is done for the trace // extraction round. -static inline __attribute__((__always_inline__)) bool -unwinder_is_done(const PerCPURecord *record, int unwinder) +static inline EBPF_INLINE bool unwinder_is_done(const PerCPURecord *record, int unwinder) { return (record->unwindersDone & (1U << unwinder)) != 0; } @@ -254,8 +263,7 @@ unwinder_is_done(const PerCPURecord *record, int unwinder) // unwinder_mark_done will mask out a given unwinder program so that it will // not be called again for the same trace. Used when interpreter unwinder has // extracted all interpreter frames it can extract. -static inline __attribute__((__always_inline__)) void -unwinder_mark_done(PerCPURecord *record, int unwinder) +static inline EBPF_INLINE void unwinder_mark_done(PerCPURecord *record, int unwinder) { record->unwindersDone |= 1U << unwinder; } @@ -271,8 +279,7 @@ unwinder_mark_done(PerCPURecord *record, int unwinder) // // Additionally, on aarch64, this means that we will not trust the current value of // `lr` to be the return address for this frame. -static inline __attribute__((__always_inline__)) void -unwinder_mark_nonleaf_frame(UnwindState *state) +static inline EBPF_INLINE void unwinder_mark_nonleaf_frame(UnwindState *state) { state->return_address = true; #if defined(__aarch64__) @@ -280,6 +287,21 @@ unwinder_mark_nonleaf_frame(UnwindState *state) #endif } +// unwinder_unwind_frame_pointer unwinds using the Frame Pointer. +static inline EBPF_INLINE bool unwinder_unwind_frame_pointer(UnwindState *state) +{ + unsigned long regs[2]; + + if (bpf_probe_read_user(regs, sizeof(regs), (void *)state->fp)) { + return false; + } + state->sp = state->fp + sizeof(regs); + state->fp = regs[0]; + state->pc = regs[1]; + unwinder_mark_nonleaf_frame(state); + return true; +} + // Push the file ID, line number and frame type into FrameList with a user-defined // maximum stack size. // @@ -288,7 +310,7 @@ unwinder_mark_nonleaf_frame(UnwindState *state) // and hotspot puts a subtype and BCI indices, amongst other things (see // calc_line). This should probably be renamed to something like "frame type // specific data". -static inline __attribute__((__always_inline__)) ErrorCode _push_with_max_frames( +static inline EBPF_INLINE ErrorCode _push_with_max_frames( Trace *trace, u64 file, u64 line, u8 frame_type, u8 return_address, u32 max_frames) { if (trace->stack_len >= max_frames) { @@ -316,7 +338,7 @@ static inline __attribute__((__always_inline__)) ErrorCode _push_with_max_frames } // Push the file ID, line number and frame type into FrameList -static inline __attribute__((__always_inline__)) ErrorCode +static inline EBPF_INLINE ErrorCode _push_with_return_address(Trace *trace, u64 file, u64 line, u8 frame_type, bool return_address) { return _push_with_max_frames( @@ -324,20 +346,19 @@ _push_with_return_address(Trace *trace, u64 file, u64 line, u8 frame_type, bool } // Push the file ID, line number and frame type into FrameList -static inline __attribute__((__always_inline__)) ErrorCode -_push(Trace *trace, u64 file, u64 line, u8 frame_type) +static inline EBPF_INLINE ErrorCode _push(Trace *trace, u64 file, u64 line, u8 frame_type) { return _push_with_max_frames(trace, file, line, frame_type, 0, MAX_NON_ERROR_FRAME_UNWINDS); } // Push a critical error frame. -static inline __attribute__((__always_inline__)) ErrorCode push_error(Trace *trace, ErrorCode error) +static inline EBPF_INLINE ErrorCode push_error(Trace *trace, ErrorCode error) { return _push_with_max_frames(trace, 0, error, FRAME_MARKER_ABORT, 0, MAX_FRAME_UNWINDS); } // Send a trace to user-land via the `trace_events` perf event buffer. -static inline __attribute__((__always_inline__)) void send_trace(void *ctx, Trace *trace) +static inline EBPF_INLINE void send_trace(void *ctx, Trace *trace) { const u64 num_empty_frames = (MAX_FRAME_UNWINDS - trace->stack_len); const u64 send_size = sizeof(Trace) - sizeof(Frame) * num_empty_frames; @@ -350,15 +371,23 @@ static inline __attribute__((__always_inline__)) void send_trace(void *ctx, Trac } // is_kernel_address checks if the given address looks like virtual address to kernel memory. -static bool is_kernel_address(u64 addr) +static inline EBPF_INLINE bool is_kernel_address(u64 addr) { return addr & 0xFF00000000000000UL; } +// Reads a bias_and_unwind_program value from PIDPageMappingInfo +static inline EBPF_INLINE void +decode_bias_and_unwind_program(u64 bias_and_unwind_program, u64 *bias, int *unwind_program) +{ + *bias = bias_and_unwind_program & 0x00FFFFFFFFFFFFFF; + *unwind_program = bias_and_unwind_program >> 56; +} + // resolve_unwind_mapping decodes the current PC's mapping and prepares unwinding information. // The state text_section_id and text_section_offset are updated accordingly. The unwinding program // index that should be used is written to the given `unwinder` pointer. -static ErrorCode resolve_unwind_mapping(PerCPURecord *record, int *unwinder) +static inline EBPF_INLINE ErrorCode resolve_unwind_mapping(PerCPURecord *record, int *unwinder) { UnwindState *state = &record->state; pid_t pid = record->trace.pid; @@ -411,10 +440,21 @@ static ErrorCode resolve_unwind_mapping(PerCPURecord *record, int *unwinder) return ERR_OK; } +// matches_interpreter_range checks if the given text section offset falls within +// the valid address ranges of a known interpreter. An OffsetRange can contain up to +// two disjoint ranges (lower_offset1-upper_offset1 and lower_offset2-upper_offset2) +// to accommodate interpreters that may have code sections split across non-contiguous +// memory regions. Returns true if the offset matches either range. +static inline EBPF_INLINE bool matches_interpreter_range(u64 section_offset, OffsetRange *range) +{ + return ((section_offset >= range->lower_offset1) && (section_offset <= range->upper_offset1)) || + ((section_offset >= range->lower_offset2) && (section_offset <= range->upper_offset2)); +} + // get_next_interpreter tries to get the next interpreter unwinder from the section id. // If the section id happens to be within the range of a known interpreter it will // return the interpreter unwinder otherwise the native unwinder. -static inline int get_next_interpreter(PerCPURecord *record) +static inline EBPF_INLINE int get_next_interpreter(PerCPURecord *record) { UnwindState *state = &record->state; u64 section_id = state->text_section_id; @@ -422,7 +462,7 @@ static inline int get_next_interpreter(PerCPURecord *record) // Check if the section id happens to be in the interpreter map. OffsetRange *range = bpf_map_lookup_elem(&interpreter_offsets, §ion_id); if (range != 0) { - if ((section_offset >= range->lower_offset) && (section_offset <= range->upper_offset)) { + if (matches_interpreter_range(section_offset, range)) { DEBUG_PRINT("interpreter_offsets match %d", range->program_index); if (!unwinder_is_done(record, range->program_index)) { increment_metric(metricID_UnwindCallInterpreter); @@ -436,7 +476,7 @@ static inline int get_next_interpreter(PerCPURecord *record) // get_next_unwinder_after_native_frame determines the next unwinder program to run // after a native stack frame has been unwound. -static inline __attribute__((__always_inline__)) ErrorCode +static inline EBPF_INLINE ErrorCode get_next_unwinder_after_native_frame(PerCPURecord *record, int *unwinder) { UnwindState *state = &record->state; @@ -463,8 +503,7 @@ get_next_unwinder_after_native_frame(PerCPURecord *record, int *unwinder) // get_next_unwinder_after_interpreter determines the next unwinder program to run // after an interpreter (non-native) frame sequence has been unwound. -static inline __attribute__((__always_inline__)) int -get_next_unwinder_after_interpreter(const PerCPURecord *record) +static inline EBPF_INLINE int get_next_unwinder_after_interpreter(const PerCPURecord *record) { // Since interpreter-only frame decoding is no longer supported, this // currently equals to just resuming native unwinding. @@ -473,7 +512,7 @@ get_next_unwinder_after_interpreter(const PerCPURecord *record) // tail_call is a wrapper around bpf_tail_call() and ensures that the number of tail calls is not // reached while unwinding the stack. -static inline __attribute__((__always_inline__)) void tail_call(void *ctx, int next) +static inline EBPF_INLINE void tail_call(void *ctx, int next) { PerCPURecord *record = get_per_cpu_record(); if (!record) { @@ -513,7 +552,7 @@ static inline __attribute__((__always_inline__)) void tail_call(void *ctx, int n // that's where normalization is required to make the stack delta lookups work. Note that if that // should ever change, we'd need a different mask for the data pointers, because it might diverge // from the mask for code pointers. -static inline u64 normalize_pac_ptr(u64 ptr) +static inline EBPF_INLINE u64 normalize_pac_ptr(u64 ptr) { // Retrieve PAC mask from the system config. u32 key = 0; @@ -532,7 +571,7 @@ static inline u64 normalize_pac_ptr(u64 ptr) #endif // Initialize state from pt_regs -static inline ErrorCode +static inline EBPF_INLINE ErrorCode copy_state_regs(UnwindState *state, struct pt_regs *regs, bool interrupted_kernelmode) { #if defined(__x86_64__) @@ -566,6 +605,7 @@ copy_state_regs(UnwindState *state, struct pt_regs *regs, bool interrupted_kerne state->fp = regs->regs[29]; state->lr = normalize_pac_ptr(regs->regs[30]); state->r22 = regs->regs[22]; + state->r28 = regs->regs[28]; // Treat syscalls as return addresses, but not IRQ handling, page faults, etc.. // https://github.com/torvalds/linux/blob/2ef5971ff3/arch/arm64/include/asm/ptrace.h#L118 @@ -587,7 +627,7 @@ copy_state_regs(UnwindState *state, struct pt_regs *regs, bool interrupted_kerne // to bpf_task_pt_regs which is emulated to support older kernels. // Once kernel requirement is increased to 5.15 this can be replaced with // the bpf_task_pt_regs() helper. -static inline long get_task_pt_regs(struct task_struct *task, SystemConfig *syscfg) +static inline EBPF_INLINE long get_task_pt_regs(struct task_struct *task, SystemConfig *syscfg) { u64 stack_ptr = (u64)task + syscfg->task_stack_offset; long stack_base; @@ -600,7 +640,7 @@ static inline long get_task_pt_regs(struct task_struct *task, SystemConfig *sysc // Determine whether the given pt_regs are from user-mode register context. // This needs to detect also invalid pt_regs in case we its kernel thread stack // without valid user mode pt_regs so is_kernel_address(pc) is not enough. -static inline bool ptregs_is_usermode(struct pt_regs *regs) +static inline EBPF_INLINE bool ptregs_is_usermode(struct pt_regs *regs) { #if defined(__x86_64__) // On x86_64 the user mode SS should always be __USER_DS. @@ -626,7 +666,7 @@ static inline bool ptregs_is_usermode(struct pt_regs *regs) // if something fails. has_usermode_regs is set to true if a user-mode register // context was found: not every thread that we interrupt will actually have // a user-mode context (e.g. kernel worker threads won't). -static inline ErrorCode +static inline EBPF_INLINE ErrorCode get_usermode_regs(struct pt_regs *ctx, UnwindState *state, bool *has_usermode_regs) { ErrorCode error; @@ -667,7 +707,7 @@ get_usermode_regs(struct pt_regs *ctx, UnwindState *state, bool *has_usermode_re #else // TESTING_COREDUMP -static inline ErrorCode +static inline EBPF_INLINE ErrorCode get_usermode_regs(struct pt_regs *ctx, UnwindState *state, bool *has_usermode_regs) { // Coredumps provide always usermode pt_regs directly. @@ -680,7 +720,7 @@ get_usermode_regs(struct pt_regs *ctx, UnwindState *state, bool *has_usermode_re #endif // TESTING_COREDUMP -static inline int collect_trace( +static inline EBPF_INLINE int collect_trace( struct pt_regs *ctx, TraceOrigin origin, u32 pid, u32 tid, u64 trace_timestamp, u64 off_cpu_time) { // The trace is reused on each call to this function so we have to reset the @@ -714,7 +754,8 @@ static inline int collect_trace( } if (!pid_information_exists(ctx, pid)) { - if (report_pid(ctx, pid, RATELIMIT_ACTION_DEFAULT)) { + u64 pid_tgid = (u64)pid << 32 | tid; + if (report_pid(ctx, pid_tgid, RATELIMIT_ACTION_DEFAULT)) { increment_metric(metricID_NumProcNew); } return 0; diff --git a/support/ebpf/tracer.ebpf.release.amd64 b/support/ebpf/tracer.ebpf.release.amd64 index c9fd60c3b..446092f78 100644 Binary files a/support/ebpf/tracer.ebpf.release.amd64 and b/support/ebpf/tracer.ebpf.release.amd64 differ diff --git a/support/ebpf/tracer.ebpf.release.arm64 b/support/ebpf/tracer.ebpf.release.arm64 index 27aa5b678..9f6e4def2 100644 Binary files a/support/ebpf/tracer.ebpf.release.arm64 and b/support/ebpf/tracer.ebpf.release.arm64 differ diff --git a/support/ebpf/tsd.h b/support/ebpf/tsd.h index 4ecd1d1aa..2f95c012a 100644 --- a/support/ebpf/tsd.h +++ b/support/ebpf/tsd.h @@ -4,7 +4,7 @@ #include "bpfdefs.h" // tsd_read reads from the Thread Specific Data location associated with the provided key. -static inline __attribute__((__always_inline__)) int +static inline EBPF_INLINE int tsd_read(const TSDInfo *tsi, const void *tsd_base, int key, void **out) { const void *tsd_addr = tsd_base + tsi->offset; @@ -30,7 +30,7 @@ tsd_read(const TSDInfo *tsi, const void *tsd_base, int key, void **out) } // tsd_get_base looks up the base address for TSD variables (TPBASE). -static inline __attribute__((__always_inline__)) int tsd_get_base(void **tsd_base) +static inline EBPF_INLINE int tsd_get_base(void **tsd_base) { #ifdef TESTING_COREDUMP *tsd_base = (void *)__cgo_ctx->tp_base; diff --git a/support/ebpf/types.h b/support/ebpf/types.h index 9a0cb63ba..4fd4a575f 100644 --- a/support/ebpf/types.h +++ b/support/ebpf/types.h @@ -301,6 +301,12 @@ enum { // number of failures to unwind code object due to its large size metricID_UnwindDotnetErrCodeTooLarge, + // number of attempts to read Go custom labels + metricID_UnwindGoLabelsAttempts, + + // number of failures to read Go custom labels + metricID_UnwindGoLabelsFailures, + // // Metric IDs above are for counters (cumulative values) // @@ -328,6 +334,7 @@ typedef enum TracePrograms { PROG_UNWIND_RUBY, PROG_UNWIND_V8, PROG_UNWIND_DOTNET, + PROG_GO_LABELS, NUM_TRACER_PROGS, } TracePrograms; @@ -524,6 +531,22 @@ typedef struct __attribute__((packed)) ApmCorrelationBuf { ApmSpanID transaction_id; } ApmCorrelationBuf; +#define CUSTOM_LABEL_MAX_KEY_LEN COMM_LEN +// Big enough to hold UUIDs, etc. +#define CUSTOM_LABEL_MAX_VAL_LEN 48 + +typedef struct CustomLabel { + char key[CUSTOM_LABEL_MAX_KEY_LEN]; + char val[CUSTOM_LABEL_MAX_VAL_LEN]; +} CustomLabel; + +#define MAX_CUSTOM_LABELS 10 + +typedef struct CustomLabelsArray { + unsigned len; + CustomLabel labels[MAX_CUSTOM_LABELS]; +} CustomLabelsArray; + // Container for a stack trace typedef struct Trace { // The process ID @@ -540,6 +563,8 @@ typedef struct Trace { ApmSpanID apm_transaction_id; // APM trace ID or all-zero if not present. ApmTraceID apm_trace_id; + // Custom Labels + CustomLabelsArray custom_labels; // The kernel stack ID. s32 kernel_stack_id; // The number of frames in the stack. @@ -573,7 +598,7 @@ typedef struct UnwindState { u64 rax, r9, r11, r13, r15; #elif defined(__aarch64__) // Current register values for named registers - u64 lr, r22; + u64 lr, r22, r28; #endif // The executable ID/hash associated with PC @@ -681,6 +706,31 @@ typedef struct PythonUnwindScratchSpace { u8 code[192]; } PythonUnwindScratchSpace; +// https://github.com/golang/go/blob/6885bad7dd/src/cmd/compile/internal/types/size.go#L28 +struct GoString { + char *str; + u64 len; +}; + +// https://github.com/golang/go/blob/6885bad7dd/src/cmd/compile/internal/types/size.go#L20 +struct GoSlice { + void *array; + u64 len; + s64 cap; +}; + +// https://github.com/golang/go/blob/6885bad7dd/src/runtime/map.go#L109 +typedef struct GoMapBucket { + char tophash[8]; + struct GoString keys[8]; + struct GoString values[8]; + void *overflow; +} GoMapBucket; + +typedef struct CustomLabelsState { + void *go_m_ptr; +} CustomLabelsState; + // Per-CPU info for the stack being built. This contains the stack as well as // meta-data on the number of eBPF tail-calls used so far to construct it. typedef struct PerCPURecord { @@ -696,6 +746,8 @@ typedef struct PerCPURecord { PHPUnwindState phpUnwindState; // The current Ruby unwinder state. RubyUnwindState rubyUnwindState; + // State for Go and Native custom labels + CustomLabelsState customLabelsState; union { // Scratch space for the Dotnet unwinder. DotnetUnwindScratchSpace dotnetUnwindScratch; @@ -705,6 +757,10 @@ typedef struct PerCPURecord { V8UnwindScratchSpace v8UnwindScratch; // Scratch space for the Python unwinder PythonUnwindScratchSpace pythonUnwindScratch; + // Go labels scratch + GoMapBucket goMapBucket; + // Scratch for Go 1.24 labels + struct GoString labels[MAX_CUSTOM_LABELS * 2]; }; // Mask to indicate which unwinders are complete u32 unwindersDone; @@ -773,8 +829,14 @@ typedef struct StackDeltaPageInfo { // the upper boundary of the loop, and the relevant index to call in the prog // array. typedef struct OffsetRange { - u64 lower_offset; - u64 upper_offset; + u64 lower_offset1; + u64 upper_offset1; + // Fields {lower,upper}_offset2 may be used to specify an optional second range + // of an interpreter function. This may be useful if the interpreter function + // consists of two non-contiguous memory ranges, which may happen due to Hot/Cold + // split compiler optimization + u64 lower_offset2; + u64 upper_offset2; u16 program_index; // The interpreter-specific program index to call. } OffsetRange; @@ -833,21 +895,6 @@ typedef struct PIDPageMappingInfo { // FUNC_TYPE_UNKNOWN indicates an unknown interpreted function. #define FUNC_TYPE_UNKNOWN 0xfffffffffffffffe -// Builds a bias_and_unwind_program value for PIDPageMappingInfo -static inline __attribute__((__always_inline__)) u64 -encode_bias_and_unwind_program(u64 bias, int unwind_program) -{ - return bias | (((u64)unwind_program) << 56); -} - -// Reads a bias_and_unwind_program value from PIDPageMappingInfo -static inline __attribute__((__always_inline__)) void -decode_bias_and_unwind_program(u64 bias_and_unwind_program, u64 *bias, int *unwind_program) -{ - *bias = bias_and_unwind_program & 0x00FFFFFFFFFFFFFF; - *unwind_program = bias_and_unwind_program >> 56; -} - // Smallest stack delta bucket that holds up to 2^8 entries #define STACK_DELTA_BUCKET_SMALLEST 8 // Largest stack delta bucket that holds up to 2^23 entries @@ -887,4 +934,14 @@ typedef struct ApmIntProcInfo { u64 tls_offset; } ApmIntProcInfo; -#endif +typedef struct GoLabelsOffsets { + u32 m_offset; + u32 curg; + u32 labels; + u32 hmap_count; + u32 hmap_log2_bucket_count; + u32 hmap_buckets; + s32 tls_offset; +} GoLabelsOffsets; + +#endif // OPTI_TYPES_H diff --git a/support/ebpf/v8_tracer.ebpf.c b/support/ebpf/v8_tracer.ebpf.c index 10ca4d5f3..29b0e7004 100644 --- a/support/ebpf/v8_tracer.ebpf.c +++ b/support/ebpf/v8_tracer.ebpf.c @@ -40,7 +40,7 @@ bpf_map_def SEC("maps") v8_procs = { }; // Record a V8 frame -static inline __attribute__((__always_inline__)) ErrorCode push_v8( +static EBPF_INLINE ErrorCode push_v8( Trace *trace, unsigned long pointer_and_type, unsigned long delta_or_marker, bool return_address) { DEBUG_PRINT( @@ -52,8 +52,7 @@ static inline __attribute__((__always_inline__)) ErrorCode push_v8( } // Verify a V8 tagged pointer -static inline __attribute__((__always_inline__)) uintptr_t -v8_verify_pointer(uintptr_t maybe_pointer) +static EBPF_INLINE uintptr_t v8_verify_pointer(uintptr_t maybe_pointer) { if ((maybe_pointer & HeapObjectTagMask) != HeapObjectTag) { return 0; @@ -62,7 +61,7 @@ v8_verify_pointer(uintptr_t maybe_pointer) } // Read and verify a V8 tagged pointer from given memory location. -static inline __attribute__((__always_inline__)) uintptr_t v8_read_object_ptr(uintptr_t addr) +static EBPF_INLINE uintptr_t v8_read_object_ptr(uintptr_t addr) { uintptr_t maybe_pointer; if (bpf_probe_read_user(&maybe_pointer, sizeof(maybe_pointer), (void *)addr)) { @@ -74,8 +73,7 @@ static inline __attribute__((__always_inline__)) uintptr_t v8_read_object_ptr(ui // Verify and parse a V8 SMI ("SMall Integer") value. // On 64-bit systems: SMI is the upper 32-bits of a 64-bit word, and the lowest bit is the tag. // Returns the SMI value, or def_value in case of errors. -static inline __attribute__((__always_inline__)) uintptr_t -v8_parse_smi(uintptr_t maybe_smi, uintptr_t def_value) +static EBPF_INLINE uintptr_t v8_parse_smi(uintptr_t maybe_smi, uintptr_t def_value) { if ((maybe_smi & SmiTagMask) != SmiTag) { return def_value; @@ -85,8 +83,7 @@ v8_parse_smi(uintptr_t maybe_smi, uintptr_t def_value) // Read the type tag of a Heap Object at given memory location. // Returns zero on error (valid object type IDs are non-zero). -static inline __attribute__((__always_inline__)) u16 -v8_read_object_type(V8ProcInfo *vi, uintptr_t addr) +static EBPF_INLINE u16 v8_read_object_type(V8ProcInfo *vi, uintptr_t addr) { if (!addr) { return 0; @@ -100,12 +97,11 @@ v8_read_object_type(V8ProcInfo *vi, uintptr_t addr) } // Unwind one V8 frame -static inline __attribute__((__always_inline__)) ErrorCode -unwind_one_v8_frame(PerCPURecord *record, V8ProcInfo *vi, bool top) +static EBPF_INLINE ErrorCode unwind_one_v8_frame(PerCPURecord *record, V8ProcInfo *vi, bool top) { UnwindState *state = &record->state; Trace *trace = &record->trace; - unsigned long regs[2], sp = state->sp, fp = state->fp, pc = state->pc; + unsigned long sp = state->sp, fp = state->fp, pc = state->pc; V8UnwindScratchSpace *scratch = &record->v8UnwindScratch; // All V8 frames have frame pointer. Check that the FP looks valid. @@ -275,20 +271,18 @@ unwind_one_v8_frame(PerCPURecord *record, V8ProcInfo *vi, bool top) u32 cookie = (code_size << 4) | code_kind; delta_or_marker = (pc - code_start) | ((uintptr_t)cookie << V8_LINE_COOKIE_SHIFT); -frame_done: - // Unwind with frame pointer - if (bpf_probe_read_user(regs, sizeof(regs), (void *)fp)) { - DEBUG_PRINT("v8: --> bad frame pointer"); - increment_metric(metricID_UnwindV8ErrBadFP); - return ERR_V8_BAD_FP; - } - +frame_done:; ErrorCode error = push_v8(trace, pointer_and_type, delta_or_marker, state->return_address); if (error) { return error; } - state->sp = fp + sizeof(regs); + // Unwind with frame pointer + if (!unwinder_unwind_frame_pointer(state)) { + DEBUG_PRINT("v8: --> bad frame pointer"); + increment_metric(metricID_UnwindV8ErrBadFP); + return ERR_V8_BAD_FP; + } // The JS Entry Frame's layout differs from other frames because some callee // saved registers might be pushed onto the stack before the [fp, lr] pair. @@ -297,10 +291,6 @@ unwind_one_v8_frame(PerCPURecord *record, V8ProcInfo *vi, bool top) if (pointer_and_type == V8_FILE_TYPE_MARKER && delta_or_marker == 1) state->sp += V8_ENTRYFRAME_CALLEE_SAVED_REGS_BEFORE_FP_LR_PAIR * sizeof(size_t); - state->fp = regs[0]; - state->pc = regs[1]; - unwinder_mark_nonleaf_frame(state); - DEBUG_PRINT( "v8: pc: %lx, sp: %lx, fp: %lx", (unsigned long)state->pc, @@ -314,7 +304,7 @@ unwind_one_v8_frame(PerCPURecord *record, V8ProcInfo *vi, bool top) // unwind_v8 is the entry point for tracing when invoked from the native tracer // or interpreter dispatcher. It does not reset the trace object and will append the // V8 stack frames to the trace object for the current CPU. -static inline __attribute__((__always_inline__)) int unwind_v8(struct pt_regs *ctx) +static EBPF_INLINE int unwind_v8(struct pt_regs *ctx) { PerCPURecord *record = get_per_cpu_record(); if (!record) { diff --git a/support/tests/main_thread_exit.c b/support/tests/main_thread_exit.c new file mode 100644 index 000000000..e9188cdeb --- /dev/null +++ b/support/tests/main_thread_exit.c @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +/* + * Implements a profiling test based on a multi-threaded process with + * a main thread that exits early. + * + * Two additional threads are created: + * 1. Burns CPU, ensures process is sampled by the profiler + * 2. Burns CPU in newly mapped pages + * + * After main thread exits, /proc/PID/maps is empty and the expected + * behavior is for the profiler to not cleanup the process, but instead + * keep profiling the remaining thread and use /proc/PID/task/TID/maps + * (TID corresponding to thread 2) to synchronize mappings. + * + * Needs OpenSSL (libssl) installed as it dynamically loads libcrypto.so. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +static void *burn(void *arg) +{ + int old_type; + + // We're just burning CPU, so asynchronous cancellation is safe + pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &old_type); + + for (;;) { + } + + // Never reached + return NULL; +} + +static void *hash(void *arg) +{ + unsigned char buf[1024]; + + printf("Thread TID: %d, sleeping for 5s\n", gettid()); + sleep(5); + + unsigned char *(*MD5)(const unsigned char *d, unsigned long n, unsigned char *md); + void *handle = dlopen("libcrypto.so", RTLD_LAZY); + if (!handle) { + fprintf(stderr, "%s\n", dlerror()); + return NULL; + } + + MD5 = dlsym(handle, "MD5"); + if (!MD5) { + fprintf(stderr, "dlsym: Could not resolve MD5\n"); + return NULL; + } + + printf("Thread TID: %d, hashing..\n", gettid()); + for (;;) { + MD5(buf, sizeof(buf), NULL); + } + + // Never reached + return NULL; +} + +int main() +{ + int ret; + pthread_t tid; + printf("Main thread is starting, PID: %d\n", getpid()); + + // Create a new thread to burn CPU / ensure process gets profiled + if ((ret = pthread_create(&tid, NULL, burn, NULL)) != 0) { + fprintf(stderr, "pthread_create: burn %d\n", ret); + exit(EXIT_FAILURE); + } + + sleep(2); + + printf("Press ENTER to exit main thread: "); + getchar(); + + // Stop CPU burn thread to reduce noise while hash thread is running + void *tret; + pthread_cancel(tid); + pthread_join(tid, &tret); + + if (tret != PTHREAD_CANCELED) { + fprintf(stderr, "pthread_join: %p\n", tret); + exit(EXIT_FAILURE); + } + + printf("Main thread is exiting\n"); + + // Create a new thread to burn CPU in newly mapped pages + if ((ret = pthread_create(&tid, NULL, hash, NULL) != 0)) { + fprintf(stderr, "pthread_create: hash %d\n", ret); + exit(EXIT_FAILURE); + } + + pthread_detach(pthread_self()); + pthread_exit(NULL); + return 0; +} diff --git a/support/types.go b/support/types.go index f24488009..836f10a2e 100644 --- a/support/types.go +++ b/support/types.go @@ -19,6 +19,7 @@ const ( FrameMarkerPerl = 0x7 FrameMarkerV8 = 0x8 FrameMarkerDotnet = 0xa + FrameMarkerGo = 0xb FrameMarkerAbort = 0xff ) @@ -32,6 +33,7 @@ const ( ProgUnwindPerl = 0x3 ProgUnwindV8 = 0x7 ProgUnwindDotnet = 0x8 + ProgGoLabels = 0x9 ) const ( @@ -47,7 +49,7 @@ const ( const MaxFrameUnwinds = 0x80 const ( - MetricIDBeginCumulative = 0x60 + MetricIDBeginCumulative = 0x62 ) const ( @@ -127,3 +129,22 @@ const ( sizeof_PHPProcInfo = 0x18 sizeof_RubyProcInfo = 0x20 ) + +const ( + UnwindOpcodeCommand uint8 = 0x0 + UnwindOpcodeBaseCFA uint8 = 0x1 + UnwindOpcodeBaseSP uint8 = 0x2 + UnwindOpcodeBaseFP uint8 = 0x3 + UnwindOpcodeBaseLR uint8 = 0x4 + UnwindOpcodeBaseReg uint8 = 0x5 + UnwindOpcodeFlagDeref uint8 = 0x80 + + UnwindCommandInvalid int32 = 0x0 + UnwindCommandStop int32 = 0x1 + UnwindCommandPLT int32 = 0x2 + UnwindCommandSignal int32 = 0x3 + UnwindCommandFramePointer int32 = 0x4 + + UnwindDerefMask int32 = 0x7 + UnwindDerefMultiplier int32 = 0x8 +) diff --git a/support/types_def.go b/support/types_def.go index c094e0767..d04b153e1 100644 --- a/support/types_def.go +++ b/support/types_def.go @@ -8,6 +8,7 @@ package support // import "go.opentelemetry.io/ebpf-profiler/support" /* #include "./ebpf/types.h" #include "./ebpf/frametypes.h" +#include "./ebpf/stackdeltatypes.h" */ import "C" @@ -24,6 +25,7 @@ const ( FrameMarkerPerl = C.FRAME_MARKER_PERL FrameMarkerV8 = C.FRAME_MARKER_V8 FrameMarkerDotnet = C.FRAME_MARKER_DOTNET + FrameMarkerGo = C.FRAME_MARKER_GO FrameMarkerAbort = C.FRAME_MARKER_ABORT ) @@ -37,6 +39,7 @@ const ( ProgUnwindPerl = C.PROG_UNWIND_PERL ProgUnwindV8 = C.PROG_UNWIND_V8 ProgUnwindDotnet = C.PROG_UNWIND_DOTNET + ProgGoLabels = C.PROG_GO_LABELS ) const ( @@ -105,3 +108,25 @@ const ( sizeof_PHPProcInfo = C.sizeof_PHPProcInfo sizeof_RubyProcInfo = C.sizeof_RubyProcInfo ) + +const ( + // UnwindOpcodes from the C header file + UnwindOpcodeCommand uint8 = C.UNWIND_OPCODE_COMMAND + UnwindOpcodeBaseCFA uint8 = C.UNWIND_OPCODE_BASE_CFA + UnwindOpcodeBaseSP uint8 = C.UNWIND_OPCODE_BASE_SP + UnwindOpcodeBaseFP uint8 = C.UNWIND_OPCODE_BASE_FP + UnwindOpcodeBaseLR uint8 = C.UNWIND_OPCODE_BASE_LR + UnwindOpcodeBaseReg uint8 = C.UNWIND_OPCODE_BASE_REG + UnwindOpcodeFlagDeref uint8 = C.UNWIND_OPCODEF_DEREF + + // UnwindCommands from the C header file + UnwindCommandInvalid int32 = C.UNWIND_COMMAND_INVALID + UnwindCommandStop int32 = C.UNWIND_COMMAND_STOP + UnwindCommandPLT int32 = C.UNWIND_COMMAND_PLT + UnwindCommandSignal int32 = C.UNWIND_COMMAND_SIGNAL + UnwindCommandFramePointer int32 = C.UNWIND_COMMAND_FRAME_POINTER + + // UnwindDeref handling from the C header file + UnwindDerefMask int32 = C.UNWIND_DEREF_MASK + UnwindDerefMultiplier int32 = C.UNWIND_DEREF_MULTIPLIER +) diff --git a/target/aarch64-unknown-linux-musl/release/libsymblib_capi.a b/target/aarch64-unknown-linux-musl/release/libsymblib_capi.a deleted file mode 100644 index a4491dc0e..000000000 Binary files a/target/aarch64-unknown-linux-musl/release/libsymblib_capi.a and /dev/null differ diff --git a/target/x86_64-unknown-linux-musl/release/libsymblib_capi.a b/target/x86_64-unknown-linux-musl/release/libsymblib_capi.a deleted file mode 100644 index 151c8dbc1..000000000 Binary files a/target/x86_64-unknown-linux-musl/release/libsymblib_capi.a and /dev/null differ diff --git a/testsupport/testfiles.go b/testsupport/testfiles.go index c19c099d5..5c0373198 100644 --- a/testsupport/testfiles.go +++ b/testsupport/testfiles.go @@ -27,7 +27,7 @@ func writeExecutable(exeContents string) (string, error) { } else if err != nil { return "", fmt.Errorf("failed to write file: %v", err) } - exeFile.Close() + _ = exeFile.Close() return exeFile.Name(), nil } diff --git a/testutils/helpers.go b/testutils/helpers.go new file mode 100644 index 000000000..3cc04847a --- /dev/null +++ b/testutils/helpers.go @@ -0,0 +1,130 @@ +package testutils // import "go.opentelemetry.io/ebpf-profiler/testutils" + +import ( + "bufio" + "context" + "errors" + "io" + "os" + "strings" + "testing" + "time" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/ebpf-profiler/host" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/reporter" + "go.opentelemetry.io/ebpf-profiler/tracer" + tracertypes "go.opentelemetry.io/ebpf-profiler/tracer/types" +) + +type MockIntervals struct{} + +func (f MockIntervals) MonitorInterval() time.Duration { return 1 * time.Second } +func (f MockIntervals) TracePollInterval() time.Duration { return 250 * time.Millisecond } +func (f MockIntervals) PIDCleanupInterval() time.Duration { return 1 * time.Second } + +type MockReporter struct{} + +func (f MockReporter) ExecutableKnown(_ libpf.FileID) bool { + return true +} + +func (f MockReporter) ExecutableMetadata(_ *reporter.ExecutableMetadataArgs) { +} + +func (f MockReporter) FrameKnown(_ libpf.FrameID) bool { + return true +} + +func (f MockReporter) FrameMetadata(_ *reporter.FrameMetadataArgs) {} + +func StartTracer(ctx context.Context, t *testing.T, et tracertypes.IncludedTracers, + r reporter.SymbolReporter) (chan *host.Trace, *tracer.Tracer) { + trc, err := tracer.NewTracer(ctx, &tracer.Config{ + Reporter: r, + Intervals: &MockIntervals{}, + IncludeTracers: et, + SamplesPerSecond: 20, + ProbabilisticInterval: 100, + ProbabilisticThreshold: 100, + DebugTracer: true, + }) + require.NoError(t, err) + + go readTracePipe(ctx) + + trc.StartPIDEventProcessor(ctx) + + err = trc.AttachTracer() + require.NoError(t, err) + + log.Info("Attached tracer program") + + err = trc.EnableProfiling() + require.NoError(t, err) + + err = trc.AttachSchedMonitor() + require.NoError(t, err) + + traceCh := make(chan *host.Trace) + + // Spawn monitors for the various result maps + err = trc.StartMapMonitors(ctx, traceCh) + require.NoError(t, err) + + return traceCh, trc +} + +func getTracePipe() (*os.File, error) { + for _, mnt := range []string{ + "/sys/kernel/debug/tracing", + "/sys/kernel/tracing", + "/tracing", + "/trace"} { + t, err := os.Open(mnt + "/trace_pipe") + if err == nil { + return t, nil + } + log.Errorf("Could not open trace_pipe at %s: %s", mnt, err) + } + return nil, os.ErrNotExist +} + +func readTracePipe(ctx context.Context) { + tp, err := getTracePipe() + if err != nil { + log.Warning("Could not open trace_pipe, check that debugfs is mounted") + return + } + + // When we're done kick ReadString out of blocked I/O. + go func() { + <-ctx.Done() + _ = tp.Close() + }() + + r := bufio.NewReader(tp) + for { + line, err := r.ReadString('\n') + if err != nil { + if errors.Is(err, io.EOF) { + continue + } + if errors.Is(err, os.ErrClosed) { + return + } + log.Error(err) + return + } + line = strings.TrimSpace(line) + if line != "" { + log.Infof("%s", line) + } + } +} + +func IsRoot() bool { + return os.Geteuid() == 0 +} diff --git a/tools/coredump/cloudstore/cloudstore.go b/tools/coredump/cloudstore/cloudstore.go new file mode 100644 index 000000000..89086b9a5 --- /dev/null +++ b/tools/coredump/cloudstore/cloudstore.go @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// cloudstore provides access to the cloud based storage used in the tests. +package cloudstore // import "go.opentelemetry.io/ebpf-profiler/tools/coredump/cloudstore" + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +// moduleStoreRegion defines the S3 bucket OCI region. +const moduleStoreRegion = "us-sanjose-1" + +// moduleStoreObjectNamespace defines the S3 bucket OCI object name space. +const moduleStoreObjectNamespace = "axtwf1hkrwcy" + +// modulePublicReadUrl defines the S3 bucket OCI public read only base path. +// +//nolint:lll +const modulePublicReadURL = "sm-wftyyzHJkBghWeexmK1o5ArimNwZC-5eBej5Lx4e46sLVHtO_y7Zf7FZgoIu_/n/axtwf1hkrwcy" + +// moduleStoreS3Bucket defines the S3 bucket used for the module store. +const moduleStoreS3Bucket = "ebpf-profiling-coredumps" + +func PublicReadURL() string { + return fmt.Sprintf("https://%s.objectstorage.%s.oci.customer-oci.com/p/%s/b/%s/o/", + moduleStoreObjectNamespace, moduleStoreRegion, modulePublicReadURL, moduleStoreS3Bucket) +} + +func ModulestoreS3Bucket() string { + return moduleStoreS3Bucket +} + +func Client() (*s3.Client, error) { + cfg, err := awsconfig.LoadDefaultConfig(context.Background()) + if err != nil { + return nil, err + } + + return s3.NewFromConfig(cfg, func(o *s3.Options) { + baseEndpoint := fmt.Sprintf("https://%s.compat.objectstorage.%s.oraclecloud.com/", + moduleStoreObjectNamespace, moduleStoreRegion) + o.Region = moduleStoreRegion + o.BaseEndpoint = aws.String(baseEndpoint) + o.UsePathStyle = true + }), nil +} diff --git a/tools/coredump/coredump_test.go b/tools/coredump/coredump_test.go index 26bbde68f..f805ee130 100644 --- a/tools/coredump/coredump_test.go +++ b/tools/coredump/coredump_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/stretchr/testify/require" + "go.opentelemetry.io/ebpf-profiler/tools/coredump/cloudstore" + "go.opentelemetry.io/ebpf-profiler/tools/coredump/modulestore" ) func TestCoreDumps(t *testing.T) { @@ -15,7 +17,10 @@ func TestCoreDumps(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, cases) - store, err := initModuleStore() + cloudClient, err := cloudstore.Client() + require.NoError(t, err) + store, err := modulestore.New(cloudClient, + cloudstore.PublicReadURL(), cloudstore.ModulestoreS3Bucket(), "modulecache") require.NoError(t, err) for _, filename := range cases { @@ -23,6 +28,9 @@ func TestCoreDumps(t *testing.T) { t.Run(filename, func(t *testing.T) { testCase, err := readTestCase(filename) require.NoError(t, err) + if testCase.Skip != "" { + t.Skip(testCase.Skip) + } ctx := context.Background() diff --git a/tools/coredump/ebpfcode.go b/tools/coredump/ebpfcode.go index 854ba2503..4ce390651 100644 --- a/tools/coredump/ebpfcode.go +++ b/tools/coredump/ebpfcode.go @@ -54,6 +54,7 @@ int bpf_log(const char *fmt, ...) #include "../../support/ebpf/ruby_tracer.ebpf.c" #include "../../support/ebpf/v8_tracer.ebpf.c" #include "../../support/ebpf/system_config.ebpf.c" +#include "../../support/ebpf/go_labels.ebpf.c" int unwind_traces(u64 id, int debug, u64 tp_base, void *ctx) { @@ -117,6 +118,9 @@ int bpf_tail_call(void *ctx, bpf_map_def *map, int index) case PROG_UNWIND_DOTNET: rc = unwind_dotnet(ctx); break; + case PROG_GO_LABELS: + rc = perf_go_labels(ctx); + break; default: return -1; } diff --git a/tools/coredump/ebpfmaps.go b/tools/coredump/ebpfmaps.go index e478be970..412864b4f 100644 --- a/tools/coredump/ebpfmaps.go +++ b/tools/coredump/ebpfmaps.go @@ -44,13 +44,11 @@ func (emc *ebpfMapsCoredump) CollectMetrics() []metrics.Metric { func (emc *ebpfMapsCoredump) UpdateInterpreterOffsets(ebpfProgIndex uint16, fileID host.FileID, offsetRanges []util.Range) error { - offsetRange := offsetRanges[0] - value := C.OffsetRange{ - lower_offset: C.u64(offsetRange.Start), - upper_offset: C.u64(offsetRange.End), - program_index: C.u16(ebpfProgIndex), + key, value, err := pmebpf.InterpreterOffsetKeyValue(ebpfProgIndex, fileID, offsetRanges) + if err != nil { + return err } - emc.ctx.addMap(&C.interpreter_offsets, C.u64(fileID), libpf.SliceFrom(&value)) + emc.ctx.addMap(&C.interpreter_offsets, C.u64(key), libpf.SliceFrom(&value)) return nil } diff --git a/tools/coredump/json.go b/tools/coredump/json.go index 388691de1..8d1c535b1 100644 --- a/tools/coredump/json.go +++ b/tools/coredump/json.go @@ -18,6 +18,7 @@ import ( // CoredumpTestCase is the data structure generated from the core dump. type CoredumpTestCase struct { CoredumpRef modulestore.ID `json:"coredump-ref"` + Skip string `json:"skip,omitempty"` Threads []ThreadInfo `json:"threads"` Modules []ModuleInfo `json:"modules"` } diff --git a/tools/coredump/main.go b/tools/coredump/main.go index d24aeb37a..3a1b8eabd 100644 --- a/tools/coredump/main.go +++ b/tools/coredump/main.go @@ -11,37 +11,24 @@ import ( "context" "errors" "flag" - "fmt" "os" - "github.com/aws/aws-sdk-go-v2/aws" - awsconfig "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/peterbourgon/ff/v3/ffcli" log "github.com/sirupsen/logrus" + "go.opentelemetry.io/ebpf-profiler/tools/coredump/cloudstore" "go.opentelemetry.io/ebpf-profiler/tools/coredump/modulestore" ) -// moduleStoreRegion defines the S3 bucket OCI region. -const moduleStoreRegion = "us-sanjose-1" - -// moduleStoreObjectNamespace defines the S3 bucket OCI object name space. -const moduleStoreObjectNamespace = "axtwf1hkrwcy" - -// modulePublicReadUrl defines the S3 bucket OCI public read only base path. -// -//nolint:lll -const modulePublicReadURL = "sm-wftyyzHJkBghWeexmK1o5ArimNwZC-5eBej5Lx4e46sLVHtO_y7Zf7FZgoIu_/n/axtwf1hkrwcy" - -// moduleStoreS3Bucket defines the S3 bucket used for the module store. -const moduleStoreS3Bucket = "ebpf-profiling-coredumps" - func main() { log.SetReportCaller(false) log.SetFormatter(&log.TextFormatter{}) - store, err := initModuleStore() + cloudClient, err := cloudstore.Client() + if err != nil { + log.Fatalf("%v", err) + } + store, err := modulestore.New(cloudClient, + cloudstore.PublicReadURL(), cloudstore.ModulestoreS3Bucket(), "modulecache") if err != nil { log.Fatalf("%v", err) } @@ -70,22 +57,3 @@ func main() { } } } - -func initModuleStore() (*modulestore.Store, error) { - publicReadURL := fmt.Sprintf("https://%s.objectstorage.%s.oci.customer-oci.com/p/%s/b/%s/o/", - moduleStoreObjectNamespace, moduleStoreRegion, modulePublicReadURL, moduleStoreS3Bucket) - - cfg, err := awsconfig.LoadDefaultConfig(context.Background()) - if err != nil { - return nil, err - } - - s3Client := s3.NewFromConfig(cfg, func(o *s3.Options) { - baseEndpoint := fmt.Sprintf("https://%s.compat.objectstorage.%s.oraclecloud.com/", - moduleStoreObjectNamespace, moduleStoreRegion) - o.Region = moduleStoreRegion - o.BaseEndpoint = aws.String(baseEndpoint) - o.UsePathStyle = true - }) - return modulestore.New(s3Client, publicReadURL, moduleStoreS3Bucket, "modulecache") -} diff --git a/tools/coredump/modulestore/store.go b/tools/coredump/modulestore/store.go index 35ac81ddf..5b7d62dd7 100644 --- a/tools/coredump/modulestore/store.go +++ b/tools/coredump/modulestore/store.go @@ -8,6 +8,8 @@ package modulestore // import "go.opentelemetry.io/ebpf-profiler/tools/coredump/ import ( "context" + "crypto/sha256" + "encoding/base64" "errors" "fmt" "io" @@ -197,16 +199,27 @@ func (store *Store) UploadModule(id ID) error { return fmt.Errorf("failed to open local file: %w", err) } + hasher := sha256.New() + if _, err = io.Copy(hasher, file); err != nil { + return fmt.Errorf("failed to hash content of %q: %v", localPath, err) + } + contentSHA256 := base64.StdEncoding.EncodeToString(hasher.Sum(nil)) + moduleKey := makeS3Key(id) contentType := "application/octet-stream" contentDisposition := "attachment" + if _, err = file.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("failed to set position in file %q: %v", localPath, err) + } + _, err = store.s3client.PutObject(context.TODO(), &s3.PutObjectInput{ Bucket: &store.bucket, Key: &moduleKey, Body: file, ContentType: &contentType, ContentDisposition: &contentDisposition, + ChecksumSHA256: &contentSHA256, }) if err != nil { return fmt.Errorf("failed to upload file: %w", err) @@ -426,19 +439,23 @@ func (store *Store) ensurePresentLocally(id ID) (string, error) { return localPath, nil } - // Download the file to a temporary location to prevent half-complete modules on crashes. - file, err := os.CreateTemp(store.localCachePath, localTempPrefix) - if err != nil { - return "", fmt.Errorf("failed to create local file: %w", err) - } - defer file.Close() - moduleKey := makeS3Key(id) resp, err := http.Get(store.publicReadURL + moduleKey) if err != nil { return "", fmt.Errorf("failed to request file: %w", err) } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + errorResponse, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("store returned %d %s", resp.StatusCode, errorResponse) + } + + // Download the file to a temporary location to prevent half-complete modules on crashes. + file, err := os.CreateTemp(store.localCachePath, localTempPrefix) + if err != nil { + return "", fmt.Errorf("failed to create local file: %w", err) + } + defer file.Close() if _, err = io.Copy(file, resp.Body); err != nil { return "", fmt.Errorf("failed to receive file: %w", err) } diff --git a/tools/coredump/new.go b/tools/coredump/new.go index 56a342508..5ea18bd96 100644 --- a/tools/coredump/new.go +++ b/tools/coredump/new.go @@ -10,6 +10,7 @@ import ( "fmt" "os" "os/exec" + "path" "strconv" "github.com/peterbourgon/ff/v3/ffcli" @@ -72,7 +73,7 @@ func (tc *trackedCoredump) warnMissing(fileName string) { func (tc *trackedCoredump) CalculateMappingFileID(m *process.Mapping) (libpf.FileID, error) { if !m.IsVDSO() && !m.IsAnonymous() { - fid, err := libpf.FileIDFromExecutableFile(tc.prefix + m.Path) + fid, err := libpf.FileIDFromExecutableFile(path.Join(tc.prefix, m.Path)) if err == nil { tc.seen[m.Path] = libpf.Void{} return fid, nil @@ -84,7 +85,7 @@ func (tc *trackedCoredump) CalculateMappingFileID(m *process.Mapping) (libpf.Fil func (tc *trackedCoredump) OpenMappingFile(m *process.Mapping) (process.ReadAtCloser, error) { if !m.IsVDSO() && !m.IsAnonymous() { - rac, err := os.Open(tc.prefix + m.Path) + rac, err := os.Open(path.Join(tc.prefix, m.Path)) if err == nil { tc.seen[m.Path] = libpf.Void{} return rac, nil @@ -96,7 +97,7 @@ func (tc *trackedCoredump) OpenMappingFile(m *process.Mapping) (process.ReadAtCl func (tc *trackedCoredump) OpenELF(fileName string) (*pfelf.File, error) { if fileName != process.VdsoPathName { - f, err := pfelf.Open(tc.prefix + fileName) + f, err := pfelf.Open(path.Join(tc.prefix, fileName)) if err == nil { tc.seen[fileName] = libpf.Void{} return f, err @@ -106,6 +107,16 @@ func (tc *trackedCoredump) OpenELF(fileName string) (*pfelf.File, error) { return tc.CoredumpProcess.OpenELF(fileName) } +func (tc *trackedCoredump) ExtractAsFile(fileName string) (string, error) { + prefixedFileName := path.Join(tc.prefix, fileName) + if _, err := os.Stat(prefixedFileName); err != nil { + tc.warnMissing(fileName) + return "", err + } + tc.seen[fileName] = libpf.Void{} + return prefixedFileName, nil +} + func newNewCmd(store *modulestore.Store) *ffcli.Command { args := &newCmd{store: store} @@ -225,7 +236,6 @@ func dumpCore(pid uint64, noModuleBundling bool) (string, error) { } // `gcore` only accepts a path-prefix, not an exact path. - //nolint:gosec err := exec.Command("gcore", "-o", gcorePathPrefix, strconv.FormatUint(pid, 10)).Run() if err != nil { return "", fmt.Errorf("gcore failed: %w", err) diff --git a/tools/coredump/rebase.go b/tools/coredump/rebase.go index 04110bffc..8b024e2ef 100644 --- a/tools/coredump/rebase.go +++ b/tools/coredump/rebase.go @@ -62,7 +62,7 @@ func (cmd *rebaseCmd) exec(context.Context, []string) (err error) { } testCase.Threads, err = ExtractTraces(context.Background(), core, false, nil) - core.Close() + _ = core.Close() if err != nil { return fmt.Errorf("failed to extract traces: %w", err) } diff --git a/tools/coredump/storecoredump.go b/tools/coredump/storecoredump.go index c189760a6..a8405d7ca 100644 --- a/tools/coredump/storecoredump.go +++ b/tools/coredump/storecoredump.go @@ -18,8 +18,9 @@ import ( type StoreCoredump struct { *process.CoredumpProcess - store *modulestore.Store - modules map[string]ModuleInfo + store *modulestore.Store + modules map[string]ModuleInfo + tempFiles map[string]string } var _ pfelf.ELFOpener = &StoreCoredump{} @@ -60,6 +61,34 @@ func (scd *StoreCoredump) OpenELF(path string) (*pfelf.File, error) { return scd.CoredumpProcess.OpenELF(path) } +func (scd *StoreCoredump) ExtractAsFile(file string) (string, error) { + info, ok := scd.modules[file] + if !ok { + return "", os.ErrNotExist + } + + f, err := os.CreateTemp("", "ebpf-profiler-coredump.*") + if err != nil { + return "", err + } + tmpFile := f.Name() + _ = f.Close() + + if err := scd.store.UnpackModuleToPath(info.Ref, tmpFile); err != nil { + _ = os.Remove(tmpFile) + return "", err + } + scd.tempFiles[file] = tmpFile + return tmpFile, nil +} + +func (scd *StoreCoredump) Close() error { + for _, tmpFile := range scd.tempFiles { + _ = os.Remove(tmpFile) + } + return scd.CoredumpProcess.Close() +} + func OpenStoreCoredump(store *modulestore.Store, coreFileRef modulestore.ID, modules []ModuleInfo) ( process.Process, error) { // Open the coredump from the module store. @@ -84,7 +113,8 @@ func OpenStoreCoredump(store *modulestore.Store, coreFileRef modulestore.ID, mod return &StoreCoredump{ CoredumpProcess: core, - store: store, - modules: moduleMap, + store: store, + modules: moduleMap, + tempFiles: make(map[string]string), }, nil } diff --git a/tools/coredump/testdata/amd64/dotnet7-helloworld-alpine.json b/tools/coredump/testdata/amd64/dotnet7-helloworld-alpine.json index 7d2cc819d..9dd580ae5 100644 --- a/tools/coredump/testdata/amd64/dotnet7-helloworld-alpine.json +++ b/tools/coredump/testdata/amd64/dotnet7-helloworld-alpine.json @@ -31,8 +31,7 @@ "helloworld+0x13db8", "helloworld+0x140a1", "ld-musl-x86_64.so.1+0x1c6d0", - "helloworld+0x73e5", - "" + "helloworld+0x73e5" ] }, { diff --git a/tools/coredump/testdata/amd64/gcloud_sdk_502.0.0_slim_3.11.9_clang_18.1.8.json b/tools/coredump/testdata/amd64/gcloud_sdk_502.0.0_slim_3.11.9_clang_18.1.8.json new file mode 100644 index 000000000..1cf798807 --- /dev/null +++ b/tools/coredump/testdata/amd64/gcloud_sdk_502.0.0_slim_3.11.9_clang_18.1.8.json @@ -0,0 +1,76 @@ +{ + "coredump-ref": "1c875546fb8c3b22bb7ed7b86c3a7f4da9a68a3ec93bb4732f0bff6ee793d23a", + "threads": [ + { + "lwp": 11, + "frames": [ + "fib+1 in /mnt/trash/fib.py:2", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "fib+3 in /mnt/trash/fib.py:4", + "+6 in /mnt/trash/fib.py:7", + "libpython3.11.so.1.0+0x32b580", + "libpython3.11.so.1.0+0x3259e3", + "libpython3.11.so.1.0+0x37a8f9", + "libpython3.11.so.1.0+0x378e32", + "libpython3.11.so.1.0+0x378972", + "libpython3.11.so.1.0+0x397899", + "libpython3.11.so.1.0+0x397102", + "libpython3.11.so.1.0+0x39739e", + "libpython3.11.so.1.0+0x3973fb", + "libc.so.6+0x27249", + "libc.so.6+0x27304", + "python3+0x1088" + ] + } + ], + "modules": [ + { + "ref": "11ce00a6490d5e4ef941e1f51faaddf40c088a1376f028cbc001985b779397ce", + "local-path": "/usr/lib/google-cloud-sdk/platform/bundledpythonunix/lib/libpython3.11.so.1.0" + }, + { + "ref": "df8e371a04bcf4ea2d455277ecc9cd47fc9b4c58ed27a7f4e6c8343122a4d270", + "local-path": "/usr/lib/x86_64-linux-gnu/libpthread.so.0" + }, + { + "ref": "067650d84b8f554cedf0b9ff26137bdd10cd03d4bbcdba1029a543c59d1798e5", + "local-path": "/usr/lib/x86_64-linux-gnu/libm.so.6" + }, + { + "ref": "fe279657c804dcec88728eeb27187f983f6e5dc0c89575c4bd01aa6a8147b3a1", + "local-path": "/usr/lib/x86_64-linux-gnu/libutil.so.1" + }, + { + "ref": "2a7334caf9516a482110c769e92985b30e9d7d9d96a4227b93d04cce8af0701e", + "local-path": "/usr/lib/google-cloud-sdk/platform/bundledpythonunix/bin/python3" + }, + { + "ref": "6445c275f2477ebf619b1e4ec6fe5a0e460b9745e360ef9b671cb5a2f9f362ae", + "local-path": "/usr/lib/x86_64-linux-gnu/librt.so.1" + }, + { + "ref": "1d25fd63234b59e4c581564c7a6d8f5c6cf36eee757e3d26f4b0808dd36a4896", + "local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6" + }, + { + "ref": "582f2d3d4edab86d601c54b37f04bd18fa2cda28be30e9f8c87df73c1c581354", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + }, + { + "ref": "d71263682766154c159a63504fec543e3ea64a932e5f30d5f50758fab0405fa2", + "local-path": "/usr/lib/x86_64-linux-gnu/libdl.so.2" + } + ] +} diff --git a/tools/coredump/testdata/amd64/go-1.24.1-hello.json.json b/tools/coredump/testdata/amd64/go-1.24.1-hello.json.json new file mode 100644 index 000000000..69ddd39c0 --- /dev/null +++ b/tools/coredump/testdata/amd64/go-1.24.1-hello.json.json @@ -0,0 +1,63 @@ +{ + "coredump-ref": "ad99fdc13a9fd30c511ae87fbd2f0d4ba8c16af65a691cce39dc9031f04b26f4", + "threads": [ + { + "lwp": 2683, + "frames": [ + "internal/runtime/syscall.Syscall6+0 in /usr/local/go/src/internal/runtime/syscall/asm_linux_amd64.s:36", + "syscall.RawSyscall6+0 in /usr/local/go/src/syscall/syscall_linux.go:66", + "syscall.Syscall+0 in /usr/local/go/src/syscall/syscall_linux.go:86", + "syscall.write+0 in /usr/local/go/src/syscall/zsyscall_linux_amd64.go:964", + "internal/poll.(*FD).Write+0 in /usr/local/go/src/syscall/syscall_unix.go:211", + "os.(*File).Write+0 in /usr/local/go/src/os/file.go:196", + "fmt.Fprintln+0 in /usr/local/go/src/fmt/print.go:305", + "main.hello+0 in /home/ec2-user/testsources/go/hello.go:14", + "main.main+0 in /home/ec2-user/testsources/go/hello.go:50", + "runtime.main+0 in /usr/local/go/src/internal/runtime/atomic/types.go:194", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_amd64.s:1701" + ] + }, + { + "lwp": 2684, + "frames": [ + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_amd64.s:135", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:6108", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:6108", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1855", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1817", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_amd64.s:396" + ] + }, + { + "lwp": 2685, + "frames": [ + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_amd64.s:558", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:75", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:48", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1888", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:3279", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:4017", + "runtime.goschedImpl+0 in /usr/local/go/src/runtime/proc.go:4176", + "runtime.gopreempt_m+0 in /usr/local/go/src/runtime/proc.go:4193", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_amd64.s:463" + ] + }, + { + "lwp": 2686, + "frames": [ + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_amd64.s:558", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:75", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:48", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1888", + "runtime.exitsyscall0+0 in /usr/local/go/src/runtime/proc.go:4875", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_amd64.s:463" + ] + } + ], + "modules": [ + { + "ref": "cfe34afe8ed0115e552d0e94cf7bd46186deb8c3775b310204f194a0b5f67cd5", + "local-path": "/home/ec2-user/testsources/go/hello" + } + ] +} diff --git a/tools/coredump/testdata/amd64/musl-signalframe.json b/tools/coredump/testdata/amd64/musl-signalframe.json index a0a52e6d6..0b293cead 100644 --- a/tools/coredump/testdata/amd64/musl-signalframe.json +++ b/tools/coredump/testdata/amd64/musl-signalframe.json @@ -18,8 +18,7 @@ "ld-musl-x86_64.so.1+0x5846a", "sig+0x11f8", "ld-musl-x86_64.so.1+0x1b87f", - "sig+0x1065", - "" + "sig+0x1065" ] } ], diff --git a/tools/coredump/testdata/amd64/pyenv_3.12.9_gcc_13.3.0.json b/tools/coredump/testdata/amd64/pyenv_3.12.9_gcc_13.3.0.json new file mode 100644 index 000000000..7d7f152fd --- /dev/null +++ b/tools/coredump/testdata/amd64/pyenv_3.12.9_gcc_13.3.0.json @@ -0,0 +1,66 @@ +{ + "coredump-ref": "b21ceabeabae6be900f44f56bb52dd89065c71903603938135fb19b128575c6b", + "threads": [ + { + "lwp": 144534, + "frames": [ + "fib+1 in /home/korniltsev/trash/fib.py:2", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "+6 in /home/korniltsev/trash/fib.py:7", + "+0 in :1", + "libpython3.12.so.1.0+0x11243a", + "libpython3.12.so.1.0+0x28e7de", + "libpython3.12.so.1.0+0x2e9dc5", + "libpython3.12.so.1.0+0x2e9ed8", + "libpython3.12.so.1.0+0x2ecdbf", + "libpython3.12.so.1.0+0x2ed39f", + "libpython3.12.so.1.0+0x314d53", + "libpython3.12.so.1.0+0x315199", + "libpython3.12.so.1.0+0x31535d", + "libc.so.6+0x2a1c9", + "libc.so.6+0x2a28a", + "python3.12+0x1094" + ] + } + ], + "modules": [ + { + "ref": "474c778ae8a8baf4d26717c9e1011846268d7f0a3767f73b30a31d124a65d169", + "local-path": "/home/korniltsev/.pyenv/versions/3.12.9/bin/python3.12" + }, + { + "ref": "1a2eb220c22ae7ba8aaf8b243e57dbc25542f8c9c269ed6100c7ad5aea7c3ada", + "local-path": "/home/korniltsev/.pyenv/versions/3.12.9/lib/libpython3.12.so.1.0" + }, + { + "ref": "e7a914a33fd4f6d25057b8d48c7c5f3d55ab870ec4ee27693d6c5f3a532e6226", + "local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6" + }, + { + "ref": "c09178edd7fbc58aa8415f4bbe54dd76c5ff6c6398ba3e56e5a4743fd7e9adfc", + "local-path": "/usr/lib/x86_64-linux-gnu/libm.so.6" + }, + { + "ref": "6c5e1b4528b704dc7081aa45b5037bda4ea9cad78ca562b4fb6b0dbdbfc7e7e7", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + } + ] +} diff --git a/tools/coredump/testdata/amd64/pyenv_3.13.2_clang_21.0.0.json b/tools/coredump/testdata/amd64/pyenv_3.13.2_clang_21.0.0.json new file mode 100644 index 000000000..c7bb497be --- /dev/null +++ b/tools/coredump/testdata/amd64/pyenv_3.13.2_clang_21.0.0.json @@ -0,0 +1,62 @@ +{ + "coredump-ref": "dc8dd740e0456edc70077e8e453facfae0775a8bf50387f05e9f4e948c3ae700", + "threads": [ + { + "lwp": 167127, + "frames": [ + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "fib+3 in /home/korniltsev/trash/fib.py:4", + "+6 in /home/korniltsev/trash/fib.py:7", + "ffffffffffffffffffffffffffffffff+0x0", + "libpython3.13.so.1.0+0x1f9740", + "libpython3.13.so.1.0+0x1f450c", + "libpython3.13.so.1.0+0x260fdc", + "libpython3.13.so.1.0+0x260cd7", + "libpython3.13.so.1.0+0x25e751", + "libpython3.13.so.1.0+0x25e262", + "libpython3.13.so.1.0+0x283a63", + "libpython3.13.so.1.0+0x2832a2", + "libpython3.13.so.1.0+0x283589", + "libpython3.13.so.1.0+0x2835dc", + "libc.so.6+0x2a1c9", + "libc.so.6+0x2a28a", + "python3.13+0x1074" + ] + } + ], + "modules": [ + { + "ref": "b76cc07b46f4a2f32a16f3a4df617353d454f7890ebd92f49a96f8f7410613f4", + "local-path": "/home/korniltsev/.pyenv/versions/3.13.2/bin/python3.13" + }, + { + "ref": "e7a914a33fd4f6d25057b8d48c7c5f3d55ab870ec4ee27693d6c5f3a532e6226", + "local-path": "/usr/lib/x86_64-linux-gnu/libc.so.6" + }, + { + "ref": "c09178edd7fbc58aa8415f4bbe54dd76c5ff6c6398ba3e56e5a4743fd7e9adfc", + "local-path": "/usr/lib/x86_64-linux-gnu/libm.so.6" + }, + { + "ref": "67997ac257675599247dc0445f4d2705f67e203678fb9920162bc2cd7f9d0009", + "local-path": "/home/korniltsev/.pyenv/versions/3.13.2/lib/libpython3.13.so.1.0" + }, + { + "ref": "6c5e1b4528b704dc7081aa45b5037bda4ea9cad78ca562b4fb6b0dbdbfc7e7e7", + "local-path": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" + } + ] +} diff --git a/tools/coredump/testdata/amd64/python311-expat.json b/tools/coredump/testdata/amd64/python311-expat.json index dbd9cafdd..5e7b651d3 100644 --- a/tools/coredump/testdata/amd64/python311-expat.json +++ b/tools/coredump/testdata/amd64/python311-expat.json @@ -30,8 +30,7 @@ "libpython3.11.so.1.0+0x270c06", "libpython3.11.so.1.0+0x235b06", "ld-musl-x86_64.so.1+0x1c9c9", - "python3.11+0x1075", - "" + "python3.11+0x1075" ] } ], diff --git a/tools/coredump/testdata/amd64/python312-alpine320-nobuildid.json b/tools/coredump/testdata/amd64/python312-alpine320-nobuildid.json new file mode 100644 index 000000000..7b04179e0 --- /dev/null +++ b/tools/coredump/testdata/amd64/python312-alpine320-nobuildid.json @@ -0,0 +1,86 @@ +{ + "coredump-ref": "3eb6bae4e0089983f436d6bbd4a0b7ee0d72738eac29f15495494f53bc82263d", + "threads": [ + { + "lwp": 80, + "frames": [ + "fib+1 in /mnt/trash/qwe.py:2", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "fib+3 in /mnt/trash/qwe.py:4", + "+6 in /mnt/trash/qwe.py:7", + "+0 in :1", + "libpython3.12.so.1.0+0x95b58", + "libpython3.12.so.1.0+0x1ecfe2", + "libpython3.12.so.1.0+0x2136b6", + "libpython3.12.so.1.0+0x20c91b", + "libpython3.12.so.1.0+0x226f72", + "libpython3.12.so.1.0+0x2264f1", + "libpython3.12.so.1.0+0x2260d3", + "libpython3.12.so.1.0+0x21f742", + "libpython3.12.so.1.0+0x1d6ed6", + "ld-musl-x86_64.so.1+0x1c709", + "python3.12+0x1045" + ] + } + ], + "modules": [ + { + "ref": "983f4cac5caf833fbf7d5d28ac8d6a55d3cbab6152d37a246af1a9991f72d8b1", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + }, + { + "ref": "497dd0d2b4a80bfd11339306c84aa752d811f612a398cb526a0a9ac2f426c0b8", + "local-path": "/usr/lib/libpython3.12.so.1.0" + }, + { + "ref": "02a162bd137903ef2511ccb6edc32eaa63a9c9c66c8474f6ce7d9d47fc5a1e71", + "local-path": "/usr/lib/debug/usr/lib/libpython3.12.so.1.0.debug" + }, + { + "ref": "9cf22fb673cad95247584fd0bc21b95aa37ae152a8bd01022a494e4f7ec3854c", + "local-path": "/usr/bin/python3.12" + }, + { + "ref": "f2c83c6e5aea0e1e42b69e344a2eb0aac35b361cad7497157f727dbe30b8565e", + "local-path": "/usr/lib/debug/usr/bin/python3.12.debug" + }, + { + "ref": "c9cbfe6a266c104f74629a257e2017020cba7a0da97caefa4c1d068ea6fe698c", + "local-path": "/lib/ld-musl-x86_64.so.1" + } + ] +} diff --git a/tools/coredump/testdata/amd64/python312-alpine320.json b/tools/coredump/testdata/amd64/python312-alpine320.json new file mode 100644 index 000000000..8986ceac1 --- /dev/null +++ b/tools/coredump/testdata/amd64/python312-alpine320.json @@ -0,0 +1,84 @@ +{ + "coredump-ref": "4652115623df00987a1a480431a360edbd67b0795e9529543d40be190e37c74d", + "threads": [ + { + "lwp": 10, + "frames": [ + "libpython3.12.so.1.0+0x19b80d", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "fib+3 in //mnt/trash/qwe.py:4", + "+6 in //mnt/trash/qwe.py:7", + "+0 in :1", + "libpython3.12.so.1.0+0x91617", + "libpython3.12.so.1.0+0x1ecfe2", + "libpython3.12.so.1.0+0x2136b6", + "libpython3.12.so.1.0+0x20c91b", + "libpython3.12.so.1.0+0x226f72", + "libpython3.12.so.1.0+0x2264f1", + "libpython3.12.so.1.0+0x2260d3", + "libpython3.12.so.1.0+0x21f742", + "libpython3.12.so.1.0+0x1d6ed6", + "ld-musl-x86_64.so.1+0x1c709", + "python3.12+0x1045" + ] + } + ], + "modules": [ + { + "ref": "a621ea443bba20ffbdb5322633c5a1bf439905baa5822973b2cafc4106c64789", + "local-path": "/usr/lib/libpython3.12.so.1.0" + }, + { + "ref": "9ef79f767e2c2cfde043a023acb0021f6261f560b27035c961d282cecc026db9", + "local-path": "/usr/lib/debug/usr/lib/libpython3.12.so.1.0.debug" + }, + { + "ref": "c9cbfe6a266c104f74629a257e2017020cba7a0da97caefa4c1d068ea6fe698c", + "local-path": "/lib/ld-musl-x86_64.so.1" + }, + { + "ref": "983f4cac5caf833fbf7d5d28ac8d6a55d3cbab6152d37a246af1a9991f72d8b1", + "local-path": "/usr/lib/debug/lib/ld-musl-x86_64.so.1.debug" + }, + { + "ref": "9cf22fb673cad95247584fd0bc21b95aa37ae152a8bd01022a494e4f7ec3854c", + "local-path": "/usr/bin/python3.12" + }, + { + "ref": "f2c83c6e5aea0e1e42b69e344a2eb0aac35b361cad7497157f727dbe30b8565e", + "local-path": "/usr/lib/debug/usr/bin/python3.12.debug" + } + ] +} diff --git a/tools/coredump/testdata/arm64/go.symbhack.readheader.json b/tools/coredump/testdata/arm64/go.symbhack.readheader.json index e6c0575db..d16b102ee 100644 --- a/tools/coredump/testdata/arm64/go.symbhack.readheader.json +++ b/tools/coredump/testdata/arm64/go.symbhack.readheader.json @@ -4,148 +4,148 @@ { "lwp": 243879, "frames": [ - "symbhack+0x575ee4", - "symbhack+0x5708b3", - "symbhack+0x570343", - "symbhack+0x825ed7", - "symbhack+0x826507", - "symbhack+0x822fc7", - "symbhack+0xf8fb27", - "symbhack+0xf8f2eb", - "symbhack+0xf8e6e3", - "symbhack+0x447677", - "symbhack+0x477ac3" + "debug/dwarf.(*unit).addrsize+0 in /usr/local/go/src/debug/dwarf/unit.go:37", + "debug/dwarf.(*LineReader).readHeader+0 in /usr/local/go/src/debug/dwarf/line.go:209", + "debug/dwarf.(*Data).LineReader+0 in /usr/local/go/src/debug/dwarf/line.go:174", + "github.com/optimyze/prodfiler/libpf/dwarfextract.(*SymbolResolver).loadLineTable+0 in /media/psf/devel/prodfiler/libpf/dwarfextract/symbols.go:297", + "github.com/optimyze/prodfiler/libpf/dwarfextract.(*SymbolResolver).loadCompilationUnit+0 in /media/psf/devel/prodfiler/libpf/dwarfextract/symbols.go:359", + "github.com/optimyze/prodfiler/libpf/dwarfextract.(*SymbolResolver).ElasticDump+0 in /media/psf/devel/prodfiler/libpf/dwarfextract/elastic.go:50", + "main.handleRegularExecutable+0 in /media/psf/devel/prodfiler/utils/symbhack/main.go:168", + "main.tryMain+0 in /media/psf/devel/prodfiler/utils/symbhack/main.go:123", + "main.main+0 in /media/psf/devel/prodfiler/utils/symbhack/main.go:43", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 243876, "frames": [ - "symbhack+0x478fcc", - "symbhack+0x44132b", - "symbhack+0x416daf", - "symbhack+0x44a1df", - "symbhack+0x44b7df", - "symbhack+0x44c31b", - "symbhack+0x44daaf", - "symbhack+0x44dfcb", - "symbhack+0x475453" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.mPark+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:2240", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2561", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 243878, "frames": [ - "symbhack+0x478914", - "symbhack+0x451f07", - "symbhack+0x44a0e7", - "symbhack+0x44a03b", - "symbhack+0x4753df" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:142", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1385", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 243880, "frames": [ - "symbhack+0x47915c", - "symbhack+0x4410c7", - "symbhack+0x44c9d7", - "symbhack+0x44daaf", - "symbhack+0x44dfcb", - "symbhack+0x475453" + "runtime.epollwait+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:801", + "runtime.netpoll+0 in /usr/local/go/src/runtime/netpoll_epoll.go:126", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2829", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 243881, "frames": [ - "symbhack+0x478fcc", - "symbhack+0x44132b", - "symbhack+0x416daf", - "symbhack+0x44a1df", - "symbhack+0x44b7df", - "symbhack+0x44c31b", - "symbhack+0x44daaf", - "symbhack+0x44dfcb", - "symbhack+0x475453" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.mPark+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:2240", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2561", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 243882, "frames": [ - "symbhack+0x478fcc", - "symbhack+0x44132b", - "symbhack+0x416daf", - "symbhack+0x44b6e7", - "symbhack+0x44a0e7", - "symbhack+0x44a03b", - "symbhack+0x4753df" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.templateThread+0 in /usr/local/go/src/runtime/proc.go:2201", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1385", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 243883, "frames": [ - "symbhack+0x478fcc", - "symbhack+0x44132b", - "symbhack+0x416daf", - "symbhack+0x44a1df", - "symbhack+0x44b7df", - "symbhack+0x44c31b", - "symbhack+0x44daaf", - "symbhack+0x44dfcb", - "symbhack+0x475453" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.mPark+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:2240", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2561", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 243884, "frames": [ - "symbhack+0x478fcc", - "symbhack+0x44132b", - "symbhack+0x416daf", - "symbhack+0x44a1df", - "symbhack+0x44b7df", - "symbhack+0x44c31b", - "symbhack+0x44daaf", - "symbhack+0x44dfcb", - "symbhack+0x475453" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.mPark+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:2240", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2561", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 243885, "frames": [ - "symbhack+0x478fcc", - "symbhack+0x44132b", - "symbhack+0x416daf", - "symbhack+0x44a1df", - "symbhack+0x44b7df", - "symbhack+0x44c31b", - "symbhack+0x44daaf", - "symbhack+0x44dfcb", - "symbhack+0x475453" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.mPark+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:2240", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2561", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 243886, "frames": [ - "symbhack+0x478fcc", - "symbhack+0x44132b", - "symbhack+0x416daf", - "symbhack+0x44a1df", - "symbhack+0x44b7df", - "symbhack+0x44c31b", - "symbhack+0x44daaf", - "symbhack+0x44dfcb", - "symbhack+0x475453" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.mPark+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:2240", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2561", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 243887, "frames": [ - "symbhack+0x478fcc", - "symbhack+0x44132b", - "symbhack+0x416daf", - "symbhack+0x44a1df", - "symbhack+0x44b7df", - "symbhack+0x44c31b", - "symbhack+0x44daaf", - "symbhack+0x44dfcb", - "symbhack+0x475453" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.mPark+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:2240", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2561", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json b/tools/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json index 0111f5f66..661ed86e0 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello3.body.stp-after-bl.json @@ -4,62 +4,62 @@ { "lwp": 3662, "frames": [ - "hello.3345+0x90c94", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 3664, "frames": [ - "hello.3345+0x6d324", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:142", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 3665, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3666, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3667, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello4.epi.add.json b/tools/coredump/testdata/arm64/hello.3345.hello4.epi.add.json index 81601ec0c..dfeba8651 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello4.epi.add.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello4.epi.add.json @@ -4,63 +4,63 @@ { "lwp": 3662, "frames": [ - "hello.3345+0x90d90", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:37", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 3664, "frames": [ - "hello.3345+0x6d324", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:142", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 3665, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3666, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3667, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json b/tools/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json index 7709c001a..9c1203c33 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello4.epi.ret.json @@ -4,63 +4,63 @@ { "lwp": 3662, "frames": [ - "hello.3345+0x90d94", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:37", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 3664, "frames": [ - "hello.3345+0x6d324", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:142", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 3665, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3666, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3667, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json b/tools/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json index 962400cb0..0945107e6 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello5.body.adrp.json @@ -4,64 +4,64 @@ { "lwp": 3662, "frames": [ - "hello.3345+0x90dcc", - "hello.3345+0x90d4b", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello5+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:41", + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:36", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 3664, "frames": [ - "hello.3345+0x6d328", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:143", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 3665, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3666, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3667, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello5.epi.add.json b/tools/coredump/testdata/arm64/hello.3345.hello5.epi.add.json index dbbe9c1e5..b8cea9b76 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello5.epi.add.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello5.epi.add.json @@ -4,77 +4,77 @@ { "lwp": 24601, "frames": [ - "hello.3345+0x90e10", - "hello.3345+0x90d4b", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello5+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:43", + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:36", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 24603, "frames": [ - "hello.3345+0x6d324", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:142", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 24604, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 24606, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 24605, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 24607, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json b/tools/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json index 5ab865957..edc772e01 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello5.epi.ret.json @@ -4,77 +4,77 @@ { "lwp": 24601, "frames": [ - "hello.3345+0x90e14", - "hello.3345+0x90d4b", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello5+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:43", + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:36", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 24603, "frames": [ - "hello.3345+0x6d324", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:142", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 24604, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 24606, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 24605, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 24607, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello5.pro.add.json b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.add.json index b36c968c0..53223e9c3 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello5.pro.add.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.add.json @@ -4,64 +4,64 @@ { "lwp": 155895, "frames": [ - "hello.3345+0x90dd0", - "hello.3345+0x90d4b", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello5+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:41", + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:36", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 155900, "frames": [ - "hello.3345+0x6d328", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:143", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 155901, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 155902, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 155903, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json index c633739ae..33c02434b 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.stp.json @@ -4,64 +4,64 @@ { "lwp": 3662, "frames": [ - "hello.3345+0x90dc8", - "hello.3345+0x90d4b", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello5+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:41", + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:36", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 3664, "frames": [ - "hello.3345+0x6d328", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:143", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 3665, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3666, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3667, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello5.pro.str.json b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.str.json index 40d21f17f..7648b4215 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello5.pro.str.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.str.json @@ -4,64 +4,64 @@ { "lwp": 3662, "frames": [ - "hello.3345+0x90dbc", - "hello.3345+0x90d4b", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello5+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:40", + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:36", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 3664, "frames": [ - "hello.3345+0x6d328", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:143", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 3665, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3666, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3667, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json index 81d1c3ce6..1722102e3 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.stur.json @@ -4,64 +4,64 @@ { "lwp": 3662, "frames": [ - "hello.3345+0x90dc0", - "hello.3345+0x90d4b", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello5+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:40", + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:36", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 3664, "frames": [ - "hello.3345+0x6d324", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:142", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 3665, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3666, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3667, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json index ef78f878d..87cf85b6b 100644 --- a/tools/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json +++ b/tools/coredump/testdata/arm64/hello.3345.hello5.pro.sub.json @@ -4,64 +4,64 @@ { "lwp": 3662, "frames": [ - "hello.3345+0x90dc4", - "hello.3345+0x90d4b", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.hello5+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:40", + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:36", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 3664, "frames": [ - "hello.3345+0x6d328", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:143", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 3665, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3666, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 3667, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.3345.leaf.ret.json b/tools/coredump/testdata/arm64/hello.3345.leaf.ret.json index ac87a7cd4..d5ad0da3b 100644 --- a/tools/coredump/testdata/arm64/hello.3345.leaf.ret.json +++ b/tools/coredump/testdata/arm64/hello.3345.leaf.ret.json @@ -4,65 +4,65 @@ { "lwp": 156629, "frames": [ - "hello.3345+0x90a90", - "hello.3345+0x90e0b", - "hello.3345+0x90d4b", - "hello.3345+0x90c93", - "hello.3345+0x90c43", - "hello.3345+0x90b3b", - "hello.3345+0x90afb", - "hello.3345+0x90e4b", - "hello.3345+0x43533", - "hello.3345+0x6c513" + "main.leaf+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:6", + "main.hello5+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:43", + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:36", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 156634, "frames": [ - "hello.3345+0x6d324", - "hello.3345+0x4eec7", - "hello.3345+0x46237", - "hello.3345+0x46187", - "hello.3345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:142", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 156635, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x485db", - "hello.3345+0x4a363", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.startlockedm+0 in /usr/local/go/src/runtime/proc.go:2471", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3241", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 156636, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 156637, "frames": [ - "hello.3345+0x6d9bc", - "hello.3345+0x3d6fb", - "hello.3345+0x1993f", - "hello.3345+0x47b13", - "hello.3345+0x4938b", - "hello.3345+0x4a3a7", - "hello.3345+0x4a8ff", - "hello.3345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/hello.345.hello5.body.add.json b/tools/coredump/testdata/arm64/hello.345.hello5.body.add.json index 2aecd5863..780bb1eb5 100644 --- a/tools/coredump/testdata/arm64/hello.345.hello5.body.add.json +++ b/tools/coredump/testdata/arm64/hello.345.hello5.body.add.json @@ -4,51 +4,51 @@ { "lwp": 155034, "frames": [ - "hello.345+0x90db0", - "hello.345+0x90d2b", - "hello.345+0x90c73", - "hello.345+0x90c2b", - "hello.345+0x90b3b", - "hello.345+0x90afb", - "hello.345+0x90e2b", - "hello.345+0x43533", - "hello.345+0x6c513" + "main.hello5+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:41", + "main.hello4+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:36", + "main.hello3+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:30", + "main.hello2+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:25", + "main.hello1+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:17", + "main.hello+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:12", + "main.main+0 in /media/psf/devel/prodfiler/utils/coredump/testsources/go/hello.go:47", + "runtime.main+0 in /usr/local/go/src/runtime/proc.go:259", + "runtime.goexit+0 in /usr/local/go/src/runtime/asm_arm64.s:1166" ] }, { "lwp": 155058, "frames": [ - "hello.345+0x6d324", - "hello.345+0x4eec7", - "hello.345+0x46237", - "hello.345+0x46187", - "hello.345+0x69f5f" + "runtime.usleep+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:142", + "runtime.sysmon+0 in /usr/local/go/src/runtime/proc.go:5162", + "runtime.mstart1+0 in /usr/local/go/src/runtime/proc.go:1428", + "runtime.mstart0+0 in /usr/local/go/src/runtime/proc.go:1359", + "runtime.mstart+0 in /usr/local/go/src/runtime/asm_arm64.s:129" ] }, { "lwp": 155059, "frames": [ - "hello.345+0x6d9bc", - "hello.345+0x3d6fb", - "hello.345+0x1993f", - "hello.345+0x47b13", - "hello.345+0x4938b", - "hello.345+0x4a3a7", - "hello.345+0x4a8ff", - "hello.345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] }, { "lwp": 155060, "frames": [ - "hello.345+0x6d9bc", - "hello.345+0x3d6fb", - "hello.345+0x1993f", - "hello.345+0x47b13", - "hello.345+0x4938b", - "hello.345+0x4a3a7", - "hello.345+0x4a8ff", - "hello.345+0x69fd3" + "runtime.futex+0 in /usr/local/go/src/runtime/sys_linux_arm64.s:666", + "runtime.futexsleep+0 in /usr/local/go/src/runtime/os_linux.go:70", + "runtime.notesleep+0 in /usr/local/go/src/runtime/lock_futex.go:161", + "runtime.stopm+0 in /usr/local/go/src/runtime/proc.go:1458", + "runtime.findRunnable+0 in /usr/local/go/src/runtime/proc.go:2867", + "runtime.schedule+0 in /usr/local/go/src/runtime/proc.go:3206", + "runtime.park_m+0 in /usr/local/go/src/runtime/proc.go:3356", + "runtime.mcall+0 in /usr/local/go/src/runtime/asm_arm64.s:193" ] } ], diff --git a/tools/coredump/testdata/arm64/node1600-inlining.json b/tools/coredump/testdata/arm64/node1600-inlining.json index 85be05ceb..716410f25 100644 --- a/tools/coredump/testdata/arm64/node1600-inlining.json +++ b/tools/coredump/testdata/arm64/node1600-inlining.json @@ -61,8 +61,7 @@ "node+0xad25bf", "libc.so.6+0x273fb", "libc.so.6+0x274cb", - "node+0xa583e3", - "" + "node+0xa583e3" ] }, { diff --git a/tools/coredump/testdata/arm64/node16110-inlining.json b/tools/coredump/testdata/arm64/node16110-inlining.json index aca0e84f8..50f7fc6ed 100644 --- a/tools/coredump/testdata/arm64/node16110-inlining.json +++ b/tools/coredump/testdata/arm64/node16110-inlining.json @@ -59,8 +59,7 @@ "node+0xabc237", "libc.so.6+0x273fb", "libc.so.6+0x274cb", - "node+0xa41c3b", - "" + "node+0xa41c3b" ] }, { diff --git a/tools/coredump/testdata/arm64/node1640-inlining.json b/tools/coredump/testdata/arm64/node1640-inlining.json index bf8bec3b9..c07ba7fa0 100644 --- a/tools/coredump/testdata/arm64/node1640-inlining.json +++ b/tools/coredump/testdata/arm64/node1640-inlining.json @@ -61,8 +61,7 @@ "node+0xae0fe7", "libc.so.6+0x273fb", "libc.so.6+0x274cb", - "node+0xa66af3", - "" + "node+0xa66af3" ] }, { diff --git a/tools/coredump/testdata/arm64/node1660-inlining.json b/tools/coredump/testdata/arm64/node1660-inlining.json index 035fee117..edbe6b96a 100644 --- a/tools/coredump/testdata/arm64/node1660-inlining.json +++ b/tools/coredump/testdata/arm64/node1660-inlining.json @@ -61,8 +61,7 @@ "node+0xabbf67", "libc.so.6+0x273fb", "libc.so.6+0x274cb", - "node+0xa41cb3", - "" + "node+0xa41cb3" ] }, { diff --git a/tools/gooffsets/info.go b/tools/gooffsets/info.go new file mode 100644 index 000000000..f2611124c --- /dev/null +++ b/tools/gooffsets/info.go @@ -0,0 +1,116 @@ +package main + +import ( + "debug/dwarf" + "errors" + "fmt" +) + +func ReadEntry(reader *dwarf.Reader, name string, expectedTag dwarf.Tag) (*dwarf.Entry, error) { + reader.Seek(0) + for { + e, err := reader.Next() + if e == nil { + return nil, err + } + if err != nil { + return nil, err + } + if !e.Children { + continue + } + for { + e, err := reader.Next() + if e == nil { + return nil, err + } + if err != nil { + return nil, err + } + if e.Tag == 0 { + break + } + for _, f := range e.Field { + if e.Tag != expectedTag { + continue + } + if f.Attr == dwarf.AttrName && f.Val == name { + return e, nil + } + } + reader.SkipChildren() + } + } +} + +func ReadChild(reader *dwarf.Reader, name string) (*dwarf.Entry, error) { + for { + e, err := reader.Next() + if err != nil { + return nil, err + } + if e == nil || e.Tag == 0 { + return nil, fmt.Errorf("field %s not found", name) + } + for _, f := range e.Field { + if f.Attr == dwarf.AttrName && f.Val == name { + return e, nil + } + } + reader.SkipChildren() + } +} + +func ReadField(e *dwarf.Entry, key dwarf.Attr) any { + for _, f := range e.Field { + if f.Attr == key { + return f.Val + } + } + return nil +} + +func readType(r *dwarf.Reader, e *dwarf.Entry, + seen map[dwarf.Offset]struct{}) (*dwarf.Entry, error) { + offset := e.Offset + if _, found := seen[offset]; found { + return nil, fmt.Errorf("infinite loop detected at %d", offset) + } + seen[offset] = struct{}{} + t, ok := ReadField(e, dwarf.AttrType).(dwarf.Offset) + if !ok { + return nil, errors.New("type not found") + } + r.Seek(t) + candidate, err := r.Next() + if err != nil { + return nil, err + } + if candidate.Tag == dwarf.TagTypedef { + return readType(r, candidate, seen) + } + return candidate, nil +} + +func ReadType(r *dwarf.Reader, e *dwarf.Entry) (*dwarf.Entry, error) { + return readType(r, e, make(map[dwarf.Offset]struct{})) +} + +func ReadChildTypeAndOffset(r *dwarf.Reader, name string) (*dwarf.Entry, int64, error) { + child, err := ReadChild(r, name) + if err != nil { + return nil, 0, err + } + + offset, ok := ReadField(child, dwarf.AttrDataMemberLoc).(int64) + if !ok { + return nil, 0, fmt.Errorf("offset not found for field %s", name) + } + + typ, err := ReadType(r, child) + if err != nil { + return nil, 0, err + } + + return typ, offset, nil +} diff --git a/tools/gooffsets/main.go b/tools/gooffsets/main.go new file mode 100644 index 000000000..a327e5c96 --- /dev/null +++ b/tools/gooffsets/main.go @@ -0,0 +1,167 @@ +package main + +import ( + "debug/buildinfo" + "debug/dwarf" + "debug/elf" + "errors" + "fmt" + "os" + "strings" + + "golang.org/x/mod/semver" +) + +type goLabelsOffsets struct { + mOffset uint32 + curg uint32 + labels uint32 + hmapCount uint32 + hmapLog2BucketCount uint32 + hmapBuckets uint32 +} + +func getOffsets(f *elf.File, version string) (*goLabelsOffsets, error) { + d, err := f.DWARF() + if err != nil { + return nil, err + } + + r := d.Reader() + g, err := ReadEntry(r, "runtime.g", dwarf.TagStructType) + if err != nil { + return nil, err + } + if g == nil { + return nil, errors.New("type runtime.g not found") + } + mPType, mOffset, err := ReadChildTypeAndOffset(r, "m") + if err != nil { + return nil, err + } + if mPType.Tag != dwarf.TagPointerType { + return nil, errors.New("type of m in runtime.g is not a pointer") + } + + mType, err := ReadType(r, mPType) + if err != nil { + return nil, err + } + + r.Seek(mType.Offset) + _, err = r.Next() + if err != nil { + return nil, err + } + + r.Seek(mType.Offset) + _, err = r.Next() + if err != nil { + return nil, err + } + curgPType, curgOffset, err := ReadChildTypeAndOffset(r, "curg") + if err != nil { + return nil, err + } + if curgPType.Tag != dwarf.TagPointerType { + return nil, errors.New("type of curg in m is not a pointer") + } + _, err = ReadType(r, curgPType) + if err != nil { + return nil, err + } + + _, labelsOffset, err := ReadChildTypeAndOffset(r, "labels") + if err != nil { + return nil, err + } + + hmap, err := ReadEntry(r, "runtime.hmap", dwarf.TagStructType) + if err != nil { + return nil, err + } + + if semver.Compare(version, "v1.24.0") >= 0 { + return &goLabelsOffsets{ + mOffset: uint32(mOffset), + curg: uint32(curgOffset), + labels: uint32(labelsOffset), + }, nil + } + + _, countOffset, err := ReadChildTypeAndOffset(r, "count") + if err != nil { + return nil, err + } + r.Seek(hmap.Offset) + _, err = r.Next() + if err != nil { + return nil, err + } + _, bOffset, err := ReadChildTypeAndOffset(r, "B") + if err != nil { + return nil, err + } + r.Seek(hmap.Offset) + _, err = r.Next() + if err != nil { + return nil, err + } + _, bucketsOffset, err := ReadChildTypeAndOffset(r, "buckets") + if err != nil { + return nil, err + } + + return &goLabelsOffsets{ + mOffset: uint32(mOffset), + curg: uint32(curgOffset), + labels: uint32(labelsOffset), + hmapCount: uint32(countOffset), + hmapLog2BucketCount: uint32(bOffset), + hmapBuckets: uint32(bucketsOffset), + }, nil +} + +func open(path string) (*elf.File, string, error) { + f, err := os.Open(path) + if err != nil { + return nil, "", err + } + ef, err := elf.NewFile(f) + if err != nil { + _ = ef.Close() + return nil, "", err + } + bi, err := buildinfo.Read(f) + if err != nil { + return nil, "", err + } + return ef, bi.GoVersion, nil +} + +func convertToSemver(goVersion string) string { + version := strings.TrimPrefix(goVersion, "go") + + return "v" + version +} + +func main() { + f, version, err := open(os.Args[1]) + if err != nil { + panic(err) + } + defer f.Close() + offs, err := getOffsets(f, convertToSemver(version)) + if err != nil { + panic(err) + } + fmt.Printf(`"%s": { +`, version) + fmt.Printf("\tm_offset: %d,\n", offs.mOffset) + fmt.Printf("\tcurg: %d,\n", offs.curg) + fmt.Printf("\tlabels: %d,\n", offs.labels) + fmt.Printf("\thmap_count: %d,\n", offs.hmapCount) + fmt.Printf("\thmap_log2_bucket_count: %d,\n", offs.hmapLog2BucketCount) + fmt.Printf("\thmap_buckets: %d,\n", offs.hmapBuckets) + fmt.Println("},") +} diff --git a/tools/stackdeltas/stackdeltas.go b/tools/stackdeltas/stackdeltas.go index d9c16b44d..c3863fba2 100644 --- a/tools/stackdeltas/stackdeltas.go +++ b/tools/stackdeltas/stackdeltas.go @@ -33,30 +33,32 @@ type stats struct { func getOpcode(opcode uint8, param int32) string { str := "" - switch opcode &^ sdtypes.UnwindOpcodeFlagDeref { - case sdtypes.UnwindOpcodeCommand: + switch opcode &^ support.UnwindOpcodeFlagDeref { + case support.UnwindOpcodeCommand: switch param { - case sdtypes.UnwindCommandInvalid: + case support.UnwindCommandInvalid: return "invalid" - case sdtypes.UnwindCommandStop: + case support.UnwindCommandStop: return "stop" - case sdtypes.UnwindCommandPLT: + case support.UnwindCommandPLT: return "plt" - case sdtypes.UnwindCommandSignal: + case support.UnwindCommandSignal: return "signal" + case support.UnwindCommandFramePointer: + return "framepointer" default: return "?" } - case sdtypes.UnwindOpcodeBaseCFA: + case support.UnwindOpcodeBaseCFA: str = "cfa" - case sdtypes.UnwindOpcodeBaseFP: + case support.UnwindOpcodeBaseFP: str = "fp" - case sdtypes.UnwindOpcodeBaseSP: + case support.UnwindOpcodeBaseSP: str = "sp" default: return "?" } - if opcode&sdtypes.UnwindOpcodeFlagDeref != 0 { + if opcode&support.UnwindOpcodeFlagDeref != 0 { preDeref, postDeref := sdtypes.UnpackDerefParam(param) if postDeref != 0 { str = fmt.Sprintf("*(%s%+x)%+x", str, preDeref, postDeref) diff --git a/tpbase/libc.go b/tpbase/libc.go index b5e3bc5f0..fa057a401 100644 --- a/tpbase/libc.go +++ b/tpbase/libc.go @@ -93,27 +93,21 @@ func IsPotentialTSDDSO(filename string) bool { // ExtractTSDInfo extracts the introspection data for pthread thread specific data. func ExtractTSDInfo(ef *pfelf.File) (*TSDInfo, error) { - sym, err := ef.LookupSymbol("__pthread_getspecific") + _, code, err := ef.SymbolData("__pthread_getspecific", 2048) if err != nil { - sym, err = ef.LookupSymbol("pthread_getspecific") + _, code, err = ef.SymbolData("pthread_getspecific", 2048) if err != nil { - return nil, fmt.Errorf("no getspecific function: %s", err) + return nil, fmt.Errorf("unable to read 'pthread_getspecific': %s", err) } } - if sym.Size < 8 { - return nil, fmt.Errorf("getspecific function size is %d", sym.Size) - } - - code := make([]byte, sym.Size) - if _, err = ef.ReadVirtualMemory(code, int64(sym.Address)); err != nil { - return nil, fmt.Errorf("failed to read getspecific function: %s", err) + if len(code) < 8 { + return nil, fmt.Errorf("getspecific function size is %d", len(code)) } info, err := ExtractTSDInfoNative(code) if err != nil { return nil, fmt.Errorf("failed to extract getspecific data: %s", err) } - return &info, nil } diff --git a/tracehandler/tracehandler.go b/tracehandler/tracehandler.go index 58775c483..e89ae1f54 100644 --- a/tracehandler/tracehandler.go +++ b/tracehandler/tracehandler.go @@ -125,6 +125,7 @@ func (m *traceHandler) HandleTrace(bpfTrace *host.Trace) { CPU: bpfTrace.CPU, ProcessName: bpfTrace.ProcessName, ExecutablePath: bpfTrace.ExecutablePath, + ContainerID: bpfTrace.ContainerID, Origin: bpfTrace.Origin, OffTime: bpfTrace.OffTime, EnvVars: bpfTrace.EnvVars, diff --git a/tracer/ebpf_integration_test.go b/tracer/ebpf_integration_test.go index 124c1679b..809c260f9 100644 --- a/tracer/ebpf_integration_test.go +++ b/tracer/ebpf_integration_test.go @@ -3,7 +3,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package tracer +package tracer_test import ( "context" @@ -20,10 +20,10 @@ import ( "go.opentelemetry.io/ebpf-profiler/host" "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/proc" - "go.opentelemetry.io/ebpf-profiler/reporter" "go.opentelemetry.io/ebpf-profiler/rlimit" "go.opentelemetry.io/ebpf-profiler/support" + "go.opentelemetry.io/ebpf-profiler/testutils" + "go.opentelemetry.io/ebpf-profiler/tracer" tracertypes "go.opentelemetry.io/ebpf-profiler/tracer/types" ) @@ -45,11 +45,12 @@ func forceContextSwitch() { // runKernelFrameProbe executes a perf event on the sched/sched_switch tracepoint // that sends a selection of hand-crafted, predictable traces. -func runKernelFrameProbe(t *testing.T, tracer *Tracer) { +func runKernelFrameProbe(t *testing.T, tr *tracer.Tracer) { coll, err := support.LoadCollectionSpec(false) require.NoError(t, err) - err = coll.RewriteMaps(tracer.ebpfMaps) //nolint:staticcheck + //nolint:staticcheck + err = coll.RewriteMaps(tr.GetEbpfMaps()) require.NoError(t, err) restoreRlimit, err := rlimit.MaximizeMemlock() @@ -85,29 +86,6 @@ func validateTrace(t *testing.T, numKernelFrames int, expected, returned *host.T } } -type mockIntervals struct{} - -func (f mockIntervals) MonitorInterval() time.Duration { return 1 * time.Second } -func (f mockIntervals) TracePollInterval() time.Duration { return 250 * time.Millisecond } -func (f mockIntervals) PIDCleanupInterval() time.Duration { return 1 * time.Second } - -type mockReporter struct{} - -func (f mockReporter) ExecutableKnown(_ libpf.FileID) bool { - return true -} - -func (f mockReporter) ExecutableMetadata(_ *reporter.ExecutableMetadataArgs) { -} - -func (f mockReporter) ReportFallbackSymbol(_ libpf.FrameID, _ string) {} - -func (f mockReporter) FrameKnown(_ libpf.FrameID) bool { - return true -} - -func (f mockReporter) FrameMetadata(_ *reporter.FrameMetadataArgs) {} - func generateMaxLengthTrace() host.Trace { var trace host.Trace for i := 0; i < support.MaxFrameUnwinds; i++ { @@ -125,9 +103,9 @@ func TestTraceTransmissionAndParsing(t *testing.T) { enabledTracers, _ := tracertypes.Parse("") enabledTracers.Enable(tracertypes.PythonTracer) - tracer, err := NewTracer(ctx, &Config{ - Reporter: &mockReporter{}, - Intervals: &mockIntervals{}, + tr, err := tracer.NewTracer(ctx, &tracer.Config{ + Reporter: &testutils.MockReporter{}, + Intervals: &testutils.MockIntervals{}, IncludeTracers: enabledTracers, FilterErrorFrames: false, SamplesPerSecond: 20, @@ -137,14 +115,15 @@ func TestTraceTransmissionAndParsing(t *testing.T) { ProbabilisticInterval: 100, ProbabilisticThreshold: 100, OffCPUThreshold: support.OffCPUThresholdMax, + DebugTracer: true, }) require.NoError(t, err) traceChan := make(chan *host.Trace, 16) - err = tracer.StartMapMonitors(ctx, traceChan) + err = tr.StartMapMonitors(ctx, traceChan) require.NoError(t, err) - runKernelFrameProbe(t, tracer) + runKernelFrameProbe(t, tr) traces := make(map[uint8]*host.Trace) timeout := time.NewTimer(1 * time.Second) @@ -253,17 +232,6 @@ Loop: } func TestAllTracers(t *testing.T) { - kernelSymbols, err := proc.GetKallsyms("/proc/kallsyms") - require.NoError(t, err) - - _, _, err = initializeMapsAndPrograms(kernelSymbols, &Config{ - IncludeTracers: tracertypes.AllTracers(), - MapScaleFactor: 1, - FilterErrorFrames: false, - KernelVersionCheck: false, - DebugTracer: false, - BPFVerifierLogLevel: 0, - OffCPUThreshold: 10, - }) - require.NoError(t, err) + _, _ = testutils.StartTracer(context.Background(), t, tracertypes.AllTracers(), + &testutils.MockReporter{}) } diff --git a/tracer/events.go b/tracer/events.go index 36238bbe9..a574d5e8d 100644 --- a/tracer/events.go +++ b/tracer/events.go @@ -47,8 +47,8 @@ func (t *Tracer) processPIDEvents(ctx context.Context) { defer pidCleanupTicker.Stop() for { select { - case pid := <-t.pidEvents: - t.processManager.SynchronizeProcess(process.New(pid)) + case pidTid := <-t.pidEvents: + t.processManager.SynchronizeProcess(process.New(pidTid.PID(), pidTid.TID())) case <-pidCleanupTicker.C: t.processManager.CleanupPIDs() case <-ctx.Done(): diff --git a/tracer/maccess.go b/tracer/maccess.go index efc68c08b..f8968cb99 100644 --- a/tracer/maccess.go +++ b/tracer/maccess.go @@ -6,9 +6,9 @@ package tracer // import "go.opentelemetry.io/ebpf-profiler/tracer" import ( "errors" "fmt" - "runtime" cebpf "github.com/cilium/ebpf" + "go.opentelemetry.io/ebpf-profiler/kallsyms" "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/maccess" ) @@ -16,42 +16,21 @@ import ( // checkForMmaccessPatch validates if a Linux kernel function is patched by // extracting the kernel code of the function and analyzing it. func checkForMaccessPatch(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, - kernelSymbols *libpf.SymbolMap) error { - faultyFunc, err := kernelSymbols.LookupSymbol( - libpf.SymbolName("copy_from_user_nofault")) + kmod *kallsyms.Module) error { + faultyFunc, err := kmod.LookupSymbol("copy_from_user_nofault") if err != nil { - return fmt.Errorf("failed to look up Linux kernel symbol "+ - "'copy_from_user_nofault': %v", err) + return fmt.Errorf("failed to lookup 'copy_from_user_nofault': %v", err) } - - code, err := loadKernelCode(coll, maps, faultyFunc.Address) - if err != nil { - return fmt.Errorf("failed to load kernel code for %s: %v", faultyFunc.Name, err) - } - - newCheckFunc, err := kernelSymbols.LookupSymbol( - libpf.SymbolName("nmi_uaccess_okay")) + code, err := loadKernelCode(coll, maps, libpf.SymbolValue(faultyFunc)) if err != nil { - //nolint:goconst - if runtime.GOARCH == "arm64" { - // On arm64 this symbol might not be available and we do not use - // the symbol address in the arm64 case to check for the patch. - // As there was an error getting the symbol, newCheckFunc is nil. - // To still be able to access newCheckFunc safely, create a dummy element. - newCheckFunc = &libpf.Symbol{ - Address: 0, - } - } else { - // Without the symbol information, we can not continue with checking the - // function and determine whether it got patched. - return fmt.Errorf("failed to look up Linux kernel symbol 'nmi_uaccess_okay': %v", err) - } + return fmt.Errorf("failed to load kernel code for 'copy_from_user_nofault': %v", err) } - patched, err := maccess.CopyFromUserNoFaultIsPatched(code, uint64(faultyFunc.Address), - uint64(newCheckFunc.Address)) + newCheckFunc, _ := kmod.LookupSymbol("nmi_uaccess_okay") + patched, err := maccess.CopyFromUserNoFaultIsPatched(code, + uint64(faultyFunc), uint64(newCheckFunc)) if err != nil { - return fmt.Errorf("failed to check if %s is patched: %v", faultyFunc.Name, err) + return fmt.Errorf("failed to check if 'copy_from_user_nofault' is patched: %v", err) } if !patched { return errors.New("kernel is not patched") diff --git a/tracer/systemconfig.go b/tracer/systemconfig.go index b8dc3ad48..d2d63a2d8 100644 --- a/tracer/systemconfig.go +++ b/tracer/systemconfig.go @@ -12,6 +12,9 @@ import ( "strings" "unsafe" + "go.opentelemetry.io/ebpf-profiler/kallsyms" + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/pacmask" "go.opentelemetry.io/ebpf-profiler/rlimit" "go.opentelemetry.io/ebpf-profiler/tracer/types" @@ -19,9 +22,6 @@ import ( "github.com/cilium/ebpf/btf" "github.com/cilium/ebpf/link" log "github.com/sirupsen/logrus" - - "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/pacmask" ) // #include "../support/ebpf/types.h" @@ -226,7 +226,7 @@ func determineStackLayout(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map } func loadSystemConfig(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, - kernelSymbols *libpf.SymbolMap, includeTracers types.IncludedTracers, + kmod *kallsyms.Module, includeTracers types.IncludedTracers, offCPUThreshold uint32, filterErrorFrames bool) error { pacMask := pacmask.GetPACMask() if pacMask != 0 { @@ -247,9 +247,10 @@ func loadSystemConfig(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, return err } - if includeTracers.Has(types.PerlTracer) || includeTracers.Has(types.PythonTracer) { + if includeTracers.Has(types.PerlTracer) || includeTracers.Has(types.PythonTracer) || + includeTracers.Has(types.Labels) { var tpbaseOffset uint64 - tpbaseOffset, err = loadTPBaseOffset(coll, maps, kernelSymbols) + tpbaseOffset, err = loadTPBaseOffset(coll, maps, kmod) if err != nil { return err } diff --git a/tracer/tpbase.go b/tracer/tpbase.go index f39f2d06b..5a797c74e 100644 --- a/tracer/tpbase.go +++ b/tracer/tpbase.go @@ -11,6 +11,7 @@ import ( cebpf "github.com/cilium/ebpf" log "github.com/sirupsen/logrus" + "go.opentelemetry.io/ebpf-profiler/kallsyms" "go.opentelemetry.io/ebpf-profiler/libpf" "go.opentelemetry.io/ebpf-profiler/tpbase" ) @@ -35,15 +36,15 @@ import ( // kernel struct. This offset varies depending on kernel configuration, so we have to learn // it dynamically at runtime. func loadTPBaseOffset(coll *cebpf.CollectionSpec, maps map[string]*cebpf.Map, - kernelSymbols *libpf.SymbolMap) (uint64, error) { + kmod *kallsyms.Module) (uint64, error) { var tpbaseOffset uint32 for _, analyzer := range tpbase.GetAnalyzers() { - sym, err := kernelSymbols.LookupSymbol(libpf.SymbolName(analyzer.FunctionName)) + sym, err := kmod.LookupSymbol(analyzer.FunctionName) if err != nil { continue } - code, err := loadKernelCode(coll, maps, sym.Address) + code, err := loadKernelCode(coll, maps, libpf.SymbolValue(sym)) if err != nil { return 0, err } diff --git a/tracer/tracepoints.go b/tracer/tracepoints.go index dc812c932..7fbe1d4c8 100644 --- a/tracer/tracepoints.go +++ b/tracer/tracepoints.go @@ -36,6 +36,6 @@ func (t *Tracer) AttachSchedMonitor() error { } defer restoreRlimit() - prog := t.ebpfProgs["tracepoint__sched_process_exit"] - return t.attachToTracepoint("sched", "sched_process_exit", prog) + prog := t.ebpfProgs["tracepoint__sched_process_free"] + return t.attachToTracepoint("sched", "sched_process_free", prog) } diff --git a/tracer/tracer.go b/tracer/tracer.go index e780bd98e..c43ef9634 100644 --- a/tracer/tracer.go +++ b/tracer/tracer.go @@ -9,10 +9,7 @@ import ( "context" "errors" "fmt" - "hash/fnv" - "math" "math/rand/v2" - "sort" "strings" "sync/atomic" "time" @@ -26,13 +23,12 @@ import ( "github.com/zeebo/xxh3" "go.opentelemetry.io/ebpf-profiler/host" + "go.opentelemetry.io/ebpf-profiler/kallsyms" "go.opentelemetry.io/ebpf-profiler/libpf" - "go.opentelemetry.io/ebpf-profiler/libpf/pfelf" "go.opentelemetry.io/ebpf-profiler/libpf/xsync" "go.opentelemetry.io/ebpf-profiler/metrics" "go.opentelemetry.io/ebpf-profiler/nativeunwind/elfunwindinfo" "go.opentelemetry.io/ebpf-profiler/periodiccaller" - "go.opentelemetry.io/ebpf-profiler/proc" pm "go.opentelemetry.io/ebpf-profiler/processmanager" pmebpf "go.opentelemetry.io/ebpf-profiler/processmanager/ebpf" "go.opentelemetry.io/ebpf-profiler/reporter" @@ -41,7 +37,6 @@ import ( "go.opentelemetry.io/ebpf-profiler/times" "go.opentelemetry.io/ebpf-profiler/tracehandler" "go.opentelemetry.io/ebpf-profiler/tracer/types" - "go.opentelemetry.io/ebpf-profiler/util" ) /* @@ -83,11 +78,8 @@ type Tracer struct { // ebpfProgs holds the currently loaded eBPF programs. ebpfProgs map[string]*cebpf.Program - // kernelSymbols is used to hold the kernel symbol addresses we are tracking - kernelSymbols *libpf.SymbolMap - - // kernelModules holds symbols/addresses for the kernel module address space - kernelModules *libpf.SymbolMap + // kernelSymbolizer does kernel fallback symbolization + kernelSymbolizer *kallsyms.Symbolizer // perfEntrypoints holds a list of frequency based perf events that are opened on the system. perfEntrypoints xsync.RWMutex[[]*perf.Event] @@ -105,19 +97,17 @@ type Tracer struct { // when process events take place (new, exit, unknown PC). triggerPIDProcessing chan bool - // pidEvents notifies the tracer of new PID events. + // pidEvents notifies the tracer of new PID events. Each PID event is a 64bit integer + // value, see bpf_get_current_pid_tgid for information on how the value is encoded. // It needs to be buffered to avoid locking the writers and stacking up resources when we // read new PIDs at startup or notified via eBPF. - pidEvents chan libpf.PID + pidEvents chan libpf.PIDTID // intervals provides access to globally configured timers and counters. intervals Intervals hasBatchOperations bool - // moduleFileIDs maps kernel module names to their respective FileID. - moduleFileIDs map[string]libpf.FileID - // reporter allows swapping out the reporter implementation. reporter reporter.SymbolReporter @@ -178,114 +168,20 @@ type progLoaderHelper struct { noTailCallTarget bool } -// processKernelModulesMetadata computes the FileID of kernel files and reports executable metadata -// for all kernel modules and the vmlinux image. -func processKernelModulesMetadata(rep reporter.SymbolReporter, kernelModules *libpf.SymbolMap, - kernelSymbols *libpf.SymbolMap) (map[string]libpf.FileID, error) { - result := make(map[string]libpf.FileID, kernelModules.Len()) - kernelModules.VisitAll(func(moduleSym libpf.Symbol) { - nameStr := string(moduleSym.Name) - if !util.IsValidString(nameStr) { - log.Errorf("Invalid string representation of file name in "+ - "processKernelModulesMetadata: %v", []byte(nameStr)) - return - } - - // Read the kernel and modules ELF notes from sysfs (works since Linux 2.6.24) - notesFile := fmt.Sprintf("/sys/module/%s/notes/.note.gnu.build-id", nameStr) - - // The vmlinux notes section is in a different location - if nameStr == "vmlinux" { - notesFile = "/sys/kernel/notes" - } - - buildID, err := pfelf.GetBuildIDFromNotesFile(notesFile) - var fileID libpf.FileID - // Require at least 16 bytes of BuildID to ensure there is enough entropy for a FileID. - // 16 bytes could happen when --build-id=md5 is passed to `ld`. This would imply a custom - // kernel. - if err == nil && len(buildID) >= 16 { - fileID = libpf.FileIDFromKernelBuildID(buildID) - } else { - fileID = calcFallbackModuleID(moduleSym, kernelSymbols) - buildID = "" - } - - result[nameStr] = fileID - rep.ExecutableMetadata(&reporter.ExecutableMetadataArgs{ - FileID: fileID, - FileName: nameStr, - GnuBuildID: buildID, - DebuglinkFileName: "", - Interp: libpf.Kernel, - }) - }) - - return result, nil -} - -// calcFallbackModuleID computes a fallback file ID for kernel modules that do not -// have a GNU build ID. Getting the actual file for the kernel module isn't always -// possible since they don't necessarily reside on disk, e.g. when modules are loaded -// from the initramfs that is later unmounted again. -// -// This fallback checksum locates all symbols exported by a given driver, normalizes -// them to offsets and hashes over that. Additionally, the module's name and size are -// hashed as well. This isn't perfect, and we can't do any server-side symbolization -// with these IDs, but at least it provides a stable unique key for the kernel fallback -// symbols that we send. -func calcFallbackModuleID(moduleSym libpf.Symbol, kernelSymbols *libpf.SymbolMap) libpf.FileID { - modStart := moduleSym.Address - modEnd := moduleSym.Address + libpf.SymbolValue(moduleSym.Size) - - // Collect symbols belonging to this module + track minimum address. - var moduleSymbols []libpf.Symbol - minAddr := libpf.SymbolValue(math.MaxUint64) - kernelSymbols.VisitAll(func(symbol libpf.Symbol) { - if symbol.Address >= modStart && symbol.Address < modEnd { - moduleSymbols = append(moduleSymbols, symbol) - minAddr = min(minAddr, symbol.Address) - } - }) - - // Ensure consistent order. - sort.Slice(moduleSymbols, func(a, b int) bool { - return moduleSymbols[a].Address < moduleSymbols[b].Address - }) - - // Hash exports and their normalized addresses. - h := fnv.New128a() - h.Write([]byte(moduleSym.Name)) - h.Write(libpf.SliceFrom(&moduleSym.Size)) - - for _, sym := range moduleSymbols { - sym.Address -= minAddr // KASLR normalization - - h.Write([]byte(sym.Name)) - h.Write(libpf.SliceFrom(&sym.Address)) - } - - var hash [16]byte - fileID, err := libpf.FileIDFromBytes(h.Sum(hash[:0])) +// NewTracer loads eBPF code and map definitions from the ELF module at the configured path. +func NewTracer(ctx context.Context, cfg *Config) (*Tracer, error) { + kernelSymbolizer, err := kallsyms.NewSymbolizer() if err != nil { - panic("calcFallbackModuleID file ID construction is broken") + return nil, fmt.Errorf("failed to read kernel symbols: %v", err) } - log.Debugf("Fallback module ID for module %s is '%s' (min addr: 0x%08X, num exports: %d)", - moduleSym.Name, fileID.Base64(), minAddr, len(moduleSymbols)) - - return fileID -} - -// NewTracer loads eBPF code and map definitions from the ELF module at the configured path. -func NewTracer(ctx context.Context, cfg *Config) (*Tracer, error) { - kernelSymbols, err := proc.GetKallsyms("/proc/kallsyms") + kmod, err := kernelSymbolizer.GetModuleByName(kallsyms.Kernel) if err != nil { return nil, fmt.Errorf("failed to read kernel symbols: %v", err) } // Based on includeTracers we decide later which are loaded into the kernel. - ebpfMaps, ebpfProgs, err := initializeMapsAndPrograms(kernelSymbols, cfg) + ebpfMaps, ebpfProgs, err := initializeMapsAndPrograms(kmod, cfg) if err != nil { return nil, fmt.Errorf("failed to load eBPF code: %v", err) } @@ -306,36 +202,26 @@ func NewTracer(ctx context.Context, cfg *Config) (*Tracer, error) { const fallbackSymbolsCacheSize = 16384 - kernelModules, err := proc.GetKernelModules("/proc/modules", kernelSymbols) - if err != nil { - return nil, fmt.Errorf("failed to read kernel modules: %v", err) - } - - moduleFileIDs, err := processKernelModulesMetadata(cfg.Reporter, kernelModules, kernelSymbols) - if err != nil { - return nil, fmt.Errorf("failed to extract kernel modules metadata: %v", err) - } - perfEventList := []*perf.Event{} - return &Tracer{ + tracer := &Tracer{ + kernelSymbolizer: kernelSymbolizer, processManager: processManager, - kernelSymbols: kernelSymbols, - kernelModules: kernelModules, triggerPIDProcessing: make(chan bool, 1), - pidEvents: make(chan libpf.PID, pidEventBufferSize), + pidEvents: make(chan libpf.PIDTID, pidEventBufferSize), ebpfMaps: ebpfMaps, ebpfProgs: ebpfProgs, hooks: make(map[hookPoint]link.Link), intervals: cfg.Intervals, hasBatchOperations: hasBatchOperations, perfEntrypoints: xsync.NewRWMutex(perfEventList), - moduleFileIDs: moduleFileIDs, reporter: cfg.Reporter, samplesPerSecond: cfg.SamplesPerSecond, probabilisticInterval: cfg.ProbabilisticInterval, probabilisticThreshold: cfg.ProbabilisticThreshold, - }, nil + } + + return tracer, nil } // Close provides functionality for Tracer to perform cleanup tasks. @@ -385,7 +271,7 @@ func buildStackDeltaTemplates(coll *cebpf.CollectionSpec) error { // initializeMapsAndPrograms loads the definitions for the eBPF maps and programs provided // by the embedded elf file and loads these into the kernel. -func initializeMapsAndPrograms(kernelSymbols *libpf.SymbolMap, cfg *Config) ( +func initializeMapsAndPrograms(kmod *kallsyms.Module, cfg *Config) ( ebpfMaps map[string]*cebpf.Map, ebpfProgs map[string]*cebpf.Program, err error) { // Loading specifications about eBPF programs and maps from the embedded elf file // does not load them into the kernel. @@ -427,7 +313,7 @@ func initializeMapsAndPrograms(kernelSymbols *libpf.SymbolMap, cfg *Config) ( return nil, nil, fmt.Errorf("failed to get kernel version: %v", err) } if hasProbeReadBug(major, minor, patch) { - if err = checkForMaccessPatch(coll, ebpfMaps, kernelSymbols); err != nil { + if err = checkForMaccessPatch(coll, ebpfMaps, kmod); err != nil { return nil, nil, fmt.Errorf("your kernel version %d.%d.%d may be "+ "affected by a Linux kernel bug that can lead to system "+ "freezes, terminating host agent now to avoid "+ @@ -486,6 +372,11 @@ func initializeMapsAndPrograms(kernelSymbols *libpf.SymbolMap, cfg *Config) ( name: "unwind_dotnet", enable: cfg.IncludeTracers.Has(types.DotnetTracer), }, + { + progID: uint32(support.ProgGoLabels), + name: "go_labels", + enable: cfg.IncludeTracers.Has(types.Labels), + }, } if err = loadPerfUnwinders(coll, ebpfProgs, ebpfMaps["perf_progs"], tailCallProgs, @@ -500,7 +391,7 @@ func initializeMapsAndPrograms(kernelSymbols *libpf.SymbolMap, cfg *Config) ( } } - if err = loadSystemConfig(coll, ebpfMaps, kernelSymbols, cfg.IncludeTracers, + if err = loadSystemConfig(coll, ebpfMaps, kmod, cfg.IncludeTracers, cfg.OffCPUThreshold, cfg.FilterErrorFrames); err != nil { return nil, nil, fmt.Errorf("failed to load system config: %v", err) } @@ -593,7 +484,7 @@ func loadPerfUnwinders(coll *cebpf.CollectionSpec, ebpfProgs map[string]*cebpf.P copy(progs, tailCallProgs) progs = append(progs, progLoaderHelper{ - name: "tracepoint__sched_process_exit", + name: "tracepoint__sched_process_free", noTailCallTarget: true, enable: true, }, @@ -774,19 +665,12 @@ func (t *Tracer) insertKernelFrames(trace *host.Trace, ustackLen uint32, var kernelSymbolCacheHit, kernelSymbolCacheMiss uint64 for i := uint32(0); i < kstackLen; i++ { - // Translate the kernel address into something that can be - // later symbolized. The address is made relative to - // matching module's ELF .text section: - // - main image should have .text section at start of the code segment - // - modules are ELF object files (.o) without program headers and - // LOAD segments. the address is relative to the .text section - mod, addr, _ := t.kernelModules.LookupByAddress( - libpf.SymbolValue(kstackVal[i])) - - fileID, foundFileID := t.moduleFileIDs[string(mod)] - - if !foundFileID { - fileID = libpf.UnknownKernelFileID + fileID := libpf.UnknownKernelFileID + address := libpf.Address(kstackVal[i]) + kmod, err := t.kernelSymbolizer.GetModuleByAddress(address) + if err == nil { + fileID = kmod.FileID() + address -= kmod.Start() } hostFileID := host.FileIDFromLibpf(fileID) @@ -794,7 +678,7 @@ func (t *Tracer) insertKernelFrames(trace *host.Trace, ustackLen uint32, trace.Frames[i] = host.Frame{ File: hostFileID, - Lineno: libpf.AddressOrLineno(addr), + Lineno: libpf.AddressOrLineno(address), Type: libpf.KernelFrame, // For all kernel frames, the kernel unwinder will always produce a @@ -803,10 +687,6 @@ func (t *Tracer) insertKernelFrames(trace *host.Trace, ustackLen uint32, ReturnAddress: true, } - if !foundFileID { - continue - } - // Kernel frame PCs need to be adjusted by -1. This duplicates logic done in the trace // converter. This should be fixed with PF-1042. frameID := libpf.NewFrameID(fileID, trace.Frames[i].Lineno-1) @@ -816,11 +696,19 @@ func (t *Tracer) insertKernelFrames(trace *host.Trace, ustackLen uint32, } kernelSymbolCacheMiss++ - if symbol, _, foundSymbol := t.kernelSymbols.LookupByAddress( - libpf.SymbolValue(kstackVal[i])); foundSymbol { + if kmod == nil { + continue + } + if funcName, _, err := kmod.LookupSymbolByAddress(libpf.Address(kstackVal[i])); err == nil { t.reporter.FrameMetadata(&reporter.FrameMetadataArgs{ FrameID: frameID, - FunctionName: string(symbol), + FunctionName: funcName, + }) + t.reporter.ExecutableMetadata(&reporter.ExecutableMetadataArgs{ + FileID: kmod.FileID(), + FileName: kmod.Name(), + GnuBuildID: kmod.BuildID(), + Interp: libpf.Kernel, }) } } @@ -843,12 +731,12 @@ func (t *Tracer) enableEvent(eventType int) { // monitorPIDEventsMap periodically iterates over the eBPF map pid_events, // collects PIDs and writes them to the keys slice. -func (t *Tracer) monitorPIDEventsMap(keys *[]uint32) { +func (t *Tracer) monitorPIDEventsMap(keys *[]libpf.PIDTID) { eventsMap := t.ebpfMaps["pid_events"] - var key, nextKey uint32 + var key, nextKey uint64 var value bool keyFound := true - deleteBatch := make(libpf.Set[uint32]) + deleteBatch := make(libpf.Set[uint64]) // Key 0 retrieves the very first element in the hash map as // it is guaranteed not to exist in pid_events. @@ -891,7 +779,7 @@ func (t *Tracer) monitorPIDEventsMap(keys *[]uint32) { // exact point), we may block sending to the channel, delay the iteration and may introduce // race conditions (related to deletion). For that reason, keys are first collected and, // after the iteration has finished, sent to the channel. - *keys = append(*keys, key) + *keys = append(*keys, libpf.PIDTID(key)) } keysToDelete := len(deleteBatch) @@ -983,6 +871,7 @@ func (t *Tracer) loadBpfTrace(raw []byte, cpu int) *host.Trace { trace := &host.Trace{ Comm: C.GoString((*C.char)(unsafe.Pointer(&ptr.comm))), ExecutablePath: procMeta.Executable, + ContainerID: procMeta.ContainerID, ProcessName: procMeta.Name, APMTraceID: *(*libpf.APMTraceID)(unsafe.Pointer(&ptr.apm_trace_id)), APMTransactionID: *(*libpf.APMTransactionID)(unsafe.Pointer(&ptr.apm_transaction_id)), @@ -1024,6 +913,16 @@ func (t *Tracer) loadBpfTrace(raw []byte, cpu int) *host.Trace { } } + if ptr.custom_labels.len > 0 { + trace.CustomLabels = make(map[string]string, int(ptr.custom_labels.len)) + for i := 0; i < int(ptr.custom_labels.len); i++ { + lbl := ptr.custom_labels.labels[i] + key := C.GoString((*C.char)(unsafe.Pointer(&lbl.key))) + val := C.GoString((*C.char)(unsafe.Pointer(&lbl.val))) + trace.CustomLabels[key] = val + } + } + // If there are no kernel frames, or reading them failed, we are responsible // for allocating the columnar frame array. if len(trace.Frames) == 0 { @@ -1045,18 +944,21 @@ func (t *Tracer) loadBpfTrace(raw []byte, cpu int) *host.Trace { // StartMapMonitors starts goroutines for collecting metrics and monitoring eBPF // maps for tracepoints, new traces, trace count updates and unknown PCs. func (t *Tracer) StartMapMonitors(ctx context.Context, traceOutChan chan<- *host.Trace) error { + if err := t.kernelSymbolizer.StartMonitor(ctx); err != nil { + log.Warnf("Failed to start kallsyms monitor: %v", err) + } eventMetricCollector := t.startEventMonitor(ctx) traceEventMetricCollector := t.startTraceEventMonitor(ctx, traceOutChan) - pidEvents := make([]uint32, 0) + pidEvents := make([]libpf.PIDTID, 0) periodiccaller.StartWithManualTrigger(ctx, t.intervals.MonitorInterval(), t.triggerPIDProcessing, func(_ bool) { t.enableEvent(support.EventTypeGenericPID) t.monitorPIDEventsMap(&pidEvents) - for _, ev := range pidEvents { - log.Debugf("=> PID: %v", ev) - t.pidEvents <- libpf.PID(ev) + for _, pidTid := range pidEvents { + log.Debugf("=> %v", pidTid) + t.pidEvents <- pidTid } // Keep the underlying array alive to avoid GC pressure @@ -1299,12 +1201,17 @@ func (t *Tracer) StartOffCPUProfiling() error { return errors.New("off-cpu program finish_task_switch is not available") } - hookSymbolPrefix := "finish_task_switch" - kprobeSymbs, err := t.kernelSymbols.LookupSymbolsByPrefix(hookSymbolPrefix) + kmod, err := t.kernelSymbolizer.GetModuleByName(kallsyms.Kernel) if err != nil { return err } + hookSymbolPrefix := "finish_task_switch" + kprobeSymbs := kmod.LookupSymbolsByPrefix(hookSymbolPrefix) + if len(kprobeSymbs) == 0 { + return errors.New("no finish_task_switch symbols found") + } + attached := false // Attach to all symbols with the prefix finish_task_switch. for _, symb := range kprobeSymbs { diff --git a/tracer/tracer_test.go b/tracer/tracer_test.go new file mode 100644 index 000000000..83d057428 --- /dev/null +++ b/tracer/tracer_test.go @@ -0,0 +1,12 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package tracer contains functionality for populating tracers. +package tracer // import "go.opentelemetry.io/ebpf-profiler/tracer" + +import cebpf "github.com/cilium/ebpf" + +// Make accessible for testing +func (t *Tracer) GetEbpfMaps() map[string]*cebpf.Map { + return t.ebpfMaps +} diff --git a/tracer/types/parse.go b/tracer/types/parse.go index 0337af250..0e28fd0ec 100644 --- a/tracer/types/parse.go +++ b/tracer/types/parse.go @@ -22,6 +22,8 @@ const ( RubyTracer V8Tracer DotnetTracer + GoTracer + Labels // maxTracers indicates the max. number of different tracers maxTracers @@ -35,6 +37,8 @@ var tracerTypeToName = map[tracerType]string{ RubyTracer: "ruby", V8Tracer: "v8", DotnetTracer: "dotnet", + GoTracer: "go", + Labels: "labels", } var tracerNameToType = make(map[string]tracerType, maxTracers)