这篇文章讲述下 EU 是如何执行 mov、jmp、add 这几个指令。
前面说过 EU 的执行流程如下:
func (e *EU) run() {
var instructions []byte
for {
// 从BIU获取一字节指令
e.biuCtrl <- FetchInstruction
instruction := byte(<-e.biuData)
// 拼接指令
instructions = append(instructions, instruction)
// 解码当前的指令字节序列
decodedInstructions := Decode(instructions)
// 当前指令字节序列是一条完整的指令
if decodedInstructions != nil {
// 执行指令
e.execute(decodedInstructions)
// 清空指令字节序列
instructions = instructions[:0]
// 如果要求程序终止,则退出循环
if e.stop {
e.stop = false
break
}
}
}
}
在解码到一条完整的指令后,它调用 execute 方法执行指令:
func (e *EU) execute(instructions []byte) {
// 指令类型
instruction := instructions[0]
e.currentInstruction = instruction
switch instruction {
case InstructionMov:
e.executeMov(instructions[1:])
case InstructionAdd, InstructionOr, InstructionAdc, InstructionSbb,
InstructionAnd, InstructionSub, InstructionXor, InstructionCmp:
e.executeAddEtc(instructions[1:])
case InstructionInc, InstructionDec, InstructionNot, InstructionNeg,
InstructionMul, InstructionImul, InstructionDiv, InstructionIdiv:
e.executeIncEtc(instructions[1:])
case InstructionSegPrefix:
e.executeSegPrefix(instructions[1:])
case InstructionPush:
e.executePush(instructions[1:])
case InstructionPop:
e.executePop(instructions[1:])
case InstructionJmp:
e.executeJmp(instructions[1:])
case InstructionCall:
e.executeCall(instructions[1:])
case InstructionRet:
e.executeRet(instructions[1:])
case InstructionLoop:
e.executeLoop(instructions[1:])
case InstructionInt:
e.executeInt(instructions[1:])
case InstructionNop:
e.executeNop(instructions[1:])
default:
log.Fatal("unsupported inssss---")
}
}
它根据中间指令的第一字节——指令类型,来调用相应的方法执行。
mov 指令的执行
执行 mov 指令时,调用 executeMov 方法,参数是中间指令的后续字节:
指令详细类型,[源操作数],[目的操作数]
来看看它是怎么实现的:
func (e *EU) executeMov(instructions []byte) {
length := len(instructions)
switch instructions[0] {
case MovReg8ToReg8: //[]byte{MovReg8ToReg8, AH, AL}
src := e.readReg8(instructions[1])
e.writeReg8(instructions[2], src)
case MovReg16ToReg16: //[]byte{MovReg16ToReg16, SP, CX}
src := e.readReg16(instructions[1])
e.writeReg16(instructions[2], src)
case MovReg8ToMemory: //[]byte{MovReg8ToMemory, CL, 0b00000}
src := e.readReg8(instructions[1])
e.writeDataMemmoryByte(e.calEffectiveAddr(instructions[2:]), src)
case MovReg16ToMemory: //[]byte{MovReg16ToMemory, CX, 0b10111, 0, 1}
src := e.readReg16(instructions[1])
e.writeDataMemmoryWord(e.calEffectiveAddr(instructions[2:]), src)
case MovMemoryToReg8: //[]byte{MovMemoryToReg8, 0b10111, 0, 1, CL}
src := e.readDataMemmoryByte(e.calEffectiveAddr(instructions[1 : length-1]))
e.writeReg8(instructions[length-1], src)
case MovMemoryToReg16: //[]byte{MovMemoryToReg16, 0b10111, 0, 1, CX}
src := e.readDataMemmoryWord(e.calEffectiveAddr(instructions[1 : length-1]))
e.writeReg16(instructions[length-1], src)
case MovRegToSeg: //[]byte{MovRegToSeg, CX, CS}
src := e.readReg16(instructions[1])
e.writeSeg(instructions[2], src)
case MovMemoryToSeg: //[]byte{MovMemoryToSeg, 0b10111, 0, 1, CS}
src := e.readDataMemmoryWord(e.calEffectiveAddr(instructions[1 : length-1]))
e.writeSeg(instructions[length-1], src)
case MovSegToReg: //[]byte{MovSegToReg, CS, CX}
src := e.readSeg(instructions[1])
e.writeReg16(instructions[2], src)
case MovSegToMemory: //[]byte{MovSegToMemory, CS, 0b10111, 0, 1}
src := e.readSeg(instructions[1])
e.writeDataMemmoryWord(e.calEffectiveAddr(instructions[2:]), src)
case MovImmediateToReg8: //[]byte{MovImmediateToReg8, 1, CL}
e.writeReg8(instructions[2], instructions[1])
case MovImmediateToReg16: //[]byte{MovImmediateToReg16, 1, 0, CX}
val := uint16(instructions[2])<<8 | uint16(instructions[1])
e.writeReg16(instructions[3], val)
case MovMemoryToAL: //[]byte{MovMemoryToAL, 1, 0}
effectiveAddr := uint16(instructions[2])<<8 | uint16(instructions[1])
src := e.readDataMemmoryByte(effectiveAddr)
e.writeReg8(AL, src)
case MovMemoryToAX: //[]byte{MovMemoryToAX, 1, 0}
effectiveAddr := uint16(instructions[2])<<8 | uint16(instructions[1])
src := e.readDataMemmoryWord(effectiveAddr)
e.writeReg16(AX, src)
case MovALToMemory: //[]byte{MovALToMemory, 1, 0}
effectiveAddr := uint16(instructions[2])<<8 | uint16(instructions[1])
src := e.readReg8(AL)
e.writeDataMemmoryByte(effectiveAddr, src)
case MovAXToMemory: //[]byte{MovAXToMemory, 1, 0}
effectiveAddr := uint16(instructions[2])<<8 | uint16(instructions[1])
src := e.readReg16(AX)
e.writeDataMemmoryWord(effectiveAddr, src)
case MovImmediate8ToMemory: //[]byte{MovImmediate8ToMemory, 1, 0b10111, 0, 1}
e.writeDataMemmoryByte(e.calEffectiveAddr(instructions[2:]), instructions[1])
case MovImmediate16ToMemory: //[]byte{MovImmediate16ToMemory, 0, 1, 0b10111, 0, 1}
val := uint16(instructions[2])<<8 | uint16(instructions[1])
e.writeDataMemmoryWord(e.calEffectiveAddr(instructions[3:]), val)
}
}
很简单,就是根据指令详细类型,做相应的动作。因为指令详细类型已经明确了源、目的操作数的类型,宽度,所以只需要调用相应的接口读取源操作数,把它的值写到目的操作数就行了。
以 MovReg8ToMemory 为例,它表示将一个 8 位寄存器的值移动到内存:
case MovReg8ToMemory: //[]byte{MovReg8ToMemory, CL, 0b00000}
// 读取源寄存器的值
src := e.readReg8(instructions[1])
// 将值写入内存地址
e.writeDataMemmoryByte(e.calEffectiveAddr(instructions[2:]), src)
其中 calEffectiveAddr 函数用来计算内存操作数表示的有效地址【偏移量】:
/*
MOD=11 EFFECTIVE ADDRESS CALCULATION
R/M w=0 w=1 R/M MOD=00 MOD=01 MOD=10
000 AL AX 000 (BX)+(SI) (BX)+(SI)+D8 (BX)+(SI)+D16
001 CL CX 001 (BX)+(DI) (BX)+(DI)+D8 (BX)+(DI)+D16
010 DL DX 010 (BP)+(SI) (BP)+(SI)+D8 (BP)+(SI)+D16
011 BL BX 011 (BP)+(DI) (BP)+(DI)+D8 (BP)+(DI)+D16
100 AH SP 100 (SI) (SI)+D8 (SI)+D16
101 CH BP 101 (DI) (DI)+D8 (DI)+D16
110 DH SI 110 DIRECTADDRESS (BP)+D8 (BP)+D16
111 BH DI 111 (BX) (BX)+D8 (BX)+D16
*/
func (e *EU) calEffectiveAddr(instructions []byte) uint16 {
rm := instructions[0] & 0b111
mod := (instructions[0] & 0b11000) >> 3
if mod == 0b00 && rm == 0b110 {
return uint16(instructions[2])<<8 | uint16(instructions[1])
}
var effectAddr uint16
switch rm {
case 0b000:
effectAddr = e.bx + e.si
case 0b001:
effectAddr = e.bx + e.di
case 0b010:
effectAddr = e.bp + e.si
case 0b011:
effectAddr = e.bp + e.di
case 0b100:
effectAddr = e.si
case 0b101:
effectAddr = e.di
case 0b110:
effectAddr = e.bp
case 0b111:
effectAddr = e.bx
}
if mod == 0b01 {
effectAddr += uint16(instructions[1])
} else if mod == 0b10 {
d16 := uint16(instructions[2])<<8 | uint16(instructions[1])
effectAddr += d16
}
//BP Used As Base Register, SS is default segment base
if rm == 0b010 || rm == 0b011 ||
(rm == 0b110 && mod == 0b01) ||
(rm == 0b110 && mod == 0b10) {
e.changeSegPrefix(SS)
}
return effectAddr
}
之前讲到过,中间指令内存操作数的格式如下:
mod | rm,[偏移量]
这个函数就是根据这个格式提取出 mod,rm ,偏移量,根据 mod 和 rm 字段的含义来计算有效地址。
jmp 指令的执行
jmp 指令的执行函数为 executeJmp:
func (e *EU) executeJmp(instructions []byte) {
switch instructions[0] {
case JmpNotShort: //16位IP偏移量
inc := int16(uint16(instructions[1]) | uint16(instructions[2])<<8)
ip := e.readIP()
e.writeIP(ip + uint16(inc))
case JmpShort: //8位IP偏移量
inc := int8(instructions[1])
ip := e.readIP()
e.writeIP(ip + uint16(inc))
case JmpDirectIntersegment: //cs 16位,IP 16位
//TODO
case JmpReg16: //新IP的值在寄存器中
dst := e.readReg16(instructions[1])
e.writeIP(dst)
case JmpIndirectWithinsegment: //新IP的值在内存中
dst := e.readDataMemmoryWord(e.calEffectiveAddr(instructions[1:]))
e.writeIP(dst)
case JmpIndirectIntersegment: //新CS和IP的值在内存中
dstIP := e.readDataMemmoryWord(e.calEffectiveAddr(instructions[1:]))
dstCS := e.readDataMemmoryWord(e.calEffectiveAddr(instructions[1:]) + 2)
e.writeSeg(CS, dstCS)
e.writeIP(dstIP)
}
if instructions[0] <= JmpIndirectIntersegment {
return
}
// 条件转移
....
}
它根据指令详细类型和操作数,修改 IP 或 IP 和 CS 寄存器的值,达到执行流程转移的目的。
修改 IP 或 CS 的值是要向 BIU 发起请求的,BIU 是这样处理请求的:
case WriteSegReg:
reg := uint8(<-b.InnerDataBus)
val := <-b.InnerDataBus
switch reg {
case ES:
b.es = val
case CS:
b.cs = val
// 先修改IP,再修改CS,可能从旧的代码段取了指令,所以需要清空指令队列
b.virtIP = b.ip
b.emptyInstructionQueue()
case SS:
b.ss = val
case DS:
b.ds = val
default:
log.Fatal("error")
}
// 写IP寄存器
case WriteIPReg:
val := <-b.InnerDataBus
b.ip = val
b.virtIP = val
b.emptyInstructionQueue()
只要修改了 IP 或 CS 寄存器,它都会清空指令队列,更新虚拟IP指针的值,这样就会从新的内存地址开始读取指令。
add 指令的执行
add 、sub 这些指令的执行就相对复杂些,这是因为 CPU 在计算时是同时做有符号数和无符号数的计算,在计算过程中会设置标志寄存器的某些标志位。具体直接看《汇编语言》11章内容。我这里直接就截下原书内容:
我实现了 addBit8、addBit16、subBit8、subBit16 几个函数来模拟CPU做加减运算,addBit8 的代码如下:
// 8 位数的加法运算
func (e *EU) addBit8(a, b uint8) uint8 {
e.addInt8(int8(a), int8(b))
return e.addUint8(a, b)
}
func (e *EU) addUint8(a, b uint8) uint8 {
//CF标志记录了无符号数运算的过程中是否发生借位
if uint16(a)+uint16(b) > 255 {
e.writeEFLAGS(cfFlag, 1)
} else {
e.writeEFLAGS(cfFlag, 0)
}
res := a + b
if res == 0 {
e.writeEFLAGS(zfFlag, 1)
} else {
e.writeEFLAGS(zfFlag, 0)
}
//1的个数为偶数,PF标志位置1
if numberOfOneBit8(res)%2 == 0 {
e.writeEFLAGS(pfFlag, 1)
} else {
e.writeEFLAGS(pfFlag, 0)
}
return res
}
func (e *EU) addInt8(a, b int8) int8 {
//OF标志记录了有符号数运算的结果是否发生溢出
if int16(a)+int16(b) < -128 ||
int16(a)+int16(b) > 127 {
e.writeEFLAGS(ofFlag, 1)
} else {
e.writeEFLAGS(ofFlag, 0)
}
//SF标志指示将数据当成有符号数时运算后的结果是否是负数
res := a + b
if res < 0 {
e.writeEFLAGS(sfFlag, 1)
} else {
e.writeEFLAGS(sfFlag, 0)
}
if res == 0 {
e.writeEFLAGS(zfFlag, 1)
} else {
e.writeEFLAGS(zfFlag, 0)
}
//1的个数为偶数,PF标志位置1
if numberOfOneBit8(uint8(res))%2 == 0 {
e.writeEFLAGS(pfFlag, 1)
} else {
e.writeEFLAGS(pfFlag, 0)
}
return res
}
16 位加法,减法运算等同理。
在执行 add 指令时,只需根据指令详细类型,调用相应的函数就行了。
如下是 EU 执行 add,or,adc,sbb,and,sub,xor,cmp,test 等指令时调用的 executeAddEtc 方法代码片段:
func (e *EU) executeAddEtc(instructions []byte) {
length := len(instructions)
switch instructions[0] {
case AddReg8ToReg8:
src := e.readReg8(instructions[1])
dst := e.readReg8(instructions[2])
var res uint8
write := true
switch e.currentInstruction {
case InstructionAdd:
res = e.addBit8(src, dst)
case InstructionOr:
res = src | dst
case InstructionAdc:
cf := e.readEFLAGS(cfFlag)
res = e.addBit8(src, dst+cf)
case InstructionSbb:
cf := e.readEFLAGS(cfFlag)
res = e.subBit8(dst, src+cf)
case InstructionAnd:
res = src & dst
case InstructionSub:
res = e.subBit8(dst, src)
case InstructionXor:
res = src ^ dst
case InstructionCmp:
res = e.subBit8(dst, src)
write = false
case InstructionTest:
res = src & dst
write = false
}
if write {
e.writeReg8(instructions[2], res)
}
// 省略后面的代码
}
因为这些指令机器指令格式都差不多,所以直接就放在一个函数里实现了。
其他指令的执行代码都和上述举例的类似,都需要根据指令的含义来做具体的操作。比如程序中经常使用"int 21h" 语句终止程序,而它的执行代码实现非常简单:
func (e *EU) executeInt(instructions []byte) {
if instructions[1] == 0x21 {
e.stop = true
}
}
就是设置一个标志位,EU 循环时判断这个标志位为 true ,就停止执行了。
当实现了某个程序包含的所有指令的解码和执行时,虚拟机就可以完全执行这个程序了!