翼度科技»论坛 云主机 LINUX 查看内容

Linux 0.11: 从开机到执行shell

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
参考

参考闪客的系列,将开机到执行shell的整个过程浓缩成本文。
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 会帮我们把它放到内存里,并且跳过去执行。
  1. _start:
  2.         mov        $BOOTSEG, %ax        #将ds段寄存器设置为0x7C0
  3.         mov        %ax, %ds
  4.         mov        $INITSEG, %ax        #将es段寄存器设置为0x900
  5.         mov        %ax, %es
  6.         mov        $256, %cx                #设置移动计数值256字
  7.         sub        %si, %si                #源地址        ds:si = 0x07C0:0x0000
  8.         sub        %di, %di                #目标地址 es:si = 0x9000:0x0000
  9.         rep                                        #重复执行并递减cx的值
  10.         movsw                                #从内存[si]处移动cx个字到[di]处
  11.         ljmp        $INITSEG, $go        #段间跳转,这里INITSEG指出跳转到的段地址,解释了cs的值为0x9000
复制代码
这里就是一件事:把代码移动到 0x90000 处,然后跳转 新位置 偏移 go 处。
ljmp $INITSEG, $go 相当于 cs = 0x90000, ip = $go
  1. go:        mov        %cs, %ax                #将ds,es,ss都设置成移动后代码所在的段处(0x9000)
  2.         mov        %ax, %ds
  3.         mov        %ax, %es
  4. # put stack at 0x9ff00.
  5.         mov        %ax, %ss # ss = 0x9000
  6.         mov        $0xFF00, %sp        #目前的栈顶地址就是ss:sp,即0x9FF00 处。
复制代码
这一部分是设置栈,把栈顶设置得离代码足够远。
  1. ##ah=0x02 读磁盘扇区到内存        al=需要独出的扇区数量
  2. ##ch=磁道(柱面)号的低八位 cl=开始扇区(位0-5),磁道号高2位(位6-7)
  3. ##dh=磁头号                                        dl=驱动器号(硬盘则7要置位)
  4. ##es:bx ->指向数据缓冲区;如果出错则CF标志置位,ah中是出错码
  5. load_setup:
  6.         mov        $0x0000, %dx                # drive 0, head 0
  7.         mov        $0x0002, %cx                # sector 2, track 0
  8.         mov        $0x0200, %bx                # address = 512, in INITSEG
  9.         .equ    AX, 0x0200+SETUPLEN
  10.         mov     $AX, %ax                # service 2, nr of sectors
  11.         int        $0x13                            # read it
复制代码
将硬盘的第 2 (cx)个扇区开始,把数据加载到内存 0x90200(bx) 处,共加载 4(SETUPLEN) 个扇区

如果加载成功则跳转到 ok_load_setup,之后的主要逻辑是把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处,然后跳转到 0x90200 处的代码,也就是 setup.s 文件的第一行代码。
  1. ok_load_setup:
  2.     ...
  3.     mov ax,#0x1000
  4.     mov es,ax       ; segment of 0x10000
  5.     call read_it
  6.     ...
  7.     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根设备号接着又是进行了内存的移动操作:
  1. ...
  2. # now we want to move to protected mode ...
  3.         cli                        # no interrupts allowed !
  4.         # 因为后面我们要把原本是 BIOS 写好的中断向量表给覆盖掉,也就是给破坏掉了,写上我们自己的中断向量表,所以这个时候是不允许中断进来的。
  5. # first we move the system to it's rightful place
  6.         mov        $0x0000, %ax
  7.         cld                        # 'direction'=0, movs moves forward
  8. do_move:
  9.         mov        %ax, %es        # destination segment
  10.         add        $0x1000, %ax
  11.         cmp        $0x9000, %ax
  12.         jz        end_move
  13.         mov        %ax, %ds        # source segment
  14.         sub        %di, %di
  15.         sub        %si, %si
  16.         mov         $0x8000, %cx
  17.         rep
  18.         movsw
  19.         jmp        do_move
复制代码

于是,现在的内存布局变成了:
  1. # then we load the segment descriptors
  2. end_move:
  3.         mov        $SETUPSEG, %ax        # right, forgot this at first. didn't work :-)
  4.         mov        %ax, %ds
  5.         lidt        idt_48                # load idt with 0,0
  6.         lgdt        gdt_48                # load gdt with whatever appropriate
