PA2:指令系统
实验目的:
掌握i386(IA-32)指令格式。
掌握NEMU平台的指令周期。
运行用户程序
任务:编写几条指令的helper函数, 使得第一个简单的C程序可以在NEMU中运行起来。
流程:
make run运行NEMU。
按c执行程序。
查看报错信息,观察出现未知指令的内存地址和对应操作码。
编写对应的指令文件xxx-template.h,xxx.h,xxx.c。
在all-instr.h中添加对应的头文件。
在exec.c中对应位置添加操作名称。
添加call指令
查找call指令对应的Opcode等信息,可知操作码为e8。
call指令
编写call-template.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "cpu/exec/template-start.h"
#define instr call
// call + 相对偏移量(视为一个立即数)
make_helper ( concat ( call_i_ , SUFFIX )) {
// 解码偏移量(立即数)
int len = concat ( decode_i_ , SUFFIX )( cpu . eip + 1 );
// 压栈操作 (push)
reg_l ( R_ESP ) -= DATA_BYTE ;
// eip寄存器的下一位地址写入esp (mov)
MEM_W ( reg_l ( R_ESP ), cpu . eip + len + 1 );
// 加上立即数偏移量,实现跳转
cpu . eip += ( DATA_TYPE_S ) op_src -> val ;
// 更新eip寄存器
print_asm ( "call: 0x%x" , cpu . eip + len + 1 );
return len + 1 ;
}
// call + 寄存器
make_helper ( concat ( call_rm_ , SUFFIX )) {
// 解码寄存器/内存
int len = concat ( decode_rm_ , SUFFIX )( eip + 1 );
reg_l ( R_ESP ) -= DATA_BYTE ;
MEM_W ( reg_l ( R_ESP ), cpu . eip + len + 1 );
cpu . eip = ( DATA_TYPE_S ) op_src -> val - len - 1 ;
print_asm ( "call: %s" , op_src -> str );
return len + 1 ;
}
#include "cpu/exec/template-end.h"
call指令示意
编写call.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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 )
make_helper_v ( call_rm )
编写call.h
1
2
3
4
5
6
7
#ifndef __CALL_H__
#define __CALL_H__
make_helper ( call_i_v );
make_helper ( call_rm_v );
#endif
在nemu/src/cpu/exec/all-instr.h中包含call.h,并在nemu/src/cpu/exec/exec.c中的opcode_table中填写相应的helper函数。
添加test指令
test指令
test指令其实相当于进行AND按位与操作,但是test是不会改变操作数的,只会改变EFLAGS寄存器中的标志位。
标志位
名称
功能
CF
进位标志
如果运算的结果最高位产生了进位或借位,其值为1,否则为0。
PF
奇偶标志
计算运算结果里1的奇偶性,偶数为1,否则为0。
ZF
零标志
相关指令结束后判断是否为0,结果为0,其值为1,否则为0。
SF
符号标志
相关指令结束后判断正负,结果为负,其值为1,否则为0。
I
中断使能标志
表示能否响应外部中断,若能响应外部中断,其值为1,否则为0。
DF
方向标志
当DF为1,ESI、EDI自动递减,否则自动递增。
OF
溢出标志
反映有符号数运算结果是否溢出,如果溢出,其值为1,否则为0。
编写test-template.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "cpu/exec/template-start.h"
#define instr test
static void do_execute () {
// 两个操作数进行与运算
DATA_TYPE result = op_dest -> val & op_src -> val ;
cpu . eflags . CF = 0 ;
cpu . eflags . OF = 0 ;
// 在/nemu/src/cpu/eflags.c中定义的函数
// 可根据结果自动修改eflags寄存器的值
update_eflags_pf_zf_sf (( DATA_TYPE_S ) result );
print_asm_template1 ();
}
make_instr_helper ( i2a )
make_instr_helper ( i2rm )
make_instr_helper ( r2rm )
#include "cpu/exec/template-end.h"
程序中经常使用一些缩写,并已成为约定俗成的惯例。
例如,为了方便理解,通常把to用2代替。因此i2rm表示立即数传到寄存器/内存中,r2rm表示寄存器传到寄存器/内存中。
i18n与此类似,是internationalization的缩写,表示“国际化”。
编写test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "cpu/exec/helper.h"
#define DATA_BYTE 1
#include "test-template.h"
#undef DATA_BYTE
#define DATA_BYTE 2
#include "test-template.h"
#undef DATA_BYTE
#define DATA_BYTE 4
#include "test-template.h"
#undef DATA_BYTE
make_helper_v ( test_i2a )
make_helper_v ( test_i2rm )
make_helper_v ( test_r2rm )
编写test.h
1
2
3
4
5
6
7
8
9
10
11
12
#ifndef _TEST_H_
#define _TEST_H_
make_helper ( test_i2a_b );
make_helper ( test_i2rm_b );
make_helper ( test_r2rm_b );
make_helper ( test_i2a_v );
make_helper ( test_i2rm_v );
make_helper ( test_r2rm_v );
#endif
添加je指令
je指令
添加je指令有两种方法,一种是参考普通指令的执行方式,用static void do_execute创建函数并使用make_instr_helper();另一种是直接定义一个新的make_jcc_helper()宏。这里采用后者,因为这样所有条件跳转如jne,jg,jge等都可以用make_jcc_helper()的宏实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "cpu/exec/template-start.h"
// 条件跳转指令译码过程复杂,make_instr_helper无法使用,需要重新定义
#define make_jcc_helper(cc) \
make_helper(concat4(j, cc, _, SUFFIX)) { \
int len = concat(decode_si_, SUFFIX)(eip + 1); \
print_asm(str(concat(j,cc)) " %x",cpu.eip+op_src->val+1+len+(DATA_BYTE == 4)); \
if (concat(check_cc_, cc)()) { \
cpu.eip += op_src->val; \
} \
return len + 1; \
}
#include "cpu/exec/template-end.h"
在include/cpu/eflags.h中定义比较函数:
1
2
3
static inline bool check_cc_e () {
return cpu . eflags . ZF == 1 ;
}
这样在调用make_je_helper时就会使用check_cc_e函数判断是否更新eflags寄存器的值。
添加其他指令
push指令
push指令
操作流程:
① esp寄存器值减4,表示栈顶指针下移一个地址的长度。
② 将读入的数据写入地址中,表示数据入栈。
cmp指令
cmp指令
操作流程:
① 记录源操作数-目标操作数的值。
② 用所得的值更新eflags寄存器。
pop指令
pop指令
操作流程:
① 向译码出的对象操作数中写入栈顶数据。
② 栈顶指针加4,回到push前的状态。
ret指令
ret指令
操作流程:
① 使eip跳转到esp中存放的地址处。
② 栈顶指针加4。
实现更多指令
jbe指令
在jcc-template.h指令中添加be字段,并在eflags.h中补充check_cc_be函数。
leave指令
leave指令
操作流程:
① 使esp指向ebp(栈底)所指的位置。
② ebp指向esp所存的地址。
③ 栈顶指针加4。
add指令
实现浮点数定点化
什么是定点化
我们约定最高位为符号位,接下来的15位表示整数部分,低16位表示小数部分,即约定小数点在第15和第16位之间(从第0位开始)。从这个约定可以看到,FLOAT类型其实是实数的一种定点表示。
更通俗的解释是:定点化数的小数点位置是固定的;或者说,定点化数的小数位数是确定的。
定点数
由这个定义,可以得出以下结论:
对于一个实数a,它的FLOAT类型表示A = a * 2^16 = a << 16。
定点数的运算
在lib-common/FLOAT.h中定义定点数与整型的基本运算:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static inline int F2int ( FLOAT a ) {
// 将定点数转换为整型
return ( a >> 16 );
}
static inline FLOAT int2F ( int a ) {
// 将整型转换为定点数
return ( a << 16 );
}
static inline FLOAT F_mul_int ( FLOAT a , int b ) {
// 定点数与整型相乘
return a * b ;
}
static inline FLOAT F_div_int ( FLOAT a , int b ) {
// 定点数与整型相除
return a / b ;
}
在lib-common/FLOAT/FLOAT.c中定义浮点数到定点数的转化和定点数的运算:
定义浮点数结构
1
2
3
4
5
6
7
8
9
// 接收IEEE编码的浮点数
typedef union {
struct {
uint32_t m : 23 ; // 尾数位
uint32_t e : 8 ; // 指数位
uint32_t s : 1 ; // 符号位
};
uint32_t val ;
} Float ;
将浮点数转化为定点数
回忆浮点数的计算公式:对于一段32位长的浮点数编码,它对应的浮点数值为:$V = (-1)^s \times M \times 2^E$
其中 $E = 1 - bias$,$M = 1 + f$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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?
*/
Float f ;
void * temp = & a ;
f . val = * ( uint32_t * ) temp ; // 联合体val接收IEEE编码
uint32_t m = f . m | ( 1 << 23 ); // 为尾数位添加1
// 计算偏移量
int shift = ( int ) f . e - ( 127 + 23 - 16 );
// 对m执行偏移操作
if ( shift > 0 ) {
m <<= shift ;
}
else {
m >>= ( - shift );
}
return ( __sign ( f . val ) ? - m : m );
}
浮点数转定点数
实现定点数的运算
运行integral和quadratic-eq,补充缺失指令
为简易调试器增加变量支持
在nemu/src/monitor/debug/elf.c文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//sym是我们需要匹配的符号名称,success指针用于设置是否匹配成功
uint32_t look_up_symtab ( char * sym ){
int i ;
//遍历符号表逐个匹配符号
for ( i = 0 ; i < nr_symtab_entry ; i ++ ){
//逐个提取符号信息中的符号类别
uint8_t type = ELF32_ST_TYPE ( symtab [ i ]. st_info );
//当遇到类别为FUNC或者OBJECT时候匹配符号名
if (( type == STT_FUNC || type == STT_OBJECT ) && strcmp ( strtab + symtab [ i ]. st_name , sym ) == 0 ){
//匹配成功后返回符号的地址
return symtab [ i ]. st_value ;
}
}
printf ( "No sym found" );
return 0 ;
}
实现kernel加载
kernel/src/elf/elf.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...
/* TODO: fix the magic number with the correct one */
//修改为elf文件的魔数
const uint32_t elf_magic = 0x464c457f ;
uint32_t * p_magic = ( void * ) buf ;
nemu_assert ( * p_magic == elf_magic );
/* Load each program segment */
//初始化ph指向program header开头,buf指向elf文件的开头,e_phoff为program header偏移
ph = ( void * ) buf + elf -> e_phoff ;
//eph指向program header的末尾,e_phnum为program header中segment的数量
//遍历program header表,加载需要加载的segment
for ( Elf32_Phdr * eph = ph + elf -> e_phnum ; ph < eph ; ph ++ ) {
/* Scan the program header table, load each segment into memory */
if ( ph -> p_type == PT_LOAD ) {
uint32_t addr = ph -> p_vaddr ; //存储segment加载到的目标地址
/* TODO: read the content of the segment from the ELF file
* to the memory region [VirtAddr, VirtAddr + FileSiz)
*/
//利用函数从当前segment中读取filesiz大小的数据到目标地址
ramdisk_read (( void * ) addr , ELF_OFFSET_IN_DISK + ph -> p_offset , ph -> p_filesz );
/* TODO: zero the memory region
* [VirtAddr + FileSiz, VirtAddr + MemSiz)
*/
//通过函数将未初始化的数据置0
memset (( void * ) addr + ph -> p_filesz , 0 , ph -> p_memsz - ph -> p_filesz );
...
进入PA3 →