记录一种在C语言中的打桩实现及原理

1 场景

考虑一个由多个模块组成的工程,如

.
└── wsw
    ├── m1
    ├── m2
    └── ...

假设 m2 中的接口会调用 m1 中的接口。

当要初步验证 m2 中各接口功能时,由于 m1 中各接口的运行往往依赖 m1 模块上下文(而在验证 m2 时往往不想参与 m1 模块的流程),此时需跳过或用其他接口模拟 m1 接口所产生的数据。

一种直接的方法是编写与 m1 中函数同名的函数,并指定 m2 链接新的接口。

若原工程管理比较复杂,不太好将 m1 从工程文件中中拆分出去,则可选择用“打桩”的方式跳过 m1 中的接口。

                                      _m1_f
m2_fn              m1_fn            +->+--------+
+----------+    +->+------------+ ② |  |        |
|   ...    |    |  | jmp _m1_fn |---+  |   .    |
+----------+  ① |  +------------+      |   .    |
|call m1_fn|----+  |    ...     |      |   .    |
+----------+<--+                       +--------+
|   ...    |   |                  ③    | return |
               +-----------------------+--------+

上图只表示“打桩”后程序执行的大体逻辑流程,不代表各函数在进程中的实际位置和实际执行过程。

2 原理

正如前一节图中所示,实现“打桩”的一种方式是修改函数头部为一条跳转指令,该跳转指令跳转到桩函数(_m1_f)中。下面大体分析下其可行原理。

[1] 修改代码段即函数头部(基于OS的应用程序可通过具 level0 权限的系统调用如 mprotect完成)

[2]在接口开始处添加跳转指令不会破坏函数栈帧关系

+=======+----------------
|       |      ↑
|       |    stack
|       |      ↓
+=======+----------------
|.......|
+=======+----------------
|m2_fn()|     ↑         ↑
+=======+m2_snippets    |
|.......|     ↓         |
+=======+-----------    |
|m1_fn()|     ↑         |
+=======+m1_snippets  .text
|.......|     ↓         |
+=======+-----------    |
|_m1_f()|     ↑         |
+=======+_m1_snippets   |
|.......|     ↓         |
+=======+-----------    |
|.......|               ↓
+=======+----------------
进程空间

+=======+----------------------------------
|   .   |m2_fn 调 _m1_f         ↑
|   .   |栈中备份PC             |m2_fn 栈帧
|   .   |修改PC跳转_m1_f        |
|m1_fn()|                       ↓
+-------+----------------------------------
|   .   |m1_fn return 时        ↑
|   .   |弹出栈中备份PC         |
|   .   |返回到调用 m1_fn() 处  |m1_fn 栈帧
|return |                       ↓
+=======+----------------------------------
|.......|
+=======+----------------  <--- running
|m2_fn()|     ↑         ↑
+=======+m2_snippets    |
|.......|     ↓         |
+=======+-----------    |
|m1_fn()|     ↑         |
+=======+m1_snippets  .text
|.......|     ↓         |
+=======+-----------    |
|_m1_f()|     ↑         |
+=======+_m1_snippets   |
|.......|     ↓         |
+=======+-----------    |
|.......|               ↓
+=======+----------------
函数运行时维护的栈帧

+=======+-----------------------------------
|   .   |m2_fn 调 m1_fn       ↑
|   .   |栈中备份PC            |m2_fn 栈帧
|   .   |修改PC跳转m1_fn       |
|m1_fn()|                     ↓
+-------+-----------------------------------
|   .   |m1_fn 开始语句为      ↑
|   .   |跳转到 _m1_f 处       |
|   .   |不会运行其内开辟       | m1_fn 无栈帧
|   .   |栈帧的语句            ↓
+-------+-----------------------------------
|   .   |_m1_f return时       ↑
|   .   |弹出m2_fn栈中备份PC   | _m1_f 栈帧
|   .   |返回到调m1_fn处       |
| return|跟在m1_fn中return一样 ↓
+=======+-----------------------------------
|.......|
+=======+
打桩对栈帧无破坏力

