思考题 上

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

p1

因为在启动加电时,操作系统本身还没有加载进内存,内存中是空的(或者说乱七八糟的东西?),无法进行执行.(CPU的逻辑电路被设定为只能运行内存中的程序) 而bios中的代码是写死的,所以可以直接跳转到biso处进行执行.

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

加载了一个扇区之后,操作系统就有能力继续加载后续的扇区了,这样做的原因大概是为了减小bios的大小? 或者提高速度? 或者给操作系统设计者更大的自由空间?

bootsect需要进行规划内存,(为啥bios不能规划呢? 因为取决于操作系统?不同的操作系统不一样),进行一些自定义的内容. 而且bios来的话,应该是比较慢的,全加载进去再执行的话,一个是慢,还有就是不灵活.所以linux采用的是边加载边执行的思路!

上面的回答大概应该都有一点

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

p8

感觉这就是个约定问题,就好像12345这样一样

挪到0x90000是因为操作系统设计者对内存的规划,原先加载到0x07c00是统一的,之后可以按照自己的分配来进行

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

Untitled

Untitled1

bootsect和setup的衔接

bootsect 把自己移位,然后先把setup加载到0x90200开始的四个扇区,又把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000 处

然后通过下面这条指令跳转到setup

jmpi 0,SETUPSEG; 0x9020 跳转到setup.s开始继续执行了!!! (此时还是实模式, 偏移地址+ 基地址, 0+ 0x9020*0x10 = 0x90200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
;移位
entry start
start: ;内存中0x07C00对应的就是这里
mov ax,#BOOTSEG ;0x07C00
mov ds,ax
mov ax,#INITSEG ;0x9000
mov es,ax ;进行复制,挪位置,把0x07c00 挪到0x9000
mov cx,#256
sub si,si
sub di,di
rep
movw ;移动一个字 两个字节 512 /2 -256
jmpi go,INITSEG ; 跳到新的位置,
;加载setup.s的四个扇区
load_setup: ; 加载4个扇区
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it bios的中断,读取磁盘
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
;加载head和剩余的操作系统
ok_load_setup:
! 把从硬盘第 6 个扇区开始往后的 240 个扇区,加载到内存 0x10000
mov ax,#SYSSEG。;0x1000
mov es,ax ! segment of 0x010000
call read_it
call kill_motor

seg cs
mov ax,root_dev
cmp ax,#0
jne root_defined
;跳转到setup.s
root_defined:
seg cs
mov root_dev,ax

jmpi 0,SETUPSEG; 0x9020 跳转到setup.s开始继续执行了!!!

setup.s和head.s的衔接

1.setup.s把head.s及之后的代码都移动到了内存0开始的地方(do_move)

2.设置gdt、ldt(end_move)

3.开保护模式, jmpi 0,8 跳转到了head.s

1
2
3
mov	ax,#0x0001	! protected mode (PE) bit。 保护模式. PE,不是任何人都可以修改
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs) gdt的1项, 0开始编号,第二项

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

p25

https://mp.weixin.qq.com/s/S5zarr9BmLhUHAmdmeNypA

1
2
3
mov ax,#0x0001  ; protected mode (PE) bit
lmsw ax ; This is it;
jmpi 0,8 ! jmp offset 0 of segment 8 (cs) gdt的1项, 0开始编号,第二项

在执行这条指令前已经转变为了保护模式, 保护模式的寻址方式变了,需要通过段选择子,段寄存器中存储的不再是地址,而是段选择子

0 表示段内偏移地址, 8代表cs(代码段)的值, 更具体而言,8是 0000,0000,0000,1000

根据下图段选择子的结构可以看出,1是代表了描述符索引,即gdt表的第一项,也就是现在地址的0!

gdt表在此之前已经初始化过,第一项是内核代码段,base address = 0 , 偏移地址也是0,所以跳转到内存的0地址

Untitled2

Untitled3

1
2
3
4
5
6
7
8
9
10
11
12
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

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

p438

保护模式在保护段,通过段机制划分不同的段,设置各个段的访问权限. 增加了段限长,防止了对代码段的覆盖以及超越权限的访问

特权级的目的是 阻止非法的访问, 对用户进程之间进行了隔离

意义是保护了资源

分页有保护作用,分页使得用户无法直接找到物理地址,用户只能操作逻辑地址,而逻辑地址需要先转化为线性地址,然后才能进一步转化为物理地址

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

p33

因为第一次设置GDT是在setup.s里面设置的数据,setup.s将来会在设计缓冲区时被覆盖,所以需要改变位置. 其实这是设计者精心打磨内存使用空间而产生的后果,尽可能不浪费一点空间,head.s执行时,gdt又被写到了head.s执行过的程序中,实现了内存的充分利用.

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

在哪?

include/linux/sched.h 是写死的,在INIT_ TASK里,运行时位于内核数据区

在未初始化进程0之前,使用的是boot阶段的内核栈(user_stack) 很奇怪吧,内核栈却叫user_stack,p34

