Commit b991fc52 authored by Eduard Zingerman's avatar Eduard Zingerman Committed by Alexei Starovoitov
Browse files

selftests/bpf: utility function to get program disassembly after jit



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 eb9d1acf ("bpftool: Add LLVM as default library for disassembling JIT-ed programs")

Acked-by: default avatarYonghong Song <yonghong.song@linux.dev>
Signed-off-by: default avatarEduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20240820102357.3372779-6-eddyz87@gmail.com


Signed-off-by: default avatarAlexei Starovoitov <ast@kernel.org>
parent f8d16175
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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
+46 −2
Original line number Diff line number Diff line
@@ -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

+234 −0
Original line number Diff line number Diff line
// 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 */
+10 −0
Original line number Diff line number Diff line
/* 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 */