谈谈iOS获取调用链

本文由云+社区发表

iOS开发过程中难免会遇到卡顿等性能问题或者死锁之类的问题,此时如果有调用堆栈将对解决问题很有帮助。那么在应用中如何来实时获取函数的调用堆栈呢?本文参考了网上的一些博文,讲述了使用mach thread的方式来获取调用栈的步骤,其中会同步讲述到栈帧的基本概念,并且通过对一个demo的汇编代码的讲解来方便理解获取调用链的原理。

一、栈帧等几个概念

先抛出一个栈帧的概念,解释下什么是栈帧。

应用中新创建的每个线程都有专用的栈空间,栈可以在线程期间自由使用。而线程中有千千万万的函数调用,这些函数共享进程的这个栈空间,那么问题就来了,函数运行过程中会有非常多的入栈出栈的过程,当函数返回backtrace的时候怎样能精确定位到返回地址呢?还有子函数所保存的一些寄存器的内容?这样就有了栈帧的概念,即每个函数所使用的栈空间是一个栈帧,所有的栈帧就组成了这个线程完整的栈

img栈帧

下面再抛出几个概念:

寄存器中的fp,sp,lr,pc

寄存器是和CPU联系非常紧密的一小块内存,经常用于存储一些正在使用的数据。对于32位架构armv7指令集的ARM处理器有16个寄存器,从r0到r15,每一个都是32位比特。调用约定指定他们其中的一些寄存器有特殊的用途,例如:

  • r0-r3:用于存放传递给函数的参数;
  • r4-r11:用于存放函数的本地参数;
  • r11:通常用作桢指针fp(frame pointer寄存器),栈帧基址寄存器,指向当前函数栈帧的栈底,它提供了一种追溯程序的方式,来反向跟踪调用的函数。
  • r12:是内部程序调用暂时寄存器。这个寄存器很特别是因为可以通过函数调用来改变它;
  • r13:栈指针sp(stack pointer)。在计算机科学内栈是非常重要的术语。寄存器存放了一个指向栈顶的指针。看这里了解更多关于栈的信息;
  • r14:是链接寄存器lr(link register)。它保存了当目前函数返回时下一个函数的地址;
  • r15:是程序计数器pc(program counter)。它存放了当前执行指令的地址。在每个指令执行完成后会自动增加;

不同指令集的寄存器数量可能会不同,pc、lr、sp、fp也可能使用其中不同的寄存器。后面我们先忽略r11等寄存器编号,直接用fp,sp,lr来讲述

如下图所示,不管是较早的帧,还是调用者的帧,还是当前帧,它们的结构是完全一样的,因为每个帧都是基于一个函数,帧伴随着函数的生命周期一起产生、发展和消亡。在这个过程中用到了上面说的寄存器,fp帧指针,它总是指向当前帧的底部;sp栈指针,它总是指向当前帧的顶部。这两个寄存器用来定位当前帧中的所有空间。编译器需要根据指令集的规则小心翼翼地调整这两个寄存器的值,一旦出错,参数传递、函数返回都可能出现问题。

其实这里这几个寄存器会满足一定规则,比如:

  • fp指向的是当面栈帧的底部,该地址存的值是调用当前栈帧的上一个栈帧的fp的地址。
  • lr总是在上一个栈帧(也就是调用当前栈帧的栈帧)的顶部,而栈帧之间是连续存储的,所以lr也就是当前栈帧底部的上一个地址,以此类推就可以推出所有函数的调用顺序。这里注意,栈底在高地址,栈向下增长

而由此我们可以进一步想到,通过sp和fp所指出的栈帧可以恢复出母函数的栈帧,不断递归恢复便恢复除了调用堆栈。向下面代码一样,每次递归pc存储的*(fp + 1)其实就是返回的地址,它在调用者的函数内,利用这个地址我们可以通过符号表还原出对应的方法名称。

while(fp) {pc = *(fp + 1);fp = *fp;
}

二、汇编解释下

如果你非要问为什么会这样,我们可以从汇编角度看下函数是怎么调用的,从而更深刻理解为什么fp总是存储了上一个栈帧的fp的地址,而fp向前一个地址为什么总是lr?

写如下一个demo程序,由于我是在mac上做实验,所以直接使用clang来编译出可执行程序,然后再用hopper工具反汇编查看汇编代码,当然也可直接使用clang的

-S参数指定生产汇编代码。

demo源码

#import <Foundation/Foundation.h>int func(int a);int main (void)
{int a = 1;func(a);return 0;
}int func (int a)
{int b = 2;return a + b;
}

汇编语言

        ; ================ B E G I N N I N G   O F   P R O C E D U R E ================; Variables:;    var_4: -4;    var_8: -8;    var_C: -12_main:
0000000100000f70         push       rbp
0000000100000f71         mov        rbp, rsp
0000000100000f74         sub        rsp, 0x10
0000000100000f78         mov        dword [rbp+var_4], 0x0
0000000100000f7f         mov        dword [rbp+var_8], 0x1
0000000100000f86         mov        edi, dword [rbp+var_8]                      ; argument #1 for method _func
0000000100000f89         call       _func
0000000100000f8e         xor        edi, edi
0000000100000f90         mov        dword [rbp+var_C], eax
0000000100000f93         mov        eax, edi
0000000100000f95         add        rsp, 0x10
0000000100000f99         pop        rbp
0000000100000f9a         ret; endp
0000000100000f9b         nop        dword [rax+rax]; ================ B E G I N N I N G   O F   P R O C E D U R E ================; Variables:;    var_4: -4;    var_8: -8_func:
0000000100000fa0         push       rbp                                         ; CODE XREF=_main+25
0000000100000fa1         mov        rbp, rsp
0000000100000fa4         mov        dword [rbp+var_4], edi
0000000100000fa7         mov        dword [rbp+var_8], 0x2
0000000100000fae         mov        edi, dword [rbp+var_4]
0000000100000fb1         add        edi, dword [rbp+var_8]
0000000100000fb4         mov        eax, edi
0000000100000fb6         pop        rbp
0000000100000fb7         ret

需要注意,由于是在mac上编译出可执行程序,指令集已经是x86-64,所以上文的fp、sp、lr、pc名称和使用的寄存器发生了变化,但含义基本一致,对应关系如下:

  • fp----rbp
  • sp----rsp
  • pc----rip

接下来我们看下具体的汇编代码,可以看到在main函数中在经过预处理和参数初始化后,通过call _func来调用了func函数,这里call _func其实等价于两个汇编命令:

Pushl %rip //保存下一条指令(第41行的代码地址)的地址,用于函数返回继续执行
Jmp _func //跳转到函数foo

于是,当main函数调用了func函数后,会将下一行地址push进栈,至此,main函数的栈帧已经结束,然后跳转到func的代码处开始继续执行。可以看出,rip指向的函数下一条地址,即上文中所说的lr已经入栈,在栈帧的顶部。

而从func的代码可以看到,首先使用push rbp将帧指针保存起来,而由于刚跳转到func函数,此时rbp其实是上一个栈帧的帧指针,即它的值其实还是上一个栈帧的底部地址,所以此步骤其实是将上一个帧底部地址保存了下来。

下一句汇编语句mov rbp, rsp将栈顶部地址rsp更新给了rbp,于是此时rbp的值就成了栈的顶部地址,也是当前栈帧的开始,即fp。而栈顶部又正好是刚刚push进去的存储上一个帧指针地址的地址,所以rbp指向的时当前栈帧的底部,但其中保存的值是上一个栈帧底部的地址。

至此,也就解释了为什么fp指向的地址存储的内容是上一个栈帧的fp的地址,也解释了为什么fp向前一个地址就正好是lr。

另外一个比较重要的东西就是出入栈的顺序,在ARM指令系统中是地址递减栈,入栈操作的参数入栈顺序是从右到左依次入栈,而参数的出栈顺序则是从左到右的你操作。包括push/pop和LDMFD/STMFD等。

三、获取调用栈步骤

其实上面的几个fp、lr、sp在mach内核提供的api中都有定义,我们可以使用对应的api拿到对应的值。如下便是64位和32位的定义

_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 */
};
_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 */
};

于是,我们只要拿到对应的fp和lr,然后递归去查找母函数的地址,最后将其符号化,即可还原出调用栈。

总结归纳了下,获取调用栈需要下面几步:

1、挂起线程

thread_suspend(main_thread);

2、获取当前线程状态上下文thread_get_state

_STRUCT_MCONTEXT ctx;#if defined(__x86_64__)mach_msg_type_number_t count = x86_THREAD_STATE64_COUNT;thread_get_state(thread, x86_THREAD_STATE64, (thread_state_t)&ctx.__ss, &count);#elif defined(__arm64__)_STRUCT_MCONTEXT ctx;mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT;thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&ctx.__ss, &count);#endif

3、获取当前帧的帧指针fp

#if defined(__x86_64__)uint64_t pc = ctx.__ss.__rip;uint64_t sp = ctx.__ss.__rsp;uint64_t fp = ctx.__ss.__rbp;
#elif defined(__arm64__)uint64_t pc = ctx.__ss.__pc;uint64_t sp = ctx.__ss.__sp;uint64_t fp = ctx.__ss.__fp;
#endif

4、递归遍历fp和lr,依次记录lr的地址

while(fp) {pc = *(fp + 1);fp = *fp;
}

这一步我们其实就是使用上面的方法来依次迭代出调用链上的函数地址,代码如下

void* t_fp[2];vm_size_t len = sizeof(record);
vm_read_overwrite(mach_task_self(), (vm_address_t)(fp),len, (vm_address_t)t_fp, &len);do {pc = (long)t_fp[1]  // lr总是在fp的上一个地址// 依次记录pc的值,这里先只是打印出来printf(pc)vm_read_overwrite(mach_task_self(),(vm_address_t)m_cursor.fp[0], len, (vm_address_t)m_cursor.fp,&len);} while (fp);

上面代码便会从下到上依次打印出调用栈函数中的地址,这个地址总是在函数调用地方的下一个地址,我们就需要拿这个地址还原出对应的符号名称。

5、恢复线程thread_resume

thread_resume(main_thread);

6、还原符号表

这一步主要是将已经获得的调用链上的地址分别解析出对应的符号。主要是参考了运行时获取函数调用栈 的方法,其中用到的dyld链接mach-o文件的基础知识,后续会专门针对这里总结一篇文章。

enumerateSegment(header, [&](struct load_command *command) {if (command->cmd == LC_SYMTAB) {struct symtab_command *symCmd = (struct symtab_command *)command;uint64_t baseaddr = 0;enumerateSegment(header, [&](struct load_command *command) {if (command->cmd == LC_SEGMENT_64) {struct segment_command_64 *segCmd = (struct segment_command_64 *)command;if (strcmp(segCmd->segname, SEG_LINKEDIT) == 0) {baseaddr = segCmd->vmaddr - segCmd->fileoff;return true;}}return false;});if (baseaddr == 0) return false;nlist_64 *nlist = (nlist_64 *)(baseaddr + slide + symCmd->symoff);uint64_t strTable = baseaddr + slide + symCmd->stroff;uint64_t offset = UINT64_MAX;int best = -1;for (int k = 0; k < symCmd->nsyms; k++) {nlist_64 &sym = nlist[k];uint64_t d = pcSlide - sym.n_value;if (offset >= d) {offset = d;best = k;}}if (best >= 0) {nlist_64 &sym = nlist[best];std::cout << "SYMBOL: " << (char *)(strTable + sym.n_un.n_strx) << std::endl;}return true;}return false;
});

参考

函数调用栈空间以及fp寄存器

函数调用栈

也谈栈和栈帧

运行时获取函数调用栈

深入解析Mac OS X & iOS 操作系统 学习笔记

此文已由作者授权腾讯云+社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/388497.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python 移动平均线_Python中的移动平均线

python 移动平均线There are situations, particularly when dealing with real-time data, when a conventional average is of little use because it includes old values which are no longer relevant and merely give a misleading impression of the current situation.…

html5字体的格式转换,font字体

路由器之家网今天精心准备的是《font字体》&#xff0c;下面是详解&#xff01;html中的标签是什么意思HTML提供了文本样式标记&#xff0c;用来控制网页中文本的字体、字号和颜色&#xff0c;多种多样的文字效果可以使网页变得更加绚丽。其基本语法格式&#xff1a;文本内容fa…

红星美凯龙牵手新潮传媒抢夺社区消费市场

瞄准线下流量红利&#xff0c;红星美凯龙牵手新潮传媒抢夺社区消费市场 中新网1月14日电 2019年1月13日&#xff0c;红星美凯龙和新潮传媒战略合作发布会在北京召开&#xff0c;双方宣布建立全面的战略合作伙伴关系。未来&#xff0c;新潮传媒的梯媒产品将入驻红星美凯龙的全国…

机器学习 啤酒数据集_啤酒数据集上的神经网络

机器学习 啤酒数据集Artificial neural networks (ANNs), usually simply called neural networks (NNs), are computing systems vaguely inspired by the biological neural networks that constitute animal brains.人工神经网络(ANN)通常简称为神经网络(NNs)&#xff0c;是…

ER TO SQL语句

ER TO SQL语句的转换&#xff0c;在数据库设计生命周期的位置如下所示。 一、转换的类别 从ER图转化得到关系数据库中的SQL表&#xff0c;一般可分为3类&#xff1a; 1&#xff09;转化得到的SQL表与原始实体包含相同信息内容。该类转化一般适用于&#xff1a; 二元“多对多”关…

dede 5.7 任意用户重置密码前台

返回了重置的链接&#xff0c;还要把&amp删除了&#xff0c;就可以重置密码了 结果只能改test的密码&#xff0c;进去过后&#xff0c;这个居然是admin的密码&#xff0c;有点头大&#xff0c;感觉这样就没有意思了 我是直接上传的一句话&#xff0c;用菜刀连才有乐趣 OK了…

nasa数据库cm1数据集_获取下一个地理项目的NASA数据

nasa数据库cm1数据集NASA provides an extensive library of data points that they’ve captured over the years from their satellites. These datasets include temperature, precipitation and more. NASA hosts this data on a website where you can search and grab in…

r语言处理数据集编码_在强调编码语言或工具之前,请学习这3个基本数据概念

r语言处理数据集编码重点 (Top highlight)I got an Instagram DM the other day that really got me thinking. This person explained that they were a data analyst by trade, and had years of experience. But, they also said that they felt that their technical skill…

HTML和CSS面试问题总结,html和css面试总结

html和cssw3c 规范结构化标准语言样式标准语言行为标准语言1) 盒模型常见的盒模型有w3c盒模型(又名标准盒模型)box-sizing:content-box和IE盒模型(又名怪异盒模型)box-sizing:border-box。标准盒子模型&#xff1a;宽度内容的宽度(content) border padding margin低版本IE盒子…

