動機

消除 duplicated member。

要了解 member 被使用的方式,如果被使用的方式類似,就可以移到 super class。除了移除 duplicated member 之外,也可以把 subclass 使用 member 的行為移到 supoer class(如果使用方式相似)。

作法

  1. 檢查候選 member 的所有使用方,確定它們用同樣的方式使用 member
  2. 如果 member 名稱彼此不同,使用 Rename Field 讓它們的名稱一致
  3. 在 super class 建立一個新 member
    這個新 member 要可以被 subclass 使用(一般語言通常是 protected)
  4. 刪除 subclass 的 member
  5. 測試

PhpStorm 操作

用 Docker Compose 啟動程式所需的 service 並且以 docker compose 的 container 執行 test。

Docker

先在 project 裡增加 Dockerfile

1
2
3
4
5
6
7
8
9
10
FROM php:8.0-cli

RUN apt-get update && \
apt-get install -y \
libpq-dev \
libzip-dev \
libpng-dev \
libjpeg62-turbo-dev && \
docker-php-ext-configure gd --with-jpeg && \
docker-php-ext-install pdo pdo_pgsql gd

這邊會安裝 libzip-devlibpng-devlibjpeg62-turbo-dev 並設定與安裝 php extension gd 是為了 image upload 的 test。

build 出 image 並且上 tag:

1
2
$ docker build .
$ docker tag <image_id> cjwind/phpstorm-php8.0-cli

Docker Compose

增加 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
version: "3"
services:
php:
image: cjwind/phpstorm-php8.0-cli:latest # docker image
deploy:
replicas: 1
volumes:
- ".:/opt" # mapping . in host to /opt in container
database:
image: postgres:12.3
container_name: "postgres"
deploy:
replicas: 1
environment:
- POSTGRES_PASSWORD=<your_password>

.env.testing

1
2
3
4
5
6
DB_CONNECTION=pgsql
DB_HOST=postgres # container name of database
DB_PORT=5432
DB_DATABASE=DB
DB_USERNAME=USERNAME
DB_PASSWORD=PASSWORD

PhpStorm Setting

Docker

因為 OS 是 Linux 所以選 Unix socket。

Troubleshooting

如果 connection 連不上,先檢查有沒有啟動 Docker daemon,有的話看看 user 是不是在 docker 這個 group 裡,沒有在 group 裡就把 user 加進 docker group、logout、login、重新啟動 PhpStorm 再試一次。

CLI Interpreter

按左上角的 + 選 From Docker

使用 Docker-compose 並且設定 config file 以及 service。設完可以看到第一張圖的樣子。

CLI Interpreter 選擇剛剛新增的 interpreter。

Run/Debug Configurations

service

按左上角的 + 選 Docker-compose

設定 Server 以及 Compose file path。

Run service

可以在 service window 看到跑起來的 container。

點 container 可以看到 log 啊 properties 等等資訊。

test

設定 test,使用 default interpreter。

Run test 可以看到是用 docker compose 執行。

About Docker Network

這裡只記錄一點點 docker network 有關的東西。

在 default 下,docker compose 裡的 container 的 network 是 bridge mode。

可以用 docker network ls 看到現在有哪些 network。

再用 docker network inspect NETWORK_NAME 可以看到該 network 的詳細資料,在其中 Containers 可以看到 container 被分配的 IP address。

Ref:https://ithelp.ithome.com.tw/articles/10206725

來了解一下 C 語言~

Generate raw machine code

讓我們從一個簡單的 C 程式開始,看看會轉成什麼樣的組合語言:

1
2
3
int foo() {
return 0xbaba;
}

用以下指令 compile:

1
$ i686-linux-gnu-gcc -fno-pie -ffreestanding -c basic.c -o basic.o

這會產生 object file basic.o。比起直接 compile 出可以執行的 machine code,compiler 會輸出的是帶有 meta 資訊的 machine code,meta 資訊保留了這些 code 最後會如何連接在一起的資訊。這麼做的好處是這段程式碼可以被 relocate 到其他 binary file 裡,因為在 object file 裡的 address 都是相對位置而非絕對位置。

