El objetivo que nos proponemos aquí es mostrar esta interacción compilador-sistema operativo que nos permite vislumbrar la complejidad subyacente a la ejecución de un programa. Como debemos centrarnos en algún compilador concreto, veremos el compilador de C de GCC.

            Para ellos, vamos a ver los siguientes elementos:

§         Compilación de programas

§         El interior del programa “Hola Mundo!”

§         Comunicación con el núcleo del sistema operativo

 

 

- - ¨¨¨ - -

 

 

*    Compilación de programas

 

La compilación de un programa puede involucrar los siguientes pasos y generar diferentes archivos (ver Figura 1):        

-    Preprocesado – se procesan los archivos de cabecera .h.

-    Compilación   se genera código ensamblador

-    Ensamblado   se genera un código objeto

-    Enlazado         se unen todos los archivos objeto que forman nuestro programa para generar el ejecutable (ver Figura 2).

Cuadro de texto:
 


Figura 1.-

Compilación

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Cuadro de texto:  









Figura 2.-

Enlazado

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

En la Figura 2 aparecen algunos archivos sobre los que volveremos después. Citaremos ahora crt0.o que es el run-time del lenguaje C cuyas funciones son inicializar el archivo ejecutable (puntero a pila y bss), definir los símbolos especiales como _start, e invocar a main(). También, encontramos ld que es el cargador.

 

Cuadro de texto:  Figura 3.-

Formato ELF

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

GCC – compilación

 

GCC = compilador C de GNU

gcc = wrapper

 

Veremos algunas opciones de gcc y su resultado en la compilación. Explicaremos con cierto detalle que ocurre durante el proceso de compilación.

 

Archivos de cabecera

 

Vamos a partir de un sencillo ejemplo:

% cat archi.c

#include “archi.h”

main()

{

     printf(“hola … %d\n”, VAR);

}

% cat archi.h

#define VAR 100

 

Cuando pasamos un archivo a gcc, este pasa por cuatro etapas. La primera de ellas el preprocesado. El preprocesador examina los archivos .c en busca de declaraciones que comiencen por #. Cuando son de tipo #include archivo, localiza el archivo y lo añade al programa.

