返回 登录
3

对一行神奇js代码的解析

原文Reverse Engineering One Line of JavaScript
作者:Alex Kras
翻译:雁惊寒

译者注:本文作者结合数学知识,图文并茂、非常细致地对一行神奇的混淆过的js代码进行了剖析。以下是译文。

几个月前,我看到一封电子邮件,询问是否有人可以破解这一行JavaScript代码。

<pre id=p><script>n=setInterval("for(n+=7,i=k,P='p.\\n';i-=1/k;P+=P[i%2?(i%2*j-j+n/k^j)&1:2])j=k/i;p.innerHTML=P",k=64)</script>

这行代码可以渲染出下面这个动态图,你可以在这个网页上看到。它的作者是Mathieu ‘p01’ Henri,www.p01.org的站长,你可以在他这个网站上找到很多很酷的东西。

好,我来接受挑战!

第一步,让这行代码变得可读

首先,将HTML标签保存在HTML文件中,将JavaScript代码保存为code.js文件。同时,用双引号把id="p"中的p引起来。

index.html

<script src="code.js"></script>
<pre id="p"></pre>

我注意到有一个变量k,它只是一个常量,所以我把它从那一行代码中移出来,并重命名为delay

code.js

var delay = 64;
var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var n = setInterval(draw, delay);

接着,var draw只是一个字符串,它在setInterval中被执行,跟eval的效果一样。因为setInterval可以接受要评估的函数或字符串。我把它移到一个实际的函数中。 旧代码仍然保留在那以供参考。

我注意到的另一件事是,元素p实际上是指在HTML中id为p的DOM元素,就是上面我用引号引起来的变量。 只要id仅由字母或数字组成,则可以通过JavaScript的id来引用元素。 我添加了一行document.getElementById("p"),这样看起来更直观。

var delay = 64;
var p = document.getElementById("p"); // < --------------
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
    for (n += 7, i = delay, P = 'p.\n'; i -= 1 / delay; P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]) {
        j = delay / i; p.innerHTML = P;
    }
};
var n = setInterval(draw, delay);

接着,我声明了变量ipj,并把他们放到函数的最前面。

var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
    var i = delay; // < ---------------
    var P ='p.\n';
    var j;
    for (n += 7; i > 0 ;P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]) {
        j = delay / i; p.innerHTML = P;
        i -= 1 / delay;
    }
};
var n = setInterval(draw, delay);

我将for循环分解成一个while循环。 仅保留for中的CHECK_EVERY_LOOP部分(for循环总共包括3个部分,分别为:RUNS_ONCE_ON_INIT; CHECK_EVERY_LOOP; DO_EVERY_LOOP),并将其他部分移到循环体的内部或外部。

var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
    var i = delay;
    var P ='p.\n';
    var j;
    n += 7;
    while (i > 0) { // <----------------------
        //Update HTML
        p.innerHTML = P;

        j = delay / i;
        i -= 1 / delay;
        P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2];
    }
};
var n = setInterval(draw, delay);

将三元操作符( condition ? do if true : do if false) P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2];展开。

i%2用于检查i是偶数还是奇数。 如果i是偶数,就返回2。如果i是奇数,则返回(i % 2 * j - j + n / delay ^ j) & 1;这个魔术值。

最后,index用于在字符串P内进行偏移,因此变为P += P[index];

var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
    var i = delay;
    var P ='p.\n';
    var j;
    n += 7;
    while (i > 0) {
        //Update HTML
        p.innerHTML = P;

        j = delay / i;
        i -= 1 / delay;

        let index;
        let iIsOdd = (i % 2 != 0); // <---------------

        if (iIsOdd) { // <---------------
            index = (i % 2 * j - j + n / delay ^ j) & 1;
        } else {
            index = 2;
        }

        P += P[index];
    }
};
var n = setInterval(draw, delay);

我把index = (i % 2 * j - j + n / delay ^ j) & 1中的& 1放到另一个if语句中。

这个方法用于检查括号中的结果是奇数还是偶数,如果是偶数,则返回0,奇数则返回1。 是按位与运算符。按位与的逻辑如下:

  • 1 & 1 = 1
  • 0 & 1 = 0

因此,something & 1会把something转换成二进制的表示形式,还会根据需要在前面填充0,以匹配某个东西的长度,并且只返回最后一位的AND结果。 例如,二进制中的5是101,如果与1进行“与”,则将得到以下结果:

    101
AND 001
    001

换句话说,5是奇数,5&1的结果是1。在JavaScript控制台中很容易就能确认这个逻辑关系。

