15.3.1 x86

尽管这个函数很简单,但是理解它的工作原理并不容易。

MSVC 2010生成

  1. PUBLIC _d_max
  2. _TEXT SEGMENT
  3. _a$ = 8 ; size = 8
  4. _b$ = 16 ; size = 8
  5. _d_max PROC
  6. push ebp
  7. mov ebp, esp
  8. fld QWORD PTR _b$[ebp]
  9. ; current stack state: ST(0) = _b
  10. ; compare _b (ST(0)) and _a, and pop register
  11. fcomp QWORD PTR _a$[ebp]
  12. ; stack is empty here
  13. fnstsw ax
  14. test ah, 5
  15. jp SHORT $LN1@d_max
  16. ; we are here only if a>b
  17. fld QWORD PTR _a$[ebp]
  18. jmp SHORT $LN2@d_max
  19. $LN1@d_max:
  20. fld QWORD PTR _b$[ebp]
  21. $LN2@d_max:
  22. pop ebp
  23. ret 0
  24. _d_max ENDP

因此,FLD将_b中的值装入ST(0)寄存器中。

FCOMP对比ST(0)寄存器和_a值,设置FPU状态字寄存器中的C3/C2/C0位,这是一个反应FPU当前状态的16位寄存器。

C3/C2/C0位被设置后,不幸的是,IntelP6之前的CPU没有任何检查这些标志位的条件转移指令。可能是历史的原因(FPU曾经是单独的一块芯片)。从Intel P6开始,现在的CPU拥有FCOMI/FCOMIP/FUCOMI/FUCOMIP指令,这些指令功能相同,但会改变CPU的ZF/PF/CF标志位。

当标志位被设好后,FCOMP指令从栈中弹出一个变量。这就是和FCOM的不同之处,FCOM只对比值,让栈保持同样的状态。

FNSTSW讲FPU状态字寄存器的内容拷贝到AX中,C3/C2/C0放置在14/10/8位中,它们会在AX寄存器中相应的位置上,并且都放在AX的高位部分—AH。

  1. 如果 b>a 在我们的例子中,C3/C2/C0位会被设置为:000
  2. 如果 a>b 标志位被设为:0,0,1
  3. 如果 a=b 标识位被设为:100

执行了 test sh,5 之后,C3和C1的标志位被设为0,但是第0位和第2位(在AH寄存器中)C0和C2位会保留。

下面我们谈谈奇偶位标志。Another notable epoch rudiment:

一个常见的原因是测试奇偶位标志事实上与奇偶没有任何关系。FPU有4个条件标志(C0到C3),但是它们不能被直接测试,必须先拷贝到标志位寄存器中,在这个时候,C0放在进位标志中,C2放在奇偶位标志中,C3放在0标志位中。当例子中不可比较的浮点数(NaN或者其他不支持的格式)使用FUCOM指令进行比较的时候,会设置C2标志位。

如果一个数字是奇数这个标志就会被设置为1。如果是偶数就会被设置为0.

因此,PF标志会被设置为1如果C0和C2都被设置为0或者都被设置为1。然后jp跳转就会实现。如果我们recall valuesof C3/C2/C0,我们将会发现条件跳转jp可能会在两种情况下触发:b>a或者a==b(C3位这里不再考虑,因为在执行test sh,5指令之后已经被清零了)

之后就简单了。如果条件跳转被触发,FLD会将_b的值放入ST(0)寄存器中,如果没有被触发,_a变量的值会被加载 但是还没有结束。

15.3.2 下面我们用msvc2010优化模式来编译它/0x

  1. _a$ = 8 ; size = 8
  2. _b$ = 16 ; size = 8
  3. _d_max PROC
  4. fld QWORD PTR _b$[esp-4]
  5. fld QWORD PTR _a$[esp-4]
  6. ; current stack state: ST(0) = _a, ST(1) = _b
  7. fcom ST(1) ; compare _a and ST(1) = (_b)
  8. fnstsw ax
  9. test ah, 65 ; 00000041H
  10. jne SHORT $LN5@d_max
  11. fstp ST(1) ; copy ST(0) to ST(1) and pop register, leave (_a) on top
  12. ; current stack state: ST(0) = _a
  13. ret 0
  14. $LN5@d_max:
  15. fstp ST(0) ; copy ST(0) to ST(0) and pop register, leave (_b) on top
  16. ; current stack state: ST(0) = _b
  17. ret 0
  18. _d_max ENDP

FCOM区别于FCOMP在某种程度上是它只比较值然后并不改变FPU的状态。和之前的例子不同的是,操作数是逆序的。这也是C3/C2/C0中的比较结果是不同的原因。

  1. 如果 a>b 在我们的例子中,C3/C3/C0会被设为000
  2. 如果 b>a 标志位被设为:001
  3. 如果 a=b 标志位被设为:100

可以这么说,test ah,65指令只保留两位—C3和C0.如果a>b那么两者都被设为0:在那种情况下,JNE跳转不会被触发。 FSTP ST(1)接下来—这个指令会复制ST(0)中的值放入操作数中,然后从FPU栈中跑出一个值。 换句话说,这个这个指令将ST(0)中的值复制到ST(1)中。然后,a的两个值现在在栈定。之后,一个值被抛出。之后,ST(0)会包含a然后函数执行完毕。

条件跳转JNE在两种情况下触发:b>a或者a==b。ST(0)中的值拷贝到ST(0)中,就像nop指令一样,然后一个值从栈中抛出,然后栈顶(ST(0))会包含ST(1)之前的包含的内容(就是_b)。函数执行完毕。这条指令在这里使用的原因可能是FPU没有从栈中抛出值的指令并且没有地方存储。 但是,还没有结束。

15.3.3 GCC 4.4.1

  1. d_max proc near
  2. b =qword ptr -10h
  3. a =qword ptr -8
  4. a_first_half = dword ptr 8
  5. a_second_half = dword ptr 0Ch
  6. b_first_half = dword ptr 10h
  7. b_second_half = dword ptr 14h
  8. push ebp
  9. mov ebp, esp
  10. sub esp, 10h
  11. ; put a and b to local stack:
  12. mov eax, [ebp+a_first_half]
  13. mov dword ptr [ebp+a], eax
  14. mov eax, [ebp+a_second_half]
  15. mov dword ptr [ebp+a+4], eax
  16. mov eax, [ebp+b_first_half]
  17. mov dword ptr [ebp+b], eax
  18. mov eax, [ebp+b_second_half]
  19. mov dword ptr [ebp+b+4], eax
  20. ; load a and b to FPU stack:
  21. fld [ebp+a]
  22. fld [ebp+b]
  23. ; current stack state: ST(0) - b; ST(1) - a
  24. fxch st(1) ; this instruction swapping ST(1) and ST(0)
  25. ; current stack state: ST(0) - a; ST(1) - b
  26. fucompp ; compare a and b and pop two values from stack, i.e., a and b
  27. fnstsw ax ; store FPU status to AX
  28. sahf ; load SF, ZF, AF, PF, and CF flags state from AH
  29. setnbe al ; store 1 to AL if CF=0 and ZF=0
  30. test al, al ; AL==0 ?
  31. jz short loc_8048453 ; yes
  32. fld [ebp+a]
  33. jmp short locret_8048456
  34. loc_8048453:
  35. fld [ebp+b]
  36. locret_8048456:
  37. leave
  38. retn
  39. d_max endp

FUCOMMP 类似FCOM指令,但是两个值都从栈中取,并且处理NaN(非数)有一些不同之处。

更多关于”非数“的:

FPU能够处理特殊的值比如非数字或者NaNs。它们是无穷大的,除零的结果等等。NaN可以是“quiet”并且“signaling”的。但是如果进行任何有关“signaling”的操作将会产生异常。

FCOM会产生异常如果操作数中有NaN。FUCOM只在操作数有signaling NaN (SNaN)的情况下产生异常。

接下来的指令是SANF—这条指令很少用,它不使用FPU。AH的8位以这样的顺序放入CPU标志位的低8位中:SF:ZF:-:AF:-:PF:-:CF<-AH。

FNSTSW将C3/C2/C0位放入AH寄存器的第6,2,0位中。

换句话说,fnstsw ax/sahf指令对是将C3/C2/C0移入CPU标志位ZF,PF,CF中。

现在我们来回顾一下,C3/C2/C0位会被设置成什么。

  1. 在我们的例子中,如果ab大,那么C3/C2/C0位会被设为000
  2. 如果ab小,这些位会被设为001
  3. 如果ab,这些位会被设为100

换句话说,在 FUCOMPP/FNSTSW/SAHF指令后,我们的CPU标志位的状态如下

  1. 如果a>b,CPU的标志位会被设为:ZF=0,PF=0,CF=0
  2. 如果a<b,CPU的标志位会被设为:ZF=0,PF=0,CF=1
  3. 如果a=b,CPU的标志位会被设为:ZF=1,PF=0,CF=0