复制代码
这里会加载idt和gdt。以gdt为例解释一下:
  1. gdt:
  2.         .word        0,0,0,0                # dummy
  3.         .word        0x07FF                # 8Mb - limit=2047 (2048*4096=8Mb),代码段描述符
  4.         .word        0x0000                # base address=0,数据段描述符
  5.         .word        0x9A00                # code read/exec
  6.         .word        0x00C0                # granularity=4096, 386
  7.         .word        0x07FF                # 8Mb - limit=2047 (2048*4096=8Mb)
  8.         .word        0x0000                # base address=0
  9.         .word        0x9200                # data read/write
  10.         .word        0x00C0                # granularity=4096, 386
  11. gdt_48: # 注意是小端序,0x800在低16位,0x9在高16位
  12.         .word        0x800                        # gdt limit=2048, 256 GDT entries
  13.         .word   512+gdt, 0x9                # gdt base = 0X9xxxx,
  14.         # 512+gdt is the real gdt after setup is moved to 0x9020 * 0x10
复制代码
gdt_48 的高32位 为 gdt 在内存中的地址(gdt是setup文件的偏移,因为setup在内存中的起始位置为0x9020,所以要加上0x9020)
ds 寄存器里存储的值,在实模式下叫做段基址(段基址左移4位加上偏移得到物理地址),在保护模式下叫段选择子。段选择子里存储着段描述符的索引。
 

 通过段描述符索引,可以从全局描述符表 gdt 中找到一个段描述符,段描述符里存储着段基址。
 

 段基址取出来,再和偏移地址相加,就得到了物理地址,整个过程如下:
 

  1. inb     $0x92, %al        # open A20 line(Fast Gate A20).
  2. orb     $0b00000010, %al
  3. outb    %al, $0x92
复制代码
打开A20地址线。这是为了兼容20位模式,如果不打开,即使有32位地址线,高于20位的位也会被丢掉。
接下来是对可编程中断控制器 8259 芯片进行的编程。
因为中断号是不能冲突的, Intel 把 0 到 0x19 号中断都作为保留中断,比如 0 号中断就规定为除零异常,软件自定义的中断都应该放在这之后,但是 IBM 在原 PC 机中搞砸了,跟保留中断号发生了冲突,以后也没有纠正过来,所以我们得重新对其进行编程,不得不做,却又一点意思也没有。这是 Linus 在上面注释上的原话。
  1. mov        %cr0, %eax        # get machine status(cr0|MSW)
  2. bts        $0, %eax        # turn on the PE-bit
  3. mov        %eax, %cr0        # protection enabled
复制代码
启用保护模式(将cr0的第0位置为1)
  1. # segment-descriptor        (INDEX:TI:RPL)
  2.         .equ        sel_cs0, 0x0008
  3.         # select for code segment 0 (  001:0 :00)
  4.         ljmp        $sel_cs0, $0        # jmp offset 0 of code segment 0 in gdt
复制代码
对照段选择子的结构,可以知道 描述符索引值是 1,也就是要去 全局描述符表(gdt) 中找第一项段描述符。这里取的就是代码段描述符,段基址是 0,偏移也是 0,那加一块就还是 0,所以最终这个跳转指令,就是跳转到内存地址的 0 地址处,开始执行。就是操作系统全部代码的 system 这个大模块的起始处。
head
  1. pg_dir: # 页目录在0地址处,会覆盖掉执行过的代码
  2. .globl startup_32
  3. startup_32:
  4. movl $0x10,%eax
  5. mov %ax,%ds
  6. mov %ax,%es
  7. mov %ax,%fs
  8. mov %ax,%gs
  9. lss stack_start,%esp
复制代码
再往下连续五个 mov 操作,分别给 ds、es、fs、gs 这几个段寄存器赋值为 0x10,根据段描述符结构解析,表示这几个段寄存器的值为指向全局描述符表中的2号段描述符,也就是数据段描述符。
最后 lss 指令相当于让 ss:esp 这个栈顶指针指向了 _stack_start 这个标号的位置。
这个 stack_start 标号定义在了 sched.c 里:
  1. long user_stack[4096 >> 2];
  2. struct{  
  3.         long *a;
  4.         short b;
  5. } stack_start = { &user_stack[4096 >> 2], 0x10 };
