6

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

 3 years ago
source link: https://segmentfault.com/a/1190000039111210
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

1、背景知识

1、ARM64寄存器介绍:

U3yEzmV.png!mobile

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

7rmim2U.png!mobile

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

EBzINv7.png!mobile

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

vUj2qeQ.png!mobile

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

mainf3f4strlen

ZBRjee.png!mobile

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

IVNnQny.png!mobile

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

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

bYb6Rf2.png!mobile

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

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

iUVnein.png!mobile

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

fyYVFbF.png!mobile

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

再回头来看之前的汇编:

mainf3f4strlen

ZBRjee.png!mobile

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

aMJjArn.png!mobile

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

IbmYnyB.png!mobile

推导任务栈:

先将sp内容导出:

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

qYzyQrE.png!mobile

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

BjMZfiQ.png!mobile

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

FnEN73f.png!mobile

查看调用者0xfffec4cebe10+8为reload_database_pools

AJzeiye.png!mobile

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

IbuE7nb.png!mobile

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

QNBZz2u.png!mobile

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

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

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

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

a6jeym7.png!mobile

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK