《x86汇编语言:从实模式到保护模式》(第8章---硬盘和显卡的访问与控制)

本章主要讲加载器代码如何一步步把用户程序代码从硬盘加载到内存的,然后把CPU交给用户程序,执行用户程序代码。用户程序功能就是显示两段字符串到屏幕上。作者讲解了CPU如何访问显卡和硬盘这样的外围设备,抽象出来说的话就是CPU通过端口向外围设备发号施令以及读写数据。笔者阅读并根据自己理解稍微更改并重新注释了作者的源码(具体细节书中说明比较详细,结合我的注释更易理解),具体如下。

		;文件名:c08_01_mbr.asm
		;文件说明:硬盘主引导扇区代码
		;参考书:《x86汇编语言:从实模式到保护模式》李忠 著
		;代码功能:从硬盘加载用户程序到内存,把CPU交给用户程序
		;(用户程序头部包含用户程序的长度、入口地址、段重定位表等信息)
		
		app_lba_start equ 100		;声明常数(用户程序所在的硬盘逻辑扇区号,人为指定)
									;常数声明不占用汇编地址
	SECTION mbr align=16 vstart=0x7c00		;段mbr,16字节对齐,相对段起始偏移从0x7c00开始
		
		;设置堆栈及用户程序被加载的段地址(用户程序被加载的物理地址人为指定,见本程序末phy_base)
		xor ax,ax
		mov ss,ax
		mov sp,ax
		
		mov ax,[cs:phy_base]		;用户程序被加载的物理起始地址的低16位
		mov dx,[cs:phy_base+0x02]	;用户程序被加载的物理起始地址的高4位是数的低4位
		mov bx,16
		div bx						;除以16,即右移4位,得到用户程序被加载的段地址
		mov ds,ax
		mov es,ax
		
		;先把用户程序头部从对应硬盘扇区(事先写入的指定扇区app_lba_start)加载到指定的内存区域
		;读写硬盘需要通过DI:SI提供扇区号
		xor di,di
		mov si,app_lba_start
		xor bx,bx					;提供偏移地址,用户程序加载到DS:0x0000
		call read_hard_disk_0
		
		;利用刚读入的用户程序头部,计算还剩下多少扇区,依次读取(用户程序是连续扇区存放的)
		mov dx,[2]
		mov ax,[0]
		mov bx,512
		div bx
		cmp dx,0					;如果除以512字节,余数如果是0,那么剩余扇区数是ax-1,否则就是ax
		jnz @1
		dec ax
	
	@1:
		cmp ax,0
		jz direct					;剩余扇区数是0,就不用再读了,直接进入下面步骤
		
									;剩余扇区数不是0,就把剩余扇区读进来
		push ds	
		mov cx,ax
	@2:
		mov ax,ds
		add ax,0x20					;段地址加0x20,相当于物理内存地址增加512字节,保证存得下1个扇区
		mov ds,ax
		xor bx,bx
		inc si						;前提得保证扇区号不会加到超过16位表示的数
		call read_hard_disk_0
		loop @2
		
		pop ds
		
		;更新被加载后的用户程序头部信息
	direct:
		mov dx,[0x08]
		mov ax,[0x06]
		call calc_segment_base
		mov [0x06],ax				;更新用户程序入口代码段的段地址
		
		mov cx,[0x0a]				;段重定位表项数
		mov bx,0x0c					;段重定位表开始地址
	
	realloc:
		mov dx,[bx+0x02]			;4位地址是dx的低4位
		mov ax,[bx]					;16位地址在ax中
		call calc_segment_base
		mov [bx],ax					;更新该重定位表项段的段地址
		add bx,4
		loop realloc
		
		jmp far [0x04]				;远转移到用户程序入口地址(当前CS,IP压栈,然后CS=DS,IP=0x04;-------------------------------------------------------------------------------
	read_hard_disk_0:               ;从硬盘读取1个逻辑扇区(通过端口操作)
									;一个逻辑扇区号(28位)需要四个端口(8位)来提供
									;0x1f3~0x1f5各提供8位,0x1f6的后四位用来提供4;输入:DI:SI=逻辑扇区号
									;      DS:BX=目标缓冲区地址
		push ax
		push bx
		push cx
		push dx

		mov dx,0x1f2				
		mov al,1
		out dx,al                   ;往端口0x1f2写入al,表示要从硬盘读取al个扇区   

		inc dx
		mov ax,si
		out dx,al                   ;往端口0x1f3写入al,提供LBA的7~0位(si的低8位)

		inc dx                        
		mov al,ah
		out dx,al                   ;往端口0x1f4写入ah,提供LBA的15~8位(si的高8位)

		inc dx                        
		mov ax,di
		out dx,al                   ;往端口0x1f5写入al,提供LBA的23~16位(di的低8位)

		inc dx                      ;端口0x1f6
		mov al,0xe0                 
		or al,ah                    ;0xe0与ah(di高8位)或运算,保证di的4位和0xe04位不变
		out dx,al					;di的低4位提供LBA的27~24位,0xe0的高4位表示主盘及LBA模式

		inc dx
		mov al,0x20
		out dx,al					;往端口0x1f7写入0x20,发送读命令

	.waits:
		in al,dx					;从端口0x1f7读状态信息
		and al,0x88					;硬盘系统准备好后,第7位是0,第3位是1,即0x08
		cmp al,0x08
		jnz .waits                  ;硬盘系统忙,准备中... 

		mov cx,256                  ;总共要读取的字数(512字节对应256字)
		mov dx,0x1f0				;0x1f0是数据端口,端口号要用dx传输

	.readw:
		in ax,dx					;利用端口0x1f0每次从扇区SI:DI读一个字到ax中
		mov [bx],ax
		add bx,2
		loop .readw
		
		pop dx
		pop cx
		pop bx
		pop ax

		ret
;-------------------------------------------------------------------------------
	calc_segment_base:              ;计算16位段地址
                                    ;输入:DX:AX=32位物理地址(原始)
                                    ;返回:AX=16位段基地址(重定位之后)                          
		push dx

		add ax,[cs:phy_base]
		adc dx,[cs:phy_base+0x02]
		shr ax,4
		ror dx,4
		and dx,0xf000
		or ax,dx

		pop dx

		ret	
		
;-------------------------------------------------------------------------------
		phy_base dd 0x10000			;用户程序被加载到的物理起始地址
									;20位地址,所以用双字存放存的下
		
		times 510-($-$$) db 0
		db 0x55,0xaa
		;文件名:c08_02_user.asm
		;文件说明:用户程序
		;参考书:《x86汇编语言:从实模式到保护模式》李忠 著
		;代码功能:把两段字符显示到屏幕,光标随着字符显示移动,支持滚屏
;-------------------------------------------------------------------------------
	SECTION header vstart=0                     ;定义用户程序头部段
		program_length  dd program_end          ;程序总长度[0x00]

		;用户程序入口点
		code_entry:
			dw start                			;偏移地址[0x04]
			dd section.code_1.start 			;入口程序所在段的物理地址[0x06] 
		
		;段重定位表项个数[0x0a]
		realloc_tbl_len dw (header_end-code_1_segment)/4
		
		;段重定位表           
		code_1_segment  dd section.code_1.start ;[0x0c]
		code_2_segment  dd section.code_2.start ;[0x10]
		data_1_segment  dd section.data_1.start ;[0x14]
		data_2_segment  dd section.data_2.start ;[0x18]
		stack_segment   dd section.stack.start  ;[0x1c]

		header_end:                
;-------------------------------------------------------------------------------
	SECTION code_1 align=16 vstart=0	;定义代码段116字节对齐)
	
	put_string:                         ;显示串(0结尾)到屏幕。
										;输入:DS:BX=串地址
		mov cl,[bx]
		or cl,cl                        
		jz .exit                        ;最后一个字符是0,是0就退出
		call put_char
		inc bx                           
		jmp put_string

	.exit:
		ret
	
