Procesor – skoki i delay slot

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;
    }
}
Ten wpis został opublikowany w kategorii Avocado, Daj się poznać 2017 - Avocado i oznaczony tagami , . Dodaj zakładkę do bezpośredniego odnośnika.