Introduction▲
Comme l'utilisation de la fonction system() soulève de nombreuses questions et qu'elle peut être utile, je me suis décidé à procéder à son explication détaillée afin de permettre aux non-initiés de la comprendre facilement.
Premier pas▲
Jetons un coup d'oeil sur le code suivant :
2.
3.
4.
5.
6.
7.
8.
9.
#include
<cstdlib>
#include
<iostream>
int
main(int
argc, char
**
argv) {
std::
cout <<
"Apprendre à utiliser la fonction system !"
<<
std::
endl;
int
value_returned =
std::
system("PAUSE"
);
std::
cout <<
"La valeur retournée est : "
<<
value_returned<<
std::
endl;
return
EXIT_SUCCESS;
}
Cette fois-ci, on a vérifié le code de retour de la commande exécutée, mais dans le reste de l'article nous ne nous occuperons plus de vérifier ce genre de code, quelle qu'en soit l'utilité.
Ce programme, une fois compilé et exécuté, affichera dans la console :
Apprendre à utiliser la fonction system !
Revenons sur la ligne 6 :
int
value_returned =
std::
system("PAUSE"
);
Pourquoi écrit-on l'expression system("PAUSE") ?▲
La console ouverte pour exécuter le programme fermera à la fin du code du main(). Comme le processeur est capable d'exécuter des milliers d'instructions par seconde, le temps entre l'affichage du message et la fermeture du code est très court, il ne permet pas au développeur de lire le message affiché. Ainsi, la ligne std::system("PAUSE") permet de mettre en pause le programme et de laisser le temps de lire le message.
Note : la commande PAUSE n'est compatible qu'avec Windows, pour Linux on utilise la commande read comme ceci :
read -
rsp $'Appuyer sur une touche pour continuer ...\n'
Qu'est-ce que la fonction system ?▲
La fonction system() permet au programmeur de pouvoir utiliser les commandes DOS/Shell dans son code C++.
À quoi sert cette fonction ?▲
Supposons que vous utilisiez un système comme Linux ou Windows et que vous vouliez exécuter des instructions DOS/Shell dans votre code, dans ce cas, la fonction system() va s'en occuper ! Mais comment ? Regardons l'exemple suivant :
si nous voulons effacer l'écran, nous pouvons tout à fait utiliser la fonction clrscr() qui se trouve dans le fichier d'en-tête conio.h(1) mais cette fois-ci, nous préférons utiliser la fonction system() en lui passant la commande à exécuter, cls dans le cas de Windows ou clear dans le cas de Linux :
system("cls"
);
system("clear"
);
Quels sont ses paramètres ?▲
La fonction system() reçoit un seul paramètre qui représente bien une commande système à exécuter. Son prototype est :
#include
<cstdlib>
int
system(const
char
*
command);
Par exemple dans le code précédent nous avons utilisé la commande PAUSE qui permet d'afficher la ligne suivante : Appuyer sur une touche pour continuer…
En ce moment le programme demeure en attente jusqu'à ce que l'utilisateur tape une touche clavier pour continuer l'exécution. Le principe de la commande PAUSE se résume dans les deux lignes suivantes : (sauf que dans cet exemple, l'utilisateur doit appuyer sur la touche entrée pour continuer l'exécution)
std::
cout <<
"Appuyer sur une touche «Entrée» pour continuer ..."
<<
std::
endl;
std::
cin.ignore(std::
numeric_limits<
int
>
::
max(), '
\n
'
);
Pour conclure :
La fonction system() joue le rôle de lanceur de commande entre l'utilisateur et la console, elle est responsable de l'exécution des commandes système.
L'utilisation de base de la fonction system() est tout à fait simple, mais quand même il faut être prudent en récupérant les erreurs.
Parmi les remarques importantes lors de l'utilisation des fonctions système (surtout la fonction system) on peut citer :
- il faut tester si le processeur de commande est disponible(2) ;
- d'une manière générale, l'utilisation des fonctions système donne un code plus ou moins « non portable » parce que les commandes systèmes diffèrent d'un système à l'autre(3) ;
- à l'origine, il n'y a pas de possibilité de lancer plusieurs commandes à la fois ;
- on ne récupère que le code de retour de la commande lancée. En plus, les codes de retour dépendent de l'implémentation ;
- system() ne permet pas de vérifier que la commande lancée a été bien exécutée ou pas ;
- pendant l'exécution de la commande, SIGCHLD sera bloquée, SIGINT et SIGQUIT seront ignorés(4) ;
- on ne sépare pas très bien le « Contrôleur » de la « Vue » si on veut faire du MVC(5) ;
- ne pas utiliser system() dans un programme de privilèges set-user-ID ou set-group-ID.(6)
Donnons des exemples maintenant pour bien comprendre la fonction system et se rendre compte de son importance.
Note : rappelons-nous que ces exemples sont spécifiques à la plate-forme Windows.
Exemple 1 : affichage d'heure et date▲
Dans cet exemple, nous voulons écrire un programme qui permet d'afficher et de changer l'heure et la date courantes quand il le faut.
Cette fois-ci nous avons besoin de la commande TIME et DATE, la première permet d'afficher l'heure et la deuxième d'afficher la date :
#include
<cstdlib>
#include
<iostream>
using
namespace
std;
int
main(int
argc, char
**
argv) {
system("TIME"
);
system("DATE"
);
system("CLS"
);
return
EXIT_SUCCESS;
}
Et si nous voulons changer l'heure, nous ajoutons l'option ou le commutateur /t :
system("TIME /t"
);
À part la fonction system(), il existe d'autres méthodes de récupérer la date et l'heure courantes, parmi lesquelles on cite(1) :
- la fonction time qui se trouve dans le fichier d'en-tête ctime ;
- la fonction GetSystemTime qui se trouve dans le fichier d'en-tête windows.h ;
- les classes CTime et CTimeSpan si vous développez avec Visual C++ et les MFC ;
- les classes TDate et TDateTime si vous développez avec C++ Builder et la VCL ;
- la bibliothèque boost::date_time si vous souhaitez garder un code C++ portable.
Exemple 2 : manipuler les états de l'ordinateur▲
Cette fois-ci nous allons écrire un programme dont voici le menu :
- Arrêter l'ordinateur
- Redémarrer
- Fermer la session
- Quitter
Pour écrire le code correspondant, nous aurons besoin des commandes suivantes :
SHUTDOWN /S /T 00
SHUTDOWN /R /T 00
SHUTDOWN /L
EXIT
Explication de la commande SHUTDOWN
La commande SHUTDOWN permet de manipuler les états de l'ordinateur (arrêt, redémarrage, mise en veille, veille prolongée…). Cette commande possède beaucoup de commutateurs dont voici quelques-uns :
- /i : manipuler les états d'ordinateur en mode graphique ;
- /l : fermer la session courante de l'utilisateur ;
- /s : arrêter l'ordinateur ;
- /r : redémarrer l'ordinateur ;
- /h : mise en veille prolongée ;
- /t xxx : définir la période de délai avant l'arrêt de l'ordinateur au bout de xxx secondes ;
- /a : annuler l'arrêt du système ;
/f : oblige la fermeture des applications en cours d'exécution sans prévenir les utilisateurs.
Pour en savoir plus, vous pouvez taper la commande : SHUTDOWN /HELP.
Sans oublier que la commande EXIT permet de quitter la console. (Oh ! je vois bien que c'est évident quand même !)
Maintenant nous pouvons tout à fait écrire le code de notre programme qui gère les états de l'ordinateur :
#include
<cstdlib>
#include
<iostream>
using
namespace
std;
int
main(int
argc, char
**
argv) {
char
choix;
cout <<
endl <<
"
\t\t
***************************************"
<<
endl
<<
endl <<
"
\t\t
******* La commande SHUTDOWN ******* "
<<
endl
<<
endl <<
"
\t\t
***************************************"
<<
endl;
cout <<
endl <<
"1 - Arreter l'ordinateur."
<<
endl
<<
endl <<
"2 - Redermarrer l'ordinateur."
<<
endl
<<
endl <<
"3 - Fermer la session."
<<
endl
<<
endl <<
"4 - Quitter le programme."
<<
endl;
cout <<
endl <<
"Donnez votre choix : "
<<
endl;
cin >>
choix;
switch
(choix) {
case
'1'
:system("SHUTDOWN /S /T 00"
);
break
;
case
'2'
:system("SHUTDOWN /R /T 00"
);
break
;
case
'3'
:system("SHUTDOWN /L"
);
break
;
case
'4'
:system("EXIT"
);
break
;
default
:
cout <<
"Erreur, choix incorrect !"
<<
endl;
}
return
EXIT_SUCCESS;
}
Si nous voulons colorer la police d'écriture et l'arrière-plan, nous devons utiliser la fonction system() en lui passant la commande COLOR qui prend les attributs des couleurs comme argument. Ces derniers sont spécifiés par DEUX chiffres hexadécimaux, le premier correspond à l'arrière-plan, le second à la police d'écriture. Chaque chiffre peut prendre n'importe laquelle de ces valeurs :
La ligne ajoutée est la suivante :
system("COLOR 2E"
);
Plaçons la commande de coloration avant l'affichage :
char
choix;
system("COLOR 2E"
);
cout <<
endl <<
"
\t\t
***************************************"
<<
endl
<<
endl <<
"
\t\t
******* La commande SHUTDOWN ******* "
<<
endl
<<
endl <<
"
\t\t
***************************************"
<<
endl;
Voici l'interface de notre code :
SetConsoleTextAttribute(GetStHandle(STD_OUTPUT_HANDLE), 14
);
Exemple 3 : changer le mot de passe du compte Windows▲
Dans cet exemple nous comptons aller un peu plus loin en écrivant un petit code qui permet la modification du mot de passe de l'utilisateur.
La commande utilisée NET USER est une sous-commande de la commande NET(7) qui permet de passer des commandes sur le réseau. Sachant qu'elle possède d'autres sous-commandes comme :
- NET SEND : envoie un message à un ordinateur distant ;
- NET START : démarre un service ;
- NETSTAT : affiche des statistiques et les connexions actives ;
- NET TIME : affiche la date et l'heure actuelles de l'ordinateur désigné comme étant le serveur de temps du domaine ;
-NET FILE : ferme un fichier partagé et supprime les verrous de fichier.
Revenons à notre mouton, la commande NET USER permet de manipuler les comptes d'utilisateurs, elle accepte certains commutateurs dont on peut citer :
- /ADD : ajoute un compte ;
- /DELETE : supprime un compte ;
- /ACTIVE:{YES | NO} : active ou désactive un compte ;
- /EXPIRES:{date | NEVER} : fait expirer le compte à une date définie ;
- /PASSWORDCHG:{YES | NO} : spécifie si les utilisateurs peuvent changer leur mot de passe ;
- /PASSWORDREQ:{YES | NO} : spécifie si un compte d'utilisateur doit avoir un mot de passe ;
- /LOGONPASSWORDCHG:{YES|NO} : spécifie si l'utilisateur doit changer son mot de passe à la prochaine ouverture de session.
Pour plus d'informations, tapez la commande : NET USER /HELP
Comme on s'intéresse ici à la modification du mot de passe d'un utilisateur, la syntaxe se présenterait ainsi : NET USER [nom_utilisateur] [mot_passe]
Une fois la commande connue, nous pouvons tout simplement changer le mot de passe de l'utilisateur courant :
#include
<cstdlib>
#include
<iostream>
using
namespace
std;
int
main(int
argc, char
**
argv) {
system("USER NET %USERNAME% 123456"
);
return
EXIT_SUCCESS;
}
L'obtention de l'erreur système 5 lors de l'exécution du code signifie que vous n'avez pas les droits nécessaires pour exécuter la commande NET USER afin de changer le mot de passe. Dans les versions récentes de Windows, pour manipuler ces genres de commandes DOS, vous êtes obligé de lancer le fichier exécutable en tant qu'administrateur pour des raisons de sécurité.
Exemple 4 : activer le gestionnaire de tâches et le registre ▲
Là, nous allons manipuler le registre de Windows afin d'activer/désactiver le gestionnaire de tâches ainsi que l'outil de registre. Évidemment, la plupart des virus désactivent automatiquement le gestionnaire des tâches afin de bloquer l'accès à ce dernier pour que l'utilisateur ne puisse plus tuer les processus virus en cours d'exécution. La commande DOS qui permet de réparer le gestionnaire des tâches est la suivante :
REG ADD hkcu\Software\Microsoft\Windows\CurrentVersion\policies\system /v DisableTaklMgr /t reg_dword /d 1
- l'instruction REG ADD permet d'ajouter une clé au registre ;
- Software\Microsoft\Windows\CurrentVersion\policies\system : représente le chemin de la clé que nous voulons ajouter ;
- /v Disable TaskMgr : nom de la clé ;
- /t reg_dword : type de clé.
- /d 0 : la valeur de clé (0 pour l'activation et 1 pour la désactivation).
Et voilà le code correspondant :
#include
<cstdlib>
using
namespace
std;
int
main(int
argc, char
**
argv) {
system("REG ADD hkcu
\\
Software
\\
Microsoft
\\
Windows
\\
CurrentVersion"
"
\\
policies
\\
system /v DisableTaklMgr /t reg_dword /d 1"
);
system("PAUSE"
);
return
EXIT_SUCCESS;
}
Nous pouvons faire pareil avec l'outil de registre :
#include
<cstdlib>
using
namespace
std;
int
main(int
argc, char
**
argv) {
system("REG ADD hkcu
\\
Software
\\
Microsoft
\\
Windows
\\
CurrentVersion"
"
\\
policies
\\
system /v DisableRegistryTools /t reg_dword /d 0"
);
system("PAUSE"
);
return
EXIT_SUCCESS;
}
Dangers de la fonction system dans la norme POSIX▲
system() est une fonction bien connue des Linux. Elle permet de lancer une commande passée en paramètre de la fonction. Mais contrairement aux fonctions exec(8), comme execve(), system() utilise certaines variables d'environnement pour chercher le fichier à lancer. Ainsi, system() se repère surtout grâce à $PATH. Cette variable contient la liste des répertoires dans lesquels les fichiers pourront être utilisés sans préciser le nom du répertoire dans lequel ils se trouvent.(2)
Par exemple, PATH contient /bin, donc si vous voulez exécuter /bin/commande, vous auriez juste à taper dans le shell : $ commande. En effet pour exécuter une commande ou un programme en C++ il est préférable de vérifier le PATH car la fonction system() utilise les variables d'environnement pour localiser la commande, sa vulnérabilité est donc due au fait que ces variables sont modifiables par l'utilisateur.(9)
Par exemple si nous avons un code qui contient l'instruction :
system("cat /etc/passwd"
);
Nous pourrions changer le PATH pour que les appels-système soient faits dans le répertoire voulu, par exemple :
export PATH
=
/home/snacker:$PATH
Dans ce cas, nous disons au Shell d'aller vérifier la présence du binaire cat dans le répertoire /home/snacker.
Maintenant, dans le répertoire /home/snacker, nous allons créer un programme qui s'appellera cat contenant l'instruction d'affichage :
cout <<
"La fonction system est vulnérable !"
<<
endl;
On compile :
g++ cat.cpp -o cat
et on exécute :
./cat
La sortie du programme va être « La fonction system est vulnérable » au lieu du contenu du fichier /etc/passwd. On remarque donc que la fonction system() exécute bien la commande cat dans notre PATH.
system() VS la famille exec▲
La famille des fonctions exec permet de recouvrir le processus courant par l'exécution d'un programme, c'est-à-dire qu'elle remplace l'image du processus appelant par une nouvelle image du processus sans changer le numéro du processus (PID). L'argument initial de toutes ces fonctions est le chemin d'accès du fichier à exécuter.(3)
La famille exec contient plusieurs fonctions de natures différentes, à savoir :
#include
<unistd.h>
extern
char
**
environ;
int
execl (const
char
*
chemin, const
char
*
arg, ...);
int
execlp (const
char
*
fichier, const
char
*
arg, ...);
int
execle (const
char
*
chemin, const
char
*
arg , ..., char
*
const
envp[]);
int
execv (const
char
*
chemin, char
*
const
argv []);
int
execvp (const
char
*
fichier, char
*
const
argv []);
int
execve (const
char
*
chemin, char
*
const
argv [], char
*
const
envp[]);
Les fonctions execl prennent comme premier argument le nom du programme à exécuter puis la liste de ses arguments terminée par NULL. Le premier argument d'un programme étant le nom de ce programme, les deux premiers arguments de execl doivent être les mêmes.
Par exemple pour recouvrir un processus par /usr/bin/ls -l :
execl("/usr/bin/ls"
, "/usr/bin/ls"
, "-l"
, NULL
);
Les fonctions execv() et execvp() utilisent un tableau de pointeurs sur des chaînes de caractères terminées par des caractères nuls, qui constituent les arguments disponibles pour le programme à exécuter. Par convention, le premier argument doit pointer sur le nom du fichier associé au programme à exécuter. Le tableau de pointeurs doit se terminer par un pointeur NULL.
Si l'une des fonctions exec() revient, c'est qu'une erreur a eu lieu. La valeur de retour est -1, et errno contient le code d'erreur.
La famille exec est plutôt réservée à Linux/Unix. Sous Windows on ne sait pas bien ce que ça peut donner. J'imagine mal qu'on puisse adapter cette famille sous Windows, car la gestion des processus y est beaucoup trop différente.
Si vous ne voulez pas utiliser la fonction system sous Windows, vous pouvez tout simplement appeler createProcess. (En fait, sous Windows, system appelle createProcess.)
La démarche d'exécution de la fonction system passe par les étapes suivantes :
tandis que celle de fork+exec se déroule ainsi :
Conclusion▲
Il n'est pas toujours facile de trouver un remplacement pour la fonction system(). L'une des possibilités est d'employer directement les appels-système execl() ou execle(). Toutefois, la sémantique diffère totalement puisque le programme externe n'est plus invoqué comme une sous-routine, mais la commande appelée remplace le processus en cours.(4)
En somme, il est préférable d'utiliser au maximum les bibliothèques alternatives en priorité, si cela ne marche pas vous pouvez utiliser la famille exec, et system dans des cas assez spécifiques.
Liens▲
(1) Comment gérer les dates et les heures en C++ ?
(4) Éviter les failles de sécurité dès le développement d'une application
Remerciements▲
Tout d'abord, je tiens à remercier le responsable de la rubrique C++, germinolegrand, pour sa gestion et son soutien et LittleWhite pour la relecture technique attentive de cet article sans oublier Neckara pour sa disponibilité spontanée malgré ses occupations multiples. Un grand merci également à ClaudeLELOUP qui m'a aidé pour l'orthographe.