Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Open more actions menu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

一次半失败的Swift获取任意线程调用栈之旅

前言

研究了一些获取任意线程调用栈的开源库,基本是从BSBacktraceLoggerKSCrash衍生出来的,核心实现都是C语言,无纯Swift实现的,所以想搞个纯Swift获取任意线程调用栈库。

什么情况需要获取线程调用栈

分析性能瓶颈,崩溃原因等等。

如何获取线程调用栈

调用栈

每个线程都有自己的栈空间,线程中会有很多函数调用,每个函数调用都有自己的stack frame栈帧,栈就是由一个一个栈帧组成。

下面这个是ARM的栈帧布局图:

StackFrame

main stack frame为调用函数的栈帧,func1 stack frame为当前函数(被调用者)的栈帧,栈底在高地址,栈向下增长。图中FP就是栈基址,它指向函数的栈帧起始地址;SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序很是规矩,依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。

NSThread 转 Mach_thread

NSThread是对pthread的封装,系统提供了pthread_mach_thread_np函数将pthread转换为Mach_thread。如果能够按照NSThread -> pthread -> Mach_thread的路径转换,即可一步步获取到内核thread。但由于NSThread没有保留线程的 pthread,所以常规手段无法满足需求。

换种思路,系统提供了pthread_from_mach_thread_np函数将Mach_thread转换为pthread,提供了pthread_getname_np函数获取pthread.name,然后将其与NSThread.name比较即可获取到内核thread。

var machThread: thread_t {
    // 特殊处理:主线程转内核 thread(主线程设置 name 后无法用 pthread_getname_np 读取到)
    if isMainThread {
        var main_thread_t: thread_t?
        if !Thread.isMainThread {
            DispatchQueue.main.sync {
                main_thread_t = mach_thread_self()
            }
        }
        return main_thread_t ?? mach_thread_self()
    }
    
    var count: mach_msg_type_number_t = 0
    var threads: thread_act_array_t!

    // 获取所有内核 thread
    guard task_threads(mach_task_self_, &(threads), &count) == KERN_SUCCESS else {
        return mach_thread_self()
    }
    
    let originName = name
    defer { name = originName }

    name = String(Int(Date().timeIntervalSince1970))
    
    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: 128)
            pthread_getname_np(p_thread, &name, name.count)
            if self.name == String(cString: name) {
                return machThread
            }
        }
    }
    
    return mach_thread_self()
}

函数调用栈符号化

  • backtrace_symbols:在桥接文件中#import <execinfo.h>即可使用。(小技巧:亦可不用桥接文件,直接使用@_silgen_name("C语言标准ABI的函数"),本文即采取此方式)
/// 亦可在桥接文件中#import <execinfo.h>
/// 函数调用栈符号化
@_silgen_name("backtrace_symbols")
private func backtrace_symbols(_ stack: UnsafePointer<UnsafeMutableRawPointer?>!, _ frames: Int32) -> UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?
  • dladdr:通过dladdr函数和Dl_info获得某个地址的符号信息。可以分解出dli_fnamedli_fbasedli_snamedli_saddr信息,比backtrace_symbols方式更加灵活。
var dlInfo = dl_info()
dladdr(UnsafeRawPointer(bitPattern: address), &dlInfo)

var image = "??"
if let dli_fname = dlInfo.dli_fname,
   let fname = String(validatingUTF8: dli_fname),
   let _ = fname.range(of: "/", options: .backwards, range: nil, locale: nil) {
    image = (fname as NSString).lastPathComponent
}

var symbol = "??"
if let dli_sname = dlInfo.dli_sname, let sname = String(validatingUTF8: dli_sname) {
    symbol = sname
} else if let dli_fname = dlInfo.dli_fname, let _ = String(validatingUTF8: dli_fname), image != "??" {
    symbol = image
} else {
    symbol = String(format: "0x%1x", UInt(bitPattern: dlInfo.dli_saddr))
}

let demangleSymbol = swift_demangle(symbol)

var offset: UInt = 0
if let dli_sname = dlInfo.dli_sname, let _ = String(validatingUTF8: dli_sname) {
    offset = address - UInt(bitPattern: dlInfo.dli_saddr)
} else if let dli_fname = dlInfo.dli_fname, let _ = String(validatingUTF8: dli_fname) {
    offset = address - UInt(bitPattern: dlInfo.dli_fbase)
} else {
    offset = address - UInt(bitPattern: dlInfo.dli_saddr)
}

