From 8486b44427a287b0028542a481c7dbaea177e9c7 Mon Sep 17 00:00:00 2001 From: Simone Magnani Date: Thu, 5 Sep 2024 14:20:51 +0200 Subject: [PATCH] add bpf2go support and wrappers for ebpf.Variable generation This commit introduces support and wrappers around ebpf.Variable. An ebpf.Variable does not define per-se wrappers method to automatically infer the underlying data type and handle typed data in Go. In fact, it relies on parameters passed to the `Variable.Get` and `Variable.Set` APIs. This commit allows `bpf2go` to emit `VariableSpec` and `Variable` in the generated code. In addition, a new set of wrapper methods are created to provide support for typed method around variables. To do so, this commit enables emitting `Dataspec`, which so far was not emitted. However, it modifies the current implementation so that for each variable in `Dataspec` the needed support is provided. Supposing to have an ebpf program that defines the following two global variables: ```c __u64 pkt_count = 0; char var_msg[] = "Hello World!"; ``` The steps performed during the Go code generation are as follows: 1. An alias with the variable name is generated in the Go file: ```go type PktCountType = uint64 type VarMsgType = [16]int8 ``` 2. A new type definition (non-alias, need new methods) for the variables is performed: ```go type PktCount ebpf.Variable type VarMsg ebpf.Variable ``` 3. The `.Get` and `.Set` methods for these types are generated: ```go func (v *PktCount) Get() (PktCountType, error) { var ret PktCountType return ret, (*ebpf.Variable)(v).Get(&ret) } func (v *PktCount) Set(val PktCountType) error { return (*ebpf.Variable)(v).Set(val) } func (v *VarMsg) Get() (VarMsgType, error) { var ret VarMsgType return ret, (*ebpf.Variable)(v).Get(&ret) } func (v *VarMsg) Set(val VarMsgType) error { return (*ebpf.Variable)(v).Set(val) } ``` 4. In case the type alias is supported by the atomic package, and then also supported by `ebpf.Variable` (int32, uint32, int64, uint64), then an additional `AtomicRef` method is created to get a reference to the underlying value while also performing atomic operations. ```go func (v *PktCount) AtomicRef() (*ebpf.Uint64, error) { return (*ebpf.Variable)(v).AtomicUint64() } ``` From the user perspective, ignoring the error catching, the following operations can be performed: ```go ... objs := bpfObjects{} err := loadBpfObjects(&objs, nil) ... // ref kept and can be used multiple times aPktCount, err := objs.PktCount.AtomicRef() err := aPktCount.Load() aPktCount.Store(rand.Uint64()) ... varMsg, _ := objs.VarMsg.Get() varMsg[0] = 'B' err := objs.VarMsg.Set(varMsg) ``` Signed-off-by: Simone Magnani --- btf/format.go | 117 ++++++++++++++++++++++++++++++++------ cmd/bpf2go/gen/output.go | 25 +++++--- cmd/bpf2go/gen/output.tpl | 20 +++++++ cmd/bpf2go/gen/types.go | 4 -- cmd/bpf2go/main.go | 6 ++ go.mod | 10 +++- go.sum | 16 +++++- 7 files changed, 166 insertions(+), 32 deletions(-) diff --git a/btf/format.go b/btf/format.go index 5e581b4a8..264d9fa64 100644 --- a/btf/format.go +++ b/btf/format.go @@ -4,10 +4,40 @@ import ( "errors" "fmt" "strings" + "text/template" + + "golang.org/x/text/cases" + "golang.org/x/text/language" ) var errNestedTooDeep = errors.New("nested too deep") +const tplVars = ` + +type {{ .Name }} ebpf.Variable + +func(v *{{ .Name }}) Get() ({{ .Type }}, error) { + var ret {{ .Type }} + return ret, (*ebpf.Variable)(v).Get(&ret) +} + +func(v *{{ .Name }}) Set(val {{ .Type }}) error { + return (*ebpf.Variable)(v).Set(val) +} +` + +const tplAtomicVars = ` +func(v *{{ .Name }}) AtomicRef() (*ebpf.{{ .CapitalizedType }}, error) { + return (*ebpf.Variable)(v).Atomic{{ .CapitalizedType }}() +} +` + +type TplVarsData struct { + Name string + Type string + CapitalizedType string +} + // GoFormatter converts a Type to Go syntax. // // A zero GoFormatter is valid to use. @@ -64,7 +94,12 @@ func (gf *GoFormatter) writeTypeDecl(name string, typ Type) error { } typ = skipQualifiers(typ) - fmt.Fprintf(&gf.w, "type %s ", name) + // custom handling Datasec types in writeDatasecLit + _, ok := typ.(*Datasec) + if !ok { + fmt.Fprintf(&gf.w, "type %s ", name) + } + if err := gf.writeTypeLit(typ, 0); err != nil { return err } @@ -295,41 +330,89 @@ func (gf *GoFormatter) writeStructField(m Member, depth int) error { } func (gf *GoFormatter) writeDatasecLit(ds *Datasec, depth int) error { - gf.w.WriteString("struct { ") + tmpl, err := template.New("varsHelpers").Parse(tplVars) + if err != nil { + return fmt.Errorf("failed to parse template: %w", err) + } + + tmplAtomic, err := template.New("varsAtomicHelpers").Parse(tplAtomicVars) + if err != nil { + return fmt.Errorf("failed to parse template: %w", err) + } - 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) + id := gf.identifier(v.Name) + tplArgs := TplVarsData{ + Name: id, + Type: id + "Type", } - gf.writePadding(vsi.Offset - prevOffset) - prevOffset = vsi.Offset + vsi.Size - - fmt.Fprintf(&gf.w, "%s ", gf.identifier(v.Name)) + fmt.Fprintf(&gf.w, "type %s =", tplArgs.Type) if err := gf.writeType(v.Type, depth); err != nil { return fmt.Errorf("variable %d: %w", i, err) } + if err := tmpl.Execute(&gf.w, tplArgs); err != nil { + return fmt.Errorf("failed to execute template for variable %s: %w", tplArgs.Name, err) + } - gf.w.WriteString("; ") + if va := getSuppAtomicType(v.Type); va != "" { + tplArgs.CapitalizedType = cases.Title(language.Und, cases.NoLower).String(va) + if tmplAtomic.Execute(&gf.w, tplArgs) != nil { + return fmt.Errorf("failed to execute template for variable %s atomics: %w", tplArgs.Name, err) + } + } } - gf.writePadding(ds.Size - prevOffset) - gf.w.WriteString("}") return nil } +// getSuppAtomicType returns the corresponding Go type for the +// provided argument if it supports package atomic primitives. +// Current support for int32, uint32, int64, uint64. +func getSuppAtomicType(t Type) string { + checkInt := func(t *Int) string { + ret := "" + switch t.Size { + // uint32/int32 and uint64/int64 + case 4: + ret = "int32" + case 8: + ret = "int64" + default: + return "" + } + if t.Encoding == Unsigned { + ret = "u" + ret + } + return ret + } + + switch v := skipQualifiers(t).(type) { + case *Int: + return checkInt(v) + case *Typedef: + if vv, ok := v.Type.(*Int); ok { + return checkInt(vv) + } + case *Enum: + i := &Int{ + Name: v.Name, + Size: v.Size, + Encoding: Unsigned, + } + if v.Signed { + i.Encoding = Signed + } + return checkInt(i) + } + return "" +} func (gf *GoFormatter) writePadding(bytes uint32) { if bytes > 0 { fmt.Fprintf(&gf.w, "_ [%d]byte; ", bytes) diff --git a/cmd/bpf2go/gen/output.go b/cmd/bpf2go/gen/output.go index a054fd2f1..2ade96375 100644 --- a/cmd/bpf2go/gen/output.go +++ b/cmd/bpf2go/gen/output.go @@ -49,6 +49,10 @@ func (n templateName) MapSpecs() string { return string(n) + "MapSpecs" } +func (n templateName) VariableSpecs() string { + return string(n) + "VariableSpecs" +} + func (n templateName) Load() string { return n.maybeExport("load" + toUpperFirst(string(n))) } @@ -65,6 +69,10 @@ func (n templateName) Maps() string { return string(n) + "Maps" } +func (n templateName) Variables() string { + return string(n) + "Variables" +} + func (n templateName) Programs() string { return string(n) + "Programs" } @@ -82,6 +90,8 @@ type GenerateArgs struct { Constraints constraint.Expr // Maps to be emitted. Maps []string + // Variables to be emitted. + Variables []string // Programs to be emitted. Programs []string // Types to be emitted. @@ -103,19 +113,16 @@ func Generate(args GenerateArgs) error { return fmt.Errorf("file %q contains an invalid character", args.ObjectFile) } - for _, typ := range args.Types { - if _, ok := btf.As[*btf.Datasec](typ); ok { - // Avoid emitting .rodata, .bss, etc. for now. We might want to - // name these types differently, etc. - return fmt.Errorf("can't output btf.Datasec: %s", typ) - } - } - maps := make(map[string]string) for _, name := range args.Maps { maps[name] = internal.Identifier(name) } + vars := make(map[string]string) + for _, name := range args.Variables { + vars[name] = internal.Identifier(name) + } + programs := make(map[string]string) for _, name := range args.Programs { programs[name] = internal.Identifier(name) @@ -146,6 +153,7 @@ func Generate(args GenerateArgs) error { Constraints constraint.Expr Name templateName Maps map[string]string + Variables map[string]string Programs map[string]string Types []btf.Type TypeNames map[btf.Type]string @@ -157,6 +165,7 @@ func Generate(args GenerateArgs) error { args.Constraints, templateName(args.Stem), maps, + vars, programs, types, typeNames, diff --git a/cmd/bpf2go/gen/output.tpl b/cmd/bpf2go/gen/output.tpl index 8d8047066..d5e2c2e8a 100644 --- a/cmd/bpf2go/gen/output.tpl +++ b/cmd/bpf2go/gen/output.tpl @@ -54,6 +54,7 @@ func {{ .Name.LoadObjects }}(obj interface{}, opts *ebpf.CollectionOptions) (err type {{ .Name.Specs }} struct { {{ .Name.ProgramSpecs }} {{ .Name.MapSpecs }} + {{ .Name.VariableSpecs }} } // {{ .Name.Specs }} contains programs before they are loaded into the kernel. @@ -80,6 +81,7 @@ type {{ .Name.MapSpecs }} struct { type {{ .Name.Objects }} struct { {{ .Name.Programs }} {{ .Name.Maps }} + {{ .Name.Variables }} } func (o *{{ .Name.Objects }}) Close() error { @@ -98,6 +100,24 @@ type {{ .Name.Maps }} struct { {{- end }} } +// {{ .Name.VariableSpecs }} contains variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type {{ .Name.VariableSpecs }} struct { +{{- range $name, $id := .Variables }} + {{ $id }} *ebpf.VariableSpec `ebpf:"{{ $name }}"` +{{- end }} +} + +// {{ .Name.Variables }} contains all variables after they have been loaded into the kernel. +// +// It can be passed to {{ .Name.LoadObjects }} or ebpf.CollectionSpec.LoadAndAssign. +type {{ .Name.Variables }} struct { +{{- range $name, $id := .Variables }} + {{ $id }} *{{ $id }} `ebpf:"{{ $name }}"` +{{- end }} +} + func (m *{{ .Name.Maps }}) Close() error { return {{ .Name.CloseHelper }}( {{- range $id := .Maps }} diff --git a/cmd/bpf2go/gen/types.go b/cmd/bpf2go/gen/types.go index 37dad0c76..2973c17a7 100644 --- a/cmd/bpf2go/gen/types.go +++ b/cmd/bpf2go/gen/types.go @@ -12,10 +12,6 @@ func CollectGlobalTypes(spec *ebpf.CollectionSpec) []btf.Type { var types []btf.Type 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. diff --git a/cmd/bpf2go/main.go b/cmd/bpf2go/main.go index fb077e139..a5164ff5e 100644 --- a/cmd/bpf2go/main.go +++ b/cmd/bpf2go/main.go @@ -370,6 +370,11 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { } } + var vars []string + for name := range spec.Variables { + vars = append(vars, name) + } + var programs []string for name := range spec.Programs { programs = append(programs, name) @@ -397,6 +402,7 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) { Stem: b2g.identStem, Constraints: constraints, Maps: maps, + Variables: vars, Programs: programs, Types: types, ObjectFile: filepath.Base(objFileName), diff --git a/go.mod b/go.mod index aca8492f9..c3cb55371 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,16 @@ require ( github.com/go-quicktest/qt v1.101.0 github.com/google/go-cmp v0.6.0 github.com/jsimonetti/rtnetlink/v2 v2.0.1 + github.com/stretchr/testify v1.9.0 golang.org/x/sys v0.20.0 ) +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + require ( github.com/josharian/native v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -17,5 +24,6 @@ require ( github.com/mdlayher/socket v0.4.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/sync v0.1.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.18.0 ) diff --git a/go.sum b/go.sum index e0fee7777..c443c7e57 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -16,12 +18,22 @@ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=