如何讓UEFI BIOS支持漢字顯示:漢字編碼與顯示實踐

我們通常看到的UEFI BIOS都是大片英文字元,對於中國用戶而言,提供中文提示可以幫助我們建立更好的用戶體驗。那麼如何在BIOS中顯示中文呢,其實比較簡單,我們分成兩個部分介紹,第一部分介紹字元編碼理論,第二部分介紹UEFI實踐。如果你對字元編碼理論很了解,可以直接從第二部分看起。

UEFI在訂立之初就充分考慮了多國語言的管理、編碼和顯示問題。多語言的管理不在本文的範圍內,大家可以參考UEFI spec。UEFI中的字元串統一採用Unicode的UCS-2編碼。說起Unicode,它很容易和ASCII、UTF-8、UTF-16、UTF-32和GB2312編碼等等概念混淆混淆。我們先來從歷史沿革來看看他們都是什麼,各自的優缺點都是什麼。

Unicode、UTF-16與UCS-2

如果你是一個生活在2003年的程序員,卻不了解字元、字符集、編碼和Unicode這些基礎知識。那你可要小心了,要是被我抓到你,我會讓你在潛水艇里剝六個月洋蔥來懲罰你。

這個邪惡的恫嚇是Joel Spolsky在1993首次發出的,時至今日,unicode已經廣泛使用在了我們周圍,2003年到了現在又過去十幾年了,如果還不知道unicode,被他抓住就不只是剝洋蔥這麼簡單了。。。

為了避免被他抓住的可悲下場。我們趕緊來複習下字元編碼的歷史和unicode的產生。

1.歷史

話說很久以前,計算機製造商有自己的表示字元的方式。他們並不需要擔心如何和其它計算機交流,並提出了各自的方式來將字形渲染到屏幕上。隨著計算機越來越流行,廠商之間的競爭更加激烈,在不同的計算機體系間轉換數據變得十分頭疼,人們厭煩了這種自定義造成的混亂。

最終,計算機製造商一起制定了一個標準的方法來描述字元。他們定義使用一個位元組的低7位來表示字元,例如,字母A是65,c是99,~是126等等, ASCII碼就這樣誕生了。原始的ASCII標準定義了從0到127 的字元,這樣正好能用七個比特表示。不過好景不長,它國家的人趁這個機會開始使用128到255範圍內的編碼來表達自己語言中的字元。例如,144在阿拉伯人的ASCII碼中是 ,而在俄羅斯的ASCII碼中是?。即使在美國,對於未使用區域也有各種各樣的利用。IBM PC就出現了「OEM 字體」或」擴展ASCII碼」,為用戶提供漂亮的圖形文字來繪製文本框並支持一些歐洲字元,例如英鎊(£)符號。

勤勞的中國人民也行動起來,我們不客氣地把那些127號之後的奇異符號們直接取消掉, 規定:一個小於127的字元的意義與原來相同,但兩個大於127的字元連在一起時,就表示一個漢字,前面的一個位元組(他稱之為高位元組)從0xA1用到0xF7,後面一個位元組(低位元組)從0xA1到0xFE,這樣我們就可以組合出大約7000多個簡體漢字了。在這些編碼里,我們還把數學符號、羅馬希臘的字母、日文的假名們都編進去了,連在 ASCII 里本來就有的數字、標點、字母都統統重新編了兩個位元組長的編碼,這就是常說的"全形"字元,而原來在127號以下的那些就叫"半形"字元了。

中國人民看到這樣很不錯,於是就把這種漢字方案叫做 "GB2312"。GB2312 是對 ASCII 的中文擴展。但是中國的漢字太多了,我們很快就就發現有許多人的人名沒有辦法在這裡打出來,於是我們不得不繼續把 GB2312 沒有用到的碼位找出來老實不客氣地用上。

後來還是不夠用,於是乾脆不再要求低位元組一定是127號之後的內碼,只要第一個位元組是大於127就固定表示這是一個漢字的開始,不管後面跟的是不是擴展字符集里的內容。結果擴展之後的編碼方案被稱為 GBK 標準,GBK 包括了 GB2312 的所有內容,同時又增加了近20000個新的漢字(包括繁體字)和符號。

