Kokudoriing

技術系与太話ブログ

JSFuckから理解するECMAScriptの仕様

JSFuckとは

JSFuckは任意のJavaScriptプログラムを[, ], (, ), !, +からなる6文字で置き換える試みです。 意味分かんないですね、サンプルを見てみましょう。

alert(1)

上記のJavaScriptコードと

[][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]][([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[!+[]+!+[]+!+[]]]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]]]+([][[]]+[])[+[[+!+[]]]]+(![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[+!+[]]]]+([][[]]+[])[+[[+[]]]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]((![]+[])[+[[+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]+(!![]+[])[+[[+[]]]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[+!+[]]]+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+[+!+[]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[+!+[]]]+[[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]]])()

このやたら長ったらしいナニカはどちらも1と表示するアラートを表示します。 下の長ったらしいナニカもれっきとしたJavaScriptコードなので、疑り深い方はブラウザのコンソールなり何なりで試してみてください。

言語仕様

JavaScriptは標準委員会により批准された言語仕様があります。ECMAScriptです。 最近バージョン6になるぞーとか言ってる彼です。 つまり、ECMAScriptを読み解けばこの長ったらしいナニカが動く理由もわかるはずです。

バージョン

ECMAScriptにはいくつかバージョンがありますが、現在主流なのは主に2つあります。

1つ目はECMAScript3thで、よっぽど変な処理系でない限り仕様を満たしている、 デファクトスタンダードと言ってもいいほど広く普及しているバージョンです。

2つ目はECMAScript5thで、2009年に仕様が策定された*1、比較的新しい仕様です。 とは言え、IE9以降や、よっぽど古くないバージョンのChromeFirefox等モダンブラウザでは既にサポートされています。

この記事ではECMAScript5thを前提に話しを進めて行きます。

と、言う訳で手元にECMAScriptの仕様が必要です! ありがたいことにEcma Internationalから無料で閲覧可能です。(http://www.ecmascript.org/docs.php)

が、英語です。 英語は辛いという方は有志による翻訳版がありますのでこちらを。 ありがたいですねー。(http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/)

本文中では仕様書から引用する必要が多々あります。 3th/5thで共通の部分は3thの上記の日本語訳から、5thに対する言及はEcma Internationalの方から説明に必要な範囲で引用を行います。

この記事を読んで得られるもの

  • ECMAScriptとは?的なもやもやした疑問に対するふわっとした理解
  • 職場でドヤ顔できる
  • JavaScriptの暗黙の型変換への理解
  • 言語仕様を学ぶ事はそんなにアレな事じゃないという見識
  • 仕様が日本語訳される喜びと日本語訳されない悲しみ

この記事を読んでも得られないもの

  • 業務で必要な知識

構成要素

ではでは早速JSFuckについて勉強していきましょー。 JSFuckには嬉しい事に何故このような変換が可能なのかについて簡単なヒントが載っています。 Basicsという項にJSFuck<->JavaScriptとなる例が15個記載されています。 ひとまずこの15個の変換について順に考えて行きましょう!

false

最初の変換はこれです。

false => ![]

これは悪名高い暗黙の型変換です。 C言語と愉快な仲間たちだと0や空の配列などなどがfalse、それ以外がtrueを意味したりしちゃうアレです。

さて、暗黙の型変換はもちろんECMAScriptで正確に規定されています。 しかし、そもそも型とは何でしょう?

型そのものの意味についてはここでは取り扱いません*2、しかし、型はもっとも単純な理解として値の集合を意味します。 型そのものはより示唆に富んだ概念ですが、この記事ではそれ以上の理解は不要です。

ECMAScriptでは9つの型が定義されています。 JavaScriptにはユーザー定義型を作成する機能が無いので、ECMAScriptに存在しうる全ての型はその9個のみです。 *3

8 型(Type)で9つの型について説明されています。

8.3 Boolean 型 (Boolean Type)

Boolean 型は、true と false と呼ばれる二値を持つ論理的実体をあらわす。

とあるのでfalseの型はBooleanですね!*4

では![]はどうでしょう? !論理否定演算子です。そして、[]は空の配列ですね。では、配列の型は何でしょう? これは比較的有名な話しですが、JavaScriptでは配列はObject型となります *5。 Array型なんてものはありません

今までわかったことをまとめてみると、Object型である空の配列に論理否定演算子!を適用するとBoolean型のfalseが返ってくる。 どうやら秘密は!演算子にあるようです。

11.4.9 論理否定演算子 (Logical NOT Operator) ( ! )

  1. UnaryExpression を評価。
  2. GetValue(Result(1)) を呼出す。
  3. ToBoolean(Result(2)) を呼出す。
  4. Result(3) が true ならば、 false を返す。
  5. true を返す。

いきなり読めなくなってしまいました><。 しかし、落ち着いて上から1つ1つ見て行きましょう。

まず、UnaryExpressionとは単項式、つまりもしくは単項演算子 式となる式の事です*6。 つまり、1. UnaryExpression を評価。というのは! expressionexpressionを評価するよーって事です。 そりゃそうだって話しですね。

次に、GetValueは8.7.1 GetValue (V)を見ると、

  1. Type(V) が Reference でなければ、 V を返す。
  2. GetBase(V) を呼出す。
  3. Result(2) が null ならば、例外 ReferenceError を投げる。
  4. Result(2) の [[Get]] メソッドを呼び、プロパティ名に GetPropertyName(V) を渡す。
  5. Result(4) を返す。

とあります。ん、Type(V)って何? あー、これあれだわー、調べると芋づる式に知らない単語が出てくるパターンだわー。

5.2 アルゴリズム記述について (Algorithm Conventions)を見ると、

Type(x) は "x の型" の略記である。

とあります。結構まんまですね。

Referenceは9つある型の1つです。

8.7 Reference 型 (Reference Type)

Reference は、オブジェクトのプロパティへの参照である。Reference は基準オブジェクト(base object) とプロパティ名の 2 つの成分から構成される。

そして、2. GetValue(Result(1))のResult(1)は5.2 アルゴリズム記述について (Algorithm Conventions)で、

表記 Result(n) は "ステップ n の結果" の略記である。

とあります。つまり、Result(1)とは1. UnaryExpression を評価。の結果、の略記という事です。

頭が痛くなってきました。整理しましょう。

まず、![]とあると、論理否定演算子!の仕様である1. UnaryExpression を評価。に基づき、[]が評価されます。 次に、2. GetValue(Result(1)) を呼出す。とは、この場合GetValue([])と同じ意味だとわかります。 そして、GetValueは1. Type(V) が Reference でなければ、 V を返す。とあります。 ここで言うReference、つまり参照とは簡単に言うと変数等識別子を意味します。 つまり、[]リテラル、つまり値なのでReferenceではありません。 なので、Type(V)はReferenceでは無くObjectを返します。 よって、GetValueはV、つまり[]を返します。

おー、頑張れば読めるもんですねー。

お次は論理否定演算子!の3ステップ目、3. ToBoolean(Result(2)) を呼出す。です。

ここでようやく暗黙の型変換の出番です!長かったナー。

JavaScriptには型変換機能がありますが、明示的なものはなく、全てが暗黙的な変換です。 9.2 ToBooleanにBoolean型に変換されるルールが記述されています。

入力型 結果
Undefined false
Null false
Boolean 結果は入力引数と等しい。(無変換)
Number 引数が +0, -0, NaN ならば結果は false; そうでなければ true
String 引数が空文字列 (長さ 0) ならば結果は false; そうでなければ true
Object true

Result(2)2. GetValue(Result(1)) を呼出す。の結果なので、[]の事です。 つまり、3. ToBoolean(Result(2)) を呼出す。3. ToBoolean([]) を呼出す。という意味です。

[]はObject型なので、上記ルールにより結果は必ずtrueが返ります。 そう、配列が空だろうとなんだろうとObject型である限りtrueが返ります。

論理否定演算子!に戻りましょう。 Result(3) が true ならば、 false を返す。とあります。 これは簡単ですね、論理否定演算子の仕事をしているだけです。

Result(3)trueだったので今回の論理否定演算子の評価はステップ3で終了し、falseが返ります。

つまり、![]falseなのです!長かった~。

true

お次は!![]です。が、これはもう簡単ですね。

!![]!(![])、つまり![]したものを!します。 ![]falseであることは先ほどやりました。つまり、!falseがわかれば良いのです。

11.4.9 論理否定演算子 (Logical NOT Operator) ( ! )

  1. UnaryExpression を評価。
  2. GetValue(Result(1)) を呼出す。
  3. ToBoolean(Result(2)) を呼出す。
  4. Result(3) が true ならば、 false を返す。
  5. true を返す。

の2ステップ目が変化します。 ![]の場合、GetValue(Result(1))GetValue([])で、これは[]を返します。

!falseの場合、GetValue(Result(1))GetValue(false)で、これはfalseを返します *7

そして、3. ToBoolean(Result(2)) を呼出す。ToBoolean(false)です。 9.2 ToBoolean の、入力型がBooleanの結果の項を見ると、

結果は入力引数と等しい。(無変換)

とあります。そりゃそうです。BooleanをBooleanに変換するということはすなわち何も変換しないということです。

ということは、ToBoolean(false)falseを返します。

4. Result(3) が true ならば、 false を返す。で、Result(3)falseなので、スルーされます。

ステップ5に評価が移りました。 > 5. true を返す。とあるので、何事も無くtrueが返ります。

そう、!false、つまり!![]trueなのです!

undefined

まだ2/15しか消化してないことに焦りつつ、お次はundefinedを得るJSFuckの式です。

undefined => [][[]]

頭が痛くなるコードですが、頑張って紐解いていきましょう。

まず、[][[]]の最初の[]は空の配列です。 次の[[]][([])]、つまり以下のコードのような意味を持ちます。

var item = [];
var index = [];
item[index];

つまり、空の配列に、空の配列をプロパティ名にプロパティを取得しているわけです。ややこしい事この上ないですね。

[]はプロパティアクセス演算子という演算子です。 早速評価ステップを見てみましょう。

11.2.1 プロパティアクセス演算子 (Property Accessors)

MemberExpression [ Expression ] は次のように評価される:

  1. MemberExpression を評価。
  2. GetValue(Result(1)) を呼出す。
  3. Expression を評価。
  4. GetValue(Result(3)) を呼出す。
  5. ToObject(Result(2)) を呼出す。
  6. ToString(Result(4)) を呼出す。
  7. 基準オブジェクトが Result(5) でプロパティ名が Result(6) である Reference 型の値を返す。

1. MemberExpression を評価。[][[]]における最初の[]です。つまり[]ですね。

2. GetValue(Result(1)) を呼出す。GetValue([])です。 そう、型がReferenceでなければ引数をそのまま返すのでした。つまり[]が返ります。

3. Expression を評価。[[]]の部分の内側の[]を評価するという意味です。 これも配列リテラルなので[]ですね。

4. GetValue(Result(3)) を呼出す。GetValue([])なので[]が返ります。

5. ToObject(Result(2)) を呼出す。にてToObjectというものが出てきました。 これはObjectに型変換する暗黙の型変換を意味します。

9.9 ToObject

入力型 結果
Undefined 例外 TypeError を投げる。
Null 例外 TypeError を投げる。
Boolean [[value]] プロパティがそのブーリアンである Boolean オブジェクトを新しく作成する。
Number [[value]] プロパティがその数値である Number オブジェクトを新しく作成する。
String [[value]] プロパティがその文字列である String オブジェクトを新しく作成する。
Object 結果は入力引数である (変換しない)。

ToObject(Result(2))ToObject([])です。[]はObject型なので、無変換ですね。 つまり[]が返ります。

6. ToString(Result(4)) を呼出す。とあるので、お次はString型への暗黙の型変換です。

9.8 ToString

入力型 結果
Undefined "undefined"
Null "null"
Boolean 引数が true ならば、結果は "true" 。引数が false ならば、結果は "false" 。
Number 下のノートを参照。
String 入力引数を返す。 (無変換)
Object 次のステップを適用:
  1. ToPrimitive(入力引数, hint String) を呼出す。
  2. ToString(Result(1)) を呼出す。
  3. Result(2) を返す。

ToString(Result(4))ToString([])であり、[]はObject型です。 おっと、Object型からString型への変換はちょい複雑です。

  1. ToPrimitive(入力引数, hint String) を呼出す。
  2. ToString(Result(1)) を呼出す。
  3. Result(2) を返す。

ToPrimitiveは引数をプリミティブ型の値、つまり非Object型に変換します。 なので、非Object型が渡された場合は無変換です。 重要なのはObject型が渡された場合です。

9.1 ToPrimitive の入力型がObject型の場合を見てみましょう。

Object のデフォルトの値を返す。オブジェクトのデフォルトの値は、オブジェクトの内部メソッド [[DefaultValue]] に選択的ヒント PreferredType を渡して取得される。[[DefalutValue]] メソッドの挙動は、全ての ECMAScript オブジェクトの仕様によって定義される。(セクション 8.6.2.6)

[[DefalutValue]]のように[[]]で囲まれたプロパティの事を内部プロパティと言います。 内部プロパティはReference型のように、JavaScript言語には存在しません。 言語仕様上にのみ存在する、説明のためのプロパティです。 当然JavaScriptからアクセスすることは出来ません。

[[DefalutValue]]は引数としてヒントを取り、primitive値、つまりObject型でもReference型でもない値を返します。

ヒントはどのようなprimitive値を得たいか?という、文字通りヒントとなる情報です。 ヒント無しで呼ばれた場合、Numberをヒントとして扱います。

ToStringの評価ステップ1では1. ToPrimitive(入力引数, hint String) を呼出す。と書かれていましたので、 今回考えるべきはヒントがStringの時のToPrimitiveの動きです。 ヒントがStringの時のToPrimitiveの動きは仕様書でまんま解説されています。

8.6.2.6 [[DefaultValue]] (hint)

O の [[DefaultValue]] メソッドがヒント String で呼出されると、次のステップが取られる:

  1. O の [[Get]] メソッドを、引数 "toString" で呼出す。
  2. Result(1) がオブジェクトでなければ、ステップ 5 へ。
  3. Result(1) の [[Call]] メソッドを、this 値 O と空の引数のリストで呼出す。
  4. Result(3) がプリミティブ値であれば、Result(3) を返す。
  5. O の [[Get]] メソッドを、引数 "valueOf" で呼出す。
  6. Result(5) がオブジェクトでなければ、ステップ 9 へ。
  7. Result(5) の [[Call]] メソッドを、this 値 O と空の引数のリストで呼出す。
  8. Result(7) がプリミティブ値であれば、 Result(7) を返す。
  9. 例外 TypeError を投げる。

最初は1. O の [[Get]] メソッドを、引数 "toString" で呼出す。です。 [[Get]]メソッドも[[DefaultValue]]メソッドと同じく、内部プロパティの1つです。

8.6.2.1 [[Get]] (P)

O の [[Get]] メソッドがプロパティ名 P で呼出されると、次のステップがとられる:

  1. O が P という名前のプロパティを持っていなければ、ステップ 4 へ進む。
  2. そのプロパティの値を取得する。
  3. Result(2) を返す。
  4. O の [[Prototype]] が null ならば、undefined を返す。
  5. [[Prototype]] の [[Get]] メソッドを、プロパティ名 P で呼び出す。
  6. Result(5) を返す。

これは単純にオブジェクトOのプロパティPを取得するためのメソッドです。 内部プロパティなのでJavaScript言語からはアクセス出来ません。 オブジェクトOのプロパティPを取得する仕組みを解説するために仮想的に存在するメソッドです。

OがPという名前のプロパティを持っていない場合の評価は少し複雑ですが、今回の場合、オブジェクトOは[]、つまり配列です。 配列にはtoStringプロパティが存在するので、ステップ2、ステップ3より、[].toStringが返ります。

さて、[[DefalutValue]]に戻りましょう。 2. Result(1) がオブジェクトでなければ、ステップ 5 へ。とあります。 Result(1)、つまり[].toStringは関数です。JavaScriptでは関数もObject型の値*8なので、次のステップ3へ評価が移ります。

3. Result(1) の [[Call]] メソッドを、this 値 O と空の引数のリストで呼出す。とあります。 [[Call]]内部プロパティは読んで字の如く、です。ただの関数呼び出しです。

お次の4. Result(3) がプリミティブ値であれば、Result(3) を返す。の前に問題です! Result(3)、つまり[].toString()は何でしょう?

















"[]"ではなく""です。 ちなみに[1].toString()"1"[1, 2, 3].toString()"1,2,3"です。

この直感的でない配列のtoStringメソッドも仕様で決まっている振る舞いです。 15.4.4.2 Array.prototype.toString ( ) にこう書かれています。

この関数の呼び出しの結果は、このオブジェクトに組込み join メソッドを引数無しで呼出すのと同様である。


以下はECMAScript3thと5.1thの仕様変更に関するちょっとしたコラムです。そこまで関係性のある話ではないので、興味の無い方は飛ばしていただいて結構です:)

配列のtoStringメソッドの仕様ですが、さらに続けてこうあります。

toString 関数は汎用的ではない; this 値が Array オブジェクトでなければ、例外 TypeError を投げる。それゆえ、他の種類のオブジェクトにメソッドとして転用できない。

これはECMAScript3thの仕様です。では、5.1thの仕様を見てみましょう。

http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf 15.4.4.2 Array.prototype.toString ( )

When the toString method is called, the following steps are taken:

  1. Let array be the result of calling ToObject on the this value.
  2. Let func be the result of calling the [[Get]] internal method of array with argument "join".
  3. If IsCallable(func) is false, then let func be the standard built-in method Object.prototype.toString (15.2.4.2).
  4. Return the result of calling the [[Call]] internal method of func providing array as the this value and an empty arguments list.

NOTE The toString function is intentionally generic; it does not require that its this value be an Array object. Therefore it can be transferred to other kinds of objects for use as

まずはNOTEの部分を読んでみましょう。

The toString function is intentionally generic

つまり、3thの、汎用的でなかったtoStringは5.1thでは汎用的なメソッドとして改められました。 NOTEの前の4つの評価ステップはtoStringを汎用的にするためのものです。

ざっくりと説明するとObject型に型変換したthis[[Get]]でjoinプロパティを取得します。 joinプロパティが関数として取得出来た場合はjoinを適用した結果を返し、取得できなかった場合はObject.prototype.toStringを適用したものを返します。

この3つめの評価ステップによってArray.prototype.toStringメソッドが汎用的になったわけですね。

そう、意外と知られてない気がするんですが、Array.prototype.toStringはArray.prototype.joinを適用したものを返します。

さて、話しを[[DefalutValue]]に戻しましょう。 4. Result(3) がプリミティブ値であれば、Result(3) を返す。でした。

Result(3)""でした。そして""はString型です。非Reference型又は非Object型の値はプリミティブ型なので、ここで""が返ります。

さて、整理しましょう。 今、プロパティアクセス演算子[]ToString(Result(4))からObject型をToStringに渡した時の

  1. ToPrimitive(input argument, hint String) を呼出す。
  2. ToString(Result(1)) を呼出す。
  3. Result(2) を返す。

のヒントがStringの時のToPrimitive([])""である事を知りました。

2. ToString(Result(1)) を呼出す。は少し奇妙です。 ToPrimitiveをヒントをStringで呼び出したものの結果をString型に変換する意味は何でしょう?

結局のところ、ToPrimitiveはプリミティブ型への変換でしかなく、ヒントは所詮ヒントでしかないということです。 ヒントがStringで呼び出された時のToPrimitiveの評価ステップを見ても、値を返す条件はプリミティブかどうかであり、String型であるかどうかでは無いということです。 なので、確実にString型を返すためにToStringを適用してる訳です。

ToString("")は無変換ですから、""が返ります。

さて、長い長いToString([])の評価の旅は""を返すことでようやく終わりました。

プロパティアクセス演算子の評価ステップも最後です。

基準オブジェクトが Result(5) でプロパティ名が Result(6) である Reference 型の値を返す。を言い換えると、 基準オブジェクトが [] でプロパティ名が "" である Reference 型の値を返す。となります。 つまり、[][""]です。

プロパティへのアクセスとは、すなわち[[Get]]です。

8.6.2.1 [[Get]] (P)

O の [[Get]] メソッドがプロパティ名 P で呼出されると、次のステップがとられる:

  1. O が P という名前のプロパティを持っていなければ、ステップ 4 へ進む。
  2. そのプロパティの値を取得する。
  3. Result(2) を返す。
  4. O の [[Prototype]] が null ならば、undefined を返す。
  5. [[Prototype]] の [[Get]] メソッドを、プロパティ名 P で呼び出す。
  6. Result(5) を返す。

これはプロトタイプチェーンを言っています。 ステップ5で行っていることはプロトタイプチェーンそのものです *9 *10

もちろん配列オブジェクトに""なんていうプロパティ名のプロパティは存在しません。 これはプロトタイプチェーンを辿った所で同様です。

プロトタイプチェーンは最終的にObjectオブジェクトに到達します。 Objectオブジェクトの[[Prototype]]はnullなので、ステップ4よりundefinedが返ります。

そう、[][[]][][""]となりundeifnedを返します。

NaN

NaN => +[![]]

+[![]]のうち、![]は既にやりました!![]falseを返すんでしたねー。

つまり[![]][false]であり、+[![]]+[false]です。 +演算子が曲者のようです。

+演算子は単項+演算子と加法演算子の2つがあります。 が、今回はオペランドを1つしか取らないので単行+演算子の方です。

11.4.6 単項 + 演算子 (Unary + Operator)

  1. UnaryExpression を評価。
  2. GetValue(Result(1)) を呼出す。
  3. ToNumber(Result(2)) を呼出す。
  4. Result(3) を返す。

ステップ1は[false]を評価し、ステップ2はGetValue([false])となり[false]を返します。 今まで散々出てきた定型パターンですね。

3. ToNumber(Result(2)) を呼出す。[false]をNumber型に型変換する事を意味します。

9.3 ToNumber

入力型 結果
Undefined NaN
Null +0
Boolean 引数が true ならば結果は 1.false ならば +0。
Number 結果は入力引数と等しい。(無変換)
String 特殊な変換が必要。後述する。
Object 次のステップを適用:
  1. ToPrimitive(input argument, hint Number) を呼出す。
  2. ToNumber(Result(1)) を呼出す。
  3. Result(2) を返す。

[false]はObject型なのでToNumberは以下の評価ステップを処理します。

  1. ToPrimitive(input argument, hint Number) を呼出す。
  2. ToNumber(Result(1)) を呼出す。
  3. Result(2) を返す。

ヒントがStringの時のObject型を受け取るToPrimitiveの評価は先ほど嫌というほど考えました。 今回はヒントがNumberの時のObject型を受け取るToPrimitiveの動きです。

Object型を受け取るのでToPrimitiveは[[DefaultValue]]内部プロパティをヒントがNumberで呼び出します。

8.6.2.6 [[DefaultValue]] (hint)

O の [[DefaultValue]] メソッドがヒント Number で呼出されると、次のステップが取られる:

  1. O の [[Get]] メソッドを、引数 "valueOf" で呼出す。
  2. Result(1) がオブジェクトでなければ、ステップ 5 へ。
  3. Result(1) の [[Call]] メソッドを、this 値 O と空の引数のリストで呼出す。
  4. Result(3) がプリミティブ値であれば、Result(3) を返す。
  5. O の [[Get]] メソッドを、引数 "toString" で呼出す。
  6. Result(5) がオブジェクトでなければ、ステップ 9 へ。
  7. Result(5) の [[Call]] メソッドを、this 値 O と空の引数のリストで呼出す。
  8. Result(7) がプリミティブ値であれば、 Result(7) を返す。
  9. 例外 TypeError を投げる。

1. O の [[Get]] メソッドを、引数 "valueOf" で呼出す。なので、[false].valueOfが返ります。 ステップ2、ステップ3より[false].valueOf()を呼び出します。

4. Result(3) がプリミティブ値であれば、Result(3) を返す。とあり、 [false].valueOf()[false]です。配列はObject型であり、プリミティブ型ではないのでステップ5へと続きます。


以下はvalueOfについての説明です。JSFuckを読み解く上ではそれほど重要ではないですが、JavaScriptコードを書く上では結構重要な内容だったりします。

valueOfメソッドはArray.prototypeには無く、Object.prototypeに存在します。 Object.prototypeの仕様を見ると、 15.2.4.4 Object.prototype.valueOf ( )

valueOf メソッドはその this 値を返す。

とあります。なので、[false].valueOf()[false]を返します。

では何故デフォルトでthisを返すメソッドがあるのかというと、オーバーライドされることを期待しているわけです。

今まで散々見てきたように、暗黙の型変換、特にToNumber(Object)ToString(Object)において、ToPrimitive(Object)が呼ばれます。 ToPrimitive(Object)[[DefaultValue]]を呼ぶわけですが、Stringをヒントに呼ぶ時とNumberをヒントに呼ぶ時では対称的な評価の変化が起きます。

8.6.2.6 [[DefaultValue]] (hint)

O の [[DefaultValue]] メソッドがヒント String で呼出されると、次のステップが取られる:

  1. O の [[Get]] メソッドを、引数 "toString" で呼出す。
  2. Result(1) がオブジェクトでなければ、ステップ 5 へ。
  3. Result(1) の [[Call]] メソッドを、this 値 O と空の引数のリストで呼出す。
  4. Result(3) がプリミティブ値であれば、Result(3) を返す。
  5. O の [[Get]] メソッドを、引数 "valueOf" で呼出す。
  6. Result(5) がオブジェクトでなければ、ステップ 9 へ。
  7. Result(5) の [[Call]] メソッドを、this 値 O と空の引数のリストで呼出す。
  8. Result(7) がプリミティブ値であれば、 Result(7) を返す。
  9. 例外 TypeError を投げる。

 

O の [[DefaultValue]] メソッドがヒント Number で呼出されると、次のステップが取られる:

  1. O の [[Get]] メソッドを、引数 "valueOf" で呼出す。
  2. Result(1) がオブジェクトでなければ、ステップ 5 へ。
  3. Result(1) の [[Call]] メソッドを、this 値 O と空の引数のリストで呼出す。
  4. Result(3) がプリミティブ値であれば、Result(3) を返す。
  5. O の [[Get]] メソッドを、引数 "toString" で呼出す。
  6. Result(5) がオブジェクトでなければ、ステップ 9 へ。
  7. Result(5) の [[Call]] メソッドを、this 値 O と空の引数のリストで呼出す。
  8. Result(7) がプリミティブ値であれば、 Result(7) を返す。
  9. 例外 TypeError を投げる。

ステップ1とステップ5で呼ばれているメソッドがtoStringとvalueOfで対称になっています。

覚えておくべき事項は、String型への型変換はtoStringを試した後にvalueOfが試されるが、Number型への型変換はvalueOfを試した後にtoStringが試されるという評価順序の差です。

さらに、もう1つ覚えておくべきことがあります。 [[DefaultValue]]をヒント無しで呼ぶと、通常Numberをヒントに呼ぶことを意味しますが、 ヒント無しにDateオブジェクトを渡すとStringをヒントに呼ぶことを意味します!

今まで見てきた演算子は全てToPrimitiveをヒント付きで呼び出すので上記の対称性やDateオブジェクトの特別扱いは無視できます。 しかし、JavaScript演算子の中にはToPrimitiveをヒント無しで呼び出すものがあります。 加法演算子+や等価演算子==、不等価演算子!=の3つがそうです。

11.6.1 加法演算子 (The Addition operator) ( + )

11.8.5 抽象的関係比較アルゴリズム (The Abstract Relational Comparison Algorithm)

加法演算子+や等価演算子==、不等価演算子!=をDateオブジェクトやvalueOfやtoStringをオーバーライドしたオブジェクトに使用する際は十分に気をつける必要があります。

最も優れた解決方法は暗黙の型変換を使用せずに明示的な変換メソッドを用意することです。

さあ、[[DefaultValue]]の続きです。 ステップ5, 6, 7により[false].toString()を呼び出します。 先ほどやりましたね、Array.prototype.toStringはArray.prototype.joinを呼ぶのでした。 つまり、"false"が返ります。

8. Result(7) がプリミティブ値であれば、 Result(7) を返す。とあり、"false"はString型です。 String型はプリミティブ型なので評価終了です。"false"が返ります。

ToNumberの評価に戻りましょう。 2. ToNumber(Result(1)) を呼出す。とあります。 これもToPrimitiveはプリミティブを返すのであって、ヒントを付けようがNumberを返すとは限らないからです。 そして今回はToPrimitive([false], hint=Number)"false"、つまりString型を返してきました。

ToNumber(String)はかなり複雑な変換を必要とします。

9.3.1 String 型に適用される ToNumber (ToNumber Applied to the String Type)

この複雑なルールは16進数表記や指数表記等をサポートするためのものです。 ここで見るべきポイントはこの一文です。

文法が文字列を StringNumericLiteral として解釈不能ならば、 ToNumber の結果は NaN である。

"false"はStringNumericLiteral、つまり16進数表記や指数表記等をサポートするための特殊な数値表現として認められていません。 よって、ToNumber("false")NaNを返します。

これで単項+演算子も終わりました。 そう、単項+演算子オペランドをToNumberするだけの演算子なのです。

そしてこれですべての評価が終わりました。 +[![]]+[false]となり、+"false"となりNaNとなります。

+[![]]は紛れも無くNaNです!

余談

ToNumberの仕様をよく見てください! 入力型がUndefinedの部分です!

そう、ToNumber(Undefined)NaNを返すのです。 そして我々はundefinedの作り方を知っています。[][[]]ですね。

つまり、+[][[]]+[![]]NaNなのです! (嘘だと思うのならブラウザのコンソールで試してみてください。今までの挙動は全てECMAScript3thの仕様を準拠しているのでIE6だろうと最新版のChromeだろうと同じ動きをします。)

では何故JSFuckでは+[![]]の方を使用しているのでしょう? 推測に過ぎませんが、恐らく文字数削減のためでしょう。 ただでさえ長ったらしいJSFuckですから、文字数削減は大きな意味がありそうです:)

