ARM64汇编、堆栈回溯、panic时回溯函数调用
看一个实例代码程序:
#include <stdio.h>
int callee_func2(int a)
{
int b = 2;
return a + b;
}
int callee_func1(int a)
{
int b = 1, c;
c = callee_func2(a);
return b + c;
}
int main(void)
{
int ret;
ret = callee_func1(0);
return 0;
}
对该程序进行编译以及反汇编操作:
aarch64-linux-gnu-gcc test.c -o test
aarch64-linux-gnu-objdump -d test > disassemble.txt
打开disassemble.txt查看汇编代码:
三个函数汇编代码的text段的排序和源码一致(从 0000000000400520 到 0000000000400594):
0000000000400520 <callee_func2>:
01 400520: d10083ff sub sp, sp, #0x20 //只是最后一级函数,不再调用其他函数,经过编译器优化后,FP/LR都没存入堆栈了,因为FP/LR在该类函数中不再改变。把sp减去32(即该函数使用32字节栈空间)。
02 400524: b9000fe0 str w0, [sp,#12] //把W0(参数a)存入栈sp+12处。
03 400528: 52800040 mov w0, #0x2 //把 #2(立即数)存入W0。
04 40052c: b9001fe0 str w0, [sp,#28] //把W0(立即数2)存入栈sp+28处。
05 400530: b9400fe1 ldr w1, [sp,#12] //把栈sp+12处数据(参数a)存入W1。
06 400534: b9401fe0 ldr w0, [sp,#28] //把栈sp+28处数据(立即数2)存入W0。
07 400538: 0b000020 add w0, w1, w0 //W0 = W1+W0,存入W0作为返回值。
08 40053c: 910083ff add sp, sp, #0x20 //把sp增加32(即恢复sp),sp指向调用callee_func2之前callee_func1函数的sp处。
09 400540: d65f03c0 ret //从X30(LR)中恢复PC寄存器为400560,即返回main_27行处执行。
0000000000400544 <callee_func1>:
10 400544: a9bd7bfd stp x29, x30, [sp,#-48]! // 索引模式:先把X30(LR)存入栈sp-48+8处,再把X29(FP)存入堆栈sp-48处,最后sp自身减去48byte(即该函数使用32字节栈空间),堆栈从高地址往低地址生长。
11 400548: 910003fd mov x29, sp //把sp存入FP。
12 40054c: b9001fa0 str w0, [x29,#28] //把W0(参数a)存入FP+28栈的位置。
13 400550: 52800020 mov w0, #0x1 // 把#1(局部变量b)存入W0寄存器。
14 400554: b9002fa0 str w0, [x29,#44] //把W0(局部变量b)存入FP+44栈的位置。???
15 400558: b9401fa0 ldr w0, [x29,#28] //把FP+28的内存(参数a)存入W0寄存器,作为callee_func2函数的参数。
16 40055c: 97fffff1 bl 400520 <callee_func2> //调用callee_func2函数,并且把返回地址400560(当前指令地址+4)存入X30(LR)寄存器(前一个函数的LR已入栈)。
17 400560: b9002ba0 str w0, [x29,#40] //callee_func2返回后,PC指向该处。把W0(callee_func2的返回值)存入栈FP+40处。
18 400564: b9402fa1 ldr w1, [x29,#44] //把栈FP+44处的数据(即变量b)存入W1。
19 400568: b9402ba0 ldr w0, [x29,#40] //把栈FP+40处的数据(即变量c,也就是calle_func2的返回值)存入W0。
20 40056c: 0b000020 add w0, w1, w0 //把W1+W0相加(即b+c),存入W0作为函数返回值。
21 400570: a8c37bfd ldp x29, x30, [sp],#48 // 先从SP处弹出X29(FP),再从SP+8处弹出X30(LR),最后把sp增加48byte,即sp指向了调用callee_func1之前main函数的sp处。
22 400574: d65f03c0 ret //从X30(LR)中恢复PC寄存器为400588,即返回main_27行处执行。
0000000000400578 <main>: //假定shell调用main函数
23 400578: a9be7bfd stp x29, x30, [sp,#-32]! // 索引模式:先把X30(LR)存入栈sp-32+8处,再把X29(FP)存入堆栈sp-32处,最后sp自身减去32byte(即该函数使用32字节栈空间),堆栈从高地址往低地址生长。
24 40057c: 910003fd mov x29, sp //把sp存入FP。
25 400580: 52800000 mov w0, #0x0 // 把#0(立即数)存入W0寄存器,作为callee_func1函数的参数。
26 400584: 97fffff0 bl 400544 <callee_func1> //调用callee_func1函数,并且把返回地址400588(当前指令地址+4)存入X30(LR)寄存器(前一个函数的LR已入栈)。
27 400588: b9001fa0 str w0, [x29,#28] //callee_func1返回后,PC指向该处。把W0(callee_func1的返回值)存入栈FP+28处。
28 40058c: 52800000 mov w0, #0x0 // 把#0(立即数)存入W0寄存器,作为main函数的返回值。
29 400590: a8c27bfd ldp x29, x30, [sp],#32 // 先从SP处弹出X29(FP),再从SP+8处弹出X30(LR),最后把sp增加32byte,即sp指向了调用callee_func1之前main函数的sp处。
30 400594: d65f03c0 ret //从X30(LR)中恢复PC寄存器,即返回shell。
上面程序的栈视图:
特殊说明,在ARM64里面压栈出栈指令:
stp x29, x30, [sp,#-32]! //索引模式,”!”表示先索引加立即数,然后再保存数据模式:先把X30压入栈sp-32+8处,然后再把X29压入堆栈sp-32处,最后把sp自身减去32byte,堆栈从高地址往低地址生长。
ldp x29, x30, [sp],#32 //索引模式,”[],#”表示先载入数据,然后再索引加立即数模式:先从SP处弹出X29,然后再从SP+8处弹出X30,最后把sp增加32byte,刚好和压栈相反。
str x19, [sp,#16] //偏移模式,把x19寄存器值存入栈sp+16位置,sp不变。
ldr x19, [sp,#16] //偏移模式,从栈sp+16位置的数据放入x19寄存器,sp不变。
str x0, [sp,#-16]! //索引模式,先把x0压入栈sp-16处,同时sp再减16byte。
ldr x0, [sp],#16 //索引模式,先从栈sp处弹出x0,然后把sp增加16byte。
从上面函数的压栈出栈模式可以看出来,每级函数调用都会把LR/FP压栈在FP所指向的内存位置(当然跟编译器有关,clang和gcc是这样做的,其他编译器未必,然后还有个编译参数-mapcs-frame,会在函数栈里面压入PC/LR/SP/FP四个寄存器),所以系统panic(coredump)的时候,可以一级一级的从栈的SP/LR/FP往前推。
比如这个栈信息:
从FP看到stack的内容,在0x0000007fd95ac150 处保存的是上一个函数的FP/SP(0x00000000d95ac270 0x000000000000007f,合起来即0x0000007fd95ac270)和LR(0x000000008e0f51d0 0x00000000000007f,合起来即0x0000007f8e0f51d0,即abort函数),再往前推一级,0x0000007fd95ac270处FP/SP(0x0000007fd95ac3b0)和LR(0x0000007f8e3d2af8,即__gun_cxx::__verbose_terminate_handler函数)。
附:ARM64寄存器
异常等级
软件运行异常级别:
-
EL0: 普通用户应用程序
-
EL1: 操作系统内核通常被描述为特权
-
EL2: 管理程序
-
EL3: 低级固件,包括安全监视器
ARMv8 寄存器
AArch 拥有 31 个通用寄存器,系统运行在 64 位状态下的时候名字叫 Xn,运行在 32 位的时候就叫 Wn.
32 位 W 寄存器构成相应 64 位 X 寄存器的下半部分。 也就是说,W0 映射到 X0 的低位字,W1 映射到 X1 的低位字。
特殊寄存器
Name
|
Size
|
Description
|
WZR
|
32bits
|
Zero register
|
XZR
|
64bits
|
Zero register
|
WSP
|
32bits
|
Current stack pointer
|
SP
|
64bits
|
Current stack pointer
|
PC
|
64bits
|
Program counter
|
ARM 64 位架构的 ABI
ARM 体系结构的应用程序二进制接口(ABI, Application Binary Interface)指定了基本规则所有可执行的本机代码模块必须遵守,以便它们可以正常工作。
通用寄存器
通用寄存器分为 4 组:
-
参数寄存器(X0-X7): 用作临时寄存器或可以保存的调用者保存的寄存器变量函数内的中间值,调用其他函数之间的值(8 个寄存器可用于传递参数)
-
调用者保存的临时寄存器(X9-X15): 如果调用者要求在任何这些寄存器中保留值调用另一个函数,调用者必须将受影响的寄存器保存在自己的堆栈中帧。 它们可以通过被调用的子程序进行修改,而无需保存并在返回调用者之前恢复它们。
-
被调用者保存的寄存器(X19-X29): 这些寄存器保存在被调用者帧中。 它们可以被被调用者修改子程序,只要它们在返回之前保存并恢复。
-
特殊用途寄存器(X8,X16-X18,X29,X30):
-
X8: 是间接结果寄存器,用于保存子程序返回地址,尽量不使用
-
X16 和 X17: 程序内调用临时寄存器
-
X18: 平台寄存器,保留用于平台 ABI,尽量不使用
-
X29: 帧指针寄存器(FP)
-
X30: 链接寄存器(LR)
-
X31: 堆栈指针寄存器 SP 或零寄存器 ZXR
-
NEON 和浮点寄存器
A64 指令集
arm64 汇编准备
寄存器
通用寄存器
31 个R0 ~ R30,每个寄存器可以存取一个 64 位大小的数。 当使用 x0 – x30访问时,是一个 64位的数;当使用 w0 – w30访问时,是一个 32 位的数,访问的是寄存器的 低 32 位,如图:
向量寄存器
(也可以说是 浮点型寄存器)每个寄存器的大小是 128 位的。 分别可以用Bn、Hn、Sn、Dn、Qn的方式来访问不同的位数;如图:
**注:**word 是 32 位,也就是 4 Byte大小。
-
Bn: 一个 Byte的大小,即 8 位
-
Hn: half word,即 16 位
-
Sn: single word,即 32 位
-
Dn: double word,即 64 位
-
Qn: quad word,即128 位
特殊寄存器
-
sp: (Stack Pointer),栈顶寄存器,用于保存栈顶地址;
-
fp(x29): (Frame Pointer)为栈基址寄存,用于保存栈底地址;
-
lr(x30): (Link Register) ,保存调用跳转指令 bl 指令的下一条指令的内存地址;
-
zr(x31): (Zero Register),xzr/wzr分别代表 64/32 位,其作用就是 0,写进去代表丢弃结果,读出来是 0;
-
pc: 保存将要执行的指令的地址(有操作系统决定其值,不能改写)。
状态寄存器 CPSR
CPSR (Current Program Status Register)和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义;而 CPSR 寄存器是按位起作用的,即,每一位都有专门的含义,记录特定的信息;如下图注: CPSR 寄存器是 32 位的。
-
CPSR 的 低8位(包括 I、F、T 和 M[4:0])称为控制位,程序无法修改,除非 CPU 运行于 特权模式 下,程序才能修改控制位。
-
N、Z、C、V 均为条件码标志位;其内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行。
-
N(Negative)标志: CPSR 的第 31 位是 N,符号标志位;记录相关指令执行后其结果是否为负数,如果为负数,则 N = 1;如果是非负数,则 N = 0。
-
Z(Zero)标志: CPSR 的第 30 位是 Z,0标志位;记录相关指令执行后,其结果是否为0,如果结果为0,则 Z = 1;如果结果不为0,则 Z = 0。
-
C(Carry)标志: CPSR 的第 29 位是C,进位标志位;
-
加法运算:当运算结果产生了 进位 时(无符号数溢出),C = 1,否则 C = 0 ;
-
减法运算(包括 CMP): 当运算时产生了 借位 时(无符号数溢出),C = 0,否则 C = 1 。
-
-
V(Overflow)标志: CPSR 的第 28 位是 V,溢出标志位;在进行有符号数运算的时候,如果超过了机器所能标识的范围,称为溢出。
-
条件码列表
操作码
|
条件码助记符
|
标志
|
含义
|
0000
|
EQ
|
Z=1
|
相等
|
0001
|
NE(Not Equal)
|
Z=0
|
不相等
|
0010
|
CS/HS(Carry Set/High or Same)
|
C=1
|
无符号数大于或等于
|
0011
|
CC/LO(Carry Clear/LOwer)
|
C=0
|
无符号数小于
|
0100
|
MI(MInus)
|
N=1
|
负数
|
0101
|
PL(PLus)
|
N=0
|
正数或零
|
0110
|
VS(oVerflow set)
|
V=1
|
溢出
|
0111
|
VC(oVerflow clear)
|
V=0
|
没有溢出
|
1000
|
HI(High)
|
C=1,Z=0
|
无符号数大于
|
1001
|
LS(Lower or Same)
|
C=0,Z=1
|
无符号数小于或等于
|
1010
|
GE(Greater or Equal)
|
N=V
|
有符号数大于或等于
|
1011
|
LT(Less Than)
|
N!=V
|
有符号数小于
|
1100
|
GT(Greater Than)
|
Z=0,N=V
|
有符号数大于
|
1101
|
LE(Less or Equal)
|
Z=1,N!=V
|
有符号数小于或等于
|
1110
|
AL
|
任何
|
无条件执行(默认)
|
1111
|
NV
|
任何
|
从不执行
|
指令读取
在 arm64 架构中,每个指令读取都是 64 位,即 8字节 空间。
arm64 约定(一般来说)
-
x0 ~ x7 分别会存放方法的前 8 个参数;如果参数个数超过了8个,多余的参数会存在栈上,新方法会通过栈来读取。
-
方法的返回值一般都在 x0 上;如果方法返回值是一个较大的数据结构时,结果会存在 x8 执行的地址上。
常见汇编指令
-
mov: 将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器与常量之间传值,不能用于内存地址),如:mov x1, x0 ; 将寄存器 x0 的值复制到寄存器 x1 中
-
add: 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中,如:add x0, x0, #1 ; 将寄存器 x0 的值和常量 1 相加后保存在寄存器 x0 中add x0, x1, x2 ; 将寄存器 x1 和 x2 的值相加后保存到寄存器 x0 中add x0, x1, [x2] ; 将寄存器 x1 的值加上寄存器 x2 的值作为地址,再取该内存地址的内容放入寄存器 x0 中
-
sub: 将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中,如:sub x0, x1, x2 ; 将寄存器 x1 和 x2 的值相减后保存到寄存器 x0 中
-
mul: 将某一寄存器的值和另一个寄存器的值 相乘 并将结果保存在另一寄存器中,如:mul x0, x1, x2 ; 将寄存器 x1 和 x2 的值相乘后结果保存到寄存器 x0 中
-
sdiv:(有符号数,对应 udiv: 无符号数)将某一寄存器的值和另一个寄存器的值 相除 并将结果保存在另一寄存器中,如:sdiv x0, x1, x2 ; 将寄存器 x1 和 x2 的值相除后结果保存到寄存器 x0 中
-
and: 将某一寄存器的值和另一寄存器的值 按位与 并将结果保存到另一寄存器中,如:and x0, x0, #0xf ; 将寄存器 x0 的值和常量 0xf 按位与后保存到寄存器 x0 中
-
orr: 将某一寄存器的值和另一寄存器的值 按位或 并将结果保存到另一寄存器中,如:orr x0, x0, #9 ; 将寄存器 x0 的值和常量 9 按位或后保存到寄存器 x0 中
-
eor: 将某一寄存器的值和另一寄存器的值 按位异或 并将结果保存到另一寄存器中,如:eor x0, x0, #0xf ; 将寄存器 x0 的值和常量 0xf 按位异或后保存到寄存器 x0 中复制代码
-
str: (store register) 将寄存器中的值写入到内存中,如:str w9, [sp, #0x8] ; 将寄存器 w9 中的值保存到栈内存 [sp + 0x8] 处
-
strb: (store register byte) 将寄存器中的值写入到内存中(只存储一个字节),如:strb w8, [sp, #7] ; 将寄存器 w8 中的低 1 字节的值保存到栈内存 [sp + 7] 处
-
ldr: (load register) 将内存中的值读取到寄存器中,如:ldr x0, [x1] ; 将寄存器 x1 的值作为地址,取该内存地址的值放入寄存器 x0 中ldr w8, [sp, #0x8] ; 将栈内存 [sp + 0x8] 处的值读取到 w8 寄存器中ldr x0, [x1, #4]! ; 将寄存器 x1 的值加上 4 作为内存地址, 取该内存地址的值放入寄存器 x0 中, 然后将寄存器 x1 的值加上 4 放入寄存器 x1 中ldr x0, [x1], #4 ; 将寄存器 x1 的值作为内存地址,取内该存地址的值放入寄存器 x0 中, 再将寄存器 x1 的值加上 4 放入寄存器 x1 中ldr x0, [x1, x2] ; 将寄存器 x1 和寄存器 x2 的值相加作为地址,取该内存地址的值放入寄存器 x0 中
-
ldrsb: (load register byte) 将内存中的值(只读取一个字节)读取到寄存器中,如:ldrsb w8, [sp, #7] ; 将栈内存 [sp + 7] 出的 低 1 字节的值读取到寄存器 w8 中
-
stur: 同 str 将寄存器中的值写入到内存中(一般用于 负 地址运算中),如:stur w10, [x29, #-0x4] ; 将寄存器 w10 中的值保存到栈内存 [x29 – 0x04] 处
-
ldur: 同 ldr 将内存中的值读取到寄存器中(一般用于 负 地址运算中),如:ldur w8, [x29, #-0x4] ; 将栈内存 [x29 – 0x04] 处的值读取到 w8 寄存器中
-
stp: 入栈指令(str 的变种指令,可以同时操作两个寄存器),如:stp x29, x30, [sp, #0x10] ; 将 x29, x30 的值存入 sp 偏移 16 个字节的位置
-
ldp: 出栈指令(ldr 的变种指令,可以同时操作两个寄存器),如:ldp x29, x30, [sp, #0x10] ; 将 sp 偏移 16 个字节的值取出来,存入寄存器 x29 和寄存器 x30
-
scvtf: (Signed Convert To Float)带符号 定点数 转换为 浮点数,如:scvtf d1, w0 ; 将寄存器 w0 的值(顶点数,转化成 浮点数) 保存到 向量寄存器/浮点寄存器 d1 中
-
fcvtzs: (Float Convert To Zero Signed)浮点数 转化为 定点数 (舍入为0),如:fcvtzs w0, s0 ; 将向量寄存器 s0 的值(浮点数,转换成 定点数)保存到寄存器 w0 中
-
cbz: 和 0 比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令);
-
cbnz: 和非 0 比较(Compare),如果结果非零(Non Zero)就转移(只能跳到后面的指令);
-
cmp: 比较指令,相当于 subs,影响程序状态寄存器 CPSR ;
-
cset: 比较指令,满足条件,则并置 1,否则置 0 ,如:cmp w8, #2 ; 将寄存器 w8 的值和常量 2 进行比较cset w8, gt ; 如果是大于(grater than),则将寄存器 w8 的值设置为 1,否则设置为 0复制代码
-
brk: 可以理解为跳转指令特殊的一种
-
LSL: 逻辑左移
-
LSR: 逻辑右移
-
ASR: 算术右移
-
ROR: 循环右移
-
adrp: 用来定位数据段中的数据用, 因为 aslr 会导致代码及数据的地址随机化, 用 adrp 来根据 pc 做辅助定位
-
b: (branch)跳转到某地址(无返回), 不会改变 lr (x30) 寄存器的值;一般是本方法内的跳转,如 while 循环,if else 等 ,如:b LBB0_1 ; 直接跳转到标签 ‘LLB0_1’ 处开始执行
-
bl: 跳转到某地址(有返回),先将下一指令地址(即函数返回地址)保存到寄存器 lr (x30)中,再进行跳转 ;一般用于不同方法直接的调用 ,如:bl 0x100cfa754 ; 先将下一指令地址(‘0x100cfa754’ 函数调用后的返回地址)保存到寄存器 ‘lr’ 中,然后再调用 ‘0x100cfa754’ 函数
-
blr: 跳转到 某寄存器 (的值)指向的地址(有返回),先将下一指令地址(即函数返回地址)保存到寄存器 lr (x30)中,再进行跳转 ;如:blr x20 ; 先将下一指令地址(‘x20’指向的函数调用后的返回地址)保存到寄存器 ‘lr’ 中,然后再调用 ‘x20’ 指向的函数
-
br: 跳转到某寄存器(的值)指向的地址(无返回), 不会改变 lr (x30) 寄存器的值。
-
brk: 可以理解为跳转指令特殊的一种。
-
ret: 子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中