[lab]csapp-archlab
archlab
该lab 要求我们在自制指令集 Y86-64 上进行编码, 并且提供一个简单的汇编器和模拟器实现.
由于是虚拟环境, 我们解压sim文件夹后要make 构建各个目标文件, 子目录如下
sim
- misc # 包含了 指令集(isa)/汇编器(yas)/模拟器(yis)
- seq # 顺序执行模式 hcl 的实现
- pipe # 流水线执行模式 hcl 的实现
- ptest # 测试脚本
- y86-code # Y86-64 指令集的示例代码
PartA
要求将 example.c 中的三个函数用汇编实现, 要求实现带有main函数和设置栈顶, 我们首先从 y86-code 里拷贝出一份作为模版, 修改函数和数据即可.
先看c语言源码
/* linked list element */ typedef struct ELE {
long val;
struct ELE *next;
} *list_ptr;
/* sum_list - Sum the elements of a linked list */
long sum_list(list_ptr ls)
{
long val = 0;
while (ls) {
val += ls->val;
ls = ls->next;
}
return val;
}
/* rsum_list - Recursive version of sum_list */
long rsum_list(list_ptr ls)
{
if (!ls)
return 0;
else {
long val = ls->val;
long rest = rsum_list(ls->next);
return val + rest;
}
}
/* copy_block - Copy src to dest and return xor checksum of src */
long copy_block(long *src, long *dest, long len)
{
long result = 0;
while (len > 0) {
long val = *src++;
*dest++ = val;
result ˆ= val;
len--;
}
return result;
}
sum_list 顺序遍历链表求和, rsum_list 递归遍历链表求和, copy_block 拷贝数组, 并计算异或和.
经过前面几个lab的洗礼, 还是很简单的, 大概从书上查阅一下(图4-2,4-3)支持的指令和意义就可以
# long sum_list(list_ptr ele)
# ele in %rdi
sum_list:
irmovq $8, %r8 # Constant 8
xorq %rax,%rax # sum = 0
andq %rdi,%rdi # Set condition codes
jmp test
loop:
mrmovq (%rdi),%r9 # tmp = val
addq %r9,%rax # sum += tmp
addq %r8,%rdi # ele = ele++
mrmovq (%rdi), %rdi # ele = [ele]
andq %rdi,%rdi # Set condition codes
test:
jne loop # Stop when 0
ret
# long rsum_list(list_ptr ele)
# ele in %rdi
rsum_list:
irmovq $8, %r8 # Constant 8
xorq %rax,%rax # sum = 0
andq %rdi,%rdi # Set condition codes
test:
je end # Stop when 0
pushq %rdi # save ele
addq %r8,%rdi # ele = ele++
mrmovq (%rdi), %rdi # ele = [ele]
call rsum_list # rsum_list(ele)
popq %rdi # restore ele
mrmovq (%rdi),%r9 # tmp = val
addq %r9,%rax # sum += tmp
end:
ret
# long copy_block(long *src, long* dest, long len)
# ele in %rdi
copy_block:
irmovq $8, %r8 # Constant 8
irmovq $1, %r10 # Constant 1
xorq %rax,%rax # result = 0
andq %rdx,%rdx # Set condition codes
jmp test
loop:
mrmovq (%rdi),%r9 # tmp = *src
addq %r8,%rdi # src++
xorq %r9,%rax # sum ^= tmp
rmmovq %r9,(%rsi) # *dest = tmp
addq %r8,%rsi # dest++
subq %r10,%rdx # len--
test:
jne loop # Stop when 0
ret
PartB
在顺序流水线中实现 iaddq, 这个指令已经在isa.c/seq-full.hcl 有定义了, 我们只需要实现一遍 iaddq 在每个指令步骤的逻辑(图4-18)即可.
## iaddq V, rB
##
## Fetch Stage
## icode: ifunc <- m_1[PC]
## rA:rB <- m_1[PC+1]
## valC <- m_8[PC+2]
## valP <- PC + 10
## Decode Stage
## valB <- R[rB]
## Execute Stage
## valE <- valB + valC
## Memory Stage
## R[rB] <- valE
## Program Counter Update
## PC <- valP
在修改文件时可以对照IIRMOVQ
指令, 因为IIADDQ
与其十分相似, 只需要改动 aluB 和添加 set_cc 即可, alufun 因为已经默认时 ALUADD, 所以我们不需要改变它.
之后我们可以使用编译出的ssim(顺序执行指令模拟器)来测试我们的实现是否正确.
PartC
要对一个c函数对应的指令代码在流水线执行环境下手动优化.
流水线环境就是4.4中的内容, 我们还需要再实现一遍 iaddq
, pipe 版多了状态传递的内容, 不过我们实现的思路可以照抄 seq 版.
然后我们来看需要优化的函数和他的初始实现
/** ncopy - copy src to dst, returning number of positive ints
* contained in src array.
*/
word_t ncopy(word_t *src, word_t *dst, word_t len)
{
word_t count = 0;
word_t val;
while (len > 0) {
val = *src++;
*dst++ = val;
if (val > 0)
count++;
len--;
}
return count;
}
# You can modify this portion
# Loop header
xorq %rax,%rax # count = 0;
andq %rdx,%rdx # len <= 0?
jle Done # if so, goto Done:
Loop: mrmovq (%rdi), %r10 # read val from src...
rmmovq %r10, (%rsi) # ...and store it to dst
andq %r10, %r10 # val <= 0?
jle Npos # if so, goto Npos:
irmovq $1, %r10
addq %r10, %rax # count++
Npos: irmovq $1, %r10
subq %r10, %rdx # len--
irmovq $8, %r10
addq %r10, %rdi # src++
addq %r10, %rsi # dst++
andq %rdx,%rdx # len > 0?
jg Loop # if so, goto Loop:
首先, 因为addq需要取寄存器两次, 我们将addq全部替换成iaddq.
在修改之后使用 make VERSION=full
编译, 然后使用 psim(并行执行指令模拟器)跑样例 sdriver.yo
ldriver.yo
来测试我们的实现是否正确.
然后我们跑脚本 correctness.pl
来过大样例, 跑 benchmark.pl
来评分, 此时的分数应该还是0分, 还需要使用循环展开(5.8)和指令重排(5.9)来加速.
循环展开主要是并行的读取相邻元素, 降低循环的次数, 从而减少了循环变量计算和比较的次数.
指令重拍也是为了提升指令并行执行的数量, 比如我们想将rdi
的内容拷贝到rsi
中, 很自然的可以写 mrmovq (%rdi), %r10
和 rmmovq %r10, (%rsi)
, 但这两条指令是有依赖的, 后面需要上一条指令将%10写入后才能开始. 我们在循环展开的条件下, 可以在中间再插入一条读取指令 mrmovq 8(%rdi), %11
有效避免了依赖造成的时钟浪费.
尽管有了思路, 但也不一定能写得出最优答案 , 在参照了blog后, 得到的代码如下
# You can modify this portion
# Loop header
xorq %rax,%rax # count = 0;
iaddq $-3, %rdx
jle BeforeTail # len <= 0? if so, goto Tail:
Extended4Loop:
mrmovq (%rdi), %r9 # read val1 from src...
mrmovq 8(%rdi), %r10 # read val2 from src...
mrmovq 16(%rdi), %r11 # read val3 from src...
mrmovq 24(%rdi), %r12 # read val4 from src...
rmmovq %r9, (%rsi) # store val to dst
rmmovq %r10, 8(%rsi) # store val to dst
rmmovq %r11, 16(%rsi) # store val to dst
rmmovq %r12, 24(%rsi) # store val to dst
andq %r9, %r9 # val1 <= 0?
jle Npos2 # if so, goto Npos:
iaddq $1, %rax # count++
Npos2:
andq %r10, %r10 # val2 <= 0?
jle Npos3 # if so, goto Npos:
iaddq $1, %rax # count++
Npos3:
andq %r11, %r11 # val3 <= 0?
jle Npos4 # if so, goto Npos:
iaddq $1, %rax # count++
Npos4:
andq %r12, %r12 # val4 <= 0?
jle Npos5 # if so, goto Npos:
iaddq $1, %rax # count++
Npos5:
iaddq $32, %rdi # src+=4
iaddq $32, %rsi # dst+=4
iaddq $-4, %rdx # len-=4
jg Extended4Loop
BeforeTail:
iaddq $3, %rdx
jle Done # if so, goto Done:
mrmovq (%rdi), %r9 # read val from src...
mrmovq 8(%rdi), %r10 # read val from src...
rmmovq %r9, (%rsi) # store val to dst
andq %r9, %r9 # val <= 0?
jle Npos6 # if so, goto Npos:
iaddq $1, %rax # count++
Npos6:
iaddq $-1, %rdx # len--
jle Done # if so, goto Done:
rmmovq %r10, 8(%rsi) # store val to dst
andq %r10, %r10 # val <= 0?
jle Npos7 # if so, goto Npos:
iaddq $1, %rax # count++
Npos7:
iaddq $-1, %rdx # len--
jle Done # if so, goto Done:
mrmovq 16(%rdi), %r11 # read val from src...
rmmovq %r11, 16(%rsi) # store val to dst
andq %r11, %r11 # val <= 0?
jle Done # if so, goto Npos:
iaddq $1, %rax # count++
结果和博客一样都是48.6, 自己在实现的过程中忽略了movq可以变址读取, 没有利用到循环展开的优势, 且计算循环展开的剩余部分比较复杂, 直接-3, 再加3, 这样可以保证进入结束部分时len就是原来 len 对4的余数, 能比直接计算余数减少2-3个指令.
What‘s more
这个lab提供的文件给出了一个完成的指令集设计和实现, 也是非常好的学习示例.
我们来看PartA, 利用Lex&Yacc
对yas,hcl2c
相关的部分和实现.
Lex&Yacc 是一组生成软件, 它接受自定义的语法规则, 生成对应规则的解析器. Flex&Bison 是Linux环境中对他们的一组实现, 更多知识可以参考这个, 简单来说 Lex就是分词器, Yacc则接受分词, 根据自定义的语法规则生成解析代码(就是我们的编译器).
我们来列举下misc中需要关注的文件
Makefile # 编译 target 的命令
isa.h/c
yas.h/c
yas-grammar.lex # 用于生成yas
yis.h/c # yis
node.h/c # 编译节点定义
outgen.h/c # 打印输出的代码
hcl.lex/y # hcl
isa 里有
- 寄存器定义
- 指令编码
- 指令描述表
- 内存操作
- 寄存器操作
- ALU操作 : 计算/更新和获取状态字
- 指令状态
stat_t
- 虚拟机状态:
state_rec
, 指令跳转, 指令执行
首先是 yas, 它是Y86的汇编器, 负责从汇编代码到机器指令(ys->yo)这一步, 依赖了yas-grammar , isa
yas-grammar.o: yas-grammar.c
$(CC) $(LCFLAGS) -c yas-grammar.c
yas-grammar.c: yas-grammar.lex
$(LEX) yas-grammar.lex
mv lex.yy.c yas-grammar.c
isa.o: isa.c isa.h
$(CC) $(CFLAGS) -c isa.c
yas.o: yas.c yas.h isa.h
$(CC) $(CFLAGS) -c yas.c
yas: yas.o yas-grammar.o isa.o
$(CC) $(CFLAGS) yas-grammar.o yas.o isa.o ${LEXLIB} -o yas
yas.h 内容如下,
void save_line(char *);
void finish_line();
void add_reg(char *);
void add_ident(char *);
void add_instr(char *);
void add_punct(char);
void add_num(long long);
void fail(char *msg);
unsigned long long atollh(const char *);
/* Current line number */
int lineno;
yas-grammar.lex 内容如下,
/* Grammar for Y86-64 Assembler */
#include "yas.h"
Instr rrmovq|cmovle|cmovl|cmove|cmovne|cmovge|cmovg|rmmovq|mrmovq|irmovq|addq|subq|andq|xorq|jmp|jle|jl|je|jne|jge|jg|call|ret|pushq|popq|"."byte|"."word|"."long|"."quad|"."pos|"."align|halt|nop|iaddq
Letter [a-zA-Z]
Digit [0-9]
Ident {Letter}({Letter}|{Digit}|_)*
Hex [0-9a-fA-F]
Blank [ \t]
Newline [\n\r]
Return [\r]
Char [^\n\r]
Reg %rax|%rcx|%rdx|%rbx|%rsi|%rdi|%rsp|%rbp|%r8|%r9|%r10|%r11|%r12|%r13|%r14
%x ERR COM
%%
^{Char}*{Return}*{Newline} { save_line(yytext); REJECT;} /* Snarf input line */
#{Char}*{Return}*{Newline} {finish_line(); lineno++;}
"//"{Char}*{Return}*{Newline} {finish_line(); lineno++;}
"/*"{Char}*{Return}*{Newline} {finish_line(); lineno++;}
{Blank}*{Return}*{Newline} {finish_line(); lineno++;}
{Blank}+ ;
"$"+ ;
{Instr} add_instr(yytext);
{Reg} add_reg(yytext);
[-]?{Digit}+ add_num(atoll(yytext));
"0"[xX]{Hex}+ add_num(atollh(yytext));
[():,] add_punct(*yytext);
{Ident} add_ident(yytext);
{Char} {; BEGIN ERR;}
<ERR>{Char}*{Newline} {fail("Invalid line"); lineno++; BEGIN 0;}
%%
unsigned int atoh(const char *s)
{
return(strtoul(s, NULL, 16));
}
我们可以看出yas的编译逻辑是按行编译, 解析出token就将他们加入到当前行中, 当前行结束时调用finish_line 产生一条指令.
hcl略微复杂, 它通过.lex文件先生成token流, 但是交给 .y 文件定义的语法规则来处理, 从而能处理更复杂的hcl语法, 将他们生成c/Verilog文件.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现