blog · git · desktop · images · contact & privacy · gopher
2010-04-15
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. :)
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.
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-ParsingLinux 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'
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.
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