从0到1使用C++实现一个模拟器-1-【实现最简CPU】

uint64_t

uint64_t是C/C++中的一种无符号整数类型,它表示一个64位的无符号整数。在计算机中,无符号整数是以二进制补码形式存储的,因此它的取值范围是从0到2^64-1(即18446744073709551615)。

uint64_t的全称是unsigned int 64-bit。

在C语言中,uint64_t是一个通过typedef定义的别名,代表了一个无符号的64位整型。这种命名方式是一种规范化的表示,其中"u"代表unsigned(无符号),"int"代表integer(整型),而"64"则指明了这是一个64位的整数类型。使用这些标准的数据类型别名可以提高代码的可读性和可维护性,因为它们明确地指出了数据类型的大小和属性。例如,uint8_t、uint16_t、uint32_t和uint64_t分别表示8位、16位、32位和64位的无符号整型。

这些数据类型都是在C99标准中定义的,它们并不是新的数据类型,而是已有数据类型的别名。这样做的好处是,即使在不同的平台上,这些类型的大小可能会有所不同,使用这些标准别名可以确保代码的一致性和移植性。

std

std是C++标准库的命名空间。

在C++编程语言中,std是一个特殊的命名空间,它包含了C++标准库的所有函数、类和其他模板。这个命名空间是为了区分标准库中的函数和对象与用户自定义的或其他库中的函数和对象。使用std命名空间,可以避免名称冲突,并且提供了一种组织代码的方式。以下是一些关于std命名空间的关键点:

标准库函数和对象:如std::cout用于输出,std::cin用于输入,以及各种数据结构和算法,如std::vector、std::sort等都在std命名空间中定义。
使用方式:在使用标准库中的任何函数或对象时,通常需要在前面加上std::来指明它们是来自std命名空间。例如,std::cout << "Hello, World!"中的std::就是必要的限定词。
简化使用:为了避免每次都要写std::,可以在代码中通过using namespace std;来告诉编译器在接下来的代码中使用std命名空间,这样就不需要重复写std::了。但这在某些情况下可能会导致命名冲突,所以需要谨慎使用。

std::array

std::array是一个固定大小的数组容器,提供了许多内置数组所不具备的功能,同时保持了与原生数组相同的随机访问能力和效率。以下是std::array的一些主要用法:

初始化:可以在声明时直接初始化std::array,也可以使用列表初始化的方式。例如:

std::array<int, 5> arr1 = {1, 2, 3, 4, 5};
std::array<int, 5> arr2{6, 7, 8, 9, 10};

  • 访问元素:可以通过下标操作符[]来访问或修改std::array中的元素,就像使用原生数组一样。

  • 获取大小:std::array提供了一个size()成员函数,可以返回数组的大小(元素个数)。

  • 获取数据指针:可以使用data()成员函数获取指向第一个元素的指针,这对于需要以C风格数组形式传递数据的情况非常有用。

  • 迭代器支持:std::array支持随机访问迭代器,可以使用迭代器遍历数组中的元素。

  • 不支持动态变化:由于std::array的大小是固定的,它不支持添加或删除元素,也不支持改变其大小。

  • 自动推导数组大小:在C++17及以后的版本中,可以利用类模板参数推导特性来简化std::array的声明,如std::array arr = {1,2,3,4,5,6};,但需要注意的是,这种推导方式无法只提供数组元素类型让编译器推导数组长度。

总的来说,std::array是一个结合了原生数组优点并增加了更多功能的安全容器。它在需要固定大小数组且希望有更多安全性和便利性的场景下是一个很好的选择。

CPU和CU

CPU(中央处理单元)和CU(控制单元)是计算机中的核心组件,它们在功能上有所区别。具体如下:

CPU:是计算机的大脑,负责执行程序指令和处理数据。它由多个部件组成,包括算术逻辑单元(ALU)、控制单元(CU)、寄存器等。CPU内部采用复杂的电路来执行各种计算和逻辑操作,同时它还负责处理与内存、输入输出设备之间的数据交换。
CU:是CPU内部的一个部件,全称为Control Unit,中文叫做控制单元。CU的主要功能是发出各种控制命令或微指令,控制整个计算机系统的工作,包括CPU内部的各个部件。CU协调各个部件的工作,确保程序能够按照预定的流程顺利执行。

在C++中,public和class是两个关键字,它们用于定义类的成员的访问权限。

