聽說你想寫個虛擬機(六)?

大家好,我是微微笑的蝸牛,??。

這是虛擬機系列的最后一篇文章,終于要寫完了??。前五篇文章可點擊下方鏈接進行查看。

今天主要介紹 LC-3 匯編格式、匯編器、如何加載可執行文件。

匯編示例

我們先來看一段 LC-3 下實際的匯編代碼:

.ORIG x3000                        
LEA R0, HELLO_STR                  
PUTs                               
HALT                               
HELLO_STR .STRINGZ "Hello World!"  
.END

一眼看上去,是不是跟我們之前介紹的指令類似?實際上,每種指令有著它自己的助記符,大部分指令的助記符就是它本身。

比如 LEA 指令,對應助記符 LEA。HALT 原本是 TRAP 指令中定義的一個系統調用,這里給它設定了單獨的助記符 HALT。PUTs 也是。

固定格式

它有著固定格式,開頭 .ORIG 和結尾 .END

.ORIG 后面跟的地址 0x3000,指定了程序的裝載地址,表示程序加載進內存后,會放到 0x3000 起始地址處。但這個地址也不能隨意指定,因為在 LC-3 中 0x0000-0x2fff 是系統內核區,程序禁止訪問。

.END 沒啥作用,僅作為結束標識。

匯編解析

先看第一條匯編,LEA R0, HELLO_STR

LEA,前面我們也介紹過了,用來取地址。HELLO_STR 是一個標簽,繼續往后看,會發現它定義了一個字符串,"Hello World!"。

那么這行代碼的意思是,將 "Hello World!" 字符串的地址放入 R0 中。

PUTs,取出 R0 中的字符串地址,將字符串輸出到屏幕。這里,也就是將 "Hello World!" 打印出來。它其實也就是 TRAP 指令中的一個系統調用,用于打印字符串。

HALT,退出程序。

以上幾行匯編的功能,千言萬語匯成一句話:將 "Hello World!" 打印到屏幕。

相比起來,寫匯編還是要比起手寫指令簡單高效的多。至少我們不需要計算出指令最終的數值表示了。手寫指令可謂是錯之毫厘,謬以千里,得非常細心謹慎,容不得半點差錯。

數據定義

之前我們在手寫指令時,都是在內存區預先構造好數據,提供給指令使用。但在具體運用時,我們不大可能像這樣操作。

那 LC-3 中有沒有提供像高級語言一樣,能直接定義數據的寫法呢。當然有啦,它分為字符串和數值定義。

字符串

字符串的定義,在上邊我們其實已經接觸過了。看看如下栗子。

HELLO_STR .STRINGZ "Hello World!"  

HELLO_STR 表示標簽,.STRINGZ 表示字符串類型,后面跟著真正的字符串數據。

它的含義是,在當前內存地址處,存儲一個字符串 "Hello World!"。

還是拿這個匯編實例來說:

.ORIG x3000                        
LEA R0, HELLO_STR                  
PUTs                               
HALT                               
HELLO_STR .STRINGZ "Hello World!"
.END

HELLO_STR 前面有 3 條指令,它表示從第 4 個存儲單元開始,寫入字符串 "Hello World!"。注意末尾有空結束字符。

一個字符占兩字節

整段匯編的內存布局如下:

數值

數值的定義,與字符串定義類似。同樣是標簽形式,只是類型不同,為 .FILL,最后跟上具體數值。如下所示:

WIDTH .FILL x4000

它表示在當前內存地址中,存入 0x4000 這個數值。

注意,匯編中不能寫成 0x4000,只能是 x4000。

假設還是上邊那段匯編,只是將字符串定義換成數值定義,此時的內存布局如下:

標簽

標簽可以用來定義數據(上邊我們剛介紹的),也可以用來定義函數。比如下面這段代碼:

WELCOME
    LEA R0, WELCOME_MESSAGE
    PUTs    

WELCOME_MESSAGE .STRINGZ "Welcome to LC3 Rogue."

WELCOME 定義了一個函數,WELCOME_MESSAGE 定義了一個字符串。

指令中是沒有標簽這個說法的,它只是用在匯編中,方便書寫和閱讀。在將匯編代碼轉換為指令時,匯編器會按照指令定義,將標簽轉換為具體的參數。

在了解標簽含義,數據定義,指令定義后,我們就可寫出更加豐富的匯編代碼了。

若想了解更多匯編寫法,可參考實例

不過,這個實例涉及到了鍵盤相關操作,我們并沒有在虛擬機中實現。若有興趣,可自己參照原有實現補充完成。

匯編器

當我們寫了匯編代碼后,如何將其轉換為二進制指令呢?

別著急,lc3tool 提供了配套的匯編器,可通過命令將匯編轉換成指令,生成可執行文件。下面,我們來介紹一下。

安裝

下載 lc3tool 源碼編譯安裝。下載鏈接可在文末查看。