SETNBE指令怎样给AL存储0或1:取决于CPU标志位。几乎是JNBE的计数器,利用设置cc码产生的异常,来给AL写入0或1,但是Jccbut Jcc do actual jump or not.SETNBE存储1只在CF=0并且ZF=0的情况下。如果为假,将会存储0。

cf和ZF都为0只存在于一种情况:a>b

然后one将会被存入AL中,接下来JZ不会被触发,函数将返回_a。在其他的情况下,返回的是_b。

15.3.4 GCC 4.4.1-03优化选项turned开关

  1. public d_max
  2. d_max proc near
  3. arg_0 = qword ptr 8
  4. arg_8 = qword ptr 10h
  5. push ebp
  6. mov ebp, esp
  7. fld [ebp+arg_0] ; _a
  8. fld [ebp+arg_8] ; _b
  9. ; stack state now: ST(0) = _b, ST(1) = _a
  10. fxch st(1)
  11. ; stack state now: ST(0) = _a, ST(1) = _b
  12. fucom st(1) ; compare _a and _b
  13. fnstsw ax
  14. sahf
  15. ja short loc_8048448
  16. ; store ST(0) to ST(0) (idle operation), pop value at top of stack, leave _b at top
  17. fstp st
  18. jmp short loc_804844A
  19. loc_8048448:
  20. ; store _a to ST(0), pop value at top of stack, leave _a at top
  21. fstp st(1)
  22. loc_804844A:
  23. pop ebp
  24. retn
  25. d_max endp

几乎相同除了一种情况:JA替代了SAHF。事实上,条件跳转指令(JA, JAE, JBE, JBE, JE/JZ, JNA, JNAE, JNB, JNBE, JNE/JNZ)检查通过检查CF和ZF标志来知晓两个无符号数字的比较结果。C3/C2/C0位在比较之后被放入这些标志位中然后条件跳转就会起效。JA会生效如果CF和ZF都为0。

因此,这里列出的条件跳转指令可以在FNSTSW/SAHF指令对之后使用。

看上去,FPU C3/C2/C0状态位故意放置在那里,传递给CPU而不需要额外的交换。

15.3.5 ARM+优化Xcode(LLVM)+ARM模式

  1. VMOV D16, R2, R3 ; b
  2. VMOV D17, R0, R1 ; a
  3. VCMPE.F64 D17, D16
  4. VMRS APSR_nzcv, FPSCR
  5. VMOVGT.F64 D16, D17 ; copy b to D16
  6. VMOV R0, R1, D16
  7. BX LR

一个简单例子。输入值放在D17到D16寄存器中,然后借助VCMPE指令进行比较。就像x86协处理器一样,ARM协处理器拥有自己的标志位寄存器(FPSCR),因为存储协处理器的特殊标志需要存储。

就像x86中一样,在ARM中没有条件跳转指令,在协处理器状态寄存器中检查位,因此这里有VMRS指令,从协处理器状态字复制4位(N,Z,C,V)放入通用状态位(APSR寄存器)

VMOVGT类似MOVGT指令,如果比较时一个操作数比其它的大,指令将会被执行。

如果被执行了,b值将会写入D16,暂时被存储在D17中。

如果没有被执行,a的值将会保留在D16寄存器中。

倒数第二个指令VMOV将会通过R0和R1寄存器对准备D16寄存去中的值来返回。

15.3.6 ARM+优化 Xcode(LLVM)+thumb-2 模式

  1. VMOV D16, R2, R3 ; b
  2. VMOV D17, R0, R1 ; a
  3. VCMPE.F64 D17, D16
  4. VMRS APSR_nzcv, FPSCR
  5. IT GT
  6. VMOVGT.F64 D16, D17
  7. VMOV R0, R1, D16
  8. BX LR

几乎和前一个例子一样,有一些小小的不同。事实上,许多ARM中的指令在ARM模式下根据条件判定,当条件为真则执行。

但是在thumb代码中没有这样的事。在16位的指令中没有空闲的4位来编码条件。

但是,thumb-2为老的thumb指令进行扩展使得特殊判断成为可能。

这里是IDA-生成的表单,我们可以看到VMOVGT指令,和在前一个例子中是相同的。

但事实上,常见的VMOV就这样编码,但是IDA加上了—GT后缀,因为以前会放置“IT GT”指令。

IT指令定义所谓的if-then块。指令后面最多放置四条指令是可能的,判断后缀会被加上。在我们的例子中,“IT GT”意味着下一条指令会被执行,如果GT(Greater Than)条件为真。

