Asserts en assembleur

Un assert est un simple test, réalisé à un moment donné de l’exécution d’un programme, qui, s’il renvoie vrai, permet de considérer que l’on est dans un état incohérent et non attendu, qui ne permet pas de continuer le déroulé du code avec des résultats satisfaisants. Si on se retrouve dans une telle situation, on décide alors de stopper le traitement en générant une erreur propre, sans altérer l’état des données, permettant l’analyse.

Cette notion existe dans de nombreux langages de haut niveau: on lève une exception contextualisée accompagnée de tout ce qui permettra de tirer une analyse. Si le code est correctement écrit, l’exception est interceptée et traitée, de façon à éviter le crash, les informations sont stockées ou envoyées vers qui de droit.


En assembleur, lorsqu’une telle situation est rencontrée, on stoppe l’exécution. Typiquement on saute à un endroit de la mémoire, on peut éventuellement changer la couleur du border pour signaler visuellement l’état, et on fait une boucle infinie sur place.

Sur émulateur, cela permet de sauvegarder un snapshot (l’état complet de la mémoire et des registres du CPC), et d’aller consulter la pile et la mémoire. Dans le cas d’un vrai hardware, il faudra être doté d’une extension périphérique supplémentaire qui permette une telle inspection de la mémoire. Le plus simple est donc d’être capable de reproduire un problème sur émulateur

Exemple Concret sur CPC

Concrètement, imaginons qu’une routine demande au registre A de valoir en entrée soit #00, soit #FF, toute autre valeur n’est pas attendue (et ferait planter méchamment la machine). Autrement dit si on a dans A toute autre valeur, on sait que quelque chose s’est mal passé en amont. On peut alors mettre ce code en entrée de cette fameuse routine :

Routine
  IF DEBUG
   PUSH AF         ; On sauve AF afin que le code qui suit n'ait pas d'impact sur les flags
   CP #00
   JR Z,.Next      ; =0 OK
   CP #FF
   CALL NZ,Assert  ; <> 0 et <> #FF --> Anormal
.Next
   POP AF          ; On rétablit AF
  ENDIF
;  ... 
;  Routine originale
;  ... 
 ret

La routine Assert on aurait alors :

Assert
   DI              ; Optionnel, si on veut que les INT ne perturbent plus rien
   PUSH BC         ; On sauve BC pour ne pas le perdre si besoin
   LD BC,#7E10     ; Bordure violette
   OUT (C),C
   LD C,#5D
   OUT (C),C       
   POP BC          ; On rétablit BC
   JR $            ; On attend

Une fois l’erreur déclenchée, on retrouve sur la pile l’adresse du saut à l’assert (le CALL NZ), et AF. Tous les autres registres, la RAM, ainsi que les autres données présentes dans la pile sont préservés, propres et intacts, prêts à être analysés. Le DI est optionnel car l’exécuter fait perdre l’information de la présence ou non des INT à l’appel de l’assert, ce qui selon certain cas pourrait être important dans l’analyse. Mais il permet aussi d’éviter que des routines d’interruption ne modifient des données en RAM.

Cout en mémoire et CPU

Evidement, il y a un coût, en mémoire, ça grossit le code, et en cpu, ça alourdit pas mal avec des PUSH/POP/CALL, c’est pour ça qu’on parle d’un assemblage de DEBUG, conditionné par une variable (IF DEBUG), dans lequel on va ajouter ces codes, à contrario d’un assemblage RELEASE qui en seront expurgés. Il faut donc garder en tête qu’un code DEBUG est plus lent à exécuter qu’un code RELEASE.

Malgré tout, la mise en œuvre d’un tel mécanisme reste simple et permet bien souvent de gagner énormément de temps. L’intérêt est d’essayer d’intercepter tout état anormal, le plus tôt possible, en amont d’un crash ou d’un état plus difficilement analysable, en ayant toutes les données permettant l’analyse accessibles.