freestanding 是沒有 standard library 存在而且程式的 entry 也不需要是 main() 的環境。因為是開發 kernel,所以當然沒有 standard library,也要自己想辦法跳到 kernel 的 main() 開始執行。-ffreestanding 是要讓 compiler 產生在 freestanding 環境的 code。Ref

至於 -fno-pie 則是讓 compiler 不要產生 position independent executables 的 code。PIE 是什麼可以參考這篇。在我們要寫 kernel 的環境下,不需要 PIE 的功能。另一方面,啟動 PIE 就要有 Global Offset Table(GOT)才能 link,我們當然沒做 GOT 啊~

我們可以用以下指令來看看 object file 的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ i686-linux-gnu-objdump -d basic.o

basic.o: file format elf32-i386


Disassembly of section .text:

00000000 <foo>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: b8 ba ba 00 00 mov $0xbaba,%eax
8: 5d pop %ebp
9: c3 ret

要產生出真正可以執行的 code,我們需要 linker 來把所有 object file link 起來。link 過程中會把相對 address 轉換成絕對 address。例如 call <function_foo_label> 會被轉換成 call 0x123450x12345 是 label function_foo_label 最後在檔案中的 offset。

這裡只有一個檔案,我們只需要用以下 command 來產生 raw machine code:

1
$ i686-linux-gnu-ld -o basic.bin -Ttext 0x0 --oformat binary basic.o

跟 compiler 一樣,linker 可以 output 出很多種格式的檔案。其中一種是帶有 meta 資訊的可執行檔,這種檔案可以在 OS 上執行,這些 meta 資訊會描述這些 code 要如何被 load 到 memory 中,也可能帶有方便 debug 的資訊。

那因為我們是要寫 OS,所以不要這些 meta 資訊,免得 CPU 以為這些是可以執行的指令。所以在上面的指令裡,我們要指定 output format 是 raw binary

至於 -Ttext 0x0,它的效果跟前面組語裡寫的 org 是一樣的,表示程式會被 load 到的 memory 位置(也可以說是在 memory 中的整體 offset)。

這樣我們就得到前面 function 的 raw machine code 了!來反組譯看看它的內容:

1
2
3
4
5
6
7
8
$ ndisasm -b 32 basic.bin

00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 B8BABA0000 mov eax,0xbaba
00000008 5D pop ebp
00000009 C3 ret
...

可以看到進入 function 時會先 push ebp 到 stack 裡保存,等到 function 要 return 前恢復,這樣回到 caller 才不會搞亂 caller 的 register 內容。

接著將 esp 的值 assign 給 ebp,也就是將 stack base pointer 設定成目前 stack top 的位置,這麼做就是在原本的 stack 上再做出一個 stack 給目前這個 function 使用,這個動作是在為目前的 function 設定它的 stack frame。stack frame 裡會儲存 function 的 local variable。

setup 好 stack frame 後,0xbaba 被 assign 給 register eax,這是因為 register eax 是 C 語言用來放 return value 的 register,caller 會預期 callee 的 return value 會放在 eax

Local Variables

接下來我們寫這樣一個 function:

1
2
3
4
int foo() {
int x = 0xbaba;
return x;
}

將它 compile 成 raw binary 再反組譯,得到:

1
2
3
4
5
6
7
00000000  55                push ebp
00000001 89E5 mov ebp,esp ; setup stack frame, stack bottom = stack top
00000003 83EC10 sub esp,byte +0x10 ; allocate 16 bytes on the top of stack
00000006 C745FCBABA0000 mov dword [ebp-0x4],0xbaba ; 將資料存到 stack 上
0000000D 8B45FC mov eax,[ebp-0x4] ; 將 local variable 的值放進 eax 作為 return value
00000010 C9 leave ; restore stack for caller
00000011 C3 ret

