]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
selftests/bpf: utility function to get program disassembly after jit
authorEduard Zingerman <eddyz87@gmail.com>
Tue, 20 Aug 2024 10:23:53 +0000 (03:23 -0700)
committerAlexei Starovoitov <ast@kernel.org>
Wed, 21 Aug 2024 18:03:01 +0000 (11:03 -0700)
This commit adds a utility function to get disassembled text for jited
representation of a BPF program designated by file descriptor.
Function prototype looks as follows:

    int get_jited_program_text(int fd, char *text, size_t text_sz)

Where 'fd' is a file descriptor for the program, 'text' and 'text_sz'
refer to a destination buffer for disassembled text.
Output format looks as follows:

    18: 77 06                                ja L0
    1a: 50                                   pushq %rax
    1b: 48 89 e0                             movq %rsp, %rax
    1e: eb 01                                jmp L1
    20: 50                                  L0: pushq %rax
    21: 50                                  L1: pushq %rax
     ^  ^^^^^^^^                             ^  ^^^^^^^^^^^^^^^^^^
     |  binary insn                          |  textual insn
     |  representation                       |  representation
     |                                       |
    instruction offset              inferred local label name

The code and makefile changes are inspired by jit_disasm.c from bpftool.
Use llvm libraries to disassemble BPF program instead of libbfd to avoid
issues with disassembly output stability pointed out in [1].

Selftests makefile uses Makefile.feature to detect if LLVM libraries
are available. If that is not the case selftests build proceeds but
the function returns -EOPNOTSUPP at runtime.

[1] commit eb9d1acf634b ("bpftool: Add LLVM as default library for disassembling JIT-ed programs")

Acked-by: Yonghong Song <yonghong.song@linux.dev>
Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20240820102357.3372779-6-eddyz87@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
tools/testing/selftests/bpf/.gitignore
tools/testing/selftests/bpf/Makefile
tools/testing/selftests/bpf/jit_disasm_helpers.c [new file with mode: 0644]
tools/testing/selftests/bpf/jit_disasm_helpers.h [new file with mode: 0644]

index 99ffea1fa5c6df44367d09a99ba77ac003783c31..e6533b3400de5ef9b0c0e02aa0e1afedcab9b349 100644 (file)
@@ -8,6 +8,7 @@ test_lru_map
 test_lpm_map
 test_tag
 FEATURE-DUMP.libbpf
+FEATURE-DUMP.selftests
 fixdep
 /test_progs
 /test_progs-no_alu32
index 7bca9a2b8bc07f8923b5396a4427993f9d2bc83a..6d6d0c29db475c13d2705d8a5ae210a4248a305a 100644 (file)
@@ -33,6 +33,13 @@ OPT_FLAGS    ?= $(if $(RELEASE),-O2,-O0)
 LIBELF_CFLAGS  := $(shell $(PKG_CONFIG) libelf --cflags 2>/dev/null)
 LIBELF_LIBS    := $(shell $(PKG_CONFIG) libelf --libs 2>/dev/null || echo -lelf)
 
+ifeq ($(srctree),)
+srctree := $(patsubst %/,%,$(dir $(CURDIR)))
+srctree := $(patsubst %/,%,$(dir $(srctree)))
+srctree := $(patsubst %/,%,$(dir $(srctree)))
+srctree := $(patsubst %/,%,$(dir $(srctree)))
+endif
+
 CFLAGS += -g $(OPT_FLAGS) -rdynamic                                    \
          -Wall -Werror -fno-omit-frame-pointer                         \
          $(GENFLAGS) $(SAN_CFLAGS) $(LIBELF_CFLAGS)                    \
@@ -60,6 +67,9 @@ progs/timer_crash.c-CFLAGS := -fno-strict-aliasing
 progs/test_global_func9.c-CFLAGS := -fno-strict-aliasing
 progs/verifier_nocsr.c-CFLAGS := -fno-strict-aliasing
 
+# Some utility functions use LLVM libraries
+jit_disasm_helpers.c-CFLAGS = $(LLVM_CFLAGS)
+
 ifneq ($(LLVM),)
 # Silence some warnings when compiled with clang
 CFLAGS += -Wno-unused-command-line-argument
@@ -168,6 +178,31 @@ endef
 
 include ../lib.mk
 
