/*
 * Copyright (c) 2000, Red Hat, Inc.
 *
 *     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.
 *
 *     A copy of the GNU General Public License can be found at
 *     http://www.gnu.org/
 *
 * Written by DJ Delorie <dj@redhat.com>
 *
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <time.h>
#include <ctype.h>
#include <windows.h>

#ifdef __GNUC__
const char *help_text = "
Usage: ssp [options] low_pc high_pc command...

The SSP is a `single-step profiler' - it uses the debug API to
single-step your program, noting *everything* your program runs, not
just random places hit by random timer interrupts.  You must specify
the range of EIP values to profile.  For example, you could profile
just a function, or just a line of code, or the whole thing.
Use \"objdump -h\" to find the start of .text and the section following
it; this is what you want.

There are many options to ssp.  Since step-profiling makes your
program run about 1,000 times slower than normal, it's best to
understand all the options so that you can narrow down the parts
of your program you need to single-step.

-v = verbose messages about debug events.

-d, -e = disable/enable single-stepping by default.  Use
OutputDebugString (\"ssp on\") to enable stepping, or \"ssp off\" to
disable it.  Thus, you can profile a single function call or block.

-t = trace every EIP value to a file TRACE.SSP.  This gets big *fast*.
Use \"addr2line -C -f -s -e foo.exe < trace.ssp > lines.ssp\" and then
\"perl cvttrace\" to convert to symbolic traces.

-tc = trace every EIP value to the console.  *Lots* slower.

-s = trace sub-threads too.  Dangerous if you have race conditions.

-dll = enable dll profiling.  A chart of relative DLL usage is
produced after the run.

Examples:
  ssp 0x401000 0x403000 hello.exe
  ssp -v -d -dll 0x401000 0x440000 foo.exe

The output is a file \"gmon.out\" that can be read with gprof:
  gprof -b foo.exe

See ssp.txt in the cygwin sources for more information.
";
#else
char *help_text = "Usage: get cygwin!\n";
#endif

#define KERNEL_ADDR 0x77000000

#define TRACE_SSP 0

#define VERBOSE	1
#define TIMES	1000

/* from winsup/gmon.h */
struct gmonhdr {
	unsigned long	lpc;	/* base pc address of sample buffer */
	unsigned long	hpc;	/* max pc address of sampled buffer */
	int	ncnt;		/* size of sample buffer (plus this header) */
	int	version;	/* version number */
	int	profrate;	/* profiling clock rate */
	int	spare[3];	/* reserved */
};
#define GMONVERSION	0x00051879
#define HISTCOUNTER unsigned short

typedef struct {
  unsigned int base_address;
  int pcount;
  int scount;
  char *name;
} DllInfo;

typedef struct {
  unsigned int address;
  unsigned char real_byte;
} PendingBreakpoints;

int low_pc=0, high_pc=0;
unsigned int last_pc=0, pc, last_sp=0, sp;
int total_cycles, count;
char *cmd_line;
HANDLE hProcess;
PROCESS_INFORMATION procinfo;
STARTUPINFO startup;
CONTEXT context;
HISTCOUNTER *hits=0;
struct gmonhdr hdr;
int running = 1, profiling = 1;
char dll_name[1024], *dll_ptr, *cp;
int eip;
unsigned opcode_count = 0;

int stepping_enabled = 1;
int tracing_enabled = 0;
int trace_console = 0;
int trace_all_threads = 0;
int dll_counts = 0;
int verbose = 0;

#define MAXTHREADS 100
int active_thread_ids[MAXTHREADS];
HANDLE active_threads[MAXTHREADS];
int thread_step_flags[MAXTHREADS];
int thread_return_address[MAXTHREADS];
int num_active_threads = 0;
int suspended_count=0;

#define MAXDLLS 100
DllInfo dll_info[MAXDLLS];
int num_dlls=0;

#define MAXPENDS 100
PendingBreakpoints pending_breakpoints[MAXPENDS];
int num_breakpoints=0;

