【问题标题】:Interpreting CISC as RISC将 CISC 解释为 RISC
【发布时间】:2017-06-04 18:13:28
【问题描述】:

我正在准备计算机科学的考试,在以前的考试中不断出现这个问题,我找不到答案。

考虑在只有一种寻址模式(Reg + Offset)的典型 RISC 架构上执行以下两条指令。

804836e:    40            inc    %eax
804836f:    89 04 91      mov    %eax,(%ecx,%edx,4)

解释证明,如何为这个架构编译这 2 条 IA-32 指令,使用 IA-32 sintaxe 来显示该代码。

评论这段代码在 RISC 架构与 IA-32 上的大小。

C 函数是:

voidpara_par (int a[], int n) {
    int i;
    for (i=0 ; i<n ; i++) {
        if (a[i] & 0x01) {
            a[i] += 1;
        }
    }
}

它接收一个整数数组并增加奇数的值。

供参考:

%eax -> int i
(%ecx,%edx,4) -> int, part of the array "a" saved to %ecx

我知道这个问题至少可以说含糊不清,但这是我的问题,我真的不知道如何开始从一种架构“转换”到另一种架构。

【问题讨论】:

    标签: assembly x86 cpu-architecture


    【解决方案1】:

    问题只是想让您意识到 x86 指令集中存在的读-修改-写 (RMW) 指令(经典的“CISC”ISA)在 RISC 指令集上不可用。您在 x86 指令中发现的复杂内存寻址也不会。

    相反,经典的 RISC ISA 只会提供一组“简化”的指令,这意味着您必须将 x86 CISC 样式的指令分解为一组更简单的指令。换句话说,您只需将它们分解为它们的组成部分。

    除了向您展示 CISC 和 RISC 之间的经典对比之外,这是一个有点有趣的练习的另一个原因是因为它与现代 x86 处理器在内部执行的操作相同。你看,即使你(程序员)为 x86 编写了这种 CISC 风格的代码,但在内部,处理器实际上更像是一个 RISC 系统,所以当它解码指令时,它会将它解码成一系列类似于程序员会为 RISC 系统编写代码。

    让我们研究一下你得到的指示:

    804836e:    40            inc    %eax
    804836f:    89 04 91      mov    %eax,(%ecx,%edx,4)
    

    参数(int)是在EAX 寄存器中传递的,所以这只是将它增加 1。很可能,在 RISC 系统上,不会有专门的 INCDEC 指令,如x86上有。这些被添加到 x86 以镜像 C 等语言中的 ++i--i 语句,但请注意它们相当于 1 的加法(或减法)。因此,在 RISC 系统上,您只需编写:

    add  $1, %eax
    

    然后是MOV 指令。这是一个doozie。首先,我们需要弄清楚它实际上在做什么。在这种 AT&T/GAS 语法中,很难看到发生了什么(在我看来),所以让我们将其重写为更正常的 Intel/MASM 语法:

    mov  DWORD PTR [ecx+edx*4], eax
    

    现在,很明显这会将EDX 寄存器的内容乘以4,将其添加到ECX 寄存器的内容中,然后将EAX 的内容存储在此地址。

    经典的 RISC 系统不支持这种复杂的内存寻址,因此我们需要将其分解为更简单的位。不过,这并不难,既然我们了解了指令的作用:

    shl  edx, 2                  ; edx *= 4
    add  edx, ecx                ; edx += ecx
    mov  DWORD PTR [edx], eax    ; store EAX at address in EDX
    

    同样值得注意的是,经典的 RISC 系统不会像 x86 那样重载 MOV 指令。通常,RISC 是 load-store architecture,它将 x86 的 MOV 指令分解为三个不同的东西:

    • 寄存器到寄存器 MOV 指令
    • LOAD 指令从内存加载到寄存器中
    • STORE 指令从寄存器存储到内存中

    因此,如果您实际上将其转换为 RISC ISA,代码可能看起来更像:

    shl    edx, 2
    add    edx, ecx
    store  [edx], eax
    

    差别不大,但重要的是要了解,在普通 RISC 系统上,MOV 不会像在 x86 上那样做所有事情。

    如果你是那种喜欢吹毛求疵的人,那是一件小事。我们最初在 x86 MOV 指令中使用的复杂内存寻址实际上并没有修改 ECXEDX 寄存器,而我们的“翻译”版本破坏了 EDX 寄存器。如果我们想要精确,我们需要:

    mov    reg9, edx    ; a reg-reg move to a new temporary register
    shl    reg9, 2
    add    reg9, ecx
    store  [reg9], eax
    

    其中reg9 是一个新的临时寄存器。在 x86 的现代实现中,这将是一个隐藏的、非体系结构寄存器,这意味着它是处理器硬件具有但不作为指令集的一部分向程序员公开的寄存器。在大多数 RISC 系统上,这也不是必需的,因为它们的指令接受 三个 操作数(操作数 1、操作数 2 和目标),因此可以将结果放在不同的寄存器中而不会破坏任何操作数。因此,您将拥有:

    shl   reg9, edx, 2
    add   reg9, reg9, ecx
    store [reg9], eax
    

    因此,将它们放在 AT&T 语法中,所需的答案将类似于:

    add    $1, %eax         # increment EAX by 1
    mov    %edx, %reg9      # reg-reg move of EDX to a temp
    shl    $2, %reg9        # scale by 4
    add    %ecx, %reg9      # add offset in ECX
    store  %eax, (%reg9)    # store EAX at this address
    

    只是为了好玩,让我们来看看给你的 C 代码,看看它在为两种不同类型的架构编译后的样子。以下是 GCC 4.8 在针对 x86(经典 CISC 架构)时生成的内容:

    para_par(int*, int):
            mov     edx, DWORD PTR [esp+8]
            mov     eax, DWORD PTR [esp+4]
            test    edx, edx
            lea     ecx, [eax+edx*4]
            jle     .L1
    .L5:
            mov     edx, DWORD PTR [eax]
            test    dl, 1
            je      .L3
            inc     edx
            mov     DWORD PTR [eax], edx
    .L3:
            add     eax, 4
            cmp     eax, ecx
            jne     .L5
    .L1:
            ret
    

    现在,将其重写为 RISC 样式(根据您的作业要求)将非常简单 — 除了 LEA 之外,没有那么多“复杂”指令。我可能会这样做:

    para_par(int*, int):
            load    ecx, DWORD PTR [esp+8]
            cmp     ecx, 0
            jle     .L1
            load    eax, DWORD PTR [esp+4]
            add     ecx, ecx
            add     ecx, ecx
            add     ecx, eax
    .L5:
            load    edx, DWORD PTR [eax]
            test    dl, 1    ; a bit "complex" because it's an AND that doesn't modify, but
                             ; on RISC, we'd have the three-operand form of AND to avoid modifying
            je      .L3
            add     edx, 1
            store   DWORD PTR [eax], edx
    .L3:
            add     eax, 4
            cmp     eax, ecx
            jne     .L5
    .L1:
            ret
    

    如果您为 PowerPC(经典的 RISC 架构)编译,GCC 4.8 会生成以下内容:

    para_par(int*, int):
            cmpwi 7,4,0       # compare to 0
            addi 3,3,-4       # add
            slwi 4,4,2        # left-shift by 2
            add 4,3,4         # add
            blelr- 7          # branch if less-than-or-equal
            subf 4,3,4        # subtract
            addi 4,4,-4       # add
            srwi 4,4,2        # right-shift by 2
            addi 4,4,1        # add
            mtctr 4           # move to count register (a special register)
    .L9:
            lwzu 9,4(3)       # load from memory
            andi. 10,9,1      # bitwise AND
            addi 9,9,1        # add
            beq- 0,.L3        # branch if equal-to
            stw 9,0(3)        # store to memory
    .L3:
            bdnz .L9          # decrement and branch if not-zero
            blr               # unconditional branch (JMP)
    

    当然,PowerPC 的助记符与 x86 的助记符都不同,如果您只研究过 x86,就很难理解。您可以查找助记符,但即使不费力气,您仍然可以挑选出很多我提到的内容——“更简单”的指令、单独的加载 (lwzu) 和存储 (stw ),以及 三个 操作数而不是两个。

    奇怪的是,由于选择作为示例的代码和优化编译器的魔力,x86 ("CISC") 反汇编和 PowerPC ("RISC") 之间并没有太大区别反汇编。

    【讨论】:

    • 是的,我不想花足够的时间回答我的问题,这要完整得多。只是为了有趣的事实:x86 上的add x,1inc x 更加棘手,因为 inc 确实 not 更新进位标志,这在某些算法中很少被利用,例如做加法数组的多个元素带有进位,并按dec 计算元素数量(保留进位以供加法本身)。一些 RISC CPU 通过加载/存储指令的 ++/-- 修饰符来弥补这一点(类似于 x86 lods/stos,但更强大)......无论如何,大多数 ISA 往往有一些有趣的细节:)
    【解决方案2】:
    inc eax
    mov [ecx+edx*4],eax
    

    几乎是在使用琐碎的指令,那么为什么这不是 RISC 类的实际答案呢?

    因为约束是“只有一种寻址模式(Reg + Offset)。”(而且 RISC 不太可能有inc,但这可以通过简单的@987654323 “修复” @)。

    因此您必须将寻址从base_reg + index_reg*index_size_imm + ofs x86 寻址模式转换为reg + ofs。如果您想一想,没有合理的方式使用“ofs”部分,除非您创建自修改代码,在执行指令操作码之前将一些漂亮的 ofs 放入指令操作码中......所以它缩小到任务“通过 (reg+0) 寻址模式来实现”。

    所以你“手动”计算地址

    add  eax,1   ; inc eax
    shl  edx,2   ; edx = edx*4
    add  ecx,edx ; ecx = ecx + edx*4
    mov  [ecx+0],eax
    

    完成。 (故意使用 Intel 语法,因为我认为 GAS/AT&T 不应该被人类使用)。


    所有关于结果的解释/推理都留给了 OP,因为他应该尝试。 :)(如果您遇到困难,请在评论中告诉我)


    顺便说一句,如果你翻译整个 C 部分,它肯定会导致更优化的机器代码,而首先没有 *4 乘法,所以 x86 和“RISC”机器代码看起来更相似,除了x86 可以直接在内存中操作数组元素:

    DANG,我设法创建了“增加每个元素”,错过了 if 部分,抱歉.... 不会修复,因为这说明了循环与索引的任何方式,这是我的初衷出,原来的mov %eax,(%ecx, %edx, 4)是相当人为的,很难在优化的机器代码中找到。

        eax = array + n*4
        ecx = -n*4
    loop:
        inc dword [eax+ecx]
        add ecx,4
        jnz loop
    

    类似 RISC 的版本:

        ebx = array
        ecx = n
    loop:
        mov eax,[ebx]
        add eax,1
        mov [ebx],eax
        add ebx,4
        sub ecx,1
        jnz loop
    

    同样不需要索引,这是高级的东西,通常可以在优化的机器代码中很容易地避免,对数据结构有足够的固定约束,比如这里每个元素都是固定的 4 字节大小。

    【讨论】:

      猜你喜欢
      • 2016-01-21
      • 1970-01-01
      • 1970-01-01
      • 2012-10-15
      • 1970-01-01
      • 1970-01-01
      • 2021-08-07
      • 2017-04-23
      • 1970-01-01
      相关资源
      最近更新 更多