逆向-除法优化下

由于除法优化实在太多了,所以这一篇继续讲,前面一篇说了除数为正数情况(常量),那么这篇就来说一说除数为负数的情况。首先里面涉及的很多基础知识请翻上一篇查看,其实如果理解了上一篇的那些基本原理,除法为负的情况也是很好理解的,因为除数为负的情况都是在除数为正的情况之上做了一点点的变化。

首先还是先来分一下情况

除数为常量-除数为负数情况
    1.被除数无符号情况
    2.被除数有符号情况
        2.1 除数为2的幂
        2.2 除数为非2的幂
            2.2.1 Magic Number值为负的情况
            2.2.2 Magic Number值为正的情况

这里细分的情况与前一篇略有不同,前一篇是先以是否为2的幂,然后再区分有无符号的问题。而在这篇博客中,是以有无符号先来区分的,为什么呢,下面来看一下第一种情况,说完就知道为什么要这样分了。

第一种情况是被除数是无符号的情况,注意我们这篇讨论的是被除数为负的情况,那么在上一篇中有说过这么一个知识点

除法有无符号混合为无符号除法(DIV)

也就是说无符号除以一个负数,不管这个负数是什么,最终都会以无符号来处理,编译器使用无符号除法(DIV),此时会把这个负数当成一个正数处理(将负数的补码当成无符号来处理)。那么一旦转为正数,本质上来说被除数无符号这种情况就不用讨论了,其可以转化为上一篇的正数情况(情况1和情况3)。当然我们下面还是需要验证下,是否真的和一样。

int main(unsigned int argc, char* argv[])
{
    printf("%d",argc/-2);
    printf("%d",argc/-4);

    printf("%d",argc/-3);
    printf("%d",argc/-7);
    printf("%d",argc/4294967289);   //4294967289 == -7
    return 0;
}

我们主要来分析下release版下的汇编,因为debug下直接根据指令还原即可

mov esi,[esp+4+argc]
mov eax,esi
xor edx,edx
mov ecx,0FFFFFFFEh  //-2
div ecx

mov eax,esi
xor edx,edx
mov ecx,0FFFFFFFCh //-4
div ecx

// 按照 3.1 还原
mov eax,40000001h  //1073741825
mul esi
shr edx,1Eh  // >> 32 + 30 = 62
push edx
// 2^62 / 1073741825  = 4294967292.0000000037252902949925
//                    = 4294967293
//                    = -3

// 按照 3.1 还原
mov eax,20000001h  //536870913
mul esi
mov esi,edx
shr esi,1Dh  // >> 29 + 32 = 61
push esi
// 2^61 / 536870913 = 4294967288.0000000149011611660921
//                  = 4294967289
//                  = -7

push esi  //说明和上一个结果一样,被优化

看完汇编代码,其实发现和我们说的还是有一点点出入的,因为在这种情况下,除数为2的幂时,是没有优化的,直接根据指令还原即可。再来看看非2的幂时,此时我们重点观察最后两个表达式,这里的还原依据是上一篇的3.1情况,那么根据我们还原的结果为4294967289,可以说明其编译器的确把-7当成了无符号数来表达,所以我们根据3.1还原的时候,出现的常量会这么大。而且第五个printf的结果都全部优化了,因为编译器认为两者的结果是等价的,所以直接拿了上一次的运算结果。

所以对于我们这里的情况一,我们根据正数情况还原即可,只是还原后自行需要根据代码上下文判断,是除以一个很大的正数,还是一个负数。

 

OK,下面来看看2.1的情况,直接先来看代码吧

int main(int argc, char* argv[])
{
    printf("%d",argc/-4);
    return 0;
}

其反汇编代码

.text:00401000                 mov     eax, [esp+argc]
.text:00401004                 cdq
.text:00401005                 and     edx, 3  //做调整,详细看上篇的2
.text:00401008                 add     eax, edx
.text:0040100A                 sar     eax, 2
.text:0040100D                 neg     eax   // -- 这条汇编指令是多出来的

OK,对比上一篇的情况二,你会发现,其实就多了一条指令,也就是最后一个指令求补,这里为什么最后需要求补呢?看下面公式

   这个公式应该是很好理解其意思的,除以一个负数,相当于除以一个正数后对其求负即可。

所以在上面的汇编代码中,最后会多一条对结果求补的指令。

 

好了,下面可以讨论2.2的情况了,也就是除数为非2的幂,其实对于2.2.1和2.2.2,和上面的2.1一样,都是与前面的正数有相关性的,前面的基础打好了,这些都很容易理解。

先来看2.2.1,Magic Number值为负的情况,也就是其值是一个大于0x7FFFFFFF的值

int main(int argc, char* argv[])
{
    printf("%d",argc/-11);
    return 0;
}

汇编代码

.text:00401000                 mov     ecx, [esp+argc]
.text:00401004                 mov     eax, 0D1745D17h
.text:00401009                 imul    ecx
.text:0040100B                 sar     edx, 1
.text:0040100D                 mov     eax, edx
.text:0040100F                 shr     eax, 1Fh
.text:00401012                 add     edx, eax
.text:00401014                 push    edx

