blog · git · desktop · images · contact & privacy · gopher


Details zum Shebang-Mechanismus unter Linux

Es gibt so Details, die mich faszinieren. Eines davon ist der Shebang-Mechanismus, der einiges an Flexibilität ermöglicht.

Im Forum von Ubuntuusers.de wurde ein Bash-Skript ungewollt mit der Dash ausgeführt -- was meistens in die Hose geht. Bei einem Aufruf über "sh /tmp/skript.sh" wird die Shebang-Zeile ignoriert. In meiner Antwort schrieb ich, dass sich bei einem "direkten" Aufruf wie "./skript.sh" der Kernel stattdessen darum kümmern und den richtigen Interpreter wählen würde.

Dass der Kernel das tut, war nichts neues, aber wie er das tut, war mir dann auch nicht ganz klar. Für den Thread im Forum war das nicht mehr wichtig, aber ich nahm ihn als Anlass, um mir das mal genauer anhand der Quellcodes der glibc 2.11.1 und Linux 2.6.34-rc4 (bzw. den jeweils aktuellen git-Checkouts) anzuschauen.

Zuerst ein bisschen Erklärung. Am Ende ist nochmal eine Übersicht und dort sind auch die relevanten Dateien verlinkt. gitweb sei Dank. ;)

– Nachtrag vom 18.09.2010: Bisweilen täte es sich schon lohnen, genauer aufzupassen, was so durch die Magazine rauscht. Völlig an mir vorbeigegangen ist ein Artikel im freien Magazin (November 2009) von meillo -- den ich ja unter anderem wegen seiner Vorträge im ChaosSeminar schätze (z.B. 1, 2 und 3). Thema des Artikels ist genau das hier, der Shebang-Mechanismus. Ein Blick lohnt sich. :)

Grundsätzlicher Aufbau der Shebang

Der Aufbau der Shebang ist erstmal ganz einfach:

#!/pfad/zum/interpreter -a -b -c

Diese ersten zwei Bytes ("#!") bilden dabei die Magic Number, die praktischerweise aber von Menschen les- und schreibbar ist. Danach der Pfad zum Interpreter, gefolgt von optionalen Argumenten.

Auf manchen Systemen können beliebig viele Argumente folgen, was unter Linux aber nicht funktioniert. Stattdessen sieht Linux alles folgende als ein Argument an.

In diesem Beispiel ...

#!/usr/bin/python -t -v

... würde "-t -v" als ein Argument aufgefasst werden.

FreeBSD soll es wohl aufsplitten und alle Argumente dann nehmen. Solaris soll auch splitten, aber nur das erste Argument nehmen.

Aufrufhierarchie – aka "Wer macht was?"

Aus dem Userland werden Prozesse mit "fork() -> exec()" erzeugt, also zuerst wird der aktuelle Prozess kopiert und dann dieser mit einem anderen Programm ersetzt. Kein Programm weiß dabei, womit es es zu tun hat -- ein "exec("skript.sh")" würde funktionieren. exec() ist also der Knackpunkt.

Die exec()-Funktionen sind erstmal Funktionen, die in der jeweiligen libc existieren, denn Syscalls setzt man in der Regel nicht unmittelbar ab. In der glibc ist das ziemlich komplex, aber es läuft darauf hinaus, dass die einzelnen exec()-Varianten alle auf execve() verweisen, was wiederum auf __execve() verweist. Das hängt dann vom Betriebssystem ab, unter Linux wird es ein Syscall zu "execve", was dann im Kernel sys_execve() heißt. Von dort aus geht es zu do_execve() und hier beginnt der interessante Teil.

binfmt_* und Shebang-Parsing

Linux verwaltet eine Liste von Handlern, die sich um ausführbare Dateien kümmern können. In do_execve() wird search_binary_handler() aufgerufen, was diese Liste abarbeitet und den passenden Handler heraussucht. Jedem Handler wird die auszuführende Datei vorgeworfen und der Handler schaut, ob er etwas damit anfangen kann.

