diff options
author | Matthew Dempsky <matthew@cvs.openbsd.org> | 2014-07-11 03:17:21 +0000 |
---|---|---|
committer | Matthew Dempsky <matthew@cvs.openbsd.org> | 2014-07-11 03:17:21 +0000 |
commit | 6368818603865a9199bf2c7b4e9fec08b87b423a (patch) | |
tree | 5fab07b1309a02c2a0db0ebb37f8b61947e1b06c /sys/ddb/db_dwarf.c | |
parent | 1e40105f9d7186da8a1b4a40cfe605f6b31a33a1 (diff) |
Add basic DWARF line table decoder
Includes a stand-alone addr2line clone for userspace testing.
Tested extensively on amd64 and expected to eventually support other
architectures too. Importing now so further development/testing can
happen in-tree. Followup commits will add to the kernel build and
integrate into ddb.
positive feedback; no objections
Diffstat (limited to 'sys/ddb/db_dwarf.c')
-rw-r--r-- | sys/ddb/db_dwarf.c | 537 |
1 files changed, 537 insertions, 0 deletions
diff --git a/sys/ddb/db_dwarf.c b/sys/ddb/db_dwarf.c new file mode 100644 index 00000000000..4ae22ec820a --- /dev/null +++ b/sys/ddb/db_dwarf.c @@ -0,0 +1,537 @@ +/* $OpenBSD: db_dwarf.c,v 1.1 2014/07/11 03:17:20 matthew Exp $ */ +/* + * Copyright (c) 2014 Matthew Dempsky <matthew@dempsky.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifdef _KERNEL +#include <sys/param.h> +#include <sys/stdint.h> +#include <sys/systm.h> +#include <sys/types.h> +#include <ddb/db_dwarf.h> +#ifdef DIAGNOSTIC +#define DWARN(fmt, ...) printf("ddb: " fmt "\n", __VA_ARGS__) +#else +#define DWARN(fmt, ...) ((void)0) +#endif +#else /* _KERNEL */ +#include <err.h> +#include <stdbool.h> +#include <stdint.h> +#include <string.h> +#define DWARN warnx +#endif /* _KERNEL */ + +enum { + DW_LNS_copy = 1, + DW_LNS_advance_pc = 2, + DW_LNS_advance_line = 3, + DW_LNS_set_file = 4, + DW_LNS_set_column = 5, + DW_LNS_negate_stmt = 6, + DW_LNS_set_basic_block = 7, + DW_LNS_const_add_pc = 8, + DW_LNS_fixed_advance_pc = 9, + DW_LNS_set_prologue_end = 10, + DW_LNS_set_epilogue_begin = 11, +}; + +enum { + DW_LNE_end_sequence = 1, + DW_LNE_set_address = 2, + DW_LNE_define_file = 3, +}; + +struct dwbuf { + const char *buf; + size_t len; +}; + +static inline bool +read_bytes(struct dwbuf *d, void *v, size_t n) +{ + if (d->len < n) + return (false); + memcpy(v, d->buf, n); + d->buf += n; + d->len -= n; + return (true); +} + +static bool +read_s8(struct dwbuf *d, int8_t *v) +{ + return (read_bytes(d, v, sizeof(*v))); +} + +static bool +read_u8(struct dwbuf *d, uint8_t *v) +{ + return (read_bytes(d, v, sizeof(*v))); +} + +static bool +read_u16(struct dwbuf *d, uint16_t *v) +{ + return (read_bytes(d, v, sizeof(*v))); +} + +static bool +read_u32(struct dwbuf *d, uint32_t *v) +{ + return (read_bytes(d, v, sizeof(*v))); +} + +static bool +read_u64(struct dwbuf *d, uint64_t *v) +{ + return (read_bytes(d, v, sizeof(*v))); +} + +/* Read a DWARF LEB128 (little-endian base-128) value. */ +static bool +read_leb128(struct dwbuf *d, uint64_t *v, bool signextend) +{ + unsigned int shift = 0; + uint64_t res = 0; + uint8_t x; + while (shift < 64 && read_u8(d, &x)) { + res |= (uint64_t)(x & 0x7f) << shift; + shift += 7; + if ((x & 0x80) == 0) { + if (signextend && shift < 64 && (x & 0x40) != 0) + res |= ~(uint64_t)0 << shift; + *v = res; + return (true); + } + } + return (false); +} + +static bool +read_sleb128(struct dwbuf *d, int64_t *v) +{ + return (read_leb128(d, (uint64_t *)v, true)); +} + +static bool +read_uleb128(struct dwbuf *d, uint64_t *v) +{ + return (read_leb128(d, v, false)); +} + +/* Read a NUL terminated string. */ +static bool +read_string(struct dwbuf *d, const char **s) +{ + const char *end = memchr(d->buf, '\0', d->len); + if (end == NULL) + return (false); + size_t n = end - d->buf + 1; + *s = d->buf; + d->buf += n; + d->len -= n; + return (true); +} + +static bool +read_buf(struct dwbuf *d, struct dwbuf *v, size_t n) +{ + if (d->len < n) + return (false); + v->buf = d->buf; + v->len = n; + d->buf += n; + d->len -= n; + return (true); +} + +static bool +skip_bytes(struct dwbuf *d, size_t n) +{ + if (d->len < n) + return (false); + d->buf += n; + d->len -= n; + return (true); +} + +static bool +read_filename(struct dwbuf *names, const char **outdirname, + const char **outbasename, uint8_t opcode_base, uint64_t file) +{ + if (file == 0) + return (false); + + /* Skip over opcode table. */ + size_t i; + for (i = 1; i < opcode_base; i++) { + uint64_t dummy; + if (!read_uleb128(names, &dummy)) + return (false); + } + + /* Skip over directory name table for now. */ + struct dwbuf dirnames = *names; + for (;;) { + const char *name; + if (!read_string(names, &name)) + return (false); + if (*name == '\0') + break; + } + + /* Locate file entry. */ + const char *basename = NULL; + uint64_t dir = 0; + for (i = 0; i < file; i++) { + uint64_t mtime, size; + if (!read_string(names, &basename) || *basename == '\0' || + !read_uleb128(names, &dir) || + !read_uleb128(names, &mtime) || + !read_uleb128(names, &size)) + return (false); + } + + const char *dirname = NULL; + for (i = 0; i < dir; i++) { + if (!read_string(&dirnames, &dirname) || *dirname == '\0') + return (false); + } + + *outdirname = dirname; + *outbasename = basename; + return (true); +} + +bool +db_dwarf_line_at_pc(const char *linetab, size_t linetabsize, uintptr_t pc, + const char **outdirname, const char **outbasename, int *outline) +{ + struct dwbuf table = { .buf = linetab, .len = linetabsize }; + + /* + * For simplicity, we simply brute force search through the entire + * line table each time. + */ + uint32_t unitsize; + struct dwbuf unit; +next: + /* Line tables are a sequence of compilation unit entries. */ + if (!read_u32(&table, &unitsize) || unitsize >= 0xfffffff0 || + !read_buf(&table, &unit, unitsize)) + return (false); + + uint16_t version; + uint32_t header_size; + if (!read_u16(&unit, &version) || version > 2 || + !read_u32(&unit, &header_size)) + goto next; + + struct dwbuf headerstart = unit; + uint8_t min_insn_length, default_is_stmt, line_range, opcode_base; + int8_t line_base; + if (!read_u8(&unit, &min_insn_length) || + !read_u8(&unit, &default_is_stmt) || + !read_s8(&unit, &line_base) || + !read_u8(&unit, &line_range) || + !read_u8(&unit, &opcode_base)) + goto next; + + /* + * Directory and file names are next in the header, but for now we + * skip directly to the line number program. + */ + struct dwbuf names = unit; + unit = headerstart; + if (!skip_bytes(&unit, header_size)) + return (false); + + /* VM registers. */ + uint64_t address = 0, file = 1, line = 1, column = 0; + uint8_t is_stmt = default_is_stmt; + bool basic_block = false, end_sequence = false; + bool prologue_end = false, epilogue_begin = false; + + /* Last line table entry emitted, if any. */ + bool have_last = false; + uint64_t last_line = 0, last_file = 0; + + /* Time to run the line program. */ + uint8_t opcode; + while (read_u8(&unit, &opcode)) { + bool emit = false, reset_basic_block = false; + + if (opcode >= opcode_base) { + /* "Special" opcodes. */ + uint8_t diff = opcode - opcode_base; + address += diff / line_range; + line += line_base + diff % line_range; + emit = true; + } else if (opcode == 0) { + /* "Extended" opcodes. */ + uint64_t extsize; + struct dwbuf extra; + if (!read_uleb128(&unit, &extsize) || + !read_buf(&unit, &extra, extsize) || + !read_u8(&extra, &opcode)) + goto next; + switch (opcode) { + case DW_LNE_end_sequence: + emit = true; + end_sequence = true; + break; + case DW_LNE_set_address: + switch (extra.len) { + case 4: { + uint32_t address32; + if (!read_u32(&extra, &address32)) + goto next; + address = address32; + break; + } + case 8: + if (!read_u64(&extra, &address)) + goto next; + break; + default: + DWARN("unexpected address length: %zu", + extra.len); + goto next; + } + break; + case DW_LNE_define_file: + /* XXX: hope this isn't needed */ + default: + DWARN("unknown extended opcode: %d", opcode); + goto next; + } + } else { + /* "Standard" opcodes. */ + switch (opcode) { + case DW_LNS_copy: + emit = true; + reset_basic_block = true; + break; + case DW_LNS_advance_pc: { + uint64_t delta; + if (!read_uleb128(&unit, &delta)) + goto next; + address += delta * min_insn_length; + break; + } + case DW_LNS_advance_line: { + int64_t delta; + if (!read_sleb128(&unit, &delta)) + goto next; + line += delta; + break; + } + case DW_LNS_set_file: + if (!read_uleb128(&unit, &file)) + goto next; + break; + case DW_LNS_set_column: + if (!read_uleb128(&unit, &column)) + goto next; + break; + case DW_LNS_negate_stmt: + is_stmt = !is_stmt; + break; + case DW_LNS_set_basic_block: + basic_block = true; + break; + case DW_LNS_const_add_pc: + address += (255 - opcode_base) / line_range; + break; + case DW_LNS_set_prologue_end: + prologue_end = true; + break; + case DW_LNS_set_epilogue_begin: + epilogue_begin = true; + break; + default: + DWARN("unknown standard opcode: %d", opcode); + goto next; + } + } + + if (emit) { + if (address > pc) { + /* Found an entry after our target PC. */ + if (!have_last) { + /* Give up on this program. */ + break; + } + /* Return the last entry. */ + *outline = last_line; + return (read_filename(&names, outdirname, + outbasename, opcode_base, file)); + } + + last_file = file; + last_line = line; + have_last = true; + } + + if (reset_basic_block) + basic_block = false; + } + + goto next; +} + +#ifndef _KERNEL +#include <sys/endian.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <elf_abi.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#ifndef ELFDATA +#if BYTE_ORDER == LITTLE_ENDIAN +#define ELFDATA ELFDATA2LSB +#elif BYTE_ORDER == BIG_ENDIAN +#define ELFDATA ELFDATA2MSB +#else +#error Unsupported byte order +#endif +#endif /* !ELFDATA */ + +static void +usage() +{ + extern const char *__progname; + errx(1, "usage: %s [-s] [-e filename] [addr addr ...]", __progname); +} + +/* + * Basic addr2line clone for stand-alone testing. + */ +int +main(int argc, char *argv[]) +{ + const char *filename = "a.out"; + + int ch; + bool showdir = true; + while ((ch = getopt(argc, argv, "e:s")) != EOF) { + switch (ch) { + case 'e': + filename = optarg; + break; + case 's': + showdir = false; + break; + default: + usage(); + } + } + + argc -= optind; + argv += optind; + + /* Start by mapping the full file into memory. */ + int fd = open(filename, O_RDONLY); + if (fd == -1) + err(1, "open"); + + struct stat st; + if (fstat(fd, &st) == -1) + err(1, "fstat"); + if (st.st_size < (off_t)sizeof(Elf_Ehdr)) + errx(1, "file too small to be ELF"); + if ((uintmax_t)st.st_size > SIZE_MAX) + errx(1, "file too big to fit memory"); + size_t filesize = st.st_size; + + const char *p = mmap(NULL, filesize, PROT_READ, MAP_SHARED, fd, 0); + if (p == MAP_FAILED) + err(1, "mmap"); + + close(fd); + + /* Read and validate ELF header. */ + Elf_Ehdr ehdr; + memcpy(&ehdr, p, sizeof(ehdr)); + if (!IS_ELF(ehdr)) + errx(1, "file is not ELF"); + if (ehdr.e_ident[EI_CLASS] != ELFCLASS) + errx(1, "unexpected word size"); + if (ehdr.e_ident[EI_DATA] != ELFDATA) + errx(1, "unexpected data format"); + if (ehdr.e_shoff > filesize) + errx(1, "bogus section table offset"); + if (ehdr.e_shentsize < sizeof(Elf_Shdr)) + errx(1, "unexpected section header size"); + if (ehdr.e_shnum > (filesize - ehdr.e_shoff) / ehdr.e_shentsize) + errx(1, "bogus section header count"); + if (ehdr.e_shstrndx >= ehdr.e_shnum) + errx(1, "bogus string table index"); + + /* Find section header string table location and size. */ + Elf_Shdr shdr; + memcpy(&shdr, p + ehdr.e_shoff + ehdr.e_shstrndx * ehdr.e_shentsize, + sizeof(shdr)); + if (shdr.sh_type != SHT_STRTAB) + errx(1, "unexpected string table type"); + if (shdr.sh_offset > filesize) + errx(1, "bogus string table offset"); + if (shdr.sh_size > filesize - shdr.sh_offset) + errx(1, "bogus string table size"); + const char *shstrtab = p + shdr.sh_offset; + size_t shstrtabsize = shdr.sh_size; + + /* Search through section table for .debug_line section. */ + size_t i; + for (i = 0; i < ehdr.e_shnum; i++) { + memcpy(&shdr, p + ehdr.e_shoff + i * ehdr.e_shentsize, + sizeof(shdr)); + if (0 == strncmp(".debug_line", shstrtab + shdr.sh_name, + shstrtabsize - shdr.sh_name)) + break; + } + if (i == ehdr.e_shnum) + errx(1, "no DWARF line number table found"); + if (shdr.sh_offset > filesize) + errx(1, "bogus line table offset"); + if (shdr.sh_size > filesize - shdr.sh_offset) + errx(1, "bogus line table size"); + const char *linetab = p + shdr.sh_offset; + size_t linetabsize = shdr.sh_size; + + const char *addrstr; + while ((addrstr = *argv++) != NULL) { + unsigned long addr = strtoul(addrstr, NULL, 16); + + const char *dir, *file; + int line; + if (!db_dwarf_line_at_pc(linetab, linetabsize, addr, + &dir, &file, &line)) { + dir = NULL; + file = "??"; + line = 0; + } + if (showdir && dir != NULL) + printf("%s/", dir); + printf("%s:%d\n", file, line); + } + + return (0); +} +#endif /* !_KERNEL */ |