whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: LLVM
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignConsecutiveAssignments: true
|
||||
AlignEscapedNewlines: DontAlign
|
||||
AlwaysBreakBeforeMultilineStrings: true
|
||||
AlwaysBreakTemplateDeclarations: false
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortFunctionsOnASingleLine: false
|
||||
BreakBeforeBraces: Attach
|
||||
IndentWidth: 4
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
TabWidth: 4
|
||||
UseTab: ForContinuationAndIndentation
|
||||
ColumnLimit: 1000
|
||||
# Go compiler comments need to stay unindented.
|
||||
CommentPragmas: '^go:.*'
|
||||
...
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior. Please include:
|
||||
|
||||
```sh
|
||||
go version
|
||||
uname -a
|
||||
cat /etc/issue
|
||||
```
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
@@ -0,0 +1,7 @@
|
||||
contact_links:
|
||||
- name: Questions
|
||||
url: https://github.com/cilium/ebpf/discussions/categories/q-a
|
||||
about: Please ask and answer questions here.
|
||||
- name: Slack
|
||||
url: https://cilium.slack.com/messages/ebpf-go
|
||||
about: Join our slack.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,14 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.o
|
||||
!*_bpf*.o
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
issues:
|
||||
exclude-rules:
|
||||
# syscall param structs will have unused fields in Go code.
|
||||
- path: syscall.*.go
|
||||
linters:
|
||||
- structcheck
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- errcheck
|
||||
- goimports
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- gofmt
|
||||
|
||||
# Could be enabled later:
|
||||
# - gocyclo
|
||||
# - maligned
|
||||
# - gosec
|
||||
@@ -0,0 +1,73 @@
|
||||
version: v1.0
|
||||
name: CI Build
|
||||
|
||||
agent:
|
||||
machine:
|
||||
type: e1-standard-2
|
||||
os_image: ubuntu2004
|
||||
|
||||
auto_cancel:
|
||||
running:
|
||||
when: "branch != 'master'"
|
||||
|
||||
blocks:
|
||||
- name: Run tests
|
||||
task:
|
||||
prologue:
|
||||
commands:
|
||||
- sudo sh -c 'swapoff -a && fallocate -l 2G /swapfile && chmod 0600 /swapfile && mkswap /swapfile && swapon /swapfile'
|
||||
- sem-version go 1.20.1
|
||||
- export PATH="$PATH:$(go env GOPATH)/bin"
|
||||
- checkout
|
||||
# Disabled, see https://github.com/cilium/ebpf/issues/898
|
||||
# - cache restore
|
||||
- go mod tidy
|
||||
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)/bin" v1.51.2
|
||||
- go install gotest.tools/gotestsum@v1.8.1
|
||||
- sudo pip3 install https://github.com/amluto/virtme/archive/beb85146cd91de37ae455eccb6ab67c393e6e290.zip
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y --no-install-recommends qemu-system-x86 clang-9 llvm-9
|
||||
- sudo dmesg -C
|
||||
epilogue:
|
||||
always:
|
||||
commands:
|
||||
- sudo dmesg
|
||||
- test-results publish junit.xml
|
||||
env_vars:
|
||||
- name: TMPDIR
|
||||
value: /tmp
|
||||
- name: CI_MAX_KERNEL_VERSION
|
||||
value: "5.19"
|
||||
- name: CI_MIN_CLANG_VERSION
|
||||
value: "9"
|
||||
jobs:
|
||||
- name: Build and Lint
|
||||
execution_time_limit:
|
||||
minutes: 10
|
||||
commands:
|
||||
- ( export GOOS=darwin; go build ./... && for p in $(go list ./...) ; do go test -c $p || exit ; done )
|
||||
- ( export GOARCH=arm GOARM=6; go build ./... && for p in $(go list ./...) ; do go test -c $p || exit ; done )
|
||||
- ( export GOARCH=arm64; go build ./... && for p in $(go list ./...) ; do go test -c $p || exit ; done )
|
||||
- make clean
|
||||
- make container-all
|
||||
- git diff --exit-code || { echo "found unformatted source files, or generated files are not up to date, run 'make'" >&2; false; }
|
||||
- pushd ./examples
|
||||
- go build -v -o "$(mktemp -d)" ./...
|
||||
- popd
|
||||
- golangci-lint run
|
||||
- cache store
|
||||
- name: Run unit tests on previous stable Go
|
||||
execution_time_limit:
|
||||
minutes: 10
|
||||
commands:
|
||||
- sem-version go 1.19.6
|
||||
- go test -v ./cmd/bpf2go
|
||||
- gotestsum --raw-command --ignore-non-json-output-lines --junitfile junit.xml -- ./run-tests.sh $CI_MAX_KERNEL_VERSION -short -count 1 -json ./...
|
||||
- name: Run unit tests
|
||||
execution_time_limit:
|
||||
minutes: 10
|
||||
matrix:
|
||||
- env_var: KERNEL_VERSION
|
||||
values: ["5.19", "5.15", "5.10", "5.4", "4.19", "4.14", "4.9"]
|
||||
commands:
|
||||
- gotestsum --raw-command --ignore-non-json-output-lines --junitfile junit.xml -- ./run-tests.sh $KERNEL_VERSION -short -count 1 -json ./...
|
||||
@@ -0,0 +1,92 @@
|
||||
Architecture of the library
|
||||
===
|
||||
|
||||
```mermaid
|
||||
graph RL
|
||||
Program --> ProgramSpec --> ELF
|
||||
btf.Spec --> ELF
|
||||
Map --> MapSpec --> ELF
|
||||
Links --> Map & Program
|
||||
ProgramSpec -.-> btf.Spec
|
||||
MapSpec -.-> btf.Spec
|
||||
subgraph Collection
|
||||
Program & Map
|
||||
end
|
||||
subgraph CollectionSpec
|
||||
ProgramSpec & MapSpec & btf.Spec
|
||||
end
|
||||
```
|
||||
|
||||
ELF
|
||||
---
|
||||
|
||||
BPF is usually produced by using Clang to compile a subset of C. Clang outputs
|
||||
an ELF file which contains program byte code (aka BPF), but also metadata for
|
||||
maps used by the program. The metadata follows the conventions set by libbpf
|
||||
shipped with the kernel. Certain ELF sections have special meaning
|
||||
and contain structures defined by libbpf. Newer versions of clang emit
|
||||
additional metadata in [BPF Type Format](#BTF).
|
||||
|
||||
The library aims to be compatible with libbpf so that moving from a C toolchain
|
||||
to a Go one creates little friction. To that end, the [ELF reader](elf_reader.go)
|
||||
is tested against the Linux selftests and avoids introducing custom behaviour
|
||||
if possible.
|
||||
|
||||
The output of the ELF reader is a `CollectionSpec` which encodes
|
||||
all of the information contained in the ELF in a form that is easy to work with
|
||||
in Go. The returned `CollectionSpec` should be deterministic: reading the same ELF
|
||||
file on different systems must produce the same output.
|
||||
As a corollary, any changes that depend on the runtime environment like the
|
||||
current kernel version must happen when creating [Objects](#Objects).
|
||||
|
||||
Specifications
|
||||
---
|
||||
|
||||
`CollectionSpec` is a very simple container for `ProgramSpec`, `MapSpec` and
|
||||
`btf.Spec`. Avoid adding functionality to it if possible.
|
||||
|
||||
`ProgramSpec` and `MapSpec` are blueprints for in-kernel
|
||||
objects and contain everything necessary to execute the relevant `bpf(2)`
|
||||
syscalls. They refer to `btf.Spec` for type information such as `Map` key and
|
||||
value types.
|
||||
|
||||
The [asm](asm/) package provides an assembler that can be used to generate
|
||||
`ProgramSpec` on the fly.
|
||||
|
||||
Objects
|
||||
---
|
||||
|
||||
`Program` and `Map` are the result of loading specifications into the kernel.
|
||||
Features that depend on knowledge of the current system (e.g kernel version)
|
||||
are implemented at this point.
|
||||
|
||||
Sometimes loading a spec will fail because the kernel is too old, or a feature is not
|
||||
enabled. There are multiple ways the library deals with that:
|
||||
|
||||
* Fallback: older kernels don't allow naming programs and maps. The library
|
||||
automatically detects support for names, and omits them during load if
|
||||
necessary. This works since name is primarily a debug aid.
|
||||
|
||||
* Sentinel error: sometimes it's possible to detect that a feature isn't available.
|
||||
In that case the library will return an error wrapping `ErrNotSupported`.
|
||||
This is also useful to skip tests that can't run on the current kernel.
|
||||
|
||||
Once program and map objects are loaded they expose the kernel's low-level API,
|
||||
e.g. `NextKey`. Often this API is awkward to use in Go, so there are safer
|
||||
wrappers on top of the low-level API, like `MapIterator`. The low-level API is
|
||||
useful when our higher-level API doesn't support a particular use case.
|
||||
|
||||
Links
|
||||
---
|
||||
|
||||
Programs can be attached to many different points in the kernel and newer BPF hooks
|
||||
tend to use bpf_link to do so. Older hooks unfortunately use a combination of
|
||||
syscalls, netlink messages, etc. Adding support for a new link type should not
|
||||
pull in large dependencies like netlink, so XDP programs or tracepoints are
|
||||
out of scope.
|
||||
|
||||
Each bpf_link_type has one corresponding Go type, e.g. `link.tracing` corresponds
|
||||
to BPF_LINK_TRACING. In general, these types should be unexported as long as they
|
||||
don't export methods outside of the Link interface. Each Go type may have multiple
|
||||
exported constructors. For example `AttachTracing` and `AttachLSM` create a
|
||||
tracing link, but are distinct functions since they may require different arguments.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at nathanjsweet at gmail dot com or i at lmb dot io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
@@ -0,0 +1,48 @@
|
||||
# How to contribute
|
||||
|
||||
Development is on [GitHub](https://github.com/cilium/ebpf) and contributions in
|
||||
the form of pull requests and issues reporting bugs or suggesting new features
|
||||
are welcome. Please take a look at [the architecture](ARCHITECTURE.md) to get
|
||||
a better understanding for the high-level goals.
|
||||
|
||||
## Adding a new feature
|
||||
|
||||
1. [Join](https://ebpf.io/slack) the
|
||||
[#ebpf-go](https://cilium.slack.com/messages/ebpf-go) channel to discuss your requirements and how the feature can be implemented. The most important part is figuring out how much new exported API is necessary. **The less new API is required the easier it will be to land the feature.**
|
||||
2. (*optional*) Create a draft PR if you want to discuss the implementation or have hit a problem. It's fine if this doesn't compile or contains debug statements.
|
||||
3. Create a PR that is ready to merge. This must pass CI and have tests.
|
||||
|
||||
### API stability
|
||||
|
||||
The library doesn't guarantee the stability of its API at the moment.
|
||||
|
||||
1. If possible avoid breakage by introducing new API and deprecating the old one
|
||||
at the same time. If an API was deprecated in v0.x it can be removed in v0.x+1.
|
||||
2. Breaking API in a way that causes compilation failures is acceptable but must
|
||||
have good reasons.
|
||||
3. Changing the semantics of the API without causing compilation failures is
|
||||
heavily discouraged.
|
||||
|
||||
## Running the tests
|
||||
|
||||
Many of the tests require privileges to set resource limits and load eBPF code.
|
||||
The easiest way to obtain these is to run the tests with `sudo`.
|
||||
|
||||
To test the current package with your local kernel you can simply run:
|
||||
```
|
||||
go test -exec sudo ./...
|
||||
```
|
||||
|
||||
To test the current package with a different kernel version you can use the [run-tests.sh](run-tests.sh) script.
|
||||
It requires [virtme](https://github.com/amluto/virtme) and qemu to be installed.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Run all tests on a 5.4 kernel
|
||||
./run-tests.sh 5.4
|
||||
|
||||
# Run a subset of tests:
|
||||
./run-tests.sh 5.4 ./link
|
||||
```
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Nathan Sweet
|
||||
Copyright (c) 2018, 2019 Cloudflare
|
||||
Copyright (c) 2019 Authors of Cilium
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Maintainers
|
||||
|
||||
Maintainers can be found in the [Cilium Maintainers file](https://github.com/cilium/community/blob/main/roles/Maintainers.md)
|
||||
@@ -0,0 +1,115 @@
|
||||
# The development version of clang is distributed as the 'clang' binary,
|
||||
# while stable/released versions have a version number attached.
|
||||
# Pin the default clang to a stable version.
|
||||
CLANG ?= clang-14
|
||||
STRIP ?= llvm-strip-14
|
||||
OBJCOPY ?= llvm-objcopy-14
|
||||
CFLAGS := -O2 -g -Wall -Werror $(CFLAGS)
|
||||
|
||||
CI_KERNEL_URL ?= https://github.com/cilium/ci-kernels/raw/master/
|
||||
|
||||
# Obtain an absolute path to the directory of the Makefile.
|
||||
# Assume the Makefile is in the root of the repository.
|
||||
REPODIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
UIDGID := $(shell stat -c '%u:%g' ${REPODIR})
|
||||
|
||||
# Prefer podman if installed, otherwise use docker.
|
||||
# Note: Setting the var at runtime will always override.
|
||||
CONTAINER_ENGINE ?= $(if $(shell command -v podman), podman, docker)
|
||||
CONTAINER_RUN_ARGS ?= $(if $(filter ${CONTAINER_ENGINE}, podman), --log-driver=none, --user "${UIDGID}")
|
||||
|
||||
IMAGE := $(shell cat ${REPODIR}/testdata/docker/IMAGE)
|
||||
VERSION := $(shell cat ${REPODIR}/testdata/docker/VERSION)
|
||||
|
||||
|
||||
# clang <8 doesn't tag relocs properly (STT_NOTYPE)
|
||||
# clang 9 is the first version emitting BTF
|
||||
TARGETS := \
|
||||
testdata/loader-clang-7 \
|
||||
testdata/loader-clang-9 \
|
||||
testdata/loader-$(CLANG) \
|
||||
testdata/manyprogs \
|
||||
testdata/btf_map_init \
|
||||
testdata/invalid_map \
|
||||
testdata/raw_tracepoint \
|
||||
testdata/invalid_map_static \
|
||||
testdata/invalid_btf_map_init \
|
||||
testdata/strings \
|
||||
testdata/freplace \
|
||||
testdata/iproute2_map_compat \
|
||||
testdata/map_spin_lock \
|
||||
testdata/subprog_reloc \
|
||||
testdata/fwd_decl \
|
||||
testdata/kconfig \
|
||||
testdata/kconfig_config \
|
||||
testdata/kfunc \
|
||||
testdata/invalid-kfunc \
|
||||
testdata/kfunc-kmod \
|
||||
btf/testdata/relocs \
|
||||
btf/testdata/relocs_read \
|
||||
btf/testdata/relocs_read_tgt \
|
||||
cmd/bpf2go/testdata/minimal
|
||||
|
||||
.PHONY: all clean container-all container-shell generate
|
||||
|
||||
.DEFAULT_TARGET = container-all
|
||||
|
||||
# Build all ELF binaries using a containerized LLVM toolchain.
|
||||
container-all:
|
||||
+${CONTAINER_ENGINE} run --rm -ti ${CONTAINER_RUN_ARGS} \
|
||||
-v "${REPODIR}":/ebpf -w /ebpf --env MAKEFLAGS \
|
||||
--env CFLAGS="-fdebug-prefix-map=/ebpf=." \
|
||||
--env HOME="/tmp" \
|
||||
"${IMAGE}:${VERSION}" \
|
||||
make all
|
||||
|
||||
# (debug) Drop the user into a shell inside the container as root.
|
||||
container-shell:
|
||||
${CONTAINER_ENGINE} run --rm -ti \
|
||||
-v "${REPODIR}":/ebpf -w /ebpf \
|
||||
"${IMAGE}:${VERSION}"
|
||||
|
||||
clean:
|
||||
-$(RM) testdata/*.elf
|
||||
-$(RM) btf/testdata/*.elf
|
||||
|
||||
format:
|
||||
find . -type f -name "*.c" | xargs clang-format -i
|
||||
|
||||
all: format $(addsuffix -el.elf,$(TARGETS)) $(addsuffix -eb.elf,$(TARGETS)) generate
|
||||
ln -srf testdata/loader-$(CLANG)-el.elf testdata/loader-el.elf
|
||||
ln -srf testdata/loader-$(CLANG)-eb.elf testdata/loader-eb.elf
|
||||
|
||||
# $BPF_CLANG is used in go:generate invocations.
|
||||
generate: export BPF_CLANG := $(CLANG)
|
||||
generate: export BPF_CFLAGS := $(CFLAGS)
|
||||
generate:
|
||||
go generate ./...
|
||||
|
||||
testdata/loader-%-el.elf: testdata/loader.c
|
||||
$* $(CFLAGS) -target bpfel -c $< -o $@
|
||||
$(STRIP) -g $@
|
||||
|
||||
testdata/loader-%-eb.elf: testdata/loader.c
|
||||
$* $(CFLAGS) -target bpfeb -c $< -o $@
|
||||
$(STRIP) -g $@
|
||||
|
||||
%-el.elf: %.c
|
||||
$(CLANG) $(CFLAGS) -target bpfel -c $< -o $@
|
||||
$(STRIP) -g $@
|
||||
|
||||
%-eb.elf : %.c
|
||||
$(CLANG) $(CFLAGS) -target bpfeb -c $< -o $@
|
||||
$(STRIP) -g $@
|
||||
|
||||
.PHONY: generate-btf
|
||||
generate-btf: KERNEL_VERSION?=5.19
|
||||
generate-btf:
|
||||
$(eval TMP := $(shell mktemp -d))
|
||||
curl -fL "$(CI_KERNEL_URL)/linux-$(KERNEL_VERSION).bz" -o "$(TMP)/bzImage"
|
||||
/lib/modules/$(uname -r)/build/scripts/extract-vmlinux "$(TMP)/bzImage" > "$(TMP)/vmlinux"
|
||||
$(OBJCOPY) --dump-section .BTF=/dev/stdout "$(TMP)/vmlinux" /dev/null | gzip > "btf/testdata/vmlinux.btf.gz"
|
||||
curl -fL "$(CI_KERNEL_URL)/linux-$(KERNEL_VERSION)-selftests-bpf.tgz" -o "$(TMP)/selftests.tgz"
|
||||
tar -xf "$(TMP)/selftests.tgz" --to-stdout tools/testing/selftests/bpf/bpf_testmod/bpf_testmod.ko | \
|
||||
$(OBJCOPY) --dump-section .BTF="btf/testdata/btf_testmod.btf" - /dev/null
|
||||
$(RM) -r "$(TMP)"
|
||||
@@ -0,0 +1,82 @@
|
||||
# eBPF
|
||||
|
||||
[](https://pkg.go.dev/github.com/cilium/ebpf)
|
||||
|
||||

|
||||
|
||||
ebpf-go is a pure Go library that provides utilities for loading, compiling, and
|
||||
debugging eBPF programs. It has minimal external dependencies and is intended to
|
||||
be used in long running processes.
|
||||
|
||||
See [ebpf.io](https://ebpf.io) for complementary projects from the wider eBPF
|
||||
ecosystem.
|
||||
|
||||
## Getting Started
|
||||
|
||||
A small collection of Go and eBPF programs that serve as examples for building
|
||||
your own tools can be found under [examples/](examples/).
|
||||
|
||||
[Contributions](CONTRIBUTING.md) are highly encouraged, as they highlight certain use cases of
|
||||
eBPF and the library, and help shape the future of the project.
|
||||
|
||||
## Getting Help
|
||||
|
||||
The community actively monitors our [GitHub Discussions](https://github.com/cilium/ebpf/discussions) page.
|
||||
Please search for existing threads before starting a new one. Refrain from
|
||||
opening issues on the bug tracker if you're just starting out or if you're not
|
||||
sure if something is a bug in the library code.
|
||||
|
||||
Alternatively, [join](https://ebpf.io/slack) the
|
||||
[#ebpf-go](https://cilium.slack.com/messages/ebpf-go) channel on Slack if you
|
||||
have other questions regarding the project. Note that this channel is ephemeral
|
||||
and has its history erased past a certain point, which is less helpful for
|
||||
others running into the same problem later.
|
||||
|
||||
## Packages
|
||||
|
||||
This library includes the following packages:
|
||||
|
||||
* [asm](https://pkg.go.dev/github.com/cilium/ebpf/asm) contains a basic
|
||||
assembler, allowing you to write eBPF assembly instructions directly
|
||||
within your Go code. (You don't need to use this if you prefer to write your eBPF program in C.)
|
||||
* [cmd/bpf2go](https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go) allows
|
||||
compiling and embedding eBPF programs written in C within Go code. As well as
|
||||
compiling the C code, it auto-generates Go code for loading and manipulating
|
||||
the eBPF program and map objects.
|
||||
* [link](https://pkg.go.dev/github.com/cilium/ebpf/link) allows attaching eBPF
|
||||
to various hooks
|
||||
* [perf](https://pkg.go.dev/github.com/cilium/ebpf/perf) allows reading from a
|
||||
`PERF_EVENT_ARRAY`
|
||||
* [ringbuf](https://pkg.go.dev/github.com/cilium/ebpf/ringbuf) allows reading from a
|
||||
`BPF_MAP_TYPE_RINGBUF` map
|
||||
* [features](https://pkg.go.dev/github.com/cilium/ebpf/features) implements the equivalent
|
||||
of `bpftool feature probe` for discovering BPF-related kernel features using native Go.
|
||||
* [rlimit](https://pkg.go.dev/github.com/cilium/ebpf/rlimit) provides a convenient API to lift
|
||||
the `RLIMIT_MEMLOCK` constraint on kernels before 5.11.
|
||||
* [btf](https://pkg.go.dev/github.com/cilium/ebpf/btf) allows reading the BPF Type Format.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A version of Go that is [supported by
|
||||
upstream](https://golang.org/doc/devel/release.html#policy)
|
||||
* Linux >= 4.9. CI is run against kernel.org LTS releases. 4.4 should work but is
|
||||
not tested against.
|
||||
|
||||
## Regenerating Testdata
|
||||
|
||||
Run `make` in the root of this repository to rebuild testdata in all
|
||||
subpackages. This requires Docker, as it relies on a standardized build
|
||||
environment to keep the build output stable.
|
||||
|
||||
It is possible to regenerate data using Podman by overriding the `CONTAINER_*`
|
||||
variables: `CONTAINER_ENGINE=podman CONTAINER_RUN_ARGS= make`.
|
||||
|
||||
The toolchain image build files are kept in [testdata/docker/](testdata/docker/).
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
### eBPF Gopher
|
||||
|
||||
The eBPF honeygopher is based on the Go gopher designed by Renee French.
|
||||
@@ -0,0 +1,149 @@
|
||||
package asm
|
||||
|
||||
//go:generate stringer -output alu_string.go -type=Source,Endianness,ALUOp
|
||||
|
||||
// Source of ALU / ALU64 / Branch operations
|
||||
//
|
||||
// msb lsb
|
||||
// +----+-+---+
|
||||
// |op |S|cls|
|
||||
// +----+-+---+
|
||||
type Source uint8
|
||||
|
||||
const sourceMask OpCode = 0x08
|
||||
|
||||
// Source bitmask
|
||||
const (
|
||||
// InvalidSource is returned by getters when invoked
|
||||
// on non ALU / branch OpCodes.
|
||||
InvalidSource Source = 0xff
|
||||
// ImmSource src is from constant
|
||||
ImmSource Source = 0x00
|
||||
// RegSource src is from register
|
||||
RegSource Source = 0x08
|
||||
)
|
||||
|
||||
// The Endianness of a byte swap instruction.
|
||||
type Endianness uint8
|
||||
|
||||
const endianMask = sourceMask
|
||||
|
||||
// Endian flags
|
||||
const (
|
||||
InvalidEndian Endianness = 0xff
|
||||
// Convert to little endian
|
||||
LE Endianness = 0x00
|
||||
// Convert to big endian
|
||||
BE Endianness = 0x08
|
||||
)
|
||||
|
||||
// ALUOp are ALU / ALU64 operations
|
||||
//
|
||||
// msb lsb
|
||||
// +----+-+---+
|
||||
// |OP |s|cls|
|
||||
// +----+-+---+
|
||||
type ALUOp uint8
|
||||
|
||||
const aluMask OpCode = 0xf0
|
||||
|
||||
const (
|
||||
// InvalidALUOp is returned by getters when invoked
|
||||
// on non ALU OpCodes
|
||||
InvalidALUOp ALUOp = 0xff
|
||||
// Add - addition
|
||||
Add ALUOp = 0x00
|
||||
// Sub - subtraction
|
||||
Sub ALUOp = 0x10
|
||||
// Mul - multiplication
|
||||
Mul ALUOp = 0x20
|
||||
// Div - division
|
||||
Div ALUOp = 0x30
|
||||
// Or - bitwise or
|
||||
Or ALUOp = 0x40
|
||||
// And - bitwise and
|
||||
And ALUOp = 0x50
|
||||
// LSh - bitwise shift left
|
||||
LSh ALUOp = 0x60
|
||||
// RSh - bitwise shift right
|
||||
RSh ALUOp = 0x70
|
||||
// Neg - sign/unsign signing bit
|
||||
Neg ALUOp = 0x80
|
||||
// Mod - modulo
|
||||
Mod ALUOp = 0x90
|
||||
// Xor - bitwise xor
|
||||
Xor ALUOp = 0xa0
|
||||
// Mov - move value from one place to another
|
||||
Mov ALUOp = 0xb0
|
||||
// ArSh - arithmatic shift
|
||||
ArSh ALUOp = 0xc0
|
||||
// Swap - endian conversions
|
||||
Swap ALUOp = 0xd0
|
||||
)
|
||||
|
||||
// HostTo converts from host to another endianness.
|
||||
func HostTo(endian Endianness, dst Register, size Size) Instruction {
|
||||
var imm int64
|
||||
switch size {
|
||||
case Half:
|
||||
imm = 16
|
||||
case Word:
|
||||
imm = 32
|
||||
case DWord:
|
||||
imm = 64
|
||||
default:
|
||||
return Instruction{OpCode: InvalidOpCode}
|
||||
}
|
||||
|
||||
return Instruction{
|
||||
OpCode: OpCode(ALUClass).SetALUOp(Swap).SetSource(Source(endian)),
|
||||
Dst: dst,
|
||||
Constant: imm,
|
||||
}
|
||||
}
|
||||
|
||||
// Op returns the OpCode for an ALU operation with a given source.
|
||||
func (op ALUOp) Op(source Source) OpCode {
|
||||
return OpCode(ALU64Class).SetALUOp(op).SetSource(source)
|
||||
}
|
||||
|
||||
// Reg emits `dst (op) src`.
|
||||
func (op ALUOp) Reg(dst, src Register) Instruction {
|
||||
return Instruction{
|
||||
OpCode: op.Op(RegSource),
|
||||
Dst: dst,
|
||||
Src: src,
|
||||
}
|
||||
}
|
||||
|
||||
// Imm emits `dst (op) value`.
|
||||
func (op ALUOp) Imm(dst Register, value int32) Instruction {
|
||||
return Instruction{
|
||||
OpCode: op.Op(ImmSource),
|
||||
Dst: dst,
|
||||
Constant: int64(value),
|
||||
}
|
||||
}
|
||||
|
||||
// Op32 returns the OpCode for a 32-bit ALU operation with a given source.
|
||||
func (op ALUOp) Op32(source Source) OpCode {
|
||||
return OpCode(ALUClass).SetALUOp(op).SetSource(source)
|
||||
}
|
||||
|
||||
// Reg32 emits `dst (op) src`, zeroing the upper 32 bit of dst.
|
||||
func (op ALUOp) Reg32(dst, src Register) Instruction {
|
||||
return Instruction{
|
||||
OpCode: op.Op32(RegSource),
|
||||
Dst: dst,
|
||||
Src: src,
|
||||
}
|
||||
}
|
||||
|
||||
// Imm32 emits `dst (op) value`, zeroing the upper 32 bit of dst.
|
||||
func (op ALUOp) Imm32(dst Register, value int32) Instruction {
|
||||
return Instruction{
|
||||
OpCode: op.Op32(ImmSource),
|
||||
Dst: dst,
|
||||
Constant: int64(value),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Code generated by "stringer -output alu_string.go -type=Source,Endianness,ALUOp"; DO NOT EDIT.
|
||||
|
||||
package asm
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[InvalidSource-255]
|
||||
_ = x[ImmSource-0]
|
||||
_ = x[RegSource-8]
|
||||
}
|
||||
|
||||
const (
|
||||
_Source_name_0 = "ImmSource"
|
||||
_Source_name_1 = "RegSource"
|
||||
_Source_name_2 = "InvalidSource"
|
||||
)
|
||||
|
||||
func (i Source) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _Source_name_0
|
||||
case i == 8:
|
||||
return _Source_name_1
|
||||
case i == 255:
|
||||
return _Source_name_2
|
||||
default:
|
||||
return "Source(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[InvalidEndian-255]
|
||||
_ = x[LE-0]
|
||||
_ = x[BE-8]
|
||||
}
|
||||
|
||||
const (
|
||||
_Endianness_name_0 = "LE"
|
||||
_Endianness_name_1 = "BE"
|
||||
_Endianness_name_2 = "InvalidEndian"
|
||||
)
|
||||
|
||||
func (i Endianness) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _Endianness_name_0
|
||||
case i == 8:
|
||||
return _Endianness_name_1
|
||||
case i == 255:
|
||||
return _Endianness_name_2
|
||||
default:
|
||||
return "Endianness(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[InvalidALUOp-255]
|
||||
_ = x[Add-0]
|
||||
_ = x[Sub-16]
|
||||
_ = x[Mul-32]
|
||||
_ = x[Div-48]
|
||||
_ = x[Or-64]
|
||||
_ = x[And-80]
|
||||
_ = x[LSh-96]
|
||||
_ = x[RSh-112]
|
||||
_ = x[Neg-128]
|
||||
_ = x[Mod-144]
|
||||
_ = x[Xor-160]
|
||||
_ = x[Mov-176]
|
||||
_ = x[ArSh-192]
|
||||
_ = x[Swap-208]
|
||||
}
|
||||
|
||||
const _ALUOp_name = "AddSubMulDivOrAndLShRShNegModXorMovArShSwapInvalidALUOp"
|
||||
|
||||
var _ALUOp_map = map[ALUOp]string{
|
||||
0: _ALUOp_name[0:3],
|
||||
16: _ALUOp_name[3:6],
|
||||
32: _ALUOp_name[6:9],
|
||||
48: _ALUOp_name[9:12],
|
||||
64: _ALUOp_name[12:14],
|
||||
80: _ALUOp_name[14:17],
|
||||
96: _ALUOp_name[17:20],
|
||||
112: _ALUOp_name[20:23],
|
||||
128: _ALUOp_name[23:26],
|
||||
144: _ALUOp_name[26:29],
|
||||
160: _ALUOp_name[29:32],
|
||||
176: _ALUOp_name[32:35],
|
||||
192: _ALUOp_name[35:39],
|
||||
208: _ALUOp_name[39:43],
|
||||
255: _ALUOp_name[43:55],
|
||||
}
|
||||
|
||||
func (i ALUOp) String() string {
|
||||
if str, ok := _ALUOp_map[i]; ok {
|
||||
return str
|
||||
}
|
||||
return "ALUOp(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package asm is an assembler for eBPF bytecode.
|
||||
package asm
|
||||
@@ -0,0 +1,46 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDSL(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
have Instruction
|
||||
want Instruction
|
||||
}{
|
||||
{"Call", FnMapLookupElem.Call(), Instruction{OpCode: 0x85, Constant: 1}},
|
||||
{"Exit", Return(), Instruction{OpCode: 0x95}},
|
||||
{"LoadAbs", LoadAbs(2, Byte), Instruction{OpCode: 0x30, Constant: 2}},
|
||||
{"Store", StoreMem(RFP, -4, R0, Word), Instruction{
|
||||
OpCode: 0x63,
|
||||
Dst: RFP,
|
||||
Src: R0,
|
||||
Offset: -4,
|
||||
}},
|
||||
{"Add.Imm", Add.Imm(R1, 22), Instruction{OpCode: 0x07, Dst: R1, Constant: 22}},
|
||||
{"Add.Reg", Add.Reg(R1, R2), Instruction{OpCode: 0x0f, Dst: R1, Src: R2}},
|
||||
{"Add.Imm32", Add.Imm32(R1, 22), Instruction{
|
||||
OpCode: 0x04, Dst: R1, Constant: 22,
|
||||
}},
|
||||
{"JSGT.Imm", JSGT.Imm(R1, 4, "foo"), Instruction{
|
||||
OpCode: 0x65, Dst: R1, Constant: 4, Offset: -1,
|
||||
}.WithReference("foo")},
|
||||
{"JSGT.Imm32", JSGT.Imm32(R1, -2, "foo"), Instruction{
|
||||
OpCode: 0x66, Dst: R1, Constant: -2, Offset: -1,
|
||||
}.WithReference("foo")},
|
||||
{"JSLT.Reg", JSLT.Reg(R1, R2, "foo"), Instruction{
|
||||
OpCode: 0xcd, Dst: R1, Src: R2, Offset: -1,
|
||||
}.WithReference("foo")},
|
||||
{"JSLT.Reg32", JSLT.Reg32(R1, R3, "foo"), Instruction{
|
||||
OpCode: 0xce, Dst: R1, Src: R3, Offset: -1,
|
||||
}.WithReference("foo")},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
if !tc.have.equal(tc.want) {
|
||||
t.Errorf("%s: have %v, want %v", tc.name, tc.have, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package asm
|
||||
|
||||
//go:generate stringer -output func_string.go -type=BuiltinFunc
|
||||
|
||||
// BuiltinFunc is a built-in eBPF function.
|
||||
type BuiltinFunc int32
|
||||
|
||||
func (_ BuiltinFunc) Max() BuiltinFunc {
|
||||
return maxBuiltinFunc - 1
|
||||
}
|
||||
|
||||
// eBPF built-in functions
|
||||
//
|
||||
// You can regenerate this list using the following gawk script:
|
||||
//
|
||||
// /FN\(.+\),/ {
|
||||
// match($1, /\(([a-z_0-9]+),/, r)
|
||||
// split(r[1], p, "_")
|
||||
// printf "Fn"
|
||||
// for (i in p) {
|
||||
// printf "%s%s", toupper(substr(p[i], 1, 1)), substr(p[i], 2)
|
||||
// }
|
||||
// print ""
|
||||
// }
|
||||
//
|
||||
// The script expects include/uapi/linux/bpf.h as it's input.
|
||||
const (
|
||||
FnUnspec BuiltinFunc = iota
|
||||
FnMapLookupElem
|
||||
FnMapUpdateElem
|
||||
FnMapDeleteElem
|
||||
FnProbeRead
|
||||
FnKtimeGetNs
|
||||
FnTracePrintk
|
||||
FnGetPrandomU32
|
||||
FnGetSmpProcessorId
|
||||
FnSkbStoreBytes
|
||||
FnL3CsumReplace
|
||||
FnL4CsumReplace
|
||||
FnTailCall
|
||||
FnCloneRedirect
|
||||
FnGetCurrentPidTgid
|
||||
FnGetCurrentUidGid
|
||||
FnGetCurrentComm
|
||||
FnGetCgroupClassid
|
||||
FnSkbVlanPush
|
||||
FnSkbVlanPop
|
||||
FnSkbGetTunnelKey
|
||||
FnSkbSetTunnelKey
|
||||
FnPerfEventRead
|
||||
FnRedirect
|
||||
FnGetRouteRealm
|
||||
FnPerfEventOutput
|
||||
FnSkbLoadBytes
|
||||
FnGetStackid
|
||||
FnCsumDiff
|
||||
FnSkbGetTunnelOpt
|
||||
FnSkbSetTunnelOpt
|
||||
FnSkbChangeProto
|
||||
FnSkbChangeType
|
||||
FnSkbUnderCgroup
|
||||
FnGetHashRecalc
|
||||
FnGetCurrentTask
|
||||
FnProbeWriteUser
|
||||
FnCurrentTaskUnderCgroup
|
||||
FnSkbChangeTail
|
||||
FnSkbPullData
|
||||
FnCsumUpdate
|
||||
FnSetHashInvalid
|
||||
FnGetNumaNodeId
|
||||
FnSkbChangeHead
|
||||
FnXdpAdjustHead
|
||||
FnProbeReadStr
|
||||
FnGetSocketCookie
|
||||
FnGetSocketUid
|
||||
FnSetHash
|
||||
FnSetsockopt
|
||||
FnSkbAdjustRoom
|
||||
FnRedirectMap
|
||||
FnSkRedirectMap
|
||||
FnSockMapUpdate
|
||||
FnXdpAdjustMeta
|
||||
FnPerfEventReadValue
|
||||
FnPerfProgReadValue
|
||||
FnGetsockopt
|
||||
FnOverrideReturn
|
||||
FnSockOpsCbFlagsSet
|
||||
FnMsgRedirectMap
|
||||
FnMsgApplyBytes
|
||||
FnMsgCorkBytes
|
||||
FnMsgPullData
|
||||
FnBind
|
||||
FnXdpAdjustTail
|
||||
FnSkbGetXfrmState
|
||||
FnGetStack
|
||||
FnSkbLoadBytesRelative
|
||||
FnFibLookup
|
||||
FnSockHashUpdate
|
||||
FnMsgRedirectHash
|
||||
FnSkRedirectHash
|
||||
FnLwtPushEncap
|
||||
FnLwtSeg6StoreBytes
|
||||
FnLwtSeg6AdjustSrh
|
||||
FnLwtSeg6Action
|
||||
FnRcRepeat
|
||||
FnRcKeydown
|
||||
FnSkbCgroupId
|
||||
FnGetCurrentCgroupId
|
||||
FnGetLocalStorage
|
||||
FnSkSelectReuseport
|
||||
FnSkbAncestorCgroupId
|
||||
FnSkLookupTcp
|
||||
FnSkLookupUdp
|
||||
FnSkRelease
|
||||
FnMapPushElem
|
||||
FnMapPopElem
|
||||
FnMapPeekElem
|
||||
FnMsgPushData
|
||||
FnMsgPopData
|
||||
FnRcPointerRel
|
||||
FnSpinLock
|
||||
FnSpinUnlock
|
||||
FnSkFullsock
|
||||
FnTcpSock
|
||||
FnSkbEcnSetCe
|
||||
FnGetListenerSock
|
||||
FnSkcLookupTcp
|
||||
FnTcpCheckSyncookie
|
||||
FnSysctlGetName
|
||||
FnSysctlGetCurrentValue
|
||||
FnSysctlGetNewValue
|
||||
FnSysctlSetNewValue
|
||||
FnStrtol
|
||||
FnStrtoul
|
||||
FnSkStorageGet
|
||||
FnSkStorageDelete
|
||||
FnSendSignal
|
||||
FnTcpGenSyncookie
|
||||
FnSkbOutput
|
||||
FnProbeReadUser
|
||||
FnProbeReadKernel
|
||||
FnProbeReadUserStr
|
||||
FnProbeReadKernelStr
|
||||
FnTcpSendAck
|
||||
FnSendSignalThread
|
||||
FnJiffies64
|
||||
FnReadBranchRecords
|
||||
FnGetNsCurrentPidTgid
|
||||
FnXdpOutput
|
||||
FnGetNetnsCookie
|
||||
FnGetCurrentAncestorCgroupId
|
||||
FnSkAssign
|
||||
FnKtimeGetBootNs
|
||||
FnSeqPrintf
|
||||
FnSeqWrite
|
||||
FnSkCgroupId
|
||||
FnSkAncestorCgroupId
|
||||
FnRingbufOutput
|
||||
FnRingbufReserve
|
||||
FnRingbufSubmit
|
||||
FnRingbufDiscard
|
||||
FnRingbufQuery
|
||||
FnCsumLevel
|
||||
FnSkcToTcp6Sock
|
||||
FnSkcToTcpSock
|
||||
FnSkcToTcpTimewaitSock
|
||||
FnSkcToTcpRequestSock
|
||||
FnSkcToUdp6Sock
|
||||
FnGetTaskStack
|
||||
FnLoadHdrOpt
|
||||
FnStoreHdrOpt
|
||||
FnReserveHdrOpt
|
||||
FnInodeStorageGet
|
||||
FnInodeStorageDelete
|
||||
FnDPath
|
||||
FnCopyFromUser
|
||||
FnSnprintfBtf
|
||||
FnSeqPrintfBtf
|
||||
FnSkbCgroupClassid
|
||||
FnRedirectNeigh
|
||||
FnPerCpuPtr
|
||||
FnThisCpuPtr
|
||||
FnRedirectPeer
|
||||
FnTaskStorageGet
|
||||
FnTaskStorageDelete
|
||||
FnGetCurrentTaskBtf
|
||||
FnBprmOptsSet
|
||||
FnKtimeGetCoarseNs
|
||||
FnImaInodeHash
|
||||
FnSockFromFile
|
||||
FnCheckMtu
|
||||
FnForEachMapElem
|
||||
FnSnprintf
|
||||
FnSysBpf
|
||||
FnBtfFindByNameKind
|
||||
FnSysClose
|
||||
FnTimerInit
|
||||
FnTimerSetCallback
|
||||
FnTimerStart
|
||||
FnTimerCancel
|
||||
FnGetFuncIp
|
||||
FnGetAttachCookie
|
||||
FnTaskPtRegs
|
||||
FnGetBranchSnapshot
|
||||
FnTraceVprintk
|
||||
FnSkcToUnixSock
|
||||
FnKallsymsLookupName
|
||||
FnFindVma
|
||||
FnLoop
|
||||
FnStrncmp
|
||||
FnGetFuncArg
|
||||
FnGetFuncRet
|
||||
FnGetFuncArgCnt
|
||||
FnGetRetval
|
||||
FnSetRetval
|
||||
FnXdpGetBuffLen
|
||||
FnXdpLoadBytes
|
||||
FnXdpStoreBytes
|
||||
FnCopyFromUserTask
|
||||
FnSkbSetTstamp
|
||||
FnImaFileHash
|
||||
FnKptrXchg
|
||||
FnMapLookupPercpuElem
|
||||
FnSkcToMptcpSock
|
||||
FnDynptrFromMem
|
||||
FnRingbufReserveDynptr
|
||||
FnRingbufSubmitDynptr
|
||||
FnRingbufDiscardDynptr
|
||||
FnDynptrRead
|
||||
FnDynptrWrite
|
||||
FnDynptrData
|
||||
FnTcpRawGenSyncookieIpv4
|
||||
FnTcpRawGenSyncookieIpv6
|
||||
FnTcpRawCheckSyncookieIpv4
|
||||
FnTcpRawCheckSyncookieIpv6
|
||||
FnKtimeGetTaiNs
|
||||
FnUserRingbufDrain
|
||||
FnCgrpStorageGet
|
||||
FnCgrpStorageDelete
|
||||
|
||||
maxBuiltinFunc
|
||||
)
|
||||
|
||||
// Call emits a function call.
|
||||
func (fn BuiltinFunc) Call() Instruction {
|
||||
return Instruction{
|
||||
OpCode: OpCode(JumpClass).SetJumpOp(Call),
|
||||
Constant: int64(fn),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// Code generated by "stringer -output func_string.go -type=BuiltinFunc"; DO NOT EDIT.
|
||||
|
||||
package asm
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[FnUnspec-0]
|
||||
_ = x[FnMapLookupElem-1]
|
||||
_ = x[FnMapUpdateElem-2]
|
||||
_ = x[FnMapDeleteElem-3]
|
||||
_ = x[FnProbeRead-4]
|
||||
_ = x[FnKtimeGetNs-5]
|
||||
_ = x[FnTracePrintk-6]
|
||||
_ = x[FnGetPrandomU32-7]
|
||||
_ = x[FnGetSmpProcessorId-8]
|
||||
_ = x[FnSkbStoreBytes-9]
|
||||
_ = x[FnL3CsumReplace-10]
|
||||
_ = x[FnL4CsumReplace-11]
|
||||
_ = x[FnTailCall-12]
|
||||
_ = x[FnCloneRedirect-13]
|
||||
_ = x[FnGetCurrentPidTgid-14]
|
||||
_ = x[FnGetCurrentUidGid-15]
|
||||
_ = x[FnGetCurrentComm-16]
|
||||
_ = x[FnGetCgroupClassid-17]
|
||||
_ = x[FnSkbVlanPush-18]
|
||||
_ = x[FnSkbVlanPop-19]
|
||||
_ = x[FnSkbGetTunnelKey-20]
|
||||
_ = x[FnSkbSetTunnelKey-21]
|
||||
_ = x[FnPerfEventRead-22]
|
||||
_ = x[FnRedirect-23]
|
||||
_ = x[FnGetRouteRealm-24]
|
||||
_ = x[FnPerfEventOutput-25]
|
||||
_ = x[FnSkbLoadBytes-26]
|
||||
_ = x[FnGetStackid-27]
|
||||
_ = x[FnCsumDiff-28]
|
||||
_ = x[FnSkbGetTunnelOpt-29]
|
||||
_ = x[FnSkbSetTunnelOpt-30]
|
||||
_ = x[FnSkbChangeProto-31]
|
||||
_ = x[FnSkbChangeType-32]
|
||||
_ = x[FnSkbUnderCgroup-33]
|
||||
_ = x[FnGetHashRecalc-34]
|
||||
_ = x[FnGetCurrentTask-35]
|
||||
_ = x[FnProbeWriteUser-36]
|
||||
_ = x[FnCurrentTaskUnderCgroup-37]
|
||||
_ = x[FnSkbChangeTail-38]
|
||||
_ = x[FnSkbPullData-39]
|
||||
_ = x[FnCsumUpdate-40]
|
||||
_ = x[FnSetHashInvalid-41]
|
||||
_ = x[FnGetNumaNodeId-42]
|
||||
_ = x[FnSkbChangeHead-43]
|
||||
_ = x[FnXdpAdjustHead-44]
|
||||
_ = x[FnProbeReadStr-45]
|
||||
_ = x[FnGetSocketCookie-46]
|
||||
_ = x[FnGetSocketUid-47]
|
||||
_ = x[FnSetHash-48]
|
||||
_ = x[FnSetsockopt-49]
|
||||
_ = x[FnSkbAdjustRoom-50]
|
||||
_ = x[FnRedirectMap-51]
|
||||
_ = x[FnSkRedirectMap-52]
|
||||
_ = x[FnSockMapUpdate-53]
|
||||
_ = x[FnXdpAdjustMeta-54]
|
||||
_ = x[FnPerfEventReadValue-55]
|
||||
_ = x[FnPerfProgReadValue-56]
|
||||
_ = x[FnGetsockopt-57]
|
||||
_ = x[FnOverrideReturn-58]
|
||||
_ = x[FnSockOpsCbFlagsSet-59]
|
||||
_ = x[FnMsgRedirectMap-60]
|
||||
_ = x[FnMsgApplyBytes-61]
|
||||
_ = x[FnMsgCorkBytes-62]
|
||||
_ = x[FnMsgPullData-63]
|
||||
_ = x[FnBind-64]
|
||||
_ = x[FnXdpAdjustTail-65]
|
||||
_ = x[FnSkbGetXfrmState-66]
|
||||
_ = x[FnGetStack-67]
|
||||
_ = x[FnSkbLoadBytesRelative-68]
|
||||
_ = x[FnFibLookup-69]
|
||||
_ = x[FnSockHashUpdate-70]
|
||||
_ = x[FnMsgRedirectHash-71]
|
||||
_ = x[FnSkRedirectHash-72]
|
||||
_ = x[FnLwtPushEncap-73]
|
||||
_ = x[FnLwtSeg6StoreBytes-74]
|
||||
_ = x[FnLwtSeg6AdjustSrh-75]
|
||||
_ = x[FnLwtSeg6Action-76]
|
||||
_ = x[FnRcRepeat-77]
|
||||
_ = x[FnRcKeydown-78]
|
||||
_ = x[FnSkbCgroupId-79]
|
||||
_ = x[FnGetCurrentCgroupId-80]
|
||||
_ = x[FnGetLocalStorage-81]
|
||||
_ = x[FnSkSelectReuseport-82]
|
||||
_ = x[FnSkbAncestorCgroupId-83]
|
||||
_ = x[FnSkLookupTcp-84]
|
||||
_ = x[FnSkLookupUdp-85]
|
||||
_ = x[FnSkRelease-86]
|
||||
_ = x[FnMapPushElem-87]
|
||||
_ = x[FnMapPopElem-88]
|
||||
_ = x[FnMapPeekElem-89]
|
||||
_ = x[FnMsgPushData-90]
|
||||
_ = x[FnMsgPopData-91]
|
||||
_ = x[FnRcPointerRel-92]
|
||||
_ = x[FnSpinLock-93]
|
||||
_ = x[FnSpinUnlock-94]
|
||||
_ = x[FnSkFullsock-95]
|
||||
_ = x[FnTcpSock-96]
|
||||
_ = x[FnSkbEcnSetCe-97]
|
||||
_ = x[FnGetListenerSock-98]
|
||||
_ = x[FnSkcLookupTcp-99]
|
||||
_ = x[FnTcpCheckSyncookie-100]
|
||||
_ = x[FnSysctlGetName-101]
|
||||
_ = x[FnSysctlGetCurrentValue-102]
|
||||
_ = x[FnSysctlGetNewValue-103]
|
||||
_ = x[FnSysctlSetNewValue-104]
|
||||
_ = x[FnStrtol-105]
|
||||
_ = x[FnStrtoul-106]
|
||||
_ = x[FnSkStorageGet-107]
|
||||
_ = x[FnSkStorageDelete-108]
|
||||
_ = x[FnSendSignal-109]
|
||||
_ = x[FnTcpGenSyncookie-110]
|
||||
_ = x[FnSkbOutput-111]
|
||||
_ = x[FnProbeReadUser-112]
|
||||
_ = x[FnProbeReadKernel-113]
|
||||
_ = x[FnProbeReadUserStr-114]
|
||||
_ = x[FnProbeReadKernelStr-115]
|
||||
_ = x[FnTcpSendAck-116]
|
||||
_ = x[FnSendSignalThread-117]
|
||||
_ = x[FnJiffies64-118]
|
||||
_ = x[FnReadBranchRecords-119]
|
||||
_ = x[FnGetNsCurrentPidTgid-120]
|
||||
_ = x[FnXdpOutput-121]
|
||||
_ = x[FnGetNetnsCookie-122]
|
||||
_ = x[FnGetCurrentAncestorCgroupId-123]
|
||||
_ = x[FnSkAssign-124]
|
||||
_ = x[FnKtimeGetBootNs-125]
|
||||
_ = x[FnSeqPrintf-126]
|
||||
_ = x[FnSeqWrite-127]
|
||||
_ = x[FnSkCgroupId-128]
|
||||
_ = x[FnSkAncestorCgroupId-129]
|
||||
_ = x[FnRingbufOutput-130]
|
||||
_ = x[FnRingbufReserve-131]
|
||||
_ = x[FnRingbufSubmit-132]
|
||||
_ = x[FnRingbufDiscard-133]
|
||||
_ = x[FnRingbufQuery-134]
|
||||
_ = x[FnCsumLevel-135]
|
||||
_ = x[FnSkcToTcp6Sock-136]
|
||||
_ = x[FnSkcToTcpSock-137]
|
||||
_ = x[FnSkcToTcpTimewaitSock-138]
|
||||
_ = x[FnSkcToTcpRequestSock-139]
|
||||
_ = x[FnSkcToUdp6Sock-140]
|
||||
_ = x[FnGetTaskStack-141]
|
||||
_ = x[FnLoadHdrOpt-142]
|
||||
_ = x[FnStoreHdrOpt-143]
|
||||
_ = x[FnReserveHdrOpt-144]
|
||||
_ = x[FnInodeStorageGet-145]
|
||||
_ = x[FnInodeStorageDelete-146]
|
||||
_ = x[FnDPath-147]
|
||||
_ = x[FnCopyFromUser-148]
|
||||
_ = x[FnSnprintfBtf-149]
|
||||
_ = x[FnSeqPrintfBtf-150]
|
||||
_ = x[FnSkbCgroupClassid-151]
|
||||
_ = x[FnRedirectNeigh-152]
|
||||
_ = x[FnPerCpuPtr-153]
|
||||
_ = x[FnThisCpuPtr-154]
|
||||
_ = x[FnRedirectPeer-155]
|
||||
_ = x[FnTaskStorageGet-156]
|
||||
_ = x[FnTaskStorageDelete-157]
|
||||
_ = x[FnGetCurrentTaskBtf-158]
|
||||
_ = x[FnBprmOptsSet-159]
|
||||
_ = x[FnKtimeGetCoarseNs-160]
|
||||
_ = x[FnImaInodeHash-161]
|
||||
_ = x[FnSockFromFile-162]
|
||||
_ = x[FnCheckMtu-163]
|
||||
_ = x[FnForEachMapElem-164]
|
||||
_ = x[FnSnprintf-165]
|
||||
_ = x[FnSysBpf-166]
|
||||
_ = x[FnBtfFindByNameKind-167]
|
||||
_ = x[FnSysClose-168]
|
||||
_ = x[FnTimerInit-169]
|
||||
_ = x[FnTimerSetCallback-170]
|
||||
_ = x[FnTimerStart-171]
|
||||
_ = x[FnTimerCancel-172]
|
||||
_ = x[FnGetFuncIp-173]
|
||||
_ = x[FnGetAttachCookie-174]
|
||||
_ = x[FnTaskPtRegs-175]
|
||||
_ = x[FnGetBranchSnapshot-176]
|
||||
_ = x[FnTraceVprintk-177]
|
||||
_ = x[FnSkcToUnixSock-178]
|
||||
_ = x[FnKallsymsLookupName-179]
|
||||
_ = x[FnFindVma-180]
|
||||
_ = x[FnLoop-181]
|
||||
_ = x[FnStrncmp-182]
|
||||
_ = x[FnGetFuncArg-183]
|
||||
_ = x[FnGetFuncRet-184]
|
||||
_ = x[FnGetFuncArgCnt-185]
|
||||
_ = x[FnGetRetval-186]
|
||||
_ = x[FnSetRetval-187]
|
||||
_ = x[FnXdpGetBuffLen-188]
|
||||
_ = x[FnXdpLoadBytes-189]
|
||||
_ = x[FnXdpStoreBytes-190]
|
||||
_ = x[FnCopyFromUserTask-191]
|
||||
_ = x[FnSkbSetTstamp-192]
|
||||
_ = x[FnImaFileHash-193]
|
||||
_ = x[FnKptrXchg-194]
|
||||
_ = x[FnMapLookupPercpuElem-195]
|
||||
_ = x[FnSkcToMptcpSock-196]
|
||||
_ = x[FnDynptrFromMem-197]
|
||||
_ = x[FnRingbufReserveDynptr-198]
|
||||
_ = x[FnRingbufSubmitDynptr-199]
|
||||
_ = x[FnRingbufDiscardDynptr-200]
|
||||
_ = x[FnDynptrRead-201]
|
||||
_ = x[FnDynptrWrite-202]
|
||||
_ = x[FnDynptrData-203]
|
||||
_ = x[FnTcpRawGenSyncookieIpv4-204]
|
||||
_ = x[FnTcpRawGenSyncookieIpv6-205]
|
||||
_ = x[FnTcpRawCheckSyncookieIpv4-206]
|
||||
_ = x[FnTcpRawCheckSyncookieIpv6-207]
|
||||
_ = x[FnKtimeGetTaiNs-208]
|
||||
_ = x[FnUserRingbufDrain-209]
|
||||
_ = x[FnCgrpStorageGet-210]
|
||||
_ = x[FnCgrpStorageDelete-211]
|
||||
_ = x[maxBuiltinFunc-212]
|
||||
}
|
||||
|
||||
const _BuiltinFunc_name = "FnUnspecFnMapLookupElemFnMapUpdateElemFnMapDeleteElemFnProbeReadFnKtimeGetNsFnTracePrintkFnGetPrandomU32FnGetSmpProcessorIdFnSkbStoreBytesFnL3CsumReplaceFnL4CsumReplaceFnTailCallFnCloneRedirectFnGetCurrentPidTgidFnGetCurrentUidGidFnGetCurrentCommFnGetCgroupClassidFnSkbVlanPushFnSkbVlanPopFnSkbGetTunnelKeyFnSkbSetTunnelKeyFnPerfEventReadFnRedirectFnGetRouteRealmFnPerfEventOutputFnSkbLoadBytesFnGetStackidFnCsumDiffFnSkbGetTunnelOptFnSkbSetTunnelOptFnSkbChangeProtoFnSkbChangeTypeFnSkbUnderCgroupFnGetHashRecalcFnGetCurrentTaskFnProbeWriteUserFnCurrentTaskUnderCgroupFnSkbChangeTailFnSkbPullDataFnCsumUpdateFnSetHashInvalidFnGetNumaNodeIdFnSkbChangeHeadFnXdpAdjustHeadFnProbeReadStrFnGetSocketCookieFnGetSocketUidFnSetHashFnSetsockoptFnSkbAdjustRoomFnRedirectMapFnSkRedirectMapFnSockMapUpdateFnXdpAdjustMetaFnPerfEventReadValueFnPerfProgReadValueFnGetsockoptFnOverrideReturnFnSockOpsCbFlagsSetFnMsgRedirectMapFnMsgApplyBytesFnMsgCorkBytesFnMsgPullDataFnBindFnXdpAdjustTailFnSkbGetXfrmStateFnGetStackFnSkbLoadBytesRelativeFnFibLookupFnSockHashUpdateFnMsgRedirectHashFnSkRedirectHashFnLwtPushEncapFnLwtSeg6StoreBytesFnLwtSeg6AdjustSrhFnLwtSeg6ActionFnRcRepeatFnRcKeydownFnSkbCgroupIdFnGetCurrentCgroupIdFnGetLocalStorageFnSkSelectReuseportFnSkbAncestorCgroupIdFnSkLookupTcpFnSkLookupUdpFnSkReleaseFnMapPushElemFnMapPopElemFnMapPeekElemFnMsgPushDataFnMsgPopDataFnRcPointerRelFnSpinLockFnSpinUnlockFnSkFullsockFnTcpSockFnSkbEcnSetCeFnGetListenerSockFnSkcLookupTcpFnTcpCheckSyncookieFnSysctlGetNameFnSysctlGetCurrentValueFnSysctlGetNewValueFnSysctlSetNewValueFnStrtolFnStrtoulFnSkStorageGetFnSkStorageDeleteFnSendSignalFnTcpGenSyncookieFnSkbOutputFnProbeReadUserFnProbeReadKernelFnProbeReadUserStrFnProbeReadKernelStrFnTcpSendAckFnSendSignalThreadFnJiffies64FnReadBranchRecordsFnGetNsCurrentPidTgidFnXdpOutputFnGetNetnsCookieFnGetCurrentAncestorCgroupIdFnSkAssignFnKtimeGetBootNsFnSeqPrintfFnSeqWriteFnSkCgroupIdFnSkAncestorCgroupIdFnRingbufOutputFnRingbufReserveFnRingbufSubmitFnRingbufDiscardFnRingbufQueryFnCsumLevelFnSkcToTcp6SockFnSkcToTcpSockFnSkcToTcpTimewaitSockFnSkcToTcpRequestSockFnSkcToUdp6SockFnGetTaskStackFnLoadHdrOptFnStoreHdrOptFnReserveHdrOptFnInodeStorageGetFnInodeStorageDeleteFnDPathFnCopyFromUserFnSnprintfBtfFnSeqPrintfBtfFnSkbCgroupClassidFnRedirectNeighFnPerCpuPtrFnThisCpuPtrFnRedirectPeerFnTaskStorageGetFnTaskStorageDeleteFnGetCurrentTaskBtfFnBprmOptsSetFnKtimeGetCoarseNsFnImaInodeHashFnSockFromFileFnCheckMtuFnForEachMapElemFnSnprintfFnSysBpfFnBtfFindByNameKindFnSysCloseFnTimerInitFnTimerSetCallbackFnTimerStartFnTimerCancelFnGetFuncIpFnGetAttachCookieFnTaskPtRegsFnGetBranchSnapshotFnTraceVprintkFnSkcToUnixSockFnKallsymsLookupNameFnFindVmaFnLoopFnStrncmpFnGetFuncArgFnGetFuncRetFnGetFuncArgCntFnGetRetvalFnSetRetvalFnXdpGetBuffLenFnXdpLoadBytesFnXdpStoreBytesFnCopyFromUserTaskFnSkbSetTstampFnImaFileHashFnKptrXchgFnMapLookupPercpuElemFnSkcToMptcpSockFnDynptrFromMemFnRingbufReserveDynptrFnRingbufSubmitDynptrFnRingbufDiscardDynptrFnDynptrReadFnDynptrWriteFnDynptrDataFnTcpRawGenSyncookieIpv4FnTcpRawGenSyncookieIpv6FnTcpRawCheckSyncookieIpv4FnTcpRawCheckSyncookieIpv6FnKtimeGetTaiNsFnUserRingbufDrainFnCgrpStorageGetFnCgrpStorageDeletemaxBuiltinFunc"
|
||||
|
||||
var _BuiltinFunc_index = [...]uint16{0, 8, 23, 38, 53, 64, 76, 89, 104, 123, 138, 153, 168, 178, 193, 212, 230, 246, 264, 277, 289, 306, 323, 338, 348, 363, 380, 394, 406, 416, 433, 450, 466, 481, 497, 512, 528, 544, 568, 583, 596, 608, 624, 639, 654, 669, 683, 700, 714, 723, 735, 750, 763, 778, 793, 808, 828, 847, 859, 875, 894, 910, 925, 939, 952, 958, 973, 990, 1000, 1022, 1033, 1049, 1066, 1082, 1096, 1115, 1133, 1148, 1158, 1169, 1182, 1202, 1219, 1238, 1259, 1272, 1285, 1296, 1309, 1321, 1334, 1347, 1359, 1373, 1383, 1395, 1407, 1416, 1429, 1446, 1460, 1479, 1494, 1517, 1536, 1555, 1563, 1572, 1586, 1603, 1615, 1632, 1643, 1658, 1675, 1693, 1713, 1725, 1743, 1754, 1773, 1794, 1805, 1821, 1849, 1859, 1875, 1886, 1896, 1908, 1928, 1943, 1959, 1974, 1990, 2004, 2015, 2030, 2044, 2066, 2087, 2102, 2116, 2128, 2141, 2156, 2173, 2193, 2200, 2214, 2227, 2241, 2259, 2274, 2285, 2297, 2311, 2327, 2346, 2365, 2378, 2396, 2410, 2424, 2434, 2450, 2460, 2468, 2487, 2497, 2508, 2526, 2538, 2551, 2562, 2579, 2591, 2610, 2624, 2639, 2659, 2668, 2674, 2683, 2695, 2707, 2722, 2733, 2744, 2759, 2773, 2788, 2806, 2820, 2833, 2843, 2864, 2880, 2895, 2917, 2938, 2960, 2972, 2985, 2997, 3021, 3045, 3071, 3097, 3112, 3130, 3146, 3165, 3179}
|
||||
|
||||
func (i BuiltinFunc) String() string {
|
||||
if i < 0 || i >= BuiltinFunc(len(_BuiltinFunc_index)-1) {
|
||||
return "BuiltinFunc(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _BuiltinFunc_name[_BuiltinFunc_index[i]:_BuiltinFunc_index[i+1]]
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cilium/ebpf/internal/sys"
|
||||
"github.com/cilium/ebpf/internal/unix"
|
||||
)
|
||||
|
||||
// InstructionSize is the size of a BPF instruction in bytes
|
||||
const InstructionSize = 8
|
||||
|
||||
// RawInstructionOffset is an offset in units of raw BPF instructions.
|
||||
type RawInstructionOffset uint64
|
||||
|
||||
var ErrUnreferencedSymbol = errors.New("unreferenced symbol")
|
||||
var ErrUnsatisfiedMapReference = errors.New("unsatisfied map reference")
|
||||
var ErrUnsatisfiedProgramReference = errors.New("unsatisfied program reference")
|
||||
|
||||
// Bytes returns the offset of an instruction in bytes.
|
||||
func (rio RawInstructionOffset) Bytes() uint64 {
|
||||
return uint64(rio) * InstructionSize
|
||||
}
|
||||
|
||||
// Instruction is a single eBPF instruction.
|
||||
type Instruction struct {
|
||||
OpCode OpCode
|
||||
Dst Register
|
||||
Src Register
|
||||
Offset int16
|
||||
Constant int64
|
||||
|
||||
// Metadata contains optional metadata about this instruction.
|
||||
Metadata Metadata
|
||||
}
|
||||
|
||||
// Unmarshal decodes a BPF instruction.
|
||||
func (ins *Instruction) Unmarshal(r io.Reader, bo binary.ByteOrder) (uint64, error) {
|
||||
data := make([]byte, InstructionSize)
|
||||
if _, err := io.ReadFull(r, data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ins.OpCode = OpCode(data[0])
|
||||
|
||||
regs := data[1]
|
||||
switch bo {
|
||||
case binary.LittleEndian:
|
||||
ins.Dst, ins.Src = Register(regs&0xF), Register(regs>>4)
|
||||
case binary.BigEndian:
|
||||
ins.Dst, ins.Src = Register(regs>>4), Register(regs&0xf)
|
||||
}
|
||||
|
||||
ins.Offset = int16(bo.Uint16(data[2:4]))
|
||||
// Convert to int32 before widening to int64
|
||||
// to ensure the signed bit is carried over.
|
||||
ins.Constant = int64(int32(bo.Uint32(data[4:8])))
|
||||
|
||||
if !ins.OpCode.IsDWordLoad() {
|
||||
return InstructionSize, nil
|
||||
}
|
||||
|
||||
// Pull another instruction from the stream to retrieve the second
|
||||
// half of the 64-bit immediate value.
|
||||
if _, err := io.ReadFull(r, data); err != nil {
|
||||
// No Wrap, to avoid io.EOF clash
|
||||
return 0, errors.New("64bit immediate is missing second half")
|
||||
}
|
||||
|
||||
// Require that all fields other than the value are zero.
|
||||
if bo.Uint32(data[0:4]) != 0 {
|
||||
return 0, errors.New("64bit immediate has non-zero fields")
|
||||
}
|
||||
|
||||
cons1 := uint32(ins.Constant)
|
||||
cons2 := int32(bo.Uint32(data[4:8]))
|
||||
ins.Constant = int64(cons2)<<32 | int64(cons1)
|
||||
|
||||
return 2 * InstructionSize, nil
|
||||
}
|
||||
|
||||
// Marshal encodes a BPF instruction.
|
||||
func (ins Instruction) Marshal(w io.Writer, bo binary.ByteOrder) (uint64, error) {
|
||||
if ins.OpCode == InvalidOpCode {
|
||||
return 0, errors.New("invalid opcode")
|
||||
}
|
||||
|
||||
isDWordLoad := ins.OpCode.IsDWordLoad()
|
||||
|
||||
cons := int32(ins.Constant)
|
||||
if isDWordLoad {
|
||||
// Encode least significant 32bit first for 64bit operations.
|
||||
cons = int32(uint32(ins.Constant))
|
||||
}
|
||||
|
||||
regs, err := newBPFRegisters(ins.Dst, ins.Src, bo)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("can't marshal registers: %s", err)
|
||||
}
|
||||
|
||||
data := make([]byte, InstructionSize)
|
||||
data[0] = byte(ins.OpCode)
|
||||
data[1] = byte(regs)
|
||||
bo.PutUint16(data[2:4], uint16(ins.Offset))
|
||||
bo.PutUint32(data[4:8], uint32(cons))
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !isDWordLoad {
|
||||
return InstructionSize, nil
|
||||
}
|
||||
|
||||
// The first half of the second part of a double-wide instruction
|
||||
// must be zero. The second half carries the value.
|
||||
bo.PutUint32(data[0:4], 0)
|
||||
bo.PutUint32(data[4:8], uint32(ins.Constant>>32))
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return 2 * InstructionSize, nil
|
||||
}
|
||||
|
||||
// AssociateMap associates a Map with this Instruction.
|
||||
//
|
||||
// Implicitly clears the Instruction's Reference field.
|
||||
//
|
||||
// Returns an error if the Instruction is not a map load.
|
||||
func (ins *Instruction) AssociateMap(m FDer) error {
|
||||
if !ins.IsLoadFromMap() {
|
||||
return errors.New("not a load from a map")
|
||||
}
|
||||
|
||||
ins.Metadata.Set(referenceMeta{}, nil)
|
||||
ins.Metadata.Set(mapMeta{}, m)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RewriteMapPtr changes an instruction to use a new map fd.
|
||||
//
|
||||
// Returns an error if the instruction doesn't load a map.
|
||||
//
|
||||
// Deprecated: use AssociateMap instead. If you cannot provide a Map,
|
||||
// wrap an fd in a type implementing FDer.
|
||||
func (ins *Instruction) RewriteMapPtr(fd int) error {
|
||||
if !ins.IsLoadFromMap() {
|
||||
return errors.New("not a load from a map")
|
||||
}
|
||||
|
||||
ins.encodeMapFD(fd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ins *Instruction) encodeMapFD(fd int) {
|
||||
// Preserve the offset value for direct map loads.
|
||||
offset := uint64(ins.Constant) & (math.MaxUint32 << 32)
|
||||
rawFd := uint64(uint32(fd))
|
||||
ins.Constant = int64(offset | rawFd)
|
||||
}
|
||||
|
||||
// MapPtr returns the map fd for this instruction.
|
||||
//
|
||||
// The result is undefined if the instruction is not a load from a map,
|
||||
// see IsLoadFromMap.
|
||||
//
|
||||
// Deprecated: use Map() instead.
|
||||
func (ins *Instruction) MapPtr() int {
|
||||
// If there is a map associated with the instruction, return its FD.
|
||||
if fd := ins.Metadata.Get(mapMeta{}); fd != nil {
|
||||
return fd.(FDer).FD()
|
||||
}
|
||||
|
||||
// Fall back to the fd stored in the Constant field
|
||||
return ins.mapFd()
|
||||
}
|
||||
|
||||
// mapFd returns the map file descriptor stored in the 32 least significant
|
||||
// bits of ins' Constant field.
|
||||
func (ins *Instruction) mapFd() int {
|
||||
return int(int32(ins.Constant))
|
||||
}
|
||||
|
||||
// RewriteMapOffset changes the offset of a direct load from a map.
|
||||
//
|
||||
// Returns an error if the instruction is not a direct load.
|
||||
func (ins *Instruction) RewriteMapOffset(offset uint32) error {
|
||||
if !ins.OpCode.IsDWordLoad() {
|
||||
return fmt.Errorf("%s is not a 64 bit load", ins.OpCode)
|
||||
}
|
||||
|
||||
if ins.Src != PseudoMapValue {
|
||||
return errors.New("not a direct load from a map")
|
||||
}
|
||||
|
||||
fd := uint64(ins.Constant) & math.MaxUint32
|
||||
ins.Constant = int64(uint64(offset)<<32 | fd)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ins *Instruction) mapOffset() uint32 {
|
||||
return uint32(uint64(ins.Constant) >> 32)
|
||||
}
|
||||
|
||||
// IsLoadFromMap returns true if the instruction loads from a map.
|
||||
//
|
||||
// This covers both loading the map pointer and direct map value loads.
|
||||
func (ins *Instruction) IsLoadFromMap() bool {
|
||||
return ins.OpCode == LoadImmOp(DWord) && (ins.Src == PseudoMapFD || ins.Src == PseudoMapValue)
|
||||
}
|
||||
|
||||
// IsFunctionCall returns true if the instruction calls another BPF function.
|
||||
//
|
||||
// This is not the same thing as a BPF helper call.
|
||||
func (ins *Instruction) IsFunctionCall() bool {
|
||||
return ins.OpCode.JumpOp() == Call && ins.Src == PseudoCall
|
||||
}
|
||||
|
||||
// IsKfuncCall returns true if the instruction calls a kfunc.
|
||||
//
|
||||
// This is not the same thing as a BPF helper call.
|
||||
func (ins *Instruction) IsKfuncCall() bool {
|
||||
return ins.OpCode.JumpOp() == Call && ins.Src == PseudoKfuncCall
|
||||
}
|
||||
|
||||
// IsLoadOfFunctionPointer returns true if the instruction loads a function pointer.
|
||||
func (ins *Instruction) IsLoadOfFunctionPointer() bool {
|
||||
return ins.OpCode.IsDWordLoad() && ins.Src == PseudoFunc
|
||||
}
|
||||
|
||||
// IsFunctionReference returns true if the instruction references another BPF
|
||||
// function, either by invoking a Call jump operation or by loading a function
|
||||
// pointer.
|
||||
func (ins *Instruction) IsFunctionReference() bool {
|
||||
return ins.IsFunctionCall() || ins.IsLoadOfFunctionPointer()
|
||||
}
|
||||
|
||||
// IsBuiltinCall returns true if the instruction is a built-in call, i.e. BPF helper call.
|
||||
func (ins *Instruction) IsBuiltinCall() bool {
|
||||
return ins.OpCode.JumpOp() == Call && ins.Src == R0 && ins.Dst == R0
|
||||
}
|
||||
|
||||
// IsConstantLoad returns true if the instruction loads a constant of the
|
||||
// given size.
|
||||
func (ins *Instruction) IsConstantLoad(size Size) bool {
|
||||
return ins.OpCode == LoadImmOp(size) && ins.Src == R0 && ins.Offset == 0
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter.
|
||||
func (ins Instruction) Format(f fmt.State, c rune) {
|
||||
if c != 'v' {
|
||||
fmt.Fprintf(f, "{UNRECOGNIZED: %c}", c)
|
||||
return
|
||||
}
|
||||
|
||||
op := ins.OpCode
|
||||
|
||||
if op == InvalidOpCode {
|
||||
fmt.Fprint(f, "INVALID")
|
||||
return
|
||||
}
|
||||
|
||||
// Omit trailing space for Exit
|
||||
if op.JumpOp() == Exit {
|
||||
fmt.Fprint(f, op)
|
||||
return
|
||||
}
|
||||
|
||||
if ins.IsLoadFromMap() {
|
||||
fd := ins.mapFd()
|
||||
m := ins.Map()
|
||||
switch ins.Src {
|
||||
case PseudoMapFD:
|
||||
if m != nil {
|
||||
fmt.Fprintf(f, "LoadMapPtr dst: %s map: %s", ins.Dst, m)
|
||||
} else {
|
||||
fmt.Fprintf(f, "LoadMapPtr dst: %s fd: %d", ins.Dst, fd)
|
||||
}
|
||||
|
||||
case PseudoMapValue:
|
||||
if m != nil {
|
||||
fmt.Fprintf(f, "LoadMapValue dst: %s, map: %s off: %d", ins.Dst, m, ins.mapOffset())
|
||||
} else {
|
||||
fmt.Fprintf(f, "LoadMapValue dst: %s, fd: %d off: %d", ins.Dst, fd, ins.mapOffset())
|
||||
}
|
||||
}
|
||||
|
||||
goto ref
|
||||
}
|
||||
|
||||
fmt.Fprintf(f, "%v ", op)
|
||||
switch cls := op.Class(); {
|
||||
case cls.isLoadOrStore():
|
||||
switch op.Mode() {
|
||||
case ImmMode:
|
||||
fmt.Fprintf(f, "dst: %s imm: %d", ins.Dst, ins.Constant)
|
||||
case AbsMode:
|
||||
fmt.Fprintf(f, "imm: %d", ins.Constant)
|
||||
case IndMode:
|
||||
fmt.Fprintf(f, "dst: %s src: %s imm: %d", ins.Dst, ins.Src, ins.Constant)
|
||||
case MemMode:
|
||||
fmt.Fprintf(f, "dst: %s src: %s off: %d imm: %d", ins.Dst, ins.Src, ins.Offset, ins.Constant)
|
||||
case XAddMode:
|
||||
fmt.Fprintf(f, "dst: %s src: %s", ins.Dst, ins.Src)
|
||||
}
|
||||
|
||||
case cls.IsALU():
|
||||
fmt.Fprintf(f, "dst: %s ", ins.Dst)
|
||||
if op.ALUOp() == Swap || op.Source() == ImmSource {
|
||||
fmt.Fprintf(f, "imm: %d", ins.Constant)
|
||||
} else {
|
||||
fmt.Fprintf(f, "src: %s", ins.Src)
|
||||
}
|
||||
|
||||
case cls.IsJump():
|
||||
switch jop := op.JumpOp(); jop {
|
||||
case Call:
|
||||
switch ins.Src {
|
||||
case PseudoCall:
|
||||
// bpf-to-bpf call
|
||||
fmt.Fprint(f, ins.Constant)
|
||||
case PseudoKfuncCall:
|
||||
// kfunc call
|
||||
fmt.Fprintf(f, "Kfunc(%d)", ins.Constant)
|
||||
default:
|
||||
fmt.Fprint(f, BuiltinFunc(ins.Constant))
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Fprintf(f, "dst: %s off: %d ", ins.Dst, ins.Offset)
|
||||
if op.Source() == ImmSource {
|
||||
fmt.Fprintf(f, "imm: %d", ins.Constant)
|
||||
} else {
|
||||
fmt.Fprintf(f, "src: %s", ins.Src)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ref:
|
||||
if ins.Reference() != "" {
|
||||
fmt.Fprintf(f, " <%s>", ins.Reference())
|
||||
}
|
||||
}
|
||||
|
||||
func (ins Instruction) equal(other Instruction) bool {
|
||||
return ins.OpCode == other.OpCode &&
|
||||
ins.Dst == other.Dst &&
|
||||
ins.Src == other.Src &&
|
||||
ins.Offset == other.Offset &&
|
||||
ins.Constant == other.Constant
|
||||
}
|
||||
|
||||
// Size returns the amount of bytes ins would occupy in binary form.
|
||||
func (ins Instruction) Size() uint64 {
|
||||
return uint64(InstructionSize * ins.OpCode.rawInstructions())
|
||||
}
|
||||
|
||||
// WithMetadata sets the given Metadata on the Instruction. e.g. to copy
|
||||
// Metadata from another Instruction when replacing it.
|
||||
func (ins Instruction) WithMetadata(meta Metadata) Instruction {
|
||||
ins.Metadata = meta
|
||||
return ins
|
||||
}
|
||||
|
||||
type symbolMeta struct{}
|
||||
|
||||
// WithSymbol marks the Instruction as a Symbol, which other Instructions
|
||||
// can point to using corresponding calls to WithReference.
|
||||
func (ins Instruction) WithSymbol(name string) Instruction {
|
||||
ins.Metadata.Set(symbolMeta{}, name)
|
||||
return ins
|
||||
}
|
||||
|
||||
// Sym creates a symbol.
|
||||
//
|
||||
// Deprecated: use WithSymbol instead.
|
||||
func (ins Instruction) Sym(name string) Instruction {
|
||||
return ins.WithSymbol(name)
|
||||
}
|
||||
|
||||
// Symbol returns the value ins has been marked with using WithSymbol,
|
||||
// otherwise returns an empty string. A symbol is often an Instruction
|
||||
// at the start of a function body.
|
||||
func (ins Instruction) Symbol() string {
|
||||
sym, _ := ins.Metadata.Get(symbolMeta{}).(string)
|
||||
return sym
|
||||
}
|
||||
|
||||
type referenceMeta struct{}
|
||||
|
||||
// WithReference makes ins reference another Symbol or map by name.
|
||||
func (ins Instruction) WithReference(ref string) Instruction {
|
||||
ins.Metadata.Set(referenceMeta{}, ref)
|
||||
return ins
|
||||
}
|
||||
|
||||
// Reference returns the Symbol or map name referenced by ins, if any.
|
||||
func (ins Instruction) Reference() string {
|
||||
ref, _ := ins.Metadata.Get(referenceMeta{}).(string)
|
||||
return ref
|
||||
}
|
||||
|
||||
type mapMeta struct{}
|
||||
|
||||
// Map returns the Map referenced by ins, if any.
|
||||
// An Instruction will contain a Map if e.g. it references an existing,
|
||||
// pinned map that was opened during ELF loading.
|
||||
func (ins Instruction) Map() FDer {
|
||||
fd, _ := ins.Metadata.Get(mapMeta{}).(FDer)
|
||||
return fd
|
||||
}
|
||||
|
||||
type sourceMeta struct{}
|
||||
|
||||
// WithSource adds source information about the Instruction.
|
||||
func (ins Instruction) WithSource(src fmt.Stringer) Instruction {
|
||||
ins.Metadata.Set(sourceMeta{}, src)
|
||||
return ins
|
||||
}
|
||||
|
||||
// Source returns source information about the Instruction. The field is
|
||||
// present when the compiler emits BTF line info about the Instruction and
|
||||
// usually contains the line of source code responsible for it.
|
||||
func (ins Instruction) Source() fmt.Stringer {
|
||||
str, _ := ins.Metadata.Get(sourceMeta{}).(fmt.Stringer)
|
||||
return str
|
||||
}
|
||||
|
||||
// A Comment can be passed to Instruction.WithSource to add a comment
|
||||
// to an instruction.
|
||||
type Comment string
|
||||
|
||||
func (s Comment) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// FDer represents a resource tied to an underlying file descriptor.
|
||||
// Used as a stand-in for e.g. ebpf.Map since that type cannot be
|
||||
// imported here and FD() is the only method we rely on.
|
||||
type FDer interface {
|
||||
FD() int
|
||||
}
|
||||
|
||||
// Instructions is an eBPF program.
|
||||
type Instructions []Instruction
|
||||
|
||||
// Unmarshal unmarshals an Instructions from a binary instruction stream.
|
||||
// All instructions in insns are replaced by instructions decoded from r.
|
||||
func (insns *Instructions) Unmarshal(r io.Reader, bo binary.ByteOrder) error {
|
||||
if len(*insns) > 0 {
|
||||
*insns = nil
|
||||
}
|
||||
|
||||
var offset uint64
|
||||
for {
|
||||
var ins Instruction
|
||||
n, err := ins.Unmarshal(r, bo)
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("offset %d: %w", offset, err)
|
||||
}
|
||||
|
||||
*insns = append(*insns, ins)
|
||||
offset += n
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the name of the function insns belongs to, if any.
|
||||
func (insns Instructions) Name() string {
|
||||
if len(insns) == 0 {
|
||||
return ""
|
||||
}
|
||||
return insns[0].Symbol()
|
||||
}
|
||||
|
||||
func (insns Instructions) String() string {
|
||||
return fmt.Sprint(insns)
|
||||
}
|
||||
|
||||
// Size returns the amount of bytes insns would occupy in binary form.
|
||||
func (insns Instructions) Size() uint64 {
|
||||
var sum uint64
|
||||
for _, ins := range insns {
|
||||
sum += ins.Size()
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// AssociateMap updates all Instructions that Reference the given symbol
|
||||
// to point to an existing Map m instead.
|
||||
//
|
||||
// Returns ErrUnreferencedSymbol error if no references to symbol are found
|
||||
// in insns. If symbol is anything else than the symbol name of map (e.g.
|
||||
// a bpf2bpf subprogram), an error is returned.
|
||||
func (insns Instructions) AssociateMap(symbol string, m FDer) error {
|
||||
if symbol == "" {
|
||||
return errors.New("empty symbol")
|
||||
}
|
||||
|
||||
var found bool
|
||||
for i := range insns {
|
||||
ins := &insns[i]
|
||||
if ins.Reference() != symbol {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ins.AssociateMap(m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("symbol %s: %w", symbol, ErrUnreferencedSymbol)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RewriteMapPtr rewrites all loads of a specific map pointer to a new fd.
|
||||
//
|
||||
// Returns ErrUnreferencedSymbol if the symbol isn't used.
|
||||
//
|
||||
// Deprecated: use AssociateMap instead.
|
||||
func (insns Instructions) RewriteMapPtr(symbol string, fd int) error {
|
||||
if symbol == "" {
|
||||
return errors.New("empty symbol")
|
||||
}
|
||||
|
||||
var found bool
|
||||
for i := range insns {
|
||||
ins := &insns[i]
|
||||
if ins.Reference() != symbol {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ins.IsLoadFromMap() {
|
||||
return errors.New("not a load from a map")
|
||||
}
|
||||
|
||||
ins.encodeMapFD(fd)
|
||||
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("symbol %s: %w", symbol, ErrUnreferencedSymbol)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SymbolOffsets returns the set of symbols and their offset in
|
||||
// the instructions.
|
||||
func (insns Instructions) SymbolOffsets() (map[string]int, error) {
|
||||
offsets := make(map[string]int)
|
||||
|
||||
for i, ins := range insns {
|
||||
if ins.Symbol() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := offsets[ins.Symbol()]; ok {
|
||||
return nil, fmt.Errorf("duplicate symbol %s", ins.Symbol())
|
||||
}
|
||||
|
||||
offsets[ins.Symbol()] = i
|
||||
}
|
||||
|
||||
return offsets, nil
|
||||
}
|
||||
|
||||
// FunctionReferences returns a set of symbol names these Instructions make
|
||||
// bpf-to-bpf calls to.
|
||||
func (insns Instructions) FunctionReferences() []string {
|
||||
calls := make(map[string]struct{})
|
||||
for _, ins := range insns {
|
||||
if ins.Constant != -1 {
|
||||
// BPF-to-BPF calls have -1 constants.
|
||||
continue
|
||||
}
|
||||
|
||||
if ins.Reference() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ins.IsFunctionReference() {
|
||||
continue
|
||||
}
|
||||
|
||||
calls[ins.Reference()] = struct{}{}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(calls))
|
||||
for call := range calls {
|
||||
result = append(result, call)
|
||||
}
|
||||
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// ReferenceOffsets returns the set of references and their offset in
|
||||
// the instructions.
|
||||
func (insns Instructions) ReferenceOffsets() map[string][]int {
|
||||
offsets := make(map[string][]int)
|
||||
|
||||
for i, ins := range insns {
|
||||
if ins.Reference() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
offsets[ins.Reference()] = append(offsets[ins.Reference()], i)
|
||||
}
|
||||
|
||||
return offsets
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter.
|
||||
//
|
||||
// You can control indentation of symbols by
|
||||
// specifying a width. Setting a precision controls the indentation of
|
||||
// instructions.
|
||||
// The default character is a tab, which can be overridden by specifying
|
||||
// the ' ' space flag.
|
||||
func (insns Instructions) Format(f fmt.State, c rune) {
|
||||
if c != 's' && c != 'v' {
|
||||
fmt.Fprintf(f, "{UNKNOWN FORMAT '%c'}", c)
|
||||
return
|
||||
}
|
||||
|
||||
// Precision is better in this case, because it allows
|
||||
// specifying 0 padding easily.
|
||||
padding, ok := f.Precision()
|
||||
if !ok {
|
||||
padding = 1
|
||||
}
|
||||
|
||||
indent := strings.Repeat("\t", padding)
|
||||
if f.Flag(' ') {
|
||||
indent = strings.Repeat(" ", padding)
|
||||
}
|
||||
|
||||
symPadding, ok := f.Width()
|
||||
if !ok {
|
||||
symPadding = padding - 1
|
||||
}
|
||||
if symPadding < 0 {
|
||||
symPadding = 0
|
||||
}
|
||||
|
||||
symIndent := strings.Repeat("\t", symPadding)
|
||||
if f.Flag(' ') {
|
||||
symIndent = strings.Repeat(" ", symPadding)
|
||||
}
|
||||
|
||||
// Guess how many digits we need at most, by assuming that all instructions
|
||||
// are double wide.
|
||||
highestOffset := len(insns) * 2
|
||||
offsetWidth := int(math.Ceil(math.Log10(float64(highestOffset))))
|
||||
|
||||
iter := insns.Iterate()
|
||||
for iter.Next() {
|
||||
if iter.Ins.Symbol() != "" {
|
||||
fmt.Fprintf(f, "%s%s:\n", symIndent, iter.Ins.Symbol())
|
||||
}
|
||||
if src := iter.Ins.Source(); src != nil {
|
||||
line := strings.TrimSpace(src.String())
|
||||
if line != "" {
|
||||
fmt.Fprintf(f, "%s%*s; %s\n", indent, offsetWidth, " ", line)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(f, "%s%*d: %v\n", indent, offsetWidth, iter.Offset, iter.Ins)
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal encodes a BPF program into the kernel format.
|
||||
//
|
||||
// insns may be modified if there are unresolved jumps or bpf2bpf calls.
|
||||
//
|
||||
// Returns ErrUnsatisfiedProgramReference if there is a Reference Instruction
|
||||
// without a matching Symbol Instruction within insns.
|
||||
func (insns Instructions) Marshal(w io.Writer, bo binary.ByteOrder) error {
|
||||
if err := insns.encodeFunctionReferences(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := insns.encodeMapPointers(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, ins := range insns {
|
||||
if _, err := ins.Marshal(w, bo); err != nil {
|
||||
return fmt.Errorf("instruction %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tag calculates the kernel tag for a series of instructions.
|
||||
//
|
||||
// It mirrors bpf_prog_calc_tag in the kernel and so can be compared
|
||||
// to ProgramInfo.Tag to figure out whether a loaded program matches
|
||||
// certain instructions.
|
||||
func (insns Instructions) Tag(bo binary.ByteOrder) (string, error) {
|
||||
h := sha1.New()
|
||||
for i, ins := range insns {
|
||||
if ins.IsLoadFromMap() {
|
||||
ins.Constant = 0
|
||||
}
|
||||
_, err := ins.Marshal(h, bo)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("instruction %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)[:unix.BPF_TAG_SIZE]), nil
|
||||
}
|
||||
|
||||
// encodeFunctionReferences populates the Offset (or Constant, depending on
|
||||
// the instruction type) field of instructions with a Reference field to point
|
||||
// to the offset of the corresponding instruction with a matching Symbol field.
|
||||
//
|
||||
// Only Reference Instructions that are either jumps or BPF function references
|
||||
// (calls or function pointer loads) are populated.
|
||||
//
|
||||
// Returns ErrUnsatisfiedProgramReference if there is a Reference Instruction
|
||||
// without at least one corresponding Symbol Instruction within insns.
|
||||
func (insns Instructions) encodeFunctionReferences() error {
|
||||
// Index the offsets of instructions tagged as a symbol.
|
||||
symbolOffsets := make(map[string]RawInstructionOffset)
|
||||
iter := insns.Iterate()
|
||||
for iter.Next() {
|
||||
ins := iter.Ins
|
||||
|
||||
if ins.Symbol() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := symbolOffsets[ins.Symbol()]; ok {
|
||||
return fmt.Errorf("duplicate symbol %s", ins.Symbol())
|
||||
}
|
||||
|
||||
symbolOffsets[ins.Symbol()] = iter.Offset
|
||||
}
|
||||
|
||||
// Find all instructions tagged as references to other symbols.
|
||||
// Depending on the instruction type, populate their constant or offset
|
||||
// fields to point to the symbol they refer to within the insn stream.
|
||||
iter = insns.Iterate()
|
||||
for iter.Next() {
|
||||
i := iter.Index
|
||||
offset := iter.Offset
|
||||
ins := iter.Ins
|
||||
|
||||
if ins.Reference() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case ins.IsFunctionReference() && ins.Constant == -1:
|
||||
symOffset, ok := symbolOffsets[ins.Reference()]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference(), ErrUnsatisfiedProgramReference)
|
||||
}
|
||||
|
||||
ins.Constant = int64(symOffset - offset - 1)
|
||||
|
||||
case ins.OpCode.Class().IsJump() && ins.Offset == -1:
|
||||
symOffset, ok := symbolOffsets[ins.Reference()]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference(), ErrUnsatisfiedProgramReference)
|
||||
}
|
||||
|
||||
ins.Offset = int16(symOffset - offset - 1)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeMapPointers finds all Map Instructions and encodes their FDs
|
||||
// into their Constant fields.
|
||||
func (insns Instructions) encodeMapPointers() error {
|
||||
iter := insns.Iterate()
|
||||
for iter.Next() {
|
||||
ins := iter.Ins
|
||||
|
||||
if !ins.IsLoadFromMap() {
|
||||
continue
|
||||
}
|
||||
|
||||
m := ins.Map()
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fd := m.FD()
|
||||
if fd < 0 {
|
||||
return fmt.Errorf("map %s: %w", m, sys.ErrClosedFd)
|
||||
}
|
||||
|
||||
ins.encodeMapFD(m.FD())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Iterate allows iterating a BPF program while keeping track of
|
||||
// various offsets.
|
||||
//
|
||||
// Modifying the instruction slice will lead to undefined behaviour.
|
||||
func (insns Instructions) Iterate() *InstructionIterator {
|
||||
return &InstructionIterator{insns: insns}
|
||||
}
|
||||
|
||||
// InstructionIterator iterates over a BPF program.
|
||||
type InstructionIterator struct {
|
||||
insns Instructions
|
||||
// The instruction in question.
|
||||
Ins *Instruction
|
||||
// The index of the instruction in the original instruction slice.
|
||||
Index int
|
||||
// The offset of the instruction in raw BPF instructions. This accounts
|
||||
// for double-wide instructions.
|
||||
Offset RawInstructionOffset
|
||||
}
|
||||
|
||||
// Next returns true as long as there are any instructions remaining.
|
||||
func (iter *InstructionIterator) Next() bool {
|
||||
if len(iter.insns) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if iter.Ins != nil {
|
||||
iter.Index++
|
||||
iter.Offset += RawInstructionOffset(iter.Ins.OpCode.rawInstructions())
|
||||
}
|
||||
iter.Ins = &iter.insns[0]
|
||||
iter.insns = iter.insns[1:]
|
||||
return true
|
||||
}
|
||||
|
||||
type bpfRegisters uint8
|
||||
|
||||
func newBPFRegisters(dst, src Register, bo binary.ByteOrder) (bpfRegisters, error) {
|
||||
switch bo {
|
||||
case binary.LittleEndian:
|
||||
return bpfRegisters((src << 4) | (dst & 0xF)), nil
|
||||
case binary.BigEndian:
|
||||
return bpfRegisters((dst << 4) | (src & 0xF)), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unrecognized ByteOrder %T", bo)
|
||||
}
|
||||
}
|
||||
|
||||
// IsUnreferencedSymbol returns true if err was caused by
|
||||
// an unreferenced symbol.
|
||||
//
|
||||
// Deprecated: use errors.Is(err, asm.ErrUnreferencedSymbol).
|
||||
func IsUnreferencedSymbol(err error) bool {
|
||||
return errors.Is(err, ErrUnreferencedSymbol)
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
var test64bitImmProg = []byte{
|
||||
// r0 = math.MinInt32 - 1
|
||||
0x18, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x7f,
|
||||
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
|
||||
}
|
||||
|
||||
func TestRead64bitImmediate(t *testing.T) {
|
||||
var ins Instruction
|
||||
n, err := ins.Unmarshal(bytes.NewReader(test64bitImmProg), binary.LittleEndian)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := uint64(InstructionSize * 2); n != want {
|
||||
t.Errorf("Expected %d bytes to be read, got %d", want, n)
|
||||
}
|
||||
|
||||
if c := ins.Constant; c != math.MinInt32-1 {
|
||||
t.Errorf("Expected immediate to be %v, got %v", int64(math.MinInt32)-1, c)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRead64bitImmediate(b *testing.B) {
|
||||
r := &bytes.Reader{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.Reset(test64bitImmProg)
|
||||
|
||||
var ins Instruction
|
||||
if _, err := ins.Unmarshal(r, binary.LittleEndian); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite64bitImmediate(t *testing.T) {
|
||||
insns := Instructions{
|
||||
LoadImm(R0, math.MinInt32-1, DWord),
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := insns.Marshal(&buf, binary.LittleEndian); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if prog := buf.Bytes(); !bytes.Equal(prog, test64bitImmProg) {
|
||||
t.Errorf("Marshalled program does not match:\n%s", hex.Dump(prog))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWrite64BitImmediate(b *testing.B) {
|
||||
ins := LoadImm(R0, math.MinInt32-1, DWord)
|
||||
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
|
||||
if _, err := ins.Marshal(&buf, binary.LittleEndian); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalInstructions(t *testing.T) {
|
||||
r := bytes.NewReader(test64bitImmProg)
|
||||
|
||||
var insns Instructions
|
||||
if err := insns.Unmarshal(r, binary.LittleEndian); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Unmarshaling into the same Instructions multiple times replaces
|
||||
// the instruction stream.
|
||||
r.Reset(test64bitImmProg)
|
||||
if err := insns.Unmarshal(r, binary.LittleEndian); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(insns) != 1 {
|
||||
t.Fatalf("Expected one instruction, got %d", len(insns))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedJump(t *testing.T) {
|
||||
insns := Instructions{
|
||||
JSGT.Imm(R0, -1, "foo"),
|
||||
}
|
||||
|
||||
insns[0].Offset = 1
|
||||
|
||||
err := insns.Marshal(io.Discard, binary.LittleEndian)
|
||||
if err != nil {
|
||||
t.Error("Can't marshal signed jump:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionRewriteMapConstant(t *testing.T) {
|
||||
ins := LoadMapValue(R0, 123, 321)
|
||||
|
||||
qt.Assert(t, ins.MapPtr(), qt.Equals, 123)
|
||||
qt.Assert(t, ins.mapOffset(), qt.Equals, uint32(321))
|
||||
|
||||
qt.Assert(t, ins.RewriteMapPtr(-1), qt.IsNil)
|
||||
qt.Assert(t, ins.MapPtr(), qt.Equals, -1)
|
||||
|
||||
qt.Assert(t, ins.RewriteMapPtr(1), qt.IsNil)
|
||||
qt.Assert(t, ins.MapPtr(), qt.Equals, 1)
|
||||
|
||||
// mapOffset should be unchanged after rewriting the pointer.
|
||||
qt.Assert(t, ins.mapOffset(), qt.Equals, uint32(321))
|
||||
|
||||
qt.Assert(t, ins.RewriteMapOffset(123), qt.IsNil)
|
||||
qt.Assert(t, ins.mapOffset(), qt.Equals, uint32(123))
|
||||
|
||||
// MapPtr should be unchanged.
|
||||
qt.Assert(t, ins.MapPtr(), qt.Equals, 1)
|
||||
|
||||
ins = Mov.Imm(R1, 32)
|
||||
if err := ins.RewriteMapPtr(1); err == nil {
|
||||
t.Error("RewriteMapPtr rewriting bogus instruction")
|
||||
}
|
||||
if err := ins.RewriteMapOffset(1); err == nil {
|
||||
t.Error("RewriteMapOffset rewriting bogus instruction")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionLoadMapValue(t *testing.T) {
|
||||
ins := LoadMapValue(R0, 1, 123)
|
||||
if !ins.IsLoadFromMap() {
|
||||
t.Error("isLoadFromMap returns false")
|
||||
}
|
||||
if fd := ins.mapFd(); fd != 1 {
|
||||
t.Error("Expected map fd to be 1, got", fd)
|
||||
}
|
||||
if off := ins.mapOffset(); off != 123 {
|
||||
t.Fatal("Expected map offset to be 123 after changin the pointer, got", off)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionsRewriteMapPtr(t *testing.T) {
|
||||
insns := Instructions{
|
||||
LoadMapPtr(R1, 0).WithReference("good"),
|
||||
Return(),
|
||||
}
|
||||
|
||||
if err := insns.RewriteMapPtr("good", 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if insns[0].Constant != 1 {
|
||||
t.Error("Constant should be 1, have", insns[0].Constant)
|
||||
}
|
||||
|
||||
if err := insns.RewriteMapPtr("good", 2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if insns[0].Constant != 2 {
|
||||
t.Error("Constant should be 2, have", insns[0].Constant)
|
||||
}
|
||||
|
||||
if err := insns.RewriteMapPtr("bad", 1); !errors.Is(err, ErrUnreferencedSymbol) {
|
||||
t.Error("Rewriting unreferenced map doesn't return appropriate error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionWithMetadata(t *testing.T) {
|
||||
ins := LoadImm(R0, 123, DWord).WithSymbol("abc")
|
||||
ins2 := LoadImm(R0, 567, DWord).WithMetadata(ins.Metadata)
|
||||
|
||||
if want, got := "abc", ins2.Symbol(); want != got {
|
||||
t.Fatalf("unexpected Symbol value on ins2: want: %s, got: %s", want, got)
|
||||
}
|
||||
|
||||
if want, got := ins.Metadata, ins2.Metadata; want != got {
|
||||
t.Fatal("expected ins and isn2 Metadata to match")
|
||||
}
|
||||
}
|
||||
|
||||
// You can use format flags to change the way an eBPF
|
||||
// program is stringified.
|
||||
func ExampleInstructions_Format() {
|
||||
|
||||
insns := Instructions{
|
||||
FnMapLookupElem.Call().WithSymbol("my_func").WithSource(Comment("bpf_map_lookup_elem()")),
|
||||
LoadImm(R0, 42, DWord).WithSource(Comment("abc = 42")),
|
||||
Return(),
|
||||
}
|
||||
|
||||
fmt.Println("Default format:")
|
||||
fmt.Printf("%v\n", insns)
|
||||
|
||||
fmt.Println("Don't indent instructions:")
|
||||
fmt.Printf("%.0v\n", insns)
|
||||
|
||||
fmt.Println("Indent using spaces:")
|
||||
fmt.Printf("% v\n", insns)
|
||||
|
||||
fmt.Println("Control symbol indentation:")
|
||||
fmt.Printf("%2v\n", insns)
|
||||
|
||||
// Output: Default format:
|
||||
// my_func:
|
||||
// ; bpf_map_lookup_elem()
|
||||
// 0: Call FnMapLookupElem
|
||||
// ; abc = 42
|
||||
// 1: LdImmDW dst: r0 imm: 42
|
||||
// 3: Exit
|
||||
//
|
||||
// Don't indent instructions:
|
||||
// my_func:
|
||||
// ; bpf_map_lookup_elem()
|
||||
// 0: Call FnMapLookupElem
|
||||
// ; abc = 42
|
||||
// 1: LdImmDW dst: r0 imm: 42
|
||||
// 3: Exit
|
||||
//
|
||||
// Indent using spaces:
|
||||
// my_func:
|
||||
// ; bpf_map_lookup_elem()
|
||||
// 0: Call FnMapLookupElem
|
||||
// ; abc = 42
|
||||
// 1: LdImmDW dst: r0 imm: 42
|
||||
// 3: Exit
|
||||
//
|
||||
// Control symbol indentation:
|
||||
// my_func:
|
||||
// ; bpf_map_lookup_elem()
|
||||
// 0: Call FnMapLookupElem
|
||||
// ; abc = 42
|
||||
// 1: LdImmDW dst: r0 imm: 42
|
||||
// 3: Exit
|
||||
}
|
||||
|
||||
func TestReadSrcDst(t *testing.T) {
|
||||
testSrcDstProg := []byte{
|
||||
// on little-endian: r0 = r1
|
||||
// on big-endian: be: r1 = r0
|
||||
0xbf, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
bo binary.ByteOrder
|
||||
dst, src Register
|
||||
}{
|
||||
{binary.BigEndian, R1, R0},
|
||||
{binary.LittleEndian, R0, R1},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.bo.String(), func(t *testing.T) {
|
||||
var ins Instruction
|
||||
_, err := ins.Unmarshal(bytes.NewReader(testSrcDstProg), tc.bo)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ins.Dst != tc.dst {
|
||||
t.Errorf("Expected destination to be %v, got %v", tc.dst, ins.Dst)
|
||||
}
|
||||
if ins.Src != tc.src {
|
||||
t.Errorf("Expected source to be %v, got %v", tc.src, ins.Src)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstructionIterator(t *testing.T) {
|
||||
insns := Instructions{
|
||||
LoadImm(R0, 0, Word),
|
||||
LoadImm(R0, 0, DWord),
|
||||
Return(),
|
||||
}
|
||||
offsets := []RawInstructionOffset{0, 1, 3}
|
||||
|
||||
iter := insns.Iterate()
|
||||
for i := 0; i < len(insns); i++ {
|
||||
if !iter.Next() {
|
||||
t.Fatalf("Expected %dth call to Next to return true", i)
|
||||
}
|
||||
|
||||
if iter.Ins == nil {
|
||||
t.Errorf("Expected iter.Ins to be non-nil")
|
||||
}
|
||||
if iter.Index != i {
|
||||
t.Errorf("Expected iter.Index to be %d, got %d", i, iter.Index)
|
||||
}
|
||||
if iter.Offset != offsets[i] {
|
||||
t.Errorf("Expected iter.Offset to be %d, got %d", offsets[i], iter.Offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataCopyOnWrite(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
// Setting metadata should copy Instruction and modify the metadata pointer
|
||||
// of the new object without touching the old Instruction.
|
||||
|
||||
// Reference
|
||||
ins := Ja.Label("my_func")
|
||||
ins2 := ins.WithReference("my_func2")
|
||||
|
||||
c.Assert(ins.Reference(), qt.Equals, "my_func", qt.Commentf("WithReference updated ins"))
|
||||
c.Assert(ins2.Reference(), qt.Equals, "my_func2", qt.Commentf("WithReference didn't update ins2"))
|
||||
|
||||
// Symbol
|
||||
ins = Ja.Label("").WithSymbol("my_sym")
|
||||
ins2 = ins.WithSymbol("my_sym2")
|
||||
|
||||
c.Assert(ins.Symbol(), qt.Equals, "my_sym", qt.Commentf("WithSymbol updated ins"))
|
||||
c.Assert(ins2.Symbol(), qt.Equals, "my_sym2", qt.Commentf("WithSymbol didn't update ins2"))
|
||||
|
||||
// Map
|
||||
ins = LoadMapPtr(R1, 0)
|
||||
ins2 = ins
|
||||
|
||||
testMap := testFDer(1)
|
||||
c.Assert(ins2.AssociateMap(testMap), qt.IsNil, qt.Commentf("failed to associate map with ins2"))
|
||||
|
||||
c.Assert(ins.Map(), qt.IsNil, qt.Commentf("AssociateMap updated ins"))
|
||||
c.Assert(ins2.Map(), qt.Equals, testMap, qt.Commentf("AssociateMap didn't update ins2"))
|
||||
}
|
||||
|
||||
type testFDer int
|
||||
|
||||
func (t testFDer) FD() int {
|
||||
return int(t)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package asm
|
||||
|
||||
//go:generate stringer -output jump_string.go -type=JumpOp
|
||||
|
||||
// JumpOp affect control flow.
|
||||
//
|
||||
// msb lsb
|
||||
// +----+-+---+
|
||||
// |OP |s|cls|
|
||||
// +----+-+---+
|
||||
type JumpOp uint8
|
||||
|
||||
const jumpMask OpCode = aluMask
|
||||
|
||||
const (
|
||||
// InvalidJumpOp is returned by getters when invoked
|
||||
// on non branch OpCodes
|
||||
InvalidJumpOp JumpOp = 0xff
|
||||
// Ja jumps by offset unconditionally
|
||||
Ja JumpOp = 0x00
|
||||
// JEq jumps by offset if r == imm
|
||||
JEq JumpOp = 0x10
|
||||
// JGT jumps by offset if r > imm
|
||||
JGT JumpOp = 0x20
|
||||
// JGE jumps by offset if r >= imm
|
||||
JGE JumpOp = 0x30
|
||||
// JSet jumps by offset if r & imm
|
||||
JSet JumpOp = 0x40
|
||||
// JNE jumps by offset if r != imm
|
||||
JNE JumpOp = 0x50
|
||||
// JSGT jumps by offset if signed r > signed imm
|
||||
JSGT JumpOp = 0x60
|
||||
// JSGE jumps by offset if signed r >= signed imm
|
||||
JSGE JumpOp = 0x70
|
||||
// Call builtin or user defined function from imm
|
||||
Call JumpOp = 0x80
|
||||
// Exit ends execution, with value in r0
|
||||
Exit JumpOp = 0x90
|
||||
// JLT jumps by offset if r < imm
|
||||
JLT JumpOp = 0xa0
|
||||
// JLE jumps by offset if r <= imm
|
||||
JLE JumpOp = 0xb0
|
||||
// JSLT jumps by offset if signed r < signed imm
|
||||
JSLT JumpOp = 0xc0
|
||||
// JSLE jumps by offset if signed r <= signed imm
|
||||
JSLE JumpOp = 0xd0
|
||||
)
|
||||
|
||||
// Return emits an exit instruction.
|
||||
//
|
||||
// Requires a return value in R0.
|
||||
func Return() Instruction {
|
||||
return Instruction{
|
||||
OpCode: OpCode(JumpClass).SetJumpOp(Exit),
|
||||
}
|
||||
}
|
||||
|
||||
// Op returns the OpCode for a given jump source.
|
||||
func (op JumpOp) Op(source Source) OpCode {
|
||||
return OpCode(JumpClass).SetJumpOp(op).SetSource(source)
|
||||
}
|
||||
|
||||
// Imm compares 64 bit dst to 64 bit value (sign extended), and adjusts PC by offset if the condition is fulfilled.
|
||||
func (op JumpOp) Imm(dst Register, value int32, label string) Instruction {
|
||||
return Instruction{
|
||||
OpCode: op.opCode(JumpClass, ImmSource),
|
||||
Dst: dst,
|
||||
Offset: -1,
|
||||
Constant: int64(value),
|
||||
}.WithReference(label)
|
||||
}
|
||||
|
||||
// Imm32 compares 32 bit dst to 32 bit value, and adjusts PC by offset if the condition is fulfilled.
|
||||
// Requires kernel 5.1.
|
||||
func (op JumpOp) Imm32(dst Register, value int32, label string) Instruction {
|
||||
return Instruction{
|
||||
OpCode: op.opCode(Jump32Class, ImmSource),
|
||||
Dst: dst,
|
||||
Offset: -1,
|
||||
Constant: int64(value),
|
||||
}.WithReference(label)
|
||||
}
|
||||
|
||||
// Reg compares 64 bit dst to 64 bit src, and adjusts PC by offset if the condition is fulfilled.
|
||||
func (op JumpOp) Reg(dst, src Register, label string) Instruction {
|
||||
return Instruction{
|
||||
OpCode: op.opCode(JumpClass, RegSource),
|
||||
Dst: dst,
|
||||
Src: src,
|
||||
Offset: -1,
|
||||
}.WithReference(label)
|
||||
}
|
||||
|
||||
// Reg32 compares 32 bit dst to 32 bit src, and adjusts PC by offset if the condition is fulfilled.
|
||||
// Requires kernel 5.1.
|
||||
func (op JumpOp) Reg32(dst, src Register, label string) Instruction {
|
||||
return Instruction{
|
||||
OpCode: op.opCode(Jump32Class, RegSource),
|
||||
Dst: dst,
|
||||
Src: src,
|
||||
Offset: -1,
|
||||
}.WithReference(label)
|
||||
}
|
||||
|
||||
func (op JumpOp) opCode(class Class, source Source) OpCode {
|
||||
if op == Exit || op == Call || op == Ja {
|
||||
return InvalidOpCode
|
||||
}
|
||||
|
||||
return OpCode(class).SetJumpOp(op).SetSource(source)
|
||||
}
|
||||
|
||||
// Label adjusts PC to the address of the label.
|
||||
func (op JumpOp) Label(label string) Instruction {
|
||||
if op == Call {
|
||||
return Instruction{
|
||||
OpCode: OpCode(JumpClass).SetJumpOp(Call),
|
||||
Src: PseudoCall,
|
||||
Constant: -1,
|
||||
}.WithReference(label)
|
||||
}
|
||||
|
||||
return Instruction{
|
||||
OpCode: OpCode(JumpClass).SetJumpOp(op),
|
||||
Offset: -1,
|
||||
}.WithReference(label)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Code generated by "stringer -output jump_string.go -type=JumpOp"; DO NOT EDIT.
|
||||
|
||||
package asm
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[InvalidJumpOp-255]
|
||||
_ = x[Ja-0]
|
||||
_ = x[JEq-16]
|
||||
_ = x[JGT-32]
|
||||
_ = x[JGE-48]
|
||||
_ = x[JSet-64]
|
||||
_ = x[JNE-80]
|
||||
_ = x[JSGT-96]
|
||||
_ = x[JSGE-112]
|
||||
_ = x[Call-128]
|
||||
_ = x[Exit-144]
|
||||
_ = x[JLT-160]
|
||||
_ = x[JLE-176]
|
||||
_ = x[JSLT-192]
|
||||
_ = x[JSLE-208]
|
||||
}
|
||||
|
||||
const _JumpOp_name = "JaJEqJGTJGEJSetJNEJSGTJSGECallExitJLTJLEJSLTJSLEInvalidJumpOp"
|
||||
|
||||
var _JumpOp_map = map[JumpOp]string{
|
||||
0: _JumpOp_name[0:2],
|
||||
16: _JumpOp_name[2:5],
|
||||
32: _JumpOp_name[5:8],
|
||||
48: _JumpOp_name[8:11],
|
||||
64: _JumpOp_name[11:15],
|
||||
80: _JumpOp_name[15:18],
|
||||
96: _JumpOp_name[18:22],
|
||||
112: _JumpOp_name[22:26],
|
||||
128: _JumpOp_name[26:30],
|
||||
144: _JumpOp_name[30:34],
|
||||
160: _JumpOp_name[34:37],
|
||||
176: _JumpOp_name[37:40],
|
||||
192: _JumpOp_name[40:44],
|
||||
208: _JumpOp_name[44:48],
|
||||
255: _JumpOp_name[48:61],
|
||||
}
|
||||
|
||||
func (i JumpOp) String() string {
|
||||
if str, ok := _JumpOp_map[i]; ok {
|
||||
return str
|
||||
}
|
||||
return "JumpOp(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package asm
|
||||
|
||||
//go:generate stringer -output load_store_string.go -type=Mode,Size
|
||||
|
||||
// Mode for load and store operations
|
||||
//
|
||||
// msb lsb
|
||||
// +---+--+---+
|
||||
// |MDE|sz|cls|
|
||||
// +---+--+---+
|
||||
type Mode uint8
|
||||
|
||||
const modeMask OpCode = 0xe0
|
||||
|
||||
const (
|
||||
// InvalidMode is returned by getters when invoked
|
||||
// on non load / store OpCodes
|
||||
InvalidMode Mode = 0xff
|
||||
// ImmMode - immediate value
|
||||
ImmMode Mode = 0x00
|
||||
// AbsMode - immediate value + offset
|
||||
AbsMode Mode = 0x20
|
||||
// IndMode - indirect (imm+src)
|
||||
IndMode Mode = 0x40
|
||||
// MemMode - load from memory
|
||||
MemMode Mode = 0x60
|
||||
// XAddMode - add atomically across processors.
|
||||
XAddMode Mode = 0xc0
|
||||
)
|
||||
|
||||
// Size of load and store operations
|
||||
//
|
||||
// msb lsb
|
||||
// +---+--+---+
|
||||
// |mde|SZ|cls|
|
||||
// +---+--+---+
|
||||
type Size uint8
|
||||
|
||||
const sizeMask OpCode = 0x18
|
||||
|
||||
const (
|
||||
// InvalidSize is returned by getters when invoked
|
||||
// on non load / store OpCodes
|
||||
InvalidSize Size = 0xff
|
||||
// DWord - double word; 64 bits
|
||||
DWord Size = 0x18
|
||||
// Word - word; 32 bits
|
||||
Word Size = 0x00
|
||||
// Half - half-word; 16 bits
|
||||
Half Size = 0x08
|
||||
// Byte - byte; 8 bits
|
||||
Byte Size = 0x10
|
||||
)
|
||||
|
||||
// Sizeof returns the size in bytes.
|
||||
func (s Size) Sizeof() int {
|
||||
switch s {
|
||||
case DWord:
|
||||
return 8
|
||||
case Word:
|
||||
return 4
|
||||
case Half:
|
||||
return 2
|
||||
case Byte:
|
||||
return 1
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// LoadMemOp returns the OpCode to load a value of given size from memory.
|
||||
func LoadMemOp(size Size) OpCode {
|
||||
return OpCode(LdXClass).SetMode(MemMode).SetSize(size)
|
||||
}
|
||||
|
||||
// LoadMem emits `dst = *(size *)(src + offset)`.
|
||||
func LoadMem(dst, src Register, offset int16, size Size) Instruction {
|
||||
return Instruction{
|
||||
OpCode: LoadMemOp(size),
|
||||
Dst: dst,
|
||||
Src: src,
|
||||
Offset: offset,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadImmOp returns the OpCode to load an immediate of given size.
|
||||
//
|
||||
// As of kernel 4.20, only DWord size is accepted.
|
||||
func LoadImmOp(size Size) OpCode {
|
||||
return OpCode(LdClass).SetMode(ImmMode).SetSize(size)
|
||||
}
|
||||
|
||||
// LoadImm emits `dst = (size)value`.
|
||||
//
|
||||
// As of kernel 4.20, only DWord size is accepted.
|
||||
func LoadImm(dst Register, value int64, size Size) Instruction {
|
||||
return Instruction{
|
||||
OpCode: LoadImmOp(size),
|
||||
Dst: dst,
|
||||
Constant: value,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadMapPtr stores a pointer to a map in dst.
|
||||
func LoadMapPtr(dst Register, fd int) Instruction {
|
||||
if fd < 0 {
|
||||
return Instruction{OpCode: InvalidOpCode}
|
||||
}
|
||||
|
||||
return Instruction{
|
||||
OpCode: LoadImmOp(DWord),
|
||||
Dst: dst,
|
||||
Src: PseudoMapFD,
|
||||
Constant: int64(uint32(fd)),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadMapValue stores a pointer to the value at a certain offset of a map.
|
||||
func LoadMapValue(dst Register, fd int, offset uint32) Instruction {
|
||||
if fd < 0 {
|
||||
return Instruction{OpCode: InvalidOpCode}
|
||||
}
|
||||
|
||||
fdAndOffset := (uint64(offset) << 32) | uint64(uint32(fd))
|
||||
return Instruction{
|
||||
OpCode: LoadImmOp(DWord),
|
||||
Dst: dst,
|
||||
Src: PseudoMapValue,
|
||||
Constant: int64(fdAndOffset),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadIndOp returns the OpCode for loading a value of given size from an sk_buff.
|
||||
func LoadIndOp(size Size) OpCode {
|
||||
return OpCode(LdClass).SetMode(IndMode).SetSize(size)
|
||||
}
|
||||
|
||||
// LoadInd emits `dst = ntoh(*(size *)(((sk_buff *)R6)->data + src + offset))`.
|
||||
func LoadInd(dst, src Register, offset int32, size Size) Instruction {
|
||||
return Instruction{
|
||||
OpCode: LoadIndOp(size),
|
||||
Dst: dst,
|
||||
Src: src,
|
||||
Constant: int64(offset),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAbsOp returns the OpCode for loading a value of given size from an sk_buff.
|
||||
func LoadAbsOp(size Size) OpCode {
|
||||
return OpCode(LdClass).SetMode(AbsMode).SetSize(size)
|
||||
}
|
||||
|
||||
// LoadAbs emits `r0 = ntoh(*(size *)(((sk_buff *)R6)->data + offset))`.
|
||||
func LoadAbs(offset int32, size Size) Instruction {
|
||||
return Instruction{
|
||||
OpCode: LoadAbsOp(size),
|
||||
Dst: R0,
|
||||
Constant: int64(offset),
|
||||
}
|
||||
}
|
||||
|
||||
// StoreMemOp returns the OpCode for storing a register of given size in memory.
|
||||
func StoreMemOp(size Size) OpCode {
|
||||
return OpCode(StXClass).SetMode(MemMode).SetSize(size)
|
||||
}
|
||||
|
||||
// StoreMem emits `*(size *)(dst + offset) = src`
|
||||
func StoreMem(dst Register, offset int16, src Register, size Size) Instruction {
|
||||
return Instruction{
|
||||
OpCode: StoreMemOp(size),
|
||||
Dst: dst,
|
||||
Src: src,
|
||||
Offset: offset,
|
||||
}
|
||||
}
|
||||
|
||||
// StoreImmOp returns the OpCode for storing an immediate of given size in memory.
|
||||
func StoreImmOp(size Size) OpCode {
|
||||
return OpCode(StClass).SetMode(MemMode).SetSize(size)
|
||||
}
|
||||
|
||||
// StoreImm emits `*(size *)(dst + offset) = value`.
|
||||
func StoreImm(dst Register, offset int16, value int64, size Size) Instruction {
|
||||
return Instruction{
|
||||
OpCode: StoreImmOp(size),
|
||||
Dst: dst,
|
||||
Offset: offset,
|
||||
Constant: value,
|
||||
}
|
||||
}
|
||||
|
||||
// StoreXAddOp returns the OpCode to atomically add a register to a value in memory.
|
||||
func StoreXAddOp(size Size) OpCode {
|
||||
return OpCode(StXClass).SetMode(XAddMode).SetSize(size)
|
||||
}
|
||||
|
||||
// StoreXAdd atomically adds src to *dst.
|
||||
func StoreXAdd(dst, src Register, size Size) Instruction {
|
||||
return Instruction{
|
||||
OpCode: StoreXAddOp(size),
|
||||
Dst: dst,
|
||||
Src: src,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Code generated by "stringer -output load_store_string.go -type=Mode,Size"; DO NOT EDIT.
|
||||
|
||||
package asm
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[InvalidMode-255]
|
||||
_ = x[ImmMode-0]
|
||||
_ = x[AbsMode-32]
|
||||
_ = x[IndMode-64]
|
||||
_ = x[MemMode-96]
|
||||
_ = x[XAddMode-192]
|
||||
}
|
||||
|
||||
const (
|
||||
_Mode_name_0 = "ImmMode"
|
||||
_Mode_name_1 = "AbsMode"
|
||||
_Mode_name_2 = "IndMode"
|
||||
_Mode_name_3 = "MemMode"
|
||||
_Mode_name_4 = "XAddMode"
|
||||
_Mode_name_5 = "InvalidMode"
|
||||
)
|
||||
|
||||
func (i Mode) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _Mode_name_0
|
||||
case i == 32:
|
||||
return _Mode_name_1
|
||||
case i == 64:
|
||||
return _Mode_name_2
|
||||
case i == 96:
|
||||
return _Mode_name_3
|
||||
case i == 192:
|
||||
return _Mode_name_4
|
||||
case i == 255:
|
||||
return _Mode_name_5
|
||||
default:
|
||||
return "Mode(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[InvalidSize-255]
|
||||
_ = x[DWord-24]
|
||||
_ = x[Word-0]
|
||||
_ = x[Half-8]
|
||||
_ = x[Byte-16]
|
||||
}
|
||||
|
||||
const (
|
||||
_Size_name_0 = "Word"
|
||||
_Size_name_1 = "Half"
|
||||
_Size_name_2 = "Byte"
|
||||
_Size_name_3 = "DWord"
|
||||
_Size_name_4 = "InvalidSize"
|
||||
)
|
||||
|
||||
func (i Size) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _Size_name_0
|
||||
case i == 8:
|
||||
return _Size_name_1
|
||||
case i == 16:
|
||||
return _Size_name_2
|
||||
case i == 24:
|
||||
return _Size_name_3
|
||||
case i == 255:
|
||||
return _Size_name_4
|
||||
default:
|
||||
return "Size(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package asm
|
||||
|
||||
// Metadata contains metadata about an instruction.
|
||||
type Metadata struct {
|
||||
head *metaElement
|
||||
}
|
||||
|
||||
type metaElement struct {
|
||||
next *metaElement
|
||||
key, value interface{}
|
||||
}
|
||||
|
||||
// Find the element containing key.
|
||||
//
|
||||
// Returns nil if there is no such element.
|
||||
func (m *Metadata) find(key interface{}) *metaElement {
|
||||
for e := m.head; e != nil; e = e.next {
|
||||
if e.key == key {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove an element from the linked list.
|
||||
//
|
||||
// Copies as many elements of the list as necessary to remove r, but doesn't
|
||||
// perform a full copy.
|
||||
func (m *Metadata) remove(r *metaElement) {
|
||||
current := &m.head
|
||||
for e := m.head; e != nil; e = e.next {
|
||||
if e == r {
|
||||
// We've found the element we want to remove.
|
||||
*current = e.next
|
||||
|
||||
// No need to copy the tail.
|
||||
return
|
||||
}
|
||||
|
||||
// There is another element in front of the one we want to remove.
|
||||
// We have to copy it to be able to change metaElement.next.
|
||||
cpy := &metaElement{key: e.key, value: e.value}
|
||||
*current = cpy
|
||||
current = &cpy.next
|
||||
}
|
||||
}
|
||||
|
||||
// Set a key to a value.
|
||||
//
|
||||
// If value is nil, the key is removed. Avoids modifying old metadata by
|
||||
// copying if necessary.
|
||||
func (m *Metadata) Set(key, value interface{}) {
|
||||
if e := m.find(key); e != nil {
|
||||
if e.value == value {
|
||||
// Key is present and the value is the same. Nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
// Key is present with a different value. Create a copy of the list
|
||||
// which doesn't have the element in it.
|
||||
m.remove(e)
|
||||
}
|
||||
|
||||
// m.head is now a linked list that doesn't contain key.
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.head = &metaElement{key: key, value: value, next: m.head}
|
||||
}
|
||||
|
||||
// Get the value of a key.
|
||||
//
|
||||
// Returns nil if no value with the given key is present.
|
||||
func (m *Metadata) Get(key interface{}) interface{} {
|
||||
if e := m.find(key); e != nil {
|
||||
return e.value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestMetadata(t *testing.T) {
|
||||
var m Metadata
|
||||
|
||||
// Metadata should be the size of a pointer.
|
||||
qt.Assert(t, unsafe.Sizeof(m), qt.Equals, unsafe.Sizeof(uintptr(0)))
|
||||
|
||||
// A lookup in a nil meta should return nil.
|
||||
qt.Assert(t, m.Get(bool(false)), qt.IsNil)
|
||||
|
||||
// We can look up anything we inserted.
|
||||
m.Set(bool(false), int(0))
|
||||
m.Set(int(1), int(1))
|
||||
qt.Assert(t, m.Get(bool(false)), qt.Equals, int(0))
|
||||
qt.Assert(t, m.Get(int(1)), qt.Equals, int(1))
|
||||
|
||||
// We have copy on write semantics
|
||||
old := m
|
||||
m.Set(bool(false), int(1))
|
||||
qt.Assert(t, m.Get(bool(false)), qt.Equals, int(1))
|
||||
qt.Assert(t, m.Get(int(1)), qt.Equals, int(1))
|
||||
qt.Assert(t, old.Get(bool(false)), qt.Equals, int(0))
|
||||
qt.Assert(t, old.Get(int(1)), qt.Equals, int(1))
|
||||
|
||||
// Newtypes are handled distinctly.
|
||||
type b bool
|
||||
m.Set(b(false), int(42))
|
||||
qt.Assert(t, m.Get(bool(false)), qt.Equals, int(1))
|
||||
qt.Assert(t, m.Get(int(1)), qt.Equals, int(1))
|
||||
qt.Assert(t, m.Get(b(false)), qt.Equals, int(42))
|
||||
|
||||
// Setting nil removes a key.
|
||||
m.Set(bool(false), nil)
|
||||
qt.Assert(t, m.Get(bool(false)), qt.IsNil)
|
||||
qt.Assert(t, m.Get(int(1)), qt.Equals, int(1))
|
||||
qt.Assert(t, m.Get(b(false)), qt.Equals, int(42))
|
||||
}
|
||||
|
||||
func BenchmarkMetadata(b *testing.B) {
|
||||
// Assume that three bits of metadata on a single instruction is
|
||||
// our worst case.
|
||||
const worstCaseItems = 3
|
||||
|
||||
type t struct{}
|
||||
|
||||
b.Run("add first", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var v Metadata
|
||||
v.Set(t{}, 0)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("add last", func(b *testing.B) {
|
||||
var m Metadata
|
||||
for i := 0; i < worstCaseItems-1; i++ {
|
||||
m.Set(i, i)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := m
|
||||
v.Set(t{}, 0)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("add existing", func(b *testing.B) {
|
||||
var m Metadata
|
||||
for i := 0; i < worstCaseItems-1; i++ {
|
||||
m.Set(i, i)
|
||||
}
|
||||
m.Set(t{}, 0)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := m
|
||||
v.Set(t{}, 0)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("get miss", func(b *testing.B) {
|
||||
var m Metadata
|
||||
for i := 0; i < worstCaseItems; i++ {
|
||||
m.Set(i, i)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if m.Get(t{}) != nil {
|
||||
b.Fatal("got result from miss")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:generate stringer -output opcode_string.go -type=Class
|
||||
|
||||
// Class of operations
|
||||
//
|
||||
// msb lsb
|
||||
// +---+--+---+
|
||||
// | ?? |CLS|
|
||||
// +---+--+---+
|
||||
type Class uint8
|
||||
|
||||
const classMask OpCode = 0x07
|
||||
|
||||
const (
|
||||
// LdClass loads immediate values into registers.
|
||||
// Also used for non-standard load operations from cBPF.
|
||||
LdClass Class = 0x00
|
||||
// LdXClass loads memory into registers.
|
||||
LdXClass Class = 0x01
|
||||
// StClass stores immediate values to memory.
|
||||
StClass Class = 0x02
|
||||
// StXClass stores registers to memory.
|
||||
StXClass Class = 0x03
|
||||
// ALUClass describes arithmetic operators.
|
||||
ALUClass Class = 0x04
|
||||
// JumpClass describes jump operators.
|
||||
JumpClass Class = 0x05
|
||||
// Jump32Class describes jump operators with 32-bit comparisons.
|
||||
// Requires kernel 5.1.
|
||||
Jump32Class Class = 0x06
|
||||
// ALU64Class describes arithmetic operators in 64-bit mode.
|
||||
ALU64Class Class = 0x07
|
||||
)
|
||||
|
||||
// IsLoad checks if this is either LdClass or LdXClass.
|
||||
func (cls Class) IsLoad() bool {
|
||||
return cls == LdClass || cls == LdXClass
|
||||
}
|
||||
|
||||
// IsStore checks if this is either StClass or StXClass.
|
||||
func (cls Class) IsStore() bool {
|
||||
return cls == StClass || cls == StXClass
|
||||
}
|
||||
|
||||
func (cls Class) isLoadOrStore() bool {
|
||||
return cls.IsLoad() || cls.IsStore()
|
||||
}
|
||||
|
||||
// IsALU checks if this is either ALUClass or ALU64Class.
|
||||
func (cls Class) IsALU() bool {
|
||||
return cls == ALUClass || cls == ALU64Class
|
||||
}
|
||||
|
||||
// IsJump checks if this is either JumpClass or Jump32Class.
|
||||
func (cls Class) IsJump() bool {
|
||||
return cls == JumpClass || cls == Jump32Class
|
||||
}
|
||||
|
||||
func (cls Class) isJumpOrALU() bool {
|
||||
return cls.IsJump() || cls.IsALU()
|
||||
}
|
||||
|
||||
// OpCode is a packed eBPF opcode.
|
||||
//
|
||||
// Its encoding is defined by a Class value:
|
||||
//
|
||||
// msb lsb
|
||||
// +----+-+---+
|
||||
// | ???? |CLS|
|
||||
// +----+-+---+
|
||||
type OpCode uint8
|
||||
|
||||
// InvalidOpCode is returned by setters on OpCode
|
||||
const InvalidOpCode OpCode = 0xff
|
||||
|
||||
// rawInstructions returns the number of BPF instructions required
|
||||
// to encode this opcode.
|
||||
func (op OpCode) rawInstructions() int {
|
||||
if op.IsDWordLoad() {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func (op OpCode) IsDWordLoad() bool {
|
||||
return op == LoadImmOp(DWord)
|
||||
}
|
||||
|
||||
// Class returns the class of operation.
|
||||
func (op OpCode) Class() Class {
|
||||
return Class(op & classMask)
|
||||
}
|
||||
|
||||
// Mode returns the mode for load and store operations.
|
||||
func (op OpCode) Mode() Mode {
|
||||
if !op.Class().isLoadOrStore() {
|
||||
return InvalidMode
|
||||
}
|
||||
return Mode(op & modeMask)
|
||||
}
|
||||
|
||||
// Size returns the size for load and store operations.
|
||||
func (op OpCode) Size() Size {
|
||||
if !op.Class().isLoadOrStore() {
|
||||
return InvalidSize
|
||||
}
|
||||
return Size(op & sizeMask)
|
||||
}
|
||||
|
||||
// Source returns the source for branch and ALU operations.
|
||||
func (op OpCode) Source() Source {
|
||||
if !op.Class().isJumpOrALU() || op.ALUOp() == Swap {
|
||||
return InvalidSource
|
||||
}
|
||||
return Source(op & sourceMask)
|
||||
}
|
||||
|
||||
// ALUOp returns the ALUOp.
|
||||
func (op OpCode) ALUOp() ALUOp {
|
||||
if !op.Class().IsALU() {
|
||||
return InvalidALUOp
|
||||
}
|
||||
return ALUOp(op & aluMask)
|
||||
}
|
||||
|
||||
// Endianness returns the Endianness for a byte swap instruction.
|
||||
func (op OpCode) Endianness() Endianness {
|
||||
if op.ALUOp() != Swap {
|
||||
return InvalidEndian
|
||||
}
|
||||
return Endianness(op & endianMask)
|
||||
}
|
||||
|
||||
// JumpOp returns the JumpOp.
|
||||
// Returns InvalidJumpOp if it doesn't encode a jump.
|
||||
func (op OpCode) JumpOp() JumpOp {
|
||||
if !op.Class().IsJump() {
|
||||
return InvalidJumpOp
|
||||
}
|
||||
|
||||
jumpOp := JumpOp(op & jumpMask)
|
||||
|
||||
// Some JumpOps are only supported by JumpClass, not Jump32Class.
|
||||
if op.Class() == Jump32Class && (jumpOp == Exit || jumpOp == Call || jumpOp == Ja) {
|
||||
return InvalidJumpOp
|
||||
}
|
||||
|
||||
return jumpOp
|
||||
}
|
||||
|
||||
// SetMode sets the mode on load and store operations.
|
||||
//
|
||||
// Returns InvalidOpCode if op is of the wrong class.
|
||||
func (op OpCode) SetMode(mode Mode) OpCode {
|
||||
if !op.Class().isLoadOrStore() || !valid(OpCode(mode), modeMask) {
|
||||
return InvalidOpCode
|
||||
}
|
||||
return (op & ^modeMask) | OpCode(mode)
|
||||
}
|
||||
|
||||
// SetSize sets the size on load and store operations.
|
||||
//
|
||||
// Returns InvalidOpCode if op is of the wrong class.
|
||||
func (op OpCode) SetSize(size Size) OpCode {
|
||||
if !op.Class().isLoadOrStore() || !valid(OpCode(size), sizeMask) {
|
||||
return InvalidOpCode
|
||||
}
|
||||
return (op & ^sizeMask) | OpCode(size)
|
||||
}
|
||||
|
||||
// SetSource sets the source on jump and ALU operations.
|
||||
//
|
||||
// Returns InvalidOpCode if op is of the wrong class.
|
||||
func (op OpCode) SetSource(source Source) OpCode {
|
||||
if !op.Class().isJumpOrALU() || !valid(OpCode(source), sourceMask) {
|
||||
return InvalidOpCode
|
||||
}
|
||||
return (op & ^sourceMask) | OpCode(source)
|
||||
}
|
||||
|
||||
// SetALUOp sets the ALUOp on ALU operations.
|
||||
//
|
||||
// Returns InvalidOpCode if op is of the wrong class.
|
||||
func (op OpCode) SetALUOp(alu ALUOp) OpCode {
|
||||
if !op.Class().IsALU() || !valid(OpCode(alu), aluMask) {
|
||||
return InvalidOpCode
|
||||
}
|
||||
return (op & ^aluMask) | OpCode(alu)
|
||||
}
|
||||
|
||||
// SetJumpOp sets the JumpOp on jump operations.
|
||||
//
|
||||
// Returns InvalidOpCode if op is of the wrong class.
|
||||
func (op OpCode) SetJumpOp(jump JumpOp) OpCode {
|
||||
if !op.Class().IsJump() || !valid(OpCode(jump), jumpMask) {
|
||||
return InvalidOpCode
|
||||
}
|
||||
|
||||
newOp := (op & ^jumpMask) | OpCode(jump)
|
||||
|
||||
// Check newOp is legal.
|
||||
if newOp.JumpOp() == InvalidJumpOp {
|
||||
return InvalidOpCode
|
||||
}
|
||||
|
||||
return newOp
|
||||
}
|
||||
|
||||
func (op OpCode) String() string {
|
||||
var f strings.Builder
|
||||
|
||||
switch class := op.Class(); {
|
||||
case class.isLoadOrStore():
|
||||
f.WriteString(strings.TrimSuffix(class.String(), "Class"))
|
||||
|
||||
mode := op.Mode()
|
||||
f.WriteString(strings.TrimSuffix(mode.String(), "Mode"))
|
||||
|
||||
switch op.Size() {
|
||||
case DWord:
|
||||
f.WriteString("DW")
|
||||
case Word:
|
||||
f.WriteString("W")
|
||||
case Half:
|
||||
f.WriteString("H")
|
||||
case Byte:
|
||||
f.WriteString("B")
|
||||
}
|
||||
|
||||
case class.IsALU():
|
||||
f.WriteString(op.ALUOp().String())
|
||||
|
||||
if op.ALUOp() == Swap {
|
||||
// Width for Endian is controlled by Constant
|
||||
f.WriteString(op.Endianness().String())
|
||||
} else {
|
||||
if class == ALUClass {
|
||||
f.WriteString("32")
|
||||
}
|
||||
|
||||
f.WriteString(strings.TrimSuffix(op.Source().String(), "Source"))
|
||||
}
|
||||
|
||||
case class.IsJump():
|
||||
f.WriteString(op.JumpOp().String())
|
||||
|
||||
if class == Jump32Class {
|
||||
f.WriteString("32")
|
||||
}
|
||||
|
||||
if jop := op.JumpOp(); jop != Exit && jop != Call {
|
||||
f.WriteString(strings.TrimSuffix(op.Source().String(), "Source"))
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Fprintf(&f, "OpCode(%#x)", uint8(op))
|
||||
}
|
||||
|
||||
return f.String()
|
||||
}
|
||||
|
||||
// valid returns true if all bits in value are covered by mask.
|
||||
func valid(value, mask OpCode) bool {
|
||||
return value & ^mask == 0
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Code generated by "stringer -output opcode_string.go -type=Class"; DO NOT EDIT.
|
||||
|
||||
package asm
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[LdClass-0]
|
||||
_ = x[LdXClass-1]
|
||||
_ = x[StClass-2]
|
||||
_ = x[StXClass-3]
|
||||
_ = x[ALUClass-4]
|
||||
_ = x[JumpClass-5]
|
||||
_ = x[Jump32Class-6]
|
||||
_ = x[ALU64Class-7]
|
||||
}
|
||||
|
||||
const _Class_name = "LdClassLdXClassStClassStXClassALUClassJumpClassJump32ClassALU64Class"
|
||||
|
||||
var _Class_index = [...]uint8{0, 7, 15, 22, 30, 38, 47, 58, 68}
|
||||
|
||||
func (i Class) String() string {
|
||||
if i >= Class(len(_Class_index)-1) {
|
||||
return "Class(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _Class_name[_Class_index[i]:_Class_index[i+1]]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestGetSetJumpOp(t *testing.T) {
|
||||
test := func(class Class, op JumpOp, valid bool) {
|
||||
t.Run(fmt.Sprintf("%s-%s", class, op), func(t *testing.T) {
|
||||
opcode := OpCode(class).SetJumpOp(op)
|
||||
|
||||
if valid {
|
||||
qt.Assert(t, opcode, qt.Not(qt.Equals), InvalidOpCode)
|
||||
qt.Assert(t, opcode.JumpOp(), qt.Equals, op)
|
||||
} else {
|
||||
qt.Assert(t, opcode, qt.Equals, InvalidOpCode)
|
||||
qt.Assert(t, opcode.JumpOp(), qt.Equals, InvalidJumpOp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Exit, call and JA aren't allowed with Jump32
|
||||
test(Jump32Class, Exit, false)
|
||||
test(Jump32Class, Call, false)
|
||||
test(Jump32Class, Ja, false)
|
||||
|
||||
// But are with Jump
|
||||
test(JumpClass, Exit, true)
|
||||
test(JumpClass, Call, true)
|
||||
test(JumpClass, Ja, true)
|
||||
|
||||
// All other ops work
|
||||
for _, op := range []JumpOp{
|
||||
JEq,
|
||||
JGT,
|
||||
JGE,
|
||||
JSet,
|
||||
JNE,
|
||||
JSGT,
|
||||
JSGE,
|
||||
JLT,
|
||||
JLE,
|
||||
JSLT,
|
||||
JSLE,
|
||||
} {
|
||||
test(Jump32Class, op, true)
|
||||
test(JumpClass, op, true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Register is the source or destination of most operations.
|
||||
type Register uint8
|
||||
|
||||
// R0 contains return values.
|
||||
const R0 Register = 0
|
||||
|
||||
// Registers for function arguments.
|
||||
const (
|
||||
R1 Register = R0 + 1 + iota
|
||||
R2
|
||||
R3
|
||||
R4
|
||||
R5
|
||||
)
|
||||
|
||||
// Callee saved registers preserved by function calls.
|
||||
const (
|
||||
R6 Register = R5 + 1 + iota
|
||||
R7
|
||||
R8
|
||||
R9
|
||||
)
|
||||
|
||||
// Read-only frame pointer to access stack.
|
||||
const (
|
||||
R10 Register = R9 + 1
|
||||
RFP = R10
|
||||
)
|
||||
|
||||
// Pseudo registers used by 64bit loads and jumps
|
||||
const (
|
||||
PseudoMapFD = R1 // BPF_PSEUDO_MAP_FD
|
||||
PseudoMapValue = R2 // BPF_PSEUDO_MAP_VALUE
|
||||
PseudoCall = R1 // BPF_PSEUDO_CALL
|
||||
PseudoFunc = R4 // BPF_PSEUDO_FUNC
|
||||
PseudoKfuncCall = R2 // BPF_PSEUDO_KFUNC_CALL
|
||||
)
|
||||
|
||||
func (r Register) String() string {
|
||||
v := uint8(r)
|
||||
if v == 10 {
|
||||
return "rfp"
|
||||
}
|
||||
return fmt.Sprintf("r%d", v)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Code generated by "stringer -type AttachType -trimprefix Attach"; DO NOT EDIT.
|
||||
|
||||
package ebpf
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[AttachNone-0]
|
||||
_ = x[AttachCGroupInetIngress-0]
|
||||
_ = x[AttachCGroupInetEgress-1]
|
||||
_ = x[AttachCGroupInetSockCreate-2]
|
||||
_ = x[AttachCGroupSockOps-3]
|
||||
_ = x[AttachSkSKBStreamParser-4]
|
||||
_ = x[AttachSkSKBStreamVerdict-5]
|
||||
_ = x[AttachCGroupDevice-6]
|
||||
_ = x[AttachSkMsgVerdict-7]
|
||||
_ = x[AttachCGroupInet4Bind-8]
|
||||
_ = x[AttachCGroupInet6Bind-9]
|
||||
_ = x[AttachCGroupInet4Connect-10]
|
||||
_ = x[AttachCGroupInet6Connect-11]
|
||||
_ = x[AttachCGroupInet4PostBind-12]
|
||||
_ = x[AttachCGroupInet6PostBind-13]
|
||||
_ = x[AttachCGroupUDP4Sendmsg-14]
|
||||
_ = x[AttachCGroupUDP6Sendmsg-15]
|
||||
_ = x[AttachLircMode2-16]
|
||||
_ = x[AttachFlowDissector-17]
|
||||
_ = x[AttachCGroupSysctl-18]
|
||||
_ = x[AttachCGroupUDP4Recvmsg-19]
|
||||
_ = x[AttachCGroupUDP6Recvmsg-20]
|
||||
_ = x[AttachCGroupGetsockopt-21]
|
||||
_ = x[AttachCGroupSetsockopt-22]
|
||||
_ = x[AttachTraceRawTp-23]
|
||||
_ = x[AttachTraceFEntry-24]
|
||||
_ = x[AttachTraceFExit-25]
|
||||
_ = x[AttachModifyReturn-26]
|
||||
_ = x[AttachLSMMac-27]
|
||||
_ = x[AttachTraceIter-28]
|
||||
_ = x[AttachCgroupInet4GetPeername-29]
|
||||
_ = x[AttachCgroupInet6GetPeername-30]
|
||||
_ = x[AttachCgroupInet4GetSockname-31]
|
||||
_ = x[AttachCgroupInet6GetSockname-32]
|
||||
_ = x[AttachXDPDevMap-33]
|
||||
_ = x[AttachCgroupInetSockRelease-34]
|
||||
_ = x[AttachXDPCPUMap-35]
|
||||
_ = x[AttachSkLookup-36]
|
||||
_ = x[AttachXDP-37]
|
||||
_ = x[AttachSkSKBVerdict-38]
|
||||
_ = x[AttachSkReuseportSelect-39]
|
||||
_ = x[AttachSkReuseportSelectOrMigrate-40]
|
||||
_ = x[AttachPerfEvent-41]
|
||||
_ = x[AttachTraceKprobeMulti-42]
|
||||
}
|
||||
|
||||
const _AttachType_name = "NoneCGroupInetEgressCGroupInetSockCreateCGroupSockOpsSkSKBStreamParserSkSKBStreamVerdictCGroupDeviceSkMsgVerdictCGroupInet4BindCGroupInet6BindCGroupInet4ConnectCGroupInet6ConnectCGroupInet4PostBindCGroupInet6PostBindCGroupUDP4SendmsgCGroupUDP6SendmsgLircMode2FlowDissectorCGroupSysctlCGroupUDP4RecvmsgCGroupUDP6RecvmsgCGroupGetsockoptCGroupSetsockoptTraceRawTpTraceFEntryTraceFExitModifyReturnLSMMacTraceIterCgroupInet4GetPeernameCgroupInet6GetPeernameCgroupInet4GetSocknameCgroupInet6GetSocknameXDPDevMapCgroupInetSockReleaseXDPCPUMapSkLookupXDPSkSKBVerdictSkReuseportSelectSkReuseportSelectOrMigratePerfEventTraceKprobeMulti"
|
||||
|
||||
var _AttachType_index = [...]uint16{0, 4, 20, 40, 53, 70, 88, 100, 112, 127, 142, 160, 178, 197, 216, 233, 250, 259, 272, 284, 301, 318, 334, 350, 360, 371, 381, 393, 399, 408, 430, 452, 474, 496, 505, 526, 535, 543, 546, 558, 575, 601, 610, 626}
|
||||
|
||||
func (i AttachType) String() string {
|
||||
if i >= AttachType(len(_AttachType_index)-1) {
|
||||
return "AttachType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _AttachType_name[_AttachType_index[i]:_AttachType_index[i+1]]
|
||||
}
|
||||
@@ -0,0 +1,869 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
"github.com/cilium/ebpf/internal/sys"
|
||||
"github.com/cilium/ebpf/internal/unix"
|
||||
)
|
||||
|
||||
const btfMagic = 0xeB9F
|
||||
|
||||
// Errors returned by BTF functions.
|
||||
var (
|
||||
ErrNotSupported = internal.ErrNotSupported
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrNoExtendedInfo = errors.New("no extended info")
|
||||
ErrMultipleMatches = errors.New("multiple matching types")
|
||||
)
|
||||
|
||||
// ID represents the unique ID of a BTF object.
|
||||
type ID = sys.BTFID
|
||||
|
||||
// Spec allows querying a set of Types and loading the set into the
|
||||
// kernel.
|
||||
type Spec struct {
|
||||
// All types contained by the spec, not including types from the base in
|
||||
// case the spec was parsed from split BTF.
|
||||
types []Type
|
||||
|
||||
// Type IDs indexed by type.
|
||||
typeIDs map[Type]TypeID
|
||||
|
||||
// The ID of the first type in types.
|
||||
firstTypeID TypeID
|
||||
|
||||
// Types indexed by essential name.
|
||||
// Includes all struct flavors and types with the same name.
|
||||
namedTypes map[essentialName][]Type
|
||||
|
||||
// String table from ELF, may be nil.
|
||||
strings *stringTable
|
||||
|
||||
// Byte order of the ELF we decoded the spec from, may be nil.
|
||||
byteOrder binary.ByteOrder
|
||||
}
|
||||
|
||||
var btfHeaderLen = binary.Size(&btfHeader{})
|
||||
|
||||
type btfHeader struct {
|
||||
Magic uint16
|
||||
Version uint8
|
||||
Flags uint8
|
||||
HdrLen uint32
|
||||
|
||||
TypeOff uint32
|
||||
TypeLen uint32
|
||||
StringOff uint32
|
||||
StringLen uint32
|
||||
}
|
||||
|
||||
// typeStart returns the offset from the beginning of the .BTF section
|
||||
// to the start of its type entries.
|
||||
func (h *btfHeader) typeStart() int64 {
|
||||
return int64(h.HdrLen + h.TypeOff)
|
||||
}
|
||||
|
||||
// stringStart returns the offset from the beginning of the .BTF section
|
||||
// to the start of its string table.
|
||||
func (h *btfHeader) stringStart() int64 {
|
||||
return int64(h.HdrLen + h.StringOff)
|
||||
}
|
||||
|
||||
// newSpec creates a Spec containing only Void.
|
||||
func newSpec() *Spec {
|
||||
return &Spec{
|
||||
[]Type{(*Void)(nil)},
|
||||
map[Type]TypeID{(*Void)(nil): 0},
|
||||
0,
|
||||
make(map[essentialName][]Type),
|
||||
nil,
|
||||
nil,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSpec opens file and calls LoadSpecFromReader on it.
|
||||
func LoadSpec(file string) (*Spec, error) {
|
||||
fh, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
return LoadSpecFromReader(fh)
|
||||
}
|
||||
|
||||
// LoadSpecFromReader reads from an ELF or a raw BTF blob.
|
||||
//
|
||||
// Returns ErrNotFound if reading from an ELF which contains no BTF. ExtInfos
|
||||
// may be nil.
|
||||
func LoadSpecFromReader(rd io.ReaderAt) (*Spec, error) {
|
||||
file, err := internal.NewSafeELFFile(rd)
|
||||
if err != nil {
|
||||
if bo := guessRawBTFByteOrder(rd); bo != nil {
|
||||
return loadRawSpec(io.NewSectionReader(rd, 0, math.MaxInt64), bo, nil)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return loadSpecFromELF(file)
|
||||
}
|
||||
|
||||
// LoadSpecAndExtInfosFromReader reads from an ELF.
|
||||
//
|
||||
// ExtInfos may be nil if the ELF doesn't contain section metadata.
|
||||
// Returns ErrNotFound if the ELF contains no BTF.
|
||||
func LoadSpecAndExtInfosFromReader(rd io.ReaderAt) (*Spec, *ExtInfos, error) {
|
||||
file, err := internal.NewSafeELFFile(rd)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
spec, err := loadSpecFromELF(file)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
extInfos, err := loadExtInfosFromELF(file, spec)
|
||||
if err != nil && !errors.Is(err, ErrNotFound) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return spec, extInfos, nil
|
||||
}
|
||||
|
||||
// symbolOffsets extracts all symbols offsets from an ELF and indexes them by
|
||||
// section and variable name.
|
||||
//
|
||||
// References to variables in BTF data sections carry unsigned 32-bit offsets.
|
||||
// Some ELF symbols (e.g. in vmlinux) may point to virtual memory that is well
|
||||
// beyond this range. Since these symbols cannot be described by BTF info,
|
||||
// ignore them here.
|
||||
func symbolOffsets(file *internal.SafeELFFile) (map[symbol]uint32, error) {
|
||||
symbols, err := file.Symbols()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read symbols: %v", err)
|
||||
}
|
||||
|
||||
offsets := make(map[symbol]uint32)
|
||||
for _, sym := range symbols {
|
||||
if idx := sym.Section; idx >= elf.SHN_LORESERVE && idx <= elf.SHN_HIRESERVE {
|
||||
// Ignore things like SHN_ABS
|
||||
continue
|
||||
}
|
||||
|
||||
if sym.Value > math.MaxUint32 {
|
||||
// VarSecinfo offset is u32, cannot reference symbols in higher regions.
|
||||
continue
|
||||
}
|
||||
|
||||
if int(sym.Section) >= len(file.Sections) {
|
||||
return nil, fmt.Errorf("symbol %s: invalid section %d", sym.Name, sym.Section)
|
||||
}
|
||||
|
||||
secName := file.Sections[sym.Section].Name
|
||||
offsets[symbol{secName, sym.Name}] = uint32(sym.Value)
|
||||
}
|
||||
|
||||
return offsets, nil
|
||||
}
|
||||
|
||||
func loadSpecFromELF(file *internal.SafeELFFile) (*Spec, error) {
|
||||
var (
|
||||
btfSection *elf.Section
|
||||
sectionSizes = make(map[string]uint32)
|
||||
)
|
||||
|
||||
for _, sec := range file.Sections {
|
||||
switch sec.Name {
|
||||
case ".BTF":
|
||||
btfSection = sec
|
||||
default:
|
||||
if sec.Type != elf.SHT_PROGBITS && sec.Type != elf.SHT_NOBITS {
|
||||
break
|
||||
}
|
||||
|
||||
if sec.Size > math.MaxUint32 {
|
||||
return nil, fmt.Errorf("section %s exceeds maximum size", sec.Name)
|
||||
}
|
||||
|
||||
sectionSizes[sec.Name] = uint32(sec.Size)
|
||||
}
|
||||
}
|
||||
|
||||
if btfSection == nil {
|
||||
return nil, fmt.Errorf("btf: %w", ErrNotFound)
|
||||
}
|
||||
|
||||
offsets, err := symbolOffsets(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if btfSection.ReaderAt == nil {
|
||||
return nil, fmt.Errorf("compressed BTF is not supported")
|
||||
}
|
||||
|
||||
spec, err := loadRawSpec(btfSection.ReaderAt, file.ByteOrder, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = fixupDatasec(spec.types, sectionSizes, offsets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
func loadRawSpec(btf io.ReaderAt, bo binary.ByteOrder, base *Spec) (*Spec, error) {
|
||||
var (
|
||||
baseStrings *stringTable
|
||||
firstTypeID TypeID
|
||||
err error
|
||||
)
|
||||
|
||||
if base != nil {
|
||||
if base.firstTypeID != 0 {
|
||||
return nil, fmt.Errorf("can't use split BTF as base")
|
||||
}
|
||||
|
||||
if base.strings == nil {
|
||||
return nil, fmt.Errorf("parse split BTF: base must be loaded from an ELF")
|
||||
}
|
||||
|
||||
baseStrings = base.strings
|
||||
|
||||
firstTypeID, err = base.nextTypeID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
rawTypes, rawStrings, err := parseBTF(btf, bo, baseStrings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
types, err := inflateRawTypes(rawTypes, rawStrings, base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
typeIDs, typesByName := indexTypes(types, firstTypeID)
|
||||
|
||||
return &Spec{
|
||||
namedTypes: typesByName,
|
||||
typeIDs: typeIDs,
|
||||
types: types,
|
||||
firstTypeID: firstTypeID,
|
||||
strings: rawStrings,
|
||||
byteOrder: bo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func indexTypes(types []Type, firstTypeID TypeID) (map[Type]TypeID, map[essentialName][]Type) {
|
||||
namedTypes := 0
|
||||
for _, typ := range types {
|
||||
if typ.TypeName() != "" {
|
||||
// Do a pre-pass to figure out how big types by name has to be.
|
||||
// Most types have unique names, so it's OK to ignore essentialName
|
||||
// here.
|
||||
namedTypes++
|
||||
}
|
||||
}
|
||||
|
||||
typeIDs := make(map[Type]TypeID, len(types))
|
||||
typesByName := make(map[essentialName][]Type, namedTypes)
|
||||
|
||||
for i, typ := range types {
|
||||
if name := newEssentialName(typ.TypeName()); name != "" {
|
||||
typesByName[name] = append(typesByName[name], typ)
|
||||
}
|
||||
typeIDs[typ] = firstTypeID + TypeID(i)
|
||||
}
|
||||
|
||||
return typeIDs, typesByName
|
||||
}
|
||||
|
||||
// LoadKernelSpec returns the current kernel's BTF information.
|
||||
//
|
||||
// Defaults to /sys/kernel/btf/vmlinux and falls back to scanning the file system
|
||||
// for vmlinux ELFs. Returns an error wrapping ErrNotSupported if BTF is not enabled.
|
||||
func LoadKernelSpec() (*Spec, error) {
|
||||
spec, _, err := kernelSpec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return spec.Copy(), nil
|
||||
}
|
||||
|
||||
var kernelBTF struct {
|
||||
sync.RWMutex
|
||||
spec *Spec
|
||||
// True if the spec was read from an ELF instead of raw BTF in /sys.
|
||||
fallback bool
|
||||
}
|
||||
|
||||
// FlushKernelSpec removes any cached kernel type information.
|
||||
func FlushKernelSpec() {
|
||||
kernelBTF.Lock()
|
||||
defer kernelBTF.Unlock()
|
||||
|
||||
kernelBTF.spec, kernelBTF.fallback = nil, false
|
||||
}
|
||||
|
||||
func kernelSpec() (*Spec, bool, error) {
|
||||
kernelBTF.RLock()
|
||||
spec, fallback := kernelBTF.spec, kernelBTF.fallback
|
||||
kernelBTF.RUnlock()
|
||||
|
||||
if spec == nil {
|
||||
kernelBTF.Lock()
|
||||
defer kernelBTF.Unlock()
|
||||
|
||||
spec, fallback = kernelBTF.spec, kernelBTF.fallback
|
||||
}
|
||||
|
||||
if spec != nil {
|
||||
return spec, fallback, nil
|
||||
}
|
||||
|
||||
spec, fallback, err := loadKernelSpec()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
kernelBTF.spec, kernelBTF.fallback = spec, fallback
|
||||
return spec, fallback, nil
|
||||
}
|
||||
|
||||
func loadKernelSpec() (_ *Spec, fallback bool, _ error) {
|
||||
fh, err := os.Open("/sys/kernel/btf/vmlinux")
|
||||
if err == nil {
|
||||
defer fh.Close()
|
||||
|
||||
spec, err := loadRawSpec(fh, internal.NativeEndian, nil)
|
||||
return spec, false, err
|
||||
}
|
||||
|
||||
file, err := findVMLinux()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
spec, err := loadSpecFromELF(file)
|
||||
return spec, true, err
|
||||
}
|
||||
|
||||
// findVMLinux scans multiple well-known paths for vmlinux kernel images.
|
||||
func findVMLinux() (*internal.SafeELFFile, error) {
|
||||
release, err := internal.KernelRelease()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use same list of locations as libbpf
|
||||
// https://github.com/libbpf/libbpf/blob/9a3a42608dbe3731256a5682a125ac1e23bced8f/src/btf.c#L3114-L3122
|
||||
locations := []string{
|
||||
"/boot/vmlinux-%s",
|
||||
"/lib/modules/%s/vmlinux-%[1]s",
|
||||
"/lib/modules/%s/build/vmlinux",
|
||||
"/usr/lib/modules/%s/kernel/vmlinux",
|
||||
"/usr/lib/debug/boot/vmlinux-%s",
|
||||
"/usr/lib/debug/boot/vmlinux-%s.debug",
|
||||
"/usr/lib/debug/lib/modules/%s/vmlinux",
|
||||
}
|
||||
|
||||
for _, loc := range locations {
|
||||
file, err := internal.OpenSafeELFFile(fmt.Sprintf(loc, release))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return file, err
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no BTF found for kernel version %s: %w", release, internal.ErrNotSupported)
|
||||
}
|
||||
|
||||
// parseBTFHeader parses the header of the .BTF section.
|
||||
func parseBTFHeader(r io.Reader, bo binary.ByteOrder) (*btfHeader, error) {
|
||||
var header btfHeader
|
||||
if err := binary.Read(r, bo, &header); err != nil {
|
||||
return nil, fmt.Errorf("can't read header: %v", err)
|
||||
}
|
||||
|
||||
if header.Magic != btfMagic {
|
||||
return nil, fmt.Errorf("incorrect magic value %v", header.Magic)
|
||||
}
|
||||
|
||||
if header.Version != 1 {
|
||||
return nil, fmt.Errorf("unexpected version %v", header.Version)
|
||||
}
|
||||
|
||||
if header.Flags != 0 {
|
||||
return nil, fmt.Errorf("unsupported flags %v", header.Flags)
|
||||
}
|
||||
|
||||
remainder := int64(header.HdrLen) - int64(binary.Size(&header))
|
||||
if remainder < 0 {
|
||||
return nil, errors.New("header length shorter than btfHeader size")
|
||||
}
|
||||
|
||||
if _, err := io.CopyN(internal.DiscardZeroes{}, r, remainder); err != nil {
|
||||
return nil, fmt.Errorf("header padding: %v", err)
|
||||
}
|
||||
|
||||
return &header, nil
|
||||
}
|
||||
|
||||
func guessRawBTFByteOrder(r io.ReaderAt) binary.ByteOrder {
|
||||
buf := new(bufio.Reader)
|
||||
for _, bo := range []binary.ByteOrder{
|
||||
binary.LittleEndian,
|
||||
binary.BigEndian,
|
||||
} {
|
||||
buf.Reset(io.NewSectionReader(r, 0, math.MaxInt64))
|
||||
if _, err := parseBTFHeader(buf, bo); err == nil {
|
||||
return bo
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseBTF reads a .BTF section into memory and parses it into a list of
|
||||
// raw types and a string table.
|
||||
func parseBTF(btf io.ReaderAt, bo binary.ByteOrder, baseStrings *stringTable) ([]rawType, *stringTable, error) {
|
||||
buf := internal.NewBufferedSectionReader(btf, 0, math.MaxInt64)
|
||||
header, err := parseBTFHeader(buf, bo)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("parsing .BTF header: %v", err)
|
||||
}
|
||||
|
||||
rawStrings, err := readStringTable(io.NewSectionReader(btf, header.stringStart(), int64(header.StringLen)),
|
||||
baseStrings)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("can't read type names: %w", err)
|
||||
}
|
||||
|
||||
buf.Reset(io.NewSectionReader(btf, header.typeStart(), int64(header.TypeLen)))
|
||||
rawTypes, err := readTypes(buf, bo, header.TypeLen)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("can't read types: %w", err)
|
||||
}
|
||||
|
||||
return rawTypes, rawStrings, nil
|
||||
}
|
||||
|
||||
type symbol struct {
|
||||
section string
|
||||
name string
|
||||
}
|
||||
|
||||
// fixupDatasec attempts to patch up missing info in Datasecs and its members by
|
||||
// supplementing them with information from the ELF headers and symbol table.
|
||||
func fixupDatasec(types []Type, sectionSizes map[string]uint32, offsets map[symbol]uint32) error {
|
||||
for _, typ := range types {
|
||||
ds, ok := typ.(*Datasec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
name := ds.Name
|
||||
|
||||
// Some Datasecs are virtual and don't have corresponding ELF sections.
|
||||
switch name {
|
||||
case ".ksyms":
|
||||
// .ksyms describes forward declarations of kfunc signatures.
|
||||
// Nothing to fix up, all sizes and offsets are 0.
|
||||
for _, vsi := range ds.Vars {
|
||||
_, ok := vsi.Type.(*Func)
|
||||
if !ok {
|
||||
// Only Funcs are supported in the .ksyms Datasec.
|
||||
return fmt.Errorf("data section %s: expected *btf.Func, not %T: %w", name, vsi.Type, ErrNotSupported)
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
case ".kconfig":
|
||||
// .kconfig has a size of 0 and has all members' offsets set to 0.
|
||||
// Fix up all offsets and set the Datasec's size.
|
||||
if err := fixupDatasecLayout(ds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fix up extern to global linkage to avoid a BTF verifier error.
|
||||
for _, vsi := range ds.Vars {
|
||||
vsi.Type.(*Var).Linkage = GlobalVar
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if ds.Size != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ds.Size, ok = sectionSizes[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("data section %s: missing size", name)
|
||||
}
|
||||
|
||||
for i := range ds.Vars {
|
||||
symName := ds.Vars[i].Type.TypeName()
|
||||
ds.Vars[i].Offset, ok = offsets[symbol{name, symName}]
|
||||
if !ok {
|
||||
return fmt.Errorf("data section %s: missing offset for symbol %s", name, symName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fixupDatasecLayout populates ds.Vars[].Offset according to var sizes and
|
||||
// alignment. Calculate and set ds.Size.
|
||||
func fixupDatasecLayout(ds *Datasec) error {
|
||||
var off uint32
|
||||
|
||||
for i, vsi := range ds.Vars {
|
||||
v, ok := vsi.Type.(*Var)
|
||||
if !ok {
|
||||
return fmt.Errorf("member %d: unsupported type %T", i, vsi.Type)
|
||||
}
|
||||
|
||||
size, err := Sizeof(v.Type)
|
||||
if err != nil {
|
||||
return fmt.Errorf("variable %s: getting size: %w", v.Name, err)
|
||||
}
|
||||
align, err := alignof(v.Type)
|
||||
if err != nil {
|
||||
return fmt.Errorf("variable %s: getting alignment: %w", v.Name, err)
|
||||
}
|
||||
|
||||
// Align the current member based on the offset of the end of the previous
|
||||
// member and the alignment of the current member.
|
||||
off = internal.Align(off, uint32(align))
|
||||
|
||||
ds.Vars[i].Offset = off
|
||||
|
||||
off += uint32(size)
|
||||
}
|
||||
|
||||
ds.Size = off
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy creates a copy of Spec.
|
||||
func (s *Spec) Copy() *Spec {
|
||||
types := copyTypes(s.types, nil)
|
||||
typeIDs, typesByName := indexTypes(types, s.firstTypeID)
|
||||
|
||||
// NB: Other parts of spec are not copied since they are immutable.
|
||||
return &Spec{
|
||||
types,
|
||||
typeIDs,
|
||||
s.firstTypeID,
|
||||
typesByName,
|
||||
s.strings,
|
||||
s.byteOrder,
|
||||
}
|
||||
}
|
||||
|
||||
type sliceWriter []byte
|
||||
|
||||
func (sw sliceWriter) Write(p []byte) (int, error) {
|
||||
if len(p) != len(sw) {
|
||||
return 0, errors.New("size doesn't match")
|
||||
}
|
||||
|
||||
return copy(sw, p), nil
|
||||
}
|
||||
|
||||
// nextTypeID returns the next unallocated type ID or an error if there are no
|
||||
// more type IDs.
|
||||
func (s *Spec) nextTypeID() (TypeID, error) {
|
||||
id := s.firstTypeID + TypeID(len(s.types))
|
||||
if id < s.firstTypeID {
|
||||
return 0, fmt.Errorf("no more type IDs")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// TypeByID returns the BTF Type with the given type ID.
|
||||
//
|
||||
// Returns an error wrapping ErrNotFound if a Type with the given ID
|
||||
// does not exist in the Spec.
|
||||
func (s *Spec) TypeByID(id TypeID) (Type, error) {
|
||||
if id < s.firstTypeID {
|
||||
return nil, fmt.Errorf("look up type with ID %d (first ID is %d): %w", id, s.firstTypeID, ErrNotFound)
|
||||
}
|
||||
|
||||
index := int(id - s.firstTypeID)
|
||||
if index >= len(s.types) {
|
||||
return nil, fmt.Errorf("look up type with ID %d: %w", id, ErrNotFound)
|
||||
}
|
||||
|
||||
return s.types[index], nil
|
||||
}
|
||||
|
||||
// TypeID returns the ID for a given Type.
|
||||
//
|
||||
// Returns an error wrapping ErrNoFound if the type isn't part of the Spec.
|
||||
func (s *Spec) TypeID(typ Type) (TypeID, error) {
|
||||
if _, ok := typ.(*Void); ok {
|
||||
// Equality is weird for void, since it is a zero sized type.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
id, ok := s.typeIDs[typ]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("no ID for type %s: %w", typ, ErrNotFound)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// AnyTypesByName returns a list of BTF Types with the given name.
|
||||
//
|
||||
// If the BTF blob describes multiple compilation units like vmlinux, multiple
|
||||
// Types with the same name and kind can exist, but might not describe the same
|
||||
// data structure.
|
||||
//
|
||||
// Returns an error wrapping ErrNotFound if no matching Type exists in the Spec.
|
||||
func (s *Spec) AnyTypesByName(name string) ([]Type, error) {
|
||||
types := s.namedTypes[newEssentialName(name)]
|
||||
if len(types) == 0 {
|
||||
return nil, fmt.Errorf("type name %s: %w", name, ErrNotFound)
|
||||
}
|
||||
|
||||
// Return a copy to prevent changes to namedTypes.
|
||||
result := make([]Type, 0, len(types))
|
||||
for _, t := range types {
|
||||
// Match against the full name, not just the essential one
|
||||
// in case the type being looked up is a struct flavor.
|
||||
if t.TypeName() == name {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AnyTypeByName returns a Type with the given name.
|
||||
//
|
||||
// Returns an error if multiple types of that name exist.
|
||||
func (s *Spec) AnyTypeByName(name string) (Type, error) {
|
||||
types, err := s.AnyTypesByName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(types) > 1 {
|
||||
return nil, fmt.Errorf("found multiple types: %v", types)
|
||||
}
|
||||
|
||||
return types[0], nil
|
||||
}
|
||||
|
||||
// TypeByName searches for a Type with a specific name. Since multiple Types
|
||||
// with the same name can exist, the parameter typ is taken to narrow down the
|
||||
// search in case of a clash.
|
||||
//
|
||||
// typ must be a non-nil pointer to an implementation of a Type. On success, the
|
||||
// address of the found Type will be copied to typ.
|
||||
//
|
||||
// Returns an error wrapping ErrNotFound if no matching Type exists in the Spec.
|
||||
// Returns an error wrapping ErrMultipleTypes if multiple candidates are found.
|
||||
func (s *Spec) TypeByName(name string, typ interface{}) error {
|
||||
typeInterface := reflect.TypeOf((*Type)(nil)).Elem()
|
||||
|
||||
// typ may be **T or *Type
|
||||
typValue := reflect.ValueOf(typ)
|
||||
if typValue.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("%T is not a pointer", typ)
|
||||
}
|
||||
|
||||
typPtr := typValue.Elem()
|
||||
if !typPtr.CanSet() {
|
||||
return fmt.Errorf("%T cannot be set", typ)
|
||||
}
|
||||
|
||||
wanted := typPtr.Type()
|
||||
if wanted == typeInterface {
|
||||
// This is *Type. Unwrap the value's type.
|
||||
wanted = typPtr.Elem().Type()
|
||||
}
|
||||
|
||||
if !wanted.AssignableTo(typeInterface) {
|
||||
return fmt.Errorf("%T does not satisfy Type interface", typ)
|
||||
}
|
||||
|
||||
types, err := s.AnyTypesByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var candidate Type
|
||||
for _, typ := range types {
|
||||
if reflect.TypeOf(typ) != wanted {
|
||||
continue
|
||||
}
|
||||
|
||||
if candidate != nil {
|
||||
return fmt.Errorf("type %s(%T): %w", name, typ, ErrMultipleMatches)
|
||||
}
|
||||
|
||||
candidate = typ
|
||||
}
|
||||
|
||||
if candidate == nil {
|
||||
return fmt.Errorf("%s %s: %w", wanted, name, ErrNotFound)
|
||||
}
|
||||
|
||||
typPtr.Set(reflect.ValueOf(candidate))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadSplitSpecFromReader loads split BTF from a reader.
|
||||
//
|
||||
// Types from base are used to resolve references in the split BTF.
|
||||
// The returned Spec only contains types from the split BTF, not from the base.
|
||||
func LoadSplitSpecFromReader(r io.ReaderAt, base *Spec) (*Spec, error) {
|
||||
return loadRawSpec(r, internal.NativeEndian, base)
|
||||
}
|
||||
|
||||
// TypesIterator iterates over types of a given spec.
|
||||
type TypesIterator struct {
|
||||
types []Type
|
||||
index int
|
||||
// The last visited type in the spec.
|
||||
Type Type
|
||||
}
|
||||
|
||||
// Iterate returns the types iterator.
|
||||
func (s *Spec) Iterate() *TypesIterator {
|
||||
// We share the backing array of types with the Spec. This is safe since
|
||||
// we don't allow deletion or shuffling of types.
|
||||
return &TypesIterator{types: s.types, index: 0}
|
||||
}
|
||||
|
||||
// Next returns true as long as there are any remaining types.
|
||||
func (iter *TypesIterator) Next() bool {
|
||||
if len(iter.types) <= iter.index {
|
||||
return false
|
||||
}
|
||||
|
||||
iter.Type = iter.types[iter.index]
|
||||
iter.index++
|
||||
return true
|
||||
}
|
||||
|
||||
// haveBTF attempts to load a BTF blob containing an Int. It should pass on any
|
||||
// kernel that supports BPF_BTF_LOAD.
|
||||
var haveBTF = internal.NewFeatureTest("BTF", "4.18", func() error {
|
||||
// 0-length anonymous integer
|
||||
err := probeBTF(&Int{})
|
||||
if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) {
|
||||
return internal.ErrNotSupported
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
// haveMapBTF attempts to load a minimal BTF blob containing a Var. It is
|
||||
// used as a proxy for .bss, .data and .rodata map support, which generally
|
||||
// come with a Var and Datasec. These were introduced in Linux 5.2.
|
||||
var haveMapBTF = internal.NewFeatureTest("Map BTF (Var/Datasec)", "5.2", func() error {
|
||||
if err := haveBTF(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := &Var{
|
||||
Name: "a",
|
||||
Type: &Pointer{(*Void)(nil)},
|
||||
}
|
||||
|
||||
err := probeBTF(v)
|
||||
if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) {
|
||||
// Treat both EINVAL and EPERM as not supported: creating the map may still
|
||||
// succeed without Btf* attrs.
|
||||
return internal.ErrNotSupported
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
// haveProgBTF attempts to load a BTF blob containing a Func and FuncProto. It
|
||||
// is used as a proxy for ext_info (func_info) support, which depends on
|
||||
// Func(Proto) by definition.
|
||||
var haveProgBTF = internal.NewFeatureTest("Program BTF (func/line_info)", "5.0", func() error {
|
||||
if err := haveBTF(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fn := &Func{
|
||||
Name: "a",
|
||||
Type: &FuncProto{Return: (*Void)(nil)},
|
||||
}
|
||||
|
||||
err := probeBTF(fn)
|
||||
if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.EPERM) {
|
||||
return internal.ErrNotSupported
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
var haveFuncLinkage = internal.NewFeatureTest("BTF func linkage", "5.6", func() error {
|
||||
if err := haveProgBTF(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fn := &Func{
|
||||
Name: "a",
|
||||
Type: &FuncProto{Return: (*Void)(nil)},
|
||||
Linkage: GlobalFunc,
|
||||
}
|
||||
|
||||
err := probeBTF(fn)
|
||||
if errors.Is(err, unix.EINVAL) {
|
||||
return internal.ErrNotSupported
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
func probeBTF(typ Type) error {
|
||||
b, err := NewBuilder([]Type{typ})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err := b.Marshal(nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fd, err := sys.BtfLoad(&sys.BtfLoadAttr{
|
||||
Btf: sys.NewSlicePointer(buf),
|
||||
BtfSize: uint32(len(buf)),
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
fd.Close()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
"github.com/cilium/ebpf/internal/testutils"
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func vmlinuxSpec(tb testing.TB) *Spec {
|
||||
tb.Helper()
|
||||
|
||||
// /sys/kernel/btf was introduced in 341dfcf8d78e ("btf: expose BTF info
|
||||
// through sysfs"), which shipped in Linux 5.4.
|
||||
testutils.SkipOnOldKernel(tb, "5.4", "vmlinux BTF in sysfs")
|
||||
|
||||
spec, fallback, err := kernelSpec()
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
if fallback {
|
||||
tb.Fatal("/sys/kernel/btf/vmlinux is not available")
|
||||
}
|
||||
return spec.Copy()
|
||||
}
|
||||
|
||||
type specAndRawBTF struct {
|
||||
raw []byte
|
||||
spec *Spec
|
||||
}
|
||||
|
||||
var vmlinuxTestdata = internal.Memoize(func() (specAndRawBTF, error) {
|
||||
b, err := internal.ReadAllCompressed("testdata/vmlinux.btf.gz")
|
||||
if err != nil {
|
||||
return specAndRawBTF{}, err
|
||||
}
|
||||
|
||||
spec, err := loadRawSpec(bytes.NewReader(b), binary.LittleEndian, nil)
|
||||
if err != nil {
|
||||
return specAndRawBTF{}, err
|
||||
}
|
||||
|
||||
return specAndRawBTF{b, spec}, nil
|
||||
})
|
||||
|
||||
func vmlinuxTestdataReader(tb testing.TB) *bytes.Reader {
|
||||
tb.Helper()
|
||||
|
||||
td, err := vmlinuxTestdata()
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
return bytes.NewReader(td.raw)
|
||||
}
|
||||
|
||||
func vmlinuxTestdataSpec(tb testing.TB) *Spec {
|
||||
tb.Helper()
|
||||
|
||||
td, err := vmlinuxTestdata()
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
return td.spec.Copy()
|
||||
}
|
||||
|
||||
func parseELFBTF(tb testing.TB, file string) *Spec {
|
||||
tb.Helper()
|
||||
|
||||
spec, err := LoadSpec(file)
|
||||
if err != nil {
|
||||
tb.Fatal("Can't load BTF:", err)
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
func TestAnyTypesByName(t *testing.T) {
|
||||
testutils.Files(t, testutils.Glob(t, "testdata/relocs-*.elf"), func(t *testing.T, file string) {
|
||||
spec := parseELFBTF(t, file)
|
||||
|
||||
types, err := spec.AnyTypesByName("ambiguous")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(types) != 1 {
|
||||
t.Fatalf("expected to receive exactly 1 types from querying ambiguous type, got: %v", types)
|
||||
}
|
||||
|
||||
types, err = spec.AnyTypesByName("ambiguous___flavour")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(types) != 1 {
|
||||
t.Fatalf("expected to receive exactly 1 type from querying ambiguous flavour, got: %v", types)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTypeByNameAmbiguous(t *testing.T) {
|
||||
testutils.Files(t, testutils.Glob(t, "testdata/relocs-*.elf"), func(t *testing.T, file string) {
|
||||
spec := parseELFBTF(t, file)
|
||||
|
||||
var typ *Struct
|
||||
if err := spec.TypeByName("ambiguous", &typ); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if name := typ.TypeName(); name != "ambiguous" {
|
||||
t.Fatal("expected type name 'ambiguous', got:", name)
|
||||
}
|
||||
|
||||
if err := spec.TypeByName("ambiguous___flavour", &typ); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if name := typ.TypeName(); name != "ambiguous___flavour" {
|
||||
t.Fatal("expected type name 'ambiguous___flavour', got:", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTypeByName(t *testing.T) {
|
||||
spec := vmlinuxTestdataSpec(t)
|
||||
|
||||
for _, typ := range []interface{}{
|
||||
nil,
|
||||
Struct{},
|
||||
&Struct{},
|
||||
[]Struct{},
|
||||
&[]Struct{},
|
||||
map[int]Struct{},
|
||||
&map[int]Struct{},
|
||||
int(0),
|
||||
new(int),
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%T", typ), func(t *testing.T) {
|
||||
// spec.TypeByName MUST fail if typ is a nil btf.Type.
|
||||
if err := spec.TypeByName("iphdr", typ); err == nil {
|
||||
t.Fatalf("TypeByName does not fail with type %T", typ)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// spec.TypeByName MUST return the same address for multiple calls with the same type name.
|
||||
var iphdr1, iphdr2 *Struct
|
||||
if err := spec.TypeByName("iphdr", &iphdr1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := spec.TypeByName("iphdr", &iphdr2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if iphdr1 != iphdr2 {
|
||||
t.Fatal("multiple TypeByName calls for `iphdr` name do not return the same addresses")
|
||||
}
|
||||
|
||||
// It's valid to pass a *Type to TypeByName.
|
||||
typ := Type(iphdr2)
|
||||
if err := spec.TypeByName("iphdr", &typ); err != nil {
|
||||
t.Fatal("Can't look up using *Type:", err)
|
||||
}
|
||||
|
||||
// Excerpt from linux/ip.h, https://elixir.bootlin.com/linux/latest/A/ident/iphdr
|
||||
//
|
||||
// struct iphdr {
|
||||
// #if defined(__LITTLE_ENDIAN_BITFIELD)
|
||||
// __u8 ihl:4, version:4;
|
||||
// #elif defined (__BIG_ENDIAN_BITFIELD)
|
||||
// __u8 version:4, ihl:4;
|
||||
// #else
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// The BTF we test against is for little endian.
|
||||
m := iphdr1.Members[1]
|
||||
if m.Name != "version" {
|
||||
t.Fatal("Expected version as the second member, got", m.Name)
|
||||
}
|
||||
td, ok := m.Type.(*Typedef)
|
||||
if !ok {
|
||||
t.Fatalf("version member of iphdr should be a __u8 typedef: actual: %T", m.Type)
|
||||
}
|
||||
u8, ok := td.Type.(*Int)
|
||||
if !ok {
|
||||
t.Fatalf("__u8 typedef should point to an Int type: actual: %T", td.Type)
|
||||
}
|
||||
if m.BitfieldSize != 4 {
|
||||
t.Fatalf("incorrect bitfield size: expected: 4 actual: %d", m.BitfieldSize)
|
||||
}
|
||||
if u8.Encoding != 0 {
|
||||
t.Fatalf("incorrect encoding of an __u8 int: expected: 0 actual: %x", u8.Encoding)
|
||||
}
|
||||
if m.Offset != 4 {
|
||||
t.Fatalf("incorrect bitfield offset: expected: 4 actual: %d", m.Offset)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseVmlinux(b *testing.B) {
|
||||
rd := vmlinuxTestdataReader(b)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
if _, err := rd.Seek(0, io.SeekStart); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := loadRawSpec(rd, binary.LittleEndian, nil); err != nil {
|
||||
b.Fatal("Can't load BTF:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCurrentKernelBTF(t *testing.T) {
|
||||
spec := vmlinuxSpec(t)
|
||||
|
||||
if len(spec.namedTypes) == 0 {
|
||||
t.Fatal("Empty kernel BTF")
|
||||
}
|
||||
|
||||
totalBytes := 0
|
||||
distinct := 0
|
||||
seen := make(map[string]bool)
|
||||
for _, str := range spec.strings.strings {
|
||||
totalBytes += len(str)
|
||||
if !seen[str] {
|
||||
distinct++
|
||||
seen[str] = true
|
||||
}
|
||||
}
|
||||
t.Logf("%d strings total, %d distinct", len(spec.strings.strings), distinct)
|
||||
t.Logf("Average string size: %.0f", float64(totalBytes)/float64(len(spec.strings.strings)))
|
||||
}
|
||||
|
||||
func TestFindVMLinux(t *testing.T) {
|
||||
file, err := findVMLinux()
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("Can't find vmlinux:", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
spec, err := loadSpecFromELF(file)
|
||||
if err != nil {
|
||||
t.Fatal("Can't load BTF:", err)
|
||||
}
|
||||
|
||||
if len(spec.namedTypes) == 0 {
|
||||
t.Fatal("Empty kernel BTF")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSpecFromElf(t *testing.T) {
|
||||
testutils.Files(t, testutils.Glob(t, "../testdata/loader-e*.elf"), func(t *testing.T, file string) {
|
||||
spec := parseELFBTF(t, file)
|
||||
|
||||
vt, err := spec.TypeByID(0)
|
||||
if err != nil {
|
||||
t.Error("Can't retrieve void type by ID:", err)
|
||||
}
|
||||
if _, ok := vt.(*Void); !ok {
|
||||
t.Errorf("Expected Void for type id 0, but got: %T", vt)
|
||||
}
|
||||
|
||||
var bpfMapDef *Struct
|
||||
if err := spec.TypeByName("bpf_map_def", &bpfMapDef); err != nil {
|
||||
t.Error("Can't find bpf_map_def:", err)
|
||||
}
|
||||
|
||||
var tmp *Void
|
||||
if err := spec.TypeByName("totally_bogus_type", &tmp); !errors.Is(err, ErrNotFound) {
|
||||
t.Error("TypeByName doesn't return ErrNotFound:", err)
|
||||
}
|
||||
|
||||
var fn *Func
|
||||
if err := spec.TypeByName("global_fn", &fn); err != nil {
|
||||
t.Error("Can't find global_fn():", err)
|
||||
} else {
|
||||
if fn.Linkage != GlobalFunc {
|
||||
t.Error("Expected global linkage:", fn)
|
||||
}
|
||||
}
|
||||
|
||||
var v *Var
|
||||
if err := spec.TypeByName("key3", &v); err != nil {
|
||||
t.Error("Cant find key3:", err)
|
||||
} else {
|
||||
if v.Linkage != GlobalVar {
|
||||
t.Error("Expected global linkage:", v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVerifierError(t *testing.T) {
|
||||
_, err := NewHandle(&Builder{})
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
var ve *internal.VerifierError
|
||||
if !errors.As(err, &ve) {
|
||||
t.Fatalf("expected a VerifierError, got: %v", err)
|
||||
}
|
||||
|
||||
if ve.Truncated {
|
||||
t.Fatalf("expected non-truncated verifier log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadKernelSpec(t *testing.T) {
|
||||
if _, err := os.Stat("/sys/kernel/btf/vmlinux"); os.IsNotExist(err) {
|
||||
t.Skip("/sys/kernel/btf/vmlinux not present")
|
||||
}
|
||||
|
||||
_, err := LoadKernelSpec()
|
||||
if err != nil {
|
||||
t.Fatal("Can't load kernel spec:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuessBTFByteOrder(t *testing.T) {
|
||||
bo := guessRawBTFByteOrder(vmlinuxTestdataReader(t))
|
||||
if bo != binary.LittleEndian {
|
||||
t.Fatalf("Guessed %s instead of %s", bo, binary.LittleEndian)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecCopy(t *testing.T) {
|
||||
spec := parseELFBTF(t, "../testdata/loader-el.elf")
|
||||
|
||||
if len(spec.types) < 1 {
|
||||
t.Fatal("Not enough types")
|
||||
}
|
||||
|
||||
cpy := spec.Copy()
|
||||
for i := range cpy.types {
|
||||
if _, ok := cpy.types[i].(*Void); ok {
|
||||
// Since Void is an empty struct, a Type interface value containing
|
||||
// &Void{} stores (*Void, nil). Since interface equality first compares
|
||||
// the type and then the concrete value, Void is always equal.
|
||||
continue
|
||||
}
|
||||
|
||||
if cpy.types[i] == spec.types[i] {
|
||||
t.Fatalf("Type at index %d is not a copy: %T == %T", i, cpy.types[i], spec.types[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecTypeByID(t *testing.T) {
|
||||
_, err := newSpec().TypeByID(0)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
|
||||
_, err = newSpec().TypeByID(1)
|
||||
qt.Assert(t, err, qt.ErrorIs, ErrNotFound)
|
||||
}
|
||||
|
||||
func TestHaveBTF(t *testing.T) {
|
||||
testutils.CheckFeatureTest(t, haveBTF)
|
||||
}
|
||||
|
||||
func TestHaveMapBTF(t *testing.T) {
|
||||
testutils.CheckFeatureTest(t, haveMapBTF)
|
||||
}
|
||||
|
||||
func TestHaveProgBTF(t *testing.T) {
|
||||
testutils.CheckFeatureTest(t, haveProgBTF)
|
||||
}
|
||||
|
||||
func TestHaveFuncLinkage(t *testing.T) {
|
||||
testutils.CheckFeatureTest(t, haveFuncLinkage)
|
||||
}
|
||||
|
||||
func ExampleSpec_TypeByName() {
|
||||
// Acquire a Spec via one of its constructors.
|
||||
spec := new(Spec)
|
||||
|
||||
// Declare a variable of the desired type
|
||||
var foo *Struct
|
||||
|
||||
if err := spec.TypeByName("foo", &foo); err != nil {
|
||||
// There is no struct with name foo, or there
|
||||
// are multiple possibilities.
|
||||
}
|
||||
|
||||
// We've found struct foo
|
||||
fmt.Println(foo.Name)
|
||||
}
|
||||
|
||||
func TestTypesIterator(t *testing.T) {
|
||||
types := []Type{(*Void)(nil), &Int{Size: 4}, &Int{Size: 2}}
|
||||
|
||||
b, err := NewBuilder(types[1:])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
raw, err := b.Marshal(nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
spec, err := LoadSpecFromReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
iter := spec.Iterate()
|
||||
|
||||
for i, typ := range types {
|
||||
if !iter.Next() {
|
||||
t.Fatal("Iterator ended early at item", i)
|
||||
}
|
||||
|
||||
qt.Assert(t, iter.Type, qt.DeepEquals, typ)
|
||||
}
|
||||
|
||||
if iter.Next() {
|
||||
t.Fatalf("Iterator yielded too many items: %p (%[1]T)", iter.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSplitSpecFromReader(t *testing.T) {
|
||||
spec := vmlinuxTestdataSpec(t)
|
||||
|
||||
f, err := os.Open("testdata/btf_testmod.btf")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
splitSpec, err := LoadSplitSpecFromReader(f, spec)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
typ, err := splitSpec.AnyTypeByName("bpf_testmod_init")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
typeID, err := splitSpec.TypeID(typ)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
typeByID, err := splitSpec.TypeByID(typeID)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, typeByID, qt.Equals, typ)
|
||||
|
||||
fnType := typ.(*Func)
|
||||
fnProto := fnType.Type.(*FuncProto)
|
||||
|
||||
// 'int' is defined in the base BTF...
|
||||
intType, err := spec.AnyTypeByName("int")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// ... but not in the split BTF
|
||||
_, err = splitSpec.AnyTypeByName("int")
|
||||
if err == nil {
|
||||
t.Fatal("'int' is not supposed to be found in the split BTF")
|
||||
}
|
||||
|
||||
if fnProto.Return != intType {
|
||||
t.Fatalf("Return type of 'bpf_testmod_init()' (%s) does not match 'int' type (%s)",
|
||||
fnProto.Return, intType)
|
||||
}
|
||||
|
||||
// Check that copied split-BTF's spec has correct type indexing
|
||||
splitSpecCopy := splitSpec.Copy()
|
||||
copyType, err := splitSpecCopy.AnyTypeByName("bpf_testmod_init")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copyTypeID, err := splitSpecCopy.TypeID(copyType)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if copyTypeID != typeID {
|
||||
t.Fatalf("'bpf_testmod_init` type ID (%d) does not match copied spec's (%d)",
|
||||
typeID, copyTypeID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixupDatasecLayout(t *testing.T) {
|
||||
ds := &Datasec{
|
||||
Size: 0, // Populated by fixup.
|
||||
Vars: []VarSecinfo{
|
||||
{Type: &Var{Type: &Int{Size: 4}}},
|
||||
{Type: &Var{Type: &Int{Size: 1}}},
|
||||
{Type: &Var{Type: &Int{Size: 1}}},
|
||||
{Type: &Var{Type: &Int{Size: 2}}},
|
||||
{Type: &Var{Type: &Int{Size: 16}}},
|
||||
{Type: &Var{Type: &Int{Size: 8}}},
|
||||
},
|
||||
}
|
||||
|
||||
qt.Assert(t, fixupDatasecLayout(ds), qt.IsNil)
|
||||
|
||||
qt.Assert(t, ds.Size, qt.Equals, uint32(40))
|
||||
qt.Assert(t, ds.Vars[0].Offset, qt.Equals, uint32(0))
|
||||
qt.Assert(t, ds.Vars[1].Offset, qt.Equals, uint32(4))
|
||||
qt.Assert(t, ds.Vars[2].Offset, qt.Equals, uint32(5))
|
||||
qt.Assert(t, ds.Vars[3].Offset, qt.Equals, uint32(6))
|
||||
qt.Assert(t, ds.Vars[4].Offset, qt.Equals, uint32(16))
|
||||
qt.Assert(t, ds.Vars[5].Offset, qt.Equals, uint32(32))
|
||||
}
|
||||
|
||||
func BenchmarkSpecCopy(b *testing.B) {
|
||||
spec := vmlinuxTestdataSpec(b)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
spec.Copy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//go:generate stringer -linecomment -output=btf_types_string.go -type=FuncLinkage,VarLinkage,btfKind
|
||||
|
||||
// btfKind describes a Type.
|
||||
type btfKind uint8
|
||||
|
||||
// Equivalents of the BTF_KIND_* constants.
|
||||
const (
|
||||
kindUnknown btfKind = iota // Unknown
|
||||
kindInt // Int
|
||||
kindPointer // Pointer
|
||||
kindArray // Array
|
||||
kindStruct // Struct
|
||||
kindUnion // Union
|
||||
kindEnum // Enum
|
||||
kindForward // Forward
|
||||
kindTypedef // Typedef
|
||||
kindVolatile // Volatile
|
||||
kindConst // Const
|
||||
kindRestrict // Restrict
|
||||
// Added ~4.20
|
||||
kindFunc // Func
|
||||
kindFuncProto // FuncProto
|
||||
// Added ~5.1
|
||||
kindVar // Var
|
||||
kindDatasec // Datasec
|
||||
// Added ~5.13
|
||||
kindFloat // Float
|
||||
// Added 5.16
|
||||
kindDeclTag // DeclTag
|
||||
kindTypeTag // TypeTag
|
||||
// Added 6.0
|
||||
kindEnum64 // Enum64
|
||||
)
|
||||
|
||||
// FuncLinkage describes BTF function linkage metadata.
|
||||
type FuncLinkage int
|
||||
|
||||
// Equivalent of enum btf_func_linkage.
|
||||
const (
|
||||
StaticFunc FuncLinkage = iota // static
|
||||
GlobalFunc // global
|
||||
ExternFunc // extern
|
||||
)
|
||||
|
||||
// VarLinkage describes BTF variable linkage metadata.
|
||||
type VarLinkage int
|
||||
|
||||
const (
|
||||
StaticVar VarLinkage = iota // static
|
||||
GlobalVar // global
|
||||
ExternVar // extern
|
||||
)
|
||||
|
||||
const (
|
||||
btfTypeKindShift = 24
|
||||
btfTypeKindLen = 5
|
||||
btfTypeVlenShift = 0
|
||||
btfTypeVlenMask = 16
|
||||
btfTypeKindFlagShift = 31
|
||||
btfTypeKindFlagMask = 1
|
||||
)
|
||||
|
||||
var btfTypeLen = binary.Size(btfType{})
|
||||
|
||||
// btfType is equivalent to struct btf_type in Documentation/bpf/btf.rst.
|
||||
type btfType struct {
|
||||
NameOff uint32
|
||||
/* "info" bits arrangement
|
||||
* bits 0-15: vlen (e.g. # of struct's members), linkage
|
||||
* bits 16-23: unused
|
||||
* bits 24-28: kind (e.g. int, ptr, array...etc)
|
||||
* bits 29-30: unused
|
||||
* bit 31: kind_flag, currently used by
|
||||
* struct, union and fwd
|
||||
*/
|
||||
Info uint32
|
||||
/* "size" is used by INT, ENUM, STRUCT and UNION.
|
||||
* "size" tells the size of the type it is describing.
|
||||
*
|
||||
* "type" is used by PTR, TYPEDEF, VOLATILE, CONST, RESTRICT,
|
||||
* FUNC and FUNC_PROTO.
|
||||
* "type" is a type_id referring to another type.
|
||||
*/
|
||||
SizeType uint32
|
||||
}
|
||||
|
||||
func mask(len uint32) uint32 {
|
||||
return (1 << len) - 1
|
||||
}
|
||||
|
||||
func readBits(value, len, shift uint32) uint32 {
|
||||
return (value >> shift) & mask(len)
|
||||
}
|
||||
|
||||
func writeBits(value, len, shift, new uint32) uint32 {
|
||||
value &^= mask(len) << shift
|
||||
value |= (new & mask(len)) << shift
|
||||
return value
|
||||
}
|
||||
|
||||
func (bt *btfType) info(len, shift uint32) uint32 {
|
||||
return readBits(bt.Info, len, shift)
|
||||
}
|
||||
|
||||
func (bt *btfType) setInfo(value, len, shift uint32) {
|
||||
bt.Info = writeBits(bt.Info, len, shift, value)
|
||||
}
|
||||
|
||||
func (bt *btfType) Kind() btfKind {
|
||||
return btfKind(bt.info(btfTypeKindLen, btfTypeKindShift))
|
||||
}
|
||||
|
||||
func (bt *btfType) SetKind(kind btfKind) {
|
||||
bt.setInfo(uint32(kind), btfTypeKindLen, btfTypeKindShift)
|
||||
}
|
||||
|
||||
func (bt *btfType) Vlen() int {
|
||||
return int(bt.info(btfTypeVlenMask, btfTypeVlenShift))
|
||||
}
|
||||
|
||||
func (bt *btfType) SetVlen(vlen int) {
|
||||
bt.setInfo(uint32(vlen), btfTypeVlenMask, btfTypeVlenShift)
|
||||
}
|
||||
|
||||
func (bt *btfType) kindFlagBool() bool {
|
||||
return bt.info(btfTypeKindFlagMask, btfTypeKindFlagShift) == 1
|
||||
}
|
||||
|
||||
func (bt *btfType) setKindFlagBool(set bool) {
|
||||
var value uint32
|
||||
if set {
|
||||
value = 1
|
||||
}
|
||||
bt.setInfo(value, btfTypeKindFlagMask, btfTypeKindFlagShift)
|
||||
}
|
||||
|
||||
// Bitfield returns true if the struct or union contain a bitfield.
|
||||
func (bt *btfType) Bitfield() bool {
|
||||
return bt.kindFlagBool()
|
||||
}
|
||||
|
||||
func (bt *btfType) SetBitfield(isBitfield bool) {
|
||||
bt.setKindFlagBool(isBitfield)
|
||||
}
|
||||
|
||||
func (bt *btfType) FwdKind() FwdKind {
|
||||
return FwdKind(bt.info(btfTypeKindFlagMask, btfTypeKindFlagShift))
|
||||
}
|
||||
|
||||
func (bt *btfType) SetFwdKind(kind FwdKind) {
|
||||
bt.setInfo(uint32(kind), btfTypeKindFlagMask, btfTypeKindFlagShift)
|
||||
}
|
||||
|
||||
func (bt *btfType) Signed() bool {
|
||||
return bt.kindFlagBool()
|
||||
}
|
||||
|
||||
func (bt *btfType) SetSigned(signed bool) {
|
||||
bt.setKindFlagBool(signed)
|
||||
}
|
||||
|
||||
func (bt *btfType) Linkage() FuncLinkage {
|
||||
return FuncLinkage(bt.info(btfTypeVlenMask, btfTypeVlenShift))
|
||||
}
|
||||
|
||||
func (bt *btfType) SetLinkage(linkage FuncLinkage) {
|
||||
bt.setInfo(uint32(linkage), btfTypeVlenMask, btfTypeVlenShift)
|
||||
}
|
||||
|
||||
func (bt *btfType) Type() TypeID {
|
||||
// TODO: Panic here if wrong kind?
|
||||
return TypeID(bt.SizeType)
|
||||
}
|
||||
|
||||
func (bt *btfType) SetType(id TypeID) {
|
||||
bt.SizeType = uint32(id)
|
||||
}
|
||||
|
||||
func (bt *btfType) Size() uint32 {
|
||||
// TODO: Panic here if wrong kind?
|
||||
return bt.SizeType
|
||||
}
|
||||
|
||||
func (bt *btfType) SetSize(size uint32) {
|
||||
bt.SizeType = size
|
||||
}
|
||||
|
||||
func (bt *btfType) Marshal(w io.Writer, bo binary.ByteOrder) error {
|
||||
buf := make([]byte, unsafe.Sizeof(*bt))
|
||||
bo.PutUint32(buf[0:], bt.NameOff)
|
||||
bo.PutUint32(buf[4:], bt.Info)
|
||||
bo.PutUint32(buf[8:], bt.SizeType)
|
||||
_, err := w.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
type rawType struct {
|
||||
btfType
|
||||
data interface{}
|
||||
}
|
||||
|
||||
func (rt *rawType) Marshal(w io.Writer, bo binary.ByteOrder) error {
|
||||
if err := rt.btfType.Marshal(w, bo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rt.data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return binary.Write(w, bo, rt.data)
|
||||
}
|
||||
|
||||
// btfInt encodes additional data for integers.
|
||||
//
|
||||
// ? ? ? ? e e e e o o o o o o o o ? ? ? ? ? ? ? ? b b b b b b b b
|
||||
// ? = undefined
|
||||
// e = encoding
|
||||
// o = offset (bitfields?)
|
||||
// b = bits (bitfields)
|
||||
type btfInt struct {
|
||||
Raw uint32
|
||||
}
|
||||
|
||||
const (
|
||||
btfIntEncodingLen = 4
|
||||
btfIntEncodingShift = 24
|
||||
btfIntOffsetLen = 8
|
||||
btfIntOffsetShift = 16
|
||||
btfIntBitsLen = 8
|
||||
btfIntBitsShift = 0
|
||||
)
|
||||
|
||||
func (bi btfInt) Encoding() IntEncoding {
|
||||
return IntEncoding(readBits(bi.Raw, btfIntEncodingLen, btfIntEncodingShift))
|
||||
}
|
||||
|
||||
func (bi *btfInt) SetEncoding(e IntEncoding) {
|
||||
bi.Raw = writeBits(uint32(bi.Raw), btfIntEncodingLen, btfIntEncodingShift, uint32(e))
|
||||
}
|
||||
|
||||
func (bi btfInt) Offset() Bits {
|
||||
return Bits(readBits(bi.Raw, btfIntOffsetLen, btfIntOffsetShift))
|
||||
}
|
||||
|
||||
func (bi *btfInt) SetOffset(offset uint32) {
|
||||
bi.Raw = writeBits(bi.Raw, btfIntOffsetLen, btfIntOffsetShift, offset)
|
||||
}
|
||||
|
||||
func (bi btfInt) Bits() Bits {
|
||||
return Bits(readBits(bi.Raw, btfIntBitsLen, btfIntBitsShift))
|
||||
}
|
||||
|
||||
func (bi *btfInt) SetBits(bits byte) {
|
||||
bi.Raw = writeBits(bi.Raw, btfIntBitsLen, btfIntBitsShift, uint32(bits))
|
||||
}
|
||||
|
||||
type btfArray struct {
|
||||
Type TypeID
|
||||
IndexType TypeID
|
||||
Nelems uint32
|
||||
}
|
||||
|
||||
type btfMember struct {
|
||||
NameOff uint32
|
||||
Type TypeID
|
||||
Offset uint32
|
||||
}
|
||||
|
||||
type btfVarSecinfo struct {
|
||||
Type TypeID
|
||||
Offset uint32
|
||||
Size uint32
|
||||
}
|
||||
|
||||
type btfVariable struct {
|
||||
Linkage uint32
|
||||
}
|
||||
|
||||
type btfEnum struct {
|
||||
NameOff uint32
|
||||
Val uint32
|
||||
}
|
||||
|
||||
type btfEnum64 struct {
|
||||
NameOff uint32
|
||||
ValLo32 uint32
|
||||
ValHi32 uint32
|
||||
}
|
||||
|
||||
type btfParam struct {
|
||||
NameOff uint32
|
||||
Type TypeID
|
||||
}
|
||||
|
||||
type btfDeclTag struct {
|
||||
ComponentIdx uint32
|
||||
}
|
||||
|
||||
func readTypes(r io.Reader, bo binary.ByteOrder, typeLen uint32) ([]rawType, error) {
|
||||
var header btfType
|
||||
// because of the interleaving between types and struct members it is difficult to
|
||||
// precompute the numbers of raw types this will parse
|
||||
// this "guess" is a good first estimation
|
||||
sizeOfbtfType := uintptr(btfTypeLen)
|
||||
tyMaxCount := uintptr(typeLen) / sizeOfbtfType / 2
|
||||
types := make([]rawType, 0, tyMaxCount)
|
||||
|
||||
for id := TypeID(1); ; id++ {
|
||||
if err := binary.Read(r, bo, &header); err == io.EOF {
|
||||
return types, nil
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("can't read type info for id %v: %v", id, err)
|
||||
}
|
||||
|
||||
var data interface{}
|
||||
switch header.Kind() {
|
||||
case kindInt:
|
||||
data = new(btfInt)
|
||||
case kindPointer:
|
||||
case kindArray:
|
||||
data = new(btfArray)
|
||||
case kindStruct:
|
||||
fallthrough
|
||||
case kindUnion:
|
||||
data = make([]btfMember, header.Vlen())
|
||||
case kindEnum:
|
||||
data = make([]btfEnum, header.Vlen())
|
||||
case kindForward:
|
||||
case kindTypedef:
|
||||
case kindVolatile:
|
||||
case kindConst:
|
||||
case kindRestrict:
|
||||
case kindFunc:
|
||||
case kindFuncProto:
|
||||
data = make([]btfParam, header.Vlen())
|
||||
case kindVar:
|
||||
data = new(btfVariable)
|
||||
case kindDatasec:
|
||||
data = make([]btfVarSecinfo, header.Vlen())
|
||||
case kindFloat:
|
||||
case kindDeclTag:
|
||||
data = new(btfDeclTag)
|
||||
case kindTypeTag:
|
||||
case kindEnum64:
|
||||
data = make([]btfEnum64, header.Vlen())
|
||||
default:
|
||||
return nil, fmt.Errorf("type id %v: unknown kind: %v", id, header.Kind())
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
types = append(types, rawType{header, nil})
|
||||
continue
|
||||
}
|
||||
|
||||
if err := binary.Read(r, bo, data); err != nil {
|
||||
return nil, fmt.Errorf("type id %d: kind %v: can't read %T: %v", id, header.Kind(), data, err)
|
||||
}
|
||||
|
||||
types = append(types, rawType{header, data})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Code generated by "stringer -linecomment -output=btf_types_string.go -type=FuncLinkage,VarLinkage,btfKind"; DO NOT EDIT.
|
||||
|
||||
package btf
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[StaticFunc-0]
|
||||
_ = x[GlobalFunc-1]
|
||||
_ = x[ExternFunc-2]
|
||||
}
|
||||
|
||||
const _FuncLinkage_name = "staticglobalextern"
|
||||
|
||||
var _FuncLinkage_index = [...]uint8{0, 6, 12, 18}
|
||||
|
||||
func (i FuncLinkage) String() string {
|
||||
if i < 0 || i >= FuncLinkage(len(_FuncLinkage_index)-1) {
|
||||
return "FuncLinkage(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _FuncLinkage_name[_FuncLinkage_index[i]:_FuncLinkage_index[i+1]]
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[StaticVar-0]
|
||||
_ = x[GlobalVar-1]
|
||||
_ = x[ExternVar-2]
|
||||
}
|
||||
|
||||
const _VarLinkage_name = "staticglobalextern"
|
||||
|
||||
var _VarLinkage_index = [...]uint8{0, 6, 12, 18}
|
||||
|
||||
func (i VarLinkage) String() string {
|
||||
if i < 0 || i >= VarLinkage(len(_VarLinkage_index)-1) {
|
||||
return "VarLinkage(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _VarLinkage_name[_VarLinkage_index[i]:_VarLinkage_index[i+1]]
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[kindUnknown-0]
|
||||
_ = x[kindInt-1]
|
||||
_ = x[kindPointer-2]
|
||||
_ = x[kindArray-3]
|
||||
_ = x[kindStruct-4]
|
||||
_ = x[kindUnion-5]
|
||||
_ = x[kindEnum-6]
|
||||
_ = x[kindForward-7]
|
||||
_ = x[kindTypedef-8]
|
||||
_ = x[kindVolatile-9]
|
||||
_ = x[kindConst-10]
|
||||
_ = x[kindRestrict-11]
|
||||
_ = x[kindFunc-12]
|
||||
_ = x[kindFuncProto-13]
|
||||
_ = x[kindVar-14]
|
||||
_ = x[kindDatasec-15]
|
||||
_ = x[kindFloat-16]
|
||||
_ = x[kindDeclTag-17]
|
||||
_ = x[kindTypeTag-18]
|
||||
_ = x[kindEnum64-19]
|
||||
}
|
||||
|
||||
const _btfKind_name = "UnknownIntPointerArrayStructUnionEnumForwardTypedefVolatileConstRestrictFuncFuncProtoVarDatasecFloatDeclTagTypeTagEnum64"
|
||||
|
||||
var _btfKind_index = [...]uint8{0, 7, 10, 17, 22, 28, 33, 37, 44, 51, 59, 64, 72, 76, 85, 88, 95, 100, 107, 114, 120}
|
||||
|
||||
func (i btfKind) String() string {
|
||||
if i >= btfKind(len(_btfKind_index)-1) {
|
||||
return "btfKind(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _btfKind_name[_btfKind_index[i]:_btfKind_index[i+1]]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
package btf_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
"github.com/cilium/ebpf/btf"
|
||||
"github.com/cilium/ebpf/internal"
|
||||
"github.com/cilium/ebpf/internal/testutils"
|
||||
)
|
||||
|
||||
func TestCORERelocationLoad(t *testing.T) {
|
||||
testutils.Files(t, testutils.Glob(t, "testdata/relocs-*.elf"), func(t *testing.T, file string) {
|
||||
fh, err := os.Open(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(fh)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if spec.ByteOrder != internal.NativeEndian {
|
||||
return
|
||||
}
|
||||
|
||||
for _, progSpec := range spec.Programs {
|
||||
t.Run(progSpec.Name, func(t *testing.T) {
|
||||
if _, err := fh.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
prog, err := ebpf.NewProgramWithOptions(progSpec, ebpf.ProgramOptions{
|
||||
KernelTypes: spec.Types,
|
||||
})
|
||||
|
||||
if strings.HasPrefix(progSpec.Name, "err_") {
|
||||
if err == nil {
|
||||
prog.Close()
|
||||
t.Fatal("Expected an error")
|
||||
}
|
||||
t.Log("Got expected error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Load program:", err)
|
||||
}
|
||||
defer prog.Close()
|
||||
|
||||
ret, _, err := prog.Test(internal.EmptyBPFContext)
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("Error when running:", err)
|
||||
}
|
||||
|
||||
if ret != 0 {
|
||||
t.Error("Assertion failed on line", ret)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCORERelocationRead(t *testing.T) {
|
||||
testutils.Files(t, testutils.Glob(t, "testdata/relocs_read-*.elf"), func(t *testing.T, file string) {
|
||||
spec, err := ebpf.LoadCollectionSpec(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if spec.ByteOrder != internal.NativeEndian {
|
||||
return
|
||||
}
|
||||
|
||||
targetFile := fmt.Sprintf("testdata/relocs_read_tgt-%s.elf", internal.ClangEndian)
|
||||
targetSpec, err := btf.LoadSpec(targetFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, progSpec := range spec.Programs {
|
||||
t.Run(progSpec.Name, func(t *testing.T) {
|
||||
prog, err := ebpf.NewProgramWithOptions(progSpec, ebpf.ProgramOptions{
|
||||
KernelTypes: targetSpec,
|
||||
})
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("Load program:", err)
|
||||
}
|
||||
defer prog.Close()
|
||||
|
||||
ret, _, err := prog.Test(internal.EmptyBPFContext)
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("Error when running:", err)
|
||||
}
|
||||
|
||||
if ret != 0 {
|
||||
t.Error("Assertion failed on line", ret)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,746 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
"github.com/cilium/ebpf/internal/testutils"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestCheckTypeCompatibility(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b Type
|
||||
compatible bool
|
||||
}{
|
||||
{&FuncProto{Return: &Typedef{Type: &Int{}}}, &FuncProto{Return: &Int{}}, true},
|
||||
{&FuncProto{Return: &Typedef{Type: &Int{}}}, &FuncProto{Return: &Void{}}, false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
err := CheckTypeCompatibility(test.a, test.b)
|
||||
if test.compatible {
|
||||
if err != nil {
|
||||
t.Errorf("Expected types to be compatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if !errors.Is(err, errIncompatibleTypes) {
|
||||
t.Errorf("Expected types to be incompatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = CheckTypeCompatibility(test.b, test.a)
|
||||
if test.compatible {
|
||||
if err != nil {
|
||||
t.Errorf("Expected reversed types to be compatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
}
|
||||
} else {
|
||||
if !errors.Is(err, errIncompatibleTypes) {
|
||||
t.Errorf("Expected reversed types to be incompatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCOREAreTypesCompatible(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b Type
|
||||
compatible bool
|
||||
}{
|
||||
{&Void{}, &Void{}, true},
|
||||
{&Struct{Name: "a"}, &Struct{Name: "b"}, true},
|
||||
{&Union{Name: "a"}, &Union{Name: "b"}, true},
|
||||
{&Union{Name: "a"}, &Struct{Name: "b"}, false},
|
||||
{&Enum{Name: "a"}, &Enum{Name: "b"}, true},
|
||||
{&Fwd{Name: "a"}, &Fwd{Name: "b"}, true},
|
||||
{&Int{Name: "a", Size: 2}, &Int{Name: "b", Size: 4}, true},
|
||||
{&Pointer{Target: &Void{}}, &Pointer{Target: &Void{}}, true},
|
||||
{&Pointer{Target: &Void{}}, &Void{}, false},
|
||||
{&Array{Index: &Void{}, Type: &Void{}}, &Array{Index: &Void{}, Type: &Void{}}, true},
|
||||
{&Array{Index: &Void{}, Type: &Int{}}, &Array{Index: &Void{}, Type: &Void{}}, false},
|
||||
{&FuncProto{Return: &Int{}}, &FuncProto{Return: &Void{}}, false},
|
||||
{
|
||||
&FuncProto{Return: &Void{}, Params: []FuncParam{{Name: "a", Type: &Void{}}}},
|
||||
&FuncProto{Return: &Void{}, Params: []FuncParam{{Name: "b", Type: &Void{}}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&FuncProto{Return: &Void{}, Params: []FuncParam{{Type: &Void{}}}},
|
||||
&FuncProto{Return: &Void{}, Params: []FuncParam{{Type: &Int{}}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&FuncProto{Return: &Void{}, Params: []FuncParam{{Type: &Void{}}, {Type: &Void{}}}},
|
||||
&FuncProto{Return: &Void{}, Params: []FuncParam{{Type: &Void{}}}},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := coreAreTypesCompatible(test.a, test.b)
|
||||
if test.compatible {
|
||||
if err != nil {
|
||||
t.Errorf("Expected types to be compatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if !errors.Is(err, errIncompatibleTypes) {
|
||||
t.Errorf("Expected types to be incompatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = coreAreTypesCompatible(test.b, test.a)
|
||||
if test.compatible {
|
||||
if err != nil {
|
||||
t.Errorf("Expected reversed types to be compatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
}
|
||||
} else {
|
||||
if !errors.Is(err, errIncompatibleTypes) {
|
||||
t.Errorf("Expected reversed types to be incompatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, invalid := range []Type{&Var{}, &Datasec{}} {
|
||||
err := coreAreTypesCompatible(invalid, invalid)
|
||||
if errors.Is(err, errIncompatibleTypes) {
|
||||
t.Errorf("Expected an error for %T, not errIncompatibleTypes", invalid)
|
||||
} else if err == nil {
|
||||
t.Errorf("Expected an error for %T", invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCOREAreMembersCompatible(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b Type
|
||||
compatible bool
|
||||
}{
|
||||
{&Struct{Name: "a"}, &Struct{Name: "b"}, true},
|
||||
{&Union{Name: "a"}, &Union{Name: "b"}, true},
|
||||
{&Union{Name: "a"}, &Struct{Name: "b"}, true},
|
||||
{&Enum{Name: "a"}, &Enum{Name: "b"}, false},
|
||||
{&Enum{Name: "a"}, &Enum{Name: "a___foo"}, true},
|
||||
{&Enum{Name: "a"}, &Enum{Name: ""}, true},
|
||||
{&Fwd{Name: "a"}, &Fwd{Name: "b"}, false},
|
||||
{&Fwd{Name: "a"}, &Fwd{Name: "a___foo"}, true},
|
||||
{&Fwd{Name: "a"}, &Fwd{Name: ""}, true},
|
||||
{&Int{Name: "a", Size: 2}, &Int{Name: "b", Size: 4}, true},
|
||||
{&Pointer{Target: &Void{}}, &Pointer{Target: &Void{}}, true},
|
||||
{&Pointer{Target: &Void{}}, &Void{}, false},
|
||||
{&Array{Type: &Int{Size: 1}}, &Array{Type: &Int{Encoding: Signed}}, true},
|
||||
{&Float{Size: 2}, &Float{Size: 4}, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := coreAreMembersCompatible(test.a, test.b)
|
||||
if test.compatible {
|
||||
if err != nil {
|
||||
t.Errorf("Expected members to be compatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if !errors.Is(err, errImpossibleRelocation) {
|
||||
t.Errorf("Expected members to be incompatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = coreAreMembersCompatible(test.b, test.a)
|
||||
if test.compatible {
|
||||
if err != nil {
|
||||
t.Errorf("Expected reversed members to be compatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
}
|
||||
} else {
|
||||
if !errors.Is(err, errImpossibleRelocation) {
|
||||
t.Errorf("Expected reversed members to be incompatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, invalid := range []Type{&Void{}, &FuncProto{}, &Var{}, &Datasec{}} {
|
||||
err := coreAreMembersCompatible(invalid, invalid)
|
||||
if errors.Is(err, errImpossibleRelocation) {
|
||||
t.Errorf("Expected an error for %T, not errImpossibleRelocation", invalid)
|
||||
} else if err == nil {
|
||||
t.Errorf("Expected an error for %T", invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCOREAccessor(t *testing.T) {
|
||||
for _, valid := range []string{
|
||||
"0",
|
||||
"1:0",
|
||||
"1:0:3:34:10:1",
|
||||
} {
|
||||
_, err := parseCOREAccessor(valid)
|
||||
if err != nil {
|
||||
t.Errorf("Parse %q: %s", valid, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, invalid := range []string{
|
||||
"",
|
||||
"-1",
|
||||
":",
|
||||
"0:",
|
||||
":12",
|
||||
"4294967296",
|
||||
} {
|
||||
_, err := parseCOREAccessor(invalid)
|
||||
if err == nil {
|
||||
t.Errorf("Accepted invalid accessor %q", invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCOREFindEnumValue(t *testing.T) {
|
||||
a := &Enum{Values: []EnumValue{{"foo", 23}, {"bar", 42}}}
|
||||
b := &Enum{Values: []EnumValue{
|
||||
{"foo___flavour", 0},
|
||||
{"bar", 123},
|
||||
{"garbage", 3},
|
||||
}}
|
||||
|
||||
invalid := []struct {
|
||||
name string
|
||||
local Type
|
||||
target Type
|
||||
acc coreAccessor
|
||||
err error
|
||||
}{
|
||||
{"o-o-b accessor", a, b, coreAccessor{len(a.Values)}, nil},
|
||||
{"long accessor", a, b, coreAccessor{0, 1}, nil},
|
||||
{"wrong target", a, &Void{}, coreAccessor{0, 1}, nil},
|
||||
{
|
||||
"no matching value",
|
||||
b, a,
|
||||
coreAccessor{2},
|
||||
errImpossibleRelocation,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range invalid {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, _, err := coreFindEnumValue(test.local, test.acc, test.target)
|
||||
if test.err != nil && !errors.Is(err, test.err) {
|
||||
t.Fatalf("Expected %s, got %s", test.err, err)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("Accepted invalid case")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
valid := []struct {
|
||||
name string
|
||||
local, target Type
|
||||
acc coreAccessor
|
||||
localValue, targetValue uint64
|
||||
}{
|
||||
{"a to b", a, b, coreAccessor{0}, 23, 0},
|
||||
{"b to a", b, a, coreAccessor{1}, 123, 42},
|
||||
}
|
||||
|
||||
for _, test := range valid {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
local, target, err := coreFindEnumValue(test.local, test.acc, test.target)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Check(t, local.Value, qt.Equals, test.localValue)
|
||||
qt.Check(t, target.Value, qt.Equals, test.targetValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCOREFindField(t *testing.T) {
|
||||
ptr := &Pointer{}
|
||||
u16 := &Int{Size: 2}
|
||||
u32 := &Int{Size: 4}
|
||||
aFields := []Member{
|
||||
{Name: "foo", Type: ptr, Offset: 8},
|
||||
{Name: "bar", Type: u16, Offset: 16},
|
||||
{Name: "baz", Type: u32, Offset: 32, BitfieldSize: 3},
|
||||
{Name: "quux", Type: u32, Offset: 35, BitfieldSize: 10},
|
||||
{Name: "quuz", Type: u32, Offset: 45, BitfieldSize: 8},
|
||||
}
|
||||
bFields := []Member{
|
||||
{Name: "foo", Type: ptr, Offset: 16},
|
||||
{Name: "bar", Type: u32, Offset: 8},
|
||||
{Name: "other", Offset: 4},
|
||||
// baz is separated out from the other bitfields
|
||||
{Name: "baz", Type: u32, Offset: 64, BitfieldSize: 3},
|
||||
// quux's type changes u32->u16
|
||||
{Name: "quux", Type: u16, Offset: 96, BitfieldSize: 10},
|
||||
// quuz becomes a normal field
|
||||
{Name: "quuz", Type: u16, Offset: 112},
|
||||
}
|
||||
|
||||
aStruct := &Struct{Members: aFields, Size: 48}
|
||||
bStruct := &Struct{Members: bFields, Size: 80}
|
||||
aArray := &Array{Nelems: 4, Type: u16}
|
||||
bArray := &Array{Nelems: 3, Type: u32}
|
||||
|
||||
invalid := []struct {
|
||||
name string
|
||||
local, target Type
|
||||
acc coreAccessor
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"unsupported type",
|
||||
&Void{}, &Void{},
|
||||
coreAccessor{0, 0},
|
||||
ErrNotSupported,
|
||||
},
|
||||
{
|
||||
"different types",
|
||||
&Union{}, &Array{Type: u16},
|
||||
coreAccessor{0},
|
||||
errImpossibleRelocation,
|
||||
},
|
||||
{
|
||||
"invalid composite accessor",
|
||||
aStruct, aStruct,
|
||||
coreAccessor{0, len(aStruct.Members)},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"invalid array accessor",
|
||||
aArray, aArray,
|
||||
coreAccessor{0, int(aArray.Nelems)},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"o-o-b array accessor",
|
||||
aArray, bArray,
|
||||
coreAccessor{0, int(bArray.Nelems)},
|
||||
errImpossibleRelocation,
|
||||
},
|
||||
{
|
||||
"no match",
|
||||
bStruct, aStruct,
|
||||
coreAccessor{0, 2},
|
||||
errImpossibleRelocation,
|
||||
},
|
||||
{
|
||||
"incompatible match",
|
||||
&Union{Members: []Member{{Name: "foo", Type: &Pointer{}}}},
|
||||
&Union{Members: []Member{{Name: "foo", Type: &Int{}}}},
|
||||
coreAccessor{0, 0},
|
||||
errImpossibleRelocation,
|
||||
},
|
||||
{
|
||||
"unsized type",
|
||||
bStruct, &Func{},
|
||||
// non-zero accessor to force calculating the offset.
|
||||
coreAccessor{1},
|
||||
errImpossibleRelocation,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range invalid {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, _, err := coreFindField(test.local, test.acc, test.target)
|
||||
if test.err != nil && !errors.Is(err, test.err) {
|
||||
t.Fatalf("Expected %s, got %s", test.err, err)
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("Accepted invalid case")
|
||||
}
|
||||
t.Log(err)
|
||||
})
|
||||
}
|
||||
|
||||
bytes := func(typ Type) uint32 {
|
||||
sz, err := Sizeof(typ)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return uint32(sz)
|
||||
}
|
||||
|
||||
anon := func(t Type, offset Bits) []Member {
|
||||
return []Member{{Type: t, Offset: offset}}
|
||||
}
|
||||
|
||||
anonStruct := func(m ...Member) Member {
|
||||
return Member{Type: &Struct{Members: m}}
|
||||
}
|
||||
|
||||
anonUnion := func(m ...Member) Member {
|
||||
return Member{Type: &Union{Members: m}}
|
||||
}
|
||||
|
||||
valid := []struct {
|
||||
name string
|
||||
local Type
|
||||
target Type
|
||||
acc coreAccessor
|
||||
localField, targetField coreField
|
||||
}{
|
||||
{
|
||||
"array[0]",
|
||||
aArray,
|
||||
bArray,
|
||||
coreAccessor{0, 0},
|
||||
coreField{u16, 0, 0, 0},
|
||||
coreField{u32, 0, 0, 0},
|
||||
},
|
||||
{
|
||||
"array[1]",
|
||||
aArray,
|
||||
bArray,
|
||||
coreAccessor{0, 1},
|
||||
coreField{u16, bytes(aArray.Type), 0, 0},
|
||||
coreField{u32, bytes(bArray.Type), 0, 0},
|
||||
},
|
||||
{
|
||||
"array[0] with base offset",
|
||||
aArray,
|
||||
bArray,
|
||||
coreAccessor{1, 0},
|
||||
coreField{u16, bytes(aArray), 0, 0},
|
||||
coreField{u32, bytes(bArray), 0, 0},
|
||||
},
|
||||
{
|
||||
"array[2] with base offset",
|
||||
aArray,
|
||||
bArray,
|
||||
coreAccessor{1, 2},
|
||||
coreField{u16, bytes(aArray) + 2*bytes(aArray.Type), 0, 0},
|
||||
coreField{u32, bytes(bArray) + 2*bytes(bArray.Type), 0, 0},
|
||||
},
|
||||
{
|
||||
"flex array",
|
||||
&Struct{Members: []Member{{Name: "foo", Type: &Array{Nelems: 0, Type: u16}}}},
|
||||
&Struct{Members: []Member{{Name: "foo", Type: &Array{Nelems: 0, Type: u32}}}},
|
||||
coreAccessor{0, 0, 9000},
|
||||
coreField{u16, bytes(u16) * 9000, 0, 0},
|
||||
coreField{u32, bytes(u32) * 9000, 0, 0},
|
||||
},
|
||||
{
|
||||
"struct.0",
|
||||
aStruct, bStruct,
|
||||
coreAccessor{0, 0},
|
||||
coreField{ptr, 1, 0, 0},
|
||||
coreField{ptr, 2, 0, 0},
|
||||
},
|
||||
{
|
||||
"struct.0 anon",
|
||||
aStruct, &Struct{Members: anon(bStruct, 24)},
|
||||
coreAccessor{0, 0},
|
||||
coreField{ptr, 1, 0, 0},
|
||||
coreField{ptr, 3 + 2, 0, 0},
|
||||
},
|
||||
{
|
||||
"struct.0 with base offset",
|
||||
aStruct, bStruct,
|
||||
coreAccessor{3, 0},
|
||||
coreField{ptr, 3*bytes(aStruct) + 1, 0, 0},
|
||||
coreField{ptr, 3*bytes(bStruct) + 2, 0, 0},
|
||||
},
|
||||
{
|
||||
"struct.1",
|
||||
aStruct, bStruct,
|
||||
coreAccessor{0, 1},
|
||||
coreField{u16, 2, 0, 0},
|
||||
coreField{u32, 1, 0, 0},
|
||||
},
|
||||
{
|
||||
"struct.1 anon",
|
||||
aStruct, &Struct{Members: anon(bStruct, 24)},
|
||||
coreAccessor{0, 1},
|
||||
coreField{u16, 2, 0, 0},
|
||||
coreField{u32, 3 + 1, 0, 0},
|
||||
},
|
||||
{
|
||||
"union.1",
|
||||
&Union{Members: aFields, Size: 32},
|
||||
&Union{Members: bFields, Size: 32},
|
||||
coreAccessor{0, 1},
|
||||
coreField{u16, 2, 0, 0},
|
||||
coreField{u32, 1, 0, 0},
|
||||
},
|
||||
{
|
||||
"interchangeable composites",
|
||||
&Struct{
|
||||
Members: []Member{
|
||||
anonStruct(anonUnion(Member{Name: "_1", Type: u16})),
|
||||
},
|
||||
},
|
||||
&Struct{
|
||||
Members: []Member{
|
||||
anonUnion(anonStruct(Member{Name: "_1", Type: u16})),
|
||||
},
|
||||
},
|
||||
coreAccessor{0, 0, 0, 0},
|
||||
coreField{u16, 0, 0, 0},
|
||||
coreField{u16, 0, 0, 0},
|
||||
},
|
||||
{
|
||||
"struct.2 (bitfield baz)",
|
||||
aStruct, bStruct,
|
||||
coreAccessor{0, 2},
|
||||
coreField{u32, 4, 0, 3},
|
||||
coreField{u32, 8, 0, 3},
|
||||
},
|
||||
{
|
||||
"struct.3 (bitfield quux)",
|
||||
aStruct, bStruct,
|
||||
coreAccessor{0, 3},
|
||||
coreField{u32, 4, 3, 10},
|
||||
coreField{u16, 12, 0, 10},
|
||||
},
|
||||
{
|
||||
"struct.4 (bitfield quuz)",
|
||||
aStruct, bStruct,
|
||||
coreAccessor{0, 4},
|
||||
coreField{u32, 4, 13, 8},
|
||||
coreField{u16, 14, 0, 0},
|
||||
},
|
||||
}
|
||||
|
||||
allowCoreField := cmp.AllowUnexported(coreField{})
|
||||
|
||||
checkCOREField := func(t *testing.T, which string, got, want coreField) {
|
||||
t.Helper()
|
||||
if diff := cmp.Diff(want, got, allowCoreField); diff != "" {
|
||||
t.Errorf("%s mismatch (-want +got):\n%s", which, diff)
|
||||
}
|
||||
}
|
||||
|
||||
for _, test := range valid {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
localField, targetField, err := coreFindField(test.local, test.acc, test.target)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
checkCOREField(t, "local", localField, test.localField)
|
||||
checkCOREField(t, "target", targetField, test.targetField)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCOREFindFieldCyclical(t *testing.T) {
|
||||
members := []Member{{Name: "foo", Type: &Pointer{}}}
|
||||
|
||||
cyclicStruct := &Struct{}
|
||||
cyclicStruct.Members = []Member{{Type: cyclicStruct}}
|
||||
|
||||
cyclicUnion := &Union{}
|
||||
cyclicUnion.Members = []Member{{Type: cyclicUnion}}
|
||||
|
||||
cyclicArray := &Array{Nelems: 1}
|
||||
cyclicArray.Type = &Pointer{Target: cyclicArray}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
local, cyclic Type
|
||||
}{
|
||||
{"struct", &Struct{Members: members}, cyclicStruct},
|
||||
{"union", &Union{Members: members}, cyclicUnion},
|
||||
{"array", &Array{Nelems: 2, Type: &Int{}}, cyclicArray},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, _, err := coreFindField(test.local, coreAccessor{0, 0}, test.cyclic)
|
||||
if !errors.Is(err, errImpossibleRelocation) {
|
||||
t.Fatal("Should return errImpossibleRelocation, got", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORERelocation(t *testing.T) {
|
||||
testutils.Files(t, testutils.Glob(t, "testdata/*.elf"), func(t *testing.T, file string) {
|
||||
rd, err := os.Open(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rd.Close()
|
||||
|
||||
spec, extInfos, err := LoadSpecAndExtInfosFromReader(rd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if extInfos == nil {
|
||||
t.Skip("No ext_infos")
|
||||
}
|
||||
|
||||
errs := map[string]error{
|
||||
"err_ambiguous": errAmbiguousRelocation,
|
||||
"err_ambiguous_flavour": errAmbiguousRelocation,
|
||||
}
|
||||
|
||||
for section := range extInfos.funcInfos {
|
||||
name := strings.TrimPrefix(section, "socket_filter/")
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var relos []*CORERelocation
|
||||
for _, reloInfo := range extInfos.relocationInfos[section] {
|
||||
relos = append(relos, reloInfo.relo)
|
||||
}
|
||||
|
||||
fixups, err := CORERelocate(relos, spec, spec.byteOrder)
|
||||
if want := errs[name]; want != nil {
|
||||
if !errors.Is(err, want) {
|
||||
t.Fatal("Expected", want, "got", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Can't relocate against itself:", err)
|
||||
}
|
||||
|
||||
for offset, fixup := range fixups {
|
||||
if want := fixup.local; !fixup.skipLocalValidation && want != fixup.target {
|
||||
// Since we're relocating against ourselves both values
|
||||
// should match.
|
||||
t.Errorf("offset %d: local %v doesn't match target %d (kind %s)", offset, fixup.local, fixup.target, fixup.kind)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCORECopyWithoutQualifiers(t *testing.T) {
|
||||
qualifiers := []struct {
|
||||
name string
|
||||
fn func(Type) Type
|
||||
}{
|
||||
{"const", func(t Type) Type { return &Const{Type: t} }},
|
||||
{"volatile", func(t Type) Type { return &Volatile{Type: t} }},
|
||||
{"restrict", func(t Type) Type { return &Restrict{Type: t} }},
|
||||
{"typedef", func(t Type) Type { return &Typedef{Type: t} }},
|
||||
}
|
||||
|
||||
for _, test := range qualifiers {
|
||||
t.Run(test.name+" cycle", func(t *testing.T) {
|
||||
root := &Volatile{}
|
||||
root.Type = test.fn(root)
|
||||
|
||||
cycle, ok := Copy(root, UnderlyingType).(*cycle)
|
||||
qt.Assert(t, ok, qt.IsTrue)
|
||||
qt.Assert(t, cycle.root, qt.Equals, root)
|
||||
})
|
||||
}
|
||||
|
||||
for _, a := range qualifiers {
|
||||
for _, b := range qualifiers {
|
||||
t.Run(a.name+" "+b.name, func(t *testing.T) {
|
||||
v := a.fn(&Pointer{Target: b.fn(&Int{Name: "z"})})
|
||||
want := &Pointer{Target: &Int{Name: "z"}}
|
||||
|
||||
got := Copy(v, UnderlyingType)
|
||||
qt.Assert(t, got, qt.DeepEquals, want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("long chain", func(t *testing.T) {
|
||||
root := &Int{Name: "abc"}
|
||||
v := Type(root)
|
||||
for i := 0; i < maxTypeDepth; i++ {
|
||||
q := qualifiers[testutils.Rand().Intn(len(qualifiers))]
|
||||
v = q.fn(v)
|
||||
t.Log(q.name)
|
||||
}
|
||||
|
||||
got := Copy(v, UnderlyingType)
|
||||
qt.Assert(t, got, qt.DeepEquals, root)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCOREReloFieldSigned(t *testing.T) {
|
||||
for _, typ := range []Type{&Int{}, &Enum{}} {
|
||||
t.Run(fmt.Sprintf("%T with invalid target", typ), func(t *testing.T) {
|
||||
relo := &CORERelocation{
|
||||
typ, coreAccessor{0}, reloFieldSigned, 0,
|
||||
}
|
||||
fixup, err := coreCalculateFixup(relo, &Void{}, 0, internal.NativeEndian)
|
||||
qt.Assert(t, fixup.poison, qt.IsTrue)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("type without signedness", func(t *testing.T) {
|
||||
relo := &CORERelocation{
|
||||
&Array{}, coreAccessor{0}, reloFieldSigned, 0,
|
||||
}
|
||||
_, err := coreCalculateFixup(relo, &Array{}, 0, internal.NativeEndian)
|
||||
qt.Assert(t, err, qt.ErrorIs, errNoSignedness)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCOREReloFieldShiftU64(t *testing.T) {
|
||||
typ := &Struct{
|
||||
Members: []Member{
|
||||
{Name: "A", Type: &Fwd{}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, relo := range []*CORERelocation{
|
||||
{typ, coreAccessor{0, 0}, reloFieldRShiftU64, 1},
|
||||
{typ, coreAccessor{0, 0}, reloFieldLShiftU64, 1},
|
||||
} {
|
||||
t.Run(relo.kind.String(), func(t *testing.T) {
|
||||
_, err := coreCalculateFixup(relo, typ, 1, internal.NativeEndian)
|
||||
qt.Assert(t, err, qt.ErrorIs, errUnsizedType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCORESkBuff(b *testing.B) {
|
||||
spec := vmlinuxTestdataSpec(b)
|
||||
|
||||
var skb *Struct
|
||||
err := spec.TypeByName("sk_buff", &skb)
|
||||
qt.Assert(b, err, qt.IsNil)
|
||||
|
||||
skbID, err := spec.TypeID(skb)
|
||||
qt.Assert(b, err, qt.IsNil)
|
||||
|
||||
var pktHashTypes *Enum
|
||||
err = spec.TypeByName("pkt_hash_types", &pktHashTypes)
|
||||
qt.Assert(b, err, qt.IsNil)
|
||||
|
||||
pktHashTypesID, err := spec.TypeID(pktHashTypes)
|
||||
qt.Assert(b, err, qt.IsNil)
|
||||
|
||||
for _, relo := range []*CORERelocation{
|
||||
{skb, coreAccessor{0, 0}, reloFieldByteOffset, skbID},
|
||||
{skb, coreAccessor{0, 0}, reloFieldByteSize, skbID},
|
||||
{skb, coreAccessor{0, 0}, reloFieldExists, skbID},
|
||||
{skb, coreAccessor{0, 0}, reloFieldSigned, skbID},
|
||||
{skb, coreAccessor{0, 0}, reloFieldLShiftU64, skbID},
|
||||
{skb, coreAccessor{0, 0}, reloFieldRShiftU64, skbID},
|
||||
{skb, coreAccessor{0}, reloTypeIDLocal, skbID},
|
||||
{skb, coreAccessor{0}, reloTypeIDTarget, skbID},
|
||||
{skb, coreAccessor{0}, reloTypeExists, skbID},
|
||||
{skb, coreAccessor{0}, reloTypeSize, skbID},
|
||||
{pktHashTypes, coreAccessor{0}, reloEnumvalExists, pktHashTypesID},
|
||||
{pktHashTypes, coreAccessor{0}, reloEnumvalValue, pktHashTypesID},
|
||||
} {
|
||||
b.Run(relo.kind.String(), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err = CORERelocate([]*CORERelocation{relo}, spec, spec.byteOrder)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Package btf handles data encoded according to the BPF Type Format.
|
||||
//
|
||||
// The canonical documentation lives in the Linux kernel repository and is
|
||||
// available at https://www.kernel.org/doc/html/latest/bpf/btf.html
|
||||
package btf
|
||||
@@ -0,0 +1,768 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"github.com/cilium/ebpf/asm"
|
||||
"github.com/cilium/ebpf/internal"
|
||||
)
|
||||
|
||||
// ExtInfos contains ELF section metadata.
|
||||
type ExtInfos struct {
|
||||
// The slices are sorted by offset in ascending order.
|
||||
funcInfos map[string][]funcInfo
|
||||
lineInfos map[string][]lineInfo
|
||||
relocationInfos map[string][]coreRelocationInfo
|
||||
}
|
||||
|
||||
// loadExtInfosFromELF parses ext infos from the .BTF.ext section in an ELF.
|
||||
//
|
||||
// Returns an error wrapping ErrNotFound if no ext infos are present.
|
||||
func loadExtInfosFromELF(file *internal.SafeELFFile, spec *Spec) (*ExtInfos, error) {
|
||||
section := file.Section(".BTF.ext")
|
||||
if section == nil {
|
||||
return nil, fmt.Errorf("btf ext infos: %w", ErrNotFound)
|
||||
}
|
||||
|
||||
if section.ReaderAt == nil {
|
||||
return nil, fmt.Errorf("compressed ext_info is not supported")
|
||||
}
|
||||
|
||||
return loadExtInfos(section.ReaderAt, file.ByteOrder, spec, spec.strings)
|
||||
}
|
||||
|
||||
// loadExtInfos parses bare ext infos.
|
||||
func loadExtInfos(r io.ReaderAt, bo binary.ByteOrder, spec *Spec, strings *stringTable) (*ExtInfos, error) {
|
||||
// Open unbuffered section reader. binary.Read() calls io.ReadFull on
|
||||
// the header structs, resulting in one syscall per header.
|
||||
headerRd := io.NewSectionReader(r, 0, math.MaxInt64)
|
||||
extHeader, err := parseBTFExtHeader(headerRd, bo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing BTF extension header: %w", err)
|
||||
}
|
||||
|
||||
coreHeader, err := parseBTFExtCOREHeader(headerRd, bo, extHeader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing BTF CO-RE header: %w", err)
|
||||
}
|
||||
|
||||
buf := internal.NewBufferedSectionReader(r, extHeader.funcInfoStart(), int64(extHeader.FuncInfoLen))
|
||||
btfFuncInfos, err := parseFuncInfos(buf, bo, strings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing BTF function info: %w", err)
|
||||
}
|
||||
|
||||
funcInfos := make(map[string][]funcInfo, len(btfFuncInfos))
|
||||
for section, bfis := range btfFuncInfos {
|
||||
funcInfos[section], err = newFuncInfos(bfis, spec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("section %s: func infos: %w", section, err)
|
||||
}
|
||||
}
|
||||
|
||||
buf = internal.NewBufferedSectionReader(r, extHeader.lineInfoStart(), int64(extHeader.LineInfoLen))
|
||||
btfLineInfos, err := parseLineInfos(buf, bo, strings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing BTF line info: %w", err)
|
||||
}
|
||||
|
||||
lineInfos := make(map[string][]lineInfo, len(btfLineInfos))
|
||||
for section, blis := range btfLineInfos {
|
||||
lineInfos[section], err = newLineInfos(blis, strings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("section %s: line infos: %w", section, err)
|
||||
}
|
||||
}
|
||||
|
||||
if coreHeader == nil || coreHeader.COREReloLen == 0 {
|
||||
return &ExtInfos{funcInfos, lineInfos, nil}, nil
|
||||
}
|
||||
|
||||
var btfCORERelos map[string][]bpfCORERelo
|
||||
buf = internal.NewBufferedSectionReader(r, extHeader.coreReloStart(coreHeader), int64(coreHeader.COREReloLen))
|
||||
btfCORERelos, err = parseCORERelos(buf, bo, strings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing CO-RE relocation info: %w", err)
|
||||
}
|
||||
|
||||
coreRelos := make(map[string][]coreRelocationInfo, len(btfCORERelos))
|
||||
for section, brs := range btfCORERelos {
|
||||
coreRelos[section], err = newRelocationInfos(brs, spec, strings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("section %s: CO-RE relocations: %w", section, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &ExtInfos{funcInfos, lineInfos, coreRelos}, nil
|
||||
}
|
||||
|
||||
type funcInfoMeta struct{}
|
||||
type coreRelocationMeta struct{}
|
||||
|
||||
// Assign per-section metadata from BTF to a section's instructions.
|
||||
func (ei *ExtInfos) Assign(insns asm.Instructions, section string) {
|
||||
funcInfos := ei.funcInfos[section]
|
||||
lineInfos := ei.lineInfos[section]
|
||||
reloInfos := ei.relocationInfos[section]
|
||||
|
||||
iter := insns.Iterate()
|
||||
for iter.Next() {
|
||||
if len(funcInfos) > 0 && funcInfos[0].offset == iter.Offset {
|
||||
*iter.Ins = WithFuncMetadata(*iter.Ins, funcInfos[0].fn)
|
||||
funcInfos = funcInfos[1:]
|
||||
}
|
||||
|
||||
if len(lineInfos) > 0 && lineInfos[0].offset == iter.Offset {
|
||||
*iter.Ins = iter.Ins.WithSource(lineInfos[0].line)
|
||||
lineInfos = lineInfos[1:]
|
||||
}
|
||||
|
||||
if len(reloInfos) > 0 && reloInfos[0].offset == iter.Offset {
|
||||
iter.Ins.Metadata.Set(coreRelocationMeta{}, reloInfos[0].relo)
|
||||
reloInfos = reloInfos[1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalExtInfos encodes function and line info embedded in insns into kernel
|
||||
// wire format.
|
||||
//
|
||||
// Returns ErrNotSupported if the kernel doesn't support BTF-associated programs.
|
||||
func MarshalExtInfos(insns asm.Instructions) (_ *Handle, funcInfos, lineInfos []byte, _ error) {
|
||||
// Bail out early if the kernel doesn't support Func(Proto). If this is the
|
||||
// case, func_info will also be unsupported.
|
||||
if err := haveProgBTF(); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
iter := insns.Iterate()
|
||||
for iter.Next() {
|
||||
_, ok := iter.Ins.Source().(*Line)
|
||||
fn := FuncMetadata(iter.Ins)
|
||||
if ok || fn != nil {
|
||||
goto marshal
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, nil, nil
|
||||
|
||||
marshal:
|
||||
var b Builder
|
||||
var fiBuf, liBuf bytes.Buffer
|
||||
for {
|
||||
if fn := FuncMetadata(iter.Ins); fn != nil {
|
||||
fi := &funcInfo{
|
||||
fn: fn,
|
||||
offset: iter.Offset,
|
||||
}
|
||||
if err := fi.marshal(&fiBuf, &b); err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("write func info: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if line, ok := iter.Ins.Source().(*Line); ok {
|
||||
li := &lineInfo{
|
||||
line: line,
|
||||
offset: iter.Offset,
|
||||
}
|
||||
if err := li.marshal(&liBuf, &b); err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("write line info: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !iter.Next() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
handle, err := NewHandle(&b)
|
||||
return handle, fiBuf.Bytes(), liBuf.Bytes(), err
|
||||
}
|
||||
|
||||
// btfExtHeader is found at the start of the .BTF.ext section.
|
||||
type btfExtHeader struct {
|
||||
Magic uint16
|
||||
Version uint8
|
||||
Flags uint8
|
||||
|
||||
// HdrLen is larger than the size of struct btfExtHeader when it is
|
||||
// immediately followed by a btfExtCOREHeader.
|
||||
HdrLen uint32
|
||||
|
||||
FuncInfoOff uint32
|
||||
FuncInfoLen uint32
|
||||
LineInfoOff uint32
|
||||
LineInfoLen uint32
|
||||
}
|
||||
|
||||
// parseBTFExtHeader parses the header of the .BTF.ext section.
|
||||
func parseBTFExtHeader(r io.Reader, bo binary.ByteOrder) (*btfExtHeader, error) {
|
||||
var header btfExtHeader
|
||||
if err := binary.Read(r, bo, &header); err != nil {
|
||||
return nil, fmt.Errorf("can't read header: %v", err)
|
||||
}
|
||||
|
||||
if header.Magic != btfMagic {
|
||||
return nil, fmt.Errorf("incorrect magic value %v", header.Magic)
|
||||
}
|
||||
|
||||
if header.Version != 1 {
|
||||
return nil, fmt.Errorf("unexpected version %v", header.Version)
|
||||
}
|
||||
|
||||
if header.Flags != 0 {
|
||||
return nil, fmt.Errorf("unsupported flags %v", header.Flags)
|
||||
}
|
||||
|
||||
if int64(header.HdrLen) < int64(binary.Size(&header)) {
|
||||
return nil, fmt.Errorf("header length shorter than btfExtHeader size")
|
||||
}
|
||||
|
||||
return &header, nil
|
||||
}
|
||||
|
||||
// funcInfoStart returns the offset from the beginning of the .BTF.ext section
|
||||
// to the start of its func_info entries.
|
||||
func (h *btfExtHeader) funcInfoStart() int64 {
|
||||
return int64(h.HdrLen + h.FuncInfoOff)
|
||||
}
|
||||
|
||||
// lineInfoStart returns the offset from the beginning of the .BTF.ext section
|
||||
// to the start of its line_info entries.
|
||||
func (h *btfExtHeader) lineInfoStart() int64 {
|
||||
return int64(h.HdrLen + h.LineInfoOff)
|
||||
}
|
||||
|
||||
// coreReloStart returns the offset from the beginning of the .BTF.ext section
|
||||
// to the start of its CO-RE relocation entries.
|
||||
func (h *btfExtHeader) coreReloStart(ch *btfExtCOREHeader) int64 {
|
||||
return int64(h.HdrLen + ch.COREReloOff)
|
||||
}
|
||||
|
||||
// btfExtCOREHeader is found right after the btfExtHeader when its HdrLen
|
||||
// field is larger than its size.
|
||||
type btfExtCOREHeader struct {
|
||||
COREReloOff uint32
|
||||
COREReloLen uint32
|
||||
}
|
||||
|
||||
// parseBTFExtCOREHeader parses the tail of the .BTF.ext header. If additional
|
||||
// header bytes are present, extHeader.HdrLen will be larger than the struct,
|
||||
// indicating the presence of a CO-RE extension header.
|
||||
func parseBTFExtCOREHeader(r io.Reader, bo binary.ByteOrder, extHeader *btfExtHeader) (*btfExtCOREHeader, error) {
|
||||
extHdrSize := int64(binary.Size(&extHeader))
|
||||
remainder := int64(extHeader.HdrLen) - extHdrSize
|
||||
|
||||
if remainder == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var coreHeader btfExtCOREHeader
|
||||
if err := binary.Read(r, bo, &coreHeader); err != nil {
|
||||
return nil, fmt.Errorf("can't read header: %v", err)
|
||||
}
|
||||
|
||||
return &coreHeader, nil
|
||||
}
|
||||
|
||||
type btfExtInfoSec struct {
|
||||
SecNameOff uint32
|
||||
NumInfo uint32
|
||||
}
|
||||
|
||||
// parseExtInfoSec parses a btf_ext_info_sec header within .BTF.ext,
|
||||
// appearing within func_info and line_info sub-sections.
|
||||
// These headers appear once for each program section in the ELF and are
|
||||
// followed by one or more func/line_info records for the section.
|
||||
func parseExtInfoSec(r io.Reader, bo binary.ByteOrder, strings *stringTable) (string, *btfExtInfoSec, error) {
|
||||
var infoHeader btfExtInfoSec
|
||||
if err := binary.Read(r, bo, &infoHeader); err != nil {
|
||||
return "", nil, fmt.Errorf("read ext info header: %w", err)
|
||||
}
|
||||
|
||||
secName, err := strings.Lookup(infoHeader.SecNameOff)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("get section name: %w", err)
|
||||
}
|
||||
if secName == "" {
|
||||
return "", nil, fmt.Errorf("extinfo header refers to empty section name")
|
||||
}
|
||||
|
||||
if infoHeader.NumInfo == 0 {
|
||||
return "", nil, fmt.Errorf("section %s has zero records", secName)
|
||||
}
|
||||
|
||||
return secName, &infoHeader, nil
|
||||
}
|
||||
|
||||
// parseExtInfoRecordSize parses the uint32 at the beginning of a func_infos
|
||||
// or line_infos segment that describes the length of all extInfoRecords in
|
||||
// that segment.
|
||||
func parseExtInfoRecordSize(r io.Reader, bo binary.ByteOrder) (uint32, error) {
|
||||
const maxRecordSize = 256
|
||||
|
||||
var recordSize uint32
|
||||
if err := binary.Read(r, bo, &recordSize); err != nil {
|
||||
return 0, fmt.Errorf("can't read record size: %v", err)
|
||||
}
|
||||
|
||||
if recordSize < 4 {
|
||||
// Need at least InsnOff worth of bytes per record.
|
||||
return 0, errors.New("record size too short")
|
||||
}
|
||||
if recordSize > maxRecordSize {
|
||||
return 0, fmt.Errorf("record size %v exceeds %v", recordSize, maxRecordSize)
|
||||
}
|
||||
|
||||
return recordSize, nil
|
||||
}
|
||||
|
||||
// The size of a FuncInfo in BTF wire format.
|
||||
var FuncInfoSize = uint32(binary.Size(bpfFuncInfo{}))
|
||||
|
||||
type funcInfo struct {
|
||||
fn *Func
|
||||
offset asm.RawInstructionOffset
|
||||
}
|
||||
|
||||
type bpfFuncInfo struct {
|
||||
// Instruction offset of the function within an ELF section.
|
||||
InsnOff uint32
|
||||
TypeID TypeID
|
||||
}
|
||||
|
||||
func newFuncInfo(fi bpfFuncInfo, spec *Spec) (*funcInfo, error) {
|
||||
typ, err := spec.TypeByID(fi.TypeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fn, ok := typ.(*Func)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type ID %d is a %T, but expected a Func", fi.TypeID, typ)
|
||||
}
|
||||
|
||||
// C doesn't have anonymous functions, but check just in case.
|
||||
if fn.Name == "" {
|
||||
return nil, fmt.Errorf("func with type ID %d doesn't have a name", fi.TypeID)
|
||||
}
|
||||
|
||||
return &funcInfo{
|
||||
fn,
|
||||
asm.RawInstructionOffset(fi.InsnOff),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newFuncInfos(bfis []bpfFuncInfo, spec *Spec) ([]funcInfo, error) {
|
||||
fis := make([]funcInfo, 0, len(bfis))
|
||||
for _, bfi := range bfis {
|
||||
fi, err := newFuncInfo(bfi, spec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("offset %d: %w", bfi.InsnOff, err)
|
||||
}
|
||||
fis = append(fis, *fi)
|
||||
}
|
||||
sort.Slice(fis, func(i, j int) bool {
|
||||
return fis[i].offset <= fis[j].offset
|
||||
})
|
||||
return fis, nil
|
||||
}
|
||||
|
||||
// marshal into the BTF wire format.
|
||||
func (fi *funcInfo) marshal(w *bytes.Buffer, b *Builder) error {
|
||||
id, err := b.Add(fi.fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bfi := bpfFuncInfo{
|
||||
InsnOff: uint32(fi.offset),
|
||||
TypeID: id,
|
||||
}
|
||||
buf := make([]byte, FuncInfoSize)
|
||||
internal.NativeEndian.PutUint32(buf, bfi.InsnOff)
|
||||
internal.NativeEndian.PutUint32(buf[4:], uint32(bfi.TypeID))
|
||||
_, err = w.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
// parseFuncInfos parses a func_info sub-section within .BTF.ext ito a map of
|
||||
// func infos indexed by section name.
|
||||
func parseFuncInfos(r io.Reader, bo binary.ByteOrder, strings *stringTable) (map[string][]bpfFuncInfo, error) {
|
||||
recordSize, err := parseExtInfoRecordSize(r, bo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string][]bpfFuncInfo)
|
||||
for {
|
||||
secName, infoHeader, err := parseExtInfoSec(r, bo, strings)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return result, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records, err := parseFuncInfoRecords(r, bo, recordSize, infoHeader.NumInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("section %v: %w", secName, err)
|
||||
}
|
||||
|
||||
result[secName] = records
|
||||
}
|
||||
}
|
||||
|
||||
// parseFuncInfoRecords parses a stream of func_infos into a funcInfos.
|
||||
// These records appear after a btf_ext_info_sec header in the func_info
|
||||
// sub-section of .BTF.ext.
|
||||
func parseFuncInfoRecords(r io.Reader, bo binary.ByteOrder, recordSize uint32, recordNum uint32) ([]bpfFuncInfo, error) {
|
||||
var out []bpfFuncInfo
|
||||
var fi bpfFuncInfo
|
||||
|
||||
if exp, got := FuncInfoSize, recordSize; exp != got {
|
||||
// BTF blob's record size is longer than we know how to parse.
|
||||
return nil, fmt.Errorf("expected FuncInfo record size %d, but BTF blob contains %d", exp, got)
|
||||
}
|
||||
|
||||
for i := uint32(0); i < recordNum; i++ {
|
||||
if err := binary.Read(r, bo, &fi); err != nil {
|
||||
return nil, fmt.Errorf("can't read function info: %v", err)
|
||||
}
|
||||
|
||||
if fi.InsnOff%asm.InstructionSize != 0 {
|
||||
return nil, fmt.Errorf("offset %v is not aligned with instruction size", fi.InsnOff)
|
||||
}
|
||||
|
||||
// ELF tracks offset in bytes, the kernel expects raw BPF instructions.
|
||||
// Convert as early as possible.
|
||||
fi.InsnOff /= asm.InstructionSize
|
||||
|
||||
out = append(out, fi)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
var LineInfoSize = uint32(binary.Size(bpfLineInfo{}))
|
||||
|
||||
// Line represents the location and contents of a single line of source
|
||||
// code a BPF ELF was compiled from.
|
||||
type Line struct {
|
||||
fileName string
|
||||
line string
|
||||
lineNumber uint32
|
||||
lineColumn uint32
|
||||
}
|
||||
|
||||
func (li *Line) FileName() string {
|
||||
return li.fileName
|
||||
}
|
||||
|
||||
func (li *Line) Line() string {
|
||||
return li.line
|
||||
}
|
||||
|
||||
func (li *Line) LineNumber() uint32 {
|
||||
return li.lineNumber
|
||||
}
|
||||
|
||||
func (li *Line) LineColumn() uint32 {
|
||||
return li.lineColumn
|
||||
}
|
||||
|
||||
func (li *Line) String() string {
|
||||
return li.line
|
||||
}
|
||||
|
||||
type lineInfo struct {
|
||||
line *Line
|
||||
offset asm.RawInstructionOffset
|
||||
}
|
||||
|
||||
// Constants for the format of bpfLineInfo.LineCol.
|
||||
const (
|
||||
bpfLineShift = 10
|
||||
bpfLineMax = (1 << (32 - bpfLineShift)) - 1
|
||||
bpfColumnMax = (1 << bpfLineShift) - 1
|
||||
)
|
||||
|
||||
type bpfLineInfo struct {
|
||||
// Instruction offset of the line within the whole instruction stream, in instructions.
|
||||
InsnOff uint32
|
||||
FileNameOff uint32
|
||||
LineOff uint32
|
||||
LineCol uint32
|
||||
}
|
||||
|
||||
func newLineInfo(li bpfLineInfo, strings *stringTable) (*lineInfo, error) {
|
||||
line, err := strings.Lookup(li.LineOff)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lookup of line: %w", err)
|
||||
}
|
||||
|
||||
fileName, err := strings.Lookup(li.FileNameOff)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lookup of filename: %w", err)
|
||||
}
|
||||
|
||||
lineNumber := li.LineCol >> bpfLineShift
|
||||
lineColumn := li.LineCol & bpfColumnMax
|
||||
|
||||
return &lineInfo{
|
||||
&Line{
|
||||
fileName,
|
||||
line,
|
||||
lineNumber,
|
||||
lineColumn,
|
||||
},
|
||||
asm.RawInstructionOffset(li.InsnOff),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newLineInfos(blis []bpfLineInfo, strings *stringTable) ([]lineInfo, error) {
|
||||
lis := make([]lineInfo, 0, len(blis))
|
||||
for _, bli := range blis {
|
||||
li, err := newLineInfo(bli, strings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("offset %d: %w", bli.InsnOff, err)
|
||||
}
|
||||
lis = append(lis, *li)
|
||||
}
|
||||
sort.Slice(lis, func(i, j int) bool {
|
||||
return lis[i].offset <= lis[j].offset
|
||||
})
|
||||
return lis, nil
|
||||
}
|
||||
|
||||
// marshal writes the binary representation of the LineInfo to w.
|
||||
func (li *lineInfo) marshal(w *bytes.Buffer, b *Builder) error {
|
||||
line := li.line
|
||||
if line.lineNumber > bpfLineMax {
|
||||
return fmt.Errorf("line %d exceeds %d", line.lineNumber, bpfLineMax)
|
||||
}
|
||||
|
||||
if line.lineColumn > bpfColumnMax {
|
||||
return fmt.Errorf("column %d exceeds %d", line.lineColumn, bpfColumnMax)
|
||||
}
|
||||
|
||||
fileNameOff, err := b.addString(line.fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file name %q: %w", line.fileName, err)
|
||||
}
|
||||
|
||||
lineOff, err := b.addString(line.line)
|
||||
if err != nil {
|
||||
return fmt.Errorf("line %q: %w", line.line, err)
|
||||
}
|
||||
|
||||
bli := bpfLineInfo{
|
||||
uint32(li.offset),
|
||||
fileNameOff,
|
||||
lineOff,
|
||||
(line.lineNumber << bpfLineShift) | line.lineColumn,
|
||||
}
|
||||
|
||||
buf := make([]byte, LineInfoSize)
|
||||
internal.NativeEndian.PutUint32(buf, bli.InsnOff)
|
||||
internal.NativeEndian.PutUint32(buf[4:], bli.FileNameOff)
|
||||
internal.NativeEndian.PutUint32(buf[8:], bli.LineOff)
|
||||
internal.NativeEndian.PutUint32(buf[12:], bli.LineCol)
|
||||
_, err = w.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
// parseLineInfos parses a line_info sub-section within .BTF.ext ito a map of
|
||||
// line infos indexed by section name.
|
||||
func parseLineInfos(r io.Reader, bo binary.ByteOrder, strings *stringTable) (map[string][]bpfLineInfo, error) {
|
||||
recordSize, err := parseExtInfoRecordSize(r, bo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string][]bpfLineInfo)
|
||||
for {
|
||||
secName, infoHeader, err := parseExtInfoSec(r, bo, strings)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return result, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records, err := parseLineInfoRecords(r, bo, recordSize, infoHeader.NumInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("section %v: %w", secName, err)
|
||||
}
|
||||
|
||||
result[secName] = records
|
||||
}
|
||||
}
|
||||
|
||||
// parseLineInfoRecords parses a stream of line_infos into a lineInfos.
|
||||
// These records appear after a btf_ext_info_sec header in the line_info
|
||||
// sub-section of .BTF.ext.
|
||||
func parseLineInfoRecords(r io.Reader, bo binary.ByteOrder, recordSize uint32, recordNum uint32) ([]bpfLineInfo, error) {
|
||||
var out []bpfLineInfo
|
||||
var li bpfLineInfo
|
||||
|
||||
if exp, got := uint32(binary.Size(li)), recordSize; exp != got {
|
||||
// BTF blob's record size is longer than we know how to parse.
|
||||
return nil, fmt.Errorf("expected LineInfo record size %d, but BTF blob contains %d", exp, got)
|
||||
}
|
||||
|
||||
for i := uint32(0); i < recordNum; i++ {
|
||||
if err := binary.Read(r, bo, &li); err != nil {
|
||||
return nil, fmt.Errorf("can't read line info: %v", err)
|
||||
}
|
||||
|
||||
if li.InsnOff%asm.InstructionSize != 0 {
|
||||
return nil, fmt.Errorf("offset %v is not aligned with instruction size", li.InsnOff)
|
||||
}
|
||||
|
||||
// ELF tracks offset in bytes, the kernel expects raw BPF instructions.
|
||||
// Convert as early as possible.
|
||||
li.InsnOff /= asm.InstructionSize
|
||||
|
||||
out = append(out, li)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// bpfCORERelo matches the kernel's struct bpf_core_relo.
|
||||
type bpfCORERelo struct {
|
||||
InsnOff uint32
|
||||
TypeID TypeID
|
||||
AccessStrOff uint32
|
||||
Kind coreKind
|
||||
}
|
||||
|
||||
type CORERelocation struct {
|
||||
// The local type of the relocation, stripped of typedefs and qualifiers.
|
||||
typ Type
|
||||
accessor coreAccessor
|
||||
kind coreKind
|
||||
// The ID of the local type in the source BTF.
|
||||
id TypeID
|
||||
}
|
||||
|
||||
func (cr *CORERelocation) String() string {
|
||||
return fmt.Sprintf("CORERelocation(%s, %s[%s], local_id=%d)", cr.kind, cr.typ, cr.accessor, cr.id)
|
||||
}
|
||||
|
||||
func CORERelocationMetadata(ins *asm.Instruction) *CORERelocation {
|
||||
relo, _ := ins.Metadata.Get(coreRelocationMeta{}).(*CORERelocation)
|
||||
return relo
|
||||
}
|
||||
|
||||
type coreRelocationInfo struct {
|
||||
relo *CORERelocation
|
||||
offset asm.RawInstructionOffset
|
||||
}
|
||||
|
||||
func newRelocationInfo(relo bpfCORERelo, spec *Spec, strings *stringTable) (*coreRelocationInfo, error) {
|
||||
typ, err := spec.TypeByID(relo.TypeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessorStr, err := strings.Lookup(relo.AccessStrOff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessor, err := parseCOREAccessor(accessorStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("accessor %q: %s", accessorStr, err)
|
||||
}
|
||||
|
||||
return &coreRelocationInfo{
|
||||
&CORERelocation{
|
||||
typ,
|
||||
accessor,
|
||||
relo.Kind,
|
||||
relo.TypeID,
|
||||
},
|
||||
asm.RawInstructionOffset(relo.InsnOff),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newRelocationInfos(brs []bpfCORERelo, spec *Spec, strings *stringTable) ([]coreRelocationInfo, error) {
|
||||
rs := make([]coreRelocationInfo, 0, len(brs))
|
||||
for _, br := range brs {
|
||||
relo, err := newRelocationInfo(br, spec, strings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("offset %d: %w", br.InsnOff, err)
|
||||
}
|
||||
rs = append(rs, *relo)
|
||||
}
|
||||
sort.Slice(rs, func(i, j int) bool {
|
||||
return rs[i].offset < rs[j].offset
|
||||
})
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
var extInfoReloSize = binary.Size(bpfCORERelo{})
|
||||
|
||||
// parseCORERelos parses a core_relos sub-section within .BTF.ext ito a map of
|
||||
// CO-RE relocations indexed by section name.
|
||||
func parseCORERelos(r io.Reader, bo binary.ByteOrder, strings *stringTable) (map[string][]bpfCORERelo, error) {
|
||||
recordSize, err := parseExtInfoRecordSize(r, bo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if recordSize != uint32(extInfoReloSize) {
|
||||
return nil, fmt.Errorf("expected record size %d, got %d", extInfoReloSize, recordSize)
|
||||
}
|
||||
|
||||
result := make(map[string][]bpfCORERelo)
|
||||
for {
|
||||
secName, infoHeader, err := parseExtInfoSec(r, bo, strings)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return result, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
records, err := parseCOREReloRecords(r, bo, recordSize, infoHeader.NumInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("section %v: %w", secName, err)
|
||||
}
|
||||
|
||||
result[secName] = records
|
||||
}
|
||||
}
|
||||
|
||||
// parseCOREReloRecords parses a stream of CO-RE relocation entries into a
|
||||
// coreRelos. These records appear after a btf_ext_info_sec header in the
|
||||
// core_relos sub-section of .BTF.ext.
|
||||
func parseCOREReloRecords(r io.Reader, bo binary.ByteOrder, recordSize uint32, recordNum uint32) ([]bpfCORERelo, error) {
|
||||
var out []bpfCORERelo
|
||||
|
||||
var relo bpfCORERelo
|
||||
for i := uint32(0); i < recordNum; i++ {
|
||||
if err := binary.Read(r, bo, &relo); err != nil {
|
||||
return nil, fmt.Errorf("can't read CO-RE relocation: %v", err)
|
||||
}
|
||||
|
||||
if relo.InsnOff%asm.InstructionSize != 0 {
|
||||
return nil, fmt.Errorf("offset %v is not aligned with instruction size", relo.InsnOff)
|
||||
}
|
||||
|
||||
// ELF tracks offset in bytes, the kernel expects raw BPF instructions.
|
||||
// Convert as early as possible.
|
||||
relo.InsnOff /= asm.InstructionSize
|
||||
|
||||
out = append(out, relo)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
)
|
||||
|
||||
func TestParseExtInfoBigRecordSize(t *testing.T) {
|
||||
rd := strings.NewReader("\xff\xff\xff\xff\x00\x00\x00\x000709171295166016")
|
||||
table, err := readStringTable(bytes.NewReader([]byte{0}), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := parseFuncInfos(rd, internal.NativeEndian, table); err == nil {
|
||||
t.Error("Parsing func info with large record size doesn't return an error")
|
||||
}
|
||||
|
||||
if _, err := parseLineInfos(rd, internal.NativeEndian, table); err == nil {
|
||||
t.Error("Parsing line info with large record size doesn't return an error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var errNestedTooDeep = errors.New("nested too deep")
|
||||
|
||||
// GoFormatter converts a Type to Go syntax.
|
||||
//
|
||||
// A zero GoFormatter is valid to use.
|
||||
type GoFormatter struct {
|
||||
w strings.Builder
|
||||
|
||||
// Types present in this map are referred to using the given name if they
|
||||
// are encountered when outputting another type.
|
||||
Names map[Type]string
|
||||
|
||||
// Identifier is called for each field of struct-like types. By default the
|
||||
// field name is used as is.
|
||||
Identifier func(string) string
|
||||
|
||||
// EnumIdentifier is called for each element of an enum. By default the
|
||||
// name of the enum type is concatenated with Identifier(element).
|
||||
EnumIdentifier func(name, element string) string
|
||||
}
|
||||
|
||||
// TypeDeclaration generates a Go type declaration for a BTF type.
|
||||
func (gf *GoFormatter) TypeDeclaration(name string, typ Type) (string, error) {
|
||||
gf.w.Reset()
|
||||
if err := gf.writeTypeDecl(name, typ); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return gf.w.String(), nil
|
||||
}
|
||||
|
||||
func (gf *GoFormatter) identifier(s string) string {
|
||||
if gf.Identifier != nil {
|
||||
return gf.Identifier(s)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (gf *GoFormatter) enumIdentifier(name, element string) string {
|
||||
if gf.EnumIdentifier != nil {
|
||||
return gf.EnumIdentifier(name, element)
|
||||
}
|
||||
|
||||
return name + gf.identifier(element)
|
||||
}
|
||||
|
||||
// writeTypeDecl outputs a declaration of the given type.
|
||||
//
|
||||
// It encodes https://golang.org/ref/spec#Type_declarations:
|
||||
//
|
||||
// type foo struct { bar uint32; }
|
||||
// type bar int32
|
||||
func (gf *GoFormatter) writeTypeDecl(name string, typ Type) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("need a name for type %s", typ)
|
||||
}
|
||||
|
||||
typ = skipQualifiers(typ)
|
||||
fmt.Fprintf(&gf.w, "type %s ", name)
|
||||
if err := gf.writeTypeLit(typ, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e, ok := typ.(*Enum)
|
||||
if !ok || len(e.Values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
gf.w.WriteString("; const ( ")
|
||||
for _, ev := range e.Values {
|
||||
id := gf.enumIdentifier(name, ev.Name)
|
||||
fmt.Fprintf(&gf.w, "%s %s = %d; ", id, name, ev.Value)
|
||||
}
|
||||
gf.w.WriteString(")")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeType outputs the name of a named type or a literal describing the type.
|
||||
//
|
||||
// It encodes https://golang.org/ref/spec#Types.
|
||||
//
|
||||
// foo (if foo is a named type)
|
||||
// uint32
|
||||
func (gf *GoFormatter) writeType(typ Type, depth int) error {
|
||||
typ = skipQualifiers(typ)
|
||||
|
||||
name := gf.Names[typ]
|
||||
if name != "" {
|
||||
gf.w.WriteString(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
return gf.writeTypeLit(typ, depth)
|
||||
}
|
||||
|
||||
// writeTypeLit outputs a literal describing the type.
|
||||
//
|
||||
// The function ignores named types.
|
||||
//
|
||||
// It encodes https://golang.org/ref/spec#TypeLit.
|
||||
//
|
||||
// struct { bar uint32; }
|
||||
// uint32
|
||||
func (gf *GoFormatter) writeTypeLit(typ Type, depth int) error {
|
||||
depth++
|
||||
if depth > maxTypeDepth {
|
||||
return errNestedTooDeep
|
||||
}
|
||||
|
||||
var err error
|
||||
switch v := skipQualifiers(typ).(type) {
|
||||
case *Int:
|
||||
err = gf.writeIntLit(v)
|
||||
|
||||
case *Enum:
|
||||
if !v.Signed {
|
||||
gf.w.WriteRune('u')
|
||||
}
|
||||
switch v.Size {
|
||||
case 1:
|
||||
gf.w.WriteString("int8")
|
||||
case 2:
|
||||
gf.w.WriteString("int16")
|
||||
case 4:
|
||||
gf.w.WriteString("int32")
|
||||
case 8:
|
||||
gf.w.WriteString("int64")
|
||||
default:
|
||||
err = fmt.Errorf("invalid enum size %d", v.Size)
|
||||
}
|
||||
|
||||
case *Typedef:
|
||||
err = gf.writeType(v.Type, depth)
|
||||
|
||||
case *Array:
|
||||
fmt.Fprintf(&gf.w, "[%d]", v.Nelems)
|
||||
err = gf.writeType(v.Type, depth)
|
||||
|
||||
case *Struct:
|
||||
err = gf.writeStructLit(v.Size, v.Members, depth)
|
||||
|
||||
case *Union:
|
||||
// Always choose the first member to represent the union in Go.
|
||||
err = gf.writeStructLit(v.Size, v.Members[:1], depth)
|
||||
|
||||
case *Datasec:
|
||||
err = gf.writeDatasecLit(v, depth)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("type %T: %w", v, ErrNotSupported)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", typ, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gf *GoFormatter) writeIntLit(i *Int) error {
|
||||
bits := i.Size * 8
|
||||
switch i.Encoding {
|
||||
case Bool:
|
||||
if i.Size != 1 {
|
||||
return fmt.Errorf("bool with size %d", i.Size)
|
||||
}
|
||||
gf.w.WriteString("bool")
|
||||
case Char:
|
||||
if i.Size != 1 {
|
||||
return fmt.Errorf("char with size %d", i.Size)
|
||||
}
|
||||
// BTF doesn't have a way to specify the signedness of a char. Assume
|
||||
// we are dealing with unsigned, since this works nicely with []byte
|
||||
// in Go code.
|
||||
fallthrough
|
||||
case Unsigned, Signed:
|
||||
stem := "uint"
|
||||
if i.Encoding == Signed {
|
||||
stem = "int"
|
||||
}
|
||||
if i.Size > 8 {
|
||||
fmt.Fprintf(&gf.w, "[%d]byte /* %s%d */", i.Size, stem, i.Size*8)
|
||||
} else {
|
||||
fmt.Fprintf(&gf.w, "%s%d", stem, bits)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("can't encode %s", i.Encoding)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gf *GoFormatter) writeStructLit(size uint32, members []Member, depth int) error {
|
||||
gf.w.WriteString("struct { ")
|
||||
|
||||
prevOffset := uint32(0)
|
||||
skippedBitfield := false
|
||||
for i, m := range members {
|
||||
if m.BitfieldSize > 0 {
|
||||
skippedBitfield = true
|
||||
continue
|
||||
}
|
||||
|
||||
offset := m.Offset.Bytes()
|
||||
if n := offset - prevOffset; skippedBitfield && n > 0 {
|
||||
fmt.Fprintf(&gf.w, "_ [%d]byte /* unsupported bitfield */; ", n)
|
||||
} else {
|
||||
gf.writePadding(n)
|
||||
}
|
||||
|
||||
fieldSize, err := Sizeof(m.Type)
|
||||
if err != nil {
|
||||
return fmt.Errorf("field %d: %w", i, err)
|
||||
}
|
||||
|
||||
prevOffset = offset + uint32(fieldSize)
|
||||
if prevOffset > size {
|
||||
return fmt.Errorf("field %d of size %d exceeds type size %d", i, fieldSize, size)
|
||||
}
|
||||
|
||||
if err := gf.writeStructField(m, depth); err != nil {
|
||||
return fmt.Errorf("field %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
gf.writePadding(size - prevOffset)
|
||||
gf.w.WriteString("}")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gf *GoFormatter) writeStructField(m Member, depth int) error {
|
||||
if m.BitfieldSize > 0 {
|
||||
return fmt.Errorf("bitfields are not supported")
|
||||
}
|
||||
if m.Offset%8 != 0 {
|
||||
return fmt.Errorf("unsupported offset %d", m.Offset)
|
||||
}
|
||||
|
||||
if m.Name == "" {
|
||||
// Special case a nested anonymous union like
|
||||
// struct foo { union { int bar; int baz }; }
|
||||
// by replacing the whole union with its first member.
|
||||
union, ok := m.Type.(*Union)
|
||||
if !ok {
|
||||
return fmt.Errorf("anonymous fields are not supported")
|
||||
|
||||
}
|
||||
|
||||
if len(union.Members) == 0 {
|
||||
return errors.New("empty anonymous union")
|
||||
}
|
||||
|
||||
depth++
|
||||
if depth > maxTypeDepth {
|
||||
return errNestedTooDeep
|
||||
}
|
||||
|
||||
m := union.Members[0]
|
||||
size, err := Sizeof(m.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gf.writeStructField(m, depth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gf.writePadding(union.Size - uint32(size))
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
fmt.Fprintf(&gf.w, "%s ", gf.identifier(m.Name))
|
||||
|
||||
if err := gf.writeType(m.Type, depth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gf.w.WriteString("; ")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gf *GoFormatter) writeDatasecLit(ds *Datasec, depth int) error {
|
||||
gf.w.WriteString("struct { ")
|
||||
|
||||
prevOffset := uint32(0)
|
||||
for i, vsi := range ds.Vars {
|
||||
v, ok := vsi.Type.(*Var)
|
||||
if !ok {
|
||||
return fmt.Errorf("can't format %s as part of data section", vsi.Type)
|
||||
}
|
||||
|
||||
if v.Linkage != GlobalVar {
|
||||
// Ignore static, extern, etc. for now.
|
||||
continue
|
||||
}
|
||||
|
||||
if v.Name == "" {
|
||||
return fmt.Errorf("variable %d: empty name", i)
|
||||
}
|
||||
|
||||
gf.writePadding(vsi.Offset - prevOffset)
|
||||
prevOffset = vsi.Offset + vsi.Size
|
||||
|
||||
fmt.Fprintf(&gf.w, "%s ", gf.identifier(v.Name))
|
||||
|
||||
if err := gf.writeType(v.Type, depth); err != nil {
|
||||
return fmt.Errorf("variable %d: %w", i, err)
|
||||
}
|
||||
|
||||
gf.w.WriteString("; ")
|
||||
}
|
||||
|
||||
gf.writePadding(ds.Size - prevOffset)
|
||||
gf.w.WriteString("}")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gf *GoFormatter) writePadding(bytes uint32) {
|
||||
if bytes > 0 {
|
||||
fmt.Fprintf(&gf.w, "_ [%d]byte; ", bytes)
|
||||
}
|
||||
}
|
||||
|
||||
func skipQualifiers(typ Type) Type {
|
||||
result := typ
|
||||
for depth := 0; depth <= maxTypeDepth; depth++ {
|
||||
switch v := (result).(type) {
|
||||
case qualifier:
|
||||
result = v.qualify()
|
||||
default:
|
||||
return result
|
||||
}
|
||||
}
|
||||
return &cycle{typ}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGoTypeDeclaration(t *testing.T) {
|
||||
tests := []struct {
|
||||
typ Type
|
||||
output string
|
||||
}{
|
||||
{&Int{Size: 1}, "type t uint8"},
|
||||
{&Int{Size: 1, Encoding: Bool}, "type t bool"},
|
||||
{&Int{Size: 1, Encoding: Char}, "type t uint8"},
|
||||
{&Int{Size: 2, Encoding: Signed}, "type t int16"},
|
||||
{&Int{Size: 4, Encoding: Signed}, "type t int32"},
|
||||
{&Int{Size: 8}, "type t uint64"},
|
||||
{&Typedef{Name: "frob", Type: &Int{Size: 8}}, "type t uint64"},
|
||||
{&Int{Size: 16}, "type t [16]byte /* uint128 */"},
|
||||
{&Enum{Values: []EnumValue{{"FOO", 32}}, Size: 4}, "type t uint32; const ( tFOO t = 32; )"},
|
||||
{&Enum{Values: []EnumValue{{"BAR", 1}}, Size: 1, Signed: true}, "type t int8; const ( tBAR t = 1; )"},
|
||||
{
|
||||
&Struct{
|
||||
Name: "enum literals",
|
||||
Size: 2,
|
||||
Members: []Member{
|
||||
{Name: "enum", Type: &Enum{Values: []EnumValue{{"BAR", 1}}, Size: 2}, Offset: 0},
|
||||
},
|
||||
},
|
||||
"type t struct { enum uint16; }",
|
||||
},
|
||||
{&Array{Nelems: 2, Type: &Int{Size: 1}}, "type t [2]uint8"},
|
||||
{
|
||||
&Union{
|
||||
Size: 8,
|
||||
Members: []Member{
|
||||
{Name: "a", Type: &Int{Size: 4}},
|
||||
{Name: "b", Type: &Int{Size: 8}},
|
||||
},
|
||||
},
|
||||
"type t struct { a uint32; _ [4]byte; }",
|
||||
},
|
||||
{
|
||||
&Struct{
|
||||
Name: "field padding",
|
||||
Size: 16,
|
||||
Members: []Member{
|
||||
{Name: "frob", Type: &Int{Size: 4}, Offset: 0},
|
||||
{Name: "foo", Type: &Int{Size: 8}, Offset: 8 * 8},
|
||||
},
|
||||
},
|
||||
"type t struct { frob uint32; _ [4]byte; foo uint64; }",
|
||||
},
|
||||
{
|
||||
&Struct{
|
||||
Name: "end padding",
|
||||
Size: 16,
|
||||
Members: []Member{
|
||||
{Name: "foo", Type: &Int{Size: 8}, Offset: 0},
|
||||
{Name: "frob", Type: &Int{Size: 4}, Offset: 8 * 8},
|
||||
},
|
||||
},
|
||||
"type t struct { foo uint64; frob uint32; _ [4]byte; }",
|
||||
},
|
||||
{
|
||||
&Struct{
|
||||
Name: "bitfield",
|
||||
Size: 8,
|
||||
Members: []Member{
|
||||
{Name: "foo", Type: &Int{Size: 4}, Offset: 0, BitfieldSize: 1},
|
||||
{Name: "frob", Type: &Int{Size: 4}, Offset: 4 * 8},
|
||||
},
|
||||
},
|
||||
"type t struct { _ [4]byte /* unsupported bitfield */; frob uint32; }",
|
||||
},
|
||||
{
|
||||
&Struct{
|
||||
Name: "nested",
|
||||
Size: 8,
|
||||
Members: []Member{
|
||||
{
|
||||
Name: "foo",
|
||||
Type: &Struct{
|
||||
Size: 4,
|
||||
Members: []Member{
|
||||
{Name: "bar", Type: &Int{Size: 4}, Offset: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
{Name: "frob", Type: &Int{Size: 4}, Offset: 4 * 8},
|
||||
},
|
||||
},
|
||||
"type t struct { foo struct { bar uint32; }; frob uint32; }",
|
||||
},
|
||||
{
|
||||
&Struct{
|
||||
Name: "nested anon union",
|
||||
Size: 8,
|
||||
Members: []Member{
|
||||
{
|
||||
Name: "",
|
||||
Type: &Union{
|
||||
Size: 4,
|
||||
Members: []Member{
|
||||
{Name: "foo", Type: &Int{Size: 4}, Offset: 0},
|
||||
{Name: "bar", Type: &Int{Size: 4}, Offset: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"type t struct { foo uint32; _ [4]byte; }",
|
||||
},
|
||||
{
|
||||
&Datasec{
|
||||
Size: 16,
|
||||
Vars: []VarSecinfo{
|
||||
{&Var{Name: "s", Type: &Int{Size: 2}, Linkage: StaticVar}, 0, 2},
|
||||
{&Var{Name: "g", Type: &Int{Size: 4}, Linkage: GlobalVar}, 4, 4},
|
||||
{&Var{Name: "e", Type: &Int{Size: 8}, Linkage: ExternVar}, 8, 8},
|
||||
},
|
||||
},
|
||||
"type t struct { _ [4]byte; g uint32; _ [8]byte; }",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprint(test.typ), func(t *testing.T) {
|
||||
have := mustGoTypeDeclaration(t, test.typ, nil, nil)
|
||||
if have != test.output {
|
||||
t.Errorf("Unexpected output:\n\t-%s\n\t+%s", test.output, have)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoTypeDeclarationNamed(t *testing.T) {
|
||||
e1 := &Enum{Name: "e1", Size: 4}
|
||||
s1 := &Struct{
|
||||
Name: "s1",
|
||||
Size: 4,
|
||||
Members: []Member{
|
||||
{Name: "frob", Type: e1},
|
||||
},
|
||||
}
|
||||
s2 := &Struct{
|
||||
Name: "s2",
|
||||
Size: 4,
|
||||
Members: []Member{
|
||||
{Name: "frood", Type: s1},
|
||||
},
|
||||
}
|
||||
td := &Typedef{Name: "td", Type: e1}
|
||||
arr := &Array{Nelems: 1, Type: td}
|
||||
|
||||
tests := []struct {
|
||||
typ Type
|
||||
named []Type
|
||||
output string
|
||||
}{
|
||||
{e1, []Type{e1}, "type t uint32"},
|
||||
{s1, []Type{e1, s1}, "type t struct { frob E1; }"},
|
||||
{s2, []Type{e1}, "type t struct { frood struct { frob E1; }; }"},
|
||||
{s2, []Type{e1, s1}, "type t struct { frood S1; }"},
|
||||
{td, nil, "type t uint32"},
|
||||
{td, []Type{td}, "type t uint32"},
|
||||
{arr, []Type{td}, "type t [1]TD"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprint(test.typ), func(t *testing.T) {
|
||||
names := make(map[Type]string)
|
||||
for _, t := range test.named {
|
||||
names[t] = strings.ToUpper(t.TypeName())
|
||||
}
|
||||
|
||||
have := mustGoTypeDeclaration(t, test.typ, names, nil)
|
||||
if have != test.output {
|
||||
t.Errorf("Unexpected output:\n\t-%s\n\t+%s", test.output, have)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoTypeDeclarationQualifiers(t *testing.T) {
|
||||
i := &Int{Size: 4}
|
||||
want := mustGoTypeDeclaration(t, i, nil, nil)
|
||||
|
||||
tests := []struct {
|
||||
typ Type
|
||||
}{
|
||||
{&Volatile{Type: i}},
|
||||
{&Const{Type: i}},
|
||||
{&Restrict{Type: i}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprint(test.typ), func(t *testing.T) {
|
||||
have := mustGoTypeDeclaration(t, test.typ, nil, nil)
|
||||
if have != want {
|
||||
t.Errorf("Unexpected output:\n\t-%s\n\t+%s", want, have)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoTypeDeclarationCycle(t *testing.T) {
|
||||
s := &Struct{Name: "cycle"}
|
||||
s.Members = []Member{{Name: "f", Type: s}}
|
||||
|
||||
var gf GoFormatter
|
||||
_, err := gf.TypeDeclaration("t", s)
|
||||
if !errors.Is(err, errNestedTooDeep) {
|
||||
t.Fatal("Expected errNestedTooDeep, got", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectBogusTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
typ Type
|
||||
}{
|
||||
{&Struct{
|
||||
Size: 1,
|
||||
Members: []Member{
|
||||
{Name: "foo", Type: &Int{Size: 2}, Offset: 0},
|
||||
},
|
||||
}},
|
||||
{&Int{Size: 2, Encoding: Bool}},
|
||||
{&Int{Size: 1, Encoding: Char | Signed}},
|
||||
{&Int{Size: 2, Encoding: Char}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprint(test.typ), func(t *testing.T) {
|
||||
var gf GoFormatter
|
||||
|
||||
_, err := gf.TypeDeclaration("t", test.typ)
|
||||
if err == nil {
|
||||
t.Fatal("TypeDeclaration does not reject bogus type")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustGoTypeDeclaration(tb testing.TB, typ Type, names map[Type]string, id func(string) string) string {
|
||||
tb.Helper()
|
||||
|
||||
gf := GoFormatter{
|
||||
Names: names,
|
||||
Identifier: id,
|
||||
}
|
||||
|
||||
have, err := gf.TypeDeclaration("t", typ)
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = format.Source([]byte(have))
|
||||
if err != nil {
|
||||
tb.Fatalf("Output can't be formatted: %s\n%s", err, have)
|
||||
}
|
||||
|
||||
return have
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
)
|
||||
|
||||
func FuzzSpec(f *testing.F) {
|
||||
var buf bytes.Buffer
|
||||
err := binary.Write(&buf, internal.NativeEndian, &btfHeader{
|
||||
Magic: btfMagic,
|
||||
Version: 1,
|
||||
HdrLen: uint32(binary.Size(btfHeader{})),
|
||||
})
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
f.Add(buf.Bytes())
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
if len(data) < binary.Size(btfHeader{}) {
|
||||
t.Skip("data is too short")
|
||||
}
|
||||
|
||||
spec, err := loadRawSpec(bytes.NewReader(data), internal.NativeEndian, nil)
|
||||
if err != nil {
|
||||
if spec != nil {
|
||||
t.Fatal("spec is not nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if spec == nil {
|
||||
t.Fatal("spec is nil")
|
||||
}
|
||||
|
||||
for _, typ := range spec.types {
|
||||
fmt.Fprintf(io.Discard, "%+10v", typ)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzExtInfo(f *testing.F) {
|
||||
var buf bytes.Buffer
|
||||
err := binary.Write(&buf, internal.NativeEndian, &btfExtHeader{
|
||||
Magic: btfMagic,
|
||||
Version: 1,
|
||||
HdrLen: uint32(binary.Size(btfExtHeader{})),
|
||||
})
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
f.Add(buf.Bytes(), []byte("\x00foo\x00barfoo\x00"))
|
||||
|
||||
emptySpec := newSpec()
|
||||
|
||||
f.Fuzz(func(t *testing.T, data, strings []byte) {
|
||||
if len(data) < binary.Size(btfExtHeader{}) {
|
||||
t.Skip("data is too short")
|
||||
}
|
||||
|
||||
table, err := readStringTable(bytes.NewReader(strings), nil)
|
||||
if err != nil {
|
||||
t.Skip("invalid string table")
|
||||
}
|
||||
|
||||
info, err := loadExtInfos(bytes.NewReader(data), internal.NativeEndian, emptySpec, table)
|
||||
if err != nil {
|
||||
if info != nil {
|
||||
t.Fatal("info is not nil")
|
||||
}
|
||||
} else if info == nil {
|
||||
t.Fatal("info is nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
"github.com/cilium/ebpf/internal/sys"
|
||||
"github.com/cilium/ebpf/internal/unix"
|
||||
)
|
||||
|
||||
// Handle is a reference to BTF loaded into the kernel.
|
||||
type Handle struct {
|
||||
fd *sys.FD
|
||||
|
||||
// Size of the raw BTF in bytes.
|
||||
size uint32
|
||||
|
||||
needsKernelBase bool
|
||||
}
|
||||
|
||||
// NewHandle loads the contents of a [Builder] into the kernel.
|
||||
//
|
||||
// Returns an error wrapping ErrNotSupported if the kernel doesn't support BTF.
|
||||
func NewHandle(b *Builder) (*Handle, error) {
|
||||
small := getByteSlice()
|
||||
defer putByteSlice(small)
|
||||
|
||||
buf, err := b.Marshal(*small, KernelMarshalOptions())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal BTF: %w", err)
|
||||
}
|
||||
|
||||
return NewHandleFromRawBTF(buf)
|
||||
}
|
||||
|
||||
// NewHandleFromRawBTF loads raw BTF into the kernel.
|
||||
//
|
||||
// Returns an error wrapping ErrNotSupported if the kernel doesn't support BTF.
|
||||
func NewHandleFromRawBTF(btf []byte) (*Handle, error) {
|
||||
if uint64(len(btf)) > math.MaxUint32 {
|
||||
return nil, errors.New("BTF exceeds the maximum size")
|
||||
}
|
||||
|
||||
attr := &sys.BtfLoadAttr{
|
||||
Btf: sys.NewSlicePointer(btf),
|
||||
BtfSize: uint32(len(btf)),
|
||||
}
|
||||
|
||||
fd, err := sys.BtfLoad(attr)
|
||||
if err == nil {
|
||||
return &Handle{fd, attr.BtfSize, false}, nil
|
||||
}
|
||||
|
||||
if err := haveBTF(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logBuf := make([]byte, 64*1024)
|
||||
attr.BtfLogBuf = sys.NewSlicePointer(logBuf)
|
||||
attr.BtfLogSize = uint32(len(logBuf))
|
||||
attr.BtfLogLevel = 1
|
||||
|
||||
// Up until at least kernel 6.0, the BTF verifier does not return ENOSPC
|
||||
// if there are other verification errors. ENOSPC is only returned when
|
||||
// the BTF blob is correct, a log was requested, and the provided buffer
|
||||
// is too small.
|
||||
_, ve := sys.BtfLoad(attr)
|
||||
return nil, internal.ErrorWithLog("load btf", err, logBuf, errors.Is(ve, unix.ENOSPC))
|
||||
}
|
||||
|
||||
// NewHandleFromID returns the BTF handle for a given id.
|
||||
//
|
||||
// Prefer calling [ebpf.Program.Handle] or [ebpf.Map.Handle] if possible.
|
||||
//
|
||||
// Returns ErrNotExist, if there is no BTF with the given id.
|
||||
//
|
||||
// Requires CAP_SYS_ADMIN.
|
||||
func NewHandleFromID(id ID) (*Handle, error) {
|
||||
fd, err := sys.BtfGetFdById(&sys.BtfGetFdByIdAttr{
|
||||
Id: uint32(id),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get FD for ID %d: %w", id, err)
|
||||
}
|
||||
|
||||
info, err := newHandleInfoFromFD(fd)
|
||||
if err != nil {
|
||||
_ = fd.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Handle{fd, info.size, info.IsModule()}, nil
|
||||
}
|
||||
|
||||
// Spec parses the kernel BTF into Go types.
|
||||
//
|
||||
// base must contain type information for vmlinux if the handle is for
|
||||
// a kernel module. It may be nil otherwise.
|
||||
func (h *Handle) Spec(base *Spec) (*Spec, error) {
|
||||
var btfInfo sys.BtfInfo
|
||||
btfBuffer := make([]byte, h.size)
|
||||
btfInfo.Btf, btfInfo.BtfSize = sys.NewSlicePointerLen(btfBuffer)
|
||||
|
||||
if err := sys.ObjInfo(h.fd, &btfInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h.needsKernelBase && base == nil {
|
||||
return nil, fmt.Errorf("missing base types")
|
||||
}
|
||||
|
||||
return loadRawSpec(bytes.NewReader(btfBuffer), internal.NativeEndian, base)
|
||||
}
|
||||
|
||||
// Close destroys the handle.
|
||||
//
|
||||
// Subsequent calls to FD will return an invalid value.
|
||||
func (h *Handle) Close() error {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return h.fd.Close()
|
||||
}
|
||||
|
||||
// FD returns the file descriptor for the handle.
|
||||
func (h *Handle) FD() int {
|
||||
return h.fd.Int()
|
||||
}
|
||||
|
||||
// Info returns metadata about the handle.
|
||||
func (h *Handle) Info() (*HandleInfo, error) {
|
||||
return newHandleInfoFromFD(h.fd)
|
||||
}
|
||||
|
||||
// HandleInfo describes a Handle.
|
||||
type HandleInfo struct {
|
||||
// ID of this handle in the kernel. The ID is only valid as long as the
|
||||
// associated handle is kept alive.
|
||||
ID ID
|
||||
|
||||
// Name is an identifying name for the BTF, currently only used by the
|
||||
// kernel.
|
||||
Name string
|
||||
|
||||
// IsKernel is true if the BTF originated with the kernel and not
|
||||
// userspace.
|
||||
IsKernel bool
|
||||
|
||||
// Size of the raw BTF in bytes.
|
||||
size uint32
|
||||
}
|
||||
|
||||
func newHandleInfoFromFD(fd *sys.FD) (*HandleInfo, error) {
|
||||
// We invoke the syscall once with a empty BTF and name buffers to get size
|
||||
// information to allocate buffers. Then we invoke it a second time with
|
||||
// buffers to receive the data.
|
||||
var btfInfo sys.BtfInfo
|
||||
if err := sys.ObjInfo(fd, &btfInfo); err != nil {
|
||||
return nil, fmt.Errorf("get BTF info for fd %s: %w", fd, err)
|
||||
}
|
||||
|
||||
if btfInfo.NameLen > 0 {
|
||||
// NameLen doesn't account for the terminating NUL.
|
||||
btfInfo.NameLen++
|
||||
}
|
||||
|
||||
// Don't pull raw BTF by default, since it may be quite large.
|
||||
btfSize := btfInfo.BtfSize
|
||||
btfInfo.BtfSize = 0
|
||||
|
||||
nameBuffer := make([]byte, btfInfo.NameLen)
|
||||
btfInfo.Name, btfInfo.NameLen = sys.NewSlicePointerLen(nameBuffer)
|
||||
if err := sys.ObjInfo(fd, &btfInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &HandleInfo{
|
||||
ID: ID(btfInfo.Id),
|
||||
Name: unix.ByteSliceToString(nameBuffer),
|
||||
IsKernel: btfInfo.KernelBtf != 0,
|
||||
size: btfSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsVmlinux returns true if the BTF is for the kernel itself.
|
||||
func (i *HandleInfo) IsVmlinux() bool {
|
||||
return i.IsKernel && i.Name == "vmlinux"
|
||||
}
|
||||
|
||||
// IsModule returns true if the BTF is for a kernel module.
|
||||
func (i *HandleInfo) IsModule() bool {
|
||||
return i.IsKernel && i.Name != "vmlinux"
|
||||
}
|
||||
|
||||
// HandleIterator allows enumerating BTF blobs loaded into the kernel.
|
||||
type HandleIterator struct {
|
||||
// The ID of the current handle. Only valid after a call to Next.
|
||||
ID ID
|
||||
// The current Handle. Only valid until a call to Next.
|
||||
// See Take if you want to retain the handle.
|
||||
Handle *Handle
|
||||
err error
|
||||
}
|
||||
|
||||
// Next retrieves a handle for the next BTF object.
|
||||
//
|
||||
// Returns true if another BTF object was found. Call [HandleIterator.Err] after
|
||||
// the function returns false.
|
||||
func (it *HandleIterator) Next() bool {
|
||||
id := it.ID
|
||||
for {
|
||||
attr := &sys.BtfGetNextIdAttr{Id: id}
|
||||
err := sys.BtfGetNextId(attr)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// There are no more BTF objects.
|
||||
break
|
||||
} else if err != nil {
|
||||
it.err = fmt.Errorf("get next BTF ID: %w", err)
|
||||
break
|
||||
}
|
||||
|
||||
id = attr.NextId
|
||||
handle, err := NewHandleFromID(id)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Try again with the next ID.
|
||||
continue
|
||||
} else if err != nil {
|
||||
it.err = fmt.Errorf("retrieve handle for ID %d: %w", id, err)
|
||||
break
|
||||
}
|
||||
|
||||
it.Handle.Close()
|
||||
it.ID, it.Handle = id, handle
|
||||
return true
|
||||
}
|
||||
|
||||
// No more handles or we encountered an error.
|
||||
it.Handle.Close()
|
||||
it.Handle = nil
|
||||
return false
|
||||
}
|
||||
|
||||
// Take the ownership of the current handle.
|
||||
//
|
||||
// It's the callers responsibility to close the handle.
|
||||
func (it *HandleIterator) Take() *Handle {
|
||||
handle := it.Handle
|
||||
it.Handle = nil
|
||||
return handle
|
||||
}
|
||||
|
||||
// Err returns an error if iteration failed for some reason.
|
||||
func (it *HandleIterator) Err() error {
|
||||
return it.err
|
||||
}
|
||||
|
||||
// FindHandle returns the first handle for which predicate returns true.
|
||||
//
|
||||
// Requires CAP_SYS_ADMIN.
|
||||
//
|
||||
// Returns an error wrapping ErrNotFound if predicate never returns true or if
|
||||
// there is no BTF loaded into the kernel.
|
||||
func FindHandle(predicate func(info *HandleInfo) bool) (*Handle, error) {
|
||||
it := new(HandleIterator)
|
||||
defer it.Handle.Close()
|
||||
|
||||
for it.Next() {
|
||||
info, err := it.Handle.Info()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("info for ID %d: %w", it.ID, err)
|
||||
}
|
||||
|
||||
if predicate(info) {
|
||||
return it.Take(), nil
|
||||
}
|
||||
}
|
||||
if err := it.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate handles: %w", err)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("find handle: %w", ErrNotFound)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package btf_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf/btf"
|
||||
"github.com/cilium/ebpf/internal/testutils"
|
||||
)
|
||||
|
||||
func TestHandleIterator(t *testing.T) {
|
||||
// There is no guarantee that there is a BTF ID allocated, but loading a module
|
||||
// triggers loading vmlinux.
|
||||
// See https://github.com/torvalds/linux/commit/5329722057d41aebc31e391907a501feaa42f7d9
|
||||
testutils.SkipOnOldKernel(t, "5.11", "vmlinux BTF ID")
|
||||
|
||||
it := new(btf.HandleIterator)
|
||||
defer it.Handle.Close()
|
||||
|
||||
if !it.Next() {
|
||||
t.Fatalf("No BTF loaded")
|
||||
}
|
||||
if it.Handle == nil {
|
||||
t.Fatal("Next doesn't assign handle")
|
||||
}
|
||||
prev := it.ID
|
||||
for it.Next() {
|
||||
// Iterate all loaded BTF.
|
||||
if it.Handle == nil {
|
||||
t.Fatal("Next doesn't assign handle")
|
||||
}
|
||||
if it.ID == prev {
|
||||
t.Fatal("Iterator doesn't advance ID")
|
||||
}
|
||||
prev = it.ID
|
||||
}
|
||||
if err := it.Err(); err != nil {
|
||||
t.Fatal("Iteration returned an error:", err)
|
||||
}
|
||||
|
||||
if it.Handle != nil {
|
||||
t.Fatal("Next doesn't clean up handle on last iteration")
|
||||
}
|
||||
if prev != it.ID {
|
||||
t.Fatal("Next changes ID on last iteration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseModuleSplitSpec(t *testing.T) {
|
||||
// See TestNewHandleFromID for reasoning.
|
||||
testutils.SkipOnOldKernel(t, "5.11", "vmlinux BTF ID")
|
||||
|
||||
module, err := btf.FindHandle(func(info *btf.HandleInfo) bool {
|
||||
if info.IsModule() {
|
||||
t.Log("Using module", info.Name)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer module.Close()
|
||||
|
||||
vmlinux, err := btf.FindHandle(func(info *btf.HandleInfo) bool {
|
||||
return info.IsVmlinux()
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer vmlinux.Close()
|
||||
|
||||
base, err := vmlinux.Spec(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = module.Spec(base)
|
||||
if err != nil {
|
||||
t.Fatal("Parse module BTF:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleHandleIterator() {
|
||||
it := new(btf.HandleIterator)
|
||||
defer it.Handle.Close()
|
||||
|
||||
for it.Next() {
|
||||
info, err := it.Handle.Info()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Found handle with ID %d and name %s\n", it.ID, info.Name)
|
||||
}
|
||||
if err := it.Err(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type MarshalOptions struct {
|
||||
// Target byte order. Defaults to the system's native endianness.
|
||||
Order binary.ByteOrder
|
||||
// Remove function linkage information for compatibility with <5.6 kernels.
|
||||
StripFuncLinkage bool
|
||||
}
|
||||
|
||||
// KernelMarshalOptions will generate BTF suitable for the current kernel.
|
||||
func KernelMarshalOptions() *MarshalOptions {
|
||||
return &MarshalOptions{
|
||||
Order: internal.NativeEndian,
|
||||
StripFuncLinkage: haveFuncLinkage() != nil,
|
||||
}
|
||||
}
|
||||
|
||||
// encoder turns Types into raw BTF.
|
||||
type encoder struct {
|
||||
MarshalOptions
|
||||
|
||||
pending internal.Deque[Type]
|
||||
buf *bytes.Buffer
|
||||
strings *stringTableBuilder
|
||||
ids map[Type]TypeID
|
||||
lastID TypeID
|
||||
}
|
||||
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() any {
|
||||
buf := make([]byte, btfHeaderLen+128)
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
func getByteSlice() *[]byte {
|
||||
return bufferPool.Get().(*[]byte)
|
||||
}
|
||||
|
||||
func putByteSlice(buf *[]byte) {
|
||||
*buf = (*buf)[:0]
|
||||
bufferPool.Put(buf)
|
||||
}
|
||||
|
||||
// Builder turns Types into raw BTF.
|
||||
//
|
||||
// The default value may be used and represents an empty BTF blob. Void is
|
||||
// added implicitly if necessary.
|
||||
type Builder struct {
|
||||
// Explicitly added types.
|
||||
types []Type
|
||||
// IDs for all added types which the user knows about.
|
||||
stableIDs map[Type]TypeID
|
||||
// Explicitly added strings.
|
||||
strings *stringTableBuilder
|
||||
}
|
||||
|
||||
// NewBuilder creates a Builder from a list of types.
|
||||
//
|
||||
// It is more efficient than calling [Add] individually.
|
||||
//
|
||||
// Returns an error if adding any of the types fails.
|
||||
func NewBuilder(types []Type) (*Builder, error) {
|
||||
b := &Builder{
|
||||
make([]Type, 0, len(types)),
|
||||
make(map[Type]TypeID, len(types)),
|
||||
nil,
|
||||
}
|
||||
|
||||
for _, typ := range types {
|
||||
_, err := b.Add(typ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add %s: %w", typ, err)
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Add a Type and allocate a stable ID for it.
|
||||
//
|
||||
// Adding the identical Type multiple times is valid and will return the same ID.
|
||||
//
|
||||
// See [Type] for details on identity.
|
||||
func (b *Builder) Add(typ Type) (TypeID, error) {
|
||||
if b.stableIDs == nil {
|
||||
b.stableIDs = make(map[Type]TypeID)
|
||||
}
|
||||
|
||||
if _, ok := typ.(*Void); ok {
|
||||
// Equality is weird for void, since it is a zero sized type.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if ds, ok := typ.(*Datasec); ok {
|
||||
if err := datasecResolveWorkaround(b, ds); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
id, ok := b.stableIDs[typ]
|
||||
if ok {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
b.types = append(b.types, typ)
|
||||
|
||||
id = TypeID(len(b.types))
|
||||
if int(id) != len(b.types) {
|
||||
return 0, fmt.Errorf("no more type IDs")
|
||||
}
|
||||
|
||||
b.stableIDs[typ] = id
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Marshal encodes all types in the Marshaler into BTF wire format.
|
||||
//
|
||||
// opts may be nil.
|
||||
func (b *Builder) Marshal(buf []byte, opts *MarshalOptions) ([]byte, error) {
|
||||
stb := b.strings
|
||||
if stb == nil {
|
||||
// Assume that most types are named. This makes encoding large BTF like
|
||||
// vmlinux a lot cheaper.
|
||||
stb = newStringTableBuilder(len(b.types))
|
||||
} else {
|
||||
// Avoid modifying the Builder's string table.
|
||||
stb = b.strings.Copy()
|
||||
}
|
||||
|
||||
if opts == nil {
|
||||
opts = &MarshalOptions{Order: internal.NativeEndian}
|
||||
}
|
||||
|
||||
// Reserve space for the BTF header.
|
||||
buf = slices.Grow(buf, btfHeaderLen)[:btfHeaderLen]
|
||||
|
||||
w := internal.NewBuffer(buf)
|
||||
defer internal.PutBuffer(w)
|
||||
|
||||
e := encoder{
|
||||
MarshalOptions: *opts,
|
||||
buf: w,
|
||||
strings: stb,
|
||||
lastID: TypeID(len(b.types)),
|
||||
ids: make(map[Type]TypeID, len(b.types)),
|
||||
}
|
||||
|
||||
// Ensure that types are marshaled in the exact order they were Add()ed.
|
||||
// Otherwise the ID returned from Add() won't match.
|
||||
e.pending.Grow(len(b.types))
|
||||
for _, typ := range b.types {
|
||||
e.pending.Push(typ)
|
||||
e.ids[typ] = b.stableIDs[typ]
|
||||
}
|
||||
|
||||
if err := e.deflatePending(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
length := e.buf.Len()
|
||||
typeLen := uint32(length - btfHeaderLen)
|
||||
|
||||
stringLen := e.strings.Length()
|
||||
buf = e.strings.AppendEncoded(e.buf.Bytes())
|
||||
|
||||
// Fill out the header, and write it out.
|
||||
header := &btfHeader{
|
||||
Magic: btfMagic,
|
||||
Version: 1,
|
||||
Flags: 0,
|
||||
HdrLen: uint32(btfHeaderLen),
|
||||
TypeOff: 0,
|
||||
TypeLen: typeLen,
|
||||
StringOff: typeLen,
|
||||
StringLen: uint32(stringLen),
|
||||
}
|
||||
|
||||
err := binary.Write(sliceWriter(buf[:btfHeaderLen]), e.Order, header)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("write header: %v", err)
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// addString adds a string to the resulting BTF.
|
||||
//
|
||||
// Adding the same string multiple times will return the same result.
|
||||
//
|
||||
// Returns an identifier into the string table or an error if the string
|
||||
// contains invalid characters.
|
||||
func (b *Builder) addString(str string) (uint32, error) {
|
||||
if b.strings == nil {
|
||||
b.strings = newStringTableBuilder(0)
|
||||
}
|
||||
|
||||
return b.strings.Add(str)
|
||||
}
|
||||
|
||||
func (e *encoder) allocateID(typ Type) error {
|
||||
id := e.lastID + 1
|
||||
if id < e.lastID {
|
||||
return errors.New("type ID overflow")
|
||||
}
|
||||
|
||||
e.pending.Push(typ)
|
||||
e.ids[typ] = id
|
||||
e.lastID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// id returns the ID for the given type or panics with an error.
|
||||
func (e *encoder) id(typ Type) TypeID {
|
||||
if _, ok := typ.(*Void); ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
id, ok := e.ids[typ]
|
||||
if !ok {
|
||||
panic(fmt.Errorf("no ID for type %v", typ))
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (e *encoder) deflatePending() error {
|
||||
// Declare root outside of the loop to avoid repeated heap allocations.
|
||||
var root Type
|
||||
skip := func(t Type) (skip bool) {
|
||||
if t == root {
|
||||
// Force descending into the current root type even if it already
|
||||
// has an ID. Otherwise we miss children of types that have their
|
||||
// ID pre-allocated via Add.
|
||||
return false
|
||||
}
|
||||
|
||||
_, isVoid := t.(*Void)
|
||||
_, alreadyEncoded := e.ids[t]
|
||||
return isVoid || alreadyEncoded
|
||||
}
|
||||
|
||||
for !e.pending.Empty() {
|
||||
root = e.pending.Shift()
|
||||
|
||||
// Allocate IDs for all children of typ, including transitive dependencies.
|
||||
iter := postorderTraversal(root, skip)
|
||||
for iter.Next() {
|
||||
if iter.Type == root {
|
||||
// The iterator yields root at the end, do not allocate another ID.
|
||||
break
|
||||
}
|
||||
|
||||
if err := e.allocateID(iter.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := e.deflateType(root); err != nil {
|
||||
id := e.ids[root]
|
||||
return fmt.Errorf("deflate %v with ID %d: %w", root, id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *encoder) deflateType(typ Type) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
var ok bool
|
||||
err, ok = r.(error)
|
||||
if !ok {
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var raw rawType
|
||||
raw.NameOff, err = e.strings.Add(typ.TypeName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v := typ.(type) {
|
||||
case *Void:
|
||||
return errors.New("Void is implicit in BTF wire format")
|
||||
|
||||
case *Int:
|
||||
raw.SetKind(kindInt)
|
||||
raw.SetSize(v.Size)
|
||||
|
||||
var bi btfInt
|
||||
bi.SetEncoding(v.Encoding)
|
||||
// We need to set bits in addition to size, since btf_type_int_is_regular
|
||||
// otherwise flags this as a bitfield.
|
||||
bi.SetBits(byte(v.Size) * 8)
|
||||
raw.data = bi
|
||||
|
||||
case *Pointer:
|
||||
raw.SetKind(kindPointer)
|
||||
raw.SetType(e.id(v.Target))
|
||||
|
||||
case *Array:
|
||||
raw.SetKind(kindArray)
|
||||
raw.data = &btfArray{
|
||||
e.id(v.Type),
|
||||
e.id(v.Index),
|
||||
v.Nelems,
|
||||
}
|
||||
|
||||
case *Struct:
|
||||
raw.SetKind(kindStruct)
|
||||
raw.SetSize(v.Size)
|
||||
raw.data, err = e.convertMembers(&raw.btfType, v.Members)
|
||||
|
||||
case *Union:
|
||||
raw.SetKind(kindUnion)
|
||||
raw.SetSize(v.Size)
|
||||
raw.data, err = e.convertMembers(&raw.btfType, v.Members)
|
||||
|
||||
case *Enum:
|
||||
raw.SetSize(v.size())
|
||||
raw.SetVlen(len(v.Values))
|
||||
raw.SetSigned(v.Signed)
|
||||
|
||||
if v.has64BitValues() {
|
||||
raw.SetKind(kindEnum64)
|
||||
raw.data, err = e.deflateEnum64Values(v.Values)
|
||||
} else {
|
||||
raw.SetKind(kindEnum)
|
||||
raw.data, err = e.deflateEnumValues(v.Values)
|
||||
}
|
||||
|
||||
case *Fwd:
|
||||
raw.SetKind(kindForward)
|
||||
raw.SetFwdKind(v.Kind)
|
||||
|
||||
case *Typedef:
|
||||
raw.SetKind(kindTypedef)
|
||||
raw.SetType(e.id(v.Type))
|
||||
|
||||
case *Volatile:
|
||||
raw.SetKind(kindVolatile)
|
||||
raw.SetType(e.id(v.Type))
|
||||
|
||||
case *Const:
|
||||
raw.SetKind(kindConst)
|
||||
raw.SetType(e.id(v.Type))
|
||||
|
||||
case *Restrict:
|
||||
raw.SetKind(kindRestrict)
|
||||
raw.SetType(e.id(v.Type))
|
||||
|
||||
case *Func:
|
||||
raw.SetKind(kindFunc)
|
||||
raw.SetType(e.id(v.Type))
|
||||
if !e.StripFuncLinkage {
|
||||
raw.SetLinkage(v.Linkage)
|
||||
}
|
||||
|
||||
case *FuncProto:
|
||||
raw.SetKind(kindFuncProto)
|
||||
raw.SetType(e.id(v.Return))
|
||||
raw.SetVlen(len(v.Params))
|
||||
raw.data, err = e.deflateFuncParams(v.Params)
|
||||
|
||||
case *Var:
|
||||
raw.SetKind(kindVar)
|
||||
raw.SetType(e.id(v.Type))
|
||||
raw.data = btfVariable{uint32(v.Linkage)}
|
||||
|
||||
case *Datasec:
|
||||
raw.SetKind(kindDatasec)
|
||||
raw.SetSize(v.Size)
|
||||
raw.SetVlen(len(v.Vars))
|
||||
raw.data = e.deflateVarSecinfos(v.Vars)
|
||||
|
||||
case *Float:
|
||||
raw.SetKind(kindFloat)
|
||||
raw.SetSize(v.Size)
|
||||
|
||||
case *declTag:
|
||||
raw.SetKind(kindDeclTag)
|
||||
raw.SetType(e.id(v.Type))
|
||||
raw.data = &btfDeclTag{uint32(v.Index)}
|
||||
raw.NameOff, err = e.strings.Add(v.Value)
|
||||
|
||||
case *typeTag:
|
||||
raw.SetKind(kindTypeTag)
|
||||
raw.SetType(e.id(v.Type))
|
||||
raw.NameOff, err = e.strings.Add(v.Value)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("don't know how to deflate %T", v)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return raw.Marshal(e.buf, e.Order)
|
||||
}
|
||||
|
||||
func (e *encoder) convertMembers(header *btfType, members []Member) ([]btfMember, error) {
|
||||
bms := make([]btfMember, 0, len(members))
|
||||
isBitfield := false
|
||||
for _, member := range members {
|
||||
isBitfield = isBitfield || member.BitfieldSize > 0
|
||||
|
||||
offset := member.Offset
|
||||
if isBitfield {
|
||||
offset = member.BitfieldSize<<24 | (member.Offset & 0xffffff)
|
||||
}
|
||||
|
||||
nameOff, err := e.strings.Add(member.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bms = append(bms, btfMember{
|
||||
nameOff,
|
||||
e.id(member.Type),
|
||||
uint32(offset),
|
||||
})
|
||||
}
|
||||
|
||||
header.SetVlen(len(members))
|
||||
header.SetBitfield(isBitfield)
|
||||
return bms, nil
|
||||
}
|
||||
|
||||
func (e *encoder) deflateEnumValues(values []EnumValue) ([]btfEnum, error) {
|
||||
bes := make([]btfEnum, 0, len(values))
|
||||
for _, value := range values {
|
||||
nameOff, err := e.strings.Add(value.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if value.Value > math.MaxUint32 {
|
||||
return nil, fmt.Errorf("value of enum %q exceeds 32 bits", value.Name)
|
||||
}
|
||||
|
||||
bes = append(bes, btfEnum{
|
||||
nameOff,
|
||||
uint32(value.Value),
|
||||
})
|
||||
}
|
||||
|
||||
return bes, nil
|
||||
}
|
||||
|
||||
func (e *encoder) deflateEnum64Values(values []EnumValue) ([]btfEnum64, error) {
|
||||
bes := make([]btfEnum64, 0, len(values))
|
||||
for _, value := range values {
|
||||
nameOff, err := e.strings.Add(value.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bes = append(bes, btfEnum64{
|
||||
nameOff,
|
||||
uint32(value.Value),
|
||||
uint32(value.Value >> 32),
|
||||
})
|
||||
}
|
||||
|
||||
return bes, nil
|
||||
}
|
||||
|
||||
func (e *encoder) deflateFuncParams(params []FuncParam) ([]btfParam, error) {
|
||||
bps := make([]btfParam, 0, len(params))
|
||||
for _, param := range params {
|
||||
nameOff, err := e.strings.Add(param.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bps = append(bps, btfParam{
|
||||
nameOff,
|
||||
e.id(param.Type),
|
||||
})
|
||||
}
|
||||
return bps, nil
|
||||
}
|
||||
|
||||
func (e *encoder) deflateVarSecinfos(vars []VarSecinfo) []btfVarSecinfo {
|
||||
vsis := make([]btfVarSecinfo, 0, len(vars))
|
||||
for _, v := range vars {
|
||||
vsis = append(vsis, btfVarSecinfo{
|
||||
e.id(v.Type),
|
||||
v.Offset,
|
||||
v.Size,
|
||||
})
|
||||
}
|
||||
return vsis
|
||||
}
|
||||
|
||||
// MarshalMapKV creates a BTF object containing a map key and value.
|
||||
//
|
||||
// The function is intended for the use of the ebpf package and may be removed
|
||||
// at any point in time.
|
||||
func MarshalMapKV(key, value Type) (_ *Handle, keyID, valueID TypeID, err error) {
|
||||
var b Builder
|
||||
|
||||
if key != nil {
|
||||
keyID, err = b.Add(key)
|
||||
if err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("add key type: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
valueID, err = b.Add(value)
|
||||
if err != nil {
|
||||
return nil, 0, 0, fmt.Errorf("add value type: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
handle, err := NewHandle(&b)
|
||||
if err != nil {
|
||||
// Check for 'full' map BTF support, since kernels between 4.18 and 5.2
|
||||
// already support BTF blobs for maps without Var or Datasec just fine.
|
||||
if err := haveMapBTF(); err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
}
|
||||
return handle, keyID, valueID, err
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
"github.com/cilium/ebpf/internal/testutils"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestBuilderMarshal(t *testing.T) {
|
||||
typ := &Int{
|
||||
Name: "foo",
|
||||
Size: 2,
|
||||
Encoding: Signed | Char,
|
||||
}
|
||||
|
||||
want := []Type{
|
||||
(*Void)(nil),
|
||||
typ,
|
||||
&Pointer{typ},
|
||||
&Typedef{"baz", typ},
|
||||
}
|
||||
|
||||
b, err := NewBuilder(want)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
|
||||
cpy := *b
|
||||
buf, err := b.Marshal(nil, &MarshalOptions{Order: internal.NativeEndian})
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b, qt.CmpEquals(cmp.AllowUnexported(*b)), &cpy, qt.Commentf("Marshaling should not change Builder state"))
|
||||
|
||||
have, err := loadRawSpec(bytes.NewReader(buf), internal.NativeEndian, nil)
|
||||
qt.Assert(t, err, qt.IsNil, qt.Commentf("Couldn't parse BTF"))
|
||||
qt.Assert(t, have.types, qt.DeepEquals, want)
|
||||
}
|
||||
|
||||
func TestBuilderAdd(t *testing.T) {
|
||||
i := &Int{
|
||||
Name: "foo",
|
||||
Size: 2,
|
||||
Encoding: Signed | Char,
|
||||
}
|
||||
pi := &Pointer{i}
|
||||
|
||||
var b Builder
|
||||
id, err := b.Add(pi)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, id, qt.Equals, TypeID(1), qt.Commentf("First non-void type doesn't get id 1"))
|
||||
|
||||
id, err = b.Add(pi)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, id, qt.Equals, TypeID(1))
|
||||
|
||||
id, err = b.Add(i)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, id, qt.Equals, TypeID(2), qt.Commentf("Second type doesn't get id 2"))
|
||||
|
||||
id, err = b.Add(i)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, id, qt.Equals, TypeID(2), qt.Commentf("Adding a type twice returns different ids"))
|
||||
|
||||
id, err = b.Add(&Typedef{"baz", i})
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, id, qt.Equals, TypeID(3))
|
||||
}
|
||||
|
||||
func TestRoundtripVMlinux(t *testing.T) {
|
||||
types := vmlinuxSpec(t).types
|
||||
|
||||
// Randomize the order to force different permutations of walking the type
|
||||
// graph. Keep Void at index 0.
|
||||
testutils.Rand().Shuffle(len(types[1:]), func(i, j int) {
|
||||
types[i+1], types[j+1] = types[j+1], types[i+1]
|
||||
})
|
||||
|
||||
// Skip per CPU datasec, see https://github.com/cilium/ebpf/issues/921
|
||||
for i, typ := range types {
|
||||
if ds, ok := typ.(*Datasec); ok && ds.Name == ".data..percpu" {
|
||||
types[i] = types[len(types)-1]
|
||||
types = types[:len(types)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
seen := make(map[Type]bool)
|
||||
limitTypes:
|
||||
for i, typ := range types {
|
||||
iter := postorderTraversal(typ, func(t Type) (skip bool) {
|
||||
return seen[t]
|
||||
})
|
||||
for iter.Next() {
|
||||
seen[iter.Type] = true
|
||||
}
|
||||
if len(seen) >= math.MaxInt16 {
|
||||
// IDs exceeding math.MaxUint16 can trigger a bug when loading BTF.
|
||||
// This can be removed once the patch lands.
|
||||
// See https://lore.kernel.org/bpf/20220909092107.3035-1-oss@lmb.io/
|
||||
types = types[:i]
|
||||
break limitTypes
|
||||
}
|
||||
}
|
||||
|
||||
buf := marshalNativeEndian(t, types)
|
||||
|
||||
rebuilt, err := loadRawSpec(bytes.NewReader(buf), binary.LittleEndian, nil)
|
||||
qt.Assert(t, err, qt.IsNil, qt.Commentf("round tripping BTF failed"))
|
||||
|
||||
if n := len(rebuilt.types); n > math.MaxUint16 {
|
||||
t.Logf("Rebuilt BTF contains %d types which exceeds uint16, test may fail on older kernels", n)
|
||||
}
|
||||
|
||||
h, err := NewHandleFromRawBTF(buf)
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
qt.Assert(t, err, qt.IsNil, qt.Commentf("loading rebuilt BTF failed"))
|
||||
h.Close()
|
||||
}
|
||||
|
||||
func BenchmarkMarshaler(b *testing.B) {
|
||||
spec := vmlinuxTestdataSpec(b)
|
||||
types := spec.types[:100]
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var b Builder
|
||||
for _, typ := range types {
|
||||
_, _ = b.Add(typ)
|
||||
}
|
||||
_, _ = b.Marshal(nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBuildVmlinux(b *testing.B) {
|
||||
types := vmlinuxTestdataSpec(b).types
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var b Builder
|
||||
for _, typ := range types {
|
||||
_, _ = b.Add(typ)
|
||||
}
|
||||
_, _ = b.Marshal(nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func marshalNativeEndian(tb testing.TB, types []Type) []byte {
|
||||
tb.Helper()
|
||||
|
||||
b, err := NewBuilder(types)
|
||||
qt.Assert(tb, err, qt.IsNil)
|
||||
buf, err := b.Marshal(nil, nil)
|
||||
qt.Assert(tb, err, qt.IsNil)
|
||||
return buf
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
type stringTable struct {
|
||||
base *stringTable
|
||||
offsets []uint32
|
||||
strings []string
|
||||
}
|
||||
|
||||
// sizedReader is implemented by bytes.Reader, io.SectionReader, strings.Reader, etc.
|
||||
type sizedReader interface {
|
||||
io.Reader
|
||||
Size() int64
|
||||
}
|
||||
|
||||
func readStringTable(r sizedReader, base *stringTable) (*stringTable, error) {
|
||||
// When parsing split BTF's string table, the first entry offset is derived
|
||||
// from the last entry offset of the base BTF.
|
||||
firstStringOffset := uint32(0)
|
||||
if base != nil {
|
||||
idx := len(base.offsets) - 1
|
||||
firstStringOffset = base.offsets[idx] + uint32(len(base.strings[idx])) + 1
|
||||
}
|
||||
|
||||
// Derived from vmlinux BTF.
|
||||
const averageStringLength = 16
|
||||
|
||||
n := int(r.Size() / averageStringLength)
|
||||
offsets := make([]uint32, 0, n)
|
||||
strings := make([]string, 0, n)
|
||||
|
||||
offset := firstStringOffset
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Split(splitNull)
|
||||
for scanner.Scan() {
|
||||
str := scanner.Text()
|
||||
offsets = append(offsets, offset)
|
||||
strings = append(strings, str)
|
||||
offset += uint32(len(str)) + 1
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(strings) == 0 {
|
||||
return nil, errors.New("string table is empty")
|
||||
}
|
||||
|
||||
if firstStringOffset == 0 && strings[0] != "" {
|
||||
return nil, errors.New("first item in string table is non-empty")
|
||||
}
|
||||
|
||||
return &stringTable{base, offsets, strings}, nil
|
||||
}
|
||||
|
||||
func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
i := bytes.IndexByte(data, 0)
|
||||
if i == -1 {
|
||||
if atEOF && len(data) > 0 {
|
||||
return 0, nil, errors.New("string table isn't null terminated")
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
return i + 1, data[:i], nil
|
||||
}
|
||||
|
||||
func (st *stringTable) Lookup(offset uint32) (string, error) {
|
||||
if st.base != nil && offset <= st.base.offsets[len(st.base.offsets)-1] {
|
||||
return st.base.lookup(offset)
|
||||
}
|
||||
return st.lookup(offset)
|
||||
}
|
||||
|
||||
func (st *stringTable) lookup(offset uint32) (string, error) {
|
||||
i := search(st.offsets, offset)
|
||||
if i == len(st.offsets) || st.offsets[i] != offset {
|
||||
return "", fmt.Errorf("offset %d isn't start of a string", offset)
|
||||
}
|
||||
|
||||
return st.strings[i], nil
|
||||
}
|
||||
|
||||
func (st *stringTable) Marshal(w io.Writer) error {
|
||||
for _, str := range st.strings {
|
||||
_, err := io.WriteString(w, str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte{0})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Num returns the number of strings in the table.
|
||||
func (st *stringTable) Num() int {
|
||||
return len(st.strings)
|
||||
}
|
||||
|
||||
// search is a copy of sort.Search specialised for uint32.
|
||||
//
|
||||
// Licensed under https://go.dev/LICENSE
|
||||
func search(ints []uint32, needle uint32) int {
|
||||
// Define f(-1) == false and f(n) == true.
|
||||
// Invariant: f(i-1) == false, f(j) == true.
|
||||
i, j := 0, len(ints)
|
||||
for i < j {
|
||||
h := int(uint(i+j) >> 1) // avoid overflow when computing h
|
||||
// i ≤ h < j
|
||||
if !(ints[h] >= needle) {
|
||||
i = h + 1 // preserves f(i-1) == false
|
||||
} else {
|
||||
j = h // preserves f(j) == true
|
||||
}
|
||||
}
|
||||
// i == j, f(i-1) == false, and f(j) (= f(i)) == true => answer is i.
|
||||
return i
|
||||
}
|
||||
|
||||
// stringTableBuilder builds BTF string tables.
|
||||
type stringTableBuilder struct {
|
||||
length uint32
|
||||
strings map[string]uint32
|
||||
}
|
||||
|
||||
// newStringTableBuilder creates a builder with the given capacity.
|
||||
//
|
||||
// capacity may be zero.
|
||||
func newStringTableBuilder(capacity int) *stringTableBuilder {
|
||||
var stb stringTableBuilder
|
||||
|
||||
if capacity == 0 {
|
||||
// Use the runtime's small default size.
|
||||
stb.strings = make(map[string]uint32)
|
||||
} else {
|
||||
stb.strings = make(map[string]uint32, capacity)
|
||||
}
|
||||
|
||||
// Ensure that the empty string is at index 0.
|
||||
stb.append("")
|
||||
return &stb
|
||||
}
|
||||
|
||||
// Add a string to the table.
|
||||
//
|
||||
// Adding the same string multiple times will only store it once.
|
||||
func (stb *stringTableBuilder) Add(str string) (uint32, error) {
|
||||
if strings.IndexByte(str, 0) != -1 {
|
||||
return 0, fmt.Errorf("string contains null: %q", str)
|
||||
}
|
||||
|
||||
offset, ok := stb.strings[str]
|
||||
if ok {
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
return stb.append(str), nil
|
||||
}
|
||||
|
||||
func (stb *stringTableBuilder) append(str string) uint32 {
|
||||
offset := stb.length
|
||||
stb.length += uint32(len(str)) + 1
|
||||
stb.strings[str] = offset
|
||||
return offset
|
||||
}
|
||||
|
||||
// Lookup finds the offset of a string in the table.
|
||||
//
|
||||
// Returns an error if str hasn't been added yet.
|
||||
func (stb *stringTableBuilder) Lookup(str string) (uint32, error) {
|
||||
offset, ok := stb.strings[str]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("string %q is not in table", str)
|
||||
}
|
||||
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
// Length returns the length in bytes.
|
||||
func (stb *stringTableBuilder) Length() int {
|
||||
return int(stb.length)
|
||||
}
|
||||
|
||||
// AppendEncoded appends the string table to the end of the provided buffer.
|
||||
func (stb *stringTableBuilder) AppendEncoded(buf []byte) []byte {
|
||||
n := len(buf)
|
||||
buf = append(buf, make([]byte, stb.Length())...)
|
||||
strings := buf[n:]
|
||||
for str, offset := range stb.strings {
|
||||
copy(strings[offset:], str)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// Copy the string table builder.
|
||||
func (stb *stringTableBuilder) Copy() *stringTableBuilder {
|
||||
return &stringTableBuilder{
|
||||
stb.length,
|
||||
maps.Clone(stb.strings),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestStringTable(t *testing.T) {
|
||||
const in = "\x00one\x00two\x00"
|
||||
const splitIn = "three\x00four\x00"
|
||||
|
||||
st, err := readStringTable(strings.NewReader(in), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := st.Marshal(&buf); err != nil {
|
||||
t.Fatal("Can't marshal string table:", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal([]byte(in), buf.Bytes()) {
|
||||
t.Error("String table doesn't match input")
|
||||
}
|
||||
|
||||
// Parse string table of split BTF
|
||||
split, err := readStringTable(strings.NewReader(splitIn), st)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
offset uint32
|
||||
want string
|
||||
}{
|
||||
{0, ""},
|
||||
{1, "one"},
|
||||
{5, "two"},
|
||||
{9, "three"},
|
||||
{15, "four"},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
have, err := split.Lookup(tc.offset)
|
||||
if err != nil {
|
||||
t.Errorf("Offset %d: %s", tc.offset, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if have != tc.want {
|
||||
t.Errorf("Offset %d: want %s but have %s", tc.offset, tc.want, have)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := st.Lookup(2); err == nil {
|
||||
t.Error("No error when using offset pointing into middle of string")
|
||||
}
|
||||
|
||||
// Make sure we reject bogus tables
|
||||
_, err = readStringTable(strings.NewReader("\x00one"), nil)
|
||||
if err == nil {
|
||||
t.Fatal("Accepted non-terminated string")
|
||||
}
|
||||
|
||||
_, err = readStringTable(strings.NewReader("one\x00"), nil)
|
||||
if err == nil {
|
||||
t.Fatal("Accepted non-empty first item")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringTableBuilder(t *testing.T) {
|
||||
stb := newStringTableBuilder(0)
|
||||
|
||||
_, err := readStringTable(bytes.NewReader(stb.AppendEncoded(nil)), nil)
|
||||
qt.Assert(t, err, qt.IsNil, qt.Commentf("Can't parse string table"))
|
||||
|
||||
_, err = stb.Add("foo\x00bar")
|
||||
qt.Assert(t, err, qt.IsNotNil)
|
||||
|
||||
empty, err := stb.Add("")
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, empty, qt.Equals, uint32(0), qt.Commentf("The empty string is not at index 0"))
|
||||
|
||||
foo1, _ := stb.Add("foo")
|
||||
foo2, _ := stb.Add("foo")
|
||||
qt.Assert(t, foo1, qt.Equals, foo2, qt.Commentf("Adding the same string returns different offsets"))
|
||||
|
||||
table := stb.AppendEncoded(nil)
|
||||
if n := bytes.Count(table, []byte("foo")); n != 1 {
|
||||
t.Fatalf("Marshalled string table contains foo %d times instead of once", n)
|
||||
}
|
||||
|
||||
_, err = readStringTable(bytes.NewReader(table), nil)
|
||||
qt.Assert(t, err, qt.IsNil, qt.Commentf("Can't parse string table"))
|
||||
}
|
||||
|
||||
func newStringTable(strings ...string) *stringTable {
|
||||
offsets := make([]uint32, len(strings))
|
||||
|
||||
var offset uint32
|
||||
for i, str := range strings {
|
||||
offsets[i] = offset
|
||||
offset += uint32(len(str)) + 1 // account for NUL
|
||||
}
|
||||
|
||||
return &stringTable{nil, offsets, strings}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __BPF_CORE_READ_H__
|
||||
#define __BPF_CORE_READ_H__
|
||||
|
||||
/*
|
||||
* enum bpf_field_info_kind is passed as a second argument into
|
||||
* __builtin_preserve_field_info() built-in to get a specific aspect of
|
||||
* a field, captured as a first argument. __builtin_preserve_field_info(field,
|
||||
* info_kind) returns __u32 integer and produces BTF field relocation, which
|
||||
* is understood and processed by libbpf during BPF object loading. See
|
||||
* selftests/bpf for examples.
|
||||
*/
|
||||
enum bpf_field_info_kind {
|
||||
BPF_FIELD_BYTE_OFFSET = 0, /* field byte offset */
|
||||
BPF_FIELD_BYTE_SIZE = 1,
|
||||
BPF_FIELD_EXISTS = 2, /* field existence in target kernel */
|
||||
BPF_FIELD_SIGNED = 3,
|
||||
BPF_FIELD_LSHIFT_U64 = 4,
|
||||
BPF_FIELD_RSHIFT_U64 = 5,
|
||||
};
|
||||
|
||||
/* second argument to __builtin_btf_type_id() built-in */
|
||||
enum bpf_type_id_kind {
|
||||
BPF_TYPE_ID_LOCAL = 0, /* BTF type ID in local program */
|
||||
BPF_TYPE_ID_TARGET = 1, /* BTF type ID in target kernel */
|
||||
};
|
||||
|
||||
/* second argument to __builtin_preserve_type_info() built-in */
|
||||
enum bpf_type_info_kind {
|
||||
BPF_TYPE_EXISTS = 0, /* type existence in target kernel */
|
||||
BPF_TYPE_SIZE = 1, /* type size in target kernel */
|
||||
};
|
||||
|
||||
/* second argument to __builtin_preserve_enum_value() built-in */
|
||||
enum bpf_enum_value_kind {
|
||||
BPF_ENUMVAL_EXISTS = 0, /* enum value existence in kernel */
|
||||
BPF_ENUMVAL_VALUE = 1, /* enum value value relocation */
|
||||
};
|
||||
|
||||
#define __CORE_RELO(src, field, info) \
|
||||
__builtin_preserve_field_info((src)->field, BPF_FIELD_##info)
|
||||
|
||||
#if __BYTE_ORDER == __LITTLE_ENDIAN
|
||||
#define __CORE_BITFIELD_PROBE_READ(dst, src, fld) \
|
||||
bpf_probe_read_kernel( \
|
||||
(void *)dst, \
|
||||
__CORE_RELO(src, fld, BYTE_SIZE), \
|
||||
(const void *)src + __CORE_RELO(src, fld, BYTE_OFFSET))
|
||||
#else
|
||||
/* semantics of LSHIFT_64 assumes loading values into low-ordered bytes, so
|
||||
* for big-endian we need to adjust destination pointer accordingly, based on
|
||||
* field byte size
|
||||
*/
|
||||
#define __CORE_BITFIELD_PROBE_READ(dst, src, fld) \
|
||||
bpf_probe_read_kernel( \
|
||||
(void *)dst + (8 - __CORE_RELO(src, fld, BYTE_SIZE)), \
|
||||
__CORE_RELO(src, fld, BYTE_SIZE), \
|
||||
(const void *)src + __CORE_RELO(src, fld, BYTE_OFFSET))
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Extract bitfield, identified by s->field, and return its value as u64.
|
||||
* All this is done in relocatable manner, so bitfield changes such as
|
||||
* signedness, bit size, offset changes, this will be handled automatically.
|
||||
* This version of macro is using bpf_probe_read_kernel() to read underlying
|
||||
* integer storage. Macro functions as an expression and its return type is
|
||||
* bpf_probe_read_kernel()'s return value: 0, on success, <0 on error.
|
||||
*/
|
||||
#define BPF_CORE_READ_BITFIELD_PROBED(s, field) ({ \
|
||||
unsigned long long val = 0; \
|
||||
\
|
||||
__CORE_BITFIELD_PROBE_READ(&val, s, field); \
|
||||
val <<= __CORE_RELO(s, field, LSHIFT_U64); \
|
||||
if (__CORE_RELO(s, field, SIGNED)) \
|
||||
val = ((long long)val) >> __CORE_RELO(s, field, RSHIFT_U64); \
|
||||
else \
|
||||
val = val >> __CORE_RELO(s, field, RSHIFT_U64); \
|
||||
val; \
|
||||
})
|
||||
|
||||
/*
|
||||
* Extract bitfield, identified by s->field, and return its value as u64.
|
||||
* This version of macro is using direct memory reads and should be used from
|
||||
* BPF program types that support such functionality (e.g., typed raw
|
||||
* tracepoints).
|
||||
*/
|
||||
#define BPF_CORE_READ_BITFIELD(s, field) ({ \
|
||||
const void *p = (const void *)s + __CORE_RELO(s, field, BYTE_OFFSET); \
|
||||
unsigned long long val; \
|
||||
\
|
||||
/* This is a so-called barrier_var() operation that makes specified \
|
||||
* variable "a black box" for optimizing compiler. \
|
||||
* It forces compiler to perform BYTE_OFFSET relocation on p and use \
|
||||
* its calculated value in the switch below, instead of applying \
|
||||
* the same relocation 4 times for each individual memory load. \
|
||||
*/ \
|
||||
asm volatile("" : "=r"(p) : "0"(p)); \
|
||||
\
|
||||
switch (__CORE_RELO(s, field, BYTE_SIZE)) { \
|
||||
case 1: val = *(const unsigned char *)p; break; \
|
||||
case 2: val = *(const unsigned short *)p; break; \
|
||||
case 4: val = *(const unsigned int *)p; break; \
|
||||
case 8: val = *(const unsigned long long *)p; break; \
|
||||
} \
|
||||
val <<= __CORE_RELO(s, field, LSHIFT_U64); \
|
||||
if (__CORE_RELO(s, field, SIGNED)) \
|
||||
val = ((long long)val) >> __CORE_RELO(s, field, RSHIFT_U64); \
|
||||
else \
|
||||
val = val >> __CORE_RELO(s, field, RSHIFT_U64); \
|
||||
val; \
|
||||
})
|
||||
|
||||
/*
|
||||
* Convenience macro to check that field actually exists in target kernel's.
|
||||
* Returns:
|
||||
* 1, if matching field is present in target kernel;
|
||||
* 0, if no matching field found.
|
||||
*/
|
||||
#define bpf_core_field_exists(field) \
|
||||
__builtin_preserve_field_info(field, BPF_FIELD_EXISTS)
|
||||
|
||||
/*
|
||||
* Convenience macro to get the byte size of a field. Works for integers,
|
||||
* struct/unions, pointers, arrays, and enums.
|
||||
*/
|
||||
#define bpf_core_field_size(field) \
|
||||
__builtin_preserve_field_info(field, BPF_FIELD_BYTE_SIZE)
|
||||
|
||||
/*
|
||||
* Convenience macro to get BTF type ID of a specified type, using a local BTF
|
||||
* information. Return 32-bit unsigned integer with type ID from program's own
|
||||
* BTF. Always succeeds.
|
||||
*/
|
||||
#define bpf_core_type_id_local(type) \
|
||||
__builtin_btf_type_id(*(typeof(type) *)0, BPF_TYPE_ID_LOCAL)
|
||||
|
||||
/*
|
||||
* Convenience macro to get BTF type ID of a target kernel's type that matches
|
||||
* specified local type.
|
||||
* Returns:
|
||||
* - valid 32-bit unsigned type ID in kernel BTF;
|
||||
* - 0, if no matching type was found in a target kernel BTF.
|
||||
*/
|
||||
#define bpf_core_type_id_kernel(type) \
|
||||
__builtin_btf_type_id(*(typeof(type) *)0, BPF_TYPE_ID_TARGET)
|
||||
|
||||
/*
|
||||
* Convenience macro to check that provided named type
|
||||
* (struct/union/enum/typedef) exists in a target kernel.
|
||||
* Returns:
|
||||
* 1, if such type is present in target kernel's BTF;
|
||||
* 0, if no matching type is found.
|
||||
*/
|
||||
#define bpf_core_type_exists(type) \
|
||||
__builtin_preserve_type_info(*(typeof(type) *)0, BPF_TYPE_EXISTS)
|
||||
|
||||
/*
|
||||
* Convenience macro to get the byte size of a provided named type
|
||||
* (struct/union/enum/typedef) in a target kernel.
|
||||
* Returns:
|
||||
* >= 0 size (in bytes), if type is present in target kernel's BTF;
|
||||
* 0, if no matching type is found.
|
||||
*/
|
||||
#define bpf_core_type_size(type) \
|
||||
__builtin_preserve_type_info(*(typeof(type) *)0, BPF_TYPE_SIZE)
|
||||
|
||||
/*
|
||||
* Convenience macro to check that provided enumerator value is defined in
|
||||
* a target kernel.
|
||||
* Returns:
|
||||
* 1, if specified enum type and its enumerator value are present in target
|
||||
* kernel's BTF;
|
||||
* 0, if no matching enum and/or enum value within that enum is found.
|
||||
*/
|
||||
#define bpf_core_enum_value_exists(enum_type, enum_value) \
|
||||
__builtin_preserve_enum_value(*(typeof(enum_type) *)enum_value, BPF_ENUMVAL_EXISTS)
|
||||
|
||||
/*
|
||||
* Convenience macro to get the integer value of an enumerator value in
|
||||
* a target kernel.
|
||||
* Returns:
|
||||
* 64-bit value, if specified enum type and its enumerator value are
|
||||
* present in target kernel's BTF;
|
||||
* 0, if no matching enum and/or enum value within that enum is found.
|
||||
*/
|
||||
#define bpf_core_enum_value(enum_type, enum_value) \
|
||||
__builtin_preserve_enum_value(*(typeof(enum_type) *)enum_value, BPF_ENUMVAL_VALUE)
|
||||
|
||||
/*
|
||||
* bpf_core_read() abstracts away bpf_probe_read_kernel() call and captures
|
||||
* offset relocation for source address using __builtin_preserve_access_index()
|
||||
* built-in, provided by Clang.
|
||||
*
|
||||
* __builtin_preserve_access_index() takes as an argument an expression of
|
||||
* taking an address of a field within struct/union. It makes compiler emit
|
||||
* a relocation, which records BTF type ID describing root struct/union and an
|
||||
* accessor string which describes exact embedded field that was used to take
|
||||
* an address. See detailed description of this relocation format and
|
||||
* semantics in comments to struct bpf_field_reloc in libbpf_internal.h.
|
||||
*
|
||||
* This relocation allows libbpf to adjust BPF instruction to use correct
|
||||
* actual field offset, based on target kernel BTF type that matches original
|
||||
* (local) BTF, used to record relocation.
|
||||
*/
|
||||
#define bpf_core_read(dst, sz, src) \
|
||||
bpf_probe_read_kernel(dst, sz, (const void *)__builtin_preserve_access_index(src))
|
||||
|
||||
/* NOTE: see comments for BPF_CORE_READ_USER() about the proper types use. */
|
||||
#define bpf_core_read_user(dst, sz, src) \
|
||||
bpf_probe_read_user(dst, sz, (const void *)__builtin_preserve_access_index(src))
|
||||
/*
|
||||
* bpf_core_read_str() is a thin wrapper around bpf_probe_read_str()
|
||||
* additionally emitting BPF CO-RE field relocation for specified source
|
||||
* argument.
|
||||
*/
|
||||
#define bpf_core_read_str(dst, sz, src) \
|
||||
bpf_probe_read_kernel_str(dst, sz, (const void *)__builtin_preserve_access_index(src))
|
||||
|
||||
/* NOTE: see comments for BPF_CORE_READ_USER() about the proper types use. */
|
||||
#define bpf_core_read_user_str(dst, sz, src) \
|
||||
bpf_probe_read_user_str(dst, sz, (const void *)__builtin_preserve_access_index(src))
|
||||
|
||||
#define ___concat(a, b) a ## b
|
||||
#define ___apply(fn, n) ___concat(fn, n)
|
||||
#define ___nth(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, __11, N, ...) N
|
||||
|
||||
/*
|
||||
* return number of provided arguments; used for switch-based variadic macro
|
||||
* definitions (see ___last, ___arrow, etc below)
|
||||
*/
|
||||
#define ___narg(...) ___nth(_, ##__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
|
||||
/*
|
||||
* return 0 if no arguments are passed, N - otherwise; used for
|
||||
* recursively-defined macros to specify termination (0) case, and generic
|
||||
* (N) case (e.g., ___read_ptrs, ___core_read)
|
||||
*/
|
||||
#define ___empty(...) ___nth(_, ##__VA_ARGS__, N, N, N, N, N, N, N, N, N, N, 0)
|
||||
|
||||
#define ___last1(x) x
|
||||
#define ___last2(a, x) x
|
||||
#define ___last3(a, b, x) x
|
||||
#define ___last4(a, b, c, x) x
|
||||
#define ___last5(a, b, c, d, x) x
|
||||
#define ___last6(a, b, c, d, e, x) x
|
||||
#define ___last7(a, b, c, d, e, f, x) x
|
||||
#define ___last8(a, b, c, d, e, f, g, x) x
|
||||
#define ___last9(a, b, c, d, e, f, g, h, x) x
|
||||
#define ___last10(a, b, c, d, e, f, g, h, i, x) x
|
||||
#define ___last(...) ___apply(___last, ___narg(__VA_ARGS__))(__VA_ARGS__)
|
||||
|
||||
#define ___nolast2(a, _) a
|
||||
#define ___nolast3(a, b, _) a, b
|
||||
#define ___nolast4(a, b, c, _) a, b, c
|
||||
#define ___nolast5(a, b, c, d, _) a, b, c, d
|
||||
#define ___nolast6(a, b, c, d, e, _) a, b, c, d, e
|
||||
#define ___nolast7(a, b, c, d, e, f, _) a, b, c, d, e, f
|
||||
#define ___nolast8(a, b, c, d, e, f, g, _) a, b, c, d, e, f, g
|
||||
#define ___nolast9(a, b, c, d, e, f, g, h, _) a, b, c, d, e, f, g, h
|
||||
#define ___nolast10(a, b, c, d, e, f, g, h, i, _) a, b, c, d, e, f, g, h, i
|
||||
#define ___nolast(...) ___apply(___nolast, ___narg(__VA_ARGS__))(__VA_ARGS__)
|
||||
|
||||
#define ___arrow1(a) a
|
||||
#define ___arrow2(a, b) a->b
|
||||
#define ___arrow3(a, b, c) a->b->c
|
||||
#define ___arrow4(a, b, c, d) a->b->c->d
|
||||
#define ___arrow5(a, b, c, d, e) a->b->c->d->e
|
||||
#define ___arrow6(a, b, c, d, e, f) a->b->c->d->e->f
|
||||
#define ___arrow7(a, b, c, d, e, f, g) a->b->c->d->e->f->g
|
||||
#define ___arrow8(a, b, c, d, e, f, g, h) a->b->c->d->e->f->g->h
|
||||
#define ___arrow9(a, b, c, d, e, f, g, h, i) a->b->c->d->e->f->g->h->i
|
||||
#define ___arrow10(a, b, c, d, e, f, g, h, i, j) a->b->c->d->e->f->g->h->i->j
|
||||
#define ___arrow(...) ___apply(___arrow, ___narg(__VA_ARGS__))(__VA_ARGS__)
|
||||
|
||||
#define ___type(...) typeof(___arrow(__VA_ARGS__))
|
||||
|
||||
#define ___read(read_fn, dst, src_type, src, accessor) \
|
||||
read_fn((void *)(dst), sizeof(*(dst)), &((src_type)(src))->accessor)
|
||||
|
||||
/* "recursively" read a sequence of inner pointers using local __t var */
|
||||
#define ___rd_first(fn, src, a) ___read(fn, &__t, ___type(src), src, a);
|
||||
#define ___rd_last(fn, ...) \
|
||||
___read(fn, &__t, ___type(___nolast(__VA_ARGS__)), __t, ___last(__VA_ARGS__));
|
||||
#define ___rd_p1(fn, ...) const void *__t; ___rd_first(fn, __VA_ARGS__)
|
||||
#define ___rd_p2(fn, ...) ___rd_p1(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__)
|
||||
#define ___rd_p3(fn, ...) ___rd_p2(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__)
|
||||
#define ___rd_p4(fn, ...) ___rd_p3(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__)
|
||||
#define ___rd_p5(fn, ...) ___rd_p4(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__)
|
||||
#define ___rd_p6(fn, ...) ___rd_p5(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__)
|
||||
#define ___rd_p7(fn, ...) ___rd_p6(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__)
|
||||
#define ___rd_p8(fn, ...) ___rd_p7(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__)
|
||||
#define ___rd_p9(fn, ...) ___rd_p8(fn, ___nolast(__VA_ARGS__)) ___rd_last(fn, __VA_ARGS__)
|
||||
#define ___read_ptrs(fn, src, ...) \
|
||||
___apply(___rd_p, ___narg(__VA_ARGS__))(fn, src, __VA_ARGS__)
|
||||
|
||||
#define ___core_read0(fn, fn_ptr, dst, src, a) \
|
||||
___read(fn, dst, ___type(src), src, a);
|
||||
#define ___core_readN(fn, fn_ptr, dst, src, ...) \
|
||||
___read_ptrs(fn_ptr, src, ___nolast(__VA_ARGS__)) \
|
||||
___read(fn, dst, ___type(src, ___nolast(__VA_ARGS__)), __t, \
|
||||
___last(__VA_ARGS__));
|
||||
#define ___core_read(fn, fn_ptr, dst, src, a, ...) \
|
||||
___apply(___core_read, ___empty(__VA_ARGS__))(fn, fn_ptr, dst, \
|
||||
src, a, ##__VA_ARGS__)
|
||||
|
||||
/*
|
||||
* BPF_CORE_READ_INTO() is a more performance-conscious variant of
|
||||
* BPF_CORE_READ(), in which final field is read into user-provided storage.
|
||||
* See BPF_CORE_READ() below for more details on general usage.
|
||||
*/
|
||||
#define BPF_CORE_READ_INTO(dst, src, a, ...) ({ \
|
||||
___core_read(bpf_core_read, bpf_core_read, \
|
||||
dst, (src), a, ##__VA_ARGS__) \
|
||||
})
|
||||
|
||||
/*
|
||||
* Variant of BPF_CORE_READ_INTO() for reading from user-space memory.
|
||||
*
|
||||
* NOTE: see comments for BPF_CORE_READ_USER() about the proper types use.
|
||||
*/
|
||||
#define BPF_CORE_READ_USER_INTO(dst, src, a, ...) ({ \
|
||||
___core_read(bpf_core_read_user, bpf_core_read_user, \
|
||||
dst, (src), a, ##__VA_ARGS__) \
|
||||
})
|
||||
|
||||
/* Non-CO-RE variant of BPF_CORE_READ_INTO() */
|
||||
#define BPF_PROBE_READ_INTO(dst, src, a, ...) ({ \
|
||||
___core_read(bpf_probe_read, bpf_probe_read, \
|
||||
dst, (src), a, ##__VA_ARGS__) \
|
||||
})
|
||||
|
||||
/* Non-CO-RE variant of BPF_CORE_READ_USER_INTO().
|
||||
*
|
||||
* As no CO-RE relocations are emitted, source types can be arbitrary and are
|
||||
* not restricted to kernel types only.
|
||||
*/
|
||||
#define BPF_PROBE_READ_USER_INTO(dst, src, a, ...) ({ \
|
||||
___core_read(bpf_probe_read_user, bpf_probe_read_user, \
|
||||
dst, (src), a, ##__VA_ARGS__) \
|
||||
})
|
||||
|
||||
/*
|
||||
* BPF_CORE_READ_STR_INTO() does same "pointer chasing" as
|
||||
* BPF_CORE_READ() for intermediate pointers, but then executes (and returns
|
||||
* corresponding error code) bpf_core_read_str() for final string read.
|
||||
*/
|
||||
#define BPF_CORE_READ_STR_INTO(dst, src, a, ...) ({ \
|
||||
___core_read(bpf_core_read_str, bpf_core_read, \
|
||||
dst, (src), a, ##__VA_ARGS__) \
|
||||
})
|
||||
|
||||
/*
|
||||
* Variant of BPF_CORE_READ_STR_INTO() for reading from user-space memory.
|
||||
*
|
||||
* NOTE: see comments for BPF_CORE_READ_USER() about the proper types use.
|
||||
*/
|
||||
#define BPF_CORE_READ_USER_STR_INTO(dst, src, a, ...) ({ \
|
||||
___core_read(bpf_core_read_user_str, bpf_core_read_user, \
|
||||
dst, (src), a, ##__VA_ARGS__) \
|
||||
})
|
||||
|
||||
/* Non-CO-RE variant of BPF_CORE_READ_STR_INTO() */
|
||||
#define BPF_PROBE_READ_STR_INTO(dst, src, a, ...) ({ \
|
||||
___core_read(bpf_probe_read_str, bpf_probe_read, \
|
||||
dst, (src), a, ##__VA_ARGS__) \
|
||||
})
|
||||
|
||||
/*
|
||||
* Non-CO-RE variant of BPF_CORE_READ_USER_STR_INTO().
|
||||
*
|
||||
* As no CO-RE relocations are emitted, source types can be arbitrary and are
|
||||
* not restricted to kernel types only.
|
||||
*/
|
||||
#define BPF_PROBE_READ_USER_STR_INTO(dst, src, a, ...) ({ \
|
||||
___core_read(bpf_probe_read_user_str, bpf_probe_read_user, \
|
||||
dst, (src), a, ##__VA_ARGS__) \
|
||||
})
|
||||
|
||||
/*
|
||||
* BPF_CORE_READ() is used to simplify BPF CO-RE relocatable read, especially
|
||||
* when there are few pointer chasing steps.
|
||||
* E.g., what in non-BPF world (or in BPF w/ BCC) would be something like:
|
||||
* int x = s->a.b.c->d.e->f->g;
|
||||
* can be succinctly achieved using BPF_CORE_READ as:
|
||||
* int x = BPF_CORE_READ(s, a.b.c, d.e, f, g);
|
||||
*
|
||||
* BPF_CORE_READ will decompose above statement into 4 bpf_core_read (BPF
|
||||
* CO-RE relocatable bpf_probe_read_kernel() wrapper) calls, logically
|
||||
* equivalent to:
|
||||
* 1. const void *__t = s->a.b.c;
|
||||
* 2. __t = __t->d.e;
|
||||
* 3. __t = __t->f;
|
||||
* 4. return __t->g;
|
||||
*
|
||||
* Equivalence is logical, because there is a heavy type casting/preservation
|
||||
* involved, as well as all the reads are happening through
|
||||
* bpf_probe_read_kernel() calls using __builtin_preserve_access_index() to
|
||||
* emit CO-RE relocations.
|
||||
*
|
||||
* N.B. Only up to 9 "field accessors" are supported, which should be more
|
||||
* than enough for any practical purpose.
|
||||
*/
|
||||
#define BPF_CORE_READ(src, a, ...) ({ \
|
||||
___type((src), a, ##__VA_ARGS__) __r; \
|
||||
BPF_CORE_READ_INTO(&__r, (src), a, ##__VA_ARGS__); \
|
||||
__r; \
|
||||
})
|
||||
|
||||
/*
|
||||
* Variant of BPF_CORE_READ() for reading from user-space memory.
|
||||
*
|
||||
* NOTE: all the source types involved are still *kernel types* and need to
|
||||
* exist in kernel (or kernel module) BTF, otherwise CO-RE relocation will
|
||||
* fail. Custom user types are not relocatable with CO-RE.
|
||||
* The typical situation in which BPF_CORE_READ_USER() might be used is to
|
||||
* read kernel UAPI types from the user-space memory passed in as a syscall
|
||||
* input argument.
|
||||
*/
|
||||
#define BPF_CORE_READ_USER(src, a, ...) ({ \
|
||||
___type((src), a, ##__VA_ARGS__) __r; \
|
||||
BPF_CORE_READ_USER_INTO(&__r, (src), a, ##__VA_ARGS__); \
|
||||
__r; \
|
||||
})
|
||||
|
||||
/* Non-CO-RE variant of BPF_CORE_READ() */
|
||||
#define BPF_PROBE_READ(src, a, ...) ({ \
|
||||
___type((src), a, ##__VA_ARGS__) __r; \
|
||||
BPF_PROBE_READ_INTO(&__r, (src), a, ##__VA_ARGS__); \
|
||||
__r; \
|
||||
})
|
||||
|
||||
/*
|
||||
* Non-CO-RE variant of BPF_CORE_READ_USER().
|
||||
*
|
||||
* As no CO-RE relocations are emitted, source types can be arbitrary and are
|
||||
* not restricted to kernel types only.
|
||||
*/
|
||||
#define BPF_PROBE_READ_USER(src, a, ...) ({ \
|
||||
___type((src), a, ##__VA_ARGS__) __r; \
|
||||
BPF_PROBE_READ_USER_INTO(&__r, (src), a, ##__VA_ARGS__); \
|
||||
__r; \
|
||||
})
|
||||
|
||||
#endif
|
||||
|
||||
Binary file not shown.
+3
@@ -0,0 +1,3 @@
|
||||
go test fuzz v1
|
||||
[]byte("\x9f\xeb\x01\x00\x1c\x00\x00\x00\x00\x00\x00\x00000\x10\x00\x00\x00\x000000\xf3\xff\xff\xff0\x00\x00\x00")
|
||||
[]byte("0")
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
go test fuzz v1
|
||||
[]byte("\x9f\xeb\x01\x00\x18\x00\x00\x00\x00\x00\x00\x000000000000000\x00\x00\x000000")
|
||||
[]byte("0")
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
go test fuzz v1
|
||||
[]byte("\x9f\xeb\x01\x00\x1c\x00\x00\x00\x00\x00\x00\x000000\xe8\xff\xff\xff000000000\x00\x00\x00")
|
||||
[]byte("0")
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,268 @@
|
||||
#include "../../testdata/common.h"
|
||||
#include "bpf_core_read.h"
|
||||
|
||||
enum e {
|
||||
// clang-12 doesn't allow enum relocations with zero value.
|
||||
// See https://reviews.llvm.org/D97659
|
||||
ONE = 1,
|
||||
TWO,
|
||||
};
|
||||
|
||||
typedef enum e e_t;
|
||||
|
||||
struct s {
|
||||
int _1;
|
||||
char _2;
|
||||
unsigned int _3;
|
||||
};
|
||||
|
||||
typedef struct s s_t;
|
||||
|
||||
union u {
|
||||
int *_1;
|
||||
char *_2;
|
||||
unsigned int *_3;
|
||||
};
|
||||
|
||||
typedef union u u_t;
|
||||
|
||||
#define local_id_zero(expr) \
|
||||
({ \
|
||||
if (bpf_core_type_id_local(expr) != 0) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
#define local_id_not_zero(expr) \
|
||||
({ \
|
||||
if (bpf_core_type_id_local(expr) == 0) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
#define target_and_local_id_match(expr) \
|
||||
({ \
|
||||
if (bpf_core_type_id_kernel(expr) != bpf_core_type_id_local(expr)) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
__section("socket_filter/type_ids") int type_ids() {
|
||||
local_id_not_zero(int);
|
||||
local_id_not_zero(struct { int frob; });
|
||||
local_id_not_zero(enum {FRAP});
|
||||
local_id_not_zero(union { char bar; });
|
||||
|
||||
local_id_not_zero(struct s);
|
||||
local_id_not_zero(s_t);
|
||||
local_id_not_zero(const s_t);
|
||||
local_id_not_zero(volatile s_t);
|
||||
local_id_not_zero(enum e);
|
||||
local_id_not_zero(e_t);
|
||||
local_id_not_zero(const e_t);
|
||||
local_id_not_zero(volatile e_t);
|
||||
local_id_not_zero(union u);
|
||||
local_id_not_zero(u_t);
|
||||
local_id_not_zero(const u_t);
|
||||
local_id_not_zero(volatile u_t);
|
||||
|
||||
// Qualifiers on types crash clang.
|
||||
target_and_local_id_match(struct s);
|
||||
target_and_local_id_match(s_t);
|
||||
// target_and_local_id_match(const s_t);
|
||||
// target_and_local_id_match(volatile s_t);
|
||||
target_and_local_id_match(enum e);
|
||||
target_and_local_id_match(e_t);
|
||||
// target_and_local_id_match(const e_t);
|
||||
// target_and_local_id_match(volatile e_t);
|
||||
target_and_local_id_match(union u);
|
||||
target_and_local_id_match(u_t);
|
||||
// target_and_local_id_match(const u_t);
|
||||
// target_and_local_id_match(volatile u_t);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define type_exists(expr) \
|
||||
({ \
|
||||
if (!bpf_core_type_exists(expr)) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
#define type_size_matches(expr) \
|
||||
({ \
|
||||
if (bpf_core_type_size(expr) != sizeof(expr)) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
__section("socket_filter/types") int types() {
|
||||
type_exists(struct s);
|
||||
type_exists(s_t);
|
||||
type_exists(const s_t);
|
||||
type_exists(volatile s_t);
|
||||
type_exists(enum e);
|
||||
type_exists(e_t);
|
||||
type_exists(const e_t);
|
||||
type_exists(volatile e_t);
|
||||
type_exists(union u);
|
||||
type_exists(u_t);
|
||||
type_exists(const u_t);
|
||||
type_exists(volatile u_t);
|
||||
// TODO: Check non-existence.
|
||||
|
||||
type_size_matches(struct s);
|
||||
type_size_matches(s_t);
|
||||
type_size_matches(const s_t);
|
||||
type_size_matches(volatile s_t);
|
||||
type_size_matches(enum e);
|
||||
type_size_matches(e_t);
|
||||
type_size_matches(const e_t);
|
||||
type_size_matches(volatile e_t);
|
||||
type_size_matches(union u);
|
||||
type_size_matches(u_t);
|
||||
type_size_matches(const u_t);
|
||||
type_size_matches(volatile u_t);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define enum_value_exists(t, v) \
|
||||
({ \
|
||||
if (!bpf_core_enum_value_exists(t, v)) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
#define enum_value_matches(t, v) \
|
||||
({ \
|
||||
if (v != bpf_core_enum_value(t, v)) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
__section("socket_filter/enums") int enums() {
|
||||
enum_value_exists(enum e, ONE);
|
||||
enum_value_exists(volatile enum e, ONE);
|
||||
enum_value_exists(const enum e, ONE);
|
||||
enum_value_exists(e_t, TWO);
|
||||
// TODO: Check non-existence.
|
||||
|
||||
enum_value_matches(enum e, TWO);
|
||||
enum_value_matches(e_t, ONE);
|
||||
enum_value_matches(volatile e_t, ONE);
|
||||
enum_value_matches(const e_t, ONE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define field_exists(f) \
|
||||
({ \
|
||||
if (!bpf_core_field_exists(f)) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
#define field_size_matches(f) \
|
||||
({ \
|
||||
if (sizeof(f) != bpf_core_field_size(f)) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
#define field_offset_matches(t, f) \
|
||||
({ \
|
||||
if (__builtin_offsetof(t, f) != __builtin_preserve_field_info(((typeof(t) *)0)->f, BPF_FIELD_BYTE_OFFSET)) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
#define field_is_signed(f) \
|
||||
({ \
|
||||
if (!__builtin_preserve_field_info(f, BPF_FIELD_SIGNED)) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
#define field_is_unsigned(f) \
|
||||
({ \
|
||||
if (__builtin_preserve_field_info(f, BPF_FIELD_SIGNED)) { \
|
||||
return __LINE__; \
|
||||
} \
|
||||
})
|
||||
|
||||
__section("socket_filter/fields") int fields() {
|
||||
field_exists((struct s){}._1);
|
||||
field_exists((s_t){}._2);
|
||||
field_exists((union u){}._1);
|
||||
field_exists((u_t){}._2);
|
||||
|
||||
field_is_signed((struct s){}._1);
|
||||
field_is_unsigned((struct s){}._3);
|
||||
// unions crash clang-14.
|
||||
// field_is_signed((union u){}._1);
|
||||
// field_is_unsigned((union u){}._3);
|
||||
|
||||
field_size_matches((struct s){}._1);
|
||||
field_size_matches((s_t){}._2);
|
||||
field_size_matches((union u){}._1);
|
||||
field_size_matches((u_t){}._2);
|
||||
|
||||
field_offset_matches(struct s, _1);
|
||||
field_offset_matches(s_t, _2);
|
||||
field_offset_matches(union u, _1);
|
||||
field_offset_matches(u_t, _2);
|
||||
|
||||
struct t {
|
||||
union {
|
||||
s_t s[10];
|
||||
};
|
||||
struct {
|
||||
union u u;
|
||||
};
|
||||
} bar, *barp = &bar;
|
||||
|
||||
field_exists(bar.s[2]._1);
|
||||
field_exists(bar.s[1]._2);
|
||||
field_exists(bar.u._1);
|
||||
field_exists(bar.u._2);
|
||||
field_exists(barp[1].u._2);
|
||||
|
||||
field_is_signed(bar.s[2]._1);
|
||||
field_is_unsigned(bar.s[2]._3);
|
||||
// unions crash clang-14.
|
||||
// field_is_signed(bar.u._1);
|
||||
// field_is_signed(bar.u._3);
|
||||
|
||||
field_size_matches(bar.s[2]._1);
|
||||
field_size_matches(bar.s[1]._2);
|
||||
field_size_matches(bar.u._1);
|
||||
field_size_matches(bar.u._2);
|
||||
field_size_matches(barp[1].u._2);
|
||||
|
||||
field_offset_matches(struct t, s[2]._1);
|
||||
field_offset_matches(struct t, s[1]._2);
|
||||
field_offset_matches(struct t, u._1);
|
||||
field_offset_matches(struct t, u._2);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct ambiguous {
|
||||
int _1;
|
||||
char _2;
|
||||
};
|
||||
|
||||
struct ambiguous___flavour {
|
||||
char _1;
|
||||
int _2;
|
||||
};
|
||||
|
||||
__section("socket_filter/err_ambiguous") int err_ambiguous() {
|
||||
return bpf_core_type_id_kernel(struct ambiguous);
|
||||
}
|
||||
|
||||
__section("socket_filter/err_ambiguous_flavour") int err_ambiguous_flavour() {
|
||||
return bpf_core_type_id_kernel(struct ambiguous___flavour);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,116 @@
|
||||
#include "../../testdata/common.h"
|
||||
#include "bpf_core_read.h"
|
||||
|
||||
#define core_access __builtin_preserve_access_index
|
||||
|
||||
// Struct with the members declared in the wrong order. Accesses need
|
||||
// a successful CO-RE relocation against the type in relocs_read_tgt.c
|
||||
// for the test below to pass.
|
||||
struct s {
|
||||
char b;
|
||||
char a;
|
||||
};
|
||||
|
||||
typedef unsigned char u8;
|
||||
typedef unsigned short u16;
|
||||
typedef unsigned int u32;
|
||||
typedef unsigned long u64;
|
||||
|
||||
// Struct with bitfields.
|
||||
struct bits {
|
||||
int x;
|
||||
u8 a : 4, b : 2;
|
||||
u16 c : 1;
|
||||
unsigned int d : 2;
|
||||
enum { ZERO = 0, ONE = 1 } e : 1;
|
||||
u64 f : 16, g : 30;
|
||||
};
|
||||
|
||||
struct nonexist {
|
||||
int non_exist;
|
||||
};
|
||||
|
||||
enum nonexist_enum { NON_EXIST = 1 };
|
||||
|
||||
// Perform a read from a subprog to ensure CO-RE relocations
|
||||
// occurring there are tracked and executed in the final linked program.
|
||||
__attribute__((noinline)) int read_subprog() {
|
||||
struct s foo = {
|
||||
.a = 0,
|
||||
.b = 1,
|
||||
};
|
||||
|
||||
if (core_access(foo.a) == 0)
|
||||
return __LINE__;
|
||||
|
||||
if (core_access(foo.b) == 1)
|
||||
return __LINE__;
|
||||
|
||||
struct bits bar;
|
||||
char *p = (char *)&bar;
|
||||
/* Target:
|
||||
* [4] STRUCT 'bits' size=8 vlen=7
|
||||
* 'b' type_id=5 bits_offset=0 bitfield_size=2
|
||||
* 'a' type_id=5 bits_offset=2 bitfield_size=4
|
||||
* 'd' type_id=7 bits_offset=6 bitfield_size=2
|
||||
* 'c' type_id=9 bits_offset=8 bitfield_size=1
|
||||
* 'e' type_id=11 bits_offset=9 bitfield_size=1
|
||||
* 'f' type_id=9 bits_offset=16
|
||||
* 'g' type_id=12 bits_offset=32 bitfield_size=30
|
||||
*/
|
||||
*p++ = 0xff; // a, b, d
|
||||
*p++ = 0x00; // c, e
|
||||
*p++ = 0x56; // f
|
||||
*p++ = 0x56; // f
|
||||
#ifdef __BIG_ENDIAN__
|
||||
*p++ = 0x55; // g
|
||||
*p++ = 0x44; // g
|
||||
*p++ = 0x33; // g
|
||||
*p++ = 0x22; // g
|
||||
#else
|
||||
*p++ = 0x22; // g
|
||||
*p++ = 0x33; // g
|
||||
*p++ = 0x44; // g
|
||||
*p++ = 0x55; // g
|
||||
#endif
|
||||
|
||||
if (BPF_CORE_READ_BITFIELD(&bar, a) != (1 << 4) - 1)
|
||||
return __LINE__;
|
||||
|
||||
if (BPF_CORE_READ_BITFIELD(&bar, b) != (1 << 2) - 1)
|
||||
return __LINE__;
|
||||
|
||||
if (BPF_CORE_READ_BITFIELD(&bar, d) != (1 << 2) - 1)
|
||||
return __LINE__;
|
||||
|
||||
if (BPF_CORE_READ_BITFIELD(&bar, c) != 0)
|
||||
return __LINE__;
|
||||
|
||||
if (BPF_CORE_READ_BITFIELD(&bar, e) != 0)
|
||||
return __LINE__;
|
||||
|
||||
if (BPF_CORE_READ_BITFIELD(&bar, f) != 0x5656)
|
||||
return __LINE__;
|
||||
|
||||
if (BPF_CORE_READ_BITFIELD(&bar, g) != 0x15443322)
|
||||
return __LINE__;
|
||||
|
||||
if (bpf_core_type_exists(struct nonexist) != 0)
|
||||
return __LINE__;
|
||||
|
||||
if (bpf_core_field_exists(((struct nonexist *)0)->non_exist) != 0)
|
||||
return __LINE__;
|
||||
|
||||
if (bpf_core_enum_value_exists(enum nonexist_enum, NON_EXIST) != 0)
|
||||
return __LINE__;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
__section("socket") int reads() {
|
||||
int ret = read_subprog();
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
return 0;
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
This file exists to emit ELFs with specific BTF types to use as target BTF
|
||||
in tests. It can be made redundant when btf.Spec can be handcrafted and
|
||||
passed as a CO-RE target in the future.
|
||||
*/
|
||||
|
||||
struct s {
|
||||
char a;
|
||||
char b;
|
||||
};
|
||||
|
||||
struct s *unused_s __attribute__((unused));
|
||||
|
||||
typedef unsigned int my_u32;
|
||||
typedef unsigned char u8;
|
||||
typedef unsigned short u16;
|
||||
typedef unsigned int u32;
|
||||
typedef unsigned long u64;
|
||||
|
||||
struct bits {
|
||||
/*int x;*/
|
||||
u8 b : 2, a : 4; /* a was before b */
|
||||
my_u32 d : 2; /* was 'unsigned int' */
|
||||
u16 c : 1; /* was before d */
|
||||
enum { ZERO = 0, ONE = 1 } e : 1;
|
||||
u16 f; /* was: u64 f:16 */
|
||||
u32 g : 30; /* was: u64 g:30 */
|
||||
};
|
||||
|
||||
struct bits *unused_bits __attribute__((unused));
|
||||
Binary file not shown.
@@ -0,0 +1,141 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
)
|
||||
|
||||
// Functions to traverse a cyclic graph of types. The below was very useful:
|
||||
// https://eli.thegreenplace.net/2015/directed-graph-traversal-orderings-and-applications-to-data-flow-analysis/#post-order-and-reverse-post-order
|
||||
|
||||
type postorderIterator struct {
|
||||
// Iteration skips types for which this function returns true.
|
||||
skip func(Type) bool
|
||||
// The root type. May be nil if skip(root) is true.
|
||||
root Type
|
||||
|
||||
// Contains types which need to be either walked or yielded.
|
||||
types typeDeque
|
||||
// Contains a boolean whether the type has been walked or not.
|
||||
walked internal.Deque[bool]
|
||||
// The set of types which has been pushed onto types.
|
||||
pushed map[Type]struct{}
|
||||
|
||||
// The current type. Only valid after a call to Next().
|
||||
Type Type
|
||||
}
|
||||
|
||||
// postorderTraversal iterates all types reachable from root by visiting the
|
||||
// leaves of the graph first.
|
||||
//
|
||||
// Types for which skip returns true are ignored. skip may be nil.
|
||||
func postorderTraversal(root Type, skip func(Type) (skip bool)) postorderIterator {
|
||||
// Avoid allocations for the common case of a skipped root.
|
||||
if skip != nil && skip(root) {
|
||||
return postorderIterator{}
|
||||
}
|
||||
|
||||
po := postorderIterator{root: root, skip: skip}
|
||||
walkType(root, po.push)
|
||||
|
||||
return po
|
||||
}
|
||||
|
||||
func (po *postorderIterator) push(t *Type) {
|
||||
if _, ok := po.pushed[*t]; ok || *t == po.root {
|
||||
return
|
||||
}
|
||||
|
||||
if po.skip != nil && po.skip(*t) {
|
||||
return
|
||||
}
|
||||
|
||||
if po.pushed == nil {
|
||||
// Lazily allocate pushed to avoid an allocation for Types without children.
|
||||
po.pushed = make(map[Type]struct{})
|
||||
}
|
||||
|
||||
po.pushed[*t] = struct{}{}
|
||||
po.types.Push(t)
|
||||
po.walked.Push(false)
|
||||
}
|
||||
|
||||
// Next returns true if there is another Type to traverse.
|
||||
func (po *postorderIterator) Next() bool {
|
||||
for !po.types.Empty() {
|
||||
t := po.types.Pop()
|
||||
|
||||
if !po.walked.Pop() {
|
||||
// Push the type again, so that we re-evaluate it in done state
|
||||
// after all children have been handled.
|
||||
po.types.Push(t)
|
||||
po.walked.Push(true)
|
||||
|
||||
// Add all direct children to todo.
|
||||
walkType(*t, po.push)
|
||||
} else {
|
||||
// We've walked this type previously, so we now know that all
|
||||
// children have been handled.
|
||||
po.Type = *t
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Only return root once.
|
||||
po.Type, po.root = po.root, nil
|
||||
return po.Type != nil
|
||||
}
|
||||
|
||||
// walkType calls fn on each child of typ.
|
||||
func walkType(typ Type, fn func(*Type)) {
|
||||
// Explicitly type switch on the most common types to allow the inliner to
|
||||
// do its work. This avoids allocating intermediate slices from walk() on
|
||||
// the heap.
|
||||
switch v := typ.(type) {
|
||||
case *Void, *Int, *Enum, *Fwd, *Float:
|
||||
// No children to traverse.
|
||||
case *Pointer:
|
||||
fn(&v.Target)
|
||||
case *Array:
|
||||
fn(&v.Index)
|
||||
fn(&v.Type)
|
||||
case *Struct:
|
||||
for i := range v.Members {
|
||||
fn(&v.Members[i].Type)
|
||||
}
|
||||
case *Union:
|
||||
for i := range v.Members {
|
||||
fn(&v.Members[i].Type)
|
||||
}
|
||||
case *Typedef:
|
||||
fn(&v.Type)
|
||||
case *Volatile:
|
||||
fn(&v.Type)
|
||||
case *Const:
|
||||
fn(&v.Type)
|
||||
case *Restrict:
|
||||
fn(&v.Type)
|
||||
case *Func:
|
||||
fn(&v.Type)
|
||||
case *FuncProto:
|
||||
fn(&v.Return)
|
||||
for i := range v.Params {
|
||||
fn(&v.Params[i].Type)
|
||||
}
|
||||
case *Var:
|
||||
fn(&v.Type)
|
||||
case *Datasec:
|
||||
for i := range v.Vars {
|
||||
fn(&v.Vars[i].Type)
|
||||
}
|
||||
case *declTag:
|
||||
fn(&v.Type)
|
||||
case *typeTag:
|
||||
fn(&v.Type)
|
||||
case *cycle:
|
||||
// cycle has children, but we ignore them deliberately.
|
||||
default:
|
||||
panic(fmt.Sprintf("don't know how to walk Type %T", v))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestPostorderTraversal(t *testing.T) {
|
||||
ptr := newCyclicalType(2).(*Pointer)
|
||||
cst := ptr.Target.(*Const)
|
||||
str := cst.Type.(*Struct)
|
||||
|
||||
t.Logf("%3v", ptr)
|
||||
pending := []Type{str, cst, ptr}
|
||||
iter := postorderTraversal(ptr, nil)
|
||||
for iter.Next() {
|
||||
qt.Assert(t, iter.Type, qt.Equals, pending[0])
|
||||
pending = pending[1:]
|
||||
}
|
||||
qt.Assert(t, pending, qt.HasLen, 0)
|
||||
|
||||
i := &Int{Name: "foo"}
|
||||
// i appears twice at the same nesting depth.
|
||||
arr := &Array{Index: i, Type: i}
|
||||
seen := make(map[Type]bool)
|
||||
iter = postorderTraversal(arr, nil)
|
||||
for iter.Next() {
|
||||
qt.Assert(t, seen[iter.Type], qt.IsFalse)
|
||||
seen[iter.Type] = true
|
||||
}
|
||||
qt.Assert(t, seen[arr], qt.IsTrue)
|
||||
qt.Assert(t, seen[i], qt.IsTrue)
|
||||
}
|
||||
|
||||
func TestPostorderTraversalVmlinux(t *testing.T) {
|
||||
spec := vmlinuxTestdataSpec(t)
|
||||
|
||||
typ, err := spec.AnyTypeByName("gov_update_cpu_data")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, typ := range []Type{typ} {
|
||||
t.Run(fmt.Sprintf("%s", typ), func(t *testing.T) {
|
||||
seen := make(map[Type]bool)
|
||||
var last Type
|
||||
iter := postorderTraversal(typ, nil)
|
||||
for iter.Next() {
|
||||
if seen[iter.Type] {
|
||||
t.Fatalf("%s visited twice", iter.Type)
|
||||
}
|
||||
seen[iter.Type] = true
|
||||
last = iter.Type
|
||||
}
|
||||
if last != typ {
|
||||
t.Fatalf("Expected %s got %s as last type", typ, last)
|
||||
}
|
||||
|
||||
walkType(typ, func(child *Type) {
|
||||
qt.Check(t, seen[*child], qt.IsTrue, qt.Commentf("missing child %s", *child))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPostorderTraversal(b *testing.B) {
|
||||
spec := vmlinuxTestdataSpec(b)
|
||||
|
||||
var fn *Func
|
||||
err := spec.TypeByName("gov_update_cpu_data", &fn)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
typ Type
|
||||
}{
|
||||
{"single type", &Int{}},
|
||||
{"cycle(1)", newCyclicalType(1)},
|
||||
{"cycle(10)", newCyclicalType(10)},
|
||||
{"gov_update_cpu_data", fn},
|
||||
} {
|
||||
b.Logf("%10v", test.typ)
|
||||
|
||||
b.Run(test.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
iter := postorderTraversal(test.typ, nil)
|
||||
for iter.Next() {
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,512 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestSizeof(t *testing.T) {
|
||||
testcases := []struct {
|
||||
size int
|
||||
typ Type
|
||||
}{
|
||||
{0, (*Void)(nil)},
|
||||
{1, &Int{Size: 1}},
|
||||
{8, &Enum{Size: 8}},
|
||||
{0, &Array{Type: &Pointer{Target: (*Void)(nil)}, Nelems: 0}},
|
||||
{12, &Array{Type: &Enum{Size: 4}, Nelems: 3}},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
name := fmt.Sprint(tc.typ)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
have, err := Sizeof(tc.typ)
|
||||
if err != nil {
|
||||
t.Fatal("Can't calculate size:", err)
|
||||
}
|
||||
if have != tc.size {
|
||||
t.Errorf("Expected size %d, got %d", tc.size, have)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPow(t *testing.T) {
|
||||
tests := []struct {
|
||||
n int
|
||||
r bool
|
||||
}{
|
||||
{0, false},
|
||||
{1, true},
|
||||
{2, true},
|
||||
{3, false},
|
||||
{4, true},
|
||||
{5, false},
|
||||
{8, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%d", tt.n), func(t *testing.T) {
|
||||
if want, got := tt.r, pow(tt.n); want != got {
|
||||
t.Errorf("unexpected result for n %d; want: %v, got: %v", tt.n, want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
_ = Copy((*Void)(nil), nil)
|
||||
|
||||
in := &Int{Size: 4}
|
||||
out := Copy(in, nil)
|
||||
|
||||
in.Size = 8
|
||||
if size := out.(*Int).Size; size != 4 {
|
||||
t.Error("Copy doesn't make a copy, expected size 4, got", size)
|
||||
}
|
||||
|
||||
t.Run("cyclical", func(t *testing.T) {
|
||||
_ = Copy(newCyclicalType(2), nil)
|
||||
})
|
||||
|
||||
t.Run("identity", func(t *testing.T) {
|
||||
u16 := &Int{Size: 2}
|
||||
|
||||
out := Copy(&Struct{
|
||||
Members: []Member{
|
||||
{Name: "a", Type: u16},
|
||||
{Name: "b", Type: u16},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
outStruct := out.(*Struct)
|
||||
qt.Assert(t, outStruct.Members[0].Type, qt.Equals, outStruct.Members[1].Type)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAs(t *testing.T) {
|
||||
i := &Int{}
|
||||
ptr := &Pointer{i}
|
||||
td := &Typedef{Type: ptr}
|
||||
cst := &Const{td}
|
||||
vol := &Volatile{cst}
|
||||
|
||||
// It's possible to retrieve qualifiers and Typedefs.
|
||||
haveVol, ok := as[*Volatile](vol)
|
||||
qt.Assert(t, ok, qt.IsTrue)
|
||||
qt.Assert(t, haveVol, qt.Equals, vol)
|
||||
|
||||
haveTd, ok := as[*Typedef](vol)
|
||||
qt.Assert(t, ok, qt.IsTrue)
|
||||
qt.Assert(t, haveTd, qt.Equals, td)
|
||||
|
||||
haveCst, ok := as[*Const](vol)
|
||||
qt.Assert(t, ok, qt.IsTrue)
|
||||
qt.Assert(t, haveCst, qt.Equals, cst)
|
||||
|
||||
// Make sure we don't skip Pointer.
|
||||
haveI, ok := as[*Int](vol)
|
||||
qt.Assert(t, ok, qt.IsFalse)
|
||||
qt.Assert(t, haveI, qt.IsNil)
|
||||
|
||||
// Make sure we can always retrieve Pointer.
|
||||
for _, typ := range []Type{
|
||||
td, cst, vol, ptr,
|
||||
} {
|
||||
have, ok := as[*Pointer](typ)
|
||||
qt.Assert(t, ok, qt.IsTrue)
|
||||
qt.Assert(t, have, qt.Equals, ptr)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCopy(b *testing.B) {
|
||||
typ := newCyclicalType(10)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Copy(typ, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// The following are valid Types.
|
||||
//
|
||||
// There currently is no better way to document which
|
||||
// types implement an interface.
|
||||
func ExampleType_validTypes() {
|
||||
var _ Type = &Void{}
|
||||
var _ Type = &Int{}
|
||||
var _ Type = &Pointer{}
|
||||
var _ Type = &Array{}
|
||||
var _ Type = &Struct{}
|
||||
var _ Type = &Union{}
|
||||
var _ Type = &Enum{}
|
||||
var _ Type = &Fwd{}
|
||||
var _ Type = &Typedef{}
|
||||
var _ Type = &Volatile{}
|
||||
var _ Type = &Const{}
|
||||
var _ Type = &Restrict{}
|
||||
var _ Type = &Func{}
|
||||
var _ Type = &FuncProto{}
|
||||
var _ Type = &Var{}
|
||||
var _ Type = &Datasec{}
|
||||
var _ Type = &Float{}
|
||||
}
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
types := []func() Type{
|
||||
func() Type { return &Void{} },
|
||||
func() Type { return &Int{Size: 2} },
|
||||
func() Type { return &Pointer{Target: &Void{}} },
|
||||
func() Type { return &Array{Type: &Int{}} },
|
||||
func() Type {
|
||||
return &Struct{
|
||||
Members: []Member{{Type: &Void{}}},
|
||||
}
|
||||
},
|
||||
func() Type {
|
||||
return &Union{
|
||||
Members: []Member{{Type: &Void{}}},
|
||||
}
|
||||
},
|
||||
func() Type { return &Enum{} },
|
||||
func() Type { return &Fwd{Name: "thunk"} },
|
||||
func() Type { return &Typedef{Type: &Void{}} },
|
||||
func() Type { return &Volatile{Type: &Void{}} },
|
||||
func() Type { return &Const{Type: &Void{}} },
|
||||
func() Type { return &Restrict{Type: &Void{}} },
|
||||
func() Type { return &Func{Name: "foo", Type: &Void{}} },
|
||||
func() Type {
|
||||
return &FuncProto{
|
||||
Params: []FuncParam{{Name: "bar", Type: &Void{}}},
|
||||
Return: &Void{},
|
||||
}
|
||||
},
|
||||
func() Type { return &Var{Type: &Void{}} },
|
||||
func() Type {
|
||||
return &Datasec{
|
||||
Vars: []VarSecinfo{{Type: &Void{}}},
|
||||
}
|
||||
},
|
||||
func() Type { return &Float{} },
|
||||
func() Type { return &declTag{Type: &Void{}} },
|
||||
func() Type { return &typeTag{Type: &Void{}} },
|
||||
func() Type { return &cycle{&Void{}} },
|
||||
}
|
||||
|
||||
compareTypes := cmp.Comparer(func(a, b *Type) bool {
|
||||
return a == b
|
||||
})
|
||||
|
||||
for _, fn := range types {
|
||||
typ := fn()
|
||||
t.Run(fmt.Sprintf("%T", typ), func(t *testing.T) {
|
||||
t.Logf("%v", typ)
|
||||
|
||||
if typ == typ.copy() {
|
||||
t.Error("Copy doesn't copy")
|
||||
}
|
||||
|
||||
var a []*Type
|
||||
walkType(typ, func(t *Type) { a = append(a, t) })
|
||||
|
||||
if _, ok := typ.(*cycle); !ok {
|
||||
if n := countChildren(t, reflect.TypeOf(typ)); len(a) < n {
|
||||
t.Errorf("walkType visited %d children, expected at least %d", len(a), n)
|
||||
}
|
||||
}
|
||||
|
||||
var b []*Type
|
||||
walkType(typ, func(t *Type) { b = append(b, t) })
|
||||
|
||||
if diff := cmp.Diff(a, b, compareTypes); diff != "" {
|
||||
t.Errorf("Walk mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagMarshaling(t *testing.T) {
|
||||
for _, typ := range []Type{
|
||||
&declTag{&Struct{Members: []Member{}}, "foo", -1},
|
||||
&typeTag{&Int{}, "foo"},
|
||||
} {
|
||||
t.Run(fmt.Sprint(typ), func(t *testing.T) {
|
||||
buf := marshalNativeEndian(t, []Type{typ})
|
||||
|
||||
s, err := loadRawSpec(bytes.NewReader(buf), internal.NativeEndian, nil)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
|
||||
have, err := s.TypeByID(1)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
|
||||
qt.Assert(t, have, qt.DeepEquals, typ)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func countChildren(t *testing.T, typ reflect.Type) int {
|
||||
if typ.Kind() != reflect.Pointer {
|
||||
t.Fatal("Expected pointer, got", typ.Kind())
|
||||
}
|
||||
|
||||
typ = typ.Elem()
|
||||
if typ.Kind() != reflect.Struct {
|
||||
t.Fatal("Expected struct, got", typ.Kind())
|
||||
}
|
||||
|
||||
var n int
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
if typ.Field(i).Type == reflect.TypeOf((*Type)(nil)).Elem() {
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
type testFormattableType struct {
|
||||
name string
|
||||
extra []interface{}
|
||||
}
|
||||
|
||||
var _ formattableType = (*testFormattableType)(nil)
|
||||
|
||||
func (tft *testFormattableType) TypeName() string { return tft.name }
|
||||
func (tft *testFormattableType) Format(fs fmt.State, verb rune) {
|
||||
formatType(fs, verb, tft, tft.extra...)
|
||||
}
|
||||
|
||||
func TestFormatType(t *testing.T) {
|
||||
t1 := &testFormattableType{"", []interface{}{"extra"}}
|
||||
t1Addr := fmt.Sprintf("%#p", t1)
|
||||
goType := reflect.TypeOf(t1).Elem().Name()
|
||||
|
||||
t2 := &testFormattableType{"foo", []interface{}{t1}}
|
||||
|
||||
t3 := &testFormattableType{extra: []interface{}{""}}
|
||||
|
||||
tests := []struct {
|
||||
t formattableType
|
||||
fmt string
|
||||
contains []string
|
||||
omits []string
|
||||
}{
|
||||
// %s doesn't contain address or extra.
|
||||
{t1, "%s", []string{goType}, []string{t1Addr, "extra"}},
|
||||
// %+s doesn't contain extra.
|
||||
{t1, "%+s", []string{goType, t1Addr}, []string{"extra"}},
|
||||
// %v does contain extra.
|
||||
{t1, "%v", []string{goType, "extra"}, []string{t1Addr}},
|
||||
// %+v does contain address.
|
||||
{t1, "%+v", []string{goType, "extra", t1Addr}, nil},
|
||||
// %v doesn't print nested types' extra.
|
||||
{t2, "%v", []string{goType, t2.name}, []string{"extra"}},
|
||||
// %1v does print nested types' extra.
|
||||
{t2, "%1v", []string{goType, t2.name, "extra"}, nil},
|
||||
// empty strings in extra don't emit anything.
|
||||
{t3, "%v", []string{"[]"}, nil},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.fmt, func(t *testing.T) {
|
||||
str := fmt.Sprintf(test.fmt, test.t)
|
||||
t.Log(str)
|
||||
|
||||
for _, want := range test.contains {
|
||||
qt.Assert(t, str, qt.Contains, want)
|
||||
}
|
||||
|
||||
for _, notWant := range test.omits {
|
||||
qt.Assert(t, str, qt.Not(qt.Contains), notWant)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newCyclicalType(n int) Type {
|
||||
ptr := &Pointer{}
|
||||
prev := Type(ptr)
|
||||
for i := 0; i < n; i++ {
|
||||
switch i % 5 {
|
||||
case 0:
|
||||
prev = &Struct{
|
||||
Members: []Member{
|
||||
{Type: prev},
|
||||
},
|
||||
}
|
||||
|
||||
case 1:
|
||||
prev = &Const{Type: prev}
|
||||
case 2:
|
||||
prev = &Volatile{Type: prev}
|
||||
case 3:
|
||||
prev = &Typedef{Type: prev}
|
||||
case 4:
|
||||
prev = &Array{Type: prev, Index: &Int{Size: 1}}
|
||||
}
|
||||
}
|
||||
ptr.Target = prev
|
||||
return ptr
|
||||
}
|
||||
|
||||
func TestUnderlyingType(t *testing.T) {
|
||||
wrappers := []struct {
|
||||
name string
|
||||
fn func(Type) Type
|
||||
}{
|
||||
{"const", func(t Type) Type { return &Const{Type: t} }},
|
||||
{"volatile", func(t Type) Type { return &Volatile{Type: t} }},
|
||||
{"restrict", func(t Type) Type { return &Restrict{Type: t} }},
|
||||
{"typedef", func(t Type) Type { return &Typedef{Type: t} }},
|
||||
{"type tag", func(t Type) Type { return &typeTag{Type: t} }},
|
||||
}
|
||||
|
||||
for _, test := range wrappers {
|
||||
t.Run(test.name+" cycle", func(t *testing.T) {
|
||||
root := &Volatile{}
|
||||
root.Type = test.fn(root)
|
||||
|
||||
got, ok := UnderlyingType(root).(*cycle)
|
||||
qt.Assert(t, ok, qt.IsTrue)
|
||||
qt.Assert(t, got.root, qt.Equals, root)
|
||||
})
|
||||
}
|
||||
|
||||
for _, test := range wrappers {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
want := &Int{}
|
||||
got := UnderlyingType(test.fn(want))
|
||||
qt.Assert(t, got, qt.Equals, want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInflateLegacyBitfield(t *testing.T) {
|
||||
const offset = 3
|
||||
const size = 5
|
||||
|
||||
var rawInt rawType
|
||||
rawInt.SetKind(kindInt)
|
||||
rawInt.SetSize(4)
|
||||
var data btfInt
|
||||
data.SetOffset(offset)
|
||||
data.SetBits(size)
|
||||
rawInt.data = &data
|
||||
|
||||
var beforeInt rawType
|
||||
beforeInt.SetKind(kindStruct)
|
||||
beforeInt.SetVlen(1)
|
||||
beforeInt.data = []btfMember{{Type: 2}}
|
||||
|
||||
afterInt := beforeInt
|
||||
afterInt.data = []btfMember{{Type: 1}}
|
||||
|
||||
emptyStrings := newStringTable("")
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
raw []rawType
|
||||
}{
|
||||
{"struct before int", []rawType{beforeInt, rawInt}},
|
||||
{"struct after int", []rawType{rawInt, afterInt}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
types, err := inflateRawTypes(test.raw, emptyStrings, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, typ := range types {
|
||||
s, ok := typ.(*Struct)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
i := s.Members[0]
|
||||
if i.BitfieldSize != size {
|
||||
t.Errorf("Expected bitfield size %d, got %d", size, i.BitfieldSize)
|
||||
}
|
||||
|
||||
if i.Offset != offset {
|
||||
t.Errorf("Expected offset %d, got %d", offset, i.Offset)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Fatal("No Struct returned from inflateRawTypes")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWalk(b *testing.B) {
|
||||
types := []Type{
|
||||
&Void{},
|
||||
&Int{},
|
||||
&Pointer{},
|
||||
&Array{},
|
||||
&Struct{Members: make([]Member, 2)},
|
||||
&Union{Members: make([]Member, 2)},
|
||||
&Enum{},
|
||||
&Fwd{},
|
||||
&Typedef{},
|
||||
&Volatile{},
|
||||
&Const{},
|
||||
&Restrict{},
|
||||
&Func{},
|
||||
&FuncProto{Params: make([]FuncParam, 2)},
|
||||
&Var{},
|
||||
&Datasec{Vars: make([]VarSecinfo, 2)},
|
||||
}
|
||||
|
||||
for _, typ := range types {
|
||||
b.Run(fmt.Sprint(typ), func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var dq typeDeque
|
||||
walkType(typ, dq.Push)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnderlyingType(b *testing.B) {
|
||||
b.Run("no unwrapping", func(b *testing.B) {
|
||||
v := &Int{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
UnderlyingType(v)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("single unwrapping", func(b *testing.B) {
|
||||
v := &Typedef{Type: &Int{}}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
UnderlyingType(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Copy can be used with UnderlyingType to strip qualifiers from a type graph.
|
||||
func ExampleCopy_stripQualifiers() {
|
||||
a := &Volatile{Type: &Pointer{Target: &Typedef{Name: "foo", Type: &Int{Size: 2}}}}
|
||||
b := Copy(a, UnderlyingType)
|
||||
// b has Volatile and Typedef removed.
|
||||
fmt.Printf("%3v\n", b)
|
||||
// Output: Pointer[target=Int[unsigned size=16]]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package btf
|
||||
|
||||
// datasecResolveWorkaround ensures that certain vars in a Datasec are added
|
||||
// to a Spec before the Datasec. This avoids a bug in kernel BTF validation.
|
||||
//
|
||||
// See https://lore.kernel.org/bpf/20230302123440.1193507-1-lmb@isovalent.com/
|
||||
func datasecResolveWorkaround(b *Builder, ds *Datasec) error {
|
||||
for _, vsi := range ds.Vars {
|
||||
v, ok := vsi.Type.(*Var)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch v.Type.(type) {
|
||||
case *Typedef, *Volatile, *Const, *Restrict, *typeTag:
|
||||
// NB: We must never call Add on a Datasec, otherwise we risk
|
||||
// infinite recursion.
|
||||
_, err := b.Add(v.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package btf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf/internal"
|
||||
"github.com/cilium/ebpf/internal/testutils"
|
||||
)
|
||||
|
||||
func TestDatasecResolveWorkaround(t *testing.T) {
|
||||
testutils.SkipOnOldKernel(t, "5.2", "BTF_KIND_DATASEC")
|
||||
|
||||
i := &Int{Size: 1}
|
||||
|
||||
for _, typ := range []Type{
|
||||
&Typedef{"foo", i},
|
||||
&Volatile{i},
|
||||
&Const{i},
|
||||
&Restrict{i},
|
||||
&typeTag{i, "foo"},
|
||||
} {
|
||||
t.Run(fmt.Sprint(typ), func(t *testing.T) {
|
||||
if _, ok := typ.(*typeTag); ok {
|
||||
testutils.SkipOnOldKernel(t, "5.17", "BTF_KIND_TYPE_TAG")
|
||||
}
|
||||
|
||||
ds := &Datasec{
|
||||
Name: "a",
|
||||
Size: 2,
|
||||
Vars: []VarSecinfo{
|
||||
{
|
||||
Size: 1,
|
||||
Offset: 0,
|
||||
// struct, union, pointer, array will trigger the bug.
|
||||
Type: &Var{Name: "a", Type: &Pointer{i}},
|
||||
},
|
||||
{
|
||||
Size: 1,
|
||||
Offset: 1,
|
||||
Type: &Var{
|
||||
Name: "b",
|
||||
Type: typ,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b, err := NewBuilder([]Type{ds})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
h, err := NewHandle(b)
|
||||
var ve *internal.VerifierError
|
||||
if errors.As(err, &ve) {
|
||||
t.Fatalf("%+v\n", ve)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h.Close()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
bpf2go
|
||||
===
|
||||
|
||||
`bpf2go` compiles a C source file into eBPF bytecode and then emits a
|
||||
Go file containing the eBPF. The goal is to avoid loading the
|
||||
eBPF from disk at runtime and to minimise the amount of manual
|
||||
work required to interact with eBPF programs. It takes inspiration
|
||||
from `bpftool gen skeleton`.
|
||||
|
||||
Invoke the program using go generate:
|
||||
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go foo path/to/src.c -- -I/path/to/include
|
||||
|
||||
This will emit `foo_bpfel.go` and `foo_bpfeb.go`, with types using `foo`
|
||||
as a stem. The two files contain compiled BPF for little and big
|
||||
endian systems, respectively.
|
||||
|
||||
You can use environment variables to affect all bpf2go invocations
|
||||
across a project, e.g. to set specific C flags:
|
||||
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cflags "$BPF_CFLAGS" foo path/to/src.c
|
||||
|
||||
By exporting `$BPF_CFLAGS` from your build system you can then control
|
||||
all builds from a single location.
|
||||
|
||||
## Generated types
|
||||
|
||||
`bpf2go` generates Go types for all map keys and values by default. You can
|
||||
disable this behaviour using `-no-global-types`. You can add to the set of
|
||||
types by specifying `-type foo` for each type you'd like to generate.
|
||||
|
||||
## Examples
|
||||
|
||||
See [examples/kprobe](../../examples/kprobe/main.go) for a fully worked out example.
|
||||
@@ -0,0 +1,210 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type compileArgs struct {
|
||||
// Which compiler to use
|
||||
cc string
|
||||
cFlags []string
|
||||
// Absolute working directory
|
||||
dir string
|
||||
// Absolute input file name
|
||||
source string
|
||||
// Absolute output file name
|
||||
dest string
|
||||
// Target to compile for, defaults to "bpf".
|
||||
target string
|
||||
// Depfile will be written here if depName is not empty
|
||||
dep io.Writer
|
||||
}
|
||||
|
||||
func compile(args compileArgs) error {
|
||||
// Default cflags that can be overridden by args.cFlags
|
||||
overrideFlags := []string{
|
||||
// Code needs to be optimized, otherwise the verifier will often fail
|
||||
// to understand it.
|
||||
"-O2",
|
||||
// Clang defaults to mcpu=probe which checks the kernel that we are
|
||||
// compiling on. This isn't appropriate for ahead of time
|
||||
// compiled code so force the most compatible version.
|
||||
"-mcpu=v1",
|
||||
}
|
||||
|
||||
cmd := exec.Command(args.cc, append(overrideFlags, args.cFlags...)...)
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
inputDir := filepath.Dir(args.source)
|
||||
relInputDir, err := filepath.Rel(args.dir, inputDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := args.target
|
||||
if target == "" {
|
||||
target = "bpf"
|
||||
}
|
||||
|
||||
// C flags that can't be overridden.
|
||||
cmd.Args = append(cmd.Args,
|
||||
"-target", target,
|
||||
"-c", args.source,
|
||||
"-o", args.dest,
|
||||
// Don't include clang version
|
||||
"-fno-ident",
|
||||
// Don't output inputDir into debug info
|
||||
"-fdebug-prefix-map="+inputDir+"="+relInputDir,
|
||||
"-fdebug-compilation-dir", ".",
|
||||
// We always want BTF to be generated, so enforce debug symbols
|
||||
"-g",
|
||||
fmt.Sprintf("-D__BPF_TARGET_MISSING=%q", "GCC error \"The eBPF is using target specific macros, please provide -target that is not bpf, bpfel or bpfeb\""),
|
||||
)
|
||||
cmd.Dir = args.dir
|
||||
|
||||
var depFile *os.File
|
||||
if args.dep != nil {
|
||||
depFile, err = os.CreateTemp("", "bpf2go")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer depFile.Close()
|
||||
defer os.Remove(depFile.Name())
|
||||
|
||||
cmd.Args = append(cmd.Args,
|
||||
// Output dependency information.
|
||||
"-MD",
|
||||
// Create phony targets so that deleting a dependency doesn't
|
||||
// break the build.
|
||||
"-MP",
|
||||
// Write it to temporary file
|
||||
"-MF"+depFile.Name(),
|
||||
)
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("can't execute %s: %s", args.cc, err)
|
||||
}
|
||||
|
||||
if depFile != nil {
|
||||
if _, err := io.Copy(args.dep, depFile); err != nil {
|
||||
return fmt.Errorf("error writing depfile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func adjustDependencies(baseDir string, deps []dependency) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
for _, dep := range deps {
|
||||
relativeFile, err := filepath.Rel(baseDir, dep.file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(dep.prerequisites) == 0 {
|
||||
_, err := fmt.Fprintf(&buf, "%s:\n\n", relativeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var prereqs []string
|
||||
for _, prereq := range dep.prerequisites {
|
||||
relativePrereq, err := filepath.Rel(baseDir, prereq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prereqs = append(prereqs, relativePrereq)
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(&buf, "%s: \\\n %s\n\n", relativeFile, strings.Join(prereqs, " \\\n "))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type dependency struct {
|
||||
file string
|
||||
prerequisites []string
|
||||
}
|
||||
|
||||
func parseDependencies(baseDir string, in io.Reader) ([]dependency, error) {
|
||||
abs := func(path string) string {
|
||||
if filepath.IsAbs(path) {
|
||||
return path
|
||||
}
|
||||
return filepath.Join(baseDir, path)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(in)
|
||||
var line strings.Builder
|
||||
var deps []dependency
|
||||
for scanner.Scan() {
|
||||
buf := scanner.Bytes()
|
||||
if line.Len()+len(buf) > 1024*1024 {
|
||||
return nil, errors.New("line too long")
|
||||
}
|
||||
|
||||
if bytes.HasSuffix(buf, []byte{'\\'}) {
|
||||
line.Write(buf[:len(buf)-1])
|
||||
continue
|
||||
}
|
||||
|
||||
line.Write(buf)
|
||||
if line.Len() == 0 {
|
||||
// Skip empty lines
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line.String(), ":", 2)
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("invalid line without ':'")
|
||||
}
|
||||
|
||||
// NB: This doesn't handle filenames with spaces in them.
|
||||
// It seems like make doesn't do that either, so oh well.
|
||||
var prereqs []string
|
||||
for _, prereq := range strings.Fields(parts[1]) {
|
||||
prereqs = append(prereqs, abs(prereq))
|
||||
}
|
||||
|
||||
deps = append(deps, dependency{
|
||||
abs(string(parts[0])),
|
||||
prereqs,
|
||||
})
|
||||
line.Reset()
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// There is always at least a dependency for the main file.
|
||||
if len(deps) == 0 {
|
||||
return nil, fmt.Errorf("empty dependency file")
|
||||
}
|
||||
return deps, nil
|
||||
}
|
||||
|
||||
// strip DWARF debug info from file by executing exe.
|
||||
func strip(exe, file string) error {
|
||||
cmd := exec.Command(exe, "-g", file)
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("%s: %s", exe, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const minimalSocketFilter = `__attribute__((section("socket"), used)) int main() { return 0; }`
|
||||
|
||||
func TestCompile(t *testing.T) {
|
||||
dir := mustWriteTempFile(t, "test.c", minimalSocketFilter)
|
||||
|
||||
var dep bytes.Buffer
|
||||
err := compile(compileArgs{
|
||||
cc: clangBin(t),
|
||||
dir: dir,
|
||||
source: filepath.Join(dir, "test.c"),
|
||||
dest: filepath.Join(dir, "test.o"),
|
||||
dep: &dep,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("Can't compile:", err)
|
||||
}
|
||||
|
||||
stat, err := os.Stat(filepath.Join(dir, "test.o"))
|
||||
if err != nil {
|
||||
t.Fatal("Can't stat output:", err)
|
||||
}
|
||||
|
||||
if stat.Size() == 0 {
|
||||
t.Error("Compilation creates an empty file")
|
||||
}
|
||||
|
||||
if dep.Len() == 0 {
|
||||
t.Error("Compilation doesn't generate depinfo")
|
||||
}
|
||||
|
||||
if _, err := parseDependencies(dir, &dep); err != nil {
|
||||
t.Error("Can't parse dependencies:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReproducibleCompile(t *testing.T) {
|
||||
clangBin := clangBin(t)
|
||||
dir := mustWriteTempFile(t, "test.c", minimalSocketFilter)
|
||||
|
||||
err := compile(compileArgs{
|
||||
cc: clangBin,
|
||||
dir: dir,
|
||||
source: filepath.Join(dir, "test.c"),
|
||||
dest: filepath.Join(dir, "a.o"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("Can't compile:", err)
|
||||
}
|
||||
|
||||
err = compile(compileArgs{
|
||||
cc: clangBin,
|
||||
dir: dir,
|
||||
source: filepath.Join(dir, "test.c"),
|
||||
dest: filepath.Join(dir, "b.o"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("Can't compile:", err)
|
||||
}
|
||||
|
||||
aBytes, err := os.ReadFile(filepath.Join(dir, "a.o"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bBytes, err := os.ReadFile(filepath.Join(dir, "b.o"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(aBytes, bBytes) {
|
||||
t.Error("Compiling the same file twice doesn't give the same result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriggerMissingTarget(t *testing.T) {
|
||||
dir := mustWriteTempFile(t, "test.c", `_Pragma(__BPF_TARGET_MISSING);`)
|
||||
|
||||
err := compile(compileArgs{
|
||||
cc: clangBin(t),
|
||||
dir: dir,
|
||||
source: filepath.Join(dir, "test.c"),
|
||||
dest: filepath.Join(dir, "a.o"),
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("No error when compiling __BPF_TARGET_MISSING")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDependencies(t *testing.T) {
|
||||
const input = `main.go: /foo/bar baz
|
||||
|
||||
frob: /gobble \
|
||||
gubble
|
||||
|
||||
nothing:
|
||||
`
|
||||
|
||||
have, err := parseDependencies("/foo", strings.NewReader(input))
|
||||
if err != nil {
|
||||
t.Fatal("Can't parse dependencies:", err)
|
||||
}
|
||||
|
||||
want := []dependency{
|
||||
{"/foo/main.go", []string{"/foo/bar", "/foo/baz"}},
|
||||
{"/foo/frob", []string{"/gobble", "/foo/gubble"}},
|
||||
{"/foo/nothing", nil},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(have, want) {
|
||||
t.Logf("Have: %#v", have)
|
||||
t.Logf("Want: %#v", want)
|
||||
t.Error("Result doesn't match")
|
||||
}
|
||||
|
||||
output, err := adjustDependencies("/foo", want)
|
||||
if err != nil {
|
||||
t.Error("Can't adjust dependencies")
|
||||
}
|
||||
|
||||
const wantOutput = `main.go: \
|
||||
bar \
|
||||
baz
|
||||
|
||||
frob: \
|
||||
../gobble \
|
||||
gubble
|
||||
|
||||
nothing:
|
||||
|
||||
`
|
||||
|
||||
if have := string(output); have != wantOutput {
|
||||
t.Logf("Have:\n%s", have)
|
||||
t.Logf("Want:\n%s", wantOutput)
|
||||
t.Error("Output doesn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func mustWriteTempFile(t *testing.T, name, contents string) string {
|
||||
t.Helper()
|
||||
|
||||
tmp, err := os.MkdirTemp("", "bpf2go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { os.RemoveAll(tmp) })
|
||||
|
||||
tmpFile := filepath.Join(tmp, name)
|
||||
if err := os.WriteFile(tmpFile, []byte(contents), 0660); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return tmp
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Program bpf2go embeds eBPF in Go.
|
||||
//
|
||||
// Please see the README for details how to use it.
|
||||
package main
|
||||
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"go/build/constraint"
|
||||
)
|
||||
|
||||
// buildTags is a comma-separated list of build tags.
|
||||
//
|
||||
// This follows the pre-Go 1.17 syntax and is kept for compatibility reasons.
|
||||
type buildTags struct {
|
||||
Expr constraint.Expr
|
||||
}
|
||||
|
||||
var _ flag.Value = (*buildTags)(nil)
|
||||
|
||||
func (bt *buildTags) String() string {
|
||||
if bt.Expr == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return (bt.Expr).String()
|
||||
}
|
||||
|
||||
func (bt *buildTags) Set(value string) error {
|
||||
ct, err := constraint.Parse("// +build " + value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bt.Expr = ct
|
||||
return nil
|
||||
}
|
||||
|
||||
func andConstraints(x, y constraint.Expr) constraint.Expr {
|
||||
if x == nil {
|
||||
return y
|
||||
}
|
||||
|
||||
if y == nil {
|
||||
return x
|
||||
}
|
||||
|
||||
return &constraint.AndExpr{X: x, Y: y}
|
||||
}
|
||||
|
||||
func orConstraints(x, y constraint.Expr) constraint.Expr {
|
||||
if x == nil {
|
||||
return y
|
||||
}
|
||||
|
||||
if y == nil {
|
||||
return x
|
||||
}
|
||||
|
||||
return &constraint.OrExpr{X: x, Y: y}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build/constraint"
|
||||
"go/token"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
const helpText = `Usage: %[1]s [options] <ident> <source file> [-- <C flags>]
|
||||
|
||||
ident is used as the stem of all generated Go types and functions, and
|
||||
must be a valid Go identifier.
|
||||
|
||||
source is a single C file that is compiled using the specified compiler
|
||||
(usually some version of clang).
|
||||
|
||||
You can pass options to the compiler by appending them after a '--' argument
|
||||
or by supplying -cflags. Flags passed as arguments take precedence
|
||||
over flags passed via -cflags. Additionally, the program expands quotation
|
||||
marks in -cflags. This means that -cflags 'foo "bar baz"' is passed to the
|
||||
compiler as two arguments "foo" and "bar baz".
|
||||
|
||||
The program expects GOPACKAGE to be set in the environment, and should be invoked
|
||||
via go generate. The generated files are written to the current directory.
|
||||
|
||||
Some options take defaults from the environment. Variable name is mentioned
|
||||
next to the respective option.
|
||||
|
||||
Options:
|
||||
|
||||
`
|
||||
|
||||
// Targets understood by bpf2go.
|
||||
//
|
||||
// Targets without a Linux string can't be used directly and are only included
|
||||
// for the generic bpf, bpfel, bpfeb targets.
|
||||
var targetByGoArch = map[string]target{
|
||||
"386": {"bpfel", "x86"},
|
||||
"amd64": {"bpfel", "x86"},
|
||||
"amd64p32": {"bpfel", ""},
|
||||
"arm": {"bpfel", "arm"},
|
||||
"arm64": {"bpfel", "arm64"},
|
||||
"loong64": {"bpfel", ""},
|
||||
"mipsle": {"bpfel", ""},
|
||||
"mips64le": {"bpfel", ""},
|
||||
"mips64p32le": {"bpfel", ""},
|
||||
"ppc64le": {"bpfel", "powerpc"},
|
||||
"riscv64": {"bpfel", ""},
|
||||
"armbe": {"bpfeb", "arm"},
|
||||
"arm64be": {"bpfeb", "arm64"},
|
||||
"mips": {"bpfeb", ""},
|
||||
"mips64": {"bpfeb", ""},
|
||||
"mips64p32": {"bpfeb", ""},
|
||||
"ppc64": {"bpfeb", "powerpc"},
|
||||
"s390": {"bpfeb", "s390"},
|
||||
"s390x": {"bpfeb", "s390"},
|
||||
"sparc": {"bpfeb", "sparc"},
|
||||
"sparc64": {"bpfeb", "sparc"},
|
||||
}
|
||||
|
||||
func run(stdout io.Writer, pkg, outputDir string, args []string) (err error) {
|
||||
b2g, err := newB2G(stdout, pkg, outputDir, args)
|
||||
switch {
|
||||
case err == nil:
|
||||
return b2g.convertAll()
|
||||
case errors.Is(err, flag.ErrHelp):
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type bpf2go struct {
|
||||
stdout io.Writer
|
||||
// Absolute path to a .c file.
|
||||
sourceFile string
|
||||
// Absolute path to a directory where .go are written
|
||||
outputDir string
|
||||
// Alternative output stem. If empty, identStem is used.
|
||||
outputStem string
|
||||
// Valid go package name.
|
||||
pkg string
|
||||
// Valid go identifier.
|
||||
identStem string
|
||||
// Targets to build for.
|
||||
targetArches map[target][]string
|
||||
// C compiler.
|
||||
cc string
|
||||
// Command used to strip DWARF.
|
||||
strip string
|
||||
disableStripping bool
|
||||
// C flags passed to the compiler.
|
||||
cFlags []string
|
||||
skipGlobalTypes bool
|
||||
// C types to include in the generatd output.
|
||||
cTypes cTypes
|
||||
// Build tags to be included in the output.
|
||||
tags buildTags
|
||||
// Base directory of the Makefile. Enables outputting make-style dependencies
|
||||
// in .d files.
|
||||
makeBase string
|
||||
}
|
||||
|
||||
func newB2G(stdout io.Writer, pkg, outputDir string, args []string) (*bpf2go, error) {
|
||||
b2g := &bpf2go{
|
||||
stdout: stdout,
|
||||
pkg: pkg,
|
||||
outputDir: outputDir,
|
||||
}
|
||||
|
||||
fs := flag.NewFlagSet("bpf2go", flag.ContinueOnError)
|
||||
fs.StringVar(&b2g.cc, "cc", getEnv("BPF2GO_CC", "clang"),
|
||||
"`binary` used to compile C to BPF ($BPF2GO_CC)")
|
||||
fs.StringVar(&b2g.strip, "strip", getEnv("BPF2GO_STRIP", ""),
|
||||
"`binary` used to strip DWARF from compiled BPF ($BPF2GO_STRIP)")
|
||||
fs.BoolVar(&b2g.disableStripping, "no-strip", false, "disable stripping of DWARF")
|
||||
flagCFlags := fs.String("cflags", getEnv("BPF2GO_CFLAGS", ""),
|
||||
"flags passed to the compiler, may contain quoted arguments ($BPF2GO_CFLAGS)")
|
||||
fs.Var(&b2g.tags, "tags", "Comma-separated list of Go build tags to include in generated files")
|
||||
flagTarget := fs.String("target", "bpfel,bpfeb", "clang target(s) to compile for (comma separated)")
|
||||
fs.StringVar(&b2g.makeBase, "makebase", getEnv("BPF2GO_MAKEBASE", ""),
|
||||
"write make compatible depinfo files relative to `directory` ($BPF2GO_MAKEBASE)")
|
||||
fs.Var(&b2g.cTypes, "type", "`Name` of a type to generate a Go declaration for, may be repeated")
|
||||
fs.BoolVar(&b2g.skipGlobalTypes, "no-global-types", false, "Skip generating types for map keys and values, etc.")
|
||||
fs.StringVar(&b2g.outputStem, "output-stem", "", "alternative stem for names of generated files (defaults to ident)")
|
||||
|
||||
fs.SetOutput(b2g.stdout)
|
||||
fs.Usage = func() {
|
||||
fmt.Fprintf(fs.Output(), helpText, fs.Name())
|
||||
fs.PrintDefaults()
|
||||
fmt.Fprintln(fs.Output())
|
||||
printTargets(fs.Output())
|
||||
}
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b2g.pkg == "" {
|
||||
return nil, errors.New("missing package, are you running via go generate?")
|
||||
}
|
||||
|
||||
if b2g.cc == "" {
|
||||
return nil, errors.New("no compiler specified")
|
||||
}
|
||||
|
||||
args, cFlags := splitCFlagsFromArgs(fs.Args())
|
||||
|
||||
if *flagCFlags != "" {
|
||||
splitCFlags, err := splitArguments(*flagCFlags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Command line arguments take precedence over C flags
|
||||
// from the flag.
|
||||
cFlags = append(splitCFlags, cFlags...)
|
||||
}
|
||||
|
||||
for _, cFlag := range cFlags {
|
||||
if strings.HasPrefix(cFlag, "-M") {
|
||||
return nil, fmt.Errorf("use -makebase instead of %q", cFlag)
|
||||
}
|
||||
}
|
||||
|
||||
b2g.cFlags = cFlags[:len(cFlags):len(cFlags)]
|
||||
|
||||
if len(args) < 2 {
|
||||
return nil, errors.New("expected at least two arguments")
|
||||
}
|
||||
|
||||
b2g.identStem = args[0]
|
||||
if !token.IsIdentifier(b2g.identStem) {
|
||||
return nil, fmt.Errorf("%q is not a valid identifier", b2g.identStem)
|
||||
}
|
||||
|
||||
sourceFile, err := filepath.Abs(args[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b2g.sourceFile = sourceFile
|
||||
|
||||
if b2g.makeBase != "" {
|
||||
b2g.makeBase, err = filepath.Abs(b2g.makeBase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if b2g.outputStem != "" && strings.ContainsRune(b2g.outputStem, filepath.Separator) {
|
||||
return nil, fmt.Errorf("-output-stem %q must not contain path separation characters", b2g.outputStem)
|
||||
}
|
||||
|
||||
targetArches, err := collectTargets(strings.Split(*flagTarget, ","))
|
||||
if errors.Is(err, errInvalidTarget) {
|
||||
printTargets(b2g.stdout)
|
||||
fmt.Fprintln(b2g.stdout)
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(targetArches) == 0 {
|
||||
return nil, fmt.Errorf("no targets specified")
|
||||
}
|
||||
b2g.targetArches = targetArches
|
||||
|
||||
// Try to find a suitable llvm-strip, possibly with a version suffix derived
|
||||
// from the clang binary.
|
||||
if b2g.strip == "" {
|
||||
b2g.strip = "llvm-strip"
|
||||
if strings.HasPrefix(b2g.cc, "clang") {
|
||||
b2g.strip += strings.TrimPrefix(b2g.cc, "clang")
|
||||
}
|
||||
}
|
||||
|
||||
return b2g, nil
|
||||
}
|
||||
|
||||
// cTypes collects the C type names a user wants to generate Go types for.
|
||||
//
|
||||
// Names are guaranteed to be unique, and only a subset of names is accepted so
|
||||
// that we may extend the flag syntax in the future.
|
||||
type cTypes []string
|
||||
|
||||
var _ flag.Value = (*cTypes)(nil)
|
||||
|
||||
func (ct *cTypes) String() string {
|
||||
if ct == nil {
|
||||
return "[]"
|
||||
}
|
||||
return fmt.Sprint(*ct)
|
||||
}
|
||||
|
||||
const validCTypeChars = `[a-z0-9_]`
|
||||
|
||||
var reValidCType = regexp.MustCompile(`(?i)^` + validCTypeChars + `+$`)
|
||||
|
||||
func (ct *cTypes) Set(value string) error {
|
||||
if !reValidCType.MatchString(value) {
|
||||
return fmt.Errorf("%q contains characters outside of %s", value, validCTypeChars)
|
||||
}
|
||||
|
||||
i := sort.SearchStrings(*ct, value)
|
||||
if i >= len(*ct) {
|
||||
*ct = append(*ct, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
if (*ct)[i] == value {
|
||||
return fmt.Errorf("duplicate type %q", value)
|
||||
}
|
||||
|
||||
*ct = append((*ct)[:i], append([]string{value}, (*ct)[i:]...)...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if val, ok := os.LookupEnv(key); ok {
|
||||
return val
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
func (b2g *bpf2go) convertAll() (err error) {
|
||||
if _, err := os.Stat(b2g.sourceFile); os.IsNotExist(err) {
|
||||
return fmt.Errorf("file %s doesn't exist", b2g.sourceFile)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !b2g.disableStripping {
|
||||
b2g.strip, err = exec.LookPath(b2g.strip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for target, arches := range b2g.targetArches {
|
||||
if err := b2g.convert(target, arches); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b2g *bpf2go) convert(tgt target, arches []string) (err error) {
|
||||
removeOnError := func(f *os.File) {
|
||||
if err != nil {
|
||||
os.Remove(f.Name())
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
outputStem := b2g.outputStem
|
||||
if outputStem == "" {
|
||||
outputStem = strings.ToLower(b2g.identStem)
|
||||
}
|
||||
stem := fmt.Sprintf("%s_%s", outputStem, tgt.clang)
|
||||
if tgt.linux != "" {
|
||||
stem = fmt.Sprintf("%s_%s_%s", outputStem, tgt.clang, tgt.linux)
|
||||
}
|
||||
|
||||
objFileName := filepath.Join(b2g.outputDir, stem+".o")
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var archConstraint constraint.Expr
|
||||
for _, arch := range arches {
|
||||
tag := &constraint.TagExpr{Tag: arch}
|
||||
archConstraint = orConstraints(archConstraint, tag)
|
||||
}
|
||||
|
||||
constraints := andConstraints(archConstraint, b2g.tags.Expr)
|
||||
|
||||
cFlags := make([]string, len(b2g.cFlags))
|
||||
copy(cFlags, b2g.cFlags)
|
||||
if tgt.linux != "" {
|
||||
cFlags = append(cFlags, "-D__TARGET_ARCH_"+tgt.linux)
|
||||
}
|
||||
|
||||
var dep bytes.Buffer
|
||||
err = compile(compileArgs{
|
||||
cc: b2g.cc,
|
||||
cFlags: cFlags,
|
||||
target: tgt.clang,
|
||||
dir: cwd,
|
||||
source: b2g.sourceFile,
|
||||
dest: objFileName,
|
||||
dep: &dep,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(b2g.stdout, "Compiled", objFileName)
|
||||
|
||||
if !b2g.disableStripping {
|
||||
if err := strip(b2g.strip, objFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(b2g.stdout, "Stripped", objFileName)
|
||||
}
|
||||
|
||||
spec, err := ebpf.LoadCollectionSpec(objFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't load BPF from ELF: %s", err)
|
||||
}
|
||||
|
||||
maps, programs, types, err := collectFromSpec(spec, b2g.cTypes, b2g.skipGlobalTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write out generated go
|
||||
goFileName := filepath.Join(b2g.outputDir, stem+".go")
|
||||
goFile, err := os.Create(goFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer removeOnError(goFile)
|
||||
|
||||
err = output(outputArgs{
|
||||
pkg: b2g.pkg,
|
||||
stem: b2g.identStem,
|
||||
constraints: constraints,
|
||||
maps: maps,
|
||||
programs: programs,
|
||||
types: types,
|
||||
obj: filepath.Base(objFileName),
|
||||
out: goFile,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't write %s: %s", goFileName, err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(b2g.stdout, "Wrote", goFileName)
|
||||
|
||||
if b2g.makeBase == "" {
|
||||
return
|
||||
}
|
||||
|
||||
deps, err := parseDependencies(cwd, &dep)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't read dependency information: %s", err)
|
||||
}
|
||||
|
||||
// There is always at least a dependency for the main file.
|
||||
deps[0].file = goFileName
|
||||
depFile, err := adjustDependencies(b2g.makeBase, deps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't adjust dependency information: %s", err)
|
||||
}
|
||||
|
||||
depFileName := goFileName + ".d"
|
||||
if err := os.WriteFile(depFileName, depFile, 0666); err != nil {
|
||||
return fmt.Errorf("can't write dependency file: %s", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(b2g.stdout, "Wrote", depFileName)
|
||||
return nil
|
||||
}
|
||||
|
||||
type target struct {
|
||||
clang string
|
||||
linux string
|
||||
}
|
||||
|
||||
func printTargets(w io.Writer) {
|
||||
var arches []string
|
||||
for arch, archTarget := range targetByGoArch {
|
||||
if archTarget.linux == "" {
|
||||
continue
|
||||
}
|
||||
arches = append(arches, arch)
|
||||
}
|
||||
sort.Strings(arches)
|
||||
|
||||
fmt.Fprint(w, "Supported targets:\n")
|
||||
fmt.Fprint(w, "\tbpf\n\tbpfel\n\tbpfeb\n")
|
||||
for _, arch := range arches {
|
||||
fmt.Fprintf(w, "\t%s\n", arch)
|
||||
}
|
||||
}
|
||||
|
||||
var errInvalidTarget = errors.New("unsupported target")
|
||||
|
||||
func collectTargets(targets []string) (map[target][]string, error) {
|
||||
result := make(map[target][]string)
|
||||
for _, tgt := range targets {
|
||||
switch tgt {
|
||||
case "bpf", "bpfel", "bpfeb":
|
||||
var goarches []string
|
||||
for arch, archTarget := range targetByGoArch {
|
||||
if archTarget.clang == tgt {
|
||||
// Include tags for all goarches that have the same endianness.
|
||||
goarches = append(goarches, arch)
|
||||
}
|
||||
}
|
||||
sort.Strings(goarches)
|
||||
result[target{tgt, ""}] = goarches
|
||||
|
||||
case "native":
|
||||
tgt = runtime.GOARCH
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
archTarget, ok := targetByGoArch[tgt]
|
||||
if !ok || archTarget.linux == "" {
|
||||
return nil, fmt.Errorf("%q: %w", tgt, errInvalidTarget)
|
||||
}
|
||||
|
||||
var goarches []string
|
||||
for goarch, lt := range targetByGoArch {
|
||||
if lt == archTarget {
|
||||
// Include tags for all goarches that have the same
|
||||
// target.
|
||||
goarches = append(goarches, goarch)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(goarches)
|
||||
result[archTarget] = goarches
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
outputDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := run(os.Stdout, os.Getenv("GOPACKAGE"), outputDir, os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
clangBin := clangBin(t)
|
||||
dir := mustWriteTempFile(t, "test.c", minimalSocketFilter)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
modRoot := filepath.Clean(filepath.Join(cwd, "../.."))
|
||||
if _, err := os.Stat(filepath.Join(modRoot, "go.mod")); os.IsNotExist(err) {
|
||||
t.Fatal("No go.mod file in", modRoot)
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "bpf2go-module-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
execInModule := func(name string, args ...string) {
|
||||
t.Helper()
|
||||
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Dir = tmpDir
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
if out := string(out); out != "" {
|
||||
t.Log(out)
|
||||
}
|
||||
t.Fatalf("Can't execute %s: %v", name, args)
|
||||
}
|
||||
}
|
||||
|
||||
module := currentModule()
|
||||
|
||||
execInModule("go", "mod", "init", "bpf2go-test")
|
||||
|
||||
execInModule("go", "mod", "edit",
|
||||
// Require the module. The version doesn't matter due to the replace
|
||||
// below.
|
||||
fmt.Sprintf("-require=%s@v0.0.0", module),
|
||||
// Replace the module with the current version.
|
||||
fmt.Sprintf("-replace=%s=%s", module, modRoot),
|
||||
)
|
||||
|
||||
err = run(io.Discard, "foo", tmpDir, []string{
|
||||
"-cc", clangBin,
|
||||
"bar",
|
||||
filepath.Join(dir, "test.c"),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Can't run:", err)
|
||||
}
|
||||
|
||||
for _, arch := range []string{
|
||||
"amd64", // little-endian
|
||||
"s390x", // big-endian
|
||||
} {
|
||||
t.Run(arch, func(t *testing.T) {
|
||||
goBin := exec.Command("go", "build", "-mod=mod")
|
||||
goBin.Dir = tmpDir
|
||||
goBin.Env = append(os.Environ(),
|
||||
"GOOS=linux",
|
||||
"GOARCH="+arch,
|
||||
)
|
||||
out, err := goBin.CombinedOutput()
|
||||
if err != nil {
|
||||
if out := string(out); out != "" {
|
||||
t.Log(out)
|
||||
}
|
||||
t.Error("Can't compile package:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
err := run(&stdout, "", "", []string{"-help"})
|
||||
if err != nil {
|
||||
t.Fatal("Can't execute -help")
|
||||
}
|
||||
|
||||
if stdout.Len() == 0 {
|
||||
t.Error("-help doesn't write to stdout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableStripping(t *testing.T) {
|
||||
dir := mustWriteTempFile(t, "test.c", minimalSocketFilter)
|
||||
|
||||
err := run(io.Discard, "foo", dir, []string{
|
||||
"-cc", clangBin(t),
|
||||
"-strip", "binary-that-certainly-doesnt-exist",
|
||||
"-no-strip",
|
||||
"bar",
|
||||
filepath.Join(dir, "test.c"),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Can't run with stripping disabled:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectTargets(t *testing.T) {
|
||||
clangArches := make(map[string][]string)
|
||||
linuxArchesLE := make(map[string][]string)
|
||||
linuxArchesBE := make(map[string][]string)
|
||||
for arch, archTarget := range targetByGoArch {
|
||||
clangArches[archTarget.clang] = append(clangArches[archTarget.clang], arch)
|
||||
if archTarget.clang == "bpfel" {
|
||||
linuxArchesLE[archTarget.linux] = append(linuxArchesLE[archTarget.linux], arch)
|
||||
continue
|
||||
}
|
||||
linuxArchesBE[archTarget.linux] = append(linuxArchesBE[archTarget.linux], arch)
|
||||
}
|
||||
for i := range clangArches {
|
||||
sort.Strings(clangArches[i])
|
||||
}
|
||||
for i := range linuxArchesLE {
|
||||
sort.Strings(linuxArchesLE[i])
|
||||
}
|
||||
for i := range linuxArchesBE {
|
||||
sort.Strings(linuxArchesBE[i])
|
||||
}
|
||||
|
||||
nativeTarget := make(map[target][]string)
|
||||
for arch, archTarget := range targetByGoArch {
|
||||
if arch == runtime.GOARCH {
|
||||
if archTarget.clang == "bpfel" {
|
||||
nativeTarget[archTarget] = linuxArchesLE[archTarget.linux]
|
||||
} else {
|
||||
nativeTarget[archTarget] = linuxArchesBE[archTarget.linux]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
targets []string
|
||||
want map[target][]string
|
||||
}{
|
||||
{
|
||||
[]string{"bpf", "bpfel", "bpfeb"},
|
||||
map[target][]string{
|
||||
{"bpf", ""}: nil,
|
||||
{"bpfel", ""}: clangArches["bpfel"],
|
||||
{"bpfeb", ""}: clangArches["bpfeb"],
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"amd64", "386"},
|
||||
map[target][]string{
|
||||
{"bpfel", "x86"}: linuxArchesLE["x86"],
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"amd64", "arm64be"},
|
||||
map[target][]string{
|
||||
{"bpfeb", "arm64"}: linuxArchesBE["arm64"],
|
||||
{"bpfel", "x86"}: linuxArchesLE["x86"],
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"native"},
|
||||
nativeTarget,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
name := strings.Join(test.targets, ",")
|
||||
t.Run(name, func(t *testing.T) {
|
||||
have, err := collectTargets(test.targets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(test.want, have); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectTargetsErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
target string
|
||||
}{
|
||||
{"unknown", "frood"},
|
||||
{"no linux target", "mips64p32le"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := collectTargets([]string{test.target})
|
||||
if err == nil {
|
||||
t.Fatal("Function did not return an error")
|
||||
}
|
||||
t.Log("Error message:", err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertGOARCH(t *testing.T) {
|
||||
tmp := mustWriteTempFile(t, "test.c",
|
||||
`
|
||||
#ifndef __TARGET_ARCH_x86
|
||||
#error __TARGET_ARCH_x86 is not defined
|
||||
#endif`,
|
||||
)
|
||||
|
||||
b2g := bpf2go{
|
||||
pkg: "test",
|
||||
stdout: io.Discard,
|
||||
identStem: "test",
|
||||
cc: clangBin(t),
|
||||
disableStripping: true,
|
||||
sourceFile: tmp + "/test.c",
|
||||
outputDir: tmp,
|
||||
}
|
||||
|
||||
if err := b2g.convert(targetByGoArch["amd64"], nil); err != nil {
|
||||
t.Fatal("Can't target GOARCH:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCTypes(t *testing.T) {
|
||||
var ct cTypes
|
||||
valid := []string{
|
||||
"abcdefghijklmnopqrstuvqxyABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_",
|
||||
"y",
|
||||
}
|
||||
for _, value := range valid {
|
||||
if err := ct.Set(value); err != nil {
|
||||
t.Fatalf("Set returned an error for %q: %s", value, err)
|
||||
}
|
||||
}
|
||||
qt.Assert(t, ct, qt.ContentEquals, cTypes(valid))
|
||||
|
||||
for _, value := range []string{
|
||||
"",
|
||||
" ",
|
||||
" frood",
|
||||
"foo\nbar",
|
||||
".",
|
||||
",",
|
||||
"+",
|
||||
"-",
|
||||
} {
|
||||
ct = nil
|
||||
if err := ct.Set(value); err == nil {
|
||||
t.Fatalf("Set did not return an error for %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
ct = nil
|
||||
qt.Assert(t, ct.Set("foo"), qt.IsNil)
|
||||
qt.Assert(t, ct.Set("foo"), qt.IsNotNil)
|
||||
}
|
||||
|
||||
func TestParseArgs(t *testing.T) {
|
||||
const (
|
||||
pkg = "eee"
|
||||
outputDir = "."
|
||||
csource = "testdata/minimal.c"
|
||||
stem = "a"
|
||||
)
|
||||
|
||||
t.Run("makebase", func(t *testing.T) {
|
||||
basePath, _ := filepath.Abs("barfoo")
|
||||
args := []string{"-makebase", basePath, stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.makeBase, qt.Equals, basePath)
|
||||
})
|
||||
|
||||
t.Run("makebase from env", func(t *testing.T) {
|
||||
basePath, _ := filepath.Abs("barfoo")
|
||||
args := []string{stem, csource}
|
||||
t.Setenv("BPF2GO_MAKEBASE", basePath)
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.makeBase, qt.Equals, basePath)
|
||||
})
|
||||
|
||||
t.Run("makebase flag overrides env", func(t *testing.T) {
|
||||
basePathFlag, _ := filepath.Abs("barfoo")
|
||||
basePathEnv, _ := filepath.Abs("foobar")
|
||||
args := []string{"-makebase", basePathFlag, stem, csource}
|
||||
t.Setenv("BPF2GO_MAKEBASE", basePathEnv)
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.makeBase, qt.Equals, basePathFlag)
|
||||
})
|
||||
|
||||
t.Run("cc defaults to clang", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cc, qt.Equals, "clang")
|
||||
})
|
||||
|
||||
t.Run("cc", func(t *testing.T) {
|
||||
args := []string{"-cc", "barfoo", stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cc, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("cc from env", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
t.Setenv("BPF2GO_CC", "barfoo")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cc, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("cc flag overrides env", func(t *testing.T) {
|
||||
args := []string{"-cc", "barfoo", stem, csource}
|
||||
t.Setenv("BPF2GO_CC", "foobar")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cc, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("strip defaults to llvm-strip", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.strip, qt.Equals, "llvm-strip")
|
||||
})
|
||||
|
||||
t.Run("strip", func(t *testing.T) {
|
||||
args := []string{"-strip", "barfoo", stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.strip, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("strip from env", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
t.Setenv("BPF2GO_STRIP", "barfoo")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.strip, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("strip flag overrides env", func(t *testing.T) {
|
||||
args := []string{"-strip", "barfoo", stem, csource}
|
||||
t.Setenv("BPF2GO_STRIP", "foobar")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.strip, qt.Equals, "barfoo")
|
||||
})
|
||||
|
||||
t.Run("no strip defaults to false", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.disableStripping, qt.IsFalse)
|
||||
})
|
||||
|
||||
t.Run("no strip", func(t *testing.T) {
|
||||
args := []string{"-no-strip", stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.disableStripping, qt.IsTrue)
|
||||
})
|
||||
|
||||
t.Run("cflags flag", func(t *testing.T) {
|
||||
args := []string{"-cflags", "x y z", stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cFlags, qt.DeepEquals, []string{"x", "y", "z"})
|
||||
})
|
||||
|
||||
t.Run("cflags multi flag", func(t *testing.T) {
|
||||
args := []string{"-cflags", "x y z", "-cflags", "u v", stem, csource}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cFlags, qt.DeepEquals, []string{"u", "v"})
|
||||
})
|
||||
|
||||
t.Run("cflags flag and args", func(t *testing.T) {
|
||||
args := []string{"-cflags", "x y z", "stem", csource, "--", "u", "v"}
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cFlags, qt.DeepEquals, []string{"x", "y", "z", "u", "v"})
|
||||
})
|
||||
|
||||
t.Run("cflags from env", func(t *testing.T) {
|
||||
args := []string{stem, csource}
|
||||
t.Setenv("BPF2GO_CFLAGS", "x y z")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cFlags, qt.DeepEquals, []string{"x", "y", "z"})
|
||||
})
|
||||
|
||||
t.Run("cflags flag overrides env", func(t *testing.T) {
|
||||
args := []string{"-cflags", "u v", stem, csource}
|
||||
t.Setenv("BPF2GO_CFLAGS", "x y z")
|
||||
b2g, err := newB2G(&bytes.Buffer{}, pkg, outputDir, args)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, b2g.cFlags, qt.DeepEquals, []string{"u", "v"})
|
||||
})
|
||||
}
|
||||
|
||||
func clangBin(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("Not compiling with -short")
|
||||
}
|
||||
|
||||
// Use a recent clang version for local development, but allow CI to run
|
||||
// against oldest supported clang.
|
||||
clang := "clang-14"
|
||||
if minVersion := os.Getenv("CI_MIN_CLANG_VERSION"); minVersion != "" {
|
||||
clang = fmt.Sprintf("clang-%s", minVersion)
|
||||
}
|
||||
|
||||
t.Log("Testing against", clang)
|
||||
return clang
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"go/build/constraint"
|
||||
"go/token"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
"github.com/cilium/ebpf/btf"
|
||||
"github.com/cilium/ebpf/internal"
|
||||
)
|
||||
|
||||
//go:embed output.tpl
|
||||
var commonRaw string
|
||||
|
||||
var commonTemplate = template.Must(template.New("common").Parse(commonRaw))
|
||||
|
||||
type templateName string
|
||||
|
||||
func (n templateName) maybeExport(str string) string {
|
||||
if token.IsExported(string(n)) {
|
||||
return toUpperFirst(str)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func (n templateName) Bytes() string {
|
||||
return "_" + toUpperFirst(string(n)) + "Bytes"
|
||||
}
|
||||
|
||||
func (n templateName) Specs() string {
|
||||
return string(n) + "Specs"
|
||||
}
|
||||
|
||||
func (n templateName) ProgramSpecs() string {
|
||||
return string(n) + "ProgramSpecs"
|
||||
}
|
||||
|
||||
func (n templateName) MapSpecs() string {
|
||||
return string(n) + "MapSpecs"
|
||||
}
|
||||
|
||||
func (n templateName) Load() string {
|
||||
return n.maybeExport("load" + toUpperFirst(string(n)))
|
||||
}
|
||||
|
||||
func (n templateName) LoadObjects() string {
|
||||
return n.maybeExport("load" + toUpperFirst(string(n)) + "Objects")
|
||||
}
|
||||
|
||||
func (n templateName) Objects() string {
|
||||
return string(n) + "Objects"
|
||||
}
|
||||
|
||||
func (n templateName) Maps() string {
|
||||
return string(n) + "Maps"
|
||||
}
|
||||
|
||||
func (n templateName) Programs() string {
|
||||
return string(n) + "Programs"
|
||||
}
|
||||
|
||||
func (n templateName) CloseHelper() string {
|
||||
return "_" + toUpperFirst(string(n)) + "Close"
|
||||
}
|
||||
|
||||
type outputArgs struct {
|
||||
// Package of the resulting file.
|
||||
pkg string
|
||||
// The prefix of all names declared at the top-level.
|
||||
stem string
|
||||
// Build constraints included in the resulting file.
|
||||
constraints constraint.Expr
|
||||
// Maps to be emitted.
|
||||
maps []string
|
||||
// Programs to be emitted.
|
||||
programs []string
|
||||
// Types to be emitted.
|
||||
types []btf.Type
|
||||
// Filename of the ELF object to embed.
|
||||
obj string
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func output(args outputArgs) error {
|
||||
maps := make(map[string]string)
|
||||
for _, name := range args.maps {
|
||||
maps[name] = internal.Identifier(name)
|
||||
}
|
||||
|
||||
programs := make(map[string]string)
|
||||
for _, name := range args.programs {
|
||||
programs[name] = internal.Identifier(name)
|
||||
}
|
||||
|
||||
typeNames := make(map[btf.Type]string)
|
||||
for _, typ := range args.types {
|
||||
typeNames[typ] = args.stem + internal.Identifier(typ.TypeName())
|
||||
}
|
||||
|
||||
// Ensure we don't have conflicting names and generate a sorted list of
|
||||
// named types so that the output is stable.
|
||||
types, err := sortTypes(typeNames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
module := currentModule()
|
||||
|
||||
gf := &btf.GoFormatter{
|
||||
Names: typeNames,
|
||||
Identifier: internal.Identifier,
|
||||
}
|
||||
|
||||
ctx := struct {
|
||||
*btf.GoFormatter
|
||||
Module string
|
||||
Package string
|
||||
Constraints constraint.Expr
|
||||
Name templateName
|
||||
Maps map[string]string
|
||||
Programs map[string]string
|
||||
Types []btf.Type
|
||||
TypeNames map[btf.Type]string
|
||||
File string
|
||||
}{
|
||||
gf,
|
||||
module,
|
||||
args.pkg,
|
||||
args.constraints,
|
||||
templateName(args.stem),
|
||||
maps,
|
||||
programs,
|
||||
types,
|
||||
typeNames,
|
||||
args.obj,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := commonTemplate.Execute(&buf, &ctx); err != nil {
|
||||
return fmt.Errorf("can't generate types: %s", err)
|
||||
}
|
||||
|
||||
return internal.WriteFormatted(buf.Bytes(), args.out)
|
||||
}
|
||||
|
||||
func collectFromSpec(spec *ebpf.CollectionSpec, cTypes []string, skipGlobalTypes bool) (maps, programs []string, types []btf.Type, _ error) {
|
||||
for name := range spec.Maps {
|
||||
// Skip .rodata, .data, .bss, etc. sections
|
||||
if !strings.HasPrefix(name, ".") {
|
||||
maps = append(maps, name)
|
||||
}
|
||||
}
|
||||
|
||||
for name := range spec.Programs {
|
||||
programs = append(programs, name)
|
||||
}
|
||||
|
||||
types, err := collectCTypes(spec.Types, cTypes)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("collect C types: %w", err)
|
||||
}
|
||||
|
||||
// Collect map key and value types, unless we've been asked not to.
|
||||
if skipGlobalTypes {
|
||||
return maps, programs, types, nil
|
||||
}
|
||||
|
||||
for _, typ := range collectMapTypes(spec.Maps) {
|
||||
switch btf.UnderlyingType(typ).(type) {
|
||||
case *btf.Datasec:
|
||||
// Avoid emitting .rodata, .bss, etc. for now. We might want to
|
||||
// name these types differently, etc.
|
||||
continue
|
||||
|
||||
case *btf.Int:
|
||||
// Don't emit primitive types by default.
|
||||
continue
|
||||
}
|
||||
|
||||
types = append(types, typ)
|
||||
}
|
||||
|
||||
return maps, programs, types, nil
|
||||
}
|
||||
|
||||
func collectCTypes(types *btf.Spec, names []string) ([]btf.Type, error) {
|
||||
var result []btf.Type
|
||||
for _, cType := range names {
|
||||
typ, err := types.AnyTypeByName(cType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, typ)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// collectMapTypes returns a list of all types used as map keys or values.
|
||||
func collectMapTypes(maps map[string]*ebpf.MapSpec) []btf.Type {
|
||||
var result []btf.Type
|
||||
for _, m := range maps {
|
||||
if m.Key != nil && m.Key.TypeName() != "" {
|
||||
result = append(result, m.Key)
|
||||
}
|
||||
|
||||
if m.Value != nil && m.Value.TypeName() != "" {
|
||||
result = append(result, m.Value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// sortTypes returns a list of types sorted by their (generated) Go type name.
|
||||
//
|
||||
// Duplicate Go type names are rejected.
|
||||
func sortTypes(typeNames map[btf.Type]string) ([]btf.Type, error) {
|
||||
var types []btf.Type
|
||||
var names []string
|
||||
for typ, name := range typeNames {
|
||||
i := sort.SearchStrings(names, name)
|
||||
if i >= len(names) {
|
||||
types = append(types, typ)
|
||||
names = append(names, name)
|
||||
continue
|
||||
}
|
||||
|
||||
if names[i] == name {
|
||||
return nil, fmt.Errorf("type name %q is used multiple times", name)
|
||||
}
|
||||
|
||||
types = append(types[:i], append([]btf.Type{typ}, types[i:]...)...)
|
||||
names = append(names[:i], append([]string{name}, names[i:]...)...)
|
||||
}
|
||||
|
||||
return types, nil
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Code generated by bpf2go; DO NOT EDIT.
|
||||
{{ with .Constraints }}//go:build {{ . }}{{ end }}
|
||||
|
||||
package {{ .Package }}
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"{{ .Module }}"
|
||||
)
|
||||
|
||||
{{- if .Types }}
|
||||
{{- range $type := .Types }}
|
||||
{{ $.TypeDeclaration (index $.TypeNames $type) $type }}
|
||||
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
|
||||
// {{ .Name.Load }} returns the embedded CollectionSpec for {{ .Name }}.
|
||||
func {{ .Name.Load }}() (*ebpf.CollectionSpec, error) {
|
||||
reader := bytes.NewReader({{ .Name.Bytes }})
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load {{ .Name }}: %w", err)
|
||||
}
|
||||
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// {{ .Name.LoadObjects }} loads {{ .Name }} and converts it into a struct.
|
||||
//
|
||||
// The following types are suitable as obj argument:
|
||||
//
|
||||
// *{{ .Name.Objects }}
|
||||
// *{{ .Name.Programs }}
|
||||
// *{{ .Name.Maps }}
|
||||
//
|
||||
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||
func {{ .Name.LoadObjects }}(obj interface{}, opts *ebpf.CollectionOptions) (error) {
|
||||
spec, err := {{ .Name.Load }}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return spec.LoadAndAssign(obj, opts)
|
||||
}
|
||||
|
||||
// {{ .Name.Specs }} contains maps and programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type {{ .Name.Specs }} struct {
|
||||
{{ .Name.ProgramSpecs }}
|
||||
{{ .Name.MapSpecs }}
|
||||
}
|
||||
|
||||
// {{ .Name.Specs }} contains programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type {{ .Name.ProgramSpecs }} struct {
|
||||
{{- range $name, $id := .Programs }}
|
||||
{{ $id }} *ebpf.ProgramSpec `ebpf:"{{ $name }}"`
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
// {{ .Name.MapSpecs }} contains maps before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type {{ .Name.MapSpecs }} struct {
|
||||
{{- range $name, $id := .Maps }}
|
||||
{{ $id }} *ebpf.MapSpec `ebpf:"{{ $name }}"`
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
// {{ .Name.Objects }} contains all objects after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to {{ .Name.LoadObjects }} or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type {{ .Name.Objects }} struct {
|
||||
{{ .Name.Programs }}
|
||||
{{ .Name.Maps }}
|
||||
}
|
||||
|
||||
func (o *{{ .Name.Objects }}) Close() error {
|
||||
return {{ .Name.CloseHelper }}(
|
||||
&o.{{ .Name.Programs }},
|
||||
&o.{{ .Name.Maps }},
|
||||
)
|
||||
}
|
||||
|
||||
// {{ .Name.Maps }} contains all maps after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to {{ .Name.LoadObjects }} or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type {{ .Name.Maps }} struct {
|
||||
{{- range $name, $id := .Maps }}
|
||||
{{ $id }} *ebpf.Map `ebpf:"{{ $name }}"`
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
func (m *{{ .Name.Maps }}) Close() error {
|
||||
return {{ .Name.CloseHelper }}(
|
||||
{{- range $id := .Maps }}
|
||||
m.{{ $id }},
|
||||
{{- end }}
|
||||
)
|
||||
}
|
||||
|
||||
// {{ .Name.Programs }} contains all programs after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to {{ .Name.LoadObjects }} or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type {{ .Name.Programs }} struct {
|
||||
{{- range $name, $id := .Programs }}
|
||||
{{ $id }} *ebpf.Program `ebpf:"{{ $name }}"`
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
func (p *{{ .Name.Programs }}) Close() error {
|
||||
return {{ .Name.CloseHelper }}(
|
||||
{{- range $id := .Programs }}
|
||||
p.{{ $id }},
|
||||
{{- end }}
|
||||
)
|
||||
}
|
||||
|
||||
func {{ .Name.CloseHelper }}(closers ...io.Closer) error {
|
||||
for _, closer := range closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not access this directly.
|
||||
//go:embed {{ .File }}
|
||||
var {{ .Name.Bytes }} []byte
|
||||
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
"github.com/cilium/ebpf/btf"
|
||||
"github.com/cilium/ebpf/internal"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestOrderTypes(t *testing.T) {
|
||||
a := &btf.Int{}
|
||||
b := &btf.Int{}
|
||||
c := &btf.Int{}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
in map[btf.Type]string
|
||||
out []btf.Type
|
||||
}{
|
||||
{
|
||||
"order",
|
||||
map[btf.Type]string{
|
||||
a: "foo",
|
||||
b: "bar",
|
||||
c: "baz",
|
||||
},
|
||||
[]btf.Type{b, c, a},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := sortTypes(test.in)
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, len(result), qt.Equals, len(test.out))
|
||||
for i, o := range test.out {
|
||||
if result[i] != o {
|
||||
t.Fatalf("Index %d: expected %p got %p", i, o, result[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
in map[btf.Type]string
|
||||
}{
|
||||
{
|
||||
"duplicate names",
|
||||
map[btf.Type]string{
|
||||
a: "foo",
|
||||
b: "foo",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := sortTypes(test.in)
|
||||
qt.Assert(t, err, qt.IsNotNil)
|
||||
qt.Assert(t, result, qt.IsNil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var typesEqual = qt.CmpEquals(cmp.Comparer(func(a, b btf.Type) bool {
|
||||
return a == b
|
||||
}))
|
||||
|
||||
func TestCollectFromSpec(t *testing.T) {
|
||||
spec, err := ebpf.LoadCollectionSpec(fmt.Sprintf("testdata/minimal-%s.elf", internal.ClangEndian))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
map1 := spec.Maps["map1"]
|
||||
|
||||
maps, programs, types, err := collectFromSpec(spec, nil, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
qt.Assert(t, maps, qt.ContentEquals, []string{"map1"})
|
||||
qt.Assert(t, programs, qt.ContentEquals, []string{"filter"})
|
||||
qt.Assert(t, types, typesEqual, []btf.Type{map1.Key, map1.Value})
|
||||
|
||||
_, _, types, err = collectFromSpec(spec, nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
qt.Assert(t, types, typesEqual, ([]btf.Type)(nil))
|
||||
|
||||
_, _, types, err = collectFromSpec(spec, []string{"barfoo"}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
qt.Assert(t, types, typesEqual, []btf.Type{map1.Value})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/cilium/ebpf/internal/testutils"
|
||||
)
|
||||
|
||||
func TestLoadingSpec(t *testing.T) {
|
||||
spec, err := loadTest()
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("Can't load spec:", err)
|
||||
}
|
||||
|
||||
if spec == nil {
|
||||
t.Fatal("Got a nil spec")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadingObjects(t *testing.T) {
|
||||
var objs testObjects
|
||||
err := loadTestObjects(&objs, nil)
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("Can't load objects:", err)
|
||||
}
|
||||
defer objs.Close()
|
||||
|
||||
if objs.Filter == nil {
|
||||
t.Error("Loading returns an object with nil programs")
|
||||
}
|
||||
|
||||
if objs.Map1 == nil {
|
||||
t.Error("Loading returns an object with nil maps")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypes(t *testing.T) {
|
||||
if testEHOOPY != 0 {
|
||||
t.Error("Expected testEHOOPY to be 0, got", testEHOOPY)
|
||||
}
|
||||
if testEFROOD != 1 {
|
||||
t.Error("Expected testEFROOD to be 0, got", testEFROOD)
|
||||
}
|
||||
|
||||
e := testE(0)
|
||||
if size := unsafe.Sizeof(e); size != 4 {
|
||||
t.Error("Expected size of exampleE to be 4, got", size)
|
||||
}
|
||||
|
||||
bf := testBarfoo{}
|
||||
if size := unsafe.Sizeof(bf); size != 16 {
|
||||
t.Error("Expected size of exampleE to be 16, got", size)
|
||||
}
|
||||
if reflect.TypeOf(bf.Bar).Kind() != reflect.Int64 {
|
||||
t.Error("Expected testBarfoo.Bar to be int64")
|
||||
}
|
||||
if reflect.TypeOf(bf.Baz).Kind() != reflect.Bool {
|
||||
t.Error("Expected testBarfoo.Baz to be bool")
|
||||
}
|
||||
if reflect.TypeOf(bf.Boo) != reflect.TypeOf(e) {
|
||||
t.Error("Expected testBarfoo.Boo to be exampleE")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Package test checks that the code generated by bpf2go conforms to a
|
||||
// specific API.
|
||||
package test
|
||||
|
||||
// $BPF_CLANG and $BPF_CFLAGS are set by the Makefile.
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG test ../testdata/minimal.c
|
||||
@@ -0,0 +1,133 @@
|
||||
// Code generated by bpf2go; DO NOT EDIT.
|
||||
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
type testBarfoo struct {
|
||||
Bar int64
|
||||
Baz bool
|
||||
_ [3]byte
|
||||
Boo testE
|
||||
}
|
||||
|
||||
type testE uint32
|
||||
|
||||
const (
|
||||
testEHOOPY testE = 0
|
||||
testEFROOD testE = 1
|
||||
)
|
||||
|
||||
// loadTest returns the embedded CollectionSpec for test.
|
||||
func loadTest() (*ebpf.CollectionSpec, error) {
|
||||
reader := bytes.NewReader(_TestBytes)
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load test: %w", err)
|
||||
}
|
||||
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// loadTestObjects loads test and converts it into a struct.
|
||||
//
|
||||
// The following types are suitable as obj argument:
|
||||
//
|
||||
// *testObjects
|
||||
// *testPrograms
|
||||
// *testMaps
|
||||
//
|
||||
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||
func loadTestObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
|
||||
spec, err := loadTest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return spec.LoadAndAssign(obj, opts)
|
||||
}
|
||||
|
||||
// testSpecs contains maps and programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testSpecs struct {
|
||||
testProgramSpecs
|
||||
testMapSpecs
|
||||
}
|
||||
|
||||
// testSpecs contains programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testProgramSpecs struct {
|
||||
Filter *ebpf.ProgramSpec `ebpf:"filter"`
|
||||
}
|
||||
|
||||
// testMapSpecs contains maps before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testMapSpecs struct {
|
||||
Map1 *ebpf.MapSpec `ebpf:"map1"`
|
||||
}
|
||||
|
||||
// testObjects contains all objects after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testObjects struct {
|
||||
testPrograms
|
||||
testMaps
|
||||
}
|
||||
|
||||
func (o *testObjects) Close() error {
|
||||
return _TestClose(
|
||||
&o.testPrograms,
|
||||
&o.testMaps,
|
||||
)
|
||||
}
|
||||
|
||||
// testMaps contains all maps after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testMaps struct {
|
||||
Map1 *ebpf.Map `ebpf:"map1"`
|
||||
}
|
||||
|
||||
func (m *testMaps) Close() error {
|
||||
return _TestClose(
|
||||
m.Map1,
|
||||
)
|
||||
}
|
||||
|
||||
// testPrograms contains all programs after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testPrograms struct {
|
||||
Filter *ebpf.Program `ebpf:"filter"`
|
||||
}
|
||||
|
||||
func (p *testPrograms) Close() error {
|
||||
return _TestClose(
|
||||
p.Filter,
|
||||
)
|
||||
}
|
||||
|
||||
func _TestClose(closers ...io.Closer) error {
|
||||
for _, closer := range closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not access this directly.
|
||||
//
|
||||
//go:embed test_bpfeb.o
|
||||
var _TestBytes []byte
|
||||
Binary file not shown.
@@ -0,0 +1,133 @@
|
||||
// Code generated by bpf2go; DO NOT EDIT.
|
||||
//go:build 386 || amd64 || amd64p32 || arm || arm64 || loong64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
type testBarfoo struct {
|
||||
Bar int64
|
||||
Baz bool
|
||||
_ [3]byte
|
||||
Boo testE
|
||||
}
|
||||
|
||||
type testE uint32
|
||||
|
||||
const (
|
||||
testEHOOPY testE = 0
|
||||
testEFROOD testE = 1
|
||||
)
|
||||
|
||||
// loadTest returns the embedded CollectionSpec for test.
|
||||
func loadTest() (*ebpf.CollectionSpec, error) {
|
||||
reader := bytes.NewReader(_TestBytes)
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load test: %w", err)
|
||||
}
|
||||
|
||||
return spec, err
|
||||
}
|
||||
|
||||
// loadTestObjects loads test and converts it into a struct.
|
||||
//
|
||||
// The following types are suitable as obj argument:
|
||||
//
|
||||
// *testObjects
|
||||
// *testPrograms
|
||||
// *testMaps
|
||||
//
|
||||
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
|
||||
func loadTestObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
|
||||
spec, err := loadTest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return spec.LoadAndAssign(obj, opts)
|
||||
}
|
||||
|
||||
// testSpecs contains maps and programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testSpecs struct {
|
||||
testProgramSpecs
|
||||
testMapSpecs
|
||||
}
|
||||
|
||||
// testSpecs contains programs before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testProgramSpecs struct {
|
||||
Filter *ebpf.ProgramSpec `ebpf:"filter"`
|
||||
}
|
||||
|
||||
// testMapSpecs contains maps before they are loaded into the kernel.
|
||||
//
|
||||
// It can be passed ebpf.CollectionSpec.Assign.
|
||||
type testMapSpecs struct {
|
||||
Map1 *ebpf.MapSpec `ebpf:"map1"`
|
||||
}
|
||||
|
||||
// testObjects contains all objects after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testObjects struct {
|
||||
testPrograms
|
||||
testMaps
|
||||
}
|
||||
|
||||
func (o *testObjects) Close() error {
|
||||
return _TestClose(
|
||||
&o.testPrograms,
|
||||
&o.testMaps,
|
||||
)
|
||||
}
|
||||
|
||||
// testMaps contains all maps after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testMaps struct {
|
||||
Map1 *ebpf.Map `ebpf:"map1"`
|
||||
}
|
||||
|
||||
func (m *testMaps) Close() error {
|
||||
return _TestClose(
|
||||
m.Map1,
|
||||
)
|
||||
}
|
||||
|
||||
// testPrograms contains all programs after they have been loaded into the kernel.
|
||||
//
|
||||
// It can be passed to loadTestObjects or ebpf.CollectionSpec.LoadAndAssign.
|
||||
type testPrograms struct {
|
||||
Filter *ebpf.Program `ebpf:"filter"`
|
||||
}
|
||||
|
||||
func (p *testPrograms) Close() error {
|
||||
return _TestClose(
|
||||
p.Filter,
|
||||
)
|
||||
}
|
||||
|
||||
func _TestClose(closers ...io.Closer) error {
|
||||
for _, closer := range closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not access this directly.
|
||||
//
|
||||
//go:embed test_bpfel.o
|
||||
var _TestBytes []byte
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
#include "../../../testdata/common.h"
|
||||
|
||||
char __license[] __section("license") = "MIT";
|
||||
|
||||
enum e { HOOPY, FROOD };
|
||||
|
||||
typedef long long int longint;
|
||||
|
||||
typedef struct {
|
||||
longint bar;
|
||||
_Bool baz;
|
||||
enum e boo;
|
||||
} barfoo;
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__type(key, enum e);
|
||||
__type(value, barfoo);
|
||||
__uint(max_entries, 1);
|
||||
} map1 __section(".maps");
|
||||
|
||||
volatile const enum e my_constant = FROOD;
|
||||
|
||||
volatile const barfoo struct_const;
|
||||
|
||||
__section("socket") int filter() {
|
||||
return my_constant + struct_const.bar;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func splitCFlagsFromArgs(in []string) (args, cflags []string) {
|
||||
for i, arg := range in {
|
||||
if arg == "--" {
|
||||
return in[:i], in[i+1:]
|
||||
}
|
||||
}
|
||||
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func splitArguments(in string) ([]string, error) {
|
||||
var (
|
||||
result []string
|
||||
builder strings.Builder
|
||||
escaped bool
|
||||
delim = ' '
|
||||
)
|
||||
|
||||
for _, r := range strings.TrimSpace(in) {
|
||||
if escaped {
|
||||
builder.WriteRune(r)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
switch r {
|
||||
case '\\':
|
||||
escaped = true
|
||||
|
||||
case delim:
|
||||
current := builder.String()
|
||||
builder.Reset()
|
||||
|
||||
if current != "" || delim != ' ' {
|
||||
// Only append empty words if they are not
|
||||
// delimited by spaces
|
||||
result = append(result, current)
|
||||
}
|
||||
delim = ' '
|
||||
|
||||
case '"', '\'', ' ':
|
||||
if delim == ' ' {
|
||||
delim = r
|
||||
continue
|
||||
}
|
||||
|
||||
fallthrough
|
||||
|
||||
default:
|
||||
builder.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
if delim != ' ' {
|
||||
return nil, fmt.Errorf("missing `%c`", delim)
|
||||
}
|
||||
|
||||
if escaped {
|
||||
return nil, errors.New("unfinished escape")
|
||||
}
|
||||
|
||||
// Add the last word
|
||||
if builder.Len() > 0 {
|
||||
result = append(result, builder.String())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func toUpperFirst(str string) string {
|
||||
first, n := utf8.DecodeRuneInString(str)
|
||||
return string(unicode.ToUpper(first)) + str[n:]
|
||||
}
|
||||
|
||||
func currentModule() string {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
// Fall back to constant since bazel doesn't support BuildInfo.
|
||||
return "github.com/cilium/ebpf"
|
||||
}
|
||||
|
||||
return bi.Main.Path
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitArguments(t *testing.T) {
|
||||
testcases := []struct {
|
||||
in string
|
||||
out []string
|
||||
}{
|
||||
{`foo`, []string{"foo"}},
|
||||
{`foo bar`, []string{"foo", "bar"}},
|
||||
{`foo bar`, []string{"foo", "bar"}},
|
||||
{`\\`, []string{`\`}},
|
||||
{`\\\`, nil},
|
||||
{`foo\ bar`, []string{"foo bar"}},
|
||||
{`foo "" bar`, []string{"foo", "", "bar"}},
|
||||
{`"bar baz"`, []string{"bar baz"}},
|
||||
{`'bar baz'`, []string{"bar baz"}},
|
||||
{`'bar " " baz'`, []string{`bar " " baz`}},
|
||||
{`"bar \" baz"`, []string{`bar " baz`}},
|
||||
{`"`, nil},
|
||||
{`'`, nil},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
have, err := splitArguments(testcase.in)
|
||||
if testcase.out == nil {
|
||||
if err == nil {
|
||||
t.Errorf("Test should fail for: %s", testcase.in)
|
||||
}
|
||||
} else if !reflect.DeepEqual(testcase.out, have) {
|
||||
t.Logf("Have: %q\n", have)
|
||||
t.Logf("Want: %q\n", testcase.out)
|
||||
t.Errorf("Test fails for: %s", testcase.in)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,841 @@
|
||||
package ebpf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/cilium/ebpf/asm"
|
||||
"github.com/cilium/ebpf/btf"
|
||||
"github.com/cilium/ebpf/internal"
|
||||
"github.com/cilium/ebpf/internal/kconfig"
|
||||
)
|
||||
|
||||
// CollectionOptions control loading a collection into the kernel.
|
||||
//
|
||||
// Maps and Programs are passed to NewMapWithOptions and NewProgramsWithOptions.
|
||||
type CollectionOptions struct {
|
||||
Maps MapOptions
|
||||
Programs ProgramOptions
|
||||
|
||||
// MapReplacements takes a set of Maps that will be used instead of
|
||||
// creating new ones when loading the CollectionSpec.
|
||||
//
|
||||
// For each given Map, there must be a corresponding MapSpec in
|
||||
// CollectionSpec.Maps, and its type, key/value size, max entries and flags
|
||||
// must match the values of the MapSpec.
|
||||
//
|
||||
// The given Maps are Clone()d before being used in the Collection, so the
|
||||
// caller can Close() them freely when they are no longer needed.
|
||||
MapReplacements map[string]*Map
|
||||
}
|
||||
|
||||
// CollectionSpec describes a collection.
|
||||
type CollectionSpec struct {
|
||||
Maps map[string]*MapSpec
|
||||
Programs map[string]*ProgramSpec
|
||||
|
||||
// Types holds type information about Maps and Programs.
|
||||
// Modifications to Types are currently undefined behaviour.
|
||||
Types *btf.Spec
|
||||
|
||||
// ByteOrder specifies whether the ELF was compiled for
|
||||
// big-endian or little-endian architectures.
|
||||
ByteOrder binary.ByteOrder
|
||||
}
|
||||
|
||||
// Copy returns a recursive copy of the spec.
|
||||
func (cs *CollectionSpec) Copy() *CollectionSpec {
|
||||
if cs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cpy := CollectionSpec{
|
||||
Maps: make(map[string]*MapSpec, len(cs.Maps)),
|
||||
Programs: make(map[string]*ProgramSpec, len(cs.Programs)),
|
||||
ByteOrder: cs.ByteOrder,
|
||||
Types: cs.Types,
|
||||
}
|
||||
|
||||
for name, spec := range cs.Maps {
|
||||
cpy.Maps[name] = spec.Copy()
|
||||
}
|
||||
|
||||
for name, spec := range cs.Programs {
|
||||
cpy.Programs[name] = spec.Copy()
|
||||
}
|
||||
|
||||
return &cpy
|
||||
}
|
||||
|
||||
// RewriteMaps replaces all references to specific maps.
|
||||
//
|
||||
// Use this function to use pre-existing maps instead of creating new ones
|
||||
// when calling NewCollection. Any named maps are removed from CollectionSpec.Maps.
|
||||
//
|
||||
// Returns an error if a named map isn't used in at least one program.
|
||||
//
|
||||
// Deprecated: Pass CollectionOptions.MapReplacements when loading the Collection
|
||||
// instead.
|
||||
func (cs *CollectionSpec) RewriteMaps(maps map[string]*Map) error {
|
||||
for symbol, m := range maps {
|
||||
// have we seen a program that uses this symbol / map
|
||||
seen := false
|
||||
for progName, progSpec := range cs.Programs {
|
||||
err := progSpec.Instructions.AssociateMap(symbol, m)
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
seen = true
|
||||
|
||||
case errors.Is(err, asm.ErrUnreferencedSymbol):
|
||||
// Not all programs need to use the map
|
||||
|
||||
default:
|
||||
return fmt.Errorf("program %s: %w", progName, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !seen {
|
||||
return fmt.Errorf("map %s not referenced by any programs", symbol)
|
||||
}
|
||||
|
||||
// Prevent NewCollection from creating rewritten maps
|
||||
delete(cs.Maps, symbol)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MissingConstantsError is returned by [CollectionSpec.RewriteConstants].
|
||||
type MissingConstantsError struct {
|
||||
// The constants missing from .rodata.
|
||||
Constants []string
|
||||
}
|
||||
|
||||
func (m *MissingConstantsError) Error() string {
|
||||
return fmt.Sprintf("some constants are missing from .rodata: %s", strings.Join(m.Constants, ", "))
|
||||
}
|
||||
|
||||
// RewriteConstants replaces the value of multiple constants.
|
||||
//
|
||||
// The constant must be defined like so in the C program:
|
||||
//
|
||||
// volatile const type foobar;
|
||||
// volatile const type foobar = default;
|
||||
//
|
||||
// Replacement values must be of the same length as the C sizeof(type).
|
||||
// If necessary, they are marshalled according to the same rules as
|
||||
// map values.
|
||||
//
|
||||
// From Linux 5.5 the verifier will use constants to eliminate dead code.
|
||||
//
|
||||
// Returns an error wrapping [MissingConstantsError] if a constant doesn't exist.
|
||||
func (cs *CollectionSpec) RewriteConstants(consts map[string]interface{}) error {
|
||||
replaced := make(map[string]bool)
|
||||
|
||||
for name, spec := range cs.Maps {
|
||||
if !strings.HasPrefix(name, ".rodata") {
|
||||
continue
|
||||
}
|
||||
|
||||
b, ds, err := spec.dataSection()
|
||||
if errors.Is(err, errMapNoBTFValue) {
|
||||
// Data sections without a BTF Datasec are valid, but don't support
|
||||
// constant replacements.
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("map %s: %w", name, err)
|
||||
}
|
||||
|
||||
// MapSpec.Copy() performs a shallow copy. Fully copy the byte slice
|
||||
// to avoid any changes affecting other copies of the MapSpec.
|
||||
cpy := make([]byte, len(b))
|
||||
copy(cpy, b)
|
||||
|
||||
for _, v := range ds.Vars {
|
||||
vname := v.Type.TypeName()
|
||||
replacement, ok := consts[vname]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := v.Type.(*btf.Var); !ok {
|
||||
return fmt.Errorf("section %s: unexpected type %T for variable %s", name, v.Type, vname)
|
||||
}
|
||||
|
||||
if replaced[vname] {
|
||||
return fmt.Errorf("section %s: duplicate variable %s", name, vname)
|
||||
}
|
||||
|
||||
if int(v.Offset+v.Size) > len(cpy) {
|
||||
return fmt.Errorf("section %s: offset %d(+%d) for variable %s is out of bounds", name, v.Offset, v.Size, vname)
|
||||
}
|
||||
|
||||
b, err := marshalBytes(replacement, int(v.Size))
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling constant replacement %s: %w", vname, err)
|
||||
}
|
||||
|
||||
copy(cpy[v.Offset:v.Offset+v.Size], b)
|
||||
|
||||
replaced[vname] = true
|
||||
}
|
||||
|
||||
spec.Contents[0] = MapKV{Key: uint32(0), Value: cpy}
|
||||
}
|
||||
|
||||
var missing []string
|
||||
for c := range consts {
|
||||
if !replaced[c] {
|
||||
missing = append(missing, c)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) != 0 {
|
||||
return fmt.Errorf("rewrite constants: %w", &MissingConstantsError{Constants: missing})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Assign the contents of a CollectionSpec to a struct.
|
||||
//
|
||||
// This function is a shortcut to manually checking the presence
|
||||
// of maps and programs in a CollectionSpec. Consider using bpf2go
|
||||
// if this sounds useful.
|
||||
//
|
||||
// 'to' must be a pointer to a struct. A field of the
|
||||
// struct is updated with values from Programs or Maps if it
|
||||
// has an `ebpf` tag and its type is *ProgramSpec or *MapSpec.
|
||||
// The tag's value specifies the name of the program or map as
|
||||
// found in the CollectionSpec.
|
||||
//
|
||||
// struct {
|
||||
// Foo *ebpf.ProgramSpec `ebpf:"xdp_foo"`
|
||||
// Bar *ebpf.MapSpec `ebpf:"bar_map"`
|
||||
// Ignored int
|
||||
// }
|
||||
//
|
||||
// Returns an error if any of the eBPF objects can't be found, or
|
||||
// if the same MapSpec or ProgramSpec is assigned multiple times.
|
||||
func (cs *CollectionSpec) Assign(to interface{}) error {
|
||||
// Assign() only supports assigning ProgramSpecs and MapSpecs,
|
||||
// so doesn't load any resources into the kernel.
|
||||
getValue := func(typ reflect.Type, name string) (interface{}, error) {
|
||||
switch typ {
|
||||
|
||||
case reflect.TypeOf((*ProgramSpec)(nil)):
|
||||
if p := cs.Programs[name]; p != nil {
|
||||
return p, nil
|
||||
}
|
||||
return nil, fmt.Errorf("missing program %q", name)
|
||||
|
||||
case reflect.TypeOf((*MapSpec)(nil)):
|
||||
if m := cs.Maps[name]; m != nil {
|
||||
return m, nil
|
||||
}
|
||||
return nil, fmt.Errorf("missing map %q", name)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type %s", typ)
|
||||
}
|
||||
}
|
||||
|
||||
return assignValues(to, getValue)
|
||||
}
|
||||
|
||||
// LoadAndAssign loads Maps and Programs into the kernel and assigns them
|
||||
// to a struct.
|
||||
//
|
||||
// Omitting Map/Program.Close() during application shutdown is an error.
|
||||
// See the package documentation for details around Map and Program lifecycle.
|
||||
//
|
||||
// This function is a shortcut to manually checking the presence
|
||||
// of maps and programs in a CollectionSpec. Consider using bpf2go
|
||||
// if this sounds useful.
|
||||
//
|
||||
// 'to' must be a pointer to a struct. A field of the struct is updated with
|
||||
// a Program or Map if it has an `ebpf` tag and its type is *Program or *Map.
|
||||
// The tag's value specifies the name of the program or map as found in the
|
||||
// CollectionSpec. Before updating the struct, the requested objects and their
|
||||
// dependent resources are loaded into the kernel and populated with values if
|
||||
// specified.
|
||||
//
|
||||
// struct {
|
||||
// Foo *ebpf.Program `ebpf:"xdp_foo"`
|
||||
// Bar *ebpf.Map `ebpf:"bar_map"`
|
||||
// Ignored int
|
||||
// }
|
||||
//
|
||||
// opts may be nil.
|
||||
//
|
||||
// Returns an error if any of the fields can't be found, or
|
||||
// if the same Map or Program is assigned multiple times.
|
||||
func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions) error {
|
||||
loader, err := newCollectionLoader(cs, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer loader.close()
|
||||
|
||||
// Support assigning Programs and Maps, lazy-loading the required objects.
|
||||
assignedMaps := make(map[string]bool)
|
||||
assignedProgs := make(map[string]bool)
|
||||
|
||||
getValue := func(typ reflect.Type, name string) (interface{}, error) {
|
||||
switch typ {
|
||||
|
||||
case reflect.TypeOf((*Program)(nil)):
|
||||
assignedProgs[name] = true
|
||||
return loader.loadProgram(name)
|
||||
|
||||
case reflect.TypeOf((*Map)(nil)):
|
||||
assignedMaps[name] = true
|
||||
return loader.loadMap(name)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type %s", typ)
|
||||
}
|
||||
}
|
||||
|
||||
// Load the Maps and Programs requested by the annotated struct.
|
||||
if err := assignValues(to, getValue); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Populate the requested maps. Has a chance of lazy-loading other dependent maps.
|
||||
if err := loader.populateMaps(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Evaluate the loader's objects after all (lazy)loading has taken place.
|
||||
for n, m := range loader.maps {
|
||||
switch m.typ {
|
||||
case ProgramArray:
|
||||
// Require all lazy-loaded ProgramArrays to be assigned to the given object.
|
||||
// The kernel empties a ProgramArray once the last user space reference
|
||||
// to it closes, which leads to failed tail calls. Combined with the library
|
||||
// closing map fds via GC finalizers this can lead to surprising behaviour.
|
||||
// Only allow unassigned ProgramArrays when the library hasn't pre-populated
|
||||
// any entries from static value declarations. At this point, we know the map
|
||||
// is empty and there's no way for the caller to interact with the map going
|
||||
// forward.
|
||||
if !assignedMaps[n] && len(cs.Maps[n].Contents) > 0 {
|
||||
return fmt.Errorf("ProgramArray %s must be assigned to prevent missed tail calls", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent loader.cleanup() from closing assigned Maps and Programs.
|
||||
for m := range assignedMaps {
|
||||
delete(loader.maps, m)
|
||||
}
|
||||
for p := range assignedProgs {
|
||||
delete(loader.programs, p)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collection is a collection of Programs and Maps associated
|
||||
// with their symbols
|
||||
type Collection struct {
|
||||
Programs map[string]*Program
|
||||
Maps map[string]*Map
|
||||
}
|
||||
|
||||
// NewCollection creates a Collection from the given spec, creating and
|
||||
// loading its declared resources into the kernel.
|
||||
//
|
||||
// Omitting Collection.Close() during application shutdown is an error.
|
||||
// See the package documentation for details around Map and Program lifecycle.
|
||||
func NewCollection(spec *CollectionSpec) (*Collection, error) {
|
||||
return NewCollectionWithOptions(spec, CollectionOptions{})
|
||||
}
|
||||
|
||||
// NewCollectionWithOptions creates a Collection from the given spec using
|
||||
// options, creating and loading its declared resources into the kernel.
|
||||
//
|
||||
// Omitting Collection.Close() during application shutdown is an error.
|
||||
// See the package documentation for details around Map and Program lifecycle.
|
||||
func NewCollectionWithOptions(spec *CollectionSpec, opts CollectionOptions) (*Collection, error) {
|
||||
loader, err := newCollectionLoader(spec, &opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer loader.close()
|
||||
|
||||
// Create maps first, as their fds need to be linked into programs.
|
||||
for mapName := range spec.Maps {
|
||||
if _, err := loader.loadMap(mapName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for progName, prog := range spec.Programs {
|
||||
if prog.Type == UnspecifiedProgram {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := loader.loadProgram(progName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Maps can contain Program and Map stubs, so populate them after
|
||||
// all Maps and Programs have been successfully loaded.
|
||||
if err := loader.populateMaps(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prevent loader.cleanup from closing maps and programs.
|
||||
maps, progs := loader.maps, loader.programs
|
||||
loader.maps, loader.programs = nil, nil
|
||||
|
||||
return &Collection{
|
||||
progs,
|
||||
maps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type collectionLoader struct {
|
||||
coll *CollectionSpec
|
||||
opts *CollectionOptions
|
||||
maps map[string]*Map
|
||||
programs map[string]*Program
|
||||
}
|
||||
|
||||
func newCollectionLoader(coll *CollectionSpec, opts *CollectionOptions) (*collectionLoader, error) {
|
||||
if opts == nil {
|
||||
opts = &CollectionOptions{}
|
||||
}
|
||||
|
||||
// Check for existing MapSpecs in the CollectionSpec for all provided replacement maps.
|
||||
for name, m := range opts.MapReplacements {
|
||||
spec, ok := coll.Maps[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("replacement map %s not found in CollectionSpec", name)
|
||||
}
|
||||
|
||||
if err := spec.Compatible(m); err != nil {
|
||||
return nil, fmt.Errorf("using replacement map %s: %w", spec.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &collectionLoader{
|
||||
coll,
|
||||
opts,
|
||||
make(map[string]*Map),
|
||||
make(map[string]*Program),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// close all resources left over in the collectionLoader.
|
||||
func (cl *collectionLoader) close() {
|
||||
for _, m := range cl.maps {
|
||||
m.Close()
|
||||
}
|
||||
for _, p := range cl.programs {
|
||||
p.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (cl *collectionLoader) loadMap(mapName string) (*Map, error) {
|
||||
if m := cl.maps[mapName]; m != nil {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
mapSpec := cl.coll.Maps[mapName]
|
||||
if mapSpec == nil {
|
||||
return nil, fmt.Errorf("missing map %s", mapName)
|
||||
}
|
||||
|
||||
if replaceMap, ok := cl.opts.MapReplacements[mapName]; ok {
|
||||
// Clone the map to avoid closing user's map later on.
|
||||
m, err := replaceMap.Clone()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cl.maps[mapName] = m
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m, err := newMapWithOptions(mapSpec, cl.opts.Maps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("map %s: %w", mapName, err)
|
||||
}
|
||||
|
||||
cl.maps[mapName] = m
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (cl *collectionLoader) loadProgram(progName string) (*Program, error) {
|
||||
if prog := cl.programs[progName]; prog != nil {
|
||||
return prog, nil
|
||||
}
|
||||
|
||||
progSpec := cl.coll.Programs[progName]
|
||||
if progSpec == nil {
|
||||
return nil, fmt.Errorf("unknown program %s", progName)
|
||||
}
|
||||
|
||||
// Bail out early if we know the kernel is going to reject the program.
|
||||
// This skips loading map dependencies, saving some cleanup work later.
|
||||
if progSpec.Type == UnspecifiedProgram {
|
||||
return nil, fmt.Errorf("cannot load program %s: program type is unspecified", progName)
|
||||
}
|
||||
|
||||
progSpec = progSpec.Copy()
|
||||
|
||||
// Rewrite any reference to a valid map in the program's instructions,
|
||||
// which includes all of its dependencies.
|
||||
for i := range progSpec.Instructions {
|
||||
ins := &progSpec.Instructions[i]
|
||||
|
||||
if !ins.IsLoadFromMap() || ins.Reference() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Don't overwrite map loads containing non-zero map fd's,
|
||||
// they can be manually included by the caller.
|
||||
// Map FDs/IDs are placed in the lower 32 bits of Constant.
|
||||
if int32(ins.Constant) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
m, err := cl.loadMap(ins.Reference())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("program %s: %w", progName, err)
|
||||
}
|
||||
|
||||
if err := ins.AssociateMap(m); err != nil {
|
||||
return nil, fmt.Errorf("program %s: map %s: %w", progName, ins.Reference(), err)
|
||||
}
|
||||
}
|
||||
|
||||
prog, err := newProgramWithOptions(progSpec, cl.opts.Programs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("program %s: %w", progName, err)
|
||||
}
|
||||
|
||||
cl.programs[progName] = prog
|
||||
return prog, nil
|
||||
}
|
||||
|
||||
func (cl *collectionLoader) populateMaps() error {
|
||||
for mapName, m := range cl.maps {
|
||||
mapSpec, ok := cl.coll.Maps[mapName]
|
||||
if !ok {
|
||||
return fmt.Errorf("missing map spec %s", mapName)
|
||||
}
|
||||
|
||||
// MapSpecs that refer to inner maps or programs within the same
|
||||
// CollectionSpec do so using strings. These strings are used as the key
|
||||
// to look up the respective object in the Maps or Programs fields.
|
||||
// Resolve those references to actual Map or Program resources that
|
||||
// have been loaded into the kernel.
|
||||
if mapSpec.Type.canStoreMap() || mapSpec.Type.canStoreProgram() {
|
||||
mapSpec = mapSpec.Copy()
|
||||
|
||||
for i, kv := range mapSpec.Contents {
|
||||
objName, ok := kv.Value.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch t := mapSpec.Type; {
|
||||
case t.canStoreProgram():
|
||||
// loadProgram is idempotent and could return an existing Program.
|
||||
prog, err := cl.loadProgram(objName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading program %s, for map %s: %w", objName, mapName, err)
|
||||
}
|
||||
mapSpec.Contents[i] = MapKV{kv.Key, prog}
|
||||
|
||||
case t.canStoreMap():
|
||||
// loadMap is idempotent and could return an existing Map.
|
||||
innerMap, err := cl.loadMap(objName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading inner map %s, for map %s: %w", objName, mapName, err)
|
||||
}
|
||||
mapSpec.Contents[i] = MapKV{kv.Key, innerMap}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate and freeze the map if specified.
|
||||
if err := m.finalize(mapSpec); err != nil {
|
||||
return fmt.Errorf("populating map %s: %w", mapName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveKconfig resolves all variables declared in .kconfig and populates
|
||||
// m.Contents. Does nothing if the given m.Contents is non-empty.
|
||||
func resolveKconfig(m *MapSpec) error {
|
||||
ds, ok := m.Value.(*btf.Datasec)
|
||||
if !ok {
|
||||
return errors.New("map value is not a Datasec")
|
||||
}
|
||||
|
||||
type configInfo struct {
|
||||
offset uint32
|
||||
typ btf.Type
|
||||
}
|
||||
|
||||
configs := make(map[string]configInfo)
|
||||
|
||||
data := make([]byte, ds.Size)
|
||||
for _, vsi := range ds.Vars {
|
||||
v := vsi.Type.(*btf.Var)
|
||||
n := v.TypeName()
|
||||
|
||||
switch n {
|
||||
case "LINUX_KERNEL_VERSION":
|
||||
if integer, ok := v.Type.(*btf.Int); !ok || integer.Size != 4 {
|
||||
return fmt.Errorf("variable %s must be a 32 bits integer, got %s", n, v.Type)
|
||||
}
|
||||
|
||||
kv, err := internal.KernelVersion()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting kernel version: %w", err)
|
||||
}
|
||||
internal.NativeEndian.PutUint32(data[vsi.Offset:], kv.Kernel())
|
||||
|
||||
case "LINUX_HAS_SYSCALL_WRAPPER":
|
||||
if integer, ok := v.Type.(*btf.Int); !ok || integer.Size != 4 {
|
||||
return fmt.Errorf("variable %s must be a 32 bits integer, got %s", n, v.Type)
|
||||
}
|
||||
var value uint32 = 1
|
||||
if err := haveSyscallWrapper(); errors.Is(err, ErrNotSupported) {
|
||||
value = 0
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("unable to derive a value for LINUX_HAS_SYSCALL_WRAPPER: %w", err)
|
||||
}
|
||||
|
||||
internal.NativeEndian.PutUint32(data[vsi.Offset:], value)
|
||||
|
||||
default: // Catch CONFIG_*.
|
||||
configs[n] = configInfo{
|
||||
offset: vsi.Offset,
|
||||
typ: v.Type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We only parse kconfig file if a CONFIG_* variable was found.
|
||||
if len(configs) > 0 {
|
||||
f, err := kconfig.Find()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot find a kconfig file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
filter := make(map[string]struct{}, len(configs))
|
||||
for config := range configs {
|
||||
filter[config] = struct{}{}
|
||||
}
|
||||
|
||||
kernelConfig, err := kconfig.Parse(f, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse kconfig file: %w", err)
|
||||
}
|
||||
|
||||
for n, info := range configs {
|
||||
value, ok := kernelConfig[n]
|
||||
if !ok {
|
||||
return fmt.Errorf("config option %q does not exists for this kernel", n)
|
||||
}
|
||||
|
||||
err := kconfig.PutValue(data[info.offset:], info.typ, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("problem adding value for %s: %w", n, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.Contents = []MapKV{{uint32(0), data}}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadCollection reads an object file and creates and loads its declared
|
||||
// resources into the kernel.
|
||||
//
|
||||
// Omitting Collection.Close() during application shutdown is an error.
|
||||
// See the package documentation for details around Map and Program lifecycle.
|
||||
func LoadCollection(file string) (*Collection, error) {
|
||||
spec, err := LoadCollectionSpec(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewCollection(spec)
|
||||
}
|
||||
|
||||
// Close frees all maps and programs associated with the collection.
|
||||
//
|
||||
// The collection mustn't be used afterwards.
|
||||
func (coll *Collection) Close() {
|
||||
for _, prog := range coll.Programs {
|
||||
prog.Close()
|
||||
}
|
||||
for _, m := range coll.Maps {
|
||||
m.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// DetachMap removes the named map from the Collection.
|
||||
//
|
||||
// This means that a later call to Close() will not affect this map.
|
||||
//
|
||||
// Returns nil if no map of that name exists.
|
||||
func (coll *Collection) DetachMap(name string) *Map {
|
||||
m := coll.Maps[name]
|
||||
delete(coll.Maps, name)
|
||||
return m
|
||||
}
|
||||
|
||||
// DetachProgram removes the named program from the Collection.
|
||||
//
|
||||
// This means that a later call to Close() will not affect this program.
|
||||
//
|
||||
// Returns nil if no program of that name exists.
|
||||
func (coll *Collection) DetachProgram(name string) *Program {
|
||||
p := coll.Programs[name]
|
||||
delete(coll.Programs, name)
|
||||
return p
|
||||
}
|
||||
|
||||
// structField represents a struct field containing the ebpf struct tag.
|
||||
type structField struct {
|
||||
reflect.StructField
|
||||
value reflect.Value
|
||||
}
|
||||
|
||||
// ebpfFields extracts field names tagged with 'ebpf' from a struct type.
|
||||
// Keep track of visited types to avoid infinite recursion.
|
||||
func ebpfFields(structVal reflect.Value, visited map[reflect.Type]bool) ([]structField, error) {
|
||||
if visited == nil {
|
||||
visited = make(map[reflect.Type]bool)
|
||||
}
|
||||
|
||||
structType := structVal.Type()
|
||||
if structType.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("%s is not a struct", structType)
|
||||
}
|
||||
|
||||
if visited[structType] {
|
||||
return nil, fmt.Errorf("recursion on type %s", structType)
|
||||
}
|
||||
|
||||
fields := make([]structField, 0, structType.NumField())
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
field := structField{structType.Field(i), structVal.Field(i)}
|
||||
|
||||
// If the field is tagged, gather it and move on.
|
||||
name := field.Tag.Get("ebpf")
|
||||
if name != "" {
|
||||
fields = append(fields, field)
|
||||
continue
|
||||
}
|
||||
|
||||
// If the field does not have an ebpf tag, but is a struct or a pointer
|
||||
// to a struct, attempt to gather its fields as well.
|
||||
var v reflect.Value
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Ptr:
|
||||
if field.Type.Elem().Kind() != reflect.Struct {
|
||||
continue
|
||||
}
|
||||
|
||||
if field.value.IsNil() {
|
||||
return nil, fmt.Errorf("nil pointer to %s", structType)
|
||||
}
|
||||
|
||||
// Obtain the destination type of the pointer.
|
||||
v = field.value.Elem()
|
||||
|
||||
case reflect.Struct:
|
||||
// Reference the value's type directly.
|
||||
v = field.value
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
inner, err := ebpfFields(v, visited)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("field %s: %w", field.Name, err)
|
||||
}
|
||||
|
||||
fields = append(fields, inner...)
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
// assignValues attempts to populate all fields of 'to' tagged with 'ebpf'.
|
||||
//
|
||||
// getValue is called for every tagged field of 'to' and must return the value
|
||||
// to be assigned to the field with the given typ and name.
|
||||
func assignValues(to interface{},
|
||||
getValue func(typ reflect.Type, name string) (interface{}, error)) error {
|
||||
|
||||
toValue := reflect.ValueOf(to)
|
||||
if toValue.Type().Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("%T is not a pointer to struct", to)
|
||||
}
|
||||
|
||||
if toValue.IsNil() {
|
||||
return fmt.Errorf("nil pointer to %T", to)
|
||||
}
|
||||
|
||||
fields, err := ebpfFields(toValue.Elem(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type elem struct {
|
||||
// Either *Map or *Program
|
||||
typ reflect.Type
|
||||
name string
|
||||
}
|
||||
|
||||
assigned := make(map[elem]string)
|
||||
for _, field := range fields {
|
||||
// Get string value the field is tagged with.
|
||||
tag := field.Tag.Get("ebpf")
|
||||
if strings.Contains(tag, ",") {
|
||||
return fmt.Errorf("field %s: ebpf tag contains a comma", field.Name)
|
||||
}
|
||||
|
||||
// Check if the eBPF object with the requested
|
||||
// type and tag was already assigned elsewhere.
|
||||
e := elem{field.Type, tag}
|
||||
if af := assigned[e]; af != "" {
|
||||
return fmt.Errorf("field %s: object %q was already assigned to %s", field.Name, tag, af)
|
||||
}
|
||||
|
||||
// Get the eBPF object referred to by the tag.
|
||||
value, err := getValue(field.Type, tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("field %s: %w", field.Name, err)
|
||||
}
|
||||
|
||||
if !field.value.CanSet() {
|
||||
return fmt.Errorf("field %s: can't set value", field.Name)
|
||||
}
|
||||
field.value.Set(reflect.ValueOf(value))
|
||||
|
||||
assigned[e] = field.Name
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
package ebpf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cilium/ebpf/asm"
|
||||
"github.com/cilium/ebpf/btf"
|
||||
"github.com/cilium/ebpf/internal"
|
||||
"github.com/cilium/ebpf/internal/testutils"
|
||||
"github.com/cilium/ebpf/internal/testutils/fdtrace"
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
fdtrace.TestMain(m)
|
||||
}
|
||||
|
||||
func TestCollectionSpecNotModified(t *testing.T) {
|
||||
cs := CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
"my-map": {
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
},
|
||||
},
|
||||
Programs: map[string]*ProgramSpec{
|
||||
"test": {
|
||||
Type: SocketFilter,
|
||||
Instructions: asm.Instructions{
|
||||
asm.LoadImm(asm.R1, 0, asm.DWord).WithReference("my-map"),
|
||||
asm.LoadImm(asm.R0, 0, asm.DWord),
|
||||
asm.Return(),
|
||||
},
|
||||
License: "MIT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
coll, err := NewCollection(&cs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
coll.Close()
|
||||
|
||||
if cs.Programs["test"].Instructions[0].Constant != 0 {
|
||||
t.Error("Creating a collection modifies input spec")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionSpecCopy(t *testing.T) {
|
||||
cs := &CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
"my-map": {
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
},
|
||||
},
|
||||
Programs: map[string]*ProgramSpec{
|
||||
"test": {
|
||||
Type: SocketFilter,
|
||||
Instructions: asm.Instructions{
|
||||
asm.LoadMapPtr(asm.R1, 0),
|
||||
asm.LoadImm(asm.R0, 0, asm.DWord),
|
||||
asm.Return(),
|
||||
},
|
||||
License: "MIT",
|
||||
},
|
||||
},
|
||||
Types: &btf.Spec{},
|
||||
}
|
||||
cpy := cs.Copy()
|
||||
|
||||
if cpy == cs {
|
||||
t.Error("Copy returned the same pointner")
|
||||
}
|
||||
|
||||
if cpy.Maps["my-map"] == cs.Maps["my-map"] {
|
||||
t.Error("Copy returned same Maps")
|
||||
}
|
||||
|
||||
if cpy.Programs["test"] == cs.Programs["test"] {
|
||||
t.Error("Copy returned same Programs")
|
||||
}
|
||||
|
||||
if cpy.Types != cs.Types {
|
||||
t.Error("Copy returned different Types")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionSpecLoadCopy(t *testing.T) {
|
||||
file := fmt.Sprintf("testdata/loader-%s.elf", internal.ClangEndian)
|
||||
spec, err := LoadCollectionSpec(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
spec2 := spec.Copy()
|
||||
|
||||
var objs struct {
|
||||
Prog *Program `ebpf:"xdp_prog"`
|
||||
}
|
||||
|
||||
err = spec.LoadAndAssign(&objs, nil)
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal("Loading original spec:", err)
|
||||
}
|
||||
defer objs.Prog.Close()
|
||||
|
||||
if err := spec2.LoadAndAssign(&objs, nil); err != nil {
|
||||
t.Fatal("Loading copied spec:", err)
|
||||
}
|
||||
defer objs.Prog.Close()
|
||||
}
|
||||
|
||||
func TestCollectionSpecRewriteMaps(t *testing.T) {
|
||||
insns := asm.Instructions{
|
||||
// R1 map
|
||||
asm.LoadMapPtr(asm.R1, 0).WithReference("test-map"),
|
||||
// R2 key
|
||||
asm.Mov.Reg(asm.R2, asm.R10),
|
||||
asm.Add.Imm(asm.R2, -4),
|
||||
asm.StoreImm(asm.R2, 0, 0, asm.Word),
|
||||
// Lookup map[0]
|
||||
asm.FnMapLookupElem.Call(),
|
||||
asm.JEq.Imm(asm.R0, 0, "ret"),
|
||||
asm.LoadMem(asm.R0, asm.R0, 0, asm.Word),
|
||||
asm.Return().WithSymbol("ret"),
|
||||
}
|
||||
|
||||
cs := &CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
"test-map": {
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
},
|
||||
},
|
||||
Programs: map[string]*ProgramSpec{
|
||||
"test-prog": {
|
||||
Type: SocketFilter,
|
||||
Instructions: insns,
|
||||
License: "MIT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Override the map with another one
|
||||
newMap, err := NewMap(cs.Maps["test-map"])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer newMap.Close()
|
||||
|
||||
err = newMap.Put(uint32(0), uint32(2))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = cs.RewriteMaps(map[string]*Map{
|
||||
"test-map": newMap,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if cs.Maps["test-map"] != nil {
|
||||
t.Error("RewriteMaps doesn't remove map from CollectionSpec.Maps")
|
||||
}
|
||||
|
||||
coll, err := NewCollection(cs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer coll.Close()
|
||||
|
||||
ret, _, err := coll.Programs["test-prog"].Test(internal.EmptyBPFContext)
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ret != 2 {
|
||||
t.Fatal("new / override map not used")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionSpecMapReplacements(t *testing.T) {
|
||||
insns := asm.Instructions{
|
||||
// R1 map
|
||||
asm.LoadMapPtr(asm.R1, 0).WithReference("test-map"),
|
||||
// R2 key
|
||||
asm.Mov.Reg(asm.R2, asm.R10),
|
||||
asm.Add.Imm(asm.R2, -4),
|
||||
asm.StoreImm(asm.R2, 0, 0, asm.Word),
|
||||
// Lookup map[0]
|
||||
asm.FnMapLookupElem.Call(),
|
||||
asm.JEq.Imm(asm.R0, 0, "ret"),
|
||||
asm.LoadMem(asm.R0, asm.R0, 0, asm.Word),
|
||||
asm.Return().WithSymbol("ret"),
|
||||
}
|
||||
|
||||
cs := &CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
"test-map": {
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
},
|
||||
},
|
||||
Programs: map[string]*ProgramSpec{
|
||||
"test-prog": {
|
||||
Type: SocketFilter,
|
||||
Instructions: insns,
|
||||
License: "MIT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Replace the map with another one
|
||||
newMap, err := NewMap(cs.Maps["test-map"])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer newMap.Close()
|
||||
|
||||
err = newMap.Put(uint32(0), uint32(2))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
coll, err := NewCollectionWithOptions(cs, CollectionOptions{
|
||||
MapReplacements: map[string]*Map{
|
||||
"test-map": newMap,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer coll.Close()
|
||||
|
||||
ret, _, err := coll.Programs["test-prog"].Test(internal.EmptyBPFContext)
|
||||
testutils.SkipIfNotSupported(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ret != 2 {
|
||||
t.Fatal("new / override map not used")
|
||||
}
|
||||
|
||||
// Check that newMap isn't closed when the collection is closed
|
||||
coll.Close()
|
||||
err = newMap.Put(uint32(0), uint32(3))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to update replaced map: %s", err)
|
||||
}
|
||||
}
|
||||
func TestCollectionSpecMapReplacements_NonExistingMap(t *testing.T) {
|
||||
cs := &CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
"test-map": {
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Override non-existing map
|
||||
newMap, err := NewMap(cs.Maps["test-map"])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer newMap.Close()
|
||||
|
||||
coll, err := NewCollectionWithOptions(cs, CollectionOptions{
|
||||
MapReplacements: map[string]*Map{
|
||||
"non-existing-map": newMap,
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
coll.Close()
|
||||
t.Fatal("Overriding a non existing map did not fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionSpecMapReplacements_SpecMismatch(t *testing.T) {
|
||||
cs := &CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
"test-map": {
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Override map with mismatching spec
|
||||
newMap, err := NewMap(&MapSpec{
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 8, // this is different
|
||||
MaxEntries: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Map fd is duplicated by MapReplacements, this one can be safely closed.
|
||||
defer newMap.Close()
|
||||
|
||||
coll, err := NewCollectionWithOptions(cs, CollectionOptions{
|
||||
MapReplacements: map[string]*Map{
|
||||
"test-map": newMap,
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
coll.Close()
|
||||
t.Fatal("Overriding a map with a mismatching spec did not fail")
|
||||
}
|
||||
if !errors.Is(err, ErrMapIncompatible) {
|
||||
t.Fatalf("Overriding a map with a mismatching spec failed with the wrong error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionRewriteConstants(t *testing.T) {
|
||||
cs := &CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
".rodata": {
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
Value: &btf.Datasec{
|
||||
Vars: []btf.VarSecinfo{
|
||||
{
|
||||
Type: &btf.Var{
|
||||
Name: "the_constant",
|
||||
Type: &btf.Int{Size: 4},
|
||||
},
|
||||
Offset: 0,
|
||||
Size: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
Contents: []MapKV{
|
||||
{Key: uint32(0), Value: []byte{1, 1, 1, 1}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := cs.RewriteConstants(map[string]interface{}{
|
||||
"fake_constant_one": uint32(1),
|
||||
"fake_constant_two": uint32(2),
|
||||
})
|
||||
qt.Assert(t, err, qt.IsNotNil, qt.Commentf("RewriteConstants did not fail"))
|
||||
|
||||
var mErr *MissingConstantsError
|
||||
if !errors.As(err, &mErr) {
|
||||
t.Fatal("Error doesn't wrap MissingConstantsError:", err)
|
||||
}
|
||||
qt.Assert(t, mErr.Constants, qt.ContentEquals, []string{"fake_constant_one", "fake_constant_two"})
|
||||
|
||||
err = cs.RewriteConstants(map[string]interface{}{
|
||||
"the_constant": uint32(0x42424242),
|
||||
})
|
||||
qt.Assert(t, err, qt.IsNil)
|
||||
qt.Assert(t, cs.Maps[".rodata"].Contents[0].Value, qt.ContentEquals, []byte{0x42, 0x42, 0x42, 0x42})
|
||||
}
|
||||
|
||||
func TestCollectionSpec_LoadAndAssign_LazyLoading(t *testing.T) {
|
||||
spec := &CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
"valid": {
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
},
|
||||
"bogus": {
|
||||
Type: Array,
|
||||
MaxEntries: 0,
|
||||
},
|
||||
},
|
||||
Programs: map[string]*ProgramSpec{
|
||||
"valid": {
|
||||
Type: SocketFilter,
|
||||
Instructions: asm.Instructions{
|
||||
asm.LoadImm(asm.R0, 0, asm.DWord),
|
||||
asm.Return(),
|
||||
},
|
||||
License: "MIT",
|
||||
},
|
||||
"bogus": {
|
||||
Type: SocketFilter,
|
||||
Instructions: asm.Instructions{
|
||||
// Undefined return value is rejected
|
||||
asm.Return(),
|
||||
},
|
||||
License: "MIT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var objs struct {
|
||||
Prog *Program `ebpf:"valid"`
|
||||
Map *Map `ebpf:"valid"`
|
||||
}
|
||||
|
||||
if err := spec.LoadAndAssign(&objs, nil); err != nil {
|
||||
t.Fatal("Assign loads a map or program that isn't requested in the struct:", err)
|
||||
}
|
||||
defer objs.Prog.Close()
|
||||
defer objs.Map.Close()
|
||||
|
||||
if objs.Prog == nil {
|
||||
t.Error("Program is nil")
|
||||
}
|
||||
|
||||
if objs.Map == nil {
|
||||
t.Error("Map is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionAssign(t *testing.T) {
|
||||
var specs struct {
|
||||
Program *ProgramSpec `ebpf:"prog1"`
|
||||
Map *MapSpec `ebpf:"map1"`
|
||||
}
|
||||
|
||||
mapSpec := &MapSpec{
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
}
|
||||
progSpec := &ProgramSpec{
|
||||
Type: SocketFilter,
|
||||
Instructions: asm.Instructions{
|
||||
asm.LoadImm(asm.R0, 0, asm.DWord),
|
||||
asm.Return(),
|
||||
},
|
||||
License: "MIT",
|
||||
}
|
||||
|
||||
cs := &CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
"map1": mapSpec,
|
||||
},
|
||||
Programs: map[string]*ProgramSpec{
|
||||
"prog1": progSpec,
|
||||
},
|
||||
}
|
||||
|
||||
if err := cs.Assign(&specs); err != nil {
|
||||
t.Fatal("Can't assign spec:", err)
|
||||
}
|
||||
|
||||
if specs.Program != progSpec {
|
||||
t.Fatalf("Expected Program to be %p, got %p", progSpec, specs.Program)
|
||||
}
|
||||
|
||||
if specs.Map != mapSpec {
|
||||
t.Fatalf("Expected Map to be %p, got %p", mapSpec, specs.Map)
|
||||
}
|
||||
|
||||
if err := cs.Assign(new(int)); err == nil {
|
||||
t.Fatal("Assign allows to besides *struct")
|
||||
}
|
||||
|
||||
if err := cs.Assign(new(struct{ Foo int })); err != nil {
|
||||
t.Fatal("Assign doesn't ignore untagged fields")
|
||||
}
|
||||
|
||||
unexported := new(struct {
|
||||
foo *MapSpec `ebpf:"map1"`
|
||||
})
|
||||
|
||||
if err := cs.Assign(unexported); err == nil {
|
||||
t.Error("Assign should return an error on unexported fields")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignValues(t *testing.T) {
|
||||
zero := func(t reflect.Type, name string) (interface{}, error) {
|
||||
return reflect.Zero(t).Interface(), nil
|
||||
}
|
||||
|
||||
type t1 struct {
|
||||
Bar int `ebpf:"bar"`
|
||||
}
|
||||
|
||||
type t2 struct {
|
||||
t1
|
||||
Foo int `ebpf:"foo"`
|
||||
}
|
||||
|
||||
type t2ptr struct {
|
||||
*t1
|
||||
Foo int `ebpf:"foo"`
|
||||
}
|
||||
|
||||
invalid := []struct {
|
||||
name string
|
||||
to interface{}
|
||||
}{
|
||||
{"non-struct", 1},
|
||||
{"non-pointer struct", t1{}},
|
||||
{"pointer to non-struct", new(int)},
|
||||
{"embedded nil pointer", &t2ptr{}},
|
||||
{"unexported field", new(struct {
|
||||
foo int `ebpf:"foo"`
|
||||
})},
|
||||
{"identical tag", new(struct {
|
||||
Foo1 int `ebpf:"foo"`
|
||||
Foo2 int `ebpf:"foo"`
|
||||
})},
|
||||
}
|
||||
|
||||
for _, testcase := range invalid {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
if err := assignValues(testcase.to, zero); err == nil {
|
||||
t.Fatal("assignValues didn't return an error")
|
||||
} else {
|
||||
t.Log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
valid := []struct {
|
||||
name string
|
||||
to interface{}
|
||||
}{
|
||||
{"pointer to struct", new(t1)},
|
||||
{"embedded struct", new(t2)},
|
||||
{"embedded struct pointer", &t2ptr{t1: new(t1)}},
|
||||
{"untagged field", new(struct{ Foo int })},
|
||||
}
|
||||
|
||||
for _, testcase := range valid {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
if err := assignValues(testcase.to, zero); err != nil {
|
||||
t.Fatal("assignValues returned", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIncompleteLoadAndAssign(t *testing.T) {
|
||||
spec := &CollectionSpec{
|
||||
Programs: map[string]*ProgramSpec{
|
||||
"valid": {
|
||||
Type: SocketFilter,
|
||||
Instructions: asm.Instructions{
|
||||
asm.LoadImm(asm.R0, 0, asm.DWord),
|
||||
asm.Return(),
|
||||
},
|
||||
License: "MIT",
|
||||
},
|
||||
"invalid": {
|
||||
Type: SocketFilter,
|
||||
Instructions: asm.Instructions{
|
||||
asm.Return(),
|
||||
},
|
||||
License: "MIT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := struct {
|
||||
// Assignment to Valid should execute and succeed.
|
||||
Valid *Program `ebpf:"valid"`
|
||||
// Assignment to Invalid should fail and cause Valid's fd to be closed.
|
||||
Invalid *Program `ebpf:"invalid"`
|
||||
}{}
|
||||
|
||||
if err := spec.LoadAndAssign(&s, nil); err == nil {
|
||||
t.Fatal("expected error loading invalid ProgramSpec")
|
||||
}
|
||||
|
||||
if s.Valid == nil {
|
||||
t.Fatal("expected valid prog to be non-nil")
|
||||
}
|
||||
|
||||
if fd := s.Valid.FD(); fd != -1 {
|
||||
t.Fatal("expected valid prog to have closed fd -1, got:", fd)
|
||||
}
|
||||
|
||||
if s.Invalid != nil {
|
||||
t.Fatal("expected invalid prog to be nil due to never being assigned")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewCollection(b *testing.B) {
|
||||
file := fmt.Sprintf("testdata/loader-%s.elf", internal.ClangEndian)
|
||||
spec, err := LoadCollectionSpec(file)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
spec.Maps["array_of_hash_map"].InnerMap = spec.Maps["hash_map"]
|
||||
for _, m := range spec.Maps {
|
||||
m.Pinning = PinNone
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
coll, err := NewCollection(spec)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
coll.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewCollectionManyProgs(b *testing.B) {
|
||||
file := fmt.Sprintf("testdata/manyprogs-%s.elf", internal.ClangEndian)
|
||||
spec, err := LoadCollectionSpec(file)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
coll, err := NewCollection(spec)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
coll.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleCollectionSpec_Assign() {
|
||||
spec := &CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
"map1": {
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
},
|
||||
},
|
||||
Programs: map[string]*ProgramSpec{
|
||||
"prog1": {
|
||||
Type: SocketFilter,
|
||||
Instructions: asm.Instructions{
|
||||
asm.LoadImm(asm.R0, 0, asm.DWord),
|
||||
asm.Return(),
|
||||
},
|
||||
License: "MIT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type maps struct {
|
||||
Map *MapSpec `ebpf:"map1"`
|
||||
}
|
||||
|
||||
var specs struct {
|
||||
maps
|
||||
Program *ProgramSpec `ebpf:"prog1"`
|
||||
}
|
||||
|
||||
if err := spec.Assign(&specs); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(specs.Program.Type)
|
||||
fmt.Println(specs.Map.Type)
|
||||
|
||||
// Output: SocketFilter
|
||||
// Array
|
||||
}
|
||||
|
||||
func ExampleCollectionSpec_LoadAndAssign() {
|
||||
spec := &CollectionSpec{
|
||||
Maps: map[string]*MapSpec{
|
||||
"map1": {
|
||||
Type: Array,
|
||||
KeySize: 4,
|
||||
ValueSize: 4,
|
||||
MaxEntries: 1,
|
||||
},
|
||||
},
|
||||
Programs: map[string]*ProgramSpec{
|
||||
"prog1": {
|
||||
Type: SocketFilter,
|
||||
Instructions: asm.Instructions{
|
||||
asm.LoadImm(asm.R0, 0, asm.DWord),
|
||||
asm.Return(),
|
||||
},
|
||||
License: "MIT",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var objs struct {
|
||||
Program *Program `ebpf:"prog1"`
|
||||
Map *Map `ebpf:"map1"`
|
||||
}
|
||||
|
||||
if err := spec.LoadAndAssign(&objs, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer objs.Program.Close()
|
||||
defer objs.Map.Close()
|
||||
|
||||
fmt.Println(objs.Program.Type())
|
||||
fmt.Println(objs.Map.Type())
|
||||
|
||||
// Output: SocketFilter
|
||||
// Array
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Package ebpf is a toolkit for working with eBPF programs.
|
||||
//
|
||||
// eBPF programs are small snippets of code which are executed directly
|
||||
// in a VM in the Linux kernel, which makes them very fast and flexible.
|
||||
// Many Linux subsystems now accept eBPF programs. This makes it possible
|
||||
// to implement highly application specific logic inside the kernel,
|
||||
// without having to modify the actual kernel itself.
|
||||
//
|
||||
// This package is designed for long-running processes which
|
||||
// want to use eBPF to implement part of their application logic. It has no
|
||||
// run-time dependencies outside of the library and the Linux kernel itself.
|
||||
// eBPF code should be compiled ahead of time using clang, and shipped with
|
||||
// your application as any other resource.
|
||||
//
|
||||
// Use the link subpackage to attach a loaded program to a hook in the kernel.
|
||||
//
|
||||
// Note that losing all references to Map and Program resources will cause
|
||||
// their underlying file descriptors to be closed, potentially removing those
|
||||
// objects from the kernel. Always retain a reference by e.g. deferring a
|
||||
// Close() of a Collection or LoadAndAssign object until application exit.
|
||||
//
|
||||
// Special care needs to be taken when handling maps of type ProgramArray,
|
||||
// as the kernel erases its contents when the last userspace or bpffs
|
||||
// reference disappears, regardless of the map being in active use.
|
||||
package ebpf
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
//go:build linux
|
||||
|
||||
package ebpf_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/cilium/ebpf"
|
||||
)
|
||||
|
||||
var program = [...]byte{
|
||||
0177, 0105, 0114, 0106, 0002, 0001, 0001, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0001, 0000, 0367, 0000, 0001, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0340, 0001, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0100, 0000, 0000, 0000, 0000, 0000, 0100, 0000, 0010, 0000, 0001, 0000,
|
||||
0277, 0026, 0000, 0000, 0000, 0000, 0000, 0000, 0060, 0000, 0000, 0000, 0027, 0000, 0000, 0000,
|
||||
0143, 0012, 0374, 0377, 0000, 0000, 0000, 0000, 0141, 0141, 0004, 0000, 0000, 0000, 0000, 0000,
|
||||
0125, 0001, 0010, 0000, 0004, 0000, 0000, 0000, 0277, 0242, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0007, 0002, 0000, 0000, 0374, 0377, 0377, 0377, 0030, 0001, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0205, 0000, 0000, 0000, 0001, 0000, 0000, 0000,
|
||||
0025, 0000, 0002, 0000, 0000, 0000, 0000, 0000, 0141, 0141, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0333, 0020, 0000, 0000, 0000, 0000, 0000, 0000, 0267, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0225, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0002, 0000, 0000, 0000, 0004, 0000, 0000, 0000,
|
||||
0010, 0000, 0000, 0000, 0000, 0001, 0000, 0000, 0000, 0000, 0000, 0000, 0002, 0000, 0000, 0000,
|
||||
0004, 0000, 0000, 0000, 0010, 0000, 0000, 0000, 0000, 0001, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0107, 0120, 0114, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0065, 0000, 0000, 0000, 0000, 0000, 0003, 0000, 0150, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0034, 0000, 0000, 0000, 0020, 0000, 0006, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0110, 0000, 0000, 0000, 0020, 0000, 0003, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0014, 0000, 0000, 0000, 0020, 0000, 0005, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0023, 0000, 0000, 0000, 0020, 0000, 0005, 0000, 0024, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0070, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0001, 0000, 0000, 0000, 0004, 0000, 0000, 0000, 0000, 0056, 0164, 0145, 0170, 0164, 0000, 0155,
|
||||
0141, 0160, 0163, 0000, 0155, 0171, 0137, 0155, 0141, 0160, 0000, 0164, 0145, 0163, 0164, 0137,
|
||||
0155, 0141, 0160, 0000, 0137, 0154, 0151, 0143, 0145, 0156, 0163, 0145, 0000, 0056, 0163, 0164,
|
||||
0162, 0164, 0141, 0142, 0000, 0056, 0163, 0171, 0155, 0164, 0141, 0142, 0000, 0114, 0102, 0102,
|
||||
0060, 0137, 0063, 0000, 0056, 0162, 0145, 0154, 0163, 0157, 0143, 0153, 0145, 0164, 0061, 0000,
|
||||
0142, 0160, 0146, 0137, 0160, 0162, 0157, 0147, 0061, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0045, 0000, 0000, 0000, 0003, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0210, 0001, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0122, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0001, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0001, 0000, 0000, 0000, 0001, 0000, 0000, 0000, 0006, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0100, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0004, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0100, 0000, 0000, 0000, 0001, 0000, 0000, 0000, 0006, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0100, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0170, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0010, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0074, 0000, 0000, 0000, 0011, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0170, 0001, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0020, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0007, 0000, 0000, 0000, 0003, 0000, 0000, 0000,
|
||||
0010, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0020, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0007, 0000, 0000, 0000, 0001, 0000, 0000, 0000, 0003, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0270, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0050, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0004, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0035, 0000, 0000, 0000, 0001, 0000, 0000, 0000, 0003, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0340, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0004, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0001, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0055, 0000, 0000, 0000, 0002, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0350, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
0220, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0001, 0000, 0000, 0000, 0002, 0000, 0000, 0000,
|
||||
0010, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0030, 0000, 0000, 0000, 0000, 0000, 0000, 0000,
|
||||
}
|
||||
|
||||
// ExampleSocketELF demonstrates how to load an eBPF program from an ELF,
|
||||
// and attach it to a raw socket.
|
||||
func Example_socketELF() {
|
||||
const SO_ATTACH_BPF = 50
|
||||
|
||||
index := flag.Int("index", 0, "specify ethernet index")
|
||||
flag.Parse()
|
||||
|
||||
spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(program[:]))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var objs struct {
|
||||
Prog *ebpf.Program `ebpf:"bpf_prog1"`
|
||||
Stats *ebpf.Map `ebpf:"my_map"`
|
||||
}
|
||||
|
||||
if err := spec.LoadAndAssign(&objs, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer objs.Prog.Close()
|
||||
defer objs.Stats.Close()
|
||||
|
||||
sock, err := openRawSock(*index)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer syscall.Close(sock)
|
||||
|
||||
if err := syscall.SetsockoptInt(sock, syscall.SOL_SOCKET, SO_ATTACH_BPF, objs.Prog.FD()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Filtering on eth index: %d\n", *index)
|
||||
fmt.Println("Packet stats:")
|
||||
|
||||
for {
|
||||
const (
|
||||
ICMP = 0x01
|
||||
TCP = 0x06
|
||||
UDP = 0x11
|
||||
)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
var icmp uint64
|
||||
var tcp uint64
|
||||
var udp uint64
|
||||
err := objs.Stats.Lookup(uint32(ICMP), &icmp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = objs.Stats.Lookup(uint32(TCP), &tcp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = objs.Stats.Lookup(uint32(UDP), &udp)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("\r\033[m\tICMP: %d TCP: %d UDP: %d", icmp, tcp, udp)
|
||||
}
|
||||
}
|
||||
|
||||
func openRawSock(index int) (int, error) {
|
||||
sock, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, int(htons(syscall.ETH_P_ALL)))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sll := syscall.SockaddrLinklayer{
|
||||
Ifindex: index,
|
||||
Protocol: htons(syscall.ETH_P_ALL),
|
||||
}
|
||||
if err := syscall.Bind(sock, &sll); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sock, nil
|
||||
}
|
||||
|
||||
// htons converts the unsigned short integer hostshort from host byte order to network byte order.
|
||||
func htons(i uint16) uint16 {
|
||||
b := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(b, i)
|
||||
return *(*uint16)(unsafe.Pointer(&b[0]))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user