A continuación, el preprocesador busca y sustituye todas las macros (#define). Una vez realizadas todas las sustituciones se invoca al compilador. Ojo: el compilador no ve el archivo .c original si no la suma del .c y .h con la macros sustituidas. Esto puede producir algunos problemas serios si el archivo de cabecera contiene errores. Si tenemos macros que se comportan como funciones, si existe algún  error en una de ellas, puede generar bastante confusión. El compilador no sabe nada sobre cabeceras, por tanto cuando ve un error, informa del  número de línea que él ve. Cuando comprobamos el .c original, podemos ver una línea completamente inocente.

 

% gcc –save-temps archi.c

% ls –l

total 44

-rwxr-xr-x    1 root     root        13640 abr 21 20:28 a.out

-rw-r--r--    1 root     root           62 abr 21 20:28 archi.c

-rw-r--r--    1 root     root           17 abr 21 20:27 archi.h

-rw-r--r--    1 root     root           95 abr 21 20:28 archi.i

-rw-r--r--    1 root     root          912 abr 21 20:28 archi.o

-rw-r--r--    1 root     root          376 abr 21 20:28 archi.s

 

El archivo generado por el preprocesador:

%cat archi.i

# 1 "archi.c"

# 1 "archi.h" 1

# 2 "archi.c" 2

 

main()

{

        printf("Hola ...%d\n", 100);

}

 

 

Todemos ver el código en ensamblador:

%cat archi.s

 

     .file "archi.c"

     .version  "01.01"

gcc2_compiled.:

          .section  .rodata

.LC0:

     .string   "Hola ...%d\n"

.text

     .align 4

.globl main

     .type main,@function

main:

     pushl %ebp

     movl %esp, %ebp

     subl $8, %esp

     subl $8, %esp

     pushl $100

     pushl $.LC0

     call printf

     addl $16, %esp

     leave

     ret

.Lfe1:

     .size main,.Lfe1-main

     .ident    "GCC: (GNU) 2.96 20000731 (Red Hat Linux 7.1 2.96-81)"

 

El programa gcc es lo que llamamos “controlador” (wrapper), no hace nada por si mismo, invoca a otros programas uno tras otro y les pasa los parámetros correctos. Por qué hacerlo así, en lugar de llamar separadamente a cada programa. Una razón es la conveniencia, la otra portabilidad. Gcc nos suministra un gran número de conmutadores transportables que afectan al comportamiento de los programas llamados. Hacerlo así nos asegura que nuestro programa compilará correctamente en varias plataformas.

 

Comenzaremos trabajando hacia atrás. Pasemos a examinar el enlazador.

%gcc –c archi.c

La opción –c indica que no se realice el enlazado. Por lo que su trabajo acaba produciendo el archivo archi.o. Para realizar el paso final debemos enlazar el archivo:

 

% ld archi.o –o archi

ld: warning: cannot find entry symbol _start; defaulting to 08048074

archi.o: In function `main’:

archi.o(.text+0x11): undefined reference to `printf’

 

Pero, ¿de dónde viene el error? Hemos utilizado la función printf de la biblioteca libc.so del directorio /usr/lib. Por tanto, debemos instruir al enlazador para que localice el código:

% ld archi.o –o archi /usr/lib/libc.so

ó

% ld archi.o –o archi –lc

ld: warning: cannot find entry symbol _start; defaulting to 08048184

 

Dejemos a un lado el aviso de momento e intentemos ejecutar nuestro programa. El shell se quejará indicándonos de que no encuentra el archivo (aunque este existe). En realidad no hay ningun problema con el shell. Este es una interfaz con el sistema y no sabe como cargar y ejecutar ejecutables. Necesita los servicios de cargador de archivos ELF, ld-linux.so.2. Al enlazador le debemos indicar que cargador añadir al ejecutable cuando se enlaza. Por tanto, enlazaremos nuestro programa:

 

% ld archi.o –o archi –lc ld archi.o –o archi –lc-dymamic-linker /lib/ls-linux.so.2

ld: warning: cannot find entry symbol _start; defaulting to 08048184

 

Cuando ejecutamos nuestro programa, main no es el primer fragmento de código que se llama. Se ejecuta primero el código de arranque que es quien invoca al main. Por tanto, debemos indicar a ld donde esta el main. Realmente, la primera línea ejecutable dentro de un programa esta etiquetada como _start, por convenio. Debemos indicar, por consiguiente, a ld que utilice main como el inicio de nuestro programa.

 

% ld –o archi archi.o –lc –e main –dynamic-linker /lib/ld-linux.so.2

% ./archi

Hola … 100

Segmentation fault (core dumped)

 

¡Bueno, persisten los problemas! Hasta ahora nos hemos ocupado del arranque del programa, consideremos su finalización. Debemos añadir a nuestro programa la función exit() para indicar al SO que finalizamos.

 

% cat archi.c

#include “archi.h”

main()

{

     printf(“hola … %d\n”, VAR);

     exit(1);

}

% gcc –c a.c

% ls –o archi archi.o –lc –e main –dymanic-linker /lib/ld-linux.so.2

% ./archi; echo $?

Hola… 100

1

 

Más sobre gcc

 

Posemos ver las opciones que las que invocamos a gcc con:

 

% gcc –v hola.c

Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/2.96/specs

gcc version 2.96 20000731 (Red Hat Linux 7.1 2.96-81)

 /usr/lib/gcc-lib/i386-redhat-linux/2.96/cpp0 -lang-c -v -D__GNUC__=2 -D__GNUC_MINOR__=96 -D__GNUC_PATCHLEVEL__=0 -D__ELF__ -Dunix -Dlinux -D__ELF__ -D__unix__ -D__linux__ -D__unix -D__linux -Asystem(posix) -Acpu(i386) -Amachine(i386) -Di386 -D__i386 -D__i386__ -D__tune_i386__ hola.c /tmp/ccLbIetn.i

GNU CPP version 2.96 20000731 (Red Hat Linux 7.1 2.96-81) (cpplib) (i386 Linux/ELF)

ignoring nonexistent directory "/usr/i386-redhat-linux/include"

#include "..." search starts here:

#include <...> search starts here:

 /usr/local/include

 /usr/lib/gcc-lib/i386-redhat-linux/2.96/include

 /usr/include

End of search list.

 /usr/lib/gcc-lib/i386-redhat-linux/2.96/cc1 /tmp/ccLbIetn.i -quiet -dumpbase hola.c -version -o /tmp/ccQ2g4QC.s

GNU C version 2.96 20000731 (Red Hat Linux 7.1 2.96-81) (i386-redhat-linux) compiled by GNU C version 2.96 20000731 (Red Hat Linux 7.1 2.96-81).

 as -V -Qy -o /tmp/ccXeGPDX.o /tmp/ccQ2g4QC.s

GNU assembler version 2.10.91 (i386-redhat-linux) using BFD version 2.10.91.0.2

 /usr/lib/gcc-lib/i386-redhat-linux/2.96/collect2 -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crt1.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crti.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/crtbegin.o -L/usr/lib/gcc-lib/i386-redhat-linux/2.96 -L/usr/lib/gcc-lib/i386-redhat-linux/2.96/../../.. /tmp/ccXeGPDX.o -lgcc -lc -lgcc /usr/lib/gcc-lib/i386-redhat-linux/2.96/crtend.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crtn.o

 

Vamos a contentar brevemente las cuatro etapas diferentes que se llevan a cabo para producir el ejecutable.

El primer programa invocado es el preprocesador C, cpp. El segundo programa es el propio compilador de C, cc1. A continuación se invoca al ensamblador, as. Y por último, al collect2 que es un wrapper que invoca a ld. Observar que las opciones con las que se invoca a collect2 son las que hemos visto para ld.

 

El preprocesador

 

Ya hemos visto que el preprocesador de C se denomina cpp:

 

% cpp archi.c

bash: cpp: command not found

 

Obviamente, cpp no esta en el camino de búsqueda (path). Podemos localizarlo mediante:

% gcc –print-file-name=cpp

/lib/cpp

 

El compilador

 

% gcc –print-file-name=cc1

/usr/lib/gcc-lib/i386-redhat-linux/egcs-2.96/cc1

La compilación de nuestro programa se llevaría a cabo:

% /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.96/cc1 archi.i

 main

Execution times (seconds)

 parser                :   0.00 ( 0%) usr   0.02 (40%) sys   0.02 (40%) wall

 varconst              :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall

 jump                  :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall

 flow analysis         :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall

 local alloc           :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall

 global alloc          :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall

 flow 2                :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall

 shorten branches      :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall

 reg stack             :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall

 final                 :   0.00 ( 0%) usr   0.01 (20%) sys   0.01 (20%) wall

 symout                :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall

 rest of compilation   :   0.00 ( 0%) usr   0.00 ( 0%) sys   0.00 ( 0%) wall

 TOTAL                 :   0.00             0.05             0.05

 

 

El compilador nos da una lista de estadísticas. En nuestro ejemplo la mayoría de los valores son ceros ya que el programa es muy pequeño. La salida del compilador es archi.s. Los siguientes pasos son ensamblar y enlazar:

 

% as hola.s

% ld –o hola hola.o –e main –l –dynamic-linker /lib/ld-linux.so.2

 

El enlazador

 

Vamos a ver las opciones con las que se invoca al enlazador. Para ello, construimos el siguiente programa.

% cat prog.c

main(int argc, char **argv)

{

int i;

for (i=0; i<argc; i++)

      printf("%s\n", argv[i]);

}

 

Una vez compilador el programa anterior lo vamos a sustituir por el enlazador verdadero y vamos a compilar un programa:

%gcc prog.c

% cp /usr/bin/ld ld.bak

% mv a.out /usr/bin/ld

% gcc a.c

/usr/bin/ld

-m

elf_i386

-dynamic-linker

/lib/ld-linux.so.2

/usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crt1.o

/usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crti.o

/usr/lib/gcc-lib/i386-redhat-linux/2.96/crtbegin.o

-L/usr/lib/gcc-lib/i386-redhat-linux/2.96

-L/usr/lib/gcc-lib/i386-redhat-linux/2.96/../../..

/tmp/cc6i2tvp.o

-lgcc

-lc

-lgcc

/usr/lib/gcc-lib/i386-redhat-linux/2.96/crtend.o

/usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crtn.o

collect2: ld returned 16 exit status

 

La información mostrada revela las opciones que se le pasan al ld. El error que aparece al final obedece a nuestro ld no ha hecho nada de lo que espera collect2. Podemos ver como la opción –m especifica para que máquina vamos a generar el código. También, vemos como se especifica el enlazador dinámico con –dynamic-linker, y crtbegin y crtend contienen el código de inicialización y finalización, respectivamente. –L especifica el directorio donde el enlazador busca los archivos .so.

 

Enlaces de interés:

- “GNU C COMP”, en http://marvin.kset.org/~cardi/filez/linux/chap2.html

- GNU Pro toolkit: http://www.redhat.com/docs/manuals/gnupro/

- Formato ELF: http://stephane.carrez.free.fr/ELF/ch4.intro.html

 

 

- - ¨¨¨ - -

 

 

*    El interior de “Hola Mundo!”

 

Vamos a profundizar en el ciclo de vida de un programa en C, desde su construcción hasta su ejecución, utilizando como ejemplo el conocido programa “Hola Mundo!”.

 

·        El código fuente

 

Comenzamos con el ya conocido código fuente de programa

  1. #include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5.         printf(“Hola Mundo!\n”);
  6.         return 0;
  7. }

 

