[Intel汇编-NASM]主引导扇区程序介绍

1. 主引导扇区的作用以及开机之后的大致流程:

    1) 为了学习实模式下的编程而不受操作系统的影响,因为在正常的开机后,经过主引导扇区的对操作系统的加载就会把计算机的控制权交给操作系统从而进入保护模式,因此就只有运行主引导扇区程序时系统处于实模式状态;

    2) 内存逻辑地址空间:

        i. 实模式下CPU有20根地址线,能访问的地址空间有1MB,但是这1MB并不全部都指向DRAM;

        ii. 在体系结构中CPU将这1MB空间映射到了多个存储设备上,其中:

        iii. ROM占据顶部64KB空间,F0000~FFFFF;

        iv. DRAM占据底部640KB的1MB中最大的空间,00000~9FFFF;

        v. 中间空出来320KB用于分配给外围设备的存储空间,其中最重要的就是字符界面的显示器,这是一种古老的显示模式,80(个字符) × 25(行)为一屏,一屏总共能满满显示2000个字符,由于每个字符包括其ASCII码和颜色等属性,因此一个字符占两个字节,因此一屏占4000个字节,而留个其的地址空间时B8000~BFFFF这32768个字节(即刚好一个段64KB),总共可以容纳8屏多一点点儿,而在屏幕上显示字符的过程仅仅就是向显存中存放字符的过程那么简单;

    3) 开机加电后系统的运作流程:

        i. 卡机加电后cs:ip自动指向0xFFFF0的BIOS固件处;

        ii. 该处只有一条指令:jmp far 0xF000:0x0000,然后跳转到了BIOS固件的起始位置,运行BIOS的程序;

        iii. 在BIOS程序中会先在0x00000处加载BIOS自己的中断向量表,然后再执行一些硬件初始化和自检程序;

        iv. 完成自检和初始化后调用BIOS的int19中断例程将控制权交给操作系统(实质上该例程是读取磁盘0号逻辑扇区的MBR主引导扇区程序,而传统上MBR是属于操作系统的,虽然该程序仍然运行于实模式);

    4) MBR:

        i. 即主引导扇区程序,位于逻辑0号扇区内,作用是加载操作系统代码,使程序从实模式变换到保护模式,真正将系统的实权交给操作系统,即扮演一个接力手的角色;

        ii. int19BIOS例程会从磁盘的0面0道1扇区(也是逻辑0号扇区)的512B内容加载到0x0000:0x7C00处;

        iii. BIOS判断该扇区是MBR的标准就是检测512字节的最后两个字节是否是0x55和0xAA,这是规范!如果检测符合规范,则执行跳转指令jmp far 0x0000:0x7C00转到主引导程序处执行操作系统引导工作,否则就会开机失败转去执行错误处理中断例程报告本次开机失败!

    5) 在这里我们不介绍如何引导操作系统,只介绍一个简单的MBR程序,让其在屏幕上显示一定的内容,并学会如何使用BOCHS单步调试,这也是调试操作系统内核的基础;

	; 此程序用于显示标号target的汇编地址的十进制形式
	; 注意,此时跳到了0x0000:0x7C00处
	; 因此cs=0x0000而ip=0x7C00

			jmp		start

stack		times 20 db 0						; 定义一个栈,程序中需要使用
len_stack	equ $ - stack	

VIDEO_SEG_BEGIN			equ		0xB800			; 显存起始段地址
THIS_SEG				equ		0x7C00			; 整个程序自成一段,起始偏移地址为0x7C00但起始汇编地址是0x0000

str_info			db		'Label offset: '	; 要显示的信息
len_str_info		equ		$ - str_info		; 上面字符串的长度

target	db	0

    start:
			; es指向显存,ds指向str_info
			mov		ax, VIDEO_SEG_BEGIN
			mov		es, ax
			mov		ax, cs						; 此时cs=0x0000
			mov		ds, ax
			
			; 显示字符串"Label offet: "
			mov		ah, 0007H					; 设置字的颜色属性,ASCII码存放在al中,颜色属性存放在ah中	
			mov		bx, 0
			mov		bp, 0
			mov		cx, len_str_info
.lp1:		mov		al, [THIS_SEG + str_info + bx]
			mov		[es:bp], ax
			inc		bx
			add		bp, 2
			loop	.lp1

			; 初始化栈段
			mov		ax, cs
			mov		ss, ax
			mov		sp, THIS_SEG + stack + len_stack

			; 将target分解成5位十进制数的ASCII码并倒序入栈
			mov		ax, target					; ax存放被除数ax
			mov		bx, 10						; bx存放除数10
			mov		cx, 5						; 最多是个五位数,一位位分解
