实现8086虚拟机(五)——mov、jmp、add等指令的执行


这篇文章讲述下 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 ,就停止执行了。

当实现了某个程序包含的所有指令的解码和执行时,虚拟机就可以完全执行这个程序了!

猜你喜欢

转载自blog.csdn.net/woay2008/article/details/129134447