BIT 2024 编译原理 Lab. 3 三代编译器实验说明和要求

实验三:三代编译器实验

一、实验要求

详细实验要求请参考文件《Lab3实验说明和要求.pdf》。

二、文件

共包括五个文件:

  • main_without_arg.cpp

    是可以在本地直接编译使用的源文件,是用于测试输入输出结果的文件。

    注意每次输入结束后,需要另起一行,并在该行输入+++(3个加号)才能终止输入并显示输出。

  • main.cpp

    是根据实验平台提交要求修改了输入输出方式的文件,是最终提交的文件之一

  • F.h

    用于声明函数,是最终提交的文件之二

  • F.cpp

    用于具体定义 F.h 中的函数,是最终提交的文件之三

  • CMakeLists.txt

    用于组装文件,是最终提交的文件之四

三、实验思路

1、大致流程

以下仅介绍我的流程,据我所知,我身边的人做法就各不相同,所以做法其实是很多的。

  1. 词法分析:预处理数据,对输入分词,把句子变成一系列的单词,这样就转化为了好处理的形式。我在①tokenization 函数中做了这个工作。
  2. 语法分析:分析成分,识别每个单词的词性。
  3. 划分函数:由于 lab3 开始出现自定义函数,所以我们需要能够②识别出有几个函数、每个函数名是什么、函数从哪里开始又到哪里结束,函数的返回值类型
  4. 划分句子:在每个函数的内部存在许多语句,我将其根据分号划分为一个个句子。
  5. 处理句子:③逐行识别句子的含义,并给出对应的汇编语句。

2、稍微精细一点的介绍

2.1 ① 分词
  • 作用:对文本进行分词

  • 接受的输入:待处理的字符串

  • 返回值:一个处理后的字符串

  • 该函数的实现原理是:扫描两遍字符串。第一次扫描,只要遇到了符号(除了 _),⑤就在符号的前后各添加一个空格;第二次扫描,使用字符流保存单词。使用字符流的好处是可以自动去除空格。

  • 下面是一个实现第一次扫描的参考代码:

    string tokenization(string text1){
        string text2 = "";
        for (char i: text){
            if (不是字母、数字或下划线) {
                text2 += (" "+i+" ");
            }
            else text2 += i;
        }
        return text2;
    }
    
  • 下面是一个实现第二次扫描的参考代码:

    stringstream sstream;
    sstream << text2;
    string stemp = "";
    vector<string> text3;
    while (sstream >> stemp) {
        text3.push_back(s0);
    }
    
  • 例如:
    text1 = "int main(){int a;a=1;return 0;}"
    经过上述处理后:
    text2 = "int main (  )  { int a ; a = 1 ; return 0 ;  } "
    text3 = ["int", "main", "(", ")", "{", "}", "int", "a", ";", "a", "=", "1", ";", "return", "0", ";", "}"]
    注意 text1 和 text2 是字符串, 而 text3 是一个数组, 它的每一项都是一个单词.
    
  • 注:该分词方法是 lab2 的文章发布后有一个同学与我讨论了这个方法,我觉得很好用,这次我就使用了这种。

2.2 ② 识别函数体
  • 识别出有几个函数:大括号有几对,就有几个函数
  • 函数从哪里开始又到哪里结束:按右大括号划分
  • 每个函数名是什么:程序的开头的第二个单词,或者右大括号往后的第二个单词,这些都是函数名
  • 函数的返回值类型:返回值可能是 void 或者 int,注意到这两种情况对应的汇编输出略有不同
  • 顺带一提,你完全不需要把 main 函数和其他函数分开处理,它们并没有任何不同
