LOADING

加载过慢请开启缓存 浏览器默认开启

NEMU PA2 阶段三(添加变量支持)

2024/9/12

课程介绍&前言

​ 此课程是天津大学智算学部大二24-25第一学期的实践课,名为《计算机系统综合实践》,主要目标是完成一个叫NEMU的模拟器,以加深《计算机系统基础》的学习。

​ 此模拟器运行在乌邦图18.04的Linux操作系统上,模拟了cpu,dram等计算机硬件的行为。

​ 编写此教程的目的,一方面是看到CSDN上面的教程年代久远,一些代码和思路已不再适用,另一方面为了加强自己的学习。由于此博客建立时,我已经 抄到 完成到了PA2第三阶段,故从这里开始写起,前面的应该会补上(大概)。

添加变量支持

理论

elf表示可重定位目标文件和可执行目标文件,是gcc编译器编译并链接之后的产物。

cpp:预处理(包含头文件,宏展开等)

cc1:编译(c源程序翻译为汇编程序)

as:汇编(将汇编程序翻译为机器语言)

下面是elf可重定位目标文件的格式

​ 这个阶段对我们非常重要的就是symtab符号表。通过查阅指导手册对应部分,我们知道了要找OBJECT类型的变量,同时也知道了Name属性存放的是这些变量名字在字符串表(string table)中的偏移量。这是因为对于一个名字,即字符串,程序不知道要留下多大的空间存储比较合适,可能会少,也可能太多了,造成浪费,因此存储一个偏移量。

那么我们要做的,就是先从输出的Section Headers中读取.strab在elf文件的偏移是多少,然后再根据Name在.strab中的偏移量在可执行文件里面找到变量名。(变量名之间是用00隔开的,转化为符号形式就是’\0’直接读取即可,比较方便)

​ 我们可以通过指令来查看ELF文件的16进制形式,注意在输入指令时,查看自己的当前操作目录有没有那个文件。

hd add

具体如何查找请看指导手册。这里再强调一下你看到的符号表里面没有宏和局部变量,有静态变量。这是因为局部变量时存储在栈上面的,他的地址并不固定,而且编译时期也不能确定他的地址。至于宏,在预编译时期已经处理完了,编译器看到的文件是宏展开之后的结果,静态变量是在编译时期就分配好地址的,也可以看到。

然后和你输入的

p 变量名

中的变量名对比,如果成功,打印符号表中的value即可,这里value是一个地址,想要查看变量值,只需要用PA1实现的解引用即可,如果不成功,那就用循环遍历即可。

实现

这里最难的地方应该是从ELF文件里面提取出符号表,字符串表,但是这已经给我们实现完了。

load_elf_tables()(定义在/nemu/src/monitor/debug/elf.c)用来读取符号表和字符串表,

 char *strtab = NULL;
 Elf32_Sym *symtab = NULL;
 int nr_symtab_entry;

本来有static关键字的,我为了写函数方便,直接去掉了,以便在其他文件里面访问。当然也可以在此文件里面实现,在elf.h中声明,直接调用elf.h中的函数(事实上这种方式更加安全,因为保证了封装性)。

这些变量看名字不难理解,然后我们看看Elf32_Sym是什么。(定义在\lib-common\uclibc\include\elf.h)

typedef struct
{
  Elf32_Word	st_name;		/* Symbol name (string tbl index) */
  Elf32_Addr	st_value;		/* Symbol value */
  Elf32_Word	st_size;		/* Symbol size */
  unsigned char	st_info;		/* Symbol type and binding */
  unsigned char	st_other;		/* Symbol visibility */
  Elf32_Section	st_shndx;		/* Section index */
} Elf32_Sym;

typedef uint32_t Elf32_Word;
typedef uint32_t Elf32_Addr;
typedef uint16_t Elf32_Section;

#define STT_NOTYPE	0		/* Symbol type is unspecified */
#define STT_OBJECT	1		/* Symbol is a data object */
#define STT_FUNC	2		/* Symbol is a code object */
#define STT_SECTION	3		/* Symbol associated with a section */
#define STT_FILE	4		/* Symbol's name is file name */
#define STT_COMMON	5		/* Symbol is a common data object */
#define STT_TLS		6		/* Symbol is thread-local data object*/
#define	STT_NUM		7		/* Number of defined types.  */
#define STT_LOOS	10		/* Start of OS-specific */
#define STT_HIOS	12		/* End of OS-specific */
#define STT_LOPROC	13		/* Start of processor-specific */
#define STT_HIPROC	15		/* End of processor-specific */

​ 下面我们研究一下load_elf_tables()

void load_elf_tables(int argc, char *argv[]) {
    int ret;
    Assert(argc == 2, "run NEMU with format 'nemu [program]'");
    exec_file = argv[1];

    FILE *fp = fopen(exec_file, "rb");
    Assert(fp, "Can not open '%s'", exec_file);

    uint8_t buf[sizeof(Elf32_Ehdr)];
    ret = fread(buf, sizeof(Elf32_Ehdr), 1, fp);
    assert(ret == 1);

    /* The first several bytes contain the ELF header. */
    Elf32_Ehdr *elf = (void *)buf;
    char magic[] = {ELFMAG0, ELFMAG1, ELFMAG2, ELFMAG3};

    /* Check ELF header */
    assert(memcmp(elf->e_ident, magic, 4) == 0);		// magic number
    assert(elf->e_ident[EI_CLASS] == ELFCLASS32);		// 32-bit architecture
    assert(elf->e_ident[EI_DATA] == ELFDATA2LSB);		// littel-endian
    assert(elf->e_ident[EI_VERSION] == EV_CURRENT);		// current version
    assert(elf->e_ident[EI_OSABI] == ELFOSABI_SYSV || 	// UNIX System V ABI
            elf->e_ident[EI_OSABI] == ELFOSABI_LINUX); 	// UNIX - GNU
    assert(elf->e_ident[EI_ABIVERSION] == 0);			// should be 0
    assert(elf->e_type == ET_EXEC);						// executable file
    assert(elf->e_machine == EM_386);					// Intel 80386 architecture
    assert(elf->e_version == EV_CURRENT);				// current version


    /* Load symbol table and string table for future use */

    /* Load section header table */
    uint32_t sh_size = elf->e_shentsize * elf->e_shnum;
    Elf32_Shdr *sh = malloc(sh_size);
    fseek(fp, elf->e_shoff, SEEK_SET);
    ret = fread(sh, sh_size, 1, fp);
    assert(ret == 1);

    /* Load section header string table */
    char *shstrtab = malloc(sh[elf->e_shstrndx].sh_size);
    fseek(fp, sh[elf->e_shstrndx].sh_offset, SEEK_SET);
    ret = fread(shstrtab, sh[elf->e_shstrndx].sh_size, 1, fp);
    assert(ret == 1);

    int i;
    for(i = 0; i < elf->e_shnum; i ++) {
        if(sh[i].sh_type == SHT_SYMTAB && 
                strcmp(shstrtab + sh[i].sh_name, ".symtab") == 0) {
            /* Load symbol table from exec_file */
            symtab = malloc(sh[i].sh_size);
            fseek(fp, sh[i].sh_offset, SEEK_SET);
            ret = fread(symtab, sh[i].sh_size, 1, fp);
            assert(ret == 1);
            nr_symtab_entry = sh[i].sh_size / sizeof(symtab[0]);
        }
        else if(sh[i].sh_type == SHT_STRTAB && 
                strcmp(shstrtab + sh[i].sh_name, ".strtab") == 0) {
            /* Load string table from exec_file */
            strtab = malloc(sh[i].sh_size);
            fseek(fp, sh[i].sh_offset, SEEK_SET);
            ret = fread(strtab, sh[i].sh_size, 1, fp);
            assert(ret == 1);
        }
    }

    free(sh);
    free(shstrtab);

    assert(strtab != NULL && symtab != NULL);

    fclose(fp);
}

