谈谈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.…

Ireport制作过程

Ireport制作过程 1、首先要到Option下设置一下ClassPath添加文件夹 2、到预览->报表字段设置一下将要用到的字段 3、到编辑->查询报表->写sql语句&#xff0c;然后把语句查询的字段结果与上面设置的报表字段的名要对应上 4、Option->选项->Compiler设置一下…

2018.09.16 loj#10243. 移棋子游戏(博弈论)

传送门 题目中已经给好了sg图&#xff0c;直接在上面跑出sg函数即可。 最后看给定点的sg值异或和是否等于0就判好了。 代码&#xff1a; #include<bits/stdc.h> #define N 2005 #define M 6005 using namespace std; int n,m,k,sg[N],first[N],First[N],du[N],cnt0,an…

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;是…

实例演示oracle注入获取cmdshell的全过程

以下的演示都是在web上的sql plus执行的&#xff0c;在web注入时 把select SYS.DBMS_EXPORT_EXTENSION.....改成   /xxx.jsp?id1 and 1<>a||(select SYS.DBMS_EXPORT_EXTENSION.....)   的形式即可。(用" a|| "是为了让语句返回true值)   语句有点长…

html视频位置控制器,html5中返回音视频的当前媒体控制器的属性controller

实例检测该视频是否有媒体控制器&#xff1a;myViddocument.getElementById("video1");alert("Controller: " myVid.controller);定义和用法controller 属性返回音视频的当前媒体控制器。默认地&#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…

注入代码oracle

--建立类 select SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES(FOO,BAR,DBMS_OUTPUT".PUT(:P1);EXECUTE IMMEDIATE DECLARE PRAGMA AUTONOMOUS_TRANSACTION;BEGIN EXECUTE IMMEDIATE  create or replace and compile java source named "LinxUtil" as …

html5包含inc文件,HTML中include file标签的用法

参数PathType将 FileName 的路径类型。路径可为以下某种类型&#xff1a;路径类型 含义文件 该文件名是带有 #include 命令的文档所在目录的相对路径。被包含文件可位于相同目录或子目录中&#xff1b;但它不能处于带有 #include 命令的页的上层目录中。虚拟 文件名为 Web 站点…

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…

springboot微服务 java b2b2c电子商务系统(一)服务的注册与发现(Eureka)

一、spring cloud简介spring cloud 为开发人员提供了快速构建分布式系统的一些工具&#xff0c;包括配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等。它运行环境简单&#xff0c;可以在开发人员的电脑上跑。Spring Cloud大型企业分布式…

linux部署服务器常用命令

fdisk -l 查分区硬盘 df -h 查空间硬盘 cd / 进目录 ls/ll 文件列表 vi tt.txt iinsert 插入 shift: 进命令行 wq 保存%退出 cat tt.txt 内容查看 pwd 当期目录信息 mkdir tt建目录 cp tt.txt tt/11.txt 拷贝文件到tt下 mv 11.txt /usr/ 移动 rm -rf tt.txt 删除不提示 rm t…

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

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

css清除浮动float的七种常用方法总结和兼容性处理

在清除浮动前我们要了解两个重要的定义&#xff1a; 浮动的定义&#xff1a;使元素脱离文档流&#xff0c;按照指定方向发生移动&#xff0c;遇到父级边界或者相邻的浮动元素停了下来。 高度塌陷&#xff1a;浮动元素父元素高度自适应&#xff08;父元素不写高度时&#xff0c;…

数据迁移测试_自动化数据迁移测试

数据迁移测试Data migrations are notoriously difficult to test. They take a long time to run on large datasets. They often involve heavy, inflexible database engines. And they’re only meant to run once, so people think it’s throw-away code, and therefore …

使用while和FOR循环分布打印字符串S='asdfer' 中的每一个元素

方法1&#xff1a; s asdfer for i in s :print(i)方法2:index 0 while 1:print(s[index])index1if index len(s):break 转载于:https://www.cnblogs.com/yuhoucaihong/p/10275800.html