鄭堰東 发表于 2024-4-14 01:02:14

Linux 0.11: 从开机到执行shell

参考

参考闪客的系列,将开机到执行shell的整个过程浓缩成本文。

[*]https://github.com/dibingfa/flash-linux0.11-talk
bootsect.s

当按下开机键的那一刻,在主板上提前写死的固件程序 BIOS 会将硬盘中启动区的 512 字节的数据,原封不动复制到内存中的 0x7c00 这个位置,并跳转到那个位置进行执行。
Linux-0.11 的最开始的代码是用汇编语言写的 bootsect.s,位于 boot 文件夹下。通过编译,这个 bootsect.s 会被编译成二进制文件,存放在启动区的第一扇区。
启动区的定义非常简单,只要硬盘中的 0 盘 0 道 1 扇区的 512 个字节的最后两个字节分别是 0x55 和 0xaa,那么 BIOS 就会认为它是个启动区。
所以对于我们理解操作系统而言,此时的 BIOS 仅仅就是个代码搬运工,把 512 字节的二进制数据从硬盘搬运到了内存中而已。所以作为操作系统的开发人员,仅仅需要把操作系统最开始的那段代码,编译并存储在硬盘的 0 盘 0 道 1 扇区即可。之后 BIOS 会帮我们把它放到内存里,并且跳过去执行。
_start:
        mov        $BOOTSEG, %ax        #将ds段寄存器设置为0x7C0
        mov        %ax, %ds
        mov        $INITSEG, %ax        #将es段寄存器设置为0x900
        mov        %ax, %es
        mov        $256, %cx                #设置移动计数值256字
        sub        %si, %si                #源地址        ds:si = 0x07C0:0x0000
        sub        %di, %di                #目标地址 es:si = 0x9000:0x0000
        rep                                        #重复执行并递减cx的值
        movsw                                #从内存处移动cx个字到处
        ljmp        $INITSEG, $go        #段间跳转,这里INITSEG指出跳转到的段地址,解释了cs的值为0x9000这里就是一件事:把代码移动到 0x90000 处,然后跳转 新位置 偏移 go 处。
ljmp $INITSEG, $go 相当于 cs = 0x90000, ip = $go
go:        mov        %cs, %ax                #将ds,es,ss都设置成移动后代码所在的段处(0x9000)
        mov        %ax, %ds
        mov        %ax, %es
# put stack at 0x9ff00.
        mov        %ax, %ss # ss = 0x9000
        mov        $0xFF00, %sp        #目前的栈顶地址就是ss:sp,即0x9FF00 处。这一部分是设置栈,把栈顶设置得离代码足够远。
##ah=0x02 读磁盘扇区到内存        al=需要独出的扇区数量
##ch=磁道(柱面)号的低八位 cl=开始扇区(位0-5),磁道号高2位(位6-7)
##dh=磁头号                                        dl=驱动器号(硬盘则7要置位)
##es:bx ->指向数据缓冲区;如果出错则CF标志置位,ah中是出错码
load_setup:
        mov        $0x0000, %dx                # drive 0, head 0
        mov        $0x0002, %cx                # sector 2, track 0
        mov        $0x0200, %bx                # address = 512, in INITSEG
        .equ    AX, 0x0200+SETUPLEN
        mov   $AX, %ax                # service 2, nr of sectors
        int        $0x13                          # read it将硬盘的第 2 (cx)个扇区开始,把数据加载到内存 0x90200(bx) 处,共加载 4(SETUPLEN) 个扇区

如果加载成功则跳转到 ok_load_setup,之后的主要逻辑是把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处,然后跳转到 0x90200 处的代码,也就是 setup.s 文件的第一行代码。
ok_load_setup:
    ...
    mov ax,#0x1000
    mov es,ax       ; segment of 0x10000
    call read_it
    ...
    jmpi 0,0x9020
setup.s