La línea 1 instruye al compilador para incluir las declaraciones necesarias para invocar printf de la biblioteca C (libc).

La línea 3 declara la función main, que pensamos que es el punto de entrada a nuestro programa (en realidad no lo es, como veremos más tarde). Esta función no tiene parámetros y retorna un entero al proceso padre – normalmente, el shell. El shell toma la convención mediante la cual un proceso hijo debe devolver un número de 8 bits que representa el estado de finalización: 0 para terminaciones normales, 0>n<128 para procesos con terminación anormal, y n>128 para procesos finalizados por señales.

Las líneas 4 a 8 comprenden la definición de la función main que invoca a printf de la biblioteca C para imprimir el mensaje y devuelve un 0 al proceso padre.

 

§         Compilación

 

Veamos el proceso de compilación del programa. Para ello, utilizaremos el compilador gcc de GNU y sus herramientas asociadas (binutils). Podemos compilar el programa como sigue:

% gcc –Os –c hola.c

Esto produce el archivo objeto hola.o. Más concretamente:

% file hola.o

hola.o: ELF 32-bits LSB relocatable, Intel 80386, version 1, not stripped

lo que nos indica que hola.o es un archivo objeto reubicable, compilado para la arquitectura IA-32, almacenado el formato ELF (Executable and Linking Format), que contiene la tabla de símbolo (not stripped).

 

Podemos obtener más información con

 

% objdump –hrt hola.o

hola.o:     file format elf32-i386

 

Sections:

Idx Name          Size      VMA       LMA       File off  Algn

  0 .text         00000014  00000000  00000000  00000034  2**2

                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE

  1 .data         00000000  00000000  00000000  00000048  2**2

                  CONTENTS, ALLOC, LOAD, DATA

  2 .bss          00000000  00000000  00000000  00000048  2**2

                  ALLOC

  3 .note         00000014  00000000  00000000  00000048  2**0

                  CONTENTS, READONLY

  4 .rodata       0000000d  00000000  00000000  0000005c  2**0

                  CONTENTS, ALLOC, LOAD, READONLY, DATA

  5 .comment      00000036  00000000  00000000  00000069  2**0

                  CONTENTS, READONLY

SYMBOL TABLE:

00000000 l    df *ABS*  00000000 hola.c

00000000 l    d  .text  00000000

00000000 l    d  .data  00000000

00000000 l    d  .bss   00000000

00000000 l       .text  00000000 gcc2_compiled.

00000000 l    d  .rodata     00000000

00000000 l    d  .note  00000000

00000000 l    d  .comment    00000000

00000000 g     F .text  00000014 main

00000000         *UND*  00000000 printf

 

 

RELOCATION RECORDS FOR [.text]:

OFFSET   TYPE              VALUE

00000007 R_386_32          .rodata

0000000c R_386_PC32        printf

 

El resultado nos indica que hola.o tiene 5 secciones:

1.      .text: contiene el programa compilado, es decir, los opcodes IA-32 correspondientes al programa. Este se utilizara por el programa cargador para inicializar el segmento de código del proceso.

2.      .data: nuestro programa no tiene ni datos globales inicializados ni variables locales estáticas inicializadas, por lo que la sección esta vacía. En otro caso, contendría los valores iniciales de las variables que deben cargarse en el segmento de datos.

3.      .bss: El programa tampoco contiene datos no inicializados, ni globales ni locales, por lo que la sección esta vacía. En otro caso, indicaría cuantos bytes deben asignarse y rellenarse a cero en el segmento de datos que se añade a la sección .data.

4.      .rodata: este segmento contiene la cadena “Hola Mundo!”, que esta etiquetada como solo lectura. La mayoría de los SOs no soportan segmentos de datos de solo-lectura para los procesos, así los contenidos de .rodata van bien al segmento de código (ya que es solo-lectura), o al segmento de datos (dado que son datos). Como el compilador no conoce la política adoptada por nuestro SO, crea esta sección ELF extra.

5.      .comment: este segmento contiene 33 bytes de comentarios que no pueden ser rastreados hacia atrás por nuestro programa, dado que no hemos escrito ningun comentario. Más tarde veremos de donde viene.

También nos muestra una tabla de símbolos con el símbolo main ligado a la dirección 00000000 y printf indefinido. Además, la tabla de reubicación nos dice como reubicar las referencias a secciones externas realizadas en la sección .text. El primer símbolo reubicable corresponde a la cadena “Hola Mundo!” contenida en la sección .rodata. El segundo símbolo reubicable, printf, designa una función de libc que se ha generado como resultado de invocar a printf. Para comprender mejor el contenido de hola.o, vamos a ver el código ensamblador:

% gcc –S hola.c

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

17

18

19

20

21

22

23

     .file "hola.c"

     .version  "01.01"

gcc2_compiled.:

          .section  .rodata

.LC0:

     .string   "Hola Mundo!\n"

.text

     .align 4                                 

.globl main

     .type main,@function

main:

     pushl %ebp

     movl %esp, %ebp

     subl $20, %esp

     pushl $.LC0

     call printf

     xorl %eax, %eax

     leave

     ret

.Lfe1:

     .size main,.Lfe1-main

     .ident    "GCC: (GNU) 2.96 20000731 (Red Hat Linux 7.1 2.96-81)"

 

A la vista del ensamblador, queda claro de donde viene los indicadores de la sección ELF. Por ejemplo, la sección .text esta alineada a 32 bits (línea 8). También, revela de donde viene la sección .comment (línea 23).

En cuanto al código producido, no hay sorpresas: una llamada a la función printf con la cadena direccionada por .LCO como argumento.

 

§         Enlazado

 

Demos el paso de transformar hola.o en un ejecutable. Podríamos pensar que la orden siguiente lo haría:

% ld –o hola hola.o –lc

ld: warning: cannot find entry symbol _start; default to 08048184

pero ¿qué significa este aviso? Intenta ejecutarlo. No funciona. Volviendo al aviso, indica que el enlazador no encuentra el _start, el punto de entrada a nuestro programa. Pero, ¿no es main? La verdad es que main es el punto de entrada de nuestro programa C desde la perspectiva de programador. De hecho, antes de llamar a main, un programa ha ejecutado un fragmento de código para “limpiar la zona de ejecución”. Nosotros obtenemos este código de forma transparente desde el compilador/SO.

