我的文章會(huì)先發(fā)布到個(gè)人博客后,再更新到簡(jiǎn)書(shū),可以到個(gè)人博客或者公眾號(hào)獲取更多內(nèi)容衰伯。
背景介紹
我們都知道游戲服務(wù)器經(jīng)常會(huì)有一些小版本或者線上問(wèn)題緊急修復(fù)版本,對(duì)于游戲業(yè)務(wù)或多或少都有一些損害拭嫁,特別是有些進(jìn)程會(huì)攜帶有狀態(tài)信息和維持和客戶端的長(zhǎng)連接可免。當(dāng)你維護(hù)重啟過(guò)于頻繁的話會(huì)影響線上玩家的體驗(yàn)和留存,同時(shí)也影響所提供服務(wù)的穩(wěn)定性做粤。
因此就有了兩種方案浇借,熱重啟和熱更新,分別或一起保障所提供服務(wù)一定的穩(wěn)定性怕品。
- 熱重啟:進(jìn)程中使用共享內(nèi)存保存狀態(tài)或玩家信息妇垢,直接殺進(jìn)程快速重啟(對(duì)于和客戶端直連的長(zhǎng)連接服務(wù)還是會(huì)有影響)。
- 熱更新:不停進(jìn)程肉康,支持代碼實(shí)時(shí)更新闯估。
熱更新方案
不同的技術(shù)棧有不同的熱更新方式,如:
- java熱更:替換內(nèi)存中已經(jīng)加載好的class字節(jié)碼
- 內(nèi)嵌lua熱更: 通過(guò)lua提供的require機(jī)制強(qiáng)制替換已加載好的模塊(由于性能不高吼和,后續(xù)如果太耗性能會(huì)需要優(yōu)化)
- python熱更: 通過(guò)python提供的reload實(shí)現(xiàn)
-
c++熱更:
- 父進(jìn)程通過(guò)fork涨薪,產(chǎn)生子進(jìn)程,然后子進(jìn)程數(shù)據(jù)更改從而觸發(fā)寫(xiě)時(shí)復(fù)制纹安,復(fù)制父進(jìn)程的數(shù)據(jù)尤辱,如果有socket,需要子進(jìn)程處理新連接厢岂,父進(jìn)程批量轉(zhuǎn)移舊連接,或等待舊連接處理完畢后自殺(復(fù)雜)
- 修改進(jìn)程中的GOT表阳距,跳轉(zhuǎn)至新函數(shù)從而達(dá)到熱更(優(yōu)雅但有局限)
- 通過(guò)匯編修改代碼段中現(xiàn)有函數(shù)最開(kāi)始的指令jmp至新加so中的函數(shù)地址(粗暴但適用性廣)
修改代碼段熱更
- 假設(shè)需要熱更新的函數(shù)是funcA
- 讓進(jìn)程在運(yùn)行的過(guò)程中塔粒,通過(guò)信號(hào)或其他的機(jī)制,觸發(fā)加載一個(gè)動(dòng)態(tài)庫(kù)
- 動(dòng)態(tài)庫(kù)中包含定義了修復(fù)后的函數(shù)funcB
- 通過(guò)加載動(dòng)態(tài)庫(kù)之后筐摘,解析動(dòng)態(tài)庫(kù)中的符號(hào)表卒茬,找到要修復(fù)的函數(shù)funcA和修復(fù)后的實(shí)現(xiàn)funcB的內(nèi)存地址
- 通過(guò)mprotect修改進(jìn)程空間代碼段的權(quán)限,添加寫(xiě)的權(quán)限咖熟。這樣意味著可以修改funcA對(duì)應(yīng)的代碼段地址中的內(nèi)容
- 在funcA的內(nèi)存地址插入一段匯編代碼圃酵,來(lái)實(shí)現(xiàn)調(diào)用funcB函數(shù)或者跳轉(zhuǎn)到funcB
歸納如下:
- 找到對(duì)應(yīng)函數(shù)的符號(hào)和合適的匯編指令
- 將指令轉(zhuǎn)換為機(jī)器碼
- 修改代碼段,替換函數(shù)地址
找到合適的匯編指令
在原函數(shù)對(duì)應(yīng)的內(nèi)存地址插入一段匯編代碼馍管,來(lái)實(shí)現(xiàn)調(diào)用新函數(shù)的邏輯或跳轉(zhuǎn)到新函數(shù)郭赐。如果對(duì)匯編熟悉的小伙伴可能就知道對(duì)應(yīng)的指令有cal和jmp,call會(huì)破壞棧平衡确沸,故適用的是jmp捌锭。
- jmp: jmp 函數(shù)地址,跳轉(zhuǎn)到對(duì)應(yīng)的函數(shù)罗捎,即修改EIP寄存器的值,不改變棧平衡
- call: 將下一條指令的所在地址(即當(dāng)時(shí)程序計(jì)數(shù)器PC的內(nèi)容)入棧观谦,修改EIP寄存器的值,會(huì)改變棧平衡桨菜。并且call會(huì)與ret對(duì)應(yīng)
- ret: 返回到CALL指令PUSH到棧頂?shù)幕坊碜矗褩m數(shù)闹礟OP出來(lái)
在jmp之前捉偏,咱們需要將新函數(shù)的地址加載到內(nèi)存,然后再jmp泻红。故需要用到movq和寄存器rax(由于64位機(jī)中會(huì)將rdi,rsi,rdx,rcx,r8,r9作為函數(shù)傳參的保存位置告私,rax會(huì)作為返回值,故使用rax不影響棧)承桥。
將指令轉(zhuǎn)換為機(jī)器碼
通過(guò)寫(xiě)一個(gè)測(cè)試匯編文件驻粟,得到對(duì)應(yīng)的機(jī)器碼。
my_test_assembler.s
movq $0x1fffffffff,%rax
jmpq *%rax
gcc -c 匯編文件my_test_assembler.s生成對(duì)應(yīng)my_test_assembler.o
然后通過(guò)objdum -d my_test_assembler.o 查看對(duì)應(yīng)的機(jī)器碼
得到對(duì)應(yīng)的機(jī)器碼以后咱們就可以開(kāi)始修改程序的代碼段來(lái)實(shí)現(xiàn)函數(shù)的更新凶异。
修改代碼段
void *dlmopen (Lmid_t lmid, const char *filename, int flags);
加載動(dòng)態(tài)共享庫(kù)
void *dlsym(void *handle, const char *symbol);
返回共享庫(kù)中對(duì)應(yīng)符號(hào)的地址
int mprotect(void *addr, size_t len, int prot);
用來(lái)修改對(duì)應(yīng)進(jìn)程中從addr開(kāi)始的len長(zhǎng)的內(nèi)存葉保護(hù)權(quán)限蜀撑,addr必須按內(nèi)存葉大小對(duì)齊。
prot可以取以下幾個(gè)值剩彬,并且可以用“|”將幾個(gè)屬性合起來(lái)使用:
1)PROT_READ:表示內(nèi)存段內(nèi)的內(nèi)容可寫(xiě)酷麦;
2)PROT_WRITE:表示內(nèi)存段內(nèi)的內(nèi)容可讀;
3)PROT_EXEC:表示內(nèi)存段中的內(nèi)容可執(zhí)行喉恋;
找到函數(shù)對(duì)應(yīng)的符號(hào)
通過(guò)指令nm 和objdum -d可以得到所有符號(hào)信息沃饶。
主要實(shí)現(xiàn)
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系統(tǒng)
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, "對(duì)應(yīng)函數(shù)的符號(hào)");
}
程序通過(guò)接收相應(yīng)的信號(hào)來(lái)觸發(fā)函數(shù)的替換和熱更操作。
代碼地址