setup.s 被编译成setup 放在磁盘的2~5扇区。
setup的开始部分就是获取一些参数,存储在内存中:
内存地址长度(字节)名称0x900002光标位置0x900022扩展内存数0x900042显示页面0x900061显示模式0x900071字符列数0x900082未知0x9000A1显示内存0x9000B1显示状态0x9000C2显卡特性参数0x9000E1屏幕行数0x9000F1屏幕列数0x9008016硬盘1参数表0x9009016硬盘2参数表0x901FC2根设备号接着又是进行了内存的移动操作:
...
# now we want to move to protected mode ...

        cli                        # no interrupts allowed !
        # 因为后面我们要把原本是 BIOS 写好的中断向量表给覆盖掉,也就是给破坏掉了,写上我们自己的中断向量表,所以这个时候是不允许中断进来的。

# first we move the system to it's rightful place

        mov        $0x0000, %ax
        cld                        # 'direction'=0, movs moves forward
do_move:
        mov        %ax, %es        # destination segment
        add        $0x1000, %ax
        cmp        $0x9000, %ax
        jz        end_move
        mov        %ax, %ds        # source segment
        sub        %di, %di
        sub        %si, %si
        mov         $0x8000, %cx
        rep
        movsw
        jmp        do_move
于是,现在的内存布局变成了:

# then we load the segment descriptors

end_move:
        mov        $SETUPSEG, %ax        # right, forgot this at first. didn't work :-)
        mov        %ax, %ds
        lidt        idt_48                # load idt with 0,0
        lgdt        gdt_48                # load gdt with whatever appropriate这里会加载idt和gdt。以gdt为例解释一下:
gdt:
        .word        0,0,0,0                # dummy

        .word        0x07FF                # 8Mb - limit=2047 (2048*4096=8Mb),代码段描述符
        .word        0x0000                # base address=0,数据段描述符
        .word        0x9A00                # code read/exec
        .word        0x00C0                # granularity=4096, 386

        .word        0x07FF                # 8Mb - limit=2047 (2048*4096=8Mb)
        .word        0x0000                # base address=0
        .word        0x9200                # data read/write
        .word        0x00C0                # granularity=4096, 386

gdt_48: # 注意是小端序,0x800在低16位,0x9在高16位
        .word        0x800                        # gdt limit=2048, 256 GDT entries
        .word   512+gdt, 0x9                # gdt base = 0X9xxxx,
        # 512+gdt is the real gdt after setup is moved to 0x9020 * 0x10gdt_48 的高32位 为 gdt 在内存中的地址(gdt是setup文件的偏移,因为setup在内存中的起始位置为0x9020,所以要加上0x9020)
ds 寄存器里存储的值,在实模式下叫做段基址(段基址左移4位加上偏移得到物理地址),在保护模式下叫段选择子。段选择子里存储着段描述符的索引。
 
 通过段描述符索引,可以从全局描述符表 gdt 中找到一个段描述符,段描述符里存储着段基址。
 
 段基址取出来,再和偏移地址相加,就得到了物理地址,整个过程如下:
 
inb   $0x92, %al        # open A20 line(Fast Gate A20).
orb   $0b00000010, %al
outb    %al, $0x92打开A20地址线。这是为了兼容20位模式,如果不打开,即使有32位地址线,高于20位的位也会被丢掉。
接下来是对可编程中断控制器 8259 芯片进行的编程。
因为中断号是不能冲突的, Intel 把 0 到 0x19 号中断都作为保留中断,比如 0 号中断就规定为除零异常,软件自定义的中断都应该放在这之后,但是 IBM 在原 PC 机中搞砸了,跟保留中断号发生了冲突,以后也没有纠正过来,所以我们得重新对其进行编程,不得不做,却又一点意思也没有。这是 Linus 在上面注释上的原话。
mov        %cr0, %eax        # get machine status(cr0|MSW)
bts        $0, %eax        # turn on the PE-bit
mov        %eax, %cr0        # protection enabled启用保护模式(将cr0的第0位置为1)
# segment-descriptor      (INDEX:TI:RPL)
        .equ        sel_cs0, 0x0008
        # select for code segment 0 (001:0 :00)
        ljmp        $sel_cs0, $0        # jmp offset 0 of code segment 0 in gdt对照段选择子的结构,可以知道 描述符索引值是 1,也就是要去 全局描述符表(gdt) 中找第一项段描述符。这里取的就是代码段描述符,段基址是 0,偏移也是 0,那加一块就还是 0,所以最终这个跳转指令,就是跳转到内存地址的 0 地址处,开始执行。就是操作系统全部代码的 system 这个大模块的起始处。