2.3 ③ 逐行识别句子的含义
  • 这是最难编写的部分,大概也是最重要的部分
  • 在定义函数时:
    • 可能带有传入参数
  • 在处理函数内语句时,最简单的有:
    • 遇到了 int,说明要定义局部变量,可能带有初始化,该初始化是一个④表达式
    • 遇到了 println_int,说明要 call printf,括号里的是一个④表达式
    • 遇到了 return,准备 leave\nret\n,对于 void 函数没有返回值;对于 int 函数的返回值是一个④表达式
  • 在处理函数内语句时,如果不属于以上三种情况,那么
    • 是仅调用函数,每一项的传参是一个④表达式
    • 如果不是仅调用函数,那必是赋值语句,该句子的第一个单词必是标识符,第二个单词必是等号,第三个及之后是一个④表达式
2.4 ④ 表达式的处理
  • 这里先对“表达式”下一个简要的定义:它可能是数字、变量、⑥调用函数或包含计算的一个式子,以下都是表达式:

    1
    a
    2*3
    f(f(4))		# f是一个自定义的函数
    

    注意到,对于 f(f(4)) 来说,内部的 f(4) 也是一个表达式;对于 f(4) 来说,内部的 4 又是一个表达式。

  • 注意句子表达式是不同的,句子是带分号的那种,表达式本质上只是一个结果或者说一个值,例如:

    int a; 是句子, 它不含有表达式
    int a=1; 是句子, 它含有表达式 1
    return 0; 是句子, 它含有表达式 0
    a=f(1,f(2,3)); 是句子, 它含有表达式 f(1,f(2,3)), 该表达式又含有表达式 1 和表达式 f(2,3), 对于后者, 它又含有表达式 2 和表达式 3
    
  • 观察③中的描述,出现了大量含有“表达式”的描述,并且考虑到表达式的内部可能又是表达式,因此一个比较好的做法可能是单独写一个函数专门用来处理表达式,下面我给出一个参考框架:

    void expression(vector<string> E) {
    	// 如果表达式的长度是 1, 可能是一个数字或一个变量
        if (E.size()==1) {
            if (是数字) {
                对数字的处理;
            }
            else {
                对变量的处理;
            }
        }
        // 否则是仅调用函数或带计算的式子
        else {
            // 如果是仅调用函数
            if (仅调用函数) {
                 对函数的处理;			----------⑦
            }
            // 是涉及运算
            else {
            	// 考虑运算符的优先级
                对运算的处理;
            }
        }
        return;
    }
    注: 我写这个函数的长度是 206 行.
    

    单独定义函数的好处是,遇到表达式,直接调用这个函数;如果遇到了表达式的内部是表达式,则递归调用这个函数。

2.5 一些对你可能有用汇编语句

假设 a 和 b 的值已经提前分别存储在 eax 和 ebx 中,并且需要把计算的结果放在 eax 中。

# a + b
add eax, ebx
# a - b
sub eax, ebx
# a * b
imul eax, ebx
# a / b
cdq
idiv ebx
# a % b
cdq
idiv ebx
mov eax, edx
# a < b
cmp eax, ebx
setl al
movzx eax, al
# a > b
cmp eax, ebx
setg al
movzx eax, al
# a <= b
cmp eax, ebx
setle al
movzx eax, al
# a >= b
cmp eax, ebx
setge al
movzx eax, al
# a == b
cmp eax, ebx
sete al
movzx eax, al
# a != b
cmp eax, ebx
setne al
movzx eax, al
# a & b
and eax, ebx
# a | b
or eax, ebx
# a ^ b
xor eax, ebx
# a && b												----------⑧
# a&&b 的意思是,当且仅当a和b都不为0时,结果为1;否则结果为0.
# 实现的方法很多,这里是一个例子
test eax, eax
jz false1
test ebx, ebx
jz false2
mov eax, 1 
jmp done
false1:
false2:
mov eax, 0
done:
# a || b												----------⑧
# a||b 的意思是,当且仅当a和b都为0时,结果为0;否则结果为1.
# 实现的方法很多,这里是一个例子:
test eax, eax
jnz true1
test ebx, ebx
jnz true2
mov eax, 0
jmp done
true1:
true2:
mov eax, 1
done:
# -a
# 也可以看作是0-a,转化为减法
neg eax
# !a
test eax, eax
setz al
movzx eax, al
# c = ~a
not eax
2.6 运算符优先级

