sábado, 12 de mayo de 2012

POSIX: System-V Executable and Linkeable Format (ELF) and the Linux ABI

ELF son las siglas en inglés de Executable and Linkeable Format, y es el formato de archivo utilizado por los file objects (.o), binarios, librerías y coredumps en Linux y en Unix en general.
El principio de todo proceso, define al mismo como un archivo en un dispositivo de almacenamiento, que una vez compilado y al ser ejecutado, es cargado en memoria para que el scheduler (mediante el sistema operativo), le asigne tiempo de CPU y recursos a fin de poder ejecutar las rutinas definidas en el.
Este archivo, ya convertido en binario, se encuentra escrito en algunos de los cientos lenguajes de programación que existen actualmente, por ejemplo ANSI C o Fortran. Cuando fue compilado, el compilador en un momento determinado ejecuto el linker, y definió en un header todas aquellas librerías que el programa necesita para poder ejecutarse y que estás les provean aquellas funciones y procedimientos que utilice.
Un binario, puede ser compilado de dos formas, una es estática y la otra dinámica. Compilar un binario de manera estática (parámetro -Bstatic en gcc(2)), significa que se incluirán en el mismo binario todas aquellas librerías que el programa requiera. Por su parte, la principal idea de las librerías compartidas, es tener una sola copia de las mismas instaladas en el sistema operativo y que todo aquel programa que las necesite, haga uso de ellas, y las cargue en memoria para ejecución, en segmentos que pueden ser mapeados de manera privada para el proceso, o compartido entre varios procesos/threads.

Los procesos en Unix, nacen de alguna de las variantes de la syscall fork(2). fork(2), bifurca el proceso padre en una nueva imagen del proceso (una nueva entrada en la estructura proc_t), y mediante exec(2) desplaza esta imagen para crear el mapeo y la estructuras en memoria para el nuevo proceso ejecutado.
Una vez realizado el exec del proceso, y si este está compilado de manera dinámica, en invocado el runtime linker, en este caso ld.so.1, para así poder efectuar el linkeo con todas las otras librerias que el objeto requiera, como por ejemplo libc.so. Veamos un sencillo ejemplo de esto:
#include <stdio.h>

int main() 
{
  printf("Hello world\n");
  return 0;
}
En nuestro primer ejemplo, hacemos un más que simple Hello world que hace uso de la función printf(1), dicha función es parte de la libc, y actua como wrapper entre nuestro código objeto, y la syscall API provista por el sistema operativo, en este casó ejecutar la syscall write a fin de poder escribir en la memoria de vídeo la frase "Hello world".
Mediante ldd(1), podemos conocer contra que librerías se encuentra linkeado un binario:
# ldd example01
linux-vdso.so.1 =>  (0x00007fff371ff000)
libc.so.6 => /lib64/libc.so.6 (0x000000384ae00000)
/lib64/ld-linux-x86-64.so.2 (0x000000384aa00000)
En este caso destacamos las dos últimas, la primera de ella contra la libc.so.6 (quién le provee al binario de la función printf), y la segunda de ellas contra el linker, osea ld-x86-64.so.2.
Podemos diferenciar básicamente dos linkers el primero de ellos ld(1) el cual es ejecutado durante la compilación, y ld.so, que es invocado por exec(2) a fin de realizar el linkeo dinámico al momento de la ejecución de programa.
La variable de entorno LD_DEBUG, puede darnos un buen vistazo de esto:
#: LD_DEBUG=libs,files ./example01
      5708: 
      5708: file=libc.so.6 [0];  needed by ./example01 [0]
      5708: find library=libc.so.6 [0]; searching
      5708:  search cache=/etc/ld.so.cache
      5708:   trying file=/lib64/libc.so.6
      5708: 
      5708: file=libc.so.6 [0];  generating link map
      5708:   dynamic: 0x000000384b1b0b40  base: 0x0000000000000000   size: 0x00000000003b7538
      5708:     entry: 0x000000384ae217b0  phdr: 0x000000384ae00040  phnum:                 10
      5708: 
      5708: calling init: /lib64/ld-linux-x86-64.so.2
      5708: calling init: /lib64/libc.so.6
      5708: initialize program: ./example
      5708: transferring control: ./example
      5708: 
Hello world
      5708: 
      5708: calling fini: ./example [0]
El funcionamiento es sencillo, podemos notar que al ejecutar example01, el linker, este nota que se hace uso de la libc.so.6, por lo cual procederá a buscarla. El primer lugar donde buscará es en el archivo /etc/ld.so.cache, que es un archivo especial, utilizado para mantener un cache de todos los paths en el filesystem aquellas librerías que son utilizadas frecuentemente. Una vez encontrado este path, intentará abri el archivo /lib64/libc.so.6 y generará el link map, creando varias estructuras en memoria que serán utilizadas por dicha instancia de la libc. Una vez hecho esto, efectivizara estos mapeos a fin de poder ejecutar, ingresando en el segmento .init del ELF, llamando primero al runtime linker, y luego a la libc, a fin de inicializar el binario, y pasarle el control del resto de las operaciones a este. En este caso imprimirá "Hello world", (habiendo llamado a la función printf(2)), y luego a fin de poder finalizar el proceso, ejecutará el segmento .fini.

ELF es parte de la "System V application binary interface (ABI)", que define una interfaz del sistema operativo para operar con programas compilados ejecutables, y sus funciones, por ejemplo: el manejo del stack, manejo del heap, señales, inicialización y finalización de procesos, llamadas al sistema, como así también información especifica a las distintas arquitecturas que tenga soporte el sistema operativo.
Existen tres tipos de archivos ELF: ejecutables, relocatable, y shared objects, el tipo de cada ELF, es generado dependiendo la opción utilizada durante la compilación de los mismos. Un ELF, en su formato executable, contiene varios segmentos, incluyendo uno muy importante: el header ELF, que contiene información específica sobre el objeto, y una serie de campos que describe los diferentes componentes del archivo. Dentro del header ELF se encuentran dos partes importantes, ellas son el Section Header y el Program Header. El Section header es definido por la Section Header Table o SHT, y en el se encuentran todas las secciones "linkeables" del ejecutable. Por su parte el Program Header Table, o PHT, define los distintos segmentos del programa, por ejemplo aquellas secciones ejecutables.

readelf(1), en Linux (a este lo provee binutils), o elfdump(1) en FreeBSD, OpenBSD o Solaris, permite inspeccionar el header ELF, y las distintas secciones que lo componen, por ejemplo si necesitamos ver el header de example01:
#: readelf -h ./example01
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4003e0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          2560 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         8
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 27
Los campos del header ELF se encuentra definido en /usr/include/elf.h. En dos estructuras Elf32_Ehdr, para procesadores de 32 bits y Elf64_Ehdr para procesadores de 64 bits.


