8086是Inter公司的第一款16微处理器,诞生于1978年,8086在市场上获得了巨大成功,所以后期芯片都兼容它。
8086有8个16位通用寄存器 AX,BX,CX,DX,SI,DI,BP,SP其中AX,BX,CX,DX 可以拆分成8个8位寄存器AH,AL,BH,BL,CH,CL,DH,DL 。4个段寄存器CS,DS,ES,SS,其中CS是代码段默认寄存器,DS是数据段默认寄存器,ES附加段寄存器,SS是栈段寄存器。一个指令指针寄存器IP与CS一起使用后来还增加了FSGS段寄存器。
8086有20位地址总线可以访问20位地址也就是2^20字节(1M字节),访问方式是:
访问地址 = 段寄存器 x 4 + 偏移地址。类似于CS:IP格式。
当处理器研发不久,人们就利用了助记符来描述2进制指令,也就产生了汇编语言,也就是mov,call ,mul ,dec,sub ,add 等指令,例如(inter汇编样式):mov ax,bx ;
还有AT&T格式也是gun编译器使用的格式,具体可查阅相关资料.
在windows端我们常用nasm编译inter汇编,具体安装以及模拟器配置方法可以参考《x86汇编语言:从实模式到保护模式》这里我使用的notepad++和bochsdbg模拟器调试汇编语言。
在编写汇编语言前需要了解,CPU怎么加载自己编写的程序,以及上电复位过程,CPU在上电时RESET引脚电平由低变高时 CS = 0xFFFF ,IP = 0x0000 所以上电默认访问地址为 0xFFFF0而正位置被BIOS的ROM占用,ROM占用映射空间64KB,物理地址范围为0xF0000-0xFFFFF也就是1M最后64KB字节,此时CS = 0xFFFF,如果执行程序时 IP > 0x000F 则 CS:IP 地址会回滚到0x00000地址处。在地址0xFFFF0处这里放着一条跳转指令使处理器从ROM中较低地址处取指令执行,执行完吧BIOS指令之后,会把启动盘的第一个扇区的内容加载到 0x07C00 处,然后跳转到这里运行(0x7c00是最开始时规定的,后来为了兼容所以BIOS一直这样了),一个有效的主引导扇区其最后两个字节应该是0x55和0xAA
jmp 0x0000:0x7c00
这里需要提及一下硬盘,硬盘从物理0面0道1扇区开始编码,采用磁头,磁道,扇区这种 CHS 模式访问不是很方便,就引入了逻辑块地址的概念 LBA ,以扇区为单位按照磁道磁面数增加扇区数。逻辑0扇区对应磁盘1扇区,现在市场上所有的磁盘都支持LBA模式。
8086可以访问1M字节的内存,其中 0x00000~0x9FFFF 属于常规内存,0xF0000~0xFFFFF是bios芯片提供的,中间的320KB字节即:0xA0000~0xEFFFF 由特定外设设备提供,其中包括显卡显示,由于历史原因个人计算机上使用的显卡,再加电自检后初始化为80x25的文本模式。可以显示25行,每行80个字符共2000个字符。一直以来0xB8000~0xBFFFF这段32KB物理地址空间留给显卡使用。直接写字符显示每个字符占用两个字节,一个字节表示颜色亮度等属性,另一个字节使用ASCII码直接显示使用例如显示ABCDEF:
org 0x7c00
mov ax,0XB800
mov ds,ax
mov bx,0x00
mov byte [0x00],'A'
mov byte [0x02],'B'
mov byte [0x04],'C'
mov byte [0x06],'D'
mov byte [0x08],'E'
mov byte [0x0A],'F'
jmp $
END_CHECK:
times 510-($-$$) db 0
db 0x55,0xaa
文章图片
程序常用指令:
movjmpcalldivxormovsbmovswincdeccldstddivnegcbwcwdsubidivjcxzcmp
声明字符串伪指令:
DB字节(1字节)
DW字(2字节)
DD双字 (4字节)
DQ四字(8个字节)
data:
db 0x1;
占用内存1个字节
dw 0x1234;
占用内存2个字节
dd 0x12345678;
占用内存4个字节
dq 0x0123456789ABCDEF;
占用内存8个字节
DIV指令常用方式:
;
第一种方式16位除以8位
mov ax,12345
mov bl,45
div bl;
div指令默认使用 ax/dl 商在al余数在ah;
第二种方式32位除以16位
mov ax,65535
mov dx,1
mov bx,6
div bx;
商在ax余数在dx
;还有其他方式可以在王爽的《汇编语言》里面有详细解释
XOR ax,ax ;
ax^ax 也就是ax=0这样操作寄存器会很快
add ax,bx ;
ax=ax+bx注意两个操作数不能都为内存单元 add byte [0x01],[0x02]这样不行
;
jmp指令有相对跳转和直接跳转
jmp label 相对跳转
jmp cs:ip 绝对跳转一般cs寄存器使用jmp和call更改;
伪指令 times 执行次数 指令
data:
times 255 db 0xff
times 5 mov ax,bx
;
伪指令 $:表示当前位置 $$:表示起始位置;
内存操作指令
mov [bp],0x00
mov [si],0x00
mov [di],0x00
mov [es:si],0x00;
其他的段寄存器也可以这样使用包括cs,偏移寄存器只能使用 BX,SI,DI,BP;
movsb movsw 是批量数据拷贝指令例如
jmp code
data1:
times 16 db 0
data2:
times 16 db 0x55
code:
mov ax,0x7c0;
初始化栈 注意 ds<<2+sp 使用的栈地址
mov ss,ax
mov ax,END_CHECK
mov sp,ax
mov ax,data1
mov bx,16
call clear
mov ax,0x7c0
mov ds,ax;
设置ds
mov es,ax ;
设置es
mov ax,data1
mov si,ax ;
设置 si
mov ax,data2
mov di,ax ;
设置 di
mov cx,8
cld;
设置从地址到高地址(si++ di++) std指令则是高地址到低地址(si-- di--)
rep movsw ;
字节传输 jmp $;
清除某块地质数据
clear: ;
ax填充地址 bx字节数
push cx
push bp
mov bp,ax
mov cx,bx
mov ax,0
l:
mov bp,ax
mov byte [ds:bp],0x00
inc ax
loop l
pop bp
pop cx
retp:
times 100 db 0
END_CHECK:
times 510-($-$$) db 0
db 0x55,0xaa
;
loop指令是根据cx的值循环次数
注意:基址变址只支持[bx+si][bx+di][bp+si][bp+di]cs:ipss:sp
;
有符号除法指令
mov ax,0x1234
mov bl,0x19
idiv bl;
有符号数扩展指令
mov al,-19
cbw;
扩展到整个ax寄存器 16位数
cwd;
扩展到dx 寄存器32位数
;
符号取反
mov ax,-56
neg ax ;
ax = 56;
逻辑运算 or and
mov ax,0xaa;
ax = 0xaa
or ax,0x55;
ax = 0xff
and ax,0x55;
ax=0xaa;
压栈出栈指令push pop注意需要提前设置好sp和ds寄存器
push ax
pop ax
常用寻址方式:
;
寄存器寻址
mov ax,bx
;
立即寻址
mov bx,0x1234
;
内存寻址
mov ax,[0x01]
add word [0x1234],0x5000
xor byte [es:1234],0x5a
;
基址寻址
inc word [bx]
mov ax,[bp]
;
变址寻址
inc word [si]
mov ax,[di]
;
基址变址寻址
mov ax,[bx+si]
mov ax,[bp+di]
mov ax,[bp+di+0x100]
关于nasm的段地址设置
文章图片
这里 fill 里面的 $ 相当于从 fill 这里计算的位置 ,这里的 fill 是全局的位置说明 $ 的位置也是全局的位置,start,t1,t2 段占用64字节(16进制0x40),
vstart指明段内汇编地址起始位置 该段内的汇编的地址都是从vstart开始算
section.段名.start可以获取该段的汇编地址
文章图片
硬盘数据的访问:硬盘数据通过io端口进行访问(数据,状态,命令端口访问)
inter的i/o端口没有映射到内存地址上面需要使用专用指令 in out 访问,主硬盘端口号是 0x1f0~0x1f7 ,副端口号是 0x170~0x177 ,在inter中只允许65535个端,端口号从0~65535
端口访问指令
;
端口读取 in的目的操作数必须是al或者ax,访问8位端口使用al,16位端口ax
;in的源端口寄存器应当是dx
in al,dx;
8位端口
in ax,dx;
16位端口
;
端口写目的操作数可以是常数或者dx,源操作数必须是al或者ax
out 0x37,al;
0x37 8位端口
out 0xf5,ax;
0xf5 16位端口
out dx,al;
dx指的8位端口
out dx,ax;
dx指的16位端口
硬盘LBA28编址:硬盘LBA编址使用28位来表示逻辑扇区从0开始编码,逻辑扇区从0x0000000~0xFFFFFFF可以表示2^28=268435456个扇区,每个扇区512个字节,所以一共可以管理128GB硬盘
硬盘LBA48编址:由于硬盘发展太快LBA28已经落后,所以新增加 LBA48 使用48位编码可以访问131072TB容量硬盘
硬盘读取例子:
;
LBA28访问
mov dx,0x1f2 ;
读取一个扇区,0x1f2端口写扇区数量,0表示256扇区 ,1表示一个扇区
mov al,0x01
out dx,al
inc dx;
0x1f3端口写扇区LBA 0-7位逻辑扇区
mov al,0x04
out dx,al
inc dx;
0x1f4端口写扇区LBA 8-15位逻辑扇区
mov al,0x00
out dx,al
inc dx;
0x1f5端口写扇区LBA 16-23位逻辑扇区
mov al,0x00
out dx,al
inc dx;
0x1f6端口写扇区LBA 24-28位逻辑扇区
mov al,0xe0
out dx,al mov dx,0x1f7 ;
0x1f7端口写读取命令
mov al,0x20
out dx,al
mov dx,0x01f7
wait1:;
0x1f7端口写读取状态
in al,dx
and al,0x88
cmp al,0x08
jnz wait1 ;
busy则跳转
mov cx,128
mov dx,0x1f0 ;
0x1f0读取数据16位端口
mov ax,0x7c0
mov ds,ax
mov bx,read
read1:
in ax,dx
mov [bx],ax
add bx,2
loop read1
jmp $
read:
times (128) dw 0x0000
fill512:
times (510 - ($-$$)) db 0xFF
db 0x55,0xAA
fill:
times (512) dw 0x55AA
fillread:
times (512) dw 0xaa55
子程序调用说明:call 调用子程序 后面一半跟要跳转的函数地址 label ,这一般是相对调用,
call CS:IP这种形式是绝对调用会压栈CS和IP,相对调用只压栈IP;
第一种相对调用:
call label ;这种是相对当前地址的调用;
call 0x5000等价于call label;
编译时使用label 绝对地址减去调用指令的地址得到偏移地址;
第二种间接绝对调用:
call [bx]
call bx
编译时bx和[bx]的内存的地址就是函数的实际偏移地址,不会再跟调用地址的指令相减得到偏移地址;
第三种直接绝对远调用:
call 0x5000:76
直接改变cs和ip地址跳转到此处运行;
第四种是间接直接远调用:
proc1 dw 0x0102,0x2000
call far [proc1]
处理器访问ds的相对proc1位置的内存数据提供给cs ip;
程序的返回:
ret 只压栈ip的用他返回
retf 压栈cs ip的用他返回
iret 中断函数用他返回
注意:ret并不总是和call一起使用
无条件跳转程序jmp:
1.相对跳转:
jmp short infinite;短相对跳转当前指令的-128~+127字节
jmp short 0x2000;跟上面使用标号一样都是相对跳转当前指令的-128~+127字节
jmp near infinite;相对跳转当前指令的-32768~32767字节
jmp near 0x3000;跟上面使用标号一样都是相对跳转当前指令的-32768~32767字节
2.绝对跳转:
- jmp near bx;直接用bx取代IP
- jmp near cx
- jump_dest dw 0xc000
- jmp [jump_dest];跟使用寄存器一样
- jmp 0x0000:0x7c00;绝对远跳转
- jump_far dw 0x33c0,0xf000
- jmp far [jump_far];间接绝对远跳转
inter允许256个中断,中断号0-255,8259芯片负责提供其中的15个中断
文章图片
如图主片8259 IRQ0使用系统定时器,从片8259芯片IRQ0使用cmos实时时钟,主片I/O端口号是0x20~0x21,从片8259端口是0xA0~0xA1,处理器的标志寄存器IF标志位控制中断,为1时可以中断,分别使用cli sti来清除和设置中断标志。实模式是中断入口程序被放在0x00000~0x3FFFF这里共1KB内容,这就是中断向量表,每个中断占用2个字(4个字节)分别是中断处理程序的偏移地址和段地址,中断0处理函数入口地址放在0x00000,中断1处理函数入口地址放在0x00004处。8259中断号可以设置,但不能单独进行,只能以芯片为单位进行。比如,可以指定主片的中断号从0x08 开始,那么它每个引脚 IR0~IR7 所对应的中断号分别是 0x08~0x0e,中断信号来自哪个引脚,8259 芯片是最清楚的,所以它会把对应的中断号告诉处理器,处理器拿着这个中断号,要顺序做以下几件事:
- 保护中断现场压栈 FLAGS寄存器,清除IF TF标志位(TF是陷阱标志位)。压栈CS IP
- 根据中断号获取中断函数偏移地址与段地址传送至IP 和 CS
- 返回中断处继续执行程序,中断处理程序返回处必须是iret这会出栈IP CS FLAGS
中断随时可能发生,中断向量表的建立和初始化工作是由 BIOS 在计算机启动时负责完成的。BIOS 为每个中断号填写入口地址,因为它不知道多数中断处理程序的位置,所以,一律将它们指向一个相同的入口地址,在那里,只有一条指令:iret。也就是说,当这些中断发生时,只做一件事,那就是立即返回。当计算机启动后,操作系统和用户程序再根据自己的需要,来修改某些中断的入口地址,使它指向自己的代码。
实时时钟:
RTC 芯片由一个振荡频率为 32.768kHz 的石英晶体振荡器(晶振)驱动,经分频后,用于对
CMOS RAM 进行每秒一次的时间刷新。
常规的日期和时间信息占据了 CMOS RAM 开始部分的 10 字节,有年、月、日和时、分、秒,报警的时、分、秒用于产生到时间报警中断,如果它们的内容为 0xC0~0xFF,则表示不使用报警功能。
文章图片
CMOS RAM 的访问,需要通过两个端口来进行。0x70 或者 0x74 是索引端口,用来指定 CMOSRAM 内的单元;0x71 或者 0x75 是数据端口,用来读写相应单元里的内容。举个例子,以下代码用于读取今天是星期几:
mov al,0x06
out 0x70,al
in al,0x71
端口 0x70 的最高位(bit 7)是控制 NMI 中断的开关。当它为 0 时,允许 NMI 中断到达处理器,为 1 时,则阻断所有的 NMI 信号,其他 7 个比特,即 0~6 位,则实际上用于指定 CMOS RAM 单元的索引号,这种规定直到现在也没有改变。
文章图片
因为rtc有不少关于时钟的信息这里我们只用定时器中断,在开启中断之前需要设置 ss sp寄存器用来存储中断的信息的栈,RTC 芯片的中断信号,通向中断控制器 8259 从片的第 1 个中断引脚 IR0。在计算机启动期间,BIOS 会初始化中断控制器,将主片的中断号设为从 0x08 开始,将从片的中断号设为从 0x70 开始。所以,计算机启动后,RTC 芯片的中断号默认是 0x70。
RTC中断例子:
;
interrupt test
ORG 0X7C00
disp_addr EQU 0xB800
cli
mov ax,0x00
mov ss,ax
mov sp,0x7c00
sti
cli;
防止改动期间发生新的0x70号中断
mov al,0x70
mov bl,4
mul bl;
计算0x70号中断在IVT中的偏移
mov bx,ax
mov ax,0x0000
mov es,ax
mov word [es:bx],new_int_0x70;
偏移地址。
mov word [es:bx+2],cs;
段地址 mov al,0x0b;
RTC寄存器B
or al,0x80;
阻断NMI
out 0x70,al
mov al,0x12;
设置寄存器B,禁止周期性中断,开放更
out 0x71,al;
新结束后中断,BCD码,24小时制 mov al,0x0c
out 0x70,al
in al,0x71;
读RTC寄存器C,复位未决的中断状态 in al,0xa1;
读8259从片的IMR寄存器
and al,0xfe;
清除bit 0(此位连接RTC)
out 0xa1,al;
写回此寄存器 sti;
重新开放中断
mov bx,0
jmp $
new_int_0x70:
push ax
push cx
push es
push bp mov al,0x0c;
寄存器C的索引。且开放NMI
out 0x70,al
in al,0x71;
读一下RTC的寄存器C,否则只发生一次中断 mov ax,disp_addr
mov es,ax
mov cx,4
mov bp,0disp:
mov al,[bx+strsetsti]
mov byte [es:bp],al
inc bp
mov byte [es:bp],0x07
inc bp
loop disp
inc bx
and bx,0x0f
pop bp
pop es
pop cx
pop ax
mov al,0x20;
中断结束命令EOI
out 0xa0,al;
向从片发送
out 0x20,al;
向主片发送 iretstrsetsti:
db 'ABCDEFGHAJKLMLNPQ123456789'
fill512:
times (510 - ($-$$)) db 0xFF
db 0x55,0xAA
该程序反复显示字符串如图中的4个EEEE
文章图片
一旦响应了中断,8259 中断控制器无法知道该中断什么时候才能处理结束。如果不清除相应的位,下次从同一个引脚出现的中断将得不到处理。在这种情况下,需要程序在中断处理过程的结尾,显式地对 8259 芯片编程来清除该标志,方法是向 8259 芯片发送中断结束命令(End Of Interrupt,EOI)。中断结束命令的代码是 0x20。如果外部中断是 8259 主片处理的,那么,EOI 命令仅发送给主片即可,端口号是 0x20;如果外部中断是由从片处理的,那么,EOI 命令既要发往从片(端口号 0xa0),也要发往主片。最后记得IRET返回
中断总结:
- 设置堆栈指针
- 打开8259总中断
- 打开时钟芯片的中断
- 中断处理时清除时钟中断芯片中断标志
- 清除8259中断标志EOI
- 中断返回
cpu有低功耗指令 hlt 可以在循环中停止cpu,会在中断中被唤醒,继续执行下一条指令。
.idle:
hlt;
使CPU进入低功耗状态,直到用中断唤醒
jmp .idle
软中中断指令测试:
;
interrupt test
ORG 0X7C00
disp_addr EQU 0xB800
cli
mov ax,0x00
mov ss,ax
mov sp,0x7c00
sti
mov ax,0x00
mov es,ax
mov al,0x80
mov bl,0x04
mul bl
mov bx,ax
mov word [es:bx],new_int_0x80
mov word [es:bx+2],0x0000
mov bx,0
int80tsts: inc bx
and bx,0x0f
int 0x80
jmp int80tsts
new_int_0x80:
push ax
push cx
push es
push bp mov ax,disp_addr
mov es,ax
mov cx,4
mov bp,0disp:
mov al,[bx+strsetsti]
mov byte [es:bp],al
inc bp
mov byte [es:bp],0x07
inc bp
loop disp
pop bp
pop es
pop cx
pop ax
iretstrsetsti:
db 'ABCDEFGHAJKLMLNPQ123456789'
fill512:
times (510 - ($-$$)) db 0xFF
db 0x55,0xAA
执行int 0x80指令之后立即跳转到自己设置的中断函数中执行。
常用bios中断:
最有名的软中断是 BIOS 中断,之所以称为 BIOS 中断,是因为这些中断功能是在计算机加电
之后,BIOS 程序执行期间建立起来的。换句话说,这些中断功能在加载和执行主引导扇区之前,就已经可以使用了。
BIOS 中断,又称 BIOS 功能调用,主要是为了方便地使用最基本的硬件访问功能。不同的硬件使用不同的中断号,比如,使用键盘服务时,中断号是 0x16,即
int 0x16
通常,为了区分针对同一硬件的不同功能,使用寄存器 AH 来指定具体的功能编号。举例来说,以下指令用于从键盘读取一个按键:
mov ah,0x00 ;
从键盘读字符
int 0x16 ;
键盘服务。返回时,字符代码在寄存器 AL 中
int号 | ah | 功能说明 | 入口参数 | 返回参数 |
10 | 0 | 设置显示方式 | AL=00 40×25 黑白方式 AL=01 40×25 彩色方式 AL=02 80×25 黑白方式 AL=03 80×25 彩色方式 AL=04 320×200 彩色图形方式 AL=05 320×200 黑白图形方式 AL=06 320×200 黑白图形方式 AL=07 80×25 单色文本方式 AL=08 160×200 16 色图形(PCjr) AL=09 320×200 16 色图形(PCjr) AL=0A 640×200 16 色图形(PCjr) AL=0B 保留(EGA) AL=0C 保留(EGA) AL=0D 320×200 彩色图形(EGA) AL=0E 640×200 彩色图形(EGA) AL=0F 640×350 黑白图形(EGA) AL=10 640×350 彩色图形(EGA) AL=11 640×480 单色图形(EGA) AL=12 640×480 16 色图形(EGA) AL=13 320×200 256 色图形(EGA) AL=40 80×30 彩色文本(CGE400) AL=41 80×50 彩色文本(CGE400) AL=42 640×400 彩色图形(CGE400) |
文章图片
文章图片
常用的读写磁盘也在其中。
每个外部设备接口,包括各种板卡,如网卡、显卡、键盘接口电路、硬件控制器等,都有自己的只读存储器(Read Only Memory,ROM),类似于 BIOS 芯片,这些 ROM 中提供了它自己的功能调用例程,以及本设备的初始化代码。按照规范,前两个单元的内容是 0x55 和 0xAA,第三个单元是本 ROM 中以 512 字节为单位的代码长度;从第四个单元开始,就是实际的 ROM 代码。其次,我们知道,从内存物理地址 A0000 开始,到 FFFFF 结束,有相当一部分空间是留给外围设备的。如果设备存在,那么,它自带的 ROM 会映射到分配给它的地址范围内。
在计算机启动期间,BIOS 程序会以 2KB 为单位搜索内存地址 C0000~E0000 之间的区域。当它发现某个区域的头两个字节是 0x55 和 0xAA 时,那意味着该区域有 ROM 代码存在,是有效的。接着,它对该区域做累加和检查,看结果是否和第三个单元相符。如果相符,就从第四个单元进入。这时,处理器执行的是硬件自带的程序指令,这些指令初始化外部设备的相关寄存器和工作状态,最后,填写相关的中断向量表,使它们指向自带的中断处理过程。
以上都是实模式的操作,可以参考inter架构方面的书籍学习,特别是inter汇编官方的手册可以参考
参考资料:
《X86汇编语言:从实模式到保护模式》作者:李忠
《汇编语言》作者:王爽
【x86架构学习实模式篇|x86架构学习笔记实模式】
推荐阅读
- linux应用与配置|linux 5.2中如何将修改PAGE_SIZE大小
- webrtc|webrtc 编译环境搭建
- 渗透蓝队|蓝队应急响应之Windows篇
- 后端|微服务化的基石——持续集成
- docker|Docker(十八)--Docker k8s--Kubernetes存储--Configmap配置管理
- Python|Linux服务器配置Python+PyTorch+CUDA深度学习环境
- hadoop|Hadoop(三)
- ubuntu|Ubuntu-Thinkpade14无线网卡驱动安装
- 笔记|Linux安装CF-812AC usb网卡驱动