代码 or 指令,浅析ARM架构下的函数的调用过程
摘要:linux程序运行的状态以及如何推导调用栈。
1、背景知识
1、ARM64寄存器介绍:

2、STP指令详解(ARMV8手册):

我们先看一下指令格式(64bit),以及指令对于寄存机执行结果的影响

类型1、STP <Xt1>, <Xt2>, [<Xn|SP>], #<imm>
将Xt1和Xt2存入Xn|SP对应的地址内存中,然后,将Xn|SP的地址变更为Xn|SP + imm偏移量的新地址
类型2、STP <Xt1>, <Xt2>, [<Xn|SP>, #<imm>]!
将Xt1和Xt2存入Xn|SP的地址自加imm对应的地址内存中,然后,将Xn|SP的地址变更为Xn|SP + imm的offset偏移量后的新地址
类型3、STP <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}]
将Xt1和Xt2存入Xn|SP的地址自加imm对应的地址内存中
手册中有三种操作码,我们只讨论程序中涉及的后两种
Pseudocode如下:
Shared decode for all encodings
integer n = UInt(Rn);
integer t = UInt(Rt);
integer t2 = UInt(Rt2);
if L:opc<0> == '01' || opc == '11' then UNDEFINED;
integer scale = 2 + UInt(opc<1>);
integer datasize = 8 << scale;
bits(64) offset = LSL(SignExtend(imm7, 64), scale);
boolean tag_checked = wback || n != 31;
Operation for all encodings
bits(64) address;
bits(datasize) data1;
bits(datasize) data2;
constant integer dbytes = datasize DIV 8;
boolean rt_unknown = FALSE;
if HaveMTEExt() then
SetNotTagCheckedInstruction(!tag_checked);
if wback && (t == n || t2 == n) && n != 31 then
Constraint c = ConstrainUnpredictable();
assert c IN {Constraint_NONE, Constraint_UNKNOWN, Constraint_UNDEF, Constraint_NOP};
case c of
when Constraint_NONE rt_unknown = FALSE; // value stored is pre-writeback
when Constraint_UNKNOWN rt_unknown = TRUE; // value stored is UNKNOWN
when Constraint_UNDEF UNDEFINED;
when Constraint_NOP EndOfInstruction();
if n == 31 then
CheckSPAlignment();
address = SP[];
else
address = X[n];
if !postindex then
address = address + offset;
if rt_unknown && t == n then
data1 = bits(datasize) UNKNOWN;
else
data1 = X[t];
if rt_unknown && t2 == n then
data2 = bits(datasize) UNKNOWN;
else
data2 = X[t2];
Mem[address, dbytes, AccType_NORMAL] = data1;
Mem[address+dbytes, dbytes, AccType_NORMAL] = data2;
if wback then
if postindex then
address = address + offset;
if n == 31 then
SP[] = address;
else
X[n] = address;
红色部分对应推栈的关键逻辑,其他汇编指令含义可自行参考armv8手册或者度娘。
2、一个例子
熟悉了上面的部分,接下来我们看一个实例:
C代码如下:

相关的几个函数反汇编如下(和推栈相关的一般只有入口两条指令):
main\f3\f4\strlen

我们通过gdb运行后,可以看到strlen地方会触发SEGFAULT,引发进程挂掉

上述通过代码编译后,没有strip,因此elf文件是带着符号的
查看运行状态(info register):关注$29、$30、SP、PC四个寄存器

一个核心的思想:CPU执行的是指令而不是C代码,函数调用和返回实际是在线程栈上面的压栈和弹栈的过程
接下来我们来看上面的调用关系在当前这个任务栈是如何玩的:

函数调用在栈中的关系(call function压栈,地址递减;return弹栈,地址递增):

以下是推栈的过程(划重点)
再回头来看之前的汇编:
main\f3\f4\strlen

从当前的sp开始,frame 0是strlen,这块没有开栈,因此上一级的调用函数仍然是x30,因此推导:frame1调用为f3

