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。

程式要在 memory 裡才能執行,那麼我們寫在 boot sector 裡的程式,也是被 BIOS load 到 memory 中的某個地方才得以執行的。

這個位置在 0x7c00,也就是 BIOS 會將 boot sector 的內容 load 到 memory 0x7c00 位置的地方。

以下是開機後的 memory layout:

接下來希望可以印出放在 memory 內的資料。

我們用些方式來嘗試印印看:

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

; first attempt
mov al, the_secret
int 0x10

; second attempt
mov al, [the_secret]
int 0x10

; third attempt
mov bx, the_secret
add bx, 0x7c00
mov al, [bx]
int 0x10

jmp $ ; infinite loop

the_secret:
db "X"

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

label 表示從 code 一開始到該位置的 offset,the_secret 的值即是從 code 最開始到該位置的 offset。

[bx] 是是去 bx register 內容所代表的的 memory address 拿資料,例如 bx 的值是 0x1e,則 [bx] 就會是 memory address 0x1e 位置上的值,而非這段 code 被 load 到的 memory address 再加上 offset。

最後只有第三種方式可以成功印出 X,這印證了 boot sector 確實被 load 到 memory 0x7c00 的位置。

既然 BIOS 會把 boot sector load 到 0x7c00,這表示對每個 label 我們都要自己加上 0x7c00 才能得到正確的 address。每次要在 offset 加上 0x7c00 也太麻煩,所以可以在最開頭加上以下指令,就能表示「這段 code 預期會被 load 到 memory 哪個位置」,也就不用自己手動加:

1
[org 0x7c00]

加上這行後,就會是第二種方式印出 X 了。

Define strings

定義 string 需要知道關於 string 的兩件事:

  1. string 所在 memory address
  2. string 長度

前者可以用 label,如下所示:

1
2
my_string:
db 'Hello World'

my_string 就是 string Hello World 的 memory address。指令 db 是 declare byte(s) of data 的意思,也就是直接將這幾個 byte 寫進 binary output file,而不要把它們當作 instruction。

那麼如何知道 string 的長度呢?

一種 convention 是定義 null-terminating string,也就是在 string 結尾加上一個 byte,其值為 0,像這樣:

1
2
my_string:
db 'Hello World',0

這樣在處理 string 時,只要遇到值為 0 的 byte 就知道 string 結束了。

我們要利用 BIOS 提供的功能來在螢幕上印出 Hello World。(如果沒有 BIOS,這個工作會變得很困難,因為有很多種不同的螢幕,每個螢幕的硬體跟 interface 都不相同…)

那我們要怎麼使用 BIOS 的功能呢?

這就要說到 interrupt。

Interrupt

interrupt 是個讓 CPU 暫時停下手上正在做的事、轉去做比較優先的事再回來繼續的機制。interrupt 可以由 software 發出,也可由 hardware device 發出。

每個 interrupt 以一個數字表示,這個數字是 interrupt vector 的 index。interrupt vector 是個由 BIOS 初始化的 table,裡面記錄著指向 interrupt service routine (ISR) 的 address pointer。一個 ISR 就是一段處理特定 interrupt 的 machine code。

那麼,BIOS 在 interrupt vector 中放了一些 ISR,每個 ISR 表示某些部份的功能,例如 0x10 跟螢幕有關、0x13 則跟 disk 有關。

如果為每個 BIOS routine 都指定一個 interrupt,有點浪費,而且 interrupt vector 的大小也是有限的,所以會透過 interrupt 配合 register ax 的值來決定執行哪個 BIOS routine。

印出 Hello

印字的方式是使用 INT 0x10(video service 的 interrupt)配合 ALAH register 內的值。register AL 裡放要印的 character,AH 則是放 0x0eALAX 的 lower part、AH 則是 higher part)。AH0x0e 表示要 call「將 AL 內容寫到在 tty mode 下寫到螢幕上」的 function。

在這個例子裡,我們只需要設定一次 AH0x0e,因為只有一個 process 在 CPU 上跑,AH 的值不會被亂改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mov ah, 0x0e ; tty mode
mov al, 'H'
int 0x10
mov al, 'e'
int 0x10
mov al, 'l'
int 0x10
int 0x10 ; al is still 'l'
mov al, 'o'
int 0x10

