首先要知道 hard disk 長怎樣、如何指定讀取哪個位置的資料,參考Day 3 進入 32 bit 模式並導入 C 語言

使用 BIOS 讀取 hard disk

BIOS 提供我們讀取 hard disk 的 routine,讓我們不用煩惱 chip 跟 bus 等硬體對讀取 hard disk 資料的問題。(BIOS 提供了一層抽象!)

要 call 這個 routine 是 intterupt 0x13 並且將 ah 設為 0x02。另外必須設置幾個 register 的值,好讓 BIOS 知道我們要讀取哪些 sector、要把讀到的資料放到 memory 的哪個位置:

  • dl:第幾個 drive,由 0 開始
  • ch:cylinder 編號
  • dh:磁片的哪一面,由 0 開始
  • cl:第幾個 sector,由 1 開始
  • al:讀取幾個 sector
  • es:預設存放資料的 memory 位置的 segment register
  • bx:預設存放資料的 memory 位置的 offset

BIOS 可能因為各種原因沒有成功讀取資料,因此我們需要處理錯誤的狀況。BIOS 在讀取有錯誤時會將 carry flag set 起來,另外在 al register 會有實際讀取多少 sector 的值。我們可以用以下程式來處理讀取 disk 的相關錯誤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
int 0x13

jc disk_error ; jump if carry flag was set

cmp al, <# of sector expected> ; 與我們預期讀取的 sector 數量比較
jne disk_error ; 不相等時跳到處理 error 的區域

disk_error:
mov bx, DISK_ERROR_MSG
call print_string
jmp $

DISK_ERROR_MSG: db "Disk read error!", 0

read hard disk routine

isk_load.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
disk_load:
push dx

mov ah, 0x02 ; BIOS read sector function
mov al, dh ; Read DH sectors
mov ch, 0x00 ; Select cylinder 0
mov dh, 0x00 ; Select head 0
mov cl, 0x02 ; Start reading from second sector
int 0x13 ; BIOS interrupt

jc disk_error

pop dx ; restore dx from the stack
cmp dh, al ; if al (sectors read) != dh (sectors expected)
jne disk_error ; display error message
ret

disk_error:
mov bx, DISK_ERROR_MSG
call print_string
jmp $

DISK_ERROR_MSG db "Disk read error!", 0

print_hex.asm

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
; prints the value of DX as hex.
print_hex:
pusha

mov ax, 5
loop: ; for (ax = 5; ax >= 2; ax--)
cmp ax, 2
jl end_loop
mov cx, 0
shr dx, 1 ; dx >> 1
jnc end_if_1
add cx, 1 ; if cf == 1, cx += 1
end_if_1:

shr dx, 1 ; dx >> 1
jnc end_if_2
add cx, 2 ; if cf == 1, cx += 2
end_if_2:

shr dx, 1 ; dx >> 1
jnc end_if_3
add cx, 4 ; if cf == 1, cx += 4
end_if_3:

shr dx, 1 ; dx >> 1
jnc end_if_4
add cx, 8 ; if cf == 1, cx += 8
end_if_4:

; only bx can be used as an index register
mov bx, HEX_OUT
add bx, ax ; bx = HEX_OUT + ax

cmp cx, 10
jl less_ten
mov byte [bx], 'a' ; [bx] = 'a'
sub cx, 10 ; cx -= 10
less_ten:
add [bx], cx ; [bx] += cx

sub ax, 1 ; ax--
jmp loop
end_loop:

mov bx, HEX_OUT
call print_string

popa
ret

print_string.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
print_string:           ; function name
pusha ; push all register to stack to preserve them

mov ah, 0x0e ; tty mode

loop_start:
cmp byte [bx], 0 ; compare [bx] which is one byte to zero, for null terminating char
je loop_end ; if [bx] == 0, end loop
mov al, [bx] ; move char printed to al
int 0x10 ; call print interrupt
add bx, 1 ; bx + 1, move to next char
jmp loop_start ; run loop body again

loop_end:
popa ; restore all register
ret ; return to callee