下面是一段更加复杂的代码,来源于“愤怒的小鸟”(ios版)

  1. ITE NE
  2. VMOVNE R2, R3, D16
  3. VMOVEQ R2, R3, D17

ITE意味着if-the-else并且它为接下来的两条指令加上后缀。第一条指令将会执行如果ITE(NE,不相等)这时为真,为假则执行第二条指令。(与NE对立的就是EQ(equal))

这段代码也来自“愤怒的小鸟”:

  1. ITTTT EQ
  2. MOVEQ R0, R4
  3. ADDEQ SP, SP, #0x20
  4. POPEQ.W {R8,R10}
  5. POPEQ {R4-R7,PC}

4个“T”符号在助记符中意味着接下来的4条指令将会被执行如果条件为真。这也是IDA在每条指令后面加上-EQ后缀的原因。

如果出现上面例子中ITEEE EQ(if-then-else-else-else),那么这些后缀将会被这样设置。

  1. -EQ
  2. -NE
  3. -NE
  4. -NE

另一段来自“愤怒的小鸟”的代码。

  1. CMP.W R0, #0xFFFFFFFF
  2. ITTE LE
  3. SUBLE.W R10, R0, #1
  4. NEGLE R0, R0
  5. MOVGT R10, R0

ITTE(if-then-then-else)意味着第一条第二条指令将会被执行,如果LE(Less or Equal)条件为真,反之第三条指令将会执行。

编译器通常不生成所有的组合。举个例子,在“愤怒的小鸟”中提到的(ios经典版)只有这些IT指令会被使用:IT,ITE,ITT,ITTE,ITTT,ITTTT.我们怎样去学习它呢?在IDA中,产生这些列举的文件是可能的,于是我这么做了,并且设置选项以4字节的格式现实操作码。因为IT操作码的高16位是0xBF,使用grep指令

cat AngryBirdsClassic.lst | grep " BF" | grep "IT" > results.lst

另外,对于thumb-2模式 ARM汇编语言的程序,通过附加的条件后缀,必要的时候汇编会自动加上IT指令和相应的标志。

15.3.7 ARM+非优化模式 Xcode(LLVM)+ARM模式

  1. b =-0x20
  2. a =-0x18
  3. val_to_return = -0x10
  4. saved_R7 = -4
  5. STR R7, [SP,#saved_R7]!
  6. MOV R7, SP
  7. SUB SP, SP, #0x1C
  8. BIC SP, SP, #7
  9. VMOV D16, R2, R3
  10. VMOV D17, R0, R1
  11. VSTR D17, [SP,#0x20+a]
  12. VSTR D16, [SP,#0x20+b]
  13. VLDR D16, [SP,#0x20+a]
  14. VLDR D17, [SP,#0x20+b]
  15. VCMPE.F64 D16, D17
  16. VMRS APSR_nzcv, FPSCR
  17. BLE loc_2E08
  18. VLDR D16, [SP,#0x20+a]
  19. VSTR D16, [SP,#0x20+val_to_return]
  20. B loc_2E10
  21. loc_2E08
  22. VLDR D16, [SP,#0x20+b]
  23. VSTR D16, [SP,#0x20+val_to_return]
  24. loc_2E10
  25. VLDR D16, [SP,#0x20+val_to_return]
  26. VMOV R0, R1, D16
  27. MOV SP, R7
  28. LDR R7, [SP+0x20+b],#4
  29. BX LR

基本和我们看到的一样,但是太多冗陈代码,因为a和b的变量存储在本地栈中,还有返回值

15.3.8 ARM+优化模式keil+thumb模式

  1. PUSH {R3-R7,LR}
  2. MOVS R4, R2
  3. MOVS R5, R3
  4. MOVS R6, R0
  5. MOVS R7, R1
  6. BL __aeabi_cdrcmple
  7. BCS loc_1C0
  8. MOVS R0, R6
  9. MOVS R1, R7
  10. POP {R3-R7,PC}
  11. loc_1C0
  12. MOVS R0, R4
  13. MOVS R1, R5
  14. POP {R3-R7,PC}

keil 不为浮点数的比较生成特殊的指令,因为他不能依靠核心CPU的支持,它也不能直接按位比较。这里有一个外部函数用于比较:__aeabi_cdrcmple. N.B. 比较的结果用来设置标志,因此接下来的BCS(标志位设置 - 大于或等于)指令可能有效并且无需额外的代码。