linux0.11 init函数,linux0.11启动与初始化

简单描述Linux0.11的启动与初始化过程。

启动过程中需要关注:IDT, GDT, LDT, TSS, 页表, 堆栈这些数据。

一:启动过程

启动的代码文件为bootsect.s、setup.s、head.s

bootsect.s也就是启动扇区的代码。这段代码主要是将setup.s和head.s中的内容读入内存的相应区域。然后开始执行setup.s

setup.s

1:使用BIOS中断来获得相关系统信息:内存大小,硬盘分区信息,显示卡信息

2:将head.s代码拷贝到内存地址为0X0000的地方。

3:加载idt表和gdt表地址

4:开启A20地址线,只有开启它了才能访问高于1M地址的内存

5:重新设定中断控制器。这之后以前的BIOS中断号就没用了。

6:置位CR0寄存器的最后一位进入保护模式, 然后用jmpi 0, 8指令跳转到地址0x08:0x0000处开始执行,也就是head.s的起始代码处。

这里设定的idt表全部为空,也即这时并不处理任何中断。

gdt表中有三个描述符:0--NULL, 1--内核代码段, 2--内核数据段描述符。

此时内核代码段与内核数据段:基地址为0x00000000, 限长为:8MB

head.s

1:将堆栈设定在static_stack处,堆栈大小为1KB

2:重新设定设定IDT和GDT,此时全部IDT的都设置为ignore_int,即仍然忽略中断。

GDT中包含256个描述符。

0--NULL, 1--内核代码段, 2--内核数据段, 3--保留, 4--进程0的TSS段, 5--进程0的LDT段, 6--进程1的TSS段, 7--进程1的LDT段......

可见系统的GDT中为每个进程都设定了一个TSS和LDT段。

内核代码段和内核数据段:基地址(0x00000000),限长(16MB)

3:设定分页(由于内存管理部分目前没看到,因此关于进程的页表如何设定暂不明白,这里是内核的页表)

在0x00000000处的第一页存放“页目录”,随后存放4页“页表”,每个页表对应于页目录中的一项。

设定的页表,要求线性地址等于物理地址。

4个页表能映射16MB的物理空间,因此这16MB的物理内存地址与线性地址是相同的。(0.11的内核没有PAGE_OFFSET)

4:开始执行init/main.c中的main函数。

方式如下:

pushl $_main                          # 将main函数地址入栈

jmp setup_paging                   # 开始分页

......

setup_paging:

.......

ret                                      # 分页完成后,用ret指令弹出堆栈中的main函数入口地址,并开始执行main函数。

好像内核代码中常用这种弹出堆栈的方式来执行其他的函数

二:main函数

在启动过程中设定好GDT和页表后,开始在main函数中设定其他的内容。

主要是:设定IDT,设备初始化,创建进程0,fork出进程1,用进程1来执行init

main.c中主要的初始化函数是:trap_init,sched_init

trap_init中调用set_trap_gate来设定相应的中断描述符表。

下面以0号中断为例,描述其实现过程

1:调用set_trap_gate(0, &divide_error)

这个宏定义用来设定IDT表中的第0项的陷阱门描述符。

#define set_trap_gate(n, addr)   _set_gate(&idt[n], 15, 0, addr)

在_set_gate中:&idt[n]为第n个描述符的地址, 15为描述符的类型(陷阱门),0为描述符的权限(最高权限),addr为要调用的代码的地址。

_set_gate宏会调用相应的汇编指令,在&idt[n]处写入8字节的描述符。

2:divide_error的实现

该函数是以汇编码的形式实现。与该函数对应有一个处理函数do_divide_error(用C语言实现)

对其他的多数陷阱门处理方式也是如此,有一个汇编方式实现的,还有一个C语言实现的处理函数。

当中断0发生时,先调用divide_error,该函数再调用do_divide_error。

void do_divide_error(long esp, long error_code)

上面为函数原型。第一个参数为出错时的代码地址的指针,第二个参数是错误码。(有些中断不产生错误码,则错误码设成0)

因此在divide_error的汇编代码中,主要的功能就是将出错地址的指针和错误码这两个参数传递给do_divide_error函数,

同时将目前的数据段设定为内核数据段。

sched_init函数

该函数设定了进程0的TSS和LDT描述符,并将它们的选择子加载进了TR和LDTR寄存器。

