Construire son environnement de "cross-compiling"
-------------------------------------------------

{ Auteur: Guillaume Thouvenin, initialement publié dans le journal LJNB }
{ http://home.nordnet.fr/rldt-adsl2/download/CrossCompiler/cross_compiler.htm }

{ Adaptation pour la cros-compilation MIPS : Christophe Cérin, 7 février 2008 }

    Plan 

    0) Introduction
    1) Préliminaires
    2) Mise en place du cross-compiler
      2.1) Compilation de binutils
      2.2) Compilation de GCC
      2.3) Compilation de la Glibc
      2.4) Compilation supplémentaires
    3) Test
    4) Conclusion
    5) Références

  ---------------------------------------------------------------------------

0 - Introduction
================

  Un "cross-compiler" est un compilateur qui produit du code pour une machine 
possédant une architecture différente de celle sur laquelle il s'exécute. Dans 
cet article nous allons voir comment configurer un environnement permettant de
compiler un programme pour une architecture mips (la cible) en utilsant une
architecture i686 (l'hôte). Nous présenterons les différentes étapes de la 
compilation avant d'entrer dans le vif du sujet.

  Dans le cours d'architecture, vous avez développé des programmes en
assembleur MIPS, testés dans le simulateur SPIM
(http://pages.cs.wisc.edu/~larus/spim.html)

  Que diriez vous de développer en language C et de demander à gcc de
générer l'assembleur MIPS, puis de le tester dans le simulateur SPIM ?
Voila ce dont nous discutons ici : compiler, sur une architecture x86,
les sources de gcc pour qu'il produise du code assembleur mips.

1 - Préliminaires
=================

  Nous pouvons distinguer quatre étapes dans la compilation:

  Étape 1 - pré-traitement "preprocessing" (cpp0) qui traite les directives 
            de compilation, les macros et en règle générale tout ce qui
            commence par #.

  Étape 2 - la compilation (cc1) qui produit du code assembleur (et donc
            spécifique à une architecture donnée) 
  
  Étape 3 - l'assemblage (as) qui produit un fichier objet à partir du code 
            assembleur issu de la compilation. L'un des formats les plus
            répandus est le format ELF [1].
  
  Étape 4 - l'édition de lien (collect2, nm, strip, ld, ...) dont le rôle 
            est de faire le lien entre les différentes fonctions utilisées 
            dans les fichiers objets. C'est à cette étape que le fichier 
            exécutable est généré


  Voici une compilation faites étapes par étapes :

  [guill@leffe] $ gcc -E prog1.c > prog1.i
  [guill@leffe] $ /usr/lib/gcc-lib/i386-redhat-linux/2.96/cc1 prog1.i
   main
  Execution times (seconds)
  parser                :   0.01 (100%) usr   0.00 ( 0%) sys   0.01 (100%) wall
  varconst              :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  jump                  :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  flow analysis         :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  local alloc           :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  global alloc          :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  flow 2                :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  shorten branches      :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  reg stack             :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  final                 :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  symout                :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  rest of compilation   :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall
  TOTAL                 :   0.01             0.00             0.01
  [guill@leffe] $ as -o prog1.o prog1.s
  [guill@leffe] $ file prog1.o
  prog1.o: ELF 32-bit LSB relocatable, Intel 80386, version 1, not stripped
  
  L'édition de liens est un peu plus compliquée puisqu'il faut indiquer ou se
trouve les différentes librairies, le symbole de départ, etc... Si vous
voulez absolument le faire nous vous conseillons de regarder une trace de
compilation (strace).
  
  Bien sûr, lorsque nous compilons un programme il n'est pas nécessaire de 
faire ces étapes manuellement puisque le compilateur de GNU (GCC) s'occupe 
d'ordonnancer ces appels avec les bons paramètres. 
	    
  Si nous avons expliqué tout ça c'est pour bien comprendre les programmes que
nous devons recompiler afin de mettre en oeuvre notre plateforme de
développement. Les outils "as" et "ld" utilisés dans les étapes 3 et 4 sont
fournis par le paquetage binutils [2]. Le compilateur GCC [3] fournit les 
outils "cpp", "cc" et "collect2" utilisés lors des étapes 1, 2 et 4. 
De plus, nous allons avoir besoin de recompiler la librairie C. Nous avons 
choisi la glibc [4] mais il existe d'autres librairies plus compactes comme 
par exemple la newlib [5] qui est surtout utilisée dans les systèmes embarqués.

  Pour récapituler, nous allons avoir besoin de recompiler au minimum trois
choses : binutils, gcc et la libc. L'ordre de compilation est important car 
comme nous l'avons vu le programme gcc s'occupe d'organiser les différentes 
étapes de la compilation et notamment. Il a donc besoin de connaître les
emplacements de certains programmes comme par exemple le gestionnaire
d'archives ar fournit par binutil. Donc, nous allons commencer par compiler
binutils, ensuite nous compilerons gcc et enfin la librairie C. Si vous
souhaitez compiler g++, il faudra le faire après la compilation de la libc
puisque celle-ci est nécessaire à sa compilation. Vous devrez donc en premier
lieu compiler gcc avec uniquement le support du C après les binutils et
après la compilation des librairies C vous pourrez ajouter le support pour
C++, Objective C ou autre. 

2 - Mise en place du cross-compiler
===================================

  La première chose que nous faisons est la création d'un espace de travail.
Nous avons créé un répertoire de travail dans le /home/guill que nous avons 
appelé cross-compiler. Nous avons voulu éviter d'être administrateur pour la 
suite de l'installation (ce qui évite d'écraser par erreur sa libc-i386 par la 
libc-ppc que nous verrons plus tard). Le répertoire accueillant notre 
cross-compiler sera /home/guill/cross-compiler/ppc. Nous allons aussi créer un
répertoire src et obj qui contiendront respectivement les sources et les 
fichiers de compilations des différentes parties de notre environnement.

  [guill@leffe:cross-compiler] $ pwd
  /home/guill/cross-compiler
  [guill@leffe:cross-compiler] $ mkdir ppc
  [guill@leffe:cross-compiler] $ mkdir src
  [guill@leffe:cross-compiler] $ mkdir obj
  
  2.1 - Compilation de binutils
  -----------------------------

  Nous avons utilisé la version 2.18 de binutils.

  [guill@leffe:src] $ tar zxvf binutils-2.18.tar.gz
  [guill@leffe:src] $ cd ../obj && mkdir binutils && cd binutils
  [guill@leffe:binutils] $ ../../src/binutils-2.18/configure \
  > --prefix=/home/guill/cross-compiler/mips \
  > --target=mips-elf && make && make install

  Pour binutils nous voyons que les options sont simples. L'option "prefix"
indique l'endroit ou nous installerons les programmes et l'option "target"
précise l'architecture cible. Nous n'avons pas précisé l'architecture de la
machine hôte car celle-ce sera détectée par le programme de configuration. Si
la machine hôte est différente de celle sur utilisée pour la mise en place de 
l'environnement de cross-compiling alors vous devrez utiliser l'option "host".
Les programmes de binutils et de gcc ne sont pas forcement synchronisés et la 
dernière version de binutils ne fonctionnera peut-être pas avec la dernière 
version de gcc. Malheureusement pour le savoir la seule solution est d'essayer.
Donc, si vous souhaitez compiler votre propre environnement de 
"cross-compiling" regardez sur le web ce qui marche ou ne marche pas. Lorsque 
la compilation et l'installation sont terminés vous devriez obtenir ça :

  [16:15:56: ~/cross-compiler/mips] $ ls -l
  total 28
  drwxr-x---  2 cerin ensinfo 4096 fév  7 16:10 bin
  drwxr-xr-x  2 cerin ensinfo 4096 fév  7 16:03 include
  drwxr-x---  2 cerin ensinfo 4096 fév  7 16:03 info
  drwxr-x---  3 cerin ensinfo 4096 fév  7 16:03 lib
  drwxr-x---  4 cerin ensinfo 4096 fév  7 16:03 man
  drwxr-x---  4 cerin ensinfo 4096 fév  7 14:30 mips-elf
  drwxr-x---  3 cerin ensinfo 4096 fév  7 14:28 share

  2.2 - Compilation de GCC
  ------------------------

  Pour la compilation de gcc nous avons utilisé gcc-3.2. Si vous souhaitez 
compiler g++, ne le faites pas maintenant car comme nous l'avons dit, nous
avons besoin des librairies pour l'architecture PowerPC qui n'est pas encore
compilée. Pour la compiler, nous avons besoin du cross-compiler donc, la
première chose est de compiler gcc avec le support C uniquement.

  [guill@leffe:ppc] $ cd ../src
  [guill@leffe:src] $ tar zxvf gcc-3.2.tar.gz
  [guill@leffe:src] $ cd ../obj && mkdir gcc && cd gcc

  Attention, ici gcc va avoir besoin de connaître l'emplacement des programmes
de binutils s'exécutant sur i686 mais ayant pour cible l'architecture mips.
Donc il faut ajouter l'emplacement de ces programmes dans le "path" (utiliser
la commande export ou setenv ou ce qui va bien)

  [guill@leffe:gcc] $ export PATH=/home/guill/cross-compiler/mips/bin:$PATH
  [guill@leffe:gcc] $ ../../src/gcc-3.2/configure \
  > --prefix=/home/guill/cross-compiler/mips \
  > --target=mips-elf \
  > --enable-languages=c \
  > --with-gnu-as \
  > --with-newlib &&
  > make && make install
  
  Si tout c'est bien passé vous devriez avoir dans le répertoire de