其实网上很容易搜得到,但是上回就有人来问我优先级,所以我也一并贴出:

( - = ~ = ! ) > ( * = / = % ) > ( + = - ) > ( > = < = >= = <= ) > ( == = != ) > & > ^ > | > && > ||

2.7 调用函数的传参

函数调用是这样的:参数是从后往前压栈的(这样调用函数的时候弹栈的顺序就是从前往后的)

一个参考做法如下,例如 f(1+2, 3*4, !!5, g(6, 7)),先划分,找到划分的位置分别是 1, 5, 9, 13, 20

f ( 1 + 2 , 3 * 4 ,  !  !  5  ,  g  (  6  ,  7  )  )
----------------------------------------------------
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

然后从后往前,依次是 14-19, 10-12, 6-8, 2-4,它们每一个都是表达式。

四、实验注意

  1. 该部分主要阐述本人在做该实验时踩过的坑

  2. 这次提交方式和 lab2 一样都需要写 CMakeList 文件,不同之处是在声讨之下从 gitlab 提交改为了压缩包提交,个人认为交压缩包还是更方便一点。

  3. ⑤:如果你使用了我的分词方法,注意在第二次扫描时处理两个符号应该连在一起作为一个单词共现的情形,它们是

    >=, <=, !=, ==, &&, ||
    
  4. ⑥:对于是否仅调用函数的判断,我注意到很多人的做法是:

    if (第一个单词是函数名 && 第二个单词是左括号 && 分号的前一个单词是右括号)
    

    然而该条件判断不完全正确,例如下面的反例存在:

    func ( 1 ) + func ( 2 ) ;
    

    显然,它的第一个单词是函数名 func,第二个单词是 (,分号前面的单词是 ),但它并不是仅函数调用,而是一个带计算的式子。

    那么正确的判断条件是怎样的呢?提示一下:你需要考虑括号,具体请你自己思考。

  5. ⑦:对于多元函数,我们为了传递参数,就需要将每个参数分别处理并依次传入,例如:

    funct(1, 2, 3)			# 函数原型: int funct(int a, int b, int c);
    

    对应的汇编过程可能是:

    push 3
    push 2
    push 1
    call funct
    

    为了能够分辨各个参数,许多人的做法是按照 , 分割,从而 f(1,2,3) 的参数被分割成 1,2,3

    这种方法也存在问题,考虑多元函数的嵌套,下面的反例存在:

    g(1, g(2, 3))			// 函数原型: int g(int a, int b);
    

    此时你再试试用 , 分割,结果是:1,g(2,3),显然划分结果中的 g(23) 都是不可被正确识别的。

    那么正确的划分应该怎样做呢?提示一下:你需要考虑括号,具体请你自己思考。

  6. - 是一个很特别的符号,务必记得正确地区分它究竟是双目运算符还是单目运算符。

  7. ⑧:我之前没有学过汇编语言,后来我稍微地学了一些,算是大概把这个看懂了。

    以计算 a&&b 为例,我在上面给出的语句是:

    test eax, eax
    jz false1		// 看这里
    test ebx, ebx
    jz false2		// 看这里
    mov eax, 1 
    jmp done		
    false1:			// 看这里
    false2:			// 看这里
    mov eax, 0
    done:			
    

    test eax, eaxjz false1 的意思是判断 eax 是否为 0,如果是 0,则跳转false1: 这一行开始执行; 同样,test ebx, ebxjz false2 的意思是判断 ebx 是否为 0,如果是 0,则跳转false2: 这一行开始执行;jmp done 的意思是跳转done: 这一行开始执行。

    我参考了这一篇:x86 汇编手册快速入门

    需要注意的是,可以多处跳转到同一个入口,例如:

    test eax, eax
    jz false		// 看这里
    test ebx, ebx
    jz false		// 看这里
    mov eax, 1 
    jmp done
    false:
    mov eax, 0
    done:
    

    是可以正确运行的。

    然而,例如有两个 && 运算,写成

    test eax, eax
    jz false
    test ebx, ebx
    jz false
    mov eax, 1 
    jmp done
    false:			// 看这里
    mov eax, 0
    done:			// 看这里
    
    test eax, eax
    jz false
    test ebx, ebx
    jz false
    mov eax, 1 
    jmp done
    false:			// 看这里
    mov eax, 0
    done:			// 看这里
    

    存在多个跳转的入口,这样就会报错。

    对于 a||b 的情况类似,不再赘述。

  8. 注意传参局部变量的区别,它们在栈里的位置有些不同,局部变量是 ebp-,而传参是 ebp+