0

0 => +[]

単項+演算子です。これはToNumber([])と同じ意味です。 これは先ほどのToNumber([false])と似てそうですね。

まず、ToPrimitive([], hint=Number)""を返します。 つまりToNumber("")となります。つまり先ほどの複雑なルールが適用されます。

9.3.1 String 型に適用される ToNumber (ToNumber Applied to the String Type)

空や空白である StringNumericLiteral は +0 に変換される。

やりました!これでToNumber("")となり、単項+演算子の評価が終了します。

+[]+0なのです!

+0, -0

+0という表記は一見すると奇妙です。まるで-0があるみたいに見えます。 そして、JavaScriptでは本当に-0が存在します。 …とは言ってもこれはJavaScriptに限ったことではありません。

8.5 Number 型 (Number Type)にあるように、 JavaScriptのNumber型の値はIEEE754の倍精度浮動小数点数です。 IEEE754は+0と-0を持つというだけの話です。

1

1 => +!+[]

+!+[]のうち、+[]は先ほどやりました。これは0です。 つまり、+!0ということになります。

論理否定演算子は最初に![]falseになる例で出てきました。 単項+演算子オペランドをToNumberする演算子なら、論理否定演算子オペランドをToBooleanする演算子です。

入力型がNumberのToBooleanの結果は以下のようになります。

9.2 ToBoolean

引数が +0, -0, NaN ならば結果は false; そうでなければ true

つまり、ToBoolean(0)falseです。

!論理否定演算子なのでToBooleanの結果をひっくり返し、!0trueとなります。

つまり、+!+[]+!0となり、+trueとなります。 +trueToNumber(true)を意味します。

入力型がBooleanのToNumberの結果は以下のようになります。

9.3 ToNumber

引数が true ならば結果は 1.false ならば +0。

そう、+true1です。

よって、+!+[]+!0となり、+trueとなり、1となります。

2

2 => !+[]+!+[]

両脇の!+[]は先ほどやりました。これはtrueです。 つまり、true+trueです。

この+演算子オペランドを2つ取っています。なので、先ほどまでの単項+演算子ではなく、加法演算子です。

11.6.1 加法演算子 (The Addition operator) ( + )

  1. AdditiveExpression を評価。
  2. GetValue(Result(1)) を呼出す。
  3. MultiplicativeExpression を評価。
  4. GetValue(Result(3)) を呼出す。
  5. ToPrimitive(Result(2)) を呼出す。
  6. ToPrimitive(Result(4)) を呼出す。
  7. Type(Result(5)) が String または Type(Result(6)) が String ならば、 ステップ 12 へ。 (このステップは、関係演算子の比較アルゴリズムのステップ 3 とは異なり、 かつ ではなく または であることに注意。)
  8. ToNumber(Result(5)) を呼出す。
  9. ToNumber(Result(6)) を呼出す。
  10. Result(8) と Result(9) に、加法演算を適用する。
  11. Result(10) を返す。
  12. ToString(Result(5)) を呼出す。
  13. ToString(Result(6)) を呼出す。
  14. Result(12) に Result(13) を連結する。
  15. Result(14) を返す。