cross-compiling les fichiers et dossiers suivants :

[16:16:05: ~/cross-compiler/mips] $ ls -l bin/
total 42132
-rwxr-xr-x  1 cerin ensinfo 2971786 fév  7 14:29 mips-elf-addr2line
-rwxr-xr-x  2 cerin ensinfo 3169711 fév  7 14:29 mips-elf-ar
-rwxr-xr-x  2 cerin ensinfo 5282008 fév  7 14:30 mips-elf-as
-rwxr-xr-x  1 cerin ensinfo 2938826 fév  7 14:29 mips-elf-c++filt
-rwxr-xr-x  1 cerin ensinfo  301813 fév  7 16:03 mips-elf-cpp
-rwxr-xr-x  1 cerin ensinfo  299534 fév  7 16:03 mips-elf-gcc
-rwxr-xr-x  1 cerin ensinfo   15757 fév  7 16:03 mips-elf-gccbug
-rwxr-xr-x  1 cerin ensinfo  118014 fév  7 16:03 mips-elf-gcov
-rwxr-xr-x  2 cerin ensinfo 3956516 fév  7 14:30 mips-elf-ld
-rwxr-xr-x  2 cerin ensinfo 3007427 fév  7 14:29 mips-elf-nm
-rwxr-xr-x  2 cerin ensinfo 3540276 fév  7 14:29 mips-elf-objcopy
-rwxr-xr-x  2 cerin ensinfo 3820562 fév  7 14:29 mips-elf-objdump
-rwxr-xr-x  1 cerin ensinfo  172718 fév  7 16:03 mips-elf-protoize
-rwxr-xr-x  2 cerin ensinfo 3169742 fév  7 14:29 mips-elf-ranlib
-rwxr-xr-x  1 cerin ensinfo  577902 fév  7 14:29 mips-elf-readelf
-rwxr-xr-x  1 cerin ensinfo 3002809 fév  7 14:29 mips-elf-size
-rwxr-xr-x  1 cerin ensinfo 2971816 fév  7 14:29 mips-elf-strings
-rwxr-xr-x  2 cerin ensinfo 3540275 fév  7 14:29 mips-elf-strip
-rwxr-xr-x  1 cerin ensinfo  165562 fév  7 16:03 mips-elf-unprotoize

