将 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系统上,不会有专门的INC
和 DEC
指令,就像 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
指令中使用的复杂内存寻址实际上并没有修改 ECX
或 EDX
寄存器,而我们的 "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") 反汇编。
我正在准备计算机科学的考试,这个问题在以前的考试中不断出现,我找不到答案。
考虑在只有一种寻址模式(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系统上,不会有专门的INC
和 DEC
指令,就像 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
指令中使用的复杂内存寻址实际上并没有修改 ECX
或 EDX
寄存器,而我们的 "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") 反汇编。