Class: Ragweed::Debuggerosx

Inherits:
Object show all
Includes:
Ragweed
Defined in:
lib/ragweed/debuggerosx.rb

Overview

Debugger class for Mac OS X You can use this class in 2 ways:

(1) You can create instances of Debuggerosx and use them to set and handle

breakpoints.

(2) If you want to do more advanced event handling, you can subclass from

debugger and define your own on_whatever events. If you handle an event
that Debuggerosx already handles, call "super", too.

Defined Under Namespace

Classes: Breakpoint

Constant Summary

Constants included from Ragweed

LIBPATH, PATH, VERSION

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Ragweed

libpath, path, require_all_libs_relative_to, require_os_libs_relative_to, version

Constructor Details

#initialize(p, opts = {}) ⇒ Debuggerosx

init object p: pid of process to be debugged opts: default options for automatically doing things (attach, install, and hook)



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/ragweed/debuggerosx.rb', line 75

def initialize(p,opts={})
  if p.kind_of? Numeric
    @pid = p
  else
    # coming soon: find process by name
    raise "Provide a PID"
  end
  @opts = opts
  default_opts(opts)

  @installed = false
  @attached = false
  @hooked = false
  @breakpoints = Hash.new do |h, k|
    bps = Array.new
    def bps.call(*args); each {|bp| bp.call(*args)}; end
    def bps.install; each {|bp| bp.install}; end
    def bps.uninstall; each {|bp| bp.uninstall}; end
    def bps.orig; each {|bp| dp.orig}; end
    h[k] = bps
  end
  @opts.each {|k, v| try(k) if v}
end

Instance Attribute Details

#breakpointsObject

Returns the value of attribute breakpoints.



21
22
23
# File 'lib/ragweed/debuggerosx.rb', line 21

def breakpoints
  @breakpoints
end

#exitedObject (readonly)

Returns the value of attribute exited.



20
21
22
# File 'lib/ragweed/debuggerosx.rb', line 20

def exited
  @exited
end

#pidObject (readonly)

Returns the value of attribute pid.



17
18
19
# File 'lib/ragweed/debuggerosx.rb', line 17

def pid
  @pid
end

#statusObject (readonly)

Returns the value of attribute status.



18
19
20
# File 'lib/ragweed/debuggerosx.rb', line 18

def status
  @status
end

#taskObject (readonly)

Returns the value of attribute task.



19
20
21
# File 'lib/ragweed/debuggerosx.rb', line 19

def task
  @task
end

Instance Method Details

#attach(opts = @opts) ⇒ Object

attach to @pid for debugging opts is a hash for automatically firing other functions as an overide for @opts returns 0 on no error



227
228
229
230
231
232
233
234
235
# File 'lib/ragweed/debuggerosx.rb', line 227

def attach(opts=@opts)
  r = Ragweed::Wraposx::ptrace(Ragweed::Wraposx::Ptrace::ATTACH,@pid,0,0)
  # Ragweed::Wraposx::ptrace(Ragweed::Wraposx::Ptrace::CONTINUE,@pid,1,0)
  @attached = true
  on_attach
  self.hook(opts) if (opts[:hook] and not @hooked)
  self.install_bps if (opts[:install] and not @installed)
  return r.first
end

#attached?Boolean

Returns:

  • (Boolean)


417
# File 'lib/ragweed/debuggerosx.rb', line 417

def attached?; @attached; end

#breakpoint_clear(ip, bpid = nil) ⇒ Object

removes breakpoint from child process ip: insertion point of breakpoints to be removed bpid: id of breakpoint to be removed



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/ragweed/debuggerosx.rb', line 303

