ldt中断函数挂接以及进程及相关设备初始化

ldt中断函数挂接以及进程及相关设备初始化

操作系统中心思想:管理所有的硬件资源为软件资源提供服务

main.c

1
2
3
4
//之前把0x9000-0x901F部分用来存储机器系统信息
#define EXT_MEM_K (*(unsigned short *)0x90002)
#define DRIVE_INFO (*(struct drive_info *)0x90080)//90080是硬盘参数表
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)//901fc是跟设备号

根文件系统设备

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;
//内存大小=1M+扩展内存
memory_end = (1<<20) + (EXT_MEM_K<<10);//左移20位:1M EXT_MEM_K机器设备信息,0x90002扩展内存kb数 左移10位kb->mb
//忽略不到4KB(一页)的内存页
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;//如果内存大于16M按照16M计算
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;
//如果在Makefile里面指定了RAMDISK则建立虚拟盘
#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;//将虚拟盘的请求处理函数与blk_dev【7】的第二项挂接
rd_start = (char *) mem_start;
rd_length = length;
cp = rd_start;
//虚拟盘所在的内存区域初始化为0
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 }, /* 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 */
};

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; //used=100 理论上是不可能出现的,因为进程最大数是64,说明这个地方已经占死了不能给比人用
i = MAP_NR(start_mem);// 然后计算可使用起始内存的页面号。
end_mem -= start_mem;
end_mem >>= 12;
while (end_mem-->0)
mem_map[i++]=0;//mem_map非常重要用于记录分配出了多少内存,这里指的是物理内存,将6M以后的内存占用清零
}
image-20231007204235125
1
2
3
4
	main_memory_start = buffer_memory_end;
//如果在Makefile里面指定了RAMDISK则建立虚拟盘
#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但是还没有挂接中断服务函数,只是一个空架子。

1
trap_init();

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;
//这里是在维护中断描述符表的内容,idt的架子在head.s里面给了
set_trap_gate(0,&divide_error);//这里的0,1,2...是中断描述符表项//除零错误
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,&parallel_interrupt);//并口
}
1
2
3
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)!0对应dpl,15对应类型(查手册):1111
//idt是中断描述符表的起始地址。idt[n]是中断描 述符表中中断号n对应项的偏移值。中断描述符的类型是15,特权级是0.意思是只能由内核处理,如果dpl特权级别=3则可以由3特权级(用户)来处理
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" \//%0对应i
"movl %%eax,%1\n\t" \//%1对应第二行的o,将eax寄存器的值放到idt表项里面,即&idt【0】,也就是gate_addr
"movl %%edx,%2" \//%2对应第三行的 放到gateaddr的后四个字节
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ //i:立即数
"o" (*((char *) (gate_addr))), \ //中断描述符前四个字节地址,o是偏移的意思
"o" (*(4+(char *) (gate_addr))), \//中断描述符后四个字节地址
"d" ((char *) (addr)),"a" (0x00080000))//d对应edx,a对应eax,这说明了为啥edx是&divide0地址

中断描述符结构: 这里面的偏移地址,就是段偏移再加上gdt里面的段基址就得到了真正的线性地址 这里本来edx里面有着完整的中断服务程序段偏移地址,为了配合中断描述符,强行把低字部分给了eax。 注意,前两条汇编这部分代码一直是在对寄存器进行操作,还没有放到内存里面,后面再添到&idt[0]里面

image-20231008154407166
1
"i" ((short) (0x8000+(dpl<<13)+(type<<8)))

movw %%dx,%%ax\n\t将edx的低字赋值给eax,也就是&divide_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

image-20231008173918140 ### 初始化块设备请求项结构 linux 将外设分为了两类,一类是块设备,一类是字符设备。

进程与块设备进行沟通的时候需要经过主机内存中的缓冲区。

如何管理缓冲区和块设备逻辑块之间的读写关系:request [32]请求项

image-20231011184551420

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct request {
int dev; /* -1 if no request */ //使用的设备号
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;//指向下一个请求项
};



struct request request[NR_REQUEST];//32个请求项

操作系统根据任务的轻重缓急,管理块设备。决定缓冲和块设备之间的读写操作,并把需要操作的缓冲块记录在请求项上,得到读写操作指令时只根据请求项决定要处理的逻辑块。隔离了进程和IO设备

1
2
3
4
5
6
7
8
9
10
void blk_dev_init(void)
{
int i;
//NR_REQUEST是请求项的数量=32
for (i=0 ; i<NR_REQUEST ; i++) {
request[i].dev = -1;//设为空闲,说明这个请求项还没有聚体对应那个设备,用于判断请求项当前设备是否空闲
request[i].next = NULL;//互不挂接,说明还没形成请求队列
}
}

