y86 Assembler
这是本人第一次在优快云上写博客,如有不足之处,请多多包涵。
简单总结一下的话,这个Lab需要我们将y86汇编码中每一句指令转换成相应的二进制代码,并且最终要求生成一份可执行的.bin文件。
废话不多说,先来解析我们主程序y64asm.c里的结构。
Main函数执行了以下几个比较关键的函数:
- void init();//初始化全局变量指针
- int assembly(FILE* in);//按行读汇编,进行转换
- int relocate();//对引用的标号进行重定位
- int binfile(FILE* out);//生成二进制可执行文件
- void finit();//释放全局变量指针,并且释放列表中存放变量名的指针
1和5的差别就在于,我们存标签标号时,存放名称的指针需要我们自行分配内存,而不必担心回收问题。
接下来我们说明几个重要的结构体:
/* 用于存放指令名称,对应的icode,ifun,和占字节数 */
typedef struct instr {
char *name;//指令名称
int len;//指令字符串长度
byte_t code; /* code for instruction+op */
//icode+ifun
int bytes; /* the size of instr */
//指最终生成的可执行代码有多少个字节
} instr_t;
/* 存放二进制可执行代码 */
typedef struct bin {
int64_t addr;//指令起始地址
byte_t codes[10];//指令具体内容
int bytes;//占字节数
} bin_t;
/* 存放一行汇编
* y64bin为生成的可执行代码
* next指向下一行汇编
*/
typedef struct line {
type_t type; /* TYPE_COMM: no y64bin, TYPE_INS: both y64bin and y64asm */
bin_t y64bin;
char *y64asm;
struct line *next;
} line_t;
/* 在这里我在与同学交流时产生意见分歧,考虑了一下就只介绍我自己的理解
* symbol_t存的是汇编头上的标号,如"Loop:"
* reloc_t存的是指令中被引用的标号,如"jmp Stack"中的"Stack"
*/
typedef struct symbol {
char *name;
int64_t addr;//标号的虚拟地址
struct symbol *next;
} symbol_t;
typedef struct reloc {
bin_t *y64bin;//指向待重定位的二进制代码
char *name;
struct reloc *next;
} reloc_t;
了解了这些结构体之后,我们就可以开始逐步填这些空缺的函数。
assembly()函数及其使用到的函数
- 定义并了解程序中的全局变量。
line_t *line_head = NULL;//行列表头
line_t *line_tail = NULL;//行列表尾
int lineno = 0;//行数
int64_t vmaddr = 0;//虚拟地址,用于标识一行指令的PC
symbol_t *symtab = NULL;//symbol_t的列表
reloc_t *reltab = NULL;//reloc_t的列表
int errno = 0;
char* err_message = NULL;
- parse_t parse_line(line_t* line);
解析一行汇编主要分三步,即处理 标号、指令、注释。
因为注释后面不会跟任何内容,所以先判断一行是否为注释可以加快转换行的速度。
char* ins_ptr = line->y64asm;
SKIP_BLANK(ins_ptr);
if(IS_END(ins_ptr)||IS_COMMENT(ins_ptr)){
line->type = TYPE_COMM;
return TYPE_COMM;
}
如果不是注释,就判断这一行指令有没有标号。
使用parse_label函数来解读标号。
if(parse_label(&ins_ptr, label) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(IS_END(ins_ptr)||IS_COMMENT(ins_ptr)){
line->type = TYPE_INS;
line->y64bin.addr = vmaddr;
line->y64bin.bytes = 0;
return TYPE_INS;
}
解读完标号并且确定这行没有结束也没有注释时,就是我们的重头戏——汇编指令的解读。
instr_t* instr;
if(parse_instr(&ins_ptr, &instr) != PARSE_ERR){
itype_t icode = HIGH(instr->code);
itype_t ifun = LOW(instr->code);
bin_t bcode;/* line->y64bin = bcode; */
regid_t rA = REG_NONE;
regid_t rB = REG_NONE;
byte_t reg_byte = 0;
long valC = 0;
char** symbol = NULL;
char delim = ',';
bcode.addr = vmaddr;
bcode.codes[0] = instr->code;
bcode.bytes = instr->bytes;
/* ...解读指令... */
在找到对应的指令后,我们定义一些变量,并将icode,ifun取出以作备用。
这里先将一些bcode的成员赋值是有用意的,稍后将会解释。
变量具体作用可以参照书上4.1与4.3节关于y86 processor的详解。
根据icode分类指令处理操作(写得有些乱,见谅):
/* ...解读指令... */
switch(icode){
case I_HALT:
case I_NOP:
break;
case I_RRMOVQ:
if(parse_reg(&ins_ptr, &rA) == -1){
errno = 1;
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_delim(&ins_ptr, delim) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_reg(&ins_ptr, &rB) == -1){
errno = 1;
line->type = TYPE_ERR;
return TYPE_ERR;
}
reg_byte = HPACK(rA, rB);
bcode.codes[1] = reg_byte;
break;
case I_IRMOVQ:
valC = (long)&line->y64bin;
if(parse_imm(&ins_ptr, symbol, &valC) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_delim(&ins_ptr, delim) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_reg(&ins_ptr, &rB) == -1){
errno = 1;
line->type = TYPE_ERR;
return TYPE_ERR;
}
reg_byte = HPACK(rA, rB);
bcode.codes[1] = reg_byte;
for(int i = 0; i < 8; ++i)
bcode.codes[i + 2] = (valC >> i*8)&0xff;
break;
case I_RMMOVQ:
if(parse_reg(&ins_ptr, &rA) == -1){
errno = 1;
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_delim(&ins_ptr, delim) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_mem(&ins_ptr, &valC, &rB) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
reg_byte = HPACK(rA, rB);
bcode.codes[1] = reg_byte;
for(int i = 0; i < 8; ++i)
bcode.codes[i + 2] = (valC >> i*8)&0xff;
break;
case I_MRMOVQ:
if(parse_mem(&ins_ptr, &valC, &rB) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_delim(&ins_ptr, delim) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_reg(&ins_ptr, &rA) == -1){
errno = 1;
line->type = TYPE_ERR;
return TYPE_ERR;
}
reg_byte = HPACK(rA, rB);
bcode.codes[1] = reg_byte;
for(int i = 0; i < 8; ++i)
bcode.codes[i + 2] = (valC >> i*8)&0xff;
break;
case I_ALU:
if(parse_reg(&ins_ptr, &rA) == -1){
errno = 1;
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_delim(&ins_ptr, delim) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_reg(&ins_ptr, &rB) == -1){
errno = 1;
line->type = TYPE_ERR;
return TYPE_ERR;
}
reg_byte = HPACK(rA, rB);
bcode.codes[1] = reg_byte;
break;
case I_JMP:
case I_CALL:
valC = (long)&line->y64bin;
if((!IS_LETTER(ins_ptr))&&
(*ins_ptr != '0'||*(ins_ptr + 1) != 'x')){
errno = 6;
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_imm(&ins_ptr, symbol, &valC) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
for(int i = 0; i < 8; ++i)
bcode.codes[i + 1] = (valC >> i*8)&0xff;
break;
case I_RET:
break;
case I_PUSHQ:
case I_POPQ:
if(parse_reg(&ins_ptr, &rA) == -1){
errno = 1;
line->type = TYPE_ERR;
return TYPE_ERR;
}
reg_byte = HPACK(rA, rB);
bcode.codes[1] = reg_byte;
break;
case I_DIRECTIVE:
if(parse_digit(&ins_ptr, &valC) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
switch(ifun){
case D_DATA:
if(IS_LETTER(ins_ptr)){
errno = 8;
valC = (long)&line->y64bin;
parse_imm(&ins_ptr, symbol, &valC);
}
for(int i = 0; i < instr->bytes; ++i)
bcode.codes[i] = (valC >> i*8)&0xff;
break;
case D_POS:
bcode.addr = valC;
vmaddr = valC;
break;
case D_ALIGN:
vmaddr += vmaddr%valC == 0?0:
(vmaddr/valC + 1)*valC - vmaddr;
bcode.addr = vmaddr;
break;
default:
line->type = TYPE_ERR;
return TYPE_ERR;
}
break;
default:
line->type = TYPE_ERR;
return TYPE_ERR;
}
经过这个分类操作,我们可以将一条指令和指令中的对象全部解析到我们事先定义的变量中。具体操作我们稍后来看,先看解读完指令后对line的赋值。
vmaddr += bcode.bytes;
line->type = TYPE_INS;
line->y64bin = bcode;
return TYPE_INS;
}
line->type = TYPE_ERR;
return line->type;
/* End of parse_line */
更新vmaddr,将line的成员赋值并返回。如果开始parse_instr时便出错,则会跳到parse_line函数尾部返回错误。
至此一行的解读全部结束。
- parse_t parse_label(char** ptr, char** name);
一行之中最先识别的内容是一行的标号,通过该函数实现,无论标号有无。
本人不太相信指针的++操作,所以所有指针的前移一位都是+1实现。
parse_t parse_label(char **ptr, char **name)
{
if(ptr == NULL)
return PARSE_ERR;
char* label = *ptr;
int cnt = 0;
/* skip the blank and check */
while(*label != ':'){
++cnt;
label += 1;
if(IS_END(label))
return PARSE_LABEL;
}
/* 如果没有读到':',就说明这一行没有标号,标号解读结束
* 读到':'便停止循环,进行赋值
*/
/* allocate name and copy to it */
char* lname = (char*)malloc(cnt + 1);
strncpy(lname, *ptr, cnt);
*(lname + cnt) = '\0';
if(find_symbol(lname)){
err_message = lname;
errno = 2;
return PARSE_ERR;
}
/* symbol重复则返回错误,errno置2 */
add_symbol(lname);//将标号加入symbol_t列表
free(lname);//lname不再需要,释放
/* set 'ptr' and 'name' */
label += 1;
SKIP_BLANK(label);
*ptr = label;//将行指针前移至下一个非空非'\n'符的字符
return PARSE_LABEL;
}
- parse_t parse_instr(char** ptr, instr_t** inst);
通过查找写好的intruction列表来解读一条行指令的icode,ifun。
具体通过对inst的赋值来实现指令解读传递。
parse_t parse_instr(char **ptr, instr_t **inst)
{
/* find_instr and check end */
if(ptr == NULL)
return PARSE_ERR;
*inst = find_instr(*ptr);
if(*inst != NULL){
/* set 'ptr' and 'inst' */
for(int i = 0; i < (*inst)->len; ++i)
*ptr += 1;
SKIP_BLANK(*ptr);
return PARSE_INSTR;
}
return PARSE_ERR;
}
- parse_t parse_reg(char** ptr, regid_t* regid);
分类操作中使用的最多也是最先使用的函数,解读寄存器。
parse_t parse_reg(char **ptr, regid_t *regid)
{
/* skip the blank and check */
if(ptr == NULL)
return PARSE_ERR;
/* find register */
const reg_t* r = find_register(*ptr);
if(r != NULL){
/* set 'ptr' and 'regid' */
*regid = r->id;
for(int i = 0; i < r->namelen; ++i)
*ptr += 1;
SKIP_BLANK(*ptr);
return PARSE_REG;
}
return PARSE_ERR;
}
- parse_t parse_delim(char** ptr, char delim);
该函数用于跳过单个的分隔符,如’,‘,’('等。
parse_t parse_delim(char **ptr, char delim)
{
/* skip the blank and check */
if(ptr == NULL)
return PARSE_ERR;
if(**ptr == delim){
/* set 'ptr' */
*ptr += 1;
if(IS_BLANK(*ptr)){
SKIP_BLANK(*ptr);
}
return PARSE_DELIM;
}
errno = 3; //delim不正确则返回错误,errno置3
return PARSE_ERR;
}
- parse_t parse_imm(char** ptr, char** name, long* val);
这个函数可以说是所有解读行指令函数中最复杂的一个函数了。
原因是imm的值可以是$…也可以是引用的标号。
如何才能将这两种情况都成功实现解读是一个难题。
实际实现中,如果imm为$…,则交给parse_digit解读
如果为引用标号,则交给parse_symbol解读
parse_t parse_imm(char **ptr, char **name, long *value)
{
/* skip the blank and check */
if(ptr == NULL)
return -1;
/* if IS_IMM, then parse the digit */
if(IS_IMM(*ptr)){
*ptr += 1;
*value = 0;
if((parse_digit(ptr, value) == -1)||(**ptr != ',')){
errno = 4;//imm格式不正确则返回错误,errno置4
return -1;
}
return PARSE_DIGIT;
}
/* if IS_LETTER, then parse the symbol */
if(IS_LETTER(*ptr)){
name = (char**)*value;
*value = 0;
return parse_symbol(ptr, name);
}
return PARSE_ERR;
}
/* --------------------switch(icode):case I_IRMOVQ----------------------- */
case I_IRMOVQ:
valC = (long)&line->y64bin;
/* 将这一行的line中成员y64bin地址赋给valC
* 如果imm是引用标号,则使用改地址来添加reloc_t
*/
if(parse_imm(&ins_ptr, symbol, &valC) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_delim(&ins_ptr, delim) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
if(parse_reg(&ins_ptr, &rB) == -1){
errno = 1;
line->type = TYPE_ERR;
return TYPE_ERR;
}
reg_byte = HPACK(rA, rB);
bcode.codes[1] = reg_byte;
for(int i = 0; i < 8; ++i)
bcode.codes[i + 2] = (valC >> i*8)&0xff;
/* 将valC按little endian逐个字节写入bcode.codes[]
* 如果是待重定位标号,valC = 0
*/
- parse_t parse_digit(char** ptr, long* value);
该函数解读一个数字,10进制或16进制。
parse_t parse_digit(char **ptr, long *value)
{
/* skip the blank and check */
if(ptr == NULL)
return PARSE_ERR;
/* calculate the digit, (NOTE: see strtoll()) */
bool_t neg = FALSE;
unsigned long val;
if(**ptr == '-'){
neg = TRUE;
*ptr += 1;
}
/* 判断是否为负数 */
int base = 10;
if(**ptr == '0'&&*(*ptr + 1) == 'x'){
*ptr += 2;
base = 16;
}
/* 判断进制 */
val = strtoll(*ptr, NULL, base);
*value = val;
if(neg)
*value *= -1;
/* set 'ptr' and 'value' */
while(IS_DIGIT(*ptr)
||(**ptr >= 'a'&&**ptr <= 'f'&&base == 16)
||(**ptr >= 'A'&&**ptr <= 'F'&&base == 16)){
*ptr += 1;
}
SKIP_BLANK(*ptr);
/* 由于使用parse_digit的函数很多,故不在该函数内作错误判断 */
return PARSE_DIGIT;
}
科普一下,关于strtoll函数有一个限制,即返回最大值为有符号长整型最大值,无法转换超出0x7fffffffffffffff的字符串到数值。
所以对于超出该值的imm,我们需要自行转换。方法很多,可以转换一位在将剩下的数字用strtoll函数转换,也可以直接将整个数值字符串解读。
我用的方法如下:
if(val == 0x7fffffffffffffff){//signed long overflow
char* newp = *ptr;
byte_t low4bit = 0;
for(int i = 0; i < 16; ++i){
low4bit = 0;
if(*newp >= '0'&&*newp <= '9'){
low4bit = *newp - 48;
}else if(*newp >= 'A'&&*newp <= 'F'){
low4bit = *newp - 55;
}else if(*newp >= 'a'&&*newp <= 'f'){
low4bit = *newp - 87;
}
//printf("%x", low4bit);
val = (val<<4)+low4bit;
newp += 1;
}
}
函数还有bug,无法检测非16进制时的字符串含有字母的情况,仅供通过测试。
- parse_t parse_symbol(char** ptr, char** name);
该函数用于解读引用标号,将引用标号加入重定位列表。
parse_t parse_symbol(char **ptr, char **name)
{
/* allocate name and copy to it */
char* symbol = *ptr;
int cnt = 0;
while(!IS_BLANK(symbol) && !IS_END(symbol)
&& (*symbol != ',') && !IS_COMMENT(symbol)){
symbol += 1;
cnt++;
}
char* sname = (char*)malloc(cnt + 1);
strncpy(sname, *ptr, cnt);
*(sname + cnt) = '\0';
bin_t* bin = (bin_t*)name;
add_reloc(sname, bin);//将引用标号加入reloc_t列表
free(sname);//sname不再需要,释放
/* set 'ptr' and 'name' */
SKIP_BLANK(symbol);
*ptr = symbol;
return PARSE_SYMBOL;
}
- parse_t parse_mem(char** ptr, long* value, regid* regid);
该函数用于解读地址,仅用于icode = I_MRMOVQ或I_RMMOVQ的情况
parse_t parse_mem(char **ptr, long *value, regid_t *regid)
{
/* skip the blank and check */
if(ptr == NULL)
return PARSE_ERR;
/* calculate the digit and register, (ex: (%rbp) or 8(%rbp)) */
if(IS_DIGIT(*ptr))
parse_digit(ptr, value);
if(parse_delim(ptr, '(') == -1){
errno = 5;//'('不存在,返回错误,errno置5
return PARSE_ERR;
}
parse_reg(ptr, regid);
if(parse_delim(ptr, ')') == -1){
errno = 5;//')'不存在,返回错误,errno置5
return PARSE_ERR;
}
/* set 'ptr', 'value' and 'regid' */
SKIP_BLANK(*ptr);
return PARSE_MEM;
}
- parse_t parse_data(char** ptr, long* value);
这个函数我没有使用,而是直接在操作分类里写了.data,.pos,.align的情况
case I_DIRECTIVE:
if(parse_digit(&ins_ptr, &valC) == -1){
line->type = TYPE_ERR;
return TYPE_ERR;
}
switch(ifun){
case D_DATA:
if(IS_LETTER(ins_ptr)){
errno = 8;//I_DIRECTIVE类指令,errno置8
valC = (long)&line->y64bin;
parse_imm(&ins_ptr, symbol, &valC);
}
for(int i = 0; i < instr->bytes; ++i)
bcode.codes[i] = (valC >> i*8)&0xff;
break;
case D_POS:
bcode.addr = valC;
vmaddr = valC;
/* 虚拟内存地址更新 */
break;
case D_ALIGN:
vmaddr += vmaddr%valC == 0?0:
(vmaddr/valC + 1)*valC - vmaddr;
bcode.addr = vmaddr;
/* 判断并更新虚拟内存地址 */
break;
default:
line->type = TYPE_ERR;
return TYPE_ERR;
}
break;
之前说明的用意便在于此,因为I_DIRECTIVE指令需要对vmaddr和bcode中的addr进行操作,所以先将bcode赋值就可以基于原有的值进行微调,例如.align只需要加上偏移量便可以完成vmaddr的更新。
- 错误情况处理
根据errno判断错误类型并用err_print输出
if (parse_line(line) == TYPE_ERR) {
if(errno == 1){
err_print("Invalid REG");
}else if(errno == 2){
err_print("Dup symbol:%s", err_message);
free(err_message);
}else if(errno == 3){
err_print("Invalid ','");
}else if(errno == 4){
err_print("Invalid Immediate");
}else if(errno == 5){
err_print("Invalid MEM");
}else if(errno == 6){
err_print("Invalid DEST");
}
return -1;
}
至此,汇编文件解读完毕。我们得到的是一个line_t的列表,一个symbol_t的列表和一个reloc_t的列表。用于后面的可执行文件生成工作。
relocate()函数及其使用到的函数
- int relocate();
该函数用于将引用标号进行重定位,使这些指令中的8字节地址获得正确的数值。
int relocate(void)
{
reloc_t *rtmp = NULL;
rtmp = reltab->next;
while (rtmp) {
/* find symbol */
symbol_t* symbol = find_symbol(rtmp->name);
/* 查找同名标号,如果找不到,返回错误 */
if(symbol != NULL){
int offset = 0;
switch(HIGH(rtmp->y64bin->codes[0])){
case I_IRMOVQ:
offset = 2;
break;
case I_JMP:
case I_CALL:
offset = 1;
break;
default:
offset = 0;//I_DIRECTIVE
}
/* relocate y64bin according itype */
for(int i = 0; i < 8; ++i)
rtmp->y64bin->codes[i + offset] = (symbol->addr >> i*8)&0xff;
}else{
err_print("Unknown symbol:'%s'", rtmp->name);
return -1;
}
/* next */
rtmp = rtmp->next;
}
return 0;
}
- symbol_t* find_symbol(char* name)
用于寻找存储在symbol_t列表中标号。
symbol_t *find_symbol(char *name)
{
symbol_t* srmp = NULL;
srmp = symtab->next;
while(srmp){
if(strcmp(srmp->name, name) == 0)
return srmp;
srmp = srmp->next;
}
return NULL;
}
- 标号和引用标号的存储
添加一个symbol_t或reloc_t的方式与查找的方式有很大关系。
由于relocate函数中遍历reloc_tab的方式已经确定,我们添加标号的方式也要顺应查找的方式。
int add_symbol(char *name)
{
/* create new symbol_t (don't forget to free it)*/
symbol_t* symbol = (symbol_t*)malloc(sizeof(symbol_t));
symbol->name = (char*)malloc(MAX_INSLEN);
strcpy(symbol->name, name);
symbol->addr = vmaddr;
/* add the new symbol_t to symbol table */
symbol->next = symtab->next;
symtab->next = symbol;
return 0;
}
void add_reloc(char *name, bin_t *bin)
{
/* create new reloc_t (don't forget to free it)*/
reloc_t* reloc = (reloc_t*)malloc(sizeof(reloc_t));
reloc->name = (char*)malloc(MAX_INSLEN);
/* add the new reloc_t to relocation table */
reloc->y64bin = bin;
strcpy(reloc->name, name);
reloc->next = reltab->next;
reltab->next = reloc;
}
binfile()函数
生成可执行代码文件的过程中一样有坑,就是虚拟地址跳转的问题,由于.pos和.align的使用,使得指令的起始地址之间可能会有空缺的字节,这些就需要我,我们自行补0。
int binfile(FILE *out)
{
/* prepare image with y64 binary code */
line_t* ltmp = NULL;
ltmp = line_head->next;
long addr = 0;
char ch[MAX_INSLEN];
for(int i = 0; i < MAX_INSLEN; ++i)
ch[i] = 0;
while(ltmp){
int bytes = ltmp->y64bin.bytes;
if(bytes != 0){
if(ltmp->y64bin.addr > addr){
if(fwrite(ch, 1, ltmp->y64bin.addr - addr, out)
!= ltmp->y64bin.addr - addr)
return -1;
}
if(ltmp->type == TYPE_INS){
/* binary write y64 code to output file (NOTE: see fwrite()) */
if(fwrite(ltmp->y64bin.codes, 1, bytes, out) != bytes)
return -1;
}
addr = ltmp->y64bin.addr + ltmp->y64bin.bytes;
}
ltmp = ltmp->next;
}
return 0;
}
说一下生成可执行代码的思路。
如果y64bin的bytes不为0,检查起始地址。
如果起始地址与上一条指令起始地址加上上一条指令bytes之和不一致,按地址之差补0,再按bytes将codes[]写入文件。
如果y64bin的bytes为0,则跳过该条指令。