具体内容?

包含了进程各项初始化的内容,具体的话可以看task_struct,附在下面了. (include/linux/sched.h )

如:进程0的进程状态,LDT,TSS等 p68

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*
* INIT_TASK is used to set up the first task table, touch at
* your own risk!. Base=0, limit=0x9ffff (=640kB)
*/ // 下面的就是进程0的各个参数的具体值
#define INIT_TASK \
/* state etc */ { 0,15,15, \ // 0是说可以跑,而不是正在跑
/* 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, \
{} \
}, \
}

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; //malloc从这里划
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;
};

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

p37

赵炯p438

注意是线性地址空间,线性地址空间远大于物理地址空间,线性地址空间是 64k * 64k = 4G。 0xFFFFFF 2的32次方

分页

4k大小为一页

setup_paging开始

第一页是页目录表,随后的4个页表是内核专属的页表

分别代表什么呢? 第一个是不是gdt?? gdt在哪??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
head.s

.align 2
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 111 */ ;111 已分页 三特权? 可读写
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 */
; 切换到main函数

页目录表、页表、页

一个页目录表项是4byte,也就是32位,通过它来寻址一个页表, 页目录表一共4* 1024 = 4k大小

一个页表也是4byte,每一个页表有1024个页表项,也是 4k 占用一页

Untitled4

开始的前7个页是什么意思呢?

p39

也就是前7个4k, 第一个4k是页目录表,第2~7个4k是第一个页表的

赵炯p78

Untitled4

挂接关系图p39

Untitled6

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

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

https://mp.weixin.qq.com/s/ISyaX5zPWRw_d-9zvZUPUg

因为main函数是整个系统的运行函数,它不能被call,没有比它等级更高的,call的话是需要返回的,main不需要返回,main结束了,就关机了(

正常函数调用是会把eip(被掉函数返回时 返回的地址) 进行压栈, ret的时候取出来,进入到这里继续执行,main不能被调用,但可以伪造被调用的假象,然后也进行ret,就可以进入到main里了

调用路线图 p42

正常的call

Untitled7

模仿的call

Untitled8

代码head.s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

startup_32:
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 ; 后面把这个pop出来
jmp setup_paging

setup_paging:
...
ret ;回到main函数

Untitled9

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

p53页

/init/main.c trap_init();

/kernel/trap.c trap_init()

/include/asm/system.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//设置中断门函数
#define set_intr_gate(n,addr) \ // n 中断号 addr 中断程序偏移地址
_set_gate(&idt[n],14,0,addr) //&idt[n]对应中断号在中断描述符表中的偏移值;中断描述符的类型是 14,特权级是 0。
//设置陷阱门函数
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr) // idt表的n项,f,0, 0对应dpl,15对应type ,以二进制来看到
//设置系统调用门函数
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \ //实现了偏移的拆分
"movw %0,%%dx\n\t" \ //将偏移的低字给dx
"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)) // jump 08》???

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

原因解释:

    这个东西和特权级有关(废话),这个是intel的规定,cli和sti与CPL和EFLAGS[IOPL]相关,通过EFLAGS中的IOOPL来保护一些敏感io指令,如cli、sti、in、out等,只有当CPL≤IOPL时才能执行,也就是说当前特权级大于IOPL设置的特权级才可以执行、否则会产生一个一般性保护异常

    IOPL位于EFLAGS的12-13位,只能通过iret改变,linux0.11的0进程,INIT_TASK中IOPL为0,在move_to_user_mode中执行了pushfl\n\t,继承了内核的EFLAGS,所以用户态的3特权级大于IOPL的0特权级,无法调用cli

代码:

move_to_user_mode

1
2
3
4
5
6
7
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
"pushfl\n\t" \
....
"iret\n" \
....
:::"ax")

INIT_TASK include/linux/sched.h

tss的第10位为0,可以在该文件下找到tss的结构,第10位就是eflags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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, \
0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, \
{} \
}, \
}

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;
};

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

init/main.c sched_init();

kernel/sched.c

1
2
3
4
5
6
7
8
9
10
11
12
13
void sched_init(void) //调度程序的初始化子程序 
{
int i;
struct desc_struct * p;

if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes"); //致命错误
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); // 设置初始任务(任务 0)的任务状态段描述符和局部数据表描述符(include/asm/system.h,65)。
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));

init_task

static union task_union init_task = {INIT_TASK,}; // 这里进行类型转换

15.在system.h里读懂代码。这里中断门、陷阱门、系统调用都是通过_set_gate设置的,用的是同一个嵌入汇编代码,比较明显的差别是dpl一个是3,另外两个是0,这是为什么?说明理由。

p51

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \ //实现了偏移的拆分
"movw %0,%%dx\n\t" \ //将偏移的低字给dx
"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)) // jump 08》???
//设置中断门函数
#define set_intr_gate(n,addr) \ // n 中断号 addr 中断程序偏移地址
_set_gate(&idt[n],14,0,addr) //&idt[n]对应中断号在中断描述符表中的偏移值;中断描述符的类型是 14,特权级是 0。
//设置陷阱门函数
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr) // idt表的n项,f,0, 0对应dpl,15对应type ,以二进制来看到
//设置系统调用门函数
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)