.lp2:		xor		dx, dx						; 将dx清零,被除数就成了dx:ax
			div		bx
			add		dl, 0x30					; 余数加0x30得到ASCII码
			mov		dh, 0004H					; 设置该数字的颜色
			push	dx							; 由于是倒序的,所以用栈反转一下
			loop	.lp2

			; 将倒序的5个十进制数顺序弹出到显存从而可以显示正确的顺序
			mov		cx, 5
.lp3:		pop		word [es:bp]
			add		bp, 2
			loop	.lp3
			mov		dh, 0004H
			mov		dl, 'D'
			mov		[es:bp], dx					; 最后补一个后缀D表示十进制数
			mov		word [es: bp + 2], 0		; 补一个空白字符冲马桶,将显存中的内容冲到显示器上

			jmp		$							; 死循环卡住程序

times 510-($-$$)	db 0						; 填满剩余空间,总共512KB
					dw 0xAA55					; 最后两字节存放MBR结束符

    

2. 局部标号和全局标号:

    1) 局部标号有一个句号"."作为前缀,而全局标号没有前缀;

    2) 局部标号的作用是用来解决程序过长时标号的命名冲突问题,局部标号可以使得同一个名字的标号可以多次重复定义并不会产生命名冲突;

    3) 局部标号的作用域:

        i. 局部标号往往夹在两个离得最近的全局标号之间,而其作用域也就位于这两个全局标号之间了;

        ii. 在作用域之外可以重复定义同名的局部标号;

        iii. 在逻辑上局部标号”属于“前面的最近的全局标号,这种属于关系类似于C语言中的结构体和结构体成员之间的关系;

        iv. 如果在作用域之内访问局部标号可以”直呼其名“,但是如果想在作用域之外访问某个局部标号就要使用和C语言访问结构体成员的一样的方式去访问那个局部标号了:

s1:			mov		ah, al
			add		cx, bx
			jmp		s2.tag1			; 作用域之外访问,所属全局标号.局部标号,否则会报错,提示没有定义该局部标号!
			nop
			nop
			sub		ax, 1
s2:			jmp		.tag1			; 在作用域范围之内访问
			mov		ax, bx
			add		ax, bx
	.tag1:	nop
			mov		bx, cx	
!!!注意:gdb拒绝在命令行中对局部标号设置断点(可能是gdb的一个bug吧!),因此只能通过外部工具比如Insight等调试工具进行设置(这些工具可以在任意一行上设置断点);


; 应用程序头
; 用于提供加载器相关加载信息
; 是应用程序规范的一部分
section header vstart=0
	app_size		dd	app_end					; [APP_SIZE:0x00] 程序的大小(字节)
	app_entry		dw	start					; [APP_ENTRY:0x04] 入口处偏移地址
	app_entry_seg	dd	section.code1.start		; [APP_ENTRY_SEG:0x06] 入口处段地址
	; section.段名.start是NASM提供的伪指令,用于段起始位置在源程序中的绝对汇编地址
	; 绝对汇编地址是指相对于整个源程序头的偏移量,而整个程序头的绝对汇编地址是0
	; 绝对汇编地址是一个32位无符号数,因此使用dd表示

	c_realloc_tbl	dw	(tbl_end - tbl_start) / 4		; [C_REALLOC_TBL:0x0A] 重定位表表项数目
tbl_start:	; [TBL_START:0x0C]
	seg_addr_code1	dd	section.code1.start
	seg_addr_code2	dd	section.code2.start
	seg_addr_data1	dd	section.data1.start
	seg_addr_data2	dd	section.data2.start
	seg_addr_stack	dd	section.stack.start
tbl_end:
; section header end


;;
;;
section stack align=16 vstart=0
	resb 256
stack_end:
; section stack end


;;
;;
section data1 align=16 vstart=0
	msg0 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 data1 end


;;
;;
section data2 align=16 vstart=0
	msg1 db '  Welcome and enjoy NASM! '
		 db '2015-01-05'
		 db 0
; section data2 end


