#    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


require 'metasm/os/main'
require 'socket'

module Metasm
# lowlevel interface to the gdbserver protocol
class GdbClient
	GDBREGS_IA32 = %w[eax ecx edx ebx esp ebp esi edi eip eflags cs ss ds es fs gs].map { |r| r.to_sym }	# XXX [77] = 'orig_eax'
	GDBREGS_X64 = %w[rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 rip rflags cs ss ds es fs gs].map { |r| r.to_sym }

	# compute the hex checksum used in gdb protocol
	def gdb_csum(buf)
		'%02x' % (buf.unpack('C*').inject(0) { |cs, c| cs + c } & 0xff)
	end

	# send the buffer, waits ack
	# return true on success
	def gdb_send(cmd, buf='')
		buf = cmd + buf
		buf = '$' << buf << '#' << gdb_csum(buf)

		5.times {
			@io.write buf
			loop do
				break if not IO.select([@io], nil, nil, 0.2)
				raise Errno::EPIPE if not ack = @io.read(1)
				case ack
				when '+'
					return true
				when '-'
					puts "gdb_send: ack neg" if $DEBUG
					break
				when nil
					return
				end
			end
		}

		log "send error #{cmd.inspect} (no ack)"
		false
	end

	def quiet_during
		pq = quiet
		@quiet = true
		yield
	ensure
		@quiet = pq
	end

	# return buf, or nil on error / csum error
	# waits IO.select(timeout) between each char
	# outstr is used internally only to handle multiline output string
	def gdb_readresp(timeout=nil, outstr=nil)
		@recv_ctx ||= {}
		@recv_ctx[:state] ||= :nosync
		buf = nil

		while @recv_ctx
			return unless IO.select([@io], nil, nil, timeout)
			raise Errno::EPIPE if not c = @io.read(1)

			case @recv_ctx[:state]
			when :nosync
				if c == '$'
					@recv_ctx[:state] = :data
					@recv_ctx[:buf] = ''
				end
			when :data
				if c == '#'
					@recv_ctx[:state] = :csum1
					@recv_ctx[:cs] = ''
				else
					@recv_ctx[:buf] << c
				end
			when :csum1
				@recv_ctx[:cs] << c
				@recv_ctx[:state] = :csum2
			when :csum2
				cs = @recv_ctx[:cs] << c
				buf = @recv_ctx[:buf]
				@recv_ctx = nil
				if cs.downcase == gdb_csum(buf).downcase
					@io.write '+'
				else
					log "transmit error"
					@io.write '-'
					return
				end
			end
		end

		case buf
		when /^E(..)$/
			e = $1.to_i(16)
			log "error #{e} (#{PTrace::ERRNO.index(e)})"
			return
		when /^O([0-9a-fA-F]*)$/
			if not outstr
				first = true
				outstr = ''
			end
			outstr << unhex($1)
			ret = gdb_readresp(timeout, outstr)
			outstr.split("\n").each { |e| log 'gdb: ' + e } if first
			return ret
		end

		puts "gdb_readresp: got #{buf[0, 64].inspect}#{'...' if buf.length > 64}" if $DEBUG
		buf
	end

	def gdb_msg(*a)
		gdb_readresp if gdb_send(*a)
	end

	# rle: build the regexp that will match repetitions of a character, skipping counts leading to invalid char
	rng = [3..(125-29)]
	[?+, ?-, ?#, ?$].sort.each { |invalid|
		invalid = invalid.unpack('C').first if invalid.kind_of? String
		invalid -= 29
		rng.each_with_index { |r, i|
			if r.include? invalid
				replace = [r.begin..invalid-1, invalid+1..r.end]
				replace.delete_if { |r_| r_.begin > r_.end }
				rng[i, 1] = replace
			end
		}
	}
	repet = rng.reverse.map { |r| "\\1{#{r.begin},#{r.end}}" }.join('|')
	RLE_RE = /(.)(#{repet})/m

	# rle-compress a buffer
	# a character followed by '*' followed by 'x' is asc(x)-28 repetitions of the char
	# eg '0* ' => '0' * (asc(' ') - 28) = '0000'
	# for the count character, it must be 32 <= char < 126 and not be '+' '-' '#' or '$'
	def rle(buf)
		buf.gsub(RLE_RE) {
			chr, len = $1, $2.length+1
			chr + '*' + (len+28).chr
		}
	end
	# decompress rle-encoded data
	def unrle(buf) buf.gsub(/(.)\*(.)/) { $1 * ($2.unpack('C').first-28) } end
	# send an integer as a long hex packed with leading 0 stripped
	def hexl(int) @pack_netint[[int]].unpack('H*').first.sub(/^0+(.)/, '\\1') end
	# send a binary buffer as a rle hex-encoded
	def hex(buf) buf.unpack('H*').first end
	# decode an rle hex-encoded buffer
	def unhex(buf)
		buf = buf[/^[a-fA-F0-9]*/]
		buf = '0' + buf if buf.length & 1 == 1
		[buf].pack('H*')
	end

	# retrieve remote regs
	def read_regs
		if buf = gdb_msg('g')
			regs = unhex(unrle(buf))
			p @unpack_int[regs].map { |v| '%x' % v } if $DEBUG
			if regs.length < @regmsgsize
				# retry once, was probably a response to something else
				puts "bad regs size!" if $DEBUG
				buf = gdb_msg('g')
				regs = unhex(unrle(buf)) if buf
				if not buf or regs.length < @regmsgsize
					raise "regs buffer recv is too short !"
				end
			end
			Hash[*@gdbregs.zip(@unpack_int[regs]).flatten]
		end
	end

	# send the reg values
	def send_regs(r = {})
		return if r.empty?
		regs = r.values_at(*@gdbregs)
		gdb_msg('G', hex(@pack_int[regs]))
	end

	# read memory (small blocks prefered)
	def getmem(addr, len)
		return '' if len == 0
		if mem = quiet_during { gdb_msg('m', hexl(addr) << ',' << hexl(len)) } and mem != ''
			unhex(unrle(mem))
		end
	end

	# write memory (small blocks prefered)
	def setmem(addr, data)
		len = data.length
		return if len == 0
		raise 'writemem error' if not gdb_msg('M', hexl(addr) << ',' << hexl(len) << ':' << rle(hex(data)))
	end

	def continue
		gdb_send('c')
	end

	def singlestep
		gdb_send('s')
	end

	def break
		@io.write("\3")
	end

	def kill
		gdb_send('k')
	end

	def detach
		gdb_send('D')
	end

	# monitor, aka remote command
	def rcmd(cmd)
		gdb_msg('qRcmd,' + hex(cmd))
	end

	attr_accessor :io, :cpu, :gdbregs
	def initialize(io, cpu='Ia32')
		cpu = Metasm.const_get(cpu).new if cpu.kind_of? String
		raise 'unknown cpu' if not cpu.kind_of? CPU
		setup_arch(cpu)
		@cpu = cpu

		case io
		when IO; @io = io
		when /^udp:(.*):(.*?)$/i; @io = UDPSocket.new ; @io.connect($1, $2)
		when /^(?:tcp:)?(.*):(.*?)$/i; @io = TCPSocket.open($1, $2)	# XXX matches C:\fail
		# TODO pipe, serial port, etc ; also check ipv6
		else raise "unknown target #{io.inspect}"
		end

		gdb_setup
	end

	def gdb_setup
		gdb_msg('q', 'Supported')
		#gdb_msg('Hc', '-1')
		#gdb_msg('qC')
		if not gdb_msg('?')
			log "nobody on the line, waiting for someone to wake up"
			IO.select([@io], nil, nil, nil)
			log "who's there ?"
		end
	end

	def set_hwbp(type, addr, len=1, set=true)
		set = (set ? 'Z' : 'z')
		type = { 'r' => '3', 'w' => '2', 'x' => '1', 's' => '0' }[type.to_s] || raise("invalid bp type #{type.inspect}")
		gdb_msg(set, type << ',' << hexl(addr) << ',' << hexl(len))
		true
	end

	def unset_hwbp(type, addr, len=1)
		set_hwbp(type, addr, len, false)
	end

	# use qSymbol to retrieve a symbol value (uint)
	def request_symbol(name)
		resp = gdb_msg('qSymbol:', hex(name))
		if resp and a = resp.split(':')[1]
			@unpack_netint[unhex(a)].first
		end
	end

	def check_target(timeout=0)
		return if not msg = gdb_readresp(timeout)
		case msg[0]
		when ?S
			sig = unhex(msg[1, 2]).unpack('C').first
			{ :state => :stopped, :info => "signal #{sig} #{PTrace::SIGNAL[sig]}" }
		when ?T
			sig = unhex(msg[1, 2]).unpack('C').first
			ret = { :state => :stopped, :info => "signal #{sig} #{PTrace::SIGNAL[sig]}" }
			ret.update msg[3..-1].split(';').inject({}) { |h, s| k, v = s.split(':', 2) ; h.update k => (v || true) }	# 'thread' -> pid
		when ?W
			code = unhex(msg[1, 2]).unpack('C').first
			{ :state => :dead, :info => "exited with code #{code}" }
		when ?X
			sig = unhex(msg[1, 2]).unpack('C').first
			{ :state => :dead, :info => "signal #{sig} #{PTrace::SIGNAL[sig]}" }
		else
			log "check_target: unhandled #{msg.inspect}"
			{ :state => :unknown }
		end
	end

	attr_accessor :logger, :quiet
	def log(s)
		return if quiet
		@logger ||= $stdout
		@logger.puts s
	end


	# setup the various function used to pack ints & the reg list
	# according to a target CPU
	def setup_arch(cpu)
		case cpu.shortname
		when 'ia32'
			@gdbregs = GDBREGS_IA32
			@regmsgsize = 4 * @gdbregs.length
		when 'x64'
			@gdbregs = GDBREGS_X64
			@regmsgsize = 8 * @gdbregs.length
		when 'arm'
			@gdbregs = cpu.dbg_register_list
			@regmsgsize = 4 * @gdbregs.length
		else
			# we can still use readmem/kill and other generic commands
			# XXX serverside setregs may fail if we give an incorrect regbuf size
			puts "unsupported GdbServer CPU #{cpu.shortname}"
			@gdbregs = [*0..32].map { |i| "r#{i}".to_sym }
			@regmsgsize = 0
		end

		# yay life !
		# do as if cpu is littleendian, fixup at the end
		case cpu.size
		when 16
			@pack_netint   = lambda { |i| i.pack('n*') }
			@unpack_netint = lambda { |s| s.unpack('n*') }
			@pack_int   = lambda { |i| i.pack('v*') }
			@unpack_int = lambda { |s| s.unpack('v*') }
		when 32
			@pack_netint   = lambda { |i| i.pack('N*') }
			@unpack_netint = lambda { |s| s.unpack('N*') }
			@pack_int   = lambda { |i| i.pack('V*') }
			@unpack_int = lambda { |s| s.unpack('V*') }
		when 64
			bswap = lambda { |s| s.scan(/.{8}/m).map { |ss| ss.reverse }.join }
			@pack_netint   = lambda { |i| i.pack('Q*') }
			@unpack_netint = lambda { |s| s.unpack('Q*') }
			@pack_int   = lambda { |i| bswap[i.pack('Q*')] }
			@unpack_int = lambda { |s| bswap[s].unpack('Q*') }
			if [1].pack('Q')[0] == ?\1	# ruby interpreter littleendian
				@pack_netint, @pack_int = @pack_int, @pack_netint
				@unpack_netint, @unpack_int = @unpack_int, @unpack_netint
			end
		else raise "GdbServer: unsupported cpu size #{cpu.size}"
		end

		# if target cpu is bigendian, use netint everywhere
		if cpu.endianness == :big
			@pack_int   = @pack_netint
			@unpack_int = @unpack_netint
		end
	end
end

# virtual string to access the remote process memory
class GdbRemoteString < VirtualString
	attr_accessor :gdb

	def initialize(gdb, addr_start=0, length=nil)
		@gdb = gdb
		length ||= 1 << (@gdb.cpu.size rescue 32)
		@pagelength = 512
		super(addr_start, length)
	end

	def dup(addr=@addr_start, len=@length)
		self.class.new(@gdb, addr, len)
	end

	def rewrite_at(addr, data)
		len = data.length
		off = 0
		while len > @pagelength
			@gdb.setmem(addr+off, data[off, @pagelength])
			off += @pagelength
			len -= @pagelength
		end
		@gdb.setmem(addr+off, data[off, len])
	end

	def get_page(addr, len=@pagelength)
		@gdb.getmem(addr, len)
	end
end

# this class implements a high-level API using the gdb-server network debugging protocol
class GdbRemoteDebugger < Debugger
	attr_accessor :gdb, :check_target_timeout
	def initialize(url, cpu='Ia32')
		@gdb = GdbClient.new(url, cpu)
		@gdb.logger = self
		@cpu = @gdb.cpu
		@memory = GdbRemoteString.new(@gdb)
		@reg_val_cache = {}
		@regs_dirty = false
		# when checking target, if no message seen since this much seconds, send a 'status' query
		@check_target_timeout = 1
		super()
	end

	def invalidate
		sync_regs
		@reg_val_cache.clear
		super()
	end

	def get_reg_value(r)
		return @reg_val_cache[r] || 0 if @state != :stopped
		sync_regs
		@reg_val_cache = @gdb.read_regs || {} if @reg_val_cache.empty?
		@reg_val_cache[r] || 0
	end
	def set_reg_value(r, v)
		@reg_val_cache[r] = v
		@regs_dirty = true
	end

	def sync_regs
		@gdb.send_regs(@reg_val_cache) if @regs_dirty and not @reg_val_cache.empty?
		@regs_dirty = false
	end

	def do_check_target
		return if @state == :dead
		t = Time.now
		@last_check_target ||= t
		if @state == :running and t - @last_check_target > @check_target_timeout
			@gdb.io.write '$?#' << @gdb.gdb_csum('?')
			@last_check_target = t
		end
		return unless i = @gdb.check_target(0.01)
		invalidate if i[:state] == :stopped and @state != :stopped
		@state, @info = i[:state], i[:info]
		@info = nil if @info =~ /TRAP/
	end

	def do_wait_target
		return unless i = @gdb.check_target(nil)
		invalidate if i[:state] == :stopped and @state != :stopped
		@state, @info = i[:state], i[:info]
		@info = nil if @info =~ /TRAP/
	end

	def do_continue(*a)
		return if @state != :stopped
		@state = :running
		@info = 'continue'
		@gdb.continue
		@last_check_target = Time.now
	end

	def do_singlestep(*a)
		return if @state != :stopped
		@state = :running
		@info = 'singlestep'
		@gdb.singlestep
		@last_check_target = Time.now
	end

	def break
		@gdb.break
	end

	def kill(sig=nil)
		# TODO signal nr
		@gdb.kill
		@state = :dead
		@info = 'killed'
	end

	def detach
		super()	# remove breakpoints & stuff
		@gdb.detach
		@state = :dead
		@info = 'detached'
	end
	
	# set to true to use the gdb msg to handle bpx, false to set 0xcc ourself
	attr_accessor :gdb_bpx
	def enable_bp(addr)
		return if not b = @breakpoint[addr]
		b.state = :active
		case b.type
		when :bpx
			if gdb_bpx
				@gdb.set_hwbp('s', addr, 1)
			else
				@cpu.dbg_enable_bp(self, addr, b)
			end
		when :hw
			@gdb.set_hwbp(b.mtype, addr, b.mlen)
		end
	end

	def disable_bp(addr)
		return if not b = @breakpoint[addr]
		b.state = :inactive
		case b.type
		when :bpx
			if gdb_bpx
				@gdb.unset_hwbp('s', addr, 1)
			else
				@cpu.dbg_disable_bp(self, addr, b)
			end
		when :hw
			@gdb.unset_hwbp(b.mtype, addr, b.mlen)
		end
	end

	def check_pre_run(*a)
		sync_regs
		super(*a)
	end

	def loadallsyms
		puts 'loadallsyms unsupported'
	end

	def ui_command_setup(ui)
		ui.new_command('monitor', 'send a remote command to run on the target') { |arg| @gdb.rcmd(arg) }
	end
end
end
