| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * Copyright (C) 2015-2017 Josh Poimboeuf <jpoimboe@redhat.com> | |
| */ | |
| #define _GNU_SOURCE | |
| #include <fnmatch.h> | |
| #include <objtool/arch.h> | |
| #include <objtool/check.h> | |
| #include <objtool/disas.h> | |
| #include <objtool/special.h> | |
| #include <objtool/warn.h> | |
| #include <bfd.h> | |
| #include <linux/string.h> | |
| #include <tools/dis-asm-compat.h> | |
| /* | |
| * Size of the buffer for storing the result of disassembling | |
| * a single instruction. | |
| */ | |
| #define DISAS_RESULT_SIZE 1024 | |
| struct disas_context { | |
| struct objtool_file *file; | |
| struct instruction *insn; | |
| bool alt_applied; | |
| char result[DISAS_RESULT_SIZE]; | |
| disassembler_ftype disassembler; | |
| struct disassemble_info info; | |
| }; | |
| /* | |
| * Maximum number of alternatives | |
| */ | |
| #define DISAS_ALT_MAX 5 | |
| /* | |
| * Maximum number of instructions per alternative | |
| */ | |
| #define DISAS_ALT_INSN_MAX 50 | |
| /* | |
| * Information to disassemble an alternative | |
| */ | |
| struct disas_alt { | |
| struct instruction *orig_insn; /* original instruction */ | |
| struct alternative *alt; /* alternative or NULL if default code */ | |
| char *name; /* name for this alternative */ | |
| int width; /* formatting width */ | |
| struct { | |
| char *str; /* instruction string */ | |
| int offset; /* instruction offset */ | |
| int nops; /* number of nops */ | |
| } insn[DISAS_ALT_INSN_MAX]; /* alternative instructions */ | |
| int insn_idx; /* index of the next instruction to print */ | |
| }; | |
| #define DALT_DEFAULT(dalt) (!(dalt)->alt) | |
| #define DALT_INSN(dalt) (DALT_DEFAULT(dalt) ? (dalt)->orig_insn : (dalt)->alt->insn) | |
| #define DALT_GROUP(dalt) (DALT_INSN(dalt)->alt_group) | |
| #define DALT_ALTID(dalt) ((dalt)->orig_insn->offset) | |
| #define ALT_FLAGS_SHIFT 16 | |
| #define ALT_FLAG_NOT (1 << 0) | |
| #define ALT_FLAG_DIRECT_CALL (1 << 1) | |
| #define ALT_FEATURE_MASK ((1 << ALT_FLAGS_SHIFT) - 1) | |
| static int alt_feature(unsigned int ft_flags) | |
| { | |
| return (ft_flags & ALT_FEATURE_MASK); | |
| } | |
| static int alt_flags(unsigned int ft_flags) | |
| { | |
| return (ft_flags >> ALT_FLAGS_SHIFT); | |
| } | |
| /* | |
| * Wrapper around asprintf() to allocate and format a string. | |
| * Return the allocated string or NULL on error. | |
| */ | |
| static char *strfmt(const char *fmt, ...) | |
| { | |
| va_list ap; | |
| char *str; | |
| int rv; | |
| va_start(ap, fmt); | |
| rv = vasprintf(&str, fmt, ap); | |
| va_end(ap); | |
| return rv == -1 ? NULL : str; | |
| } | |
| static int sprint_name(char *str, const char *name, unsigned long offset) | |
| { | |
| int len; | |
| if (offset) | |
| len = sprintf(str, "%s+0x%lx", name, offset); | |
| else | |
| len = sprintf(str, "%s", name); | |
| return len; | |
| } | |
| #define DINFO_FPRINTF(dinfo, ...) \ | |
| ((*(dinfo)->fprintf_func)((dinfo)->stream, __VA_ARGS__)) | |
| #define bfd_vma_fmt \ | |
| __builtin_choose_expr(sizeof(bfd_vma) == sizeof(unsigned long), "%#lx <%s>", "%#llx <%s>") | |
| static int disas_result_fprintf(struct disas_context *dctx, | |
| const char *fmt, va_list ap) | |
| { | |
| char *buf = dctx->result; | |
| int avail, len; | |
| len = strlen(buf); | |
| if (len >= DISAS_RESULT_SIZE - 1) { | |
| WARN_FUNC(dctx->insn->sec, dctx->insn->offset, | |
| "disassembly buffer is full"); | |
| return -1; | |
| } | |
| avail = DISAS_RESULT_SIZE - len; | |
| len = vsnprintf(buf + len, avail, fmt, ap); | |
| if (len < 0 || len >= avail) { | |
| WARN_FUNC(dctx->insn->sec, dctx->insn->offset, | |
| "disassembly buffer is truncated"); | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static int disas_fprintf(void *stream, const char *fmt, ...) | |
| { | |
| va_list arg; | |
| int rv; | |
| va_start(arg, fmt); | |
| rv = disas_result_fprintf(stream, fmt, arg); | |
| va_end(arg); | |
| return rv; | |
| } | |
| /* | |
| * For init_disassemble_info_compat(). | |
| */ | |
| static int disas_fprintf_styled(void *stream, | |
| enum disassembler_style style, | |
| const char *fmt, ...) | |
| { | |
| va_list arg; | |
| int rv; | |
| va_start(arg, fmt); | |
| rv = disas_result_fprintf(stream, fmt, arg); | |
| va_end(arg); | |
| return rv; | |
| } | |
| static void disas_print_addr_sym(struct section *sec, struct symbol *sym, | |
| bfd_vma addr, struct disassemble_info *dinfo) | |
| { | |
| char symstr[1024]; | |
| char *str; | |
| if (sym) { | |
| sprint_name(symstr, sym->name, addr - sym->offset); | |
| DINFO_FPRINTF(dinfo, bfd_vma_fmt, addr, symstr); | |
| } else { | |
| str = offstr(sec, addr); | |
| DINFO_FPRINTF(dinfo, bfd_vma_fmt, addr, str); | |
| free(str); | |
| } | |
| } | |
| static bool disas_print_addr_alt(bfd_vma addr, struct disassemble_info *dinfo) | |
| { | |
| struct disas_context *dctx = dinfo->application_data; | |
| struct instruction *orig_first_insn; | |
| struct alt_group *alt_group; | |
| unsigned long offset; | |
| struct symbol *sym; | |
| /* | |
| * Check if we are processing an alternative at the original | |
| * instruction address (i.e. if alt_applied is true) and if | |
| * we are referencing an address inside the alternative. | |
| * | |
| * For example, this happens if there is a branch inside an | |
| * alternative. In that case, the address should be updated | |
| * to a reference inside the original instruction flow. | |
| */ | |
| if (!dctx->alt_applied) | |
| return false; | |
| alt_group = dctx->insn->alt_group; | |
| if (!alt_group || !alt_group->orig_group || | |
| addr < alt_group->first_insn->offset || | |
| addr > alt_group->last_insn->offset) | |
| return false; | |
| orig_first_insn = alt_group->orig_group->first_insn; | |
| offset = addr - alt_group->first_insn->offset; | |
| addr = orig_first_insn->offset + offset; | |
| sym = insn_sym(orig_first_insn); | |
| disas_print_addr_sym(orig_first_insn->sec, sym, addr, dinfo); | |
| return true; | |
| } | |
| static void disas_print_addr_noreloc(bfd_vma addr, | |
| struct disassemble_info *dinfo) | |
| { | |
| struct disas_context *dctx = dinfo->application_data; | |
| struct instruction *insn = dctx->insn; | |
| struct symbol *sym = insn_sym(insn); | |
| if (disas_print_addr_alt(addr, dinfo)) | |
| return; | |
| if (sym && (addr < sym->offset || addr >= sym->offset + sym->len)) | |
| sym = NULL; | |
| disas_print_addr_sym(insn->sec, sym, addr, dinfo); | |
| } | |
| static void disas_print_addr_reloc(bfd_vma addr, struct disassemble_info *dinfo) | |
| { | |
| struct disas_context *dctx = dinfo->application_data; | |
| struct instruction *insn = dctx->insn; | |
| unsigned long offset; | |
| struct reloc *reloc; | |
| char symstr[1024]; | |
| char *str; | |
| reloc = find_reloc_by_dest_range(dctx->file->elf, insn->sec, | |
| insn->offset, insn->len); | |
| if (!reloc) { | |
| /* | |
| * There is no relocation for this instruction although | |
| * the address to resolve points to the next instruction. | |
| * So this is an effective reference to the next IP, for | |
| * example: "lea 0x0(%rip),%rdi". The kernel can reference | |
| * the next IP with _THIS_IP_ macro. | |
| */ | |
| DINFO_FPRINTF(dinfo, bfd_vma_fmt, addr, "_THIS_IP_"); | |
| return; | |
| } | |
| offset = arch_insn_adjusted_addend(insn, reloc); | |
| /* | |
| * If the relocation symbol is a section name (for example ".bss") | |
| * then we try to further resolve the name. | |
| */ | |
| if (is_sec_sym(reloc->sym)) { | |
| str = offstr(reloc->sym->sec, reloc->sym->offset + offset); | |
| DINFO_FPRINTF(dinfo, bfd_vma_fmt, addr, str); | |
| free(str); | |
| } else { | |
| sprint_name(symstr, reloc->sym->name, offset); | |
| DINFO_FPRINTF(dinfo, bfd_vma_fmt, addr, symstr); | |
| } | |
| } | |
| /* | |
| * Resolve an address into a "<symbol>+<offset>" string. | |
| */ | |
| static void disas_print_address(bfd_vma addr, struct disassemble_info *dinfo) | |
| { | |
| struct disas_context *dctx = dinfo->application_data; | |
| struct instruction *insn = dctx->insn; | |
| struct instruction *jump_dest; | |
| struct symbol *sym; | |
| bool is_reloc; | |
| /* | |
| * If the instruction is a call/jump and it references a | |
| * destination then this is likely the address we are looking | |
| * up. So check it first. | |
| */ | |
| jump_dest = insn->jump_dest; | |
| if (jump_dest && insn_sym(jump_dest) && jump_dest->offset == addr) { | |
| if (!disas_print_addr_alt(addr, dinfo)) | |
| disas_print_addr_sym(jump_dest->sec, insn_sym(jump_dest), | |
| addr, dinfo); | |
| return; | |
| } | |
| /* | |
| * If the address points to the next instruction then there is | |
| * probably a relocation. It can be a false positive when the | |
| * current instruction is referencing the address of the next | |
| * instruction. This particular case will be handled in | |
| * disas_print_addr_reloc(). | |
| */ | |
| is_reloc = (addr == insn->offset + insn->len); | |
| /* | |
| * The call destination offset can be the address we are looking | |
| * up, or 0 if there is a relocation. | |
| */ | |
| sym = insn_call_dest(insn); | |
| if (sym && (sym->offset == addr || (sym->offset == 0 && is_reloc))) { | |
| DINFO_FPRINTF(dinfo, bfd_vma_fmt, addr, sym->name); | |
| return; | |
| } | |
| if (!is_reloc) | |
| disas_print_addr_noreloc(addr, dinfo); | |
| else | |
| disas_print_addr_reloc(addr, dinfo); | |
| } | |
| /* | |
| * Initialize disassemble info arch, mach (32 or 64-bit) and options. | |
| */ | |
| int disas_info_init(struct disassemble_info *dinfo, | |
| int arch, int mach32, int mach64, | |
| const char *options) | |
| { | |
| struct disas_context *dctx = dinfo->application_data; | |
| struct objtool_file *file = dctx->file; | |
| dinfo->arch = arch; | |
| switch (file->elf->ehdr.e_ident[EI_CLASS]) { | |
| case ELFCLASS32: | |
| dinfo->mach = mach32; | |
| break; | |
| case ELFCLASS64: | |
| dinfo->mach = mach64; | |
| break; | |
| default: | |
| return -1; | |
| } | |
| dinfo->disassembler_options = options; | |
| return 0; | |
| } | |
| struct disas_context *disas_context_create(struct objtool_file *file) | |
| { | |
| struct disas_context *dctx; | |
| struct disassemble_info *dinfo; | |
| int err; | |
| dctx = malloc(sizeof(*dctx)); | |
| if (!dctx) { | |
| WARN("failed to allocate disassembly context"); | |
| return NULL; | |
| } | |
| dctx->file = file; | |
| dinfo = &dctx->info; | |
| init_disassemble_info_compat(dinfo, dctx, | |
| disas_fprintf, disas_fprintf_styled); | |
| dinfo->read_memory_func = buffer_read_memory; | |
| dinfo->print_address_func = disas_print_address; | |
| dinfo->application_data = dctx; | |
| /* | |
| * bfd_openr() is not used to avoid doing ELF data processing | |
| * and caching that has already being done. Here, we just need | |
| * to identify the target file so we call an arch specific | |
| * function to fill some disassemble info (arch, mach). | |
| */ | |
| dinfo->arch = bfd_arch_unknown; | |
| dinfo->mach = 0; | |
| err = arch_disas_info_init(dinfo); | |
| if (err || dinfo->arch == bfd_arch_unknown || dinfo->mach == 0) { | |
| WARN("failed to init disassembly arch"); | |
| goto error; | |
| } | |
| dinfo->endian = (file->elf->ehdr.e_ident[EI_DATA] == ELFDATA2MSB) ? | |
| BFD_ENDIAN_BIG : BFD_ENDIAN_LITTLE; | |
| disassemble_init_for_target(dinfo); | |
| dctx->disassembler = disassembler(dinfo->arch, | |
| dinfo->endian == BFD_ENDIAN_BIG, | |
| dinfo->mach, NULL); | |
| if (!dctx->disassembler) { | |
| WARN("failed to create disassembler function"); | |
| goto error; | |
| } | |
| return dctx; | |
| error: | |
| free(dctx); | |
| return NULL; | |
| } | |
| void disas_context_destroy(struct disas_context *dctx) | |
| { | |
| free(dctx); | |
| } | |
| char *disas_result(struct disas_context *dctx) | |
| { | |
| return dctx->result; | |
| } | |
| #define DISAS_INSN_OFFSET_SPACE 10 | |
| #define DISAS_INSN_SPACE 60 | |
| #define DISAS_PRINSN(dctx, insn, depth) \ | |
| disas_print_insn(stdout, dctx, insn, depth, "\n") | |
| /* | |
| * Print a message in the instruction flow. If sec is not NULL then the | |
| * address at the section offset is printed in addition of the message, | |
| * otherwise only the message is 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; | |
| } | |
| static int disas_print(FILE *stream, struct section *sec, unsigned long offset, | |
| int depth, const char *format, ...) | |
| { | |
| va_list args; | |
| int len; | |
| va_start(args, format); | |
| len = disas_vprint(stream, sec, offset, depth, format, args); | |
| va_end(args); | |
| return len; | |
| } | |
| /* | |
| * 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. | |
| * | |
| * If alt_applied is true then insn should be an instruction from of an | |
| * alternative (i.e. insn->alt_group != NULL), and it is disassembled | |
| * at the location of the original code it is replacing. When the | |
| * instruction references any address inside the alternative then | |
| * these references will be re-adjusted to replace the original code. | |
| */ | |
| static size_t disas_insn_common(struct disas_context *dctx, | |
| struct instruction *insn, | |
| bool alt_applied) | |
| { | |
| disassembler_ftype disasm = dctx->disassembler; | |
| struct disassemble_info *dinfo = &dctx->info; | |
| dctx->insn = insn; | |
| dctx->alt_applied = alt_applied; | |
| dctx->result[0] = '\0'; | |
| if (insn->type == INSN_NOP) { | |
| DINFO_FPRINTF(dinfo, "nop%d", insn->len); | |
| return insn->len; | |
| } | |
| /* | |
| * Set the disassembler buffer to read data from the section | |
| * containing the instruction to disassemble. | |
| */ | |
| dinfo->buffer = insn->sec->data->d_buf; | |
| dinfo->buffer_vma = 0; | |
| dinfo->buffer_length = sec_size(insn->sec); | |
| return disasm(insn->offset, &dctx->info); | |
| } | |
| size_t disas_insn(struct disas_context *dctx, struct instruction *insn) | |
| { | |
| return disas_insn_common(dctx, insn, false); | |
| } | |
| static size_t disas_insn_alt(struct disas_context *dctx, | |
| struct instruction *insn) | |
| { | |
| return disas_insn_common(dctx, insn, true); | |
| } | |
| static struct instruction *next_insn_same_alt(struct objtool_file *file, | |
| struct alt_group *alt_grp, | |
| struct instruction *insn) | |
| { | |
| if (alt_grp->last_insn == insn || alt_grp->nop == insn) | |
| return NULL; | |
| return next_insn_same_sec(file, insn); | |
| } | |
| #define alt_for_each_insn(file, alt_grp, insn) \ | |
| for (insn = alt_grp->first_insn; \ | |
| insn; \ | |
| insn = next_insn_same_alt(file, alt_grp, insn)) | |
| /* | |
| * Provide a name for the type of alternatives present at the | |
| * specified instruction. | |
| * | |
| * An instruction can have alternatives with different types, for | |
| * example alternative instructions and an exception table. In that | |
| * case the name for the alternative instructions type is used. | |
| * | |
| * Return NULL if the instruction as no alternative. | |
| */ | |
| const char *disas_alt_type_name(struct instruction *insn) | |
| { | |
| struct alternative *alt; | |
| const char *name; | |
| name = NULL; | |
| for (alt = insn->alts; alt; alt = alt->next) { | |
| if (alt->type == ALT_TYPE_INSTRUCTIONS) { | |
| name = "alternative"; | |
| break; | |
| } | |
| switch (alt->type) { | |
| case ALT_TYPE_EX_TABLE: | |
| name = "ex_table"; | |
| break; | |
| case ALT_TYPE_JUMP_TABLE: | |
| name = "jump_table"; | |
| break; | |
| default: | |
| name = "unknown"; | |
| break; | |
| } | |
| } | |
| return name; | |
| } | |
| /* | |
| * Provide a name for an alternative. | |
| */ | |
| char *disas_alt_name(struct alternative *alt) | |
| { | |
| char pfx[4] = { 0 }; | |
| char *str = NULL; | |
| const char *name; | |
| int feature; | |
| int flags; | |
| int num; | |
| switch (alt->type) { | |
| case ALT_TYPE_EX_TABLE: | |
| str = strdup("EXCEPTION"); | |
| break; | |
| case ALT_TYPE_JUMP_TABLE: | |
| str = strdup("JUMP"); | |
| break; | |
| case ALT_TYPE_INSTRUCTIONS: | |
| /* | |
| * This is a non-default group alternative. Create a name | |
| * based on the feature and flags associated with this | |
| * alternative. Use either the feature name (it is available) | |
| * or the feature number. And add a prefix to show the flags | |
| * used. | |
| * | |
| * Prefix flags characters: | |
| * | |
| * '!' alternative used when feature not enabled | |
| * '+' direct call alternative | |
| * '?' unknown flag | |
| */ | |
| if (!alt->insn->alt_group) | |
| return NULL; | |
| feature = alt->insn->alt_group->feature; | |
| num = alt_feature(feature); | |
| flags = alt_flags(feature); | |
| str = pfx; | |
| if (flags & ~(ALT_FLAG_NOT | ALT_FLAG_DIRECT_CALL)) | |
| *str++ = '?'; | |
| if (flags & ALT_FLAG_DIRECT_CALL) | |
| *str++ = '+'; | |
| if (flags & ALT_FLAG_NOT) | |
| *str++ = '!'; | |
| name = arch_cpu_feature_name(num); | |
| if (!name) | |
| str = strfmt("%sFEATURE 0x%X", pfx, num); | |
| else | |
| str = strfmt("%s%s", pfx, name); | |
| break; | |
| } | |
| return str; | |
| } | |
| /* | |
| * Initialize an alternative. The default alternative should be initialized | |
| * with alt=NULL. | |
| */ | |
| static int disas_alt_init(struct disas_alt *dalt, | |
| struct instruction *orig_insn, | |
| struct alternative *alt) | |
| { | |
| dalt->orig_insn = orig_insn; | |
| dalt->alt = alt; | |
| dalt->insn_idx = 0; | |
| dalt->name = alt ? disas_alt_name(alt) : strdup("DEFAULT"); | |
| if (!dalt->name) | |
| return -1; | |
| dalt->width = strlen(dalt->name); | |
| return 0; | |
| } | |
| static int disas_alt_add_insn(struct disas_alt *dalt, int index, char *insn_str, | |
| int offset, int nops) | |
| { | |
| int len; | |
| if (index >= DISAS_ALT_INSN_MAX) { | |
| WARN("Alternative %lx.%s has more instructions than supported", | |
| DALT_ALTID(dalt), dalt->name); | |
| return -1; | |
| } | |
| len = strlen(insn_str); | |
| dalt->insn[index].str = insn_str; | |
| dalt->insn[index].offset = offset; | |
| dalt->insn[index].nops = nops; | |
| if (len > dalt->width) | |
| dalt->width = len; | |
| return 0; | |
| } | |
| static int disas_alt_jump(struct disas_alt *dalt) | |
| { | |
| struct instruction *orig_insn; | |
| struct instruction *dest_insn; | |
| char suffix[2] = { 0 }; | |
| char *str; | |
| int nops; | |
| orig_insn = dalt->orig_insn; | |
| dest_insn = dalt->alt->insn; | |
| if (orig_insn->type == INSN_NOP) { | |
| if (orig_insn->len == 5) | |
| suffix[0] = 'q'; | |
| str = strfmt("jmp%-3s %lx <%s+0x%lx>", suffix, | |
| dest_insn->offset, insn_sym(dest_insn)->name, | |
| dest_insn->offset - insn_sym(dest_insn)->offset); | |
| nops = 0; | |
| } else { | |
| str = strfmt("nop%d", orig_insn->len); | |
| nops = orig_insn->len; | |
| } | |
| if (!str) | |
| return -1; | |
| disas_alt_add_insn(dalt, 0, str, 0, nops); | |
| return 1; | |
| } | |
| /* | |
| * Disassemble an exception table alternative. | |
| */ | |
| static int disas_alt_extable(struct disas_alt *dalt) | |
| { | |
| struct instruction *alt_insn; | |
| char *str; | |
| alt_insn = dalt->alt->insn; | |
| str = strfmt("resume at 0x%lx <%s+0x%lx>", | |
| alt_insn->offset, insn_sym(alt_insn)->name, | |
| alt_insn->offset - insn_sym(alt_insn)->offset); | |
| if (!str) | |
| return -1; | |
| disas_alt_add_insn(dalt, 0, str, 0, 0); | |
| return 1; | |
| } | |
| /* | |
| * Disassemble an alternative and store instructions in the disas_alt | |
| * structure. Return the number of instructions in the alternative. | |
| */ | |
| static int disas_alt_group(struct disas_context *dctx, struct disas_alt *dalt) | |
| { | |
| struct objtool_file *file; | |
| struct instruction *insn; | |
| int offset; | |
| char *str; | |
| int count; | |
| int nops; | |
| int err; | |
| file = dctx->file; | |
| count = 0; | |
| offset = 0; | |
| nops = 0; | |
| alt_for_each_insn(file, DALT_GROUP(dalt), insn) { | |
| disas_insn_alt(dctx, insn); | |
| str = strdup(disas_result(dctx)); | |
| if (!str) | |
| return -1; | |
| nops = insn->type == INSN_NOP ? insn->len : 0; | |
| err = disas_alt_add_insn(dalt, count, str, offset, nops); | |
| if (err) | |
| break; | |
| offset += insn->len; | |
| count++; | |
| } | |
| return count; | |
| } | |
| /* | |
| * Disassemble the default alternative. | |
| */ | |
| static int disas_alt_default(struct disas_context *dctx, struct disas_alt *dalt) | |
| { | |
| char *str; | |
| int nops; | |
| int err; | |
| if (DALT_GROUP(dalt)) | |
| return disas_alt_group(dctx, dalt); | |
| /* | |
| * Default alternative with no alt_group: this is the default | |
| * code associated with either a jump table or an exception | |
| * table and no other instruction alternatives. In that case | |
| * the default alternative is made of a single instruction. | |
| */ | |
| disas_insn(dctx, dalt->orig_insn); | |
| str = strdup(disas_result(dctx)); | |
| if (!str) | |
| return -1; | |
| nops = dalt->orig_insn->type == INSN_NOP ? dalt->orig_insn->len : 0; | |
| err = disas_alt_add_insn(dalt, 0, str, 0, nops); | |
| if (err) | |
| return -1; | |
| return 1; | |
| } | |
| /* | |
| * For each alternative, if there is an instruction at the specified | |
| * offset then print this instruction, otherwise print a blank entry. | |
| * The offset is an offset from the start of the alternative. | |
| * | |
| * Return the offset for the next instructions to print, or -1 if all | |
| * instructions have been printed. | |
| */ | |
| static int disas_alt_print_insn(struct disas_alt *dalts, int alt_count, | |
| int insn_count, int offset) | |
| { | |
| struct disas_alt *dalt; | |
| int offset_next; | |
| char *str; | |
| int i, j; | |
| offset_next = -1; | |
| for (i = 0; i < alt_count; i++) { | |
| dalt = &dalts[i]; | |
| j = dalt->insn_idx; | |
| if (j == -1) { | |
| printf("| %-*s ", dalt->width, ""); | |
| continue; | |
| } | |
| if (dalt->insn[j].offset == offset) { | |
| str = dalt->insn[j].str; | |
| printf("| %-*s ", dalt->width, str ?: ""); | |
| if (++j < insn_count) { | |
| dalt->insn_idx = j; | |
| } else { | |
| dalt->insn_idx = -1; | |
| continue; | |
| } | |
| } else { | |
| printf("| %-*s ", dalt->width, ""); | |
| } | |
| if (dalt->insn[j].offset > 0 && | |
| (offset_next == -1 || | |
| (dalt->insn[j].offset < offset_next))) | |
| offset_next = dalt->insn[j].offset; | |
| } | |
| printf("\n"); | |
| return offset_next; | |
| } | |
| /* | |
| * Print all alternatives side-by-side. | |
| */ | |
| static void disas_alt_print_wide(char *alt_name, struct disas_alt *dalts, int alt_count, | |
| int insn_count) | |
| { | |
| struct instruction *orig_insn; | |
| int offset_next; | |
| int offset; | |
| int i; | |
| orig_insn = dalts[0].orig_insn; | |
| /* | |
| * Print an header with the name of each alternative. | |
| */ | |
| disas_print_info(stdout, orig_insn, -2, NULL); | |
| if (strlen(alt_name) > dalts[0].width) | |
| dalts[0].width = strlen(alt_name); | |
| printf("| %-*s ", dalts[0].width, alt_name); | |
| for (i = 1; i < alt_count; i++) | |
| printf("| %-*s ", dalts[i].width, dalts[i].name); | |
| printf("\n"); | |
| /* | |
| * Print instructions for each alternative. | |
| */ | |
| offset_next = 0; | |
| do { | |
| offset = offset_next; | |
| disas_print(stdout, orig_insn->sec, orig_insn->offset + offset, | |
| -2, NULL); | |
| offset_next = disas_alt_print_insn(dalts, alt_count, insn_count, | |
| offset); | |
| } while (offset_next > offset); | |
| } | |
| /* | |
| * Print all alternatives one above the other. | |
| */ | |
| static void disas_alt_print_compact(char *alt_name, struct disas_alt *dalts, | |
| int alt_count, int insn_count) | |
| { | |
| struct instruction *orig_insn; | |
| int width; | |
| int i, j; | |
| int len; | |
| orig_insn = dalts[0].orig_insn; | |
| len = disas_print(stdout, orig_insn->sec, orig_insn->offset, 0, NULL); | |
| printf("%s\n", alt_name); | |
| /* | |
| * If all alternatives have a single instruction then print each | |
| * alternative on a single line. Otherwise, print alternatives | |
| * one above the other with a clear separation. | |
| */ | |
| if (insn_count == 1) { | |
| width = 0; | |
| for (i = 0; i < alt_count; i++) { | |
| if (dalts[i].width > width) | |
| width = dalts[i].width; | |
| } | |
| for (i = 0; i < alt_count; i++) { | |
| printf("%*s= %-*s (if %s)\n", len, "", width, | |
| dalts[i].insn[0].str, dalts[i].name); | |
| } | |
| return; | |
| } | |
| for (i = 0; i < alt_count; i++) { | |
| printf("%*s= %s\n", len, "", dalts[i].name); | |
| for (j = 0; j < insn_count; j++) { | |
| if (!dalts[i].insn[j].str) | |
| break; | |
| disas_print(stdout, orig_insn->sec, | |
| orig_insn->offset + dalts[i].insn[j].offset, 0, | |
| "| %s\n", dalts[i].insn[j].str); | |
| } | |
| printf("%*s|\n", len, ""); | |
| } | |
| } | |
| /* | |
| * Trim NOPs in alternatives. This replaces trailing NOPs in alternatives | |
| * with a single indication of the number of bytes covered with NOPs. | |
| * | |
| * Return the maximum numbers of instructions in all alternatives after | |
| * trailing NOPs have been trimmed. | |
| */ | |
| static int disas_alt_trim_nops(struct disas_alt *dalts, int alt_count, | |
| int insn_count) | |
| { | |
| struct disas_alt *dalt; | |
| int nops_count; | |
| const char *s; | |
| int offset; | |
| int count; | |
| int nops; | |
| int i, j; | |
| count = 0; | |
| for (i = 0; i < alt_count; i++) { | |
| offset = 0; | |
| nops = 0; | |
| nops_count = 0; | |
| dalt = &dalts[i]; | |
| for (j = insn_count - 1; j >= 0; j--) { | |
| if (!dalt->insn[j].str || !dalt->insn[j].nops) | |
| break; | |
| offset = dalt->insn[j].offset; | |
| free(dalt->insn[j].str); | |
| dalt->insn[j].offset = 0; | |
| dalt->insn[j].str = NULL; | |
| nops += dalt->insn[j].nops; | |
| nops_count++; | |
| } | |
| /* | |
| * All trailing NOPs have been removed. If there was a single | |
| * NOP instruction then re-add it. If there was a block of | |
| * NOPs then indicate the number of bytes than the block | |
| * covers (nop*<number-of-bytes>). | |
| */ | |
| if (nops_count) { | |
| s = nops_count == 1 ? "" : "*"; | |
| dalt->insn[j + 1].str = strfmt("nop%s%d", s, nops); | |
| dalt->insn[j + 1].offset = offset; | |
| dalt->insn[j + 1].nops = nops; | |
| j++; | |
| } | |
| if (j > count) | |
| count = j; | |
| } | |
| return count + 1; | |
| } | |
| /* | |
| * Disassemble an alternative. | |
| * | |
| * Return the last instruction in the default alternative so that | |
| * disassembly can continue with the next instruction. Return NULL | |
| * on error. | |
| */ | |
| static void *disas_alt(struct disas_context *dctx, | |
| struct instruction *orig_insn) | |
| { | |
| struct disas_alt dalts[DISAS_ALT_MAX] = { 0 }; | |
| struct instruction *last_insn = NULL; | |
| struct alternative *alt; | |
| struct disas_alt *dalt; | |
| int insn_count = 0; | |
| int alt_count = 0; | |
| char *alt_name; | |
| int count; | |
| int i, j; | |
| int err; | |
| alt_name = strfmt("<%s.%lx>", disas_alt_type_name(orig_insn), | |
| orig_insn->offset); | |
| if (!alt_name) { | |
| WARN("Failed to define name for alternative at instruction 0x%lx", | |
| orig_insn->offset); | |
| goto done; | |
| } | |
| /* | |
| * Initialize and disassemble the default alternative. | |
| */ | |
| err = disas_alt_init(&dalts[0], orig_insn, NULL); | |
| if (err) { | |
| WARN("%s: failed to initialize default alternative", alt_name); | |
| goto done; | |
| } | |
| insn_count = disas_alt_default(dctx, &dalts[0]); | |
| if (insn_count < 0) { | |
| WARN("%s: failed to disassemble default alternative", alt_name); | |
| goto done; | |
| } | |
| /* | |
| * Initialize and disassemble all other alternatives. | |
| */ | |
| i = 1; | |
| for (alt = orig_insn->alts; alt; alt = alt->next) { | |
| if (i >= DISAS_ALT_MAX) { | |
| WARN("%s has more alternatives than supported", alt_name); | |
| break; | |
| } | |
| dalt = &dalts[i]; | |
| err = disas_alt_init(dalt, orig_insn, alt); | |
| if (err) { | |
| WARN("%s: failed to disassemble alternative", alt_name); | |
| goto done; | |
| } | |
| count = -1; | |
| switch (dalt->alt->type) { | |
| case ALT_TYPE_INSTRUCTIONS: | |
| count = disas_alt_group(dctx, dalt); | |
| break; | |
| case ALT_TYPE_EX_TABLE: | |
| count = disas_alt_extable(dalt); | |
| break; | |
| case ALT_TYPE_JUMP_TABLE: | |
| count = disas_alt_jump(dalt); | |
| break; | |
| } | |
| if (count < 0) { | |
| WARN("%s: failed to disassemble alternative %s", | |
| alt_name, dalt->name); | |
| goto done; | |
| } | |
| insn_count = count > insn_count ? count : insn_count; | |
| i++; | |
| } | |
| alt_count = i; | |
| /* | |
| * Print default and non-default alternatives. | |
| */ | |
| insn_count = disas_alt_trim_nops(dalts, alt_count, insn_count); | |
| if (opts.wide) | |
| disas_alt_print_wide(alt_name, dalts, alt_count, insn_count); | |
| else | |
| disas_alt_print_compact(alt_name, dalts, alt_count, insn_count); | |
| last_insn = orig_insn->alt_group ? orig_insn->alt_group->last_insn : | |
| orig_insn; | |
| done: | |
| for (i = 0; i < alt_count; i++) { | |
| free(dalts[i].name); | |
| for (j = 0; j < insn_count; j++) | |
| free(dalts[i].insn[j].str); | |
| } | |
| free(alt_name); | |
| return last_insn; | |
| } | |
| /* | |
| * Disassemble a function. | |
| */ | |
| static void disas_func(struct disas_context *dctx, struct symbol *func) | |
| { | |
| struct instruction *insn_start; | |
| struct instruction *insn; | |
| printf("%s:\n", func->name); | |
| sym_for_each_insn(dctx->file, func, insn) { | |
| if (insn->alts) { | |
| insn_start = insn; | |
| insn = disas_alt(dctx, insn); | |
| if (insn) | |
| continue; | |
| /* | |
| * There was an error with disassembling | |
| * the alternative. Resume disassembling | |
| * at the current instruction, this will | |
| * disassemble the default alternative | |
| * only and continue with the code after | |
| * the alternative. | |
| */ | |
| insn = insn_start; | |
| } | |
| DISAS_PRINSN(dctx, insn, 0); | |
| } | |
| printf("\n"); | |
| } | |
| /* | |
| * Disassemble all warned functions. | |
| */ | |
| void disas_warned_funcs(struct disas_context *dctx) | |
| { | |
| struct symbol *sym; | |
| if (!dctx) | |
| return; | |
| for_each_sym(dctx->file->elf, sym) { | |
| if (sym->warned) | |
| disas_func(dctx, sym); | |
| } | |
| } | |
| void disas_funcs(struct disas_context *dctx) | |
| { | |
| bool disas_all = !strcmp(opts.disas, "*"); | |
| struct section *sec; | |
| struct symbol *sym; | |
| for_each_sec(dctx->file->elf, sec) { | |
| if (!is_text_sec(sec)) | |
| continue; | |
| sec_for_each_sym(sec, sym) { | |
| /* | |
| * If the function had a warning and the verbose | |
| * option is used then the function was already | |
| * disassemble. | |
| */ | |
| if (opts.verbose && sym->warned) | |
| continue; | |
| if (disas_all || fnmatch(opts.disas, sym->name, 0) == 0) | |
| disas_func(dctx, sym); | |
| } | |
| } | |
| } |