iis多进程下的全局变量_Linux 下 C++so 热更新

ee7ae172fa8de9fa6e7c4fc917c186ee.png

作者:markshuang,腾讯 IEG 后台开发工程师

1 背景介绍

我们知道游戏服务器经常有小版本发布,如果每次一点小的改动就重启,对于游戏业务来说或多或少是有损服务的,如果是有状态的进程影响更大,因此服务器支持热更能够使得服务更加稳定、用户体验更好。

2 不同热更方案

不同的服务器有各自的热更方式,比如 java 热更(替换内存中已经加载好的 class 字节码)、内嵌 lua 热更(通过 lua 提供的 require 机制强制刷新已加载好的模块)、C++热更(例如通过修改现有函数最开始的指令 jmp 至新函数地址处,其中涉及的细节比较多、还有一种针对 so 的热更方式修改进程内存中的 GOT 表,跳转至新函数从而达到热更这也正是本文要介绍的方法)。

3 原理介绍

C++程序在运行时有两种方式加载动态连接库:隐式链接和显式链接。 (1) 隐式链接就是在编译的时候使用-l 参数链接的动态库,进程在开始执行时就将动态库文件映射到内存空间中。 (2) 显示链接使用 libdl.so 库的 API 接口在运行中加载和卸载动态库,主要的 API 有 dlopen、dlclose、dlsym、dlerror。

动态修改 GOT 表的方式适用于上面描述中的第一种情况。

针对隐式链接 so 库的函数调用和数据访问方式做了个总结:

(1) 模块内部的非静态函数调用 使用 plt 的方式访问

(2) 模块内部的静态函数调用 使用相对地址的方式访问

(3) 模块间的函数访问 使用 plt 的方式访问

(4) 模块内部的全局变量访问 使用 got 表的方式进行访问

(5) 模块内部的静态变量访问 使用相对地址的方式访问

(6) 模块间的数据访问 使用 got 表的方式进行访问

针对其中函数调用的情况总结下来只有模块内部的静态函数调用不会通过 plt 的方式去调用,另外两种函数调用方式都适用于本文介绍的热更方法。

先来分析下什么是 PLT 和 GOT,通过一个最简单的例子来看下。

94b14579a11ce00a142cce6836bb2c85.png

fun 函数是定义在 lib1.so 里面的函数

9c4a37d0790141b1a48fec4b71a64cc4.png

可以看到调用 fun 的时候并不是直接通过函数地址跳转而是采用了 fun@plt 的方式,这里的 plt 就是上文讲的 plt,那为什么要用这个呢?因为程序在链接动态库的时候并不知道进程启动加载链接库之后函数的地址在哪里,所以上面代码里面的 fun 函数地址是没法确定和填充的,那么什么时候可以确定函数的地址呢?进程运行加载完动态库之后可以找到函数地址,于是就把真正解析函数地址的时机留到了运行时去处理。没错这个 plt 就是用来干这件事的,plt 通过一段代码指令来完成动态库函数的运行时重定位,但是这样的话还面临两个问题:

1 调用函数的地方在代码段,目前的操作系统中默认情况下代码段没有可写属性,不过数据段是可以修改的

2 进程 mmap 动态库到内存里面使用的是 private 方式(从下图代码中可以看出),如果修改了代码段里面的内容,那么就会触发写时复制机制,这些代码段内容就无法做到系统内所有进程共享。

dd2a9671d0979c7f6d7e09811f8a4545.png

bd395d7e190f04c30b5f13826237965c.png

所以 fun 函数地址不能回写到代码段,而应该写到数据段,还记得上面提到的 GOT 吗?没错 GOT 就是在数据段的,GOT 里面会记录 fun 的地址,继续刚刚函数运行的流程往下看。

6dd68ffcf6479c0de361024a79b92655.png

fun@plt 第一条指令就是 jmp 到 fun@got.plt,这个地方记录的的是记录 fun 函数地址的 got 表项,不过第一次调用的时候 fun 在 GOT 里面的表项还没初始化,所以跳转到 fun@plt 后面的指令,后面的指令会真正解析 fun 函数地址,如果每次调用都去解析一遍会比较浪费,所以在第一次解析成功之后会覆盖 got 表项,那么再次调到这里就不用再走后面的解析流程了,0x601018 这个地址是 fun 的 got 表项地址,目前记录的是 plt 后面的指令地址,下图中在正确解析之后覆盖 0x601018 内容更新为 fun 真实的地址。