def breakpoint_clear(ip, bpid=nil)
  if not bpid
    @breakpoints[ip].uninstall
    @breakpoints.delete ip
  else
    found = nil
    @breakpoints[ip].each_with_index do |bp, i|
      if bp.bpid == bpid
        found = i
        if bp.orig != Breakpoint::INT3
          if @breakpoints[ip][i+1]
            @breakpoints[ip][i + 1].orig = bp.orig
          else
            bp.uninstall
          end
        end
      end
    end
    raise "couldn't find #{ ip }" if not found
    @breakpoints[ip].delete_at(found) if found
  end
end

#breakpoint_set(ip, name = "", callable = nil, &block) ⇒ Object

adds a breakpoint and callable block to be installed into child process ip: address of insertion point callable: object to receive call() when this breakpoint is hit



293
294
295
296
297
298
# File 'lib/ragweed/debuggerosx.rb', line 293

def breakpoint_set(ip, name="", callable=nil, &block)
  if not callable and block_given?
    callable = block
  end
  @breakpoints[ip] << Breakpoint.new(self, ip, callable, name)
end

#continue(addr = 1, data = 0) ⇒ Object

continue stopped child process. addr: address from which to continue child. defaults to current position. data: signal to be sent to child. defaults to no signal.



395
396
397
# File 'lib/ragweed/debuggerosx.rb', line 395

def continue(addr = 1, data = 0)
  Ragweed::Wraposx::ptrace(Ragweed::Wraposx::Ptrace::CONTINUE,@pid,addr,data)
end

#detach(opts = @opts) ⇒ Object

remove breakpoints and release child opts is a hash for automatically firing other functions as an overide for @opts returns 0 on no error



240
241
242
243
244
245
246
247
# File 'lib/ragweed/debuggerosx.rb', line 240

def detach(opts=@opts)
  self.uninstall_bps if @installed
  r = Ragweed::Wraposx::ptrace(Ragweed::Wraposx::Ptrace::DETACH,@pid,0,Ragweed::Wraposx::Wait::UNTRACED)
  @attached = false
  on_detach
  self.unhook(opts) if opts[:hook] and @hooked
  return r.first
end

#get_heap_rangesObject



475
476
477
# File 'lib/ragweed/debuggerosx.rb', line 475

def get_heap_ranges
  get_mapping_by_name "MALLOC", false
end

#get_mapping_by_name(name, exact = true) ⇒ Object

XXX watch this space for an object to hold this information Get memory ranges by mapping name name: name of memory range to search for exact: if true require an exact match, otherwise use regex



452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/ragweed/debuggerosx.rb', line 452

def get_mapping_by_name name, exact = true
  ret = []
  IO.popen("vmmap -interleaved #{@pid}") do |pipe|
    pipe.each_line do |line|
      next if pipe.lineno < 5
      break if line == "==== Legend\n"
      rtype, saddr, eaddr, sz, perms, sm, purpose =
        line.scan(/^([[:graph:]]+(?:\s[[:graph:]]+)?)\s+([[:xdigit:]]+)-([[:xdigit:]]+)\s+\[\s+([[:digit:]]+[A-Z])\s*\]\s+([-rwx\/]+)\s+SM=(COW|PRV|NUL|ALI|SHM|ZER|S\/A)\s+(.*)$/).
          first
      if exact && (rtype == name || purpose == name)
        ret << [saddr, eaddr].map{|x| x.to_i(16)}
      elsif rtype && purpose && (rtype.match(name) || purpose.match(name))
        ret << [saddr, eaddr].map{|x| x.to_i(16)}
      end
    end
  end
  ret
end

#get_registers(thread = nil) ⇒ Object

returns a Ragweed::Wraposx::ThreadContext object containing the register states thread: thread to get the register state of



378
379
380
381
# File 'lib/ragweed/debuggerosx.rb', line 378

def get_registers(thread=nil)
  thread ||= self.threads.first
  Ragweed::Wraposx.thread_get_state(thread, Ragweed::Wraposx::ThreadContext::X86_THREAD_STATE)
end

#get_stack_rangesObject



471
472
473
# File 'lib/ragweed/debuggerosx.rb', line 471