かなり複雑ですね。 ステップ6までが言っているのはleft + rightという加法演算式においてToPrimitive(left)ToPrimitive(right)を評価しなさい、という事です。

この時のToPrimitiveはヒントが無いことに注意してください。 Dateオブジェクト以外がToPrimitiveにヒント無しで渡された場合、Numberをヒントに[[DefaultValue]]を評価します。

Numberがヒントの場合、valueOfを試した後にtoStringを試すのでしたね。 true.valueOf()trueです。true、つまりBoolean型はプリミティブ型なのでToPrimitive(true)trueを返します。

加法演算子の評価に戻りましょう。 ステップ7ではToPrimitiveの結果がString型であると特別扱いをするようですが、今回の場合はBoolean型なので該当しません。

ステップ8,9によりToNumber(true)となります。 ToNumber(Boolean)は先ほどやりましたね、trueなら1、falseなら+0でした。 なのでToNumber(true)1を返します。

ステップ10は再帰的なものではありません。 ここで言う加法演算とは単純に数値の加算を意味します。 つまり、1+1であり、2が返ります。

以上で加法演算子の評価が終了し、!+[]+!+[]2を返します。大成功です!

余談

ちょっと待ってください!我々は先ほど1を作ることに成功しました。+!+[]です。これは紛れも無く1そのものです。

