• 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 18/07/2024

Faites le lien entre la compilation C et l'assembleur

Il est clair qu’un projet, même modeste ne sera pas écrit en langage d’assemblage, mais directement en langage C. La connaissance de ce premier et primitif langage bas niveau permet juste de mieux appréhender le code produit par un compilateur. Le but de ce chapitre est donc d’explorer les liens entre ces deux étages de la fabrication d’un code exécutable. C’est l’occasion aussi de détailler l’ensemble de cette chaîne.

Principe et chaîne de compilation pour ARM

Pour construire une application embarquée, il est plus agréable d’utiliser une suite logicielle appelée IDE (Integrated Development Environment). Keil µVision en est un exemple.

Un bon IDE est constituée de 6 éléments :

  • Un éditeur : permet de saisir du code. Il doit être syntaxique (il reconnait par exemple les mots clés du langage et les colorise) pour maximiser le confort et la productivité de celui qui se trouve de l’autre côté du clavier.

  • Un compilateur : outil logiciel qui transforme un code en langage structuré (langage C dans notre cas) en un code assembleur compatible avec le processeur sur lequel le code va être exécuté. Généralement le fichier en langage d’assemblage n’est pas directement produit.

  • Un assembleur : outil logiciel qui permet de transformer un fichier assembleur en fichier « objet » c’est-à-dire en un fichier exécutable incomplet. Il manque notamment toutes les adresses où seront implantées les variables et les procédures dans l’exécutable final.

  • Un linker ou éditeur de liens : outil logiciel qui réunit l’ensemble des différents fichiers « objets » constituant le projet et qui attribue les adresses physiques de toutes les entités. Au final, il fabrique une image complète de l’exécutable.

  • Un loader ou chargeur : outil logiciel et matériel qui permet de transférer l’image du programme en mémoire du processeur cible.

  • Un débugger : outil logiciel et matériel qui permet de « prendre la main » sur l’exécution du processeur cible afin d’observer et de contrôler le déroulement du code sans modification de celui-ci.

Le loader et le débugger utilise une sonde pour faire l’interface matériel entre le PC de développement et le processeur. Il existe plusieurs types de sondes qui utilisent un protocole JTAG ou SW(Serial Wire).

Schématiquement cela mène à une structuration sous la forme : 

d

Mis à part les fichiers sources (*.c, *.s ou *.asm) et les fichiers objets (*.o ou *.obj) les principaux fichiers crées par les différents étages sont :

  • Les fichier listing (*.lst) :  contiennent des informations sur les erreurs de compilation et/ou d’assemblage.

  • Le fichier mapping (*.map) : contient l’ensemble des informations relatives à l’organisation mémoire de l’application. On peut y trouver entre autres les adresses physiques où seront implémentées les variables, les procédures, les sections, etc.

  • Le fichier exécutable ( *.axf ou *.elf ou *.hex) : contient l’image (en binaire ou en version éditable de l’application)

D’autres fichiers peuvent être utilisées :

  • des librairies (*.lib) : ce sont des fichiers objets un peu particuliers contenant un ensemble de procédures. Ils sont fournis généralement par un tiers qui ne tient pas à divulguer ses sources. Lors de l’édition de liens, ne seront insérés dans le code final que les parties des procédures réellement utilisées et nécessaires à l’application. En agissant ainsi, l’application ne se trouve pas alourdie par l’ajout d’une librairie complète et souvent conséquente en volume de code,

  • des scatter files (*.sct ou *.ld) : pour faire son travail l’éditeur de lien a besoin d’avoir des informations sur les quantités et les types de mémoire qui existent dans la cible. Une telle spécification est contenue dans les scatter files. À noter que lorsque l’on utilise une IDE, cette information peut aussi directement être éditée dans les options de la cible. Pour Keil par exemple on peut voir :

    s

Mixer langage d’assemblage et langage  C

Dans une application il peut être intéressant de mixer des fichiers écrits en langage C avec des fichiers écrits en langage d’assemblage. Quand on procède ainsi deux questions se posent :

  • comment appeler une fonction écrite en langage d’assemblage depuis un source C ?

  • comment appeler une fonction écrite en langage C depuis un source en langage d’assemblage ?

Pour cela il faut connaître les règles de passage par défaut du compilateur C. Il est en effet incontournable d’en passer par là car on ne peut pas modifier ces règles.

Comme il a été écrit dans le chapitre 2.4, le compilateur passe jusqu'à quatre arguments d’entrée par R0, R1, R2 et R3. Au-delà il passera par la pile système, mais nous ne développerons pas ce point ici (on va supposer que nous jouons modeste et que nos fonctions ne seront pas plus consommatrices d’arguments d’entrée).

Le return du C se fera uniquement par R1, éventuellement étendu à R2 si la donnée à retourner est codée sur 64 bits.

Prenons un exemple d’une fonction Mulint écrite en langage d’assemblage et qui multiplie 2 entiers 32 bits, le résultat sera sur 64 bits si on ne veut pas perdre de données. Connaissant les techniques du compilateur, cette fonction pourra s’écrire directement :

Mulint 	PROC 
	SMULL R0,R1,R0,R1
	BX LR
	ENDP

Voyons maintenant le code C et le désassemblage de l’appel à cette fonction :

Le compilateur dépose bien (❶) Valeur1 dans R0 (après un accès à la literal pool, voir section 2.2.2 à ce sujet) et (❷) Valeur2 dans R1. L’appel à la procédure se fait classiquement par un BL (Branch and Link). Au retour le stockage dans Resu  (❸) est réalisé avec une instruction de store multiple (STM) , textuellement le contenu des registres R0 et R1 est stocké à l’adresse contenue dans R2.

Pour faire cet appel croisé, il suffit donc d’écrire la procédure en langage d’assemblage qui respecte la convention.

Symétriquement l’appel à une procédure en C ne posera pas plus de souci.  Regardons cela à travers un screencast :

Une dernière technique existe pour mixer C et assembleur. Elle consiste à inclure une partie de code assembleur à l’intérieur d’une fonction C en utilisant la directive de compilation __asm {}. En voici un exemple :

int AsmInC(int x)
{
	int locale;
	__asm
	{
		ADD locale, x, 1
		EOR x, locale, x
	}
	return x;
}

Par rapport aux deux techniques précédentes, il n’y a pas beaucoup d’avantages mis à part que l’on peut utiliser des variables du langage C en langage d’assemblage (ici x et locale). Mais cette technique possède des limitations et des restrictions en terme de jeu d’instructions. Le code produit est assez lisible si on utilise des entiers 32 bits puisqu’ils seront affectés directement à des registres. Dans les autres cas, cela devient plus obscur. L’usage de cette façon de mixer les langages doit donc être réservée à des besoins très spécifiques et aux programmeurs avancés qui auront pris soin d’aller consulter la documentation du compilateur.

  

À la fin de ce chapitre, vous êtes capable :  

  • d'expliquer la chaîne de compilation d'une application avec Keil µVision,

  • de réaliser un appel à du code écrit en assembleur depuis un code C. 

Exemple de certificat de réussite
Exemple de certificat de réussite