head

pg_dir: # 页目录在0地址处,会覆盖掉执行过的代码
.globl startup_32
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp再往下连续五个 mov 操作,分别给 ds、es、fs、gs 这几个段寄存器赋值为 0x10,根据段描述符结构解析,表示这几个段寄存器的值为指向全局描述符表中的2号段描述符,也就是数据段描述符。
最后 lss 指令相当于让 ss:esp 这个栈顶指针指向了 _stack_start 这个标号的位置。
这个 stack_start 标号定义在了 sched.c 里:
long user_stack;
struct{  
        long *a;
        short b;
} stack_start = { &user_stack, 0x10 };stack_start 结构中的高位 16 字节是 0x10,将会赋值给 ss 栈段寄存器,低位 32 字节是 user_stack 这个数组的最后一个元素的地址值,将其赋值给 esp 寄存器。
赋值给 ss 的 0x10 仍然按照保护模式下的段选择子去解读,其指向的是全局描述符表中的第二个段描述符(数据段描述符),段基址是 0。
call setup_idt
call setup_gdt
movl $0x10,%eax                # reload all the segment registers
mov %ax,%ds                # after changing gdt. CS was already
mov %ax,%es                # reloaded in 'setup_gdt'
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp重新设置idt和gdt,因为原来的是在setup中的,这块地方接下来要被缓冲区覆盖掉。所以这里重新将其设置在head中。因为重新设置了gdt,所以还要重新执行mov刷新一遍才能生效。
...
        jmp after_page_tables
...
after_page_tables:
        pushl $0                # These are the parameters to main :-)
        pushl $0
        pushl $0
        pushl $L6                # return address for main, if it decides to.
        pushl $main
        jmp setup_paging
L6:
        jmp L6                        # main should never return here, but

.align 2
setup_paging:
        movl $1024*5,%ecx        /* 5 pages - pg_dir(页目录)占一页,4 个页表分别占一页 */
        xorl %eax,%eax
        xorl %edi,%edi               /* pg_dir is at 0x000 */
        cld;rep;stosl      /* 将开头的5页内存清零 */
        movl $pg0+7,pg_dir       /* set present r/w bit/user*/
        movl $pg1+7,pg_dir+4 /* 这里加7是为了将最低3位置1,即页存在,用户可读写*/
        movl $pg2+7,pg_dir+8
        movl $pg3+7,pg_dir+12
        movl $pg3+4092,%edi

.org 0x1000 pg0:
.org 0x2000 pg1:
.org 0x3000 pg2:
.org 0x4000 pg3:
.org 0x5000setup_paging 会初始化分页机制,也就是设置好页目录和页表。注意 pg_dir 在 0地址,也就是将之前执行的代码覆盖掉,作为页目录,存储了四个页目录项。一个页表包含1024个页表项,1页为4KB,因此16M 的地址空间可以用 1 个页目录表 + 4 个页表搞定。
        movl $pg3+4092,%edi   /* 从最后一个页表的最后一个页表项开始 */
        movl $0xfff007,%eax                /*16Mb - 4096 + 7 (r/w user,p) */
        std             /* 向低地址遍历 */
1:        stosl                        /* fill pages backwards - more efficient :-) */
        subl $0x1000,%eax
        jge 1b
        cld这一步通过一个循环来填充页表项,使得线性地址和对应的物理地址一样。
        xorl %eax,%eax                /* pg_dir is at 0x0000 */
        movl %eax,%cr3                /* cr3 - page directory start */
        movl %cr0,%eax
        orl $0x80000000,%eax
        movl %eax,%cr0                /* set paging (PG) bit */这一步设置了页目录的起始地址(存储在cr3寄存器),并且设置cr0的最高位为1以开启分页。
        ret                        /* this also flushes prefetch-queue */ret会跳转到main函数。这是怎么实现的呢?注意到在 jmp setup_paging 之前压入了5个参数,实际上这是模拟call指令的压栈过程,因此ret后pop出栈顶作为返回地址,即可跳转到main函数执行。
        pushl $0                # These are the parameters to main :-)
        pushl $0
        pushl $0
        pushl $L6                # return address for main, if it decides to.
        pushl $main
        jmp setup_pagingmain

