Die klassische Unix-Rechteverwaltung dürfte den meisten ein Begriff sein. Es gibt drei Arten von Zugriffsrechten: read, write und execute. Für jede Datei sind diese Rechte jeweils für den User, die Group und Other gesetzt. Macht insgesammt 9 Bits, es gibt jedoch noch drei weitere Bits, nämlich für setuid, setgid und das sticky bit. Die Bedeutung dieser zusätzlichen Rechte hängt teilweise davon ab, um was für eine Art Datei es sich handelt.
setuid
Wenn das setuid-Bit für Executables gesetzt ist, dann wird das Programm mit den Rechten des Dateieigentümers ausgeführt. Dies ist zum Beispiel nötig für su
, welches Root-Rechte für seine Funkion benötigt.
setgid
Bei Executables bewirkt das setgid-Bit, ähnlich wie bei setuid, dass Programme die Gruppenrechte des Eigentümers beim Ausführen erhalten.
Das setgid-Bit kann jedoch auch für Verzeichnisse gesetzt werden. Dies bewirkt, dass Dateien, die in diesem Verzeichnis erstellt werden, die Gruppe des Verzeichnisses erhalten, und nicht die Gruppe des Benutzers, der die Datei erstellt.
Sticky Bit
Das Sticky Bit kann auf Verzeichnissen angewendet werden und bewirkt bei diesen, dass die dort enthaltenen Dateien nur von ihrem Besitzer gelöscht werden können. Dies ist sinnvoll bei Verzeichnissen, auf die mehrere Benutzer vollen Zugriff haben, denn ohne Sticky Bit kann jeder enthaltene Dateien löschen.
Beispiel: chmod und ls
Erstellen wir kurz eine Datei:
$ touch file
$ ls -l
total 0
-rw-r--r-- 1 olaf user 0 Apr 22 12:51 file
setuid:
$ chmod u+s file
$ ls -l
total 0
-rwSr--r-- 1 olaf user 0 Apr 22 12:51 file
setgid:
$ chmod g+s file
$ ls -l
total 0
-rwSr-Sr-- 1 olaf user 0 Apr 22 12:51 file
Sticky Bit:
$ mkdir dir
$ ls -l
total 4
drwxr-xr-x 2 olaf user 4096 Apr 22 12:53 dir
-rwSr-Sr-- 1 olaf user 0 Apr 22 12:51 file
$ chmod +t dir
$ ls -l
total 4
drwxr-xr-t 2 olaf user 4096 Apr 22 12:53 dir
-rwSr-Sr-- 1 olaf user 0 Apr 22 12:51 file
Wer sich C schon mal angeschaut hat, der weiß, dass es dort keine Überladung von Funktionen gibt. Unterschiedliche Deklarationen von Funktionen mit dem selben Namen sind nicht erlaubt.
void myfunction(int i);
void myfunction(char *s); /* geht nicht */
void myfunction(int a, int b); /* geht erst recht nicht */
Je nach dem, was man vorhat, gibt es allerdings ein paar Tricks, um doch noch ans Ziel zu kommen.
In meinem Fall wollte ich Funktionen in zwei Varianten, die eine dieser Structs als Argument erhält:
typedef struct {
char *ptr;
size_t length;
} sstr_t;
/* new struct for const strings */
typedef struct {
const char *ptr;
size_t length;
} scstr_t;
Wie man sieht, sind beide Structs sehr ähnlich. Mein Plan war, dass diverse alte Funktionen, die bisher sstr_t
-Parameter erwartet haben, dann mit beiden Structs funktionieren. Da man Structs (ohne Pointer) nicht in andere casten kann, konnte ich nicht einfache Wrapper-Makros für die Funktionen schreiben, die einfach einen Cast enthalten.
Das ganze brauche ich, um die String-API von UCX zu verbessern. Es soll auch eine eine Struct für konstante Strings geben, die dann auch benutzt werden kann, ohne die Quellcodekompatiblität groß zu brechen. Dabei habe ich mehrere Lösungen gefunden:
C11 _Generic
Seit C11 gibt es das _Generic
Keyword für generische Ausdrücke. Damit können zur Compile-Time je nach Typ eines Arguments unterschiedliche Ausdrücke ausgewählt werden. Zum Beispiel:
void print_int(int i);
void print_string(char *s);
#define print(x) _Generic(x, int: print_int, char *: print_string)(x)
Benutzt man das Makro print
nun mit einem int
, wird die Funktion print_int
ausgeführt. Bei einem char *
wird print_string
verwendet.
Was übrigens nicht geht ist folgendes:
#define print(x) _Generic(x, int: print_int(x), char *: print_string(x))
gcc/clang builtins
Gcc hat ein paar nützliche Builtins, mit denen man das Gleiche erreichen kann wie mit dem C11 _Generic. Praktischerweise unterstützt clang diese Builtins auch. Trotzdem verlässt man damit natürlich den Pfad der C-Standardkonformität.
Die beiden Builtins, mit denen man _Generics nachbauen kann, sind:
type __builtin_choose_expr (const_exp, exp1, exp2)
int __builtin_types_compatible_p (type1, type2)
Mit __builtin_types_compatible_p
prüfen wir, ob der Typ kompatibel ist, und nehmen dies als Bedingung um eine Expression mit __builtin_choose_expr
auszuwählen.
#define print(x) __builtin_choose_expr(__builtin_types_compatible_p(x, int), print_int, print_string)(x)
Spracherweiterungen: Statement Expressions und typeof
Statement Expressions sind eine C Erweiterung, die von einigen Compilern, unter anderem gcc, clang, aber auch anderen, unterstützt werden. Damit kann ein Compound-Statement wie eine Expression funktionieren. Die Anweisungen müssen nur mit ({ })
eingeschlossen werden. Dies kann dann überall stehen, wo Expressions erlaubt sind, also z.B. auch als Funktionsparameter.
Allgemein sowas wie _Generic nachbauen geht mit Statement Expressions nicht. Mein ursprüngliches Problem mit den zwei ähnlichen Structs hingegen lässt sich lösen. Statt jeweils eine Funktion für jeden Typ zu implementieren, reicht eine einzelne Funktion, die über ein Makro aufgerufen wird. Das Makro enthält dann eine Statement Expression, die sstr_t oder scstr_t in den richtigen Typ umwandelt.
Zum Einsatz kommt dabei auch typeof
, was wieder eine Erweiterung ist, die auch von mehreren Compilern unterstützt wird. Hiermit kann eine Variable mit dem selben Typ wie eine andere deklariert werden.
void print_s(scstr_t s);
#define print(x) print_s( ({ \
typeof(x) tmp = x; \
scstr_t arg = { tmp.ptr, tmp.length }; \
arg; \
}) )
Leere Parameterliste
Deklariert man in C eine Funktion ohne Angabe irgendeines Parameters, dann heißt dies nicht, das keine Parameter übergeben werden können, sondern dass der Compiler beliebige Parameter erlaubt:
void func();
void voidfunc(void);
int main(void) {
func(1);
func(1, 2, 3);
func("string");
func();
voidfunc(); // no arguments allowed
return 0;
}
Ähnlich wie beim Beispiel mit den Statement Expressions kann ich damit ein Makro für meine Funktion bauen, welches mit Hilfe einer Funktion das sstr_t
oder scstr_t
Argument umwandelt. Die Umwandlungsfunktion wird mit leerer Argumentliste deklariert und implementiert wird sie folgendermaßen:
scstr_t convert2scstr(sstr_t s) {
scstr_t ret;
ret.ptr = s.ptr;
ret.length = s.length;
return ret;
}
Dies funktioniert logischerweise nur, weil die beiden Structs gleich groß sind und einen gleichen Aufbau haben. Der Unterschied zwischen char *
und const char*
ist in diesem Fall egal. Sieht etwas frickelig aus, ist aber standardkonform und kann als Fallback-Lösung für alte oder unbekannte Compiler benutzt werden.
Insgesammt also genug Lösungsansätze, um sowohl alte Compiler zu unterstützen, aber bei neueren Compilern das ganze etwas sauberer umzusetzen.
Alt, aber immer noch lustig.
Apple is like a ship with a hole in the bottom, leaking water, and my job is to get the ship pointed in the right direction.
Der Spruch stammt ursprünglich von Gil Amelio, der CEO von Apple war, bevor Steve Jobs 1997 wieder zurückkehrte.
Ebenfalls lustig, oder aber ziemlich traurig, sind einige Kommentare auf Youtube dazu:
when your rich everything seems to be funny!
Im not rich enough to understand the joke :(
get it? it's funny because they're rich
Neben Java bzw JVM-Bytecode unterstützt GraalVM auch die Ausführung von LLVM-Bitcode.
$ cat > test.c
#include <stdio.h>
int main(void) {
printf("Hello World!\n");
return 0;
}
$ clang -c -emit-llvm test.c
$ $GRAALVM/bin/lli test.bc
Hello World!
Schon mal interessant, lustig wird es aber erst, wenn man dies mit Java kombiniert. Mit der Polyglot-API kann die Bitcode-Datei geladen, und einzelne Funktionen daraus ausgeführt werden.
import org.graalvm.polyglot.*;
import java.io.*;
public class HelloPolyglot {
public static void main(String[] args) {
try {
// create a polyglot context that can access the native interface
Context ctx = Context.newBuilder().allowAllAccess(true).build();
// load an llvm bitcode file
File file = new File("test.bc");
Source source = Source.newBuilder("llvm", file).build();
Value c = ctx.eval(source);
// get the main function and execute it
c.getMember("main").execute();
} catch (IOException e) {
System.err.println(e);
}
}
}
Ich hab auch ein etwas größeres Beispielprogramm.
Links:
GraalVM kann aus Java-Bytecode ein natives Image, also nativen Maschinencode, erstellen. Damit erhält man ein normales Executable, das keine Abhängigkeit zur JVM hat, deutlich schneller startet und einen geringeren Ressourcenverbrauch hat.
Zum Test erstellen wir fix ein einfaches Java Hello World.
Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Manifest.txt
Manifest-version: 1.0
Main-Class: Hello
Java-Code kompilieren und jar erstellen:
$ javac Hello.java
$ jar cfm Hello.jar Manifest.txt Hello.java
$ java -jar Hello.jar
Hello World!
Das native Image kann mit dem GraalVM-Tool native-image
erstellt werden:
$ native-image -jar Hello.jar
Build on Server(pid: 8912, port: 26682)
classlist: 350.53 ms
(cap): 605.69 ms
setup: 856.57 ms
(typeflow): 4,224.92 ms
(objects): 1,103.80 ms
(features): 29.86 ms
analysis: 5,452.54 ms
universe: 195.50 ms
(parse): 917.86 ms
(inline): 783.22 ms
(compile): 5,169.52 ms
compile: 7,097.16 ms
image: 437.47 ms
write: 115.94 ms
[total]: 14,536.87 ms
$ ./Hello
Hello World
Die erzeugte Executable ist mit 5,4 Mb nicht gerade klein, enthält aber auch alle verwendeten Teile der Java Runtime schon precompiled.
Die schlechte Nachricht ist, dass dies nicht mit allem funktioniert. Einige Klassen können offenbar nicht kompiliert werden. Bei meinem Versuch ein einfaches Swing-Beispiel zu kompilieren, wurde ich darauf hingewiesen, dass sun.misc.URLClassPath$JarLoader
unsupported ist:
Build on Server(pid: 9251, port: 26682)*
classlist: 1,526.88 ms
(cap): 990.22 ms
setup: 2,164.54 ms
analysis: 19,376.53 ms
error: unsupported features in 3 methods
Detailed message:
Error: Must not have a started Thread in the image heap.
Trace: object sun.java2d.opengl.OGLRenderQueue
field sun.java2d.opengl.OGLRenderQueue.theInstance
Error: Unsupported type sun.misc.URLClassPath$JarLoader is reachable
...
Ergibt ja auch Sinn, dass ein JarLoader nicht mit reinem nativen Code ohne JVM funktioniert. Wer also hofft seine lieblings IDE, die dank Java fett und träge ist, zu einem schnellen nativen Kompilat umzuwandeln, der wird leider enttäuscht (und wehe mir sagt jemand, dass Visual Studio oder XCode gar nicht Java verwenden).