Linux 保護模式記憶體架構介紹

Linux 保護模式記憶體架構介紹




,前言

X86的保護模式下,Linux 作業系統的記憶體定址可以擁有0--4GB的空間,相對於User-Space(Ring 3)的程式碼,屬於Kernel-Space(Ring 0)的作業系統核心程式可以存取執行整個0--4GB的虛擬記憶體空間,而使用者的程式只能存取屬於User-Mode03GB記憶體空間. 如下圖()所示,為保護模式下Linux記憶體的架構圖,這張圖清楚的區分了核心的記憶體空間與屬於使用者的記憶體範圍.

(),User-Kernel Mode記憶體架構


如下圖()所示,保護模式下的Linux核心,藉由使用者程序各自獨立的記憶體空間,實現了Preemptive 多工環境. 讓在Linux環境下運作的使用者程序擁有各自獨立的記憶體空間與CPU執行時間,即使任一個使用者程序發生錯誤,也不會影響到系統整體的運作. 由於各使用者程序擁有各自獨立的記憶體空間,所以彼此所共同參考到的函式庫或是檔案都可以透過虛擬記憶體對應到同一塊實體記憶體,只有當記憶體內容改變時,才會藉由Copy-On-Write的機制,使用更多的記憶體空間來紀錄已改變的資料內容.


(),User-Kernel Mode Process


有了User-ModeKernel-Mode基本的概念之後,接下來就開始這次文章的介紹,筆者將以Linux 2.4.x的核心為例,為各位逐步說明Linux下的保護模式記憶體架構.


,容我再多提一下保護模式

在文章主菜上場前,筆者先帶入保護模式下GDT(Global Descriptor Table,全域描述器表),IDT(Interrupt Descriptor Table,中斷描述器表)LDT(Local Descriptor Table,區域節區描述器表)的概念,基本上,藉由了解這三個描述器表功能,各位就可以對於保護模式下的多工與記憶體架構有一定程度的認識了.

提到保護模式,首先最重要的概念就是不同程式碼可以位於不同特權等級的觀念,如下圖()所示,基本上在x86的保護模式架構下可以分為4個等級(Ring0-Ring3),其中在Linux的環境下使用者的程式屬於Ring3,Linux核心與驅動程式屬於Ring0. 在基於x86保護模式的基礎下, 使用者的程式相對於核心的程式而言是比較不可靠的,也就是說屬於Ring 3的程式並不能存取屬於Ring0的記憶體空間,並且屬於Ring3的程式也無法直接去執行屬於Ring0 的程式碼範圍. 因為屬於Ring 3的使用者程式,很可能是其他使用者自行撰寫的惡意程式, 透過保護模式的特權等級架構,可以讓相對不可安全的程式碼,無法接觸位於核心較為可靠的程式碼.

而位於Ring0的程式碼,由於屬於最高的特權等級,所以可以隨意的存取Ring3程式的記憶體空間, 並且可以接觸Ring0-Ring3所有特權等級的記憶體空間與程式碼. Linux目前作業系統中僅使用了Ring3Ring0兩個特權等級,目前常用的Windows 9x/ME/NT/2000/XP 也是使用Ring3Ring0兩個特權等級,分別代表了User-ModeKernel-Mode.

(),保護模式 Ring 概念

不過在Linux的保護模式架構下, Ring3的程式必須要依靠Linux核心所提供的系統呼叫才能順利的執行各式的服務,例如:開檔讀檔,收送網路封包,都會需要透過位於核心的檔案系統驅動程式或是網路協定驅動程式才得以完成,也因此位於Ring 0Linux核心記憶體空間就有必要提供一些機制讓屬於Ring3的程式可以呼叫Ring0提供的服務.

因此,Linux的架構下提供了一個可以讓Ring3程式呼叫的中斷80h,也就是在保護模式中的中斷閘道(Interrupt Gate), 透過觸發一個中斷可以導致特權等級由Ring 3轉移到Ring0,進而使得Ring3呼叫核心服務的參數可以透過中斷的暫存器被帶入到屬於Ring0的服務函式中. 透過這樣的方式,位於Ring0 80h號中斷服務常式就可以根據使用者所填入的參數(CPU暫存器ah來區分)來提供所需完成的對應服務. 如此一來,Linux環境下的使用者程式就可以透過80h號中斷所提供的各式系統呼叫來建構出各式各樣的函式庫(例如:Glibclibc.sold-linux.so),再來提供給使用者呼叫使用了.