El primer campo e_ident, (Magic en la salida de readelf(1)), es un array que especifica el Magic number del archivo ELF que analizamos, este Magic number, identifica de manera única el tipo de archivo y se encuentra compuesto de una serie de valores que representan en forma de códigos hexadecimales distintos datos, por ejemplo: 0x7f, dice que es un ELF ejecutable, 0x45 es la letra "E" en ASCII, 0x4c es la letra "L", y 0x46 es la letra "F". Luego el número 0x02 significa que es un object file compilado para CPU's de 64 bits (0x01 para CPU's de 32 bits), y 0x01 que es little-endian (0x02 en caso de ser big-endian).
Este magic es leído por la syscall read(2) al momento de ejecutarse el binario, lo cual le dará la pauta de que tipo de archivo es:
# strace ./simple 
execve("./simple", ["./simple"], [/* 49 vars */]) = 0
brk(0)                                  = 0x1653000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe6f50bf000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=109077, ...}) = 0
mmap(NULL, 109077, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fe6f50a4000
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\27\342J8\0\0\0"..., 832) = 832
Vemos que en la última línea se efectúa la syscall read(2) a la cual toma como argumento el file descriptor (fd) 3, que no es otra cosa que el binario a ejectuar, y toma como segundo argumento el magic que obtuvo previamente al verificar el header ELF del archivo, como tercer argumento tomará el tamaño (size_t count).
El tipo de archivo ELF también es obtenido de e_type, que se define de la siguiente manera:


El header ELF tiene básicamente dos vistas una vista desde el linker, y otra en ejecución, como se muestra en la figura anterior. Esto es, como es visto por el linker, y como es visto al momento de ejecución, ya que el mapeo de memoria difiere de un momento a otro.
Ahora bien, veamos el layout de un archivo ELF, como comenté anteriormente, el header ELF mantiene referencias hacia dos tablas la SHT (Section Header Table) y la PHT (Program Header Table).

Para imprimir la PHT utilizamos nuevamente readelf(1) de la siguiente manera:
# readelf -l ./example01
Elf file type is EXEC (Executable file)
Entry point 0x4003e0
There are 8 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001c0 0x00000000000001c0  R E    8
  INTERP         0x0000000000000200 0x0000000000400200 0x0000000000400200
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000006b4 0x00000000000006b4  R E    200000
  LOAD           0x00000000000006b8 0x00000000006006b8 0x00000000006006b8
                 0x00000000000001ec 0x0000000000000200  RW     200000
  DYNAMIC        0x00000000000006e0 0x00000000006006e0 0x00000000006006e0
                 0x0000000000000190 0x0000000000000190  RW     8
  NOTE           0x000000000000021c 0x000000000040021c 0x000000000040021c
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x00000000000005e4 0x00000000004005e4 0x00000000004005e4
                 0x000000000000002c 0x000000000000002c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     8

 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 .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
La PHT, contiene información para el kernel de como iniciar el programa. La directiva LOAD determina que partes del archivo ELF deben ser mapeadas dentro de memoria y es el único que es mapeado a esta durante la ejecución del programa. La directiva INTERP especifica el interprete para el archivo ELF, que normalmente es /lib/ld-linux.so.2 en Linux y ld.so.1 en Solaris o FreeBSD. La directiva DYNAMIC es el punto de entrada para la sección .dynamic, esta sección mantiene información usada por el interprete ELF para configurar la ejecución del binario.

Podemos ver también otro tipo de información como que tipo de archivo es (EXEC en este caso, osea un archivo ejecutable), la cantidad de program headers (cantidad entradas en la PHT), el Entry point, que es la dirección de memoría en la región .text (segmento de memoria al que se mapea el código ejecutable del ELF), aquí se encuentra la sección _start, lugar donde se ejecuta el Procedure Log (PROLOG), a fin de iniciar la ejecución del binario, podemos ver esto utilizando objdump(1):
#: objdump -d -j .text example01

example:     file format elf64-x86-64
Disassembly of section .text:

00000000004003e0 <_start>:
  4003e0: 31 ed                 xor    %ebp,%ebp
  4003e2: 49 89 d1              mov    %rdx,%r9
  4003e5: 5e                    pop    %rsi
  4003e6: 48 89 e2              mov    %rsp,%rdx
  4003e9: 48 83 e4 f0           and    $0xfffffffffffffff0,%rsp
  4003ed: 50                    push   %rax
  4003ee: 54                    push   %rsp
  4003ef: 49 c7 c0 70 05 40 00  mov    $0x400570,%r8
  4003f6: 48 c7 c1 e0 04 40 00  mov    $0x4004e0,%rcx
  4003fd: 48 c7 c7 c4 04 40 00  mov    $0x4004c4,%rdi
  400404: e8 c7 ff ff ff        callq  4003d0 <__libc_start_main@plt>
  400409: f4                    hlt    
  40040a: 90                    nop
  40040b: 90                    nop
Para imprimir la SHT utilizamos nuevamente readelf(1) de la siguiente manera:
# readelf -S example01
There are 30 section headers, starting at offset 0xa00:

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         0000000000400200  00000200
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             000000000040021c  0000021c
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             000000000040023c  0000023c
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400260  00000260
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           0000000000400280  00000280
       0000000000000060  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000004002e0  000002e0
       000000000000003d  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           000000000040031e  0000031e
       0000000000000008  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400328  00000328
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400348  00000348
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400360  00000360
       0000000000000030  0000000000000018   A       5    12     8
  [11] .init             PROGBITS         0000000000400390  00000390
       0000000000000018  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004003b0  000003b0
       0000000000000030  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         00000000004003e0  000003e0
       00000000000001d8  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         00000000004005b8  000005b8
       000000000000000e  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         00000000004005c8  000005c8
       000000000000001c  0000000000000000   A       0     0     8
  [16] .eh_frame_hdr     PROGBITS         00000000004005e4  000005e4
       000000000000002c  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000400610  00000610
       00000000000000a4  0000000000000000   A       0     0     8
  [18] .ctors            PROGBITS         00000000006006b8  000006b8
       0000000000000010  0000000000000000  WA       0     0     8
  [19] .dtors            PROGBITS         00000000006006c8  000006c8
       0000000000000010  0000000000000000  WA       0     0     8
  [20] .jcr              PROGBITS         00000000006006d8  000006d8
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .dynamic          DYNAMIC          00000000006006e0  000006e0
       0000000000000190  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000600870  00000870
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         0000000000600878  00000878
       0000000000000028  0000000000000008  WA       0     0     8
  [24] .data             PROGBITS         00000000006008a0  000008a0
       0000000000000004  0000000000000000  WA       0     0     4
  [25] .bss              NOBITS           00000000006008a8  000008a4
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .comment          PROGBITS         0000000000000000  000008a4
       0000000000000058  0000000000000001  MS       0     0     1
  [27] .shstrtab         STRTAB           0000000000000000  000008fc
       00000000000000fe  0000000000000000           0     0     1
  [28] .symtab           SYMTAB           0000000000000000  00001180
       0000000000000600  0000000000000018          29    46     8
  [29] .strtab           STRTAB           0000000000000000  00001780
       00000000000001f4  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)
