memory_io 0.3.0 → 1.0.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f287dc7b3453557bbcda2f2ce09e03d779d0dbff69f8ef059069f03c76da427
4
- data.tar.gz: a471ab55f6abd70783a220d0939cdd8f84231f59a533a6470a9f64119c184a41
3
+ metadata.gz: ae436fb71c1c77ce99b26657dc3e9497e8730ee578f0fe412186a67ee4881d12
4
+ data.tar.gz: 822612ae59c6c669a6136724dc9636349a3f6b3b314b20d3e024bc1481faf9e2
5
5
  SHA512:
6
- metadata.gz: 959a45d2dcab5ced28be35f6988a6c6fadbd1fd6510e21747727f16e87cabddf23e951ffba71d8deec4baa1be440ce5bd79b1fe54e1bad66a17deb9185c54db9
7
- data.tar.gz: a1fe1e032d09a53ce4611e2738255ac1444b5a0283e959606a1da232db28c9d0d42a7277977b17d2bb8fcfa476cc58c53368ad64fa970d0b7b91a5536e59e79e
6
+ metadata.gz: 1fc69554905f1fbd661045c65fd0570e0baea78191d531a7faa39157f9a5faa6618f3693a2cabd7f2117a7a849cd1fc5925123868103b8a414359ed49843068e
7
+ data.tar.gz: f5449be7a6af8e443e857492aa73533735fdc639d4215a79c4b7b0a7effae9db34d5511ec46fec11aba545185f0d6b9f9947ab02f9de88d17a10b78901a96c19
data/README.md CHANGED
@@ -15,7 +15,7 @@ I usually need to dump a structure, say `string` in C++, from memory for debuggi
15
15
  This is not hard if using gdb.
16
16
  However, gdb doesn't support writing Ruby scripts
