objtool, x86: Add facility for asm code to provide unwind hints
authorJosh Poimboeuf <jpoimboe@redhat.com>
Tue, 11 Jul 2017 15:33:43 +0000 (10:33 -0500)
committerIngo Molnar <mingo@kernel.org>
Tue, 18 Jul 2017 08:57:44 +0000 (10:57 +0200)
Some asm (and inline asm) code does special things to the stack which
objtool can't understand.  (Nor can GCC or GNU assembler, for that
matter.)  In such cases we need a facility for the code to provide
annotations, so the unwinder can unwind through it.

This provides such a facility, in the form of unwind hints.  They're
similar to the GNU assembler .cfi* directives, but they give more
information, and are needed in far fewer places, because objtool can
fill in the blanks by following branches and adjusting the stack pointer
for pushes and pops.

Signed-off-by: Josh Poimboeuf <jpoimboe@redhat.com>
Cc: Andy Lutomirski <luto@kernel.org>
Cc: Borislav Petkov <bp@alien8.de>
Cc: Brian Gerst <brgerst@gmail.com>
Cc: Denys Vlasenko <dvlasenk@redhat.com>
Cc: H. Peter Anvin <hpa@zytor.com>
Cc: Jiri Slaby <jslaby@suse.cz>
Cc: Linus Torvalds <torvalds@linux-foundation.org>
Cc: Mike Galbraith <efault@gmx.de>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Thomas Gleixner <tglx@linutronix.de>
Cc: live-patching@vger.kernel.org
Link: http://lkml.kernel.org/r/0f5f3c9104fca559ff4088bece1d14ae3bca52d5.1499786555.git.jpoimboe@redhat.com
Signed-off-by: Ingo Molnar <mingo@kernel.org>
arch/x86/include/asm/orc_types.h [new file with mode: 0644]
arch/x86/include/asm/unwind_hints.h [new file with mode: 0644]
tools/objtool/Makefile
tools/objtool/check.c
tools/objtool/check.h
tools/objtool/orc_types.h

