第28天 文件操作與文字顯示

第28天 文件操作與文字顯示

2020.5.8

1. alloca(1)(harib25a)

  • 編寫一個求素數的應用程序sosu.c:

    #include <stdio.h>
    #include "apilib.h"
    
    #define MAX		1000
    
    void HariMain(void)
    {
        char flag[MAX], s[8];
        int i, j;
        for (i = 0; i < MAX; i++) {
            flag[i] = 0;
        }
        for (i = 2; i < MAX; i++) {
            if (flag[i] == 0) {
                /* 沒有標記的爲素數 */
                sprintf(s, "%d ", i);
                api_putstr0(s);
                for (j = i * 2; j < MAX; j += i) {
                    flag[j] = 1;	/* 給它的倍數做上標記 */
                }
            }
        }
        api_end();
    }
    
    • 這個應用程序求1000以內的素數。
  • 在suso目錄下生成一個精簡的OS磁盤映像並用VMware運行:

  • 修改一下MAX的宏定義,改成求10000以內的素數,並另存爲新的應用程序sosu2.c。

    • sosu2.c需要在棧中保存很多變量,光flag[10000]就大概需要10KB的空間,因此在Makefile中指定棧大小改成了11k。
  • make run後,出現了一條警告“Warning: can’t link __alloca”。不管它,運行sosu2.hrb試試:

    • 產生了一般保護性中斷!
  • 警告在提示:缺少一個叫__alloca的函數。

    • 使用的C語言編譯器規定:如果棧中的變量超過4KB,需要調用__alloca這個函數。這個函數的功能是根據OS的規格來獲取棧的空間。在Windows或者Liunx中,如果不調用這個函數,而是僅對ESP進行減法運算的話,貌似無法成功獲得內存空間。小於4KB時,只要對ESP進行減法運算即可。
    • 不過在此OS中,對棧的管理並沒有什麼特殊的設計,因此也用不着去調用__alloca函數,可C語言編譯器並不是此OS專用的,於是又會擅自去調用這個函數了。
  • 爲了解決上述問題,需要編寫一個__alloca函數,只對ESP進行減法運算,而不做其他多餘的操作。

  • 其實,不用__alloca函數也可以運行,因爲可以調用OS的9號API,使用api_malloc函數申請內存空間(笑)。編寫應用程序sosu3.c:

    #include <stdio.h>
    #include "apilib.h"
    
    #define MAX		10000
    
    void HariMain(void)
    {
        char *flag, s[8];
        int i, j;
        api_initmalloc();
        flag = api_malloc(MAX);  /*申請10000字節的內存空間*/
        for (i = 0; i < MAX; i++) {
            flag[i] = 0;
        }
        for (i = 2; i < MAX; i++) {
            if (flag[i] == 0) {
                sprintf(s, "%d ", i);
                api_putstr0(s);
                for (j = i * 2; j < MAX; j += i) {
                    flag[j] = 1;	
                }
            }
        }
        api_end();
    }
    
  • make後用VMware運行:

2. alloca(2)(harib25b)

  • 思前想後,雖然能夠使用api_malloc申請內存空間,但是__alloca函數還是要寫。編寫__alloca函數(apilib目錄下的alloca.nass):

    [FORMAT "WCOFF"]
    [INSTRSET "i486p"]
    [BITS 32]
    [FILE "alloca.nas"]
    
            GLOBAL	__alloca
    
    [SECTION .text]
    
    __alloca:
            ADD		EAX,-4
            SUB		ESP,EAX
            JMP		DWORD [ESP+EAX]		; 代替RET
    
    • alloca.nas的歸類有點難分,所以暫時現將它放在apilib目錄下,雖然它不是個API。
  • 詳細講解__alloca函數:

    • __alloca函數會在下述情況下被C語言的程序調用(採用near-CALL的方式):
      • 要執行的操作:從棧中分配EAX個字節的內存空間(ESP-=EAX)。
      • 要遵守的規則:不能改變ECX、EDX、EBX、EBP、ESI、EDI的值(可以臨時改變,但是要使用PUSH/POP來複原)
    • 根據上述描述,於是編寫出了第一版錯誤的alloca:
      SUB     ESP,EAX
      RET
      
      • 這個程序是無法運行的,因爲RET返回的地址保存在ESP中,而ESP的值在這裏被改變了,於是讀取了錯誤的返回地址。注意:RET相當於POP EIP
    • 接着又編寫了第二版錯誤的alloca:
      SUB     ESP,EAX  
      JMP     DWORD [ESP+EAX]     ;代替RET
      
      • JMP的目標地址從[ESP]變成了[ESP+EAX],ESP+EAX的值剛好是減法運算之前的ESP值,也就是正確的地址。
      • RET指令相當於POP EIP,而POP EIP又相當於下面兩條指令:
        MOV     EIP,[ESP]       ;沒有這個指令,用JMP [ESP]代替。
        ADD     ESP,4
        
        • 也就是說剛剛忘記給ESP+4了。
    • 編寫第三版錯誤的alloca:
      SUB     ESP,EAX  
      JMP     DWORD [ESP+EAX]     ;代替RET  
      ADD     ESP,4
      
      • 第三版錯誤的原因是ADD指令的位置,將ADD指令放在了JMP指令的後面,所以ADD指令不會被執行。
    • 編寫第四版正確的alloca:
      SUB     ESP,EAX  
      ADD     ESP,4
      JMP     DWORD [ESP+EAX-4]     ;代替RET  
      
      • 用這個程序直接作爲alloca.nas是完全沒有問題的。
    • 編寫第五版正確的alloca:
      ADD		EAX,-4
      SUB		ESP,EAX
      JMP		DWORD [ESP+EAX]		; 代替RET
      
      • 和第四版大同小異,不多精簡了一點兒。
  • make後用VMware重新運行sosu2.hrb,這次成功輸出,沒有產生一般保護性中斷。

  • 這樣的話sosu2.hrb和sosu3.hrb在運行結果上沒有任何區別,看一下文件大小:

    • sosu2.hrb:1484字節
    • sosu3.hrb:1524字節
    • 雖然差別不大,但是還是sosu2.hrb小一點。既然小一點,那麼把winhelo也從棧中分配空間吧,不再用malloc了。
    • 修改winhelo.c:
      #include "apilib.h"
      
      void HariMain(void)
      {
          int win;
          char buf[150 * 50];
          win = api_openwin(buf, 150, 50, -1, "hello");
          for (;;) {
              if (api_getkey(1) == 0x0a) {
                  break;
              }
          }
          api_end();
      }
      
      • 在Makefile中設定STACK = 8k,因爲buf大概需要7.5KB的空間。
      • make後可以成功運行應用程序winhelo,要知道修改前有7664KB。之所以對文件大小這樣苛刻,是因爲擔心磁盤空間不夠(後面還要支持漢字字庫),因此應用程序能小就小。
  • 順便把winhelo2.c也改了:

    #include "apilib.h"
    
    void HariMain(void)
    {
        int win;
        char buf[150 * 50];
        win = api_openwin(buf, 150, 50, -1, "hello");
        api_boxfilwin(win,  8, 36, 141, 43, 3);
        api_putstrwin(win, 28, 28, 0, 12, "hello, world");
        for (;;) {
            if (api_getkey(1) == 0x0a) {
                break; 
            }
        }
        api_end();
    }
    
    • 把Makefile中的STACK設置爲8K。
  • 比較一下winhelo[23]?前後的大小:

    winhelo(buf[]) winhelo2(buf[]) winhelo3(malloc方式)
    改良前 7664b 7808b 359b(未改良)
    改良後 174b 315b 359b(未改良)

3. 文件操作API(harib25c)

  • 所謂文件操作API,就是可以指定文件,並能夠自由讀寫文件內容的API。現在的OS還不能對磁盤進行寫入操作,因此只要能夠讀取文件內容就可以了。

  • 一般的OS中,輸入輸出文件的API基本上都有如下的功能:

    • 打開:open
      • 打開和關閉API用來對要讀寫的文件進行打開和關閉的操作。一個文件必須先打開才能進行讀寫操作,因爲在打開時,OS需要對讀寫的文件進行準備工作,關閉時也需要進行一些善後處理。
      • 打開文件時需要指定文件名,如果打開成功,OS返回文件句柄。在隨後的操作中,只要提供這個文件的句柄就可以進行讀寫操作了,操作結束後將文件關閉。
    • 定位:seek
      • 定位API的功能是指定下次讀取、寫入命令需要操作的目標位於文件中的位置。
    • 讀取:read
      • 讀取和寫入API需要指定需要讀取(寫入)的數據長度以及內存地址,文件的內容會被傳送至內存。(寫入操作時是由內存傳至文件)
    • 寫入:write
      • 同讀取
    • 關閉:close
      • 同打開
  • 設計API:

    • 打開文件
      • EDX = 21
      • EBX = 文件名
      • 返回值EAX = 文件句柄(當OS返回0時,代表文件打開失敗)
    • 關閉文件
      • EDX = 22
      • EAX = 文件句柄
    • 文件定位
      • EDX = 23
      • EAX = 文件句柄
      • ECX = 定位模式
        • 0:定位起點爲文件開頭
        • 1:定位起點爲當前訪問位置
        • 2:定位起點爲文件末尾
      • EBX = 定位偏移量
    • 獲取文件大小
      • EDX = 24
      • EAX = 文件句柄
      • ECX = 文件大小獲取模式
        • 0:普通文件大小
        • 1:當前讀取位置從文件開頭算起的偏移量
        • 2:當前讀取位置從文件末尾算起的偏移量
      • 返回值EAX = 文件大小
    • 文件讀取
      • EDX = 25
      • EAX = 文件句柄
      • EBX = 緩衝區地址
      • ECX = 最大讀取字節數
      • 返回值EAX = 本次讀取到的字節數
  • 修改bootpack.h:

    struct TASK {
        int sel, flags; 
        int level, priority;
        struct FIFO32 fifo;
        struct TSS32 tss;
        struct SEGMENT_DESCRIPTOR ldt[2];
        struct CONSOLE *cons;
        int ds_base, cons_stack;
        struct FILEHANDLE *fhandle;
        int *fat;
    };  
    
    struct FILEHANDLE {
        char *buf;
        int size;
        int pos;
    };
    
    • 結構體TASK增加了成員fhandle和fat,是爲了讓hrb_api和cmd_app能夠使用在console_task中聲明的變量。
      • fhandle是一個指向FILEHANDLE的指針,用於存放應用程序打開文件的信息(應用程序可能打開不止一個文件)。
      • fat是指向文件配置表的指針。
    • 結構體FILEHANDLE,文件句柄:
      • buf:指向文件在內存空間中的地址
      • size:文件大小
      • pos:當前讀取的文件位置
  • 修改console.c,添加21~25號API:

    void console_task(struct SHEET *sheet, int memtotal)
    {
        ……
        struct FILEHANDLE fhandle[8]; /*一個命令行任務至多打開8個文件*/
        ……
        for (i = 0; i < 8; i++) {
            fhandle[i].buf = 0;	/* 該文件句柄未使用標誌 */
        }
        task->fhandle = fhandle;
        task->fat = fat;
        ……
    }
    
    int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
    {
        ……
        if (finfo != 0) {
            /* 找到文件的情況 */
            ……
            if (finfo->size >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {
                ……
                for (i = 0; i < 8; i++) {	/* 將未關閉的文件關閉 */
                    if (task->fhandle[i].buf != 0) {
                        memman_free_4k(memman, (int) task->fhandle[i].buf, task->fhandle[i].size);
                        task->fhandle[i].buf = 0;
                    }
                }
                ……
            } else {
                cons_putstr0(cons, ".hrb file format error.\n");
            }
            ……
        }
        return 0;
    }
    
    int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
    {
        ……
        struct FILEINFO *finfo;
        struct FILEHANDLE *fh;
        struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
    
        ……
        } else if (edx == 21) { /*打開文件API*/
            for (i = 0; i < 8; i++) {
                if (task->fhandle[i].buf == 0) { /*找到一個還未使用的句柄*/
                    break;
                }
            }
            fh = &task->fhandle[i]; /*文件句柄*/
            reg[7] = 0; /*暫時設置未找到,如果i=8,則真未找到*/
            if (i < 8) {
                finfo = file_search((char *) ebx + ds_base,
                        (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224); /*根據文件名搜尋文件*/
                if (finfo != 0) { /*找到該文件*/
                    reg[7] = (int) fh; /*返回句柄*/
                    fh->buf = (char *) memman_alloc_4k(memman, finfo->size); /*該文件的內存始址*/
                    fh->size = finfo->size; /*該文件大小*/
                    fh->pos = 0; /*定位位置*/
                    file_loadfile(finfo->clustno, finfo->size, fh->buf, task->fat, (char *) (ADR_DISKIMG + 0x003e00)); /*將文件寫入內存*/
                }
            }
        } else if (edx == 22) { /*關閉文件API*/
            fh = (struct FILEHANDLE *) eax; /*獲得文件句柄*/
            memman_free_4k(memman, (int) fh->buf, fh->size); /*釋放該內存空間*/
            fh->buf = 0; /*句柄置零*/
        } else if (edx == 23) { /*文件定位API*/
            fh = (struct FILEHANDLE *) eax; /*獲得文件句柄*/
            if (ecx == 0) { /*定位起點是文件開頭*/
                fh->pos = ebx; /*pos=0+偏移量*/
            } else if (ecx == 1) { /*定位起點是當前訪問位置*/
                fh->pos += ebx; /*pos代表的就是當前訪問位置,因此pos+=ebx*/
            } else if (ecx == 2) { /*定位起點是文件末尾*/
                fh->pos = fh->size + ebx; /*當前訪問位置是size+ebx*/
            }
            if (fh->pos < 0) { /*修正pos*/
                fh->pos = 0;
            }
            if (fh->pos > fh->size) { /*修正pos*/
                fh->pos = fh->size;
            }
        } else if (edx == 24) { /*獲取文件大小API*/
            fh = (struct FILEHANDLE *) eax; /*獲取文件句柄*/
            if (ecx == 0) { /*普通文件大小*/
                reg[7] = fh->size;
            } else if (ecx == 1) { /*當前讀取位置從文件開頭算起的偏移量*/
                reg[7] = fh->pos;
            } else if (ecx == 2) { /*當前讀取位置從文件末尾算起的偏移量*/
                reg[7] = fh->pos - fh->size;
            }
        } else if (edx == 25) { /*文件讀取API*/
            fh = (struct FILEHANDLE *) eax; /*獲取文件句柄*/
            for (i = 0; i < ecx; i++) { /*根據最大讀取字節數讀取*/
                if (fh->pos == fh->size) {
                    break;
                }
                *((char *) ebx + ds_base + i) = fh->buf[fh->pos]; /*ebx存放的是緩衝區地址*/
                fh->pos++; /*當前讀取位置+1*/
            }
            reg[7] = i; /*返回讀取的字節數*/
        }
        return 0;
    }
    
  • 添加apilib的函數,以便C語言可以使用新的API:

    • api021.nas:
      [FORMAT "WCOFF"]
      [INSTRSET "i486p"]
      [BITS 32]
      [FILE "api021.nas"]
      
              GLOBAL	_api_fopen
      
      [SECTION .text]
      
      _api_fopen:			; int api_fopen(char *fname);
              PUSH	EBX
              MOV		EDX,21
              MOV		EBX,[ESP+8]			; fname
              INT		0x40
              POP		EBX
              RET
      
    • api022.nas:
      ……
      _api_fclose:		; void api_fclose(int fhandle);
              MOV		EDX,22
              MOV		EAX,[ESP+4]			; fhandle
              INT		0x40
              RET
      
    • api023.nas:
      ……
      _api_fseek:			; void api_fseek(int fhandle, int offset, int mode);
              PUSH	EBX
              MOV		EDX,23
              MOV		EAX,[ESP+8]			; fhandle
              MOV		ECX,[ESP+16]		; mode
              MOV		EBX,[ESP+12]		; offset
              INT		0x40
              POP		EBX
              RET
      
    • api024.nas:
      ……
      _api_fsize:			; int api_fsize(int fhandle, int mode);
              MOV		EDX,24
              MOV		EAX,[ESP+4]			; fhandle
              MOV		ECX,[ESP+8]			; mode
              INT		0x40
              RET
      
    • api025.nas:
      ……
      _api_fread:			; int api_fread(char *buf, int maxsize, int fhandle);
              PUSH	EBX
              MOV		EDX,25
              MOV		EAX,[ESP+16]		; fhandle
              MOV		ECX,[ESP+12]		; maxsize
              MOV		EBX,[ESP+8]			; buf
              INT		0x40
              POP		EBX
              RET
      
    • api022~api025省略的部分和api021的前半部分大同小異。
    • 注意,當用到EBX的時候,需要PUSH和POP。(EAX,ECX,EDX不需要PUSH,這是彙編的既定規定)
  • 編寫用於測試的應用程序typeipl.c,用於將ipl10.nas的內容type出來。

    #include "apilib.h"
    
    void HariMain(void)
    {
        int fh;
        char c;
        fh = api_fopen("ipl10.nas");
        if (fh != 0) {
            for (;;) {
                if (api_fread(&c, 1, fh) == 0) { /*直到讀到的字節數是0*/
                    break;
                }
                api_putchar(c); /*逐字節地輸出*/
            }
        }
        api_end();
    }
    
    • ipl10.nas已經在Makefile中寫好了寫入到磁盤映像文件中。
  • make後用VMware運行:

    • 運行成功。typeipl.nas是程序,可以使用Shift+F1強制結束,這一點比type指令好一點。不過,CPU速度太快,還沒來得及按Shift+F1便已經完成了顯示,真是無奈啊(笑)

4. 命令行API(harib25d)

  • typeipl.hrb看起來很不錯(主要是可以強制結束這一點)。用這個應用程序來代替type命令

    • 首先,從命令行窗口刪除type這個命令。然後在console.c中刪除cmd_type。接下來,將函數cons_runcmd中用於調用cmd_type的部分也刪除掉。
    • 這些刪除代碼此處不再贅述。
    • 現在的typeipl.hrb還只能顯示ipl10.nas這個文件,需要實現能夠任意指定文件名的功能。這樣就能完全代替type命令了。
    • 當用戶輸入type ipl10.nas時,應用程序type能夠獲取文件名ipl10.nas。這個功能稱爲獲取命令行。因此,需要編寫一個API來獲取命令行。
    • 不同OS下獲取命令行的形式也不盡相同。Windows的API在獲取命令行時會獲取整個命令行的輸入,而非只是文件名。因此,我們也照貓畫虎吧(笑)。
  • 設計獲取命令行的API:

    • EDX = 26
    • EBX = 存放命令行內容的地址
    • ECX = 最多可以存放多少字節
    • 返回值EAX = 實際存放了多少字節
  • 修改bootpack.h中的TASK結構體:

    struct TASK {
        ……
        char *cmdline;
    };
    
    • 添加成員變量cmdline,只是爲了把console_task中的cmdline傳遞到hrb_api。
  • 修改console.c中的console_task和hrb_api:

    void console_task(struct SHEET *sheet, int memtotal)
    {
        ……
        task->cmdline = cmdline;
        ……
    }
    
    int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
    {
        ……
        } else if (edx == 26) {
            i = 0;
            for (;;) {
                *((char *) ebx + ds_base + i) =  task->cmdline[i]; /*將命令行的內容寫入指定內存地址*/
                if (task->cmdline[i] == 0) { 
                    break;
                }
                if (i >= ecx) {
                    break;
                }
                i++;
            }
            reg[7] = i; /*返回實際存放的字節數*/
        }
        return 0;
    }
    
  • 添加apilib的函數api_cmdline(api026.nas)

    _api_cmdline:		; int api_cmdline(char *buf, int maxsize);
            PUSH	EBX
            MOV		EDX,26
            MOV		ECX,[ESP+12]		; maxsize
            MOV		EBX,[ESP+8]			; buf
            INT		0x40
            POP		EBX
            RET
    
  • 編寫應用程序type.c:

    #include "apilib.h"
    
    void HariMain(void)
    {
        int fh;
        char c, cmdline[30], *p;
    
        api_cmdline(cmdline, 30);
        for (p = cmdline; *p > ' '; p++) { }	/* 跳過之前的內容,直到遇到空格 */
        for (; *p == ' '; p++) { }	/* 跳過空格 */
        fh = api_fopen(p);
        if (fh != 0) {
            for (;;) {
                if (api_fread(&c, 1, fh) == 0) {
                    break;
                }
                api_putchar(c);
            }
        } else {
            api_putstr0("File not found.\n");
        }
        api_end();
    }
    
    • 在之前的命令行窗口中,指定了p=cmdline+5,這是爲了跳過type直接取出用戶指定的文件名。現在通過應用程序實現type,用戶可能通過type.hrb 文件名的形式來運行,而且,應用程序type.hrb還有可能被改成cat.hrb。
    • 因此,爲了能夠在任何情況下都能順利運行,需要逐字節讀取cmdline的內容,遇到比空格字符編碼大的字符連續出現時,將它們全部跳過,這樣,無論是type、type.hrb還是cat都可以跳過去了。跳過應用程序名稱以後,還要跳過若干空格。
  • make後用VMware運行:

    • 成功運行。
  • 不過,這裏還有一個不算bug的小bug。

    • 在console_task中,在命令行中輸入一串字符以後會被存入cmdline。當輸入回車以後,會在cmdline後面先存入一個空格,然後再存入結束符0。
    • 在cons_runcmd中運行應用程序,去執行cmd_app函數時,cmdline會被傳遞到cmd_app中。
    • 然後根據cmdline中空格的位置爲止判斷應用程序的名稱。空格之後的字符會被忽略。cmdline自始至終沒有變化,在cmd_app中是改變的name數組。
      • 也就是說,在命令行中輸入type ipl10.nas後,cmd_app發現type是一個應用程序,就會去執行應用程序type,而將ipl10.nas忽略。type應用程序再對ipl10.nas進行處理。
      • 那麼,這就出現小bug了,比如,輸入stars abc。cmd_app判斷到第一個空格就停止了。回車前的空格便不會產生作用。因此abc是沒有任何作用的。stars應用程序不會對abc產生作用。
    • 其實這也不算什麼bug,有時應用程序的確會加入參數,這個abc就是參數,不過現在還沒實現,這個小bug保證了以後應用程序的可拓展性

