Summary

  • 自動化重構工具
    • 像 JetBrains、Visual Studio 等 IDE 的 refactor 功能。
    • 在工具夠好的情況下,可以在沒有為 code 寫測試的情況下用工具進行自動化 refactor。但有時工具可能有問題,refactor 卻會修改到程式行為,所以使用工具進行 refactor 前,如果能先寫測試還是比較好的。
    • 要注意工具在 refactor 時會跟不會進行哪些檢查
      • 例如 extract method,如果將新 method 命名為已存在 method 的名字,工具會不會顯示錯誤?
  • Unit Test 工具
    • xUnit framework
      這類型 framework 執行測試大致的作法:找到 test class 裡所有 test method(依據語言不同有不同作法,有些語言可以用 reflection),為每個 test method 產生一個單獨的 object,該 object 的任務是去執行那個 test method。利用不同 object 隔離 test case,讓 test case 不會互相影響。
  • 一般測試控制工具
    • FIT (Framework for Integration Test)
    • Fitnesse
    • Selenium
    • ……等等其他

Seam & Enabling Point

seam(接縫)是指程式中一些特殊的點,在這些點上你不需要修改它本身就可以變動程式行為。

例如某段程式 call 了一個 function 來計算商品價格,現在想改變計算價格的 strategy。想在不改變 call 計價 function 的情況下,改變計價結果(程式行為)。

又例如程式 call 到牽連龐大子系統的 function,我們希望在 test 中避免執行到那些複雜的 code(不然很難測或無法測),又要在 prodcution code 裡照常執行到。如果這段 code 有 seam,便能在不改動到原本 call function 的情況下,換掉該 function 的行為來避免在 test 中碰到子系統。

每個 seam 都有一個 enabling point,在這裡你可以決定使用哪種行為。

seam 是可以讓你改變程式行為的「縫隙」,enabling point 則是決定那個 seam 要是什麼行為。在 enabling point 給不同的值,可以讓 seam 有不同的行為。例如物件 seam,物件 method 的參數列表是 enabling point,因為我們可以傳入不同的物件來改變程式行為。

為一團亂的 code 測試時,最好別去修改它本身,盡可能透過 seam 去解開 dependency 來測試。

Seam Types

seam 有很多種類型。不同的語言,可以使用的類型也不同。一個語言從程式碼轉換到 machine code 的各個階段,擁有不同種類的 seam。

  • preprocess 時期 seam
    • 在 C/C++ 裡可以用 macro 直接把字換掉來達到改變行為,例如直接把有 dependency 的 function call 用 #define#ifdef TESTING 時換成空的 function。
    • enabling point 是 #define TESTING
  • link 時期 seam:在將多個 object file link 起來時的 seam
    • dynamic link:直接換掉 dynamic link 要去找的 library 或 object file。
    • static link:透過 build script(像 Makefile),在測試環境去 link 抽換的 object file 而非原本 production code 的 object file。
    • enabling point 通常在 build 或 deploy script 裡,因為這時候才決定 link 誰。
  • object seam
    • 一般用 interface、polymorphism 來做 dependency injection,以及 extract and override。

先安裝 ibus:

1
$ sudo apt-get install ibus ibus-chewing

~/.bashrc  加入:

1
2
3
export GTK_IM_MODULE=ibus
export XMODIFIERS=@im=ibus
export QT_IM_MODULE=ibus

寫個 shell script:

1
2
#!/bin/bash
ibus-daemon -d -x

在 KDE Setting 的 Autostart 設定啟動時執行 shell script 來啟動 ibus

這天很多在介紹 C,尤其是 pointer,作者用組語的角度去看 C,很有趣。這邊只寫些簡單的筆記。

組語跟 C 一起使用時,只有 EAXECXEDX 三個 register 可以使用,其他 register 只能 read 不能 write,因為它們存著 C 語言程式的相關資料。

C 語言中,普通數值跟表示 memory address 的數值被當作兩種不同的東西。

根據 C 語言規定,組語執行 RET 指令時, EAX 中的值就被看作是 function return value。

