參考 參考閃客的系列,將開機到執行shell的整個過程濃縮成本文。 https://github.com/dibingfa/flash-linux0.11-talk bootsect.s 當按下開機鍵的那一刻,在主板上提前寫死的固件程式 BIOS 會將硬碟中啟動區的 512 位元組的數據,原封不動複製 ...
參考
參考閃客的系列,將開機到執行shell的整個過程濃縮成本文。
bootsect.s
當按下開機鍵的那一刻,在主板上提前寫死的固件程式 BIOS 會將硬碟中啟動區的 512 位元組的數據,原封不動複製到記憶體中的 0x7c00 這個位置,並跳轉到那個位置進行執行。
Linux-0.11 的最開始的代碼是用彙編語言寫的 bootsect.s,位於 boot 文件夾下。通過編譯,這個 bootsect.s 會被編譯成二進位文件,存放在啟動區的第一扇區。
啟動區的定義非常簡單,只要硬碟中的 0 盤 0 道 1 扇區的 512 個位元組的最後兩個位元組分別是 0x55 和 0xaa,那麼 BIOS 就會認為它是個啟動區。
所以對於我們理解操作系統而言,此時的 BIOS 僅僅就是個代碼搬運工,把 512 位元組的二進位數據從硬碟搬運到了記憶體中而已。所以作為操作系統的開發人員,僅僅需要把操作系統最開始的那段代碼,編譯並存儲在硬碟的 0 盤 0 道 1 扇區即可。之後 BIOS 會幫我們把它放到記憶體里,並且跳過去執行。
_start:
mov $BOOTSEG, %ax #將ds段寄存器設置為0x7C0
mov %ax, %ds
mov $INITSEG, %ax #將es段寄存器設置為0x900
mov %ax, %es
mov $256, %cx #設置移動計數值256字
sub %si, %si #源地址 ds:si = 0x07C0:0x0000
sub %di, %di #目標地址 es:si = 0x9000:0x0000
rep #重覆執行並遞減cx的值
movsw #從記憶體[si]處移動cx個字到[di]處
ljmp $INITSEG, $go #段間跳轉,這裡INITSEG指出跳轉到的段地址,解釋了cs的值為0x9000
這裡就是一件事:把代碼移動到 0x90000 處,然後跳轉 新位置 偏移 go 處。
ljmp $INITSEG, $go
相當於 cs = 0x90000, ip = $go
go: mov %cs, %ax #將ds,es,ss都設置成移動後代碼所在的段處(0x9000)
mov %ax, %ds
mov %ax, %es
# put stack at 0x9ff00.
mov %ax, %ss # ss = 0x9000
mov $0xFF00, %sp #目前的棧頂地址就是ss:sp,即0x9FF00 處。
這一部分是設置棧,把棧頂設置得離代碼足夠遠。
##ah=0x02 讀磁碟扇區到記憶體 al=需要獨出的扇區數量
##ch=磁軌(柱面)號的低八位 cl=開始扇區(位0-5),磁軌號高2位(位6-7)
##dh=磁頭號 dl=驅動器號(硬碟則7要置位)
##es:bx ->指向數據緩衝區;如果出錯則CF標誌置位,ah中是出錯碼
load_setup:
mov $0x0000, %dx # drive 0, head 0
mov $0x0002, %cx # sector 2, track 0
mov $0x0200, %bx # address = 512, in INITSEG
.equ AX, 0x0200+SETUPLEN
mov $AX, %ax # service 2, nr of sectors
int $0x13 # read it
將硬碟的第 2 (cx)個扇區開始,把數據載入到記憶體 0x90200(bx) 處,共載入 4(SETUPLEN) 個扇區
如果載入成功則跳轉到 ok_load_setup
,之後的主要邏輯是把從硬碟第 6 個扇區開始往後的 240 個扇區,載入到記憶體 0x10000 處,然後跳轉到 0x90200
處的代碼,也就是 setup.s 文件的第一行代碼。
ok_load_setup:
...
mov ax,#0x1000
mov es,ax ; segment of 0x10000
call read_it
...
jmpi 0,0x9020
setup.s
setup.s 被編譯成setup 放在磁碟的2~5扇區。
setup的開始部分就是獲取一些參數,存儲在記憶體中:
記憶體地址 | 長度(位元組) | 名稱 |
---|---|---|
0x90000 | 2 | 游標位置 |
0x90002 | 2 | 擴展記憶體數 |
0x90004 | 2 | 顯示頁面 |
0x90006 | 1 | 顯示模式 |
0x90007 | 1 | 字元列數 |
0x90008 | 2 | 未知 |
0x9000A | 1 | 顯示記憶體 |
0x9000B | 1 | 顯示狀態 |
0x9000C | 2 | 顯卡特性參數 |
0x9000E | 1 | 屏幕行數 |
0x9000F | 1 | 屏幕列數 |
0x90080 | 16 | 硬碟1參數表 |
0x90090 | 16 | 硬碟2參數表 |
0x901FC | 2 | 根設備號 |
接著又是進行了記憶體的移動操作:
...
# now we want to move to protected mode ...
cli # no interrupts allowed !
# 因為後面我們要把原本是 BIOS 寫好的中斷向量表給覆蓋掉,也就是給破壞掉了,寫上我們自己的中斷向量表,所以這個時候是不允許中斷進來的。
# first we move the system to it's rightful place
mov $0x0000, %ax
cld # 'direction'=0, movs moves forward
do_move:
mov %ax, %es # destination segment
add $0x1000, %ax
cmp $0x9000, %ax
jz end_move
mov %ax, %ds # source segment
sub %di, %di
sub %si, %si
mov $0x8000, %cx
rep
movsw
jmp do_move
於是,現在的記憶體佈局變成了:
# then we load the segment descriptors
end_move:
mov $SETUPSEG, %ax # right, forgot this at first. didn't work :-)
mov %ax, %ds
lidt idt_48 # load idt with 0,0
lgdt gdt_48 # load gdt with whatever appropriate
這裡會載入idt和gdt。以gdt為例解釋一下:
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
gdt_48: # 註意是小端序,0x800在低16位,0x9在高16位
.word 0x800 # gdt limit=2048, 256 GDT entries
.word 512+gdt, 0x9 # gdt base = 0X9xxxx,
# 512+gdt is the real gdt after setup is moved to 0x9020 * 0x10
gdt_48 的高32位 為 gdt 在記憶體中的地址(gdt是setup文件的偏移,因為setup在記憶體中的起始位置為0x9020,所以要加上0x9020)
ds 寄存器里存儲的值,在實模式下叫做段基址(段基址左移4位加上偏移得到物理地址),在保護模式下叫段選擇子。段選擇子里存儲著段描述符的索引。
通過段描述符索引,可以從全局描述符表 gdt 中找到一個段描述符,段描述符里存儲著段基址。
段基址取出來,再和偏移地址相加,就得到了物理地址,整個過程如下:
inb $0x92, %al # open A20 line(Fast Gate A20).
orb $0b00000010, %al
outb %al, $0x92
打開A20地址線。這是為了相容20位模式,如果不打開,即使有32位地址線,高於20位的位也會被丟掉。
接下來是對可編程中斷控制器 8259 晶元進行的編程。
因為中斷號是不能衝突的, Intel 把 0 到 0x19 號中斷都作為保留中斷,比如 0 號中斷就規定為除零異常,軟體自定義的中斷都應該放在這之後,但是 IBM 在原 PC 機中搞砸了,跟保留中斷號發生了衝突,以後也沒有糾正過來,所以我們得重新對其進行編程,不得不做,卻又一點意思也沒有。這是 Linus 在上面註釋上的原話。
mov %cr0, %eax # get machine status(cr0|MSW)
bts $0, %eax # turn on the PE-bit
mov %eax, %cr0 # protection enabled
啟用保護模式(將cr0的第0位置為1)
# segment-descriptor (INDEX:TI:RPL)
.equ sel_cs0, 0x0008
# select for code segment 0 ( 001:0 :00)
ljmp $sel_cs0, $0 # jmp offset 0 of code segment 0 in gdt
對照段選擇子的結構,可以知道 描述符索引值是 1,也就是要去 全局描述符表(gdt) 中找第一項段描述符。這裡取的就是代碼段描述符,段基址是 0,偏移也是 0,那加一塊就還是 0,所以最終這個跳轉指令,就是跳轉到記憶體地址的 0 地址處,開始執行。就是操作系統全部代碼的 system 這個大模塊的起始處。
head
pg_dir: # 頁目錄在0地址處,會覆蓋掉執行過的代碼
.globl startup_32
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp
再往下連續五個 mov 操作,分別給 ds、es、fs、gs 這幾個段寄存器賦值為 0x10,根據段描述符結構解析,表示這幾個段寄存器的值為指向全局描述符表中的2號段描述符,也就是數據段描述符。
最後 lss 指令相當於讓 ss:esp 這個棧頂指針指向了 _stack_start
這個標號的位置。
這個 stack_start 標號定義在了 sched.c 里:
long user_stack[4096 >> 2];
struct{
long *a;
short b;
} stack_start = { &user_stack[4096 >> 2], 0x10 };
stack_start 結構中的高位 16 位元組是 0x10,將會賦值給 ss 棧段寄存器,低位 32 位元組是 user_stack 這個數組的最後一個元素的地址值,將其賦值給 esp 寄存器。
賦值給 ss 的 0x10 仍然按照保護模式下的段選擇子去解讀,其指向的是全局描述符表中的第二個段描述符(數據段描述符),段基址是 0。
call setup_idt
call setup_gdt
movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs
mov %ax,%gs
lss stack_start,%esp
重新設置idt和gdt,因為原來的是在setup中的,這塊地方接下來要被緩衝區覆蓋掉。所以這裡重新將其設置在head中。因為重新設置了gdt,所以還要重新執行mov刷新一遍才能生效。
...
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
jmp setup_paging
L6:
jmp L6 # main should never return here, but
.align 2
setup_paging:
movl $1024*5,%ecx /* 5 pages - pg_dir(頁目錄)占一頁,4 個頁表分別占一頁 */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl /* 將開頭的5頁記憶體清零 */
movl $pg0+7,pg_dir /* set present r/w bit/user*/
movl $pg1+7,pg_dir+4 /* 這裡加7是為了將最低3位置1,即頁存在,用戶可讀寫*/
movl $pg2+7,pg_dir+8
movl $pg3+7,pg_dir+12
movl $pg3+4092,%edi
.org 0x1000 pg0:
.org 0x2000 pg1:
.org 0x3000 pg2:
.org 0x4000 pg3:
.org 0x5000
setup_paging
會初始化分頁機制,也就是設置好頁目錄和頁表。註意 pg_dir 在 0地址,也就是將之前執行的代碼覆蓋掉,作為頁目錄,存儲了四個頁目錄項。一個頁表包含1024個頁表項,1頁為4KB,因此16M 的地址空間可以用 1 個頁目錄表 + 4 個頁表搞定。
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
cld
這一步通過一個迴圈來填充頁表項,使得線性地址和對應的物理地址一樣。
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 */
這一步設置了頁目錄的起始地址(存儲在cr3寄存器),並且設置cr0的最高位為1以開啟分頁。
ret /* this also flushes prefetch-queue */
ret會跳轉到main函數。這是怎麼實現的呢?註意到在 jmp setup_paging
之前壓入了5個參數,實際上這是模擬call指令的壓棧過程,因此ret後pop出棧頂作為返回地址,即可跳轉到main函數執行。
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
main
記憶體初始化
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/* Interrupts are still disabled. Do necessary setups, then enable them */
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
// EXT_MEM_K 是之前在setup中獲取和設置的
// EXT_MEM_K 存儲的是系統從1MB開始的擴展記憶體數值,單位是KB,所以和以位元組為單位的1MB相加時需要左移10位。
memory_end = (1<<20) + (EXT_MEM_K<<10); // 忽略不到4KB(1頁)的記憶體
memory_end &= 0xfffff000;
// 如果記憶體超過16MB,則按照16MB計算
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
// 如果記憶體大於12MB則緩衝區末端為4MB
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
// 如果記憶體大於6MB則緩衝區末端為2MB
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
// 剩下的情況,也就是記憶體為0MB---6MB,則緩衝區末端為1MB
else
buffer_memory_end = 1*1024*1024;
// 主記憶體起始地址 = 緩衝區末端
main_memory_start = buffer_memory_end;
...
}
這一堆就是劃定主記憶體和緩衝區,確定三個邊界變數:buffer_memory_end、main_memory_start、memory_end
具體主記憶體區是如何管理和分配的,要看 mem_init 里做了什麼。而緩衝區是如何管理和分配的,就要看再後面的 buffer_init 里幹了什麼。
先看mem_init
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;
}
就是對記憶體分頁,mem_map這個數組的每一項管理一頁。
以上圖為例:
- 1M 以下的記憶體這個數組乾脆沒有記錄,這裡的記憶體是無需管理的,或者換個說法是無權管理的,也就是沒有權利申請和釋放,因為這個區域是內核代碼所在的地方,不能被“污染”。
- 1M 到 2M 這個區間是緩衝區,2M 是緩衝區的末端,緩衝區的開始在哪裡之後再說,這些地方不是主記憶體區域,因此直接標記為 USED,產生的效果就是無法再被分配了。
- 2M 以上的空間是主記憶體區域,而主記憶體目前沒有任何程式申請,所以初始化時統統都是零,未來等著應用程式去申請和釋放這裡的記憶體資源。
中斷初始化
- trap_init:給0到48號中斷設置中斷處理函數
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); /* 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,¶llel_interrupt);
}
- tty_init:設置鍵盤中斷的中斷處理函數
- sti:開啟中斷(set interrupt flag)
#define sti() __asm__ ("sti"::)
塊設備初始化
一次讀盤的請求用一個request結果來表示,使用request數組維護所有的請求。
/*
* The request-struct contains all necessary data
* to load a nr of sectors into memory
*/
struct request request[NR_REQUEST];
/*
* Ok, this is an expanded form so that we can use the same
* request for paging requests when that is implemented. In
* paging, 'bh' is NULL, and 'waiting' is used to wait for
* read/write completion.
*/
struct request {
int dev; /* 設備號,-1 表示無請求 */
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; /* 鏈表,指向下一個 */
};
void blk_dev_init(void)
{
int i;
for (i=0 ; i<NR_REQUEST ; i++) {
request[i].dev = -1;
request[i].next = NULL;
}
}
時間初始化
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_READ 負責從CMOS指定埠讀取時間數據
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})
進程調度初始化
void sched_init(void)
{
int i;
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
// 設置init_task的TSS和LDT
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;
// 餘下的項清0
for(i=1;i<NR_TASKS;i++) {
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
ltr(0);
lldt(0);
// 設置定時器
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
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);
}
TSS 叫任務狀態段,就是保存和恢復進程的上下文的,所謂上下文,其實就是各個寄存器的信息而已,這樣進程切換的時候,才能做到保存和恢覆上下文,繼續執行。
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;
};
而 LDT 叫局部描述符表,是與 GDT 全局描述符表相對應的,內核態的代碼用 GDT 里的數據段和代碼段,而用戶進程的代碼用每個用戶進程自己的 LDT 里的數據段和代碼段。
每個進程用一個 task_struct
表示,裡面就有 ldt
和 tss
兩個成員。ldt包含三項,分別為0、cs(代碼段)、ds&ss(數據段)
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;
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;
};
緩衝區初始化
緩衝區被分成一個個1024byte的塊,每個塊對應一個buffer_head
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;
};
extern int end; // end 是鏈接器計算出的內核代碼的末尾地址
struct buffer_head * start_buffer = (struct buffer_head *) &end;
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;
// 緩衝區結尾側的 b 每次迴圈 -1024,也就是一頁的值,緩衝區開頭側的 h 每次迴圈 +1(一個 buffer_head 大小的記憶體),直到碰一塊為止。
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;
}
硬碟初始化
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);
}
/* blk_dev_struct is:
* do_request-address
* next-request
*/
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 */
};
一個新進程的產生
move_to_user_mode
數據訪問只能高特權級訪問低特權級,代碼跳轉只能同特權級跳轉,要想實現特權級轉換,可以通過中斷和中斷返回來實現。
沒有中斷,就構造中斷,中斷發生時會按順序push 5個寄存器,中斷返回時再pop出來,我們只需要在自己push的值裡面做點手腳,把特權級設置成用戶態,中斷返回後就是處於用戶態了。
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \ // SS
"pushl %%eax\n\t" \ // ESP
"pushfl\n\t" \ // EFLAGS
"pushl $0x0f\n\t" \ // CS
"pushl $1f\n\t" \ // EIP
"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")
iret 返回後,pop出我們push的值給相應的寄存器,因此:
- CS = 0x0f
- EIP = 標簽1的地址
段選擇子最後兩位 11 表示特權級為 3,即用戶態。倒數第三位 TI 表示,前面的描述符索引,是從 GDT 還是 LDT 中取,1 表示 LDT,也就是從局部描述符表中取。而LDT的第二項剛好就是代碼段描述符。因此只需要讓最後三位為1,iret返回後自然就是用戶態了。
進程調度
我們在shed_init中設置過時鐘中斷的處理函數:
set_intr_gate(0x20,&timer_interrupt);
這樣,當時鐘中斷,也就是 0x20 號中斷來臨時,CPU 會查找中斷向量表中 0x20 處的函數地址,即中斷處理函數,並跳轉過去執行。
.align 2
timer_interrupt:
push %ds # save ds,es and put kernel data space
push %es # into them. %fs is used by _system_call
push %fs
pushl %edx # we save %eax,%ecx,%edx as gcc doesn't
pushl %ecx # save those across function calls. %ebx
pushl %ebx # is saved as we use that in ret_sys_call
pushl %eax
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax
mov %ax,%fs
incl jiffies
movb $0x20,%al # EOI to interrupt controller #1
outb %al,$0x20
movl CS(%esp),%eax # 發生中斷時處理器自動壓入CS,這裡讀取出來,檢查CPL(current privilege level)
andl $3,%eax # %eax is CPL (0 or 3, 0=supervisor)
pushl %eax # CPL 作為參數
call do_timer # 'do_timer(long CPL)' does everything from
addl $4,%esp # task switching to accounting ...
jmp ret_from_sys_call
timer_interrupt
函數做了兩件事,一個是將系統滴答數這個變數 jiffies 加一,一個是調用了另一個函數 do_timer。
void do_timer(long cpl)
{
extern int beepcount;
extern void sysbeepstop(void);
if (beepcount)
if (!--beepcount)
sysbeepstop();
if (cpl)
current->utime++;
else
current->stime++;
...
if (current_DOR & 0xf0)
do_floppy_timer();
if ((--current->counter)>0) return; // 時間片未到0,返回
current->counter=0;
if (!cpl) return; // 如果當前是內核態則不調度
schedule(); // 時間片到0,且為用戶模式,進行調度。
}
#define FIRST_TASK task[0]
#define LAST_TASK task[NR_TASKS-1]
void schedule(void)
{
int i,next,c;
struct task_struct ** p;
/* check alarm, wake up any interruptible tasks that have got a signal */
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) {
if ((*p)->alarm && (*p)->alarm < jiffies) {
(*p)->signal |= (1<<(SIGALRM-1));
(*p)->alarm = 0;
}
// (*p)->signal 表示待處理的信號
// ~(_BLOCKABLE & (*p)->blocked)) 表示未被屏蔽的信號
// TASK_INTERRUPTIBLE: 處於睡眠狀態,並且等待某個信號
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE)
(*p)->state=TASK_RUNNING;
}
/* this is the scheduler proper: */
while (1) {
c = -1; // 所有進程剩餘時間片的最大值
next = 0; // 最大剩餘時間片進程的索引
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c) break; // 如果存在一個剩餘時間片不為0的任務,則break,否則設置所有任務的剩餘時間片
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
// 切換到目標進程
switch_to(next);
}
#define FIRST_TSS_ENTRY 4
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
// FIRST_TSS_ENTRY<<3表示左移3位,因為TI和RPL總共占3位
// n<<4,實際上索引加上 n<<1,因為一個進程占一個TSS和一個LDT
#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
/*
* switch_to(n) should switch tasks to task nr n, first
* checking that n isn't the current task, in which case it does nothing.
* This also clears the TS-flag if the task we switched to has used
* tha math co-processor latest.
*/
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" # 先比較是不是要切換到當前任務 \
"je 1f\n\t" # 如果是就什麼都不做 \
"movw %%dx,%1\n\t" # 把TSS賦給__tmp.b \
"xchgl %%ecx,current\n\t" # 交換 ecx 和 current \
"ljmp *%0\n\t" # 將__tmp.b作為段選擇子 \
"cmpl %%ecx,last_task_used_math\n\t" \
"jne 1f\n\t" \
"clts\n" \
"1:" \
::"m" (*&__tmp.a),"m" (*&__tmp.b), \
"d" (_TSS(n)),"c" ((long) task[n])); \
}
這裡 "d" (_TSS(n))
表示把要切換到的進程的TSS段選擇子載入給 edx,"c" ((long) task[n])
表示把要切換到的進程的任務結構指針載入給 ecx
CPU 規定,如果 ljmp 指令後面跟的是一個 tss 段選擇子,那麼,會由硬體將當前各個寄存器的值保存在當前進程的 tss 中,並將新進程的 tss 信息載入到各個寄存器。 CPU得到TSS描述符後,就會將其載入到任務寄存器TR中,然後根據TSS描述符的信息(主要是基址)找到任務的tss內容(包括所有的寄存器信息,如eip),根據其內容就可以開始新任務的運行。
那麼CPU怎麼識別描述符是TSS描述符而不是其他描述符呢?這是因為所有描述符(一個描述符是64位)中都有4位用來指示該描述符的類型,如描述符類型值是9或11都表示該描述符是TSS描述符。
fork
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
static inline _syscall0(int,fork)
巨集展開:
int fork(void) {
volatile long __res;
_asm {
_asm mov eax,__NR_fork
_asm int 80h
_asm mov __res,eax
}
if (__res >= 0)
return (void) __res;
errno = -__res;
return -1;
}
系統調用統一通過 int 0x80 中斷來進入,具體調用這個表裡的哪個功能函數,就由 eax 寄存器傳過來,這裡的值是個數組索引的下標,通過這個下標就可以找到在 sys_call_table 這個數組裡的具體函數。
.align 2
system_call:
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call *sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
ret_from_sys_call:
movl current,%eax # task[0] cannot have signals
cmpl task,%eax
je 3f
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
movl signal(%eax),%ebx
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
linux/sys.h
中可以找到 sys_call_table
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid, sys_iam, sys_whoami };
如果是fork,則會調用到sys_fork
.align 2
sys_fork:
call find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call copy_process
addl $20,%esp
1: ret
int find_empty_process(void)
{
int i;
repeat:
if ((++last_pid)<0) last_pid=1;
for(i=0 ; i<NR_TASKS ; i++)
if (task[i] && task[i]->pid == last_pid) goto repeat; // 如果last_pid被某個進程使用了,就增加last_pid
for(i=1 ; i<NR_TASKS ; i++)
if (!task[i]) // 找到一個空位
return i;
return -EAGAING;
}
/*
* Ok, this is the main fork-routine. It copies the system process
* information (task[nr]) and sets up the necessary registers. It
* also copies the data segment in it's entirety.
*/
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)
{
struct task_struct *p;
int i;
struct file *f;
p = (struct task_struct *) get_free_page(); // 為每個進程分配一頁
if (!p)
return -EAGAIN;
task[nr] = p; // 將新分配出的頁的起始地址記錄到task[]中,也就是把頁的底部作為task_struct
// NOTE!: the following statement now work with gcc 4.3.2 now, and you
// must compile _THIS_ memcpy without no -O of gcc.#ifndef GCC4_3
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
p->father = current->pid;
p->counter = p->priority;
p->signal = 0;
p->alarm = 0;
p->leader = 0; /* process leadership doesn't inherit */
p->utime = p->stime = 0;
p->cutime = p->cstime = 0;
p->start_time = jiffies;
p->tss.back_link = 0;
// ss0 和 esp0 表示 0 特權級也就是內核態時的 ss:esp 的指向。
p->tss.esp0 = PAGE_SIZE + (long) p; // 因此新分配的頁的頂部作為內核棧
p->tss.ss0 = 0x10;
p->tss.eip = eip;
p->tss.eflags = eflags;
p->tss.eax = 0;
p->tss.ecx = ecx;
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
p->tss.ebp = ebp;
p->tss.esi = esi;
p->tss.edi = edi;
p->tss.es = es & 0xffff;
p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
p->tss.ldt = _LDT(nr);
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
for (i=0; i<NR_OPEN;i++)
if ((f=p->filp[i]))
f->f_count++;
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
p->state = TASK_RUNNING; /* do this last, just in case */
return last_pid;
}
copy_mem 主要負責ldt的賦值,邏輯地址通過分段機制轉為線性地址,線性地址再通過分頁機制轉為物理地址。
ldt保存了進程代碼段和數據段的段選擇子。
#define _set_base(addr,base) \
__asm__ ("push %%edx\n\t" \
"movw %%dx,%0\n\t" \
"rorl $16,%%edx\n\t # edx >>= 16" \
"movb %%dl,%1\n\t" \
"movb %%dh,%2\n\t" \
"pop %%edx" \
::"m" (*((addr)+2)), \
"m" (*((addr)+4)), \
"m" (*((addr)+7)), \
"d" (base) \
)
#define set_base(ldt,base) _set_base( ((char *)&(ldt)) , (base) )
// 一個段描述符通常是8位元組,基地址占其中的4位元組,分佈在第2、第3、第4和第7位元組中。
int copy_mem(int nr,struct task_struct * p)
{
unsigned long old_data_base,new_data_base,data_limit;
unsigned long old_code_base,new_code_base,code_limit;
code_limit=get_limit(0x0f); // 取進程0設置好的段長
data_limit=get_limit(0x17);
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
if (data_limit < code_limit)
panic("Bad data_limit");
new_data_base = new_code_base = nr * 0x4000000; // 每個進程占線性地址空間 64M
p->start_code = new_code_base;
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
printk("free_page_tables: from copy_mem\n");
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return 0;
}
copy_mem
最後進行了 copy_page_tables
,將老進程的頁表拷貝給新進程,讓新舊進程共用同一份物理地址空間
/*
* Well, here is one of the most complicated functions in mm. It
* copies a range of linerar addresses by copying only the pages.
* Let's hope this is bug-free, 'cause this one I don't want to debug :-)
*
* Note! We don't copy just any chunks of memory - addresses have to
* be divisible by 4Mb (one page-directory entry), as this makes the
* function easier. It's used only by fork anyway.
*
* NOTE 2!! When from==0 we are copying kernel space for the first
* fork(). Then we DONT want to copy a full page-directory entry, as
* that would lead to some serious memory waste - we just copy the
* first 160 pages - 640kB. Even that is more than we need, but it
* doesn't take any more memory - we don't copy-on-write in the low
* 1 Mb-range, so the pages can be shared with the kernel. Thus the
* special case for nr=xxxx.
*/
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
unsigned long * from_page_table;
unsigned long * to_page_table;
unsigned long this_page;
unsigned long * from_dir, * to_dir;
unsigned long nr;
if ((from&0x3fffff) || (to&0x3fffff))
panic("copy_page_tables called with wrong alignment");
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to>>20) & 0xffc);
size = ((unsigned) (size+0x3fffff)) >> 22;
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir)
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
if (!(to_page_table = (unsigned long *) get_free_page())) // 分配一個頁作為頁表
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7; // 頁表地址填入頁目錄
nr = (from==0)?0xA0:1024;
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) { // 從from_page_table拷貝頁表項到to_page_table
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2; // 設置為只讀,實現Copy On Write,新老進程一開始共用同一個物理記憶體空間,如果只有讀,那就相安無事,但如果任何一方有寫操作,由於頁面是只讀的,將觸發缺頁中斷,然後就會分配一塊新的物理記憶體給產生寫操作的那個進程,此時這一塊記憶體就不再共用了。
*to_page_table = this_page;
if (this_page > LOW_MEM) {
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();
return 0;
}
shell 的到來
由於 fork 函數一調用,就又多出了一個進程,子進程(進程 1)會返回 0,父進程(進程 0)返回子進程的 ID,所以 init 函數只有進程 1 才會執行。
void main(void) {
...
move_to_user_mode();
if (!fork()) {
init();
}
for(;;) pause();
}
void init(void) {
...
setup((void *) &drive_info);
...
}
setup 是個系統調用,會通過中斷最終調用到 sys_setup 函數
setup 傳入的drive_info 是來自記憶體 0x90080 的數據,這部分是由之前 setup.s 程式將硬碟 1 的參數信息放在這裡了,包括柱面數、磁頭數、扇區數等信息。
#define MAX_HD 2
struct hd_i_struct { // 硬碟參數
int head; // 磁頭數
int sect; // 每磁軌扇區數
int cyl; // 柱面數
int wpcom; // 寫前預補償柱面號
int lzone; // 磁頭著陸區柱面號
int ctl; // 控制位元組
};
struct hd_i_struct hd_info[] = { {0,0,0,0,0,0},{0,0,0,0,0,0} };
static struct hd_struct { // 硬碟分區
long start_sect; // 開始扇區
long nr_sects; // 總扇區數
} hd[5*MAX_HD] = {}
struct partition {
unsigned char boot_ind; /* 0x80 - active (unused) */
unsigned char head; /* ? */
unsigned char sector; /* ? */
unsigned char cyl; /* ? */
unsigned char sys_ind; /* ? */
unsigned char end_head; /* ? */
unsigned char end_sector; /* ? */
unsigned char end_cyl; /* ? */
unsigned int start_sect; /* starting sector counting from 0 */
unsigned int nr_sects; /* nr of sectors in partition */
};
int sys_setup(void * BIOS)
{
static int callable = 1; // callable 保證 sys_setup 只被調用一次
int i,drive;
unsigned char cmos_disks;
struct partition *p;
struct buffer_head * bh;
if (!callable)
return -1;
callable = 0;
#ifndef HD_TYPE
for (drive=0 ; drive<2 ; drive++) {
hd_info[drive].cyl = *(unsigned short *) BIOS;
hd_info[drive].head = *(unsigned char *) (2+BIOS);
hd_info[drive].wpcom = *(unsigned short *) (5+BIOS);
hd_info[drive].ctl = *(unsigned char *) (8+BIOS);
hd_info[drive].lzone = *(unsigned short *) (12+BIOS);
hd_info[drive].sect = *(unsigned char *) (14+BIOS);
BIOS += 16;
}
if (hd_info[1].cyl)
NR_HD=2;
else
NR_HD=1;
#endif
// 設置硬碟分區表
for (i=0 ; i<NR_HD ; i++) {
hd[i*5].start_sect = 0;
// 總扇區數 = 磁頭數 * 每磁軌扇區數 * 柱面數
hd[i*5].nr_sects = hd_info[i].head * hd_info[i].sect * hd_info[i].cyl;
}
if ((cmos_disks = CMOS_READ(0x12)) & 0xf0)
if (cmos_disks & 0x0f)
NR_HD = 2;
else
NR_HD = 1;
else
NR_HD = 0;
for (i = NR_HD ; i < 2 ; i++) {
hd[i*5].start_sect = 0;
hd[i*5].nr_sects = 0;
}
for (drive=0 ; drive<NR_HD ; drive++) {
// 0x300:第一塊設備的主設備號
// 0:讀取第一個塊
if (!(bh = bread(0x300 + drive*5,0))) {
printk("Unable to read partition table of drive %d\n\r",
drive);
panic("");
}
// 檢查魔數
if (bh->b_data[510] != 0x55 || (unsigned char)
bh->b_data[511] != 0xAA) {
printk("Bad partition table on drive %d\n\r",drive);
panic("");
}
// 硬碟分區表的信息在硬碟的第一個扇區的 0x1BE 偏移處
p = 0x1BE + (void *)bh->b_data;
for (i=1;i<5;i++,p++) {
hd[i+5*drive].start_sect = p->start_sect;
hd[i+5*drive].nr_sects = p->nr_sects;
}
brelse(bh);
}
if (NR_HD)
printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":"");
rd_load();
mount_root();
return (0);
}
setup 方法中的最後一個函數 mount_root,載入根文件系統。有了根文件系統之後,操作系統才能從一個根兒開始找到所有存儲在硬碟中的文件,所以它是文件系統的基石,很重要。
從整體上說,它就是要把硬碟中的數據,以文件系統的格式進行解讀,載入到記憶體中設計好的數據結構,這樣操作系統就可以通過記憶體中的數據,以文件系統的方式訪問硬碟中的一個個文件了。
struct m_inode {
unsigned short i_mode;
unsigned short i_uid;
unsigned long i_size;
unsigned long i_mtime;
unsigned char i_gid;
unsigned char i_nlinks;
unsigned short i_zone[9];
/* these are in memory also */
struct task_struct * i_wait;
unsigned long i_atime;
unsigned long i_ctime;
unsigned short i_dev;
unsigned short i_num;
unsigned short i_count;
unsigned char i_lock;
unsigned char i_dirt;
unsigned char i_pipe;
unsigned char i_mount;
unsigned char i_seek;
unsigned char i_update;
};
struct file {
unsigned short f_mode;
unsigned short f_flags;
unsigned short f_count;
struct m_inode * f_inode;
off_t f_pos;
};
struct super_block {
unsigned short s_ninodes; // inode 的總數
unsigned short s_nzones;
unsigned short s_imap_blocks;
unsigned short s_zmap_blocks;
unsigned short s_firstdatazone;
unsigned short s_log_zone_size;
unsigned long s_max_size;
unsigned short s_magic;
/* These are only in memory */
struct buffer_head * s_imap[8];
struct buffer_head * s_zmap[8];
unsigned short s_dev;
struct m_inode * s_isup;
struct m_inode * s_imount;
unsigned long s_time;
struct task_struct * s_wait;
unsigned char s_lock;
unsigned char s_rd_only;
unsigned char s_dirt;
};
#define NR_FILE 64
struct file file_table[NR_FILE];
void mount_root(void)
{
int i,free;
struct super_block * p;
struct m_inode * mi;
for(i=0;i<NR_FILE;i++)
file_table[i].f_count=0; // f_count 表示被引用的次數
for(p = &super_block[0] ; p < &super_block[NR_SUPER] ; p++) {
p->s_dev = 0;
p->s_lock = 0;
p->s_wait = NULL;
}
if (!(p=read_super(ROOT_DEV))) // 讀取硬碟的超級塊信息到記憶體中來
panic("Unable to mount root");
if (!(mi=iget(ROOT_DEV,ROOT_INO))) // 讀取根 inode 信息
panic("Unable to read root i-node");
mi->i_count += 3 ; /* NOTE! it is logically used 4 times, not 1 */
p->s_isup = p->s_imount = mi;
current->pwd = mi;
current->root = mi;
free=0;
i=p->s_nzones;
while (-- i >= 0)
if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data))
free++;
printk("%d/%d free blocks\n\r",free,p->s_nzones);
free=0;
i=p->s_ninodes+1;
while (-- i >= 0)
if (!set_bit(i&8191,p->s_imap[i>>13]->b_data))
free++;
printk("%d/%d free inodes\n\r",free,p->s_ninodes);
}
首先硬碟中的文件系統,無非就是硬碟中的一堆數據,我們按照一定格式去解析罷了。Linux-0.11 中的文件系統是 MINIX 文件系統,它就長成這個樣子。
每一個塊結構的大小是 1024 位元組,也就是 1KB,硬碟里的數據就按照這個結構,妥善地安排在硬碟里。
可是硬碟中憑什麼就有了這些信息呢?這就是個雞生蛋蛋生雞的問題了。你可以先寫一個操作系統,然後給一個硬碟做某種文件系統類型的格式化,這樣你就得到一個有文件系統的硬碟了,有了這個硬碟,你的操作系統就可以成功啟動了。
MINIX 文件系統的格式:
- 引導塊就是我們系列最開頭說的啟動區,當然不一定所有的硬碟都有啟動區,但我們還是得預留出這個位置,以保持格式的統一。
- 超級塊用於描述整個文件系統的整體信息,我們看它的欄位就知道了,有後面的 inode 數量,塊數量,第一個塊在哪裡等信息。有了它,整個硬碟的佈局就清晰了。
- inode 點陣圖和塊點陣圖,就是點陣圖的基本操作和作用了,表示後面 inode 和塊的使用情況。
- inode 存放著每個文件或目錄的元信息和索引信息,元信息就是文件類型、文件大小、修改時間等,索引信息就是大小為 9 的 i_zone[9] 塊數組,表示這個文件或目錄的具體數據占用了哪些塊。其中塊數組裡,0~6 表示直接索引,7 表示一次間接索引,8 表示二次間接索引。當文件比較小時,比如只占用 2 個塊就夠了,那就只需要 zone[0] 和 zone[1] 兩個直接索引即可。
再往後,就都是存放具體文件或目錄實際信息的塊了。如果是一個普通文件類型的 inode 指向的塊,那裡面就直接是文件的二進位信息。如果是一個目錄類型的 inode 指向的塊,那裡面存放的就是這個目錄下的文件和目錄的 inode 索引以及文件或目錄名稱等信息。
init 接下來會調用open打開"/dev/tty0"文件
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
int sys_open(const char * filename,int flag,int mode)
{
struct m_inode * inode;
struct file * f;
int i,fd;
mode &= 0777 & ~current->umask;
// 在進程文件描述符數組 filp 中找到一個空閑項
for(fd=0 ; fd<NR_OPEN ; fd++)
if (!current->filp[fd])
break;
if (fd>=NR_OPEN)
return -EINVAL;
current->close_on_exec &= ~(1<<fd);
// 在系統文件表 file_table 中找到一個空閑項
f=0+file_table;
for (i=0 ; i<NR_FILE ; i++,f++)
if (!f->f_count) break;
if (i>=NR_FILE)
return -EINVAL;
// 將進程的文件描述符數組項和系統的文件表項,對應起來
(current->filp[fd]=f)->f_count++;
// 根據文件名從文件系統中找到這個文件
if ((i=open_namei(filename,flag,mode,&inode))<0) {
current->filp[fd]=NULL;
f->f_count=0;
return i;
}
/* ttys are somewhat special (ttyxx major==4, tty major==5) */
if (S_ISCHR(inode->i_mode)) {
if (MAJOR(inode->i_zone[0])==4) {
if (current->leader && current->tty<0) {
current->tty = MINOR(inode->i_zone[0]);
tty_table[current->tty].pgrp = current->pgrp;
}
} else if (MAJOR(inode->i_zone[0])==5)
if (current->tty<0) {
iput(inode);
current->filp[fd]=NULL;
f->f_count=0;
return -EPERM;
}
}
/* Likewise with block-devices: check for floppy_change */
if (S_ISBLK(inode->i_mode))
check_disk_change(inode->i_zone[0]);
f->f_mode = inode->i_mode;
f->f_flags = flag;
f->f_count = 1;
f->f_inode = inode;
f->f_pos = 0;
return (fd);
}
// 從進程的 filp 中找到下一個空閑項,然後把要複製的文件描述符 fd 的信息,統統複製到這裡
static int dupfd(unsigned int fd, unsigned int arg)
{
if (fd >= NR_OPEN || !current->filp[fd])
return -EBADF;
if (arg >= NR_OPEN)
return -EINVAL;
// 在進程文件描述符數組 filp 中找到一個空閑項
while (arg < NR_OPEN)
if (current->filp[arg])
arg++;
else
break;
if (arg >= NR_OPEN)
return -EMFILE;
current->close_on_exec &= ~(1<<arg);
(current->filp[arg] = current->filp[fd])->f_count++;
return arg;
}
int sys_dup(unsigned int fildes)
{
return dupfd(fildes,0);
}
execve
void init(void) {
...
if (!(pid=fork())) {
close(0);
open("/etc/rc",O_RDONLY,0);
execve("/bin/sh",argv_rc,envp_rc);
_exit(2);
}
...
}
init 進程接著fork出一個新進程,新進程通過 close 和 open 函數,將 0 號文件描述符指向的標準輸入 /dev/tty0 更換為指向 /etc/rc 文件
接下來進程 2 就將變得不一樣了,會通過一個 execve 函數調用,使自己搖身一變,成為 /bin/sh 程式繼續運行!
.align 2
sys_execve:
lea EIP(%esp),%eax
pushl %eax # 傳入do_execve的第一個參數:調用方觸發系統調用時由 CPU 壓入棧空間中的 eip 的指針
call do_execve
addl $4,%esp
ret
struct exec {
unsigned long a_magic; /* Use macros N_MAGIC, etc for access */
unsigned a_text; /* length of text, in bytes */
unsigned a_data; /* length of data, in bytes */
unsigned a_bss; /* length of uninitialized data area for file, in bytes */
unsigned a_syms; /* length of symbol table data in file, in bytes */
unsigned a_entry; /* start address */
unsigned a_trsize; /* length of relocation info for text, in bytes */
unsigned a_drsize; /* length of relocation info for data, in bytes */
};
/*
* MAX_ARG_PAGES defines the number of pages allocated for arguments
* and envelope for the new program. 32 should suffice, this gives
* a maximum env+arg of 128kB !
*/
#define MAX_ARG_PAGES 32
/*
* 'do_execve()' executes a new program.
* eip 指向的記憶體處保存了系統調用時由 CPU 壓入棧空間中的 eip
* tmp 是一個無用的占位參數。
* filename 是 "/bin/sh"
* argv 是 { "/bin/sh", NULL }
* envp 是 { "HOME=/", NULL }
*/
int do_execve(unsigned long * eip,long tmp,char * filename,
char ** argv, char ** envp)
{
struct m_inode * inode;
struct buffer_head * bh;
struct exec ex;
unsigned long page[MAX_ARG_PAGES];
int i,argc,envc;
int e_uid, e_gid;
int retval;
int sh_bang = 0;
unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;
if ((0xffff & eip[1]) != 0x000f)
panic("execve called from supervisor mode");
for (i=0 ; i<MAX_ARG_PAGES ; i++) /* clear page-table */
page[i]=0;
// 根據文件名 /bin/sh 獲取 inode
if (!(inode=namei(filename))) /* get executables inode */
return -ENOENT;
argc = count(argv);
envc = count(envp);
restart_interp:
if (!S_ISREG(inode->i_mode)) { /* must be regular file */
retval = -EACCES;
goto exec_error2;
}
i = inode->i_mode;
e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
if (current->euid == inode->i_uid)
i >>= 6;
else if (current->egid == inode->i_gid)
i >>= 3;
if (!(i & 1) &&
!((inode->i_mode & 0111) && suser())) {
retval = -ENOEXEC;
goto exec_error2;
}
// 根據 inode 讀取文件第一塊數據(1024KB)
if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {
retval = -EACCES;
goto exec_error2;
}
// 解析這 1KB 的數據為 exec 結構
ex = *((struct exec *) bh->b_data); /* read exec-header */
if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {
/*
* This section does the #! interpretation.
* Sorta complicated, but hopefully it will work. -TYT
*/
...
}
// 已經把這個緩衝塊內容解析成 exec 結構保存到我們程式的棧空間里了,那麼這個緩衝塊就可以釋放
brelse(bh);
if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
ex.a_text+ex.a_data+ex.a_bss>0x3000000 ||
inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) {
retval = -ENOEXEC;
goto exec_error2;
}
if (N_TXTOFF(ex) != BLOCK_SIZE) {
printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
retval = -ENOEXEC;
goto exec_error2;
}
if (!sh_bang) {
// 往參數表裡面存放信息,不過具體存放的只是字元串常量值的信息,隨後他們將被引用
p = copy_strings(envc,envp,page,p,0);
p = copy_strings(argc,argv,page,p,0);
if (!p) {
retval = -ENOMEM;
goto exec_error2;
}
}
/* OK, This is the point of no return */
if (current->executable)
iput(current->executable);
current->executable = inode;
for (i=0 ; i<32 ; i++)
current->sigaction[i].sa_handler = NULL;
for (i=0 ; i<NR_OPEN ; i++)
if ((current->close_on_exec>>i)&1)
sys_close(i);
current->close_on_exec = 0;
free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
if (last_task_used_math == current)
last_task_used_math = NULL;
current->used_math = 0;
// change_ldt: 根據 ex.a_text 修改局部描述符中的代碼段限長 code_limit
// ex 結構里的 a_text 是生成 /bin/sh 這個 a.out 格式的文件時,寫在頭部的值,用來表示代碼段的長度。至於具體是怎麼生成的,我們無需關心。
// 由於這個函數返回值是數據段限長,也就是 64M,所以最終的 p 值被調整為了以每個進程的線性地址空間視角下的地址偏移
p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
p = (unsigned long) create_tables((char *)p,argc,envc);
current->brk = ex.a_bss +
(current->end_data = ex.a_data +
(current->end_code = ex.a_text));
current->start_stack = p & 0xfffff000;
current->euid = e_uid;
current->egid = e_gid;
i = ex.a_text+ex.a_data;
while (i&0xfff)
put_fs_byte(0,(char *) (i++));
// 代碼指針 eip 決定了 CPU 將執行哪一段指令,棧指針 esp 決定了 CPU 壓棧操作的位置,以及讀取棧空間數據的位置,在高級語言視角下就是局部變數以及函數調用鏈的棧幀。
// 所以這兩行代碼,第一行重新設置了代碼指針 eip 的值,指向 /bin/sh 這個 a.out 格式文件的頭結構 exec 中的 a_entry 欄位,表示該程式的入口地址。
// 第二行重新設置了棧指針 esp 的值,指向了我們經過一路計算得到的 p,也就是圖中 sp 的值。將這個值作為新的棧頂十分合理。
eip[0] = ex.a_entry; /* eip, magic happens :-) */
eip[3] = p; /* stack pointer */
// 設置完 eip 和 esp,中斷返回後pop出來就能達到執行新進程的效果。
return 0;
exec_error2:
iput(inode);
exec_error1:
for (i=0 ; i<MAX_ARG_PAGES ; i++)
free_page(page[i]);
return(retval);
}
缺頁中斷
execve 載入 /bin/sh
返回後就會跳轉到 a.out 格式頭部數據結構 exec.a_entry 所指向的記憶體地址去執行指令。
gdb 調試發現會發現跳到邏輯地址 0處。由於我們現在所處的代碼是屬於進程 2,所以邏輯地址 0 通過分段機制映射到線性地址空間,就是 0x8000000,表示 128M 位置處。
128M 這個線性地址,隨後將會通過分頁機制的映射轉化為物理地址,這才定位到最終的真實物理記憶體。
可是,128M 這個線性地址並沒有頁表映射它,也就是因為上面我們說的,我們除了 /bin/sh 文件的頭部載入到了記憶體外,其他部分並沒有進行載入操作。
再準確點說,是 0x8000000 這個線性地址的訪問,遇到了頁表項的存在位 P 等於 0 的情況。
一旦遇到了這種情況,CPU 會觸發一個中斷:頁錯誤(Page-Fault),CPU 會幫我們保存在中斷的出錯碼 Error Code 里。
.globl page_fault
page_fault:
xchgl %eax,(%esp) # 將錯誤碼保存到eax
pushl %ecx
pushl %edx
push %ds
push %es
push %fs
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
movl %cr2,%edx
pushl %edx
pushl %eax
testl $1,%eax # 檢查錯誤碼的第0位,如果為0則走do_no_page,處理缺頁邏輯
jne 1f
call do_no_page
jmp 2f
1: call do_wp_page
2: addl $8,%esp
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret
void do_no_page(unsigned long error_code,unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;
// 對齊到 4KB
address &am