保護模式中,定義了一些屬於作業系統等級(Ring0)才能執行的指令,這些屬於特權的指令集,如果由位於Ring 3的程式來執行就會產生保護模式下的例外錯誤, 例如:屬於Ring 3指令集的sidtsgdt 作用在於讀出IDTGDT表的位址與大小,但是儲存IDTGDT表的指令為LIDTLGDT 就是屬於Ring0的指令,一旦使用者程式執行這些特權等級指令,就會引發相關例外錯誤產生.

這部分的保護模式觀念就介紹到此,接下來就讓我們進一步的討論本文所涉及到的GDT,IDTLDT描述器表的意義與架構,其它所沒提到的保護模式概念,各位可以自行參閱相關書籍,如果以後有機會或許我也可以針對特定的部分再加以介紹補充.


GDT介紹

X86的保護模式下,可以擁有唯一一個GDT(Global Descriptor Table, 全域描述器表)用來紀錄屬於系統全域的記憶體區段描述資料,如下圖()所示,我們可以透過SGDT指令來取得6 bytes GDT資料,2bytes為整個GDT表的大小,4bytesGDT表所存在的記憶體位址.


() ),x86 GDT架構


每個GDTEntry都可以用來代表一個保護模式下的記憶體節區(Segment),所以每一個Entry主要都會定義了記憶體節區的大小(可以由04GB)以及所起始的基底位址,以下筆者針對幾個主要的欄位加以說明

  1. Segment Limit(20bits)定義節區的大小,會先確認欄位G(Granularity)的值

如果G=0Segment Limit的單位為 1Bytes,所以Segment Limit2^20-1=1MB-1如果G=1Segment Limit的單位為 4kBytes,所以Segment Limit4k*2^20-1=4GB-1

(2)Base Address(32bits)定義節區的起始位址,可以由02^32-1(4GB-1)
(3)DPL(2bits)(Descriptor Privilege Level)定義節區所屬特權等級,可以由03(Ring0Ring3)

其它的欄位筆者在此就不多述,各位可以自行查閱相關技術文件即可得知.

IDT介紹

保護模式下的中斷向量表起始位址,已經不是過去在DOS環境下(真實模式)0:0,而是在處理器由真實模式切換到保護模式時會重新Reset 8259chip,把中斷向量表定義到新的IDT起始位址,並且把IRQ所對應的中斷重新配置,而這個起始位址就會紀錄在處理器的IDTR暫存器.每當重新觸發一個中斷時,就會根據IDTR所紀錄的記憶體位址來尋找中斷服務的進入點,進而執行中斷服務常式.

如下圖()所示,我們可以透過SIDT指令來取得6 bytesIDT資料 ,2bytes為整個IDT表的大小,4bytesIDT表所存在的記憶體位址.

(),x86 IDT架構

同樣的筆者針對幾個本文會提到的欄位加以說明

(1)Segment Selector(16bits)定義中斷服務常式所在的節區選擇器,根據選擇器的值,會對應到GDT表的欄位.
(2)Offset(32bits)定義中斷服務常式在指定節區內進入點的相對位址(通常都會根據GDT欄位中的BaseAddress+IDT欄位中的Offset來找出中斷服務常式的進入點).
(3)DPL(2bits)(Descriptor Privilege Level)定義中斷所屬特權等級,可以由03(Ring0Ring3),例如:由核心提供給Ring3程式碼使用的系統呼叫,就是透過DPL380h中斷來完成,其它的中斷DPL0, 使用者程式則無法直接使用.


x86保護模式下前32個中斷(00h1fh)都被Intel定義為系統例外與預留的中斷服務,因此現在的保護模式作業系統都會把中斷服務定義在20h以後的中斷編號(包括IRQ所對應的中斷).


LDT介紹


X86保護模式下,如果要實現多工的環境LDT會是一個極為重要的系統機制,透過LDT我們可以充分運用X86系統中Ring0Ring3的特權等級架構. 如果沒有LDT的協助,X86的保護模式下將只能提供單一特權等級的多工環境, 也就是每個使用者程式都會與系統核心同屬於Ring 0特權等級.

如下圖()所示,如果我們不透過LDT來建立保護模式的環境,只以GDTIDT來產生保護模式機制的話, 我們會建立一個只有單一特權等級的保護模式環境,也就是說,在這樣的架構下,所有的使用者程式與Linux核心都會屬於同一個特權等級,也就是Ring0. 如此的架構是不安全的,因為每個使用者程式都有可能透過記憶體的覆蓋破壞了核心的正常執行. 不過目前這樣的架構也適用於Real-Time作業系統或是軟體架構較為精簡的執行環境中. 因為可以避免User-ModeKerenl-Mode彼此資料交換與多工切換上的執行成本.


