計算機系統分為三大部分: 處理器 (CPU)、內存 (memory) 和輸出入裝置 (I/O)。分別被作業系統抽象為: 進程 (process)、虛擬內存 (virtual memory) 和文件 (file)。

內核研究

主要研究 Linux-0.11/12 版。

內存

虛擬內存 (virtual memory) 的中心思想是在硬盤上配置進程的虛擬地址空間 (virtual address space),將實體內存當作該虛擬地址空間的緩存。虛擬地址空間 (virtual address space) 和物理位址空間 (physical address space) 的對映 (mapping) 存放在作業系統維護的頁表當中。硬體會利用該頁表進行轉換。

處理器送出虛擬位址 (virtual address),透過位在處理器上的硬件 MMU 轉換成物理位址 (physical address),送至內存存取其中的內容。MMU 透過作業系統維護在內存中的頁表進行位址轉換。處理器上的 MMU 的緩存 TLB 可用來加速此一過程。虛擬位址可分成兩個欄位: 虛擬頁號 (virtual page number, VPN) 和虛擬頁面偏移量 (virtual page offset, VPO); 物理位址也可分成兩個欄位: 物理頁號 (physical page number, PPN) 和物理頁面偏移量 (physical page offset, PPO)。由於虛擬頁面和物理頁面具有相同大小,地址轉換基本上是將虛擬頁號換成是物理頁號,偏移量不變。地址轉換過程如下:

  1. 處理器送出虛擬位址給 MMU。
  2. MMU 利用虛擬頁號配合頁表基址寄存器 (page table base register, PTBR) 送出頁表條目地址 (page table entry address, PTEA) 給緩存。
    1. 緩存命中,將頁表條目 (page table entry, PTE) 送回給 MMU。
    2. 緩存不命中,將頁表條目地址送至內存查詢頁表,並將內存返回的頁表條目緩存起來。
  3. MMU 利用頁表條目的內容將虛擬位址轉換成物理位址,送至緩存。
    1. 緩存命中,將該物理位址存放的資料返回給處理器。
    2. 緩存不命中,將該物理位址送至內存,並將內存返回的資料緩存起來。

上述過程二可以利用處理器上的硬件 TLB 加速,TLB 即為頁表條目的緩存。虛擬頁號又可再分為兩欄位: TLB 索引 (TLBI) 和 TLB 標記 (TLBT)。前者用來索引 TLB,後者用來確認索引到的頁表條目是否正確。過程如下:

  1. MMU 將虛擬頁號 (TLB 索引 + TLB 標記) 送至 TLB。
    1. TLB 命中,返回頁表條目。MMU 利用頁表條目的內容將虛擬位址轉換成物理位址,送至緩存。
    2. TLB 不命中,MMU 送出頁表條目地址給緩存。
      1. 緩存命中,返回頁表條目給 MMU,並緩存在 TLB。
      2. 緩存不命中,將頁表條目地址送至內存查詢頁表,並將內存返回的頁表條目緩存起來。

可以看到,如果 TLB 命中,則地址轉換是在處理器上完成,不需存取處理器外部的儲存器。

x86 處理器在將位址轉換成物理位址之前,已經過一次轉換,即虛擬位址到線性位址的轉換,中間透過段機制。請見http://en.wikipedia.org/wiki/X86_memory_segmentation#Practices

中斷

以單處理器為例。設備發出中斷請求 (Interrupt request) 至可編程中斷控制器 (Programmable Interrupt Controller (PIC)),可編程中斷控制器根據優先權將中斷請求送至處理器,透過拉起 INT (中斷)管腳,並隨後將設備號送至處理器。多處理器的情況則稍有不同,高級可編程中斷控制器 (Advanced Programmable Interrupt Controller) 取代原先的可編程中斷控制器,由位於南橋中的 I/O 高級可編程中斷控制器 (IOAPIC) 和位於處理器上的本地高級可編程中斷控制器 (LAPIC) 組成。

外設

PCI 總線 (Bus) 是一種樹狀結構,其節點可以是 PCI 外設或是橋 (ISA 轉 PCI,PCI 轉 PCI)。BIOS 於 POST (Power-On Self-Test) 時,會枚舉系統上所有的 PCI 外設。PCI 外設以其設備標識符 (BDF,Bus、Device and Function) 來辨識。一般透過 PCI 配置空間 (PCI configuration space) 操作 PIC 外設。PCI 配置空間有三個欄位較為重要:

  1. 基地址暫存器 (Base Address Register): 也稱作 PCI Bar。其值為外設暫存器或外設內存在 IO 端口地址空間 (或物理地址空間) 的地址。此地址是由 BIOS 或作業系統動態配置。軟件 (BIOS 或作業系統) 在枚舉系統上所有的 PCI 外設之後,會為所有的 PCI 外設分配 IO 端口 (或物理內存)。外設再把該 IO 端口 (物理內存) 映射到自己的暫存器或內存。
  2. 中斷針腳 (Interrupt Pin): 其值代表外設連接的是哪一個中斷針腳。
  3. 外設中斷線 (Interrupt Line): BIOS 通常用來保存外設所連接的 PIC/IOPIC 的管腳號。

枚舉外設的過程就是要在內存中建立一個和實際總線相符的設備樹。

開機程序

一般開機程序如下: BIOS → 引導程序 → 操作系統

  1. BIOS 是系統啟動 (機器上電) 後執行的第一道指令。進行硬件自檢和初始化、設置基本的中斷向量和載入引導程序。
  2. 引導程序負責設置操作系統的運行環境、將操作系統從外存載入內存,並跳至操作系統開始執行。
  3. 這部分才是嚴格意義上的操作系統。

嵌入式系統通常會直接把引導程序寫入 flash 或 rom,直接從引導程序開始執行。此時,引導程序會負責初始化硬件和載入操作系統。更簡單的嵌入式操作系統自己負責初始化硬件,進而不需要引導程序。

由軟碟開機的流程如下:

BIOS → bootsect.S → setup.S → head.s → main.c

  1. 開機後,處理器處於實模式並從 0xFFFF0 開始執行代碼,該位址會被映射至 ROM 中 BIOS 所在位址。BIOS 開始系統檢測並在內存起始位址初始化中段向量。
  2. BIOS 將軟碟上的第一個扇區,又稱引導扇區 (bootsect.S) 讀入到內存 0x7C00 (物理位址),跳轉至該位址開始執行。
  3. bootsect.S 將自己移動至 0x90000,並把 setup.S 以及內核映像分別讀入到內存 0x90200 和 0x10000,跳轉至 setup.S 執行。
  4. setup.S 利用 BIOS 在內存 0x0000 設置的中段向量獲取系統參數,將此資訊存放在以 0x90000 起始的位址,bootsect.S 此時會被覆蓋掉。再將內核映像搬移至 0x0000,此時會覆蓋掉 BIOS 設置的中段向量。切到保護模式並跳轉至 0x0000 開始執行。
  5. head.s 已處於保護模式執行。

由硬碟開機的流程如下:

BIOS → MBR → bootloader → kernel

  1. 開機後,處理器處於實模式並從 0xFFFF0 開始執行代碼,該位址會被映射至 ROM 中 BIOS 所在位址。BIOS 開始系統檢測並在內存起始位址初始化中段向量。
  2. BIOS 將硬碟上的第一個扇區,又稱主引導扇區 (MBR) 讀入到內存 0x7C00 (物理位址),跳轉至該位址開始執行。MBR 作用類似 bootsect.S,但它需要能用識別檔案系統,這是因為內核映像會存放在根文件系統中。
  3. MBR 將自己移動至 0x600,然後讀入 MBR 中的分區表所指明的活動分區中位於第一扇區 (引導扇區) 的 bootloader 至內存 0x7C00 (物理位址),跳轉至該位址開始執行。
  4. bootloader 讀入內核映像。此時仍處於實模式以便使用 BIOS 設置的中段向量。實模式可存取 1MB 的內存,但是內核映像往往超過 1MB。非實模式unreal mode 即是使得處理器能夠存取超過 1MB 範圍的內存,同時又能如同在實模式底下使用 BIOS 設置的中段向量。

開機過程中會做段間跳轉 (intersegment jump)。

檔案系統

一個硬盤上可以有多個分割區 (partition),分割區上有檔案系統 (file system)。檔案系統存放兩類資料: 使用者的資料,和描述該檔案系統的資料 ,又稱元數據 (metadata)。兩者皆以塊 (block) 為基本單位储存。

