从零开始实现一个汇编模拟器——初

960 阅读7分钟

记录一下做汇编代码模拟器的过程,根据“程序=状态机”的视角,模拟实现了汇编程序的执行过程,下面就开始进入正题吧。

第一部分:CPU

在状态机的视角下,CPU看作多个寄存器的全部状态,因而可以用一个struct来保存所有寄存器的值。同时,CPU还有MMU来实现虚拟内存向物理内存的切换(目前暂时不实现MMU的功能,而是简单的将虚拟地址取模得到物理地址)。

register.h

#ifndef _REGISTER_
#define _REGISTER_

#include <stdlib.h>
#include <stdint.h>
// 真正拥有的只有CPU和Memory.离散.状态机

typedef struct REG_STRUCT
// 这是对CPU的模拟
{
    union 
    {
        struct
        {
            uint8_t al;
            uint8_t ah;
        };
        uint16_t ax;
        uint32_t eax;
        uint64_t rax;
    };
    
    // 后面rbx..也是一样的写法,这边就偷懒不弄了.
    uint64_t rbx;
    uint64_t rcx;
    uint64_t rdx;
    uint64_t rsi;
    uint64_t rdi;
    uint64_t rbp;
    uint64_t rsp;

    uint64_t rip;
} reg_t;//register type

reg_t reg;

#endif

mmu.h

#ifndef _MMU_
#define _MMU_

// mmu:memory management unit 内存管理单元
// 实现虚拟地址到物理地址的翻译
#include <stdint.h>

uint64_t va2pa(uint64_t vaddr);


#endif

mmu.c

#include "cpu/mmu.h"
#include "memory/dram.h"

uint64_t va2pa(uint64_t vaddr)
{
    return vaddr % MM_LEN;
}

第二部分:内存

物理内存可以看作一个固定长度的数组,指令可以可以看作是操作符+操作数1+操作数2的状态机。

instruction.h

#ifndef _INSTRUCTION_
#define _INSTRUCTION_

#include <stdlib.h>
#include <stdint.h>

#define NUM_INSTRTYPE 30 //OP的数量

typedef enum OP
{
    mov_reg_reg,        //0
    mov_reg_mem,        //1
    mov_mem_reg,        //2
    push_reg,           //3
    pop_reg,            //4
    call,               //5
    ret,                //6
    add_reg_reg         //7
} op_t;


typedef enum OD_TYPE
{
    EMPTY,          
    IMM,            //立即数寻址:Imm
    REG,            //寄存器寻址:R[reg]
    MM_IMM,         //绝对寻址 :M[Imm]
    MM_REG,         //间接寻址:M[R[reg]]
    MM_IMM_REG,     //(基址+偏移量)寻址:M[Imm+R[reg]]
    MM_REG1_REG2,   //变址寻址:M[R[reg1]+R[reg2]]
    MM_IMM_REG1_REG2,   //变址寻址:M[Imm+R[reg1]+R[reg2]]
    MM_REG2_S,      //比例变址查询:M[R[reg]*S]
    MM_IMM_REG2_S,  //比例变址查询:M[Imm+R[reg]*S]
    MM_REG1_REG2_S, //比例变址查询:M[R[reg1]+R[reg2]*S]
    MM_IMM_REG1_REG2_S  //比例变址查询:M[Imm+R[reg1]+R[reg2]*S]
} od_type_t;

typedef struct OD
// 这是对操作数的模拟
{
    od_type_t type;

    int64_t imm;     //立即数
    int64_t scal;    //乘数
    uint64_t *reg1;  //寄存器1
    uint64_t *reg2;  //寄存器2
// 最复杂的是imm(reg1,reg2,scal)
} od_t;

typedef struct INSTRUCT_STRUCT
// 这是对指令的模拟
{
    op_t op;    // 操作符operator 例如mov push ..
    od_t src;   // 操作数operand
    od_t dst;   // 操作数operand
    char code[100];
} inst_t;



typedef void (*handler_t)(uint64_t,uint64_t); //handler是void*(uint64_t,uint64_t)的函数指针

handler_t handler_table[NUM_INSTRTYPE]; //函数指针数组

void init_handler_table();  //对指令集初始化

void instruction_cycle();//指令周期

void add_reg_reg_handler(uint64_t src, uint64_t dst);

void mov_reg_reg_handler(uint64_t src, uint64_t dst);

#endif

instruction.c

 // 实现译码.
#include "memory/instruction.h"
#include "cpu/mmu.h"
#include "cpu/register.h"
#include "memory/instruction.h"
#include <stdio.h>
 