内存初始化

void main(void) /* This really IS void, no error here. */
{         /* The startup routine assumes (well, ...) this */
        /* Interrupts are still disabled. Do necessary setups, then enable them */
        ROOT_DEV = ORIG_ROOT_DEV;
        drive_info = DRIVE_INFO;

        // EXT_MEM_K 是之前在setup中获取和设置的
        // EXT_MEM_K 存储的是系统从1MB开始的扩展内存数值,单位是KB,所以和以字节为单位的1MB相加时需要左移10位。
        memory_end = (1<<20) + (EXT_MEM_K<<10); // 忽略不到4KB(1页)的内存
        memory_end &= 0xfffff000;
        // 如果内存超过16MB,则按照16MB计算
        if (memory_end > 16*1024*1024)
                memory_end = 16*1024*1024;
        // 如果内存大于12MB则缓冲区末端为4MB
        if (memory_end > 12*1024*1024)
                buffer_memory_end = 4*1024*1024;
        // 如果内存大于6MB则缓冲区末端为2MB
        else if (memory_end > 6*1024*1024)
                buffer_memory_end = 2*1024*1024;
        // 剩下的情况,也就是内存为0MB---6MB,则缓冲区末端为1MB
        else
                buffer_memory_end = 1*1024*1024;
        // 主内存起始地址 = 缓冲区末端
        main_memory_start = buffer_memory_end;
        ...
}就是对内存分页,mem_map这个数组的每一项管理一页。

以上图为例:

[*]1M 以下的内存这个数组干脆没有记录,这里的内存是无需管理的,或者换个说法是无权管理的,也就是没有权利申请和释放,因为这个区域是内核代码所在的地方,不能被“污染”。
[*]1M 到 2M 这个区间是缓冲区,2M 是缓冲区的末端,缓冲区的开始在哪里之后再说,这些地方不是主内存区域,因此直接标记为 USED,产生的效果就是无法再被分配了。
[*]2M 以上的空间是主内存区域,而主内存目前没有任何程序申请,所以初始化时统统都是零,未来等着应用程序去申请和释放这里的内存资源。
中断初始化


[*]trap_init:给0到48号中断设置中断处理函数
void mem_init(long start_mem, long end_mem)
{
        int i;

        HIGH_MEMORY = end_mem;
        for (i=0 ; i<PAGING_PAGES ; i++)
                mem_map = USED;
        i = MAP_NR(start_mem);
        end_mem -= start_mem;
        end_mem >>= 12;
        while (end_mem-->0)
                mem_map=0;
}TSS 叫任务状态段,就是保存和恢复进程的上下文的,所谓上下文,其实就是各个寄存器的信息而已,这样进程切换的时候,才能做到保存和恢复上下文,继续执行。
void trap_init(void)
{
        int i;

        set_trap_gate(0,&divide_error);
        set_trap_gate(1,&debug);
        set_trap_gate(2,&nmi);
        set_system_gate(3,&int3);        /* int3-5 can be called from all */
        set_system_gate(4,&overflow);
        set_system_gate(5,&bounds);
        set_trap_gate(6,&invalid_op);
        set_trap_gate(7,&device_not_available);
        set_trap_gate(8,&double_fault);
        set_trap_gate(9,&coprocessor_segment_overrun);
        set_trap_gate(10,&invalid_TSS);
        set_trap_gate(11,&segment_not_present);
        set_trap_gate(12,&stack_segment);
        set_trap_gate(13,&general_protection);
        set_trap_gate(14,&page_fault); // 缺页中断
        set_trap_gate(15,&reserved);
        set_trap_gate(16,&coprocessor_error);
        for (i=17;i<48;i++)
                set_trap_gate(i,&reserved);
        set_trap_gate(45,&irq13);
        outb_p(inb_p(0x21)&0xfb,0x21);
        outb(inb_p(0xA1)&0xdf,0xA1);
        set_trap_gate(39,&parallel_interrupt);
}而 LDT 叫局部描述符表,是与 GDT 全局描述符表相对应的,内核态的代码用 GDT 里的数据段和代码段,而用户进程的代码用每个用户进程自己的 LDT 里的数据段和代码段。
每个进程用一个 task_struct 表示,里面就有 ldt 和 tss 两个成员。ldt包含三项,分别为0、cs(代码段)、ds&ss(数据段)
#define sti() __asm__ ("sti"::)
缓冲区初始化