元數據有以下幾類:

  1. 超級塊 (super block): 描述該檔案系統的格式、大小、狀態和其它元數據的資訊。如果超級塊損毀,其它資料極有可能讀取不到。
  2. 索引節點 (inode,index node): 描述一個檔案的屬性,如: 檔案格式、存取權限、擁有者及所屬群組,但不包含該檔案的檔名。另外也存放該檔案內含資料所在的塊號。檔案中的資料存放在塊 (block) 上。
  3. 目錄 (directory): 本身也是一個檔案,因此也有相對應的索引節點。目錄檔案內存該目錄底下的檔案其檔名和相對應的索引節點。

硬連結 (hard link) 用來將多個檔名連到同一個索引節點。一個索引節點用來表示一個檔案系統中的檔案,因此硬連結不能跨檔案系統。否則的話,無法區分該索引節點是位在哪一個檔案系統。軟連結 (soft link) 則是建立一個檔案,其中存放欲連結的檔名。

一個檔案系統必須透過掛載點 (mount point) 和目錄樹相連接才能被存取,掛載點必須是一個目錄,該目錄是該檔案系統的進入點。

建置系統

# 底下為書中代碼
# http://www.cs.nctu.edu.tw/~chenwj/source/linux-0.12.tar.gz
$ wget http://www.cs.nctu.edu.tw/~chenwj/source/linux-0.11-20110805.tar.gz
$ tar xvf linux-0.11-20110805.tar.gz
# 取得硬盤映像檔 hdc-0.11-new.img
$ wget http://oldlinux.org/Linux.old/bochs/linux-0.11-devel-060625.zip
$ unzip linux-0.11-devel-060625.zip
$ mkdir rootfs
$ cp linux-0.11-devel-060625/hdc-0.11-new.img rootfs
$ cd linux-0.11
$ make
# 修改 Makefile 加上 -vnc 0.0.0.0:1 使用 vnc 螢幕輸出; -monitor stdio 將 QEMU 監視器導至標準輸出。
$ make start
# 取得 CPU 暫存器內容
(qemu) info registers

BIOS

实际上 processor 的第 1 条指令最大的目的是刷新 cs 寄存器,开辟混沌。

-----------

ffff0:  ljmp   $0xf000,$0xe05b

-----------

fe05b:  cmpl   $0x0,%cs:-0x2ca0
fe062:  jne    0xfe3e3

  ... 略 ...

----------------
IN:
0x000f3623:  call   0xfc7bf

OP:
 ---- 0xf3623
 movi_i32 tmp0,$0xf3628
 mov_i32 tmp2,esp
 movi_i32 tmp12,$0xfffffffc
 add_i32 tmp2,tmp2,tmp12
 qemu_st32 tmp0,tmp2,$0x0
 mov_i32 esp,tmp2
 movi_i32 tmp4,$0xfc7bf
 st_i32 tmp4,env,$0x20
 exit_tb $0x0

以 x86 (qemu-system-i386) 為例,

  1. (hw/pc.c)
    void pc_memory_init(MemoryRegion *system_memory, ...)
    {
        /* BIOS load */
        if (bios_name == NULL)
            bios_name = BIOS_FILENAME;  // 預設為 bios.bin,其實是 seabios。可以從開機畫面看出。
        filename = qemu_find_file(QEMU_FILE_TYPE_BIOS, bios_name);
        if (filename) {
            bios_size = get_image_size(filename);
        } else {
            bios_size = -1;
        }
        if (bios_size <= 0 ||
            (bios_size % 65536) != 0) {
            goto bios_error;
        }
        bios = g_malloc(sizeof(*bios));
        memory_region_init_ram(bios, NULL, "pc.bios", bios_size);
        memory_region_set_readonly(bios, true);
        ret = rom_add_file_fixed(bios_name, (uint32_t)(-bios_size), -1);
    }
  • ${QEMU_SRC}/pc-bios
  • ${QEMU_SRC}/rom

因為 BIOS 本身混雜 16/32 bit 的指令,objdump -b binary -m i386 不能看出有用的東西。SeaBIOS 有源碼可以下載1)

processor 执行的第一条指针在 0xFFFFFFF0 处,这个地址经过 North Bridge(北桥)和 South ridge(南桥)芯片配合解码,最终会访问到固化的 ROM 块,同时,经过别名机制映射在地址空间低端,实际上等于 ROM 被映射到地址空间最高端和低端位置。

此时在系统的内存里其实并不存在 BIOS 代码,ROM BIOS 的一部分职责是负责安装 BIOS 代码进入系统内存。 典型是这条指令就是 0xFFFFFFF0 处的 ROM BIOS 指令,执行后它将跳到 0x000FE05B 处,这条指令的作用很大:

  • 更新 CS.base 使 processor 变成纯正的 real mode
  • 跳转到低端内存,使之进入 1M 低端区域

前面说过,此时内存中也不存在 BIOS,也就是说 IVT(中断向量表)也是不存在的,中断系统此时是不可用的,那么由 ROM BIOS 设置 IVT 。

bootsect.S

螢幕出現 Loading system …。這段程序位在硬盤第零道第零軌,BIOS 會將這段程序搬到內存物理位址 0x7c00 的地方。bootsect.S 先將自己搬到內存物理位址 0x90000,再將 setup.s 搬至內存物理位址 0x90200,最後把系統模塊搬至內存物理位址 0x10000

----------------
IN:
0x00007c00:  ljmp   $0x7c0,$0x5

OP:
 ---- 0x7c00
 movi_i32 tmp0,$0x7c0
 movi_i32 tmp1,$0x5
 movi_i32 tmp12,$0xffff
 and_i32 tmp0,tmp0,tmp12
 st_i32 tmp0,env,$0x50
 movi_i32 tmp12,$0x4
 shl_i32 tmp0,tmp0,tmp12
 st_i32 tmp0,env,$0x54
 mov_i32 tmp0,tmp1
 st_i32 tmp0,env,$0x20
 exit_tb $0x0

----------------
IN:
0x00007c05:  mov    $0x7c0,%ax
0x00007c08:  mov    %ax,%ds
0x00007c0a:  mov    $0x9000,%ax
0x00007c0d:  mov    %ax,%es
0x00007c0f:  mov    $0x100,%cx
0x00007c12:  sub    %si,%si
0x00007c14:  sub    %di,%di
0x00007c16:  rep movsw %ds:(%si),%es:(%di)
.global begtext, begdata, begbss, endtext, enddata, endbss, _start
 
.text
begtext:
.data
begdata:
.bss
begbss:
.text
BOOTSEG = 0x7c00
 
.code16
 
_start:
#  ljmp $BOOTSEG, $go
#go:
  mov %cs, %ax
  mov %ax, %ds
  mov %ax, %es
  mov $20, %cx
  mov $0x1004, %dx
  mov $0x000c, %bx
  mov $msg1, %bp
  mov $0x1301, %ax
  int $0x10
loop0:
  jmp loop0
msg1:
  .ascii "Loading system ..."
  .byte 13,10
.org 510
  .word 0xAA55
 
.text
endtext:
.data
enddata:
.bss
endbss:
$ as -a boot.S -o boot.o > boot.map
$ ld -r -Ttext 0x7c00 -e _start -s -o boot.out boot.o
$ objcopy -O binary -j .text boot.out disk.img
$ qemu-system-i386 -hda disk.img

setup.s

setup.s 把位於 0x10000 - 0x8ffff 的系統模塊 (head.s + main) 搬移至內存絕對位址 0 處,設置環境,設置臨時段描述符表 (GDT 和 IDT) 並開啟保護模式 (段機制),最後跳至內存絕對位址 0 處開始執行。這段程序位於內存物理位址 0x90200 的地方,緊接在 bootsect.S 之後。在搬移系統模塊之前,setup.s 會使用之前 BIOS 放在內存物理位址 0 的中斷向量獲取設備參數,並將這些資訊寫在內存物理位址 0x90000,覆寫掉 bootsect.S。

----------------
IN:
0x00090200:  ljmp   $0x9020,$0x5

OP:
 ---- 0x90200
 movi_i32 tmp0,$0x9020
 movi_i32 tmp1,$0x5
 movi_i32 tmp12,$0xffff
 and_i32 tmp0,tmp0,tmp12
 st_i32 tmp0,env,$0x50
 movi_i32 tmp12,$0x4
 shl_i32 tmp0,tmp0,tmp12
 st_i32 tmp0,env,$0x54
 mov_i32 tmp0,tmp1
 st_i32 tmp0,env,$0x20
 exit_tb $0x0

