精华内容
下载资源
问答
  • 代码插桩

    千次阅读 2019-05-19 09:33:16
    【转载】http://wiki.dzsc.com/info/5081.html 代码插桩是实现覆盖测试的关键技术之一,而高效的插桩技术对于嵌入式软件的测试 来说又是至关重要的。在对CodeTeST 中插桩技术研究的基础上,以G...

    https://blog.csdn.net/woshidujian19881029/article/details/6558260

    【转载】http://wiki.dzsc.com/info/5081.html

    •   代码插桩是实现覆盖测试的关键技术之一,而高效的插桩技术对于嵌入式软件的测试 来说又是至关重要的。在对CodeTeST 中插桩技术研究的基础上,以GCC 作为开发平台,应用并实现了新的插装器,采用增加一个词法语法分析器的方法,提高了插桩的效率。经过实验证明新的插装器具有代码膨胀率小,插桩速度块的优 点,在一定程度上做到了高效插桩。
    代码插桩

    代码插桩概述

    •   在实现覆盖测试的过程中,往往需要知道某些信息,如:程序中可执行语句 被执行(即被覆盖)的情况,程序执行的路径,变量的引用、定义等。要想获取这类信息,需要跟踪被测程序的执行过程,或者是由计算机在被测程序执行的过程中 自动记录。前者需要人工进行,效率低下且枯燥乏味;后者则需要在被测程序中插入完成相应工作的代码,即代码插桩技术。如今大多数的覆盖测试工具 均采用代码插桩技术。

        在对普通应用的软件进行测试时,由于现在电脑的配置越来越高,电脑的运行速度越来越快,代码插桩所 引起的问题还不是很明显或者说是在可以接受的范围之内。但是对于嵌入式软件来说这却是致命的问题。因为嵌入式软件的系统资源有限(内存较小、I/O 通道较少等),过大的代码膨胀率将使得程序不能在嵌入式系统中运行;同时嵌入式软件通常具有很强的实时性,程序的输出只在有限的时间内有效,迟到的“正确 的”结果是无用的甚至会变成错误的、有害的。

        代码插桩技术会破坏程序的时间特性等,导致软件执行的错误。因此我们需要更高效的代码插桩技术来完成覆盖测试,尤其是嵌入式软件的覆盖测试。

    代码插桩方式比较

    •   由于程序插桩技术是在被测程序中插入探针,然后通过探针的执行来获得程序的控制流和数据流信息,以此来实现测试的目的。因此,根据探针插入的时间可以分为目标代码插桩和源代码插桩。

        (1)目标代码插桩的前提是对目标代码进:

        行必要的分析以确定需要插桩的地点和内容。由于目标代码的格式主要和操作系统相关,和具体的编程语 言及版本无关,所以得到了广泛的应用,尤其是在需要对内存进行监控的软件中。但是由于目标代码中语法、语义信息不完整,而插桩技术需要对代码词法语法的分 析有较高的要求,故在覆盖测试工具中多采用源代码插桩。

        (2)源代码插桩是在对源文件进行完整的:

        词法分析和语法分析的基础上进行的,这就保证对源文件的插桩能够达到很高的准确度和针对性。但是源代码插桩需要接触到源代码,使得工作量较大,而且随着编码语言和版本的不同需要做一定的修改。在后面我们所提到的程序插桩均指源代码插桩。

    代码插桩设计

    •   (1)插桩位置:

        探针的植入要做到紧凑精干,才能保证在做到收集的信息全面而无冗余,减少代码的膨胀率。因此,在确定插桩位置时,要将程序划分,基本的划分方法是基于“块”结构。

        按照块结构的划分,探针的植入位置有以下几种情况:

        a. 程序的第一条语句;b. 分支语句的开始;c. 循环语句的开始;d. 下一个入口语句之前的语句;e. 程序的结束语句;f. 分支语句的结束;g. 循环语句的结束;除此之外,根据覆盖测试要求的不同,插桩的位置除了上面所说的几种情况外,也会随着覆盖测试要求的不同有所变化。

        (2)插桩策略:

        插桩策略是解决“如何插”的问题。传统的插桩策略是在所有需要插桩的位置插入探针,在程序运行过程 收集所有可能用到得程序信息,将其写入数据库进行分析和处理。这种方法对于大型的程序来说,将会造成相当大的工作量,效率很低,且会造成很大的代码膨胀 率。而我们会根据不同的测试要求,每次插入不同的探针,采用相应的插桩策略,这样就减少了代码的膨胀率,保证了程序执行的效率。下面简单介绍几种探针的插 桩策略。

        语句覆盖探针(基本块探针):在基本块的入口和出口处,分别植入相应的探针,以确定程序执行时该基本块是否被覆盖。

        分支覆盖探针:C/C++语言中,分支由分支点确定。对于每个分支,在其开始处植入一个相应的探针,以确定程序执行时该分支是否被覆盖。

        条件覆盖探针:C/C++语言中,if, swich,while, do-while, for 几种语法结构都支持条件判定,在每个条件表达式的布尔表达式处植入探针,进行变量跟踪取值,以确定其被覆盖情况。

        根据不同测试要求采用不用的插桩策略 ,每次在不同的位置植入相应的探针,使得每次只是植入有限的探针,这就更大大减少了代码的膨胀率和插桩的速度。

    展开全文
  • 静态代码插桩

    2021-01-27 18:28:52
    静态代码插桩设置插桩参数 代码插桩是指根据一定的策略在代码中插入桩点来统计代码覆盖的技术手段.一般可以分为三个粒度: 函数(function): 按照函数为单位进行插桩; 基本块(basic block): 按照代码执行单元进行...


    代码插桩是指根据一定的策略在代码中插入桩点来统计代码覆盖的技术手段.一般可以分为三个粒度:

    • 函数(function): 按照函数为单位进行插桩;
    • 基本块(basic block): 按照代码执行单元进行分组的执行单元,单元内部的代码执行次数一定是相同的;
    • 边界(Edge): 按照代码执行路径进行插桩。

    针对iOS来说,clang支持以上粒度的插桩方式。这里先介绍一些函数粒度的插桩实现.

    函数覆盖

    Clang 是一个高度模块化开发的轻量级编译器。可以通过设置Clang的编译参数实现静态插桩.

    • 在Xcode->Build Settings中搜索"Other C Flags",然后在其中添加
    // 基本块覆盖可以使用参数:  -fsanitize-coverage=bb,trace-pc-guard
    // 边缘覆盖可以使用参数:  -fsanitize-coverage=edge,trace-pc-guard
    -fsanitize-coverage=func,trace-pc-guard
    

    同时在任意实现文件中添加一下两个函数:

    // 哨兵初始化函数,其中[*start,*end)表示了哨兵的标志,这里可以理解为每个哨兵guard是一个指针,保存了一个uint32_t的整形数据来作为自己的标记
    void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
      static uint32_t N;  // Counter for the guards.
      if (start == stop || *start) return;  // Initialize only once.
        for (uint32_t *x = start; x < stop; x++) {
            *x = ++N;  // Guards should start from 1.
        }
    }
    
    // 当每个函数开始调用时会被插入该回调,所以在方法调用开始就会执行该回调。函数中guard就是__sanitizer_cov_trace_pc_guard_init中[start, end)区间中一个
    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    	printf("guard = %p, *guard=%d", guard, *guard);
    // 在这里可以尝试获取到执行函数的信息
    }
    

    然后运行项目会发现,回调正常执行.那么问题来了,如何在回调函数中获取到当前执行函数的信息呢?

    获取当前执行函数的信息

    在汇编语言中,如果函数包含了子函数,则方法执行过程中会在进行bl跳转指令之前将下一条指令的地址保存在特定的寄存器中(x30寄存器),而clang中封装了这样的获取方法:

    // 返回当前函数或其调用者的返回地址,LEVEL表示调用层级:
    // 0: 表示当前函数的返回地址;
    // 1:表示当前函数调用者的返回地址,依次类推
    void *__builtin_return_address(int LEVEL);
    

    拿到改地址之后如何获取该方法的信息呢?系统同样提供了开放的方法。在 dlfcn.h 中有一个方法如下 :

    typedef struct dl_info {
            const char      *dli_fname;     /* 所在文件 */
            void            *dli_fbase;     /* 文件地址 */
            const char      *dli_sname;     /* 符号名称 */
            void            *dli_saddr;     /* 函数起始地址 */
    } Dl_info;
    
    //这个函数能通过函数内部地址找到函数符号
    int dladdr(const void *, Dl_info *);
    

    这样就可以通过函数内部的地址获取到函数的符号信息:

    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
        // 获取寄存器中下一条指令的地址
        void *PC = __builtin_return_address(0);
        Dl_info info;
        // 通过函数内部地址获取当前函数的符号信息
        dladdr(PC, &info);
        printf("fname=%s \nfbase=%p \nsname=%s\nsaddr=%p \n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
    }
    

    再次启动工程就可以发现,已经获取到了项目中执行方法的符号信息.

    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=main
    saddr=0x1008257d4 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[AppDelegate application:didFinishLaunchingWithOptions:]
    saddr=0x100825520 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate setWindow:]
    saddr=0x100825d60 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate scene:willConnectToSession:options:]
    saddr=0x1008259f4 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate window]
    saddr=0x100825d08 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[ViewController setImageView:]
    saddr=0x10082541c 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[ViewController viewDidLoad]
    saddr=0x100824538 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate sceneWillEnterForeground:]
    saddr=0x100825c20 
    fname=/private/var/containers/Bundle/Application/9B098A1A-27A2-48C4-8402-B528A872C362/Demo001.app/Demo001 
    fbase=0x100820000 
    sname=-[SceneDelegate sceneDidBecomeActive:]
    saddr=0x100825b38 
    

    保存符号

    既然获得了符号,就需要对符号进行保存,以留作后续使用.这里尝试保存在第一个界面正式出现之前的所有符号。
    为了方便操作将上述两个方法从main中移动到第一个控制器中.

    • 由于回调函数可能出在多个线程中,为了线程安全,同时减少频繁加锁开锁的系统开销使用原子队列来进行数据存储.
      需要注意的是,原子队列只能添加结构体节点,不能直接添加oc对象节点。
    #import <libkern/OSAtomic.h>
    // 向队列中添加节点
    void  OSAtomicEnqueue( OSQueueHead *__list, void *__new, size_t __offset);
    
    // 可以使用循环从院子队列中取出元素
    void* OSAtomicDequeue( OSQueueHead *__list, size_t __offset);
    
    • 初始化原子队列,并定义结构体节点:
    static OSQueueHead queueHead = OS_ATOMIC_QUEUE_INIT;
    // 定义节点
    struct SYNode {
        char *symbol; // 记录符号
        struct SYNode *next; // 记录符号的写一个地址
    };
    
    • 向节点中写入数据:
    void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
        if (!*guard) return;  // Duplicate the guard check.
    
        void *PC = __builtin_return_address(0);
        Dl_info info;
        dladdr(PC, &info);
        
    //
    //    printf("fname=%s \nfbase=%p \nsname=%s\nsaddr=%p \n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
        
        
        char *symbol = malloc(sizeof(char) * strlen(info.dli_sname));
        strcpy(symbol, info.dli_sname);
        struct SYNode *node = malloc(sizeof(struct SYNode));
        *node = (struct SYNode){symbol, NULL};
        // 向节点中写入数据
        OSAtomicEnqueue(&queueHead, node, offsetof(struct SYNode, next));
    }
    
    • 在控制器的viewDidAppear方法中,获取院子队列中的符号信息,并进行本地保存:
    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        
        NSMutableArray<NSString *> *allSymbols = [NSMutableArray array];
        
        struct SYNode *node = OSAtomicDequeue(&queueHead, offsetof(struct SYNode, next));
        while (node) {
            NSLog(@"--->%s", node->symbol);
            
            [allSymbols addObject:[NSString stringWithUTF8String:node->symbol]];
            free(node);
            node = OSAtomicDequeue(&queueHead, offsetof(struct SYNode, next));
        }
        
        NSString *cacheRootPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, true).firstObject;
        NSString *path = [cacheRootPath stringByAppendingPathComponent:@"symbols.txt"];
    
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            BOOL success = [allSymbols writeToFile:path atomically:true];
             NSLog(@"写入%@", success ? @"成功" : @"失败");
        });
    }
    
    • 使用Xcode连接真机,启动应用直至第一个控制器界面家在完成.使用快捷键cmd+shift+2进入Devices and Simulators界面,选择对应应用并点击Download Containers选择保存路径下载文件。
      在这里插入图片描述
    • 在下载的.xcappd中右键显示包内容,在AppData->Library->Caches路径下即可保存的.txt文件.
      由于队列的特性,这里的符号与实际调用顺序是相反的。
      在这里插入图片描述

    这样就可查看到在应用首个控制器显示之前系统调用的所有符号,从而为应用启动优化奠定基础.

    静态插桩作用

    通过静态插桩,可以查看项目中的代码执行情况,进而为项目优化提供依据.

    • 重排二进制文件:可以根据启动时调用的方法,存储在.order文件中,认为干预二进制文件的生成,优化启动速度;
    • 删除无用代码:可以根据项目中方法的执行情况,查看方法的覆盖率,将没有使用到的方法进行删除,减少二进制文件的大小;
    • 跟踪方法调用顺序:可以将调用的符号进行保存来查看应用中方法的调用顺序,跟踪异常。
    展开全文
  • 代码插桩是实现覆盖测试的关键技术之一,而高效的插桩技术对于嵌入式软件的测试来说又是至关重要的。文章在对CodeTeST 中插桩技术研究的基础上,以GCC 作为开发平台,应用并实现了新的插装器,采用增加一个词法语法...
  • 代码插桩技术能够让我们在不更改已有源码的前提下,从外部注入、拦截各种自定的逻辑。这为施展各种黑魔法提供了巨大的想象空间。
  • 摘要:代码插桩是实现覆盖测试的关键技术之一,而高效的插桩技术对于嵌入式软件的测试来说又是至关重要的。文章在对CodeTest 中插桩技术研究的基础上,以GCC 作为开发平台,应用并实现了新的插装器,采用增加一个词...
  • 说到代码插桩,你可能会想到 AspectJ、Transfrom Api + ASM 等等。 代码插桩的用处自不必说,可以做埋点、热修复、组件化路由等等。 然而,AspectJ感觉不好用,ASM 比较复杂,需要自定义 gradle 插件。好在前段时间...

    1. 前言

    说到代码插桩,你可能会想到 AspectJTransfrom Api + ASM 等等。

    代码插桩的用处自不必说,可以做埋点、热修复、组件化路由等等。

    然而,AspectJ感觉不好用,ASM 比较复杂,需要自定义 gradle 插件。好在前段时间,我遇到了新的方法 —— AnnotationProcessor。(下面简称为 apt

    apt 是否只能生成新的 java 文件?还是有什么方法可以直接插入代码,达到 ASM 的效果?

    留个悬念,咱们接着往下看。

    2. apt 与 ButterKnife

    说到 apt,不得不说 ButterKnife。

    通过注解生成XXX_ViewBinding的操作深入人心,然后Javapoet也逐渐家喻户晓。

    回顾一下,以下是 jdk 中提供的 apt 相关的 api。

    - javax
      - annotation.processing
        - AbstractProcessor       // 入口
        - ProcessingEnvironment   // 编译器环境,可理解为 Application
        - Filer                   // 文件读写 util
      - lang.model
        - element
          - Element               // 代码结构信息
        - type
          - TypeMirror            // 编译时的类型信息(非常类似 Class,但那是运行时的东西,注意现在是编译时) 
    复制代码

    一个常规的注解处理器有这么几步:

    1. 继承 AbstractProcessor
    2. 根据注解获取相关 Element
    3. 写入 Filer
    4. app/build/generated/source/apt/下将生成相关 java 文件

    然而,Filer 有局限性,只有 create 相关的接口。

    public interface Filer {
        JavaFileObject createSourceFile(CharSequence name,
                                        Element... originatingElements) throws IOException;
        ...
    }
    复制代码

    我们得寻找别的方式。

    3. javac 与 重写 AST

    让我们来思考一个问题:

    1. AbstractProcessor.process() 这个入口是被什么东西所调用的呢?

    当然是编译器啦,通常而言,我们一般用的是javac编译器。

    现在,我们只需要通读一下 javac 的源码java 编译过程概览),就会发现,编译流程大致如下:

    1. Parse and Enter: 解析 .java 文件,在内存中生成 AST (抽象语法树)填充符号表
    2. Annotation Processing: 调用 AbstractProcessor.process(),若有新的 java 文件生成,则回到步骤 1
    3. Analyse and Generate: 依次执行标注检查数据及控制分析解语法糖生成并写入.class文件

    如此一来,我们知道了我们编写的apt代码执行在 java 编译过程中的第2步。

    如果说,编译过程是 .java -> AST -> .class 的过程,那么我们可以在apt里修改AST这个中间产物,改变最终的.class,从而达到等同于ASM的效果。

    具体而言,我们需要用到一些 javac 内部的 api,它们不属于 jdk 的java/或者javax/包下。而是在 tools.jarcom.sun.tools.javac/ 下,具体不再展开。

    AST 详细介绍:安卓AOP之AST:抽象语法树

    4. 一个例子,一行注解搞定单例

    设想,我现在有一个UserManager,想搞成单例。

    按照原本的生成新文件的方式肯定是不行的。不过现在我们可以插入代码。

    1. 自定义一个注解@Singleton,以及一个注解处理器SingletonProcessor
    2. 源代码加一行@Singleton:
    // UserManager.java
    @Singleton
    class UserManager {
    }
    复制代码

    apt 插桩后的代码,自动生成getInstance(),以及InstanceHolder,有没有很爽:

    // build 目录下,UserManager.class
    @Singleton
    class UserManager {
    
        public static UserManager getInstance() {
            return UserManager._InstanceHolder._sInstance;
        }
    
        UserManager() {
        }
    
        private static class _InstanceHolder {
            private static final UserManager _sInstance = new UserManager();
    
            private _InstanceHolder() {
            }
        }
    }
    复制代码

    实现细节请移步:github.com/fashare2015…

    5. 后记

    作为 java 的忠实粉丝,希望搞几个语法糖出来。因此,胡乱捣鼓出了java-sugar这个项目。

    其中实现了单例Builder观察者等几个常用的设计模式。

    另外还做了自动生成GetterSetter,这样一来,java应该不输给kotlin了吧(滑稽)。

    也许,大致上可以把 kotlin 的语法糖都抄袭一遍?

    6. 参考

    openjdk.java.net/groups/comp…

    Java编译(二)Java前端编译: Java源代码编译成Class文件的过程

    Javac黑客指南

    安卓AOP之AST:抽象语法树

    Lombok

    展开全文
  • 代码插桩技术能够让我们在不更改已有源码的前提下,从外部注入、拦截各种自定的逻辑。这为施展各种黑魔法提供了巨大的想象空间。下面我们将介绍浏览器环境中一些插桩技术的原理与应用实践。 插桩基础概念 前端插桩...

    代码插桩技术能够让我们在不更改已有源码的前提下,从外部注入、拦截各种自定的逻辑。这为施展各种黑魔法提供了巨大的想象空间。下面我们将介绍浏览器环境中一些插桩技术的原理与应用实践。

    插桩基础概念

    前端插桩的基本理念,可以用这个问题来表达:假设有一个被业务广泛使用的函数,我们是否能够在既不更改调用它的业务代码,也不更改该函数源码的前提下,在其执行前后注入一段我们自定义的逻辑呢?

    举个更具体的例子,如果业务逻辑中有许多 console.log 日志代码,我们能否在不改动这些代码的前提下,将这些 log 内容通过网络请求上报呢?一个简单的思路是这样的:

    1. 封装一个「先执行自定义逻辑,然后执行原有 log 方法的函数」。
    2. 将原生 console.log 替换为该函数。

    如果希望我们的解法具备通用性,那么不难将第一步中的操作泛化为一个高阶函数:

    function withHookBefore (originalFn, hookFn) {
      return function () {
        hookFn.apply(this, arguments)
        return originalFn.apply(this, arguments)
      }
    }
    复制代码

    于是,我们的插桩代码就很简洁了。只需要形如这样:

    console.log = withHookBefore(console.log, (...data) => myAjax(data))
    复制代码

    原生的 console.log 会在我们插入的逻辑之后继续。下面考虑这个问题:我们能否从外部阻断 console.log 的执行呢?有了高阶函数,这同样是小菜一碟:

    function withHookBefore (originalFn, hookFn) {
      return function () {
        if (hookFn.apply(this, arguments) === false) {
          return
        }
        return originalFn.apply(this, arguments)
      }
    }
    复制代码

    只要钩子函数返回 false,那么原函数就不会被执行。例如下面就给出了一种清爽化控制台的骚操作:

    console.log = withHookBefore(console.log, () => false)
    复制代码

    这就是在浏览器中「偷天换日」的基本原理了。

    对 DOM API 的插桩

    单纯的函数替换还不足以完成一些较为 HACK 的操作。下面让我们考虑一个更有意思的场景:如何捕获浏览器中所有的用户事件?

    你当然可以在最顶层的 document.body 上添加各种事件 listener 来达成这一需求。但这时的问题在于,一旦子元素中使用 e.stopPropagation() 阻止了事件冒泡,顶层节点就无法收到这一事件了。难道我们要遍历所有 DOM 中元素并魔改其事件监听器吗?比起暴力遍历,我们可以选择在原型链上做文章。

    对于一个 DOM 元素,使用 addEventListener 为其添加事件回调是再正常不过的操作了。这个方法其实位于公共的原型链上,我们可以通过前面的高阶插桩函数,这样劫持它:

    EventTarget.prototype.addEventListener = withHookBefore(
      EventTarget.prototype.addEventListener,
      myHookFn // 自定义的钩子函数
    )
    复制代码

    但这还不够。因为通过这种方式,真正添加的 listener 参数并没有被改变。那么,我们能否劫持 listener 参数呢?这时,我们实际上需要这样的高阶函数:

    1. 把原函数的参数传入自定义的钩子中,返回一系列新参数。
    2. 用魔改后的新参数来调用原函数。

    这个函数大概长这样:

    function hookArgs (originalFn, argsGetter) {
      return function () {
        var _args = argsGetter.apply(this, arguments)
        // 在此魔改 arguments
        for (var i = 0; i < _args.length; i++) arguments[i] = _args[i]
        return originalFn.apply(this, arguments)
      }
    }
    复制代码

    结合这个高阶函数和已有的 withHookBefore,我们就可以设计出完整的劫持方案了:

    • 使用 hookArgs 替换掉传入 addEventListener 的各个参数。
    • 被替换的参数中,第二个参数就是真正的 listener 回调。将这个回调替换为 withHookBefore 的定制版本。
    • 在我们为 listener 添加的钩子中,执行我们定制的事件采集代码。

    这个方案的基本逻辑结构大致形如这样:

    EventTarget.prototype.addEventListener = hookArgs(
      EventTarget.prototype.addEventListener,
      function (type, listener, options) {
        const hookedListener = withHookBefore(listener, e => myEvents.push(e))
        return [type, hookedListener, options]
      }
    )
    复制代码

    只要保证上面这段代码在所有包含 addEventListener 的实际业务代码之前执行,我们就能超越事件冒泡的限制,采集到所有我们感兴趣的用户事件了 :)

    对前端框架的插桩

    在我们理解了对 DOM API 插桩的原理后,对于前端框架的 API,就可以照猫画虎地搞起来了。比如,我们能否在 Vue 中收集甚至定制所有的 this.$emit 信息呢?这同样可以通过原型链劫持来简单地实现:

    import Vue from 'vue'
    
    Vue.prototype.$emit = withHookBefore(Vue.prototype.$emit, (name, payload) => {
      // 在此发挥你的黑魔法
      console.log('emitting', name, payload)
    })
    复制代码

    当然了,对于已经封装出一套完善 API 接口的框架,通过这种方式定制它,很可能有违其最佳实践。但在需要开发基础库或开发者工具的时候,相信这一技术是有其用武之地的。举几个例子:

    • 基于对 console.log 的插桩,可以让我们实现跨屏的日志收集(比如在你的机器上实时查看其他设备的操作日志)
    • 基于对 DOM API 的插桩,可以让我们实现对业务无侵入的埋点,以及用户行为的录制与回放。
    • 基于对组件生命周期钩子的插桩,可以让我们实现更精确而无痛的性能收集与分析。
    • ……

    总结

    到此为止,我们已经介绍了插桩技术的基本概念与若干实践。如果你感兴趣,一个好消息是我们已经将常用的插桩高阶函数封装为了开箱即用的 NPM 基础库 runtime-hooks,其中包括了这些插桩函数:

    • withHookBefore - 为函数添加 before 钩子
    • withHookAfter - 为函数添加 after 钩子
    • hookArgs - 魔改函数参数
    • hookOutput - 魔改函数返回值

    欢迎在 GitHub 上尝鲜我司这一开源项目,也欢迎大家关注这个前端专栏噢 :)

    P.S. 我们 base 厦门的前端团队活跃招人中,简历求砸 xuebi at gaoding.com 呀~

    展开全文
  • 代码插桩技术能够让我们在不更改已有源码的前提下,从外部注入、拦截各种自定的逻辑。这为施展各种黑魔法提供了巨大的想象空间。下面我们将介绍浏览器环境中一些插桩技术的原理与应用实践。插桩基础概念前端插桩的...
  • 代码插桩【转载】

    千次阅读 2011-06-21 11:59:00
    【转载】http://wiki.dzsc.com/info/5081.html   代码插桩是实现覆盖测试的关键技术之一,而高效的插桩技术对于嵌入式软件的测试来说又是至关重要的。在对CodeTeST 中插桩技术研究的基础上,以GCC ...
  • C# 代码插桩

    2016-05-05 18:28:33
    想监测代码执行的过程,目前是通过修改IL的方式进行代码静态注入,例如使用如下方式获取所有Console.WriteLine函数的输入值 [img=https://img-bbs.csdn.net/upload/201605/05/1462442859_313682.png][/img] 一个...
  • 通过安卓修改大师可以很轻松的在任何apk中添加新的代码逻辑,实现额外添加的功能,本次教程通过对一款名为“VMware Horizon”的软件进行反编译,实现在登录界面添加自动登录功能(该登录界面之前已经有自动登录功能...
  • smali代码插桩打印日志

    千次阅读 2016-10-26 14:02:08
    1.将以下代码复制并保存为"log.smali"到你本地,文件名可任意,后缀名必须为“smali”。 .class public Lcrack; .super Ljava/lang/Object; .source "crack.java" .method public static log1(Ljava/lang/String;)V...
  • 字节码注入 入口 想要不修改原始的java代码,并且实现SDK的代码的集成,实现初始化init方法的调用,就不得不在编译或者打包的过程中干预原始代码的编译结果。安卓APK文件的编译过程。从宏观上是: .java -->.class ...
  • } } 通过如上代码我们发现最后调用AccessibilityDelegate实例对象方法sendAccessibilityEvent(this, eventType),那么是否可以重写AccessibilityDelegate类和sendAccessibilityEvent(this, eventType)来完成埋点...
  • tracsformer包:代码插桩过滤器,使用责任连模式,对字节码进行多次插桩,每个插桩器只负责自己想要实现的逻辑。 event包:事件的封装,埋点代码抛出的事件放入事件队列,异步分派事件给监听器进行处理。 logs包:...
  • LLVM的核心思想是将各种语言解析成LLVM的中间表示语言(LLVM IR),然后LLVM通过各种pass在IR上进行优化,最后通过各种代码生成后端生成目标机器上的机器代码,这样就可以最大程度重用前端解析、中端优化以及后端代码...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 473
精华内容 189
关键字:

代码插桩