#!/bin/sh

# Copyright (c) 2003-2013 Aleksey Cheusov <vle@gmx.net>
# 
# 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.

set -e

. pipestatus

LC_ALL=C
export LC_ALL

libdir=/usr/pkg/lib
sysconfdir=/usr/pkg/etc

: ${LMDBG_SOEXT:=so}
: ${LMDBG_LIB:=$libdir/liblmdbg.$LMDBG_SOEXT}
: ${LMDBG_GDB:=gdb}

usage (){
    cat <<'EOF'
lmdbg-sym analyses lmdbg-run's output and converts
function addresses to source code position.
Set LMDBG_LIB environment variable to liblmdbg.so
The default is $libdir/liblmdbg.so.

usage:
    lmdbg-sym [OPTIONS] [files...]
OPTIONS:
    -h        displays this screen
    -V        display version
    -g        use 'gdb' for resolving (the default).
    -a        use 'addr2line' for resolving. By default 'gdb' is used.
    -s <filename>:<flags>
              dlopen(3) specified libraries with specified flags.
              Possible flags are: RTLD_LAZY, RTLD_NOW and RTLD_GLOBAL
    -P <prog> path to program
EOF
}

version (){
    cat <<'EOF'
lmdbg-sym 1.3.0
EOF
}

additional_libs=

with_gdb=1

while getopts hVgaps:P: f; do
    case $f in
	h)
	    usage
	    exit 0;;
	V)
	    version
	    exit 0;;
	g)
	    with_gdb=1;;
	a)
	    with_gdb='';;
	p)
	    true;; # ignored for backward compatibility
	P)
	    prog="$OPTARG";;
        s)
	    additional_libs="${additional_libs},$OPTARG";;
	'?')
	    usage 1>&2
	    exit 1;;
    esac
done
shift $(expr $OPTIND - 1)

tmp_dir="/tmp/lmdbg-sym.$$"
trap "rm -rf $tmp_dir" 0 1 2 15
mkdir -m 700 "$tmp_dir"

input=$tmp_dir/0
tmp_file1=$tmp_dir/1
tmp_file3=$tmp_dir/3
pid_file=$tmp_dir/4
gdb_cmds_file=$tmp_dir/5
info_sections_fn=$tmp_dir/6

if test $# -ne 1; then
    cat "$@" > "$input"
else
    input="$*"
fi

if test -z "$prog"; then
    prog=`awk '/^info progname / {print $3; exit}' $input`
fi

test $? || exit 3

collect_address (){
    # input: text files in lmdbg-run format
    # output: all addresses (uniqued) mensioned in input

    /usr/bin/awk '
$0 ~ /^ / && NF == 1 {
    hash [$1] = ""
}
END {
    for (addr in hash){
	print addr
    }
}' "$@"
}

generate_gdb_commands (){
    # input: addresses, one per line
    # output: gdb command for converting addresses to source position

    echo 'set width 0'
    echo 'break main'
    echo 'set print demangle on'
    echo "set environment LD_PRELOAD=$LMDBG_LIB $LMDBG_ENV"
#   echo "set environment LMDBG_PIDFILE=$pid_file"
    if test "$additional_libs"; then
	echo "set environment LMDBG_ADD_LIBS=$additional_libs"
    fi
    echo 'run'
#   echo 'p print_pid()'
#   echo 'p lmdbg_dlopen_add_libs()'
    echo 'print/x 0x22552255'

    if grep '^info section ' $input > "$info_sections_fn"; then
	/usr/bin/awk '
	BEGIN {
	    br_cnt = 2
	    sect_num = 0;
	}
	$1 == "info" && $2 == "section" {
	    sub(/^(0x)?0*/, "", $3)
	    sub(/^(0x)?0*/, "", $4)

	    addr = sprintf("%16s", $3)
	    gsub(/ /, "0", addr)
	    beg [sect_num] = (addr "")

	    addr = sprintf("%16s", $4)
	    gsub(/ /, "0", addr)
	    end [sect_num] = (addr "")

	    ++sect_num
	    next
	}
	{
	    sub(/^(0x)?0*/, "")

	    addr = sprintf("%16s", $0)
	    gsub(/ /, "0", addr)

	    for (sect=0; sect < sect_num; ++sect){
#		print addr, sect, beg [sect], end [sect] > "/dev/stderr"

		if (addr >= beg [sect] && addr < end [sect])
		    break
	    }
	    if (sect == sect_num)
		next

	    printf "print/x 0x%s\n", $0
	    printf "break *lmdbg_get_addr(0x%s, 0x%s, %d)\n", $0, beg [sect], sect
	    printf "info break %s\n", br_cnt
	    printf "delete %s\n", br_cnt
	    ++br_cnt
	}' "$info_sections_fn" "$@"
    else
	/usr/bin/awk '{
	    if ($0 !~ /^0x/)
		$0 = "0x" $0 # This is necessary for Solaris
	    print "b *" $0 "\ninfo break " NR+1
	}' "$@"
    fi
    echo 'quit'
}

