Protection mémoire pour eCos/ARM9
Auteur : Ivan Djelic
<ivan.djelic@parrot.fr>
Dernière mise à jour : 16/08/2005
La protection mémoire est un package pour eCos qui permet d'exécuter du code
sur cible avec des restrictions sur l'accès à la mémoire, en exploitant la MMU
des processeurs ARM9. L'intérêt principal est de détecter des bugs typiques du
C difficiles à trouver autrement (pointeurs fantômes, débordement de piles,
corruption de mémoire, etc).
Note: Dans toute la suite, on entend par noyau l'ensemble du système
eCos, c'est-à-dire non seulement le noyau eCos (kernel),
mais également les
librairies libc, libm, et plus généralement
l'ensemble des paquets disponibles.
La protection mémoire possède les caractéristiques suivantes:
- L'ensemble des threads est partitionné en 16 sous-ensembles ou
domaines d'exécution, numérotés de 0 à 15.
Une thread possède des privilèges qui dépendent de son domaine :
- domaine 0 (threads système) :
Les threads de ce domaine s'exécutent dans le mode SYSTEM du
processeur ARM9, elles ont un accès aux :
- code (en lecture seule) et données du noyau,
- registres d'entrée/sortie, registres de coprocesseurs, registres FPGA,
RAM physique, etc.,
- code (en lecture seule) et données de toutes les threads.
Les seules régions inaccessibles de la mémoire sont celles qui n'ont pas
été mappées par la MMU.
- domaine 1 à 15 (threads utilisateur) :
Ces threads s'exécutent dans le mode USER du processeur ARM9.
Une thread du domaine d a accès à :
- son code (en lecture seule),
- ses données (accès partagé avec toutes les autres threads du domaine d),
- sa pile (et aux piles des autres threads du domaine d),
- code et données du noyau (en lecture seule).
Les autres régions de la mémoire (notamment les
code/données/pile d'une thread appartenant à un autre domaine, les
registres d'entrées/sorties, etc.) sont totalement inaccessibles.
- La pile d'une thread utilisateur est précédée et suivie de zones mémoire
inaccessibles de manière à détecter un débordement de pile.
- Lorqu'une thread tente d'outrepasser ses droits d'accès, une exception
est déclenchée et la thread est arrêtée.
- Lorsqu'une thread exécute un appel à une fonction du noyau
(i.e. une fonction interne à eCos), les arguments sont vérifiés, puis l'appel
est effectué en mode SYSTEM.
- Le domaine d'une thread est choisi statiquement lors de la compilation,
il ne peut pas être modifié dynamiquement.
Pour installer la protection mémoire, il vous faut :
-
Récupérer la dernière version d'eCos disponible sur le serveur CVS
canari [192.168.1.209].
-
Dans l'outil configtool, ajouter
(menu Build → Packages) le paquet suivant :
"Memory protection for ARM architectures with MMU" (memprot).
Vous pouvez également importer directement un fichier de configuration pour
eCos (voir la
liste des configurations disponibles).
Vous aurez éventuellement à cliquer sur Continuer dans la fenêtre de
résolution automatique de conflits.
Après installation, vous devriez voir les options suivantes dans le configtool:
- La protection mémoire est maintenant prête à être utilisée.
Lorsque la protection mémoire est activée, il faut pour l'utiliser associer
à chaque module de code xxx.o un domaine d'exécution.
Lors de la compilation, cette association est effectuée à l'aide d'une
réécriture du code objet par la commande objcopy. Le script shell
put_in_domain
permet d'effectuer simplement cette association :
domain=$( printf "%02d" "$1" )
if [ $domain = "00" ]; then
echo "Invalid domain: domain 0 is implicit and needs no assignment."
exit 1
fi
temp=$( tempfile )
out=$2
if [ ! -z "$3" ]; then
out=$3
fi
arm-elf-objcopy --prefix-sections=__memprot_vp${domain}_ $2 $temp
arm-elf-objcopy \
--rename-section __memprot_vp${domain}_.debug_abbrev=.debug_abbrev \
--rename-section __memprot_vp${domain}_.debug_macinfo=.debug_macinfo \
--rename-section __memprot_vp${domain}_.debug_loc=.debug_loc \
--rename-section __memprot_vp${domain}_.debug_ranges=.debug_ranges \
--rename-section __memprot_vp${domain}_.debug_info=.debug_info \
--rename-section __memprot_vp${domain}_.debug_line=.debug_line \
--rename-section __memprot_vp${domain}_.debug_str=.debug_str \
--rename-section __memprot_vp${domain}_.debug_frame=.debug_frame \
--rename-section __memprot_vp${domain}_.debug_aranges=.debug_aranges \
--rename-section __memprot_vp${domain}_.debug_pubnames=.debug_pubnames \
--rename-section __memprot_vp${domain}_.debug_pubtypes=.debug_pubtypes \
$temp $out
Ce script peut être utilisé indifféremment sur un module .o ou sur
une librarie statique .a. Hormis l'utilisation de ce script,
certaines options de compilation doivent être ajoutées :
MEMPROT_CFLAGS = -fno-common -mlong-calls
L'option -fno-common est nécessaire pour séparer l'allocation de
variables entre modules, l'option -mlong-calls est requise en raison
des sauts d'adresses supérieurs à 24 Mo liés à l'utilisation de la mémoire
virtuelle.
Exemple
Par exemple, supposons que l'on veuille associer
le code et les variables de l'objet appli.o au domaine 4. Avec les
variables de compilation classiques d'eCos, cela donne :
- Compilation de l'objet appli.o:
$(XCC) -c
$(CFLAGS) $(ECOS_GLOBAL_CFLAGS) $(MEMPROT_CFLAGS) appli.c
- Association de l'objet au domaine 04:
put_in_domain 04 appli.o
- L'édition des liens se fait de manière identique, à ceci près que le script
utilisé est différent: l'option -Ttarget.ld est remplacée par
-Ttarget_mp.ld
Si l'on supprime l'étape 2, le code sera exécuté par défaut en domaine 0,
c'est-à-dire en mode privilégié.
Utilisation dans un Makefile
Il y a plusieurs façons de procéder pour intégrer ces modifications dans un
Makefile; en voici un exemple qui permet de compiler
un programme avec ou sans protection mémoire (en changeant la valeur de la
variable USE_MEMPROT).
Notes importantes
- L'utilisation de la protection mémoire entraîne quelques modifications dans
la sémantique de certains appels système :
- cyg_thread_create(), cyg_thread_resume(), cyg_thread_kill(),
cyg_thread_delete()
La création d'une thread depuis un contexte utilisateur est possible seulement
si le point d'entrée de cette thread se situe dans le domaine de l'appelant.
Les appels cyg_thread_resume(), cyg_thread_kill() et cyg_thread_delete() depuis
un contexte utilisateur sont permis seulement si la thread passée en argument
se situe dans le domaine de l'appelant.
Ainsi une thread d'un domaine i > 0 ne peut agir sur une thread d'un
domaine j > 0 si i ≠ j.
-
Les
paramètres cyg_thread *thread et void *stack_base passés
à la fonction cyg_thread_create() sont ignorés, l'allocation de la
thread et de sa pile étant à la charge du noyau.
-
La fonction pthread_create() du package POSIX ne retient du
paramètre pthread_attr_t *attr que le champ stacksize, les
autres attributs sont ignorés.
-
D'une manière générale, les appels système écrivant à une adresse fournie
par l'appelant ne fonctionnent que si cette adresse se situe dans le domaine
de l'appelant (et si celui-ci en possède les droits d'écriture).
- Les déclarations multiples de variables globales sans utilisation du
mot-clef extern sont interdites; ainsi, si vous déclarez par exemple
la variable globale a dans deux fichiers séparés :
- Dans fichier1.c : int a;
- Dans fichier2.c : int a;
vous obtiendrez une erreur du compilateur. La manière correcte de procéder
est la suivante :
- Dans fichier1.c : int a;
- Dans fichier2.c : extern int a;
Utilisation d'un debugger
L'utilisation de la MMU ainsi que la surcharge des appels système introduisent
quelques subtilités et pièges dans l'utilisation d'un debugger :
- Les points d'arrêt sur du code en domaine > 0 ne peuvent être
initialisés au démarrage d'eCos
(car le code n'est pas encore mappé en mémoire), et l'on obtient de la part de
gdb une erreur du type:
Warning:
Cannot insert breakpoint 1.
Error accessing memory address 0x40000034: Erreur inconnue 4294967295.
The same program may be running in another process.
Une solution consiste à placer un premier point d'arrêt sur la fonction
cyg_user_start(), puis lancer l'exécution jusqu'à ce point. Il est
alors possible de placer les autres points d'arrêt (car la MMU a été programmée
à ce stade).
- Pour placer un point d'arrêt à l'entrée d'un appel système, il est
nécessaire de préfixer le nom de l'appel système par la chaîne
__internal_. Dans le cas contraire, le point d'arrêt serait placé
sur une instruction swi qui permet le passage en mode privilégié,
mais ne permet pas de débugger pas à pas. Donc pour s'arrêter par exemple
à l'entrée de l'appel cyg_thread_resume, il faut placer un point
d'arrêt sur la fonction __internal_cyg_thread_resume.
Une exception à cette règle : certains appels système ne sont pas
surchargés, auxquels cas la fonction __internal_xxx n'existe pas et
l'on peut utiliser le nom habituel de l'appel système.
-
Juste avant d'être tuée, une thread fautive passe par la fonction
cyg_memprot_breakme. Cette fonction existe dans le seul but de
permettre d'y placer un point d'arrêt. Ce point d'arrêt peut être
systématiquement posé lors du debug, car la fonction n'est appelée que
lorsqu'une violation de privilèges est commise.
Messages d'erreur
Lorsqu'une violation de privilège a lieu, le système affiche un message
d'erreur avant d'arrêter la thread fautive. L'exemple ci-dessous s'est
produit lors d'une tentative d'accès à une adresse invalide:
memprot: data abort @ pc = 0x4510042c (page translation fault)
memprot: [trying to access 0x00074528 in domain 0]
memprot: killing thread "Bad Thread" (handle = 0x49165000, domain = 2)
A noter que la thread incriminée est simplement arrêtée, elle n'est pas
détruite. Pour libérer les ressources qu'elle occupe (pile), il faut appeler
cyg_thread_delete() depuis une autre thread.
L'implémentation se décompose en 2 parties:
- Un ensemble de patches du noyau permettant la transition entre les modes
USER et SYSTEM, ainsi que la gestion des exceptions et des droits d'accès.
- Un package memprot implémentant la protection mémoire; notamment
la programmation dynamique de la MMU, la gestion transparente des domaines de
threads, la gestion des exceptions,
l'édition de liens en mémoire virtuelle alignée, l'allocation de mémoire
privée par domaine, l'allocation de RAM physique, etc.
5. Détails de l'implémentation
Patches
system_mode_mp.patch
Ce patch permet de faire tourner eCos en mode ARM SYSTEM à la place du mode
SVC par défaut. L'intérêt du mode SYSTEM est qu'il partage la totalité de ses
registres avec le mode USER, simplifiant ainsi les allers-retours entre les
modes utilisateur et privilégié. Le patch modifie également le traitement
des exceptions et le changement de contexte:
-
packages/hal/arm/arch/current/cdl/hal_arm.cdl
Ajout d'une option CYGOPT_HAL_ARM_USE_SYSTEM_MODE.
- packages/hal/arm/arch/current/include/hal_arch.h
Ajout des modes USER, ABORT et SYSTEM. Modification de CPSR_THREAD_INITIAL
et CPSR_INITIAL.
- packages/hal/arm/arch/current/src/context.S
Modification des routines de changement de contexte
hal_thread_switch_context() et
hal_thread_load_context().
- packages/hal/arm/arch/current/src/hal_mk_defs.c
Ajout des modes USER, ABORT et SYSTEM. Ajout du champ
armreg_domain (alias de armreg_vector).
- packages/hal/arm/arch/v2_0/src/vectors.S
Beaucoup de modifications assez subtiles.
Initialisation de la pile en mode ABORT.
Passage en mode SYSTEM après le reset.
Exécution des gestionnaire d'exceptions et d'interruptions en mode SYSTEM.
Restauration de sp en cas d'exception.
Gestion du DACR lors d'un changement de contexte.
Etc, etc,...
- packages/kernel/current/include/sched.hxx
Export pour l'assembleur du symbole cyg_current_thread_table.
Packages