return image.utf8CString.withUnsafeBufferPointer { (imageBuffer: UnsafeBufferPointer<CChar>) -> String in
    #if arch(x86_64) || arch(arm64)
    return String(format: "%-4ld%-35s 0x%016llx %@ + %ld", index, UInt(bitPattern: imageBuffer.baseAddress), address, demangleSymbol, offset)
    #else
    return String(format: "%-4d%-35s 0x%08lx %@ + %d", index, UInt(bitPattern: imageBuffer.baseAddress), address, demangleSymbol, offset)
    #endif
}

Swift命名重整

对于OC函数,我们无需进行命名重整。但Swift函数经过编译后的符号无法直接辨认,需要调用swift_demangle对重整过的符号进行还原。

/// https://github.com/apple/swift/blob/main/include/swift/Demangling/Demangle.h
@_silgen_name("swift_demangle")
private func get_swift_demangle(mangledName: UnsafePointer<CChar>?, mangledNameLength: UInt, outputBuffer: UnsafeMutablePointer<CChar>?, outputBufferSize: UnsafeMutablePointer<UInt>?, flags: UInt32) -> UnsafeMutablePointer<CChar>?

以上一切顺利,但在Mach_thread获取线程调用栈时失败了

从上面调用栈示意图中我们可以看到当前栈帧中FP的值存储的是上一个栈帧的FP地址。拿到本函数的FP寄存器,所指示的栈地址,出栈,就能得到调用函数的LR寄存器的值,然后就能通过dynsym动态链接表,找到对应的函数名。

一开始参考RCBacktracemach_backtrace.c实现,核心代码如下,但在运行过程中偶尔会因为获取cur__fp.pointee时发生EXC_BAD_ACCESS崩溃。

guard var cur__fp = UnsafeMutablePointer<UnsafeMutableRawPointer?>(bitPattern: UInt(__fp)) else { return i }

while i < maxSymbols  {
    guard let prev__fp = UnsafeMutablePointer<UnsafeMutableRawPointer?>(bitPattern: UInt(bitPattern: cur__fp.pointee)) else { return i }
    
    stack[i] = cur__fp.successor().pointee
    cur__fp = prev__fp
    i += 1
}

后来参考BSBacktraceLoggervm_read_overwrite实现,核心代码如下,但StackFrameEntry会丢失部分previous栈帧。

/// 栈帧结构体
struct StackFrameEntry {
    /// 前一个栈帧地址
    var previous: UnsafePointer<StackFrameEntry>?
    /// 栈帧的函数返回地址
    var return_address: UInt = 0
}
guard __fp > 0 else { return i }

var sf = StackFrameEntry() // 栈帧
var outsize: vm_size_t = 0

var sf_kret = withUnsafeMutablePointer(to: &sf) {
    return vm_read_overwrite(mach_task_self_, vm_address_t(__fp), vm_size_t(MemoryLayout<StackFrameEntry>.size), vm_address_t(bitPattern: $0), &outsize)
}
if sf_kret != KERN_SUCCESS { return i }

while i < maxSymbols {
    guard sf.return_address > 0 else { return i }
    stack[i] = UnsafeMutableRawPointer(bitPattern: sf.return_address)
    
    guard let prev__sf = sf.previous, UInt(bitPattern: prev__sf) > 0 else { return i }
    sf_kret = withUnsafeMutablePointer(to: &sf) {
        return vm_read_overwrite(mach_task_self_, vm_address_t(bitPattern: prev__sf), vm_size_t(MemoryLayout<StackFrameEntry>.size), vm_address_t(bitPattern: $0), &outsize)
    }
    if sf_kret != KERN_SUCCESS { return i }
    
    i += 1
}

最后综合了前两者,丢失栈帧有所好转,但仍然无法保证获取到完整的调用栈信息。核心代码如下。

let sf = UnsafeMutablePointer<UnsafeMutableRawPointer?>.allocate(capacity: 1) // 栈帧
defer { sf.deallocate() }

var outsize: vm_size_t = 0
let size: vm_size_t = vm_size_t(MemoryLayout.size(ofValue: sf) * 2)
    
var sf_kret = vm_read_overwrite(mach_task_self_, vm_address_t(__fp), size, vm_address_t(bitPattern: sf), &outsize)
if sf_kret != KERN_SUCCESS { return i }

while i < maxSymbols {
  guard let next_fp = sf.successor().pointee else { return i }
  stack[i] = next_fp
        
  guard let prev_fp = sf.pointee else { return i }
  sf_kret = vm_read_overwrite(mach_task_self_, vm_address_t(bitPattern: prev_fp), size, vm_address_t(bitPattern: sf), &outsize)
  if sf_kret != KERN_SUCCESS { return i }
        
  i += 1
}

最后,如果你读到本文,知道为什么以上方式无法生效的原因,或者有什么更好的方法,欢迎与我交流。

参考

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages

Morty Proxy This is a proxified and sanitized view of the page, visit original site.