調色盤設定顏色

在 8 bit 彩色模式下,顏色以 8 個 bit 表示,也就是 0~255。

每個數字表示什麼顏色,是由 developer 來決定的,不像 RGB 的 #ffffff 固定代表某個顏色。使用 0~255 表示顏色前,developer 要先幫這些數字指定好對應的顏色,例如 25 對應 #ffffff 等等。這種方式稱為「調色盤」。

設定調色盤的步驟如下:

  1. 先 block interrupt
  2. 依據設定調色盤的方式,對 IO device 的某些 port 寫入資料
  3. 恢復對 interrupt 的處理

向 IO Device read/write

先來看怎麼對 IO device 讀寫。

CPU 與 IO device 相連,CPU 要能控制 IO device 當然有向 device 發送訊號與從 device 接收訊號的指令。

向 device 發送訊號的指令是 OUT,反之從 device 接收訊號的指令是 IN。就像 memory 用 memory address 區分不同位置,device 以 device port 區分不同 device。組語實作從 device read 與 write 到 device 的 function 們:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
; read 8 bit
_io_in8: ; int io_in8(int port);
MOV EDX,[ESP+4] ; port (port number 只有 16 bit)
MOV EAX,0 ; 把 return value 清成 0
IN AL,DX ; read 8 bit (AL)
RET ; EAX 的值是 return value

_io_in16: ; int io_in16(int port);
MOV EDX,[ESP+4] ; port
MOV EAX,0
IN AX,DX ; read 16 bit (AX)
RET

_io_in32: ; int io_in32(int port);
MOV EDX,[ESP+4] ; port
IN EAX,DX ; read 32 bit (EAX)
RET

; write 8 bit
_io_out8: ; void io_out8(int port, int data);
MOV EDX,[ESP+4] ; port
MOV AL,[ESP+8] ; data
OUT DX,AL
RET

_io_out16: ; void io_out16(int port, int data);
MOV EDX,[ESP+4] ; port
MOV EAX,[ESP+8] ; data
OUT DX,AX
RET

_io_out32: ; void io_out32(int port, int data);
MOV EDX,[ESP+4] ; port
MOV EAX,[ESP+8] ; data
OUT DX,EAX
RET

我們要設定調色板,就是找出對應 device 設定調色板的指令,然後照著做~

CLI 與 STI

CLI 是將 interrupt flag clear 為 0 的指令, STI 則是將 interrupt flag set 為 1 的指令。

interrupt flag 為 0 時,CPU 遇到 interrupt 會忽略它、不處理,flag 為 1 時 CPU 就會處理 interrupt。

EFLAGS register

EFLAGS 是由 FLAGS 16 bit 的 register 擴展而來的 32 bit register。

FLAGS 儲存 carry flag(進位 flag)與 interrupt flag 等 flag,不同 bit 代表不同 flag(有 1 個 bit 表示一個 flag 也有 2 個 bit),如下所示:

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
NT IOPL IOPL OF DF IF TF SF ZF AF PF CF

IOPL 是第 12 跟 13 bit 放在一起處理。

EFLAGS 沒有 MOV 指令,只能使用 PUSHFDPOPFD 來讀寫。

PUSHFD 是 push flags double-word 的縮寫,以 double word 的長度(32 bit)將 flag 的值 push 進 stack,等同 PUSH EFLAGSPOPFD 是 pop flags double-word,將 double word 長度的值從 stack pop 出來到 flag,等同 POP EFLAGS

如果想把 EFLAGS 內的資料放到 EAX 裡,不能用 MOV,而要先 PUSHFDPOP EAX。反過來想把 EAX 的資料放進 EFLAGS 裡則是 PUSH EAXPOPFD。操作 EFLAGS 的組語 code 如下:

1
2
3
4
5
6
7
8
9
10
_io_load_eflags:	; int io_load_eflags(void);
PUSHFD ; PUSH EFLAGS
POP EAX
RET

_io_store_eflags: ; void io_store_eflags(int eflags);
MOV EAX,[ESP+4]
PUSH EAX
POPFD ; POP EFLAGS
RET

