Proyectos de Subversion Iphone Microlearning

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// Copyright 2019 Google
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//      http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
 
15
#include "Crashlytics/Crashlytics/Components/FIRCLSBinaryImage.h"
16
 
17
#include <libkern/OSAtomic.h>
18
#include <mach-o/dyld.h>
19
 
20
#include <mach-o/getsect.h>
21
 
22
#include <stdatomic.h>
23
 
24
#include "Crashlytics/Crashlytics/Components/FIRCLSGlobals.h"
25
#include "Crashlytics/Crashlytics/Components/FIRCLSHost.h"
26
#include "Crashlytics/Crashlytics/Helpers/FIRCLSFeatures.h"
27
#include "Crashlytics/Crashlytics/Helpers/FIRCLSFile.h"
28
#include "Crashlytics/Crashlytics/Helpers/FIRCLSUtility.h"
29
#include "Crashlytics/Shared/FIRCLSByteUtility.h"
30
#include "Crashlytics/Shared/FIRCLSMachO/FIRCLSMachO.h"
31
 
32
#include <dispatch/dispatch.h>
33
 
34
// this is defined only if __OPEN_SOURCE__ is *not* defined in the TVOS SDK's mach-o/loader.h
35
// also, it has not yet made it back to the OSX SDKs, for example
36
#ifndef LC_VERSION_MIN_TVOS
37
#define LC_VERSION_MIN_TVOS 0x2F
38
#endif
39
 
40
#pragma mark Prototypes
41
static bool FIRCLSBinaryImageOpenIfNeeded(bool* needsClosing);
42
 
43
static void FIRCLSBinaryImageAddedCallback(const struct mach_header* mh, intptr_t vmaddr_slide);
44
static void FIRCLSBinaryImageRemovedCallback(const struct mach_header* mh, intptr_t vmaddr_slide);
45
static void FIRCLSBinaryImageChanged(bool added,
46
                                     const struct mach_header* mh,
47
                                     intptr_t vmaddr_slide);
48
static bool FIRCLSBinaryImageFillInImageDetails(FIRCLSBinaryImageDetails* details);
49
 
50
static void FIRCLSBinaryImageStoreNode(bool added, FIRCLSBinaryImageDetails imageDetails);
51
static void FIRCLSBinaryImageRecordSlice(bool added, const FIRCLSBinaryImageDetails imageDetails);
52
 
53
#pragma mark - Core API
54
void FIRCLSBinaryImageInit(FIRCLSBinaryImageReadOnlyContext* roContext,
55
                           FIRCLSBinaryImageReadWriteContext* rwContext) {
56
  // initialize our node array to all zeros
57
  memset(&_firclsContext.writable->binaryImage, 0, sizeof(_firclsContext.writable->binaryImage));
58
  _firclsContext.writable->binaryImage.file.fd = -1;
59
 
60
  dispatch_async(FIRCLSGetBinaryImageQueue(), ^{
61
    if (!FIRCLSUnlinkIfExists(_firclsContext.readonly->binaryimage.path)) {
62
      FIRCLSSDKLog("Unable to reset the binary image log file %s\n", strerror(errno));
63
    }
64
 
65
    bool needsClosing;  // unneeded
66
    if (!FIRCLSBinaryImageOpenIfNeeded(&needsClosing)) {
67
      FIRCLSSDKLog("Error: Unable to open the binary image log file during init\n");
68
    }
69
  });
70
 
71
  _dyld_register_func_for_add_image(FIRCLSBinaryImageAddedCallback);
72
  _dyld_register_func_for_remove_image(FIRCLSBinaryImageRemovedCallback);
73
 
74
  dispatch_async(FIRCLSGetBinaryImageQueue(), ^{
75
    FIRCLSFileClose(&_firclsContext.writable->binaryImage.file);
76
  });
77
}
78
 
