There are currently two ways to get an arbitrary thread call stack. The first way is to get the StackPointer (StackPointer) and stack FramePointer (FramePointer) and recurse to the bottom of the stack.

The task_threads method is provided to retrieve all threads, note that this is the lowest level of Mach threads.

For each thread, all of its information can be retrieved using the thread_get_state method, which is populated with parameters of type _STRUCT_MCONTEXT (two parameters in this method change depending on the CPU architecture).

We need to store the thread’s StackPointer and the top FramePointer, recursively fetching the entire call stack.

Gets the symbolic name of the function call from the Frame Pointer on the stack Frame

Implementation idea:

  1. Get the thread’s StackPointer and FramePointer
  2. Find which image file FramePointer belongs to (.m)
  3. Gets the symbol table of the image file
  4. Locate the symbol name corresponding to the function call address in the symbol table
  5. Return to the FramePointer of the previous calling function, and repeat step 2
  6. At the bottom of the stack, exit

The author of KSCrash came up with the problem of Printing a stack trace from another thread, but he finally figured it out himself. Bestswifter wrote BSBacktraceLogger based on this, which is still very useful in OC, but it cannot print the result well in Swift. I don’t know why, I hope you can tell me if you know.

Printing a stack trace from another thread is implemented through Signal handling.

Signal

Here is a general introduction to the knowledge point need to understand.

The essence of a signal is a simulation of interrupts at the software level. It is a processing mechanism for asynchronous communication, in fact, the process does not know when the signal is coming.

Signal source:

  1. Program errors, such as illegal memory access
  2. External signals such as CTRL+C are pressed
  3. Send signals to another process by kill or SIGQueue

The process of signal processing functions

  1. The signal processing is handled by the kernel. First, the program registers the signal processing function for each signal through sigAL or SIGAction function, and the kernel maintains a signal direction table corresponding to the signal processing mechanism. In this way, after the signal is deregistered in the process, the corresponding handler function is called for processing.
  2. Signal detection and response time
  3. The process

Basic signal processing functions

The most commonly used method of signal operation is signal shielding, which mainly uses the following functions:

Int sigemptySET (sigset_t *set) : The function initializes the set of signals and sets the set to null

Int sigfillSET (sigset_t *set) : The function initializes the signal set, but sets the signal set to the set of all signals.

Int sigaddSet (sigset_t *set,int sigNO) : Adds signal signo to signal set

Int sigDELSET (sigset_t *set,int sigNO) : Deletes sigNO signals from the signal set.

Int sigismemeber(sigset_t* set,int SIGno) : Checks if the signal is suspended.

Int sigprocmask(int how,const sigset_t*set,sigset_t *oset) : Adds the specified signal set to the signal block set of the process. If oSET is provided, the current signal blocking set will be saved to the oset.

There are two ways to initialize a signal set: one is to use sigemptySet to make the signal set contain no signals, and then use sigaddSet to add signals to the signal set. The other is to use sigfillset to contain all signals in the signal set, and then use sigdelset to delete signals to initialize.

Implementation approach

1. Register signal handlers with sigAction

private func setupCallStackSignalHandler() {
    let action = __sigaction_u(__sa_sigaction: signalHandler)
    var sigActionNew = sigaction(__sigaction_u: action, sa_mask: sigset_t(), sa_flags: SA_SIGINFO)

    ifsigaction(SIGUSR2, &sigActionNew, nil) ! = 0 {return} } private func signalHandler(code: Int32, info: UnsafeMutablePointer<__siginfo>? , uap: UnsafeMutableRawPointer?) -> Void { guard pthread_self() == targetThreadelse {
        return
    }

    callstack = frame()
}
Copy the code

2. Send a signal to a specified thread via pthread_kill()

ifpthread_kill(threadId, SIGUSR2) ! = 0 {return nil
}

Copy the code

3. In the signal processing function gain the function call stack backtrace (can use NSThread. CallstackSymbols) 4. 5. Use the swift_demangle function to perform symbol Name reordering, which is specific to Swift. See Swift Name Mangling

6. Use sigfillSet to include all signals in the signal set, and then use sigdelset to delete signals to initialize

var mask = sigset_t()
sigfillset(&mask)
sigdelset(&mask, SIGUSR2)
Copy the code

There’s a lot of code for 3, 4, and 5, so I’m not going to post it, but if you look at backtrace-swift, there’s not a lot of code for pure SWIFT.

The test results

Note that in Xcode, since Xcode blocks signal callbacks, we need to type the following command in the LLDB to let signal callbacks in

pro hand -p true -s false SIGUSR2
Copy the code

Reference:

Getting a backtrace of other thread Synchronization issue with usage of pthread_kill() to terminate thread blocked for I/O Printing a stack trace from another thread gets those things for any thread to call the stack