/* ----------------------------------------------------------------------- *
 *
 *   Copyright 2007-2008 H. Peter Anvin - All Rights Reserved
 *
 *   Permission is hereby granted, free of charge, to any person
 *   obtaining a copy of this software and associated documentation
 *   files (the "Software"), to deal in the Software without
 *   restriction, including without limitation the rights to use,
 *   copy, modify, merge, publish, distribute, sublicense, and/or
 *   sell copies of the Software, and to permit persons to whom
 *   the Software is furnished to do so, subject to the following
 *   conditions:
 *
 *   The above copyright notice and this permission notice shall
 *   be included in all copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 *   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 *   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 *   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 *   OTHER DEALINGS IN THE SOFTWARE.
 *
 * ----------------------------------------------------------------------- */

/*
 * elf.c
 *
 * Module to load a protected-mode ELF kernel
 */

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <minmax.h>
#include <sys/stat.h>
#include <elf.h>
#include <console.h>

#include <syslinux/loadfile.h>
#include <syslinux/movebits.h>
#include <syslinux/bootpm.h>

/* If we don't have this much memory for the stack, signal failure */
#define MIN_STACK	512

#define DEBUG 0
#if DEBUG
# define dprintf printf
#else
# define dprintf(f, ...) ((void)0)
#endif

static inline void error(const char *msg)
{
  fputs(msg, stderr);
}