では+!+[]+!+[]を足し合わせれば2が出来るのでしょうか?やってみましょう!

+!+[]++!+[]
> SyntaxError: Unexpected token !

おっと、シンタックスエラーです。JavaScriptでは++のように単項+演算子と加法演算子の連結を正しく受理しません。 このコードは下記のようにすると上手く行きます。

(+!+[])+(+!+[])

もう少し短くすることも出来ます。

+!+[]+(+!+[])

しかし!+[]+!+[]の方が暗黙の型変換の力を借りることが出来るのでより短く書くことが出来ます。

10

10 => [+!+[]]+[+[]]

ここまで来ると見知ったパーツばかりになってきました。 +!+[]1なので、[+!+[]][1]ですね。 +[]0なので、[+[]][0]です。

つまり、[+!+[]]+[+[]][1]+[0]と同じです。 またしても加法演算子の出番です。

11.6.1 加法演算子 (The Addition operator) ( + )

  1. AdditiveExpression を評価。
  2. GetValue(Result(1)) を呼出す。
  3. MultiplicativeExpression を評価。
  4. GetValue(Result(3)) を呼出す。
  5. ToPrimitive(Result(2)) を呼出す。
  6. ToPrimitive(Result(4)) を呼出す。
  7. Type(Result(5)) が String または Type(Result(6)) が String ならば、 ステップ 12 へ。 (このステップは、関係演算子の比較アルゴリズムのステップ 3 とは異なり、 かつ ではなく または であることに注意。)
  8. ToNumber(Result(5)) を呼出す。
  9. ToNumber(Result(6)) を呼出す。
  10. Result(8) と Result(9) に、加法演算を適用する。
  11. Result(10) を返す。
  12. ToString(Result(5)) を呼出す。
  13. ToString(Result(6)) を呼出す。
  14. Result(12) に Result(13) を連結する。
  15. Result(14) を返す。

ステップ5,6によりToPrimitive([1])ToPrimitive(0)を評価しましょう。 ヒント無しなのでNumberをヒントとして[[DefaultValue]]が評価されます。

[1].valueOf()[1]であり、[0]も同様です。 [1]はプリミティブではないので、[1].toString()が試されます。 [1].toString()"1"であり、[0].toString()"0"です。 どちらもString型なのでプリミティブです。

加法演算子のステップ7では先ほどのToPrimitiveの結果がString型ならステップ12へ移動するよう行っています。 ステップ12,13はString型に対するToStringを呼び出すので今回の場合はどちらも無変換です *11

14. Result(12) に Result(13) を連結する。とあるので"1""0"が連結され、"10"が返ります。

以上で評価が終了し、[+!+[]]+[+[]]"10"を返します。

ん?、先ほどまでの数値はNumber型でしたが、今回の10はString型ですね。

型による変換まとめ

疲れました。今まで何をやってきたのか、よくわからなくなってきたのでここらで振り返ってみましょう。

Array => []

Arrayオブジェクトを作るのは簡単でした。配列リテラルを使うだけです。 JSFuckの使用可能文字種の都合上、このArrayオブジェクトを以下に変換するかがJSFuckのキモです。 単純ですが、JSFuckにおける根幹を担うオブジェクトです。

Number => +[]

単項+演算子はToNumberを評価するのでNumber型を得るのにうってつけです。

String => []+[]

加法演算子は主に2つの型を得られます。 1つはNumber型で、これはleftもrightもString型でない場合です。 もう1つはString型で、これはleftもしくはrightがString型の場合です。

String型を得ることのできる演算子は貴重です。 ほとんどの演算子はNumber型かBoolean型を返します。 貴重なString型を得るためにも、加法演算子は抑えておきたい演算子です。

Boolean => ![]

否定論理演算子はToBooleanを評価するのでBoolean型が必要なときに便利です。

Function => []["filter"]

これは[].filterと同じことですね。 確かに関数はobj["propName"]形式で取り出すことができます。

でも待ってください!そもそも"filter"という文字をJSFuckで作る方法が無いのではないでしょうか? 実はJSFuckで任意の文字列*12を作ることが出来ます。 詳しくは後述します。

暗黙の型変換

簡単に振り返ってみると、全ての変換は暗黙の型変換に依存していることがわかります。 つまり、型毎に定形の変形パターンがある、ということです。

eval => []["filter"]["constructor"]( CODE )()

[]["filter"]["constructor"]( CODE )()

ここまでくるとようやくJavaScriptコードっぽくなりますね。 Function.prototype.constructorはFunctionを参照しています。 Function.prototype.constructor === Functionはtrueです。

つまり、[]["filter"]["constructor"]( CODE )()[].filter.constructor( CODE )()となり、Function( CODE )()となります。

また、Function( CODE )new Function( CODE )と等価なので、結局のところ、new Function( CODE )()となります。

15.3.1 関数として呼出される Function コンストラクタ

Function がコンストラクタとしてではなく関数として呼出される場合、それは新しい Function オブジェクトを作成し初期化する。したがって、関数呼び出し Function(…) は、同じ引数をとるオブジェクト生成式 new Function(…) と等価である。

new Function( CODE )はCODE文字列が関数の内部となるような無名関数を返します。 つまり、これはeval*13をしているわけです。

window => []["filter"]["constructor"]("return this")()

[]["filter"]["constructor"]("return this")()

これは先ほどのevalを元にreturn thisをeval*14したもの、windowオブジェクトが返ります。

最後に

さて、JSFuckのBasics項15個をECMAScriptという言語仕様の観点から考えてみました。

でも待ってください!肝心の任意文字列の生成がまだです!

では文字列aについて考えてみましょう。

![]falseでした。 ![]+[]"false"""の文字列結合となり、"false"です。 "false"[1]"a"なので、(![]+[])[+!+[]]"a"です。

JSFuckにはこのような方法でアルファベットが全て生成できることが示されています!

https://github.com/aemkei/jsfuck/blob/master/jsfuck.js

ですがこの方法では日本語を対応することが出来ません。 しかし、アルファベットが全て使えるevalのあるJavaScriptという環境がある今、何も恐れる必要はありません。

String.fromCharCode(12354)"あ"を返します。 あとはString.fromCharCode(12354)となる文字列をJSFuckで作るだけです。 これらは全てJSFuckで生成可能な文字列なので、JSFuckはUnicode文字全てに対応したことになります。

合わせて読みたい

http://sla.ckers.org/forum/read.php?24,32930

*1:修正版の5.1thは2011年策定

*2:TaPLとか読んで下さい

*3:内3つはECMAScriptにしかなくJavaScriptには無いのでJavaScriptでは型は6つしかありません

*4:仕様を見るまでも無い事ですが…

*5:15.4 Array オブジェクト (Array Objects)

*6:厳密には異なります…が、今はこの理解で十分です

*7:GetValueは引数の型がReferenceでなければ引数をそのまま返すのでした

*8:15.3 Function オブジェクト

*9:[[Prototype]]をJavaScript言語上で扱えるようにしたChromeFirefoxの独自実装が_proto__プロパティです

*10:ECMAScript5hが使える環境なら_proto__、つまり[[Prototype]]内部プロパティ相当を返すObject.getPrototypeOfが使用可能です

*11:ステップ6ではleft + rightのleft, rightどちらか一方がString型である事を要求しています。 しかし、ステップ12ではどちらか一方がString型で無く、また、どちらがそうなのかが判別できないのでどちらもToStringに渡しています。 これはString型と加法演算子による暗黙の型変換を意図した挙動です

*12:日本語を含む!

*13:任意文字列のプログラム評価

*14:つまりはプログラムとして評価