C to ELF - Demostración de las fases de construcción de un programa

Intro

Construiremos un ejecutable usando dos archivos: main.c y foo.c.

main.c

#include <stdio.h>

#define MYCONST 15
// The function prototype of foo, which
// will be implemented elsewhere
int foo(char *input);

static int i = 100;

int aGlobal = 10;
long long int anotherGlobal = 20;

int main(void) {
    int a = MYCONST;
    int b = 6;
    int c = 7;
    int d = (a + b)*4 + c;
    int ret = d + foo("Hello, World!");
    return ret;
}

foo.c:

#include <stdio.h>

static int i = 100;

/* Declard as extern since defined in hello.c */
extern int aGlobal;

int foo(char *input) {
    printf("In foo: %s\n", input);
    return aGlobal;
}

Fase 1 - De código fuente a archivo pre-procesado

Usamos el comando gcc -E main.c para desplegar el código pre-procesado. Dos observaciones importantes:

  1. La constante definida MYCONST fue sustituida por su valor (15) el preprocesador.

  2. El directivo #include<stdio.h> fue sutitiuido por el contenido del archivo stdio.h (y por los contenidos de los archivos que incluye stdio.h).

Fase 2 - De pre-procesado a assembly

Usamos los comandos gcc -m32 -S main.c y gcc -m32 -S foo.c para obtener los files main.s y foo.s, que contienen el resultado del proceso de compilación, i.e. un programa en assembly. A continuación fragmentos de los archivos con algunas anotaciones (como comentairios)

main.s (fragmento)

.section .rodata
.LC0:
.string "Hello, World!"    ; el string constante que se pasara a foo
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)

pushl%ebp                  ; \
movl %esp, %ebp            ; }- Prólogo de la función main
pushl %ecx                 ; |
subl $36, %esp             ; /

movl $15, -24(%ebp)        ; variable a
movl $6, -20(%ebp)         ; variable b
movl $7, -16(%ebp)         ; variable c

movl -20(%ebp), %eax       ; \
addl -24(%ebp), %eax       ; }- expresion d = (a + b)*4 + c
sall $2, %eax              ; |
addl -16(%ebp), %eax       ; |
movl %eax, -12(%ebp)       ; /

movl $.LC0, (%esp)         ; parámetro para foo
call foo                   ; invocación a foo
addl -12(%ebp), %eax       ; sumandole el valor de d al return value de foo
movl %eax, -8(%ebp)        ; ret = ....
movl -8(%ebp), %eax        ; colocando valor de ret para ser devuelto        

addl $36, %esp             ; \
popl %ecx                  ; }- Epílogo
popl %ebp                  ; /
leal -4(%ecx), %esp
ret

foo.s (fragmento)

.LC0:
 .string "In foo: %s\n"  ; el string constante que se pasará a printf
 .text
.globl foo
 .type foo, @function
foo:
 pushl %ebp              ; \
 movl %esp, %ebp         ; }- Prólogo de la función main
 subl $8, %esp           ; /

 movl 8(%ebp), %eax      ; leemos el argumento, lo copiamos a eax
 movl %eax, 4(%esp)      ; guardamos en stack para que sirva como argumento
                         ; a printf
 movl $.LC0, (%esp)      ; ponemos el primer argumento de printf en el stack
 call printf             ; invocamos printf

 movl aGlobal, %eax      ; colocamos valor de variable aGlobal en eax
                         ; para que sea el return value de esta función

 leave                   ; epilogo de la función
 ret

De assembly a objective file

Usamos el assembler as de la siguiente forma: as --32 -o main.o main.s y as --32 -o foo.o foo.s para obtener main.o y foo.o. Los archivos a partir de este paso son de tipo binario. En linux se utiliza el formato ELF para los object files y los ejecutables. Puedes usar programas como readelf o objdump para obtener información sobre los archivos de formato ELF.

Por ejemplo, el siguiente es el output de objdump -s main.o:

main.o:     file format elf32-i386

Contents of section .text:
 0000 8d4c2404 83e4f0ff 71fc5589 e55183ec  .L$.....q.U..Q..
 0010 24c745e8 0f000000 c745ec06 000000c7  $.E......E......
 0020 45f00700 00008b45 ec0345e8 c1e00203  E......E..E.....
 0030 45f08945 f4c70424 00000000 e8fcffff  E..E...$........
 0040 ff0345f4 8945f88b 45f883c4 24595d8d  ..E..E..E...$Y].
 0050 61fcc3                               a..
Contents of section .data:
 0000 64000000 0a000000 14000000 00000000  d...............
Contents of section .rodata:
 0000 48656c6c 6f2c2057 6f726c64 2100      Hello, World!.
Contents of section .comment:
 0000 00474343 3a202847 4e552920 342e312e  .GCC: (GNU) 4.1.
 0010 32203230 30383037 30342028 52656420  2 20080704 (Red
 0020 48617420 342e312e 322d3438 2900      Hat 4.1.2-48).

