【编译原理】代码生成器的构建与测试(山东大学实验三)

编译原理:代码生成器程序设计与实现

🌈 个人主页:十二月的猫-CSDN博客
🔥 系列专栏: 🏀编译原理_十二月的猫的博客-CSDN博客

💪🏻 十二月的寒冬阻挡不了春天的脚步,十二点的黑夜遮蔽不住黎明的曙光 

目录

1. 前言

2. 代码生成是什么

2.1 代码生成器的任务

2.2 代码整体架构分析 

2.2.1 计算资源分配

2.2.2 生成中间代码(在语法语义分析器中完成)​编辑

2.2.3 执行中间代码 

3. 代码分析 

3.1 code中提取分析机器指令

3.2 executeCycle函数实现

3.2.1 一些背景知识的理解

3.2.2 executeCycle函数的具体理解

4. 完整代码实现

5. 总结


1. 前言

编译原理的第三个实验:设计、编制并调试一个代码生成器程序,加深对代码生成器原理的理解。

如果对词法分析、语法分析以及语义分析的实现方法还不熟练的同学,可以来看看另外两篇文章:

【编译原理】山东大学实验·词法分析器设计_山东大学编译原理实验-CSDN博客

【编译原理】语法、语义分析器设计(山东大学实验二)_编译原理语法分析实验-CSDN博客

通过这两篇文章,你能够明白词法分析、语法分析、语义分析的原理,这都是代码生成器的基础。

在这两篇的基础上,我们来进入今天的课题:代码生成器!!!!

代码生成器本质:写一个函数,能够根据抽象语法树(中间代码)生成更为低级的汇编代码(也可以说是机器指令),并执行这些指令来验证生成的正确性。

早期语法语义分析器:并不会生成抽象语法树(抽象语法树仅仅用于检查/优化,可以不需要),而是直接在语法语义分析器后进行代码生成,生成并执行汇编代码

本实验,我们生成了抽象语法树,但是仅仅是形式化生成。因为代码生成器的输入并不是抽象语法树,也没有根据抽象语法树进行优化后再生成中间代码。本质上,是早期的语法语义分析器。

2. 代码生成是什么

代码生成器包括两种实现方式:

本实验中,我们选用栈计算机。 

2.1 代码生成器的任务

代码生成器包括如下内容: 

所有任务如下:

  1. 给源程序数据分配计算资源(数据放在栈、堆、代码区等地方)
  2. 给源程序的代码选择指令,生成汇编指令(中间代码)
  3.  执行中间代码(汇编指令)

2.2 代码整体架构分析 

2.2.1 计算资源分配

2.2.2 生成中间代码(在语法语义分析器中完成)

结果如下:

6 0 19
5 0 4
1 0 10
1 0 2
8 0 3
3 1 3
1 0 10
1 0 3
8 0 4
3 1 4
2 1 3
2 1 4
8 0 8

这有什么含义呢?

猫猫来解释一波:

  1. 每一行都是一条instruction(目标指令)。
  2. 目标指令就是中间代码。
  3. 中间代码就是我们分析源程序得到的和源程序功能一致但没有源程序容易理解的代码。
  4. 每一条instruction包括:类型、层级、操作数。
  5. 其实一条指令我们只需要知道其类型以及操作数便可以知道其具体的运行效果。但是由于table表格(语法分析生成用于存储变量的表格)在语义分析中已经被删除,因此我们需要层级来帮助我们在栈(变量、常量等都存储在栈中)(中间代码是一种更加偏向底层,类似于机器指令的代码)中查找变量位置以及程序跳转(JMP、JPC等指令)
  6. 本质上中间代码就是原本语句的逐句翻译,只不过从人类很容易看得懂的自然语言程序(存储起来浪费内存,但是偏向自然语言)转化为仅仅依靠数字/简单字母表示的中间程序(例如机器指令等都有此类功能效果)。
  7. 因为机器擅长于记忆/查找。因此使用数字表示,再用简短字母代替一系列特定功能效果记录在表格中(例如JMP表示跳转,在原程序中可能需要好几个自然语言程序)。机器通过数字查找简短字母再实现特定一系列功能就能够大大提高程序执行中的运行效率,减少内存占用。
  8. 这一类中间代码只有非常熟悉机器内部指令设置的专业人士才能够理解。因此在java、c++等高级语言(所谓高级也就是更偏向于自然语言)出来前,程序员只能通过机器指令(类似于中间代码)编程,虽然运行效率高(比起高级语言,少了转化中间代码的过程)但是入门难度大。
  9. 计组中我们需要完成的是机器指令本身的编写和实现,这里的语义分析本质就是将高级语言PL0转化为中间代码(机器指令)。

