/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

/*
 * interface used by unwind support to query frame descriptor info
 */

#ifndef _LIBCRUN_
#include "lint.h"
#endif
#include <sys/types.h>
#include <limits.h>
#include "stack_unwind.h"
#include "unwind_context.h"
#include <dlfcn.h>

/*
 * CIE:
 *	UNUM32		length
 *	UNUM32		ID
 *	UNUM8		version
 *	ZTSTRING	augmentation
 *	ULEB128		Code Align Factor
 *	SLEB128		Data Align Factor
 *	UNUM8		RA
 *	ULEB128		length
 *	UNUM8		personality enc
 *	ADDR		personality
 *	UNUM8		code_enc
 *	UNUM8		lsda_enc
 *
 * FDE:
 *	UNUM32		length
 *	UNUM32		ID
 *	ADDR		initial loc
 *	SIZE		size
 *	ULEB128		length
 *	ADDR		lsda
 */


struct eh_frame_fields *
_Unw_Decode_FDE(struct eh_frame_fields *f, struct _Unwind_Context *ctx)
{
	void *fde_data;    /* location in this process of fde */
	void *fde_end;
	void *data;
	ptrdiff_t reloc;
	uintptr_t base;
	void *cie_data;    /* location in this process of cie */
	void *cie_end;
	void *cdata;
	ptrdiff_t creloc;
	int lsda_enc = 0;
	int per_enc = 0;
	int code_enc = 0;
	char augment[8];
	char *p;
	uint64_t scratch;

	uint64_t func = 0;
	uint64_t range = 0;
	_Unwind_Personality_Fn	pfn = 0;
	void* lsda = 0;

	/* here is where data mapping would happen ??REMOTE?? */
	fde_data = ctx->fde;
	data = fde_data;
	fde_end = (void *)(((intptr_t)fde_data) + 4 +
	    _Unw_get_val(&data, 0, UNUM32, 1, 1, 0));
	reloc = 0;
	base = ((intptr_t)data) + reloc;
	cie_data = (void *)(base -  _Unw_get_val(&data, 0, UNUM32, 1, 1, 0));
	cdata = cie_data;
	cie_end = (void *)(((intptr_t)cie_data) + 4 +
	    _Unw_get_val(&cdata, 0, UNUM32, 1, 1, 0));
	creloc = 0;
	/* data mapping has happened */

	f->cie_ops_end = cie_end;
	f->cie_reloc = creloc;
	f->fde_ops_end = fde_end;
	f->fde_reloc = reloc;

	(void) _Unw_get_val(&cdata, creloc, UNUM32, 1, 1, 0);
	(void) _Unw_get_val(&cdata, creloc, UNUM8, 1, 1, 0);
	/* LINTED alignment */
	(*((uint64_t *)(&(augment[0]))))  =
	    _Unw_get_val(&cdata, creloc, ZTSTRING, 1, 1, 0);
	f->code_align = _Unw_get_val(&cdata, creloc, ULEB128, 1, 1, 0);
	f->data_align = _Unw_get_val(&cdata, creloc, SLEB128, 1, 1, 0);
	(void) _Unw_get_val(&cdata, creloc, UNUM8, 1, 1, 0);
	if (augment[0] == 'z' &&
	    (scratch = _Unw_get_val(&cdata, creloc, ULEB128, 1, 1, 0)) != 0) {
		for (p = &(augment[1]); *p != 0; p++) {
			switch (*p) {
			case 'P':
				per_enc = _Unw_get_val(&cdata, creloc,
				    UNUM8, 1, 1, 0);
				if (per_enc == 0)
					per_enc = 0x4;
				pfn = (_Unwind_Personality_Fn)
				    _Unw_get_val(&cdata, creloc,
				    ADDR, 1, 1, per_enc);
				break;
			case 'R':
				code_enc = _Unw_get_val(&cdata, creloc,
				    UNUM8, 1, 1, 0);
				break;
			case 'L':
				lsda_enc = _Unw_get_val(&cdata, creloc,
				    UNUM8, 1, 1, 0);
				break;
			}
		}
	}
	if (code_enc == 0)
		code_enc = 0x4;

	func = _Unw_get_val(&data, reloc, ADDR, 1, 1, code_enc);
	range = _Unw_get_val(&data, reloc, SIZE, 1, 1, code_enc);
	if ((ctx->pc < func) || (ctx->pc > (func+range)))
		return (0);
	ctx->func = func;
	ctx->range = range;
	if (augment[0] == 'z') {
		scratch = _Unw_get_val(&data, reloc, ULEB128, 1, 1, 0);
		if (scratch == 4 && lsda_enc) {
			/*
			 * without the two work-arounds test would be
			 * (scratch > 0 & lsda_enc)
			 */
			lsda = (void *)_Unw_get_val(&data, reloc,
			    ADDR, 1, 1, lsda_enc);
		} else if (scratch == 4) {
			/*
			 * 11/24/04 compiler is sometimes not outputing
			 * lsda_enc
			 */
			lsda = (void*)_Unw_get_val(&data, reloc,
			    ADDR, 1, 1, 0x1b);
		} else if (scratch == 8) {
			/*
			 * 11/12/04 - compiler is putting out relative
			 * encoding byte and absolute data - inconsistancy
			 * is caught here.
			 */
			lsda = (void *)_Unw_get_val(&data, reloc,
			    ADDR, 1, 1, 0x4);
		}
	}
	if (pfn)
		ctx->pfn = pfn;
	if (lsda)
		ctx->lsda = lsda;
	f->fde_ops = data;
	f->cie_ops = cdata;
	f->code_enc = code_enc;
	return (f);
}