設定調色盤要先執行 CLI,為了在設定完後恢復 interrupt flag 的值,要先把原本的值記下來。我們可以直接把整個 eflags 記下來、執行 CLI、設定調色盤,最後直接把整個 eflags 的值再存回去,達到恢復 interrupt flag 的效果。

在螢幕上畫圖

螢幕上每個像素都對應到 VRAM 中的一個 address。向 VRAM 指定值,便能指定螢幕上像素的顏色。

製作真正的 IPL

IPL 是 Initial Program Loader,現在要開始做真正的 IPL──把磁碟上的東西 load 到 memory 裡!

這部分書上是一點一點的增加 assembly code,從讀一個 sector 到讀多個 cylinder 的所有 sector。

floppy disk 的 cylinder、sector

floppy disk 的磁片由外而內的一個個同心圓稱為 cylinder(柱面),共有 80 個 cylinder。由圓心往外切(像切蛋糕),cylinder 切成一格格的稱為 sector,一個 cylinder 有 18 個 sector。磁片正反面都可以存放資料,因此磁頭有正反面,以 0 跟 1 表示。

一張磁片共有 2 * 80 * 18 個 sector,每個 sector 是 512 byte,所以一張磁片的容量是 2 * 80 * 18 * 512 = 1474560 byte = 1440 KB就是古早時代的 3.5 磁片…現在應該有些人都不知道那是什麼了…

指定某個 sector 需要指定磁頭、cylinder 編號以及 sector 編號。

floppy disk

指令

  • JC:jump if carry
    • 如果 carry flag 是 1 就 jump
    • carry flag 是個只能存一個 bit 的 register,通常用於表示計算有無進位,也會被一些 BIOS function 用來表示回傳值。
  • JNC:jump if not carry
    • JC 的相反囉
  • JAE:jump if above or equal
    • 大於等於時 jump
  • JBE:jump if below or equal
  • JB:jump if below
  • EQU
    • 相當於 C 的 #define ,可以用來宣告 const
    • CYLS EQU 10 就是 CYLS = 10 的意思

Segment Register

一個 16 bit register 的值的範圍是 0 ~ 65535,只能表示 64 KB 的 memory address。在還沒有 32 bit register 的時代用 segment register 來表示更多 memory address。

使用 segment register 時,以 ES:BX 表示 ES * 16 + BX 的 memory address,在指令裡寫成 MOV AL, [ES:BX]。實際上指定 memory address 時都需要指定 segment register。省略 segment register 會預設使用 DS register,所以寫 MOV CX, [1234] 實際上是 MOV CS, [DS:1234]

因為 ES:BX 表示 memory address ES * 16 + BX,如果想將 address 往前移動 512 byte,可以在 ES 加上 512 / 16 = 32 = 0x20

IPL 主要 read disk 的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
CYLS	EQU		10				; 宣告常數 CYLS = 10

; (中略)

; read disk
MOV AX,0x0820
MOV ES,AX
MOV CH,0 ; cylinder 0
MOV DH,0 ; 磁頭 0
; sector 2, 因為 floppy disk 第一個 sector 是 boot sector, 所以從第二個 sector 開始 read
MOV CL,2
readloop:
MOV SI,0 ; 紀錄失敗次數的 register
retry:
MOV AH,0x02 ; AH=0x02 : read floppy disk
MOV AL,1 ; 1 sector
MOV BX,0
MOV DL,0x00 ; A 驅動器
INT 0x13 ; call BIOS
JNC next ; 沒出錯就跳到 next
ADD SI,1 ; SI + 1 (這段開始在做 retry)
CMP SI,5 ; SI 跟 5 比較
JAE error ; SI >= 5 就跳到 error
MOV AH,0x00
MOV DL,0x00 ; A 驅動器
INT 0x13 ; 重置驅動器
JMP retry ; 回到 retry 再嘗試讀一次
next:
MOV AX,ES ; 將 memory address 往後移 0x200 (512)
ADD AX,0x0020
MOV ES,AX ; 因為沒有 ADD ES,0x020 的指令所以用比較迂迴的方式做
ADD CL,1 ; CL + 1 (sector)
CMP CL,18 ; CL 跟 18 比較
JBE readloop ; CL <= 18 則跳到 readloop (這邊就是 read 到 sector 18 的 loop)
MOV CL,1 ; CL = 1
ADD DH,1 ; DH + 1
CMP DH,2
JB readloop ; DH < 2 則跳到 readloop (這邊是 read 另一個磁頭的 loop)
MOV DH,0 ; 回到磁頭 0
ADD CH,1 ; CH + 1 (cylinder + 1)
CMP CH,CYLS
JB readloop ; CH < CYLS 則跳到 readloop (這邊是 read cylinder 的 loop, CYLS 是常數)

