逆向-除法优化上

Posted 嘻嘻兮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向-除法优化上相关的知识,希望对你有一定的参考价值。

上一篇说完加减乘的优化,这篇来说说除法,首先先打个铺垫,除法的优化涉及到各种数学公式,这里我们主要探讨一下结论,具体的证明可以参考《C++反汇编与逆向分析技术揭秘》,这里做一个总结。

首先,如果除数为变量时,是没有任何的优化空间的,所以老老实实的上对应的汇编代码即可,所以就不讨论了。下面主要来讨论下除数为常量的情况,并且这个常量还是正数,负数情况下一篇博客介绍。

首先,先来看一下需要讨论的情况

除数为常量-除数为正数情况
    1.除数为2的幂-被除数无符号
    2.除数为2的幂-被除数有符号
    3.除数为非2的幂 - 被除数无符号	
    	根据Magic Number的是否需要进位(下面简称M)
            3.1 M值无进位
            3.2 M值有进位
    4.除数为非2的幂 - 被除数有符号
    	根据Magic Number的正负情况(下面简称M)
            4.1 M值为正数
            4.2 M值为负数

这个情况是有那么点多,不过其实总体算下来,应该就两套公式,分别为除数为2的幂和除数为非2的幂两套,只是情况不同,有些公式的变种而已。还有对于Magic Number这个概念,后面会提的,这里留个先个印象就好

首先,先来打一些基础的铺垫

1.乘法有无符号混合为有符号乘法(IMUL)
2.除法有无符号混合为无符号除法(DIV)
3.计算机中的除法为向零取整
4.移位运算是向下取整(不大于的关系)

首先对于向上取整和向下取整,对于计算机的同学应该十分熟悉了,其实对于向零取整其实就是往中间的零值靠齐

3 / 2 = 1.5   //往零靠齐取1    3/2=1
-3 / 2 = -1.5 //往零靠齐取-1  -3/2=-1

还有对于第四点一定要理解,因为在下面会有用到,移位运算,这里说的更细一点吧,对于除法需要用到的就是右移运算,你可以这样想,当二进制往右移动,那么肯定有一些位需要丢失,那么肯定是不大于的关系。

假设我们需要计算-5/2,那么我们很容易想到,使用右移一位,那么如下结果计算出来为-3(因为向下取整的原因),如果此时可以向上取整(向零取整),那么就符合我们的数学运算结果了。

-5>>1
FFFFFFFB     >> 1
.......1011  >> 1
.......1101  -> FFFFFFFD = -3

首先,先来说下除数为2的幂的情况,这里我们可以使用右移,但是右移,对于负数的情况就会出现上面的问题,如下代码

	for(int i=-20;i <= 20;++i)
	
		printf("%d / 8 = %d\\r\\n",i,i/8);
		printf("%d >> 3 = %d\\r\\n\\r\\n",i,i>>3);
	 

打印运算结果,其实可以发现,在负数中的确会有问题,那么我们可以这样子来调整一下,加上n-1的值,n为除数

	for(int i=-20;i <= 20;++i)
	
		printf("%d / 8 = %d\\r\\n",i,i/8);
		if(i >= 0)
			printf("%d >> 3 = %d\\r\\n\\r\\n",i,i>>3);
		else
			printf("%d >> 3 = %d\\r\\n\\r\\n",i,(i+8-1)>>3);
	 

部分结果

-13 / 8 = -1
-13 >> 3 = -1

-12 / 8 = -1
-12 >> 3 = -1

-11 / 8 = -1
-11 >> 3 = -1

-10 / 8 = -1
-10 >> 3 = -1

此时可以发现,结果吻合了,那么为什么加上n-1呢,你可以这样子想,负数右移与实际数学运算不符合的原因是因为右移向下取整了,那么如何才能向上取整呢,其实我们先来加上一个n值,你会发现大部分情况是对的

printf("%d >> 3 = %d\\r\\n\\r\\n",i,(i+8)>>3);

不正确的情况如下:

-16 / 8 = -2
-16 >> 3 = -1

-8 / 8 = -1
-8 >> 3 = 0

当这个被除数可以整除时,这个结果就会大一,这个就应该很清楚了为啥了,那么如何避免呢,其实我们可以加上n-1的值,我们可以这样子来考虑一下

