Wie funktioniert dieses C-Programm ohne libc?

Lesezeit: 4 Minuten

Benutzer-Avatar
qwr

Ich bin auf einen minimalen HTTP-Server gestoßen, der ohne libc geschrieben ist: https://github.com/Francesco149/nolibc-httpd

Ich kann sehen, dass grundlegende String-Handling-Funktionen definiert sind, was zu dem führt write Systemaufruf:

#define fprint(fd, s) write(fd, s, strlen(s))
#define fprintn(fd, s, n) write(fd, s, n)
#define fprintl(fd, s) fprintn(fd, s, sizeof(s) - 1)
#define fprintln(fd, s) fprintl(fd, s "\n")
#define print(s) fprint(1, s)
#define printn(s, n) fprintn(1, s, n)
#define printl(s) fprintl(1, s)
#define println(s) fprintln(1, s)

Und die grundlegenden Systemaufrufe sind in der C-Datei deklariert:

size_t read(int fd, void *buf, size_t nbyte);
ssize_t write(int fd, const void *buf, size_t nbyte);
int open(const char *path, int flags);
int close(int fd);
int socket(int domain, int type, int protocol);
int accept(int socket, sockaddr_in_t *restrict address,
           socklen_t *restrict address_len);
int shutdown(int socket, int how);
int bind(int socket, const sockaddr_in_t *address, socklen_t address_len);
int listen(int socket, int backlog);
int setsockopt(int socket, int level, int option_name, const void *option_value,
               socklen_t option_len);
int fork();
void exit(int status);

Ich schätze also, die Magie passiert in start.Swas beinhaltet _start und eine spezielle Methode zum Codieren von Systemaufrufen durch Erstellen globaler Labels, die durchfallen, und Ansammeln von Werten in r9, um Bytes zu sparen:

.intel_syntax noprefix

/* functions: rdi, rsi, rdx, rcx, r8, r9 */
/*  syscalls: rdi, rsi, rdx, r10, r8, r9 */
/*                           ^^^         */
/* stack grows from a high address to a low address */

#define c(x, n) \
.global x; \
x:; \
  add r9,n

c(exit, 3)       /* 60 */
c(fork, 3)       /* 57 */
c(setsockopt, 4) /* 54 */
c(listen, 1)     /* 50 */
c(bind, 1)       /* 49 */
c(shutdown, 5)   /* 48 */
c(accept, 2)     /* 43 */
c(socket, 38)    /* 41 */
c(close, 1)      /* 03 */
c(open, 1)       /* 02 */
c(write, 1)      /* 01 */
.global read     /* 00 */
read:
  mov r10,rcx
  mov rax,r9
  xor r9,r9
  syscall
  ret

.global _start
_start:
  xor rbp,rbp
  xor r9,r9
  pop rdi     /* argc */
  mov rsi,rsp /* argv */
  call main
  call exit

Ist dieses Verständnis richtig? GCC verwendet die in definierten Symbole start.S für die Systemaufrufe, dann startet das Programm in _start und ruft main aus der C-Datei?

Auch wie funktioniert das trennen httpd.asm benutzerdefinierte binäre Arbeit? Nur eine handoptimierte Bestückung, die C-Quelle kombiniert und die Bestückung startet?

  • Übrigens, mit clang -Oz, habe ich die .c + .S-Version auf 992 Bytes runterbekommen. Siehe oben in meiner Antwort.

    – Peter Cordes

    30. März 2021 um 4:59 Uhr

  • In dem httpd.asm Datei, die mediocrevegetable1 gefunden hat, sind die Systemaufrufe unten _startund es könnte einfach hineinfallen _exitgenannt ?_017 dort, aber es gibt eine call ?_017 Anweisung kurz vor dem ?_017 Etikett. Es scheint wahrscheinlich, dass sie sich nur darauf verlassen, dass GCC R9 nicht verwendet, und dies hoffentlich während ihrer Handoptimierung überprüfen.

    – Rossgrat

    29. März 2021 um 18:30 Uhr

  • Wäre der Code im Allgemeinen kleiner, wenn sie 32-Bit-Abi verwenden würden? Ich denke, als ich vor einiger Zeit meine Code-Golf-Antworten geschrieben habe, habe ich das immer verwendet. Vielleicht sollte das eine Antwort auf der x86-Golftipps-Seite sein.

    – qwr

    29. März 2021 um 20:52 Uhr

  • @qwr: vielleicht, besonders bei vom Compiler generiertem Code, der nicht darauf achtet, REX-Präfixe wo immer möglich zu vermeiden.

    – Peter Cordes

    29. März 2021 um 20:54 Uhr

  • Gibt es nicht ungefähr 20+ Bytes, die gewonnen werden können, wenn man einfach die Funktionsprototypen fallen lässt? Wenn Sie sie alle ohne Spezifikation deklariert haben, könnten Sie ein zusätzliches Argument übergeben … dann kann der Mechanismus den Alias ​​erhöhen, da er nur die Argumente zählt.

    – lk

    30. März 2021 um 15:52 Uhr

  • @lk: Ich kann dem, was du sagst, überhaupt nicht folgen. Ohne Prototypen müsste jeder Funktionsaufruf EAX auf Null setzen, da er 0 Argumente in XMM-Regs übergibt. Und alles andere wäre gleich, außer implizit int Rückgabewerte. (Aber sie kehren übrigens alle signiert zurück ssize_t ist definiert). Wenn Sie möchten, dass jede Call-Site r8 oder r9 explizit auf Null setzt, wäre das im Vergleich zu dem, was jetzt passiert, mehr zusätzliche Codegröße.

    – Peter Cordes

    30. März 2021 um 15:56 Uhr


  • Sieht nicht aus wie eine von Hand geschriebene Montage. Eher wie eine handgestimmte Demontage. Erstens sind die Beschriftungen nur Zahlen anstelle von lesbaren Namen. Dann einige seltsame mnemonische Entscheidungen wie jnz Anstatt von jne nach cmp.

    – Ruslan

    29. März 2021 um 17:59 Uhr


  • @Ruslan stimmt, jetzt wo ich darüber nachdenke. Ich frage mich, was diese Baugruppe dann produziert haben könnte, es sieht aus wie NASM im Gegensatz zu dem, was wie GAS in start.S aussieht, also frage ich mich, was das produziert haben könnte. Wie auch immer, letzte Zeile des Beitrags entfernt.

    – mittelmäßig angebautes Gemüse1

    29. März 2021 um 18:34 Uhr

  • ?_033: Etiketten sehen aus wie der Stil von Agner Fog objconv (was die NASM-Syntax für die Ausgabe unterstützt).

    – Ruslan

    29. März 2021 um 19:03 Uhr

  • @Ruslan ah ok, das macht Sinn.

    – mittelmäßig angebautes Gemüse1

    29. März 2021 um 19:04 Uhr

  • könnte das Programm nicht auch einfach rax statt r9 erhöht haben? nicht sicher, warum sie r9 verwendet haben

    – qwr

    29. März 2021 um 21:01 Uhr

1366900cookie-checkWie funktioniert dieses C-Programm ohne libc?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy