1. 打开电源

打开电源时,指针IP指向哪里,指向什么内容?IP必定指向的内存的某块位置。内存上有一块固化的程序,程序中存储的内容由硬件设计者决定。以x86 PC为例:

1.1. bootsect.s

x86 PC

  1. x86 PC刚开机时PC处于实模式

  2. 开机时 CS=0xFFFF;IP=0x0000(CS,段寄存器;IP,偏移)

  3. 寻址0xFFFF0(指向ROM BIOS映射区) (内存中唯一有程序的地方)

  4. 检查RAM,键盘,显示器,软硬磁盘

  5. 将磁盘0磁道0扇区读入0x7c00处(一个扇区512字节。0磁道0扇区就是引导扇区,OS的第一段代码)

  6. 设置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做了这些事:

  1. 读硬件参数

  2. 挪动system

  3. 启动保护模式

  4. 跳转到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. 总结

  1. bootsect把OS从硬盘读到内存

  2. setup读硬件并初始化GDT,IDT

  3. head.s设置页表

  4. 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,“进入内核”,这是用户程序发起调用内核代码的唯一方式。 系统调用的核心:

  1. 用户程序中包含一段int指令的代码(由库函数进行)

  2. 操作系统写中断处理,获取想调程序的编号

  3. 操作系统根据编号执行相应代码

例如: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三个参数