复制代码
stack_start 结构中的高位 16 字节是 0x10,将会赋值给 ss 栈段寄存器,低位 32 字节是 user_stack 这个数组的最后一个元素的地址值,将其赋值给 esp 寄存器。
赋值给 ss 的 0x10 仍然按照保护模式下的段选择子去解读,其指向的是全局描述符表中的第二个段描述符(数据段描述符),段基址是 0。
  1. call setup_idt
  2. call setup_gdt
  3. movl $0x10,%eax                # reload all the segment registers
  4. mov %ax,%ds                # after changing gdt. CS was already
  5. mov %ax,%es                # reloaded in 'setup_gdt'
  6. mov %ax,%fs
  7. mov %ax,%gs
  8. lss stack_start,%esp
复制代码
重新设置idt和gdt,因为原来的是在setup中的,这块地方接下来要被缓冲区覆盖掉。所以这里重新将其设置在head中。因为重新设置了gdt,所以还要重新执行mov刷新一遍才能生效。
  1. ...
  2.         jmp after_page_tables
  3. ...
  4. after_page_tables:
  5.         pushl $0                # These are the parameters to main :-)
  6.         pushl $0
  7.         pushl $0
  8.         pushl $L6                # return address for main, if it decides to.
  9.         pushl $main
  10.         jmp setup_paging
  11. L6:
  12.         jmp L6                        # main should never return here, but
  13. .align 2
  14. setup_paging:
  15.         movl $1024*5,%ecx        /* 5 pages - pg_dir(页目录)占一页,4 个页表分别占一页 */
  16.         xorl %eax,%eax
  17.         xorl %edi,%edi                 /* pg_dir is at 0x000 */
  18.         cld;rep;stosl        /* 将开头的5页内存清零 */
  19.         movl $pg0+7,pg_dir         /* set present r/w bit/user*/
  20.         movl $pg1+7,pg_dir+4 /* 这里加7是为了将最低3位置1,即页存在,用户可读写*/
  21.         movl $pg2+7,pg_dir+8
  22.         movl $pg3+7,pg_dir+12
  23.         movl $pg3+4092,%edi
  24. .org 0x1000 pg0:  
  25. .org 0x2000 pg1:  
  26. .org 0x3000 pg2:  
  27. .org 0x4000 pg3:  
  28. .org 0x5000
复制代码
setup_paging 会初始化分页机制,也就是设置好页目录和页表。注意 pg_dir 在 0地址,也就是将之前执行的代码覆盖掉,作为页目录,存储了四个页目录项。一个页表包含1024个页表项,1页为4KB,因此16M 的地址空间可以用 1 个页目录表 + 4 个页表搞定。
  1.         movl $pg3+4092,%edi     /* 从最后一个页表的最后一个页表项开始 */
  2.         movl $0xfff007,%eax                /*  16Mb - 4096 + 7 (r/w user,p) */
  3.         std             /* 向低地址遍历 */
  4. 1:        stosl                        /* fill pages backwards - more efficient :-) */
  5.         subl $0x1000,%eax
  6.         jge 1b
  7.         cld
复制代码
这一步通过一个循环来填充页表项,使得线性地址和对应的物理地址一样。
  1.         xorl %eax,%eax                /* pg_dir is at 0x0000 */
  2.         movl %eax,%cr3                /* cr3 - page directory start */
  3.         movl %cr0,%eax
  4.         orl $0x80000000,%eax
  5.         movl %eax,%cr0                /* set paging (PG) bit */
复制代码
这一步设置了页目录的起始地址(存储在cr3寄存器),并且设置cr0的最高位为1以开启分页。
  1.         ret                        /* this also flushes prefetch-queue */
复制代码
ret会跳转到main函数。这是怎么实现的呢?注意到在 jmp setup_paging 之前压入了5个参数,实际上这是模拟call指令的压栈过程,因此ret后pop出栈顶作为返回地址,即可跳转到main函数执行。
  1.         pushl $0                # These are the parameters to main :-)
  2.         pushl $0
  3.         pushl $0
  4.         pushl $L6                # return address for main, if it decides to.
  5.         pushl $main
  6.         jmp setup_paging
