游戏开发论坛

 找回密码
 立即注册
搜索
查看: 16427|回复: 32

网络游戏服务器内存补丁技术(更新x64相关)

[复制链接]

15

主题

368

帖子

406

积分

中级会员

Rank: 3Rank: 3

积分
406
发表于 2009-7-25 03:52:00 | 显示全部楼层 |阅读模式
当一款网游产品规模越做越大,服务器数量越来越多时,全服停机一次的成本会变成很高。一款几十万人在线的游戏全服停机维护维护一次至少一至两小时(已经算是比较快的了),一般情况下至少会减少当天约五分之一的收入。

正常的版本更新或是例行维护是不可避免的需要停机的,这个似乎没有什么比较好的解决方案。我们需要解决的是降低因为服务器程序软件的BUG导致的停机维护。

在很多情况下,临时修复一个程序的BUG并不需要改动太多代码,甚至只要对判断条件稍作一些修改,或只是把某个函数屏蔽掉就能够解决问题。请注意这里指的是“临时”的解决方案。我们并不排除某个BUG要完美的解决需要动到很多底层机制,需要涉及修改大量代码,但基本上“临时”解决方案只需要修改很少的代码就能达到可以接受的效果。这个时候就可以考虑使用内存补丁技术。

内存补丁技术主要是通过动态修改服务器的可执行代码而达到临时解决问题的效果。除去编写内存补丁有一定的难度外,建立这样的一套机制成本很低,基本不会给服务器带来额外的负面影响,是否启用完全可以取决于当时的实际状况。

对于建立这样一套机制,需要注意的一些问题:

1。被修改的代码可能正在被执行
虽然这种机率很低,但还是需要考虑一下。为了简单,内存补丁机制应该提供在服务器内部实现,在每次逻辑轮循完成后检测是否存在内存补丁,如果存在则给进程内存打上补丁。这样基本能够避免逻辑代码上的改动同时正在被执行。框架底层的代码是否使用可以稍微赌一下自己的人品,觉得自己人品不算很差的话其实也可以用,基本上问题不会太大。

2。提供的内存补丁接口可能被误操作
要求提供原有地址的代码进行检测,完全相同才进行操作,再对代码提供一个特殊算法生成的校验码进行检测,可以完全避免运维人员失误操作调用或者是错误的版本调用。

3。修改后的代码有所添加,修改容纳不了新添加的代码
正常情况下不应该添加太多代码。同时在程序中事先定义好无用的废弃函数,专们用于新添加的代码存放。放不下时直接call到废弃函数的地址。注意:一定需要使用预先定义好的废弃函数地址存放而不能简单定义一个数据块存放补丁代码。否则会被DEP强制关闭程序。虽然关闭程序的DEP保护也可以达到效果,但不建议这么做。安全性倒是其次(拿不到完整的服务器程序分析,是不可能写出有危害的缓冲区利用代码的),主要是一旦溢出后数据段能够运行,可能会导致程序崩溃的地址变得很怪异,不利于事后分析问题。

4。修改后利用的变量有所添加
同上,预先定义好一块数据块。

5。代码段的内存修改权限
使用VirtualProtect设置代码页的读写属性

6。VC编译器不要设置随机基址(/DYNAMICBASE:NO 在链接器->高级中设置)
随机基址本意就是为了让黑客的shellcode编写更为复杂,但我们不是黑客攻击,所以不需要打开这个选项给自己找麻烦。

下面的提供的代码(部分):

CMemFix::CMemFix()
{
    //避免此函数被优化掉不生成,调用一下。
    if (EmptyFunc() != 0)
    {
        memset(m_DataSeg, 0xCC, sizeof(m_DataSeg));
    }
}

CMemFix::~CMemFix()
{
}

bool CMemFix::FixMem(void* pProcAddr, const char* pOldCode, const char* pNewCode, size_t nCodeLen)
{
    DWORD dwOldProtece = 0;
    if (::VirtualProtect(pProcAddr, nCodeLen, PAGE_EXECUTE_WRITECOPY, &dwOldProtece) == FALSE)
    {
        return false;
    }

    try
    {
        if (memcmp(pProcAddr, pOldCode, nCodeLen) != 0)
        {
            ::VirtualProtect(pProcAddr, nCodeLen, dwOldProtece, &dwOldProtece);
            return false;
        }

        memcpy(pProcAddr, pNewCode, nCodeLen);
    }
    catch (...)
    {
        try
        {
            memcpy(pProcAddr, pOldCode, nCodeLen);
        }
        catch (...)
        {
        }
        ::VirtualProtect(pProcAddr, nCodeLen, dwOldProtece, &dwOldProtece);
        return false;
    }

    ::VirtualProtect(pProcAddr, nCodeLen, dwOldProtece, &dwOldProtece);
    return true;
}

//填充废弃代码(x64下无法直接嵌入汇编指令,使用正常代码替代)
int CMemFix::EmptyFunc()
{
#define ADDONE nTmp=0;nTmp=0;nTmp=0;nTmp=0;nTmp=0;nTmp=0;nTmp=0;nTmp=0;
#define LOOPADD ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;     ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;     ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;     ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;     ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;

    volatile int nTmp = 0;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    LOOPADD;
    return nTmp;
}


x64下的shellcode编写可能有些困难,因为相关的资料文档比较少,也没有像OllyDbg这么直观的双击相应的汇编指令即可进行修改的软件,不过可以使用Windbg的命令在调试过程中动态修改汇编指令,只是用起来不如OD那么舒服了。如果只用VC写shellcode也可以,把相应的代码地址拖进内存窗口可以直接修改内存opcode,在VC的汇编窗口下会立即显示为修改过后的汇编指令,不过这样需要涉及到opcode的相关知识,建议去intel网站上下载最新的《Intel® 64 and IA-32 Architectures Software Developer’s Manual》,看一看第二章Instruction Format中关于opcode和指令前缀的知识。大概了解下能当手册查即可。


最后:内存补丁只是一种手段,它不能完全的解决因为程序BUG而需要停机的问题,就像反外挂一样没有任何一种技术可以完全的解决。但是它在某些特定的情况下是可以有效的解决一些问题。还有往常的一些不是很紧要但确实会带来不便,还上升不到需要停机的问题也可以用它来解决。至于什么时候使用它就看使用者各自的情况了。


附上一个小小的修改示例:这个程序中未判断除数为0的情况,将导致输入除数为0时程序会崩溃。这个时候我们使用内存补丁将它修复,当除数为0时,商为0。将内存补丁的文字复制下来在控制台的输入中粘贴进去回车后即可看到补丁是否已经成功打上。如果不想打补丁则直接输入回车或随便乱输一气使补丁打失败即可。

示例使用VC2008编译,未安装VC2008则需要安装VC2008 SP1的C RUNTIME:



内存补丁示例
VC2008 SP1 C RUNTIME(大小4M)

149

主题

4981

帖子

5033

积分

论坛元老

Rank: 8Rank: 8

积分
5033
QQ
发表于 2009-7-25 11:17:00 | 显示全部楼层

Re:网络游戏服务器内存补丁技术

晕了,这种情况我更多的希望是通过使用动态语言来解决……
假如C++编写的是比较底层的部分,用LUA之类来编写上层逻辑,那么如果底层有bug,这时我觉得停机更新更稳妥,如果是上层,重新载入lua脚本就可以了。
lz这方法感觉太夸张了……

15

主题

368

帖子

406

积分

中级会员

Rank: 3Rank: 3

积分
406
 楼主| 发表于 2009-7-25 12:21:00 | 显示全部楼层

Re:网络游戏服务器内存补丁技术

哈哈,杠王出现了

其实从单纯的降低服务器停机维护这一问题来看,我是赞同你所说的,使用动态脚本语言的。不过实际情况必须从全局考虑。完全使用脚本语言开发逻辑也会带来很多负面问题。

我写这篇文章的出发点也仅仅是为了给大家提供一种当游戏已经成型并且在线人数不算很低后降低临时停机的频率的一种方法。纯用脚本开发逻辑的项目(我所知道的是只有听说过网易用的python)自然对我的这种方法不屑一顾了,不过绝大多数的项目都是大部分的C++代码加相对少量的脚本代码。

在线人数比较低的游戏服务器数量也不会太多,停机一次的时间不会太长,可以接受。而且碰上某些会导致宕机的BUG,平均半小时宕一台服务器的这种情况。当你写好内存补丁时服务器差不多已经全宕了个遍,想不停机都不行,其实也是不太适用这种方法的。

