7e4dc77b28
Pull perf updates from Ingo Molnar: "With over 300 commits it's been a busy cycle - with most of the work concentrated on the tooling side (as it should). The main kernel side enhancements were: - Add per event callchain limit: Recently we introduced a sysctl to tune the max-stack for all events for which callchains were requested: $ sysctl kernel.perf_event_max_stack kernel.perf_event_max_stack = 127 Now this patch introduces a way to configure this per event, i.e. this becomes possible: $ perf record -e sched:*/max-stack=2/ -e block:*/max-stack=10/ -a allowing finer tuning of how much buffer space callchains use. This uses an u16 from the reserved space at the end, leaving another u16 for future use. There has been interest in even finer tuning, namely to control the max stack for kernel and userspace callchains separately. Further discussion is needed, we may for instance use the remaining u16 for that and when it is present, assume that the sample_max_stack introduced in this patch applies for the kernel, and the u16 left is used for limiting the userspace callchain (Arnaldo Carvalho de Melo) - Optimize AUX event (hardware assisted side-band event) delivery (Kan Liang) - Rework Intel family name macro usage (this is partially x86 arch work) (Dave Hansen) - Refine and fix Intel LBR support (David Carrillo-Cisneros) - Add support for Intel 'TopDown' events (Andi Kleen) - Intel uncore PMU driver fixes and enhancements (Kan Liang) - ... other misc changes. Here's an incomplete list of the tooling enhancements (but there's much more, see the shortlog and the git log for details): - Support cross unwinding, i.e. collecting '--call-graph dwarf' perf.data files in one machine and then doing analysis in another machine of a different hardware architecture. This enables, for instance, to do: $ perf record -a --call-graph dwarf on a x86-32 or aarch64 system and then do 'perf report' on it on a x86_64 workstation (He Kuang) - Allow reading from a backward ring buffer (one setup via sys_perf_event_open() with perf_event_attr.write_backward = 1) (Wang Nan) - Finish merging initial SDT (Statically Defined Traces) support, see cset comments for details about how it all works (Masami Hiramatsu) - Support attaching eBPF programs to tracepoints (Wang Nan) - Add demangling of symbols in programs written in the Rust language (David Tolnay) - Add support for tracepoints in the python binding, including an example, that sets up and parses sched:sched_switch events, tools/perf/python/tracepoint.py (Jiri Olsa) - Introduce --stdio-color to set up the color output mode selection in 'annotate' and 'report', allowing emit color escape sequences when redirecting the output of these tools (Arnaldo Carvalho de Melo) - Add 'callindent' option to 'perf script -F', to indent the Intel PT call stack, making this output more ftrace-like (Adrian Hunter, Andi Kleen) - Allow dumping the object files generated by llvm when processing eBPF scriptlet events (Wang Nan) - Add stackcollapse.py script to help generating flame graphs (Paolo Bonzini) - Add --ldlat option to 'perf mem' to specify load latency for loads event (e.g. cpu/mem-loads/ ) (Jiri Olsa) - Tooling support for Intel TopDown counters, recently added to the kernel (Andi Kleen)" * 'perf-core-for-linus' of git://git.kernel.org/pub/scm/linux/kernel/git/tip/tip: (303 commits) perf tests: Add is_printable_array test perf tools: Make is_printable_array global perf script python: Fix string vs byte array resolving perf probe: Warn unmatched function filter correctly perf cpu_map: Add more helpers perf stat: Balance opening and reading events tools: Copy linux/{hash,poison}.h and check for drift perf tools: Remove include/linux/list.h from perf's MANIFEST tools: Copy the bitops files accessed from the kernel and check for drift Remove: kernel unistd*h files from perf's MANIFEST, not used perf tools: Remove tools/perf/util/include/linux/const.h perf tools: Remove tools/perf/util/include/asm/byteorder.h perf tools: Add missing linux/compiler.h include to perf-sys.h perf jit: Remove some no-op error handling perf jit: Add missing curly braces objtool: Initialize variable to silence old compiler objtool: Add -I$(srctree)/tools/arch/$(ARCH)/include/uapi perf record: Add --tail-synthesize option perf session: Don't warn about out of order event if write_backward is used perf tools: Enable overwrite settings ...
1212 lines
28 KiB
C
1212 lines
28 KiB
C
/*
|
|
* Copyright (C) 2015 Josh Poimboeuf <jpoimboe@redhat.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*
|
|
* objtool check:
|
|
*
|
|
* This command analyzes every .o file and ensures the validity of its stack
|
|
* trace metadata. It enforces a set of rules on asm code and C inline
|
|
* assembly code so that stack traces can be reliable.
|
|
*
|
|
* For more information, see tools/objtool/Documentation/stack-validation.txt.
|
|
*/
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <subcmd/parse-options.h>
|
|
|
|
#include "builtin.h"
|
|
#include "elf.h"
|
|
#include "special.h"
|
|
#include "arch.h"
|
|
#include "warn.h"
|
|
|
|
#include <linux/hashtable.h>
|
|
|
|
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
|
|
|
|
#define STATE_FP_SAVED 0x1
|
|
#define STATE_FP_SETUP 0x2
|
|
#define STATE_FENTRY 0x4
|
|
|
|
struct instruction {
|
|
struct list_head list;
|
|
struct hlist_node hash;
|
|
struct section *sec;
|
|
unsigned long offset;
|
|
unsigned int len, state;
|
|
unsigned char type;
|
|
unsigned long immediate;
|
|
bool alt_group, visited;
|
|
struct symbol *call_dest;
|
|
struct instruction *jump_dest;
|
|
struct list_head alts;
|
|
struct symbol *func;
|
|
};
|
|
|
|
struct alternative {
|
|
struct list_head list;
|
|
struct instruction *insn;
|
|
};
|
|
|
|
struct objtool_file {
|
|
struct elf *elf;
|
|
struct list_head insn_list;
|
|
DECLARE_HASHTABLE(insn_hash, 16);
|
|
struct section *rodata, *whitelist;
|
|
bool ignore_unreachables, c_file;
|
|
};
|
|
|
|
const char *objname;
|
|
static bool nofp;
|
|
|
|
static struct instruction *find_insn(struct objtool_file *file,
|
|
struct section *sec, unsigned long offset)
|
|
{
|
|
struct instruction *insn;
|
|
|
|
hash_for_each_possible(file->insn_hash, insn, hash, offset)
|
|
if (insn->sec == sec && insn->offset == offset)
|
|
return insn;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static struct instruction *next_insn_same_sec(struct objtool_file *file,
|
|
struct instruction *insn)
|
|
{
|
|
struct instruction *next = list_next_entry(insn, list);
|
|
|
|
if (&next->list == &file->insn_list || next->sec != insn->sec)
|
|
return NULL;
|
|
|
|
return next;
|
|
}
|
|
|
|
#define for_each_insn(file, insn) \
|
|
list_for_each_entry(insn, &file->insn_list, list)
|
|
|
|
#define func_for_each_insn(file, func, insn) \
|
|
for (insn = find_insn(file, func->sec, func->offset); \
|
|
insn && &insn->list != &file->insn_list && \
|
|
insn->sec == func->sec && \
|
|
insn->offset < func->offset + func->len; \
|
|
insn = list_next_entry(insn, list))
|
|
|
|
#define sec_for_each_insn_from(file, insn) \
|
|
for (; insn; insn = next_insn_same_sec(file, insn))
|
|
|
|
|
|
/*
|
|
* Check if the function has been manually whitelisted with the
|
|
* STACK_FRAME_NON_STANDARD macro, or if it should be automatically whitelisted
|
|
* due to its use of a context switching instruction.
|
|
*/
|
|
static bool ignore_func(struct objtool_file *file, struct symbol *func)
|
|
{
|
|
struct rela *rela;
|
|
struct instruction *insn;
|
|
|
|
/* check for STACK_FRAME_NON_STANDARD */
|
|
if (file->whitelist && file->whitelist->rela)
|
|
list_for_each_entry(rela, &file->whitelist->rela->rela_list, list) {
|
|
if (rela->sym->type == STT_SECTION &&
|
|
rela->sym->sec == func->sec &&
|
|
rela->addend == func->offset)
|
|
return true;
|
|
if (rela->sym->type == STT_FUNC && rela->sym == func)
|
|
return true;
|
|
}
|
|
|
|
/* check if it has a context switching instruction */
|
|
func_for_each_insn(file, func, insn)
|
|
if (insn->type == INSN_CONTEXT_SWITCH)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* This checks to see if the given function is a "noreturn" function.
|
|
*
|
|
* For global functions which are outside the scope of this object file, we
|
|
* have to keep a manual list of them.
|
|
*
|
|
* For local functions, we have to detect them manually by simply looking for
|
|
* the lack of a return instruction.
|
|
*
|
|
* Returns:
|
|
* -1: error
|
|
* 0: no dead end
|
|
* 1: dead end
|
|
*/
|
|
static int __dead_end_function(struct objtool_file *file, struct symbol *func,
|
|
int recursion)
|
|
{
|
|
int i;
|
|
struct instruction *insn;
|
|
bool empty = true;
|
|
|
|
/*
|
|
* Unfortunately these have to be hard coded because the noreturn
|
|
* attribute isn't provided in ELF data.
|
|
*/
|
|
static const char * const global_noreturns[] = {
|
|
"__stack_chk_fail",
|
|
"panic",
|
|
"do_exit",
|
|
"__module_put_and_exit",
|
|
"complete_and_exit",
|
|
"kvm_spurious_fault",
|
|
"__reiserfs_panic",
|
|
"lbug_with_loc"
|
|
};
|
|
|
|
if (func->bind == STB_WEAK)
|
|
return 0;
|
|
|
|
if (func->bind == STB_GLOBAL)
|
|
for (i = 0; i < ARRAY_SIZE(global_noreturns); i++)
|
|
if (!strcmp(func->name, global_noreturns[i]))
|
|
return 1;
|
|
|
|
if (!func->sec)
|
|
return 0;
|
|
|
|
func_for_each_insn(file, func, insn) {
|
|
empty = false;
|
|
|
|
if (insn->type == INSN_RETURN)
|
|
return 0;
|
|
}
|
|
|
|
if (empty)
|
|
return 0;
|
|
|
|
/*
|
|
* A function can have a sibling call instead of a return. In that
|
|
* case, the function's dead-end status depends on whether the target
|
|
* of the sibling call returns.
|
|
*/
|
|
func_for_each_insn(file, func, insn) {
|
|
if (insn->sec != func->sec ||
|
|
insn->offset >= func->offset + func->len)
|
|
break;
|
|
|
|
if (insn->type == INSN_JUMP_UNCONDITIONAL) {
|
|
struct instruction *dest = insn->jump_dest;
|
|
struct symbol *dest_func;
|
|
|
|
if (!dest)
|
|
/* sibling call to another file */
|
|
return 0;
|
|
|
|
if (dest->sec != func->sec ||
|
|
dest->offset < func->offset ||
|
|
dest->offset >= func->offset + func->len) {
|
|
/* local sibling call */
|
|
dest_func = find_symbol_by_offset(dest->sec,
|
|
dest->offset);
|
|
if (!dest_func)
|
|
continue;
|
|
|
|
if (recursion == 5) {
|
|
WARN_FUNC("infinite recursion (objtool bug!)",
|
|
dest->sec, dest->offset);
|
|
return -1;
|
|
}
|
|
|
|
return __dead_end_function(file, dest_func,
|
|
recursion + 1);
|
|
}
|
|
}
|
|
|
|
if (insn->type == INSN_JUMP_DYNAMIC && list_empty(&insn->alts))
|
|
/* sibling call */
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int dead_end_function(struct objtool_file *file, struct symbol *func)
|
|
{
|
|
return __dead_end_function(file, func, 0);
|
|
}
|
|
|
|
/*
|
|
* Call the arch-specific instruction decoder for all the instructions and add
|
|
* them to the global instruction list.
|
|
*/
|
|
static int decode_instructions(struct objtool_file *file)
|
|
{
|
|
struct section *sec;
|
|
struct symbol *func;
|
|
unsigned long offset;
|
|
struct instruction *insn;
|
|
int ret;
|
|
|
|
list_for_each_entry(sec, &file->elf->sections, list) {
|
|
|
|
if (!(sec->sh.sh_flags & SHF_EXECINSTR))
|
|
continue;
|
|
|
|
for (offset = 0; offset < sec->len; offset += insn->len) {
|
|
insn = malloc(sizeof(*insn));
|
|
memset(insn, 0, sizeof(*insn));
|
|
|
|
INIT_LIST_HEAD(&insn->alts);
|
|
insn->sec = sec;
|
|
insn->offset = offset;
|
|
|
|
ret = arch_decode_instruction(file->elf, sec, offset,
|
|
sec->len - offset,
|
|
&insn->len, &insn->type,
|
|
&insn->immediate);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (!insn->type || insn->type > INSN_LAST) {
|
|
WARN_FUNC("invalid instruction type %d",
|
|
insn->sec, insn->offset, insn->type);
|
|
return -1;
|
|
}
|
|
|
|
hash_add(file->insn_hash, &insn->hash, insn->offset);
|
|
list_add_tail(&insn->list, &file->insn_list);
|
|
}
|
|
|
|
list_for_each_entry(func, &sec->symbol_list, list) {
|
|
if (func->type != STT_FUNC)
|
|
continue;
|
|
|
|
if (!find_insn(file, sec, func->offset)) {
|
|
WARN("%s(): can't find starting instruction",
|
|
func->name);
|
|
return -1;
|
|
}
|
|
|
|
func_for_each_insn(file, func, insn)
|
|
if (!insn->func)
|
|
insn->func = func;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Warnings shouldn't be reported for ignored functions.
|
|
*/
|
|
static void add_ignores(struct objtool_file *file)
|
|
{
|
|
struct instruction *insn;
|
|
struct section *sec;
|
|
struct symbol *func;
|
|
|
|
list_for_each_entry(sec, &file->elf->sections, list) {
|
|
list_for_each_entry(func, &sec->symbol_list, list) {
|
|
if (func->type != STT_FUNC)
|
|
continue;
|
|
|
|
if (!ignore_func(file, func))
|
|
continue;
|
|
|
|
func_for_each_insn(file, func, insn)
|
|
insn->visited = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Find the destination instructions for all jumps.
|
|
*/
|
|
static int add_jump_destinations(struct objtool_file *file)
|
|
{
|
|
struct instruction *insn;
|
|
struct rela *rela;
|
|
struct section *dest_sec;
|
|
unsigned long dest_off;
|
|
|
|
for_each_insn(file, insn) {
|
|
if (insn->type != INSN_JUMP_CONDITIONAL &&
|
|
insn->type != INSN_JUMP_UNCONDITIONAL)
|
|
continue;
|
|
|
|
/* skip ignores */
|
|
if (insn->visited)
|
|
continue;
|
|
|
|
rela = find_rela_by_dest_range(insn->sec, insn->offset,
|
|
insn->len);
|
|
if (!rela) {
|
|
dest_sec = insn->sec;
|
|
dest_off = insn->offset + insn->len + insn->immediate;
|
|
} else if (rela->sym->type == STT_SECTION) {
|
|
dest_sec = rela->sym->sec;
|
|
dest_off = rela->addend + 4;
|
|
} else if (rela->sym->sec->idx) {
|
|
dest_sec = rela->sym->sec;
|
|
dest_off = rela->sym->sym.st_value + rela->addend + 4;
|
|
} else {
|
|
/* sibling call */
|
|
insn->jump_dest = 0;
|
|
continue;
|
|
}
|
|
|
|
insn->jump_dest = find_insn(file, dest_sec, dest_off);
|
|
if (!insn->jump_dest) {
|
|
|
|
/*
|
|
* This is a special case where an alt instruction
|
|
* jumps past the end of the section. These are
|
|
* handled later in handle_group_alt().
|
|
*/
|
|
if (!strcmp(insn->sec->name, ".altinstr_replacement"))
|
|
continue;
|
|
|
|
WARN_FUNC("can't find jump dest instruction at %s+0x%lx",
|
|
insn->sec, insn->offset, dest_sec->name,
|
|
dest_off);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Find the destination instructions for all calls.
|
|
*/
|
|
static int add_call_destinations(struct objtool_file *file)
|
|
{
|
|
struct instruction *insn;
|
|
unsigned long dest_off;
|
|
struct rela *rela;
|
|
|
|
for_each_insn(file, insn) {
|
|
if (insn->type != INSN_CALL)
|
|
continue;
|
|
|
|
rela = find_rela_by_dest_range(insn->sec, insn->offset,
|
|
insn->len);
|
|
if (!rela) {
|
|
dest_off = insn->offset + insn->len + insn->immediate;
|
|
insn->call_dest = find_symbol_by_offset(insn->sec,
|
|
dest_off);
|
|
if (!insn->call_dest) {
|
|
WARN_FUNC("can't find call dest symbol at offset 0x%lx",
|
|
insn->sec, insn->offset, dest_off);
|
|
return -1;
|
|
}
|
|
} else if (rela->sym->type == STT_SECTION) {
|
|
insn->call_dest = find_symbol_by_offset(rela->sym->sec,
|
|
rela->addend+4);
|
|
if (!insn->call_dest ||
|
|
insn->call_dest->type != STT_FUNC) {
|
|
WARN_FUNC("can't find call dest symbol at %s+0x%x",
|
|
insn->sec, insn->offset,
|
|
rela->sym->sec->name,
|
|
rela->addend + 4);
|
|
return -1;
|
|
}
|
|
} else
|
|
insn->call_dest = rela->sym;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* The .alternatives section requires some extra special care, over and above
|
|
* what other special sections require:
|
|
*
|
|
* 1. Because alternatives are patched in-place, we need to insert a fake jump
|
|
* instruction at the end so that validate_branch() skips all the original
|
|
* replaced instructions when validating the new instruction path.
|
|
*
|
|
* 2. An added wrinkle is that the new instruction length might be zero. In
|
|
* that case the old instructions are replaced with noops. We simulate that
|
|
* by creating a fake jump as the only new instruction.
|
|
*
|
|
* 3. In some cases, the alternative section includes an instruction which
|
|
* conditionally jumps to the _end_ of the entry. We have to modify these
|
|
* jumps' destinations to point back to .text rather than the end of the
|
|
* entry in .altinstr_replacement.
|
|
*
|
|
* 4. It has been requested that we don't validate the !POPCNT feature path
|
|
* which is a "very very small percentage of machines".
|
|
*/
|
|
static int handle_group_alt(struct objtool_file *file,
|
|
struct special_alt *special_alt,
|
|
struct instruction *orig_insn,
|
|
struct instruction **new_insn)
|
|
{
|
|
struct instruction *last_orig_insn, *last_new_insn, *insn, *fake_jump;
|
|
unsigned long dest_off;
|
|
|
|
last_orig_insn = NULL;
|
|
insn = orig_insn;
|
|
sec_for_each_insn_from(file, insn) {
|
|
if (insn->offset >= special_alt->orig_off + special_alt->orig_len)
|
|
break;
|
|
|
|
if (special_alt->skip_orig)
|
|
insn->type = INSN_NOP;
|
|
|
|
insn->alt_group = true;
|
|
last_orig_insn = insn;
|
|
}
|
|
|
|
if (!next_insn_same_sec(file, last_orig_insn)) {
|
|
WARN("%s: don't know how to handle alternatives at end of section",
|
|
special_alt->orig_sec->name);
|
|
return -1;
|
|
}
|
|
|
|
fake_jump = malloc(sizeof(*fake_jump));
|
|
if (!fake_jump) {
|
|
WARN("malloc failed");
|
|
return -1;
|
|
}
|
|
memset(fake_jump, 0, sizeof(*fake_jump));
|
|
INIT_LIST_HEAD(&fake_jump->alts);
|
|
fake_jump->sec = special_alt->new_sec;
|
|
fake_jump->offset = -1;
|
|
fake_jump->type = INSN_JUMP_UNCONDITIONAL;
|
|
fake_jump->jump_dest = list_next_entry(last_orig_insn, list);
|
|
|
|
if (!special_alt->new_len) {
|
|
*new_insn = fake_jump;
|
|
return 0;
|
|
}
|
|
|
|
last_new_insn = NULL;
|
|
insn = *new_insn;
|
|
sec_for_each_insn_from(file, insn) {
|
|
if (insn->offset >= special_alt->new_off + special_alt->new_len)
|
|
break;
|
|
|
|
last_new_insn = insn;
|
|
|
|
if (insn->type != INSN_JUMP_CONDITIONAL &&
|
|
insn->type != INSN_JUMP_UNCONDITIONAL)
|
|
continue;
|
|
|
|
if (!insn->immediate)
|
|
continue;
|
|
|
|
dest_off = insn->offset + insn->len + insn->immediate;
|
|
if (dest_off == special_alt->new_off + special_alt->new_len)
|
|
insn->jump_dest = fake_jump;
|
|
|
|
if (!insn->jump_dest) {
|
|
WARN_FUNC("can't find alternative jump destination",
|
|
insn->sec, insn->offset);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (!last_new_insn) {
|
|
WARN_FUNC("can't find last new alternative instruction",
|
|
special_alt->new_sec, special_alt->new_off);
|
|
return -1;
|
|
}
|
|
|
|
list_add(&fake_jump->list, &last_new_insn->list);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* A jump table entry can either convert a nop to a jump or a jump to a nop.
|
|
* If the original instruction is a jump, make the alt entry an effective nop
|
|
* by just skipping the original instruction.
|
|
*/
|
|
static int handle_jump_alt(struct objtool_file *file,
|
|
struct special_alt *special_alt,
|
|
struct instruction *orig_insn,
|
|
struct instruction **new_insn)
|
|
{
|
|
if (orig_insn->type == INSN_NOP)
|
|
return 0;
|
|
|
|
if (orig_insn->type != INSN_JUMP_UNCONDITIONAL) {
|
|
WARN_FUNC("unsupported instruction at jump label",
|
|
orig_insn->sec, orig_insn->offset);
|
|
return -1;
|
|
}
|
|
|
|
*new_insn = list_next_entry(orig_insn, list);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Read all the special sections which have alternate instructions which can be
|
|
* patched in or redirected to at runtime. Each instruction having alternate
|
|
* instruction(s) has them added to its insn->alts list, which will be
|
|
* traversed in validate_branch().
|
|
*/
|
|
static int add_special_section_alts(struct objtool_file *file)
|
|
{
|
|
struct list_head special_alts;
|
|
struct instruction *orig_insn, *new_insn;
|
|
struct special_alt *special_alt, *tmp;
|
|
struct alternative *alt;
|
|
int ret;
|
|
|
|
ret = special_get_alts(file->elf, &special_alts);
|
|
if (ret)
|
|
return ret;
|
|
|
|
list_for_each_entry_safe(special_alt, tmp, &special_alts, list) {
|
|
alt = malloc(sizeof(*alt));
|
|
if (!alt) {
|
|
WARN("malloc failed");
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
|
|
orig_insn = find_insn(file, special_alt->orig_sec,
|
|
special_alt->orig_off);
|
|
if (!orig_insn) {
|
|
WARN_FUNC("special: can't find orig instruction",
|
|
special_alt->orig_sec, special_alt->orig_off);
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
|
|
new_insn = NULL;
|
|
if (!special_alt->group || special_alt->new_len) {
|
|
new_insn = find_insn(file, special_alt->new_sec,
|
|
special_alt->new_off);
|
|
if (!new_insn) {
|
|
WARN_FUNC("special: can't find new instruction",
|
|
special_alt->new_sec,
|
|
special_alt->new_off);
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (special_alt->group) {
|
|
ret = handle_group_alt(file, special_alt, orig_insn,
|
|
&new_insn);
|
|
if (ret)
|
|
goto out;
|
|
} else if (special_alt->jump_or_nop) {
|
|
ret = handle_jump_alt(file, special_alt, orig_insn,
|
|
&new_insn);
|
|
if (ret)
|
|
goto out;
|
|
}
|
|
|
|
alt->insn = new_insn;
|
|
list_add_tail(&alt->list, &orig_insn->alts);
|
|
|
|
list_del(&special_alt->list);
|
|
free(special_alt);
|
|
}
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static int add_switch_table(struct objtool_file *file, struct symbol *func,
|
|
struct instruction *insn, struct rela *table,
|
|
struct rela *next_table)
|
|
{
|
|
struct rela *rela = table;
|
|
struct instruction *alt_insn;
|
|
struct alternative *alt;
|
|
|
|
list_for_each_entry_from(rela, &file->rodata->rela->rela_list, list) {
|
|
if (rela == next_table)
|
|
break;
|
|
|
|
if (rela->sym->sec != insn->sec ||
|
|
rela->addend <= func->offset ||
|
|
rela->addend >= func->offset + func->len)
|
|
break;
|
|
|
|
alt_insn = find_insn(file, insn->sec, rela->addend);
|
|
if (!alt_insn) {
|
|
WARN("%s: can't find instruction at %s+0x%x",
|
|
file->rodata->rela->name, insn->sec->name,
|
|
rela->addend);
|
|
return -1;
|
|
}
|
|
|
|
alt = malloc(sizeof(*alt));
|
|
if (!alt) {
|
|
WARN("malloc failed");
|
|
return -1;
|
|
}
|
|
|
|
alt->insn = alt_insn;
|
|
list_add_tail(&alt->list, &insn->alts);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int add_func_switch_tables(struct objtool_file *file,
|
|
struct symbol *func)
|
|
{
|
|
struct instruction *insn, *prev_jump;
|
|
struct rela *text_rela, *rodata_rela, *prev_rela = NULL;
|
|
int ret;
|
|
|
|
prev_jump = NULL;
|
|
|
|
func_for_each_insn(file, func, insn) {
|
|
if (insn->type != INSN_JUMP_DYNAMIC)
|
|
continue;
|
|
|
|
text_rela = find_rela_by_dest_range(insn->sec, insn->offset,
|
|
insn->len);
|
|
if (!text_rela || text_rela->sym != file->rodata->sym)
|
|
continue;
|
|
|
|
/* common case: jmpq *[addr](,%rax,8) */
|
|
rodata_rela = find_rela_by_dest(file->rodata,
|
|
text_rela->addend);
|
|
|
|
/*
|
|
* rare case: jmpq *[addr](%rip)
|
|
*
|
|
* This check is for a rare gcc quirk, currently only seen in
|
|
* three driver functions in the kernel, only with certain
|
|
* obscure non-distro configs.
|
|
*
|
|
* As part of an optimization, gcc makes a copy of an existing
|
|
* switch jump table, modifies it, and then hard-codes the jump
|
|
* (albeit with an indirect jump) to use a single entry in the
|
|
* table. The rest of the jump table and some of its jump
|
|
* targets remain as dead code.
|
|
*
|
|
* In such a case we can just crudely ignore all unreachable
|
|
* instruction warnings for the entire object file. Ideally we
|
|
* would just ignore them for the function, but that would
|
|
* require redesigning the code quite a bit. And honestly
|
|
* that's just not worth doing: unreachable instruction
|
|
* warnings are of questionable value anyway, and this is such
|
|
* a rare issue.
|
|
*
|
|
* kbuild reports:
|
|
* - https://lkml.kernel.org/r/201603231906.LWcVUpxm%25fengguang.wu@intel.com
|
|
* - https://lkml.kernel.org/r/201603271114.K9i45biy%25fengguang.wu@intel.com
|
|
* - https://lkml.kernel.org/r/201603291058.zuJ6ben1%25fengguang.wu@intel.com
|
|
*
|
|
* gcc bug:
|
|
* - https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70604
|
|
*/
|
|
if (!rodata_rela) {
|
|
rodata_rela = find_rela_by_dest(file->rodata,
|
|
text_rela->addend + 4);
|
|
if (rodata_rela)
|
|
file->ignore_unreachables = true;
|
|
}
|
|
|
|
if (!rodata_rela)
|
|
continue;
|
|
|
|
/*
|
|
* We found a switch table, but we don't know yet how big it
|
|
* is. Don't add it until we reach the end of the function or
|
|
* the beginning of another switch table in the same function.
|
|
*/
|
|
if (prev_jump) {
|
|
ret = add_switch_table(file, func, prev_jump, prev_rela,
|
|
rodata_rela);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
prev_jump = insn;
|
|
prev_rela = rodata_rela;
|
|
}
|
|
|
|
if (prev_jump) {
|
|
ret = add_switch_table(file, func, prev_jump, prev_rela, NULL);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* For some switch statements, gcc generates a jump table in the .rodata
|
|
* section which contains a list of addresses within the function to jump to.
|
|
* This finds these jump tables and adds them to the insn->alts lists.
|
|
*/
|
|
static int add_switch_table_alts(struct objtool_file *file)
|
|
{
|
|
struct section *sec;
|
|
struct symbol *func;
|
|
int ret;
|
|
|
|
if (!file->rodata || !file->rodata->rela)
|
|
return 0;
|
|
|
|
list_for_each_entry(sec, &file->elf->sections, list) {
|
|
list_for_each_entry(func, &sec->symbol_list, list) {
|
|
if (func->type != STT_FUNC)
|
|
continue;
|
|
|
|
ret = add_func_switch_tables(file, func);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int decode_sections(struct objtool_file *file)
|
|
{
|
|
int ret;
|
|
|
|
ret = decode_instructions(file);
|
|
if (ret)
|
|
return ret;
|
|
|
|
add_ignores(file);
|
|
|
|
ret = add_jump_destinations(file);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = add_call_destinations(file);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = add_special_section_alts(file);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = add_switch_table_alts(file);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool is_fentry_call(struct instruction *insn)
|
|
{
|
|
if (insn->type == INSN_CALL &&
|
|
insn->call_dest->type == STT_NOTYPE &&
|
|
!strcmp(insn->call_dest->name, "__fentry__"))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool has_modified_stack_frame(struct instruction *insn)
|
|
{
|
|
return (insn->state & STATE_FP_SAVED) ||
|
|
(insn->state & STATE_FP_SETUP);
|
|
}
|
|
|
|
static bool has_valid_stack_frame(struct instruction *insn)
|
|
{
|
|
return (insn->state & STATE_FP_SAVED) &&
|
|
(insn->state & STATE_FP_SETUP);
|
|
}
|
|
|
|
static unsigned int frame_state(unsigned long state)
|
|
{
|
|
return (state & (STATE_FP_SAVED | STATE_FP_SETUP));
|
|
}
|
|
|
|
/*
|
|
* Follow the branch starting at the given instruction, and recursively follow
|
|
* any other branches (jumps). Meanwhile, track the frame pointer state at
|
|
* each instruction and validate all the rules described in
|
|
* tools/objtool/Documentation/stack-validation.txt.
|
|
*/
|
|
static int validate_branch(struct objtool_file *file,
|
|
struct instruction *first, unsigned char first_state)
|
|
{
|
|
struct alternative *alt;
|
|
struct instruction *insn;
|
|
struct section *sec;
|
|
struct symbol *func = NULL;
|
|
unsigned char state;
|
|
int ret;
|
|
|
|
insn = first;
|
|
sec = insn->sec;
|
|
state = first_state;
|
|
|
|
if (insn->alt_group && list_empty(&insn->alts)) {
|
|
WARN_FUNC("don't know how to handle branch to middle of alternative instruction group",
|
|
sec, insn->offset);
|
|
return 1;
|
|
}
|
|
|
|
while (1) {
|
|
if (file->c_file && insn->func) {
|
|
if (func && func != insn->func) {
|
|
WARN("%s() falls through to next function %s()",
|
|
func->name, insn->func->name);
|
|
return 1;
|
|
}
|
|
|
|
func = insn->func;
|
|
}
|
|
|
|
if (insn->visited) {
|
|
if (frame_state(insn->state) != frame_state(state)) {
|
|
WARN_FUNC("frame pointer state mismatch",
|
|
sec, insn->offset);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
insn->visited = true;
|
|
insn->state = state;
|
|
|
|
list_for_each_entry(alt, &insn->alts, list) {
|
|
ret = validate_branch(file, alt->insn, state);
|
|
if (ret)
|
|
return 1;
|
|
}
|
|
|
|
switch (insn->type) {
|
|
|
|
case INSN_FP_SAVE:
|
|
if (!nofp) {
|
|
if (state & STATE_FP_SAVED) {
|
|
WARN_FUNC("duplicate frame pointer save",
|
|
sec, insn->offset);
|
|
return 1;
|
|
}
|
|
state |= STATE_FP_SAVED;
|
|
}
|
|
break;
|
|
|
|
case INSN_FP_SETUP:
|
|
if (!nofp) {
|
|
if (state & STATE_FP_SETUP) {
|
|
WARN_FUNC("duplicate frame pointer setup",
|
|
sec, insn->offset);
|
|
return 1;
|
|
}
|
|
state |= STATE_FP_SETUP;
|
|
}
|
|
break;
|
|
|
|
case INSN_FP_RESTORE:
|
|
if (!nofp) {
|
|
if (has_valid_stack_frame(insn))
|
|
state &= ~STATE_FP_SETUP;
|
|
|
|
state &= ~STATE_FP_SAVED;
|
|
}
|
|
break;
|
|
|
|
case INSN_RETURN:
|
|
if (!nofp && has_modified_stack_frame(insn)) {
|
|
WARN_FUNC("return without frame pointer restore",
|
|
sec, insn->offset);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
|
|
case INSN_CALL:
|
|
if (is_fentry_call(insn)) {
|
|
state |= STATE_FENTRY;
|
|
break;
|
|
}
|
|
|
|
ret = dead_end_function(file, insn->call_dest);
|
|
if (ret == 1)
|
|
return 0;
|
|
if (ret == -1)
|
|
return 1;
|
|
|
|
/* fallthrough */
|
|
case INSN_CALL_DYNAMIC:
|
|
if (!nofp && !has_valid_stack_frame(insn)) {
|
|
WARN_FUNC("call without frame pointer save/setup",
|
|
sec, insn->offset);
|
|
return 1;
|
|
}
|
|
break;
|
|
|
|
case INSN_JUMP_CONDITIONAL:
|
|
case INSN_JUMP_UNCONDITIONAL:
|
|
if (insn->jump_dest) {
|
|
ret = validate_branch(file, insn->jump_dest,
|
|
state);
|
|
if (ret)
|
|
return 1;
|
|
} else if (has_modified_stack_frame(insn)) {
|
|
WARN_FUNC("sibling call from callable instruction with changed frame pointer",
|
|
sec, insn->offset);
|
|
return 1;
|
|
} /* else it's a sibling call */
|
|
|
|
if (insn->type == INSN_JUMP_UNCONDITIONAL)
|
|
return 0;
|
|
|
|
break;
|
|
|
|
case INSN_JUMP_DYNAMIC:
|
|
if (list_empty(&insn->alts) &&
|
|
has_modified_stack_frame(insn)) {
|
|
WARN_FUNC("sibling call from callable instruction with changed frame pointer",
|
|
sec, insn->offset);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
|
|
case INSN_BUG:
|
|
return 0;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
insn = next_insn_same_sec(file, insn);
|
|
if (!insn) {
|
|
WARN("%s: unexpected end of section", sec->name);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool is_gcov_insn(struct instruction *insn)
|
|
{
|
|
struct rela *rela;
|
|
struct section *sec;
|
|
struct symbol *sym;
|
|
unsigned long offset;
|
|
|
|
rela = find_rela_by_dest_range(insn->sec, insn->offset, insn->len);
|
|
if (!rela)
|
|
return false;
|
|
|
|
if (rela->sym->type != STT_SECTION)
|
|
return false;
|
|
|
|
sec = rela->sym->sec;
|
|
offset = rela->addend + insn->offset + insn->len - rela->offset;
|
|
|
|
list_for_each_entry(sym, &sec->symbol_list, list) {
|
|
if (sym->type != STT_OBJECT)
|
|
continue;
|
|
|
|
if (offset >= sym->offset && offset < sym->offset + sym->len)
|
|
return (!memcmp(sym->name, "__gcov0.", 8));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool is_kasan_insn(struct instruction *insn)
|
|
{
|
|
return (insn->type == INSN_CALL &&
|
|
!strcmp(insn->call_dest->name, "__asan_handle_no_return"));
|
|
}
|
|
|
|
static bool is_ubsan_insn(struct instruction *insn)
|
|
{
|
|
return (insn->type == INSN_CALL &&
|
|
!strcmp(insn->call_dest->name,
|
|
"__ubsan_handle_builtin_unreachable"));
|
|
}
|
|
|
|
static bool ignore_unreachable_insn(struct symbol *func,
|
|
struct instruction *insn)
|
|
{
|
|
int i;
|
|
|
|
if (insn->type == INSN_NOP)
|
|
return true;
|
|
|
|
if (is_gcov_insn(insn))
|
|
return true;
|
|
|
|
/*
|
|
* Check if this (or a subsequent) instruction is related to
|
|
* CONFIG_UBSAN or CONFIG_KASAN.
|
|
*
|
|
* End the search at 5 instructions to avoid going into the weeds.
|
|
*/
|
|
for (i = 0; i < 5; i++) {
|
|
|
|
if (is_kasan_insn(insn) || is_ubsan_insn(insn))
|
|
return true;
|
|
|
|
if (insn->type == INSN_JUMP_UNCONDITIONAL && insn->jump_dest) {
|
|
insn = insn->jump_dest;
|
|
continue;
|
|
}
|
|
|
|
if (insn->offset + insn->len >= func->offset + func->len)
|
|
break;
|
|
insn = list_next_entry(insn, list);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static int validate_functions(struct objtool_file *file)
|
|
{
|
|
struct section *sec;
|
|
struct symbol *func;
|
|
struct instruction *insn;
|
|
int ret, warnings = 0;
|
|
|
|
list_for_each_entry(sec, &file->elf->sections, list) {
|
|
list_for_each_entry(func, &sec->symbol_list, list) {
|
|
if (func->type != STT_FUNC)
|
|
continue;
|
|
|
|
insn = find_insn(file, sec, func->offset);
|
|
if (!insn)
|
|
continue;
|
|
|
|
ret = validate_branch(file, insn, 0);
|
|
warnings += ret;
|
|
}
|
|
}
|
|
|
|
list_for_each_entry(sec, &file->elf->sections, list) {
|
|
list_for_each_entry(func, &sec->symbol_list, list) {
|
|
if (func->type != STT_FUNC)
|
|
continue;
|
|
|
|
func_for_each_insn(file, func, insn) {
|
|
if (insn->visited)
|
|
continue;
|
|
|
|
insn->visited = true;
|
|
|
|
if (file->ignore_unreachables || warnings ||
|
|
ignore_unreachable_insn(func, insn))
|
|
continue;
|
|
|
|
WARN_FUNC("function has unreachable instruction", insn->sec, insn->offset);
|
|
warnings++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
static int validate_uncallable_instructions(struct objtool_file *file)
|
|
{
|
|
struct instruction *insn;
|
|
int warnings = 0;
|
|
|
|
for_each_insn(file, insn) {
|
|
if (!insn->visited && insn->type == INSN_RETURN) {
|
|
WARN_FUNC("return instruction outside of a callable function",
|
|
insn->sec, insn->offset);
|
|
warnings++;
|
|
}
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
static void cleanup(struct objtool_file *file)
|
|
{
|
|
struct instruction *insn, *tmpinsn;
|
|
struct alternative *alt, *tmpalt;
|
|
|
|
list_for_each_entry_safe(insn, tmpinsn, &file->insn_list, list) {
|
|
list_for_each_entry_safe(alt, tmpalt, &insn->alts, list) {
|
|
list_del(&alt->list);
|
|
free(alt);
|
|
}
|
|
list_del(&insn->list);
|
|
hash_del(&insn->hash);
|
|
free(insn);
|
|
}
|
|
elf_close(file->elf);
|
|
}
|
|
|
|
const char * const check_usage[] = {
|
|
"objtool check [<options>] file.o",
|
|
NULL,
|
|
};
|
|
|
|
int cmd_check(int argc, const char **argv)
|
|
{
|
|
struct objtool_file file;
|
|
int ret, warnings = 0;
|
|
|
|
const struct option options[] = {
|
|
OPT_BOOLEAN('f', "no-fp", &nofp, "Skip frame pointer validation"),
|
|
OPT_END(),
|
|
};
|
|
|
|
argc = parse_options(argc, argv, options, check_usage, 0);
|
|
|
|
if (argc != 1)
|
|
usage_with_options(check_usage, options);
|
|
|
|
objname = argv[0];
|
|
|
|
file.elf = elf_open(objname);
|
|
if (!file.elf) {
|
|
fprintf(stderr, "error reading elf file %s\n", objname);
|
|
return 1;
|
|
}
|
|
|
|
INIT_LIST_HEAD(&file.insn_list);
|
|
hash_init(file.insn_hash);
|
|
file.whitelist = find_section_by_name(file.elf, "__func_stack_frame_non_standard");
|
|
file.rodata = find_section_by_name(file.elf, ".rodata");
|
|
file.ignore_unreachables = false;
|
|
file.c_file = find_section_by_name(file.elf, ".comment");
|
|
|
|
ret = decode_sections(&file);
|
|
if (ret < 0)
|
|
goto out;
|
|
warnings += ret;
|
|
|
|
ret = validate_functions(&file);
|
|
if (ret < 0)
|
|
goto out;
|
|
warnings += ret;
|
|
|
|
ret = validate_uncallable_instructions(&file);
|
|
if (ret < 0)
|
|
goto out;
|
|
warnings += ret;
|
|
|
|
out:
|
|
cleanup(&file);
|
|
|
|
/* ignore warnings for now until we get all the code cleaned up */
|
|
if (ret || warnings)
|
|
return 0;
|
|
return 0;
|
|
}
|