(),不使用LDTTSS的單一特權等級多工架構圖

如果說,我們希望使用者程式與Linux核心分屬於不同的特權等級,進而提供更為完整與強固的保護模式,我們就會需要使用LDT來架構這樣的保護模式環境.

透過LDT的協助. 每一個使用者的程式都會有一個獨立的節區描述機制,如此一來針對每個節區描述範圍就可以提供不同的特權等級. 如下圖()所示, 每個LDT都會透過一個GDT欄位來紀錄,也因此當使用者程式使用了LDT,就會由處理器自動透過GDT把所對應到的LDT欄位中的Code SegmentData Segment載入到系統中,如此一來每個使用者程式都可以根據自己所屬的特權等級來執行(也就是Ring 3),也不容易危害到核心Ring 0程式碼的運作安全.




(),使用LDTTSS的特權等級多工架構圖


最後,在圖()與圖()都包括了IDT表與中斷觸發的流程,在此也針對保護模式下中斷觸發流程加以說明,首先如果系統觸發了一個中斷(不論是軟體中斷或是透過IRQ的硬體中斷),都會先透過IDTR(Interrupt Descriptor Table Register)找到指定中斷的進入點,如之前介紹IDT表的圖()所示,透過一個IDT的欄位,我們可以取得一個GDT的選擇器與中斷服務常式在這個選擇器記憶體節區空間中的Offset,之後透過一個GDT的欄位,我們可以得到這個選擇器所屬記憶體節區的基底位址(Base Address)與空間大小(Segment Limit,可由04GB的範圍),因此有了BaseAddressOffset我們就可以找到中斷服務常式在保護模式記憶體中的位址,進而順利的執行中斷呼叫.



取得GDTIDT的範例程式


如下所示,為一個可以用來取得IDTGDT內容的Kernel Module程式碼,可以透過以下方式編譯

gcc -D__KERNEL__ -DMODULE -Wall -O2 -I/usr/include/linux -I/usr/src/linux/include -c kernel.c -o kernel.o

再執行insmod kernel.o,就可以在console看到顯示結果了


kernel.c 程式碼

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/string.h>
//
unsigned int bCS,bDS,bES,bSS;
unsigned int bEIP,bEDI,bESI,bEBP,bESP;
unsigned int GDT,IDT,LDT;
unsigned char gdt_b[6],idt_b[6],ldt_b[6];
int i,j;
unsigned int *tt;
unsigned char *idt_base_addr;
unsigned char *gdt_base_addr;
unsigned short gdt_limit;
//
int init_module(void)
{
//
printk("\ninit_module\n\n");
//================================================================
//Begin
//================================================================
asm ("movl $0x3879, %eax\n\t"
/*Selector*/
"movl %CS, bCS\n\t"
"movl %DS, bDS\n\t"
"movl %ES, bES\n\t"
"movl %SS, bSS\n\t"
/*Offset*/
// "movl %%EIP, EIP\n\t"
"movl %EDI, bEDI\n\t"
"movl %ESI, bESI\n\t"
"movl %EBP, bEBP\n\t"
"movl %ESP, bESP\n\t"
/*Register*/
"sgdt gdt_b\n\t"
"sidt idt_b\n\t"
"sldt LDT\n\t"
);
printk("\nCS:%xh DS:%xh ES:%xh SS:%xh",bCS,bDS,bES,bSS);
printk("\nEDI:%xh ESI:%xh EBP:%xh ESP:%xh",bEDI,bESI,bEBP,bESP);
printk("\nLDT:%xh",LDT);
printk("\nIDTR__%x_%x_%x_%x_%x_%x\n",0x000000FF&idt_b[0],0x000000FF&idt_b[1],0x000000FF&idt_b[2],0x000000FF&idt_b[3],0x000000FF&idt_b[4],0x000000FF&idt_b[5]);
printk("\nGDTR__%x_%x_%x_%x_%x_%x\n",0x000000FF&gdt_b[0],0x000000FF&gdt_b[1],0x000000FF&gdt_b[2],0x000000FF&gdt_b[3],0x000000FF&gdt_b[4],0x000000FF&gdt_b[5]);
//================================================================
//show GDT
//================================================================
tt=(unsigned int *)&gdt_b[2];
gdt_base_addr=(unsigned char *)*tt;
tt=(unsigned int *)&gdt_b[0];
gdt_limit=(unsigned short )*tt;
printk("\ngdt_limit:%xh gdt_base_addr:%xh\n",gdt_limit,(unsigned int)gdt_base_addr);
for(j=0x00;j<=gdt_limit;j+=8)
{
printk("\nSelector:%xh_Limit:%x_%x_%xh_BaseAddress:%x_%x_%x_%xh_Attr:%x_%x",
j,
0x0000000F & gdt_base_addr[6+j],
0x000000FF & gdt_base_addr[1+j],0x000000FF & gdt_base_addr[0+j], 0x000000FF & gdt_base_addr[7+j],0x000000FF & gdt_base_addr[4+j], 0x000000FF & gdt_base_addr[3+j],0x000000FF & gdt_base_addr[2+j], 0x000000F0 & gdt_base_addr[6+j],0x000000FF & gdt_base_addr[5+j]
);
}
//================================================================
//show IDT
//================================================================
//idt_base_addr=(unsigned char *)&idt_b[2];
tt=(unsigned int *)&idt_b[2];
idt_base_addr=(unsigned char *)*tt;
printk("\nidt_base_addr:%xh\n",(unsigned int)idt_base_addr);

for(i=0x00;i<256;i++)
{
j=i*8;
printk("\nInterrupt:%xh_Offset:%x_%x_%x_%xh_Selector:%x_%xh_Attr:%x_%x",
i,
0x000000FF & idt_base_addr[7+j],0x000000FF & idt_base_addr[6+j],
0x000000FF & idt_base_addr[1+j],0x000000FF & idt_base_addr[0+j],
0x000000FF & idt_base_addr[3+j],0x000000FF & idt_base_addr[2+j],
0x000000FF & idt_base_addr[5+j],0x000000FF & idt_base_addr[4+j]
);
}
//================================================================
//End
//================================================================
printk("\n");
//
return 0;
}
//
void cleanup_module(void)
{
printk("\nclean_module\n\n");
}