後來少數民族也要用電腦了,於是我們再擴展,又加了幾千個新的少數民族的字,GBK 擴成了 GB18030。從此之後,中華民族的文化就可以在計算機時代中傳承了。

這帶來了一點小麻煩:字元串長度和編碼的長度不唯一對應,以為常常處理中英夾雜的問題。這也難不倒中國的程序猿們,一個有(di)趣(xiao)的函數就可以解決。另一個稍大點的麻煩是錯誤的傳染性,在某處一個位元組的缺失會使隨後的其他文章成為亂碼!

當各個國家都像中國這樣搞出一套自己的編碼標準,結果互相之間誰也不懂誰的編碼,誰也不支持別人的編碼,連大陸和台灣這樣只相隔了150海里,使用著同一種語言的兄弟地區,也搓弄出來自己的編碼:BIG5。天哪,從此黑暗籠罩在編碼領域,工程師和文檔管理大師們都念下面這個咒語:「換個編碼,這些亂碼一定就會好的,芝麻開門吧!」

ISO國際組織實在不能不管了,於是他們在1991年重新搞一個包括了地球上所有文化、所有字母和符號的編碼!他們打算叫它"Universal Multiple-Octet Coded Character Set",簡稱 UCS, 俗稱 "UNICODE"。他們雄心勃勃,創立了一個65535個碼錶的字符集,認為能Cover所有的字元,這個集合就是UCS-2。這裡我只能說他們Too young, too simple, sometimes…咳咳。我天朝文字界表示不服,康熙字典就有4萬多,再看看我們的詞源和辭海,嚇死你們!韓國人也表示不服:「漢字是我們韓國人發明的!」,更別提還有別的國家的各種蝌蚪文。幸虧ISO知錯能改後面推出UCS-4方案,說簡單了就是四個位元組來表示一個字元,這樣我們就可以組合出21億個不同的字元出來(最高位有其他用途),這大概將來外星人入侵時可以將外星人的文字也包括進來!

Unicode背後的想法非常簡單,然而卻被普遍的誤解了。Unicode就像一個電話本,標記著字元和數字之間的映射關係。Joel稱之為「神奇數字」,因為它們可能是隨機指定的,而且不會給出任何解釋。官方術語是碼位(Code Point),總是用U+開頭。理論上每種語言中的每種字元都被Unicode協會指定了一個神奇數字。例如希伯來文中的第一個字母?,是U+2135,字母A是U+0061。Unicode並不涉及字元是怎麼在位元組中表示的,它僅僅指定了字元對應的數字,僅此而已。他的編碼和傳輸的問題是UTF(Unicode Transformation Formats)定義的,我們通常會遇到的是UTF-8,UTF-16和UTF-32。

2.UTF-8

UTF-8是一個非常驚艷的概念,它漂亮的實現了對ASCII碼的向後兼容,以保證Unicode可以被大眾接受。發明它的人至少應該得個諾貝爾和平獎。Java的預設編碼便是UTF-8。

在UTF-8中,0-127號的字元用1個位元組來表示,使用和US-ASCII相同的編碼。這意味著1980年代寫的文檔用UTF-8打開一點問題都沒有。只有128號及以上的字元才用2個,3個或者4個位元組來表示。因此,UTF-8被稱作可變長度編碼。

3.UTF-16

另一個流行的可變長度編碼方案是UTF-16,它使用2個或者4個位元組來存儲字元。如果字元編碼小於0x10000,則UTF-16直接用UCS-2的兩個位元組表示,否則用變換後的四位元組表示。變換通過一個特殊保留的區間用來表示大於FFFF的值。這些值會通過4個位元組來編碼。這裡不再詳述。Windows系統在NT後預設即是UTF-16。

4.UTF-32

定長編碼,所有字元都是四個位元組。即UCS-4直接搬過來。

5.各種編碼方式優缺點以及UEFI的選擇

英語國家通常都和字母打交道,使用UTF-32會是一種巨大的浪費,想像一下你的.c程序忽然擴大四倍的樣子!用UTF-16也會有些浪費。似乎UTF-8很優秀了?其實還是那個問題,字元串長度和編碼長度不一致,導致要做些運算才能得到字元串長度。而UTF-32就方便了,直接字元串長度除以4即可!

UEFI採取了折中的方式,沒有採用他們中任何一個,而是採用標準的UCS-2編碼,即每個字元佔兩個位元組,字元串長度=編碼長度 / 2;(強迫症發作,不得不加個分號) ,對於UTF-16的擴展部分,答案很簡單,不支持

字元顯示原理

了解完了字元編碼,我們接著看看一個字元如何顯示出來。UEFI提供GOP或UGA protocol,我們可以用它來顯示一幅點陣圖。同樣,如果我們有了點陣字型檔,我們也可以用它來顯示字元。幸運的是,UEFI已經替我們考慮好了怎麼顯示字元,它定義了SimpleFont格式。SimpleFont是一種點陣字體,有兩種格式,一種是窄體字,一種是寬體字。窄體字是一種8×19的點陣字型檔,寬體字是16×19的點陣字型檔,分成兩半,前一半表示左邊部分,後一半表示右邊部分。點陣字型檔中每一位(bit)代表一個像素,有色是1,空白是0。我們以英文E做例子:

它的編碼是:

EFI_NARROW_GLYPH GlyphData[] = {…n {0x0045, 0x00,{0x00, 0x00, 0x00, 0xFE, 0x66, 0x62,0x60, 0x68, 0x78, 0x68,0x60, 0x60, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, 0x00}},n …};n

我們可以通過HiiAddPackages的形式註冊字體點陣到HII的數據倉庫中。在EDKII的GraphicConsoleDxe驅動中有現成的例子。窄體字點陣在

MdeModulePkgUniversalConsoleGraphicsConsoleDxe LaffStd.cn

中,註冊函數在同一個驅動的GraphicsConsole.c的RegisterFontPackage函數里:

PackageLength = sizeof (EFI_HII_SIMPLE_FONT_PACKAGE_HDR) + mNarrowFontSize + 4;n Package = AllocateZeroPool (PackageLength);n ASSERT (Package != NULL);n WriteUnaligned32((UINT32 *) Package,PackageLength);n SimplifiedFont = (EFI_HII_SIMPLE_FONT_PACKAGE_HDR *) (Package + 4);n SimplifiedFont->Header.Length = (UINT32) (PackageLength - 4);n SimplifiedFont->Header.Type = EFI_HII_PACKAGE_SIMPLE_FONTS;n SimplifiedFont->NumberOfNarrowGlyphs = (UINT16) (mNarrowFontSize / sizeof (EFI_NARROW_GLYPH));n Location = (UINT8 *) (&SimplifiedFont->NumberOfWideGlyphs + 1);n CopyMem (Location, gUsStdNarrowGlyphData, mNarrowFontSize);nnn mHiiHandle = HiiAddPackages (n &mFontPackageListGuid,n NULL,n Package,n NULLn );n

注意Simple Font同時支持寬體和窄體兩種。那麼漢字應該是什麼樣呢,我們以「永」字來舉個例子:

按照寬體字的編碼標準應該編做:

EFI_WIDE_GLYPH gUsStdWideGlyphData[] = {n { 0x6c38, 0x00, {0x00,0x02,0x01,0x00,0x1F,0x01,0x01,0x7D,0x05,0x05,0x09,0x09,0x11,0x21,0x41,0x05,0x02,0x00,0x00}, {0x00,0x00,0x00,0x00,0x00,0x08,0x18,0xA0,0xC0,0x40,0x20,0x20,0x10,0x0E,0x04,0x00,0x00,0x00,0x00}, n {0x00,0x00,0x00}},n …};n

如前文,注意分成左邊和右邊兩部分。很簡單是不是?

實踐

1。環境搭建

有了前面的理論我們來驗證一下, NT32提供了很好的實驗環境,我們不必有個物理的機器就可以驗證對EDKII內核的改動。我們先從Github上下載最新的EDKII的源程序。接下來配置編譯環境:

Edksetup –nt32n

環境設置好後我們編譯一下:

Buildn

一切順利的話,我們可以運行看看:

Build runn

2。編程

到這裡,實驗環境搭建完成。我們開始進行我們最喜歡的活動吧!打開LaffStd.c,加入「永」的點陣字模:

EFI_WIDE_GLYPH gUsStdWideGlyphData[] = {n { 0x6c38, 0x00, {0x00,0x02,0x01,0x00,0x1F,0x01,0x01,0x7D,0x05,0x05,0x09,0x09,0x11,0x21,0x41,0x05,0x02,0x00,0x00}, {0x00,0x00,0x00,0x00,0x00,0x08,0x18,0xA0,0xC0,0x40,0x20,0x20,0x10,0x0E,0x04,0x00,0x00,0x00,0x00}, {0x00,0x00,0x00}},n { 0x0000, 0x00, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, {0x00,0x00,0x00}} //EOLn};nUINT32 mWideFontSize = sizeof (gUsStdWideGlyphData);n

接下來在GraphicConsoleDxe.c里將其和窄體字一起註冊到HII(注意Wide部分才是新加的):

PackageLength = sizeof (EFI_HII_SIMPLE_FONT_PACKAGE_HDR) + mNarrowFontSize +mWideFontSize + 4;n Package = AllocateZeroPool (PackageLength);n ASSERT (Package != NULL);n WriteUnaligned32((UINT32 *) Package,PackageLength);n SimplifiedFont = (EFI_HII_SIMPLE_FONT_PACKAGE_HDR *) (Package + 4);n SimplifiedFont->Header.Length = (UINT32) (PackageLength - 4);n SimplifiedFont->Header.Type = EFI_HII_PACKAGE_SIMPLE_FONTS;n SimplifiedFont->NumberOfNarrowGlyphs = (UINT16) (mNarrowFontSize / sizeof (EFI_NARROW_GLYPH));n Location = (UINT8 *) (&SimplifiedFont->NumberOfWideGlyphs + 1);n CopyMem (Location, gUsStdNarrowGlyphData, mNarrowFontSize);nn SimplifiedFont->NumberOfWideGlyphs = (UINT16) (mWideFontSize / sizeof (EFI_WIDE_GLYPH));n Location += mNarrowFontSize;n CopyMem (Location, gUsStdWideGlyphData, mWideFontSize);n

大功告成,開香檳!Wait a minute,好像需要驗證一下。我們從MdeModulePackage的HelloWorld Shell應用下手,加入下面兩行到main函數:

Print (L"I like it!n");nPrint (L"永n");n

試試看吧。運行NT32環境,進入shell,鍵入HelloWorld,得到如下:

一切正常!順便考驗一下HII對寬體和窄體字混排會不會出問題,代碼改成如下:

Print (L"I like it!n");nPrint (L"永n");nPrint (L"I永like it!n");nPrint (L"Il永ike it!n");n

再次編譯測試,結果如下:

噠噠,HII順利過關!

後記

這裡只舉了一個字的字模,其他的字怎麼辦?有一種辦法是在Windows上用程序抓取True Type的字型檔,將其轉換成點陣字型檔。即寫個簡單的python程序,將true type的字投影到16×19的方格上,投影的每個格子有色即是1,無色即是0。簡單方便,瞬間就可以完成。這裡有兩個小問題:

1. True Type字型檔是矢量字型檔,放大不會變形。放到我們的小格子裡面,有些邊邊角角需要在修飾一下。可以藉助網上免費的小工具,數量也不會太多。

2. 字體是有版權的!不能亂用。我們可以找些Open的字型檔,將其copy到windows的font目錄下即可。

最後要提醒的是,如果用在產品中最好另外寫個模塊單獨註冊,不要全部加到GraphicConsoleDxe中去了。

歡迎大家關注本專欄和用微信掃描下方二維碼加入微信公眾號"UEFIBlog",在那裡有最新的文章。同時歡迎大家給本專欄和公眾號投稿!

用微信掃描二維碼加入UEFIBlog公眾號


推薦閱讀:

UEFI快速上手:如何用VS調試NT32模擬環境
UEFI到操作系統的虛擬地址轉換

TAG:UEFI | 固件 | 计算机 |