当前位置: 首页 > news >正文

ARM 栈和函数调用

阅读本文前,可以先阅读下述文档,对函数栈、栈帧等的概念会有所了解,会对本文章的理解大有益处
X86_64 栈和函数调用

1、调试环境

Ubuntu:

liangjie@liangjie-virtual-machine:~/Desktop$ cat /proc/version
Linux version 6.5.0-35-generic (buildd@lcy02-amd64-079) 
(x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0, 

交叉编译器使用:gcc-linaro-7.4.1-2019.02-x86_64_arm-linux-gnueabi

2、调试原码

/* proc.c */
void func1()
{
}int func2(int a, long b, char *c)
{*c = a * b;func1();return a * b;
}int main()
{char value;int rc = func2(1, 2, &value);
}

3、反汇编结果

3.1 ARM 基础

  ARM 是精简指令集(RISC:Reduced Instruction set Computing)处理器,拥有更简单的指令集(少于100个)和更多的通用寄存器。与 X86 不同,ARM 指令只操作寄存器,且只能使用 Load/Stroe(取/存) 命令来读取和写入内存。也就是说,如果增加某个地址处的 32 位数据的值,你起码需要三个指令(取,加,存):首先将该地址处的数据加载到寄存器(取),然后增加寄存器里的值(加),最后再将寄存器里的值存储到原来的地址处(存)。

ARM 在任何时候都可以看到 16 个通用寄存器,具体取决于当前的处理器模式。它们是 R0-R12、SP、LR、PC (R15)

  • R0-R12:可用于常见操作期间存储临时值、指针(内存位置)等等。例如 R0,在算术运算期间可以称为累加器,或用于存储调用的函数时返回的结果。R7 在进行系统调用时非常有用,因为它存储了系统号,R11(FP 栈底)可帮助我们跟踪作为帧指针的堆栈上的边界。此外,ARM上的函数调用约定函数的前四个参数存储在寄存器 r0-r3 中(关于栈底,不同的编译器实现可能不同,有些编译器将 R7 作为栈底,而有些则是 R11 作为栈底
  • SP(或 R13)是堆栈指针。C 和 C++ 编译器始终使用 SP 作为堆栈指针。不鼓励将 SP 用作通用寄存器。在 Thumb 中,SP 被严格定义为堆栈指针。汇编程序参考中的说明页描述了何时可以使用 SP 和 PC
  • 在用户模式下,LR(或R14)用作链接寄存器,用于在调用子程序时存储返回地址。如果返回地址存储在堆栈中,它也可以用作通用寄存器。在异常处理模式中,LR 保存异常的返回地址,或者如果在异常内执行子例程调用,则保存子例程返回地址。如果返回地址存储在堆栈中,LR 可以用作通用寄存器
  • CPSR:状态寄存器:在它下面你可以看到工作状态标志,用户模式,中断标志,溢出标志,进位标志,零标志位,符号标志。这些标志代表了CPSR寄存器中特定的位,并根据CPSR的值进行设置,如果标志位有效则会进行加粗。N、Z、C 和 V 位与 x86 上的 EFLAG 寄存器中的 SF、ZF、CF 和 OF 位相同
  • SPSR:程序保存状态寄存器(saved program status register)SPSR用于保存CPSR的状态,以便异常返回后恢复异常发生时的工作状态。当特定的异常中断发生时,这个寄存器用于存放当前程序状态寄存器的内容。在异常中断退出时,可以用 SPSR 来恢复 CPSR
    在这里插入图片描述

3.2 源码讲解

00010398 <func1>:10398:	b480      	push	{r7}1039a:	af00      	add	r7, sp, #01039c:	bf00      	nop1039e:	46bd      	mov	sp, r7103a0:	bc80      	pop	{r7}103a2:	4770      	bx	lr000103a4 <func2>:103a4:	b580      	push	{r7, lr}103a6:	b084      	sub	sp, #16103a8:	af00      	add	r7, sp, #0103aa:	60f8      	str	r0, [r7, #12]103ac:	60b9      	str	r1, [r7, #8]103ae:	607a      	str	r2, [r7, #4]103b0:	68fb      	ldr	r3, [r7, #12]103b2:	b2da      	uxtb	r2, r3103b4:	68bb      	ldr	r3, [r7, #8]103b6:	b2db      	uxtb	r3, r3103b8:	fb12 f303 	smulbb	r3, r2, r3103bc:	b2da      	uxtb	r2, r3103be:	687b      	ldr	r3, [r7, #4]103c0:	701a      	strb	r2, [r3, #0]103c2:	f7ff ffe9 	bl	10398 <func1>103c6:	68fb      	ldr	r3, [r7, #12]103c8:	68ba      	ldr	r2, [r7, #8]103ca:	fb02 f303 	mul.w	r3, r2, r3103ce:	4618      	mov	r0, r3103d0:	3710      	adds	r7, #16103d2:	46bd      	mov	sp, r7103d4:	bd80      	pop	{r7, pc}000103d6 <main>:103d6:	b580      	push	{r7, lr}103d8:	b082      	sub	sp, #8103da:	af00      	add	r7, sp, #0103dc:	1cfb      	adds	r3, r7, #3103de:	461a      	mov	r2, r3103e0:	2102      	movs	r1, #2103e2:	2001      	movs	r0, #1103e4:	f7ff ffde 	bl	103a4 <func2>103e8:	6078      	str	r0, [r7, #4]103ea:	2300      	movs	r3, #0103ec:	4618      	mov	r0, r3103ee:	3708      	adds	r7, #8103f0:	46bd      	mov	sp, r7103f2:	bd80      	pop	{r7, pc}

  r7 对应于 x86 下的 bp 寄存器,相对与 sp,r7 就是栈底,在进入一个新栈帧之后先把原来的 r7 压栈,然后 r7 保存当前 bp。Linux 下,r7 大部分情况用来保存系统调用号(syscall number)

3.2.1 func1

  func1 函数比较简单,由 func1 先讲起。 func1 是叶子函数,主要关注其函数调用关系流程。

  ARM 中的 bl 指令为相对跳转指令,在跳转之前,会先将当前指令的下一条指令地址保存到 lr 寄存器中,然后才跳转到标号执行。
  所以当进入一个函数时,如果该函数可能会修改 lr 寄存器,则 lr 需要入栈保存;如果该函数是叶子函数,则不需要保存 lr 寄存器值

103c2:	f7ff ffe9 	bl	10398 <func1>

再看函数 func1,

00010398 <func1>:10398:	b480      	push	{r7}				# r7 入栈保存值1039a:	af00      	add	r7, sp, #0				# r7 = sp + 01039c:	bf00      	nop1039e:	46bd      	mov	sp, r7					# sp = r7103a0:	bc80      	pop	{r7}					# r7 出栈103a2:	4770      	bx	lr						# 返回
  • push {r7},将 r7 压栈的,保存原来的栈底 r7
  • add r7, sp, #0,原来的栈底(r7 指向的位置)成为了新的栈顶(sp 指向的位置)
    在这里插入图片描述
  • mov sp, r7,恢复原来的栈顶
  • pop {r7},恢复原来的栈底
    在这里插入图片描述
3.2.2 func2

我们再看看 func2,func2 则主要关注函数传参以及局部变量的存储。

000103a4 <func2>:103a4:	b580      	push	{r7, lr}         	# r7、lr 入栈保存值103a6:	b084      	sub	sp, #16				 	# sp = sp - 16103a8:	af00      	add	r7, sp, #0				# r7 = sp + 0103aa:	60f8      	str	r0, [r7, #12]			# (r7 + 12) = r0103ac:	60b9      	str	r1, [r7, #8]			# (r7 + 8) = r1103ae:	607a      	str	r2, [r7, #4]			# (r7 + 4) = r2103b0:	68fb      	ldr	r3, [r7, #12]			# r3 = (r7 + 12)103b2:	b2da      	uxtb	r2, r3				# r2 = r3 低 8 位103b4:	68bb      	ldr	r3, [r7, #8]			# r3 = (r7 + 8)103b6:	b2db      	uxtb	r3, r3				# r3 = r3 低 8 位103b8:	fb12 f303 	smulbb	r3, r2, r3			# r3 = r2 * r3103bc:	b2da      	uxtb	r2, r3				# r2 = r3 低 8 位103be:	687b      	ldr	r3, [r7, #4]			# r3 = (r7 + 4)103c0:	701a      	strb	r2, [r3, #0]		# (r3 + 0) = r2 (8位)103c2:	f7ff ffe9 	bl	10398 <func1>103c6:	68fb      	ldr	r3, [r7, #12]			# r3 = (r7 + 12)103c8:	68ba      	ldr	r2, [r7, #8]			# r2 = (r7 + 8)103ca:	fb02 f303 	mul.w	r3, r2, r3			# r3 = r2 * r3 (16位乘法)103ce:	4618      	mov	r0, r3					# r0 = r3 (返回值)103d0:	3710      	adds	r7, #16				# r7 = (r7 + 16) 103d2:	46bd      	mov	sp, r7					# sp = r7103d4:	bd80      	pop	{r7, pc}				# r7 、lr 出栈,lr 出栈赋值给 pc
  • push {r7, lr},保存 lr 和 r7
  • sub sp, #16,开辟 16 字节空间,即 func2 的参数大小 (sp = sp - 16)
  • add r7, sp, #0,更新 r7 栈底(r7 = sp + 0) —— 这里,栈底 r7 其实和 sp 已经重合了
  • str r0, [r7, #12],参数入栈 —— 其实从这里也能看到,r7 栈底的功能,就是访问栈中的参数变量

到这里,函数栈情况如下图:
在这里插入图片描述

TG : func2 参数为 12 字节,但是实际开辟大小为 16 字节,这与不同编译器对栈对齐、调用约定等原因有关

其中,恢复 func1 栈顶 sp

  • adds r7, #16
  • mov sp, r7

其中,恢复 func1 栈底 r7、pc(r7 = r7,pc = lr)

  • pop {r7, pc}

恢复后栈帧如下:
在这里插入图片描述
总的函数调用关系如下图:
在这里插入图片描述

4、关于编译器版本

较老版本的 gcc 编译器,编译出的栈帧逻辑和新版本的 gcc 编译器可能不同,例如:

使用 arm-linux-gcc-4.3.2 编译器,编译出效果如下:

00008350 <func2>:8350:	e92d4800 	push	{fp, lr}8354:	e28db004 	add	fp, sp, #4	; 0x48358:	e24dd010 	sub	sp, sp, #16	; 0x10835c:	e50b0008 	str	r0, [fp, #-8]8360:	e50b100c 	str	r1, [fp, #-12]8364:	e50b2010 	str	r2, [fp, #-16]8368:	e51b2008 	ldr	r2, [fp, #-8]836c:	e51b300c 	ldr	r3, [fp, #-12]8370:	e0030392 	mul	r3, r2, r38374:	e20330ff 	and	r3, r3, #255	; 0xff8378:	e51b2010 	ldr	r2, [fp, #-16]837c:	e5c23000 	strb	r3, [r2]8380:	ebffffed 	bl	833c <func1>8384:	e51b2008 	ldr	r2, [fp, #-8]8388:	e51b300c 	ldr	r3, [fp, #-12]838c:	e0030392 	mul	r3, r2, r38390:	e1a00003 	mov	r0, r38394:	e24bd004 	sub	sp, fp, #4	; 0x48398:	e8bd8800 	pop	{fp, pc}
  1. 针对栈底寄存器,不再使用 r7,而是使用 fp 替代
  2. 栈底的功能,不再是保存新栈帧的栈顶,而是调用者栈帧的栈底 fp 的值,这样会方便栈回溯。使用 gcc-7.3 默认选项编译,GNU 说可以使用 unwind 方法回溯,这里暂时不会介绍 unwind 方法
    在这里插入图片描述

其实不管编译器版本怎么变化,关于栈帧的逻辑最终都是大同小异,只要看懂了一个,其它的都能无师自通。


http://www.mrgr.cn/news/33317.html

相关文章:

  • 本地快速部署一个简洁美观的个人Halo博客网站并发布公网远程访问
  • 【Linux】当前进展
  • Python实现图形学曲线和曲面的Bezier曲线算法
  • CentOS Stream 9部署docker,并开启API
  • 银河麒麟桌面操作系统如何添加WPS字体
  • C++速通LeetCode中等第18题-删除链表的倒数第N个结点(最简单含注释)
  • zynq中断
  • git仓库服务器端损坏如何用本地code重新部署
  • 一次使用threading.Thread来实现Pytorch多个模型并发运行的失败案例
  • 罗德岛战记游戏源码(客户端+服务端+数据库+全套源码)游戏大小9.41G
  • 探秘 Web Bluetooth API:连接蓝牙设备的新利器
  • openEuler系统安装内网穿透工具实现其他设备公网环境远程ssh连接
  • GS-SLAM论文阅读笔记--TAMBRIDGE
  • Redis数据结构之list列表
  • 一款前后端分离CRM客户关系管理系统,支持客户,商机,线索,合同,发票,审核,商品等功能(附源码)
  • 基于redis的HyperLogLog数据结构实现的布隆过滤器在信息流中历史数据的应用
  • html 几行的空间分成3个区域
  • 【机器学习】--- 决策树与随机森林
  • Cisco 基础网络汇总
  • django+vue