+NON_CHECK_FEAT_TARGETS := clean docs-clean
+CHECK_FEAT := $(filter-out $(NON_CHECK_FEAT_TARGETS),$(or $(MAKECMDGOALS), "none"))
+ifneq ($(CHECK_FEAT),)
+FEATURE_USER := .selftests
+FEATURE_TESTS := llvm
+FEATURE_DISPLAY := $(FEATURE_TESTS)
+
+# Makefile.feature expects OUTPUT to end with a slash
+$(let OUTPUT,$(OUTPUT)/,\
+       $(eval include ../../../build/Makefile.feature))
+endif
+
+ifeq ($(feature-llvm),1)
+  LLVM_CFLAGS  += -DHAVE_LLVM_SUPPORT
+  LLVM_CONFIG_LIB_COMPONENTS := mcdisassembler all-targets
+  # both llvm-config and lib.mk add -D_GNU_SOURCE, which ends up as conflict
+  LLVM_CFLAGS  += $(filter-out -D_GNU_SOURCE,$(shell $(LLVM_CONFIG) --cflags))
+  LLVM_LDLIBS  += $(shell $(LLVM_CONFIG) --libs $(LLVM_CONFIG_LIB_COMPONENTS))
+  ifeq ($(shell $(LLVM_CONFIG) --shared-mode),static)
+    LLVM_LDLIBS += $(shell $(LLVM_CONFIG) --system-libs $(LLVM_CONFIG_LIB_COMPONENTS))
+    LLVM_LDLIBS += -lstdc++
+  endif
+  LLVM_LDFLAGS += $(shell $(LLVM_CONFIG) --ldflags)
+endif
+
 SCRATCH_DIR := $(OUTPUT)/tools
 BUILD_DIR := $(SCRATCH_DIR)/build
 INCLUDE_DIR := $(SCRATCH_DIR)/include
@@ -612,6 +647,10 @@ ifeq ($(filter clean docs-clean,$(MAKECMDGOALS)),)
 include $(wildcard $(TRUNNER_TEST_OBJS:.o=.d))
 endif
 
+# add per extra obj CFGLAGS definitions
+$(foreach N,$(patsubst $(TRUNNER_OUTPUT)/%.o,%,$(TRUNNER_EXTRA_OBJS)), \
+       $(eval $(TRUNNER_OUTPUT)/$(N).o: CFLAGS += $($(N).c-CFLAGS)))
+
 $(TRUNNER_EXTRA_OBJS): $(TRUNNER_OUTPUT)/%.o:                          \
                       %.c                                              \
                       $(TRUNNER_EXTRA_HDRS)                            \
@@ -628,6 +667,9 @@ ifneq ($2:$(OUTPUT),:$(shell pwd))
        $(Q)rsync -aq $$^ $(TRUNNER_OUTPUT)/
 endif
 
+$(OUTPUT)/$(TRUNNER_BINARY): LDLIBS += $$(LLVM_LDLIBS)
+$(OUTPUT)/$(TRUNNER_BINARY): LDFLAGS += $$(LLVM_LDFLAGS)
+
 # some X.test.o files have runtime dependencies on Y.bpf.o files
 $(OUTPUT)/$(TRUNNER_BINARY): | $(TRUNNER_BPF_OBJS)
 
@@ -637,7 +679,7 @@ $(OUTPUT)/$(TRUNNER_BINARY): $(TRUNNER_TEST_OBJS)                   \
                             $(TRUNNER_BPFTOOL)                         \
                             | $(TRUNNER_BINARY)-extras
        $$(call msg,BINARY,,$$@)
-       $(Q)$$(CC) $$(CFLAGS) $$(filter %.a %.o,$$^) $$(LDLIBS) -o $$@
+       $(Q)$$(CC) $$(CFLAGS) $$(filter %.a %.o,$$^) $$(LDLIBS) $$(LDFLAGS) -o $$@
        $(Q)$(RESOLVE_BTFIDS) --btf $(TRUNNER_OUTPUT)/btf_data.bpf.o $$@
        $(Q)ln -sf $(if $2,..,.)/tools/build/bpftool/$(USE_BOOTSTRAP)bpftool \
                   $(OUTPUT)/$(if $2,$2/)bpftool
@@ -656,6 +698,7 @@ TRUNNER_EXTRA_SOURCES := test_progs.c               \
                         cap_helpers.c          \
                         unpriv_helpers.c       \
                         netlink_helpers.c      \
+                        jit_disasm_helpers.c   \
                         test_loader.c          \
                         xsk.c                  \
                         disasm.c               \
@@ -798,7 +841,8 @@ EXTRA_CLEAN := $(SCRATCH_DIR) $(HOST_SCRATCH_DIR)                   \
        $(addprefix $(OUTPUT)/,*.o *.d *.skel.h *.lskel.h *.subskel.h   \
                               no_alu32 cpuv4 bpf_gcc bpf_testmod.ko    \
                               bpf_test_no_cfi.ko                       \
-                              liburandom_read.so)
+                              liburandom_read.so)                      \
+       $(OUTPUT)/FEATURE-DUMP.selftests
 
 .PHONY: docs docs-clean
 