static int
table_ent_log_size(int enc)
{
	int val = enc & 0xf;
	int res;

	switch (val) {
	case 0x3:
		res = 3;
		break;
	case 0x04:
		res = 4;
		break;
	case 0x0b:
		res = 3;
		break;
	case 0x0c:
		res = 4;
		break;
	default:
		break;
	}
	return (res);
}

static void
get_table_ent_val(unsigned char *data, unsigned char *data_end,
	int enc, ptrdiff_t reloc, uintptr_t base,
	uint64_t *codep, uint64_t *next_codep, void **fdep)
{
	int val = enc & 0xf;
	int rel = (enc >> 4) & 0xf;
	unsigned char *second = data;
	unsigned char *third = data;
	uint64_t code;
	void *fde;
	uint64_t next_code;

	switch (val) {
	case 0x3:
		/* LINTED alignment */
		code = (uint64_t)(*((uint32_t *)data));
		second += 4;
		/* LINTED alignment */
		fde = (void *)(uint64_t)(*((uint32_t *)second));
		third += 8;
		next_code = (third >= data_end)? ULONG_MAX :
		    /* LINTED alignment */
		    (uint64_t)(*((uint32_t *)third));
		break;
	case 0x04:
		/* LINTED alignment */
		code = (uint64_t)(*((uint64_t *)data));
		second += 8;
		/* LINTED alignment */
		fde = (void *)(uint64_t)(*((uint64_t *)second));
		third += 16;
		next_code = (third >= data_end)? ULONG_MAX :
		    /* LINTED alignment */
		    (uint64_t)(*((uint64_t *)third));
		break;
	case 0x0b:
		/* LINTED alignment */
		code = (uint64_t)(int64_t)(*((int32_t *)data));
		second += 4;
		/* LINTED alignment */
		fde = (void *)(uint64_t)(int64_t)(*((int32_t *)second));
		third += 8;
		next_code = (third >= data_end)? ULONG_MAX :
		    /* LINTED alignment */
		    (uint64_t)(int64_t)(*((int32_t *)third));
		break;
	case 0x0c:
		/* LINTED alignment */
		code = (uint64_t)(*((int64_t *)data));
		second += 8;
		/* LINTED alignment */
		fde = (void *)(uint64_t)(*((int64_t *)second));
		third += 16;
		next_code = (third >= data_end)? ULONG_MAX :
		    /* LINTED alignment */
		    (uint64_t)(*((int64_t *)third));
		break;
	}

	switch (rel) {
	case 0:
		break;
	case 1:
		code += (uint64_t)data + reloc;
		fde = (void *)(((uint64_t)fde) + (uint64_t)second + reloc);
		if (next_code != ULONG_MAX)
			next_code += (uint64_t)third + reloc;
		break;
	case 3:
		code += base;
		fde = (void *)(((uint64_t)fde) +  base);
		if (next_code != ULONG_MAX)
			next_code += base;
		break;
	default:
		/* remainder not implemented */
		break;
	}
	*codep = code;
	*fdep = fde;
	*next_codep = next_code;
}