static uint64_t decode_od(od_t od)
{
    if (od.type == IMM)
    {
        return *((uint64_t*)&od.imm );
    }
    else if (od.type == REG)
    {
        return (uint64_t)od.reg1;
    }
    else
    {
        // main memory
        uint64_t vaddr = 0;

        if (od.type == MM_IMM)
        {
            vaddr = od.imm;
        }
        else if (od.type == MM_REG)
        {
            vaddr = *(od.reg1);
        }
        else if (od.type == MM_IMM_REG)
        {
            vaddr = od.imm + *(od.reg1);
        }
        else if (od.type == MM_REG1_REG2)
        {
            vaddr = *(od.reg1) + *(od.reg2);
        }
        else if (od.type == MM_IMM_REG1_REG2)
        {
            vaddr = *(od.reg1) + *(od.reg2) + od.imm;
        }
        else if (od.type == MM_REG2_S)
        {
            vaddr = (*(od.reg2)) * od.scal;
        }
        else if (od.type == MM_IMM_REG2_S)
        {
            vaddr = (*(od.reg2)) * od.scal + od.imm;
        }
        else if (od.type == MM_REG1_REG2_S)
        {
            vaddr = (*(od.reg2)) * od.scal + *(od.reg1);
        }
        else if (od.type == MM_IMM_REG1_REG2_S)
        {
            vaddr = od.imm + (*(od.reg2)) * od.scal + *(od.reg1);
        }

        return va2pa(vaddr);
    
    }
    
}

void instruction_cycle()
{
    /* while (1) {从PC位置取指令->执行指令->更新PC}*/

    inst_t *instr = (inst_t *)reg.rip;

    uint64_t src = decode_od(instr->src);
    uint64_t dst = decode_od(instr->dst);
    // 解码成值value

    handler_t handler = handler_table[instr->op];
    handler(src ,dst);

    printf("    %s\n",instr->code);
}

void init_handler_table()
{
    handler_table[mov_reg_reg] = &mov_reg_reg_handler;
    handler_table[add_reg_reg] = &add_reg_reg_handler;
}

void mov_reg_reg_handler(uint64_t src,uint64_t dst)
{
    *(uint64_t *)dst = *(uint64_t *)src;
    reg.rip = reg.rip + sizeof(inst_t);
}

void add_reg_reg_handler(uint64_t src, uint64_t dst)
{
    *(uint64_t *)dst += *(uint64_t *)src;
    reg.rip = reg.rip + sizeof(inst_t);
}

dram.h

#ifndef _DRAM_
#define _DRAM_

// 这个dram.h是为后面的虚拟内存以及Cache准备.

#include <stdint.h>

#define MM_LEN 1000 // 物理内存就规定了1000个字节

uint8_t mm[MM_LEN]; //physical memory
/*
禁止直接读这个内存(因为实际是通过IO桥..才读到)
要通过其他的函数来读这个物理内存
*/

// virtual address  = 0 ~ 0xffffffffffffffff
// physical address = 000 ~ 999

uint64_t read64bits_dram (uint64_t paddr);  //从内存读数据
void     write64bits_dram(uint64_t paddr,uint64_t data);    //从内存写数据

void print_stack();
void print_register();
#endif

dram.c

#include "memory/dram.h"
#include "cpu/register.h"
#include <stdio.h>
#include "cpu/mmu.h"

#define SRAM_CACHE_SETTING 0 //设置为0,关闭cache功能,设置为1开启cache

uint64_t read64bits_dram (uint64_t paddr)//从内存读数据
{
    if (SRAM_CACHE_SETTING == 1)
    {
        return 0x0;
    }

    uint64_t val = 0x0;

    val += ( ( (uint64_t)mm[paddr + 0] ) << 0 );
    val += ( ( (uint64_t)mm[paddr + 1] ) << 8 );
    val += ( ( (uint64_t)mm[paddr + 2] ) << 16 );
    val += ( ( (uint64_t)mm[paddr + 3] ) << 24 );
    val += ( ( (uint64_t)mm[paddr + 4] ) << 32 );
    val += ( ( (uint64_t)mm[paddr + 5] ) << 40 );
    val += ( ( (uint64_t)mm[paddr + 6] ) << 48 );
    val += ( ( (uint64_t)mm[paddr + 7] ) << 56 );

    return val;
}

