一.概述
语言分类
1.机器语言:
- 由0和1组成的机器指令,如:0101 0001 1101 0110
2.汇编语言
- 使用符号代替难编写和很难阅读机器语言,也成为符号语言。如:mov,ax,bx,call
3.高级语言
- C/C++/OC/Java/Swift,更让人易读和编写的语言(更接近人类的自然语言)
编程语言编译时的区别:
C++/OC/Swift... ------编译器编译------>汇编代码------->机器码
JS/Python... ------脚本引擎执行------>中间代码------->机器码
Java... ------编译器编译------>字节码----JVM--->机器码
汇编语言与机器语言一一对应, 每一条机器指令都有与之对应的汇编指令
汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
高级语言可以通过编译得到汇编语言/机器语言,但汇编语言/机器语言几乎不可能还原成高级语言
特点
- 可直接访问、控制各种硬件设备。比如存储器、CPU等,能最大限度地发挥硬件的功能
- 能够不受编译器的限制,对生成的二进制代码进行完全的控制
- 目标代码简短,占用内存少,执行速度快
- 汇编指令是机器指令的助记符,同机器指令一一对应。每种CPU都有自己的机器指令集/汇编指令集,所以汇编语言不具备可移植性
- 汇编语言知识点过多,开发者需要对CPU等硬件结构有所了解,不宜于编写、调试、维护
- 不区分大小写,比如mov和MOV是一样的
用途
- 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
- 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内嵌汇编)
- 软件安全
- 病毒分析和防治
- 逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
- 理解整个计算机系统的最佳起点和最有效途径
- 为编写高效代码打下基础
- 弄清楚代码的本质
- 函数的本质是什么?
sizeof
++a + ++a + ++a 底层如何执行的? - 编译器到底帮我们干了什么?
- DEBUG模式和RELEASE模式有什么关键的地方被我们忽略
种类
目前讨论比较多的汇编语言
- 8086汇编(8086处理器是16bit的CPU)
- Win32汇编
- Win64汇编
- ARM汇编(嵌入式、Mac、iOS)
我们iPhone里面的用到的是ARM汇编,但是不同的设备也有差异。因为CPU的架构不同,5s以后的手机都是ARM64汇编
二.ARM64汇编
1.寄存器
通用寄存器
64bit x0-x28
32bit w0-w28
(lldb) register read x0 //读取x0地址
x0 = 0x0000000000000003
(lldb) register read //读取所有寄存器地址
(lldb) register write x0 0x0000000000000123 //写入寄存器x0地址
(lldb) register read x0
x0 = 0x0000000000000123
(lldb) register read w0
w0 = 0x00000123
零寄存器
用来清空内存数据
- wzr(32bit),存储#0x0000,
- xzr(64bit) ,存储#0x00000000,
程序计数器
-
pc 记录当前执行的指令的内存的位置
-
lr 记录函数返回的内存地址的位置,也是x30寄存器
可以用bl指令验证,调用bl 跳转函数段后,lr保存下一行代码内存地址(也就是函数返回地址)
堆栈指针
- sp
- fp 也就是x29寄存器
2.指令
- ret 函数返回
- mov a b 把b赋给a
测试:
1.创建一个armtest头文件和汇编的实现.s文件
//在程序启动的时候调用函数test
int main(int argc, char * argv[]) {
@autoreleasepool {
test();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
![WX20190917-165220.png](https://upload-images.jianshu.io/upload_images/5734145-05ac6f41ad4d0aa4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
//armtest.h:
#ifndef armtest_h
#define armtest_h
void test();
#endif /* armtest_h */
//armtest.s:
.text //声明代码段
.global _test //指定test函数为全局函数
_test:
mov x0,#0x9 //16进制9赋给x0寄存器
mov x1,x0 //x0赋给x1寄存器
ret
2.断点测试
- add a,b,c 把b,c相加赋值给a
- sub a,b,c 把b,c相减赋值给a
一般寄存器x0-x7存放函数参数,更多的存放在堆栈中,x0存放函数的返回值
//1.定义函数:
int addM(int a,int b);
int subM(int a,int b);
//2.调用函数
NSLog(@"%d",addM(2,5));
NSLog(@"%d",subM(8,3));
//3.函数的汇编实现
.text
.global _addM,_subM
_addM:
add x0,x0,x1 //x0,x1表示存放函数参数,x0f存放函数返回值
ret
_subM:
sub x0,x0,x1
ret
- cmp a b 比较a,b(a-b的值寄存器)的结果放到cpsr寄存器中的标志位
- b 跳转到指定程序段[可以结合条件和cmp指令进行跳转eq(相等) ne(不等) gt(大于) ge(大于等于) lt(小于) le(小于等于)]
//register read 查看所有寄存器的值
_cmpM:
mov x0,#0x8
mov x1,#0x2
cmp x0,x1 //x0>x1 cpsr =0x80000000 1000...0000
mov x0,#0x1
mov x1,#0x1
cmp x0,x1 //x0=x1 cpsr = 0x20000000 0010...0000
mov x0,#0x1
mov x1,#0x6
cmp x0,x1 //x0<x1 cpsr = 0x60000000 0110...0000
ret
_bM:
mov x0,#0x7
mov x1,#0x7
cmp x0,x1
beq _bres //eq ne gt ge lt le
mov x0,#0x0 //如果结果相等就跳转到_bres方法,否则继续向下执行
ret
_bres:
mov x0,#0x1
ret
- bl 带返回的跳转指令
_blM:
mov x1,#0x1
mov x2,#0x2
//b code 直接执行code段代码,不执行mov x3,#0x3
//bl code 直接执行code段代码,然后再接着执行mov x3,#0x3,类似函数调用
bl code
mov x3,#0x3
ret
code:
add x0,x1,x2
ret
使用b,bl指令可以简单处理if-else和函数调用。
查看if-else语句的汇编代码
int a=3;
int b=5;
if(a>b)
{
printf("a>b");
}else
{
NSLog(@"a<=b");
}
cmp w8,w9 (3,5)
b.le
w8小于等于w9 b.le指令跳到NSLog代码段
否则直接向下执行,遇到NSLog代码段直接b指令跳过
查看mytest()函数调用的汇编代码
void mytest(void)
{
NSLog(@"test");
}
int main(int argc, char * argv[]) {
@autoreleasepool {
int a=1;
int b=2;
if(a<b)
{
mytest();
}
printf("continue");
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
bl指令跳转到mytest()代码块,然后再执行printf代码块
从内存中读取数据的指令
- ldr w0,[x1,#0x5] 偏移位是正,[x1,#0x5]表示x1+#0x5赋值给w0;[x1,#0x5]!表示x1+#0x5赋值x1,再赋值给w0
- ldur w0,[x1,#-0x3] 偏移位是负
- ldp w0,w1,[x1,#0x7]使用两个寄存器按低到高位存储数据
_ldrM:
ldr w0,[x1] //把x1的地址(前32bit)赋给w0寄存器
ret
随便打断点运行一段程序都可以看到这些指令的存在
从内存中写入数据的指令(用法基本同读取指令)
- str
- stur
- stp
int a=8;
strM();
strM:
_strM:
str w0,[x1] //把w0寄存器的值赋给x1的内存地址
ret
随便打断点运行一段程序都可以看到这些指令的存在
3.堆栈
函数的分类:
- 叶子函数
函数内部不调用其他函数 - 非叶子函数
函数内部还要调用其他函数
叶子函数的堆栈调用:
void leafF()
{
int a=3;
int b=9;
}
int main(int argc, char * argv[]) {
@autoreleasepool {
leafF();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
leafF对应的汇编:
ARMTest`leafF:
-> 0x100ce66b0 <+0>: sub sp, sp, #0x10 ; =0x10 //sp堆栈指针sp=sp-16
0x100ce66b4 <+4>: orr w8, wzr, #0x3 //3赋值给w8
0x100ce66b8 <+8>: str w8, [sp, #0xc] //sp+12写入w8
0x100ce66bc <+12>: mov w8, #0x9 //9赋值给w8
0x100ce66c0 <+16>: str w8, [sp, #0x8] //sp+8写入w8
0x100ce66c4 <+20>: add sp, sp, #0x10 ; =0x10 //sp堆栈指针sp=sp+16
0x100ce66c8 <+24>: ret
说明:
上图就是模拟一段函数调用时的栈空间;
sub sp,sp #0x10 就是把起始的sp位置-16,目的是分配函数调用空间。
str w8, [sp, #0xc]和str w8, [sp, #0x8] 就是把sp+12和sp+8的内存分配给a,b(4个字节);
add sp,sp #0x10 就是把sp的位置+16,如果不+16,那么会导致内存被浪费,这里是局部变量,所以+16之后让sp回到起点,3,9分配的空间被忽略。
非叶子函数的堆栈调用:
void leafF()
{
int a=3;
int b=9;
}
void NOleafF()
{
int a=4;
int b=5;
leafF();
}
int main(int argc, char * argv[]) {
@autoreleasepool {
NOleafF();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
NOleafF对应的汇编:
ARMTest`NOleafF:
0x10466e6a0 <+0>: sub sp, sp, #0x20 ; =0x20 //sp堆栈指针sp=sp-32
0x10466e6a4 <+4>: stp x29, x30, [sp, #0x10] //x29(fp),x30(lr)寄存器顺序写入sp+16的地址
0x10466e6a8 <+8>: add x29, sp, #0x10 ; =0x10 //fp指针指向当前的fp区域
-> 0x10466e6ac <+12>: orr w8, wzr, #0x4
0x10466e6b0 <+16>: stur w8, [x29, #-0x4] //4写入x29-4
0x10466e6b4 <+20>: mov w8, #0x5
0x10466e6b8 <+24>: str w8, [sp, #0x8] //5写入sp+8
0x10466e6bc <+28>: bl 0x10466e684 ; leafF at main.m:13 //跳转到内部函数(leafF)执行段(叶子函数执行)
0x10466e6c0 <+32>: ldp x29, x30, [sp, #0x10] //从sp+16顺序读取出赋值给x29,x30
0x10466e6c4 <+36>: add sp, sp, #0x20 ; =0x20 //sp堆栈指针sp=sp+32
0x10466e6c8 <+40>: ret
说明:
stp x29, x30, [sp, #0x10] 函数开始时用sp-32这段内存空间写入该函数fp(堆栈信息),lr(函数返回)的地址
非叶子函数会跳转到其他函数体执行代码,很可能修改x29,x30寄存器中的值,所以函数跳转回来之后,需要执行
ldp x29, x30, [sp, #0x10] ,为了恢复当前函数x29,x30寄存器为之前存储fp,lr的值,达到堆栈平衡。
所以,也可以称sp寄存器为栈顶指针,fp寄存器为栈底指针。