由於除法優化實在太多了,所以這一篇繼續講,前面一篇說了除數爲正數情況(常量),那麼這篇就來說一說除數爲負數的情況。首先裏面涉及的很多基礎知識請翻上一篇查看,其實如果理解了上一篇的那些基本原理,除法爲負的情況也是很好理解的,因爲除數爲負的情況都是在除數爲正的情況之上做了一點點的變化。
首先還是先來分一下情況
除數爲常量-除數爲負數情況
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)後按基本公式可還原出除數的絕對值
注意上面討論的除數都是常量的時候,畢竟當除數爲變量時,是沒有優化空間的,直接按指令還原即可。