----------------
IN:
0x00090205:  mov    $0x9000,%ax
0x00090208:  mov    %ax,%ds
0x0009020a:  mov    $0x3,%ah
0x0009020c:  xor    %bh,%bh
0x0009020e:  int    $0x10

head.s

boot/setup.s → boot/head.s → main (init/main.c)。在 main 開頭行數下斷點,直接在 main 下斷點沒效果。

這段程序是從內存物理位址 0 處開始執行。這裡會開啟分頁機制,將頁目錄表放置在內存物理位址 0 處,亦即覆寫掉 head.s 自己。內核代碼位於線性/物理地址前 1MB (0xfffff)。

# -d in_asm (boot/head.s) 的片斷。
# 可以觀察 target-i386/translate.c 中的 gen_eob,gen_eob 代表一個基本塊的終點。
----------------
IN:
0x00000000:  mov    $0x10,%eax  // mov 0x10 至 eax
0x00000005:  mov    %eax,%ds

OP:
 ---- 0x0
 movi_i32 tmp0,$0x10  // mov 0x10 至 tmp0
 mov_i32 eax,tmp0     // mov tmp0 至 0x10

 ... 略 ...

----------------
IN:
0x00005495:  cld
0x00005496:  xor    %eax,%eax
0x00005498:  mov    %eax,%cr3  // 頁目錄表基址位於 0x0000

----------------
IN:
0x0000549b:  mov    %cr0,%eax  // 讀出 cr0 至 eax
0x0000549e:  or     $0x80000000,%eax
0x000054a3:  mov    %eax,%cr0  // 開啟 cr0 PG 位 (31)

OP:
 ---- 0x549b
 movi_i32 tmp4,$0x549b
 st_i32 tmp4,env,$0x20  // 將 0x549b 寫回 guest eip 
 movi_i32 tmp12,$0x0    // 告知 helper_read_crN 要讀取 cr0
 movi_i64 tmp13,$read_crN
 call tmp13,$0x0,$1,tmp0,tmp12  // 呼叫 helper_read_crN (target-i386/op_helper.c)
 mov_i32 eax,tmp0       // 將 helper_read_crN 返回值寫到 eax

----------------
IN:
0x000054a6:  ret    // 彈出之前押入 main 的位址,並跳轉至 main 開始執行

----------------

main

boot/head.s → main (init/main.c)。可以利用 QEMU Monitor 觀察機器各種狀態。

  1. main (init/main.c) 為任務 0,fork 出任務 1 執行 init。
    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
     */
     
      sched_init();  // 準備 task 0 運行環境
     
      sti();  // 初始化完畢,開啟中斷。
      move_to_user_mode();  // 將 task 0 從 kernel mode 切至 user mode
      // (gdb) set follow-fork-mode child 
      if (!fork()) {    /* we count on this going ok */
        init();  // child process (task 1)
      }
     
      for(;;) pause();  // parent process (task 0) 等待
     
    }
  2. init (init/main.c) 為第一個要執行的 shell 準備好環境,創建任務 2 載入該 shell 並執行。任務 2 執行完後退出,init 再創建其它任務。螢幕顯示 Free mem:
    void init(void)
    {
      int pid,i;
     
      setup((void *) &drive_info);
      (void) open("/dev/tty0",O_RDWR,0);
      (void) dup(0);
      (void) dup(0);
      printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
        NR_BUFFERS*BLOCK_SIZE);
      printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
      if (!(pid=fork())) {
        // task 2 讀取 /etc/rc 做為 /bin/sh 的輸入,並執行 /bin/sh。
        close(0);
        if (open("/etc/rc",O_RDONLY,0))
          _exit(1);
        execve("/bin/sh",argv_rc,envp_rc);
        _exit(2);  // 若執行 /bin/sh 失敗,退出。
      }
      // init (task 1) 執行以下程序。 
      if (pid>0)
        while (pid != wait(&i))
          /* nothing */;
      // init 創建的 task 2 已終止,再創建 task。 
      while (1) {
        if ((pid=fork())<0) {
          printf("Fork failed in init\r\n");
          continue;
        }
        // task 2, 3, ...
        if (!pid) {
          close(0);close(1);close(2);
          setsid();
          (void) open("/dev/tty0",O_RDWR,0);
          (void) dup(0);
          (void) dup(0);
          _exit(execve("/bin/sh",argv,envp));
        }
        while (1)
          if (pid == wait(&i))
            break;
        printf("\n\rchild %d died with code %04x\n\r",pid,i);
        sync();
      }
      _exit(0); /* NOTE! _exit, not exit() */
    }
    • 任務 0: idle (scheduler) task
    • 任務 1: init,其它 process 由它 fork 出來。
    • 任務 2: shell 執行 /etc/rc 之後退出。
  3. 登入後,下命令時,shell 會再創建出另一個任務執行該命令。我們來看 fork。
    // nr 是新進程的任務號
    int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, ...)
    {
      struct task_struct *p;  // 每個進程都有一個資料結構。
      int i;
      struct file *f;
     
      p = (struct task_struct *) get_free_page();
      if (!p)
        return -EAGAIN;
      task[nr] = p;
     
      /* 略 */
     
      // task_struct 中的 tss_struct (Task State Segment,include/linux/sched.h) 用來做 context switch 時保留狀態之用。  
      p->tss.back_link = 0;
      p->tss.esp0 = PAGE_SIZE + (long) p;  // 內核棧頂
      p->tss.ss0 = 0x10;                   // 內核棧段
      p->tss.eip = eip;
      p->tss.eflags = eflags;
      p->tss.eax = 0;
     
    }

Context Switch

