ruby-macho 0.0.2
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 +7 -0
- data/lib/cstruct.rb +346 -0
- data/lib/int_helpers.rb +17 -0
- data/lib/macho.rb +14 -0
- data/lib/macho/exceptions.rb +54 -0
- data/lib/macho/file.rb +351 -0
- data/lib/macho/headers.rb +122 -0
- data/lib/macho/load_commands.rb +588 -0
- data/lib/macho/sections.rb +114 -0
- data/lib/macho/structure.rb +15 -0
- data/lib/macho/utils.rb +28 -0
- data/lib/otool_helpers.rb +3 -0
- metadata +55 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 686c8eeee9cd964f7c1112259c64ba6f37650262
|
4
|
+
data.tar.gz: 9c7a05e36acc9b2120263e3847ca0dbb8e8f9fd9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ff579faa1788421e587e995b4434ff2e8ef0f5491b33c4d599577ea70fff6ac7ce3b3e4eea166d5e5e94cd5128f3050adab90b38949fdc854419fbacfa059aa2
|
7
|
+
data.tar.gz: 107c76d944b6aa7987ac1c0299f2d49ee8e48eb8a4b8a9fa046ea9fbd6821586910c0892792c14c3aefb094b02d0a2a60c8d811cddc2c3f73309933f330c6b2c
|
data/lib/cstruct.rb
ADDED
@@ -0,0 +1,346 @@
|
|
1
|
+
# Struct does some trickery with custom allocators so we can't
|
2
|
+
# subclass it without writing C. Instead we define a CStruct class
|
3
|
+
# that does something similar enough for our purpose. It is
|
4
|
+
# subclassed just like any other class. A nice side-effect of this
|
5
|
+
# syntax is that it is always clear that a CStruct is just a class and
|
6
|
+
# instances of the struct are objects.
|
7
|
+
#
|
8
|
+
# Some light metaprogramming is used to make the following syntax possible:
|
9
|
+
#
|
10
|
+
# class MachHeader < CStruct
|
11
|
+
# uint :magic
|
12
|
+
# int :cputype
|
13
|
+
# int :cpusubtype
|
14
|
+
# ...
|
15
|
+
# int :flags
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Inheritance works as you would expect.
|
19
|
+
#
|
20
|
+
# class LoadCommand < CStruct
|
21
|
+
# uint32 :cmd
|
22
|
+
# uint32 :cmdsize
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # inherits cmd and cmdsize as the first 2 fields
|
26
|
+
# class SegmentCommand < LoadCommand
|
27
|
+
# string :segname, 16
|
28
|
+
# uint32 :vmaddr
|
29
|
+
# uint32
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Nothing tricky or confusing there. Members of a CStruct class are
|
33
|
+
# declared in the class definition. A different definition using a
|
34
|
+
# more static approach probably wouldn't be very hard... if
|
35
|
+
# performance is critical ... but then why are you using Ruby? ;-)
|
36
|
+
#
|
37
|
+
#
|
38
|
+
# TODO support bit fields
|
39
|
+
#
|
40
|
+
# Bit fields should be supported by passing the number of bits a field
|
41
|
+
# should occupy. Perhaps we could use the size 'pack' for the rest of
|
42
|
+
# the field.
|
43
|
+
#
|
44
|
+
# class RelocationInfo < CStruct
|
45
|
+
# int32 :address
|
46
|
+
# uint32 :symbolnum, 24
|
47
|
+
# pack :pcrel, 1
|
48
|
+
# pack :length, 2
|
49
|
+
# pack :extern, 1
|
50
|
+
# pack :type, 4
|
51
|
+
# end
|
52
|
+
|
53
|
+
class CStruct
|
54
|
+
|
55
|
+
|
56
|
+
###################
|
57
|
+
# Class Constants #
|
58
|
+
###################
|
59
|
+
|
60
|
+
# Size in bytes.
|
61
|
+
SizeMap = {
|
62
|
+
:int8 => 1,
|
63
|
+
:uint8 => 1,
|
64
|
+
:int16 => 2,
|
65
|
+
:uint16 => 2,
|
66
|
+
:int32 => 4,
|
67
|
+
:uint32 => 4,
|
68
|
+
:int64 => 8,
|
69
|
+
:uint64 => 8,
|
70
|
+
:string => lambda { |*opts| opts.first }, # first opt is size
|
71
|
+
# the last 3 are to make the language more C-like
|
72
|
+
:int => 4,
|
73
|
+
:uint => 4,
|
74
|
+
:char => 1
|
75
|
+
}
|
76
|
+
|
77
|
+
# 32-bit
|
78
|
+
PackMap = {
|
79
|
+
:int8 => 'c',
|
80
|
+
:uint8 => 'C',
|
81
|
+
:int16 => 's',
|
82
|
+
:uint16 => 'S',
|
83
|
+
:int32 => 'i',
|
84
|
+
:uint32 => 'I',
|
85
|
+
:int64 => 'q',
|
86
|
+
:uint64 => 'Q',
|
87
|
+
:string => lambda do |str, *opts|
|
88
|
+
len = opts.first
|
89
|
+
str.ljust(len, "\0")[0, len]
|
90
|
+
end,
|
91
|
+
# a few C-like names
|
92
|
+
:int => 'i',
|
93
|
+
:uint => 'I',
|
94
|
+
:char => 'C'
|
95
|
+
}
|
96
|
+
|
97
|
+
# Only needed when unpacking is different from packing, i.e. strings w/ lambdas in PackMap.
|
98
|
+
UnpackMap = {
|
99
|
+
:string => lambda do |str, *opts|
|
100
|
+
len = opts.first
|
101
|
+
val = str[0, len-1].sub(/\0*$/, '')
|
102
|
+
str.slice!((len-1)..-1)
|
103
|
+
val
|
104
|
+
end
|
105
|
+
}
|
106
|
+
|
107
|
+
##########################
|
108
|
+
# Class Instance Methods #
|
109
|
+
##########################
|
110
|
+
|
111
|
+
# Note: const_get and const_set are used so the constants are bound
|
112
|
+
# at runtime, to the real class that has subclassed CStruct.
|
113
|
+
# I figured Ruby would do this but I haven't looked at the
|
114
|
+
# implementation of constants so it might be tricky.
|
115
|
+
#
|
116
|
+
# All of this could probably be avoided with Ruby 1.9 and
|
117
|
+
# private class variables. That is definitely something to
|
118
|
+
# experiment with.
|
119
|
+
|
120
|
+
class <<self
|
121
|
+
|
122
|
+
def inherited(subclass)
|
123
|
+
subclass.instance_eval do
|
124
|
+
|
125
|
+
# These "constants" are only constant references. Structs can
|
126
|
+
# be modified. After the struct is defined it is still open,
|
127
|
+
# but good practice would be not to change a struct after it
|
128
|
+
# has been defined.
|
129
|
+
#
|
130
|
+
# To support inheritance properly we try to get these
|
131
|
+
# constants from the enclosing scope (and clone them before
|
132
|
+
# modifying them!), and default to empty, er, defaults.
|
133
|
+
|
134
|
+
members = const_get(:Members).clone rescue []
|
135
|
+
member_index = const_get(:MemberIndex).clone rescue {}
|
136
|
+
member_sizes = const_get(:MemberSizes).clone rescue {}
|
137
|
+
member_opts = const_get(:MemberOptions).clone rescue {}
|
138
|
+
|
139
|
+
const_set(:Members, members)
|
140
|
+
const_set(:MemberIndex, member_index)
|
141
|
+
const_set(:MemberSizes, member_sizes)
|
142
|
+
const_set(:MemberOptions, member_opts)
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
# Define a method for each size name, and when that method is called it updates
|
149
|
+
# the struct class accordingly.
|
150
|
+
SizeMap.keys.each do |type|
|
151
|
+
|
152
|
+
define_method(type) do |name, *args|
|
153
|
+
name = name.to_sym
|
154
|
+
const_get(:MemberIndex)[name] = const_get(:Members).size
|
155
|
+
const_get(:MemberSizes)[name] = type
|
156
|
+
const_get(:MemberOptions)[name] = args
|
157
|
+
const_get(:Members) << name
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
# Return the number of members.
|
164
|
+
def size
|
165
|
+
const_get(:Members).size
|
166
|
+
end
|
167
|
+
alias_method :length, :size
|
168
|
+
|
169
|
+
# Return the number of bytes occupied in memory or on disk.
|
170
|
+
def bytesize
|
171
|
+
const_get(:Members).inject(0) { |size, name| size + sizeof(name) }
|
172
|
+
end
|
173
|
+
|
174
|
+
def sizeof(name)
|
175
|
+
value = SizeMap[const_get(:MemberSizes)[name]]
|
176
|
+
value.respond_to?(:call) ? value.call(*const_get(:MemberOptions)[name]) : value
|
177
|
+
end
|
178
|
+
|
179
|
+
def new_from_bin(bin)
|
180
|
+
new_struct = new
|
181
|
+
new_struct.unserialize(bin)
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
####################
|
188
|
+
# Instance Methods #
|
189
|
+
####################
|
190
|
+
|
191
|
+
attr_reader :values
|
192
|
+
|
193
|
+
def initialize(*args)
|
194
|
+
@values = args
|
195
|
+
end
|
196
|
+
|
197
|
+
def serialize
|
198
|
+
vals = @values.clone
|
199
|
+
membs = members.clone
|
200
|
+
pack_pattern.map do |patt|
|
201
|
+
name = membs.shift
|
202
|
+
if patt.is_a?(String)
|
203
|
+
[vals.shift].pack(patt)
|
204
|
+
else
|
205
|
+
patt.call(vals.shift, *member_options[name])
|
206
|
+
end
|
207
|
+
end.join
|
208
|
+
end
|
209
|
+
|
210
|
+
def unserialize(bin)
|
211
|
+
bin = bin.clone
|
212
|
+
@values = []
|
213
|
+
membs = members.clone
|
214
|
+
unpack_pattern.each do |patt|
|
215
|
+
name = membs.shift
|
216
|
+
if patt.is_a?(String)
|
217
|
+
@values += bin.unpack(patt)
|
218
|
+
bin.slice!(0, sizeof(name))
|
219
|
+
else
|
220
|
+
@values << patt.call(bin, *member_options[name])
|
221
|
+
end
|
222
|
+
end
|
223
|
+
self
|
224
|
+
end
|
225
|
+
|
226
|
+
def pack_pattern
|
227
|
+
members.map { |name| PackMap[member_sizes[name]] }
|
228
|
+
end
|
229
|
+
|
230
|
+
def unpack_pattern
|
231
|
+
members.map { |name| UnpackMap[member_sizes[name]] || PackMap[member_sizes[name]] }
|
232
|
+
end
|
233
|
+
|
234
|
+
def [](name_or_idx)
|
235
|
+
case name_or_idx
|
236
|
+
|
237
|
+
when Numeric
|
238
|
+
idx = name_or_idx
|
239
|
+
@values[idx]
|
240
|
+
|
241
|
+
when String, Symbol
|
242
|
+
name = name_or_idx.to_sym
|
243
|
+
@values[member_index[name]]
|
244
|
+
|
245
|
+
else
|
246
|
+
raise ArgumentError, "expected name or index, got #{name_or_idx.inspect}"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def []=(name_or_idx, value)
|
251
|
+
case name_or_idx
|
252
|
+
|
253
|
+
when Numeric
|
254
|
+
idx = name_or_idx
|
255
|
+
@values[idx] = value
|
256
|
+
|
257
|
+
when String, Symbol
|
258
|
+
name = name_or_idx.to_sym
|
259
|
+
@values[member_index[name]] = value
|
260
|
+
|
261
|
+
else
|
262
|
+
raise ArgumentError, "expected name or index, got #{name_or_idx.inspect}"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def ==(other)
|
267
|
+
puts @values.inspect
|
268
|
+
puts other.values.inspect
|
269
|
+
other.is_a?(self.class) && other.values == @values
|
270
|
+
end
|
271
|
+
|
272
|
+
# Some of these are just to quack like Ruby's built-in Struct. YAGNI, but can't hurt either.
|
273
|
+
|
274
|
+
def each(&block)
|
275
|
+
@values.each(&block)
|
276
|
+
end
|
277
|
+
|
278
|
+
def each_pair(&block)
|
279
|
+
members.zip(@values).each(&block)
|
280
|
+
end
|
281
|
+
|
282
|
+
def size
|
283
|
+
members.size
|
284
|
+
end
|
285
|
+
alias_method :length, :size
|
286
|
+
|
287
|
+
def sizeof(name)
|
288
|
+
self.class.sizeof(name)
|
289
|
+
end
|
290
|
+
|
291
|
+
def bytesize
|
292
|
+
self.class.bytesize
|
293
|
+
end
|
294
|
+
|
295
|
+
alias_method :to_a, :values
|
296
|
+
|
297
|
+
|
298
|
+
# A few convenience methods.
|
299
|
+
|
300
|
+
def members
|
301
|
+
self.class::Members
|
302
|
+
end
|
303
|
+
|
304
|
+
def member_index
|
305
|
+
self.class::MemberIndex
|
306
|
+
end
|
307
|
+
|
308
|
+
def member_sizes
|
309
|
+
self.class::MemberSizes
|
310
|
+
end
|
311
|
+
|
312
|
+
def member_options
|
313
|
+
self.class::MemberOptions
|
314
|
+
end
|
315
|
+
|
316
|
+
# The last expression is returned, so return self instead of junk.
|
317
|
+
self
|
318
|
+
end
|
319
|
+
|
320
|
+
|
321
|
+
# a small test
|
322
|
+
if $0 == __FILE__
|
323
|
+
class MachHeader < CStruct
|
324
|
+
uint :magic
|
325
|
+
int :cputype
|
326
|
+
int :cpusubtype
|
327
|
+
string :segname, 16
|
328
|
+
end
|
329
|
+
puts MachHeader::Members.inspect
|
330
|
+
puts MachHeader::MemberIndex.inspect
|
331
|
+
puts MachHeader::MemberSizes.inspect
|
332
|
+
puts "# of MachHeader members: " + MachHeader.size.to_s + ", size in bytes: " + MachHeader.bytesize.to_s
|
333
|
+
mh = MachHeader.new(0xfeedface, 7, 3, "foobar")
|
334
|
+
%w[magic cputype cpusubtype segname].each do |field|
|
335
|
+
puts "#{field}(#{MachHeader.sizeof(field.to_sym)}): #{mh[field.to_sym].inspect}"
|
336
|
+
end
|
337
|
+
puts mh.pack_pattern.inspect
|
338
|
+
binstr = mh.serialize
|
339
|
+
puts "values: " + mh.values.inspect
|
340
|
+
newmh = MachHeader.new_from_bin(binstr)
|
341
|
+
puts "new values: " + newmh.values.inspect
|
342
|
+
newbinstr = newmh.serialize
|
343
|
+
puts "serialized: " + binstr.inspect
|
344
|
+
puts "unserialized: " + newbinstr.inspect
|
345
|
+
puts "new == old ? " + (newbinstr == binstr).to_s
|
346
|
+
end
|
data/lib/int_helpers.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module INTHelpers
|
2
|
+
Change = Struct.new("Changes", :old, :new)
|
3
|
+
|
4
|
+
Rpath = Struct.new("Rpaths", :old, :new, :found) do
|
5
|
+
def found?
|
6
|
+
found
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
AddRpath = Struct.new("AddRpaths", :new)
|
11
|
+
|
12
|
+
DeleteRpath = Struct.new("DeleteRpaths", :old, :found) do
|
13
|
+
def found?
|
14
|
+
found
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/macho.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require "cstruct"
|
4
|
+
require "macho/headers"
|
5
|
+
require "macho/structure"
|
6
|
+
require "macho/load_commands"
|
7
|
+
require "macho/sections"
|
8
|
+
require "macho/file"
|
9
|
+
require "macho/exceptions"
|
10
|
+
require "macho/utils"
|
11
|
+
|
12
|
+
module MachO
|
13
|
+
# nothing to see here.
|
14
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module MachO
|
2
|
+
# generic toplevel error
|
3
|
+
class MachOError < RuntimeError
|
4
|
+
end
|
5
|
+
|
6
|
+
# raised when a file's magic bytes are not valid mach-o magic
|
7
|
+
class MagicError < MachOError
|
8
|
+
def initialize(num)
|
9
|
+
super "Unrecognized Mach-O magic: 0x#{"%02x" % num}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# raised when a file's magic bytes are those of a fat binary
|
14
|
+
class FatBinaryError < MachOError
|
15
|
+
def initialize(num)
|
16
|
+
super "Unsupported fat binary (magic 0x#{"%02x" % num})"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class CPUTypeError < MachOError
|
21
|
+
def initialize(num)
|
22
|
+
super "Unrecognized CPU type: 0x#{"%02x" % num}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class CPUSubtypeError < MachOError
|
27
|
+
def initialize(num)
|
28
|
+
super "Unrecognized CPU sub-type: 0x#{"%02x" % num}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# raised when a mach-o file's filetype field is unknown
|
33
|
+
class FiletypeError < MachOError
|
34
|
+
def initialize(num)
|
35
|
+
super "Unrecognized Mach-O filetype code: 0x#{"%02x" % num}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# raised when an unknown load command is encountered
|
40
|
+
class LoadCommandError < MachOError
|
41
|
+
def initialize(num)
|
42
|
+
super "Unrecognized Mach-O load command: 0x#{"%02x" % num}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# raised when load commands are too large to fit in the current file
|
47
|
+
class HeaderPadError < MachOError
|
48
|
+
def initialize(filename)
|
49
|
+
super "Updated load commands do not fit in the header of " +
|
50
|
+
"#{filename}. #{filename} needs to be relinked, possibly with " +
|
51
|
+
"-headerpad or -headerpad_max_install_names"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/macho/file.rb
ADDED
@@ -0,0 +1,351 @@
|
|
1
|
+
module MachO
|
2
|
+
class MachOFile
|
3
|
+
attr_reader :header, :load_commands
|
4
|
+
|
5
|
+
def initialize(filename)
|
6
|
+
raise ArgumentError.new("filename must be a String") unless filename.is_a? String
|
7
|
+
|
8
|
+
@filename = filename
|
9
|
+
@raw_data = open(@filename, "rb") { |f| f.read }
|
10
|
+
@header = get_mach_header
|
11
|
+
@load_commands = get_load_commands
|
12
|
+
end
|
13
|
+
|
14
|
+
def magic32?
|
15
|
+
Utils.magic32?(header[:magic])
|
16
|
+
end
|
17
|
+
|
18
|
+
def magic64?
|
19
|
+
Utils.magic64?(header[:magic])
|
20
|
+
end
|
21
|
+
|
22
|
+
# is the file executable?
|
23
|
+
def executable?
|
24
|
+
header[:filetype] == MH_EXECUTE
|
25
|
+
end
|
26
|
+
|
27
|
+
# is the file a dynamically bound shared object?
|
28
|
+
def dylib?
|
29
|
+
header[:filetype] == MH_DYLIB
|
30
|
+
end
|
31
|
+
|
32
|
+
# is the file a dynamically bound bundle?
|
33
|
+
def bundle?
|
34
|
+
header[:filetype] == MH_BUNDLE
|
35
|
+
end
|
36
|
+
|
37
|
+
def magic
|
38
|
+
header[:magic]
|
39
|
+
end
|
40
|
+
|
41
|
+
# string representation of the header's magic bytes
|
42
|
+
def magic_string
|
43
|
+
MH_MAGICS[header[:magic]]
|
44
|
+
end
|
45
|
+
|
46
|
+
# string representation of the header's filetype field
|
47
|
+
def filetype
|
48
|
+
MH_FILETYPES[header[:filetype]]
|
49
|
+
end
|
50
|
+
|
51
|
+
# string representation of the header's cputype field
|
52
|
+
def cputype
|
53
|
+
CPU_TYPES[header[:cputype]]
|
54
|
+
end
|
55
|
+
|
56
|
+
# string representation of the header's cpusubtype field
|
57
|
+
def cpusubtype
|
58
|
+
CPU_SUBTYPES[header[:cpusubtype]]
|
59
|
+
end
|
60
|
+
|
61
|
+
# number of load commands in the header
|
62
|
+
def ncmds
|
63
|
+
header[:ncmds]
|
64
|
+
end
|
65
|
+
|
66
|
+
# size of all load commands
|
67
|
+
def sizeofcmds
|
68
|
+
header[:sizeofcmds]
|
69
|
+
end
|
70
|
+
|
71
|
+
# various execution flags
|
72
|
+
def flags
|
73
|
+
header[:flags]
|
74
|
+
end
|
75
|
+
|
76
|
+
# get load commands by name
|
77
|
+
def command(name)
|
78
|
+
load_commands.select { |lc| lc.to_s == name }
|
79
|
+
end
|
80
|
+
|
81
|
+
alias :[] :command
|
82
|
+
|
83
|
+
# get all segment commands
|
84
|
+
def segments
|
85
|
+
if magic32?
|
86
|
+
command("LC_SEGMENT")
|
87
|
+
else
|
88
|
+
command("LC_SEGMENT_64")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# get the file's dylib id, if it is a dylib
|
93
|
+
def dylib_id
|
94
|
+
if !dylib?
|
95
|
+
return nil
|
96
|
+
end
|
97
|
+
|
98
|
+
dylib_id_cmd = command('LC_ID_DYLIB').first
|
99
|
+
|
100
|
+
cmdsize = dylib_id_cmd.cmdsize
|
101
|
+
offset = dylib_id_cmd.offset
|
102
|
+
stroffset = dylib_id_cmd.name
|
103
|
+
|
104
|
+
dylib_id = @raw_data.slice(offset + stroffset...offset + cmdsize).unpack("Z*").first
|
105
|
+
|
106
|
+
dylib_id
|
107
|
+
end
|
108
|
+
|
109
|
+
def dylib_id=(new_id)
|
110
|
+
if !new_id.is_a?(String)
|
111
|
+
raise ArgumentError.new("argument must be a String")
|
112
|
+
end
|
113
|
+
|
114
|
+
if !dylib?
|
115
|
+
return nil
|
116
|
+
end
|
117
|
+
|
118
|
+
if magic32?
|
119
|
+
cmd_round = 4
|
120
|
+
else
|
121
|
+
cmd_round = 8
|
122
|
+
end
|
123
|
+
|
124
|
+
new_sizeofcmds = header[:sizeofcmds]
|
125
|
+
dylib_id_cmd = command('LC_ID_DYLIB').first
|
126
|
+
old_id = dylib_id
|
127
|
+
|
128
|
+
new_pad = Utils.round(new_id.size, cmd_round) - new_id.size
|
129
|
+
old_pad = Utils.round(old_id.size, cmd_round) - old_id.size
|
130
|
+
|
131
|
+
# pad the old and new IDs with null bytes to meet command bounds
|
132
|
+
old_id << "\x00" * old_pad
|
133
|
+
new_id << "\x00" * new_pad
|
134
|
+
|
135
|
+
# calculate the new size of the DylibCommand and sizeofcmds in MH
|
136
|
+
new_size = DylibCommand.bytesize + new_id.size
|
137
|
+
new_sizeofcmds += new_size - dylib_id_cmd.cmdsize
|
138
|
+
|
139
|
+
# calculate the low file offset (offset to first section data)
|
140
|
+
low_fileoff = 2**64 # ULLONGMAX
|
141
|
+
|
142
|
+
segments.each do |seg|
|
143
|
+
sections(seg).each do |sect|
|
144
|
+
if sect.size != 0 && !sect.flag?(S_ZEROFILL) &&
|
145
|
+
!sect.flag?(S_THREAD_LOCAL_ZEROFILL) &&
|
146
|
+
sect.offset < low_fileoff
|
147
|
+
|
148
|
+
low_fileoff = sect.offset
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
if new_sizeofcmds + header.bytesize > low_fileoff
|
154
|
+
raise HeaderPadError.new(@filename)
|
155
|
+
end
|
156
|
+
|
157
|
+
# update sizeofcmds in mach_header
|
158
|
+
set_sizeofcmds(new_sizeofcmds)
|
159
|
+
|
160
|
+
# update cmdsize in the dylib_command
|
161
|
+
@raw_data[dylib_id_cmd.offset + 4, 4] = [new_size].pack("V")
|
162
|
+
|
163
|
+
# delete the old id
|
164
|
+
@raw_data.slice!(dylib_id_cmd.offset + dylib_id_cmd.name...dylib_id_cmd.offset + dylib_id_cmd.cmdsize)
|
165
|
+
|
166
|
+
# insert the new id
|
167
|
+
@raw_data.insert(dylib_id_cmd.offset + dylib_id_cmd.name, new_id)
|
168
|
+
|
169
|
+
# pad/unpad after new_sizeofcmds until offsets are corrected
|
170
|
+
null_pad = old_id.size - new_id.size
|
171
|
+
|
172
|
+
if null_pad < 0
|
173
|
+
@raw_data.slice!(new_sizeofcmds + header.bytesize, null_pad.abs)
|
174
|
+
else
|
175
|
+
@raw_data.insert(new_sizeofcmds + header.bytesize, "\x00" * null_pad)
|
176
|
+
end
|
177
|
+
|
178
|
+
# synchronize fields with the raw data
|
179
|
+
header = get_mach_header
|
180
|
+
load_commands = get_load_commands
|
181
|
+
end
|
182
|
+
|
183
|
+
# get a list of dylib paths linked to this file
|
184
|
+
def linked_dylibs
|
185
|
+
dylibs = []
|
186
|
+
dylib_cmds = command('LC_LOAD_DYLIB')
|
187
|
+
|
188
|
+
dylib_cmds.each do |dylib_cmd|
|
189
|
+
cmdsize = dylib_cmd.cmdsize
|
190
|
+
offset = dylib_cmd.offset
|
191
|
+
stroffset = dylib_cmd.name
|
192
|
+
|
193
|
+
dylib = @raw_data.slice(offset + stroffset...offset + cmdsize).unpack("Z*").first
|
194
|
+
|
195
|
+
dylibs << dylib
|
196
|
+
end
|
197
|
+
|
198
|
+
dylibs
|
199
|
+
end
|
200
|
+
|
201
|
+
# get all sections in a segment by name
|
202
|
+
def sections(segment)
|
203
|
+
sections = []
|
204
|
+
|
205
|
+
if !segment.is_a?(SegmentCommand) && !segment.is_a?(SegmentCommand64)
|
206
|
+
raise ArgumentError.new("not a valid segment")
|
207
|
+
end
|
208
|
+
|
209
|
+
if segment.nsects.zero?
|
210
|
+
return sections
|
211
|
+
end
|
212
|
+
|
213
|
+
offset = segment.offset + segment.class.bytesize
|
214
|
+
|
215
|
+
segment.nsects.times do
|
216
|
+
if segment.is_a? SegmentCommand
|
217
|
+
sections << Section.new_from_bin(@raw_data.slice(offset, Section.bytesize))
|
218
|
+
offset += Section.bytesize
|
219
|
+
else
|
220
|
+
sections << Section64.new_from_bin(@raw_data.slice(offset, Section64.bytesize))
|
221
|
+
offset += Section64.bytesize
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
sections
|
226
|
+
end
|
227
|
+
|
228
|
+
def write(filename)
|
229
|
+
File.open(filename, "wb") { |f| f.write(@raw_data) }
|
230
|
+
end
|
231
|
+
|
232
|
+
def write!
|
233
|
+
File.open(@filename, "wb") { |f| f.write(@raw_data) }
|
234
|
+
end
|
235
|
+
|
236
|
+
#######
|
237
|
+
private
|
238
|
+
|
239
|
+
def get_mach_header
|
240
|
+
magic = get_magic
|
241
|
+
cputype = get_cputype
|
242
|
+
cpusubtype = get_cpusubtype
|
243
|
+
filetype = get_filetype
|
244
|
+
ncmds = get_ncmds
|
245
|
+
sizeofcmds = get_sizeofcmds
|
246
|
+
flags = get_flags
|
247
|
+
|
248
|
+
if Utils.magic32?(magic)
|
249
|
+
header = MachHeader.new(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags)
|
250
|
+
else
|
251
|
+
# the reserved field is reserved, so just fill it with 0
|
252
|
+
header = MachHeader64.new(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags, 0)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def get_magic
|
257
|
+
magic = @raw_data[0..3].unpack("N").first
|
258
|
+
|
259
|
+
if !Utils.magic?(magic)
|
260
|
+
raise MagicError.new(magic)
|
261
|
+
end
|
262
|
+
|
263
|
+
# TODO: support fat (universal) binaries
|
264
|
+
if Utils.fat_magic?(magic)
|
265
|
+
raise FatBinaryError.new(magic)
|
266
|
+
end
|
267
|
+
|
268
|
+
magic
|
269
|
+
end
|
270
|
+
|
271
|
+
def get_cputype
|
272
|
+
cputype = @raw_data[4..7].unpack("V").first
|
273
|
+
|
274
|
+
if !CPU_TYPES.keys.include?(cputype)
|
275
|
+
raise CPUTypeError.new(cputype)
|
276
|
+
end
|
277
|
+
|
278
|
+
cputype
|
279
|
+
end
|
280
|
+
|
281
|
+
def get_cpusubtype
|
282
|
+
cpusubtype = @raw_data[8..11].unpack("V").first
|
283
|
+
|
284
|
+
# this mask isn't documented!
|
285
|
+
cpusubtype &= ~CPU_SUBTYPE_LIB64
|
286
|
+
|
287
|
+
if !CPU_SUBTYPES.keys.include?(cpusubtype)
|
288
|
+
raise CPUSubtypeError.new(cpusubtype)
|
289
|
+
end
|
290
|
+
|
291
|
+
cpusubtype
|
292
|
+
end
|
293
|
+
|
294
|
+
def get_filetype
|
295
|
+
filetype = @raw_data[12..15].unpack("V").first
|
296
|
+
|
297
|
+
if !MH_FILETYPES.keys.include?(filetype)
|
298
|
+
raise FiletypeError.new(filetype)
|
299
|
+
end
|
300
|
+
|
301
|
+
filetype
|
302
|
+
end
|
303
|
+
|
304
|
+
def get_ncmds
|
305
|
+
ncmds = @raw_data[16..19].unpack("V").first
|
306
|
+
|
307
|
+
ncmds
|
308
|
+
end
|
309
|
+
|
310
|
+
def get_sizeofcmds
|
311
|
+
sizeofcmds = @raw_data[20..23].unpack("V").first
|
312
|
+
|
313
|
+
sizeofcmds
|
314
|
+
end
|
315
|
+
|
316
|
+
# TODO: parse flags, maybe?
|
317
|
+
def get_flags
|
318
|
+
flags = @raw_data[24..27].unpack("V").first
|
319
|
+
|
320
|
+
flags
|
321
|
+
end
|
322
|
+
|
323
|
+
def get_load_commands
|
324
|
+
offset = header.bytesize
|
325
|
+
load_commands = []
|
326
|
+
|
327
|
+
header[:ncmds].times do
|
328
|
+
cmd = @raw_data.slice(offset, 4).unpack("V").first
|
329
|
+
|
330
|
+
if !LC_STRUCTURES.has_key?(cmd)
|
331
|
+
raise LoadCommandError.new(cmd)
|
332
|
+
end
|
333
|
+
|
334
|
+
# why do I do this? i don't like declaring constants below
|
335
|
+
# classes, and i need them to resolve...
|
336
|
+
klass = Object.const_get "MachO::#{LC_STRUCTURES[cmd]}"
|
337
|
+
command = klass.new_from_bin(offset, @raw_data.slice(offset, klass.bytesize))
|
338
|
+
|
339
|
+
load_commands << command
|
340
|
+
offset += command.cmdsize
|
341
|
+
end
|
342
|
+
|
343
|
+
load_commands
|
344
|
+
end
|
345
|
+
|
346
|
+
def set_sizeofcmds(size)
|
347
|
+
new_size = [size].pack("V")
|
348
|
+
@raw_data[20..23] = new_size
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|