假设原先的商为q
如果可以被整除,加上 n-1,由于没有加满n,所以结果肯定还是q
如果不能被整除,加上 n-1,由于其值肯定会大于等于 (q+1) * n,所以结果为 q+1
    因为不能被整除,说明肯定有余数(>0),那么余数加上n-1后,其值肯定>=n,所以商会+1

好了,道理讲完了,其实这里已有一个论证的公式,公式如下

我们来看一下红框框中的那个公式,这个公式的意思的,a/b的向上取整 = 右边部分的向下取整值。

这个公式我们从右往左看,就可以发现正好满足我们的,右移向下取整调整到向上取整,如何调整呢?

也就是公式中椭圆圈中的部分,被除数加上(除数-1)值。

OK,下面我们就可以来看一下2有符号的情况了(1的情况无符号,说明都为正数,那么直接右移即可)

int main(int argc, char* argv[])

    printf("%d",argc/8);
    return 0;

首先,这里对于1和2的情况,debug和release的情况都一致,所以拿一个进行讨论其反汇编

00401298 8B 45 08             mov         eax,dword ptr [ebp+8]
0040129B 99                   cdq
0040129C 83 E2 07             and         edx,7
0040129F 03 C2                add         eax,edx
004012A1 C1 F8 03             sar         eax,3
004012A4 50                   push        eax

看完上面的汇编代码,7这个值我们应该会有印象的,用于调整(加n-1),还有下面的右移三次也是知道的,只是奇怪的是,在我们上面的C源码中,是有if条件判断,也就是说这个公式只是用于负数情况的调整,那么这里的判断分支呢,其实这里的汇编代码做了一个无分支判断,下面具体来分析一下

00401298 8B 45 08             mov         eax,dword ptr [ebp+8]  ;取被除数

;扩展高位 if(eax >= 0)  edx=0  else edx = -1
0040129B 99                   cdq 

;and运算  if(eax >= 0)  edx=0  else edx = 0xFFFFFFFF & 7 = 7
0040129C 83 E2 07             and         edx,7  

;加上 edx if(eax >= 0)  add 0  else add 7
0040129F 03 C2                add         eax,edx

;调整完毕,进行右移取得结果
004012A1 C1 F8 03             sar         eax,3
004012A4 50                   push        eax

看完上面的代码注释,应该就明白了,对于正数而言,其调整需要加的值为0,相当于不调整,这样子就做了一个无分支判断的优化。

 

 

下面再来说一说3和4的情况吧,这里对于情况3和4而言(debug下不优化),我们来探讨一下release的情况,其基本原理其实用到的都是同一套的公式。

假设x为被除数,c为除数(该值为非2的幂),那么由如下推导

这里的推倒就毕竟容易了,首先推倒导到第一步,拿 1/c,此时会发现1和c都为常量,那么可以做常量折叠,但是一旦做了常量折叠,其结果非0即1,误差太大。所以再进行调整,调整为第三步的结构,此时n值给小了也不行,因为小了可能会存在误差。C越大n越大,编译器有一套误差规避的准则(n值又编译器确定),那么此时2^n / c就可在编译期间算出,该值为一个常量值,称为Magic Number。

那么我们设2^n/c为m,下面再来调整一下公式,使其更顺眼一点

好了,下面的变形其实最终都是围绕上面的这个式子展开的。

 

迷迷糊糊的话也没事,我们先来看3.1的情况,这是最基础的情况,基本上后面的变种都是以这种情况展开的,所以这种情况一定要理解哦。

int main(unsigned int argc, char* argv[])

    printf("%d",argc/3);
    return 0;

汇编代码如下

.text:00401000                 mov     eax, 0AAAAAAABh  ;m值
.text:00401005                 mul     [esp+argc]       ;x*m
.text:00401009                 shr     edx, 1           ;右移
.text:0040100B                 push    edx

看完上面的代码,然后再对比一下上面的公式,你会发现这段代码是不是很像套公式,首先我们先来加上如果上述代码就是公式,那么我们该如何还原呢?

1.确定右移n的数值
2.确定m的数组
2.根据公式还原

  所以当我们确定下来n和m后,只需要套用后面的公式还原c即可

好了,看上面的汇编代码,m值是很好确定的,就是0AAAAAAABh,那么n呢,是1么,显然这里肯定不是的,因为前面有说过n值不会太小,因为太小误差会太大。

