iOS中线程Call Stack的捕获和解析(一)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介:

http://blog.csdn.net/jasonblog/article/details/49909209这里对上个月做的一个技术项目做部分技术小结,这篇文章描述的功能和我们在使用Xcode进行调试时点击暂停的效果类似。

一、获取任意一个线程的Call Stack

如果要获取当前线程的调用栈,可以直接使用现有API:[NSThread callStackSymbols]

但是并没有相关API支持获取任意线程的调用栈,所以只能自己编码实现。

1. 基础结构

一个线程的调用栈是什么样的呢?

我的理解是应该包含当前线程的执行地址,并且从这个地址可以一级一级回溯到线程的入口地址,这样就反向构成了一条链:线程入口执行某个方法,然后逐级嵌套调用到当前现场。

Call_stack_layout_svg(图片来源于维基百科)

如图所示,每一级的方法调用,都对应了一张活动记录,也称为活动帧。也就是说,调用栈是由一张张帧结构组成的,可以称之为栈帧。

我们可以看到,一张栈帧结构中包含着Return Address,也就是当前活动记录执行结束后要返回的地址(展开)。

那么,在我们获取到栈帧后,就可以通过返回地址来进行回溯了。

2. 指令指针和基址指针

我们明确了两个目标:(1)当前执行的指令,(2)当前栈帧结构。

以x86为例,寄存器用途如下:

SP/ESP/RSP: Stack pointer for top address of the stack.
BP/EBP/RBP: Stack base pointer for holding the address of the current stack frame.
IP/EIP/RIP: Instruction pointer. Holds the program counter, the current instruction address.

可以看到,我们可以通过指令指针来获取当前指令地址,以及通过栈基址指针获取当前栈帧地址。

那么问题来了,我们怎么获取到相关寄存器呢?

3. 线程执行状态

考虑到一个线程被挂起时,后续继续执行需要恢复现场,所以在挂起时相关现场需要被保存起来,比如当前执行到哪条指令了。

那么就要有相关的结构体来为线程保存运行时的状态,经过一番查阅,得到如下信息:

The function thread_get_state returns the execution state (e.g. the machine registers) of target_thread as specified by flavor.

Function - Return the execution state for a thread.

SYNOPSIS

kern_return_t   thread_get_state
                (thread_act_t                     target_thread,
                 thread_state_flavor_t                   flavor,
                 thread_state_t                       old_state,
                 mach_msg_type_number_t         old_state_count);
/*
 * THREAD_STATE_FLAVOR_LIST 0
 *  these are the supported flavors
 */
#define x86_THREAD_STATE32      1
#define x86_FLOAT_STATE32       2
#define x86_EXCEPTION_STATE32       3
#define x86_THREAD_STATE64      4
#define x86_FLOAT_STATE64       5
#define x86_EXCEPTION_STATE64       6
#define x86_THREAD_STATE        7
#define x86_FLOAT_STATE         8
#define x86_EXCEPTION_STATE     9
#define x86_DEBUG_STATE32       10
#define x86_DEBUG_STATE64       11
#define x86_DEBUG_STATE         12
#define THREAD_STATE_NONE       13
/* 14 and 15 are used for the internal x86_SAVED_STATE flavours */
#define x86_AVX_STATE32         16
#define x86_AVX_STATE64         17
#define x86_AVX_STATE           18

所以我们可以通过这个API搭配相关参数来获得想要的寄存器信息:

bool jdy_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
    mach_msg_type_number_t state_count = x86_THREAD_STATE64_COUNT;
    kern_return_t kr = thread_get_state(thread, x86_THREAD_STATE64, (thread_state_t)&machineContext->__ss, &state_count);
    return (kr == KERN_SUCCESS);
}

这里引入了一个结构体叫_STRUCT_MCONTEXT

4. 不同平台的寄存器

_STRUCT_MCONTEXT在不同平台上的结构不同:

x86_64,如iPhone 6模拟器:

_STRUCT_MCONTEXT64
{
    _STRUCT_X86_EXCEPTION_STATE64   __es;
    _STRUCT_X86_THREAD_STATE64  __ss;
    _STRUCT_X86_FLOAT_STATE64   __fs;
};

