Analyse du processus d'exécution de exec dans le noyau Linux

Lors de l'appel système exec dans le noyau Linux, l'exécutable est chargé et la pile est configurée. Cet article détaille ces étapes en se basant sur le code source du noyau version 2.6.32, en mettant l'accent sur le chargement ELF et la mise en place de la pile.

La fonction sys_execve est l'entrée de l'appel système exec. Son implémentation récupère le nom du fichier depuis l'espace utilisateur, puis délègue le travail à do_execve. La fonction do_execve gère le partage des descripteurs de fichiers, l'allocation de la structure linux_binprm pour stocker les informations d'exécution, et l'ouverture du fichier binaire.

int sys_execve(char *chemin_fichier, char **args, char **env_vars, struct pt_regs *registres)
{
    int code_erreur;
    char *nom_fichier = obtenir_nom_fichier(chemin_fichier);
    if (est_erreur(nom_fichier)) {
        code_erreur = extraire_erreur(nom_fichier);
        goto sortie;
    }
    code_erreur = executer_chargement(nom_fichier, args, env_vars, registres);
    liberer_nom_fichier(nom_fichier);
sortie:
    return code_erreur;
}

La structure linux_binprm est cruciale pour le processus de chargement. Elle contient des tampons pour les en-têtes, des pointeurs vers la mémoire et le fichier, ainsi que des informations sur les arguments et l'environnement.

struct info_executable {
    char tampon_entete[TAILLE_TAMPON];
    struct vm_area_struct *region_memoire;
    struct mm_struct *gestion_memoire;
    unsigned long position_courante;
    struct fichier *fichier;
    struct credentials *identifiants;
    int compteur_args, compteur_env;
    char *nom_fichier;
    char *interpreteur;
    // autres champs
};

La fonction preparer_info_executable initialise les identifiants, ouvre le fichier, et lit les premiers octets pour vérifier le format. Elle gère également les bits setuid/setgid et la sécurité.

int preparer_info_executable(struct info_executable *info)
{
    struct inode *inoded_fichier = info->fichier->inode;
    mode_t mode_fichier = inoded_fichier->i_mode;

    info->identifiants->uid_effectif = uid_effectif_courant();
    info->identifiants->gid_effectif = gid_effectif_courant();

    if (mode_fichier & S_ISUID) {
        info->identifiants->uid_effectif = inoded_fichier->i_uid;
        info->bits_a_effacer |= EFFACER_SUR_SETID;
    }
    if ((mode_fichier & S_ISGID) && (mode_fichier & S_IXGRP)) {
        info->identifiants->gid_effectif = inoded_fichier->i_gid;
        info->bits_a_effacer |= EFFACER_SUR_SETID;
    }

    securite_definir_identifiants(info);
    memset(info->tampon_entete, 0, TAILLE_TAMPON);
    return lire_fichier_noyau(info->fichier, 0, info->tampon_entete, TAILLE_TAMPON);
}

Les arguments et les variables d'environnement sont copiés dans la pile à l'aide de la fonction copier_chaines. Cette fonction alloue des pages mémoire et copie les données depuis l'espace utilisateur de manière inverse, en utilisant une pile croissante vers les basses adresses.

static int copier_chaines(int nombre_args, char **tableau_args, struct info_executable *info)
{
    struct page *page_courante = NULL;
    char *adresse_noyau = NULL;
    unsigned long position_page = 0;

    while (nombre_args-- > 0) {
        char *chaine_utilisateur;
        int longueur_chaine;
        unsigned long position;

        if (obtenir_chaine_utilisateur(tableau_args + nombre_args, &chaine_utilisateur) ||
            (longueur_chaine = mesurer_chaine(chaine_utilisateur, MAX_LONGUEUR_ARG)) == 0) {
            return -EFAULT;
        }
        if (!verifier_longueur_arg(info, longueur_chaine)) {
            return -E2BIG;
        }

        position = info->position_courante;
        chaine_utilisateur += longueur_chaine;
        info->position_courante -= longueur_chaine;

        while (longueur_chaine > 0) {
            int decalage, octets_a_copier;
            decalage = position % TAILLE_PAGE;
            if (decalage == 0) decalage = TAILLE_PAGE;
            octets_a_copier = min(decalage, longueur_chaine);
            decalage -= octets_a_copier;
            position -= octets_a_copier;
            chaine_utilisateur -= octets_a_copier;
            longueur_chaine -= octets_a_copier;

            if (!page_courante || position_page != (position & MASQUE_PAGE)) {
                struct page *nouvelle_page = obtenir_page_argument(info, position, 1);
                if (!nouvelle_page) return -E2BIG;
                if (page_courante) liberer_page_noyau(page_courante);
                page_courante = nouvelle_page;
                adresse_noyau = mapper_page_noyau(page_courante);
                position_page = position & MASQUE_PAGE;
            }
            if (copier_depuis_utilisateur(adresse_noyau + decalage, chaine_utilisateur, octets_a_copier)) {
                return -EFAULT;
            }
        }
    }
    if (page_courante) liberer_page_noyau(page_courante);
    return 0;
}

Après la copie des chaînes, la fonction chercher_gestionnaire_binaire recherche un format de fichier exécutable compaitble. Le noyau maintient une liste de formats (linux_binfmt), chacun avec des fonctions pour charger le binaire, les bibliothèques partagées et générer des fichiers core.

struct format_binaire {
    struct list_head liste;
    struct module *module;
    int (*charger_binaire)(struct info_executable *, struct pt_regs *);
    int (*charger_bibliotheque)(struct fichier *);
    int (*generer_core)(long signal, struct pt_regs *, struct fichier *, unsigned long limite);
    unsigned long taille_min_core;
    int supporte_vdso;
};