def get_stack_ranges
  get_mapping_by_name "Stack", false
end

#hook(opts = @opts) ⇒ Object Also known as: attach_mach

Deprecated.
  • This function will change to attach_mach and instead become similar to the Debugger32#hook function used for tracing the entry and exit of functions in the child.

get task port for @pid and store in @task so mach calls can be made opts is a hash for automatically firing other functions as an overide for @opts returns the task port for @pid



253
254
255
256
257
258
# File 'lib/ragweed/debuggerosx.rb', line 253

def hook(opts=@opts)
  @task = Ragweed::Wraposx::task_for_pid(@pid)
  @hooked = true
  self.attach(opts) if opts[:attach] and not @attached
  return @task
end

#hooked?Boolean

Returns:

  • (Boolean)


416
# File 'lib/ragweed/debuggerosx.rb', line 416

def hooked?; @hooked; end

#install_bpsObject

installs all breakpoints into child process add breakpoints to install via breakpoint_set



208
209
210
211
212
213
214
# File 'lib/ragweed/debuggerosx.rb', line 208

def install_bps
  self.hook if not @hooked
  @breakpoints.each do |k,v|
    v.install
  end
  @installed = true
end

#installed?Boolean

Returns:

  • (Boolean)


418
# File 'lib/ragweed/debuggerosx.rb', line 418

def installed?; @installed; end

#kill(sig = 0) ⇒ Object

sends a signal to process with id @pid sig: signal to be sent to process @pid



286
287
288
# File 'lib/ragweed/debuggerosx.rb', line 286

def kill(sig = 0)
  Ragweed::Wraposx::kill(@pid,sig)
end

#loop(times = nil) ⇒ Object

loop calls to wait. This is the main mode this class will be used at runtime. First, install the desired breakpoints. Then, run loop().

times: number of times to loop

if nil this will loop until @exited is set


105
106
107
108
109
110
111
112
113
# File 'lib/ragweed/debuggerosx.rb', line 105

def loop(times=nil)
  if times.kind_of? Numeric
    times.times do
      self.wait
    end
  elsif times.nil?
    self.wait while not @exited
  end
end

#on_attachObject

Fired when attaching to the child process succeeds.



173
174
# File 'lib/ragweed/debuggerosx.rb', line 173

def on_attach
end

#on_breakpoint(thread) ⇒ Object

default method for breakpoint handling thread: id of the thread stopped at a breakpoint



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/ragweed/debuggerosx.rb', line 328

def on_breakpoint(thread)
  r = self.get_registers(thread)
  # rewind eip to correct position
  r.eip -= 1
  # don't use r.eip since it may be changed by breakpoint callback
  eip = r.eip
  # clear stuff set by INT3
  # r.esp -=4
  # r.ebp = r.esp
  # fire callback
  @breakpoints[eip].call(thread, r, self)
  if @breakpoints[eip].first.installed?
    # uninstall breakpoint to continue past it
    @breakpoints[eip].first.uninstall
    # set trap flag so we don't go too far before reinserting breakpoint
    r.eflags |= Ragweed::Wraposx::EFlags::TRAP
    # set registers to commit eip and eflags changes
    self.set_registers(thread, r)

    # step once
    self.stepp

    # now we wait() to prevent a race condition that'll SIGBUS us
    # Yup, a race condition where the child may not complete a single 
    # instruction before the parent completes many
    Ragweed::Wraposx::waitpid(@pid,0)

    # reset the breakpoint
    @breakpoints[eip].first.install
  end
end

#on_continueObject

Called when the child process is continued. If used by an implementation, this will be a very noisy function.



203
204
# File 'lib/ragweed/debuggerosx.rb', line 203

def on_continue
end

#on_detachObject

Fired when detaching from the child process succeeds.



177
178
# File 'lib/ragweed/debuggerosx.rb', line 177

def on_detach
end