Intentemos

% ld –static –o hola –L`gcc –print-file-name= `/usr/lib/crt1.o /usr/lib/crti.o hola.o /usr/lib/crtn.o` -lc –lgcc

 

         Ahora si tenemos un ejecutable real. Hemos utilizado el enlazado estático por dos razones: primero, no deseamos entrar aquí en como funcionan la bibliotecas dinámicas; segundo, el deseo de mostrar la cantidad de código innecesario va dentro del programa “Hola Mundo” debido a la forma en que estas están implementadas. Se hacemos

%find hola.c hola.o hola – printf “%f\t%s\n”

hola.c    ??

hola.o ???

hola      ????

Podemos también, hacer “nm jola” o “objdump –d hola” para hacernos una idea de que se ha enlazado en el ejecutable.

 

§         Carga y ejecución

 

En un sistema operativo POSIX, la carga de un programa para su ejecución se realiza desde un proceso padre que invoca a la llamada al sistema fork para crear un proceso hijo. Esta llamada lo hace es replicar al proceso invocador. Como el hijo comienza siendo una replica de otro proceso, lo que suele hacer es invocar a la llamada al sistema exec para carga y ejecutar el programa deseado. Este procedimiento lo realiza, por ejemplo, por el shell cuando tecleamos una orden. Para ver esto, podemos utilizar strace:

% /usr/bin/strace –i ./hola >/dev/null

upeek: ptrace(PTRACE_PEEKUSER, ... ): No such process

[????????] execve("./hola", ["./hola"], [/* 24 vars */]) = 0

[40011c3d] uname({sys="Linux", node="localhost.localdomain", ...}) = 0

[40012844] brk(0)                       = 0x80495f8

[40012a8d] old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40017000

[40012384] open("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or directory)

[40012384] open("/etc/ld.so.cache", O_RDONLY) = 3

[4001230f] fstat64(3, {st_mode=S_IFREG|0644, st_size=53827, ...}) = 0

[40012a8d] old_mmap(NULL, 53827, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40018000

[400123bd] close(3)                     = 0

[40012384] open("/lib/i686/libc.so.6", O_RDONLY) = 3

[40012404] read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\200\302"..., 1024) = 1024

[4001230f] fstat64(3, {st_mode=S_IFREG|0755, st_size=5634864, ...}) = 0

[40012a8d] old_mmap(NULL, 1242920, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x40026000

[40012b14] mprotect(0x4014c000, 38696, PROT_NONE) = 0

[40012a8d] old_mmap(0x4014c000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x125000) = 0x4014c000

[40012a8d] old_mmap(0x40152000, 14120, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40152000

[400123bd] close(3)                     = 0

[40012ad1] munmap(0x40018000, 53827)    = 0

[400e1017] getpid()                     = 1154

[400ff0c3] fstat64(1, {st_mode=S_IFREG|0644, st_size=1413, ...}) = 0

[40109a43] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40018000

[400fff84] write(1, "Hola Mundo!\n", 12Hola Mundo!

) = 12

[40109a91] munmap(0x40018000, 4096)     = 0

[400e09ad] _exit(0)                     = ?

 

En la salida, hemos destacado en negrita las llamadas al sistema: execve que ejecuta el nuevo programa, el cual invoca a write para realizar la impresión, y exit que pone fin al programa.

Para comprender los detalles que se esconden tras el procedimiento de carga realizado por execve, echemos un vistazo la ejecutable ELF:

% readelf –l hola

Elf file type is EXEC (Executable file)

Entry point 0x8048360

There are 6 program headers, starting at offset 52

 

Program Headers:

  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align

  PHDR           0x000034 0x08048034 0x08048034 0x000c0 0x000c0 R E 0x4

  INTERP         0x0000f4 0x080480f4 0x080480f4 0x00013 0x00013 R   0x1

      [Requesting program interpreter: /lib/ld-linux.so.2]

  LOAD           0x000000 0x08048000 0x08048000 0x004f5 0x004f5 R E 0x1000

  LOAD           0x0004f8 0x080494f8 0x080494f8 0x000e8 0x00100 RW  0x1000

  DYNAMIC        0x000540 0x08049540 0x08049540 0x000a0 0x000a0 RW  0x4

  NOTE           0x000108 0x08048108 0x08048108 0x00020 0x00020 R   0x4

 

 Section to Segment mapping:

  Segment Sections...

   00    

   01     .interp

   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.got .rel.plt .init .plt .text .fini .rodata

   03     .data .eh_frame .ctors .dtors .got .dynamic .bss

   04     .dynamic

   05     .note.ABI-tag

 

La salida muestra la estructura general de hola. La primera cabecera de programa corresponde al segmento de código del proceso, que se cargará del archivo desde el desplazamiento 0x000000 en una región de memoria que será proyectada en el espacio de direcciones del proceso en la dirección 0x08048000. El segmento de código tiene 0x004f5 bytes y debe estar alineado a página (0x1000). Este segmento comprenderá los segmentos ELF .text y .rodata, discutidos anteriormente, más segmentos adicionales generados durante el procedimiento de enlazado. Como esperábamos, esta marcado como sólo lectura( R) y ejecutable (E), pero no escribible (W).

         La segunda cabecera de programa corresponde al segmento de datos. La carga de este segmento sigue los mismos pasos mencionados anteriormente. Sin embargo, observar que la longitud del segmento es de tamaño 0x000e8 sobre el archivo y 0x00100 en memoria. Esto se debe a la sección .bss, que se rellena a ceros y, por tanto, no debe estar presente en el archivo. Este segmento también debe estar alineado a página y contiene los segmentos ELF .data y .bss. Esta marcado como lectura/escritua (RW). La tercera cabecera de programa resulta del procedimiento de enlazado y es irrelevante a nuestra discusión.

         Si disponemos de un sistema de archivos /proc podemos comprobar esto mientras se ejecuta el proceso “Hola Mundo”:

% cat /proc/`ps –C hola – pid h|tr “ “ “\null”/maps

08048000-08049000 r-xp 00000000 08:05 277304     /root/hola

08049000-0804a000 rw-p 00000000 08:05 277304     /root/hola

40000000-40016000 r-xp 00000000 08:05 271030     /lib/ld-2.2.2.so

40016000-40017000 rw-p 00015000 08:05 271030     /lib/ld-2.2.2.so

40017000-40018000 rw-p 00000000 00:00 0

40018000-40019000 rw-p 00000000 00:00 0

40026000-4014c000 r-xp 00000000 08:05 350657     /lib/i686/libc-2.2.2.so