image-20231011192529023

与建立人机交互界面相关的外设的中断服务程序挂接(非重点)

这是一个空函数:

1
chr_dev_init();//字符设备,例如标准输出

初始化字符设备:

1
2
3
4
5
6
7
8
9
10
tty_init();


// tty 终端初始化函数。
// 初始化串口终端和控制台终端
void tty_init(void)
{
rs_init();// 初始化串行中断程序和串行接口 1 和 2。
con_init();// 初始化控制台终端。(console.c, 617)
}

串口设置

1
2
3
4
5
6
7
8
void rs_init(void)
{
set_intr_gate(0x24,rs1_interrupt);//设置串口1中断,中断门序号:0x24
set_intr_gate(0x23,rs2_interrupt);
init(tty_table[1].read_q.data);//初始化串口1
init(tty_table[2].read_q.data);
outb(inb_p(0x21)&0xE7,0x21);//允许IRQ3,IRQ4
}
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); /* set DLAB of line control reg */
outb_p(0x30,port); /* LS of divisor (48 -> 2400 bps *//* 发送波特率因子低字节,0x30->2400bps */
outb_p(0x00,port+1); /* MS of divisor *//* 发送波特率因子高字节,0x00 */
outb_p(0x03,port+3); /* reset DLAB */
outb_p(0x0b,port+4); /* set DTR,RTS, OUT_2 *//* 设置 DTR,RTS,辅助用户输出 2 */
outb_p(0x0d,port+1); /* enable all intrs but writes *//* 除了写(写保持空)以外,允许所有中断源中断 */
(void)inb(port); /* read data port to reset things (?) */
}
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)


/* Initialize the variables used for scrolling (mostly EGA/VGA) */

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);//查看机器系统数据 // 初始化光标位置 x,y 和对应的内存位置 pos。
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
time_init();
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;
// CMOS 的访问速度很慢。为了减小时间误差,在读取了下面循环中所有数值后,若此时 CMOS 中秒值
// 发生了变化,那么就重新读取所有值。这样内核就能把与 CMOS 的时间误差控制在 1 秒之内
do {
time.tm_sec = CMOS_READ(0); // 当前时间秒值(均是 BCD 码值)。
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--;// tm_mon 中月份范围是 0—11
// 调用 kernel/mktime.c 中函数,计算从 1970 年 1 月 1 日 0 时起到开机当日经过的秒数,作为开机
// 时间。
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];// 任务局部描述符表。0-空,1-代码段 cs,2-数据和堆栈段 ds&ss

//四对esp ss代表了四个段特权级,每个特权级都要配合栈
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; //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;
};
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));//FIRST_TSS_ENTRY=4
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));//FIRST_LTD_ENTRY=5

