写点什么

代码 or 指令,浅析 ARM 架构下的函数的调用过程

发布于: 2021 年 01 月 28 日

摘要:linux 程序运行的状态以及如何推导调用栈。


1、背景知识


1、ARM64 寄存器介绍:


image


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


image


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


image


类型 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 代码如下:


image


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


mainf3f4strlen


image


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


image


上述通过代码编译后,没有 strip,因此 elf 文件是带着符号的


查看运行状态(info register):关注 29、29、30、SP、PC 四个寄存器


image


一个核心的思想:CPU 执行的是指令而不是 C 代码,函数调用和返回实际是在线程栈上面的压栈和弹栈的过程


接下来我们来看上面的调用关系在当前这个任务栈是如何玩的:


image


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


image


以下是推栈的过程(划重点


再回头来看之前的汇编:


mainf3f4strlen


image


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


image


函数 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 的起始入口汇编为:


函数 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 四个寄存器的值


image


推导任务栈:


先将 sp 内容导出:


下图实际已先将结果标出,我们下面来详细描述如何推导


image


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 的地方


image


因此就找到了下一级的 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


image


查看调用者 0xfffec4cebe10+8 为 reload_database_pools


image


继续看 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


image


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


image


以上基本可以够用来分析问题了,因此不需要再继续推导


TIPS:arm 架构下一般调用都会使用这种指令,


stp x29, x30, [sp,#immediate]! 有叹号或者无叹号


因此在每一层的 frame 都保存了上一层 frame 的栈顶地址和 LR 指令,通过准确找到底层的 frame 0 栈顶后,就可以快速推导出所有的调用关系(红色虚线圈出来的部分),函数的反解依赖符号表,只要原始的 elf 文件的 symbol 段没有 strip 掉,是都可以找到对应的函数符号(通过 readelf -S 查看即可)


image


找到 Frame 后,每一层 frame 里面的内容,结合汇编基本就可以用来推导过程变量了


本文分享自华为云社区《代码 or 指令,浅析 ARM 架构下的函数的调用过程》,原文作者:K______。


点击关注,第一时间了解华为云新鲜技术~


发布于: 2021 年 01 月 28 日阅读数: 52
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
代码 or 指令,浅析ARM架构下的函数的调用过程