1. 程式人生 > >UNIX v6原始碼分析除錯之三:單步除錯系統程式碼 main函式之 kvmalloc

UNIX v6原始碼分析除錯之三:單步除錯系統程式碼 main函式之 kvmalloc

      kvmalloc();      // kernel page table

     kvmalloc函式初始化核心的記憶體分頁頁表。關於虛擬記憶體,線性地址,記憶體分頁,記憶體分段等等在作業系統原理的書籍中都有詳細說明,我這裡就不囉嗦了。

      從程式碼實現的角度來理解和分析記憶體分頁管理。kvmalloc的函式如下:

// Allocate one page table for the machine for the kernel address
// space for scheduler processes.
void
kvmalloc(void)
{
  kpgdir = setupkvm();
  switchkvm();
}

    這裡分為2步,第一步是建立核心使用的記憶體分頁表項(使用二級目錄),第二步是通過設定cr3控制暫存器,來設定分頁表的表頭地址。第二步比較簡單,通過 asm volatile("movl %0,%%cr3" : : "r" (val)); 設定即可。

    但是在這2步之前,需要預先設定好cr0控制暫存器,開啟分頁功能。否則僅僅設定cr3暫存器是徒勞的。  cr0是在哪裡設定的呢?答案是在main函式之前,entry.s檔案裡面開啟了分頁功能,開啟程式碼如下:

  # Turn on paging.
  movl    %cr0, %eax
  orl     $(CR0_PG|CR0_WP), %eax
  movl    %eax, %cr0

    具體設定值的定義和意義可以參考Intel彙編手冊<<Intel® 64 and IA-32 Architectures Software Developer's Manual>>文件。

     重點是setupkvm函式,實現如下:

// This table defines the kernel's mappings, which are present in
// every process's page table.
static struct kmap {
  void *virt;
  uint phys_start;
  uint phys_end;
  int perm;
} kmap[] = {
 { (void*)KERNBASE, 0,             EXTMEM,    PTE_W}, // I/O space
 { (void*)KERNLINK, V2P(KERNLINK), V2P(data), 0},     // kern text+rodata
 { (void*)data,     V2P(data),     PHYSTOP,   PTE_W}, // kern data+memory
 { (void*)DEVSPACE, DEVSPACE,      0,         PTE_W}, // more devices
};

// Set up kernel part of a page table.
pde_t*
setupkvm(void)
{
  pde_t *pgdir;
  struct kmap *k;

  if((pgdir = (pde_t*)kalloc()) == 0)
    return 0;
  memset(pgdir, 0, PGSIZE);
  if (p2v(PHYSTOP) > (void*)DEVSPACE)
    panic("PHYSTOP too high");
  for(k = kmap; k < &kmap[NELEM(kmap)]; k++)
    if(mappages(pgdir, k->virt, k->phys_end - k->phys_start, 
                (uint)k->phys_start, k->perm) < 0)
      return 0;
  return pgdir;
}

      setupkvm函式分為這幾步:

      1 分配頁目錄記憶體: pgdir = (pde_t*)kalloc(),kalloc固定分配4k的記憶體,那麼頁目錄的大小為4k(4096位元組),每個頁目錄指向一個頁表。32位機器中,地址為4位元組,那麼最多有1k的頁表。

     2 初始化記憶體,同時判斷是否超出範圍。

     3 對核心中各個不同用途的記憶體(定義了4個不同用途的記憶體塊),呼叫mappages函式初始化頁表。

     接著分析 mappages 函式, 註釋新增在原有程式碼裡面,如下:

// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned.
static int
mappages(pde_t *pgdir, void *va, uint size, uint pa, int perm)
{
  char *a, *last;
  pte_t *pte;
  
  a = (char*)PGROUNDDOWN((uint)va);  //4096位元組對齊,也就是低3位置0
  last = (char*)PGROUNDDOWN(((uint)va) + size - 1); //4096位元組對齊,也就是低3位置0

  for(;;){
    if((pte = walkpgdir(pgdir, a, 1)) == 0)   //通過虛擬地址的頁目錄偏移、頁表偏移計算出虛擬地址的頁項地址 
      return -1;
    if(*pte & PTE_P)
      panic("remap");
    *pte = pa | perm | PTE_P;   //設定地址和相關的flag
    if(a == last)
      break;  //需要計算頁表項的地址段都計算完成了
    a += PGSIZE; //虛擬地址往後移動一頁(4k為一頁),計算下一頁的頁表項
    pa += PGSIZE;
  }
  return 0;
}

    到這裡,程式碼分析完成。不過感覺這個二級目錄的頁表並沒有展示的很清楚。這裡列出部分記憶體資料來進一步說明。以I/O space的地址段為例。

{ (void*)KERNBASE, 0,             EXTMEM,    PTE_W}, // I/O space

   

   I/O地址空間:虛擬地址為0x80000000-0x800ff000,實體地址為0x0-0x100000。頁目錄的頭地址為0x803ff000,大小為4k位元組。我們來看看建立的二級目錄表是什麼樣子的。

   首先,對於邏輯地址,計算出頁目錄的偏移。pde = &pgdir[PDX(va)];  0x80000000-0x800ff000計算出PDX為0x200,每個目錄佔用4位元組,在頁目錄的偏移為0x800。如下圖:

   

     偏移0x800處,值為0x003FE007,   這個值是這樣計算出來的 *pde = v2p(pgtab) | PTE_P | PTE_W | PTE_U;  最低位元組的07是3個記憶體標準位按位或得到。0x003FE000是頁目錄0x800偏移地址指向的頁表地址,這個頁表地址是實體地址。虛擬地址應該為0x8003FE000,該頁表的內容即指向實體地址 實體地址為 0x000000-0x100000,具體值如下:

    

       這些值怎麼理解呢,其實是通過 *pte = pa | perm | PTE_P; 這條語句去設定的頁項。每4個位元組都是一個頁表項。比如第一個,0x00000003, 最低位的03就是記憶體標準位,最低3位置0後即為頁表指向的實體地址。比如0x000000,0x00001000 ,0x00002000,可以看到每個表項的大小和間隔都是4k,4096個位元組。

     注意,這個page頁的大小在UNIX v6裡面定義為4K,其它作業系統就不一定了。