#    This file is part of Metasm, the Ruby assembly manipulation suite
#    Copyright (C) 2006-2009 Yoann GUILLOT
#
#    Licence is LGPL, see LICENCE in the top-level directory


# metasm debugger plugin
# adds a 'trace_func' method to the debugger
# the methods sets a breakpoint at the beginning of a function, and logs the execution of the instruction blocks
# does not descend in subfunctions

# setup the initial breakpoint at func start
def trace_func(addr)
	counter = 0
	bp = bpx(addr) { |h|
		counter += 1
		id = [disassembler.normalize(addr), counter, @cpu.dbg_func_retaddr(self)]
		trace_func_newtrace(id)
		trace_func_block(id)
		continue if h[:pre_state] == 'continue'
	}
	bp.action.call({}) if addr == pc
end

# we hit the beginning of a block we want to trace
def trace_func_block(id)
	blockaddr = pc
	if b = trace_get_block(blockaddr)
		trace_func_add_block(id, blockaddr)
		if b.list.length == 1
			trace_func_blockend(id, blockaddr)
		else
			bpx(b.list.last.address, true) { |h|
				finished = trace_func_blockend(id, blockaddr)
				continue if h[:pre_state] == 'continue' and not finished
			}
		end
	else
		# invalid opcode ?
		trace_func_blockend(id, blockaddr)
	end
end

# we are at the end of a traced block, find whats next
def trace_func_blockend(id, blockaddr)
	if di = disassembler.di_at(pc)
		if @cpu.dbg_end_stepout(self, di.address, di) and trace_func_istraceend(id, di)
			# trace ends there
			trace_func_finish(id)
			return true
		elsif di.opcode.props[:saveip] and not trace_func_entersubfunc(id, di)
			# call to a subfunction
			bpx(di.next_addr, true) { |h|
				trace_func_block(id)
				continue if h[:pre_state] == 'continue'
			}
		else
			singlestep	# XXX would need a callback on singlestep completion (to avoid multithread/exception)
			wait_target
			newaddr = pc
			trace_func_block(id)

			trace_func_linkdasm(di.address, newaddr)
		end
	else
		# XXX should link in the dasm somehow
		singlestep
		wait_target
		trace_func_block(id)
	end
	false
end

# retrieve an instructionblock, disassemble if needed
def trace_get_block(addr)
	# TODO trace all blocks from addr for which we know the target, stop on call / jmp [foo]
	disassembler.disassemble_fast_block(addr)
	if di = disassembler.di_at(addr)
		di.block
	end
end

# update the blocks links in the disassembler
def trace_func_linkdasm(from_addr, new_addr)
	di = disassembler.di_at(from_addr)
	ndi = disassembler.di_at(new_addr)

	return if not di

	# is it a subfunction return ?
	if @cpu.dbg_end_stepout(self, di.address, di) and cdi = (1..8).map { |i|
				disassembler.di_at(new_addr - i)
			}.compact.find { |cdi_|
				cdi_.opcode.props[:saveip] and cdi_.next_addr == new_addr
			}
		cdi.block.add_to_subfuncret new_addr
		ndi.block.add_from_subfuncret cdi.address if ndi
		cdi.block.each_to_normal { |f|
			disassembler.function[f] ||= DecodedFunction.new if disassembler.di_at(f)
		}
	else
		di.block.add_to_normal new_addr
		ndi.block.add_from_normal from_addr if ndi
	end
end

################################################################################################
# you can redefine the following functions in another plugin to handle trace events differently

# a new trace is about to begin
def trace_func_newtrace(id)
	@trace_func_counter ||= {}
	@trace_func_counter[id] = 0

	puts "start tracing #{Expression[id[0]]}"

	# setup a bg_color_callback on the disassembler
	if not defined? @trace_func_dasmcolor
		@trace_func_dasmcolor = true
		return if not disassembler.gui
		oldcb = disassembler.gui.bg_color_callback
		disassembler.gui.bg_color_callback = lambda { |addr|
			if oldcb and c = oldcb[addr]
				c
			elsif di = disassembler.di_at(addr) and di.block.list.first.comment.to_s =~ /functrace/
				'ff0'
			end
		}
	end
end

# a new block is added to a trace
def trace_func_add_block(id, blockaddr)
	@trace_func_counter[id] += 1
	if di = disassembler.di_at(blockaddr)
		di.add_comment "functrace #{@trace_func_counter[id]}"
	end
end

# the trace is finished
def trace_func_finish(id)
	puts "finished tracing #{Expression[id[0]]}"
end

def trace_subfuncs=(v) @trace_subfuncs = v end
def trace_subfuncs; @trace_subfuncs ||= false end

# the tracer is on a end-of-func instruction, should the trace end ?
def trace_func_istraceend(id, di)
	if trace_subfuncs
		if target = disassembler.get_xrefs_x(di)[0]
			# check the current return address against the one saved at trace start
			resolve(disassembler.normalize(target)) == id[2]
		end
	else
		true
	end
end

# the tracer is on a subfunction call instruction, should it trace into or stepover ?
def trace_func_entersubfunc(id, di)
	if trace_subfuncs
		@trace_func_subfunccache ||= {}
		if not target = @trace_func_subfunccache[di.address]
			# even if the target is dynamic, its module should be static
			if target = disassembler.get_xrefs_x(di)[0]
				@trace_func_subfunccache[di.address] =
				target = resolve(disassembler.normalize(target))
			end
		end
		# check if the target subfunction is in the same module as the main
		# XXX should check against the list of loaded modules etc
		# XXX call thunk_foo -> jmp [other_module]
		true if target.kind_of? Integer and target & 0xffc0_0000 == id[0] & 0xffc0_0000
	else
		false
	end
end

if gui
	gui.new_command('trace_func', 'trace execution inside a target function') { |arg| trace_func arg }
	gui.new_command('trace_now', 'trace til the end of the current function') { trace_func pc ; gui.wrap_run { continue } }
	gui.new_command('trace_subfunctions', 'define if the tracer should enter subfunctions') { |arg|
		case arg.strip
		when 'on', '1', 'yes', 'y'; @trace_subfuncs = true
		else @trace_subfuncs = false
		end
		puts "#{'not ' if not @trace_subfuncs}tracing subfunctions"
	}
end