0 & 1 // 0 - even return 0
1 & 1 // 1 - odd return 1
2 & 1 // 0 - even return 0
3 & 1 // 1 - odd return 1
4 & 1 // 0 - even return 0
5 & 1 // 1 - odd return 1

请注意,我还将其余的index重命名为magic,所以转换了&1的代码如下所示。

var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
    var i = delay;
    var P ='p.\n';
    var j;
    n += 7;
    while (i > 0) {
        //Update HTML
        p.innerHTML = P;

        j = delay / i;
        i -= 1 / delay;

        let index;
        let iIsOdd = (i % 2 != 0);

        if (iIsOdd) {
            let magic = (i % 2 * j - j + n / delay ^ j);
            let magicIsOdd = (magic % 2 != 0); // &1 < --------------------------
            if (magicIsOdd) { // &1 <--------------------------
                index = 1;
            } else {
                index = 0;
            }
        } else {
            index = 2;
        }

        P += P[index];
    }
};
var n = setInterval(draw, delay);

接下来,我将P += P[index];转换成switch语句。现在很清楚了,index只能是0、1、2这三个值中的一个。因此,P总是用这个值进行初始化:var P ='p.\n';。 其中0指向p,1指向.,2指向\n(换行符)。

var delay = 64;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";
var draw = function() {
    var i = delay;
    var P ='p.\n';
    var j;
    n += 7;
    while (i > 0) {
        //Update HTML
        p.innerHTML = P;

        j = delay / i;
        i -= 1 / delay;

        let index;
        let iIsOdd = (i % 2 != 0);

        if (iIsOdd) {
            let magic = (i % 2 * j - j + n / delay ^ j);
            let magicIsOdd = (magic % 2 != 0); // &1
            if (magicIsOdd) { // &1
                index = 1;
            } else {
                index = 0;
            }
        } else {
            index = 2;
        }

        switch (index) { // P += P[index]; <-----------------------
            case 0:
                P += "p"; // aka P[0]
                break;
            case 1:
                P += "."; // aka P[1]
                break;
            case 2:
                P += "\n"; // aka P[2]
        }
    }
};

var n = setInterval(draw, delay);

我清理了var n = setInterval(draw, delay);中的魔术值。 setInterval返回一个从1开始的整数,每次调用“setInterval”时,它就会递增1。 该整数可作为clearInterval(取消定时)的参数。 在我们这个例子中,setInterval只调用一次,而n只是简单地设置为1。

我还将delay命名为DELAY,以提醒开发者它只是一个常数。

最后,我将括号放在i % 2 * j - j + n / DELAY ^ j中,是要指出^按位异或的优先级低于%*-+/运算符。 换句话说,上述所有计算将在^之前执行。 新的表达式为:(i % 2 * j - j + n / DELAY) ^ j)

const DELAY = 64; // approximately 15 frames per second 15 frames per second * 64 seconds = 960 frames
var n = 1;
var p = document.getElementById("p");
// var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P";

/**
 * Draws a picture
 * 128 chars by 32 chars = total 4096 chars
 */
var draw = function() {
    var i = DELAY; // 64
    var P ='p.\n'; // First line, reference for chars to use
    var j;

    n += 7;

    while (i > 0) {

        j = DELAY / i;
        i -= 1 / DELAY;

        let index;
        let iIsOdd = (i % 2 != 0);

        if (iIsOdd) {
            let magic = ((i % 2 * j - j + n / DELAY) ^ j); // < ------------------
            let magicIsOdd = (magic % 2 != 0); // &1
            if (magicIsOdd) { // &1
                index = 1;
            } else {
                index = 0;
            }
        } else {
            index = 2;
        }

        switch (index) { // P += P[index];
            case 0:
                P += "p"; // aka P[0]
                break;
            case 1:
                P += "."; // aka P[1]
                break;
            case 2:
                P += "\n"; // aka P[2]
        }
    }
    //Update HTML
    p.innerHTML = P;
};

setInterval(draw, 64);

你可以在此处看到最终结果。

第二步,理解代码做了什么

