Intuitivement, il semblerait qu'un compilateur pour le langage Foo
ne puisse pas lui-même être écrit dans Foo. Plus précisément, le compilateur first pour la langue Foo
ne peut pas être écrit dans Foo, mais tout compilateur ultérieur pourrait être écrit pour Foo
.
Mais est-ce vraiment vrai? J'ai un souvenir très vague de la lecture d'une langue dont le premier compilateur a été écrit en "lui-même". Est-ce possible, et si oui comment?
C'est ce qu'on appelle le "bootstrapping". Vous devez d'abord construire un compilateur (ou interprète) pour votre langue dans une autre langue (généralement Java ou C). Une fois cela fait, vous pouvez écrire une nouvelle version du compilateur dans la langue Foo . Vous utilisez le premier compilateur bootstrap pour compiler le compilateur, puis utilisez ce compilateur compilé pour compiler tout le reste (y compris les futures versions de lui-même).
La plupart des langages sont en effet créés de cette manière, en partie parce que les concepteurs de langage aiment utiliser le langage qu'ils créent, et aussi parce qu'un compilateur non trivial sert souvent de référence utile pour savoir à quel point le langage peut être "complet".
Un exemple de ceci serait Scala. Son premier compilateur a été créé dans Pizza, un langage expérimental de Martin Odersky. Depuis la version 2.0, le compilateur a été complètement réécrit en Scala. À partir de ce moment, l'ancien compilateur Pizza pourrait être complètement éliminé, car le nouveau compilateur Scala pourrait être utilisé pour se compiler pour les futures itérations.
Je me souviens avoir écouté un podcast Radio Engineering Software dans lequel Dick Gabriel a parlé d'amorcer l'interpréteur LISP original en écrivant une version simple en LISP sur papier et l'assemblage manuel en code machine. À partir de ce moment, les autres fonctionnalités LISP ont été à la fois écrites et interprétées avec LISP.
Ajout d'une curiosité aux réponses précédentes.
Voici une citation du manuel Linux From Scratch , à l'étape où l'on commence à construire le compilateur GCC à partir de sa source. (Linux From Scratch est un moyen d'installer Linux qui est radicalement différent de l'installation d'une distribution, en ce sens que vous devez vraiment compiler chaque binaire unique de la cible système.)
make bootstrap
La cible 'bootstrap' ne compile pas seulement GCC, mais la compile plusieurs fois. Il utilise les programmes compilés au premier tour pour se compiler une deuxième fois, puis à nouveau une troisième fois. Il compare ensuite ces deuxième et troisième compilations pour s'assurer qu'il peut se reproduire parfaitement. Cela implique également qu'il a été compilé correctement.
Cette utilisation de la cible "bootstrap" est motivée par le fait que le compilateur utilisé pour construire la chaîne d'outils du système cible peut ne pas avoir la même version du compilateur cible. En procédant de cette façon, on est sûr d'obtenir, dans le système cible, un compilateur qui peut se compiler lui-même.
Lorsque vous écrivez votre premier compilateur pour C, vous l'écrivez dans un autre langage. Maintenant, vous avez un compilateur pour C dans, disons, assembleur. Finalement, vous arriverez à l'endroit où vous devez analyser les chaînes, en particulier les séquences d'échappement. Vous écrirez du code pour convertir \n
au caractère avec le code décimal 10 (et \r
à 13, etc.).
Une fois ce compilateur prêt, vous commencerez à le réimplémenter en C. Ce processus est appelé " bootstrapping ".
Le code d'analyse de chaîne deviendra:
...
if (c == 92) { // backslash
c = getc();
if (c == 110) { // n
return 10;
} else if (c == 92) { // another backslash
return 92;
} else {
...
}
}
...
Lorsque cela se compile, vous avez un binaire qui comprend '\ n'. Cela signifie que vous pouvez modifier le code source:
...
if (c == '\\') {
c = getc();
if (c == 'n') {
return '\n';
} else if (c == '\\') {
return '\\';
} else {
...
}
}
...
Alors, où est l'information que '\ n' est le code pour 13? C'est dans le binaire! C'est comme l'ADN: la compilation du code source C avec ce binaire héritera de ces informations. Si le compilateur se compile, il transmettra ces connaissances à sa progéniture. À partir de là, il n'y a aucun moyen de voir à partir de la seule source ce que fera le compilateur.
Si vous voulez cacher un virus dans la source d'un programme, vous pouvez le faire comme ceci: Obtenez la source d'un compilateur, trouvez la fonction qui compile les fonctions et remplacez-la par celle-ci:
void compileFunction(char * name, char * filename, char * code) {
if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) {
code = A;
} else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) {
code = B;
}
... code to compile the function body from the string in "code" ...
}
Les parties intéressantes sont A et B. A est le code source de compileFunction
y compris le virus, probablement crypté d'une certaine manière, il n'est donc pas évident de chercher dans le binaire résultant. Cela garantit que la compilation avec le compilateur lui-même préservera le code d'injection de virus.
B est le même pour la fonction que nous voulons remplacer par notre virus. Par exemple, il pourrait s'agir de la fonction "login" dans le fichier source "login.c" qui provient probablement du noyau Linux. Nous pourrions le remplacer par une version qui acceptera le mot de passe "joshua" pour le compte root en plus du mot de passe normal.
Si vous compilez cela et le diffusez sous forme binaire, il n'y aura aucun moyen de trouver le virus en regardant la source.
La source originale de l'idée: http://cm.bell-labs.com/who/ken/trust.html
Vous ne pouvez pas écrire un compilateur en soi car vous n'avez rien pour compiler votre code source de départ. Il existe deux approches pour résoudre ce problème.
Le moins favorisé est le suivant. Vous écrivez un compilateur minimal dans l'assembleur (beurk) pour un ensemble minimal du langage, puis utilisez ce compilateur pour implémenter des fonctionnalités supplémentaires du langage. Construisez votre chemin jusqu'à ce que vous ayez un compilateur avec toutes les fonctionnalités du langage pour lui-même. Un processus douloureux qui ne se fait généralement que lorsque vous n'avez pas d'autre choix.
L'approche préférée consiste à utiliser un compilateur croisé. Vous modifiez l'extrémité arrière d'un compilateur existant sur une autre machine pour créer une sortie qui s'exécute sur la machine cible. Ensuite, vous avez un compilateur complet Nice et vous travaillez sur la machine cible. Le plus populaire pour cela est le langage C, car il existe de nombreux compilateurs existants qui ont des backends enfichables qui peuvent être échangés.
Un fait peu connu est que le compilateur C GNU C++ a une implémentation qui utilise uniquement le sous-ensemble C. La raison étant qu'il est généralement facile de trouver un compilateur C pour une nouvelle machine cible qui vous permet de puis compilez le GNU compilateur C++ à partir de celui-ci. Vous avez maintenant démarré vous-même pour avoir un compilateur C++ sur la machine cible.
Généralement, vous devez avoir une coupe de travail (si primitive) du compilateur fonctionnant en premier - alors vous pouvez commencer à penser à le rendre auto-hébergé. Ceci est en fait considéré comme un jalon important dans certains langages.
D'après ce que je me souviens de "mono", il est probable qu'ils devront ajouter quelques éléments à la réflexion pour le faire fonctionner: l'équipe mono continue de souligner que certaines choses ne sont tout simplement pas possibles avec Reflection.Emit
; bien sûr, l'équipe MS pourrait leur prouver le contraire.
Cela a quelques avantages réels : c'est un assez bon test unitaire, pour commencer! Et vous n'avez qu'un seul langage à vous soucier (c'est-à-dire qu'il est possible qu'un expert C # ne connaisse pas beaucoup C++; mais maintenant, vous pouvez réparer le compilateur C #). Mais je me demande s'il n'y a pas une grande fierté professionnelle au travail ici: ils veulent simplement qu'il soit auto-hébergé.
Pas tout à fait un compilateur, mais j'ai récemment travaillé sur un système qui s'auto-héberge; le générateur de code est utilisé pour générer le générateur de code ... donc si le schéma change, je le lance simplement sur lui-même: nouvelle version. S'il y a un bug, je reviens simplement à une version antérieure et réessaye. Très pratique et très facile à entretenir.
Je viens de regarder cette vidéo d'Anders au PDC, et (environ une heure) il donne des raisons beaucoup plus valables - tout sur le compilateur en tant que service. Juste pour info.
GNAT, le GNU compilateur Ada, nécessite un compilateur Ada pour être entièrement construit. Cela peut être pénible lors du portage sur une plate-forme où il n'y a pas de binaire GNAT facilement disponible.
Le compilateur C # du projet Mono est "auto-hébergé" depuis longtemps, ce qui signifie qu'il a été écrit en C # lui-même.
Ce que je sais, c'est que le compilateur a été démarré en tant que code C pur, mais une fois que les fonctionnalités "de base" d'ECMA ont été implémentées, ils ont commencé à réécrire le compilateur en C #.
Je ne suis pas conscient des avantages d'écrire le compilateur dans le même langage, mais je suis sûr que cela a à voir avec les fonctionnalités que le langage lui-même peut offrir (C, par exemple, ne prend pas en charge la programmation orientée objet) .
Vous pouvez trouver plus d'informations ici .
En fait, la plupart des compilateurs sont écrits dans la langue qu'ils compilent, pour les raisons indiquées ci-dessus.
Le premier bootstrap compilateur est généralement écrit en C, C++ ou Assembly.
Vous pouvez peut-être écrire un BNF décrivant BNF.