2016年2月26日 星期五

porting simple c++ library 的 x86 16bit 版本

博學之、審問之、慎思之、明辨之、篤行之。

先把 cs, ds, es, ss 指到同一個值, 再把 sp 暫存器設定好完成 stack 設定就可以順利完成 x86 16 bit mode 的 c runtime, 這裡都要用上組合語言, 而從這之後, 就可以用 c 語言了, 開心吧, bss 初始化就用 c 來撰寫了, 當然也可以開始用 c++ 了。再把 read/write bios call 加入就擁有了 input/output 的能力, x86 自然是從鍵盤 input, output 到螢幕上。這樣就完成整個移植了, 說起來很簡單, 而過程就很刺激, 遇到不少問題, 紀錄如下:

  1. x86/start.s bios_print_char

    這是用組合語言寫的 function, 讓 c++ call, 用途是在螢幕上印出一個字元。return 時必須要用 retl 而不是 ret, 差個 l 差很多。我本來打算用 inline assembly 寫這個 function 的, 不過寫不出來, 只好用純組合語言寫, inline assembly 可參考《Writing a 16-bit dummy kernel in C/C++》。

    inline assembly bios_call 範例
     1 /* io functions */
     2 /* this function is used to get a chara*/
     3 /* cter from keyboard with no echo     */
     4 void getch() {
     5      __asm__ __volatile__ (
     6           "xorw %ax, %ax\n"
     7           "int $0x16\n"
     8      );
     9 }
    10 
    11 /* this function is same as getch()    */
    12 /* but it returns the scan code and    */
    13 /* ascii value of the key hit on the   */
    14 /* keyboard                            */
    15 short getchar() {
    16      short word;
    17 
    18      __asm__ __volatile__(
    19           "int $0x16" : : "a"(0x1000)
    20      );
    21 
    22      __asm__ __volatile__(
    23           "movw %%ax, %0" : "=r"(word)
    24      );
    25 
    26      return word;
    27 }
    28 
    29 /* this function is used to display the*/
    30 /* key on the screen                   */
    31 void putchar(short ch) {
    32      __asm__ __volatile__(
    33           "int $0x10" : : "a"(0x0e00 | (char)ch)
    34      );
    35 }
    

    retl 會讓 sp + 4
    ret 會讓 sp + 2

    +4 才是我要的值, 這樣 stack 才不會亂掉。

  2. 在這個組合語言寫的 function 要保存 ebx/eax register, 否則以下程式碼

    char cc='A';
    cout << ": " << cc << endl;
    

    會錯誤。

    原因就是沒保存 ebx/eax, 我很辛苦的從組合語言去除錯, 找到問題後, 覺得自己還蠻厲害的。這段程式碼有用到 ebx, 而印出到螢幕的程式碼也用到 ebx, 呼叫印出到螢幕的程式碼後, ebx 就不是原來的值了, 所以造成錯誤。

  3. 縮小 heap 空間, 因為整個執行環境的記憶體只有 64 k (明明有 640k 記憶體可以用, 你有點疑惑吧! 參考: 深入认识 Turbo C 编译器 ), 包含執行檔案的大小, 若執行檔佔了 10k, 我只剩下 56 k 可用, 包含 bss, stack。這已經比 stm32f4discovyer 的 192k ram 還小了。

  4. 另外是 bss type 的問題, 紀錄在 linker script symbol type R_386_16, R_386_32

有兩個版本, 一個是透過 boot loader 載入, 一個是 dos 下的 .com 檔案。

完成後我把 simple scheme 也移植過來, 畢竟已經有了 dos 平台的標準程式庫, 照裡來說應該要很容易, 不過只能使用 64k 的 ram, 實在太小, 我又很辛苦的縮到 64k, 才算移植成功, 參考以下影片:



64k 實在大小, 該怎麼辦? 有一個方法是使用 big real mode, 另外一個是使用不同的節區, 我不打算搞 big real mode 這樣的方法, 來試試看不同節區的方法吧!

把 es, ds, ss 指到另外的地方, 可以再增加 64k, 所以把 start.s (這是 x86 simple c++ library 程式進入點) 改成以下的樣子:

mov $0x8000,%ax
  mov %ax,%ds
  mov %ax,%es
  mov %ax, %ss

這樣就可以了嗎? 當然沒那麼簡單, 這樣是會出事的。還要把 data section 複製到 0x8000 這個 segment, ex:

0x9000:data_section_addr_begin ~ 0x9000:data_section_addr_end -> 0x8000:data_section_addr_begin ~ 0x8000:data_section_addr_end

init_data_asm 就是在做這樣的事情。為什麼我會知道呢? 因為我把反組譯的程式碼看過後, 發現補上這樣的行為, 就可以符合 c 語言的正確行為, 否則在函式參數傳遞會有問題, 可能會在傳遞指標時發生問題。

再來是 ctor 的位址我在 linker script 這邊沒處理好, x86_16.ld.error L37 ~ 39 會抓到錯誤的 call_ctor address, x86_16.ld 的版本才是對的。

因為 align 的問題, ex:
00 01 02 00
本來應該要抓到 01 02, 結果抓到 00 01,
調整 linker script alignment, 就變成
01 02 00 00
就可以正確抓到 01 02。

你一定好奇我怎麼找到這問題, 使用 bochs 內建除錯器, 慢慢和組合語言搏鬥, dump 記憶體位址來觀察, 很辛苦的。

x86_16.ld.error
 1 ENTRY(begin)
 2 
 3 SECTIONS
 4 {
 5   . = 0x100;
 6 
 7   .text :
 8   {
 9     KEEP(*(booting))
10     KEEP(*(.isr_vector))
11     *(.text)
12     *(.text.*)
13     _etext = .;
14     _sidata = .;
15   } 
16   .bss : 
17   {
18     _bss = .;
19     _sbss = .;
20     *(.bss)         /* Zero-filled run time allocate data memory */
21     _ebss = .;
22   } 
23 
24 
25   .= ALIGN(32);
26 
27   /* Initialized data will initially be loaded in FLASH at the end of the .text section. */
28   /* .data : AT(0x5000) */
29   .data : 
30   {
31     _data = .;
32 
33     *(.ccm)
34     *(.rodata)
35     *(.rodata.*)
36     _sdata = .;
37     __start_global_ctor__ = .;
38     *(.init_array)
39     __end_global_ctor__ = .;
40 
41     *(.data)  /* Initialized data */
42     _edata = .;
43   } 
44 
45   .myheap (NOLOAD):
46   {
47     . = ALIGN(8);
48     *(.myheap)
49     . = ALIGN(8);
50   }
51 
80 }  


x86_16.ld
 1 ENTRY(begin)
 2 
 3 SECTIONS
 4 {
 5   . = 0x100;
 6 
 7   .text :
 8   {
 9     KEEP(*(booting))
10     KEEP(*(.isr_vector))
11     *(.text)
12     *(.text.*)
13     _etext = .;
14     _sidata = .;
15   } 
16   .bss : 
17   {
18     _bss = .;
19     _sbss = .;
20     *(.bss)         /* Zero-filled run time allocate data memory */
21     _ebss = .;
22   } 
23 
24 
25   .= ALIGN(32);
26 
27   /* Initialized data will initially be loaded in FLASH at the end of the .text section. */
28   /* .data : AT(0x5000) */
29   .data : 
30   {
31     _data = .;
32 
33     _sdata = .;
34     __start_global_ctor__ = .;
35     *(.init_array)
36     __end_global_ctor__ = .;
37     *(.ccm)
38     *(.rodata)
39     *(.rodata.*)
40 
41     *(.data)  /* Initialized data */
42     _edata = .;
43   } 
44 
45   .myheap (NOLOAD):
46   {
47     . = ALIGN(8);
48     *(.myheap)
49     . = ALIGN(8);
50   }
51 
80 }  

那要怎麼像 turbo c 做到 huge mode 的記憶體模式呢? 這我就不知道了, 我不想再去研究過時的東西了。

git url:
https://github.com/descent/simple_stdcpplib
branch: x86_16_support_diff_segment

考古一下, 在 ms dos real mode 上, sizeof int 是 2byte, 有玩過 dos 程式設計的人應該都知道, 不過一定都是這樣嗎?

以下 fig1, fig2 是在 dos real mode 測試 borland c++ 和 g++ sizeof int 的結果。

fig 1 bc 31 sizeof int: 2


fig 2 g++ sizeof int: 4
g++ 的 int 是 4 bytes, 而 borland c 是 2 bytes, 很不一樣吧?

borland c 是 dos 下開發的霸主, 很好用, gcc 比較少人使用, c 的 type 不只和 os 有關, 就算在同一個 os 下, 不同編譯器也有著不同的結果。

ref:
深入认识Turbo C编译器

沒有留言:

張貼留言

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

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