root/sys/ddb/db_dwarf.c
/*      $OpenBSD: db_dwarf.c,v 1.7 2017/10/27 08:40:15 mpi 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/systm.h>
#include <machine/db_machdep.h>
#include <ddb/db_sym.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, last_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.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(void)
{
        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 */