|
公告: 此文档翻译自Marat Fayzullin的主页,原文著作权为原作者所有.
中文翻译,则为译者 张
研[zhyan@usa.net]所有.欢迎传阅,但不可作为商业用途!
我写这个文档是由于收到了很多人的email,这些人想写一个或另一个计算机模 拟器又不知从那里开始.以下的文档中的任何意见和建议都是我的个人见解,有 可能并不绝对正确.这个文档主要包括被称为"解释(interpreting)"的模拟器, 它恰 好和"编译(compiling)"相反.我在重编译技术上没有太多的经验.这个文 档中提供了一两个地方,在那里你可以找到关于这些技术的信息.
如果你认为这个文档遗漏了一些东西或者想要更正,有时间的时候可以把你的 见解email给我.但是,我不回答白痴和那些要求提供ROM映像的请求.在这个文 档的末尾我遗漏了一些重要的FTP/WWW地址,所以如果你知道一些有价值的 地址,请告诉我.同样,一些常见问题也没有包含在这个文档中.
这个文档由Bero, 翻译成日文 .
while(CPUIsRunning)
{
Fetch OpCode
Interpret OpCode
}
一个很明显的缺点是性能. 这种解释(interpretation)消耗很多的CPU 时间, 并且你可能需要相当快的计算机才能在满意的速度上运行你的 代码.
+ 一般来说, 允许处理更快的代码. + CPU寄存器可以被直接用于存储被模拟的CPU的寄存器. + 很多被模拟CPU的操作码(opcodes)可以被模拟成CPU类似的操作码(opcodes) - 代码不能被移植,例如它不能在结构不同的计算机上运行. - 调试和维护代码很困难.
+ 代码可以被移植,这样就可以工作在不同的计算机和操作系统上. + 调试和维护代码相对容易. + 硬件是怎样真正工作的不同假设可以很快的被测试. - C 一般来说比纯汇编代码慢.熟练掌握选择的语言对写一个工作的模拟器是绝对必要的,因为它是一个复杂的项目,并且你的代码应该优化得尽可能的快. 计算机模拟器肯定不是学习一门语言的项目.
典型的例子:
comp.sys.msx MSX/MSX2/MSX2+/TurboR 计算机 comp.sys.sinclair Sinclair ZX80/ZX81/ZXSpectrum/QL计算机 comp.sys.apple2 Apple ][ 计算机 etc.
为了那些想要写他们自己的CPU模拟器的核心或者对它怎样工作感兴趣的人,我下面给出一个用C语 言写的一个典型的CPU模拟器的框架.在真正的模拟器中你可能要跳过一些部分并且增加你自己的东 西.
Counter=InterruptPeriod;
PC=InitialPC;
for(;;)
{
OpCode=Memory[PC++];
Counter-=Cycles[OpCode];
switch(OpCode)
{
case OpCode1:
case OpCode2:
...
}
if(Counter<=0)
{
/* Check for interrupts and do other */
/* cyclic tasks here */
...
Counter+=InterruptPeriod;
if(ExitRequired) break;
}
}
首先, 我们给CPU周期计数器(COUNTER)和程序计数器(PC)定义初始值:
Counter=InterruptPeriod; PC=InitialPC;Counter 包含留给下一个估计的中断处理的CPU周期数. 注意当这个计数器到指定值时中断 不必发生: 你能把它用于很多其他目的, 诸如同步时钟, 或者更新屏幕上的扫描线. 更多 的会在后面提到. PC包含我们模拟的CPU将要读取的下一条操作码的存储器地址.
初始化值定义以后,我们开始主循环:
for(;;)
{
注意这循环也可以这样执行
while(CPUIsRunning)
{
这里CPUIsRunning 是一个布尔变量.
这有很多优点,因为你可以通过设置CPUIsRunning=0在
任何时候终止这个循环. 不幸的是每一遍检查这个变量时会消耗一些CPU
时间,如果可能的 话应该避免.同样,
不要这样执行这个循环
while(1)
{
因为在这种情况下, 一些编译器会生成检查1是真是假的代码.
你当然不想编译器在每次循 环都做这些无用的工作.
现在,当我们在循环里的时候,第一件事就是读下一个操作码, 并且修改程序计数器:
OpCode=Memory[PC++];注意这是最简单最快速地从存储器读操作码的方法, 但这不总是切实可行的.一个更普遍的 访问存储器的方法包含在后面的文档中.
操作码被读取之后,我们要根据这个操作码需要的周期,减少CPU周期计数值:
Counter-=Cycles[OpCode];Cycles[] 数组应该包含每条操作码的CPU周期数. 注意一些操作码(诸如条件跳转或者 子程序调用)消耗的周期数依赖于它们的参数. 但这以后可以在代码上被调整.
现在到解释操作码并执行它的时候了:
switch(OpCode)
{
switch()结构通常被误解为效率很低,
因为它编译进入一个if() ... else if() ...
语 句链内. 对于少量的case语句结构来说确实是这样的,
对于数量多的 (100-200 或者更多)
cases 通常编译成跳转表, 显然这使得它们效率很高.
有两个替换的方法来解释操作码. 第一种方法是产生一个函数表并且调用合适的函数. 这 种方法显然比switch()结构效率低, 因为在函数调用的时候增加了间接开销. 第一种方法 是产生一个标号表, 并且使用goto语句. 这种方法比switch()结构稍微快一些, 它只能工 作在支持"预计标号(precomputed labels)"的编译器上. 其他的编译器不允许创建一个标 号地址的数组.
成功地解释并执行了一条操作码之后, 是检查是否需要一个中断的时候了. 在这个时候, 你同样能执行任何需要和系统时钟同步的任务:
if(Counter<=0)
{
/* Check for interrupts and do other hardware emulation here */
...
Counter+=InterruptPeriod;
if(ExitRequired) break;
}
在以后的文档中包含了这些周期任务的内容.
注意我们不能简单地定义Counter=InterruptPeriod, 而是作一个 Counter+=InterruptPeriod: 这是周期计数更精确, 因为在Counter中的周期计数可能是负数.
同样, 看看这行.
if(ExitRequired) break;由于在每个循环通过时检查是否退出太浪费CPU时间了, 所以我们只在Counter到指定值时检查: 当你设置ExitRequired=1时, 将退出模拟器.但着不会消耗太多的CPU时间.
Data=Memory[Address1]; /* Read from Address1 */ Memory[Address2]=Data; /* Write to Address2 */但是由于下面的原因这种简单的访问存储器的方法并不总是可能的:
Data=ReadMemory(Address1); /* Read from Address1 */ WriteMemory(Address2,Data); /* Write to Address2 */所有的特殊处理诸如页访问,镜像, I/O 处理, 等等., 在这些函数内部处理.
ReadMemory() 和 WriteMemory() 通常在模拟器上带来间接的开销,因为它们被频繁地调用. 所以, 它们必须被作成尽可能的t高效. 这是一个这些函数访问分页地址空间的例子:
static inline byte ReadMemory(register word Address)
{
return(MemoryPage[Address13][Address&0x1FFF]);
}
static inline void WriteMemory(register word Address,register byte Value)
{
MemoryPage[Address13][Address&0x1FFF]=Value;
}
注意inline 关键字.
它告诉编译器把函数嵌入到代码当中去, 而不是产生对它的调用.
如果编 译器不支持inline
或者 _inline, 试着产生static型函数:
一些编译器 (例如WatcomC)会通过
内嵌优化short static函数.
同样, 记住在大多数情形下ReadMemory()被调用的频率比WriteMemory()被调用的频率更高. 因 此, 在WriteMemory()执行大多数代码值得的, 留给ReadMemory()的代码尽可能的短小.
2500000/50 = 50000 CPU 周期现在, 如果我们假定整个屏幕(包括VBlank) 是256条扫描线高并且它们中的212条确实显示在 屏幕上(例如: 其他44条落入VBlank), 我们得到,你的模拟器必须每次刷新一条扫描线
50000/256 ~= 195 CPU 周期然后, 你应该生成一个VBlank中断,什么也不做直到完成了VBlank, 例如:
(256-212)*50000/256 = 44*50000/256 ~= 8594 CPU 周期小心的计算每个任务需要的CPU周期, 然后在InterruptPeriod里使用最小数并且让其他任务 依赖它(它们不必在每次Counter到指定值的时候都执行).
Watcom C++ -oneatx -zp4 -5r -fp3 GNU C++ -O3 -fomit-frame-pointer Borland C++如果你发现了其中的一个编译器或不同的编译器的更好的选项组合,请让我知道..
1997-1998 Copyright by Marat Fayzullin [fms@cs.umd.edu]
| Member of LinkUnion - Click Here to Join |