class:用于定义一个类,它表示一个抽象的数据类型,可以包含数据成员(属性)和成员函数(方法)。类的定义通常包括类名、类的属性和方法等。例如:

class MyClass {
    // 类的属性和方法
};

public:用于指定类的成员的访问权限为公共(public),即可以在类的外部直接访问这些成员。在类的定义中,使用public关键字修饰的成员可以被其他代码直接访问和操作。例如:

class MyClass {
    public:
        int x; // 公共属性
        void myFunction(); // 公共方法
};

在上面的例子中,x是一个公共属性,可以在类的外部直接访问和修改;myFunction()是一个公共方法,也可以在类的外部直接调用。

需要注意的是,在C++中,如果没有显式地指定访问权限,默认情况下,类的成员都是私有的(private),只能在类的内部访问和操作。如果需要让类的成员在类的外部可访问,就需要使用public关键字来声明。

构造函数

C++中的构造函数是一种特殊的成员函数,它在创建对象时被自动调用。构造函数的名称与类名相同,没有返回值类型,可以有参数。构造函数的主要作用是对对象进行初始化。

以下是一个简单的C++构造函数示例:

#include <iostream>
using namespace std;

class MyClass {
public:
    // 构造函数
    MyClass() {
        cout << "构造函数被调用" << endl;
    }
};

int main() {
    MyClass obj; // 创建对象,自动调用构造函数
    return 0;
}

在这个示例中,我们定义了一个名为MyClass的类,并在其中定义了一个构造函数。当我们在main函数中创建MyClass的对象obj时,构造函数会自动被调用,输出"构造函数被调用"。

size_t

size_t是C++中的一种无符号整数类型,通常用于表示对象的大小或数组的长度。它的定义取决于具体的编译器和操作系统,但通常足够大以表示任何可能的对象大小。在32位系统中,size_t通常是32位的,而在64位系统中,size_t通常是64位的。

static_cast

static_cast 是 C++ 中的一种类型转换操作符,用于将一个表达式从一种类型转换为另一种类型。它主要用于基本数据类型之间的转换,如 int 转 float、double 转 int 等。

std::ifstream

std::ifstream 是 C++ 标准库中的一个类,用于从文件中读取数据。它继承自 std::istream 类,提供了一些成员函数和操作符,可以方便地从文件中读取文本或二进制数据。

使用 std::ifstream 打开文件时,需要指定文件名和打开模式。其中,文件名可以是相对路径或绝对路径,而打开模式则决定了文件的访问方式。常用的打开模式包括:

std::ios::in:以只读方式打开文件;
std::ios::out:以写入方式打开文件;
std::ios::binary:以二进制方式打开文件;
std::ios::ate:打开文件后立即定位到文件末尾;
std::ios::app:以追加方式打开文件。

riscv64-unknown-elf-objcopy -O binary add-addi add-addi.bin

这个命令的作用是将名为"add-addi"的ELF格式文件转换为二进制格式,并将结果保存为"add-addi.bin"。

具体解释如下:

  • "riscv64-unknown-elf-objcopy"是一个用于处理目标文件的工具,它可以将一个文件从一种格式转换为另一种格式。
  • "-O binary"选项表示要将输入文件转换为二进制格式。
  • "add-addi"是要转换的输入文件名。
  • "add-addi.bin"是转换后的输出文件名。

riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s

这个命令的作用是使用RISC-V 64位编译器将汇编文件"add-addi.s"编译为可执行文件"add-addi"。

具体解释如下:

  • "riscv64-unknown-elf-gcc"是一个用于编译和链接程序的工具,它是针对RISC-V架构的交叉编译器。
  • "-Wl,-Ttext=0x0"选项指定了程序的起始地址为0x0。
  • "-nostdlib"选项表示不链接标准库。
  • “-o add-addi"选项指定了输出文件名为"add-addi”。
  • "add-addi.s"是要编译的汇编源文件名。

-wl

-Wl 是一个常用的编译选项,它用于将命令行参数传递给链接器(linker)。在 GCC 编译器中,-Wl 后面通常跟着的是链接器的选项,这些选项可以影响链接过程和生成的可执行文件。

例如,-Wl,-Ttext=0x0 就是一个使用 -Wl 传递的链接器选项,其中 -Ttext=0x0 是链接器的一个具体选项,它告诉链接器将程序的文本段(通常包含程序的代码)的起始地址设置为 0x0。

