zoukankan      html  css  js  c++  java
  • 一种基于so的C/C++服务热更新方案

    对于线上的服务,经常会出现xxx服务的某一段逻辑里面有bug,需要紧急修复。对于无状态的服务,可以修复之后,直接重启。但是,对于有状态的服务,重启意味着内存状态丢失和长连接断开。比如,如果魔兽的服务器要重启,那么已经登录上来的玩家就会出现连接中断。对于不能容忍重启的有状态的服务,可以采取热更新的方式,来修复错误的逻辑。

    它的基本原理很简单:

    1. 假设需要热更新的函数是func_a
    2. 进程在运行的过程中,通过信号或其他的机制,触发加载一个动态库。
    3. 动态库中包含定义了修复后的函数func_b
    4. 通过加载动态库之后,解析动态库中的符号表,找到要修复的函数func_a和修复后的实现func_b的内存地址
    5. 通过mprotect修改进程空间代码段的权限,添加写的权限。这样意味着可以修改func_a内存地址了。
    6. 在func_a的内存地址插入一段汇编代码,将调用func_a的逻辑跳转到func_b。
      // 可以这么粗暴的理解
      func_a()
      {
          // 插入代码
          func_b(); return;
              
          // 错误的逻辑
      }
      
    7. 替换之后,原来func_a代码段的内容已经覆盖,新的内容是跳转到func_b。这样在后面的逻辑中,如果执行到调用func_a的逻辑,会跳转到修复后的func_b。逻辑被修正,程序实现了热更新。

    下面开始具体的实现上述流程中的几个重要的步骤:

    • 如何在运行的过程中加载一个so的库,并且解析到里面的符号表。
      linux提供了下面的几个api
      #include <dlfcn.h>
      ...
      void *dlopen(const char *__file, int __mode)
      void *dlsym(void *__restrict__ __handle, const char *__restrict__ __name)
      int dlclose(void *__handle)
      char *dlerror(void)
      
      举一个简单的例子,把一个函数打包为一个so库
      int print_age(int val)
      {
          cout << "val : " << val << endl;
          return 0;
      }
      
      /*
      g++ -fPIC -shared test_shared_so.cc -o test_shared.so
      */
      

      编译的时候加上-fpic,生成位置无关代码。查看so的符号表,如下图:


       

    当然,我这里是用了g++生成的符号表,如果希望看到的是干净的print_age符号,可以改为gcc。


    下面写一个main函数去加载这个so库:

    typedef int (*FUNC_PTR)(int);
    
    int main()
    {
        //1. 调用dlopen加载so库
        char patch[] = "./test_shared.so";
        void *lib = dlopen(patch, RTLD_NOW);
        if (NULL == lib)
        {
            cout << "dlopen failed , patch " << patch << endl;
            return 0;
        }
    
        // 2. 查找函数符号表并且替换
        FUNC_PTR p_func = (FUNC_PTR)dlsym(lib, "_Z9print_agei");
        if (NULL == p_func)
        {
            cout << "fix symbol failed" << endl;
            dlclose(lib);
            return 0;
        }
    
        // 3. 执行函数
        p_func(100);
        return 0;
    }
    
    
    g++ dlopen.cc -rdynamic -ldl
    -rdynamic
     它将指示连接器把所有符号(而不仅仅只是程序已使用到的外部符号)
    都添加到动态符号表(即.dynsym表)里,
    以便那些通过 dlopen() (这一系列函数使用.dynsym表内符号)这样的函数使用。
    
    -ldl
    如果你的程序中使用dlopen、dlsym、dlclose、dlerror 显示加载动态库,需要设置链接选项 -ldl
    

    通过dlopen,dlsym实现了在运行过程中加载一个动态库,并且可以解析到动态库里面的符号,实现调用。

    • 如何获得代码段可写权限

      #include <sys/mman.h>
      int mprotect(void *addr, size_t len, int prot);
      

      具体的用法:

      addr: 修改保护属性区域的起始地址,addr必须是一个内存页的起始地址,简而言之为页大小(一般是 4KB == 4096字节)整数倍。
      
      len: 被修改保护属性区域的长度 (如果len小于4096会被填充为4096)
      
      prot:可以取以下几个值,并可以用“|”将几个属性结合起来使用:
      1)PROT_READ:内存段可读;
      2)PROT_WRITE:内存段可写;
      3)PROT_EXEC:内存段可执行;
      4)PROT_NONE:内存段不可访问。
      返回值:0;成功,-1;失败(并且errno被设置)
      
    • 获得获得对应函数的addr地址的页起始地址

      // 获得系统内存分页
      // 一般默认的页大小是4096
      size_t page = getpagesize();    
      

      通过getpagesize找到要修改权限的内存页的起始地址,然后作为参数传入mprotect,给这段地址添加写的权限。

      func_begin_addr = &need_fix_func;
      char * begin_page_addr = (char *)func_begin_addr - ((uint64_t)(char *)func_begin_addr % page );
      
      int ret = mprotect (begin_page_addr, (char *)old_func - align_point + inst_len,     PROT_READ | PROT_WRITE | PROT_EXEC)) ;
      if ( 0 != ret)
      {
          return -1;
      }
      
    • 如何给要修复的函数插入跳转到新的函数的汇编

    mov $new_func_entry, %rax # 48 b8 xx xx xx xx xx xx xx xx 
    jmp %rax                  # ff e0
    
    //MOV new_func %rax
    //JMP %rax
    char prefix[] = {'\x48', '\xb8'}; 
    char postfix[] = {'\xff', '\xe0'};    
    
    //将跳转指令写入原函数开头
    memcpy(old_func, prefix, sizeof(prefix));
    memcpy((char *)old_func + sizeof(prefix), &new_func, sizeof(void *));
    memcpy((char *)old_func + sizeof(prefix) + sizeof(void *), postfix, sizeof(postfix));
    

    DEMO 路径:

    $ tree -L 2
    .
    |-- hot_fix
    |   |-- Makefile
    |   |-- hot_fix.cc
    |   |-- hot_fix.h
    |   |-- hot_fix.o
    |   |-- hot_fix_lib
    |   `-- libhot_fix.a
    `-- test_prj
        |-- Makefile
        |-- app.cc
        |-- app.h
        |-- fix_patch.cc
        |-- main
        |-- main.cc
        `-- patch.so
    

    main.cc

    #include <iostream>
    #include "app.h"
    #include "hot_fix.h"
    using namespace std;
    
    int main()
    {
        init_hot_fix_signal();
    
        business_logic();
    
        return 0;
    }
    

    app.cc

    #include <iostream>
    #include <unistd.h>
    using namespace std;
    
    // need fix here
    int need_fix_func()
    {
        cout << "before fix_func addr : " << (void*)&need_fix_func <<endl;
    
        int times = 10;
        for (int i = 0; i < times; i++) 
        {
            cout << "before fix cur times " << i << endl;
        }
        return 0;
    }
    
    int business_logic()
    {
        // do something
        while(1)
        {
            sleep(2);
            need_fix_func();
        }
        return 0;
    }
    

    hot_fix.cc

    #include <iostream>
    #include <signal.h>
    #include <dlfcn.h>
    #include <errno.h>
    #include <stdint.h>
    #include <sys/mman.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>
    #include "hot_fix.h"
    using namespace std;
    
    static int fix_func(const void* new_func, void *old_func) 
    {
        cout << "begin fix func " << endl;
    
        //跳转指令
        char prefix[] = {'\x48', '\xb8'};   //MOV new_func %rax
        char postfix[] = {'\xff', '\xe0'};  //JMP %rax
    
        //开启代码可写权限
        size_t page_size= getpagesize();
        const int inst_len = sizeof(prefix) + sizeof(void *) + sizeof(postfix);
        char *align_point = (char *)old_func - ((uint64_t)(char *)old_func % page_size);
        if (0 != mprotect(align_point, (char *)old_func - align_point + inst_len, PROT_READ | PROT_WRITE | PROT_EXEC)) {
            return -1;
        }
    
        //将跳转指令写入原函数开头
        memcpy(old_func, prefix, sizeof(prefix));
        memcpy((char *)old_func + sizeof(prefix), &new_func, sizeof(void *));
        memcpy((char *)old_func + sizeof(prefix) + sizeof(void *), postfix, sizeof(postfix));
    
        //关闭代码可写权限
        if (0 != mprotect(align_point, (char *)old_func - align_point + inst_len, PROT_READ | PROT_EXEC)) {
            return -1;
        }
        return 0;
    }
    
    static void do_fix(int signum)
    {
        cout << "do fix" << endl;
    
        //1. 调用dlopen加载so库
        char patch_patch[] = "../test_prj/patch.so";
        void *lib = dlopen(patch_patch, RTLD_NOW);
        if (NULL == lib)
        {
            cout << "dlopen failed , patch " << patch_patch << endl;
            return;
        }
    
        // 2. 查找函数符号表并且替换
        FIXTABLE *fix_item = (FIXTABLE *)dlsym(lib, "fix_table");
        if (NULL == fix_item) 
        {
            cout << "fix symbol failed" << endl;
            dlclose(lib);
            return;
        }
        
        void * result = dlopen(NULL, RTLD_NOW);
        if (NULL == result) 
        {
            cout << "result is null" << endl;
            dlclose(lib);
            return;
        }
    
        // 3. 执行更新
        int ret = fix_func(fix_item->new_func, fix_item->old_func);
        cout << "fix result ret " << ret << endl;
        return;
    }
    
    int init_hot_fix_signal() 
    {
        if (signal(SIGUSR1, do_fix) == SIG_ERR) 
        {
            return -1;
        }
        return 0;
    }
    

    patch.cc

    #include <iostream>
    #include "app.h"
    #include "hot_fix.h"
    
    using namespace std;
    
    // 定义要热更新的函数
    int fix_func()
    {
        cout << "before fix_func addr : " << (void*)&need_fix_func << endl;
        cout << "after  fix_func addr : " << (void*)&fix_func <<endl;
        
        cout << "load new fix function" << endl;
        // fix here
        int times = 3;
        for (int i = 0; i < times; i++)
        {
            cout << "after fix cur times " << i << endl;
        }
        return 0;
    }
    
    // 定义替换的函数和更新后的函数
    FIXTABLE fix_table = {(void *)&fix_func, (void *)&need_fix_func};
    

    执行结果:
    通过触发signal,进程不重启的情况下被更新:

    kill -USR1 `ps -ef|grep main|grep -v grep|awk '{print $2}'`
    


    参考:https://www.jianshu.com/p/b7c7102119fa

  • 相关阅读:
    信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1063:最大跨度值
    信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1063:最大跨度值
    信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1062:最高的分数
    信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1057:简单计算器
    信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1057:简单计算器
    信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1062:最高的分数
    信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言—— 1062:最高的分数
    C#怎么给新建的winform程序添加资源文件夹Resources
    C#怎么给新建的winform程序添加资源文件夹Resources
    C#Win32API编程之PostMessage
  • 原文地址:https://www.cnblogs.com/lidabo/p/15508871.html
Copyright © 2011-2022 走看看