越底层越单纯!真正的程序员都需要了解的一门非常重要的语言,汇编!
机器语言
- 我们所写的语言最终安装在机器上的是什么东西?
- 是机器语言,一堆的0和1.
如0101 001 1101 0110,由0和1组成的机器指令,说白了还是电信号.
这些指令让我们的cup执行
汇编语言
- 使用符号代替机器语言,也称符号语言.如 mov ax,bx
- 汇编和机器指令是一一对应的,每一条机器指令都有与之对应的汇编指令
- 汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
- 高级语言可以通过编译得到汇编语言,但机器语言/汇编语言不能还原成高级语言
高级语言
- 我们平时写的Oc/swift/C/C++都属于高级语言,更加接近人类的自然语言
汇编语言的特点
- 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能
- 能够不受编译器的限制,对生成的二进制代码进行完全的控制
- 目标代码简短,占用内存少,执行速度快
- 每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
- 知识点过多,开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护
- 不区分大小写,比如mov和MOV是一样的
- 在汇编中,大部分指令都是和CPU与内存相关的
汇编的用途
- 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
- 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编)
- 软件安全
- 病毒分析与防治
- 逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
哇...破解 哇...外挂 哇...黑客 来自小学生的惊叹 前段时间我弟弟让我帮他盗号...! 我很无奈啊有木有...😒
- 理解整个计算机系统的最佳起点和最有效途径
- 为编写高效代码打下基础
- 弄清代码的本质
- 函数的本质究竟是什么?
- sizeof
- ++a + ++a + ++a 底层如何执行的?
- 编译器到底帮我们干了什么?
- DEBUG模式和RELEASE模式有什么关键的地方被我们忽略
汇编语言的种类
- 讨论最多的汇编语言
- 8086汇编(8086处理器是16bit的CPU)
- Win32汇编
- Win64汇编
- ARM汇编(嵌入式、Mac、iOS)
- ......
架构 | 设备 |
---|---|
armv6 | iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch |
armv7 | iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4 |
armv7s | iPhone5, iPhone5C, iPad4(iPad with Retina Display) |
armv64 | iPhone6s , iphone6s plus,iPhone6, iPhone6 plus,iPhone5S ,iPad Air, iPad mini2 |
总线
- 每一个CPU芯片都有许多管脚,这些管脚和总线相连,CPU通过总线跟外部器件进行交互
- 总线:一根根导线的集合
- 总线的分类
- 地址总线
- 数据总线
- 控制总线
地址总线
- 它的宽度决定了CPU的寻址能力
- 8086的地址总线宽度是20,所以寻址能力是1M( 2^20 )
2 ^ 10 == 1024
2 ^ 20 == 2 ^ 10 * 2 ^ 10 = 1024 * 1024 = 1M
16^5 == 2^4^5 == 2^(4*5) == 2 ^ 20
2^20 == 1M
2^30 == 1G
2^32 == 1G*2^2 == 4G
数据总线
- 它的宽度决定了cup单次传输数据的大小,也就是数据传送速度
- 数据总线直接影响cpu的吞吐量
- 8086数据总线宽度是16,所以单次最大传送2个字节的数据.
- 1根数据总线代表一个bit位
十根 = 1KB
每一个16进制位代表4个bit,因为两个16进制位位代表一个字节,一个字节8个bit
一个16进制位 = 4 bit
两个16进制位 = 1字节
1字节 = 8bit
一个字节 = 8个bit = 2个16进制位
一个字 = 2个字节 (分别为高字节和低字节)
1 Byte == 8 bit
1B == 1Byte == 一个字节
1KB == 1024 Byte
8KB == 1024 * 2^3
1MB == 1024 * 1024 Byte
控制总线
- 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制
练习
- 一个CPU 的寻址能力为8KB,那么它的地址总线的宽度为 (
13
)
寻址能力均是以字节为单元的
1KB = 10根线
1KB = 1024Byte
8=2^3
1024=2^10
8KB = 8*1024=2^3*2^10=2^(3+10)=2^13
- 8080,8088,80286,80386 的地址总线宽度分别为16根,20根,24根,32根.那么他们的寻址能力分别为多少(
64
)KB, (1
)MB
16根 = 2^6 = 64KB
20根 = 2^20 = 1MB
24根 = 2^24 = 2^20 * 2^4 = 16MB
32根 = 2^32 = 1G*2^2 = 4G
- 从内存中读取1024字节的数据,8086至少要读(
512
)次,80386至少要读取(256
)次.
8086数据总线宽度是16,单次最大传送2个字节的数据
```
#寄存器
* 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
* 不同的CPU,寄存器的个数、结构是不相同的(8086是16位结构的CPU)
* 8086有14个寄存器
* 都是16位寄存器,可以放两个字节
![8086内部寄存器](http://upload-images.jianshu.io/upload_images/6990647-3392e0666c641b12.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#####通用寄存器
* AX、BX、CX、DX这4个寄存器通常用来存放一般性的数据,称为通用寄存器(有时也有特定用途)
* 通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
* 假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间
![](http://upload-images.jianshu.io/upload_images/6990647-aaa413d7a51629bc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
* 过程
* cpu通过地址总线找到红色内存空间
* 再通过控制总线告诉内存条我要读
- 再通过数据总线读给cpu(cpu通过运算器+1 )
- 在通过cpu通过地址总线找到蓝色内存
- 通过控制总线告诉内存我要写
- 通过数据总线把值传给蓝色内存
* 代码过程
- mov ax,红色内存空间
- add ax,1
- mov 蓝色内存,ax
- 上一代8086的寄存器都是8位的,为了保证兼容, AX、BX、CX、DX都可分为2个独立的8位寄存器来使用
- H代表高位寄存器
- L代表低位寄存器
![](http://upload-images.jianshu.io/upload_images/6990647-608b1e8abe5aa552.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![](http://upload-images.jianshu.io/upload_images/6990647-0775d3c4209e7cbe.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#8086的寻址方式
* CPU访问内存单元时,要给出内存单元的地址,所有的内存单元都有唯一的地址,叫做物理地址
* 8086有20位地址总线,可以传送20位的地址,1M的寻址能力
* 但它又是16位结构的CPU,它内部能够一次性处理、传输、暂时存储的地址为16位。如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出来的寻址能力只有64KB
**8086采用一种在内部用2个16位地址合成的方法来生成1个20位的物理地址**
![](http://upload-images.jianshu.io/upload_images/6990647-423c026e836e94ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![](http://upload-images.jianshu.io/upload_images/6990647-27dfee5fbe7449b3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
#内存分段管理
- 8086是用“基础地址(段地址×16) + 偏移地址 = 物理地址”的方式给出物理地址
- 在编程时可以根据需要,将若干连续地址的内存单元看做一个段,用段地址×16定为段的起始地址(基础地址),用偏移地址定位段中的内存单元
#段寄存器
* 8086有4个段寄存器:CS、DS、SS、ES,当CPU需要访问内存时由这4个段寄存器提供内存单元的段地址
- CS (Code Segment):代码段寄存器
- DS (Data Segment):数据段寄存器
- SS (Stack Segment):堆栈段寄存器
- ES (Extra Segment):附加段寄存器
#CS和IP
- CS为代码段寄存器,IP为指令指针寄存器,它们指示了CPU当前要读取指令的地址
- 任意时刻,8086CPU都会将CS:IP指向的指令作为下一条需要取出执行的指令 (指向下一条指令的地址)
#指令和数据
* 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
- CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义
- CPU根据什么将内存中的信息看做指令?
- CPU将CS:IP指向的内存单元的内容看做指令
- 如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被CS:IP指向过
#jmp指令<修改cs、ip的值>
- CPU从何处执行指令是由CS、IP中的内容决定的,我们可以通过改变CS、IP的内容来控制CPU执行目标指令
- 8086提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如
- mov ax,10、mov bx,20、mov cx,30、mov dx,40
- 但是,mov指令不能用于设置CS、IP的值,8086没有提供这样的功能
- 8086提供了另外的指令来修改CS、IP的值,这些指令统称为转移指令,最简单的是jmp指令
#DS和[address]
- CPU要读写一个内存单元时,必须要先给出这个内存单元的地址,在8086中,内存地址由段地址和偏移地址组成
- 8086中有一个DS段寄存器,通常用来存放要访问数据的段地址
```
mov bx,1000H
mov ds,bx
mov al,[0]
```
- 上面3条指令的作用将10000H(1000:0)中的内存数据赋值到al寄存器中
- mov al,[address]的意思将DS:address中的内存数据赋值到al寄存器中
- 由于al是8位寄存器,所以是将一个字节的数据赋值给al寄存器
- 8086不支持将数据直接送入段寄存器中,mov ds,1000H是错误的
---
** 写几条指令,将al中的数据送入内存单元1000H中 **
```
mov bx,1000H
mov ds,bx
mov [0],al
```
>注意:“mov 内存单元, 内存单元”是不允许的,比如mov[0], [1]
#大小端
- 大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中(高低\低高)(Big Endian)
- 小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中(高高\低低) (Little Endian)
>注意:ARM既可以工作在大端模式,也可以工作在小端模式
#栈
![表情最丰富的羽毛球](http://upload-images.jianshu.io/upload_images/6990647-4f53a210562f1591.gif?imageMogr2/auto-orient/strip)
- 栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)
- 8086会将SS作为栈段的段地址,任意时刻,SS:SP指向栈顶元素(偏移地址)
- 8086提供了PUSH(入栈)和POP(出栈)指令来操作栈段的数据
- 比如push ax是将ax的数据入栈,pop ax是将栈顶的数据送入ax
![](http://upload-images.jianshu.io/upload_images/6990647-c89e59550c287bca.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
>栈是没有底的,底是我们自己想象的,栈顶是有的,就是sp指针
内存里是绝对有数据的,哪怕是00它也是数据,push相当于改变内存里的数据,我们平时所说的新开辟一块内存空间,里面没有数据并不是里面就是0000,而是对于我们来说,这里面我可以改它而不去读它,读的话就读了一个野指针,读的这块野指针已经被释放了,所谓的野指针报错,就是我们的地址被释放了,里面并没有存任何的数据,这时候去拿野指针里的面的东西,我不认识!这叫野.😏
- 栈大小可以叫内存的大小吗?
- 8086 栈大小只有64K
- 内存区域里越往高地址走,我们系统自带的一些内容不让我们修改(系统不让我们修改的内容在高地址)
- pop的时候sp是加(也就是往高地址走)
>pop和push只是偏移我们的sp指针
pop仅仅是读数据,并没有改,把sp偏移的数据读出来
push是改数据,把sp偏移的内存改数据
pop越读越大
push越改越小
###栈越界
- 当栈满的时候在使用push指令入栈,或栈空的时候再使用pop指令出栈,都将发生栈顶超界的问题
- push超界比pop越界要危险的多,因为pop只是读,而push是改,一旦push越界他有可能把别人的数据改掉(我这个app本来好好的你过来把我的数据改了,那这个app就完蛋了)
>栈以外的地址很有可能存放着具有其他用途的数据,代码等.这些数据代码可能在我们的程序中,也有可能在别的程序中(但是由于我们入栈时不小心修改这些代码,数据,会引发一连串的错误)
#栈段
- 对于8086来说,在编程时,可以根据需要,将一组内存单元定义为一个段
- 我们可以将一组长度为N(N<=64KB)、地址连续、起始地址为16倍数的内存单元,当做栈空间来使用,称为栈段。比如用10010H~1001FH这段内存空间当做栈来使用,我们就可以认为10010H~1001FH是一个栈段,它的段地址为1001H,长度为16字节
- 如何使用push、pop等栈操作指令访问我们定义的栈段
- 用SS存放栈段的段地址,用SP存放栈顶的偏移地址
#Loop指令
- loop指令和cx寄存器配合使用,用于循环操作类似高级语言的for,while
- 使用格式
```
mov cx,循环次数
标号:
循环执行的程序代码
loop 标号
```
- loop指令执行流程
步骤1 先将cx寄存器的值 - 1, cx = cx - 1
步骤2 判断cx 的值
- 如果不为零执行标号的代码,又执行 步骤 1
- 如果为零执行loop后面的代码
>补充:
获取数据,除了通过ds段来获取.还可以利用其它段地址来获取
mov ax,ds:[0]
mov ax,cs:[0]
mov ax,ss:[0]
mov ax,es:[0]
#8086伪指令
- db(define byte) 自定义字节
- dw(define word)自定义字
#Call和ret指令
- call指令 (相当于执行一个函数)
- call标号
- 将下一条指令的偏移地址入栈
- 跳转到定位的地址执行指令!
- ret指令
- ret指令就是将栈顶的值POP给IP,也就是执行下一条指令
- 栈顶得值是什么?
- 下一条指令的偏移地址
###**此文章用作学习笔记**