Linux基础:ARM架构下的函数的调用过程
我们先看一下指令格式(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[]; address = X[n]; if !postindex then address = address + offset; if rt_unknown t == n then data1 = bits(datasize) UNKNOWN; data1 = X[t]; if rt_unknown t2 == n then data2 = bits(datasize) UNKNOWN; 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里面的内容,结合汇编基本就可以用来推导过程变量了。
1、背景知识 1、ARM64寄存器介绍我们先看一下指令格式(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[]; address = X[n]; if !postindex then address = address + offset; if rt_unknown t == n then data1 = bits(datasize) UNKNOWN; data1 = X[t]; if rt_unknown t2 == n then data2 = bits(datasize) UNKNOWN; 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里面的内容,结合汇编基本就可以用来推导过程变量了。
以上就是Linux基础:ARM架构下的函数的调用过程的详细内容,更多关于ARM架构下的函数的调用过程的资料请关注其它相关文章!
我想要获取技术服务或软件
服务范围:MySQL、ORACLE、SQLSERVER、MongoDB、PostgreSQL 、程序问题
服务方式:远程服务、电话支持、现场服务,沟通指定方式服务
技术标签:数据恢复、安装配置、数据迁移、集群容灾、异常处理、其它问题
本站部分文章参考或来源于网络,如有侵权请联系站长。
数据库远程运维 Linux基础:ARM架构下的函数的调用过程
相关文章
- Linux驱动实现读写文件功能(linux驱动读写文件)
- Linux网络抓包技术指南(linux网卡抓包)
- 运用 ARM 架构搭建 Linux 操作系统(arm架构linux)
- Linux如何更改读写权限(linux更改读写权限)
- Linux动态链接库:让系统更加高效(linux动态库so)
- Linux下如何添加字体文件(linux添加字体文件)
- 深入了解Linux系统的CPU使用率监控(linux监控cpu使用率)
- Linux培训:掌握基础技能的必修课(linux培训教程)
- Linux开发基础指南:掌握一切必备技能(linux开发基础)
- Linux系统编程:从基础到专业,第2版(linux系统编程第2版)
- 入门马哥Linux技术:从基础入门到高级应用(马哥linux基础)
- 配置Linux下快速删除Nginx配置的技巧(linux删除nginx)
- Linux的崭新旅程:各种分支的介绍(linux的分支)
- Linux发展史:从基础分支到最新发展(linux的分支)
- 调试解决 Linux 设备驱动问题:调试迈出最重要一步(linux设备和驱动)
- Linux设备文件操作:了解基础命令,轻松掌握文件读写与权限控制(linux设备文件操作)
- Linux挂载: 了解它的奥秘吧(什么是linux挂载)
- Linux实现本地网络内IP设置(linux设置内网ip)
- Linux和Mac:各自的优势与劣势(linux 和mac)
- Linux下如何将后台进程改为前台进程(linux 前台进程)
- Linux系统中GRUB文件的重要性(linux grub文件)