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-ANS
里exec
文件夹下的所有文件,导致执行某个程序时一直出错,经过检查发现是左移右移指令未实现EFLAGS
的更新而出错;(5)对于
REG
与reg_l
,MEM_R
与swaddr_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
是跳转相关,可以和jmp
、jcc
、ret
等放在一起)
#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.wanshu
和matrix-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.c
、main
、test_data
等等,这也都是用ascii码来存储的,顺序为.symtab
从上到下的顺序(.symtab
的Name
表示该串从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.c
中add
函数中添加断点,然后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_read
和memset
实现两个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。最近有点忙可能不能很仔细回复,抱歉~