记录一下做汇编代码模拟器的过程,根据“程序=状态机”的视角,模拟实现了汇编程序的执行过程,下面就开始进入正题吧。
第一部分: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, ®.rdx,NULL},
{REG, 0, 0, ®.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来进行解析.