void     write64bits_dram(uint64_t paddr,uint64_t data)//从内存写数据
{
    if (SRAM_CACHE_SETTING == 1)
    {
        return ;
    }

    mm[paddr + 0] = (data >> 0)  & 0xff;
    mm[paddr + 1] = (data >> 8)  & 0xff;
    mm[paddr + 2] = (data >> 16) & 0xff;
    mm[paddr + 3] = (data >> 24) & 0xff;
    mm[paddr + 4] = (data >> 32) & 0xff;
    mm[paddr + 5] = (data >> 40) & 0xff;
    mm[paddr + 6] = (data >> 48) & 0xff;
    mm[paddr + 7] = (data >> 56) & 0xff;
}

void print_register()
{
    printf("rax = %16lx\trbx = %16lx\trcx = %16lx\trdx = %16lx\n",
        reg.rax, reg.rbx, reg.rcx, reg.rdx);
    printf("rsi = %16lx\trdi = %16lx\trbp = %16lx\trsp = %16lx\n",
        reg.rsi, reg.rdi, reg.rbp, reg.rsp);
    printf("rip = %16lx\n", reg.rip);
}

void print_stack()
{
    int n = 10;  

    uint64_t *high = (uint64_t*)&mm[va2pa(reg.rsp)];
    high = &high[n];

    uint64_t rsp_start = reg.rsp + n * 8;
    
    for (int i = 0; i < 2 * n; ++ i)
    {
        uint64_t *ptr = (uint64_t *)(high - i);
        printf("0x%016lx : %16lx", rsp_start, (uint64_t)*ptr);

        if (i == n)
        {
            printf(" <== rsp");
        }

        rsp_start = rsp_start - 8;

        printf("\n");
    }
}

第三部分:磁盘

由于该模拟区分了虚拟内存和物理内存,故需要区分磁盘和内存。程序在加载前处于磁盘后,加载后映射到物理内存去执行。

elf.h

#ifndef _ELF_
#define _ELF_

#include <stdlib.h>
#include <stdint.h>
#include "memory/instruction.h"

/* begin:模拟一个instruct */
#define INST_LEN 100

inst_t program[INST_LEN];
/* end:模拟一个instruct*/

#endif

code.c

#include <stdlib.h>
#include "elf.h"
#include "cpu/register.h"



inst_t program[INST_LEN] = 
{
    // main entry point
    {   
        mov_reg_reg,
        {REG, 0, 0, &reg.rdx,NULL},
        {REG, 0, 0, &reg.rsi,NULL},
        "mov   \%rdx,\%rsi\n"
    },
    {
        mov_reg_reg,
        {REG, 0}
    }
    // 后面还有很多的,但是太多了,暂时就写一个
};

第四部分:main

这里main其实是想做成一个验证程序,作为一个test来判断该模拟器是否能实现预期的功能,根据程序=状态机的视角,给定一个初始状态,让程序运行固定次数,事先判断好正确的结束状态,然后比较即可。(目前就只是能够解析mov reg1,reg2的代码,验证为正确.)

main.c

// 做一个汇编的代码模拟器

#include "cpu/register.h"
#include "cpu/mmu.h"

#include "memory/instruction.h"
#include "memory/dram.h"

#include "disk/elf.h"

#include <stdio.h>
#include <stdint.h>


int main()
{
    
    init_handler_table();

    // init
    reg.rax = 0x12340000;
    reg.rbx = 0x0;
    reg.rcx = 0x8000660;
    reg.rdx = 0xabcd;
    reg.rsi = 0x7ffffffee2f8;
    reg.rdi = 0x1;
    reg.rbp = 0x7ffffffee210;
    reg.rsp = 0x7ffffffee1f0;
    
    reg.rip = (uint64_t)&program[11];

    write64bits_dram(va2pa(0x7ffffffee210), 0x08000660);    // rbp
    write64bits_dram(va2pa(0x7ffffffee208), 0x0);
    write64bits_dram(va2pa(0x7ffffffee200), 0xabcd);
    write64bits_dram(va2pa(0x7ffffffee1f8), 0x12340000);
    write64bits_dram(va2pa(0x7ffffffee1f0), 0x08000660);    // rsp

    print_register();
    print_stack();

    // run inst

    reg.rip = (uint64_t)program;    //自己加的,因为我code就写了一个,所以必须指向这个第一个指令.

    for (int i = 0; i < 1; i ++)
    {
        instruction_cycle();
        print_register();
        print_stack();
    }
    


    // verify

    int match = 1;

    match = match &&   (reg.rax == 0x1234abcd);
    match = match &&   (reg.rbx == 0x0);
    match = match &&   (reg.rcx == 0x8000660);
    match = match &&   (reg.rdx == 0x12340000);
    match = match &&   (reg.rsi == 0xabcd);
    match = match &&   (reg.rdi == 0x12340000);
    match = match &&   (reg.rbp == 0x7ffffffee210);
    match = match &&   (reg.rsp == 0x7ffffffee1f0);

    if (match == 1)
    {
        printf("register match\n");
    }
    else
    {
        printf("register not match\n");
    }
    

    match = match && (read64bits_dram(va2pa(0x7ffffffee210)) == 0x08000660); 
    match = match && (read64bits_dram(va2pa(0x7ffffffee208)) == 0x1234abcd); 
    match = match && (read64bits_dram(va2pa(0x7ffffffee200)) == 0xabcd);
    match = match && (read64bits_dram(va2pa(0x7ffffffee1f8)) == 0x12340000);
    match = match && (read64bits_dram(va2pa(0x7ffffffee1f0)) == 0x08000660); 

    if (match == 1)
    {
        printf("memory match\n");
    }
    else
    {
        printf("memory not match\n");
    }
    

    return 0;
}