79
static bool FIRCLSBinaryImageOpenIfNeeded(bool* needsClosing) {
80
  if (!FIRCLSIsValidPointer(_firclsContext.writable)) {
81
    return false;
82
  }
83
 
84
  if (!FIRCLSIsValidPointer(_firclsContext.readonly)) {
85
    return false;
86
  }
87
 
88
  if (!FIRCLSIsValidPointer(needsClosing)) {
89
    return false;
90
  }
91
 
92
  *needsClosing = false;
93
 
94
  if (FIRCLSFileIsOpen(&_firclsContext.writable->binaryImage.file)) {
95
    return true;
96
  }
97
 
98
  if (!FIRCLSFileInitWithPath(&_firclsContext.writable->binaryImage.file,
99
                              _firclsContext.readonly->binaryimage.path, false)) {
100
    FIRCLSSDKLog("Error: unable to open binary image log file\n");
101
    return false;
102
  }
103
 
104
  *needsClosing = true;
105
 
106
  return true;
107
}
108
 
109
#if CLS_COMPACT_UNWINDING_SUPPORTED
110
bool FIRCLSBinaryImageSafeFindImageForAddress(uintptr_t address,
111
                                              FIRCLSBinaryImageRuntimeNode* image) {
112
  if (!FIRCLSContextIsInitialized()) {
113
    return false;
114
  }
115
 
116
  if (address == 0) {
117
    return false;
118
  }
119
 
120
  if (!FIRCLSIsValidPointer(image)) {
121
    return false;
122
  }
123
 
124
  FIRCLSBinaryImageRuntimeNode* nodes = _firclsContext.writable->binaryImage.nodes;
125
  if (!nodes) {
126
    FIRCLSSDKLogError("The node structure is NULL\n");
127
    return false;
128
  }
129
 
130
  for (uint32_t i = 0; i < CLS_BINARY_IMAGE_RUNTIME_NODE_COUNT; ++i) {
131
    FIRCLSBinaryImageRuntimeNode* node = &nodes[i];
132
    if (!FIRCLSIsValidPointer(node)) {
133
      FIRCLSSDKLog(
134
          "Invalid node pointer encountered in context's writable binary image at index %i", i);
135
      continue;
136
    }
137
 
138
    if ((address >= (uintptr_t)node->baseAddress) &&
139
        (address < (uintptr_t)node->baseAddress + node->size)) {
140
      *image = *node;  // copy the image
141
      return true;
142
    }
143
  }
144
 
145
  return false;
146
}
147
 
148
bool FIRCLSBinaryImageSafeHasUnwindInfo(FIRCLSBinaryImageRuntimeNode* image) {
149
  return FIRCLSIsValidPointer(image->unwindInfo);
150
}
151
#endif
152
 
153
bool FIRCLSBinaryImageFindImageForUUID(const char* uuidString,
154
                                       FIRCLSBinaryImageDetails* imageDetails) {
155
  if (!imageDetails || !uuidString) {
156
    FIRCLSSDKLog("null input\n");
157
    return false;
158
  }
159
 
160
  uint32_t imageCount = _dyld_image_count();
161
 
162
  for (uint32_t i = 0; i < imageCount; ++i) {
163
    const struct mach_header* mh = _dyld_get_image_header(i);
164
 
165
    FIRCLSBinaryImageDetails image;
166
 
167
    image.slice = FIRCLSMachOSliceWithHeader((void*)mh);
168
    FIRCLSBinaryImageFillInImageDetails(&image);
169
 
170
    if (strncmp(uuidString, image.uuidString, FIRCLSUUIDStringLength) == 0) {
171
      *imageDetails = image;
172
      return true;
173
    }
174
  }
175
 
176
  return false;
177
}
178
 
179
#pragma mark - DYLD callback handlers
180
static void FIRCLSBinaryImageAddedCallback(const struct mach_header* mh, intptr_t vmaddr_slide) {
181
  FIRCLSBinaryImageChanged(true, mh, vmaddr_slide);
182
}
183
 
184
static void FIRCLSBinaryImageRemovedCallback(const struct mach_header* mh, intptr_t vmaddr_slide) {
185
  FIRCLSBinaryImageChanged(false, mh, vmaddr_slide);
186
}
187
 
