ldt中断函数挂接以及进程及相关设备初始化
操作系统中心思想:管理所有的硬件资源为软件资源提供服务
main.c
1 2 3 4 #define EXT_MEM_K (*(unsigned short *)0x90002) #define DRIVE_INFO (*(struct drive_info *)0x90080) #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)
根文件系统设备
Linux0.11要求系统必须存在一个跟文件系统,其他文件系统挂载在上面,这里指的是文件系统格式化设备例如软盘。
image-20231007004249231
规划物理内存
image-20231005095121645
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ROOT_DEV = ORIG_ROOT_DEV; drive_info = DRIVE_INFO; memory_end = (1 <<20 ) + (EXT_MEM_K<<10 ); memory_end &= 0xfffff000 ; if (memory_end > 16 *1024 *1024 ) memory_end = 16 *1024 *1024 ; if (memory_end > 12 *1024 *1024 ) buffer_memory_end = 4 *1024 *1024 ; else if (memory_end > 6 *1024 *1024 ) buffer_memory_end = 2 *1024 *1024 ; else buffer_memory_end = 1 *1024 *1024 ; main_memory_start = buffer_memory_end; #ifdef RAMDISK main_memory_start += rd_init(main_memory_start, RAMDISK*1024 );
设置虚拟盘(块设备)
什么是虚拟盘:在内存中开辟一块空间按照盘处理,这个是比较快的。
相对应的虚拟内存是在磁盘中开辟一块空间当作内存处理,这个速度就很慢。
1 #define DEVICE_REQUEST do_rd_request
1 2 3 4 5 6 7 8 9 10 11 12 13 14 long rd_init (long mem_start, int length) { int i; char *cp; blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; rd_start = (char *) mem_start; rd_length = length; cp = rd_start; for (i=0 ; i < length; i++) *cp++ = '\0' ; return (length); }
image-20231007095026059
1 2 3 4 5 6 7 8 9 struct blk_dev_struct blk_dev [NR_BLK_DEV ] = { { NULL , NULL }, { NULL , NULL }, { NULL , NULL }, { NULL , NULL }, { NULL , NULL }, { NULL , NULL }, { NULL , NULL } };
blk_dev
主要功能是将某一类设备与对应的请求处理项函数挂接。操作系统最多可以管理六类设备。
内存管理结构与mem_map初始化
1 mem_init(main_memory_start,memory_end);
系统通过mem_map 对1MB
以上的内存进行分页管理,记录一个页面的使用次数。
1 static unsigned char mem_map [ PAGING_PAGES ] = {0 ,};
1 2 3 4 5 6 7 8 9 10 11 12 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 ; }
image-20231007204235125
1 2 3 4 main_memory_start = buffer_memory_end; #ifdef RAMDISK main_memory_start += rd_init(main_memory_start, RAMDISK*1024 );
可知在设置缓冲和虚拟盘后main_memory_start就是虚拟盘后面的地址,操作系统对内核和用户采用了不同的管理方法,1M以内是内核的内存区域不允许用户方法,所以used了。缓冲区和虚拟盘也没有分页访问的权限。
中断流程再分析
外部中断、软件中断和异常是通过中断描述符表(IDT)处理的。 IDT
中包含访问中断和异常处理程序的门描述符的集合。像 GDT 一样,IDT
不是一个段, IDT 的线性基地址包含在 IDT 寄存器中(IDTR)。
IDT
中的描述符可以是中断门、陷阱门或任务门。处理器必须先从内部硬件、外部
中断控制器或者通过诸如 INT、INTO、INT 3、BOUND
指令收到一个中断向量(中断号), 才去访问中断或异常处理程序。中断向量是
IDT 中门描述符的索引。如果选中的门描
述符是中断门或者陷阱门,就如同通过调用门调用过程一样去访问相应的处理程序;
如果是任务门,就通过任务切换访问其处理程序。
与中断相关的标志位:iF 中断许可(第 9 位)
image-20231008171319133
IDT 中可以包含以下三种门描述符:
z 任务门描述符。
z 中断门描述符。
z 陷阱门描述符
中断描述符表的预初始化:
head.s
用256
个指向ignore_int中断门的入口地址填充中断描述符表。它不是真正的初始化idt,等到分页和内核跳转到PAGE_OFFSET处时才真正的进行
初始化。确定所有相关的准备都已就绪之后,中断可以在任何地方发生。
image-20231008154407166
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 setup_idt: lea ignore_int,%edx #取ignore_int的有效地址到edx寄存器 movl $0x00080000,%eax #把内核代码段选择符左移16位,送到eax存器,此时eax的高16位存放选择符。 movw %dx,%ax /* selector = 0x0008 = cs */#ignore_int的有效地址存入eax的底16位。此时,eax中含有门描述符底4字节(32位)的值。 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ lea _idt,%edi #取中断描述符表的地址到edi中 mov $256,%ecx rp_sidt: movl %eax,(%edi) #将通用的中断描述符存入表中(将ignore_int存入edi所指向的地址中) movl %edx,4(%edi) addl $8,%edi #edi指向表中下一项,从上面的图中可以看出idt描述符是8个字节 dec %ecx jne rp_sidt #条件跳转,使得idt表有256项 lidt idt_descr ret idt_descr: .word 256*8-1 # idt contains 256 entries .long _idt _idt: .fill 256,8,0 # idt is uninitialized
定义了中断处理函数:
Ignore_int()中断处理程序,可以看作是一个空的处理程序,它执行的主要动
作有: 1、在栈中保存一些寄存器的内容。 2、调用printk()函数打印“Unknown
interrupt”系统消息。 3、 恢复栈中寄存器的内容。
4、执行iret指令,恢复被中断的程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ignore_int: pushl %eax pushl %ecx pushl %edx push %ds push %es push %fs movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %ax,%fs pushl $int_msg call _printk popl %eax pop %fs pop %es pop %ds popl %edx popl %ecx popl %eax iret
中断描述符表的第二遍初始化
在上述预初始化之后后,内核将在
IDT中进行第二遍初始化,用有意义的陷阱和中断处理程序替换空处理程序。第二遍处理过程完成后,针对控制单元产生的每个不同的异常,IDT都有一个专门
的陷阱门或系统门;而对于可编程控制器确认的每一个IRQ,IDT都将包含一个专门的中断门。Trap_init()函数的工作就是将一些处理异常的函数
插入到IDT的非屏蔽中断及异常表项中。
IA-32
系统架构也定义了一套称为门(调用门、中断门、陷阱门和任务门)的特殊
描述符,以提供一种对不同于应用程序特权级的系统过程和处理程序的保护性访问途
经。Trap_init()函数用于设置
中断描述符表开头的陷阱门和系统门。这些中断向量都是CPU保留,用于异常处理的。
异常处理类中断服务程序挂接
linux
通过int80中断翻特权级,通过iret返回。要通过门的机制来控制低特权级到高特权级的落点
之前head.s虽然建立了idt但是还没有挂接中断服务函数,只是一个空架子。
trap_init();函数把中断异常处理服务程序与IDT进行挂接
image-20231007210524694
将异常处理函数插入IDT的表项是由set_trap_gate()和set_system_gate()函数来完成的
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 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); 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); }
1 2 3 #define set_trap_gate(n,addr) \ _set_gate(&idt[n],15,0,addr)!0对应dpl,15对应类型(查手册):1111
image-20231007212008081
与陷阱门不同,系统陷阱门的特权级是3,即系统陷阱门设置的中断处理过程能够被所有进程调用(如单步调试、溢出出错和超出边界出错等)
1 2 3 4 5 6 7 8 9 10 11 #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 ))
中断描述符结构:
这里面的偏移地址,就是段偏移再加上gdt里面的段基址就得到了真正的线性地址
这里本来edx里面有着完整的中断服务程序段偏移地址,为了配合中断描述符,强行把低字部分给了eax。
注意,前两条汇编这部分代码一直是在对寄存器进行操作,还没有放到内存里面,后面再添到&idt[0]里面
image-20231008154407166
1 "i" ((short ) (0x8000 +(dpl<<13 )+(type<<8 )))
movw %%dx,%%ax\n\t
将edx的低字赋值给eax,也就是÷_error的低字,使得中断服务程序偏移地址符合上述中断描述符
系统描述符:我们之前在总结段描述符的时候有一个代码段和数据段的描述符(跳转链接 ),这里的1111即_set_gate(gate_addr,type,dpl,addr)
里面的type,属于系统描述符,从下面这张表中可以看出这里15是32位的陷阱门。
例如这个输入的是14就是32位中断门
1 2 #define set_intr_gate(n,addr) \ _set_gate(&idt[n],14,0,addr)
image-20231008172751154
image-20231008154231967
中断门陷阱门执行流程
当执行int
n时,就去IDT表寻找对应的描述符,这个n是几就找到IDT表对应的第n+1个(从0开始)。
获取到段描述符后检查权限,进行段权限检查(没有RPL,只检查CPL)。
权限检查通过后,获取新的段选择子与之对应的gtd表中的段描述符的base,再加上IDT表中的limit作为EIP去跳转。
image-20231008161635109
### 初始化块设备请求项结构 linux
将外设分为了两类,一类是块设备,一类是字符设备。
进程与块设备进行沟通的时候需要经过主机内存中的缓冲区。
如何管理缓冲区和块设备逻辑块之间的读写关系:request
[32]请求项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct request { int dev; int cmd; int errors; unsigned long sector; unsigned long nr_sectors; char * buffer; struct task_struct * waiting ; struct buffer_head * bh ; struct request * next ; }; struct request request [NR_REQUEST ];
操作系统根据任务的轻重缓急,管理块设备。决定缓冲和块设备之间的读写操作,并把需要操作的缓冲块记录在请求项 上,得到读写操作指令时只根据请求项决定要处理的逻辑块。隔离了进程和IO设备
1 2 3 4 5 6 7 8 9 10 void blk_dev_init (void ) { int i; for (i=0 ; i<NR_REQUEST ; i++) { request[i].dev = -1 ; request[i].next = NULL ; } }
image-20231011192529023
与建立人机交互界面相关的外设的中断服务程序挂接(非重点)
这是一个空函数:
初始化字符设备:
1 2 3 4 5 6 7 8 9 10 tty_init(); void tty_init (void ) { rs_init(); con_init(); }
串口设置
1 2 3 4 5 6 7 8 void rs_init (void ) { set_intr_gate(0x24 ,rs1_interrupt); set_intr_gate(0x23 ,rs2_interrupt); init(tty_table[1 ].read_q.data); init(tty_table[2 ].read_q.data); outb(inb_p(0x21 )&0xE7 ,0x21 ); }
1 2 #define set_intr_gate(n,addr) \ _set_gate(&idt[n],14,0,addr)
这里设置串口用到的是中断门,dpl仍然是内核级。
1 2 3 4 5 6 7 8 9 10 static void init (int port) { outb_p(0x80 ,port+3 ); outb_p(0x30 ,port); outb_p(0x00 ,port+1 ); outb_p(0x03 ,port+3 ); outb_p(0x0b ,port+4 ); outb_p(0x0d ,port+1 ); (void )inb(port); }
image-20231011200603640
设置显示器
image-20231011201010562
设置键盘
将键盘中断服务程序与IDT挂接,取消8259A对键盘的中断屏蔽。
image-20231011201231132
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 con_init()调用: #define ORIG_X (*(unsigned char *)0x90000) #define ORIG_Y (*(unsigned char *)0x90001) origin = video_mem_start; scr_end = video_mem_start + video_num_lines * video_size_row; top = 0 ; bottom = video_num_lines; gotoxy(ORIG_X,ORIG_Y); set_trap_gate(0x21 ,&keyboard_interrupt); outb_p(inb_p(0x21 )&0xfd ,0x21 ); a=inb_p(0x61 ); outb_p(a|0x80 ,0x61 ); outb(a,0x61 );
设置开机启动时间(非重点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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); }
这段代码用设置开机启动时间,后面文件修改时间,访问时间等均需要根据这个进行推算。CMOS是主板上面的一个存储芯片,上面记录了时间数据。这里对这个芯片上记录的时间信息进行采集。
image-20231011202733967
初始化进程0
程序是一个可执行的文件,而进程(process)是一个执行中的程序实例。利用分时技术,在
Linux
操作系统上同时可以运行多个进程。分时技术的基本原理是把 CPU
的运行时间划分成一个个规定长度的
时间片,让每个进程在一个时间片内运行。当进程的时间片用完时系统就利用调度程序切换到另一个进
程去运行。因此实际上对于具有单个 CPU
的机器来说某一时刻只能运行一个进程。但由于每个进程运行
的时间片很短(例如 15 个系统滴答=150
毫秒),所以表面看来好象所有进程在同时运行着。 对于 Linux 0.11
内核来讲,系统最多可有 64
个进程同时存在 。除了第一个进程是“手工”建立以外,
其余的都是进程使用系统调用 fork
创建的新进程,被创建的进程称为子进程(child process),创建者,
则称为父进程(parent process)。内核程序使用进程标识号(process
ID,pid)来标识每个进程。进程由
可执行的指令代码、数据和堆栈区组成。进程中的代码和数据部分分别对应一个执行文件中的代码段、
数据段。每个进程只能执行自己的代码和访问自己的数据及堆栈区 。进程之间相互之间的通信需要通过系统调用来进行。对于只有一个
CPU 的系统,在某一时刻只能有一个进程正在运行。内核通过调度程序
分时调度各个进程运行。 Linux 系统中,一个进程可以在内核态(kernel
mode)或用户态(user
mode)下执行,因此,Linux内核堆栈和用户堆栈是分开的。用户堆栈用于进程在用户态下临时保存调用函数的参数、局部变量等数
据。内核堆栈则含有内核程序执行函数调用时的信息。
操作系统最大共有64个进程
ldt,tss结构体代码如下所示(tss结构体在task结构体里面用到过):
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 struct desc_struct ldt [3];struct tss_struct { long back_link; long esp0; long ss0; long esp1; long ss1; long esp2; long ss2; long cr3; long eip; long eflags; long eax,ecx,edx,ebx; long esp; long ebp; long esi; long edi; long es; long cs; long ss; long ds; long fs; long gs; long ldt; long trace_bitmap; struct i387_struct i387 ; };
image-20231011203649878
LDT0:描述的是进程0的LDT段。是LDT0的基址 LDT的段描述的段是线性地址的段
CR3:一个CR3意味着一个线性地址空间,CR3记录了页目录表的地址,这里面指的是物理地址
LDT:三项,0:空的,1:代码段,2:数据段描述符。一个进程有一个LDT
TSS:有一堆寄存器的值,用于进程切换 esp:栈顶指针,和ss配对
esp0:这里的0是特权级的意思
进程切换除了要存储寄存器还要存储了显示器的状态,文件的状态(但是不一定所有进程都打开了文件)等,任务切换必定要切换TSS
在A进程切换到B的时候,把进程A的cpu状态存到A进程的TSS里面。把B的TSS加载到cppu里面
设计思想:剥夺进程访问外设,访问内核,访问其他进程
从cpu的层面,同特权之间的代码段是可以互相访问的,但是从操作系统层面不可以这样。
操作系统怎么做到的:
利用LDT实现了隔离。每一个进程想跳到代码段的时候都是jmpi
1111,写不出来有效的跳转到其他段的跳转指令 EFLAGS
里面的IOPL设置了特权级,因此只有内核可以关中断开中断
设置TSS,LDT
1 2 3 4 5 6 set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); #define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89" ) #define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82" )
上述的gdt+FIRST_TSS_ENTRY
就是对应图中寻找gdt表中的描述符的过程,gdt是gdt表的基地址,保存在GDTR之中,上述操作在设置TSS0和LDT0在GDT中的段描述符
TSS0的段描述符格式如下所示:
image-20231011205601753
这段嵌入式汇编代码就是在凑出上述的段描述符格式:
1 2 3 4 5 6 7 8 9 10 11 12 #define _set_tssldt_desc(n,addr,type) \ __asm__ ("movw $104,%1\n\t" \ "movw %%ax,%2\n\t" \ "rorl $16,%%eax\n\t" \ "movb %%al,%3\n\t" \ "movb $" type ",%4\n\t" \ "movb $0x00,%5\n\t" \ "movb %%ah,%6\n\t" \ "rorl $16,%%eax" \ ::"a" (addr), "m" (*(n)), "m" (*(n+2 )), "m" (*(n+4 )), \ "m" (*(n+5 )), "m" (*(n+6 )), "m" (*(n+7 )) \ )
image-20231011205953927
描述符表结构体:
1 2 3 4 5 typedef struct desc_struct { unsigned long a,b; } desc_table[256 ]; extern desc_table idt,gdt;
32位系统上,unsigned long
通常是4字节,也就是32位。
64位系统上,unsigned long
通常是8字节,也就是64位。
参数对应关系如下图所示:
image-20231011210554759
TSS的基地址就是eax里面的addr。type
0x89,代表页存在,DPL是00内核级,S(描述符类 型)标志**
确定段描述符是系统描述符(S 标记为 0)或者代码、数据段描述符 (S
标记为 1)
这里后面的1001是系统描述符类型:查表可以看出是32位的TSS
image-20231008172751154
movw $104,%1\n\t
填入段限长,说明段限长是104字节,104=b(1101000),G=0说明粒度是以字节为单位的。LDT一共只有三项,是3*8=24字节,所以是完全够用的。
task_struct 和 init_task
sched_init部分代码:
1 2 3 4 5 6 7 for (i=1 ;i<NR_TASKS;i++) { task[i] = NULL ; p->a=p->b=0 ; p++; p->a=p->b=0 ; p++; }
这里面的task定义:
1 struct task_struct * task [NR_TASKS ] = {&(init_task.task), };
task是一个结构体数组,其中第一项由init_task.task初始化,其他项暂时为空。内核程序通过进程表对进程进行管理,每个进程在进程表中占有一项。在
Linux 系统中,进程表项 是一个 task_struct
任务结构指针。任务数据结构定义在头文件 include/linux/sched.h
中。有些书上称其为 进程控制块 PCB(Process Control Block)或进程描述符
PD(Processor Descriptor)。其中保存着用于控
制和管理进程的所有信息。主要包括进程当前运行的状态信息、信号、进程号、父进程号、运行时间累
计值、正在使用的文件和本任务的局部描述符以及任务状态段信息。该结构每个字段的具体含义如下所
示。
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 struct task_struct { long state; long counter; long priority; long signal; struct sigaction sigaction [32]; long blocked; 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; int tty; 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 ]; struct desc_struct ldt [3]; struct tss_struct tss ; };
task的初始化:,可见这里只有第一个被初始化其它为空
1 static union task_union init_task = {INIT_TASK,};
union
是一种C语言数据结构,它可以存储不同数据类型的成员,但一次只能存储其中的一个成员。
1 2 3 4 union task_union { struct task_struct task ; char stack [PAGE_SIZE]; };
task union结构图如下所示
INIT_TASK:
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 #define INIT_TASK \ { 0 ,15 ,15 , \ 0 ,{{},},0 , \ 0 ,0 ,0 ,0 ,0 ,0 , \ 0 ,-1 ,0 ,0 ,0 , \ 0 ,0 ,0 ,0 ,0 ,0 , \ 0 ,0 ,0 ,0 ,0 ,0 , \ 0 , \ -1 ,0022 ,NULL ,NULL ,NULL ,0 , \ {NULL ,}, \ { \ {0 ,0 }, \ {0x9f ,0xc0fa00 }, \ {0x9f ,0xc0f200 }, \ }, \ {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 , \ {} \ }, \ }
idt 描述符:8个字节:
image-20231011205601753
idt设置:第一项为空项{0,0}
0x9f,0xc0fa00:
image-20231014101301111
数据段描述符 (S 标记为 1),类型1010代码段,基地址0x00,段限长9f
image-20231006185241375
tss0的设置:
1 2 3 4 5 6 {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 , \ {} \ }
结合下述代码进行解读:
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 struct tss_struct { long back_link; long esp0; long ss0; long esp1; long ss1; long esp2; long ss2; long cr3; long eip; long eflags; long eax,ecx,edx,ebx; long esp; long ebp; long esi; long edi; long es; long cs; long ss; long ds; long fs; long gs; long ldt; long trace_bitmap; struct i387_struct i387 ; };
这里給back_link赋值为0,
ESP:栈指针寄存器(extended stack
pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶(下一个压入栈的活动记录的顶部),是栈指针
内核栈的栈顶指针指向static union task_union init_task = {INIT_TASK,};
即内核栈的尾部,因为init——task是在内核栈的
起始位置,内核栈的大小正好是一页,如下图所示
ss0记录的是内核栈的段选择子。0x10:00010000
0特权级,GDT第10项,即内核数据段。
esp1=0,ss1=0,esp2=0,ss2=0
(因为;inux0.11只用到了0和三特权级)。
eip=0;因为进程0的代码还没开始执行
EAX、ECX、EDX、EBX:為ax,bx,cx,dx的延伸,各為32位元
esp=0,EBP:基址指针寄存器(extended base
pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。用户栈
esi,edi=0.寄存器ESI 、EDI 、SI 和DI 称为变址寄存器(Index
Register) ,它们主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。变址寄存器不可分割成8 位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。
它们可作一般的存储器指针使用。在字符串操作指令的执行过程中,对它们有特定的要求,而且还具有特殊的功能。
0x17是段选择子: 00010111.用户特权级,ldt第三项,即数据段。es=0x17
ES(Extra
Segment):附加段寄存器。存放当前执行程序中一个辅助数据段的段地址。
进程的代码段cs,数据段ds栈顶指针ss,标志段寄存器fs,全局段寄存器gs都指向0x17
ldt=_LDT(0)\#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
进程的ldt相对gdt的偏移量
trace0 的位图 0x80000000
进程0设置相关代码解读
main函数:调用进程初始化函数
1 2 3 4 5 6 void main(void){ ... sched_init(); ... }
1 2 3 4 5 6 7 8 9 union task_union { struct task_struct task ; char stack [PAGE_SIZE]; }; static union task_union init_task = {INIT_TASK,};struct task_struct * task [NR_TASKS ] = {&(init_task.task), };
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 typedef struct desc_struct { unsigned long a,b; } desc_table[256 ]; extern desc_table idt,gdt;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)); set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); p = gdt+2 +FIRST_TSS_ENTRY; for (i=1 ;i<NR_TASKS;i++) { task[i] = NULL ; p->a=p->b=0 ; p++; p->a=p->b=0 ; p++; } __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl" ); ltr(0 ); lldt(0 ); outb_p(0x36 ,0x43 ); outb_p(LATCH & 0xff , 0x40 ); outb(LATCH >> 8 , 0x40 ); set_intr_gate(0x20 ,&timer_interrupt); outb(inb_p(0x21 )&~0x01 ,0x21 ); set_system_gate(0x80 ,&system_call);
上述的:
ltr(0);,task register是cpu的寄存器,这里指向了tss0 lldt(0);
这里将当前进程0的ldt挂到cpu的ldtr上 操作系统真正激活了进程0
1 2 3 4 5 6 #define FIRST_TSS_ENTRY 4 #define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1) #define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3)) #define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3)) #define ltr(n) __asm__("ltr %%ax" ::"a" (_TSS(n))) #define lldt(n) __asm__("lldt %%ax" ::"a" (_LDT(n)))
进程初始化总览:
设置时钟中断
1 2 3 4 5 6 7 8 9 10 void sched_init (void ) { ... outb_p(0x36 ,0x43 ); outb_p(LATCH & 0xff , 0x40 ); outb(LATCH >> 8 , 0x40 ); set_intr_gate(0x20 ,&timer_interrupt); outb(inb_p(0x21 )&~0x01 ,0x21 ); ... }
其中LATCH宏定义:\#define LATCH (1193180/HZ)
即系统每10毫秒发生一次中断
image-20231017233515389
系统调用
1 set_system_gate(0x80 ,&system_call);
image-20231017233811932
初始化缓冲区管理结构
操作系统通过hash_table[NR_HASH],buffer_head双向链表组成的复杂哈希表管理缓冲区。
1 2 void main () { buffer_init(buffer_memory_end);
从内核的末端和缓冲区的末端同时开始,方向相对增长,配对的做出buffer_head(低地址端)和缓冲块(高地址端),直到不足一对buffer_head缓冲块
image-20231017235712928
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct buffer_head * start_buffer = (struct buffer_head *) &end;struct buffer_head * hash_table [NR_HASH ];static struct buffer_head * free_list ;struct buffer_head { char * b_data; unsigned long b_blocknr; unsigned short b_dev; unsigned char b_uptodate; unsigned char b_dirt; unsigned char b_count; unsigned char b_lock; 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 ; };
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 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; 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 ; }
image-20231018001249142
struct buffer_head * start_buffer = (struct buffer_head *) &end;
这里的end就是内核代码末端的地址,设计者较难事先预估在内核模块链接期间设置end值,在这里使用。
image-20231018084734659
初始化硬盘
1 2 3 4 5 6 7 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 ); }
将硬盘请求项服务程序do_hd_request与blk_dev控制结构挂接,将中断服务程序与LDT相挂接
image-20231018084850085
初始化软盘
初始化与软盘相关的中断
1 2 3 4 5 6 void floppy_init (void ) { blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; set_trap_gate(0x26 ,&floppy_interrupt); outb(inb_p(0x21 )&~0x40 ,0x21 ); }
和硬盘初始化的步骤相同
image-20231018085045864
开启中断
sti();//开中断,cli关中断在setup.s里面
image-20231018085257981
进程0特权级翻转,成为真正的进程
除了进程0外,其它所有进程都要由一个已有进程在3 特权级下创建
此时进程0还在0特权还不算真正的进程
1 2 3 4 5 void main () {... move_to_user_mode(); ... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define move_to_user_mode() \ __asm__ ("movl %%esp,%%eax\n\t" \ "pushl $0x17\n\t" \ "pushl %%eax\n\t" \ "pushfl\n\t" \ "pushl $0x0f\n\t" \ "pushl $1f\n\t" \ "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" )
中段函数与普通函数调用最大的不同是,不知道在哪里调用因此压栈工作是由硬件完成的,cpu硬件会将SS,ESP,EFLAGS,CS,EIP按顺序进栈。iret会引发CPU硬件将栈中的值返还给寄存器
这部分代码是手动模拟了中断压栈的过程
中断期间硬件会完成保护现场恢复现场,以及特权级翻转
CPU响应中断时,根据DPL设置可以实现指定特权级之间的翻转
特权级翻转
3 ->0 :system_call
0->3 : move_to_user_mode() iret
user_stack 与进程0的栈分析
user_stack在第一章节笔记中的记录
分析共用一个栈:怎么证明:esp一样的对应的ss的段指的位置上一样的
iret之前:0特权栈 iret之后:进程0的用户栈
进程0的内核栈?每一个进程的内核栈不能与其他1进程共用
进程0的内核栈和原来的0特权栈不是一回事
原来的0特权栈没用了,历史使命完成:保护模式打开,分页打开,中断建立完了,中断打开,0进程开始运行了。
进程0的内核栈:task init