3 实现

略加体验一下吧。

3.1 做一些跨平台的封装
/**
 * wsw_mprotect.h
 * snippets on modify protections.
 *
 * lxr, 2020.08 */
#ifndef _WSW_MPROTECT_H_INCLUDED_
#define _WSW_MPROTECT_H_INCLUDED_

#include <sys/mman.h>

#define WSW_MPROTECT_RX             (PROT_READ | PROT_EXEC)
#define WSW_MPROTECT_RWX            (PROT_READ | PROT_WRITE | PROT_EXEC)
#define wsw_mprotect(a, len, prot)  mprotect(a, len, prot)

#endif /* _WSW_MPROTECT_H_INCLUDED_ */

/**
 * wsw_page.h
 * snippets on memory page.
 *
 * lxr, 2020.011 */
#ifndef _WSW_PAGE_H_INCLUDED_
#define _WSW_PAGE_H_INCLUDED_

#ifdef WSW_HAS_GETPAGESIZE
#include <unistd.h>
#define wsw_getpagesize()   getpagesize()

#else
#define wsw_getpagesize()   (4096)
#endif

#endif /* _WSW_PAGE_H_INCLUDED_ */
3.2 组装打桩接口
/**
 * wsw_stub.h
 * snippets on stub.
 *
 * lxr, 2020.11 */

#ifndef _WSW_STUB_H_INCLUDED_
#define _WSW_STUB_H_INCLUDED_

#include <stdlib.h>
#include "wsw_main.h"

/* if the jmp instruction changed,
   then the len follows the change. */
#define WSW_STUB_JMPQ       0xe9
#define WSW_STUB_JMPQ_LEN   (1)
#define WSW_STUB_JMPQ_OPLEN (4)
#define WSW_STUB_LEN        (WSW_STUB_JMPQ_LEN + WSW_STUB_JMPQ_OPLEN)

/** 
 * instr|operands
 * +----+-+-+-+-+
 * |jmpq| | | | |
 * +----+-+-+-+-+
 * 0    1 2 3 4 5
 * if the jmp instruction changed,
 * then type of operand for jmp follows the changed. */
typedef WSW_UINT32_T                WSW_STUB_JMPQ_OP_T;
typedef struct wsw_stub_manage_s    WSW_STUB_MAN_S;

extern WSW_STUB_MAN_S *
wsw_stub_init(size_t stub_len);

static wsw_inline void 
wsw_stub_deinit(WSW_STUB_MAN_S *stubm)
{
    free(stubm);
    return ;
}

extern int 
wsw_stub_reset(WSW_STUB_MAN_S *stub_m);

extern int 
wsw_stub_set(WSW_STUB_MAN_S *stub_m, void *fn, void *stub_fn);

#endif /* _WSW_STUB_H_INCLUDED_ */

/**
 * wsw_stub.c
 * snippets on stub.
 *
 * lxr, 2020.11 */

#include "wsw_stub.h"

#define WSW_STUB_MAN_SSIZE  sizeof(WSW_STUB_MAN_S)

static unsigned _wsw_stub_pagesize;

struct wsw_stub_manage_s {
    void   *fn;
    char   *stub;
    size_t  len;
};

static wsw_inline WSW_UINTPTR_T 
wsw_alignpage(WSW_UINTPTR_T v)
{
    unsigned pg;

    pg = _wsw_stub_pagesize;
    return (v & ~((WSW_UINTPTR_T) (pg - 1u)));
}

static wsw_inline int 
wsw_mprotect_write(void *fn)
{
    int      ret;
    void    *ap;
    size_t   pg;

    pg = _wsw_stub_pagesize;
    ap = (void *)wsw_alignpage((WSW_UINTPTR_T) fn);

    ret = wsw_mprotect(ap, pg, WSW_MPROTECT_RWX);
    WSW_IF_EXPS_RETURN(WSW_ERR_SYSCALL == ret, ret);

    return WSW_ERR_NONE;
}

