【文章标题】: 苍之涛XP下花屏分析及补丁制作!
【文章作者】: 晓欣
【作者邮箱】: qwerty789@tom.com
【软件名称】: 苍之涛
【下载地址】: 自己搜索下载
【使用工具】: OD
【软件介绍】: 苍之涛,超经典游戏,不知道的该打PP!
--------------------------------------------------------------------------------
【详细过程】
  近期想重温苍之涛,结果发现进去之后菜单的字都花屏,小地图也是花了。到网上搜了一通,发现都是说显卡驱动太新了造成的。无奈只有发挥看雪PEDIY的精神,把这个东西拿起来DIY一翻了。
  
  说明:游戏版本是1.04宇之大众版,这个版本去掉了STARFORCE加密,不然也没有这篇文章了。虽然有menting大牛的STARFORCE分析文章,无奈我等小菜根本不知所云,只好捡软柿子来捏一捏了。
  
  闲话少说,开始正题。OD载入SWDMD_GB.EXE,F9运行之后,成了这个样子:

  字体有问题当然就找字体函数了,很快找到TextOutA函数,先下断再说,F9运行之后断在这里,容易看出这个函数是把一个字符显示在临时的Surface上的:
  0040C381      8B3D 74D04C00    MOV     EDI,DWORD PTR DS:[<&GDI32.TextOutA>]  ; GDI32.TextOutA
  0040C387      FFD7             CALL    EDI                                   ; 显示空格
  0040C389      FF75 14          PUSH    DWORD PTR SS:[EBP+14]
  0040C38C      E8 6F5C0B00      CALL    SWDMD_GB.004C2000
  0040C391      59               POP     ECX
  0040C392      50               PUSH    EAX
  0040C393      FF75 14          PUSH    DWORD PTR SS:[EBP+14]
  0040C396      6A 00            PUSH    0
  0040C398      6A 00            PUSH    0
  0040C39A      FF75 FC          PUSH    DWORD PTR SS:[EBP-4]
  0040C39D      FFD7             CALL    EDI                                   ; 显示当前字符
  返回之后会来到:
  0040D84B           57               PUSH    EDI
  0040D84C           57               PUSH    EDI
  0040D84D           FFB6 78120000    PUSH    DWORD PTR DS:[ESI+1278]
  0040D853           E8 9FEAFFFF      CALL    <SWDMD_GB.Print_Single_Char>          ; 显示一个字符
  0040D858           8BCE             MOV     ECX,ESI
  0040D85A           E8 64FFFFFF      CALL    SWDMD_GB.0040D7C3
  0040D85F           39BE 80120000    CMP     DWORD PTR DS:[ESI+1280],EDI
  0040D865           897D 08          MOV     DWORD PTR SS:[EBP+8],EDI
  0040D868           7E 71            JLE     SHORT SWDMD_GB.0040D8DB
  ....
  0040D879           8365 FC 00       AND     DWORD PTR SS:[EBP-4],0
  0040D87D           8BF9             MOV     EDI,ECX
  0040D87F           BA 80000000      MOV     EDX,80
  0040D884           C1E7 03          SHL     EDI,3
  0040D887           8B86 7C120000    MOV     EAX,DWORD PTR DS:[ESI+127C]
  0040D88D           3BF8             CMP     EDI,EAX
  0040D88F           7D 32            JGE     SHORT SWDMD_GB.0040D8C3
  0040D891           0FAF45 08        IMUL    EAX,DWORD PTR SS:[EBP+8]         ; 取行的起始点
  0040D895           8B9E C4020000    MOV     EBX,DWORD PTR DS:[ESI+2C4]       ; 一行起始位置?
  0040D89B           03C7             ADD     EAX,EDI                          ; 加上已经显示过的点,得到应该显示的点?
  0040D89D           66:833C43 00     CMP     WORD PTR DS:[EBX+EAX*2],0        ; 这个点是0(黑色)就不显示
  0040D8A2           74 13            JE      SHORT SWDMD_GB.0040D8B7
  0040D8A4           8B86 C8020000    MOV     EAX,DWORD PTR DS:[ESI+2C8]       ; 一行的字节数
  0040D8AA           8B5D 0C          MOV     EBX,DWORD PTR SS:[EBP+C]         ; 点阵的起始地址
  0040D8AD           0FAF45 08        IMUL    EAX,DWORD PTR SS:[EBP+8]         ; 一行开始是第几个字节
  0040D8B1           03C1             ADD     EAX,ECX                          ; 得到当前点在第几个字节上
  0040D8B3           03C3             ADD     EAX,EBX                          ; 取得计算到的目标地址
  0040D8B5           0810             OR      BYTE PTR DS:[EAX],DL             ; 显示上去!
  0040D8B7           D1FA             SAR     EDX,1
  0040D8B9           FF45 FC          INC     DWORD PTR SS:[EBP-4]             ; 列加1
  0040D8BC           47               INC     EDI                              ; 一行中已经显示过的点
  0040D8BD           837D FC 08       CMP     DWORD PTR SS:[EBP-4],8           ; 一字节8个点(8位)
  0040D8C1         ^ 7C C4            JL      SHORT SWDMD_GB.0040D887
  0040D8C3           41               INC     ECX
  0040D8C4           3B8E C8020000    CMP     ECX,DWORD PTR DS:[ESI+2C8]       ; 一行是否显示完?
  0040D8CA         ^ 7C AD            JL      SHORT SWDMD_GB.0040D879
  0040D8CC           FF45 08          INC     DWORD PTR SS:[EBP+8]             ; 行加1
  0040D8CF           8B45 08          MOV     EAX,DWORD PTR SS:[EBP+8]
  0040D8D2           3B86 80120000    CMP     EAX,DWORD PTR DS:[ESI+1280]      ; 是否显示完所有行?
  0040D8D8         ^ 7C 93            JL      SHORT SWDMD_GB.0040D86D
  0040D8DA           5B               POP     EBX
  0040D8DB           5F               POP     EDI
  0040D8DC           5E               POP     ESI
  0040D8DD           C9               LEAVE
  0040D8DE           C2 0800          RETN    8
  
  上面这个函数是把前面显示在临时表面上的字符逐点取出来,生成一个点阵汉字的字模,到后面要往整个图像上画的时候用。
  分析上面这个函数的时候觉得有点眼熟,拿出天之痕的分析结果一看,结果发现苍之涛里面整个字符显示的部分都和天之痕是一样的。前面分析天之痕的时候看过一遍,这次看起来就快多了。
  顺便打个广告,天之痕的贴子链接:http://bbs.pediy.com/showthread.php?t=98941
  
  下面继续,上面这个函数几次返回之后会来到:
  0040DC59        A8 10            TEST    AL,10                                ; 开头画面时AL=10
  0040DC5B        74 18            JE      SHORT SWDMD_GB.0040DC75
  0040DC5D        50               PUSH    EAX                                  ; 显示类型?
  0040DC5E        8D041F           LEA     EAX,DWORD PTR DS:[EDI+EBX]
  0040DC61        FF75 18          PUSH    DWORD PTR SS:[EBP+18]                ; 颜色
  0040DC64        8BCE             MOV     ECX,ESI
  0040DC66        FF75 0C          PUSH    DWORD PTR SS:[EBP+C]                 ; 共需要显示的字符数??
  0040DC69        FF75 10          PUSH    DWORD PTR SS:[EBP+10]                ; Y坐标
  0040DC6C        50               PUSH    EAX                                  ; X坐标
  0040DC6D        FF75 08          PUSH    DWORD PTR SS:[EBP+8]                 ; Buffer
  0040DC70        E8 5EF7FFFF      CALL    <SWDMD_GB.Show_Char_Title>           ; 标题画面显示字符?
  0040DC75        8B86 8C120000    MOV     EAX,DWORD PTR DS:[ESI+128C]          
  0040DC7B        8D8E 70120000    LEA     ECX,DWORD PTR DS:[ESI+1270]
  0040DC81        2B45 F8          SUB     EAX,DWORD PTR SS:[EBP-8]
  
  再返回之后会来到:
  
  00470810        6A 10            PUSH    10                                   ; ??
  00470812        50               PUSH    EAX                                  ; 颜色
  00470813        57               PUSH    EDI                                  ; 要显示的字符串(BIG5码)
  00470814        0FB703           MOVZX   EAX,WORD PTR DS:[EBX]
  00470817        50               PUSH    EAX                                  ; Y坐标
  00470818        55               PUSH    EBP                                  ; X坐标
  00470819        FF35 F05AA800    PUSH    DWORD PTR DS:[<lpFontSurface_Buffer>>; Buffer
  0047081F        8BCE             MOV     ECX,ESI                              ; lplpDD2
  00470821        E8 D6D1F9FF      CALL    <SWDMD_GB.Show_Char_On_Buffer_2>     ; 显示标题画面的汉字
  00470826        6A 00            PUSH    0
  
  再往下面返回就回到主循环了,没有看出什么问题,也没有可以利用的东西。
  再换个办法,进去游戏之后发现对话都是OK的,所以来看看对话的显示是怎么处理的。显示单个字符都是一样的,几次返回之后来到:
  0040DC1D        A8 04            TEST    AL,4                                 ; 对话时AL=4
  0040DC1F        74 1A            JE      SHORT SWDMD_GB.0040DC3B
  0040DC21        50               PUSH    EAX
  0040DC22        8D041F           LEA     EAX,DWORD PTR DS:[EDI+EBX]           ; ??
  0040DC25        FF75 18          PUSH    DWORD PTR SS:[EBP+18]                ; ??
  0040DC28        8BCE             MOV     ECX,ESI
  0040DC2A        FF75 0C          PUSH    DWORD PTR SS:[EBP+C]                 ; 共需要显示的字符数
  0040DC2D        FF75 10          PUSH    DWORD PTR SS:[EBP+10]                ; Y坐标
  0040DC30        50               PUSH    EAX                                  ; X坐标
  0040DC31        FF75 08          PUSH    DWORD PTR SS:[EBP+8]                 ; Buffer/宽/高/512
  0040DC34        E8 51F1FFFF      CALL    <SWDMD_GB.Show_Char_Talk>            ; 显示一个对话字符?
  0040DC39        EB 3A            JMP     SHORT SWDMD_GB.0040DC75
  
  这里再往下翻一点点就看到刚才标题画面显示字符的函数了!说明标题画面和对话是调用的同一个函数来显示的!
  这个函数返回之后来到:
  00464406         B9 7003A600      MOV     ECX,OFFSET <SWDMD_GB.lplpDD>
  0046440B         E8 2500FBFF      CALL    SWDMD_GB.00414435
  00464410         6A 04            PUSH    4
  00464412         8D4D FC          LEA     ECX,DWORD PTR SS:[EBP-4]
  00464415         FF75 DC          PUSH    DWORD PTR SS:[EBP-24]
  00464418         A3 F05AA800      MOV     DWORD PTR DS:[<lpFontSurface_Buffer>],>
  0046441D         51               PUSH    ECX
  0046441E         B9 8892A700      MOV     ECX,OFFSET <SWDMD_GB.lplpDD2>
  00464423         FF75 EC          PUSH    DWORD PTR SS:[EBP-14]
  00464426         57               PUSH    EDI
  00464427         50               PUSH    EAX
  00464428         E8 CF95FAFF      CALL    <SWDMD_GB.Show_Char_On_Buffer_2>              ;这个函数调用的方式和上面标题画面是一样的,说明显示字符串是同一个函数!
  0046442D         FF35 F05AA800    PUSH    DWORD PTR DS:[<lpFontSurface_Buffer>]
  00464433         B9 7003A600      MOV     ECX,OFFSET <SWDMD_GB.lplpDD>
  00464438         E8 2500FBFF      CALL    SWDMD_GB.00414462                             ;这里跟进去
  0046443D         DB45 F4          FILD    DWORD PTR SS:[EBP-C]
  00464440         D85D F8          FCOMP   DWORD PTR SS:[EBP-8]
  
  00464438这里跟进去之后,发现这是解锁表面的函数:
  00414462 <SWDM>  FF7424 04        PUSH    DWORD PTR SS:[ESP+4]
  00414466         8B09             MOV     ECX,DWORD PTR DS:[ECX]
  00414468         68 12270000      PUSH    2712
  0041446D         E8 86BB0000      CALL    <SWDMD_GB.GetSurface_Ptr>              ; 获取一个表面的指针
  00414472         50               PUSH    EAX
  00414473         E8 68BF0000      CALL    <SWDMD_GB.DDraw_UnlockSurface>         ; 这里跟进去之后发现是Unlock()函数,解锁表面
  00414478         59               POP     ECX
  00414479         59               POP     ECX
  0041447A         C2 0400          RETN    4
  
  再从0046440B这里跟进去,发现这是锁定表面的函数:
  00414435 <SWDM>  8B09             MOV     ECX,DWORD PTR DS:[ECX]
  00414437         56               PUSH    ESI
  00414438         68 12270000      PUSH    2712
  0041443D         E8 B6BB0000      CALL    <SWDMD_GB.GetSurface_Ptr>              ; 获取表面指针
  00414442         50               PUSH    EAX
  00414443         E8 51BF0000      CALL    <SWDMD_GB.DDraw_LockSurface>           ; 锁定表面
  00414448         8BF0             MOV     ESI,EAX
  0041444A         33C0             XOR     EAX,EAX
  0041444C         50               PUSH    EAX
  0041444D         50               PUSH    EAX
  0041444E         50               PUSH    EAX
  0041444F         50               PUSH    EAX
  00414450         8935 0C058A00    MOV     DWORD PTR DS:[<lpBack_Buffer>],ESI
  00414456         E8 F4BC0000      CALL    SWDMD_GB.0042014F
  0041445B         83C4 14          ADD     ESP,14
  0041445E         8BC6             MOV     EAX,ESI
  00414460         5E               POP     ESI
  00414461         C3               RETN
  
  到这里就比较清楚了,标题画面和设定画面的字符花是由于显示字符之前没有锁定表面。对话时不花是锁定表面之后再进行操作的!所以在
  所以直接在0040D9FC这个显示字符的函数上面打个补丁好了,先锁定表面取表面的指针,如果表面已经锁定DDraw的Lock()函数会返回非0值。
  把40D9FC的入口改成:
  00EC6600          60               PUSHAD
  00EC6601          B9 7003A600      MOV     ECX,OFFSET <SWDMD_GB.lplpDD>
  00EC6606          E8 F5FEFFFF      CALL    <SWDMD_GB.Patch_GetFontBuffer>         ; 补丁函数
  00EC660B          85C0             TEST    EAX,EAX                                ; 成功返回表面的数据Buffer
  00EC660D          74 11            JE      SHORT SWDMD_GB.00EC6620
  00EC660F          A3 F05AA800      MOV     DWORD PTR DS:[<lpFontSurface_Buffer>],>
  00EC6614          C705 38000B01 01>MOV     DWORD PTR DS:[<SurfaceLocked>],1       ; Patch锁定表面
  00EC661E          EB 0A            JMP     SHORT SWDMD_GB.00EC662A
  00EC6620          C705 38000B01 00>MOV     DWORD PTR DS:[<SurfaceLocked>],0       ; 表面本身已经锁定,
  00EC662A          61               POPAD
  00EC662B          55               PUSH    EBP
  00EC662C          8BEC             MOV     EBP,ESP
  00EC662E          83EC 0C          SUB     ESP,0C
  00EC6631          56               PUSH    ESI
  00EC6632        - E9 CC7354FF      JMP     SWDMD_GB.0040DA03
  
  Patch_GetFontBuffer的内容:
  00EC6500 <SWDMD>  8B09             MOV     ECX,DWORD PTR DS:[ECX]                 ; Patch_GetFontBuffer
  00EC6502          56               PUSH    ESI
  00EC6503          68 12270000      PUSH    2712
  00EC6508          E8 EB9A55FF      CALL    <SWDMD_GB.GetSurface_Ptr>              ; 获取表面指针
  00EC650D          50               PUSH    EAX
  00EC650E          E8 869E55FF      CALL    <SWDMD_GB.DDraw_LockSurface>           ; 取得表面的Buffer,如果表面已经锁定则返回0
  00EC6513          85C0             TEST    EAX,EAX                                ; 这个函数是程序里面本来就有的
  00EC6515          75 0F            JNZ     SHORT SWDMD_GB.00EC6526
  00EC6517          C705 38000B01 00>MOV     DWORD PTR DS:[<SurfaceLocked>],0       ; 临时变量,是否由我们自己来锁定表面
  00EC6521          83C4 04          ADD     ESP,4
  00EC6524          5E               POP     ESI
  00EC6525          C3               RETN
  00EC6526          8BF0             MOV     ESI,EAX
  00EC6528          33C0             XOR     EAX,EAX
  00EC652A          50               PUSH    EAX
  00EC652B          50               PUSH    EAX
  00EC652C          50               PUSH    EAX
  00EC652D          50               PUSH    EAX
  00EC652E          8935 0C058A00    MOV     DWORD PTR DS:[<lpBack_Buffer>],ESI
  00EC6534          E8 169C55FF      CALL    SWDMD_GB.0042014F
  00EC6539          83C4 14          ADD     ESP,14
  00EC653C          8BC6             MOV     EAX,ESI
  00EC653E          5E               POP     ESI
  00EC653F          C3               RETN
  
  在Show_Char_On_Buffer这个函数完了要返回之前再解锁表面:
  00EC6680          60               PUSHAD
  00EC6681          833D 38000B01 01 CMP     DWORD PTR DS:[<SurfaceLocked>],1       ; 如果是Patch锁定表面,则要解锁
  00EC6688          75 1A            JNZ     SHORT SWDMD_GB.00EC66A4
  00EC668A          FF35 F05AA800    PUSH    DWORD PTR DS:[<lpFontSurface_Buffer>]
  00EC6690          B9 7003A600      MOV     ECX,OFFSET <SWDMD_GB.lplpDD>
  00EC6695          E8 C8DD54FF      CALL    <SWDMD_GB.Font_UnlockSurface>
  00EC669A          C705 38000B01 00>MOV     DWORD PTR DS:[<SurfaceLocked>],0       ; 如果不是,则无需再解锁!
  00EC66A4          61               POPAD
  00EC66A5          5E               POP     ESI
  00EC66A6          C9               LEAVE
  00EC66A7          C2 1800          RETN    18
  
  修改之后,字体终于可以正常显示了!

  之后发现小地图花,放BIK动画时花屏,按P键截图时花屏都是同样原因造成的!所以按上面的同样的办法改了就可以了!
  显示小地图的函数在00420448处,按P键截图的函数在0046E6ED处,修改办法都是一样的。
  
  播放BIK动画前不能解锁表面:
  0046C194      50               PUSH    EAX
  改为:
  0046C194     /EB 05            JMP     SHORT SWDMD_GB.0046C19B
  
  设定BIK动画的位置:
  0046C1DD      33DB             XOR     EBX,EBX
  0046C1DF      FF75 FC          PUSH    DWORD PTR SS:[EBP-4]                     ; 某种FLAG
  0046C1E2      51               PUSH    ECX                                      ; Y坐标
  0046C1E3      57               PUSH    EDI                                      ; X坐标
  0046C1E4      52               PUSH    EDX                                      ; 表面的高度
  0046C1E5      FF75 F8          PUSH    DWORD PTR SS:[EBP-8]                     ; 表面的lPitch
  0046C1E8      FF75 F4          PUSH    DWORD PTR SS:[EBP-C]                     ; 目标表面的Buffer
  0046C1EB      50               PUSH    EAX                                      ; BIK动画文件的句柄
  0046C1EC      FF15 64D24C00    CALL    DWORD PTR DS:[<&binkw32.inkw32._BinkCopy>; binkw32.BinkCopyToBuffer
  
  00EC6210      A3 045DEE00      MOV     DWORD PTR DS:[<Temp1>],EAX
  00EC6215      A1 C48E5100      MOV     EAX,DWORD PTR DS:[518EC4]
  00EC621A      03C1             ADD     EAX,ECX
  00EC621C      50               PUSH    EAX
  00EC621D      A1 C08E5100      MOV     EAX,DWORD PTR DS:[<WndRect>]
  00EC6222      03C7             ADD     EAX,EDI
  00EC6224      50               PUSH    EAX
  00EC6225      A1 045DEE00      MOV     EAX,DWORD PTR DS:[<Temp1>]
  00EC622A      52               PUSH    EDX
  00EC622B      FF75 F8          PUSH    DWORD PTR SS:[EBP-8]
  00EC622E      FF75 F4          PUSH    DWORD PTR SS:[EBP-C]
  00EC6231    - E9 B55F5AFF      JMP     SWDMD_GB.0046C1EB
  
  播放BIK动画完成解锁表面:0EC6240
  0046C1F2      A1 8454A800      MOV     EAX,DWORD PTR DS:[A85484]
  0046C1F7      5F               POP     EDI
  0046C1F8      8B48 0C          MOV     ECX,DWORD PTR DS:[EAX+C]
  0046C1FB      3B48 08          CMP     ECX,DWORD PTR DS:[EAX+8]
  
  00EC6240      60               PUSHAD
  00EC6241      68 11270000      PUSH    2711
  00EC6246      B9 986CA700      MOV     ECX,OFFSET <SWDMD_GB.lpDD>
  00EC624B      E8 A89D55FF      CALL    <SWDMD_GB.GetSurface_Ptr>
  00EC6250      50               PUSH    EAX
  00EC6251      E8 8AA155FF      CALL    <SWDMD_GB.DDraw_UnlockSurface>
  00EC6256      83C4 04          ADD     ESP,4
  00EC6259      61               POPAD
  00EC625A      A1 8454A800      MOV     EAX,DWORD PTR DS:[A85484]
  00EC625F      5F               POP     EDI
  00EC6260    - E9 935F5AFF      JMP     SWDMD_GB.0046C1F8
  
  到此基本完成,注意上面我给出的代码都是在窗口模式下的,全屏改窗口只要把4734B0处的PUSH 4E22改为PUSH 4E21即可。
  窗口化之后为了美观要把窗口放在屏幕中间,直接把SetWindowPos函数的参数改掉即可,这个很简单。
  窗口位置移到屏幕中间之后要把Blt函数的目标也修正过来,程序会不停在41FADA这个函数里面把窗口位置改到左上角,所以在这个函数返回时修正一下即可:
  00EC6A0A          60               PUSHAD
  00EC6A0B          B9 04000000      MOV     ECX,4
  00EC6A10          BE C08E5100      MOV     ESI,OFFSET <SWDMD_GB.WndRect>               ; 一开始GetWindowRect保存的窗口位置
  00EC6A15          BF 0C6DA700      MOV     EDI,OFFSET <SWDMD_GB.Wnd_Rect_Show>         ; Blt函数使用的DestRect参数的窗口位置
  00EC6A1A          F3:A5            REP     MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI]  
  00EC6A1C          61               POPAD
  00EC6A1D        - E9 0C9155FF      JMP     SWDMD_GB.0041FB2E                           ; 跳回去


  结束收工!
  
--------------------------------------------------------------------------------
【经验总结】
  1.用IDA载入DDRAW.DLL,会提示下载符号文件,然后导出MAP,分析的时候加载到OD里面,就可以很方便看出库函数的名称
  了
  2.可以看到,主程序是一个人写的,界面和天书是另一个人写的(吴东兴还是吕志凯?)
  3.花屏的原因是锁定表面之后还没有把数据写进去就先解锁表面了,新显卡要往显存里面写数据必须先锁定表面,否则会出
  错,老显卡就不用
  4.这个游戏用的是DirectX7的技术,所有的3D操作都是通过DirectDrawSurface来完成的
  5.苍之涛的引擎和轩辕剑4是一样的,所以也可以这样解决花屏问题。
  
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!

                                                       2010年01月18日 下午 11:31:43

上传的附件 SWDMD_GB.rar