|
"陷阱"技术探秘 7 l! h Y; m' X
──动态汉化Windows技术的分析
6 N" B/ D) ]) ?- e! N % e4 a2 Y& A5 y5 R% @
各位朋友,请加入本站邮件列表,你将得到本站最新更新及动态。 1 u8 E0 P1 t- B! l. j
6 H1 n' h! u8 k: X# s( \ z
四通利方(RichWin)、中文之星(CStar)是大家广为熟知的汉化Windows产品,"陷阱"技术即动态修改Windows代码,一直是其对外宣称的过人技术。本文从Windows的模块调用机制与重定位概念着手,介绍了"陷阱"技术的实现,并给出了采用"陷阱"技术动态修改Windows代码的示例源程序。
0 c& B9 J+ E+ c5 f8 J/ w4 R* i一、发现了什么? - v2 ]" R! R7 v
笔者多年来一直从事Windows下的软件开发工作,经历了Windows 2.0 、 3.0 、3.1 ,直至Windows 95、NT的成长过程,也遍历了长青窗口、长城窗口、DBWin、CStar、RichWin等多个Windows汉化产品。从现在看来,影响最大也最为成功的,当推四通利方的RichWin;此外,中文之星CStar与RichWin师出一门,其核心技术自然也差不多。其对外宣传采用独特的"陷阱" 技术即动态修改Windows代码,一直是笔者感兴趣的地方。
& I; i4 ~- D& R6 }& [( BEXEHDR是Microsoft Visual C++开发工具中很有用的一个程序,它可以检查NE(New-Exe cutable)格式文件,用它来分析RichWin的WSENGINE.DLL或CStar的CHINESE.DLL,就会发现与众不同的两点(以CStar 1.20为例):
/ u& K5 ~) ~7 N8 z( m: \4 w3 W7 `& w. Q. A
C:\CSTAR>exehdr chinese.dll /v: ^' W+ b7 H+ A
.................................. ! b& a. ]5 w% V; W P" e8 @4 D
6 type offset target
) }' E S8 F; R) n3 G0 |0 R/ Z BASE 060a seg 2 offset 0000 ) C, G4 `; Y# ^) b9 E, Z$ O# f" i
PTR 047e imp GDI.GETCHARABCWIDTHS
( R) k6 s0 z7 n* P3 r; X/ m' N& |. m PTR 059b imp GDI.ENUMFONTFAMILIES
! P* ]& ~/ d$ N1 y: ^- i PTR 0451 imp DISPLAY.14 ( EXTTEXTOUT ) 6 V; `9 \$ z2 ^2 O2 m7 e6 e
PTR 0415 imp KEYBOARD.4 ( TOASCII ) # H' Q2 J3 F2 Y
PTR 04ba imp KEYBOARD.5 ( ANSITOOEM ) . X8 w* A1 Z8 F0 \; W6 c# b
PTR 04c9 imp KEYBOARD.6 ( OEMTOANSI )
. w- G8 N1 U0 } PTR 04d8 imp KEYBOARD.134( ANSITOOEMBUFF )
& f' O2 \6 p2 ^" w7 v+ M# P% T. h PTR 05f5 imp USER.430 ( LSTRCMP ) & r/ ~" o$ d' C, D9 g- G' Z
PTR 04e7 imp KEYBOARD.135( OEMTOANSIBUFF )
5 H/ K9 ]8 ~. |# U2 Q, H8 e PTR 0514 imp USER.431 ( ANSIUPPER )
- V; M' p' H- J" f PTR 0523 imp USER.432 ( ANSILOWER ) % z+ x# Y* P- B0 o# \6 Y" ^
PTR 05aa imp GDI.56 ( CREATEFONT ) ) [' d' ~( m/ [+ `! K
PTR 056e imp USER.433 ( ISCHARALPHA ) ) I- R, ?! o- x2 R7 b5 h
PTR 05b9 imp GDI.57 ( CREATEFONTINDIRECT )
9 x, `* g( a' I d; r4 _ PTR 057d imp USER.434 ( ISCHARALPHANUMERIC ) 1 J" m3 J' I+ `# c# L$ \$ s* P
PTR 049c imp USER.179 ( GETSYSTEMMETRICS ) 8 X3 |# a5 `+ e/ S
PTR 0550 imp USER.435 ( ISCHARUPPER ) * w2 A" _( S# t/ ]! l. E% B1 `9 u
PTR 055f imp USER.436 ( ISCHARLOWER ) + x3 S) d- }" K/ E5 J% Z
PTR 0532 imp USER.437 ( ANSIUPPERBUFF )
# d& ]. S: l! K$ @2 U M; C2 V* ` PTR 0541 imp USER.438 ( ANSILOWERBUFF )
1 q6 I8 n4 p: a PTR 05c8 imp GDI.69 ( DELETEOBJECT )
& p" U9 E Q: J# R4 w7 q' d PTR 058c imp GDI.70 ( ENUMFONTS )
. B& y" e' e6 W# s1 n {- \ PTR 04ab imp KERNEL.ISDBCSLEADBYTE
0 @+ A, U- w* N5 |' k6 p ?* ? PTR 05d7 imp GDI.82 ( GETOBJECT ) * m7 ~6 v- X& y; V0 C6 Q% H7 E% g/ K
PTR 048d imp KERNEL.74 ( OPENFILE )
" E+ K- x' ~! @6 ~ PTR 0460 imp GDI.91 ( GETTEXTEXTENT )
$ K% x v3 S$ {9 v PTR 05e6 imp GDI.92 ( GETTEXTFACE ) & J, x/ J) U( f8 b0 @7 G
PTR 046f imp GDI.350 ( GETCHARWIDTH )
' K7 `& w; O2 I4 w3 p3 w3 V PTR 0442 imp GDI.351 ( EXTTEXTOUT )
0 Y: M' ?0 x) ~ u PTR 0604 imp USER.471 ( LSTRCMPI ) " m! B5 f# y( J2 I) u, ~8 o
PTR 04f6 imp USER.472 ( ANSINEXT )
& w |8 M- [6 U' p- u PTR 0505 imp USER.473 ( ANSIPREV )1 h& U2 {* o4 g) [8 f3 T
* K2 n$ L) W0 l PTR 0424 imp USER.108 ( GETMESSAGE )
2 Z6 r; {( _% B! r6 s& }2 l PTR 0433 imp USER.109 ( PEEKMESSAGE )
# \: f& I- x5 v' K5 \: q1 w35 relocations
8 u1 x9 b) n, a(括号内为笔者加上的对应Windows API函数。)
& d+ x$ q/ J5 u第一,在数据段中,发现了重定位信息。 2 K$ l, T g( X( W6 J) p
第二,这些重定位信息提示的函数,全都与文字显示输出和键盘、字符串有关。也就是说汉化Windows,必须修改这些函数。 + e% j. J$ e5 ?( V3 ?8 l/ W
在这非常特殊的地方,隐藏着什么呢?毋庸置疑,这与众不同的两点,对打开"陷阱"技术之门而言,不是金钥匙,也是敲门砖。
/ o# Y; d" p7 }( Y; J( r# \' O* }二、Windows的模块调用机制与重定位概念
, P$ U! U2 t% _0 S g为了深入探究"陷阱"技术,我们先来介绍Windows的模块调用机制。 . V; \ ^! W/ Q9 m1 ?8 z
Windows的运行分实模式、标准模式和增强模式三种,虽然这几种模式各不相同,但其核心模块的调用关系却是完全一致的,见图一。
- w' W d7 E- t! Q% f9 u: o9 x& b" w主要的三个模块,有如下的关系: + H* l7 R7 V/ s, F( l8 y2 h
·KERNEL是Windows系统内核,它不依赖其它模块。
' y1 h" {8 [5 Z& E4 Z·GDI是Windows图形设备接口模块,它依赖于KERNEL模块。 ! a+ o( @. H, V/ h5 J
·USER是Windows用户接口服务模块,它依赖于KERNEL、GDI模块及设备驱动程序等所有模块。
& a" ^: a( P4 y& n y这三个模块,实际上就是Windows的三个动态链接库。KERNEL有三种系统存在形式:Kern el.exe(实模式)、Krnl286.exe(标准模式)、Krnl386.exe(386增强模式);GDI模块是Gdi.ex e;USER模块是User.exe。虽然文件名都以EXE为扩展名,但它们实际都是动态链接库。 % e* @# T9 ?$ q* W
<图片>
5 J5 G& p+ _$ E5 `% Y, Q图1 Windows的模块调用机制
3 s! q- R0 j" _7 Q同时,几乎所有的API函数都隐藏在这三个模块中。用EXEHDR对这三个模块分析,就可列出一大堆大家所熟悉的Windows API函数。 4 ~$ @5 G, Z( F: ~" a% \3 e
以GDI模块为例,运行结果如下: ' t. @5 y$ [) p/ U! r. T, T
C:\WINDOWS\SYSTEM>exehdr gdi.exe
- s$ }" O! o1 ^0 zExports: " [ @2 s% j; W8 z4 e
rd seg offset name
" e2 c8 C7 W6 v d............
2 z; Q1 O& e, |* o9 K351 1 923e EXTTEXTOUT exported, shared data " w8 T3 S5 e+ [7 N5 `, _2 j
56 3 19e1 CREATEFONT exported, shared data 2 [$ }; T7 \0 d7 f6 e
............ * D5 {0 i& O+ x/ e9 \
至此,读者已能从Windows纷繁复杂的系统中理出一些头续来。下面,再引入一个重要概念——重定位。 + k2 x0 o; o# ~& z% A1 X$ h
一个Windows执行程序对调用API函数或对其它动态库的调用,在程序装入内存前,都是一些不能定位的动态链接;当程序调入内存时,这些远调用都需要重新定位,重新定位的依据就是重定位表。在Windows执行程序(包括动态库)的每个段后面,通常都跟有这样一个重定位表。重定位包含调用函数所在模块、函数序列号以及定位在模块中的位置。 : q7 x& s Z7 r7 D( ?8 \+ D% ]
例如,用EXEHDR /v 分析CHINESE.DLL得到:
( i) W5 F4 ]- l s8 \+ n- s6 type offset target 6 ^2 ?8 W6 C& F( f* J
..........
: R# O0 Q1 Y. ^( p. Q( C8 LPTR 0442 imp GDI.351 * ~# H' C7 x( X# O
8 u% K( l7 o( s% b..........
$ i! c% e( O- ]! |3 x v就表明,在本段的0442H偏移处,调用了GDI的第351号函数。如果在0442H处是0000:FFFF ,表示本段内仅此一处调用了GDI.351函数;否则,表明了本段内还有一处调用此函数,调用的位置就是0442H处所指向的内容,实际上重定位表只含有引用位置的链表的链头。那么,GDI. 351是一个什么函数呢?用EXEHDR对GDI.EXE作一分析,就可得出,在GDI的出口(Export)函数中,第351号是ExtTextOut。 , {. U# k- M0 r) N3 T; I, {
这样,我们在EXEHDR这一简单而非常有用的工具帮助下,已经在Windows的浩瀚海洋中畅游了一会,下面让我们继续深入下去。 0 v+ ^3 `$ A/ K
三、动态汉化Windows原理 & Z0 \, W0 L0 I0 I2 o
我们知道,传统的汉化Windows的方法,是要直接修改Windows的显示、输入、打印等模块代码,或用DDK直接开发"中文设备"驱动模块。这样不仅工作量大,而且,系统的完备性很难保证,性能上也有很多限制(早期的长青窗口就是如此),所以只有从内核上修改Windows核心代码才是最彻底的办法。
; f Q$ F2 i* {( X G从Windows的模块调用机制,我们可以看到,Windows实际上是由包括在KERNEL、GDI、US ER等几个模块中的众多函数支撑的。那么,修改其中涉及语言文字处理的函数,使之能适应中文需要,不就能达到汉化目的了吗? ' ^7 H3 [4 a, T. }& f! @
因而,我们可以得出这样的结论:在自己的模块中重新编写涉及文字显示、输入的多个函数,然后,将Windows中对这些函数的引用,改向到自己的这些模块中来。修改哪些函数才能完成汉化,这需要深入分析Windows的内部结构,但CHINESE.DLL已明确无误地告诉了我们,在其数据段的重定位表中列出的引用函数,正是CStar修改了的Windows函数!为了验证这一思路, 我们利用RichWin作一核实。
; G# T- ]! Z9 J3 Z) l* B用EXEHDR分析GDI.EXE,得出ExtTextOut函数在GDI的第一代码段6139H偏移处(不同版本的Windows其所在代码段和偏移可能不一样)。然后,用HelpWalk(也是Microsoft Visual C+ +开发工具中的一个)检查GDI的Code1段,6139H处前5个字节是 B8 FF 05 45 55,经过运行Ri chWin 4.3 for Internet后,再查看同样的地方,已改为 EA 08 08 8F 3D。其实反汇编就知道,这5个字节就是 Jmp 3D8F:0808,而句柄为0x3D8F的模块,用HelpWalk能观察正是RichWin 的WSENGINE.DLL的第一代码段( 模块名为TEXTMAN)。而偏移0808H处 B8 B7 3D 45 55 8B E C 1E,正是一个函数起始的地方,这实际上就是RichWin所重改写的ExtTextOut函数。退出Ri chWin后,再用HelpWalk观察GDI的Code1代码段,一切又恢复正常!这与前面的分析结论完全吻合!那么,下一个关键点就是如何动态修改Windows的函数代码,也就是汉化Windows的核心——"陷阱"技术。
5 @* K% a0 w/ `7 D8 ^% j* \' W四、"陷阱"技术 $ [1 U' P5 e# K: U5 q
讨论"陷阱"技术,还要回到前面的两个发现。发现之二,已能解释为修改的Windows函数,而发现之一却仍是一个迷。 / u$ N+ H6 h% q" ^. ] |
数据段存放的是变量及常量等内容,如果这里面包含有重定位信息,那么,必定要在变量说明中将函数指针赋给一个FARPROC类型的变量,于是,在变量说明中写下:
h( I7 x0 g- _4 _& [FARPROC FarProcFunc=ExtTextOut; 4 e1 @) u% A/ d0 |
果然,在自己程序的数据段中也有了重定位信息。这样,当程序调入内存时,变量FarPro cFunc已是函数ExtTextOut的地址了。 7 U: M5 J# K5 r2 Y& R1 M
要直接修改代码段的内容,还遇到一个难题,就是代码段是不可改写的。这时,需要用到一个未公开的Windows函数AllocCStoDSAlias,取得与代码段有相同基址的可写数据段别名, 其函数声明为:
) ]: K7 l. U I2 Z) M3 gWORD FAR PASCAL AllocCStoDSAlias(WORD code_sel);
2 ? k) E7 A/ b/ m参数是代码段的句柄,返回值是可写数据段别名句柄。
$ Z( k3 m. l/ h2 iWindows中函数地址是32位,高字节是其模块的内存句柄,低字节是函数在模块内的偏移。将得到的可写数据段别名句柄锁定,再将函数偏移处的5个字节保留下来,然后将其改为转向替代函数(用 EA Jmp):
2 M8 g) T' Z6 e+ g1 @6 c*(lpStr+wOffset) =0xEA; ' Z9 O9 ^) E! K( C8 l7 p
四通利方(RichWin)、中文之星(CStar)是大家广为熟知的汉化Windows产品,"陷阱"技术即动态修改Windows代码,一直是其对外宣称的过人技术。本文从Windows的模块调用机制与重定位概念着手,介绍了"陷阱"技术的实现,并给出了采用"陷阱"技术动态修改Windows代码的示例源程序。
( p2 @% K4 s( F$ z6 G. J" ^/ P( |7 u& j/ \
//源程序 relocate.c
. f* \9 r& T2 d' d" f5 s' @& c#include <WINDOWS.H>
* h6 X+ V+ I! k |+ T#include <dos.h> 2 ~3 ~7 \$ i! I- t4 p7 l- m! g! f6 P5 @
BOOL WINAPI MyExtTextOut(HDC hDC, int x, int y, UINT nInt1, const RECTFAR*l
1 X0 `$ q) }8 M4 f) NpRect,LPCSTR lpStr, UINT nInt2, int FAR* lpInt); " \ P* \$ X, ]+ {# C
WORD FAR PASCAL AllocCStoDSAlias(WORD code_sel); , T. Y" f" K& \
typedef struct tagFUNC ( h% k3 R; y' v' {+ O" T
{ 9 F" v- f8 L O0 E* ~# Q% n
FARPROC lpFarProcReplace; //替代函数地址 " C, M) U+ w: A4 |. ]. t# B
FARPROC lpFarProcWindows; //Windows函数地址
3 l0 U4 W; W% N* I1 ?$ qBYTE bOld; //保存原函数第一字节 J+ y% {( A4 M& v9 s
LONG lOld; //保存原函数接后的四字节长值
- |- [1 ^+ q! E2 ^9 _; A}FUNC; + N. |7 A; q1 \: q
* J) [! R1 M) M* X9 G$ p
FUNC Func={MyExtTextOut,ExtTextOut};
' @9 H6 b" O# V1 ]7 J: U9 \//Windows主函数
) s4 J, H7 K8 u# T# o* t2 aint PASCAL WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdL
+ a1 w# Z0 y' ?9 U: fine,int nCmdShow){ d! h* U' w: R' u+ x
HANDLE hMemCode; //代码段句柄
- b/ N0 R' W! Z5 zWORD hMemData; //相同基址的可写数据段别名 $ Y6 J# v/ Q% V2 z4 D9 s3 } {
WORD wOffset; //函数偏移 + t1 S G3 Q% i: `1 g" J6 L
LPSTR lpStr; 1 Q+ `, v6 b2 U
LPLONG lpLong;
1 d( J7 a( t# R, D' N% }, `" E5 Kchar lpNotice[96]; ' U7 k m$ `$ Z7 m9 |, G7 ?
hMemCode=HIWORD((LONG) Func.lpFarProcWindows ); - n/ a4 `, A( c0 A, a
wOffset=LOWORD((LONG) Func.lpFarProcWindows );
, n% c! ?& ]# h: N# L% cwsprintf(lpNotice,"函数所在模块句柄 0x%4xH,偏移 0x%4xH",hMemCode,wOffset);
8 g. i- I, e- K; Y4 f MessageBox(NULL,lpNotice,"提示",MB_OK); + T! y! m' K- A7 a8 g
//取与代码段有相同基址的可写数据段别名
! S5 g. w) J* r- Z! I; K hMemData=AllocCStoDSAlias(hMemCode); 2 b1 m* b! S8 v/ `$ Z H
lpStr=GlobalLock(hMemData);
. D+ s7 u0 M0 G2 _. s lpLong=(lpStr+wOffset+1 ); & B; K( Z3 S( A( |; {
//保存原函数要替换的头几个字节 . A! {' u r* v3 N) s
Func.bOld=*(lpStr+wOffset);
" Y$ x! T# C6 G3 w7 Z% [) {' _$ dFunc.lOld=*lpLong;
& l4 }7 a6 [3 `7 l*(lpStr+wOffset)=0xEA; . d3 y# h% \* [4 W0 i/ x
*lpLong=Func.lpFarProcReplace; : W/ x% o1 h/ p
GlobalUnlock(hMemData); 3 h% R+ F( [# k/ x$ M$ A
MessageBox(NULL,"改为自己的函数","提示",MB_OK); ! ?# H- M" w1 u7 U) t* r
//将保留的内容改回来
" V5 M$ I0 Z+ ]' ^& ~hMemData=AllocCStoDSAlias(hMemCode);
% p6 X( h/ I M. qlpStr=GlobalLock(hMemData);
" [9 X& Q- e3 ulpLong=(lpStr+wOffset+1 ); : X1 R$ P; V* `/ t
*(lpStr+wOffset)=Func.bOld;
7 t% N7 g) g1 F3 C*lpLong=Func.lOld;
$ o9 c% H- c( w$ iGlobalUnlock(hMemData); % e( N0 O) K9 t& a% m& k) @) }
MessageBox(NULL,"改回原Windows函数","提示",MB_OK);
u' Q" o5 R/ A t! v Sreturn 1;
9 K9 O$ f4 I6 p/ c5 N2 Y} h; j8 y, I# R4 h/ K' Y) Q
//自己的替代函数 9 C* L/ G2 y1 @
BOOL WINAPI MyExtTextOut(HDC hDC, int x, int y, UINT nInt1, const RECT FAR*5 N5 x8 X$ V2 S; o4 J
lpRect, LPCSTR lpStr, UINT nInt2, int FAR* lpInt){
& S! |0 D4 l' p" wBYTE NameDot[96]={
; F9 S7 x; ?9 X 0x09, 0x00, 0xfd, 0x08, 0x09, 0x08, 0x09, 0x10, 0x09, 0x20,
7 ?+ h# x9 T$ ~2 z' k: Y 0x79, 0x40, 0x41, 0x04, 0x47, 0xfe, 0x41, 0x40, 0x79, 0x40, , L$ L& a* V+ [5 \ U/ P8 ]* r
0x09, 0x20, 0x09, 0x20, 0x09, 0x10, 0x09, 0x4e, 0x51, 0x84,
; `3 L/ `# i* l, ] 0x21, 0x00, 0x02, 0x00, 0x01, 0x04, 0xff, 0xfe, 0x00, 0x00,
; K( B3 i0 Q% J$ z! T1 X" s! Q- ^ 0x1f, 0xf0, 0x10, 0x10, 0x10, 0x10, 0x1f, 0xf0, 0x00, 0x00,
0 U! R! n( i1 S5 q ~. f) O$ n 0x7f, 0xfc, 0x40, 0x04, 0x4f, 0xe4, 0x48, 0x24, 0x48, 0x24,
5 w7 n2 \6 {, o$ T- F# S- d/ \ 0x4f, 0xe4, 0x40, 0x0c, 0x10, 0x80, 0x10, 0xfc, 0x10, 0x88, 2 _9 x. C, \1 O" B7 G* g
0x11, 0x50, 0x56, 0x20, 0x54, 0xd8, 0x57, 0x06, 0x54, 0x20,
9 h3 f: N6 q) U 0x55, 0xfc, 0x54, 0x20, 0x55, 0xfc, 0x5c, 0x20, 0x67, 0xfe, 5 V' l7 V2 W# w5 E% K: d
0x00, 0x20, 0x00, 0x20, 0x00, 0x20
$ `4 C1 |* b5 A5 c! S2 B! W};
, C, w) `3 x5 hHBITMAP hBitmap,hOldBitmap;
% v( M/ m" ]! H4 Q- S HDC hMemDC; " u4 p! j1 N3 a6 R2 |
BYTE far *lpDot; $ X) i) y7 D* D% n7 f5 ~7 ^$ k6 F
int i; " N7 V- e: }1 p) `0 D
for ( i=0;i<3;i++ )
7 e3 c: k0 {- [5 J; k! |4 \{ - M. X/ k6 [6 K& i- ]3 J2 l( v
lpDot=(LPSTR)NameDot+i*32;
9 D7 |+ o( Y8 u. D+ [) i2 {hMemDC=CreateCompatibleDC(hDC);
- ^7 T% N" e1 F8 Z& MhBitmap=CreateBitmap(16,16,1,1,lpDot);
0 n0 x) }' g, ? V& I, E" ?SetBitmapBits(hBitmap,32L,lpDot); 4 ^& t! }/ i, x) A% m* G
hOldBitmap=SelectObject(hMemDC,hBitmap);
1 o" j1 C3 K/ J8 p. i+ \BitBlt(hDC,x+i*16,y,16,16,hMemDC,0,0,SRCCOPY); , Y3 O) d; G) Y/ o9 _* k0 C
DeleteDC(hMemDC);
; A" U+ B# F) ~9 EDeleteObject(hBitmap); 6 {' ^ q6 @- b( w* ~& `
} " h+ @( h3 {. h$ k+ ?& A
return TRUE; ! J. X1 K9 L: ~% B: i
}
0 E+ l% a2 b$ d
# Z* L2 o0 \0 O. I! h n# G* J//模块定义文件 relocate.def
0 W* j# V7 K8 ~, G' y XNAME RELOCATE
# |2 R% P' d, OEXETYPE WINDOWS
1 Q$ `5 v3 `) N' _2 E: H6 o6 g0 RCODE PRELOAD MOVEABLE DISCARDABLE
. q, F' V, k- \9 `3 gDATA PRELOAD MOVEABLE MULTIPLE
* }9 v( X4 k8 } R, T6 |HEAPSIZE 1024 7 o, T, p" M4 ~# O# j2 X0 K i
EXPORTS , m$ {& y, M! k; X6 W" T
. l; H$ O4 ?8 {* X& {$ H0 g3 e五、结束语 , B+ I) X/ l2 o. ] Q g7 E2 G, G$ L
本文从原理上分析了称为"陷阱"技术的动态汉化Windows方法,介绍了将任一Windows函数调用改向到自己指定函数处的通用方法,这种方法可以拓展到其它应用中,如多语种显示、不同内码制式的切换显示等。 : }4 u! `, j/ |9 K1 N; m0 W- z. h* U9 f
+ P* q6 K/ p# w' E- |3 r0 [5 F% l |