188
#if CLS_BINARY_IMAGE_RUNTIME_NODE_RECORD_NAME
189
static bool FIRCLSBinaryImagePopulateRuntimeNodeName(FIRCLSBinaryImageDetails* details) {
190
  if (!FIRCLSIsValidPointer(details)) {
191
    return false;
192
  }
193
 
194
  memset(details->node.name, 0, CLS_BINARY_IMAGE_RUNTIME_NODE_NAME_SIZE);
195
 
196
  // We have limited storage space for the name. And, we really want to store
197
  // "CoreFoundation", not "/System/Library/Fram", so we have to play tricks
198
  // to make sure we get the right side of the string.
199
  const char* imageName = FIRCLSMachOSliceGetExecutablePath(&details->slice);
200
  if (!imageName) {
201
    return false;
202
  }
203
 
204
  const size_t imageNameLength = strlen(imageName);
205
 
206
  // Remember to leave one character for null-termination.
207
  if (imageNameLength > CLS_BINARY_IMAGE_RUNTIME_NODE_NAME_SIZE - 1) {
208
    imageName = imageName + (imageNameLength - (CLS_BINARY_IMAGE_RUNTIME_NODE_NAME_SIZE - 1));
209
  }
210
 
211
  // subtract one to make sure the string is always null-terminated
212
  strncpy(details->node.name, imageName, CLS_BINARY_IMAGE_RUNTIME_NODE_NAME_SIZE - 1);
213
 
214
  return true;
215
}
216
#endif
217
 
218
// There were plans later to replace this with FIRCLSMachO
219
static FIRCLSMachOSegmentCommand FIRCLSBinaryImageMachOGetSegmentCommand(
220
    const struct load_command* cmd) {
221
  FIRCLSMachOSegmentCommand segmentCommand;
222
 
223
  memset(&segmentCommand, 0, sizeof(FIRCLSMachOSegmentCommand));
224
 
225
  if (!cmd) {
226
    return segmentCommand;
227
  }
228
 
229
  if (cmd->cmd == LC_SEGMENT) {
230
    struct segment_command* segCmd = (struct segment_command*)cmd;
231
 
232
    memcpy(segmentCommand.segname, segCmd->segname, 16);
233
    segmentCommand.vmaddr = segCmd->vmaddr;
234
    segmentCommand.vmsize = segCmd->vmsize;
235
  } else if (cmd->cmd == LC_SEGMENT_64) {
236
    struct segment_command_64* segCmd = (struct segment_command_64*)cmd;
237
 
238
    memcpy(segmentCommand.segname, segCmd->segname, 16);
239
    segmentCommand.vmaddr = segCmd->vmaddr;
240
    segmentCommand.vmsize = segCmd->vmsize;
241
  }
242
 
243
  return segmentCommand;
244
}
245
 
246
static bool FIRCLSBinaryImageMachOSliceInitSectionByName(FIRCLSMachOSliceRef slice,
247
                                                         const char* segName,
248
                                                         const char* sectionName,
249
                                                         FIRCLSMachOSection* section) {
250
  if (!FIRCLSIsValidPointer(slice)) {
251
    return false;
252
  }
253
 
254
  if (!section) {
255
    return false;
256
  }
257
 
258
  memset(section, 0, sizeof(FIRCLSMachOSection));
259
 
260
  if (FIRCLSMachOSliceIs64Bit(slice)) {
261
    const struct section_64* sect =
262
        getsectbynamefromheader_64(slice->startAddress, segName, sectionName);
263
    if (!sect) {
264
      return false;
265
    }
266
 
267
    section->addr = sect->addr;
268
    section->size = sect->size;
269
    section->offset = sect->offset;
270
  } else {
271
    const struct section* sect = getsectbynamefromheader(slice->startAddress, segName, sectionName);
272
    if (!sect) {
273
      return false;
274
    }
275
 
276
    section->addr = sect->addr;
277
    section->size = sect->size;
278
    section->offset = sect->offset;
279
  }
280
 
281
  return true;
282
}
283
 