diff --git a/arch/x86/include/asm/orc_types.h b/arch/x86/include/asm/orc_types.h
new file mode 100644 (file)
index 0000000..7dc777a
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2017 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/>.
+ */
+
+#ifndef _ORC_TYPES_H
+#define _ORC_TYPES_H
+
+#include <linux/types.h>
+#include <linux/compiler.h>
+
+/*
+ * The ORC_REG_* registers are base registers which are used to find other
+ * registers on the stack.
+ *
+ * ORC_REG_PREV_SP, also known as DWARF Call Frame Address (CFA), is the
+ * address of the previous frame: the caller's SP before it called the current
+ * function.
+ *
+ * ORC_REG_UNDEFINED means the corresponding register's value didn't change in
+ * the current frame.
+ *
+ * The most commonly used base registers are SP and BP -- which the previous SP
+ * is usually based on -- and PREV_SP and UNDEFINED -- which the previous BP is
+ * usually based on.
+ *
+ * The rest of the base registers are needed for special cases like entry code
+ * and GCC realigned stacks.
+ */
+#define ORC_REG_UNDEFINED              0
+#define ORC_REG_PREV_SP                        1
+#define ORC_REG_DX                     2
+#define ORC_REG_DI                     3
+#define ORC_REG_BP                     4
+#define ORC_REG_SP                     5
+#define ORC_REG_R10                    6
+#define ORC_REG_R13                    7
+#define ORC_REG_BP_INDIRECT            8
+#define ORC_REG_SP_INDIRECT            9
+#define ORC_REG_MAX                    15
+
+/*
+ * ORC_TYPE_CALL: Indicates that sp_reg+sp_offset resolves to PREV_SP (the
+ * caller's SP right before it made the call).  Used for all callable
+ * functions, i.e. all C code and all callable asm functions.
+ *
+ * ORC_TYPE_REGS: Used in entry code to indicate that sp_reg+sp_offset points
+ * to a fully populated pt_regs from a syscall, interrupt, or exception.
+ *
+ * ORC_TYPE_REGS_IRET: Used in entry code to indicate that sp_reg+sp_offset
+ * points to the iret return frame.
+ *
+ * The UNWIND_HINT macros are used only for the unwind_hint struct.  They
+ * aren't used in struct orc_entry due to size and complexity constraints.
+ * Objtool converts them to real types when it converts the hints to orc
+ * entries.
+ */
+#define ORC_TYPE_CALL                  0
+#define ORC_TYPE_REGS                  1
+#define ORC_TYPE_REGS_IRET             2
+#define UNWIND_HINT_TYPE_SAVE          3
+#define UNWIND_HINT_TYPE_RESTORE       4
+
+#ifndef __ASSEMBLY__
+/*
+ * This struct is more or less a vastly simplified version of the DWARF Call
+ * Frame Information standard.  It contains only the necessary parts of DWARF
+ * CFI, simplified for ease of access by the in-kernel unwinder.  It tells the
+ * unwinder how to find the previous SP and BP (and sometimes entry regs) on
+ * the stack for a given code address.  Each instance of the struct corresponds
+ * to one or more code locations.
+ */
+struct orc_entry {
+       s16             sp_offset;
+       s16             bp_offset;
+       unsigned        sp_reg:4;
+       unsigned        bp_reg:4;
+       unsigned        type:2;
+};
+
+/*
+ * This struct is used by asm and inline asm code to manually annotate the
+ * location of registers on the stack for the ORC unwinder.
+ *
+ * Type can be either ORC_TYPE_* or UNWIND_HINT_TYPE_*.
+ */
+struct unwind_hint {
+       u32             ip;
+       s16             sp_offset;
+       u8              sp_reg;
+       u8              type;
+};
+#endif /* __ASSEMBLY__ */
+
+#endif /* _ORC_TYPES_H */
diff --git a/arch/x86/include/asm/unwind_hints.h b/arch/x86/include/asm/unwind_hints.h
new file mode 100644 (file)
index 0000000..5e02b11
--- /dev/null
@@ -0,0 +1,103 @@
+#ifndef _ASM_X86_UNWIND_HINTS_H
+#define _ASM_X86_UNWIND_HINTS_H
+
+#include "orc_types.h"
+
+#ifdef __ASSEMBLY__
+
+/*
+ * In asm, there are two kinds of code: normal C-type callable functions and
+ * the rest.  The normal callable functions can be called by other code, and
+ * don't do anything unusual with the stack.  Such normal callable functions
+ * are annotated with the ENTRY/ENDPROC macros.  Most asm code falls in this
+ * category.  In this case, no special debugging annotations are needed because
+ * objtool can automatically generate the ORC data for the ORC unwinder to read
+ * at runtime.
+ *
+ * Anything which doesn't fall into the above category, such as syscall and
+ * interrupt handlers, tends to not be called directly by other functions, and
+ * often does unusual non-C-function-type things with the stack pointer.  Such
+ * code needs to be annotated such that objtool can understand it.  The
+ * following CFI hint macros are for this type of code.
+ *
+ * These macros provide hints to objtool about the state of the stack at each
+ * instruction.  Objtool starts from the hints and follows the code flow,
+ * making automatic CFI adjustments when it sees pushes and pops, filling out
+ * the debuginfo as necessary.  It will also warn if it sees any
+ * inconsistencies.
+ */
+.macro UNWIND_HINT sp_reg=ORC_REG_SP sp_offset=0 type=ORC_TYPE_CALL
+#ifdef CONFIG_STACK_VALIDATION
+.Lunwind_hint_ip_\@:
+       .pushsection .discard.unwind_hints
+               /* struct unwind_hint */
+               .long .Lunwind_hint_ip_\@ - .
+               .short \sp_offset
+               .byte \sp_reg
+               .byte \type
+       .popsection
+#endif
+.endm
+
+.macro UNWIND_HINT_EMPTY
+       UNWIND_HINT sp_reg=ORC_REG_UNDEFINED
+.endm
+
+.macro UNWIND_HINT_REGS base=%rsp offset=0 indirect=0 extra=1 iret=0
+       .if \base == %rsp && \indirect
+               .set sp_reg, ORC_REG_SP_INDIRECT
+       .elseif \base == %rsp
+               .set sp_reg, ORC_REG_SP
+       .elseif \base == %rbp
+               .set sp_reg, ORC_REG_BP
+       .elseif \base == %rdi
+               .set sp_reg, ORC_REG_DI
+       .elseif \base == %rdx
+               .set sp_reg, ORC_REG_DX
+       .elseif \base == %r10
+               .set sp_reg, ORC_REG_R10
+       .else
+               .error "UNWIND_HINT_REGS: bad base register"
+       .endif
+
+       .set sp_offset, \offset
+
+       .if \iret
+               .set type, ORC_TYPE_REGS_IRET
+       .elseif \extra == 0
+               .set type, ORC_TYPE_REGS_IRET
+               .set sp_offset, \offset + (16*8)
+       .else
+               .set type, ORC_TYPE_REGS
+       .endif
+
+       UNWIND_HINT sp_reg=sp_reg sp_offset=sp_offset type=type
+.endm
+
+.macro UNWIND_HINT_IRET_REGS base=%rsp offset=0
+       UNWIND_HINT_REGS base=\base offset=\offset iret=1
+.endm
+
+.macro UNWIND_HINT_FUNC sp_offset=8
+       UNWIND_HINT sp_offset=\sp_offset
+.endm
+
+#else /* !__ASSEMBLY__ */
+
+#define UNWIND_HINT(sp_reg, sp_offset, type)                   \
+       "987: \n\t"                                             \
+       ".pushsection .discard.unwind_hints\n\t"                \
+       /* struct unwind_hint */                                \
+       ".long 987b - .\n\t"                                    \
+       ".short " __stringify(sp_offset) "\n\t"         \
+       ".byte " __stringify(sp_reg) "\n\t"                     \
+       ".byte " __stringify(type) "\n\t"                       \
+       ".popsection\n\t"
+
+#define UNWIND_HINT_SAVE UNWIND_HINT(0, 0, UNWIND_HINT_TYPE_SAVE)
+
+#define UNWIND_HINT_RESTORE UNWIND_HINT(0, 0, UNWIND_HINT_TYPE_RESTORE)
+
+#endif /* __ASSEMBLY__ */
+
+#endif /* _ASM_X86_UNWIND_HINTS_H */
index 0e2765e243c06c55060874629f085cdcee1f9502..3a6425fefc43e10731a94bd318d47734149ec302 100644 (file)
@@ -52,6 +52,9 @@ $(OBJTOOL): $(LIBSUBCMD) $(OBJTOOL_IN)
        diff -I'^#include' arch/x86/insn/inat.h ../../arch/x86/include/asm/inat.h >/dev/null && \
        diff -I'^#include' arch/x86/insn/inat_types.h ../../arch/x86/include/asm/inat_types.h >/dev/null) \
        || echo "warning: objtool: x86 instruction decoder differs from kernel" >&2 )) || true