On peut maintenant compiler et générer notre premier assembleur mips.

[16:22:06: ~/cross-compiler/mips/bin] $ ./mips-elf-gcc -S essai.c

et le fichier essai.s contient alors :

	.file	1 "essai.c"
	.section .mdebug.abi32
	.previous
	.text
	.align	2
	.globl	main
	.ent	main
main:
	.frame	$fp,16,$31		# vars= 8, regs= 1/0, args= 0, extra= 0
	.mask	0x40000000,-8
	.fmask	0x00000000,0
	subu	$sp,$sp,16
	sw	$fp,8($sp)
	move	$fp,$sp
	sw	$0,0($fp)
	sw	$0,4($fp)
	sw	$0,0($fp)
$L2:
	lw	$2,0($fp)
	slt	$2,$2,10
	bne	$2,$0,$L5
	j	$L3
$L5:
	lw	$2,4($fp)
	addu	$2,$2,10
	sw	$2,4($fp)
	lw	$2,0($fp)
	addu	$2,$2,1
	sw	$2,0($fp)
	j	$L2
$L3:
	lw	$2,4($fp)
	move	$sp,$fp
	lw	$fp,8($sp)
	addu	$sp,$sp,16
	j	$31
	.end	main


Ce fichier peut maintenant être repris dans SPIM (après avoir enlevé
la deuxième et troisième ligne) :

[16:24:06: ~/cross-compiler/mips/bin] $ spim
SPIM Version 7.0 of July 7, 2004
Copyright 1990-2004 by James R. Larus (larus@cs.wisc.edu).
All Rights Reserved.
See the file README for a full copyright notice.
Loaded: /var/lib/spim/exceptions.s
(spim) load "essai.s"
(spim) run
(spim)


On peut demander à voir les registres pour retrouver la valeur 100
qui est le résultat :

 (spim) print_all_regs
 PC      = 00000000   EPC     = 00000000   Cause   = 00000000   BadVAddr= 00000000
 Status  = 3000ff10   HI      = 00000000   LO      = 00000000
                                 General Registers
R0  (r0) = 0          R8  (t0) = 0          R16 (s0) = 0          R24 (t8) = 0 
R1  (at) = 0          R9  (t1) = 0          R17 (s1) = 0          R25 (t9) = 0 
R2  (v0) = 10         R10 (t2) = 0          R18 (s2) = 0          R26 (k0) = 0 
R3  (v1) = 0          R11 (t3) = 0          R19 (s3) = 0          R27 (k1) = 0 
R4  (a0) = 0          R12 (t4) = 0          R20 (s4) = 0          R28 (gp) = 268468224
R5  (a1) = 2147476028 R13 (t5) = 0          R21 (s5) = 0          R29 (sp) = 2147476024
R6  (a2) = 2147476032 R14 (t6) = 0          R22 (s6) = 0          R30 (s8) = 0 
R7  (a3) = 0          R15 (t7) = 0          R23 (s7) = 0          R31 (ra) = 4194328

 FIR    = 00009800    FCSR    = 00000000    FCCR   = 00000000   FEXR    = 00000000
 FENR   = 00000000
                              Double Floating Point Registers
FP0  = 0.00000       FP8  = 0.00000       FP16 = 0.00000       FP24 = 0.00000 
FP2  = 0.00000       FP10 = 0.00000       FP18 = 0.00000       FP26 = 0.00000 
FP4  = 0.00000       FP12 = 0.00000       FP20 = 0.00000       FP28 = 0.00000 
FP6  = 0.00000       FP14 = 0.00000       FP22 = 0.00000       FP30 = 0.00000 
                              Single Floating Point Registers
FP0  = 0.00000       FP8  = 0.00000       FP16 = 0.00000       FP24 = 0.00000 
FP1  = 0.00000       FP9  = 0.00000       FP17 = 0.00000       FP25 = 0.00000 
FP2  = 0.00000       FP10 = 0.00000       FP18 = 0.00000       FP26 = 0.00000 
FP3  = 0.00000       FP11 = 0.00000       FP19 = 0.00000       FP27 = 0.00000 
FP4  = 0.00000       FP12 = 0.00000       FP20 = 0.00000       FP28 = 0.00000 
FP5  = 0.00000       FP13 = 0.00000       FP21 = 0.00000       FP29 = 0.00000 
FP6  = 0.00000       FP14 = 0.00000       FP22 = 0.00000       FP30 = 0.00000 
FP7  = 0.00000       FP15 = 0.00000       FP23 = 0.00000       FP31 = 0.00000 

On ne la retrouve pas ! Veuillez expliquer.

Par contre, si on recompile avec l'option d'optimisation -O4:

[16:34:25: ~/cross-compiler/mips/bin] $ ./mips-elf-gcc -O4 -S essai.c

on obtient l'assembleur suivant où on remarque que le compilateur a 
déduit la valeur de sortie de boucle et n'implémente pas d'itération :

	.file	1 "essai.c"
	.section .mdebug.abi32
	.previous
	.text
	.align	2
	.globl	main
	.ent	main
main:
	.frame	$sp,0,$31		# vars= 0, regs= 0/0, args= 0, extra= 0
	.mask	0x00000000,0
	.fmask	0x00000000,0
	li	$2,9			# 0x9
	addu	$2,$2,-1
$L9:
	.set	noreorder
	.set	nomacro
	bgez	$2,$L9
	addu	$2,$2,-1
	.set	macro
	.set	reorder

	addu	$2,$2,1
	.set	noreorder
	.set	nomacro
	j	$31
	li	$2,100			# 0x64
	.set	macro
	.set	reorder

	.end	main

Note 1 : si votre code C contient des I/O (prinft par exemple), il faudra
isoler dans l'assembleur produit par gcc l'appel et remplacer le morceau
de code par l'interface adéquate SPIM (voir la doc SPIM). Idem
pour la lecture au clavier.

Note 2 : le code produit par notre gcc n'est pas tout a fait conforme
a ce qui est attendu par SPIM concernant le préambule et le
post-ambule (le code concernant la sortie du programme). Ces deux
parties de code sont à retoucher à la main en se
reportant à la documentation SPIM.

  2-3 Compilation de la Glibc
  ---------------------------

  Les compilations précédentes sont suffisantes pour compiler un
programme C jusqu'à la production de l'assembleur. Si vous voulez
aller jusqu'à la génération d'un exécutable, gérer les #include, il
vous faudra installer la glibc, la compiler avec notre nouveau gcc
pour mips. Nous ne le faisons pas ici car c'est beaucoup plus difficile.

3 - Références
==============

      CrossGCC Frequently Asked Questions
      http://www.sthoward.com/CrossGCC/

      penguinppc.org - The home of the linux/ppc port
      http://penguinppc.org/embedded/cross-compiling

  [1] ELF: Executable and Linking Format
      http://www.cs.ucdavis.edu/~haungs/paper/node10.html

  [2] ftp://ftp.kernel.org/pub/linux/devel/binutils/

  [3] GNU C Compiler
      http://gcc.gnu.org/

  [4] GNU C Library
      ftp://ftp.gnu.org/pub/gnu/glibc/

  [5] Newlib C library
      http://sources.redhat.com/newlib/