Linux教程

bootsect.s 解读——Linux-0.11 剖析笔记(二)

本文主要是介绍bootsect.s 解读——Linux-0.11 剖析笔记(二),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 一些符号常量
    • 老式Linux设备号的命名规则
  • bootsect 把自己搬运到 0x90000,并跳转
  • 加载 setup 模块到 0x90200
    • INT 13H AH=02H:读扇区
    • INT 13H AH=00H:磁盘控制器复位
  • 获得磁盘驱动器参数(主要是每磁道的扇区数)
    • INT 13H AH=08H:读取驱动器参数
  • 打印 “Loading system ...”
    • INT 10H AH=03H:获取光标位置和形状
    • INT 10H AH=13H:写字符串
    • 回车和换行
  • 加载 system 到 0x10000
    • 过程 `read_it`
    • 过程 `read_track`
    • 过程 `kill_motor`
    • DOR(数字输出寄存器)
  • 确认根文件系统设备号
  • 跳转到 setup 去执行
  • 总结

说明:本文据 bootsect.s 分析  这篇博文修改而来,增加了一些内容。读者看这篇就可以。
为了节省篇幅,完整的代码就不贴了。感兴趣的朋友可以去下载,下载地址是:
http://oldlinux.org/Linux.old/

本文,我打算详解bootsect.s。如有纰缪,还请各位看官斧正。为了不使代码片太长,我删去了一些原来的注释。
 

一些符号常量

; 在 as86 汇编语言程序中,凡是以感叹号’!’或分号’;’开始的语句均为注释


SYSSIZE = 0x3000  ;system模块的长度

; globl 表明标识符是全局的,使得符号对连接器可见,变为对整个工程可用的全局变量
.globl begtext, begdata, begbss, endtext, enddata, endbss

; .text, .data, .bss 分别定义当前代码段、数据段和未初始化数据段。
; 在链接 多个目标模块时,链接程序 ld86 会根据它们的类别把各个目标模块中的相应段
; 分别组合在一起。这里把三个段都定义在同一重叠地址范围中,因此本程序实际上不分段。

.text
begtext:
.data
begdata:
.bss
begbss:
.text

SETUPLEN = 4			    ! setup模块的长度,4个扇区
BOOTSEG  = 0x07c0			! original address of boot-sector
INITSEG  = 0x9000			! bootsect把自身搬运到0x90000
SETUPSEG = 0x9020			! setup模块被加载到 0x90200
SYSSEG   = 0x1000			! system模块被加载到0x10000
ENDSEG   = SYSSEG + SYSSIZE	! where to stop loading, 0x1000 + 0x3000 = 0x4000, 停止加载的段地址


; 这个可以根据情况修改,比如我的实验环境,就修改为 0x301

ROOT_DEV = 0x306            !第2个硬盘的第1个分区

ROOT_DEV = 0x306 ,这里的0x306表示第2个硬盘的第1个分区,当年Linus是在第2个硬盘的第1个分区上安装了Linux-0.11操作系统。

老式Linux设备号的命名规则

设备号 = 主设备号 * 256 + 次设备号

或者说:

dev_no = (major << 8) + minor

这里的主设备号是事先定义好的(1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道)。譬如对于硬盘,主设备号为3,因此3*256+0=0x300即为系统中第一个硬盘的设备号。更多的例子如下表:

设备号设备文件对应的设备
0x300/dev/hd0系统中第一个硬盘
0x301/dev/hd1系统中第一个硬盘的第一分区
0x302/dev/hd2系统中第一个硬盘的第二分区
0x303/dev/hd3系统中第一个硬盘的第三分区
0x304/dev/hd4系统中第一个硬盘的第四分区
0x305/dev/hd5系统中第二个硬盘
0x306/dev/hd6系统中第二个硬盘的第一分区
0x307/dev/hd7系统中第二个硬盘的第二分区
0x308/dev/hd8系统中第二个硬盘的第三分区
0x309/dev/hd9系统中第二个硬盘的第四分区

bootsect 把自己搬运到 0x90000,并跳转

entry _start  ; 告知链接程序,程序从 start 标号开始执行
_start:
    mov ax,#BOOTSEG 
    mov ds,ax      ! ds = 0x07c0
    mov ax,#INITSEG
    mov es,ax      ! es = 0x9000
    mov cx,#256    ! 搬运 256 次
    sub si,si      ! si = 0
    sub di,di      ! di = 0
                   ! ds:si = 0x07c0:0x0 
                   ! es:di = 0x9000:0x0
    rep
    movw           ! 每次搬运 2 字节
    jmpi go,INITSEG  ! 跳转到 0x9000:go

以上代码表示把 ds:si 处(物理地址 0x7c00)的内容搬运到 es:di(物理地址 0x90000),一共搬运 512 字节,即主引导扇区把自己移动到了 0x90000 处

jmpi go,INITSEG 是段间跳转,INITSEG 是段地址,go 是偏移地址。这句话执行完,CPU 一下子跑到了0x9000:go 处。

对于movw指令,可以参考我的博文。
http://blog.csdn.net/longintchar/article/details/50949923

我的疑问是,Linus 为什么没有清除 DF 标志呢?是不是设置 DF=0 会更严谨呢?
在这里插入图片描述

在这里插入图片描述 

CPU 上电后,EFLAGS 的初始值是 00000002H,所以 DF 是 0

跳转后继续执行下面的指令

go:	mov	ax,cs
	mov	ds,ax
	mov	es,ax   ; cs = ds = es = ss = 0x9000
! put stack at 0x9ff00.   
! 因为从 0x90200 地址开始处还要放置 setup 程序,setup 大约 
! 为 4 个扇区,因此 SP 要指向大于(0x200 *5 + 堆栈大小)处。
! 0x200 * 5 = 0xa00 , 0xFF00 - 0xA00 = 0xF500, 栈的大小是 0xF500,足够大
	mov	ss,ax
	mov	sp,#0xFF00		 !es:sp = 0x9000:0xff00 ,栈的设置	

加载 setup 模块到 0x90200

load_setup:
	mov	dx,#0x0000		! 驱动器号(DL)0,磁头号(DH)0
	mov	cx,#0x0002		! 起始扇区号2, 磁道号0
	mov	bx,#0x0200		! 偏移地址0x200
	mov	ax,#0x0200+SETUPLEN	! 功能号AH=0x02,AL=要读的扇区数目=SETUPLEN=4 
	int	0x13			! read it
	jnc	ok_load_setup	! CF = 0 则跳转
	mov	dx,#0x0000      !需要复位的驱动器号=DL=0
	mov	ax,#0x0000		!功能号AH=0
	int	0x13            ! 复位磁盘
	j	load_setup

以上代码利用 INT 13H, AH=02H 把 setup 模块从磁盘(2~5扇区)加载到 0x90200 后面。
注意:柱面(或者磁道)号和磁头号都从 0 开始,扇区号从 1 开始。

INT 13H AH=02H:读扇区

此功能从磁盘上把一个或更多的扇区内容读进内存。这是一个低级功能,在一个操作中读取的全部扇区必须在同一条磁道上。

参数说明
入口参数
AH=02H ,指明调用读扇区功能。
AL要读的扇区数目,不允许使用读磁道末端以外的数值,也不允许使该寄存器为0。
DL需要进行读操作的驱动器号,0表示软盘,80H表示硬盘。
DH所读磁盘的磁头号。
CH磁道号的低8位数(磁道号共10位)。
CL低5位放入所读起始扇区号,位7-6表示磁道号的高2位。
ES:BX读出数据的缓冲区地址。
返回参数
CF=0,操作成功;=1,操作失败。
AH错误返回码。
AL实际读到的扇区数。

INT 13H AH=00H:磁盘控制器复位

此功能用于复位磁盘(软盘和硬盘)。当磁盘I/O功能调用出现错误时,需要调用此功能。

