/*****************************************************************************
 * stacktrace.c
 *
 * This file implements a simple stacktrace library.
 *
 * ---------------------------------------------------------------------------
 * stacktrace - Stacktrace printer.
 *   (C) 2008-2010 Gerardo García Peña <gerardo@kung-foo.net>
 *
 *   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, write to the Free Software Foundation, Inc., 51
 *   Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *****************************************************************************/

#include <elf.h>
#include <errno.h>
#include <execinfo.h>
#include <fcntl.h>
#include <link.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

#define __STACKTRACE_MSG_MACROS__
#include "stacktrace.h"

/* FIXME - ugly hack for sunos */
#if __sun
#if defined(__amd64__) || defined(__x86_64__)
#define ElfW(x)            Elf64_##x
#else
#define ElfW(x)            Elf32_##x
#endif
typedef ElfW(Half)   Elf_Symndx;
#endif

static int binfile = -1;
static FILE *stfd = NULL;
static stack_t ss;

/*****************************************************************************
 * INTERNAL TYPES
 *****************************************************************************/

struct _tag_stacktrace_info {
  int              ptok;
  struct link_map *lm;
  ElfW(Addr)       stringTable;
  ElfW(Word)       stringTableSize;
  ElfW(Addr)       symbolTable;
  ElfW(Word)       symbolSize;
  ElfW(Addr)       hashTable;

  Elf_Symndx      *hashTableData;
  Elf_Symndx       hashTableSize;
} stacktrace_info;

struct elf_info {
  void       *base;
  off_t       size;
  int         nsyms;
  ElfW(Shdr) *symtab;
  ElfW(Shdr) *shstrtab;
  ElfW(Shdr) *strtab;
};

/*****************************************************************************
 * PROTOTYPES
 *****************************************************************************/

/* Initialization and finalization */
static void stacktrace_sigaction(int s, siginfo_t *i, void *c);

/* ELF manipulation */
static void elf_close(struct elf_info *ei);
static char *elf_shname(struct elf_info *ei, ElfW(Shdr) *shdr);
static void elf_open(int f, struct elf_info *ei);
static int elf_chekaddr(struct elf_info *ei, void *addr);
static char *elf_symname(struct elf_info *ei, ElfW(Sym) *sym);
static ElfW(Sym) *elf_resolve(struct elf_info *ei, void *addr);

/*****************************************************************************
 * LOGGING
 *****************************************************************************/

void stacktrace_set_file(FILE *file)
{
  stfd = file;
}

void stacktrace_msg(char *p, char *f, ...)
{
  va_list args;

  if(stfd == NULL)
    stfd = stderr;

  va_start(args, f);
  fputs(p, stfd);
  vfprintf(stfd, f, args);
  fputc('\n', stfd);
  va_end(args);
}

void stacktrace_cat(char *p, char *path)
{
  int f, c, r, i;
  char buff[255];

  if((f = open(path, O_RDONLY)) < 0)
  {
    ERR_ERRNO("%s'", path);
    return;
  }
  i = 0;
  while((r = read(f, buff + i, 1)) > 0)
  {
    c = buff[i];
    if(c == '\n' || i >= sizeof(buff)-4)
    {
      if(c != '\n') i++;
      buff[i] = '\0';
      if(c != '\n') strcpy(buff + i, "...");
      MSG("%s%s: %s", p, path, buff);
      if(c != '\n')
      {
        strcpy(buff, "  ... ");
        i = strlen(buff);
      } else
        i = 0;
    } else
      i++;
  }
  if(r < 0)
    ERR_ERRNO("reading %s", path);
  if(i > 0)
  {
    buff[i] = '\0';
    MSG("%s%s: %s", p, path, buff);
  }
  if(close(f) < 0)
    ERR_ERRNO("closing %s'", path);
}

/*****************************************************************************
 * ELF MANIPULATION
 *****************************************************************************/

static void elf_close(struct elf_info *ei)
{
  if(!ei->base)
    return;
  if(munmap(ei->base, ei->size) < 0)
    ERR_ERRNO("Cannot munmap() binfile");
  ei->base = NULL;
  ei->size = 0;
}

static char *elf_shname(struct elf_info *ei, ElfW(Shdr) *shdr)
{
  if(!ei->base)
    return NULL;
  return ei->base + ei->shstrtab->sh_offset + shdr->sh_name;
}

