實驗2前篇——X86內存管理



    實驗1“洋洋灑灑的寫了那麼多的內容,看起來很豐富;但其實沒有進入主題。然而它們卻是實現與理解操作系統的必要步驟與開端。純粹的操作系統的理論總是讓人無法身臨其境的去理解而且實用性不是很強 ,而且我覺得操作系統的相關理論都是經驗的總結,而無其他,實際中使用的往往是實踐跑在前面,然後被總結爲理論,最後被無休止的進行復制。這樣確實一個好的軟件發展模式,不斷以代碼的方式沉澱,從而讓軟件發展得更好。

    操作系統發展了很多年了,其中也包含了很多的內容,同時也可以被分割爲相對對立的許多部分,如下圖(來源於《操作系統精髓與設計原理(原書第6)》)所示:

    通過上圖可以看出存儲器管理處於所有操作系統模塊的核心部位,與其他每個部分都有關係。當然對於程序來說,對內存的管理——分配與釋放也是最常用的操作。所以本實驗的主要內容是介紹操作系統內存的管理。

    本篇爲實驗2的前篇,主要是爲了理解與學習實驗2做好鋪墊與準備。因爲該實驗需要自己動手寫代碼滿足實驗代碼中的測試,所以需要能夠完全理解實驗的代碼流程與相關的概念。本篇主要從兩個方面介紹相關知識:1.X86系統的模式切換;2.系統內存的檢測。

   一)X86系統的模式切換

    內存管理的基本策略是分頁與分段,根據它們的特點,對於我們實現時的啓示是:1.進程中的所有存儲器訪問都是邏輯地址,這些邏輯地址在運行時被動態地轉換成物理地址;2.一個進程可以劃分爲許多塊(頁和段),在執行過程中,這些塊不需要連續地位於主存中。

    對於如上描述的內存管理策略的啓示,我們需要實現地址的動態轉換與進程的分割,對於這些需求,在以處理器爲核心的計算機構架中,對其的實現在硬件方面提供了必要的支持。而我們的實驗環境爲x8632位平臺,所以我們先用如下圖來描述對內存的管理的遷移與相關概念,然後進一步詳細介紹其中的細節,當然根據的內容可以參考Intel軟件開發手冊;而且對於目前的計算機構架來說,只要有一款處理器實現了利於操作系統資源管理的方案,其他處理器必然有相類似或者更優的方案,所以我們對於它們的理解可以一通百通,從而舉一反三。爲此我們總結一張圖來描述系統啓動流程的模式切換:

    如上圖所示,x86處理器一開機處於最簡單的模式——8088模式,然後通過設置全局段表(GDT)與開啓保護模式,進入到32位保護平坦模式;接着,爲了支持操作系統的內存管理,我們需要進入32位分頁模式,當然這個過程不是一蹴而就的,因爲最開始操作系統還沒有運行它的環境(它依賴於分頁模式),所以需要有一個過渡階段,搭建一個臨時的分頁模式4M,足夠它運行即可,等到操作系統運行起來之後,它需要對系統內存進行管理,而且需要根據不同的進程動態的創建頁表,所以最終才進入完整的32位分頁模式。從如上的模式切換流程,可以看出系統運行,狀態的切換是一個從簡單到負責,逐步爲操作系統最終運行,不斷創造執行環境的過程。

    在如上的描述內存管理的過程中一個繞不開的概念就是地址轉換——將程序使用的邏輯地址轉換成硬件訪問的物理地址。而對於X86的處理器有3個類似的概念——邏輯地址,線性地址,物理地址;而它們的關係與區別爲邏輯地址爲進程運行時訪問的地址(程序編譯時生成與鏈接的使用的地址),線性地址爲邏輯地址通過分段機制轉換之後的地址,物理地址爲程序運行時實際訪問的內存地址,當然也是線性地址經過分頁機制轉換之後的地址。