首先看完这段汇编代码,是不是感觉很熟悉,对比前一篇的4.2的情况,其实就只是少了一个Add edx,xxx的调整指令。首先先来看一下数学模型

设A为被除数,C为除数常量

M = 2^n/C
A/C = AM >> n
	
当 C < 0 时
    M1  = -(2^n/|C|)  可将负号提取到外面
    A/C = (A * M1) >> n
        = (A * -M) >> n

看完上面的公式,其实说的就是此时的Magic Number是一个求负后值。上面公式中的最后那个表达式中的负数会体现到Magic Number上(A * -M),因为在最后的结果中求负划不来,在常量中直接求负就好。

所以还原的话我们只需要将Magic Number求负(补)后,根据正数的情况还原即可,只是这里的结果求出来会是个正数,最终结果再加上个负号即可。

//neg(0D1745D17h) = 2E8BA2E9 = 780903145  对Magic Number求补
// 2^33 / 780903145 = 10.99 -> 11   计算出代码的中右移的位数后按照原表达式还原
// argc / -11  //结果加个-

好了,对于这种情况,那么是不是很容易和正数情况搞混呢,这里我们只需这样区分即可,由于Magic Number为负数,但是在imul和shr之间未见Add edx,xxx的调整代码,故推断其除数为负数。

 

再来看最后一种情况2.2.2,Magic Number为正的情况。

int main(int argc, char* argv[])
{
    printf("%d",argc/-3);
    return 0;
}

反汇编代码

.text:00401000                 mov     ecx, [esp+argc]
.text:00401004                 mov     eax, 55555555h
.text:00401009                 imul    ecx
.text:0040100B                 sub     edx, ecx  ;这条指令是多出来的
.text:0040100D                 sar     edx, 1    ; >>32 + 1 = 33
.text:0040100F                 mov     eax, edx
.text:00401011                 shr     eax, 1Fh  ;对最后的结果进行调整
.text:00401014                 add     edx, eax
.text:00401016                 push    edx

首先,这里的套路和前面的一样,这里对比的是上一篇的4.1,可以发现这里只多出了一行代码,也就是sub edx,ecx这行代码。

根据2.2.1的结论,这里的M值是一个求负后的值,那么说明原先的值应该就是一个负数值,回顾上一篇中的4.2,虽然M的值是一个负数值,但是其真实含义为无符号的数值,所以需要做调整,那么此时我们对M值求负后,是不是也需要调整呢?

设A的被除数,M值为Magic Number,数据宽度为 WORD
由于此时M值为求负后的Magic Number,那么设其原先的值为M1
	M1 = -M (M1为负数)
neg(M1) = 0x10000 - M1
A * M  = -A*M1
       = A*(0x10000 - M1)
       = A*10000 -A*M1
       = -A*M1 + A*10000 (- A*10000)
所以此时我们需要计算的就是-A*M1,所以需要做调整再sub A*10000,也就是 sub edx,A

所以根据上面的推导,我们现在也可以明白了为什么会多出这条sub edx,ecx的语句了。由于这里只是多了些调整的代码,所以其还原的方法与上面一样。这里我们先来说一说如何区分,然后在还原。

Magic Number为正数,但是除法和移位之间见到 sub edx,xxx的调整指令,推断此处除数为负数,Magic Number为求补后的结果

    //Magic Number为求补后的结果,这里只需反求,就可根据正数情况公式还原
// neg(55555555h) = AAAAAAAB = 2863311531 
// 2^33 / 2863311531 = 2.99 -> 3
// argc / -3

到此,除法部分就都记录完了,下面对除法的还原做一个最后的总结。

1.除数的2的幂 - 基本公式-右移
    当被除数为正数时,直接右移
    当被除数为负数时,在右移之前需要做调整(+n-1)
        一般编译器会利用符号位做无分支优化
2.除数不为2的幂 - 基本公式 C = 2^n / M  (n为右移的位数,M为Magic Number,结果向上取整)  
    被除数无符号的情况
        当M值无进位时直接按基本公式还原
        当M值有进位时,需要将M加上进位(M+2^32),然后根据基本公式还原
    被除数有符号的情况
        先按下面的原则确定除数的符号
            MagicNumber为正数,imul和sar之间无调整的,其除数为正
            MagicNumber为正数,imul和sar之间有sub edx 乘积调整的,其除数为负
            MagicNumber为负数,imul和sar之间无调整的,其除数为负
            MagicNumber为负数,imul和sar之间有add edx 乘积调整的,其除数为正
        判定除数为正数的都按基本公式还原;
        判定除数为负时,Magic Number需对其求补(2^32-M)后按基本公式可还原出除数的绝对值

注意上面讨论的除数都是常量的时候,毕竟当除数为变量时,是没有优化空间的,直接按指令还原即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章