问题只是想让您意识到 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 系统上,不会有专门的 INC 和 DEC 指令,如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 指令中使用的复杂内存寻址实际上并没有修改 ECX 或 EDX 寄存器,而我们的“翻译”版本破坏了 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") 之间并没有太大区别反汇编。