这里也是特权级保护的思想, 大概申请者用于大于该东西的特权级才能访问(具体的比这个复杂, 参加IA32 第三卷)

set_system_gate是3,意思是系统调用可以由3特权级(即用户特权级)进行调用,其余两个为0的意思是只能由内核处理,禁止用户进程进行调用,这样就起到了保护系统的作用

16.进程0 fork进程1之前,为什么先调用move_to_user_mode()?用的是什么方法?解释其中的道理。

p78

为什么要调用move_to_user_mode()?

    因为Linux操作系统规定,除了进程0之外,所有进程都要由一个已有进程在3特权级下进行创建,所以进程0在fork进程1之前,要先从0特权级翻转到3特权级

Linux0.11是通过move_to_user_mode(),模仿中断返回动作,实现从0特权级转变为3特权级

方法(怎么实现翻转呢?

    IA32体系结构翻转特权级的方法之一是中断,进入中断的时候由3转0,返回的时候由0转3. int指令会引发CPU硬件完成SS、ESP、EFLAGS、CS、EIP的值按顺序进栈,返回时CPU执行iret指令会将栈中的值自动按反序恢复给这五个寄存器

    既然要模拟中断返回,那么就需要模拟int(中断)时的压栈,就是前面5个push,最后调用iret进行返回,将SS,ESP,EFLAGS,CS,EIP按顺序交给CPU,CPU此时就翻转到了3特权级(具体的呢??)

代码

include/asm/system.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \ //SS 0x17 = 10(第三项)1(ldt)11(特权级3 ) 代替inc int? 这是个段值,用户程序数据段 由0特权变为3特权
"pushl %%eax\n\t" \ //里面就是esp 对的,看第一句
"pushfl\n\t" \
"pushl $0x0f\n\t" \ // 0000000000001111 最后两位11(特权级3)
"pushl $1f\n\t" \
"iret\n" \ // 中断返回 (与中断不配套其实,单独出现的一个返回,前面是一个模拟中断)
"1:\tmovl $0x17,%%eax\n\t" \ //开始3特权级 切3态的话 为什么说切到task0了? ldtr tr->
"movw %%ax,%%ds\n\t" \ // 进程0代码 分页的时候 + 7、 task数组,都可以说明是进程0
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")

17.在Linux操作系统中大量使用了中断、异常类的处理,究竟有什么好处?

    在此之前是采用“主动轮巡”的方式来处理这些请求,在轮巡的时候干不了别的,很浪费时间. 所以不如采用被动的模式,即当有需要的时候,发送中断信号,告诉CPU进入到具体的中断处理程序进行处理. 这样使得CPU的处理更加高效.

18.copy_process函数的参数最后五项是:long eip,long cs,long eflags,long esp,long ss。查看栈结构确实有这五个参数,奇怪的是其他参数的压栈代码都能找得到,确找不到这五个参数的压栈代码,反汇编代码中也查不到,请解释原因。

p83

是在int 0x80的时候,CPU硬件自动将ss、esp、eflags、cs、eip进行压栈(压入进程0的内核栈), (本意是保护存储压栈现场,使得中断返回后能够继续正常执行,在这里我们灵活取用了)

init/main.c

19.分析get_free_page()函数的代码,叙述在主内存中获取一个空闲页的技术路线。

p89

赵炯p446

kernel/fork.c

1
2
3
4
5
6
7
8
9
10
11
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, //none是
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss) //int 0x80 ss esp啥呀这是
{ //参数哪来的呢?? 传参了吗? 压栈放在栈里了, 主调函数往里放 在哪传的?
struct task_struct *p;
int i;
struct file *f;

p = (struct task_struct *) get_free_page(); //返回的指针做强制类型转换,把一块空间转换成一个struct
if (!p)

mm/memory.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* Get physical address of first (actually last :-) free page, and mark it
* used. If no free pages left, return 0.
*/
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");

__asm__("std ; repne ; scasb\n\t"
"jne 1f\n\t"
"movb $1,1(%%edi)\n\t"
"sall $12,%%ecx\n\t"
"addl %2,%%ecx\n\t"
"movl %%ecx,%%edx\n\t"
"movl $1024,%%ecx\n\t"
"leal 4092(%%edx),%%edi\n\t"
"rep ; stosl\n\t"
"movl %%edx,%%eax\n"
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
:"di","cx","dx");
return __res;
}

20.分析copy_page_tables()函数的代码,叙述父进程如何为子进程复制页表。

p97

参考资料

《Linux内核设计的艺术 第二版》 新设计团队

《Linux内核完全注释》 赵炯

《IA32》 手册 第三卷

https://www.likecs.com/show-204742912.html

https://github.com/sunym1993/flash-linux0.11-talk