漫谈 | 从52个思考题来看《Linux内核设计的艺术》

本文纯属学习笔记,为个人理解,内容正确性不能保证。访问请移步至(David’s Wikipedia) https://www.qingdujun.com/ ,这里有能“击穿”平行宇宙的乱序并行字节流…


1.为什么开始启动计算机的时候,执行的是BIOS代码而不是操作系统自身的代码?(P1)

  • 答:加电的一瞬间,计算机内存中,准确的说是RAM中,中空空如也,什么程序也没有。软盘里虽然有操作系统程序,但CPU的逻辑电路被设计为只能运行内存中的程序,没有能力直接从软盘运行操作系统。这就需要硬件主动加载0xffff0处的BIOS程序,由BIOS准备好中断向量表、中断服务程序,接着通过中断“int 0x19”将引导程序bootsect加载至内存,以及后续的一系列操作,最终操作系统自身代码才能位于内存中,被CPU执行。

2.为什么BIOS只加载了一个扇区,后续扇区却是由bootsect代码加载?为什么BIOS没有直接把所有需要加载的扇区都加载?(P6)

  • 答:BIOS和操作系统通常由不同的专业团队开发的,为了能协调工作,对BIOS而言,“约定”接到启动操作系统命令,“定位识别”只从启动扇区把代码加载至0x7c00(BOOTSEG)位置,至于该扇区内容是什么,一概不管。BIOS程序是固化在主板ROM中,ROM内容一般无法改变,为保证其正确性以及生产、维护成本,BIOS只做必要工作。

3.为什么BIOS把bootsect加载到0x07c00,而不是0x00000?加载后又马上挪到0x90000处,是何道理?为什么不一次加载到位?(p5-17)

  • 答:其一,0x07c00是历史约定。其二,0x000000为BIOS中断向量表位置,而后续一段时间内用的都还是BIOS中断,所以不能将其覆盖。其三,挪到0x90000处是操作系统内存规划行为,主要为了避免在内核system占据0x000000处时可能将0x07c00(bootsect)覆盖,造成在main中设置根设备时取不到正确数据。

4.bootsect、setup、head程序之间是怎么衔接的?给出代码证据。(P15,P26)

  • 答:A)bootsect跳转至setup程序:jmpi 0, SETUPSEG;
    解释:通过BIOS的“int 0x13”中断,找到bootsect自身的中断服务程序,将setup加载至SETUPSEG(0x90200)处。同样手法,将system加载至SYSSEG(0x10000)处。bootsect程序任务都已经完成。然后,通过“jmpi 0, SETUPSEG”跳转至setup程序的加载位置,此时CS:IP指向setup程序的第一条指令。
    B)setup跳转至head程序:jmpi 0, 8
    解释:setup通过BIOS提供的中断服务程序提取了系统数据,存储在原来的bootsect位置只保留最后2字节未被覆盖(0x901fc,根设备号)。接着,将IF至0,完成关中断操作。然后,将system移动到0x00000位置,此时head已经占据了0x00000处,同时BIOS中断向量表彻底被覆盖。为此,setup开始为保护模式做准备,设置GDT、IDT并用CPU中专用寄存器IDTR、GDTR看住。接着,打开A20,也就是32位寻址模式,再对可编程中断控制器8259A进行重新编程,并置PE位为1,即设定处理器工作方式为保护模式,以后根据GDT决定执行哪里的程序。最后,通过“jmpi 0,8”跳转到head。“0”表示段内偏移,“8(1000)”是保护模式下的段选择符,最后两位“00”表示内核态,第二位“0”表示GDT,第一位“1”表示GDT表中GDT[1]项(内核代码段),从该项中得知段基址为0x00000000。结合上述偏移0,可知最终跳转至0x0000000处,执行head程序。

5.setup程序的最后是jmpi 0,8 ,为什么这个8不能简单的当作阿拉伯数字8看待,究竟有什么内涵?(P25)

  • 答:此时,工作在32位保护模式下,“0”表示段内偏移,“8(1000)”是段选择符,需要当二进制来看。最后两位“00”表示内核态,如为“11”则表示用户态;第二位“0”表示GDT,如为“1”则表示LDT;最前面的“1”表示GDT [1]。最后从响应的位置(如,GDT[1])获取段基址、限长等内容。

