Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions cmd/bpf2go/gen/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ func (n templateName) Programs() string {
return string(n) + "Programs"
}

func (n templateName) StructOps() string {
return string(n) + "StructOps"
}

func (n templateName) CloseHelper() string {
return "_" + toUpperFirst(string(n)) + "Close"
}
Expand All @@ -104,6 +108,62 @@ type GenerateArgs struct {
Output io.Writer
// Function which transforms the input into a valid go identifier. Uses the default behaviour if nil
Identifier func(string) string
// StructOps to be emitted
StructOps []StructOpsSpec
}

type StructOpsSpec struct {
Name string
Type *btf.Struct
}

// generateStructOpsShadowType produces a Go struct definition that mirrors the memory
// layout of a BTF struct, specifically for use with struct_ops maps.
func generateStructOpsShadowType(goTypeName string, st *btf.Struct, gf *btf.GoFormatter) (string, error) {
var sb strings.Builder

sb.WriteString(fmt.Sprintf("// %s is a struct type for the struct_ops map.\n", goTypeName))
sb.WriteString(fmt.Sprintf("type %s struct {\n", goTypeName))
sb.WriteString("\t_ structs.HostLayout\n")

prevOffset := uint32(0)
for _, m := range st.Members {
offset := m.Offset.Bytes()
if padding := offset - prevOffset; padding > 0 {
sb.WriteString(fmt.Sprintf("\t_ [%d]byte\n", padding))
}

fieldName := gf.Identifier(m.Name)
if fieldName == "" {
continue
}

var fieldType string
if ptr, ok := btf.As[*btf.Pointer](btf.UnderlyingType(m.Type)); ok {
if _, ok := btf.As[*btf.FuncProto](btf.UnderlyingType(ptr.Target)); ok {
fieldType = "*ebpf.Program"
}
}
Comment on lines +141 to +146
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic of emitting a *ebpf.Program for pointers to functions seems to be the only reason for having this logic here. I think it would be better to adjust the GoFormatter in btf/format.go to do this instead, so you can reuse the rest of the existing logic for writing go types.


if fieldType == "" {
decl, err := gf.TypeDeclaration("T", m.Type)
if err != nil {
return "", fmt.Errorf("field %s: %w", m.Name, err)
}
fieldType = strings.TrimPrefix(decl, "type T ")
fieldType = strings.Split(fieldType, ";")[0]
}

sb.WriteString(fmt.Sprintf("\t%s %s `ebpf:\"%s\"`\n", fieldName, fieldType, m.Name))
size, err := btf.Sizeof(m.Type)
if err != nil {
return "", fmt.Errorf("field %s: %w", m.Name, err)
}
prevOffset = offset + uint32(size)
}

sb.WriteString("}\n")
return sb.String(), nil
}

// Generate bindings for a BPF ELF file.
Expand Down Expand Up @@ -149,6 +209,12 @@ func Generate(args GenerateArgs) error {
typeNames[typ] = args.Stem + args.Identifier(typ.TypeName())
}

structOps := make(map[string]string)
for _, stOps := range args.StructOps {
goTypeName := "StructOps" + args.Identifier(stOps.Name)
structOps[stOps.Name] = goTypeName
}

// 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)
Expand All @@ -174,6 +240,16 @@ func Generate(args GenerateArgs) error {
typeDecls = append(typeDecls, decl)
}

for _, st := range args.StructOps {
goTypeName := args.Stem + "StructOps" + args.Identifier(st.Name)
decl, err := generateStructOpsShadowType(goTypeName, st.Type, gf)
if err != nil {
return err
}
typeDecls = append(typeDecls, decl)
needsStructsPkg = true
}