到這裡,我們就有一個將 floppy disk 資料讀到 memory 的 IPL 了!

最簡單的 OS

IPL 寫好,當然要來寫 OS 啦!

在這個部分,無論是切換 video mode、從 BIOS 取得各種資料、把一些資訊保存在 memory 中等等,都是由組語寫成的 haribote.nas —— 我們最簡單的 OS 程式,像這樣一個什麼事也沒幹的 OS:

1
2
3
fin:
HLT
JMP fin

從 boot sector 執行 OS

首先透過 image file 了解在 floppy disk 上檔案是怎麼被放置的(放在哪些位置)。image file 可以想成是把 disk 上的 data「原封不動」的一個個 bit 存下來而成的一個檔案。

利用 image file 我們知道在 floppy disk 的一個檔案,檔名會寫在 0x002600 後(這裡的 address 是 disk 上的 address),檔案內容會在 0x004200 後。

所以我們把 OS 內容寫到名為 haribote.sys 的檔案中並存到 floppy disk,它的內容會在 0x004200 之後,所以我們只要從 boot sector 去執行這個位置上的東西就行了!

要怎麼去執行 disk 上 0x004200 後的程式呢?現在的程式是從 boot sector 開始,把 floppy disk 上的東西 load 到 memory address 0x8000 ,所以 disk 上 0x4200 的內容會位於 memory address 0x8000 + 0x4200 = 0xc200

所以我們在 OS 程式加上 ORG 0xc200 ,並且在 IPL 的最後加上 JMP 0xc200 ,讓 IPL 完成工作後跳到 OS 程式所在位置開始執行!

切換顯示模式(video mode)

切換 video mode 的 BIOS 可以參考 BIOS 網頁知道要用哪些 register(雖然是日文的)。

VRAM 是 video RAM,用來顯示畫面的 memory。它的每個 address 都對應畫面上的像素,在這塊 memory 裡填值就能在畫面上畫東西~VRAM 在 memory 中有好幾塊,分別給不同 video mode 使用。

進入 32 bit mode 前的準備

32 bit mode 是 CPU 的 mode。不同 mode 有不同 instruction,所以 16 bit 跟 32 bit 的程式是不相容的。register 使用上的方便程度也不同(例如在 16 bit mode 可以方便使用 register AX,但 EAX 不好用)。另外 CPU 的保護功能(識別出可疑的 machine code 並屏蔽它)要在 32 bit mode 才能用。

不過 32 bit mode 不能使用 BIOS 功能,因為 BIOS 是用 16 bit mode 的 instruction 寫的。如果有什麼是想用 BIOS 做,要一開始先做。

後來出了想取代 BIOS 的 UEFI,又是另一回事了…

導入 C 語言

終於開始要用 C 語言啦~在這之前得在 haribote.nas 加入一些組語 code,好讓我們能 call C 程式,不過作者在這裡還不想解釋那段,所以也先略過。

現在 OS 開始有組語跟 C 語言的部分,先將組語部分的 haribote.nas rename 成 asmhead.has。再加入一個很簡單的 C 程式叫 bootpack.c

1
2
3
4
5
6
// 程式是從這個 function 開始的,function name 不能更改,應該跟處理 C 語言的組語有關
void HariMain(void)
{
fin:
goto fin;
}