参数说明
入口参数
AH=00H,指明调用复位磁盘功能。
DL需要复位的驱动器号。软盘:00H-7FH;硬盘:80H-FFH
返回参数
CF=0,操作成功;=1,则操作失败
AH错误返回码。

获得磁盘驱动器参数(主要是每磁道的扇区数)

ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track

	mov	dl,#0x00    !驱动器号为0,说明是软盘
	mov	ax,#0x0800	! AH=8 is get drive parameters
	int	0x13
	mov	ch,#0x00    !这里用不上软盘的最大磁道号,可以使CH=0
	; seg 用于将段超越前缀中指明的段寄存器去取代指令中默认的段寄存器,它只影响其下一条语句
	; 实际上,由于本程序代码段和数据段重叠,因此本程序中此处可以不使用该指令
	seg cs         
	mov	sectors,cx  
	; 以上两句也可以写为  mov cs:[sectors], cx	
	; 保存每磁道最大扇区数。对于软盘,最大磁道号不会超过256,所以CH足以表示,CL[7:6]为0
	
	mov	ax,#INITSEG
	mov	es,ax       !因为上面ES的值被修改,所以令 ES=0x9000

INT 13H AH=08H:读取驱动器参数

参数说明
入口参数
AH=08H,读取驱动器参数
DL驱动器号(如果是硬盘则[7]=1)
返回参数
CF0-操作成功;1-操作失败
AH错误返回码
BL驱动器类型
CH最大磁道号的[7:0]
CL[7:6]最大磁道号的[9:8]
CL[5:0]每磁道最大扇区数
DH最大磁头数
DL驱动器数量
ES:DI指向软驱磁盘参数表

打印 “Loading system …”

	mov	ah,#0x03	!读光标的位置
	xor	bh,bh       !bh=页号
	int	0x10

我们主要是用行号(DH中)和列号(DL中)。

INT 10H AH=03H:获取光标位置和形状

参数说明
入口参数
AH=03H,读光标的位置
BH页号
返回参数
CH行扫描开始
CL行扫描结束
DH行号
DL列号

这里的“行扫描开始”和“行扫描结束”如何理解?

其实这两个参数描述了光标的形状。通常一个字符单元有 8 个扫描行(0-7)。所以,CX = 0607H 是一个正常的光标,CX = 0007H 是一个完整块光标。如果设置 CH 的第 5 位,这通常意味着“隐藏光标”,所以 CX = 2607H 是一种无形光标。

                   —— 摘自《维基百科“INT 10H”词条》

页号如何理解?

在屏幕上显示的字符及其属性被保存在显示缓冲区中,可以认为页号是显示缓冲区的编号,可通过五号功能设置显示页(即选择活动的显示页),默认第 0 页是活动的显示页。

INT 10H AH=13H:写字符串

参数说明
入口参数
AH=13H,写字符串
BH页码
BL属性(若 AL=00H 或 01H)
CX要显示的字符串的长度
DH、DL坐标(行、列)
ES:BP指向要显示的字符串
AL显示输出方式
返回参数

AL 表示显示输出方式,取值如下:

取值说明字符串格式
0字符串中只含显示字符,显示属性在BL中;显示后,光标位置不变char1,char2,…,charN
1字符串中只含显示字符,显示属性在BL中;显示后,光标位置跟随字符串改变char1,char2,…,charN
2字符串中含有显示字符和显示属性;显示后,光标位置不变char1,attri1,char2,attri2,…,charN,attriN
3字符串中含有显示字符和显示属性;显示后,光标位置跟随字符串改变char1,attri1,char2,attri2,…,charN,attriN
	mov	cx,#24          ! 24个字符
	mov	bx,#0x0007		! page 0, attribute 7 , 黑底白字
	mov	bp,#msg1
	mov	ax,#0x1301		! write string, move cursor
	int	0x10
msg1:
	.byte 13,10
	.ascii "Loading system ..."
	.byte 13,10,13,10

13是回车,10是换行。它们的区别如下表。

回车和换行

加载 system 到 0x10000

