diff --git a/cmd/bpf2go/README.md b/cmd/bpf2go/README.md index 3a89d9a5e..b3a732303 100644 --- a/cmd/bpf2go/README.md +++ b/cmd/bpf2go/README.md @@ -23,6 +23,12 @@ across a project, e.g. to set specific C flags: 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. diff --git a/cmd/bpf2go/main.go b/cmd/bpf2go/main.go index 4ff3c1fd8..6f7e61ec1 100644 --- a/cmd/bpf2go/main.go +++ b/cmd/bpf2go/main.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "runtime" "sort" "strings" @@ -78,6 +79,8 @@ func run(stdout io.Writer, pkg, outputDir string, args []string) (err error) { fs.StringVar(&b2g.tags, "tags", "", "list of Go build tags to include in generated files") flagTarget := fs.String("target", "bpfel,bpfeb", "clang target to compile for") fs.StringVar(&b2g.makeBase, "makebase", "", "write make compatible depinfo files relative to `directory`") + 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.SetOutput(stdout) fs.Usage = func() { @@ -193,6 +196,44 @@ func run(stdout io.Writer, pkg, outputDir string, args []string) (err error) { return 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 +} + type bpf2go struct { stdout io.Writer // Absolute path to a .c file. @@ -209,7 +250,10 @@ type bpf2go struct { strip string disableStripping bool // C flags passed to the compiler. - cFlags []string + cFlags []string + skipGlobalTypes bool + // C types to include in the generatd output. + cTypes cTypes // Go tags included in the .go tags string // Base directory of the Makefile. Enables outputting make-style dependencies @@ -282,12 +326,14 @@ func (b2g *bpf2go) convert(tgt target, arches []string) (err error) { } defer removeOnError(goFile) - err = writeCommon(writeArgs{ - pkg: b2g.pkg, - ident: b2g.ident, - tags: tags, - obj: objFileName, - out: goFile, + err = output(outputArgs{ + pkg: b2g.pkg, + ident: b2g.ident, + cTypes: b2g.cTypes, + skipGlobalTypes: b2g.skipGlobalTypes, + tags: tags, + obj: objFileName, + out: goFile, }) if err != nil { return fmt.Errorf("can't write %s: %s", goFileName, err) diff --git a/cmd/bpf2go/main_test.go b/cmd/bpf2go/main_test.go index 8492b8696..56884d281 100644 --- a/cmd/bpf2go/main_test.go +++ b/cmd/bpf2go/main_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + qt "github.com/frankban/quicktest" "github.com/google/go-cmp/cmp" ) @@ -243,3 +244,37 @@ func TestConvertGOARCH(t *testing.T) { 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) +} diff --git a/cmd/bpf2go/output.go b/cmd/bpf2go/output.go index 56559a8b9..6eeafc3a1 100644 --- a/cmd/bpf2go/output.go +++ b/cmd/bpf2go/output.go @@ -5,13 +5,15 @@ import ( "fmt" "go/token" "io" - "os" + "io/ioutil" "path/filepath" + "sort" "strings" "text/template" "github.com/cilium/ebpf" "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/btf" ) const ebpfModule = "github.com/cilium/ebpf" @@ -32,6 +34,13 @@ import ( "{{ .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 }}) @@ -173,15 +182,15 @@ func (n templateName) Bytes() string { } func (n templateName) Specs() string { - return n.maybeExport(string(n) + "Specs") + return string(n) + "Specs" } func (n templateName) ProgramSpecs() string { - return n.maybeExport(string(n) + "ProgramSpecs") + return string(n) + "ProgramSpecs" } func (n templateName) MapSpecs() string { - return n.maybeExport(string(n) + "MapSpecs") + return string(n) + "MapSpecs" } func (n templateName) Load() string { @@ -193,36 +202,39 @@ func (n templateName) LoadObjects() string { } func (n templateName) Objects() string { - return n.maybeExport(string(n) + "Objects") + return string(n) + "Objects" } func (n templateName) Maps() string { - return n.maybeExport(string(n) + "Maps") + return string(n) + "Maps" } func (n templateName) Programs() string { - return n.maybeExport(string(n) + "Programs") + return string(n) + "Programs" } func (n templateName) CloseHelper() string { return "_" + toUpperFirst(string(n)) + "Close" } -type writeArgs struct { - pkg string - ident string - tags []string - obj string - out io.Writer +type outputArgs struct { + pkg string + ident string + tags []string + cTypes []string + skipGlobalTypes bool + obj string + out io.Writer } -func writeCommon(args writeArgs) error { - obj, err := os.ReadFile(args.obj) +func output(args outputArgs) error { + obj, err := ioutil.ReadFile(args.obj) if err != nil { return fmt.Errorf("read object file contents: %s", err) } - spec, err := ebpf.LoadCollectionSpecFromReader(bytes.NewReader(obj)) + rd := bytes.NewReader(obj) + spec, err := ebpf.LoadCollectionSpecFromReader(rd) if err != nil { return fmt.Errorf("can't load BPF from ELF: %s", err) } @@ -242,21 +254,68 @@ func writeCommon(args writeArgs) error { programs[name] = internal.Identifier(name) } + // Collect any types which we've been asked for explicitly. + cTypes, err := collectCTypes(spec.BTF, args.cTypes) + if err != nil { + return err + } + + typeNames := make(map[btf.Type]string) + for _, cType := range cTypes { + typeNames[cType] = args.ident + internal.Identifier(cType.TypeName()) + } + + // Collect map key and value types, unless we've been asked not to. + if !args.skipGlobalTypes { + 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 + } + + typeNames[typ] = args.ident + 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 + } + + gf := &btf.GoFormatter{ + Names: typeNames, + Identifier: internal.Identifier, + } + ctx := struct { - Module string - Package string - Tags []string - Name templateName - Maps map[string]string - Programs map[string]string - File string + *btf.GoFormatter + Module string + Package string + Tags []string + Name templateName + Maps map[string]string + Programs map[string]string + Types []btf.Type + TypeNames map[btf.Type]string + File string }{ + gf, ebpfModule, args.pkg, args.tags, templateName(args.ident), maps, programs, + types, + typeNames, filepath.Base(args.obj), } @@ -268,6 +327,62 @@ func writeCommon(args writeArgs) error { return internal.WriteFormatted(buf.Bytes(), args.out) } +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.BTF == nil { + continue + } + + if m.BTF.Key != nil && m.BTF.Key.TypeName() != "" { + result = append(result, m.BTF.Key) + } + + if m.BTF.Value != nil && m.BTF.Value.TypeName() != "" { + result = append(result, m.BTF.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 +} + func tag(str string) string { return "`ebpf:\"" + str + "\"`" } diff --git a/cmd/bpf2go/output_test.go b/cmd/bpf2go/output_test.go new file mode 100644 index 000000000..bf7daabd6 --- /dev/null +++ b/cmd/bpf2go/output_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "testing" + + "github.com/cilium/ebpf/internal/btf" + qt "github.com/frankban/quicktest" +) + +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) + }) + } +} diff --git a/cmd/bpf2go/test/api_test.go b/cmd/bpf2go/test/api_test.go index 5f2d53e4e..76f57157e 100644 --- a/cmd/bpf2go/test/api_test.go +++ b/cmd/bpf2go/test/api_test.go @@ -1,14 +1,16 @@ package test import ( + "reflect" "testing" + "unsafe" - // Raise RLIMIT_MEMLOCK - _ "github.com/cilium/ebpf/internal/testutils" + "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) } @@ -20,7 +22,9 @@ func TestLoadingSpec(t *testing.T) { func TestLoadingObjects(t *testing.T) { var objs testObjects - if err := loadTestObjects(&objs, nil); err != nil { + err := loadTestObjects(&objs, nil) + testutils.SkipIfNotSupported(t, err) + if err != nil { t.Fatal("Can't load objects:", err) } defer objs.Close() @@ -33,3 +37,31 @@ func TestLoadingObjects(t *testing.T) { 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") + } +} diff --git a/cmd/bpf2go/test/test_bpfeb.go b/cmd/bpf2go/test/test_bpfeb.go index 1c366127e..2f99ba39a 100644 --- a/cmd/bpf2go/test/test_bpfeb.go +++ b/cmd/bpf2go/test/test_bpfeb.go @@ -13,6 +13,20 @@ import ( "github.com/cilium/ebpf" ) +type testBarfoo struct { + Bar int64 + Baz bool + _ [3]byte + Boo testE +} + +type testE int32 + +const ( + testEHOOPY testE = 0 + testEFROOD testE = 1 +) + // loadTest returns the embedded CollectionSpec for test. func loadTest() (*ebpf.CollectionSpec, error) { reader := bytes.NewReader(_TestBytes) diff --git a/cmd/bpf2go/test/test_bpfeb.o b/cmd/bpf2go/test/test_bpfeb.o index c445a7a0f..d5c25f79c 100644 Binary files a/cmd/bpf2go/test/test_bpfeb.o and b/cmd/bpf2go/test/test_bpfeb.o differ diff --git a/cmd/bpf2go/test/test_bpfel.go b/cmd/bpf2go/test/test_bpfel.go index 9f93826eb..a19190bbf 100644 --- a/cmd/bpf2go/test/test_bpfel.go +++ b/cmd/bpf2go/test/test_bpfel.go @@ -13,6 +13,20 @@ import ( "github.com/cilium/ebpf" ) +type testBarfoo struct { + Bar int64 + Baz bool + _ [3]byte + Boo testE +} + +type testE int32 + +const ( + testEHOOPY testE = 0 + testEFROOD testE = 1 +) + // loadTest returns the embedded CollectionSpec for test. func loadTest() (*ebpf.CollectionSpec, error) { reader := bytes.NewReader(_TestBytes) diff --git a/cmd/bpf2go/test/test_bpfel.o b/cmd/bpf2go/test/test_bpfel.o index e89a2253d..0320088a3 100644 Binary files a/cmd/bpf2go/test/test_bpfel.o and b/cmd/bpf2go/test/test_bpfel.o differ diff --git a/cmd/bpf2go/testdata/minimal.c b/cmd/bpf2go/testdata/minimal.c index 321805684..1639d6ef0 100644 --- a/cmd/bpf2go/testdata/minimal.c +++ b/cmd/bpf2go/testdata/minimal.c @@ -2,13 +2,27 @@ char __license[] __section("license") = "MIT"; -struct bpf_map_def map1 __section("maps") = { - .type = BPF_MAP_TYPE_HASH, - .key_size = 4, - .value_size = 4, - .max_entries = 1, -}; +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 0; + return my_constant + struct_const.bar; } diff --git a/collection.go b/collection.go index fb32ada88..0e0dc5322 100644 --- a/collection.go +++ b/collection.go @@ -27,6 +27,9 @@ type CollectionSpec struct { Maps map[string]*MapSpec Programs map[string]*ProgramSpec + // The BTF used by maps and programs. + BTF *btf.Spec + // ByteOrder specifies whether the ELF was compiled for // big-endian or little-endian architectures. ByteOrder binary.ByteOrder @@ -409,6 +412,10 @@ func (cl *collectionLoader) loadMap(mapName string) (*Map, error) { return nil, fmt.Errorf("missing map %s", mapName) } + if mapSpec.BTF != nil && cl.coll.BTF != mapSpec.BTF.Spec { + return nil, fmt.Errorf("map %s: BTF doesn't match collection", mapName) + } + m, err := newMapWithOptions(mapSpec, cl.opts.Maps, cl.handles) if err != nil { return nil, fmt.Errorf("map %s: %w", mapName, err) @@ -434,6 +441,10 @@ func (cl *collectionLoader) loadProgram(progName string) (*Program, error) { return nil, fmt.Errorf("cannot load program %s: program type is unspecified", progName) } + if progSpec.BTF != nil && cl.coll.BTF != progSpec.BTF.Spec() { + return nil, fmt.Errorf("program %s: BTF doesn't match collection", progName) + } + progSpec = progSpec.Copy() // Rewrite any reference to a valid map in the program's instructions, diff --git a/elf_reader.go b/elf_reader.go index bbc883108..bde05f5f5 100644 --- a/elf_reader.go +++ b/elf_reader.go @@ -164,7 +164,7 @@ func LoadCollectionSpecFromReader(rd io.ReaderAt) (*CollectionSpec, error) { return nil, fmt.Errorf("load programs: %w", err) } - return &CollectionSpec{maps, progs, ec.ByteOrder}, nil + return &CollectionSpec{maps, progs, btfSpec, ec.ByteOrder}, nil } func loadLicense(sec *elf.Section) (string, error) { diff --git a/elf_reader_test.go b/elf_reader_test.go index cbb24e9af..01a35df14 100644 --- a/elf_reader_test.go +++ b/elf_reader_test.go @@ -143,7 +143,7 @@ func TestLoadCollectionSpec(t *testing.T) { return false }), cmpopts.IgnoreTypes(new(btf.Map), new(btf.Program)), - cmpopts.IgnoreFields(CollectionSpec{}, "ByteOrder"), + cmpopts.IgnoreFields(CollectionSpec{}, "ByteOrder", "BTF"), cmpopts.IgnoreFields(ProgramSpec{}, "Instructions", "ByteOrder"), cmpopts.IgnoreUnexported(ProgramSpec{}), cmpopts.IgnoreMapEntries(func(key string, _ *MapSpec) bool { diff --git a/internal/btf/btf.go b/internal/btf/btf.go index df4f78efd..3d72be2b3 100644 --- a/internal/btf/btf.go +++ b/internal/btf/btf.go @@ -635,6 +635,22 @@ func (s *Spec) AnyTypesByName(name string) ([]Type, error) { 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. diff --git a/internal/btf/types.go b/internal/btf/types.go index a6b5a10aa..c23c3e7a6 100644 --- a/internal/btf/types.go +++ b/internal/btf/types.go @@ -990,3 +990,23 @@ func newEssentialName(name string) essentialName { } return essentialName(name) } + +// UnderlyingType skips qualifiers and Typedefs. +// +// May return typ verbatim if too many types have to be skipped to protect against +// circular Types. +func UnderlyingType(typ Type) Type { + result := typ + for depth := 0; depth <= maxTypeDepth; depth++ { + switch v := (result).(type) { + case qualifier: + result = v.qualify() + case *Typedef: + result = v.Type + default: + return result + } + } + // Return the original argument, since we can't find an underlying type. + return typ +} diff --git a/internal/btf/types_test.go b/internal/btf/types_test.go index 88658d559..a53f2dd47 100644 --- a/internal/btf/types_test.go +++ b/internal/btf/types_test.go @@ -249,3 +249,55 @@ func newCyclicalType(n int) Type { 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} }}, + } + + for _, test := range wrappers { + t.Run(test.name+" cycle", func(t *testing.T) { + root := &Volatile{} + root.Type = test.fn(root) + + got := UnderlyingType(root) + qt.Assert(t, got, 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 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) + } + }) +}