diff --git a/bcc/pom.xml b/bcc/pom.xml index 674020c..6d29b86 100644 --- a/bcc/pom.xml +++ b/bcc/pom.xml @@ -77,11 +77,6 @@ - - org.jetbrains - annotations - 24.0.1 - me.bechberger ebpf-annotations diff --git a/bpf/src/main/java/me/bechberger/ebpf/bpf/map/BPFRingBuffer.java b/bpf/src/main/java/me/bechberger/ebpf/bpf/map/BPFRingBuffer.java index 4cb2ca2..40ae610 100644 --- a/bpf/src/main/java/me/bechberger/ebpf/bpf/map/BPFRingBuffer.java +++ b/bpf/src/main/java/me/bechberger/ebpf/bpf/map/BPFRingBuffer.java @@ -84,7 +84,7 @@ record CaughtBPFRingBufferCallbackError(Throwable exception, E event) impleme /** * Create a new ring buffer - * + *^ * @param fd file descriptor of the ring buffer * @param eventType type of the event * @param callback callback that is called when a new event is received diff --git a/bpf/src/test/java/me/bechberger/ebpf/bpf/TypeLayoutTest.java b/bpf/src/test/java/me/bechberger/ebpf/bpf/TypeLayoutTest.java new file mode 100644 index 0000000..2e4e6c7 --- /dev/null +++ b/bpf/src/test/java/me/bechberger/ebpf/bpf/TypeLayoutTest.java @@ -0,0 +1,125 @@ +package me.bechberger.ebpf.bpf; + +import me.bechberger.ebpf.annotations.Unsigned; +import me.bechberger.ebpf.shared.BPFType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.foreign.MemoryLayout; +import java.util.List; +import java.util.stream.Stream; + +import static me.bechberger.ebpf.shared.BPFType.BPFStructType; +import static me.bechberger.ebpf.shared.BPFType.UBPFStructMember; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests the auto-layouting of types + */ +public class TypeLayoutTest { + + private static Stream provideTestAlignmentOfScalarsParameters() { + return Stream.of(Arguments.of(1, BPFType.BPFIntType.INT8), Arguments.of(2, BPFType.BPFIntType.INT16), + Arguments.of(4, BPFType.BPFIntType.INT32), Arguments.of(8, BPFType.BPFIntType.INT64), Arguments.of(1, + BPFType.BPFIntType.UINT8), Arguments.of(2, BPFType.BPFIntType.UINT16), Arguments.of(4, + BPFType.BPFIntType.UINT32), Arguments.of(8, BPFType.BPFIntType.UINT64), Arguments.of(8, + BPFType.BPFIntType.POINTER)); + } + + @ParameterizedTest + @MethodSource("provideTestAlignmentOfScalarsParameters") + public void testAlignmentOfScalars(int expectedAlignment, BPFType bpfType) { + assertEquals(expectedAlignment, bpfType.alignment()); + } + + private static void assertOffsetSameInLayoutAndMembers(BPFStructType type) { + for (var member : type.members()) { + assertEquals(type.getOffsetOfMember(member.name()), + type.layout().byteOffset(MemoryLayout.PathElement.groupElement(member.name()))); + } + } + + /** + * Test layouting of {@link RingBufferTest.Event} struct + */ + @Test + public void testRingSampleStruct() { + + final int FILE_NAME_LEN = 256; + final int TASK_COMM_LEN = 16; + record Event(@Unsigned int pid, String filename, String comm) { + } + + var eventType = BPFStructType.autoLayout("rb", List.of(new UBPFStructMember<>("e_pid", + BPFType.BPFIntType.UINT32, null), new UBPFStructMember<>("e_filename", + new BPFType.StringType(FILE_NAME_LEN), null), new UBPFStructMember<>("e_comm", + new BPFType.StringType(TASK_COMM_LEN), null)), null, fields -> null); + + assertEquals(0, eventType.getOffsetOfMember("e_pid")); + assertEquals(4, eventType.getOffsetOfMember("e_filename")); + assertEquals(260, eventType.getOffsetOfMember("e_comm")); + assertOffsetSameInLayoutAndMembers(eventType); + assertEquals(276, eventType.layout().byteSize()); + assertEquals(4, eventType.layout().byteAlignment()); + assertEquals(276, eventType.sizePadded()); + } + + /** + * Test layouting more complex struct + */ + @Test + public void testStructWithPadding() { + record PaddingEvent(byte c, long l, int i, @Unsigned long x, boolean b) { + } + + var type = BPFStructType.autoLayout("padding", List.of(new UBPFStructMember<>("c", BPFType.BPFIntType.UINT8, + null), new UBPFStructMember<>("l", BPFType.BPFIntType.UINT64, null), new UBPFStructMember<>("i", + BPFType.BPFIntType.INT32, null), new UBPFStructMember<>("x", BPFType.BPFIntType.UINT64, null), + new UBPFStructMember<>("b", BPFType.BPFIntType.BOOL, null)), null, fields -> null); + + assertEquals(0, type.getOffsetOfMember("c")); + assertEquals(8, type.getOffsetOfMember("l")); + assertEquals(16, type.getOffsetOfMember("i")); + assertEquals(24, type.getOffsetOfMember("x")); + assertEquals(32, type.getOffsetOfMember("b")); + assertOffsetSameInLayoutAndMembers(type); + assertEquals(8, type.alignment()); + assertEquals(8, type.layout().byteAlignment()); + assertEquals(33, type.size()); + assertEquals(40, type.sizePadded()); + } + + /** + * Test layouting of basic array + */ + @Test + public void testBasicArray() { + var type = new BPFType.BPFArrayType<>("arr", BPFType.BPFIntType.INT32, 10); + + assertEquals(0, type.getOffsetAtIndex(0)); + assertEquals(4, type.getOffsetAtIndex(1)); + assertEquals(40, type.size()); + assertEquals(40, type.sizePadded()); + assertEquals(4, type.alignment()); + } + + @Test + public void testArrayWithPadding() { + // assume inner struct has 8 byte alignment and size 16 + record PaddingEntry(long l, byte c) { + } + + var entryType = BPFStructType.autoLayout("entry", List.of(new UBPFStructMember<>("l", BPFType.BPFIntType.UINT64, + null), new UBPFStructMember<>("c", BPFType.BPFIntType.UINT8, null)), null, fields -> null); + + var type = BPFType.BPFArrayType.of(entryType, 10); + assertEquals(0, type.getOffsetAtIndex(0)); + assertEquals(16, type.getOffsetAtIndex(1)); + assertEquals(160, type.size()); + assertEquals(160, type.sizePadded()); + assertEquals(8, type.alignment()); + } + +} diff --git a/shared/src/main/java/me/bechberger/ebpf/shared/BPFType.java b/shared/src/main/java/me/bechberger/ebpf/shared/BPFType.java index ce0871a..0c917e8 100644 --- a/shared/src/main/java/me/bechberger/ebpf/shared/BPFType.java +++ b/shared/src/main/java/me/bechberger/ebpf/shared/BPFType.java @@ -7,13 +7,12 @@ import java.lang.foreign.MemoryLayout; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.function.Function; import java.util.stream.IntStream; +import static me.bechberger.ebpf.shared.BPFType.BPFIntType.CHAR; + /** * A BPF type, see Linux BTF documentation for @@ -61,11 +60,16 @@ default long size() { return layout().byteSize(); } + /** + * Alignment of the type in bytes + */ + long alignment(); + /** * Padded size of the type in bytes, use for all array index computations */ default long sizePadded() { - return PanamaUtil.padSize(layout().byteSize()); + return PanamaUtil.padSize(layout().byteSize(), alignment()); } /** @@ -76,7 +80,6 @@ default long sizePadded() { /** * Make sure to guarantee type-safety */ - @SuppressWarnings("unchecked") default T parseMemory(MemorySegment segment) { return parser().parse(segment); } @@ -89,7 +92,7 @@ default void setMemory(MemorySegment segment, T obj) { * Integer */ record BPFIntType(String bpfName, MemoryLayout layout, MemoryParser parser, MemorySetter setter, - AnnotatedClass javaClass, int encoding) implements BPFType { + AnnotatedClass javaClass, int encoding) implements BPFType { static final int ENCODING_SIGNED = 1; /** * used for pretty printing @@ -112,63 +115,140 @@ public boolean isBool() { return (encoding & ENCODING_BOOL) != 0; } + @Override + public long alignment() { + return size(); + } + + private static final Map> registeredTypes = new HashMap<>(); + /** - * u64 mapped to {@code @Unsigned long} + * Create a new BPFIntType and register it */ - public static BPFIntType UINT64 = new BPFIntType<>("u64", ValueLayout.JAVA_LONG, segment -> { - return segment.get(ValueLayout.JAVA_LONG, 0); - }, (segment, obj) -> { - segment.set(ValueLayout.JAVA_LONG, 0, obj); - }, new AnnotatedClass(long.class, List.of(AnnotationInstances.UNSIGNED)), 0); + private static BPFIntType createType(String bpfName, Class klass, V layout, + MemoryParser parser, + MemorySetter setter, boolean signed) { + var type = new BPFIntType<>(bpfName, layout, parser, setter, new AnnotatedClass(klass, signed ? + List.of(AnnotationInstances.UNSIGNED) : List.of()), signed ? ENCODING_SIGNED : 0); + if (registeredTypes.containsKey(type.javaClass())) { + throw new IllegalArgumentException("Type " + type.javaClass() + " already registered as " + registeredTypes.get(type.javaClass()).bpfName()); + } + registeredTypes.put(type.javaClass(), type); + return type; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Optional> getRegisteredType(AnnotatedClass klass) { + return (Optional>) (Optional) Optional.ofNullable(registeredTypes.get(klass)); + } /** - * u32 mapped to {@code @Unsigned int} + * bool/u8 mapped to {@code boolean} */ - public static BPFIntType UINT32 = new BPFIntType<>("u32", ValueLayout.JAVA_INT, segment -> { - return segment.get(ValueLayout.JAVA_INT, 0); + public static final BPFIntType BOOL = createType("bool", Boolean.class, ValueLayout.JAVA_BYTE, + segment -> { + return segment.get(ValueLayout.JAVA_BYTE, 0) == 1; }, (segment, obj) -> { - segment.set(ValueLayout.JAVA_INT, 0, obj); - }, new AnnotatedClass(int.class, List.of(AnnotationInstances.UNSIGNED)), 0); + segment.set(ValueLayout.JAVA_BYTE, 0, obj ? (byte) 1 : 0); + }, false); /** - * s32 mapped to {@code int} + * char mapped to {@code char} */ - public static BPFIntType INT32 = new BPFIntType<>("s32", ValueLayout.JAVA_INT, segment -> { - return segment.get(ValueLayout.JAVA_INT, 0); + public static final BPFIntType CHAR = createType("char", Character.class, ValueLayout.JAVA_CHAR, + segment -> { + return segment.get(ValueLayout.JAVA_CHAR, 0); }, (segment, obj) -> { - segment.set(ValueLayout.JAVA_INT, 0, obj); - }, new AnnotatedClass(int.class, List.of()), ENCODING_SIGNED); + segment.set(ValueLayout.JAVA_CHAR, 0, obj); + }, false); /** - * char mapped to {@code char} + * i8 mapped to {@code byte} */ - public static BPFIntType CHAR = new BPFIntType<>("char", ValueLayout.JAVA_BYTE, segment -> { + public static final BPFIntType INT8 = createType("s8", Byte.class, ValueLayout.JAVA_BYTE, segment -> { return segment.get(ValueLayout.JAVA_BYTE, 0); }, (segment, obj) -> { segment.set(ValueLayout.JAVA_BYTE, 0, obj); - }, new AnnotatedClass(byte.class, List.of()), ENCODING_CHAR); + }, true); - public static BPFIntType UINT8 = new BPFIntType<>("u8", ValueLayout.JAVA_BYTE, segment -> { + /** + * u8 mapped to {@code @Unsigned byte} + */ + public static final BPFIntType UINT8 = createType("u8", Byte.class, ValueLayout.JAVA_BYTE, segment -> { return segment.get(ValueLayout.JAVA_BYTE, 0); }, (segment, obj) -> { segment.set(ValueLayout.JAVA_BYTE, 0, obj); - }, new AnnotatedClass(byte.class, List.of()), 0); + }, false); - public static BPFIntType INT16 = new BPFIntType<>("s16", ValueLayout.JAVA_SHORT, segment -> { + /** + * s16 mapped to {@code short} + */ + public static final BPFIntType INT16 = createType("s16", Short.class, ValueLayout.JAVA_SHORT, + segment -> { return segment.get(ValueLayout.JAVA_SHORT, 0); }, (segment, obj) -> { segment.set(ValueLayout.JAVA_SHORT, 0, obj); - }, new AnnotatedClass(short.class, List.of()), ENCODING_SIGNED); + }, true); - public static BPFIntType UINT16 = new BPFIntType<>("u16", ValueLayout.JAVA_SHORT, segment -> { + /** + * u16 mapped to {@code @Unsigned short} + */ + public static final BPFIntType UINT16 = createType("u16", Short.class, ValueLayout.JAVA_SHORT, + segment -> { return segment.get(ValueLayout.JAVA_SHORT, 0); }, (segment, obj) -> { segment.set(ValueLayout.JAVA_SHORT, 0, obj); - }, new AnnotatedClass(short.class, List.of()), 0); + }, false); + + /** + * s32 mapped to {@code int} + */ + public static final BPFIntType INT32 = createType("s32", Integer.class, ValueLayout.JAVA_INT, + segment -> { + return segment.get(ValueLayout.JAVA_INT, 0); + }, (segment, obj) -> { + segment.set(ValueLayout.JAVA_INT, 0, obj); + }, true); + + /** + * u32 mapped to {@code @Unsigned int} + */ + public static final BPFIntType UINT32 = createType("u32", Integer.class, ValueLayout.JAVA_INT, + segment -> { + return segment.get(ValueLayout.JAVA_INT, 0); + }, (segment, obj) -> { + segment.set(ValueLayout.JAVA_INT, 0, obj); + }, false); + + + /** + * s64 mapped to {@code long} + */ + public static final BPFIntType INT64 = createType("s64", Long.class, ValueLayout.JAVA_LONG, segment -> { + return segment.get(ValueLayout.JAVA_LONG, 0); + }, (segment, obj) -> { + segment.set(ValueLayout.JAVA_LONG, 0, obj); + }, true); + + /** + * u64 mapped to {@code @Unsigned long} + */ + public static final BPFIntType UINT64 = createType("u64", Long.class, ValueLayout.JAVA_LONG, segment -> { + return segment.get(ValueLayout.JAVA_LONG, 0); + }, (segment, obj) -> { + segment.set(ValueLayout.JAVA_LONG, 0, obj); + }, false); + + + /** + * void* + */ + public static final BPFType POINTER = new BPFTypedef<>("void*", BPFIntType.UINT64); + } /** - * Struct member + * Struct member with manually set offset * * @param name name of the member * @param type type of the member @@ -179,30 +259,114 @@ record BPFStructMember(String name, BPFType type, int offset, Function< } /** - * Struct + * Unpositioned struct member * - * @param bpfName name of the struct in BPF - * @param members members of the struct, order should be the same as in the constructor - * @param javaClass class that represents the struct - * @param constructor constructor that takes the members in the same order as in the constructor + * @param name name of the member + * @param type type of the member + * @param getter function that takes the struct and returns the member */ - record BPFStructType(String bpfName, List> members, AnnotatedClass javaClass, - Function, T> constructor) implements BPFType { + record UBPFStructMember(String name, BPFType type, Function getter) { + public BPFStructMember position(int offset) { + return new BPFStructMember<>(name, type, offset, getter); + } + } + /** + * Struct + */ + final class BPFStructType implements BPFType { + private final String bpfName; + private final MemoryLayout layout; + + private final long alignment; + private final List> members; + private final AnnotatedClass javaClass; + private final Function, T> constructor; + + /** + * Create a new struct type with manually set layout, + * consider using {@link #autoLayout(String, List, AnnotatedClass, Function)} + * for creating the layout automatically + * + * @param bpfName name of the struct in BPF + * @param members members of the struct, order should be the same as in the constructor + * @param javaClass class that represents the struct + * @param constructor constructor that takes the members in the same order as in the constructor + */ + public BPFStructType(String bpfName, List> members, AnnotatedClass javaClass, + Function, T> constructor) { + this.bpfName = bpfName; + this.layout = createLayout(members); + this.alignment = members.stream().mapToLong(m -> m.type.alignment()).max().orElse(1); + this.members = members; + this.javaClass = javaClass; + this.constructor = constructor; + } + + public static BPFStructType autoLayout(String bpfName, List> members, + AnnotatedClass javaClass, Function, T> constructor) { + return new BPFStructType<>(bpfName, layoutMembers(members), javaClass, constructor); + } + + /** + * Creates the memory layout, inserting padding where neccessary + */ + private MemoryLayout createLayout(List> members) { + List layouts = new ArrayList<>(); + for (int i = 0; i < members.size(); i++) { + var member = members.get(i); + if (i != 0) { + var prev = members.get(i - 1); + var padding = member.offset - (prev.offset + prev.type.size()); + if (padding > 0) { + layouts.add(MemoryLayout.paddingLayout(padding)); + } + } + layouts.add(member.type.layout().withName(member.name())); + } + return MemoryLayout.structLayout(layouts.toArray(new MemoryLayout[0])); + } + + private static List> layoutMembers(List> members) { + List> result = new ArrayList<>(); + long offset = 0; + for (var member : members) { + offset = PanamaUtil.padSize(offset, member.type.alignment()); + result.add(member.position((int) offset)); + offset += member.type.size(); + } + return result; + } + + /** + * Layout that represents the struct, including padding, first level members are properly named + */ @Override public MemoryLayout layout() { - return MemoryLayout.sequenceLayout(size(), ValueLayout.JAVA_BYTE); + return layout; + } + + public long alignment() { + return alignment; + } + + /** + * Returns the offset of the passed member + */ + public int getOffsetOfMember(String memberName) { + return members.stream().filter(m -> m.name().equals(memberName)).findFirst().map(BPFStructMember::offset).orElseThrow(); } @Override public long size() { - return members.stream().mapToLong(member -> member.type.size() + member.offset).max().orElseThrow(); + return layout.byteSize(); } @Override public MemoryParser parser() { return segment -> { - List args = members.stream().map(member -> (Object)member.type.parseMemory(segment.asSlice(member.offset))).toList(); + List args = + members.stream().map(member -> (Object) member.type.parseMemory(segment.asSlice(member.offset))).toList(); return constructor.apply(args); }; } @@ -212,11 +376,47 @@ public MemoryParser parser() { public MemorySetter setter() { return (segment, obj) -> { for (BPFStructMember member : members) { - ((BPFType)member.type).setMemory(segment.asSlice(member.offset), - member.getter.apply(obj)); + ((BPFType) member.type).setMemory(segment.asSlice(member.offset), member.getter.apply(obj)); } }; } + + @Override + public String bpfName() { + return bpfName; + } + + public List> members() { + return members; + } + + @Override + public AnnotatedClass javaClass() { + return javaClass; + } + + public Function, T> constructor() { + return constructor; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (BPFStructType) obj; + return Objects.equals(this.bpfName, that.bpfName) && Objects.equals(this.members, that.members) && Objects.equals(this.javaClass, that.javaClass) && Objects.equals(this.constructor, that.constructor); + } + + @Override + public int hashCode() { + return Objects.hash(bpfName, members, javaClass, constructor); + } + + @Override + public String toString() { + return "BPFStructType[" + "bpfName=" + bpfName + ", " + "members=" + members + ", " + "javaClass=" + javaClass + ", " + "constructor=" + constructor + ']'; + } + } /** @@ -226,7 +426,16 @@ record BPFArrayType(String bpfName, BPFType memberType, int length) implem @Override public MemoryLayout layout() { - return MemoryLayout.sequenceLayout(length, memberType.layout()); + return MemoryLayout.sequenceLayout(length, paddedMemberLayout()); + } + + public MemoryLayout paddedMemberLayout() { + var padding = memberType.sizePadded() - memberType.size(); + if (padding == 0) { + return memberType.layout(); + } else { + return MemoryLayout.structLayout(memberType.layout(), MemoryLayout.paddingLayout(padding)); + } } @Override @@ -246,14 +455,22 @@ public MemorySetter> setter() { }; } + @Override + public long alignment() { + return memberType.alignment(); + } + @Override public AnnotatedClass javaClass() { return new AnnotatedClass(List.class, List.of(AnnotationInstances.size(length))); } + public long getOffsetAtIndex(int index) { + return index * memberType.sizePadded(); + } + public static BPFArrayType of(BPFType memberType, int length) { - return new BPFArrayType<>(memberType.bpfName() + "[" + length + "]", - memberType, length); + return new BPFArrayType<>(memberType.bpfName() + "[" + length + "]", memberType, length); } } @@ -297,6 +514,11 @@ public MemorySetter setter() { }; } + @Override + public long alignment() { + return CHAR.alignment(); + } + @Override public AnnotatedClass javaClass() { return new AnnotatedClass(String.class, List.of(AnnotationInstances.size(length))); @@ -323,6 +545,11 @@ public MemorySetter setter() { return wrapped.setter(); } + @Override + public long alignment() { + return wrapped.alignment(); + } + @Override public AnnotatedClass javaClass() { return wrapped.javaClass(); @@ -340,7 +567,8 @@ record BPFUnionTypeMember(String name, BPFType type) { * @param shared type that is shared between all members * @param members members of the union, including the shared type members */ - record BPFUnionType(String bpfName, @Nullable BPFType shared, List members) implements BPFType> { + record BPFUnionType(String bpfName, @Nullable BPFType shared, + List members) implements BPFType> { @Override public MemoryLayout layout() { @@ -349,10 +577,12 @@ public MemoryLayout layout() { @Override public long size() { - return members.stream() - .mapToLong(member -> member.type.size()) - .max() - .orElseThrow(); + return members.stream().mapToLong(member -> member.type.size()).max().orElseThrow(); + } + + @Override + public long alignment() { + return members.stream().mapToLong(member -> member.type.alignment()).max().orElse(1); } @Override @@ -379,7 +609,8 @@ public MemorySetter> setter() { if (union.current() == null) { throw new IllegalArgumentException("Union must have a current member"); } - BPFUnionTypeMember current = members().stream().filter(m -> m.name.equals(union.current())).findFirst().orElseThrow(); + BPFUnionTypeMember current = + members().stream().filter(m -> m.name.equals(union.current())).findFirst().orElseThrow(); current.type.setMemory(segment, union.get(union.current())); }; } @@ -448,8 +679,8 @@ public boolean equals(Object obj) { if (obj == this) return true; if (obj == null || obj.getClass() != this.getClass()) return false; var that = (BPFUnionFromMemory) obj; - return Objects.equals(this.shared, that.shared) && - Objects.equals(this.possibleMembers, that.possibleMembers); + return Objects.equals(this.shared, that.shared) && Objects.equals(this.possibleMembers, + that.possibleMembers); } } } diff --git a/shared/src/main/java/me/bechberger/ebpf/shared/PanamaUtil.java b/shared/src/main/java/me/bechberger/ebpf/shared/PanamaUtil.java index 6275a0c..47d5528 100644 --- a/shared/src/main/java/me/bechberger/ebpf/shared/PanamaUtil.java +++ b/shared/src/main/java/me/bechberger/ebpf/shared/PanamaUtil.java @@ -154,6 +154,10 @@ public static long padSize(long size) { return (size + 7) & ~7; } + public static long padSize(long size, long alignment) { + return (size + alignment - 1) & -alignment; + } + /** * Allocate a reference to an int in the given arena */