​ 我们一点一点来看,首先是前面几行。

    int ret;
    Assert(argc == 2, "run NEMU with format 'nemu [program]'");
    exec_file = argv[1];

    FILE *fp = fopen(exec_file, "rb");
    Assert(fp, "Can not open '%s'", exec_file);

    uint8_t buf[sizeof(Elf32_Ehdr)];
    ret = fread(buf, sizeof(Elf32_Ehdr), 1, fp);
    assert(ret == 1);

    /* The first several bytes contain the ELF header. */
    Elf32_Ehdr *elf = (void *)buf;
    char magic[] = {ELFMAG0, ELFMAG1, ELFMAG2, ELFMAG3};

​ 然后是Elf32_Ehdr的定义

typedef struct
{
  unsigned char	e_ident[EI_NIDENT];	/* Magic number and other info */
  Elf32_Half	e_type;			/* Object file type */
  Elf32_Half	e_machine;		/* Architecture */
  Elf32_Word	e_version;		/* Object file version */
  Elf32_Addr	e_entry;		/* Entry point virtual address */
  Elf32_Off	e_phoff;		/* Program header table file offset */
  Elf32_Off	e_shoff;		/* Section header table file offset */
  Elf32_Word	e_flags;		/* Processor-specific flags */
  Elf32_Half	e_ehsize;		/* ELF header size in bytes */
  Elf32_Half	e_phentsize;		/* Program header table entry size */
  Elf32_Half	e_phnum;		/* Program header table entry count */
  Elf32_Half	e_shentsize;		/* Section header table entry size */
  Elf32_Half	e_shnum;		/* Section header table entry count */
  Elf32_Half	e_shstrndx;		/* Section header string table index */
} Elf32_Ehdr;

​ exec_file为用户程序编译来的可执行文件,先打开这个文件,其中”rb”表示以二进制方式读取,然后buf表示读取文件的前面一部分,就是ELF头了。

​ 然后是下面一部分,正如注释所说,就是检查ELF头读取有没有问题

/* Check ELF header */
assert(memcmp(elf->e_ident, magic, 4) == 0);		// magic number
assert(elf->e_ident[EI_CLASS] == ELFCLASS32);		// 32-bit architecture
assert(elf->e_ident[EI_DATA] == ELFDATA2LSB);		// littel-endian
assert(elf->e_ident[EI_VERSION] == EV_CURRENT);		// current version
assert(elf->e_ident[EI_OSABI] == ELFOSABI_SYSV || 	// UNIX System V ABI
        elf->e_ident[EI_OSABI] == ELFOSABI_LINUX); 	// UNIX - GNU
assert(elf->e_ident[EI_ABIVERSION] == 0);			// should be 0
assert(elf->e_type == ET_EXEC);						// executable file
assert(elf->e_machine == EM_386);					// Intel 80386 architecture
assert(elf->e_version == EV_CURRENT);				// current version

​ 下面几行是读取section header table,它的作用是看string table的偏移量,以便在ELF文件中找到string table.

​ fseek是c语言的一个函数,用于移动文件指针到指定位置,这里就是把指针(fp)移到以SEEK_SET为基准,偏移量为elf->e_shoff的地方,而SEEK_SET是一个宏定义,为0,所以意思就是移动到section header table的开始位置。

​ fread用于读取数据,就是把section header table读取到sh指向的区域,一共读取sh_size个。

​ ret用于检验读取是否成功。

/* Load section header table */
uint32_t sh_size = elf->e_shentsize * elf->e_shnum;
Elf32_Shdr *sh = malloc(sh_size);
fseek(fp, elf->e_shoff, SEEK_SET);
ret = fread(sh, sh_size, 1, fp);
assert(ret == 1);

