2010年1月26日 星期二

NUC501-實作虛擬記憶體(ROM篇)

前次提到在NUC501實作虛擬記憶體的可行性,愈想愈覺得可以試試看,但前提是NUC501的ABORT例外沒有被閹割掉才行,所以想先做個實驗看看NUC501對於ABORT有沒有反應。

從技術資料得知,ARM的ABORT例外分為二個,一個是指令預取例外,當程式碼(R15)執行的區域不存在時,會引發指令預取例外,另一個則是資料取得例外,當程式嘗試去存取不存在的記憶體時,會引發資料取得例外。所以最簡單的測試方法,是先嘗試資料取得例外,就能知道NUC501的ABORT是否能用。

不過沒ICE的情況下,不容易知道是不是有中斷進來,還好開發板上已經裝配了8顆LED,所以可以透過LED的亮暗情形來得知。從範例程式中複製了LED的初始化設定,讓LED恆亮,然後修改crt0裡的DataAbortHandler,讓它在收到中斷時關閉LED。最後在測試程式中寫一行:
*((unsigned long *)0x10000000)=0;
編譯好後放到開發板上執行,哇!沒想到真的會動耶~~看到LED熄滅,害我著實的高興了好一陣子。既然NUC501可以支援ABORT(看來連SRAM的MMU機制都是預先規劃好的!),那麼不僅是ROM,連RAM都可以做虛擬記憶體耶(當時還沒想到同時實現的困難點)~~衝著好的開始是成功的一半,便興沖沖的規劃了要把SRAM分一半給虛擬記憶體做換頁,其中的四頁給ROM,另外四頁給RAM,規劃把ROM放在0x10000000的位址,而RAM預定放在0x08000000的位址之後,便開始深入了解ARM7TDMI的ABORT機制。

指令預取的部份還算容易,以NUC501的架構為例,在收到例外後,把LR的位址減去4,做2048的對齊之後,就是需要換頁的位址。由於ROM不需要回寫,所以只要找到空閒的頁面,把正確的資料複製進去,再把頁面mapping到應出現的位址即可。不過由於我不懂頁面切換演算法,所以就簡單的直接以位址線來決定使用哪個頁面,如位址0x10000000、0x10002000、0x10004000會使用相同的頁面,以此類推。資料例外的部份比較複雜,就想先不去管它,反正還沒有要實作RAM的部份。把crt0做必要修改,並寫好PrefetchAbortHandler後,修改link script把.text放到0x10000000,然後燒到SpiROM上執行,我遭遇到悲慘的當機狀況~~"

在反覆的驗證指令預取是否有問題,我突然想到我犯了一個嚴重的錯誤,那就是指令預取與資料例外是相輔相成的!由於ARM大量使用參考讀取的指令,那麼執行程式時,除了指令預取外,資料例外也會出現!這代表我得先寫好資料例外之後,才能看到成果,害我著實的沮喪好一陣子。上網搜尋看看有沒有簡單的解決方法,大部分的網站都說資料例外要寫一個小型的disassembler,然後去看是哪個指令引發例外,然後修正它!洋洋灑灑說得到容易,我會寫的話,啊事情不就好辦很多!

該來的逃不掉,為了實現虛擬記憶體,還是得做資料例外的處理了。打開ARM7TDMI的技術資料,開始認真的研究,關於資料例外是這麼說的:
If a data abort occurs, the action taken depends on the instruction type:
  1. Single data transfer instructions (LDR, STR) write back modified base registers: the Abort handler must be aware of this.
  2. The swap instruction (SWP) is aborted as though it had not been executed.
  3. Block data transfer instructions (LDM, STM) complete. If write-back is set, the base is updated. If the instruction would have overwritten the base with data (ie it has the base in the transfer list), the overwriting is prevented. All register overwriting is prevented after an abort is indicated, which means in particular that R15 (always the last register to be transferred) is preserved in an aborted LDM instruction.
