linux y86模拟器使用,Y86指令集(模拟器篇-下篇)

接上篇,我们从“中间环境”开始,分析模拟器实现中一些关键的部分。

中间环境与单步循环

中间环境中,ESP指向reg数组,换一种说法就是reg数组的一部分变成了运行时栈。这个部分如下:

1 yr_cc = 0x8, // Non-standard: Flags, ZF SF OF

2 yr_rey = 0x9, // Non-standard: Y return address

3 yr_rex = 0xA // Non-standard: X return address

需要切换到中间环境时,执行如下代码。其中MM1存储内部环境的ESP而MM2指向reg[yr_rex]。

1 movd %esp, %mm1

2 movd %mm2, %esp

3 call (%esp)

从中间环境返回内部环境后,只需要从MM1中还原ESP就可以继续程序的执行了。

中间环境中执行的代码,首先是记录Flags,并保存EAX中的值,以便之后的代码使用这个寄存器:

1 y86_check:

2 pushf

3 movd %eax, %mm3

然后检查位于MM7的状态码。如果状态是ys_aok(正常运行),那么继续,否则跳转到之后的代码,按状态码进行相应的处理:

1 y86_check_1:

2 movd %mm7, %eax

3 testl %eax, %eax

4 jnz y86_int

然后是存储在MM6的步骤数。为了方便计算,我们从步骤总数(yr_sx)开始,每执行一步减1,到0时就应该退出了:

1 y86_check_2:

2 movd %mm6, %eax

3 decl %eax

4 movd %eax, %mm6

最后,还原EAX和Flags,返回内部环境:

1 y86_call:

2 movd %mm3, %eax

3 popf

4 ret

另外,代码库中有一个“max版”,在y86sim.c的基础上去除了中间环境。“max版”执行Y86程序时的性能几乎和对应的原生x86程序相同,不过它不能按步数停机,也不包含内存地址等的检查。

特例

在程序执行过程中,会出现一些特殊的情形,是翻译后的x86程序无法直接处理的:

访问不存在的内存地址,抛出地址错误;

写入到不存在的内存地址;

写入Y86机器码,修改程序自身;

ret指令跳转到不存在或没有被翻译过的地址。

因此,遇到上面这些情形时,程序会通过一些特殊的状态码来标记它们,并暂时离开Y86的执行过程(从内部环境切换到中间环境、外部环境)。

在实现中,前两个情形可以合并,即“内存地址合法性检查”。我们为它分配了一个状态码ys_ima。

它发生在需要访问内存的指令rmmovl、mrmovl、pushl、popl、call执行前:

1 y86_int_ima:

2 movd %mm4, %eax

3 andl $Y_MASK_NOT_MEM, %eax

4 jnz y86_int_brk

5 xorl %eax, %eax

6 movd %eax, %mm7

7 jmp y86_call

第三个情形对应状态码ys_imc,模拟器为了方便实现,又加了一条约定,认为Y86机器码只存在于内存中靠前部的区域。

对于在Y86机器码范围内进行的写入,我们需要切换到外部环境中处理。

它发生在rmmovl、pushl、call执行后:

1 y86_int_imc:

2 movd %mm4, %eax

3 cmpl 16(%esp), %eax

4 jg y86_check_2

5 jmp y86_fin

在外部环境中,会重新把修改后的Y86指令翻译成x86机器码,然后继续程序的执行。

我们对ret指令分配一个单独的状态码ys_ret。遇到ret指令时,程序会直接切换到外部环境,用普通的模拟器的方式执行这条指令。

指令翻译

计算、数据移动等指令会被直接翻译为对应的x86指令。

跳转指令会将跳转目标压入中间环境的堆栈中,然后借用环境切换过程完成跳转。“max版”中是直接跳转。

涉及内存访问和堆栈操作的指令需要对地址加上或减去mem数组的偏移。为了避免破坏Flags,地址运算主要通过lea指令完成。

“max版”中的call和ret直接使用x86的指令,在停机后再将内存、寄存器中所有x_inst地址范围内的值转换成对应的Y86机器码地址。这个实现是不严谨的,但实际使用中很少产生问题。

还有一个很容易被忽视的细节。

在指令翻译中,需要注意Y86机器码是有分割歧义的。这很像中文分词——“中外科学名著作品大全”如果从第二个字开始解读,就会变成“外科/学名/著作品/大全”,这也是通顺的。

说到中文的分割,我不禁想起了书名最后一个字被遮住的那本——

b28561aa7e419d5466308358f4f27b4a.png

《动词大词典》……(图片来源:@蓝雪枫)

我们来看一个Y86中的例子,从位置0、1、2、3、4开始,机器码可以代表完全不同的指令序列:

1 70 00 20 20 10

2 ^ jmp 0x10202000

3 ^ nop ; mov %edx, %eax ; hlt

4 ^ mov %edx, %eax ; hlt

5 ^ mov %ecx, %eax

6 ^ hlt

因此我们需要借助x_map来解决这个问题。

翻译程序每次遇到跳转到未知地址的jmp指令,都会将x_map标记为一个特殊的值。一段程序翻译完成后会扫描一遍x_map,如果存在被标记的还没有翻译的位置,就会继续从这个位置开始翻译。如果翻译过程中遇到了x_map已经存在的地址(也就是已经翻译的),则会直接插入一个跳转并结束翻译。

遇到错误指令时,考虑到指令并不一定会被执行到,因此会就地插入一个类似halt的指令并返回错误对应的状态码。这样,只有执行到相应位置时,程序才会返回错误状态。

模拟器的实现中还有更多的细节,限于篇幅就不详细梳理了。有兴趣的读者可以到GitHub仓库查看模拟器和汇编器的实现代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值