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,÷_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;/*
* 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]