_STRUCT_X86_THREAD_STATE64
{
    __uint64_t  __rax;
    __uint64_t  __rbx;
    __uint64_t  __rcx;
    __uint64_t  __rdx;
    __uint64_t  __rdi;
    __uint64_t  __rsi;
    __uint64_t  __rbp;
    __uint64_t  __rsp;
    __uint64_t  __r8;
    __uint64_t  __r9;
    __uint64_t  __r10;
    __uint64_t  __r11;
    __uint64_t  __r12;
    __uint64_t  __r13;
    __uint64_t  __r14;
    __uint64_t  __r15;
    __uint64_t  __rip;
    __uint64_t  __rflags;
    __uint64_t  __cs;
    __uint64_t  __fs;
    __uint64_t  __gs;
};

x86_32,如iPhone 4s模拟器:

_STRUCT_MCONTEXT32
{
    _STRUCT_X86_EXCEPTION_STATE32   __es;
    _STRUCT_X86_THREAD_STATE32  __ss;
    _STRUCT_X86_FLOAT_STATE32   __fs;
};

_STRUCT_X86_THREAD_STATE32
{
    unsigned int    __eax;
    unsigned int    __ebx;
    unsigned int    __ecx;
    unsigned int    __edx;
    unsigned int    __edi;
    unsigned int    __esi;
    unsigned int    __ebp;
    unsigned int    __esp;
    unsigned int    __ss;
    unsigned int    __eflags;
    unsigned int    __eip;
    unsigned int    __cs;
    unsigned int    __ds;
    unsigned int    __es;
    unsigned int    __fs;
    unsigned int    __gs;
};

ARM64,如iPhone 5s:

_STRUCT_MCONTEXT64
{
    _STRUCT_ARM_EXCEPTION_STATE64   __es;
    _STRUCT_ARM_THREAD_STATE64  __ss;
    _STRUCT_ARM_NEON_STATE64    __ns;
};

_STRUCT_ARM_THREAD_STATE64
{
    __uint64_t    __x[29];  /* General purpose registers x0-x28 */
    __uint64_t    __fp;     /* Frame pointer x29 */
    __uint64_t    __lr;     /* Link register x30 */
    __uint64_t    __sp;     /* Stack pointer x31 */
    __uint64_t    __pc;     /* Program counter */
    __uint32_t    __cpsr;   /* Current program status register */
    __uint32_t    __pad;    /* Same size for 32-bit or 64-bit clients */
};

ARMv7/v6,如iPhone 4s:

_STRUCT_MCONTEXT32
{
    _STRUCT_ARM_EXCEPTION_STATE __es;
    _STRUCT_ARM_THREAD_STATE    __ss;
    _STRUCT_ARM_VFP_STATE       __fs;
};

_STRUCT_ARM_THREAD_STATE
{
    __uint32_t  __r[13];    /* General purpose register r0-r12 */
    __uint32_t  __sp;       /* Stack pointer r13 */
    __uint32_t  __lr;       /* Link register r14 */
    __uint32_t  __pc;       /* Program counter r15 */
    __uint32_t  __cpsr;     /* Current program status register */
};

可以对照《iOS ABI Function Call Guide》,其中在ARM64相关章节中描述到:

The frame pointer register (x29) must always address a valid frame record, although some functions–such as leaf functions or tail calls–may elect not to create an entry in this list. As a result, stack traces will always be meaningful, even without debug information

而在ARMv7/v6上描述到:

The function calling conventions used in the ARMv6 environment are the same as those used in the Procedure Call Standard for the ARM Architecture (release 1.07), with the following exceptions:

*The stack is 4-byte aligned at the point of function calls.
Large data types (larger than 4 bytes) are 4-byte aligned.
Register R7 is used as a frame pointer
Register R9 has special usage.*

所以,通过了解以上不同平台的寄存器结构,我们可以编写出比较通用的回溯功能。

5. 算法实现

/**
 * 关于栈帧的布局可以参考:
 * https://en.wikipedia.org/wiki/Call_stack
 * http://www.cs.cornell.edu/courses/cs412/2008sp/lectures/lec20.pdf
 * http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/
 */
