1. 程式人生 > >ucore-lab0:作業系統實驗準備

ucore-lab0:作業系統實驗準備

1.1. 實驗目的:

瞭解作業系統開發實驗環境
熟悉命令列方式的編譯、除錯工程
掌握基於硬體模擬器的除錯技術
熟悉C語言程式設計和指標的概念
瞭解X86組合語言

1.2.準備知識

1.2.1. 瞭解OS實驗

1.啟動作業系統的bootloader,用於瞭解作業系統啟動前的狀態和要做的準備工作,瞭解執行作業系統的硬體支援,作業系統如何載入到記憶體中,理解兩類中斷–“外設中斷”,“陷阱中斷”等;
2.實體記憶體管理子系統,用於理解x86分段/分頁模式,瞭解作業系統如何管理實體記憶體;
3.虛擬記憶體管理子系統,通過頁表機制和換入換出(swap)機制,以及中斷-“故障中斷”、缺頁故障處理等,實現基於頁的記憶體替換演算法;


4核心執行緒子系統,用於瞭解如何建立相對與使用者程序更加簡單的核心態執行緒,如果對核心執行緒進行動態管理等;
5.使用者程序管理子系統,用於瞭解使用者態程序建立、執行、切換和結束的動態管理過程,瞭解在使用者態通過系統呼叫得到核心態的核心服務的過程
6 處理器排程子系統,用於理解作業系統的排程過程和排程演算法;
7 同步互斥與程序間通訊子系統,瞭解程序間如何進行資訊交換和共享,並瞭解同步互斥的具體實現以及對系統性能的影響,研究死鎖產生的原因,以及如何避免死鎖;
8,檔案系統,瞭解檔案系統的具體實現,與程序管理等的關係,瞭解快取對作業系統IO訪問的效能改進,瞭解虛擬檔案系統(VFS)、buffer cache和disk driver之間的關係。

圖1 ucore系統結構圖

1.2.2.設定實驗環境

1.2.2.1. 安裝使用Linux實驗

常用指令

(6) 複製檔案: cp
[email protected]:~$ cp file1.txt file1_copy.txt
[email protected]:~$ cat file1_copy.txt
Roses are red.
Violets are blue,
and you have the bird-flue!
(7) 移動檔案:mv
[email protected]:~$ ls
file1.txt
file2.txt
[email protected]
:~$ mv file1.txt new_file.txt [email protected]:~$ ls file2.txt new_file.txt

注意:在命令操作時系統基本上不會給你什麼提示,當然,絕大多數的命令可以通過加上一個引數-v來要求系統給出執行命令的反饋資訊;

[email protected]:~$ mv -v file1.txt new_file.txt
`file1.txt' -> `new_file.txt'
(8) 建立一個空文字檔案:touch
[email protected]:~$ ls
file1.txt
[email protected]:~$ touch tempfile.txt
[email protected]:~$ ls
file1.txt
tempfile.txt
(9) 建立一個目錄:mkdir
[email protected]:~$ ls
file1.txt
tempfile.txt
[email protected]:~$ mkdir test_dir
[email protected]:~$ ls
file1.txt
tempfile.txt
test_dir
(10) 刪除檔案/目錄:rm
[email protected]:~$ ls -p
file1.txt
tempfile.txt
test_dir/
[email protected]:~$ rm -i tempfile.txt
rm: remove regular empty file `test.txt'? y
[email protected]:~$ ls -p
file1.txt
test_dir/
[email protected]:~$ rm test_dir
rm: cannot remove `test_dir': Is a directory
[email protected]:~$ rm -R test_dir
[email protected]:~$ ls -p
file1.txt

在上面的操作:首先我們通過ls命令查詢可知當前目下有兩個檔案和一個資料夾;

[1] 你可以用引數 -p來讓系統顯示某一項的型別,比如是檔案/資料夾/快捷連結等等;
[2] 接下來我們用rm -i嘗試刪除檔案,-i引數是讓系統在執行刪除操作前輸出一條確認提示;i(interactive)也就是互動性的意思;
[3] 當我們嘗試用上面的命令去刪除一個資料夾時會得到錯誤的提示,因為刪除資料夾必須使用-R(recursive,迴圈)引數
特別提示:在使用命令操作時,系統假設你很明確自己在做什麼,它不會給你太多的提示,比如你執行rm -Rf /,它將會刪除你硬碟上所有的東西,並且不會給你任何提示,所以,儘量在使用命令時加上-i的引數,以讓系統在執行前進行一次確認,防止你幹一些蠢事。如 果你覺得每次都要輸入-i太麻煩,你可以執行以下的命令,讓-i成為預設引數:

alias rm='rm -i'
(11) 查詢當前程序:ps
[email protected]:~$ ps
PID TTY          TIME CMD
21071 pts/1    00:00:00 bash
22378 pts/1    00:00:00 ps

這條命令會例出你所啟動的所有程序;

ps -a #可以例出系統當前執行的所有程序,包括由其他使用者啟動的程序;
ps auxww #是一條相當人性化的命令,它會例出除一些很特殊程序以外的所有程序,並會以一個高可讀的形式顯示結果,每一個程序都會有較為詳細的解釋;
基本命令的介紹就到此為止,你可以訪問網路得到更加詳細的Linux命令介紹。

控制流程

(1) 輸入/輸出

input用來讀取你通過鍵盤(或其他標準輸入裝置)輸入的資訊,output用於在螢幕(或其他標準輸出裝置)上輸出你指定的輸出內容.另外還有一些標準的出錯提示也是通過這個命令來實現的。通常在遇到操作錯誤時,系統會自動呼叫這個命令來輸出標準錯誤提示;

我們能重定向命令中產生的輸入和輸出流的位置。

(2) 重定向

如果你想把命令產生的輸出流指向一個檔案而不是(預設的)終端,你可以使用如下的語句:

[email protected]:~$ ls >file4.txt
[email protected]:~$ cat file4.txt
file1.txt  file2.txt file3.txt

以上例子將建立檔案file4.txt如果file4.txt不存在的話。注意:如果file4.txt已經存在,那麼上面的命令將覆蓋檔案的內容。如果你想將內容新增到已存在的檔案內容的最後,那你可以用下面這個語句:

command >> filename 

示例:

[email protected]:~$ ls >> file4.txt
[email protected]:~$ cat file4.txt
file1.txt  file2.txt file3.txt
file1.txt  file2.txt file3.txt file4.txt

在這個例子中,你會發現原有的檔案中添加了新的內容。接下來我們會見到另一種重定向方式:我們將把一個檔案的內容作為將要執行的命令的輸入。以下是這個語句:

command < filename 

示例:

[email protected]:~$ cat > file5.txt
a3.txt
a2.txt
file2.txt
file1.txt
<Ctrl-D>  # 這表示敲入Ctrl+D鍵
[email protected]:~$ sort < file5.txt
a2.txt
a3.txt
file1.txt
file2.txt
(3) 管道

