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/ChangeLog +6 -0
- data/LICENSE +674 -0
- data/README +105 -0
- data/examples/kill.rb +14 -0
- data/examples/peekpoke.rb +53 -0
- data/examples/regs.rb +47 -0
- data/examples/syscall.rb +33 -0
- data/lib/Ptrace.rb +571 -0
- data/module/Ptrace.c +683 -0
- data/module/Ptrace.h +51 -0
- data/module/extconf.rb +11 -0
- data/module/rdoc_input/Ptrace_ext.rb +120 -0
- data/module/ruby_compat.c +17 -0
- data/module/ruby_compat.h +28 -0
- data/tests/ut_ptrace.rb +597 -0
- metadata +83 -0
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
|