首先,先簡單回顧一下三目運算符(條件表達式)的格式
表達式一 ? 表達式二 : 表達式三
當表達式一的結果爲真時,選擇執行表達式二,否則執行表達式三。
看完這個格式,很明顯這是一個有分支的結構,那麼編譯器會老老實實的都按分支語句進行編譯麼,下面我們還是需要來分情況討論一下。
1.當表達式二或表達式三不爲常量
2.表達式二或表達式三爲常量
2.1 當表達式一爲0的等值比較,表達式二和表達式三差值爲一
2.2 當表達式一擴展爲非0等值比較,表達式2和3擴展爲其他任意常量時
2.3 當表達式一擴展爲區間比較,表達式2和3爲其他任意常量時
OK,我們下面主要來看一下下面的這幾種情況,首先,來看情況一,這種情況是無優化的情況,對應的彙編代碼就是一個分支結構
int main(int argc, char* argv[])
{
printf("%d",argc == 0 ? (int)argv : -1);
return 0;
}
對應的彙編代碼
.text:00401000 mov eax, [esp+argc]
.text:00401004 test eax, eax
.text:00401006 jnz short loc_40101D
.text:00401008 mov eax, [esp+argv] ;argc爲0的情況,直接拿argv
.text:0040100C push eax
.text:0040100D push offset unk_407030
.text:00401012 call sub_401040
.text:00401017 add esp, 8
.text:0040101A xor eax, eax
.text:0040101C retn
.text:0040101D ; ---------------------------------------------------------------------------
.text:0040101D
.text:0040101D loc_40101D: ; CODE XREF: _main+6↑j
.text:0040101D or eax, 0FFFFFFFFh ;當argc不爲0時,eax = -1
.text:00401020 push eax
.text:00401021 push offset unk_407030
.text:00401026 call sub_401040
.text:0040102B add esp, 8
.text:0040102E xor eax, eax
.text:00401030 retn
這種情況沒啥好記錄的,所以主要看下面爲常量的優化,對於常量的優化,debug和release下都是相同的。
首先來看2.1,這種情況一定要好好理解,因爲這種情況其實可以說是下面情況的原型,後面兩種都是在此基礎上進行擴展的而已。先來看例子
int main(int argc, char* argv[])
{
printf("%d",argc == 0 ? 0 : -1); //例一
printf("%d",argc == 0 ? 1 : 0); //例二
return 0;
}
這裏的例子有2個,先來看例一的反彙編
.text:00401001 mov esi, [esp+4+argc]
.text:00401005 mov eax, esi
.text:00401007 neg eax ;neg指令是對其求補(0-eax)
.text:00401009 sbb eax, eax ;sbb r1,r2 相當於 r1 = r1 - r2 - CF
.text:0040100B push eax
對於指令的說明寫在上面的註釋中了,對於sbb指令,可以發現這裏是自己減去自己,那麼肯定結果爲一,所以決定sbb的值肯定在於CF位是多少,而CF位又取決於上一行的取補指令,下面我們來分支討論一下CF位的情況
.text:00401007 neg eax ;if eax == 0 CF = 0 else CF = 1
.text:00401009 sbb eax, eax ;if CF == 0 eax = 0 else eax = -1
當eax爲0時,其求補結果爲0,所以CF位不會進位(CF=0),當CF不會進位時,其結果自然爲零了。反之當eax不爲零時,對其求補必然會產生進位,因爲這裏求補的本質是用零去減該數,那麼很明顯只有零值纔不會產生進/借位。
下面來看例二
.text:00401016 xor ecx, ecx ;需要注意這裏要清0
.text:00401018 test esi, esi
.text:0040101A setz cl ;當ZF=1時,cl=1 else cl = 0
.text:0040101D push ecx
setxx r8
當條件(標誌寄存器)滿足,r8寄存器會被設值1
對於setxx類型的指令上面解釋了,這裏彙編就比較明顯了,當esi的值爲0時,cl爲1,那麼反之cl等於0。
下面再來看2.2的情況,這種情況其實相當於對於上面情況的擴展
int main(int argc, char* argv[])
{
printf("%d",argc == 77 ? 88 : 66);
return 0;
}
對應的彙編代碼
.text:00401000 mov eax, [esp+argc]
.text:00401004 sub eax, 4Dh //4dh=77
.text:00401007 neg eax
.text:00401009 sbb eax, eax ;這裏構造結果 0 和 0xFFFFFFFF(-1)
.text:0040100B and al, 0EAh //0EAh=-22
.text:0040100D add eax, 58h //58h=88
.text:00401010 push eax
首先對於401004處的彙編代碼,減去一個77,相當於平移對齊到零值,這樣子就轉化爲上面2.1的例一情況了,也就是減去77後,其值爲零,那麼必然結果爲真,反之其值必然不等於77(爲假)。
下面對於40100B和40100D處的代碼進行分析,這裏還是需要分支來討論,畢竟上面sbb的結果就是一個分支情況
.text:00401009 sbb eax, eax ;eax = 0 || 0xFFFFFFFF
.text:0040100B and al, 0EAh ;if eax == 0 eax = 0 else eax = 0xFFFFFFEA
.text:0040100D add eax, 58h ; eax += 58h
if eax == 0
eax = 0 + 58h = 58h = 88
else
eax = 0xFFFFFFEA + 58h = 42h = 66
通過最後的分析也能得出當eax等於零(相當於等於77),其值爲88,否則其值爲66。
最後再來分析一下最後這種情況,這種情況其實是對上面案例的綜合體現,其實通過上面的分析,可以發現編譯器其實優化的特點就是構造出一個零值和一個0xFFFFFFFF值(-1),然後對其做and和add的操作。
int main(int argc, char* argv[])
{
printf("%d",argc >= 66 ? 77 : 55);
return 0;
}
對應的反彙編代碼
.text:00401004 xor eax, eax ;注意這裏eax需要清0
.text:00401006 cmp edx, 42h ;42h=66
.text:00401009 setl al ;當edx小於66時 al=1 else al=0
.text:0040100C dec eax
.text:0040100D and eax, 16h ;16h=22
.text:00401010 add eax, 37h ;37h=55
.text:00401013 push eax
下面我們先來找出上面所說的編譯器優化的特點-構造0和-1
.text:00401009 setl al ;if edx < 66 al=1 else al=0
.text:0040100C dec eax ;if edx < 66 eax = 0 else eax = 0xFFFFFFFF
通過了上面兩行的代碼執行後,這裏子就構造出了0和-1值,對於剩下的情況其實就和2.2一樣了,這裏簡單過一下
.text:0040100C dec eax 此時 eax = 0 || -1
.text:0040100D and eax, 16h ;16h=22
.text:00401010 add eax, 37h ;37h=55
if eax == 0 ;也就是小於66的情況
eax = 0 + 37h = 37h = 55
else ;大於等於66的情況
eax = 16h + 37h = 4dh = 77
看完上面的分析,其實按照我們的分析還原一下
argc < 66 ? 55 : 77
可以發現,編譯器生成的條件表達和源代碼是相反的,不過這樣也不影響代碼的還原,比較只是順序交換了一下而已。
對於這種情況,高版本還使用了CMOVXX系列的彙編指令進行優化
cmovxx S,R
當條件(標誌寄存器)滿足,S=R
如對上述的代碼,使用vs2015編譯出的彙編如下:
.text:00401043 cmp [ebp+argc], 42h //66
.text:00401047 mov ecx, 4Dh //77
.text:0040104C mov eax, 37h //55
.text:00401051 cmovge eax, ecx //大於等於條件滿足執行
.text:00401054 push eax
argc >= 66
eax = ecx = 77
else
eax = 55
所以對於高版本編譯器來說,此時還原高級代碼可能會更加的輕鬆。