·汉化新世纪 ·汉化新世纪论坛 ·百家争鸣 ·论坛集萃 ·汉化问答 ·软件介绍
文章首页 >> 汉化时事 >> 汉化动态 >> 野蛮人战记    Creative Commons License,创作共用协议(中文版)  署名 非商业性使用 禁止演绎

野蛮人战记

作者: 梁利峰 来源:点睛工作室 时间:2003-08-02 点击:7849


野蛮人战记


声明

个人可以自由转载本文,不过应保持原文的完整性,并通知我;商业转载先请和我联系。

本文没有任何明确或不明确地提示说本文完全正确,阅读和使用本文的内容是您自己的选择,本人不负任何责任。

如果您发现本文有错漏的地方,请您给我指出;如果有什么不理解的,请您给我提出。

意见、建议和提出的问题最好写在我的主页 http://llf.126.com 的留言版上。

乱码的问题

汉化软件时偶尔会出现乱码的问题,不过其中有一些可以通过修改资源来解决,我在这里不讨论这种乱码,而主要介绍通过 CreateFont 类函数时出现的乱码的解决方法,所以以下讨论都是专指 CreateFont 类函数的。

虽然说是 CreateFont “类”函数,其实只有两个,一个 CreateFont ,一个 CreateFontIndirect 。

一般的,我认为,只要出现乱码,一定和语系有关(游戏汉化中的乱码也可以认为是间接和语系有关,不过这里不讨论此问题),也就是 CreateFont 类函数中的那个 CharSet 的参数。只要把它设置为简体中文的 0x86 就可以了。

关于 CreateFont 类函数的介绍,可以看我写的《C 程序字号的修改》,我在这里是给出一种野蛮的办法完成修改字体字号及语系,所谓野蛮者,是有副作用的,但是同时也有好处,比如可以不太关心其中的汇编代码。

虽然说不太关心,但是并不是说可以完全不了解。所以我们需要先对汇编有一些了解。

汇编起步

要用本文介绍的方法修改字体字号,汇编语言必须有所了解,而不能乱改。不过我在这里并不打算长篇累牍的介绍汇编,这些知识应该是大家不断接触而熟练的。在这里,我主要介绍一下我学习汇编的方法。

对于汇编语言,我并不熟悉,也不精通,当然,我并没有打算精通它 —— 我只是在需要使用时才查找它的用法,而且一般是在反编译出来的文件中查找,因为我手里也没有关于汇编语言的书。 :)

先介绍一些基本知识。

Inter 的 CPU 之中,8086 就已经是完全的十六位的 CPU 了,而 80386 开始的 CPU 都是三十二位的 CPU ,直至现在都是如此,不过这种三十二位的 CPU 是同时支持十六位和三十二位的,它执行那种指令取决于运行于实模式还是运行与保护模式:实模式下执行十六位指令,保护模式下执行三十二位指令。

CPU 都有寄存器,而对于 Inter 系列的 CPU ,寄存器一般像 ax、bx、eax、ebx 之类,类似 ax、bx 这样的寄存器是十六位的寄存器,大小就是两个字节,所以又分为 al、ah、bl、bh ,al 为 ax 的低字节,而 ah 为 ax 的高字节,类似的,bl、bh 也是 bx 的高低字节。而 eax、ebx 之类的寄存器是三十二位的寄存器,而 ax 就是 eax 的低十六位,bx 是 ebx 的低十六位。

另外,我简单介绍几个指令:mov、lea、push、pop、call 。

mov 指令就是 move 的意思,不过并非真的移动,可以把它看作复制命令,比如 mov eax, dword ptr [004081C0] 的意思就是:把地址 004081c0 处的 dword(word=2个byte;dword=2个word,也就是4个字节) 类型的指针复制到寄存器 eax 里。而 mov eax, 55555556 的意思就是:把数值 55555556 复制到寄存器 eax 里。

lea 指令是计算地址用的,而且它有一个好处,在特定的条件下执行不耗费 CPU 的时间,它的用法有些类似 mov。比如 lea eax, dword ptr [esi+4*esi] 的意思就是把地址 esi+4*esi 里的 dword 类型的指针放入寄存器 eax 里。我们见到在这个例子中,地址是计算出来的,这是 mov 指令不支持的,而且其中有乘法运算,而且用可以不耗费 CPU 时间,所以很多程序员用它来代替部分的乘法运算,虽然计算的数值有限制,效果也同样非常好。

