汇编
汇编
机器语言上面的最底层语言
参考笔记 : https://fishc.com.cn/thread-7043-1-1.html
计算机组成原理
计算机发展史:
- 电子管时代 笨重 速度慢 电子管是可见的(so big) 第一台计算机是二战期间为了计算导弹飞机的运行轨迹
- 晶体管时代 瘦小 贝尔实验室的三个科学家发明了晶体管 第一次装备了显示器
- 集成电路 近代计算机
- 超大规模继承电路计算机 多核 CPU
计算机体系结构
冯诺依曼 将程序指令和数据一起存储的计算机设计概念结构(把程序存储起来并设计通用电路) 解决了一台计算机执行不同程序需要设计不同电路的问题
运算器 控制器 存储器 输入 输出
计算机总线:
- 片内总线 : 芯片内部的总线;寄存器与寄存器之间;寄存器与控制器、运算器之间 -- 高集成度芯片内部的信息传输线
- 系统总线: CPU 主存 硬盘 IO 设备 USB PCI 插槽 显卡声卡 等设备的信息传输总线 -- 分成三大类(数据 地址 控制)
怎么设计的 就和网络结构中的 直线型 一样 系统总线如下
CPU 主存
| |
-------总线总线总线总线总线总线总线总线总线总线总线-----
| | | | |
键盘 鼠标 硬盘 USB 插槽 PCI 等
总线仲裁(和计算机发送数据一样 CSMA/CD): 仲裁控制器(分析设备使用总线优先级)
比如主存要和键盘和鼠标交换数据,谁占用总线呢?? 解决总线使用权冲突问题
方法:
- 链式查询 连接到总线上的设备通过允许使用线连接,A 设备不使用总线,就会允许下一个设备使用总线 (电路复杂度低,仲裁方式简单, 优先级低的设备难以获得总线使用权, 对电路故障敏感)
- 计数器定时查询 仲裁控制器接收到信号之后,给总线中发送允许获取总线权的设备号 从 0 开始,0 设备接收到判断是不是自己发的请求,不是就告诉仲裁器不是我的 计数器+1 发送 1 给总线
- 独立请求
计算机的输入输出设备
输入:
- 字符输入设备
- 图像输入设备
CPU 和 IO 设备通信方式
输入输出设备触发程序中断信号给 CPU CPU 转去执行指定的中断处理代码,执行完成再继续之前的操作
外部设备提供通知 CPU 的一种异步的方式,CPU 高速运转的同时兼顾低速设备的响应
但是老是打断 CPU 也不是办法,出现了 DMA 在主存和 IO 设备之间,控制主存和外部设备的交互
计算机存储
缓存(CPU 高速缓存/寄存器)
主存 各种设备的 RAM
辅村(磁盘 U 盘)
| |
CPU----高速缓存----主存----辅存
| |
都是使用局部性原理
高速缓存存储数据
字(字长,字占多少位) 字块(由多个字组成)
字的地址两部分
前 m 位指定字块的地址;后 b 位指定字在字块中的地址
高速缓存的性能指标,命中率 高速缓存的替换策略
- 随机置换
- FIFO 先进先出
- LFU 最不经常使用 需要额外空间记录使用频率
- LRU 最近最少使用 多种实现方式,一般使用双向链表 最新使用的放入链表头,或者已经存在链表中的提到最前面
机器指令:
形式: 操作码(进行哪种操作)+地址码(给出操作数或者操作数的地址,可能出现 1 个地址,2 个地址,三个地址)
三地址指令: 操作指令 OP 地址 1 地址 2 地址三 比如 1 + 2 结果放到 3 中
二地址指令: 两个操作数结果放到某个地址中
一地址指令: 自己对自己操作
零地址指令: 空操作 停机操作 中断返回操作
操作类型:
数据传输 算数逻辑操作 移位操作 控制指令
机器指令的寻址方式:
顺序寻址: CPU 执行的指令是按照程序的生成的指令的顺序执行的
跳跃寻址: JMP 调到某个地方去执行
数据寻址方式:
指令执行过程:
取指令 -- 分析指令 --执行指令
指令和数据缓存到 CPU 的高速缓存 程序计数器指向第一个要执行的指令地址,取出指令操作码地址码需要取数据的话导数据缓存中/地址总线的主存中取 到指令寄存器 发送到指令译码器 同时指令计数器指向下一条指令的起始地址 译码器翻译发送控制信号给运算器 数据装载到寄存器 ALU 计算数据, 状态寄存器记录运算状态,如果进位,或者溢出记录, 送出运算结果
https://www.bilibili.com/video/BV1zW411n79C?p=10
https://www.bilibili.com/video/BV1rV411k7Xf?p=17
控制器和运算器并没有同时工作,效率低
有了 CPU 流水线设计: 分析执行同时取指令分析
汇编语言
编程语言是汇编语言 ; 汇编语言可以翻译成可直接执行的机器语言; 完成翻译的过程的程序就是汇编器
汇编指令是及其指令便于记忆的书写方式
汇编指令和机器指令是一对一的关系 所有可以通过汇编进行反编译
汇编指令:
MOV AX BX 寄存器 BX 内容送到 AX 寄存器(CPU 里面可以存储数据的器件,比二级缓存低)中
汇编语言的组成:
- 汇编指令
- 伪指令(由编译器执行)
- 其他符号(编译器识别 + - * / 等)
存储器 CPU 是计算机的核心,但是想让 CPU 工作,必须向他提供指令和数据(存放在存储器中--内存/显存/网卡存储/BIOS 存...设备都是有内存的)
指令和数据都是二进制信息,把一个二进制数据可以看成是数据,也可以是指令
比如: 1000100111011000 数据: 89D8H 指令:MOV AX,BX
我们指定这个二进制信息是什么就是什么
CPU 进行数据读写时,必须和外部芯片(各种内存)进行 3 类信息交换: 1.地址信息 即地址总线 寻找数据在存储设备的位置 2.控制信息 即控制总线 对外部设备的控制 控制总线是用来发出各种控制信号的传输线, 控制信号经由控制总线从一个组件发给另外一个组件(CPU 发给内存等),控制总线可以监视不同组件之间的状态(就绪/未就绪) 3.数据信息 即数据总线 把数据传递到 CPU 中 一个整型数据占用 4 字节
CPU 发出读写某块地址的地址信息给地址总线,给控制总线发送是读操作还是写操作,和控制哪块设备,然后数据就从数据总线传递到 CPU 里面某块地址上/CPU 把数据写入到那块地址上
也就是说地址总线里面的二进制数据表示地址,数据总线里面的表示数据,控制总线里面的表示控制的语句
BIOS 是主板和各类接口卡(显卡网卡等)厂商提供的软件系统,存储在 ROM 中 接口卡也有 BIOS RAM ROM
内存地址空间: CPU 将各类存储器看作一个逻辑存储器,所有的设备接口卡内存组成一个大的内存空间
寄存器(CPU 互作原理)
CPU 内部: 运算器 控制器 寄存器 使用内部总线连接 CPU 连接外部器件是外部总线(三大类的那个)
寄存器 > 缓存 》 二级缓存 》
CPU 要取数据,处 bai 理数据,都要放到 du 寄存器处 zhi 理。一般寄存器不用太 dao 大,它只要存放 zhuan 指令一次操作的 shu 数据就够了。
高速缓存是内存的部分拷贝,因为高速缓存速度快,把常用的数据放这里可以提高速度。
高速缓存一般不能被程序直接更改,它由硬件自己处理。程序直接读写 CPU 的寄存器,来完成操作。
一般两者都集成在 CPU 上。
寄存器是 CPU 内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果 把 111 放在 AX 222 放在 BX 运算器命令 ax + bx 放在 CX 中
8086CPU 14 个寄存器 都是 16 位 存放两个字节 就是一个字 其中 8 个通用寄存器
通用寄存器 AX BX CX DX 这四个寄存器为了兼容之前的*位寄存器,可以拆开独立使用 AH AL
8086CPU 把地址线 控制线 数据线都从 8 位升成 16 位 所以一次可以读取两个字节 就是一个字存放在寄存器中
几条汇编指令:
不分大小写 ;
MOV ax,18 把 18 放入 ax 寄存器 高级语言表示: AX = 18
ADD ax,8 ax 寄存器数值+8 AX = AX + 8
MOV ax,bx 把 bx 数据送入 ax 中
ADD ax,bx 把 ax + bx 放入 ax
CPU 访问内存单元时要给出内存单元的地址,所有的内存单元构成的存储单元空间是一个一维的线性空间
16 位 CPU 具有以下性质:
运算器 ALU 一次最多处理 16 位数据 寄存器最大宽度 16 位 寄存器和运算器之间的通路是 16 位
8086CPU 的地址总线是 20 位,能够遍历的地址是 2 的 20 次方 但是内部是 16 位的,只能遍历 16 位的地址,那么多出来的地址怎么表示呢?
内部使用两个 16 位表示表示 20 位物理地址 段地址+偏移地址的方式
段地址 偏移地址 传送到地址加法器 生成 20 位物理地址= 段地址*16 + 偏移地址 也就是段地址左移 1 位
内存被划分成一个一个段?? 没有,只是 CPU 的不足人为加入段
CPU 可以用不同的段地址 + 偏移地址找到同一个物理地址
数据在 21F60H 地址中 == 数据在内存 2000:1F60 单元中 == 数据在内存 2000 段 1F60 中
段地址存放在段寄存器 CS 代码段寄存器 DS 数据段寄存器 SS 堆栈 stack 段寄存器 ES 额外的段地址
IP 偏移地址寄存器
CS + ip 指定 CPU 当前要读取的指令的地址
不能直接把数据 1000H 放入段寄存器,只能把通用寄存器传递到段寄存器
8086CPU 执行指令:
CS+ip 寄存器里面的地址数据传入到 地址加法器 得到真实的物理地址, 通过控制器 送上地址总线,取得指令(操作码+地址码) 送入数据总线传入指令寄存器 译码器 计算
8086CPU 加电或者复位时候,CS 设置 FFFF IP 设置 0000, CPU 从内存 FFFF0H 单元读取指令执行第一条指令
程序员只能通过改变 CS IP 寄存器的内容实现对 CPU 的控制!!
使用 mov 关键词不能改变这两个寄存器 而是通过 JMP 加以区分 (JMP 段地址:偏移地址)
仅修改 IP 寄存器 JMP ax 类似 mov ip ax
在编程时候,可以将长度为 N (N<=64KB) 的一组代码存放在连续的起始地址为 16 的倍数的内存单元中(这样可以通过一个段地址+ 偏移地址找到), 从而定义了一个代码段
比如: MOV ax 0000 机器指令(B8 00 00)
ad ax 0123 (05 23 01)
mv bx ax (8b d8)
jmp bx (ff e3) 占用 10 个字节 存放在某一块连续的地址中,这就是代码段
怎么执行这个代码段呢?? 是不是把 CS+ip 指向首地指就可以了 对的
CPU 只认被 CS+ ip 指向的内存单元中的内容当做指令来执行 如果是 DS 指向的 CPU 当做是数据而不当做指令去执行
寄存器(内存访问)
任何两个连续的内存单元,N 单元和 N+1 单元,可以看成一个地址为 N 的字单元中的高低位
CPU 要读取内存单元的时候,必须知道内存单元的地址,由段地址和偏移地址组成
DS 寄存器存放要读取数据的段地址 [address]表示偏移地址为 address 的内存单元(一个字节)
MOV al [0] 把内存单元的偏移地址的内容放入寄存器 那么段地址哪里来的呢? 就是 DS 里面的数据 只能把通用寄存器的数据 mov 进去 DS 不能直接 MOV 数据进去
怎么把数据从内存中送入寄存器?? mov Ax 1000H
怎么把数据从寄存器送入内存单元?? mov bx 1000H mov ds bx; mov [0] ax(把 ax 寄存器的 16 位数据送入 1000:0 处高位 1000:1 低位 1000:0);
因为 8086CPU 是 16 位的数据总线,一次性可传输 16 位数据,一个字
mov bx 1000H ; mov ds bx; mov ax [0] 把 1000:0 处的字型数据送入 ax 两个字节的数据哦 因为 ax 是 16 位的 取得数据也就是 16 位的
add bx [1] ; 也是把 1001 和 1002 地址的 16 位数据与 bx 相加给 bx
命令后面的寄存器是 16 位的 那么偏移地址也看成 16 位
mov add sub
mov 寄存器,数据/寄存器/内存单元(偏移地址[0]) /段寄存器
mov 内存单元/段寄存器,寄存器
add sub 和 mov 一样 除了不能 add 段寄存器 寄存器
怎么使用数据段?? 将数据段段地址放入到段寄存器, 使用偏移地址[0/1/2/3] 访问具体的数据
栈
LIFO last in first out
所有的 CPU 都提供栈的设计,CPU 提供相关的指令来以栈的方式访问内存空间
PUSh 入栈 pop 出栈
PUSh AX 把 ax 寄存器的数据放入栈中 POP ax 把栈顶的数据放入 AX 寄存器 所以操作的都是字 不是字节
我们把某块连续的地址当做栈来使用 10000H - 1000FH 当作栈, 或者说以栈的方式访问内存空间
执行 MOV ax 0123H
push ax
mov bx 2266H
push bx
mov cx 1122H
push cs
pop ax
pop bx; pop cx
字型数据两个字节存放的
那么 CPU 怎么知道哪一块空间被当成栈使用?? push pop 时候怎么知道栈顶单元??
SS 段寄存器 存放栈顶的段地址 SP 寄存器 存放栈顶的偏移地址 那么一开始时候 SP 的地址是多少?? 就是栈空间的最高地址的下一个单元 任意时刻 SS:SP 指向栈顶的元素,栈为空时候,就不存在栈顶元素,偏移地址为栈最底部的字单元的偏移地址+2
如上面的地址 SS= 1000H SP=栈长度的后一个单元
PUSh AX 的时候 CPU 执行操作: SP=SP-2 将 AX 的内容存放到 SS:SP 地址单元
那么 CPU 怎么知道栈长度,而不会出现栈顶溢出呢??
栈顶超界是危险的,程序就访问了不属于他的内存的数据,黑客就利用这种方式溢出攻击
如果能够提供寄存器记录栈顶栈低地址就好了 8086CPU 没有解决这种问题
入栈.出栈都要防止越界
push/pop 段寄存器/内存单元 可以的
16 位 CPU push 和 pop 的都是 16 位 SP 寄存器 栈顶最大范围: FFFFH
妙用: 用栈暂存寄存器需要恢复的数据,先将寄存器内容入栈,再出栈到寄存器中 (函数调用就是这样的!!)
栈段 SS:SP
汇编编程
GCC 编译器在编译一个 C 语言程序时需要经过以下 4 步:
将 C 语言源程序预处理,生成.i 文件。
预处理后的.i 文件编译成为汇编语言,生成.s 文件。
将汇编语言文件经过汇编,生成目标文件.o 文件。
将各个模块的.o 文件链接起来生成一个可执行程序文件。
汇编语言:
源代码 编译连接 二进制可执行程序
源代码: 包括伪指令(汇编器执行的)和汇编指令(对应成机器码)
assume cs:codeg
codeg segment
start: mov ax 0123H
mov bx 0456H
codeg ends
end
XXX segment 定义一个段名为 XXX 段 成对使用
XXX ends
一个汇编程序是多个段组成,这些段被用来存放代码/数据/栈空间,至少有一个段:代码段
end 汇编程序的结束标志,编译器退出
assume 假设假如 上面假设代码段是 codeg 段
源程序: 源程序文件中所有的内容
程序: 源程序中最终由计算机执行处理的指令或数据,可执行文件
计算 2 的 3 次方
assume cs:abc
abc segement
start :move ax,2
add ax,ax
add ax,ax
add ax,ax
abc ends
end start
程序 P1 返回: 就是把 CPU 的控制权交还给调用这个程序 P1 的程序
mov ax,4c00H int 21H (中断来实现各种 dos 功能的调用) 但是 linux 中都是通过内核来实现的,因此所有的功能都需要系统调用来实现 int 8oh
汇编器: win 平台 masm linux 平台 nasm
nasm -f elf aa.asm win: masm aa.asm 编译生成 obj 文件(将源程序转换机器码,伪指令和汇编指令转成机器码)
windows 链接 link aa.obj >>aa.exe 链接:多个目标文件链接到一起,生成一个可执行程序;或者需要调用库程序的某个子程序,需要将库文件和程序的目标文件链接到一起
执行二进制程序后 秒弹黑窗口 又回到 dos
程序没有任何输出,因为只是做了寄存器加法的操作,没向显存写入任何东西
BX 和 loop(循环)
[bx] 是什么?? [0]表示一个内存单元时,0 表示偏移地址,段地址在 ds 中,单元的长度可以有具体指令操作的对象指出
[bx] 也表示一个内存单元,他的偏移地址在 bx 寄存器中 bx 存的是内存单元的偏移地址
mov ax [0] 使用编译器的时候 有可能变成 mov ax 0
所以有了 mov bx 0 , mov ax [bx]
inc bx bx 自增 1
为了描述方便
描述性符号 " () " 表示一个寄存器或者内存单元中的内容
比如 ax 的内容是 0010H ==> (ax) =0010H
mov ax,[2] ==> (ax) = ((ds)*16 + 2)
idata 表示常量 mov ax [idata]
cx 寄存器是与 loop 指令紧密相关的, 通常我们用 loop 指令来实现循环功能,cx 中存放循环的次数
循环的结构:
mov cx 10 s: add ax ax loop s 这样 s 标记的指令段 就循环了 10 次
段前缀 mov ax ds:[0] 段地址在 ds 中
也可以使用 mov ax cs:[0] 取 cs 中的内容为段地址 mov es:[0] ds
一段安全的空间: 不能动用核心内存空间哦 被占用的
在存在操作系统的情况下,用汇编语言去操作真实的硬件是不可能的,硬件已经被操作系统利用 CPU 保护模式所提供的功能严格管理起来了
dos 下面的安全空间: 0:200H ~ 0:2ffH
包含多个段的程序
目前我们只在安全空间下面编程,如果内存不够用怎么办?? 找操作系统分配
先来看 dw define word 定义字型数据 db 定义字节型数据
在代码段中使用数据
dw 1234H,1245H,1256H dw 写在 segment 里面,的开头 里面的数据占用的内存空间 段地址就是 segment 是哪个 cs? ds? 就是哪个
偏移地址就是 0 2 4 6
start 伪指令放在 dw 下面就好了 指向真正的要执行的机器指令 而不是让 CPU 从 dw 的数据开始执行
在代码段中使用栈
dw 声明几个字的内存空间,然后把栈段 ss 指向代码段的段地址,sp 指向空间的最高地址+1 这样声明的那块内存空间就可以被我们当成栈使用
怎么将数据 代码 栈 放入不同的段
使用 assume 声明多个段
assume ds:data,cs:code,ss:stack
data segment
dw ,,,
data ends
start: mov ax,data
mov ds,ax
and 指令(逻辑与指令) or 指令
改变字母的大小写就是改变 AScll 码值
idata 就是数字
偏移地址可以用 [bx + idata]表示 ,比如 bx+1 或者 1[bx]
DI 和 SI 是对 BX 寄存器的补充,但是不能拆分成 8 位寄存器
比如复制的操作,就用 DS:SI 表示原始的内存空间,用 DS:DI 表示目标地址空间
[bx + si + 1] 表示一个内存的偏移地址
双层循环怎么做?? loop 默认跟 cx 绑定 在第一层循环中,把 cx 的值暂存到寄存器中,但是寄存器是有限的,如果程序比较大,就寄存器都在二层循环中用了,只有存到内存中!! 才是合理的
8086CPU 中只有四个寄存器可以用在偏 x 移地址中[],bx si di bp 进行内存单元的寻址 只能 bx + si/di , bp + si/di
bp 对应的段地址是 ss 是支援 sp 用的
CPU 处理的数据可以在三个地方: CPU 内部(寄存器) 内存(偏移地址) 端口
CPU 找数据的方式就是寻址方式 直接寻址(就是直接的数值 mov ax,1) 寄存器寻址(mov ax,bx 数据存放在 bx 寄存器中) 寄存器相对寻址(mov ax,[bx+idata]) 基址变址寻址(mov ax,[bx+si]) 相对基址变址寻址(mov ax,[bx+si+idata])
div 指令: division(除法)
dd (double word) 32 位
转移指令
offset 在汇编语言中,由编译器处理的符号,是: 取得标号的偏移地址
start: mov ax, offset start ==> mov ax,0
s:mov ax,offset s => mov ax,3
jmp 是无条件跳转指令,可以只修改 IP 也可以同时修改 CS 和 IP
call 和 ret 指令都是转移指令,可以修改 IP 或者 CS 和 IP
ret 使用栈中的数据,修改 IP 的内容,实现近转移 相当于 pop IP 栈数据放到 IP 寄存器中
retf 使用栈中的数据,修改 CS + IP 的内容 实现远转移 相当于 pop IP pop cs
call 指令 call 标号 把当前 IP 或者 cs+ip 入栈 然后跳转 JMP 到标号的地方去执行
就是函数调用啦!! easy
mul 指令: 乘法
标志寄存器
8086 标志寄存器都是 16 位的,存储的信息被称为程序状态字 (PSW)
ZF 标志 上一条指令操作结果为 0 那么 0 标志位就是 1
SF 标志 记录指令执行成功后结果的正负数
CF 标志 进位标志位 当数据相加益处的时候,用 CF 标志进位了
DF 标志 方向标志位
内中断
CPU 去处理事件,处理完成,继续 CPU 原来的操作
外部中断: 键盘 打印机 定时器 等 是可以屏蔽的中断,利用中断控制器是可以屏蔽的
内部中断: 是指因硬件错误或者运算错误,不可屏蔽的
中断向量表 就是去找中断处理程序的中间产物,在内存中存储,存放 256 个中断源所对应的中断处理程序的入口,所以这块内存是很重要的
知道了处理中断程序的地址,我们就可以重新写中断处理程序,把程序的入口放到中断向量表中
单步中断
CPU 在执行了一条指令后,就去作其他的事情了,就会有单步中断的效果。
CPU 在执行了一条指令后,如果检测到标记寄存器的 TF 位为 1 则产生单步中断 引发中断过程, 单步中断的中断类型码为 1
int 指令
int n (n 为中断类型码) 他的功能是引发中断程序
一般情况下,系统将一些具有一定功能的子程序,以中断处理程序的方式提供给应用程序调用
BIOS CPU 加电后从 cs=0ffffH ip=0 内存开始执行 ffff:0 有一条调转指令,转去执行 bios 硬件系统检测和初始化程序(建立 BIOS 提供的中断历程的入口地址登记在中断向量中)完成后调用 int 19 进行操作系统引导
端口
CPU 可以直接读取三个地方的数据:
CPU 内部的寄存器
内存单元
端口
对端口的读写
in out 指令
in al,60H 从 60H 号端口读入一个字节
CMOS RAM 芯片
包含一个实时钟和一个 128 个存储单元的 RAM
该芯片依靠电池功供电,保存 BIOS 的配置信息
芯片内部有两个端口,70H 和 71H,CPU 通过这两个端口读写 CMOS RAM
CPU 除了具有运算的能力还有 I/O 能力
外设的输入不直接送入内存中,而是送入相关的接口芯片的端口中
CPU 向外设的输出也不是直接送入到外设,而是先送入到端口中,在有相关的芯片送到外设