前言:在做基础技术的时候,我们经常遇到的一个问题是如何让我们提供的代码对用户的侵入性更小,更不敏感。比如我提供了一个SDK来收集Node.js进程的HTTP请求耗时。最简单的方法就是给用户提供一个请求方法,然后让用户统一调用,这样我就可以拿到请求中的数据了。但这种方法往往很不方便。这时候我们需要破解Node.js的HTTP模块或者提交PR给Node.js。在操作系统层面,提供了很多技术来解决这个问题,比如ebpf、uprobe、kprobe。但是应用层不能用这个技术来解决我们的问题,因为操作系统的这些技术都是针对底层功能的。比如我想知道一个JS函数的耗时,只能在V8层面或者JS层面去解决。切面似乎并没有提供很好的能力,所以目前我们更关注纯JS或者Node.js内核层面。本文介绍了一些在JS级别破解用户代码的方法。在Node.js中,通常统计JS函数耗时的方式是cpuprofile,但是这种方式只能获取一段时间的耗时。如果我想实时采集耗时数据,cpuprofile做起来有点难。直接的方式就是定期收集cpuprofile数据,然后我们手动分析profile数据并上报。除了这种方法,本文还介绍了另一种方法。只需破解JS代码即可。假设您具有以下功能。functioncompute(){//dosomething}如果我们要统计这种函数的执行时间,最自然的方法就是在函数的开头和结尾插入一些代码。但是我们不希望这种事情由用户手动完成,而是使用一种更优雅的方式。也就是分析源码,得到AST,然后重写AST。让我们看看如何。constacorn=require('acorn');constescodegen=require('escodegen');constb=require('ast-types').builders;constwalk=require("acorn-walk");constfs=require('fs');//分析源代码,拿到ASTconstast=acorn.parse(fs.readFileSync('./test.js','utf-8'),{ecmaVersion:'latest',});functioninject(node){//在函数前插入代码constentryNode=b.variableDeclaration('const',[b.variableDeclarator(b.identifier('start'),b.callExpression(b.identifier('(()=>{returnDate.now();})'),[],))]);constexitNode=b.returnStatement(b.callExpression(b.identifier('((start)=>{console.log(Date.now()-start);})'),[b.identifier('start')],));如果(node.body.body){node.body.body.unshift(entryNode);node.body.body.push(exitNode);}}//遍历AST,修改ASTwalk.simple(ast,{ArrowFunctionExpression:inject,ArrowFunctionDeclaration:inject,FunctionDeclaration:inject,FunctionExpression:inject});//根据修改后的AST重新生成代码constnewCode=escodegen.generate(ast);fs.writeFileSync('test.js',newCode)执行上面的代码得到如下结果functioncompute(){conststart=(()=>{returnDate.now();})();return((start)=>{console.log(Date.now()-start);})(start);}这样我们就可以得到各个函数的耗时数据了。但是这种方式是静态分析源码,需要用户主动操作,不太友好。然后基于这个基础,我们利用V8调试协议中的DebuggerDomain实现动态改写,同样可以改写Node.js内部的JS代码。首先更改测试代码。functioncompute(){//dosomething}setInterval(compute,1000)然后看看重写代码的逻辑。const{Session}=require('inspector');constacorn=require('acorn');constescodegen=require('escodegen');constb=require('ast-types').builders;constwalk=要求("acorn-walk");constsession=newSession();session.connect();require('./test_ast');//监听JS代码解析事件,获取所有JSession.on('Debugger.scriptParsed',(message)=>{//只处理这个文件if(message.params.url.indexOf('test_ast')===-1){return;}//获取源代码session.post('Debugger.getScriptSource',{scriptId:message.params.scriptId},(err,ret)=>{constast=acorn.parse(ret.scriptSource,{ecmaVersion:'latest',});functioninject(node){constentry=b.variableDeclaration('const',[b.variableDeclarator(b.identifier('start'),b.callExpression(b.identifier('(()=>{returnDate.now();})'),[],))]);constexit=b.returnStatement(b.callExpression(b.identifier('((start)=>{console.log(Date.now()-start);})'),[b.identifier('start')],));如果(node.body.body){node.body.body.unshift(条目);node.body.body.push(退出);}}walk.simple(ast,{ArrowFunctionExpression:inject,ArrowFunctionDeclaration:inject,FunctionDeclaration:inject,FunctionExpression:inject});constnewCode=escodegen.生成(AST);//经过分析,重写AST后生成新代码,重写session.post('Debugger.setScriptSource',{scriptId:message.params.scriptId,scriptSource:newCode,dryRun:false});})});session.post('Debugger.enable',()=>{});正常情况下setInterval执行的函数是没有输出的,但是我们发现它会不断的输出0,比较耗时,因为这里使用的是毫秒级别的统计,所以是0,但是我们不需要注意这一点,所以我们完成了hack用户的代码,对用户无动于衷。唯一需要做的就是引入我们提供的SDK。但是这种方法的难点在于重写代码的逻辑,风险比较大,但是如果我们解决了这个问题,我们就可以随意hack用户的代码,为所欲为,当然是商业.