4014c000-40152000 rw-p 00125000 08:05 350657     /lib/i686/libc-2.2.2.so

40152000-40156000 rw-p 00000000 00:00 0

bfffe000-c0000000 rwxp fffff000 00:00 0

 

         La primera región proyectada es el segmento de código del proceso, la segunda y la tercera construyen el segmento de datos (datos + bss + heap), y la cuarta, que no tiene correspondencia en el archivo ELF, es la pila. Se puede obtener información adicional mediante time, ps, y /proc/pid/stat de GNU.

 

§         Terminando

 

Cuando nuestro programa ejecuta la declaración return de la función main, pasa un parámetro a las funciones que lo rodean discutidas en la sección de enlazado. Una de estas funciones invoca a la llamada al sistema exit pasándole como argumento el argumento de exit. La llamada exit devuelve esta valor al padre, que esta bloqueado en la llamada al sistema wait. Además, conduce la terminación ordenada del proceso, devolviendo los recursos al sistema. Este procedimiento puede ser traceado parcialmente mediante:

% strace –e trace=process –f sh –c “./hola; echo $?” >/dev/null

execve("/bin/sh", ["sh", "-c", "./hola;echo 0"], [/* 23 vars */]) = 0

fork()                                  = 1653

[pid  1653] execve("./hola", ["./hola"], [/* 23 vars */]) = 0

Hola Mundo!

[pid  1653] _exit(0)                    = ?

--- SIGCHLD (Child exited) ---

wait4(-1, [WIFEXITED(s) && WEXITSTATUS(s) == 0], WNOHANG, NULL) = 1653

wait4(-1, 0xbffff40c, WNOHANG, NULL)    = -1 ECHILD (No child processes)

0

_exit(0)                                = ?

 

§         Conclusión

 

La intención de este ejercicio es mostrar la atención a los estudiantes de informática el hecho de que un programa no se ejecuta por magia: hay muchísimo software de sistema tras el más simple programa.

 

Referencias:

 

“The True Story of the Hello World”, en http://www.lisha.ufsc.br/~guto/teaching/os/exercise/hello.html

 

 

- - ¨¨¨ - -

 

 

*    Comunicación con el kernel de Linux

 

 

Partamos del programa siguiente:

 

int main() {
  return(123);

 }

 

Si lo compilamos podemos observar que su tamaño en considerable. Para ver de donde viene, podemos examinar la dependencia de las bibliotecas (ldd) y la definición de nombres de símbolos (nm):

$ ldd test0
        libc.so.6 => /lib/libc.so.6 (0x40017000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
 
$ nm test0
08048314 t Letext
0804945c ? _DYNAMIC
08049440 ? _GLOBAL_OFFSET_TABLE_
0804841c R _IO_stdin_used
08049434 ? __CTOR_END__
08049430 ? __CTOR_LIST__
0804943c ? __DTOR_END__
08049438 ? __DTOR_LIST__
0804942c ? __EH_FRAME_BEGIN__
0804942c ? __FRAME_END__
080494fc A __bss_start
08049420 D __data_start
         w __deregister_frame_info@@GLIBC_2.0
080483d0 t __do_global_ctors_aux
08048320 t __do_global_dtors_aux
         w __gmon_start__
         U __libc_start_main@@GLIBC_2.0
         w __register_frame_info@@GLIBC_2.0
080494fc A _edata
08049514 A _end
080483fc ? _fini
         U _fp_hw
08048274 ? _init
080482f0 T _start
08049428 d completed.4
08049420 W data_start
08048370 t fini_dummy
0804942c d force_to_data
0804942c d force_to_data
08048378 t frame_dummy
08048314 t gcc2_compiled.
08048320 t gcc2_compiled.
080483d0 t gcc2_compiled.
080483fc t gcc2_compiled.
080483b0 t gcc2_compiled.
0804839c t init_dummy
080483f4 t init_dummy
080483b0 T main
080494fc b object.11

08049424 d p.3

 

ldd nos indica que nuestro programa depende de libc.so.6 (GNU libc6) y nm nos muestra entre otros símbolos desconocidos por el momento el familar T main de nuestro programa principal.

 

$ gcc -v -o test0 test0.c
Reading specs from /usr/lib/gcc-lib/i386-linux/2.95.2/specs
gcc version 2.95.2 20000220 (Debian GNU/Linux)
 /usr/lib/gcc-lib/i386-linux/2.95.2/cpp -lang-c -v -D__GNUC__=2 -D__GNUC_MINOR__=95 -D__ELF__ -Dunix -D__i386__ -Dlinux
 -D__ELF__ -D__unix__ -D__i386__ -D__linux__ -D__unix -D__linux -Asystem(posix) -Acpu(i386) -Amachine(i386) -Di386 -D__i386
 -D__i386__ test0.c /tmp/cc6P8NZ9.i
GNU CPP version 2.95.2 20000220 (Debian GNU/Linux) (i386 Linux/ELF)
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/include
 /usr/lib/gcc-lib/i386-linux/2.95.2/include
 /usr/include
End of search list.
The following default directories have been omitted from the search path:
 /usr/lib/gcc-lib/i386-linux/2.95.2/../../../../include/g++-3
 /usr/lib/gcc-lib/i386-linux/2.95.2/../../../../i386-linux/include
End of omitted list.
 /usr/lib/gcc-lib/i386-linux/2.95.2/cc1 /tmp/cc6P8NZ9.i -quiet -dumpbase test0.c -version -o /tmp/ccgz1veb.s
GNU C version 2.95.2 20000220 (Debian GNU/Linux) (i386-linux) compiled by GNU C version 2.95.2 20000220 (Debian GNU/Linux).
 as -V -Qy -o /tmp/ccgJ2zye.o /tmp/ccgz1veb.s
GNU assembler version 2.9.5 (i386-linux) using BFD version 2.9.5.0.37
 /usr/lib/gcc-lib/i386-linux/2.95.2/collect2 -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test0 /usr/lib/crt1.o
 /usr/lib/crti.o /usr/lib/gcc-lib/i386-linux/2.95.2/crtbegin.o -L/usr/lib/gcc-lib/i386-linux/2.95.2 /tmp/ccgJ2zye.o
 -lgcc -lc -lgcc /usr/lib/gcc-lib/i386-linux/2.95.2/crtend.o /usr/lib/crtn.o

GCC ejecuta automáticamente varias herramientas. Para ver el la ejecución de fondo especificamos -v. GCC ejecuta los siguientes programas:

1) "cpp ... test0.c /tmp/cc6P8NZ9.i" preprocesa test0.c listado fuente y produce la salida /tmp/cc6P8NZ9.i.

