Linux中1號進程是由0號進程來創建的,因此必須要知道的是如何創建0號進程,由於在創建進程時,程序一直運行在內核態,而進程運行在用戶態,因此創建0號進程涉及到特權級的變化,即從特權級0變到特權級3,Linux是通過模擬中斷返回來實現特權級的變化以及創建0號進程,通過將0號進程的代碼段選擇子以及程序計數器EIP直接壓入內核態堆棧,然後利用iret匯編指令中斷返回跳轉到0號進程運行。
move_to_user_mode();//創建0號進程,開始進入0號進程,切換到特權級3運行
if (!fork()) {init();}//創建1號進程
跟蹤代碼:
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \//將esp寄存器的內容存入eax中
"pushl $0x17\n\t" \//壓入0號任務的數據段選擇符
"pushl %%eax\n\t" \//壓入堆棧指針
"pushfl\n\t" \//壓入標志寄存器
"pushl $0x0f\n\t" \//壓入0號任務的代碼段選擇符
"pushl $1f\n\t" \//壓入EIP,即切換到0號任務後CPU運行的位置
"iret\n" \//中斷返回指令
"1:\tmovl $0x17,%%eax\n\t" \//由於發生了切換,需要更改各段寄存器
"movw %%ax,%%ds\n\t" \//更改段寄存器ds
"movw %%ax,%%es\n\t" \//更改段寄存器es
"movw %%ax,%%fs\n\t" \//更改段寄存器fs
"movw %%ax,%%gs" \//更改段寄存器gs
:::"ax")
分析如下,注釋已經很清楚:
代碼為嵌入匯編語句的C程序,::”ax”表示的是輸出為空,輸入為空,在這個宏定義的執行過程中可以發生改變的是ax寄存器,這屬於GNU的gas語法,不作解釋
0x17與0x0f的真實意義,跟蹤查看前先寫成二進制形式
0x17=0000 0000 0001 0111
0x0f=0000 0000 0000 1111
0x17與0x0f的後三們均是111,段選擇子的後三位分別表示RPL以及TI,因此後三位即表示請示特權級為3,描述符在LDT中,故0x17與0x0f分別表示LDT中的第二項與第一項,即然是LDT表,在使用之前肯定要進行初始化,幫初始化代碼肯定在move_to_user_mode之前,跟蹤分析可以發現在sched_init中,源碼如下:
void sched_init(void)
{
int i;
//desc_struct表示是描述符表類型typedef struct desc_struct{a,b}desc_table[256];
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
//這裡開始是關鍵部分,gdt是全局描述符表的基地址
//FIRST_TSS_ENTRY與FIRST_LDT_ENTRY分別是4,5即全局描述符表中的第4項
//與第五項代表的是第一個任務,對其進行設置
//查看static union task_union init_task = {INIT_TASK,};可以看到INIT_TASK可以看到//INIT_TASK是個宏定義,即下面的注釋
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
//p指向GDT表中0號任務的下一個位置,即GDT表中第6項
p = gdt+2+FIRST_TSS_ENTRY;
//NR_TASKS是Linux 0.11中最多支持的進程數64個
for(i=1;i<NR_TASKS;i++) {
task[i] = NULL;
//重復兩次是因為每個進程對應一個LDT與一個TSS
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
//將標志寄存器的NT位禁止
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
//#define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))這是宏定義,很顯然吧
//加載當前的任務寄存器與ldtr寄存器
ltr(0);
//#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))
lldt(0);
//定時器8253的初始化
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
//#define LATCH (1193180/HZ),用此設置後時鐘中斷為每10ms一次
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
//後面是設置定時器的中斷以及打開定時器
set_intr_gate(0x20,&timer_interrupt);
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call);
//備注:
//定時器有三個鎖存器,他們各有其則,鎖存器0用於維護系統時鐘,地址為0x40
//鎖存器1用於周期性的向DMA發送數據信號,供存儲器刷新用,地址為0x41
//鎖存器2用於揚聲器發出聲音,地址為0x42,因此這裡向0x40設定值
}
INIT_TASK宏定義,其實就是0號任務,看起來比較混亂,其實就是初始化task_struct結構體
#define INIT_TASK \
//0表示可運行的,15表示運行時間片,15表示運行優化級
/* state etc */ { 0,15,15, \
//0表示沒有信號,{{}}信號處理句柄設為0,0表示不屏蔽信號
/* signals */ 0,{{},},0, \//初始化信號設置
//將exit_code以及start_code,end_code,end_data,brk,start_stack均設為0
/* ec,brk... */ 0,0,0,0,0,0, \
//0表示進程號,-1表示父進程,後面三個0表示,pgrp,session,leader
/* pid etc.. */ 0,-1,0,0,0, \
//設置進程的這6個成員unsigned short uid,euid,suid; unsigned short gid,egid,sgid;
/* uid etc */ 0,0,0,0,0,0, \
//設置進程的報警定時器以及5個時間函數
/* alarm */ 0,0,0,0,0,0, \
//表明該進程未使用協處理器
/* math */ 0, \
/* fs info */ -1,0022,NULL,NULL,NULL,0, \
/* filp */ {NULL,}, \
//這裡就是很關鍵的一部份,表始初始化一個局部LDT表,即第一個任務的
{ \
{0,0}, \
/* ldt */ {0x9f,0xc0fa00}, \
{0x9f,0xc0f200}, \
}, \
//第一個任務的任務狀態表,跟蹤struct tss_struct可以知道其詳細意義
/*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
,0,0,0,0,0,0,0, \
,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, \
{} \
}, \
}
到這了,也差不多了,額外的部份再看看第一個任務的LDT表與TSS表,由上面可知0號任務的LDT的代碼段與數據段分別為{0x9f,0xc0fa00}與{0x9f,0xc0f200},根據保護模式下的定義,可以代碼段的段基址為0,段限長為640KB,段屬性為存在於內存中、特權級為3,代碼段,同理分析得數據段的段基址為0,段限長為640KB,段屬性為存在於內存中,特權級為3,數據段
第一個任務的狀態表,提一下吧
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;
};
根據這個表可以看到的是任務0的內核態堆棧指針esp0=PAGE_SIZE+(long)&init_task,即第一個PCB塊(task_struct)的頂部空間,PAGE_SIZE=4k,ss0=0x10,0x10查一下head.s你就會發現是gdt的第一個描述符,即內核代碼段,後面還有幾個是對數據段寄存器的定義以及ldt的定義