Linux的強大之處在於它能把幾個簡單的命令聯合成為複雜的功能,通過鍵盤上的管道符號’|’ 完成。現在,我們來排序上面的"grep"命令:

grep -i command < myfile | sort > result.text 

搜尋 myfile 中的命令,將輸出分類並寫入分類檔案到 result.text 。 有時候用ls列出很多命令的時候很不方便 這時“|”就充分利用到了 ls -l | less 慢慢看吧.

(4) 後臺程序

CLI 不是系統的序列介面。您可以在執行其他命令時給出系統命令。要啟動一個程序到後臺,追加一個“&”到命令後面。

sleep 60 &
ls

睡眠命令在後臺執行,您依然可以與計算機互動。除了不同步啟動命令以外,最好把 ‘&’ 理解成 ‘;’。

如果您有一個命令將佔用很多時間,您想把它放入後臺執行,也很簡單。只要在命令執行時按下ctrl-z,它就會停止。然後鍵入 bg使其轉入後臺。fg 命令可使其轉回前臺。

sleep 60
<ctrl-z> # 這表示敲入Ctrl+Z鍵
bg
fg

最後,您可以使用 ctrl-c 來殺死一個前臺程序。

環境變數

特殊變數。PATH, PS1, …

(1) 不顯示中文

可通過執行如下命令避免顯示亂碼中文。在一個shell中,執行:

export LANG=””

這樣在這個shell中,output資訊預設時英文。

獲得軟體包

(1) 命令列獲取軟體包

Ubuntu 下可以使用 apt-get 命令,apt-get 是一條 Linux 命令列命令,適用於 deb 包管理式的作業系統,主要用於自動從網際網路軟體庫中搜索、安裝、升級以及解除安裝軟體或者作業系統。一般需要 root 執行許可權,所以一般跟隨 sudo 命令,如:

sudo apt-get install gcc [ENTER]

常見的以及常用的 apt 命令有:

apt-get install <package>
    下載 <package> 以及所依賴的軟體包,同時進行軟體包的安裝或者升級。
apt-get remove <package>
    移除 <package> 以及所有依賴的軟體包。
apt-cache search <pattern>
    搜尋滿足 <pattern> 的軟體包。
apt-cache show/showpkg <package>
    顯示軟體包 <package> 的完整描述。
(2) 圖形介面軟體包獲取

Ubuntu 下面管理軟體包得圖形介面程式,相當於命令列中得 apt 命令。進入方法可以是

選單欄 > 系統管理 > 新立得軟體包管理器
(System > Administration > Synaptic Package Manager)
使用更新管理器可以通過標記選擇適當的軟體包進行更新操作。
(3) 配置升級源

Ubuntu的軟體包獲取依賴升級源,可以通過修改 “/etc/apt/sources.list” 檔案來修改升級源(需要 root 許可權);或者修改新立得軟體包管理器中 “設定 > 軟體庫”。

查詢幫助檔案 Ubuntu 下提供 man 命令以完成幫助手冊得查詢。man 是 manual 的縮寫,通過 man 命令可以對 Linux 下常用命令、安裝軟體、以及C語言常用函式等進行查詢,獲得相關幫助。

例如:

[email protected]:~$man printf
PRINTF(1)                 BSD General Commands Manual                    PRINTF(1)

NAME
     printf -- formatted output

SYNOPSIS
     printf format [arguments ...]

DESCRIPTION
     The printf utility formats and prints its arguments, after the first, under control of the format. The format is a character string which contains three types of objects: plain characters, which are simply copied to standard output, character escape sequences which are converted and copied to the standard output, and format specifications, each of which causes ...
           ...
     The characters and their meanings are as follows:
           \e      Write an <escape> character.
           \a      Write a <bell> character.
           ...

通常可能會用到的幫助檔案例如:

gcc-doc cpp-doc glibc-doc

上述幫助檔案可以通過 apt-get 命令或者軟體包管理器獲得。獲得以後可以通過 man 命令進行命令或者引數查詢。

1.2.2.2. 實驗中可能使用的軟體

編輯器

(1) Ubuntu 下自帶的編輯器可以作為程式碼編輯的工具。例如 gedit 是 gnome 桌面環境下相容UTF-8的文字編輯器。它十分的簡單易用,有良好的語法高亮,對中文支援很好。通常可以通過雙擊或者命令列開啟目標檔案進行編輯。

(2) Vim 編輯器:Vim是一款極方便的文字編輯軟體,是UNIX下的同類型軟體VI的改進版本。Vim經常被看作是“專門為程式設計師打造的文字編輯器”,功能強大且方便使用,便於進行程式開發。 Ubuntu 下預設安裝的 vi 版本較低,功能較弱,建議在系統內安裝或者升級到最新版本的 Vim。

[1]關於Vim的常用命令以及使用,可以通過網路進行查詢。

[2]配置檔案:Vim 的使用需要配置檔案進行設定,例如:

set nocompatible
set encoding=utf-8
set fileencodings=utf-8,chinese
set tabstop=4
set cindent shiftwidth=4
set backspace=indent,eol,start
autocmd Filetype c set omnifunc=ccomplete#Complete
autocmd Filetype cpp set omnifunc=cppcomplete#Complete
set incsearch
set number
set display=lastline
set ignorecase
syntax on
set nobackup
set ruler
set showcmd
set smartindent
set hlsearch
set cmdheight=1
set laststatus=2
set shortmess=atI
set formatoptions=tcrqn
set autoindent  

可以將上述配置檔案儲存到:

~/.vimrc

注意:.vimrc 預設情況下隱藏不可見,可以在命令列中通過 “ls -a” 命令進行檢視。如果 ‘~’ 目錄下不存在該檔案,可以手動建立。修改該檔案以後,重啟 Vim 可以使配置生效。

exuberant-ctags exuberant-ctags 可以為程式語言物件生成索引,其結果能夠被一個文字編輯器或者其他工具簡捷迅速的定位。支援的編輯器有 Vim、Emacs 等。 實驗中,可以使用命令:

ctags -h=.h.c.S -R

預設的生成檔案為 tags (可以通過 -f 來指定),在相同路徑下使用 Vim 可以使用改索引檔案,例如:

使用 “ctrl + ]” 可以跳轉到相應的宣告或者定義處,使用 “ctrl + t” 返回(查詢堆疊)等。
提示:習慣GUI方式的同學,可採用圖形介面的understand、source insight等軟體。 diff & patch

diff 為 Linux 命令,用於比較文字或者資料夾差異,可以通過 man 來查詢其功能以及引數的使用。使用 patch 命令可以對檔案或者資料夾應用修改。

例如實驗中可能會在 proj_b 中應用前一個實驗proj_a 中對檔案進行的修改,可以使用如下命令:

diff -r -u -P proj_a_original proj_a_mine > diff.patch
cd proj_b
patch -p1 -u < ../diff.patch

