2012年9月12日 星期三

作業系統之前的程式 (0) - hello XYZ

the 1st edition: 20120506
the 2nd edition: 20150918
the 3rd edition: 20221207
environment: x86 32bit pc

本篇文章介紹在 pc 上寫所謂的「開機程式」, 這是在 legacy BIOS 的環境, UEFI 就不是這樣了。可能有點過時, 不過有些觀念還是可以在應用在其他平台, ex: arm。

uefi 的「開機程式」寫法請參考:
http://descent-incoming.blogspot.tw/search/label/uefi
https://descent-incoming.blogspot.com/search/label/uefi%20os%20loader

作業系統之前的程式有個很威的英文名詞 bare metal program, 什麼意思? 就是字面上的意思, 不另外解釋了。但它不見得是 pc 開機的第一隻程式, BIOS/UEFI 才是 pc 上開機時跑的的一支程式, 你可能覺得有些失落, 但沒辦法, 除非你打算寫 BIOS/UEFI。和你想像中的「開機程式」可能有些不同, 不過就算是 arm 平台的機器, 也有類似的東西, 通常會有一個 rom code 才是所謂的一開機執行的程式, 一般開發者是碰不到這塊的。

相關文章:
  1. 作業系統之前的程式 (1) - c++ 篇
  2. 作業系統之前的程式 (2) - c++ global object ctor/dtor can not be invoked
這是我在 mosut 分享的主題。介紹一下在作業系統之前的程式 (僅僅印出個字串)。寫一個作業系統之前的程式就和在作業系統下很類似, 不過有幾點要注意。
  1. bootloader hello world 被載入的位址是 0x7c00, 也就是從這裡開始執行。
  2. 510, 511 byte 必須是 0xaa, 0x55 結尾。
  3. 不能大於 512 byte, 其實扣掉 0x11, 0x55 2 byte, 只能有 510 byte。
  4. 沒有 printf 可以用, 得使用 bios call 或是寫入 video ram 來完成螢幕的輸出, 或是使用 com1 來輸出訊息。
可以使用組合語言, 還有大家很熟悉的 c/c++ 語言來寫這樣的練習程式, 其他可用的語言我就不知道了, 我只實作過這幾種語言。不過使用 toolchain 的方式和在作業系統下有點不同。也就是說, 程式的寫法是一樣的, 但是使用 compiler 產生執行檔的方式是不一樣的。而使用 C 語言只是展示用, 實際上一個 bootloader 要完成的事情可能無法使用 C 語言來完成, 因為很容易就超過 512 byte。

bh.asm
 1  org 07c00h   ; 告訴編譯器程序加載到 7c00 處
 2  mov ax, cs
 3  mov ds, ax
 4  mov es, ax
 5  call DispStr   ; 使用顯示字元串例程
 6  jmp $   ; 無限循環
 7 DispStr:
 8  mov ax, BootMessage
 9  mov bp, ax   ; ES:BP = 串位址
10  mov cx, 16   ; CX = 串長度
11  mov ax, 01301h  ; AH = 13,  AL = 01h
12  mov bx, 000ch  ; 頁號為 0(BH = 0) 黑底紅字 (BL = 0Ch, 高亮)
13  mov dl, 0
14  int 10h   ; 10h 號中斷
15  ret
16 BootMessage:  db "Hello, NASM!"
17 times  510-($-$$) db 0 ; 填充剩下的空間,使生成的二進制程式碼恰好為 512 字節
18 dw  0xaa55    ; 結束標誌