;;
;;
section code1 align=16 vstart=0
start:
			mov		ax, [seg_addr_stack]
			mov		ss, ax
			mov		sp, stack_end

			mov		ax, [seg_addr_data1]
			mov		ds, ax
			mov		bx, msg0
			call	put_string					; 显示第一段信息

			; 在加载程序中将es指向header了
			push	word [es:seg_addr_code2]	; 先将code2的偏移地址和段地址入栈
			mov		ax, _start.begin
			push	ax
			retf								; 利用retf修改cs:ip使其跳转至code2
	.continue:
			mov		ax, [es:seg_addr_data2]
			mov		ds, ax
			mov		bx, msg1
			call	put_string					; 使ds:bx指向msg1并输出

			jmp		$
; end start

; 字符串控制宏以及显卡光标端口宏
CHAR_TRAIL			equ		0x00		; 字符串结束符
CHAR_RET			equ		0x0D		; 回车符
CHAR_NL				equ		0x0A		; 换行符
DCHAR_NONE			equ		0x0720		; 显存中显示空的字

PORT_CHOOSE			equ		0x3D4		; 索引端口,用于选择子端口(8位) 
SUBPORT_HIGH		equ		0x0E		; 子端口号
SUBPORT_LOW			equ		0x0F		; 这两个子端口分别存放光标位置的高位和低位
PORT_DATA			equ		0x3D5		; 数据端口,存放选定的端口中的数据(8位)

VIDEO_SEG_BEGIN		equ		0xB800		; 显卡区域起始段地址

; func put_string
; <- [ds:bx]:msg0
; colision register: es
; 将msg0打印至屏幕
put_string:
			push	es

			; 获取当前光标位置保存在ax中
			mov		dx, PORT_CHOOSE
			mov		al, SUBPORT_HIGH		
			out		dx, al					; 选择一个子端口
			mov		dx, PORT_DATA
			in		al, dx
			mov		ah, al					; 从子端口中读取光标高位保存在ah中

			mov		dx, PORT_CHOOSE
			mov		al, SUBPORT_LOW
			out		dx, al
			mov		dx, PORT_DATA
			in		al, dx					; 同理从子端口中读取光标低位保存在al中
											; 最终将整个结果保存在ax中

			; 目前ax存放着光标的位置

	.lp:	mov		cl, [bx]					; 读取一个字符保存在cl中
			cmp		cl, CHAR_TRAIL				; 判断该字符是否是结束符
			je		.ret
			call	put_char					; 不是结束符就打印该字符
			inc		bx							; 继续读取下一个字符
			jmp		.lp

	.ret:	pop		es
			ret

; func put_char
; <- cl:当前读取的一个字符
; colision register: ds, bx
put_char:	
			push	ds
			push	bx						; 备份

			; ds和es都指向显卡
			mov		bx,	VIDEO_SEG_BEGIN
			mov		ds, bx
			mov		es, bx
			
			; 目前ax存放着光标的位置

			cmp		cl, CHAR_RET			; 判断字符是否是回车
			jne		.next0					; 不是回车则继续接下来的步骤
	.deal_ret: ; 是回车则处理回车
			mov		bl, 80
			div		bl
			mul		bl						; 除去光标位置中80的余数即可
											; ax中得到的是回车后光标的位置
			jmp		.set_cursor

	.next0:	cmp		cl, CHAR_NL				; 判断是否是换行符
			jne		.next1					; 如果不是换行符则继续接下来的代码
	.deal_nl: ; 处理换行的情形
			add		ax, 80					; 换行很简单,只要加80即可
			jmp		.deal_roll_screen		; 换行可能会造成屏幕滚动,因此需要处理

	.next1:	; 结束、回车、换行都不是那就是普通字符了,因此需要打印出来,并且光标后移一位
			mov		bx, ax					; 先将ax复制到bx中
			shl		bx, 1					; 显卡区域每个字符占两个字节(还有一个属性字节)
			mov		[bx], cl
			inc		ax						; 光标后移一位
			; jmp	.deal_roll_screen		; 光标后移也可能会造成滚屏

	.deal_roll_screen:
			cmp		ax, 2000			
			jl		.set_cursor				; 检查光标是否越界,如果越界则需要滚屏,否则可以直接设置光标
		.roll_screen: ; 滚屏处理
			mov		si, 80 * 2
			mov		di, 0
			mov		cx, 2000 - 80
			cld
			rep		movsw
		.clear_bottom_line: ; 滚屏后需要清除最后一行
			mov		bx, (2000 - 80) * 2
			mov		cx, 80
		.cls:
			mov		word [bx], DCHAR_NONE
			add		bx, 2
			loop	.cls
			
			mov		ax, 2000 - 80		; 滚屏后光标位置设置成最后一行起始
			; jmp	.set_cursor			; 滚屏完成后方可显示新的光标的位置了

	.set_cursor:
			mov		bx, ax				; 将光标位置备份到bx中,因为访问端口会用到ax

			mov		dx, PORT_CHOOSE
			mov		al, SUBPORT_HIGH
			out		dx, al
			mov		dx, PORT_DATA
			mov		al, bh
			out		dx, al

			mov		dx, PORT_CHOOSE
			mov		al, SUBPORT_LOW
			out		dx, al
			mov		dx, PORT_DATA
			mov		al, bl
			out		dx, al

			pop		bx
			pop		ds

			ret
