一步步编写操作系统 50 加载内核3
接上節(jié),在這里,我們把參數(shù)放到了棧中保存,大家注意到了,參數(shù)入棧的順序是先從最右邊的開始,最后壓入的參數(shù)最左邊的,其實(shí)這是某種約定,要不,為什么不先把中間的參數(shù)src入棧呢。既然主調(diào)函數(shù)按照從右到左的順序在棧中壓入?yún)?shù),被調(diào)函數(shù)中必須分清楚這三個(gè)參數(shù)分別在棧中哪個(gè)位置。棧是向下擴(kuò)展的,這一點(diǎn)通過push指令壓棧時(shí),棧指針esp的值越來越小能體現(xiàn)出來,所以最后壓入的第1個(gè)參數(shù)是離棧頂(esp指向的地址)最近,最先入棧的第3個(gè)參數(shù)離棧頂最遠(yuǎn)。我們來看下在參數(shù)入棧后并調(diào)用函數(shù)時(shí),棧中布局是什么,還是拿call mem_cpy為例。如圖
由于棧指針esp已經(jīng)在loader.S中被加上了0xc0000000,所以其棧中地址都是內(nèi)核所在的0xc0000000以上的高地址。用call指令進(jìn)行函數(shù)調(diào)用時(shí),cpu會(huì)自動(dòng)在棧中壓入返回地址,由圖可見,當(dāng)調(diào)用kernel_init函數(shù)時(shí),當(dāng)時(shí)的棧指針是0xc00008fc,所以kernel_init的返回地址被存儲(chǔ)在0xc00008fc處。棧中地址0xc00008f8處的內(nèi)容是提供給函數(shù)mem_cpy的第三個(gè)參數(shù),即size。地址較低的0xc00008f4處是它的第二個(gè)參數(shù),即src地址,0xc00008f0處是它的第一個(gè)參數(shù),即dst。
在mem_cpy的實(shí)現(xiàn)中,我們訪問棧中的參數(shù)是基于ebp來訪問的,這通常意味著要將esp的值賦給ebp。由于不知道ebp中的值是不是重要,好的習(xí)慣是提前將ebp備份起來,這就是在第228行的目的,將ebp入棧備份,這樣在函數(shù)結(jié)束時(shí)能夠?qū)⑵浠謴?fù)。我們在第229行將esp賦值給了ebp。所以上圖中,標(biāo)出了ebp的指向,由于后來在第230行又將ecx入棧,故esp已經(jīng)小于ebp。
棧中每個(gè)單元占用4字節(jié),既然是基于ebp來獲得棧中的參數(shù),那么如圖所示,第1個(gè)參數(shù)dst的地址是ebp+8,第2個(gè)參數(shù)src的地址是ebp+12,第3個(gè)參數(shù)size的地址是ebp+16。分別對這些地址用中括號(hào)取值后,便可以得到實(shí)際的參數(shù)。
在繼續(xù)往下說之前,要給大家介紹個(gè)數(shù)據(jù)復(fù)制小團(tuán)隊(duì)。
首先要說一下字符串“搬運(yùn)”指令族:movsb、movsw、movsd。其中的movs代表move string,后面的b代表byte,w代表word,d代表dword。所以movsb的功能是搬運(yùn)(復(fù)制)1字節(jié),movsw的功能是搬運(yùn)(復(fù)制)2字節(jié),movsd的功能是搬運(yùn)(復(fù)制)4字節(jié)。數(shù)據(jù)從哪里來,搬到哪里去呢?這三條指令是將DS:[E]SI指向的地址處的1或2或4個(gè)字節(jié)搬到ES:[E]DI指向的地址處,16位環(huán)境下源地址指針用SI寄存器,目的地址指針用DI寄存器,32位環(huán)境下源地址則用ESI,目的地址則用EDI。話說雖然這三個(gè)指令叫字符串指令,但它們可不是只用在字符串上,因?yàn)樽址械淖址灰彩前醋止?jié)來存儲(chǔ)嗎,任何數(shù)據(jù)在內(nèi)存中都以字節(jié)存儲(chǔ)單元來訪問,字符串只是表相,本質(zhì)上是復(fù)制字節(jié),所以它更多的被通用于復(fù)制數(shù)據(jù)。
以上三個(gè)命令只是復(fù)制固定的字節(jié)數(shù),每執(zhí)行一次就復(fù)制1字節(jié)或2字節(jié)或4字節(jié),如果大量的數(shù)據(jù)需要復(fù)制,則需要連續(xù)的運(yùn)行,所以要介紹另外一個(gè)指令rep。
rep指令是repeat重復(fù)的意思,該指令是按照ecx寄存器中指定的次數(shù)重復(fù)執(zhí)行后面的指定的指令,每執(zhí)行一次,ecx自減1,直到ecx等于0時(shí)為止,所以在用rep重復(fù)執(zhí)行某個(gè)指令之前,一定要將ecx寄存器提前賦值。
似乎說完了,但其實(shí)還差點(diǎn)什么,您想,如果想要復(fù)制一大塊數(shù)據(jù)的話,總該有人更新數(shù)據(jù)的來源和目的地吧。movs [bwd]只是從[e]si指向的地址處搬運(yùn)1、2、4字節(jié)到[e]di指向的地址處,它不會(huì)自動(dòng)更新[e]si和[e]di。咱們總不能翻來覆去從同一個(gè)源地址搬運(yùn)數(shù)據(jù)到另一個(gè)相同的目的地址吧。所以,cld和sld指令就派上用場了,這兩個(gè)指令本質(zhì)上是控制重復(fù)執(zhí)行字符串指令時(shí)的[e]si 和[e]di的遞增方式,遞增方式是指它們的值逐漸變大還是逐漸變小,也就是說,地址是往高地址方向變化,還是往低地址方向變化,這就是所說的方向。cld是指clean direction,該指令是將eflags寄存器中的方向標(biāo)志位DF置為0,這樣rep在循環(huán)執(zhí)行后面的字符串指令時(shí),[e]si和[e]di根據(jù)使用的字符串搬運(yùn)指令,自動(dòng)加上所搬運(yùn)數(shù)據(jù)的字節(jié)大小,這是由cpu自動(dòng)完成的,不用人工干預(yù)。比如,執(zhí)行一次movsd,[e]si和[e]di就自動(dòng)加4,執(zhí)行一次movsb,[e]si和[e]di就自動(dòng)加1。有清除方向標(biāo)志位就會(huì)有設(shè)置方向標(biāo)志位,std是set direction,該指令是將方向標(biāo)志位DF置為1,每次rep循環(huán)執(zhí)行后面字符串指令時(shí),[e]si和[e]di自動(dòng)減去所搬運(yùn)數(shù)據(jù)的字節(jié)大小。
也許cpu認(rèn)為地址由低向高處發(fā)展是理所應(yīng)當(dāng)?shù)?#xff0c;這無須設(shè)置,所以此時(shí)DF標(biāo)志為0。當(dāng)由高地址向低地址發(fā)展時(shí),這不是正常自然的現(xiàn)象,所以需要強(qiáng)調(diào)一下,故要將DF標(biāo)志置為1。
注意,并不是在任何字符串控制指令中[e]si和[e]di都同時(shí)增減,這要看字符串操作指令是否都用到了它們,處理器只會(huì)增加用到的那個(gè)。字符串操作指令有很多,比如有movs[bwd]、ins[bwd]和outs[bwd]、lods[bwd]和stos[bwd],esi和edi并不是被以上三組指令同時(shí)使用,只有movs[bwd]才同時(shí)使用esi和edi,通過rep指令組合執(zhí)行時(shí),esi和edi根據(jù)DF位的值自增或自減。ins[bwd]是從端口讀入數(shù)據(jù)到內(nèi)存的目的地址,故只涉及到edi的自增自減。outs[bwd]是把內(nèi)存中的源數(shù)據(jù)寫入端口,故只涉及到esi的自增自減。lods[bwd]是把內(nèi)存中的源數(shù)據(jù)加載到寄存器al、ax或eax,自增自減操作也只涉及到esi。而stos[bwd]是將al、ax、eax中的值寫入到內(nèi)存中的目的地址,故也只涉及到edi的自增自減。
好啦,在稍微擴(kuò)展了一小下之后,咱們回到正題。
有了movs[bdw]指令族、重復(fù)執(zhí)行指令rep,方向指令cld和std,這三劍客在一起配合工作就能夠自由復(fù)制任何大塊數(shù)據(jù)啦。萬事俱備,回到正題。
第227行的cld指令其實(shí)放在movsb之前就行,它是用于清除方向標(biāo)志,讓數(shù)據(jù)的源地址和目的地址逐漸增大。
由于外層函數(shù)也要用ecx做為遍歷段的循環(huán)計(jì)數(shù),所以您明白了,這里的第230行為什么要將ecx入棧備份啦,這樣在ecx用完之后,在mem_cpy執(zhí)行結(jié)束前通過pop指令將ecx和ebp恢復(fù),以便外層遍歷段的循環(huán)中保持ecx正確。
在第231~233行,為復(fù)制工作所需要的條件初始化,esi和edi指向了要復(fù)制的段的來源地址和目的地址,ecx是為rep指令做準(zhǔn)備的,指定了調(diào)用movsb指令的次數(shù)。在此提醒一下,段寄存器DS和ES在進(jìn)入保護(hù)模式之初就被賦成相同的選擇子了,它們都指向同一個(gè)段描述符,故它們在此工作正確,請大伙兒放心。
一切就緒之后,在第234行,rep movsb,這三劍客團(tuán)隊(duì)就開始合作啦。
mem_cpy返回后,程序流程回到第216行,這是清理在調(diào)用mem_cpy之前在棧中壓入的size,src,dst,這三個(gè)參數(shù)共占3*4=12字節(jié),所以將esp加上12,于是棧頂跨過了它們,這三個(gè)參數(shù)所占的空間可被其它壓棧操作覆蓋。
每個(gè)函數(shù)中都要有個(gè)返回指令,這里用的是ret指令,以后我們還會(huì)接觸到其它返回指令。之前在用call指令調(diào)用函數(shù)時(shí),無論是調(diào)用kernel_init還是mem_cpy,cpu都會(huì)將函數(shù)的返回地址壓入棧中保存,這是為函數(shù)體中的ret指令準(zhǔn)備的,換句話說函數(shù)不會(huì)自己返回,是通過ret來返回的。ret指令將棧頂中的值做為返回地址,所以,一定要確保在調(diào)用ret時(shí),位于棧頂處的數(shù)據(jù)是正確的返回地址。一般情況下,我們在函數(shù)體中保證push操作和pop操作配套成對,正如在mem_cpy的實(shí)現(xiàn)中,有兩個(gè)push入棧操作,在函數(shù)返回前就要有兩個(gè)pop出棧操作。
咱們的函數(shù)中用的都是ret近返回指令,所以只會(huì)在棧頂彈出4字節(jié)的數(shù)據(jù)做為代碼段的偏移地址為EIP寄存器賦值,從而恢復(fù)了程序執(zhí)行流.
【再續(xù)】
總結(jié)
以上是生活随笔為你收集整理的一步步编写操作系统 50 加载内核3的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 福建松溪现历史最大洪水 洪涝严重:暴雨警
- 下一篇: 《操作系统真象还原》-阅读笔记(下)