今天在反編譯一段程序時發現了一些奇怪的代碼,耗費了一天的時間終於弄懂了這段算法
| push ebp |
| mov ebp,esp |
| mov ecx,ss:[ebp+0x8] |
| mov eax,0x88888889 |
| mul ecx |
| shr edx,0x5 |
| mov eax,edx |
| shl eax,0x4 |
| sub eax,edx |
| add eax,eax |
| add eax,eax |
| sub ecx,eax |
| push esi |
| mov esi,edx |
| mov eax,0x88888889 |
| mul esi |
| push edi |
| mov edi,ss:[ebp+0xC] |
| mov ds:[edi],ecx |
| mov ecx,edx |
| shr ecx,0x5 |
| mov edx,ecx |
| shl edx,0x4 |
| sub edx,ecx |
| add edx,edx |
| add edx,edx |
| sub esi,edx |
| mov eax,0xAAAAAAAB |
| mul ecx |
| mov ds:[edi+0x4],esi |
| mov esi,edx |
| shr esi,0x4 |
| lea eax,ds:[esi+esi*2] |
| add eax,eax |
| add eax,eax |
| add eax,eax |
| sub ecx,eax |
| mov eax,0x8421085 |
| mul esi |
| mov ds:[edi+0x8],ecx |
| mov ecx,esi |
| sub ecx,edx |
| shr ecx,0x1 |
| add ecx,edx |
| shr ecx,0x4 |
| mov edx,ecx |
| shl edx,0x5 |
| sub edx,ecx |
| sub esi,edx |
| mov eax,0xAAAAAAAB |
| mul ecx |
| shr edx,0x3 |
| lea eax,ds:[edx+edx*2] |
| add eax,eax |
| add eax,eax |
| sub ecx,eax |
| inc ecx |
| add edx,0x7D0 |
| inc esi |
| push edi |
| mov ds:[edi+0xC],esi |
| mov ds:[edi+0x10],ecx |
| mov ds:[edi+0x14],edx |
| call <sub_45630C0> |
| add esp,0x4 |
| cmp ds:[edi+0x4],0x0 |
| mov eax,edi |
| jge 0x4563293 |
| mov ds:[edi+0x4],0x0 |
| pop edi |
| pop esi |
| pop ebp |
| ret |
將除法轉換為乘法的MagicNumber:
除以60 0x88888889
MagicNumber=2^33/被除數+1
所以被除數等於2^33/(MagicNumber-1)
不過除以有2的倍數因子的數時候,后邊會跟shr edx,xx一類的指令或者mov xxx,edx
shr xxx,xx
比如除以72
會先除以9,再把商右移3次
除數的算法,例如開頭的一段匯編代碼:
mov eax,0x88888889
mul ecx
shr edx,0x5
除數=(2^(32+5))/(0x88888889-1)=60.000000013969838622484784252589
所以: 除數=60
乘法也會優化
31乘以某個數能不能寫成這個數乘以2的次冪 再減去這個數。
用數學語言表達一下就是:
設這個數為x
31*x=x*2^n-x
這個等式是否存在,如果存在,求n的值
那我們計算一下,
31=2^n -1
得2^n=32
得n=5
也就是說存在那么一個n使得,31乘以某個數的結果等於這個數乘以2的n次冪再減去一個數。
所以,一個乘法運算,最后就轉化成了一個速度較快的移位運算了。
以下資料參考網頁:
https://www.jianshu.com/p/c2ecadbb7b19
0x01 除法優化淺析
最近花了點時間去逆向一些小程序,遇到“(R0 * 0xAAAAAAAB) >> 32”這樣的運算時,一時看不出何意。后來經過搜索,才知道這是編譯器對除法做的優化(因為除法指令比較耗時)。在這里做個小筆記。
對於除法操作,如果除數是2的整數次方,那直接右移就可以了。比如:R0/4可以用R0>>2代替。如果除數不是2的整數次方,那如何優化呢?簡單寫一下原理:
結合示例來看:
void test(unsigned int a)
{ LOG("unsigned int a / 3 = %d", a / 3); }
test函數很簡單,看一下反匯編代碼(主要關心其中的a/3):
LDR R2, =0xAAAAAAAB UMULL.W R2, R3, R0, R2 LSRS R1, R3, #1
R0即test函數的參數a,最后a/3的計算結果保存在R1中。
首先,R0和R2做無符號數乘法(UMULL),結果的高32位保存到R3,低32位保存到R2。R2的值后續並沒有用到,相當於舍棄了,即只保留R0*R2的高32位,也就是相當於整個乘法運算的結果右移了32位。所以前2行代碼即:(R0 * R2) >> 32。
第3行代碼,又把R3邏輯右移了1位,所以這3行代碼合起來就是:(R0 * R2) >> 33。而R2的值是0xAAAAAAAB,所以最終結果就是:(R0 * 0xAAAAAAAB) >> 33。也就是編譯器將a/3優化成了(a * 0xAAAAAAAB) >> 33。那么這個結果,與上面提到的除法優化原理(a/b = (a*c) >> n,其中c=(2^n)/b)吻合嗎?
從“(a * 0xAAAAAAAB) >> 33”可知,編譯器選擇的n值為33,那么c=(2 ^ 33)/b。這里除數b為3,所以c=(2^33)/3=2863311530.67,向上取整為2863311531,換成16進制,即:0xAAAAAAAB。所以,這里編譯器所做的優化與上面提到的優化原理正好吻合。
剛才有一個c從2863311530.67向上取整為2863311531的操作,那么c的值就有一個0.33的誤差。那為什么這個誤差不會影響到最后的計算結果呢?這個是可以進行推理證明的,可以參考:https://www.cnblogs.com/shines77/p/4189074.html
0x02 由匯編反推除法
再來看一個例子,鞏固一下。假設有以下3行反匯編代碼,現在來反推回高級代碼。
LDR R2, =0xCCCCCCCD
UMULL.W R2, R3, R0, R2
LSRS R1, R3, #2
3行代碼合起來即:(R0 * 0xCCCCCCCD) >> 34。
除法優化原理:a/b = (a*c) >> n,其中c=(2^n)/b。
由(R0 * 0xCCCCCCCD) >> 34,可知n=34,c=0xCCCCCCCD。根據c=(2^ n)/b,可知b=(2^ n)/c=(2^34)/0xCCCCCCCD=4.99999999971,即b=5(因為c值有一個很小的,不影響除法運算結果的誤差,所以這里得到的值近似5)。所以,上述3行匯編代碼對應的高級代碼即:R0/5。與實際的源碼正好對應的上:
void test(int a) { LOG("int a / 5 = %d", a / 5); }
再回頭看一下剛開始提到的“R0 * 0xAAAAAAAB >> 32”,這個對應的高級代碼應該是什么?
除法優化原理:a/b = (a*c) >> n,其中c=(2^n)/b。
由“(R0 * 0xAAAAAAAB) >> 32”,可知n=32,c=0xAAAAAAAB。根據c=(2^ n)/b,可知b=(2^ n)/c=(2^32)/0xAAAAAAAB=1.49999999983,即b=1.5。所以“(R0 * 0xAAAAAAAB) >> 32”即R0/1.5。不過,這里提到的除法優化是針對整數常量來說的,所以實際就是R0/(3/2),即R0*2/3。
0x03 有符號數的除法優化
現在把test函數簡單修改一下:
void test(int a) { LOG("int a / 3 = %d", a / 3); }
原先參數類型是unsigned int,現在參數類型是int。看一下a/3對應的反匯編代碼:
LDR R2, =0x55555556
MOV R1, R0
SMULL.W R2, R3, R0, R2
SUB.W R1, R3, R1,ASR#31
這4行代碼合起來就是:(R0 * 0x55555556) >> 32 – (R0 >> 31),其中R0 >> 31是算數右移。先忽略后面的減法,只關心“(R0*0x55555556)>>32”。
除法優化原理:a/b = (a*c) >> n,其中c=(2^n)/b。
由“(R0 * 0x55555556) >> 32”,可知n=32,c=0x55555556。根據c=(2^ n)/b,可知b=(2^ n)/c=(2^32)/0x55555556=2.9999999986,即b=3。所以“(R0 * 0x55555556) >> 32”即R0/3。這么一看,貌似后面的“– (R0 >> 31)”是多余的。其實不然,簡單分析一下。
參數類型是int,“R0 >> 31”就是取符號位(算數右移)。那么有兩種情況:
1)R0是正數,那么R0 >> 31結果為0,減法相當於什么也沒做。
除法優化原理還是:a/b = (a*c) >> n,其中c=(2^n)/b。
2)R0是負數,那么R0 >> 31結果為0xFFFFFFFF,即-1,減-1相當於加1。
除法優化原理變成:a/b =( (a*c) >> n) + 1,其中c=(2^n)/b。
為什么被除數為負數時,后面要加1呢?因為“(a*c) >> n”是向下取整的結果。加1是為了向0取整,而c/c++語言對於整數除法的規定正是向0取整。
關於除法優化,還有很多更復雜的情況,以及一系列的理論推導。限於時間,我就先了解到這。對於簡單的情況,能根據反匯編代碼,反推回優化之前的除法操作了。
作者:十八垧
鏈接:https://www.jianshu.com/p/c2ecadbb7b19
來源:簡書