缓冲区被分成一个个1024byte的块,每个块对应一个buffer_head
/*
* The request-struct contains all necessary data
* to load a nr of sectors into memory
*/
struct request request;/*
* Ok, this is an expanded form so that we can use the same
* request for paging requests when that is implemented. In
* paging, 'bh' is NULL, and 'waiting' is used to wait for
* read/write completion.
*/
struct request {
        int dev;                /* 设备号,-1 表示无请求 */
        int cmd;                /* READ or WRITE */
        int errors;
        unsigned long sector; /* 起始扇区 */
        unsigned long nr_sectors; /* 扇区数 */
        char * buffer; /* 数据缓冲区,读盘后数据放在内存中的位置 */
        struct task_struct * waiting; /* 哪个进程发起的请求 */
        struct buffer_head * bh; /* 缓冲区头指针 */
        struct request * next; /* 链表,指向下一个 */
};宏展开:
void blk_dev_init(void)
{
        int i;

        for (i=0 ; i<NR_REQUEST ; i++) {
                request.dev = -1;
                request.next = NULL;
        }
}系统调用统一通过 int 0x80 中断来进入,具体调用这个表里的哪个功能函数,就由 eax 寄存器传过来,这里的值是个数组索引的下标,通过这个下标就可以找到在 sys_call_table 这个数组里的具体函数。
static void time_init(void)
{
        struct tm time;

        do {
                time.tm_sec = CMOS_READ(0);
                time.tm_min = CMOS_READ(2);
                time.tm_hour = CMOS_READ(4);
                time.tm_mday = CMOS_READ(7);
                time.tm_mon = CMOS_READ(8);
                time.tm_year = CMOS_READ(9);
        } while (time.tm_sec != CMOS_READ(0));
        BCD_TO_BIN(time.tm_sec);
        BCD_TO_BIN(time.tm_min);
        BCD_TO_BIN(time.tm_hour);
        BCD_TO_BIN(time.tm_mday);
        BCD_TO_BIN(time.tm_mon);
        BCD_TO_BIN(time.tm_year);
        time.tm_mon--;
        startup_time = kernel_mktime(&time);
}linux/sys.h 中可以找到 sys_call_table
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})如果是fork,则会调用到sys_fork
void sched_init(void)
{
        int i;
        struct desc_struct * p;

        if (sizeof(struct sigaction) != 16)
                panic("Struct sigaction MUST be 16 bytes");
        // 设置init_task的TSS和LDT
        set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
        set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
        p = gdt+2+FIRST_TSS_ENTRY;
        // 余下的项清0
        for(i=1;i<NR_TASKS;i++) {
                task = NULL;
                p->a=p->b=0;
                p++;
                p->a=p->b=0;
                p++;
        }
/* Clear NT, so that we won't have troubles with that later on */
        __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
        ltr(0);
        lldt(0);
        // 设置定时器
        outb_p(0x36,0x43);                /* binary, mode 3, LSB/MSB, ch 0 */
        outb_p(LATCH & 0xff , 0x40);        /* LSB */
        outb(LATCH >> 8 , 0x40);        /* MSB */
        // 设置时钟中断处理程序
        set_intr_gate(0x20,&timer_interrupt);
        // 启用时钟中断
        outb(inb_p(0x21)&~0x01,0x21);
        // 设置系统调用处理函数
        set_system_gate(0x80,&system_call);
}struct tss_struct {
        long        back_link;        /* 16 high bits zero */
        long        esp0;
        long        ss0;                /* 16 high bits zero */
        long        esp1;
        long        ss1;                /* 16 high bits zero */
        long        esp2;
        long        ss2;                /* 16 high bits zero */
        long        cr3;
        long        eip;
        long        eflags;
        long        eax,ecx,edx,ebx;
        long        esp;
        long        ebp;
        long        esi;
        long        edi;
        long        es;                /* 16 high bits zero */
        long        cs;                /* 16 high bits zero */
        long        ss;                /* 16 high bits zero */
        long        ds;                /* 16 high bits zero */
        long        fs;                /* 16 high bits zero */
        long        gs;                /* 16 high bits zero */
        long        ldt;                /* 16 high bits zero */
        long        trace_bitmap;        /* bits: trace 0, bitmap 16-31 */
        struct i387_struct i387;
};copy_mem 最后进行了 copy_page_tables ,将老进程的页表拷贝给新进程,让新旧进程共享同一份物理地址空间
struct task_struct {
/* these are hardcoded - don't touch */
        long state;        /* -1 unrunnable, 0 runnable, >0 stopped */
        long counter;
        long priority;
        long signal;
        struct sigaction sigaction;
        long blocked;        /* bitmap of masked signals */
/* various fields */
        int exit_code;
        unsigned long start_code,end_code,end_data,brk,start_stack;
        long pid,father,pgrp,session,leader;
        unsigned short uid,euid,suid;
        unsigned short gid,egid,sgid;
        long alarm;
        long utime,stime,cutime,cstime,start_time;
        unsigned short used_math;
/* file system info */
        int tty;                /* -1 if no tty, so it must be signed */
        unsigned short umask;
        struct m_inode * pwd;
        struct m_inode * root;
        struct m_inode * executable;
        unsigned long close_on_exec;
        struct file * filp;
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
        struct desc_struct ldt;
/* tss for this task */
        struct tss_struct tss;
};shell 的到来

