将 CISC 解释为 RISC

Interpreting CISC as RISC

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

考虑在只有一种寻址模式(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

我知道这个问题至少可以说是模糊的,但这是我的问题,我真的不知道如何开始"translating" 从一种架构到另一种架构。

inc eax
mov [ecx+edx*4],eax

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

因为约束是 "only one addressing mode (Reg + Offset)."(而且 RISC 不太可能有 inc,但可以通过简单的 "fixed" add eax,1).

因此您必须将寻址方式从base_reg + index_reg*index_size_imm + ofs x86 寻址方式转换为reg + ofs。如果你想一想,没有合理的方法来使用 "ofs" 部分,除非你创建自修改代码,在执行它之前将一些漂亮的东西放入指令操作码中..所以它缩小了任务 "do it by (reg+0) addressing mode".

所以你做地址数学"manually"

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 不应该被人类使用)。


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


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

DANG,我设法创建了 "increase every element",错过了 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 字节大小。

这个问题只是想让你意识到 x86 指令集中存在的读-修改-写 (RMW) 指令(经典的 "CISC" ISA)在RISC 指令集。您在 x86 指令中找到的复杂内存寻址也不会。

相反,经典的 RISC ISA 将仅提供 "simplified" 组指令,这意味着您必须将 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  , %eax

然后是MOV指令。这是一个傻瓜。首先,我们需要弄清楚它到底在做什么。在这个 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 寄存器,而我们的 "translated" 版本破坏了 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    , %eax         # increment EAX by 1
mov    %edx, %reg9      # reg-reg move of EDX to a temp
shl    , %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 之外,那里没有那么多 "complex" 指令。我可能会做类似的事情:

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,理解起来会有些困难。您可以查找助记符,但即使不费吹灰之力,您仍然可以找出我提到的很多内容——"simpler" 指令、单独的加载 (lwzu) 和存储(stw),以及 三个操作数而不是两个。

奇怪的是,由于被选为示例的代码和优化编译器的魔力,x86 ("CISC") 反汇编和 PowerPC ( "RISC") 反汇编。