返回介绍

15.3 对比实例

发布于 2025-02-22 14:00:45 字数 11166 浏览 0 评论 0 收藏 0

试试这个

#!bash
double d_max (double a, double b)
{
    if (a>b)
    return a;
    return b;
};

15.3.1 x86

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

MSVC 2010 生成

#!bash
PUBLIC      _d_max
_TEXT   SEGMENT
_a$ = 8         ; size = 8
_b$ = 16        ; size = 8
_d_max      PROC
    push    ebp
    mov     ebp, esp
    fld     QWORD PTR _b$[ebp]

; current stack state: ST(0) = _b
; compare _b (ST(0)) and _a, and pop register

    fcomp   QWORD PTR _a$[ebp]

; stack is empty here

    fnstsw  ax
    test    ah, 5
    jp      SHORT $LN1@d_max

; we are here only if a>b

    fld     QWORD PTR _a$[ebp]
    jmp     SHORT $LN2@d_max
$LN1@d_max:
    fld     QWORD PTR _b$[ebp]
$LN2@d_max:
    pop     ebp
    ret     0
_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。

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

执行了 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

#!bash
_a$ = 8         ; size = 8
_b$ = 16        ; size = 8
_d_max  PROC
    fld     QWORD PTR _b$[esp-4]
    fld     QWORD PTR _a$[esp-4]
; current stack state: ST(0) = _a, ST(1) = _b
    fcom ST(1) ; compare _a and ST(1) = (_b)
    fnstsw ax
    test ah, 65 ; 00000041H
    jne SHORT $LN5@d_max
    fstp ST(1) ; copy ST(0) to ST(1) and pop register, leave (_a) on top
; current stack state: ST(0) = _a
    ret 0
$LN5@d_max:
    fstp ST(0) ; copy ST(0) to ST(0) and pop register, leave (_b) on top
; current stack state: ST(0) = _b
    ret 0
_d_max ENDP

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

如果 a>b  在我们的例子中,C3/C3/C0 会被设为 0,0,0
如果 b>a  标志位被设为:0,0,1
如果 a=b  标志位被设为:1,0,0

可以这么说,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

#!bash
d_max proc near
b               =qword ptr -10h
a               =qword ptr -8
a_first_half    = dword ptr 8
a_second_half   = dword ptr 0Ch
b_first_half    = dword ptr 10h
b_second_half   = dword ptr 14h

    push    ebp
    mov     ebp, esp
    sub     esp, 10h

; put a and b to local stack:

    mov     eax, [ebp+a_first_half]
    mov     dword ptr [ebp+a], eax
    mov     eax, [ebp+a_second_half]
    mov     dword ptr [ebp+a+4], eax
    mov     eax, [ebp+b_first_half]
    mov     dword ptr [ebp+b], eax
    mov     eax, [ebp+b_second_half]
    mov     dword ptr [ebp+b+4], eax

; load a and b to FPU stack:

    fld     [ebp+a]
    fld     [ebp+b]
; current stack state: ST(0) - b; ST(1) - a

    fxch    st(1) ; this instruction swapping ST(1) and ST(0)

; current stack state: ST(0) - a; ST(1) - b

    fucompp     ; compare a and b and pop two values from stack, i.e., a and b
    fnstsw  ax  ; store FPU status to AX
    sahf        ; load SF, ZF, AF, PF, and CF flags state from AH
    setnbe  al  ; store 1 to AL if CF=0 and ZF=0
    test    al, al               ; AL==0 ?
    jz      short loc_8048453    ; yes
    fld     [ebp+a]
    jmp     short locret_8048456

loc_8048453:
    fld     [ebp+b]
locret_8048456:
    leave
    retn
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 位会被设置成什么。

在我们的例子中,如果 a 比 b 大,那么 C3/C2/C0 位会被设为 0,0,0
如果 a 比 b 小,这些位会被设为 0,0,1
如果 a=b,这些位会被设为 1,0,0

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

