Low Level Programming Notes du cours de Lin Jensen, septembre-décembre 2006 I. Quelques rappels de calcul Les nombres en hexadécimal sont précédés de 0x pour éviter une confusion dans les bases. Quand on veut multiplier 0x3FCB par 2, il suffit de le convertir en binaire et d’opérer un décalage : 0111 1111 1001 0110, soit le résultat 0x7F96. On rappelle que chaque nombre s’écrit sur 4 bits. Pour soustraite lorsqu’on a des nombres signés, on boucle. Ainsi 0000 moins 1 nous donne 1111. La règle du complément à deux consiste à inverser les bits et à rajouter 1 : 0101 = 5 → 1010 (inversion) → 1011 (ajout du 1) : -5 0110 = 6 → 1001 → 1010 = - 6 Il y a donc un seul circuit pour toute l’arithmétique, puisque les nombres négatifs sont convertis grâce à la règle du complément à deux. De même, il n’y a pas de circuit soustracteur mais juste un additionneur ; on utilise la règle du complément à deux en cas de soustraction. Le résultat doit être complémenté à nouveau pour l’exprimer en notation décimale. Par exemple si l’on obtient 111110, en le complémentant on obtient 000010 soit +2 ; donc le nombre obtenu était -2. II. Eléments d’architecture d’un ordinateur MIPS Control Unit Instruction Adress Decode (IAD) instruction Address bus ADD MEMORY 1 5 6 $1 $2 $3 Par exemple un 0 dans l’IAD va faire un ADD. Philippe Giabbanelli Data bus 1 – Fetch instruction 2 - Decode 3 – Increment program count 4 – Execute instructions ↑ OLD MIPS MIPS 1 ← MIPS Notes sur l’assembleur Le Program Count (PC) nous donne l’instruction à exécuter. Si on passe d’une instruction à l’autre linéairement, alors on passe de la 0 à la 4, i.e. on avance de 4 bytes. On notera qu’il y a différents langages d’assembleur. Par exemple, il y a différentes syntaxes pour les processeurs Intel. En plus des syntaxes, les noms des commandes et des registres ne sont pas les mêmes. En assembleur x86 on trouve par exemple les registres eax et ebx, tandis qu’en MIPS on a des $t0, etc. Les opinions sur la syntaxe sont également divisés dans le sens de lecture des commandes : de gauche à droite, ou de droite à gauche. Est-il mieux d’écrire ADD $3 $2 $1 pour $3 ← $2+$1 ou $3+$2 → $1. Processeur 6502, Atari III. Quand on dépasse des bornes lors d’un calcul, on parle d’arithmetic overflow, cela entraîne des erreurs. Certains processeurs le détecte et font un trap ou interruption request, d’autres ne réagissent pas ; ils utilisent en général un overflow flag, chargé dans les processeurs x86 par les instructions arithmétiques et de comparaison (équivalent à une soustraction sans stockage de résultat), et non utilisé par les opérations logiques. Ne pas confondre : la division par 0 n’est pas un arithmetic overflow ; la division par 0 est mathématique indéfinie : ce n’est pas que sa valeur est trop large, mais plutôt qu’elle n’a pas de valeur ! Opérations arithmétiques et premiers programmes MIPS EDIT ASSEMBLE LINK TO THE LIBRARIES EXECUTE SYNTAX ERROR We will use SPIM or XSPIM to show which line of the program code is going to be executed. We will also use bank of test data, i.e. tests units, with MIPS MARK (command mipsmark file.a). At the end, we use submit116 file.a. In MIPS, the results always go back to register. ## Text segment .text .globl __start __start: lw $t1, mynum li $t2, 5 add $t3, $t1, $t2 sw $t3, mynum # execution starts here # load word mynum into temporary register 1 # load immediate (for a constant) 5 # add the numbers and put them into t3 # store word from register to memory (location of mysum) .data mynum: .word 1 mysum: .word 0 # will be changed to 6 at the end If we want to move something from a register to a data, we use instruction store. If we want to take something, then we use load. A simple operating system is simulator system calls, called syscall. It tells the O/S that we need help. Register $v0 has to contain the service number, i.e. what we want syscall to do for us. Another registers will be use by the service. $a0 stands for argument 0 for any kind of a call.. Service Call code Arguments (input) Results print integer 1 $a0 = integer print string 4 $a0 = address of string Prints every character until the terminating ‘\0’ read integer 5 (none) $v0 holds integer that was entered read string 8 $a0=address to store $a1= length limit characters are stored exit 10 (none) Ends the program Philippe Giabbanelli signed decimal integer printed in console window 2 Notes sur l’assembleur MIPS # the goal of the program is to add 2 + 3 and print “5 oranges” .text .globl __start __start: lw $t0, mynum # t0 ← 2 (mynum) li $t1, 3 # t1 ← 3 add $a0, $t0, $t1 # a0 ← mynum + 3 li $v0, 1 # ready to print an int syscall # ask the OS to run service 1 la $a0, str # load the address of str li $v0, 4 # ready to print a string syscall # print the string li $v0, 10 # ready to end syscall # exit Il y a des pseudo-instructions qui sont ensuite expansées, comme neg $t3, $t0 → sub $t0, $0, $t0 Où $0 contient toujours 0. La forme normale est : <add|sub> Rdest, Rsrc1, src2 Où src2 est un registre ou une constante. Parfois, addi est utilisé pour la forme à 2 registres et une constante. Il y aussi un raccourci : add $t0, 1 → add $t0, $t0, 1 Cela marche de la même manière avec la soustraction, mais il n’y a pas de subi. C’est plutôt : addi $t5, $t0, -5 sub $t5, $t5, 4 ≡ addi $t5, $t5, -4 .data mynum: .word 2 # a word is 32 bits. mynum is a label str: .asciiz “oranges\n” # asciiz ends the string with \0 Avec ‘mul Rdest, Rsrc1, src2’, on fait une multiplication de nombres signés et on stocke le résultat sur 32 bits. Or, multiplier 32 bits par 32 bits peut donner un résultat sur 64 bits, et alors ça ne loge pas, il y a une arithmetic overflow. La pseudo-instruction mulo regarde s’il y a overflow, et arrête proprement la chose ; c’est implémenté par plusieurs instructions, pour disposer de la vérification. L’instruction div marche de même manière que mul : li $t0, 20 div $a0, $t0, 3 li $v0, 1 syscall # affiche 6, à savoir le résultat de la division entière Avec rem Rdest, Rsrc1, src2 on obtient le reste (remainder) de la division. Il y a aussi une version nonsignée de la multiplication et de la division, en mettant un ‘u’ à la fin (exemple : mulou). L’instruction réelle de la machine pour la multiplication est mult Rsrc1, Rsrc2. Comme le résultat fait 64 bits, il est stocké dans deux registres : S’ajoutent aux Register hi Register lo 32 registres de l’ordinateur En contrôlant l’allure de hi et de lo, on sait s’il y a eu overflow ou non. C’est le même principe avec la division, où on met le quotient en lo et le reste en hi. lw $t0, apples div $t1, $t0, 8 rem $t2, $t0, 8 # apples / student # remainder Philippe Giabbanelli li $1, 8 div $t0, $1 mflo $t1 li $1, 8 div $t0, $1 mfhi $t2 expansion 3 Optimisation (compilateur) li $1, 8 div $t1, $1 mflo $t2 mflo $t3 Notes sur l’assembleur MIPS # The problem is to find a point on a line, given by the equation: y = slope*x + intercept for some value # x, calculate and print the corresponding y value that is (nearly) on the line. Since we are doing integer # arithmetic, slope will be given as two numbers, dy and dx, see figure. You will need to multiply before # dividing, and the answer will be only approximate, simply ignore any remainder. .text .globl __start __start: # execution starts here la $a0, hi li $v0, 4 syscall # we print a welcome message `cause we are polite # be ready to print the message # ok the message has been print lw $t0,x lw $t1,dy mult $t0,$t1 mflo $t2 lw $t1,dx div $t2,$t1 mflo $t1 lw $t0, intercept add $t3,$t0,$t1 sw $t3, y # let us have the variable we want to compute... x # and here we get dy # we multiply the number, it goes in Lo # the result is now in register t2 `cause we know it is a little number li $v0, 4 la $a0,ans syscall lw $a0,y li $v0, 1 syscall li $v0,4 la $a0,endl syscall li $v0,10 syscall # be ready to print # "answer =" # and now we have (dy.x)/dx # we move the result from Low because we assume it is the quotient # we make the final computing for additionning every part of the expression # we store the final result in the variable y # print the answer # print "\n" # exit program # ciao ! ## -----------------------------------------------------.data x: .word 9 # find y for this value dy: .word 10 # vertical rise dx: .word 6 # horizontal run intercept: .word -2 #line crosses y-axis here y: .word -99 #optionally use this memory... ans: .asciiz "answer = " endl: .asciiz "\n" hi: .asciiz "hi there we are gonna make some calculus\n" Philippe Giabbanelli 4 Notes sur l’assembleur MIPS IV. Control Flow Instructions On discerne trois patterns : appel de functions (call functions), sauter une partie (skip), boucle (loop). ? T Branch to label F Keep going in normal way Instruction Branch to label if beq Rsrc1, Src2, label Rsrc1 = Src2 bne Rsrc1, Src2, label Rsrc1 <> Src2 blt Rsrc1, Src2, label Rsrc1 < Src2 bgt Rsrc1, Src2, label Rsrc1 > Src2 bgtu ble Rsrc1, Rsrc1 <= Src2 Src2, label bleu bge Rsrc1, Rsrc1 >= Src2 Src2, label bgeu Unsigned bltu On utilisera principalement l’instruction ‘j’ qui permet d’aller à un label. Par exemple, j backthere saute à au label (étiquette) backthere. # affichons un message 5 fois .text .globl –start li $t0, 5 # initialization of loop counter la $a0, mess li $v0, 4 again : beqz $t0, done # while t0 != 0… syscall # loop body sub $t0, 1 # decrement t0 j again # go to the test done: li $v0, 10 syscall .data mess: .asciiz “hello again\n” # affiche un message # nombre de fois : ∞ .text .globl –start la $a0, mess li $v0, 4 again : syscall j again done: li $v0, 10 syscall .data mess: .asciiz “hey\n” On utilise aussi les pseudo-instructions avec un z à la fin pour signifier « zéro ». beqz $t0, done → beq $t0, $0, done .text .globl –start --start li $v0, 4 la $a0, what # ‘what is the temperature ?’ syscall li $v0, 5 # read int, store result in $v0 syscall move $t0, $v0 blt $t0, 5, chilly # s’il fait moins de 5°C la $a0, nice # ‘it’s hot !’ li $v0, 4 syscall j endif chilly : la $a0, cold # ‘brr !’ li $v0, 4 syscall endif: li $v0, 10 syscall Philippe Giabbanelli Ces instructions de branchement sont parmi les soucis qu’engendre l’écriture d’un code assembleur à la main. En effet, si le programme devient gros et qu’il y a plusieurs boucles, on risque de leur attribuer les mêmes labels… un compilateur utilise un générateur de label, qui asure qu’il n’a pas déjà été utilisé. {sum non-negative numbers} sum := 0 num := 0 WHILE num >= 0 DO sum := sum + num read (num) ENDWHILE {write 10 lines} FOR countdown := 10 downto 1 DO print ("Hello again...") ENDFOR # add $t0, $0, $0 really: # set up print arguments before loop move $t0, $0 #sum=0 la $a0, hello_again move $v0, $0 #num=0 li $v0, 4 #print string call while5: li $t0, 10 # countdown bltz $v0, endwhile5 for10: add $t0, $v0 beqz $t0, endfor10 # ---- $v0 switches role here syscall #print another line li $v0, 5 #read int call sub $t0, 1 #decr. countdown syscall j for10 j while5 endfor10: endwhile5: 5 Notes sur l’assembleur MIPS V. Parcours d’un tableau T 3 +0 Soit un tableau T d’entiers {3, 6, 9, 2}. T[2] représente le contenu du mot mémoire 6 +4 adressé par T + 2 * sizeof(int), c’est-à-dire habituellement T + 2*4 = 2 + 8. 9 +8 2 +12 En MIPS, on peut charger l’adresse d’un registre, i.e. créer une sorte de pointeur. lw $t3, ($t0) # les ( ) signifient qu’on ce qui est pointé par t0, c’est de l’adressage indirect Nous allons donc transcrire nos pointeurs C en assembleur, comme suit : int arr[] = {3, 6, 9, 2} ; .globl –start int *a = arr ; --start int sum = 0; move $t2, $0 # t2 = sum = 0 while(i<4){ la $t0, arr # t0 points to array, we load address sum += *a; li $t1, 0 # loop counter a++; i++; while: } bge $t1, 4, endwhile # opposite of the loop condition lw $t3, ($t0) # t3 = *a, pointed value add $t2, $t2, $t3 # sum += *a Procédons de même pour déterminer add $t0, 4 # a++, point to the next element la taille d’une chaîne de caractère. add $t1, 1 # i++, loop counter Nous savons qu’une chaîne, au sens C, j while se termine toujours par le caractère \0, endwhile: # we can print the result if we want à savoir 0000 0000 en binaire. Si on écrit li $v0, 10 ceci en C, on peut s’aider du fait que 0 est syscall faux et tout le reste est vrai. D’où : char *nextchar = ans ; .data int count = 0 ; arr: word 3,6,9,-2 while(*nextchar++) count++; Quand on veut transcrire en code assembleur, on commence par poser les variables : # t1 : compteur t0 : nextchar t2 : character pointed to # string length la $t0, ans # points to string li $t1, 0 # count = 0. The same than doing move $t1, $0 lenloop: lb $t2, ($t0) # load byte (character) pointed by t0 beqz $t2, lendone # while char ≠ ‘\0’ add $t0, 1 # advance pointer # copy of a string add $t1, 1 # increase count --start: j lenloop li $t0, 0 lendone: # $t1 is now string length copyloop : lb $t1, mystring($t0) # load char De même avec la copie d’une chaîne de caractère : sb $t1, copy($t0) # copy it char copy[15] ; char copy[15]; beqz $t1, endcopy # if \0, end pcopy = copy ; pcopy = copy; add $t0, 1 # else continue char this ; while(*pcopy++ = *nextchar++) j copyloop while(this = *nextchar++) {;} endcopy : *pcopy++ = this; la $a0, copy li $v0, 4 syscall # print the string On utilise ici un nouveau mode d’adressage : indexed addressing. li $v0, 10 syscall Philippe Giabbanelli 6 Notes sur l’assembleur MIPS Exercice 2 Faire la somme des entiers non divisibles par 4 dans un tableau. Exercice 1 Permuter deux à deux les composants d’une chaîne de caractère de longueur paire. la $s0, numbers # a pointer to 'navigate' through the array li $s1, 5 # numbers of elements in the array li $s2, 0 # final sum. OUR result ! loop: lw $a0, ($s0) # taking the number as argument for function beqz $s1, endloop # is it done ? if so, stop counting ! jal isDivisableFour beqz $v0, skip lw $t1, ($s0) # load the word from memory add $s2, $s2, $t1 # add because not divisable by 4 skip: add $s0, 4 # go to the next digit by jumping 4 bits sub $s1, 1 # decrement the loop counter j loop endloop: la $a0, ans # write "sum = " li $v0, 4 syscall move $a0, $s2 # write the value we computed li $v0, 1 syscall la $a0, endl # write the end of line (carriage return) li $v0, 4 syscall li $v0, 10 # end the program syscall isDivisableFour: # we want to know if it begins with a 00 and $t0, $a0, 3 # see the first two digits, which must be 00 sne $v0, $t0, $0 # if it's not 0, there it's not divisable jr $ra # $v0 is 0 if it's not divisable, and 1 else. .text .globl __start __start: # execution starts here */ #the algorithm is very easy : #while(character pointed by array is not 0) # swap *(array) and *(array + 1) la $t0, chararray # t0 is pointer to char loop: lb $t1, ($t0) # put (*t0) in t1, as temp beqz $t1, endloop # if *(t0) = '\0', stop lb $t2, 1($t0) # load register’s content sb $t2, ($t0) # put *(t0) = *(t0 + 1) sb $t1, 1($t0) # the letter are swap add $t0, 2 # go to next pair j loop # we begin the loop again endloop: li $v0, 4 la $a0, chararray syscall # we print the result li $v0, 10 syscall # and we close the program .data chararray: .asciiz "abcdef" endl: .asciiz "\n" .data #*/ numbers: .word 3,4,12,28,17 ans: .asciiz "sum = " endl: .asciiz "\n" VI. Les différents modes d’adressage Immediate adressing : li $v0, 10 Direct memory addressing : lw $t0, mynum Indirect addressing : lb $t0, ($t2) Indexed addressing : lb $t0, mystring($t3) (register and constant) (register and memory) (store in register the value pointed by another register) (mystring is a big constant number (t3 starts at 0 and get incrementend : 0, 1, 2… If it is a string, then we go from one to one and we load a byte : lb $t0, mystring($t3), where t3 = 0, 1, 2… If is an array of integers, then we load a word : lw $t4, mynum($t5), where t5 = 0, 4, 8, 12… Philippe Giabbanelli 7 Notes sur l’assembleur MIPS VII. Utilisation de la pile On utilise le stack pointer $sp (valeur $29, initialisé par le système d’exploitation). Pour chaque appel d’une fonction à une autre, le schéma est le suivant : save $ra jal function restore $ra jr $ra Push 42 Push 47 Pop $v0 li $t0, 42 # push $t0 on the stack sub $sp, 4 sw $t0, ($sp) add $t0, 4 # now 47 sub $sp, 4 # push t0 sw $t0, ($sp) lw $v0, ($sp) add $sp, 4 Code segment Data segment On ajoute à la pile pour créer de l’espace, et on soustrait pour libérer. li $t0, 0x40 # the number that we are going to use as a mask lw $s0, ($sp) # the number in top of the stack is the amount of numbers in the stack add $sp, 4 # POP ! li $s1, 0 # the current sum addition: beqz $s0, done # have we saw all the numbers ? If so, we stop. jal popword and $t1, $v0, $t0 beqz $t1, skip # if the number doesn't have the bit set, we skip it add $s1, $s1, $v0 # else we sum it skip: sub $s0, $s0, 1 j addition done: la $a0, ans # we print "sum is = " li $v0, 4 syscall move $a0, $s1 # we print the sum that we have computed li $v0, 1 syscall la $a0, endl # we print an endline li $v0, 4 syscall li $v0, 10 # we end the program syscall popword: lw $v0, ($sp) add $sp, 4 j $ra Philippe Giabbanelli ## The program must sum numbers ## stored on the stack that have bit 6 set. ## The word on the top of the stack tells ## you how many numbers are in the sequence. ## Do not include this first word in the sum. 8 Notes sur l’assembleur MIPS Heap ↓ ↑ Stack search: # launcher to the algorithm sub $sp, 4 sw $ra,($sp) jal search_start # we launch the algorithm lw $ra, ($sp) # restore return adress add $sp, 4 # free the space on stack jr $ra # the result is already in $v0 because of search_start search_start: # a1 is the level in which we are ; a0 is the current node sub $sp, $sp, 12 sw $ra,($sp) sw $a1,4($sp) sw $a0,8($sp) lw $a0,($a0) # take the pointer to the string as parameter a1 jal store_path lw $a0, 8($sp) lw $t0,12($a0) # t0 is the value in the node li $t3, 1 bne $t0, $t3, ssskip1 # if value == 1... li $v0, 1 # v0 = 1 lw $ra,($sp) # we take the return ## Write a function named search that will do a addi $sp,12 # free the space ## depth first search of a tree for a marked jr $ra # and return ## node. A marked node is one that has a value ## field equal to 1. Only one node in the tree is ssskip1: ## marked. lw $t1, 4($a0) # pointer to the left tree ## beqz $t1, ssskip2 # if we have a left node ## The parameters to search are a pointer to the lw $a1, 4($sp) # current level in a0 ## tree and the current depth. On each recursive addi $a1, 1 # increase it ## call add 1 to the depth. This parameter is move $a0, $t1 # give the node as parameter ## used to keep track of the path from the root jal search_start ## to the marked node; as you visit a node, you beqz $v0, ssskip2 # if result is true ## will call a procedure named store_path to lw $ra, ($sp) # take return ## record the fact that you have visited this addi $sp, 12 # free space ## node. The code for store_path and print_path jr $ra # return ## (called after you get back from the procedure) ## have been written for you -- all you need to ssskip2: ## do is understand how to set up their parameters lw $a0, 8($sp) ## and make the call. lw $t2, 8($a0) # pointer to the right tree ## beqz $t2, sssend # same stuff than previously ## The code for search could look like: lw $a1, 4($sp) ## call store_path addi $a1, 1 ## if (value == 1) move $a0, $t2 ## return 1 jal search_start ## if (left tree exists) beqz $v0, sssend ## if (search(left tree, depth+1)) lw $ra, ($sp) ## return 1 addi $sp, 12 ## if (right tree exists) jr $ra ## return search(right tree, depth+1) sssend: ## return 0 li $v0, 0 # else we didn't find it : v0 = 0 ## lw $ra, ($sp) # load return, free space, return ## Output format must be: addi $sp, 12 ## "apple-->orange-->plum-->grape-->star-->passion" jr $ra Philippe Giabbanelli 9 Notes sur l’assembleur MIPS # Formal algorithm that we will implement # evaluate(Tree T){ # if( OPERATOR ) # return OPERATOR :: Right, Left # else if ( NUMBER ) # return Number ## Write a function "evaluate" that will return the integer result ## of evaluating an arithmetic expression tree. ## Argument is a pointer to the root of the tree. Each node contains ## -- an operation: '+', '-', '*', or '/' ## followed by two pointers (not null) to a subtree ## OR ## -- the value 0, followed by an integer # Nodes will either contain an operator and 2 pointers, or 0 and one binary number evaluate: # a0 is the root of the expression tree move $t0, $ra jal isOperator move $ra, $t0 bgt $v0, $0, numberDetected sub $sp, 12 sw $ra, 0($sp) # 0($sp) = return adress sw $a0, 4($sp) # 4($sp) = our node lw $a0, 4($a0) # point to the first subtree jal evaluate # call on first subtree sw $v0, 8($sp) # 8($sp) = result from the first subtree lw $a0, 4($sp) # point to the root... lw $a0, 8($a0) # ...and take its subtree jal evaluate # call on the other subtree lw $t0, 4($sp) # what is the kind of operator that we had ? lw $t0, ($t0) lw $t1, 8($sp) # result from the first subtree bne $t0, '+', step2 # RETURN 1 IF NOT AN OPERATOR. # apply + isOperator: add $v0, $t1, $v0 # v0 = subtree1 + subtree2 sub $sp, 4 j stepEnd sw $a0, ($sp) step2: lw $a0, ($a0) bne $t0, '-', step3 bne $a0, '+', oStep2 # apply j oEndTrue sub $v0, $t1, $v0 # v0 = subtree1 - subtree2 oStep2: j stepEnd bne $a0, '-', oStep3 step3: j oEndTrue bne $t0, '*', step4 oStep3: # apply * bne $a0, '/', oStep4 mul $v0, $t1, $v0 # v0 = subtree1 * subtree2 j oEndTrue j stepEnd oStep4: step4: bne $a0, '*', oEndFalse # apply / j oEndTrue div $v0, $t1, $v0 # v0 = subtree1 / subtree2 oEndFalse: stepEnd: li $v0, 1 lw $ra, 0($sp) lw $a0, ($sp) add $sp, 12 add $sp, 4 jr $ra jr $ra numberDetected: oEndTrue: lw $t0, 4($a0) # take the number... li $v0, -1 add $v0, $t0, $0 # put the number in v0 lw $a0, ($sp) jr $ra add $sp, 4 jr $ra Philippe Giabbanelli 10 Notes sur l’assembleur MIPS