Une application miroir en C

Table des matières

Un miroir ? Euh on parle de quoi là, d'un dépôt de fichiers ? Non, pas du tout ! Je vais vous présenter un programme dont la source est identique à la sortie console. Uh ?

Le problème

Si nous avons une source C comme suit :

#include <stdio.h>
#include <stdlib.h>
int main()
{
	[ .... ] /* Tout plein de code ici */

	return 0;
}

On veut que la console affiche exactement la même chose, quelque soit le code entre [ .... ] :

#include <stdio.h>
#include <stdlib.h>

int main()
{
	[ .... ] /* Tout plein de code ici */
	return 0;

}

Cela paraît chose aisée, mais ça demande pas mal de réflexion (j'ai mis beaucoup de temps à trouver la solution !). Si vous voulez chercher par vous même, ne lisez pas la suite.

La réflexion

La base de la solution : la fonction printf(). Ca, tout le monde s'en serait douté. Mais la solution aurait été beaucoup plus compliqué avec un std::cout de c++ (voire impossible, je n'y ai pas vraiment réfléchi). Il va falloir tirer parti du couple de paramètres format / chaine de paramètres variables (les ... quand on regarde la syntaxe d'appel de la fonction !). Si vous voulez en savoir plus à ce sujet, je vous conseille de regarder ceci : http://msdn.microsoft.com/en-us/library/kb57fad8(VS.71).aspx. Ce n'est PAS une librairie exclusive MS ou quelque chose du genre, comme vous le verrez dans l'exemple, MS donne également un exemple sous UNIX.

Voici donc l'idée de départ : utiliser une variable à la fois comme format et comme paramètre de printf :

#include <stdio.h>
#include <stdlib.h>

int main()
{
	const char* var = "#include <stdio.h>

			#include <stdlib.h>
			int main()
			{
				const char* var = "%s";
				printf(var, var);
				return 0;
			}";
	printf(var, var);

	return 0;
}

Bien sûr ca ne fonctionne pas, on ne peut pas définir une chaîne sur plusieurs lignes 😉 Et puis les tabulations / nouvelles lignes ne seraient pas interprétées. Deuxième étape donc : mettre la variable sur une ligne

#include <stdio.h>
#include <stdlib.h>

int main()
{
	const char* var = "#include <stdio.h>\n#include <stdlib.h>\nint main()\n{\n\tconst char* var = \"%s\";\n\tprintf(var, var);\n\tsystem(\"pause\");\n\treturn 0;\n}";

	printf(var, var);
	system("pause");
	return 0;

}

Ce code fonctionne. Voici la sortie que vous devriez obtenir :

#include <stdio.h>
#include <stdlib.h>
int main()
{
	const char* var = "#include <stdio.h>

	#include <stdlib.h>
	int main()
	{
		const char* var = "%s";
		printf(var, var);
		system("pause");
		return 0;
	}";
	printf(var, var);
	system("pause");
	return 0;
}Appuyez sur une touche pour continuer...

Le system("pause"); c'est pas nécessaire, un fail de ma part.
Sur le principe, "l'énigme" est résolue, cependant la mise en forme n'est pas la même que dans notre code ! Il se pose donc une autre problématique. Comment faire en sorte que le const char* var affiché en console montre les \n les \t etc ?

La solution

Il va nous falloir créer une sorte de addslashes en c (pour les amateurs de php x) )
Voici donc le code final. Je vous passe les étapes pour en arriver là. Si vous ne comprenez pas tout demandez. Le code est un peu "condensé"/"compacté" pour la mise en ligne dans la variable

#include <stdio.h>
#include <stdlib.h>

#include <string.h>
char* strrep(const char *str) {
	char ret[4096]; int oi=0, ri=0;

	for (;;)
	{
		if(str[oi] == 10) { ret[ri]=92; ret[++ri]='n'; }

		else if (str[oi] == 9) { ret[ri]=92; ret[++ri]='t'; }

			ri++; oi++;
	else if (str[oi] == 34) { ret[ri]=92; ret[++ri]=34; }

		else ret[ri] = str[oi];
		if(oi>=strlen(str)) break;

	}
	ret[ri]=0; return ret;
}
int main()

{
	const char* var = "#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\nchar* strrep(const char *str) {\n\tchar ret[4096]; int oi=0, ri=0;\n\tfor (;;)\n\t{\n\t\tif(str[oi] == 10) { ret[ri]=92; ret[++ri]='n'; }\n\t\telse if (str[oi] == 9) { ret[ri]=92; ret[++ri]='t'; }\n\t\telse if (str[oi] == 34) { ret[ri]=92; ret[++ri]=34; }\n\t\telse ret[ri] = str[oi];\n\t\tri++; oi++;\n\t\tif(oi>=strlen(str)) break;\n\t}\n\tret[ri]=0; return ret;\n}\nint main()\n{\n\tconst char* var = \"%s\";\n\tprintf(var, strrep(var));\n\treturn 0;\n}";

	printf(var, strrep(var));
	return 0;
}

et voici la sortie console :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* strrep(const char *str) {
	char ret[4096]; int oi=0, ri=0;
	for (;;)

	{
		if(str[oi] == 10) { ret[ri]=92; ret[++ri]='n'; }
		else if (str[oi] == 9) { ret[ri]=92; ret[++ri]='t'; }
			ri++; oi++;
	else if (str[oi] == 34) { ret[ri]=92; ret[++ri]=34; }
		else ret[ri] = str[oi];
		if(oi>=strlen(str)) break;
	}
	ret[ri]=0; return ret;
}
int main()

{
	const char* var = "#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\nchar* strrep(const char *str) {\n\tchar ret[4096]; int oi=0, ri=0;\n\tfor (;;)\n\t{\n\t\tif(str[oi] == 10) { ret[ri]=92; ret[++ri]='n'; }\n\t\telse if (str[oi] == 9) { ret[ri]=92; ret[++ri]='t'; }\n\t\telse if (str[oi] == 34) { ret[ri]=92; ret[++ri]=34; }\n\t\telse ret[ri] = str[oi];\n\t\tri++; oi++;\n\t\tif(oi>=strlen(str)) break;\n\t}\n\tret[ri]=0; return ret;\n}\nint main()\n{\n\tconst char* var = \"%s\";\n\tprintf(var, strrep(var));\n\treturn 0;\n}";
	printf(var, strrep(var));
	return 0;
}

Parfaitement identiques ! Désormais vous pouvez ajouter autant de code que vous le désirez, tout ce qu'il reste à faire est de l'ajouter également dans la variable var (et de le formatter bien entendu).

Rapide explication du nom des variables et de ce que fait le code ajouté : la fonction strrep (quand même ^^) :
oi = original index (l'index de la lettre courante dans la variable str passée en paramètre)
ri = return index (l'index de la lettre courante dans la variable retournée)
code ascii 92 = l'antislash
code ascii 10 = retour à la ligne (sous windows ! il se peut qu'il s'agisse de 13 sous linux et des deux sous mac, si mes souvenirs sont bons)
code ascii 9 = antislash t = tabulation
code ascii 34 = guillemets doubles

La boucle passe chacun des caractères du buffer d'entrée. S'il s'agit d'un caractère spécial (que nous avons écrit \t, \n ou \"), il est décomposé en 2 caractères ascii pour être affiché et non interprété.
Le ret[ri]=0 de la fin ajoute le caractère de fin de chaine pour le printf

Voilà, faites passer l'énigme, ne divulguez pas (directement) la solution, et réfléchissez un peu quand même =)

ÉDIT du 15/07/2012 à 21h30 par GuiGui : Si vous voulez en savoir plus et/ou voir d'autres exemples, je vous conseille ce billet : Programmes auto-reproducteurs (quines) chez Rom1v

True Warrior Tools

Rah, mon premier article. Il fallait un titre frappant 🙂

Que sont donc les True Warrior Tools ? En français, les outils du véritable guerrier. Pour être plus clair, je vais détailler ici les outils nécessaires à tout "véritable" développeur. Par véritable, je sous entends programmation (très) bas niveau (c natif, asm natif), et par "natif", j'entends : sans utiliser de librairies existantes, et sans utiliser d'API système, même s'il se peut que l'on ait recours aux fonctionnalités offertes par le bios

Vous l'aurez compris, nous allons faire un tour des outils existants pour parler directement à votre processeur sans passer par un OS. Plus précisément, les outils disponibles sous Windows. Je ne parlerai pas (ou peu) des outils utilisés sous Linux, puisqu'ils sont sûrement déjà installés et prêt à fonctionner.

Tout d'abord, il vous faudra un bon éditeur de texte (ça revient à chaque fois sur le tapis, mais on a toujours pas trouvé mieux). Personnellement, j'aime beaucoup la coloration syntaxique de Notepad++ (Npp) pour l'asm et le batch (oui oui nous allons faire du batch, aussi étrange que cela puisse paraître), mais pas pour le C. Si vous souhaitez le télécharger, http://sourceforge.net/projects/notepad-plus/files/

Ensuite, je vous conseille d'avoir une invite de commande intégrée dans votre explorateur, de sorte qu'un simple clic droit dans votre arborescence des dossiers ouvre une invite de commande déjà positionnée sur le dossier (ca vous évitera des cd dans tous les sens etc). Il va falloir tremper les mains dans le cambouis. Tout d'abord, Win+R / regedit. Rendez vous dans HKEY_CLASSES_ROOT (vache la liste !). Amateurs de sensations fortes, jetez un oeil au passage à la clef CLSID. Rendez vous dans la clef "Folder/Shell". Créez une clef nommée comme vous voulez (le nom ne sert pas). Puis modifiez la valeur de la REG_SZ par défaut. J'ai simplement mis "Invite de commande", mais mettez ce que vous voulez, c'est ce qui apparaîtra dans le menu contextuel. Puis créez une nouvelle clef "command" (pas d'erreur de frappe ici, sinon c'est le fail !) . Modifiez la REG_SZ par défaut de la nouvelle clef et donnez lui cette valeur :

C:\Windows\System32\cmd.exe /k cd "%1%"

. Voilà, pas besoin de reboot ni rien, c'est appliqué.

On est loin d'avoir fini la liste 😉 Alors on continue avec une machine virtuelle / un émulateur. J'utilise Vmware Workstation 7 personnellement. Mais quitte à se lancer, autant partir sur du gratuit avec Virtual Box disponible ici : http://www.virtualbox.org/wiki/Downloads . On entend beaucoup parler de Bochs. Un peu moche à mon gout, mais ca a l'air efficace, et surtout léger à côté de Vmware =p Disponible ici : http://sourceforge.net/projects/bochs/files/. Si vous recompilez Bochs sous linux vous pouvez spécifier d'ajouter l'interface graphique du debugger (assez poussée). Il y a aussi QEMU bien sûr, téléchargeable pour Windows ici : http://lassauge.free.fr/qemu/

Attention, si vous avez choisi Vmware, je vous déconseille d'activer le full debugger. A la première interruption non gérée, j'ai eu le droit à un joli freeze de ma machine (réelle, pas virtuelle !), avec le debugger qui tournait en tâche de fond jusqu'à kill du process.

Outil suivant : le compilateur asm. Je ne vais pas vous laisser le choix pour celui-ci, nasm à tout prix ! masm et tasm vous généreront des syntaxes un peu particulières, et le gas utilise la syntaxe AT&T (moins vous la fréquenterez, mieux vous vous porterez =) ) . nasm est disponible ici : http://www.nasm.us/pub/nasm/releasebuilds/?C=M;O=D . Je vous conseille la dernière version non RC (attention elles ne sont pas toujours triées par numéro de version). Puis dans le sous répertoire win32 vous avez un joli installeur ou un joli zip. Une fois installé / décompressé, il va vous falloir ajouter le dossier bin de nasm au PATH. Pour cela allez dans les propriétés systèmes / paramètres avancés / variables d'environnement / système : PATH / Modifier. Ajoutez un ; et collez le chemin du dossier bin (inclus !)

Héhé, je gardais le meilleur pour la fin : le compilateur C. Je vous conseille le portage de GCC sous win32, MinGW. Disponible ici : http://sourceforge.net/projects/mingw/files/ . Personnellement, j'ai pris l'Automated MinGW Installer / MinGW x.xx / MinGWxxx.exe . Qui marche très bien. Une fois installé (ou autre), ajoutez le dossier bin de MinGW au PATH.

C'est bien joli tout ça, vous pouvez désormais compiler n'importe quel .c en ouvrant une cmd dans le dossier du .c en tapant "gcc fichier.c". Aussi simple que ça. Ah non j'oubliais, MinGW étant le portage Windows de GCC, vous aurez un joli .exe bourré d'entêtes PE spécifiques à Windows : autrement dit vous ne pourrez rien en faire en dehors de Windows. Pour remédier à cela, nous allons devoir nous détacher de l'OS en laissant tomber ses entêtes et ses librairies (nous allons compiler un fichier binaire plat, ou "flat binary executable" =o).

Pour cela, 3 étapes sont nécessaires (sous Windows du moins !) :

- Changer ses habitudes

- Demander à gcc de compiler et d'assembler, mais de ne PAS linker

- Demander à ld de linker proprement

- Demander à objcopy de ne récupérer que l'essentiel

Tout d'abord, changer ses habitudes, en quoi cela consiste-t-il ? Désormais le point d'entrée de vos applications NE s'appellera PAS main. Pourquoi ? Parce que c'est un cas particulier, et lors du linkage il est détecté et traité spécialement. C'est agaçant :p Tout ce qu'il vous faudra faire c'est choisir un autre nom, du style "monMain" (vache c'est moche !) ou "entry_point" (ca pète déjà un peu plus 🙂 )

Deuxièmement, demander à gcc de faire du bon boulot :

gcc -c -ffreestanding -nostdinc -nostdlib -mno-stack-arg-probe -o #OUPUT#.o #INPUT#.c

#INPUT# et #OUTPUT# étant les noms de vos fichiers d'entrée et de sortie. Les options : -c demande de s'arrêter après l'assemblage, -ffreestanding demande de se passer du main et des librairies standards du c, -nostdinc -nostdlib demandent de ne pas inclure automatiquement les librairies standards, -mno-stack-arg-probe demande de ne pas utiliser l'instruction _alloca pour allouer la stack, et de simplement décrémenter le pointeur de pile à la place, et -o c'est juste pour choisir le nom =) GCC va vous générer un .o si vous compilez quelque chose avec cette commande

Troisième étape, le linker :

ld -i -e _#ENTRY POINT# -Ttext 0x0 -o #OUTPUT#.o #INPUT#.o

#INPUT# de ld = #OUTPUT# de gcc, attention #ENTRY POINT# correspond au nom de la fonction qui se substitue à l'habituel main (surtout ne pas oublier l'underscore devant le nom, sinon vous aurez une jolie erreur !). Les options : -o pour la sortie, comme avec gcc, -i pour pouvoir utiliser objcopy par la suite, -e pour spécifier le point d'entrée, -Ttext pour choisir l'adresse à laquelle doit être chargé le segement de code (0x0 pour être chargé au tout début =) ).

Enfin :

objcopy -R .note -R .comment -S -O binary #INPUT#.o #OUTPUT#

#INPUT# de objcopy = #OUTPUT# de ld. Les options : -R = enlève le segment spécifié, -O binary formatte la sortie en flat binary executable, et -S enlève les symboles et les informations écrites par -i de ld

Au final, vous avez un joli fichier sans extension. Pour le disséquer, rien de tel qu'un désassembleur ! J'imagine déjà "ah mince encore un soft à télécharger..." =) Point du tout, nasm apporte son propre désassembleur, qui se nomme ndisasm. Il suffit donc de faire "ndisasm #INPUT#" ou #INPUT# de ndisasm = #OUTPUT# de objcopy.

C'est bien joli tout ca mais je vais pas me retaper tout ça à chaque fois que je recompile ! C'est à ça que sert le batch. Créez un fichier .bat nommé comme vous le souhaitez, soit dans le dossier bin de nasm, soit dans le dossier bin de mingw (il faut qu'il soit facilement référencable par le PATH). Le contenu du fichier :

@echo off
gcc -c -ffreestanding -nostdinc -nostdlib -mno-stack-arg-probe -o %1.o %1.c
ld -i -e _%2 -Ttext 0x0 -o tmp.o %1.o
objcopy -R .note -R .comment -S -O binary tmp.o %1

ndisasm %1
del tmp.o
del %1.o

Voilà. Les del servent simplement à nettoyer un peu le dossier. La syntaxe d'appel de votre batch : "#BATCH# #FICHIER# #ENTRY POINT#", #BATCH# = nom du batch, #ENTRY POINT# = nom du main SANS l'underscore devant (ajoutée automatiquement dans le script), #FICHIER# = le nom du fichier à compiler SANS l'extension .c

Voilà, ce premier article est fini =p Si vous avez des outils à me suggérer, des critiques, des remarques, n'hésitez pas =)

Il y aura sûrement (beaucoup) d'autres articles dans cette catégorie =p