操作系统启动
什么是操作系统
操作系统是计算机硬件和应用之间的一层软件 。
操作系统管理哪些硬件 |
---|
CPU管理 |
内存管理 |
终端管理 |
磁盘管理 |
文件管理 |
计算机工作的原理是取指执行,而指对应的就是各种指令。
开机时执行了什么?
计算模型,我们要关注指针IP及其指向的内容,也就是所谓的取指执行。
- 计算机刚开电源时,IP=多少呢?
- 由硬件设计者决定
以x86架构的pc为例
-
x86PC刚开机时CPU进入实模式。
-
开机时,CS(段地址)=0xFFFF;IP(偏移地址)=0x0000。这是设计时就决定好了
-
寻址物理地址0xFFFF0(ROM BIOS映射区)这是刚开机唯一有代码的地方。由BIOS读取bootsect.s。
也就是运行图解中1号的bootsect.s程序了。
段 地 址 ∗ 16 + 偏 移 地 址 = 物 理 地 址 段地址*16+偏移地址=物理地址 段地址∗16+偏移地址=物理地址
( C S < < 4 ) + I P = 物 理 地 址 (CS << 4) + IP = 物理地址 (CS<<4)+IP=物理地址
( 0 x F F F F < < 4 ) + 0 = ( 0 x F F F F 0 ) + 0 = 0 x F F F F 0 (0xFFFF << 4) + 0 = (0xFFFF0) + 0 = 0xFFFF0 (0xFFFF<<4)+0=(0xFFFF0)+0=0xFFFF0
-
检查RAM,键盘,显示器,软硬磁盘。
-
将磁盘0磁道0扇区(引导扇区512字节)读入0x7c00处。
-
设置cs=0x07c0,ip=0x0000
运行图解
boot扇区
该扇区在硬盘的0柱面、0磁头、1扇区,该记录占用512个字节,它用于硬盘启动时将系统控制权转给用户指定的、在分区表中登记了某个操作系统分区。
bootsect.s
bootsect.s是操作系统代码中的操作系统引导程序代码,是通过第一个扇区读入地址0x07C00的内存里。
.globl begtext,begdata,begbss,endtext,enddata,endbss
.text ;文本段
begtext:
.data ;数据段
begdata:
.bss ;未初始化数据段
begbss:
entry start ;关键字entry告诉链接器“程序入口”
#BOOTSEG=0×07C0
#INITSEG=0×9000
#SETUPLEN=4
start:
mov ax,#BOOTSEG ; 将ds段寄存器置为BOOTSEG=0×07C0;
mov ds,ax
mov ax,#INITSEG ;将es段寄存器置为INITSEG=0×9000;
mov es,ax
mov cx,#100 ; 移动计数值0x100=256;
sub si,si ; 源地址 ds:si = 0×07C0:0×0000
sub di,di ; 目的地址 es:di = 0×9000:0×0000
rep movw ; 重复执行移动1个字,直到cx = 0 移动256个word=移动512个byte
jmpi go,#INITSEG ; 间接跳转。这里cs=INITSEG,IP=go跳转到INITSEG:go。
go: mov ax,cs ; 将ds、es 和ss 都置成移动后代码所在的段处(cs=0×9000)。为call做准备
mov ds,ax
mov es,ax
mov ss,ax
mov sp,#0xff00
load_setup: //载入setup模块
mov dx,#0x0000
mov cx,#0x0002
mov bx,#0x0200
mov ax,#0x0200+SETUPLEN ; ah=0x02功能号,al=读取几个扇区(SETUPLEN=4)
int 0x13 ; BIOS中断
jnc ok_load_setup
mov dx,#0x0000
mov ax,#0x0000 ;复位
int 0x13
j load_setup ;重读
一一进行讲解
start模块
这个模块通过最开始的图可以清晰的看出是把bootsect.s移动到0x90000
rep movw
表示移动字,移动的个数是cx=256,也就是512个字节,也就是说这段代码的作用就是将从0x07c00地址处开始的512个字节移动到0x90000处。
在下面这条指令执行之前,对应图中的1号。而当下面的指令开始到load_setup模块结束就对应运行图解中的3号了。
jmpi go,#INITSEG
这条指令是间接跳转,go->ip,INITSEG->CS,前面已经说了INITSEG就是0x9000,go是一个逻辑上的概念,go表示的是距离start模块的地址偏移,现在因为已经将0x07c00处的512个字节移动到了0x90000处,所以逻辑上的start模块已经到了0x90000,go相较于start的偏移其实也就是相较于INITSEG的偏移,所以也就是顺序执行。
跳转之后就是各种对寄存器,段寄存器初始化,这一切都是为了下面的int 0x13中断功能做准备,或者说为了从硬盘上将setup.s读入内存
0x13是BIOS读磁盘扇区的中断:
下图摘自王爽《汇编语言》
ax | ah | al |
---|---|---|
0x0200+SETUPLEN | 0x02 | 0x04 |
int 0x13中断功能结束后,setup模块就完整地从硬盘中读入到内存了。对应运行图解中的3号
在读入setup模块后,来到ok_load_setup模块。
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
mov bp,#msg1
mov ax,#1301
int 0x10 //显示字符
mov ax,#SYSSEG ; SYSSEG=0x1000
mov es,ax
call read_it ; 读入system模块
jmpi 0,SETUPSEG ; SETUPSEG=0×9020
这里又出现了一个int 0x13来这里查查对应的功能号吧BIOS int 13H中断介绍,
mov dl,#0x00
mov ax,#0x0800 //ah=8获得磁盘/驱动器参数
int 0x13
下面又有2个int 0x10中断功能,这个又是什么呢?INT 10H功能INT 10H功能
mov ch,#0x00
mov sectors,cx
mov ah,#0x03
xor bh,bh
int 0x10 ; 读光标
mov cx,#24
mov bx,#0x0007
mov bp,#msg1
mov ax,#1301
int 0x10 ;显示字符
这里是bootsect.s中的数据
;bootsect.s中的数据
sectors: .word 0 ; 磁道扇区数
msg1:.byte 13,10
.ascii “Loading system...”
.byte 13,10,13,10
第一个int 10就是在读光标的位置,第二个int 10是将msg1=“Loading system…”输出到光标所在处。下面的call是读入system模块。读入system模块后
jmpi 0,SETUPSEG ; SETUPSEG=0×9020
指针跳回0x90200也就是setup.s的起始位置,这里开始bootsect.s的任务就结束了,下面的事情就交给setup.s了。
setup.s
setup.s是完成系统启动前设置的,它将硬件的参数存放在0x90000处,然后将system部分移动到从地址0开始的位置,对应的就是运行图解的5号了;临时建立gdt、idt表,并且从实模式进入到了保护模式(16位到32位)。
start:
mov ax,#INITSEG
mov ds,ax
mov ah,#0x03
xor bh,bh
int 0x10 ; 取光标位置dx mov [0],dx
mov ah,#0x88
int 0x15
mov [2],ax ...
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模块移到0地址
movsw
jmp do_move
start模块
操作系统是管理各种硬件的,要管理这些硬件必要首先要知道这些硬件到底是什么东西,是什么型号的,应该用怎样的数据结构来管理。对应运行图解的4号。
上面这段代码的作用就是获取硬件参数,然后将这些信息放在0x90000开始的地方;最后将操作系统从0x90000开始处移到地址0开始处,这就是为什么一开始要移动bootsect.s的原因,因为操作系统可能会覆盖0x07c00这个地址。
地址 | 长度 | 名称 |
---|---|---|
0x90000 | 2 | 光标位置 |
0x90002 | 2 | 扩展内存数 |
0x9000C | 2 | 显卡参数 |
0x901FC | 2 | 根设备号 |
do_move模块
将system模块移动到0x00000后,就是运行图解的5号了。需要特别注意的地方是最后的几行汇编
end_move:
mov ax,#SETUPSEG
mov ds,ax
lidt idt_48
lgdt gdt_48 ;设置保护模式下的中断和寻址 进入保护模式的命令...
进入保护模式代码
;8042是键盘控制器,其输出端口P2用来控制A20地址线
call empty_8042
mov al,#0xD1
out #0x64,al
call empty_8042
mov al,#0xDF
out #0x60,al
;选通A20地址线
call empty_8042
;初始化8259(中断控制) 一段非常机械化的程序
mov ax,#0x0001
mov cr0,ax
jmpi 0,8
不得不说一下cr0寄存器。
mov ax,#0x0001
mov cr0,ax
可以看到cr0是一个32位寄存器,ax为1,将ax赋值给cr0,也就是第0位/PE位为1,启动保护模式/32位模式。寻址模式发生改变。
jmpi 0,8
前面提到过jmpi这条指令,意思是将前操作数赋给ip,后操作数赋给cs,然后跳转到cs、ip所表示的位置执行。
但是这里能不能这样解释呢?如果是那么就是跳到0x00080处执行,对吗?
刚刚执行完setup.s后面应该是继续执行system模块才是,system模块都被移动到0地址处了,也就是说现在system模块最开始位置是0地址处,即应该执行0x00000处代码而不是0x00080处,如果直接执行0x00080,结果肯定是死机。
所以说现在jmpi这条指令肯定不能这么解释了。这条指令应该被解释成查GDT表。
保护模式的寻址方式
上面也提到了进入保护模式后,寻址模式发生了改变,所以下面讲一讲保护模式中该如何寻址。保护模式的另一种说法就是32位模式,换句话说保护模式的寻址方式是为了能够对32位的地址进行寻址。最多寻址4G,这里我总会想到之前逆向学习的时候听到的一句话,32位就是4GB。
lidt idt_48 ! load idt with 0,0
lgdt gdt_48 ! load gdt with whatever appropriate
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
以前CS里面放的是地址,现在CS里面放的是表的下标(称为”选择子”),这个表就是gdt(全局描述表)表,那么这个表是哪里来的呢?在setup.s的时候建立的。
把目光放到GDT表,每一行都是一个表项,每一个表项由4个word组成。但是在保护模式下jmpi 0,8中的8指的是GDT表第8个byte,或者说是第4个word。也就是第二行的表项。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QWK4u03x-1602170907757)(https://i.loli.net/2020/10/08/ywUDFgG2zNfEYlZ.jpg)]
63-48(16-31) | 32-47(0-15) |
---|---|
0x00C0 | 0x9A00 |
0x0000 | 0x07FF |
BASE指的是包含段的首字节的线性地址。
————《深入理解Linux内核》P43
我们将BASE对应的几个部分拿出来看看
BASE(0-15) | BASE(16-23) | BASE(24-31) |
---|---|---|
0000 | 00 | 00 |
刚好是一个32位的地址,众所周知32位的地址就是32位的,到这里应该就豁然开朗了。因为在16位中无法直接寻址32位地址,所以通过GDT表这样的方式来拼接出一个32位地址来寻址。
简单来说,jmpi指令应该被解释成查GDT表,而保护模式的寻址方式就是通过GDT表来寻址。
将地址指向0x00000意味着setup.s的任务也完成了。接下来的事情就交给位于0x00000的代码了。
head.s
head.s就是位于0x00000的代码了,同时也是system模块的开始。
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表(call setup_idt、call_setup_gdt),前面setup里面设置的gdt和idt都是临时的;这里会重新设置。
还会开启A20地址线(je 1b),开启A20地址线之后寻址范围就是4G而不再是1M。
IDT表是中断函数表,从此int n 不再是DOS中断了,而是在IDT表中找到中断函数的地址执行,
注意是:硬件查表,不是软件,idt、gdt表的查表方法都是硬件规定好的,目的就是为了加快速度。
注意,在head.s使用的汇编又和前面bootsect、setup里面使用的汇编不一样,在head.s里面使用的是产生32位代码汇编,而bootsect、setup里面使用的是产生16位代码的汇编。另外在操作系统的.c文件里面还使用了一种汇编,叫做“内嵌汇编”。
after_page_tables:
pushl $0
pushl $0
pushl $0
pushl $L6
pushl $_main
jmp setup_paging
L6: jmp L6
setup_paging: ; 设置页表 ret
前面开启20号地址线之后就jmp到after_page_tables这个标号,在setup_paging执行完后,ret到哪里呢?
到main()函数。在after_page_tables里面将main函数三个参数、L6、main函数的入口地址都压入栈中,在setup_paging的ret直接跳**_main**,如果main函数再返回的话就跳到L6处,从上面可以看到
L6: jmp L6
这是一个死循环,也就是说如果操作系统执行了这条指令,那么就会死机。main函数是不会返回的。
其实从head.s到main.c的过程和c语言里面的函数调用是一样的,首先将函数执行完之后的下一个地址和函数参数压入栈中,然后通过jmp命令跳到子函数的执行处,执行完了之后再利用ret跳到程序原来执行的地方。
main.c
main函数完成了各种硬件数据结构的初始化。永远不会退出,如果退出就死机了。
void main(void)
{
mem_init();
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_init();
sched_init();
buffer_init();
hd_init();
floppy_init();
sti();
move_to_user_mode();
if(!fork()){init();} // 这行永远不会退出
}
前面说了main函数由三个参数,为什么这里没写出来呢?
main函数的三个参数为envp、argc、argv,但是此处并没有使用,所以此处的main只保留传统main形式。
从main函数内容可以看到,main函数的工作就是init:内存、中断、设备、时钟、CPU等内容的初始化。
men_init
这里介绍内存的初始化函数
void mem_init(long start_mem,long end_mem)
{
int i;
for(i=0; i<PAGING_PAGES; i++)
mem_map[i] = USED;
i = MAP_NR(start_mem);
end_mem -= start_mem;
end_mem >>= 12;
while(end_mem -- > 0)
mem_map[i++] = 0;
}
其实这个函数就是初始化mem_map这个数组,start_men、end_men这些参数都是在setup的时候就获取到的。可以回头翻看setup.s的start模块存储了哪些硬件参数。
操作系统干了什么
其实bootsect.s 、setup.s 、heads.s 、main.c这些文件就做了两件事:
- 读入操作系统并移动到合适的位置.
- 初始化(为每一个硬件建立数据结构、并初始化)