純DOS下內存的管理—實模式下訪問4GB內存<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
DOS操作系統最早設計時,PC機的硬件系統只支持1M字節的尋址空間,所以DOS只能管理最多1M字節的連續內存空間。在這1M內存中,又只有640K被留給應用程序使用,它們被稱爲常規內存或基本內存,其它384K被稱爲高端內存,是留給視頻顯示和BIOS等使用的。在1982年,640K內存對微型計算機來說顯得綽綽有餘,人們甚至認爲,640K的內存可以用來幹任何事。現在看起來有些可笑,但在當時,情況確實如此。
現在的情況是,即使你的電腦裝有幾兆或幾十兆內存,但如果你使用DOS操作系統,那麼你也只有640K的內存可以直接使用,1M以上的內存要通過一些內存管理工具才能使用。值得慶幸的是,Windows 95已經不存在常規內存的限制了,你所有的內存,不管是8M還是128M,都可以被直接使用。
在DOS下,系統中存在以下四種內存:
常規內存(Conventional Memory);
高端內存(Upper Memory);
擴充內存(Expanded Memory);
擴展內存(Extended Memory)。
常規內存指的是0-640K的內存區。在DOS下,一般的應用程序只能使用系統的常規內存,因而都要受到640KB內存的限制。而且由於DOS本身和config.sys文件中的安裝的設備驅動程序和autoexec.bat文件中執行的內存駐留程序都要佔用一些常規內存,所以應用程序能使用的常規內存是不到640K的。有很多時候,我們都要想方設法地整理內存,好爲一些“胃口”比較大的應用程序留出足夠的常規內存,這一點想必是許多DOS時代的電腦愛好者最熟悉不過的了。
高端內存是指位於常規內存之上的384K內存。程序一般不能使用這個內存區域,但是EMM386.exe可以激活高端內存的一部分,並且它允許用戶將某些設備驅動程序和用戶程序用Devicehigh或LH(即loadhigh)裝入高端內存。dos=high,umb也是把DOS的一部分裝到高端內存裏。這裏的umb是高端內存塊(Upper Memory Block)的縮寫。
擴充內存是一種早期的增加內存的標準,最多可擴充到32M。使用擴充內存必須在計算機中安裝專門的擴充內存板,而且還要安裝管理擴充內存板的管理程序。由於擴充內存是在擴展內存之前推出的,所以大多數程序都被設計成能使用擴充內存,而不能使用擴展內存。由於擴充內存使用起來比較麻煩,所以在擴展內存出現後不久就被淘汰了。
擴展內存只能用在80286或更高檔次的機器上,目前幾乎所有使用DOS的機器上超過1M的內存都是擴展內存。擴展內存同樣不能被DOS直接使用,DOS5.0以後提供了Himem.sys這個擴展內存管理程序,我們可以通過它來管理擴展內存。emm386.exe可以把擴展內存(XMS)仿真成擴充內存(EMS),以滿足一些要求使用擴充內存的程序。
最後再強調一下,不管擴充內存或擴展內存有多大,DOS的應用程序只能在常規內存下運行。有的程序可以通過DOS擴展器(比如DOS4GW.exe等程序)使CPU進入保護模式,從而直接訪問擴展內存;但是要注意,進入保護模式以後,計算機就脫離了DOS狀態。
最常見的方法有以下幾種:
1. 精減程序的尺寸,同時儘量避免一次使用太多的內存 (有沒有搞錯?偶只是初學者,不是算法專家耶)
2. 使用文件覆蓋技術,只在使用指定代碼時纔將其讀入內存,執行後釋放 (偶的程序怎麼就這麼慢呢?硬盤狂轉中…)
3. 用標準的DOS擴展技術如EMS、XMS、DPMI (哇!功能好強,不過程序改起來好累啊)
4. 自己寫保護模式平臺 (誰要這麼做了別忘了發一份源程序給我啊)
5. 轉移到其他平臺如Windows/UNIX/LINUX (拜託,我只想在DOS下運行啊)
程序壓縮效果再好也是有限,總不能達到50%以上吧,程序要用2M乃至10M內存呢?文件覆蓋需要自己編寫調度模塊,而且因爲頻繁讀寫硬盤,將導致程序運行速度變得很低;EMS、XMS、DPMI功能強大,兼容性強,尤其是DPMI規範不 但支持大內存訪問,而且還可以在保護模式下運行代碼並能超越64KB的段地址限制,實在是寫大型程序時的首選。但EMS 、XMS只能將擴展內存當作高速硬盤使用,所有的訪問都只能通過一系列中斷調用來完成,對於經常需要小尺寸大批量內 存訪問的程序就不太適合,而且使用它們均受制於實模式的64KB段地址空間,對於大數據量訪問也不太方便;用DPMI當 然就沒有這些限制,但它需要將所有的程序按保護模式的結構改寫,這也是一件麻煩的事。自己寫保護模式平臺(汗!… 這個我想沒有幾個人做得到吧,我就算做得到也不會做的)。至於轉移到其他平臺那就不用我說了。看到這裏,讀者不禁會問:“那照這麼說來沒有一種方法是好的了?” 其實也不是這樣,每種方法都有它的優點和缺點,要看你的需要來決定到底使用哪種方法。
好了,廢話說了這麼多,再不切入正題的話估計會有人向我扔雞蛋了,下面就來告訴大家怎麼做到在實模式下訪問4GB內存。這種技術需要保護模式支持,所以只能在80386以上的CPU中運行。 學過一點保護模式的讀者都知道,在保護模式下段地址寄存器中內容的不再象實模式那樣是段的基地址,而只是描述符表中的一個索引,段的真正信息(基地址、限長、訪問權限等)放在描述符表中,當訪問一數據時CPU會從描述符表取出段的描述信息來檢查訪問是否合法,不合法就產生異常,合法則允許訪問。每次訪問都要讀出描述符信息再檢查是一個比較費時的過程,爲了提高內存訪問的速度,Intel公司在CPU中爲每個段寄存器配備了一個高速緩衝器來存放段的描述符信息,這樣訪問內存時就不用頻繁地訪問描述表,只要從高速緩衝進行校驗就行,只有在改變段寄存器的值時才訪問描述符表將新的段描述符裝入高速緩衝中。我們就利用CPU的這個特性來達成我們的目的。首先進入保護模式,把某個段寄存器設爲基地址0H,限長4GB,然後再退回實模式。這樣就可以通過該段寄存器直接訪問4GB的內存了(實際上只能訪問你的機器上所有的內存而並不是4GB)!還有一點要注意的是一定要打開A20線,否則……別怪我言之不預!
下面列出所需要的代碼:
========================================================
Make4GBSegment MACRO _seg
local MyGdt,PM_Service,Old_GDTR,GDTR,Real_Service,MyGdt
local _Exit
Push DS
Push ES
Pushad
Pushfd ;保護現場
Sub EBX,EBX
Mov BX,CS
Mov DS,BX
Shl EBX,4
Push EBX
Rol EBX,8
Mov BYTE Ptr MyGdt[8+7],BL
Mov BL,BYTE Ptr MyGdt[8+5]
Ror EBX,8
Mov Dword Ptr MyGdt[8+2],EBX
Pop EBX
lea EBX,[EBX+MyGdt]
Mov DWORD Ptr [GDTR+2],EBX
Mov WORD Ptr [GDTR],31 ;建立新的GDTR
Cli
Sgdt FWORD Ptr [Old_GDTR] ;保存舊的GDTR
Lgdt FWORD Ptr [GDTR] ;設置新的GDTR
Mov EBX,CR0
Or BL,1
Mov CR0,EBX ;進入保護模式
db 0eah
DW PM_Service
DW 8 ;跳轉到保護模式代碼執行
PM_Service:
Mov AX,16
Mov _seg,AX
Mov EBX,CR0
And EBX,0fffffffeh
Mov CR0,EBX
DB 0eah
DW Real_Service
DW seg Real_Service
Real_Service:
Lgdt FWORD Ptr [Old_GDTR]
Popfd ;恢復現場
Popad
Pop ES
Pop DS
Jmp _Exit
MyGdt DQ 0
DW -1,0,9a00h,0
DW -1,0,9200h,0cfh
DQ 0
Old_GDTR DW 0,0,0
GDTR DW 0,0,0
_Exit:
Endm
========================================================
在這裏爲了方便我只把FS改成4GB段,讀者可以按需要自行決定使用哪個段寄存器。只要將這段代碼拷貝到你的程序中,然後在開始的時候調用它,就可以通過該段寄存器直接訪問大內存了,爽吧!最後還有一點一定要注意:如果你的程序運行時有任何擴展內存管理程序存在(HIMEM、EMM386等)都要千萬小心,因爲很容易會破壞到它們的內部數據或其他程序的數據,如果是這樣就只有死機一條路可走了。切記切記!我的建議是最好從內存頂端開始使用擴展內存。這時破壞其他數據的可能要小一些。
文章二:實模式下訪問4GB內存
作爲軟件開發人員,大多數對於保護模式都感到神祕和不易理解。本人在開發32位微內核搶佔式多線程操作系統過程中,深入瞭解到CPU的地址機理,在這裏將分析CPU的工作原理,解開保護模式的神祕面紗,讀者將會發現保護模式其實與實模式一樣簡單和易於控制。在此基礎上用四五十行C語言程序做到進出保護模式和在實模式之下直接訪問整個4GB內存空間。
雖然有許多書籍對保護模式作解釋,但沒有一本能簡單明瞭地解釋清楚,冗長煩雜的術語讓人看着想打瞌睡,甚至還有許多用匯編寫的(可能根本不能運行的)保護模式試驗程序,事實上用C語言本身就可以做保護模式的進出工作。
我們可能知道CPU上電後從ROM中的BIOS開始運行,而Intel文檔卻說80x86CUP上電總是從最高內存下16字節開始執行,那麼BIOS是處在內存的最頂端64K(FFFF0000H)還是1M之下的64K(F0000H)處呢?事實上在這兩個地方都同時出現(可用後面存取4GB內存的程序驗證)。爲什麼?爲了弄清楚以上問題,首先要了解CPU是如何處理物理地址的?真的是在實模式下用段寄存器左移4位與偏移量相加,在保護模式下用段描述符中的基地址加偏移量而兩者是毫無關聯的嗎?答案是兩者其實是一樣的。當Intel把80286推出時其地址空間變成了24位,從8086的20位到24位,十分自然地要加大段寄存器才行,實際上它們都被加大了,只是由於保護的原因加大的部分沒有被程序看見,到了80386之後地址又從24位加大到32位(80386SX是24位)。
在8086中CPU只有“看得見部分”,從而也直接參與了地址形成運算,但在80286之後,在“看不見部分”中已經包含了地址值,“看得見部分”就退化爲只是一個標號再也不用參與地址形成運算了。地址的形成總是從“不可看見部分”取出基址值與偏移相加形成地址。也就是說在實模式下當一個段寄存器被裝入一個值時,“看不見部分”的界限被設成FFFFH,基址部分纔是要裝入值左移4位,屬性部分設成16位0特權級。這個過程與保護模式時裝入一個段存器是同理的,只是保護模式的“不可見部分”是從描述表中取值,而實模式是一套固定的過程。
對於CPU在形成地址時,是沒有實模式與保護模式之分的,它只管用基址(“不可見部分”)去加上偏移量。實模式與保護模式的差別實際上只是保護處理部件是否工作得更精確而已,比如不允許代碼段的寫入。實模式下的段寄存裝入有固定的形成辦法從而也就不需要保護模式的“描述符”了,因此保持了與8086/8088的兼容性。而“描述符”也只是爲了裝入段寄存器的“不可見部分”而設的。
從上面的“整個段寄存器”可見CPU的地址形成與“看得見部分”的當前值毫無關係,這也解釋了爲什麼在剛進入保護模式時後面的代碼依然被正確地運行而這時代碼段寄存器CS的值卻還是進入保護模式前的實模式值,或者從保護模式回到實模式時代碼段CS被改變之前程序是正常地工作,而不會“突變”到CS左移4位的地址上去,比如在保護模式時CS是08H的選擇器,到了實模式時CS還是08H但地址不會突然變成80H加上偏段量中去。因爲地址的形成不理會段寄存器“看得見部分”的當前值,這一個值只是在被裝入時對CPU有用。
地址的形成與CPU的工作模式無關,也就是說實模式與0特權級保護模式不分頁時是一模一樣的。明白了這一機理,在實模式下一樣可以處理通常被認爲只有在保護模式才能做的事,比如訪問整個機器的內存。可以不必理會保護模式下的衆多術語,或者更易於理解,如選擇器就是“看得見部分”,描述符是爲了裝入“不可見部分”而設的。
作爲驗證CPU的這種機理,這裏寫了一個實模式下訪問4GB內存的C程序。有一些書籍也介紹有同樣功能的彙編程序,但它們都錯誤地認爲是利用80386芯片的設計疏漏。實際上Intel本身就在使用這種辦法,使得CPU上電時能從FFFFFFF0H處開始第一條指令,這種技術在286之後的每一臺機器每一次冷啓動時都使用,只是我們不知道罷了。CPU上電也整個代碼段寄存器是這樣的:
EIP=0000FFF0H這樣CS∶EIP形成了FFFFFFF0H的物理地址,當CPU進行一次遠跳轉重新裝入CS時,基址就變了。
爲了訪問4G內存空間,必須有一個段寄存器的“不可見部分”的界限爲4G-1,基址爲0,這樣就包含了4GB內存,不必理會可見部分的值。顯然要讓段寄存器在實模式下直接裝入這些值是不可能的。唯一的辦法是讓CPU進入一會兒保護模式在裝入了段寄存器之後馬上回到實模式。
進入保護模式十分簡單,只要建好GDT把CRO寄存器的位0置上1,CPU就在保護模式了,從前面所分析CPU地址形成機理可知,這時不必理會寄存器的“看得見部分”值是否合法,各種段寄存器是一樣可用的,就像沒進保護模式一樣。在把一個包含有4GB地址空間的值裝入某個段寄存器之後就可返回實模式。
預先可建好GDT如下:
unsigned long GDT-Table[]=
{0,0, //空描述符,必須爲零
0x0000FFFF,0xCF9A00, //32位平面式代碼段
0x0000FFFF,0xCF9200 } , //32位平面式數據段只是爲了訪問數據的話只要2個GDT就足夠了,因爲並沒有重裝代碼段,這裏給出3個GDT只是爲了完整性。
通常在進入保護模式時要關閉所有的中斷,把IDTR的界限設置爲0,CPU自動關閉所有中斷,包括NMI,返回實模式後恢復IDTR並開中斷。另外A20地址線的控制對於正確訪問整個內存也很重要,在進入保護模式前要讓8042打開A20地址線。
在這個例子裏FS段寄存器設成可訪問4GB內存的基址和界限,由於在DOS中很少有程序會用到GS、FS這兩個386增加的段寄存器,當要讀寫4GB範圍中的任一個地方都可通過FS段來達到,直到FS在實模式下被重裝入沖掉爲止。
這個例子在386SX、386DX、486上都運行通過。例子里加有十分詳細的註釋,由於這一程序是用BC 3.1編譯連接的,而其連接器不能爲DOS程序處理32位寄存器,所以直接在代碼中加入操作碼前綴0x66和地址前綴0x67,以便讓DOS實模式下的16位程序可用32位寄存器和地址。程序的右邊以註釋形式給出等效的32位指令。要注意16位的指令中mov al, byte ptr [BX]的指令碼正好是32位的指令mov al, byte ptr[EDI]。
讀者可用這個程序驗證BIOS是否同時在兩個區域出現。如果有線性定址能力的VESA顯示卡(如TVGA9440)還可進一步驗證線性顯示緩衝區在1MB之上的工作情況。
- #include <dos.h>
- unsigned long GDT-Table[]=
- {0,0, //NULL - 00H
- 0x0000FFFF,0x00CF9A00, //Code32 - 08h Base=0 Limit=4G-1 Size=4G
- 0x0000FFFF,0x00CF9200 //Data32 - 10h Base=0 Limit=4G-1 Size=4G
- };
- unsigned char OldIDT [6]={0}; //Save The IDTR before Enter Protect Mode.
- unsigned char pdescr-tmp [6]={0}; //NULL The IDTR s Limit=0 CPU will
- // disable all Interrupts, include NMI.
- #define KeyWait() {while(inportb(0x64) &2);}
- void A20Enable(void)
- {
- keyWait ();
- outportb(0x64,0xD1);
- KeyWait();
- outportb(0x60,0xDF); //Enable A20 with 8042.
- KeyWait();
- outportb(0x64,0xFF);
- KeyWait ();
- }
- void LoadFSLimit4G(void)
- {
- A20Enable (); //Enable A20
- //***
- Disable ints & Null IDT
- //***
- asm {
- CLI //Disable inerrupts
- SIDT OldIDT //Save OLD IDTR
- LIDT pdescr-tmp //Set up empty IDT.Disable any interrupts,
- } // Include NMI.
- //***
- Lodd GDTR
- //***
- asm{ // The right Code is Real, But BC++ s Linker NOT
- // Work with 32bits Code.
- db 0x66 //32 bit Operation Prefix in 16 Bit DOS.
- MOV CX,DS //MOV ECX,DS
- db 0x66 //Get Data segment physical Address
- SHL CX,4 //SHL ECX,4
- MOV word ptr pdescr-tmp [0],(3*8-1)
- //MOV word ptr pdescr-tmp [0], (3*8-1)
- db 0x66
- XOR AX,AX //XOR EAX,EAX
- MOV AX,offset GDT-Table
- // MOV AX,offset GDT-Table
- db 0x66
- ADD AX,CX //ADD EAX,ECX
- MOV word ptr pdescr-tmp [2], AX
- //GDTR Base low16 bits
- db 0x66
- SHR AX,16 //SHR EAX,16
- MOV word ptr pdescr-tmp [4],AX
- //GDTR Base high16 bits
- LGDT pdescr-tmp //Load GDTR
- }
- //****
- //* Enter 32 bit Flat Protected Mode
- //****
- asm{
- mov DX,0x10 // The Data32 Selector
- db 0x66,0x0F,0x20,0xC0 // MOV EAX,CR0
- db 0x66
- MOV BX,AX // MOV EBX,EAX
- OR AX,1
- db 0x66,0x0F,0x22,0xC0
- //MOV CRO,EAX // Set Protection enable bit
- JMP Flsuh
- } //Clear machine perform cache.
- flush: // Now In Flat Mode, But The CS is Real Mode Value.
- asm { //And it s attrib is 16Bit Code Segment.
- db 0x66
- MOV AX,BX //MOV EAX,EBX
- db 0x8E,0xE2 //MOV FS,DX
- //Load FS Base=0 Size=4G now
- db 0x66,0x0F,0x22,0xC0 //MOV CRO,EAX
- //Return Real Mode.
- LIDT OldIDT //LIDT OldIDT //Restore IDTR
- STI // STI //Enable INTR
- }
- }
- unsigned char ReadByte (unsigned long Address)
- {
- asm db 0x66
- asm mov di,word ptr Address // MOV EDI, Address
- asm db 0x67 //32 bit Address Prefix
- asm db 0x64 //FS:
- asm mov al,byte ptr [BX] // =MOV AL, FS: [EDI]
- return -AL;
- }
- unsigned char WriteByte(unsigned Long Address)
- {
- asm db 0x66
- asm mov di,word ptr Address //MOV EDI, Address
- asm db 0x67 //32 bit Address Prefix
- asm db 0x64 //FS:
- asm mov byte ptr [BX],al //=MOV FS: [EDI],AL
- return -AL;
- }
- //////// Don t Touch Above Code ///
- # include <stdio, h>
- void Dump4G (unsigned long Address)
- {
- int i;
- int j;
- for (i=0; i<20; i++)
- {
- printf (“%081X: ”, (Address+i*16));
- for (j=0; j<16;j++)
- printf ("% 02X" ,ReadByte (Address+i*16+j));
- printf (" ");
- for (j=0;j<16;j++)
- {
- if (ReadByte (Address+i*16+j) <0x20) printf (" . ");
- else printf (" %C ", ReadByte (Address+i*16+j));
- }
- printf ("/n");
- }
- }
- main ()
- {
- unsigned long Address=0;
- unsigned long tmp;
- LoadFSLimit4G ();
- printf ("====Designed By Southern. 1995.7.17====/n");
- printf (" Now you can Access The Machine All 4G Memory./n");
- printf (" Input the Start Memory Physical to DUMP. /n");
- printf (" Press D to Cuntinue DUMP, 0 to End & Quit, /n");
- do {
- printf ("-");
- scanf ("%IX", &tmp);
- if (tmp==0x0d) Address+=(20*16);
- else Address=tmp;
- Dump4G (Address);
- }while (Address !=0);
- return 0;
- }