首页 > 编程语言 >程序执行和模拟

程序执行和模拟

时间:2024-04-19 14:56:01浏览次数:23  
标签:fp 1024 程序执行 inst 0x1f else uint32 模拟

一、freestanding

图片名称 在之前的学习中都是在linux进行编译。那么从学习的角度看还是freestanding比较简单,图中_start这个程序是作为死循环的,输出一个A后处于while(1)的循环状态。 那么如果我们在freestanding中进行编译呢?要在freestanding中编译首先要明白一个问题,程序如何结束运行? 图片名称 通过yzh视频中得知,C99的手册中写到freetanding environment 的结束是一个未定义行为,那么怎么解决这个问题呢?就要在exit地址写暗号~,如果把//*exit注释,则会出现 死循环,不断输出A的情况。 正如C99手册所说,freestanding在手册中很多都是处于未定义的状态,在上篇博客中我们也提到未定义状态可能引起的问题,所以我们最好自己设计一个! 图片名称

二、YEMU (ysyx EMU)

要实现YEMU,也就是实现指令模拟,我们回顾上次课的“状态机”相关知识

图片名称 正如图片中所描述的,我们只要把这个状态机实现出来,就可以来执行指令。那么又在上节课介绍:C程序是个状态机,那么我们是不是可以用C语言来实现这个状态机,从而实现指令运行。 图片名称 如何用C语言实现寄存器和内存,例如在RISCV32中有32个32位的寄存器,那么我们定义一个数组就可以了,每个元素都是uint32_t,也就是32位 32个寄存器了。特殊的是用PC,还有内存
#include <stdint.h>
uint32_t R[32], PC; // according to the RISC-V manual
uint8_t M[64];      // 64-Byte memory

为什么我们不适用int32_t和int8_t?

图片名称 8位最大是127,如果内存值为127,在+1后溢出,溢出值为undefine behavior,但是无符号数不会溢出 ![image](/i/l/?n=24&i=blog/3412936/202404/3412936-20240419113907886-619491592.png) 模拟器本质上执行的就是上述几条行为,正如右下角程序所呈现的,如果halt为false,那么while循环会一直执行inst_cycle(), 这个cycle就是 指令周期。所以本质上我们要做的就是把 inst_cycle给实现出来,这样就是一个模拟器。但是在进行模拟前要先清楚inst的语义,也就是每个指令是什么意思。搞清楚 每个指令的意思,就需要去查手册,这个行为就称作“译码”。如下是用C实现一个模拟器
点击查看代码
void inst_cycle() {
  uint32_t inst = *(uint32_t *)&M[PC];
  if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0) { // addi
    if (((inst >> 7) & 0x1f) != 0) {
      R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] +
        (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
    }
  } else if (inst == 0x00100073) { // ebreak
    if (R[10] == 0) { putchar(R[11] & 0xff); }
    else if (R[10] == 1) { halt = true; }
    else { printf("Unsupported ebreak command\n"); }
  } else { printf("Unsupported instuction\n"); }
  PC += 4;
}

可以看到对指令进行分析,如果为addi指令,则进行xxx操作,如果位ebreak指令,则进行xxx操作。在指令执行完毕后需要把pc + 4,这对应这我们的"更新pc"。这里值得注意的是R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] +...这条指令是把立即数和rs寄存器的值相加,存到rd寄存器中。但是可以看到他后面还有个减法 -((inst & 0x80000000) ? 4096 : 0)。具体解释如下


最高位通常用于表示一个立即数是否为负。如果最高位是1,这通常意味着立即数是负的,需要进行符号扩展;如果最高位是0,则表示立即数是非负的。
inst & 0x80000000 这个表达式通过和 0x80000000 进行位与操作来检查 inst 的最高位是否设置(即为1)。这里的 0x80000000 是一个只有最高位为1的32位整数。如果 inst 的最高位是1,这个操作的结果将不为0,这表示立即数是负的,否则为正。
当确定立即数为负时,代码使用 4096(或者 0x1000)来进行符号扩展。在这种情况下,负的立即数将从12位扩展到32位,而12位立即数的表示方法是使用补码。在补码中,一个负数可以通过从2的指数次方中减去其绝对值来表示。由于立即数是12位,所以2的指数次方就是 2^12,也就是 4096。