5. 日文文字顯示(1)(harib25e)

  • 本着求同存異的原則,日文中也有大量漢字,中文顯示比日文顯示還簡單一點兒。因此,參考日文文字顯示的方式,來實現中文漢字顯示

  • 日文顯示,其實只要準備好相應的字庫就好了。

    • 如果即將字庫內置到OS的核心中,OS會變得很大。因此,將日文字庫單獨生成一個名叫nihongo.fnt的文件,在OS啓動時先檢查是否存在該文件,如果存在則自動將其讀入到內存中。
  • 字庫文件的大小。

    • 日文字符基本上都是用全角來顯示的,相對於8*16點陣的半角字符來講,1個全角字符的大小是16*16點陣。如果1個半角字符的字庫數據需要16字節的話,那麼1個全角字符就需要32字節。
    • 日文漢字編碼表按照使用頻率劃分,常用的漢字是第一水準,偶爾使用的是第二水準,基本上不會用到的是第三水準,用得更少的是第四水準(如果你的姓名或地址中用到了第四水準的漢字,那麼對你來講,第四水準的漢字更加常用)。
    • 在JIS指定的漢字編碼表中,非漢字加上第一水準~第三水準的漢字一共有94*94=8836個字符(再加上第四水準的漢字的話就更多了)。如果用上這所有的8836個字符的話,就需要32*8836=282752字節=276KB。
    • 在漢字編碼標準GB2312中,也按照漢字的常用度劃分了一級漢字和二級漢字,其中一級漢字3755個,二級漢字3008個,再加上非漢字(拉丁字母、希臘字母等)字符682個,一共7445個字符。基本上與上述JIS非漢字加上第一水準~第三水準的漢字的容量相當。

    • 276KB實在是太大了,佔到了軟盤容量的20%(276/1440)。因此需要給字庫文件瘦身。
  • 根據JIS的規格,全角字符的編碼以“點、區、面”爲單位來進行定義:

    • 1個點就對應一個全角字符
    • 1個區中包含94個點
    • 1個面中包含94個區
    • 第一水準~第三水準全部位於1面,第四水準全部位於2面。
  • 字符編碼大致分爲如下幾類:

    • 01區~13區:非漢字
    • 14區~15區:第三水準漢字
    • 16區~47區:第一水準漢字
    • 48區~84區:第二水準漢字
    • 84區~94區:第三水準漢字
    • 爲了節省容量,準備只將01區~47區的裝入nihongo.fnt,這樣就只需要47*94*32=141376字節。
  • 在GB2312中,字符編碼分類如下:

    • 01區~09區:非漢字
    • 10區~15區:空白
    • 16區~55區:一級漢字
    • 56區~87區:二級漢字
    • 88區~94區:空白
    • 爲了節省容量,可以只使用非漢字和一級漢字的部分,即01區~55區,一共55*94*32=165440字節。
  • 字庫的字模數據:

    • 不可能自己設計字模數據,因此採用SASK中的字庫文件jpn16v00.fnt(56.7KB),實際大小是304KB。304KB有點大,所以OSASK把這個文件進行了壓縮,大小變成了56.7KB。因此,需要先對字庫文件進行解壓縮注:jpn16v00.fnt分爲兩個版本,一個只包含第一水準漢字,另一個包含第一到第三水準的全部漢字,56.7KB的是隻包含第一水準漢字的版本,48~94區的內容是空白的。
    • 工具edimg內置瞭解壓縮的功能。首先將工具edimg.exe複製到目錄nihongo,然後在當前目錄下打開命令行輸入指令edimg copy nocmp: from:jpn16v00.fnt to:jpn16v00.bin,然後就得到了jpn16v00.bin文件(304KB)。
    • 將01區47區的字模數據提取出來(只要將jpn16v00.bin從開頭算起的前141376字節提取出來即可,用BZ編輯器可以提取)。日文顯示還需要半角片假名的字庫。在jpn16v00.bin中已經包含了顯示日文用的半角片假名字模,共256個字符,位於04A00004AFFF,共4096字節。
    • 最終nihongo.fnt包含的內容如下:
      • 000000~000FFF:顯示日文用的半角字模,共256個字符,4096字節
      • 001000~02383F:顯示日文用的全角字模,共4418個字符,141376字節
    • 關於中文的字模數據:

  • 修改bootpack.c,使OS可以自動裝載字庫:

    void HariMain(void)
    {
        ……
        int *fat;
        unsigned char *nihongo;
        struct FILEINFO *finfo;
        extern char hankaku[4096];
        ……
        /* 載入nihongo.fnt */
        nihongo = (unsigned char *) memman_alloc_4k(memman, 16 * 256 + 32 * 94 * 47);
        fat = (int *) memman_alloc_4k(memman, 4 * 2880);
        file_readfat(fat, (unsigned char *) (ADR_DISKIMG + 0x000200));
        finfo = file_search("nihongo.fnt", (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
        if (finfo != 0) { /*找到nihongo.fnt*/
            file_loadfile(finfo->clustno, finfo->size, nihongo, fat, (char *) (ADR_DISKIMG + 0x003e00));
        } else {
            for (i = 0; i < 16 * 256; i++) {
                nihongo[i] = hankaku[i]; /* 沒有字庫,半角部分直接複製英文字庫 */
            }
            for (i = 16 * 256; i < 16 * 256 + 32 * 94 * 47; i++) {
                nihongo[i] = 0xff; /* 沒有字庫,全角部分以0xff填充 */
            }
        }
        *((int *) 0x0fe8) = (int) nihongo; /*這裏!*/
        memman_free_4k(memman, (int) fat, 4 * 2880);
        ……
    }
    
    • 上述代碼實現的功能:首先分配出用於存放nihongo.fnt內容的內存空間,然後尋找文件,如果找到的話載入內存。如果沒有找到字庫文件,則只好用內置的半角字庫代替日文半角字符,並用方塊填充全角字庫的部分。最後,將用於存放nihongo.fnt內容的內存地址寫入0x0fe8作爲記錄
      • 0xfec:task_a的緩衝區地址
      • 0xfe8:nihongo.fnt的內存地址
      • 0xfe4:shtctl
  • 修改bootpack.h中TASK結構體:

    struct TASK{
        ……
        char langmode;
    }
    
    • 新添加的成員變量langmode(language mode,語言模式),用於指定一個任務使用的是內置的英文字庫還是使用nihongo.fnt的日文字庫。有了這個langmode,就可以對每一個任務單獨設置語言模式,例如爲某個應用程序設置日文模式,爲另一個應用程序設置英文模式。
      • langmode=0,英文模式;langmode=1:日文模式。
  • 修改graphic.c中用於顯示字符的putfonts8_asc函數:

    void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
    {
        extern char hankaku[4096];
        struct TASK *task = task_now();
        char *nihongo = (char *) *((int *) 0x0fe8); /*獲取字模數據的內存地址*/
    
        if (task->langmode == 0) { /*英文模式*/
            for (; *s != 0x00; s++) {
                putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
                x += 8;
            }
        }
        if (task->langmode == 1) { /*日文模式*/
            for (; *s != 0x00; s++) {
                putfont8(vram, xsize, x, y, c, nihongo + *s * 16);
                x += 8;
            }
        }
        return;
    }
    
    • 注意,此時日文模式還是隻能顯示半角。
  • 添加一個命令langmode對成員變量langmode設置,修改console.c:

    void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, int memtotal)
    {
        ……
        } else if (strncmp(cmdline, "langmode ", 9) == 0) {
            cmd_langmode(cons, cmdline);
        } else if (cmdline[0] != 0) {
        ……
    }  
    
    void cmd_langmode(struct CONSOLE *cons, char *cmdline)
    {
        struct TASK *task = task_now();
        unsigned char mode = cmdline[9] - '0'; /*獲取模式*/
        if (mode <= 1) {
            task->langmode = mode;
        } else {
            cons_putstr0(cons, "mode number error.\n");
        }
        cons_newline(cons);
        return;
    }
    
    • 只要在命令行中輸入langmode 0就可以設置該命令行爲英文模式,輸入langmode 1就可以設置該命令行爲日文模式。
  • 啓動命令行時,langmode沒有默認值,修改console_task:

    void console_task(struct SHEET *sheet, int memtotal)
    {
        ……
        unsigned char *nihongo = (char *) *((int *) 0x0fe8);
        ……
        if (nihongo[4096] != 0xff) {	/* 是否載入了日文字庫 */
            task->langmode = 1; /*日文模式*/
        } else {
            task->langmode = 0; /*英文模式*/
        }
        ……
    }
    
    • 如果載入了日文字庫,nihongo[]的4096號元素一定不是0xff,而是全角空格0x00。
  • 修改bootpack.c設置task_a的langmode默認值:

    void HariMain(void){
        ……
        task_a->langmode = 0; /*英文模式*/
        ……
    }
    
  • 由於現在還無法顯示全角字符,只能顯示半角字符。但是還是能夠測試是否成功載入nihongo.fnt。因此,編寫用於測試的應用程序iroha.c:

    #include "apilib.h"
    
    void HariMain(void)
    {
        static char s[9] = { 0xb2, 0xdb, 0xca, 0xc6, 0xce, 0xcd, 0xc4, 0x0a, 0x00 };
        api_putstr0(s);
        api_end();
    }
    
    • s[]是一串半角片假名的字符編碼+換行+0。由於是日語,因此,不深究究竟是什麼
    • 這裏之所以採用字符編碼的形式,是爲了兼容字符在Windows和Linux中字符編碼方式不同而導致實際生成字符編碼數據不同的問題。具體的細節這裏不再深究。
      • Windows常用的日文編碼規範是Shift-JIS;Linux常用的日文編碼規範是EUC。
  • make後用VMware運行:

    • 如果是langmode 0,命令行顯示是一串亂碼。
  • 關於中文字符

6. 日文文字顯示(2)(harib25f)

  • 實現全角字符顯示。在全角字符顯示方面,Shift-JIS和EUC的處理方式是不同的,先從Shift-JIS開始。

  • 各種半角字符,包括字母、數字、符號等,加起來總的字符數也不是很多,用1個字節完全可以容納。不過漢字就不可以了,漢字需要2個字節來表示(在某些編碼方式中,某些漢字甚至需要3個字節來表示)。

  • 下面分析一下如何將0x82和0xa0這兩個字節的編碼方式轉化爲區點的編號。

    • 首先,兩張表格:

    • 參照第一個字節的表格,0x82代表“全角字符(1面03區~04區)”;參照第二個字節的表格,0xa0代表“全角字符(較大區的02點)”,因此0x82和0xa0所表示的日文漢字隊用的編號是:04區02點。
  • 修改graphic.c中的putfonts8_asc:

    void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
    {
        extern char hankaku[4096];
        struct TASK *task = task_now();
        char *nihongo = (char *) *((int *) 0x0fe8), *font;
        int k, t;
    
        if (task->langmode == 0) {
            for (; *s != 0x00; s++) {
                putfont8(vram, xsize, x, y, c, hankaku + *s * 16);
                x += 8;
            }
        }
        if (task->langmode == 1) {
            for (; *s != 0x00; s++) {
                if (task->langbyte1 == 0) {
                    if ((0x81 <= *s && *s <= 0x9f) || (0xe0 <= *s && *s <= 0xfc)) {
                        task->langbyte1 = *s;
                    } else {
                        putfont8(vram, xsize, x, y, c, nihongo + *s * 16);
                    }
                } else {
                    if (0x81 <= task->langbyte1 && task->langbyte1 <= 0x9f) {
                        k = (task->langbyte1 - 0x81) * 2;
                    } else {
                        k = (task->langbyte1 - 0xe0) * 2 + 62;
                    }
                    if (0x40 <= *s && *s <= 0x7e) {
                        t = *s - 0x40;
                    } else if (0x80 <= *s && *s <= 0x9e) {
                        t = *s - 0x80 + 63;
                    } else {
                        t = *s - 0x9f;
                        k++;
                    }
                    task->langbyte1 = 0;
                    font = nihongo + 256 * 16 + (k * 94 + t) * 32;
                    putfont8(vram, xsize, x - 8, y, c, font     );	/* 左半部分 */
                    putfont8(vram, xsize, x    , y, c, font + 16);	/* 右半部分 */
                }
                x += 8;
            }
        }
        return;
    }
    
    • 上述代碼是根據兩個表格所編寫的。
    • 修改了TASK結構體:
      struct TASK{
          ……
          unsigned char langmode, langbyte1;
      }
      
      • 成員變量langbyte1是當接收到全角字符時用來存放第1個字節內容的變量。當接收到半角字符,或者全角字符顯示完畢後,該變量被置爲0。
    • 變量k用來存放區號,變量t用來存放點號,爲了計算方法便,存放的是減1之後的值。沒有考慮第四水準的漢字,此代碼拓展性不高
    • putfonts8_asc中每接收1個字節就會執行x+=8;,當現實全角字符時,需要在接收到第2個字節之後,再往左移8個像素並繪製字模的左半部分。
  • 需要設置一開始langbyte1默認值是0,修改console.c:

    void console_task(struct SHEET *sheet, int memtotal)
    {  
        task->langbyte1 = 0;
    }  
    
    int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
    {
        ……
        if (finfo != 0) {
            ……
            if (finfo->size >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {
                ……
                task->langbyte1 = 0;
            } else {
                cons_putstr0(cons, ".hrb file format error.\n");
            }
            ……
        }
        return 0;
    }
    
    • 對console_task的修改只是設置langbyte1的默認值是0。
    • 對cmd_app的修改,主要是:當應用程序出現bug或者被強制結束時可能出現在顯示全角字符第1個字節時停止的情況。
  • 對於換行還有一點問題,當字符串很長時,可能在全角字符的第1個字節處就遇到自動換行了,這樣一來當接收到第2個字節時,字模的左半部分就會畫到命令行窗口外面去。所以在遇到第1個字節換行時,可以特意將cur_x再右移8個像素。修改cons_newline:

    void cons_newline(struct CONSOLE *cons)
    {
        int x, y;
        struct SHEET *sheet = cons->sht;
        struct TASK *task = task_now();
        if (cons->cur_y < 28 + 112) {
            cons->cur_y += 16; /* 到下一行 */
        } else {
            /* 屏幕滾動 */
            ……
        }
        cons->cur_x = 8;
        if (task->langmode == 1 && task->langbyte1 != 0) { /*日文模式,且是處於langbyte1=1時換行*/
            cons->cur_x = 16; /多加8個像素**/
        }
        return;
    }
    
  • make後用VMware運行,輸入type ipl10.nas

    • 書上說一個日文漢字沒有顯示清楚(具體是哪個我也看不懂),然後書上解釋說是10個柱面太小,nihongo.fnt太大,ipl10.nas無法全部載入。

7. 日文文字顯示(3)(harib25g)

  • 寫一個ipl20.nas來代替ipl10.nas。只需要將ipl10.nas前面的一行代碼修改以後重命名即可(注意還要修改Makefile哦)。

    CYLS	EQU		20	
    
  • make後用VMware運行:

    • 應該是紅框中的日文漢字沒有顯示出來吧~
  • 接下來實現Linux的日文EUC的支持。

    • EUC中半角片假名佔用2個字節,會出現字節數與字符寬度不匹配的現象,因此,修改太麻煩了,這次算了。
    • 日文EUC中k和t的計算公式很簡單:
      • k = langbyte1 - 0xa1
      • t = *s - 0xa1
    • 第一個字節和第二個字節的範圍都在0xa1~0xfe。
  • 修改graphic.c:

    void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
    {
        ……
        if (task->langmode == 0) {
            ……
        }
        if (task->langmode == 1) {
            ……
        }
        if (task->langmode == 2) {
            for (; *s != 0x00; s++) {
                if (task->langbyte1 == 0) {
                    if (0x81 <= *s && *s <= 0xfe) {
                        task->langbyte1 = *s;
                    } else {
                        putfont8(vram, xsize, x, y, c, nihongo + *s * 16);
                    }
                } else {
                    k = task->langbyte1 - 0xa1;
                    t = *s - 0xa1;
                    task->langbyte1 = 0;
                    font = nihongo + 256 * 16 + (k * 94 + t) * 32;
                    putfont8(vram, xsize, x - 8, y, c, font     );	
                    putfont8(vram, xsize, x    , y, c, font + 16);
                }
                x += 8;
            }
        }
        return;
    }
    
    • putfonts8_asc這個函數名已經不太適用了,因爲不僅支持ASCII、還支持Shift-JIS和日文EUC。
    • langmode也改變了:
      • 0:ASCII英文模式
      • 1:Shift-JIS日文模式
      • 2:EUC日文模式
  • 因此,還需要修改console.c:

    void cmd_langmode(struct CONSOLE *cons, char *cmdline)
    {
        struct TASK *task = task_now();
        unsigned char mode = cmdline[9] - '0';
        if (mode <= 2) { /*1修改成2*/
            task->langmode = mode;
        } else {
            cons_putstr0(cons, "mode number error.\n");
        }
        cons_newline(cons);
        return;
    }
    
  • 在make之前需要製作一個EUC編碼的文本文件。

    • 具體的方式沿用書上說將,因爲我對日文一竅不通(笑)
  • 修改Makefile,然後make並用VMware運行:

    • 注意,默認情況下的語言模式時Shift-JIS,因此現需要輸入langmode 2將語言模式轉化成EUC。
  • 對於中文:

    • 前面已經提到過,中文GB2312編碼採用的是EUC方式,因此中文字符二進制編碼轉化爲區位碼的公式和日文EUC是完全相同的,再加上中文不需要設計類似半角片假名的問題,因此到這裏實現中文顯示的原理基本就結束了。
  • 編寫一個查詢當前langmode的API,設計API:

    • EDX = 27
    • 返回值EAX = langmode
  • 修改hrb_api:

    int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
    {
        ……
        } else if (edx == 27) {
            reg[7] = task->langmode;
        }
        return 0;
    }
    
  • 編寫api027.nas中的api_getlang函數:

    _api_getlang:		; int api_getlang(void);
            MOV		EDX,27
            INT		0x40
            RET
    
  • 編寫用於測試27號API的應用程序chklang.c:

    #include "apilib.h"
    
    void HariMain(void)
    {
        int langmode = api_getlang();
        static char s1[23] = {	/* 輸出的是日語,Shift-JIS模式,看不懂 */
            0x93, 0xfa, 0x96, 0x7b, 0x8c, 0xea, 0x83, 0x56, 0x83, 0x74, 0x83, 0x67,
            0x4a, 0x49, 0x53, 0x83, 0x82, 0x81, 0x5b, 0x83, 0x68, 0x0a, 0x00
        };
        static char s2[17] = {	/* 輸出的是日語,EUC模式,看不懂 */
            0xc6, 0xfc, 0xcb, 0xdc, 0xb8, 0xec, 0x45, 0x55, 0x43, 0xa5, 0xe2, 0xa1,
            0xbc, 0xa5, 0xc9, 0x0a, 0x00
        };
        if (langmode == 0) {
            api_putstr0("English ASCII mode\n");
        }
        if (langmode == 1) {
            api_putstr0(s1);
        }
        if (langmode == 2) {
            api_putstr0(s2);
        }
        api_end();
    }
    
    • s1和s2沒有寫成字符串時爲了在make是避免受到源代碼字符編碼方式的影響。
  • make後用VMware運行:

    • 顯示有問題?
    • 貌似移動一下窗口就好了。
    • 這個bug明天再解決吧

8. 中文文字顯示(harib25h)

  • 接下來的過程是經過不斷試錯,不斷改正後,個人探索出來比較正確的流程。

  • 首先,製作字庫文件hzk16.fnt。

    • 從網上下載字庫文件HZK16.fnt。
    • 然後取其前165440字節(0x28640),製作成字庫文件HZK16_0x28640.fnt。
    • 新建一個文件hzk16.fnt。
      • 將harib25g使用make full後在haribote目錄下獲得文件hankaku.bin文件(4096字節)。
      • 然後將這4096字節用BZ賦值到hzk16.fnt中。
      • 然後將HZK16_0x28640.fnt的全部複製到hzk16.fnt中。
      • 這樣,hzk16.fnt中:
        • 000000~000FFF:hankaku.bin
        • 001000~02963F:HZK16_0x28640.fnt
  • 然後在harib25g的基礎上修改成harib25h:首先,將harib25h的Makefile和app_make.txt中的copy from:../nihongo/nihongo.fnt to:@: \改成copy from:../nihongo/hzk16.fnt to:@: \

  • 修改bootpack.c,將nihongo變量全部替換成爲hkz16.

    void HariMain(void){
        int *fat;
        unsigned char *hzk16;
        struct FILEINFO *finfo;
        extern char hankaku[4096];
        ……
        /*載入hzk16.fnt*/  
        hzk16 = (unsigned char*)memman_alloc_4k(memman, 16 * 256 + 0x28640);
        fat = (int*)memman_alloc_4k(memman, 4 * 2880);
        file_readfat(fat, (unsigned char*)(ADR_DISKIMG + 0x000200));
        finfo = file_search("hzk16.fnt", (struct FILEINFO*) (ADR_DISKIMG + 0x002600), 224);
        if (finfo != 0) {
            file_loadfile(finfo->clustno, finfo->size, hzk16, fat, (char*)(ADR_DISKIMG + 0x003e00));
        }
        else {
            for (i = 0; i < 16 * 256; i++) {
                hzk16[i] = hankaku[i]; 
            }
            for (i = 16 * 256; i < 16 * 256 + 0x28640; i++) {
                hzk16[i] = 0xff; 
            }
        }
        *((int*)0x0fe0) = (int)hzk16;
        memman_free_4k(memman, (int)fat, 4 * 2880); 
        ……
    }
    
  • 修改console_task,nihongo改成hzk16:

    void console_task(struct SHEET *sheet, int memtotal)
    {
        ……
        unsigned char* hzk16 = (char*) * ((int*)0x0fe0);
        ……
        if (hzk16[4096] != 0xff) {	
            task->langmode = 1;
        } else {
            task->langmode = 0;
        }
        ……
    }
    
  • 修改graphic.c中的putfonts8_asc函數:

    void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
    {
        extern char hankaku[4096];
        struct TASK *task = task_now();
        char *font;  
        char* hzk16 = (char*) * ((int*)0x0fe0);
        int k, t;
    
        if (task->langmode == 0) {
            ……
        }
        if (task->langmode == 1) {
            for (; *s != 0x00; s++) {
                if (task->langbyte1 == 0) {
                    if (0x81 <= *s && *s <= 0xfe) {
                        task->langbyte1 = *s;
                    }
                    else {
                        putfont8(vram, xsize, x, y, c, hzk16 + *s * 16);
                    }
                }
                else {
                    k = task->langbyte1 - 0xa1;
                    t = *s - 0xa1;
                    task->langbyte1 = 0;
                    font = hzk16 + 256 * 16 + (k * 94 + t) * 32;
                    putfont32(vram, xsize, x-8, y, c, font, font + 16);
                }
                x += 8;
            }
        }
        return;
    }
    
    • 將langmode=2的部分刪除,並修改langmode=1時爲EUC模式(中文)。
    • 添加新的函數putfont32:
      void putfont32(char* vram, int xsize, int x, int y, char c, char* font1, char* font2) {
          int i;
          char* p, d;
          //上半部分
          for (i = 0; i < 16; i++) {
              p = vram + (y + (i >> 1)) * xsize + x + (i % 2) * 8;
              d = font1[i];
              if ((d & 0x80) != 0) { p[0] = c; }
              if ((d & 0x40) != 0) { p[1] = c; }
              if ((d & 0x20) != 0) { p[2] = c; }
              if ((d & 0x10) != 0) { p[3] = c; }
              if ((d & 0x08) != 0) { p[4] = c; }
              if ((d & 0x04) != 0) { p[5] = c; }
              if ((d & 0x02) != 0) { p[6] = c; }
              if ((d & 0x01) != 0) { p[7] = c; }
          }
          //下半部分
          for (i = 0; i < 16; i++) {
              p = vram + (y + (i >> 1) + 8)* xsize + x + (i % 2) * 8;
              d = font2[i];
              if ((d & 0x80) != 0) { p[0] = c; }
              if ((d & 0x40) != 0) { p[1] = c; }
              if ((d & 0x20) != 0) { p[2] = c; }
              if ((d & 0x10) != 0) { p[3] = c; }
              if ((d & 0x08) != 0) { p[4] = c; }
              if ((d & 0x04) != 0) { p[5] = c; }
              if ((d & 0x02) != 0) { p[6] = c; }
              if ((d & 0x01) != 0) { p[7] = c; }
          }
          return;
      }
      
      • 從這個函數可以看出,HKZ16.fnt中的漢字的兩個字節不再是左右結構,而是上下結構(這裏多謝博客https://www.cnblogs.com/wunaozai/p/3858473.html的提醒)。
      • 圖解如下:
      • 注意:除2是右移1位!不是右移2位!(低級錯誤,以後不可再犯)。
  • 修改完成後,幾個點需要提示一下:

    • 之所以刪掉nihongo.fnt是因爲這個文件是在是太大了,軟盤只有1440KB,如果兼容日文和中文顯示,經實踐,hzk16.fnt將無法全部寫入。日文顯示本來就看不懂,所以索性刪掉了。
    • 因此,現在langmode=1,中文模式;langmode=0,英文模式。
  • 編寫c.txt並將該文件通過Makefile寫入磁盤映像:

    你好,世界!  
    這是來自中國的聲音!
    
  • make full後用VMware運行:

    • 中文漢字成功顯示!
  • 接着修改chklang.c:

    #include "apilib.h"
    
    void HariMain(void)
    {
        int langmode = api_getlang();
        static char s1[17] = {	/* 中文中文EUC模式 */
            0xd6, 0xd0, 0xce, 0xc4, 0x45, 0x55, 0x43, 0xc4, 0xa3, 0xca, 0xbd, 0x0a, 0x00
        };
        if (langmode == 0) {
            api_putstr0("English ASCII mode\n");
        }
        if (langmode == 1) {
            api_putstr0(s1);  
            //api_putstr0("中文EUC模式");
        }
        api_end();
    }
    
    • s1[]的值是通過在BZ中輸入漢字中文EUC模式獲得的:
    • 經檢驗,使用註釋掉的一行也可。
    • 使用VMware運行:
    • 由於是在harib25g的基礎上修改的,因此還收有小bug,不過不要緊,已經很棒了!

9. 寫在1226行

  • 寫到這裏已是2020.5.10 0:27,此Markdown文檔已經再次超越記錄,達到了1226+行。
  • 立的flag又倒了,5.10依舊不能完成既定任務。不過,解決掉小bug以後,便真的接近尾聲了。不廢不立,再立flag:預計5.12能夠完成既定任務。
  • 現在有點累了,早點休息吧。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章