bh.asm 是用 nasm 的 bootloader 程式 (from Orange's 一個作業系統的實現 p 1-3), bh.asm 的組合語言是 intel 語法, 使用 nasm 產生 bootloader 的可執行檔很簡單。

nasm bh.asm -o bh.asm.bin

bh.asm L1 的註解不算精確, 應該是告訴 linker 這個程式需要計算的位址以 0x07c00 為基準去計算。把它載入 0x07c00 可以正常執行, 載入到其他位址可能會有問題。

bh.asm.bin 為 512 byte 的執行檔, 寫入軟碟第 1 個磁區即可。把軟碟放入開機後即可看到 Hello, NASM!



hb.s
 1 .code16
 2 .text
 3 .global begin
 4 begin:
 5   mov     %cs,%ax
 6   mov     %ax,%ds
 7   mov     %ax,%es
 8   movw    $0xb800, %ax
 9   movw    %ax, %gs
10 
11 
12   mov    $0, %edi    /* Destination */
13   mov    $msg, %esi   /* Source */
14 
15 1:
16   #cmp $0, %ecx
17   cmpb $0, (%esi)
18   jz 2f
19   movb %ds:(%esi), %al
20   inc %esi
21   movb %al, %gs:(%edi)
22   inc %edi
23   movb $0xc, %gs:(%edi)
24   inc %edi
25   dec %ecx
26   jmp 1b
27 2:
28   movb $'E', %gs:(160)
29   jmp .
30 #msg:.ascii "Hello GAS"
31 msg:
32   .asciz "Hello GAS"
33   #.asciz "Hello World"
34 .org 510
35 .word 0xaa55


bh.s 是使用 gas 來完成的 bootloader 程式, 範例的組合語言是 at & t 語法, 使用 gas 產生 bootloader 的可執行檔有點複雜, 除了程式碼本身, 還要一個 linker script, 在連結的時候使用。

hb.s L1 .code16 是告知 gcc 輸出 x86 16 bit 的組合語言, 應該很少人看過這個用法。

as.ld
 1 ENTRY(begin);
 2 SECTIONS
 3 {
 4     . = 0x7C00;
 5     .text : AT(0x7C00)
 6     {
 7         _text = .;
 8         *(.text);
 9         _text_end = .;
10     }
11     .data :
12     {
13         _data = .;
14         *(.bss);
15         *(.bss*);
16         *(.data);
17         *(.rodata*);
18         *(COMMON)
19         _data_end = .;
20     }
21 /*
22     .sig : AT(0x7DFE)
23     {
24         SHORT(0xaa55);
25     }
26 */
27     /DISCARD/ :
28     {
29         *(.note*);
30         *(.iplt*);
31         *(.igot*);
32         *(.rel*);
33         *(.comment);
34 /* add any unwanted sections spewed out by your version of gcc and flags here */
35     }
36 }


產生 bootloader 執行檔的步驟為:

as -o bh.s.o bh.s
ld -Tas.ld -o bh.s.elf bh.s.o
objcopy -O binary bh.s.elf bh.s.bin




bh.s.bin 就是 bootloader 執行檔, 不像 nasm 那麼乾脆是吧! 你一定不喜歡, 不過這樣的學習負擔是有額外的收穫的。下面的 C 語言版本就需要這樣的作法。

cb.c
 1 __asm__(".code16gcc\n");
 2 /*
 3  * c bootloader
 4  */
 5 
 6 void main(const char   *s);
 7 
 8 int bbb=0; // test bss section
 9 
10 void WinMain(void)
11 {
12   main("hello world");
13   while(1);
14 }
15 
16 void main(const char   *s)
17 {
18   while(*s)
19   {
20     __asm__ __volatile__ ("int  $0x10" : : "a"(0x0E00 | *s), "b"(7));
21     s++;
22   }
23 }


很像一般的 C 程式吧! 不過我們有著 windows 才有的 WinMain 和一般 C 的 main, 哪個才是程式開始的地方呢?仍然需要搭配的 linker script, 從以下的 linker script 可以看出, 我們的程式由 WinMain 開始。

l.ld
 1 /* for cb.c */
 2 ENTRY(WinMain);
 3 SECTIONS
 4 {
 5     . = 0x7C00;
 6     .text : AT(0x7C00)
 7     {
 8         _text = .;
 9         *(.text);
10         _text_end = .;
11     }
12     .data :
13     {
14         _data = .;
15         *(.bss);
16         *(.bss*);
17         *(.data);
18         *(.rodata*);
19         *(COMMON)
20         _data_end = .;
21     }
22     .sig : AT(0x7DFE)
23     {
24         SHORT(0xaa55);
25     }
26     /DISCARD/ :
27     {
28         *(.note*);
29         *(.iplt*);
30         *(.igot*);
31         *(.rel*);
32         *(.comment);
33 /* add any unwanted sections spewed out by your version of gcc and flags here */
34     }
35 }


使用以下的步驟來產生 bootloader 執行檔:

gcc -fno-stack-protector -std=c99 -march=i686 -ffreestanding -Wall -c cb.c
ld -Tl.ld -nostdlib -o cb.elf cb.o
objcopy -R .pdr -R .comment -R.note -S -O binary cb.elf cb.bin


最前面提到的第一點: 載入的位址是 0x7c00, 這表示, 在程式裡變數的位址要從 0x7c00 來當作計算標準。nasm 使用 org 07c00h 來達成目的。而 gas 使用 linker script 來完成。C 版本也是一樣。

as.ld L4 . = 0x7C00; 就是告訴 ld 用 0x7c00 來計算位址, 而不是從 0 開始算起。

至於第二點: nasm 使用 bh.asm L17, 18 的方式來產生 0xaa55; gas 使用 bh.s L34, 35 的方法來產生 0xaa55; C 版本則使用 linker script 來產生 0xaa55, l.ld L21 ~ 24

0x7DFE 是 0x7C00 + 510 得到, 落在 512 最後的 2 byte, 從執行位址 0x7C00 開始算。

20120611 補充 C 語言篇:
模擬器畢竟是模擬器, 在真實的機器上測試果然就有問題了, 需要加入 L14 - L17, 將 %ds, %ss, %sp 設定好才行。

cb.c
 1 __asm__(".code16gcc\n");
 2 /*
 3  * c bootloader
 4  */
 5 
 6 //#define POINTER_TEST
 7 
 8 void main(const char   *s);
 9 
10 int bbb=0; // test bss section
11 
12 void WinMain(void)
13 {
14   __asm__ ("mov  %cs, %ax\n");
15   __asm__ ("mov  %ax, %ds\n");
16   __asm__ ("mov  %ax, %ss\n");
17   __asm__ ("mov  $0xff00, %sp\n");
     ...
35 }
36 
37 #ifndef POINTER_TEST
38 void main(const char   *s)
39 {
40   while(*s)
41   {
42     __asm__ __volatile__ ("int  $0x10" : : "a"(0x0E00 | *s), "b"(7));
43     s++;
44   }
45 }
46 #endif


執行畫面像這樣:



20120726 補充:
身為 c++ 愛好者, 怎麼可以沒有 c++ 版本, 要不是因為 c++ runtime 實在太過複雜, 只好先屈就 c runtime, 我實際上很想用 c++ 來寫 os。

先來看看 c++ 版本, 不知道為什麼 g++ 一定會檢查 main() 的 prototype (加上 -ffreestanding 即可, 這樣 g++ 就不會檢查 main() prototype), 所以無法任意宣告 (一定要 int main(int argc, char** argv)), 我只好將 main 改成 print, 我不知道有無 option 可以關掉這種檢查。其他和 c 版本差不多。
12 extern "C" void WinMain(void)

c++ Mangling 的特性會把 function 重新改名, 使用 L:12 來避免這樣的情形, 因為 linker script 指定 WinMain 當進入點, 要不然自己改 linker script。

用了一個 c++ 的特性, function 的參數可以有預設值, 要不然就和 c 版本沒什麼差別了。

 8 void print(const char   *s, const char *msg="\r\ng++ test");

cppb.cpp
 1 __asm__(".code16gcc\n");
 5 
 7 
 8 void print(const char   *s, const char *msg="\r\ng++ test");
 9 
11 
12 extern "C" void WinMain(void)
13 {
14   __asm__ ("mov  %cs, %ax\n");
15   __asm__ ("mov  %ax, %ds\n");
16   __asm__ ("mov  %ax, %ss\n");
17   __asm__ ("mov  $0xfff0, %sp\n");
25   print("hello cpp");
36   while(1);
37 }
38 
40 void print(const char   *s, const char *msg)
41 {
42 #if 1
43   while(*s)
44   {
45     __asm__ __volatile__ ("int  $0x10" : : "a"(0x0E00 | *s), "b"(7));
46     s++;
47   }
48 #endif
49 #if 1
50   while(*msg)
51   {
52     __asm__ __volatile__ ("int  $0x10" : : "a"(0x0E00 | *msg), "b"(7));
53     msg++;
54   }
55 #endif
56 }


使用以下的步驟來產生 bootloader 執行檔 (linker script 同 c 版本那個):

g++ -m32  -g -Wall -Wextra -Werror -nostdlib -fno-builtin -nostartfiles -nodefaultlibs -fno-exceptions -fno-rtti -fno-stack-protector -c cppb.cpp
ld -m elf_i386 -static -Tl.ld -nostdlib -M -o cppb.elf cppb.o > cb.elf.map
objcopy -R .pdr -R .comment -R.note -S -O binary cppb.elf cppb.bin


ref: http://wiki.osdev.org/C%2B%2B_Bare_Bones

真實機器的執行畫面。



以下方式可以取得本文的程式碼:

git clone git@github.com:descent/simple_os.git
cd simple_os
git checkout origin/cpp_bootloader -b cpp_bootloader

以下的簡報是之前在 mosut 的分享, 也就是這篇文章的主題之一:


ref:
使用 GCC 和 GNU Binutils 编写能在 x86 实模式运行的 16 位代码
X86 Booting Sequence - X86 開機流程小

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。