; section code1 end


;;
;;
section code2 align=16 vstart=0
_start:
	.begin:	push	word [es:seg_addr_code1]		; code2没做什么实事就是再跳回code1的continue继续执行
			mov		ax, start.continue
			push	ax
			retf
; section code2 end

			
;;
;;
section trail align=16
app_end:
; section trail end


; 主引导扇区程序作为应用程序加载器

; 虽然就只有一个段但是也需要定义
; 最主要是为了使用段属性vstart=0x7C00
; 这样就可以使得段内的所有汇编地址都是相对0x7C00开始的
; 因为MBR加载在0x0000:0x7C00处,因此IP初始化为0x7C00
; 而所有偏移地址都是相对0x7C00的
; 有了这一步程序中的所有标号都能真正代表偏移地址了
section loader align=16 vstart=0x7C00
			jmp		near start

	LBA_APP_START		equ		100				; 应用程序所在硬盘的起始逻辑扇区号,这里是人为规定的
	ADDR_20_LOAD_START	dd		0x10000			; 内存中加载的起始20位绝对物理地址

	; 应用程序头中信息的偏移地址
	APP_SIZE_LOW		equ		0x00		
	APP_SIZE_HIGH		equ		0x02
	APP_ENTRY			equ		0x04
	APP_ENTRY_SEG		equ		0x06
	APP_ENTRY_SEG_LOW	equ		0x06
	APP_ENTRY_SEG_HIGH	equ		0x08
	C_REALLOC_TBL		equ		0x0A
	TBL_START			equ		0x0C

			; 从0x0FFFF往下(即地址减小)的一段区域一般都作为MBR的栈!
			; 因此ss:sp指向0x0000:0x0000
			; 这样在push的时候sp能回到0xFFFF
start:		mov		ax, 0
			mov		ss, ax
			mov		sp, ax

			; ds -> 内存中加载的起始位置段地址
			mov		ax, [cs:ADDR_20_LOAD_START]
			mov		dx, [cs:ADDR_20_LOAD_START+2]
			mov		bx, 16
			div		bx
			mov		ds, ax

			mov		es, ax

			; 先读取一个扇区,即应用程序头所在的扇区
			xor		di, di						
			mov		si, LBA_APP_START			; [di:si]全局保存当前读取的逻辑扇区号
			mov		cx, 1						; 读取一个扇区
			call	read_lba				
			; 读取完毕,ds:0指向程序的第一扇区中的内容

			mov		dx, [APP_SIZE_HIGH]
			mov		ax, [APP_SIZE_LOW]
			mov		bx, 512
			div		bx
			cmp		dx, 0
			jne		.deal_left				; 有余数,可以将已经读取的那个扇区看做余数的扇区
			dec		ax						; 无余数则需要减去已经读取的那个扇区
	.deal_left:
			cmp		ax, 0
			je		redirect_entry			; 如果没有剩余扇区要读则直接去重定位程序入口点
			push	ds						; 备份并改变其指向

			mov		cx, ax					; 剩余要读的扇区数量
			mov		ax, ds
			add		ax, 0x20				; 使其指向下一个512字节起始处(必然是16位对齐的)
			mov		ds, ax
			inc		si						; 指向下一个要读的扇区
			call	read_lba

			pop		ds						; 恢复ds使其指向加载的程序的开始处

		; 到此为止程序彻底加载完毕
		
		; 接下来的工作是将程序头中的入口地址,以及重定位表中的地址
		; 修改成实际的物理地址
		; 这里所重定位的地址都是段地址
		; 将程序中段的绝对汇编地址更新成加载在内存中的实际物理段地址
		; 公式是:16位物理段地址 = (整个程序起始位置的20位物理 + 段的32位绝对汇编地址) >> 4

	redirect_entry: ; 重定位入口处地址
			mov		dx, [APP_ENTRY_SEG_HIGH]		; [dx:ax]中保存入口处的绝对汇编地址
			mov		ax, [APP_ENTRY_SEG_LOW]
			call	calc_seg_phy_addr_16			; 计算段的16位段地址(即物理段地址),结果保存在ax中
			mov		[APP_ENTRY_SEG], ax				; 更新

			; 处理重定位表
			mov		cx, [C_REALLOC_TBL]
			mov		bx, TBL_START
	.realloc:
			mov		dx, [bx + 2]
			mov		ax, [bx]
			call	calc_seg_phy_addr_16
			mov		[bx], ax
			add		bx, 4
			loop	.realloc

			;mov		ax, ds	
			;mov		es, ax							; 使es初始化成加载的起始位置并交给应用程序处理
			jmp		far [APP_ENTRY]					; 控制权交给应用程序


