C to ELF - Demostración de las fases de construcción de un programa - versión 64 bits
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>
/* Declared 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
MYCONSTfue 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)
.size i, 4
i: ; las variables globales
.long 100 ; son definidas de forma distinta
.globl aGlobal ; a las locales
.align 4
.type aGlobal, @object
.size aGlobal, 4
aGlobal: ; otra variable global
.long 10
.globl anotherGlobal
.align 8
.type anotherGlobal, @object
.size anotherGlobal, 8
anotherGlobal: ; otra variable global
.quad 20
.section .rodata
.LC0: ; literal cuenta con su propio label
.string "Hello, World!"
.text
.globl main
.type main, @function
main:
endbr64
pushq %rbp }
movq %rsp, %rbp } prologo de main
subq $32, %rsp }
movl $15, -20(%rbp)
...
leaq .LC0(%rip), %rax ; literal siendo referenciado
movq %rax, %rdi
call foo@PLT
...
foo.s (fragmento)
.LC0:
.string "In foo: %s\n"
.text
.globl foo
.type foo, @function
foo:
endbr64
pushq %rbp
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq %rax, %rsi
leaq .LC0(%rip), %rax
movq %rax, %rdi
movl $0, %eax
call printf@PLT
movl aGlobal(%rip), %eax
leave
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 elf64-x86-64
Contents of section .text:
0000 f30f1efa 554889e5 4883ec20 c745ec0f ....UH..H.. .E..
0010 000000c7 45f00600 0000c745 f4070000 ....E......E....
0020 008b55ec 8b45f001 d08d1485 00000000 ..U..E..........
0030 8b45f401 d08945f8 488d0500 00000048 .E....E.H......H
0040 89c7e800 0000008b 55f801d0 8945fc8b ........U....E..
0050 45fcc9c3 E...
Contents of section .data:
0000 64000000 0a000000 14000000 00000000 d...............
Contents of section .rodata:
0000 48656c6c 6f2c2057 6f726c64 2100 Hello, World!.
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 '.rela.text' at offset 0x300 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000003b 000600000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000043 000f00000004 R_X86_64_PLT32 0000000000000000 foo - 4
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 '.rela.text' at offset 0x2a8 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000001a 000600000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000027 000d00000004 R_X86_64_PLT32 0000000000000000 printf - 4
00000000002d 000e00000002 R_X86_64_PC32 0000000000000000 aGlobal - 4
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 -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 '.rela.dyn' at offset 0x528 contains 8 entries:
Offset Info Type Sym. Value Sym. Name + Addend
...
000000003fd8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000003fe0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000003fe8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000003ff0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000003ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
Relocation section '.rela.plt' at offset 0x5e8 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000003fd0 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
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.
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 64-bit LSB relocatable, x86-64, 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 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5a239bec3ac7bb8f555fd4701c1cb6a435c39559, for GNU/Linux 3.2.0, 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
.textcomienza en el bytes0x1060del archivo y tiene tamaño0x01e5(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
.symtabcomienza en el byte0x3050y se extiende por0x6a8bytes.
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000000338 00000338
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.gnu.build-i NOTE 0000000000000358 00000358
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000037c 0000037c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003a0 000003a0
0000000000000024 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003c8 000003c8
00000000000000a8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000470 00000470
0000000000000084 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000000004f4 000004f4
000000000000000e 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000508 00000508
0000000000000020 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000528 00000528
00000000000000c0 0000000000000018 A 6 0 8
[11] .rela.plt RELA 00000000000005e8 000005e8
0000000000000018 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020
0000000000000020 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 0000000000001040 00001040
0000000000000010 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 0000000000001050 00001050
0000000000000010 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000001060 00001060
00000000000001e5 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 0000000000001248 00001248
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000
000000000000001e 0000000000000000 A 0 0 4
[19] .eh_frame_hdr PROGBITS 0000000000002020 00002020
000000000000004c 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 0000000000002070 00002070
0000000000000128 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000003db8 00002db8
0000000000000008 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003dc0 00002dc0
0000000000000008 0000000000000008 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003dc8 00002dc8
00000000000001f0 0000000000000010 WA 7 0 8
[24] .got PROGBITS 0000000000003fb8 00002fb8
0000000000000048 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000004000 00003000
0000000000000024 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004024 00003024
0000000000000004 0000000000000000 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 00003024
000000000000002b 0000000000000001 MS 0 0 1
[28] .symtab SYMTAB 0000000000000000 00003050
00000000000006a8 0000000000000018 29 49 8
[29] .strtab STRTAB 0000000000000000 000036f8
0000000000000221 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 00003919
000000000000011a 0000000000000000 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
maindice (en la primera fila que dice LOAD ) que cuando se ejecutemainhay que separar un espacio de memoria cuya dirección virtual comience en0x00001000y ocupe0x00255bytes y que sea ejecutable. En ese espacio vivirán, entre otras, las secciones .text, .init, .fini. -
la fila
NOTEindica que al cargar este programa a memoria se debe reservar un espacio que comience en la direccción virtual0x0000000000000338para guardar infomación sobre el compilador, entre otras. Ese segmento será Read Only -
la fila
DYNAMICindica que se debe separar un segmento de memoria que comience en la dirección virtual03dc8que 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 Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000600 0x0000000000000600 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000255 0x0000000000000255 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000198 0x0000000000000198 R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x000000000000026c 0x0000000000000270 RW 0x1000
DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000358 0x0000000000000358 0x0000000000000358
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002020 0x0000000000002020 0x0000000000002020
0x000000000000004c 0x000000000000004c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000248 0x0000000000000248 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got