一開始先在 stack 保存 ebp,接著 setup stack frame,再來由 sub esp,byte +0x10 在 stack top 保留 16 byte 的空間。但我們明明是要儲存 4 byte 的 int,為什麼需要保留 16 byte 呢?這是因為 CPU 在 memory 的 data 有對齊(alignment)的時候,會有比較好的效能,這是 CPU 最佳化的一個方式。所以雖然我們只要存 4 byte 的資料,但為了讓資料能對齊,會採用最大的 datatype width 16 byte 來放每個 stack 中的 element。dword 表示 double word,也就是 4 bytes。

最後的 leave 是用來恢復 stack 的,等同以下:

1
2
mov esp, ebp		; restore stack top
pop ebp ; restore stack bottom

Calling Functions

再來讓我們看看 C 語言在 call function 時會做些什麼事:

1
2
3
4
5
6
7
int callee(int arg) {
return arg;
}

void caller() {
callee(0xdede);
}

compile 後再反組譯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
; callee()
00000000 55 push ebp
00000001 89E5 mov ebp,esp ; setup stack frame
00000003 8B4508 mov eax,[ebp+0x8] ; 往 stack 底部多 8 byte 取得參數資料
00000006 5D pop ebp
00000007 C3 ret

; caller()
00000008 55 push ebp
00000009 89E5 mov ebp,esp ; setup stack frame
0000000B 68DEDE0000 push dword 0xdede ; push data into stack
00000010 E8EBFFFFFF call 0x0 ; call callee()
00000015 83C404 add esp,byte +0x4
00000018 90 nop
00000019 C9 leave
0000001A C3 ret

callee()mov eax, [ebp+0x8] 取得 caller 傳給它的參數。因為 stack 在 memory 中是往 address 小的位置長的,所以加上 8 byte 實際上是往「stack 的底部再往下 8 byte」的位置,也就是 caller push 參數的位置。

C 語言預設的 calling convention 是將 argument 以相反的順序 push 到 stack,所以第一個 argument 會在 stack top。

Pointers

在解讀一個 memory 位置上的資料時,除了 memory address 之外,還需要知道這個資料有多長,是 1 byte?2 byte?還是 4 byte?否則會解讀錯誤。

C 語言的 pointer,在 32 bit 下,其值的長度都是 32 bit。而它指到的 memory address 內的資料的長度則可以從 pointer 的 type 知道。例如 *int 就是個指向 4 byte 資料的 pointer。

C 語言裡我們會用 *char 來表示 string,這是因為 string 是一個 char 的 array,我們不知道它確切上有多長。

因為現在電腦都是 64 bit,compiler 預設也是 64 bit,那我們要 compile 出 32 bit 的 code,要安裝 cross compiler,也就是跨平台的 compiler。

install cross compiler toolchain in Debian 10:

1
$ sudo apt install gcc-multilib-i686-linux-gnu
  • compile 時用 i686-linux-gnu-gcc
  • link 時用 i686-linux-gnu-ld

電腦一開機我們是在 16-bit mode 裡,為了能使用更多 memory 以及各種 CPU 強大的功能,我們要切換到 32-bit mode。

首先要暫停 interrupt handling,因為 interrupt handling 在 16-bit mode 跟 32-bit mode 的實作方式完全不一樣。暫停 interrupt handling 的方式是使用 cli instruction,它會 clear interrupt flag。

接著告訴 CPU 我們設好的 GDT 在哪裡:

1
lgdt [gdt_descriptor]

最後透過 set control register cr0 的第一個 bit 做 mode switch。我們無法直接 set cr0,要先把它的 value load 到 generral register,set bit 再 store 回 cr0

1
2
3
mov eax, cr0
or eax, 0x1
mov cr0, eax

cr0 update 之後,CPU 就是在 32-bit mode 了。

要注意現代的 CPU 都有 pipeline 機制,可以同時做 instruction 不同 stage。例如每個 instruction 要先從 memory fetch,接著 decode,再 execute,最後 store 回 memory。因為這四個階段是可以獨立運作的,所以他們可以在同一個 CPU cycle、不同 circuitry 同時被執行,例如 execute 目前 instruction 時同時 fetch
下一個 instruction。

一般使用 CPU 不需要考慮 CPU 內部 pipeline 的問題,但在 switch CPU mode 時就要注意,不然可能會有些 stage 在錯誤的 mode 中執行。

