NEMU PA2实验思路

PA2实验思路

版权归zzy所有,不许外传!

2023.9.11更新 感谢各位学弟学妹的支持~本人博客为:zzy991212的博客,pa3的密码为:pa3ans,但是我当时偷懒了没有写完hhhh。
再次感谢大家支持!

本文主要是提供PA2思路,为了避免踩了一堆坑而浪费时间。若想copy代码请移步他处,本文仅供学习交流用,谢谢!

阅读前请确保仔细阅读了PA2实验指导书的有关内容!

TIP

Q:为什么HIT BAD TRAP了?

A:这是我的一些总结,当然因人而异了。

(1)未仔细阅读i386手册以及勘误手册,导致某个jcc命令的判断条件的&&||写错;

(2)call指令和ret指令跳转地址时出现错误,导致$eip无法跳转到正确的地址;

(3)未能仔细理解有关string的指令的实现,导致经常地址溢出或者程序卡死;

(4)未复制PA1-ANSexec文件夹下的所有文件,导致执行某个程序时一直出错,经过检查发现是左移右移指令未实现EFLAGS的更新而出错;

(5)对于REGreg_lMEM_Rswaddr_read等混用,导致有些内容写入读入错误;

(6)关于有符号DATA_TYPE和无符号DATA_TYPE_S类型的使用出错。

必做任务1 运行用户程序 mov-c

特别注意:如果不使用PA1-ANS,先将PA1-ANS里的nemu/src/cpu/exec覆盖自己的!

1.按照指导书,更改Makefile,将运行文件换成mov-c.c

2.make run,然后输入c,发现报错:

invalid opcode(eip = 0x0010000a): e8 06 00 00 00 b8 00 00