static void elf_open(int f, struct elf_info *ei)
{
  ElfW(Ehdr) *ehdr;
  ElfW(Shdr) *shdr;
  struct stat stbuf;
  int i;
  char *shname;

  /* map binary */
  if(fstat(binfile, &stbuf) < 0)
    ERR_ERRNO("Cannot stat binfile");
  ei->size = stbuf.st_size;
  if((ei->base = mmap(NULL, ei->size, PROT_READ, MAP_PRIVATE, f, 0)) < 0)
    ERR_ERRNO("Cannot mmap binfile");

  /* get elf header */
  ehdr = ei->base;

  /* get section header string table */
  if(ehdr->e_shstrndx != SHN_UNDEF
  && ehdr->e_shstrndx < ehdr->e_shnum)
  {
    ei->shstrtab = ei->base + ehdr->e_shoff + ehdr->e_shstrndx * ehdr->e_shentsize;
  } else {
    ERR("This file has not a section header string table :(");
    elf_close(ei);
    return;
  }

  /* parse header and search sections */
  for(i = 0; i < ehdr->e_shnum; i++)
  {
    shdr = ei->base + ehdr->e_shoff + i * ehdr->e_shentsize;
    if((void *) shdr >= ei->base + ei->size)
    {
      ERR("Invalid ELF binfile (shdr out of range).");
      elf_close(ei);
      return;
    }
    shname = elf_shname(ei, shdr);
    if(!strcmp(shname, ".symtab")) ei->symtab = shdr; else
    if(!strcmp(shname, ".strtab")) ei->strtab = shdr;
  }
  if(!ei->symtab || !ei->strtab)
  {
    ERR("No symbol or string table present on main :(");
    elf_close(ei);
    return;
  }
  ei->nsyms = ei->symtab->sh_size / ei->symtab->sh_entsize;
  if(ei->symtab->sh_size > (ei->nsyms * ei->symtab->sh_entsize))
    WRN("symtab is not multiple of entsize(%d)", ei->symtab->sh_entsize);
}

static int elf_chekaddr(struct elf_info *ei, void *addr)
{
  ElfW(Ehdr) *ehdr;
  ElfW(Shdr) *shdr;
  int i;

  if(!ei->base)
    return 0;

  ehdr = ei->base;
  for(i = 0; i < ehdr->e_shnum; i++)
  {
    shdr = ei->base + ehdr->e_shoff + i * ehdr->e_shentsize;
    if((void *) shdr >= ei->base + ei->size)
    {
      ERR("Invalid ELF binfile (shdr out of range).");
      return 0;
    }
    if(shdr->sh_type == SHT_PROGBITS
    && (ElfW(Addr)) addr >= shdr->sh_addr
    && (ElfW(Addr)) addr <= shdr->sh_addr + shdr->sh_size)
      return -1;
  }
  return 0;
}

static char *elf_symname(struct elf_info *ei, ElfW(Sym) *sym)
{
  int i;

  if(!ei->base)
    return NULL;

  if(sym->st_name >= ei->strtab->sh_size)
  {
    i = ((unsigned) sym - (unsigned) ei->base - ei->symtab->sh_offset) / ei->symtab->sh_entsize;
    i = 0;
    ERR("Symbol %d (binfile offset 0x%08x), name outside of string table (st_name = 0x%08x).",
        i, ei->symtab->sh_offset + ei->symtab->sh_entsize * i,
        sym->st_name);
    return NULL;
  }
  return !sym->st_name
           ? "[no-name]"
           : ei->base + ei->strtab->sh_offset + sym->st_name;
}

static ElfW(Sym) *elf_resolve(struct elf_info *ei, void *addr)
{
  int i;
  ElfW(Sym) *sym, *minsym = NULL;

  if(!ei->base || !elf_chekaddr(ei, addr))
    return NULL;

  for(i = 0; i < ei->nsyms; i++)
  {
    sym = ei->base + ei->symtab->sh_offset + ei->symtab->sh_entsize * i;
    if(minsym == NULL
    || (minsym->st_value < sym->st_value && sym->st_value < (ElfW(Addr)) addr))
      minsym = sym;
    if((void *) sym->st_value == addr)
      return sym;
  }

  return minsym;
}

/*****************************************************************************
 * STACKTRACE PRINTING
 *****************************************************************************/

void stacktrace(void)
{
  void *stt[255];
  char **syt, *name;
  size_t s, i;
  struct elf_info ei;
  ElfW(Sym) *sym;
  char buff[512];

  /* dump process' /proc data */
  MSG("status table:");
  stacktrace_cat("  ", "/proc/self/status");

  MSG("limits table:");
  stacktrace_cat("  ", "/proc/self/limits");

  MSG("mmap table:");
  stacktrace_cat("  ", "/proc/self/maps");

  /* get void*'s for all entries on the stack */
  s = backtrace(stt, sizeof(stt)/sizeof(void *));
  syt = backtrace_symbols(stt, s);

  /* open elf file (if system supports elf bin format) */
  elf_open(binfile, &ei);

  /* print out all the frames to stderr */
  MSG("stacktrace: (size %d):", s);
  for(i = 0; i < s; i++)
  {
    if((sym = elf_resolve(&ei, stt[i])) != NULL)
    {
      snprintf(buff, sizeof(buff), "MAIN(%s+%x) %s",
                 elf_symname(&ei, sym),
                 ((unsigned) stt[i]) - sym->st_value,
                 /* ((unsigned) stt[i])); */
                 syt[i]);
      name = buff;
    } else
      name = syt[i];
    MSG("  %s", name);
  }
  MSG("stacktrace: << end of stacktrace >>");

  /* close elf file */
  elf_close(&ei);

  free(syt);
}