​ 下面是Elf_Shdr的定义:它的类型是Section Table

typedef struct
{
  Elf32_Word	sh_name;		/* Section name (string tbl index) */
  Elf32_Word	sh_type;		/* Section type */
  Elf32_Word	sh_flags;		/* Section flags */
  Elf32_Addr	sh_addr;		/* Section virtual addr at execution */
  Elf32_Off	sh_offset;		/* Section file offset */
  Elf32_Word	sh_size;		/* Section size in bytes */
  Elf32_Word	sh_link;		/* Link to another section */
  Elf32_Word	sh_info;		/* Additional section information */
  Elf32_Word	sh_addralign;		/* Section alignment */
  Elf32_Word	sh_entsize;		/* Entry size if section holds table */
} Elf32_Shdr;

​ 下面的代码好好思考一下就能看懂,elf是ELF头,它里面有一个信息,就是e_shstrndx,即string table的索引值,然后sh[]就是在Section Table中找到了string table.

​ 结合上面section table中的定义,不难想到sh[elf->e_shstrndx].sh_size就是string table的大小,而sh[elf->e_shstrndx].sh_offset就是string table的偏移量。

​ 然后就是读取的string table存入shstrtab,这是一个字符串。注意前面理论我们说了不同的Name是用00也就是’\0’隔开的,这里直接把整个表读入,也包括了00,你要是直接打印shstrtab是不会打印完整的

​ 注意这里的string table 是section header的,我们需要的整个程序的string table还没有读取。

/* Load section header string table */
char *shstrtab = malloc(sh[elf->e_shstrndx].sh_size);
fseek(fp, sh[elf->e_shstrndx].sh_offset, SEEK_SET);
ret = fread(shstrtab, sh[elf->e_shstrndx].sh_size, 1, fp);
assert(ret == 1);

​ 然后接下来读取symbol table.

​ sh[i].sh_type == SHT_SYMTAB就是从section table中找到symbol table那一项。

​ strcmp(shstrtab + sh[i].sh_name, “.symtab”) == 0就是再检查一遍是不是真的symbol table ,sh[i].sh_name是各个选项在string table中的索引值。

​ 如果检查成功,就加载symbol table,存入symtab

​ 如果检测的是string table,那就加载到strtab

int i;
for(i = 0; i < elf->e_shnum; i ++) {
    if(sh[i].sh_type == SHT_SYMTAB && 
            strcmp(shstrtab + sh[i].sh_name, ".symtab") == 0) {
        /* Load symbol table from exec_file */
        symtab = malloc(sh[i].sh_size);
        fseek(fp, sh[i].sh_offset, SEEK_SET);
        ret = fread(symtab, sh[i].sh_size, 1, fp);
        assert(ret == 1);
        nr_symtab_entry = sh[i].sh_size / sizeof(symtab[0]);
    }
    else if(sh[i].sh_type == SHT_STRTAB && 
            strcmp(shstrtab + sh[i].sh_name, ".strtab") == 0) {
        /* Load string table from exec_file */
        strtab = malloc(sh[i].sh_size);
        fseek(fp, sh[i].sh_offset, SEEK_SET);
        ret = fread(strtab, sh[i].sh_size, 1, fp);
        assert(ret == 1);
    }
}

后面就是释放掉不需要的内存了。这个函数我们只要明白什么功能就够了,不用明白咋读的,看的我有点难受。

至此,我们就明白了怎么使用以下三个,来查找变量了

 char *strtab = NULL;
 Elf32_Sym *symtab = NULL;
 int nr_symtab_entry;

typedef struct
{
  Elf32_Word	st_name;		/* Symbol name (string tbl index) */
  Elf32_Addr	st_value;		/* Symbol value */
  Elf32_Word	st_size;		/* Symbol size */
  unsigned char	st_info;		/* Symbol type and binding */
  unsigned char	st_other;		/* Symbol visibility */
  Elf32_Section	st_shndx;		/* Section index */
} Elf32_Sym;

