Linux下c++熱更新

我的文章會(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ù)棧有不同的熱更新方式,如:

  1. java熱更:替換內(nèi)存中已經(jīng)加載好的class字節(jié)碼
  2. 內(nèi)嵌lua熱更: 通過(guò)lua提供的require機(jī)制強(qiáng)制替換已加載好的模塊(由于性能不高吼和,后續(xù)如果太耗性能會(huì)需要優(yōu)化)
  3. python熱更: 通過(guò)python提供的reload實(shí)現(xiàn)
  4. 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

歸納如下:

  1. 找到對(duì)應(yīng)函數(shù)的符號(hào)和合適的匯編指令
  2. 將指令轉(zhuǎn)換為機(jī)器碼
  3. 修改代碼段,替換函數(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ī)器碼


機(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ù)的替換和熱更操作。
代碼地址

修改GOT表熱更

后續(xù)完善

參考資料

歡迎關(guān)注我的其它發(fā)布渠道

個(gè)人博客
公眾號(hào)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末轻黑,一起剝皮案震驚了整個(gè)濱河市糊肤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌氓鄙,老刑警劉巖馆揉,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異抖拦,居然都是意外死亡升酣,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)态罪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)噩茄,“玉大人,你說(shuō)我怎么就攤上這事复颈〖ㄆ福” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵券膀,是天一觀的道長(zhǎng)君纫。 經(jīng)常有香客問(wèn)我,道長(zhǎng)芹彬,這世上最難降的妖魔是什么蓄髓? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮舒帮,結(jié)果婚禮上会喝,老公的妹妹穿的比我還像新娘陡叠。我一直安慰自己,他們只是感情好肢执,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布枉阵。 她就那樣靜靜地躺著,像睡著了一般预茄。 火紅的嫁衣襯著肌膚如雪兴溜。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,718評(píng)論 1 305
  • 那天耻陕,我揣著相機(jī)與錄音拙徽,去河邊找鬼。 笑死诗宣,一個(gè)胖子當(dāng)著我的面吹牛膘怕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播召庞,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼岛心,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了篮灼?” 一聲冷哼從身側(cè)響起忘古,我...
    開(kāi)封第一講書(shū)人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎穿稳,沒(méi)想到半個(gè)月后存皂,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡逢艘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了骤菠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片它改。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖商乎,靈堂內(nèi)的尸體忽然破棺而出央拖,到底是詐尸還是另有隱情,我是刑警寧澤鹉戚,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布鲜戒,位于F島的核電站,受9級(jí)特大地震影響抹凳,放射性物質(zhì)發(fā)生泄漏遏餐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一赢底、第九天 我趴在偏房一處隱蔽的房頂上張望失都。 院中可真熱鬧柏蘑,春花似錦、人聲如沸粹庞。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)庞溜。三九已至革半,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間流码,已是汗流浹背又官。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旅掂,地道東北人赏胚。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像商虐,于是被迫代替她去往敵國(guó)和親觉阅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容