Es gibt mehrere solcher Handler, zum Beispiel für das ELF-Format, für a.out, den noch allgemeineren binfmt_misc-Mechanismus und eben auch einen Handler für Dateien mit Shebang.

Dieser Handler ist fs/binfmt_script.c. Er springt an, wenn die ersten zwei Zeichen der Datei "#!" sind. Hier wird die Zeile zerlegt, was ziemlich freaky aussieht. Das hat auch der Autor gemerkt:

/*
 * This section does the #! interpretation.
 * Sorta complicated, but hopefully it will work.  -TYT
 */

;)

Hier finden sich dann die interessanten Details. Zum einen wird nur nach "\n" als Ende der Shebang gesucht, was zu Fehlern führt, wenn Skripte zum Beispiel mit Windows-Zeilenenden abgespeichert werden. Das wird über strchr() gemacht, weswegen es auch eher mühsam wäre, noch eine Fallunterscheidung einzufügen ("\r" oder "\n"). Man könnte vielleicht alle "\r" mit "\n" ersetzen, aber ob sich das wirklich lohnen würde oder ob es sinnvoll wäre, sei mal dahingestellt. Die Erzeugung eines Prozesses ist auch so schon teuer genug.

Wird gar kein Newline gefunden oder ist der Pfad zu lang, dann werden maximal die ersten 127 Byte verwendet. Auch das Argument wird abgeschnitten. In diese Zählung fallen aber auch Leerzeichen und Tabs, die in der Shebang-Zeile eingestreut sind.

Um das Zerlegen besser zu verstehen, kann man sich ganz einfach den Teil aus load_script() kopieren und in ein kleines Testprogramm setzen. Kommentare, printf()'s und etwas Formatierung von mir:

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

#define BINPRM_BUF_SIZE 128

int main(int argc, char **argv)
{
    char buf[BINPRM_BUF_SIZE] = "";
    char *cp, *i_name, *i_arg;

    if (argc < 2)
    {
        printf("Shebang-Zeile als argv[1] übergeben.\n");
        exit(EXIT_FAILURE);
    }
    strncpy(buf, argv[1], BINPRM_BUF_SIZE);

    if ((buf[0] != '#') || (buf[1] != '!'))
    {
        printf("Keine Shebang.\n");
        exit(EXIT_FAILURE);
    }

    /* Finde das Ende -- ein \n oder maximale Länge. */
    buf[BINPRM_BUF_SIZE - 1] = '\0';
    if ((cp = strchr(buf, '\n')) == NULL)
        cp = buf + BINPRM_BUF_SIZE - 1;
    *cp = '\0';

    /* Ignoriere abschließende Spaces oder Tabs. */
    while (cp > buf)
    {
        cp--;
        if ((*cp == ' ') || (*cp == '\t'))
            *cp = '\0';
        else
            break;
    }

    /* Ignoriere führende Spaces oder Tabs nach der Shebang. */
    for (cp = buf + 2; (*cp == ' ') || (*cp == '\t'); cp++);
    if (*cp == '\0')
    {
        printf("Kein Interpretername gefunden.\n");
        exit(EXIT_FAILURE);
    }

    /* Der Name des Interpreters steht jetzt fest, aber das Argument
     * fehlt noch. */
    i_name = cp;
    i_arg = NULL;

    /* Suche den ersten Space oder Tab nach dem Interpreter. */
    for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
        /* nothing */ ;

    /* Ignoriere führende Spaces oder Tabs vor dem Argument. */
    while ((*cp == ' ') || (*cp == '\t'))
        *cp++ = '\0';

    /* Wenn jetzt noch nicht das Ende des Strings erreicht ist, dann
     * haben wir ein Argument. Sonst nicht. */
    if (*cp)
        i_arg = cp;

    printf("Interpreter: `%s'\n", i_name);
    printf("Argument: `%s'\n", i_arg);
    exit(EXIT_SUCCESS);
}