+       @(test -d ../../kernel -a -d ../../tools -a -d ../objtool && (( \
+       diff ../../arch/x86/include/asm/orc_types.h orc_types.h >/dev/null) \
+       || echo "warning: objtool: orc_types.h differs from kernel" >&2 )) || true
        $(QUIET_LINK)$(CC) $(OBJTOOL_IN) $(LDFLAGS) -o $@
 
 
index cb57c526ba178464beeb1e97c0ac13c647cfe6e3..368275de5f23690f71d2259c86296c58b967c456 100644 (file)
@@ -100,7 +100,6 @@ static bool gcov_enabled(struct objtool_file *file)
 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)
@@ -113,11 +112,6 @@ static bool ignore_func(struct objtool_file *file, struct symbol *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;
 }
 
@@ -879,6 +873,99 @@ static int add_switch_table_alts(struct objtool_file *file)
        return 0;
 }
 
+static int read_unwind_hints(struct objtool_file *file)
+{
+       struct section *sec, *relasec;
+       struct rela *rela;
+       struct unwind_hint *hint;
+       struct instruction *insn;
+       struct cfi_reg *cfa;
+       int i;
+
+       sec = find_section_by_name(file->elf, ".discard.unwind_hints");
+       if (!sec)
+               return 0;
+
+       relasec = sec->rela;
+       if (!relasec) {
+               WARN("missing .rela.discard.unwind_hints section");
+               return -1;
+       }
+
+       if (sec->len % sizeof(struct unwind_hint)) {
+               WARN("struct unwind_hint size mismatch");
+               return -1;
+       }
+
+       file->hints = true;
+
+       for (i = 0; i < sec->len / sizeof(struct unwind_hint); i++) {
+               hint = (struct unwind_hint *)sec->data->d_buf + i;
+
+               rela = find_rela_by_dest(sec, i * sizeof(*hint));
+               if (!rela) {
+                       WARN("can't find rela for unwind_hints[%d]", i);
+                       return -1;
+               }
+
+               insn = find_insn(file, rela->sym->sec, rela->addend);
+               if (!insn) {
+                       WARN("can't find insn for unwind_hints[%d]", i);
+                       return -1;
+               }
+
+               cfa = &insn->state.cfa;
+
+               if (hint->type == UNWIND_HINT_TYPE_SAVE) {
+                       insn->save = true;
+                       continue;
+
+               } else if (hint->type == UNWIND_HINT_TYPE_RESTORE) {
+                       insn->restore = true;
+                       insn->hint = true;
+                       continue;
+               }
+
+               insn->hint = true;
+
+               switch (hint->sp_reg) {
+               case ORC_REG_UNDEFINED:
+                       cfa->base = CFI_UNDEFINED;
+                       break;
+               case ORC_REG_SP:
+                       cfa->base = CFI_SP;
+                       break;
+               case ORC_REG_BP:
+                       cfa->base = CFI_BP;
+                       break;
+               case ORC_REG_SP_INDIRECT:
+                       cfa->base = CFI_SP_INDIRECT;
+                       break;
+               case ORC_REG_R10:
+                       cfa->base = CFI_R10;
+                       break;
+               case ORC_REG_R13:
+                       cfa->base = CFI_R13;
+                       break;
+               case ORC_REG_DI:
+                       cfa->base = CFI_DI;
+                       break;
+               case ORC_REG_DX:
+                       cfa->base = CFI_DX;
+                       break;
+               default:
+                       WARN_FUNC("unsupported unwind_hint sp base reg %d",
+                                 insn->sec, insn->offset, hint->sp_reg);
+                       return -1;
+               }
+
+               cfa->offset = hint->sp_offset;
+               insn->state.type = hint->type;
+       }
+
+       return 0;
+}
+
 static int decode_sections(struct objtool_file *file)
 {
        int ret;
@@ -909,6 +996,10 @@ static int decode_sections(struct objtool_file *file)
        if (ret)
                return ret;
 
+       ret = read_unwind_hints(file);
+       if (ret)
+               return ret;
+
        return 0;
 }
 