2) "cc1 /tmp/cc6P8NZ9.i ... /tmp/ccgz1veb.s" analiza y compila /tmp/cc6P8NZ9.i y genera  fuente en ensamblador /tmp/ccgz1veb.s.

3) "as ... -o /tmp/ccgJ2zye.o /tmp/ccgz1veb.s" ensambla /tmp/ccgz1veb.s y produce código objeto /tmp/ccgJ2zye.o.

4) Finalmente "collect2 -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o test0 ... crt1.o crti.o crtbegin.o /tmp/ccgJ2zye.o -lgcc -lc crtend.o crtn.o" enlazar el archivo objeto con cinco objectos predefinidos (crtxxx.o; listamos más abajo). El enlazador también referencia las bibliotecas de GCC y C, y crea el archivo ejecutable test0 (formato elf_i386).

-rw-r--r--    1 root     root         1188 May  2  2000 /usr/lib/crt1.o
-rw-r--r--    1 root     root         1096 May  2  2000 /usr/lib/crti.o
-rw-r--r--    1 root     root          827 May  2  2000 /usr/lib/crtn.o
-rw-r--r--    1 root     root         1900 Jun 20  2000 /usr/lib/gcc-lib/i386-linux/2.95.2/crtbegin.o
-rw-r--r--    1 root     root         1408 Jun 20  2000 /usr/lib/gcc-lib/i386-linux/2.95.2/crtend.o
 

Ahora, identificamos la identidad del software hinchado. La sobrecarga viene de los archivos de arranque y las bibliotecas.

 

Independencia de GLIBC

 

En nuestro fuente no hay llamadas a funciones estándar. ¿ necesitamos realmente las bibliotecas? ¿por qué no nos ahorramos la gordura? La opción –nostdlib no enlaza las bibliotecas y ficheros de arranque.

$ gcc -nostdlib -o test0 test0.c
$ ./test0
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 08048080

 

GCC se queja de la no existencia del punto de entrada _start. ELF asume que el punto de entrada inicial es este.¿dónde esta _start?

 

$ nm /usr/lib/crt1.o
00000024 t Letext
00000004 R _IO_stdin_used
00000000 D __data_start
         U __libc_start_main
         U _fini
         U _fp_hw
         U _init
00000000 T _start
00000000 W data_start
         U main

 

Aquí está. Crt1.o contiene el punto de entrada del programa y es el que llama a nuestro famoso main() (la U que aparece es de indefinido). ¿Cómo escapar del problema?

 

$ gcc -c test0.c
$ ls -l test0*
-rw-------    1 root     src            30 Jan  6 20:37 test0.c
-rw-r--r--    1 root     src           745 Jan  6 21:00 test0.o
 
$ ld -e main -o test0 test0.o
$ ls -l test0
-rwxr-xr-x    1 root     src           989 Jan  6 21:00 test0
 
$ ldd test0
        statically linked (ELF)
 
$ nm test0
08049094 A __bss_start
08049094 A _edata
08049094 A _end
08048080 t gcc2_compiled.
08048080 T main

 

 

Podemos observar que cuando generamos test.o su tamaño es de solo 989 bytes. Ldd nos indica que nuestro programa ya no depende de GLIBC. Es un programa independiente (la mayoría de los símbolos han desaparecido).

 

Necesitamos la función exit().

 

Si ejecutamos nuestro programa:

 

$ ./test0

Segmentation fault

 

Para ver la razón del problema, analizaremos nuestro programa con GDB:

 

$ gdb test0
GNU gdb 19990928
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x8048080 <main>:       push   %ebp
0x8048081 <main+1>:     mov    %esp,%ebp
0x8048083 <main+3>:     mov    $0x7b,%eax
0x8048088 <main+8>:     jmp    0x8048090 <main+16>
0x804808a <main+10>:    lea    0x0(%esi),%esi
0x8048090 <main+16>:    leave
0x8048091 <main+17>:    ret
End of assembler dump.

 

Las primeras dos instrucciones de ensamblador (push %ebp; mov %esp,%ebp) son una formula en ensamblador (como veremos después). El programa carga 123 (0x7b) en eax y sencillamente retorna al llamador. Sin embargo, ¿Cuál es su llamador? Como excluimos crt1.o del código de test0, sencillamente no hay llamador. Como resultado, la declaración ret obtiene de la pila una dirección de retorno indefinida y la CPU salta a una dirección sin sentido (lo que produce el mensaje “Segmentation fault”).

 

¿Cómo comunicarnos con las llamadas al sistema en ensamblador?

 

Vamos a crear una función exit original utilizando NASM (Netwide Assembler) en lugar de la función de GLBIC.

xexit.asm

     
;
; N A M E : x e x i t . a s m
;
; D E S C : exit() by assembly
;
; A U T H : Wataru Nishida, M.D., Ph.D.
;           wnishida@skyfree.org
;           http://www.skyfree.org
;
; M A K E : nasm -f elf xexit.asm
;
; V E R S : 1.0
;
; D A T E : Jan. 5, 2001
;
 
bits    32              ; Use 32bit mode.
 
; [ NOTE ] Code starts from here.
 
section .text           ; Code must resides in the ".text" in GCC.
global  xexit           ; Declare xexit() as a public function.
 
;
;   x e x i t
;
;       void xexit (int status)
;
;                                               Jan. 5, 2001
 
xexit:
        mov     ebp, esp                ; Now, [ ebp ] points return address.
                                        ; and [ ebp+4 ] points 1st argument.
 
        mov     ebx, [ ebp + 4 ]        ; Read status code.
        mov     eax, 1                ; Call sys_exit().
        int     0x80
                                        ; Never returns.

 