/*****************************************************************************
 * INITIALIZATION AND FINALIZATION
 *****************************************************************************/

static void stacktrace_sigaction(int s, siginfo_t *i, void *c)
{
  MSG("Received signal %d - printing stacktrace.", s);

  /* dump signal related info */
  switch(s)
  {
    case SIGILL:
    case SIGFPE:
    case SIGSEGV:
    case SIGBUS:
      MSG("Faulty instruction at 0x%08x", i->si_addr);
      break;
  }

  /* dump process info */
  stacktrace();
  _exit(1);
}

void stacktrace_init_internal(int signal_hook, va_list siglist)
{
  int i;
  struct sigaction sa;

/* FIXME - ugly hack for sunos */
#if __sun
  if((binfile = open("/proc/self/object/a.out", O_RDONLY)) < 0)
#else
  if((binfile = open("/proc/self/exe", O_RDONLY)) < 0)
#endif
  {
    ERR_ERRNO("Cannot open exec file");
    return;
  }

  if(signal_hook)
  {
    stack_t oss;

    /* use alternate stack for signal handlers */
    if((ss.ss_sp = malloc(SIGSTKSZ)) == NULL)
    {
      ERR("No memory for alternative signal stack.");
      return;
    }
    ss.ss_sp += SIGSTKSZ;
    ss.ss_size = SIGSTKSZ;
    MSG("Allocating an alternative stack of %d bytes.", ss.ss_size);
    ss.ss_flags = 0;
    /* Manejar error */;
    if(sigaltstack(&ss, &oss))
    {
      ERR_ERRNO("sigaltstack()");
      return;
    }
    if(!(oss.ss_flags & SS_DISABLE))
      WRN("Alt stack was already set.");

    /* install signal handlers */
    sa.sa_sigaction = stacktrace_sigaction;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
    while((i = va_arg(siglist, int)) != 0)
      if(sigaction(i, &sa, NULL) < 0)
        ERR_ERRNO("Cannot install signal handler %d", i);
  }
}

void stacktrace_fini_internal(void)
{
  close(binfile);
}

void stacktrace_init_siglist(int signal_hook, ...)
{
  va_list siglist;

  va_start(siglist, signal_hook);
  stacktrace_init_internal(signal_hook, siglist);
  va_end(siglist);
  if(atexit(stacktrace_fini_internal))
    ERR("Cannot install finalization routine.");
}

void stacktrace_init(int signal_hook)
{
  stacktrace_init_siglist(signal_hook,
                          SIGHUP,  /* Term - Hangup detected or death of controlling process */
                          SIGINT,  /* Term - Interrupt from keyboard                         */
                          SIGQUIT, /* Core - Quit from keyboard                              */
                          SIGILL,  /* Core - Illegal Instruction                             */
                          SIGABRT, /* Core - Abort signal from abort(3)                      */
                          SIGFPE,  /* Core - Floating point exception                        */
                          SIGSEGV, /* Core - Invalid memory reference                        */
                          SIGPIPE, /* Term - Broken pipe: write to pipe with no readers      */
                          SIGALRM, /* Term - Timer signal from alarm(2)                      */
                          SIGTERM, /* Term - Termination signal                              */
                          SIGUSR1, /* Term - User-defined signal 1                           */
                          SIGUSR2, /* Term - User-defined signal 2                           */
                          SIGCHLD, /* Ign  - Child stopped or terminated                     */
                          SIGCONT, /* Cont - Continue if stopped                             */
                          SIGTSTP, /* Stop - Stop typed at tty                               */
                          SIGTTIN, /* Stop - tty input for background process                */
                          SIGTTOU, /* Stop - tty output for background process               */
                          0);
}

void stacktrace_version_get(int *maj, int *min, int *rev)
{
  *maj = *min = *rev = 0;
  sscanf(PACKAGE_VERSION, "%d.%d.%d", maj, min, rev);
}

int stacktrace_version_check(int maj, int min, int rev)
{
  int rmaj, rmin, rrev;

  stacktrace_version_get(&rmaj, &rmin, &rrev);

  if(maj != rmaj) return (maj < rmaj);
  if(min != rmin) return (min < rmin);
  return (rev <= rrev);
}

