Diretório de artigos
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:
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!