Obligement - L'Amiga au maximum

Dimanche 02 octobre 2022 - 21:16  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

Actualité (récente)
Actualité (archive)
Comparatifs
Dossiers
Entrevues
Matériel (tests)
Matériel (bidouilles)
Points de vue
En pratique
Programmation
Reportages
Quizz
Tests de jeux
Tests de logiciels
Tests de compilations
Trucs et astuces
Articles divers

Articles in english


Twitter

Suivez-nous sur Twitter




Liste des jeux Amiga

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


Trucs et astuces

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


Glossaire

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


Galeries

Menu des galeries

BD d'Amiga Spécial
Caricatures Dudai
Caricatures Jet d'ail
Diagrammes de Jay Miner
Images insolites
Fin de jeux (de A à E)
Fin de Jeux (de F à O)
Fin de jeux (de P à Z)
Galerie de Mike Dafunk
Logos d'Obligement
Pubs pour matériels
Systèmes d'exploitation
Trombinoscope Alchimie 7
Vidéos


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

Sites de téléchargements
Associations
Pages Personnelles
Matériel
Réparateurs
Revendeurs
Presse et médias
Logiciels
Jeux
Scène démo
Divers


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


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 le 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 :

extern ULONG Main(void);

La prochaine chose importante est de définir un mystérieux symbole global "__abox__".

ULONG __abox__ = 1;

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 :

  return return_code;
}

$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";


[Retour en haut] / [Retour aux articles]