var i = DELAY;i的初始值设为64,并且在每个循环中,i -= 1 / DELAY;i递减1/64(0.015625)。一直到i不再大于0循环才结束 while (i > 0) {。每循环一次,i减少1/64,所以,64次循环后,i将减少1(64/64 = 1)。所以,需要减少64×64 = 4096次,才会小于0。

该图像由32行字符组成,每行128个字符。很简单,64 x 64 = 32 x 128 = 4096。当i是偶数的时候,iIsOdd才会等于0(let iIsOdd = (i % 2 != 0);)。这种情况会出现32次,分别是i等于64、62、60……的时候。这32次,index会被设置为2 index = 2;,并且新增一行字符P += "\n"; // aka P[2]。该行剩下的127个字符将被设置为p.

但是我们应该在什么时候设置为p,什么时候设置为.呢?

当魔术值let magic = ((i % 2 * j - j + n / DELAY) ^ j);是奇数的时候,会设置为.,当魔术值是偶数的时候,设置为p

var P ='p.\n';

...

if (magicIsOdd) { // &1
    index = 1; // second char in P - .
} else {
    index = 0; // first char in P - p
}

但是这个魔术值什么时候是奇数,什么时候是偶数呢?在谈论这个之前,让我们先来看另外一个东西。

如果我们从let magic = ((i % 2 * j - j + n / DELAY) ^ j);中删除+ n/DELAY,我们最终会得到如下的静态布局。

现在,让我们看一下已经删掉+ n/DELAYmagic。我们最终如何得到上面那个漂亮的图片呢?

(i % 2 * j - j) ^ j

注意,对于每一个循环,我们都有:

j = DELAY / i;
i -= 1 / DELAY;

我们可以把给i的表达式带入到给j的表达式中,这样就变成j = DELAY /(i + 1 / DELAY),由于1/DELAY是一个非常小的数字,所以我们可以丢弃+ 1/DELAY并简化为j = DELAY/i = 64/i

同时,我们将(i % 2 * j - j) ^ j变为(i % 2 * 64/i - 64/i) ^ 64/i

我们来使用在线图形计算器绘制其中的一些函数。

首先,我们来绘制i%2

如果我们绘制64/i,将会看到这样的图形。

如果我们绘制左边的表达式,我们得到的图形看起来就像是以上两个图形的组合。

最后,我们把两个函数并列,可以得到以下图形。

这些图形告诉了我们什么?

我们知道,如果魔术值(i % 2 * j - j) ^ j是偶数,则添加p,如果是奇数,则添加.

我们放大一下图表的前16行,即i的值从64到32。

JavaScript中的按位异或会将小数点右边的值丢弃,这个有点像对一个数字做Math.floor运算。

j的值从1开始,并慢慢接近2,但始终小于2,所以我们可以将把他看成是1(Math.floor(1.9999) === 1),我们需要左边表达式的值为1,这样就能得到结果0(意思是偶数),并最终得到一个p

换句话说,每条绿色的斜线代表了图表中的一行。对于前16行,j始终是高于1但低于2,我们可以得到奇数值的唯一方法是(i % 2 * j - j) ^ j,也就是i % 2 * 64/i - 64/i,也就是绿色斜线要大于1或者小于-1。

以下是JavaScript控制台的一些输出,0或-2意味着结果是偶数,1表示结果是奇数。

1 ^ 1 // 0 - even p
1.1 ^ 1.1 // 0 - even p
0.9 ^ 1 // 1 - odd .
0 ^ 1 // 1 - odd .
-1 ^ 1 // -2 - even p
-1.1 ^ 1.1 // -2 - even p

看一下画出来的图形,我们能看到右边的斜线几乎刚巧超过1或小于-1(几乎没有奇数,也就是几乎没有p),下一行则更接近一点。第16行刚巧小于2或大于-2。第16行后,静态图换了一种模式。

第16行j跨越2行,预期结果翻转。现在,当绿色斜线大于2,小于-2,或者2和-2之间,但不等于1和-1时,我们会得到一个偶数。这就是为什么我们从第17行开始看到两组或两组以上的p

如果仔细观察运动图像的底部几行,你会注意到,它们不再遵循相同的模式。

现在我们回到+ n/DELAY上来。在代码中,我们可以看到,n是从8开始,然后每次setInteval触发后就会增加7。

当n变为64时,图形则变成如下这样。

生成HTML如下所示。与我们的预期相符。

在这一点上来讲,p的数量已经增长到一个恒定值。例如,对于第一行,有一半是偶数。而从现在开始,p.只是改变他们之间的位置。

为了说明这一点,当n在下一个setInterval增加7时,图形将略有变化。

请注意,第一行的斜线已经移动了大约1个小方块。假设4个更大的正方形代表128个字符,则1个大的正方形代表32个字符,1个小的正方形将代表32/5 = 6.4个字符(近似值)。如果看一下渲染的HTML,我们能看到第一行实际上是向右移动了7个字符。

当setInterval被调用7次以上,n等于64+9×7时,会发生什么。

对于第一行,j仍然等于1。现在,64左右的红色斜线的上半部分为〜2,底部为〜1。从1^2 = 3 // 奇数 - .1 ^ 1 = 0 //偶数 - p开始,图片翻转了。所以我们预计是一堆点,然后是个p

渲染出来的HTML是这样的。

这个图形会以类似的方式无限循环下去。

评论