这个 lab 涉及了 Y86-64 的实现。题目难度不大,做题的主要困难在实验环境安装和测试,做之前要仔细阅读文档。
首先建立实验环境,解压 sim
包,这里是所使用的工具,需要 make
,刚开始我总是出错,后来从网上找到如下解决方法:
修改Makefile文件( sim 目录下),注释掉(因为 ubuntu 没有安装相关库,这样模拟器就不支持 GUI 界面了):
#GUIMODE=-DHAS_GUI
#TKLIBS=-L/usr/lib -ltk -ltcl
#TKINC=-isystem /usr/include
然后
unix> make clean;make
如果仍出错,可能是没装相关的依赖软件,安装 flex
和 bison
试试。
这次 lab 分了三个部分: Part A 、B 、C
Part A 要求将 C 代码翻译成 Y86-64 ,Part B 要求实现 SEQ 命令,这两部分都为 Part C 做铺垫,Part C 要求修改 PIPE 和 Y86-64 实现程序的优化。
参考资料:
Seterplus/CSAPP
CSAPP:Architecture Lab - IT閱讀
csapp archlab 模拟器安装
深入理解计算机系统:体系结构实验
要求将 C 代码翻译成 Y86-64,工作目录在 ../sim/misc
中,第一题题解在 sum.ys 中,是 Y86-64 形式的汇编代码,使用 make sum.yo
或 ./yas sum.ys
编译,使用 ./yis sum.yo
查看模拟器运行结果。
第一题要求写个计算链表和的函数,给了该函数的 C 代码。
可以考虑将题目的 C 代码用 gcc
和 objdump
翻译成汇编代码,然后再将汇编翻译成 Y86-64 ;这里程序较简单,可以直接写出代码。
下面详细讲解第一题的题解。(这份题解参考了不少资料)
首先是指令的开头:
# Execution begins at address 0
.pos 0
irmovq stack, %rsp
# Set up stack pointer
设置栈指针,栈的位置在最后一行设置了:
# Stack starts here and grows to lower addresses
.pos 0x200
stack:
然后调用 main
,结束:
call main
# Execute main program
halt
# Terminate program
这是整个代码的框架;
# Sample linked list
.align 8
ele1:
.quad 0x00a
.quad ele2
ele2:
.quad 0x0b0
.quad ele3
ele3:
.quad 0xc00
.quad 0
上面定义了链表,并赋了初值;
main:
irmovq ele1,%rdi # 传参数值
call sumlist # 调用函数
ret
这是 main
的定义,其中调用了 sumlist
函数,该函数定义为如下:
sumlist:
xorq %rax,%rax # 设置 sum 初值为 0,long val = 0
# sum = 0
andq %rdi , %rdi # 判断 ls (就是链表指针,最初为传入值,链表的首地址)是否为 0
je end # ls 为 0 时直接返回
loop: mrmovq (%rdi) , %rcx # 循环: 保存 ls->val
addq %rcx , %rax # 给 sum 累加值,就是 val += ls->val
irmovq $8 , %rbx # 保存 8
addq %rbx , %rdi # 链表指针加 8 ,就是 &(ls->next)
mrmovq (%rdi), %rdi # ls=ls->next
andq %rdi , %rdi # 判断 ls 是否为 0
jne loop # ls 不为 0 时循环
end:
ret # 返回
可以看到题解就是直接翻译了 C 代码,运行测试,模拟器得到了结果:
Stopped in 31 steps at PC = 0x13. Status 'HLT', CC Z=1 S=0 O=0
Changes to registers:
%rax: 0x0000000000000000 0x0000000000000cba
%rcx: 0x0000000000000000 0x0000000000000c00
%rbx: 0x0000000000000000 0x0000000000000008
%rsp: 0x0000000000000000 0x0000000000000200
Changes to memory:
0x01f0: 0x0000000000000000 0x000000000000005b
0x01f8: 0x0000000000000000 0x0000000000000013
这题要求实现第一题的递归版本,题解在 rsum.ys 中,同样直接翻译 C 代码,但是要实现递归时一定要注意分清哪些变量应该是 调用者保存的
!
下面是递归版本的 rsumlist
函数的实现:
rsumlist:
pushq %rcx # val: 是调用者保存的寄存器
andq %rdi , %rdi # 判断条件
je end # 直接返回
mrmovq (%rdi) , %rcx # ls->val
irmovq $8 , %rbx
addq %rbx , %rdi
mrmovq (%rdi), %rdi # ls->next
call rsumlist # 递归
addq %rcx , %rax # return val+rest;
end:
popq %rcx
ret
测试得到 sum 和第一题相同。
实现一个将源数组(src)复制到目标数组(dest)的函数,并计算原数组中所有项的异或(Xor)值,这题的题解保存在 copy_block.ys 中,和前两题一样,直接翻译原函数。
这里调用函数时要分别传入三个值,实现 main 如下:
main:
irmovq src , %rdi
irmovq dest , %rsi
irmovq $3 , %rdx
call copyblock
ret
调用的函数实现如下:
copyblock:
xorq %rax , %rax # long result = 0;
loop: # 循环
andq %rdx , %rdx # len
jle end # 判断 len 是否 > 0
mrmovq (%rdi) , %rcx # long val = *src;
irmovq $8 , %rbx
addq %rbx , %rdi # src++;
rmmovq %rcx , (%rsi) # *dest = val;
addq %rbx , %rsi # dest++;
xorq %rcx , %rax # result ˆ= val;
irmovq $1 , %rbx
subq %rbx , %rdx # len--;
jmp loop
end:
ret
测试得到结果:
Stopped in 45 steps at PC = 0x13. Status 'HLT', CC Z=1 S=0 O=0
Changes to registers:
%rax: 0x0000000000000000 0x0000000000000cba
%rcx: 0x0000000000000000 0x0000000000000c00
%rbx: 0x0000000000000000 0x0000000000000001
%rsp: 0x0000000000000000 0x0000000000000200
%rsi: 0x0000000000000000 0x0000000000000048
%rdi: 0x0000000000000000 0x0000000000000030
Changes to memory:
0x0030: 0x0000000000000111 0x000000000000000a
0x0038: 0x0000000000000222 0x00000000000000b0
0x0040: 0x0000000000000333 0x0000000000000c00
0x01f0: 0x0000000000000000 0x000000000000006f
0x01f8: 0x0000000000000000 0x0000000000000013
工作目录在 sim/seq
中,要求将 SEQ 扩展以支持 iaddq
指令(该指令在书上家庭作业的 4.51 和 4.52 说明了,第二版书在 4.48 和 4.50 上,我这里的文档只要求实现 iaddq
,参考资料里是二版的题,还实现了 leave
指令),该指令要求一步实现将常数值添加到目的寄存器。做法就是修改目录下的 seq-full.hcl ,它实现了一个和书上一样的 SEQ 。
参考书上 OP1
和 mrmovl
的实现,可以写出 iaddq
的实现步骤如下:
iaddq A , rB
icode:ifun <-- M1[PC] # 取指
rA:rB <-- M1[PC+1]
valC <-- M8[PC+2]
valP <-- PC+10
valB <-- R[rB] # 译码
valE <-- valB + valC # 执行
R[rB] <-- valE # 写回
PC <-- valP # 更新 PC
然后根据实现修改 hcl 代码:
# 取指
# 指令是否有效?
bool instr_valid = icode in
{ INOP, IHALT, IRRMOVQ, IIRMOVQ, IRMMOVQ, IMRMOVQ, IOPQ, IJXX, ICALL, IRET, IPUSHQ, IPOPQ,IIADDQ };
# Does fetched instruction require a regid byte?
bool need_regids =
icode in { IRRMOVQ, IOPQ, IPUSHQ, IPOPQ, IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ };
# Does fetched instruction require a constant word?
bool need_valC =
icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IJXX, ICALL, IIADDQ };
# 译码和写回,指定读入和写入
## What register should be used as the B source?
word srcB = [
icode in { IOPQ, IRMMOVQ, IMRMOVQ ,IIADDQ } : rB;
icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
1 : RNONE; # Don't need register
];
## What register should be used as the E destination?
word dstE = [
icode in { IRRMOVQ } && Cnd : rB;
icode in { IIRMOVQ, IOPQ,IIADDQ } : rB;
icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
1 : RNONE; # Don't write any register
];
# 执行
## Select input A to ALU
word aluA = [
icode in { IRRMOVQ, IOPQ } : valA;
icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ } : valC;
icode in { ICALL, IPUSHQ } : -8;
icode in { IRET, IPOPQ } : 8;
# Other instructions don't need ALU
];
## Select input B to ALU
word aluB = [
icode in { IRMMOVQ, IMRMOVQ, IOPQ, ICALL, IPUSHQ, IRET, IPOPQ, IIADDQ } : valB;
icode in { IRRMOVQ, IIRMOVQ } : 0;
# Other instructions don't need ALU
];
## Should the condition codes be updated?
bool set_cc = icode in { IOPQ, IIADDQ };
完成后 make
,注意如果无法使用 gui 界面修改 Makefile
,注释掉相关内容,使用官方文档所给的命令 unix> ./ssim -t ../y86-code/asumi.yo
在命令行下测试得到 ISA Check Succeeds
,再用 unix> (cd ../y86-code; make testssim)
和 unix> (cd ../ptest; make SIM=../seq/ssim)
测试均成功。
这部分工作目录在 sim/pipe
下,题目给定了 ncopy 函数的 C 代码,这个函数和 Part A 的第三题差不多,将 src 数组复制到 dest 数组,并返回数组中的正数的总数。题目还给定了这个函数的 Y86-64 代码,并在文件 pipe-full.hcl
中实现了一个包含 IIADDQ
常量的 PIPE 。题目要求修改 ncopy.ys和 pipe-full.hcl ,使得 ncopy.ys
运行得尽可能快。
这题的测试命令比较多,按需要参考官方文档:
使用 unix> make VERSION=full
重建测试环境;
然后使用 unix> ./psim -t sdriver.yo
和 unix> ./psim -t ldriver.yo
在命令行下模拟运行和测试 PIPE 是否正确,同时得到 CPI ;
使用 unix> ./correctness.pl
测试 ncopy.ys
代码是否正确;
使用 unix> ./benchmark.pl
自动测试得到平均 CPE 。
最初用给定的代码测试得到 CPI ( cycles per instruction ) 为 1.14 ,平均 CPE 约为 15.18 。这里要拿满分平均 CPE 应在 7.5 以下。
首先和 Part B 一样实现 iaddq
指令,此时平均 CPE 降为 13.70 ,将减法改为 iaddq
得到 CPE 为 12.70 由于 iaddq
会更新状态码,可以利用它减少一个比较指令,得到 CPE 为 11.70 。
考虑将循环展开,这里展开八次,CPE 降为 8.81(这里有个问题,使用 %r15 寄存器会出现错误,不知道什么原因),最终优化的函数保存在 ncopy.ys
中,通过了所有的测试,得分为 33.9/60.0 。得分有点低,说明还有很大的优化空间,进一步优化的尝试参见附[1]。
这个 lab 难度不大,但是上手不易,开始看到文档内容那么多,一度想放弃。上手要看懂文档,准备好安装环境,还要熟悉很多测试命令,可以参考他人经验快速上手。熟悉后,题目和之前几个 lab 相比就简单多了,这里难题主要在 Part C ,Part C 还很开放,要得高分不容易,还很耗时间。
通过这个 lab ,可以熟悉 Y86-64 指令集,对 SEQ 和 PIPE 的实现方式也能有更深的理解。
[1]可以很容易看出,循环展开之后的余项可能有八位之多,对性能造成了较大的影响,可以进一步将余项循环展开四次。这里我写了一个四次展开的版本,保存在 4ncopy.ys 中,也保存了一份八次展开的版本在 8ncopy.ys 中,这两个版本都通过了测试,将八次展开和四次展开综合起来,就得到一个优化版本,保存在 mncopy.ys 中,但是这个版本只能部分通过测试,这个 bug 太玄学了。。经过近两小时的挣扎,我最终放弃治疗,有时间再看吧。