0cb0df24c7fb586bf6c5a0cb81f8996f.png

等我们再次调用这个函数就直接 jmp 到 fun 函数了。画了个调用流程图:

dfe17c3dd9671766d55d98520c0c681b.png

可以看出来调用完之后 GOT 表项里面就是真实的 fun 函数地址,这个在数据段是可写的,热更的时候把这个表项地址覆盖为新的函数地址即可。

4 数据继承问题

下面是进程在虚拟内存中的布局图:

9b9dc2a68e772f15c95fe2e156b36e05.png

热更新之后对于数据继承问题我把数据拆分成几种类型依次说下: (1) 全局变量 -Wl,-export-dynamic 选项可以把原进程数据导出。 (2) 静态变量 这个随着库加载而确定地址的,而且 linux 有 ASLR 机制每次启动都不一样有随机性,但是离库 mmap 的首地址偏移是固定的,所以使用 offset 方式访问。 (3) 局部非静态变量 这个生命周期和函数调用栈帧一致,函数内部维护堆栈平衡即可,访问权限也只仅限函数调用期间的函数内部访问,所以不需要处理。

5 设计与实现

void fun()->void fun_v_1()

为了访问各种类型的数据和函数我特意定义了全局变量、静态变量、局部静态变量

(1) 首先把 1.c 编译成 lib1.so

f6d917c713e2eec2f2ece1dd66f1a253.png

gcc -fPIC -shared -o lib1.so 1.c

b58b8bb8f6f158abeb31ffebfbae852f.png

(2) a.out 隐式链接 lib1.so,并且调用 fun 函数

gcc main.c -L./ -l1 -g -Wl,-rpath ./ -Wl,-export-dynamic -ldl
./a.out

(3)热更 fun 函数,先定义新函数 fun_v_1 原型和 fun 一样,编译生成 lib2.so

46707d3e7007c938ad712601ccfd9b29.png

(4) a.out 捕获 SIGUSR1 信号,在这个函数里面热更新 fun 函数,赋值 GOT 表

1b3cfe53be3e6c6bc7bc4433cc5fa93a.png

(5)运行效果打印了变量地址可以发现热更前后全局变量和静态变量地址都是一致的,热更之后再次调用 fun 进入到函数 fun_v_1 了

572f619cdb83aabff59ab7fc718252ac.png

6 与 textcode jmp 热更方案对比

这种做法是通过 ptrace attach 到进程,先保存当前的寄存器信息,然后修改 rip 地址跳转到 dl_open 函数,dl_open 函数加载 so 的时候会先执行 init 段代码,然后在 init 里面找到原函数地址修改最开始的指令为 jmp 到新的热更函数,完成之后恢复之前保存的寄存器,detach 进程,后面再次调用原函数就会跳转到新函数从而完成热更,下面对比两种方案的优缺点。

(1) 性能方面 可以看到修改 GOT 表之后性能没有任何损失和之前的调用流程完全一样,textcode jmp 方式多了一次 jmp,另外现代 CPU 会预取指令,jmp 的话会使得预取到的指令失效 (2) 适用场景 修改 GOT 表方式只能应用于隐式加载 so 的场景,textcode jmp 适用的场景更广 (3) 便捷与安全性方面 修改 GOT 表方式修改的是具有可写属性的数据段,对进程没有影响,textcode jmp 修改了原本没有可写属性的代码段需要 attach 到原进程从过程上来说更为凶残(^^)一点

7 总结与后续

从上可知修改 GOT 表确实可以热更 so,但是也有一些限制只能热更使用 GOT 跳转的函数,对于其他函数还是要通过其他的比如入侵式的修改函数指令方式热更,从性能上来说热更前后性能一致,没有多余的指令,但是如果用在多线程里面需要注意一点,第一次调用解析函数覆盖之前热更函数,会出现解析完之后覆盖回去的问题如下图中 A 线程第一次调用这个函数在即将解析成功之前(严格来说执行到第 3 步之后第七步之前),B 线程热更,等 A 线程解析完成返回的时候会覆盖回去,导致热更失效。

f94b021a3f89e945283a1e48eaae11de.png

以上就是对 GOT 热更 so 的探讨,后期的话准备进一步工程化,规范一些流程和操作,使得可以方便的在项目中去使用。

