用Java做一個最小的作業系統核心2
上一節,我用Java製作了一個虛擬軟盤,當把虛擬軟盤插入虛擬機器,啟動機器後,螢幕就打印出了Java程式中設定的語句,然後卡死。
在Java程式碼中,有一個二進位制陣列imgContent,它裡面儲存的實際上是一端二進位制程式碼,當虛擬機器設為從軟盤啟動後,這段程式碼會被BIOS讀到記憶體中,然後指示CPU去執行imgContent所儲存的二進位制程式碼
彙編程式碼
下面我就將imgContent陣列中的內容,用匯編語言實現,並且詳細闡述其作用,首先看看組合語言怎麼寫
org0x7c00; jmpentry db0x90 DB"OSKERNEL" DW512 DB1 DW1 DB2 DW224 DW2880 DB0xf0 DW9 DW18 DW2 DD0 DD2880 DB0,0,0x29 DD0xFFFFFFFF DB"MYFIRSTOS" DB"FAT12" RESB18 entry: movax, 0 movss, ax movds, ax moves, ax movsi, msg putloop: moval, [si] addsi, 1 cmpal, 0 jefin movah, 0x0e movbx, 15 int0x10 jmpputloop fin: HLT jmpfin msg: DB0x0a,0x0a db"hello, world" db0x0a db0
首先看第一行程式碼org 0x7c00
,org
的意思是“起始,起源”,org
後面的7c00
是實體記憶體地址,假設實體記憶體是一個位元組陣列,例如byte[] memory
,如果你有2M內容,就需要new2097152位元組的記憶體,byte[] memory = new byte[2097152]
。org 0x7c00
的意思就是將彙編編譯後的二進位制資料從memory[0x7c00]
處寫入memory
。至於為什麼是0x7c00
,這就要問微軟
jmp entry
中的jmp
起始就是c語言中的goto
,jmp entry
其實是讓CPU跳轉到entry
處,執行entry
下面的程式碼,如果entry
是一個函式名,jmp entry
相當於呼叫entry()
函式
從db 0x90
到RESB 18
這段程式碼做的是一些初始化工作。jmp entry
對應的機器碼長度是3位元組,那麼db 0x90
的作用就是memory[0x7c00+3]=9x90
,也就是說db 9x90
實際上做的是賦值操作
DB
和db
是相同的作用,所以DB "OSKERNEL"
的意思是strcpy(memory + 0x7c00 + 3 + 1,"OSKERNEL")
,也就是把"OSKERNEL"這個字串拷貝到記憶體0x7c00 + 3 + 1處。3就是jmp entry
所佔的3位元組長度,1就是db ox90
所複製的那個位元組的長度
DW
和DB
是相同的意思,只不過DB
是將資料賦值給一個位元組,DW
是將資料賦值給兩個位元組,從上面程式碼也能看出來DW 512
,因為512轉換為二進位制肯定超過了8位,所以不能用DB
DD 0xFFFFFFFF
就是將0xFFFFFFFF
儲存到四個位元組長的記憶體中
RESB 18
表示把接下來的18個位元組的內容全部初始化為0,類似於下面的Java程式碼
byte[] block = new byte[18]; for(int i = 0;i < 18;i++) block[i] = 0;
接下來看entry
程式碼,它的作用是初始化一系列暫存器,暫存器相當於Java程式中定義的變數,ax
是一個2位元組長的暫存器,mov ax,0
就是把數值0放到ax
暫存器中,類似Java的char ax = 0
,char型別的資料在Java中也是2位元組長。
類似的,mov ss,ax
相當於Java中的char ss = ax
其中比較重要的語句是mov si,msg
,msg
相當於一段記憶體
msg: DB0x0a,0x0a db"hello, world" db0x0a
上面這段程式碼類似於C語言中的char* msg = "\n\nhello,world\n"
,\n
的ASCII值就是0x0a
。那麼mov si,msg
就相當於把msg
記憶體的真實地址放到暫存器si
裡,如果用C語言表示就是char* si = msg
接著看後面的程式碼mov al,[si]
,[si]
表示讀取si
儲存的記憶體地址中一個位元組長度的資訊,這行程式碼的含義就是把[si]
內容資料儲存到暫存器al
中,ax
是兩位元組長的暫存器,這樣ax
就可以分解成兩部分,第一部分是al
,第二部分是ah
,對應C語言就相當於char ax[2]
,al
表示的是ax[0]
,ah
表示的是ax[1]
,mov ai,[si]
轉換成C語言就是char al = *si
add si,1
表示將暫存器si
中的數值加1,也就相當於C語言中的si++
cmp al,0
表示將暫存器al
暫存器中的資料跟0比較,看al
中的值是否等於0
je fin
中的je
表示jump if equal
,也就是如果al
的值確實等於0,那麼就跳轉到fin
所表示的程式碼處去執行,轉換成C語言就是
if(al == 0) goto fin
mov ah,0xe
就是把0xe
賦值給ah
,mov bx,15
同理
接下來呼叫的是一箇中斷函式,我們在寫C或Java程式時,往往需要呼叫一些系統庫函式,例如printf
,或System.out.print
,中斷函式就是BIOS提供給組合語言的庫函式,這些庫函式都存放在一個數組裡,int 0x10
的意思是在庫函式陣列中取出第0x10
個庫函式執行
有的函式呼叫時需要傳遞引數,如果想要呼叫BIOS提供的函式,在螢幕上輸出字元,那麼就要將傳遞的引數放入到指定的暫存器中。BIOS提供的編號為0x10的庫函式可以實現這個功能,按照規定,要把暫存器ah
的值設定為0x0e
,把要輸出字元的ASCII值放到暫存器al
,同時要把暫存器bh
的值設為0,字元的顏色可以通過暫存器bl
的值來設定。
程式碼段
putloop: moval, [si] addsi, 1 cmpal, 0 jefin movah, 0x0e movbx, 15 int0x10 jmpputloop
就相當於C語言中的
do { char al = *si; si++; if(al == 0) goto fin print("%c",al); } while(true);
fin
處的程式碼就兩條語句,htl
表示halt,也就是讓CPU進入休眠狀態,如果此時我們點選一下鍵盤或動一下滑鼠,那麼CPU就會被喚醒,然後執行hlt
後面的語句jmp fin
,又回到fin
開始處去執行,進入無限迴圈
編譯彙編程式碼
把上面的彙編程式碼儲存成一個檔案:boot.asm,然後利用匯編編譯器nasm來編譯,nasm boot.asm
,編譯後會得到一個二進位制檔案,內容如下:
e94e 0090 4f53 4b45 524e 454c 0002 0101 0002 e000 400b f009 0012 0002 0000 0000 0040 0b00 0000 0029 ffff ffff 4d59 4649 5253 544f 5320 2046 4154 3132 2020 2000 0000 0000 0000 0000 0000 0000 0000 0000 00b8 0000 8ed0 8ed8 8ec0 be73 7c8a 0481 c601 003c 0074 09b4 0ebb 0f00 cd10 ebed f4eb fd0a 0a68 656c 6c6f 2c20 776f 726c 640a 00
修改Java程式碼
把這個二進位制檔案改名為boot.bat,拷貝到上一個Java程式的工程目錄下,然後把Java程式進行修改,將這段二進位制資料都入到imgContent陣列中,程式碼如下
import java.util.ArrayList; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; public class OperatingSystem { private int[] imgContent = new int[] { 0xeb, 0x4e, 0x90, 0x48, 0x45, 0x4c, 0x4c, 0x4f, 0x49, 0x50, 0x4c, 0x00, 0x02, 0x01, 0x01, 0x00, 0x02, 0xe0, 0x00, 0x40, 0x0b, 0xf0, 0x09, 0x00, 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x29, 0xff, 0xff, 0xff, 0xff, 0x48, 0x45, 0x4c, 0x4c, 0x4f, 0x2d, 0x4f, 0x53, 0x20, 0x20, 0x20, 0x46, 0x41, 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x8e, 0xd0, 0xbc, 0x00, 0x7c, 0x8e, 0xd8, 0x8e, 0xc0, 0xbe, 0x74, 0x7c, 0x8a, 0x04, 0x83, 0xc6, 0x01, 0x3c, 0x00, 0x74, 0x09, 0xb4, 0x0e, 0xbb, 0x0f, 0x00, 0xcd, 0x10, 0xeb, 0xee, 0xf4, 0xeb, 0xfd }; private ArrayList<Integer> imgByteToWrite = new ArrayList<Integer>(); private void readKernelFromFile(String fileName) { File file = new File(fileName); InputStream in = null; try { in = new FileInputStream(file); int tempbyte; while ((tempbyte = in.read()) != -1) { imgByteToWrite.add(tempbyte); } } catch (IOException e) { e.printStackTrace(); return; } imgByteToWrite.add(0x55); imgByteToWrite.add(0xaa); imgByteToWrite.add(0xf0); imgByteToWrite.add(0xff); imgByteToWrite.add(0xff); } public OperatingSystem(String s) { readKernelFromFile("boot.bat"); int len = 0x168000; int curSize = imgByteToWrite.size(); for (int l = 0; l < len - curSize; l++) { imgByteToWrite.add(0); } } public void makeFllopy() { try { DataOutputStream out = new DataOutputStream(new FileOutputStream("system.img")); for (int i = 0; i < imgByteToWrite.size(); i++) { out.writeByte(imgByteToWrite.get(i).byteValue()); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { OperatingSystem op = new OperatingSystem("hello, this is my first line of my operating system code"); op.makeFllopy(); } }