筆者的這段程式主要是透過sgdtsidt取得GDTIDT的大小與起始位址.

在筆者的電腦上執行結果如下


CS:10h DS:18h ES:18h SS:18h 相關Segment Selector的值
EDI:0h ESI:0h EBP:c137bf28h ESP:c137bf10h
LDT:a8h
IDTR__ff_7_0_10_29_c0
GDTR__bf_0_40_6_24_c0
gdt_limit:bfh gdt_base_addr:c0240640h
Selector:0h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:0_0
Selector:8h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:0_0
Selector:10h_Limit:f_ff_ffh_BaseAddress:0_0_0_0h_Attr:c0_9b
CS Segment起始位址為0x00000000,大小為4GB
Selector:18h_Limit:f_ff_ffh_BaseAddress:0_0_0_0h_Attr:c0_93
DS,ES,SS Segment起始位址為0x00000000,大小為4GB
Selector:20h_Limit:f_ff_ffh_BaseAddress:0_0_0_0h_Attr:c0_fb
Selector:28h_Limit:f_ff_ffh_BaseAddress:0_0_0_0h_Attr:c0_f3
Selector:30h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:0_0
Selector:38h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:0_0
Selector:40h_Limit:0_b_ffh_BaseAddress:c0_0_4_0h_Attr:40_92
Selector:48h_Limit:0_ff_ffh_BaseAddress:c0_f_0_0h_Attr:40_9b
Selector:50h_Limit:0_ff_ffh_BaseAddress:c0_f_0_0h_Attr:0_9b
Selector:58h_Limit:0_ff_ffh_BaseAddress:c0_0_4_0h_Attr:40_92
Selector:60h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:c0_9a
Selector:68h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:80_9a
Selector:70h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:80_92
Selector:78h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:80_92
Selector:80h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:80_92
Selector:88h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:0_0
Selector:90h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:0_0
Selector:98h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:0_0
Selector:a0h_Limit:0_0_ebh_BaseAddress:c0_29_18_0h_Attr:0_8b
Selector:a8h_Limit:0_0_27h_BaseAddress:c0_24_15_90h_Attr:0_82
Selector:b0h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:0_0
Selector:b8h_Limit:0_0_0h_BaseAddress:0_0_0_0h_Attr:0_0