! we want to load the system (at 0x10000)

	mov	ax,#SYSSEG  ! SYSSEG=0x1000
	mov	es,ax		! segment of 0x010000
	call	read_it
	call	kill_motor

3~5行,把 system 模块加载到 0x10000。

第 6 行,关闭驱动器马达。

过程 read_it

这个过程的功能是把还未读取的扇区加载到 es:0x0000 处。注意:es 作为参数,必须是 0x1000 的整数倍,否则会陷入死循环。每读 64KB,都会使 es 的值增加 0x1000(0x1000 左移 4 位,是 0x10000,刚好是 64KB),当 es=0x4000 的时候,停止读取,实际上读取了 192KB,也就是说,system 模块最大是 192 KB.

这段代码有几个难点:

调用了过程 read_track(后文会分析),read_track 调用的是 int 13H(AH = 02H),这是一个低级功能,在一个操作中读取的全部扇区必须在同一条磁道上。 所以就要小心,时刻记住磁道上还有多少扇区没有读

int 13H(AH = 02H),每次调用的时候,读出数据的缓冲区起始地址是 ES:BX,这就需要注意 ES 和 BX 的值,要适时调整。

太过底层,需要考虑磁盘的工作原理,读扇区的顺序是什么,什么时候换磁头,什么时候换磁道。

对于软盘,读取顺序是这样的:

假设软盘有 2 个盘面,每个盘面有 M 个磁道,每个磁道有 N 个扇区,

0面0道1扇区

0面0道2扇区

0面0道N扇区

1面0道1扇区

1面0道2扇区

1面0道N扇区

0面1道1扇区

0面1道2扇区

0面1道N扇区

1面1道1扇区

1面1道2扇区

1面1道N扇区

0面2道1扇区

可以看到,扇区增加最快,扇区增到头了,盘面(或磁头)增,盘面增到头了,磁道增。

这 3 个难点导致这段代码比较复杂,可读性不好。
 

sread:	.word 1+SETUPLEN ! 当前磁道已经读取的扇区数(引导扇区 + setup模块 = 5)
head:	.word 0			 ! current head,当前磁头号
track:	.word 0			 ! current track,当前磁道号

read_it:
	mov ax,es
	test ax,#0x0fff     !使ax与0xfff按位与,测试es是否为0x1000的整数倍
die:	jne die			!结果不为0(说明es不是0x1000的整数倍)则陷入死循环
	xor bx,bx		    ! bx(作为段内偏移地址)清零
rp_read:
	mov ax,es
	cmp ax,#ENDSEG		! 实际上求(ax-ENDSEG),看是否到了末尾
	jb ok1_read         ! jump when below,当CF=1(ax<ENDSEG, 有借位)时跳转
	ret                 ! 当ax>=ENDSEG时返回(我认为不会出现大于的情况)
ok1_read:
	seg cs
	mov ax,sectors      ! 这两句相当于 mov ax, cs:[sectors]; 获得每磁道扇区数
	sub ax,sread        ! ax = ax - sread, 得出本磁道未读扇区数
	mov cx,ax
	shl cx,#9           ! cx乘以512,求出字节数
	add cx,bx           ! 以上3行相当于 cx = ax * 512 + bx
                    	! bx是段内偏移,加上欲读的字节数,看看是否越界(和0x10000比较)
	                    
	jnc ok2_read        ! 若cx < 0x10000(CF=0,没有进位)则跳转到ok2_read,不会越界
	je ok2_read         ! 若cx = 0(ZF=1),说明刚好不越界,则跳转到ok2_read
	xor ax,ax           ! 执行这里说明越界,令 ax = 0x0000
	sub ax,bx           ! 计算bx离边界有多远,用0减去bx,结果在ax中
	shr ax,#9           ! 除以512,得到要读的扇区数,AL 作为参数,传给read_track