因此,当你看到 ((inst & 0x80000000) ? 4096 : 0) 这段代码时,它是在做这样的操作:如果立即数是负的,就从 4096 减去其绝对值来得到32位的负立即数;如果是正数,就直接使用该值。这是RISC-V和其他许多处理器架构在处理立即数时常用的一种技巧。


指令部分分析结束后,我们还需要设定初始值,也就是rest:

图片名称 riscv手册中定义 pc set 一个 0,0号寄存器恒为0,其他state 是unspecified的,也就是自己说了算的。 综合上述信息,我们就可以得到YEMY(Ysyx EMUlator )1.0版本
点击查看代码
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
uint32_t R[32], PC;
uint8_t M[64] = {
  0x13, 0x05, 0x00, 0x00, 0x93, 0x05, 0x10, 0x04, 0x73, 0x00, 0x10, 0x00,
  0x13, 0x05, 0x10, 0x00, 0x93, 0x05, 0x00, 0x00, 0x73, 0x00, 0x10, 0x00,
  0x6f, 0x00, 0x00, 0x00,
};
bool halt = false;

void inst_cycle() {
  uint32_t inst = *(uint32_t *)&M[PC];
  if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0) { // addi
    if (((inst >> 7) & 0x1f) != 0) {
      R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] +
        (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
    }
  } else if (inst == 0x00100073) { // ebreak
    if (R[10] == 0) { putchar(R[11] & 0xff); }
    else if (R[10] == 1) { halt = true; }
    else { printf("Unsupported ebreak command\n"); }
  } else { printf("Unsupported instuction\n"); }
  PC += 4;
}

int main() {
  PC = 0; R[0] = 0; // can be omitted since uninitialized global variables are initialized with 0
  while (!halt) { inst_cycle(); }
  return 0;
}

当然也可以从文件来读入程度,例如
点击查看代码
uint8_t M[1024];
int main(int argc, char *argv[]) {
  PC = 0; R[0] = 0;
  FILE *fp = fopen(argv[1], "r");
  fread(M, 1, 1024, fp);
  fclose(fp);
  while (!halt) { inst_cycle(); }
  return 0;
}
根据读入的文件再次进行模拟,如果我们把可执行文件(prog)的指令序列抽取到prog.bin中,可以看到.bin的内容和反汇编内容是指令序列 图片名称

三、编写可读可维护的代码
1、写assert,assert可以避免出现 Segmentation Fault 等类似无法调试的错误,例如

