16位汇编1-6
文章目录
1.基础
1.1 cpu
汇编语言有以下3类指令
- 汇编指令:有对应机器码,是汇编语言的核心。
- 伪指令:无机器码,由编译器执行,计算机不执行。
- 其它符号:编译器识别,无机器码。
cpu工作需要有指令和数据,而指令和数据存储于内存,所以内存的作用仅次于cpu。
CPU读写数据,要和外部器件(芯片)进行三类信息交互:
- 地址信息:存储单元的地址。
- 控制信息:器件选择,读或写的命令。
- 数据信息:读或写的数据。
每种cpu都有自己的汇编指令集。
真正的64位要满足的条件:
- 64 bit cpu
- 64 bit os
- 64 bit app
1.2 总线
cpu不能直接控制外部器件,而是通过总线连接其它芯片,逻辑上分为地址总线,控制总线,数据总线。
地址总线
一个CPU有N根地址线,则说该CPU的地址总线的宽度为N,可寻址2N 个内存单元。
N决定了CPU与外界的数据传输速度。
控制总线
控制总线数量意味着CPU提供了对外部器件的多少种控制,所以控制总线的宽度决定了CPU对外部器件的控制能力。
数据总线宽度决定了数据传送速度;
控制总线宽度决定能控制多少种外部器件;
地址总线宽度决定cpu寻址能力。
指令和数据在存储器中没有区别,都是二进制信息,属于哪种信息取决于所在的总线。
读操作的步骤:
- 地址线:cpu --> 内存,传送地址
- 控制线:cpu --> 内存,读指令
- 数据线:内存 – > cpu,数据
写操作同理。
1.3 内存地址空间
地址总线宽度为n,则可寻址的 2 n 2^n 2n个内存单元构成内存地址空间。
对cpu来说,所有存储器的存储单元都位于一个统一的逻辑存储器中,容量受cpu寻址能力的限制。这个逻辑存储器就是内存地址空间。
1.4 存储
寄存器:可以理解为嵌入到cpu中的内存。
微机存储器容量以字节为最小单元计算,即一个存储单元存储一个字节。
其实计算机的任何部件都有独立存储器,比如显卡的显存,将数据交给gpu,而且gpu的速度比cpu还要快。
存储器按读写属性分类:ram和rom
按功能和连接分类:
- ram
- rom with bios
- ram on the interface card , 比如显存
各种存储器虽然物理上独立,但有两个相同点:
- 和cpu总线相连
- 读写操作要通过控制总线
1.6 masm
命令 | 功能 |
---|---|
r | 查看,更改寄存器 |
d | 查看内存 |
e | 改写内存,写入机器码 |
a | 改写内存,写入汇编指令 |
t | 执行一条机器指令 |
u | 把机器指令翻译为汇编指令 |
p | 执行指令。 p命令执行到int 21时,程序退出, 而t命令不会退出 |
g | 跳转 |
q | 退出 |
-d fff0:f0
可以查看主板rom生产日期,-e
无法更改rom。
-d 1000:0 8
查看8个字节。
debug
的原理:不放弃对cpu的控制。程序加载顺序为cmd->debug->1.exe
dos加载exe文件时,ds
存放程序所在内存区的段地址SA
,并且系统会创建段前缀psp
,dos用它来和程序通信,里面包括了传递给待运行程序的命令行参数、 程序运行结束时返回DOS所需的地址等有用的信息,位于ds:0
,占用256字节。从ds:100H
开始才是exe程序。
所以,程序的物理地址是:SA*16+0+256 = (SA+16)*16+0
2.寄存器
三条外部总线连接cpu和外部器件;内部总线实现cpu内部各个器件的联系。
8086CPU有14个寄存器:
- 通用寄存器:ax,bx,cx,dx
- si,di
- sp,bp,ip
- 段寄存器:cs,ss,ds,es
- psw
它们全是16位的,可分为2个8位寄存器使用,如AX
分为AH
和AL
。
16位结构代表了:
- 寄存器最大宽度
- 运算器一次最多处理16位数据
- 寄存器和运算器之间的通路是16位的
寄存器 | 解释 |
---|---|
AX | Accumulator累加器 |
BX | Base Register基地址寄存器 |
CX | Count Register计数寄存器 |
DX | Data Register数据寄存器 |
SI | source index register |
DI | destination index register |
SP | stack pointer |
BP | base pointer |
IP | intruction pointer |
CS | code segment |
SS | stack segment |
DS | data segment |
ES | extra segment附加段寄存器 |
PSW | Program Status Word |
2.1 指令
8086可 一次性处理两种尺寸的数据:
- 字节:byte,8个bit组成。
- 字:word,高字节和低字节两个字节组成。
汇编指令 | 高级语言描述 |
---|---|
mov ax,18 | ax=18 |
mov ah,78 | ah=78 |
add ax,8 | ax=ax+8 |
mov ax,bx | ax=bx |
add ax,bx | ax=ax+bx |
写指令时不区分大小写。
数据传输或运算时,两个操作对象的位数应一致。
add al,al
,如果有进位,1不会保存进ah
.
add ax,al``,如果有进位,1会保存进
ah`.
2.2 物理地址
存储空间时一维线性空间,每一个内存单元在这个空间中都有唯一的地址,即物理地址。
8086有20位地址总线,即1MB寻址能力,但内部为16位结构,表现出来的寻址能力只有64k。8086采用合成两个16位地址的方法形成一个20位物理地址。
物理地址=段地址*16+偏移地址
段地址*16可看作基础地址。
0:200~0:20b
等同0020:0~0020:b
2.3 段
错误认识:内存被划分为段
其实,内存没有分段,段的划分来自于cpu。
编程时可根据需要,将地址连续、起始地址为16的倍数的一组内存单元定义为一个段。用段地址*16
定位段的起始地址,用偏移地址定位内存单元。
偏移地址为16位,寻址能力为64k,所以一个段的最大长度为64k。
对于物理地址21F60H
段地址 | 偏移地址 |
---|---|
2000H | 1F60H |
2100H | 0F60H |
所以,cpu可以用不同的段地址和偏移地址形成同一个物理地址。
21F60H有两种描述:
- 2000:1F60单元
- 2000段的1F60单元
思考,当段地址给定多少时,CPU无论怎么变化偏移地址都无法寻到20000H单元?
20000h=SA*16+EA
SA=(20000h-EA)/16=2000h-EA/16
EA取最大值时,SA=2000h-ffffh/16=1001h,SA为最小值
EA取最小值时,SA=2000h-0h/16=2000h,SA为最大值所以答案是 1001H 以下和 2000H 以上。
2.4 CS和IP
8086有4个段寄存器:CS,DS,SS,ES.
CS和IP是两个最关键的寄存器。CPU会一直重复以下过程
- 将从
(cs)*16+(ip)
单元开始读取指令并执行 - IP=IP+读取指令的长度
8086启动后,CS=FFFFH,IP=0000H
,所以FFFF0H
是8086主机开机执行的第一条指令。
cpu只认cs:ip单元的内容为指令
2.5 修改CS,IP
不是传送指令mov
,而是跳转指令jmp
。
用法1
jmp 2AE3:3
之后CS=2AE3H,IP=0003H
用法2
AX=1000H,CS=2000H,IP=0003H
jmp ax
之后AX=1000H,CS=2000H,IP=1000H
修改段寄存器:数据–>通用寄存器–>段寄存器
切记,8086cpu不支持将数据直接送入段寄存器。
段的访问:
- 代码段:
cs:ip
- 数据段:
ds:[]
- 栈段:
ss:sp
2.6 内存访问
0地址处存放2000(4E20H)
----------
| 20 | <-- 0
----------
| 4E | <-- 1
----------
这是小端存储
0地址处的字节型数据为20H
0地址处的字型数据为4E20H
3.寄存器
3.1 字的存储
字单元:存放一个字形数据的内存单元,由两个字节单元组成。
N地址字单元:起始地址为N的字单元。
3.2 DS和[address]
DS用来存储要访问数据的段地址。
[]
表示一个内存单元的偏移地址,段地址默认在ds
中。
mov bx,1000H
mov ds,bx
mov al,[0]
3.3字的传送
mov bx,1000H
mov ds,bx
mov ax,[0] ;1000:0处的字型数据传入ax
mov [0],cx ;cx中的16位数据送到1000:0处
3.4 mov、add、sub指令
mov的3种常用功能:
mov ax,8 ; mov 寄存器,数据
mov ax,bx ; mov 寄存器,寄存器
mov ax,[0] ; mov 寄存器,内存单元,实际可能会出问题,因为ax是16位,[0]是8位。
其它:
mov [0],ax ; mov 内存单元,寄存器
mov ds,ax ; mov 段寄存器,寄存器
mov ax,ds
mov [0],cs
mov ds,[0]
add,sub同mov
3.5数据段
将123B0H~123B9H定义为数据段,现在累加其中前三个单元内的数据
mov ax,123BH
mov ds,ax
mov al,0
add al,[0]
add al,[1]
add al,[2]
如果是累加字型数据,则改为
mov ax,0
add al,[0]
add al,[2]
add al,[4]
3.6栈、SS、SP
操作规则:LIFO
8086的入栈和出栈都是以字为单位进行的。
SS
存放栈顶的段地址,SP
存放栈顶的偏移地址。任意时刻,SS:SP
指向栈顶元素。
push ax
由两步完成
- SP=SP-2,SS:SP指向当前栈顶前面的单元。
- 送入ax,SS:SP为新栈顶。
pop
相反,而且pop
后数据还存在栈内,再次push
会被覆盖。内存不提供删除机制,此处联系文件的删除。
SS=1000H SP=000EH
PUSH ax ;ax=2266H
1000DH=22 1000CH=66 SP=000CH
---------
| 66 | <-- 1000C
---------
| 22 | <-- 1000D
---------
8086不保证栈操作不会出界,所以要合理安排栈大小。出栈时也要确定栈不为空。
mov ax,1000H
mov ds,ax
mov ax,2266H
mov [0],ax
等同以下
mov ax,1000H
mov ss,ax
mov sp,2
mov ax,2266H
push,ax
假设栈空间:10000H-1000FH
:
- 栈最底部的字单元地址为
1000:000E
; - 栈只有一个元素时,
sp
指向000EH
; - 栈为空时,
sp
指向0010H
。
若栈空间为10000H-1FFFFH
,栈为空时,sp
指向0000H
;栈满时sp
也指向0000H
,再次push
会覆盖。
因为这两个栈指令只修改
sp
,所以栈顶最大变化范围为0-FFFFH
push
和pop
可以操作:
- 通用寄存器
- 段寄存器
- 内存单元
栈是为存放临时数据而设计的,早期用来保存函数地址。
4.第一个程序
assume cs:codesg ; 将codesg与cs联系起来。
codesg segment ;伪指令,段开始。段名称为codesg
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00H ;两条指令用于程序返回,即把cpu控制权交给使该程序运行的程序,如cmd.exe.
int 21H
codesg ends ;伪指令,codesg段结束。
end ;结束编译
一个汇编程序由多个段组成,至少要有一个代码段。
指令,数据,和栈被划分到不同的段里。
伪指令没有对应机器码,不被cpu执行,由编译器执行。编译器根据伪指令进行编译。
汇编需要编辑器,编译器,连接器,调试器。
连接器:
- 把很大的源程序分为多个源程序文件来编译为目标文件,再连接为可执行文件exe。
- 程序调用库文件的子程序,也需要库文件和该程序目标文件连接为exe。
- 存有机器码的目标文件中有内容需要连接程序处理后才能生成exe。
dos系统是单任务系统,中断机制是基础;windows的消息机制是多任务的基础。
编译时使用;
可以简化编译步骤,或者直接使用ml.exe
。
5.[BX]和loop指令
5.1 [bx]
描述一个内存单元需要两种信息:
- 内存单元地址
- 内存单元长度
[bx]
表示内存单元,它的偏移地址为bx,段地址(段前缀)默认在ds中。例如mov ax,[bx]
。也可以显示指定段地址为ds:,cs:,ss:,es:
。
mov ax,[idata]
会被编译为mov ax,idata
,所以需要用[bx]
或者mov ax,ds:[0]
我们用()
表示寄存器或内存单元中的内容,可包含寄存器名,段寄存器名,内存单元物理地址(20位)。
(ax),(ds),(20000H).
用idata
表示常量。
mov ax,[bx]
功能:(ax)=( (ds)*16+(bx) )
mov [bx],ax
功能:( (ds)*16+(bx) )=(ax)
5.2 inc
inc ax
即自加1
5.3 loop
loop指令有两步操作:
- (cx)=(cx)-1
- 判断(cx),不为0则转至标号处执行程序,为0则向下执行。
mov cx,循环次数
s:
循环代码段
loop s
计算2^12
assume cs:code
code segment
mov ax,2
mov cx,11
s: add ax,ax ;类似do{}while{}
loop s
mov ax,4c00H
int 21H
code ends
end
汇编中,标号实际上是一个地址。上面的s
地址处有add ax,ax
指令。
计算ffff:0~ffff:b单元中的和
assume cs:code
code segment
mov ax,0ffffH ;汇编源程序中,数据不能以字母开头,所以要在前面加0.
mov ds,ax
mov bx,0
mov dx,0
mov cx,12
s: mov al,[bx]
mov ah,0 ;可以尝试直接mov ax,[bx],看看是否会出错。
add dx,ax
inc bx
loop s
mov ax,4c00H
int 21h
code ends
end
调试时可以先用u指令查看地址,g直接跳过循环,也可以执行到loop时,p指令结束循环。
上面的题目有两个问题:类型匹配和越界问题。我们用ax寄存器作中介来解决这两个问题。后面还有更好的解决方法
我们经常会碰到用循环处理连续内存单元数据的问题。
5.4 安全空间
dos和其它合法程序一般不使用0:200-0:2ff
这256个字节,所以我们可以向这段空间里写入数据。
谨慎起见,先用d指令查看一下。
将ffff:0~ffff:b复制到0:200~0:20b
assume cs:code
code segment
mov bx,0
mov cx,12
s: mov ax,0ffffH
mov ds,ax
mov dl,[bx]
mov ax,0020H
mov ds,ax
mov es:[bx],dl
inc bx
loop s
mov ax,4c00H
int 21H
code ends
end
改进
assume cs:code
code segment
mov ax,0ffffH
mov ds,ax
mov ax,0020H
mov ds,ax ;优化后
mov bx,0
mov cx,12
s: mov dl,[bx]
mov es:[bx],dl
inc bx
loop s
mov ax,4c00H
int 21H
code ends
end
6.包含多个段的程序
6.1 在代码段中使用数据
assume cs:codesg
codesg segment
.. ..
数据
..
start:
..
代码
..
codesg ends
end start
要有start
标号表明代码开始地址,end start
会告诉编译器开始和结束地址,否则会把数据当作代码执行。
如果没有start
,则会从第一个可识别的机器码开始执行。
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;dw:define word,16 bytes
start:
mov bx,0 ;数据偏移地址即代码段最开始,即0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
codesg ends
end start
dw
定义的8个字型数据地址分别为:cs:0,cs:2,...,cs:e
处。
在代码段中使用栈实现逆序
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0 ;32 bytes for stack
start:
mov ax,cs
mov ss,ax
mov sp,20h ;栈底 ss:sp=cs:30
mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0: pop cs:[bx]
add bx,2
loop s0
mov ax,4c00h
mov 21h
codesg ends
end start
6.2 将数据、代码、栈放入不同段
assume cs:code, ds:data, ss:stack
data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start:
mov ax,stack ;段名代表段地址
mov ss,ax
mov sp,20h
mov ax,data
mov ds,ax
mov bx,0
mov cx,8
s: push [bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0: pop [bx]
add bx,2
loop s0
mov ax,4c00h
mov 21h
code ends
end start
assume
是伪指令,是给编译器看的,cpu并不会将cs,ds,ss
指向code,data,stack
。
如果段中的数据占N个字节,则实际占有的空间为 16 × ( N / 16 + 1 ) 16\times (N/16+1) 16×(N/16+1)