摘要: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______。
转载:https://blog.csdn.net/devcloud/article/details/113309507