下載完成后,按如下步驟操作,注意將安裝路徑替換為自己本機路徑。

// 解壓
unzip lc3tools_v12.zip

// 進入目錄
cd lc3tools

// 將路徑替換成要安裝的本機路徑
./configure --installdir /path/to/install/dir

// 編譯
make
make install

編譯完成后,在命令行輸入 lc3as,如果看到如下輸出,表示安裝成功。

另外,建議將 lc3as 路徑添加到 PATH 中。這樣就不用每次都跑到 lc3tools 目錄下執行命令,一勞永逸。

我使用的是 zsh,編輯 ~/.zshrc,添加要導出的路徑。

export LC3TOOL_HOME="/Users/liusilan/Downloads/package/lc3tools"
export PATH=$LC3TOOL_HOME:$PATH

最后執行 source ~/.zshrc,使更改生效。

轉換

lc3as 工具準備好之后,我們先寫段簡單的代碼轉換一下。

  1. 將以下匯編代碼存儲為 test.asm。
.ORIG x3000                        
LEA R0, HELLO_STR                  
PUTs                               
HALT                               
HELLO_STR .STRINGZ "Hello World!"  
WIDTH .FILL x4000
.END

這段匯編包括代碼和數據,數據部分定義了字符串 HELLO_STR 和數值 WIDTH

  1. 使用如下命令將其轉換為指令。
lc3as test.asm

若轉換成功,控制臺會輸出 0 errors,同時在同級目錄下會生成 test.obj 可執行文件。雖說后綴 .obj 是目標文件的意思,但這里我們就將其稱為可執行文件吧。

文件內容

接著,我們來看看 test.obj 中的內容。可見,匯編代碼都被轉換成了指令。

3000 e002 f022 f025 0048 0065 006c 006c
006f 0020 0057 006f 0072 006c 0064 0021
0000 4000

在上面的指令中,喵一眼就能看到我們熟知的 f025,也就是 HALT,程序停止指令。

我們再來詳細介紹一下文件內容的構成。

  1. 載入地址

文件的頭兩個字節是 0x3000,也就是 .ORIG 指定的地址。那我們可以得知,.ORIG 這個標識的作用就是將程序載入地址寫入到文件開頭兩字節

另外,還需注意一點,這里生成文件的數據存儲方式是大端字節序,高位在低地址,低位在高地址,符合人類閱讀習慣。

為什么說是大端序?再簡單解釋一下。

因為文件數據存儲是從低地址到高地址,而文件中寫入的內容是 0x3000,那么表明 30 存儲在低地址,00 存儲在高地址。

而 0x3000 這個數值,30 是高位,00 是低位。再根據上一段的分析,也就對應著高位 30 在低地址,低位 00 在高地址,所以是大端存儲。

如果換作是小端存儲,那么文件內容應該是 0030,大家若有疑問可以實踐一下。

  1. 指令

再來看 e002 這條指令,它對應著 LEA R0, HELLO_STR 這條匯編代碼。我們來詳細分析一下,指令是如何生成的。

首先我們回顧一下 LEA 的指令格式:

操作碼是 1110;第一個參數是 R0,下標為 0,用三位表示是 000;第二個參數是相對于 PC 的偏移量。

那偏移量如何計算呢?看如下分析,就能明白了。

  • LEA 是第一條指令,假設它在地址 x 處,PC 指向下一條指令,值為 x+1
  • HELLO_STR 在地址 x+3 處,因為它與 LEA 相差 3 個存儲單元。
  • 偏移量為 (x+3)-(x+1)=2。第二個參數 pc_offset 用 9 位表示為:000000010

這是常規計算偏移量的方式。其實,大可不必這么復雜,在算出兩者相差多少個存儲單元后,再減一即可。

最后,將操作碼、兩個操作數拼起來形成完整指令,得到:1110000000000010,轉換為十六進制表示:e002。??,正好可以對應得上。

f022,可以自己試著分析一下。

f025,程序退出指令,不必多說。

  1. 數據

接下來,就是我們定義的數據。

0048,剛好是字符 'H' 的 ASIIC 碼表示。那么,我們可以猜到,從它開始,存儲的是字符串 "Hello World!",對應著 0048 ~ 0000 這一段數據。

最后的 4000,也就是我們定義的數值 WIDTH

復雜些的栗子

在上面的栗子中,代碼和數據看似是分開的,其實是可混合在一起的,這取決于匯編如何編寫。

下面這個例子,定義了 MAIN 和 FUNC 標簽,表示函數。MAIN 中有代碼和數據,FUNC 中只有代碼。那么轉換之后,數據仍在兩段代碼之間,還是按照原有順序。

.ORIG x3000 ; OS is <3000

MAIN
    LEA R0, HELLO_STR                  
    PUTs                                       
    JSR FUNC                     
    HELLO_STR .STRINGZ "Hello World!"  
    WIDTH .FILL x4000