更多干货尽在腾讯技术,官方微信/QQ交流群已建立,交流讨论可加:Journeylife1900、711094866(备注腾讯技术) 。

热更新框架设计系列课程总体介绍:     本系列课程由《热更新框架设计之Xlua基础》、《热更新框架设计之热更流程与热补丁技术》、《热更新框架设计之游戏客户端框架》三套课程组成。 三套课程是一个不可分割有机的整体,笔者带领大家由浅入深逐级深入 ,在领悟热更精髓的基础之上,通过高端架构设计,**完成设计出“低耦合”、“低侵入”、“高复用”性的游戏(VR/AR)客户端热更框架。《热更新框架设计之热更流程与热补丁技术》课程介绍:        本课程为热更框架系列课程中的热更流程设计部分,主要起到承上启下的作用,为后续的框架大整合提供必要技术准备。本作主要通过“客户端与(下载)服务器内部数据核心交换”的示意图,给学员展现当今商业热更框架中,基本与核心的热更流程实现原理与设计思考。    课程主要分为四大部分:    一: 热更流程核心脚本与接口开发、核心编辑器类开发。    二: IIS服务器安装与配置。    三:   lua脚本加载资源与整体热更流程检验。    四: 基于xlua的热补丁技术讲解与案例应用。 温馨提示:     1: 本套课程需要具备一定的框架理解与驾驭能力,为了更好的理解本作,强烈推荐广大学员首先学完必要的前导课程:“UI客户端框架设计”、“AssetBundle 框架设计”、“lua基础与中级篇”。     2:  本课程使用Unity2017版本讲解,但是本课程主要讲解开发思想与具体实现技术,所以对Unity版本不敏感。 学员使用后续的Unity2018/19/2020..... 等版本基本没有影响。一、热更新系列(技术含量:中高级):A:《lua热更新技术中级篇》https://edu.csdn.net/course/detail/27087B:《热更新框架设计之Xlua基础视频课程》https://edu.csdn.net/course/detail/27110C:《热更新框架设计之热更流程与热补丁技术》https://edu.csdn.net/course/detail/27118D:《热更新框架设计之客户端热更框架(上)》https://edu.csdn.net/course/detail/27132E:《热更新框架设计之客户端热更框架(中)》https://edu.csdn.net/course/detail/27135F:《热更新框架设计之客户端热更框架(下)》https://edu.csdn.net/course/detail/27136二:框架设计系列(技术含量:中级): A:《游戏UI界面框架设计系列视频课程》https://edu.csdn.net/course/detail/27142B:《Unity客户端框架设计PureMVC篇视频课程(上)》https://edu.csdn.net/course/detail/27172C:《Unity客户端框架设计PureMVC篇视频课程(下)》https://edu.csdn.net/course/detail/27173D:《AssetBundle框架设计_框架篇视频课程》https://edu.csdn.net/course/detail/27169三、Unity脚本从入门到精通(技术含量:初级)A:《C# For Unity系列之入门篇》https://edu.csdn.net/course/detail/4560B:《C# For Unity系列之基础篇》https://edu.csdn.net/course/detail/4595C: 《C# For Unity系列之中级篇》https://edu.csdn.net/course/detail/24422D:《C# For Unity系列之进阶篇》https://edu.csdn.net/course/detail/24465四、虚拟现实(VR)与增强现实(AR):(技术含量:初级)A:《虚拟现实之汽车仿真模拟系统 》https://edu.csdn.net/course/detail/26618五、Unity基础课程系列(技术含量:初级) A:《台球游戏与FlappyBirds—Unity快速入门系列视频课程(第1部)》 https://edu.csdn.net/course/detail/24643B:《太空射击与移动端发布技术-Unity快速入门系列视频课程(第2部)》https://edu.csdn.net/course/detail/24645 C:《Unity ECS(二) 小试牛刀》https://edu.csdn.net/course/detail/27096六、Unity ARPG课程(技术含量:初中级):A:《MMOARPG地下守护神_单机版实战视频课程(上部)》https://edu.csdn.net/course/detail/24965B:《MMOARPG地下守护神_单机版实战视频课程(中部)》https://edu.csdn.net/course/detail/24968C:《MMOARPG地下守护神_单机版实战视频课程(下部)》https://edu.csdn.net/course/detail/24979
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值