boot_sector_load_disk.asm

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
[org 0x7c00]

mov [BOOT_DRIVE], dl ; BIOS stores our boot drive in dl, so it's best to remember this for later

mov bp, 0x8000 ; set stack
mov sp, bp

mov bx, 0x9000 ; Load 2 sectors to 0x0000(es):0x9000(bx)
mov dh, 2
mov dl, [BOOT_DRIVE]
call disk_load

mov dx, [0x9000] ; print first loaded word
call print_hex

mov dx, [0x9000 + 512] ; print first word from the second loaded sector
call print_hex

jmp $

%include "print_string.asm"
%include "print_hex.asm"
%include "disk_load.asm"

; global variables
HEX_OUT: db '0x0000', 0
BOOT_DRIVE: db 0

times 510-($-$$) db 0
dw 0xaa55

times 256 dw 0xdada
times 256 dw 0xface

當 CPU 執行在 16-bit mode 時,表示 register 的最大 size 為 16 bit。這表示最大的 address 是 0xffff,也就是 memory 最大只能到 64KB。

那如果 memory 的 size 超過 64KB 該怎麼辦呢?

在 16-bit 下,我們可以用 special register csdsss 以及 es――稱為 segment register――來超過 64KB 的 memory size 限制。

我們將 memory 想像成被切成好幾塊 segment,這些 segment 以 segment register 為 index。當我們指定某個 16-bit address 時,CPU 會自動以某個 segment register 及我們指定的 address 計算出真正的 memory address。

其中這「某個」segment register 如何決定的?除非特別指定,否則 CPU 會以 instruction 的 context 來決定要用哪個 segment register。例如 mov ax, [0x34ef] 的 instruction 就會預設使用 data segment ds

那麼 CPU 是怎麼計算實際 address 的呢?

CPU 會將 segment register 的值乘 16,再加上我們指定的 address(也就是 offset)得到實際 address。因為我們通常用 16 進位表示 register 的值,例如 0x32,乘 16 就是將 16 進位的數字左移一格,變成 0x320。如果我們設定 ds0x32,則 mov ax, [0x20][0x20] 實際 address 是 0x320 + 0x20 = 0x340

另外,segment register 無法直接用 mov 來 assign 值,需要先將值放到 general register,再 mov 到 segment register 裡,如下:

1
2
mov ax, 0x123
mov ds ds, ax

segment-based 的 address 方式讓我們可以使用更多 memory,因為總共的 address 可以有 0xffff * 16 + 0xffff 個,也就是 1MB 多一點。

寫一個可以印出 hex 內容的 function。

shr 是 bitwise 的 right shift instruction。右移的最後一個 bit 會放到 carry flag 裡。可以用 jcjnc 來做 branch,jc 是如果 carry flag 有被 set 則 jump,後者是沒 set 的情況會 jump。

第一版:處理最後一個字

這個版本可以印出 0x0006。基本概念是用 right shift 得知 dx 最右邊的 bit 是 0 還是 1,以 4 個 bit 為一單位處理。

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
[org 0x7c00]

mov dx, 0x1fb6
call print_hex
jmp $

; prints the value of DX as hex.
print_hex:
pusha

; only bx can be used as an index register
mov bx, HEX_OUT
add bx, 5 ; bx = HEX_OUT + 5

shr dx, 1 ; dx >> 1
jnc end_if_1
mov cx, 1 ; if cf == 1
add byte [bx], cl ; [bx] += 1
end_if_1:

shr dx, 1 ; dx >> 1
jnc end_if_2
mov cx, 2
add byte [bx], cl ; [bx] += 2
end_if_2:

shr dx, 1 ; dx >> 1
jnc end_if_3
mov cx, 4
add byte [bx], cl
end_if_3:

shr dx, 1 ; dx >> 1
jnc end_if_4
mov cx, 8
add byte [bx], cl
end_if_4:


mov bx, HEX_OUT
call print_string

popa
ret

%include "print_string.asm"

; global variables
HEX_OUT: db '0x0000', 0

; padding and magic number
times 510-($-$$) db 0
dw 0xaa55

然後遇到超過 10 就會錯…因為 ASCII code 在 '9' 後面不是 'a'

第二版

修正 10 以上的錯誤

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
[org 0x7c00]

mov dx, 0x1fb6
call print_hex
jmp $

; prints the value of DX as hex.
print_hex:
pusha

mov ax, 5
; only bx can be used as an index register
mov bx, HEX_OUT
add bx, ax ; bx = HEX_OUT + ax

mov cx, 0
shr dx, 1 ; dx >> 1
jnc end_if_1
add cx, 1 ; if cf == 1
end_if_1:

shr dx, 1
jnc end_if_2
add cx, 2
end_if_2:

shr dx, 1
jnc end_if_3
add cx, 4
end_if_3:

shr dx, 1
jnc end_if_4
add cx, 8
end_if_4:

cmp cx, 10
jl less_ten
mov byte [bx], 'a'
sub cx, 10
less_ten:
add [bx], cx

mov bx, HEX_OUT
call print_string

popa
ret

%include "print_string.asm"

; global variables
HEX_OUT: db '0x0000', 0

; padding and magic number
times 510-($-$$) db 0
dw 0xaa55

第三版:加上外層 loop

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
[org 0x7c00]

mov dx, 0x1fb6
call print_hex
jmp $

; prints the value of DX as hex.
print_hex:
pusha

mov ax, 5
loop: ; for (ax = 5; ax >= 2; ax--)
cmp ax, 2
jl end_loop
mov cx, 0
shr dx, 1 ; dx >> 1
jnc end_if_1
add cx, 1 ; if cf == 1, cx += 1
end_if_1:

shr dx, 1 ; dx >> 1
jnc end_if_2
add cx, 2 ; if cf == 1, cx += 2
end_if_2:

shr dx, 1 ; dx >> 1
jnc end_if_3
add cx, 4 ; if cf == 1, cx += 4
end_if_3:

shr dx, 1 ; dx >> 1
jnc end_if_4
add cx, 8 ; if cf == 1, cx += 8
end_if_4:

; only bx can be used as an index register
mov bx, HEX_OUT
add bx, ax ; bx = HEX_OUT + ax

cmp cx, 10
jl less_ten
mov byte [bx], 'a' ; [bx] = 'a'
sub cx, 10 ; cx -= 10
less_ten:
add [bx], cx ; [bx] += cx

sub ax, 1 ; ax--
jmp loop
end_loop:

mov bx, HEX_OUT
call print_string

popa
ret

%include "print_string.asm"

; global variables
HEX_OUT: db '0x0000', 0

; padding and magic number
times 510-($-$$) db 0
dw 0xaa55

第四版:拆成多個檔案

print_string.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
print_string:           ; function name
pusha ; push all register to stack to preserve them

mov ah, 0x0e ; tty mode

loop_start:
cmp byte [bx], 0 ; compare [bx] which is one byte to zero, for null terminating char
je loop_end ; if [bx] == 0, end loop
mov al, [bx] ; move char printed to al
int 0x10 ; call print interrupt
add bx, 1 ; bx + 1, move to next char
jmp loop_start ; run loop body again

loop_end:
popa ; restore all register
ret ; return to callee

print_hex.asm

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
; prints the value of DX as hex.
print_hex:
pusha

mov ax, 5
loop: ; for (ax = 5; ax >= 2; ax--)
cmp ax, 2
jl end_loop
mov cx, 0
shr dx, 1 ; dx >> 1
jnc end_if_1
add cx, 1 ; if cf == 1, cx += 1
end_if_1:

shr dx, 1 ; dx >> 1
jnc end_if_2
add cx, 2 ; if cf == 1, cx += 2
end_if_2:

shr dx, 1 ; dx >> 1
jnc end_if_3
add cx, 4 ; if cf == 1, cx += 4
end_if_3:

shr dx, 1 ; dx >> 1
jnc end_if_4
add cx, 8 ; if cf == 1, cx += 8
end_if_4:

; only bx can be used as an index register
mov bx, HEX_OUT
add bx, ax ; bx = HEX_OUT + ax

cmp cx, 10
jl less_ten
mov byte [bx], 'a' ; [bx] = 'a'
sub cx, 10 ; cx -= 10
less_ten:
add [bx], cx ; [bx] += cx

sub ax, 1 ; ax--
jmp loop
end_loop:

mov bx, HEX_OUT
call print_string

popa
ret

%include "print_string.asm"

boot_sect.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[org 0x7c00]

mov dx, 0x1fb6
call print_hex
jmp $

%include "print_hex.asm"

; global variables
HEX_OUT: db '0x0000', 0

; padding and magic number
times 510-($-$$) db 0
dw 0xaa55

接下來我們可以把所有東西組合起來,寫一個 function 用迴圈印出字串!

boot_sect_print_main.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[org 0x7c00]

mov bx, HELLO_MSG
call print_string

mov bx, GOODBYE_MSG
call print_string

jmp $

%include "print_string.asm"

; data
HELLO_MSG:
db 'Hello, World!', 0
GOODBYE_MSG:
db 'Goodbye!', 0

; padding and magic number
times 510-($-$$) db 0
dw 0xaa55

print_string.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
print_string:           ; function name
pusha ; push all register to stack to preserve them

mov ah, 0x0e ; tty mode

loop_start:
cmp byte [bx], 0 ; compare [bx] which is one byte to zero, for null terminating char
je loop_end ; if [bx] == 0, end loop
mov al, [bx] ; move char printed to al
int 0x10 ; call print interrupt
add bx, 1 ; bx + 1, move to next char
jmp loop_start ; run loop body again

loop_end:
popa ; restore all register
ret ; return to callee

組譯再執行,可以看到以下畫面:

nasm 可以 include file,如下所示:

1
2
3
4
%include "print.asm"	; will be replaced by content of the file

...
call my_print ; my_print is in print.asm

在 CPU 的層級,call function 就是 jump 到 routine 的 address 再 jump 回原本所在的 address 繼續執行。

caller 跟 callee 會說好要怎麼傳參數,例如放在某個 register。

caller 的 address 也需要被儲存,這樣 callee 執行完才能回到 caller 繼續執行。CPU 以 register ip 記錄目前要執行的 instruction 的 address,但是我們不能直接 access register ip

CPU 提供兩個 instruction 來 call function:callretcall 會將 caller 的 address push 到 stack 接著 jump 到 callee 執行,ret 則會從 stack pop 出 address 並 jump 回到 caller 繼續執行。

接下來我們要考慮的是 register 的內容可能會被 callee 修改,那麼如何在 callee 執行結束後回到 caller 時 register 能夠有原本 caller 所擁有的值?

callee 可以將所有它用到的 register 在開始執行任何工作前先 push 進 stack,等到要 return 回 caller 前再全部 pop 回 register,這樣 caller 繼續執行時在 register 裡就有原本它所使用的值。

為了方便,CPU 提供兩個 instruction 能夠一次 push / pop 所有 register 的值到 stack:pusha 以及 popa

就是 jump。

jump 有 unconditional jump 跟 conditional jump。

conditional jump 會配合 comparison instruction 使用,例如:

1
2
3
4
5
6
7
8
cmp ax, 3
je then_block
mov bx, 1
jmp the_end

then_block:
mov bx, 2
the_end:

等同:

1
2
3
4
5
6
if (ax == 3) {
bx = 2
}
else {
bx = 1
}

comparison instruction 在比較後會設置 flags register 的值,接著 conditional jump instruction 就能依照 flags register 的內容決定要跳到哪執行。

對於 cmp x, y,conditional jump 有以下幾種:

1
2
3
4
5
6
je	target	; jump if equal (x == y)
jne target ; jump if not equal (x != y)
jl target ; jump if less than (x < y)
jle target ; jump if less than or equal (x <= y)
jg target ; jump if greater than (x > y)
jge target ; jump if greater than or equal (x >= y)

將產生某種類物件的邏輯跟動作包成一個 class。

這樣的 class 為一個 factory class,它的 method 依據 input 產生出對應的具體 object。如果要新增不同的物件只需修改該 method。

UML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Factory {
+createOperate()
}

abstract class Operate {
+X
+Y
+GetResult()
}

class OperateAdd {
+GetResult()
}

class OperateSub {
+GetResult()
}

Factory -> Operate
OperateAdd -up-|> Operate
OperateSub -up-|> Operate

如果要增加 OperateMul 表示乘法,只需要加一個 class OperateMul 並且在 Factory::createOperate() 的 switch 增加一個 case 即可。這樣增加乘法完全不會動到加法及減法。

Go 的 slice 可以看成一個帶有一些資訊及指向底層 array 指標的 struct。append() 會依照底層 array 擁有的空間以及空間擴張演算法決定是否 allocate 一塊新的 array 來當底層 array。

以下用兩個例子顯示有時候 append() 會 allocate 新的底層 array,有時不會。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
x := make([]int, 5)

fmt.Println("x =", x)
fmt.Println("---")

y := append(x, 3)
fmt.Println("x =", x)
fmt.Println("y =", y)
fmt.Println("---")

x[0] = 123
fmt.Println("x =", x)
fmt.Println("y =", y)
}

執行結果:

1
2
3
4
5
6
7
x = [0 0 0 0 0]
---
x = [0 0 0 0 0]
y = [0 0 0 0 0 3]
---
x = [123 0 0 0 0]
y = [0 0 0 0 0 3]

這個例子可以看到 append 後的 y 底層的 array 已經跟 x 底層的 array 不同。

再來看看底層 array 相同的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
x := make([]int, 5)
x = x[:3]

fmt.Println("x =", x)
fmt.Println("---")

y := append(x, 3)
fmt.Println("x =", x)
fmt.Println("y =", y)
fmt.Println("---")

x[0] = 123
fmt.Println("x =", x)
fmt.Println("y =", y)
}

執行結果:

1
2
3
4
5
6
7
x = [0 0 0]
---
x = [0 0 0]
y = [0 0 0 3]
---
x = [123 0 0]
y = [123 0 0 3]

從結果可以看到,當把 x[0] 設為 123 時 y[0] 也變成 123,表示 xy 的底層 array 是相同的。

我們不知道 append() 操作後,是否會重新分配 array。因此不能假設 append() 操作後產生的 slice 跟原本的 slice 是同一個,通常會寫成:

1
nums = append(nums, x)

CPU 提供兩個指令 push 以及 pop,讓我們可以方便的塞東西進 stack 以及從 stack 拿東西出來。

在 16-bit mode 中,我們要以 16 bit 為單位 push / pop 東西至 stack。

stack 由兩個特殊的 register 實作:bpsp。它們分別記錄 stack 的 base(stack 底部)以及 stack top。

stack 長的方向不是「往上」(memory address 增加),而是「往下」(memory address 減少)。也就是如果我們一開始將 stack base 設在 0x8000,push 一個值後,sp 會是 0x8000 - 0x2 = 0x7ffe。之所以是減 0x2 是因為一次 push 的 element 的大小是 16 bit。

由下面的例子可以確認 stack 的增長方向是往 memory address 小的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mov ah, 0x0e

mov bp, 0x8000 ; setup stack base to 0x8000
mov sp, bp ; setup stack top

push 'A'
push 'B'
push 'C'

pop bx ; element size is 16 bit, so use bx
mov al, bl
int 0x10 ; print 'C'

mov al, [0x7ffe]
int 0x10 ; print 'A'

mov al, [0x7ffc]
int 0x10 ; print 'B'

jmp $

times 510-($-$$) db 0
dw 0xaa55

中間 pop 之所以要先 pop 至 bx 再從 bl 中拿值放到 al 印出,是因為 pop 出來的 size 是 16 bit,而其中有內容(字元的 ASCII code)是低位的部份,高位部份會是 0。

0%