乐趣区

关于函数:代码-or-指令浅析ARM架构下的函数的调用过程

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

1、背景常识

1、ARM64 寄存器介绍:

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

咱们先看一下指令格局(64bit),以及指令对于存放机执行后果的影响

类型1STP <Xt1>, <Xt2>, [<Xn|SP>], #<imm>

将 Xt1 和 Xt2 存入 Xn|SP 对应的地址内存中,而后,将 Xn|SP 的地址变更为 Xn|SP + imm 偏移量的新地址

类型2STP <Xt1>, <Xt2>, [<Xn|SP>, #<imm>]!

将 Xt1 和 Xt2 存入 Xn|SP 的地址自加 imm 对应的地址内存中,而后,将 Xn|SP 的地址变更为 Xn|SP + imm 的 offset 偏移量后的新地址

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

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

mainf3f4strlen

咱们通过 gdb 运行后,能够看到 strlen 中央会触发 SEGFAULT,引发过程挂掉

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

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

一个外围的思维:CPU执行的是指令而不是 C 代码,函数调用和返回理论是在线程栈下面的压栈和弹栈的过程

接下来咱们来看下面的调用关系在以后这个工作栈是如何玩的:

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

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

再回头来看之前的汇编:

mainf3f4strlen

从以后的 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 的起始入口汇编为:

函数 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______。

点击关注,第一工夫理解华为云陈腐技术~

退出移动版