逆向-取模运算

Posted 嘻嘻兮

tags:

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

除法终于整理结束了,这篇开始整理取模运算,对于取模运算来说,个别的情况还是需要使用除法的,所以除法运算的基础还是得牢靠。

取模也叫取余,表达为 a % b。下面需要对b的情况进行区分来分析

1. 变量
2. 常量
    2.1 无符号
        2.1.1 2的幂
        2.1.2 非2的幂
    2.2 有符号
        2.1.1 2的幂
            2.1.1.1 正数
            2.1.1.2 负数
        2.1.2 非2的幂

首先,这里和除法一样,对于为变量时候,都是没有优化的,所以直接会使用div/idiv指令,可以据此判断出有无符号,取模的结果在edx中。

下面主要探讨常量的情况,先来补充一点基础知识,先看如下代码

	printf("%d\\r\\n",8%3);
	printf("%d\\r\\n",8%-3); 
	printf("%d\\r\\n",-8%3); 
	printf("%d\\r\\n",-8%-3);

上面的运算结果

2
2
-2
-2

如果对于结果有误的可以看下面的公式

设被除数为a,除数为b,商为q,余数为r
a / b = q ..余.. r
=> a = q*b + r
=> r = a - q*b;  //求余公式

也就是说余数等于被除数减去商乘以除数,这里应该比较好理解。这里对于某些情况中,取模也会使用该公式进行优化。

我们挑最后第三个来套一下公式看是否正确,剩余几个同理可求得

-8%3
已知除法向零取整,-8/-3可得商q=-2
r = a - q*b
  = -8 - -2*3
  = -8 + 6
  = -2

OK,其实对于上面的结果来说,其实我们还可以得出一个规律,就是余数的符号跟着被除数走,这里的应用具体后面会提。

 

好了,下面来看2.1.1情况,也就是无符号中2的幂情况,对于无符号的情况,debug都不优化,主要来看release中的情况

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

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

对应汇编代码

.text:00401043                 mov     eax, [ebp+argc]
.text:00401046                 and     eax, 7
.text:00401049                 push    eax

可以发现,其取模的结果就用了一个and 7,下面我们来分析一下

假设一个数为2的幂(2^n),那么其二进制的后n位肯定是0
    这里同理,2的倍数后1位肯定是0,4的倍数后2位肯定是0 ...
为什么呢?
    2的二进制为10,那么此后对于2的幂而言,都是*2*2*2..
    那么每乘一个2,其实相当于左移一位(右边补0)
    4 = 2 * 2 = 10 << 1 = 100
    8 = 4 * 2 = 100 << 1 = 1000

7的二进制   111
8的二进制  1000

由二进制可发现,对于and操作,也就是对后n位进行取值的操作
为什么呢?
    对于2的幂而言,对其取余范围肯定是[0,2^n-1]
    而2的幂二进制的其后n位肯定为0,所以直接获取后n位即为余数
    假设对8取余,那么其余数肯定是小于8的,也就是范围 [0,7]
    8的二进制1000,假设某一个数为1xyz,那么xyz必定为余数(xyz非0即1)

所以对于这种情况求余,就是一个

and reg,2^n-1

 

下面来看2.1.2的情况,在vc6.0上测试是没有优化的,而在高版本vs2015(自己使用的版本)中是有优化,其他版本未测,所以我们就来讨论一下这个高版本中优化的情况吧,其实优化的原理很简单,就是上面的求余公式

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

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

对应汇编代码

.text:00401046                 mov     eax, 0CCCCCCCDh
.text:0040104B                 mul     ecx
.text:0040104D                 shr     edx, 2  ;计算商 q = edx
.text:00401050                 lea     eax, [edx+edx*4] ;q*5 = edx*5 此时的5就是除数5(常量)
.text:00401053                 sub     ecx, eax  ;r = a - q*b = ecx - edx*5
.text:00401055                 push    ecx

对于上面的求余公式而言,我们是要知道商的多少的,所以此时比然会涉及到除法运算,而除法运算是可以优化的,所以说上上面的汇编代码中才没有看到一行的除法运算。如果除法都掌握的话,这里就很简单了。

 

下面看有符号数取模,还是先看除数为2的幂的情况,首先对于debug和release的优化情况一致。然后又分正数和负数..这里可能很多人就会有疑问了,上面不是说余数的符号是跟着被除数走的么,我们现在讨论的都是除数的情况,为什么还要区分正负呢?

其实这里区分正负并不是说影响其结果,而是这里对于vs的低版本系列,微软提供了两套方案,虽然结果是一致的,只是对于正数和负数其产生的汇编代码不一致。而在高版本系列中,好像都优化成正数的这种方案了,具体大家自己测试吧。

下面先来看正数的情况

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

    printf("%d",argc%4);
    return 0;

对应汇编代码

.text:00401043                 mov     eax, [ebp+argc]
.text:00401046                 and     eax, 80000003h
.text:0040104B                 jns     short loc_401052
.text:0040104D                 dec     eax
.text:0040104E                 or      eax, 0FFFFFFFCh
.text:00401051                 inc     eax
.text:00401052
.text:00401052 loc_401052:                             ; CODE XREF: _main+B↑j
.text:00401052                 push    eax

大致看一下上面的汇编代码,可以发现有一个分支跳转,对于分支跳转,那么我们就对于分支的情况独立进行分析,这样子就能看懂上面那段汇编代码了。

对于jns指令来说,是不为负则跳转,那么跳转条件其实就是正数或者负数,先来看正数的情况分析

.text:00401043                 mov     eax, [ebp+argc]
                               ;这里的and很明显是和之前无符号的目的是一样的,只是保留符号位
                               ;保留符号位是因为前面说了余数的符号位和被除数是一致的
                               ;正数不影响,相当于无符号数的处理 and 2^n-1
.text:00401046                 and     eax, 10000000000000000000000000000011b ;转化二进制
.text:0040104B                 jns     short loc_401052  
                                ;省略负数的情况
.text:00401052
.text:00401052 loc_401052:                             ; CODE XREF: _main+B↑j
.text:00401052                 push    eax  ;直接得到结果

OK,正数分支的情况其实就是和上面无符号的情况处理一致,下面分析一下负数分支

;首先,先不看第一行的第三行的代码,这里为了一种特殊情况
;这里为什么需要or呢?因为前面说了余数的符号是跟着被除数,那么当余数为负数时,我们肯定得补回中间的1
;因为只有补回中间的1,此时表达的才是一个补码
.text:0040104E                 or      eax, 11111111111111111111111111111100b


;下面来看一下加上一三行代码的情况,其实这里为的是一种特殊情况,就是当余数为0的时候
;当余数为零,dec eax,eax = 0xFFFFFFFF
;.text:0040104D                 dec     eax
;这里-1去or任何值最后肯定还是-1
.text:0040104E                 or      eax, 11111111111111111111111111111100b
;此时加1后变回0
;.text:0040104D                 dec     eax
;而对于其他正常情况来说,第一行代码减一,第三行代码加一,其实相当于结果没有变化

上面应该解释的比较清楚了。

 

下面来看一下2.1.1.2的情况,注意这里与上面正数情况的结果是一致的,只是这里算是微软对于正数和负数都给了一套方案。在看这套方案之前,我们需要先来看一个取绝对值的案例。

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

    printf("%d",abs(argc));
    return 0;

对应的汇编代码,这里使用了无分支判断

.text:00401043                 mov     eax, [ebp+argc]
.text:00401046                 cdq             ;if eax >= 0 edx = 0   else edx = 0xFFFFFFFF
.text:00401047                 xor     eax, edx;if eax >= 0 eax = eax else eax = ~eax
.text:00401049                 sub     eax, edx;if eax >= 0 eax -= 0  else eax -= -1
.text:0040104B                 push    eax

对于上面的汇编代码,只需分eax是否大于等于0,也就是当该数为正数时,其值不发生变化,而其值为负数时,其值做取反加一,这样子就变成了取绝对值的操作。

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

    printf("%d", argc % -4);
    return 0;

对应的汇编代码,这段汇编代码其实对于上面正数的情况,优点其实是无分支

.text:00401043                 mov     eax, [ebp+argc]
.text:00401046                 cdq
.text:00401047                 xor     eax, edx
.text:00401049                 sub     eax, edx  ;这里做了一个绝对值
.text:0040104B                 and     eax, 3
.text:0040104E                 xor     eax, edx
.text:00401050                 sub     eax, edx
.text:00401052                 push    eax

前四行代码很明显应该就是做绝对值的操作,那么后面的代码做什么,我们先来分析一下,其实就是前面说的余数的符号是跟着被除数走的原理。

r = |a| % |b|
if a < 0
    r = -r 

所以40104B这行代码表示的就是按照无符号的方式获取余数(and 2^n-1),获取余数后,如果被除数为负数的话,那么我们是不是需要取个负(取反加一)呢,所以下面两行代码又使用了一个无分支

;此时edx之前已经保存了被除数的符号位
;如果为正,那么结果不变,如果为负,那么对其取反加一(求负)
.text:0040104E                 xor     eax, edx 
.text:00401050                 sub     eax, edx

 

好了,下面就剩下最后这2.1.2的情况了,这里和2.1.2原理一样,就当记录过一下,低版本也是没有优化的,高版本使用求余公式进行优化

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

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

对应的汇编代码分析

.text:00401044                 mov     esi, [ebp+argc]
.text:00401047                 mov     eax, 92492493h
.text:0040104C                 imul    esi
.text:0040104E                 add     edx, esi
.text:00401050                 sar     edx, 2
.text:00401053                 mov     ecx, edx
.text:00401055                 shr     ecx, 1Fh
.text:00401058                 add     ecx, edx  ;上面一段都是除法的优化 ecx就是商的结果
.text:0040105A                 lea     eax, ds:0[ecx*8]  ;eax = ecx * 8
.text:00401061                 sub     eax, ecx ;eax - ecx = ecx*8-ecx =ecx*7
.text:00401063                 sub     esi, eax ;q = a - qb = esi - ecx*7

 

以上是关于逆向-取模运算的主要内容,如果未能解决你的问题,请参考以下文章

算数运算——四则与取模运算

java中取余运算符 (%)

基础模运算

取余数运算

取余数运算

Java 基本算数运算符