Operacje skoków nie są takie oczywiste jak mogłoby to się wydawać - jest tutaj kilka pułapek o chciałem wspomnieć.
Kodowanie instrukcji skoku
Przykładowa instrukcja skoku (j) w assemblerze będzie wyglądać tak:
0xbfc00070 0bf00054: j 0xfc00150 0xbfc00074 00000000: nop
Na zapisanie adresu skoku dostępne jest tylko 26 bitów. Jak wspominałem we wcześniejszym poście - każda instrukcja zajmuje 4 bajty i niemożliwy jest skok pod adres niebędący wielokrotnością 4 - taki skok zakończyłby się wyjątkiem procesora. Dlatego też adres docelowy zapisany w TARGET nie zawiera dwóch najmniej znaczących bitów. Aby odzyskać adres fizyczny z TARGET należy go przemnożyć przez 4 lub przesunąć o dwa bity w lewo.
Zostały 4 górne bity, które nie są nigdzie zakodowane. Skok jest wykonywany względem obecnego PC i właśnie z niego pobierane są 4 najwyższe bity:
uint32_t jumpPC = (PC & 0xf0000000) | (i.target << 2);
Delay slot
Po wykonaniu instrukcji nie następuje zmiana adresu. W architekturze MIPS występuje rzecz nazywana delay slot, która wynika z budowy pipeline. W skrócie - procesor wykonuje jedną instrukcję na przód. Kiedy program wykonywany jest liniowo - nie ma żadnego problemu. Gdy natomiast wykonywany jest skok, zostaje zaczytana kolejna instrukcja, jest wykonywana i dopiero wtedy procesor zmienia PC.
Sprawiało mi to dużo problemów, gdy próbowałem analizować kod generowany przez kompilator. Kod napisany ręcznie zazwyczaj po instrukcji skoku zawierał opcode nop, czyli nic-nie-rób - tak jak na przykładzie wyżej. Kompilatory natomiast lubią umieszczać tu prawidłowe instrukcje - dla wywołań funkcji jest to na przykład przekazanie argumentu do rejestru.
Warto wspomnieć, że dzisiejsze procesory są dużo bardziej zaawansowane i ilość wykonywanych naprzód instrukcji jest dużo większa - wiąże się to z ciekawym problemem branch prediction fail.
Implementacja
Rozwiązanie problemu jest dosyć proste - adres skoku zapisywany jest do zmiennej pomocniczej, a nie do PC - w moim przypadku pole to nazywa się jumpPC. Oprócz tego ustawiana jest flaga inDelaySlot, która będzie sprawdzona przy następnym wywołaniu funkcji.
Po modyfikacji kodu z poprzedniego postu funkcja wygląda tak:
void CPU::executeInstruction() { mipsInstructions::Opcode opcode; opcode.opcode = readMemory32(PC); bool isJumpCycle = inDelaySlot; if (opcode.op == 0x13) { // LUI, I-Type reg[opcode.rt] = opcode.imm << 16; } else if (opcode.op == 0x02) { // J, J-Type jumpPC = (PC & 0xf0000000) | (i.target << 2); inDelaySlot = true; } else { // PANIC } if (isJumpCycle) { inDelaySlot = false; PC = jumpPC; } else { PC += 4; } }