int boot_elf(void *ptr, size_t len, char **argv)
{
  char *cptr = ptr;
  Elf32_Ehdr *eh = ptr;
  Elf32_Phdr *ph;
  unsigned int i;
  struct syslinux_movelist *ml = NULL;
  struct syslinux_memmap *mmap = NULL, *amap = NULL;
  struct syslinux_pm_regs regs;
  int argc;
  addr_t argsize;
  char **argp;
  addr_t lstart, llen;
  char *stack_frame = NULL;
  addr_t stack_frame_size;
  addr_t stack_pointer;
  uint32_t *spp;
  char *sfp;
  addr_t sfa;

  memset(&regs, 0, sizeof regs);

  /*
   * Note: mmap is the memory map (containing free and zeroed regions)
   * needed by syslinux_shuffle_boot_pm(); amap is a map where we keep
   * track ourselves which target memory ranges have already been
   * allocated.
   */

  if ( len < sizeof(Elf32_Ehdr) )
    goto bail;

  /* Must be ELF, 32-bit, littleendian, version 1 */
  if ( memcmp(eh->e_ident, "\x7f""ELF\1\1\1", 6) )
    goto bail;

  /* Is this a worthwhile test?  In particular x86-64 normally
     would imply ELF64 support, which we could do as long as
     the addresses are 32-bit addresses, and entry is 32 bits.
     64-bit addresses would take a lot more work. */
  if ( eh->e_machine != EM_386 && eh->e_machine != EM_486 &&
       eh->e_machine != EM_X86_64 )
    goto bail;

  if ( eh->e_version != EV_CURRENT )
    goto bail;

  if ( eh->e_ehsize < sizeof(Elf32_Ehdr) || eh->e_ehsize >= len )
    goto bail;

  if ( eh->e_phentsize < sizeof(Elf32_Phdr) )
    goto bail;

  if ( !eh->e_phnum )
    goto bail;

  if ( eh->e_phoff+eh->e_phentsize*eh->e_phnum > len )
    goto bail;

  mmap = syslinux_memory_map();
  amap = syslinux_dup_memmap(mmap);
  if (!mmap || !amap)
    goto bail;

#if DEBUG
  dprintf("Initial memory map:\n");
  syslinux_dump_memmap(stdout, mmap);
#endif

  ph = (Elf32_Phdr *)(cptr+eh->e_phoff);

  for (i = 0; i < eh->e_phnum; i++) {
    if (ph->p_type == PT_LOAD || ph->p_type == PT_PHDR) {
      /* This loads at p_paddr, which is arguably the correct semantics.
	 The SysV spec says that SysV loads at p_vaddr (and thus Linux does,
	 too); that is, however, a major brainfuckage in the spec. */
      addr_t addr  = ph->p_paddr;
      addr_t msize = ph->p_memsz;
      addr_t dsize = min(msize, ph->p_filesz);

      dprintf("Segment at 0x%08x data 0x%08x len 0x%08x\n",
	      addr, dsize, msize);

      if (syslinux_memmap_type(amap, addr, msize) != SMT_FREE) {
	printf("Memory segment at 0x%08x (len 0x%08x) is unavailable\n",
	       addr, msize);
	goto bail;		/* Memory region unavailable */
      }

      /* Mark this region as allocated in the available map */
      if (syslinux_add_memmap(&amap, addr, dsize, SMT_ALLOC))
	goto bail;

      if (ph->p_filesz) {
	/* Data present region.  Create a move entry for it. */
	if (syslinux_add_movelist(&ml, addr, (addr_t)cptr+ph->p_offset,
				  dsize))
	  goto bail;
      }
      if (msize > dsize) {
	/* Zero-filled region.  Mark as a zero region in the memory map. */
	if (syslinux_add_memmap(&mmap, addr+dsize, msize-dsize, SMT_ZERO))
	  goto bail;
      }
    } else {
      /* Ignore this program header */
    }

    ph = (Elf32_Phdr *)((char *)ph + eh->e_phentsize);
  }

  /* Create the invocation record (initial stack frame) */

  argsize = argc = 0;
  for (argp = argv; *argp; argp++) {
    dprintf("argv[%2d] = \"%s\"\n", argc, *argp);
    argc++;
    argsize += strlen(*argp)+1;
  }

  /* We need the argument strings, argument pointers,
     argc, plus four zero-word terminators. */
  stack_frame_size = argsize + argc*sizeof(char *) + 5*sizeof(long);
  stack_frame_size = (stack_frame_size+15) & ~15;
  stack_frame = calloc(stack_frame_size, 1);
  if (!stack_frame)
    goto bail;

#if DEBUG
  dprintf("Right before syslinux_memmap_largest()...\n");
  syslinux_dump_memmap(stdout, amap);
#endif

  if (syslinux_memmap_largest(amap, SMT_FREE, &lstart, &llen))
    goto bail;			/* NO free memory?! */

  if (llen < stack_frame_size+MIN_STACK+16)
    goto bail;			/* Insufficient memory  */

  /* Initial stack pointer address */
  stack_pointer = (lstart+llen-stack_frame_size) & ~15;

  dprintf("Stack frame at 0x%08x len 0x%08x\n",
	  stack_pointer, stack_frame_size);

  /* Create the stack frame.  sfp is the pointer in current memory for
     the next argument string, sfa is the address in its final resting place.
     spp is the pointer into the argument array in current memory. */
  spp = (uint32_t *)stack_frame;
  sfp = stack_frame + argc*sizeof(char *) + 5*sizeof(long);
  sfa = stack_pointer + argc*sizeof(char *) + 5*sizeof(long);

  *spp++ = argc;
  for (argp = argv; *argp; argp++) {
    int bytes = strlen(*argp) + 1; /* Including final null */
    *spp++ = sfa;
    memcpy(sfp, *argp, bytes);
    sfp += bytes;
    sfa += bytes;
  }
  /* Zero fields are aready taken care of by calloc() */

  /* ... and we'll want to move it into the right place... */
#if DEBUG
  if (syslinux_memmap_type(amap, stack_pointer, stack_frame_size)
      != SMT_FREE) {
    dprintf("Stack frame area not free (how did that happen?)!\n");
    goto bail;		/* Memory region unavailable */
  }
#endif

  if (syslinux_add_memmap(&amap, stack_pointer, stack_frame_size, SMT_ALLOC))
    goto bail;

  if (syslinux_add_movelist(&ml, stack_pointer, (addr_t)stack_frame,
			    stack_frame_size))
    goto bail;

  memset(&regs, 0, sizeof regs);
  regs.eip = eh->e_entry;
  regs.esp = stack_pointer;

#if DEBUG
  dprintf("Final memory map:\n");
  syslinux_dump_memmap(stdout, mmap);

  dprintf("Final available map:\n");
  syslinux_dump_memmap(stdout, amap);

  dprintf("Movelist:\n");
  syslinux_dump_movelist(stdout, ml);
#endif

  /* This should not return... */
  fputs("Booting...\n", stdout);
  syslinux_shuffle_boot_pm(ml, mmap, 0, &regs);

 bail:
  if (stack_frame)
    free(stack_frame);
  syslinux_free_memmap(amap);
  syslinux_free_memmap(mmap);
  syslinux_free_movelist(ml);

  return -1;
}

int main(int argc, char *argv[])
{
  void *data;
  size_t data_len;

  openconsole(&dev_null_r, &dev_stdcon_w);

  if (argc < 2) {
    error("Usage: elf.c32 elf_file arguments...\n");
    return 1;
  }

  if (loadfile(argv[1], &data, &data_len)) {
    error("Unable to load file\n");
    return 1;
  }

  boot_elf(data, data_len, &argv[1]);
  error("Invalid ELF file or insufficient memory\n");
  return 1;
}