; func read_lba
; <- [di:si]:读取的逻辑扇区号
; <- cx:读取的扇区数量
; <- ds:目的区域段地址
; 将cx个扇区的内容读取到ds:0所指向的内存空间中
read_lba:
	PORT_DATA		equ		0x1F0		; 数据端口(16位)
	PORT_ERRNO		equ		0x1F1		; 错误端口(8位)保存最后一次执行命令后的状态(错误原因)
	PORT_CLBA		equ		0x1F2		; 计数端口(8位)保存读写的扇区数量
	PORT_LBA_START	equ		0x1F3		; 逻辑扇区号端口(32位共4个8位口)
										; 低28位确定待操作的起始扇区号
										; 最高的4位指定扇区寻址模式以及类型选择符)
	PORT_CTRL		equ		0x1F7		; 控制端口(8位)下读写命令同时又能反映硬盘工作状态

	CTRL_READ		equ		0x20		; 读命令,向控制端口发送

	BIT_MASK		equ		10001000B	; 位掩码,取控制端口的第7位和第3位
										; 第7位表示硬盘是否忙,1表示忙
										; 第3位表示硬盘是否就绪,1表示就绪
	STATUS_READY	equ		00001000B	; 彻底就绪时第7位是0,第3位是1,用于检测硬盘是否就绪

			; 指定读取的扇区数量
			mov		dx, PORT_CLBA
			mov		al, cl
			out		dx, al

			; 向LBA地址口写入28位逻辑扇区号

			mov		dx, PORT_LBA_START		; 0~7位
			mov		ax, si
			out		dx, al

			inc		dx						; 8~15位
			mov		al, ah
			out		dx, al

			inc		dx						; 16~23位
			mov		ax, di
			out		dx, al

			inc		dx						; 24~27位
			mov		al, 0xE0
			;mov		al, 111_1_0000B		; ah保存24~27位,al中保存扇区寻址模式以及类型选择符
			or		al, ah
			out		dx, al

			; 发出读命令
			mov		dx, PORT_CTRL
			mov		al, CTRL_READ
			out		dx, al
	.waits:	; 检测硬盘是否就绪,没就绪就一直等待就绪
			in		al, dx
			and		al, BIT_MASK
			cmp		al, STATUS_READY
			jne		.waits

			; 准备就绪就开始读取
			shl		cx, 2
			mov		dx, PORT_DATA
			xor		bx, bx
	.readw: ; 循环读取程序,将其加载至ds:0处
			in		ax, dx
			mov		[bx], ax
			add		bx, 2
			loop	.readw

			ret


; func calc_seg_phy_addr_16
; <- [dx:ax]:段32位绝对汇编地址
; -> ax:16位物理段地址
calc_seg_phy_addr_16:
			; 这里的20位起始加载地址使用32位保存的
			; 因此可以通过带进位的加法得到段起始位置的实际的20位物理地址
			add		ax, [cs:ADDR_20_LOAD_START]
			adc		dx, [cs:ADDR_20_LOAD_START+2]

			; 现在将绝对的20位物理地址右移4位就能得到16位的物理段地址了
			; 必须dx和ax同时右移
			; 方法是ax右移4位即可
			; 而dx采用循环右移4位,应该移到ax高4位的那4位重新回到dx高4位
			; 然后用位掩码去的dx高4位
			; 再利用or将这4位写入ax的高4位即可
			shr		ax, 4			; 低16位右移4位
			ror		dx, 4
			and		dx, 0xF000		; 位掩
			or		ax, dx			; 写入

			ret

times 510-($-$$) db 0
				 dw 0xAA55


你可能感兴趣的:(MBR,nasm,intel汇编)