1. 打开电源¶
打开电源时,指针IP指向哪里,指向什么内容?IP必定指向的内存的某块位置。内存上有一块固化的程序,程序中存储的内容由硬件设计者决定。以x86 PC为例:
1.1. bootsect.s¶
x86 PC
x86 PC刚开机时PC处于实模式
开机时 CS=0xFFFF;IP=0x0000(CS,段寄存器;IP,偏移)
寻址0xFFFF0(指向ROM BIOS映射区) (内存中唯一有程序的地方)
检查RAM,键盘,显示器,软硬磁盘
将磁盘0磁道0扇区读入0x7c00处(一个扇区512字节。0磁道0扇区就是引导扇区,OS的第一段代码)
设置cs=0x07c0, ip=0x0000
注解
实模式:与保护模式对应,实模式的寻址CS:IP(CS左移4位+IP),与保护模式不同
0x7c00处存放的代码,从磁盘引导扇区读入的那512个字节。引导扇区就是启动设备的第1个扇区,启动设备信息被设置在CMOS中,因此,硬盘的第1个扇区上存放着开机后执行的第一段我们可以控制的程序。
引导扇区代码:bootsect.s( * .s就是汇编)
.global begtext,begdata,begbss,entext,enddata,endbss
.text //文本段
begtext:
.data //数据段
begdata:
.bss //未初始化数据段
begbss:
entry start //关键字entry告诉链接器“程序入口”
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
BOOTSEG=0x7c0,INITSEG=0x9000,SETUPSEG=0x9020,start后ds为0x07c0,es为0x9000,si=0,di=0(段偏移),这样 (ds<<4+si)=0x7c00, (es<<4+di)=0x90000. 然后cx移256个字,这将bootsect从0x7c00移到0x90000。最后jmpi将go赋给IP,INITSEG赋给CS,得到go的位置,此即跳到go执行。
go:
mov ax, cs //cs=0x9000
mov ds, ax
mov es, ax
mov ss, ax
mov sp, #0xff00
load_setup:
mov dx, #0x00
mov cs, #0x0002
mov bx, #0x0200
mov ax, #0x200+SETUPLEN
int 0x13 //BIOS中断
jnc ok_load_setup
mov dx, #0x0000
mov ax, #0x0000 //复位
int 0x13
j load_setup //重读
现在只读入了引导扇区,通过int 0x13先读入磁盘。setup由4个扇区组成,应该读入bootsect的之后地址为0x90200的地方。读进了setup之后,继续向后读:
注解
0x13是BIOS读磁盘扇区的中断:ah=0x02-读磁盘,al=扇区数量(SETUPLEN=4),ch=柱面号,cl=开始扇区,dh=磁头号,dl=驱动器号,es:bx=内存地址。
Ok_load_setup:
mov dl, #0x0000
mov ax, #0x0800 //ah=8获得磁盘参数
int 0x13
mov ch, #0x0000
mov sectors, cs
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
jmpi 0,SETUPSEG
这里的关键是int 0x10显示字符,bp表示要显示的字符在内存中的位置,#msg是一个偏移。先要将字符显示在光标后,先读出光标的位置,然后显示这个字符。
read_it 仍然0x13中断,读入system模块。
read_it:
mov ax, es
cmp ax, #ENDSEG
jb ok1_read
ret
ok1_read:
mov ax, sectors
sub ax,sred //sread 当前磁道已读扇区,ax未读扇区
call read_track //读磁道
.org 510
.word 0xAA55 //扇区的最后两个字节
读完bootsect之后就将控制权交给setup。jmpi 0, SETUPSEG转入setup执行。
1.2. setup.s¶
操作系统是管理硬件的,要知道内存的大小对OS是非常重要的。setup在进行OS要接管硬件。
start:
mov ax, #INITSEG
mov ds, ax
mov ah, #0x03
xor bh, bh
int 0x10 //取光标位置
mov [0], dx
mov ah, #0x88
int 0x15 //获得物理内存的大小
mov [2], ax // 在 0x9000<<4 + 2 存放扩展内存的大小
cli
mov ax, #0x0000
cld
do_move:
mov es, ax
add ax, #0x1000
cmp ax, #9000 js end_move
mov ds, ax
sub si, si
mov cx, #0x8000
rep
movsw
jmp do_move
将setup移到0,这样setup就结束了。 但0地址处有重要内容,以后就不调用int了吗?因为操作系统要让硬件进行保护模式了,保护模式下int n和cs:ip解释不再和实模式一样。
jmpi 0,8是做什么的?从语句上是ip=0,cs=8,这样形成0x008+ip = 0x0080,实际0x0080是非法的不能被执行的。那么OS在这里寻址方式要改变,因为如果不改变只有1M可以访问。这里要切换为32位机的寻址方式,也就是保护模式。
注解
16位,32位的区别在于:CPU解释程序不一样,具体来说是CPU电路不同。CPU内有cr0寄存器,如果cr0是1就是保护模式。
保护模式下的地址翻译:cs,selector,作为查gdt的索引,gdt内需要有内容,setup就要先初始化表:
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之后再jmpi 0, 8就可以正常进行了。
保护模式下的中断处理,是IDT表中找到中断处理函数入口
进入保护模式
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
empty_8042:
.word 0x00ab,0x00ab
in al, 0x64
test al, #2
jnz empty_8042
ret
jmpi 0,8用来查gdt。现在跳到0地址执行,0地址处现在是system,就是操作系统的核心模块,就是OS开始的地方,这样setup就结束了。
总结来说,setup做了这些事:
读硬件参数
挪动system
启动保护模式
跳转到0地址
1.3. system模块¶
system模块第1部分:head.s,(在Makefile可以看到编译的结构)
head是进入之后的初始化,接下来要真正工作,所以需要 call setup_idt, call setup_gdt。从现在开始,汇编是32位的,进入了保护模式。
注解
使用了as86汇编(产生16位代码的Intel 8086(386)汇编)、GNU as汇编(产生32位代码,使用AT&T系统V语法),内嵌汇编(gcc编译x.c会产生中间结果as汇编文件x.s)。
after_page_tables:
pushl $0
pushl $0
pushl $0
pushl $L6
pushl $_main
jmp set_paging //设置页表
L6:
jmp L6
set_paging:
...//设置页表
ret
ret之后执行main,在将0,0,0作为main参数压栈,$L6作为main的返回地址压栈。即如果main有了返回值就会返回L6,而L6只会jmp L6,这是一个死循环。如果执行到L6那么计算机就陷入死机状态了,即正常情况下main永远不能执行完成。
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; // (每次减4K),即4K作为一页
while(end_mem -- > 0)
{
mem_map[i++]=0;
}
}
int main(int envp,int argc,char *argv[])
{
mem_init();
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_iit();
sched_init();
buffer_init();
hd_init();
floppy_init();
sti();
return 0;
}
完成这一些初始化,操作系统就可以开始工作了。
1.4. 总结¶
bootsect把OS从硬盘读到内存
setup读硬件并初始化GDT,IDT
head.s设置页表
main进行初始化
2. 操作系统接口¶
2.1. 接口(Interface)¶
接口就是连接两个东西,信号转换,屏蔽细节。操作系统接口就是连接上层用户和操作系统软件,方便使用,屏蔽细节。用户是怎么使用操作系统的:命行、图形按钮、应用程序。命令就是一段程序而已。图形按钮就是通过硬件输入响应的一个程序,应用程序也是程序。操作系统提供一些重要函数,接口表现为函数调用。
type |
POSIX defination |
description |
task manager |
fork |
create a process |
excel |
run a executable program |
|
pthread_create |
create a thread |
|
file system |
open |
open file or directory |
EACCES |
means access denied |
|
mod_t st_mode |
file head: attribute |
2.2. 系统调用的实现¶
实现一个 whoami调用调用:用户程序调用whoami,一个字符串存放在操作系统中(系统引导时载入),取出打印。
应用程序也在内存中,OS也在内存中,但是并不能让应用程序直接访问操作系统:
不能随意调用数据,不能随意jmp
可以看到root密码,可以修改
可以通过显示看到别的应用程序中的内容。
那是非常危险的
内核态/用户态,内核段/用户段:将内核程序和用户隔离。内核态可以访问任何数据,用户态不能访问内核数据。 最内部是核心态,外两层是OS服务,最外层是用户态。这是处理器硬件设计。硬件提供了“主动进入内核的方法”,对于Inter x86,就是中断指令int。int指令将CS中的CPL改为0,“进入内核”,这是用户程序发起调用内核代码的唯一方式。 系统调用的核心:
用户程序中包含一段int指令的代码(由库函数进行)
操作系统写中断处理,获取想调程序的编号
操作系统根据编号执行相应代码
例如:C程序语句printf(), 调用库函数printf(), 调用库函数write(), 调用系统内核write().
详细的write:在linux/include/unistd.h中:
#define _syscall3(type,name,atype,a,btype,b,ctype,c)\
type name(atype a, btype b, ctype c)\
{ long __res;\
__asm__ volatle("int 0x80":"=a"(res):""(__NR_##name),
"b"((long)(a)),"c"((long)(b)),"d"((long)(c))));
if(__rec>=0) return (type)__res;
erron=-_res;
return -1;
}
__NR_write是系统调用号,放在eax中同时eax也存放返回值,ebx,ecx,edx三个参数