作者简介:王超,UCloud内核团队
前言
热补丁是一种在程序运行时动态修复内存中代码bug的技术。在UCloud,我们使用内核热补丁和应用程序热补丁(也就是进程热补丁)来在线修复核心业务的缺陷和安全漏洞。
多年来我们使用内核热补丁技术避免了系统重启导致的业务中断、保证了操作系统的可用性。属于核心业务组件的应用程序,尤其是单点的虚拟化组件和有状态的应用程序,同样面对高可用的挑战,每次重启都会导致服务受损。然而业界并没有成熟可靠的应用程序热补丁方案可以参考,原因在于应用程序热补丁比内核热补丁更加困难和复杂,比如内核对外提供完整的模块加载功能,可以直接加载内核模块形式的热补丁,而应用程序需要通过外部程序通过一系列对其内存和寄存器的复杂操作来注入动态链接库形式的热补丁;应用程序包含多线程;内核在编译时会被限定使用特定的编译方式,而应用程序的编译方式则更加宽泛;内核的二进制相对简单,而应用程序二进制因为需要链接到多种的动态链接库,本身的结构会更复杂。
经过大量的研究和实践,我们针对应用程序如何免重启修复BUG,自研了一套应用程序热补丁技术而且在UCloud内部已经经过数十万台次修复验证。后面通过一系列文章分享其技术实现。本文先介绍一种简单实用的应用程序热补丁技术,不少场景下采用该方法编写几行代码即可免修复应用程序BUG。
原理
一般来说,应用程序热补丁的流程是,首先通过编译器将热补丁源码制作成可加载的动态链接库,然后通过加载程序将热补丁加载到目标进程的地址空间,最后在进行一致性模型检查确认安全的情况下,把原始代码替换成新的代码,完成在线修复的过程。
下面我们分别介绍热补丁本身和热补丁加载程序,热补丁本身是因patch而异的,加载程序是通用的。
假设我们有热补丁加载程序Loader、目标进程T、热补丁patch.so,目标程序的func函数替换为func_v2。
热补丁:
- 编写热补丁源码,编译成动态链接库的格式的热补丁patch.so,patch.so中包含func和func_v2的信息。
- 热补丁patch.so在被加载程序Loader加载到目标进程T地址空间的过程中,通过dlsym调用找到func的地址,并将func的入口指令改为可写,同时改变为跳转到func_v2。
- 至此,所有对func的调用都会被重定向到func_v2,func_v2执行完毕后返回,程序继续运行。
- 如图所示:
热补丁加载程序:
- 加载程序Loader找到目标进程T的dlopen函数入口地址。
- Loader通过ptrace依附到目标进程T,Loader将热补丁的名字放入放入目标进程T的堆栈,将IP寄存器设置为dlopen函数的地址。
- Loader使目标进程T继续运行。因为IP寄存器已经设置为dlopen函数的入口,目标进程T会调用dlopen把热补丁加载到T的地址空间中。
- 如图所示:
了解原理之后,我们一步步实现一种简单的基于x86_64的热补丁。
(对于需要制作热补丁的同学,只需自己编写patch.so,而Loader是通用的。patch.so编写可以参考下面的例子,往往只需几行代码做相应替换。)
实现
热补丁
1.目标进程T执行dlopen的过程中,通过预先在热补丁(动态链接库)中写入的constructor函数,在加载过程中函数func_v1替换函数func。
static void __attribute__((constructor)) init(void)
{
int numpages;
void *old_func_entry, *new_func_entry;
old_func_entry = dlsym(NULL, "func");
new_func_entry = dlsym(NULL, "func_v2");
#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#define PAGE_MASK (~(PAGE_SIZE-1))
numpages = (PAGE_SIZE - (old_func_entry & ~PAGE_MASK) >= size) ? 1 : 2;
mprotect((void *)(old_func_entry & PAGE_MASK), numpages * PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC);
/*
* Translate the following instructions
*
* mov $new_func_entry, %rax
* jmp %rax
*
* into machine code
*
* 48 b8 xx xx xx xx xx xx xx xx
* ff e0
*/
memset(old_func_entry,