x86架构学习实模式篇|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是栈段寄存器。一个指令指针寄存器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

x86架构学习实模式篇|x86架构学习笔记实模式
文章图片

程序常用指令:
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的段地址设置
x86架构学习实模式篇|x86架构学习笔记实模式
文章图片
这里 fill 里面的 $ 相当于从 fill 这里计算的位置 ,这里的 fill 是全局的位置说明 $ 的位置也是全局的位置,start,t1,t2 段占用64字节(16进制0x40),
vstart指明段内汇编地址起始位置 该段内的汇编的地址都是从vstart开始算
section.段名.start可以获取该段的汇编地址
x86架构学习实模式篇|x86架构学习笔记实模式
文章图片

硬盘数据的访问:硬盘数据通过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架构学习实模式篇|x86架构学习笔记实模式
文章图片

如图主片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架构学习实模式篇|x86架构学习笔记实模式
文章图片

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架构学习实模式篇|x86架构学习笔记实模式
文章图片

因为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
x86架构学习实模式篇|x86架构学习笔记实模式
文章图片

一旦响应了中断,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,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)

x86架构学习实模式篇|x86架构学习笔记实模式
文章图片

x86架构学习实模式篇|x86架构学习笔记实模式
文章图片

常用的读写磁盘也在其中。
每个外部设备接口,包括各种板卡,如网卡、显卡、键盘接口电路、硬件控制器等,都有自己的只读存储器(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架构学习笔记实模式】

    推荐阅读