FUNC
    LD  R5, WIDTH
    ADD R5, R5, #1
    HALT
.END

轉換后的指令和數據如下:

3000 e002 f022 480e 0048 0065 006c 006c
006f 0020 0057 006f 0072 006c 0064 0021
0000 4000 2bfe 1b61 f025

不難看出,最后三條指令,2bfe 1b61 f025,剛好對應著 FUNC 標簽中的代碼。前面的 4000 則是數據 WIDTH,再往前,就是字符串 "Hello World!",這也就印證了代碼和數據是混合的

另外,標簽不會進行轉換,因為它只是為了方便編寫代碼,沒有真正含義。但它會被用來計算偏移量,可認為它代表著函數第一條指令的地址,或者是數據的地址。

舉個栗子。JSR FUNC,表示跳轉到函數 FUNC 去執行。這里假設指令從地址 0 開始存放。

  • JSR 是第三條指令,地址為 2,那么 PC = 3。
  • FUNC 的地址,也就是第一條指令 LD 的地址,為 17。
  • 由此得出的偏移量為 17 - 3 = 14。
  • 480e 是轉換后指令的十六進制表示,最后 11 位是偏移,數值是 e,剛好等于 14。

至于 FUNC 的地址 17 是怎么計算出來的,可看下圖,也就是依照 FUNC 前面的指令和數據所占空間來推導。

因此,匯編中有效轉換部分是代碼和數據定義,并且是按順序轉換,代碼和數據的相對位置不變。也可以認為去除標簽后,將代碼和數據按序依次放到內存中。

加載可執行文件

既然使用工具生成了可執行文件,那就只剩最后一步,將其加載到內存,運行程序。可執行文件的路徑可通過參數指定。

不過,有幾個問題需要處理:

  1. 指令和數據存儲是大端序,而我們的機器是小端序,這就涉及到大端轉小端的問題。
  2. 文件前兩個字節是程序載入地址,需要把它提取出來。
  3. 由于指定了載入地址,而內存空間有限,那么可讀入的最大指令數是有限制的。

大端轉小端

大端和小端的順序恰好是相反的,因此,只需將高 8 位 和低 8 位交換即可。

uint16_t swap16(uint16_t x)
{
  // 兩個字節交換
  return (x << 8) | (x >> 8);
}

提取載入地址

載入地址在開頭兩字節,先從文件中讀取兩字節數據,然后轉換為小端表示。

// 載入地址
uint16_t origin;

// 讀取 2 字節
fread(&origin, sizeof(origin), 1, file);

// 大端轉小端
origin = swap16(origin);

讀取指令和數據到內存

由于載入地址的指定,可讀取最大指令數有限。這里簡單處理:

最大讀取指令數 = 最大內存空間 - 載入地址

a. 根據載入地址,計算出可讀取最大指令數。

uint16_t max_read = UINT16_MAX - origin;

b. 將指針指向載入地址:

uint16_t *p = mem + origin;

c. 讀取指令到載入地址:

size_t read = fread(p, sizeof(uint16_t), max_read, file);

d. 指令逐條轉換為小端序:

// 大端轉小端
while (read-- > 0)
{
    *p = swap16(*p);
    ++p;
}

這樣,可執行文件的處理與載入就完成了。

指令執行

現在指令和數據已經放入內存,一切準備就緒。那就開始執行指令吧~

不過,別忘了將 PC 的初始值修改為載入地址喲。這樣,程序才會從第一條指令開始執行。

// 設置初始值
PC = origin;

再啰嗦一下載入地址。假設載入地址 origin = 0x3000。這時,內存布局和 PC 指向如下所示,載入地址處存放第一條指令。

最后,運行虛擬機,參數帶上可執行文件路徑,程序就能跑起來啦。

gcc -o vm_lc_3_all vm_lc_3_all.c
./vm_lc_3_all test.obj

從此,我們從手工勞作時代跨越到了機器時代,可以更加歡快的寫代碼了~

LC-3 實現的完整代碼可點此查看

總結

至此,虛擬機系列就落下帷幕,全部完結,總共六篇文章。

從一個最小虛擬機開始介紹,到完成 LC-3 這個更加貼近真實意義的虛擬機。一步步從簡單,到復雜,由淺入深,慢慢掀開了虛擬機的神秘面紗。當然,現代虛擬機要復雜的多,這里我們主要是講述它的大體思想。

同時,這也是我自主學習過程的記錄。通過思考與實踐,慢慢發現原來虛擬機并沒有那么難。不過話又說回來,手寫指令,著實讓我深深的體會到了老一輩程序員的痛苦。相比起來,現在的程序員簡直幸福得不要不要的。

另外,這個系列雖已完結,但是我還會繼續學習,輸出其他系列,比如《動手寫 shell》、《動手寫瀏覽器渲染引擎》等等,歡迎關注~

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374