我們要做的是在切換到 32-bit mode 後,立刻強制 CPU 結束所有在 pipeline 中的工作。我們利用 jump 到很遠的 code 的方式來達到強制 CPU 結束所有正在 pipeline 中的工作。這麼做的原理是因為 CPU 在 pipeline 的運作下會 pre-fetch instruction 來知道下一個 instruction 是什麼,但像是 jmpcall instruction 就會讓 CPU 無法知道下一個 instruction 是什麼,進而達到 flush CPU pipeline 的作用。

要做一個很遠的 jump,要額外提供 target segment,如下:

1
jmp <segment>:<address offset>

https://github.com/cjwind/mini-os/tree/main/bootsect 這裡有完整的從 16 bit mode 進到 32 bit mode 的 boot sector 程式,成功組譯執行可以看到以下畫面:

要注意的是 32 bit mode 在印東西時會從左上角開始印,這是因為我們直接把值寫在 video memory,沒有特別計算顯示字元的位置。

16 bit mode 是以 segment based 的方式 access memory。32 bit mode 將 logic address 轉成 physical address 的方式跟 16 bit mode 不同。在 32 bit mode 下,segment register 的值代表的是 global descriptor table 中某個 segment descriptor 的 index。

segment descriptor 是個 8 byte 的 structure,包含以下資訊:

  • base address (32 bits):定義 segment 從 physical memory 的起始位置
  • segment limit (20 bit):segment 的 size
  • flags:這些 flag 會影響 CPU 如何解讀 segment,例如 privilige level 跟這塊 segment 是不是 read-only 或 write-only 等。

下圖是 segment descriptor 的結構:

最簡單的 segment configuration 稱為 basic flat mode,是由兩個重疊的 segment 組成,一個是給 code 使用,另一個是放 data。這兩個 segment 會 cover 整個 4GB 的 memory。那也因為這兩個 segment 位置是重疊的,當然不可能有什麼保護機制(保護一個 segment 不被另一個 segment 亂搞之類的)。

GDT 的 descriptor entry 就是用來描述每個 segment 的。除了 code 跟 data segment 之外,GDT 的第一個 descriptor 必須要是 null descriptor,內容是 8 byte 的 0。這是在轉換到 protcted mode 時,如果不小心忘記設定 segment register,會讓我們剛好 access 到 null descriptor。而 CPU 在 access 到 null descriptor 的時候會 raise exception(interrupt)讓我們知道有錯誤。

code segment 的 descriptor configuration:

  • Base:0x0
  • Limit:0xffff
  • Present:1,表示這個 segment 是 present 在 memory 中。這個 config 之後會用於 virtual memory。
  • Privilege:0,ring 0 是最高 privilige
  • Descriptor type:1,code 或 data segment
  • Type:
    • Code:1,因為這是 code segment
    • Confirming:0,比較低 privilege 的 segment 不能 call 這個 segment 的 code,這是 memory protection 的關鍵
    • Readable:1,1 為 readable,0 為 execute only
    • Accessed:0,這個參數通常用在 virtual memory 的 debug
  • Other flags:
    • Granularity:1,這會將 Limit 乘以 4K(16 * 16 * 16),所以 0xffff 的 limit 會變成 0xffff000
    • 32-bit default:1,因為我們的 segment 會放 32-bit code。0 則是 16-bit。這實際上是設定 operation 的 default size,例如 push 0x4 到 stack 的時候,會自動被擴展成 32-bit 的數字。
    • 64-bit code segment:0,32-bit CPU 不會用到
    • AVL:0,這是可以設給自己使用的 bit

因為我們的 data segment 跟 code segment 位置是重疊的,所以除了以下的 type flag 之外,data segment 的 configuration 跟 code segment 是一樣的:

  • Code:0
  • Expand down:0
  • Writable:1
  • Accessed:0

Define the GDT in Assembly

我們會用 dbdwdd 這幾個指令來放 bytes、建立 GDT。