/*
 *  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" \
  "xchgl %%ecx,current\n\t" \
  "ljmp *%0\n\t" \                        // 切換至新任務 (進程)
  "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])); \
}

x86 支援透過 call/jmp TSS descriptor 做硬體 context switch。可以參考 QEMU 的代碼。

  1. disas_insn (target-i386/translate.c)
            case 5: /* ljmp Ev */
                gen_op_ld_T1_A0(ot + s->mem_index);
                gen_add_A0_im(s, 1 << (ot - OT_WORD + 1));
                gen_op_ldu_T0_A0(OT_WORD + s->mem_index);
            do_ljmp:
                if (s->pe && !s->vm86) {
                    if (s->cc_op != CC_OP_DYNAMIC)
                        gen_op_set_cc_op(s->cc_op);
                    gen_jmp_im(pc_start - s->cs_base);
                    tcg_gen_trunc_tl_i32(cpu_tmp2_i32, cpu_T[0]);
                    gen_helper_ljmp_protected(cpu_tmp2_i32, cpu_T[1],
                                              tcg_const_i32(s->pc - pc_start));
                } else {
                    gen_op_movl_seg_T0_vm(R_CS);
                    gen_op_movl_T0_T1();
                    gen_op_jmp_T0();
                }
                gen_eob(s);
                break;
  2. helper_ljmp_protected (target-i386/op_helper.c)
    /* protected mode jump */
    void helper_ljmp_protected(int new_cs, target_ulong new_eip,
                               int next_eip_addend)
    {
        } else {
            /* jump to call or task gate */
            dpl = (e2 >> DESC_DPL_SHIFT) & 3;
            rpl = new_cs & 3;
            cpl = env->hflags & HF_CPL_MASK;
            type = (e2 >> DESC_TYPE_SHIFT) & 0xf;
            switch(type) {
            case 1: /* 286 TSS */
            case 9: /* 386 TSS */
            case 5: /* task gate */
                if (dpl < cpl || dpl < rpl)
                    raise_exception_err(EXCP0D_GPF, new_cs & 0xfffc);
                next_eip = env->eip + next_eip_addend;
                switch_tss(new_cs, e1, e2, SWITCH_TSS_JMP, next_eip);
                CC_OP = CC_OP_EFLAGS;
                break;
    }
  3. switch_tss (target-i386/op_helper.c)
    /* XXX: restore CPU state in registers (PowerPC case) */
    static void switch_tss(int tss_selector,
                           uint32_t e1, uint32_t e2, int source,
                           uint32_t next_eip)
    {
        // 這裡可以看到我們載入新進程的 cr3 指向其頁目錄表,同時伴隨沖掉 tlb。
        if ((type & 8) && (env->cr[0] & CR0_PG_MASK)) {
            cpu_x86_update_cr3(env, new_cr3);
        }
    }

現代的作業系統大多改用軟體實現 context switch。

Process

  1. .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
  2. sys_fork (kernel/system_call.s)
    .align 2
    sys_fork:
      call find_empty_process
      testl %eax,%eax
      js 1f              ; 如果 find_empty_process 返回負值,跳至 1: ret 返回。
      push %gs
      pushl %esi
      pushl %edi
      pushl %ebp
      pushl %eax
      call copy_process  ; copy_process 的參數不只這裡看到的,還包括之前 sys_call 和其它過程壓入內核棧的參數。
      addl $20,%esp
    1:  ret
  3. copy_process (kernel/fork.c)
    /*
     *  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.
     */
    //
    // 參數由右至左依序是:
    //
    // CPU 執行中斷指令壓入的用戶棧地址 ss 和 esp、eflags 和返回用戶態的地址 cs 和 eip。
    // 執行 _system_call (kernel/system_call.s) 時入棧的段寄存器 ds、es、fs 和 edx、ecx、ebx。
    // 執行 sys_call_table (include/linux/sys.h) 的返回值。
    // 最後才是呼叫 copy_process 之前壓入的參數。
    //
    // Breakpoint 1, copy_process (nr=4, ebp=67104960, edi=362209, esi=76483, gs=23, none=30459, ebx=448012, ecx=76462, edx=362304, fs=23, es=23, ds=23,
    //    eip=235695, cs=15, eflags=582, esp=67104948, ss=23) at fork.c:74
    //
    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)
    {
      // 先取得一個頁面用來放置新進程任務數據結構 (task_struct)。 
      p = (struct task_struct *) get_free_page();
     
      // 複製父進程的任務數據結構。
      *p = *current;
     
      // 更新新進程的任務數據結構。
      p->state = TASK_UNINTERRUPTIBLE;
      p->pid = last_pid;
      p->father = current->pid;
      p->tss.eax = 0;  // fork 之後,新進程的返回值為 0。
      /* 略 */
     
      // 複製父進程的頁表至新進程。此時,新進程 (nr) 和其父進程共享物理頁面,並沒有為新進程分配新的物理頁面。
      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++;
      // 在 GDT 設置新任務的 TSS 段和 LDT 段描述符項。
      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-on-write
    • 當在命令行按下 enter 時,即使沒有下命令也會 fork。以 QEMU 為例可以看出,
      #0  tlb_flush (env=0x13b40b0, flush_global=0) at /tmp/chenwj/qemu-1.0/exec.c:1990
      #1  0x00000000005ef6e1 in cpu_x86_update_cr3 (env=0x13b40b0, new_cr3=0) at /tmp/chenwj/qemu-1.0/target-i386/helper.c:523
      #2  0x000000000061314d in switch_tss (tss_selector=64, e1=3001548904, e2=35327, source=0, next_eip=28249)
          at /tmp/chenwj/qemu-1.0/target-i386/op_helper.c:538
      #3  0x0000000000616afb in helper_ljmp_protected (new_cs=64, new_eip=21992, next_eip_addend=4) at /tmp/chenwj/qemu-1.0/target-i386/op_helper.c:2401
    • ----------------
      IN:
      0x00008416:  push   %ebp
      0x00008417:  push   %edi
      0x00008418:  push   %esi
      0x00008419:  push   %ebx
      0x0000841a:  sub    $0x5c,%esp
      0x0000841d:  cld
      0x0000841e:  call   0xa5af      // 呼叫 get_free_page
    • ls 會觸發 tb_phys_invalidate。懷疑是把 ls 的映像載入物理內存時所觸發。
      Breakpoint 1, tb_phys_invalidate (tb=0x7fffe7378d30, page_addr=18446744073709551615) at /tmp/chenwj/qemu-1.0/exec.c:869
      869     {
      (gdb) bt
      #0  tb_phys_invalidate (tb=0x7fffe7378d30, page_addr=18446744073709551615) at /tmp/chenwj/qemu-1.0/exec.c:869
      #1  0x00000000005cc5c2 in tb_invalidate_phys_page_range (start=16551344, end=16551348, is_cpu_write_access=1) at /tmp/chenwj/qemu-1.0/exec.c:1097
      #2  0x00000000005cc782 in tb_invalidate_phys_page_fast (start=16551344, len=4) at /tmp/chenwj/qemu-1.0/exec.c:1150
      #3  0x00000000005d0a00 in notdirty_mem_writel (opaque=0x0, ram_addr=16551344, val=0) at /tmp/chenwj/qemu-1.0/exec.c:3375
      #4  0x000000000061f441 in io_writel (physaddr=16551344, val=0, addr=16551344, retaddr=0x400acfa1) at /tmp/chenwj/qemu-1.0/softmmu_template.h:218
      #5  0x000000000061f531 in __stl_mmu (addr=16551344, val=0, mmu_idx=0) at /tmp/chenwj/qemu-1.0/softmmu_template.h:250
      #6  0x00000000400acfa2 in ?? ()
      #7  0x00007fffe6a2c940 in ?? ()
      #8  0x0000000000000000 in ?? ()
  4. copy_mem (kernel/fork.c)
    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);
      data_limit=get_limit(0x17);
      old_code_base = get_base(current->ldt[1]);
      old_data_base = get_base(current->ldt[2]);
      // Linux 要求代碼/數據段基址一致。
      if (old_data_base != old_code_base)
        panic("We don't support separate I&D");
      if (data_limit < code_limit)
        panic("Bad data_limit");
      // 新進程在線性地址中的位址為 64MB * 任務號。
      new_data_base = new_code_base = nr * 0x4000000;
      p->start_code = new_code_base;
      // 設置新進程的 LDT。
      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;
    }
  5. copy_page_tables (mm/memory.c)
    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++) {
          this_page = *from_page_table;
          if (!(1 & this_page))
            continue;
          this_page &= ~2;  // 將頁設為只讀,為 COW 之用。
          *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;
    }
  1. 系統呼叫 sys_execve (kernel/system_call.s)。
    .align 2
    sys_execve:
      lea EIP(%esp),%eax  ; 將用戶程序 eip 放至 eax
      pushl %eax
      call do_execve
      addl $4,%esp        ; 回復棧指針
      ret
  2. do_execve (fs/exec.c) 加載可執行文件或是 shell 腳本。
    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;
      // 取得存放執行文件的 i 節點。
      if (!(inode=namei(filename)))   /* get executables inode */
        return -ENOENT;
      argc = count(argv);
      envc = count(envp);
     
      /* 檢查權限 */
     
      /* 解析文件 */
     
    /* OK, This is the point of no return */
      // current 即為當前進程。
      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;
      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 數組換成執行文件入口,並調整其棧指針。
      eip[0] = ex.a_entry;    /* eip, magic happens :-) */
      eip[3] = p;     /* stack pointer */
      return 0;
    exec_error2:
      iput(inode);
    exec_error1:
      for (i=0 ; i<MAX_ARG_PAGES ; i++)
        free_page(page[i]);
      return(retval);
    }
  3. create_tables (fs/exec.c) 準備進程所需的命令行參數和環境變量。
    static unsigned long * create_tables(char * p,int argc,int envc)
    {
      unsigned long *argv,*envp;
      unsigned long * sp;
     
      // p 指向命令行參數和環境變數可用空間。
      sp = (unsigned long *) (0xfffffffc & (unsigned long) p);
      sp -= envc+1;
      envp = sp;
      sp -= argc+1;
      argv = sp;
      put_fs_long((unsigned long)envp,--sp);
      put_fs_long((unsigned long)argv,--sp);
      put_fs_long((unsigned long)argc,--sp);
      while (argc-->0) {
        put_fs_long((unsigned long) p,argv++);
        while (get_fs_byte(p++)) /* nothing */ ;
      }
      put_fs_long(0,argv);
      while (envc-->0) {
        put_fs_long((unsigned long) p,envp++);
        while (get_fs_byte(p++)) /* nothing */ ;
      }
      put_fs_long(0,envp);
      return sp;
    }
  1. 進程結束時,會呼叫系統呼叫 sys_exit (kernel/exit.c)。
    int sys_exit(int error_code)
    {
      return do_exit((error_code&0xff)<<8);
    }
  2. int do_exit(long code)
    {
    }