makefile

# makefile像是一个脚本,帮我们在bash上用gcc命令封装起来
CC = /usr/bin/gcc-9
CFLAGS = -Wall -g -O2 -Werror -std=gnu99

EXE = program # 目标生成的文件

SRC = ./src #路径

CODE = ./src/./memory/instruction.c ./src/disk/code.c ./src/memory/dram.c ./src/cpu/mmu.c ./src/main.c #main最好放在最后 


.PHONY:program
main:
	$(CC) $(CFLAGS) -I$(SRC) $(CODE) -o $(EXE)
	
	# -I表示将include目录设置在当前目录,好处就是main.c里的
	# #include "cpu/register.h" 就可以直接找到了那个头文件
run:
	./$(EXE)

make main + make run => 得到结果如下

kkbabe@ubuntu:~/ym/ass$ make run
./program 
rax =         12340000	rbx =                0	rcx =          8000660	rdx =             abcd
rsi =     7ffffffee2f8	rdi =                1	rbp =     7ffffffee210	rsp =     7ffffffee1f0
rip =     560bb9e36860
0x00007ffffffee240 :                0
0x00007ffffffee238 :                0
0x00007ffffffee230 :                0
0x00007ffffffee228 :                0
0x00007ffffffee220 :                0
0x00007ffffffee218 :                0
0x00007ffffffee210 :          8000660
0x00007ffffffee208 :                0
0x00007ffffffee200 :             abcd
0x00007ffffffee1f8 :         12340000
0x00007ffffffee1f0 :          8000660 <== rsp
0x00007ffffffee1e8 :                0
0x00007ffffffee1e0 :                0
0x00007ffffffee1d8 :                0
0x00007ffffffee1d0 :                0
0x00007ffffffee1c8 :                0
0x00007ffffffee1c0 :                0
0x00007ffffffee1b8 :                0
0x00007ffffffee1b0 :                0
0x00007ffffffee1a8 :                0
    mov   %rdx,%rsi

rax =         12340000	rbx =                0	rcx =          8000660	rdx =             abcd
rsi =             abcd	rdi =                1	rbp =     7ffffffee210	rsp =     7ffffffee1f0
rip =     560bb9e360e0
0x00007ffffffee240 :                0
0x00007ffffffee238 :                0
0x00007ffffffee230 :                0
0x00007ffffffee228 :                0
0x00007ffffffee220 :                0
0x00007ffffffee218 :                0
0x00007ffffffee210 :          8000660
0x00007ffffffee208 :                0
0x00007ffffffee200 :             abcd
0x00007ffffffee1f8 :         12340000
0x00007ffffffee1f0 :          8000660 <== rsp
0x00007ffffffee1e8 :                0
0x00007ffffffee1e0 :                0
0x00007ffffffee1d8 :                0
0x00007ffffffee1d0 :                0
0x00007ffffffee1c8 :                0
0x00007ffffffee1c0 :                0
0x00007ffffffee1b8 :                0
0x00007ffffffee1b0 :                0
0x00007ffffffee1a8 :                0
register not match
memory not match

该指令为mov %rdx,%rsi。 可以看到一开始:rdx=abcd,rsi=7ffffffee2f8,该指令结束后:rdx=abcd,rsi=abcd 可见该指令可以正确的被解析和执行.

至此,就验证了我们的整个代码框架的正确性,剩下的只是实现一些预留的功能以及增加更多的handler来进行解析.