Alweer geen Excel

Afronding TASM: hoe procedures echt werken

Arnout van Kempen schrijft in deze rubriek over pret maken met computers. Hij gaat aan de slag met Pascal.

Met JMP kunnen we door code springen, maar als je met procedures wilt werken is dat niet genoeg. Je moet parameters door kunnen geven, terug kunnen naar de plaats waar je de procedure aanriep en dezelfde procedure moet vanaf verschillende lokaties aangeroepen kunnen worden. Pascal doet dat moeiteloos, maar Pascal is uiteindelijk alleen maar een abstractie van machinecode. Dus hoe werkt dat op het niveau van het ijzer nu echt? De kern van het verhaal zit niet in CALL en RET, maar in de stack. Begrijp de stack en je begrijpt procedures echt. 

De stack is een LIFO-structuur (Last In, First Out) die van hoog naar laag groeit in het geheugen. Het SS:SP register-paar wijst naar de top. PUSH legt een waarde op de stack en verlaagt SP met 2 (voor een 16-bits waarde). POP haalt de bovenste waarde eraf en verhoogt SP weer. 

PUSH AX    ; zet AX op stack, SP -= 2
PUSH BX    ; zet BX op stack, SP -= 2
POP  CX    ; haal BX in CX, SP += 2
POP  DX    ; haal AX in DX, SP += 2

CALL combineert intern wat je ook met PUSH + JMP zou bereiken: het bewaart het return-adres op de stack en springt naar de procedure. Bij een near call (binnen hetzelfde segment) wordt alleen IP gepusht. Bij een far call (naar een ander segment) worden zowel CS als IP gepusht – logisch, want je moet straks terug naar het juiste segment. We zagen die near/far-verdeling al bij pointers en memory models.

RET popt het return-adres en springt ernaar terug. Voor far calls bestaat RETF, die zowel IP als CS terugzet. TASM ziet overigens zelf of je een near of een far call nodig hebt, en staat toe dat je alleen RET gebruikt. Bij de vertaling naar een binary vult de assembler dat zelf aan naar RETF indien nodig.

Stel, je wilt parameters aan een procedure meegeven én lokale variabelen gebruiken. Die moeten ergens op de stack, maar SP verandert telkens als je PUSH of POP doet. Hoe vind je ze dan terug? Met BP (Base Pointer) als anker.
Zo call je een procedure met parameters: 

PUSH 42          ; parameter 1
PUSH 100         ; parameter 2
CALL MijnProc

CALL pusht het return-adres (IP). De stack ziet er nu zo uit: [... | 42 | 100 | return-IP], met SP wijzend naar return-IP.
In de procedure zelf bouwen we een stack frame:

MijnProc:
  PUSH BP    ; bewaar oude BP op stack
   ; stack: [... | 42 | 100 | return-IP | oude BP]
  MOV  BP,SP ; BP wordt anker, wijst naar oude BP
  SUB  SP,4  ; reserveer 4 bytes voor lokale variabelen
   ; stack: [... | 42 | 100 | return-IP | oude BP | 4 bytes
ruimte]

Nu kunnen we alles bereiken via BP:
- [BP+0] = oude BP (waar BP naar wijst)
- [BP+2] = return-IP
- [BP+4] = parameter 2 (100, laatst gepusht)
- [BP+6] = parameter 1 (42)
- [BP-2] = eerste lokale variabele
- [BP-4] = tweede lokale variabele

Het mooie: omdat BP vast blijft staan, kunnen we ondertussen gewoon PUSH/POP doen voor registers. SP beweegt, BP niet.
Aan het eind ruimen we netjes op:

  MOV  SP,BP; gooi lokale variabelen weg (SP = BP)
  POP  BP   ; herstel oude BP
  RET       ; pop return-IP en spring terug

Dit is exact hetzelfde mechaniek dat Turbo Pascal gebruikt voor procedures met parameters en lokale variabelen.
 

Een software-interrupt (INT n) werkt als een CALL, maar springt via de Interrupt Vector Table op adres 0000:0000. INT pusht FLAGS, CS en IP op de stack, schakelt interrupts uit (IF=0), en springt naar de handler. IRET zet alles terug: IP, CS en FLAGS worden gepopt en de CPU keert precies terug waar hij was. 

MOV  AH,09h       ; DOS functie 9: print string
MOV  DX,OFFSET MSG
INT  21h          ; roep DOS aan

Hardware-interrupts (toetsenbord, timer) werken hetzelfde, maar worden door devices getriggerd in plaats van door INT-instructies. Een belangrijk verschil tussen een CALL of software-INT en een hardware INT, is dat bij die laatste geen parameters kunnen worden meegegeven. Het programma 'weet' immers niet dat een hardware interrupt wordt ontvangen op een bepaald moment. Als je INT gebruikt om de handler van een hardware interrupt op te roepen, moet je daar dus rekening mee houden. En ook bij pure software INT's zie je dat parameters via de registers worden doorgegeven en niet via de stack. Dit beperkt mogelijkheden een beetje, maar maakt dit soort calls wel snel en overzichtelijk.

Arnout van Kempen is naast computernerd ook directeur compliance & risk bij aaff. Hij schrijft op persoonlijke titel.

Gerelateerd

reacties

Reageer op dit artikel

Spelregels debat

    Aanmelden nieuwsbrief

    Ontvang elke werkdag (maandag t/m vrijdag) de laatste nieuwsberichten, opinies en artikelen in uw mailbox.

    Bent u NBA-lid? Dan kunt u zich ook aanmelden via uw ledenprofiel op MijnNBA.nl.