阅读本文前,可以先阅读下述文档,对函数栈、栈帧等的概念会有所了解,会对本文章的理解大有益处
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, 

bash

    1
    2
    3

交叉编译器使用: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);
}

c
运行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

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, #0
   1039c:    bf00          nop
   1039e:    46bd          mov    sp, r7
   103a0:    bc80          pop    {r7}
   103a2:    4770          bx    lr

000103a4 <func2>:
   103a4:    b580          push    {r7, lr}
   103a6:    b084          sub    sp, #16
   103a8:    af00          add    r7, sp, #0
   103aa:    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, r3
   103b4:    68bb          ldr    r3, [r7, #8]
   103b6:    b2db          uxtb    r3, r3
   103b8:    fb12 f303     smulbb    r3, r2, r3
   103bc:    b2da          uxtb    r2, r3
   103be:    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, r3
   103ce:    4618          mov    r0, r3
   103d0:    3710          adds    r7, #16
   103d2:    46bd          mov    sp, r7
   103d4:    bd80          pop    {r7, pc}

000103d6 <main>:
   103d6:    b580          push    {r7, lr}
   103d8:    b082          sub    sp, #8
   103da:    af00          add    r7, sp, #0
   103dc:    1cfb          adds    r3, r7, #3
   103de:    461a          mov    r2, r3
   103e0:    2102          movs    r1, #2
   103e2:    2001          movs    r0, #1
   103e4:    f7ff ffde     bl    103a4 <func2>
   103e8:    6078          str    r0, [r7, #4]
   103ea:    2300          movs    r3, #0
   103ec:    4618          mov    r0, r3
   103ee:    3708          adds    r7, #8
   103f0:    46bd          mov    sp, r7
   103f2:    bd80          pop    {r7, pc}

bash

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47

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>

bash

    1

再看函数 func1,

00010398 <func1>:
   10398:    b480          push    {r7}                # r7 入栈保存值
   1039a:    af00          add    r7, sp, #0                # r7 = sp + 0
   1039c:    bf00          nop
   1039e:    46bd          mov    sp, r7                    # sp = r7
   103a0:    bc80          pop    {r7}                    # r7 出栈
   103a2:    4770          bx    lr                        # 返回

bash

    1
    2
    3
    4
    5
    6
    7

    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 - 16
   103a8:    af00          add    r7, sp, #0                # r7 = sp + 0
   103aa:    60f8          str    r0, [r7, #12]            # (r7 + 12) = r0
   103ac:    60b9          str    r1, [r7, #8]            # (r7 + 8) = r1
   103ae:    607a          str    r2, [r7, #4]            # (r7 + 4) = r2
   103b0:    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 * r3
   103bc:    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 = r7
   103d4:    bd80          pop    {r7, pc}                # r7 、lr 出栈,lr 出栈赋值给 pc

bash

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    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    ; 0x4
    8358:    e24dd010     sub    sp, sp, #16    ; 0x10
    835c:    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, r3
    8374:    e20330ff     and    r3, r3, #255    ; 0xff
    8378:    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, r3
    8390:    e1a00003     mov    r0, r3
    8394:    e24bd004     sub    sp, fp, #4    ; 0x4
    8398:    e8bd8800     pop    {fp, pc}

bash

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    针对栈底寄存器,不再使用 r7,而是使用 fp 替代
    栈底的功能,不再是保存新栈帧的栈顶,而是调用者栈帧的栈底 fp 的值,这样会方便栈回溯。使用 gcc-7.3 默认选项编译,GNU 说可以使用 unwind 方法回溯,这里暂时不会介绍 unwind 方法
    在这里插入图片描述

还有一个需要注意的是,如果你编译出来的文件,不带 fp 栈底,可能是编译时被优化掉了。编译器优化关闭了帧指针(如使用 -fomit-frame-pointer,这是 GCC 默认行为)。

如果你希望强制使用 fp:
你可以在编译时显式告诉编译器 保留帧指针,例如使用:

gcc -fno-omit-frame-pointer -O0 -marm -o test test.c

bash

    1

    -fomit-frame-pointer :省略函数调用期间 fp 指针的存储
    -fno-omit-frame-pointer:指示编译器将 fp 存储在寄存器中

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

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/weixin_43275558/article/details/139399170

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