idt_base_addr:c0291000h
Interrupt:0h_Offset:c0_10_6f_d0h_Selector:0_10h_Attr:8f_0
Interrupt:1h_Offset:c0_10_70_80h_Selector:0_10h_Attr:8f_0
Interrupt:2h_Offset:c0_10_70_90h_Selector:0_10h_Attr:8e_0
Interrupt:3h_Offset:c0_10_70_d0h_Selector:0_10h_Attr:ef_0
Interrupt:4h_Offset:c0_10_70_e0h_Selector:0_10h_Attr:ef_0
Interrupt:5h_Offset:c0_10_70_f0h_Selector:0_10h_Attr:ef_0
Interrupt:6h_Offset:c0_10_71_0h_Selector:0_10h_Attr:8f_0
Interrupt:7h_Offset:c0_10_70_40h_Selector:0_10h_Attr:8f_0
Interrupt:8h_Offset:c0_10_71_20h_Selector:0_10h_Attr:8f_0
Interrupt:9h_Offset:c0_10_71_10h_Selector:0_10h_Attr:8f_0
Interrupt:ah_Offset:c0_10_71_30h_Selector:0_10h_Attr:8f_0
Interrupt:bh_Offset:c0_10_71_40h_Selector:0_10h_Attr:8f_0
Interrupt:ch_Offset:c0_10_71_50h_Selector:0_10h_Attr:8f_0
Interrupt:dh_Offset:c0_10_71_60h_Selector:0_10h_Attr:8f_0
Interrupt:eh_Offset:c0_10_71_80h_Selector:0_10h_Attr:8e_0
Interrupt:fh_Offset:c0_10_71_a0h_Selector:0_10h_Attr:8f_0
Interrupt:10h_Offset:c0_10_70_20h_Selector:0_10h_Attr:8f_0
Interrupt:11h_Offset:c0_10_71_70h_Selector:0_10h_Attr:8f_0
Interrupt:12h_Offset:c0_10_71_90h_Selector:0_10h_Attr:8f_0
Interrupt:13h_Offset:c0_10_70_30h_Selector:0_10h_Attr:8f_0
Interrupt:14h_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:15h_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:16h_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:17h_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:18h_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:19h_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:1ah_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:1bh_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:1ch_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:1dh_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:1eh_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:1fh_Offset:c0_10_2_20h_Selector:0_10h_Attr:8e_0
Interrupt:20h_Offset:c0_21_29_30h_Selector:0_10h_Attr:8e_0
Interrupt:21h_Offset:c0_21_29_40h_Selector:0_10h_Attr:8e_0
Interrupt:22h_Offset:c0_21_29_50h_Selector:0_10h_Attr:8e_0
Interrupt:23h_Offset:c0_21_29_60h_Selector:0_10h_Attr:8e_0
Interrupt:24h_Offset:c0_21_29_70h_Selector:0_10h_Attr:8e_0

Interrupt:25h_Offset:c0_21_29_80h_Selector:0_10h_Attr:8e_0

Interrupt:26h_Offset:c0_21_29_90h_Selector:0_10h_Attr:8e_0
Interrupt:27h_Offset:c0_21_29_a0h_Selector:0_10h_Attr:8e_0
Interrupt:28h_Offset:c0_21_29_b0h_Selector:0_10h_Attr:8e_0
Interrupt:29h_Offset:c0_21_29_c0h_Selector:0_10h_Attr:8e_0
Interrupt:2ah_Offset:c0_21_29_d0h_Selector:0_10h_Attr:8e_0
Interrupt:2bh_Offset:c0_21_29_e0h_Selector:0_10h_Attr:8e_0
Interrupt:2ch_Offset:c0_21_29_f0h_Selector:0_10h_Attr:8e_0
Interrupt:2dh_Offset:c0_21_2a_0h_Selector:0_10h_Attr:8e_0
Interrupt:2eh_Offset:c0_21_2a_10h_Selector:0_10h_Attr:8e_0
Interrupt:2fh_Offset:c0_21_2a_20h_Selector:0_10h_Attr:8e_0
Interrupt:30h_Offset:0_0_0_0h_Selector:0_10h_Attr:8e_0
Interrupt:31h_Offset:0_0_0_0h_Selector:0_10h_Attr:8e_0
Interrupt:32h_Offset:0_0_0_0h_Selector:0_10h_Attr:8e_0
Interrupt:33h_Offset:0_0_0_0h_Selector:0_10h_Attr:8e_0
Interrupt:34h_Offset:0_0_0_0h_Selector:0_10h_Attr:8e_0

........................................

Interrupt:7fh_Offset:0_0_0_0h_Selector:0_10h_Attr:8e_0
Interrupt:80h_Offset:c0_10_6e_c0h_Selector:0_10h_Attr:ef_0 給使用者呼叫的80h中斷
Interrupt:81h_Offset:0_0_0_0h_Selector:0_10h_Attr:8e_0

.............................................
Interrupt:ffh_Offset:0_0_0_0h_Selector:0_10h_Attr:8e_0




set_fsget_fs設定記憶體存取範圍

