class Metasm::GdbClient

lowlevel interface to the gdbserver protocol

Constants

GDBREGS_IA32
GDBREGS_X64
RLE_RE

Attributes

cpu[RW]
gdbregs[RW]
io[RW]
logger[RW]
ptrsz[RW]
quiet[RW]

Public Class Methods

new(io, cpu='Ia32') click to toggle source
# File metasm/os/gdbremote.rb, line 243
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 /^ser:(.*)/i; @io = File.open($1, 'rb+')
        when /^udp:\[?(.*)\]?:(.*?)$/i; @io = UDPSocket.new ; @io.connect($1, $2)
        when /^(?:tcp:)?\[?(..+)\]?:(.*?)$/i; @io = TCPSocket.open($1, $2)
        else raise "unknown target #{io.inspect}"
        end

        gdb_setup
end

Public Instance Methods

break() click to toggle source
# File metasm/os/gdbremote.rb, line 225
def break
        @io.write("\3")
end
check_target(timeout=0) click to toggle source
# File metasm/os/gdbremote.rb, line 294
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
continue() click to toggle source
# File metasm/os/gdbremote.rb, line 217
def continue
        gdb_send('c')
end
detach() click to toggle source
# File metasm/os/gdbremote.rb, line 233
def detach
        gdb_send('D')
end
gdb_csum(buf) click to toggle source

compute the hex checksum used in gdb protocol

# File metasm/os/gdbremote.rb, line 18
def gdb_csum(buf)
        '%02x' % (buf.unpack('C*').inject(0) { |cs, c| cs + c } & 0xff)
end
gdb_msg(*a) click to toggle source
# File metasm/os/gdbremote.rb, line 134
def gdb_msg(*a)
        gdb_readresp if gdb_send(*a)
end
gdb_readresp(timeout=nil, outstr=nil) click to toggle source

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

# File metasm/os/gdbremote.rb, line 65
def gdb_readresp(timeout=nil, outstr=nil)
        @recv_ctx ||= {}
        @recv_ctx[:state] ||= :nosync
        buf = nil

        while @recv_ctx
                if !@recv_ctx[:rbuf]
                        return unless IO.select([@io], nil, nil, timeout)
                        if @io.kind_of?(UDPSocket)
                                raise Errno::EPIPE if not @recv_ctx[:rbuf] = @io.recvfrom(65536)[0]
                        else
                                raise Errno::EPIPE if not c = @io.read(1)
                        end
                end
                if @recv_ctx[:rbuf]
                        c = @recv_ctx[:rbuf].slice!(0, 1)
                        @recv_ctx.delete :rbuf if @recv_ctx[:rbuf] == ''
                end

                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 { |o| log 'gdb: ' + o } if first
                return ret
        end

        log "gdb_readresp: got #{buf[0, 64].inspect}#{'...' if buf.length > 64}" if $DEBUG
        buf
end
gdb_send(cmd, buf='') click to toggle source

send the buffer, waits ack return true on success

# File metasm/os/gdbremote.rb, line 24
def gdb_send(cmd, buf='')
        buf = cmd + buf
        buf = '$' << buf << '#' << gdb_csum(buf)
        log "gdb_send #{buf.inspect}" if $DEBUG

        5.times {
                @io.write buf
                out = ''
                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 '-'
                                log "gdb_send: ack neg" if $DEBUG
                                break
                        when nil
                                return
                        else
                                out << ack
                        end
                end
                log "no ack, got #{out.inspect}" if out != ''
        }

        log "send error #{cmd.inspect} (no ack)"
        false
end
gdb_setup() click to toggle source
# File metasm/os/gdbremote.rb, line 260
def gdb_setup
        pnd = ''
        pnd << @io.read(1) while IO.select([@io], nil, nil, 0.2)
        log "startpending: #{pnd.inspect}" if pnd != ''

        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
getmem(addr, len) click to toggle source

read memory (small blocks prefered)

# File metasm/os/gdbremote.rb, line 203
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
hex(buf) click to toggle source

send a binary buffer as a rle hex-encoded