;-------------------------------------------------------------------------------
	put_char:                           ;显示一个字符
										;输入:cl=字符ascii
		push ax
		push bx
		push cx
		push dx
		push ds
		push es

		;以下取当前光标位置
		mov dx,0x3d4
		mov al,0x0e
		out dx,al						;向端口0x3d4表明要从寄存器0x0e得到光标高8位
		mov dx,0x3d5
		in al,dx                        ;从端口0x3d5读取高8位到al
		mov ah,al						;并存放到ah中

		mov dx,0x3d4
		mov al,0x0f
		out dx,al						;向端口0x3d4表明要从寄存器0x0f得到光标低8位
		mov dx,0x3d5
		in al,dx                        ;从端口0x3d5读取低8位到al 
		mov bx,ax						;BX=代表光标位置的16位数
		
		cmp cl,0x0d                     ;回车符?
		jnz .put_0a                     ;不是。看看是不是换行等字符 
		mov bl,80 						                     
		div bl							;是回车符,除以80得到光标所在行号放入al中
		mul bl							;行号乘以80得到当前行的行首位置
		mov bx,ax
		jmp .set_cursor

	.put_0a:
		cmp cl,0x0a                     ;换行符?
		jnz .put_other                  ;不是,那就正常显示字符 
		add bx,80
		jmp .roll_screen

	.put_other:                         ;正常显示字符
		mov ax,0xb800
		mov es,ax
		shl bx,1						;光标位置乘2代表字符的偏移地址(1个字符占2字节)
		mov [es:bx],cl

		;以下将光标位置推进一个字符
		shr bx,1
		add bx,1

	.roll_screen:
		cmp bx,2000                     ;光标超出屏幕?滚屏
		jl .set_cursor

		mov ax,0xb800
		mov ds,ax
		mov es,ax
		cld
		mov si,0xa0						;整体上移一行(80字符160字节)
		mov di,0x00
		mov cx,1920
		rep movsw
		mov bx,3840                     ;清除屏幕最底一行
		mov cx,80
	.cls:
		mov word[es:bx],0x0720
		add bx,2
		loop .cls

		mov bx,1920

	.set_cursor:						;设置光标在屏幕上的显示位置
										;输入:BX=光标位置
		mov dx,0x3d4
		mov al,0x0e
		out dx,al
		mov dx,0x3d5
		mov al,bh
		out dx,al
		mov dx,0x3d4
		mov al,0x0f
		out dx,al
		mov dx,0x3d5
		mov al,bl
		out dx,al

		pop es
		pop ds
		pop dx
		pop cx
		pop bx
		pop ax

		ret