爲了更具體而詳細的描述如上過程,我們需要去仔細的解剖x86處理器的分頁與分段模式,而獲取這些信息最簡單而直接的方式是參考intel開發手冊的相關章節。對於分段與分頁的機制詳細描述如下:

   1)x86的內存管理之分段——詳細參考第3卷第3

   對於分段的實現方式,x86的處理爲設置一個段表用於表示每個段的長度,基地址以及訪問權限,而該地址段表被段寄存器(它對程序是不可見的,)所指向,所以x86使用分段機制,其實是根據段選擇符去讀段寄存器去指向的數組(段表)項,從而進行地址轉換,詳細的見如上模式轉換圖中32位保護平坦模式地址訪問部分。

   針對如上的管理機制,得出初始化分段機制步驟如下:

  1.設置段表與段寄存器內容——在內存中分配一段內存初始化它們

  2.加載段表到段寄存器——LGDT &seg_table_reg_content

  3.跳轉到分段模式的地址中執行代碼——LJMP &code_at_seg_mode

  4.設置段選擇符——CSDSSSESFSGS

  A)段表描述圖:

    如上圖所示,段表項長度爲8,段表首地址也爲8Byte對齊,而段表寄存器主要爲兩部分——段表的長度(以字節爲單位,其實爲段表長度-1),段表所在地址。其中所描述的地址爲絕對物理地址。

    B)段表項——段描述符

   段描述符包含了段的基地址,段長度,段的訪問權限等內容。詳細介紹見第3卷3.4.5節。

   C)段選擇符

    段選擇符包含了訪問端的索引爲8的倍數,訪問權限。其實是8088所熟知的段寄存器爲CSDSESSSFSGS等。

    2)x86的內存管理之分頁——詳細參考第3卷第4章:

    對於分頁機制的實現,x86的處理爲將線性地址進行分割,然後通過設置的頁目錄寄存器(CR3)所指向數組(頁目錄)進行轉換。如下爲我們使用的32位分頁模式的訪問方式爲例進行詳細描述:

   如上圖所示,將32位線性地址進行3部分分割——頁目錄索引(10位),頁表索引(10位),偏移量(12位),地址轉換過程爲首先查詢CR3指向的頁目錄地址,通過頁目錄索引得到,頁表地址,再通過頁表索引得到物理基地址,然後與偏移量組合成物理地址。

   對於x86處理器來說,它所支持的分頁模式如下表所示:

    如上圖所示,分頁模式有3種,頁幀的大小支持4K/4M的,而我們實驗使用的是32-bit且頁幀爲4KByte的模式。

    A)頁表項與頁目錄項:

    如上圖所示,頁表與頁目錄的首地址需要4KByte對齊,其中也包含了頁表與頁目錄的訪問權限。其中會常用到的權限爲:0bit表示是否在內存中,1bit表示是否只讀,2bit表示是否用戶空間可訪問。

   二)系統內存的檢測

    在理解了x86對內存管理的分頁與分段機制之後,我們需要對內存進行管理,首先需要檢測系統能夠提供給我們的物理空間有多大?

    爲了讀取x86系統的信息,目前只有通過bios提供的服務來讀取。而爲了考慮兼容性x86的內存分爲兩部分——低1M內存空間,擴展內存。所以要檢測系統的內存,需要讀取這兩部分的內存空間大小,這裏的內存大小爲系統可以用來作爲RAM來使用的(內核與應用程序能使用的空間)

。詳細的介紹可以參考:http://wiki.osdev.org/Detecting_Memory_(x86)。在實驗2的內存檢測函數中,它使用的是通過IO口讀取CMOS所配的內存大小,這是不準的,所以我們需要修改之,使用中斷(INT 0x15 EAX=0xE820)的方式讀取所有的內存。

   A)1M內存檢測:

   最簡單方式使用中斷0x12,默認值爲640K,正常讀取也爲這個值。也可以使用CMOS讀取。詳情見源代碼。

   B)擴展內存檢測:

   內存檢測協議——流程如下:

     內存檢測內容——內存映射段的描述,以c語言數據結構的模式:

typedef struct SMAP_entry { 	
uint32_t BaseL; // base address uint64_t	
uint32_t BaseH;	
uint32_t LengthL; // length uint64_t	
uint32_t LengthH;	
uint32_t Type; // entry Type	
uint32_t ACPI; // extended
 }__attribute__((packed)) SMAP_entry_t;

     如上所示:64位開始地址,64位段長度,段類型,ACPI段(當映射長度爲24纔有)

     段類型如下:

 

類型

功能

1

正常RAM使用

2

保留內存——不能被使用

3

ACPI內存

4

ACPI NVS內存

5

壞的內存區

    C)代碼實現

    在我實現的內存檢測代碼中有兩種方式:1.將內存檢測的intel彙編代碼,經過轉換成at&t彙編直接生成的代碼;2.移植linux內核的內存檢測代碼。

    (1)將內存檢測的intel彙編代碼,先用nasm彙編生成elf文件,然後再用objdump進行反彙編,接着去掉反彙編產生的機器碼與地址,最後再修改相關的地址與相關部分。如上過程我已經用腳本實現了,方便處理。

     實現的intel彙編如下:

segment .text
global do_e820
; use the INT 0x15, eax= 0xE820 BIOS function to get a memory map
; inputs: es:di -> destination buffer for 24 byte entries
; outputs: bp = entry count, trashes all registers except esi
do_e820:
	xor ebx, ebx		; ebx must be 0 to start
	xor bp, bp		; keep an entry count in bp
	mov edx, dword 0x0534D4150	; Place "SMAP" into edx
	mov eax, 0xe820
	mov [es:di + 20], dword 1	; force a valid ACPI 3.X entry
	mov ecx, 24		; ask for 24 bytes
	int 0x15
	jc short .failed	; carry set on first call means "unsupported function"
	mov edx, 0x0534D4150	; Some BIOSes apparently trash this register?
	cmp eax, edx		; on success, eax must have been reset to "SMAP"
	jne short .failed
	test ebx, ebx		; ebx = 0 implies list is only 1 entry long (worthless)
	je short .failed
	jmp short .jmpin
.e820lp:
	mov eax, 0xe820		; eax, ecx get trashed on every int 0x15 call
	mov [es:di + 20], dword 1	; force a valid ACPI 3.X entry
	mov ecx, 24		; ask for 24 bytes again
	int 0x15
	jc short .e820f		; carry set means "end of list already reached"
	mov edx, 0x0534D4150	; repair potentially trashed register
.jmpin:
	jcxz .skipent		; skip any 0 length entries
	cmp cl, 20		; got a 24 byte ACPI 3.X response?
	jbe short .notext
	test byte [es:di + 20], 1	; if so: is the "ignore this data" bit clear?
	je short .skipent
.notext:
	mov ecx, [es:di + 8]	; get lower uint32_t of memory region length
	or ecx, [es:di + 12]	; "or" it with upper uint32_t to test for zero
	jz .skipent		; if length uint64_t is 0, skip entry
	inc bp			; got a good entry: ++count, move to next storage spot
	add di, 24
.skipent:
	test ebx, ebx		; if ebx resets to 0, list is complete
	jne short .e820lp
.e820f:
	mov [mmap_ent], bp	; store the entry count
	clc			; there is "jc" on end of list to this point, so the carry must be cleared
	ret
.failed:
	stc			; "function unsupported" error exit
	ret
mmap_ent dd 0x8004

    轉換腳本——見附件change_asm2S.sh:

if [ -z "$1" ];then
	echo "input the intel\'s asm file!!!"
fi

#nasm 編譯intel彙編爲elf文件
nasm -o $1.elf -f elf $1
#反彙編elf文件
objdump -M 16 -d $1.elf > $1.elf.dump
#清除地址信息與機器碼 clear_dump_addr_info.pl腳本見附件
./clear_dump_addr_info.pl $1.elf.dump
rm -fr $1.elf $1.elf.dump 
    生成之後的AT&T彙編:
.section .text
.global do_e820
.code16
do_e820:
	xorl    %ebx,%ebx
	xorw    %bp,%bp
	movl    $0x534d4150,%edx
	movl    $0xe820,%eax
	movl   $0x1,%es:0x14(%di)
	
	mov    $0x18,%ecx
	int    $0x15
	jb do_e820.failed
	movl    $0x534d4150,%edx
	cmp    %edx,%eax
	jne do_e820.failed
	test   %ebx,%ebx
	je do_e820.failed
	jmp do_e820.jmpin
do_e820.e820lp:
	mov    $0xe820,%eax
	movl   $0x1,%es:0x14(%di)
	
	mov    $0x18,%ecx
	int    $0x15
	jb do_e820.e820f
	mov    $0x534d4150,%edx
do_e820.jmpin:
	jcxz do_e820.skipent
	cmp    $0x14,%cl
	jbe do_e820.notext
	testb  $0x1,%es:0x14(%di)
	je do_e820.skipent
do_e820.notext:
	mov    %es:0x8(%di),%ecx
	or     %es:0xc(%di),%ecx
	je do_e820.skipent
	inc    %bp
	add    $0x18,%di
do_e820.skipent:
	test   %ebx,%ebx
	jne do_e820.e820lp
