導航:首頁 > 編程系統 > linuxljmp

linuxljmp

發布時間:2023-02-07 04:26:23

⑴ 內存映射的初始化

內核在進入保護模式前,還沒有啟用分頁功能,在這之前內核要先建立一個臨時內核頁表,因為在進入保護模式後,內核繼續初始化直到建
立完整的內存映射機制之前,仍然需要用到頁表來映射相應的內存地址。臨時頁表的初始化是在arch/i386/kernel/head.S中進行的:
swapper_pg_dir是臨時頁全局目錄表,它是在內核編譯過程中靜態初始化的.
pg0是第一個頁表開始的地方,它也是內核編譯過程中靜態初始化的.
內核通過以下代碼建立臨時頁表:
ENTRY(startup_32)
…………
/*得到開始目錄項的索引,從這可以看出內核是在swapper_pg_dir的768個表項開始進行建立的,其對應的線性地址就是0xc0000000以上的地
址,也就是內核在初始化它自己的頁表*/
page_pde_offset=(__PAGE_OFFSET>>20);
/*pg0地址在內核編譯的時候,已經是加上0xc0000000了,減去0xc00000000得到對應的物理地址*/
movl$(pg0-__PAGE_OFFSET),%edi
/*將目錄表的地址傳給edx,表明內核也要從0x00000000開始建立頁表,這樣可以保證從以物理地址取指令到以線性地址在系統空間取指令
的平穩過渡,下面會詳細解釋*/
movl$(swapper_pg_dir-__PAGE_OFFSET),%edx
movl$0x007,%eax
leal0x007(%edi),%ecx
Movl%ecx,(%edx)
movl%ecx,page_pde_offset(%edx)
addl$4,%edx
movl$1024,%ecx
11:
stosladdl$0x1000,%eax
loop11b
/*內核到底要建立多少頁表,也就是要映射多少內存空間,取決於這個判斷條件。在內核初始化程中內核只要保證能映射到包括內
核的代碼段,數據段,初始頁表和用於存放動態數據結構的128k大小的空間就行*/
leal(INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
cmpl%ebp,%eax
jb10b
movl%edi,(init_pg_tables_end-__PAGE_OFFSET)
在上述代碼中,內核為什麼要把用戶空間和內核空間的前幾個目錄項映射到相同的頁表中去呢,雖然在head.S中內核已經進入保護模式,但是
內核現在是處於保護模式的段式定址方式下,因為內核還沒有啟用分頁映射機制,現在都是以物理地址來取指令,如果代碼中遇到了符號地址
,只能減去0xc0000000才行,當開啟了映射機制後就不用了現在cpu中的取指令指針eip仍指向低區,如果只建立內核空間中的映射,那麼當
內核開啟映射機制後,低區中的地址就沒辦法定址了,應為沒有對應的頁表,除非遇到某個符號地址作為絕對轉移或調用子程序為止。因此
要盡快開啟CPU的頁式映射機制.
movl$swapper_pg_dir-__PAGE_OFFSET,%eax
movl%eax,%cr3/*cr3控制寄存器保存的是目錄表地址*/
movl%cr0,%eax/*向cr0的最高位置1來開啟映射機制*/
orl$0x80000000,%eax
movl%eax,%cr0
ljmp$__BOOT_CS,$1f/*Clearprefetchandnormalize%eip*/
1:
lssstack_start,%esp
通過ljmp$__BOOT_CS,$1f這條指令使CPU進入了系統空間繼續執行因為__BOOT_CS是個符號地址,地址在0xc0000000以上。
在head.S完成了內核臨時頁表的建立後,它繼續進行初始化,包括初始化INIT_TASK,也就是系統開啟後的第一個進程;建立完整的中斷處理程
序,然後重新載入GDT描述符,最後跳轉到init/main.c中的start_kernel函數繼續初始化.
3.3內核頁表的完整建立
內核在start_kernel()中繼續做第二階段的初始化,因為在這個階段中,內核已經處於保護模式下,前面只是簡單的設置了內核頁表,內核
必須首先要建立一個完整的頁表才能繼續運行,因為內存定址是內核繼續運行的前提。
pagetable_init()的代碼在mm/init.c中:
[start_kernel()>setup_arch()>paging_init()>pagetable_init()]
為了簡單起見,我忽略了對PAE選項的支持。
staticvoid__initpagetable_init(void)
{
……
pgd_t*pgd_base=swapper_pg_dir;
……
kernel_physical_mapping_init(pgd_base);
……
}
在這個函數中pgd_base變數指向了swapper_pg_dir,這正是內核目錄表的開始地址,pagetable_init()函數在通過
kernel_physical_mapping_init()函數完成內核頁表的完整建立。
kernel_physical_mapping_init函數同樣在mm/init.c中,我略去了與PAE模式相關的代碼:
staticvoid__initkernel_physical_mapping_init(pgd_t*pgd_base)
{
unsignedlongpfn;
pgd_t*pgd;
pmd_t*pmd;
pte_t*pte;
intpgd_idx,pmd_idx,pte_ofs;
pgd_idx=pgd_index(PAGE_OFFSET);
pgd=pgd_base+pgd_idx;
pfn=0;
for(;pgd_idx<PTRS_PER_PGD;pgd++,pgd_idx++){
pmd=one_md_table_init(pgd);
if(pfn>=max_low_pfn)
continue;
for(pmd_idx=0;pmd_idx<PTRS_PER_PMD&&pfn<max_low_pfn;pmd++,pmd_idx++){
unsignedintaddress=pfn*PAGE_SIZE+PAGE_OFFSET;
……
pte=one_page_table_init(pmd);
for(pte_ofs=0;pte_ofs<PTRS_PER_PTE&&pfn<max_low_pfn;pte++,pfn++,pte_ofs++){
if(is_kernel_text(address))
set_pte(pte,pfn_pte(pfn,PAGE_KERNEL_EXEC));
else
set_pte(pte,pfn_pte(pfn,PAGE_KERNEL));
……
}
}
通過作者的注釋,可以了解到這個函數的作用是把整個物理內存地址都映射到從內核空間的開始地址,即從0xc0000000的整個內核空間中,
直到物理內存映射完畢為止。這個函數比較長,而且用到很多關於內存管理方面的宏定義,理解了這個函數,就能大概理解內核是如何建立
頁表的,將這個抽象的模型完全的理解。下面將詳細分析這個函數:
函數開始定義了4個變數pgd_t*pgd,pmd_t*pmd,pte_t*pte,pfn;
pgd指向一個目錄項開始的地址,pmd指向一個中間目錄開始的地址,pte指向一個頁表開始的地址pfn是頁框號被初始為0.pgd_idx根據
pgd_index宏計算結果為768,也是內核要從目錄表中第768個表項開始進行設置。從768到1024這個256個表項被linux內核設置成內核目錄項,
低768個目錄項被用戶空間使用.pgd=pgd_base+pgd_idx;pgd便指向了第768個表項。
然後函數開始一個循環即開始填充從768到1024這256個目錄項的內容。
one_md_table_init()函數根據pgd找到指向的pmd表。
它同樣在mm/init.c中定義:
staticpmd_t*__initone_md_table_init(pgd_t*pgd)
{
pmd_t*pmd_table;
#ifdefCONFIG_X86_PAE
pmd_table=(pmd_t*)alloc_bootmem_low_pages(PAGE_SIZE);
set_pgd(pgd,__pgd(__pa(pmd_table)|_PAGE_PRESENT));
if(pmd_table!=pmd_offset(pgd,0))
BUG();
#else
pmd_table=pmd_offset(pgd,0);
#endif
returnpmd_table;
}
可以看出,如果內核不啟用PAE選項,函數將通過pmd_offset返回pgd的地址。因為linux的二級映射模型,本來就是忽略pmd中間目錄表的。
接著又個判斷語句:
>>if(pfn>=max_low_pfn)
>>continue;
這個很關鍵,max_low_pfn代表著整個物理內存一共有多少頁框。當pfn大於max_low_pfn的時候,表明內核已經把整個物理內存都映射到了系
統空間中,所以剩下有沒被填充的表項就直接忽略了。因為內核已經可以映射整個物理空間了,沒必要繼續填充剩下的表項。
緊接著的第2個for循環,在linux的3級映射模型中,是要設置pmd表的,但在2級映射中忽略,只循環一次,直接進行頁表pte的設置。
>>address=pfn*PAGE_SIZE+PAGE_OFFSET;
address是個線性地址,根據上面的語句可以看出address是從0xc000000開始的,也就是從內核空間開始,後面在設置頁表項屬性的時候會用
到它.
>>pte=one_page_table_init(pmd);
根據pmd分配一個頁表,代碼同樣在mm/init.c中:
staticpte_t*__initone_page_table_init(pmd_t*pmd)
{
if(pmd_none(*pmd)){
pte_t*page_table=(pte_t*)alloc_bootmem_low_pages(PAGE_SIZE);
set_pmd(pmd,__pmd(__pa(page_table)|_PAGE_TABLE));
if(page_table!=pte_offset_kernel(pmd,0))
BUG();
returnpage_table;
}
returnpte_offset_kernel(pmd,0);
}
pmd_none宏判斷pmd表是否為空,如果為空則要利用alloc_bootmem_low_pages分配一個4k大小的物理頁面。然後通過set_pmd(pmd,__pmd
(__pa(page_table)|_PAGE_TABLE));來設置pmd表項。page_table顯然屬於線性地址,先通過__pa宏轉化為物理地址,在與上_PAGE_TABLE宏,
此時它們還是無符號整數,在通過__pmd把無符號整數轉化為pmd類型,經過這些轉換,就得到了一個具有屬性的表項,然後通過set_pmd宏設
置pmd表項.
接著又是一個循環,設置1024個頁表項。
is_kernel_text函數根據前面提到的address來判斷address線性地址是否屬於內核代碼段,它同樣在mm/init.c中定義:
staticinlineintis_kernel_text(unsignedlongaddr)
{
if(addr>=(unsignedlong)_stext&&addr<=(unsignedlong)__init_end)
return1;
return0;
}
_stext,__init_end是個內核符號,在內核鏈接的時候生成的,分別表示內核代碼段的開始和終止地址.
如果address屬於內核代碼段,那麼在設置頁表項的時候就要加個PAGE_KERNEL_EXEC屬性,如果不是,則加個PAGE_KERNEL屬性.
#define_PAGE_KERNEL_EXEC
(_PAGE_PRESENT|_PAGE_RW|_PAGE_DIRTY|_PAGE_ACCESSED)
#define_PAGE_KERNEL
(_PAGE_PRESENT|_PAGE_RW|_PAGE_DIRTY|_PAGE_ACCESSED|_PAGE_NX)
最後通過set_pte(pte,pfn_pte(pfn,PAGE_KERNEL));來設置頁表項,先通過pfn_pte宏根據頁框號和頁表項的屬性值合並成一個頁表項值,
然戶在用set_pte宏把頁表項值寫到頁表項里。
當pagetable_init()函數返回後,內核已經設置好了內核頁表,緊著調用load_cr3(swapper_pg_dir);
#defineload_cr3(pgdir)
asmvolatile(movl%0,%%cr3::r(__pa(pgdir)))
將控制swapper_pg_dir送入控制寄存器cr3.每當重新設置cr3時,CPU就會將頁面映射目錄所在的頁面裝入CPU內部高速緩存中的TLB部分.現
在內存中(實際上是高速緩存中)的映射目錄變了,就要再讓CPU裝入一次。由於頁面映射機制本來就是開啟著的,所以從這條指令以後就擴大
了系統空間中有映射區域的大小,使整個映射覆蓋到整個物理內存(高端內存)除外.實際上此時swapper_pg_dir中已經改變的目錄項很可能還
在高速緩存中,所以還要通過__flush_tlb_all()將高速緩存中的內容沖刷到內存中,這樣才能保證內存中映射目錄內容的一致性。
3.4對如何構建頁表的總結
通過上述對pagetable_init()的剖析,我們可以清晰的看到,構建內核頁表,無非就是向相應的表項寫入下一級地址和屬性。在內核空間
保留著一部分內存專門用來存放內核頁表.當cpu要進行定址的時候,無論在內核空間,還是在用戶空間,都會通過這個頁表來進行映射。對於
這個函數,內核把整個物理內存空間都映射完了,當用戶空間的進程要使用物理內存時,豈不是不能做相應的映射了?其實不會的,內核
只是做了映射,映射不代表使用,這樣做是內核為了方便管理內存而已。

⑵ 單片機是什麼意思。

單片機定義
[編輯本段]

單片機是指一個集成在一塊晶元上的完整計算機系統。盡管他的大部分功能集成在一塊小晶元上,但是它具有一個完整計算機所需要的大部分部件:CPU、內存、內部和外部匯流排系統,目前大部分還會具有外存。同時集成諸如通訊介面、定時器,實時時鍾等外圍設備。而現在最強大的單片機系統甚至可以將聲音、圖像、網路、復雜的輸入輸出系統集成在一塊晶元上。

單片機也被稱為微控制器(Microcontroler),是因為它最早被用在工業控制領域。單片機由晶元內僅有CPU的專用處理器發展而來。最早的設計理念是通過將大量外圍設備和CPU集成在一個晶元中,使計算機系統更小,更容易集成進復雜的而對提及要求嚴格的控制設備當中。INTEL的Z80是最早按照這種思想設計出的處理器,從此以後,單片機和專用處理器的發展便分道揚鑣。

早期的單片機都是8位或4位的。其中最成功的是INTEL的8031,因為簡單可靠而性能不錯獲得了很大的好評。此後在8031上發展出了MCS51系列單片機系統。基於這一系統的單片機系統直到現在還在廣泛使用。隨著工業控制領域要求的提高,開始出現了16位單片機,但因為性價比不理想並未得到很廣泛的應用。90年代後隨著消費電子產品大發展,單片機技術得到了巨大的提高。隨著INTEL i960系列特別是後來的ARM系列的廣泛應用,32位單片機迅速取代16位單片機的高端地位,並且進入主流市場。而傳統的8位單片機的性能也得到了飛速提高,處理能力比起80年代提高了數百倍。目前,高端的32位單片機主頻已經超過300MHz,性能直追90年代中期的專用處理器,而普通的型號出廠價格跌落至1美元,最高端的型號也只有10美元。當代單片機系統已經不再只在裸機環境下開發和使用,大量專用的嵌入式操作系統被廣泛應用在全系列的單片機上。而在作為掌上電腦和手機核心處理的高端單片機甚至可以直接使用專用的Windows和Linux操作系統。

單片機比專用處理器最適合應用於嵌入式系統,因此它得到了最多的應用。事實上單片機是世界上數量最多的計算機。現代人類生活中所用的幾乎每件電子和機械產品中都會集成有單片機。手機、電話、計算器、家用電器、電子玩具、掌上電腦以及滑鼠等電腦配件中都配有1-2部單片機。而個人電腦中也會有為數不少的單片機在工作。汽車上一般配備40多部單片機,復雜的工業控制系統上甚至可能有數百台單片機在同時工作!單片機的數量不僅遠超過PC機和其他計算的綜合,甚至比人類的數量還要多。

單片機介紹
[編輯本段]

單片機又稱單片微控制器,它不是完成某一個邏輯功能的晶元,而是把一個計算機系統集成到一個晶元上。概括的講:一塊晶元就成了一台計算機。它的體積小、質量輕、價格便宜、為學習、應用和開發提供了便利條件。同時,學習使用單片機是了解計算機原理與結構的最佳選擇。
單片機內部也用和電腦功能類似的模塊,比如CPU,內存,並行匯流排,還有和硬碟作用相同的存儲器件,不同的是它的這些部件性能都相對我們的家用電腦弱很多,不過價錢也是低的,一般不超過10元即可......用它來做一些控制電器一類不是很復雜的工作足矣了。我們現在用的全自動滾筒洗衣機、排煙罩、VCD等等的家電裡面都可以看到它的身影!......它主要是作為控制部分的核心部件。
它是一種在線式實時控制計算機,在線式就是現場控制,需要的是有較強的抗干擾能力,較低的成本,這也是和離線式計算機的(比如家用PC)的主要區別。

單片機是靠程序的,並且可以修改。通過不同的程序實現不同的功能,尤其是特殊的獨特的一些功能,這是別的器件需要費很大力氣才能做到的,有些則是花大力氣也很難做到的。一個不是很復雜的功能要是用美國50年代開發的74系列,或者60年代的CD4000系列這些純硬體來搞定的話,電路一定是一塊大PCB板!但是如果要是用美國70年代成功投放市場的系列單片機,結果就會有天壤之別!只因為單片機的通過你編寫的程序可以實現高智能,高效率,以及高可靠性!

由於單片機對成本是敏感的,所以目前占統治地位的軟體還是最低級匯編語言,它是除了二進制機器碼以上最低級的語言了,既然這么低級為什麼還要用呢?很多高級的語言已經達到了可視化編程的水平為什麼不用呢?原因很簡單,就是單片機沒有家用計算機那樣的CPU,也沒有像硬碟那樣的海量存儲設備。一個可視化高級語言編寫的小程序裡面即使只有一個按鈕,也會達到幾十K的尺寸!對於家用PC的硬碟來講沒什麼,可是對於單片機來講是不能接受的。 單片機在硬體資源方面的利用率必須很高才行,所以匯編雖然原始卻還是在大量使用。一樣的道理,如果把巨型計算機上的操作系統和應用軟體拿到家用PC上來運行,家用PC的也是承受不了的。

可以說,二十世紀跨越了三個「電」的時代,即電氣時代、電子時代和現已進入的電腦時代。不過,這種電腦,通常是指個人計算機,簡稱PC機。它由主機、鍵盤、顯示器等組成。還有一類計算機,大多數人卻不怎麼熟悉。這種計算機就是把智能賦予各種機械的單片機(亦稱微控制器)。顧名思義,這種計算機的最小系統只用了一片集成電路,即可進行簡單運算和控制。因為它體積小,通常都藏在被控機械的「肚子」里。它在整個裝置中,起著有如人類頭腦的作用,它出了毛病,整個裝置就癱瘓了。現在,這種單片機的使用領域已十分廣泛,如智能儀表、實時工控、通訊設備、導航系統、家用電器等。各種產品一旦用上了單片機,就能起到使產品升級換代的功效,常在產品名稱前冠以形容詞——「智能型」,如智能型洗衣機等。現在有些工廠的技術人員或其它業余電子開發者搞出來的某些產品,不是電路太復雜,就是功能太簡單且極易被仿製。究其原因,可能就卡在產品未使用單片機或其它可編程邏輯器件上。

單片機歷史

單片機誕生於20世紀70年代末,經歷了SCM、MCU、SoC三大階段。
1.SCM即單片微型計算機(Single Chip Microcomputer)階段,主要是尋求最佳的單片形態嵌入式系統的最佳體系結構。「創新模式」獲得成功,奠定了SCM與通用計算機完全不同的發展道路。在開創嵌入式系統獨立發展道路上,Intel公司功不可沒。
2.MCU即微控制器(Micro Controller Unit)階段,主要的技術發展方向是:不斷擴展滿足嵌入式應用時,對象系統要求的各種外圍電路與介面電路,突顯其對象的智能化控制能力。它所涉及的領域都與對象系統相關,因此,發展MCU的重任不可避免地落在電氣、電子技術廠家。從這一角度來看,Intel逐漸淡出MCU的發展也有其客觀因素。在發展MCU方面,最著名的廠家當數Philips公司。
Philips公司以其在嵌入式應用方面的巨大優勢,將MCS-51從單片微型計算機迅速發展到微控制器。因此,當我們回顧嵌入式系統發展道路時,不要忘記Intel和Philips的歷史功績。
3.單片機是嵌入式系統的獨立發展之路,向MCU階段發展的重要因素,就是尋求應用系統在晶元上的最大化解決;因此,專用單片機的發展自然形成了SoC化趨勢。隨著微電子技術、IC設計、EDA工具的發展,基於SoC的單片機應用系統設計會有較大的發展。因此,對單片機的理解可以從單片微型計算機、單片微控制器延伸到單片應用系統。

單片機的應用領域
[編輯本段]

目前單片機滲透到我們生活的各個領域,幾乎很難找到哪個領域沒有單片機的蹤跡。導彈的導航裝置,飛機上各種儀表的控制,計算機的網路通訊與數據傳輸,工業自動化過程的實時控制和數據處理,廣泛使用的各種智能IC卡,民用豪華轎車的安全保障系統,錄象機、攝象機、全自動洗衣機的控制,以及程式控制玩具、電子寵物等等,這些都離不開單片機。更不用說自動控制領域的機器人、智能儀表、醫療器械了。因此,單片機的學習、開發與應用將造就一批計算機應用與智能化控制的科學家、工程師。

單片機廣泛應用於儀器儀表、家用電器、醫用設備、航空航天、專用設備的智能化管理及過程式控制制等領域,大致可分如下幾個范疇:

1.在智能儀器儀表上的應用
單片機具有體積小、功耗低、控制功能強、擴展靈活、微型化和使用方便等優點,廣泛應用於儀器儀表中,結合不同類型的感測器,可實現諸如電壓、功率、頻率、濕度、溫度、流量、速度、厚度、角度、長度、硬度、元素、壓力等物理量的測量。採用單片機控制使得儀器儀表數字化、智能化、微型化,且功能比起採用電子或數字電路更加強大。例如精密的測量設備(功率計,示波器,各種分析儀)。

2.在工業控制中的應用
用單片機可以構成形式多樣的控制系統、數據採集系統。例如工廠流水線的智能化管理,電梯智能化控制、各種報警系統,與計算機聯網構成二級控制系統等。

3.在家用電器中的應用
可以這樣說,現在的家用電器基本上都採用了單片機控制,從電飯褒、洗衣機、電冰箱、空調機、彩電、其他音響視頻器材、再到電子秤量設備,五花八門,無所不在。

4.在計算機網路和通信領域中的應用
現代的單片機普遍具備通信介面,可以很方便地與計算機進行數據通信,為在計算機網路和通信設備間的應用提供了極好的物質條件,現在的通信設備基本上都實現了單片機智能控制,從手機,電話機、小型程式控制交換機、樓宇自動通信呼叫系統、列車無線通信、再到日常工作中隨處可見的行動電話,集群移動通信,無線電對講機等。

5.單片機在醫用設備領域中的應用
單片機在醫用設備中的用途亦相當廣泛,例如醫用呼吸機,各種分析儀,監護儀,超聲診斷設備及病床呼叫系統等等。

此外,單片機在工商,金融,科研、教育,國防航空航天等領域都有著十分廣泛的用途。

學習應中六大重要部分
[編輯本段]

單片機學習應中的六大重要部分

一、匯流排:我們知道,一個電路總是由元器件通過電線連接而成的,在模擬電路中,連線並不成為一個問題,因為各器件間一般是串列關系,各器件之間的連線並不很多,但計算機電路卻不一樣,它是以微處理器為核心,各器件都要與微處理器相連,各器件之間的工作必須相互協調,所以就需要的連線就很多了,如果仍如同模擬電路一樣,在各微處理器和各器件間單獨連線,則線的數量將多得驚人,所以在微處理機中引入了匯流排的概念,各個器件共同享用連線,所有器件的8根數據線全部接到8根公用的線上,即相當於各個器件並聯起來,但僅這樣還不行,如果有兩個器件同時送出數據,一個為0,一個為1,那麼,接收方接收到的究竟是什麼呢?這種情況是不允許的,所以要通過控制線進行控制,使器件分時工作,任何時候只能有一個器件發送數據(可以有多個器件同時接收)。器件的數據線也就被稱為數據匯流排,器件所有的控制線被稱為控制匯流排。在單片機內部或者外部存儲器及其它器件中有存儲單元,這些存儲單元要被分配地址,才能使用,分配地址當然也是以電信號的形式給出的,由於存儲單元比較多,所以,用於地址分配的線也較多,這些線被稱為地址匯流排。
二、數據、地址、指令:之所以將這三者放在一起,是因為這三者的本質都是一樣的——數字,或者說都是一串『0』和『1』組成的序列。換言之,地址、指令也都是數據。指令:由單片機晶元的設計者規定的一種數字,它與我們常用的指令助記符有著嚴格的一一對應關系,不可以由單片機的開發者更改。地址:是尋找單片機內部、外部的存儲單元、輸入輸出口的依據,內部單元的地址值已由晶元設計者規定好,不可更改,外部的單元可以由單片機開發者自行決定,但有一些地址單元是一定要有的(詳見程序的執行過程)。數據:這是由微處理機處理的對象,在各種不同的應用電路中各不相同,一般而言,被處理的數據可能有這么幾種情況:
1•地址(如MOV DPTR,#1000H),即地址1000H送入DPTR。
2•方式字或控制字(如MOV TMOD,#3),3即是控制字。
3•常數(如MOV TH0,#10H)10H即定時常數。
4•實際輸出值(如P1口接彩燈,要燈全亮,則執行指令:MOV P1,#0FFH,要燈全暗,則執行指令:MOV P1,#00H)這里0FFH和00H都是實際輸出值。又如用於LED的字形碼,也是實際輸出的值。
理解了地址、指令的本質,就不難理解程序運行過程中為什麼會跑飛,會把數據當成指令來執行了。
三、P0口、P2口和P3的第二功能用法:初學時往往對P0口、P2口和P3口的第二功能用法迷惑不解,認為第二功能和原功能之間要有一個切換的過程,或者說要有一條指令,事實上,各埠的第二功能完全是自動的,不需要用指令來轉換。如P3.6、P3.7分別是WR、RD信號,當微片理機外接RAM或有外部I/O口時,它們被用作第二功能,不能作為通用I/O口使用,只要一微處理機一執行到MOVX指令,就會有相應的信號從P3.6或P3.7送出,不需要事先用指令說明。事實上『不能作為通用I/O口使用』也並不是『不能』而是(使用者)『不會』將其作為通用I/O口使用。你完全可以在指令中按排一條SETB P3.7的指令,並且當單片機執行到這條指令時,也會使P3.7變為高電平,但使用者不會這么去做,因為這通常這會導致系統的崩潰。
四、程序的執行過程: 單片機在通電復位後8051內的程序計數器(PC)中的值為『0000』,所以程序總是從『0000』單元開始執行,也就是說:在系統的ROM中一定要存在『0000』這個單元,並且在『0000』單元中存放的一定是一條指令。
五、堆棧: 堆棧是一個區域,是用來存放數據的,這個區域本身沒有任何特殊之處,就是內部RAM的一部份,特殊的是它存放和取用數據的方式,即所謂的『先進後出,後進先出』,並且堆棧有特殊的數據傳輸指令,即『PUSH』和『POP』,有一個特殊的專為其服務的單元,即堆棧指針SP,每當執一次PUSH指令時,SP就(在原來值的基礎上)自動加1,每當執行一次POP指令,SP就(在原來值的基礎上)自動減1。由於SP中的值可以用指令加以改變,所以只要在程序開始階段更改了SP的值,就可以把堆棧設置在規定的內存單元中,如在程序開始時,用一條MOV SP,#5FH指令,就時把堆棧設置在從內存單元60H開始的單元中。一般程序的開頭總有這么一條設置堆棧指針的指令,因為開機時,SP的初始值為07H,這樣就使堆棧從08H單元開始往後,而08H到1FH這個區域正是8031的第二、三、四工作寄存器區,經常要被使用,這會造成數據的渾亂。不同作者編寫程序時,初始化堆棧指令也不完全相同,這是作者的習慣問題。當設置好堆棧區後,並不意味著該區域成為一種專用內存,它還是可以象普通內存區域一樣使用,只是一般情況下編程者不會把它當成普通內存用了。
六、單片機的開發過程: 這里所說的開發過程並不是一般書中所說的從任務分析開始,我們假設已設計並製作好硬體,下面就是編寫軟體的工作。在編寫軟體之前,首先要確定一些常數、地址,事實上這些常數、地址在設計階段已被直接或間接地確定下來了。如當某器件的連線設計好後,其地址也就被確定了,當器件的功能被確定下來後,其控制字也就被確定了。然後用文本編輯器(如EDIT、CCED等)編寫軟體,編寫好後,用編譯器對源程序文件編譯,查錯,直到沒有語法錯誤,除了極簡單的程序外,一般應用模擬機對軟體進行調試,直到程序運行正確為止。運行正確後,就可以寫片(將程序固化在EPROM中)。在源程序被編譯後,生成了擴展名為HEX的目標文件,一般編程器能夠識別這種格式的文件,只要將此文件調入即可寫片。在此,為使大家對整個過程有個認識,舉一例說明:
ORG 0000H
LJMP START
ORG 040H
START:
MOV SP,#5FH ;設堆棧
LOOP:
NOP
LJMP LOOP ;循環
END ;結束

單片機學習
[編輯本段]

目前,很多人對匯編語言並不認可。可以說,掌握用C語言單片機編程很重要,可以大大提高開發的效率。不過初學者可以不了解單片機的匯編語言,但一定要了解單片機具體性能和特點,不然在單片機領域是比較致命的。如果不考慮單片機硬體資源,在KEIL中用C胡亂編程,結果只能是出了問題無法解決!可以肯定的說,最好的C語言單片機工程師都是從匯編走出來的編程者因為單片機的C語言雖然是高級語言,但是它不同於台式機個人電腦上的VC++什麼的單片機的硬體資源不是非常強大,不同於我們用VC、VB等高級語言在台式PC上寫程序畢竟台式電腦的硬體非常強大,所以才可以不考慮硬體資源的問題。

以8051單片機為例講解單片機的引腳及相關功能;

《單片機引腳圖》

40個引腳按引腳功能大致可分為4個種類:電源、時鍾、控制和I/O引腳。

⒈ 電源:

⑴ VCC - 晶元電源,接+5V;
⑵ VSS - 接地端;

註:用萬用表測試單片機引腳電流一般為0v或者5v,這是標準的TTL電平,但有時候在單片機程序正在工作時候測試結果並不是這個值而是介於0v-5v之間,其實這之是萬用表反映沒這么快而已,在某一個瞬間單片機引腳電流還是保持在0v或者5v的。

⒉ 時鍾:XTAL1、XTAL2 - 晶體振盪電路反相輸入端和輸出端。

⒊ 控制線:控制線共有4根,

⑴ ALE/PROG:地址鎖存允許/片內EPROM編程脈沖
① ALE功能:用來鎖存P0口送出的低8位地址
② PROG功能:片內有EPROM的晶元,在EPROM編程期間,此引腳輸入編程脈沖。

⑵ PSEN:外ROM讀選通信號。

⑶ RST/VPD:復位/備用電源。

① RST(Reset)功能:復位信號輸入端。

② VPD功能:在Vcc掉電情況下,接備用電源。

⑷ EA/Vpp:內外ROM選擇/片內EPROM編程電源。

① EA功能:內外ROM選擇端。

② Vpp功能:片內有EPROM的晶元,在EPROM編程期間,施加編程電源Vpp。

⒋ I/O線

80C51共有4個8位並行I/O埠:P0、P1、P2、P3口,共32個引腳。
P3口還具有第二功能,用於特殊信號輸入輸出和控制信號(屬控制匯流排)

⑶ 什麼是虛擬地址保護模式

保護模式
(Protected Mode,或有時簡寫為 pmode) 是一種 80286 系列和之後的 x86 兼容 CPU 操作模式。保護模式有一些新的特色,設計用來增強 多工 和系統穩定度,像是 內存保護,分頁 系統,以及硬體支援的 虛擬內存。大部分的現今 x86 操作系統 都在保護模式下運行,包含 Linux、FreeBSD、以及 微軟 Windows 2.0 和之後版本

另外一種 286 和其之後 CPU 的操作模式是 真實模式,一種向前兼容且關閉這些特色的模式。設計用來讓新的晶元可以執行舊的軟體。依照設計的規格,所有的 x86 CPU 都是在真實模式下開機來確保傳統操作系統的向前兼容性。在任何保護模式的特色可用前,他們必須要由某些程序手動地切換到保護模式。在現今的電腦,這種切換通常是由 操作系統 在開機時候必須完成的第一件工作的一個。它也可能當 CPU 在保護模式下運行時,使用 虛擬86模式 來執行設計給真實模式的程序碼。

盡管用軟體的方式也有某些可能在真實模式的系統下使用多工,但保護模式下內存保護的特色,可以避免有問題的程序破壞其他工作或是 操作系統 核心所擁有的內存。保護模式也有中斷正在執行程序的硬體支援,可以把 execution content 交給其他工作,得以實現 先佔式多工。

大部分可以使用保護模式的 CPU 也擁有 32 位元暫存器 的特色 (例如 80386 系列和其後任何的晶元),導入了融合保護模式而成為 32 位元處理的概念。80286 晶元雖有支援保護模式,但是仍然只有 16 位元暫存器。Windows 2.0 和之後版本中的保護模式增強稱為 "386 增強模式",是因為他們除了保護模式外,還需要 32 位元的暫存器,並且無法在 286 上面執行 (即使 286 支援保護模式)。

即使在 32 位元晶元上已經打開了保護模式,但是 1 MB 以上的內存並無法存取,是由於一種仿照 IBM XT 系統設計特性的 memory wrap-around(內存連續) 的因素。這種限制可以由打開 A20 line 來迴避。

在保護模式下,前面 32 個中斷都是保留給 CPU 例外處理用。舉個例子,中斷 0D (十進制 13) 是 一般保護模式錯物 和 中斷 00 是 除以零。

在8086/8088時代,處理器只存在一種操作模式(Operation Mode),當時由於不存在其它操作模式,因此這種模式也沒有被命名。自從80286到80386開始,處理器增加了另外兩種操作模式——保護模式PM (Protected Mode)和系統管理模式SMM(System Management Mode),因此,8086/8088的模式被命名為實地址模式RM(Real-address Mode)。
PM是處理器的native模式,在這種模式下,處理器支持所有的指令和所有的體系結構特性,提供最高的性能和兼容性。對於所有的新型應用程序和操作系統來說,建議都使用這種模式。為了保證PM的兼容性,處理器允許在受保護的,多任務的環境下執行RM程序。這個特性被稱做虛擬8086模式(Virtual -8086 Mode),盡管它並不是一個真正的處理器模式。Virtual-8086模式實際上是一個PM的屬性,任何任務都可以使用它。
RM提供了Intel 8086處理器的編程環境,另外有一些擴展(比如切換到PM或SMM的能力)。當主機被Power-up或Reset後,處理器處於RM下。
SMM是一個對所有Intel處理器都統一的標准體系結構特性。出現於Intel386 SL晶元。這個模式為OS實現平台指定的功能(比如電源管理或系統安全)提供了一種透明的機制。當外部的SMM interrupt pin(SMI#)被激活或者從APIC(Advanced Programming Interrupt Controller)收到一個SMI,處理器將進入SMM。在SMM下,當保存當前正在運行程序的整個上下文(Context)時,處理器切換到一個分離的地址空間。然後SMM指定的代碼或許被透明的執行。當從SMM返回時,處理器將回到被系統管理中斷之前的狀態。

由於機器在Power-up或Reset之後,處理器處於RM狀態,而對於Intel 80386以及其後的晶元,只有使用PM才能發揮出最大的作用。所以我們就面臨著一個從RM切換到PM的問題。
本文不討論SMM,本節的重點集中於在Booting階段如何從RM切換到PM,這里不會過多的討論PM的細節,因為《Intel Architecture Software Developer』s Manual Volume 3: System Programming》中有非常詳盡和准確的介紹。
1. What is GDT
在Protected Mode下,一個重要的必不可少的數據結構就是GDT(Global Descriptor Table)。
為什麼要有GDT?我們首先考慮一下在Real Mode下的編程模型:
在Real Mode下,我們對一個內存地址的訪問是通過Segment:Offset的方式來進行的,其中Segment是一個段的Base Address,一個Segment的最大長度是64 KB,這是16-bit系統所能表示的最大長度。而Offset則是相對於此Segment Base Address的偏移量。Base Address+Offset就是一個內存絕對地址。由此,我們可以看出,一個段具備兩個因素:Base Address和Limit(段的最大長度),而對一個內存地址的訪問,則是需要指出:使用哪個段?以及相對於這個段Base Address的Offset,這個Offset應該小於此段的Limit。當然對於16-bit系統,Limit不要指定,默認為最大長度64KB,而 16-bit的Offset也永遠不可能大於此Limit。我們在實際編程的時候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)來指定Segment,CPU將段積存器中的數值向左偏移4-bit,放到20-bit的地址線上就成為20-bit的Base Address。
到了Protected Mode,內存的管理模式分為兩種,段模式和頁模式,其中頁模式也是基於段模式的。也就是說,Protected Mode的內存管理模式事實上是:純段模式和段頁式。進一步說,段模式是必不可少的,而頁模式則是可選的——如果使用頁模式,則是段頁式;否則這是純段模式。
既然是這樣,我們就先不去考慮頁模式。對於段模式來講,訪問一個內存地址仍然使用Segment:Offset的方式,這是很自然的。由於 Protected Mode運行在32-bit系統上,那麼Segment的兩個因素:Base Address和Limit也都是32位的。IA-32允許將一個段的Base Address設為32-bit所能表示的任何值(Limit則可以被設為32-bit所能表示的,以2^12為倍數的任何指),而不象Real Mode下,一個段的Base Address只能是16的倍數(因為其低4-bit是通過左移運算得來的,只能為0,從而達到使用16-bit段寄存器表示20-bit Base Address的目的),而一個段的Limit只能為固定值64 KB。另外,Protected Mode,顧名思義,又為段模式提供了保護機制,也就說一個段的描述符需要規定對自身的訪問許可權(Access)。所以,在Protected Mode下,對一個段的描述則包括3方面因素:[Base Address, Limit, Access],它們加在一起被放在一個64-bit長的數據結構中,被稱為段描述符。這種情況下,如果我們直接通過一個64-bit段描述符來引用一個段的時候,就必須使用一個64-bit長的段積存器裝入這個段描述符。但Intel為了保持向後兼容,將段積存器仍然規定為16-bit(盡管每個段積存器事實上有一個64-bit長的不可見部分,但對於程序員來說,段積存器就是16-bit的),那麼很明顯,我們無法通過16-bit長度的段積存器來直接引用64-bit的段描述符。
怎麼辦?解決的方法就是把這些長度為64-bit的段描述符放入一個數組中,而將段寄存器中的值作為下標索引來間接引用(事實上,是將段寄存器中的高13 -bit的內容作為索引)。這個全局的數組就是GDT。事實上,在GDT中存放的不僅僅是段描述符,還有其它描述符,它們都是64-bit長,我們隨後再討論。
GDT可以被放在內存的任何位置,那麼當程序員通過段寄存器來引用一個段描述符時,CPU必須知道GDT的入口,也就是基地址放在哪裡,所以Intel的設計者門提供了一個寄存器GDTR用來存放GDT的入口地址,程序員將GDT設定在內存中某個位置之後,可以通過LGDT指令將GDT的入口地址裝入此積存器,從此以後,CPU就根據此積存器中的內容作為GDT的入口來訪問GDT了。
GDT是Protected Mode所必須的數據結構,也是唯一的——不應該,也不可能有多個。另外,正象它的名字(Global Descriptor Table)所揭示的,它是全局可見的,對任何一個任務而言都是這樣。
除了GDT之外,IA-32還允許程序員構建與GDT類似的數據結構,它們被稱作LDT(Local Descriptor Table),但與GDT不同的是,LDT在系統中可以存在多個,並且從LDT的名字可以得知,LDT不是全局可見的,它們只對引用它們的任務可見,每個任務最多可以擁有一個LDT。另外,每一個LDT自身作為一個段存在,它們的段描述符被放在GDT中。
IA-32為LDT的入口地址也提供了一個寄存器LDTR,因為在任何時刻只能有一個任務在運行,所以LDT寄存器全局也只需要有一個。如果一個任務擁有自身的LDT,那麼當它需要引用自身的LDT時,它需要通過LLDT將其LDT的段描述符裝入此寄存器。LLDT指令與LGDT指令不同的時,LGDT指令的操作數是一個32-bit的內存地址,這個內存地址處存放的是一個32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的操作數是一個16-bit的選擇子,這個選擇子主要內容是:被裝入的LDT的段描述符在GDT中的索引值——這一點和剛才所討論的通過段積存器引用段的模式是一樣的。

LDT只是一個可選的數據結構,你完全可以不用它。使用它或許可以帶來一些方便性,但同時也帶來復雜性,如果你想讓你的OS內核保持簡潔性,以及可移植性,則最好不要使用它。
引用GDT和LDT中的段描述符所描述的段,是通過一個16-bit的數據結構來實現的,這個數據結構叫做Segment Selector——段選擇子。它的高13位作為被引用的段描述符在GDT/LDT中的下標索引,bit 2用來指定被引用段描述符被放在GDT中還是到LDT中,bit 0和bit 1是RPL——請求特權等級,被用來做保護目的,我們這里不詳細討論它。

前面所討論的裝入段寄存器中作為GDT/LDT索引的就是Segment Selector,當需要引用一個內存地址時,使用的仍然是Segment:Offset模式,具體操作是:在相應的段寄存器裝入Segment Selector,按照這個Segment Selector可以到GDT或LDT中找到相應的Segment Descriptor,這個Segment Descriptor中記錄了此段的Base Address,然後加上Offset,就得到了最後的內存地址。如下圖所示:

2. Setup GDT
由上一節的討論得知,GDT是Protected Mode所必須的數據結構,那麼我們在進入Protected Mode之前,必須設定好GDT,並通過LGDT將其裝入相應的寄存器。
盡管GDT允許被放在內存的任何位置,但由於GDT中的元素——描述符——都是64-bit長,也就是說都是8個位元組,所以為了讓CPU對GDT的訪問速度達到最快,我們應該將GDT的入口地址放在以8個位元組對齊,也就是說是8的倍數的地址位置。
GDT中第一個描述符必須是一個空描述符,也就是它的內容應該全部為0。如果引用這個描述符進行內存訪問,則是產生General Protection異常。
如果一個OS不使用虛擬內存,段模式會是一個不錯的選擇。但現代OS沒有不使用虛擬內存的,而實現虛擬內存的比較方便和有效的內存管理方式是頁式管理。但是在IA-32上如果我們想使用頁式管理,我們只能使用段頁式——沒有方法可以完全禁止段模式。但我們可以盡力讓段的效果降低的最小。
IA-32提供了一種被稱作「Basic Flat Model」的分段模式可以達到這種效果。這種模式要求在GDT中至少要定義兩個段描述符,一個用來引用Data Segment,另一個用來引用Code Segment。這2個Segment都包含整個線性空間,即Segment Limit = 4 GB,即使實際的物理內存遠沒有那麼多,但這個空間定義是為了將來由頁式管理來實現虛擬內存。
在這里,我們只是處於Booting階段,所以我們只需要初步設置一下GDT,等真正進入Protected Mode,啟動了OS Kernel之後,具體OS打算如何設置GDT,使用何種內存管理模式,由Kernel自身來設置,Booting只需要給Kernel的數據段和代碼段設置全部線性空間就可以了。
段描述符的格式如下圖所示:

具體到代碼段和數據段,它們的格式如下圖所示:

下面就是在Booting階段為進入Protected Mode而設置的臨時的gdt。這里定義了3個段描述符:第一個是系統規定的空描述符,第2個是引用4 GB線性空間的代碼段,第3個是引用4 GB線性空間的數據段。這是"Basic Flat Model"所要求的最下GDT設置,但就booting階段,只是為了進入Protected Mode,並為內核提供一個連續的,最大的線性空間這個目的而言,已經足夠了。
# Descriptor tables
gdt:
.word 0, 0, 0, 0 # mmy
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9A00 # code read/exec
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9200 # data read/write
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
3. Load GDT
設置好GDT之後,我們需要通過LGDT指令將設定的gdt的入口地址和gdt表的大小裝入GDTR寄存器。
GDTR寄存器包括兩部分:32-bit的線性基地址,以及16-bit的GDT大小(以位元組為單位)。需要注意的是,對於32-bit線性基地址,必須是32-bit絕對物理地址,而不是相對於某個段的偏移量。而我們在Booting階段,在進入Protected Mode之前,我們CS和DS設置很可能不是0,所以我們必須計算出gdt的絕對物理地址。
為了執行LGDT指令,你需要把這兩部分內容放在內存的某個位置,然後將這個位置的內存地址作為操作數傳遞給LGDT指令。然後LGDT指令會自動將保存在這個位置的這兩部分值裝入GDTR寄存器。
# 這是存放GDTR所需的兩部分內容的位置
gdt_48:
.word 0x8000 # gdt limit=2048,
# 256 GDT entries
.word 0, 0 # gdt base (filled in later)
# 下面這段代碼用來計算GDT的32-bit線性地址,並將其裝入GDTR寄存器。
xorl %eax, %eax # Compute gdt_base
movw %ds, %ax # (Convert %ds:gdt to a linear ptr)
shll , %eax
addl $gdt, %eax
movl %eax, (gdt_48+2)
lgdt gdt_48 # load gdt with whatever is appropriate
4. Other Preparing Stuff
在進入Protected Mode之前,除了需要設置和裝入GDT之外,還需要做如下一些事情:
屏蔽所有可屏蔽中斷;
裝入IDTR;
所有協處理器被正確的Reset。
由於在Real Mode和Protected Mode下的中斷處理機制有一些不同,所以在進入Protected Mode之前,務必禁止所有可屏蔽中斷,這可以通過下面兩種方法之一:
使用CLI指令;
對8259A可編程中斷控制器編程以屏蔽所有中斷。
即使當我們進入Protected Mode之後,也不能馬上將中斷打開,這時因為我們必須在OS Kernel中對相關的Protected Mode中斷處理所需的數據結構正確的初始化之後,才能打開中斷,否則會產生處理器異常。
在Real Mode下,中斷處理使用IVT(Interrupt Vector Table),在Protected Mode下,中斷處理使用IDT(Interrupt Descriptor Table),所以,我們必須在進入Protected Mode之前設置IDTR。
IDTR的格式和GDTR相同,IDTR的裝入方式和GDTR也相同。由於IDT中相關的中斷處理程序需要讓OS Kernel來設定,所以在Booting階段,我們只需要將IDTR中IDT的基地址和Size都設為0就可以了,隨後,等進入Protected Mode之後,由OS Kernel來真正設置它。
關於中斷機制和中斷處理,請參考 Interrupt & Exception ,這里不再贅述。
#
# 這是存放IDTR所需的兩部分內容的位置
#
idt_48:
.word 0 # idt limit = 0
.word 0, 0 # idt base = 0L
# 對於IDTR的處理,只需要這一條指令即可
lidt idt_48 # load idt with 0,0
#
# 通過設置8259A PIC,屏蔽所有可屏蔽中斷
#
movb xFF, %al # mask all interrupts for now
outb %al, xA1
call delay

movb xFB, %al # mask all irq's but irq2 which
outb %al, x21 # is cascaded
# 保證所有的協處理都被正確的Reset
xorw %ax, %ax
outb %al, xf0
call delay
outb %al, xf1
call delay
# Delay is needed after doing I/O
delay:
outb %al,x80
ret

5. Let's Go
好了,一切准備就緒,Fire!:)
進入Protected Mode,還是進入Real Mode,完全靠CR0寄存器的PE標志位來控制:如果PE=1,則CPU切換到PM,否則,則進入RM。

設置CR0-PE位的方法有兩種:
第一種是80286所使用的LMSW指令,後來的80386及更高型號的CPU為了保持向後兼容,都保留了這個指令。這個指令只能影響最低的4 bit,即PE,MP,EM和TS,對其它的沒有影響。
#
#通過LMSW指令進入Protected Mode
#
movw , %ax # protected mode (PE) bit
lmsw %ax # This is it!
第二種是Intel所建議的在80386以後的CPU上使用的進入PM的方式,即通過MOV指令。MOV指令可以設置CR0寄存器的所有域的值。
#
#通過MOV指令進入Protected Mode
#
movl %cr0, %eax
xorb , %al # set PE = 1
movl %eax, %cr0 # go!!
OK,現在已經進入Protected Mode了。
很簡單,right?But It's not over yet!

6. Start Kernel
我們已經從Real Mode進入Protected Mode,現在我們馬上就要啟動OS Kernel了。
OS Kernel運行在32-bit段模式,而當前我們卻仍然處於16-bit段模式。這是怎麼回事?為了了解這個問題,我們需要仔細探討一下IA-32的段模式的實現方法。
IA-32共提供了6個16-bit段寄存器:CS,DS,SS,ES,FS,GS。但事實上,這16-bit只是對程序員可見的部分,但每個寄存器仍然包括64-bit的不可見部分。

可見部分是為了供程序員裝載段寄存器,但一旦裝載完成,CPU真正使用的就只是不可見部分,可見部分就完全沒有用了。
不可見部分存放的內容是什麼?具體格式我沒有看到相關資料,但可以確定的是隱藏部分的內容和段描述符的內容是一致的(請參考段描述的格式),只不過格式可能不完全相同。但格式對我們理解這一點並不重要,因為程序員不可能能夠直接操作它。
我們以CS寄存器為例,對於其它寄存器也是一樣的:
在Real Mode下,當我們執行一個裝載CS寄存器的指令的時候(jmp,call,ret等),相關的值會被裝入CS寄存器的可見部分,但同時CPU也會根據可見部分的內容來設置不可見部分。比如我們執行"ljmp x1234, $go "之後,CS寄存器的可見部分的內容就是1234h,同時,不可見部分的32-bit Base Address域被設置為00001234h,20-bit的Limit域被設置為固定值10000h,也就是64 KB,Access Information部分的其它值我們不去考慮,只考慮其D/B位,由於執行此指令時處於Real Mode模式,所以D/B被設置為0,表示此段是一個16-bit段。當對CS寄存器的可見部分和不可見部分的內容都被設置之後,CS寄存器的裝載工作完成。隨後當CPU需要通過CS的內容進行地址運算的時候,則僅僅引用不可見部分。
在Protected Mode下,當我們執行一個裝載CS寄存器的指令的時候,段選擇子(Segment Selector)被裝入CS寄存器的可見部分,同時CPU根據此選擇子到相應的描述符表中(GDT或LDT)找到相應的段描述符並將其內容裝載入CS寄存器的不可見部分。隨後CPU當需要通過CS的內容進行地址運算的時候,也僅僅引用不可見部分。
從上面的描述可以看出,事實上CPU在引用段寄存器的內容進行地址運算時,Real Mode和Protected Mode是一致的。另外,也明白了為什麼我們在Real Mode下設置的段寄存器的內容到了Protected Mode下仍然引用的是16-bit段。
那麼我們如何將CS設置為引用32-bit段?方法就像我們前面所討論的,使用jmp或call指令,引用一個段選擇子,到GDT中裝載一個引用32-bit段的段描述符。
需要注意的是,如果CS寄存器的內容指出當前是一個16-bit段,那麼當前的地址模式也就是16-bit地址模式,這與你當前是出於Real Mode還是Protected Mode無關。而我們裝載32-bit段的jmp指令或call指令必須使用的是32-bit地址模式。而我們當前的boot部分代碼是16-bit代碼,所以我們必須在此jmp/call指令前加上地址轉換前綴代碼66h。
下面的例子就是使用jmp指令裝入32-bit段。Jmpi指令的含義是段間跳轉,其Opcode為Eah,其格式為:jmpi Offset, Segment Selector。
# 由於當前的代碼是16-bit代碼,而我們要執行32-bit地址模式的指令,指令前
# 需要有地址模式切換前綴66h,如果我們直接寫jmp指令,由編譯器來生成代碼
# 的話,是無法作到這一點的,所以我們直接寫相關數據。
.byte 0x66, 0xea # prefix + jmpi-opcode
.long 0x1000 # Offset
.word __KERNEL_CS # CS segment selector
上面的代碼相當於32-bit指令:
jmpi 0x1000,__KERNEL_CS
如果__KERNEL_CS段選擇子所引用的段描述符設置的段空間為線形地址[0,4 GB],而我們將OS Kernel放在物理地址1000h,那麼此jmpi指令就跳轉到OS Kernel的入口處,並開始執行它。
此時,Booting階段結束,OS正式開始運行!

⑷ LJMP $ 什麼意思

LJMP是單片機的跳轉指令

ljmp是linux下AT$T格式的匯編指令

⑸ 怎樣能將匯編語言轉換成c語言

1、打開IAR FOR STM8工程。

⑹ cjne指令後面直接跟JC指令,請問這樣怎麼翻譯到C語言,程序如下

這篇文章將首先介紹一些所需的基本知識,如操作系統對進程的內存管理以及相關的系統調用,然後逐步實現一個簡單的malloc。為了簡單起見,這篇文章將只考慮x86_64體系結構,操作系統為Linux。

1 什麼是malloc

2 預備知識

2.2.1 內存排布

2.2.2 Heap內存模型

2.2.3 brk與sbrk

2.2.4 資源限制與rlimit

2.1.1 虛擬內存地址與物理內存地址

2.1.2 頁與地址構成

2.1.3 內存頁與磁碟頁

2.1 Linux內存管理

2.2 Linux進程級內存管理

3 實現malloc

3.2.1 數據結構

3.2.2 尋找合適的block

3.2.3 開辟新的block

3.2.4 分裂block

3.2.5 malloc的實現

3.2.6 calloc的實現

3.2.7 free的實現

3.2.8 realloc的實現

3.1 玩具實現

3.2 正式實現

3.3 遺留問題和優化

4 其它參考

1 什麼是malloc

在實現malloc之前,先要相對正式地對malloc做一個定義。

根據標准C庫函數的定義,malloc具有如下原型:

void* malloc(size_t size);

這個函數要實現的功能是在系統中分配一段連續的可用的內存,具體有如下要求:

malloc分配的內存大小至少為size參數所指定的位元組數

malloc的返回值是一個指針,指向一段可用內存的起始地址

多次調用malloc所分配的地址不能有重疊部分,除非某次malloc所分配的地址被釋放掉

malloc應該盡快完成內存分配並返回(不能使用NP-hard的內存分配演算法)

實現malloc時應同時實現內存大小調整和內存釋放函數(即realloc和free)

對於malloc更多的說明可以在命令行中鍵入以下命令查看:

man malloc

2 預備知識

在實現malloc之前,需要先解釋一些Linux系統內存相關的知識。

2.1 Linux內存管理

2.1.1 虛擬內存地址與物理內存地址

為了簡單,現代操作系統在處理內存地址時,普遍採用虛擬內存地址技術。即在匯編程序(或機器語言)層面,當涉及內存地址時,都是使用虛擬內存地址。採用這種技術時,每個進程彷彿自己獨享一片2N

位元組的內存,其中N是機器位數。例如在64位CPU和64位操作系統下,每個進程的虛擬地址空間為264

Byte。

這種虛擬地址空間的作用主要是簡化程序的編寫及方便操作系統對進程間內存的隔離管理,真實中的進程不太可能(也用不到)如此大的內存空間,實際能用到的內存取決於物理內存大小。

由於在機器語言層面都是採用虛擬地址,當實際的機器碼程序涉及到內存操作時,需要根據當前進程運行的實際上下文將虛擬地址轉換為物理內存地址,才能實現對真實內存數據的操作。這個轉換一般由一個叫MMU(Memory Management Unit)的硬體完成。

2.1.2 頁與地址構成

在現代操作系統中,不論是虛擬內存還是物理內存,都不是以位元組為單位進行管理的,而是以頁(Page)為單位。一個內存頁是一段固定大小的連續內存地址的總稱,具體到Linux中,典型的內存頁大小為4096Byte(4K)。

所以內存地址可以分為頁號和頁內偏移量。下面以64位機器,4G物理內存,4K頁大小為例,虛擬內存地址和物理內存地址的組成如下:

實現代碼:

void split_block(t_block b,size_t s){

t_block new;

new= b->data + s;

new->size = b->size - s - BLOCK_SIZE ;

new->next = b->next;

new->free =1;

b->size = s;

b->next =new;

}

3.2.5 malloc的實現

有了上面的代碼,我們可以利用它們整合成一個簡單但初步可用的malloc。注意首先我們要定義個block鏈表的頭first_block,初始化為NULL;另外,我們需要剩餘空間至少有BLOCK_SIZE + 8才執行分裂操作。

由於我們希望malloc分配的數據區是按8位元組對齊,所以在size不為8的倍數時,我們需要將size調整為大於size的最小的8的倍數:

size_t align8(size_t s){

if(s &0x7==0)

return s;

return((s >>3)+1)<<3;

}

#define BLOCK_SIZE 24

void*first_block=NULL;

/* other functions... */

void*malloc(size_t size){

t_block b, last;

size_t s;

/* 對齊地址 */

s = align8(size);

if(first_block){

/* 查找合適的block */

last = first_block;

b = find_block(&last, s);

if(b){

/* 如果可以,則分裂 */

if((b->size - s)>=( BLOCK_SIZE +8))

split_block(b, s);

b->free =0;

}else{

/* 沒有合適的block,開辟一個新的 */

b = extend_heap(last, s);

if(!b)

return NULL;

}

}else{

b = extend_heap(NULL, s);

if(!b)

return NULL;

first_block = b;

}

return b->data;

}

3.2.6 calloc的實現

有了malloc,實現calloc只要兩步:

malloc一段內存

將數據區內容置為0

由於我們的數據區是按8位元組對齊的,所以為了提高效率,我們可以每8位元組一組置0,而不是一個一個位元組設置。我們可以通過新建一個size_t指針,將內存區域強制看做size_t類型來實現。

void*calloc(size_t number,size_t size){

size_t*new;

size_t s8, i;

new= malloc(number * size);

if(new){

s8 = align8(number * size)>>3;

for(i =0; i < s8; i++)

new[i]=0;

}

returnnew;

}

3.2.7 free的實現

free的實現並不像看上去那麼簡單,這里我們要解決兩個關鍵問題:

如何驗證所傳入的地址是有效地址,即確實是通過malloc方式分配的數據區首地址

如何解決碎片問題

首先我們要保證傳入free的地址是有效的,這個有效包括兩方面:

地址應該在之前malloc所分配的區域內,即在first_block和當前break指針范圍內

這個地址確實是之前通過我們自己的malloc分配的

第一個問題比較好解決,只要進行地址比較就可以了,關鍵是第二個問題。這里有兩種解決方案:一是在結構體內埋一個magic number欄位,free之前通過相對偏移檢查特定位置的值是否為我們設置的magic number,另一種方法是在結構體內增加一個magic pointer,這個指針指向數據區的第一個位元組(也就是在合法時free時傳入的地址),我們在free前檢查magic pointer是否指向參數所指地址。這里我們採用第二種方案:

首先我們在結構體中增加magic pointer(同時要修改BLOCK_SIZE):

typedefstruct s_block *t_block;

struct s_block {

size_t size;/* 數據區大小 */

t_block next;/* 指向下個塊的指針 */

int free; /* 是否是空閑塊 */

int padding;/* 填充4位元組,保證meta塊長度為8的倍數 */

void*ptr; /* Magic pointer,指向data */

char data[1]/* 這是一個虛擬欄位,表示數據塊的第一個位元組,長度不應計入meta */

};

然後我們定義檢查地址合法性的函數:

t_block get_block(void*p){

char*tmp;

tmp = p;

return(p = tmp -= BLOCK_SIZE);

}

int valid_addr(void*p){

if(first_block){

if(p > first_block && p < sbrk(0)){

return p ==(get_block(p))->ptr;

}

}

return0;

}

當多次malloc和free後,整個內存池可能會產生很多碎片block,這些block很小,經常無法使用,甚至出現許多碎片連在一起,雖然總體能滿足某此malloc要求,但是由於分割成了多個小block而無法fit,這就是碎片問題。

一個簡單的解決方式時當free某個block時,如果發現它相鄰的block也是free的,則將block和相鄰block合並。為了滿足這個實現,需要將s_block改為雙向鏈表。修改後的block結構如下:

typedefstruct s_block *t_block;

struct s_block {

size_t size;/* 數據區大小 */

t_block prev;/* 指向上個塊的指針 */

t_block next;/* 指向下個塊的指針 */

int free; /* 是否是空閑塊 */

int padding;/* 填充4位元組,保證meta塊長度為8的倍數 */

void*ptr; /* Magic pointer,指向data */

char data[1]/* 這是一個虛擬欄位,表示數據塊的第一個位元組,長度不應計入meta */

};

合並方法如下:

t_block fusion(t_block b){

if(b->next && b->next->free){

b->size += BLOCK_SIZE + b->next->size;

b->next = b->next->next;

if(b->next)

b->next->prev = b;

}

return b;

}

有了上述方法,free的實現思路就比較清晰了:首先檢查參數地址的合法性,如果不合法則不做任何事;否則,將此block的free標為1,並且在可以的情況下與後面的block進行合並。如果當前是最後一個block,則回退break指針釋放進程內存,如果當前block是最後一個block,則回退break指針並設置first_block為NULL。實現如下:

void free(void*p){

t_block b;

if(valid_addr(p)){

b = get_block(p);

b->free =1;

if(b->prev && b->prev->free)

b = fusion(b->prev);

if(b->next)

fusion(b);

else{

if(b->prev)

b->prev->prev = NULL;

else

first_block = NULL;

brk(b);

}

}

}

3.2.8 realloc的實現

為了實現realloc,我們首先要實現一個內存復制方法。如同calloc一樣,為了效率,我們以8位元組為單位進行復制:

void _block(t_block src, t_block dst){

size_t*sdata,*ddata;

size_t i;

sdata = src->ptr;

ddata = dst->ptr;

for(i =0;(i *8)< src->size &&(i *8)< dst->size; i++)

ddata[i]= sdata[i];

}

然後我們開始實現realloc。一個簡單(但是低效)的方法是malloc一段內存,然後將數據復制過去。但是我們可以做的更高效,具體可以考慮以下幾個方面:

如果當前block的數據區大於等於realloc所要求的size,則不做任何操作

如果新的size變小了,考慮split

如果當前block的數據區不能滿足size,但是其後繼block是free的,並且合並後可以滿足,則考慮做合並

下面是realloc的實現:

void*realloc(void*p,size_t size){

size_t s;

t_block b,new;

void*newp;

if(!p)

/* 根據標准庫文檔,當p傳入NULL時,相當於調用malloc */

return malloc(size);

if(valid_addr(p)){

s = align8(size);

b = get_block(p);

if(b->size >= s){

if(b->size - s >=(BLOCK_SIZE +8))

split_block(b,s);

}else{

/* 看是否可進行合並 */

if(b->next && b->next->free

&&(b->size + BLOCK_SIZE + b->next->size)>= s){

fusion(b);

if(b->size - s >=(BLOCK_SIZE +8))

split_block(b, s);

}else{

/* 新malloc */

newp = malloc (s);

if(!newp)

return NULL;

new= get_block(newp);

_block(b,new);

free(p);

return(newp);

}

}

return(p);

}

return NULL;

}

3.3 遺留問題和優化

以上是一個較為簡陋,但是初步可用的malloc實現。還有很多遺留的可能優化點,例如:

同時兼容32位和64位系統

在分配較大快內存時,考慮使用mmap而非sbrk,這通常更高效

可以考慮維護多個鏈表而非單個,每個鏈表中的block大小均為一個范圍內,例如8位元組鏈表、16位元組鏈表、24-32位元組鏈表等等。此時可以根據size到對應鏈表中做分配,可以有效減少碎片,並提高查詢block的速度

可以考慮鏈表中只存放free的block,而不存放已分配的block,可以減少查找block的次數,提高效率

還有很多可能的優化,這里不一一贅述。下面附上一些參考文獻,有興趣的同學可以更深入研究。

⑺ linux進程怎麼區分系統進程

1:沒有內核進程和用戶進程之分;
2:每個進程可以在用戶態運行和內核態運行;
3:每個進程可以認為是一個指令運行路線+背景信息(如:打開文件),這些指令沿著路線的執行會影響到進程的信息(進程的路線是由CPU的eip決定的);
4:進程的切換:進程的切換需要由當前正在運行的進程准備好「切換到的進程」的相關信息的設置(如:current_proc),然後在執行一個CPU指令(如:ljmp可以通過TSS完成相關寄存器的設置),在這之後,地址空間變了,且CPU按照新的的eip執行了,也就是說新的進程得到了運行。

⑻ reboot 自動重啟如何編寫

使用notifier_call_chain向其它部分發出重啟的消息,然後調用machine_restart函數完成重啟。

machine_restart函數的開始部分有一段SMP相關的代碼,主要完成多CPU時由一個CPU完成重啟操作,其它CPU處於等待狀態。之後系統根據一個變數reboot_thru_bios的內容判斷重啟方式,通過閱讀reboot_setup我們可以得知,這個參數的內容是在系統啟動時指定的,決定了是否利用bios,事實上是系統復位後的入口(FFFF:0000)地址的程序進行重啟。在不通過bios進行重啟的情況下,系統首先設定了重啟標志,然後向埠0xfe寫入數字0x64,這種重啟的具體原理我還不大清楚,似乎是模擬了一次reset鍵的按下,希望大家和我討論。在通過bios重啟的情況下,系統同樣先設定了重啟模式,然後切換到了實模式,通過一條ljmp $0xffff,$0x0完成了重啟。

⑼ 晶元(單片機)是怎麼認識程序轉化來的二進制,0是低電平,1是高電平使硬體運行呢

手機不再是單純的手機,它是有情感的智能機器人,而它的晶元被別人控制,我們不僅僅要學會和人相處,更要學會和手機處理好關系,它一不高興,就有辦法給我們設置阻礙,天下之大,能人倍出,望老天爺快速研究出新的東西來智勝它吧,不想被手機控制,望轉發!保護自己自己的隱私是每個公民的權利!

⑽ 什麼是軟體陷阱

所謂軟體陷阱,就是一條引導指令,強行將亂飛的程序引向一個指定的地址,在那裡有一段專門對程序出錯進行處理的程序。如果我們把這段程序的入口標號稱為ERR的話,軟體陷阱即為一條LJMP ERR指令。為加強其捕捉效果,一般還在它前面加2條NOP指令。

閱讀全文

與linuxljmp相關的資料

熱點內容
90版本貪食之源屬性 瀏覽:348
文件許可權600 瀏覽:109
蘋果手機使用miui免費電話 瀏覽:732
qtudp發送文件 瀏覽:295
三星手機牆紙文件夾 瀏覽:478
iphone7輸錯密碼震動 瀏覽:944
季度申報數據從哪裡看 瀏覽:645
安卓的郵箱文件保存在哪裡 瀏覽:441
蘋果奧維導出文件在哪裡 瀏覽:405
qq頭像比較社會的女 瀏覽:840
手機風景修圖教程 瀏覽:173
程序員用什麼計算機語言 瀏覽:337
有票APP客服在哪裡 瀏覽:692
國資委63號文件從哪裡查 瀏覽:37
哪個app能顯示lrc字幕 瀏覽:53
jsdate轉換數字 瀏覽:198
賣票的網站取什麼名字好 瀏覽:355
羅湖免費網站製作怎麼樣 瀏覽:274
蘋果6plus測速度 瀏覽:290
u盤的文件變成快捷方式 瀏覽:970

友情鏈接