如果 a>b,CPU 的标志位会被设为:ZF=0,PF=0,CF=0
如果 a<b,CPU 的标志位会被设为:ZF=0,PF=0,CF=1
如果 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 开关

#!bash
            public d_max
d_max       proc near
arg_0       = qword ptr 8
arg_8       = qword ptr 10h
            push    ebp
            mov     ebp, esp
            fld     [ebp+arg_0] ; _a
            fld     [ebp+arg_8] ; _b

; stack state now: ST(0) = _b, ST(1) = _a
            fxch    st(1)

; stack state now: ST(0) = _a, ST(1) = _b
            fucom   st(1) ; compare _a and _b

            fnstsw  ax
            sahf
            ja      short loc_8048448
; store ST(0) to ST(0) (idle operation), pop value at top of stack, leave _b at top
            fstp    st
            jmp     short loc_804844A

loc_8048448:
; store _a to ST(0), pop value at top of stack, leave _a at top
            fstp    st(1)
loc_804844A:
            pop     ebp
            retn
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 模式

#!bash
VMOV        D16, R2, R3 ; b
VMOV        D17, R0, R1 ; a
VCMPE.F64   D17, D16
VMRS        APSR_nzcv, FPSCR
VMOVGT.F64  D16, D17 ; copy b to D16
VMOV        R0, R1, D16
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 模式

#!bash
VMOV        D16, R2, R3 ; b
VMOV        D17, R0, R1 ; a
VCMPE.F64   D17, D16
VMRS        APSR_nzcv, FPSCR
IT GT
VMOVGT.F64  D16, D17
VMOV        R0, R1, D16
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 版)

#!bash
ITE NE
VMOVNE    R2, R3, D16
VMOVEQ    R2, R3, D17

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

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

#!bash
ITTTT EQ
MOVEQ       R0, R4
ADDEQ       SP, SP, #0x20
POPEQ.W     {R8,R10}
POPEQ       {R4-R7,PC}

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

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

#!bash
-EQ
-NE
-NE
-NE

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

#!bash
CMP.W       R0, #0xFFFFFFFF
ITTE LE
SUBLE.W     R10, R0, #1
NEGLE       R0, R0
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 指令

#!bash
cat AngryBirdsClassic.lst | grep " BF" | grep "IT" > results.lst

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

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

#!bash
b               =-0x20
a               =-0x18
val_to_return   = -0x10
saved_R7        = -4
                STR         R7, [SP,#saved_R7]!
                MOV         R7, SP
                SUB         SP, SP, #0x1C
                BIC         SP, SP, #7
                VMOV        D16, R2, R3
                VMOV        D17, R0, R1
                VSTR        D17, [SP,#0x20+a]
                VSTR        D16, [SP,#0x20+b]
                VLDR        D16, [SP,#0x20+a]
                VLDR        D17, [SP,#0x20+b]
                VCMPE.F64   D16, D17
                VMRS        APSR_nzcv, FPSCR
                BLE         loc_2E08
                VLDR        D16, [SP,#0x20+a]
                VSTR        D16, [SP,#0x20+val_to_return]
                B           loc_2E10
loc_2E08
                VLDR        D16, [SP,#0x20+b]
                VSTR        D16, [SP,#0x20+val_to_return]
loc_2E10
                VLDR        D16, [SP,#0x20+val_to_return]
                VMOV        R0, R1, D16
                MOV         SP, R7
                LDR         R7, [SP+0x20+b],#4
                BX          LR

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

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

#!bash
        PUSH    {R3-R7,LR}
        MOVS    R4, R2
        MOVS    R5, R3
        MOVS    R6, R0
        MOVS    R7, R1
        BL      __aeabi_cdrcmple
        BCS     loc_1C0
        MOVS    R0, R6
        MOVS    R1, R7
        POP     {R3-R7,PC}
loc_1C0
        MOVS    R0, R4
        MOVS    R1, R5
        POP     {R3-R7,PC}

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文