#on_exit(status) ⇒ Object

Called with the child process’s status on exit Implementations overriding this function should either set @exited to true or call super.



187
188
189
# File 'lib/ragweed/debuggerosx.rb', line 187

def on_exit(status)
  @exited = true
end

#on_signal(signal) ⇒ Object

Called with the signal used to exit kill the child process Implementations overriding this function should either set @exited to true or call super.



193
194
195
# File 'lib/ragweed/debuggerosx.rb', line 193

def on_signal(signal)
  @exited = true
end

#on_single_stepObject

Fired when single stepping at every step Not currently used in OSX



182
183
# File 'lib/ragweed/debuggerosx.rb', line 182

def on_single_step
end

#on_stop(signal) ⇒ Object

Called when the child process is stopped with the signal used.



198
199
# File 'lib/ragweed/debuggerosx.rb', line 198

def on_stop(signal)
end

#region_info(addr, flavor = :basic) ⇒ Object

returns information about a memory region addr: address contained in the memory region - usually the start address flavor: type of information to retrieve. May be specified as either symbol [:basic, :extended, :top] or by integer flavor id.

Currently, only the basic flavor is supported by Apple.


424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/ragweed/debuggerosx.rb', line 424

def region_info(addr, flavor = :basic)
  flav = case flavor
  when :basic
    Ragweed::Wraposx::Vm::RegionBasicInfo::FLAVOR

  # Extended and Top info flavors are included in case Apple re implements them
  when :extended
    Ragweed::Wraposx::Vm::RegionExtendedInfo::FLAVOR
  when :top
    Ragweed::Wraposx::Vm::RegionTopInfo::FLAVOR
  when Integer
    flavor
  else
    warn "Unknown flavor requested. Returning RegionBasicInfo."
    Ragweed::Wraposx::RegionBasicInfo::FLAVOR
  end
  
  if Ragweed::Wraposx.respond_to? :vm_region_64
    Ragweed::Wraposx.vm_region_64(@task, addr, flav)
  else
    Ragweed::Wraposx.vm_region(@task, addr, flav)
  end
end

#resume(thread = nil) ⇒ Object

resumes thread that has been suspended via thread_suspend thread: thread id of thread to be resumed



272
273
274
275
# File 'lib/ragweed/debuggerosx.rb', line 272

def resume(thread = nil)
  thread ||= self.threads.first
  Ragweed::Wraposx::thread_resume(thread)
end

#resume_taskObject

decrement our tasks suspend count



367
368
369
# File 'lib/ragweed/debuggerosx.rb', line 367

def resume_task
  Ragweed::Wraposx::task_resume(@task)
end

#set_registers(thread, regs) ⇒ Object

sets the register state of a thread thread: thread id to set registers for regs: Ragweed::Wraposx::ThreadContext object containing the new register state for the thread



386
387
388
389
390
# File 'lib/ragweed/debuggerosx.rb', line 386

def set_registers(thread, regs)
  # XXX - needs updated conditions
  # raise "Must supply registers and thread to set" if (not (thread and regs) or not thread.kind_of? Numeric or not regs.kind_of? Ragweed::Wraposx::ThreadContext)
  Ragweed::Wraposx.thread_set_state(thread, regs.class::FLAVOR, regs)
end

#stepp(addr = 1, data = 0) ⇒ Object

Do not use this function unless you know what you’re doing! It causes a kernel panic in some situations (fine if the trap flag is set in theory) same arguments as Debugerosx#continue single steps the child process



403
404
405
# File 'lib/ragweed/debuggerosx.rb', line 403

def stepp(addr = 1, data = 0)
  Ragweed::Wraposx::ptrace(Ragweed::Wraposx::Ptrace::STEP,@pid,addr,data)
end

#suspend(thread = nil) ⇒ Object

suspends thread (increments the suspend count) thread: thread id of thread to be suspended defaults to first thread



