本文参考MOOC哈工大操作系统课程与课件
主要基于Linux 0.11系统展开
”Author:Mayiming“
CS:IP
(CS左移4位+IP)CS=0xFFFF,IP=0x0000
,此时指向的地址为0xFFFF0
(ROM BIOS映射区,上电后内存中只有此处有代码)0xFFFF0
此处的BIOS代码检查RAM、键盘、鼠标、磁盘等硬件和IO设备(此处执行不通过则不会执行操作系统,代表硬件有问题)0x7c00
处(从磁盘读取到内存0x7c00
处),磁盘0磁道0扇区(一个扇区512
字节)处的数据即为引导扇区,存放着操作系统的引导程序CS=0x07c0
,IP=0x0000
即指向地址0x7c00
下面解析离开BIOS后,首先执行的bootsect.s
汇编代码
.globl begtext,begdata,begbss,endtext,enddata,endbss
.text //文本段
begtext:
.data //数据段
begdata:
.bss //未初始化数据段
begbss:
entry start //关键字entry告诉链接器“程序入口”
start:
mov ax, #BOOTSEG mov ds, ax // 设置ax=0x07c0,ds=ax
mov ax, #INITSEG mov es, ax // 设置ax=0x9000,es=ax
mov cx, #256 // 设置cx=256 (十进制256)
sub si, si sub di, di // si,di归零
rep movw // rep movw 重复执行移动直到cx归零,即移动256个16位字(也就是512字节) 源地址ds:si,目的地址es:di
jmpi go, INITSEG // jmpi go, INITSET 间接跳转,意思就是跳转到go标签处的代码处
// BOOTSEG = 0x07c0
// INITSEG = 0x9000
// SETUPSEG = 0x9020
上述代码将0x07c00
处的512
字节也就是引导扇区传送到0x90000
处
此时start
下面的这部分代码已经是移动到了0x90000
处了
从上一段代码跳转到go
处:
go: mov ax,cs //cs=0x9000
mov ds,ax mov es,ax mov ss,ax mov sp,#0xff00 // 设置ds=0x9000,es=0x9000,ss=0x9000,sp=0xff00
load_setup: //载入setup模块
mov dx,#0x0000 mov cx,#0x0002 mov bx,#0x0200 // 设置dx=0x0000,cx=0x0002,bx=0x0200,ax=0x0200+setup长度
mov ax,#0x0200+SETUPLEN int 0x13 //BIOS中断
jnc ok_load_setup
mov dx,#0x0000
mov ax,#0x0000 //复位
int 0x13
jmp load_setup //重读
// SETUPLEN=4
上述代码从驱动器0
的柱面0
磁头号0
扇区号2
开始读取SETUPLEN = 4
个扇区,放到内存地址0x90200
后面(也就是引导扇区的512
字节后面)
INT 0x13
中断解释:
ax
寄存器高八位为ah
,低八位为al
,此时ah=0x02
代表读磁盘,al
为setup长度,代表读取的扇区个数
同样cx
分为ch
和cl
,dx
分为dh
和dl
,此时读取的磁盘的位置为柱面号(磁道号)ch = 0
开始扇区为cl = 2
磁头号为dh = 0
驱动器号为dl = 0
,读取到的内存地址为es:bx=0x9000:0x0200=0x90200
(0x90000
到0x90200
之间刚好512
个字节)
INT 0x08 参数参考
载入setup
模块的代码后,执行bootsect.s
最后一段代码
从boot
扇区的代码最后跳转到Ok_load_setup
:
Ok_load_setup: //载入setup模块
mov dl,#0x00 mov ax,#0x0800 //ah=8获得磁盘参数
int 0x13 mov ch,#0x00 mov sectors,cx
mov ah,#0x03 xor bh,bh int 0x10 //读光标
mov cx,#24 mov bx,#0x0007 // cx=24为显示字符长度 bx=7是显示属性
mov bp,#msg1 mov ax,#1301 int 0x10 //显示msg1位置的字符 Loading system...
mov ax,#SYSSEG //SYSSEG=0x1000
mov es,ax
call read_it //读入system模块
jmpi 0,SETUPSEG // 转入0x9020:0x0000 执行setup.s
// bootsect.s中的数据 //在文件末尾
// sectors: .word 0 //磁道扇区数
// msg1:.byte 13,10
// .ascii “Loading system...”
// .byte 13,10,13,10
上述代码获取磁盘参数放到sectors
,读取光标,显示加载系统字符,读入system
模块,最后jmpi
跳转到setup
模块
总结一下bootsect.s代码的功能就是:
0x07c00
处的512
字节也就是引导扇区传送到0x90000
处0
的柱面0
磁头号0
扇区号2
开始读取SETUPLEN = 4
个扇区(setup
模块),放到内存地址0x90200
后面(也就是boot
扇区的512
字节后面)sectors
,读取光标,显示加载系统字符,读入system
模块,最后jmpi
跳转到setup
模块下面单独解析一下读入system
模块的call read_it
:
system
模块可能很大,要跨越磁道
jb
代表jump below
,此时应该是为了防止程序跳到system
外
read_it: mov ax,es cmp ax,#ENDSEG jb ok1_read // ax=es,ax与#ENDSEG比较 ENGSEG代表镜像结束的位置(具体与镜像大小SYSSIZE有关)
ret // 函数返回 return
ok1_read:
mov ax,sectors // 设置ax为磁盘扇区数目
sub ax,sread //sread是当前磁道已读扇区数, ax = ax - sread 等于未读扇区数
call read_track //读磁道...
// ENDSEG=SYSSEG+SYSSIZE
// SYSSIZE=0x8000 //该变量可根据
// Image大小设定(编译操作系统时)
上述这段代码把system
整体读取到内存中
至此bootsect.s
引导扇区的代码执行完毕,此时需要转入setup
模块进行执行。
setup
模块作用是完成OS
启动前的设置部分
start: mov ax,#INITSEG mov ds,ax mov ah,#0x03 // #INITSEG=0x9000
xor bh,bh int 0x10 mov [0],dx //取光标位置dx放入内存[0]=0x90000表示间接寻址,段地址ds:偏移地址[0]
mov ah,#0x88 int 0x15 mov [2],ax ... // 获取扩展内存大小ax, 放入[2]=0x90002位置
cli ///不允许中断
mov ax,#0x0000 cld
do_move: mov es,ax add ax,#0x1000
cmp ax,#0x9000 jz end_move
mov ds,ax sub di,di
sub si,si
mov cx,#0x8000
rep // 将system模块移动到0x00000地址
movsw // 源地址ds:si,目的地址es:di
jmp do_move
// SYSSEG = 0x1000
因为此时CS:IP
最多指向的地址空间为1M
(地址位数为20
位),2^20bit=1M
,所以需要扩展内存。
SYSSET=0x1000
,system
模块的代码在地址0x10000
处起始,上述代码将0x10000-0x90000
的代码平移到0x00000-0x80000
处
此时可以解释为何上面要移动0x07c00
的代码到0x90000
,就是因为这段地址要放system
。
下面CPU
要转入保护模式,因为实模式的内存寻址空间太小了,转入保护模式后CS:IP
寻址方式会改变,可以寻址32
位地址空间(4G
),同时INT
中断的方式也会发生一些变化。
end_move: mov ax,#SETUPSEG mov ds,ax
lidt idt_48 lgdt gdt_48//设置保护模式下的中断和寻址
进入保护模式的命令...
idt_48:.word 0 .word 0,0 //保护模式中断函数表
gdt_48:.word 0x800 .word 512+gdt,0x9
gdt: .word 0,0,0,0
.word 0x07FF, 0x0000, 0x9A00, 0x00C0
.word 0x07FF, 0x0000, 0x9200, 0x00C0
上述代码的功能为写GDT
表,保护模式寻址的过程为从GDT
表中寻找CS
指向的地址,取出表中的地址与IP
组合成32
位地址。
call empty_8042 mov al,#0xD1 out #0x64,al // 0xD1表示写数据到8042的P2端口
//8042是键盘控制器,其输出端口P2用来控制A20地址线
call empty_8042 mov al,#0xDF out #0x60,al
//选通A20地址线 call empty_8042
初始化8259(中断控制) //一段非常机械化的程序
mov ax,#0x0001 mov cr0,ax // cr0寄存器最低位 置1
jmpi 0,8 // IP=0, CS=8, 此时是保护模式了,需要根据CS去查GDT表
// 通过查表该地址指向 0 地址
empty_8042:
.word 0x00eb,0x00eb
in al,#0x64 // 读取到al
test al,#2 // 跳转test
jnz empty_8042
ret
system
模块(目标代码)中的第一部分代码? head.s
为什么head.s
是system
模块的第一段代码,这是由makefile
控制的
linux/Makefile:
disk: Image
dd bs=8192 if=Image of=/dev/PS0 // /dev/PS0是软驱
Image: boot/bootsect boot/setup tools/system tools/build
tools/build boot/bootsect boot/setup tools/system > Image
tools/system: boot/head.o init/main.o $(DRIVERS) …
$(LD) boot/head.o init/main.o $(DRIVERS) … -o tools/system
下面来看head.s
做了什么:
stratup_32: movl $0x10,%eax mov %ax,%ds mov %ax,%es
mov %as,%fs mov %as,%gs //指向gdt的0x10项(数据段)
lss _stack_start,%esp //设置栈(系统栈)
call setup_idt // 设置idt
call setup_gdt // 设置gdt
xorl %eax,%eax
1:incl %eax
movl %eax,0x000000 cmpl %eax,0x100000
je 1b //0地址处和1M地址处相同(A20没开启),就死循环
jmp after_page_tables //页表,什么东东?
setup_idt: lea ignore_int,%edx
movl $0x00080000,%eax movw %dx,%ax
lea _idt,%edi movl %eax,(%edi)
head.s
做了一系列设置(堆栈、idt
、gdt
、设置地址线等)执行后需要执行main.c
,转入C语言执行
after_page_tables:
pushl $0 pushl $0 pushl $0 pushl $L6 // 将0,0,0,L6压入堆栈
pushl $_main jmp set_paging // 将_main压入堆栈 跳转到设置页表
L6: jmp L6
setup_paging: 设置页表 ret //设置页表 然后return
为什么堆栈要压入 0,0,0,L6,_main
这样的顺序,和下图C
执行函数的堆栈结构有关系。
main(0,0,0)
三个参数其实没有用只是为了envp,argv,argc
完整性。
下面main函数中执行了一系列的初始化操作,初始化了内存、时间、硬盘、缓冲区等。
在linux/mm/memory.c中
void mem_init(long start_mem,long end_mem) // start_mem与系统大小有关,end_men参数从3.1中扩展内存获取处得到
{
int i;
for(i=0; i<PAGING_PAGES; i++)
mem_map[i] = USED; // 将内存的页表从0开始的地方设置一段为USED, 这一段即为系统所在的地址
i = MAP_NR(start_mem);
end_mem -= start_mem; // end_men-start_men为剩余的内存的大小
end_mem >>= 12; // end_men >>=12,end_men右移12位,代表除以4k, 此时mem_map每一位代表了一个4k的内存页是否被使用
while(end_mem -- > 0)
mem_map[i++] = 0; } // 将除系统地址外的内存页初始化为未使用
通过main.c
初始化完成后,操作系统即启动了,main()函数是一个永不返回的函数,会一直执行下去。