山师计算机专业研究生怎么样,山东师范大学有计算机专业硕士吗?

山东师范大学位于山东省济南市&#xff0c;学校是一所综合性高等师范院校。该院校深受广大报考专业硕士学员的欢迎&#xff0c;因此很多学员想要知道山东师范大学有没有计算机专业硕士&#xff1f;山东师范大学是有计算机专业硕士的。下面就和大家介绍一下培养目标有哪些&#…

使用TensorFlow概率预测航空乘客人数

TensorFlow Probability uses structural time series models to conduct time series forecasting. In particular, this library allows for a “scenario analysis” form of modelling — whereby various forecasts regarding the future are made.TensorFlow概率使用结构…

python画激活函数图像

导入必要的库 import math import matplotlib.pyplot as plt import numpy as np import matplotlib as mpl mpl.rcParams[axes.unicode_minus] False 绘制softmax函数图像 fig plt.figure(figsize(6,4)) ax fig.add_subplot(111) x np.linspace(-10,10) y sigmoid(x)ax.s…

pdf.js插件使用记录,在线打开pdf

pdf.js插件使用记录&#xff0c;在线打开pdf 原文:pdf.js插件使用记录&#xff0c;在线打开pdf天记录一个js库&#xff1a;pdf.js。主要是实现在线打开pdf功能。因为项目需求需要能在线查看pdf文档&#xff0c;所以就研究了一下这个控件。 有些人很好奇&#xff0c;在线打开pdf…

程序员 sql面试_非程序员SQL使用指南

程序员 sql面试Today, the word of the moment is DATA, this little combination of 4 letters is transforming how all companies and their employees work, but most people don’t really know how data behaves or how to access it and they also think that this is j…

r a/b 测试_R中的A / B测试

r a/b 测试什么是A / B测试&#xff1f; (What is A/B Testing?) A/B testing is a method used to test whether the response rate is different for two variants of the same feature. For instance, you may want to test whether a specific change to your website lik…

Java基础回顾

内容&#xff1a; 1、Java中的数据类型 2、引用类型的使用 3、IO流及读写文件 4、对象的内存图 5、this的作用及本质 6、匿名对象 1、Java中的数据类型 Java中的数据类型有如下两种&#xff1a; 基本数据类型: 4类8种 byte(1) boolean(1) short(2) char(2) int(4) float(4) l…

计算机部分应用显示模糊,win10系统打开部分软件字体总显示模糊的解决方法-电脑自学网...

win10系统打开部分软件字体总显示模糊的解决方法。方法一&#xff1a;win10软件字体模糊1、首先&#xff0c;在Win10的桌面点击鼠标右键&#xff0c;选择“显示设置”。2、在“显示设置”的界面下方&#xff0c;点击“高级显示设置”。3、在“高级显示设置”的界面中&#xff0…

Tomcat调节

Tomcat默认可以使用的内存为128MB&#xff0c;在较大型的应用项目中&#xff0c;这点内存是不够的&#xff0c;需要调大,并且Tomcat本身不能直接在计算机上运行&#xff0c;需要依赖于硬件基础之上的操作系统和一个java虚拟机。 AD&#xff1a; 这里向大家描述一下如何使用Tom…

turtle 20秒画完小猪佩奇“社会人”

转载&#xff1a;https://blog.csdn.net/csdnsevenn/article/details/80650456 图片源自网络 作者 丁彦军 如需转载&#xff0c;请联系原作者授权。 今年社交平台上最火的带货女王是谁&#xff1f;范冰冰&#xff1f;杨幂&#xff1f;Angelababy&#xff1f;不&#xff0c;是猪…

最佳子集aic选择_AutoML的起源:最佳子集选择

最佳子集aic选择As there is a lot of buzz about AutoML, I decided to write about the original AutoML; step-wise regression and best subset selection. Then I decided to ignore step-wise regression because it is bad and should probably stop being taught. That…