如下圖()所示,Linux x86保護模式下的系統呼叫因為屬於Ring0的特權等級,所以具備了可以存取任意記憶體位址的權利,因此這些系統呼叫都會參考目前使用者程序所能存取的記憶體上限(例如: current->addr_limit 設定為0xc0000000,也就是定義了只能存取03GB這範圍內的記憶體),作為本次執行時記憶體所能存取的範圍限定.

所以說在核心的程式碼需要使用到這些函式時,就會需要透過get_fs保存原本的記憶體範圍,再透過set_fs把記憶體範圍加大,執行完畢後,再透過set_fs把記憶體所限定的範圍給還原.






(),Linux透過Set_FS來設定記憶體可存取範圍


因為有這樣的限制存在,所以當我們在Linux撰寫程式碼時,如果也叫用了這些系統呼叫的函式,由於這些函式被限定只能存取03GB的記憶體空間,可是因為我們目前是在核心程式碼使用這些系統函式,所以說我們所配置的記憶體空間會是在3GB4GB之間,所以如果沒有把系統函式所能存取的記憶體空間重新設定為04GB的話,那我們在核心使用的系統函式將會發生無法存取3GB以上記憶體空間的錯誤.

所以,我們可以在許多的Linux核心函式中看到以下的程式碼區段


oldfs=get_fs(); 原本只能存取03GB
set_fs(KERNEL_DS); 設定為可以存取 04GB,包括Linux核心所屬的記憶體空間

執行核心所提供的系統呼叫

set_fs(oldfs); 執行完系統呼叫後,重新把記憶體空間設定回03GB

接下來,就讓我們針對Linux核心相關程式碼部份作一個簡要的介紹





,進入Linux核心(2.4.x)


如下所示,LinuxX86保護模式初始化時,會定義屬於Kernel-ModeCode SgementData SegmentSelector, CS(Code-Segment)就是屬於作業系統程式碼在運作時所會使用的CS Selector(如同我們在Real-Mode所使用的CS Segment暫存器,目前CPU執行指令的位址可以透過CS Selector: EIP指定,如同Real-ModeCS:IP), DS(Data-Segment)則是屬於資料位址的DS Selector,Linux Kernel Mode使用的CSDS涵概的記憶體空間都是為0—4GB,CS Selector0x10,DS Selecotr0x18.各位也可以透過剛剛筆者所使用的kernel.c程式碼即可驗證得知.

/src/include/asm/segment.h

#ifndef _ASM_SEGMENT_H
#define _ASM_SEGMENT_H
#define __KERNEL_CS 0x10
#define __KERNEL_DS 0x18
#define __USER_CS 0x23
#define __USER_DS 0x2B
#endif
程式碼(1)

linux Kernel-Mode當中,如果烤貝User-Mode記憶體的資料,就必需要透過copy_to_usercopy_from_user的核心函數.

include/asm/uaccess.h
#define copy_to_user(to,from,n) \
(__builtin_constant_p(n) ? \
__constant_copy_to_user((to),(from),(n)) : \
__generic_copy_to_user((to),(from),(n)))
#define copy_from_user(to,from,n) \
(__builtin_constant_p(n) ? \
__constant_copy_from_user((to),(from),(n)) : \
__generic_copy_from_user((to),(from),(n)))
程式碼(2)

如下所示,KERNEL_DSUSER_DS分別代表了核心與使用者空間所能存取的記憶體空間上限, KERNEL_DS值為0xFFFFFFFF,所指的就是以4GB的記憶體空間為上限,所以核心可以存取0-4GB的空間.

USER_DS則需參考include/asm/ page_offset.h(程式碼4)中的PAGE_OFFSET_RAW 定義,include/linux/ autoconf.h(程式碼5)我們可以看到在X86系統上預設為CONFIG_1GB,所以在include/asm/ page_offset.h我們可以看到PAGE_OFFSET_RAW的值為0xC0000000,也就是Kernel-Mode程式碼可以擁有1GB的記憶體空間(3GB4GB),User-Mode程式碼可以擁有3GB的記憶體空間(0GB3GB)

get_fsset_fs為取得和設定目前正在執行的使用者程序記憶體空間上限(參考(程式碼3)),當核心的程式想要讓系統呼叫的函式可以存取0—3GB以上的記憶體時,就要透過set_fs把目前正在執行的使用者程序(current參數指向目前正在行的程序)addr_limit指到0xffffffff(4GB-1),這樣系統函式就可以使用0—4GB的空間,也就可以存取屬於3—4GB Kernel Mode記憶體空間的資料了.