diff --git a/tools/testing/selftests/bpf/jit_disasm_helpers.c b/tools/testing/selftests/bpf/jit_disasm_helpers.c
new file mode 100644 (file)
index 0000000..1b0f1fd
--- /dev/null
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
+#include <bpf/bpf.h>
+#include <bpf/libbpf.h>
+#include <test_progs.h>
+
+#ifdef HAVE_LLVM_SUPPORT
+
+#include <llvm-c/Core.h>
+#include <llvm-c/Disassembler.h>
+#include <llvm-c/Target.h>
+#include <llvm-c/TargetMachine.h>
+
+/* The intent is to use get_jited_program_text() for small test
+ * programs written in BPF assembly, thus assume that 32 local labels
+ * would be sufficient.
+ */
+#define MAX_LOCAL_LABELS 32
+
+static bool llvm_initialized;
+
+struct local_labels {
+       bool print_phase;
+       __u32 prog_len;
+       __u32 cnt;
+       __u32 pcs[MAX_LOCAL_LABELS];
+       char names[MAX_LOCAL_LABELS][4];
+};
+
+static const char *lookup_symbol(void *data, uint64_t ref_value, uint64_t *ref_type,
+                                uint64_t ref_pc, const char **ref_name)
+{
+       struct local_labels *labels = data;
+       uint64_t type = *ref_type;
+       int i;
+
+       *ref_type = LLVMDisassembler_ReferenceType_InOut_None;
+       *ref_name = NULL;
+       if (type != LLVMDisassembler_ReferenceType_In_Branch)
+               return NULL;
+       /* Depending on labels->print_phase either discover local labels or
+        * return a name assigned with local jump target:
+        * - if print_phase is true and ref_value is in labels->pcs,
+        *   return corresponding labels->name.
+        * - if print_phase is false, save program-local jump targets
+        *   in labels->pcs;
+        */
+       if (labels->print_phase) {
+               for (i = 0; i < labels->cnt; ++i)
+                       if (labels->pcs[i] == ref_value)
+                               return labels->names[i];
+       } else {
+               if (labels->cnt < MAX_LOCAL_LABELS && ref_value < labels->prog_len)
+                       labels->pcs[labels->cnt++] = ref_value;
+       }
+       return NULL;
+}
+
+static int disasm_insn(LLVMDisasmContextRef ctx, uint8_t *image, __u32 len, __u32 pc,
+                      char *buf, __u32 buf_sz)
+{
+       int i, cnt;
+
+       cnt = LLVMDisasmInstruction(ctx, image + pc, len - pc, pc,
+                                   buf, buf_sz);
+       if (cnt > 0)
+               return cnt;
+       PRINT_FAIL("Can't disasm instruction at offset %d:", pc);
+       for (i = 0; i < 16 && pc + i < len; ++i)
+               printf(" %02x", image[pc + i]);
+       printf("\n");
+       return -EINVAL;
+}
+
+static int cmp_u32(const void *_a, const void *_b)
+{
+       __u32 a = *(__u32 *)_a;
+       __u32 b = *(__u32 *)_b;
+
+       if (a < b)
+               return -1;
+       if (a > b)
+               return 1;
+       return 0;
+}
+
+static int disasm_one_func(FILE *text_out, uint8_t *image, __u32 len)
+{
+       char *label, *colon, *triple = NULL;
+       LLVMDisasmContextRef ctx = NULL;
+       struct local_labels labels = {};
+       __u32 *label_pc, pc;
+       int i, cnt, err = 0;
+       char buf[64];
+
+       triple = LLVMGetDefaultTargetTriple();
+       ctx = LLVMCreateDisasm(triple, &labels, 0, NULL, lookup_symbol);
+       if (!ASSERT_OK_PTR(ctx, "LLVMCreateDisasm")) {
+               err = -EINVAL;
+               goto out;
+       }
+
+       cnt = LLVMSetDisasmOptions(ctx, LLVMDisassembler_Option_PrintImmHex);
+       if (!ASSERT_EQ(cnt, 1, "LLVMSetDisasmOptions")) {
+               err = -EINVAL;
+               goto out;
+       }
+
+       /* discover labels */
+       labels.prog_len = len;
+       pc = 0;
+       while (pc < len) {
+               cnt = disasm_insn(ctx, image, len, pc, buf, 1);
+               if (cnt < 0) {
+                       err = cnt;
+                       goto out;
+               }
+               pc += cnt;
+       }
+       qsort(labels.pcs, labels.cnt, sizeof(*labels.pcs), cmp_u32);
+       for (i = 0; i < labels.cnt; ++i)
+               /* use (i % 100) to avoid format truncation warning */
+               snprintf(labels.names[i], sizeof(labels.names[i]), "L%d", i % 100);
+
+       /* now print with labels */
+       labels.print_phase = true;
+       pc = 0;
+       while (pc < len) {
+               cnt = disasm_insn(ctx, image, len, pc, buf, sizeof(buf));
+               if (cnt < 0) {
+                       err = cnt;
+                       goto out;
+               }
+               label_pc = bsearch(&pc, labels.pcs, labels.cnt, sizeof(*labels.pcs), cmp_u32);
+               label = "";
+               colon = "";
+               if (label_pc) {
+                       label = labels.names[label_pc - labels.pcs];
+                       colon = ":";
+               }
+               fprintf(text_out, "%x:\t", pc);
+               for (i = 0; i < cnt; ++i)
+                       fprintf(text_out, "%02x ", image[pc + i]);
+               for (i = cnt * 3; i < 12 * 3; ++i)
+                       fputc(' ', text_out);
+               fprintf(text_out, "%s%s%s\n", label, colon, buf);
+               pc += cnt;
+       }
+
+out:
+       if (triple)
+               LLVMDisposeMessage(triple);
+       if (ctx)
+               LLVMDisasmDispose(ctx);
+       return err;
+}
+
+int get_jited_program_text(int fd, char *text, size_t text_sz)
+{
+       struct bpf_prog_info info = {};
+       __u32 info_len = sizeof(info);
+       __u32 jited_funcs, len, pc;
+       __u32 *func_lens = NULL;
+       FILE *text_out = NULL;
+       uint8_t *image = NULL;
+       int i, err = 0;
+
+       if (!llvm_initialized) {
+               LLVMInitializeAllTargetInfos();
+               LLVMInitializeAllTargetMCs();
+               LLVMInitializeAllDisassemblers();
+               llvm_initialized = 1;
+       }
+
+       text_out = fmemopen(text, text_sz, "w");
+       if (!ASSERT_OK_PTR(text_out, "open_memstream")) {
+               err = -errno;
+               goto out;
+       }
+
+       /* first call is to find out jited program len */
+       err = bpf_prog_get_info_by_fd(fd, &info, &info_len);
+       if (!ASSERT_OK(err, "bpf_prog_get_info_by_fd #1"))
+               goto out;
+
+       len = info.jited_prog_len;
+       image = malloc(len);
+       if (!ASSERT_OK_PTR(image, "malloc(info.jited_prog_len)")) {
+               err = -ENOMEM;
+               goto out;
+       }
+
+       jited_funcs = info.nr_jited_func_lens;
+       func_lens = malloc(jited_funcs * sizeof(__u32));
+       if (!ASSERT_OK_PTR(func_lens, "malloc(info.nr_jited_func_lens)")) {
+               err = -ENOMEM;
+               goto out;
+       }
+
+       memset(&info, 0, sizeof(info));
+       info.jited_prog_insns = (__u64)image;
+       info.jited_prog_len = len;
+       info.jited_func_lens = (__u64)func_lens;
+       info.nr_jited_func_lens = jited_funcs;
+       err = bpf_prog_get_info_by_fd(fd, &info, &info_len);
+       if (!ASSERT_OK(err, "bpf_prog_get_info_by_fd #2"))
+               goto out;
+
+       for (pc = 0, i = 0; i < jited_funcs; ++i) {
+               fprintf(text_out, "func #%d:\n", i);
+               disasm_one_func(text_out, image + pc, func_lens[i]);
+               fprintf(text_out, "\n");
+               pc += func_lens[i];
+       }
+
+out:
+       if (text_out)
+               fclose(text_out);
+       if (image)
+               free(image);
+       if (func_lens)
+               free(func_lens);
+       return err;
+}
+
+#else /* HAVE_LLVM_SUPPORT */
+
+int get_jited_program_text(int fd, char *text, size_t text_sz)
+{
+       if (env.verbosity >= VERBOSE_VERY)
+               printf("compiled w/o llvm development libraries, can't dis-assembly binary code");
+       return -EOPNOTSUPP;
+}
+
+#endif /* HAVE_LLVM_SUPPORT */
diff --git a/tools/testing/selftests/bpf/jit_disasm_helpers.h b/tools/testing/selftests/bpf/jit_disasm_helpers.h
new file mode 100644 (file)
index 0000000..e6924fd
--- /dev/null
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
+
+#ifndef __JIT_DISASM_HELPERS_H
+#define __JIT_DISASM_HELPERS_H
+
+#include <stddef.h>
+
+int get_jited_program_text(int fd, char *text, size_t text_sz);
+
+#endif /* __JIT_DISASM_HELPERS_H */