ok2_read:               
	call read_track  !调用read_track过程,用AL传参,读取AL个扇区到ES:BX
	                 !read_track 中有出错检测,能返回,说明读取成功(AH=0)
	mov cx,ax        !AL中是返回值,即实际读到的扇区数目,cx是该次操作已经读取的扇区数
	add ax,sread     !ax是当前磁道已经读取的扇区数
	seg cs
	cmp ax,sectors   
	jne ok3_read     !如果当前磁道还有扇区未读,跳转到ok3_read
	mov ax,#1        !说明当前磁道的扇区都已读完
	sub ax,head      !ax = 1 - 磁头号
	jne ok4_read     !不相等说明磁头号为0,跳转到 ok4_read,ax=1,读完0磁头,再读1磁头
	inc track        !说明磁头号为1,ax=0,设置磁头号为0,磁道号增加1
ok4_read:
	mov head,ax  ! 保存当前磁头号
	xor ax,ax    !ax=0, 当前磁道已读扇区数置0
ok3_read:
	mov sread,ax      !更新当前磁道已经读取的扇区数
	shl cx,#9		  !cx是该次操作已经读取的扇区数,乘以512字节
	add bx,cx         !更新偏移地址
	jnc rp_read       !没有进位,则跳转到rp_read,继续读
	mov ax,es         !有进位,说明BX达到了64KB边界
	add ax,#0x1000    
	mov es,ax         !es增加0x1000
	xor bx,bx         !bx = 0
	jmp rp_read       !继续读取

以上汇编代码看起来实在是费劲。为了便于理解,写成C语言伪代码如下:

void read_it(es)//参数是es
{ 
	if((es & 0xFFF) != 0) //es 必须是0x1000的倍数,否则进入死循环
		while(1);  //dead loop
	
	bx = 0;
	while(es < ENDSEG){
		// 1. 看看要读多少个扇区,用ax表示
		// 2. sread:本磁道已经读取的扇区数	
		ax = SECTORS - sread;
		if((ax * 512 + bx) > 0x10000){
			ax = (0x10000 - bx) / 512;
		}
	
		read_track(ax); //调用读扇区过程,al:要读的扇区数,es:bx->缓冲区
		cx = ax; //该次操作读取的扇区数	
		ax += sread; //ax是本磁道已读取的扇区总数
		
		if(ax==SECTORS){
			//本磁道的扇区全部读完
			if(head == 1){ //0和1磁头都已经读完,更新磁头和磁道
				++track;
				head = 0; //从0磁头开始
			}
			else{
				head = 1; //切换到1磁头			
			}		
			ax = 0; //本磁道已读扇区数置为0
		}
		
		sread = ax; //更新本磁道已读扇区数
		bx += cx * 512; 更新偏移地址bx
		
		if(bx == 0x10000)
		{
			//如果偏移地址到达0x10000,则更新es,并使bx=0
			es += 0x1000;
			bx = 0;
		}
	}
	return;
}

过程 read_track

读取 AL 个扇区到 ES:BX。此过程的入口参数是:

AL-要读的扇区数目

ES:BX-缓冲区地址

这个过程可读性不好,因为参数太多,需要耐心理解。

read_track:
	push ax
	push bx
	push cx
	push dx
	mov dx,track   !当前磁道号
	mov cx,sread   !已经读取的扇区数
	inc cx         !CL是起始扇区号
	mov ch,dl      !CH是磁道号
	mov dx,head    !当前磁头号
	mov dh,dl      !DH是磁头号
	mov dl,#0      !DL是驱动器号,0表示软盘
	and dx,#0x0100 !DH是磁头号,磁头号不大于 1
	mov ah,#2      !功能号2,读扇区
	int 0x13
	jc bad_rt      !CF=1,表示出错,复位磁盘
	pop dx
	pop cx
	pop bx
	pop ax
	ret
bad_rt:	mov ax,#0  !AH=0,磁盘复位功能
	mov dx,#0      !DL是驱动器号,0表示软盘
	int 0x13
	pop dx
	pop cx
	pop bx
	pop ax
	jmp read_track  !重新读取

过程 kill_motor

