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

推荐阅读更多精彩内容