其他常见的 -Wl 选项包括:

  • -Wl,–start-group 和 -Wl,–end-group:这两个选项用于在链接时指定一个输入文件的顺序,确保这些文件按照指定的顺序被链接。
  • -Wl,-Map:这个选项告诉链接器生成一个映射文件,该文件描述了输入对象和可执行文件之间的映射关系。
  • Wl,-rpath:这个选项用于设置运行时库搜索路径。

在使用 -Wl 选项时,通常需要确保链接器理解并支持你传递的选项。不同的目标平台和操作系统可能会有不同的链接器选项。

std::hex std::setw() std::setfill()各自的用法

std::hex、std::setw() 和 std::setfill() 是 C++ 标准库中的流操作符,用于格式化输出。

std::hex:将后面输出的数据都以十六进制形式显示。它通常与 std::cout 一起使用,例如:

#include <iostream>

int main() {
    int num = 255;
    std::cout << std::hex << num<<123; // 输出 "ff7b"
    return 0;
}

std::setw():设置下一次输出字段的宽度。它通常与 std::left 或 std::right 一起使用来指定对齐方式,但如果没有指定对齐方式,默认是右对齐。例如:

#include <iostream>

int main() {
    int num = 42;
    std::cout << std::setw(5) << num; // 输出 "   42"
    return 0;
}

std::setfill():设置填充字符。它通常与 std::setw() 一起使用,用于在输出字段不足指定宽度时用指定的字符进行填充。例如:

#include <iostream>

int main() {
    int num = 42;
    std::cout << std::setw(5) << std::setfill('0') << num; // 输出 "00042"
    return 0;
}

hexdump

在这里插入图片描述

add与addi的risv编码

在这里插入图片描述
在 RISC-V 中,指令编码格式分为几种类型,其中 addi 指令使用 I-type 格式,而 add 指令使用 R-type 格式。每种类型的指令机器码是32位宽,而且每一位都有特定的作用。

addi (Add Immediate) 指令的 I-type 格式: RISC-V 的 I-type 指令格式如下:

┌───────┬───────┬───────┬─────────────┬───────┬───────┐
│ funct7 │  rs2  │  rs1  │    funct3   │   rd  │ opcode│
├───────┼───────┼───────┼─────────────┼───────┼───────┤
│    立即数 (12 bits)rs1 (5 bits)  │  funct3 |   rd (5 bits)opcode (7 bits) │
└────────────────────────┴─────────────┴────────┴─────────────┘ 

opcode (7 bits): 操作码,对于 addi 是 0010011。
rd (5 bits): 目标寄存器的编号。
funct3 (3 bits): 功能码,对于 addi 是 000。
rs1 (5 bits): 源寄存器1的编号。
立即数 (12 bits): 要加的立即数,它是有符号的,并且在执行时会自动进行符号扩展到32位或更多。
例如,对于指令 addi x5, x10, 15,其二进制编码可能如下所示(只展示了操作相关的二进制位,不包括具体的立即数和寄存器编号):

00000000000011110110000010010011

add (Add) 指令的 R-type 格式: RISC-V 的 R-type 指令格式如下:

┌───────┬───────┬───────┬─────────────┬───────┬───────┐
│ funct7 │  rs2  │  rs1  │    funct3   │   rd  │ opcode│
├───────┼───────┼───────┼─────────────┼───────┼───────┤
│ funct7 (7 bits)rs2 (5 bits)rs1 (5 bits)funct3 (3 bits)rd (5 bits)opcode (7 bits)│
└───────┴───────┴───────┴─────────────┴───────┴───────┘

opcode (7 bits): 操作码,对于 add 是 0110011。
rd (5 bits): 目标寄存器的编号。
funct3 (3 bits): 功能码,对于 add 是 000。
rs1 (5 bits): 源寄存器1的编号。
rs2 (5 bits): 源寄存器2的编号。
funct7 (7 bits): 对于 add,这个字段是 0000000。
例如,对于指令 add x5, x10, x20,其二进制编码可能如下所示(只展示了操作相关的二进制位):

00000001010010100000010110110011

简单CPU工作流程