284
static bool FIRCLSBinaryImageFillInImageDetails(FIRCLSBinaryImageDetails* details) {
285
  if (!FIRCLSIsValidPointer(details)) {
286
    return false;
287
  }
288
 
289
  if (!FIRCLSIsValidPointer(details->slice.startAddress)) {
290
    return false;
291
  }
292
 
293
#if CLS_BINARY_IMAGE_RUNTIME_NODE_RECORD_NAME
294
  // this is done for debugging purposes, so if it fails, its ok to continue
295
  FIRCLSBinaryImagePopulateRuntimeNodeName(details);
296
#endif
297
 
298
  // This cast might look a little dubious, but its just because we're using the same
299
  // struct types in a few different places.
300
  details->node.baseAddress = (void* volatile)details->slice.startAddress;
301
 
302
  FIRCLSMachOSliceEnumerateLoadCommands(
303
      &details->slice, ^(uint32_t type, uint32_t size, const struct load_command* cmd) {
304
        switch (type) {
305
          case LC_UUID: {
306
            const uint8_t* uuid = FIRCLSMachOGetUUID(cmd);
307
            FIRCLSSafeHexToString(uuid, 16, details->uuidString);
308
          } break;
309
          case LC_ENCRYPTION_INFO:
310
            details->encrypted = FIRCLSMachOGetEncrypted(cmd);
311
            break;
312
          case LC_SEGMENT:
313
          case LC_SEGMENT_64: {
314
            FIRCLSMachOSegmentCommand segmentCommand = FIRCLSBinaryImageMachOGetSegmentCommand(cmd);
315
 
316
            if (strncmp(segmentCommand.segname, SEG_TEXT, sizeof(SEG_TEXT)) == 0) {
317
              details->node.size = segmentCommand.vmsize;
318
            }
319
          } break;
320
          case LC_VERSION_MIN_MACOSX:
321
          case LC_VERSION_MIN_IPHONEOS:
322
          case LC_VERSION_MIN_TVOS:
323
          case LC_VERSION_MIN_WATCHOS:
324
            details->minSDK = FIRCLSMachOGetMinimumOSVersion(cmd);
325
            details->builtSDK = FIRCLSMachOGetLinkedSDKVersion(cmd);
326
            break;
327
        }
328
      });
329
 
330
  // We look up the section we want, and we *should* be able to use:
331
  //
332
  // address of data we want = start address + section.offset
333
  //
334
  // However, the offset value is coming back funky in iOS 9. So, instead we look up the address
335
  // the section should be loaded at, and compute the offset by looking up the address of the
336
  // segment itself.
337
 
338
  FIRCLSMachOSection section;
339
 
340
#if CLS_COMPACT_UNWINDING_SUPPORTED
341
  if (FIRCLSBinaryImageMachOSliceInitSectionByName(&details->slice, SEG_TEXT, "__unwind_info",
342
                                                   &section)) {
343
    details->node.unwindInfo = (void*)(section.addr + details->vmaddr_slide);
344
  }
345
#endif
346
 
347
#if CLS_DWARF_UNWINDING_SUPPORTED
348
  if (FIRCLSBinaryImageMachOSliceInitSectionByName(&details->slice, SEG_TEXT, "__eh_frame",
349
                                                   &section)) {
350
    details->node.ehFrame = (void*)(section.addr + details->vmaddr_slide);
351
  }
352
#endif
353
 
354
  if (FIRCLSBinaryImageMachOSliceInitSectionByName(&details->slice, SEG_DATA, "__crash_info",
355
                                                   &section)) {
356
    details->node.crashInfo = (void*)(section.addr + details->vmaddr_slide);
357
  }
358
 
359
  return true;
360
}
361
 
362
static void FIRCLSBinaryImageChanged(bool added,
363
                                     const struct mach_header* mh,
364
                                     intptr_t vmaddr_slide) {
365
  //    FIRCLSSDKLog("Binary image %s %p\n", added ? "loaded" : "unloaded", mh);
366
  FIRCLSBinaryImageDetails imageDetails;
367
  memset(&imageDetails, 0, sizeof(FIRCLSBinaryImageDetails));
368
 
369
  imageDetails.slice = FIRCLSMachOSliceWithHeader((void*)mh);
370
  imageDetails.vmaddr_slide = vmaddr_slide;
371
  FIRCLSBinaryImageFillInImageDetails(&imageDetails);
372
 
373
  // Do these time-consuming operations on a background queue
374
  dispatch_async(FIRCLSGetBinaryImageQueue(), ^{
375
    // this is an atomic operation
376
    FIRCLSBinaryImageStoreNode(added, imageDetails);
377
    FIRCLSBinaryImageRecordSlice(added, imageDetails);
378
  });
379
}
380
 
