ARM64 main 汇编分析
名词解释
名词 | 解释 | |
---|---|---|
sp | Stack Pointer(栈指针)的缩写。它通常指向程序的堆栈顶部,用于追踪函数调用中的栈帧的顶部位置。 | |
fp | Frame Pointer(帧指针)的缩写。它用于指向当前函数栈帧的开始位置,这在遍历栈帧或进行函数调用时特别有用。 | |
lr | Link Register(链接寄存器)的缩写。在某些架构(如ARM)中,当一个函数被调用时,返回地址被存储在这个寄存器中。这样,一旦函数执行完毕,控制可以返回到调用它的代码位置。 lr 是 x30 寄存器 |
原始 Rust 代码
fn main() {
let x = test_invoke_method(222, 666);
println!("{}", x);
}
fn test_invoke_method(x: i64, y: i64) -> i64 {
return x + y;
}
原始汇编
example`example::main:
0x10075506c <+0>: sub sp, sp, #0x80
0x100755070 <+4>: stp x29, x30, [sp, #0x70]
0x100755074 <+8>: add x29, sp, #0x70
0x100755078 <+12>: mov w8, #0xde
0x10075507c <+16>: mov x0, x8
0x100755080 <+20>: mov w8, #0x29a
0x100755084 <+24>: mov x1, x8
0x100755088 <+28>: bl 0x100755100 ; example::test_invoke_method at main.rs:13
-> 0x10075508c <+32>: add x9, sp, #0x8
0x100755090 <+36>: str x0, [sp, #0x8]
0x100755094 <+40>: mov x8, x9
0x100755098 <+44>: stur x8, [x29, #-0x10]
0x10075509c <+48>: adrp x8, 49
0x1007550a0 <+52>: add x8, x8, #0xadc ; core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt at num.rs:283
example`example::test_invoke_method:
0x100755100 <+0>: sub sp, sp, #0x30
0x100755104 <+4>: stp x29, x30, [sp, #0x20]
0x100755108 <+8>: add x29, sp, #0x20
0x10075510c <+12>: str x0, [sp, #0x10]
0x100755110 <+16>: stur x1, [x29, #-0x8]
-> 0x100755114 <+20>: adds x8, x0, x1
0x100755118 <+24>: str x8, [sp, #0x8]
0x10075511c <+28>: cset w8, vs
0x100755120 <+32>: tbnz w8, #0x0, 0x100755138 ; <+56> at main.rs:14:12
0x100755124 <+36>: b 0x100755128 ; <+40> at main.rs
0x100755128 <+40>: ldr x0, [sp, #0x8]
0x10075512c <+44>: ldp x29, x30, [sp, #0x20]
0x100755130 <+48>: add sp, sp, #0x30
0x100755134 <+52>: ret
0x100755138 <+56>: adrp x0, 53
0x10075513c <+60>: add x0, x0, #0x100 ; str.0
0x100755140 <+64>: mov w8, #0x1c
0x100755144 <+68>: mov x1, x8
0x100755148 <+72>: adrp x2, 67
0x10075514c <+76>: add x2, x2, #0x238
0x100755150 <+80>: bl 0x1007899e8 ; core::panicking::panic at panicking.rs:120
执行过程
假设在执行 sub sp, sp, #0x80 之前的栈内存结构如下
sp = 2128
x29 = x29_value1
x30 = x30_value1
2000
2001
2002
2003
...
2126
2127
sp -> 2128
执行 sub sp, sp, #0x80 后,栈内存变成下面的结构
sp = 2128
x29 = x29_value1
x30 = x30_value1
sp -> 2000
2001
2002
2003
...
2126
2127
2128
执行 stp x29, x30, [sp, #0x70] 后,栈内存变成下面的结构
sp = 2000
x29 = x29_value1
x30 = x30_value1
sp -> 2000
2001
2002
2003
2004
2005
2006
...
2111
2112 |
2113 |
2114 |
2115 |
2116 |
2117 |
2118 |
2119 | = x29_value1
2120 |
2121 |
2122 |
2123 |
2124 |
2125 |
2126 |
2127 | = x30_value1
2128
执行 mov w8, #0xde 后,栈内存变成下面的结构,w 表示使用低 32 位
sp = 2000
x8 = 0x00000000_000000de
x29 = 2112
x30 = x30_value1
sp -> 2000
2001
2002
2003
2004
2005
2006
...
2111
x29 -> 2112 |
2113 |
2114 |
2115 |
2116 |
2117 |
2118 |
2119 | = x29_value1
2120 |
2121 |
2122 |
2123 |
2124 |
2125 |
2126 |
2127 | = x30_value1
2128
执行 mov x0, x8 后,栈内存变成下面的结构
sp = 2000
x0 = 0x00000000_000000de
x8 = 0x00000000_000000de
x29 = 2112
x30 = x30_value1
sp -> 2000
2001
2002
2003
2004
2005
2006
...
2111
x29 -> 2112 |
2113 |
2114 |
2115 |
2116 |
2117 |
2118 |
2119 | = x29_value1
2120 |
2121 |
2122 |
2123 |
2124 |
2125 |
2126 |
2127 | = x30_value1
2128
执行 mov w8, #0x29a 后,栈内存变成下面的结构
sp = 2000
x0 = 0x00000000_000000de
x8 = 0x00000000_0000029a
x29 = 2112
x30 = x30_value1
sp -> 2000
2001
2002
2003
2004
2005
2006
...
2111
x29 -> 2112 |
2113 |
2114 |
2115 |
2116 |
2117 |
2118 |
2119 | = x29_value1
2120 |
2121 |
2122 |
2123 |
2124 |
2125 |
2126 |
2127 | = x30_value1
2128
执行 mov x1, x8 后,栈内存变成下面的结构
sp = 2000
x0 = 0x00000000_000000de
x1 = 0x00000000_0000029a
x8 = 0x00000000_0000029a
x29 = 2112
x30 = x30_value1
sp -> 2000
2001
2002
2003
2004
2005
2006
...
2111
x29 -> 2112 |
2113 |
2114 |
2115 |
2116 |
2117 |
2118 |
2119 | = x29_value1
2120 |
2121 |
2122 |
2123 |
2124 |
2125 |
2126 |
2127 | = x30_value1
2128
执行 bl 0x100755100,会将这条指令的下一条指令的地址存入 x30,然后将 pc 改为 0x100755100,执行后的内存结构如下
pc = 0x100755100
sp = 2000
x0 = 0x00000000_000000de
x1 = 0x00000000_0000029a
x8 = 0x00000000_0000029a
x29 = 2112
x30 = 0x10075508c
sp -> 2000
2001
2002
2003
2004
2005
2006
...
2111
x29 -> 2112 |
2113 |
2114 |
2115 |
2116 |
2117 |
2118 |
2119 | = x29_value1
2120 |
2121 |
2122 |
2123 |
2124 |
2125 |
2126 |
2127 | = x30_value1
2128
下面开始进入 test_invoke_method 方法
执行 sub sp, sp, #0x30 = 48 后的栈内存变化
pc = 0x100755100
sp = 1952
x0 = 0x00000000_000000de = 222
x1 = 0x00000000_0000029a = 666
x8 = 0x00000000_0000029a = 666
x29 = 2112
x30 = 0x10075508c
1950
1951
sp -> 1952
...
2000
2001
2002
...
2111
x29 -> 2112 |
2113 |
2114 |
2115 |
2116 |
2117 |
2118 |
2119 | = x29_value1
2120 |
2121 |
2122 |
2123 |
2124 |
2125 |
2126 |
2127 | = x30_value1
2128
执行 stp x29, x30, [sp, #0x20 = 32] 后的栈变化
pc = 0x100755100
sp = 1952
x0 = 0x00000000_000000de
x1 = 0x00000000_0000029a
x8 = 0x00000000_0000029a
x29 = 2112
x30 = 0x10075508c
1950
1951
sp -> 1952
...
1983
1984 |
1985 |
1986 |
1987 |
1988 |
1989 |
1990 |
1991 | = 2112
1992 |
1993 |
1994 |
1995 |
1996 |
1997 |
1998 |
1999 | = 0x100031100
2000
2001
2002
...
2111
x29 -> 2112 |
2113 |
2114 |
2115 |
2116 |
2117 |
2118 |
2119 | = x29_value1
2120 |
2121 |
2122 |
2123 |
2124 |
2125 |
2126 |
2127 | = x30_value1
2128
执行 add x29, sp, #0x20 = 32 后的栈变化
pc = 0x100755100
sp = 1952
x0 = 0x00000000_000000de
x1 = 0x00000000_0000029a
x8 = 0x00000000_0000029a
x29 = 1984
x30 = 0x10075508c
1950
1951
sp -> 1952
...
1983
x29 -> 1984 |
1985 |
1986 |
1987 |
1988 |
1989 |
1990 |
1991 | = 2112
1992 |
1993 |
1994 |
1995 |
1996 |
1997 |
1998 |
1999 | = 0x100031100
2000
2001
2002
...
2111
2112 |
2113 |
2114 |
2115 |
2116 |
2117 |
2118 |
2119 | = x29_value1
2120 |
2121 |
2122 |
2123 |
2124 |
2125 |
2126 |
2127 | = x30_value1
2128
当执行 str x0, [sp, #0x10 = 16] 后的变化
str x0, [sp, #0x10]
str
是“store register”的缩写,用于将一个寄存器的内容存储到内存中。这条指令将寄存器
x0
的内容存储到栈指针sp
指向的地址加上偏移量0x10
(16字节)的位置。简单来说,这是将寄存器
x0
的值保存到栈上一个特定的位置。
pc = 0x100755100
sp = 1952
x0 = 0x00000000_000000de = 222
x1 = 0x00000000_0000029a = 666
x8 = 0x00000000_0000029a = 666
x29 = 1984
x30 = 0x10075508c
1950
1951
sp -> 1952
...
1967
1968 |
1969 |
1970 |
1971 |
1972 |
1973 |
1974 |
1975 | = 0x00000000_000000de = 222
1976
...
1983
x29 -> 1984 |
1985 |
1986 |
1987 |
1988 |
1989 |
1990 |
1991 | = 2112
1992 |
1993 |
1994 |
1995 |
1996 |
1997 |
1998 |
1999 | = 0x100031100
2000
2001
2002
...
2111
2112 |
2113 |
2114 |
2115 |
2116 |
2117 |
2118 |
2119 | = x29_value1
2120 |
2121 |
2122 |
2123 |
2124 |
2125 |
2126 |
2127 | = x30_value1
2128
当执行 stur x1, [x29, #-0x8 = 8] 后的变化
stur x1, [x29, #-0x8]
stur
也是一种存储指令,但通常用于更复杂的地址计算。这里使用的是帧指针(x29
)。这条指令将寄存器
x1
的内容存储到帧指针x29
指向的地址减去偏移量0x8
(8字节)的位置。这通常用于在函数调用的栈帧中保存局部变量或参数。
pc = 0x100755100
sp = 1952
x0 = 0x00000000_000000de = 222
x1 = 0x00000000_0000029a = 666
x8 = 0x00000000_0000029a = 666
x29 = 1984
x30 = 0x10075508c
1950
1951
sp -> 1952
...
1967
1968 |
1969 |
1970 |
1971 |
1972 |
1973 |
1974 |
1975 | = 0x00000000_000000de = 222
1976 |
1977 |
1978 |
1979 |
1980 |
1981 |
1982 |
1983 | = 0x00000000_0000029a = 666
x29 -> 1984 |
1985 |
1986 |
1987 |
1988 |
1989 |
1990 |
1991 | = 2112
1992 |
1993 |
1994 |
1995 |
1996 |
1997 |
1998 |
1999 | = 0x10075508c
2000
执行 adds x8, x0, x1 的后的变化
adds x8, x0, x1
adds
是“add and set flags”的缩写,用于执行加法操作,并更新处理器的状态标志(比如零标志、负标志等)。这条指令将寄存器
x0
和x1
的内容相加,结果存储在寄存器x8
中。状态标志的更新允许随后的指令根据加法操作的结果(例如是否产生溢出、结果是否为零等)进行决策。
pc = 0x100755100
sp = 1952
x0 = 0x00000000_000000de = 222
x1 = 0x00000000_0000029a = 666
x8 = 0x00000000_00000378 = 888
x29 = 1984
x30 = 0x10075508c
1950
1951
sp -> 1952
...
1967
1968 |
1969 |
1970 |
1971 |
1972 |
1973 |
1974 |
1975 | = 0x00000000_000000de = 222
1976 |
1977 |
1978 |
1979 |
1980 |
1981 |
1982 |
1983 | = 0x00000000_0000029a = 666
x29 -> 1984 |
1985 |
1986 |
1987 |
1988 |
1989 |
1990 |
1991 | = 2112
1992 |
1993 |
1994 |
1995 |
1996 |
1997 |
1998 |
1999 | = 0x10075508c
2000
当执行 str x8, [sp, #0x8 = 8] 后的内存变化
pc = 0x100755100
sp = 1952
x0 = 0x00000000_000000de = 222
x1 = 0x00000000_0000029a = 666
x8 = 0x00000000_00000378 = 888
x29 = 1984
x30 = 0x10075508c
1950
1951
sp -> 1952
...
1958
1959
1960 |
1961 |
1962 |
1963 |
1964 |
1965 |
1966 |
1967 | = 0x00000000_00000378 = 888
1968 |
1969 |
1970 |
1971 |
1972 |
1973 |
1974 |
1975 | = 0x00000000_000000de = 222
1976 |
1977 |
1978 |
1979 |
1980 |
1981 |
1982 |
1983 | = 0x00000000_0000029a = 666
x29 -> 1984 |
1985 |
1986 |
1987 |
1988 |
1989 |
1990 |
1991 | = 2112
1992 |
1993 |
1994 |
1995 |
1996 |
1997 |
1998 |
1999 | = 0x10075508c
2000
接下来的三个指令这里给出解释,简单的说就是如果加溢出了会跳到 panic,没溢出则到 0x100755128,我们接着看 0x100755128 的指令
cset w8, vs
cset
是“conditional set”的缩写,用于基于条件标志设置寄存器的值。w8
是目标寄存器。vs
是“overflow set”(溢出设置)的条件代码,指示如果溢出标志被设置,则将w8
设置为1,否则设置为0。
tbnz w8, #0x0, 0x100755138
tbnz
是“test bit and branch if non-zero”的缩写,用于测试寄存器中的特定位,并在该位非零时执行分支操作。这条指令测试
w8
寄存器的第0位。如果测试的位非零,跳转到地址
0x100755138
执行,这是一个条件跳转。
b 0x100755128
b
是“branch”的缩写,用于无条件跳转到指定的地址。这条指令将程序的执行流跳转到地址
0x100755128
接下来执行 ldr x0, [sp, #0x8]
ldr x0, [sp, #0x8]
ldr
是“load register”的缩写,用于从内存加载数据到寄存器。这条指令从栈指针
sp
加上偏移量0x8
的地址加载数据到寄存器x0
。这通常用于恢复函数调用前保存的某个值或准备返回值。
pc = 0x100755100
sp = 1952
x0 = 0x00000000_00000378 = 888
x1 = 0x00000000_0000029a = 666
x8 = 0x00000000_00000378 = 888
x29 = 1984
x30 = 0x10075508c
1950
1951
sp -> 1952
...
1958
1959
1960 |
1961 |
1962 |
1963 |
1964 |
1965 |
1966 |
1967 | = 0x00000000_00000378 = 888
1968 |
1969 |
1970 |
1971 |
1972 |
1973 |
1974 |
1975 | = 0x00000000_000000de = 222
1976 |
1977 |
1978 |
1979 |
1980 |
1981 |
1982 |
1983 | = 0x00000000_0000029a = 666
x29 -> 1984 |
1985 |
1986 |
1987 |
1988 |
1989 |
1990 |
1991 | = 2112
1992 |
1993 |
1994 |
1995 |
1996 |
1997 |
1998 |
1999 | = 0x10075508c
2000
接下来执行 ldp x29, x30, [sp, #0x20 = 32],恢复 x29 栈底指针和 x30 返回地址
ldp x29, x30, [sp, #0x20]
ldp
是“load pair”的缩写,用于同时从内存加载两个寄存器的值。这条指令从栈中恢复
x29
(帧指针)和x30
(链接寄存器)的原始值。[sp, #0x20]
表示从栈指针sp
加上偏移量0x20
的地址开始加载数据。
pc = 0x100755100
sp = 1952
x0 = 0x00000000_00000378 = 888
x1 = 0x00000000_0000029a = 666
x8 = 0x00000000_00000378 = 888
x29 = 2112
x30 = 0x10075508c
1950
1951
sp -> 1952
...
1958
1959
1960 |
1961 |
1962 |
1963 |
1964 |
1965 |
1966 |
1967 | = 0x00000000_00000378 = 888
1968 |
1969 |
1970 |
1971 |
1972 |
1973 |
1974 |
1975 | = 0x00000000_000000de = 222
1976 |
1977 |
1978 |
1979 |
1980 |
1981 |
1982 |
1983 | = 0x00000000_0000029a = 666
1984 |
1985 |
1986 |
1987 |
1988 |
1989 |
1990 |
1991 | = 2112
1992 |
1993 |
1994 |
1995 |
1996 |
1997 |
1998 |
1999 | = 0x10075508c
2000
...
x29 -> 2112
接下来执行
add sp, sp, #0x30 = 48
add
是“addition”的缩写,用于执行加法操作。这条指令把栈指针
sp
增加0x30
,实际上是收缩栈空间,清理当前函数的栈帧。
pc = 0x100755100
sp = 2000
x0 = 0x00000000_00000378 = 888
x1 = 0x00000000_0000029a = 666
x8 = 0x00000000_00000378 = 888
x29 = 2112
x30 = 0x10075508c
1950
1951
1952
...
1958
1959
1960 |
1961 |
1962 |
1963 |
1964 |
1965 |
1966 |
1967 | = 0x00000000_00000378 = 888
1968 |
1969 |
1970 |
1971 |
1972 |
1973 |
1974 |
1975 | = 0x00000000_000000de = 222
1976 |
1977 |
1978 |
1979 |
1980 |
1981 |
1982 |
1983 | = 0x00000000_0000029a = 666
1984 |
1985 |
1986 |
1987 |
1988 |
1989 |
1990 |
1991 | = 2112
1992 |
1993 |
1994 |
1995 |
1996 |
1997 |
1998 |
1999 | = 0x10075508c
sp -> 2000
...
x29 -> 2112
接下来执行
ret
ret
是“return”的缩写,标志着函数的结束。这条指令使得程序返回到
x30
寄存器中存储的返回地址,即调用此函数的下一条指令。
后续这里的返回值存储在了 x0 的寄存器里,返回后使用 x0 就能拿到返回值了
结论
sp 指向栈顶,当需要访问内部的变量的时候可能会通过 sp + offset 的方式去访问
x29 指向栈底,当需要访问内部的变量的时候可能会通过 x29 - offset 的方式去访问
传递函数变量的时候会放在 x0, x1 这样的寄存器里面
返回值最终也放回了 x0 这样的寄存器
- 0
- 0
-
分享