#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" \ //循环右移16位,高低字节互换
"movb %%al,%3\n\t" \ //将基地址eax高字中低字节移入描述符第 4 字节
"movb $" type ",%4\n\t" \
"movb $0x00,%5\n\t" \
"movb %%ah,%6\n\t" \ // 将基地址eax高字中高字节移入描述符第 7 字节。
"rorl $16,%%eax" \ //循环右移16位,高低字节互换
::"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 {
/* these are hardcoded - don't touch */
long state; /* -1 unrunnable, 0 runnable, >0 stopped 任务的运行状态(-1 不可运行,0 可运行(就绪),>0 已停止)*/
long counter;//任务运行时间计数(递减)(滴答数),运行时间片。
long priority;//运行优先数。任务开始运行时 counter=priority,越大运行越长。
long signal;//信号。是位图,每个比特位代表一种信号,信号值=位偏移值+1。
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;//代码段地址。代码长度(字节数)。代码长度 + 数据长度(字节数)。总长度(字节数)。堆栈段地址。
long pid,father,pgrp,session,leader;//进程标识号(进程号)。 父进程号。父进程组号。会话号。 会话首领。
unsigned short uid,euid,suid;//用户标识号(用户 id)。 有效用户 id。 保存的用户 id。
unsigned short gid,egid,sgid;//组标识号(组 id)。有效组 id。保存的组 id。
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 进程使用 tty 的子设备号。-1 表示没有使用。 */
unsigned short umask;// 文件创建属性屏蔽位
struct m_inode * pwd;//当前工作目录 i 节点结构。
struct m_inode * root;//根目录 i 节点结构。
struct m_inode * executable;//执行文件 i 节点结构。
unsigned long close_on_exec;//执行时关闭文件句柄位图标志。(参见 include/fcntl.h)
struct file * filp[NR_OPEN];//文件结构指针表,最多 32 项。表项号即是文件描述符的值。
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3];// 任务局部描述符表。0-空,1-代码段 cs,2-数据和堆栈段 ds&ss。
/* tss for this task */
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结构图如下所示 image-20231013112118390

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
/*
* INIT_TASK is used to set up the first task table, touch at
* your own risk!. Base=0, limit=0x9ffff (=640kB)
*/
//进程0 :这里只写了一个进程结构,相当于户口本,按照task_struct来的
//INIT_TASK 用于设置第 1 个任务表
#define INIT_TASK \
//时间片设为15
/* state etc */ { 0,15,15, \ // state:就绪状态, counter, priority .
/* signals */ 0,{{},},0, \ // signal, sigaction[32], blocked
/* ec,brk... */ 0,0,0,0,0,0, \ // exit_code,start_code,end_code,end_data,brk,start_stack
/* pid etc.. */ 0,-1,0,0,0, \ // pid, father, pgrp, session, leader
/* uid etc */ 0,0,0,0,0,0, \ // uid, euid, suid, gid, egid, sgid
/* alarm */ 0,0,0,0,0,0, \ // alarm, utime, stime, cutime, cstime, start_time
/* math */ 0, \ // used_math
/* fs info */ -1,0022,NULL,NULL,NULL,0, \ // tty,umask,pwd,root,executable,close_on_exec
/* filp */ {NULL,}, \ // filp[20]
{ \ //ldt[3] 空+cs+ds
{0,0}, \
/* ldt */ {0x9f,0xc0fa00}, \ // 注意这里的地址是从低到高0x9f是低地址,代码长 640K,基址 0x0,G=1,D=1,DPL=3,P=1 TYPE=0x0a
{0x9f,0xc0f200}, \ // 数据长 640K,基址 0x0,G=1,D=1,DPL=3,P=1 TYPE=0x02
}, \

/*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, \
{} \
}, \
}

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
/*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, \
{} \
}

结合下述代码进行解读:

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

这里給back_link赋值为0,

  • ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶(下一个压入栈的活动记录的顶部),是栈指针

​ 内核栈的栈顶指针指向static union task_union init_task = {INIT_TASK,};即内核栈的尾部,因为init——task是在内核栈的 起始位置,内核栈的大小正好是一页,如下图所示

image-20231013112118390

  • ss0记录的是内核栈的段选择子。0x10:00010000 0特权级,GDT第10项,即内核数据段。

​ esp1=0,ss1=0,esp2=0,ss2=0 (因为;inux0.11只用到了0和三特权级)。

  • cr3=(long)&pg_dir ,即页目录项物理地址。

  • EIP(instruction pointer):EIP寄存器,用来存储CPU要读取指令的地址,CPU通过EIP寄存器读取即将要执行的指令。

​ eip=0;因为进程0的代码还没开始执行

  • eflags=0;决定了cli这类指令只能在零特权级使用

  • 一般寄存器:AX、BX、CX、DX AX:累积暂存器,BX:基底暂存器,CX:计数暂存器,DX:资料暂存器

​ EAX、ECX、EDX、EBX:為ax,bx,cx,dx的延伸,各為32位元

  • esp=0,EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。用户栈

  • esi,edi=0.寄存器ESIEDISIDI称为变址寄存器(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
//定义task和内核栈的共用体
union task_union {
struct task_struct task;
char stack[PAGE_SIZE];//内核栈,大小是一个页
};
//初始化进程0的task_struct
static union task_union init_task = {INIT_TASK,};
//初始化进程槽第一个进程为进程0.让进程0占用task[0]
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");
//这两个和用户进程有关
//tss task state segment 任务状态段
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));//FIRST_TSS_ENTRY=4
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));//FIRST_LTD_ENTRY=5
p = gdt+2+FIRST_TSS_ENTRY;
//从GDT第六项(TSS1开始向上全部清零,并将进程槽从1往后的项清空
for(i=1;i<NR_TASKS;i++) {
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* 清除标志寄存器中的位 NT,这样以后就不会有麻烦 */
// NT 标志用于控制程序的递归调用(Nested Task)。当 NT 置位时,那么当前中断任务执行
// iret 指令时就会引起任务切换。NT 指出 TSS 中的 back_link 字段是否有效。
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); // 复位 NT 标志。
ltr(0);//tr寄存器指向当前tss,task 寄存器/ 将任务 0 的 TSS 加载到任务寄存器 tr。
lldt(0);//load ldt 将局部描述符表加载到局部描述符表寄存器。
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */// 定时值低字节。,每10ms一次中断
outb(LATCH >> 8 , 0x40); /* MSB */// 定时值高字节。
set_intr_gate(0x20,&timer_interrupt);// 设置时钟中断处理程序句柄(设置时钟中断门)。这是进程调度的基础
outb(inb_p(0x21)&~0x01,0x21);// 修改中断控制器屏蔽码,允许时钟中断。
set_system_gate(0x80,&system_call);//设置系统调用 int80 // 设置系统调用中断门。