include/asm/uaccess.h
/* * The fs value determines whether argument validity checking should be
* performed or not. If get_fs() == USER_DS, checking is performed, with
* get_fs() == KERNEL_DS, checking is bypassed.
*
* For historical reasons, these macros are grossly misnamed. */
#define MAKE_MM_SEG(s) ((mm_segment_t) { (s) })
#define KERNEL_DS MAKE_MM_SEG(0xFFFFFFFF)
#define USER_DS MAKE_MM_SEG(PAGE_OFFSET)
#define get_ds() (KERNEL_DS)
#define get_fs() (current->addr_limit)
#define set_fs(x) (current->addr_limit = (x))
#define segment_eq(a,b) ((a).seg == (b).seg)
程式碼(3)


include/asm/ page_offset.h
#include <linux/config.h>
#ifdef CONFIG_1GB
#define PAGE_OFFSET_RAW 0xC0000000
#elif defined(CONFIG_2GB)
#define PAGE_OFFSET_RAW 0x80000000
#elif defined(CONFIG_3GB)
#define PAGE_OFFSET_RAW 0x40000000
#endif
程式碼(4)


include/linux/ autoconf.h
#define CONFIG_1GB 1
#undef CONFIG_2GB
#undef CONFIG_3GB
程式碼(5)

接下來我們來說明系統函式copy_to_usercopy_from_user的運作流程,(程式碼2)當中,我們可以看到如果__builtin_constant_p(用來確認是否為const變數,也就是不能改變其值內容的變數,compiler在編譯階段處理)成立,copy_to_user會呼叫__constant_copy_to_user,__constant_copy_to_user首先會呼叫accecc_ok,確認記憶體的範圍是否正確(有無超出目前current所能接觸到的最大記憶體上限),與該記憶體是否為可寫入.並驗證記憶體上限是否為KERNEL_DS(核心記憶體最大值為4GB-1). 如果__builtin_constant_p不成立,copy_to_user會呼叫 __generic_copy_to_user.

如下為__constant_copy_to_user__constant_copy_from_userSource Code

include/asm/uaccess.h
static inline unsigned long
__constant_copy_to_user(void *to, const void *from, unsigned long n)
{
prefetch(from);
if (access_ok(VERIFY_WRITE, to, n))
__constant_copy_user(to,from,n);
return n;
}
static inline unsigned long
__constant_copy_from_user(void *to, const void *from, unsigned long n)
{
if (access_ok(VERIFY_READ, from, n))
__constant_copy_user_zeroing(to,from,n);
else
memset(to, 0, n);
return n;
}


如下為__generic_copy_to_user__generic_copy_from_userSource Code
arch/i386/lib/usercopy.c
unsigned long __generic_copy_to_user(void *to, const void *from, unsigned long n)
{
prefetch(from);
if (access_ok(VERIFY_WRITE, to, n))
__copy_user(to,from,n);
return n;
}
unsigned long __generic_copy_from_user(void *to, const void *from, unsigned long n)
{
prefetchw(to);
if (access_ok(VERIFY_READ, from, n))
__copy_user_zeroing(to,from,n);
else
memset(to, 0, n);
return n;
}



早期Linux Kernel 1.3.x ,並不支援copy_from_usercopy_to_user,當時copy_from_user 是由memcpy_fromfs所取代, copy_to_user是由 memcpy_tofs取代, 筆者舉一個在Linux 核心程式碼中的範例作為說明,如下drivers/char/rocket.c,為了相容於舊版與新版本的Linux Kernel,於是透過Linux_VERSION_CODE來判斷核心版本,如果為舊版,就用自己定義的copy_from_usercopy_to_user再透過呼叫memcpy_fromfsmemcpy_tofs,如果為新版的核心就直接透過include/asm/uaccess.h 所定義的copy_from_usercopy_to_user來完成工作.

drivers/char/rocket.c
#if (LINUX_VERSION_CODE < 131336)
int copy_from_user(void *to, const void *from_user, unsigned long len)
{
int error;
error = verify_area(VERIFY_READ, from_user, len);
if (error)
return len;
memcpy_fromfs(to, from_user, len);
return 0;
}
int copy_to_user(void *to_user, const void *from, unsigned long len)
{
int error;
error = verify_area(VERIFY_WRITE, to_user, len);
if (error)
return len;
memcpy_tofs(to_user, from, len);
return 0;
}
static inline int signal_pending(struct task_struct *p)
{
return (p->signal & ~p->blocked) != 0;
}
#else
#include <asm/uaccess.h>
#endif



Linux原始程式碼當中的檔案src/include/asm/uaccess.h ,我門可以找到__copy_user函式的原始程式碼,這個函式主要的功能在與複製UserKernel Mode的記憶資料,Linux保護模式下最後的User-Kernel Mode記憶體覆製的動作基本上會透過以下的組合語言程式碼來完成