279
280
281
282
# File 'lib/ragweed/debuggerosx.rb', line 279

def suspend(thread = nil)
  thread ||= self.threads.first
  Ragweed::Wraposx::thread_suspend(thread)
end

#suspend_taskObject

increment our tasks suspend count



372
373
374
# File 'lib/ragweed/debuggerosx.rb', line 372

def suspend_task
  Ragweed::Wraposx::task_suspend(@task)
end

#thread_update(thread = nil, sig = 0) ⇒ Object

sends a signal to a thread of the child’s this option to ptrace is undocumented in OS X, usage pulled from gdb and googling thread: id of thread to which a signal is to be sent sig: signal to be sent to child’s thread



411
412
413
414
# File 'lib/ragweed/debuggerosx.rb', line 411

def thread_update(thread = nil, sig = 0)
  thread = thread or self.threads.first
  Ragweed::Wraposx::ptrace(Ragweed::Wraposx::Ptrace::THUPDATE,@pid,thread,sig)
end

#threadsObject

returns an array of the thread ids of the child process



361
362
363
364
# File 'lib/ragweed/debuggerosx.rb', line 361

def threads
  self.hook if not @hooked
  Ragweed::Wraposx::task_threads(@task)
end

#unhook(opts = @opts) ⇒ Object

Deprecated.
  • This will be removed at some point.

theoretically to close the task port but, no way to close the port has yet been found. This function currently does little/nothing.



265
266
267
268
# File 'lib/ragweed/debuggerosx.rb', line 265

def unhook(opts=@opts)
  self.detach(opts) if opts[:attach] and @attached
  self.unintsall_bps if opts[:install] and @installed
end

#uninstall_bpsObject

removes all breakpoints from child process



217
218
219
220
221
222
# File 'lib/ragweed/debuggerosx.rb', line 217

def uninstall_bps
  @breakpoints.each do |k,v|
    v.uninstall
  end
  @installed = false
end

#wait(opts = 0) ⇒ Object

wait for process and run callback on return then continue child This is usually called by loop() FIXME - need to do signal handling better (loop through threads only for breakpoints and stepping) opts: option flags to waitpid(2)

returns an array containing the pid of the stopped or terminated child and the status of that child r: pid of stopped/terminated child or 0 if Ragweed::Wraposx::Wait:NOHANG was passed and there was nothing to report r: staus of child or 0 if Ragweed::Wraposx::Wait:NOHANG was passed and there was nothing to report



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ragweed/debuggerosx.rb', line 123

def wait(opts = 0)
  r = Ragweed::Wraposx::waitpid(@pid,opts)
  status = r[1]
  wstatus = status & 0x7f
  signal = status >> 8
  found = false
  if r[0] != 0 #r[0] == 0 iff wait had nothing to report and NOHANG option was passed
    case
    when wstatus == 0 #WIFEXITED
      @exited = true
      try(:on_exit, signal)
    when wstatus != 0x7f #WIFSIGNALED
      @exited = false
      try(:on_signaled, wstatus)
    when signal != 0x13 #WIFSTOPPED
      self.threads.each do |t|
        if @breakpoints.has_key?(self.get_registers(t).eip-1)
          found = true
          try(:on_breakpoint, t)
        end
      end
      if not found # no breakpoint so iterate through Signal constants to find the current SIG
        Signal.list.each do |sig, val|
          try("on_sig#{ sig.downcase }".intern) if signal == val
        end
      end
      try(:on_stop, signal)
      begin
        self.continue
      rescue Errno::EBUSY
        # Yes this happens and it's wierd
        # Not sure it should happen
        if $DEBUG
          puts 'unable to self.continue'
          puts self.get_registers
        end
      retry
      end
    when signal == 0x13 #WIFCONTINUED
      try(:on_continue)
    else
      raise "Unknown signal '#{signal}' recieved: This should not happen - ever."
    end
  end
  return r
end