Obtaining Swift stack information

This project complete code: QYBacktrace, if you are helpful welcome star ~

OC version can refer to zhang xingyu’s BSBacktraceLogger

What is a thread call stack

Call stack, also known as execution stack, control stack, runtime stack and machine stack, is an important data structure for storing running subroutines in computer science. It mainly stores the return address, local variables, parameters and environment transfer. It is used to track the point at which each active subroutine should return to control after completion of execution. In simple terms, it is a place to store information about the current thread calling function. They are stored in a stack structure that allows functions to move down and back up.

How do I get the thread call stack

Thread provides Thread.callstackSymbols to obtain the callstack of the current Thread. It can also be obtained through the Backtrace/Backtrace_symbols interface, but can only obtain the callstack of the current Thread, not the callstack of other threads.

Is it possible to get stack information for all threads? There are currently two options:

  • Through Mach Thread (currently the mainstream solution)
  • Through Signal handle

The system method gets the current thread call stack

Thread.callstackSymbols

DispatchQueue.global().async {
    let symbols = Thread.callStackSymbols
    for symbol in symbols {
    		print(symbol.description)
    }
}
Copy the code

backtrace_symbols

OC version

+ (NSArray *)backtrace {// Define a pointer array void* callstack[128]; // this function is used to get the callstack of the current thread. This information will be stored in the callstack. The 128 parameter specifies how many void* elements can be stored in the callStack. The pointer in the CallStack is actually the return address retrieved from the stack. Each stack frame has a return address. int frames = backtrace(callstack, 128); // The callstack argument should be the pointer to the array from the backtrace function. Frames is the number of elements in the array (the value returned by backtrace). The function returns a pointer to an array of strings the same size as callStack. Char ** STRS = backtrace_symbols(callstack, frames); NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames]; for (int i=0; i < frames; i++) { [backtrace addObject:[NSString stringWithUTF8String:strs[i]]]; } // Pay attention to free(STRS); return backtrace; }Copy the code

Swift version

@_silgen_name("backtrace_symbols")
fileprivate func backtrace_symbols(_ stack: UnsafePointer<UnsafeMutableRawPointer? >! ._ frame: Int32) -> UnsafeMutablePointer<UnsafeMutablePointer<Int8>? >!@_silgen_name("backtrace")
fileprivate func backtrace(_ stack: UnsafePointer<UnsafeMutableRawPointer? >! ._ size: Int32) -> Int32

func getBacktrace(a) -> String {
    let maxSize: Int32 = 128
    let addrs = UnsafeMutablePointer<UnsafeMutableRawPointer? >.allocate(capacity:Int(maxSize))
    defer { addrs.deallocate() }
    let count = backtrace(addrs, maxSize)
    var symbols: [String] = []
    if let bs = backtrace_symbols(addrs, count) {
        symbols = UnsafeBufferPointer(start: bs, count: Int(count)).map {
            guard let symbol = $0 else {
                return "<null>"
            }
            return String(cString: symbol)
        }
    }
    return symbols.joined(separator: "\n")}Copy the code

What @_silgen_name does is call the function below the keyword and actually call the keyword wrapped function.

Note: the keyword and the following declared function must be up and down, and cannot be separated by other functions.

Get the thread call stack through a Mach Thread

Gets the call stack for all threads

  1. Mach provides a system method, task_threads, that fetches all the threads of the current process. All threads are stored in the Threads array.

    var count: mach_msg_type_number_t = 0
    var threads: thread_act_array_t!
    let kert = task_threads(mach_task_self_, &(threads), &count)
    Copy the code

Note: The concept of task and process is one-to-one, that is, iOS system processes (corresponding applications) are associated with a Mach task object at the bottom. Therefore, mach_task_self_ can be used to obtain the task object corresponding to the current process.

Here, the thread is the lowest level of Mach kernel thread. The POSIX thread pthread corresponds to the kernel thread one by one, which is the abstraction of kernel thread. NSThread is the object-oriented encapsulation of Pthread.

  1. Mach also provides a method, thread_get_state, that gets context information about the current thread, populated with parameters of type _STRUCT_MCONTEXT.

    Two of the parameters in this method (THREAD_STATE_COUNT and THREAD_STATE_FLAVOR) vary by CPU architecture, so you need to be aware of the differences between cpus.

    _STRUCT_MCONTEXT machineContext;
    mach_msg_type_number_t stateCount = THREAD_STATE_COUNT;
    kern_return_t kret = thread_get_state(thread, THREAD_STATE_FLAVOR, (thread_state_t)&(machineContext.__ss), &stateCount);
    if(kret ! = KERN_SUCCESS) {return 0;
    }
    Copy the code
  2. A _STRUCT_MCONTEXT structure stores the Stack Pointer of the current thread and the Frame Pointer of the topmost Stack Frame to retrieve the call Stack of the entire thread.

    int mach_backtrace(thread_t thread, void支那stack.int maxSymbols) {
        _STRUCT_MCONTEXT machineContext;
        mach_msg_type_number_t stateCount = THREAD_STATE_COUNT;
    
        kern_return_t kret = thread_get_state(thread, THREAD_STATE_FLAVOR, (thread_state_t)&(machineContext.__ss), &stateCount);
        if(kret ! = KERN_SUCCESS) {return 0;
        }
    
        int i = 0;
        // Get the PC from the top stack frame, i.e. the current function pointer
        stack[i] = (void *)machineContext.__ss.__programCounter;
        ++i;
    #if defined(__arm__) || defined (__arm64__)
        // Get the LR of the top stack frame, i.e. return pointer
        stack[i] = (void *)machineContext.__ss.__lr;
        ++i;
    #endif
        FP is a pointer to the FP pointer of the previous stack frame
        void **currentFramePointer = (void **)machineContext.__ss.__framePointer;
        while (i < maxSymbols) {
            // Get the FP of the last stack frame
            void **previousFramePointer = *currentFramePointer;
            if(! previousFramePointer)break;
            // Store the LR of the previous stack frame in the Call Stack Address Array
            stack[i] = *(currentFramePointer+1);
            currentFramePointer = previousFramePointer;
            ++i;
        }
        return i;
    }
    Copy the code

    ARM64 stack frame architecture

    X86_64 stack frame architecture

    By the above two pictures you can see, the FP point to is on the stack frames stored in a stack frame of FP, and address offset 8 bytes up to its, is the return pointer LR, through constant recursive of FP, we can get to all the thread stack frames, and then through each stack frame of the LR, to get to a function pointer, That is, the function call stack of the entire thread is retrieved.

    It took me a lot of time to understand this part, mainly because the information on the Internet is not clear, but I am not quite sure whether my idea is correct, it seems to be consistent with the code logic at present, if anyone has a better understanding, welcome to correct!

  3. After retrieving the function address array for the entire thread call stack, there are two options:

    1. Call the system’s C methodbacktrace_symbolsParse the symbol information and convert the symbols into strings.
    2. The dlADDR function and dl_info function are used to obtain the symbol information of an address, and then it is converted into a string, and Swift symbol reorganization is performed.

    The complete code is a bit long, please go to my GitHub

Gets the call stack for a thread

Sometimes we don’t need to get the call stack information for all threads, but only for one thread. However, Thread instances cannot get thread_state_T information, and there is no interface to get callstackSymbols.

So what we need to do now is: Thread → PThread → Mach Thread

However, Apple did not provide the method of Thread → Pthread, and pthread and Mach thread can be converted into each other, so the key is to see if there is any relationship between pthread and thread. Zhang Xingyu proposed a solution. Check whether threads and Pthreads correspond according to their names.

Now all you need to do is give the thread a name and iterate through all the Mach threads. If the names are the same, you can determine that the Mach thread corresponds to the instance thread. With the Mach Thread identified, we can get the call stack information.

Another problem is that the name of the main thread is not valid, so special handling is required.

fileprivate static func machThread(from thread: Thread) -> thread_t {
    var count: mach_msg_type_number_t = 0
    var threads: thread_act_array_t!
    guard task_threads(mach_task_self_, &(threads), &count) = = KERN_SUCCESS else {
        return mach_thread_self()
    }

    if thread.isMainThread {
        return main_thread_t ?? mach_thread_self()
    }
    
    let originName = thread.name

    for i in 0..<count {
        let machThread = threads[Int(i)]
        if let p_thread = pthread_from_mach_thread_np(machThread) {
            var name: [Int8] = Array<Int8>(repeating: 0, count: 256)
            pthread_getname_np(p_thread, &name, name.count)
            if thread.name = = String(cString: name) {
                thread.name = originName
                return machThread
            }
        }
    }

    thread.name = originName
    return mach_thread_self()
}
Copy the code

instructions

Xcode debugging output is unstable, sometimes there is a call to print but no output, it is recommended to go to the console according to the UUID of the device to view the complete output.

For optimization purposes, some symbol tables are stored in dSYM files on disk that cannot be parsed at runtime, so the symbol name is displayed as

for real machine debugging and Release mode.

Refer to the article

IOS development – Explore the iOS thread call stack and symbolization

Capture any thread call stack information via Mach Thread -Swift

IOS gets an arbitrary thread call stack

Get any thread call stack through Signal handling