push 和 pop 是操作堆栈的指令。堆栈有些像堆盘子,后来居上的,我们如果“堆上一个黄盘子,再堆上一个红盘子”,这时如果要取出的话,就要先取出红盘子,而后才能取出黄盘子。堆栈一般用来传递参数,关于这一点,我在《C 字号的修改》中已经介绍的比较清楚,这一次就不说了。那么 push 和 pop 是否必须成对出现?会不会出现 push 的数据在函数调用后仍然起作用呢?我必须承认,push 和 pop 不一定成对出现,特别是在一些 Delphi 或 C++ builder 的程序中,不过它的原因是可以用其它指令完成 pop 或 push 的功能,比如 mov eax,[esp];sub esp,00000004; 就可以完成 pop eax 的功能,那么为什么会出现这种情况呢?因为速度。某些情况下,这样的代码比直接使用 pop 指令的速度更快,虽然我个人认为其加速并不会很多,不过编译器能进行这样的优化还是很让人满意的。这也就是说,push 和 pop 必须成对出现,不过可以使用间接的方法。所以不会出现 push 的数据在函数调用后仍然起作用的现象。

call 指令。顾名思义,就是呼叫函数的指令。因为需要参数,所以一般的 call 指令之前都有 push 指令,特别是调用 API 的使用,都是遵循这种规律的,不过在 Delphi 或 C++ Builder 的程序里调用自己内部的函数时,偶尔也会出现不使用 push ,而使用 mov 指令的例子,原因也同样也是速度,不过这种情况不会出现在调用 API 函数上。另外,call 呼叫的函数会把它的返回值放在寄存器 eax 中。

设身处地

对于修改汇编代码,Ronnier 推荐使用 HIEW ,不过说实在的,我对这种控制台程序很反感,各位当然仍然可以使用它,不过我要简单介绍一下我一向的做法,即使你不是用这样的方法,知道这些方法仍然是有益的。

拿 push 指令为例,它有两种常见的格式,一种是压入四字节指令的 push ,一种是压入单字节的 push ,为什么需要两种呢?还是因为速度,而且两种 push 指令的字节代码不同,四字节的 push 的字节代码是 68 ,单字节的字节代码是 6a ,也就是说 push 00400300 的字节代码是 6800034000 ,而 push 35 的字节代码是 6a35 ,为什么不都使用 68 呢?想象自己是 CPU ,当读到一个 68 时,我们把 68 后面的四个字节压入堆栈,而把这四个字节后的字节作为下一条指令的开始;如果读到一个 6a ,就把 6a 后面的一个字节压入堆栈,而把这一个字节后的字节作为下一条指令的开始。如果都使用 68 的话,就没有办法判断究竟该压入几个字节的数据,也没有办法判断下一条指令的开始了。

我以前说过,使用 6a 压入的仍然是四字节的数据,不过 CPU 自己添加了三个字节,如果压入的数据大于 7f ,那三个字节就是 ffffff ,否则就是 000000 ,利用它,可以缩短代码的长度,以便为自己在程序中添加代码制造条件,我在破解 WinIMP 的自校验时就使用了这种方法。比如 mov eax, fffffff8 的字节代码是 B8f8ffffff ,共 5 个字节,而 push f8; pop eax 完成相同的任务,字节代码是 6af858 ,只有三个字节,节省了两个字节。

另外,我说过,我并不精通汇编,对于它的字节代码就更不可能精通了,比如说,如果我想知道 pop ebx 的字节代码,我会在反编译出来的文件里查找,然后就会知道 pop ebx 的字节代码是 5b 。

我们再来看一条跳转语句:

	:004014E7 7D0B                    jge 004014F4

指令是 jge 004014f4 ,而字节代码只有 7d0b 两个字节,为什么呢?因为这里使用的是相对偏移,7d 固然是 jge ,而究竟跳转到哪里,就需要计算了,004014e7 + 0b + 02 = 004014f4 ,最后的 02 是本语句自己占用的两个字节。而如果后一个字节大于 7f ,就是进行反方向的跳转,看下一条语句:

	:00401949 74BE                    je 00401909

地址的计算方法如下:00401949 + be + 2 - 100 = 00401909 。同样的,我们可以把自己想象成 CPU ,它并不比我们聪明,只是它处理的信息比较少,也比较死板,绝对不会凭空想象一个地址出来,各个指令都是可以计算得到的,而这样的公式一般都是加加减减的,想必难不住大家的。再看一条语句:

	:004012A2 E859FDFFFF              call 00401000

我在《VB与UniCode补遗》中说过,Inter 的 CPU 因为高位在后,所以我们的数据比如 00400300 在字节代码中就是 00034000 ,在这里,字节代码是 e859fdffff ,e8 就是 call 了,不过后面的 59fdffff 和 00401000 之间并不是直接进行字节翻转就可以的,所以也是相对偏移,59fdffff 就是 fffffd59,计算方法相同,也是 004012A2 + fffffd59 + 5 - 100000000 = 00401000 。(注意,Windows 的计算器自动把 fffffd59 当作负数计算,所以不需要减去最后的 100000000)

CreateFont

关于 CreateFont 的野蛮战法,我在《汉化中的几个问题》已经介绍过了,就不再复述了,在这里澄清一个问题。

我以前使用“6a86”的方法压入语系的参数,后来发现应该使用“6886000000”,不过有人提出使用“6a86”也是正常的,对于这个问题,我仔细考虑了一下,发现确实两种方法都是可行的 —— 至少现在是可行的。

在 CreateFont 的参数中,语系是这样的:“BYTE lfCharSet”。也就是说,语系是一个单字节参数,在 Win32 环境中,压入堆栈的只能是四字节参数,所以“6a86”压入的是“FFFFFF86”,而“6886000000”压入的是“00000086”,但是对于这种单字节参数,系统的处理方法是简单的舍弃前三个字节,所剩的就是“86”了。

什么时候“6a86”会无效呢?大概需要等到语系的总数超过 FF 个吧。

如果字节数还不够,那么可以使用寄存器,比如 push eax 的字节代码只有一个字节 50 ,如果我们先把 eax 赋值成 0 ,那么以后的 push 00 都可以改成 push eax ,则修改后的代码的长度可以进一步缩短。

CreateFontIndirect

对于 CreateFontIndirect ,一个很讨厌的问题是它只有一个参数,而这个参数是指向 logFONT 结构的指针,我们通过分析汇编代码很难知道程序是在什么时候修改的这个结构,更何况让汉化人分析汇编代码本身就有一定的难度。

首先,在查找到的 Call CreateFontIndirect 上面找到那一个 push 语句,祈祷从 push 到 Call 之间有超过 5 个字节可供我们使用,然后,找一处空白的地方,也就是有很多“00”的地方,把其修改成“f4ffffff000000000000000000000000900100000000008600000000cbcecce5”,这就是表示“宋体,12 pixel”的 logFONT 结构,然后用 PosConv 把 f4 处的偏移量转换成代码,比如代码是“35314000”,则把原 push 语句改成“6835314000”,如果到 Call 语句之间还有多余的字节,都用“90”填充。

我想,大家可以体会到用这种方法的野蛮之处:我们另造了一个 logFONT 结构,但是这一个 CreateFontIndirect 语句并不一定只被调用了一次,如果调用此函数原先希望创建不同大小的字体,现在会发现,创建出的字体统统都是“宋体,12 pixel”了。

还有一个问题,就是找一处有很多“00”的地方并不容易,有很多地方虽然有这些“00”,但是程序运行起来以后,通常这些地方都被修改成其它的内容了,这时我的做法是在文件头中找一块地方,通常是文件偏移的 0x300 处,不过需要注意,一定要和上面有内容的地方留 4~5 行“00”,而不要直接和上面相接,如果 0x300 和上面的区段信息部分太接近,可以再向下几行。

另外一个问题是 DLL 文件,这些文件不一定被加载到哪一部分空间,所以使用这种方法的话,有时候不能在本身写入 logFONT 结构的内容,而应该在调用它的可执行文件的头部加入此信息,然后,在 DLL 中的 Call CreateFontIndirect 之前修改 Push 语句,比如修改了“NDD32.EXE”的 0x300 处成为 logFONT 结构,然后在其中的 DLL 中修改 push 语句,就应该是“6800034000”。不过需要注意,这种方法只适用于那种专用 DLL ,如果是很多程序都要调用的 DLL ,要么每一个调用者都加入 logFONT 结构,要么使用其它的方法修改。

我的这种修改方法只能对于 push 语句已经有 5 个字节的情况,对于这种限制,Ronnier 提出一种新的方法,就是把 push 语句和 call 语句都改成 call 自己的一段函数,而自己的函数如下:

	push 00400300
	call CreateFontIndirectA
	ret

虽然 Ronnier 说他是把这一段代码加在代码段后面的,不过我觉得把它加在文件头中更合适,所幸代码不长,正可以加在 logFONT 结构之后。:)

这样的修改,只要 push 和 call 语句加起来有 5 个字节就可以了,实在是一大突破,真的应该向 Ronnier 致意!

例外

不过这样的修改有时候会遇上类似下面的代码:

:00401FC2 56                      push esi

* Reference To: GDI32.CreateFontIndirectA, Ord:0053h
                                  |
:00401FC3 8B3D08704000            mov edi, dword ptr [00407008]
:00401FC9 FFD7                    call edi

这样,因为 call 语句呼叫的是上一句 mov 指令放入 edi 的指令,所以这一句 mov 指令也同样是必不可少的,是不能盲目消除的,当然,这样的程序可能有各种变化,我不可能在这里尽列,所以需要修改者自行进行这一方面的的判断。其实这样的问题在 CreateFontA 函数中出现的可能性更大,所以在修改 CreateFontA 函数更要小心。

另外,需要注意,如下的代码:

* Reference To: GDI32.CreateFontIndirectA, Ord:0053h
                                  |
:00401FC2 8B3D08704000            mov edi, dword ptr [00407008]
:00401FC8 56                      push esi
:00401FC9 FFD7                    call edi

虽然符合我的这种修改的方法所说的顺序,不过 push 和 call 语句加起来只有 2 个字节,所以还是需要认识到可以把这一句 mov 指令一起移入我们自己的函数才能修改的。

大字体

ChinEase 提出这样的修改只能在小字体正常,如果改为大字体正常,则于小字体下又不正常,希望能有两全其美的办法,不过看来是很难的了。

关于这个问题,我也一直是知道的,不过不容易解决,可以分情况讨论。

1 如果原程序是可以自动变换的,则应该查看其反汇编代码,找到其中做变换的部分,修改那里,则修改后的程序也同样有自动变换的能力。比如 Delphi 程序的 MessageBox 和 InputBox 以及 Hint 部分都可以这样修改,而且对于 Delphi 程序来说,一般会有一种固定的模式以便查找,比较方便,即使不很了解汇编的人员也可以自行修改,不过对于 C 程序来说,可能只有懂汇编的人才能自行修改了。一般的,原程序是可以自动变换的,不建议使用野蛮人的战法。 :)

2 如果原程序不能自动变换,也同样可以制作大小字体的不同版本以解决这个问题,不过如果有一个程序,能计算当前字体模式下应该有的字体大小,写入可执行文件,也同样可以达到目的,这样的程序可以在安装的时候执行一次,在用户改变字体设置时再执行一次即可。这样做比制作大小字体的不同版本有一个好处在于用户不只可以设置大字体或小字体,而且可以自定义字体大小。

对于不懂汇编的同志们,第二种方式非常不错,而且对于第一类程序也同样有效。

点睛工作室·梁利锋 结稿于 2000.12.8

汉化新世纪 责任编辑: 乾 .:|:. 标签(Tag): 字体 汇编

·上一篇: 解除 WinImp 的自校验 ·下一篇: Delphi 字号修改之二

· 版权申明: 本文引自《点睛工作室》,如有版权疑问请及时联系本站,以便本站处理。

· 转载申明: 本文引自《点睛工作室》[ 作者: 梁利峰],如需转载请直接联系原始作者,并请注明原始出处。

相关文章                                                                                发表评论 打印此文 关闭窗口

| 设为首页 | 加入收藏 | 联系我们 | 友情链接
Creative Commons License,创作共用协议(中文版)  署名 非商业性使用 禁止演绎
本站内容,除转载或版权特别申明的内容外,皆遵守 创造共用协议中文版之“署名-非商业性使用-禁止演绎 2.5 中国大陆”条款
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 2.5 China License.
本网站内容源自汉化新世纪论坛的摘录和汉化新世纪成员的原创文章。
凡汉化新世纪论坛的文字皆默认为汉化新世纪与原作者共同拥有并授权发布。
如对本站发布文章有所异议请来信告知,我们将及时删除。
凡商业摘录本站文字请先与我们联系,本站将保留非授权商业发布的追究权利。
凡非商业摘录本站文字请明显注明出处和原作者,并不得改动,凡改动必先征求原作者同意。
苏ICP备05002283号