jmp $ ; jump to current address = infinite loop

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

一樣的組譯並執行:

1
2
$ nasm -f bin boot_sect_hello.asm -o boot_sect_hello.bin
$ qemu-system-x86_64 boot_sect_hello.bin

會看到像這樣的畫面:

variadic function(可變函式)可用不同數量的參數呼叫,最常見的是 fmt.Printf 系列。

宣告 variadic function 時,在最後一個參數的 type 前面加上 ...,表示這個 function 可以用任意數量的該 type 參數呼叫。

1
2
3
4
5
6
7
func sum(values ...int) int {
total := 0
for _, val := range values {
total += val
}
return total
}

在此 function 中,values 的 type 是 []int,call sum() 時可以給任意數量的 int

1
2
3
sum()
sum(3)
sum(1, 2, 3)

caller 會 implicit 分配 array、複製參數,然後將整個 array 的 slice 傳給 sum()sum(1, 2, 3) 的行為類似:

1
2
values := []int{1, 2, 3}
sum(values...)

參數已經在 slice 中時,以上述方式 call variadic function,即在最後一個參數後面加上 ...

雖然 ...int 參數在 function 中的行為如同 slice,但是 func f(...int) {}func g([]int) {} 兩個 function 的 type 是不同的。

1
2
3
4
5
6
7
8
; Infinite loop (e9 fd ff)
loop:
jmp loop

; Fill with 510 zeros minus the size of the previous code
times 510-($-$$) db 0
; Magic number
dw 0xaa55

組譯:

1
$ nasm -f bin boot_sect.asm -o boot_sect.bin
  • -f 為 format
  • -o 為 output file

這樣就可以產生 01 Boot Process 用 binary 寫的 512 byte 的 disk image 並且最後兩個 byte 值是 0xaa55。可以用 xxd 看一個檔案的 hex dump。

最後用 QEMU 執行!

1
$ qemu-system-x86_64 boot_sect.bin

可以在畫面看到 Booting from Hard Disk... 然後停住。

16-bit Real Mode

現代 CPU 都不是 16-bit 了,但為了 backward compatibility,CPU 一啟動都是在模擬出來的 16-bit real mode,之後才會轉到 32-bit 或 64-bit protected mode。

當我們說 16-bit CPU 時,表示 instruction 在一個 cycle 中一次最多只能處理 16 bit。

有三種方式測試前面最簡單的 boot sector:

  • 想辦法把它直接寫進某個 hard / floopy disk 之類的第一個 sector :D
  • 用像是 VMWare 或 VirtualBox 的 VM 軟體,把這個 image 當 disk image 然後開機
  • 使用如 Bochs 或 Qemu 的 CPU emulator
    CPU emulator 是個程式,會模擬某種 CPU 架構的運作。

要用 CPU emulator 來跑的 file,其格式要是 disk image,也就是 machine code 以及 data 的 raw data,即會直接寫在 hard disk 之類 media 上的 data。

Qemu

在 Debian 安裝好 Qemu 後,以 Qemu 執行 disk image:

1
$ qemu-system-x86_64 <disk-image-file>

  1. Source stage 選 ECR 及對應的 registry
    Source stage 會產生 artifact imageDetail.json
  2. Build stage 選 CodeBuild
    CodeBuild 的 buildspec 如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    version: 0.2
    phases:
    build:
    commands:
    - ContainerName="[CONTAINER_NAME]"
    - ImageURI=$(cat imageDetail.json | jq -r '.ImageURI')
    - printf '[{"name":"CONTAINER_NAME","imageUri":"IMAGE_URI"}]' > imagedefinitions.json
    - sed -i -e "s|CONTAINER_NAME|$ContainerName|g" imagedefinitions.json
    - sed -i -e "s|IMAGE_URI|$ImageURI|g" imagedefinitions.json
    - cat imagedefinitions.json

    artifacts:
    files:
    - imagedefinitions.json
    這是把 Source 產生的 imageDetail.json 產生 imagedefinitions.json。要把其中的 [CONTAINER_NAME] 改成 container 名稱。
  3. Deploy stage 選 ECS,選擇對應的 cluster 跟 service
  4. 調整 ECS minimum healthy percentage 跟 maximum percentage 的值以及 cluster 內 EC2 instance 的數量,沒調整好 deployment 可能會卡住。
  5. 可以從 ECS service deployment 看 deploy 的狀況。

電腦一開機,會從 chip load BIOS 到 memory 中,並且 initialize 它,這時候我們能用的功能只有 BIOS。

因為這時候還沒有 file system 的概念,BIOS 無法像 load file 一樣從一個 file load OS,只能讀取特定 sector 的 data。通常一個 sector 的 size 是 512 byte,並且以 cylinder I、head J、Sector K 表示其位置。

第一個 sector(clyiner 0、head 0、sector 0)稱為 boot sector。BIOS 由第一個 sector 的最後兩個 byte 是不是 0xaa55 來判斷這個 sector 是不是 bootable。

所以說,最簡單的 boot sector 長這樣:

1
2
3
4
e9 fd ff 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
... (many zero)
00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa

最前面的三個 byte 0xe9 0xfd 0xff 是一個 infinite loop 的 machine instruction,會一直 jump 到同一個位置。

最後兩個 byte 是 0x550xaa。這裡之所以不是像前面寫的 0xaa55 是因為 x86 架構是以 little endian 來表示數值,也就是低位在前、高位在後(跟我們平常習慣高位在前相反),所以才會是先 0x550xaa

最經典的例子就是 printf()

1
int printf(char* fmt, ...)

後面的 ... 表示 variable length argument list,必須放在最後面。function 要用 variable length argument list 至少要有一個有名稱的參數,而且可以從這個參數中得知 variable length argument list 總共有多少參數。

在 function 裡處理 variable length argument list 主要要做的是從沒有對應名稱的 list 中取得內容,也就是要拿到參數們。標準函式庫的 stdarg.h 提供一組 macro 來做這件事,Ref

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
#include <stdio.h>
#include <stdarg.h>

void myfunc(char* fmt, ...)
{
va_list ap;
char* p = NULL;
va_start(ap, fmt);
for (p = fmt; *p; p++)
{
if (*p != '%')
{
putchar(*p);
continue;
}

++p; // %
switch (*p)
{
case 'd':
{
int val = va_arg(ap, int);
printf("%d", val);
break;
}
case 'f':
{
double val = va_arg(ap, double);
printf("%f", val);
break;
}
case 's':
{
for (char* val = va_arg(ap, char*); val && *val; ++val)
{
putchar(*val);
}
break;
}
default:
putchar(*p);
break;
}
}
va_end(ap);
}

int main()
{
int n = 123;
double f = 330.732;
char str[] = "KEKEKE";
myfunc("haha, test format: %d, %f, %s\n", n, f, str);
return 0;
}

至於 stdarg.h 裡怎麼定義 macro 的呢?

在 Ubuntu 底下追到 /usr/lib/gcc/x86_64-linux-gnu/5/include/stdarg.h 裡的:

1
2
3
#define va_start(v,l)   __builtin_va_start(v,l)
#define va_end(v) __builtin_va_end(v)
#define va_arg(v,l) __builtin_va_arg(v,l)

之後就不知道去哪了,似乎要到 GCC 的 source code 裡去看跟 builtin 有關的東西才找得到了。Ref

用測試執行時間可以很容易區分出哪些是 unit test 哪些是 integration test,速度快是 unit test。除此之外,也可以依照測試執行時是否 depend on external resource 來區分,例如 db、cache、third party library or API。

區分出 unit test 跟 integration test 後,把它們分開放。讓 unit test 形成一個綠色安全區域──如果這裡的 test failed,就是 code 有問題而非環境或者設定等等造成的 false alarm。

developer 應該要對綠色安全區域有信任感,建立信任感的方式是「fail 就是真的 code 有問題而非 false alarm」。一旦開發人員對 test 失去信任,可能變得不想執行或者 fail 時認為「fail 是正常的」而忽略它。

integration test 除了執行比較慢之外,還可能受到 external dependency 影響而結果不穩定,所以在面對 integration test 的時候,就要考慮有可能是 external dependency 造成 fail。

在自動化流程裡,unit test 應該要每次 check in code 都執行,integration test 則是至少在 daily build 要執行。