gdb_output2position (){
    /usr/bin/awk '
    !flag {
	flag = /22552255/
	next
    }
    $1 ~ /^[$]/ && $2 == "=" {
	addr = $3
    }
    $1 == "Breakpoint" { # result of `b *<address>` command
	if (!addr){
	    addr = $4
	    sub(/:+$/, "", addr)
	}
	printf "%s\t", addr
	for (i=1; i <= NF; ++i){
	    if ($i == "file"){
		gsub(/[,.]$/, "", $(i+1))
		printf "%s:", $(i+1)
	    }
	    if ($i == "line"){
		gsub(/[,.]$/, "", $(i+1))
		printf "%s", $(i+1)
	    }
	}

	addr = ""
	next
    }
    $2 == "breakpoint" { # result of `info break <NUM>` command
	if (match($0, / in .* at /)){
	    print "\t" substr($0, RSTART+4, RLENGTH-8)
	}else{
	    print ""
	}

	next
    }
    ' "$@"
}

address2position_gdb (){
    # input: addresses, one per line
    # output: source code positions, one per line

    generate_gdb_commands "$@" > "$gdb_cmds_file"

    runpipe $LMDBG_GDB -nx -q -batch -x "$gdb_cmds_file" "$prog" '|' gdb_output2position

    if test $pipestatus_1 -ne 0; then
	echo "WARNINGS: gdb exited with exit status $pipestatus_1" 1>&2
	return $pipestatus_1
    fi
}

address2position_addr2line_nomaps (){
    # input: addresses, one per line
    # output: source code positions, one per line

    info_fn=$tmp_dir/info
    xargs addr2line -e "$prog" < "$tmp_file1" > "$info_fn"
    /usr/bin/awk -v tmp_file1="$tmp_file1" -v info_fn="$info_fn" '
    BEGIN {
	while ((getline addr < tmp_file1) > 0 &&
	        (getline info < info_fn) > 0)\
	{
	    print addr "\t" info
        }
    }
    '
}

#address2position_addr2line_maps (){
#    # input: addresses, one per line
#    # output: source code positions, one per line
#    grep 'info section ' "$input"
#}

collect_address $input > "$tmp_file1"

if test "_$with_gdb" = "_1"; then
    # command file for GDB
    address2position_gdb "$tmp_file1"
#elif grep '^info section' "$input" > /dev/null; then
#    address2position_addr2line_maps "$tmp_file1"
else
    # ADDR2LINE 
    address2position_addr2line_nomaps "$tmp_file1"
fi > $tmp_file3

#cat $tmp_file3
#exit

/usr/bin/awk -v tmp3=$tmp_file3 '
BEGIN {
    FS="\t"
    while (0 < (getline < tmp3)){
	if (NF > 1 && $2 != "??:0"){
	    addr = $1
	    sub(/^[^\t]*\t/, "", $0)
	    h [addr] = $0
	}
    }
    FS = " "
}
NF == 1 && $0 ~ /^ / {
    if ($1 !~ /^0x/)
	$1 = "0x" $1 # This is necessary for Solaris

    if ($1 in h){
	print " " $1 "\t" h [$1]
    }else{
	print " " $1
    }

    next
}
$1 == "info" && $2 == "section" { next }
{
    print $0
}' $input