static wsw_inline int 
wsw_mprotect_recovery(void *fn)
{
    int      ret;
    void    *ap;
    size_t   pg;

    pg = _wsw_stub_pagesize;
    ap = (void *)wsw_alignpage((WSW_UINTPTR_T) fn);

    ret = wsw_mprotect(ap, pg, WSW_MPROTECT_RX);
    WSW_IF_EXPS_RETURN(WSW_ERR_SYSCALL == ret, ret);

    return WSW_ERR_NONE;
}

WSW_STUB_MAN_S *
wsw_stub_init(size_t stub_len)
{
    WSW_STUB_MAN_S *m;

    m = wsw_calloc(WSW_STUB_LEN + stub_len);
    WSW_IF_EXPS_RETURN(!m, NULL);

    m->len  = stub_len;
    m->stub = wsw_memb(m) + WSW_STUB_MAN_SSIZE;
    _wsw_stub_pagesize = wsw_getpagesize();

    return m;
}

int 
wsw_stub_set(WSW_STUB_MAN_S *stub_m, void *fn, void *stub_fn)
{
    int     ret;
    void   *opaddr;
    size_t  jmpq_op_len;

    WSW_IF_EXPS_RETURN(!stub_m || !fn || !stub_fn, WSW_ERR_BADPARAM);

    ret = wsw_mprotect_write(fn);
    WSW_IF_EXPS_RETURN(WSW_ERR_NONE != ret, ret);

    stub_m->fn  = fn;
    jmpq_op_len = stub_m->len;
    memcpy(stub_m->stub, fn, jmpq_op_len);

    *((WSW_UINT8_T *) fn) = WSW_STUB_JMPQ;
    opaddr = wsw_memb(fn) + WSW_STUB_JMPQ_LEN;
    /* 注释见后文 */
    *((WSW_STUB_JMPQ_OP_T *) opaddr) =             \
    wsw_memb(stub_fn) - wsw_memb(fn) - WSW_STUB_LEN;

    (void) wsw_mprotect_recovery(fn);

    return WSW_ERR_NONE;
}

int 
wsw_stub_reset(WSW_STUB_MAN_S *stub_m)
{
    int      ret;
    void    *fn;

    WSW_IF_EXPS_RETURN(!stub_m, WSW_ERR_BADPARAM);

    fn = stub_m->fn;
    ret = wsw_mprotect_write(fn);
    WSW_IF_EXPS_RETURN(WSW_ERR_NONE != ret, ret);

    memcpy(fn, stub_m->stub, stub_m->len);

    (void) wsw_mprotect_recovery(fn);

    return WSW_ERR_NONE;
}

对 WSW_STUB_JMPQ 操作数的注释说明。

  +------------+
  |    ...     |
0 +============+-------------------------------
  |jmpq offset |                             ↑
5 +------------+-------                      |
  |     .      | |                           |
  |     .      | | offset = stub_fn - fn - 5 |fn
x +============+ |                           |
  |    ...     | ↓                           ↓
0 +============+-------------------------------
  |     .      |   ↑
  |     .      |stub_fn
  |     .      |   ↓
y +============+-------
.text 
3.3 调用接口体验
static void 
test_stub_fn(void)
{
    fprintf(stderr, "%s\n", __func__);
    return ;
}

void 
test_fn(void)
{
    fprintf(stderr, "%s\n", __func__);
    return ;
}

int main(void)
{
    int             i, ret;
    WSW_STUB_MAN_S *m;

    m = wsw_stub_init(WSW_STUB_LEN);
    if (!m) return -1;

    ret = wsw_stub_set(m, test_fn, test_stub_fn);
    if (WSW_ERR_NONE != ret) {
        wsw_stub_deinit(m);
        return -1;
    }

    test_fn();
    wsw_stub_reset(m);
    test_fn();
    
    wsw_stub_deinit(m);

    return 0;
}

运行输出:

test_stub_fn
test_fn
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值