static void
add_breakpoint (unsigned int address)
{
  int i;
  DWORD rv;
  static char int3[] = { 0xcc };
  for (i=0; i<num_breakpoints; i++)
    {
      if (pending_breakpoints[i].address == address)
	return;
      if (pending_breakpoints[i].address == 0)
	break;
    }
  if (i == MAXPENDS)
    return;
  pending_breakpoints[i].address = address;
  ReadProcessMemory (hProcess,
		    (void *)address,
		    &(pending_breakpoints[i].real_byte),
		    1, &rv);

  WriteProcessMemory (hProcess,
		     (void *)address,
		     (LPVOID)int3, 1, &rv);
  if (i >= num_breakpoints)
    num_breakpoints = i+1;
}

static int
remove_breakpoint (unsigned int address)
{
  int i;
  DWORD rv;
  for (i=0; i<num_breakpoints; i++)
    {
      if (pending_breakpoints[i].address == address)
	{
	  pending_breakpoints[i].address = 0;
	  WriteProcessMemory (hProcess,
			     (void *)address,
			     &(pending_breakpoints[i].real_byte),
			     1, &rv);
	  return 1;
	}
    }
  return 0;
}

static HANDLE
lookup_thread_id (int threadId, int *tix)
{
  int i;
  for (i=0; i<num_active_threads; i++)
    if (active_thread_ids[i] == threadId)
      {
	if (tix) *tix = i;
	return active_threads[i];
      }
  return 0;
}

static void
set_step_threads (int threadId, int trace)
{
  int rv, tix;
  HANDLE thread = lookup_thread_id (threadId, &tix);

  rv = GetThreadContext (thread, &context);
  if (rv != -1)
    {
      thread_step_flags[tix] = trace;
      if (trace)
	context.EFlags |= 0x100; /* TRAP (single step) flag */
      else
	context.EFlags &= ~0x100; /* TRAP (single step) flag */
      SetThreadContext (thread, &context);
    }
}

static void
set_steps ()
{
  int i, s;
  for (i=0; i<num_active_threads; i++)
    {
      GetThreadContext (active_threads[i], &context);
      s = context.EFlags & 0x0100;
      if (!s && thread_step_flags[i])
	{
	  set_step_threads (active_thread_ids[i], 1);
	}
    }
}

static int
dll_sort (const void *va, const void *vb)
{
  DllInfo *a = (DllInfo *)va;
  DllInfo *b = (DllInfo *)vb;
  if (a->base_address < b->base_address)
    return -1;
  return 1;
}

static char *
addr2dllname (unsigned int addr)
{
  int i;
  for (i=num_dlls-1; i>=0; i--)
    {
      if (dll_info[i].base_address < addr)
	{
	  return dll_info[i].name;
	}
    }
  return (char *)"";
}

static void
dump_registers (HANDLE thread)
{
  context.ContextFlags = CONTEXT_FULL;
  GetThreadContext (thread, &context);
  printf ("eax %08lx ebx %08lx ecx %08lx edx %08lx eip\n",
	  context.Eax, context.Ebx, context.Ecx, context.Edx);
  printf ("esi %08lx edi %08lx ebp %08lx esp %08lx %08lx\n",
	  context.Esi, context.Esi, context.Ebp, context.Esp, context.Eip);
}

typedef struct Edge {
  struct Edge *next;
  unsigned int from_pc;
  unsigned int to_pc;
  unsigned int count;
} Edge;

Edge *edges[4096];

void
store_call_edge (unsigned int from_pc, unsigned int to_pc)
{
  Edge *e;
  unsigned int h = ((from_pc + to_pc)>>4) & 4095;
  for (e=edges[h]; e; e=e->next)
    if (e->from_pc == from_pc && e->to_pc == to_pc)
      break;
  if (!e)
    {
      e = (Edge *)malloc (sizeof (Edge));
      e->next = edges[h];
      edges[h] = e;
      e->from_pc = from_pc;
      e->to_pc = to_pc;
      e->count = 0;
    }
  e->count++;
}