ctx := struct {
Module string
Package string
Expand All @@ -185,6 +261,7 @@ func Generate(args GenerateArgs) error {
TypeDeclarations []string
File string
NeedsStructsPkg bool
StructOps map[string]string
}{
b2gInt.CurrentModule,
args.Package,
Expand All @@ -196,6 +273,7 @@ func Generate(args GenerateArgs) error {
typeDecls,
args.ObjectFile,
needsStructsPkg,
structOps,
}

var buf bytes.Buffer
Expand Down
8 changes: 8 additions & 0 deletions cmd/bpf2go/gen/output.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ type {{ .Name.Objects }} struct {
{{ .Name.Programs }}
{{ .Name.Maps }}
{{ .Name.Variables }}
{{ .Name.StructOps }}
}

// {{ .Name.StructOps }} contains all struct_ops types.
type {{ .Name.StructOps }} struct {
{{- range $name, $id := .StructOps }}
{{ $id }} {{ $.Name }}{{ $id }} `ebpf:"{{ $name }}"`
{{- end }}
Comment on lines +100 to +104
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider surrounding this with an {{ if .StructOps }}. Programs, maps, and variables are expected to be in every generated file. But is odd to have empty struct ops related types, even when no struct ops are in use, which will be the majority of cases.

}

func (o *{{ .Name.Objects }}) Close() error {
Expand Down
4 changes: 4 additions & 0 deletions cmd/bpf2go/gen/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ func CollectGlobalTypes(spec *ebpf.CollectionSpec) []btf.Type {
// collectMapTypes collects all types used by MapSpecs.
func collectMapTypes(types []btf.Type, maps map[string]*ebpf.MapSpec) []btf.Type {
for _, m := range maps {
if m.Type == ebpf.StructOpsMap {
continue
}

if m.Key != nil && m.Key.TypeName() != "" {
types = addType(types, m.Key)
}
Expand Down
9 changes: 9 additions & 0 deletions cmd/bpf2go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,11 +404,19 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
}

var maps []string
var structOps []gen.StructOpsSpec
for name := range spec.Maps {
// Skip .rodata, .data, .bss, etc. sections
if !strings.HasPrefix(name, ".") {
maps = append(maps, name)
}

ms := spec.Maps[name]
if ms.Type == ebpf.StructOpsMap {
userSt, _ := btf.As[*btf.Struct](ms.Value)
stOpsSpec := gen.StructOpsSpec{Name: name, Type: userSt}
structOps = append(structOps, stOpsSpec)
}
}

var variables []string
Expand Down Expand Up @@ -448,6 +456,7 @@ func (b2g *bpf2go) convert(tgt gen.Target, goarches gen.GoArches) (err error) {
Types: types,
ObjectFile: filepath.Base(objFileName),
Output: goFile,
StructOps: structOps,
})
if err != nil {
return fmt.Errorf("can't write %s: %s", goFileName, err)
Expand Down
5 changes: 5 additions & 0 deletions cmd/bpf2go/test/test_bpfeb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions cmd/bpf2go/test/test_bpfel.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 49 additions & 5 deletions collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func copyMapOfSpecs[T interface{ Copy() T }](m map[string]T) map[string]T {
// Returns an error if any of the eBPF objects can't be found, or
// if the same Spec is assigned multiple times.
func (cs *CollectionSpec) Assign(to interface{}) error {
getValue := func(typ reflect.Type, name string) (interface{}, error) {
getValue := func(typ reflect.Type, name string, fieldVal reflect.Value) (interface{}, error) {
switch typ {
case reflect.TypeOf((*ProgramSpec)(nil)):
if p := cs.Programs[name]; p != nil {
Expand Down Expand Up @@ -183,7 +183,19 @@ func (cs *CollectionSpec) LoadAndAssign(to interface{}, opts *CollectionOptions)
assignedProgs := make(map[string]bool)
assignedVars := make(map[string]bool)

getValue := func(typ reflect.Type, name string) (interface{}, error) {
getValue := func(typ reflect.Type, name string, val reflect.Value) (interface{}, error) {
ms := cs.Maps[name]
if ms != nil && ms.Type == StructOpsMap {
vType := typ
if vType.Kind() == reflect.Ptr {
vType = vType.Elem()
}

if vType.Kind() == reflect.Struct && typ != reflect.TypeOf((*Map)(nil)) {
return loader.loadStructOps(name, val)
}
}
Comment on lines +187 to +197
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should go into loadMap, as its a special case of loading a map.


switch typ {

case reflect.TypeOf((*Program)(nil)):
Expand Down Expand Up @@ -544,6 +556,38 @@ func (cl *collectionLoader) loadVariable(varName string) (*Variable, error) {
return v, nil
}

// loadStructOps synchronizes a user-provided shadow struct with its MapSpec
// before the map is created in the kernel.
//
// If the field is already instantiated by the user, its values are patched
// into the MapSpec's raw data buffer. This allows setting initial flags,
// parameters, and function pointers before the kernel performs
// struct_ops validation.
func (cl *collectionLoader) loadStructOps(name string, field reflect.Value) (interface{}, error) {
ms := cl.coll.Maps[name]
if ms == nil {
return nil, fmt.Errorf("map %s: not found in loader", name)
}

if field.IsValid() && field.Kind() == reflect.Struct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you invert this there is far less code indented.

Suggested change
if field.IsValid() && field.Kind() == reflect.Struct {
if !field.IsValid() || field.Kind() != reflect.Struct {
return field.Interface(), nil
}
// [...]

userType, ok := btf.As[*btf.Struct](ms.Value)
if !ok {
return nil, fmt.Errorf("map %s: value type is not a Struct", name)
}

rawData, ok := ms.Contents[0].Value.([]byte)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ms.dataSection instead, it has guards in place for unexpected map values. It is also what is used in populateStructOps.

if !ok {
return nil, fmt.Errorf("map %s: spec contents are not a byte slice", name)
}

if err := structOpsPatchValue(field, userType, rawData); err != nil {
return nil, fmt.Errorf("patching spec from field for %s: %q", name, err)
}
}

return field.Interface(), nil
}

// populateDeferredMaps iterates maps holding programs or other maps and loads
// any dependencies. Populates all maps in cl and freezes them if specified.
func (cl *collectionLoader) populateDeferredMaps() error {
Expand Down Expand Up @@ -845,7 +889,7 @@ func (coll *Collection) Assign(to interface{}) error {

// Assign() only transfers already-loaded Maps and Programs. No extra
// loading is done.
getValue := func(typ reflect.Type, name string) (interface{}, error) {
getValue := func(typ reflect.Type, name string, fieldVal reflect.Value) (interface{}, error) {
switch typ {

case reflect.TypeOf((*Program)(nil)):
Expand Down Expand Up @@ -999,7 +1043,7 @@ func ebpfFields(structVal reflect.Value, visited map[reflect.Type]bool) ([]struc
// 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 {
getValue func(typ reflect.Type, name string, val reflect.Value) (interface{}, error)) error {

toValue := reflect.ValueOf(to)
if toValue.Type().Kind() != reflect.Ptr {
Expand Down Expand Up @@ -1037,7 +1081,7 @@ func assignValues(to interface{},
}

// Get the eBPF object referred to by the tag.
value, err := getValue(field.Type, tag)
value, err := getValue(field.Type, tag, field.value)
if err != nil {
return fmt.Errorf("field %s: %w", field.Name, err)
}
Expand Down
2 changes: 1 addition & 1 deletion collection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ func TestNewCollectionFdLeak(t *testing.T) {
}

func TestAssignValues(t *testing.T) {
zero := func(t reflect.Type, name string) (interface{}, error) {
zero := func(t reflect.Type, name string, val reflect.Value) (interface{}, error) {
return reflect.Zero(t).Interface(), nil
}

Expand Down
5 changes: 5 additions & 0 deletions docs/examples/getting_started/counter_bpfeb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/examples/getting_started/counter_bpfel.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/examples/variables/variables_bpfeb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/examples/variables/variables_bpfel.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions examples/cgroup_skb/bpf_bpfeb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions examples/cgroup_skb/bpf_bpfel.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions examples/fentry/bpf_bpfeb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions examples/fentry/bpf_bpfel.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading