L3 操作系统启动
[1] setup 模块
[1.1] setup.s
此部分的代码用来完成 OS 启动前的设置。
start: mov ax,#INITSEG mov ds,ax mov ah,#0x03
xor bh,bh int 0x10 // 取光标位置dx
mov ah,#0x88 int0x15 mov [2],ax //cs:[2]
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
movsw
jmp do_move
此时的内存分布:
内存地址 | 长度 | 名称 |
---|---|---|
0x90000 | 2 | 光标位置 |
0x90002 | 2 | 扩展内存数 |
0x9000C | 2 | 显卡参数 |
0x901FC | 2 | 根设备号 |
前面 boot 空出来的位置就是为了这里留给操作系统,操作系统占据从 0 地址开始的位置。
[1.2] 进入保护模式
call empty_8042 mov al,#0xD1 out #0x64,al
call empty_8042 mov al,#0xDF out #0x60,al
mov ax,#0x0001 mov cr0,ax
jmpi 0,8 //关键作用!
若在实模式下,ip = 0, cs = 8,cs << 4 + ip = 0x80,在 system 模块中,显然不合法。
因此实际上,这里的 寻址方式 发生了改变,也就是说 CPU 的地址解释程序发生了变化。
20 位地址最多只能代表 1M 内存,因此不能使用传统的左移 4 位加 ip,而应该改成 32 位机器模式,能表达 4G内存,即 保护模式。
cr0 寄存器就用来实现这种变换, PE=1 代表保护模式。
[1.3] 将 setup 移动到 0 地址处
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 //0字节
.word 0x07FF, 0x0000, 0x9A00, 0x00C0 // 8字节
.word 0x07FF, 0x0000, 0x9200, 0x00C0 // 16字节
生成 setup 需要的 gdt 表。
[1.4] 保护模式下的地址翻译和中断处理
在 Intel 硬件中实现了 GDT(Global Description Table),此时 cs:ip 中的 cs 代表选择子,ip 代表偏移,通过查表 + 偏移产生 32 位地址。
同时,中断也是通过 IDT 表去查询中断处理函数入口,包含系统接口。
[1.5] jmpi 0,8
上图是 GDT 表项。
根据 setup 中 gdt 的内容,可得基址为 0x00,因此实际上是跳转到内存 0x0000 处执行,即 操作系统的模块开始处 执行。
setup 的工作到此完成。
[2] 跳到 system 模块执行
[2.1] 第一部分代码 head.s
// linux/Makefile
disk: Image
dd bs=8192 if=Image of=/dev/PS0
Image: boot/bootsect boot/setup toolssystem 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
Image:操作系统镜像(boot | setup | OS | …)
head.s(head.o):操作系统的第一个部分
// head.s, 此处为 32 位汇编代码
stratup_32: ......
call setup_idt // 正式建立 idt 和 gdt 表
call setup_gdt
......
l: incl %eax
......
setup_idt: lea ignore_int,%edx
movl $0x00080000,%eax movw %dx,%ax
lea _idt,%edi movl %eax,(%edi)
setup 进入保护模式,完成少部分硬件的初始化;
而 head 是在进入保护模式后,完成操作系统正式运行之前的初始化。
[2.2] 三种汇编
- as86 汇编:16位
- GNU as 汇编:32位
- 内嵌汇编
[2.3] after_page_tables
after_page_tables:
pushl $0 pushl $0 pushl $0 pushl $L6
pushl $_main jmp set_paging
L6: jmp L6
setup_paging: 设置页表 ret
从汇编跳转到操作系统的 C 代码入口(main函数)。
[2.4] main 函数
// init/main.c
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();}
}
包含内存、中断、设备、时钟、CPU 等内容的初始化……
[2.5] mem_init()
// linux/mm/memory.c
void mem_init(long start_mem, long end_mem)
{
int i;
for(i=0; i<PAGING_PAGES; i++)
mem_map[i] = USED; // PAGIN_PAGES 被 OS 自己使用
i = MAP_NR(start_mem);
end_mem -= start_mem;
end_mem >>= 12; // 每个页的大小为 2^12 = 4K
while(end_mem -- > 0)
mem_map[i++] = 0; }
end_mem 由前面的步骤得出,代表总扩展内存大小。
[3] 小结
系统启动步骤:
boot -> setup -> head -> main -> mem_init -> …
或者简单概括为两大步骤:
(1)将操作系统读入内存
(2)完成初始化,获取各类硬件信息,生成对应数据结构