whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -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
@@ -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
@@ -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)
}
}
}