6.保护模式在“保护”什么?它的“保护”体现在哪里?特权级的目的和意义是什么?分页有“保护”作用吗?(P436-P439、P443)

  • 答:其一,保护操作系统不受恶意侵害。其二,主要体现在利用保护和分页、特权级、中断等技术依托CPU提供的硬件机制,对进程调度、内存管理、文件系统等方面进行保护。其三,为了更好的管理资源并保护系统不受侵害,操作系统利用先机,以时间换取特权,先霸占所有特权;依托CPU提供的保护模式,着眼于“段”,在所有的段选择符最后两位标示特权级,禁止用户执行那些至关重要的指令。其四,对于分页来说,用户进程只能使用逻辑地址,而逻辑地址要经过内核转化为线性地址,实现了用户进程不可能访问内核地址,也不能进程间相互访问,起到保护作用。

7.在setup程序里曾经设置过gdt,为什么在head程序中将其废弃,又重新设置了一个?为什么设置两次,而不是一次搞好?(P33)

  • 答:原来GDT所在的位置是设计代码时在setup.s里面设置的数据,将来这个setup模块所在的内存位置会在设计缓冲区时被覆盖。如果不改变位置,将来GDT的内容肯定会被缓冲区覆盖掉,从而影响系统的运行。这样一来,将来整个内存空间中唯一安全的地方就是现在head.s所在的位置了。那么有没有可能在执行setup程序时直接把GDT的内容复制到head.s所在的位置呢?肯定不能。如果先复制GDT内容,后移动system模块,它就会被后者覆盖;如果先移动system模块,后复制GDT内容,它又会把head.s对应的程序覆盖,而这时head.s还没有执行。所以,无论如何,都要重新建立GDT。

8.进程0的task_struct在哪?具体内容是什么?(P70)

  • 答:内核数据段。具体内容包括状态、信号、pid、alarm、ldt、tss等管理该进程所需的数据。
    \linux0.11\include\linux\sched.h
    #define INIT_TASK \
    /* state etc */	{
            0,15,15, \
    /* signals */	0,{
           {
           },},0, \
    /* ec,brk... */	0,0,0,0,0,0, \
    /* pid etc.. */	0,-1,0,0,0, \
    /* uid etc */	0,0,0,0,0,0, \
    /* alarm */	0,0,0,0,0,0, \
    /* math */	0, \
    /* fs info */	-1,0022,NULL,NULL,NULL,0, \
    /* filp */	{
           NULL,}, \
    	{
            \
    		{
           0,0}, \
    /* ldt */	{
           0x9f,0xc0fa00}, \
    		{
           0x9f,0xc0f200}, \
    	}, \
    /*tss*/	{
           0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
    	 0,0,0,0,0,0,0,0, \
    	 0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
    	 _LDT(0),0x80000000, \
    		{
           } \
    	}, \
    }
    \linux0.11\kernel\sched.c
    struct task_struct *task[NR_TASKS] = {
          &(init_task.task), };
    

9.内核的线性地址空间是如何分页的?画出从0x000000开始的7个页(包括页目录表、页表所在页)的挂接关系图,就是页目录表的前四个页目录项、第一个个页表的前7个页表项指向什么位置?给出代码证据。(P39)

  • 答:图参考P39。
    注意,页目录表需指向全部页表;页表需要指向全部页;页目录表、页表本身也是页。
    \linux0.11\boot\head.s
    setup_paging:
    	movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */
    	xorl %eax,%eax
    	xorl %edi,%edi			/* pg_dir is at 0x000 */
    	cld;rep;stosl
    	movl $pg0+7,_pg_dir		/* set present bit/user r/w */
    	movl $pg1+7,_pg_dir+4		/*  --------- " " --------- */
    	movl $pg2+7,_pg_dir+8		/*  --------- " " --------- */
    	movl $pg3+7,_pg_dir+12		/*  --------- " " --------- */
    	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
    	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 */
    	ret			/* this also flushes prefetch-queue */	
    

10.在head程序执行结束的时候,在idt的前面有184个字节的head程序的剩余代码,剩余了什么?为什么要剩余?(P31、P36、P40)

  • 答:剩余内容0x054b8~0x05400处,包含了after_page_tables、ignore_int中断服务程序和setup_paging设置分页的代码。after_page_tables中压入了一些参数,为内核进入main函数跳转做准备,为了谨慎起见,设计者在栈中压入了L6,以使得系统可能出错时,返回L6处执行;ignore_int是IDT的默认初始化值,既可以防止无意中覆盖代码或数据而引起的逻辑混乱,也可以对开发过程中的误操作给出及时的提示;setup_paging在分页完成前不能被覆盖。

11.为什么不用call,而是用ret“调用”main函数?画出调用路线图,给出代码证据。(P42)

  • 答:参考课本P42页。
    \linux0.11\boot\head.s
    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
    				# just in case, we know what happens.
    

12.用文字和图说明中断描述符表是如何初始化的,可以举例说明(比如:set_trap_gate(0,&divide_error)),并给出代码证据。(P52、P55)

  • 答:对中断描述符表的初始化,就是将中断、异常处理的服务程序与IDT进行挂接,逐步重建中断服务体系。 set_trap_gate(0,&divide_error); //除零错误
    \linux0.11\include\asm\system.h
    #define set_trap_gate(n,addr) \
    	_set_gate(&idt[n],15,0,addr)
    #define _set_gate(gate_addr,type,dpl,addr) \
    __asm__ ("movw %%dx,%%ax\n\t" \
    	"movw %0,%%dx\n\t" \
    	"movl %%eax,%1\n\t" \
    	"movl %%edx,%2" \
    	: \
    	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
    	"o" (*((char *) (gate_addr))), \
    	"o" (*(4+(char *) (gate_addr))), \
    	"d" ((char *) (addr)),"a" (0x00080000))
    
    可以看出,n是0;gate_addr是&idt[0],也就是IDT的第一项中断描述符的地址;type是15;dpl(描述符特权级)是0;addr是中断服务程序divide_error(void)的入口地址。

13.在IA-32中,有大约20多个指令是只能在0特权级下使用,其他的指令,比如cli,并没有这个约定。奇怪的是,在Linux0.11中,3特权级的进程代码并不能使用cli指令,这是为什么?请解释并给出代码证据。 (P68、P79、P92)

  • 答:根据Intel Manual,cli和sti指令与CPL和EFLAGS[IOPL]有关。cli,如果CPL的权限高于等于EFLAGS中的IOPL的权限,即数值上CPL<=IOPL,则IF位清除为0,否则它不受影响。如果CPL大于当前程序或过程的IOPL,则产生保护模式异常。由于在内核中IOPL的值初始为0,且未经改变。INIT_TASK的TSS中设置了EFLAGS值,进程0又在move_to_user_mode中,继承了内核的EFLAGS。
    \linux0.11\include\linux\sched.h
    #define INIT_TASK \
    //..
    /*tss*/	{
         0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
    	 0,0,0,0,0,0,0,0, \     //eflags的值,决定了cli这类指令只能在0特权级使用
    	 0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
    	 _LDT(0),0x80000000, \
    		{
         } \
    	}, \
    }
    \linux0.11\include\asm\system.h
    #define move_to_user_mode() \
    __asm__ ("movl %%esp,%%eax\n\t" \
    	"pushl $0x17\n\t" \
    	"pushl %%eax\n\t" \
    	"pushfl\n\t" \   //eflags进栈
    	"pushl $0x0f\n\t" \
    	"pushl $1f\n\t" \
    	"iret\n" \
    	//..
    	:::"ax")
    
    而进程1在copy_process中TSS里,设置了EFLAGS的IOPL位为0。总之,通过设置IOPL,可以限制3特权级的进程代码使用cli。
    \linux0.11\kernel\fork.c
    int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
    		long ebx,long ecx,long edx,
    		long fs,long es,long ds,
    		long eip,long cs,long eflags,long esp,long ss)
    {
         
    	//…
    	p->tss.eip = eip;
    	p->tss.eflags = eflags;
    	p->tss.eax = 0;
    	//…
    	return last_pid;
    }
    

14.进程0的task_struct在哪?具体内容是什么?给出代码证据。

  • 答:同第8题。(题目重复)

15.在system.h里

\linux0.11\include\asm\system.h
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ 
  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值