C

Le parseur de wmfs

date
20 / 4 / 2010
comments
0

EDIT: ce billet se voulait une vulgarisation, mais il faut quand même savoir manipuler les notions d'automates, de grammaire etc. Si ça donne envie d'en savoir plus, tant mieux.

En même temps que j'ai laché dwm pour revenir au bon vieux (pas si vieux que ça) wmfs, j'ai totalement recodé le parseur du fichier de configuration.

Un fichier de configuration se doit d'être lisible, et sa projection en mémoire doit être facile à manipuler, malheureusement utiliser le même langage (comme le fait dwm avec son fichier de configuration directement en C) rend la configuration moins convi pour peu que l'on ne soit pas expert. Le parseur sert à ça, traduire un langage simple en langage compréhensible par la machine.

L'idée c'est d'expliquer un peu comment le parseur de wmfs fonctionne parce que ça touche une partie de l'informatique théorique que j'aime bien : l'étude des langages formels.

Yacc et lex sont souvent utilisés pour ce genre de choses. Mon code est juste une implémentation C d'un automate fini déterministe, il ne doit pas être loin du fonctionnement de yacc pour une grammaire et une syntaxe particulière (évidement yacc&lex sont beaucoup plus évolués que mon petit bout de code).

Donc le travail de lex c'est de lire le fichier caractère par caractère et de renvoyer des token, c'est à dire un identifiant syntaxique.

Le langage de configuration est comme ça :

[section]
    option = valeur
    truc = { "liste", 1, False }
    [sous-section]
        [sous-sous-section]
            machin = True
        [/sous-sous-section]
    [/sous-section]
[/section]

Je fais une première transformation purement syntaxique :

enum conf_type { SEC_START, SEC_END, WORD, EQUAL, LIST_START, LIST_END, NONE };
/* qui correspond à  [        [/    un_mot  =        {         }       autre */

Et je range ça dans deux listes chainées, une qui contient des conf_type et une autre qui contient les mots (des char *) :

Liste des TOKEN             Liste des mots (WORD)
SEC_START
WORD                        "section"
WORD                        "option"
EQUAL
WORD                        "valeur"
WORD                        "truc"
EQUAL
LIST_START
WORD                        "liste"
WORD                        "1"
WORD                        "False"
SEC_START
WORD                        "sous-section"
SEC_START
WORD                        "sous-sous-section"
WORD                        "machin"
EQUAL
WORD                        "True"
SEC_END
WORD                        "sous-sous-section"
SEC_END
WORD                        "sous-section"
SEC_END
    WORD                        "section"