Jetzt kann man damit spielen und eventuell noch weitere printf()'s ergänzen, wenn man das will. Die $'...'-Syntax der Bash bietet sich hier an, um einfach Newlines und Tabs einzufügen:

$ ./shebang $'#!/bin/bash  \t  -a -b  \n das hier nicht mehr'
Interpreter: `/bin/bash'
Argument: `-a -b'

Man sieht zum Beispiel, dass Whitespace vor und nach dem Interpreter keine Rolle spielt, ebenso nach dem Argument. Im Argument jedoch werden Spaces und Tabs einfach übernommen:

$ ./shebang $'#!/bin/bash    -a\t\t-b     \n blah'
Interpreter: `/bin/bash'
Argument: `-a           -b'

Aufruf des Interpreters

load_script() soll letztendlich den Pfad für die auszuführende Binary auf den des Interpreters setzen und argv[1] auf den Namen des Skriptes. Wird ein Argument an den Interpreter übergeben, dann ist argv[1] das Argument und argv[2] der Skriptname.

Das heißt, wenn in einem Skript "#!/bin/awk -f" steht, dann würde also in Wirklichkeit "/bin/awk" mit den Parametern "-f" und "<Skriptname>" aufgerufen werden.

Dazu muss natürlich, um zu bestimmen, wie der Interpreter selbst funktioniert, rekursiv dessen Typ bestimmt werden. load_script() endet also mit einem weiteren search_binary_handler()-Aufruf. Endlos funktioniert diese Rekursion natürlich nicht, sondern nur bis BINPRM_MAX_RECURSION, was in include/linux/binfmts.h als 4 definiert ist.

In letzter Konsequenz heißt das, dass auch Skripte selbst als Interpreter für andere Skripte genutzt werden können. Wobei sie natürlich keine eigentlichen Interpreter sein müssen, sondern alles mögliche machen können.

Außerdem heißt das, dass kein Interpreter jemals direkt mitbekommt, dass er über eine Shebang gestartet wurde. Er muss nur der Konvention folgen und eine Datei als Argument entgegennehmen können, um diese dann auszuführen. Und selbst dann, wenn er das nicht könnte, so wäre es möglich, ein Shell-Skript als Wrapper zu schreiben, was die nötige Funktionalität nachrüstet.

Ein solches Verschachteln von Shebangs ist aber erst seit git-Commit bf2a9a3 verfügbar, was laut "git describe --contains bf2a9a3" dann spätestens in 2.6.28 "öffentlich" wurde.

Wo steht das im Quelltext?

Die relevanten Quellen gibt es an gewohnter Stelle:

In der glibc steht die exec()-Familie in posix/exec*.c. Alle laufen auf __execve() hinaus, dieses steht dann für Linux in sysdeps/unix/sysv/linux/execve.c. Dort sieht man, dass es ein Syscall ist.

Dankbarerweise ist das erstmal ein Präprozessor-Makro namens INLINE_SYSCALL, das in sysdeps/unix/sysv/linux/i386/sysdep.h definiert ist. Dort steht dann ein Assembler-Snippet, was den Sprung in den Kernel durchführt.

Im Kernel wird der Call aufgefangen, es wird in der sys_call_table nachgeschaut und man landet in sys_execve(). Dort geht es über do_execve() zu search_binary_handler(). Dieses klappert die Liste ab und man landet, sofern eine Shebang drin ist, in load_script().

Nochmal ein Überblick.

Grob:

programm -> glibc -> linux

glibc:

exec*                  >     posix/exec*.c
  |
__execve               >     sysdeps/unix/sysv/linux/execve.c
  |
call                   >     sysdeps/unix/sysv/linux/i386/sysdep.h

Im Kernel:

entry                  >     arch/*/kernel/entry*.S
  |
sys_call_table         \     arch/*/kernel/syscall_table_32.S
  |                    /     arch/um/sys-i386/sys_call_table.S
  |
sys_execve             >     arch/*/kernel/process.c
  |
do_execve              \     fs/exec.c
search_binary_handler  /
  |
load_script            >     fs/binfmt_script.c

Referenzen