heapinfo 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +130 -0
- data/lib/heapinfo.rb +75 -0
- data/lib/heapinfo/arena.rb +193 -0
- data/lib/heapinfo/chunk.rb +96 -0
- data/lib/heapinfo/chunks.rb +20 -0
- data/lib/heapinfo/dumper.rb +204 -0
- data/lib/heapinfo/ext/string.rb +29 -0
- data/lib/heapinfo/helper.rb +123 -0
- data/lib/heapinfo/libc.rb +46 -0
- data/lib/heapinfo/nil.rb +27 -0
- data/lib/heapinfo/process.rb +205 -0
- data/lib/heapinfo/segment.rb +34 -0
- data/lib/heapinfo/tools/get_arena.c +29 -0
- data/lib/heapinfo/version.rb +3 -0
- data/spec/chunk_spec.rb +40 -0
- data/spec/chunks_spec.rb +25 -0
- data/spec/dumper_spec.rb +79 -0
- data/spec/files/32bit_maps +23 -0
- data/spec/files/64bit_maps +29 -0
- data/spec/files/victim.cpp +32 -0
- data/spec/helper_spec.rb +73 -0
- data/spec/nil_spec.rb +15 -0
- data/spec/process_spec.rb +157 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/string_spec.rb +18 -0
- metadata +81 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module HeapInfo
|
2
|
+
# Self-defined array for collecting chunk(s)
|
3
|
+
class Chunks
|
4
|
+
# Instantiate a <tt>HeapInfo::Chunks</tt> object
|
5
|
+
def initialize
|
6
|
+
@chunks = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(method_sym, *arguments, &block)
|
10
|
+
return super unless @chunks.respond_to? method_sym
|
11
|
+
@chunks.send(method_sym, *arguments, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Hook <tt>#to_s</tt> for pretty printing.
|
15
|
+
# @return [String]
|
16
|
+
def to_s
|
17
|
+
@chunks.map(&:to_s).join("\n")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
module HeapInfo
|
2
|
+
# Class for memory dump relation works
|
3
|
+
class Dumper
|
4
|
+
# Default dump length
|
5
|
+
DUMP_BYTES = 8
|
6
|
+
|
7
|
+
# Instantiate a <tt>HeapInfo::Dumper</tt> object
|
8
|
+
#
|
9
|
+
# @param [Hash] segments With values <tt>HeapInfo::Segment</tt>
|
10
|
+
# @param [String] mem_filename The filename that can be access for dump. Should be <tt>/proc/[pid]/mem</tt>.
|
11
|
+
def initialize(segments, mem_filename)
|
12
|
+
@segments, @filename = segments, mem_filename
|
13
|
+
need_permission unless dumpable?
|
14
|
+
end
|
15
|
+
|
16
|
+
# A helper for <tt>HeapInfo::Process</tt> to dump memory.
|
17
|
+
# @param [Mixed] args The use input commands, see examples of <tt>HeapInfo::Process#dump</tt>.
|
18
|
+
# @return [String, NilClass] Dump results. If error happend, <tt>nil</tt> is returned.
|
19
|
+
# @example
|
20
|
+
# p dump(:elf, 4)
|
21
|
+
# # => "\x7fELF"
|
22
|
+
def dump(*args)
|
23
|
+
return need_permission unless dumpable?
|
24
|
+
base, offset, len = Dumper.parse_cmd(args)
|
25
|
+
if base.instance_of?(Symbol) and @segments[base].instance_of?(Segment)
|
26
|
+
addr = @segments[base].base
|
27
|
+
elsif base.is_a? Integer
|
28
|
+
addr = base
|
29
|
+
else
|
30
|
+
fail # invalid usage
|
31
|
+
end
|
32
|
+
file = mem_f
|
33
|
+
file.pos = addr + offset
|
34
|
+
mem = file.read len
|
35
|
+
file.close
|
36
|
+
mem
|
37
|
+
rescue
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return the dump result as chunks.
|
42
|
+
# see <tt>HeapInfo::Chunks</tt> and <tt>HeapInfo::Chunk</tt> for more information.
|
43
|
+
#
|
44
|
+
# Note: Same as <tt>dump</tt>, need permission of attaching another process.
|
45
|
+
# @return [HeapInfo::Chunks] An array of chunk(s).
|
46
|
+
# @param [Mixed] args Same as arguments of <tt>#dump</tt>
|
47
|
+
def dump_chunks(*args)
|
48
|
+
base = base_of(*args)
|
49
|
+
dump(*args).to_chunks(bits: @segments[:bits], base: base)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Show dump results like in gdb's command <tt>x</tt>.
|
53
|
+
#
|
54
|
+
# Details are in <tt>HeapInfo:Process#x</tt>
|
55
|
+
# @param [Integer] count The number of result need to dump.
|
56
|
+
# @param [Mixed] commands Same format as <tt>#dump(*args)</tt>.
|
57
|
+
# @param [IO] io <tt>IO</tt> that use for printing.
|
58
|
+
# @return [NilClass] The return value of <tt>io.puts</tt>.
|
59
|
+
# @example
|
60
|
+
# x 3, 0x400000
|
61
|
+
# # 0x400000: 0x00010102464c457f 0x0000000000000000
|
62
|
+
# # 0x400010: 0x00000001003e0002
|
63
|
+
def x(count, *commands, io: $stdout)
|
64
|
+
commands = commands + [count * size_t]
|
65
|
+
base = base_of(*commands)
|
66
|
+
res = dump(*commands).unpack(size_t == 4 ? "L*" : "Q*")
|
67
|
+
str = res.group_by.with_index{|_, i| i / (16 / size_t) }.map do |round, values|
|
68
|
+
"%#x:\t" % (base + round * 16) + values.map{|v| Helper.color "0x%0#{size_t * 2}x" % v}.join("\t")
|
69
|
+
end.join("\n")
|
70
|
+
io.puts str
|
71
|
+
end
|
72
|
+
|
73
|
+
# Search a specific value/string/regexp in memory.
|
74
|
+
# <tt>#find</tt> only return the first matched address.
|
75
|
+
# @param [Integer, String, Regexp] pattern The desired search pattern, can be value(<tt>Integer</tt>), string, or regular expression.
|
76
|
+
# @param [Integer, String, Symbol] from Start address for searching, can be segment(<tt>Symbol</tt>) or segments with offset. See examples for more information.
|
77
|
+
# @param [Integer] length The length limit for searching.
|
78
|
+
# @return [Integer, NilClass] The first matched address, <tt>nil</tt> is returned when no such pattern found.
|
79
|
+
# @example
|
80
|
+
# find(/E.F/, :elf)
|
81
|
+
# # => 4194305
|
82
|
+
# find(0x4141414141414141, 'heap+0x10', 0x1000)
|
83
|
+
# # => 6291472
|
84
|
+
# find('/bin/sh', :libc)
|
85
|
+
# # => 140662379588827
|
86
|
+
def find(pattern, from, length)
|
87
|
+
from = base_of(from)
|
88
|
+
length = 1 << 40 if length.is_a? Symbol
|
89
|
+
case pattern
|
90
|
+
when Integer; find_integer(pattern, from, length)
|
91
|
+
when String; find_string(pattern, from, length)
|
92
|
+
when Regexp; find_regexp(pattern, from, length)
|
93
|
+
else; nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
# Parse the dump command into <tt>[base, offset, length]</tt>
|
99
|
+
# @param [Array] args The command, see examples for more information
|
100
|
+
# @return [Array<Symbol, Integer>] <tt>[base, offset, length]</tt>, while <tt>base</tt> can be a [Symbol] or an [Integer]. <tt>length</tt> has default value equal to <tt>8</tt>.
|
101
|
+
# @example
|
102
|
+
# HeapInfo::Dumper.parse_cmd([:heap, 32, 10])
|
103
|
+
# # [:heap, 32, 10]
|
104
|
+
# HeapInfo::Dumper.parse_cmd(['heap+0x10, 10'])
|
105
|
+
# # [:heap, 16, 10]
|
106
|
+
# HeapInfo::Dumper.parse_cmd(['heap+0x10'])
|
107
|
+
# # [:heap, 16, 8]
|
108
|
+
# HeapInfo::Dumper.parse_cmd([0x400000, 4])
|
109
|
+
# # [0x400000, 0, 4]
|
110
|
+
def self.parse_cmd(args)
|
111
|
+
args = split_cmd args
|
112
|
+
return :fail unless args.size == 3
|
113
|
+
len = args[2].nil? ? DUMP_BYTES : Integer(args[2])
|
114
|
+
offset = Integer(args[1])
|
115
|
+
base = args[0]
|
116
|
+
base = Helper.integer?(base) ? Integer(base) : base.delete(':').to_sym
|
117
|
+
[base, offset, len]
|
118
|
+
end
|
119
|
+
|
120
|
+
# Helper for <tt>#parse_cmd</tt>.
|
121
|
+
#
|
122
|
+
# Split commands to exactly three parts: <tt>[base, offset, length]</tt>
|
123
|
+
# <tt>length</tt> is <tt>nil</tt> if not present.
|
124
|
+
# @param [Array] args
|
125
|
+
# @return [Array<String>] <tt>[base, offset, length]</tt> in string expression.
|
126
|
+
# @example
|
127
|
+
# HeapInfo::Dumper.split_cmd([:heap, 32, 10])
|
128
|
+
# # ['heap', '32', '10']
|
129
|
+
# HeapInfo::Dumper.split_cmd([':heap+0x10, 10'])
|
130
|
+
# # [':heap', '0x10', '10']
|
131
|
+
# HeapInfo::Dumper.split_cmd([':heap+0x10'])
|
132
|
+
# # [':heap', '0x10', nil]
|
133
|
+
# HeapInfo::Dumper.split_cmd([0x400000, 4])
|
134
|
+
# # ['4194304', 0, '4']
|
135
|
+
def self.split_cmd(args)
|
136
|
+
args = args.join(',').delete(' ').split(',').reject(&:empty?) # 'heap, 100', 32 => 'heap', '100', '32'
|
137
|
+
return [] if args.empty?
|
138
|
+
if args[0].include? '+' # 'heap+0x1'
|
139
|
+
args.unshift(*args.shift.split('+', 2))
|
140
|
+
elsif args.size <= 2 # no offset given
|
141
|
+
args.insert(1, 0)
|
142
|
+
end
|
143
|
+
args << nil if args.size <= 2 # no length given
|
144
|
+
args[0, 3]
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
def need_permission
|
149
|
+
puts Helper.color(%q(Could not attach to process. Check the setting of /proc/sys/kernel/yama/ptrace_scope, or try again as the root user. For more details, see /etc/sysctl.d/10-ptrace.conf), sev: :fatal)
|
150
|
+
end
|
151
|
+
|
152
|
+
# use /proc/[pid]/mem for memory dump, must sure have permission
|
153
|
+
def dumpable?
|
154
|
+
mem_f.close
|
155
|
+
true
|
156
|
+
rescue => e
|
157
|
+
if e.is_a? Errno::EACCES
|
158
|
+
false
|
159
|
+
else
|
160
|
+
throw e
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def mem_f
|
165
|
+
File.open(@filename)
|
166
|
+
end
|
167
|
+
|
168
|
+
def base_of(*args)
|
169
|
+
base, offset, _ = Dumper.parse_cmd(args)
|
170
|
+
base = @segments[base].base if @segments[base].is_a? Segment
|
171
|
+
base + offset
|
172
|
+
end
|
173
|
+
|
174
|
+
def find_integer(value, from, length)
|
175
|
+
find_string([value].pack(size_t == 4 ? "L*" : "Q*"), from, length)
|
176
|
+
end
|
177
|
+
|
178
|
+
def find_string(string, from ,length)
|
179
|
+
batch_dumper(from, length) {|str| str.index string}
|
180
|
+
end
|
181
|
+
|
182
|
+
def find_regexp(pattern, from ,length)
|
183
|
+
batch_dumper(from, length) {|str| str =~ pattern}
|
184
|
+
end
|
185
|
+
|
186
|
+
def batch_dumper(from, remain_size)
|
187
|
+
page_size = 0x1000
|
188
|
+
while remain_size > 0
|
189
|
+
dump_size = [remain_size, page_size].min
|
190
|
+
str = dump(from, dump_size)
|
191
|
+
break if str.nil? # unreadable
|
192
|
+
break unless (idx = yield(str)).nil?
|
193
|
+
break if str.length < dump_size # remain is unreadable
|
194
|
+
remain_size -= str.length
|
195
|
+
from += str.length
|
196
|
+
end
|
197
|
+
return if idx.nil?
|
198
|
+
from + idx
|
199
|
+
end
|
200
|
+
def size_t
|
201
|
+
@segments[:bits] / 8
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module HeapInfo
|
2
|
+
module Ext
|
3
|
+
module String
|
4
|
+
# Methods to be mixed into String
|
5
|
+
module InstanceMethods
|
6
|
+
def to_chunk(bits: 64, base: 0)
|
7
|
+
size_t = bits / 8
|
8
|
+
dumper = lambda{|addr, len| self[addr-base, len]}
|
9
|
+
Chunk.new(size_t, base, dumper)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_chunks(bits: 64, base: 0)
|
13
|
+
size_t = bits / 8
|
14
|
+
chunks = Chunks.new
|
15
|
+
cur = 0
|
16
|
+
while cur + size_t * 2 <= self.length
|
17
|
+
now_chunk = self[cur, size_t * 2].to_chunk
|
18
|
+
sz = now_chunk.size
|
19
|
+
chunks << self[cur, sz + 1].to_chunk(bits: bits, base: base + cur) # +1 for dump prev_inuse
|
20
|
+
cur += sz
|
21
|
+
end
|
22
|
+
chunks
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
::String.send(:include, HeapInfo::Ext::String::InstanceMethods)
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module HeapInfo
|
2
|
+
# Some helper functions
|
3
|
+
module Helper
|
4
|
+
# Get the process id of a process
|
5
|
+
# @param [String] prog The request process name
|
6
|
+
# @return [Fixnum] process id
|
7
|
+
def self.pidof(prog)
|
8
|
+
# plz, don't cmd injection your self :p
|
9
|
+
pid = `pidof #{prog}`.strip.to_i
|
10
|
+
return nil if pid == 0 # process not exists yet
|
11
|
+
throw "pidof #{prog} fail" unless pid.between?(2, 65535)
|
12
|
+
pid
|
13
|
+
#TODO: handle when multi processes exists
|
14
|
+
end
|
15
|
+
|
16
|
+
# Create read <tt>/proc/[pid]/*</tt> methods
|
17
|
+
%w(exe maps).each do |method|
|
18
|
+
self.define_singleton_method("#{method}_of".to_sym) do |pid|
|
19
|
+
begin
|
20
|
+
IO.binread("/proc/#{pid}/#{method}")
|
21
|
+
rescue
|
22
|
+
throw "reading /proc/#{pid}/#{method} error"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Parse the contents of <tt>/proc/[pid]/maps</tt>.
|
28
|
+
#
|
29
|
+
# @param [String] content The file content of <tt>/proc/[pid]/maps</tt>
|
30
|
+
# @return [Array] In form of <tt>[[start, end, permission, name], ...]</tt>. See examples.
|
31
|
+
# @example
|
32
|
+
# HeapInfo::Helper.parse_maps(<<EOS
|
33
|
+
# 00400000-0040b000 r-xp 00000000 ca:01 271708 /bin/cat
|
34
|
+
# 00bc4000-00be5000 rw-p 00000000 00:00 0 [heap]
|
35
|
+
# 7f2788315000-7f2788316000 r--p 00022000 ca:01 402319 /lib/x86_64-linux-gnu/ld-2.19.so
|
36
|
+
# EOS
|
37
|
+
# )
|
38
|
+
# # [[0x400000, 0x40b000, 'r-xp', '/bin/cat'],
|
39
|
+
# # [0xbc4000, 0xbe5000, 'rw-p', '[heap]'],
|
40
|
+
# # [0x7f2788315000, 0x7f2788316000, 'r--p', '/lib/x86_64-linux-gnu/ld-2.19.so']]
|
41
|
+
def self.parse_maps(content)
|
42
|
+
lines = content.split("\n")
|
43
|
+
lines.map do |line|
|
44
|
+
s = line.scan(/^([0-9a-f]+)-([0-9a-f]+)\s([rwxp-]{4})[^\/|\[]*([\/|\[].+)$/)[0]
|
45
|
+
next nil if s.nil?
|
46
|
+
s[0],s[1] = s[0,2].map{|h|h.to_i(16)}
|
47
|
+
s
|
48
|
+
end.compact
|
49
|
+
end
|
50
|
+
|
51
|
+
# Color codes for pretty print
|
52
|
+
COLOR_CODE = {
|
53
|
+
esc_m: "\e[0m",
|
54
|
+
normal_s: "\e[38;5;1m", # red
|
55
|
+
integer: "\e[38;5;12m", # light blue
|
56
|
+
fatal: "\e[38;5;197m", # dark red
|
57
|
+
bin: "\e[38;5;120m", # light green
|
58
|
+
klass: "\e[38;5;155m", # pry like
|
59
|
+
sym: "\e[38;5;229m", # pry like
|
60
|
+
}
|
61
|
+
# Wrapper color codes for for pretty inspect
|
62
|
+
# @param [String] s Contents for wrapper
|
63
|
+
# @param [Symbol?] sev Specific which kind of color want to use, valid symbols are defined in <tt>#COLOR_CODE</tt>.
|
64
|
+
# If this argument is not present, will detect according to the content of <tt>s</tt>
|
65
|
+
# @return [String] wrapper with color codes.
|
66
|
+
def self.color(s, sev: nil)
|
67
|
+
s = s.to_s
|
68
|
+
color = ''
|
69
|
+
cc = COLOR_CODE
|
70
|
+
if cc.keys.include?(sev)
|
71
|
+
color = cc[sev]
|
72
|
+
elsif s =~ /^(0x)?[0-9a-f]+$/ # integers
|
73
|
+
color = cc[:integer]
|
74
|
+
else #normal string
|
75
|
+
color = cc[:normal_s]
|
76
|
+
end
|
77
|
+
"#{color}#{s.sub(cc[:esc_m], color)}#{cc[:esc_m]}"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Unpack strings to integer.
|
81
|
+
#
|
82
|
+
# Like the <tt>p32</tt> and <tt>p64</tt> in pwntools.
|
83
|
+
#
|
84
|
+
# @param [Integer] size_t Either 4 or 8.
|
85
|
+
# @param [String] data String to be unpack.
|
86
|
+
# @return [Integer] Unpacked result.
|
87
|
+
# @example
|
88
|
+
# HeapInfo::Helper.unpack(4, "\x12\x34\x56\x78")
|
89
|
+
# # 0x78563412
|
90
|
+
# HeapInfo::Helper.unpack(8, "\x12\x34\x56\x78\xfe\xeb\x90\x90")
|
91
|
+
# # 0x9090ebfe78563412
|
92
|
+
def self.unpack(size_t, data)
|
93
|
+
data.unpack(size_t == 4 ? 'L*' : 'Q*')[0]
|
94
|
+
end
|
95
|
+
|
96
|
+
# Retrieve pure class name(without module) of an object
|
97
|
+
# @param [Object] obj Any instance
|
98
|
+
# @return [String] Class name of <tt>obj</tt>
|
99
|
+
# @example
|
100
|
+
# # suppose obj is an instance of HeapInfo::Chunk
|
101
|
+
# Helper.class_name(obj)
|
102
|
+
# # => 'Chunk'
|
103
|
+
def self.class_name(obj)
|
104
|
+
obj.class.name.split('::').last || obj.class.name
|
105
|
+
end
|
106
|
+
|
107
|
+
# For checking a string is actually an integer
|
108
|
+
# @param [String] str String to be checked
|
109
|
+
# @return [Boolean] If <tt>str</tt> can be converted into integer
|
110
|
+
# @example
|
111
|
+
# Helper.integer? '1234'
|
112
|
+
# # => true
|
113
|
+
# Helper.integer? '0x1234'
|
114
|
+
# # => true
|
115
|
+
# Helper.integer? '0xheapoverflow'
|
116
|
+
# # => false
|
117
|
+
def self.integer?(str)
|
118
|
+
!!Integer(str)
|
119
|
+
rescue ArgumentError, TypeError
|
120
|
+
false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module HeapInfo
|
2
|
+
class Libc < Segment
|
3
|
+
def main_arena_offset
|
4
|
+
return @_main_arena_offset if @_main_arena_offset
|
5
|
+
return nil unless exhaust_search :main_arena
|
6
|
+
@_main_arena_offset
|
7
|
+
end
|
8
|
+
|
9
|
+
def main_arena
|
10
|
+
return @_main_arena.reload if @_main_arena
|
11
|
+
off = main_arena_offset
|
12
|
+
return if off.nil?
|
13
|
+
@_main_arena = Arena.new(off + self.base, process.bits, lambda{|*args|process.send(:dumper).dump(*args)})
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.find(maps, name, process)
|
17
|
+
obj = super(maps, name)
|
18
|
+
obj.send(:process=, process)
|
19
|
+
obj
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
private
|
24
|
+
attr_accessor :process
|
25
|
+
# only for searching offset of main_arena now
|
26
|
+
def exhaust_search(symbol)
|
27
|
+
return false if symbol != :main_arena
|
28
|
+
# TODO: read from cache
|
29
|
+
@_main_arena_offset = resolve_main_arena_offset
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def resolve_main_arena_offset
|
34
|
+
tmp_elf = HeapInfo::TMP_DIR + "/get_arena"
|
35
|
+
libc_file = HeapInfo::TMP_DIR + "/libc.so.6"
|
36
|
+
ld_file = HeapInfo::TMP_DIR + "/ld.so"
|
37
|
+
flags = "-w #{@process.bits == 32 ? '-m32' : ''}"
|
38
|
+
%x(cp #{self.name} #{libc_file} && \
|
39
|
+
cp #{@process.ld.name} #{ld_file} && \
|
40
|
+
chdir #{File.expand_path('../tools', __FILE__)} && \
|
41
|
+
gcc #{flags} get_arena.c -o #{tmp_elf} 2>&1 > /dev/null && \
|
42
|
+
#{ld_file} --library-path #{HeapInfo::TMP_DIR} #{tmp_elf} && \
|
43
|
+
rm #{tmp_elf} #{libc_file} #{ld_file}).to_i(16)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/heapinfo/nil.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module HeapInfo
|
2
|
+
# Self defined <tt>nil</tt> like class.
|
3
|
+
#
|
4
|
+
# Be the return values of <tt>#dump</tt> or <tt>#dump_chunks</tt>, to prevent use the return value for calculating accidentally while exploiting remote.
|
5
|
+
class Nil
|
6
|
+
%i(nil? inspect to_s).each do |method_sym|
|
7
|
+
define_method(method_sym){|*args, &block| nil.send(method_sym, *args, &block)}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Hook all missing methods
|
11
|
+
# @return [HeapInfo::Nil] return <tt>self</tt> so that it can be a <tt>nil</tt> chain.
|
12
|
+
# @example
|
13
|
+
# # h.dump would return Nil when process not found
|
14
|
+
# p h.dump(:heap)[8,8].unpack("Q*)
|
15
|
+
# # => nil
|
16
|
+
def method_missing(method_sym, *args, &block)
|
17
|
+
return nil.send(method_sym, *args, &block) if nil.respond_to? method_sym
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# To prevent error raised when using <tt>puts Nil.new</tt>
|
22
|
+
# @return [Array] Empty array
|
23
|
+
def to_ary
|
24
|
+
[]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|