main() 函数解析(一)——Linux-0.11 剖析笔记(六)
文章目錄
- 1. 宏定義`_syscall0`
- 2. `setup.s`讀取的參數(shù)
- 3. 讀取CMOS實(shí)時(shí)時(shí)鐘信息
- 3.1 `outb_p(value,port)`
- 3.2 `inb_p(port)`
- 3.3 `outb(value,port)`和`inb(port)`
- 3.4 CMOS與RTC
- 3.5 time_init函數(shù)
- 4. main函數(shù)
- 4.1 根設(shè)備號(hào)
- 4.2 復(fù)制硬盤參數(shù)表
- 4.3 計(jì)算主內(nèi)存起始位置
- 4.4 虛擬盤
- 4.5 `mem_init`函數(shù)
- 4.6 `trap_init`函數(shù)
- 4.6.1 `set_trap_gate(n,addr) `
- 4.6.2 `idt`數(shù)組
- 4.6.3 `set_system_gate(n,addr)`
說明:本文由舊文 main()函數(shù)解析(一) 修改而來,略有改動(dòng)。
經(jīng)過了前面的各種鋪墊,終于來到了main函數(shù)。
1. 宏定義_syscall0
先說這幾個(gè)內(nèi)聯(lián)函數(shù)
static inline _syscall0(int,fork) static inline _syscall0(int,pause) static inline _syscall1(int,setup,void *,BIOS) static inline _syscall0(int,sync)比如
static inline _syscall0(int,fork)_syscall0()是在文件unistd.h中定義,它以內(nèi)嵌匯編的形式調(diào)用 Linux 的系統(tǒng)調(diào)用中斷 int 0x80。
系統(tǒng)調(diào)用(通常稱為syscalls)是 Linux內(nèi)核與上層應(yīng)用程序進(jìn)行交互通信的唯一接口。用戶程序通過直接或間接(通過庫函數(shù))調(diào)用中斷int 0x80(在eax寄存器中指定系統(tǒng)調(diào)用功能號(hào)),即可使用內(nèi)核資源,包括系統(tǒng)硬件資源。
_syscall0()其實(shí)是一個(gè)宏,這個(gè)宏定義在include/unistd.h 文件第 133 行:
#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; \ }第 5 行:匯編語句,表示系統(tǒng)調(diào)用,產(chǎn)生軟中斷,0x80 號(hào)中斷;
第 6 行:輸出部分,"=a" 表示用寄存器 eax,當(dāng)匯編語句執(zhí)行后,把 eax 的值傳給變量__res,作為返回值;
第 7 行:輸入部分,“0” 表示和第 0 個(gè)輸出變量有相同的約束,即使用寄存器 eax,把__NR_name的值賦給 eax,指明系統(tǒng)調(diào)用功能號(hào);
第 8~9 行: 如果返回值 >=0,則直接返回該值;
第10~11行: 否則置出錯(cuò)號(hào)(errno 是全局變量),并返回-1。
內(nèi)嵌匯編語法如下。對(duì)此不熟悉的朋友可以專門找資料學(xué)習(xí),可以參考我的博文:
C 語言內(nèi)聯(lián)匯編介紹
__asm__(匯編語句模板: 輸出部分: 輸入部分: 破壞描述部分)根據(jù)_syscall0()的宏定義,我們把static inline _syscall0(int,fork)展開,得到:
static inline int fork(void) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (2)); if (__res >= 0) return (int) __res; errno = -__res; return -1; }
實(shí)際上展開結(jié)果就是上面一行。
可以手工展開,也可以用命令展開。用命令展開的方法是:
首先進(jìn)入到 Linux-0.11 源碼路徑下,比如~/oslab/linux-0.11,然后輸入命令:
gcc -E init/main.c -o main.i -I./include
如果你還沒有實(shí)驗(yàn)環(huán)境,那趕緊弄一個(gè)吧,方法是 Linux 0.11 實(shí)驗(yàn)環(huán)境搭建或者Linux 0.11 實(shí)驗(yàn)環(huán)境搭建與調(diào)試
以上的展開結(jié)果實(shí)在是太長了,分行寫如下:
static inline int fork(void) {long __res;__asm__ volatile ("int $0x80" : "=a" (__res) : "0" (2)); if (__res >= 0)return (int) __res; errno = -__res;return -1; }第6行:括號(hào)里的“2”是因?yàn)樵谖募nistd.h中有#define __NR_fork 2,表示系統(tǒng)中斷調(diào)用號(hào)。
gcc會(huì)把上述“函數(shù)”體中的語句直接插入到調(diào)用fork()語句的代碼處,因此執(zhí)行fork()不會(huì)引起函數(shù)調(diào)用。另外,宏名稱字符串syscall0中最后的0 表示無參數(shù),1表示帶1個(gè)參數(shù)。如果系統(tǒng)調(diào)用帶有1個(gè)參數(shù),那么就應(yīng)該使用宏_syscall1()。
2. setup.s讀取的參數(shù)
/** This is set up by the setup-routine at boot-time*/ #define EXT_MEM_K (*(unsigned short *)0x90002) #define DRIVE_INFO (*(struct drive_info *)0x90080) #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)以上三行,右側(cè)的地址其實(shí)是setup.s運(yùn)行時(shí),讀取了一些參數(shù),并保存到了相應(yīng)位置。忘了的同學(xué)可以參考我的博文 bootsect.s 分析—— Linux-0.11 學(xué)習(xí)筆記(一)
EXT_MEM_K (0x9002):系統(tǒng)從 1MB 開始的擴(kuò)展內(nèi)存大小,以KB為單位;
DRIVE_INFO (0x90080) :硬盤參數(shù)表,包括第1個(gè)和第2個(gè)硬盤,共32字節(jié);
ORIG_ROOT_DEV :根文件系統(tǒng)所在的設(shè)備號(hào)3.
3. 讀取CMOS實(shí)時(shí)時(shí)鐘信息
#define CMOS_READ(addr) ({ \ outb_p(0x80|addr,0x70); \ // 把 (0x80|addr) 寫入端口0x70 inb_p(0x71); \ // 讀端口0x71 })要想搞清楚上面的代碼,就先要弄清楚outb_p和inb_p。outb_p和inb_p都是宏,在文件\include\asm\io.h中定義。
3.1 outb_p(value,port)
#define outb_p(value,port) \__asm__ ("outb %%al,%%dx\n" \"\tjmp 1f\n" \"1:\tjmp 1f\n" \"1:"::"a" (value),"d" (port))功能:向端口 port 寫值 value
注意:第4行和第5行開頭的 “1:” 是標(biāo)號(hào)。
第2行:把 al 的值寫入端口 dx;
第3行:跳轉(zhuǎn)到1處,即下一句;這樣寫是為了延時(shí);
第4行:同第3行;
第5行:內(nèi)聯(lián)匯編的輸入部分,port作為端口號(hào),傳給 edx; value作為要寫入的值,傳給 eax;
所以, outb_p(value,port)表示把value寫入端口port.
3.2 inb_p(port)
#define inb_p(port) ({ \unsigned char _v; \__asm__ volatile ("inb %%dx,%%al\n" \"\tjmp 1f\n" \"1:\tjmp 1f\n" \"1:":"=a" (_v):"d" (port)); \_v; \})第3行:讀端口 dx 到 al;
第4~5行:跳轉(zhuǎn)到1處,即下一句;為了延時(shí);
第 6 行分為兩部分,輸入部分是"d" (port),表示把端口號(hào)port傳給edx;輸出部分是"=a" (_v),表示把 eax 的值傳給_v
第7行:_v 的值作為整個(gè)表達(dá)式的返回值。
所以, inb_p(port)表示讀取端口port的值。
對(duì)于第 7 行,要說明一下,這種用法是可以的。
比如
int main(void) {int x = 10;int y = 1;int c = ({x;y;});printf("c = %d\n",c); }第 5 行,這樣寫合法,結(jié)果輸出 1
可以參考我的博文:C語言的復(fù)合語句表達(dá)式
3.3 outb(value,port)和inb(port)
#define outb(value,port) \ __asm__ ("outb %%al,%%dx"::"a" (value),"d" (port))#define inb(port) ({ \ unsigned char _v; \ __asm__ volatile ("inb %%dx,%%al":"=a" (_v):"d" (port)); \ _v; \ })既然都分析到這里了,那就把這兩個(gè)宏也說了吧。這兩個(gè)宏和上面的差不多,只不過不帶延遲。
3.4 CMOS與RTC
PC 機(jī)的 CMOS 內(nèi)存是由電池供電的 64 或 128 字節(jié)內(nèi)存塊,通常是系統(tǒng)實(shí)時(shí)鐘芯片RTC (Real Time Chip) 的一部分。有些機(jī)器還有更大的內(nèi)存容量。該 64 字節(jié)的CMOS原先在IBM PC-XT機(jī)器上用于保存時(shí)鐘和日期信息,存放的格式是BCD碼。由于這些信息僅用去 14 字節(jié),因此剩余的字節(jié)就可用來存放一些系統(tǒng)配置數(shù)據(jù)。
CMOS的地址空間在基本地址空間之外。**要訪問它需要通過端口 0x70、 0x71 進(jìn)行。0x70 是地址端口,0x71 是數(shù)據(jù)端口。**為了讀取指定偏移位置的字節(jié),必須首先使用out指令向地址端口 0x70 發(fā)送指定字節(jié)的偏移位置值,然后使用in指令從數(shù)據(jù)端口 0x71 讀取指定的字節(jié)信息。同樣,對(duì)于寫操作也需要首先向地址端口 0x70 發(fā)送指定字節(jié)的偏移值,然后把數(shù)據(jù)寫到數(shù)據(jù)端口 0x71 中去。
outb_p(0x80|addr,0x70);把欲讀取的字節(jié)地址(addr)與0x80進(jìn)行或操作是沒有必要的。因?yàn)槟菚r(shí)的CMOS內(nèi)存容量還沒有超過128(=111_1111b)字節(jié),因此不需要把b7設(shè)為1。之所以會(huì)有這樣的操作是因?yàn)楫?dāng)時(shí)Linus手頭缺乏有關(guān)CMOS方面的資料,CMOS中時(shí)鐘和日期的偏移地址都是他逐步實(shí)驗(yàn)出來的,也許在他的實(shí)驗(yàn)中將偏移地址與0x80進(jìn)行或操作(并且還修改了其他地方)后正好取得了所有正確的結(jié)果,因此他的代碼中也就有了這步不必要的操作。不過從1.0版本之后,該操作就被去除了。
下表是 CMOS 內(nèi)存信息的一張簡表。
CMOS 64 字節(jié)信息簡表
3.5 time_init函數(shù)
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); // 時(shí)time.tm_mday = CMOS_READ(7); // 日time.tm_mon = CMOS_READ(8); // 月time.tm_year = CMOS_READ(9); // 年(since 1900)} 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);}結(jié)合上面的表格,6~11行非常好懂。
第12行:while (time.tm_sec != CMOS_READ(0));為什么有這個(gè)do-while循環(huán)呢?
CMOS的訪問速度很慢。為了減小時(shí)間誤差,在讀取了所有數(shù)值后,若此時(shí)CMOS中秒值發(fā)生了變化,那么就重新讀取所有值。這樣內(nèi)核就能把與CMOS時(shí)間誤差控制在1秒之內(nèi)。
注意,讀取的值是BCD(Binary Coded Decimal)碼格式。
BCD碼:是一種十進(jìn)制數(shù)字編碼的形式。在這種編碼下,每個(gè)十進(jìn)制數(shù)字用一串單獨(dú)的二進(jìn)制比特來存儲(chǔ)與表示。常見的有以4位表示1個(gè)十進(jìn)制數(shù)字,稱為壓縮的BCD碼(compressed or packed);或者以8位表示1個(gè)十進(jìn)制數(shù)字,稱為未壓縮的BCD碼(uncompressed or zoned)。
比如當(dāng)前時(shí)間是10:35:20,那么讀出的二進(jìn)制數(shù)是:
0001_0000b:0011_0101b:0010_0000b
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)// (val)&15 即 (val)&0xF, 得到個(gè)位數(shù);// (val)>>4)*10 把十位上的數(shù)字乘以10;這個(gè)宏的作用是把BCD格式的值轉(zhuǎn)換成二進(jìn)制(或者說十進(jìn)制,總之存到PC里都是二進(jìn)制)
time.tm_mon--;startup_time = kernel_mktime(&time);第2行:調(diào)用函數(shù)kernel_mktime(),計(jì)算從 1970 年 1 月 1 日 0 時(shí)起到現(xiàn)在經(jīng)過的秒數(shù),作為開機(jī)時(shí)間,保存到全局變量startup_time 中。更具體的分析可以參考我的博文 kernel_mktime() 詳解
4. main函數(shù)
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; //0x21Cdrive_info = DRIVE_INFO;memory_end = (1<<20) + (EXT_MEM_K<<10); //EXT_MEM_K = 0x3c00, memory_end = 0x100_0000memory_end &= 0xfffff000; //0x100_0000 = 16Mif (memory_end > 16*1024*1024)memory_end = 16*1024*1024;if (memory_end > 12*1024*1024) buffer_memory_end = 4*1024*1024; //buffer_memory_end = 4M else if (memory_end > 6*1024*1024)buffer_memory_end = 2*1024*1024;elsebuffer_memory_end = 1*1024*1024;main_memory_start = buffer_memory_end; //4M #ifdef RAMDISK_SIZE //=1025main_memory_start += rd_init(main_memory_start, RAMDISK_SIZE*1024); #endifmem_init(main_memory_start,memory_end);trap_init();blk_dev_init();chr_dev_init();tty_init();time_init();sched_init();buffer_init(buffer_memory_end);hd_init();floppy_init();sti();move_to_user_mode();if (!fork()) { /* we count on this going ok */init();} /** NOTE!! For any other task 'pause()' would mean we have to get a* signal to awaken, but task0 is the sole exception (see 'schedule()')* as task 0 gets activated at every idle moment (when no other tasks* can run). For task0 'pause()' just means we go check if some other* task can run, and if not we return here.*/for(;;) pause(); }4.1 根設(shè)備號(hào)
ROOT_DEV = ORIG_ROOT_DEV;ROOT_DEV 是什么? 在 fs/super.c 中,定義了 int ROOT_DEV = 0;
ORIG_ROOT_DEV 是什么?本文件內(nèi)有宏定義
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)
ROOT_DEV = ORIG_ROOT_DEV;這條語句執(zhí)行后(依據(jù)我的實(shí)驗(yàn)環(huán)境),ROOT_DEV = 0x21C
在bootsect.s中,有
mov %cs:root_dev+0, %axcmp $0, %axjne root_definedmov %cs:sectors+0, %bxmov $0x0208, %ax # /dev/ps0 - 1.2Mbcmp $15, %bxje root_definedmov $0x021c, %ax # /dev/PS0 - 1.44Mb, excute here when debugcmp $18, %bxje root_defined undef_root:jmp undef_root root_defined:mov %ax, %cs:root_dev+0....org 508 root_dev:.word ROOT_DEV !這里存放根文件系統(tǒng)所在設(shè)備號(hào)(init/main.c中會(huì)用)這段代碼的意思,可以參考我的博文 bootsect.s 解讀——Linux-0.11 剖析筆記(二)
“確認(rèn)根文件系統(tǒng)設(shè)備號(hào)” 這節(jié)
設(shè)備號(hào) = 主設(shè)備號(hào)*256 + 次設(shè)備號(hào)(即 dev_no = (major << 8) + minor )
在 Linux 中軟驅(qū)的主設(shè)備號(hào)是 2,次設(shè)備號(hào) = type*4 + nr,其中 nr 為 0-3 分別對(duì)應(yīng)軟驅(qū) A、B、C 或 D; type 是軟驅(qū)的類型(2 表示1.2 MB 或 7 表示 1.44 MB 等)。
0x21C = 2<<8 + (7*4+0),所以根設(shè)備是 1.44M 的 A 驅(qū)動(dòng)器。
4.2 復(fù)制硬盤參數(shù)表
第 9 行,drive_info = DRIVE_INFO;
本文件內(nèi)有代碼
#define DRIVE_INFO (*(struct drive_info *)0x90080) ... struct drive_info { char dummy[32]; } drive_info;用意是復(fù)制 0x90080 處的硬盤參數(shù)表
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來直接上傳(img-tt1JtmY9-1596350698833)(pics/image-20200801084436199.png)]
4.3 計(jì)算主內(nèi)存起始位置
本文件中有
#define EXT_MEM_K (*(unsigned short *)0x90002)EXT_MEM_K 表示系統(tǒng)從 1MB 開始的擴(kuò)展內(nèi)存值,單位是 KB,所以要左移 10 位
memory_end = (1<<20) + (EXT_MEM_K<<10); memory_end &= 0xfffff000; // 忽略不到 4KB(1頁)的內(nèi)存數(shù)if (memory_end > 16*1024*1024) //如果內(nèi)存超過16M,則按16M計(jì)memory_end = 16*1024*1024;if (memory_end > 12*1024*1024) //如果內(nèi)存超過12M,則設(shè)置緩沖區(qū)末端=4Mbuffer_memory_end = 4*1024*1024; else if (memory_end > 6*1024*1024)//如果內(nèi)存超過6M,則設(shè)置緩沖區(qū)末端=2Mbuffer_memory_end = 2*1024*1024;elsebuffer_memory_end = 1*1024*1024;//否則設(shè)置緩沖區(qū)末端=1Mmain_memory_start = buffer_memory_end; //主內(nèi)存起始位置=緩沖區(qū)末端注意,代碼注釋部分的值是我通過實(shí)驗(yàn)測試出來的,你的實(shí)驗(yàn)環(huán)境不一定是這個(gè)值。
第1行:計(jì)算出內(nèi)存大小
第2行:忽略不到4KB的內(nèi)存數(shù)
這里的緩沖區(qū)需要解釋一下。
為了提高系統(tǒng)訪問塊設(shè)備的速度,內(nèi)核在內(nèi)存中開辟了一塊高速緩沖區(qū),將其劃分為一個(gè)個(gè)與磁盤塊大小相等的緩沖塊來暫存與塊設(shè)備之間的交換數(shù)據(jù),以減少I/O操作的次數(shù),提高系統(tǒng)的性能。緩沖塊中保存著最近訪問磁盤的數(shù)據(jù),內(nèi)核在讀塊設(shè)備之前先搜索緩沖區(qū),如果數(shù)據(jù)在緩沖區(qū)中就不需要再從磁盤中讀,否則向塊設(shè)備發(fā)出讀的指令。當(dāng)內(nèi)核寫塊設(shè)備時(shí),先將數(shù)據(jù)寫入緩沖區(qū),什么時(shí)候?qū)?shù)據(jù)同步到塊設(shè)備視具體情況而定,這樣做是為了盡可能久地將數(shù)據(jù)停留在內(nèi)存以減少對(duì)塊設(shè)備的操作。
在我的環(huán)境中,通過單步調(diào)試,代碼執(zhí)行第6行,也就是說緩沖區(qū)末端(buffer_memory_end)在4M處,也就是主內(nèi)存的起始位置(main_memory_start)。
4.4 虛擬盤
#ifdef RAMDISKmain_memory_start += rd_init(main_memory_start, RAMDISK*1024); #endif當(dāng) linux/Makefile文件中設(shè)置的RAMDISK值不為零時(shí),表示系統(tǒng)會(huì)創(chuàng)建 RAM 虛擬盤設(shè)備。 在這種情況下,就會(huì)執(zhí)行第2行,即主內(nèi)存區(qū)的起始地址后移,也就是說主內(nèi)存區(qū)頭部還要?jiǎng)澣ヒ徊糠?#xff0c;供虛擬盤存放數(shù)據(jù)。
對(duì)于我的實(shí)驗(yàn)環(huán)境,Makefile 文件中有
RAMDISK = #-DRAMDISK=512
所以,虛擬盤的大小是0.5 * 1K * 1K = 0.5 M
如圖所示,內(nèi)核程序占據(jù)在物理內(nèi)存的開始部分,接下來是供硬盤或軟盤等塊設(shè)備使用的高速緩沖區(qū)部分(其中要扣除顯卡內(nèi)存和 ROM BIOS 所占用的內(nèi)存,它們的地址范圍是 640KB~1MB)。
關(guān)于高速緩沖區(qū):如前文所述,當(dāng)一個(gè)進(jìn)程需要讀取塊設(shè)備中的數(shù)據(jù)時(shí),系統(tǒng)會(huì)首先把數(shù)據(jù)讀到高速緩沖區(qū)中;當(dāng)有數(shù)據(jù)需要寫到塊設(shè)備上時(shí),系統(tǒng)也是先將數(shù)據(jù)放到高速緩沖區(qū)中,然后由塊設(shè)備驅(qū)動(dòng)程序?qū)懙较鄳?yīng)的設(shè)備上。
內(nèi)存的最后部分是供所有程序可以隨時(shí)申請(qǐng)和使用的主內(nèi)存區(qū)。內(nèi)核程序在使用主內(nèi)存區(qū)時(shí),也同樣先要向內(nèi)核內(nèi)存管理模塊提出申請(qǐng),在申請(qǐng)成功后方能使用。
對(duì)于含有 RAM 虛擬盤的系統(tǒng),主內(nèi)存區(qū)頭部還要?jiǎng)澣ヒ徊糠?#xff0c;供虛擬盤存放數(shù)據(jù)。
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); }第6行:MAJOR_NR 的值是1。因?yàn)樵谶@個(gè)函數(shù)所在文件里,有 #define MAJOR_NR 1
blk_dev是一個(gè)數(shù)組,其成員類型是struct blk_dev_struct
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 */ };struct blk_dev_struct的定義是
struct blk_dev_struct {void (*request_fn)(void);struct request * current_request; };可以看出,2個(gè)成員都是指針,request_fn指向函數(shù),current_request指向struct request.
回到函數(shù)rd_init:
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
DEVICE_REQUEST實(shí)際上是設(shè)備請(qǐng)求函數(shù)do_rd_request
因?yàn)?define DEVICE_REQUEST do_rd_request
void do_rd_request(void) {int len;char *addr;INIT_REQUEST;addr = rd_start + (CURRENT->sector << 9);len = CURRENT->nr_sectors << 9;if ((MINOR(CURRENT->dev) != 1) || (addr+len > rd_start+rd_length)) {end_request(0);goto repeat;}if (CURRENT-> cmd == WRITE) {(void) memcpy(addr,CURRENT->buffer,len);} else if (CURRENT->cmd == READ) {(void) memcpy(CURRENT->buffer, addr,len);} elsepanic("unknown ramdisk-command");end_request(1);goto repeat; }此函數(shù)的代碼,我們先不深入,以后用到再說。我們關(guān)注的是rd_init函數(shù)的以下幾行:
rd_start = (char *) mem_start;rd_length = length;cp = rd_start; // cp是 char * 類型for (i=0; i < length; i++)*cp++ = '\0'; //以上3行, 盤區(qū)清零return(length);rd_start和rd_length都是全局變量,定義在文件kernel\blk_drv\ramdisk.c中:
char *rd_start; //虛擬盤的起始地址 int rd_length = 0; //虛擬盤空間大小,以B為單位4.5 mem_init函數(shù)
該函數(shù)對(duì) 1MB 以上內(nèi)存區(qū)域以頁面為單位進(jìn)行管理前的初始化設(shè)置工作。
一個(gè)頁面長度為4KB字節(jié)。該函數(shù)把1MB以上所有物理內(nèi)存劃分成一個(gè)個(gè)頁面,并使用一個(gè)頁面映射字節(jié)數(shù)組mem_map[] 來管理這些頁面。對(duì)于具有 16MB 內(nèi)存容量的機(jī)器,該數(shù)組共有3840( (16M-1M)/4K=3840 )項(xiàng) ,即可管理3840個(gè)物理頁面。
每當(dāng)一個(gè)物理內(nèi)存頁面被占用時(shí)就把 mem_map[]中對(duì)應(yīng)的的字節(jié)值增1 ;若釋放一個(gè)物理頁面,就把對(duì)應(yīng)字節(jié)值減 1。 若字節(jié)值為0 , 則表示對(duì)應(yīng)頁面空閑; 若字節(jié)值 >=1,則表示對(duì)應(yīng)頁面被占用或被不同程序共享占用。
在該版本內(nèi)核中,最多能管理16MB的物理內(nèi)存,大于16MB的內(nèi)存將棄掉不用。對(duì)于具有16MB內(nèi)存的PC機(jī)系統(tǒng),在沒有設(shè)置虛擬盤 RAMDISK 的情況下start_mem通常是4MB,end_mem是 16MB。因此主內(nèi)存區(qū)范圍是4MB~16MB,共有3072(=12M/4K)個(gè)物理頁面可供分配。如果設(shè)置了 RAMDISK,那么start_mem會(huì)大于4MB.
void mem_init(long start_mem, long end_mem) {int i;HIGH_MEMORY = end_mem;// 參數(shù)start_mem是可用作頁面分配的主內(nèi)存區(qū)起始地址//(已去除RAMDISK所占內(nèi)存空間)。 // end_mem是實(shí)際物理內(nèi)存最大地址。//地址范圍start_mem到end_mem是主內(nèi)存區(qū)。 for (i=0 ; i<PAGING_PAGES ; i++) //PAGING_PAGES = 3840mem_map[i] = USED; // 表示不可用?i = MAP_NR(start_mem); // i=主內(nèi)存區(qū)起始位置處頁面號(hào)end_mem -= start_mem; // 首尾相減,算出主內(nèi)存區(qū)的大小end_mem >>= 12; // 主內(nèi)存區(qū)的總頁面數(shù)while (end_mem-->0)mem_map[i++]=0; // 以上2行, 主內(nèi)存區(qū)頁面對(duì)應(yīng)字節(jié)值清零 }第11~12行: 首先將 1MB 到 16MB 范圍內(nèi)所有內(nèi)存頁面設(shè)置為已占用狀態(tài),即各項(xiàng)字節(jié)值全部設(shè)置成 USED(100)
PAGING_PAGES 被定義為(PAGING_MEM0RY>>12),即(15*1024*1024)>>12=3840
#define LOW_MEM 0x100000 #define PAGING_MEMORY (15*1024*1024) // 從 1M 到 16M #define PAGING_PAGES (PAGING_MEMORY>>12) // 除以 4K #define MAP_NR(addr) (((addr)-LOW_MEM)>>12) #define USED 100第13行:MAP_NR(start_mem) 即是(start_mem-0x100000)>>12,計(jì)算出主內(nèi)存區(qū)起始位置處頁面號(hào)。
4.6 trap_init函數(shù)
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);......}以上代碼主要是安裝陷阱門。我們拿第5行作為例子,具體分析一下。
4.6.1 set_trap_gate(n,addr)
因?yàn)?/p>
#define set_trap_gate(n,addr) \ _set_gate(&idt[n],15,0,addr)
所以,set_trap_gate(n,addr)其實(shí)是_set_gate(&idt[n],15,0,addr),也就是下面7~15行的內(nèi)嵌匯編代碼。
#define set_trap_gate(n,addr) \_set_gate(&idt[n],15,0,addr)...#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))d: 表示 edx
a: 表示 eax
i: 允許一個(gè)立即整形操作數(shù),包括其值僅在匯編時(shí)確定的符號(hào)常量。
o: 允許一個(gè)內(nèi)存操作數(shù),但只有當(dāng)?shù)刂肥强善频摹<丛摰刂芳由弦粋€(gè)小的偏移量,結(jié)果是一個(gè)有效的內(nèi)存地址。
第 8-10 行,分別有 %0, %1,2%,這分別代表 12-14 行的表達(dá)式。
以上內(nèi)嵌匯編代碼沒有輸出部分,僅有輸入部分。
上圖是陷阱門的格式,上面是高4字節(jié)(代碼中用 edx 表示),下面是低4字節(jié)(代碼中用 eax 表示)。注意:過程入口點(diǎn)偏移值不是物理地址,而是線性地址。
第15行:
"d" ((char *) (addr))表示用 addr 加載 edx;此時(shí),偏移值的[31:16]就位。
addr 是異常處理函數(shù)入口點(diǎn)的地址。因?yàn)閮?nèi)核代碼段的線性基址是0,所以偏移值等于函數(shù)的線性地址,又因?yàn)閮?nèi)核在之前的分頁中采用了恒等映射機(jī)制——線性地址等于物理地址,所以偏移值等于函數(shù)的物理地址。
"a" (0x00080000) :表示用 0x0008_0000 加載 eax;此時(shí),段選擇符就位。
段選擇子(符)的值是0x08,為什么是這個(gè)值呢?因?yàn)樵谶M(jìn)入main函數(shù)之前,已經(jīng)設(shè)置好了GDT,0x08是代碼段的選擇子。忘了的話可以參考我的博文 head.s 分析 第三節(jié)。
第7行的"movw %%dx,%%ax\n\t"表示用 dx 加載 ax;此時(shí),偏移值的[15:0]就位,eax也就位。
第8行的"movw %0,%%dx\n\t",表示用(0x8000+(dpl<<13)+(type<<8))加載 dx,
這里的 8 表示 P=1; 此時(shí),edx 就位。
根據(jù)_set_gate(&idt[n],15,0,addr)的參數(shù)可知type=15(表示陷阱門), dpl=0。(0x8000+(dpl<<13)+(type<<8))拼出了陷阱門的第4~5字節(jié)(edx的低字)。
第9行"movl %%eax,%1\n\t"表示把 eax 的值賦給*((char *) (gate_addr)),就是賦給idt[n]的前4字節(jié)。
第10行"movl %%edx,%2" 表示把 edx 的值賦給*(4+(char *)(gate_addr)),就是賦給idt[n]的后4字節(jié)。這8字節(jié)拼起來就是完整的idt[n].
4.6.2 idt數(shù)組
idt是中斷描述符表(其實(shí)是數(shù)組),一共有 256 個(gè)表項(xiàng),一個(gè)表項(xiàng)占8字節(jié)。
%1對(duì)應(yīng)第13行的(*((char *) (gate_addr)))
gate_addr就是第2行的&idt[n],那么idt是什么呢?在文件include\linux\head.h中有:
typedef struct desc_struct {unsigned long a,b; } desc_table[256];extern desc_table idt,gdt;1~3行:為struct desc_struct [256]取了一個(gè)別名——desc_table,也就是說desc_table的類型是“struct desc_struct類型的數(shù)組”。
第6行,注意extern關(guān)鍵字,聲明(而不是定義)了 idt 和 gdt,它們的類型都是desc_table,即“struct desc_struct類型的數(shù)組”。所以,&idt[n]是數(shù)組idt第n個(gè)元素的地址。
可能有人要問, idt 和 gdt的定義在哪里呢?
它們是在匯編代碼boot/head.s中定義的。
在本文件末尾有:
另外本文件開頭有
.globl idt,gdt,pg_dir,tmp_floppy_area.globl xxx表示把符號(hào)xxx聲明為全局變量/標(biāo)號(hào),以供其他源文件訪問。
好了,我們總結(jié)一下 _set_gate(gate_addr,type,dpl,addr) 這個(gè)宏
#define _set_gate(gate_addr,type,dpl,addr) \ __asm__ ("movw %%dx,%%ax\n\t" \ //將偏移地址低字與選擇符組合成描述符低4字節(jié)(eax)"movw %0,%%dx\n\t" \ //將類型標(biāo)志與偏移地址高字組合成描述符高4字節(jié)(edx)"movl %%eax,%1\n\t" \ //分別設(shè)置門描述符的低4字節(jié)和高4字節(jié)"movl %%edx,%2" \ : \: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \"o" (*((char *) (gate_addr))), \"o" (*(4+(char *) (gate_addr))), \"d" ((char *) (addr)),"a" (0x00080000))_set_gate(gate_addr,type,dpl,addr)此宏用于設(shè)置門描述符,即填寫 IDT 中的某一項(xiàng)
根據(jù)參數(shù)中的中斷或異常處理過程地址 addr 、門描述符類型 type 和特權(quán)級(jí)信息 dpl ,設(shè)置位于地址 gate_addr 處的門描述符。(注意:下面的“偏移”是相對(duì)于內(nèi)核代碼或數(shù)據(jù)段來說的。)
gate_addr:描述符存儲(chǔ)地址;
type:描述符類型;
dpl:描述符特權(quán)級(jí);
addr:偏移地址。
%0:由dpl,type組合成的類型值;
%1:描述符低 4 字節(jié)的存儲(chǔ)地址;
%2:描述符高 4 字節(jié)的存儲(chǔ)地址;
%3:即 (char *) (addr),edx(程序偏移地址addr);
%4:即 0x00080000,eax(高字中含有段選擇符0x8) 。
4.6.3 set_system_gate(n,addr)
#define set_system_gate(n,addr) \_set_gate(&idt[n],15,3,addr)這個(gè)宏和set_trap_gate(n,addr)的區(qū)別僅有一點(diǎn):前者的dpl=3,后者的dpl=0;
分析到這里, trap_init函數(shù)的大意已經(jīng)明了。
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); // 設(shè)置協(xié)處理器中斷0x2d(=45)的陷阱門描述符outb_p(inb_p(0x21)&0xfb,0x21); // 允許8259A主芯片的IRQ2中斷請(qǐng)求outb(inb_p(0xA1)&0xdf,0xA1);set_trap_gate(39,¶llel_interrupt); //設(shè)置并行口1的中斷0x27(=39)陷阱門描述符 }5~21 行:設(shè)置IDT的描述符。其中斷點(diǎn)陷阱中斷int3、溢出中斷overflow、邊界出錯(cuò)中斷bounds可以由任何程序產(chǎn)生,所以 DPL = 3
22~23行:把int 17 ~ int 48的陷阱門先設(shè)置為reserved,以后各個(gè)硬件初始化時(shí)會(huì)重新設(shè)置自己的陷阱門。
注意:set_trap_gate的第二個(gè)參數(shù)是中斷處理函數(shù)的入口點(diǎn),它們的代碼在文件linux/kernel/asm.s或者linux/kernel/system_call.s中。
第25行:outb_p(inb_p(0x21)&0xfb,0x21);
0x21是 8259A 主片命令字OCW1的端口地址,用于對(duì)其中斷屏蔽寄存器 IMR 進(jìn)行讀/寫操作。
inb_p(0x21)&0xfb讀出 IMR 的值,然后與0xfb(=1111_1011b),即清零D2位,也就是允許主片的 IRQ2 中斷請(qǐng)求。
注意:Linux-0.11 系統(tǒng)把主片的 ICW2 設(shè)置為 0x20,表示主片中斷請(qǐng)求0~7級(jí)對(duì)應(yīng)的中斷號(hào)是 0x20~0x27;把從片的 ICW2 設(shè)置成 0x28,表示從片中斷請(qǐng)求8~15級(jí)對(duì)應(yīng)的中斷號(hào)是 0x28~0x2f。
第26行:outb(inb_p(0xA1)&0xdf,0xA1);
0xA1是 8259A 從片命令字OCW1的端口地址。原理同上,inb_p(0xA1)&0xdf讀出從片 IMR 的值,然后與0xdf(=1101_1111),即清零D5位,由上圖可知,允許從片 IRQ13 協(xié)處理器中斷。
關(guān)于8259A的編程,可以參考我的博文: 詳解8259A
囿于篇幅,對(duì)main()函數(shù)的分析先到這里。
—【未完待續(xù)】—
參考資料
《Linux內(nèi)核完全剖析》(趙炯,機(jī)械工業(yè)出版社,2006)
與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的main() 函数解析(一)——Linux-0.11 剖析笔记(六)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mac os touch命令_MacOS
- 下一篇: strlen函数_四种好用的PHP自定义