如何將它變成可以在我們自製 OS 上執行的 machine code?

  1. 使用 cc1.exebootpack.c 產生 bootpack.gas
    cc1.exe 是作者用 gcc 改造而來的 C compiler。gcc 是以 gas 組語為基礎,它 output 的是 gas 的 source code。gas 是 GNU 的 assembler。
  2. gas2nask.exebootpack.gas 轉成 bootpack.nas
    作者使用 nask 當 assembler,所以得把 gas 組語轉成 nask 組語。
  3. nask.exebootpack.nas 組譯為 bootpack.obj
  4. obi2bim.exebootpack.obj 產生 bootpack.bim
    • obi2bim.exe 是 linker,這邊是將 object file bootpack.obj link 成完整的 machine code。
    • 雖然現在只有一個 object file,不是多個 object file 要 link 在一起。但即使只有一個 object file 也要幫它加些資訊才能成為完整可執行的 machine code,所以需要 link。
    • bim 是作者設計的一種格式,是種 binary image file。
  5. 最後用 bim2hrb.exebootpack.bim 產生 bootpack.hrb
    這是將 bootpack.bim 依照我們自製 OS 的需要做點加工,成為最後的 bootpack.hrb

到這裡終於把 C 語言程式編譯成可以在自製 OS 上執行的 machine code,最後用 copy 指令將 asmhead.nas 組譯出來的 asmhead.binbootpack.hrb 結合起來,成為最終的 haribote.sys 。(原本 haribote.sys 是直接由 haribote.nas 組譯而來,現在有了 C 語言的部分,要把兩個部分結合起來才是完整的 haribote.sys

Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
TOOLPATH = ../z_tools/
INCPATH = ../z_tools/haribote/

MAKE = $(TOOLPATH)make.exe -r
NASK = $(TOOLPATH)nask.exe
CC1 = $(TOOLPATH)cc1.exe -I$(INCPATH) -Os -Wall -quiet
GAS2NASK = $(TOOLPATH)gas2nask.exe -a
OBJ2BIM = $(TOOLPATH)obj2bim.exe
BIM2HRB = $(TOOLPATH)bim2hrb.exe
RULEFILE = $(TOOLPATH)haribote/haribote.rul
EDIMG = $(TOOLPATH)edimg.exe
IMGTOL = $(TOOLPATH)imgtol.com
COPY = copy
DEL = del

default :
$(MAKE) img

; 組譯 IPL
ipl10.bin : ipl10.nas Makefile
$(NASK) ipl10.nas ipl10.bin ipl10.lst

; 組譯 OS 前面的組語部分
asmhead.bin : asmhead.nas Makefile
$(NASK) asmhead.nas asmhead.bin asmhead.lst

; 使用 cc1 compile bootpack.c 得到 gas 組語
bootpack.gas : bootpack.c Makefile
$(CC1) -o bootpack.gas bootpack.c

; 將 gas 組語轉成 nask 組語
bootpack.nas : bootpack.gas Makefile
$(GAS2NASK) bootpack.gas bootpack.nas

; nask 組譯
bootpack.obj : bootpack.nas Makefile
$(NASK) bootpack.nas bootpack.obj bootpack.lst

; linking
bootpack.bim : bootpack.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
bootpack.obj
# 3MB+64KB=3136KB

; 幫 bootpack.bim 做點加工好能在我們的 OS 上執行
bootpack.hrb : bootpack.bim Makefile
$(BIM2HRB) bootpack.bim bootpack.hrb 0

; 將兩個部份的 machine code 結合在一起
haribote.sys : asmhead.bin bootpack.hrb Makefile
copy /B asmhead.bin+bootpack.hrb haribote.sys

; 將 IPL 跟 OS 的 machine code 做成 floopy disk 的 image file
haribote.img : ipl10.bin haribote.sys Makefile
$(EDIMG) imgin:../z_tools/fdimg0at.tek \
wbinimg src:ipl10.bin len:512 from:0 to:0 \
copy from:haribote.sys to:@: \
imgout:haribote.img

img :
$(MAKE) haribote.img

run :
$(MAKE) img
$(COPY) haribote.img ..\z_tools\qemu\fdimage0.bin
$(MAKE) -C ../z_tools/qemu

install :
$(MAKE) img
$(IMGTOL) w a: haribote.img

clean :
-$(DEL) *.bin
-$(DEL) *.lst
-$(DEL) *.gas
-$(DEL) *.obj
-$(DEL) bootpack.nas
-$(DEL) bootpack.map
-$(DEL) bootpack.bim
-$(DEL) bootpack.hrb
-$(DEL) haribote.sys

src_only :
$(MAKE) clean
-$(DEL) haribote.img

在 C 語言程式 call 組語程式

如果想在 C 語言寫的程式 call 組語的程式該怎麼做呢?寫了個組語程式 naskfunc.nas 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; naskfunc
; TAB=4

[FORMAT "WCOFF"] ; 製作 object file 的模式
[BITS 32] ; 製作 32 bit mode 用的 machine code

; 製作 object file 的資訊

[FILE "naskfunc.nas"] ; source code file name

GLOBAL _io_hlt ; 程式中包含的 function name

; 以下是實際 function

[SECTION .text]

_io_hlt: ; void io_hlt(void);
HLT
RET ; 相當於 C 語言的 return

這段組語包含一個 _io_hlt function。

在 nask object file 模式下,要設定 file name 資訊並且寫清楚程式的 function name。function name 要在前面加上 _ ,不然不能跟 C 語言 function link。需要 link 的 function name 都要用 GLOBAL 指令宣告。

來看看在 C 語言裡要怎麼使用 _io_hlt function:

1
2
3
4
5
6
7
8
9
// 告訴 C compiler,這個 function 在別的 file 裡
void io_hlt(void);

void HariMain(void)
{
fin:
io_hlt();
goto fin;
}

這邊的 Makefile 跟前面的差異只有增加了 naskfunc.obj 的 target 跟修改 bootpack.bim target 的 link 內容:

1
2
3
4
5
6
7
naskfunc.obj : naskfunc.nas Makefile
$(NASK) naskfunc.nas naskfunc.obj naskfunc.lst

# 多 link naskfunc.obj
bootpack.bim : bootpack.obj naskfunc.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
bootpack.obj naskfunc.obj

到這天結束,我們已經有以下 source code:

  • ipl10.nas:IPL 程式
  • asmhead.nas:OS 的組語部份
  • bootpack.c:OS 的 C 語言部份
  • naskfunc.nas:給 C 語言部份使用的組語 function

來解釋這段組語在幹嘛~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
; hello-os
; TAB=4

ORG 0x7c00 ; 指明程式的 load 位置

; 以下的記述用於標準 FAT12 格式的磁片
JMP entry
DB 0x90

--- (中略) ---

; 程式核心
entry: ; label
MOV AX,0 ; initialize register
MOV SS,AX
MOV SP,0x7c00
MOV DS,AX
MOV ES,AX

MOV SI,msg ; SI = msg 的 address
putloop:
; 將 memory address 為 SI register 值的資料 assign 給 AL
MOV AL,[SI]
ADD SI,1 ; SI + 1
CMP AL,0 ; 比較 AL 的值是否為 0
JE fin
MOV AH,0x0e ; 顯示一個文字
MOV BX,15 ; 指定文字顏色
INT 0x10 ; call 顯卡 BIOS
JMP putloop
fin:
HLT ; 讓 CPU 停止,等待指令
JMP fin ; infinite loop

msg:
DB 0x0a, 0x0a ; 換行兩次
DB "hello, world"
DB 0x0a ; 換行
DB 0

ORG 指令告訴 nask 程式要從指定的 address 開始,即要把程式 load 到 memory 中指定的 address (這裡總有點半懂不懂)。有這個指令時,Day 1 從計算機結構到組合語言程式入門 提到的 $ 不再是 output file 的第幾個 byte,而是代表將要 read 的 memory address。

entry: 是 label,每個 label 都代表一個 address。位置的值是由 assembler 根據 ORG 計算出「label 所在的地方對應的 address」。

MOV 基本上就是 assignment,source 跟 destination 可以是 register、常數或者 memory address。

CPU 裡比較重要的 register 及其完整名稱:

  • AX:accumulator
  • CX:counter
  • DX:data
  • BX:base
  • SP:stack pointer
  • BP:base pointer
  • SI:source index
  • DI:destination index

這些 register 都是 16 bit。

另外 CPU 還有 8 個 8 bit register:

  • AL:accumulator low
  • CL:counter low
  • DL:data low
  • BL:base low
  • AH:accumulator high
  • CH:counter high
  • DH:data high
  • BH:base high

這些 register 是上面其中幾個 16 bit register 的高位跟低位而已,不是額外的 register。

那 32 bit register 呢?就是 EAX、ECX、EDX、EBX、ESP、EBP、ESI 跟 EDI 啦~也就是 16 bit register 再擴展一倍,實際上 EAX 有一部分是跟 AX 共用的,跟 8 bit register 與 16 bit register 的關係一樣。

MOV 中,以方括號 [ ] 包起來,表示 memory address。

MOV WORD [678], 123 表示把 123 以 WORD 的大小(16 bit)放到 memory address 678 的地方。數字 123 的二進位表示成 16 bit 是 0000000001111011 ,低位 byte 01111011 會放在 address 678(memory address 小),高位 byte 00000000 則會放在 address 679(memory address 大)。

位元組順序(Endianness)是指資料在 memory 或傳輸過程中 byte 的擺放順序。將資料的低位 byte 放在 memory address 小、高位 byte 放在 memory address 大的擺放方式稱為 Little-Endian。反之,資料低位 byte 放在 memory address 大、高位 byte 放在 memory address 小,稱為 Big-Endian。一般 x86 的機器是使用 Little-Endian。

指定 address 的方式,除了用常數,也可以用 register 的值。不過不是所有 register 都可以拿來做這件事,只有 BX、BP、SI 跟 DI 可以。所以 MOV AL, BYTE [SI] 是將 SI register 裡的值當作 address 去 memory 取得一個 byte 的資料並將 assign 給 register AL。另外,MOV 指令有個規則:source 跟 destination 的 byte 數必須相同。能放進 AL 的只有 BYTE,所以上面的指令可以省略 BYTE 變成 MOV AL, [SI]

JE:如果比較結果相等,則 jump 到特定的 address;若不等,則不 jump,繼續做下一個指令。

BIOS 是讓 OS 開發人員可以使用的各種 function 的集合。可以用指令 INT call 這些 function,它後面是個數字,不同數字代表不同 function,0x10 的功能是控制顯卡。

使用 BIOS 的 function 跟其他 level 的程式 call function 一樣,查要的功能是用哪個數字(像 function name),接著照 function 定義在各 register 放值(像 function 參數),register 設好後用 INT call function。

HLT 是讓 CPU 進入待機狀態的指令,才不會 infinite loop 瞎跑。

在整個 memory 裡,有些區域是有特定用途、不能隨便用的。其中 0x00007c000x00007dff 這段是 boot sector 的 loading address。所以 ORG 指令才要寫 0x00007c00 ,把這份程式 load 到那個 address 才會被當作 boot sector 來 run~(至於為什麼要是這個 address……不知道又是誰訂的XD)

電腦開機時 BIOS 會在可開機 device 中找 boot signature──在 boot sector 裡最後兩個 byte 0x55 AA。BIOS 找到這樣的 boot sector,便將 boot sector load 到 memory address 0x00007c00 的位置。接著便開始執行剛 load 進來的 boot record。floppy disk 的整個 boot sector 都是可執行 code(除了最後兩個 byte)。hard disk 的 MBR(Master Boot Record)在(硬碟 boot sector)位置 0x0000 ~ 0x01bd 是可執行 code,接著後面是 parition table(0x01be ~ 0x01fd)以及 boot signature(0x01fe ~ 0x01ff)。

因為 BIOS 會把 boot sector load 到 memory address 0x00007c00,所以用 ORG 指令就要寫 0x00007c00。這樣組譯出來的 machine code 裡的各個 memory address 才會是對的。

Ref

開始看買來放了一陣的《30 天自制操作系統》(簡中翻譯日文書),原本以為是本很硬很難啃的 OS 書,沒想到意外有趣!作者有各種各樣的 murmur 跟吐槽滿點。翻譯除了專有名詞,語句上蠻通順的。

Day 1 直接用 binary editor 幹出一個 hello world OS。

本書使用的組譯器是作者自己開發的 nask,其中很多語法模仿 NASM。

nask 指令 & 符號

  • DB:define byte,直接在 file 裡輸入一個 byte 的內容
  • RESB:reserve byte,從現在的位置空出幾個 byte
    • nask 會幫空出來的 byte 填上 0x00
  • DW:define word
    • word 在這邊是 16 bit,2 byte。
    • word 長度應該是因機器架構而異。
  • DD:define double word
    • double word 則是 32 bit,4 byte。

指令中符號 $ 代表 file 到目前這行所在的 byte 數。

術語解釋

  • boot sector(啟動區)
    • 電腦讀寫磁碟不是一個個 byte 讀寫,而是以 512 byte 為一個單位讀寫。512 byte 稱為一個 sector。
    • floppy disk 第一個 sector 稱為 boot sector。
      • 我猜對硬碟也適用
    • 電腦啟動時,會從第一個 sector 讀起,檢查這個 sector 最後兩個 byte 的內容。如果最後兩 byte 是 0x55 AA 則認為這個 sector 的開頭是啟動程序,接著開始執行這個程序。反之,則是沒有啟動程序,會出現啟動錯誤。
      • 至於為什麼是 0x55 AA ,就當初的人亂訂的吧……(爆
  • IPL:initial program loader 的縮寫
    • boot sector 只有 512 byte,通常 OS 都放不進去,所以 boot sector 通常是放 load OS 的 loader 程式。
    • 有時也將 boot sector 稱為 IPL
  • boot
    • 是 bootstrap 的縮寫
    • 有「自力更生完成任務」的意思(來源是德國《吹牛大王歷險記》)

Chapter Summary

為了能夠測試,有兩個進行解依賴的理由:

  • Sense(感測)是指「得知執行某一塊 code 後會有什麼樣的結果、產生什麼影響」
    • 著重在「不是簡單可以透過 return value 得知執行結果」的結果類型
    • 《單元測試》的互動測試
  • Separation(分離)是想把某段程式邏輯從它所在的地方分離出來,好能夠在測試裡使用

與《單元測試》比對

  • fake object
    • 《單元測試》中是各種假物件的通稱,包含 stub、mock object 都是 fake object。
    • 本書用來 sense 執行某段 code 的影響,比較像《單元測試》裡簡易的 mock object。
  • mock object
    • 兩書的概念一樣,都是互動測試用的。

Murmur

用不同方式看同一個東西的感覺真有趣~可以互相比較異同~

《Working Effectively with Legacy Code》對 unit test 跟更高層的 test 概念跟《單元測試》差不多,只是沒特別用 integration test 這個詞而已。

改 code 的兩種方法:edit and pray 以及 cover and modify XD

Summary

  • 要用 unit test 保護修改
    • 才知道新修改有沒有改對,以及有沒有改壞原本的東西
    • 跑測試的速度:需要 0.1s 才能跑完的 unit test 就算慢了
  • 修改 legacy code 步驟 overview
    1. 確定變動點(理解 code):ch 16、17
    2. 找出測試點:ch 11、12
    3. 解依賴的技術
      - 將 code 放進測試時,讓下手的第一刀更安全:ch 23
      - 解決一般的依賴問題:ch 9、10
      - 無法寫測試是因為沒解決大型 method 中的依賴:ch 22
      - build code 時間太長:ch 7
    4. 編寫測試:ch 13
    5. 修改及 refactor
      - 如何增加 feature:ch 8
      - 將 legacy code 變得結構更好:ch 20 ~ 22