在现代计算机体系结构中,尤其是遵循冯·诺依曼架构的计算机系统,程序的执行可以分解为几个连续的阶段,这些阶段共同构成了 CPU 的指令周期。这些阶段包括取指(Instruction Fetch, IF)、解码(Instruction Decode, ID)、执行(Execute, EX)、访存(Memory Access, MEM)和写回(Write Back, WB)。这个过程是循环进行的,每个阶段完成特定的任务,确保计算机程序顺利执行。


+--------+   +--------+   +--------+   +--------+   +--------+
| 取指IF  |-->| 解码ID |-->| 执行EX  |-->| 访存MEM |-->| 写回WB |
+--------+   +--------+   +--------+   +--------+   +--------+

在这个示意图中,每行代表 CPU 中的一条指令随时间前进经过不同的处理阶段。每个方框代表流水线的一个阶段,箭头表示指令从一个阶段移动到下一个阶段的流程。这种设计使得在任何给定的时钟周期内,最多可以有五条指令处于不同的执行阶段,极大提高了 CPU 的效率和性能。

  • 取指 IF:从内存中读取指令。
    CPU 使用程序计数器(PC)指向的地址从内存中读取指令。读取后,PC 会更新到下一条指令的地址,为下一个周期准备。
  • 解码 ID:解析指令并准备必要的操作数。
    指令解码器(ID)解析取出的指令,确定需要执行的操作和操作数。这可能涉及到从指令中提取立即数、计算地址、确定寄存器编号等。
  • 执行 EX:执行计算或其他操作。
    执行阶段根据解码阶段的结果,进行相应的算术逻辑运算(ALU 操作)、分支决策或其他操作。此阶段可能需要使用到 ALU(算术逻辑单元)。
  • 访存 MEM:进行内存访问,如数据加载或存储。
    如果指令需要读取内存(如加载操作)或向内存写入数据(如存储操作),这一阶段将完成该操作。这一步是可选的,因为不是所有指令都需要访问内存。
  • 写回 WB:将执行结果写回寄存器。

将执行阶段的结果或访存阶段从内存读取的数据写回到指定的寄存器。这一步确保了指令的执行结果可以被后续指令使用。
这五个阶段共同构成了指令的完整执行周期,是现代 CPU 设计的基础。通过将指令执行分解为这些阶段,计算机能够以高效和有序的方式运行程序。每个阶段都由 CPU 的不同部件负责,使得计算机能够在任何给定时刻执行多条指令的不同阶段,这种设计是流水线处理的基础,极大提高了 CPU 的执行效率。

实现流程

创建 add-addi.s 并写入下面的内容

.global _start
_start:
    addi x29, x0, 100
    addi x30, x0, 200
    add x31, x30, x29

在汇编语言中,.global _start 和 _start: 的语句定义了程序的入口点。

  • .global _start:这条指令告诉链接器(linker),_start 标签是一个全局符号,可以被程序的其他部分或其他链接的文件访问。更重要的是,它标示 _start 为程序的入口点,即程序执行的起始位置。这对于操作系统(OS)来说非常关键,因为在程序被加载到内存并执行时,操作系统需要知道从哪里开始执行程序。

  • _start::这是一个标签,紧跟在它后面的是程序的入口点。在这个位置上编写的指令将会是程序执行的第一批指令。在一个裸机(bare-metal)环境或操作系统内核开发中,_start 是执行流程的起点,没有标准库或运行时环境的初始化过程。

综上所述,.global _start 和 _start: 一起定义了程序开始执行的地方,为操作系统提供了一个明确的起点来运行程序。如果不写的话会报错。

这段代码是使用 RISC-V 汇编语言编写的,它执行了一个非常简单的任务:计算两个数值的和,并将结果存储。具体来说,代码做了以下几件事情:

  • 将数字 100存储到寄存器 x29。
  • 将数字 200 存储到寄存器 x30。
  • 将寄存器 x29 和 x30 中的数值相加,将结果存储到寄存器 x31。

总结来说,这段代码简单地计算了 5 和 37 的和,然后将结果 42 存储到寄存器 x31 中。

将汇编转为二进制文件:

其中addi指令可能会被优化位li指令,此时将编译优化关闭,然后故意设置一些特定的值使得没有优化即可
在这里插入图片描述

字节码

地址低处才是opcode
在这里插入图片描述

