测试打桩的方法

测试打桩常用有两种方法:

  1. 通过宏定义来实现
  2. 通过 jmp 指令来实现

通过 jmp 实现打桩

实现代码如下:

  1. #define JMP_LEN 5
  2. #define JMP_INS 0xe9 // 该jmp指令的操作数是相对地址
  3. typedef char backup_t[JMP_LEN];
  4. /**
  5. * 在运行期间打桩
  6. * @param [in]old_func 旧的函数
  7. * @param [in]new_func 新的函数
  8. * @param [out]backup 用于备份旧的函数的入口指令
  9. */
  10. void install_stub(void *old_func, void *new_func, backup_t *backup)
  11. {
  12. char jmp_cmd[JMP_LEN] = {0};
  13. unsigned offset = 0; // 新旧函数相对地址
  14. void *page_addr;
  15. int ret = 0;
  16. /* 将函数入口所在页设为可读可写可执行 */
  17. page_addr = (void*)((uint64_t)old_func & ~(getpagesize() - 1));
  18. ret = mprotect(page_addr, getpagesize(), PROT_WRITE | PROT_READ | PROT_EXEC);
  19. if (ret) {
  20. fprintf(stderr, "mprotect failed.\n");
  21. }
  22. /* 替换入口指令 */
  23. jmp_cmd[0] = JMP_INS;
  24. offset = (unsigned long)new_func - (unsigned long)old_func - JMP_LEN;
  25. memcpy(&jmp_cmd[1], &offset, sizeof(offset));
  26. memcpy(*backup, old_func, JMP_LEN);
  27. memcpy(old_func, jmp_cmd, JMP_LEN);
  28. }

运行时打桩是否破坏调用关系

结论是:不会

首先看下简单的调用关系。对于如下代码:

  1. int foo(int a) {
  2. ...
  3. }
  4. int main() {
  5. int res = foo(7);
  6. }

汇编代码如下:( gcc -S -O0 -m32 )

  1. foo:
  2. pushl %ebp
  3. movl %esp, %ebp
  4. subl $16, %esp # 申请栈空间
  5. ...
  6. leave # 释放栈空间,等价于:movl %ebp, %esp; pop %ebp
  7. ret # 等价于:pop %eip
  8. main:
  9. pushl $7 # 传参
  10. call foo # 等价于:pushl %eip, %esp; movl $foo, %eip

从上面的汇编代码可以得知:
函数调用者将返回地址写到栈里面;函数被调者在返回时只需要将返回地址从栈中拿出即可
因此,通过 jmp 进行打桩,不会对函数的调用关系产生影响