Implementando máquina virtual 8086 (5) - execução de mov, jmp, add e outras instruções


Este artigo descreve como EU executa as instruções mov, jmp e add.

Conforme mencionado anteriormente, o processo de execução da UE é o seguinte:

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
			}
		}
	}
}

Após decodificar uma instrução completa, ele chama o método execute para executar a instrução:

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---")
	}
}

Ele chama o método correspondente para execução com base no primeiro byte da instrução intermediária - o tipo de instrução.

Execução da instrução mov

Quando uma instrução mov é executada, o método executeMov é chamado e os parâmetros são os bytes subsequentes da instrução intermediária:

Tipo detalhado da instrução, [operando de origem], [operando de destino]

Vamos ver como isso é feito:

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)
	}
}

É muito simples, basta realizar a ação correspondente de acordo com o tipo detalhado da instrução. Como o tipo detalhado da instrução esclareceu o tipo e a largura dos operandos de origem e destino, você só precisa chamar a interface correspondente para ler o operando de origem e escrever seu valor no operando de destino.

Tomando MovReg8ToMemory como exemplo, significa mover o valor de um registrador de 8 bits para a memória:

	case MovReg8ToMemory: //[]byte{MovReg8ToMemory, CL, 0b00000}
	    // 读取源寄存器的值
		src := e.readReg8(instructions[1])
		// 将值写入内存地址
		e.writeDataMemmoryByte(e.calEffectiveAddr(instructions[2:]), src)

A função calEffectiveAddr é usada para calcular o endereço efetivo [offset] representado pelo operando de memória:

/*
	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
}

Conforme mencionado anteriormente, o formato dos operandos da memória de instruções intermediárias é o seguinte:

mod|rm,[deslocamento]

Esta função extrai mod, rm e offset com base neste formato e calcula o endereço efetivo com base no significado dos campos mod e rm.

Execução da instrução jmp

A função de execução da instrução 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
	}

	// 条件转移
	....
}

Ele modifica o valor dos registradores IP ou IP e CS de acordo com o tipo detalhado e operandos da instrução para atingir o objetivo de executar a transferência de fluxo.

Para modificar o valor de IP ou CS, é necessário iniciar uma solicitação à BIU. A BIU trata a solicitação da seguinte forma:

			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()

Sempre que o registro IP ou CS é modificado, ele limpa a fila de instruções e atualiza o valor do ponteiro IP virtual para que as instruções sejam lidas a partir do novo endereço de memória.

Execução do comando adicionar

A execução dessas instruções add e sub é relativamente complicada, porque a CPU realiza cálculos de números assinados e não assinados ao mesmo tempo, e certos bits de flag do registrador de flag são definidos durante o processo de cálculo. . Para obter detalhes, consulte o Capítulo 11 de "Linguagem Assembly". Vou cortar diretamente o conteúdo do livro original aqui:Insira a descrição da imagem aqui
Insira a descrição da imagem aqui
Insira a descrição da imagem aqui
Implementei várias funções addBit8, addBit16, subBit8 e subBit16 para simular as operações de adição e subtração da CPU. O código de addBit8 é o seguinte :

// 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
}

Adição, subtração de 16 bits e similares.

Ao executar a instrução add, você só precisa chamar a função correspondente de acordo com o tipo detalhado da instrução.

A seguir está um trecho de código do método executeAddEtc chamado quando EU executa add, or, adc, sbb, and, sub, xor, cmp, test e outras instruções:

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)
		}
	// 省略后面的代码
}

Como os formatos de instrução de máquina dessas instruções são semelhantes, eles são implementados diretamente em uma função.

Os códigos de execução de outras instruções são semelhantes aos exemplos acima e todos precisam realizar operações específicas de acordo com o significado das instruções. Por exemplo, a instrução "int 21h" é frequentemente usada em programas para encerrar o programa, e sua implementação de código de execução é muito simples:

func (e *EU) executeInt(instructions []byte) {
    
    
	if instructions[1] == 0x21 {
    
    
		e.stop = true
	} 
}

Ele apenas define um bit de sinalização. Quando o loop EU determina que o bit de sinalização é verdadeiro, ele interrompe a execução.

Quando todas as instruções contidas em um programa são decodificadas e executadas, a máquina virtual pode executar totalmente o programa!

Acho que você gosta

Origin blog.csdn.net/woay2008/article/details/129134447
Recomendado
Clasificación