文章目录
前言
由于,最近一直在学以及使用了linux操作系统。我对于操作系统的底层运行,对于linux的内核产生了极其浓厚的兴趣。因此,产生了想要自写一个小操作系统来加深对于操作系统内核的理解。话不多说,从今天开始!!!
在正式开始前,我们首先需要学会c与汇编(至少知道基础语法结构啥的),因为后续基本上是要使用到c与汇编为基础的代码的!!!!
汇编语言 == > 推荐王爽的教材。
==> 以下的简要介绍是基于王爽的汇编以及博主:洋葱汪
在此十分感谢提供帮助的书籍已经博主,如有错误之处欢迎指出!!
汇编基础简要介绍:
🙌先安装能编译汇编的环境
这里我首推使用VScode进行安装(因为直接下载插件使用就行了,不用搞其他那么多的弯弯绕绕)
打开vscode,安装masm/tasm这个插件:
建立一个文件夹用于后续的汇编代码存放(我的是asm),并在这里建立一个汇编文件(这里的汇编代码文件一定要以asm为后缀)
然后打开这个文件夹,按下:ctrl+shift+p,在这个框里输入setting,然后找到工作区这个选项,进行配置:
这里的设置我参考了该博主的:web—mark 十分感谢!!!
{
"masmtasm.ASM.emulator": "msdos player",
"masmtasm.dosbox.run": "exit",
"masmtasm.ASM.assembler": "MASM-v5.00",
"masmtasm.ASM.actions": {
"TASM": {
"baseBundle": "<built-in>/TASM.jsdos",
"before": [
"set PATH=C:\\TASM"
],
"run": [
"TASM ${file}",
"TLINK ${filename}",
">${filename}"
],
"debug": [
"TASM /zi ${file}",
"TLINK /v/3 ${filename}.obj",
"TD ${filename}.exe"
]
},
"MASM-v6.11": {
"baseBundle": "<built-in>/MASM-v6.11.jsdos",
"before": [
"set PATH=C:\\MASM"
],
"run": [
"masm ${file};",
"link ${filename};",
">${filename}"
],
"debug": [
"masm ${file};",
"link ${filename}.OBJ;",
">debug ${filename}.exe"
]
},
"MASM-v5.00": {
"baseBundle": "<built-in>/MASM-v5.00.jsdos",
"before": [
"set PATH=C:\\MASM"
],
"run": [
"masm ${file};",
"link ${filename};",
">${filename}"
],
"debug": [
"masm ${file};",
"link ${filename}.OBJ;",
">debug ${filename}.exe"
],
"support": [
"jsdos",
"dosbox",
"dosboxX",
"msdos player"
]
}
}
}
然后,回到刚刚那个页面,右击即可运行:(左下角这里,可以选择以那种模式运行)
在以下学习过程中,我通常使用调试来进行运行的:
-t 就是逐条执行的意思
-r 查看各寄存器当前的值
-q 离开debug
以下内容建议,先去博主:洋葱汪 处大致看一遍,或者看一遍书.
因为下述内容,是我在学习过程中的简记!!!
😜一、总线
1、总线 ==> 是连接各个部件的信息传输线,主要用于各个部件共享的传输介质(cpu对于各部件管理也主要依靠总线,利用控制总线、数据总线、地址总线)。
地址总线:cpu利用其进行寻址,找到对应的内存进行读取信息。其宽度,代表了cpu的寻址能力!
数据总线:cpu与内存之间的数据传送。
控制总线:cpu对于外部器件的控制。
2、cpu的读写过程 ==> 当cpu需要读取信息时,首先利用地址总线,将内存中对应地址的信息发出;然后再利用控制线,对该地址发出读/写命令;利用数据线进行传送对应的数据。 当然,cpu对于外设则是通过拓展插槽处的接口卡进行读取的,例如:cpu想要再屏幕输出对应的信息,则cpu会向你的4060显卡发出对应命令,再由你的显卡向屏幕进行发出对应的信息。
3、受限于地址总线的宽度(即寻址能力有限),且方便于管理,cpu会将各类存储器看为一个逻辑的地址空间。因为这类空间通常比较大,因此,cpu将会利用段存储的方法进行标识。即cpu利用两个16位地址(一个是段地址,一个是偏移地址)来合成一个20位的物理空间地址。
物理地址=段地址x16+偏移地址。
😜二、寄存器
一般来说cpu是由运算器、控制器、寄存器等器件进行构成的,而这些内部器件的信息传输当然也是由前面所说的总线相连(片内总线)。
==>运算器:进行各种信息的处理;控制器:协调各器件有序的工作;寄存器:进行信息存储。
在8086cpu中,主要有以下几种寄存器:
通用寄存器 :
数据寄存器:
AX ⇒ 累加
BX ⇒ 基址
CX ⇒ 计数
DX ⇒ 数据
指针寄存器:
SP ⇒ 堆栈指针
BP ⇒ 基址指针
变址寄存器:
SI ⇒ 源变址
DI ⇒ 目的变址
控制寄存器:
IP ⇒ 指令指针
psw ⇒ 标志
段寄存器:
CS ⇒ 代码段
SS ⇒ 堆栈段
DS ⇒ 数据段 ==> 这里通常用[address]来表示一个偏移地址位address的内存单元(段地址为DS)这里的address可以是数值也可以是寄存器
ES ⇒ 附加段
以上的寄存器于8086中都是16位的。因此,8086cpu中为方便处理,数据分为了两种,一是字节,由8bit组成;一种是字,由16bit组成,也就是两个字节组成。8086一个寄存器是可以分为高8位与低8位的,例如AX就可以分为AH与AL。AH与AL都可以独立存放8bit的数据。
==> 8086cpu中有着20位的地址总线,寻址能力就达到了2^20=1mb。但是其寄存器是16位结构的,不能满足20位地址总线的要求。因此8086设计了一种方法来解决这个问题:即在内部用2各16位的地址进行合成一个20位的物理地址进行寻址。物理地址=段地址x16(向左移动一位,在末尾添加个0)+偏移地址,这里就是操作系统中的段式存储了。
==> 8086中cpu的工作过程简述,结合上面的所述:8086中采用段式存储,因此cpu就会将CS作为段地址,而IP作为段内的偏移地址,因此CS:IP就是cpu指令(代码段)的存储位置!
1、从CS:IP指向的内存单元中读取指令,将读到的指令送入指令缓冲器;
2、IP+所读指令的长度,以此达到指向下一条指令(这里是因为cpu的 指令采取顺序存储);
3、执行指令,重复1、2、3步骤。
==> 任意时刻,SS:SP 都指向栈顶元素。在8086中,若入栈时,栈顶都是从高地址向低地址方向增长!
push ax 将ax中数据放入栈中,此时会进行如下步骤:
1、sp=sp-2(因为一个内存单元为一个字节,而ax有2个字节);
2、将ax数据存入内存当中,依照ah放在高地址处、a放在低地址处。
pop ax同理,sp=sp+2
😜灵活的定位内存地址
首先,要记住的是在8086中,是不允许将数值直接传进段寄存器的。因此,要想设置段寄存器,我们必须利用通用寄存器进行传参。
而后,对于数据段的内存地址我们可以直接使用[bx]这种格式进行代指内存地址,因为8086中会默认去读取ds:[bx]处的数据,即段地址为ds,偏移地址为[bx]。当然,对于代码段我们也可以使用cs:[bx]这种来进行表示。
除此之外,还可以利用[bx+data]的格式来表示一个内存单元,如下格式:
[200+bx]
200[bx]
[bx].200
即这个表示的是偏移地址为bx+200处的内存地址。
参考如下代码:
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC';转为大写
db 'MinIx';转为小写
datasg ends
codesg segment
start:
mov ax, datasg
mov ds, ax
mov bx, 0 ;初始ds:bx
mov cx, 5
s:
mov al, 0[bx] ;大小写字母的ascii的二进制下的差距只在第6位上,大写字母第六位上全为0,小写全为1,也就是小写字母ascii值比大写的大了20h
and al, 11011111b ;转为大写字母,指向了BaSiC
mov 0[bx], al ;写回
mov al, 5[bx] ;[5 + bx],指向了MinIx
or al, 00100000b ;转为小写字母
mov 5[bx], al
inc bx
loop s
mov ax, 4c00h
int 21h
codesg ends
end start
SI 源变址和DI 目的变址 这两个在8086中功能是与bx功能相近的寄存器,但是要注意的是si与di是不能被拆分成两个8位寄存器进行使用的!!
;1.asm
assume cs:code,ds:data
data segment
db 'welcome to masm!';用si和di实现将字符串‘welcome to masm!"复制到它后面的数据区中。
db '................' ;都是16各字符
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0
mov cx,8 ;这里8是因为,si是16位寄存通常定位的是一个内存单元字数据,一个字是两个字节!
s:
mov ax,0[si]
mov 16[si],ax
add si,2
loop s
mov ax,4c00h
int 21h
code ends
end start
结合起前面的表示内存单元的方法这里进行将不同的寻址方式进行如下总结:(以下的si与di是可以互换的)
[idata] 用常量来表示地址,可直接定位到一个内存单元
[bx] 用bx变量来表示地址,可间接定位到一个内存单元
[bx+idata] [si+idata] 用变量与常量表示
[bx+si] 用两个变量表示
[bx+si+idata] 用两个变量与一个常量表示
参考如下例子:
;1.asm
;将datasg段中每个单词改为大写字母
assume cs:code,ds:data,ss:stacksg
data segment
db 'ibm ' ;16
db 'dec '
db 'dos '
db 'vax ' ;看成二维数组,就是c的那个二维数组了啦
data ends
stacksg segment ;定义一个段,用来做栈段,容量为16个字节
dw 0, 0, 0, 0, 0, 0, 0, 0
stacksg ends
code segment
start:
mov ax,stacksg
mov ss,ax
mov sp,16 ;栈都是从高地址指向低地址的!
mov ax,data
mov ds,ax
mov bx,0 ;初始ds:bx
mov cx,4 ;cx为默认循环计数器,二重循环只有一个计数器,所以外层循环先保存cx值,再恢复,我们采用栈保存
;这里可以说相当于设计了两个for循环,第一层for循环为s0,用于控制上述二维数组的行;第二层for循环为s,用于控制列。cx的值相当于for循环中的判断循环条件
s0:
push cx ;将外层循环的cx值入栈
mov si,0
mov cx,3 ;cx设置为内层循环的次数
s:
mov al,[bx+si]
and al,11011111B ;每个字符转为大写字母
mov [bx+si],al
inc si
loop s
add bx,16 ;下一行q
pop cx ;恢复cx值
loop s0 ;外层循环的loop指令将cx中的计数值减1
mov ax,4c00h
int 21h
😜汇编书写格式简要介绍
在8086的汇编语法中
1、; 代表着注释符
2、assume 代表着声明一个段空间。通常使用 assume 段寄存器:该段名称。例如:assume cs:code,assume ds:data 该语句声明了一个代码段和数据段!!该段的名称通常指向的是该段的段首空间地址。
3、segment ends 代表着一个段的开始与结束位置,例如:
code segment
【具体代码】
code ends
data segement
【定义数据】
data ends
4、end 【具体标号】 代表程序到此结束,如果后有标号则通常为了指明程序的入口,这一个入口将被写入可执行文件的描述信息,当该可执行文件被载入内存后,cpu的cs:ip将被设定指向该入口处!
上述的语法可参照下述的代码例子进行理解:
assume cs:code,ds:data,ss:stack ;定义多个段
data segment ;数据段
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;0-15单元
data ends
stack segment ;栈段
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;0-31单元
stack ends
code segment ;代码段
start: mov ax, stack;将名称为“stack”的段的段地址送入ax
mov ss, ax
mov sp, 20h ;设置栈顶ss:sp指向stack:20。 20h = 32d
mov ax, data ;将名称为“data”的段的段地址送入ax
mov ds, ax ;ds指向data段
mov bx, 0 ;ds:bx指向data段中的第一个单元
mov cx, 8
s: push [bx]
add bx, 2
loop s ;以上将data段中的0~15单元中的8个字型数据依次入栈
mov bx, 0
mov cx, 8
s0: pop [bx]
add bx, 2
loop s0 ;以上依次出栈8个字型数据到data段的0~15单元中
mov ax, 4c00h
int 21h
code ends
end start
;“end start”说明了程序的入口,这个入口将被写入可执行文件的描述信息,
;可执行文件中的程序被加载入内存后,CPU的CS:IP被设置指向这个入口,从而开始执行程序中的第一条指令
可自行观察,ip的指向!
😜数据处理的基本问题
在8086中,只有四个寄存器可以用[…]来进行内存单元的寻址,即:bx、si、di、bp。其中这四个寄存器可以单独出现,若以组合形式出现只能是如下四种组合:bx和si、bx和di、bp和si、bp和di。
注:当使用[]中含bp时,如[BP],且没有显性的给出段地址的话,则段地址默认在ss堆栈段中。
汇编语言中的数据位置:
- 立即数(idata)
mov ax,1 这里的1就是立即数,执行前数据在cpu的指令缓冲器中
- 寄存器
mov ax,bx 这里指令要处理的数据就是在寄存器当中,数据在指令执行前在cpu内部的寄存器当中
- 内存[0]
数据在指令执行前在cpu内部的内存当中 。
指令处理的数据长度:
- 利用寄存器指明:
如mov al,[0] ; 因为al是8位的,因此会默认数据是字节类型。 - 可以使用X ptr进行指明,X可以位word或byte
例如: mov byte ptr ds:[0],1 ;这里指明了数据类型是byte,即字节型 - 部分指令会默认是字节数据或者字数据
例如:push [1000H] ,push指令只进行字操作
😜转移指令简要概括:
所谓转义指令就是jmp,主要是用于修改ip或者cs:ip的值,用于控制cpu执行内存中指定处的代码。
8086的转移有以下几类:
- 只改ip,则为段内转移⇒ jmp ax
- 同时改cs与ip,则为段间转移⇒ jmp 1000:0
对于ip修改的范围不同,又可以区分为短转移与近转移:
- 短转移:-128~127
- 近转移:-32768~32767
转移指令可以有以下几种:
- 无条件转移指令(如:jmp)
- 条件转移指令
- 循环指令(如:loop)
- 过程
- 中断
😜标志寄存器
主要有以下三个作用:
- 用于存储相关指令的某些执行结果;
- 用于为cpu执行某些指令提供依据;
- 用于控制cpu的相关工作方式
注:在8086当中传送指令(mov、pop、push等)都不会对标志寄存器有影响;而运算指令(add、sub、mul、div、inc、or、and等)通常会对标志寄存器有影响。
以下简要介绍标志寄存器的几个标志位:
- ZF 零标志位,记录运算结果是否为0。ZF=1 表示结果是0;
- PF 奇偶标志位, 记录结果的bit位中1的个数是否位偶数,PF=1 表示1的个数为偶数;
- SF 符号标志位,判断结果是否为负,SF=1 结果为负
- CF 进位标志位,在进行无符号数运算时,记录运算结果的最高有效位是否向更高位进行了借位或进位;
- OF 溢出标志位,记录有符号数的运算结果是否发生了溢出;
- DF 方向标志位,在串指令中控制si、di的递增与递减,df=0则每次递增,df=1则递减;用cld设置起为0,std设置为1,结合movsb与movsw使用
这里有利用cf与zf进行判断是否转移的指令:
😜内中断
⇒ 所谓中断就是指cpu在执行完当前正在执行的指令之后,检测到从cpu内部(内中断)或者外部(外中断)发出来的一种特殊信息后,可以立即转到对这个特殊信息进行处理。这个特殊信息就可以称之为中断。
⇒ 8086的内中断有以下几种类型:
- 除法错误,比如,执行div指令产生的除法溢出;
- 单步执行;
- 执行 into指令;
- 执行 int指令。
在8086cpu中,中断信息是包含中断信息码的,该信息码是一个字节型数据,可以表示256种中断信息的来源(中断源)
在8086中设置有中断处理程序(实际中断信息码所指向的代码程序),中断向量表(是中断处理程序的入口地址列表),cpu用中断信息码在中断信息表中找到对应的中断处理程序的入口地址,从而指向对应的中断处理程序。
⇒ 除法中断:产生除法溢出错误时候会引起对应的中断,默认为0号的中断,以下给出自己调试后的代码:
;中断向量表放在0000:0000处,除法中断是第零号
;中断处理程序do0放到0000:0200,再将其地址登记在中断向量表对应表项
;0号表项的地址0:0。0:0字单元存放偏移地址,0:2字单元存放段地址
;将do0的段地址0存放在0000:0002字单元中,将偏移地址200H存放在0000:0000字单元
assume cs:code
code segment
start:
mov ax,cs
mov ds,ax
mov si, offset d0 ; 设置ds:si指向d0处的代码,以便后面使用movsb将代码从ds段移至es段
mov ax,0
mov es,ax
mov di,200h ;此时es:di为0000:0200h 指向的是中断处理程序部分
mov cx, offset d0end - offset d0 ; 方便rep进行循环,以便将所有代码存入中断处理程序的部分
cld ; 设置DF=1,以便movsb进行递增
rep movsb; 将d0的代码送入0:200处
;设置0:0处的中断向量表,低地址放偏移地址,高地址放段地址
mov ax,0
mov es,ax
mov word ptr es:[0*4],200h
mov word ptr es:[0*4+2],0
mov ax,4c00h
int 21h
;d0显示字符串
d0:
jmp short d0start
db "overflow!"
d0start:
mov ax,cs
mov ds,ax
mov si,202h ;设置ds:si指向字符串
mov ax,0b800h
mov es,ax
mov di,12*160+36*2 ;设置es:di指向显存空间的中间位置
mov cx,9 ;进行显示的循环
s:
mov al,[si]
mov es:[di],ax ;将字符传输到屏幕中间
inc si ;指向下一个字符
add di,1 ;一个字母将会占用一个字的空间,因为一个字母占一个字节,其设置的颜色也将占一个字节
mov al,02h
mov es:[di],al
add di,1
loop s
mov ax,4c00h
int 21h
d0end:nop
code ends
end start
这里,再运行上述代码后,直接使用int 0 调用该中断,效果如下图:
⇒ 单步中断:cpu执行完一条指令后,检测标志寄存器的TF,若为1则执行单步中断。Debug提供了单步中断的中断处理程序,功能为显示所有寄存器中的内容后等待输入命令,有如下代码进行设置:
先安装我们需要的7ch:
;编程:安装中断7ch的中断例程
;功能:求一word型数据的平方。
;参数:(ax) = 要计算的数据。
;返回值:dx、ax中存放结果的高16位和低16位。
assume cs:code
code segment
start:
mov ax,cs
mov ds,ax
mov si,offset sqr ;设置ds:si指向源地址
mov ax,0
mov es,ax
mov di,200h ;设置es:di指向目的地址
mov cx,offset sqrend - offset sqr ;设置cx为传输长度
cld ;设置传输方向为正
rep movsb
mov ax,0
mov es,ax
mov word ptr es:[7ch*4], 200h
mov word ptr es:[7ch*4+2], 0
mov ax,4c00h
int 21h
sqr:
mul ax
iret ;CPU执行int 7ch指令进入中断例程之前,标志寄存器、当前的CS和IP被压入栈
;在执行完中断例程后,应该用iret 指令恢复int 7ch执行前的标志寄存器和CS、IP的
sqrend: nop
code ends
end start
然后,这是的7ch已经改变了,因此我们可以使用如下代码验证:
;求2 * 3^2
assume cs:code
code segment
start:
mov ax, 3;(ax)=3
int 7ch ; 调用中断7ch的中断例程,计算ax中的数据的
add ax,ax
mov ax,4c00h
int 21h
code ends
end start
⇒ int 指令格式:int n;其中n就为中断类型码,其主要功能是引发中断过程。int指令可以进行调用任何一个中断处理程序,参照除法中断的int 0。
⇒ BIOS提供的中断
在主板中存放着一套程序,称为bios(基本输入输出系统),主要做以下几个内容:
- 硬件系统的检测和初始化程序;
- 外部中断和内部中断的中断例程;
- 用于硬件设备进行I/O操作的中断例程;
- 其他和硬件相关的中断例程。
我们一般可以直接使用int进行调用BIOS和DOS系统所提供的中断例程。
BIOS和DOS中断例程的安装过程:
1、开机后,CPU一加电,初始化(CS)= 0FFFFH,(IP)= 0,自动从FFFF:0单元开始执行程序。FFFF:0处有一条转跳指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。
2、初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。
注意,对于BIOS所提供的中断例程,只需将入口地址登记在中断向量表中即可,因为它们是固化到ROM中的程序,一直在内存中存在。
3、硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。
4、DOS启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。
BIOS中断例程应用
一般来说,一个供程序员调用的中断例程中往往包括多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。
BIOS和DOS提供的中断例程,都用 ah 来传递内部子程序的编号。 例子:
assume cs:code
code segment
;int 10h中断例程的"设置光标位置"功能
mov ah, 2;设置光标调用第10h号中断例程的2号子程序,功能为设置光标位置(可以提供光标所在的行号、列号和页号作为参数)
;设置光标到第0页,第5行,第12列
mov bh, 0;第0页
mov dh, 5;dh中放行号
mov dl, 12;dl中放列号
int 10h
;int10h中断例程的"在光标位置显示字符"功能。
mov ah,9 ;调用第10h号中断例程的9号子程序,功能为在光标位置显示字符
;提供要显示的字符、颜色属性、页号、字符重复个数作为参数
mov al,'a' ;字符
mov b1,11001010B ;颜色属性
mov bh,0 ;第0页
mov cx,3 ;字符重复个数
int 10h
code ends
end
⇒ 这里再简要说一下之前的 int 21h : int 21h中断例程是DOS提供的中断例程,而我们使用的mov ax,4c00h 则是指:4ch号功能,即程序返回功能,可以提供返回值作为参数。
这里再给一个编程的例子:
assume cs:code
data segment
db 'Welcome to masm', '$' ;“$”本身并不显示,只起到边界的作用
data ends
code segment
start: mov ah, 2 ;10号中断设置光标位置功能
mov bh, 0 ;第0页
mov dh, 5;dh中放行号
mov dl, 12 ;dl中放列号
int 10h
mov ax, data
mov ds, ax
mov dx, 0 ;ds:dx指向字符串的首地址data:0 (参数)
mov ah, 9 ;调用第21h号中断例程的9号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数
int 21h
mov ax, 4c00h ;21号中断程序返回功能
int 21h
code ends
end start
## 😜端口
在pc机子中cpu通过总线相连的主要有以下几种芯片:
- 各种存储器
- 接口卡上的接口芯片(如显卡,网卡)
- 主板上的接口芯片(cpu通过其与外设进行访问)
- 其他芯片,用于存储相关系统信息(如固件)
在这些芯片中,都有一组可以由CPU读写的寄存器。这些寄存器,它们在物理上可能处于不同的芯片中。
cpu将以上的芯片都当作端口,对他们进行了统一的编址,从而建立一个统一的端口地址空间。
cpu可以直接读写的主要有以下3个地方:
- cpu内部寄存器;
- 内存单元;
- 端口
端口的读写
读指令in;写指令out。
;对0~255以内的端口进行读写时:
in al, 20h ;从20h端口读入一个字节
out 20h, al ;往20h端口写入一个字节
;对256~65535的端口进行读写时,端口号放在dx中:
mov dx, 3f8h ;将端口号3f8h送入dx
in al, dx ;从3f8h端口读入一个字节
out dx, al ;向3f8h端口写入一个字节
CMOS RAM芯片
C机中,有一个CMOS RAM芯片,一般简称为CMOS。此芯片的特征如
-
包含一个实时钟和一个有128个存储单元的RAM存储器
-
该芯片靠电池供电。关机后内部的实时钟正常工作,RAM中的信息不丢失
-
128个字节的RAM中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。BIOS也提供了相关的程序,使我们可以在开机的时候配置CMOS RAM中的系统信息。
-
该芯片内部有两个端口,端口地址为70h和71h。CPU通过这两个端口来读写CMOS RAM
-
70h为地址端口,存放要访问的CMOS RAM单元的地址
-
71h为数据端口,存放从选定的CMOS RAM单元中读取的数据,或要写入到其中的数据。
可见,CPU对CMOS RAM的读写分两步进行,比如,读CMOS RAM的2号单元:
①将2送入端口70h;
②从端口71h读出2号单元的内容。
😜外中断
cpu通过端口来达到与外部设备进行联系。即当cpu外部有需要处理的事情发生时(如外设输入到达),相关芯片就会像cpu发出对应的中断信息(外中断),其主要有以下两种类型:
-
可屏蔽中断
这类中断 屏蔽与否主要看标志寄存器IF的设置。当IF=1时进行响应,反之不响应。我们可以利用sti,设置IF=1,cli,这是IF=0. -
不可屏蔽中断
这类中断是cpu必须响应的。一般固定的中断类型码为2。所以中断过程中,不需要取中断类型码。则不可屏蔽中断的中断过程为:①标志寄存器入栈,IF=0,TF=0;②CS、IP入栈;③(IP)=(8),(CS)=(0AH)。
几乎所有由外设引发的外中断,都是可屏蔽中断。当外设有需要处理的事件(比如说键盘输入)发生时,相关芯片向CPU发出可屏蔽中断信息。不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。
BIOS提供了int 9中断例程,用来进行基本的键盘输入处理,主要的工作如下:
(1)读出60h端口中的扫描码;
(2)如果是字符键的扫描码,将该扫描码和它所对应的字符码(即ASCII码)送入内存中的BIOS键盘缓冲区; 如果是控制键(比如Ctrl)和切换键(比如CapsLock)的扫描码,则将其转变为状态字节写入内存中存储状态字节的单元;
(3)对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。
BIOS键盘缓冲区可以存储15个键盘输入,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。
CPU对外设输入的通常处理方法
(1)外设的输入送入端口;
(2)向CPU发出外中断(可屏蔽中断)信息;
(3)CPU检测到可屏蔽中断信息,如果IF=1,CPU在执行完当前指令后响应中断,执行相应的中断例程;
(4)可在中断例程中实现对外设输入的处理
例子,int 9 中断:
;编程:在屏幕中间依次显示“a”~“z”,并可以让人看清。在显示的过程中,按下'Esc'键后,改变显示的颜色。
;完整功能代码:
assume cs:code
stack segment
db 128 dup(0)
stack ends
data segment
dw 0,0
data ends
code segment
start:
;设置栈
mov ax,stack
mov ss,ax
mov sp,128
;设置数据段
mov ax,data
mov ds,ax
;设置中断向量表的地址
mov ax,0
mov es,ax
;将原来的int 9中断例程的入口地址保存在ds:0、ds:2单元中
push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[0+2]
;在中断向量表中设置新的int 9中断例程的入口地址,上述过程是为了做备份使用的,下述可能会修改int 9
mov word ptr es:[9*4], offset int9
mov word ptr es:[9*4+2],cs
;显示字符串,设置地址指向显存的地址空间
mov ax,0b800h
mov es,ax
mov ah,'a'
s:
mov es:[160*12+40*2],ah
call delay
inc ah
cmp ah,'z'
jna s
mov ax,0
mov es,ax
;将中断向量表中int 9中断例程的入口恢复为原来的地址
push ds:[0]
pop es:[9*4]
push ds:[0+2]
pop es:[9*4+2]
mov ax,4c00h
int 21h
;将循环延时的程序段写为一个子程序
delay:
push ax
push dx
;用两个16位寄存器来存放32位的循环次数
mov dx,2000h
mov ax,0
s1:
sub ax,1
sbb dx,0
cmp ax,0
jne s1
cmp dx,0
jne s1
pop dx
pop ax
ret
;------以下为新的int 9中断例程--------------------
int9:
push ax
push bx
push es
;从端口60h读出键盘的输入
in al,60h
;标志寄存器入栈,以便在输入esc后,cpu进入中断时能保存现场,结束后便可以返回
pushf
;为了下部分对标志寄存器进行设置的IF
pushf
pop bx
;TF=0,IF=0
and bh,11111100B
push bx
popf
;对int指令进行模拟,调用原来的int 9中断例程
call dword ptr ds:[0]
cmp al,1
jne int9ret
mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1]
int9ret:
pop es
pop bx
pop ax
iret
code ends
end start
按一次esc 就会变一次颜色(字母更换的时间由前面已经设定了)
😜汇编命令简要概括
div:
1.除数:有8或16位,一般在一个寄存器或者内存单元中
2. 被除数:默认在ax(为16位)或dx和ax(为32位,dx存高16位数,ax存低16位数)中。
3. 结果:如果除数为8位⇒ AL存商,AH存余数
如果除数为16为⇒AX存商,DX存余数。
使用方法: div 除数 即可
例如:
;利用除法指令计算100001/100。
;100001D = 186A1H
mov dx, 1
mov ax, 86A1H ;(dx)*10000H+(ax)=100001
mov bx, 100
div bx ;这里商的结果已经超8为了因此会默认存在ax里,即ax当作商
;利用除法指令计算1001/100
mov ax, 1001
mov bl, 100
div b1
offset:
功能是取得标号的偏移地址,用法可如下:
mov si,offset s ;即取得标号s的偏移地址并存在si中
jmp:
jmp无条件转移!jmp使用时需要给出如下信息:
- 转移的目的地址
- 转移的距离(段间、段内短转移、段内近转移)
- 用法:
jmp 段地址:偏移地址 ==> 可直接跳至给定的内存位置中(此处修改了CS与IP) ==> jmp 3CE3:3
jmp 寄存器 ==> 仅修改IP ==> jmp ax
- 常见的用法:
1、jmp short 标号(段内短转移)
功能实际为:jmp ip 而(IP)=(IP)+8位位移。实际表现出来的效果就是就是:转到标号处执行指令。
(1)8位位移 = “标号”处的地址 - jmp指令后的第一个字节的地址;这里之所以要减去jmp指令后第一个字节地址,是因为:实际执行时,当执行到jmp这条命令时,ip会自动指向下一条指令(8086中ip一般都是指向下一条指令的),即jmp指令后面的位置,因此此时的IP=jmp指令后的第一个字节的地址。而上述的公式为:(IP)=(IP)+8位位移,在将8位位移的等式带入就可以实现跳转到标号处了。
(2)short指明此处的位移为8位位移;
(3)8位位移的范围为-128~127,用补码表示
(4)8位位移由编译程序在编译时算出。
2、jmp near ptr 标号(段内短转移)
(IP)=(IP)+16位位移
3、jmp far ptr 标号(段间转移)
far ptr 指明了用标号的段地址和偏移地址修改CS和ip
4、jmp word ptr 内存单元地址(段内转移)
4、jmp dword ptr 内存单元地址(段间转移)
高地址处的字是段地址,低地址处的字是偏移地址。
使用时,jmp后的内存地址单元,使用的是最低地址处。
mov ax, 0123H
mov ds:[0], ax;偏移地址
mov word ptr ds:[2], 0;段地址
jmp dword ptr ds:[0]
;执行后,
;(CS)=0
;(IP)=0123H
;CS:IP 指向 0000:0123。
😢8086中不支持将数据直接送入段寄存器!
jcxv:
jcxv是有条件转移,所有的有条件转移都是短转移!!
指令格式:jxcv 标号 ;(如果(cx)=0则转移到标号处执行)
注意区分这个和loop的区别,loop是相当于for循环,这个相当于if循环。
loop:
mov cx,11
s : add ax,ax
loop s
==> loop 标号,即cpu执行时会经历如下两步:
1、cx=cx-1
2、判断cx是否等于0,不为0则跳至标号处,如上述代码中会跳至s处,即继续执行s冒号后面的语句;为0 则向下顺序执行。
ret和retf:
ret指令就是利用栈中的数据,修改IP地址的内容,从而实现近转移;
retf指令就是利用栈中数据,对cs、ip的内容,从而实现远转移;其中低地址的字空间是ip的内容,高地址的字空间是cs的内容。
cpu执行retf指令时候,相当于进行:pop IP , pop CS:
⇒ (IP) = ( (ss) * 16 + (sp) )
⇒ (sp) = (sp) + 2
⇒ (sp) = (sp) + 2
⇒ (sp) = (sp) + 2
call:
常与ret结合使用。cou执行call指令通常有两步:
- 将当前的IP或则CS和IP压入栈中。
- 进行转移指令(jmp)
call 除了不能短转移外,其余用法和jmp一样。下面举一个例子:
call dword ptr 【内存单元】 此时系统会做如下三个步骤:push cs push ip jmp dword ptr 内存单元。
以下给出与ret结合的代码:
assume cs:code
code segment
start: mov ax,1
mov cx,3
call s ;当cpu执行到此处时,cs:ip指向的是下一条指令也就是mov bx,ax这一条,然后再执行call,即将ip压栈,然后jmp,这里是近转移
mov bx,ax ;(4)IP重新指向这里 bx = 8
mov ax,4c00h
int 21h
s: add ax,ax
loop s;(2)循环3次ax = 8
ret;(3)return : pop IP
code ends
end start
mov:
mov 寄存器,数据 ==> 这里的寄存器一般是通用寄存器,不允许是段寄存器 ==> bx,1000H ==> 将1000H 这个数值存入bx中
mov 寄存器,寄存器 ==> mov ds,bx ==> 将bx的值放入ds中
mov 数据内存单元,寄存器 ==> 这里的这两个位置可调换 ==> 含义是将右边的值送入左边的位置当中
movsb、movsw:
格式:movsb
功能:将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器df位的值,将si和di递增或递减
格式:movsw
功能:将ds:si指向的内存字单元中的字送入es:di中,然后根据标志寄存器df位的值,将si和di递增2或递减2。
格式:rep movsb
movsb和movsw进行的是串传送操作中的一个步骤,一般来说,movsb和movsw都和rep配合使用,
功能:rep的作用是根据cx的值,重复执行后面的串传送指令
add:
add ax,[2] ==> 将偏移地址2中的数据与ax中数据相加,并存到ax中
sub:
sub ax,[2] ==> 将偏移地址2中的数据与ax中数据相减,并存到ax中
adc和sbb:
adc 是带进位加法指令,利用CF的值;指令:adc 操作对象1,操作对象2 ;功能:操作对象1=操作对象1+操作对象2+CF。主要用于高位相加(因为低位相加产生的进位,就可以用adc在高位上进行弥补。毕竟汇编中存数据是分高低位的)
;计算1EF000H+201000H,结果放在ax(高16位)和bx(低16位)中。
;将计算分两步进行,先将低16位相加,然后将高16位和进位值相加。
;计算1EF000H+201000H,结果放在ax(高16位)和bx(低16位)中。
;将计算分两步进行,先将低16位相加,然后将高16位和进位值相加。
mov ax, 001EH
mov bx, 0F000H
add bx, 1000H
adc ax, 0020H
sbb是带借位减法指令,它利用了CF位上记录的借位值;功能:操作对象1 = 操作对象1 - 操作对象2 - CF
cmp:
cmp只是比较指令,相当于减法指令,不保存结果:
cmp ah, bh
(1)如果sf=1,而of=0 。 of=0说明没有溢出,逻辑上真正结果的正负=实际结果的正负; sf=1,实际结果为负,所以逻辑上真正的结果为负,所以(ah)<(bh)
(2)如果sf=1,而of=1: of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负; sf=1,实际结果为负。
实际结果为负,而又有溢出,这说明是由于溢出导致了实际结果为负,,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正。 这样,sf=1,of=1,说明了(ah)>(bh)。
(3)如果sf=0,而of=1。of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负;sf=0,实际结果非负。而of=1说明有溢出,则结果非0,所以,实际结果为正。
实际结果为正,而又有溢出,这说明是由于溢出导致了实际结果非负,如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负。这样,sf=0,of=1,说明了(ah)<(bh)。
(4)如果sf=0,而of=0
of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负;sf=0,实际结果非负,所以逻辑上真正的结果非负,所以(ah)≥(bh)。
mul:
mul 乘法指令。用mul进行乘法时,相乘的两个数要么都是8位,要么都是16位的。
- 8位⇒ 一位存于AL中,另一位存于8位寄存器或内存字节单元中。
- 16位⇒存于AX中,另一位存于16位寄存器或内存字单元中。
- 结果:若是8位乘法则存于AX中,若是16位乘法则默认DX为高位,AX为地位进行存储。
- 用法: mul 【另一位的存放地址】
mov al,100
mov bl,10
mul bl
;计算100*10000
;100小于255,可10000大于255,所以必须做16位乘法,程序如下:
mov ax,100
mov bx,10000
mul bx
;结果: (ax)=4240H,(dx)=000FH (F4240H=1000000)
pop、push:
pop ax
push ax
inc:
inc 寄存器 ==> 该寄存器的值+1
dw(d/b):
dw,是定义2字节即一个字的内存空间。如 dw 0123h 指分配一个word并且填入一个16进制的数0123h进。
db,是定义一个字节的内存空间。
dd,定义一个双字的内存空间。
注:
一般使用了
assume cs:code
code segment
就代表着代码段的开始 。此时,会默认给予一段空闲区分配给本代码段。而此时在这之下使用了dw后,一般默认会从偏移地址零处开始存储数据。即dw 0123h 后 0123h就存入CS:[0]处。
请看如下代码:
;计算 8 个数据的和存到 ax 寄存器
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;define word 定义8个字形数据
start: mov bx, 0 ;标号start,指真正代码指令执行的位置!!
mov ax, 0
mov cx, 8
s: add ax, cs:[bx] ;此处就是从零处开始执行
add bx, 2
loop s
mov ax, 4c00h
int 21h
code ends
end start ;end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方
;用end指令指明了程序的入口在标号start处,也就是说,“mov bx,0”是程序的第一条指令。
;读者可以自行将start位置更改,然后查看不同位置处的ip的值也会有所不同的
dup:
由编译器识别处理的符号。
它和db、dw、dd等数据定义伪指令配合使用,用来进行数据的重复。
db 3 dup (0) ;定义了3个字节,它们的值都是0,相当于db 0,0,0。
db 3 dup (0, 1, 2) ;定义了9个字节,它们是0、1、2、0、1、2、0、1、2,相当于db 0,1,2,0,1,2,0,1,2。
db 3 dup ('abc', 'ABC') ;定义了18个字节,它们是abcABCabcABCabcABCC,相当于db 'abc', 'ABC' ,'abc' , 'ABC, 'abc', 'ABC'。
and、or:
and 逻辑与,按位进行与运算(即都为1才为1),例如:
mov al,01100011B
and al,00111011B
这里进行逐位与运算 ⇒ 最终结果:al=00100011B
or 逻辑或,即按位进行或运算(一个为1则为1)
mov al,01100011B
or al,00111011B
这里进行逐位或运算 ⇒ 最终结果:al=01111011B
shl、shr:
shl是逻辑左移指令,它的功能为:
- 将一个寄存器或内存单元中的数据向左移位;
- 将最后移出的一位写入CF中;
- 最低位用0补充。
shr是逻辑右移指令,同理。
注:如果移动位数大于1时,必须将移动位数放在cl中
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。