引言

千呼万唤始出来,从今天起,《UCloud技术大观园》系列正式开张,撒花╭(●`∀´●)╯!

UCloud生而为云,一直专注在云计算的泥潭里摸爬滚打,踩过数不清的坑,写过数不清的BUG。所幸,在不断的试错中,也锤炼出一些能在江湖傍身的大杀器。这些经过千锤百炼的大杀器和宝贵的踩坑经验,一起成为今天UCloud的核心科技。

现在,我们将在《UCloud技术大观园》系列里,把这些核心科技全部开放出来,毫无保留,逐一为大家讲解,哪些坑是我们已经踩过的,引以为诫,哪些是优质的技术实践经验,值得借鉴。

我们始终相信——开放,才是技术的本心。

本篇作为《UCloud技术大观园》系列的开篇,聚焦UCloud应用程序热补丁技术,将介绍一种简单实用的应用程序热补丁技术。不少场景下,用该方法编写几行代码即可免重启修复应用程序BUG!

那,我们开始吧~

前言

应用程序,作为核心业务组件,每天都面临着严峻的高可用挑战,每次重启,都会导致服务受损。尤其是单点的虚拟化组件和有状态的应用程序,一旦重启,影响更甚。

热补丁,一种在程序运行时动态修复内存中代码bug的技术,能避免系统重启导致的业务中断、有效保证操作系统的可用性。

经过大量的研究和实践,UCloud从0到1,自研了一套应用程序热补丁技术。千锤百炼出真金,经过内部数十万台次修复验证,UCloud应用程序热补丁技术已自成体系,成为UCloud核心黑科技之一。

原理

一般来说,应用程序热补丁的流程是,首先通过编译器将热补丁源码制作成可加载的动态链接库,然后通过加载程序将热补丁加载到目标进程的地址空间,最后在进行一致性模型检查确认安全的情况下,把原始代码替换成新的代码,完成在线修复的过程。

下面我们分别介绍热补丁本身和热补丁加载程序,热补丁本身是因patch而异的,加载程序是通用的。

假设我们有热补丁加载程序Loader、目标进程T、热补丁patch.so,目标程序的func函数替换为func_v2。

热补丁

  1. 编写热补丁源码,编译成动态链接库的格式的热补丁patch.so,patch.so中包含func和func_v2的信息。

  2. 热补丁patch.so在被加载程序Loader加载到目标进程T地址空间的过程中,通过dlsym调用找到func的地址,并将func的入口指令改为可写,同时改变为跳转到func_v2。

  3. 至此,所有对func的调用都会被重定向到func_v2,func_v2执行完毕后返回,程序继续运行。

  4. 如图所示:

热补丁加载程序

  1. 加载程序Loader找到目标进程T的dlopen函数入口地址。

  2. Loader通过ptrace依附到目标进程T,Loader将热补丁的名字放入放入目标进程T的堆栈,将IP寄存器设置为dlopen函数的地址。

  3. Loader使目标进程T继续运行。因为IP寄存器已经设置为dlopen函数的入口,目标进程T会调用dlopen把热补丁加载到T的地址空间中。

  4. 如图所示:

了解原理之后,我们一步步实现一种简单的基于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, 0x48, 1);memset(old_func_entry + 1, 0xb8, 1); memcpy(old_func_entry + 2, &new_func_entry, 8); memset(old_func_entry + 10, 0xff, 1);memset(old_func_entry + 11, 0xe0, 1);}
    

热补丁加载程序

  1. Loader得到目标进程T地址空间中dlopen入口地址

    1.1. dlopen函数有libdl提供,并不是所有的程序都加载libdl,幸运的是,libc中提供了同样功能的函数libc_dlopen_mode,并且接受的参数和dlopen相同。除非特殊情况,所有程序都会加载libc。所以我们需要找到libc_dlopen_mode在目标进程T地址空间中的函数入口地址。

    1.2. 我们知道,不同进程中libc会被加载到不同的基地址,但是libc中函数的地址相对基地址的偏移是不变的。

    1.3. 通过Loader和目标进程T的/proc/pid/maps,我们可以得到libc在Loader和目标进程T中加载的基地址。通过Loader运行dlsym,我们可以得到Loader中的libc_dlopen_mode的地址。这样我们可以得到目标进程T中libc_dlopen_mode的地址(Loader_dlopen - Loader_libc + T_libc)。

    / Take a hint and find start addr in /proc/pid/maps /static unsigned long find_lib_base(pid_t pid, char *so_hint){FILE *fp;char maps[4096], mapbuf[4096], perms[32], libpath[4096];char *libname;unsigned long start, end, file_offset, inode, dev_major, dev_minor;sprintf(maps, "/proc/%d/maps", pid);fp = fopen(maps, "rb");if (!fp) {fprintf(stderr, "Failed to open %s: %s\n", maps, strerror(errno));return 0;}while (fgets(mapbuf, sizeof(mapbuf), fp)) {sscanf(mapbuf, "%lx-%lx %s %lx %lx:%lx %lu %s", &start,&end, perms, &file_offset, &dev_major, &dev_minor, &inode, libpath);libname = strrchr(libpath, '/');if (libname)libname++;elsecontinue;if (!strncmp(perms, "r-xp", 4) && strstr(libname, so_hint)) {fclose(fp);return start;}}fclose(fp);   return 0; }loader_libc = find_lib_base(getpid(), “libc-c”);T_libc = find_lib_base(T_pid, “libc-“);Loader_dlopen = (unsigned long)dlsym(NULL, “__libc_dlopen_mode”);T_dlopen = T_libc + (Loader_dlopen - Loader_libc);
  2. Loader对目标进程T使用ptrace attach,并保存T此时的寄存器信息。

    static int ptrace_attach(pid_t pid){int status;if (ptrace(PTRACE_ATTACH, pid, NULL, NULL)) {fprintf(stderr, "Failed to ptrace_attach: %s\n", strerror(errno));return 1;}if (waitpid(pid, &status, __WALL) < 0) {fprintf(stderr, "Failed to wait for PID %d, %s\n", pid, strerror(errno));return 1;}return 0;
    }static int ptrace_call(pid_t pid, unsigned long func_addr, unsigned long arg1, unsigned long arg2, unsigned long *func_ret){…memset(&saved_regs, 0, sizeof(struct user_regs_struct));ptrace_getregs(pid, &saved_regs);…}
  3. 将目标进程T的%RIP指向dlopen,热补丁的名字的字符串放入堆栈,字符串的地址写入%rdi,RTLD_NOW的值写入%rsi作为dlopen的flag。同时把dlopen返回地址设置为非法地址0x0(把0x0压入栈中),这样Loader可以捕获目标进程T产生的SIGSEGV信号进而重新获得T的控制权。

      unsigned long invalid = 0x0;regs.rsp -= sizeof(invalid);ptrace_poketext(pid, regs.rsp, ((void *)&invalid), sizeof(invalid));ptrace_poketext(pid, regs.rsp + 512, filename, strlen(filename) + 1);regs.rip = dlopen_addr;regs.rdi = regs.rsp + 512;regs.rsi = RTLD_NOW;ptrace_setregs(pid, &regs);
  4. Loader使目标进程T继续运行。当T执行完dlopen之后,T产生的SIGSEGV信号被Loader捕获,Loader重新获得T进程的控制权。

static int ptrace_cont(pid_t pid){int status;if (ptrace(PTRACE_CONT, pid, NULL, 0)) {fprintf(stderr, "Failed to ptrace_cont: %s\n", strerror(errno));return 1;}if (waitpid(pid, &status, __WALL) < 0) {fprintf(stderr, "Failed to wait for PID %d, %s\n", pid, strerror(errno));return 1;}
return 0;}

5. Loader通过读取目标进程T此时的%rax寄存器得到dlopen的返回值,恢复T最开始的执行状态,最后释放对T的控制

  ptrace_getregs(pid, &regs);dlopen_ret = regs.rax;ptrace_setregs(pid, &saved_regs);ptrace_detach(pid);

至此对目标进程T的热补丁就完成了。下面我们看一个例子。

验证

假设我们运行target程序,每隔一秒打印Hello一次:

# ./target
Hello
Hello
…

target程序由target本身和libold.so组成,分别代码如下:

/* target.c */
#include <unistd.h>
#include "old.h"int main() {for (;;) {print();sleep(1);}
}/* old.c */
#include <stdio.h>void print(void)
{printf("Hello\n");
}

编译

gcc -fPIC --shared old.c -o libold.so
gcc target.c ./libold.so -o target

我们想要修改print函数,变成打印“Goodbye”。我们需要编写热补丁new.c,并添加新函数和constructor:

/* new.c */
#include <stdio.h>#include <string.h>
#include <sys/mman.h>
#include <dlfcn.h> print_v2(void)
{printf("Goodbye\n");
}static void __attribute__((constructor)) init(void)
{ int numpages;void *old_func_entry, *new_func_entry;old_func_entry = dlsym(NULL, print);new_func_entry = dlsym(NULL, print_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);memset(old_func_entry, 0x48, 1);memset(old_func_entry + 1, 0xb8, 1); memcpy(old_func_entry + 2, &new_func_entry, 8); memset(old_func_entry + 10, 0xff, 1);memset(old_func_entry + 11, 0xe0, 1);
}

编译:

gcc -fPIC --shared new.c -ldl -o libnew.so

然后通过加载程序对target进程打入热补丁libnew.so,最后我们对target程序打入这个热补丁,观察变化:

# ./target
Hello
Hello
Goodbye
Goodbye
…

我们发现热补丁确实改变了print函数,最后通过gdb进一步确认,可以看出print函数的入口被修改成48 b8 dc b6 15 a9 c1 7f 00 00 ff e0,与我们的预期相符:

(gdb) disas /r print
Dump of assembler code for function print:0x00007fc1a98f456c <+0>:     48 b8 dc b6 15 a9 c1 7f 00 00   movabs $0x7fc1a915b6dc,%rax0x00007fc1a98f4576 <+10>:    ff e0   jmpq   *%rax # 这里print在入口处跳转到0x7fc1a915b6dc这个地址
…
(gdb) info symbol 0x7fc1a915b6dc
print_v2 in section .text of /root/process-hotupgrade/test/libnew.so # 0x7f2ea417971c这个地址就是print_v2函数的地址

总结

我们介绍了应用程序热补丁的基本原理,实践了一个应用程序热补丁demo。此类热补丁适用于动态替换共享链接库中的可见函数,可以修复例如glibc “GHOST漏洞”(CVE-2015-0235)等等,在UCloud我们利用热补丁修复了若干缺陷,在用户没有感知的情况下把bug快速及时的修复。这些热补丁修复程序里,绝大多数代码是通用的,只需少数几行做特殊替换。

上文介绍的热补丁技术对于适用的场景非常理想,简单可靠,但存在几个缺点:

  • 手写热补丁代码门槛较高,特别是被修复函数的依赖函数链较长时手写热补丁很容易出错
  • 无法修复局部函数和局部变量(只能修复全局可见的函数和变量)

后面的文章我们会介绍如一种更加先进的应用程序热补丁技术。

应用程序热补丁(一):如何用几行代码打造应用程序热补丁相关推荐

  1. ds--8600使用手册_我如何用57行代码复制一个价值8600万美元的项目

    ds--8600使用手册 by Tait Brown 泰特·布朗(Tait Brown) 我如何用57行代码复制一个价值8600万美元的项目 (How I replicated an $86 mill ...

  2. 【python】一个目录里面多个python程序文件,统计一下里面有多少行代码。即分别列出:代码、空行、注释的行数。

    一个目录里面多个python程序文件,统计一下里面有多少行代码.即分别列出:代码.空行.注释的行数. 题目 代码 结果 题目 一个目录里面多个python程序文件,统计一下里面有多少行代码.即分别列出 ...

  3. 2行代码实现小程序分享到朋友圈功能

    期盼已久的小程序直接分享到朋友圈的功能,官方终于支持了.今天就来带大家实现小程序分享到朋友圈的功能.代码很简单. 老规矩,以图为证 新加分享到朋友圈的按钮 分享到朋友圈的效果 分享成功 打开朋友圈分享 ...

  4. 程序员的小技能,1行代码修改开机密码、1张图片让你电脑死机

    程序员很忙,忙着敲代码debug,程序员有时候也很闲,闲下来的就是就开始自黑,自黑的段子越来越多还被编出了一首诗:"格子衬衫双肩包.钱多话少死得早,晚睡晚起加班多,没事就和产品吵" ...

  5. 教你如何用91行代码实现一朵玫瑰花的绘制

    教你如何用91行代码实现一朵玫瑰花的绘制. 文章目录 教你如何用91行代码实现一朵玫瑰花的绘制. 主体: 初始位置的设定 绘制花朵形状: 绘制花枝形状 绘制一个绿色叶子 要用到的工具: Pycharm ...

  6. 想了解直播系统开发美颜的原理,先来学习如何用1行代码实现人脸识别

    想了解直播系统开发美颜的原理,先来学习如何用1行代码实现人脸识别 环境搭建: 1. 安装 Ubuntu17.10 > 安装步骤在这里 2. 安装 Python2.7.14 (Ubuntu17.1 ...

  7. 铅笔素描算法_如何用10行代码将任何图像变成铅笔素描

    铅笔素描算法 by Rishav Agarwal 通过里沙夫·阿加瓦尔 如何用10行代码将任何图像变成铅笔素描 (How to turn any image into a pencil sketch ...

  8. 应用程序热补丁(一): 几行代码构造免重启修复补丁

    作者简介:王超,UCloud内核团队 前言 热补丁是一种在程序运行时动态修复内存中代码bug的技术.在UCloud,我们使用内核热补丁和应用程序热补丁(也就是进程热补丁)来在线修复核心业务的缺陷和安全 ...

  9. 如何用 60 行代码爬取知乎神回复?

    作者 | 强哥 责编 | 郭芮 知乎上经常会有很多令人忍俊不禁的神回复,初看之下拍案叫绝,细思之下更是回味无穷.本文就来介绍下如何爬取知乎的神回复,揭晓其背后的原理. 知乎神回复都有些什么特点呢?我们 ...

  10. lstm代码_贼好理解,这个项目教你如何用百行代码搞定各类NLP模型

    机器之心报道 参与:思源.贾伟 NLP 的研究,从词嵌入到 CNN,再到 RNN,再到 Attention,以及现在正红火的 Transformer,模型已有很多,代码库也成千上万.对于初学者如何把握 ...

最新文章

  1. 每天一个linux命令(23):Linux 目录结构
  2. java中synchronized的用法详解
  3. Promise 的基本使用 与 Ajax的jQuery封装
  4. 清除浮动小记,兼容Ie6,7
  5. 每日签到html特效,前端这种连续签到的效果要怎么写
  6. 2021年ICT趋势白皮书:不确定性中的确定
  7. 不购买2021新款MacBook Pro的理由
  8. 图像处理界双线性插值算法的优化
  9. mescroll报错
  10. idea 调用webservice接口
  11. [日推荐]『小恩故事』育儿助手!
  12. .chm格式的电子书打开是空白的解决办法
  13. 爬取北京市公交线路信息
  14. 远程教育两周,家长崩溃简史
  15. linux编辑pdf文件内容,PDF修改文字的步骤
  16. 转屏动画 - 安卓R
  17. 安全编码实践:什么是安全编码标准?
  18. 使用scrapy爬虫,爬取17k小说网的案例-方法一
  19. nodejs图片处理工具gm用法
  20. 爬虫数据分析-----matplotlib图形展示

热门文章

  1. mybatis实现延迟加载多对一
  2. Weblogic常见故障常:JDBC Connection Pools
  3. 国产免费的visio替代品edraw mind map,用来话流程图够用了
  4. ArcGIS 10 SDE for ORACLE ---迁移 (1)
  5. 如何将SL的image保存到SL的独立存储文件系统
  6. WPF下通过附加属性实现单实例启动
  7. 使用 SqlDependency 对象缓存数据以提高性能
  8. 每周百万封业务邮件的服务器不知道为啥就down掉了?
  9. 改变浏览器窗口的大小和位置
  10. Velocity 语法(转)