复制代码
main

内存初始化
  1. void main(void) /* This really IS void, no error here. */
  2. {         /* The startup routine assumes (well, ...) this */
  3.         /* Interrupts are still disabled. Do necessary setups, then enable them */
  4.         ROOT_DEV = ORIG_ROOT_DEV;
  5.         drive_info = DRIVE_INFO;
  6.         // EXT_MEM_K 是之前在setup中获取和设置的
  7.         // EXT_MEM_K 存储的是系统从1MB开始的扩展内存数值,单位是KB,所以和以字节为单位的1MB相加时需要左移10位。
  8.         memory_end = (1<<20) + (EXT_MEM_K<<10); // 忽略不到4KB(1页)的内存
  9.         memory_end &= 0xfffff000;
  10.         // 如果内存超过16MB,则按照16MB计算
  11.         if (memory_end > 16*1024*1024)
  12.                 memory_end = 16*1024*1024;
  13.         // 如果内存大于12MB则缓冲区末端为4MB
  14.         if (memory_end > 12*1024*1024)
  15.                 buffer_memory_end = 4*1024*1024;
  16.         // 如果内存大于6MB则缓冲区末端为2MB
  17.         else if (memory_end > 6*1024*1024)
  18.                 buffer_memory_end = 2*1024*1024;
  19.         // 剩下的情况,也就是内存为0MB---6MB,则缓冲区末端为1MB
  20.         else
  21.                 buffer_memory_end = 1*1024*1024;
  22.         // 主内存起始地址 = 缓冲区末端
  23.         main_memory_start = buffer_memory_end;
  24.         ...
  25. }
复制代码
就是对内存分页,mem_map这个数组的每一项管理一页。

以上图为例:

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


  • trap_init:给0到48号中断设置中断处理函数
  1. void mem_init(long start_mem, long end_mem)
  2. {
  3.         int i;
  4.         HIGH_MEMORY = end_mem;
  5.         for (i=0 ; i<PAGING_PAGES ; i++)
  6.                 mem_map[i] = USED;
  7.         i = MAP_NR(start_mem);
  8.         end_mem -= start_mem;
  9.         end_mem >>= 12;
  10.         while (end_mem-->0)
  11.                 mem_map[i++]=0;
  12. }
复制代码
TSS 叫任务状态段,就是保存和恢复进程的上下文的,所谓上下文,其实就是各个寄存器的信息而已,这样进程切换的时候,才能做到保存和恢复上下文,继续执行。
  1. void trap_init(void)
  2. {
  3.         int i;
  4.         set_trap_gate(0,&divide_error);
  5.         set_trap_gate(1,&debug);
  6.         set_trap_gate(2,&nmi);
  7.         set_system_gate(3,&int3);        /* int3-5 can be called from all */
  8.         set_system_gate(4,&overflow);
  9.         set_system_gate(5,&bounds);
  10.         set_trap_gate(6,&invalid_op);
  11.         set_trap_gate(7,&device_not_available);
  12.         set_trap_gate(8,&double_fault);
  13.         set_trap_gate(9,&coprocessor_segment_overrun);
  14.         set_trap_gate(10,&invalid_TSS);
  15.         set_trap_gate(11,&segment_not_present);
  16.         set_trap_gate(12,&stack_segment);
  17.         set_trap_gate(13,&general_protection);
  18.         set_trap_gate(14,&page_fault); // 缺页中断
  19.         set_trap_gate(15,&reserved);
  20.         set_trap_gate(16,&coprocessor_error);
  21.         for (i=17;i<48;i++)
  22.                 set_trap_gate(i,&reserved);
  23.         set_trap_gate(45,&irq13);
  24.         outb_p(inb_p(0x21)&0xfb,0x21);
  25.         outb(inb_p(0xA1)&0xdf,0xA1);
  26.         set_trap_gate(39,&parallel_interrupt);
  27. }