2.2.3 执行中间代码 

下一步,代码生成器就是基于code去逐步解释执行代码。

基于前面分析的code就是机器指令组成的程序,这里的代码生成器本质就是生成更为低级的机器指令(可能没有机器指令低级,可能只是汇编代码)

由于计算机的根本作用是计算,其他功能的实现都是基于计算。

因此运行机器指令本质是:机器指令运算的实现

运算的实现:基于栈的加减乘除

对于PL0语言机器指令有如下:

  • 加载常数
  • 加载变量
  • 存储变量
  • 调用过程
  • 分配局部变量
  • 跳转
  • 条件跳转
  • 操作指令

看到这几个语句种类,想起来之前学计算机组成原理,以及后面做计算机组成原理课程设计时,自己设计机器指令的痛苦时光了。。。。。。。

这几个指令就是计算机需要的最基础的几个机器指令,其他指令都可以依靠这些基础指令去组合出来。

对这几个机器指令的分析也就是对不同指令实行不同的一系列操作,具体方法如下:

后面我们再来一一分析具体的操作是什么。

这里先理解:因为要对不同指令实行不同操作,所以需要用到case。 


3. 代码分析 

3.1 code中提取分析机器指令

code文件是语法分析的输出结果,这一部分的功能就是将code中的机器指令一条条取出来,并利用:executeCycle函数完成机器指令分析与执行。

// vm函数:虚拟机主函数,负责加载代码文件并执行虚拟机周期
void vm(char *codefilename) {
    // 初始化虚拟机寄存器和栈
    pc = 0;       // 程序计数器初始化为0
    bp = 0;       // 基址指针初始化为0
    sp = -1;      // 栈顶指针初始化为-1(空栈)
    memset(stack, 0, sizeof(stack)); // 清空堆栈

    // 打开并读取代码文件(code)
    // code中:ins 语句类型;l 语句所在层级;m 语句操作数
    // 操作数:
    FILE *ifp;
    ifp = fopen(codefilename, "r");
    codeLength = 0;

    // 读取文件中的每条指令,并将其存储到arrayStruct数组中
    while (fscanf(ifp, "%d %d %d", &arrayStruct[codeLength].ins,
                  &arrayStruct[codeLength].l, &arrayStruct[codeLength].m) != EOF) {
        codeLength++;
    }

    // 逐条执行指令直到程序结束
    for (; pc < codeLength - 1;) {
        ir = arrayStruct[pc++];  // 将当前指令载入指令寄存器

        // 执行一个虚拟机周期,指令执行与栈操作
        executeCycle();
    }
}

3.2 executeCycle函数实现

3.2.1 一些背景知识的理解

首先先理解三个变量:bp、pc、sp

再理解一个函数:base()

1. bp(基址指针,Base Pointer):

bp基址指针,用于标记当前栈帧的基地址。栈帧是函数调用时在栈上分配的内存区域,bp 指向的是栈帧的基地址。栈帧通常包含函数的参数、局部变量、返回地址等信息。

  • 作用bp 用于访问当前函数调用栈帧中的局部变量和参数。它指向当前栈帧的起始位置。
  • 变化:在函数调用时,bp 会更新为当前栈帧的基地址。通常 bp 会在进入子程序时设置为 sp + 1,这时 sp 指向栈顶,bp 指向新的栈帧开始的位置。

示例:

  • 当调用子程序时,bp 被设置为 sp + 1,即新的栈帧的起始位置。
  • 在函数返回时,bp 会恢复到上一层函数的栈帧基地址,确保栈的正确回退。

2. pc(程序计数器,Program Counter):

pc程序计数器,它指向当前程序的下一条指令的地址,也就是指令流的控制指针。

  • 作用pc 控制程序的执行流程,指向当前正在执行的指令地址。
  • 变化:在函数调用时,pc 会被修改为子程序的地址,从而跳转到子程序去执行。函数执行完毕后,pc 会返回到调用它的指令,继续执行。

示例:

  • 当执行 CALL 指令时,pc 会被设置为子程序的起始地址。
  • 当子程序执行完并返回时,pc 会恢复为返回地址,即调用它的指令之后的位置。

3. sp(栈指针,Stack Pointer)