由于 fork 函数一调用,就又多出了一个进程,子进程(进程 1)会返回 0,父进程(进程 0)返回子进程的 ID,所以 init 函数只有进程 1 才会执行。
struct buffer_head {
        char * b_data;                        /* pointer to data block (1024 bytes) */
        unsigned long b_blocknr;        /* block number */
        unsigned short b_dev;                /* device (0 = free) */
        unsigned char b_uptodate;
        unsigned char b_dirt;                /* 0-clean,1-dirty */
        unsigned char b_count;                /* users using this block */
        unsigned char b_lock;                /* 0 - ok, 1 -locked */
        struct task_struct * b_wait;
        struct buffer_head * b_prev;
        struct buffer_head * b_next;
        struct buffer_head * b_prev_free;
        struct buffer_head * b_next_free;
};setup 是个系统调用,会通过中断最终调用到 sys_setup 函数
setup 传入的drive_info 是来自内存 0x90080 的数据,这部分是由之前 setup.s 程序将硬盘 1 的参数信息放在这里了,包括柱面数、磁头数、扇区数等信息。
extern int end; // end 是链接器计算出的内核代码的末尾地址
struct buffer_head * start_buffer = (struct buffer_head *) &end;

void buffer_init(long buffer_end)
{
        struct buffer_head * h = start_buffer;
        void * b;
        int i;

        if (buffer_end == 1<<20)
                b = (void *) (640*1024);
        else
                b = (void *) buffer_end;
        // 缓冲区结尾侧的 b 每次循环 -1024,也就是一页的值,缓冲区开头侧的 h 每次循环 +1(一个 buffer_head 大小的内存),直到碰一块为止。
        while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {
                h->b_dev = 0;
                h->b_dirt = 0;
                h->b_count = 0;
                h->b_lock = 0;
                h->b_uptodate = 0;
                h->b_wait = NULL;
                h->b_next = NULL;
                h->b_prev = NULL;
                h->b_data = (char *) b;
                h->b_prev_free = h-1;
                h->b_next_free = h+1;
                h++;
                NR_BUFFERS++;
                if (b == (void *) 0x100000)
                        b = (void *) 0xA0000;
        }
        h--;
        free_list = start_buffer;
        free_list->b_prev_free = h;
        h->b_next_free = free_list;
        for (i=0;i<NR_HASH;i++)
                hash_table=NULL;
}setup 方法中的最后一个函数 mount_root,加载根文件系统。有了根文件系统之后,操作系统才能从一个根儿开始找到所有存储在硬盘中的文件,所以它是文件系统的基石,很重要。
从整体上说,它就是要把硬盘中的数据,以文件系统的格式进行解读,加载到内存中设计好的数据结构,这样操作系统就可以通过内存中的数据,以文件系统的方式访问硬盘中的一个个文件了。
void hd_init(void)
{
        blk_dev.request_fn = DEVICE_REQUEST; // 初始化硬盘的请求处理函数
        set_intr_gate(0x2E,&hd_interrupt); // 设置硬盘中断的处理函数
        // 允许硬盘控制器发送中断请求信号
        outb_p(inb_p(0x21)&0xfb,0x21);
        outb(inb_p(0xA1)&0xbf,0xA1);
}首先硬盘中的文件系统,无非就是硬盘中的一堆数据,我们按照一定格式去解析罢了。Linux-0.11 中的文件系统是 MINIX 文件系统,它就长成这个样子。

