[No000031]操作系统 Operating Systems 之Open the OS!
从打开电源开始…
这神秘的黑色背后发生着什么?…
打开电源,计算机执行的第一句指令什么?
计算模型(图灵机) ⇒ 我们要 关注 指针IP 及其 指向的内容
看看x86 PC
(1) 刚开机时CPU 处于实模式(和保护模式对应,实模式的寻址CS:IP(CS 左移4 位+IP) ,和保护模式不一样!)
(2) 开机时,CS=0xFFFF; IP=0x0000
(3) 寻址0xFFFF0(ROM BIOS 映射区)
(4) 检查RAM ,键盘,显示器,软硬磁盘
(5) 将磁盘0 磁道0 扇区读入0x7c00 处
(6) 设置cs=0x07c0 ,ip=0x0000
0x7c00 处存放的代码
就是从磁盘引导扇区读入的那512 个字节
-
引导扇区就是 启动设备的第一个扇区(开机时按住del 键可进入启动设备设置界面,可以设置为光盘启动!)
-
启动设备信息被设置在CMOS(CMOS: 互补金属氧化物半导体(64B-128B) 。用来存储实时钟和硬件配置信息。) 中…
-
因此,硬盘的第一个扇区上存放着开机后执行的第一段我们可以控制的程序。
-
操作系统的故事从这里开始…
引导扇区代码: bootsect.s
.globl begtext,begdata,begbss,endtext,enddata,endbss .text // 文本段 .text 等是伪操作符,告诉编译器产生文本段,.text 用于标识文本段的开始位置。此处的.text 、.data 、.bss 表明这3 个段重叠,不分段! begtext: .data // 数据段 begdata: .bss // 未初始化数据段 begbss: entry start // 关键字entry 告诉链接器 "程序入口" start: mov ax, #BOOTSEG mov ds, ax//此条语句就是0x7c00处存放的语句! mov ax, #INITSEG mov es, ax mov cx, #256 sub si, si sub di,di//将0x07c0:0x0000 处的256 个字移动到0x9000:0x0000处 rep movw jmpi go, INITSEG BOOTSEG = 0x07c0 INITSEG = 0x9000 SETUPSEG = 0x9020
jmpi go, INITSEG
jmpi (jump intersegment 段间跳转): cs=INITSEG, ip=go
go: mov ax,cs //cs=0x9000 mov ds,ax mov es,ax mov ss,ax mov sp,#0xff00//为call 做准备! load_setup: // 载入setup 模块 mov dx,#0x0000 mov cx,#0x0002 mov bx,#0x0200 mov ax,#0x0200+SETUPLEN int 0x13 //BIOS 中断 0x13 是BIOS 读磁盘扇区的中断: ah=0x02- 读磁盘,al=扇区数量(SETUPLEN=4) ,ch= 柱面号,cl= 开始扇区,dh= 磁头号,dl= 驱动器号,es:bx=内存地址
jnc ok_load_setup mov dx,#0x0000 mov ax,#0x0000 // 复位 int 0x13 j load_setup //重读
读入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//7 是显示属性!
mov bp,#msg1 mov ax,#1301 int 0x10 // 显示字符
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
boot 工作: 读setup ,读system…
read_it // 读入system
为什么读入system 模块还需要定义一个函数?
system 模块可能很大,要跨越磁道!
read_it: mov ax,es cmp ax,#ENDSEG jb ok1_read ENDSEG=SYSSEG+SYSSIZE SYSSIZE=0x8000 // 该变量可根据 Image 大小设定( 编译操作系统时)
ret
ok1_read:
mov ax,sectors
sub ax,sread //sread 是当前磁道已读扇区数,ax 未读扇区数
call read_track // 读磁道...
引导扇区的末尾 //BIOS 用以识别引导扇区
.org 510
.word 0xAA55 //扇区的最后两个字节
否则会打出非引导设备
可以转入setup 执行了,jmpi 0, SETUPSEG
Power On…
setup 模块,即setup.s
根据名字就可以想到: setup 将完成OS启动前的设置
start: mov ax,#INITSEG mov ds,ax mov ah,#0x03
xor bh,bh int 0x10// 取光标位置dx mov [0],dx 取出光标位置( 包括其他硬件参数)到0x90000
mov ah,#0x88 int 0x15 mov [2],ax ... //扩展内存大小 SYSSEG = 0x1000
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
内存地址 |
长度 |
名称 |
0x90000 |
2 |
光标位置 |
0x90002 |
2 |
扩展内存数 |
0x901FC |
2 |
根设备号 |
0x9000C |
2 |
显卡参数 |
将setup 移到0 地址处...
-
但0 地址处是有重要内容的
-
以后不调用int 指令了吗?
-
因为操作系统要让硬件进入保护模式了…
-
保护模式下int n 和cs:ip 解释不再和实模式一样
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 将cs:ip 变成物理地址
保护模式下的地址翻译和中断处理
进入保护模式
call empty_8042 mov al,#0xD1 out #0x64,al
//8042 是键盘控制器,其输出端口P2 用来控制A20 地址线
call empty_8042 mov al,#0xDF out #0x60,al
// 选通A20 地址线 call empty_8042
初始化8259( 中断控制) // 一段非常机械化的程序
mov ax,#0x0001 mov cr0,ax
jmpi 0,8
D1 表示写数据到8042 的P2
jmpi 0,8 //gdt中的8
跳到system 模块执行...
-
system 模块( 目标代码) 中的第一部分代码? head.s
-
system 由许多文件编译而成,为什么是head.s?
disk: Image
dd bs=8192 if=Image of=/dev/PS0 if=input file /dev/PS0是软驱A
Image: boot/bootsect boot/setup tools/system tools/build
tools/build boot/bootsect boot/setup tools/system > Image linux/Makefile
tools/system: boot/head.o init/main.o $(DRIVERS) …
$(LD) boot/head.o init/main.o $(DRIVERS) … -o tools/system
明白为什么head.s 就这样一个名字了吧?
head.s // 一段在保护模式下运行的代码
setup 是进入保护模式,head 是进入之后的初始化
stratup_32: movl $0x10,%eax mov %ax,%ds mov %ax,%es
mov %as,%fs mov %as,%gs // 指向gdt 的0x10 项( 数据段) idt_48:.word 0 word 0,0
lss _stack_start,%esp // 设置栈( 系统栈) 现在忽略中断_ idt: .fill 256,8,0
call setup_idt struct{long *a; short b;}stack_start={&user_stack[PAGE_SIZE>>2],0x10};
call setup_gdt 和前面的代码不一样了? 因为是32 位汇编代码!
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的汇编和前面不一样?
-
as86 汇编:能产生16 位代码的Intel 8086(386) 汇编
mov ax, cs //cs -> ax, 目标操作数在前
-
GNU as 汇编:产生32 位代码,使用AT&T 系统V 语法
AT&T 美国电话电报公司,包含贝尔实验室等,1983 年AT&T UNIX 支持组发布了系统V
movl var, %eax //(var) ->%eax
movb -4(%ebp), %al // 取出一字节
(3) 内嵌汇编,gcc 编译x.c 会产生中间结果as 汇编文件x.s
__asm__(" 汇编语句": 输出 : 输入 : 破坏部分描述);
__asm__("movb %%fs:%2, %%al" :"=a"(_res) : "0"(seg), "m"(*(addr)) );
0 或空表示使用与相应输出一样的寄存器
a 表示使用eax ,并编号%0
%2 表示addr ,m表示使用内存
after_page_tables // 设置了页表之后
setup 是进入保护模式,head
after_page_tables:
pushl $0 pushl $0 pushl $0 pushl $L6
pushl $_main jmp set_paging
L6: jmp L6 //将来学到!
setup_paging: 设置页表 ret
简单的几句程序,控制流却很复杂
setup_paging 执行ret 后? 会执行函数main() |
进入main() 后的栈为0 ,0 ,0 ,L6 |
main() 函数的三个参数是0 ,0 ,0 |
main() 函数返回时进入L6 ,死循环... |
进入main 函数
在init/main.c 中 //开始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();}
}
为什么是void?
三个参数分别是envp,argv,argc但此处main 并没使用
此处的main 只保留传统main 的形式和命名main 表示C 语言函数的入口!
main 的工作就是xx_init: 内存、中断、设备、时钟、CPU 等内容的初始化…
看一看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;
i = MAP_NR(start_mem);
end_mem -= start_mem;
end_mem >>= 12;
while(end_mem -- > 0) //干了些什么?
mem_map[i++] = 0; }
管理硬件? 如何管理?就是用数据结构+ 算法…