lunedì 24 aprile 2017

Hello World! ridotto all'osso

Per quanto stimolante possa essere un Hello World l'impressione è sempre quella che si tratti di un'applicazione banale. Per rendere le cose un po' più interessanti si potrebbe alzare il livello di sfida e cercare, ad esempio, di creare il più piccolo Hello World in termini di spazio occupato su disco. Si potrebbe scoprire che, alla fine, anche un semplice programma come questo possa in realtà nascondere una grande quantità di dettagli implementativi celati agli occhi del programmatore. L'ambiente di riferimento usato per questo articolo è quello Unix-like ed il formato degli eseguibili è quello ELF.

#include <stdio.h>
 
int main(void)
{
 printf("Hello, world!\n");
 return 0;
}


Per compilare e linkare:
gcc hello.c -o hello

Risultato:
wc hello
8600 hello

8600 byte, 8,6 Kb. Non moltissimo ma per un programma che semplicemente stampa a schermo una riga di 12 caratteri non è neanche poco.
Di fatto, per stampare questa semplice riga il programma usa printf() e il linker deve linkare libc, la libreria standard del C. Ed è questo che appesantisce il file su disco.

Invece di usare la libreria standard si può usare direttamente una chiamata di sistema per scrivere sull'output di default.

#include <unistd.h>
 
int main(void)
{
 write(1, "Hello, world!\n", 14);
 return 0;
}

Risultato:
wc hello
8608 hello

La dimensione è addirittura aumentata. Il problema è che libc viene linkata di default. Per evitare questa inclusione implicita da parte del linker si può usare
gcc -nostdlib hello.c -o hello 

Risultato:
/usr/bin/ld: attenzione: impossibile trovare il simbolo d'ingresso _start; ripristinato il predefinito 0000000000400144
/tmp/ccHmXQRm.o: nella funzione "main":
hello.c:(.text+0x14): riferimento non definito a "write"
collect2: error: ld returned 1 exit status


In questo caso ci sono due problemi. Il primo è che ld, il linker, non ha trovato il punto d'ingresso dell'applicazione. Il secondo è che c'è un riferimento non definito a write().

Per quanto riguarda il primo problema, il fatto è che la funzione main() è il punto di ingresso dal punto di vista del programmatore e del compilatore, non del linker. Per quest'ultimo il punto d'ingresso è la funzione _start(), definita in crt1.o (il codice di runtime del C). La funzione _start(), a sua volta, dopo varie inizializzazioni, chiama la nostra main() che, alla fine chiama exit() che termina il processo. Ora, il problema è che la funzione _start() e la chiamata alla main() richiedono la partecipazione della libreria standard e del codice di runtime del C. Usando -nostdlib l'effetto è quello di rimuoverle completamente insieme alle loro funzionalità. Si potrebbe usare -nodefaultlibs che aggiunge solo crt1.o ed evita la libreria standard in fase di linking. A quel punto, però, risulterebbero altri simboli non definiti poiché esiste una forte dipendenza tra la libreria standard e il codice di runtime del C. Quindi, per risolvere, si potrebbe "ingannare" il linker e rinominare la nostra main in _start, così da definire il simbolo per il linker evitando dipendenze esterne. Alla fine quello che ci interessa è solo che il linker trovi il suo punto di ingresso. In questo modo, però, si tratterebbe della nostra _start(), non di quella generata dal compilatore e che richiede la presenza del codice di runtime del C. Non essendoci una main() da chiamare, la nostra _start() dovrà solo preoccuparsi di chiamare exit() per terminare il processo. Vedremo perché e come risolvere quest'ultimo aspetto dopo aver risolto il secondo problema. Per il momento il codice, anche se non funzionante, diventa

#include <unistd.h>
 
_start
{
  write(1, "Hello, world!\n", 14);
  return 0;
}

Per quanto riguarda il secondo problema invece, il fatto è che tutte le chiamate di sistema come open(), read(), write(), ecc. che si usano normalmente includendo l'header unisdt.h in realtà non sono vere chiamate di sistema. Sono wrapper usate da libc per chiamare quelle reali. Per aggirare questo problema, ma anche il primo visto precedentemente, basta usare del codice assembly inline. In questo modo si può chiamare la reale chiamata di sistema.

// Non è più necessario includere alcun header. Si effettuano solo due chiamate di sistema
_start()
{
 const char hello[14] = "Hello world!\n";
 unsigned int fd_out = 1;
 unsigned int count = 14;
 
 __asm__ __volatile__("movq $1, %%rax\t\n"
  "syscall\n\t" // Chiama write (1 in rax) con fd = 1 (output di default)
  "movq $60, %%rax\t\n"
  "syscall\n\t" // Chiama exit (60 in rax)// Nessun output"D" (fd_out), "S" (hello), "d" (count)); // Input: fd in rdi, stringa in rsi e numero di caratteri in rdx
}

I problemi riguardati la nostra _start() vengono risolti invocando direttamente la chiamata di sistema exit, che termina il processo immediatamente. Senza di essa l'esecuzione del programma provocherebbe un Segmentation Fault alla fine della funzione _start(). Questo perchè abbiamo definito _start() come fosse la nostra main() e per questo occuperà il primo stack frame sullo stack. Quando lo stack frame di _start() viene "rimosso" al ritorno da _start(), lo stack stesso ed i registri si troveranno in una situazione definita ma iniziale, non più utilizzabile a ritroso. Nel senso che all'inizio dello stack non c'è un valore di ritorno verso cui andare. Allo stesso tempo non c'è alcun modo di riconoscere l'inizio dello stack con valori sentinella. La macchina continuerà a scalare lo stack per recuperare indirizzi di ritorno.

Risultato:
wc hello
1568 hello

1568 byte, 1.5 Kb. Non male ma si può fare ancora meglio eliminando alcune tabelle dall'eseguibile (symbol e section header table, le rispettive string table, più varie info di debugging) con
strip hello

Risultato:
wc hello
1064 hello 

1064 byte, 1 Kb. Si può fare certamente di meglio [1]. Tuttavia, già così non è male se si tiene conto che si è partiti da 8,6 Kb.


Riferimenti:
[1] http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html
[2] https://blogs.oracle.com/ksplice/hello-from-a-libc-free-world-part-1
[3] https://blog.packagecloud.io/eng/2016/04/05/the-definitive-guide-to-linux-system-calls/
[4] https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-r252.pdf
[5] https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
[6] http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/
[7] The Linux Programming interface - Kerrisc
[8] Beginning Linux Programming - Matthew, Stones
[9] The Definitive Guide to GCC - von Hagen

Nessun commento:

Posta un commento