From 70589843b36fee0c6e73632469da4e5fd11f0968 Mon Sep 17 00:00:00 2001 From: Alexandre Chartre Date: Fri, 21 Nov 2025 10:53:20 +0100 Subject: [PATCH] objtool: Add option to trace function validation Add an option to trace and have information during the validation of specified functions. Functions are specified with the --trace option which can be a single function name (e.g. --trace foo to trace the function with the name "foo"), or a shell wildcard pattern (e.g. --trace foo* to trace all functions with a name starting with "foo"). Signed-off-by: Alexandre Chartre Signed-off-by: Peter Zijlstra (Intel) Acked-by: Josh Poimboeuf Link: https://patch.msgid.link/20251121095340.464045-11-alexandre.chartre@oracle.com --- tools/objtool/Build | 1 + tools/objtool/builtin-check.c | 1 + tools/objtool/check.c | 104 +++++++++++++++++---- tools/objtool/disas.c | 115 ++++++++++++++++++++++++ tools/objtool/include/objtool/builtin.h | 1 + tools/objtool/include/objtool/check.h | 6 +- tools/objtool/include/objtool/disas.h | 11 +++ tools/objtool/include/objtool/trace.h | 68 ++++++++++++++ tools/objtool/include/objtool/warn.h | 1 + tools/objtool/trace.c | 9 ++ 10 files changed, 299 insertions(+), 18 deletions(-) create mode 100644 tools/objtool/include/objtool/trace.h create mode 100644 tools/objtool/trace.c diff --git a/tools/objtool/Build b/tools/objtool/Build index 9d1e8f28ef953..9982e665d58da 100644 --- a/tools/objtool/Build +++ b/tools/objtool/Build @@ -9,6 +9,7 @@ objtool-y += elf.o objtool-y += objtool.o objtool-$(BUILD_DISAS) += disas.o +objtool-$(BUILD_DISAS) += trace.o objtool-$(BUILD_ORC) += orc_gen.o orc_dump.o objtool-$(BUILD_KLP) += builtin-klp.o klp-diff.o klp-post-link.o diff --git a/tools/objtool/builtin-check.c b/tools/objtool/builtin-check.c index aab7fa9c7e00a..3329d370006b4 100644 --- a/tools/objtool/builtin-check.c +++ b/tools/objtool/builtin-check.c @@ -103,6 +103,7 @@ static const struct option check_options[] = { OPT_STRING('o', "output", &opts.output, "file", "output file name"), OPT_BOOLEAN(0, "sec-address", &opts.sec_address, "print section addresses in warnings"), OPT_BOOLEAN(0, "stats", &opts.stats, "print statistics"), + OPT_STRING(0, "trace", &opts.trace, "func", "trace function validation"), OPT_BOOLEAN('v', "verbose", &opts.verbose, "verbose warnings"), OPT_BOOLEAN(0, "werror", &opts.werror, "return error on warnings"), diff --git a/tools/objtool/check.c b/tools/objtool/check.c index 0fbf0eb370516..409dec9efb494 100644 --- a/tools/objtool/check.c +++ b/tools/objtool/check.c @@ -4,6 +4,7 @@ */ #define _GNU_SOURCE /* memmem() */ +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -37,7 +39,9 @@ static struct cfi_state init_cfi; static struct cfi_state func_cfi; static struct cfi_state force_undefined_cfi; -static size_t sym_name_max_len; +struct disas_context *objtool_disas_ctx; + +size_t sym_name_max_len; struct instruction *find_insn(struct objtool_file *file, struct section *sec, unsigned long offset) @@ -3556,8 +3560,10 @@ static bool skip_alt_group(struct instruction *insn) return false; /* ANNOTATE_IGNORE_ALTERNATIVE */ - if (insn->alt_group->ignore) + if (insn->alt_group->ignore) { + TRACE_INSN(insn, "alt group ignored"); return true; + } /* * For NOP patched with CLAC/STAC, only follow the latter to avoid @@ -3663,6 +3669,8 @@ static void checksum_update_insn(struct objtool_file *file, struct symbol *func, static int validate_branch(struct objtool_file *file, struct symbol *func, struct instruction *insn, struct insn_state state); +static int do_validate_branch(struct objtool_file *file, struct symbol *func, + struct instruction *insn, struct insn_state state); static int validate_insn(struct objtool_file *file, struct symbol *func, struct instruction *insn, struct insn_state *statep, @@ -3684,8 +3692,10 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, if (!insn->hint && !insn_cfi_match(insn, &statep->cfi)) return 1; - if (insn->visited & visited) + if (insn->visited & visited) { + TRACE_INSN(insn, "already visited"); return 0; + } } else { nr_insns_visited++; } @@ -3722,8 +3732,10 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, * It will be seen later via the * straight-line path. */ - if (!prev_insn) + if (!prev_insn) { + TRACE_INSN(insn, "defer restore"); return 0; + } WARN_INSN(insn, "objtool isn't smart enough to handle this CFI save/restore combo"); return 1; @@ -3751,13 +3763,23 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, return 1; if (insn->alts) { + int i, num_alts; + + num_alts = 0; + for (alt = insn->alts; alt; alt = alt->next) + num_alts++; + + i = 1; for (alt = insn->alts; alt; alt = alt->next) { + TRACE_INSN(insn, "alternative %d/%d", i, num_alts); ret = validate_branch(file, func, alt->insn, *statep); if (ret) { BT_INSN(insn, "(alt)"); return ret; } + i++; } + TRACE_INSN(insn, "alternative DEFAULT"); } if (skip_alt_group(insn)) @@ -3769,10 +3791,16 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, switch (insn->type) { case INSN_RETURN: + TRACE_INSN(insn, "return"); return validate_return(func, insn, statep); case INSN_CALL: case INSN_CALL_DYNAMIC: + if (insn->type == INSN_CALL) + TRACE_INSN(insn, "call"); + else + TRACE_INSN(insn, "indirect call"); + ret = validate_call(file, insn, statep); if (ret) return ret; @@ -3788,13 +3816,18 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, case INSN_JUMP_CONDITIONAL: case INSN_JUMP_UNCONDITIONAL: if (is_sibling_call(insn)) { + TRACE_INSN(insn, "sibling call"); ret = validate_sibling_call(file, insn, statep); if (ret) return ret; } else if (insn->jump_dest) { - ret = validate_branch(file, func, - insn->jump_dest, *statep); + if (insn->type == INSN_JUMP_UNCONDITIONAL) + TRACE_INSN(insn, "unconditional jump"); + else + TRACE_INSN(insn, "jump taken"); + + ret = validate_branch(file, func, insn->jump_dest, *statep); if (ret) { BT_INSN(insn, "(branch)"); return ret; @@ -3804,10 +3837,12 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, if (insn->type == INSN_JUMP_UNCONDITIONAL) return 0; + TRACE_INSN(insn, "jump not taken"); break; case INSN_JUMP_DYNAMIC: case INSN_JUMP_DYNAMIC_CONDITIONAL: + TRACE_INSN(insn, "indirect jump"); if (is_sibling_call(insn)) { ret = validate_sibling_call(file, insn, statep); if (ret) @@ -3820,6 +3855,7 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, break; case INSN_SYSCALL: + TRACE_INSN(insn, "syscall"); if (func && (!next_insn || !next_insn->hint)) { WARN_INSN(insn, "unsupported instruction in callable function"); return 1; @@ -3828,6 +3864,7 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, break; case INSN_SYSRET: + TRACE_INSN(insn, "sysret"); if (func && (!next_insn || !next_insn->hint)) { WARN_INSN(insn, "unsupported instruction in callable function"); return 1; @@ -3836,6 +3873,7 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, return 0; case INSN_STAC: + TRACE_INSN(insn, "stac"); if (!opts.uaccess) break; @@ -3848,6 +3886,7 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, break; case INSN_CLAC: + TRACE_INSN(insn, "clac"); if (!opts.uaccess) break; @@ -3865,6 +3904,7 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, break; case INSN_STD: + TRACE_INSN(insn, "std"); if (statep->df) { WARN_INSN(insn, "recursive STD"); return 1; @@ -3874,6 +3914,7 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, break; case INSN_CLD: + TRACE_INSN(insn, "cld"); if (!statep->df && func) { WARN_INSN(insn, "redundant CLD"); return 1; @@ -3886,8 +3927,10 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, break; } - *dead_end = insn->dead_end; + if (insn->dead_end) + TRACE_INSN(insn, "dead end"); + *dead_end = insn->dead_end; return 0; } @@ -3897,8 +3940,8 @@ static int validate_insn(struct objtool_file *file, struct symbol *func, * each instruction and validate all the rules described in * tools/objtool/Documentation/objtool.txt. */ -static int validate_branch(struct objtool_file *file, struct symbol *func, - struct instruction *insn, struct insn_state state) +static int do_validate_branch(struct objtool_file *file, struct symbol *func, + struct instruction *insn, struct insn_state state) { struct instruction *next_insn, *prev_insn = NULL; bool dead_end; @@ -3907,7 +3950,8 @@ static int validate_branch(struct objtool_file *file, struct symbol *func, if (func && func->ignore) return 0; - while (1) { + do { + insn->trace = 0; next_insn = next_insn_to_validate(file, insn); if (opts.checksum && func && insn->sec) @@ -3930,10 +3974,15 @@ static int validate_branch(struct objtool_file *file, struct symbol *func, ret = validate_insn(file, func, insn, &state, prev_insn, next_insn, &dead_end); - if (dead_end) - break; - if (!next_insn) { + if (!insn->trace) { + if (ret) + TRACE_INSN(insn, "warning (%d)", ret); + else + TRACE_INSN(insn, NULL); + } + + if (!dead_end && !next_insn) { if (state.cfi.cfa.base == CFI_UNDEFINED) return 0; if (file->ignore_unreachables) @@ -3947,7 +3996,20 @@ static int validate_branch(struct objtool_file *file, struct symbol *func, prev_insn = insn; insn = next_insn; - } + + } while (!dead_end); + + return ret; +} + +static int validate_branch(struct objtool_file *file, struct symbol *func, + struct instruction *insn, struct insn_state state) +{ + int ret; + + trace_depth_inc(); + ret = do_validate_branch(file, func, insn, state); + trace_depth_dec(); return ret; } @@ -4408,10 +4470,18 @@ static int validate_symbol(struct objtool_file *file, struct section *sec, if (opts.checksum) checksum_init(func); + if (opts.trace && !fnmatch(opts.trace, sym->name, 0)) { + trace_enable(); + TRACE("%s: validation begin\n", sym->name); + } + ret = validate_branch(file, func, insn, *state); if (ret) BT_INSN(insn, "<=== (sym)"); + TRACE("%s: validation %s\n\n", sym->name, ret ? "failed" : "end"); + trace_disable(); + if (opts.checksum) checksum_finish(func); @@ -4823,8 +4893,6 @@ static void free_insns(struct objtool_file *file) free(chunk->addr); } -static struct disas_context *objtool_disas_ctx; - const char *objtool_disas_insn(struct instruction *insn) { struct disas_context *dctx = objtool_disas_ctx; @@ -4846,8 +4914,10 @@ int check(struct objtool_file *file) * disassembly context to disassemble instruction or function * on warning or backtrace. */ - if (opts.verbose || opts.backtrace) { + if (opts.verbose || opts.backtrace || opts.trace) { disas_ctx = disas_context_create(file); + if (!disas_ctx) + opts.trace = false; objtool_disas_ctx = disas_ctx; } diff --git a/tools/objtool/disas.c b/tools/objtool/disas.c index a030b06c121d2..0ca6e6c8559fd 100644 --- a/tools/objtool/disas.c +++ b/tools/objtool/disas.c @@ -308,6 +308,121 @@ char *disas_result(struct disas_context *dctx) return dctx->result; } +#define DISAS_INSN_OFFSET_SPACE 10 +#define DISAS_INSN_SPACE 60 + +/* + * Print a message in the instruction flow. If insn is not NULL then + * the instruction address is printed in addition of the message, + * otherwise only the message is printed. In all cases, the instruction + * itself is not printed. + */ +static int disas_vprint(FILE *stream, struct section *sec, unsigned long offset, + int depth, const char *format, va_list ap) +{ + const char *addr_str; + int i, n; + int len; + + len = sym_name_max_len + DISAS_INSN_OFFSET_SPACE; + if (depth < 0) { + len += depth; + depth = 0; + } + + n = 0; + + if (sec) { + addr_str = offstr(sec, offset); + n += fprintf(stream, "%6lx: %-*s ", offset, len, addr_str); + free((char *)addr_str); + } else { + len += DISAS_INSN_OFFSET_SPACE + 1; + n += fprintf(stream, "%-*s", len, ""); + } + + /* print vertical bars to show the code flow */ + for (i = 0; i < depth; i++) + n += fprintf(stream, "| "); + + if (format) + n += vfprintf(stream, format, ap); + + return n; +} + +/* + * Print a message in the instruction flow. If insn is not NULL then + * the instruction address is printed in addition of the message, + * otherwise only the message is printed. In all cases, the instruction + * itself is not printed. + */ +void disas_print_info(FILE *stream, struct instruction *insn, int depth, + const char *format, ...) +{ + struct section *sec; + unsigned long off; + va_list args; + + if (insn) { + sec = insn->sec; + off = insn->offset; + } else { + sec = NULL; + off = 0; + } + + va_start(args, format); + disas_vprint(stream, sec, off, depth, format, args); + va_end(args); +} + +/* + * Print an instruction address (offset and function), the instruction itself + * and an optional message. + */ +void disas_print_insn(FILE *stream, struct disas_context *dctx, + struct instruction *insn, int depth, + const char *format, ...) +{ + char fake_nop_insn[32]; + const char *insn_str; + bool fake_nop; + va_list args; + int len; + + /* + * Alternative can insert a fake nop, sometimes with no + * associated section so nothing to disassemble. + */ + fake_nop = (!insn->sec && insn->type == INSN_NOP); + if (fake_nop) { + snprintf(fake_nop_insn, 32, " (%d bytes)", insn->len); + insn_str = fake_nop_insn; + } else { + disas_insn(dctx, insn); + insn_str = disas_result(dctx); + } + + /* print the instruction */ + len = (depth + 1) * 2 < DISAS_INSN_SPACE ? DISAS_INSN_SPACE - (depth+1) * 2 : 1; + disas_print_info(stream, insn, depth, "%-*s", len, insn_str); + + /* print message if any */ + if (!format) + return; + + if (strcmp(format, "\n") == 0) { + fprintf(stream, "\n"); + return; + } + + fprintf(stream, " - "); + va_start(args, format); + vfprintf(stream, format, args); + va_end(args); +} + /* * Disassemble a single instruction. Return the size of the instruction. */ diff --git a/tools/objtool/include/objtool/builtin.h b/tools/objtool/include/objtool/builtin.h index bb0b25eb08ba4..991365c10f0e9 100644 --- a/tools/objtool/include/objtool/builtin.h +++ b/tools/objtool/include/objtool/builtin.h @@ -41,6 +41,7 @@ struct opts { const char *output; bool sec_address; bool stats; + const char *trace; bool verbose; bool werror; }; diff --git a/tools/objtool/include/objtool/check.h b/tools/objtool/include/objtool/check.h index f96aabd7d54dc..fde958683485f 100644 --- a/tools/objtool/include/objtool/check.h +++ b/tools/objtool/include/objtool/check.h @@ -66,7 +66,8 @@ struct instruction { visited : 4, no_reloc : 1, hole : 1, - fake : 1; + fake : 1, + trace : 1; /* 9 bit hole */ struct alt_group *alt_group; @@ -143,4 +144,7 @@ struct instruction *next_insn_same_sec(struct objtool_file *file, struct instruc const char *objtool_disas_insn(struct instruction *insn); +extern size_t sym_name_max_len; +extern struct disas_context *objtool_disas_ctx; + #endif /* _CHECK_H */ diff --git a/tools/objtool/include/objtool/disas.h b/tools/objtool/include/objtool/disas.h index 1aee1fbe0bb97..5db75d06f2197 100644 --- a/tools/objtool/include/objtool/disas.h +++ b/tools/objtool/include/objtool/disas.h @@ -19,6 +19,11 @@ int disas_info_init(struct disassemble_info *dinfo, const char *options); size_t disas_insn(struct disas_context *dctx, struct instruction *insn); char *disas_result(struct disas_context *dctx); +void disas_print_info(FILE *stream, struct instruction *insn, int depth, + const char *format, ...); +void disas_print_insn(FILE *stream, struct disas_context *dctx, + struct instruction *insn, int depth, + const char *format, ...); #else /* DISAS */ @@ -51,6 +56,12 @@ static inline char *disas_result(struct disas_context *dctx) return NULL; } +static inline void disas_print_info(FILE *stream, struct instruction *insn, + int depth, const char *format, ...) {} +static inline void disas_print_insn(FILE *stream, struct disas_context *dctx, + struct instruction *insn, int depth, + const char *format, ...) {} + #endif /* DISAS */ #endif /* _DISAS_H */ diff --git a/tools/objtool/include/objtool/trace.h b/tools/objtool/include/objtool/trace.h new file mode 100644 index 0000000000000..3f3c830ed114e --- /dev/null +++ b/tools/objtool/include/objtool/trace.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. + */ + +#ifndef _TRACE_H +#define _TRACE_H + +#include +#include + +#ifdef DISAS + +extern bool trace; +extern int trace_depth; + +#define TRACE(fmt, ...) \ +({ if (trace) \ + fprintf(stderr, fmt, ##__VA_ARGS__); \ +}) + +#define TRACE_INSN(insn, fmt, ...) \ +({ \ + if (trace) { \ + disas_print_insn(stderr, objtool_disas_ctx, \ + insn, trace_depth - 1, \ + fmt, ##__VA_ARGS__); \ + fprintf(stderr, "\n"); \ + insn->trace = 1; \ + } \ +}) + +static inline void trace_enable(void) +{ + trace = true; + trace_depth = 0; +} + +static inline void trace_disable(void) +{ + trace = false; +} + +static inline void trace_depth_inc(void) +{ + if (trace) + trace_depth++; +} + +static inline void trace_depth_dec(void) +{ + if (trace) + trace_depth--; +} + +#else /* DISAS */ + +#define TRACE(fmt, ...) ({}) +#define TRACE_INSN(insn, fmt, ...) ({}) + +static inline void trace_enable(void) {} +static inline void trace_disable(void) {} +static inline void trace_depth_inc(void) {} +static inline void trace_depth_dec(void) {} + +#endif + +#endif /* _TRACE_H */ diff --git a/tools/objtool/include/objtool/warn.h b/tools/objtool/include/objtool/warn.h index f32abc7b1be14..25ff7942b4d54 100644 --- a/tools/objtool/include/objtool/warn.h +++ b/tools/objtool/include/objtool/warn.h @@ -97,6 +97,7 @@ static inline char *offstr(struct section *sec, unsigned long offset) _len = (_len < 50) ? 50 - _len : 0; \ WARN(" %s: " format " %*s%s", _str, ##__VA_ARGS__, _len, "", _istr); \ free(_str); \ + __insn->trace = 1; \ } \ }) diff --git a/tools/objtool/trace.c b/tools/objtool/trace.c new file mode 100644 index 0000000000000..134cc33ffe970 --- /dev/null +++ b/tools/objtool/trace.c @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2025, Oracle and/or its affiliates. + */ + +#include + +bool trace; +int trace_depth; -- 2.47.3