381
#pragma mark - In-Memory Storage
382
static void FIRCLSBinaryImageStoreNode(bool added, FIRCLSBinaryImageDetails imageDetails) {
383
  // This function is mutating a structure that needs to be accessed at crash time. We
384
  // need to make sure the structure is always in as valid a state as possible.
385
  //    FIRCLSSDKLog("Storing %s node %p\n", added ? "loaded" : "unloaded",
386
  //    (void*)imageDetails.node.baseAddress);
387
 
388
  if (!_firclsContext.writable) {
389
    FIRCLSSDKLog("Error: Writable context is NULL\n");
390
    return;
391
  }
392
 
393
  void* searchAddress = NULL;
394
  bool success = false;
395
  FIRCLSBinaryImageRuntimeNode* nodes = _firclsContext.writable->binaryImage.nodes;
396
 
397
  if (!added) {
398
    // capture the search address first
399
    searchAddress = imageDetails.node.baseAddress;
400
 
401
    // If we are removing a node, we need to set its entries to zero. By clearing all of
402
    // these values, we can just copy in imageDetails.node. Using memset here is slightly
403
    // weird, since we have to restore one field. But, this way, if/when the structure changes,
404
    // we still do the right thing.
405
    memset(&imageDetails.node, 0, sizeof(FIRCLSBinaryImageRuntimeNode));
406
 
407
    // restore the baseAddress, which just got zeroed, and is used for indexing
408
    imageDetails.node.baseAddress = searchAddress;
409
  }
410
 
411
  for (uint32_t i = 0; i < CLS_BINARY_IMAGE_RUNTIME_NODE_COUNT; ++i) {
412
    FIRCLSBinaryImageRuntimeNode* node = &nodes[i];
413
 
414
    if (!node) {
415
      FIRCLSSDKLog("Error: Binary image storage is NULL\n");
416
      break;
417
    }
418
 
419
    // navigate through the array, looking for our matching address
420
    if (node->baseAddress != searchAddress) {
421
      continue;
422
    }
423
 
424
    // Attempt to swap the base address with whatever we are searching for. Success means that
425
    // entry has been claims/cleared. Failure means some other thread beat us to it.
426
    if (atomic_compare_exchange_strong(&node->baseAddress, &searchAddress,
427
                                       imageDetails.node.baseAddress)) {
428
      *node = imageDetails.node;
429
      success = true;
430
 
431
      break;
432
    }
433
 
434
    // If this is an unload, getting here means two threads unloaded at the same time. I think
435
    // that's highly unlikely, and possibly even impossible. So, I'm choosing to abort the process
436
    // at this point.
437
    if (!added) {
438
      FIRCLSSDKLog("Error: Failed to swap during image unload\n");
439
      break;
440
    }
441
  }
442
 
443
  if (!success) {
444
    FIRCLSSDKLog("Error: Unable to track a %s node %p\n", added ? "loaded" : "unloaded",
445
                 (void*)imageDetails.node.baseAddress);
446
  }
447
}
448
 
449
#pragma mark - On-Disk Storage
450
static void FIRCLSBinaryImageRecordDetails(FIRCLSFile* file,
451
                                           const FIRCLSBinaryImageDetails imageDetails) {
452
  if (!file) {
453
    FIRCLSSDKLog("Error: file is invalid\n");
454
    return;
455
  }
456
 
457
  FIRCLSFileWriteHashEntryString(file, "uuid", imageDetails.uuidString);
458
  FIRCLSFileWriteHashEntryUint64(file, "base", (uintptr_t)imageDetails.slice.startAddress);
459
  FIRCLSFileWriteHashEntryUint64(file, "size", imageDetails.node.size);
460
}
461
 