do_e820.e820f:
	movl	mmap_ent,%ebx
	mov    %bp,(%ebx)
	clc    
	ret    
do_e820.failed:
	stc    
	ret    
mmap_ent:
	.int 0x8004	

   (1)移植linux內核的內存檢測代碼:

    當將linux內存檢測的相關代碼移植到程序中出現了一個可以預見的問題,編譯出來的bin檔大小會大於512Byte,而且讀取出來的信息如何給我們編譯的操作系統。

    解決第一個問題的方法,將移植的linux內存檢測的代碼放到MBR之後的空間,然後讀取,再加載的內存中運行之。

    解決第二個問題的方法,將讀取的內存信息加載固定的地址(約定地址),然後由編譯之後的操作系統去讀取即可。

     根據如上兩方面的分析,我們需要採取類似linux內核的啓動機制,首先位於MBR的代碼只作加載內核引導橋代碼到內存中運行之,然後由內核引導橋代碼進行加載內核,然後再運行之。爲此,我們也需要把橋代碼讀取的內存信息以固定地址與格式作爲約定,被操作系統訪問。具體如下圖所示:

    在移植linux代碼過程中有如下幾個技術點需要理解:

    1.以寄存器的方式進行參數傳遞實現,如下兩種方式:

     編譯時添加編譯參數: -mregparm=3

    聲明函數屬性爲:__attribute__((regparm(3))) 

    這樣做的好處是提高參數傳遞效率,加快程序運行。爲某些程序調用提供接口。

    參數對應方式,在x86平臺上:

    Arg1-->EAX

    Arg2-->ECX

    Arg3-->EDX

    2.在彙編代碼中嵌入機器碼:

    好處是手動修改操作指令的操作碼,這樣可以使需要常量的指令(比如intljmp等)使用變量,從而更靈活的處理指令。

   

.code16
	.section ".inittext","ax"
	.globl	intcall
	.type	intcall, @function
intcall:
	/* Self-modify the INT instruction.  Ugly, but works.將傳入的參數1作爲中斷號,修改3f處的操作數支持動態修改int指令的操作數 */
	cmpb	%al, 3f
	je	1f
	movb	%al, 3f
	jmp	1f		/* Synchronize pipeline */
1:
	/* Save state */
	pushfl
	pushw	%fs
	pushw	%gs
	pushal

	/* Copy input state to stack frame */
	subw	$44, %sp
	movw	%dx, %si
	movw	%sp, %di
	movw	$11, %cx
	rep; movsd

	/* Pop full state from the stack */
	popal 
	popw	%gs
	popw	%fs
	popw	%es
	popw	%ds
	popfl

	/* Actual INT 用機器碼定義中斷操作,然後通過代碼修改操作數實現動態修改int 中斷號*/
	.byte	0xcd		/* INT opcode */
3:	.byte	0

	/* Push full state to the stack */
	pushfl
	pushw	%ds
	pushw	%es
	pushw	%fs
	pushw	%gs
	pushal

	/* Re-establish C environment invariants */
	cld
	movzwl	%sp, %esp
	movw	%cs, %ax
	movw	%ax, %ds
	movw	%ax, %es

	/* Copy output state from stack frame */
	movw	68(%esp), %di	/* Original %cx == 3rd argument */
	andw	%di, %di
	jz	4f
	movw	%sp, %si
	movw	$11, %cx
	rep; movsd
4:	addw	$44, %sp

	/* Restore state and return */
	popal
	popw	%gs
	popw	%fs
	popfl
	retl
	.size	intcall, .-intcall

    3.內存管理的模式——分段與分頁與ljmp的地址只能是絕對物理地址。

一葉說:操作系統的主要任務就是資源管理與任務調度,當然也可以換句話說是,爲處理器與存儲器寫驅動,爲應用軟件提供軟件接口;所以從這個角度來理解操作系統,需要對處理器有很好的理解才行,而處理器對緩存,內存管理,中斷管理,總線管理,io管理都要有一定的規範,這就只有從處理器製造商獲取第一手資料,然後進行反覆查閱。而目前我們實現的操作系統,是在unix的經典模型下進行開發,可以說unix爲操作系統的實現提供了設計規範,所以就需要我們能夠去理解unix的實現,從而以此作爲基礎,理解現實中的操作系統。而在實現代碼過程中,我們參考了已經存在的代碼。通過學習經典的代碼,能夠提高我們的編程技巧與對系統的理解。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章