kernel源码(五)bootsect.s
编译器
由于当时的特殊情况,bootsect.s使用的是XX编译器
汇编格式
bootsect.s使用的是Intel的汇编格式
1 源码
! ! SYS_SIZE is the number of clicks (16 bytes) to be loaded. ! 0x3000 is 0x30000 bytes = 196kB, more than enough for current ! versions of linux ! SYSSIZE = 0x3000 ! ! bootsect.s (C) 1991 Linus Torvalds ! ! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves ! iself out of the way to address 0x90000, and jumps there. ! ! It then loads 'setup' directly after itself (0x90200), and the system ! at 0x10000, using BIOS interrupts. ! ! NOTE! currently system is at most 8*65536 bytes long. This should be no ! problem, even in the future. I want to keep it simple. This 512 kB ! kernel size should be enough, especially as this doesn't contain the ! buffer cache as in minix ! ! The loader has been made as simple as possible, and continuos ! read errors will result in a unbreakable loop. Reboot by hand. It ! loads pretty fast by getting whole sectors at a time whenever possible. .globl begtext, begdata, begbss, endtext, enddata, endbss .text begtext: .data begdata: .bss begbss: .text SETUPLEN = 4 ! nr of setup-sectors BOOTSEG = 0x07c0 ! original address of boot-sector INITSEG = 0x9000 ! we move boot here - out of the way SETUPSEG = 0x9020 ! setup starts here SYSSEG = 0x1000 ! system loaded at 0x10000 (65536). ENDSEG = SYSSEG + SYSSIZE ! where to stop loading ! ROOT_DEV: 0x000 - same type of floppy as boot. ! 0x301 - first partition on first drive etc ROOT_DEV = 0x306 entry start start: mov ax,#BOOTSEG mov ds,ax mov ax,#INITSEG mov es,ax mov cx,#256 sub si,si sub di,di rep movw jmpi go,INITSEG go: mov ax,cs mov ds,ax mov es,ax ! put stack at 0x9ff00. mov ss,ax mov sp,#0xFF00 ! arbitrary value >>512 ! load the setup-sectors directly after the bootblock. ! Note that 'es' is already set up. load_setup: mov dx,#0x0000 ! drive 0, head 0 mov cx,#0x0002 ! sector 2, track 0 mov bx,#0x0200 ! address = 512, in INITSEG mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors int 0x13 ! read it jnc ok_load_setup ! ok - continue mov dx,#0x0000 mov ax,#0x0000 ! reset the diskette int 0x13 j load_setup ok_load_setup: ! Get disk drive parameters, specifically nr of sectors/track mov dl,#0x00 mov ax,#0x0800 ! AH=8 is get drive parameters int 0x13 mov ch,#0x00 seg cs mov sectors,cx mov ax,#INITSEG mov es,ax ! Print some inane message mov ah,#0x03 ! read cursor pos xor bh,bh int 0x10 mov cx,#24 mov bx,#0x0007 ! page 0, attribute 7 (normal) mov bp,#msg1 mov ax,#0x1301 ! write string, move cursor int 0x10 ! ok, we've written the message, now ! we want to load the system (at 0x10000) mov ax,#SYSSEG mov es,ax ! segment of 0x010000 call read_it call kill_motor ! After that we check which root-device to use. If the device is ! defined (!= 0), nothing is done and the given device is used. ! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending ! on the number of sectors that the BIOS reports currently. seg cs mov ax,root_dev cmp ax,#0 jne root_defined seg cs mov bx,sectors mov ax,#0x0208 ! /dev/ps0 - 1.2Mb cmp bx,#15 je root_defined mov ax,#0x021c ! /dev/PS0 - 1.44Mb cmp bx,#18 je root_defined undef_root: jmp undef_root root_defined: seg cs mov root_dev,ax ! after that (everyting loaded), we jump to ! the setup-routine loaded directly after ! the bootblock: jmpi 0,SETUPSEG ! This routine loads the system at address 0x10000, making sure ! no 64kB boundaries are crossed. We try to load it as fast as ! possible, loading whole tracks whenever we can. ! ! in: es - starting address segment (normally 0x1000) ! sread: .word 1+SETUPLEN ! sectors read of current track head: .word 0 ! current head track: .word 0 ! current track read_it: mov ax,es test ax,#0x0fff die: jne die ! es must be at 64kB boundary xor bx,bx ! bx is starting address within segment rp_read: mov ax,es cmp ax,#ENDSEG ! have we loaded all yet? jb ok1_read ret ok1_read: seg cs mov ax,sectors sub ax,sread mov cx,ax shl cx,#9 add cx,bx jnc ok2_read je ok2_read xor ax,ax sub ax,bx shr ax,#9 ok2_read: call read_track mov cx,ax add ax,sread seg cs cmp ax,sectors jne ok3_read mov ax,#1 sub ax,head jne ok4_read inc track ok4_read: mov head,ax xor ax,ax ok3_read: mov sread,ax shl cx,#9 add bx,cx jnc rp_read mov ax,es add ax,#0x1000 mov es,ax xor bx,bx jmp rp_read read_track: push ax push bx push cx push dx mov dx,track mov cx,sread inc cx mov ch,dl mov dx,head mov dh,dl mov dl,#0 and dx,#0x0100 mov ah,#2 int 0x13 jc bad_rt pop dx pop cx pop bx pop ax ret bad_rt: mov ax,#0 mov dx,#0 int 0x13 pop dx pop cx pop bx pop ax jmp read_track /* * This procedure turns off the floppy drive motor, so * that we enter the kernel in a known state, and * don't have to worry about it later. */ kill_motor: push dx mov dx,#0x3f2 mov al,#0 outb pop dx ret sectors: .word 0 msg1: .byte 13,10 .ascii "Loading system ..." .byte 13,10,13,10 .org 508 root_dev: .word ROOT_DEV boot_flag: .word 0xAA55 .text endtext: .data enddata: .bss endbss:
SYSSIZE = 0x3000 表示的是system模块编译后占用的大小。单位是节,1节是16字节,即0x30000字节(约196KB)
下面定义了一些.开头的指令,称之为伪指令,是给编译器用的。.globl,表示这些标号链接时可见。然后定义了代码段、数据段和bss段,比如“.text:”表示代码段的地址。因为伪指令和注释以及类似于SYSSIZE=0x3000编译后不占用内存地址空间,所以标号begtext、begdata、begbss是重合的,也就是代码段、数据段、bss段他们是重合的,说明其是不分段的。我们可以这样理解:比如bootsect.S编译完之后放到了磁盘的第一个扇区,其中的这些伪指令不存在了,当linux启动时bootsect被加载到内存的0x7c0,当指令执行时此时的代码段不存在伪指令。
因为bootsect.s编译后是放到第一个扇区里面的,bios将其加载到实模式内存0x7c00处,也就是说,代码段、数据段、bss段都是在0x7c00作为起始地址。
.globl begtext, begdata, begbss, endtext, enddata, endbss .text begtext: .data begdata: .bss begbss: .text
SETUPLEN = 4 ! setup程序的扇区数 BOOTSEG = 0x07c0 ! boot-sector的段地址,实模式下,左移4位为0x07c00 INITSEG = 0x9000 ! 我们把bootsect移动到这里 SETUPSEG = 0x9020 ! setup在这里 SYSSEG = 0x1000 ! system模块放到这个位置 ENDSEG = SYSSEG + SYSSIZE ! 停止加载的地址
设备号
! ROOT_DEV: 0x000 - same type of floppy as boot. ! 0x301 - 表示第一个磁盘的第一个分区 ROOT_DEV = 0x306 第2个磁盘的第一个分区。为什么?稍微有点复杂,如果想了解请参考教材
entry start,entry为伪指令,告诉链接程序,程序的入口标号为start。
cx寄存器为计数寄存器,其用处:1、可做通用寄存器,2、数据寄存器(CH,CL),3、用在循环指令中,每次循环自动减1
sub减操作,sub aaa bbb,作用是aaa-bbb,然后放到aaa中
rep循环指令,功能是根据cx的值循环执行cx下面的指令,直到cx减到0
ds:si和es:di表示相关寄存器所引用的segment:offset。这里ds:si = 0x07c0:0x0000 = 0x7c00,es:di = 0x9000:0x0000 = 0x90000
不带参数的movw指令指的是,把ds:si移动到es:di位置
这样循环256次,就实现了把0x7c00位置开始接下来的256字节移动到了0x90000位置。
接着下面的跳转指令 jmpi go,INITSEG ,跳转到段的位置为INITSEG,偏移值为go标号的位置。
entry start start: mov ax,#BOOTSEG //把0x7c00放到ax中 mov ds,ax //把0x7c00放到数据段寄存器ds中,也就是数据段现在是0x7c00,(参考https://www.cnblogs.com/zhenjingcool/p/15929907.html)我们知道段寄存器存放段选择符,进而可以定位数据段的位置 mov ax,#INITSEG //ax设置成0x90000 mov es,ax //把0x90000移动到附加段寄存器es中 mov cx,#256 //cx存放立即数256 sub si,si //sub为减法运算符,si为源变址寄存器。si-si然后放入si中,这句的意思就是si=0, sub di,di //di为目的变址寄存器 rep //表示循环 movw jmpi go,INITSEG //上面完成移动之后,跳转到0x90000的go标签处执行
上一条指令,跳转到0x90000位置的go标签对应的偏移量处执行,同时,代码段寄存器cs的值默认将修改为0x90000。
我们再看一下下面的代码,
go: mov ax,cs //代码段寄存器的值赋给ax,因为上一步已经jump到0x90000的go标签处,因此此时代码段寄存器存储的是0x90000.此处把0x90000赋值给ax mov ds,ax //代码段寄存器的值赋给数据段寄存器,即0x90000 mov es,ax //代码段寄存器的值赋给附加段寄存器,即0x90000
下面的代码,堆栈段ss也赋值0x90000,堆栈指针sp赋值0xFF000
! put stack at 0x9ff00. mov ss,ax mov sp,#0xFF00 ! arbitrary value >>512
下面代码是加载setup模块到0x90200位置。
因为bios程序还驻留在内存中(参考:https://www.cnblogs.com/zhenjingcool/p/15938330.html),我们可以借助bios中断来实现访问硬盘。其中在内存0x13处就是bios的某个中断向量用于操作硬盘。bios中断的参数是通过设置ax、bx、cx、dx、es这些寄存器来实现的,具体可参考bios手册。
load_setup: mov dx,#0x0000 ! dl表示驱动器号 0, dh表示磁头号 0 mov cx,#0x0002 ! cl低6位为开始扇区,也就是从2号扇区开始读, ch+cl的高2位为磁道号,这里为0号磁道 mov bx,#0x0200 ! 指向数据缓冲区,这里是es段的bx 这里既是:0x9000:0x200,也就是0x90200.
mov ax,#0x0200+SETUPLEN ! ah为0x02,表示service 2 读磁盘扇区到内存, al为4,表示读取4个扇区(setup模块占4个扇区) int 0x13 ! BIOS中断,查找中断向量表0x13表示读取磁盘到内存 jnc ok_load_setup ! 如果CF(标志寄存器的CF位,表示进位标志)没有进位,则说明int 0x13执行成功。jnc为转移指令,表示如果CF没有进位则跳转 mov dx,#0x0000 mov ax,#0x0000 ! reset the diskette int 0x13 j load_setup
mov dx,#0x0000 ! dl表示驱动器号 0, dh表示磁头号 0
mov cx,#0x0002 ! cl低6位为开始扇区,也就是从2号扇区开始读, ch+cl的高2位为磁道号,这里为0号磁道
mov bx,#0x0200 ! 指向数据缓冲区,这里是es段的bx 这里既是:0x9000:0x200,也就是0x90200.
mov ax,#0x0200+SETUPLEN ! ah为0x02,表示service 2 读磁盘扇区到内存, al为4,表示读取4个扇区(setup模块占4个扇区)
int 0x13 ! BIOS中断,0x13表示读取磁盘到内存
上面这些步骤表示:从磁盘驱动器0的0号磁道的第2个扇区开始读取连续的4个扇区到0x90200位置。
jnc ok_load_setup ! 如果CF(标志寄存器的CF位,表示进位标志)没有进位,则说明int 0x13执行成功。jnc为转移指令,表示如果CF没有进位则跳转
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
如果CF有进位,则不跳转,继续执行下面指令。 mov dx,#0x0000 和 mov ax,#0x0000 、 int 0x13 表示复位软驱,如果我们使用镜像启动,这里其实是没有意义的。一般情况下CF不会有进位,此处不会执行,万一执行到此处, j load_setup 又会跳转到load_setup执行,将会出现死循环,电脑死机。
下面我们看看ok_load_setup
这段代码作用是读取软盘参数,比如磁道数和扇区数
ok_load_setup: ! Get disk drive parameters, specifically nr of sectors/track mov dl,#0x00 mov ax,#0x0800 ! AH=8 is get drive parameters int 0x13 mov ch,#0x00 seg cs mov sectors,cx mov ax,#INITSEG mov es,ax
还是使用BIOS的int 0x13中断,查看中断向量表(参考https://blog.csdn.net/weixin_37656939/article/details/79684611),我们知道,ah=0x08表示取得驱动器参数,dl表示哪个驱动器,0x00 ~ 0x7F:软盘;0x80~0xFF:硬盘,这里读的是软盘。(因为linus开发linux内核时使用的软盘来做的)
设置好ah和dl,调用中断
int 0x13
如果中断执行成功,则
CF=0 操作成功 BL =驱动器类型 CH=柱面数的低8位 CL =扇区数(位 0-5 ),柱面数的高2位(位6-7) DH=磁头数 DL=驱动器数 ES:DI=磁盘驱动器参数表
如果执行失败。则
CF=1 操作失败 AH=状态代码
这句指令 mov ch,#0x00 把ch置位0x00,因为这里读取的是软盘,1.44M的软盘柱面数是80,所以cl的高两位肯定是0,因为ch已经足够保存柱面数,所以cl的值就是每个柱面的扇区数。
seg cs
mov sectors,cx
这里seg表示接下来的一行代码只针对cs段
mov sectors,cx 表示把cs段偏移量cx处的值放入sectors中。这里是磁道0的总扇区数。
mov ax,#INITSEG 把0x9000放入ax
mov es,ax ,因为执行int 0x13获取软盘参数时es发生了变化,这里再设置回来,设置为原来的0x9000.
这一段代码,获取软盘的扇区数,好像没什么用处。
下面代码作用是显示信息到屏幕上,显示的内容为 #msg1 ,即[CRLF]Loading system ...[CRLF][CRLF]
msg1: .byte 13,10 .ascii "Loading system ..." .byte 13,10,13,10
! Print some inane message mov ah,#0x03 ! read cursor pos xor bh,bh int 0x10 mov cx,#24 mov bx,#0x0007 ! page 0, attribute 7 (normal) mov bp,#msg1 mov ax,#0x1301 ! write string, move cursor int 0x10
首先获取光标位置,调用BIOS中断 int 0x10,查找中断向量表可知,这个中断是显示服务。参数由ah决定,这里ah传递的是0x03,表示获取光标的位置。
执行
int 0x10
执行完该指令后,返回光标位置存储在DX中(DH存储行,DL存储列)
mov cx,#24 mov bx,#0x0007 ! page 0, attribute 7 (normal) mov bp,#msg1 mov ax,#0x1301 ! write string, move cursor int 0x10
这些指令实现写字符串,移动光标到指定位置。同样是调用BIOS中断 0x10实现的。
接下来,加载system模块到内存的0x10000位置。
! ok, we've written the message, now ! we want to load the system (at 0x10000) mov ax,#SYSSEG mov es,ax ! 附加段寄存器es赋值0x10000, call read_it !加载 call kill_motor !关闭软驱马达
接下来,判断root_dev是否为0,如果是,则跳转到root_defined;如果不是0,判断扇区数是否为15,如果扇区数是15,则为1.2Mb软盘,跳转到root_defined;如果扇区数不是15则继续判断扇区数是否为18,如果是,则为1.44Mb软盘,跳转到root_defined;如果不是则执行undef_root,死循环,表示没有可用的软盘。
如果有软盘,则会执行标号root_defined,这里面设置root_defined,以存储当前使用的软盘类型。
! After that we check which root-device to use. If the device is ! defined (!= 0), nothing is done and the given device is used. ! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending ! on the number of sectors that the BIOS reports currently. seg cs mov ax,root_dev cmp ax,#0 jne root_defined seg cs mov bx,sectors //sectors为上面读取驱动器参数时设定的,该值表示的是当前磁道上的扇区数,并不是整个软盘的扇区数。 mov ax,#0x0208 ! /dev/ps0 - 1.2Mb cmp bx,#15 je root_defined mov ax,#0x021c ! /dev/PS0 - 1.44Mb cmp bx,#18 je root_defined undef_root: jmp undef_root root_defined: seg cs mov root_dev,ax
接下来是一条跳转指令,跳转到0x90200位置。
! after that (everyting loaded), we jump to ! the setup-routine loaded directly after ! the bootblock: jmpi 0,SETUPSEG
我们知道,0x90200处是我们的setup.s
上面加载system模块的代码,我们这里再深入了解一下
read_it这个标签进行system模块的加载。
sread: .word 1+SETUPLEN ! 当前磁道的已读扇区 head: .word 0 ! 当前磁头 track: .word 0 ! 当前磁道 read_it: mov ax,es test ax,#0x0fff die: jne die ! es must be at 64kB boundary xor bx,bx ! bx is starting address within segment
附加段寄存器es指向0x10000,这里首先把0x10000赋给ax,然后 test ax,#0x0fff 是与操作,这里与操作结果是1,。 die: jne die 如果test操作结果es中不是存储的0x10000,说明es被篡改,因此执行 die: jne die ,死循环。
接下来,我们看一下rp_read,这个标签作用是判断是否已经读取全部数据了。
rp_read: mov ax,es cmp ax,#ENDSEG ! have we loaded all yet? jb ok1_read ret
这里比较附加段寄存器es是否为0x10200,也就是说判断setup.s是否都加载完了,如果es不是0x10200执行ok1_read,否则ret
ok1_read
计算并验证当前磁道需要读取的扇区数量,放入ax当中。
ok1_read: seg cs mov ax,sectors //sectors存储的是磁道的总扇区数 sub ax,sread //sread是已读取的扇区数,这里计算还需要读取的扇区数放到ax中 mov cx,ax //cx存放还需要读取的扇区数 shl cx,#9 //cx左移9位,表示cx乘以512(因为一个扇区512字节,所以这里计算还需要读取的字节数) add cx,bx //bx是基址寄存器,这里计算cx=cx+bx,如果cx没有溢出,也就是flag寄存器的进位标志位CF为0表示,没有超出要读取的最大范围64KB(因为cx16位,2^16=64KB) jnc ok2_read //如果CF没有进位,则跳转到ok2_read je ok2_read //这一句还不明白 xor ax,ax //如果CF有进位,则ax通过异或方式清零 sub ax,bx //ax=0-bx,相当于取bx反码 shr ax,#9 //ax右移9位,相当于ax除以512,计算结果表示把ax转换成扇区数(因为一个扇区512字节)
ok2_read:
ok2_read: call read_track mov cx,ax add ax,sread seg cs cmp ax,sectors jne ok3_read mov ax,#1 sub ax,head jne ok4_read inc track
read_track:
read_track: push ax //ax、bx、cx、dx入栈 push bx push cx push dx mov dx,track //dx存储当前磁道号0 mov cx,sread //cx存储已经读取的扇区数 inc cx //下一个扇区 mov ch,dl //ch放入当前磁道号0 mov dx,head //dx放入当前磁头0 mov dh,dl // dh放入当前磁头号 mov dl,#0 //dl放入0 and dx,#0x0100 //限定dx要么是0x0000,要么0x0100,也就是磁头号不大于1 mov ah,#2 //ah放入2,配合int 0x13 int 0x13 //查看中断向量表,可知ah=2表示读扇区, jc bad_rt pop dx pop cx pop bx pop ax ret
int 0x13(BIOS中断参数大全:https://blog.csdn.net/weixin_37656939/article/details/79684611):
ah=0x02表示读取扇区
上面设置了ch(磁道),cl(扇区),dh(磁头),dl(驱动器),根据这些设置读取驱动器0上的0号磁道的某一个扇区。
如果读取出错,则跳转到bad_rt。如果没有出错,则弹出dx,cx,bx,ax。返回到ok2_read处继续执行。
al放入传输的扇区数。
ok2_read: call read_track mov cx,ax //int 0x13调用成功后,把传输的扇区数放入al中;这里cx放入读取的扇区数 add ax,sread //ax放入已经读取的扇区数 seg cs cmp ax,sectors //比较已经读取的扇区数=总扇区数 jne ok3_read //如果不相等,说明还没有读完,跳转到ok3-read mov ax,#1 //如果相等,则ax放入1 sub ax,head //ax=1-head jne ok4_read //如果当前磁头head不是1磁头,则跳转到ok4_read inc track //当前磁道号加1
ok3_read:
ok3_read: mov sread,ax //sread放入已读取的扇区数 shl cx,#9 //cx中存放的是上次读取硬盘时传输的扇区数,这里cx左移9位(相当于乘以512,磁盘扇区大小为512)也就是cx存入了传输的字节数. add bx,cx //bx=bx+cx jnc rp_read //如果CF没有置位,说明还没读够64kb(因为bx为16位,2^16=64KB),跳转到rp_read处,rp_read在前面介绍过,这里就是循环调用这个标签,直到读取到64KB数据为止 mov ax,es add ax,#0x1000 //以上这两行的目的是将段的基地址调整到指向下一个64KB内存的位置。 mov es,ax xor bx,bx jmp rp_read
当读完64KB数据,回调rp_read调用ok1_read的位置
rp_read: mov ax,es cmp ax,#ENDSEG ! have we loaded all yet? jb ok1_read ret
jb ok1_read下一条是ret,返回到
! ok, we've written the message, now ! we want to load the system (at 0x10000) mov ax,#SYSSEG mov es,ax ! segment of 0x010000 call read_it call kill_motor
下面执行 call kill_motor 关闭软驱马达
/* * This procedure turns off the floppy drive motor, so * that we enter the kernel in a known state, and * don't have to worry about it later. */ kill_motor: push dx mov dx,#0x3f2 mov al,#0 outb pop dx ret