|
参考
参考闪客的系列,将开机到执行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 会帮我们把它放到内存里,并且跳过去执行。- _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 #从内存[si]处移动cx个字到[di]处
- 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 * 0x10
复制代码 gdt_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[4096 >> 2];
- struct{
- long *a;
- short b;
- } stack_start = { &user_stack[4096 >> 2], 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 0x5000
复制代码 setup_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_paging
复制代码 main
内存初始化
- 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[i] = USED;
- i = MAP_NR(start_mem);
- end_mem -= start_mem;
- end_mem >>= 12;
- while (end_mem-->0)
- mem_map[i++]=0;
- }
复制代码 TSS 叫任务状态段,就是保存和恢复进程的上下文的,所谓上下文,其实就是各个寄存器的信息而已,这样进程切换的时候,才能做到保存和恢复上下文,继续执行。- void trap_init(void)
- {
- int i;
- set_trap_gate(0,÷_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,¶llel_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[NR_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[i].dev = -1;
- request[i].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[i] = 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[32];
- 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[NR_OPEN];
- /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
- struct desc_struct ldt[3];
- /* 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[i]=NULL;
- }
复制代码 setup 方法中的最后一个函数 mount_root,加载根文件系统。有了根文件系统之后,操作系统才能从一个根儿开始找到所有存储在硬盘中的文件,所以它是文件系统的基石,很重要。
从整体上说,它就是要把硬盘中的数据,以文件系统的格式进行解读,加载到内存中设计好的数据结构,这样操作系统就可以通过内存中的数据,以文件系统的方式访问硬盘中的一个个文件了。- void hd_init(void)
- {
- blk_dev[MAJOR_NR].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[9] 块数组,表示这个文件或目录的具体数据占用了哪些块。其中块数组里,0~6 表示直接索引,7 表示一次间接索引,8 表示二次间接索引。当文件比较小时,比如只占用 2 个块就够了,那就只需要 zone[0] 和 zone[1] 两个直接索引即可。
再往后,就都是存放具体文件或目录实际信息的块了。如果是一个普通文件类型的 inode 指向的块,那里面就直接是文件的二进制信息。如果是一个目录类型的 inode 指向的块,那里面存放的就是这个目录下的文件和目录的 inode 索引以及文件或目录名称等信息。
init 接下来会调用open打开"/dev/tty0"文件- /* blk_dev_struct is:
- * do_request-address
- * next-request
- */
- struct blk_dev_struct blk_dev[NR_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_call
复制代码 init 进程接着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[0]
- #define LAST_TASK task[NR_TASKS-1]
- 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[NR_TASKS];
- 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[n])); \
- }
复制代码 来源:https://www.cnblogs.com/iku-iku-iku/p/17991016
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|