注意:proj_a_original 指 proj_a 的原始檔,即未經修改的原始碼包,proj_a_mine 是修改後的程式碼包。第一條命令是遞迴的比較資料夾差異,並將結果重定向輸出到 diff.patch 檔案中;第三條命令是將 proj_a 的修改應用到 proj_b 資料夾中的程式碼中。

提示:習慣GUI方式的同學,可採用圖形介面的meld、kdiff3、UltraCompare等軟體。

1.2.3. 瞭解程式設計開發除錯的基本工具

在Ubuntu Linux中的C語言程式設計主要基於GNU C的語法,通過gcc來編譯並生成最終執行檔案。GNU彙編(assembler)採用的是AT&T彙編格式,Microsoft 彙編採用Intel格式。

1.2.3.1. gcc的基本用法

1.2.3.1.1.編譯簡單的 C 程式

C 語言經典的入門例子是 Hello World,下面是一示例程式碼:

#include <stdio.h>
int
main(void)
{
    printf("Hello, world!\n");
    return 0;
}

我們假定該程式碼存為檔案‘hello.c’。要用 gcc 編譯該檔案,使用下面的命令:

$ gcc -Wall hello.c -o hello

該命令將檔案‘hello.c’中的程式碼編譯為機器碼並存儲在可執行檔案 ‘hello’中。機器碼的檔名是通過 -o 選項指定的。該選項通常作為命令列中的最後一個引數。如果被省略,輸出檔案預設為 ‘a.out’。

注意到如果當前目錄中與可執行檔案重名的檔案已經存在,它將被複蓋。 選項 -Wall 開啟編譯器幾乎所有常用的警告──強烈建議你始終使用該選項。編譯器有很多其他的警告選項,但 -Wall 是最常用的。預設情況下GCC 不會產生任何警告資訊。當編寫 C 或 C++ 程式時編譯器警告非常有助於檢測程式存在的問題。

本例中,編譯器使用了 -Wall 選項而沒產生任何警告,因為示例程式是完全合法的。

要執行該程式,輸入可執行檔案的路徑如下:

$ ./hello Hello, world!

這將可執行檔案載入記憶體,並使 CPU 開始執行其包含的指令。 路徑 ./ 指代當前目錄,因此 ./hello 載入並執行當前目錄下的可執行檔案 ‘hello’。

1.2.3.1.2. AT&T彙編基本語法

Ucore中用到的是AT&T格式的彙編,與Intel格式的彙編有一些不同。二者語法上主要有以下幾個不同:

* 暫存器命名原則
    AT&T: %eax                      Intel: eax
* 源/目的運算元順序 
    AT&T: movl %eax, %ebx           Intel: mov ebx, eax
* 常數/立即數的格式 
    AT&T: movl $_value, %ebx        Intel: mov eax, _value
  把value的地址放入eax暫存器
    AT&T: movl $0xd00d, %ebx        Intel: mov ebx, 0xd00d
* 運算元長度標識 
    AT&T: movw %ax, %bx             Intel: mov bx, ax
* 定址方式 
    AT&T:   immed32(basepointer, indexpointer, indexscale)
    Intel:  [basepointer + indexpointer × indexscale + imm32)

如果作業系統工作於保護模式下,用的是32位線性地址,所以在計算地址時不用考慮segment:offset的問題。上式中的地址應為:

imm32 + basepointer + indexpointer × indexscale

下面是一些例子:

* 直接定址 
        AT&T:  foo                         Intel: [foo]
        boo是一個全域性變數。注意加上$是表示地址引用,不加是表示值引用。對於區域性變數,可以通過堆疊指標引用。

* 暫存器間接定址 
        AT&T: (%eax)                        Intel: [eax]

* 變址定址 
        AT&T: _variable(%eax)               Intel: [eax + _variable]
        AT&T: _array( ,%eax, 4)             Intel: [eax × 4 + _array]
        AT&T: _array(%ebx, %eax,8)          Intel: [ebx + eax × 8 + _array]

1.2.3.1.3. GCC基本內聯彙編

GCC 提供了兩內內聯彙編語句(inline asm statements):基本內聯彙編語句(basic inline asm statement)和擴充套件內聯彙編語句(extended inline asm statement)。GCC基本內聯彙編很簡單,一般是按照下面的格式:

asm("statements");

例如:

asm("nop"); asm("cli");

“asm” 和 “asm” 的含義是完全一樣的。如果有多行彙編,則每一行都要加上 “\n\t”。其中的 “\n” 是換行符,"\t” 是 tab 符,在每條命令的 結束加這兩個符號,是為了讓 gcc 把內聯彙編程式碼翻譯成一般的彙編程式碼時能夠保證換行和留有一定的空格。對於基本asm語句,GCC編譯出來的彙編程式碼就是雙引號裡的內容。例如:

    asm( "pushl %eax\n\t"
         "movl $0,%eax\n\t"
         "popl %eax"
    );

實際上gcc在處理彙編時,是要把asm(…)的內容"列印"到彙編檔案中,所以格式控制字元是必要的。再例如:

asm("movl %eax, %ebx");
asm("xorl %ebx, %edx");
asm("movl $0, _boo);

在上面的例子中,由於我們在內聯彙編中改變了 edx 和 ebx 的值,但是由於 gcc 的特殊的處理方法,即先形成彙編檔案,再交給 GAS 去彙編,所以 GAS 並不知道我們已經改變了 edx和 ebx 的值,如果程式的上下文需要 edx 或 ebx 作其他記憶體單元或變數的暫存,就會產生沒有預料的多次賦值,引起嚴重的後果。對於變數 _boo也存在一樣的問題。為了解決這個問題,就要用到擴充套件 GCC 內聯彙編語法。

1.2.3.1.4. GCC擴充套件內聯彙編

使用GCC擴充套件內聯彙編的例子如下:

#define read_cr0() ({ \
    unsigned int __dummy; \
    __asm__( \
        "movl %%cr0,%0\n\t" \
        :"=r" (__dummy)); \
    __dummy; \
})

它代表什麼含義呢?這需要從其基本格式講起。GCC擴充套件內聯彙編的基本格式是:

asm [volatile] ( Assembler Template
   : Output Operands
   [ : Input Operands
   [ : Clobbers ] ])

其中,asm 表示彙編程式碼的開始,其後可以跟 volatile(這是可選項),其含義是避免 “asm” 指令被刪除、移動或組合,在執行程式碼時,如果不希望彙編語句被 gcc 優化而改變位置,就需要在 asm 符號後新增 volatile 關鍵詞:asm volatile(…);或者更詳細地說明為:asm volatile(…);然後就是小括弧,括弧中的內容是具體的內聯彙編指令程式碼。
“” 為彙編指令部分,例如,“movl %%cr0,%0\n\t”。數字前加字首 “%“,如%1,%2等表示使用暫存器的樣板運算元。可以使用的運算元總數取決於具體CPU中通用暫存器的數 量,如Intel可以有8個。指令中有幾個運算元,就說明有幾個變數需要與暫存器結合,由gcc在編譯時根據後面輸出部分和輸入部分的約束條件進行相應的處理。由於這些樣板運算元的字首使用了”%“,因此,在用到具體的暫存器時就在前面加兩個“%”,如%%cr0。
輸出部分(output operand list),用以規定對輸出變數(目標運算元)如何與暫存器結合的約束(constraint),輸出部分可以有多個約束,互相以逗號分開。每個約束以“=”開頭,接著用一個字母來表示運算元的型別,然後是關於變數結合的約束。
例如,上例中:

