汇编
基础知识
什么是汇编语言
直接在硬件之上工作的编程语言
其主体为汇编指令(每一种CPU有其自己的汇编指令集)
汇编指令与机器指令一一对应
其差别在于指令的表达方式上(便于记忆)
组成:汇编指令(机器码的助记符)
伪指令(由编译器执行)
其他符号(由编译器识别)
寄存器
CPU中可以存储数据的器件 一个CPU有多个寄存器
存储器
指令,数据存放处(即内存) 什么是数据什么是指令?
他们是应用上的概念 在内存和磁盘上并无区别
都是二进制信息 看你如何取用
存储器被分成若干个存储单元
从零开始编号
一个存储单元=1 byte=8 bit 1 KB的存储器有多少个
即8位二进制数 存储单元?
1 KB=2^10 B 1 KB = 1024 B
1 MB=2^10 KB=2^20 B 那就是1024个存储单元
1GB=2^10 MB=2^20 KB=2^30 B (0~1023)
这些 0 1 124 编号 就好比门牌号码
CPU如果要从里面取东西
首先要知道它在哪(地址信息)
要做什么(控制信息)
读写什么(数据信息)
而这些信息都电信号 所以要用导线来传送
引出三个概念
地址总线
数据总线
控制总线
在CPU上会有很多管脚与总线相连 可以说 管脚引出总线
地址总线
CPU是通过地址总线来指定存储单元的
地址总线能传送多少个不同的信息 CPU就可对多少个存储单元进行寻址
其宽度决定了寻址能力 (有N根地址总线就可说地址总线的宽度为N)
宽度为N的地址总线 最多可以寻找2^N个内存单元
这里会涉及到计算问题 因为1内存单元为1byte 又分别与2的次幂有关系
寻址能力为8 KB 宽度为多少?
8 KB=8*2^10 B =>> 2^3 * 2^10 = 2^13 即13根
16根的寻址能力为多少?
2^16 =>> 2^6 * 2^10 = 2^6 KB
数据总线
CPU与内存或其他器件间的数据传送
其宽度决定了CPU与外界数据的交换速度
八根数据总线 十六跟数据总线
一次可传送8个二进制数据 一次可传送16个bit
即1 字节 (1 byte=8 bit) 两字节
8088CPU 数据总线宽度为8 8086CPU数据总线宽度为16
就好比单行车道和双行车道 路拓宽了 提高了通行速度
控制总线
决定了CPU对外部器件的控制能力
有多少跟控制总线就意味着CPU提供
了对外部器件的多少种控制
内存地址空间
一个CPU的地址线宽度为10 则可寻址2^10=1024个单元
这1024个 可寻到的内存单元就构成这个CPU的内存地址空间
深入讨论需两个基本知识 主板 接口卡
主板 连接CPU和各个主要器件
接口卡 对于无法直接控制的器件(显示器 音响等外部器件)通过接口卡实现间接控制
RAM ROM 接口卡上的RAM……
内存地址空间 物理层面上
(假象的逻辑存储器) (各自独立)
所有的物理存储器都被看作一个由若干存储单元组成的逻辑存储器
每个物理存储器在这个逻辑存储器中占有一个地址段 即一个地址空间
比如假设
0~7FFF 的32KB的空间为主随机存储器的地址空间 在1000H写 就进了主随机存储器8000~9FFF 的8KB空间为ROM地址空间 在8000H写 并不会改变 因为ROM只读
……
2^15=>> 2^5 * 2^10 2^13=>>2^3 * 2^10
32 KB 8 KB
内存地址空间的大小受CPU地址总线宽度的限制
地址总线宽度为 20 可传送2^20 个地址信息 即可定义2^20个内存单元
内存地址空间大小为 1MB(2^20 B)
地址总线宽度为32 可传送 2^32个地址信息 即可定义 2^32个内存单元
内存地址空间大小为4GB (2^2 * 2^30)
综上 我们要在某类存储器中读写数据的话 需要知道其首尾单元的地址 (类比区间)
要注意的是 不同的计算机系统 其内存地址空间分配是大不相同的
寄存器
CPU内部用于信息存储的部件
我们无法改变运算器和控制器 所以寄存器是我们实现控制CPU的唯一方式
通用寄存器
AX BX CX DX
其实本来 应该是16位的
但由于之前都是8位 为了兼容 将AX分为了AH和AL (BX CX DX 同理)
分为高低位
AH AL都是可独立的8位寄存器
可以单纯当做ah 或者al使用 也可以当成整体ax使用
得到的结果也会因此大不相同
如 01001110 00100000
当成ax就是4E20H
当成ah就是4EH
当成al就是20H
问题 16位的存储器可放的最大数据是多少?
2^16? 从0开始 所以 2^16 -1
字的存储
字节:byte
字:word =2 byte 一个字由两个字节组成
字的概念由来
因为8086进化到了16位
所以把一次性读取16位的内存单元称为字
对应ah al 就有了高位字节(高八位) 和 低位字节的说法(低八位)
用16进制?
16进制的一位刚好等于2进制的四位 (2^4=16) BIN文件夹?
用16进制可以直观的看出这个数据是有那些8位数据构成的 代表二进制
比如 20000变成4E20 一般存放源文件
就可以很容易知道ax中 ah为4E al为20
汇编指令(1)
mov
add
不分大小写
注意
当ah 与 al 作为独立寄存器时 al丢失的数据并不会到ah中去
至于去哪了???
mov ax,bl
mov bh,ax
mov al,20000
mov al,100H ?将一个高于8位的数据加入8位寄存器
都是错的
分析一下
寄存器的 操作对象 位数要相对应 八位传八位(al ah bh bl等可以互传)(ax bx等间互传)
要考虑内存地址空间大小 al是八位寄存器 最大可放值为2^8-1 也就是255的数据
1111111 20000远大于255
计算
F4A3
31A3
3123
6246
826c
6246
826c
04D8
0482
6c82
D882
D888
D810
6246
0 1 2 3 4 5 6 7 8 9 a b c d e f 0 1 2 3 4 5 6 7 8 9 a b c d e f ……
物理地址
CPU访问内存单元 需要给出地址
而内存单元构成的存储空间是一个一维线性空间
如图
每个内存单元在该空间都有一个唯一地址
称为 物理地址
需要由CPU内部传到存储器(外部)
要传首先得有吧
这个物理地址是如何形成的(内部)
之前说的地址总线是CPU外部的(CPU与其他硬件)
外部这个宽度为20 可以寻1MB
在CPU内部也存在地址总线(寄存器与运算器间)
比如8086为16位 只可以传送16位地址
2^16=>> 2^6 * 2^10
64 kB
单次可传64KB的数据
内部只有16位 寻64KB
内外存在矛盾
怎么办?
二合一
物理地址=段地址16+偏移地址 为什么乘16 十进制里面 1010 相当于左移一位
SA EA 16进制不就乘16嘛
本质含义:CPU在访问内存时 用一个基础地址(段地址*16)和一个相对于基础地址的偏移地址相加,给出内存单元的物理地址
数据在21F60H中
对于8086CPU可以这样描述
数据存在内存 2000:1F60中间
数据存在内存的2000段中的1F60单元中
段
不存在段 段只是自己方便理解的定义
(根据需要把若干个连续的内存单元看做一个段)
同样一个地址空间 可以把它当做一个段也可以当成两个
段的起始地址是16的倍数 (由段地址控制 段地址都是乘16得来的)
长度最长为64KB(由偏移地址决定 偏移地址只有16位 2^16 =>64 KB)
0~ffff
一个物理地址可以用多种组合得出 如图:
由此可得几个问题
段地址为0001H 仅变化偏移地址
CPU的寻址能力为多少?
偏移地址0000~FFFF
0001*16+0000=0010
0001*16+FFFF=1000F
物理地址=SA*16+EA
20000H=SA*16+EA
SA=(20000H-EA)/16
=2000H-EA/16
EA 0000~FFFF
SA(min)=2000H-FFFFH/16=1001H
SA(max)=2000H-0000H/16=2000H
(代码)段寄存器
段地址需要地方存储
主要有CS DS SS ES
告诉CPU要去哪里
CS IP
代码段寄存器 指令指针寄存器
由CS:IP指向内存单元读取指令
读取的指令进入指令缓冲器
IP=IP+所读取指令的长度 从而指向下一条指令
执行指令转到步骤1 重复过程
CPU将CS、IP中的内容当做指令中的段地址和偏移地址 用他们合成指令的物理地址
到内存从读取指令码 执行
前面有说过我们只能改变寄存器来控制CPU
那CS和IP怎么改
可以用传送指令mov吗
如mov ax,123
不行
对于CS IP的值 需要用 传移指令 改变
Jmp
想同时改CS IP的值
jmp 段地址:偏移地址
jmp 3:01B6 约等于 mov CS,3 mov IP,01B6
想只改IP地址
jmp 某一合法寄存器
Jmp ax约等于 mov IP,ax
代码段
可根据需要将一组内存单元定义成一个段
长度为N(N<=64KB)的一组代码 存放在一组地址连续的 起始地址为16的倍数的内存单元中 这段内存是用来存放代码的 也就是所谓代码段
N<=64KB:偏移地址不能超过16位==》》一个段的索引长度不能超过2^16 也就是64KB 一个段最大存放64KB
起始地址为16的倍数:段地址要×16
如何使得代码段中的指令被执行?CS:IP指定
这只是自己的安排 CPU不会自动将我们定义的代码段的指令当做指令来执行
必须由CS:IP来指定 想执行代码就要指定该“代码段”的首地址
如果指定在“代码段”的非首行 那这个代码(程序)就不会执行 这也是暴力破解的原理
从CS:IP指向内存单元读取指令 读取的指令进入指令缓冲器;
IP指向下一条指令;(自动会加上上一条的长度)
执行指令
Debug
R D E U T A
R
查看寄存器的内容
改变寄存器的内容
改变段寄存器的内容
D
查看内存中的内容
指定位置查看
E
改写内存中的内容
看看效果
U
将内存中的机器指令翻译成汇编指令
A
以汇编指令格式在内存中写入机器指令
T
执行一条机器指令
接着上面的 这里用a输入了几条汇编指令 用d查看一下相应地址的内存情况
但是机器码是看不懂的 用u翻译一下 会发现跟我们填入的是一样的(当然一样 用a修改了 再次确认罢了)
如何让它执行呢
先改变CS:IP的值 让它指向这里
单步执行
(数据)段寄存器
CS是代码段寄存器 DS是数据段寄存器
内存中字如何存储
两个16进制位为一个字节
4E20
4E为高位 20为低位
对应内存单元 20放在0单元(低位) 4E放在1单元(高位)
字节型数据占一个内存单元 字型占两个内存单元
如上图 1地址字单元中存放的字型数据是什么——124E
1地址单元中存放的字节型数据是什么——4E
任何两个地址连续的内存单元,N号单元和N+1号单元,可以将他们看成两个内存单元,也可看成一个地址为N的字单元中的高位字节单元和低位字节单元。
DS [address]
存放要访问的数据的段地址
读取10000H单元的内容 将1000:0中的数据读到al中
MOV bx,1000H
MOV ds,bx
MOV al,[0]
段地址1000放到bx 再从bx放到ds
(至于为什么不直接mov ds,1000H 无法解释 规定如此 可以比喻为 塞红包不好直接塞到手里 得放在口袋 等ds自己拿出来) mov只能把数据直接送到通用寄存器
而偏移地址则使用 mov al,[0] 不可直接放在段寄存器
[…]代表的是一个内存单元 这个[0]代表的是这个内存单元的偏移地址
此时段地址会默认为ds中的1000
综上 此时指向1000:0
数据=》通用寄存器=》段寄存器
刚才把内存单元的东西放在寄存器中 那能不能把寄存器中的东西送入内存单元?
把al的数据放入内存10000H
首先还是一样给出段地址
MOV bx,1000H
MOV ds,bx
MOV [0],al 把al数据放入1000:0000
e改写内存数据
a查看内存验证修改
r查看寄存器内容
a写入汇编指令
t单步执行
汇编指令(2)
MOV
mov 寄存器,数据 mov ax,6
mov 寄存器,寄存器 mov bx,ax
mov 寄存器,内存单元 mov ax,[0]
mov 内存单元,寄存器 mov [0],ax
mov 段寄存器,寄存器 mov ds,ax
mov 寄存器,段寄存器 mov ax,ds
mov 内存单元,段寄存器 mov [0],ds
mov 段寄存器,内存单元 mov ds,[0]
ADD
add 寄存器,数据 add ax,8
add 寄存器,寄存器 add ax,bx
add 寄存器,内存单元 add ax,[0]
add 内存单元,寄存器 add [0],ax
SUB
sub 寄存器,数据 sub ax,9
sub 寄存器,寄存器 sub ax,bx
sub 寄存器,内存单元 sub ax,[0]
sub 内存单元,寄存器 sub [0],ax
数据段
同代码段一个意思 这也是自己定义的
怎么访问?
用DS :[address]指向它
栈
最先进入的最先出去
两个基本操作:入栈和出栈
入栈(PUSH):把一个新元素放在栈顶
出栈(POP):从栈顶取出一个元素
LIFO(Last In First Out)
栈也是我们设定的“当成栈”来使用
Push ax:将寄存器ax中的数据送入栈中
POP ax:从栈顶取出数据送入ax
一般以字为单元 一下子两个内存空间
CPU如何知道当前要执行的指令所在的位置
CS:IP
如何知道所指向的数据位置
DS:[address]
而段寄存器SS 和寄存器SP
分别存放栈顶的段地址和偏移地址
任何时候SS:SP指向栈顶元素
而当指向时 CPU也就知道了该内存空间被当为栈来使用
使用push时
SP=SP-2 新的SS:SP指向新的栈顶
先减2再送入(不先减就会把原数据覆盖掉)
使用POP时
SP=SP+2 指向新地址
删去栈里的数据后 (POP后)数据是还在里面的 只是地址变了 然后执行下一个push的时候会把原来的内容覆盖掉 这也就是为什么格式化硬盘后还可以恢复数据
准确来说不叫删除 只是改一下指向地址而已
当栈满时再使用push和栈空时再使用pop
都将发生栈顶超界问题
CPU不会保证栈的操作不会超界
它只知道SS:SP指向的是栈顶 但不知道我们安排的栈空间有多大
就好比之前的 CPU只知道当前要执行的指令在何处(用CS:IP)表示
而不知道要执行的指令有多少
怎么解决栈顶超界问题?
编程时自己操心
根据可能要用的最大栈空间来安排栈的大小 防止栈的数据太多而导致超界。执行出栈时也要防止栈空的时候继续出栈而导致的超界
汇编指令(3)
PUSH POP
PUSH 寄存器:将一个寄存器中的数据入栈 push ax
POP 寄存器:出栈 用一个寄存器接收出栈的数据 pop bx
PUSH 段寄存器:将一个段寄存器中的数据入栈 push ds
POP段寄存器:出栈,用一个段寄存器接收出栈的数据 pop es
(通用寄存器都是以x结尾 段寄存器都是以s结尾)
PUSH 内存单元:将一个内存单元处的字入栈(栈操作都是以字为单位)push [0] ds:0
POP 内存单元:出栈,用一个内存字单元接收出栈的数据 pop [2] ds:2
(数据的段地址在ds 代码段地址在cs 栈段地址在ss)
第一步要注意的是 SS:SP指向的是1000:0010 (栈底+1)
原理:同c调函数
c语言调用函数时
比如main函数调用a函数时 会把main函数里的ax bx等等全都入栈 等调用完后又将他们全部还原 ax bx cx就好像没发生过这件事 继续进行原有的事情 中间就保存还原了一下
Push、pop指令与mov不同
Mov只需一步操作 就是传送
而push和pop需要俩步
执行push时 先改变SP 再向SS:SP传送
执行pop时 先读取SS:SP 后改变SP
比如
想在10000H处写入2266H
mov ax,1000H
mov SS,ax
mov SP,2 因为下面的push的执行是先减2再输入 所以这里需要提前加2
mov ax,2266H
Push ax
修改的只是SP 所以栈顶的变化范围为0~FFFFH
栈的小结
栈段
将长度为N(N<=64KB)的一组地址连续、起始地址为16的倍数的内存单元当做栈来使用
从而定义了一个栈段
栈顶的范围是0~FFFF 栈空时SP=0,一直压栈 直到栈满时SP=0
如果再次压栈,栈顶将环绕,覆盖原来栈的内容
比如
要让10000~1FFFFH为空栈 sp会是FFFE+2或者说FFFF+1 即为0000H
早期的堆栈是为了保存一个返回地址(函数返回地址)
调用时需要一个东西去暂时保存ax bx等寄存器
这个东西就是栈
栈是用来临时存放东西的 避免数据流失或者被覆盖 栈是为了函数而存在 面对过程而存在
在函数里定义局部变量 这个局部变量就是放在堆栈里 调用完这个函数 对应的局部变量就不见了 因为那个堆栈随着函数的消失而消失 (被编译器释放掉了)
段的小结
我们可以将一段内存定义为一个段 用一个段地址指示段 用偏移地址访问段内的单元
这完全是我们自己的安排
我们可以用一个段存放数据 将它定义为数据段
也可以用一个段存放代码 将它定义为代码段
或者用一个段当作栈 将它定义为栈段
我们可以这样安排 但若要让CPU按照我们的安排来访问这些段
就需要
*对于数据段 将它的段地址放在DS中 用mov add sub等访问内存单元的指令时
CPU就将我们定义的数据段中的内容当做数据段来访问
*对于代码段 将它的段地址防在CS中 将段的第一条指令的偏移地址放在IP中
这样CPU就将执行我们定义的代码段中的指令
*对于栈段 将它的段地址放在SS中 将栈顶单元的偏移地址放在SP中
这样CPU在需要执行栈操作时 比如使用PUSH和POP指令时 就将我们定义的栈段当作栈空间使用
可见 不管我们怎么安排 CPU将内存中的某段内存当作代码 是因为CS:IP指向了那里
CPU将某段内存当作栈 是因为SS:IP指向了那里
一段内存可以是代码的存储空间又是数据的存储空间还可以是栈空间也可以什么都不是
关键在于CPU中寄存器的设置 即CS、IP、SS、SP、DS的指向
附件
📄 汇编.docx(原 word 笔记):汇编.docx
⬅️ C进阶 图形化 easyX 🏠 00-编程语言
💬 评论