复制代码
而 LDT 叫局部描述符表,是与 GDT 全局描述符表相对应的,内核态的代码用 GDT 里的数据段和代码段,而用户进程的代码用每个用户进程自己的 LDT 里的数据段和代码段。
每个进程用一个 task_struct 表示,里面就有 ldt 和 tss 两个成员。ldt包含三项,分别为0、cs(代码段)、ds&ss(数据段)
  1. #define sti() __asm__ ("sti"::)
复制代码

缓冲区初始化

缓冲区被分成一个个1024byte的块,每个块对应一个buffer_head
  1. /*
  2. * The request-struct contains all necessary data
  3. * to load a nr of sectors into memory
  4. */
  5. struct request request[NR_REQUEST];
复制代码
  1. /*
  2. * Ok, this is an expanded form so that we can use the same
  3. * request for paging requests when that is implemented. In
  4. * paging, 'bh' is NULL, and 'waiting' is used to wait for
  5. * read/write completion.
  6. */
  7. struct request {
  8.         int dev;                /* 设备号,-1 表示无请求 */
  9.         int cmd;                /* READ or WRITE */
  10.         int errors;
  11.         unsigned long sector; /* 起始扇区 */
  12.         unsigned long nr_sectors; /* 扇区数 */
  13.         char * buffer; /* 数据缓冲区,读盘后数据放在内存中的位置 */
  14.         struct task_struct * waiting; /* 哪个进程发起的请求 */
  15.         struct buffer_head * bh; /* 缓冲区头指针 */
  16.         struct request * next; /* 链表,指向下一个 */
  17. };
复制代码
宏展开:
  1. void blk_dev_init(void)
  2. {
  3.         int i;
  4.         for (i=0 ; i<NR_REQUEST ; i++) {
  5.                 request[i].dev = -1;
  6.                 request[i].next = NULL;
  7.         }
  8. }
复制代码
系统调用统一通过 int 0x80 中断来进入,具体调用这个表里的哪个功能函数,就由 eax 寄存器传过来,这里的值是个数组索引的下标,通过这个下标就可以找到在 sys_call_table 这个数组里的具体函数。
  1. static void time_init(void)
  2. {
  3.         struct tm time;
  4.         do {
  5.                 time.tm_sec = CMOS_READ(0);
  6.                 time.tm_min = CMOS_READ(2);
  7.                 time.tm_hour = CMOS_READ(4);
  8.                 time.tm_mday = CMOS_READ(7);
  9.                 time.tm_mon = CMOS_READ(8);
  10.                 time.tm_year = CMOS_READ(9);
  11.         } while (time.tm_sec != CMOS_READ(0));
  12.         BCD_TO_BIN(time.tm_sec);
  13.         BCD_TO_BIN(time.tm_min);
  14.         BCD_TO_BIN(time.tm_hour);
  15.         BCD_TO_BIN(time.tm_mday);
  16.         BCD_TO_BIN(time.tm_mon);
  17.         BCD_TO_BIN(time.tm_year);
  18.         time.tm_mon--;
  19.         startup_time = kernel_mktime(&time);
  20. }
复制代码
linux/sys.h 中可以找到 sys_call_table
  1. #define CMOS_READ(addr) ({ \
  2. outb_p(0x80|addr,0x70); \
  3. inb_p(0x71); \
  4. })
复制代码
如果是fork,则会调用到sys_fork
  1. void sched_init(void)
  2. {
  3.         int i;
  4.         struct desc_struct * p;
  5.         if (sizeof(struct sigaction) != 16)
  6.                 panic("Struct sigaction MUST be 16 bytes");
  7.         // 设置init_task的TSS和LDT
  8.         set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
  9.         set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
  10.         p = gdt+2+FIRST_TSS_ENTRY;
  11.         // 余下的项清0
  12.         for(i=1;i<NR_TASKS;i++) {
  13.                 task[i] = NULL;
  14.                 p->a=p->b=0;
  15.                 p++;
  16.                 p->a=p->b=0;
  17.                 p++;
  18.         }
  19. /* Clear NT, so that we won't have troubles with that later on */
  20.         __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
  21.         ltr(0);
  22.         lldt(0);
  23.         // 设置定时器
  24.         outb_p(0x36,0x43);                /* binary, mode 3, LSB/MSB, ch 0 */
  25.         outb_p(LATCH & 0xff , 0x40);        /* LSB */
  26.         outb(LATCH >> 8 , 0x40);        /* MSB */
  27.         // 设置时钟中断处理程序
  28.         set_intr_gate(0x20,&timer_interrupt);
  29.         // 启用时钟中断
  30.         outb(inb_p(0x21)&~0x01,0x21);
  31.         // 设置系统调用处理函数
  32.         set_system_gate(0x80,&system_call);
  33. }