static void *
locate_fde_for_pc(uint64_t pc, int enc,
	unsigned char *table, unsigned char *table_end,
	ptrdiff_t reloc, uintptr_t base);

/*
 * Search the eh_frame info with a given pc.  Return a pointer to a
 * FDE.  The search is performed in two stages.
 * First rtld.so identifies the load module containing the target location.
 * This returns the appropiate eh_frame_hdr, and a binary search is
 * then performed on the eh_frame_hdr to locate the entry with
 * a matching pc value.
 */
void *
_Unw_EhfhLookup(struct _Unwind_Context *ctx)
{
	Dl_amd64_unwindinfo dlef;
	void* data;
	void* data_end;
	uint64_t pc = ctx->pc;
	int fp_enc, fc_enc, ft_enc;
	unsigned char *pi, *pj;
	ptrdiff_t reloc;
	uintptr_t base;

	dlef.dlui_version = 1;

	/* Locate the appropiate exception_range_entry table first */
	if (0 == dlamd64getunwind((void*)pc, &dlef)) {
		return (0);
	}

	/*
	 * you now know size and position of block of data needed for
	 * binary search ??REMOTE??
	 */
	data = dlef.dlui_unwindstart;
	if (0 == data)
		return (0);
	base = (uintptr_t)data;
	data_end = dlef.dlui_unwindend;
	reloc = 0;
	/* ??REMOTE?? */

	(void) _Unw_get_val(&data, reloc, UNUM8, 1, 1, 0);
	fp_enc = _Unw_get_val(&data, reloc, UNUM8, 1, 1, 0);
	fc_enc = _Unw_get_val(&data, reloc, UNUM8, 1, 1, 0);
	ft_enc = _Unw_get_val(&data, reloc, UNUM8, 1, 1, 0);
	(void) _Unw_get_val(&data, reloc, ADDR, 1, 1, fp_enc);
	(void) _Unw_get_val(&data, reloc, SIZE, 1, 1, fc_enc);
	pi = data;
	pj = data_end;
	ctx->fde = locate_fde_for_pc(pc, ft_enc, pi,  pj, reloc, base);
	return ((void *)(ctx->fde));
}

static void *
locate_fde_for_pc(uint64_t pc, int enc,
	unsigned char *table_bg, unsigned char *table_end,
	ptrdiff_t reloc, uintptr_t base)
{
	unsigned char *pi = table_bg;
	unsigned char *pj = table_end;
	uint64_t range_start, range_end;
	void* fde;
	int log_size = table_ent_log_size(enc);

	/*
	 * Invariant -- if there is a containing range,
	 * it must lie in the interval [pi,pj).  That is,
	 * pi <= p < pj, if p exists.
	 */
	while (pi < pj) {
		unsigned char *pr =
		    pi + (((pj - pi) >> (log_size + 1)) << log_size);
				/* Don't use (pi+pj)>>1 */
		get_table_ent_val(pr, table_end, enc, reloc, base,
		    &range_start, &range_end, &fde);

		/* Return fde if tpc is in this range. */

		if (range_start <= pc && pc < range_end) {
			return ((void*) fde);
		}

		if (range_start < pc)
			pi = pr + (1 << log_size);
		else
			pj = pr;
	}
	return (0);
}