typedef uint32_t Elf32_Word;
typedef uint32_t Elf32_Addr;
typedef uint16_t Elf32_Section;

我的思路是在nemu/src/monitor/debug/expr.c中实现,直接访问这三个,才把static去掉了。

我首先在enum中再添加一个变量OBJECT来指定type的值为变量值

enum {
    NOTYPE = 256,NUMBER = 6,EQ = 2,ZUO = 3,YOU = 4,REG = 5,HEX = 1,NOTEQ = 7,AND = 8,OR = 9,NOT = 10,FU = 11,JIE = 12,OBJECT = 13,
    /* TODO: Add more token types */
};

然后在rules中添加规则:

{"[a-zA-Z_][a-zA-Z0-9_]*", OBJECT},

注意这个一定要先提取NUMBER,再提取HEX否则可能会有问题。

make_token()中就按照前面添加即可,我在最后面贴出我的代码。

接下来我是在expr()里面实现的,for循环遍历,如果识别到了OBJECT,那就找变量,把变量给tokens成员str[],把type改成NUMBER,然后计算即可,如果没有找到,就直接return 0 ,报告错误。我这么写是不想直接结束程序,不然万一打错一个字,程序结束了,还要从头开始单步执行跳过,有点难受。不过要是直接结束程序,用panic()也没有什么不妥。

for (i = 0; i < nr_token; i++) {
        if (tokens[i].type == OBJECT) {
            char temp_qaq[32];
            strncpy(temp_qaq, &tokens[i].str[0], how_long_x[num_x]);
            temp_qaq[how_long_x[num_x++]] = '\0';
            int qwq = 0;
            bool pipei = false;
            for (; qwq < nr_symtab_entry; qwq++) {
                if ((symtab[qwq].st_info & 0xf) == STT_OBJECT){
                    char tmp[30];
                    int tmplen = symtab[qwq + 1].st_name - symtab[qwq].st_name - 1;
                    strncpy(tmp, strtab + symtab[qwq].st_name, tmplen);
                    tmp[tmplen] = '\0';
                    if (strcmp(temp_qaq, tmp) == 0) {
                        pipei = true;
                        tokens[i].type = NUMBER;
                        long long object_ = symtab[qwq].st_value;
                        sprintf(tokens[i].str, "%lld", object_);
                        break;
                    }
                }
            }
            if (!pipei) {
                printf("wrong name!");
                return 0;
            }
        }
    }

我首先把变量的那个字符串复制到了temp_qaq[]数组,并在最后添加’\0’。STT_OBJECT是一个宏定义,为1。tem为symbol table中的变量名复制到的数组。

如果找到OBJECT类型的token,那就遍历symbol table,找可以匹配的变量即可。

这是我的作业远程仓库(链接等这一届完事再给),NEMU全部作业都在这里。

补充

2024.9.13

尝试运行了一下学校上自动评测的样例:以obj/testcase/add为ENTRY,运行指令:

p *(test_data + 12)

发现程序出现了缓冲区溢出的运行时错误,通过定位错误,发现是expr中处理token类型为OBJECT时有错误

就是上边的for循环,当识别出symtabl中的类型是STT_OBJECT之后的处理不当。

int tmplen = symtab[qwq + 1].st_name - symtab[qwq].st_name - 1;

这一行如果qwq此时就是symtabl中的最后一个,那么symtab[qwq + 1].st_name的值就是0,因此会造成tmplen < 0,导致溢出(因为strncpy的函数原型中的那个长度n是一个无符号整型,传入负值,将会自动类型转换为一个很大的无符号数,这是因为有符号数使用的是补码编码)。

我的解决方法是,既然这些Name在string table中已经用’\0’隔开了,那么我们不妨就不用再用char数组了,直接用char*

char *tmp;
tmp = strtab + symtab[qwq].st_name;

这样if (strcmp(temp_qaq, tmp) == 0)也可以正常运行。