复制代码
  1. struct tss_struct {
  2.         long        back_link;        /* 16 high bits zero */
  3.         long        esp0;
  4.         long        ss0;                /* 16 high bits zero */
  5.         long        esp1;
  6.         long        ss1;                /* 16 high bits zero */
  7.         long        esp2;
  8.         long        ss2;                /* 16 high bits zero */
  9.         long        cr3;
  10.         long        eip;
  11.         long        eflags;
  12.         long        eax,ecx,edx,ebx;
  13.         long        esp;
  14.         long        ebp;
  15.         long        esi;
  16.         long        edi;
  17.         long        es;                /* 16 high bits zero */
  18.         long        cs;                /* 16 high bits zero */
  19.         long        ss;                /* 16 high bits zero */
  20.         long        ds;                /* 16 high bits zero */
  21.         long        fs;                /* 16 high bits zero */
  22.         long        gs;                /* 16 high bits zero */
  23.         long        ldt;                /* 16 high bits zero */
  24.         long        trace_bitmap;        /* bits: trace 0, bitmap 16-31 */
  25.         struct i387_struct i387;
  26. };
复制代码
copy_mem 最后进行了 copy_page_tables ,将老进程的页表拷贝给新进程,让新旧进程共享同一份物理地址空间
  1. struct task_struct {
  2. /* these are hardcoded - don't touch */
  3.         long state;        /* -1 unrunnable, 0 runnable, >0 stopped */
  4.         long counter;
  5.         long priority;
  6.         long signal;
  7.         struct sigaction sigaction[32];
  8.         long blocked;        /* bitmap of masked signals */
  9. /* various fields */
  10.         int exit_code;
  11.         unsigned long start_code,end_code,end_data,brk,start_stack;
  12.         long pid,father,pgrp,session,leader;
  13.         unsigned short uid,euid,suid;
  14.         unsigned short gid,egid,sgid;
  15.         long alarm;
  16.         long utime,stime,cutime,cstime,start_time;
  17.         unsigned short used_math;
  18. /* file system info */
  19.         int tty;                /* -1 if no tty, so it must be signed */
  20.         unsigned short umask;
  21.         struct m_inode * pwd;
  22.         struct m_inode * root;
  23.         struct m_inode * executable;
  24.         unsigned long close_on_exec;
  25.         struct file * filp[NR_OPEN];
  26. /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
  27.         struct desc_struct ldt[3];
  28. /* tss for this task */
  29.         struct tss_struct tss;
  30. };
复制代码
shell 的到来

由于 fork 函数一调用,就又多出了一个进程,子进程(进程 1)会返回 0,父进程(进程 0)返回子进程的 ID,所以 init 函数只有进程 1 才会执行。
  1. struct buffer_head {
  2.         char * b_data;                        /* pointer to data block (1024 bytes) */
  3.         unsigned long b_blocknr;        /* block number */
  4.         unsigned short b_dev;                /* device (0 = free) */
  5.         unsigned char b_uptodate;
  6.         unsigned char b_dirt;                /* 0-clean,1-dirty */
  7.         unsigned char b_count;                /* users using this block */
  8.         unsigned char b_lock;                /* 0 - ok, 1 -locked */
  9.         struct task_struct * b_wait;
  10.         struct buffer_head * b_prev;
  11.         struct buffer_head * b_next;
  12.         struct buffer_head * b_prev_free;
  13.         struct buffer_head * b_next_free;
  14. };
