本文目的,总结汇编的使用方法,达到能无障碍阅读linux ,boot文件夹下的汇编代码,以及一些常用的C语言内联汇编。
使用书籍《汇编语言(第3版) 》王爽著
机器只认识电平脉冲,高低电平,也就是只有0->1 , 1 -> 0 , 0 , 1
很难查错,机器语言
汇编指令是机器指令便于记忆的书写格式。
3类指令:
汇编指令: 机器码的助记符,有对应的机器码
伪指令:没有对应的机器码,由编译器执行,计算机不执行。
其他符合:比如,加减乘除,由编译器识别,没有对应的机器码
指令和数据在存储器中存放。
聪明的大脑没有记忆无法进行思考。
CPU是如何从内存中读取信息,以及向内存中写入信息的。
这i一个应用上的概念,在存储器上没有区别,都是1和0。
CPU工作的时候会把有的信息看作指令,有的信息看作数据。
就像棋子,在盒子里都一样,在对弈时,就有不同的意义了。
一个存储单元 = 8bit = 1Byte
1KB = 1024Byte , MB,GB,TB
一个存储器被划分为多个存储单元,从0开始编号,这些编号就是存储单元在存储器中的地址。就像一条街,每个房子都有自己的门牌号。
CPU要进行数据的读写,需要有3类的信息交互:
地址信息,存储单元的地址
控制信息,器件的选择,读或写的命令。
数据信息,读写的数据。
总线:连接CPU和其他芯片的导线。
地址总线
控制总线
数据总线
CPU读写内存过程:
通过地址总线将要操作的地址告诉内存。
通过控制总线选中内存,告诉内存,CPU是要读还是写。
通过数据总线,把数据发过去,或者把数据读出来。
一个CPU有N根地址线,则可以说这个CPU的地址总线的宽度为N,这样的CPU最多可以寻址2的N次方个内存单元。
数据总线的宽度决定了CPU与外界的数据传送的速度
8根线,一次可以传1Byte。
16根线,一次可以传2Byte
CPU对外部的控制,都是通过控制总线完成的。这是一个总称。
是不同控制线的集合。
控制总线的宽度,决定CPU对外部器件的控制能力。
CPU的地址总线宽度==10 ,可以寻址的空间是1024个存储单元,1024个可寻到的内存单元就构成了这个CPU的内存地址空间。
每一台PC机中,都有一个主板,主板上有核心器件和一些主要器件,这些器件通过总线相连。
这些器件有:CPU,存储器,外围芯片组,拓展插槽(GPU,RAM等接口卡。)
计算机系统中,所有可用程序控制其工作的设备,必须受到CPU的控制。
CPU不直接控制外设,而是通过总线,接上外设的接口卡,通过控制接口卡,来控制外设。
随机存储器 RAM
只读存储器 ROM
接口卡也有BIOS,也有RAM
各类存储器,物理上独立,但有以下相同点:
都和CPU通过总线相连
CPU对这些设备进行读写时,都通过控制线发出内存读写命令。
对于CPU来说,这一切设备都被抽象为内存,我们称之为逻辑存储器。也就是我们说的内存地址空间。
内存地址空间,完成了对这些设备的抽象。
地址空间的大小受CPU地址总线宽度的限制。
CPU 由 运算器,控制器,寄存器 等器件构成。
在CPU中:
运算器进行信息处理
寄存器进行信息存储
控制器控制各种器件进行工作
内部总线连接各种器件,在它们之间进行数据传送。
8086 CPU的14个寄存器:
AX, BX, CX ,DX , SI , DI , SP , BP ,IP , CS, SS, DS , ES ,PSW
AX = AH, AL
BX = BH, BL
CX = CH, CL
DX = DH, DL
字节 , byte
字,word , 16bit,2Byte,高8位,低8位
cpu在执行8位寄存器运算时,不会往高位进位。
也就是说。操作al,进位不会写到ah中。
内存单元构成的存储空间是一个一维线性空间。
在CPU向地址总线发出物理地址前,必须先在内部形成这个物理地址。
什么是16位结构的CPU
运算器一次最多处理16位数据
寄存器的最大宽度为16位
寄存器和运算器之间的通路为16位。
在8086内部,一次性能够处理,传输,暂时存储的信息的最大长度是16位。
8086有20根地址线,可以传输20位地址。
但是他是16位的CPU,这里就涉及一个关键知识点:
段地址,偏移地址, CS:IP
CPU内部用两个16位地址合成一个20位的地址。
段地址,偏移地址, CS:IP
段地址偏移地址通过内部总线送入 地址加法器 ;
地址加法器 合成一个20位物理地址。
地址加法器 将这个电路
输入输出控制电路发送这个20位的物理地址给内存。
物理地址 = 段地址 << 4 + 偏移地址
条件受限,无法一次表达20位地址,所以分两次。
段是从CPU视角看到的概念。
CS , DS , SS , ES
CS :代码段寄存器
IP : 指令指针寄存器
任意时刻都是执行内存 CS<<4+IP 的内容。
CPU怎么知道一次要读多少呢?指令是变长的,CPU怎么知道呢?
mov -> 传送指令。
mov 不能用于设置CS,IP值。
一般用 jmp 指令。
形如:jmp 2AE3:3 , CS = 2AE3 H , IP = 0003 H, CPU 读取 2AE33H处 的指令。
如果只想修改IP,可以使用:
jmp ax == mov IP , ax
用汇编解释汇编,是一个好的方法。
就像用英文解释英文。
有了jmp就可以执行循环。
CPU并不认代码段,CPU只会忠实的去执行CS:IP 所指向的内存地址存放的指令。
环境搭建:
VS code配置汇编运行环境_不爱敲代码的入门程序猿的博客-CSDN博客_vscode配置汇编环境
MASM/TASM - Visual Studio Marketplace
VSCode下DOS汇编插件: VSCode DOS汇编的支持在DOSBox等模拟器中运行汇编相关的组件
DEBUG:
R , 查看,改变CPU寄存器的内容
D , 查看内存中的内容
E , 改写内存中的内容
U , 将内存中的机器指令翻译成汇编指令
T , 执行一条机器指令
A , 以汇编指令的格式在内存中写入一条机器指令。
任务:
1.. 使用A命令:
A 1000:0
编写汇编指令
再用A命令,把当前CS:IP的地方用A命令写入: jmp 1000:0
用T命令开始执行。
2.. 操作同上
3..
使用D命令,D FFF0:0 FF
打印内存值
使用E命令改写: E FFF0:0 1 2 3 4 5
再用D命令查看,发现并没有修改。
我们可以看到,这是因为,这个段,是不给改的。
地址 C0000~FFFFF 的24KB空间是 各类ROM的地址空间。
4..
修改这个内存,会影响显示。
因为 A0000 ~ BFFFF 是显示部分的内存。
小端,低位在小,高位在大。
DS寄存器,用于存放段地址。
mov al , [0]
[...] 表示内存单元,但是只有偏移地址是不能定位一个内存单元的,必须要有段地址。
当不指明段地址时,我们就用DS中的值作为段地址。
DS不支持数据直接传入段寄存器,所以需要先
寄存器到内存单元:
mov bx,1000H
mov ds,bx
mov [0] , al
16根数据线,一次可以传一个字
mov bx,1000H
mov ds,bx
mov ax,[0]
mov [0],cx
mov 寄存器,数据
mov 寄存器,寄存器
mov 寄存器,内存单元
mov 内存单元,寄存器
mov 段寄存器,寄存器
mov 寄存器,段寄存器
mov 内存单元,段寄存器
mov 段寄存器,内存单元
段寄存器不能执行加法。
通过DS存放数据段的段地址,再根据需要,进行偏移。
后进先出。
push ,pop 指令
关键问题:
CPU如何知道哪段内存是栈呢?
CPU怎么知道栈顶的单元?
新的寄存器:段寄存器SS , 寄存器SP
栈顶的段地址存放在SS,偏移地址存放在SP
任意时刻,SS:SP 指向栈顶元素。
栈是从高地址向低地址演进。每次执行push , SP = SP-2
每次执行pop, SP = SP+2
栈的大小由SP决定。
CPU并不会去管理越界的问题,需要我们自己小心。
push pop 也可以用于段寄存器和内存单元
push,pop就是一种内存传送指令。访问的内存地址由SS:SP给出。
mov是一步操作,push,pop是两步操作。
push: 改变SP,向SS:SP传送值。
pop: 先读取SS:SP的值,后改变SP
栈的最大范围是0~FFFFH
2的16次方,相当于64位。
内存里面是什么是由CPU决定的,更详细的说,是由CPU中几个特殊寄存器决定的。
CS , IP
SS , SP
DS
DS可以用来作为段地址,再debug模式下可以使用。
T命令执行修改栈段寄存器SS的指令会多执行一步。(这里涉及到中断机制。)
为什么2000:0~2000:f 会被奇怪的值占据?
编写源程序->对源程序进行编译链接
可执行文件:
程序和数据
相关的描述信息
assume cs:codesg
codesg segment
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4C00H
int 21H
codesg ends
end
源程序中有两种指令:
汇编指令,有机器码,最终被CPU执行。
伪指令,没有机器码,最终不被CPU执行,伪指令是由编译器来执行的。
segment , ends 是一对伪指令。
这对指令定义一个段,被编译器识别。
上诉例子的段名是 codesg
一个汇编程序是由多个段组成,这些段被用来存放代码、数据或当作栈空间来使用。
一个源程序中,所有将被计算机所处理的信息:指令、数据、栈、被划分到不同的段中。
一个有意义的汇编程序,至少要有一个段,这个段,用来存放代码。
end:
是一个汇编程序的结束标记。编译器看到end就结束对源程序进行编译。写完要用这个做好结束标记,否则编译器不知道什么时候结束。
assume:
这个伪指令的含义是“假设" 。
它假设某一 段寄存器 与 程序中用 segment...ends 定义的段相关联。
assume cs:codesg
编译程序可以将段寄存器跟程序段相关联。
上面语句是 将codesg 这个段跟段寄存器cs联系起来。
源程序文件中的所有内容称为源程序(包括伪指令,程序)
程序:源程序中最终由计算机执行,处理的指令或数据,称为程序。
程序最先以汇编指令的形式存在源程序中,经编译,链接后转变为机器码,存储在可执行文件中。
codesg就是一个标号。
标号指代一个地址,比如codesg 在segment的前面,作为一个段的名称,这个段的名称最终将被编译,连接程序处理为一个段的段地址。
其实是通过伪指令来组织的。
一个程序运行结束后,将CPU的控制权交还给使他能正常运行的程序,这个过程称之为:程序返回。
mv ax,4C00H
int 21H
这两句就是实现程序返回的语句。
直接用vscode
源程序文件 -> 目标程序文件 -> LST文件,这个是编译器将源程序编译为目标文件时产生的中间结果 -> CRF文件,交叉引用的文件名。
目前只关心目标文件
连接的作用:
源程序很大,分多个文件来编译。
调用库文件中的子程序。
目标文件中,有些内容还不能直接用来生成可执行文件,连接程序要最终处理这些信息。
直接在VSCODE就有这个功能。
程序已经执行了,只是没有显示
DOS中的命令解释器command.com ,也就是shell
找到文件 -> 将文件载入内存 -> 将CS:IP 指向程序入口 -> command停止运行,CPU开始执行可执行文件 -> 返回到command,等待用户下一次输入。
从写代码到执行:
编程 -> 1.asm -> 编译 -> 1.obj -> 连接 -> 1.exe -> 加载 -> 内存中的程序 -> 运行
直接run的话。command将指针指向程序入口后,等程序返回就可以。
debug 不放弃CPU的控制权。
CX寄存器存放了 程序的长度。
dos加载exe文件的过程。
找到一个空闲的地方, SA:0
在这个内存前256字节创建一个 程序段前缀 PSP , DOS利用PSP与被加载程序进行通讯。
SA+10H:0 , 程序装入地址。
设置CS:IP 指向 程序入口。
上诉过程不涉及重定位
返回语句要使用P命令。
在堆栈这个地方,会记录当前的一些信息,
看上去会有pop出来的值,有IP,CS,SS 等寄存器的值。
每次POP,push , 堆栈附近会有变化。
完整描述一个内存单元的内容需要2个信息:
内存单元的地址
内存单元的长度(类型)
mov ax,[bx]
偏移地址在bx中。
段地址还是在ds中。
描述性符号:"()"
"( )" 可以有3种类型:
寄存器名
段寄存器名
20位内存地址
"( )" 相当于指针的解引用,找到( )里面的数值,把这个数值作为地址,去访问这个地址。
字节型,字型 , 由寄存器决定。 al, bl, cl 字节型 , ds,ax,bx为字型。
约定符号idata表示常量,
有一个例子,描述[BX]的操作。
loop指令:
(cx) = (cx) - 1
判断cx中的值不为零,则转到标号处,如果为零,则向下执行。
cx存放循环次数。
在汇编源程序中,数据不能以字母开头,要补个0。
g命令,
g 0012
一直执行,直到IP = 0012H
p命令,
在loop时,使用p,可以让循环一直执行到(cx) =0为止。
masm中
mov al , [0]
会被解析成,mov al , 0
masm 会把 [idata] 解释为 idata
可以用bx先存偏移地址,然后再[bx]
也可以显示注明: ds:[0]
assume cs:code
code segment
mov ax,0ffffH
mov ds,ax
mov bx,0
mov dx,0
mov cx,12
s: mov al,[bx]
mov ah,0
add dx,ax
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end
bx 可以作为i++
ds: , cs: , ss: , es: 这些称为段前缀,如果没有的话,默认用ds
不能乱改写内存地址。
ds = 0
mov ds:[26h],ax
会被解析为:mov [0026h], ax
这样会引发系统错误。
在实模式下,直接用汇编操作真实硬件,这种纯DOS是没有能力对硬件系统进行全面的,严格的管理的。
在linux,windows这种系统都是运行在保护模式下,CPU不会让这种事情这么简单的发送。
不要使用 00200 ~ 002ff 这段地址。
es 是一个段前缀, 通过这个寄存器,在内存拷贝过程中,不必反复修改ds的值。
用9条指令完成这个实验:
assume cs:code
code segment
mov sp,0040h
mov bx,3f3eh
mov cx,20H
mov ss,cx
s: push bx
sub bx,0202H
loop s
mov ax,4c00h
int 21h
code ends
end
什么空间是合法的
程序获得所需空间的方法有2种:
加载程序的时候为程序分配 (目前只讨论这种情况)
程序执行过程中,向系统申请。
dw 是 define word 的缩写。
CS 存放代码的段地址,CS:0 , CS:2 ...就是dw的数据。
end除了能通知程序结束,还可以通知程序的入口的地方。
“ end 标号 ” ,程序是从这个标号开始。
程序框架安排:
assume cs:code
code segment
.
.
数据
.
.
start:
.
.
代码
.
.
code ends
end start
在数据段 和代码段直接放一个栈。
不同的段要有不同的段名。
code , data , stack
data segment
...
data ends
stack segment
...
stack ends
code segment
...
code ends
可以通过
assume cs:code,ds:data,ss:stack
将cs,ds,ss跟这几个段相连。
cs : code segment
ds : data segment
ss : stack segment
解释了不同段之间的关系。
跟C语言一致
61H代表a,有一个码表
dec | hex | char | dec | hex | char | dec | hex | char | dec | hex | char | |||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
032 | 20 | 056 | 38 | 8 | 080 | 50 | P | 104 | 68 | h | ||||
033 | 21 | ! | 057 | 39 | 9 | 081 | 51 | Q | 105 | 69 | i | |||
034 | 22 | " | 058 | 3A | : | 082 | 52 | R | 106 | 6A | j | |||
035 | 23 | # | 059 | 3B | ; | 083 | 53 | S | 107 | 6B | k | |||
036 | 24 | $ | 060 | 3C | < | 084 | 54 | T | 108 | 6C | l | |||
037 | 25 | % | 061 | 3D | = | 085 | 55 | U | 109 | 6D | m | |||
038 | 26 | & | 062 | 3E | > | 086 | 56 | V | 110 | 6E | n | |||
039 | 27 | ' | 063 | 3F | ? | 087 | 57 | W | 111 | 6F | o | |||
040 | 28 | ( | 064 | 40 | @ | 088 | 58 | X | 112 | 70 | p | |||
041 | 29 | ) | 065 | 41 | A | 089 | 59 | Y | 113 | 71 | q | |||
042 | 2A | * | 066 | 42 | B | 090 | 5A | Z | 114 | 72 | r | |||
043 | 2B | + | 067 | 43 | C | 091 | 5B | [ | 115 | 73 | s | |||
044 | 2C | , | 068 | 44 | D | 092 | 5C | \ | 116 | 74 | t | |||
045 | 2D | - | 069 | 45 | E | 093 | 5D | ] | 117 | 75 | u | |||
046 | 2E | . | 070 | 46 | F | 094 | 5E | ^ | 118 | 76 | v | |||
047 | 2F | / | 071 | 47 | G | 095 | 5F | _ | 119 | 77 | w | |||
048 | 30 | 0 | 072 | 48 | H | 096 | 60 | ` | 120 | 78 | x | |||
049 | 31 | 1 | 073 | 49 | I | 097 | 61 | a | 121 | 79 | y | |||
050 | 32 | 2 | 074 | 4A | J | 098 | 62 | b | 122 | 7A | z | |||
051 | 33 | 3 | 075 | 4B | K | 099 | 63 | c | 123 | 7B | { | |||
052 | 34 | 4 | 076 | 4C | L | 100 | 64 | d | 124 | 7C | | | |||
053 | 35 | 5 | 077 | 4D | M | 101 | 65 | e | 125 | 7D | } | |||
054 | 36 | 6 | 078 | 4E | N | 102 | 66 | f | 126 | 7E | ~ | |||
055 | 37 | 7 | 079 | 4F | O | 103 | 67 | g | 127 | 7F | DEL |
可以用db来完成ascii码跟机器码的转换
db 'unIX' db 75H,6EH,49H,58H mov al,'a' mov al,61H
小写比大写字母 的ascii码值大0020H
从二进制来看,
0000,0000,0010,0000B
小写字母第5位是1,大写是0
可以通过这个办法,不去判断直接将这个位置位。
mov ax,[bx+200]
(ax) = ( (ds)*16 + (bx) + 200 )
mov ax,[bx+200] 还有几种写法:
mov ax,200[bx] , 数组
mov ax,[bx].200 ,结构体
可以为数组提供便利的机制。
SI,DI 是跟bx功能近似的寄存器,但是不能被拆分为2个8位寄存器
mov ax,[bx+si]
等价于
mov ax, [bi] [si] , 二维数组
mov ax,[bx+si+idata]
一般来说,需要暂存数据的时候,我们都应该使用栈
assume cs:codesg,ss:stacksg,ds:datasg stacksg segment dw 0,0,0,0,0,0,0,0 stacksg ends datasg segment db '1. display ' db '2. brows ' db '3. replace ' db '4. modify ' datasg ends codesg segment start: mov ax,stacksg mov ss,ax mov sp,16 mov ax,datasg mov ds,ax mov bx,0 mov cx,4 s0: push cx mov si,3 mov cx,4 s: mov al,[bx+si] and al,11011111B mov [bx+si],al inc si loop s add bx,16 pop cx loop s0 mov ax,4c00h int 21H codesg ends end start
处理的数据在什么地方
要处理的数据多长
reg代表寄存器,sreg代表段寄存器
reg : ax,bx,cx,dx,~h,~l,sp,bp,si,di
sreg : ds,ss,cs,es
只有这4个寄存器可以用在[...] , 也就是只有这几个寄存器可以代表计数器去做迭代。
bx,bp 不能同时出现在[...]
si,di 不能同时出现在[...]
在[...] 中使用bp ,没有显式给出段地址,段地址默认是ss
bx默认是ds
一个操作栈,一个操作data
绝大部分机器指令都是进行数据处理的指令,大致分3种:读取,写入,运算。
机器指令这一层并不关心数据值是多少,而关心指令执行前一刻,它将要处理的数据在哪。
要处理的数据可以在3个地方:CPU内部,内存,端口。
3个概念来表达数据的位置
立即数,在CPU的指令缓冲器中,在汇编中直接给出。
寄存器,在寄存器中,在汇编指令中给出相应的寄存器名
段地址(SA):偏移地址(EA) , 在内存中。
当数据存放在内存中,有多种方式来给定这个内存单元的偏移地址,这被称为寻址方式。
直接寻址: [idata]
间接寻址: [bx]
寄存器相对寻址: [bx].idata(结构体) ,idata[si] , idata[di](数组) , [bx] [idata] (二维数组)
基址变址寻址:[bx] [si] (二维数组)
相对基址变址寻址:[bx].idata[si] 表格(结构)中的数组, idata[bx] [si] 二维数组
怎么知道有多长?
有两种长度字word,字节byte
有几种方式:
寄存器指明处理数据的大小
没有寄存器名,用操作符 word ptr , byte ptr 指明内存单元的长度 , mov word ptr ds:[0] , 1
其他方法,默认使用字节长度。
访问结构体,跟C语言结合。
[bx].10H[si]
bx找到结构体开头,10H偏移到具体的位置,找到这个结构体的元素, 这个元素是个数组,要用si去访问数组中的元素。
除法注意事项:
除数,可以是8位或者16位,在寄存器或内存单元中
被除数,默认放在AX,或者DX和AX中,除数为8,被除数为16,除数为16,被除数为32
结果,除数为8位,AL存除法操作的商,AH存储除法操作的余数,如果除数为16位,AX存商,AD存余数。
AX低位,AD高位。
使用除法指令计算 100001/100
mov dx,1 mov ax,86A1H ;(dx)*10000H + (ax) = 100001 mov bx,100 div bx
db , dw ,分别定义字节型数据和字型数据
dd 用来定义dword 双字数据
dup是一个操作符。
db 重复次数 dup (重复的字节)
dw, dd 类似
字符替换,不困难。
修改IP ,或者修改CS和IP,的指令称为转移指令。
只修改IP,称为段内转移,比如:jmp ax
同时修改CS和IP时,称为段间转移,比如:jmp 1000:0
IP的修改范围不同,段内转移还分为:短转移和近转移
短转移IP的修改范围:-128~127
近转移IP的修改范围:-32768 ~ 32767
转移指令分几类:
无条件转移,jmp
条件转移指令
循环指令
过程
中断
获得标号的偏移地址
assume cs:codesg codesg segment start: mov ax,offset start ;相当于mov ax,0 s: mov ax,offset s ;相当于mov ax,3 codesg ends end start
jmp修改CS:IP
也可以只修改IP
jmp指令要给出两种信息
转移的目的地址
转移的距离
jmp short 标号 : (IP) = (IP) + 8位位移
实现段内短转移,对IP修改范围是-128~127。
其机器码中,存的是下一条指令相对当前指令的位移。
jmp near ptr 标号 : (IP) = (IP) + 16位位移
JMP far ptr 标号
机器码中包含目标地址的段地址和偏移地址
jmp 16位寄存器
ip = (16位寄存器)
转移地址在内存中。
jmp word ptr 内存单元地址(段内转移)
从内存单元地址处开始存放着一个字,是转移的目的偏移地址
jmp dword ptr 内存单元地址 (段间转移)
CS = (内存单元地址+2)
IP = (内存单元地址)
有条件转移指令,短转移,机器码中包含位移,不是目的地址。ip范围 -128,127
格式:
jcxz 标号 (如果cx = 0 跳转到标号处)
if( (cx) == 0 )
jmp short 标号
9.2
mov cl,[bx]
mov ch,0
jcxz ok
inc bx
loop指令中包含的是短转移,
(cx) --
if( (cx) != 0 )
jmp short 标号
9.3
inc cx
机器码中不包含转移的目的地址。
超界就会报错。
这个例子,可以把一段空的地方填入指令。
只是写内存的操作。
ret指令用栈中的数据,修改IP的内容,从而实现近转移。
retf 指令用栈中的数据,修改CS,IP的内容,从而实现远转移。
ret 等价于:
pop IP
retf
pop IP
pop CS
将当前IP或CS IP压入栈,然后转移
call 标号 , IP+16位数
push IP
jmp near ptr 标号
call far ptr 标号
push CS
push IP
jmp far ptr 标号
call ax
push IP
jmp ax
call word ptr 内存单元地址
push IP
jmp word ptr 内存单元地址
call dword ptr 内存单元地址
push CS
push IP
jmp dword ptr 内存单元地址
通过 call 去调用子程序,子程序ret返回到call后面
子程序框架:
标号: 指令 ret
具有子程序的源程序框架
assume cs:code code segment main: .. .. call sub1 .. .. mov ax,4c00h int 21H sub1: .. .. call sub2 .. .. ret sub2: .. .. ret code ends end main
乘法指令
注意:
两个相乘的数:都是8位,或者都是16位。
8位:一个放在AL中,一个放在8位寄存器,或者一个内存字节单元中。
16位:一个放在AX中,一个放在16位reg或内存字单元中
结果:如果是8位乘法,结果存在AX中,16位,dx高位,AX低位
mul reg
mul 内存单元
mul byte ptr ds:[0] , (ax) = (al) * ( (ds)*16+0 )
mul word ptr [bx+si+8] , (ax) = (ax) * ( (ds)*16 + (bx) + (si) + 8 ) 低16位; (dx) = (ax) * ( (ds)*16 + (bx) + (si) + 8 ) 高16位
call,ret 可以实现模块化设计
用一个寄存器来保存传递过去的值,用一个数据段去保存计算结果。
参数太多,用内存传递。
在这个例子中,我们可以看到使用栈进行参数传递是如何进行的。
这个例子中,bp寄存器始终存放着本函数栈开始的地方。
函数的入参,总是在bp之前,通过bp+x的方式去访问入参。
函数内部的局部变量,总是在bp之后,通过bp-x的方式去访问局部变量。
被调用的函数,先要保存好上一个bp的值,然后再做其他事,最后把bp的值再弹出。
传参会在函数返回后,立刻被释放,具体的方式是通过修改SP寄存器的值去完成。
参数传参之所以不会被保存,主要是因为这个原因。
所谓的函数传参在函数调用结束之后,就释放了,就是这个意思,这个栈被回收使用了。
void add(int,int,int); main(){ int a = 1; int b = 2; int c = 0; add(a,b,c); c++; } void add(int a,int b,int c){ c = a+b; }
mov bp,sp sub sp,6 mov word ptr [bp-6],0001 ;int a mov word ptr [bp-4],0002 ;int b mov word ptr [bp-2],0000 ;int c push [bp-2] push [bp-4] push [bp-6] call ADDR add sp,6 inc word ptr [bp-2] ADDR: push bp mov bp,sp mov ax,[bp+4] add ax,[bp+6] mov [bp+8],ax mov sp,bp pop bp ret
10.2应该是CX的问题,子程序修改了CX,应该用栈存起来,最后要把CX改回来。
解决这个问题的便捷:
在子程序开始时,将所有用到的寄存器都保存起来,在子程序返回前,再全部恢复。
先不处理。
CPU内部寄存器中,有一种特殊的寄存器,具有3种作用:
用来存储相关指令的某些执行结果
用来为CPU执行相关指令提供行为依据
用来控制CPU的相关工作方式。
标志寄存器 flag,16位,存储的信息被称为程序状态字(PSW) 。
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
OF | DF | IF | TF | SF | ZF | AF | PF | CF |
zero flag 执行结果为0,zf=1
大多数情况下,传送指令不会影响ZF , 运算指令影响ZF
奇偶标志位,记录相关指令执行后,所有bit中的个数是否为偶数,如果是,pf=1,不是 pf = 0
SF是否为负,如果是负,sf = 1,如果非负,sf = 0
用补码来表示负数,不管我们怎么看,CPU算出的结果都包含了两种含义。
检测结果的最高位符号位是否是负数。
进位标志位,假象更高位。
发生进位或者借位时,cf=1。
溢出标志位,发生溢出,OF=1,没有发生溢出。OF =0
溢出和进位没有关系。
溢出全部看成有符号数运算。
带进位的加法指令。
adc obj1 obj2
(ax) = (ax) + (bx) +CF
提供这个指令可以完成下面的操作:
add al,bl
adc ah,bh
当al,bl发生溢出时,ah+bh+1就能算对了。
用inc不用add是因为防止清空进位。
带借位减法。
sbb ax bx
(ax) = (ax) - (bx) - CF
cmp不保存运算结果。
如果相等 zf = 1
e : equal
b : below
a : above
DF 方向标志位
movsb 根据 df 的值,决定si,di的递增还是递减
DF = 0 , inc
DF = 1 , dec
movsw , 传字。
配合 rep使用。
rep movsb
等价于
s: movsb
loop s
rep还是根据cx寄存器的值。
df位的控制
cld 指令 , 将DF置0
std 指令 , 将df 置1
f : flag
标志寄存器压栈,与 出栈
含义 | 标志 | =1 | =0 |
---|---|---|---|
溢出 | OF | OV | NV |
负数 | SF | NG | PL |
0 | ZF | ZR | NZ |
偶 | PF | PE | PO |
进位 | CF | CY | NC |
方向,di,si | DF | DN | UP |
可以在执行完当前正在执行的指令后,检测到从CPU外部发送过来的或内部产生的一种特殊信息,并且可以立即对所收到的信息进行处理。这种特殊的信息,我们称之为:中断信息。
中断: CPU不再接着往下执行,而是去处理这个特殊信息。
CPU中断可以来自内部和外部,主要讨论内部
发生情况:
除法错误,溢出问题
单步执行
执行into指令
执行int指令
中断类型码:
除法错误: 0
单步执行:1
执行into指令:4
执行int指令,int N ,N是提供给CPU中断类型码
收到中断信息,需要进行处理。
CPU要找到中断处理程序的入口。
CPU用8位的中断类型码,通过中断向量表找到对于的中断处理程序的入口地址。
中断向量 : 中断处理程序的入口地址。
中断向量表存储位置: 0000:0000 ~ 0000:03FF
CPU硬件自动完成, 通过中断类型码 找到中断向量,设置CS:IP。
这个过程叫做中断过程。
中断过程的描述:
取得中断类型码N
pushf
TF = 0 , IF = 0
PUSH CS
PUSH IP
(IP) = (N*4) , (CS) = (N * 4 +2)
中断处理程序编写方法跟子程序类似。
只是ret 换成 iret
iret 等价于:
pop ip
pop cs
popf
提示divide overflow
把中断处理函数写到对应的位置,等待CPU来执行。
用两个标号标记中断处理函数的开始和结束,通过movsb来复制到对应的位置。
要把字符放到do0内部,不能放在外面的段。
在0:0 , 0:2 中放CS:IP就可以。
CPU如果执行完一条指令,发现TF为1, 产生单步中断,引发中断过程。单步中断类型码为 1 ,它引发的中断过程如下:
取得中断类型码1
标志寄存器入栈 , TF,IF 设置为0
CS,IP 入栈
IP = 1*4 , CS = 1 * 4 +2
debug ,用的就是用这个中断机制。
一般中断发生都会响应。
修改SS的值时,中断不会工作。
因为ss与sp共同决定栈的位置,如果此时去中断,有错。
int指令引发的中断
调用一个中断程序,类似调用子程序。
int , iret
类似于call,ret
可以通过改压入的CS:IP 来完成循环。
在系统板的ROM中存放着一套程序,称之为BIOS。其包含:
硬件系统的检测和初始化程序
外部中断和内部中断的中断例程
基于对硬件设备进行I/O操作的中断例程
其他的硬件系统相关的中断例程
DOS中断例程是操作系统向程序员提供的编程资源。
和硬件设备相关的DOS中断例程中,一般都调用了BIOS的中断例程。
安装过程:
开机后,CPU一加电,初始化 (CS) = 0FFFFH , (IP) = 0 , 自动从这个单元开始执行,CPU执行跳转指令,去执行BIOS中硬件系统检查和初始化程序
初始化程序建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表。这些中断例程已经固化到ROM中,一直存在于内存中。
硬件系统检测和初始化完成后,调用int 19h 进行操作系统导引,从此将计算机交由操作系统控制。
DOS启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。
int 10h中断例程是BIOS提供的中断例程,其中包含了多个和屏幕输出相关的子程序。
int 10h 中断例程
mov ah,2 ;置光标 mov bh,0 ;第0页 mov dh,5 ;行号 mov dl , 12 ;列号 int 10h
ah = 2 ,10h号中断例程的2号子程序,功能为设置光标。需要行号,列号,页号作为参数
bh页号,有8页,每页4kB , 地址 B8000H ~ BFFFFH
通常情况是0页, B8000H~B8F9FH
int 21H中断例程的4CH号功能,即程序返回功能。
mov ax,4c00h int 21h
CPU通过总线相连的芯片除了各类存储器以外,还有3种芯片:
各种接口卡 (网卡,显卡)上的接口芯片,它们控制接口卡进行工作
主板上的接口芯片,CPU通过它们对部分外设进行访问
其他芯片,用来存储相关的系统信息,或进行相关的输入输出处理。
这些芯片中,都有一组由CPU读写的寄存器,这些寄存器存在以下两点相同:
都和CPU的总线相连,当然这种连接是通过它们所在的芯片进行的
CPU对它们进行读写时,都通过控制线向它们所在的芯片发出端口读写命令。
对这些寄存器当做端口,对它们统一进行编制,建议统一的端口地址空间,每个端口的地址空间都有一个地址。
CPU可以直接读写以下3个地方的数据。
CPU内部寄存器
内存单元
端口
CPU通过端口地址,定位端口。
端口所在的芯片和CPU通过总线相连。
端口地址范围:0~65535
只能用in, out
访问端口:
in al,60h ;从60h号端口读入一个字节
只能用ax, al
out 20h,al ;往20H端口写入一个字节
芯片特征:
包含一个实时时钟,有128个存储单元的RAM存储
该芯片靠电池供电,关键实时时钟还能正常工作,RAM中的信息不丢失
128B 的RAM中,内部实时钟占用0~0dh单元来保存时间信息。其余大部分保存系统配置信息,供系统启动时,BIOS程序读取,BIOS也提供了相关的程序,使我们可以在开机的时候去配置CMOS RAM中的系统信息。
该芯片内部有2个端口,70H,71H。CPU通过这两个端口来读写CMOS RAM
70h为地址端口,71h为数据端口,
shl : shift left
shr : shift right
mov al,8 out 70h,al in al,71h mov ah,al mov cl,4 shr ah,cl and al,00001111b ;读入月份数据到al,十位存在ah,个位存在al
CPU除了有运算能力以外,还需要有IO能力。
外设输入随时都可能发生,CPU怎么知道?
CPU从何处得到外设输入?
CPU通过端口和外部设备进行联系
外中断源:
可屏蔽中断 ,IF = 1 ,响应外部中断,
sti : IF = 1
cli : IF = 0
不可屏蔽的中断 , 中断类型码固定为2
基本上都是可屏蔽的中断。
键盘输入,按下一个键,芯片产生扫描码,松开也产生一个扫描码, 分别叫做 通码,断码。
断码 = 通码 + 80H
引发9号中断
执行int9 中断例程
读出60h端口中的扫描码
对于字符扫描码,送入内存中的BIOS键盘缓冲区。对于控制键,切换键,则转化为状态字节,写入内存中存储状态字的单元
对键盘系统的相关控制,比如向相关芯片发出应答信息
0040:17 单元存储键盘状态字节
0:右shift
1:左shift
2:Ctrl
3:Alt
4: ScrollLock
5:NumLock
6:CapsLock
7:Insert
键盘出入处理过程:
键盘产生扫描码
扫描码送入60h端口
引发9号中断
CPU执行int9中断例程处理键盘输入
我们想做一个程序捕获某个键盘输入,然后做一些事情
可以修改中断向量表,在这个程序初始化的时候,将这个地方改掉,程序退出,然后恢复。
如何保证其他按键正常呢?我们识别到之后,从定向回去原来的int9就可以,我们只捕捉我们的中断。
端口和中断机制是CPU进行IO的基础。
数据传送指令
mov , push , pop , pushf, popf ,xchg
运算指令
add, sub , adc , sbb , inc ,dec , cmp , imul , idiv , aaa ,
逻辑指令
and , or , not , xor , test ,shl ,shr , sal , sar , rol ,ror,rcl ,rcr
转移指令
jmp , jcxz , je ,jb ,ja ,jnb ,jna ,loop , call ,ret ,retf ,int ,iret
处理机控制指令
cld, std , cli, sti , nop , clc , cmc ,stc, hlt ,wait ,esc , lock
串处理指令
movsb , movsw , cmps ,scas ,lods ,stos ,rep ,repe,repne
如何有效的组织数据?
a db 1,2,3,4,5,6,7,8
b dw 0
可以用b 来代替 cs:[8]
类似C语言中的变量。
a[si] ,类似C语言中的数组。
查表的相关技巧
查表效率比计算快。
函数表。
缓冲区,在控制符按下时,操作标志位。
先清空,然后读入。
清空,然后把固定地址的内容刷新进来。
解释了一些C语言中跟汇编之间的联系。