1、概述
学习路线:
-
语言
-
进制
-
进制如何运算
-
二进制
-
数据宽度
-
有符号数和无符号数
-
原码反码补码
-
位运算
-
位运算计算
-
汇编
-
寄存器
-
内存
-
汇编指令
-
内存复制
-
堆栈的指令
-
汇编如何写函数
-
堆栈传参
-
堆栈平衡
2、机器语言
什么是机器语言?
# 我们目前主流的电子计算机!
状态: 0和1
# 最早的程序员:穿孔卡带!
# 需要解决的最基本的问题:
# 假如:
加 0100 0000
减 0100 1000
乘 0100 1000 0100 1000
除 0100 1000 1100 1000
这些加减乘除的机器语言能简化么?----用助记符!即汇编语言,把人能理解的语言转换成机器能够理解的语言。
加 INC -编译器-->0100 0000
减 DEC -编译器-->0100 1000
乘 MUL -编译器-->0100 1000 0100 1000
除 DIV -编译器-->0100 1000 1100 1000
因此,在人和程序的本质之间有隔阂,这就需要汇编的帮助。汇编一般用于底层的编写,如单片机…
再进一步:
C语言
加 A+B -编译器-->0100 0000
减 A-B -编译器-->0100 1000
乘 A*B -编译器-->0100 1000 0100 1000
除 A/B -编译器-->0100 1000 1100 1000
底层学习软件:VC6 OD
3、进制
3.1、定义
学习进制的障碍**--------10进制
人类天然的选择就是10进制,因为有10个指头。因此需要跳出固有的思维。
二进制
思想:每一种进制都是完美的,都有自己的计算方式!
进制?
-
1进制: 一进一,结绳记事。1 1
-
2进制: 二进一,计算机
-
八进制: 八进一。8个符号组成:0 1 2 3 4 5 6 7
-
10进制: 10进一。10个符号组成:0 1 2 3 4 5 6 7 8 9
-
16进制: 16进一。16个符号组成:0 1 2 3 4 5 6 7 8 9 a b c d e f
测试
# 1进制: 1~20
1
1 1
1 1 1
...
# 3进制 1~20
0 1 2
10 11 12
20 21 22
100 101 102
110 111 112
# 7进制 1~20
0 1 2 3 4 5 6
10 11 12 13 14 15 16
20 21 22 23 24 25 26
30 ...
# 2进制 1~20
0 1
10 11
100 101
110 111
1000
问题:你真的理解进制了吗? 1+1=3对吗?! 如果你可以使用进制来解答这个问题,那么你就学会了!
十进制:0 1 2 3 4 5 6 7 8 9
狂神的十进制: 0 2 4 7 8 a b r d f,可以自己随便定义的,学习,创造者!
加密解密:程序员,破解程序的人! 进制的加密
数字量一大,总是有规律的!
3.2、进制的运算
运算的本质就是查数。
# 八进制的运算
0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 27
# 则八进制简单计算
2+3=5 2往后数3个数是5
2*3=6
4+5=11 4往后数5个数是11
4*5=24 1 2 3 4是1个4,5 6 7 10是2个4,21 22 23 24是5个4
# 八进制复杂计算
277+333=
276*54=
237-54=
234/4=
八进制加法表:
1+1=2 | ||||||
---|---|---|---|---|---|---|
1+2=3 | 2+2=4 | |||||
1+3=4 | 2+3=5 | 3+3=6 | ||||
1+4=5 | 2+4=6 | 3+4=7 | 4+4=10 | |||
1+5=6 | 2+5=7 | 3+5=10 | 4+5=11 | 5+5=12 | ||
1+6=7 | 2+6=10 | 3+6=11 | 4+6=12 | 5+6=13 | 6+6=14 | |
1+7=10 | 2+7=11 | 3+7=12 | 4+7=13 | 5+7=14 | 6+7=15 | 7+7=16 |
八进制乘法表:
1*1=1 | 1*2=2 | 1*3=3 | 1*4=4 | 1*5=5 | 1*6=6 | 1*7=7 |
---|---|---|---|---|---|---|
2*2=4 | 2*3=6 | 2*4=10 | 2*5=12 | 2*6=14 | 2*7=16 | |
3*3=11 | 3*4=14 | 3*5=17 | 3*6=22 | 3*7=25 | ||
4*4=20 | 4*5=24 | 4*6=30 | 4*7=34 | |||
5*5=31 | 5*6=36 | 5*7=43 | ||||
6*6=44 | 6*7=52 | |||||
7*7=61 |
# 加法 277+333=
277
333 +
------
632
===================================================
# 乘法 276*54=
276
54 *
------
1370
1666 +
--------
20250
====================================
# 运算的本质就是查数
# 减法 本质其实是加法 237-54=237+(-54)
237
-54
-----
163
# 除法 除数乘以那个数最接近结果即可!
234
4 /
-------
23/4=4···3
34/4=7
即234/4=47
结论:无论是什么进制,本身都是有一套完美的运算体系的,我们都可以通过列表的方式将它计算出来!
4、二进制
计算机使用二进制 0 1—>表示状态!目前的计算机都是电子计算机!
物理极限:摩尔定律–>当价格不变时,集成电路上可容纳的晶体管数目,约每隔18个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18个月翻两倍以上。这一定律揭示了信息技术进步的速度。 硬操作!
当物理极限达到以后,就会追求语言的极限—>并发语言! 软操作!
量子计算机:(传道)
- 可以实现量子计算的机器。
传统的计算机:集成电路!!0 1 。硅晶片!
量子计算机的单位:昆比特。(量子比特!)量子的两态来表示。
光子:正交偏振方向。
磁场:电子的自旋方向。
回到电子计算机: 0 1
# 二进制
0 1 10 11 100 101 110 111 1000 1001 1010 1011 1100 1101 1110 1111
# 二进制的标志位
2 10
4 100
8 1000
16 10000
二进制这么写很麻烦,能否简写?
0 1 2 3 4 5 6 7 8 9 a b c d e f
即1111用f表示,1110用e表示
这就是十六进制。
为什么要学习理解二进制?
寄存器、内存、位! 底层的每一个位都是有含义的。汇编入门理解的基础!
汇编高级:了解程序的深层!操作系统的内核?
5、数据宽度
C、C++、Java都需要定义数据的类型,因为计算机需要我们给这些数据定义宽度。
位 0 1
字节 8位 0~0xFF 0x表示十六进制 F表示1111
字 16位 0~0xFFFF
双字 32位 0~0xFFFFFFFF
在计算机中,每一个数据都需要定义类型,目的是定义它的宽度,在内存中的宽度。
6、有符号数和无符号数
数据都是有宽度的。每个数据代表什么意思呢?
0 1 0 1 0 1 这表示的是什么意思?
因此,需要定制规则来表示数据代表的含义。
无符号数规则
数字是什么,就表示什么
二进制:1001 1010 十六进制:0x9A 十进制:154
有符号数规则
最高位是符号位:1(负数) 0(正数)
二进制:1001 1010 该如何转换?
7、原码反码补码
编码规则
有符号数的编码规则
原码:最高位为符号位,对其它的位进行本身绝对值即可。
反码:
- 正数:反码和原码相同
- 负数:符号位一定是1。其余位对原码取反。
补码:
- 正数:反码和原码相同
- 负数:符号位一定是1。反码+1
# 测试
# 以下都是8位的二进制数
# 如果是正数,那都是一样的
1
# 原码 0000 0001
# 反码 0000 0001
# 补码 0000 0001
-1
# 原码 1000 0001
# 反码 1111 1110 除符号位外其余位取反
# 补码 1111 1111 反码+1
-7
# 原码 1000 0111
# 反码 1111 1000 除符号位外其余位取反
# 补码 1111 1001 反码+1
如果看到一个数字,二进制的,需要了解它是有符号数还是无符号数。
8、位运算
计算机现在可以存储所有的数字(整数、浮点数、字符),进行运算。
存储数字的时候,会有补码的形式存储。
如果可以把这些数字加以运算,我们就可以做到世界上的一切。无论多复杂的运算,底层都是加减乘除。我们只要把位运算的位如何操作运算记住、突破就可以了。
首先有一个面试高频题:2*8最高效的计算方式?
这道题不论怎样都非常慢,只有通过位运算才是最快的,比如左移、右移。而且要记住一句话:很多底层的调试器(例如C语言调试器),当我们手写调试器的时候就需要通过位来判断CPU的状态。
位运算就是我们常说的与或非 异或运算等…我们一个一个来看:
与运算:
在JAVA语言中用 & 符号去表示,但是在汇编中用 and 代表与。下面图片方便我们的理解:
1011 0001
1101 1000
-------------- 与运算的结果
1001 0000
或运算:
在JAVA语言中用(|)表示,在汇编语言中用or表示,同样根据或运算也有一张电路图可以帮助理解:
1011 0001
1101 1000
--------------- 或运算
1111 1001
异或运算:
在我们JAVA语言中用(^)表示,在汇编语言中xor表示。说白了记住一句话:不一样就是1。再来一张电路图理解:
1011 0001
1101 1000
--------------------异或运算
0110 1001
非运算(单目运算符):
我们所谓的取反(非),在JAVA语言中是(!),在C语言中是(~),在汇编语言中是not。
说白了一句话:0就是1,1就是0。
1101 1000
----------------- 非运算
0010 0111
通过这些可以完成我们的加减乘除。怎么通过位运算实现加减乘除呢?
位运算:
它是一个移动位,分为左移,右移。(在10进制中,左移相当于 *2,右移相当于 /2)。
左移(shl <<):
0000 0001 所有的二进制位全部左移若干位,高位就丢弃,低位补0
0000 0010
右移(shr >>):
0000 0001 所有二进制全部右移若干位,低位就丢弃,高位补0或1(根据符号位决定,负数补1,正数补0)
0000 0000
9、位运算的加减乘除
接下来我们讲,如何通过位运算实现加减乘除。我们的计算机只认识0和1,但是基本的数学是建立在加减乘除上。
案例:
4+5=?
十进制是9,那么计算机是怎么做的?
# 计算机操作步骤
0000 0100
0000 0101
----------- 人为可以直接加法操作,但计算机不会,只能进行与、或、异或运算
0000 1001
================================
# 那么计算机的实现原理是什么呢?
# 怎么将两个数加起来?核心是:异或。
# 第一步,异或(不一样为1):如果不考虑进位,异或就可以直接出结果
0000 0100
0000 0101
--------------
0000 0001
# 第二步,计算机在这个异或的结果上再做与运算操作:
# 与运算(判断进位),如果与运算结果为0,那么就没有进位。
0000 0100
0000 0101
--------------
0000 0100 结果中有1,说明有进位
# 第三步,将第二步与运算的结果左移一位:
0000 0100--->0000 1000
# 第四步,异或 将第一步异或的结果与第三步与运算的结果进行异或运算
0000 0001
0000 1000
------------
0000 1001
#第五步,继续与运算判断有没有进位
0000 0001
0000 1000 上一步异或的数
------------
0000 0000 结果都为0,没有进位了
# 则最终计算结果为,与运算结果都为0的上一步异或运算结果:
0000 1001 此结果即为十进制的9
4-5=?
# 计算机操作步骤
# -5 原码:1000 0101 反码:1111 1010 补码:1111 1011
0000 0100
1111 1011
---------- 直接加
1111 1111 ff
# 第一步:异或
0000 0100
1111 1011
----------
1111 1111
# 第二步:与运算
0000 0100
1111 1011
----------
0000 0000 没有进位
# 结果为:1111 1111 16进制表示:ff 10进制表示:-1
# 因为计算机是以补码的形式存储数字的,因此转回原码:1111 1111(补码)-->1111 1110(反码)-->1000 0001(原码)
9、汇编语言环境说明
目前为止,我们可以从零设计一套自己的进制规则。自己设计电路来实现加减乘除。但是最终乘除的结果是一个二进制,例如:我们有32个灯泡,就可以显示32个数值,最终亮起灯泡的数就是最终的结果。手动转换这个结果和值!(十进制和二进制的转换)
机器语言并不会做很多事情,它很“笨”。机器语言说白了就是位运算,(加减乘除)
都是电路来实现的。这就是计算机最底层的本质。
但是,我们发现,学完了这些东西依旧是不懂,只是对现在的程序做了一些提高的理解。但是想通过理解写程序写不出来,难道我们真的写不出来吗?
通过机器语言来实现加法计算器,这就是设计电路。
我们通过指令来代替我们的一些二进制编码!比如说:刚才的加法运算是通过各种操作能否通过一个符号计算呢?比如说我就想叫它(ADD指令),假设我给计算机发一个ADD指令,它通过识别我的指令转换成机器语言(也就是编译)ADD指令转换为二进制操作。
汇编语言说白了,底层还是二进制,但是二进制写起来太麻烦了。这个时候我们通过汇编指令给计算机发一些操作,然后让计算机执行。这个地方就要涉及到编译器!因为我们说的编译命令不是机器直接能识别的,需要把命令转码,转成计算机认识的信息,不然没法识别。这个时候就涉及到编译器的发展。
如果学底层,一定不要用特别智能的编译器(IDEA,VSCODE等),就是用越远古的越好(记事本,vim等)。很多人学习C语言使用,用的是vim编辑器去写C语言,用gcc来执行。这是学习C的正确方式。底层的大佬几乎都是最原始的idea。
在学习汇编之前,先要掌握环境的配置:
-
Vc6(程序到汇编的理解,通过C语言实现)
-
OD
-
抓包工具
-
加密解密工具
尽量不要使用java去学汇编,学完了汇编去学jvm就会觉得很简单。但是如果我学java再学汇编就有点痛苦,建议使用C++学汇编。因为C++可以直接看到内存的地址,可以打印通过指针直接找到内存的地址,通过地址判断信息。
学汇编不是为了写代码,就是为了理解程序的本质。
如果懂汇编,就能够理解所有复杂的概念。
如果我们是一个做应用的程序员,别人调试不了的程序,如果学过汇编,都可以调试。因为知道底层堆栈到底做了什么。如果是做安全的(反外挂,反病毒),就要理解汇编到二进制的全部知识。
现在的计算机至少是32位,还有的是64位。我们要知道,它是由32位演化过来的。底层架构没有发生变化,只是多了寄存器,主要是寻址能力增加。
汇编入门: 了解汇编和程序的对应关系,程序的本质即可。
学会了这些,不理解的java原码就理解了。汇编非常重要!这对我们向上学习有很大的帮助。有些编程技术学进不去,很大原因就是因为底层不够。底层精通了在学习编程语言就发现太轻松了!
10、寄存器的理解
学习汇编,要学习三个重要的概念:
-
寄存器
-
内存
-
汇编指令
通用寄存器:可以存储任何值
存储数据:CPU–>内存–>硬盘
CPU分为32位和64位。
-
32位:8 16 32
-
64位:8 16 32 64
32位的通用寄存器只有8个:EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI、EIP
寄存器中的存值范围:0 ~ FFFFFFFF
计算机如何向寄存器中存值呢?
对于二进制来说,就是直接修改值。但是修改值需要找到对应的位置,所以才有内存地址。
mov指令
mov 存的地址,存的数 作用:可以将数字写入到寄存器
mov 存的地址1,存的地址1 作用:可以将寄存器中的值,写到寄存器。
计算机的本质:计算力! 就是为了计算!(鼠标光标在移动都是在计算)
32位代表八个F(FFFF FFFF),一个F代表4位(1111)
FFFF FF
32位 16位 8位
EAX AX AL
ECX CX CL
EDX DX DL
EBX BX BL
ESP SP AH
EBP BP CH
ESI SI DH
EDI DI BH
对于8位:L代表低8位,H代表高8位
16位是FFFF 高八位占前两个FF,低八位占后两个FF
除了这些通用寄存器之外,其他的寄存器每一位都有自己特定的功能(比如:开机关机)。
我们一般写值都会写到通用寄存器中。
11、内存
寄存器很小,而且不够用。所以我们会把数据放到内存中。
有句话:每个应用程序进程都有4GB的内存空间。 程序用的内存就是空头支票,虽然每个应用程序的进程都有4GB内存空间,但是真正到机器上使用时候并没有那么大。
程序真正运行的时候,才会用到物理内存。
1B = 8bit
1KB = 1024B
1MB = 1024KB
1GB = 1024MB
假设是4GB内存电脑,就等于4096m => 最终计算为位,就是可以存储的最大容量。
计算机中的内存地址很多,空间很大。
内存地址:
存一个数:占用大小,数据宽度。存到哪里呢?
计算机中的内存地址很多,空间很大。我们要给空间取名字,每个空间分配一个地址,名字。
这些给内存起的编号就是我们的内存地址。32位(8个十六进制的值)
32位:决定寻址能力!
FFFFFFFF + 1 = 100000000,用十六进制表示的能够存储的最大的值。
位是怎么限制内存大小呢?
100000000 内存地址 * 8位 :800000000。 1个字节占8个位,所以乘8
转换为十进制并/8(除8是为了把位转换成字节):4,294,967,296字节。
4,294,967,296/1024=4,194,304KB/1024=4096MB/1024=4GB
按照规则/1024最终发现就是 4GB,因此32位
64位
FFFFFFFFFFFFFFFF + 1 =
理论上能装128GB内存,但受物理因素影响,最多只能装到64GB
所以每个内存地址都有一个编号:可以通过编号向里面存值
很多人学C语言搞不懂指针,原因就是 不懂内存。
内存如何存值?(mov指令)
存值需要知道数据宽度:byte word dword
地址位置:0xFFFFFFFF
不是任意的地址都可以写东西,只有程序申请过的内存地址我们才可以使用。
# 汇编如何向内存中写值:
mov 数据宽度 内存地址,要存储的值
mov byte ptr ds:[0x19ff70],1 # ptr ds:[]是固定写法
# 传递的值的大小一定要和数据宽度要相等,如果大放不进去。
内存地址有多种写法:
ds:[0x19FF70+4](内存地址偏移):加 偏移(4),地址就变成了:0x19FF74
ds:[eax](寄存器):把寄存器中的值放到内存中。
ds:[eax + 4](寄存器偏移)
以数组为例:
ds:[reg + reg * {1,2,4,8}]
ds:[reg + reg * {1,2,4,8} + 4] 偏移