每一个块结构的大小是 1024 字节,也就是 1KB,硬盘里的数据就按照这个结构,妥善地安排在硬盘里。
可是硬盘中凭什么就有了这些信息呢?这就是个鸡生蛋蛋生鸡的问题了。你可以先写一个操作系统,然后给一个硬盘做某种文件系统类型的格式化,这样你就得到一个有文件系统的硬盘了,有了这个硬盘,你的操作系统就可以成功启动了。
MINIX 文件系统的格式:

[*]引导块就是我们系列最开头说的启动区,当然不一定所有的硬盘都有启动区,但我们还是得预留出这个位置,以保持格式的统一。
[*]超级块用于描述整个文件系统的整体信息,我们看它的字段就知道了,有后面的 inode 数量,块数量,第一个块在哪里等信息。有了它,整个硬盘的布局就清晰了。
[*]inode 位图和块位图,就是位图的基本操作和作用了,表示后面 inode 和块的使用情况。
[*]inode 存放着每个文件或目录的元信息和索引信息,元信息就是文件类型、文件大小、修改时间等,索引信息就是大小为 9 的 i_zone 块数组,表示这个文件或目录的具体数据占用了哪些块。其中块数组里,0~6 表示直接索引,7 表示一次间接索引,8 表示二次间接索引。当文件比较小时,比如只占用 2 个块就够了,那就只需要 zone 和 zone 两个直接索引即可。

再往后,就都是存放具体文件或目录实际信息的块了。如果是一个普通文件类型的 inode 指向的块,那里面就直接是文件的二进制信息。如果是一个目录类型的 inode 指向的块,那里面存放的就是这个目录下的文件和目录的 inode 索引以及文件或目录名称等信息。
init 接下来会调用open打开"/dev/tty0"文件
/* blk_dev_struct is:
*        do_request-address
*        next-request
*/
struct blk_dev_struct blk_dev = {
        { NULL, NULL },                /* no_dev */
        { NULL, NULL },                /* dev mem */
        { NULL, NULL },                /* dev fd */
        { NULL, NULL },                /* dev hd */
        { NULL, NULL },                /* dev ttyx */
        { NULL, NULL },                /* dev tty */
        { NULL, NULL }                /* dev lp */
};#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
        "pushl $0x17\n\t" \         // SS
        "pushl %%eax\n\t" \         // ESP
        "pushfl\n\t" \                         // EFLAGS
        "pushl $0x0f\n\t" \         // CS
        "pushl $1f\n\t" \                 // EIP
        "iret\n" \
        "1:\tmovl $0x17,%%eax\n\t" \
        "movw %%ax,%%ds\n\t" \
        "movw %%ax,%%es\n\t" \
        "movw %%ax,%%fs\n\t" \
        "movw %%ax,%%gs" \
        :::"ax")set_intr_gate(0x20,&timer_interrupt);
execve

