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