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 <alexandre.chartre@oracle.com>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Acked-by: Josh Poimboeuf <jpoimboe@kernel.org>
Link: https://patch.msgid.link/20251121095340.464045-11-alexandre.chartre@oracle.com
This commit is contained in:
Alexandre Chartre
2025-11-21 10:53:20 +01:00
committed by Peter Zijlstra
parent de0248fbbf
commit 70589843b3
10 changed files with 299 additions and 18 deletions

View File

@@ -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

View File

@@ -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"),

View File

@@ -4,6 +4,7 @@
*/
#define _GNU_SOURCE /* memmem() */
#include <fnmatch.h>
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
@@ -15,6 +16,7 @@
#include <objtool/disas.h>
#include <objtool/check.h>
#include <objtool/special.h>
#include <objtool/trace.h>
#include <objtool/warn.h>
#include <objtool/checksum.h>
#include <objtool/util.h>
@@ -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;
}

View File

@@ -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, "<fake nop> (%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.
*/

View File

@@ -41,6 +41,7 @@ struct opts {
const char *output;
bool sec_address;
bool stats;
const char *trace;
bool verbose;
bool werror;
};

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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 <objtool/check.h>
#include <objtool/disas.h>
#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 */

View File

@@ -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; \
} \
})

9
tools/objtool/trace.c Normal file
View File

@@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (c) 2025, Oracle and/or its affiliates.
*/
#include <objtool/trace.h>
bool trace;
int trace_depth;