逆向-取模運算

除法終於整理結束了,這篇開始整理取模運算,對於取模運算來說,個別的情況還是需要使用除法的,所以除法運算的基礎還是得牢靠。

取模也叫取餘,表達爲 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

 

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