Ptrace 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/Ptrace.rb ADDED
@@ -0,0 +1,571 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright 2011 Thoughtgang <http://www.thoughtgang.org>
3
+ # Ruby additions to Ptrace extension
4
+
5
+ require 'Ptrace_ext' # Load C extension wrapping ptrace(3)
6
+ require 'forwardable'
7
+
8
+ =begin rdoc
9
+ Note: Debugger is a class composed of singleton methods, defined in the C
10
+ extension module.
11
+ =end
12
+ module Ptrace
13
+
14
+ # -----------------------------------------------------------------------
15
+ =begin rdoc
16
+ =end
17
+ class Error < RuntimeError
18
+ end
19
+
20
+ =begin rdoc
21
+ "No such process" returned in errno.
22
+ =end
23
+ class InvalidProcessError < Error
24
+ end
25
+
26
+ =begin rdoc
27
+ "Operation not permitted" returned in errno.
28
+ =end
29
+ class OperationNotPermittedError < Error
30
+ end
31
+
32
+ =begin rdoc
33
+ Requested command is not listed in PTRACE_COMMANDS and is considered
34
+ unsupported.
35
+ =end
36
+ class OperationNotSupportedError < Error
37
+ end
38
+
39
+ # -----------------------------------------------------------------------
40
+ =begin rdoc
41
+ Hash mapping ptrace symbols to command numbers. This is filled by the
42
+ Debugger based on the #defines in sys/ptrace.h.
43
+ =end
44
+ PTRACE_COMMANDS = Debugger.commands
45
+
46
+ # -----------------------------------------------------------------------
47
+ =begin rdoc
48
+ =end
49
+ # all ptrace options
50
+ class Options
51
+ def initialize(pid)
52
+ @pid = pid
53
+ end
54
+
55
+ def write
56
+ opts = []
57
+
58
+ begin
59
+ Debugger.send_data( Debugger.commands[:set_options], @pid, nil, opts )
60
+ rescue RuntimeError => e
61
+ case e.message
62
+ when 'PTRACE: Operation not permitted'
63
+ raise OperationNotPermittedError.new(e.message)
64
+ when 'PTRACE: No such process'
65
+ raise InvalidProcessError.new(e.message)
66
+ else
67
+ raise
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ # -----------------------------------------------------------------------
74
+ =begin rdoc
75
+ A region of Memory in the target process.
76
+
77
+ Usage:
78
+ mem = MemArea.new(MemArea::DATA, pid)
79
+ val = mem.peek(0x0804100)
80
+ mem.poke(0x0804100, 0x0)
81
+ =end
82
+ class MemArea
83
+
84
+ =begin rdoc
85
+ Target user area.
86
+ =end
87
+ MEM_USER = 1
88
+ =begin rdoc
89
+ Target text (code) area.
90
+ =end
91
+ MEM_TEXT = 2
92
+ =begin rdoc
93
+ Target data area.
94
+ =end
95
+ MEM_DATA = 3
96
+
97
+ =begin rdoc
98
+ Valid memory region types.
99
+ =end
100
+ TYPES = [MEM_USER, MEM_TEXT, MEM_DATA]
101
+
102
+ =begin rdoc
103
+ Type of memory region.
104
+ =end
105
+ attr_reader :mem_type
106
+ =begin rdoc
107
+ PID of process owning this memory region.
108
+ =end
109
+ attr_reader :pid
110
+
111
+ =begin rdoc
112
+ Create a new memory region of the specified type for process 'pid'.
113
+ =end
114
+ def initialize(type, pid)
115
+ @mem_type = type
116
+ @pid = pid
117
+ case type
118
+ when MEM_USER
119
+ @getter_sym = :peekusr
120
+ @setter_sym = :pokeusr
121
+ when MEM_TEXT
122
+ @getter_sym = :peektext
123
+ @setter_sym = :poketext
124
+ when MEM_DATA
125
+ @getter_sym = :peekdata
126
+ @setter_sym = :pokedata
127
+ end
128
+ end
129
+
130
+ =begin rdoc
131
+ Read a word of data from address 'addr' in memory region.
132
+ This can raise an OperationNotPermittedError if access is denied, or an
133
+ InvalidProcessError if the target process has exited.
134
+ =end
135
+ def peek(addr)
136
+ ptrace_send(:peek, @getter_sym, addr)
137
+ end
138
+
139
+ =begin rdoc
140
+ Write a word of data to address 'addr' in memory region.
141
+ This can raise an OperationNotPermittedError if access is denied, or an
142
+ InvalidProcessError if the target process has exited.
143
+ =end
144
+ def poke(addr, value)
145
+ ptrace_send(:poke, @setter_sym, addr, value)
146
+ end
147
+
148
+ private
149
+
150
+ def ptrace_send( sym, cmd, addr, arg=nil )
151
+ begin
152
+ raise OperationNotSupportedError if (not PTRACE_COMMANDS.include? cmd)
153
+ args = [PTRACE_COMMANDS[cmd], @pid, addr]
154
+ args << arg if arg
155
+ Debugger.send( sym, *args )
156
+ rescue RuntimeError => e
157
+ case e.message
158
+ when 'PTRACE: Operation not permitted'
159
+ raise OperationNotPermittedError.new(e.message)
160
+ when 'PTRACE: No such process'
161
+ raise InvalidProcessError.new(e.message)
162
+ else
163
+ raise
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ # -----------------------------------------------------------------------
170
+ =begin rdoc
171
+ =end
172
+ class Signal
173
+ =begin rdoc
174
+ =end
175
+ # create signinfo
176
+ def initialize()
177
+ end
178
+
179
+ =begin rdoc
180
+ =end
181
+ # send a signal to the child
182
+ def send
183
+ # TODO: begin/rescue
184
+ #Debugger.signal
185
+ end
186
+
187
+ =begin rdoc
188
+ =end
189
+ # read a signal from the child. returns a Signal data struct.
190
+ def self.get(pid)
191
+ # TODO: begin/rescue
192
+ # send message
193
+ #Debugger.signal=
194
+ # Signal.new
195
+ end
196
+
197
+ end
198
+
199
+ # -----------------------------------------------------------------------
200
+ =begin rdoc
201
+ The CPU registers for the process. This acts as a Hash mapping register names
202
+ to contents. The Hash acts as a snapshot of the CPU state; the registers are
203
+ read from the process using read(), and written to the process using write().
204
+
205
+ Usage:
206
+ regs = RegSet.new(RegSet::GEN, pid)
207
+ regs.read
208
+ puts regs.inspect
209
+ regs['eax'] = 0x0
210
+ regs.write
211
+ =end
212
+ class RegSet
213
+ extend Forwardable
214
+ extend Enumerable
215
+
216
+ =begin rdoc
217
+ General register set (EAX and friends in x86).
218
+ =end
219
+ GEN = 0
220
+ =begin rdoc
221
+ Floating-point register set (ST(0) and friends in x86).
222
+ =end
223
+ FP = 1
224
+ =begin rdoc
225
+ Extended floating-point register set (fpx on Linux).
226
+ =end
227
+ EXT = 2
228
+ =begin rdoc
229
+ Valid register set types.
230
+ =end
231
+ TYPES = [GEN, FP, EXT]
232
+
233
+ =begin rdoc
234
+ Method names for read accessor, keyed by register type.
235
+ =end
236
+ GETTER_SYMS = [:regs, :fpregs, :fpxregs ]
237
+ =begin rdoc
238
+ Method names for write accessor, keyed by register type.
239
+ =end
240
+ SETTER_SYMS = [:regs=, :fpregs=, :fpxregs= ]
241
+
242
+ =begin rdoc
243
+ Type of this register set.
244
+ =end
245
+ attr_reader :reg_type
246
+ =begin rdoc
247
+ PID of process owning this register set.
248
+ =end
249
+ attr_reader :pid
250
+
251
+ =begin rdoc
252
+ Create a new register set of the specified type for process 'pid'.
253
+ =end
254
+ def initialize(type, pid)
255
+ @reg_type = type
256
+ @getter = GETTER_SYMS[type]
257
+ @setter = SETTER_SYMS[type]
258
+ @pid = pid
259
+ @regs = {}
260
+ end
261
+
262
+ =begin rdoc
263
+ Read the current state of the CPU registers from the process. This fills the
264
+ contents of the RegSet Hash, and returns the Hash.
265
+ This can raise an OperationNotPermittedError if access is denied, or an
266
+ InvalidProcessError if the target process has exited.
267
+ =end
268
+ def read
269
+ @regs = ptrace_send(@getter)
270
+ end
271
+
272
+ =begin rdoc
273
+ Write the contents of the RegSet Hash to the process CPU registers.
274
+ This can raise an OperationNotPermittedError if access is denied, or an
275
+ InvalidProcessError if the target process has exited.
276
+ =end
277
+ def write
278
+ ptrace_send(@setter, @regs)
279
+ end
280
+
281
+ def_delegators :@regs, :[], :[]=, :clear, :each, :delete, :each_key,
282
+ :each_pair, :each_value, :empty?, :fetch, :has_key?,
283
+ :has_value?, :include?, :index, :indexes, :invert,
284
+ :key?, :keys, :length?, :member, :reject, :replace,
285
+ :shift, :size, :sort, :store, :to_a, :to_s, :update,
286
+ :value?, :values
287
+
288
+ private
289
+
290
+ def ptrace_send( sym, arg=nil )
291
+ begin
292
+ args = [@pid]
293
+ args << arg if arg
294
+ Debugger.send( sym, *args )
295
+ rescue RuntimeError => e
296
+ case e.message
297
+ when 'PTRACE: Operation not permitted'
298
+ raise OperationNotPermittedError.new(e.message)
299
+ when 'PTRACE: No such process'
300
+ raise InvalidProcessError.new(e.message)
301
+ else
302
+ raise
303
+ end
304
+ end
305
+ end
306
+ end
307
+
308
+ # -----------------------------------------------------------------------
309
+ =begin rdoc
310
+ A target process managed by ptrace.
311
+
312
+ Usage:
313
+ tgt = Target.attach(pid)
314
+ loop do
315
+ begin
316
+ tgt.step
317
+ puts tgt.regs.read.inspect
318
+ rescue Ptrace::InvalidProcessError
319
+ break
320
+ end
321
+ end
322
+
323
+ tgt = Target.launch(cmd)
324
+ loop do
325
+ begin
326
+ state = tgt.syscall_state
327
+ puts state.inspect
328
+ rescue Ptrace::InvalidProcessError
329
+ break
330
+ end
331
+ end
332
+ =end
333
+ class Target
334
+
335
+ =begin rdoc
336
+ PID of target process.
337
+ =end
338
+ attr_accessor :pid
339
+ =begin rdoc
340
+ General (CPU) registers for process.
341
+ =end
342
+ attr_accessor :regs
343
+ =begin rdoc
344
+ FPU registers for process.
345
+ =end
346
+ attr_accessor :fpregs
347
+ =begin rdoc
348
+ extended FPU registers for process.
349
+ =end
350
+ attr_accessor :fpregs
351
+ =begin rdoc
352
+ Text (code) segment for process.
353
+ =end
354
+ attr_accessor :text
355
+ =begin rdoc
356
+ Data segment for process.
357
+ =end
358
+ attr_accessor :data
359
+ =begin rdoc
360
+ Target 'user' (task) area.
361
+ =end
362
+ attr_accessor :user
363
+ =begin rdoc
364
+ Ptrace options.
365
+ =end
366
+ attr_accessor :options
367
+
368
+ =begin rdoc
369
+ Create a Ptrace::Target object for process 'pid'. The process is assumed to
370
+ have been launched or attached to by ptrace, e.g. using Target.launch or
371
+ Target.attach.
372
+ =end
373
+ def initialize(pid)
374
+ @pid = pid
375
+ @text = MemArea.new(MemArea::MEM_TEXT, pid)
376
+ @data = MemArea.new(MemArea::MEM_DATA, pid)
377
+ @user = MemArea.new(MemArea::MEM_USER, pid)
378
+ @regs = RegSet.new(RegSet::GEN, pid)
379
+ @fpregs = RegSet.new(RegSet::FP, pid)
380
+ @fpxregs = RegSet.new(RegSet::Ext, pid)
381
+ @options = Options.new(pid)
382
+ @valid = true
383
+ end
384
+
385
+ =begin rdoc
386
+ PT_ATTACH : Attach to running process 'pid' and return a Ptrace::Target object.
387
+ Raises an exception if the attach fails.
388
+ =end
389
+ def self.attach(pid)
390
+ tgt = Target.new(pid)
391
+ begin
392
+ Ptrace::Debugger.send_cmd( Ptrace::Debugger.commands[:attach], pid, nil)
393
+ Process.waitpid(pid)
394
+ rescue RuntimeError => e
395
+ case e.message
396
+ when 'PTRACE: Operation not permitted'
397
+ raise OperationNotPermittedError.new(e.message)
398
+ when 'PTRACE: No such process'
399
+ raise InvalidProcessError.new(e.message)
400
+ else
401
+ raise
402
+ end
403
+ end
404
+ return tgt
405
+ end
406
+
407
+ =begin rdoc
408
+ PT_TRACE_ME : Launch command 'cmd' and return a Ptrace::Target object for
409
+ controlling it.
410
+ Raises an exception if the command cannot be launched; returns nil if
411
+ the command cannot be traced.
412
+ =end
413
+ def self.launch(cmd)
414
+ pid = fork
415
+ if ! pid
416
+ begin
417
+ Ptrace::Debugger.send_cmd(Ptrace::Debugger.commands[:traceme], nil,
418
+ nil)
419
+ exec(cmd)
420
+ rescue RuntimeError => e
421
+ case e.message
422
+ when 'PTRACE: Operation not permitted'
423
+ raise OperationNotPermittedError.new(e.message)
424
+ when 'PTRACE: No such process'
425
+ raise InvalidProcessError.new(e.message)
426
+ else
427
+ raise
428
+ end
429
+ end
430
+
431
+ elsif pid == -1
432
+ return nil
433
+
434
+ else
435
+ Process.waitpid(pid)
436
+ tgt = Target.new(pid)
437
+ return tgt
438
+ end
439
+ end
440
+
441
+ =begin rdoc
442
+ =end
443
+ def signal(sig=nil)
444
+ # if sig, send
445
+ # else:
446
+ Signal.read(pid)
447
+ end
448
+
449
+ =begin rdoc
450
+ =end
451
+ def event_msg
452
+ # send signal
453
+ # For PTRACE_EVENT_EXIT this is the child's exit status. For PTRACE_EVENT_FORK, PTRACE_EVENT_VFORK and PTRACE_EVENT_CLONE this is the PID of the new process.
454
+ # Debugger.event_msg
455
+ end
456
+
457
+ =begin rdoc
458
+ PT_KILL : Terminate the process.
459
+ Note: This makes the Ptrace::Target object invalid.
460
+ =end
461
+ def kill
462
+ ptrace_send( :kill )
463
+ Process.waitpid(@pid)
464
+ @valid = false
465
+ end
466
+
467
+ =begin rdoc
468
+ PT_DETACH : Detach from the process.
469
+ Note: This makes the Ptrace::Target object invalid.
470
+ =end
471
+ def detach
472
+ ptrace_send( :detach )
473
+ Process.waitpid(@pid)
474
+ @valid = false
475
+ end
476
+
477
+ =begin rdoc
478
+ PT_STEP : Step a single instruction.
479
+ =end
480
+ def step
481
+ ptrace_send( :singlestep )
482
+ Process.waitpid(@pid)
483
+ end
484
+
485
+ =begin rdoc
486
+ PT_SYSCALL : Execute until the start or end of the next system call.
487
+
488
+ Usage:
489
+ tgt.syscall
490
+ in_regs = tgt.regs.read
491
+ tgt.syscall
492
+ out_regs = tgt.regs.read
493
+ =end
494
+ def syscall
495
+ ptrace_send( :syscall )
496
+ Process.waitpid(@pid)
497
+ end
498
+
499
+ =begin rdoc
500
+ Wrapper for recording syscalls. This issues a PT_SYSCALL to stop the target at
501
+ the next syscall, records the 'in' register set, issues a PT_SYSCALL to stop the
502
+ target after the syscall returns, records the 'out' register set, and
503
+ returns a Hash { :in, :out } of the register sets. The target is stopped
504
+ on return from this syscall.
505
+ =end
506
+ def syscall_state
507
+ begin
508
+ state = {}
509
+ syscall
510
+ state[:in] = @regs.read
511
+ syscall
512
+ state[:out] = @regs.read
513
+ state
514
+ rescue InvalidProcessError
515
+ # Program exited without a syscall
516
+ return state
517
+ end
518
+ end
519
+
520
+ =begin rdoc
521
+ PT_CONTINUE: Continue execution of target.
522
+ =end
523
+ def cont
524
+ ptrace_send( :cont )
525
+ Process.waitpid(@pid)
526
+ end
527
+
528
+ =begin rdoc
529
+ PT_SYSEMU
530
+ =end
531
+ def sysemu
532
+ ptrace_send( :sysemu )
533
+ end
534
+
535
+ =begin rdoc
536
+ PT_SYSEMU_SINGLESTEP
537
+ =end
538
+ def sysemu_step
539
+ ptrace_send( :sysemu_singlestep )
540
+ end
541
+
542
+ =begin rdoc
543
+ =end
544
+ # misc
545
+ def options=
546
+ # TODO
547
+ # ptrace_send_data( :setoptions, )
548
+ end
549
+
550
+ private
551
+
552
+ def ptrace_send( cmd, arg=nil )
553
+ begin
554
+ raise OperationNotSupportedError if (not PTRACE_COMMANDS.include? cmd)
555
+ raise OperationNotPermittedError if (not @valid)
556
+
557
+ Debugger.send_cmd( PTRACE_COMMANDS[cmd], @pid, arg )
558
+ rescue RuntimeError => e
559
+ case e.message
560
+ when 'PTRACE: Operation not permitted'
561
+ raise OperationNotPermittedError.new(e.message)
562
+ when 'PTRACE: No such process'
563
+ raise InvalidProcessError.new(e.message)
564
+ else
565
+ raise
566
+ end
567
+ end
568
+ end
569
+
570
+ end
571
+ end