typedef struct JDYStackFrame {
    const struct JDYStackFrame* const previous;
    const uintptr_t returnAddress;
} JDYStackFrame;

//

int jdy_backtraceThread(thread_t thread, uintptr_t *backtraceBuffer, int limit) {
    if (limit <= 0) return 0;

    _STRUCT_MCONTEXT mcontext;
    if (!jdy_fillThreadStateIntoMachineContext(thread, &mcontext)) {
        return 0;
    }

    int i = 0;
    uintptr_t pc = jdy_programCounterOfMachineContext(&mcontext);
    backtraceBuffer[i++] = pc;
    if (i == limit) return i;

    uintptr_t lr = jdy_linkRegisterOfMachineContext(&mcontext);
    if (lr != 0) {
        /* 由于lr保存的也是返回地址,所以在lr有效时,应该会产生重复的地址项 */
        backtraceBuffer[i++] = lr;
        if (i == limit) return i;
    }

    JDYStackFrame frame = {0};
    uintptr_t fp = jdy_framePointerOfMachineContext(&mcontext);
    if (fp == 0 || jdy_copyMemory((void *)fp, &frame, sizeof(frame)) != KERN_SUCCESS) {
        return i;
    }

    while (i < limit) {
        backtraceBuffer[i++] = frame.returnAddress;
        if (frame.returnAddress == 0
            || frame.previous == NULL
            || jdy_copyMemory((void *)frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
            break;
        }
    }

    return i;
}

如上。

二、编码实现对一个地址进行符号化解析

后续iOS中线程Call Stack的捕获和解析(二)

目录
相关文章
|
1月前
|
缓存 Java 调度
多线程编程核心:上下文切换深度解析
在现代计算机系统中,多线程编程已成为提高程序性能和响应速度的关键技术。然而,多线程编程中一个不可避免的概念就是上下文切换(Context Switching)。本文将深入探讨上下文切换的概念、原因、影响以及优化策略,帮助你在工作和学习中深入理解这一技术干货。
49 10
|
2月前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
58 12
|
1月前
|
存储 安全 数据安全/隐私保护
深入解析iOS 14隐私保护功能:用户数据安全的新里程碑
随着数字时代的到来,个人隐私保护成为全球关注的焦点。苹果公司在最新的iOS 14系统中引入了一系列创新的隐私保护功能,旨在为用户提供更透明的数据使用信息和更强的控制权。本文将深入探讨iOS 14中的几项关键隐私功能,包括App跟踪透明性、简化的隐私设置以及增强的系统安全性,分析它们如何共同作用以提升用户的隐私保护水平。
106 3
|
1月前
|
调度 开发者
核心概念解析:进程与线程的对比分析
在操作系统和计算机编程领域,进程和线程是两个基本而核心的概念。它们是程序执行和资源管理的基础,但它们之间存在显著的差异。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
60 4
|
1月前
|
算法 调度 开发者
多线程编程核心:上下文切换深度解析
在多线程编程中,上下文切换是一个至关重要的概念,它直接影响到程序的性能和响应速度。本文将深入探讨上下文切换的含义、原因、影响以及如何优化,帮助你在工作和学习中更好地理解和应用多线程技术。
43 4
|
1月前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
14天前
|
Java 程序员 调度
【JavaEE】线程创建和终止,Thread类方法,变量捕获(7000字长文)
创建线程的五种方式,Thread常见方法(守护进程.setDaemon() ,isAlive),start和run方法的区别,如何提前终止一个线程,标志位,isinterrupted,变量捕获
|
2月前
|
数据安全/隐私保护 iOS开发 开发者
iOS 14隐私保护新特性深度解析####
随着数字时代的到来,隐私保护已成为全球用户最为关注的问题之一。苹果在最新的iOS 14系统中引入了一系列创新功能,旨在增强用户的隐私和数据安全。本文将深入探讨iOS 14中的几大隐私保护新特性,包括App跟踪透明度、剪贴板访问通知和智能防追踪功能,分析这些功能如何提升用户隐私保护,并评估它们对开发者和用户体验的影响。 ####
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
93 2
|
15天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析

推荐镜像

更多