Memory

  1. trap_init (kernel/traps.c) 設定 page fault 的處理常式。
    void trap_init(void)
    {
      int i;
     
      set_trap_gate(0,&divide_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);          // 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,&parallel_interrupt);
    }
  2. page_fault (mm/page.s) 根據情況呼叫頁缺失或頁寫保護異常處理程序。
    .globl page_fault
     
    page_fault:
      xchgl %eax,(%esp)  ; CPU 會產生並將 error code 入棧。將 error code 取出至 eax。error code 後三位為 U/S、W/R 和 P。
      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     ; 取出觸發 page fault 的線性地址至 edx
      pushl %edx
      pushl %eax
      testl $1,%eax      ; 測試頁存在標誌。若該頁不在內存,調用 do_no_page; 否則調用 do_wp_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
  3. get_free_page (mm/memory.c) 取得可用的物理頁,再交由 put_page 設置頁表。
    unsigned long get_free_page(void)
    {
    register unsigned long __res asm("ax");
     
    __asm__("std ; repne ; scasb\n\t"
      "jne 1f\n\t"
      "movb $1,1(%%edi)\n\t"
      "sall $12,%%ecx\n\t"
      "addl %2,%%ecx\n\t"
      "movl %%ecx,%%edx\n\t"
      "movl $1024,%%ecx\n\t"
      "leal 4092(%%edx),%%edi\n\t"
      "rep ; stosl\n\t"
      " movl %%edx,%%eax\n"
      "1: cld"
      :"=a" (__res)
      :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
      "D" (mem_map+PAGING_PAGES-1)
      );
    return __res;
    }
    • mem_map (mm/memory.c) 用來維護目前物理內存分配的情況,以頁為單位。mem_map[i] 代表第 i 頁使用狀況,0 代表未使用。
    • gdb tools/system 下斷點可以得到 get_free_page 的地址。
      ----------------
      IN:
      0x0000a5af:  push   %edi
      0x0000a5b0:  push   %ebx
      0x0000a5b1:  mov    $0x0,%eax
      0x0000a5b6:  mov    $0xf00,%edx
      0x0000a5bb:  mov    $0x1f55f,%ebx
      0x0000a5c0:  mov    %edx,%ecx
      0x0000a5c2:  mov    %ebx,%edi
      0x0000a5c4:  std
      0x0000a5c5:  repnz scas %es:(%edi),%al
      
      ----------------
      IN:
      0x0000a5c7:  jne    0xa5e7
      
      ----------------
      IN:
      0x0000a5c9:  movb   $0x1,0x1(%edi)
      0x0000a5cd:  shl    $0xc,%ecx
      0x0000a5d0:  add    $0x100000,%ecx
      0x0000a5d6:  mov    %ecx,%edx
      0x0000a5d8:  mov    $0x400,%ecx
      0x0000a5dd:  lea    0xffc(%edx),%edi
      0x0000a5e3:  rep stos %eax,%es:(%edi)
  4. put_page 設置頁表。
    unsigned long put_page(unsigned long page,unsigned long address)
    {
      unsigned long tmp, *page_table;
     
    /* NOTE !!! This uses the fact that _pg_dir=0 */
     
      if (page < LOW_MEM || page >= HIGH_MEMORY)
        printk("Trying to put page %p at %p\n",page,address);
      if (mem_map[(page-LOW_MEM)>>12] != 1)
        printk("mem_map disagrees with %p at %p\n",page,address);
      page_table = (unsigned long *) ((address>>20) & 0xffc);
      if ((*page_table)&1)
        page_table = (unsigned long *) (0xfffff000 & *page_table);
      else {
        if (!(tmp=get_free_page()))
          return 0;
        *page_table = tmp|7;
        page_table = (unsigned long *) tmp;
      }
      page_table[(address>>12) & 0x3ff] = page | 7;
    /* no need for invalidate */
      return page;
    }

Swap

  1. swap_bitmap (mm/swap.c) 用來管理 swap 空間,swap 空間通常位於硬盤。
    static char * swap_bitmap = NULL;
     
    void init_swapping(void)
    {
      if (swap_size > SWAP_BITS)
        swap_size = SWAP_BITS;
      // 申請一頁內存存放 swap_bitmap。其中每一個 bit 代表一個頁面。
      swap_bitmap = (char *) get_free_page();
      if (!swap_bitmap) {
        printk("Unable to start swapping: out of memory :-)\n\r");
        return;
      }
      // 把 swap 空間上的第一個頁面讀進 swap_bitmap。
      read_swap_page(0,swap_bitmap);
      // swap 空間上的第一個頁面尾部有字串 SWAP-SPACE。若無,代表沒有 swap 空間,
      // 釋放 swap_bitmap。
      if (strncmp("SWAP-SPACE",swap_bitmap+4086,10)) {
        printk("Unable to find swap-space signature\n\r");
        free_page((long) swap_bitmap);
        swap_bitmap = NULL;
        return;
      }
    }
    • void swap_in(unsigned long *table_ptr)
      {
        int swap_nr;
        unsigned long page;
       
        if (!swap_bitmap) {
          printk("Trying to swap in without swap bit-map");
          return;
        }
        if (1 & *table_ptr) {
          printk("trying to swap in present page\n\r");
          return;
        }
        swap_nr = *table_ptr >> 1;
        if (!swap_nr) {
          printk("No swap page in swap_in\n\r");
          return;
        }
        // 從內存中申請一個物理頁。
        if (!(page = get_free_page()))
          oom();
        // 將該頁由 swap 空間讀入該物理頁。
        read_swap_page(swap_nr, (char *) page);
        // 設置 swap_bitmap。
        if (setbit(swap_bitmap,swap_nr))
          printk("swapping in multiply from same page\n\r");
        // 設置頁表項。
        *table_ptr = page | (PAGE_DIRTY | 7);
      }
    • include/linux/mm.h:#define read_swap_page(nr,buffer) ll_rw_page(READ,SWAP_DEV,(nr),(buffer));
  2. swap_out (mm/swap.c)。當內存沒有空間時,會從中挑選一個頁,將其內容寫至 swap 空間。
    // 試圖從內存中取得一個空閒頁面。
    unsigned long get_free_page(void)
    {
    register unsigned long __res asm("ax");
     
    repeat:
      __asm__("std ; repne ; scasb\n\t"
        "jne 1f\n\t"
        "movb $1,1(%%edi)\n\t"
        "sall $12,%%ecx\n\t"
        "addl %2,%%ecx\n\t"
        "movl %%ecx,%%edx\n\t"
        "movl $1024,%%ecx\n\t"
        "leal 4092(%%edx),%%edi\n\t"
        "rep ; stosl\n\t"
        "movl %%edx,%%eax\n"
        "1:"
        :"=a" (__res)
        :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
        "D" (mem_map+PAGING_PAGES-1)
        :"di","cx","dx");
      if (__res >= HIGH_MEMORY)
        goto repeat;
      // 沒有空閒頁面,進行 swap out。
      if (!__res && swap_out())
        goto repeat;
      return __res;
    }
  3. swap_in (mm/swap.c)。當發生缺頁 (page fault) 時,do_no_page (mm/memory.c) 會先查找 swap 空間,看該頁是否存在; 若否,則從硬盤讀入該頁。
    void do_no_page(unsigned long error_code,unsigned long address)
    {
      page = *(unsigned long *) ((address >> 20) & 0xffc);
      if (page & 1) {
        page &= 0xfffff000;
        page += (address >> 10) & 0xffc;
        tmp = *(unsigned long *) page;
        if (tmp && !(1 & tmp)) {
          swap_in((unsigned long *) page);
          return;
        }
      }
    }

File System

內核會為塊設備在內存開闢一塊高速緩衝區,藉此加快存取速度。

  1. 任務 1 執行 init。
    static inline _syscall1(int,setup,void *,BIOS)
     
    void init(void)
    {
      int pid,i;
     
      // 呼叫 sys_setup
      setup((void *) &drive_info);
     
      /* 略 */
     
    }
  2. sys_setup (kernel/blk_drv/hd.c)
    int sys_setup(void * BIOS)
    {
      /* 讀取硬盤分區表 */
     
      mount_root();  // 掛起根文件系統。
      return (0);
    }
  3. mount_root (fs/super.c) 掛起根文件系統。
    void mount_root(void)
    {
      int i,free;
      struct super_block * p;
      struct m_inode * mi;
     
      if (32 != sizeof (struct d_inode))
        panic("bad i-node size");
      for(i=0;i<NR_FILE;i++)
        file_table[i].f_count=0;
      if (MAJOR(ROOT_DEV) == 2) {
        printk("Insert root floppy and press ENTER");
        wait_for_keypress();
      }
      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)))
        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);
    }

