逆向-除法優化下

由於除法優化實在太多了,所以這一篇繼續講,前面一篇說了除數爲正數情況(常量),那麼這篇就來說一說除數爲負數的情況。首先裏面涉及的很多基礎知識請翻上一篇查看,其實如果理解了上一篇的那些基本原理,除法爲負的情況也是很好理解的,因爲除數爲負的情況都是在除數爲正的情況之上做了一點點的變化。

首先還是先來分一下情況

除數爲常量-除數爲負數情況
    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)後按基本公式可還原出除數的絕對值

注意上面討論的除數都是常量的時候,畢竟當除數爲變量時,是沒有優化空間的,直接按指令還原即可。

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