@@ -1382,7 +1473,7 @@ static int validate_branch(struct objtool_file *file, struct instruction *first,
                           struct insn_state state)
 {
        struct alternative *alt;
-       struct instruction *insn;
+       struct instruction *insn, *next_insn;
        struct section *sec;
        struct symbol *func = NULL;
        int ret;
@@ -1397,6 +1488,8 @@ static int validate_branch(struct objtool_file *file, struct instruction *first,
        }
 
        while (1) {
+               next_insn = next_insn_same_sec(file, insn);
+
                if (file->c_file && insn->func) {
                        if (func && func != insn->func) {
                                WARN("%s() falls through to next function %s()",
@@ -1414,13 +1507,54 @@ static int validate_branch(struct objtool_file *file, struct instruction *first,
                }
 
                if (insn->visited) {
-                       if (!!insn_state_match(insn, &state))
+                       if (!insn->hint && !insn_state_match(insn, &state))
                                return 1;
 
                        return 0;
                }
 
-               insn->state = state;
+               if (insn->hint) {
+                       if (insn->restore) {
+                               struct instruction *save_insn, *i;
+
+                               i = insn;
+                               save_insn = NULL;
+                               func_for_each_insn_continue_reverse(file, func, i) {
+                                       if (i->save) {
+                                               save_insn = i;
+                                               break;
+                                       }
+                               }
+
+                               if (!save_insn) {
+                                       WARN_FUNC("no corresponding CFI save for CFI restore",
+                                                 sec, insn->offset);
+                                       return 1;
+                               }
+
+                               if (!save_insn->visited) {
+                                       /*
+                                        * Oops, no state to copy yet.
+                                        * Hopefully we can reach this
+                                        * instruction from another branch
+                                        * after the save insn has been
+                                        * visited.
+                                        */
+                                       if (insn == first)
+                                               return 0;
+
+                                       WARN_FUNC("objtool isn't smart enough to handle this CFI save/restore combo",
+                                                 sec, insn->offset);
+                                       return 1;
+                               }
+
+                               insn->state = save_insn->state;
+                       }
+
+                       state = insn->state;
+
+               } else
+                       insn->state = state;
 
                insn->visited = true;
 
@@ -1497,6 +1631,14 @@ static int validate_branch(struct objtool_file *file, struct instruction *first,
 
                        return 0;
 
+               case INSN_CONTEXT_SWITCH:
+                       if (func && (!next_insn || !next_insn->hint)) {
+                               WARN_FUNC("unsupported instruction in callable function",
+                                         sec, insn->offset);
+                               return 1;
+                       }
+                       return 0;
+
                case INSN_STACK:
                        if (update_insn_state(insn, &state))
                                return -1;
@@ -1510,7 +1652,7 @@ static int validate_branch(struct objtool_file *file, struct instruction *first,
                if (insn->dead_end)
                        return 0;
 
-               insn = next_insn_same_sec(file, insn);
+               insn = next_insn;
                if (!insn) {
                        WARN("%s: unexpected end of section", sec->name);
                        return 1;
@@ -1520,6 +1662,27 @@ static int validate_branch(struct objtool_file *file, struct instruction *first,
        return 0;
 }
 
+static int validate_unwind_hints(struct objtool_file *file)
+{
+       struct instruction *insn;
+       int ret, warnings = 0;
+       struct insn_state state;
+
+       if (!file->hints)
+               return 0;
+
+       clear_insn_state(&state);
+
+       for_each_insn(file, insn) {
+               if (insn->hint && !insn->visited) {
+                       ret = validate_branch(file, insn, state);
+                       warnings += ret;
+               }
+       }
+
+       return warnings;
+}
+
 static bool is_kasan_insn(struct instruction *insn)
 {
        return (insn->type == INSN_CALL &&
@@ -1665,8 +1828,9 @@ int check(const char *_objname, bool _nofp, bool orc)
        hash_init(file.insn_hash);
        file.whitelist = find_section_by_name(file.elf, ".discard.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");
+       file.ignore_unreachables = false;
+       file.hints = false;
 
        arch_initial_func_cfi_state(&initial_func_cfi);
 
@@ -1683,6 +1847,11 @@ int check(const char *_objname, bool _nofp, bool orc)
                goto out;
        warnings += ret;
 
+       ret = validate_unwind_hints(&file);
+       if (ret < 0)
+               goto out;
+       warnings += ret;
+
        if (!warnings) {
                ret = validate_reachable_instructions(&file);
                if (ret < 0)
index 046874bbe22664a23968249774e6c475e0d0c660..ac3d4b13f17bff3d973f40c856ed1d9e1350d2de 100644 (file)
@@ -43,7 +43,7 @@ struct instruction {
        unsigned int len;
        unsigned char type;
        unsigned long immediate;
-       bool alt_group, visited, dead_end, ignore;
+       bool alt_group, visited, dead_end, ignore, hint, save, restore;
        struct symbol *call_dest;
        struct instruction *jump_dest;
        struct list_head alts;
@@ -58,7 +58,7 @@ struct objtool_file {
        struct list_head insn_list;
        DECLARE_HASHTABLE(insn_hash, 16);
        struct section *rodata, *whitelist;
-       bool ignore_unreachables, c_file;
+       bool ignore_unreachables, c_file, hints;
 };
 
 int check(const char *objname, bool nofp, bool orc);
index fc5cf6cffd9abbbe3238152984b662f39ca32b75..9c9dc579bd7db172287f5fdea9628cabaeff56af 100644 (file)
  *
  * ORC_TYPE_REGS_IRET: Used in entry code to indicate that sp_reg+sp_offset
  * points to the iret return frame.
+ *
+ * The UNWIND_HINT macros are used only for the unwind_hint struct.  They
+ * aren't used in struct orc_entry due to size and complexity constraints.
+ * Objtool converts them to real types when it converts the hints to orc
+ * entries.
  */
 #define ORC_TYPE_CALL                  0
 #define ORC_TYPE_REGS                  1
 #define ORC_TYPE_REGS_IRET             2
+#define UNWIND_HINT_TYPE_SAVE          3
+#define UNWIND_HINT_TYPE_RESTORE       4
 
+#ifndef __ASSEMBLY__
 /*
  * This struct is more or less a vastly simplified version of the DWARF Call
  * Frame Information standard.  It contains only the necessary parts of DWARF
@@ -82,4 +90,18 @@ struct orc_entry {
        unsigned        type:2;
 } __packed;
 
+/*
+ * This struct is used by asm and inline asm code to manually annotate the
+ * location of registers on the stack for the ORC unwinder.
+ *
+ * Type can be either ORC_TYPE_* or UNWIND_HINT_TYPE_*.
+ */
+struct unwind_hint {
+       u32             ip;
+       s16             sp_offset;
+       u8              sp_reg;
+       u8              type;
+};
+#endif /* __ASSEMBLY__ */
+
 #endif /* _ORC_TYPES_H */