17
17
  (unless you use [gdb-ruby](https://github.com/david942j/gdb-ruby), which has **MemoryIO** as its dependency).
18
- So I created this projected to make the debug procedure much easier.
18
+ So I created this project to make the debug procedure much easier.
19
19
 
20
20
  This repository has two main goals:
21
21
 
@@ -167,6 +167,34 @@ process.read('libc', 4)
167
167
 
168
168
  ## Developing
169
169
 
170
+ ```bash
171
+ $ git clone https://github.com/david942j/memory_io
172
+ $ cd memory_io
173
+ $ bundle install
174
+ $ bundle exec rake
175
+ ```
176
+
177
+ The default rake task regenerates README.md, runs RuboCop, and runs all specs.
178
+
170
179
  ### To Add a New Structure
171
180
 
172
- TBA
181
+ Pull Requests of new structures are welcome! Say you want to add the structure `Vec` of Rust:
182
+
183
+ 1. Create a file `lib/memory_io/types/rust/vec.rb`.
184
+ 2. Define class `MemoryIO::Types::Rust::Vec` and make it inherit from `MemoryIO::Types::Type`.
185
+ Types are registered automatically when the class is defined;
186
+ the symbols to access it are derived from the class name.
187
+ `MemoryIO::Types::Rust::Vec` gets the full-name `:'rust/vec'` and the alias `:vec`.
188
+ 3. Implement class method `read(stream)`, which reads bytes from `stream`
189
+ and returns an instance of your class.
190
+ Implement class method `write(stream, obj)` as well if the structure supports writing.
191
+ Some helper methods, such as `read_size_t` and `keep_pos`, are defined
192
+ in [Types::Type](https://www.rubydoc.info/github/david942j/memory_io/MemoryIO/Types/Type) for you.
193
+ 4. Write the doc-comment right above the class definition.
194
+ The first line of it will be shown in the section [Implemented Structures](#implemented-structures),
195
+ which is auto-generated by `rake readme`.
196
+ 5. Add specs in `spec/types/rust/vec_spec.rb`.
197
+ 6. Run `bundle exec rake` and make sure everything is green.
198
+
199
+ See [lib/memory_io/types/cpp/string.rb](lib/memory_io/types/cpp/string.rb)
200
+ ([spec](spec/types/cpp/string_spec.rb)) as a complete example.
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elftools'
4
+
5
+ require 'memory_io/stream'
6
+
7
+ module MemoryIO
8
+ # Describes how the memory being accessed lays out its data.
9
+ #
10
+ # The context belongs to the memory, not to the machine running this library.
11
+ # They only coincide when the memory belongs to a process on the same host.
12
+ class Context
13
+ # Byte orders that can be asked for. +:native+ resolves to the byte order
14
+ # of the host, which is the right answer whenever the memory belongs to a
15
+ # process running on it.
16
+ ENDIANS = %i[little big native].freeze
17
+
18
+ # The byte order of the host.
19
+ NATIVE_ENDIAN = [1].pack('S') == "\x01\x00".b ? :little : :big
20
+
21
+ # Assumed when nothing more specific is known.
22
+ DEFAULT_POINTER_SIZE = 8
23
+
24
+ # @return [:little, :big]
25
+ # Byte order of the memory. +:native+ has already been resolved.
26
+ attr_reader :endian
27
+
28
+ # @return [Integer]
29
+ # Size of a pointer, in bytes.
30
+ attr_reader :pointer_size
31
+
32
+ # @return [Hash]
33
+ # The attributes, in the form {#initialize} accepts.
34
+ def to_h
35
+ { endian: endian, pointer_size: pointer_size }
36
+ end
37
+
38
+ # @param [:little, :big, :native] endian
39
+ # Byte order of the memory.
40
+ # @param [Integer] pointer_size
41
+ # Size of a pointer, in bytes.
42
+ #
43
+ # @raise [ArgumentError]
44
+ # +endian+ is not one of {ENDIANS}.
45
+ #
46
+ # @example
47
+ # Context.new(endian: :big).endian
48
+ # #=> :big
49
+ def initialize(endian: :native, pointer_size: DEFAULT_POINTER_SIZE)
50
+ raise ArgumentError, "endian must be one of #{ENDIANS.inspect}, got #{endian.inspect}" \
51
+ unless ENDIANS.include?(endian)
52
+
53
+ @endian = endian == :native ? NATIVE_ENDIAN : endian
54
+ @pointer_size = pointer_size
55
+ end
56
+
57
+ class << self
58
+ # @return [MemoryIO::Context]
59
+ # Used when a stream carries no context of its own.
60
+ def default
61
+ @default ||= new
62
+ end
63
+
64
+ # @param [Object] stream
65
+ # The stream a type is reading from.
66
+ #
67
+ # @return [MemoryIO::Context]
68
+ # The context +stream+ was tagged with, or {.default} when it carries none.
69
+ def of(stream)
70
+ stream.is_a?(MemoryIO::Stream) ? stream.context : default
71
+ end
72
+
73
+ # Derive a context from an ELF file, which describes the memory it is
74
+ # loaded into.
75
+ #
76
+ # @param [String] path
77
+ # Path of the ELF file.
78
+ #
79
+ # @return [MemoryIO::Context?]
80
+ # +nil+ if +path+ is unreadable or is not an ELF file.
81
+ #
82
+ # @example
83
+ # Context.from_elf('/proc/self/exe')
84
+ # #=> #<MemoryIO::Context @endian=:little, @pointer_size=8>
85
+ def from_elf(path)
86
+ ::File.open(path, 'rb') do |file|
87
+ elf = ELFTools::ELFFile.new(file)
88
+ new(endian: elf.endian, pointer_size: elf.elf_class / 8)
89
+ end
90
+ rescue SystemCallError, ELFTools::ELFError
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MemoryIO
4
+ # The base class of all errors raised by {MemoryIO}.
5
+ #
6
+ # Rescue this class to catch every error this library raises on its own.
7
+ # Errors that propagate from Ruby itself are not covered.
8
+ #
9
+ # @example
10
+ # begin
11
+ # MemoryIO.attach(0)
12
+ # rescue MemoryIO::Error => e
13
+ # puts e.message
14
+ # end
15
+ # # /proc/0/mem does not exist
16
+ class Error < StandardError; end
17
+
18
+ # Raised when the memory of the target process is not accessible.
19
+ #
20
+ # @example
21
+ # MemoryIO.attach(0)
22
+ # # MemoryIO::ProcessNotFoundError: /proc/0/mem does not exist
23
+ class ProcessNotFoundError < Error; end
24
+
25
+ # Raised when an address expression cannot be evaluated.
26
+ #
27
+ # @example
28
+ # MemoryIO.attach('self').read('heep + 0x10', 4)
29
+ # # MemoryIO::InvalidAddressError: Failed to evaluate address: "heep + 0x10"
30
+ class InvalidAddressError < Error; end
31
+
32
+ # Raised when a value doesn't fit in the type it is written as.
33
+ #
34
+ # @example
35
+ # MemoryIO::IO.new(stream).write(0x100000041, as: :u32)
36
+ # # MemoryIO::ValueOutOfRangeError: 0x100000041 is out of range for 32-bit unsigned integer (0x0..0xffffffff)
37
+ class ValueOutOfRangeError < Error; end
38
+ end
data/lib/memory_io/io.rb CHANGED
@@ -1,12 +1,18 @@
1
1
  # encoding: ascii-8bit
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'memory_io/context'
5
+ require 'memory_io/stream'
4
6
  require 'memory_io/types/types'
5
7
 
6
8
  module MemoryIO
7
9
  # Main class to use {MemoryIO}.
8
10
  class IO
9
- attr_reader :stream # @return [#pos, #pos=, #read, #write]
11
+ # @!attribute [r] stream
12
+ # @return [#pos, #pos=, #read, #write] The stream given at instantiation.
13
+ # @!attribute [r] context
14
+ # @return [MemoryIO::Context] The context of the memory reached through {#stream}.
15
+ attr_reader :stream, :context
10
16
 
11
17
  # Instantiate an {IO} object.
12
18
  #
@@ -15,17 +21,29 @@ module MemoryIO
15
21
  # +file+ can be un-writable if you will not invoke any write-related method.
16
22
  #
17
23
  # If +stream.read(*)+ returns empty string or +nil+, it would be seen as reaching EOF.
18
- def initialize(stream)
24
+ # @param [:little, :big, :native] endian
25
+ # Byte order of the memory reached through +stream+.
26
+ # The default is right whenever that memory belongs to a process on this host,
27
+ # and should be given when it does not, such as a dump taken elsewhere.
28
+ # @param [Integer] pointer_size
29
+ # Size of a pointer in that memory, in bytes.
30
+ #
31
+ # @example
32
+ # # a dump captured on a 32-bit big endian machine
33
+ # MemoryIO::IO.new(File.open('core.dump', 'rb'), endian: :big, pointer_size: 4)
34
+ def initialize(stream, endian: :native, pointer_size: MemoryIO::Context::DEFAULT_POINTER_SIZE)
19
35
  @stream = stream
36
+ @context = MemoryIO::Context.new(endian: endian, pointer_size: pointer_size)
37
+ @tagged = MemoryIO::Stream.new(stream, @context)
20
38
  end
21
39
 
22
40
  # Read and convert result into custom type/structure.
23
41
  #
24
42
  # @param [Integer] num_elements
25
43
  # Number of elements to be read.
26
- # This parameter must be positive and larger than zero.
44
+ # Zero reads nothing, as it does for +::IO#read+.
27
45
  #
28
- # This parameter may effect the return type,
46
+ # This parameter may affect the return type,
29
47
  # see documents of return value.
30
48
  # @param [Integer?] from
31
49
  # Invoke +stream.pos = from+ before starting to read.
@@ -57,7 +75,11 @@ module MemoryIO
57
75
  # * +as != nil+ and +num_elements > 1+:
58
76
  # An array with length +num_elements+ is returned.
59
77
  #
60
- # If EOF is occured, object(s) read will be returned.
78
+ # If EOF occurred, only the objects that could be read in full are returned,
79
+ # so the result may be shorter than +num_elements+ (possibly empty).
80
+ #
81
+ # @raise [ArgumentError]
82
+ # +num_elements+ is negative or is not an Integer.
61
83
  #
62
84
  # @example
63
85
  # stream = StringIO.new('A' * 8 + 'B' * 8)
@@ -67,7 +89,7 @@ module MemoryIO
67
89
  # io.read(100)
68
90
  # #=> "BBBBBBB"
69
91
  #
70
- # # read two unsigned 32-bit integers starts from posistion 4
92
+ # # read two unsigned 32-bit integers starting from position 4
71
93
  # io.read(2, from: 4, as: :u32)
72
94
  # #=> [1094795585, 1111638594] # [0x41414141, 0x42424242]
73
95
  #
@@ -89,6 +111,17 @@ module MemoryIO
89
111
  # io.read(2, as: :c_str)
90
112
  # #=> ["123", "45678"]
91
113
  # @example
114
+ # # reading beyond the end of stream returns what was read
115
+ # stream = StringIO.new("\x01\x02\x03\x04")
116
+ # io = MemoryIO::IO.new(stream)
117
+ # io.read(3, as: :u32)
118
+ # #=> [67305985]
119
+ #
120
+ # # an object that can't be read in full is not returned
121
+ # io.rewind
122
+ # io.read(1, as: :u64)
123
+ # #=> nil
124
+ # @example
92
125
  # # pass lambda to `as`
93
126
  # stream = StringIO.new("\x03123\x044567")
94
127
  # io = MemoryIO::IO.new(stream)
@@ -101,12 +134,15 @@ module MemoryIO
101
134
  #
102
135
  # @see Types
103
136
  def read(num_elements, from: nil, as: nil, force_array: false)
137
+ unless num_elements.is_a?(Integer) && !num_elements.negative?
138
+ raise ArgumentError, "num_elements must be a non-negative Integer, got #{num_elements.inspect}"
139
+ end
140
+
104
141
  stream.pos = from if from
105
142
  return stream.read(num_elements) if as.nil?
106
143
 
107
144
  conv = to_proc(as, :read)
108
- # TODO: handle eof
109
- ret = Array.new(num_elements) { conv.call(stream) }
145
+ ret = read_elements(num_elements, conv)
110
146
  ret = ret.first if num_elements == 1 && !force_array
111
147
  ret
112
148
  end
@@ -125,7 +161,7 @@ module MemoryIO
125
161
  #
126
162
  # A +Proc+ is allowed, which should accept +stream+ and one object as arguments.
127
163
  #
128
- # If +objects+ is a descendant instance of {Types::Type} and +as+ is +nil,
164
+ # If +objects+ is a descendant instance of {Types::Type} and +as+ is +nil+,
129
165
  # +objects.class+ will be used for +as+.
130
166
  # Otherwise, when +as = nil+, this method will simply call +stream.write(objects)+.
131
167
  #
@@ -167,7 +203,7 @@ module MemoryIO
167
203
  return stream.write(objects) if as.nil?
168
204
 
169
205
  conv = to_proc(as, :write)
170
- Array(objects).map { |o| conv.call(stream, o) }
206
+ Array(objects).map { |o| conv.call(@tagged, o) }
171
207
  end
172
208
 
173
209
  # Set +stream+ to the beginning.
@@ -180,6 +216,42 @@ module MemoryIO
180
216
 
181
217
  private
182
218
 
219
+ # @api private
220
+ #
221
+ # Read up to +num_elements+ objects, stopping early at the end of stream.
222
+ #
223
+ # An object is only collected if it could be read in full, so a stream
224
+ # that ends mid-object yields the objects preceding it rather than a
225
+ # truncated one.
226
+ #
227
+ # @return [Array<Object>]
228
+ def read_elements(num_elements, conv)
229
+ ret = []
230
+ num_elements.times do
231
+ break if eof?
232
+
233
+ begin
234
+ ret << conv.call(@tagged)
235
+ rescue ::EOFError
236
+ break
237
+ end
238
+ end
239
+ ret
240
+ end
241
+
242
+ # @api private
243
+ #
244
+ # @return [Boolean]
245
+ # Whether +stream+ has no more data to be read.
246
+ def eof?
247
+ return stream.eof? if stream.respond_to?(:eof?)
248
+
249
+ pos = stream.pos
250
+ byte = stream.read(1)
251
+ stream.pos = pos
252
+ byte.nil? || byte.empty?
253
+ end
254
+
183
255
  # @api private
184
256
  def to_proc(as, rw)
185
257
  ret = as.respond_to?(rw) ? as.method(rw) : as
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ # MemoryIO - Read/Write structures in memory.
6
+ module MemoryIO
7
+ class << self
8
+ # Diagnostics that are worth surfacing but don't stop the operation
9
+ # are written here, so they can be silenced or redirected.
10
+ #
11
+ # @return [Logger]
12
+ # Defaults to a logger writing to +$stderr+.
13
+ #
14
+ # @example
15
+ # MemoryIO.logger.level = Logger::ERROR
16
+ #
17
+ # MemoryIO.logger = Logger.new('memory_io.log')
18
+ def logger
19
+ @logger ||= ::Logger.new($stderr, progname: 'memory_io', formatter: FORMATTER)
20
+ end
21
+
22
+ attr_writer :logger
23
+ end
24
+
25
+ # @api private
26
+ #
27
+ # Keeps a message readable when it is shown to a human.
28
+ #
29
+ # @example
30
+ # # [memory_io] WARN: something happened
31
+ FORMATTER = proc { |severity, _datetime, progname, msg| "[#{progname}] #{severity}: #{msg}\n" }
32
+ end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'memory_io/error'
4
+ require 'memory_io/io'
5
+ require 'memory_io/logger'
6
+ require 'memory_io/util'
7
+
3
8
  module MemoryIO
4
9
  # Records information of a process.
5
10
  class Process
@@ -7,25 +12,41 @@ module MemoryIO
7
12
  # @return [#readable?, #writable?]
8
13
  attr_reader :perm
9
14
 
15
+ # @return [MemoryIO::Context]
16
+ # The context of this process's memory.
17
+ attr_reader :context
18
+
10
19
  # @api private
11
20
  #
12
21
  # Create process object from pid.
13
22
  #
14
23
  # @param [Integer] pid
15
24
  # Process id.
25
+ # @param [:little, :big, :native, nil] endian
26
+ # Byte order of the process's memory.
27
+ # @param [Integer?] pointer_size
28
+ # Size of a pointer in the process, in bytes.
29
+ #
30
+ # Both default to what the process's executable declares, so a 32-bit
31
+ # process is read correctly without being told. Pass them to override
32
+ # a target whose executable can't be examined, or names an interpreter
33
+ # rather than the program itself.
34
+ #
35
+ # @raise [MemoryIO::ProcessNotFoundError]
36
+ # The memory of +pid+ is not accessible.
16
37
  #
17
38
  # @note
18
39
  # This class only supports procfs-based system. i.e. /proc is mounted and readable.
19
- def initialize(pid)
40
+ def initialize(pid, endian: nil, pointer_size: nil)
20
41
  @pid = pid
21
42
  @mem = "/proc/#{pid}/mem"
22
43
  # check permission of '/proc/pid/mem'
23
44
  @perm = MemoryIO::Util.file_permission(@mem)
24
- # TODO: raise custom exception
25
- raise Errno::ENOENT, @mem if perm.nil?
45
+ raise MemoryIO::ProcessNotFoundError, "#{@mem} does not exist" if perm.nil?
46
+
47
+ @context = build_context(endian, pointer_size)
26
48
 
27
- # FIXME: use logger
28
- warn(<<-EOS.strip) unless perm.readable? || perm.writable?
49
+ MemoryIO.logger.warn(<<-EOS.strip) unless perm.readable? || perm.writable?
29
50
  You have no permission to read/write this process.
30
51
 
31
52
  Check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
@@ -74,9 +95,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
74
95
 
75
96
  # Read from process's memory.
76
97
  #
77
- # This method has *almost* same arguements and return types as {IO#read}.
98
+ # This method has *almost* same arguments and return types as {IO#read}.
78
99
  # The only difference is this method needs parameter +addr+ (which
79
- # will be passed to paramter +from+ in {IO#read}).
100
+ # will be passed to parameter +from+ in {IO#read}).
80
101
  #
81
102
  # @param [Integer, String] addr
82
103
  # The address start to read.
@@ -89,6 +110,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
89
110
  # @return [String, Object, Array<Object>]
90
111
  # See {IO#read}.
91
112
  #
113
+ # @raise [MemoryIO::InvalidAddressError]
114
+ # +addr+ is an expression that cannot be evaluated.
115
+ #
92
116
  # @example
93
117
  # process = MemoryIO.attach(`pidof victim`.to_i)
94
118
  # puts process.read('heap', 4, as: :u64).map { |c| '0x%016x' % c }
@@ -104,7 +128,7 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
104
128
  # #=> "\x7fELF"
105
129
  # @see IO#read
106
130
  def read(addr, num_elements, **options)
107
- mem_io(:read) { |io| io.read(num_elements, from: MemoryIO::Util.safe_eval(addr, **bases), **options) }
131
+ mem_io(:read) { |io| io.read(num_elements, from: resolve_address(addr), **options) }
108
132
  end
109
133
 
110
134
  # Write objects at +addr+.
@@ -120,6 +144,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
120
144
  #
121
145
  # @return [void]
122
146
  #
147
+ # @raise [MemoryIO::InvalidAddressError]
148
+ # +addr+ is an expression that cannot be evaluated.
149
+ #
123
150
  # @example
124
151
  # process = MemoryIO.attach('self')
125
152
  # s = 'A' * 16
@@ -128,14 +155,53 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
128
155
  # #=> 'BBBBCCCCAAAAAAAA'
129
156
  # @see IO#write
130
157
  def write(addr, objects, **options)
131
- mem_io(:write) { |io| io.write(objects, from: MemoryIO::Util.safe_eval(addr, **bases), **options) }
158
+ mem_io(:write) { |io| io.write(objects, from: resolve_address(addr), **options) }
132
159
  end
133
160
 
134
161
  private
135
162
 
163
+ # The executable of a process describes the memory it runs in, so prefer it
164
+ # over assuming this host's context. What the caller gave wins over both.
165
+ #
166
+ # A process started through an interpreter names the interpreter here, whose
167
+ # context can differ from the program's. Recovering the program's own context
168
+ # would mean picking it out of the mappings, which aren't populated yet when
169
+ # a process is attached to right after it starts, so leave that to the caller.
170
+ #
171
+ # @return [MemoryIO::Context]
172
+ def build_context(endian, pointer_size)
173
+ declared = MemoryIO::Context.from_elf("/proc/#{@pid}/exe") || MemoryIO::Context.new
174
+ MemoryIO::Context.new(endian: endian || declared.endian,
175
+ pointer_size: pointer_size || declared.pointer_size)
176
+ end
177
+
178
+ # Resolve +addr+ into an absolute address.
179
+ #
180
+ # {#bases} is only consulted when +addr+ is an expression that can
181
+ # reference it, so an address that is already absolute costs no extra work.
182
+ #
183
+ # @param [Integer, String] addr
184
+ # The address to resolve.
185
+ #
186
+ # @return [Integer]
187
+ # The resolved address.
188
+ #
189
+ # @raise [MemoryIO::InvalidAddressError]
190
+ # +addr+ is an expression that cannot be evaluated,
191
+ # or evaluates to something that is not an address.
192
+ def resolve_address(addr)
193
+ return addr if addr.is_a?(Integer)
194
+
195
+ address = MemoryIO::Util.safe_eval(addr, **bases)
196
+ raise MemoryIO::InvalidAddressError, "Failed to evaluate address: #{addr.inspect}" unless address.is_a?(Numeric)
197
+ raise MemoryIO::InvalidAddressError, "Address is not an integer: #{addr.inspect}" unless address == address.to_i
198
+
199
+ address.to_i
200
+ end
201
+
136
202
  def mem_io(perm)
137
203
  flags = perm == :write ? 'wb' : 'rb'
138
- File.open(@mem, flags) { |f| yield MemoryIO::IO.new(f) }
204
+ File.open(@mem, flags) { |f| yield MemoryIO::IO.new(f, **@context.to_h) }
139
205
  end
140
206
  end
141
207
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MemoryIO
4
+ # @api private
5
+ #
6
+ # A stream tagged with the {Context} of the memory it accesses.
7
+ #
8
+ # Types are handed one of these instead of the bare stream, so that a type
9
+ # can learn how to interpret the bytes it reads without the reading
10
+ # interface having to grow another argument.
11
+ #
12
+ # Every other message is forwarded, so a stream behaves as it did before.
13
+ class Stream
14
+ # @return [MemoryIO::Context]
15
+ attr_reader :context
16
+
17
+ # @param [#read, #write] stream
18
+ # The stream to be tagged.
19
+ # @param [MemoryIO::Context] context
20
+ # The context of the memory reached through +stream+.
21
+ def initialize(stream, context)
22
+ @stream = stream
23
+ @context = context
24
+ end
25
+
26
+ def read(*)
27
+ @stream.read(*)
28
+ end
29
+
30
+ def write(*)
31
+ @stream.write(*)
32
+ end
33
+
34
+ def pos
35
+ @stream.pos
36
+ end
37
+
38
+ def pos=(val)
39
+ @stream.pos = val
40
+ end
41
+
42
+ private
43
+
44
+ def method_missing(name, *, &)
45
+ return super unless @stream.respond_to?(name)
46
+
47
+ @stream.public_send(name, *, &)
48
+ end
49
+
50
+ def respond_to_missing?(name, include_private = false)
51
+ @stream.respond_to?(name, include_private) || super
52
+ end
53
+ end
54
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'memory_io/context'
4
+ require 'memory_io/error'
3
5
  require 'memory_io/types/type'
4
6
 
5
7
  module MemoryIO
@@ -8,64 +10,144 @@ module MemoryIO
8
10
  module Basic
9
11
  # Register numbers to {Types}.
10
12
  #
11
- # All types registered by this class are assumed as *little endian*.
13
+ # The byte order is taken from the {MemoryIO::Context} of the stream being
14
+ # read, so the same type reads correctly from memory of either byte order.
12
15
  #
13
- # This class registered (un)signed {8, 16, 32, 64)-bit integers and IEEE-754 floating numbers.
16
+ # This class registered (un)signed (8, 16, 32, 64)-bit integers and IEEE-754 floating numbers.
14
17
  class Number
18
+ # Indicators of the integer widths, by size in bytes.
19
+ # A single byte has no byte order to indicate.
20
+ INTEGER_INDICATORS = { 1 => 'C', 2 => 'S', 4 => 'L', 8 => 'Q' }.freeze
21
+
22
+ # Indicators of the real widths, by size in bytes and then byte order.
23
+ #
24
+ # @example
25
+ # # 'e' and 'E' are IEEE-754 single and double precision, little endian.
26
+ REAL_INDICATORS = { 4 => { little: 'e', big: 'g' }, 8 => { little: 'E', big: 'G' } }.freeze
27
+
28
+ # Appended to an integer indicator to fix its byte order.
29
+ ENDIAN_INDICATORS = { little: '<', big: '>' }.freeze
30
+
15
31
  # @param [Integer] bytes
16
32
  # Bytes.
17
33
  # @param [Boolean] signed
18
34
  # Signed or unsigned.
19
- # @param [String] pack_str
20
- # The indicator to be passed to +Array#pack+ and +String#unpack+.
21
- def initialize(bytes, signed, pack_str)
35
+ # @param [Boolean] real
36
+ # Whether this type holds a real number rather than an integer.
37
+ def initialize(bytes, signed, real: false)
22
38
  @bytes = bytes
23
39
  @signed = signed
24
- @pack_str = pack_str
40
+ @real = real
41
+ @range = value_range unless real
42
+ @indicators = ENDIAN_INDICATORS.keys.to_h { |e| [e, indicator(e)] }
25
43
  end
26
44
 
27
45
  # @return [Integer]
46
+ #
47
+ # @raise [EOFError]
48
+ # Fewer than +bytes+ bytes remain in +stream+.
28
49
  def read(stream)
29
- unpack(stream.read(@bytes))
50
+ endian = MemoryIO::Context.of(stream).endian
51
+ unpack(MemoryIO::Util.read_exactly(stream, @bytes), endian)
30
52
  end
31
53
 
32
54
  # @param [Integer] val
55
+ #
56
+ # @raise [MemoryIO::ValueOutOfRangeError]
57
+ # +val+ doesn't fit in this type, which would otherwise be written truncated.
33
58
  def write(stream, val)
34
- stream.write(pack(val))
59
+ endian = MemoryIO::Context.of(stream).endian
60
+ raise MemoryIO::ValueOutOfRangeError, out_of_range_message(val) if out_of_range?(val, endian)
61
+
62
+ stream.write(pack(val, endian))
35
63
  end
36
64
 
37
65
  private
38
66
 
39
- def unpack(str)
40
- val = str.unpack1(@pack_str)
67
+ # Anything that isn't a number of the matching kind is left for
68
+ # +Array#pack+ to reject.
69
+ #
70
+ # @return [Boolean]
71
+ def out_of_range?(val, endian)
72
+ if @range
73
+ val.is_a?(Integer) && !@range.cover?(val)
74
+ else
75
+ val.is_a?(Numeric) && overflows?(val, endian)
76
+ end
77
+ end
78
+
79
+ # A finite value that packs to an infinity has exceeded what the type
80
+ # can represent. Losing precision is inherent to the type and allowed,
81
+ # as is writing an infinity that was asked for.
82
+ #
83
+ # @return [Boolean]
84
+ def overflows?(val, endian)
85
+ val.infinite?.nil? && pack(val, endian).unpack1(@indicators[endian]).infinite?
86
+ end
87
+
88
+ # @return [Range]
89
+ def value_range
90
+ bits = @bytes * 8
91
+ @signed ? (-(2**(bits - 1))..((2**(bits - 1)) - 1)) : (0..((2**bits) - 1))
92
+ end
93
+
94
+ # @return [String]
95
+ def out_of_range_message(val)
96
+ return format('%s exceeds the range of %d-bit floating number', val, @bytes * 8) unless @range
97
+
98
+ format('%s is out of range for %d-bit %s integer (%s..%s)',
99
+ hex(val), @bytes * 8, @signed ? 'signed' : 'unsigned',
100
+ hex(@range.first), hex(@range.last))
101
+ end
102
+
103
+ # +format+'s '%#x' renders a negative number in two's complement notation.
104
+ #
105
+ # @return [String]
106
+ #
107
+ # @example
108
+ # hex(-128)
109
+ # #=> '-0x80'
110
+ def hex(val)
111
+ format('%s0x%x', val.negative? ? '-' : '', val.abs)
112
+ end
113
+
114
+ # @return [String]
115
+ # The +Array#pack+ indicator of this type in +endian+ byte order.
116
+ def indicator(endian)
117
+ return REAL_INDICATORS[@bytes][endian] if @real
118
+ return INTEGER_INDICATORS[@bytes] if @bytes == 1
119
+
120
+ INTEGER_INDICATORS[@bytes] + ENDIAN_INDICATORS[endian]
121
+ end
122
+
123
+ def unpack(str, endian)
124
+ val = str.unpack1(@indicators[endian])
125
+ # a real is already signed by its representation
126
+ return val if @real
127
+
41
128
  val -= (2**(@bytes * 8)) if @signed && val >= (2**((@bytes * 8) - 1))
42
129
  val
43
130
  end
44
131
 
45
- def pack(val)
46
- [val].pack(@pack_str)
132
+ def pack(val, endian)
133
+ [val].pack(@indicators[endian])
47
134
  end
48
135
 
49
136
  # Register (un)signed n-bits integers.
50
- {
51
- 8 => 'C',
52
- 16 => 'S',
53
- 32 => 'I',
54
- 64 => 'Q'
55
- }.each do |t, c|
56
- Type.register(Number.new(t / 8, true, c),
137
+ [8, 16, 32, 64].each do |t|
138
+ Type.register(Number.new(t / 8, true),
57
139
  alias: [:"basic/s#{t}", :"s#{t}"],
58
140
  doc: "A signed #{t}-bit integer.")
59
- Type.register(Number.new(t / 8, false, c),
141
+ Type.register(Number.new(t / 8, false),
60
142
  alias: [:"basic/u#{t}", :"u#{t}"],
61
143
  doc: "An unsigned #{t}-bit integer.")
62
144
  end
63
145
 
64
146
  # Register floating numbers.
65
- Type.register(Number.new(4, false, 'F'),
147
+ Type.register(Number.new(4, true, real: true),
66
148
  alias: %i[basic/float float],
67
149
  doc: 'IEEE-754 32-bit floating number.')
68
- Type.register(Number.new(8, false, 'D'),
150
+ Type.register(Number.new(8, true, real: true),
69
151
  alias: %i[basic/double double],
70
152
  doc: 'IEEE-754 64-bit floating number.')
71
153
  end
@@ -12,17 +12,31 @@ module MemoryIO
12
12
  # A null-terminated string.
13
13
  class CStr < Types::Type
14
14
 
15
+ # Number of bytes to fetch at a time while searching for the terminator.
16
+ CHUNK_SIZE = 1024
17
+
15
18
  # @api private
16
19
  #
20
+ # The terminator is searched for in blocks rather than byte by byte,
21
+ # and +stream+ is left just after it so the next read starts at the
22
+ # following string.
23
+ #
17
24
  # @return [String]
18
25
  # String without null byte.
19
26
  def self.read(stream)
20
27
  ret = +''
21
28
  loop do
22
- c = stream.read(1)
23
- break if c.nil? || c == '' || c == "\x00"
29
+ chunk = stream.read(CHUNK_SIZE)
30
+ break if chunk.nil? || chunk.empty?
31
+
32
+ terminator = chunk.index("\x00")
33
+ if terminator
34
+ ret << chunk[0, terminator]
35
+ stream.pos -= chunk.size - terminator - 1
36
+ break
37
+ end
24
38
 
25
- ret << c
39
+ ret << chunk
26
40
  end
27
41
  ret
28
42
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'memory_io/context'
4
+ require 'memory_io/logger'
3
5
  require 'memory_io/types/type'
6
+ require 'memory_io/util'
4
7
 
5
8
  module MemoryIO
6
9
  module Types
@@ -50,20 +53,20 @@ module MemoryIO
50
53
  # @param [String] str
51
54
  def data=(str)
52
55
  @data = str
53
- warn("Length of str (#{str.size}) is larger than capacity (#{capacity})") if str.size > capacity
56
+ return unless str.size > capacity
57
+
58
+ MemoryIO.logger.warn("Length of str (#{str.size}) is larger than capacity (#{capacity})")
54
59
  end
55
60
 
56
61
  # Custom inspect view.
57
62
  #
58
63
  # @return [String]
59
64
  def inspect
60
- # rubocop:disable Lint/FormatParameterMismatch
61
- format("#<%s @data=%s, @capacity=%d, @dataplus=0x%0#{SIZE_T * 2}x>",
65
+ format('#<%s @data=%s, @capacity=%d, @dataplus=0x%s>',
62
66
  self.class.name,
63
67
  data.inspect,
64
68
  capacity,
65
- dataplus)
66
- # rubocop:enable Lint/FormatParameterMismatch
69
+ dataplus.to_s(16).rjust(SIZE_T * 2, '0'))
67
70
  end
68
71
 
69
72
  class << self
@@ -71,6 +74,11 @@ module MemoryIO
71
74
  #
72
75
  # @return [CPP::String]
73
76
  #
77
+ # @raise [EOFError]
78
+ # The object is incomplete, or its characters cannot be read from
79
+ # where it points. {MemoryIO::IO#read} answers with the objects it
80
+ # read in full rather than one that is partly filled in.
81
+ #
74
82
  # @example
75
83
  # # echo '#include <string>\n#include <cstdio>\nint main() {' > a.cpp && \
76
84
  # # echo 'std::string a="abcd"; printf("%p\\n", &a);' >> a.cpp && \
@@ -85,10 +93,11 @@ module MemoryIO
85
93
  def read(stream)
86
94
  dataplus = read_size_t(stream)
87
95
  length = read_size_t(stream)
88
- union = stream.read(LOCAL_CAPACITY + 1)
96
+ union = MemoryIO::Util.read_exactly(stream, LOCAL_CAPACITY + 1)
89
97
  if length > LOCAL_CAPACITY
90
- capacity = MemoryIO::Util.unpack(union[0, Type::SIZE_T])
91
- data = keep_pos(stream, pos: dataplus) { |s| s.read(length) }
98
+ context = MemoryIO::Context.of(stream)
99
+ capacity = MemoryIO::Util.unpack(union[0, context.pointer_size], context.endian)
100
+ data = keep_pos(stream, pos: dataplus) { |s| MemoryIO::Util.read_exactly(s, length) }
92
101
  else
93
102
  capacity = LOCAL_CAPACITY
94
103
  data = union[0, length]
@@ -19,12 +19,14 @@ module MemoryIO
19
19
  #
20
20
  # @param [Object] object
21
21
  # @param [Array<Symbol>] keys
22
+ # @param [Hash] option
23
+ # Extra options.
22
24
  #
23
- # @option [Thread::Backtrace::Location] caller
24
- # This option should present if and only if +object+ is a subclass of {Types::Type}.
25
- # @option [String] doc
25
+ # @option option [Thread::Backtrace::Location] :caller
26
+ # This option should be present if and only if +object+ is a subclass of {Types::Type}.
27
+ # @option option [String] :doc
26
28
  # Doc-string.
27
- # Automatically parse from caller location if this parameter doesn't present.
29
+ # Automatically parsed from caller location if this option isn't present.
28
30
  def initialize(object, keys, option = {})
29
31
  @obj = object
30
32
  @keys = keys
@@ -1,17 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'memory_io/context'
3
4
  require 'memory_io/types/record'
4
5
  require 'memory_io/util'
5
6
 
6
7
  module MemoryIO
7
8
  module Types
8
- # The base class, all descendants of this class would be consider as a valid 'type'.
9
+ # The base class, all descendants of this class would be considered as a valid 'type'.
9
10
  class Type
10
- # The size of +size_t+. i.e. +sizeof(size_t)+.
11
- SIZE_T = 8
11
+ # The size of +size_t+ assumed when the stream says nothing more specific.
12
+ #
13
+ # @see MemoryIO::Context#pointer_size
14
+ SIZE_T = MemoryIO::Context::DEFAULT_POINTER_SIZE
12
15
 
13
16
  class << self
14
- # Read {Type::SIZE_T} bytes and cast to a little endian unsigned integer.
17
+ # Read one +size_t+ and cast it to an unsigned integer.
18
+ #
19
+ # Its width and byte order are taken from the {MemoryIO::Context} of +stream+.
15
20
  #
16
21
  # @param [#read] stream
17
22
  # Stream to read.
@@ -19,15 +24,21 @@ module MemoryIO
19
24
  # @return [Integer]
20
25
  # Result.
21
26
  #
27
+ # @raise [EOFError]
28
+ # Fewer bytes than a +size_t+ remain in +stream+.
29
+ #
22
30
  # @example
23
31
  # s = StringIO.new("\xEF\xBE\xAD\xDExV4\x00")
24
32
  # Type.read_size_t(s).to_s(16)
25
33
  # #=> '345678deadbeef'
26
34
  def read_size_t(stream)
27
- MemoryIO::Util.unpack(stream.read(SIZE_T))
35
+ context = MemoryIO::Context.of(stream)
36
+ MemoryIO::Util.unpack(MemoryIO::Util.read_exactly(stream, context.pointer_size), context.endian)
28
37
  end
29
38
 
30
- # Pack +val+ into {Type::SIZE_T} bytes and write to +stream+.
39
+ # Pack +val+ into one +size_t+ and write it to +stream+.
40
+ #
41
+ # Its width and byte order are taken from the {MemoryIO::Context} of +stream+.
31
42
  #
32
43
  # @param [#write] stream
33
44
  # Stream to write.
@@ -42,11 +53,16 @@ module MemoryIO
42
53
  # s.string
43
54
  # #=> "\x23\x01\x00\x00\x00\x00\x00\x00"
44
55
  def write_size_t(stream, val)
45
- stream.write(MemoryIO::Util.pack(val, SIZE_T))
56
+ context = MemoryIO::Context.of(stream)
57
+ stream.write(MemoryIO::Util.pack(val, context.pointer_size, context.endian))
46
58
  end
47
59
 
48
60
  # Yield a block and resume the position of stream.
49
61
  #
62
+ # The position is resumed even if the block raises,
63
+ # so a failure while following an invalid pointer doesn't
64
+ # leave the stream at an unexpected position.
65
+ #
50
66
  # @param [#pos, #pos=] stream
51
67
  # Stream.
52
68
  # @param [Integer] pos
@@ -69,9 +85,11 @@ module MemoryIO
69
85
  def keep_pos(stream, pos: nil)
70
86
  org = stream.pos
71
87
  stream.pos = pos if pos
72
- ret = yield stream
73
- stream.pos = org
74
- ret
88
+ begin
89
+ yield stream
90
+ ensure
91
+ stream.pos = org
92
+ end
75
93
  end
76
94
 
77
95
  # @api private
@@ -81,8 +99,9 @@ module MemoryIO
81
99
  # @param [Symbol] symbol
82
100
  # Symbol that has been registered in {.register}.
83
101
  #
84
- # @return [{Symbol => Object}]
85
- # The object that registered in {.register}.
102
+ # @return [MemoryIO::Types::Record?]
103
+ # The record that was registered in {.register},
104
+ # or +nil+ if +symbol+ has never been registered.
86
105
  #
87
106
  # @see .register
88
107
  def find(symbol)
@@ -93,10 +112,12 @@ module MemoryIO
93
112
  #
94
113
  # @param [#read, #write] object
95
114
  # Normally, +object+ is a descendant class of {Type}.
115
+ # @param [Hash] option
116
+ # Extra options.
96
117
  #
97
- # @option [Symbol, Array<Symbol>] alias
118
+ # @option option [Symbol, Array<Symbol>] :alias
98
119
  # Custom symbol name(s) that can be used in {.find}.
99
- # @option [String] doc
120
+ # @option option [String] :doc
100
121
  # Doc string that will be shown in README.md.
101
122
  #
102
123
  # @return [Array<Symbol>]
@@ -117,7 +138,7 @@ module MemoryIO
117
138
  #
118
139
  # @note
119
140
  # If all symbols in +alias+ have been registered, an ArgumentError will be raised.
120
- # However, if at least one of aliases hasn't been used, registration will success.
141
+ # However, if at least one of aliases hasn't been used, registration will succeed.
121
142
  #
122
143
  # @see .find
123
144
  def register(object, option = {})
@@ -18,16 +18,16 @@ module MemoryIO
18
18
  #
19
19
  # This method will search all descendants of {Types::Type}.
20
20
  #
21
- # @return [Symbol] name
21
+ # @param [Symbol] name
22
22
  # Class name to be searched.
23
23
  #
24
24
  # @return [#read, #write]
25
25
  # Any object that implemented method +read+ and +write+.
26
- # Usually returns a class inherit {Types::Type}.
26
+ # Usually returns a class inherited from {Types::Type}.
27
27
  #
28
28
  # @example
29
29
  # Types.find(:c_str)
30
- # #=> MemoryIO::Types::CStr
30
+ # #=> MemoryIO::Types::Clang::CStr
31
31
  #
32
32
  # Types.find(:u64)
33
33
  # #=> #<MemoryIO::Types::Number:0x000055ecc017a310 @bytes=8, @pack_str="Q", @signed=false>
@@ -50,7 +50,7 @@ module MemoryIO
50
50
  #
51
51
  # @example
52
52
  # Types.get_proc(:c_str, :write)
53
- # #=> #<Method: MemoryIO::Types::CStr.write>
53
+ # #=> #<Method: MemoryIO::Types::Clang::CStr.write>
54
54
  # Types.get_proc(:s32, :read)
55
55
  # #=> #<Method: MemoryIO::Types::Number#read>
56
56
  def get_proc(name, rw)
@@ -18,12 +18,12 @@ module MemoryIO
18
18
  @writable = stat.writable_real?
19
19
  # we do a trick here because /proc/[pid]/mem might be marked as writeable but fails at sysopen.
20
20
  begin
21
- @readable && File.open(file, 'rb').close
21
+ @readable && File.open(file, 'rb', &:close)
22
22
  rescue Errno::EACCES
23
23
  @readable = false
24
24
  end
25
25
  begin
26
- @writable && File.open(file, 'wb').close
26
+ @writable && File.open(file, 'wb', &:close)
27
27
  rescue Errno::EACCES
28
28
  @writable = false
29
29
  end
@@ -78,25 +78,53 @@ module MemoryIO
78
78
  # @param [{Symbol => Integer}] vars
79
79
  # Predefined variables
80
80
  #
81
- # @return [Integer]
82
- # Result.
81
+ # @return [Integer?]
82
+ # Result, or +nil+ if +str+ is not a valid expression.
83
83
  #
84
84
  # @example
85
85
  # Util.safe_eval('heap + 0x10 * pp', heap: 0xde00, pp: 8)
86
86
  # #=> 56960 # 0xde80
87
+ #
88
+ # Util.safe_eval('0xzz')
89
+ # #=> nil
87
90
  def safe_eval(str, **vars)
88
91
  return str if str.is_a?(Integer)
89
92
 
90
- # dentaku 2 doesn't support hex
91
- str = str.gsub(/0x[0-9a-zA-Z]+/) { |c| c.to_i(16) }
92
93
  Dentaku::Calculator.new.store(vars).evaluate(str)
93
94
  end
94
95
 
96
+ # @api private
97
+ #
98
+ # Read exactly +size+ bytes from +stream+.
99
+ #
100
+ # @param [#read] stream
101
+ # Stream to read.
102
+ # @param [Integer] size
103
+ # Number of bytes to read.
104
+ #
105
+ # @return [String]
106
+ # The bytes read.
107
+ #
108
+ # @raise [EOFError]
109
+ # Fewer than +size+ bytes remain in +stream+.
110
+ #
111
+ # @example
112
+ # Util.read_exactly(StringIO.new('1234'), 8)
113
+ # # EOFError: Requires 0x8 bytes, but only 0x4 bytes remain
114
+ def read_exactly(stream, size)
115
+ str = stream.read(size)
116
+ remain = str.nil? ? 0 : str.size
117
+ raise ::EOFError, format('Requires 0x%x bytes, but only 0x%x bytes remain', size, remain) if remain < size
118
+
119
+ str
120
+ end
121
+
95
122
  # Unpack a string into an integer.
96
- # Little endian is used.
97
123
  #
98
124
  # @param [String] str
99
125
  # String.
126
+ # @param [:little, :big] endian
127
+ # Byte order of +str+.
100
128
  #
101
129
  # @return [Integer]
102
130
  # Result.
@@ -106,12 +134,14 @@ module MemoryIO
106
134
  # #=> 255
107
135
  # Util.unpack("@\xE2\x01\x00")
108
136
  # #=> 123456
109
- def unpack(str)
110
- str.bytes.reverse.reduce(0) { |s, c| (s * 256) + c }
137
+ # Util.unpack("\x00\x01\xE2@", :big)
138
+ # #=> 123456
139
+ def unpack(str, endian = :little)
140
+ bytes = endian == :little ? str.bytes.reverse : str.bytes
141
+ bytes.reduce(0) { |s, c| (s * 256) + c }
111
142
  end
112
143
 
113
144
  # Pack an integer into +b+ bytes.
114
- # Little endian is used.
115
145
  #
116
146
  # @param [Integer] val
117
147
  # The integer to pack.
@@ -119,6 +149,8 @@ module MemoryIO
119
149
  # only lower +b+ bytes in +val+ will be packed.
120
150
  #
121
151
  # @param [Integer] b
152
+ # @param [:little, :big] endian
153
+ # Byte order to pack +val+ in.
122
154
  #
123
155
  # @return [String]
124
156
  # Packing result with length +b+.
@@ -126,8 +158,12 @@ module MemoryIO
126
158
  # @example
127
159
  # Util.pack(0x123, 4)
128
160
  # #=> "\x23\x01\x00\x00"
129
- def pack(val, b)
130
- Array.new(b) { |i| (val >> (i * 8)) & 0xff }.pack('C*')
161
+ # Util.pack(0x123, 4, :big)
162
+ # #=> "\x00\x00\x01\x23"
163
+ def pack(val, b, endian = :little)
164
+ bytes = Array.new(b) { |i| (val >> (i * 8)) & 0xff }
165
+ bytes.reverse! if endian == :big
166
+ bytes.pack('C*')
131
167
  end
132
168
 
133
169
  # Remove extension name (.so) and version in library name.
@@ -2,5 +2,5 @@
2
2
 
3
3
  module MemoryIO
4
4
  # Current gem version.
5
- VERSION = '0.3.0'
5
+ VERSION = '1.0.0'
6
6
  end
data/lib/memory_io.rb CHANGED
@@ -9,12 +9,36 @@ module MemoryIO
9
9
  # Get a process by process id.
10
10
  #
11
11
  # @param [Integer] pid
12
- # Process Id in linux.
12
+ # Process id in Linux.
13
+ # @param [:little, :big, :native, nil] endian
14
+ # Byte order of the process's memory.
15
+ # @param [Integer?] pointer_size
16
+ # Size of a pointer in the process, in bytes.
17
+ #
18
+ # Both are taken from the process's executable when not given.
13
19
  #
14
20
  # @return [MemoryIO::Process]
15
21
  # A process object for further usage.
16
- def attach(pid)
17
- MemoryIO::Process.new(pid)
22
+ #
23
+ # @raise [MemoryIO::ProcessNotFoundError]
24
+ # The memory of +pid+ is not accessible.
25
+ #
26
+ # @example
27
+ # process = MemoryIO.attach(`pidof victim`.to_i)
28
+ # process.read('heap', 8)
29
+ # @example
30
+ # # a program started through an interpreter is described by the
31
+ # # interpreter, so state the context of the program instead
32
+ # MemoryIO.attach(`pidof victim32`.to_i, pointer_size: 4)
33
+ #
34
+ # @note
35
+ # The context is read from +/proc/[pid]/exe+, which names the interpreter
36
+ # when the process was started through one. Pass +endian+ and
37
+ # +pointer_size+ for such a process, whose context may differ from
38
+ # the interpreter running it.
39
+ # @see MemoryIO::Process#initialize
40
+ def attach(pid, endian: nil, pointer_size: nil)
41
+ MemoryIO::Process.new(pid, endian: endian, pointer_size: pointer_size)
18
42
  end
19
43
  end
20
44
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memory_io
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - david942j
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-11-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: dentaku
@@ -15,14 +15,48 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '3'
18
+ version: '4'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '3'
25
+ version: '4'
26
+ - !ruby/object:Gem::Dependency
27
+ name: elftools
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.3'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '3.0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '1.3'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '3.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: logger
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '1.6'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '1.6'
26
60
  - !ruby/object:Gem::Dependency
27
61
  name: ostruct
28
62
  requirement: !ruby/object:Gem::Requirement
@@ -85,14 +119,14 @@ dependencies:
85
119
  requirements:
86
120
  - - "~>"
87
121
  - !ruby/object:Gem::Version
88
- version: '0.22'
122
+ version: '1.0'
89
123
  type: :development
90
124
  prerelease: false
91
125
  version_requirements: !ruby/object:Gem::Requirement
92
126
  requirements:
93
127
  - - "~>"
94
128
  - !ruby/object:Gem::Version
95
- version: '0.22'
129
+ version: '1.0'
96
130
  - !ruby/object:Gem::Dependency
97
131
  name: yard
98
132
  requirement: !ruby/object:Gem::Requirement
@@ -119,8 +153,12 @@ files:
119
153
  - LICENSE
120
154
  - README.md
121
155
  - lib/memory_io.rb
156
+ - lib/memory_io/context.rb
157
+ - lib/memory_io/error.rb
122
158
  - lib/memory_io/io.rb
159
+ - lib/memory_io/logger.rb
123
160
  - lib/memory_io/process.rb
161
+ - lib/memory_io/stream.rb
124
162
  - lib/memory_io/types/basic/number.rb
125
163
  - lib/memory_io/types/clang/c_str.rb
126
164
  - lib/memory_io/types/cpp/string.rb
@@ -141,14 +179,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
141
179
  requirements:
142
180
  - - ">="
143
181
  - !ruby/object:Gem::Version
144
- version: '3.2'
182
+ version: '3.3'
145
183
  required_rubygems_version: !ruby/object:Gem::Requirement
146
184
  requirements:
147
185
  - - ">="
148
186
  - !ruby/object:Gem::Version
149
187
  version: '0'
150
188
  requirements: []
151
- rubygems_version: 3.6.2
189
+ rubygems_version: 3.6.9
152
190
  specification_version: 4
153
191
  summary: memory_io
154
192
  test_files: []