:"=r" (__dummy)

“=r”表示相應的目標運算元(指令部分的%0)可以使用任何一個通用暫存器,並且變數__dummy 存放在這個暫存器中,但如果是:

:“=m”(__dummy)

“=m”就表示相應的目標運算元是存放在記憶體單元__dummy中。表示約束條件的字母很多,下表給出幾個主要的約束字母及其含義:

字母 字母
m, v, o 記憶體單元
R 任何通用暫存器
Q 暫存器eax, ebx, ecx,edx之一
I, h 直接運算元
E, F 浮點數
G 任意
a, b, c, d 暫存器eax/ax/al, ebx/bx/bl, ecx/cx/cl或edx/dx/dl
S, D 暫存器esi或edi
I 常數(0~31

輸入部分(input operand list):輸入部分與輸出部分相似,但沒有“=”。如果輸入部分一個運算元所要求使用的暫存器,與前面輸出部分某個約束所要求的是同一個暫存器,那就把對應運算元的編號(如“1”,“2”等)放在約束條件中。在後面的例子中,可看到這種情況。修改部分(clobber list,也稱 亂碼列表):這部分常常以“memory”為約束條件,以表示操作完成後記憶體中的內容已有改變,如果原來某個暫存器的內容來自記憶體,那麼現在記憶體中這個單元的內容已經改變。亂碼列表通知編譯器,有些暫存器或記憶體因內聯彙編塊造成亂碼,可隱式地破壞了條件暫存器的某些位(欄位)。 注意,指令部分為必選項,而輸入部分、輸出部分及修改部分為可選項,當輸入部分存在,而輸出部分不存在時,分號“:“要保留,當“memory”存在時,三個分號都要保留,例如

#define __cli() __asm__ __volatile__("cli": : :"memory")

下面是一個例子:

   int count=1;
    int value=1;
    int buf[10];
    void main()
    {
        asm(
            "cld nt"
            "rep nt"
            "stosl"
        :
        : "c" (count), "a" (value) , "D" (buf[0])
        : "%ecx","%edi"
        );
    }

得到的主要彙編程式碼為:

movl count,%ecx
movl value,%eax
movl buf,%edi
#APP
cld
rep
stosl
#NO_APP

cld,rep,stos這幾條語句的功能是向buf中寫上count個value值。冒號後的語句指明輸入,輸出和被改變的暫存器。通過冒號以後的語句,編譯器就知道你的指令需要和改變哪些暫存器,從而可以優化暫存器的分配。其中符號"c"(count)指示要把count的值放入ecx暫存器。類似的還有:

a eax
b ebx
c ecx
d edx
S esi
D edi
I 常數值,(0 - 31)
q,r 動態分配的暫存器
g eax,ebx,ecx,edx或記憶體變數
A 把eax和edx合成一個64位的暫存器(use long longs)

也可以讓gcc自己選擇合適的暫存器。如下面的例子:

asm("leal (%1,%1,4),%0"
    : "=r" (x)
    : "0" (x)
);

這段程式碼到的主要彙編程式碼為:

movl x,%eax
#APP
leal (%eax,%eax,4),%eax
#NO_APP
movl %eax,x

幾點說明:

[1] 使用q指示編譯器從eax, ebx, ecx, edx分配暫存器。 使用r指示編譯器從eax, ebx, ecx, edx, esi, edi分配暫存器。
[2] 不必把編譯器分配的暫存器放入改變的暫存器列表,因為暫存器已經記住了它們。
[3] "="是標示輸出暫存器,必須這樣用。
[4] 數字%n的用法:數字表示的暫存器是按照出現和從左到右的順序對映到用"r"或"q"請求的暫存器.如果要重用"r"或"q"請求的暫存器的話,就可以使用它們。
[5] 如果強制使用固定的暫存器的話,如不用%1,而用ebx,則:

  asm("leal (%%ebx,%%ebx,4),%0"
      : "=r" (x)
      : "0" (x) 
  );

注意要使用兩個%,因為一個%的語法已經被%n用掉了。

1.2.3.2. make和Makefile

GNU make(簡稱make)是一種程式碼維護工具,在大中型專案中,它將根據程式各個模組的更新情況,自動的維護和生成目的碼。

make命令執行時,需要一個 makefile (或Makefile)檔案,以告訴make命令需要怎麼樣的去編譯和連結程式。首先,我們用一個示例來說明makefile的書寫規則。以便給大家一個感興認識。這個示例來源於gnu的make使用手冊,在這個示例中,我們的工程有8個c檔案,和3個頭檔案,我們要寫一個makefile來告訴make命令如何編譯和連結這幾個檔案。我們的規則是:

如果這個工程沒有編譯過,那麼我們的所有c檔案都要編譯並被連結。
如果這個工程的某幾個c檔案被修改,那麼我們只編譯被修改的c檔案,並連結目標程式。
如果這個工程的標頭檔案被改變了,那麼我們需要編譯引用了這幾個標頭檔案的c檔案,並連結目標程式。
只要我們的makefile寫得夠好,所有的這一切,我們只用一個make命令就可以完成,make命令會自動智慧地根據當前的檔案修改的情況來確定哪些檔案需要重編譯,從而自己編譯所需要的檔案和連結目標程式。

makefile的規則

在講述這個makefile之前,還是讓我們先來粗略地看一看makefile的規則。

target ... : prerequisites ...
    command
    ...
    ...

target也就是一個目標檔案,可以是object file,也可以是執行檔案。還可以是一個標籤(label)。prerequisites就是,要生成那個target所需要的檔案或是目標。command也就是make需要執行的命令(任意的shell命令)。 這是一個檔案的依賴關係,也就是說,target這一個或多個的目標檔案依賴於prerequisites中的檔案,其生成規則定義在 command中。如果prerequisites中有一個以上的檔案比target檔案要新,那麼command所定義的命令就會被執行。這就是makefile的規則。也就是makefile中最核心的內容。

1.2.3.3. gdb使用

gdb 是功能強大的除錯程式,可完成如下的除錯任務:

設定斷點
監視程式變數的值
程式的單步(step in/step over)執行
顯示/修改變數的值
顯示/修改暫存器
檢視程式的堆疊情況
遠端除錯
除錯執行緒
在可以使用 gdb 除錯程式之前,必須使用 -g 或 –ggdb編譯選項編譯原始檔。執行 gdb 除錯程式時通常使用如下的命令:

gdb progname

在 gdb 提示符處鍵入help,將列出命令的分類,主要的分類有:

aliases:命令別名
breakpoints:斷點定義;
data:資料檢視;
files:指定並檢視檔案;
internals:維護命令;
running:程式執行;
stack:呼叫棧檢視;
status:狀態檢視;
tracepoints:跟蹤程式執行。
鍵入 help 後跟命令的分類名,可獲得該類命令的詳細清單。gdb的常用命令如下表所示。

表 gdb 的常用命令
在這裡插入圖片描述

下面以一個有錯誤的例子程式來介紹gdb的使用:

/*bugging.c*/ 
 #include "stdio"
 static char buff [256]; 
 static char* string; 
 int main () 
 { 
 printf ("Please input a string: "); 
 gets (string); 
 printf ("\nYour string is: %s\n", string); 
 } 

這個程式是接受使用者的輸入,然後將使用者的輸入打印出來。該程式使用了一個未經過初始化的字串地址 string,因此,編譯並執行之後,將出現 "Segment Fault"錯誤: $ gcc -o bugging -g bugging.c $ ./bugging Please input a string: asdf Segmentation fault (core dumped) 為了查詢該程式中出現的問題,我們利用 gdb,並按如下的步驟進行:

[1] 執行 “gdb bugging” ,載入 bugging 可執行檔案; $gdb bugging
[2] 執行裝入的 bugging 命令; (gdb) run
[3] 使用 where 命令檢視程式出錯的地方; (gdb) where
[4] 利用 list 命令檢視呼叫 gets 函式附近的程式碼; (gdb) list
[5] 在 gdb 中,我們在第 11 行處設定斷點,看看是否是在第11行出錯; (gdb) break 11
[6] 程式重新執行到第 11 行處停止,這時程式正常,然後執行單步命令next; (gdb) next
[7] 程式確實出錯,能夠導致 gets 函數出錯的因素就是變數 string。重新執行測試程,用 print 命令檢視 string 的值; (gdb) run (gdb) print string (gdb) $1=0x0
[8] 問題在於string指向的是一個無效指標,修改程式,在10行和11行之間增加一條語句 “string=buff; ”,重新編譯程式,然後繼續執行,將看到正確的程式執行結果

用gdb檢視原始碼可以用list命令,但是這個不夠靈活。可以使用"layout src"命令,或者按Ctrl-X再按A,就會出現一個視窗可以檢視原始碼。也可以用使用-tui引數,這樣進入gdb裡面後就能直接開啟程式碼檢視視窗.
其他程式碼視窗相關命令:
GSX

1.2.4. 基於硬體模擬器實現原始碼

1.2.4.1. 安裝硬體模擬器QEMU

1.2.4.1.1. Linux執行環境

QEMU用於模擬一臺x86計算機,讓ucore能夠執行在QEMU上。為了能夠正確的編譯和安裝 qemu,儘量使用最新版本的qemu(http://wiki.qemu.org/Download),或者os ftp伺服器上提供的qemu原始碼:qemu-1.1.0.tar.gz)。目前 qemu 能夠支援最新的 gcc-4.x 編譯器。例如:在 Ubuntu 12.04 系統中,預設得版本是 gcc-4.6.x (可以通過 gcc -v 或者 gcc --version 進行檢視)。

可直接使用ubuntu中提供的qemu,只需執行如下命令即可。

sudo apt-get install qemu-system

也可採用下面描述的方法對qemu進行原始碼級安裝。

1.2.4.1.2. Linux環境下的原始碼級安裝過程

1.2.4.1.2.1. 獲得並應用修改

編譯qemu還會用到的庫檔案有 libsdl1.2-dev 等。安裝命令如下:

sudo apt-get install libsdl1.2-dev    # 安裝庫檔案 libsdl1.2-dev

獲得 qemu 的安裝包以後,對其進行解壓縮(如果格式無法識別,請下載相應的解壓縮軟體)。

例如 qemu.tar.gz/qemu.tar.bz2 檔案,在命令列中可以使用:

tar zxvf qemu.tar.gz

或者

tar jxvf qemu.tar.bz2

對 qemu 應用修改:如果實驗中使用的 qemu 需要打patch,應用過程如下所示:

[email protected]:~$ls
qemu.patch      qemu
[email protected]:~$cd qemu
[email protected]:~$patch -p1 -u < ../qemu.patch
1.2.4.1.2.2. 配置、編譯和安裝

編譯以及安裝 qemu 前需要使用 (表示qemu解壓縮路徑)下面的 configure 指令碼生成相應的配置檔案等。而 configure 指令碼有較多的引數可供選擇,可以通過如下命令進行檢視:

configure  --help 

實驗中可能會用到的命令例如:

configure --target-list="i386-softmmu"  # 配置qemu,可模擬X86-32硬體環境
make                                    # 編譯qemu
sudo make install                       # 安裝qemu

qemu執行程式將預設安裝到 /usr/local/bin 目錄下。

如果使用的是預設的安裝路徑,那麼在 “/usr/local/bin” 下面即可看到安裝結果:

qemu-system-i386 qemu-img qemu-nbd ……

建立符號連結檔案qemu

sudo ln –s /usr/local/bin/qemu-system-i386  /usr/local/bin/qemu

1.2.4.2. 使用硬體模擬器QEMU

執行引數
如果 qemu 使用的是預設 /usr/local/bin 安裝路徑,則在命令列中可以直接使用 qemu 命令執行程式。qemu 執行可以有多引數,格式如:

qemu [options] [disk_image]

其中 disk_image 即硬碟映象檔案。

部分引數說明:

`-hda file'        `-hdb file' `-hdc file' `-hdd file'`
    使用 file  作為硬碟0、1、2、3映象。
    
`-fda file'  `-fdb file'
    使用 file  作為軟盤映象,可以使用 /dev/fd0 作為 file 來使用主機軟盤。
    
`-cdrom file'
    使用 file  作為光碟映象,可以使用 /dev/cdrom 作為 file 來使用主機 cd-rom。
    
`-boot [a|c|d]'
    從軟盤(a)、光碟(c)、硬碟啟動(d),預設硬碟啟動。
    
`-snapshot'
    寫入臨時檔案而不寫回磁碟映象,可以使用 C-a s 來強制寫回。
    
`-m megs'
    設定虛擬記憶體為 msg M位元組,預設為 128M 位元組。
    
`-smp n'
    設定為有 n 個 CPU 的 SMP 系統。以 PC 為目標機,最多支援 255 個 CPU。
    
`-nographic'
    禁止使用圖形輸出。
    
其他:
    可用的主機裝置 dev 例如:
        vc
            虛擬終端。
        null
            空裝置
        /dev/XXX
            使用主機的 tty。
        file: filename
            將輸出寫入到檔案 filename 中。
        stdio
            標準輸入/輸出。
        pipe:pipename       
            命令管道 pipename。
           
    使用 dev 裝置的命令如:
        `-serial dev'
            重定向虛擬串列埠到主機裝置 dev 中。
        `-parallel dev'
            重定向虛擬並口到主機裝置 dev 中。
        `-monitor dev'
            重定向 monitor 到主機裝置 dev 中。
    其他引數:
        `-s'
            等待 gdb 連線到埠 1234。
        `-p port'
            改變 gdb 連線埠到 port。
        `-S'
            在啟動時不啟動 CPU, 需要在 monitor 中輸入 'c',才能讓qemu繼續模擬工作。
        `-d'
            輸出日誌到 qemu.log 檔案。

其他引數說明可以參考:http://bellard.org/qemu/qemu-doc.html#SEC15 。其他qemu的安裝和使用的說明可以參考http://bellard.org/qemu/user-doc.html。

或者在命令列收入 qemu (沒有引數) 顯示幫助。

在實驗中,例如 lab1,可能用到的命令如:

qemu -hda ucore.img -parallel stdio        # 讓ucore在qemu模擬的x86硬體環境中執行

qemu -S -s -hda ucore.img -monitor stdio    # 用於與gdb配合進行原始碼除錯

1.2.4.2.2. 常用除錯命令

qemu中monitor的常用命令:
在這裡插入圖片描述

其他具體的命令格式以及說明,參見 qemu help 命令幫助。

注意:qemu 預設有 ‘singlestep arg’ 命令(arg 為 引數),該命令為設定單步標誌命令。例如:‘singlestep off’ 執行結果為禁止單步,‘singlestep on’ 結果為允許單步。在允許單步條件下,使用 cont 命令進行單步操作。如:

(qemu) xp /3i $pc
0xfffffff0: ljmp $0xf000, $0xe05b
0xfffffff5: xor    %bh, (%bx, %si)
0xfffffff7: das
(qemu) singlestep on
(qemu) cont
0x000fe05b: xor %ax, %ax

step命令為單步命令,即qemu執行一步,能夠跳過 breakpoint 斷點執行。如果此時使用cont命令,則qemu 執行改為連續執行。

log命令能夠儲存qemu模擬過程產生的資訊(與qemu執行引數 `-d’ 相同),具體引數可以參考命令幫助。產生的日誌資訊儲存在 “/tmp/qemu.log” 中,例如使用 'log in_asm’命令以後,執行過程產生的的qemu.log 檔案為:

1  ----------------
2  IN:
3  0xfffffff0:  ljmp   $0xf000,$0xe05b
4
5  ----------------
6  IN:
7  0x000fe05b:  xor    %ax,%ax
8  0x000fe05d:  out    %al,$0xd
9  0x000fe05f:  out    %al,$0xda
10 0x000fe061:  mov    $0xc0,%al
11 0x000fe063:  out    %al,$0xd6
12 0x000fe065:  mov    $0x0,%al
13 0x000fe067:  out    %al,$0xd4

1.2.4.3. 結合gdb和qemu原始碼級除錯ucore

1.2.4.3.1. 編譯可除錯的目標檔案

為了使得編譯出來的程式碼是能夠被gdb這樣的偵錯程式除錯,我們需要在使用gcc編譯原始檔的時候新增引數:"-g"。這樣編譯出來的目標檔案中才會包含可以用於偵錯程式進行除錯的相關符號資訊。

1.2.4.3.2. ucore 程式碼編譯

(1) 編譯過程:在解壓縮後的 ucore 原始碼包中使用 make 命令即可。例如 lab1中:

[email protected]: ~/lab1$  make

在lab1目錄下的bin目錄中,生成一系列的目標檔案:

  • ucore.img:被qemu訪問的虛擬硬碟檔案
  • kernel: ELF格式的toy ucore kernel執行文,被嵌入到ucore.img中
  • bootblock: 虛擬的硬碟主引導扇區(512位元組),包含了-bootloader執行程式碼,被嵌入到了ucore.img中
  • sign:外部執行程式,用來生成虛擬的硬碟主引導扇區
    還生成了其他很多檔案,這裡就不一一列舉了。

(2) 儲存修改:

使用 diff 命令對修改後的 ucore 程式碼和 ucore 原始碼進行比較,比較之前建議使用 make clean 命令清除不必要檔案。(如果有ctags 檔案,需要手工清除。)

(3)應用修改:參見 patch 命令說明。

1.2.4.3.3. 使用遠端除錯

為了與qemu配合進行原始碼級別的除錯,需要先讓qemu進入等待gdb偵錯程式的接入並且還不能讓qemu中的CPU執行,因此啟動qemu的時候,我們需要使用引數-S –s這兩個引數來做到這一點。在使用了前面提到的引數啟動qemu之後,qemu中的CPU並不會馬上開始執行,這時我們啟動gdb,然後在gdb命令列介面下,使用下面的命令連線到qemu:

(gdb)  target remote 127.0.0.1:1234

然後輸入c(也就是continue)命令之後,qemu會繼續執行下去,但是gdb由於不知道任何符號資訊,並且也沒有下斷點,是不能進行原始碼級的除錯的。為了讓gdb獲知符號資訊,需要指定除錯目標檔案,gdb中使用file命令:

(gdb)  file ./bin/kernel

之後gdb就會載入這個檔案中的符號資訊了。

通過gdb可以對ucore程式碼進行除錯,以lab1中除錯memset函式為例:

  • (1) 執行 qemu -S -s -hda ./bin/ucore.img -monitor stdio

  • (2) 執行 gdb並與qemu進行連線

  • (3) 設定斷點並執行

  • (4) qemu 單步除錯。

執行過程以及結果如下:
在這裡插入圖片描述

1.2.4.3.4. 使用gdb配置檔案

在上面可以看到,為了進行原始碼級除錯,需要輸入較多的東西,很麻煩。為了方便,可以將這些命令存在指令碼中,並讓gdb在啟動的時候自動載入。

以lab1為例,在lab1/tools目錄下,執行完make後,我們可以建立檔案gdbinit,並輸入下面的內容:

target remote 127.0.0.1:1234
file bin/kernel

為了讓gdb在啟動時執行這些命令,使用下面的命令啟動gdb:

$ gdb -x tools/gdbinit

如果覺得這個命令太長,可以將這個命令存入一個檔案中,當作指令碼來執行。

另外,如果直接使用上面的命令,那麼得到的介面是一個純命令列的介面,不夠直觀,就像下圖這樣:
在這裡插入圖片描述
如果想獲得上面右圖那樣的效果,只需要再加上引數-tui就行了,比如:

gdb -tui -x tools/gdbinit

1.2.4.3.5. 載入除錯目標

在上面小節,我們提到為了能夠讓gdb識別變數的符號,我們必須給gdb載入符號表等資訊。在進行gdb本地應用程式除錯的時候,因為在指定了執行檔案時就已經載入了檔案中包含的除錯資訊,因此不用再使用gdb命令專門載入了。但是在使用qemu進行遠端除錯的時候,我們必須手動載入符號表,也就是在gdb中用file命令。

這樣載入除錯資訊都是按照elf檔案中制定的虛擬地址進行載入的,這在靜態連線的程式碼中沒有任何問題。但是在除錯含有動態連結庫的程式碼時,動態連結庫的ELF執行檔案頭中指定的載入虛擬地址都是0,這個地址實際上是不正確的。從作業系統角度來看,使用者態的動態連結庫的載入地址都是由作業系統動態分配的,沒有一個固定值。然後作業系統再把動態連結庫載入到這個地址,並由使用者態的庫連結器(linker)把動態連結庫中的地址資訊重新設定,自此動態連結庫才可正常執行。

由於分配地址的動態性,gdb並不知道這個分配的地址是多少,因此當我們在對這樣動態連結的程式碼進行除錯的時候,需要手動要求gdb將除錯資訊載入到指定地址。

下面,我們要求gdb將linker載入到0x6fee6180這個地址上:

(gdb) add-symbol-file android_test/system/bin/linker 0x6fee6180

這樣的命令預設是將程式碼段(.data)段的除錯資訊載入到0x6fee6180上,當然,你也可以通過“-s”這個引數來指定,比如: (gdb) add-symbol-file android_test/system/bin/linker –s .text 0x6fee6180

這樣,在執行到linker中程式碼時gdb就能夠顯示出正確的程式碼和除錯資訊出來。

這個方法在作業系統中除錯動態連結器時特別有用。

1.2.4.3.6. 設定除錯目標架構

在除錯的時候,我們也許需要除錯不是i386保護模式的程式碼,比如8086真實模式的程式碼,我們需要設定當前使用的架構:

(gdb) set arch i8086

這個方法在除錯不同架構或者說不同模式的程式碼時還是有點用處的。

1.2.5. 瞭解處理器硬體

要想深入理解ucore,就需要了解支撐ucore執行的硬體環境,即瞭解處理器體系結構(瞭解硬體對ucore帶來影響)和機器指令集(讀懂ucore的彙編)。ucore目前支援的硬體環境是基於Intel 80386以上的計算機系統。更多的硬體相關內容(比如保護模式等)將隨著實現ucore的過程逐漸展開介紹。

1.2.5.1. Intel 80386執行模式

一般CPU只有一種執行模式,能夠支援多個程式在各自獨立的記憶體空間中併發執行,且有使用者特權級和核心特權級的區分,讓一般應用不能破壞作業系統核心和執行特權指令。80386處理器有四種執行模式:真實模式、保護模式、SMM模式和虛擬8086模式。這裡對涉及ucore的真實模式、保護模式做一個簡要介紹。

真實模式:這是個人計算機早期的8086處理器採用的一種簡單執行模式,當時微軟的MS-DOS作業系統主要就是執行在8086的真實模式下。80386加電啟動後處於真實模式執行狀態,在這種狀態下軟體可訪問的實體記憶體空間不能超過1MB,且無法發揮Intel 80386以上級別的32位CPU的4GB記憶體管理能力。真實模式將整個實體記憶體看成分段的區域,程式程式碼和資料位於不同區域,作業系統和使用者程式並沒有區別對待,而且每一個指標都是指向實際的實體地址。這樣使用者程式的一個指標如果指向了作業系統區域或其他使用者程式區域,並修改了內容,那麼其後果就很可能是災難性的。

對於ucore其實沒有必要涉及,這主要是Intel x86的向下相容需求導致其一直存在。其他一些CPU,比如ARM、MIPS等就沒有真實模式,而是隻有類似保護模式這樣的CPU模式。

保護模式:保護模式的一個主要目標是確保應用程式無法對作業系統進行破壞。實際上,80386就是通過在真實模式下初始化控制暫存器(如GDTR,LDTR,IDTR與TR等管理暫存器)以及頁表,然後再通過設定CR0暫存器使其中的保護模式使能位置位,從而進入到80386的保護模式。當80386工作在保護模式下的時候,其所有的32根地址線都可供定址,物理定址空間高達4GB。在保護模式下,支援記憶體分頁機制,提供了對虛擬記憶體的良好支援。保護模式下80386支援多工,還支援優先順序機制,不同的程式可以執行在不同的特權級上。特權級一共分0~3四個級別,作業系統執行在最高的特權級0上,應用程式則執行在比較低的級別上;配合良好的檢查機制後,既可以在任務間實現資料的安全共享也可以很好地隔離各個任務。

這一段中很多術語沒有解釋,在後續的章節中會逐一展開闡述。

1.2.5.2. Intel 80386記憶體架構

地址是訪問記憶體空間的索引。一般而言,記憶體地址有兩個:一個是CPU通過匯流排訪問實體記憶體用到的實體地址,一個是我們編寫的應用程式所用到的邏輯地址(也有人稱為虛擬地址)。比如如下C程式碼片段:

int boo=1;
int *foo=&a;

這裡的boo是一個整型變數,foo變數是一個指向boo地址的整型指標變數,foo中儲存的內容就是boo的邏輯地址。

80386是32位的處理器,即可以定址的實體記憶體地址空間為2^32=4G位元組。為更好理解面向80386處理器的ucore作業系統,需要用到三個地址空間的概念:實體地址、線性地址和邏輯地址。實體記憶體地址空間是處理器提交到總線上用於訪問計算機系統中的記憶體和外設的最終地址。一個計算機系統中只有一個實體地址空間。線性地址空間是80386處理器通過段(Segment)機制控制下的形成的地址空間。在作業系統的管理下,每個執行的應用程式有相對獨立的一個或多個記憶體空間段,每個段有各自的起始地址和長度屬性,大小不固定,這樣可讓多個執行的應用程式之間相互隔離,實現對地址空間的保護。

在作業系統完成對80386處理器段機制的初始化和配置(主要是需要作業系統通過特定的指令和操作建立全域性描述符表,完成虛擬地址與線性地址的對映關係)後,80386處理器的段管理功能單元負責把虛擬地址轉換成線性地址,在沒有下面介紹的頁機制啟動的情況下,這個線性地址就是實體地址。

相對而言,段機制對大量應用程式分散地使用大記憶體的支援能力較弱。所以Intel公司又加入了頁機制,每個頁的大小是固定的(一般為4KB),也可完成對記憶體單元的安全保護,隔離,且可有效支援大量應用程式分散地使用大記憶體的情況。

在作業系統完成對80386處理器頁機制的初始化和配置(主要是需要作業系統通過特定的指令和操作建立頁表,完成虛擬地址與線性地址的對映關係)後,應用程式看到的邏輯地址先被處理器中的段管理功能單元轉換為線性地址,然後再通過80386處理器中的頁管理功能單元把線性地址轉換成實體地址。

頁機制和段機制有一定程度的功能重複,但Intel公司為了向下相容等目標,使得這兩者一直共存。

上述三種地址的關係如下:

  • 分段機制啟動、分頁機制未啟動:邏輯地址—>段機制處理—>線性地址=實體地址

  • 分段機制和分頁機制都啟動:邏輯地址—>段機制處理—>線性地址—>頁機制處理—>實體地址

1.2.5.3. Intel 80386暫存器

這裡假定讀者對80386 CPU有一定的瞭解,所以只作簡單介紹。80386的暫存器可以分為8組:通用暫存器,段暫存器,指令指標暫存器,標誌暫存器,系統地址暫存器,控制暫存器,除錯暫存器,測試暫存器,它們的寬度都是32位。一般程式設計師看到的暫存器包括通用暫存器,段暫存器,指令指標暫存器,標誌暫存器。

General Register(通用暫存器):EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP這些暫存器的低16位就是8086的 AX/BX/CX/DX/SI/DI/SP/BP,對於AX,BX,CX,DX這四個暫存器來講,可以單獨存取它們的高8位和低8位 (AH,AL,BH,BL,CH,CL,DH,DL)。它們的含義如下

    EAX:累加器
    EBX:基址暫存器
    ECX:計數器
    EDX:資料暫存器
    ESI:源地址指標暫存器
    EDI:目的地址指標暫存器
    EBP:基址指標暫存器
    ESP:堆疊指標暫存器

在這裡插入圖片描述

Segment Register(段暫存器,也稱 Segment Selector,段選擇符,段選擇子):除了8086的4個段外(CS,DS,ES,SS),80386還增加了兩個段FS,GS,這些段暫存器都是16位的,用於不同屬性記憶體段的定址,它們的含義如下:

CS:程式碼段(Code Segment)
DS:資料段(Data Segment)
ES:附加資料段(Extra Segment)
SS:堆疊段(Stack Segment)
FS:附加段
GS 附加段

在這裡插入圖片描述

Instruction Pointer(指令指標暫存器):EIP的低16位就是8086的IP,它儲存的是下一條要執行指令的記憶體地址,在分段地址轉換中,表示指令的段內偏移地址。
在這裡插入圖片描述

Flag Register(標誌暫存器):EFLAGS,和8086的16位標誌暫存器相比,增加了4個控制位,這20位控制/標誌位的位置如下圖所示:
在這裡插入圖片描述
相關的控制/標誌位含義是:

CF(Carry Flag):進位標誌位;
PF(Parity Flag):奇偶標誌位;
AF(Assistant Flag):輔助進位標誌位;
ZF(Zero Flag):零標誌位;
SF(Singal Flag):符號標誌位;
IF(Interrupt Flag):中斷允許標誌位,由CLI,STI兩條指令來控制;設定IF位使CPU可識別外部(可遮蔽)中斷請求,復位IF位則禁止中斷,IF位對不可遮蔽外部中斷和故障中斷的識別沒有任何作用;
DF(Direction Flag):向量標誌位,由CLD,STD兩條指令來控制;
OF(Overflow Flag):溢位標誌位;
IOPL(I/O Privilege Level):I/O特權級欄位,它的寬度為2位,它指定了I/O指令的特權級。如果當前的特權級別在數值上小於或等於IOPL,那麼I/O指令可執行。否則,將發生一個保護性故障中斷;
NT(Nested Task):控制中斷返回指令IRET,它寬度為1位。若NT=0,則用堆疊中儲存的值恢復EFLAGS,CS和EIP從而實現中斷返回;若NT=1,則通過任務切換實現中斷返回。在ucore中,設定NT為0。

還有一些應用程式無法訪問的控制暫存器,如CR0,CR2,CR3…,將在後續章節逐一講解。

1.2.6. 瞭解ucore程式設計方法和通用資料結構

1.2.6.1. 面向物件程式設計方法

面向物件程式設計方法
uCore設計中採用了一定的面向物件程式設計方法。雖然C 語言對面向物件程式設計並沒有原生支援,但沒有原生支援並不等於我們不能用 C 語言寫面向物件程式。需要注意,我們並不需要用 C語言模擬出一個常見 C++ 編譯器已經實現的物件模型。如果是這樣,還不如直接採用C++程式設計。

uCore的面向物件程式設計方法,目前主要是採用了類似C++的介面(interface)概念,即是讓實現細節不同的某類核心子系統(比如實體記憶體分配器、排程器,檔案系統等)有共同的操作方式,這樣雖然記憶體子系統的實現千差萬別,但它的訪問介面是不變的。這樣不同的核心子系統之間就可以靈活組合在一起,實現風格各異,功能不同的作業系統。介面在 C 語言中,表現為一組函式指標的集合。放在 C++ 中,即為虛表。介面設計的難點是如果找出各種核心子系統的共性訪問/操作模式,從而可以根據訪問模式提取出函式指標列表。

比如對於uCore核心中的實體記憶體管理子系統,首先通過分析核心中其他子系統可能對實體記憶體管理子系統,明確實體記憶體管理子系統的訪問/操作模式,然後我們定義了pmm_manager資料結構(位於lab2/kern/mm/pmm.h)如下:

// pmm_manager is a physical memory management class. A special pmm manager - XXX_pmm_manager
// only needs to implement the methods in pmm_manager class, then XXX_pmm_manager can be used
// by ucore to manage the total physical memory space.
struct pmm_manager {
    // XXX_pmm_manager's name
    const char *name;  
    // initialize internal description&management data structure
    // (free block list, number of free block) of XXX_pmm_manager 
    void (*init)(void); 
    // setup description&management data structcure according to
    // the initial free physical memory space 
    void (*init_memmap)(struct Page *base, size_t n); 
    // allocate >=n pages, depend on the allocation algorithm 
    struct Page *(*alloc_pages)(size_t n);  
    // free >=n pages with "base" addr of Page descriptor structures(memlayout.h)
    void (*free_pages)(struct Page *base, size_t n);   
    // return the number of free pages 
    size_t (*nr_free_pages)(void);                     
    // check the correctness of XXX_pmm_manager
    void (*check)(void);                               
};

這樣基於此資料結構,我們可以實現不同連續記憶體分配演算法的實體記憶體管理子系統,而這些實體記憶體管理子系統需要編寫演算法,把演算法實現在此結構中定義的init(初始化)、init_memmap(分析空閒實體記憶體並初始化管理)、alloc_pages(分配物理頁)、free_pages(釋放物理頁)函式指標所對應的函式中。而其他記憶體子系統需要與實體記憶體管理子系統互動時,只需呼叫特定實體記憶體管理子系統所採用的pmm_manager資料結構變數中的函式指標即可

1.2.6.2. 通用資料結構雙向迴圈連結串列

雙向迴圈連結串列
在“資料結構”課程中,如果建立某種資料結構的雙迴圈連結串列,通常採用的辦法是在這個資料結構的型別定義中有專門的成員變數 data, 並且加入兩個指向該型別的指標next和prev