Este programa es esencialmente el de una llamada al sistema en ensamblador (¡sólo necesita 4 instrucciones!). En lenguaje C, el llamador pone los argumentos y la dirección de retorno en la pila (normalmente las operaciones de apilado son en unidades palabra, 32 bits). [ebp] apunta a la dirección de retorno, [ebp+4] al primer argumento, [ebp+8] al segundo, y así sucesivamente. Ponemos a 1 el registro eax, ya que este es el número de la llamada exit (podemos verlo en /usr/incluye/asm/unistd.h :  #define __NR_exit  1), y, finalmente, invocamos la llamada mediante la interrupción software 128 (0x80).

 

Ahora utilizaremos esta nueva función en nuestro programa.

 

test2.c

     
extern void xexit(int);
 
main() {
  xexit(123);
 }

 

    
$ gcc -c test2.c
$ ld -e main -o test2 test2.o xexit.o
$ ls -l test2*
-rwxr-xr-x    1 root     src          1089 Jan  6 21:32 test2
-rw-------    1 root     src            51 Jan  5 19:10 test2.c
-rw-r--r--    1 root     src           816 Jan  6 21:32 test2.o
 
$ ldd test2
        statically linked (ELF)
 
$ nm test2
080490ac A __bss_start
080490ac A _edata
080490ac A _end
08048080 t gcc2_compiled.
08048080 T main
080480a0 T xexit
 
$ ./test2 ; echo $?
123

 

Bien, el código nuevo se ha creado con éxito, y es independiente de la biblioteca. Sin embargo el código sigue siendo mayor de lo esperado. Main y xexit contiene unas 10 declaraciones. Su código objeto necesita menos de 100 bytes. Sigue existiendo otro inflado.

 

Breve anatomía de ELF (Executable and Linking Format)

 

ELF es muy utilizado en los Unix modernos. Es flexible, pero difícil de comprender en su estructura. La utilidad elfdump (en http://www.skyfree.org/index.html ) analiza y genera un resumen de los archivos ELF:

 

     
$ elfdump test2
[ ELF header ]
         Header ID: 0x7F E L F
       ELF version: Current
   ELF header size: 52
        File class: 32-bit objects
     Data encoding: Little endian
      Pad position: 0
 
         File type: Executable
        Target CPU: 80386
      File version: Current
     Entry address: 8048080
   Processor flags: 0
  Name table index: 7
 
* Program header table
            Offset: 52
       Header size: 32
 Number of headers: 2
 Total header size: 64
 
* Section header table
            Offset: 332
       Header size: 40
 Number of headers: 10
 Total header size: 400
 
[ Program headers ]
Idx Type  Offset     VMA      PMA      FSIZ     MSIZ  Flag   Algn
-----------------------------------------------------------------
 0) LOAD        0  8048000  8048000      172      172  R X   4096
 1) LOAD      172  80490AC  80490AC        0        0  RW    4096
 
[ Section headers ]
Idx       Name       Type Flag   VMA     Offset   Size   Lnk Inf Algn Tbl
-------------------------------------------------------------------------
 0)                  NULL            0        0        0   0   0    0   0
 1) .text            PROG XA   8048080      128       44   0   0   16   0
 2) .data            PROG  AW  80490AC      172        0   0   0    4   0
 3) .sbss            PROG   W  80490AC      172        0   0   0    1   0
 4) .bss             NOSP  AW  80490AC      172        0   0   0    4   0
 5) .comment         PROG            0      172       75   0   0    1   0
 6) .note            NOTE            0      247       20   0   0    1   0
 7) .shstrtab        STRT            0      267       65   0   0    1   0
 8) .symtab          SYMT            0      732      288   9  13    4  16

 9) .strtab          STRT            0     1020       69   0   0    1   0

 

Este es el contenido de test2. Como podemos observar, el programa tiene 9 secciones. La sección .text contiene el código real de nuestro programa y tiene 44 bytes de tamaño. ¿qué son las secciones .symtab y .strtab que ocupan el espacio principal de test2? Inspeccionémoslas con dump.

 

$ dump test2
... skipped ...
 
01008 003F0 | A0 80 04 08 00 00 00 00 10 00 01 00 00 74 65 73 | .............tes
01024 00400 | 74 32 2E 63 00 67 63 63 32 5F 63 6F 6D 70 69 6C | t2.c.gcc2_compil
01040 00410 | 65 64 2E 00 78 65 78 69 74 2E 61 73 6D 00 5F 5F | ed..xexit.asm.__
01056 00420 | 62 73 73 5F 73 74 61 72 74 00 6D 61 69 6E 00 5F | bss_start.main._
01072 00430 | 65 64 61 74 61 00 5F 65 6E 64 00 78 65 78 69 74 | edata._end.xexit

 

Podemos ver nombres familiares  como main y xexit. .symtab y .strtab son secciones para nombres de símbolos y no son estrictamente necesarias para la ejecución. Por tanto, podemos eliminarlas.

 
$ strip test2
$ l test2
-rwxr-xr-x    1 root     src           652 Jan  6 21:36 test2
$ elfdump test2
... información eliminada...
 
[ Section headers ]
Idx       Name       Type Flag   VMA     Offset   Size   Lnk Inf Algn Tbl
-------------------------------------------------------------------------
 0)                  NULL            0        0        0   0   0    0   0
 1) .text            PROG XA   8048080      128       44   0   0   16   0
 2) .data            PROG  AW  80490AC      172        0   0   0    4   0
 3) .sbss            PROG   W  80490AC      172        0   0   0    1   0
 4) .bss             NOSP  AW  80490AC      172        0   0   0    4   0
 5) .comment         PROG            0      172       75   0   0    1   0
 6) .note            NOTE            0      247       20   0   0    1   0

 7) .shstrtab        STRT            0      267       65   0   0    1   0

 

La eliminación de estas secciones con strip ha reducido el código de 1089 a 652 bytes. Ahora vamos a centrarnos en .comment y .note que contienen los nombres de versión de GCC y NASM, y las vamos a borrar.

 

$ dump test2
... información suprimida ...
 
00160 000A0 | 89 E5 8B 5D 04 B8 01 00 00 00 CD 80 00 47 43 43 | ...].........GCC
00176 000B0 | 3A 20 28 47 4E 55 29 20 32 2E 39 35 2E 32 20 32 | : (GNU) 2.95.2 2
00192 000C0 | 30 30 30 30 32 32 30 20 28 44 65 62 69 61 6E 20 | 0000220 (Debian
00208 000D0 | 47 4E 55 2F 4C 69 6E 75 78 29 00 00 54 68 65 20 | GNU/Linux)..The
00224 000E0 | 4E 65 74 77 69 64 65 20 41 73 73 65 6D 62 6C 65 | Netwide Assemble
00240 000F0 | 72 20 30 2E 39 38 00 08 00 00 00 00 00 00 00 01 | r 0.98..........

 

$ strip --remove-section=.comment --remove-section=.note test2
$ ls -l test2
-rwxr-xr-x    1 root     src           464 Jan  6 21:46 test2
 
$ elfdump test2
[ ELF header ]
         Header ID: 0x7F E L F
       ELF version: Current
   ELF header size: 52
        File class: 32-bit objects
     Data encoding: Little endian
      Pad position: 0
 
         File type: Executable
        Target CPU: 80386
      File version: Current
     Entry address: 8048080
   Processor flags: 0
  Name table index: 2
 
* Program header table
            Offset: 52
       Header size: 32
 Number of headers: 1
 Total header size: 32
 
* Section header table
            Offset: 208
       Header size: 40
 Number of headers: 3
 Total header size: 120
 
[ Program headers ]
Idx Type  Offset     VMA      PMA      FSIZ     MSIZ  Flag   Algn
-----------------------------------------------------------------
 0) LOAD        0  8048000  8048000      172      172  R X   4096
 
[ Section headers ]
Idx       Name       Type Flag   VMA     Offset   Size   Lnk Inf Algn Tbl
-------------------------------------------------------------------------
 0)                  NULL            0        0        0   0   0    0   0
 1) .text            PROG XA   8048080      128       44   0   0   16   0
 2) .data            PROG  AW  80490AC      172        0   0   0    4   0
 3) .sbss            PROG   W  80490AC      172        0   0   0    1   0
 4) .bss             NOSP  AW  80490AC      172        0   0   0    4   0
 5) .shstrtab        STRT            0      172       50   0   0    1   0

 

strip con la opción –remove-section= elimina la sección especificada de un archivo ELF. Ahora el código a alcanzado 464 bytes.

 

$ strip --remove-section=.data --remove-section=.sbss --remove-section=.bss test2
$ ls -l test2
-rwxr-xr-x    1 root     src           328 Jan 11 18:55 a.out
$ elfdump test2
... skipped...
 
[ Section headers ]
Idx       Name       Type Flag   VMA     Offset   Size   Lnk Inf Algn Tbl
-------------------------------------------------------------------------
 0)                  NULL            0        0        0   0   0    0   0
 1) .text            PROG XA   8048080      128       44   0   0   16   0
 2) .shstrtab        STRT            0      172       33   0   0    1   0
 
$ ./test2 ; echo $?
123

 

Hemos eliminado las secciones .data, .sbss, y .bss y el codigo tiene 328 bytes. Estas secciones son para almacenar datos, y normalmente deberemos incluirlas. Nuestro ejemplo es un caso especial y no las necesita.

 

$ dump test2
... información suprimida ...
     
00160 000A0 | 89 E5 8B 5D 04 B8 01 00 00 00 CD 80 00 2E 73 79 | ...]..........sy
00176 000B0 | 6D 74 61 62 00 2E 73 74 72 74 61 62 00 2E 73 68 | mtab..strtab..sh

00192 000C0 | 73 74 72 74 61 62 00 2E 74 65 78 74 00 00 00 00 | strtab..text....

 

Finalmente, la sección .shstrtab es esencialmente un contenedor de nombres de secciones. Debemos dejarlo.

 

“Hola mundo” independiente de la biblioteca

 

Vamos a presentar una versión del programa “Hola Mundo” que esta libre de GLIBC. Para ello, vamos a construir una rutina en ensamblador para imprimir, en lugar de utilizar printf() o fpritnf(), que utiliza la llamada al sistema write (función número 4) con tres argumentos: un descriptor de archivo, un puntero a un búfer de caracteres, y la longitud de la cadena. Utilizamos a salida estándar, stdout, que usa 1 como descriptor de archivo. Para el búfer temporal, declaramos la variable “msgbuf” en la sección .data. Si deseamos escribir una cadena, deberemos escribir una función strlen() original.

 

xputchar.asm

     
;
; N A M E : x p u t c h a r . a s m
;
; D E S C : putchar() by assembly
;
; A U T H : Wataru Nishida, M.D., Ph.D.
;           wnishida@skyfree.org
;           http://www.skyfree.org
;
; M A K E : nasm -f elf xputchar.asm
;
; V E R S : 1.0
;
; D A T E : Dec. 17, 2000
;
 
bits    32             ; Use 32bit mode.
 
; [ NOTE ] Code starts from here.
 
section .text          ; Code must resides in the ".text" in GCC.
global  xputchar       ; Declare xputchar() as a public function.
 
;
;   x p u t c h a r
;
;       int xputchar (unsigned int ch)
;
;       --- return one (success) or -1 (error)
;
;                                             Dec. 17, 2000
 
xputchar:
        push    ebp                    ; Remember EBP.
        mov            ebp, esp       ; Prepare my stack frame.
                                              ; Now, [ ebp ] points saved EBP,
                                              ; and [ ebp+4 ] points return address.
 
        ; [ NOTE ] We must preserve registers except EAX, ECX, and EDX.
 
        push    ebx                    ; Save EBX.
 
        ; [ NOTE ] 1st argument appears in [ ebp+8 ], and 2nd [ ebp+12 ]...
 
        mov     eax, [ ebp + 8 ]; Get character code.
        mov     [ msgbuf ], al ; And save it.
        mov     edx, 1                 ; Set character count.
        mov     ecx, msgbuf            ; Set buffer pointer.
        mov     ebx, 1                 ; Set STDOUT.
        mov     eax, 4                 ; Call sys_write().
        int     0x80
 
        pop     ebx                            ; Recover EBX.
 
        leave                          ; Perform ESP=EBP and EBP=pop().
        ret
 
; [ NOTE ] Storage area for the variables starts from here.
 
section .data   ; Variables must reside in the ".data" in GCC.
 
msgbuf  db      0       ; Message buffer.

 

 

hello.c

     
extern int xputchar(unsigned int);
extern int xexit(int);
 
main(){
  char msg[]="Hello World!\n";
  char *ptr;
  int res;
 
  ptr = msg;
  while (*ptr)
    res = xputchar(*ptr++);
  xexit(128);
 }

 

Aquí esta el cuerpo del ensamblado “Hola Mundo”. Es un sencillo programa en C. vamos a crear el programa.

 

$ nasm -f elf xputchar.asm
$ gcc -c hello.c
$ ld -s -e main -o hello hello.o xputchar.o xexit.o
$ strip --remove-section=.comment --remove-section=.note hello
 
$ ldd hello
        statically linked (ELF)
 
$ ls -l
total 25
drwxr-sr-x    2 root     src           208 Jan  6 22:03 .
drwxr-sr-x    4 root     src            96 Jan  6 21:57 ..
-rwxr-xr-x    1 root     src           676 Jan  6 22:03 hello
-rw-------    1 root     src           196 Jan  6 22:02 hello.c
-rw-r--r--    1 root     src          1040 Jan  6 22:02 hello.o
-rw-r--r--    1 root     src           512 Jan  6 21:31 xexit.o
-rw-------    1 root     src          1360 Jan  5 18:49 xputchar.asm
-rw-r--r--    1 root     src           704 Jan  5 18:49 xputchar.o
 
$ ./hello ; echo $?
Hello World!
128

 

Con la opción –s, ld elimina automáticamente las secciones de la tabla de símbolos. El programa es independiente de la biblioteca, y sólo tiene 676 bytes.

 

Referencia:

 

http://www.skyfree.org/linux/assembly/section1.html