System Call

  • sys_read/sys_write 根據文件屬性呼叫相對應的函式讀取資料。
    int sys_read(unsigned int fd,char * buf,int count)
    {
      struct file * file;
      struct m_inode * inode;
     
      if (fd>=NR_OPEN || count<0 || !(file=current->filp[fd]))
        return -EINVAL;
      if (!count)
        return 0;
      verify_area(buf,count);
      inode = file->f_inode;
      if (inode->i_pipe)
        return (file->f_mode&1)?read_pipe(inode,buf,count):-EIO;
      if (S_ISCHR(inode->i_mode))
        return rw_char(READ,inode->i_zone[0],buf,count,&file->f_pos);
      if (S_ISBLK(inode->i_mode))
        return block_read(inode->i_zone[0],&file->f_pos,buf,count);
      if (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode)) {
        if (count+file->f_pos > inode->i_size)
          count = inode->i_size - file->f_pos;
        if (count<=0)
          return 0;
        return file_read(inode,file,buf,count);
      }
      // 欲讀取檔案並非管道、字設備、塊設備或是一般目錄/檔案,無法判斷其屬性。
      printk("(Read)inode->i_mode=%06o\n\r",inode->i_mode);
      return -EINVAL;
    }

其它

  1. printk (kernel/printk.c)
    int printk(const char *fmt, ...)
    {
      va_list args;
      int i;
     
      va_start(args, fmt);
      i=vsprintf(buf,fmt,args);
      va_end(args);
      __asm__("push %%fs\n\t"
        "push %%ds\n\t"
        "pop %%fs\n\t"
        "pushl %0\n\t"
        "pushl $buf\n\t"
        "pushl $0\n\t"
        "call tty_write\n\t"
        "addl $8,%%esp\n\t"
        "popl %0\n\t"
        "pop %%fs"
        ::"r" (i):"ax","cx","dx");
      return i;
    }
  1. 用戶態通過系統呼叫執行內核代碼時,內核代碼會先把 GDT 中的內核數據段描述符載入 ds 和 es,之後透過 ds 和 es 訪問內核數據段; 在 fs 加載 LDT 中的當前任務的數據段描述符,之後透過 fs 訪問用戶數據段。在執行內核代碼時,如果要存取用戶程序 (任務) 中的數據就需要透過以下函式。(include/asm/segment.h)
    // 將 val 寫入 fs:addr,即用戶數據段。
    static inline void put_fs_byte(char val,char *addr)
    {
      // val 作為輸入 %0,*addr 作為輸入 %1,%%fs 即是 %fs。
      __asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr));
    }
    ----------------
    IN:
    0x0000e4c0:  sub    $0x4,%esp
    0x0000e4c3:  mov    0x8(%esp),%eax  // eax = val
    0x0000e4c7:  mov    %al,(%esp)
    0x0000e4ca:  movzbl (%esp),%eax     // 取 eax 中的 al (byte) 至 eax
    0x0000e4ce:  mov    0xc(%esp),%edx  // edx = addr
    0x0000e4d2:  mov    %al,%fs:(%edx)  // 對應上面的 inline assembly
    0x0000e4d5:  add    $0x4,%esp
    0x0000e4d8:  ret
  2. sys_uname (kernel/sys.c) 實現 sys_uname 系統呼叫。
    int sys_uname(struct utsname * name)
    {
      static struct utsname thisname = {
        "linux .0","nodename","release ","version ","machine "
      };
      int i;
     
      if (!name) return -ERROR;
      verify_area(name,sizeof *name);
      // thisname 位於內核空間,將 thisname 的內容寫至用戶空間。
      for(i=0;i<sizeof *name;i++)
        put_fs_byte(((char *) &thisname)[i],i+(char *) name);
      return 0;
    }
  3. 又或者需要從高速緩衝搬移資料到用戶空間。block_read (fs/block_dev.c)。
    int block_read(int dev, unsigned long * pos, char * buf, int count)
    {
      int block = *pos >> BLOCK_SIZE_BITS;
      int offset = *pos & (BLOCK_SIZE-1);
      int chars;
      int read = 0;
      struct buffer_head * bh;
      register char * p;
     
      while (count>0) {
        chars = BLOCK_SIZE-offset;
        if (chars > count)
          chars = count;
        if (!(bh = breada(dev,block,block+1,block+2,-1)))
          return read?read:-EIO;
        block++;
        p = offset + bh->b_data;
        offset = 0;
        *pos += chars;
        read += chars;
        count -= chars;
        while (chars-->0)
          put_fs_byte(*(p++),buf++);  // 將資料讀至用戶緩衝區 (buf)。
        brelse(bh);
      }
      return read;
    }
  4. 塊 (block) 設備。
  5. IO 端口 (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; \
    })
     
    #define outb_p(value,port) \
    __asm__ ("outb %%al,%%dx\n" \
        "\tjmp 1f\n" \
        "1:\tjmp 1f\n" \
        "1:"::"a" (value),"d" (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; \
    })

常用指令的代碼通常位於 Coreutils

除錯

$ cd linux-0.11
# 開啟 gdbserver
$ make debug
$ gdb tools/system
# 在引導啟動程序設斷點,此時螢幕應會出現 "Boot from Floppy..."
(gdb) break *0x7c00
# 下 contine 之後,QEMU 才會有動作,QEMU Monitor 也是如此。
(gdb) c

移植 linux-0.12

  1. 用 AT&T 語法改寫 boot/bootsect.S。
    • ! 換成 #
      :%s/!/#/g
    • 修改 mov 賦值順序 (左到右)。立即數前面改用 $,寄存器前面改用 %
    • 不能用 #include
      $ as --32 -o bootsect.o bootsect.S
      $ ld -m elf_i386 -Ttext 0 -o bootsect bootsect.o
      $ objcopy -R .pdr -R .comment -R.note -S -O binary bootsect
      $ qemu-system-i386 -boot a -fda bootsect -hda hdc-0.11-new.img -vnc 0.0.0.0:1
  2. 改寫 boot/setup.s。
  3. 改寫 tools/build.c。
  4. 改寫 Makefile,新增 Makefile.header。
  5. fs/ 新增 select.c,mm/ 新增 swap.c。
$ git config --global user.name "Chen Wei-Ren"
$ git config --global user.email chenwj@iis.sinica.edu.tw
$ mkdir linux-0.12
$ cd linux-0.12
$ git init
$ touch README
$ git add README
$ git commit -m 'first commit'
$ git remote add origin git@github.com:azru0512/linux-0.12.git
$ git push -u origin master
$ git clone https://azru0512@github.com/azru0512/linux-0.12.git

as86/ld86 是 Minix 使用的工具。

We shoudn’t list the input and output registers in this list. Because, gcc knows that "asm" uses them (because they are specified explicitly as constraints). If the instructions use any other registers, implicitly or explicitly (and the registers are not present either in input or in the output constraint list), then those registers have to be specified in the clobbered list. 5.3 Clobber List http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#ss5.3

You may not write a clobber description in a way that overlaps with an input or output operand. 6.41 Assembler Instructions with C Expression Operands http://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html

  • 在編譯 kernel/blk_drv/ramdisk.c,出現以下訊息。
    ramdisk.c:36:10: error: can't find a register in class 'CREG' while reloading 'asm'

    include/asm/memory.h 中另一份 memcpy 註解掉之後,錯誤消失。

  • 在編譯 include/string.h,出現以下訊息。
    ../include/string.h:280:1: error: can't find a register in class 'SIREG' while reloading 'asm'
    ../include/string.h:280:1: error: 'asm' operand has impossible constraints

    將用 register 關鍵字所定義的變數之後的 asm 去掉,不指定要存在哪個暫存器。

This error message occurs because a cast does not produce an lvalue (value that can be used on the left side of an assignment). Expression must be a modifiable lvalue error http://supp.iar.com/Support/?note=82120

  • 編譯 kernel/blk_drv/blk.h 會出現以下訊息。
    error: #elif with no expression

    自 GCC 4.4 版以後,必定會計算 #elif 後的表達式,若 #elif 後無表達式,便會出現此錯誤。

When using the preprocessor statement #elif, the argument is now evaluated even if earlier #if or #elif conditionals evaluated non-zero. Preprocessor conditionals always evaluated http://gcc.gnu.org/gcc-4.4/porting_to.html

Bochs

