Wat is Retouradres?
Adres waarnaar wordt teruggekeerd wanneer een functie klaar is. In RISC-V zet de jal-instructie het adres van de volgende instructie in ra (x1); in x86 zet de call-instructie het op de stack.
Japanse benaming: リターンアドレス
Vakgebied: Instructieset en assembly
Hoe een call het opslaat
Bij een call moet worden vastgelegd waar de uitvoering moet worden hervat, namelijk het adres van de instructie na de call. In RISC-V schrijft jal (jump and link) PC + 4 naar een register, volgens conventie ra (x1), en springt; de terugkeer gebeurt met ret, een pseudo-instructie voor jalr x0, 0(ra). Calls via functiepointers, virtuele functies en stubs van gedeelde bibliotheken gebruiken jalr met een register als doel. In x86 zet call het retouradres op de stack en haalt ret het terug in de instructiepointer, dus de stack bevat het altijd.
Leaf- en non-leaf-functies
Omdat elke jal ra overschrijft, moet een RISC-V-functie die andere functies aanroept eerst haar eigen retouradres opslaan. Een typische proloog is addi sp, sp, -16 om een frame te reserveren op de naar beneden groeiende stack, gevolgd door sw ra, 12(sp) en sw s0, 8(sp) om ra en een callee-saved register op te slaan. De epiloog laadt ze opnieuw, herstelt sp en voert ret uit. Een leaf-functie, die niets aanroept, kan ra in het register laten staan en heeft mogelijk helemaal geen stackframe nodig, wat kleine functies goedkoop maakt.
Retouradressen in hardware
Out-of-order-CPU's voorspellen waar een terugkeer naartoe gaat, lang voordat ra of de stack wordt gelezen. Een return address stack (RAS) in de branch predictor pusht bij elke call het fall-through-adres en popt het bij elke return, wat zeer nauwkeurige voorspellingen geeft voor goed geneste code. Excepties en interrupts werken anders: de CPU slaat de onderbroken PC op in een speciaal register, zoals mepc of sepc in RISC-V, samen met de vorige privilegemodus, en mret of sret herstelt die allemaal in één keer, in plaats van een gewone ret.
Veelgestelde vragen
Waar wordt het retouradres opgeslagen in RISC-V?
In register ra (x1), dat bij de call door jal of jalr wordt geschreven. Een non-leaf-functie kopieert het naar haar stackframe voordat ze zelf calls doet en herstelt het vóór ret.
Wat is het verschil tussen call/ret in x86 en jal/ret in RISC-V?
In x86 zet call het retouradres altijd op de stack en haalt ret het eraf, terwijl jal in RISC-V het in een register plaatst en alleen het geheugen gebruikt als de functie ervoor kiest het op te slaan.
Waarom is het overschrijven van het retouradres gevaarlijk?
Wanneer de functie terugkeert, springt de CPU naar het adres dat daar staat, dus een aanvaller die het via een buffer overflow overschrijft, kan de controle over het programma overnemen.