memory_io 0.2.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: 3de5ed7bf7e1f8543bbb290ed472f008ed9d8f4c1af032a11831bd904378c11e
4
- data.tar.gz: e99b29afb7cdcc7941bb6249bbe453cea2cb3a6cb6eab72a8965bfa5ef9802e3
3
+ metadata.gz: ae436fb71c1c77ce99b26657dc3e9497e8730ee578f0fe412186a67ee4881d12
4
+ data.tar.gz: 822612ae59c6c669a6136724dc9636349a3f6b3b314b20d3e024bc1481faf9e2
5
5
  SHA512:
6
- metadata.gz: d4d3d9e60c649dd8d4e6f3c3dfe3bb777d2a7ffd2065699e3f86fef27f9ddf079b90acc7d7a5be244301d45ba46d9c713b950bbaa7a113843631287fb1e9653f
7
- data.tar.gz: 2246f04654d036e8df50440cf2c3d9ebe50933921f5720bf5f69308648ab34849d869b3969f581579dba8930ae1fbd0e5a1d6e27246900129cef39212e2876bf
6
+ metadata.gz: 1fc69554905f1fbd661045c65fd0570e0baea78191d531a7faa39157f9a5faa6618f3693a2cabd7f2117a7a849cd1fc5925123868103b8a414359ed49843068e
7
+ data.tar.gz: f5449be7a6af8e443e857492aa73533735fdc639d4215a79c4b7b0a7effae9db34d5511ec46fec11aba545185f0d6b9f9947ab02f9de88d17a10b78901a96c19
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 david942j
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- [![Build Status](https://travis-ci.org/david942j/memory_io.svg?branch=master)](https://travis-ci.org/david942j/memory_io)
2
1
  [![Gem Version](https://badge.fury.io/rb/memory_io.svg)](https://badge.fury.io/rb/memory_io)
3
- [![Maintainability](https://api.codeclimate.com/v1/badges/dc8da34c5a8ab0095530/maintainability)](https://codeclimate.com/github/david942j/memory_io/maintainability)
4
- [![Test Coverage](https://api.codeclimate.com/v1/badges/dc8da34c5a8ab0095530/test_coverage)](https://codeclimate.com/github/david942j/memory_io/test_coverage)
5
- [![Inline docs](https://inch-ci.org/github/david942j/memory_io.svg?branch=master)](https://inch-ci.org/github/david942j/memory_io)
2
+ [![Build Status](https://github.com/david942j/memory_io/workflows/build/badge.svg)](https://github.com/david942j/memory_io/actions)
3
+ [![Maintainability](https://qlty.sh/gh/david942j/projects/memory_io/maintainability.svg)](https://qlty.sh/gh/david942j/projects/memory_io)
4
+ [![Code Coverage](https://qlty.sh/gh/david942j/projects/memory_io/coverage.svg)](https://qlty.sh/gh/david942j/projects/memory_io)
5
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/github/david942j/memory_io/)
6
6
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](http://choosealicense.com/licenses/mit/)
7
7
 
8
8
  # MemoryIO
@@ -14,8 +14,8 @@ Read/Write complicated structures in memory easily.
14
14
  I usually need to dump a structure, say `string` in C++, from memory for debugging.
15
15
  This is not hard if using gdb.
16
16
  However, gdb doesn't support writing Ruby scripts
17
- (unless you use [gdb-ruby](https://github.com/david942j/gdb-ruby), which has dependency of **MemoryIO**).
18
- So I create this repo and want to make the debug procedure much easier.
17
+ (unless you use [gdb-ruby](https://github.com/david942j/gdb-ruby), which has **MemoryIO** as its dependency).
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
 
@@ -25,16 +25,14 @@ This repository has two main goals:
25
25
  ## Why
26
26
 
27
27
  It's not hard to read/write a process's memory (simply open the file `/proc/$PID/mem`),
28
- but it still worth to wrap it.
28
+ but it's still worthy to make a utility.
29
29
 
30
- This repo also targets to collect all common structures, such as how to parse a C++/Rust/Python object from memory.
30
+ This project also targets to collect all common structures, such as how to parse a C++/Rust/Python object from memory.
31
31
  Therefore, **Pull Requests of adding new structures** are welcome :D
32
32
 
33
33
  ## Supported Platform
34
34
 
35
35
  - Linux
36
- - (TODO) Windows
37
- - (TODO) MacOS
38
36
 
39
37
  ## Implemented Structures
40
38
 
@@ -122,7 +120,7 @@ string
122
120
  require 'memory_io'
123
121
  process = MemoryIO.attach(`pidof victim`.to_i)
124
122
 
125
- # An example that read a chunk of pt-malloc.
123
+ # An example that reads a chunk of pt-malloc.
126
124
  read_chunk = lambda do |stream|
127
125
  _prev_size = stream.read(8)
128
126
  size = (stream.read(8).unpack('Q').first & -16) - 8
@@ -169,6 +167,34 @@ process.read('libc', 4)
169
167
 
170
168
  ## Developing
171
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
+
172
179
  ### To Add a New Structure
173
180
 
174
- 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,10 +216,46 @@ 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
186
- ret = ret.respond_to?(:call) ? ret : MemoryIO::Types.get_proc(ret, rw)
258
+ ret = MemoryIO::Types.get_proc(ret, rw) unless ret.respond_to?(:call)
187
259
  raise ArgumentError, <<-EOERR.strip unless ret.respond_to?(:call)
188
260
 
189
261
  Invalid argument `as`: #{as.inspect}. It should be either a Proc or a supported type of MemoryIO::Types.
@@ -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,30 +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
- #
20
- # @todo
21
- # Support MacOS
22
- # @todo
23
- # Support Windows
24
- def initialize(pid)
40
+ def initialize(pid, endian: nil, pointer_size: nil)
25
41
  @pid = pid
26
42
  @mem = "/proc/#{pid}/mem"
27
43
  # check permission of '/proc/pid/mem'
28
44
  @perm = MemoryIO::Util.file_permission(@mem)
29
- # TODO: raise custom exception
30
- 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)
31
48
 
32
- # FIXME: use logger
33
- warn(<<-EOS.strip) unless perm.readable? || perm.writable?
49
+ MemoryIO.logger.warn(<<-EOS.strip) unless perm.readable? || perm.writable?
34
50
  You have no permission to read/write this process.
35
51
 
36
52
  Check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
@@ -79,9 +95,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
79
95
 
80
96
  # Read from process's memory.
81
97
  #
82
- # 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}.
83
99
  # The only difference is this method needs parameter +addr+ (which
84
- # will be passed to paramter +from+ in {IO#read}).
100
+ # will be passed to parameter +from+ in {IO#read}).
85
101
  #
86
102
  # @param [Integer, String] addr
87
103
  # The address start to read.
@@ -94,6 +110,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
94
110
  # @return [String, Object, Array<Object>]
95
111
  # See {IO#read}.
96
112
  #
113
+ # @raise [MemoryIO::InvalidAddressError]
114
+ # +addr+ is an expression that cannot be evaluated.
115
+ #
97
116
  # @example
98
117
  # process = MemoryIO.attach(`pidof victim`.to_i)
99
118
  # puts process.read('heap', 4, as: :u64).map { |c| '0x%016x' % c }
@@ -109,7 +128,7 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
109
128
  # #=> "\x7fELF"
110
129
  # @see IO#read
111
130
  def read(addr, num_elements, **options)
112
- 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) }
113
132
  end
114
133
 
115
134
  # Write objects at +addr+.
@@ -125,6 +144,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
125
144
  #
126
145
  # @return [void]
127
146
  #
147
+ # @raise [MemoryIO::InvalidAddressError]
148
+ # +addr+ is an expression that cannot be evaluated.
149
+ #
128
150
  # @example
129
151
  # process = MemoryIO.attach('self')
130
152
  # s = 'A' * 16
@@ -133,14 +155,53 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
133
155
  # #=> 'BBBBCCCCAAAAAAAA'
134
156
  # @see IO#write
135
157
  def write(addr, objects, **options)
136
- 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) }
137
159
  end
138
160
 
139
161
  private
140
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
+
141
202
  def mem_io(perm)
142
203
  flags = perm == :write ? 'wb' : 'rb'
143
- File.open(@mem, flags) { |f| yield MemoryIO::IO.new(f) }
204
+ File.open(@mem, flags) { |f| yield MemoryIO::IO.new(f, **@context.to_h) }
144
205
  end
145
206
  end
146
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