CPU 需要知道 GDT 有多長,我們會用 GDT descriptor 來描述 GDT,GDT descriptor 包含:

  • GDT size (16 bit)
  • GDT address (32 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
gdt_start:

gdt_null: ; the mandatory null descriptor
dd 0x0 ; dd means define double word (4 bytes)
dd 0x0

gdt_code: ; the code segment descriptor
; base = 0x0, limit = 0xfffff
; 1st flags: (present)1 (privilege)00 (descriptor type)1 -> 1001b
; type flags: (code)1 (conforming)0 (readable)1 (accessed)0 -> 1010b
; 2nd flags: (granularity)1 (32-bit default)1 (64-bit seg)0 (AVL)0 -> 1100
dw 0xffff ; Limit (bits 0-15)
dw 0x0 ; Base (bits 0-15)
db 0x0 ; Base (bits 0-15)
db 10011010b ; 1st flags, type flags
db 11001111b ; 2nd flags, Limit (bits 16-19)
db 0x0 ; Base (bits 24-31)

gdt_data: ; the data segment descriptor
; Same as code segment except for the type flags:
; type flags: (code)0 (expand down)0 (writable)1 (accessed)0 -> 0010b
dw 0xffff ; Limit (bits 0-15)
dw 0x0 ; Base (bits 0-15)
db 0x0 ; Base (bits 0-15)
db 10010010b ; 1st flags, type flags
db 11001111b ; 2nd flags, Limit (bits 16-19)
db 0x0 ; Base (bits 24-31)

gdt_end:

; GDT descriptor
gdt_descriptor:
dw gdt_end - gdt_start - 1 ; Size of GDT, always less one of the true size
dd gdt_start ; Start address of GDT

; segment descriptor offset
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

32-bit protected mode 與 16-bit real mode 的差別:

  • register 變成 32 bit
  • 多了兩個 general purpose 的 segment register,fsgs
  • 可以用 32-bit memory offset 了,所以可以 reference 到 4 GB 的 memory
  • memory segmentation 的機制變得更好,有以下優點:
    • 在 priviledge 比較高的 segment 的程式可以終止在 priviledge 比較低的 segment 的程式。
    • CPU 可以為 user process 使用 virtual memory 機制(像是 page),可以讓 memory 有更有效率的使用。
  • interrupt 變得更完善

要進入 32 bit protected mode,我們必須設置 GDT(global descriptor table),它定義了 memory segment 們跟它們的 protected-mode attribute。

另外進入 32 bit mode 後,就不能用 BIOS 了。因為進到 32 bit 沒有 BIOS 可用,32 bit OS 必須要自己提供所有硬體的 driver,才能使用這些硬體。

如何在螢幕上顯示訊息

我們要使用 32 bit mode 又沒有 BIOS 的情況下,首先遇到一個問題:怎麼在螢幕上顯示訊息呢?就像前面的 print_string routine。

display device 可以被設定成 text 模式與 graphics 模式兩種。而什麼東西會顯示在螢幕上——也就是螢幕的哪個 pixel 要亮——是由一塊表示螢幕上每個 pixel 的 memory 區域控制的。因此我們要在螢幕上顯示東西,等同在那塊 memory 寫入資料。兩種模式會用不同的 memory 區域,操作時要注意。

在 text 模式下,我們只需要在一塊特定的 memory 裡指定我們要印的字元以及其 attribute 即可畫出一個字元。這塊 memory 從 0xb8000 開始,用兩個 bit 表示一個字元,第一個 bit 是要印字元的 ASCII code,第二個 bit 則是 attribute,例如顏色。

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
[bits 32]

VIDEO_MEMORY equ 0xb8000
WHITE_ON_BLACK equ 0x0f

print_string_pm:
pusha
mov edx, VIDEO_MEMORY

print_string_pm_loop:
mov al, [ebx]
mov ah, WHITE_ON_BLACK

; if encounter null terminate
cmp al, 0
je print_string_pm_done

; write data into video memory, 1 char has 2 bits in video memory
mov [edx], ax

; index of string and video memory
add ebx, 1
add edx, 2

jmp print_string_pm_loop

print_string_pm_done:
popa
ret

首先要知道 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
0%