$ wget http://sourceforge.net/projects/bochs/files/bochs/2.4.6/bochs-2.4.6.tar.gz/download
$ ../bochs-2.4.6/configure --prefix=$INSTALL --with-nogui
$ make install
# 下載開機映像檔
$ wget wget http://bochs.sourceforge.net/guestos/linux-img.tar.gz
$ tar xvf linux-img.tar.gz; cd linux-img
$ bochs

SkyEye

$ export PATH=/PATH/TO/LLVM_2_8:$PATH
$ wget http://sourceforge.net/projects/skyeye/files/skyeye/skyeye-1.3.4/skyeye-1.3.4_rc1.tar.gz/download
$ tar xvf skyeye-1.3.4_rc1.tar.gz; cd skyeye
$ ./configure --enable-dyncom=no --enable-targets=arm
$ make lib; make
$ make install_lib; make install
$ skyeye -c skyeye.conf -e leeos.bin

Chapter 2

  1. 安裝 SkyEye。
    $ wget http://sourceforge.net/projects/skyeye/files/skyeye/skyeye-1.2.6_rc1/skyeye-1.2.6_rc1.tar.bz2/download
    # device/nandflash/nandflash_smallblock.c:519 open 需要加上第三個參數。
    $ ./configure --prefix=$INSTALL
    $ make; make install
    --- skyeye-1.2.6_rc1/device/nandflash/nandflash_smallblock.c    2007-10-14 16:19:40.000000000 +0800
    +++ skyeye-1.2.6_rc1_new/device/nandflash/nandflash_smallblock.c        2012-02-10 20:24:07.610761142 +0800
    @@ -516,7 +516,7 @@
     #endif
            nf->writebuffer=(u8*)malloc(dev->pagedumpsize);
            //nf->memsize=528*32*4096;
    -       if ((nf->fdump= open(dev->dump, FILE_FLAG)) < 0)
    +       if ((nf->fdump= open(dev->dump, FILE_FLAG, S_IRWXU)) < 0)
            {
                    free(nf);
                    printf("error open nandflash dump!\n");
    
  2. 設定檔和範例程式。
    # skyeye.conf
    cpu: arm920t
    mach: s3c2410x
    mem_bank: map=M, type=RW, addr=0x00000000, size=0x00800000, file=./leeos.bin, boot=yes
    mem_bank: map=I, type=RW, addr=0x48000000, size=0x20000000
    #define UFCON0 ((volatile unsigned int *)(0x50000020))
     
    void helloworld(void) {
      const char *p = "hello world!\n";
     
      while (*p) {
        *UFCON0 = *p++;
      };
     
      while (1)
        ;
    }
  3. 編譯並執行。
    $ arm-none-linux-gnueabi-gcc -nostdlib -O2 -c helloworld.c
    # arm-none-linux-gnueabi-ld -e helloworld -Ttext 0x0 helloworld.o -o helloworld
    $ arm-none-linux-gnueabi-ld -T helloworld.lds helloworld.o -o helloworld
    $ arm-none-linux-gnueabi-objcopy -O binary helloworld leeos.bin
    $ skyeye
    ENTRY(helloworld)
    SECTIONS
    {
        . = 0x00000000;
        .text :
        {
            *(.text)
        }
        . = ALIGN(32);
        .data :
        {
            *(.data)
        }
        . = ALIGN(32);
        .bss :
        {
            *(.bss)
        }
    }
  4. 組語。
    .arch armv4
    .global helloworld
     
    .equ REG_FIFO, 0X50000020
     
    .text
    .align 2
     
    helloworld:
      ldr r1,=REG_FIFO  ; ldr 將一常量載入暫存器。
      adr r0,.L0        ; adr 將一地址載入暫存器。
     
    .L2:
      ldrb r2,[r0],#0x1
      str r2,[r1]       ; 將字串寫到串型端口。
      cmp r2,#0x0
      bne .L2
     
    .L1:
      b .L1             ; 無窮迴圈。
     
    .align 2
    .L0:
      .ascii "hello world!\n\0"
  5. 編譯並執行。
    $ arm-none-linux-gnueabi-as helloworld.s -o helloworld.o
    $ arm-none-linux-gnueabi-ld -T helloworld.lds helloworld.o -o helloworld
    $ arm-none-linux-gnueabi-objcopy -O binary helloworld leeos.bin
    $ skyeye
  6. 組語和 C 混合編程。
    .arch armv4
    .global _start
     
    .equ REG_FIFO, 0X50000020
     
    .text
    .align 2
     
    _start:
      ; r0 - r3 用來傳遞參數
      ldr r0,=REG_FIFO  ; addr
      adr r1,.L0        ; p
      bl helloworld
     
    .L1:
      b .L1  ; 無窮迴圈
     
    .align 2
    .L0:
      .ascii "hello world!\n\0"
    int helloworld(unsigned int *addr, const char *p) {
     
      while (*p) {
        *addr = *p++;
      };
     
      return 0;
    }
  7. 編譯並執行。
    $ arm-none-linux-gnueabi-gcc -nostdlib -O2 helloworld.s -c -o start.o
    $ arm-none-linux-gnueabi-gcc -nostdlib -O2 helloworld.c -c -o helloworld.o
    $ arm-none-linux-gnueabi-ld -e _start -Ttext 0x0 start.o helloworld.o -o helloworld
    $ arm-none-linux-gnueabi-objcopy -O binary helloworld leeos.bin
    $ skyeye

Chapter 3

  1. Makefile 和鏈結腳本。
    CC=arm-elf-gcc
    LD=arm-elf-ld
    OBJCOPY=arm-elf-objcopy
     
    CFLAGS= -O2 -nostdlib -g
    ASFLAGS= -O2 -g
    LDFLAGS=-Tleeos.lds -Ttext 30000000
     
    OBJS=init.o start.o boot.o abnormal.o
     
    .c.o:
      $(CC) $(CFLAGS) -c $<
    .s.o:
      $(CC) $(ASFLAGS) -c $<
     
    leeos:$(OBJS)
      $(CC) -static -nostartfiles -nostdlib $(LDFLAGS) $? -o $@ -lgcc
      $(OBJCOPY) -O binary $@ leeos.bin
     
    clean:
      rm *.o leeos leeos.bin -f
    OUTPUT_ARCH(arm)
    ENTRY(_start)
    
    SECTIONS
    {
      . = 0x00000000;
      .text :
      {
        *(.startup)
        *(.text)
      }
      . = ALIGN(32);
    
      .data :
      {
        *(.data)
      }
    
      . = ALIGN(32);
      __bss_start__ = .;
      .bss :
      {
        *(.bss)
      }
      __bss_end__ = .;
    }
  2. start.s 跳轉至 _vector_reset。
    .section .startup ; 以下代碼屬於 .startup 段。
    .code 32          ; 產生 32 位指令集的代碼,和 .arm 相同作用。
    .align 0
     
    .global _start    ; 將符號 _start export,使其外部可見。
    .extern __vector_reset
    .extern __vector_undefined
    .extern __vector_swi
    .extern __vector_prefetch_abort
    .extern __vector_data_abort
    .extern __vector_reserved
    .extern __vector_irq
    .extern __vector_fiq
     
    _start:
     
      ldr pc,_vector_reset  ; 跳轉至中斷向量 _vector_reset。此條指令以下就是所謂的中斷向量表,底下其它指令只有在發生異常時會執行。  
      ldr pc,_vector_undefined
      ldr pc,_vector_swi
      ldr pc,_vector_prefetch_abort
      ldr pc,_vector_data_abort
      ldr pc,_vector_reserved
      ldr pc,_vector_irq
      ldr pc,_vector_fiq
     
    .align 4
     
    _vector_reset:  .word __vector_reset
    _vector_undefined:  .word __vector_undefined
    _vector_swi:  .word __vector_swi
    _vector_prefetch_abort: .word __vector_prefetch_abort
    _vector_data_abort: .word __vector_data_abort
    _vector_reserved: .word __vector_reserved
    _vector_irq:  .word __vector_irq
    _vector_fiq:  .word __vector_fiq
  3. init.s 跳轉至 plat_boot。
    .equ DISABLE_IRQ,   0x80
    .equ DISABLE_FIQ,   0x40
    .equ SYS_MOD,     0x1f
    .equ IRQ_MOD,     0x12
    .equ FIQ_MOD,     0x11
    .equ SVC_MOD,     0x13
    .equ ABT_MOD,     0x17
    .equ UND_MOD,     0x1b
     
    .equ MEM_SIZE,      0x800000               ; 假設內存大小。
    .equ TEXT_BASE,     0x30000000             ; SDRAM 起始位址。
     
    .equ _SVC_STACK,    (TEXT_BASE+MEM_SIZE-4) ; SVC 的棧從內存最高位址開始往下分配 1K。
    .equ _IRQ_STACK,    (_SVC_STACK-0x400)
    .equ _FIQ_STACK,    (_IRQ_STACK-0x400)
    .equ _ABT_STACK,    (_FIQ_STACK-0x400)
    .equ _UND_STACK,    (_ABT_STACK-0x400)
    .equ _SYS_STACK,    (_UND_STACK-0x400)
     
    .text
    .code 32
    .global __vector_reset
     
    .extern plat_boot
    .extern __bss_start__
    .extern __bss_end__
    __vector_reset:
      msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|SVC_MOD) ; 切換模式,並設置該模式的棧指針。不同模式使用不同的棧。user/system mode 共用棧。
      ldr sp,=_SVC_STACK
      msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|IRQ_MOD)
      ldr sp,=_IRQ_STACK
      msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|FIQ_MOD)
      ldr sp,=_FIQ_STACK
      msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|ABT_MOD)
      ldr sp,=_ABT_STACK
      msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|UND_MOD)
      ldr sp,=_UND_STACK
      msr cpsr_c,#(DISABLE_IRQ|DISABLE_FIQ|SYS_MOD)
      ldr sp,=_SYS_STACK
     
    _clear_bss:                                     ; 清空 bss 段。
      ldr r1,_bss_start_
      ldr r3,_bss_end_
      mov r2,#0x0
    1:
      cmp r1,r3
      beq _main                                     ; bss 段已清空,跳至 _main。
      str r2,[r1],#0x4                              ; [r1] = r2; r1 = r1 + 4
      b 1b                                          ; 跳至 1:
     
    _main:
      b plat_boot
     
    _bss_start_:.word   __bss_start__               ; __bss_start__ 和 __bss_end__ 分別由鏈結器確定。               
    _bss_end_:.word   __bss_end__
  4. boot.c
    typedef void (*init_func)(void);
     
    #define UFCON0  ((volatile unsigned int *)(0x50000020))
     
    void helloworld(void){
      const char *p="helloworld\n";
      while(*p){
        *UFCON0=*p++;
      };
    }
     
    static init_func init[]={
      helloworld,
      0,
    };
     
    void plat_boot(void){
      int i;
      // 初始化
      for(i=0;init[i];i++){
        init[i]();
      }
      while(1);
    }
  5. abnormal.s 實作其它中斷向量。
    .global __vector_undefined
    .global __vector_swi
    .global __vector_prefetch_abort
    .global __vector_data_abort
    .global __vector_reserved
    .global __vector_irq
    .global __vector_fiq
     
    .text
    .code 32
     
    __vector_undefined:
      nop
    __vector_swi:
      nop
    __vector_prefetch_abort:
      nop
    __vector_data_abort:
      nop
    __vector_reserved:
      nop
    __vector_irq:
      nop
    __vector_fiq:
      nop