x*m = edx.eax
相当于edx存储x*m的高32位
由于后面直接用了高32位的edx,相当于乘积>>32,此时再右移一位,相当于总共移动33位
.text:00401009                 shr     edx, 1           ;右移

注意,后面直接用了高32位的edx
.text:0040100B                 push    edx

所以这里的n值应该是33,上面的需要好好理解一下,因为最终使用的是edx,其实默认起步移动为32位。下面可以还原了

// 2^33 / 0AAAAAAABh  = 2.99 -> 3
// argc/3

注意这里的还原结果需向上取整即可。

 

 

下面再看一下3.2的情况,也就是M值有进位的情况

int main(unsigned int argc, char* argv[])

    printf("%d",argc/63);
    return 0;

汇编代码

.text:00401000                 mov     ecx, [esp+argc]
.text:00401004                 mov     eax, 4104105h  ;m
.text:00401009                 mul     ecx            ;x*m
.text:0040100B                 sub     ecx, edx
.text:0040100D                 shr     ecx, 1
.text:0040100F                 add     ecx, edx
.text:00401011                 shr     ecx, 5
.text:00401014                 push    ecx

看完上面的代码,估计只能分析前两句,虽然样子其实很像上面的公式,但是下面的代码不知道是干什么的,这里其实是上面的公式的一个变种,我们来根据其代码来还原一下数学表达式。

此时ecx为x(被除数),m值为4104105h,后面的公式中都用ecx和m值进行代替

首先,前三行代码很好理解

ecx * m -> edx.eax
    相乘,结果高32位存放在edx,低32位存放在eax

sub ecx,edx,说明减去的是乘积的高位    

shr ecx,1 相当于除以2                               

add ecx,edx,加上乘积高位                       

shr ecx,5 相当于除以2^5                             

好了,还原出的数学表达式就是上面的这个样子,是不是一脸的蒙蔽,这完全不像前面的基础公式,下面我们来化简一下

可以发现,对上面的式子做化简后,我们原来熟悉的模板公式就出现了,此时的M = (2^32 + m)

那么这里为什么需要加上2^32呢,因为其编译器确定的N值推理出的Magic Number大于0xFFFFFFFF,32位存放不下的缘故,相当于产生的进位,但是编译器只会控制进位一。

好了,因为此时MagicNumber存放不下,所以需要进行大数运算,为了优化除法而进行大数运算是很划不来的,所以编译器作者就对该式子进行了简而化繁(避免大数),这里繁琐的公式也就是一开始根据汇编反推的数学公式,开始的公式里面是没有大数运算的。

好了,说到这里,m值我们就可以确定了,需要加上2^32,那么如何确定n值呢,其实看懂了上面的推导,其实可以发现这个38其实也就是数右移了多少位,再加上32即可

;....
.text:0040100D                 shr     ecx, 1
;....
.text:00401011                 shr     ecx, 5  ;该右移可有可无

下面开始还原

// 2^38 / (2^32+4104105h) = 2^38 / 4363141381 = 62.99 -> 63
// argc / 63

 

下面开始分析第4种的情况,首先来看4.1,这里位Magic Number 为正数的情况,这种情况如果前面都理解的情况下是很好理解的,相当于是前面3.1的情况,然后需要加个一个移位的分支判断,因为前面说了,右移是向下取整的,所以我们得将结果进行向上调整。

int main(int argc, char* argv[])

    printf("%d",argc/5);
    return 0;

反汇编代码

.text:00401000                 mov     ecx, [esp+argc]
.text:00401004                 mov     eax, 66666667h
.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

对比前面的3.1,就可以发现,其代码只多出了三行,也就是0D-12的那三行,这三行主要用于负数情况,需要调整向上取整,看这三行代码也是无分支的,所以也可以肯定这里用到无分支优化。那么我们还是使用上面的推导七么,这里的话很明显不是,为什么呢,因为推倒七是在移位以前做的调整,而此时调整很明显是在移位后,所以我们使用推导三的结论

这个其实也很好理解,也就是说我们右移完(下整) + 1后结果就是上整了。这里其实不理解的话可以自己带一些数值进去测试即可。

;取结果的高32位
.text:0040100D                 mov     eax, edx 
;右移动31位,相当于取符号位  if(eax >= 0) eax = 0 else eax = 1
.text:0040100F                 shr     eax, 1Fh 
; if (eax >= 0) add 0 else add 1
.text:00401012                 add     edx, eax 

好了,由于多出来的这部分是用于调整的,所以其还原的公式是与3.1一致的,下面开始还原

// 2^33 / 66666667h = 4.99 -> 5
// -> argc / 5

 

 

OK,下面分析最后一种情况,也就是当Magic Number 为负数的情况,我们先来看完汇编代码后再来细说

int main(int argc, char* argv[])

    printf("%d",argc/7);
    return 0;

反汇编代码

.text:00401000                 mov     ecx, [esp+argc]
.text:00401004                 mov     eax, 92492493h
.text:00401009                 imul    ecx
.text:0040100B                 add     edx, ecx  ;对比4.1就多出了这一行
.text:0040100D                 sar     edx, 2
.text:00401010                 mov     eax, edx
.text:00401012                 shr     eax, 1Fh
.text:00401015                 add     edx, eax
.text:00401017                 push    edx

首先,我们来看一下Magic Number,其值为92492493h,可以发现其值的确大于了0x7FFFFFFF,也就是为负数。那么说到这里其实大家可能就是有疑问了,在上面的公式意义上,Magic Number就是一个无符号数,那么现在成了负数,是不是有问题呢?

是的,问题也就是出在这里,所以我们需要解决如何将有符号数相乘转为无符号数相乘的问题?

对比4.1,其实就多出了一行代码,那么这行代码的具体什么作用呢

因为这里的有符号的情况,观察其0x401009,用到也是有符号的imul,那么在有符号数下,如果大于0x7FFFFFFF,其表示为一个负数,而对于我们之前的除法数学模型来说,x*m,这里Magic Number是一个无符号数,自然意义就不一样了。所以其实多出来的这行add edx,ecx 其实用于调整的,使其还原乘以无符号数的本意。

设有符号数为A,无符号数为M(M在有符号数下为负数),数据宽度为WORD
neg(M) = (~M + 1)  //取反加一
neg(M) + M = ~M + 1 + M
neg(M) + M = 0xFFFF + 1
neg(M) + M = 0x10000
neg(M) = 0x10000 - M
任何数据在计算机中都是用二进制表示的,且都是用补码表示的,所以此时M的真值为 -neg(M)

A*M(有) = A*-neg(M)
        = A*-(0x10000 - M)
        = A*(M - 0x10000)
        = A*M - A*0x10000
A*M(无) = A*M
    	= A*M - A*0x10000 + A*0x10000
    	= A*M(有) + A*0x10000

如果将M作为无符号看看待,说明需要加上A*0x10000,这样子最终结果就相同了
imul reg = edx.eax
此时的edx为结果的高位,也就是10000起步,*A可调整为 edx+A
Add edx,A

所以根据上面可以得出 A*M(有) + A * 0x10000 = A * M(无)。好了因为上面验证里面的数据宽度为WORD,然后在我们的例子里是DWORD,所以我们需要加上的是 A * 2^32。

;edx = edx + ecx,也就是高32位加上ecx
;高32位加上ecx,其实也就相当于乘积加上了 ecx*2^32
.text:0040100B                 add     edx, ecx

这里的ecx也就是上面的A。总的说来,因为有符号相乘结果肯定会小,那么我们将少的这部分加上就和无符号相乘一样了。

下面我们可以使用任意一个数值来进行验证下

0x14523687 * 0x98563221 = 0xC17A7F926996567  //计算器得出

打开OD进行写入汇编验证

可以发现,其edx.eax的拼接结果与上面的计算器中运算的结果一致。

好了,明白了4.2中多出来的这行代码后,其实也就可以还原了,因为4.2对于4.1来说,也是一个调整,相当于是有符号乘调整为无符号乘,所以其基本的数学模型不变,按照4.1的方法还原即可

// 2^34 / 92492493h = 6.99 -> 7
// argc / 7

 

以上是关于逆向-除法优化上的主要内容,如果未能解决你的问题,请参考以下文章

逆向课程第五讲逆向中的优化方式,除法原理,以及除法优化下

逆向-除法优化上

逆向课程第四讲逆向中的优化方式,除法原理,以及除法优化上

计算机原理 3.6 定点数除法

逆向-取模运算

什么具有更好的性能:乘法还是除法?