# File metasm/os/gdbremote.rb, line 169
def hex(buf) buf.unpack('H*').first end
hexl(int) click to toggle source

send an integer as a long hex packed with leading 0 stripped

# File metasm/os/gdbremote.rb, line 167
def hexl(int) @pack_netint[[int]].unpack('H*').first.sub(/^0+(.)/, '\1') end
kill() click to toggle source
# File metasm/os/gdbremote.rb, line 229
def kill
        gdb_send('k')
end
log(s) click to toggle source
# File metasm/os/gdbremote.rb, line 317
def log(s)
        puts s if $DEBUG and logger
        return if quiet
        logger ? logger.log(s) : puts(s)
end
quiet_during() { || ... } click to toggle source
# File metasm/os/gdbremote.rb, line 54
def quiet_during
        pq = quiet
        @quiet = true
        yield
ensure
        @quiet = pq
end
rcmd(cmd) click to toggle source

monitor, aka remote command

# File metasm/os/gdbremote.rb, line 238
def rcmd(cmd)
        gdb_msg('qRcmd,' + hex(cmd))
end
read_regs() click to toggle source

retrieve remote regs

# File metasm/os/gdbremote.rb, line 178
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 ! (#{regs.length} < #{@regmsgsize})"
                        end
                end
                Hash[*@gdbregs.zip(@unpack_int[regs]).flatten]
        end
end
request_symbol(name) click to toggle source

use qSymbol to retrieve a symbol value (uint)

# File metasm/os/gdbremote.rb, line 287
def request_symbol(name)
        resp = gdb_msg('qSymbol:', hex(name))
        if resp and a = resp.split(':')[1]
                @unpack_netint[unhex(a)].first
        end
end
rle(buf) click to toggle source

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 '$'

# File metasm/os/gdbremote.rb, line 158
def rle(buf)
        buf.gsub(RLE_RE) {
                chr, len = $1, $2.length+1
                chr + '*' + (len+28).chr
        }
end
send_regs(r = {}) click to toggle source

send the reg values

# File metasm/os/gdbremote.rb, line 196
def send_regs(r = {})
        return if r.empty?
        regs = r.values_at(*@gdbregs)
        gdb_msg('G', hex(@pack_int[regs]))
end
set_hwbp(type, addr, len=1, set=true) click to toggle source
# File metasm/os/gdbremote.rb, line 275
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
setmem(addr, data) click to toggle source

write memory (small blocks prefered)

# File metasm/os/gdbremote.rb, line 211
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
setup_arch(cpu) click to toggle source

setup the various function used to pack ints & the reg list according to a target CPU

# File metasm/os/gdbremote.rb, line 328
def setup_arch(cpu)
        @ptrsz = cpu.size

        case cpu.shortname
        when /^ia32/
                @ptrsz = 32
                @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
        when 'arm64'
                @gdbregs = cpu.dbg_register_list
                @regmsgsize = 8 * @gdbregs.length
        when 'mips'
                @gdbregs = cpu.dbg_register_list
                @regmsgsize = cpu.size/8 * @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 @ptrsz
        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 #{@ptrsz}"
        end

        # if target cpu is bigendian, use netint everywhere
        if cpu.endianness == :big
                @pack_int   = @pack_netint
                @unpack_int = @unpack_netint
        end
end
singlestep() click to toggle source
# File metasm/os/gdbremote.rb, line 221
def singlestep
        gdb_send('s')
end
unhex(buf) click to toggle source

decode an rle hex-encoded buffer

# File metasm/os/gdbremote.rb, line 171
def unhex(buf)
        buf = buf[/^[a-fA-F0-9]*/]
        buf = '0' + buf if buf.length & 1 == 1
        [buf].pack('H*')
end
unrle(buf) click to toggle source

decompress rle-encoded data

# File metasm/os/gdbremote.rb, line 165
def unrle(buf) buf.gsub(/(.)\*(.)/) { $1 * ($2.unpack('C').first-28) } end
unset_hwbp(type, addr, len=1) click to toggle source
# File metasm/os/gdbremote.rb, line 282
def unset_hwbp(type, addr, len=1)
        set_hwbp(type, addr, len, false)
end