kill_motor:
	push dx
	mov dx,#0x3f2 !软盘控制器的端口-数字输出寄存器端口,只写
	mov al,#0     !驱动器A,关闭FDC,禁止DMA和中断请求,关闭马达
	outb          !将al的值写入端口dx
	pop dx
	ret

DOR(数字输出寄存器)

DOR是一个8位寄存器,他控制驱动器马达的开启、驱动器选择、启动/复位FDC以及允许/禁止DMA及中断请求。

NameDescription
7MOT_EN3Driver D motor:1-start;0-stop
6MOT_EN2Driver C motor:1-start;0-stop
5MOT_EN1Driver B motor:1-start;0-stop
4MOT_EN0Driver A motor:1-start;0-stop
3DMA_INTDMA and IRQs; 1 enable; 0-disable
2RESET0= enter reset mode;1= normal operation
1 and 0DRV_SEL1, DRV_SEL0“Select” drive number(A-D) for next access

至于为什么要调用 kill_motor,我还无法理解,这里存疑,估计后面就懂了。

确认根文件系统设备号

	seg cs
	mov	ax,root_dev     ! ax = ROOT_DEV
	cmp	ax,#0   
	jne	root_defined    ! 如果 ROOT_DEV 不等于 0 则跳转到 root_defined
	seg cs
	mov	bx,sectors       ! 取每磁道扇区数
	mov	ax,#0x0208		 ! /dev/ps0 - 1.2Mb
	cmp	bx,#15           ! 判断每磁道扇区数是否等于15
	je	root_defined     ! 如果等于15,说明是1.2MB的软盘
	mov	ax,#0x021c		 ! /dev/PS0 - 1.44Mb
	cmp	bx,#18           ! 判断每磁道扇区数是否等于18
	je	root_defined     ! 如果等于18,说明是1.44MB的软盘
undef_root:
	jmp undef_root       ! 死循环
root_defined:
	seg cs
	mov	root_dev,ax      ! 将检查过的设备号保存到 root_dev 中

软驱的主设备号是 2,次设备号 = type * 4 + nr.

其中,type 是软驱的类型(比如 2 表示 1.2MB,7 表示 1.44MB)

nr 等于 0~3 时分别对应软驱 A、B、C、D

因为是可引导的驱动器,所以肯定是 A 驱,所以 nr = 0;

前文已经说过,设备号 = (主设备号 << 8) + 次设备号

对于 1.2MB 的软驱,设备号 = 2 << 8 + 2 * 4 + 0 = 0x208;

对于1.44MB 的软驱,设备号 = 2 << 8 + 7 * 4 + 0 = 0x21C;

这就可以解释上面代码第 7 行和第 10 行了。
 

.org 508
; .org 伪指令的格式是 .org new_lc, fill
; 把当前区的位置计数器设置为 new_lc
; 当位置计数器值増长时,所跳跃过的字节将被填入值 fill
; 如果省略了逗号和 fill,则填入 0

root_dev:
	.word ROOT_DEV !存放根文件系统所在设备号(init/main.c中会用)
boot_flag:
	.word 0xAA55

ROOT_DEV 到底有何用,怎么用,这里先存疑,后面再探究。

跳转到 setup 去执行

jmpi	0,SETUPSEG   !到此本程序就结束了。

段间跳转,跳转到0x9020:0x0000(setup.s程序开始处)去执行。

总结

本程序干的工作有:

bootsect 把自己搬运到 0x90000,并跳转
设置栈指针
加载 setup 模块到 0x90200
获得磁盘驱动器参数(主要是每磁道的扇区数)
打印 “Loading system …”
加载 system 到 0x10000
调用 kill_motor
确认根文件系统设备号
跳转到 setup 去执行
代码分析到这里,就差不多明白了。虽然是一个引导扇区,编译后只有512字节,可是涉及的知识点还真不少。真是太佩服Linus了,一个大学生就能写出这样的代码,实属出众。
 

这篇关于bootsect.s 解读——Linux-0.11 剖析笔记(二)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!