|
"陷阱"技术探秘
2 e, y* J4 c0 F6 ]0 t# D──动态汉化Windows技术的分析
; }; `$ V1 m2 R" b% R / K3 K3 H& D$ ]/ j* F) r
各位朋友,请加入本站邮件列表,你将得到本站最新更新及动态。 & ~) _, R0 q6 l; C& M6 {
" k- o3 V3 I N
四通利方(RichWin)、中文之星(CStar)是大家广为熟知的汉化Windows产品,"陷阱"技术即动态修改Windows代码,一直是其对外宣称的过人技术。本文从Windows的模块调用机制与重定位概念着手,介绍了"陷阱"技术的实现,并给出了采用"陷阱"技术动态修改Windows代码的示例源程序。 / C v$ V6 O8 v$ y, x5 s
一、发现了什么?
3 p4 K" Z5 V" N( I3 y笔者多年来一直从事Windows下的软件开发工作,经历了Windows 2.0 、 3.0 、3.1 ,直至Windows 95、NT的成长过程,也遍历了长青窗口、长城窗口、DBWin、CStar、RichWin等多个Windows汉化产品。从现在看来,影响最大也最为成功的,当推四通利方的RichWin;此外,中文之星CStar与RichWin师出一门,其核心技术自然也差不多。其对外宣传采用独特的"陷阱" 技术即动态修改Windows代码,一直是笔者感兴趣的地方。
) S+ y' {: s% M' E7 qEXEHDR是Microsoft Visual C++开发工具中很有用的一个程序,它可以检查NE(New-Exe cutable)格式文件,用它来分析RichWin的WSENGINE.DLL或CStar的CHINESE.DLL,就会发现与众不同的两点(以CStar 1.20为例): # C/ ?- g* P q0 [" `' c
7 \ i$ h9 i2 x$ l0 j; wC:\CSTAR>exehdr chinese.dll /v
6 _6 T5 F6 b8 t. `.................................. 1 C& x/ @, S$ Y% y1 M0 |* ~/ t1 d' z9 v
6 type offset target
3 q' R% M6 i" b( p. J# q BASE 060a seg 2 offset 0000
1 e, h" M- j- a$ X6 q2 k PTR 047e imp GDI.GETCHARABCWIDTHS
- @4 Y" J4 \* F0 X PTR 059b imp GDI.ENUMFONTFAMILIES
2 ~ M0 f1 Y# Y; w2 k4 x) F8 c PTR 0451 imp DISPLAY.14 ( EXTTEXTOUT )
% c3 ~/ W V5 ~4 S1 E. F3 X PTR 0415 imp KEYBOARD.4 ( TOASCII ) + z+ E, z, u0 d5 g7 ]
PTR 04ba imp KEYBOARD.5 ( ANSITOOEM ) / c( ?! B; R( L w
PTR 04c9 imp KEYBOARD.6 ( OEMTOANSI )
0 r! z! }/ {: j+ u. h PTR 04d8 imp KEYBOARD.134( ANSITOOEMBUFF )
' J; r6 O% A7 ? PTR 05f5 imp USER.430 ( LSTRCMP ) " L# C+ A. ]8 h0 j% z, B5 B
PTR 04e7 imp KEYBOARD.135( OEMTOANSIBUFF ) $ a: U6 t0 F" A5 ^0 P, K3 |
PTR 0514 imp USER.431 ( ANSIUPPER )
* R0 z( E6 g$ m; j PTR 0523 imp USER.432 ( ANSILOWER )
. W7 H" d# L+ r/ O PTR 05aa imp GDI.56 ( CREATEFONT ) E r, e' H% x+ Y
PTR 056e imp USER.433 ( ISCHARALPHA )
% f- V* F: k, N, z PTR 05b9 imp GDI.57 ( CREATEFONTINDIRECT ) , N& C3 q8 k9 A: ]3 n, V
PTR 057d imp USER.434 ( ISCHARALPHANUMERIC ) * l1 t6 I% K- Y) N0 I
PTR 049c imp USER.179 ( GETSYSTEMMETRICS ) , ~( ?# z1 {0 p6 P
PTR 0550 imp USER.435 ( ISCHARUPPER ) - m1 m3 K* d" {8 d3 G
PTR 055f imp USER.436 ( ISCHARLOWER )
0 S" y0 d% d9 c) o! [( |3 M0 R PTR 0532 imp USER.437 ( ANSIUPPERBUFF )
' }8 _7 ]0 d: }! x7 @+ T% b% j PTR 0541 imp USER.438 ( ANSILOWERBUFF ) / q6 N. u$ K$ @( K
PTR 05c8 imp GDI.69 ( DELETEOBJECT ) 2 s' r9 T3 h9 r1 k' S/ [8 S
PTR 058c imp GDI.70 ( ENUMFONTS ) 4 q C6 V% J7 w; m6 L5 Q5 V/ \
PTR 04ab imp KERNEL.ISDBCSLEADBYTE
3 V) w! K2 G, J/ C+ f PTR 05d7 imp GDI.82 ( GETOBJECT ) " W, `+ g- k7 Y, r1 b* |; k! x
PTR 048d imp KERNEL.74 ( OPENFILE )
+ G# q W0 ?( x+ ~1 D! ~( p. a PTR 0460 imp GDI.91 ( GETTEXTEXTENT )
. a5 Z* q) u ~: ` PTR 05e6 imp GDI.92 ( GETTEXTFACE ) 3 l9 t6 f3 K+ ~8 `- I
PTR 046f imp GDI.350 ( GETCHARWIDTH ) 3 J; I. P4 l0 e" n+ J
PTR 0442 imp GDI.351 ( EXTTEXTOUT )
0 q4 u% u* S- Z e0 c PTR 0604 imp USER.471 ( LSTRCMPI )
) K) x' t4 u& X# R t. _% @5 X% f& Z PTR 04f6 imp USER.472 ( ANSINEXT ) 2 K# D2 G0 G. _& b% \& H- w5 u
PTR 0505 imp USER.473 ( ANSIPREV )
: X3 m& B! ]$ n4 t. G# y, P 7 t# k$ O, H! X1 k( g3 `; I9 s: z
PTR 0424 imp USER.108 ( GETMESSAGE ) 3 v, a$ G. i$ D; A) O; a
PTR 0433 imp USER.109 ( PEEKMESSAGE )
8 ~% z- R. h) J1 L8 `, v4 k35 relocations
" Q+ m2 q/ r1 x# K9 s4 I(括号内为笔者加上的对应Windows API函数。)
/ m0 K4 a1 C, l9 a+ e% ^, W第一,在数据段中,发现了重定位信息。 : w1 i( ^/ t+ i
第二,这些重定位信息提示的函数,全都与文字显示输出和键盘、字符串有关。也就是说汉化Windows,必须修改这些函数。 4 r% P `4 M- r u& U
在这非常特殊的地方,隐藏着什么呢?毋庸置疑,这与众不同的两点,对打开"陷阱"技术之门而言,不是金钥匙,也是敲门砖。 * [/ d6 Q9 t$ V" J& e
二、Windows的模块调用机制与重定位概念 ( z2 I* W' x5 Q* g" `$ ?
为了深入探究"陷阱"技术,我们先来介绍Windows的模块调用机制。 7 |1 q7 |* u, s1 B' Y0 o1 T
Windows的运行分实模式、标准模式和增强模式三种,虽然这几种模式各不相同,但其核心模块的调用关系却是完全一致的,见图一。
. \1 q0 b' u' P9 E! J, j `! h3 m6 s主要的三个模块,有如下的关系:
' F7 k( D( W' e8 Y4 ?' w·KERNEL是Windows系统内核,它不依赖其它模块。
9 O3 p0 R' R4 o% t+ M: H·GDI是Windows图形设备接口模块,它依赖于KERNEL模块。 " M" M3 C7 d; c% m X" Z! j
·USER是Windows用户接口服务模块,它依赖于KERNEL、GDI模块及设备驱动程序等所有模块。 3 s- A# a* C- q
这三个模块,实际上就是Windows的三个动态链接库。KERNEL有三种系统存在形式:Kern el.exe(实模式)、Krnl286.exe(标准模式)、Krnl386.exe(386增强模式);GDI模块是Gdi.ex e;USER模块是User.exe。虽然文件名都以EXE为扩展名,但它们实际都是动态链接库。
# T- J Q5 e. l' x M4 b7 q, H4 G7 C<图片> : d6 b+ D2 t! I# {
图1 Windows的模块调用机制
' h- B, `0 |( j. K Y同时,几乎所有的API函数都隐藏在这三个模块中。用EXEHDR对这三个模块分析,就可列出一大堆大家所熟悉的Windows API函数。 f' |" c# U0 D3 y. X% I/ _3 t
以GDI模块为例,运行结果如下:
5 C/ j( e& E7 b0 U8 J( V$ RC:\WINDOWS\SYSTEM>exehdr gdi.exe % ?* m5 r% l! x* k4 v4 |0 z
Exports: 8 K% | u. G/ ~0 N& ?
rd seg offset name / }, H+ D# }8 z- ], n
............
) O3 D, @- m0 v& ?351 1 923e EXTTEXTOUT exported, shared data
, g8 F; r4 V2 @9 Q56 3 19e1 CREATEFONT exported, shared data
{( I) ^$ `2 ?............ ! d& _- X1 a- l( j' T" m* }
至此,读者已能从Windows纷繁复杂的系统中理出一些头续来。下面,再引入一个重要概念——重定位。
- l, z) H3 @9 F+ s' z+ B1 B, p" x一个Windows执行程序对调用API函数或对其它动态库的调用,在程序装入内存前,都是一些不能定位的动态链接;当程序调入内存时,这些远调用都需要重新定位,重新定位的依据就是重定位表。在Windows执行程序(包括动态库)的每个段后面,通常都跟有这样一个重定位表。重定位包含调用函数所在模块、函数序列号以及定位在模块中的位置。 ( l8 x7 e, g; c( ]7 ^& C
例如,用EXEHDR /v 分析CHINESE.DLL得到: ) }8 s2 G) p1 \& ~" h! t
6 type offset target 0 T! d! |) ]. V9 A$ X/ \! m& h' F* o
..........
7 B" `% e/ _' }1 R8 X7 W6 |PTR 0442 imp GDI.351
1 M; E1 ?+ o$ }2 d4 @7 ^ - K! K/ c. D' ]& o
..........
& s* E2 L) ^8 j$ p就表明,在本段的0442H偏移处,调用了GDI的第351号函数。如果在0442H处是0000:FFFF ,表示本段内仅此一处调用了GDI.351函数;否则,表明了本段内还有一处调用此函数,调用的位置就是0442H处所指向的内容,实际上重定位表只含有引用位置的链表的链头。那么,GDI. 351是一个什么函数呢?用EXEHDR对GDI.EXE作一分析,就可得出,在GDI的出口(Export)函数中,第351号是ExtTextOut。
( l" U3 n9 M$ M2 ^# E这样,我们在EXEHDR这一简单而非常有用的工具帮助下,已经在Windows的浩瀚海洋中畅游了一会,下面让我们继续深入下去。
7 g+ A D# y4 I. f: |/ T. z三、动态汉化Windows原理
' L. M3 |0 `' q7 s4 K- p我们知道,传统的汉化Windows的方法,是要直接修改Windows的显示、输入、打印等模块代码,或用DDK直接开发"中文设备"驱动模块。这样不仅工作量大,而且,系统的完备性很难保证,性能上也有很多限制(早期的长青窗口就是如此),所以只有从内核上修改Windows核心代码才是最彻底的办法。 7 G; j, L3 O" v% E) E7 d: @
从Windows的模块调用机制,我们可以看到,Windows实际上是由包括在KERNEL、GDI、US ER等几个模块中的众多函数支撑的。那么,修改其中涉及语言文字处理的函数,使之能适应中文需要,不就能达到汉化目的了吗? : u- |! G" \7 O0 P
因而,我们可以得出这样的结论:在自己的模块中重新编写涉及文字显示、输入的多个函数,然后,将Windows中对这些函数的引用,改向到自己的这些模块中来。修改哪些函数才能完成汉化,这需要深入分析Windows的内部结构,但CHINESE.DLL已明确无误地告诉了我们,在其数据段的重定位表中列出的引用函数,正是CStar修改了的Windows函数!为了验证这一思路, 我们利用RichWin作一核实。 - o; v) y @/ D# |) i6 U
用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的核心——"陷阱"技术。 * S& k( D' W S+ v6 k# y# c
四、"陷阱"技术 2 R: G' R2 d' ~
讨论"陷阱"技术,还要回到前面的两个发现。发现之二,已能解释为修改的Windows函数,而发现之一却仍是一个迷。
! ~6 D6 g) {+ m; [6 {数据段存放的是变量及常量等内容,如果这里面包含有重定位信息,那么,必定要在变量说明中将函数指针赋给一个FARPROC类型的变量,于是,在变量说明中写下:
# D# k8 G2 [/ D5 X/ ^* jFARPROC FarProcFunc=ExtTextOut; . X6 ^5 n8 J+ N! A' f
果然,在自己程序的数据段中也有了重定位信息。这样,当程序调入内存时,变量FarPro cFunc已是函数ExtTextOut的地址了。 5 d8 L0 A. \% Z' @4 ^
要直接修改代码段的内容,还遇到一个难题,就是代码段是不可改写的。这时,需要用到一个未公开的Windows函数AllocCStoDSAlias,取得与代码段有相同基址的可写数据段别名, 其函数声明为:
7 d5 v0 u: p: E8 b4 c9 KWORD FAR PASCAL AllocCStoDSAlias(WORD code_sel);
- @& ?) S8 H1 G3 S, j参数是代码段的句柄,返回值是可写数据段别名句柄。 - d# y9 I* A' A: i; r5 E$ U
Windows中函数地址是32位,高字节是其模块的内存句柄,低字节是函数在模块内的偏移。将得到的可写数据段别名句柄锁定,再将函数偏移处的5个字节保留下来,然后将其改为转向替代函数(用 EA Jmp): 8 [$ g: A8 s3 m
*(lpStr+wOffset) =0xEA; , U5 o) ~: \( m( K: ]
四通利方(RichWin)、中文之星(CStar)是大家广为熟知的汉化Windows产品,"陷阱"技术即动态修改Windows代码,一直是其对外宣称的过人技术。本文从Windows的模块调用机制与重定位概念着手,介绍了"陷阱"技术的实现,并给出了采用"陷阱"技术动态修改Windows代码的示例源程序。 ( x/ n7 ]* k& A: v$ d/ J9 G
' C3 |+ P" a& x0 f3 S+ V9 |/ d3 J3 s//源程序 relocate.c * |. U4 T% l G6 p6 J
#include <WINDOWS.H> 1 B' e- k" ?6 W% U$ R s
#include <dos.h>
8 D+ b3 n! Y& S! D) OBOOL WINAPI MyExtTextOut(HDC hDC, int x, int y, UINT nInt1, const RECTFAR*l, B& r: P) D& |, p; w2 t
pRect,LPCSTR lpStr, UINT nInt2, int FAR* lpInt);
/ D r$ e" n+ Z. F4 o2 {WORD FAR PASCAL AllocCStoDSAlias(WORD code_sel);
8 Z& V8 x d! P+ U8 T' j8 a; }typedef struct tagFUNC * S7 B7 y% D# j8 i1 z
{
- {, P( s4 O+ I! aFARPROC lpFarProcReplace; //替代函数地址 8 W; K! B; e7 a
FARPROC lpFarProcWindows; //Windows函数地址 * j( P& ~8 n* y Z1 }2 [3 f" _
BYTE bOld; //保存原函数第一字节 9 \0 B; {1 {7 X, P2 f
LONG lOld; //保存原函数接后的四字节长值
& C9 `) z1 \: y9 T* D3 Q0 i1 }}FUNC; % Z* N5 Z3 ]; X) y3 c& `* n
. ^! t9 o ?& M7 e) I7 r6 c5 Y6 X
FUNC Func={MyExtTextOut,ExtTextOut};
* a# I2 g$ S4 p$ n" w& j. J: n//Windows主函数
8 G s: |) w, Q+ L" ]! Dint PASCAL WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdL
" ?2 C. K' x+ L* |* }& cine,int nCmdShow){
& Q8 p# E7 w. t* g9 aHANDLE hMemCode; //代码段句柄
1 a; y. Z: `0 u; ? p" t- w4 Z4 _WORD hMemData; //相同基址的可写数据段别名
" @ V* `8 V/ B2 Q% `WORD wOffset; //函数偏移 - ]7 i9 X, e! r' L5 Y4 p
LPSTR lpStr;
- G( T5 ]8 Z \3 r; {- lLPLONG lpLong; 5 ~- [' `/ k' Y( Z1 e6 @
char lpNotice[96]; 2 f" O9 v( y9 a1 {
hMemCode=HIWORD((LONG) Func.lpFarProcWindows );
$ i: E/ t* c- o8 Q0 iwOffset=LOWORD((LONG) Func.lpFarProcWindows );
9 H& f Q. U! @4 [1 `# wwsprintf(lpNotice,"函数所在模块句柄 0x%4xH,偏移 0x%4xH",hMemCode,wOffset); 8 O( X& ^$ j P9 d, j- T% \! N
MessageBox(NULL,lpNotice,"提示",MB_OK);
& t& Q0 m3 i( {- b2 D/ I$ @ //取与代码段有相同基址的可写数据段别名 % k- `+ W$ \" k& z
hMemData=AllocCStoDSAlias(hMemCode);
) K* T7 z7 D, |( o lpStr=GlobalLock(hMemData);
8 s, W1 D$ h, y" e lpLong=(lpStr+wOffset+1 );
; D- `; {' ^1 ]% X+ W, t) N7 L //保存原函数要替换的头几个字节
t1 _: u) [: M/ E% Z! t0 vFunc.bOld=*(lpStr+wOffset);
0 L% `& y0 [1 e4 _4 z# M* ~Func.lOld=*lpLong;
0 h$ u' y. A; ^. n% b*(lpStr+wOffset)=0xEA; * S2 |5 w: _* q$ x! s1 e9 ]
*lpLong=Func.lpFarProcReplace;
( q' U) h1 e" G. c0 N) rGlobalUnlock(hMemData); \: }/ m6 C) S$ w
MessageBox(NULL,"改为自己的函数","提示",MB_OK);
( n J; f( }: r//将保留的内容改回来
$ z# W' Y& K3 B: qhMemData=AllocCStoDSAlias(hMemCode);
! n2 B2 T+ b6 ]7 ulpStr=GlobalLock(hMemData);
, a5 g2 x* n! @ J% p0 O+ N" OlpLong=(lpStr+wOffset+1 ); 3 ]! B* Q* T) ]
*(lpStr+wOffset)=Func.bOld;
( z( D% g1 u" A I*lpLong=Func.lOld;
2 j! A2 E0 W# pGlobalUnlock(hMemData); , E( r% v8 K* I' Z0 Q* @2 I9 k
MessageBox(NULL,"改回原Windows函数","提示",MB_OK);
. N% a2 n* P) v" t7 nreturn 1;
* A8 Y) d* I5 r/ N, a}
( v8 t$ m4 O7 e6 w& v$ n% Y5 I//自己的替代函数
( e+ [8 \3 Q* E3 zBOOL WINAPI MyExtTextOut(HDC hDC, int x, int y, UINT nInt1, const RECT FAR*
; }, p- k, S+ F' q/ h8 A4 } lpRect, LPCSTR lpStr, UINT nInt2, int FAR* lpInt){ 7 e7 s$ Q# N! X% s' A2 \$ A
BYTE NameDot[96]={ ; |! ]) N/ C# o( I2 I8 Z. |
0x09, 0x00, 0xfd, 0x08, 0x09, 0x08, 0x09, 0x10, 0x09, 0x20, / ~" Q: L- v& |3 U) x
0x79, 0x40, 0x41, 0x04, 0x47, 0xfe, 0x41, 0x40, 0x79, 0x40,
9 D" T. V# z# I! E% \5 w 0x09, 0x20, 0x09, 0x20, 0x09, 0x10, 0x09, 0x4e, 0x51, 0x84, 0 n1 \ s: R# T/ ?
0x21, 0x00, 0x02, 0x00, 0x01, 0x04, 0xff, 0xfe, 0x00, 0x00, / n. E8 X2 G/ D
0x1f, 0xf0, 0x10, 0x10, 0x10, 0x10, 0x1f, 0xf0, 0x00, 0x00, $ }8 l% F/ y" @1 U( w5 W2 _
0x7f, 0xfc, 0x40, 0x04, 0x4f, 0xe4, 0x48, 0x24, 0x48, 0x24, & U: ` i( Y* y7 j* \
0x4f, 0xe4, 0x40, 0x0c, 0x10, 0x80, 0x10, 0xfc, 0x10, 0x88, / H$ D- r) D$ v; i8 L
0x11, 0x50, 0x56, 0x20, 0x54, 0xd8, 0x57, 0x06, 0x54, 0x20, " C2 ~' C( n6 J
0x55, 0xfc, 0x54, 0x20, 0x55, 0xfc, 0x5c, 0x20, 0x67, 0xfe, " B! i* g/ p) D& t( P/ r" a
0x00, 0x20, 0x00, 0x20, 0x00, 0x20 % X) T( p& v. l
};
; b& r" h' X1 k( V, C- ?, VHBITMAP hBitmap,hOldBitmap;
4 M! {& ~0 u r! \ HDC hMemDC;
$ o! O, u% C E$ v/ Y- Y# s- p BYTE far *lpDot; W5 z" z D$ R4 W6 p1 m
int i; , i; h8 x8 D7 t- O0 U1 C
for ( i=0;i<3;i++ ) , g& N* y( W% J. y
{ 5 T; t' M6 m% L s& G
lpDot=(LPSTR)NameDot+i*32; 2 X0 Z" H4 g* G) W3 _2 c
hMemDC=CreateCompatibleDC(hDC);
* D" D z4 w0 k9 w: O. X e) lhBitmap=CreateBitmap(16,16,1,1,lpDot); 3 W' ~' i3 W6 F; F7 }2 W$ G, ~) u* R
SetBitmapBits(hBitmap,32L,lpDot); - k& j' X4 A* v3 E8 [ N' Z
hOldBitmap=SelectObject(hMemDC,hBitmap); ( P) e% S; Z' s% k
BitBlt(hDC,x+i*16,y,16,16,hMemDC,0,0,SRCCOPY);
; U4 s' c! B+ uDeleteDC(hMemDC); ; S5 }/ D$ r+ s/ d( z
DeleteObject(hBitmap); $ b& [" o0 ~) s; p8 a
}
7 R7 U9 g) u, l5 c# Mreturn TRUE;
& S# @' O9 b) x& z1 M; C @5 a} $ j% i+ n" {: ~* M1 r4 S+ @2 M0 y$ f
3 m( H: }: f1 I5 ?6 ~2 Z- P//模块定义文件 relocate.def 1 G8 D t# j; Z) h
NAME RELOCATE
% `3 h) S7 \' |8 F+ JEXETYPE WINDOWS
- v, ^$ _3 Y% \2 X9 TCODE PRELOAD MOVEABLE DISCARDABLE , H4 m9 o7 @1 I' q3 z8 q. N" r: T
DATA PRELOAD MOVEABLE MULTIPLE
4 y, f4 ~; b$ P$ ]) j, ]4 m K6 D+ NHEAPSIZE 1024 " ~) @, F+ P- O
EXPORTS , F6 ?2 m' V2 L) m" q: j0 ^
, D+ T. Y/ {* C
五、结束语 * Q4 f/ S% P$ V" o' r' [ G" {3 t" |
本文从原理上分析了称为"陷阱"技术的动态汉化Windows方法,介绍了将任一Windows函数调用改向到自己指定函数处的通用方法,这种方法可以拓展到其它应用中,如多语种显示、不同内码制式的切换显示等。
4 c! W* t' f/ V- W
8 m) G! r* c% T' h$ U |