Architecture des ordinateurs
Corrigé du TP 1 : Assembleur S PARC
Arnaud Giersch, Benoît Meister et Frédéric Vivien
Remarques préliminaires :
– le nom d’un fichier contenant un programme assembleur doit obligatoirement avoir le suffixe .s ;
– on passe d’un programme assembleur à un fichier exécutable en utilisant gcc de la même manière qu’avec un
programme C.
1. Étude d’un programme en assembleur S PARC
(a) Récupérez le programme addition.s qui réalise la somme de 2 entiers. Compilez en utilisant la com-
mande gcc -o addition addition.s et exécutez-le.
URL : [Link]
Étudiez ce programme et rajoutez des commentaires explicatifs dans ce fichier addition.s.
Correction :
save %sp,-104,%sp ! réserve de l’espace sur la pile:
! Fonction addition ! 96 + 8 octets pour deux variables
! -> retourne la somme de ses deux arguments ! locale aux adresses %fp-4 et %fp-8
!------------------------------------------------------------
! printf
.section ".text" ! -> code sethi %hi(.PRINTF1),%o0 ! %o0 <- .PRINTF1
.align 4 ! aligné sur 4 octets or %o0,%lo(.PRINTF1),%o0
.global add ! exporte le symbole « add » call printf ! appel de « printf (.PRINTF1) »
nop ! « nop » après un « call »
add:
save %sp,-64,%sp ! réserve de l’espace sur la pile ! scanf
add %i0,%i1,%i0 ! calcule la somme des paramètres, add %fp,-4,%o1 ! %o1 <- %fp-4
! le résultat est la valeur de retour add %fp,-8,%o2 ! %o2 <- %fp-8
! de la fonction sethi %hi(.SCANF),%o0 ! %o0 <- .SCANF
ret ! retour de la fonction add or %o0,%lo(.SCANF),%o0
restore call scanf ! appel de
! « scanf (.SCANF1, %fp-4, %fp-8) »
nop ! « nop » après un « call »
! Programme principal
!------------------------------------------------------------ ! addition
ld [%fp-4],%o0 ! %o0 <- [%fp-4]
.section ".data" ! -> données ld [%fp-8],%o1 ! %o1 <- [%fp-8]
.align 8 ! alignées sur 8 octets call add ! appel de « add ([%fp-4], [%fp-8]) »
.PRINTF1: ! résultat dans %o0
.asciz "Entrez a et b : " ! chaîne de caractères avec zéro nop ! « nop » après un « call »
! terminal
.SCANF: ! printf
.asciz "%d %d" ! idem mov %o0,%o1 ! %o1 <- %o0
.PRINTF2: sethi %hi(.PRINTF2),%o0 ! %o0 <- .PRINTF2
.asciz "c : %d\n" ! idem or %o0,%lo(.PRINTF2),%o0
call printf ! appel de « printf (.PRINTF2, %o0) »
.section ".text" ! -> code nop ! « nop » après un « call »
.align 4 ! aligné sur 4 octets
.global main ! exporte le symbole « main » ret ! retour de la fonction main
restore
main:
(b) Réalisez un programme C faisant appel à une fonction prenant 8 paramètres, produisez le programme
assembleur correspondant (option -S de gcc : gcc -S fichiersource).
Déterminez ensuite quels paramètres sont placés dans les registres, lesquels ne le sont pas et trouvez
l’emplacement mémoire de ces derniers.
Correction :
– le programme C :
1
#include <stdio.h> int main (void)
{
int add8 (int a, int b, int c, int d, int e, int f, int g, int h) printf ("somme 1..8 = %d\n",
{ add8 (1, 2, 3, 4, 5, 6, 7, 8));
return a + b + c + d + e + f + g + h;
} return 0;
}
– le code assembleur correspondant :
.file "8arg.c" .section ".rodata"
gcc2_compiled.: .align 8
.section ".text" .LLC0:
.align 4 .asciz "somme 1..8 = %d\n"
.global add8 .section ".text"
.type add8,#function .align 4
.proc 04 .global main
add8: .type main,#function
!#PROLOGUE# 0 .proc 04
save %sp, -112, %sp main:
!#PROLOGUE# 1 !#PROLOGUE# 0
st %i0, [%fp+68] save %sp, -120, %sp
st %i1, [%fp+72] !#PROLOGUE# 1
st %i2, [%fp+76] mov 7, %o0
st %i3, [%fp+80] st %o0, [%sp+92]
st %i4, [%fp+84] mov 8, %o0
st %i5, [%fp+88] st %o0, [%sp+96]
ld [%fp+68], %o0 mov 1, %o0
ld [%fp+72], %o1 mov 2, %o1
add %o0, %o1, %o0 mov 3, %o2
ld [%fp+76], %o1 mov 4, %o3
add %o0, %o1, %o0 mov 5, %o4
ld [%fp+80], %o1 mov 6, %o5
add %o0, %o1, %o0 call add8, 0
ld [%fp+84], %o1 nop
add %o0, %o1, %o0 mov %o0, %o1
ld [%fp+88], %o1 sethi %hi(.LLC0), %o2
add %o0, %o1, %o0 or %o2, %lo(.LLC0), %o0
ld [%fp+92], %o1 call printf, 0
add %o0, %o1, %o0 nop
ld [%fp+96], %o1 mov 0, %i0
add %o0, %o1, %o0 b .LL3
mov %o0, %i0 nop
b .LL2 .LL3:
nop ret
.LL2: restore
ret .LLfe2:
restore .size main,.LLfe2-main
.LLfe1: .ident "GCC: (GNU) 2.95.3 20010315 (release)"
.size add8,.LLfe1-add8
On remarque que les 6 premiers paramètres sont passés par les registres %o0 à %o5 (%i0 à %i5 dans la
fonction), les deux derniers sont passés sur la pile aux adresses %sp+92 et %sp+96 (%fp+92 et %fp+96
dans la fonction).
2. Exercices de programmation
(a) Écrivez un programme assembleur calculant la factorielle d’un entier de manière itérative (une seule fonc-
tion principale contenant une boucle).
Correction :
!!! sethi %hi(.SCANF), %o0
!!! calcul de la factorielle d’un entier, version itérative or %o0, %lo(.SCANF), %o0
!!! add %fp, -4, %o1
call scanf ! scanf (.SCANF, %fp-4)
! Programme principal nop
!------------------------------------------------------------
! -> calcul de [%fp-4]! dans %l1,
.section ".data" ! -> données ! utilisation de %l0 comme compteur
.align 8
.PRINTF1: ld [%fp-4], %l0 ! %l0 <- [%fp-4]
.asciz "n? " mov 1, %l1 ! %l1 <- 1
.SCANF: loop:
.asciz "%u" cmp %l0, 1 ! while (%l0 > 1) {
.PRINTF2: ble end_loop !
.asciz "n! = %u\n" nop !
umul %l0, %l1, %l1 ! %l1 <- %l1 * %l0
dec %l0 ! %l0 --
.section ".text" ! -> code b loop ! }
.align 4 nop
.global main
end_loop:
main: sethi %hi(.PRINTF2), %o0
save %sp, -96, %sp ! réserve de la place sur la pile or %o0, %lo(.PRINTF2), %o0
! pour un entier [%fp-4] (mais on mov %l1, %o1
! arrondit à un multiple de 8) call printf ! printf (.PRINTF2, %l1)
nop
sethi %hi(.PRINTF1), %o0
or %o0, %lo(.PRINTF1), %o0 clr %i0 ! return 0
call printf ! printf (.PRINTF1) ret
nop restore
2
(b) Écrivez un programme assembleur calculant la factorielle d’un entier de manière récursive.
Correction :
!!! .SCANF:
!!! calcul de la factorielle d’un entier, version récursive .asciz "%u"
!!! .PRINTF2:
.asciz "n! = %u\n"
! fonction fact
! -> retourne la factorielle de son argument .section ".text" ! -> code
!------------------------------------------------------------ .align 4
.global main
.section ".text" ! -> code
.align 4 main:
.global fact save %sp, -96, %sp ! réserve de la place sur la pile
! pour un entier [%fp-4] (mais on
fact: ! arrondit à un multiple de 8)
save %sp, -96, %sp
sethi %hi(.PRINTF1), %o0
cmp %i0, 1 ! if (%i0 > 1) { or %o0, %lo(.PRINTF1), %o0
ble end_fact1 ! call printf ! printf (.PRINTF1)
nop ! nop
sub %i0, 1, %o0 ! %o0 <- %i0 - 1
call fact ! %o0 <- fact (%o0) sethi %hi(.SCANF), %o0
nop ! or %o0, %lo(.SCANF), %o0
umul %i0, %o0, %i0 ! %i0 <- %o0 * %i0 add %fp, -4, %o1
b end_fact ! } else { call scanf ! scanf (.SCANF, %fp-4)
nop ! nop
end_fact1: !
mov 1, %i0 ! %i0 <- 1 ld [%fp-4], %o0
! } call fact ! %o0 <- fact ([%fp-4])
end_fact: nop
ret ! return %i0
restore mov %o0, %o1
sethi %hi(.PRINTF2), %o0
or %o0, %lo(.PRINTF2), %o0
! Programme principal call printf ! printf (.PRINTF2, %o0)
!------------------------------------------------------------ nop
.section ".data" ! -> données clr %i0 ! return 0
.align 8 ret
.PRINTF1: restore
.asciz "n? "
(c) Modifiez-le programme précédent pour qu’il affiche à chaque étape de la récursion les valeurs des pointeurs
de pile (%sp et %fp).
Correction :
!!!
!!! calcul de la factorielle d’un entier, version récursive, ! Programme principal
!!! affichage de %fp et %sp !------------------------------------------------------------
!!!
.section ".data" ! -> données
! fonction fact .align 8
! -> retourne la factorielle de son argument .PRINTF1:
!------------------------------------------------------------ .asciz "n? "
.SCANF:
.section ".data" ! -> données .asciz "%u"
.align 8 .PRINTF2:
.PRINTF_DEBUG: .asciz "n! = %u\n"
.asciz "# fact (%u): %%fp = %p, %%sp = %p\n"
.section ".text" ! -> code
.section ".text" ! -> code .align 4
.align 4 .global main
.global fact
main:
fact: save %sp, -96, %sp ! réserve de la place sur la pile
save %sp, -96, %sp ! pour un entier [%fp-4] (mais on
! arrondit à un multiple de 8)
sethi %hi(.PRINTF_DEBUG), %o0
or %o0, %lo(.PRINTF_DEBUG), %o0 sethi %hi(.PRINTF1), %o0
mov %i0, %o1 or %o0, %lo(.PRINTF1), %o0
mov %fp, %o2 call printf ! printf (.PRINTF1)
mov %sp, %o3 nop
call printf ! printf (.PRINTF_DEBUG,
! %i0, %fp, %sp) sethi %hi(.SCANF), %o0
nop or %o0, %lo(.SCANF), %o0
add %fp, -4, %o1
cmp %i0, 1 ! if (%i0 > 1) { call scanf ! scanf (.SCANF, %fp-4)
ble end_fact1 ! nop
nop !
sub %i0, 1, %o0 ! %o0 <- %i0 - 1 ld [%fp-4], %o0
call fact ! %o0 <- fact (%o0) call fact ! %o0 <- fact ([%fp-4])
nop ! nop
umul %i0, %o0, %i0 ! %i0 <- %o0 * %i0
b end_fact ! } else { mov %o0, %o1
nop ! sethi %hi(.PRINTF2), %o0
end_fact1: ! or %o0, %lo(.PRINTF2), %o0
mov 1, %i0 ! %i0 <- 1 call printf ! printf (.PRINTF2, %o0)
! } nop
end_fact:
ret ! return %i0 clr %i0 ! return 0
restore ret
restore
Exemple d’exécution :
3
$ ./fact_r_print
n? 6
# fact (6): %fp = ffbef800, %sp = ffbef7a0
# fact (5): %fp = ffbef7a0, %sp = ffbef740
# fact (4): %fp = ffbef740, %sp = ffbef6e0
# fact (3): %fp = ffbef6e0, %sp = ffbef680
# fact (2): %fp = ffbef680, %sp = ffbef620
# fact (1): %fp = ffbef620, %sp = ffbef5c0
n! = 720
(d) Dans un processeur où la multiplication n’est pas implémentée par un circuit, celle-ci peut être réalisée
efficacement en se basant sur l’algorithme suivant :
a × b := si b = 0 alors 0
sinon si b = 2 × b0 alors (a × 2) × b0
sinon ((a × 2) × b0 ) + a
En vous appuyant sur cette méthode, proposez une fonction assembleur réalisant la multiplication entière
en utilisant uniquement des additions et des décalages.
Correction :
Deux versions,
– une version récursive :
!!! call mult ! %o0 <- mult (%o0, %o1)
!!! fonction multiplication: version récursive nop !
!!! andcc %i1, 1, %g0 ! if (%i1 & 1 != 0) {
bz even ! // -> équivalent à
.section ".text" nop ! // if (%i1 % 2 == 1) {
.align 4 add %o0, %i0, %o0 ! %o0 <- %o0 + %i0
.global mult even: ! }
mov %o0, %i0 ! %i0 <- %o0
mult: b return !
save %sp, -96, %sp nop !
zero: ! } else {
cmp 0, %i1 ! if (%i1 != 0) { mov 0, %i0 ! %i0 <- 0
be zero ! ! }
nop ! return:
sll %i0, 1, %o0 ! %o0 <- %i0 << 1 ret ! return %i0
srl %i1, 1, %o1 ! %o1 <- %i1 >> 1 restore
– une version itérative :
!!! nop !
!!! fonction multiplication: version itérative andcc %i1, 1, %g0 ! if (%i1 & 1 != 0) {
!!! bz even ! // -> équivalent à
nop ! // if (%i1 % 2 == 1) {
.section ".text" add %l0, %i0, %l0 ! %l0 <- %l0 + %i0
.align 4 even: !
.global mult sll %i0, 1, %i0 ! %i0 <- %i0 << 1
srl %i1, 1, %i1 ! %i1 <- %i1 >> 1
mult: b loop ! }
save %sp, -96, %sp nop
end_loop:
clr %l0 ! %l0 <- 0 mov %l0, %i0 ! return %l0
loop: ret
cmp 0, %i1 ! while (%i1 != 0) { restore
be end_loop !
(e) Utilisez la fonction précédente dans un programme C. L’exécutable s’obtient en donnant simplement le
fichier C et le fichier assembleur au programme gcc, ce dernier s’occupe de faire les liens.
Correction :
#include <stdio.h>
printf ("a, b? ");
extern unsigned mult (unsigned, unsigned); scanf ("%u %u", &a, &b);
printf (" %u x %u = %u\n", a, b, mult (a, b));
int main (void)
{ return 0;
unsigned a, b; }
(f) Écrivez un programme assembleur de recherche de l’élément minimum d’un tableau. Le tableau est une
variable locale à la routine principale. Cette dernière fait appel à une routine pour la recherche de l’élément
minimum d’un tableau.
Correction :
4
!!! .asciz "%d"
!!! recherche du minimum dans un tableau .PRINTF2:
!!! .asciz "minimum: [%d] = %d\n"
! Fonction min: recherche du minimum dans un tableau d’entiers
! 2 arguments: adresse du tableau et nombre d’éléments .section ".text" ! -> code
! retourne l’indice du minimum dans le tableau .align 4
! précondition: la tableau contient au moins 1 élément .global main
!------------------------------------------------------------
main:
.section ".text" ! -> code save %sp, -136, %sp ! réserve de la place pour un
.align 4 ! tableau de 10 entiers à
.global min ! l’adresse %fp-40
set .PRINTF1, %o0
min: call printf ! printf (.PRINTF1)
save %sp, -64, %sp nop
! registres locaux utilisés:
! %l0: index du min. courant ! lecture des entiers
! %l1: minimum courant mov 10, %l0 ! %l0 <- 10 (nombre d’entiers
! %l2: index courant ! à lire)
! %l3: valeur courante sub %fp, 40, %l1 ! %l1 <- %fp-40 (adresse du
! tableau)
clr %l0 ! %l0 <- 0 read_loop: ! do {
ld [%i0], %l1 ! %l1 <- tab[0] set .SCANF, %o0 !
clr %l2 ! %l2 <- 0 mov %l1, %o1 !
orcc %i1, %g0, %g0 call scanf ! scanf (.SCANF, %l1)
min_loop: ! while (%i1 != 0) { nop !
bz end_min ! add %l1, 4, %l1 ! %l1 <- %l1
nop ! ! + sizeof (int)
inc %l2 ! %l2 ++ deccc %l0 ! %l0 --
add %i0, 4, %i0 ! %i0 <- %i0 bnz read_loop ! } while (%l0 != 0)
! + sizeof(int) nop
ld [%i0], %l3 ! %l3 <- tab[%l2]
cmp %l1, %l3 ! if (%l1 > %l3) { ! calcul du minimum
ble min_ok ! sub %fp, 40, %o0 ! %o0 <- %fp-40 (adresse du
nop ! ! tableau)
mov %l2, %l0 ! %l0 <- %l2 mov 10, %o1 ! %o1 <- 10 (taille du
mov %l3, %l1 ! %l1 <- %l3 ! tableau)
min_ok: ! } call min ! %o0 <- min (%o0, %o1)
deccc %i1 ! %i1 -- nop
b min_loop ! }
nop ! affichage du résultat
mov %o0, %o1 ! %o1 <- %o0 (indice du min.)
end_min: sll %o0, 2, %o0 ! %o0 <- %o0 * 4
mov %l0, %i0 ! return %l0 ! (4 == sizeof (int))
ret sub %o0, 40, %o0 ! %o0 <- %o0 - 40
restore ld [%fp+%o0], %o2 ! %o2 <- [%fp+%o0] (tab[%o1],
! valeur du min.)
set .PRINTF2, %o0
! Programme principal call printf ! printf (.PRINTF2,
! -> lit 10 entiers et trouve le minimum ! indice_du_min,
!------------------------------------------------------------ ! valeur_du_min)
nop
.section ".data" ! -> données
.align 8 clr %i0 ! return 0
.PRINTF1: ret
.asciz "Entrez 10 entiers:\n" restore
.SCANF: