Linux 操作系统分析实验:理解存储程序计算机和函数调用框架——以 ARM64 汇编为例

实验目的:通过分析 C 语言源程序经过 gcc 12.2 生成的 ARM64 汇编代码,理解存储程序计算机与函数调用框架。

这里推荐一个网站:https://godbolt.org,可以在线地将常见的高级语言源程序编译成对应指令集的汇编,支持 x86、ARM、MIPS、RISC-V 甚至龙芯的 Loongarch。本文以 ARM64 gcc 12.2 为例,解析汇编代码。

示例 C 语言源程序如下:

int g(int x)
{
    return x + 3;
}

int f(int x)
{
    return g(x);
}

int main(void)
{
    return f(8) + 1;
}

在线生成的 ARM64 汇编如下:

g:
        sub     sp, sp, #16
        str     w0, [sp, 12]
        ldr     w0, [sp, 12]
        add     w0, w0, 3
        add     sp, sp, 16
        ret
f:
        stp     x29, x30, [sp, -32]!
        mov     x29, sp
        str     w0, [sp, 28]
        ldr     w0, [sp, 28]
        bl      g
        ldp     x29, x30, [sp], 32
        ret
main:
        stp     x29, x30, [sp, -16]!
        mov     x29, sp
        mov     w0, 8
        bl      f
        add     w0, w0, 1
        ldp     x29, x30, [sp], 16
        ret

下面我们从 main 函数开始,逐行解析汇编代码的执行过程。

stp x29, x30, [sp, -16]!

main 第 1 条指令(第 17 行)将 x29(调用 main 函数之前的函数栈基址)、x30 (main 函数返回后执行的下一条指令的内存地址)分别压入栈中,即 sp - 16 的内存地址所在的 16 个字节的存储空间。这里用到了回写前变址寻址方式,sp 被回写为 sp - 16。这条指令备份了 main 函数调用者的栈基址和跳转指令的下一条指令的内存地址,以便 main 函数返回后继续执行 main 函数调用者的指令。

mov x29, sp

main 第 2 条指令(第 18 行)将栈顶地址赋给栈基址,形成了逻辑上的空栈,即构造了 main 函数的初始栈空间。

mov w0, 8

main 第 3 条指令(第 19 行)将 f 函数的参数 8 赋给寄存器 w0 进行传参。

bl      f

main 第 4 条指令(第 20 行)调用函数 f,先将下一指令地址(即函数返回地址)保存到 x30 寄存器中,再进行跳转。

stp     x29, x30, [sp, -32]!

f 第 1 条指令(第 9 行)同样是将 x29 和 x30 压栈,只不过这里 sp 被回写成了 sp - 32,是为了多生成一些栈空间存放函数调用的参数,这会在后面的指令中体现。

mov     x29, sp

f 第 2 条指令(第 10 行)构造了 f 函数的初始栈空间。

str     w0, [sp, 28]
ldr     w0, [sp, 28]

f 第 3、4 条指令(第 11、12 行)先将上 main 函数传递过来的参数从 w0 存入栈中(第 9 行提前预留好了栈空间),再从栈中取回 w0(疑问:为何不能直接把这个步骤优化掉呢,不是很明白)。

bl      g

f 第 5 条指令(第 13 行)调用函数 g。

sub sp, sp, #16

g 第 1 条指令(第 2 行)为 g 开辟了 16 个字节的栈空间,以存放 g 函数传来的参数。由于 g 函数不需要再执行函数调用,所以就不需要再构建逻辑上的空栈,只要留出栈空间即可。

str     w0, [sp, 12]
ldr     w0, [sp, 12]
add     w0, w0, 3

g 第 2~4 条指令(第 3~5 行)先将参数从 w0 存入栈中再取回,接着执行 +3 操作,准备作为返回值返回。

add     sp, sp, 16
ret

g 第 5~6 条指令(第 6~7 行)将栈指针上移(销毁形参和临时变量),并执行返回操作。ret 指令将 x30 寄存器的值赋给指令指针寄存器 PC。后续汇编代码将从第 14 行开始执行。

ldp x29, x30, [sp], 32
ret

f 第 6~7 条指令(第 14~15 行)将 x29 和 x30 寄存器的内容退栈、销毁 f 的栈空间(x29 和 x30 恢复成 main 函数的状态),并将 x30 寄存器的值赋给指令指针寄存器 PC。后续汇编代码将从第 21 行开始执行。

add     w0, w0, 1

main 第 5 条指令(第 21 行)将 f 函数的返回值 w0 加 1。

ldp     x29, x30, [sp], 16
ret

main 第 6~7 条指令(第 22~23 行)与第 14~15 行指令的过程类似,区别只是销毁的栈空间大小不同。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注