La SHT, mantiene información de las distintas secciones contenidas en el archivo ELF. Son interesantes la sección REL (relocations), SYMTAB/DYNSYM (symbol tables), VERSYM/VERDEF/VERNEED sections (symbol versioning information). También podemos apreciar que las distintas secciones mapeadas a la SHT, tienen permisos, por ejemplo que se permita escribir (W), en una sección determinada, o que se permita ejecutar (X) en ella. La siguiente tabla describe brevemente las principales secciones de un ELF, mapeadas en la SHT:

ELF SectionPurpose
.bssUninitialized global data ("Block Started by Symbol").
.commentA series of NULL-terminated strings containing compiler information.
.ctorsPointers to functions which are marked as __attribute__ ((constructor)) as well as static C++ objects' constructors. They will be used by __libc_global_ctors function.See paragraphs below.
.dataInitialized data.
.data.rel.roSimilar to .data section, but this section should be made Read-Only after relocation is done.
.debug_XXXDebugging information (for the programs which are compiled with -g option) which is in the DWARF 2.0 format.
.dtorsPointers to functions which are marked as __attribute__ ((destructor)) as well as static C++ objects' destructors.See paragraphs below.
.dynamicFor dynamic binaries, this section holds dynamic linking information used by ld.so. See paragraphs below.
.dynstrNULL-terminated strings of names of symbols in .dynsym section.One can use commands such as readelf -p .dynstr a.out to see these strings.
.dynsymRuntime/Dynamic symbol table. For dynamic binaries, this section is the symbol table of globally visible symbols. For example, if a dynamic link library wants to export its symbols, these symbols will be stored here. On the other hand, if a dynamic executable binary uses symbols from a dynamic link library, then these symbols are stored here too.The symbol names (as NULL-terminated strings) are stored in .dynstr section.
.eh_frame
.eh_frame_hdr
Frame unwind information (EH = Exception Handling).
To see the content of .eh_frame section, use
readelf --debug-dump=frames-interp a.out
.finiCode which will be executed when program exits normally. See paragraphs below.
.fini_arrayPointers to functions which will be executed when program exits normally. See paragraphs below.
.GCC.command.lineA series of NULL-terminated strings containing GCC command-line (that is used to compile the code) options.This feature is supported since GCC 4.5 and the program must be compiled with -frecord-gcc-switches option.
.gnu.hashGNU's extension to hash table for symbols.
.gnu.linkonceXXXGNU's extension. It means only a single copy of the section will be used in linking. This is used to by g++. g++ will emit each template expansion in its own section. The symbols will be defined as weak, so that multiple definitions are permitted.
.gnu.versionVersions of symbols.
.gnu.version_dVersion definitions of symbols.
.gnu.version_rVersion references (version needs) of symbols.
.gotFor dynamic binaries, this Global Offset Table holds the addresses of variables which are relocated upon loading. See paragraphs below.
.got.pltFor dynamic binaries, this Global Offset Table holds the addresses of functions in dynamic libraries. They are used by trampoline code in .plt section. If .got.plt section is present, it contains at least three entries, which have special meanings. See paragraphs below.
.hashHash table for symbols.
.initCode which will be executed when program initializes. See paragraphs below.
.init_arrayPointers to functions which will be executed when program starts. See paragraphs below.
.interpFor dynamic binaries, this holds the full pathname of runtime linker ld.so
.jcrJava class registration information.
.note.ABI-tagThis Linux-specific section is structured as a note section in ELF specification. 
.note.gnu.build-idA unique build ID.
.nvFatBinSegmentThis segment contains information of nVidia's CUDA fat binary container. Its format is described by struct __cudaFatCudaBinaryRec in __cudaFatFormat.h
.pltFor dynamic binaries, this Procedure Linkage Table holds the trampoline/linkage code. See paragraphs below.
.preinit_arraySimilar to .init_array section. See paragraphs below.
.rela.dynRuntime/Dynamic relocation table.For dynamic binaries, this relocation table holds information of variables which must be relocated upon loading. Each entry in this table is a struct Elf64_Rela which has only three members:
  • offset (the variable's [usually position-independent] virtual memory address which holds the "patched" value during the relocation process)
  • info (Index into .dynsym section and Relocation Type)
  • addend
See paragraphs below for details about runtime relocation.
.rela.pltRuntime/Dynamic relocation table.This relocation table is similar to the one in .rela.dyn section; the difference is this one is for functions, not variables.
The relocation type of entries in this table is R_386_JMP_SLOT or R_X86_64_JUMP_SLOT and the "offset" refers to memory addresses which are inside .got.plt section.
Simply put, this table holds information to relocate entries in .got.plt section.
.rel.text
.rela.text
Compile-time/Static relocation table.For programs compiled with -c option, this section provides information to the link editor ld where and how to "patch" executable code in .text section.
The difference between .rel.text and .rela.text is entries in the former does not have addend member. (Compare struct Elf64_Rel with struct Elf64_Rela in /usr/include/elf.h) Instead, the addend is taken from the memory location described by offset member.
Whether to use .rel or .rela is platform-dependent. For x86_32, it is .rel and for x86_64, .rela
.rel.XXX
.rela.XXX
Compile-time/Static relocation table for other sections. For example, .rela.init_array is the relocation table for .init_array section.
.rodataRead-only data.
.shstrtabNULL-terminated strings of section names.One can use commands such as readelf -p .shstrtab a.out to see these strings.
.strtabNULL-terminated strings of names of symbols in .symtab section.One can use commands such as readelf -p .strtab a.out to see these strings.
.symtabCompile-time/Static symbol table.This is the main symbol table used in compile-time linking or runtime debugging.
The symbol names (as NULL-terminated strings) are stored in .strtab section.
Both .symtab and .symtab can be stripped away by the strip command.
.tbssSimilar to .bss section, but for Thread-Local data. See paragraphs below.
.tdataSimilar to .data section, but for Thread-Local data. See paragraphs below.
.textUser's executable code

3 comentarios:

  1. Buen post, interesante, sin embargo tengo una duda, en C como abres un archivo ELF para poder obtener los campos de esas estructuras?

    Saludos.

    ResponderEliminar
  2. Buenas, lo hace leyendo el magic del archivo e identificando la ABI mediante este.

    ResponderEliminar
  3. It was very nice article and it is very useful to Linux learners.We also provide Linux online training

    ResponderEliminar