void
write_call_edges (FILE *f)
{
  int h;
  Edge *e;
  for (h=0; h<4096; h++)
    for (e=edges[h]; e; e=e->next)
      fwrite (&(e->from_pc), 1, 3*sizeof (unsigned int), f);
}

char *
wide_strdup (char *cp)
{
  unsigned short *s = (unsigned short *)cp;
  int len;
  char *rv;
  for (len=0; s[len]; len++);
  rv = (char *)malloc (len+1);
  for (len=0; s[len]; len++)
    rv[len] = s[len];
  rv[len] = 0;
  return rv;
}

void
run_program (char *cmdline)
{
  FILE *tracefile = 0;
  int tix, i;
  HANDLE hThread;
  char *string;

  memset (&startup, 0, sizeof (startup));
  startup.cb = sizeof (startup);

  if (!CreateProcess (0, cmd_line, 0, 0, 0,
		     CREATE_NEW_PROCESS_GROUP
		     | CREATE_SUSPENDED
		     | DEBUG_PROCESS
		     | DEBUG_ONLY_THIS_PROCESS,
		     0, 0, &startup, &procinfo))
    {
      fprintf (stderr, "Can't create process: error %ld\n", GetLastError ());
      exit (1);
    }

  hProcess = procinfo.hProcess;
#if 0
  printf ("procinfo: %08x %08x %08x %08x\n",
	 hProcess, procinfo.hThread, procinfo.dwProcessId, procinfo.dwThreadId);
#endif

  active_threads[0] = procinfo.hThread;
  active_thread_ids[0] = procinfo.dwThreadId;
  thread_step_flags[0] = stepping_enabled;
  num_active_threads = 1;

  dll_info[0].base_address = 0;
  dll_info[0].pcount = 0;
  dll_info[0].scount = 0;
  dll_info[0].name = cmd_line;
  num_dlls = 1;

  SetThreadPriority (procinfo.hThread, THREAD_PRIORITY_IDLE);

  context.ContextFlags = CONTEXT_FULL;

  ResumeThread (procinfo.hThread);

  total_cycles = 0;

  if (tracing_enabled)
    {
      tracefile = fopen ("trace.ssp", "w");
      if (!tracefile)
	{
	  tracing_enabled = 0;
	  perror ("trace.ssp");
	}
    }

  running = 1;
  while (running)
    {
      int src, dest;
      DWORD rv;
      DEBUG_EVENT event;
      int contv = DBG_CONTINUE;

      event.dwDebugEventCode = -1;
      if (!WaitForDebugEvent (&event, INFINITE))
	{
	  printf ("idle...\n");
	}

      hThread = lookup_thread_id (event.dwThreadId, &tix);

#if 0
      printf ("DE: %x/%d %d %d ",
	     hThread, tix,
	     event.dwDebugEventCode, num_active_threads);
      for (src=0; src<num_active_threads; src++)
	{
	  int sc = SuspendThread (active_threads[src]);
	  int rv = GetThreadContext (active_threads[src], &context);
	  ResumeThread (active_threads[src]);
	  printf (" [%x,%x,%x]",
		 active_threads[src], context.Eip, active_thread_ids[src]);
	}
      printf ("\n");
#endif

      switch (event.dwDebugEventCode)
	{

	case CREATE_PROCESS_DEBUG_EVENT:
	  break;

	case CREATE_THREAD_DEBUG_EVENT:
	  if (verbose)
	    printf ("create thread %08lx at %08x %s\n",
		   event.dwThreadId,
		   (int)event.u.CreateThread.lpStartAddress,
		   addr2dllname ((unsigned int)event.u.CreateThread.lpStartAddress));

	  active_thread_ids[num_active_threads] = event.dwThreadId;
	  active_threads[num_active_threads] = event.u.CreateThread.hThread;
	  thread_return_address[num_active_threads] = 0;
	  num_active_threads++;

	  if (trace_all_threads && stepping_enabled)
	    {
	      thread_step_flags[num_active_threads-1] = stepping_enabled;
	      add_breakpoint ((int)event.u.CreateThread.lpStartAddress);
	    }

	  break;

	case EXIT_THREAD_DEBUG_EVENT:
	  if (verbose)
	    printf ("exit thread %08lx, code=%ld\n",
		   event.dwThreadId,
		   event.u.ExitThread.dwExitCode);

	  for (src=0, dest=0; src<num_active_threads; src++)
	    if (active_thread_ids[src] != event.dwThreadId)
	      {
		active_thread_ids[dest] = active_thread_ids[src];
		active_threads[dest] = active_threads[src];
		dest++;
	      }
	  num_active_threads = dest;
	  break;

	case EXCEPTION_DEBUG_EVENT:
	  rv = GetThreadContext (hThread, &context);
	  switch (event.u.Exception.ExceptionRecord.ExceptionCode)
	    {
	    case STATUS_BREAKPOINT:
	      if (remove_breakpoint ((int)event.u.Exception.ExceptionRecord.ExceptionAddress))
		{
		  context.Eip --;
		  if (rv != -1)
		    SetThreadContext (hThread, &context);
		  if (ReadProcessMemory (hProcess, (void *)context.Esp, &rv, 4, &rv))
		      thread_return_address[tix] = rv;
		}
	      set_step_threads (event.dwThreadId, stepping_enabled);
	    case STATUS_SINGLE_STEP:
	      opcode_count++;
	      pc = (unsigned int)event.u.Exception.ExceptionRecord.ExceptionAddress;
	      sp = (unsigned int)context.Esp;
	      if (tracing_enabled)
		fprintf (tracefile, "%08x %08lx\n", pc, event.dwThreadId);
	      if (trace_console)
		{
		  printf ("%d %08x\n", tix, pc);
		  fflush (stdout);
		}

	      if (dll_counts)
		{
		  int i;
		  for (i=num_dlls-1; i>=0; i--)
		    {
		      if (dll_info[i].base_address < context.Eip)
			{
			  if (hThread == procinfo.hThread)
			    dll_info[i].pcount++;
			  else
			    dll_info[i].scount++;
			  break;
			}
		    }
		}

	      if (pc < last_pc || pc > last_pc+10)
		{
		  static int ncalls=0;
		  static int qq=0;
		  if (++qq % 100 == 0)
		    fprintf (stderr, " %08x %d %d \r",
			    pc, ncalls, opcode_count);

		  if (sp == last_sp-4)
		    {
		      ncalls++;
		      store_call_edge (last_pc, pc);
		      if (last_pc < KERNEL_ADDR && pc > KERNEL_ADDR)
			{
			  int retaddr;
			  DWORD rv;
			  ReadProcessMemory (hProcess,
					    (void *)sp,
					    (LPVOID)&(retaddr),
					    4, &rv);
#if 0
			  printf ("call last_pc = %08x pc = %08x rv = %08x\n",
				 last_pc, pc, retaddr);
			  /* experimental - try to skip kernel calls for speed */
			  add_breakpoint (retaddr);
			  set_step_threads (event.dwThreadId, 0);
#endif
			}
		    }
		}

	      total_cycles++;
	      last_sp = sp;
	      last_pc = pc;
	      if (pc >= low_pc && pc < high_pc)
		hits[(pc - low_pc)/2] ++;
	      break;
	    default:
	      if (verbose)
		{
		  printf ("exception %ld, ", event.u.Exception.dwFirstChance);
		  printf ("code: %lx flags: %lx\n",
			 event.u.Exception.ExceptionRecord.ExceptionCode,
			 event.u.Exception.ExceptionRecord.ExceptionFlags);
		  if (event.u.Exception.dwFirstChance == 1)
		    dump_registers (hThread);
		}
	      contv = DBG_EXCEPTION_NOT_HANDLED;
	      running = 0;
	      break;
	    }

	  if (rv != -1)
	    {
	      if (pc == thread_return_address[tix])
		{
		  if (context.EFlags & 0x100)
		    {
		      context.EFlags &= ~0x100; /* TRAP (single step) flag */
		      SetThreadContext (hThread, &context);
		    }
		}
	      else if (stepping_enabled)
		{
		  if (!(context.EFlags & 0x100))
		    {
		      context.EFlags |= 0x100; /* TRAP (single step) flag */
		      SetThreadContext (hThread, &context);
		    }
		}
	    }
	  break;

	case OUTPUT_DEBUG_STRING_EVENT:
	  string = (char *)malloc (event.u.DebugString.nDebugStringLength+1);
	  i = ReadProcessMemory (hProcess,
			    event.u.DebugString.lpDebugStringData,
			    (LPVOID)string,
			    event.u.DebugString.nDebugStringLength,
			    &rv);
	  if (!i)
	    {
	      printf ("error reading memory: %ld %ld\n", rv, GetLastError ());
	    }
	  if (verbose)
	    printf ("ODS: %x/%d \"%s\"\n",
		   (int)hThread, tix, string);

	  if (strcmp (string, "ssp on") == 0)
	    {
	      stepping_enabled = 1;
	      set_step_threads (event.dwThreadId, 1);
	    }

	  if (strcmp (string, "ssp off") == 0)
	    {
	      stepping_enabled = 0;
	      set_step_threads (event.dwThreadId, 0);
	    }

	  break;


	case LOAD_DLL_DEBUG_EVENT:
	  if (verbose)
	    printf ("load dll %08x:",
		   (int)event.u.LoadDll.lpBaseOfDll);

	  dll_ptr = (char *)"( u n k n o w n ) \0\0";
	  if (event.u.LoadDll.lpImageName)
	    {
	      ReadProcessMemory (hProcess,
				event.u.LoadDll.lpImageName,
				(LPVOID)&src,
				sizeof (src),
				&rv);
	      if (src)
		{
		  ReadProcessMemory (hProcess,
				    (void *)src,
				    (LPVOID)dll_name,
				    sizeof (dll_name),
				    &rv);
		  dll_name[rv] = 0;
		  dll_ptr = dll_name;
		  for (cp=dll_name; *cp; cp++)
		    {
		      if (*cp == '\\' || *cp == '/')
			{
			  dll_ptr = cp+1;
			}
		      *cp = tolower (*cp);
		    }
		}
	    }


	  dll_info[num_dlls].base_address
	    = (unsigned int)event.u.LoadDll.lpBaseOfDll;
	  dll_info[num_dlls].pcount = 0;
	  dll_info[num_dlls].scount = 0;
	  dll_info[num_dlls].name = wide_strdup (dll_ptr);
	  if (verbose)
	    printf (" %s\n", dll_info[num_dlls].name);
	  num_dlls++;
	  qsort (dll_info, num_dlls, sizeof (DllInfo), dll_sort);

	  break;

	case UNLOAD_DLL_DEBUG_EVENT:
	  if (verbose)
	    printf ("unload dll\n");
	  break;

	case EXIT_PROCESS_DEBUG_EVENT:
	  if (verbose)
	    printf ("process %08lx %08lx exit %ld\n",
		   event.dwProcessId, event.dwThreadId,
		   event.u.ExitProcess.dwExitCode);

	  running = 0;
	  break;
	}

      set_steps ();
      ContinueDebugEvent (event.dwProcessId, event.dwThreadId, contv);
    }

  count = 0;
  for (pc=low_pc; pc<high_pc; pc+=2)
    {
      count += hits[(pc - low_pc)/2];
    }
  printf ("total cycles: %d, counted cycles: %d\n", total_cycles, count);

  if (tracing_enabled)
    fclose (tracefile);

}

