Part Ⅲ 方法与技巧
1. 代码生成编写技巧
总结这次编写C――程序中的编写方式,可以得到写的过程中按照先由上到小,再由下到上的方式;
例如:要翻译能够处理语句if(2 > 1){}此语句,我们要用到的文法规则与程序分析过程的图示如下:
2. 空间分配技巧
对于推导式的左边一般先分配空间,分配如下:
$$ = ( expr * ) malloc( sizeof( expr ) );
通常来说,由下一级传上的串如果直接向上一级继续传输,则不需要再分配空间,直接可以使用如$$->code = $1->code;进行赋值;
但是如果在由下一级传上的串需要与新的串合并时,一般我们都重新分配空间,然后进行字符串的拼接,如:
$$ = ( expr * ) malloc( sizeof( expr ) );
$$->code = (char *) malloc( strlen( $1->code ) +需要增加的字符串的长度 );
调用sprintf($$->code, “”,”” );进行字符串拼接;
3. 调试程序技巧
程序的调试过程中发现,大多数的错误都是由于地址空间的分配造成的,而我们采用的检测方法就是在同一个块的翻译的开头进行信息打印检测,再块的结束再采用一次信息打印检测;
例如:
printf( "Level %d: Line %d: program_start\n", currentLevel, lineno );
//开头信息,打印当前处理到的层次,当前处理的行号,以及信息;
/*其他代码*/
printf( "Level %d: Line %d: program_end: %s %s\n", currentLevel, lineno, yytext, $$-code );
//结尾信息,打印层次、行号、信息、当前处理字符之外,再打印出当前推导式左边产生的所有代码;
调试信息图示如下所示:
Part Ⅳ 问题与解决方案
1. 文法引起的移动规约冲突问题
fun-declaration=>type-specifer calling-convention ID ( params ) compound-stmt|
type-specifer calling-convention ID ( params ) ;
calling-convention=> __cdecl | __stdcall | ε
此处将会产生一个移动规约冲突
解决方案:
fun-declaration=>type-specifer calling-convention ID ( params ) compound-stmt|
type-specifer calling-convention ID ( params ) |
type-specifer ID ( params ) compound-stmt |
type-specifer calling-convention ID ( params );
calling-convention=> __cdecl | __stdcall
消除ξ以此消除移动规约冲突
if-stmt=>if ( expression ) statement | if ( expression ) statement else statement
此处也将产生移动规约冲突,但是yacc默认移动,所以可以消除这个问题。
2. 词法中插入符号引发的问题
当我们在处理时,遇到ID如果符号表中没有符号,就将结点插入(我们原本进行的操作),原本的词法分析器的书写如下:
{id} {if( yylval.symType = lookup( yytext, currentTable ) == NULL )
yylval.symType = install(yytext, ¤tTable, currentLevel, ID); return ID;}
这样会引发一个问题,当我们要检验一个变量是否重复定义,比如int a;在词法分析中,扫描到a,然后检测符号表中没有,所以插入符号表,当ID返回到语法分析器中时,那么语法分析器如果要检验的话,那么无论如何都会发现a已经在符号表中,无法断定是不是重复定义的符号。
解决方案:
将符号的插入转移到语法分析程序中,同时如结构体说明的文档里边说的makeSym()函数来建立一个临时的符号结构,在词法中返回给语法分析,然后语法分析中采用如下的程序段进行检测;
if( lookup( $2->name, currentTable ) == NULL )
{
$2 = install($2->name, ¤tTable, currentLevel, ID);//插入符号表
}
else
{
char msg[100];
sprintf( msg, "Variable %s redefinition: ", $2->name );//打印出错信息,说明变量重复定义;
yyerror( msg );
}
这样就可以正确地检测ID的重复定义问题,而且,在语法分析中插表,还能检验比如函数调用是否函数声明的函数等等,可以提供更大的灵活性;
3. sizeof和strlen引发的问题;
对于结点分配空间的时候,空间的大小应该按照sizeof还是strlen进行分配,如果空间分配不足的话,那么将会发生地址错误,所以要正确区分好这两种取得长度的方式;
给出简单的c测试程序:
void main()
{
char temp[100];
sprintf( temp, "jfkdkslfds" );
printf( "%d\n", sizeof( temp ) );
printf( "%d\n", strlen( temp ) );
}
程序运行的结果时100、10,很明显,sizeof计算的是空间大小,而strlen计算的是串的长度。
解决方案:
我们可以按照以下的原则分配,对于定长的结构分配空间,采用sizeof,对于相对变长的字符串的合并,则采用strlen来计算每一个子串的长度,才能够有效的利用空间。
例如:
$$ = ( expr * ) malloc( sizeof( expr ) );
$$->code = (char *) malloc( strlen( $2->name ) + 13 );
4. 函数调用代码产生引起的问题(__stdcall和__cdecl)
由于c语言的函数有两种调用方式__stdcall和__cdecl两种,这两种函数调用的方式主要的区别在于参数空间释放方式的不同,如下:
__stdcall:由被调用者释放,也就是说,在函数定义的内部进行参数空间的释放,指令为ret x,x指示恢复的栈上的字节数,Intel32中是4的倍数;
__cdecl:由调用者释放,在调用其他的函数的函数中,调用之后如
call fun
add esp, x
将栈指针加x,以释放被参数占用的空间。
实质上,对于这两种调用的方式,一种相当于自动,一种相当于手动。
解决方案
首先,需要记录每个函数的调用方式,所以我们在符号结构体中加入了calltype域,用于记录函数的调用方式,它的值与ID关联,当处理call时,进行符号表查找,找到调用的方式,然后决定是否要在调用之后加入add esp,x,同时,要记录参数的个数。这个可以通过参数的计数得到,如下:
temp = lookup( $1->name, currentTable );
if( temp == NULL )
{
sprintf( a, "The function %s haven't been declared: ", $1->name );
yyerror( a );//打印错误信息
}
else
{
if( temp->calltype == 1 )
{
sprintf( $$->code, "%s\n\tadd esp, %d", $$->code, 4 * callparam );
}
}
同时,对于函数定义上,同样要判断是否应该加入ret x,即释放x应该写入字符串中,处理如下:
if( *$2== 0 && params > 0 )//判断调用方式,和是否有参数
{
$3->calltype = 0;//保存调用方式,此为自动恢复
sprintf( $$->code, "%s PROC%s\n%s\tret %d\n%s ENDP\n", $3->name, $5->code, $7->code, 4*params, $3->name );//4*params同样是要恢复的长度
}
else
{
$3->calltype = 1;
sprintf( $$->code, "%s PROC%s\n%s\tret\n%s ENDP\n", $3->name, $5->code, $7->code, $3->name );
}
5. 字符串常量问题
在Intel32汇编语言中,字符串必须作为全局的,才能够为其他变量赋值,所以,可以通过一个字符串缓冲区进行所有的字符串的保存,在规约到最顶端时,打印在程序的数据段中。
实现如下(varBuffer用于保存字符串):
factor : STRING//原本文法不支持字符串,在这里添加
{
$$ = (expr *) malloc( sizeof( expr ) );
$$->code = (char *) malloc( strlen( $1->name ) + 1 );
sprintf( $$->code, "" );
$$->is_const = 1;
$$->name = (char *) malloc( 20 );
sprintf( $$->name, "_msg%d", msgIndex );
sprintf( varBuffer, "%s\t%s BYTE %s, 0\n", varBuffer, $$->name, $1->name );//产生临时变量的标号,并存入缓冲区varBuffer
sprintf( $$->name, "offset _msg%d", msgIndex );
msgIndex++;
}
后来的改进中,变量采用了相同的处理方法
6. 关于printf和puts函数的处理问题
由于老师所给的文件采用TASM,而我们需要用MASM,尝试后,发现不能和所提供的文件进行链接,尝试通过高级语言接口进行链接,结构失败,最终,通过MASM重新写了一下一个链接库,代码请看光盘中Example\Printf Lib;
7. 关于while的翻译问题
在翻译的过程中,while语句的翻译有一定的难度,其在结构上与if语句有一定类似,在if的翻译中,我们采用的是条件表达式外提的方法,即将条件表达式,先行进行计算,完毕后将计算的结果赋给一个临时变量,这样,我们就可以直接通过.IF条件伪指令进行操作;
但是,在while语句中,它有自己独特的方面,总结起来说,就是while的条件表达式不只进行一次计算,每一次的循环都需要进行计算,这样,表达式外提的方法,便不是很实用;
解决方案:
将while语句进行三地址码的分析,很容易可以得到对应的汇编代码的以下结构,直观上,还是将代码进行外提,但是,外提的代码并不脱离循环体,而是作为循环体的首部每次进行一次计算,然后进行一次if选择,决定退出循环,还是执行循环体;
_label0:
mov edx, I;这是最简单的循环,如果里边有负责表达式,则表达式扩展出的代码应该放在此处进行处理,即循环的起点和.IF之间;
.IF !( edx < 10 )
jmp _label1
.ENDIF
;循环体内容
jmp _label0
_label1:
8. 全局变量数组与局部变量数组问题(实现上没有写,但方法已经想出来了)
由于全局变量数组采用offset进行寻址,而局部变量在栈上分配,寻址不能通过offset操作符,要通过lea指令将地址存入寄存器,这样的话,在使用数组时,如果对不同种类的变量使用了错误的取地址方式,便会引起错误。
解决方案:
由于在每一个ID中存在scope域用于存放变量的作用域,所以在对数组进行操作时,我们查找到变量的同时,可以确定变量处于哪一个层次,如果处于0层,需要使用offset操作符,而如果处于其他层次,需要使用lea指令取值。
9. 关于递归产生的问题
int count( int num )
{
if( num == 1 )
{
return 1;
}
else
{
return num+count( num - 1 );
}
}
在此递归代码中,翻译产生
count PROC, num:DWORD
sub esp, 8
mov edx, num
.IF edx == 1
mov eax, 1
ret
.ELSE
mov eax, num
sub eax, 1
mov [ebp-4], eax
push [ebp-4]
call count
mov eax, num
add eax, eax
mov [ebp-8], eax
mov eax, [ebp-8]
ret
.ENDIF
ret 4
count ENDP
由于对eax不正确覆盖,所以将使递归不正确(这是由于我们当时没有良好的选择寄存器的结果,现在如果要改的话,似乎已经不可能),但是考虑左递归由于赋值顺序的不同,便不会有这样的问题,所以,可以将程序的右递归变为左递归。如下:
return count( num - 1 ) + num;
count PROC, num:DWORD
sub esp, 8
mov edx, num
.IF edx == 1
mov eax, 1
ret
.ELSE
mov eax, num
sub eax, 1
mov [ebp-4], eax
push [ebp-4]
call count
mov eax, eax
add eax, num
mov [ebp-8], eax
mov eax, [ebp-8]
ret
.ENDIF
ret 4
count ENDP
Part Ⅴ 程序仍存在的问题
1. 对于数组和指针的处理的不恰当,数组和指针可以作为左值正确使用,但数组和指针作为右值时,便会发生代码产生的错位,问题是由书写过程中,用于代码保存的串太过于混乱,在有的地方,只能够以牺牲一个功能的同时来获取另一个功能。
2. 对于return语句的问题,return语句在翻译时,无法确定它的调用方式,虽然可以在声明语句中明确的记录它的调用方式,但是,由于分析是自底向上的,所以当规约return 时,我们并不知道方法体的名字是什么,所以无法查符号表,这样的话也就无法确定调用方式,这样的话,给方法的调用造成很大的影响,目前,方法还在考虑…
3. 常量优化没有做,问题原因与1相同;
4. 在递归时,必须在递归函数前声明自身,因为进行了方法的调用错误检测,递归函数调用了自身,需要再次声明;
5. 变量的覆盖问题,有时候寄存器的值被不正确的覆盖,主要由于对寄存器使用规划不好,应该合理安排每个寄存器保存的信息;