上述的:

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)))

进程初始化总览: image-20231017143210782

设置时钟中断

1
2
3
4
5
6
7
8
9
10
   void sched_init(void)
{
...
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */// 定时值低字节。,每10ms一次中断
outb(LATCH >> 8 , 0x40); /* MSB */// 定时值高字节。
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);//设置系统调用 int80 // 设置系统调用中断门。 
image-20231017233811932

初始化缓冲区管理结构

操作系统通过hash_table[NR_HASH],buffer_head双向链表组成的复杂哈希表管理缓冲区。

1
2
void main(){	
buffer_init(buffer_memory_end);//块设备缓冲区init,开在内存里面

从内核的末端和缓冲区的末端同时开始,方向相对增长,配对的做出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; /* 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;
};
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;
// 如果缓冲区高端等于 1Mb,则由于从 640KB-1MB 被显示内存和 BIOS 占用,因此实际可用缓冲区内存
// 高端应该是 640KB。否则内存高端一定大于 1MB。
if (buffer_end == 1<<20)
b = (void *) (640*1024);
else
b = (void *) buffer_end;
// 这段代码用于初始化缓冲区,建立空闲缓冲区环链表,并获取系统中缓冲块的数目。
// 操作的过程是从缓冲区高端开始划分 1K 大小的缓冲块,与此同时在缓冲区低端建立描述该缓冲块
// 的结构 buffer_head,并将这些 buffer_head 组成双向链表。
// h 是指向缓冲头结构的指针,而 h+1 是指向内存地址连续的下一个缓冲头地址,也可以说是指向 h
// 缓冲头的末端外。为了保证有足够长度的内存来存储一个缓冲头结构,需要 b 所指向的内存块
// 地址 >= h 缓冲头的末端,也即要>=h+1。

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; // 指向具有相同 hash 值的下一个缓冲头。
h->b_prev = NULL;// 指向具有相同 hash 值的前一个缓冲头。
h->b_data = (char *) b;// 指向对应缓冲区数据块(1024 字节)。
h->b_prev_free = h-1;// 指向链表中前一项。
h->b_next_free = h+1;// 指向链表中下一项。
h++;// h 指向下一新缓冲头位置
NR_BUFFERS++;// 缓冲区块数累加。
if (b == (void *) 0x100000)// 如果地址 b 递减到等于 1MB,则跳过 384KB,
b = (void *) 0xA0000;// 让 b 指向地址 0xA0000(640KB)处。
}
h--;// 让 h 指向最后一个有效缓冲头。
free_list = start_buffer;// 让空闲链表头指向头一个缓冲区头。
free_list->b_prev_free = h;// 链表头的 b_prev_free 指向前一项(即最后一项)。
h->b_next_free = free_list;// h 的下一项指针指向第一项,形成一个环链。
// 初始化 hash 表(哈希表、散列表),置表中所有的指针为 NULL。
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();//特权级从0变到3,开始运行进程0
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//模仿中断硬件压栈,顺序是ss,esp,eflags,cs,eip
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \ //当前的esp赋值给eax,因为后面模仿压栈后esp会变化
"pushl $0x17\n\t" \ //ss进栈,0x17:00010111(3特权级,LDT,数据段
"pushl %%eax\n\t" \ //esp进栈。esp即是0特权也是三特权,栈的实际内存空间是一样的,就是特权级不一样,压进来的是esp0,弹出的是esp3,特权级翻转但是物理位置没变
"pushfl\n\t" \ //eflags进栈
"pushl $0x0f\n\t" \ //cs进栈。 1111 三特权级,LDT,代码段
"pushl $1f\n\t" \ //eip进栈
"iret\n" \ //出栈恢复现场,特权级翻转,因为上面压栈的都是三特权级的
"1:\tmovl $0x17,%%eax\n\t" \ //为什么这里指的是进程0:因为ltr和ldtr寄存器指向0进程。
"movw %%ax,%%ds\n\t" \ //ds,es,fs,gs与ss一致
"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