深入了解C++的So动态更新

一月 19, 2024 [C/C++] #C++ #obsidian #操作系统原理 #编译链接

深入了解C++的So动态更新

深入了解C++的So动态更新!今天我将与大家分享,作为鹅厂后台开发工程师的经验,我们正在研究基于So动态链接库的C++进程热更新方案。热更新是在程序运行时修复内存中的代码错误,而无需终止程序运行的技术。主题包括技术探索、相关知识回顾、So热更的基本原理以及热更方式的演示等方面的知识点。

(迁移注:原图片缺失 assets/c9ef31ceacd64468ba15afd1a7dd2ea5tplv-obj10801439-20240119102458-y5x0hnp.image,已移除)> 📄 [图片文字版] ptrace 控制目标进程:ATTACH 使 tracee 停止;SETREGS 改寄存器、CONT 恢复运行。原型 ptrace(request, pid, addr, data)。参数:TRACEME 被追踪;PEEKTEXT/PEEKDATA 读一字;POKETEXT/POKEDATA 写一字;PEEK/POKEUSER 用户区读写;GETREGS/SETREGS 读写寄存器;ATTACH 追踪并停止;CONT 恢复(data 非空作信号);DETACH 脱离。> 📄 [图片文字版] 找 so 函数地址:读 /proc/pid/maps 或 pmap -p 8031。系统 so 的 section 全加载可访问、普通 so 才可 open。Func 查找:读 Ehdr → 按 e_shnum 读 section 数组 → e_shstrndx 找名字表 → 定位 .dynsym/.dynstr/.rela.plt 等;已访问:从 .dynsym 按名找 st_value;未访问:.rela.plt 取条目,地址=ELF 起始+r_offset。> 📄 [图片文字版] 访问目标进程寄存器与内存:GETREGS 读通用寄存器到 data,SETREGS 用 data 修改。x86-64:rdi/rsi/rdx/rcx/r8/r9 传前 6 参、rax 为返回值。内存读写用 PEEKTEXT/POKETEXT 或 process_vm_readv/writev。执行指令:Save 源寄存器和内存 → 改成目标所需 → PTRACE_CONT 执行 → trap 后还原。例 mmap:rax=9、rdi~r9 依次填参、rip 指向 glibc 起始地址执行。> 📄 [图片文字版] 注入 so:调 dlopen(pathname, mode)。步骤:找 __libc_dlopen_mode 地址 → 填寄存器和内存指令 → 执行 callq *%rax。“鸡和蛋”:pathname 要内存而分配内存的指令也要占内存——借系统 so 内存中 ELF 头 8 字节存指令。流程:按 so 名/长度估内存 → save 寄存器执行 mmap → 返回地址写 so 名 → 调 __libc_dlopen_mode → 取 handle → write 回寄存器恢复现场。> 📄 [图片文字版] 确定待改函数入口:已被调用过——.got 存真实地址,.dynsym 可查符号,其地址即入口;未调用过——先跳 .plt 桩再到 .got.plt 条目,首次调用条目存解析地址,动态链接器查 .rela.plt 后转交对应库,函数地址 = .rela.plt 的 r_offset + ELF 起始地址。再把旧函数 TEXT 段改为跳新函数指令(截图为 MoveR 函数 prologue 反汇编)。> 📄 [图片文字版] 按新老地址偏移选指令:iCheckOffset = abs(新地址 − (旧地址+5)),为 jmp+4 字节 offset 留位;近距离 [jmp offset],远距离 [movabs funcaddr,%rax; jmp *%rax]。原子性方案1:待热更函数若正在执行则停止热更——按 rip/rbp 遍历调用栈(FillCallerStack 读栈帧),若栈中 addr 落在替换代码范围内则终止本次热更。> 📄 [图片文字版] 原子性方案2:提前插桩。gcc -pg 在函数开头插入 call mcount()(5 字节,原用于调用统计),借其实现原子补丁。示例 testA:push %rbp; mov %rsp,%rbp; callq mcount@plt; …; retq。覆盖规则:jmp 正好 5 字节,直接覆盖 mcount 即原子;超 32 位用跳板,可在 movabs 前加 mov %rbp,%rsp; pop %rbp。> 📄 [图片文字版] 数据继承:进程虚拟内存分布——0xc0000000 内核空间(用户不可见);用户栈(运行时创建,%esp 指示);共享库映射(0x40000000);运行时堆(brk);读写段 .data/.bss;只读段 .init/.text/.rodata(0x08048000 起)。堆上变量:类成员增删或改类型会越界/值错乱(示例 Logic 类新增 m_uNewOne1、m_uNewOne2);静态变量同样要处理(LOG_DEBUG 打印 m_uStatic)。> 📄 [图片文字版] 业界热更新方案:1) 共享内存,技术点为数据恢复(C++ 对象:指针、容器、多态虚表、全局数据、静态成员);配图虚拟内存:内核态(页表/task&mm、内核栈、内核代码数据)+用户态(用户栈、共享库映射、堆、.bss/.data/.text)。2) Lua 虚拟机,技术点为 C++ 对象与 lua 绑定、C++ 函数导出;时序 lua_newstate → lua_dofile → lua_parse → lua_pcall → luaV_execute。> 📄 [图片文字版] 业界方案(续):3) 进程热替换,技术点:新进程 fork、Listen FD 迁移、读/写数据迁移(参考《Sidecar 平滑升级方案探索》),时序图含 switch_sock、AcceptNewConnection 等连接迁移。4) 动态链接库,技术点:新老函数地址查找、So 注入、新老函数替换;示意 program(0x400000)/libfoo.so(0x600000)/libbar.so(0x800000) 的 PLT 条目,新库 hook_foo_func 替换 foo_func。> 📄 [图片文字版] 动态链接库业界方案:内核热补丁以 SUSE kGraft、Red Hat kpatch、Oracle Ksplice 为代表——补丁后函数开头改 call ftrace/kpatch 检查点,命中转 New Function,可不重启修复内核缺陷。进程热补丁:新函数替换旧函数,Android 常用 plthook、android-inline-hook;修复后 func_v1 改为 jump func_v2→return。> 📄 [图片文字版] ELF 结构:Ehdr 含 e_ident(0x7f ELF)、e_type/e_machine/e_entry、e_phoff/e_shoff、e_shnum、e_shstrndx;Shdr 含 sh_name/sh_addr/sh_offset/sh_size 等。readelf -h libWorldHotfix.so:ELF64、DYN、X86-64、8 程序头、42 节头、节名表索引 41;readelf -l 可看 PHDR/LOAD/DYNAMIC 等 segment 映射。> 📄 [图片文字版] 地址无关代码 PIC:可在内存任意位置正确运行、不受加载位置影响。示例:program(0x400000) 经 PLT(bar_func 0x800300、foo_func 0x603400)相对寻址调用 libfoo.so(0x600000)/libbar.so(0x800000)。要点:偏移量固定;GOT 运行期填充查真实地址;库 .data/.text 多进程共享、.got 独享;外部符号加载时由动态链接器填 .got;lazy binding 走 .plt→.got.plt。