.align 2
timer_interrupt:
        push %ds                # save ds,es and put kernel data space
        push %es                # into them. %fs is used by _system_call
        push %fs
        pushl %edx                # we save %eax,%ecx,%edx as gcc doesn't
        pushl %ecx                # save those across function calls. %ebx
        pushl %ebx                # is saved as we use that in ret_sys_call
        pushl %eax
        movl $0x10,%eax
        mov %ax,%ds
        mov %ax,%es
        movl $0x17,%eax
        mov %ax,%fs
        incl jiffies
        movb $0x20,%al                # EOI to interrupt controller #1
        outb %al,$0x20
        movl CS(%esp),%eax# 发生中断时处理器自动压入CS,这里读取出来,检查CPL(current privilege level)
        andl $3,%eax                # %eax is CPL (0 or 3, 0=supervisor)
        pushl %eax                        # CPL 作为参数
        call do_timer                # 'do_timer(long CPL)' does everything from
        addl $4,%esp                # task switching to accounting ...
        jmp ret_from_sys_callinit 进程接着fork出一个新进程,新进程通过 close 和 open 函数,将 0 号文件描述符指向的标准输入 /dev/tty0 更换为指向 /etc/rc 文件
接下来进程 2 就将变得不一样了,会通过一个 execve 函数调用,使自己摇身一变,成为 /bin/sh 程序继续运行!
void do_timer(long cpl)
{
        extern int beepcount;
        extern void sysbeepstop(void);

        if (beepcount)
                if (!--beepcount)
                        sysbeepstop();

        if (cpl)
                current->utime++;
        else
                current->stime++;

        ...
        if (current_DOR & 0xf0)
                do_floppy_timer();
        if ((--current->counter)>0) return; // 时间片未到0,返回
        current->counter=0;
        if (!cpl) return; // 如果当前是内核态则不调度
        schedule(); // 时间片到0,且为用户模式,进行调度。
}#define FIRST_TASK task
#define LAST_TASK task
void schedule(void)
{
        int i,next,c;
        struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */

        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
                if (*p) {
                        if ((*p)->alarm && (*p)->alarm < jiffies) {
                                        (*p)->signal |= (1<<(SIGALRM-1));
                                        (*p)->alarm = 0;
                                }
                                // (*p)->signal 表示待处理的信号
                                // ~(_BLOCKABLE & (*p)->blocked)) 表示未被屏蔽的信号
                                // TASK_INTERRUPTIBLE: 处于睡眠状态,并且等待某个信号
                        if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
                        (*p)->state==TASK_INTERRUPTIBLE)
                                (*p)->state=TASK_RUNNING;
                }

/* this is the scheduler proper: */

        while (1) {
                c = -1; // 所有进程剩余时间片的最大值
                next = 0; // 最大剩余时间片进程的索引
                i = NR_TASKS;
                p = &task;
                while (--i) {
                        if (!*--p)
                                continue;
                        if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
                                c = (*p)->counter, next = i;
                }
                if (c) break; // 如果存在一个剩余时间片不为0的任务,则break,否则设置所有任务的剩余时间片
                for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
                        if (*p)
                                (*p)->counter = ((*p)->counter >> 1) +
                                                (*p)->priority;
        }
        // 切换到目标进程
        switch_to(next);
}#define FIRST_TSS_ENTRY 4
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
// FIRST_TSS_ENTRY<<3表示左移3位,因为TI和RPL总共占3位
// n<<4,实际上索引加上 n<<1,因为一个进程占一个TSS和一个LDT
#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
/*
*        switch_to(n) should switch tasks to task nr n, first
* checking that n isn't the current task, in which case it does nothing.
* This also clears the TS-flag if the task we switched to has used
* tha math co-processor latest.
*/
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" # 先比较是不是要切换到当前任务 \
        "je 1f\n\t" # 如果是就什么都不做 \
        "movw %%dx,%1\n\t" # 把TSS赋给__tmp.b \
        "xchgl %%ecx,current\n\t" # 交换 ecx 和 current \
        "ljmp *%0\n\t" # 将__tmp.b作为段选择子 \
        "cmpl %%ecx,last_task_used_math\n\t" \
        "jne 1f\n\t" \
        "clts\n" \
        "1:" \
        ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
        "d" (_TSS(n)),"c" ((long) task)); \
}
来源:https://www.cnblogs.com/iku-iku-iku/p/17991016
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: Linux 0.11: 从开机到执行shell