sp栈指针,它指向栈的当前顶部。栈是用来存储函数调用时的局部变量、返回地址和其他临时数据的区域。

  • 作用sp 指向栈的顶部,通常用于分配和释放栈空间。当有新的函数调用时,sp 会下降以分配栈空间;当函数返回时,sp 会上升,释放栈空间。
  • 变化:在函数调用时,sp 向下移动,分配新的栈帧;在函数返回时,sp 恢复原状。

示例:

  • sp 会随栈的增长或收缩而变化。
  • 当执行函数调用时,sp 会减少,栈中会被压入返回地址、局部变量等数据。

4. base() 函数

base() 是一个函数,用于返回当前栈帧的基址,通常与 bp 紧密相关。具体来说,base() 的作用是提供 当前函数栈帧的基地址

  • 作用base() 用来确定当前栈帧的基地址,通常会根据 bp 的值来计算。它是通过某些算法(比如栈帧链的查找)来确定父函数的栈帧基址。
  • 变化base() 通过计算来找到当前栈帧的基地址,它通常与 bp 相关联。例如,base() 可能会返回 bp 的值或者是 bp 所指向的某个偏移地址。

它们的关系和交互

  • bpspbpsp 都与栈相关。sp 是栈指针,它指向栈的顶部,而 bp 则指向当前栈帧的基地址,通常在函数调用时,bp 被设置为 sp + 1 或某个偏移量。栈的增长和回退通过 sp 来控制,而 bp 用来定位当前栈帧的位置。

  • bpbase()base() 通过 bp 来查找和返回当前栈帧的基地址。具体的实现可能会根据调用约定的不同有所不同。通常 base() 会返回一个偏移量,指向当前栈帧的父函数栈帧的基址。

  • pcbppcbp 的作用不同,pc 控制程序的执行流,而 bp 控制栈帧的管理。pc 控制当前指令的执行,而 bp 帮助定位栈中的数据。函数调用时,pc 跳转到子程序的入口地址,bp 更新为新的栈帧基址,保证正确的栈帧管理。

总结

  • pc:程序计数器,指向当前正在执行的指令地址
  • sp:栈指针,指向栈的当前顶部
  • bp:基址指针,用于管理函数调用中的局部变量
  • base():一个函数,用于计算并返回当前栈帧的基地址,通常与 bp 的值相关。

语法分析产生code(机器指令),机器指令分为三个部分:类型、层级和操作数。

类型没有太多可以说的地方,总共有八个类型:

  • 加载常数
  • 加载变量
  • 存储变量
  • 调用过程
  • 分配局部变量
  • 跳转
  • 条件跳转
  • 操作指令

接着来理解层级的意思。

错误理解:层级是指令在抽象树的层次或者作用域

正确理解:层级是指令操作数所在层次(作用域)到目标层次的差

例如:

8 0 3
3 1 5
8 0 0
5 0 4
2 1 3
3 1 7
1 0 0
3 1 6

第一条:8 0 3 中,0代表3这个操作数和8(类型)所代表的指令在同一个层级

第二条:3 1 5 中,1代表5这个操作数和3(类型)所代表的指令相差一个层级

背景:由于作用域外层的能给内层使用,内层不能给外层使用。因此层级相差一个等级表示需要从内作用域找向外作用域。


理解完层级,为了进一步理解指令,让我们来看看操作数

不同类型语句的操作数意义不同,如下:

  • 加载常数:常数的值
  • 加载变量:待加载变量的地址
  • 存储变量:存储变量的地址
  • 调用过程:跳转子程序的地址
  • 分配局部变量:局部变量空间大小
  • 跳转:跳转的目标地址
  • 条件跳转:跳转的目标地址
  • 操作指令

接下来,我们来理解栈在中间代码(机器指令)运行过程中的作用。

因为PL0语言机器指令有如下(加减乘除所需要的指令):

  • 加载常数
  • 加载变量
  • 存储变量
  • 调用过程
  • 分配局部变量
  • 跳转
  • 条件跳转
  • 操作指令

因此我们所要作的就是:通过栈实现这些指令在栈上的运算

具体理解思路如下:

理解到这里,我们就不难明确自己的目标:使用栈对不同机器指令完成计算的任务。


3.2.2 executeCycle函数的具体理解

栈顶:目前正常操作的数。

函数内容如下:

// executeCycle函数:执行一个周期,处理当前指令并更新寄存器和栈
void executeCycle() {
    switch (ir.ins) {
        case LIT: // 1. 载入常量
            stack[++sp] = ir.m; // 将常量m压入栈
            break;
        case LOD: // 2. 加载局部变量
            stack[++sp] = stack[base() + ir.m]; // 将基址+偏移地址位置的值压入栈
            break;
        case STO: // 3. 存储变量
            stack[base() + ir.m] = stack[sp--]; // 将栈顶的值存储到指定位置
            break;
        case CAL: // 4. 调用子程序
            stack[sp + 1] = base(); // 保存当前基址到栈
            stack[sp + 2] = bp;     // 保存返回地址
            stack[sp + 3] = pc;     // 保存程序计数器(返回地址)

            bp = sp + 1;   // 更新基址
            pc = ir.m;     // 跳转到子程序地址
            break;
        case INT: // 5. 分配空间
            sp = sp + ir.m; // 栈指针增加m,用于分配局部变量空间
            break;
        case JMP: // 6. 无条件跳转
            pc = ir.m; // 跳转到目标地址
            break;
        case JPC: // 7. 条件跳转
            if (!stack[sp--])  // 如果栈顶值为0,则跳转
                pc = ir.m;
            break;
        case OPR: // 8. 执行操作
            opr();
            break;
        default:
            printf("Illegal INS!\n"); // 无效指令
    }
}

LIT:将操作数(所要压入的常数)压到栈顶。

LOD:从局部变量所在地址(base:操作数层级起始地址;操作数:相对地址)加载数到栈顶。

STO:将栈顶元素存储到对应地址(base:操作数层级起始地址;操作数:相对地址)

CAL:对bp、pc、base的理解。bp:指令所在层级变量入口地址;base:操作数所在层级变量入口;pc:程序返回地址

INT:分配对应空间

JMP:跳转到操作数所在的程序位置

JPC:有条件跳转到操作数所在的程序位置

OPR:利用栈进行对应的操作


OPR具体操作:

// opr函数:执行操作指令(如加法、减法、乘法等)
void opr() {
    int temp;
    switch (ir.m) {
        case ADD: // 加法
            temp = stack[sp - 1] + stack[sp];
            stack[--sp] = temp; // 计算并将结果压入栈
            break;
        case SUB: // 减法
            temp = stack[sp - 1] - stack[sp];
            stack[--sp] = temp;
            break;
        case DIV: // 除法
            temp = stack[sp - 1] / stack[sp];
            stack[--sp] = temp;
            break;
        case MINUS: // 取负
            stack[sp] = -stack[sp];
            break;
        case MUL: // 乘法
            temp = stack[sp - 1] * stack[sp];
            stack[--sp] = temp;
            break;
        case EQ: // 等于
            temp = (stack[sp - 1] == stack[sp]);
            stack[--sp] = temp;
            break;
        case NE: // 不等于
            temp = (stack[sp - 1] != stack[sp]);
            stack[--sp] = temp;
            break;
        case GE: // 大于等于
            temp = (stack[sp - 1] >= stack[sp]);
            stack[--sp] = temp;
            break;
        case GT: // 大于
            temp = (stack[sp - 1] > stack[sp]);
            stack[--sp] = temp;
            break;
        case LE: // 小于等于
            temp = (stack[sp - 1] <= stack[sp]);
            stack[--sp] = temp;
            break;
        case LT: // 小于
            temp = (stack[sp - 1] < stack[sp]);
            stack[--sp] = temp;
            break;
        case ODD: // 判断奇数
            temp = stack[sp] % 2;
            stack[sp] = temp;
            break;
        case WRITE: // 输出栈顶的值
            printf("%d\n", stack[sp--]);
            break;
        case READ: // 读入一个整数并压入栈
            scanf("%d", &stack[++sp]);
            break;
        case 0: // 返回操作
            sp = bp - 1;  // 恢复栈指针
            pc = stack[sp + 3];  // 恢复返回地址
            bp = stack[sp + 2];  // 恢复基址
            break;
        default:
            printf("Illegal OPR!\n"); // 无效操作
    }
}

base():操作数所在目标层级的基地址

bp:指令初始基地址(也就是stack[bp]为一个base()的值)

base()的值存入stack能够构成层级的变量域链表

为什么要同时保存 bp 和 base()

在函数调用过程中,栈帧之间存在层次结构,函数可能调用另一个函数,这会导致栈的嵌套。而 bpbase() 的作用有所不同,因此在某些情况下需要同时保存这两个值:

  • bp 保存当前栈帧的基地址。它是一个局部的、固定的指针,指向当前函数的栈帧。
  • base() 计算的是特定层次的栈帧的基地址。它允许我们跳过一系列的栈帧,定位到更高层次的栈帧(例如,调用栈中的父函数或更上层的函数)。

举个例子:

假设有三个函数调用的嵌套关系:mainfuncAfuncB

  • bp 指向 funcB 的栈帧基址。
  • base() 可以用来查找 funcA 的栈帧基址(如果 l = 1,它会返回 funcA 的栈帧基址)。如果要定位 main 的栈帧基址,l 可以设置为 2。

为了能够恢复并正确执行多层函数调用,保存 bpbase() 是必要的:

  • bp 用于恢复当前栈帧的基址。
  • base() 用于定位调用链中某一层次的栈帧。

总结:

  • bp:保存当前栈帧的基地址,指向当前函数的栈帧,便于访问当前函数的局部变量和参数。
  • base():用于查找指定层次的栈帧的基地址,它在多层嵌套的函数调用中非常重要,帮助找到不同层次的栈帧。
// base函数:计算给定层次的基址
int base() {
    int l = ir.l;  // 层次
    int SL = bp;   // 初始基址为bp

    while (l > 0) {
        SL = stack[SL]; // 向上查找基地址
        l--;
    }
    return SL;
}

4. 完整代码实现

//
// Created by csh on 2024/12/02 in shangDongUniversity
//

#include <cstdio>
#include <cstring>
#include "data.h"    // 包含数据结构定义
#include "vm.h"      // 包含虚拟机功能相关声明

// 全局变量定义
int sp;     // 栈顶指针 (Stack Pointer)
int bp;     // 基址指针 (Base Pointer)
int pc;     // 程序计数器 (Program Counter)

int codeLength;                             // 目标代码的长度
instruction arrayStruct[CODE_SIZE];         // 存储目标代码的指令数组

instruction ir;                             // 指令寄存器 (Instruction Register)

int stack[MAX_STACK_HEIGHT];                // 虚拟机的堆栈

// vm函数:虚拟机主函数,负责加载代码文件并执行虚拟机周期
void vm(char *codefilename) {
    // 初始化虚拟机寄存器和栈
    pc = 0;       // 程序计数器初始化为0
    bp = 0;       // 基址指针初始化为0
    sp = -1;      // 栈顶指针初始化为-1(空栈)
    memset(stack, 0, sizeof(stack)); // 清空堆栈

    // 打开并读取代码文件(code)
    // code中:ins 语句类型;l 语句所在层级;m 语句操作数
    // 操作数:
    FILE *ifp;
    ifp = fopen(codefilename, "r");
    codeLength = 0;

    // 读取文件中的每条指令,并将其存储到arrayStruct数组中
    while (fscanf(ifp, "%d %d %d", &arrayStruct[codeLength].ins,
                  &arrayStruct[codeLength].l, &arrayStruct[codeLength].m) != EOF) {
        codeLength++;
    }

    // 逐条执行指令直到程序结束
    for (; pc < codeLength - 1;) {
        ir = arrayStruct[pc++];  // 将当前指令载入指令寄存器

        // 执行一个虚拟机周期,指令执行与栈操作
        executeCycle();
    }
}

// executeCycle函数:执行一个周期,处理当前指令并更新寄存器和栈
void executeCycle() {
    switch (ir.ins) {
        case LIT: // 1. 载入常量
            stack[++sp] = ir.m; // 将常量m压入栈
            break;
        case LOD: // 2. 加载局部变量
            stack[++sp] = stack[base() + ir.m]; // 将基址+偏移地址位置的值压入栈
            break;
        case STO: // 3. 存储变量
            stack[base() + ir.m] = stack[sp--]; // 将栈顶的值存储到指定位置
            break;
        case CAL: // 4. 调用子程序
            stack[sp + 1] = base(); // 保存当前基址到栈
            stack[sp + 2] = bp;     // 保存返回地址
            stack[sp + 3] = pc;     // 保存程序计数器(返回地址)

            bp = sp + 1;   // 更新基址
            pc = ir.m;     // 跳转到子程序地址
            break;
        case INT: // 5. 分配空间
            sp = sp + ir.m; // 栈指针增加m,用于分配局部变量空间
            break;
        case JMP: // 6. 无条件跳转
            pc = ir.m; // 跳转到目标地址
            break;
        case JPC: // 7. 条件跳转
            if (!stack[sp--])  // 如果栈顶值为0,则跳转
                pc = ir.m;
            break;
        case OPR: // 8. 执行操作
            opr();
            break;
        default:
            printf("Illegal INS!\n"); // 无效指令
    }
}

