├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── arm64-to-sim │ ├── Extensions.swift │ ├── Patcher.swift │ ├── Transmogrifier.swift │ └── main.swift └── build.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | *.xcworkspacedata 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Bogo Giertler 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. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ShellOut", 6 | "repositoryURL": "https://github.com/JohnSundell/ShellOut", 7 | "state": { 8 | "branch": null, 9 | "revision": "e1577acf2b6e90086d01a6d5e2b8efdaae033568", 10 | "version": "2.3.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-argument-parser", 15 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 16 | "state": { 17 | "branch": null, 18 | "revision": "e1465042f195f374b94f915ba8ca49de24300a0d", 19 | "version": "1.0.2" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "arm64-to-sim", 6 | platforms: [ 7 | .macOS(.v11) 8 | ], 9 | products: [ 10 | .executable(name: "arm64-to-sim", targets: ["arm64-to-sim"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/JohnSundell/ShellOut", from: "2.0.0"), 14 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), 15 | ], 16 | targets: [ 17 | .executableTarget(name: "arm64-to-sim", dependencies: [ 18 | .product(name: "ShellOut", package: "ShellOut"), 19 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 20 | ]), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # arm64-to-sim 2 | 3 | A simple command-line tool for hacking native ARM64 binaries to run on the Apple Silicon iOS Simulator. 4 | 5 | ## Background 6 | 7 | If you are running your iOS project on a Apple Silicon simulator, you might have encountered the following error: 8 | 9 | ``` 10 | In ...somelib.a(SomeObject.o), building for iOS Simulator, but linking in object file built for iOS, file '...somelib.a' for architecture arm64 11 | ``` 12 | 13 | ![error](https://user-images.githubusercontent.com/47009/144603266-8eb1fde7-6459-4c48-b105-18bc16df8c08.png) 14 | 15 | The third-party static libraries you are using don't have support for ARM64 simulator. And technically they can't unless they are migrated to the XCFramework format. 16 | 17 | This tool will hack the static libraries to make them run for your ARM64 simulator. 18 | 19 | ## Prepare 20 | 21 | Compile the code using Swift 5.5. 22 | 23 | ```bash 24 | swift build -c release 25 | ``` 26 | 27 | You'll find the `arm64-to-sim` binary in your `.build/release` directory. 28 | 29 | Or you can just download a pre-compiled binary from [Releases](https://github.com/luosheng/arm64-to-sim/releases). 30 | 31 | ## USage 32 | 33 | ```bash 34 | USAGE: arm64-to-sim 35 | 36 | OPTIONS: 37 | --version Show the version. 38 | -h, --help Show help information. 39 | 40 | SUBCOMMANDS: 41 | patch 42 | restore 43 | 44 | See 'arm64-to-sim help ' for detailed help. 45 | ``` 46 | 47 | Start by patching the library with `arm64-to-sim patch [file]`. 48 | 49 | `arm64_to_sim` will back up your original file as `[file].original`, and create a patched one named `[file].patch`. It will then create a symbolic link to the patched file. Now you are ready to run your apps targeting simulators. 50 | 51 | If you are preparing for a release, just use the `restore` command and it will point the symbolic link back to your original library file. 52 | 53 | ## Can it go further? 54 | 55 | Sure. Put `arm64_to_sim` to the root directory of your iOS projects (or wherever you like). Then add a `Run Script` phase to your `Build Phases` and move it above `Compile Sources`. 56 | 57 | Paste the following script to the editor, then add the libraries to the `Input Files` section. 58 | 59 | ```bash 60 | if [[ "${ARCHS}" == *arm64* ]]; then 61 | i=0 62 | while [ $i -ne $SCRIPT_INPUT_FILE_COUNT ]; do 63 | lib=SCRIPT_INPUT_FILE_$i 64 | if [[ "${SDKROOT}" == *Simulator* ]]; then 65 | ./arm64-to-sim patch "${!lib}" 66 | else 67 | ./arm64-to-sim restore "${!lib}" 68 | fi 69 | i=$(($i + 1)) 70 | done 71 | fi 72 | ``` 73 | 74 | ![screenshot](https://user-images.githubusercontent.com/47009/144601342-4d55108e-c1c1-4f39-a64d-89348e7f12fc.png) 75 | 76 | This script phase will target ARM64 archs only, patch your libraries when you are running on a simulator, and restore them otherwise. 77 | -------------------------------------------------------------------------------- /Sources/arm64-to-sim/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Luo Sheng on 2021/12/3. 6 | // 7 | 8 | import Foundation 9 | 10 | // support checking for Mach-O `cmd` and `cmdsize` properties 11 | extension Data { 12 | var loadCommand: UInt32 { 13 | let lc: load_command = withUnsafeBytes { $0.load(as: load_command.self) } 14 | return lc.cmd 15 | } 16 | 17 | var commandSize: Int { 18 | let lc: load_command = withUnsafeBytes { $0.load(as: load_command.self) } 19 | return Int(lc.cmdsize) 20 | } 21 | 22 | func asStruct(fromByteOffset offset: Int = 0) -> T { 23 | return withUnsafeBytes { $0.load(fromByteOffset: offset, as: T.self) } 24 | } 25 | } 26 | 27 | extension Array where Element == Data { 28 | func merge() -> Data { 29 | return reduce(into: Data()) { $0.append($1) } 30 | } 31 | } 32 | 33 | // support peeking at Data contents 34 | extension FileHandle { 35 | func peek(upToCount count: Int) throws -> Data? { 36 | // persist the current offset, since `upToCount` doesn't guarantee all bytes will be read 37 | let originalOffset = offsetInFile 38 | let data = try read(upToCount: count) 39 | try seek(toOffset: originalOffset) 40 | return data 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/arm64-to-sim/Patcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Luo Sheng on 2021/12/3. 6 | // 7 | 8 | import Foundation 9 | import ShellOut 10 | 11 | struct Patcher { 12 | 13 | private static let ORIGINAL_EXTENSION = "original" 14 | private static let PATCH_EXTENSION = "patched" 15 | 16 | private static func getArchitectures(atUrl url: URL) throws -> [String] { 17 | let output = try shellOut(to: "file", arguments: [url.path]) 18 | let pattern = #"for architecture (?\w*)"# 19 | let regex = try NSRegularExpression(pattern: pattern, options: []) 20 | let nsrange = NSRange(output.startIndex.. (Data, [Data], Data) { 13 | guard let handle = FileHandle(forReadingAtPath: path) else { 14 | fatalError("Cannot open a handle for the file at \(path). Aborting.") 15 | } 16 | 17 | // chop up the file into a relevant number of segments 18 | let headerData = try! handle.read(upToCount: MemoryLayout.stride)! 19 | 20 | let header: mach_header_64 = headerData.asStruct() 21 | if header.magic != MH_MAGIC_64 || header.cputype != CPU_TYPE_ARM64 { 22 | fatalError("The file is not a correct arm64 binary. Try thinning (via lipo) or unarchiving (via ar) first.") 23 | } 24 | 25 | let loadCommandsData: [Data] = (0...stride) 27 | return try! handle.read(upToCount: Int(loadCommandPeekData!.commandSize))! 28 | } 29 | 30 | if isDynamic { 31 | let bytesToDiscard = abs(MemoryLayout.stride - MemoryLayout.stride) 32 | _ = handle.readData(ofLength: bytesToDiscard) 33 | } 34 | 35 | let programData = try! handle.readToEnd()! 36 | 37 | try! handle.close() 38 | 39 | return (headerData, loadCommandsData, programData) 40 | } 41 | 42 | private static func updateSegment64(_ data: Data, _ offset: UInt32) -> Data { 43 | // decode both the segment_command_64 and the subsequent section_64s 44 | var segment: segment_command_64 = data.asStruct() 45 | 46 | let sections: [section_64] = (0...stride + index * MemoryLayout.stride 48 | return data.asStruct(fromByteOffset: offset) 49 | } 50 | 51 | // shift segment information by the offset 52 | segment.fileoff += UInt64(offset) 53 | segment.filesize += UInt64(offset) 54 | segment.vmsize += UInt64(offset) 55 | 56 | let offsetSections = sections.map { section -> section_64 in 57 | let sectionType = section.flags & UInt32(SECTION_TYPE) 58 | switch Int32(sectionType) { 59 | case S_ZEROFILL, S_GB_ZEROFILL, S_THREAD_LOCAL_ZEROFILL: 60 | return section 61 | case _: break 62 | } 63 | 64 | var section = section 65 | section.offset += UInt32(offset) 66 | section.reloff += section.reloff > 0 ? UInt32(offset) : 0 67 | return section 68 | } 69 | 70 | var datas = [Data]() 71 | datas.append(Data(bytes: &segment, count: MemoryLayout.stride)) 72 | datas.append(contentsOf: offsetSections.map { section in 73 | var section = section 74 | return Data(bytes: §ion, count: MemoryLayout.stride) 75 | }) 76 | 77 | return datas.merge() 78 | } 79 | 80 | private static func updateVersionMin(_ data: Data, _ offset: UInt32, minos: UInt32, sdk: UInt32) -> Data { 81 | var command = build_version_command(cmd: UInt32(LC_BUILD_VERSION), 82 | cmdsize: UInt32(MemoryLayout.stride), 83 | platform: UInt32(PLATFORM_IOSSIMULATOR), 84 | minos: minos << 16 | 0 << 8 | 0, 85 | sdk: sdk << 16 | 0 << 8 | 0, 86 | ntools: 0) 87 | 88 | return Data(bytes: &command, count: MemoryLayout.stride) 89 | } 90 | 91 | private static func updateDataInCode(_ data: Data, _ offset: UInt32) -> Data { 92 | var command: linkedit_data_command = data.asStruct() 93 | command.dataoff += offset 94 | return Data(bytes: &command, count: data.commandSize) 95 | } 96 | 97 | private static func updateSymTab(_ data: Data, _ offset: UInt32) -> Data { 98 | var command: symtab_command = data.asStruct() 99 | command.stroff += offset 100 | command.symoff += offset 101 | return Data(bytes: &command, count: data.commandSize) 102 | } 103 | 104 | private static func computeLoadCommandsEditor(_ loadCommandsData: [Data], isDynamic: Bool) -> ((Data, UInt32, UInt32) -> Data) { 105 | 106 | if isDynamic { 107 | return updateDylibFile 108 | } 109 | 110 | var contains_LC_VERSION_MIN_IPHONEOS = false 111 | var contains_LC_BUILD_VERSION = false 112 | for lc in loadCommandsData { 113 | let loadCommand = UInt32(lc.loadCommand) 114 | if loadCommand == LC_VERSION_MIN_IPHONEOS { 115 | contains_LC_VERSION_MIN_IPHONEOS = true 116 | } else if loadCommand == LC_BUILD_VERSION { 117 | contains_LC_BUILD_VERSION = true 118 | } 119 | } 120 | 121 | if contains_LC_VERSION_MIN_IPHONEOS == contains_LC_BUILD_VERSION { 122 | if contains_LC_BUILD_VERSION == true { 123 | fatalError("Bad Mach-O Object file: Both LC_VERSION_MIN_IPHONEOS and LC_BUILD_VERSION are present.\nEither one of them should be present") 124 | } else { 125 | fatalError("Bad Mach-O Object file: does not contain LC_VERSION_MIN_IPHONEOS or LC_BUILD_VERSION.\nEither one of them should be present") 126 | } 127 | } 128 | 129 | if contains_LC_VERSION_MIN_IPHONEOS { 130 | // `offset` is kind of a magic number here, since we know that's the only meaningful change to binary size 131 | // having a dynamic `offset` requires two passes over the load commands and is left as an exercise to the reader 132 | return updatePreiOS12ObjectFile 133 | } else { 134 | return updatePostiOS12ObjectFile 135 | } 136 | } 137 | 138 | 139 | static func updatePostiOS12ObjectFile(lc: Data, minos: UInt32, sdk: UInt32) -> Data { 140 | let cmd = Int32(bitPattern: lc.loadCommand) 141 | switch cmd { 142 | case LC_BUILD_VERSION: 143 | return updateVersionMin(lc, 0, minos: minos, sdk: sdk) 144 | default: 145 | return lc 146 | } 147 | } 148 | 149 | static func updatePreiOS12ObjectFile(lc: Data, minos: UInt32, sdk: UInt32) -> Data { 150 | // `offset` is kind of a magic number here, since we know that's the only meaningful change to binary size 151 | // having a dynamic `offset` requires two passes over the load commands and is left as an exercise to the reader 152 | let offset = UInt32(abs(MemoryLayout.stride - MemoryLayout.stride)) 153 | let cmd = Int32(bitPattern: lc.loadCommand) 154 | switch cmd { 155 | case LC_SEGMENT_64: 156 | return updateSegment64(lc, offset) 157 | case LC_VERSION_MIN_IPHONEOS: 158 | return updateVersionMin(lc, offset, minos: minos, sdk: sdk) 159 | case LC_DATA_IN_CODE, LC_LINKER_OPTIMIZATION_HINT: 160 | return updateDataInCode(lc, offset) 161 | case LC_SYMTAB: 162 | return updateSymTab(lc, offset) 163 | case LC_BUILD_VERSION: 164 | return updateVersionMin(lc, offset, minos: minos, sdk: sdk) 165 | default: 166 | return lc 167 | } 168 | } 169 | 170 | static func updateDylibFile(lc: Data, minos: UInt32, sdk: UInt32) -> Data { 171 | // `offset` is kind of a magic number here, since we know that's the only meaningful change to binary size 172 | // having a dynamic `offset` requires two passes over the load commands and is left as an exercise to the reader 173 | let offset = UInt32(abs(MemoryLayout.stride - MemoryLayout.stride)) 174 | let cmd = Int32(bitPattern: lc.loadCommand) 175 | guard cmd != LC_BUILD_VERSION else { 176 | fatalError("This arm64 binary already contains an LC_BUILD_VERSION load command!") 177 | } 178 | if cmd == LC_VERSION_MIN_IPHONEOS { 179 | return updateVersionMin(lc, offset, minos: minos, sdk: sdk) 180 | } 181 | return lc 182 | } 183 | 184 | 185 | public static func processBinary(atPath path: String, minos: UInt32 = 13, sdk: UInt32 = 13, isDynamic: Bool = false) { 186 | let (headerData, loadCommandsData, programData) = readBinary(atPath: path, isDynamic: isDynamic) 187 | 188 | let editor = computeLoadCommandsEditor(loadCommandsData, isDynamic: isDynamic) 189 | 190 | let editedCommandsData = loadCommandsData 191 | .map { return editor($0, minos, sdk) } 192 | .merge() 193 | 194 | var header: mach_header_64 = headerData.asStruct() 195 | header.sizeofcmds = UInt32(editedCommandsData.count) 196 | 197 | // reassemble the binary 198 | let reworkedData = [ 199 | Data(bytes: &header, count: MemoryLayout.stride), 200 | editedCommandsData, 201 | programData 202 | ].merge() 203 | 204 | // save back to disk 205 | try! reworkedData.write(to: URL(fileURLWithPath: path)) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /Sources/arm64-to-sim/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ArgumentParser 3 | 4 | struct Arm64ToSim: ParsableCommand { 5 | 6 | static var configuration = CommandConfiguration( 7 | abstract: "A simple command-line tool for hacking native ARM64 binaries to run on the Apple Silicon iOS Simulator.", 8 | version: "1.0.0", 9 | subcommands: [Patch.self, Restore.self] 10 | ) 11 | 12 | } 13 | 14 | extension Arm64ToSim { 15 | struct Patch: ParsableCommand { 16 | @Argument(help: "The path of the library to patch.") 17 | var path: String 18 | 19 | @Option() 20 | var minOS: UInt32 = 13 21 | 22 | @Option() 23 | var sdk: UInt32 = 13 24 | 25 | func run() throws { 26 | try Patcher.patch(atPath: path, minos: minOS, sdk: sdk) 27 | } 28 | } 29 | 30 | struct Restore: ParsableCommand { 31 | @Argument(help: "The path of the library to restore.") 32 | var path: String 33 | 34 | func run() throws { 35 | try Patcher.restore(atPath: path) 36 | } 37 | } 38 | } 39 | 40 | Arm64ToSim.main() 41 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | swift build -c release --arch arm64 --arch x86_64 2 | --------------------------------------------------------------------------------