Une fois ces deux piles construites, il suffit de dépiler en suivant la grammaire (c'est ce que fait yacc). C'est là qu'on parle d'états finis. Désolé ça aurait été plus compréhensible avec un schéma, mais je sais pas en faire donc j'ai écrit ça avec la syntaxe qu'on utilise pour décrire des grammaires.

start: /* rien */
       | SEC_START WORD sections SEC_END WORD start /* une suite de sections */
       ;

sections: /* rien */
           | SEC_START WORD sections SEC_END WORD /* des sous sections */
           | WORD EQUAL option sections /* des options */
           ;
option: WORD /* un mot tout simple (une valeur quoi) */
          | LIST_START list LIST_END /* une liste de valeurs */
          ;
 list: /* rien */
     | WORD list /* une liste de mots */
     ;

Le parseur va ainsi remplir une structure générique capable de contenir toutes les configurations qui respectent la grammaire et la syntaxe.

Pour wmfs en gros c'est ça :

struct conf_opt { /* options */
    char *name;
    char *val[10]; /* au plus 10 éléments dans une liste */
    SLIST_ENTRY(conf_opt) entry;
}
struct conf_sec { /* section */
    char *name;
    SLIST_HEAD(, conf_opt) optlist; /* liste chaînée des options de la section */
    TAILQ_HEAD(, conf_sec) sub; /* liste chaînée des sous sections */
    TAILQ_ENTRY(conf_sec) entry;
}

J'utilise abusivement des macros de <sys/queue.h>.

Ensuite il suffit de coder une API qui va récupérer des options/sections précises sur la structure, mais c'est pas le plus dur à faire.

Voilà un peu l'idée, l'intérêt de cette méthode c'est qu'elle est certaine (pas ou peu de bugs incompréhensibles), rapide : le fichier de configuration n'est traversé qu'une seule fois, pas besoin de strlen(), strchr(), strcmp(), strstr() qui prennent des plombes.

EDIT : j'ai failli oublier de donner le lien vers le code

realpath secure ?

date
15 / 3 / 2010
date
C
comments
3

Dans le cadre d'une application serveur (type transfert de fichier : http, ftp, ...), pour contrôler l'accès aux fichiers j'utilise realpath(3). Cette fonction permet d'avoir le path (chemin) réel d'un fichier, sans lien relatif dans un des dossiers. Ainsi on a juste à faire un strncmp(3) pour le comparer à la racine de l'application.

Par exemple une partie du code du serveur HTTP que je suis en train de coder (uri est quelque chose de la forme /../foo/bar/../machin.html et conf_root la racine du style /usr/local/www/

char path[PATH_MAX];
char root[PATH_MAX];

if (!realpath(uri+1, path) ||
        !realpath(conf_root, root) ||
        strncmp(path, root, strlen(root)) != 0)
    return send_error(404);

Prenez ce post pour une question, à part le chroot(2) et realpath, comment bien contrôler l'accès aux fichiers ?

J'y connais rien en filesystems, mais si on pouvait avoir un moyen rapide de savoir si un fichier est dans tel ou tel fichier ce serait grandement pratique. Sans me vanter j'ai souvent des idées géniales en informatique, mais bien souvent ça a été pensé et implémenté depuis plus de 10 ans ! Du coup si vous avez plus d'information, je suis prêt à converser sur ce sujet avec vous...

Jouons avec kvm

date
13 / 2 / 2010
comments
1

J'ai codé un petit programme tout simple qui fait clignoter les leds de mon routeur dès qu'un service est par terre. Ma méthode (qui ne doit pas être la meilleure mais je m'en fout un peu) consiste à regarder régulièrement la liste des processus et détecter l'arrêt d'un service quand le processus associé n'existe plus. J'ai commencé avec un script shell, puis étant assez mauvais pour tout ce qui ressemble de près ou de loin à du script j'ai décidé de le faire en C (langage d'excellence n'est ce pas...?).

Je me suis volontairement compliqué la tache en utilisant des fonctions très proches du système, ainsi s'il est possible d'obtenir une liste de processus via ps(1) avec un popen(3) super crade ou encore sysctl(3) j'ai voulu utiliser kvm(3) kernel memory interface qui est en programmation système la méthode la plus appropriée (d'ailleurs 'ps' est codé avec ça), c'est juste une interface qui va nous permettre d'accéder au données du kernel (une copie bien entendu), ici la liste des processus en cours.

Le hic c'est que si les fonctions de kvm sont très standard l'implémentation est très différente suivant les systèmes, comme à peu près toute implémentation, l'important c'est que l'interface soit standard. Ainsi à la lecture de la structure qui décrit les processus kinfo_proc de sys/user.h on se dit chouette pleins d'info à disposition, sauf que sur OpenBSD c'est pas du tout la même forme (ni le même fichier d'ailleurs). D'où l'importance d'une interface riche et c'est ce que nous promet kvm.

Assez causé passons au code (je n'explique pas ou peu le code en dehors des fonctions kvm)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <err.h>
#include <libgen.h>
#include <fcntl.h>
#include <limits.h>
#include <paths.h>
#include <kvm.h>
#include <sys/param.h>
#include <sys/sysctl.h>
#include <sys/wait.h>
#include <sys/user.h>

void
start_leds(void)
{
    /* Ici on execute /usr/sbin/gioctl -q gpio0 led3 (on|off)
     * suivant la parité du conteur statique 'i'
     * 60 fois avec une pause d'une seconde. */
    static int i = 1;
    switch(fork())
    {
        case -1:
            err(1, "fork");
            break;
        case 0:
            execl("/usr/sbin/gpioctl", "gpioctl", "-q", "gpio0",
                    "led3", (i%2) ? "on" : "off", (char*)NULL);
            err(1, "execl");
            break;
        default:
            wait(NULL);
            break;
    }
    sleep(1);
    if (i++ <= 60)
        return start_leds();
    return;
}

int
main(int argc, char *argv[])
{
    kvm_t *kd;
    char berr[_POSIX2_LINE_MAX];
    struct kinfo_proc *p, *procs;
    int n, j, k, found = 0;
    char **pargv;

    if (argc < 2)
        errx(1, "Usage check_proc procs ...");

    /* On accède aux données du kernel qui tourne actuellement (NULL) */
    if (!(kd = kvm_open(NULL, NULL, NULL, O_RDONLY, berr)))
        errx(1, "%s", berr);

    /* On demande la liste des processus, kinfo_proc est décrite
     * dans sys/sysctl.h sur OpenBSD et sys/user.h sur FreeBSD.
     * Il nous la donne dans un tableau continu de pointeurs de
     * taille qu'on récupère dans 'n'. */
    if (!(procs = kvm_getprocs(kd, KERN_PROC_ALL, 0, &n)))
        err(1, "kvm_getprocs: %s", kvm_geterr(kd));

    for (k = 1; k < argc; k++)
    {
        for (j = 0, p = procs; j < n; j++, p++)
        {
            /* On accède à la liste des arguments du programme qui a
             * généré le processus courant. */
            pargv = kvm_getargv(kd, p, 0);
            if (pargv && pargv[0] &&
                    strstr(pargv[0], argv[k]))
                {
                    found++;
                    break;
                }
        }
    }

    if (-1 == kvm_close(kd))
        err(1, "kvm_close");

    if (found != argc-1)
        start_leds();

    return 0;
}

Et l'exécution, on veut tester si un processus du nom de 'dhcpd' tourne sur le système, si ce n'est pas le cas on lance le clignotement de la led pendant une minute :

# gcc -o check_proc check_proc.c -lkvm
# ./check_proc dhcpd

Le mien s'exécute dans un cron toute les minutes :

* * * * * /root/bin/check_proc named dhcpd adsuck ntpd

Un système de ramasse miette en C

date
3 / 10 / 2009
date
C
comments
0

Je viens d'implémenter dans mon shell un système de ramasse miette. Le choix de faire un tel système est particulièrement adapté pour un shell ou pour tout programme qui est fortement cyclique et avec un cycle assez court où chaque allocation peut être libérée rapidement.

En gros l'astuce consiste à créer des fonctions wrapper pour free(3) et malloc(3). Une fois que tous nos appels d'allocation et de libération de mémoire passent par eux il est facile de créer une structure de type pile qui empile l'adresse de chaque allocation pour ensuite tout libérer d'un coup à la fin du cycle.

Pour cela j'ai utilisé les SLIST de sys/queue.h pour la pile que voilà :

typedef struct stack_s {
    void * adr;
    SLIST_ENTRY(stack_s) next;
} stack_s;

Ensuite le wrapper pour malloc (notez qu'on déclare la tête de pile comme variable globale (dans le fichier avec les fonctions wrapper) pour pouvoir y accéder facilement.

/* déclaration de la pile mstack (qu'on initialise à NULL) */
static SLIST_HEAD(, stack_s) mstack = SLIST_HEAD_INITIALIZER(&mstack);

/* Le wrapper pour malloc */
void *
xmalloc(size_t size)
{
    void *ret;
    stack_s *el; /* l'élément a rajouter dans la pile */

    ret = malloc(size);

    if (!ret)
    {
        perror("Malloc ");
        exit (EXIT_FAILURE);
    }

    el = malloc(sizeof(*el));
    el->adr = ret;
    SLIST_INSERT_HEAD(&mstack, el, next);

    return ret;
}

/* Cette fonction vide toute la pile et libère la mémoire */
void
stack_delete(void)
{
    stack_s *el;

    while (!SLIST_EMPTY(&mstack))
    {
        el = SLIST_FIRST(&mstack);
        SLIST_REMOVE_HEAD(&mstack, next);
        free(el->adr);
        free(el);
    }

    return;
}

Voilà, maintenant faites tous vos allocations temporaires avec _malloc et videz la pile de temps en temps (ie à la fin du cycle général de votre programme).

Par exemple dans la boucle principale ça donne quelque chose comme ça :

for (;;)
{
    machin = xmalloc(sizeof(*machin) * 10);
    /*
        ...
        ...
    */

    stack_delete();
}

J'espère que c'est compréhensible, si vous avez une question n'hésitez surtout pas à me demander plus d'information.

Petite présentation de sys/queue.h

date
26 / 9 / 2009
date
C
comments
2

Dans vos programmes C, il arrive très fréquemment d'utiliser des listes chaînée (de toutes sortes), afin de ne pas avoir à recréer le monde à chaque fois vous pouvez utiliser les macros super pratiques de queue(3). Voici une petite introduction à l'utilisation de sys/queue.h avant d'aller se plonger dans le man pour aller plus loin.

Avec sys/queue.h , vous pouvez manipuler facilement quatres sortes de listes :

  • Listes simplement chainées (SLIST) de type pile
  • Listes doublement chainées (LIST) de type pile
  • Listes simplement chainées (STAILQ) de type file
  • Listes doublement chainées (TAILQ) de type file

Voici un exemple de SLIST :

Le header :

#ifndef HEADER_H
#define HEADER_H

#include <sys/queue.h>

typedef struct MaStruct {
    /* ici on met nos elements par exemple : */
    char *str;

    /*
     * On déclare notre liste avec SLIST_ENTRY qui est equivalente à
     * struct { struct MaStruct *sle_next; } next;
     */

    SLIST_ENTRY(MaStruct) next;
} MaStruct;

#endif /* HEADER_H */

Ensuite voyons comment manipuler cette liste :

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

#include "header.h"

int
main(void)
{
    /* SLIST_HEAD crée une variable pour acceder à notre liste */
    SLIST_HEAD(, MaStruct) head;

    /* on crée un element MaStruct */
    MaStruct *p;

    /* On l'initialise à NULL */
    SLIST_INIT(&head);

    p = malloc(sizeof(*p));
    p->str = malloc(sizeof(char) * 10);
    strcpy(p->str, "blah");

    /* On insere p dans la SLIST */
    SLIST_INSERT_HEAD(&head, p, next);

    /* On empile un deuxième element */
    p = malloc(sizeof(*p));
    p->str = malloc(sizeof(char) * 10);
    strcpy(p->str, "blu");

    SLIST_INSERT_HEAD(&head, p, next);

    /*
     * Il est très facile de parcourir la liste
     * que ce soit pour du traitement ou de
     * la libération de mémoire
     */
    SLIST_FOREACH(p, &head, next) {
        /* ici p est l'element courant */
        printf("element : %s\n", p->str);
    }

    /* exemple de libération */
    while (!SLIST_EMPTY(&head))
    {
        p = SLIST_FIRST(&head);
        SLIST_REMOVE_HEAD(&head);
        free(p->str);
        free(p);
    }

    return 0;
}

Parmis les macros interessantes pour les SLIST il y a :

SLIST_FIRST(&head);                /* renvoie la tête de liste */
SLIST_REMOVE_HEAD(&head, next);    /* supprime la tete de liste */
SLIST_EMPTY(&head); /* renvoie vrai si la liste est vide */

Ben entendu, il y a beaucoup plus de possibilités avec LIST et TAILQ... Mais globalement vous devriez avoir compris le principe.

La lecture de sys/queue.h devrait vous éclairer pour comprendre comment marchent toutes ces macros, c'est assez facile à comprendre si vous avez (comme moi) un niveau moyen en C.

Voyez aussi ce papier d'iMil.