簡單來說,就是
  1. 處理LDR/STR時要注意write back會破壞基底暫存器的問題,也就是說,收到例外後必須找到並還原基底暫存器的原始值,不然就等著當機吧(原廠沒這麼說啦,我自己加的啦~~")
  2. SWP(這我還是第一次知道有這個指令),在資料例外的時候尚未執行。
  3. 區塊搬移指令LDM/STM,寫一堆文言文看不懂,只知道好像暫存器列表中的暫存器不會被破壞等等的,只好做實驗才知道怎麼回事了。
看來真的得寫disassembler,不然就無法知道那個暫存器是基底暫存器,被加了多少位移,要把一個一個暫存器的值去mapping嗎?別開玩笑了。除此之外,還必須檢查系統例外前的工作模式,所以不僅要處理ARM指令,還有Thumb指令也要做disassembler!為了避免弄到昏到,決定先從ARM指令開始做,到最後Thumb不能做的話,大不了不跑16-bit模式就是了(太天真了!)。便開始跟指令預取一樣,用組合語言在crt0寫了起來。

結果愈寫愈不對,這樣我要怎麼debug咧?買ICE大概無望,我也不想自己出錢,唯一有的就是COM埠輸出可以用,但程式不能在RAM跑要怎麼知道處理正不正確呢?這讓我對要不要繼續弄下去猶豫了許久,如果最後成果高不成低不就,用起來限制重重的話,還不如不要弄了。最後終於在回家的路上想到了解決的辦法!

方法是:
  1. 首先在crt0內把ABORT的堆疊加大,讓系統在呼叫DrvSIO_printf有足夠的記憶體不致於當機,也能讓ABORT有足夠的空間保存所有的暫存器,這樣我就可以用C來寫資料例外了!不僅除錯方便許多,也不用自己做最佳化(GCC的ARM最佳化做得不錯)。
  2. 接下來把程式放在SpiROM上執行(這時我真感謝有SpiROM呀!),在非必要時不要呼叫DrvSIO_printf以免發生重入。
  3. 最後要解決程式僅在SpiROM上執行,而不會跑去不存在位址的問題。其實只是我多慮了,你只能從LR-8的位址取得發生例外時的指令碼而已,實際需要發生換頁的頁面,必須透過自己寫disassembler來解決。所以解決方法,就是用GCC的inline assembly自己寫一定會發生例外的指令就好啦,這樣還可以控制要測試那個指令呢!
確定了debug方法以後,便開始實作,流程是這樣的:

  1. 負責資料例外處理的函式必須放在固定RAM中,所以透過修改link script把程式夾在startup與main之間的RAM區域。
  2. 直接修改crt0的DataAbort進入點,讓它直接跳到自己寫的處理函式中。
  3. 處理函式一開始先用inline assembly把所有暫存器(R0-R12,LR,SPSR)放到堆疊中。由於已經固定堆疊的位址,所以在C中可以透過固定位址來存取原始暫存器的值。
  4. 檢查SPSR中的T旗標,如果是從Thumb模式來的,就直接當機不理它(哈哈)。
  5. 如果是ARM模式,則從LR-8的位址取得指令碼,然後判斷指令形式,舉凡LDR(B)/STR(B)、LDRH/STRH/LDRSB/STRSH、LDM/STM、SWP都在處理之列。其他與NUC501架構無關的指令(如LDC/STC),就不用處裡。
  6. 處理時要注意ARM的先加後存且回寫以及先存後加,這二者都會破壞基底暫存器,必須修正堆疊內的基底暫存器,這樣在重新執行該指令時才不會發生錯誤。
  7. LDM/STM必須處理跨頁面的問題,所以很有可能一次要mapping二個頁面。
  8. 依據基底位址對齊2048,並計算好使用的頁面後,便從SpiROM複製資料進來,並將該頁面mapping到應存在的位址。
  9. 從堆疊復原暫存器(R0-R12,LR, SPSR不要),調整LR,讓CPU重新執行引發例外的指令。

經過幾天的努力,從一開始不斷的當機,到連Thumb模式也做了(後來發現DevKitPro GCC的程式庫有些是Thumb code的,最終還是無法避開),終於讓我看到程式能在0x10000000的位址跑起來!而且真的很快!但興奮感在我打開了GCC的最佳化以後很快就消失了。首先是DrvSIO_printf印字時變成老牛推車,在加了除錯用的程式碼後,會在不定位址當機,究竟是什麼問題呢?

為了找到問題點,又透過LED來幫忙(在RAM跑了以後便無法使用DrvSIO_printf了,以防止例外重入),分別在指令預取與資料例外中加入讓LED閃爍的功能,不過在努力修改除錯以後,始終找不到問題點,我幾乎快要放棄了,看來沒有ICE終究是困難呀!放棄好了。

失望之餘,卻又在回家的路上想到了問題的答案!這不就是作業系統上的「死結」嗎?死結發生的原因,是執行間接資料讀取的指令碼時,發生資料例外,而資料例外處理時,意外的又把指令所在的頁面給換掉了(二者使用同一個頁面),造成例外返回時的程式記憶體不存在,又引發指令預取例外,然後不停的循環,這在可用頁面特別少的情況下很容易發生,完全都要歸功於愚蠢的我設計了那個爛換頁程式所造成的啊!

為了確定問題是死結造成的,用DrvSYS_SetCPUClock把CPU降到18Mhz執行,使LED閃爍更明顯。果然發生死結時LED閃爍的特別厲害,永遠也跑不完,看了這情況真是又好氣又好笑。不過問題還是要解決,一是弄個頁面演算法,讓死結發生的機率降低,這有點困難。二是我另外想到的偷懶方法,從未來要處理RAM換頁的頁面借二個頁面,讓資料例外與指令預取分別使用不同的頁面區域,二者不互搶頁面,問題就解決啦!即使執行時資料頁面切換,指令好死不死剛好也執行到資料頁面上的程式碼,也不會引發預取例外(因為頁面存在),直到資料頁面切換掉被程式用的頁面時,會引發預取例外讓指令用回自己的頁面(效能損失應該只有一點點),反之亦然。

終於,NUC501的虛擬記憶體實作算是完成,說「算是」是因為不知道哪裡還有隱藏的錯誤。不過我知道的是,這一切都很值得,光是在18Mhz的速度下,輸出訊息的速度,就像是386時代把VGA BIOS放到Shadow RAM一樣,跑DIR就像飛的(以前在學校時,同學最喜歡跑DIR訊息看電腦夠不夠快)!

ROM虛擬記憶體完成了,也該實驗RAM的虛擬記憶體了。但想想在實作上有困難,主要是因為想把RAM的SWAP放在SD卡上,而檔案系統有不能重入的問題在,所以不能從例外中呼叫。就算能呼叫也勢必引發ROM頁面切換,使得系統除錯更複雜。看來這個問題還得好好想想才行。

相關文章
NUC501-實作虛擬 記憶體(RAM篇)
ROM虛擬記憶體改進篇

3 則留言:

  1. Good Job.
    看你這樣土法煉鋼的的精神 好生佩服, 我真想借你一台 ICE, 加快你研究的腳步 .

    回覆刪除
  2. 另外 , 看你對遊戲機有些瞭解 , 想請問一下
    PS2 的 USB 你是否有研究 ?

    回覆刪除
  3. WrightWu 提到...看你這樣土法煉鋼的的精神 好生佩服, 我真想借你一台 ICE, 加快你研究的腳步 .

    您客氣了,不過也不好意思跟您借就是了

    WrightWu 提到...PS2 的 USB 你是否有研究 ?

    PS2的話,很抱歉沒有

    回覆刪除