另外该函数设定了时钟中断和系统调用

这里主要说下系统调用的执行,以fork函数为例。

1:在sched_init中初始化系统调用

set_system_gate(0x80, &system_call)

#define set_system_gate(0x80, &system_call)  _set_gate(&idt[n], 15, 3, addr)

可见系统调用也是一个陷阱门。区别是权限值为3, 因此用户进程能通过int 0x80的中断进入内核,执行系统调用。

2:每个系统调用都有一个对应的编号,fork为第二个系统调用,因此fork的调用号为2。

当执行fork函数的时候,它会用int 0x80来调用system_call函数。

此时fork的调用号被放入eax寄存器中。

3:全部的系统调用函数指针都保存在数组sys_call_table中。

在system_call函数中会执行指令

call _sys_call_table(,%eax, 4)来跳转到eax指定的系统函数代码上,对fork来说就是sys_fork函数。

4:system_call

i) system_call首先将相应的寄存器入栈。

pushl %edx

pushl %ecx

pushl %ebx             这三个寄存器对应了相应的系统调用函数的3个参数

因此0.11中,系统函数最多只能有3个参数。

ii)将ds和es设定为内核数据段,将fs设定为用户进程的数据段,需要用户进程的数据时,可使用fs来访问

iii)用call _sys_call_table(,%eax, 4)来执行系统调用

iii)检查当前进程是否处于可执行状态,检查当前进程的时间片是否用完,相应的执行schedule

iv)最后是对进程信号的处理。(信号机制没看完)

三:进入用户态

在main函数中相关初始化后,main以进程0的身份进入用户态。

然后调用fork函数,创建进程1,进程1调用init函数

init函数加载根文件系统,运行初始化配置命令,然后执行shell程序,这样便进入了命令行窗口。

0.11内核中,每个进程都有一个TSS段和一个LDT段,它们保存在进程描述符strut task_struct结构中。

相应段的描述符保存在GDT表中。

在LDT段中,有3个LDT描述符,0--NULL, 1--进程代码段, 2--进程数据段。

进程n的代码段和数据段:基地址=n*64MB,限长=64MB。(进程0和1的限长为640KB)

因此系统中最多有64个进程。

进程0的task_struct为INIT_TASK,进程0的TSS和LDT描述符在sched_init中设定。

main函数调用move_to_user_mode函数来执行进程0,进入用户态。

0.11内核中所有进程都是属于用户态,不像之后的Linux内核里有内核线程。

move_to_user_mode函数

此函数使用iret返回的方式,从内核态进入用户态。

+------------+

+   ss      +             pushl    $0x17

+------------+

+   esp    +             pushl %%eax                #eax中保存了esp

+------------+

+  eflags  +             pushfl

+------------+

+  cs       +             pushl $0x0f

+------------+

+   eip     +             pushl $1f                       #目的代码的偏移地址

+------------+

首先采用上面的push指令,将相关的数据压入堆栈,然后执行iret将它们弹出堆栈。

于是进程0从堆栈中的cs:eip指向的代码开始执行。

四:fork函数

进程0执行fork函数创建出进程1.

1:调用get_free_page为进程描述符分配内存。

p = (struct task_struct *) get_free_page();

这一页内存,前面保存task_struct内存, 页尾为进程的内核栈,

当一个用户程序调用系统函数进入内核态后,系统函数执行时使用的栈就是这个。

2:设定进程的task_struct结构体

3:内存拷贝,将父亲进程的内存拷贝给新进程。

4:设定新进程在GDT中的TSS和LDT描述符

有关fork最主要的是弄明白了,为什么它可以“返回”两次。

1:调用fork时,CPU自动将父进程的返回地址入栈(即eip寄存器入栈)

2:创建子进程的task_struct后,将TSS段中的eax字段设成0,eip字段设成父进程的返回地址。

3:将子进程的状态设成TASK_RUNNING(就绪状态)

4:fork函数以子进程的pid返回。

5:等到执行schedule,调度到子进程时。会自动将子进程的TSS内容加载进寄存器。

因此这时CPU中eax寄存器值为0, eip为父进程的返回地址。所以子进程从fork函数的下一条指令开始执行,返回值在eax中,为0。

阅读(2793) | 评论(0) | 转发(0) |

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值