From 54f010a32048c86c07660b337016c72f82643b75 Mon Sep 17 00:00:00 2001 From: Dave Lee Date: Sun, 23 Jul 2017 15:05:55 -0700 Subject: [PATCH] Add findinstances, and new support framework in Chisel.xcodeproj This adds a new command, `findinstances`, which is implemented mostly in native code and so comes with Chisel.xcodeproj which contains a new Chisel framework. Future Chisel commands can implement the heavy lifing this way too. The `findinstances` command scans the heap using available iOS/macOS malloc API. For each allocation, a number of heuristics are peformed to identify likely Objective-C instances. The heuristics do not call methods on the objects, instead relying only on objc runtime functions to passively match the instance based on its class metadata. This avoids allocations and stateful side effects in the objc runtime. After this first pass, the candidate objects go through a second pass that checks if they match against an optional `NSPredicate`. If there's no predicate, the object is printed with minimal information. If there is a predicate, and the object passes the predicate, then the object will be printed out with more detail, specifically the detail queried in the predicate. --- Chisel/Chisel.xcodeproj/project.pbxproj | 439 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + Chisel/Chisel/CHLAllocations.c | 38 ++ Chisel/Chisel/CHLAllocations.h | 17 + Chisel/Chisel/CHLObjcInstanceCommands.h | 15 + Chisel/Chisel/CHLObjcInstanceCommands.mm | 183 ++++++++ Chisel/Chisel/CHLObjcInstances.h | 21 + Chisel/Chisel/CHLObjcInstances.mm | 164 +++++++ Chisel/Chisel/CHLPredicateTools.h | 13 + Chisel/Chisel/CHLPredicateTools.m | 53 +++ Chisel/Chisel/Chisel.h | 11 + Chisel/Chisel/Info.plist | 24 + Chisel/Chisel/zone_allocator.h | 43 ++ Chisel/ChiselTests/ChiselTests.m | 33 ++ Chisel/ChiselTests/Info.plist | 22 + 15 files changed, 1083 insertions(+) create mode 100644 Chisel/Chisel.xcodeproj/project.pbxproj create mode 100644 Chisel/Chisel.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Chisel/Chisel/CHLAllocations.c create mode 100644 Chisel/Chisel/CHLAllocations.h create mode 100644 Chisel/Chisel/CHLObjcInstanceCommands.h create mode 100644 Chisel/Chisel/CHLObjcInstanceCommands.mm create mode 100644 Chisel/Chisel/CHLObjcInstances.h create mode 100644 Chisel/Chisel/CHLObjcInstances.mm create mode 100644 Chisel/Chisel/CHLPredicateTools.h create mode 100644 Chisel/Chisel/CHLPredicateTools.m create mode 100644 Chisel/Chisel/Chisel.h create mode 100644 Chisel/Chisel/Info.plist create mode 100644 Chisel/Chisel/zone_allocator.h create mode 100644 Chisel/ChiselTests/ChiselTests.m create mode 100644 Chisel/ChiselTests/Info.plist diff --git a/Chisel/Chisel.xcodeproj/project.pbxproj b/Chisel/Chisel.xcodeproj/project.pbxproj new file mode 100644 index 0000000..95110e1 --- /dev/null +++ b/Chisel/Chisel.xcodeproj/project.pbxproj @@ -0,0 +1,439 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 7A04088C1DF9A2C7009C5BFA /* CHLObjcInstanceCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A04088A1DF9A2C7009C5BFA /* CHLObjcInstanceCommands.h */; }; + 7A04088D1DF9A2C7009C5BFA /* CHLObjcInstanceCommands.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A04088B1DF9A2C7009C5BFA /* CHLObjcInstanceCommands.mm */; }; + 7A20C4B31DFDB8D200C89959 /* CHLPredicateTools.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A20C4B11DFDB8D200C89959 /* CHLPredicateTools.h */; }; + 7A20C4B41DFDB8D200C89959 /* CHLPredicateTools.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A20C4B21DFDB8D200C89959 /* CHLPredicateTools.m */; }; + 7ABD17951DF7F998006118F8 /* Chisel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ABD178B1DF7F998006118F8 /* Chisel.framework */; }; + 7ABD179A1DF7F998006118F8 /* ChiselTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD17991DF7F998006118F8 /* ChiselTests.m */; }; + 7ABD179C1DF7F998006118F8 /* Chisel.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ABD178E1DF7F998006118F8 /* Chisel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7ABD17A71DF7F9FD006118F8 /* CHLAllocations.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ABD17A51DF7F9FD006118F8 /* CHLAllocations.h */; }; + 7ABD17A81DF7F9FD006118F8 /* CHLAllocations.c in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD17A61DF7F9FD006118F8 /* CHLAllocations.c */; }; + 7ABD17AB1DF7FCF9006118F8 /* CHLObjcInstances.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ABD17A91DF7FCF9006118F8 /* CHLObjcInstances.h */; }; + 7ABD17AC1DF7FCF9006118F8 /* CHLObjcInstances.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD17AA1DF7FCF9006118F8 /* CHLObjcInstances.mm */; }; + 7ABD17AF1DF88520006118F8 /* zone_allocator.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ABD17AD1DF88520006118F8 /* zone_allocator.h */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7ABD17961DF7F998006118F8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7ABD17821DF7F998006118F8 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7ABD178A1DF7F998006118F8; + remoteInfo = Chisel; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 7A04088A1DF9A2C7009C5BFA /* CHLObjcInstanceCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CHLObjcInstanceCommands.h; sourceTree = ""; }; + 7A04088B1DF9A2C7009C5BFA /* CHLObjcInstanceCommands.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CHLObjcInstanceCommands.mm; sourceTree = ""; }; + 7A20C4B11DFDB8D200C89959 /* CHLPredicateTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CHLPredicateTools.h; sourceTree = ""; }; + 7A20C4B21DFDB8D200C89959 /* CHLPredicateTools.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CHLPredicateTools.m; sourceTree = ""; }; + 7ABD178B1DF7F998006118F8 /* Chisel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Chisel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7ABD178E1DF7F998006118F8 /* Chisel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Chisel.h; sourceTree = ""; }; + 7ABD178F1DF7F998006118F8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7ABD17941DF7F998006118F8 /* ChiselTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChiselTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7ABD17991DF7F998006118F8 /* ChiselTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ChiselTests.m; sourceTree = ""; }; + 7ABD179B1DF7F998006118F8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7ABD17A51DF7F9FD006118F8 /* CHLAllocations.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CHLAllocations.h; sourceTree = ""; }; + 7ABD17A61DF7F9FD006118F8 /* CHLAllocations.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = CHLAllocations.c; sourceTree = ""; }; + 7ABD17A91DF7FCF9006118F8 /* CHLObjcInstances.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CHLObjcInstances.h; sourceTree = ""; }; + 7ABD17AA1DF7FCF9006118F8 /* CHLObjcInstances.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CHLObjcInstances.mm; sourceTree = ""; }; + 7ABD17AD1DF88520006118F8 /* zone_allocator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = zone_allocator.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7ABD17871DF7F998006118F8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7ABD17911DF7F998006118F8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7ABD17951DF7F998006118F8 /* Chisel.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7ABD17811DF7F998006118F8 = { + isa = PBXGroup; + children = ( + 7ABD178D1DF7F998006118F8 /* Chisel */, + 7ABD17981DF7F998006118F8 /* ChiselTests */, + 7ABD178C1DF7F998006118F8 /* Products */, + ); + sourceTree = ""; + }; + 7ABD178C1DF7F998006118F8 /* Products */ = { + isa = PBXGroup; + children = ( + 7ABD178B1DF7F998006118F8 /* Chisel.framework */, + 7ABD17941DF7F998006118F8 /* ChiselTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 7ABD178D1DF7F998006118F8 /* Chisel */ = { + isa = PBXGroup; + children = ( + 7ABD178E1DF7F998006118F8 /* Chisel.h */, + 7ABD178F1DF7F998006118F8 /* Info.plist */, + 7ABD17AD1DF88520006118F8 /* zone_allocator.h */, + 7ABD17A51DF7F9FD006118F8 /* CHLAllocations.h */, + 7ABD17A61DF7F9FD006118F8 /* CHLAllocations.c */, + 7ABD17A91DF7FCF9006118F8 /* CHLObjcInstances.h */, + 7ABD17AA1DF7FCF9006118F8 /* CHLObjcInstances.mm */, + 7A04088A1DF9A2C7009C5BFA /* CHLObjcInstanceCommands.h */, + 7A04088B1DF9A2C7009C5BFA /* CHLObjcInstanceCommands.mm */, + 7A20C4B11DFDB8D200C89959 /* CHLPredicateTools.h */, + 7A20C4B21DFDB8D200C89959 /* CHLPredicateTools.m */, + ); + path = Chisel; + sourceTree = ""; + }; + 7ABD17981DF7F998006118F8 /* ChiselTests */ = { + isa = PBXGroup; + children = ( + 7ABD17991DF7F998006118F8 /* ChiselTests.m */, + 7ABD179B1DF7F998006118F8 /* Info.plist */, + ); + path = ChiselTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 7ABD17881DF7F998006118F8 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 7ABD17A71DF7F9FD006118F8 /* CHLAllocations.h in Headers */, + 7ABD179C1DF7F998006118F8 /* Chisel.h in Headers */, + 7A20C4B31DFDB8D200C89959 /* CHLPredicateTools.h in Headers */, + 7A04088C1DF9A2C7009C5BFA /* CHLObjcInstanceCommands.h in Headers */, + 7ABD17AF1DF88520006118F8 /* zone_allocator.h in Headers */, + 7ABD17AB1DF7FCF9006118F8 /* CHLObjcInstances.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 7ABD178A1DF7F998006118F8 /* Chisel */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7ABD179F1DF7F998006118F8 /* Build configuration list for PBXNativeTarget "Chisel" */; + buildPhases = ( + 7ABD17861DF7F998006118F8 /* Sources */, + 7ABD17871DF7F998006118F8 /* Frameworks */, + 7ABD17881DF7F998006118F8 /* Headers */, + 7ABD17891DF7F998006118F8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Chisel; + productName = Chisel; + productReference = 7ABD178B1DF7F998006118F8 /* Chisel.framework */; + productType = "com.apple.product-type.framework"; + }; + 7ABD17931DF7F998006118F8 /* ChiselTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7ABD17A21DF7F998006118F8 /* Build configuration list for PBXNativeTarget "ChiselTests" */; + buildPhases = ( + 7ABD17901DF7F998006118F8 /* Sources */, + 7ABD17911DF7F998006118F8 /* Frameworks */, + 7ABD17921DF7F998006118F8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7ABD17971DF7F998006118F8 /* PBXTargetDependency */, + ); + name = ChiselTests; + productName = ChiselTests; + productReference = 7ABD17941DF7F998006118F8 /* ChiselTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7ABD17821DF7F998006118F8 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0810; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 7ABD178A1DF7F998006118F8 = { + CreatedOnToolsVersion = 8.1; + ProvisioningStyle = Automatic; + }; + 7ABD17931DF7F998006118F8 = { + CreatedOnToolsVersion = 8.1; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 7ABD17851DF7F998006118F8 /* Build configuration list for PBXProject "Chisel" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 7ABD17811DF7F998006118F8; + productRefGroup = 7ABD178C1DF7F998006118F8 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7ABD178A1DF7F998006118F8 /* Chisel */, + 7ABD17931DF7F998006118F8 /* ChiselTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7ABD17891DF7F998006118F8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7ABD17921DF7F998006118F8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7ABD17861DF7F998006118F8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7ABD17A81DF7F9FD006118F8 /* CHLAllocations.c in Sources */, + 7ABD17AC1DF7FCF9006118F8 /* CHLObjcInstances.mm in Sources */, + 7A04088D1DF9A2C7009C5BFA /* CHLObjcInstanceCommands.mm in Sources */, + 7A20C4B41DFDB8D200C89959 /* CHLPredicateTools.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7ABD17901DF7F998006118F8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7ABD179A1DF7F998006118F8 /* ChiselTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7ABD17971DF7F998006118F8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7ABD178A1DF7F998006118F8 /* Chisel */; + targetProxy = 7ABD17961DF7F998006118F8 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 7ABD179D1DF7F998006118F8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = NO; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = c11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 7ABD179E1DF7F998006118F8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = NO; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = c11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 7ABD17A01DF7F998006118F8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Chisel/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.Chisel; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 7ABD17A11DF7F998006118F8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Chisel/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.Chisel; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 7ABD17A31DF7F998006118F8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = ChiselTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.ChiselTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 7ABD17A41DF7F998006118F8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = ChiselTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.ChiselTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7ABD17851DF7F998006118F8 /* Build configuration list for PBXProject "Chisel" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7ABD179D1DF7F998006118F8 /* Debug */, + 7ABD179E1DF7F998006118F8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7ABD179F1DF7F998006118F8 /* Build configuration list for PBXNativeTarget "Chisel" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7ABD17A01DF7F998006118F8 /* Debug */, + 7ABD17A11DF7F998006118F8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7ABD17A21DF7F998006118F8 /* Build configuration list for PBXNativeTarget "ChiselTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7ABD17A31DF7F998006118F8 /* Debug */, + 7ABD17A41DF7F998006118F8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7ABD17821DF7F998006118F8 /* Project object */; +} diff --git a/Chisel/Chisel.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Chisel/Chisel.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..b6c5750 --- /dev/null +++ b/Chisel/Chisel.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Chisel/Chisel/CHLAllocations.c b/Chisel/Chisel/CHLAllocations.c new file mode 100644 index 0000000..1c8c805 --- /dev/null +++ b/Chisel/Chisel/CHLAllocations.c @@ -0,0 +1,38 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "CHLAllocations.h" + +static kern_return_t reader(__unused task_t remote_task, vm_address_t remote_address, __unused vm_size_t size, void **local_memory) +{ + *local_memory = (void *)remote_address; + return KERN_SUCCESS; +} + +typedef struct { + CHLRangeHandler handler; + void *context; +} RangeEnumeratorArgs; + +static void rangeEnumerator(__unused task_t task, void *context, __unused unsigned type, vm_range_t *ranges, unsigned int count) +{ + const RangeEnumeratorArgs *args = (RangeEnumeratorArgs *)context; + for (unsigned int i = 0; i < count; ++i) { + args->handler(ranges[i], args->context); + } +} + +void CHLScanAllocations(CHLRangeHandler handler, void *context, const malloc_zone_t *sideZone) +{ + vm_address_t *zones; + unsigned int count; + malloc_get_all_zones(TASK_NULL, &reader, &zones, &count); + + RangeEnumeratorArgs args = {handler, context}; + + for (unsigned int i = 0; i < count; ++i) { + malloc_zone_t *zone = (malloc_zone_t *)zones[i]; + if (zone != sideZone) { + zone->introspect->enumerator(TASK_NULL, &args, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t)zone, reader, rangeEnumerator); + } + } +} diff --git a/Chisel/Chisel/CHLAllocations.h b/Chisel/Chisel/CHLAllocations.h new file mode 100644 index 0000000..5cd8715 --- /dev/null +++ b/Chisel/Chisel/CHLAllocations.h @@ -0,0 +1,17 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef void (*CHLRangeHandler)(vm_range_t range, void *context); + +// Enumerate live allocations in all malloc zones. If callers allocate memory in the handler, those +// allocations should be within the given `sideZone`. +void CHLScanAllocations(CHLRangeHandler handler, void *context, const malloc_zone_t *sideZone); + +#if defined(__cplusplus) +} +#endif diff --git a/Chisel/Chisel/CHLObjcInstanceCommands.h b/Chisel/Chisel/CHLObjcInstanceCommands.h new file mode 100644 index 0000000..e19f979 --- /dev/null +++ b/Chisel/Chisel/CHLObjcInstanceCommands.h @@ -0,0 +1,15 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +@class NSPredicate; + +#if defined(__cplusplus) +extern "C" { +#endif + +// Debugger interface for finding and printing instances of a type, with an optional predicate. +// The predicate format is anything supported by NSPredicate. +void PrintInstances(const char *type, const char *pred); + +#if defined(__cplusplus) +} +#endif diff --git a/Chisel/Chisel/CHLObjcInstanceCommands.mm b/Chisel/Chisel/CHLObjcInstanceCommands.mm new file mode 100644 index 0000000..faa6c20 --- /dev/null +++ b/Chisel/Chisel/CHLObjcInstanceCommands.mm @@ -0,0 +1,183 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "CHLObjcInstanceCommands.h" + +#include +#include + +#import +#import + +#import "CHLObjcInstances.h" +#import "CHLPredicateTools.h" +#include "zone_allocator.h" + +#if __has_feature(objc_arc) +#error Disable ARC for this file +#endif + +struct IsValidArgs { + const std::unordered_set &classSet; + bool isValid = true; +}; + +static void isValidObject(const void *value, void *context) +{ + const auto args = reinterpret_cast(context); + if (!args->isValid) { + return; + } + + vm_range_t range = {(vm_address_t)value, malloc_size(value)}; + if (CHLViableObjcInstance(range, args->classSet) == nil) { + args->isValid = false; + } +} + +static void isValidKeyValue(const void *key, const void *value, void *context) +{ + const auto args = reinterpret_cast(context); + isValidObject(key, context); + if (args->isValid) { + isValidObject(value, context); + } +} + +static bool predicatePrecheck(id obj, const std::unordered_set &classSet) +{ + IsValidArgs args{classSet}; + + if ([obj isKindOfClass:objc_getClass("__NSCFDictionary")]) { + CFDictionaryApplyFunction((CFDictionaryRef)obj, &isValidKeyValue, &args); + } else if ([obj isKindOfClass:objc_getClass("__NSCFSet")]) { + CFSetApplyFunction((CFSetRef)obj, &isValidObject, &args); + } else { + // Skip classes containing NSPlaceholder. + // TODO: Figure out better way to ignore invalid instances. + char *name = (char *)object_getClassName(obj); + while (*name == '_') ++name; + if (strncmp(name, "NSPlaceholder", sizeof("NSPlaceholder") - 1) == 0) { + args.isValid = false; + } + } + + if (!args.isValid && getenv("FINDINSTANCES_DEBUG")) { + printf("%p has class %s but contains non objc data\n", obj, object_getClassName(obj)); + } + + return args.isValid; +} + +static void printObject(id obj, NSSet *keyPaths) { + printf("<%s: %p", object_getClassName(obj), obj); + for (NSString *keyPath in keyPaths) { + printf("; %s = %s", keyPath.UTF8String, [[obj valueForKeyPath:keyPath] description].UTF8String); + } + printf(">\n"); +} + +static bool objectIsMatch(NSPredicate *predicate, id obj, const std::unordered_set &classSet) +{ + if (!predicate) { + return true; + } + + bool debug = getenv("FINDINSTANCES_DEBUG"); + + if (!predicatePrecheck(obj, classSet)) { + if (debug) { + printf("%p has class %s but has non objc contents\n", obj, object_getClassName(obj)); + } + return false; + } + + @try { + return [predicate evaluateWithObject:obj]; + } @catch (...) { + if (debug) { + printf("%p has class %s but failed predicate evaluation\n", obj, object_getClassName(obj)); + } + return false; + } +} + +// Function reimplementation of +[NSObject isSubclassOf:] to avoid the objc runtime side +// effects that can happen when calling methods, like realizing classes, +initialize, etc. +static bool isSubclassOf(Class base, Class target) +{ + for (auto cls = base; cls != Nil; cls = class_getSuperclass(cls)) { + if (cls == target) { + return true; + } + } + return false; +} + +// Function reimplementation of +[NSObject conformsToProtocol:] to avoid the objc runtime side +// effects that can happen when calling methods, like realizing classes, +initialize, etc. +static bool conformsToProtocol(Class base, Protocol *protocol) +{ + for (auto cls = base; cls != Nil; cls = class_getSuperclass(cls)) { + if (class_conformsToProtocol(cls, protocol)) { + return true; + } + } + return false; +} + +void PrintInstances(const char *type, const char *pred) +{ + NSPredicate *predicate = nil; + if (pred != nullptr && *pred != '\0') { + predicate = [NSPredicate predicateWithFormat:@(pred)]; + } + + const std::unordered_set objcClasses = CHLObjcClassSet(); + std::unordered_set matchClasses; + + Protocol *protocol = objc_getProtocol(type); + if (protocol != nullptr && strcmp("NSObject", type) != 0) { + for (auto cls : objcClasses) { + if (conformsToProtocol(cls, protocol)) { + matchClasses.insert(cls); + } + } + } + + if (type[0] == '*') { + ++type; + Class cls = objc_getClass(type); + if (cls != nullptr) { + matchClasses.insert(cls); + } + } else if (Class kind = objc_getClass(type)) { + // This could be optimized for type == "NSObject", but it won't be a typical search. + for (auto cls : objcClasses) { + if (isSubclassOf(cls, kind)) { + matchClasses.insert(cls); + } + } + } + + if (matchClasses.empty()) { + // TODO: Accept name of library/module, and list instances of classes defined there. + printf("Unknown type: %s\n", type); + return; + } + + NSSet *keyPaths = CHLVariableKeyPaths(predicate); + + std::vector> instances = CHLScanObjcInstances(matchClasses); + unsigned int matches = 0; + + for (id obj : instances) { + if (objectIsMatch(predicate, obj, objcClasses)) { + ++matches; + printObject(obj, keyPaths); + } + } + + if (matches > 1) { + printf("%d matches\n", matches); + } +} diff --git a/Chisel/Chisel/CHLObjcInstances.h b/Chisel/Chisel/CHLObjcInstances.h new file mode 100644 index 0000000..d0545d0 --- /dev/null +++ b/Chisel/Chisel/CHLObjcInstances.h @@ -0,0 +1,21 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#if defined(__cplusplus) + +#include +#include +#include + +#include "zone_allocator.h" + +// Create a set containing all known Classes. +std::unordered_set CHLObjcClassSet(); + +// Enumerates the heap and returns all objects that appear to be legitimate. +std::vector> CHLScanObjcInstances(const std::unordered_set &classSet); + +// Performs a number of heuristic checks on the memory range, to determine if the memory appears to +// be a viable Objective-C object. +id CHLViableObjcInstance(vm_range_t range, const std::unordered_set &classSet); + +#endif diff --git a/Chisel/Chisel/CHLObjcInstances.mm b/Chisel/Chisel/CHLObjcInstances.mm new file mode 100644 index 0000000..4d7fef3 --- /dev/null +++ b/Chisel/Chisel/CHLObjcInstances.mm @@ -0,0 +1,164 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "CHLObjcInstances.h" + +#import + +#include "CHLAllocations.h" + +#include +#include +#include +#include +#if !defined(__LP64__) +using mach_header_t = mach_header; +#else +using mach_header_t = mach_header_64; +#endif + +#include +#include + +#if __has_feature(objc_arc) +#error Disable ARC for this file +#endif + +// Informal protocol to make it easy to call -_isDeallocating +@interface NSObject (Private) +- (BOOL)_isDeallocating; +@end + +static id embeddedObjcInstance(vm_range_t range) { + Dl_info info; + bool aligned = range.address % alignof(void *) == 0; + uint8_t *pointer = (uint8_t *)range.address; + if (aligned && dladdr(pointer, &info)) { + unsigned long size = 0; + uint8_t *start = getsectiondata((mach_header_t *)info.dli_fbase, SEG_DATA, "__cfstring", &size); + uint8_t *end = start + size; + if (start <= pointer || pointer < end) { + // Found NSString/CFString constant. + return reinterpret_cast(range.address); + } + } + return nil; +} + +// TODO: Should this cache results instead of repeated lookups. +bool isFixedSizeClass(Class cls) { + const auto meta = object_getClass(cls); + const auto root = objc_getMetaClass("NSObject"); + + SEL allocs[] = { @selector(allocWithZone:), @selector(alloc) }; + for (const auto &sel : allocs) { + IMP imp = class_getMethodImplementation(meta, sel); + if (imp != class_getMethodImplementation(root, sel)) { + // Class overrides NSObject alloc method, may not have fixed sizes. + return false; + } + } + + return true; +} + +// Runs a number of heuristics on the given address. Returns nil if any heuristic fails, otherwise +// returns that address casted as an object. +// +// Currently the heuristics don't fully guarantee that the returned object is an actual object, but +// when using MallocScribble=1, false positives are unlikely. Further, callers will generally do +// higher level filtering, for example checking a value on the object, which can further eliminate +// false positives. +// +// There's also one known false negative case, NSConcreteValue can store data in the malloc memory +// beyond its instance size. Currently this false negative is allowed. +id CHLViableObjcInstance(vm_range_t range, const std::unordered_set &classSet) +{ + // Check if this address points to an object embedded into Mach-O. + if (range.size == 0) { + return embeddedObjcInstance(range); + } + + id obj = reinterpret_cast(range.address); + + // It's safe to call object_getClass on memory that isn't objc objects. + // Check that the returned Class points to an expected class. + Class cls = object_getClass(obj); + if (classSet.find(cls) == classSet.end()) { + return nil; + } + + // Instance size is the byte count needed for an object's ivars, plus any padding. + // Allocation size is the byte count that malloc will actually allocate for instances of a Class. + const auto instanceSize = class_getInstanceSize(cls); + const auto expectedAllocationSize = malloc_good_size(instanceSize); + const auto extraSize = expectedAllocationSize - instanceSize; + + const bool debug = getenv("FINDINSTANCES_DEBUG") != NULL; + + if (range.size < expectedAllocationSize) { + if (debug) { + printf("%p has class %s but is too small\n", obj, class_getName(cls)); + } + return nil; + } + + if (range.size > expectedAllocationSize && isFixedSizeClass(cls)) { + // Range is too big and the class has no way of allocating larger instances. + if (debug) { + printf("%p has fixed size class %s but is too large\n", obj, class_getName(cls)); + } + return nil; + } + + if (range.size == expectedAllocationSize && extraSize) { + // ObjC instances are allocated with calloc, memory beyond the instance size should be zeros. + // Some classes have been known to store data in the extra space, ex NSConcreteValue. + static const unsigned char ZEROS[1024] = {0}; + auto extra = object_getIndexedIvars(obj); + auto compareSize = std::min(extraSize, sizeof(ZEROS)); + if (memcmp(extra, &ZEROS, compareSize) != 0) { + if (debug) { + printf("%p has class %s but has non-zero memory\n", obj, class_getName(cls)); + } + return nil; + } + } + + // Ignore deallocating objects. + if ([obj _isDeallocating]) { + return nil; + } + + return obj; +} + +struct FindViableObjcInstancesArgs { + const std::unordered_set &classSet; + std::vector> &instances; +}; + +static void findViableObjcInstances(vm_range_t range, void *context) +{ + const auto args = reinterpret_cast(context); + id obj = CHLViableObjcInstance(range, args->classSet); + if (obj != nil) { + args->instances.push_back(obj); + } +} + +std::vector> CHLScanObjcInstances(const std::unordered_set &classSet) +{ + std::vector> instances; + FindViableObjcInstancesArgs args{classSet, instances}; + CHLScanAllocations(&findViableObjcInstances, &args, instances.get_allocator().zone()); + return instances; +} + +std::unordered_set CHLObjcClassSet() +{ + unsigned int count = 0; + auto classList = objc_copyClassList(&count); + std::unordered_set classSet{classList, classList + count, count}; + free(classList); + return classSet; +} diff --git a/Chisel/Chisel/CHLPredicateTools.h b/Chisel/Chisel/CHLPredicateTools.h new file mode 100644 index 0000000..9dd473e --- /dev/null +++ b/Chisel/Chisel/CHLPredicateTools.h @@ -0,0 +1,13 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#if defined(__cplusplus) +extern "C" { +#endif + +NSSet *CHLVariableKeyPaths(NSPredicate *predicate); + +#if defined(__cplusplus) +} +#endif diff --git a/Chisel/Chisel/CHLPredicateTools.m b/Chisel/Chisel/CHLPredicateTools.m new file mode 100644 index 0000000..1145cc5 --- /dev/null +++ b/Chisel/Chisel/CHLPredicateTools.m @@ -0,0 +1,53 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "CHLPredicateTools.h" + +static bool isEqualToConstantComparison(NSComparisonPredicate *predicate) +{ + bool equality = predicate.predicateOperatorType == NSEqualToPredicateOperatorType; + bool direct = predicate.comparisonPredicateModifier == NSDirectPredicateModifier; + bool constantLeft = predicate.leftExpression.expressionType == NSConstantValueExpressionType; + bool constantRight = predicate.rightExpression.expressionType == NSConstantValueExpressionType; + return equality && direct && (constantLeft || constantRight); +} + +NSSet *CHLVariableKeyPaths(NSPredicate *predicate) +{ + if (predicate == nil) { + return nil; + } + + NSMutableSet *keyPaths = [NSMutableSet new]; + + NSMutableArray *predicateStack = [NSMutableArray arrayWithObject:predicate]; + while (predicateStack.count > 0) { + NSPredicate *subpredicate = [predicateStack lastObject]; + [predicateStack removeLastObject]; + + if ([subpredicate isKindOfClass:[NSCompoundPredicate class]]) { + NSCompoundPredicate *compoundPredicate = (NSCompoundPredicate *)subpredicate; + [predicateStack addObjectsFromArray:compoundPredicate.subpredicates]; + continue; + } + + if ([subpredicate isKindOfClass:[NSComparisonPredicate class]]) { + NSComparisonPredicate *comparisonPredicate = (NSComparisonPredicate *)subpredicate; + + if (isEqualToConstantComparison(comparisonPredicate)) { + // Keypaths equal to constants are not variable. Skip these to not be noisy. + // ex `username == "jonalan"` or `alpha == 0` + continue; + } + + // TODO: Handle NSFunctionExpressionType + if (comparisonPredicate.leftExpression.expressionType == NSKeyPathExpressionType) { + [keyPaths addObject:comparisonPredicate.leftExpression.keyPath]; + } + if (comparisonPredicate.rightExpression.expressionType == NSKeyPathExpressionType) { + [keyPaths addObject:comparisonPredicate.rightExpression.keyPath]; + } + } + } + + return keyPaths; +} diff --git a/Chisel/Chisel/Chisel.h b/Chisel/Chisel/Chisel.h new file mode 100644 index 0000000..3ba7cdb --- /dev/null +++ b/Chisel/Chisel/Chisel.h @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +//! Project version number for Chisel. +FOUNDATION_EXPORT double ChiselVersionNumber; + +//! Project version string for Chisel. +FOUNDATION_EXPORT const unsigned char ChiselVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import diff --git a/Chisel/Chisel/Info.plist b/Chisel/Chisel/Info.plist new file mode 100644 index 0000000..fbe1e6b --- /dev/null +++ b/Chisel/Chisel/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Chisel/Chisel/zone_allocator.h b/Chisel/Chisel/zone_allocator.h new file mode 100644 index 0000000..cab8ff2 --- /dev/null +++ b/Chisel/Chisel/zone_allocator.h @@ -0,0 +1,43 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include + +template +struct zone_allocator { + using value_type = T; + + T *allocate(std::size_t n) + { + auto allocation = malloc_zone_malloc(_zone.get(), n * sizeof(T)); + return reinterpret_cast(allocation); + } + + void deallocate(T *p, __unused std::size_t n) + { + malloc_zone_free(_zone.get(), p); + } + + const malloc_zone_t *zone() const + { + return _zone.get(); + } + +private: + std::shared_ptr _zone{malloc_create_zone(0x200, 0), &malloc_destroy_zone}; +}; + +template +bool operator==(const zone_allocator &a, const zone_allocator &b) noexcept +{ + return a.zone() == b.zone(); +} + +template +bool operator!=(const zone_allocator &a, const zone_allocator &b) noexcept +{ + return !(a == b); +} diff --git a/Chisel/ChiselTests/ChiselTests.m b/Chisel/ChiselTests/ChiselTests.m new file mode 100644 index 0000000..4ad3af8 --- /dev/null +++ b/Chisel/ChiselTests/ChiselTests.m @@ -0,0 +1,33 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@interface ChiselTests : XCTestCase + +@end + +@implementation ChiselTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testExample { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. +} + +- (void)testPerformanceExample { + // This is an example of a performance test case. + [self measureBlock:^{ + // Put the code you want to measure the time of here. + }]; +} + +@end diff --git a/Chisel/ChiselTests/Info.plist b/Chisel/ChiselTests/Info.plist new file mode 100644 index 0000000..6c6c23c --- /dev/null +++ b/Chisel/ChiselTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + +