Linux下c++热更新

我的文章会先发布到个人博客后,再更新到简书,可以到个人博客或者公众号获取更多内容。


背景介绍

我们都知道游戏服务器经常会有一些小版本或者线上问题紧急修复版本,对于游戏业务或多或少都有一些损害,特别是有些进程会携带有状态信息和维持和客户端的长连接。当你维护重启过于频繁的话会影响线上玩家的体验和留存,同时也影响所提供服务的稳定性。
因此就有了两种方案,热重启热更新,分别或一起保障所提供服务一定的稳定性。

  • 热重启:进程中使用共享内存保存状态或玩家信息,直接杀进程快速重启(对于和客户端直连的长连接服务还是会有影响)。
  • 热更新:不停进程,支持代码实时更新。

热更新方案

不同的技术栈有不同的热更新方式,如:

  1. java热更:替换内存中已经加载好的class字节码
  2. 内嵌lua热更: 通过lua提供的require机制强制替换已加载好的模块(由于性能不高,后续如果太耗性能会需要优化)
  3. python热更: 通过python提供的reload实现
  4. c++热更:
    • 父进程通过fork,产生子进程,然后子进程数据更改从而触发写时复制,复制父进程的数据,如果有socket,需要子进程处理新连接,父进程批量转移旧连接,或等待旧连接处理完毕后自杀(复杂)
    • 修改进程中的GOT表,跳转至新函数从而达到热更(优雅但有局限)
    • 通过汇编修改代码段中现有函数最开始的指令jmp至新加so中的函数地址(粗暴但适用性广)

修改代码段热更

  • 假设需要热更新的函数是funcA
  • 让进程在运行的过程中,通过信号或其他的机制,触发加载一个动态库
  • 动态库中包含定义了修复后的函数funcB
  • 通过加载动态库之后,解析动态库中的符号表,找到要修复的函数funcA和修复后的实现funcB的内存地址
  • 通过mprotect修改进程空间代码段的权限,添加写的权限。这样意味着可以修改funcA对应的代码段地址中的内容
  • 在funcA的内存地址插入一段汇编代码,来实现调用funcB函数或者跳转到funcB

归纳如下:

  1. 找到对应函数的符号和合适的汇编指令
  2. 将指令转换为机器码
  3. 修改代码段,替换函数地址

找到合适的汇编指令

在原函数对应的内存地址插入一段汇编代码,来实现调用新函数的逻辑或跳转到新函数。如果对汇编熟悉的小伙伴可能就知道对应的指令有cal和jmp,call会破坏栈平衡,故适用的是jmp。

  • jmp: jmp 函数地址,跳转到对应的函数,即修改EIP寄存器的值,不改变栈平衡
  • call: 将下一条指令的所在地址(即当时程序计数器PC的内容)入栈,修改EIP寄存器的值,会改变栈平衡。并且call会与ret对应
  • ret: 返回到CALL指令PUSH到栈顶的基址,并把栈顶的值POP出来
    在jmp之前,咱们需要将新函数的地址加载到内存,然后再jmp。故需要用到movq和寄存器rax(由于64位机中会将rdi,rsi,rdx,rcx,r8,r9作为函数传参的保存位置,rax会作为返回值,故使用rax不影响栈)。

将指令转换为机器码

通过写一个测试汇编文件,得到对应的机器码。
my_test_assembler.s

movq $0x1fffffffff,%rax
jmpq *%rax

gcc -c 汇编文件my_test_assembler.s生成对应my_test_assembler.o
然后通过objdum -d my_test_assembler.o 查看对应的机器码


机器码

得到对应的机器码以后咱们就可以开始修改程序的代码段来实现函数的更新。

修改代码段

void *dlmopen (Lmid_t lmid, const char *filename, int flags);
加载动态共享库

void *dlsym(void *handle, const char *symbol);
返回共享库中对应符号的地址

int mprotect(void *addr, size_t len, int prot);
用来修改对应进程中从addr开始的len长的内存叶保护权限,addr必须按内存叶大小对齐。
prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:
1)PROT_READ:表示内存段内的内容可写;
2)PROT_WRITE:表示内存段内的内容可读;
3)PROT_EXEC:表示内存段中的内容可执行;

找到函数对应的符号

通过指令nm 和objdum -d可以得到所有符号信息。

主要实现

int replaceFunction(void* pSoHandle, const char* pSymbol) {
    void* pNewAddr = dlsym(pSoHandle, pSymbol);
    if (nullptr == pNewAddr) {
        fprintf(stderr, "get new addr, Error: %s\n" ,strerror(errno));
        return -1;
    }

    void* pOldAddr = dlsym(nullptr, pSymbol);
    if (nullptr == pOldAddr) {
        fprintf(stderr, "get old addr, Error: %s\n" ,strerror(errno));
        return -1;
    }

    size_t iPageSize = sysconf(_SC_PAGE_SIZE);
    uintptr_t pAlignAddr = (uintptr_t)(pOldAddr) & (~(iPageSize - 1));
    if (mprotect((void*)(pAlignAddr), 2 * iPageSize, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
        fprintf(stderr, "mprotect old addr, Error: %s\n" ,strerror(errno));
        return -1;
    }

    // 以下只支持64系统
    memset(static_cast<char*>(pOldAddr), 0x48, 1);
    memset(static_cast<char*>(pOldAddr)+1, 0xb8, 1);
    memcpy(static_cast<char*>(pOldAddr)+2, &pNewAddr, 8);
    memset(static_cast<char*>(pOldAddr)+10, 0xff, 1);
    memset(static_cast<char*>(pOldAddr)+11, 0xe0, 1);
    if (mprotect((void*)(pAlignAddr), 2 * iPageSize, PROT_READ | PROT_EXEC) != 0) {
        fprintf(stderr, "mprotect2 old addr, Error: %s\n" ,strerror(errno));
    }

    return 0;
}

void signalHandle(int) {
    void* pSoHandle = dlopen(SO_FILE, RTLD_NOW);
    if (nullptr == pSoHandle) {
        fprintf(stderr, "Error:%s\n", dlerror());
        exit(-1);
    }

    replaceFunction(pSoHandle, "_ZN4Role12getClassNameB5cxx11Ev");
    //replaceFunction(so_handle, "对应函数的符号");
}

程序通过接收相应的信号来触发函数的替换和热更操作。
代码地址

修改GOT表热更

后续完善

参考资料

欢迎关注我的其它发布渠道

个人博客
公众号

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容