#define WHITE(s) while (isspace (*s)) s++

int
main (int argc, char **argv)
{
  int i, n;
  int total_pcount, total_scount;
  FILE *gmon;

  setbuf (stdout, 0);

  cmd_line = GetCommandLine ();

  /* strip off the program part */
  n = sscanf (cmd_line, "%*s%n", &i);
  cmd_line += i;
  WHITE (cmd_line);

  /* parse arguments.  Ugly, but we need to preserve the tail of the line */
  while (*cmd_line == '-')
    {
      char opt[100];
      n = sscanf (cmd_line, " %s%n", opt, &i);
      cmd_line += i;
      WHITE (cmd_line);

      if (strcmp (opt, "-d") == 0)
	{
	  printf ("stepping disabled; enable via OutputDebugString (\"ssp on\")\n");
	  stepping_enabled = 0;
	}
      if (strcmp (opt, "-e") == 0)
	{
	  printf ("stepping enabled; disable via OutputDebugString (\"ssp off\")\n");
	  stepping_enabled = 1;
	}
      if (strcmp (opt, "-t") == 0)
	{
	  printf ("tracing all $eip to trace.ssp\n");
	  tracing_enabled = 1;
	}
      if (strcmp (opt, "-tc") == 0)
	{
	  printf ("tracing *all* $eip to the console\n");
	  trace_console = 1;
	}
      if (strcmp (opt, "-s") == 0)
	{
	  printf ("tracing all sub-threads too, not just the main one\n");
	  trace_all_threads = 1;
	}
      if (strcmp (opt, "-dll") == 0)
	{
	  printf ("profiling dll usage\n");
	  dll_counts = 1;
	}
      if (strcmp (opt, "-v") == 0)
	{
	  printf ("verbose messages enabled\n");
	  verbose = 1;
	}
    }

  n = sscanf (cmd_line, " %i %i %n", &low_pc, &high_pc, &i);
  if (n < 2)
    {
      fputs (help_text, stderr);
      exit (1);
    }
  cmd_line += i;

  if (low_pc > high_pc-8)
    {
      fprintf (stderr, "Hey, low_pc must be lower than high_pc\n");
      exit (1);
    }

  hits = (HISTCOUNTER *)malloc (high_pc-low_pc+4);
  memset (hits, 0, high_pc-low_pc+4);

  fprintf (stderr, "prun: [%08x,%08x] Running `%s'\n",
	  low_pc, high_pc, cmd_line);

  run_program (cmd_line);

  hdr.lpc = low_pc;
  hdr.hpc = high_pc;
  hdr.ncnt = high_pc-low_pc + sizeof (hdr);
  hdr.version = GMONVERSION;
  hdr.profrate = 100;

  gmon = fopen ("gmon.out", "wb");
  fwrite (&hdr, 1, sizeof (hdr), gmon);
  fwrite (hits, 1, high_pc-low_pc, gmon);
  write_call_edges (gmon);
  fclose (gmon);

  if (dll_counts)
    {
      /*      1234567 123% 1234567 123% 12345678 xxxxxxxxxxx */
      printf (" Main-Thread Other-Thread BaseAddr DLL Name\n");

      total_pcount = 0;
      total_scount = 0;
      for (i=0; i<num_dlls; i++)
	{
	  total_pcount += dll_info[i].pcount;
	  total_scount += dll_info[i].scount;
	}

      if (total_pcount == 0) total_pcount++;
      if (total_scount == 0) total_scount++;

      for (i=0; i<num_dlls; i++)
	if (dll_info[i].pcount || dll_info[i].scount)
	  {
	    printf ("%7d %3d%% %7d %3d%% %08x %s\n",
		   dll_info[i].pcount,
		   (dll_info[i].pcount*100)/opcode_count,
		   dll_info[i].scount,
		   (dll_info[i].scount*100)/opcode_count,
		   dll_info[i].base_address,
		   dll_info[i].name);
	  }
    }

  exit (0);
}