;-------------------------------------------------------------------------------
	start:
		;已经经过加载器代码加载并更新头部信息
		mov ax,[stack_segment]           ;设置用户程序自己的堆栈 
		mov ss,ax
		mov sp,stack_end

		mov ax,[data_1_segment]          ;设置用户程序自己的数据段
		mov ds,ax

		mov bx,msg0
		call put_string                  ;显示第一段信息

		push word [es:code_2_segment]	 ;从代码段1跳到代码段2。。。。。。。。
										 ;把代码段2的地址压入栈,retf后,弹栈,
										 ;将用该地址设置CS:IP,从而跳至代码段2;这会导致代码段2执行完后回不到加载器,
										 ;可以像记录代码段1的入口地址一样,记录
										 ;代码段2的入口地址,代码段1执行完回到
										 ;加载器,再转移到代码段2,代码段2执行
										 ;完就能回到加载器了
		mov ax,begin
		push ax                          ;可以直接push begin,80386+

		retf                             ;转移到代码段2执行

	continue:
		mov ax,[es:data_2_segment]       ;段寄存器DS切换到数据段2 
		mov ds,ax

		mov bx,msg1
		call put_string                  ;显示第二段信息 

		jmp $ 
;-------------------------------------------------------------------------------
	SECTION code_2 align=16 vstart=0     ;定义代码段216字节对齐)

	begin:
		push word [es:code_1_segment]
		mov ax,continue
		push ax                          ;可以直接push continue,80386+

		retf                             ;转移到代码段1接着执行 

;-------------------------------------------------------------------------------
	SECTION data_1 align=16 vstart=0

	msg0:								 ;0x0d,0x0a回车换行标志。0是字符串结束标志
		db '  This is NASM - the famous Netwide Assembler. '
		db 'Back at SourceForge and in intensive development! '
		db 'Get the current versions from http://www.nasm.us/.'
		db 0x0d,0x0a,0x0d,0x0a
		db '  Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a
		db '     xor dx,dx',0x0d,0x0a
		db '     xor ax,ax',0x0d,0x0a
		db '     xor cx,cx',0x0d,0x0a
		db '  @@:',0x0d,0x0a
		db '     inc cx',0x0d,0x0a
		db '     add ax,cx',0x0d,0x0a
		db '     adc dx,0',0x0d,0x0a
		db '     inc cx',0x0d,0x0a
		db '     cmp cx,1000',0x0d,0x0a
		db '     jle @@',0x0d,0x0a
		db '     ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a
		db 0

;-------------------------------------------------------------------------------
	SECTION data_2 align=16 vstart=0

	msg1:
		db '  The above contents is written by LeeChung. '
		db '2011-05-06'
		db 0
;-------------------------------------------------------------------------------
	SECTION stack align=16 vstart=0

		resb 256						;未初始化哦!有可能是历史遗留数据

		stack_end: 
;-------------------------------------------------------------------------------
	SECTION trail align=16
		program_end:

把编译后的mbr代码写入0扇区,把编译后的user代码写入100扇区,之后运行虚拟机结果:
在这里插入图片描述

参考资料

[1] 《x86汇编语言:从实模式到保护模式》李忠 著
[2] 《汇编语言》王爽 著

发布了323 篇原创文章 · 获赞 193 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/ccnuacmhdu/article/details/103647847
今日推荐