函数f3的起始入口汇编:
(gdb) x/2i f3
0x400600 <f3>: stp x29, x30, [sp,#-48]!
0x400604 <f3+4>: mov x29, sp
可以看到,f3函数开辟的栈空间为48字节,因此,倒推frame2的栈顶为当前的sp + 48字节:0xfffffffff2c0
(gdb) x/gx 0xfffffffff2c0+8
0xfffffffff2c8: 0x000000000040065c
(gdb) x/i 0x000000000040065c
0x40065c <f4+36>: mov w0, #0x0 // #0
frame2的函数为sp+8:0x000000000040065c -> <f4+36>
继续从sp = 0xfffffffff2c0倒推frame1的函数
函数f4的起始入口汇编为:
(gdb) x/2i f4
0x400638 <f4>: stp x29, x30, [sp,#-48]!
0x40063c <f4+4>: mov x29, sp
可以看到,f4函数开辟的栈空间也是为48字节,因此,倒推frame3的栈顶为当前的0xfffffffff2c0 + 48字节:0xfffffffff2f0
frame2的函数为0xfffffffff2c0 + 8:0x000000000040065c -> <f4+36>
(gdb) x/gx 0xfffffffff2f0+8
0xfffffffff2f8: 0x0000000000400684
(gdb) x/i 0x0000000000400684
0x400684 <main+28>: mov w0, #0x0 // #0
因此frame3的函数为main函数,main函数对应的栈顶为0xfffffffff320
至此推导结束(有兴趣的同学可以继续推导,可以看到libc如何拉起main的过程)
总结:
推栈的关键:
- 当前的现场
- 熟悉cpu体系架构的开栈的方式
3、实战讲解
现场有如下的core:可以看到,所有的符号找不到,加载了符号表依然不好使,解析不出来实际的调用栈
(gdb) bt
#0 0x0000ffffaeb067bc in ?? () from /lib64/libc.so.6
#1 0x0000aaaad15cf000 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
先看info register,关注x29、x30、sp、pc四个寄存器的值

推导任务栈:
先将sp内容导出:
下图实际已先将结果标出,我们下面来详细描述如何推导

pc代表当前执行的函数指令,如果当前指令未开栈,一般情况x30代表上一级的frame调用当前函数的下一条指令,查看汇编,可以反解为如下函数
(gdb) x/i 0xaaaacd3de4fc
0xaaaacd3de4fc <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)+108>: mov x27, x0
找到栈顶函数后,查看该函数的栈操作:
(gdb) x/6i PGXCNodeConnStr
0xaaaacd3de490 <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)>: sub sp, sp, #0xd0
0xaaaacd3de494 <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)+4>: stp x29, x30, [sp,#80]
0xaaaacd3de498 <PGXCNodeConnStr(char const*, int, char const*, char const*, char const*, char const*, int, char const*)+8>: add x29, sp, #0x50
可以看到,上一级的frame存在了当前的sp + 0xd0 - 0x80也就是0xfffec4cebd40 + 0xd0 - 0x80 = 0xfffec4cebd90的地方,而栈底在0xfffec4cebd40+ 0xd0 = 0xfffec4cebe10的地方

因此就找到了下一级的frame对应的栈顶和上一级的LR返回指令,反解,可以得到函数build_node_conn_str
(gdb) x/i 0x0000aaaacd414e08
0xaaaacd414e08 <build_node_conn_str(Oid, DatabasePool*)+224>: mov x21, x0
继续重复上述推导,可以看到这个函数build_node_conn_str开了176字节的栈,
(gdb) x/4i build_node_conn_str
0xaaaacd414d28 <build_node_conn_str(Oid, DatabasePool*)>: stp x29, x30, [sp,#-176]!
0xaaaacd414d2c <build_node_conn_str(Oid, DatabasePool*)+4>: mov x29, sp
因此继续用0xfffec4cebe10 + 176 = 0xfffec4cebec0

查看调用者0xfffec4cebe10+8为reload_database_pools

继续看reload_database_pools
(gdb) x/8i reload_database_pools
0xaaaacd4225e8 <reload_database_pools(PoolAgent*)>: sub sp, sp, #0x1c0
0xaaaacd4225ec <reload_database_pools(PoolAgent*)+4>: adrp x5, 0xaaaad15cf000
0xaaaacd4225f0 <reload_database_pools(PoolAgent*)+8>: adrp x3, 0xaaaacf0ed000
0xaaaacd4225f4 <reload_database_pools(PoolAgent*)+12>: adrp x4, 0xaaaaceeed000 <_ZN4llvm18ConvertUTF8toUTF16EPPKhS1_PPtS3_NS_15ConversionFlagsE>
0xaaaacd4225f8 <reload_database_pools(PoolAgent*)+16>: add x3, x3, #0x9e0
0xaaaacd4225fc <reload_database_pools(PoolAgent*)+20>: adrp x1, 0xaaaacf0ee000 <_ZZ25PoolManagerGetConnectionsP4ListS0_E8__func__+24>
0xaaaacd422600 <reload_database_pools(PoolAgent*)+24>: stp x29, x30, [sp,#-96]!
实际开栈0x220字节,因此这一层frame的栈底为0xfffec4cebec0 + 0x220 = 0xfffec4cec0e0

因此得到基本的调用关系的结构如下

以上基本可以够用来分析问题了,因此不需要再继续推导
TIPS:arm架构下一般调用都会使用这种指令,
stp x29, x30, [sp,#immediate]! 有叹号或者无叹号
因此在每一层的frame都保存了上一层frame的栈顶地址和LR指令,通过准确找到底层的frame 0栈顶后,就可以快速推导出所有的调用关系(红色虚线圈出来的部分),函数的反解依赖符号表,只要原始的elf文件的symbol段没有strip掉,是都可以找到对应的函数符号(通过readelf -S查看即可)

找到Frame后,每一层frame里面的内容,结合汇编基本就可以用来推导过程变量了。
本文分享自华为云社区《代码 or 指令,浅析ARM架构下的函数的调用过程》,原文作者:K______。
代码 or 指令,浅析ARM架构下的函数的调用过程的更多相关文章
- ARM架构下的Docker环境,OpenJDK官方没有8版本镜像,如何完美解决?
为什么需要ARM架构下的OpenJDK8的Docker镜像? 对现有的Java应用,之前一直运行在x86处理器环境下,编译和运行都是JDK8,如今在树莓派的Docker环境运行(或者其他ARM架构电脑 ...
- Arm架构下VUE环境的安装
最近因为项目需要在arm环境下搭建vue环境,网上有基于Linux的 教程,路径略有不同,现整理如下 1.安装文件下载 1.下载地址:http://nodejs.cn/download/ 2.选择一个 ...
- c++中六种构造函数的实现以及9中情况下,构造函数的调用过程
六种构造函数的实现代码例如以下: #include<iostream> using namespace std; //c++中六种默认的构造函数 class Test { public: ...
- MFC浅析(7) CWnd类虚函数的调用时机、缺省实现
CWnd类虚函数的调用时机.缺省实现 FMD(http://www.fmdstudio.net) 1. Create 2. PreCreateWindow 3. PreSubclassWindow 4 ...
- ARM架构下linux设备树加载的方法
引入设备树后bootloader加载DTB方法: 1. 标准方法 将linux kernel放到内存地址为<kernel img addr>的内存中. 将DTB放到地址为<dtb a ...
- NanoPi arm架构下的程序 ./ 运行黑屏 Qt环境可运行
首先之所以QtCreator环境下可直接运行,但是在终端下 ./ 则不能运行(黑屏但是不报错),判断肯定不是程序或者是库的问题.于是猜想是环境问题,即终端环境与QtCreator环境不同. 然后就查看 ...
- <一>从指令角度了解函数堆栈调用过程
代码 点击查看代码 #include <iostream> using namespace std; int sum(int a,int b){ int temp=0; temp= a + ...
- AngularJs中ng-controller下的函数在调用时为什么会执行两次?
最近在学习AngularJs的过程中,自己做了个demo,但程序运行后却发现有个地方运行不对劲,纠结了半天,也问了,也查了,但是没有一个满意的答案,所以特地贴出来,请教各位大神(先说声谢谢了!).为了 ...
- 给 Arm 生态添把火,腾讯 Kona JDK Arm 架构优化实践
前言 Arm 架构以其兼具性能与功耗的特点,在智能终端以及嵌入式领域得到了广泛的使用,不断扩大其影响力.而在 PC 端以及数据中心,之前往往是 x86 架构在其中发挥着主要的作用.最近,随着人工智能. ...
随机推荐
- Centos上配置nginx+uwsgi+负载均衡配置
负载均衡在服务端开发中算是一个比较重要的特性.因为Nginx除了作为常规的Web服务器外,还会被大规模的用于反向代理后端,Nginx的异步框架可以处理很大的并发请求,把这些并发请求hold住之后就可以 ...
- NET 5 收发邮件之 MailKit
大家都用过SmtpClient来处理发送邮件的操作,不过这个类以及被标记已过时,所以介绍一个微软推荐的库MailKit来处理. MailKit开源地址:https://github.com/jsted ...
- NET 5 MemoryCache与Redis使用以及StackExchange.Redis和CSRedisCore
简介以及区别 ASP.NET Core 缓存Caching,.NET Core 中为我们提供了Caching 的组件. 目前Caching 组件提供了三种存储方式. Memory Redis SqlS ...
- [EF] - Entity Framework 6处理User Defined Function(UDF SQL Server)
随着EF5的发布,新增了对数据库(SQL Server) UDF的支持,具体可以看以下的连接:https://msdn.microsoft.com/en-us/data/hh859577.aspx,新 ...
- 10分钟带你入门git到github
git的产生背景 开局先来一个故事吧,故事看完如果不想看枯燥无味的指令,没关系我已经把这篇文章的内容录制成了一个视频,点击文末阅读原文就可以观看.或者说你已经熟练掌握git的使用了,可以直接跳到总结部 ...
- 无延时去斗按键实现方法(不好CPU)
这一灵感来源于定时器计数的方法,最后可以实现的效果跟咱们电脑键盘按键的效果一样!我先来介绍下基本原理吧! 采用定时器中断的方法,比如定时器终端我们设置为5ms,我们需要按键按下超过40ms时才算有按键 ...
- java数组基础知识
数组的定义:int[] array=new array[n];int array[]={, , , ,};定义了数组,JVM就会给其一个空间,数组是应用类型的数据类型,其存储方式是随机存储. 数组的遍 ...
- java.io.NotSerializableException: org.apache.kafka.clients.consumer.ConsumerRecord
kafka 与spark集成 序列化问题 sparkConf.set("spark.serializer", "org.apache.spark.serializer.K ...
- 风炫安全WEB安全学习第十九节课 XSS的漏洞基础知识和原理讲解
风炫安全WEB安全学习第十九节课 XSS的漏洞基础知识和原理讲解 跨站脚本攻击(Cross-site scripting,通常简称为XSS) 反射型XSS原理与演示 交互的数据不会存储在数据库里,一次 ...
- WixVersionControl Wix项目版本控制
原文链接:https://www.swack.cn/wiki/001565675133949eff0d3d5a51f48288cf6d8248905e28f000/001569821278313e6b ...