第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
。
- 這個程序是無法運行的,因爲RET返回的地址保存在ESP中,而ESP的值在這裏被改變了,於是讀取了錯誤的返回地址。注意:RET相當於
- 接着又編寫了第二版錯誤的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
- 和第四版大同小異,不多精簡了一點兒。
- __alloca函數會在下述情況下被C語言的程序調用(採用near-CALL的方式):
-
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。之所以對文件大小這樣苛刻,是因爲擔心磁盤空間不夠(後面還要支持漢字字庫),因此應用程序能小就小。
- 在Makefile中設定
-
順便把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
- 同打開
- 打開:open
-
設計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:當前讀取的文件位置
- 結構體TASK增加了成員fhandle和fat,是爲了讓hrb_api和cmd_app能夠使用在console_task中聲明的變量。
-
修改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,這是彙編的既定規定)
- api021.nas:
-
編寫用於測試的應用程序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
- 上述代碼實現的功能:首先分配出用於存放nihongo.fnt內容的內存空間,然後尋找文件,如果找到的話載入內存。如果沒有找到字庫文件,則只好用內置的半角字庫代替日文半角字符,並用方塊填充全角字庫的部分。最後,將用於存放nihongo.fnt內容的內存地址寫入0x0fe8作爲記錄。
-
修改bootpack.h中TASK結構體:
struct TASK{ …… char langmode; }
- 新添加的成員變量langmode(language mode,語言模式),用於指定一個任務使用的是內置的英文字庫還是使用nihongo.fnt的日文字庫。有了這個langmode,就可以對每一個任務單獨設置語言模式,例如爲某個應用程序設置日文模式,爲另一個應用程序設置英文模式。
- langmode=0,英文模式;langmode=1:日文模式。
- 新添加的成員變量langmode(language mode,語言模式),用於指定一個任務使用的是內置的英文字庫還是使用nihongo.fnt的日文字庫。有了這個langmode,就可以對每一個任務單獨設置語言模式,例如爲某個應用程序設置日文模式,爲另一個應用程序設置英文模式。
-
修改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。
- 注意,默認情況下的語言模式時Shift-JIS,因此現需要輸入
-
對於中文:
- 前面已經提到過,中文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使用
-
然後在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,不過不要緊,已經很棒了!
- s1[]的值是通過在BZ中輸入漢字
9. 寫在1226行
- 寫到這裏已是2020.5.10 0:27,此Markdown文檔已經再次超越記錄,達到了1226+行。
- 立的flag又倒了,5.10依舊不能完成既定任務。不過,解決掉小bug以後,便真的接近尾聲了。不廢不立,再立flag:預計5.12能夠完成既定任務。
- 現在有點累了,早點休息吧。