点击查看代码
#include <assert.h>
// ...
int main(int argc, char *argv[]) {
  PC = 0; R[0] = 0;
  assert(argc >= 2);  // 要求至少包含一个参数
  FILE *fp = fopen(argv[1], "r");
  assert(fp != NULL); // 要求argv[1]是一个可以成功打开的文件
  int ret = fseek(fp, 0, SEEK_END);
  assert(ret != -1); // 要求fseek()成功
  long fsize = ftell(fp);
  assert(fsize != -1); // 要求ftell()成功
  rewind(fp);
  assert(fsize < 1024); // 要求程序大小不超过1024字节
  ret = fread(M, 1, 1024, fp);
  assert(ret == fsize); // 要求完全读出程序的内容
  fclose(fp);
  while (!halt) { inst_cycle(); }
  return 0;
}
在每一个容易出错的地方都插入assert,即使会出现错误也会非常清晰的呈现:
./yemu not-exist.bin
yemu: yemu.c:27: main: Assertion `fp != NULL' failed.

同时也可以利用宏,来让assert 失败时输出更多信息,例如:

点击查看代码
#define Assert(cond, format, ...) \
  do { \
    if (!(cond)) { \
      fprintf(stderr, format "\n", ## __VA_ARGS__); \
      assert(cond); \
    } \
  } while (0)

int main(int argc, char *argv[]) {
  PC = 0; R[0] = 0;
  Assert(argc >= 2, "Program is not given");  // 要求至少包含一个参数
  FILE *fp = fopen(argv[1], "r");
  Assert(fp != NULL, "Fail to open %s", argv[1]); // 要求argv[1]是一个可以成功打开的文件
  int ret = fseek(fp, 0, SEEK_END);
  Assert(ret != -1, "Fail to seek the end of the file"); // 要求fseek()成功
  long fsize = ftell(fp);
  Assert(fsize != -1, "Fail to return the file position"); // 要求ftell()成功
  rewind(fp);
  Assert(fsize < 1024, "Program size exceeds 1024 Bytes"); // 要求程序大小不超过1024字节
  ret = fread(M, 1, 1024, fp);
  Assert(ret == fsize, "Fail to load the whole program"); // 要求完全读出程序的内容
  fclose(fp);
  while (!halt) { inst_cycle(); }
  return 0;
}
但是要注意Assert中有if ,而编译器中else会匹配最近的if,所以用宏的时候要注意最近匹配的原则。 2、减少代码中的隐含依赖 这里举了一个例子
uint8_t M[512];
Assert(fsize < 1024, "Program size exceeds 1024 Bytes");
ret = fread(M, 1, 1024, fp);  // BUG: 忘了改, 可能发生缓冲区溢出!

例如我们的代码原来是uint8_t M[1024],但是在后期调试的时候进行更改,改为了512,那么就会发生溢出。后续的内容收到影响进而出现非常难调试的bug。

#define MSIZE 1024
uint8_t M[MSIZE];
// 另一种方式
uint8_t M[1024];
#define MSIZE (sizeof(M) / sizeof(M[0]))
Assert(fsize < MSIZE, "Program size exceeds %d Bytes", MSIZE);
ret = fread(M, 1, MSIZE, fp);

如果把这些参数定义为宏,那么消灭了上述的代码依赖。
3、编写可复用代码
未来可能一块代码复用多次,不可以直接copy paste,(容易出现一处出现错误,所有的copy-paste都需要进行修改的情况)
例如:

点击查看代码
if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0) { // addi
  if (((inst >> 7) & 0x1f) != 0) {
    R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] +
      (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
  }
} else if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0x4) { // xori
  if (((inst >> 7) & 0x1f) != 0) {
    R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] ^
      (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
  }
} else if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0x6) { // ori
  if (((inst >> 7) & 0x1f) != 0) {
    R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] |
      (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
  }
} else if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0x4) { // andi
  if (((inst >> 7) & 0x1f) != 0) {
    R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] &
      (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
  }
} else if (...) {  ...  }
这其中其实是有错误的,但是明显很难找出。所以我们需要把它简化,可以让代码进行复用,such as
点击查看代码
uint32_t inst = *(uint32_t *)&M[PC];
uint32_t opcode = inst & 0x7f;
uint32_t funct3 = (inst >> 12) & 0x7;
uint32_t rd  = (inst >> 7 ) & 0x1f;
uint32_t rs1 = (inst >> 15) & 0x1f;
uint32_t imm = ((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0);
if (opcode == 0x13) {
  if      (funct3 == 0x0) { R[rd] = R[rs1] + imm; } // addi
  else if (funct3 == 0x4) { R[rd] = R[rs1] ^ imm; } // xori
  else if (funct3 == 0x6) { R[rd] = R[rs1] | imm; } // ori
  else if (funct3 == 0x7) { R[rd] = R[rs1] & imm; } // andi
  else { panic("Unsupported funct3 = %d", funct3); }
  R[0] = 0; // 若指令写入了R[0], 此处将其重置为0
} else if (...) {  ...  }
PC += 4;
把代码进行模块化,需要的话直接调用即可,对齐的代码更容易阅读并发现错误 4、使用合适语言特性 其实上述代码还可以进行更标准的写法,如我们所知RISCV有很多类型例如I,J等。那么我们可以定义合适的结构体,such as
点击查看代码
typedef union {
  struct {
    uint32_t opcode  :  7;
    uint32_t rd      :  5;
    uint32_t funct3  :  3;
    uint32_t rs1     :  5;
     int32_t imm11_0 : 12;
  } I;
  struct { /* ... */ } R;
  uint32_t bytes;
} inst_t;

inst_t *inst = (inst_t *)&M[PC];
uint32_t rd  = inst->I.rd;
uint32_t rs1 = inst->I.rs1;
uint32_t imm = (int32_t)inst->I.imm11_0;
if (inst->I.opcode == 0b0010011) {
  switch (inst->I.funct3) {
    case 0b000: R[rd] = R[rs1] + imm; break; // addi
    case 0b100: R[rd] = R[rs1] ^ imm; break; // xori
    case 0b110: R[rd] = R[rs1] | imm; break; // ori
    case 0b111: R[rd] = R[rs1] & imm; break; // andi
    default: panic("Unsupported funct3 = %d", inst->I.funct3);
  }
  R[0] = 0; // 若指令写入了R[0], 此处将其重置为0
} else if (inst->bytes == 0x00100073) {  ...  }
后续可以仿照I类型的把其余J S等类型补齐。

标签:fp,1024,程序执行,inst,0x1f,else,uint32,模拟
From: https://www.cnblogs.com/ink-bai/p/18145578

相关文章

  • P4423 / YC271A [ 20240411 CQYC省选模拟赛 T1 ] 三角形(triangle)
    题意给定\(n\)个点,求平面最小三角形周长。Sol其实挺简单一算法,一直没学。先随机转个∠,然后按照\(x\)排序。考虑分治。注意到分治左右两边的答案对当前可用的区间有限制。将满足限制的点按照\(y\)排序。这里可以归并做到一只\(log\)。然后集中注意力,发现对于每个点......
  • NOI 2024省选OIFC模拟21 T1(思维题)
    原我觉得非常有思维含量的一题没看懂题解,大佬讲的还是没有看懂对于一个集合S,不妨设要将这个集合设为蓝色,考虑一个包含元素最多的一个为蓝色的子集T,那么在包含有S-T集合中的元素的集合必定为红色,因为如果有一个为蓝色,那么这个与前面那个极大蓝色集合交一下就会有一个更大的蓝......
  • 如何从头手动制作一个冲压仿真软件 —— 《冲压模可视化仿真模拟》
    因为工作需要,前段时间曾思考过如何手动做一个冲压仿真软件,但是研究发现这东西居然需要用到很多的数学和物理学的知识,而这方面的知识我又不具备,于是只好作罢,但是后来看到了本文中的这个论文,虽然没有看到全文,但是感觉这个主题还是比较贴切的。原文地址:https://wap.cnki.net/touch......
  • 模拟电路学习笔记——晶体管电流放大作用
    基本共射放大电路△u1为输入电压信号,接入基极——发射极回路,称为输入回路;放大后的信号在集电极——发射极回路,称为输出回路;因发射极是两个回路的公共端,故称该电路为共射放大电路晶体管工作在放大状态的外部条件:发射结正向偏置,集电结反向偏置输入回路中基极电......
  • 2024省选OIFC模拟T1
    题意:给定k颗有n个点的树对于每个点对(i,j),求出其在每棵树上的路径经过的点(含端点)的并集大小。做法:一个比较简单的想法是搞出每个(i,j)在第k颗树上的点的集合,然后所有树并一下,这个再用bitset优化一下,然后有人就过了,而我这位大常数选手就没过。首先容斥为求不经过点的交。考......
  • 模拟电路学习笔记——晶体三极管(一)
    1.晶体三极管*晶体三极管中有两种带有不同极性电荷的载流子参与导电,故称为双极型晶体管(BJT:BipolarJunctionTransistor),又称半导体三极管,简称晶体管*根据不同的掺杂方式,在同一硅片上制造出三个掺杂区域,并形成两个PN结,就构成晶体管2.几张常见晶体管外形3.晶体管的结......
  • 模拟电路学习笔记——二极稳压管
    1.稳压二极管*硅材料制成的面接触型晶体二极管*稳压管在反向击穿时,在一定的电流范围内(或者说在一定的功率损耗范围内),端电压几乎不变,表现出稳压特性*广泛应用于稳压电源/限幅电路中2.稳压管伏安特性*稳压器伏安特性与普通二极管类似*稳压管正向特性为指......
  • 模拟电路学习笔记——半导体基础知识
    1.纯净的具有晶体结构的半导体称为本征半导体2.导体导电只有一种载流子,即自由电子导电本征半导体含两种载流子:自由电子和空穴载流子:运载电荷的粒子3.杂质半导体*本征半导体中掺入少量合适的杂质元素,便可得到杂质半导体3.1N型半导体:纯净的硅晶体中掺入五价......
  • 模拟电路学习笔记——电子信息系统
    1.对模拟信号处理的电路称为模拟电路,对数字信号处理的电路称为数字电路;目前实用系统常常是两种信号都存在的模-数混合系统 2.电子信息系统的组成原则*满足功能和性能指标要求*电路尽量简单  电路简单、元器件越少,连线、焊点越少,故障出现概率越小*电磁兼容性  在预定......
  • 模拟电路学习笔记——电信号
    1、电信号:指随时间而变化的电压或电流电子电路中的电信号简称信号2.模拟信号和数字信号 ****模拟信号:在时间和数值上均有连续性,如正弦波信号是典型的模拟信号****数字信号:在时间和数值上均具有离散性 3.大多数物理量的电信号均为模拟信号;计算机只能识别数字信号;模拟......