3.结合obj\testcase\mov-c.txt找报错的那行的opcode,实现它就能跑过这个地方(比如上面的e8

4.查i386手册第十七章,找到对应的指令名的对应的opcode,将其形式以:instr_XXX_X填入exec.c的表中(具体格式见PA2实验指导书,instr为指令名称)

5.创建instr.h文件,文件内容模仿已有的其他的文件即可;

6.创建instr.c文件,将所有_v后缀的形式填入make_helper_v(...)中,其他形式模仿已有文件;

7.创建instr-template.h文件,若该指令译码方式存在(具体形式看decode相关文件),只需要填入make_instr_helper中,再实现do_execute完成实现函数即可;若译码方式不存在,自己写make_helper函数(模仿mov-template.h)实现译码+实现功能,最后return eip的偏移量。

8.在all-instr.h里引用文件;

9.make run,按c查看是否过了刚刚的地方,否则用PA1实现的监视点或在文件里printf查相关值看看是否按要求变化。

10.有些涉及eflags寄存器的实现,需要按照指导书要求加入,格式在手册里有。

eg: 下面以e8这个第一个指令为例:

1.找到eip = 0x0010000a的内容,发现是:

10000a:	e8 06 00 00 00       	call   100015 <main>

2.查手册的call指令的位置,找到e8的含义:

E8 cw CALL rel16 ......
...
E8 cd CALL rel32 ......

3.在exec.c的表e8处填写指令名称:call_i_v(v表示该指令可以是16位或32位)

4.创建call.h,填写如下:(可以根据指令类型创新的文件夹存该指令实现文件,比如call是跳转相关,可以和jmpjccret等放在一起)

#ifndef __CALL_H__
#define __CALL_H__

make_helper(call_i_v);

#endif

5.创建call.c,编写如下:

#include "cpu/exec/helper.h"

#define DATA_BYTE 1
#include "call-template.h"
#undef DATA_BYTE

#define DATA_BYTE 2
#include "call-template.h"
#undef DATA_BYTE

#define DATA_BYTE 4
#include "call-template.h"
#undef DATA_BYTE

make_helper_v(call_i)

6.创建call-template.h,根据该指令的含义实现。比如该指令(CALL rel16/32)表示的意思是:跳转到下一条指令的首地址加偏移量的位置,比如mov-c.txt中为10000f + 6 = 100015。该指令需要自己编写make_helper函数。

#include "cpu/exec/template-start.h"

#define instr call

make_helper(concat(call_i_, SUFFIX)) {
    //根据SUFFIX进行译码,len为取出来的参数的字节数
    int len = concat(decode_i_, SUFFIX)(cpu.eip + 1);
    //堆栈操作
    reg_l(R_ESP) -= DATA_BYTE;
    swaddr_write(reg_l(R_ESP), 4, cpu.eip + len + 1);
    //imm为偏移量的值
    DATA_TYPE_S imm = op_src -> val;
    //打印指令
    print_asm("call\t%x",cpu.eip + 1 + len + imm);
    // 当前地址eip加上偏移量
    cpu.eip += imm;
    //返回该call指令总长度,最后会在外面再加上该值,正好满足指令:eip跳到下一条指令位置加上偏移量的位置
    return len + 1;
} 

#include "cpu/exec/template-end.h"

call指令的实现不一定一定要与上面相同,只需要与ret函数实现同步即可

7.在all-instr.h里引用文件,添加一条:

#include "call.h放的文件夹名/call.h"

8.make run,按c查看是否过了刚刚的地方,过了就说明该条call指令能顺利通过。(能顺利通过不代表这个一定写对了)

关于EFLAGS寄存器的实现

查i386手册,查看关于各个位的信息,看完后在reg.h中实现。

实现如下,注意顺序(写在CPU_STATE里):

union{
    struct{
        uint32_t CF:	1;
        uint32_t :		1;
        uint32_t PF:	1;
        uint32_t :		1;
        uint32_t AF:	1;
        uint32_t :		1;
        uint32_t ZF:	1;
        uint32_t SF:	1;
        uint32_t TF:	1;
        uint32_t IF:	1;
        uint32_t DF:	1;
        uint32_t OF:	1;
        uint32_t IOPL:	2;
        uint32_t NT:	1;
        uint32_t :		1;
        uint32_t RF:	1;
        uint32_t VM:	1;
        uint32_t :		14;
    };
    uint32_t EFLAGS;
};

然后需要在restart()函数中置初值,初值在手册中有写,此处略过,自行完成。

test指令实现如下(主要是更改EFLAGS位),需要理解各个位的含义!

#include "cpu/exec/template-start.h"

#define instr test

static void do_execute () {
    DATA_TYPE ret = op_dest -> val & op_src -> val;
    cpu.SF = ret >> ((DATA_BYTE << 3) - 1);
    cpu.ZF = !ret;
    cpu.CF = 0;
    cpu.OF = 0;
    ret ^= ret >> 4;
    ret ^= ret >> 2;
    ret ^= ret >> 1;
    ret &= 1;
    cpu.PF = !ret;
    print_asm_template2();
}

make_instr_helper(r2rm)

#include "cpu/exec/template-end.h"

其余指令自行完成,然后就能HIT GOOD TRAP了!

必做任务 2:实现更多指令

注意事项:

1.一些指令需要更新elfags,需要自己添加,主要是逻辑运算指令和算术运算指令,找下面这个注释来添加:

/* TODO: Update EFLAGS.*/

2.千万别实现所有的指令再进行测试!建议先把需要实现的基本指令全部实现,然后再跑一个程序实现一条指令(比如jcc很多,没必要全都实现,碰到再加就行)。全部实现再运行debug代价十分高!!跑另一个程序只需要改Makefile文件即可。

指导书:经过 PA1 的训练之后, 你应该不会实现所有指令之后才进行测试了。

翻译:不会吧不会吧,不会真有人写完所有指令再测试吧

3.wanshumatrix-mul运行时间很长,其中matrix-mul运行需要占2G内存,千万不要因为空间小了导致虚拟机炸了(炸了虚拟机导致两行泪的我)。每次运行完执行make clean清空缓存。

4.在.c文件中加断点可以方便debug,只需在需要断的地方加set_bp()

必做任务 3:实现 binary scaling

该部分需要实现浮点数的定点化相关函数,看完了指导书,应当大部分实现不难,比如F2int函数只需要截取定点数前几位即可:

static inline int F2int(FLOAT a) {
	int ret = a & 0xffff0000;
	return ret >> 16;
}

int2F只需要把整数部分放到高16位即可:

static inline FLOAT int2F(int a) {
	return a << 16;
}

唯一难的是f2F函数,因为涉及到浮点数的相关知识,以下给出参考,需要自行回忆浮点数转化的知识。

FLOAT f2F(float a) {
	/* You should figure out how to convert `a' into FLOAT without
	 * introducing x87 floating point instructions. Else you can
	 * not run this code in NEMU before implementing x87 floating
	 * point instructions, which is contrary to our expectation.
	 *
	 * Hint: The bit representation of `a' is already on the
	 * stack. How do you retrieve it to another variable without
	 * performing arithmetic operations on it directly?
	 */

	int b = *(int *)&a;
	int sign = b & 0x80000000;
	int exp = (b >> 23) & 0xff;
	int last = b & 0x7fffff;	
	
	if(exp == 255) {
		if (sign) return -0x7fffffff;
		else return 0x7fffffff;
	}
	
	if(exp == 0) return 0;

	last |= 1 << 23;
	exp -= 134;	
	if (exp < 0) last >>= -exp;
	if (exp > 0) last <<= exp;

	if (sign) return -last;else return last;
}

另外,lib-common/FLOAT/Makefile.part 的编写在网上没有现成的例子,直接替换成下面即可:

# This file will be included by the Makefile under the project directory.

FLOAT_O := $(FLOAT:.a=.o)
FLOAT_VFPRINTF_O := $(dir $(FLOAT))FLOAT_vfprintf.o

FLOAT_A_OBJ := $(FLOAT_O) $(FLOAT_VFPRINTF_O)

$(FLOAT): $(FLOAT_A_OBJ)
	ar r $@ $^

# TODO: complete the following rules

FLOAT_CFLAGS := -O2 -m32 -fno-builtin -fno-stack-protector -D_FORTIFY_SOURCE=0 -march=i386 -mtune=i386

$(FLOAT_O):
	mkdir -p obj/lib-common/FLOAT
	$(CC) $(FLOAT_CFLAGS) -c lib-common/FLOAT/FLOAT.c -o $(FLOAT_O) -Ilib-common

$(FLOAT_VFPRINTF_O):
	mkdir -p obj/lib-common/FLOAT
	$(CC) $(FLOAT_CFLAGS) -c lib-common/FLOAT/FLOAT_vfprintf.c -o $(FLOAT_VFPRINTF_O) -Ilib-common

之后按照指导书的要求改相关的东西,就可以通过那两个测试用例了。

必做任务 4:为表达式求值添加变量的支持

看完指导书,可以知道我们需要做的就是需要得到某个程序中的某个字符串的值(如*test_data,表示该数组第一个值)。所以我们需要现在expr.c中添加对这种字符串的正则分解的支持:

{"\\b[a-zA-Z0-9_]+\\b",MARK}

然后如何查询它的值呢?当然是通过上面的elf文件来查询!字符串表在elf文件的.strtab段中,其中从该地址(off)往后偏移size长度均为该测试用例的全局变量的值,包括add.cmaintest_data等等,这也都是用ascii码来存储的,顺序为.symtab从上到下的顺序(.symtabName表示该串从off地址第几个开始)。

需要注意的是,因为我们要识别例如test_data而不是main,需要判断.symtab的类型是否为OBJECT

以下给出伪代码:

uint32_t GetMarkValue(char* str){
	int i;
	for (i = 0; i < nr_symtab_entry; i++){
		if (symtab表中该项为OBJECT){
			取出str相同长度的串,并判断其是否相等,相等返回其VALUE
		}
	}
	return 0;
}

关于如何判断表项类型以及如何取值,在elf.c中有给出symtab的定义,跳转至lib-common/uclibc/include/elf.h中查看,便可知晓判断的方式。

仔细查看elf.h中的宏,它很有用!

关于如何测试:运行add后若输入p *(test_data + 4 * i)
若输出的值和test_data[i]相同,即为正确。

选做任务 1:打印栈帧链

实现bt指令,即需要通过$ebp找到每次进入的函数的名称以及前四个参数,并按顺序输出。

首先,将地址在elf表中查询当前所处的函数段,并得到函数名称,这个过程和上一个任务很相似,就不再说明。通过一步步回溯找到$ebp的旧值即可找到经历的所有函数。参数通过指导书上的栈帧链的图应该也很容易找到。

下面是我实现的大致过程,一些部分未给出,相信你一定能写出来(斜眼笑)。当然,不用下面这种写法也可以,实现的方法有很多的。

PartOfStackFrame EBP;
char name[32];
int cnt = 0;
EBP.ret_addr = cpu.eip;
swaddr_t addr = cpu.ebp;
int i;
while (addr){
    GetFunctionAddr(EBP.ret_addr,name);
    if (name[0] == '\0') break;
    printf("#%d\t0x%08x\t",cnt++,EBP.ret_addr);
    printf("%s",name);
    EBP.prev_ebp = ???
    EBP.ret_addr = ???
    printf("(");
    for (i = 0;i < 4;i ++){
        EBP.args[i] = ???
        printf("0x%x",EBP.args[i]);
        if (i == 3) printf(")\n");else printf(", ");
    }
    addr = EBP.prev_ebp;
}

关于如何测试的问题,在add.cadd函数中添加断点,然后c一次打印一次,测试每次是否参数发生了变化以及是否函数调用正确。

必做任务 5:实现 loader

首先,先看完指导书,指导书要求要改的几个地方一定要记得改!

嗯,kernel/src/elf/elf.c文件是个关键。它需要改什么?

读入ramdisk

再仔细看看,漏了点什么?

嗯。。。需要改魔数!(指导书写了,并且注意到这个应该就不会忘了)

/* TODO: fix the magic number with the correct one */

魔数是能够识别该段的一段数,你可以理解为身份证于你的功能,它在文件的最开头声明。利用你PA1实现的扫描内存的功能去完成它吧!(你需要知道elf文件从何处开始的,这就够了)

然后读入ramdisk该如何实现呢?还是在elf.h里找到Elf32_Ehdr的定义,所有为LOAD类型的内容都需要加载,最后只需要用ramdisk_readmemset实现两个TODO即可。关于地址起点、偏移量、大小等信息都在elf.h中的声明中有详细说明。

实现完成后,使用make test即可测试所有程序!

选做任务2:扭转乾坤: 改变程序的行为

这个部分挺难的。。。学有余力的同学可以尝试一波。

按照指导书说的一步步做,对于每个选做任务都仔细思考,最终就能实现代码劫持了。涉及的内容主要是修改某个地址上的值,使得读取不了浮点数相关的opcode,并且变成读取你想要的内容,再实现format_FLOAT()的内容,最终实现代码劫持。

选做任务的题答案如下,供参考:(反汇编后每行指令的地址因人而异,若抄袭很明显!)

选做任务:知己知彼

答:首先找到_fpmaxtostr的调用位置:

8048ea2:	e8 66 f6 ff ff       	call   804850d <_fpmaxtostr>

这段代码附近应当就是该部分的汇编代码,大致范围经分析后如下:

 8048e80:	db 2a                	fldt   (%edx)
 8048e82:	eb 02                	jmp    8048e86 <_vfprintf_internal+0x2ea>
 8048e84:	dd 02                	fldl   (%edx)
 8048e86:	53                   	push   %ebx
 8048e87:	53                   	push   %ebx
 8048e88:	68 35 8b 04 08       	push   $0x8048b35
 8048e8d:	8d 84 24 a4 00 00 00 	lea    0xa4(%esp),%eax
 8048e94:	50                   	push   %eax
 8048e95:	83 ec 0c             	sub    $0xc,%esp
 8048e98:	db 3c 24             	fstpt  (%esp)
 8048e9b:	ff b4 24 8c 01 00 00 	pushl  0x18c(%esp)
 8048ea2:	e8 66 f6 ff ff       	call   804850d <_fpmaxtostr>
 8048ea7:	83 c4 20             	add    $0x20,%esp
 8048eaa:	85 c0                	test   %eax,%eax
 8048eac:	0f 88 82 01 00 00    	js     8049034 <_vfprintf_internal+0x498>
 8048eb2:	01 44 24 08          	add    %eax,0x8(%esp)
 8048eb6:	e9 63 01 00 00       	jmp    804901e <_vfprintf_internal+0x482>

选做任务:知己知彼(2)

1.调用_fpmaxtostr()的参数是如何压栈的?

答:函数的参数采用从右到左压栈方式压栈。

2.变量 argptr 存放在哪一个寄存器中?

答:根据以下汇编代码得出,在$esp中。

 8048e98:	db 3c 24             	fstpt  (%esp)
3.变量 argptr 指向的内容是什么?

答:根据题2,指向的内容是$esp的地址的值,即传入的浮点数。

4.变量 stream 的地址在哪里?

答:应当为 0x18c + $esp

 8048e9b:	ff b4 24 8c 01 00 00 	pushl  0x18c(%esp)
5.调用_fpmaxtostr()返回后会清理栈上若干字节的内容, 这若干字节都有些什么内容?

答:调用_fpmaxtostr()返回后,$esp += 0x20,即抹除了$esp向上32个byte的内容。

这32个byte分别为:文件流(4byte)、浮点数的值(4byte)、_fpmaxtostr()分配的栈空间(12byte)、ppfs->info的地址(4byte)、常量$0x8048b35(4byte)、基地址寄存器$ebx的值(4byte)。

这一部分要写的话可能要写好多。。。由于我太懒了。。有问题的话私戳我吧


PA2就此完结!当然你一定会碰到各种HIT BAD TRAP的问题。。(犹记得我改一个地方改了一晚上)尝试自己debug,如果实在找不到错可以问问身边的dalao们!

另:有任何问题欢迎私戳我,QQ:1978750368。最近有点忙可能不能很仔细回复,抱歉~

  • 21
    点赞
  • 146
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值