對於線上的服務,經常會出現xxx服務的某一段邏輯里面有bug,需要緊急修復。對於無狀態的服務,可以修復之后,直接重啟。但是,對於有狀態的服務,重啟意味着內存狀態丟失和長連接斷開。比如,如果魔獸的服務器要重啟,那么已經登錄上來的玩家就會出現連接中斷。對於不能容忍重啟的有狀態的服務,可以采取熱更新的方式,來修復錯誤的邏輯。
它的基本原理很簡單:
- 假設需要熱更新的函數是func_a
- 進程在運行的過程中,通過信號或其他的機制,觸發加載一個動態庫。
- 動態庫中包含定義了修復后的函數func_b
- 通過加載動態庫之后,解析動態庫中的符號表,找到要修復的函數func_a和修復后的實現func_b的內存地址
- 通過mprotect修改進程空間代碼段的權限,添加寫的權限。這樣意味着可以修改func_a內存地址了。
- 在func_a的內存地址插入一段匯編代碼,將調用func_a的邏輯跳轉到func_b。
// 可以這么粗暴的理解 func_a() { // 插入代碼 func_b(); return; // 錯誤的邏輯 }
- 替換之后,原來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)
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