Le format ELF est défini dans binfmt_elf.c avec les fonctions correspondantes. La fonction charger_binaire_elf est responsable du chargement principal. Elle analyse les en-têtes ELF, charge le programme et l'interpréteur dynamique, et configure la pile.

int charger_binaire_elf(struct info_executable *info, struct pt_regs *registres)
{
    struct fichier *interpreteur = NULL;
    unsigned long adresse_chargement = 0, biais_chargement = 0;
    int adresse_chargement_fixe = 0;
    char *chemin_interpreteur = NULL;
    struct en_tete_elf *entete_elf = NULL;
    struct programme_header *headers_programme = NULL;
    unsigned long debut_code, fin_code, debut_donnees, fin_donnees;
    int pile_executable = EXSTACK_DEFAUT;

    // Lecture et validation de l'en-tête ELF
    entete_elf = (struct en_tete_elf *)info->tampon_entete;
    if (memcmp(entete_elf->magic, ELF_MAGIC, 4) != 0) {
        return -ENOEXEC;
    }

    // Allocation et lecture des headers de programme
    int taille_headers = entete_elf->nombre_ph * sizeof(struct programme_header);
    headers_programme = kmalloc(taille_headers, GFP_KERNEL);
    if (!headers_programme) return -ENOMEM;
    if (lire_fichier_noyau(info->fichier, entete_elf->offset_ph, (char *)headers_programme, taille_headers) != taille_headers) {
        kfree(headers_programme);
        return -EIO;
    }

    // Recherche de l'interpréteur dynamique
    for (int i = 0; i < entete_elf->nombre_ph; i++) {
        if (headers_programme[i].type == PT_INTERP) {
            chemin_interpreteur = kmalloc(headers_programme[i].taille_fichier, GFP_KERNEL);
            lire_fichier_noyau(info->fichier, headers_programme[i].offset, chemin_interpreteur, headers_programme[i].taille_fichier);
            interpreteur = ouvrir_executable(chemin_interpreteur);
            break;
        }
    }

    // Chargement des segments PT_LOAD
    for (int i = 0; i < entete_elf->nombre_ph; i++) {
        if (headers_programme[i].type == PT_LOAD) {
            int protections = 0;
            if (headers_programme[i].flags & PF_R) protections |= PROT_READ;
            if (headers_programme[i].flags & PF_W) protections |= PROT_WRITE;
            if (headers_programme[i].flags & PF_X) protections |= PROT_EXEC;
            mapper_segmente(info->fichier, biais_chargement + headers_programme[i].adresse_virtuelle, &headers_programme[i], protections, MAP_PRIVATE);
        }
    }

    // Chargement de l'interpréteur si présent
    unsigned long entree_programme = entete_elf->entree;
    if (interpreteur) {
        entree_programme = charger_interpreteur_elf(interpreteur, biais_chargement);
    }

    // Configuration de la pile
    configurer_pages_argument(info, pile_executable);
    creer_tables_elf(info, entete_elf, adresse_chargement);

    // Nettoyage et finalisation
    kfree(headers_programme);
    kfree(chemin_interpreteur);
    demarrer_thread(registres, entree_programme, info->position_courante);
    return 0;
}

La configuration de la pile dans créer_tables_elf implique plusieurs étapes : alignement de la pile avec un décalage aléatoire, copie des identifiants de plateforme et de données aléatoires, allocation d'espace pour les vecteurs auxiliaires, et initialisation des arguments et de l'environnement.

int creer_tables_elf(struct info_executable *info, struct en_tete_elf *entete, unsigned long adresse_chargement)
{
    unsigned long position_pile = info->position_courante;
    position_pile = aligner_pile(position_pile); // Applique un décalage aléatoire et alignement 16 octets

    // Copie de la chaîne de plateforme
    copier_plateforme_utilisateur(&position_pile);

    // Copie des octets aléatoires pour la sécurité
    copier_octets_aleatoires(&position_pile);

    // Allocation pour les vecteurs auxiliaires
    int nombre_vecteurs = calculer_nombre_vecteurs(entete);
    position_pile -= nombre_vecteurs * sizeof(struct vecteur_auxiliaire);

    // Allocation pour argc, argv et envp
    int total_elements = info->compteur_args + 1 + info->compteur_env + 1 + 1;
    position_pile = arrondir_pile(position_pile, total_elements);
    info->position_courante = position_pile;

    // Initialisation de argc
    if (ecrire_utilisateur(position_pile, &info->compteur_args, sizeof(int))) return -EFAULT;
    position_pile += sizeof(unsigned long);

    // Initialisation de argv
    initialiser_argv(&position_pile, info->args, info->compteur_args);

    // Initialisation de envp
    initialiser_envp(&position_pile, info->env_vars, info->compteur_env);

    // Initialisation des vecteurs auxiliaires
    initialiser_vecteurs_auxiliaires(&position_pile, entete, adresse_chargement);

    return 0;
}

La pile finale est structurée avec argc au sommet, suivi des pointeurs vers argv et envp, des vecteurs auxiliaires, des octets aléatoires, de la chaîne de plateforme, et enfin des chaînes d'argument et d'environnement. L'espace de pile peut contenir du remplissage dû à l'alignement et à la distribution aléatoire.

Ce processus complet assure le chargement sécurisé et correct des exécutables ELF et la mise en place de l'environnement d'exécution dans le noyau Linux.

Étiquettes: linux-kernel exec-system-call elf-loading memory-management stack-setup

Publié le 1 août à 06h10