Symbolicating Stack Trace without Crash
I know this is a rather old question, but I had the same issue now and it took quite some time to find the answer, so I thought I should rather document it (somewhere).
If you have the dSYM for the app version where the stack trace comes from then you can actually turn that into something useful. Reading this answer here lead to this article which helped me a lot. I had this line on top of my stack trace:
0 MyApp 0x000000010010da68 MyApp + 236136 ^ stack address ^ symbol offset
You have two options from here, both involves some math. If you go with atos
you just have to do the math once though and you can look up all steps with one call.
Using atos
To use atos
you need the stack address from the stack trace and you need to find out the load address through some math:
Calculate the load address value by subtracting the symbol offset value from the stack address value (
load address
=stack address
-symbol offset
) of course you have to convert them to the same base to do thatIn my case this was
0x1000D4000
Look up your stack trace entries with
atos
using the load address and the stack addresses from the stack trace withatos -arch <architecture> -o <path to executable inside (!) the dSYM> -l <load address> <stack address 1> <stack address 2> ...
In my case this was
atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x1000D4000 0x000000010010da68
Please keep in mind that you have to supply the path to the actual executable inside the dSYM, otherwise you'll only get an error message.The nice thing about doing all this with atos
is that you can just list all the addresses from your stack trace and you'll get a readable format at once.
Using dwarfdump
To use dwarfdump
you need the file address corresponding to the stack address in the stack trace.
Find out the slide value for the architecture where the stack trace comes from (see Getting the Slide Value in the linked article).
In my case this was
0x100000000
for 64-bit.Convert the symbol offset value (the number right after MyApp + ... in the stack trace,
236136
in my case) to hex and add the result to the slide value. The number you get now is called the file address (file address
=symbol offset
+slide
)In my case this resulted in
0x100039A68
.Look up your stack trace entries with
dwarfdump
using the file address withdwarfdump --lookup <file address> --arch <architecture> <path to dSYM>
In my case this was
dwarfdump --lookup 0x100039A68 --arch arm64 MyApp.dSYM
I ran into the same issue and this answer worked for me: https://stackoverflow.com/a/4954949/299262
You can use atos to symbolicate individual addresses as long as you have the dSYM.
example command:
atos -arch armv7 -o 'app name.app'/'app name' 0x000000000
You could get binary image information at runtime for the build in question, then use that information to symbolicate frames of your stack trace using the atos
command.
Using below code, the output looks like this for example:
YourApp 0x00000001adb1e000 - arm64e - E9B05479-3D07-390C-BD36-73EEDB2B1F75CoreGraphics 0x00000001a92dd000 - arm64e - 2F7F6EE8-635C-332A-BAC3-EFDA4894C7E2CoreImage 0x00000001afc00000 - arm64e - CF56BCB1-9EE3-392D-8922-C8894C9F94C7
Code:
import Foundationimport MachOpublic struct BinaryImagesInspector { #if arch(x86_64) || arch(arm64) typealias MachHeader = mach_header_64 #else typealias MachHeader = mach_header #endif /// Provides binary infos that are then used with the atos command to symbolicate stack traces /// - Parameter imageNamesToLog: an optional array of binary image names to restrict the infos to /// - Returns: An array of strings containing info on loaded binary name, its load address, architecture /// - Note: Example: /// /// atos -arch arm64 -o [YOUR-DSYM-ID].dSYM/Contents/Resources/DWARF/[YOUR APP] -l 0x0000000000000000 0x0000000000000000 public static func getBinaryImagesInfo(imageNamesToLog: [String]? = nil) -> [String] { let count = _dyld_image_count() var stringsToLog = [String]() for i in 0..<count { guard let dyld = _dyld_get_image_name(i) else { continue } let dyldStr = String(cString: dyld) let subStrings = dyldStr.split(separator: "/") guard let imageName = subStrings.last else { continue } if let imageNamesToLog = imageNamesToLog { guard imageNamesToLog.contains(String(imageName)) else { continue } } guard let uncastHeader = _dyld_get_image_header(i) else { continue } let machHeader = uncastHeader.withMemoryRebound(to: MachHeader.self, capacity: MemoryLayout<MachHeader>.size) { $0 } guard let info = NXGetArchInfoFromCpuType(machHeader.pointee.cputype, machHeader.pointee.cpusubtype) else { continue } guard let archName = info.pointee.name else { continue } let uuid = getBinaryImageUUID(machHeader: machHeader) let logStr = "\(imageName) \(machHeader.debugDescription) - \(String(cString: archName)) - \(uuid ?? "uuid not found")" stringsToLog.append(logStr) } return stringsToLog } private static func getBinaryImageUUID(machHeader: UnsafePointer<MachHeader>) -> String? { guard var header_ptr = UnsafePointer<UInt8>.init(bitPattern: UInt(bitPattern: machHeader)) else { return nil } header_ptr += MemoryLayout<MachHeader>.size guard var command = UnsafePointer<load_command>.init(bitPattern: UInt(bitPattern: header_ptr)) else { return nil } for _ in 0..<machHeader.pointee.ncmds { if command.pointee.cmd == LC_UUID { guard let ucmd_ptr = UnsafePointer<uuid_command>.init(bitPattern: UInt(bitPattern: header_ptr)) else { continue } let ucmd = ucmd_ptr.pointee let cuuidBytes = CFUUIDBytes(byte0: ucmd.uuid.0, byte1: ucmd.uuid.1, byte2: ucmd.uuid.2, byte3: ucmd.uuid.3, byte4: ucmd.uuid.4, byte5: ucmd.uuid.5, byte6: ucmd.uuid.6, byte7: ucmd.uuid.7, byte8: ucmd.uuid.8, byte9: ucmd.uuid.9, byte10: ucmd.uuid.10, byte11: ucmd.uuid.11, byte12: ucmd.uuid.12, byte13: ucmd.uuid.13, byte14: ucmd.uuid.14, byte15: ucmd.uuid.15) guard let cuuid = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault, cuuidBytes) else { return nil } let suuid = CFUUIDCreateString(kCFAllocatorDefault, cuuid) let encoding = CFStringGetFastestEncoding(suuid) guard let cstr = CFStringGetCStringPtr(suuid, encoding) else { return nil } let str = String(cString: cstr) return str } header_ptr += Int(command.pointee.cmdsize) guard let newCommand = UnsafePointer<load_command>.init(bitPattern: UInt(bitPattern: header_ptr)) else { continue } command = newCommand } return nil }}
Further reading:
- How to determine binary image architecture at runtime?
- https://github.com/apple/swift/blob/master/stdlib/private/SwiftReflectionTest/SwiftReflectionTest.swift
- https://lowlevelbits.org/parsing-mach-o-files/
- https://developer.apple.com/library/archive/technotes/tn2151/_index.html
Also available as swift package on GitHub.