-
前言
汇编语言是各种CPU提供的机器指令的助记符的集合,你们可以用汇编语言直接控制硬件系统进行工作。
学习汇编的主要目的,就是通过用汇编语言进行编程而深入地理解计算机底层的基本工作机理,达到可以随心所欲地控制计算机的目的。
第1章 基础知识
1.4 存储器
离开了内存,性能再好的CPU也无法工作。这就像再聪明的大脑,没有了记忆也无法进行思考。
1.5 指令和数据
指令和数据是应用上的概念。在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。
Tips:那怎么区分是数据还是程序?
1.6 存储单元
微机存储器的容量是以字节为最小单位来计算的。对于拥有128个存储单元的存储器,我们可以说,它的容量是128个字节。
1.7 CPU对存储器的读写
1.8 地址总线
1.9 数据总线
1.10 控制总线
小结
1.11 内存地址空间
1.12 主板
1.13 接口卡
1.14 各类存储器芯片
1.15 内存地址空间
第2章 寄存器
一个典型的CPU不是只会计算的,它是由 运算器、控制器、寄存器等器件构成,这些器件靠内部总线相连。简单地说,在CPU中:
- 运算器进行信息处理
- 寄存器进行信息存储
- 控制器控制各种期间进行工作
- 内部总线连接各种期间,在它们之前进行数据的传送
对汇编程序员来说,需要额外关注的是CPU中的寄存器,寄存器是程序员可控制的,可以写入寄存器中的不同指令来实现对CPU的控制。
不同的CPU,寄存器个数和结构是不同的,8086CPU有14个寄存器,每个寄存器都有一个名称。这些寄存器是:
- AX、BX、CX、DX(简称:ABCD):通用寄存器
- SI、DI
- SP、BP、IP
- CS、SS、DS、ES
- PSW
1 | AH&AL=AX(accumulator register):累加寄存器 |
补充:
1 | BX叫Base Register应该算是一个历史问题了。 |
2.1 通用寄存器
还是以 8086 CPU 为例,8086 CPU 的 所有
寄存器都是16位的,也就是说可以存放两个字节。
ABCD 这4个寄存器通常用来存放一般性的数据,被称为通用寄存器。
这里存在一个兼容性的问题:
8086CPU上一代的寄存器都是8位的,最新的是16位的,那怎么保证兼容呢?
聪明的你应该也想到了,把一个16位的拆成2个8位的寄存器不就可以了吗?确实也是这样做的:
H :high
L :low
- AX -> AH + AL
- BX -> BH + BL
- CX -> CH + CL
- DX -> DH + DL
2.2 字在寄存器中的存储
一个 字 就等于 两个字节,所以一个字可以存在一个寄存器中。
2.3 几条汇编指令
能存储 16位二进制 的寄存器,可以存储 4位十六进制 的寄存器,即:
2 ^ 16 = 16 ^ 4
2.4 物理地址
我们之前说过 CPU 访问内存单元时,CPU肯定是需要知道要访问对象的地址的,这个地址我们就称为物理地址。
那么有一个问题:CPU通过地址总线送入存储器,是物理地址没问题。那这个地址CPU是怎么知道的呢?
2.5 16位结构的CPU
我们经常听到某某架构支持16位、32位、64位,这里的位数到底是什么意思呢?
我们回归 CPU 价值的本身,CPU本身是用来计算的工具,如果CPU支持一次性计算的数值越大,说明CPU就越强大,那么这里的 16位,说的就是 CPU 可以一次性计算 16位的数据。
那么与之相匹配的,就是 传入 和 存储 也支持 16位的数据,总结下来就是:
- 运算器一次最多可以处理 16位 的数据
- 寄存器的最大宽度为16位
- 寄存器和运算器之前的通路为 16位。
那么还是回归 2.4 说的问题, 谁把 物理地址 告诉了 CPU?又是什么方式告诉CPU的?
2.6 8086 CPU 给出物理地址的方法
2.6.1 物理地址的来源
首先我们可以明白的是: CPU 获取的地址信息是 通过 数据总线 由外界传进来的,也即流程是:
- 数据总线传入 A的地址
- 控制总线在计算时发现需要 A的地址,就把 A的地址告诉 地址总线
- 地址总线 去访问对应的物理地址
- 得到的结果通过 数据总线 传回给控制总线用来做计算。
但这里我们即将要介绍一个新的不可思议的知识点: 2.5 部分我们说了,16位CPU 数据总线、控制总线、地址总线 都保持16位就可以达到 16位计算的效果了。
2.6.2 为什么要扩展地址总线
但实际上 8086CPU 有 20根地址总线 ,可以达到 1MB 的寻址能力,第一个问题来了,为什么要扩展地址总线的个数?老老实实用16根地址总线不可以吗?
其实增大地址总线的目的是想扩大内存,为什么增大地址会扩大内存呢?
因为如果一个地址总线寻址能力只有 64KB ,即使把内存扩展到 64G 也访问不到地址的。所以将地址总线提升到20根,相当于内存上限也提升到了1MB。
那么为什么要提升内存上限?我就用64KB不行吗?
这个问题问得其实有点业余,大家都知道,软件操作系统会占用大量的内存,如果想给用户提供好的操作流程,内存必须能支持软件操作系统的大小,不然就只能用老式的计算器操作了。
2.6.3 如何扩展地址总线
好,现在搞明白 8086将16根地址总线扩展到 20根地址总线 的意义,那么相继又来了一个问题,数据总线一次只能传输16位,地址总线20位的数据从哪里来呢?
8086 CPU 采用一种用两个 16位地址合成 20位物理地址的方法,流程是:
这里引出了三个名字:
- 地址加法器
- 段地址
- 偏移地址
地址加法器采用 : 段地址 x 16 + 偏移地址 = 物理地址 的方式合成物理地址,操作流程如下:
2.7 “段地址x16 + 偏移地址 = 物理地址”的本质含义
2.6 部分我们说了用 2个16位地址 构建 物理地址 的方式,但有人可能就不明白了,为什么要用”段地址x16 + 偏移地址 = 物理地址”这种方式来构建 物理地址 ,这是一种什么样的思想?
我们用这样一个例子来表达这个思想:
假设你现在在学校,你问我怎么去图书馆。
我可以直接告诉你 走2826米 就可以到图书馆。
但我现在说话的约束是:我不能说超过2000米的距离,所以我只能给你说:先走2000米到体育馆,然后走826米到图书馆。
实际上这种思想就是 “基础地质 + 偏移地址 = 物理地址”,通过段偏移的方式,扩大了允许的寻址范围。
2.8 段的概念
2.7 举的 图书馆示例,可能又会有同学说了:我也可以说先走 1000米,再走 1826米 到图书馆;不一定非得说 先走 2000米,再走826米 啊。
也就是说使用”基础地址 + 偏移地址”构成一个物理地址的方式有N种,那么微机是怎么确定”基础地址”的呢?而且段地址为什么叫 段 ?内存是分段了吗?
其实,内存没有分段,段的划分来自于 CPU。因为 16位 所能寻访的 内存是 64KB,所以我们会针对 64KB 以上的地址开始使用 段 的概念。
比如我们可以认为 :
- 10000H ~ 100FFH 的内存单元组成一个段,该段的起始地址为 10000H,段地址为 1000H, 大小为 100H。
因为偏移地址最大为16位,所以一个段的最大为64KB。
明白了段的概念后,相信也就解答你关于 “基础地址 + 偏移地址” 可以有N种方式拼出”物理地址”的疑惑了,也即:64kb以下的地址不用基础地址,64KB以上的地址,按64KB为一个单位,划分段。
2.9 段寄存器
CS、DS、SS、ES 四个S用于段寄存器(专门新增了段寄存器,用于扩大内存,是空间换时间的好例子)。
说到读址方式,CS和IP 是最关键的两个寄存器:
- CS(Code segment):代码段寄存器
- IP(Instruction Pointer):指令指针寄存器
在8086PC机中,任意时刻,设CS中的内容为M,IP中的内容为N,8086CPU将从内存 Mx16 + N 单元开始,读取一条指令并执行。
下面,我将用几张大图向大家描述 8086 CPU 读取和执行指令的流程:
简要描述 8086CPU 的工作过程就是:
(1)从 CS:IP 指向的内存单元读取指令,读取的指令进入指令缓冲区
(2)IP = IP + 所读取指令的长度,从而指向下一条指令
(3)执行指令。转到步骤(1),重复这个过程。
可能有小伙伴会问了,如果我的CPU刚刚启动,CS和IP都没有被赋值,怎么处理呢?
实际上, 8086CPU 在加电启动或复位后 ,CS和IP被重置为 CS=FFFFH,IP=0000H,即在 8086PC 机刚启动时,CPU 从内存 FFFF0H 单元中读取指令执行,FFFF0H 单元中的指令是 8086PC机 开机后执行的一条指令。
这时候可能会有小伙伴问出很久之前就想问的问题:怎么保证 CS:IP 指向的区域一定是 指令? 因为数据在内存中也是会存储的呀。
实际上 任何时候,CPU 使用 CS:IP 合成的都是指令地址,因为数据是被指令调用的,CS:IP 执行完一段指令后,跳向的是下一条指令的地址,而不是一个数据的地址。
2.11 修改 CS、IP 的指令
既然 CS:IP 那么强大,可能就有小伙伴想搞破坏了,我能不能通过指令修改 CS:IP 的值呢?
还真的有相应的汇编指令:jmp指令。
若想同时修改 CS、IP 的内容,可用 “jmp 段地址:偏移地址”的指令完成,比如:
jmp 2AE3:3,执行后:CS=2AE3H,IP=0003H,CPU将从 2AE33H 处读取指令。
Tips:使用jmp可以操控 CS:IP 指向的不是指令,而是数据吗?这样会有什么问题吗?
基本上我们操作 jmp 都是会明确跳转到哪个指令中,不会出现操作 十六进制 的情况出现,如果出现了,你要考虑下是否应该是用jmp了。因为官方也没说明 jmp 跳到 数据二进制中,会导致jmp失效,还是默认跳过相应数据二进制。
第3章 寄存器(内存访问)
3.1 内存中字的存储
一个字 = 2 个字节
内存单元 = 1 个字节 = 8位 = 2 ^ 8 = 16 ^ 2 = 2 个十六进制
Tips:1个字节等于8位,也就是 2^8 ,用十六进制就是 2个十六进制。
所以一个字在内存中要占2个单元。
3.2 DS 和 [address]
上面我们明白了 指令 读取的方式,那么数据该如何读取呢?
数据读取需要使用到 DS(data segment)寄存器,数据读取的方式有两点需要额外注意的:
- 8086CPU 并不支持将数据直接送入段进村器,ds是一个段寄存器,所以 mov ds,1000H 这条指令是非法的。
那么如何将 1000H 送入 ds 呢? 志浩用一个寄存器来进行中转,即先将 1000H 送入一个通用寄存器,比如 bx,再将 bx 中的内容送入 ds。
- mov al,[0] 的含义,[…]表示一个内存单元,我们知道,只有内存单元是不能定位的,所以执行时,8086CPU自动取 ds 中的数据为内存单元的 段地址。
目标:读取 10000H 单元的内容,可以用如下的程序段执行:
1 | move bx,1000H |
Tips:汇编指令就是在直接操作内存。
3.3 字的传送
因为 8086CPU 是16位结构,有16根数据线,所以可以一次性传送16位数据,也就是时候可以一次性传送一个字。
3.4 mov、add、sub指令
Tips:暂不关心,略。
3.5 数据段
Tips:暂不关心,略。
3.6 栈
简单描述了栈,已知,略。
3.7 CPU 提供的栈机制
现在的 CPU 都有栈的设计,栈也不是什么特殊的东西,栈也是内存的一部分体验,CPU会将一段内存当做栈来使用,最基本的两个指令是:push 和 pop。
push ax 表示将寄存器 ax 中的数据送入栈中,pop ax 表示将栈顶数据送入ax。
8086CPU 的入栈和出栈操作都是以字为单位进行的,比如下面这一段执行过程:
这时候会出现一个问题,CPU 如何知道 10000H~1000FH 这段空间被当作栈来使用了?
另外还有个问题,当执行 push 或者 pop 的时候,必须知道哪个单元是栈顶单元,可是这如何知道呢?
所以这里还需要有寄存器来记录栈的地址,在8086CPU中,需要的是 段寄存器SS 和 寄存器SP。任意时刻,SS:SP指向栈顶元素。
3.8 栈顶超界的问题
也即:既然栈是在内存中开辟的空间,那么怎么做才能保证栈不越界呢?
我们自然是希望CPU可以保证push/pop的时候不越界,但实际上CPU保证不了。需要我们自己编程时注意。
3.9 push 和 pop
涉及具体指令用法,知道作用即可,工程中暂时用不到,略过。
3.10 栈段
在汇编时使用 栈 的流程是:
- 开辟栈段(连续的内存单元)
- 使用SS:SP记录栈顶
- 使用 push 、 pop 操作栈
第4章 第一个程序
4.1 一个源程序从写出到执行的过程
写汇编 -> 编译 -> 连接 -> 执行
4.2 源程序
Tips:介绍了些汇编的一些技巧,暂时不用写汇编,看懂就好,略。
4.3 编辑源程序
Tips:介绍了编写汇编代码的流程,略。
第12章 内中断
Tips:之所以跨到了内中断,是因为在学操作系统的分片调度算法时,说到了中断相关的知识点,没有汇编的储备不好读懂,所以把这部分读完。
按我们上面描述的流程,CPU执行代码是按 CS:IP 流程决定执行指令的,但 CPU 也提供了一种能力:CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。
12.3 中断向量表
中断 这个概念,我们比较好理解,但这里有个问题是:
CPU 中断后要去执行的指令地址,从哪里来? 中断类型码 只有8位,怎么用 8位的中断类型码 推出相应的 指令的物理地址呢?
这时就要用中断向量表,中断向量表在内存中保存,其中存放着 256 个中断源所对应的中断处理程序的入口,如图:
所以,CPU通过中断类型码,然后查找中断向量表,就可以得到中断处理程序的入口地址。
那么继而又产生了一个问题,CPU如何找到中断向量表呢?
中断向量表在内存中存放,存放的地址是固定死的,不能变更。
对于8086PC机,中断向量表指定放在内存地址0处,从内存 0000:0000 到 0000:03FF 的 1024 个单元中存放着中断向量表。
12.4 中断过程
中断这个过程实际上是CPU硬件完成的,流程是:
- 接受到中断类型码,在中断向量表中找到中断处理程序的入口
- 将找到的入口设置 CS 和 IP,使 CPu 执行中断处理程序
当然了,CPU在执行完中断指令后,还需要回到中断前的指令继续执行,所以需要将原来的CS和IP的值保存起来。
第14章 端口
Tips:之所以看了这章,是因为之前一直觉得端口是计算机网络的知识,后来发现端口是汇编层的知识,太棒了,一起学了。
首先我们要思考的是,为什么需要端口?端口是用来干什么的?
背景是在PC机中,和CPU通过总线相连的芯片除了存储器外,还有各种接口卡、主板芯片,所以从CPU的角度,将这些寄存器当做端口,对他们进行统一编址,每一个端口在地址空间都有一个地址。
14.1 端口的读写
端口的读写有什么特殊呢?
还真挺特殊的额,端口的读写指令只有两条:in和out。
端口和进程的区别
用一个例子说明二者的区别
现在有一个手机,这个手机好比一个和外界通信的端口。
你现在想给你女朋友打电话,可是手机被占用了,你就开始大喊,谁拿了我的手机(翻译一下就是:哪个进程占用了这个端口)。于是你开始查找哪个进程占用了这个端口lsof -i:portID。
于是,你发现你老姐正在用你手机给她男朋友打电话。此时,这个端口就被占用了。使用手机的人就是进程。
那你现在急不可耐,咋办呢,抢过来嘛,好比杀死进程(kill -9 pid)。