x86架构学习笔记实模式

        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是栈段寄存器。一个指令指针寄存器IPCS一起使用后来还增加了FS  GS段寄存器。

        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 = 0xFFFFIP = 0x0000 所以上电默认访问地址为 0xFFFF0而正位置被BIOSROM占用,ROM占用映射空间64KB,物理地址范围为0xF0000-0xFFFFF也就是1M最后64KB字节,此时CS = 0xFFFF,如果执行程序时 IP > 0x000F CS:IP 地址会回滚到0x00000地址处。在地址0xFFFF0处这里放着一条跳转指令使处理器从ROM中较低地址处取指令执行,执行完吧BIOS指令之后,会把启动盘的第一个扇区的内容加载到 0x07C00 处,然后跳转到这里运行(0x7c00是最开始时规定的,后来为了兼容所以BIOS一直这样了),一个有效的主引导扇区其最后两个字节应该是0x550xAA

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

x86架构学习笔记实模式_第1张图片

         程序常用指令:

        mov        jmp        call        div        xor        movsb        movsw        inc        dec        cld        std        div        neg        cbw        cwd        sub        idiv        jcxz        cmp

 声明字符串伪指令:

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
	
	ret

p:
	times 100 db 0
END_CHECK:
	times 510-($-$$) db 0
	db 0x55,0xaa
;loop指令是根据cx的值循环次数

注意:基址变址只支持[bx+si]        [bx+di]        [bp+si]        [bp+di]   cs:ip        ss: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的段地址设置

x86架构学习笔记实模式_第2张图片这里 fill 里面的 $ 相当于从 fill 这里计算的位置 ,这里的 fill 是全局的位置说明 $ 的位置也是全局的位置,start,t1,t2 段占用64字节(16进制0x40),

vstart指明段内汇编地址起始位置 该段内的汇编的地址都是从vstart开始算

section.段名.start可以获取该段的汇编地址

x86架构学习笔记实模式_第3张图片

 硬盘数据的访问:硬盘数据通过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.绝对跳转:

  1.         jmp near bx        ;直接用bx取代IP
  2.         jmp near cx
  3.        jump_dest dw 0xc000
  4.         jmp [jump_dest]        ;跟使用寄存器一样
  5.         jmp 0x0000:0x7c00         ;绝对远跳转
  6.         jump_far dw 0x33c0,0xf000 
  7.         jmp far [jump_far]        ;间接绝对远跳转

实模式中断:

        inter允许256个中断,中断号0-255,        8259芯片负责提供其中的15个中断

 x86架构学习笔记实模式_第4张图片

         如图主片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 芯片是最清楚的,所以它会把对应的中断号告诉处理器,处理器拿着这个中断号,要顺序做以下几件事:

  1. 保护中断现场压栈 FLAGS寄存器,清除IF TF标志位(TF是陷阱标志位)。压栈CS IP
  2. 根据中断号获取中断函数偏移地址与段地址传送至IP 和 CS
  3. 返回中断处继续执行程序,中断处理程序返回处必须是iret这会出栈IP CS FLAGS

NMI 发生时,处理器不会从外部获得中断号,它自动生成中断号码 2,其他处理过程和可屏蔽中断相同。

中断随时可能发生,中断向量表的建立和初始化工作是由 BIOS 在计算机启动时负责完成的。BIOS 为每个中断号填写入口地址,因为它不知道多数中断处理程序的位置,所以,一律将它们指向一个相同的入口地址,在那里,只有一条指令:iret。也就是说,当这些中断发生时,只做一件事,那就是立即返回。当计算机启动后,操作系统和用户程序再根据自己的需要,来修改某些中断的入口地址,使它指向自己的代码。

实时时钟:

        RTC 芯片由一个振荡频率为 32.768kHz 的石英晶体振荡器(晶振)驱动,经分频后,用于对
CMOS RAM 进行每秒一次的时间刷新。

        常规的日期和时间信息占据了 CMOS RAM 开始部分的 10 字节,有年、月、日和时、分、秒,报警的时、分、秒用于产生到时间报警中断,如果它们的内容为 0xC0~0xFF,则表示不使用报警功能。

x86架构学习笔记实模式_第5张图片

         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 单元的索引号,这种规定直到现在也没有改变。

x86架构学习笔记实模式_第6张图片

         因为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,0


disp:
	
	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                        ;向主片发送 

	iret
	  
strsetsti:
	db 'ABCDEFGHAJKLMLNPQ123456789'
	
fill512:
	times (510 - ($-$$)) db 0xFF
	db 0x55,0xAA


该程序反复显示字符串如图中的4个EEEE

x86架构学习笔记实模式_第7张图片

         一旦响应了中断,8259 中断控制器无法知道该中断什么时候才能处理结束。如果不清除相应的位,下次从同一个引脚出现的中断将得不到处理。在这种情况下,需要程序在中断处理过程的结尾,显式地对 8259 芯片编程来清除该标志,方法是向 8259 芯片发送中断结束命令(End Of Interrupt,EOI)。中断结束命令的代码是 0x20。如果外部中断是 8259 主片处理的,那么,EOI 命令仅发送给主片即可,端口号是 0x20;如果外部中断是由从片处理的,那么,EOI 命令既要发往从片(端口号 0xa0),也要发往主片。最后记得IRET返回

中断总结:

  1. 设置堆栈指针
  2. 打开8259总中断
  3. 打开时钟芯片的中断
  4. 中断处理时清除时钟中断芯片中断标志
  5. 清除8259中断标志EOI
  6. 中断返回

具体时钟芯片资料还须查询资料。

        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,0


disp:
	
	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
	

	iret
	  
strsetsti:
	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)

        

x86架构学习笔记实模式_第8张图片

x86架构学习笔记实模式_第9张图片

 常用的读写磁盘也在其中。

         每个外部设备接口,包括各种板卡,如网卡、显卡、键盘接口电路、硬件控制器等,都有自己的只读存储器(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架构学习实模式篇,架构,linux,windows,gnu,ubuntu)