复制代码
setup 是个系统调用,会通过中断最终调用到 sys_setup 函数
setup 传入的drive_info 是来自内存 0x90080 的数据,这部分是由之前 setup.s 程序将硬盘 1 的参数信息放在这里了,包括柱面数、磁头数、扇区数等信息。
  1. extern int end; // end 是链接器计算出的内核代码的末尾地址
  2. struct buffer_head * start_buffer = (struct buffer_head *) &end;
  3. void buffer_init(long buffer_end)
  4. {
  5.         struct buffer_head * h = start_buffer;
  6.         void * b;
  7.         int i;
  8.         if (buffer_end == 1<<20)
  9.                 b = (void *) (640*1024);
  10.         else
  11.                 b = (void *) buffer_end;
  12.         // 缓冲区结尾侧的 b 每次循环 -1024,也就是一页的值,缓冲区开头侧的 h 每次循环 +1(一个 buffer_head 大小的内存),直到碰一块为止。
  13.         while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {
  14.                 h->b_dev = 0;
  15.                 h->b_dirt = 0;
  16.                 h->b_count = 0;
  17.                 h->b_lock = 0;
  18.                 h->b_uptodate = 0;
  19.                 h->b_wait = NULL;
  20.                 h->b_next = NULL;
  21.                 h->b_prev = NULL;
  22.                 h->b_data = (char *) b;
  23.                 h->b_prev_free = h-1;
  24.                 h->b_next_free = h+1;
  25.                 h++;
  26.                 NR_BUFFERS++;
  27.                 if (b == (void *) 0x100000)
  28.                         b = (void *) 0xA0000;
  29.         }
  30.         h--;
  31.         free_list = start_buffer;
  32.         free_list->b_prev_free = h;
  33.         h->b_next_free = free_list;
  34.         for (i=0;i<NR_HASH;i++)
  35.                 hash_table[i]=NULL;
  36. }
复制代码
setup 方法中的最后一个函数 mount_root,加载根文件系统。有了根文件系统之后,操作系统才能从一个根儿开始找到所有存储在硬盘中的文件,所以它是文件系统的基石,很重要。
从整体上说,它就是要把硬盘中的数据,以文件系统的格式进行解读,加载到内存中设计好的数据结构,这样操作系统就可以通过内存中的数据,以文件系统的方式访问硬盘中的一个个文件了。
  1. void hd_init(void)
  2. {
  3.         blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; // 初始化硬盘的请求处理函数
  4.         set_intr_gate(0x2E,&hd_interrupt); // 设置硬盘中断的处理函数
  5.         // 允许硬盘控制器发送中断请求信号
  6.         outb_p(inb_p(0x21)&0xfb,0x21);
  7.         outb(inb_p(0xA1)&0xbf,0xA1);
  8. }
复制代码
首先硬盘中的文件系统,无非就是硬盘中的一堆数据,我们按照一定格式去解析罢了。Linux-0.11 中的文件系统是 MINIX 文件系统,它就长成这个样子。

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

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

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

execve
  1. .align 2
  2. timer_interrupt:
  3.         push %ds                # save ds,es and put kernel data space
  4.         push %es                # into them. %fs is used by _system_call
  5.         push %fs
  6.         pushl %edx                # we save %eax,%ecx,%edx as gcc doesn't
  7.         pushl %ecx                # save those across function calls. %ebx
  8.         pushl %ebx                # is saved as we use that in ret_sys_call
  9.         pushl %eax
  10.         movl $0x10,%eax
  11.         mov %ax,%ds
  12.         mov %ax,%es
  13.         movl $0x17,%eax
  14.         mov %ax,%fs
  15.         incl jiffies
  16.         movb $0x20,%al                # EOI to interrupt controller #1
  17.         outb %al,$0x20
  18.         movl CS(%esp),%eax  # 发生中断时处理器自动压入CS,这里读取出来,检查CPL(current privilege level)
  19.         andl $3,%eax                # %eax is CPL (0 or 3, 0=supervisor)
  20.         pushl %eax                        # CPL 作为参数
  21.         call do_timer                # 'do_timer(long CPL)' does everything from
  22.         addl $4,%esp                # task switching to accounting ...
  23.         jmp ret_from_sys_call