// opr函数:执行操作指令(如加法、减法、乘法等)
void opr() {
    int temp;
    switch (ir.m) {
        case ADD: // 加法
            temp = stack[sp - 1] + stack[sp];
            stack[--sp] = temp; // 计算并将结果压入栈
            break;
        case SUB: // 减法
            temp = stack[sp - 1] - stack[sp];
            stack[--sp] = temp;
            break;
        case DIV: // 除法
            temp = stack[sp - 1] / stack[sp];
            stack[--sp] = temp;
            break;
        case MINUS: // 取负
            stack[sp] = -stack[sp];
            break;
        case MUL: // 乘法
            temp = stack[sp - 1] * stack[sp];
            stack[--sp] = temp;
            break;
        case EQ: // 等于
            temp = (stack[sp - 1] == stack[sp]);
            stack[--sp] = temp;
            break;
        case NE: // 不等于
            temp = (stack[sp - 1] != stack[sp]);
            stack[--sp] = temp;
            break;
        case GE: // 大于等于
            temp = (stack[sp - 1] >= stack[sp]);
            stack[--sp] = temp;
            break;
        case GT: // 大于
            temp = (stack[sp - 1] > stack[sp]);
            stack[--sp] = temp;
            break;
        case LE: // 小于等于
            temp = (stack[sp - 1] <= stack[sp]);
            stack[--sp] = temp;
            break;
        case LT: // 小于
            temp = (stack[sp - 1] < stack[sp]);
            stack[--sp] = temp;
            break;
        case ODD: // 判断奇数
            temp = stack[sp] % 2;
            stack[sp] = temp;
            break;
        case WRITE: // 输出栈顶的值
            printf("%d\n", stack[sp--]);
            break;
        case READ: // 读入一个整数并压入栈
            scanf("%d", &stack[++sp]);
            break;
        case 0: // 返回操作
            sp = bp - 1;  // 恢复栈指针
            pc = stack[sp + 3];  // 恢复返回地址
            bp = stack[sp + 2];  // 恢复基址
            break;
        default:
            printf("Illegal OPR!\n"); // 无效操作
    }
}

// base函数:计算给定层次的基址
int base() {
    int l = ir.l;  // 层次
    int SL = bp;   // 初始基址为bp

    while (l > 0) {
        SL = stack[SL]; // 向上查找基地址
        l--;
    }
    return SL;
}

// printStackFrame函数:打印当前的栈帧信息
void printStackFrame() {
    printf("pc:%d\t", pc); // 打印程序计数器
    printf("bp:%d\t", bp); // 打印基址指针
    printf("sp:%d\t", sp); // 打印栈顶指针

    // 打印栈中的数据
    printf("stack:(");
    int max = sp >= bp ? sp : bp; // 栈的最大值用于遍历栈
    for (int i = 0; i <= max; i++)
        if (i)
            printf(" %d", stack[i]);
        else
            printf("%d", stack[i]);
    printf(")\n");

    printf("ir:(%d %d %d)\n", ir.ins, ir.l, ir.m); // 打印当前指令
}

// printIr函数:输出当前指令的内容
void printIr() {
    printf("ir:(%d %d %d)\t", ir.ins, ir.l, ir.m);
    if(ir.ins == OPR)
        printf("(%s %d %s)\n", insStr[ir.ins], ir.l, oprStr[ir.m]); // 如果是操作指令,则输出其对应的操作名称
    else
        printf("(%s %d %d)\n", insStr[ir.ins], ir.l, ir.m); // 否则输出标准指令信息
}

// printReg函数:输出寄存器的值
void printReg() {
    printf("pc:%d\t", pc); // 程序计数器
    printf("bp:%d\t", bp); // 基址指针
    printf("sp:%d\t", sp); // 栈顶指针
}

// printStack函数:输出栈的内容
void printStack() {
    printf("stack:(");
    int max = sp >= bp ? sp : bp;
    for (int i = 0; i <= max; i++)
        if (i)
            printf(" %d", stack[i]);
        else
            printf("%d", stack[i

5. 总结

本文到这里就结束啦~~
如果觉得对你有帮助,辛苦友友点个赞哦~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十二月的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值