iOS - Symbolicate Stack trace symbols

3.3k views Asked by At

I want to symbolicate the stack trace symbols logged using [NSThread callStackSymbols] to identify the full stack backtrace calls. But, this doesn't give symbolicated trace.

0   TestApp                               0x0029616f TestApp + 1823087
    1   TestApp                               0x003ef18d TestApp + 3236237
    2   UIKit                               0x2ab7bb1f <redacted> + 438
    3   UIKit                               0x2ac0bea3 <redacted> + 306
    4   UIKit                               0x2ab7bb1f <redacted> + 438
    5   CoreFoundation                      0x2757546d <redacted> + 48
    6   CoreFoundation                      0x2756e4c3 <redacted> + 234
    7   UIKit                               0x2ab7bc9b <redacted> + 818
    8   UIKit                               0x2ae32799 <redacted> + 584
    9   UIKit                               0x2abdfbd9 <redacted> + 308
    10  UIKit                               0x2ab5bdd7 <redacted> + 458
    11  CoreFoundation                      0x2760fffd <redacted> + 20
    12  CoreFoundation                      0x2760d6bb <redacted> + 278
    13  CoreFoundation                      0x2760dac3 <redacted> + 914
    14  CoreFoundation                      0x2755b3b1 CFRunLoopRunSpecific + 476
    15  CoreFoundation                      0x2755b1c3 CFRunLoopRunInMode + 106
    16  GraphicsServices                    0x2eb88201 GSEventRunModal + 136
    17  UIKit                               0x2abc543d UIApplicationMain + 1440
    18  TestApp                               0x0031581b TestApp + 2344987
    19  libdyld.dylib                       0x35a6baaf <redacted> + 2 

I tried this with development as well as enterprise build. Neither worked out. Is there anyway to symbolicate this? I searched lot of forums and all are asking to do this with atos command like below

atos -arch armv7 -o 'app name.app'/'app name' 0x000000000

But, I am not sure whats the memory address i have to use for the above command and how to get it.

Any help would be greatly appreciated, thanks.

2

There are 2 answers

0
schmittsfn On

Here an example of how you can get binary image information at runtime.

The output of below code looks like this for example:

YourApp 0x00000001adb1e000 - arm64e - E9B05479-3D07-390C-BD36-73EEDB2B1F75
CoreGraphics 0x00000001a92dd000 - arm64e - 2F7F6EE8-635C-332A-BAC3-EFDA4894C7E2
CoreImage 0x00000001afc00000 - arm64e - CF56BCB1-9EE3-392D-8922-C8894C9F94C7

You then use the memory address of the binary image whose frame you want to symbolicate as argument in the atos command you mentioned.

Code:

import Foundation
import MachO

public 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:

Also available as swift package on GitHub.

1
cjbeauchamp On

There's a few pieces you're skipping here. The memory addresses in atos should reference the load address and the stack address, and you'll need the dSYM file as well.

There's actually a very good writeup on apteligent called Symbolicating an iOS Crash Report about doing this manually. I would suggest reading it thoroughly to understand how symbolication works.