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