在这里插入图片描述

        uint32_t code=static_cast<uint32_t>(recv_dram_addr[pc])
            |static_cast<uint32_t>(recv_dram_addr[pc+1]<<8)
            |static_cast<uint32_t>(recv_dram_addr[pc+2]<<16)
            |static_cast<uint32_t>(recv_dram_addr[pc+3]<<24);
            
        uint32_t opcode=(code)&0x7f;
        uint32_t rd=(code>>7)&0x1f;
        uint32_t funct3=(code>>12)&0x3;
        uint32_t rs1=(code>>15)&0x1f;
        uint32_t rs2=(code>>20)&0x1f;
        uint32_t funct7=(code>>25)&0x7f;
        uint32_t imm=(code>>20)&0xfff;

cpu.cpp

#include<bits/stdc++.h>

class DRAM
{   
    public:

    std::vector<uint8_t>code;

    public:
    
    DRAM(std::vector<uint8_t> paranment)
    {
        code=paranment;
    }
    
};

class CPU    //CPU 从存储器中读取指令,解析指令,然后执行指令
{
    std::array<uint64_t,32> registers={0};
    
    uint64_t pc;
    
    uint32_t ir;

    std::vector<uint8_t> recv_dram_addr;

    public:
    CPU(std::vector<uint8_t>positon)
    {
        pc=0;
        ir=0;
        recv_dram_addr=positon;
    }

    uint32_t get_code()
    {
        
        uint32_t code=static_cast<uint32_t>(recv_dram_addr[pc])
            |static_cast<uint32_t>(recv_dram_addr[pc+1]<<8)
            |static_cast<uint32_t>(recv_dram_addr[pc+2]<<16)
            |static_cast<uint32_t>(recv_dram_addr[pc+3]<<24);
        pc=pc+4;
        return code; //little-endian
    } 
    
    void decode_execute(uint32_t code)
    {
        uint32_t opcode=(code)&0x7f;
        uint32_t rd=(code>>7)&0x1f;
        uint32_t funct3=(code>>12)&0x3;
        uint32_t rs1=(code>>15)&0x1f;
        uint32_t rs2=(code>>20)&0x1f;
        uint32_t funct7=(code>>25)&0x7f;
        uint32_t imm=(code>>20)&0xfff;

        switch(opcode)
        {
            case 0x33: //add
            {
                registers[rd]=registers[rs1]+registers[rs2];
                break;
            }
            case 0x13: //addi
            {
                registers[rd]=registers[rs1]+imm;
                break;
            }
            default:
            {
                std::cout<<"invalid opcode";
                break;
            }
        }
    }

    const std::array<std::string,32>registers_name=
    {
        "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2",
        "s0", "s1", "a0", "a1", "a2", "a3", "a4", "a5",
        "a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7",
        "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6",
    };
    
    void show_registers()
    {
        for(size_t i=0;i<32;i++)
        {
            std::cout<<registers[i];
        }
        
        for(size_t i=0;i<32;i+=4)
        {   
            std::cout<<std::hex
            <<" x"<<std::setw(1)<<std::setfill('0')<<i<<"("<<registers_name[i]<<") "<<registers[i]
            <<" x"<<std::setw(1)<<std::setfill('0')<<i+1<<"("<<registers_name[i+1]<<") "<<registers[i+1]
            <<" x"<<std::setw(1)<<std::setfill('0')<<i+2<<"("<<registers_name[i+2]<<") "<<registers[i+2]
            <<" x"<<std::setw(1)<<std::setfill('0')<<i+3<<"("<<registers_name[i+3]<<") "<<registers[i+3]
            <<std::endl;
        }
    }

    void run()
    {
        while(pc<recv_dram_addr.size())
        {
            uint32_t code=get_code();
            decode_execute(code);
        }
    }

};

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        std::cout<<"please use like \n"<<"./nowfilename <filename>";
        return 0;
    }

    std::ifstream file(argv[1],std::ios::binary);

    std::vector<uint8_t> code(std::istreambuf_iterator<char>(file), {});
    std::cout<<code.size();
    DRAM dram(code);

    CPU cpu(dram.code);
    
    if(!file)
    {
        std::cout<<"can't open "<<argv[1]<<std::endl;
        return 0;
    }

    cpu.run();

    std::cout<<std::setw(50)<<std::setfill('~')<<"\n"<<std::endl;
    cpu.show_registers();
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

看星猩的柴狗

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

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

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

打赏作者

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

抵扣说明:

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

余额充值