MMU

ARM 透過激活 CP15 協處理器開啟分頁機制。操作協處理器有專門的指令,協處理器有自己的暫存器。

  1. 在內存中初始化頁表。
    #define PAGE_TABLE_L1_BASE_ADDR_MASK  (0xffffc000)
     
    // 由虛擬位址取得查詢頁表的索引。
    // 取高 12 位,又頁表項大小為 4 byte,故右移 18 位。最後在和頁表基址取 or,得頁表項位址。
    // 此索引共 14 位,頁表基址後 14 位應為零 (見上面 #define)。
    #define VIRT_TO_PTE_L1_INDEX(addr)  (((addr)&0xfff00000)>>18)
     
    #define PTE_L1_SECTION_NO_CACHE_AND_WB  (0x0<<2)
    #define PTE_L1_SECTION_DOMAIN_DEFAULT (0x0<<5)
    #define PTE_ALL_AP_L1_SECTION_DEFAULT (0x1<<10)
     
    #define PTE_L1_SECTION_PADDR_BASE_MASK  (0xfff00000)
    #define PTE_BITS_L1_SECTION       (0x2)
     
    #define L1_PTR_BASE_ADDR      0x30700000  // 假定頁表基址。
    #define PHYSICAL_MEM_ADDR     0x30000000  // 假定物理內存起始位址。
    #define VIRTUAL_MEM_ADDR      0x30000000  // 假定虛擬內存起始位址。
    #define MEM_MAP_SIZE          0x800000    // 內存大小。
    #define PHYSICAL_IO_ADDR      0x48000000
    #define VIRTUAL_IO_ADDR       0xc8000000
    #define IO_MAP_SIZE           0x18000000
     
    // 給定一物理位址,返回其頁表項內容。 
    unsigned int gen_l1_pte(unsigned int paddr) {
      // paddr & PTE_L1_SECTION_PADDR_BASE_MASK 得某物理位址所屬頁框。
      // PTE_BITS_L1_SECTION 代表其為頁表項內容 (page table entry, pte)。
      return (paddr & PTE_L1_SECTION_PADDR_BASE_MASK) | \
                        PTE_BITS_L1_SECTION;
    }
     
    // 給定頁表基址 (baddr) 和虛擬位址 (vaddr),返回頁表項地址。
    unsigned int gen_l1_pte_addr(unsigned int baddr,\
                        unsigned int vaddr) {
      return (baddr & PAGE_TABLE_L1_BASE_ADDR_MASK) |\
                    VIRT_TO_PTE_L1_INDEX(vaddr);
    }
     
    void init_sys_mmu(void){
      unsigned int pte;
      unsigned int pte_addr;
      int j;
     
      // 一般內存空間
      for (j = 0; j < MEM_MAP_SIZE>>20; j++) {
        pte = gen_l1_pte(PHYSICAL_MEM_ADDR + (j << 20));  // 給定一物理位址,返回其頁表項內容。
        pte |= PTE_ALL_AP_L1_SECTION_DEFAULT;             // 設置權限。
        pte |= PTE_L1_SECTION_NO_CACHE_AND_WB;
        pte |= PTE_L1_SECTION_DOMAIN_DEFAULT;
        pte_addr = gen_l1_pte_addr(L1_PTR_BASE_ADDR,\     // 給定頁表基址 (baddr) 和虛擬位址 (vaddr),返回頁表項地址。
                    VIRTUAL_MEM_ADDR + (j << 20));
        *(volatile unsigned int *)pte_addr = pte;         // 將頁表項內容填入其頁表項地址。
      }
     
      // MMIO
      for ( j = 0; j < IO_MAP_SIZE>>20; j++) {
        pte = gen_l1_pte(PHYSICAL_IO_ADDR + (j << 20));
        pte |= PTE_ALL_AP_L1_SECTION_DEFAULT;
        pte |= PTE_L1_SECTION_NO_CACHE_AND_WB;
        pte |= PTE_L1_SECTION_DOMAIN_DEFAULT;
        pte_addr = gen_l1_pte_addr(L1_PTR_BASE_ADDR,\
                    VIRTUAL_IO_ADDR + (j << 20));
        *(volatile unsigned int *)pte_addr = pte;
      }
    }
  2. 將 CR15 指向頁表基址。
    void start_mmu(void){
      unsigned int ttb = L1_PTR_BASE_ADDR;
     
      // %n 代表出現在輸出入列表的第 n 個參數。
      // mcr 將通用暫存器 (r) 其值傳送至協處理器 (c) 的暫存器。
      // p15 (cp15) 的 c2 暫存器保存頁表基址,c3 分為 16 個域 (domain) 控管頁存取權限,c1 置 1 開啟 MMU (分頁機制)。
      asm (
        "mcr p15,0,%0,c2,c0,0\n"    /* set base address of page table*/
        "mvn r0,#0\n"               // r0 = ~0 (0xffffffff)
        "mcr p15,0,r0,c3,c0,0\n"    /* enable all region access*/
     
        "mov r0,#0x1\n"            
        "mcr p15,0,r0,c1,c0,0\n"    /* set back to control register */
        "mov r0,r0\n"               /* nop,清空流水線 */
        "mov r0,r0\n"
        "mov r0,r0\n"
        :                           /* 輸出暫存器 */
        : "r" (ttb)                 /* 輸入暫存器,由 ttb 給值 */
        :"r0"                       /* r0 其值會被修改 */
      );
    }

Chapter 4

參考資料

Q & A

  1. Linux 0.11 沒有 swap,Linux 0.12 才有。

外部連結

登录