462
static void FIRCLSBinaryImageRecordLibraryFrameworkInfo(FIRCLSFile* file, const char* path) {
463
  if (!file) {
464
    FIRCLSSDKLog("Error: file is invalid\n");
465
    return;
466
  }
467
 
468
  if (!path) {
469
    return;
470
  }
471
 
472
  // Because this function is so expensive, we've decided to omit this info for all Apple-supplied
473
  // frameworks. This really isn't that bad, because we can know their info ahead of time (within a
474
  // small margin of error). With this implemenation, we will still record this info for any
475
  // user-built framework, which in the end is the most important thing.
476
  if (strncmp(path, "/System", 7) == 0) {
477
    return;
478
  }
479
 
480
  // check to see if this is a potential framework bundle
481
  if (!strstr(path, ".framework")) {
482
    return;
483
  }
484
 
485
  // My.framework/Versions/A/My for OS X
486
  // My.framework/My for iOS
487
 
488
  NSString* frameworkPath = [NSString stringWithUTF8String:path];
489
#if TARGET_OS_IPHONE
490
  frameworkPath = [frameworkPath stringByDeletingLastPathComponent];
491
#else
492
  frameworkPath = [frameworkPath stringByDeletingLastPathComponent];  // My.framework/Versions/A
493
  frameworkPath = [frameworkPath stringByDeletingLastPathComponent];  // My.framework/Versions
494
  frameworkPath = [frameworkPath stringByDeletingLastPathComponent];  // My.framework
495
#endif
496
 
497
  NSBundle* const bundle = [NSBundle bundleWithPath:frameworkPath];
498
 
499
  if (!bundle) {
500
    return;
501
  }
502
 
503
  FIRCLSFileWriteHashEntryNSStringUnlessNilOrEmpty(file, "bundle_id", [bundle bundleIdentifier]);
504
  FIRCLSFileWriteHashEntryNSStringUnlessNilOrEmpty(
505
      file, "build_version", [bundle objectForInfoDictionaryKey:@"CFBundleVersion"]);
506
  FIRCLSFileWriteHashEntryNSStringUnlessNilOrEmpty(
507
      file, "display_version", [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]);
508
}
509
 
510
static void FIRCLSBinaryImageRecordSlice(bool added, const FIRCLSBinaryImageDetails imageDetails) {
511
  bool needsClosing = false;
512
  if (!FIRCLSBinaryImageOpenIfNeeded(&needsClosing)) {
513
    FIRCLSSDKLog("Error: unable to open binary image log file\n");
514
    return;
515
  }
516
 
517
  FIRCLSFile* file = &_firclsContext.writable->binaryImage.file;
518
 
519
  FIRCLSFileWriteSectionStart(file, added ? "load" : "unload");
520
 
521
  FIRCLSFileWriteHashStart(file);
522
 
523
  const char* path = FIRCLSMachOSliceGetExecutablePath((FIRCLSMachOSliceRef)&imageDetails.slice);
524
 
525
  FIRCLSFileWriteHashEntryString(file, "path", path);
526
 
527
  if (added) {
528
    // this won't work if the binary has been unloaded
529
    FIRCLSBinaryImageRecordLibraryFrameworkInfo(file, path);
530
  }
531
 
532
  FIRCLSBinaryImageRecordDetails(file, imageDetails);
533
 
534
  FIRCLSFileWriteHashEnd(file);
535
 
536
  FIRCLSFileWriteSectionEnd(file);
537
 
538
  if (needsClosing) {
539
    FIRCLSFileClose(file);
540
  }
541
}
542
 
543
bool FIRCLSBinaryImageRecordMainExecutable(FIRCLSFile* file) {
544
  FIRCLSBinaryImageDetails imageDetails;
545
 
546
  memset(&imageDetails, 0, sizeof(FIRCLSBinaryImageDetails));
547
 
548
  imageDetails.slice = FIRCLSMachOSliceGetCurrent();
549
  FIRCLSBinaryImageFillInImageDetails(&imageDetails);
550
 
551
  FIRCLSFileWriteSectionStart(file, "executable");
552
  FIRCLSFileWriteHashStart(file);
553
 
554
  FIRCLSFileWriteHashEntryString(file, "architecture",
555
                                 FIRCLSMachOSliceGetArchitectureName(&imageDetails.slice));
556
 
557
  FIRCLSBinaryImageRecordDetails(file, imageDetails);
558
  FIRCLSFileWriteHashEntryBoolean(file, "encrypted", imageDetails.encrypted);
559
  FIRCLSFileWriteHashEntryString(file, "minimum_sdk_version",
560
                                 [FIRCLSMachOFormatVersion(&imageDetails.minSDK) UTF8String]);
561
  FIRCLSFileWriteHashEntryString(file, "built_sdk_version",
562
                                 [FIRCLSMachOFormatVersion(&imageDetails.builtSDK) UTF8String]);
563
 
564
  FIRCLSFileWriteHashEnd(file);
565
  FIRCLSFileWriteSectionEnd(file);
566
 
567
  return true;
568
}