Notarás que al igual que en el archivo en assembly, en el object file existe una distinción entre lo que son variables globales (section .data), constantes globales (section .rodata) e instrucciones (section .text). En la sección .data puedes ver los valores que asumirán las variables globales.

0000 64000000 0a000000 14000000 00000000  
---- -------- -------- -----------------
 |      |        |        |
 |      |        |        +-- el valor 20 que le es asignado a anotherGlobal
 |      |        +-- el valor de 10 que es asignado a aGlobal
 |      +-- el valor de 100 de la variable estática i
 +-- el offset desde el comienzo de la sección .data

Si hacemos readelf -all main.o encontramos información como la siguiente:

Relocation section '.rel.text' at offset 0x3dc contains 2 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000038  00000601 R_386_32          00000000   .rodata
0000003d  00000c02 R_386_PC32        00000000   foo

El relocation section contiene símbolos que son referenciados en el main.c pero cuyas implementaciones aun no se conocen. El prototipo de la función foo está en main.c pero no así su implementación.

La misma información para foo.o es:

Relocation section '.rel.text' at offset 0x378 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000010  00000601 R_386_32          00000000   .rodata
00000015  00000a02 R_386_PC32        00000000   printf
0000001a  00000b01 R_386_32          00000000   aGlobal

Noten que foo.c no conoce sobre la implementación de printf, ni sabe lo que es valor del símbolo aGlobal. En el próximo paso, el linker combinará los archivos main.o y foo.o.

Enlazando los objective files en un ejecutable

El comando gcc -m32 -o main main.o foo.o invoca al linker para generar el ejecutable main. Si miramos el relocation section del ejecutable notaremos que se han resuelto algunos problemas pero se han añadido otros relocations.

Relocation section '.rel.plt' at offset 0x258 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804966c  00000107 R_386_JUMP_SLOT   00000000   __gmon_start__
08049670  00000207 R_386_JUMP_SLOT   00000000   __libc_start_main
08049674  00000307 R_386_JUMP_SLOT   00000000   printf

Nota que ya no aparece ni foo ni aGlobal. Sin embargo printf sigue apareciendo. Esto se debe a que la implementación de printf no estará incluida dentro del archivo ejecutable main. Cada vez que main invoque a printf el código de printf se accesará de una librería compartida que reside en otra parte de memoria. De hecho reside en otra parte de memoria para que otros ejecutables la puedan compartir. El loader se encarga de especificar la dirección de esa printf cuando carga a main a memoria. En otras palabras el loader cargará un puntero en la dirección 08049674 de main que apuntara a la direccíon donde reside al código de la función printf.

ELF para dos cosas diferentes

Los archivos de formato Executable and Linkable Format se usan tanto para los object files como para los ejecutables en linux. ¿Como puedes diferenciar entre ellos? Basta con usar el comando file:

file main.o devuelve:

main.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

La palabra relocatable delata que es un object file que todavía no está listo para ser ejecutado.

file main devuelve:

main: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=91ec53ed6df5e6cac39ec2f23ed4e556317fc9be, not stripped

La info dentro del ELF relocatable sirve para ayudar al LINKER a hacer su trabajo

A continuación una de las informaciones que desplega readelf -a main.o y que es útil para el linker: la tabla de las secciones. Los archivos ELF se organizan en secciones. Por ejemplo:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 00005c 00  AX  0   0  1
  [ 2] .rel.text         REL             00000000 00022c 000028 08   I 11   1  4
  [ 3] .data             PROGBITS        00000000 000090 000000 00  WA  0   0  1
  [ 4] .bss              NOBITS          00000000 000090 000000 00  WA  0   0  1
  [ 5] .rodata           PROGBITS        00000000 000090 000011 00   A  0   0  1
  [ 6] .comment          PROGBITS        00000000 0000a1 000035 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 0000d6 000000 00      0   0  1
  [ 8] .eh_frame         PROGBITS        00000000 0000d8 000044 00   A  0   0  4
  [ 9] .rel.eh_frame     REL             00000000 000254 000008 08   I 11   8  4
  [10] .shstrtab         STRTAB          00000000 00025c 00005f 00      0   0  1
  [11] .symtab           SYMTAB          00000000 00011c 0000e0 10     12   9  4
  [12] .strtab           STRTAB          00000000 0001fc 00002e 00      0   0  1

La info dentro del ELF ejecutable sirve para ayudar al LOADER a hacer su trabajo

El ELF de un ejecutable contiene una tabla que es utilizada por el loader para saber cómo organizar la memoria que se dedicará al ejecutar este programa. Por ejemplo,

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  INTERP         0x000154 0x08048154 0x08048154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00694 0x00694 R E 0x1000
  LOAD           0x000f08 0x08049f08 0x08049f08 0x00118 0x100138 RW  0x1000
  DYNAMIC        0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x000574 0x08048574 0x08048574 0x00034 0x00034 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version 
   .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got