前往Shuct.Net首页

Shudepb PB反编译专家长时间以来,为业内同类软件事实上的唯一选择.细节,彰显专业.态度,决定品质.

关于反编译的搜索

-------------- \ ^__^ \ (**)\__$__$__ (__)\ )\/\ U ||------| || || --> JavaScripter : [翻译]Javascript中函数反编译的历史,现状和未来 - 推酷 推酷 文章 主题 站点 微博 活动 应用 周刊 编程狂人 设计匠艺 登录 × --> JavaScripter : [翻译]Javascript中函数反编译的历史,现状和未来 时间 2014-03-06 15:50:05 图灵社区 相似文章 (0) 原文 http://www.ituring.com.cn/article/71466 添加到推刊 收藏到你的推刊 创建推刊 JavaScripter : [翻译]Javascript中函数反编译的历史,现状和未来 选择推刊 收藏 取消 已收藏到推刊! 创建推刊 × Modal header --> 请填写推刊名 描述不能大于100个字符! 权限设置: 公开 仅自己可见 创建 取消 Javascript中的函数反编译的历史,现状和未来 理论 实践 现在的情况 反编译的目的 用户定义的函数 函数的构造器 绑定函数 非标准的情况 ES6所增加的 Minifiers及预处理器 长话短说 去发现那些在Javascript世界中被称之为“magic”的东西,总是一件有趣的事情。 我最近遇到的一个这样的例子是AngularJS的 依赖注入机制 。我从来没有去熟悉过这个概念,但我觉得它在实践中看起来聪明又方便,虽然并不是特别的神奇。 它是干什么的?简而言之:通过函数的参数来定义所需的“模块”。像这样: angular.module('App', [ ]) .controller('Ctrl', function($scope, $timeout, $http) { ... }); 注意 $scope , $timeout , $http 这些标识符。 啊哈,所以,它并非将它们作为字符串或变量或什么其它的东西来传递,而是将他们定义为代码的一部分。当然,为了“读懂”这代码,有件事不得不提。 函数反编译 我们在prototype.js中使用的那种 实现$super 的方法还是早在2007年?是的,就是它。后来实现它的方式来自于Resig的 simple inheritance (用一个 安全 的方式)或其它地方。 看到像Angular这样一个现代化的框架使用函数反编译让我很惊讶。即使它不是Angular的 专有 依赖 ,但这个黑魔法已经让人觉得不习惯了很多年。我在2009年 写了一些有关这个问题的东西 。 像这样 本质上来说不标准 并且 在不同实现中也不一样 的东西只能通过用户代理嗅探来进行比较。 但它是真的是这样吗?或者说, 近日里 事情并没有那么糟糕 ?我4年前研究过这一点 - 用了一大片的时间。当涉及到函数字符串表示形式时,将来会不会取得某种统一的实现?我是不是已经完全过时了? 出于好奇,我决定去看看目前的状况。函数反编译现在可以依赖么?我们究竟能够依赖什么? 首先。。。 理论 简单地说,函数反编译是将函数代码作为一个字符串来访问(然后解析它的内容或提取参数或其他)的过程。 在Javascript中,这是通过函数对象的 toString() 方法来实现的,所以 fn.toString() 或 String(fn) 或 fn + '' 或其他任何委托到 Function.prototype.toString 的用法都可以实现相同功能 。 这在Javascript中被认为是不可靠的原因是由于其 不规范的性质 。ES5规范的一段著名引用这么说: 15.3.4.2 Function.prototype.toString ( ) 返回一个依赖于实现的函数表达形式。这个表达符合一个FunctionDeclaration的语法。请特别注意在表达字>符串中空格符,行终止,分 号的使用是实现相关的。 当然,当某些东西是与 实现相关 的的时候 ,它必然会以各种可以想象的方式偏离轨道。 实践 ..事实就是这样。比如你会认为这样的一个函数: function foo(x, y) { return x + y; } ..会被序列化成这样的字符串: "function foo(x, y) {\n return x + y;\n }" 它几乎确实这么做了,除了某些JS引擎可能会忽略掉换行。另一些引擎可能忽略掉注释。另一些会忽略掉那些“dead code”。另外一些则会包括进注释和(!)函数。而其它的则可能完全隐藏掉代码。 在以前,事情是 非常 糟糕的。例如在版本号<=2.X的Safari浏览器中,甚至不会检查函数声明的语法是否合法。如果使用像 "(Inner Function)" 或 "[function]" 或从 NFE 中舍弃那些标示符 将会是件疯狂的行为,这只是因为— 在以前,一些移动浏览器(黑莓,Opera Turbo)将完全隐藏代码(而代之以礼貌的 "/ *源代码不可用* /" 或 类似的 注释 ),也许这是为了“节省”内存。真是一个公平的优化。 现在的情况 但现在又是什么情况呢?当然,事情肯定会变得更好。现在我们有着引擎的趋同,相对合理的WebKit的占有率,大量的标准化,和引擎性能的巨大增长。 事实上,事情看起来还不错。但是并不是所有的事情都很好,有些更加“好玩”的东西出现在我们的视野里。 我做了一个 简单的测试页面 ,检查了函数和它们的字符串表示形式的不同情况。然后测试了它在桌面浏览器,包括那些非常“古老”的那些(IE6 +,FF3 +,Safari4 +,Opera9.6 +,Chrome浏览器),以及 手机上 的表现,并观察了一些通用模式。 反编译的目的 了解Javascript函数反编译的 不同目的 ,是非常重要的。 原生序列,内置函数和用户定义的函数在序列化上是不同的。例如,从Angular的角度来看,我们正在谈论的是 用户定义的 函数 ,所以我们不必关注本地函数是如何被序列化的。此外,如果我们谈论的 只是获取参数 ,那需要处理的问题相比“解析”源代码就少得多了。 有些东西是更可靠的。 但是别人的东西,就少一些。 用户定义的函数 当涉及到用户定义的函数时,所发生的事情是相当一致的。 除了那些古怪的和垂死挣扎的环境,例如IE<9 — 这些浏览器有时会在字符串结果表示中包含那些函数周围的注释(甚至括号)——或Konqueror,它会在省略从 new Function 中产生的(即生成的函数)函数体的括号。 多数的差别都在 空格 (和换行符)。有些浏览器(如Firefox<17)会从源代码中去除所有的注释,并删除“side”,无法访问的代码。 但是,不要高兴得太早,因为我们还没谈论未来会是什么样的呢... 函数的构造器 对于 生成的函数 ( new Function(...) )来说,事情会有些忙乱,但不会很麻烦。虽然大多数的引擎会给这些函数创建一个“anonymous”的标示符,但是对于空格和换行的处理是不一致的。Chrome将会在参数列表后插入额外的注释(额外的注释并不会影响什么,对么?) new Function('x, y', 'return x + y') 事情会变成这样: "function anonymous(x, y /**/) { return x + y }" 绑定函数 在我所测试的每个引擎中,绑定(通过 Function.prototype.bind )函数的表现与本地函数是一样的。是的,这意味着绑定函数在字符串表示中 “丢失”了它们的源代码 。 "function () { [native code] }" 可以说,这是一个合理的做法,虽然有点“奇葩?”当你第一次看到它的时候,你可能会问:“为什么不直接使用“ [绑定代码] ”这样来表示呢?” 奇怪的是,有些引擎(例如最新的WebKit) 会保留函数的原始标识符 ,而另一些则不会。 非标准的情况 非标准的扩展是怎样的呢? 比如 Mozilla的表达式闭包 var expressionClosure = function(x, y) x + y 是的,那些(非标准的函数)将像它们源代码一样被表示出来,没有函数体的括号(从技术上讲,这是不符合函数声明的语法的,但是 Function.prototype.toString 的MDN页面 甚至都没有提及。 有些东西必须要进行修改了!)。 ES6所增加的 在我几乎已经完成了编写测试用例的时候,突然一个想法出现在我心里。等等,这些问题在 EcmaScript 6 里会是什么样的 ? 所有这些在这门语言中新加入的东西;让函数看起来不一样的新语法——类,生成器,不定参数,默认参数,箭头函数等功能。这些会不会影响到函数的序列化表示呢? 一些快速测试给出了答案 - 是的,它们会有影响。显而易见的是,Firefox24 +,ES6大队的引领者,向我们展示了这些新构造形式的字符串表示: // Arrow functions var fn = () => 5; // "() => 5" // Rest params function fn(...args) { } // "function fn(...args) { }" // Default params function fn(foo=1) { } // "function fn(foo=1) { }" // Generators (function *(){ yield 1 }); // "function *() { yield 1 }" 通过检查 ES6规范 我们进一步证实了这一点 : 将返回一个实现相关的此对象源代码的字符串表示。这个表示需要有FunctionDeclaration,FunctionExpression,GeneratorDeclaration,GeneratorExpession,ClassDeclaration,ClassExpression,ArrowFunction,MethodDefinition,或GeneratorMethod中的一个语法,这具体取决于对象的实际特性。需要特别注意的是对于空格、行终止符和分号在字符串表示中是如何被替换的,因为这也与实现相关。 如果对象是使用ECMAScript代码所定义的,并且返回的字符串表示形式是FunctionDeclaration,FunctionExpression,GeneratorDeclaration,GeneratorExpession,ClassDeclaration,ClassExpression,或ArrowFunction中的一种,则字符串表示必须是这样:如果使用eval来执行了字符串,并且是在一个等同于用来创建原始对象的词法上下文的上下文中使用eval,则将会产生一个新的但功能等效的对象。返回的源代码表示不能使用任何没有在原始函数的源代码中使用到的变量,即使这些“额外”的变量名确实原本就是在作用域中的。如果源代码字符串表示不符合这些规定,那么它就是一个在eval时将会抛出一个SyntaxError异常的字符串。 请注意ES6仍然将函数的表示留给 实现自己决定 ,尽管它说明了不再仅检查是否符合FunctionDeclaration语法。我还发现了一个有趣的附加要求 —— “返回的源代码(表示)不能自由地使用任何没有在原始函数源代码中使用过的变量”(如果你在不到7次尝试中就理解了这一点,你可以得到加分!)。 我不太清楚这将如何影响未来的引擎和他们的表现,但有一点是可以肯定的,随着ES6的崛起,函数表示将不再只是一个后面跟着参数和函数体的可选的标识符。正在有 一大堆新的东西 将袭来。 那些已有的正则表达式将会再次被强制要求更新来应对这些变化。(话说我有说过这类似于UA嗅探吗? 嘘。) Minifiers及预处理器 我还要提到几个陈词滥调,这些玩意从来没有和函数反编译好好相处过—— minifiers和 预处理器 。 像UglifyJS这样的minifiers,和像 Caja 这样的预处理器/编译器倾向于自己调整这些地狱般的源代码,并重新命名参数。这就是为什么Angular的依赖注入 与minifiers不能正常一起工作 ,除非你使用 替代方法 。 也许这些并不是什么大不了的事,但它仍然是一个需要记住相关问题,不是吗? 长话短说 总结一下这些东西:看来函数反编译变得更安全了,但是——这取决于你的分析需求——它可能仍然还没有智能到你能够 完全依赖 它 。 你想把它用在你的应用程序/库中? 切记: 它仍然还不是 标准 用户定义的函数 一般看上去结果都很正常 有些 引擎很古怪 (特别是当它涉及到 源代码的布局,空格,注释,死代码等 ) 要注意有可能存在的 未来的奇怪引擎 (特别是那些需要保守处理内存/电量消耗的移动设备或其它 不寻常的 设备) 绑定函数 的字符串表示不会显示其源代码(但 有时会保留标识符 ) 你可能会遇到 非标准的扩展 (如Mozilla的表达式闭包) (冬天) ES6来了 ,函数现在可能会看起来与他们过去的样子非常不同 Minifiers /预处理器 不是你的好伙伴 P.S. 那些被重写了 toString 方法和/或 Proxy.createFunction 的函数是一种特别的情况,在那些特殊的情况下,我们需要特殊的考虑一下。 特别感谢 Andrea Giammarchi 提供的一些移动端的测试(在BrowserStack上所没有的)。 如果你喜欢这个,欢迎捐款给我 :) Gittip / Flattr 如果你感兴趣 --> 喜 欢 收 藏 分享该文章 分享到 印象笔记 人人网 Facebook Twitter Linkedin Google + E-Mail 推酷壹时代首发小米商店 > --> 相关微博 共有()条 请登录后评论 已发表评论数() 评论加载中 没有更多评论了^^ 所有评论--> 更多评论 评论加载失败,重新加载 &times; 文章纠错 邮箱 错误类型 正文不准确 排版有问题 没有分页内容 视频无法显示 图片无法显示 补充信息 提交 &times; 用户登陆 邮箱 密码 登 陆 关于 应用 反馈 讨论 &times; 有一些书要送给你[第一季] -->