复制代码
init 进程接着fork出一个新进程,新进程通过 close 和 open 函数,将 0 号文件描述符指向的标准输入 /dev/tty0 更换为指向 /etc/rc 文件
接下来进程 2 就将变得不一样了,会通过一个 execve 函数调用,使自己摇身一变,成为 /bin/sh 程序继续运行!
  1. void do_timer(long cpl)
  2. {
  3.         extern int beepcount;
  4.         extern void sysbeepstop(void);
  5.         if (beepcount)
  6.                 if (!--beepcount)
  7.                         sysbeepstop();
  8.         if (cpl)
  9.                 current->utime++;
  10.         else
  11.                 current->stime++;
  12.         ...
  13.         if (current_DOR & 0xf0)
  14.                 do_floppy_timer();
  15.         if ((--current->counter)>0) return; // 时间片未到0,返回
  16.         current->counter=0;
  17.         if (!cpl) return; // 如果当前是内核态则不调度
  18.         schedule(); // 时间片到0,且为用户模式,进行调度。
  19. }
复制代码
  1. #define FIRST_TASK task[0]
  2. #define LAST_TASK task[NR_TASKS-1]
  3. void schedule(void)
  4. {
  5.         int i,next,c;
  6.         struct task_struct ** p;
  7. /* check alarm, wake up any interruptible tasks that have got a signal */
  8.         for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
  9.                 if (*p) {
  10.                         if ((*p)->alarm && (*p)->alarm < jiffies) {
  11.                                         (*p)->signal |= (1<<(SIGALRM-1));
  12.                                         (*p)->alarm = 0;
  13.                                 }
  14.                                 // (*p)->signal 表示待处理的信号
  15.                                 // ~(_BLOCKABLE & (*p)->blocked)) 表示未被屏蔽的信号
  16.                                 // TASK_INTERRUPTIBLE: 处于睡眠状态,并且等待某个信号
  17.                         if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
  18.                         (*p)->state==TASK_INTERRUPTIBLE)
  19.                                 (*p)->state=TASK_RUNNING;
  20.                 }
  21. /* this is the scheduler proper: */
  22.         while (1) {
  23.                 c = -1; // 所有进程剩余时间片的最大值
  24.                 next = 0; // 最大剩余时间片进程的索引
  25.                 i = NR_TASKS;
  26.                 p = &task[NR_TASKS];
  27.                 while (--i) {
  28.                         if (!*--p)
  29.                                 continue;
  30.                         if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
  31.                                 c = (*p)->counter, next = i;
  32.                 }
  33.                 if (c) break; // 如果存在一个剩余时间片不为0的任务,则break,否则设置所有任务的剩余时间片
  34.                 for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
  35.                         if (*p)
  36.                                 (*p)->counter = ((*p)->counter >> 1) +
  37.                                                 (*p)->priority;
  38.         }
  39.         // 切换到目标进程
  40.         switch_to(next);
  41. }
复制代码
  1. #define FIRST_TSS_ENTRY 4
  2. #define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
  3. #define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
  4. // FIRST_TSS_ENTRY<<3表示左移3位,因为TI和RPL总共占3位
  5. // n<<4,实际上索引加上 n<<1,因为一个进程占一个TSS和一个LDT
  6. #define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
  7. /*
  8. *        switch_to(n) should switch tasks to task nr n, first
  9. * checking that n isn't the current task, in which case it does nothing.
  10. * This also clears the TS-flag if the task we switched to has used
  11. * tha math co-processor latest.
  12. */
  13. #define switch_to(n) {\
  14. struct {long a,b;} __tmp; \
  15. __asm__("cmpl %%ecx,current\n\t" # 先比较是不是要切换到当前任务 \
  16.         "je 1f\n\t" # 如果是就什么都不做 \
  17.         "movw %%dx,%1\n\t" # 把TSS赋给__tmp.b \
  18.         "xchgl %%ecx,current\n\t" # 交换 ecx 和 current \
  19.         "ljmp *%0\n\t" # 将__tmp.b作为段选择子 \
  20.         "cmpl %%ecx,last_task_used_math\n\t" \
  21.         "jne 1f\n\t" \
  22.         "clts\n" \
  23.         "1:" \
  24.         ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
  25.         "d" (_TSS(n)),"c" ((long) task[n])); \
  26. }
复制代码
来源:https://www.cnblogs.com/iku-iku-iku/p/17991016
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具