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:
-
La constante definida
MYCONST
fue sustituida por su valor (15
) el preprocesador. -
El directivo
#include<stdio.h>
fue sutitiuido por el contenido del archivostdio.h
(y por los contenidos de los archivos que incluyestdio.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:
-
el linker necesita saber en qué parte del file se encuentran las instrucciones en lenguaje de máquina de este object file. La tabla indica que la sección
.text
comienza en el bytes0x34
del archivo y tiene tamaño0x5c
(mira la fila [ 1]). -
el linker necesita saber en qué parte del file está la información sobre los símbolos de este object file. La tabla indica que la sección
.symtab
comienza en el byte0x11c
y se extiende por0xe0
bytes.
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,
-
el ELF de
main
dice (en la primera fila que dice LOAD ) que cuando se ejecutemain
hay que separar un espacio de memoria cuya dirección virtual comience en0x08048000
y ocupe0x00694
bytes y que sea ejecutable. En ese espacio vivirán, entre otras, las secciones .text, .init, .fini. -
la fila
NOTE
indica que al cargar este programa a memoria se debe reservar un espacio que comience en la direccción virtual0x08048168
para guardar infomación sobre el compilador, entre otras. Ese segmento será Read Only -
la fila
DYNAMIC
indica que se debe separar un segmento de memoria que comience en la dirección virtual0x08049f14
que sea readable and writeable. Entre otras cosas, en ese segmento es donde el loader debe escribir las direcciones donde están printf, scanf y otras funciones que viven en librerías compartidas.
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