五、测试

平台上已经为你准备了 8 道测试用例,你可以自己测测看,这次我就不写用例了。

注:哥们不卖代码。

  • 34
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单片机中断系统是由中断控制器、中断向量表和中断源三部分组成的。 中断控制器是一个硬件模块,用于管理和调度中断请求。它可以根据中断请求的优先级,自动选择最高优先级的中断请求,并将中断请求发送给CPU。中断控制器还可以控制中断请求的屏蔽和响应,以及中断处理的完成。 中断向量表是一个存储中断服务程序地址的表格。当中断请求被触发时,中断控制器会将中断请求的向量号发送给CPU,CPU会到中断向量表中查找对应的中断服务程序地址,并跳转到该地址执行相应的中断服务程序。 中断源是指触发中断请求的硬件模块或软件程序。当中断源产生中断请求时,中断向量号会被送到CPU进行处理。 下面以STM32F103为例,说明单片机中断系统的工作原理和程序实验。 1. 硬件结构 STM32F103中断系统的硬件结构如下图所示: ![STM32F103中断系统的硬件结构](https://img-blog.csdn.net/20171010124715850) 其中,NVIC是中断控制器,它可以管理和调度STM32F103的所有中断请求。当有多个中断请求同时到达NVIC时,它会根据中断请求的优先级自动选择最高优先级的中断请求,并将中断请求发送给CPU。同时,NVIC还可以控制中断请求的屏蔽和响应,以及中断处理的完成。 2. 工作原理 STM32F103的中断系统工作原理如下: (1)当某个硬件模块或软件程序产生中断请求时,中断源会向NVIC发送中断请求。 (2)NVIC会根据中断请求的优先级自动选择最高优先级的中断请求,并将中断请求发送给CPU。 (3)CPU会将当前的执行状态保存到堆栈中,并跳转到中断向量表中查找对应的中断服务程序地址。 (4)CPU会执行中断服务程序,直到中断服务程序执行完成。 (5)CPU会从堆栈中恢复之前的执行状态,并继续执行之前的程序。 3. 程序实验 下面是一个在STM32F103上使用TIM2定时器中断的程序实验: (1)初始化TIM2定时器,设置为1秒中断一次。 ``` void TIM2_Init(uint16_t arr, uint16_t psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; NVIC_InitTypeDef NVIC_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseInitStruct.TIM_Period = arr; TIM_TimeBaseInitStruct.TIM_Prescaler = psc; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct); NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); } ``` (2)编写中断服务程序,当TIM2定时器中断到达时,LED灯会翻转一次。 ``` void TIM2_IRQHandler(void) { if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); GPIO_WriteBit(GPIOB, GPIO_Pin_12, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_12))); } } ``` (3)在main函数中调用TIM2_Init函数进行初始化,并等待中断服务程序执行。 ``` int main(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE); GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStruct); TIM2_Init(1000 - 1, 7200 - 1); while(1) { } } ``` 以上程序实验中,当TIM2定时器中断到达时,中断服务程序会将LED灯翻转一次。通过这个实验可以看出,单片机中断系统可以方便地处理多个硬件模块或软件程序的中断请求,提高单片机的响应速度和实时性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值