main函式解析(一)——Linux-0.11 學習筆記(五)
main()
函式解析(一)——Linux-0.11 學習筆記(五)
經過了前面的各種鋪墊,終於來到了main
函式。這篇部落格的任務是把init/main.c
講清楚。由於牽扯到很多的函式呼叫,要想一次就說明白是很難的,所以我們把目標定得低一點,把脈絡理清楚就行。
1. 巨集定義_syscall0
檔案開頭的標頭檔案包含等就不多說了。對於C語言比較熟悉的朋友,我想第一個攔路虎就是“GCC內嵌彙編”。
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
中定義,它以內嵌彙編的形式呼叫 Linux 的系統呼叫中斷 int 0x80
。
系統呼叫(通常稱為syscalls
)是 Linux核心與上層應用程式進行互動通訊的唯一介面。使用者程式通過直接或間接(通過庫函式)呼叫中斷int 0x80
(在eax暫存器中指定系統呼叫功能號),即可使用核心資源,包括系統硬體資源。
_syscall0()
其實是一個巨集,這個巨集定義在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行:彙編語句,表示系統呼叫,0x80號中斷;
第6行:輸出部分,把eax的值傳給變數__res
;
第7行:輸入部分,把__NR_name
的值賦給eax,即指明系統呼叫功能號;
第8~9行: 如果返回值>=0,則直接返回該值;
第10~11行: 否則置出錯號errno
(全域性變數),並返回-1
。
順便提一下,內嵌彙編語法如下。對此不熟悉的朋友可以專門找資料學習。
__asm__(彙編語句模板: 輸出部分: 輸入部分: 破壞描述部分)
根據_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; }
實際上展開結果就是上面一行。
可以手工展開,也可以用命令展開。用命令展開的方法是:
首先進入到 Linux-0.11 原始碼路徑下,比如~/oslab/linux-0.11
,然後輸入命令:
gcc -E init/main.c -o main.i -I./include
如果你還沒有實驗環境,那趕緊弄一個吧,方法是 Linux 0.11 實驗環境搭建或者Linux 0.11 實驗環境搭建與除錯
以上的展開結果實在是太長了,分行寫如下:
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行:括號裡的“2”是因為在檔案unistd.h
中有#define __NR_fork 2
gcc會把上述“函式”體中的語句直接插入到呼叫fork()
語句的程式碼處,因此執行fork()
不會引起函式呼叫。另外,巨集名稱字串syscall0
中最後的0
表示無引數,1表示帶1個引數。如果系統呼叫帶有1個引數,那麼就應該使用巨集_syscall1()
。
2. setup.s
讀取的引數
/*
* 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)
以上三行,右側的地址其實是setup.s
執行時,讀取了一些引數,並儲存到了相應位置。忘了的同學可以參考我的博文 bootsect.s 分析—— Linux-0.11 學習筆記(一)
EXT_MEM_K (0x9002)
:系統從 1MB 開始的擴充套件記憶體大小,以KB為單位;DRIVE_INFO (0x90080)
:硬碟參數列,包括第1個和第2個硬碟,共32位元組;ORIG_ROOT_DEV
:根檔案系統所在的裝置號3.
3. 讀取CMOS實時時鐘資訊
#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))
注意:第4行和第5行的“1”是標號。
第2行:把al的值寫入埠dx;
第3行:跳轉到1處,即下一句;這樣寫是為了延時;
第4行:同第3行;
第5行:port
作為埠號,傳給edx; 把eax的值傳給value
;
所以, 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行:跳轉到1處,即下一句;為了延時;
第6行:port
作為埠號,傳給edx; 把eax的值傳給_v
;
第7行:_v
的值作為整個表示式的返回值。
所以, inb_p(port)
表示讀取埠port
的值。
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; \
})
既然都分析到這裡了,那就把這兩個巨集也說了吧。這兩個巨集和上面的差不多,只不過不帶延遲。
3.4 CMOS與RTC
PC 機的 CMOS 記憶體是由電池供電的 64 或 128 位元組記憶體塊,通常是系統實時鐘晶片RTC (Real Time Chip) 的一部分。有些機器還有更大的記憶體容量。該 64 位元組的CMOS原先在IBM PC-XT機器上用於儲存時鐘和日期資訊,存放的格式是BCD碼。由於這些資訊僅用去 14 位元組,因此剩餘的位元組就可用來存放一些系統配置資料。
CMOS的地址空間在基本地址空間之外,因此其中不包括可執行程式碼。要訪問它需要通過埠 0x70、 0x71 進行。0x70 是地址埠,0x71 是資料埠。為了讀取指定偏移位置的位元組,必須首先使用out
指令向地址埠 0x70 傳送指定位元組的偏移位置值,然後使用in
指令從資料埠 0x71 讀取指定的位元組資訊。同樣,對於寫操作也需要首先向地址埠 0x70 傳送指定位元組的偏移值,然後把資料寫到資料埠 0x71 中去。
outb_p(0x80|addr,0x70);
把欲讀取的位元組地址(addr)與0x80進行或操作是沒有必要的。因為那時的CMOS記憶體容量還沒有超過128(=111_1111b)位元組,因此不需要把b7設為1。之所以會有這樣的操作是因為當時Linus手頭缺乏有關CMOS方面的資料,CMOS中時鐘和日期的偏移地址都是他逐步實驗出來的,也許在他的實驗中將偏移地址與0x80進行或操作(並且還修改了其他地方)後正好取得了所有正確的結果,因此他的程式碼中也就有了這步不必要的操作。不過從1.0版本之後,該操作就被去除了。
下表是 CMOS 記憶體資訊的一張簡表。
CMOS 64 位元組資訊簡表
3.5 time_init函式
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); // 年(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);
}
結合上面的表格,6~11行非常好懂。
第12行:while (time.tm_sec != CMOS_READ(0));
為什麼有這個do-while
迴圈呢?
CMOS的訪問速度很慢。為了減小時間誤差,在讀取了所有數值後,若此時CMOS中秒值發生了變化,那麼就重新讀取所有值。這樣核心就能把與CMOS時間誤差控制在1秒之內。
注意,讀取的值是BCD(Binary Coded Decimal)碼格式。
BCD碼:是一種十進位制數字編碼的形式。在這種編碼下,每個十進位制數字用一串單獨的二進位制位元來儲存與表示。常見的有以4位表示1個十進位制數字,稱為壓縮的BCD碼(compressed or packed);或者以8位表示1個十進位制數字,稱為未壓縮的BCD碼(uncompressed or zoned)。
比如當前時間是10:35:20,那麼讀出的二進位制數是:
0001_0000b:0011_0101b:0010_0000b
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
// (val)&15 即 (val)&0xF, 得到個位數;
// (val)>>4)*10 把十位上的數字乘以10;
這個巨集的作用是把BCD格式的值轉換成二進位制(或者說十進位制,總之存到PC裡都是二進位制)
time.tm_mon--;
startup_time = kernel_mktime(&time);
第2行:呼叫函式kernel_mktime()
,計算從 1970 年 1 月 1 日 0 時起到現在經過的秒數,作為開機時間,儲存到全域性變數startup_time
中。更具體的分析可以參考我的博文 kernel_mktime() 詳解
4. 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; //0x21C
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10); //EXT_MEM_K = 0x3c00, memory_end = 0x100_0000
memory_end &= 0xfffff000; //0x100_0000 = 16M
if (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;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end; //4M
#ifdef RAMDISK_SIZE //=1025
main_memory_start += rd_init(main_memory_start, RAMDISK_SIZE*1024);
#endif
mem_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 根裝置號
ROOT_DEV = ORIG_ROOT_DEV;
在 fs/super.c
中,定義了 int ROOT_DEV = 0;
本檔案內有巨集定義
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)
ROOT_DEV = ORIG_ROOT_DEV;
這條語句執行後(依據我的實驗環境),ROOT_DEV = 0x21C
在bootsect.s
中,有
mov %cs:root_dev+0, %ax
cmp $0, %ax
jne root_defined
mov %cs:sectors+0, %bx
mov $0x0208, %ax # /dev/ps0 - 1.2Mb
cmp $15, %bx
je root_defined
mov $0x021c, %ax # /dev/PS0 - 1.44Mb, excute here when debug
cmp $18, %bx
je root_defined
undef_root:
jmp undef_root
root_defined:
mov %ax, %cs:root_dev+0
...
.org 508
root_dev:
.word ROOT_DEV !這裡存放根檔案系統所在裝置號(init/main.c中會用)
裝置號 = 主裝置號*256 + 次裝置號(也即 dev_no = (major << 8) + minor )
在 Linux 中軟碟機的主裝置號是 2,次裝置號 = type*4 + nr,其中 nr 為 0-3 分別對應軟碟機 A、B、C 或 D; type 是軟碟機的型別(2 表示1.2 MB 或 7 表示 1.44 MB 等)。
0x21C = 2<<8 + (7*4+0),所以根裝置是 1.44M 的 A 驅動器。
4.2 計算主記憶體起始位置
memory_end = (1<<20) + (EXT_MEM_K<<10); //EXT_MEM_K = 0x3c00, memory_end = 0x100_0000
memory_end &= 0xfffff000; //0x100_0000 = 16M
if (memory_end > 16*1024*1024) //如果記憶體超過16M,則按16M計
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024) //如果記憶體超過12M,則設定緩衝區末端=4M
buffer_memory_end = 4*1024*1024; //buffer_memory_end = 4M
else if (memory_end > 6*1024*1024)//如果記憶體超過6M,則設定緩衝區末端=2M
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;//否則設定緩衝區末端=1M
main_memory_start = buffer_memory_end; //主記憶體起始位置=緩衝區末端
注意,程式碼註釋部分的值是我通過實驗測試出來的,你的實驗環境不一定是這個值。
第1行:計算出記憶體大小
第2行:忽略不到4KB的記憶體數
在我的環境中,通過單步除錯,程式碼執行第6行,也就是說緩衝區末端(buffer_memory_end
)在4M處,也就是主記憶體的起始位置(main_memory_start
)。
4.3 虛擬盤
#ifdef RAMDISK_SIZE // 如果定義了虛擬盤
main_memory_start += rd_init(main_memory_start, RAMDISK_SIZE*1024);
#endif
當 linux/Makefile
檔案中設定的RAMDISK
值不為零時,表示系統會建立 RAM 虛擬盤裝置。 在這種情況下,就會執行第2行,即主記憶體區的起始地址後移,也就是說主記憶體區頭部還要劃去一部分,供虛擬盤存放資料。
根據單步除錯的結果,main_memory_start = 4194304(4M)
,RAMDISK_SIZE = 1025
如圖所示,核心程式佔據在實體記憶體的開始部分,接下來是供硬碟或軟盤等塊裝置使用的高速緩衝區部分(其中要扣除顯示卡記憶體和 ROM BIOS 所佔用的記憶體,它們的地址範圍是640KB~1MB)。
關於高速緩衝區:當一個程式需要讀取塊裝置中的資料時,系統會首先把資料讀到高速緩衝區中;當有資料需要寫到塊裝置上時,系統也是先將資料放到高速緩衝區中,然後由塊裝置驅動程式寫到相應的裝置上。
記憶體的最後部分是供所有程式可以隨時申請和使用的主記憶體區。核心程式在使用主記憶體區時,也同樣先要向核心記憶體管理模組提出申請,在申請成功後方能使用。
對於含有 RAM 虛擬盤的系統,主記憶體區頭部還要劃去一部分,供虛擬盤存放資料。
long rd_init(long mem_start, int length)
{
int i;
char *cp;
blk_dev[MAJOR_NR/*=1*/].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。
blk_dev
是一個陣列,其成員型別是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個成員都是指標,request_fn
指向函式,current_request
指向struct request
.
回到函式rd_init
:
blk_dev[MAJOR_NR/*=1*/].request_fn = DEVICE_REQUEST;
DEVICE_REQUEST
實際上是裝置請求函式do_rd_request
因為#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);
} else
panic("unknown ramdisk-command");
end_request(1);
goto repeat;
}
此函式的程式碼,我們先不深入,以後用到再說。我們關注的是rd_init
函式的以下幾行:
rd_start = (char *) mem_start;
rd_length = length;
cp = rd_start; // cp是 char * 型別
for (i=0; i < length; i++)
*cp++ = '\0'; //以上3行, 盤區清零
return(length);
rd_start
和rd_length
都是全域性變數,定義在檔案kernel\blk_drv\ramdisk.c
中:
char *rd_start; //虛擬盤的起始地址
int rd_length = 0; //虛擬盤空間大小,以B為單位
4.4 mem_init
函式
該函式對1MB以上記憶體區域以頁面為單位進行管理前的初始化設定工作。
一個頁面長度為4KB位元組。該函式把1MB以上所有實體記憶體劃分成一個個頁面,並使用一個頁面對映位元組陣列mem_map[]
來管理這些頁面。對於具有 16MB 記憶體容量的機器,該陣列共有3840( (16M-1M)/4K=3840 )
項 ,即可管理3840個物理頁面。
每當一個實體記憶體頁面被佔用時就把 mem_map[]
中對應的的位元組值增1 ;若釋放一個物理頁面,就把對應位元組值減 1。 若位元組值為0 , 則表示對應頁面空閒; 若位元組值 >=1,則表示對應頁面被佔用或被不同程式共享佔用。
在該版本核心中,最多能管理16MB的實體記憶體,大於16MB的記憶體將棄掉不用。對於具有16MB記憶體的PC機系統,在沒有設定虛擬盤 RAMDISK 的情況下start_mem
通常是4MB,end_mem
是 16MB。因此主記憶體區範圍是4MB~16MB,共有3072個物理頁面可供分配。如果設定了 RAMDISK,那麼start_mem
會大於4MB,比如我的實驗環境是5243904(=5121K)
即RAMDISK佔用了1025K(=5121K-4096K)
.
void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem;
// 引數start_mem是可用作頁面分配的主記憶體區起始地址
//(已去除RAMDISK所佔記憶體空間)。
// end_mem是實際實體記憶體最大地址。
//地址範圍start_mem到end_mem是主記憶體區。
for (i=0 ; i<PAGING_PAGES ; i++) //PAGING_PAGES = 3840
mem_map[i] = USED;
i = MAP_NR(start_mem); // i=主記憶體區起始位置處頁面號
end_mem -= start_mem; // 首尾相減,算出主記憶體區的大小
end_mem >>= 12; // 主記憶體區的總頁面數
while (end_mem-->0)
mem_map[i++]=0; // 以上2行, 主記憶體區頁面對應位元組值清零
}
第11~12行: 首先將 1MB 到 16MB 範圍內所有記憶體頁面設定為已佔用狀態,即各項位元組值全部設定成 USED(100)
PAGING_PAGES
被定義為(PAGING_MEM0RY>>12)
,即(15*1024*1024)>>12=3840
#define LOW_MEM 0x100000
#define PAGING_MEMORY (15*1024*1024)
#define PAGING_PAGES (PAGING_MEMORY>>12)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
#define USED 100
第13行:MAP_NR(start_mem)
即是(start_mem-0x100000)>>12
,計算出主記憶體區起始位置處頁面號。
4.5 trap_init
函式
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.5.1 set_trap_gate(n,addr)
set_trap_gate(n,addr)
其實是_set_gate(&idt[n],15,0,addr)
,也就是下面7~15行的內嵌彙編程式碼。
#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
: 允許一個立即整形運算元,包括其值僅在彙編時確定的符號常量。
o
: 允許一個記憶體運算元,但只有當地址是可偏移的。即該地址加上一個小的偏移量,結果是一個有效的記憶體地址。
以上內嵌彙編程式碼沒有輸出部分,僅有輸入部分。
上圖是陷阱門的格式,上面是高4位元組(程式碼中用 edx 表示),下面是低4位元組(程式碼中用 eax 表示)。注意:過程入口點偏移值不是實體地址,而是線性地址。
第15行:
"d" ((char *) (addr))
表示用 addr 載入edx;此時,偏移值的[31:16]就位。
addr 是異常處理函式入口點的地址。因為核心程式碼段的線性基址是0,所以偏移值等於函式的線性地址,又因為核心在之前的分頁中採用了恆等對映機制——線性地址等於實體地址,所以偏移值等於函式的實體地址。
"a" (0x00080000)
:表示用 0x0008_0000 載入 eax;此時,段選擇符就位。
段選擇子(符)的值是0x08,為什麼是這個值呢?因為在進入main函式之前,已經設定好了GDT,0x08是程式碼段的選擇子。忘了的話可以參考我的博文head.s——第三節。
第7行的"movw %%dx,%%ax\n\t"
表示用 dx 載入 ax;此時,偏移值的[15:0]就位,eax也就位。
第8行的"movw %0,%%dx\n\t"
,表示用(0x8000+(dpl<<13)+(type<<8))
載入 dx,
這裡的 8 表示 P=1; 此時,edx 就位。
根據_set_gate(&idt[n],15,0,addr)
的引數可知type=15(表示陷阱門), dpl=0
。(0x8000+(dpl<<13)+(type<<8))
拼出了陷阱門的第4~5位元組(edx的低字)。
第9行"movl %%eax,%1\n\t"
表示把 eax 的值賦給*((char *) (gate_addr))
,就是賦給idt[n]
的前4位元組。
第10行"movl %%edx,%2"
表示把edx的值賦給*(4+(char *)(gate_addr))
,就是賦給idt[n]
的後4位元組。這8位元組拼起來就是完整的idt[n]
.
4.5.2 idt
陣列
idt
是中斷描述符表(其實是陣列),一共有 256 個表項,一個表項佔8位元組。
%1
對應第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]
取了一個別名——desc_table
,也就是說desc_table
的型別是“struct desc_struct
型別的陣列”。
第6行,注意extern
關鍵字,宣告(而不是定義)了 idt
和 gdt
,它們的型別都是desc_table
,即“struct desc_struct
型別的陣列”。所以,&idt[n]
是陣列idt
第n
個元素的地址。
可能有人要問, idt
和 gdt
的定義在哪裡呢?
它們是在彙編程式碼boot/head.s
中定義的。
在本檔案末尾有:
idt: .fill 256,8,0 # idt is uninitialized
gdt:
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x00c09a0000000fff /* 16Mb */
.quad 0x00c0920000000fff /* 16Mb */
.quad 0x0000000000000000 /* TEMPORARY - don't use */
.fill 252,8,0 /* space for LDT's and TSS's etc */
另外本檔案開頭有
.globl idt,gdt,pg_dir,tmp_floppy_area
.globl xxx
表示把符號xxx
宣告為全域性變數/標號,以供其他原始檔訪問。
4.5.3 _set_gate(gate_addr,type,dpl,addr)
總結
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \ //將偏移地址低字與選擇符組合成描述符低4位元組(eax)
"movw %0,%%dx\n\t" \ //將型別標誌與偏移地址高字組合成描述符高4位元組(edx)
"movl %%eax,%1\n\t" \ //分別設定門描述符的低4位元組和高4位元組
"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)
此巨集用於設定門描述符。
根據引數中的中斷或異常處理過程地址 addr
、門描述符型別 type
和特權級資訊 dpl
,設定位於地址 gate_addr
處的門描述符。(注意:下面的“偏移”是相對於核心程式碼或資料段來說的。)
gate_addr
:描述符儲存地址;
type
:描述符型別;
dpl
:描述符特權級;
addr
:偏移地址。
%0
:由dpl,type組合成的型別值;
%1
:描述符低 4 位元組的儲存地址;
%2
:描述符高 4 位元組的儲存地址;
%3
:edx(程式偏移地址addr);
%4
: eax(高字中含有段選擇符0x8) 。
4.5.4 set_system_gate(n,addr)
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
這個巨集和set_trap_gate(n,addr)
的區別僅有一點:前者的dpl=3,後者的dpl=0;
分析到這裡, trap_init
函式的大意已經明瞭。
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); // 設定協處理器中斷0x2d(=45)的陷阱門描述符
outb_p(inb_p(0x21)&0xfb,0x21); // 允許8259A主晶片的IRQ2中斷請求
outb(inb_p(0xA1)&0xdf,0xA1);
set_trap_gate(39,¶llel_interrupt); //設定並行口1的中斷0x27(=39)陷阱門描述符
}
5~2行:設定IDT的描述符。其中斷點陷阱中斷int3
、溢位中斷overflow
、邊界出錯中斷bounds
可以由任何程式產生。
22~23行:把int 17
~ int 48
的陷阱門先設定為reserved
,以後各個硬體初始化時會重新設定自己的陷阱門。
注意:set_trap_gate
的第二個引數是中斷處理函式的入口點,它們的程式碼在檔案linux/kernel/asm.s
或者linux/kernel/system_call.s
中。
第25行:outb_p(inb_p(0x21)&0xfb,0x21);
0x21是 8259A 主片命令字OCW1的埠地址,用於對其中斷遮蔽暫存器 IMR 進行讀/寫操作。
inb_p(0x21)&0xfb
讀出 IMR 的值,然後與0xfb(=1111_1011b),即清零D2位,也就是允許主片的 IRQ2 中斷請求。
注意:Linux-0.11 系統把主片的 ICW2 設定為 0x20,表示主片中斷請求0~7級對應的中斷號是 0x20~0x27
;把從片的 ICW2 設定成 0x28,表示從片中斷請求8~15級對應的中斷號是 0x28~0x2f
。
第26行:outb(inb_p(0xA1)&0xdf,0xA1);
0xA1是 8259A 從片命令字OCW1的埠地址。原理同上,inb_p(0xA1)&0xdf
讀出從片 IMR 的值,然後與0xdf(=1101_1111),即清零D5位,由上圖可知,允許從片 IRQ13 協處理器中斷。
關於8259A的程式設計,可以參考我的博文: 詳解8259A
囿於篇幅,對main()函式的分析先到這裡,剩下的內容下次再說。謝謝您的閱讀!
—【未完待續】—
參考資料
《Linux核心完全剖析》(趙炯,機械工業出版社,2006)
相關文章
- main 函式解析(二)—— Linux-0.11 學習筆記(六)AI函式Linux筆記
- bootsect.s 分析—— Linux-0.11 學習筆記(一)bootLinux筆記
- js純函式學習筆記(一)JS函式筆記
- async函式學習筆記。函式筆記
- 生成函式 學習筆記函式筆記
- setup.s 分析—— Linux-0.11 學習筆記(二)Linux筆記
- Python中的main函式解析PythonAI函式
- Golang學習筆記-1.6 函式Golang筆記函式
- JavaScript學習筆記 - 原生函式JavaScript筆記函式
- MYSQL學習筆記14: 函式MySql筆記函式
- python學習筆記(六)——函式Python筆記函式
- TS學習筆記(四):函式筆記函式
- Oracle學習筆記(6)——函式Oracle筆記函式
- PHP 手冊 (類與物件) 學習筆記五:建構函式和解構函式PHP物件筆記函式
- 函式學習五函式
- 深度學習——loss函式的學習筆記深度學習函式筆記
- kernel_mktime() 詳解 —— Linux-0.11 學習筆記(四)Linux筆記
- PHP 第五週函式學習記錄PHP函式
- MYSQL學習筆記7: 聚合函式MySql筆記函式
- C++學習筆記(二)——函式C++筆記函式
- OpenCV學習筆記(4)——mixChannels函式OpenCV筆記函式
- OpenCV學習筆記(5)——normalize函式OpenCV筆記ORM函式
- Flutter學習筆記(4)--Dart函式Flutter筆記Dart函式
- c語言學習筆記===函式C語言筆記函式
- javascript學習筆記--函式的返回值可以是一個函式JavaScript筆記函式
- (C++通訊架構學習筆記):日誌列印、優化main函式呼叫順序C++架構筆記優化AI函式
- MYSQL學習筆記15: 數值函式MySql筆記函式
- PHP 第八週函式學習筆記PHP函式筆記
- 學習筆記:javascript中的Generator函式筆記JavaScript函式
- JavaScript學習筆記(七)—— 再說函式JavaScript筆記函式
- 工作學習筆記(三)to_char函式筆記函式
- pandas之常用基本函式學習筆記函式筆記
- 莫比烏斯函式 - 學習筆記函式筆記
- Manim 學習筆記(一)--常用的幾個函式和操作筆記函式
- cmake學習筆記(五)筆記
- JVM 學習筆記(五)JVM筆記
- ES6學習筆記(三)【函式,物件】筆記函式物件
- Solidity語言學習筆記————28、純函式Solid筆記函式