Suivez-nous sur X

|
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
ALL
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z
|
|
A propos d'Obligement
|
|
David Brunet
|
|
|
|
Programmation : C - Écrire son propre code de démarrage
(Article écrit par Grzegorz Kraszewski et extrait de MorphOS Library - décembre 2012)
|
|
Note : traduction par David Brunet.
On trouve dans tous les manuels de programmation en C que l'exécution d'un programme commence à partir de
la fonction "main()". En fait, nous avons cette impression lorsque nous écrivons un programme. Il n'y a aucune
preuve qui puisse réfuter cela. Mais en fait, ce n'est pas vrai. Il y a au moins quelques octets, et parfois même des
dizaines de kilo-octets de code entre le début de l'exécution du programme et la première ligne de "main()".
Et la plus grande partie de ce code n'est pas vraiment nécessaire dans la plupart des cas.
Que fait ce code ? Disons pour commencer qu'il est parfaitement possible d'avoir un programme sans aucun
code de démarrage. Le système peut passer à "main()" tout de suite. Malheureusement, un tel programme ne
fonctionnerait que dans une fenêtre en ligne de commande. Il se planterait s'il était lancé depuis
Ambient. Ceci parce que Ambient envoie un message spécial à un port de message d'un processus fraîchement
créé. Ce message a deux objectifs. Premièrement, il contient les paramètres de lancement d'Ambient, à savoir
le descripteur de l'icône du programme et, éventuellement, les descripteurs d'autres icônes, qui ont été
cliquées avec Shift ou déposées sur un panneau. Deuxièmement, une réponse à ce message donne le signal à
Ambient que le programme a terminé son exécution. L'envoi d'une réponse est obligatoire. C'est l'ensemble
minimal de choses à faire par le code de démarrage. En pratique, il devrait également ouvrir les bibliothèques
partagées requises. Lorsque l'on veut utiliser la bibliothèque standard C, que ce soit avec libnix ou ixemul.library,
le code de démarrage crée également un "environnement standard" pour la bibliothèque standard C et les fonctions
POSIX. Pour cette raison, le code de démarrage lié lors de l'utilisation d'une de ces bibliothèques est assez
complexe, donc également long.
Les raisons d'écrire son propre code de démarrage
Le principal avantage d'écrire son propre code de démarrage est sa brièveté. La réduction du temps
de démarrage du programme est négligeable. Un code de démarrage très court est bon pour les programmes
très courts, par exemple de quelques ko comme les commandes du Shell. Dans ce cas, le code de démarrage standard
peut être facilement plus long que le code du programme lui-même. On peut aussi utiliser son propre code
de démarrage, juste pour la satisfaction de rendre le programme plus court de ces quelques kilo-octets.
A noter qu'un code de démarrage personnalisé ne peut pas être utilisé lorsque le programme utilise ixemul.library. Lorsque
le programme est lié avec libnix, la possibilité d'utiliser son propre code de démarrage dépend des fonctions
de la bibliothèque C standard utilisées. La plupart d'entre elles ne nécessitent aucune préparation et
fonctionneront avec n'importe quel code de démarrage. Certaines fonctions plus complexes nécessitent cependant
l'exécution de constructeurs au démarrage. Si nous utilisons de telles fonctions, nous obtiendrons des erreurs
de liens pour des symboles non résolus. Dans un tel cas, il y a un choix simple - on doit soit remplacer ces
fonctions par autre chose, soit utiliser le code de démarrage standard. Notre propre démarrage est alors utile,
surtout lorsque la bibliothèque C standard n'est pas du tout utilisée (en faveur de l'API native de MorphOS),
ou si vous utilisez seulement des fonctions simples de celle-ci.
Si nous sommes toujours déterminés à utiliser notre propre code de démarrage, il est temps d'en informer
le compilateur. L'argument "-nostartfiles" permet de sauter le code de démarrage standard. Ensuite,
lorsque nous essayons d'utiliser notre code de démarrage avec libnix, nous utilisons "-nostartfiles" avec
"-noixemul". Les programmeurs qui veulent utiliser l'API MorphOS pure (sans la bibliothèque C), doivent
utiliser l'option "-nostdlib", qui implique aussi "-nostartfiles".
Écrivons notre code de démarrage
Avant de commencer à écrire le code, notez qu'à part les choses exécutées avant l'appel de la fonction
"main()", certains codes doivent également être appelés après son retour. Nous avons donc aussi du "code de
nettoyage". Comme ce code est généralement placé dans la même fonction (celle qui appelle "main()"), les deux
parties sont communément appelées "code de démarrage".
Comme mentionné précédemment, l'exécution du programme ne commence pas vraiment à partir de la fonction
"main()". Où commence-t-elle alors ? Lorsqu'un exécutable ELF est chargé depuis le disque, une section nommée
".text" est trouvée et le système d'exploitation saute au début de son contenu. Lorsqu'un programme est
écrit en C, cela signifie le début de la première fonction du code, car en C il n'y a aucun moyen d'écrire
du code en dehors d'une fonction. Il faut noter que le compilateur C peut réordonner les fonctions dans un même
fichier objet. Le compilateur GCC 2.95.3 ne le fait jamais, mais l'optimiseur agressif de GCC 4 peut changer
l'ordre des fonctions. Heureusement, cela ne se fait qu'à l'intérieur d'un seul fichier source. Pour s'assurer
que notre fonction de démarrage sera la première, elle doit être placée dans un fichier séparé. Ensuite, le
fichier objet résultant doit être lié comme en premier, car l'ordre de liaison est toujours préservé. Après
cette note importante, il est temps de passer au code :
#include <proto/exec.h>
#include <proto/dos.h>
#include <dos/dos.h>
#include <workbench/startup.h>
|
Tout commence par l'inclusion des fichiers d'en-tête nécessaires. Nous aurons besoin de deux bibliothèques
système de base : exec.library et dos.library. Cela explique pourquoi le code de démarrage standard, que
ce soit libnix ou ixemul.library, ouvre ces deux bibliothèques - il en a simplement besoin pour lui-même.
struct Library *SysBase;
struct Library *DOSBase;
|
Comme notre code va utiliser ces deux bibliothèques, nous devons définir leurs bases.
extern ULONG Main(struct WBStartup *wbmessage);
|
Ceci est une déclaration de la fonction principale de notre programme. Comme le fichier objet contenant le
code de démarrage ne doit contenir qu'une seule fonction (celle d'entrée), le reste du code doit être
déplacé dans d'autres fichiers objets, pour les raisons expliquées ci-dessus. C'est pourquoi la fonction
principale doit être déclarée ici, car nous l'appelons depuis le code de démarrage. Alternativement, sa
déclaration peut être placée dans un fichier d'en-tête et incluse ici. Le nom "Main()" est arbitraire, il peut
être n'importe quoi. Je l'ai juste appelé comme ça par habitude, en mettant la première lettre en majuscule pour éviter
une confusion possible avec la bibliothèque standard. L'argument de "Main()" est le message de démarrage
(mentionné ci-dessus) envoyé par Ambient. Si nous ne prévoyons pas de l'utiliser dans "Main()", nous pouvons
simplement le déclarer de cette façon :
La prochaine chose importante est de définir un mystérieux symbole global "__abox__".
Bien qu'il ne soit pas nécessaire dans le code, ce symbole est utilisé par le chargeur d'exécutable système
pour différencier les exécutables ELF MorphOS des autres binaires ELF PowerPC possibles. S'il n'y a pas de
"__abox__" défini, l'exécutable sera reconnu comme natif PowerUP et exécuté par la ppc.library, avec des résultats
imprévisibles.
ULONG Start(void)
{
struct Process *myproc = 0;
struct Message *wbmessage = 0;
BOOL have_shell = FALSE;
ULONG return_code = RETURN_OK;
|
"Start()" est le point d'entrée du code. Encore une fois, le nom de cette fonction n'est pas important, il
peut être n'importe quoi. Elle doit simplement être la première fonction dans l'exécutable lié. Quelques
variables locales sont déclarées ici, qui seront nécessaires plus tard. "myproc" contiendra un pointeur
vers notre processus, "wbmessage" contiendra le pointeur du message de démarrage d'Ambient. La variable
"have_shell" sera utilisée pour détecter si le programme a été lancé depuis la console Shell ou depuis Ambient.
Enfin, "return_code" est juste le code de retour du programme, il sera renvoyé au système. La valeur de retour
est généralement 0 lorsque le programme s'est exécuté avec succès et la constante "RETURN_OK" est juste 0.
SysBase = *(struct Library**)4L;
|
C'est le moment de l'initialisation de SysBase, la base de exec.library. La bibliothèque est toujours ouverte.
Pour des raisons historiques et de rétrocompatibilité, le pointeur de base est toujours placé par le système à
l'adresse $00000004, donc nous le prenons juste à partir de là. Une fois exec.library disponible, notre code
peut vérifier s'il a été lancé depuis le Shell ou depuis Ambient.
myproc = (struct Process*)FindTask(0);
if (myproc->pr_CLI) have_shell = TRUE;
|
Ces informations sont tirées de la structure Process qui n'est qu'un descripteur de processus système.
L'appel "FindTask()" de la bibliothèque exec.library renvoie le propre descripteur de la tâche appelante si 0
est passé comme argument. Dans le cas d'un lancement depuis Ambient, recevoir son message est obligatoire :
if (!have_shell)
{
WaitPort(&myproc->pr_MsgPort);
wbmessage = GetMsg(&myproc->pr_MsgPort);
}
|
Le message de démarrage est envoyé au port système du processus, c'est donc là que nous le recevons. Le
message peut ensuite être transmis à notre fonction "Main()", si nous prévoyons d'en faire un usage quelconque,
comme la gestion d'arguments d'icône supplémentaires.
if (DOSBase = OpenLibrary((STRPTR) "dos.library", 0))
{
|
L'étape suivante est l'ouverture de la dos.library, cette bibliothèque est ouverte d'une manière assez
standard. En fait, ce code de démarrage minimal n'en a pas besoin. Il y a deux raisons de l'ouvrir quand
même. Tout d'abord, il est difficile d'imaginer un programme qui n'ait pas besoin de la bibliothèque dos.library -
même le programme "Hello world!" l'utilise. Deuxièmement, tous les codes de démarrage standard l'ouvrent,
donc généralement le code principal le tient pour acquis. Alors mon code de démarrage se comporte de manière
conventionnelle et ouvre également la dos.library.
return_code = Main((struct WBStartup*)wbmessage);
|
Oui, après ces quelques lignes, nous sommes prêts à appeler le code principal. Comme indiqué plus haut,
la transmission du message de démarrage d'Ambient est facultative. Par contre, recevoir le résultat et le
renvoyer au système plus tard est obligatoire.
CloseLibrary(DOSBase);
}
else return_code = RETURN_FAIL;
|
A partir de là, le code de démarrage devient un code de nettoyage. Notez également qu'une gestion correcte
des erreurs doit être effectuée. La dos.library est fermée, mais si son ouverture a échoué auparavant, le
résultat de l'exécution est changé en "RETURN_FAIL". C'est l'échec le plus critique et signifie l'incapacité
totale d'exécuter un programme. En pratique, MorphOS ne peut pas démarrer si la dos.library n'est pas présente dans le
système. Mais "OpenLibrary()" peut échouer pour d'autres raisons, par exemple un simple manque de mémoire
libre. Alors le code de démarrage doit gérer cela d'une manière raisonnable.
if (wbmessage)
{
Forbid();
ReplyMsg(wbmessage);
}
|
Ce bout de code gère le message de démarrage d'Ambient. Même si nous n'en faisons pas usage, il doit être traité
à la sortie. Mais que fait "Forbid()" ici ? Cette fonction arrête le multitâche du système, c'est-à-dire
qu'elle empêche le planificateur de processus du système de passer notre processus. Habituellement, cela ne
peut être fait que pour une très courte période et suivi d'un "Permit()" correspondant. À première
vue, ce code n'a aucun sens : un processus met fin au changement de processus et... sort. Il faut cependant savoir
une chose importante : la commutation de processus est automatiquement réactivée lorsque le processus qui a
appelé "Forbid()" se termine. Voici donc ce qui se passe :
- Notre tâche appelle "Forbid()", ainsi aucun autre processus ne peut l'interrompre.
- Elle répond au message de démarrage d'Ambient. Comme le multitâche est arrêté, Ambient n'est pas encore
en mesure de le recevoir. Le message attend simplement sur son port de message.
- Notre tâche se termine. Puis le système rétablit le multitâche.
- Ambient obtient du temps processeur et reçoit le message. Notez qu'à ce stade, il est absolument certain
que notre tâche n'existe plus. La possibilité d'une situation de compétition est éliminée. Sans "Forbid()",
il serait possible que notre processus soit supprimé du système alors qu'il est toujours en cours d'exécution.
Bien sûr, la période d'arrêt du multitâche est extrêmement courte, car notre code de nettoyage se termine
immédiatement après avoir répondu à Ambient :
$VER:, la chaîne d'identification de programme
Ce sujet n'est pas strictement lié au code de démarrage, mais la chaîne de version y est généralement
intégrée, j'ai donc décidé d'écrire quelques mots à son sujet.
La chaîne de version est un texte court dans un format défini. Cette chaîne contient le nom du programme,
le numéro de version et de révision, la date de compilation et éventuellement des informations sur
l'auteur ou le droit d'auteur. La chaîne de version est décodée par de nombreuses applications, notamment
Ambient, la commande "version", le programme d'installation, etc. Le texte commence par "$VER:", ce qui
permet de le trouver facilement dans l'exécutable du programme. Comme les outils de version recherchent
la chaîne de version à partir du début du fichier exécutable, il est préférable que la chaîne de version
soit placée aussi près que possible du début du fichier. Si la chaîne de version est déclarée comme une
simple constante de chaîne, elle est malheureusement placée dans l'une des sections de données ELF.
Ces sections sont placées après la section de code par l'éditeur de liens. Cependant, nous pouvons forcer
la chaîne de version à être placée dans la section de code :
__attribute__ ((section(".text"))) UBYTE VString[] =
"$VER: program 1.0 (21.6.2011) © 2011 morphos.pl\r\n";
|
En utilisant une extension "__attribute__" spécifique à GCC, nous pouvons pousser la chaîne dans
la section ELF nommée ".text", qui est la section de code. L'objet de code de démarrage étant lié
comme le premier objet, la chaîne de version apparaîtra au début de l'exécutable, juste après le code
de la fonction "Start()". Pourquoi après ? C'est simple, si nous la plaçons avant le vrai code, le
système d'exploitation sautera "dans" la chaîne, en essayant de l'exécuter, et ensuite bien sûr il
se plantera.
Un exemple complet
Voici un exemple complet
de "Hello world!" avec un code de démarrage personnalisé qui montre les idées décrites ci-dessus en
fonctionnement. Il n'utilise que l'API de MorphOS, et est donc compilé avec l'option
"-nostdlib". La taille de l'exécutable est de 1592 octets. Pour comparaison, le démarrage de libnix
et "printf()" donne 30 964 octets, quand on remplace "printf()" par le "Printf()" de MorphOS à partir
de la dos.library, il reste 13 500 octets.
Comme le projet consiste en deux fichiers *.c, un simple makefile y est ajouté. L'exemple peut être
compilé juste en entrant "make" dans une console.
#include <proto/exec.h>
#include <proto/dos.h>
#include <dos/dos.h>
#include <workbench/startup.h>
struct Library *SysBase = 0;
struct Library *DOSBase = 0;
extern ULONG Main(void);
ULONG __abox__ = 1;
/*----------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------*/
ULONG Start(void)
{
struct Process *myproc = 0;
struct Message *wbmessage = 0;
BOOL have_shell = FALSE;
ULONG return_code = RETURN_OK;
SysBase = *(struct Library**)4L;
myproc = (struct Process*)FindTask(0);
if (myproc->pr_CLI) have_shell = TRUE;
if (!have_shell)
{
WaitPort(&myproc->pr_MsgPort);
wbmessage = GetMsg(&myproc->pr_MsgPort);
}
if ((DOSBase = OpenLibrary((STRPTR)"dos.library", 0)))
{
return_code = Main();
CloseLibrary(DOSBase);
}
else return_code = RETURN_FAIL;
if (wbmessage)
{
Forbid();
ReplyMsg(wbmessage);
}
return return_code;
}
__attribute__ ((section(".text"))) UBYTE VString[] = "$VER: helloworld 1.0 (22.6.2011)\r\n";
|
|