是否使用就根据自己的实际情况见仁见智了。

149

主题

4981

帖子

5033

积分

论坛元老

Rank: 8Rank: 8

积分
5033
QQ
发表于 2009-7-25 13:09:00 | 显示全部楼层

Re:网络游戏服务器内存补丁技术

我主要是觉得这种hack有点夸张了…… = =!
其实我现在比较倾向的是,凡是做资源/概念管理的都用C++写,在这个基础上的功能逻辑则用脚本来写。不过还没实践……

4

主题

42

帖子

96

积分

注册会员

Rank: 2

积分
96
发表于 2009-7-26 22:04:00 | 显示全部楼层

Re:网络游戏服务器内存补丁技术

汽车发动机每分钟4000转,气门每秒要工作10多次,工作温度是1000度左右,在这个极度的环境下,控制气门开关只靠一个简单的弹簧,任何高科技的东西都难以保证这个工作长时间正常工作,只能靠弹簧自身的质量,所以弹簧的质量,材质是非常重要的,我想代码也一样!

22

主题

309

帖子

353

积分

中级会员

Rank: 3Rank: 3

积分
353
QQ
发表于 2009-7-26 23:37:00 | 显示全部楼层

Re:网络游戏服务器内存补丁技术

不用脚本的话..用dll如何?出问题重新加载一下dll...

15

主题

368

帖子

406

积分

中级会员

Rank: 3Rank: 3

积分
406
 楼主| 发表于 2009-7-27 02:17:00 | 显示全部楼层

Re:网络游戏服务器内存补丁技术

用dll会有太多的限制,反而不如直接修改内存来得直接方便。

内存补丁是一项很成熟的技术,在破解、外挂方面它已经被广泛使用。正常做网络游戏服务器的程序员擅长汇编的不是很多所以目前还没有被广泛的使用。可以认为使用这套机制有一定门槛但绝对不能认为这套机制很复杂。其实只是补丁代码编写难度比较高,但写好后测试一两次使用它就很安全了,根本不用担心它是什么洪水猛兽。

其实这就是飞机能手动控制和自动控制一样。到底是自动控制是高科技还是手动控制是高科技呢?况且人家保留一套手动控制的成本可是很高的,而我们建立这样一套机制成本很低,何乐不为呢?没出问题时不用它就是了嘛!

149

主题

4981

帖子

5033

积分

论坛元老

Rank: 8Rank: 8

积分
5033
QQ
发表于 2009-7-27 21:37:00 | 显示全部楼层

Re:网络游戏服务器内存补丁技术

汇编……不懂…… = =!
DLL我以前也想过,主要的问题是,假如原有的DLL创建了一些对象,并且这些对象需要一直保留在内存中使用,那么如何处理这些对象就是个问题……

2

主题

123

帖子

123

积分

注册会员

Rank: 2

积分
123
发表于 2009-7-27 21:42:00 | 显示全部楼层

Re:网络游戏服务器内存补丁技术

其实,最大的难点不是更新代码,而是在更新代码时正在处理的数据怎么办。
一般的service可以把逻辑层更新重启(可以起完新的再关旧的),而数据都在后台数据库里,新的请求还在前端等待着。网游的服务器没法这样吧。

15

主题

368

帖子

406

积分

中级会员

Rank: 3Rank: 3

积分
406
 楼主| 发表于 2009-7-28 01:29:00 | 显示全部楼层

Re:网络游戏服务器内存补丁技术(附测试实例及源代码)

提供了一个示例及相关的源代码(代码里面有个小的内存泄漏,不过无伤大雅了),里面对一个没有判断除数为0而导致崩溃的BUG使用内存补丁进行了修正。实际服务器开发中也可能会碰到此类问题。算是一个还有一定实用价值的示例。后面有时间我再慢慢放出一些更高级的示例。

其实写内存补丁就是写shellcode,而且比写shellcode舒服,考虑的东西要少。因为程序是自己的,可以为自己的shellcode大开方便之门。比如定位几个基本的API地址,我们自己的程序里都可以预先为它查找好,shellcode直接把查好的地址拿去用即可。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-6-20 00:42

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表