nop
movl $Src_Addr, %esi
movl $Des_Addr, %edi
movl $Count, %ecx
#APP
0: rep; movsl


其中ESI指的是來源的記憶體位址,EDI則為目標記憶體位址,ECX則是所要覆製的次數,movsl指的就是CPU指令movsd,主要是用來把資料由記憶體位址DS:ESI 覆製到記憶體位址ES:EDI,每次移動一個Dobuleword(也就是4 bytes).

例如我們透過movsd每次覆製4 bytes,如果今天要覆致512 bytes的大小,就要把ECX設為128,也就是每次覆製4 bytes,128.


不過筆者在此強調,這是我簡化後的結果,基本上都會包括一些額外的判斷與機制,筆者以此組合語言程式碼來說明,只是想表達User-Kernel Mode記憶體的覆製,其實也都是透過 movsd這類CPU指令來執行,讓對User-Kernel Mode記憶體覆製有疑問的人可以有所了解.


,最後再舉一個例子

筆者舉一個在核心進行開檔,讀檔與寫檔的程式作為範例,作為剛剛所提到的set_fs,get_fs運作的例子,

如下所示,透過filp_open(filename,O_CREAT|O_RDWR,0777) 開檔以前,要先執行

oldfs=get_fs();
set_fs(KERNEL_DS);

把目前執行程式current可存取的記憶體範圍設定為4GB,這樣當Kernel-Mode的程式碼呼叫函式時,就可以存取目前在3-4GB記憶體範圍內核心程式碼所包含的參數. 在開檔動作之後,就可以透過filp->f_pos移動檔案指標,寫入檔案,與讀取檔案的動作,而且這些動作所帶入的參數都是位於核心所屬的記憶體範圍內(3GB-4GB).

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/errno.h>
//
#include <asm/atomic.h>
#include <asm/processor.h>
#include <asm/uaccess.h>
#include <linux/file.h>
//
char buf[]="This is a kernel-mode file operation testing program";
char b_buf[128];
//
int init_module(void)
{
char filename[]="/linux.tmp";
struct file *filp;
int r;
mm_segment_t oldfs;
//
printk("\ninit_module\n\n");
//
oldfs=get_fs();
set_fs(KERNEL_DS);
filp=filp_open(filename,O_CREAT|O_RDWR,0777);
if(IS_ERR(filp))
{
printk("\nFile Open Error:%s\n",filename);
return 0;
}
if(!filp->f_op)
{
printk("\nFile Operation Method Error!!\n");
return 0;
}
//
printk("\nFile Offset Position:%xh\n",(unsigned)filp->f_pos);
r=filp->f_op->write(filp,buf,sizeof(buf),&filp->f_pos);
printk("\nWrite :%xh bytes Offset=%xh\n",r,(unsigned)filp->f_pos);
//
filp->f_pos=0x00;
r=filp->f_op->read(filp,b_buf,sizeof(b_buf),&filp->f_pos);
printk("\nRead %xh bytes %s\n",r,b_buf);
filp_close(filp,NULL);
set_fs(oldfs);
return 0;
}
//
void cleanup_module(void)
{
printk("\nclean_module\n\n");
}

,結語

相信各位都知道,透過x86保護模式的架構,Linux作業系統可以突破過去在DOS 真實模式下1MB記憶體的限制,透過充份發揮x86保護模式的效能,我們才有這樣一個多工與穩定的作業系統. 也幸虧Linux這樣一個自由開放的作業系統,我們才有機會可以站在巨人的肩膀上看世界.
透過這次的文章,筆者簡述了保護模式的基本概念,並且針對Linuxx86保護模式下的記憶體架構作一個說明,不過筆者在此強調一點就是,這不過是整個Linux保護模式記憶體管理的一小部份,其它諸如記憶體分頁(Memory Paging)以及磁碟Swap的機制,都是非常值得深入了解的部份,不過限於文章的涵蓋範圍有限,筆者日後有機會的話,會更進一步的針對這些部份的運作來加以說明.


對於本篇文章有何問題,都歡迎各位可以來信指,筆者的E-Mailhlchou@mail2000.com.tw


,參考資料

1, Intel Architecture Software Developer's Manual Volume 1 Basic Architecture
2, Intel Architecture Software Developer's Manual Volume 2 Instruction Set Reference Manual
3,Intel Architecture Software Developer's Manual Volume 3 System Programming Guide
5, Protected-Mode Memory Management,http://www.internals.com/articles/protmode/protmode.htm
6, Real-Mode Memory Management,http://www.internals.com/articles/protmode/realmode.htm