A long story about Elves. ELF file explained. Part 1

Introduction

ELF (executable and linkable format) binary format is commonly used in Unix-like systems. It contains executable instructions along with metadata that is required to load it with all required dependencies, allocate memory and execute. Those files contain the code itself, static data used by this code and all metadata required to execute it.

Is it possible to execute some code in Linux environment without those file structures?

Every program in Linux environment is started using execve system call that checkes binary for interpreter path, it's part of ELF file metadata, so binary file with just instructions will not be executed, so the answer is NO. It can be loaded and executed by other code, but this code needs to be loaded first.

When kernel detects ELF file it runs Dynamic Loader which usually is ld.so.

In few blog posts i would like to introduce ELF format and explain the loading process. I have written ELF loader in Rust, so check it for more details.

You can find ELF64 specification HERE

Simple Example

Ok so let's start with a simple assembly code written using NASM for amd64 architecture. It uses Linux syscalls so it doesn't need any libraries.

bits 64
section .text
global _start

%define WRITE 1
%define EXIT 60

%define STDOUT 1

_start:
    mov rax, WRITE
    mov rdi, STDOUT
    lea rsi, msg
    mov rdx, msg_len
    syscall

    mov rax, EXIT
    mov rdi, 0
    syscall


section .rodata
msg: db 'Hello World!', 0, 10
msg_len: equ $ - msg

You can build it with:

nasm -f elf64 -o hello.o hello.asm
ld -o hello hello.o

Short explanation from the beginning for those who don't speak assembly:

First line informes that this is 64 bits architecture. Second line informes assembler, that everything below it is a .text section content. This section is a part of ELF structure and it contains executable code. Next line defines _start symbol as global, it means that it will be saved in ELF file and visible to linker. _start is a default program entry that GNU LInker uses, you can change it to anything else, for example my_entry, and then run linker with:

ld -o hello --entry my_entry hello.o

Label _start: begins the program code. It loads proper registers with systall arguments according to AMD64 ABI, registers RDI, RSI and RDX are used to pass first, second and third function argument. RAX register is used to choose proper kernel function. Function number 1 represents write syscall, function number 60 represents exit syscall. The first argument of write function is a file descriptor. In Linux environment every process has three file descriptors opened from the beginning of execution:

0: STDIN
1: STDOUT
2: STDERR

Also some string needs to be defined as write input. It can be done in other ELF section called .rodata, everything that will be put into this section will not be executable neither writable.

So those two lines:

msg: db 'Hello World!', 0, 10
msg_len: equ $ - msg

Declares that .rodata section should contain ASCII string "Hello World\0\n". msg_len calculates message length, it will be calculated by NASM and final ELF file will contain value 13. Because write function required a pointer to data buffer LEA instruction is used to load effective address of the string.

ELF64 structure

Header

The output file format begins with structure called Elf Header, It begins with 16 bytes of ELF file identifier, where first 3 bytes are ASCII characters encoding "ELF" string. Other 13 bytes contain information about version, Endianess and other information required to verify that this ELF file may run on our architecture.

Other important Elf Header fields are:

  • entry: It contains memory address of program entry point, for our code it's _start address.
  • phoff: Offset from the beginning of ELF file to the Program Header metadata
  • phentsize: Size of a single program header entry (in bytes)
  • phnum: Number of program header entries
  • shoff: Offset from the beginning of ELF file to Section Headers
  • shentsize: Size of a single Section Header
  • shnum: Number of Section Headers

You can display Elf64 Header with:

readelf -h hello

Sections

Section contains the data or code in our ELF file (except the special case when section is empty and it only contains information how much memory should be allocated during loading. It's commonly used for BSS sections).

Where the section should be loaded is defined in Section Header, it contains memory address where Dynamic Loader should load the section along with memory protection information (EXECUTABLE/READ/WRITE).

Elf64 Header contains offset to the first Section Header, number of headers and size of a single header entry.

My ELF64 header is:

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:               0x401000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          8504 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         3
  Size of section headers:           64 (bytes)
  Number of section headers:         6
  Section header string table index: 5

So the first Section Header is at 8504 byte from the beginning of our ELF file.

You can display all section headers with:

readelf -S hello

For me it's:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000401000  00001000
       0000000000000025  0000000000000000  AX       0     0     16
  [ 2] .rodata           PROGBITS         0000000000402000  00002000
       000000000000000e  0000000000000000   A       0     0     4
  [ 3] .symtab           SYMTAB           0000000000000000  00002010
       00000000000000c0  0000000000000018           4     4     8
  [ 4] .strtab           STRTAB           0000000000000000  000020d0
       0000000000000038  0000000000000000           0     0     1
  [ 5] .shstrtab         STRTAB           0000000000000000  00002108
       0000000000000029  0000000000000000           0     0     1

As you can see .text section will be loaded at address 0x401000, it's the same as defined in entry field of Elf64 Header. So the beginning of .text section is actually entry of our program. Instructions of our program start from file offset 0x1000, because this is the offset defined in .text section header.

NOTE: This program code HAS to be loaded at 0x401000 and data section at 0x402000 otherwise it will break. It will happen beacause it's position dependent. I will introduce Position Independent Code later and by using this approach i will be able to load my sections at (address + offset).

Program Header

You can load sections into memory one by one, but ELF file aggregates the into loadable segments that are defined in Program Headers. Offset to this data structure is defined in Elf64 Header field phoff.

You can display this data with:

readelf -l hello

For me it looks like:

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000000e8 0x00000000000000e8  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x0000000000000025 0x0000000000000025  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
                 0x000000000000000e 0x000000000000000e  R      0x1000

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .text 
   02     .rodata

Type LOAD means that this segment should be loaded into process memory at specified address. Every entry contains also file offset, memory address where segment should be loaded into and memory protection information. Other types are:

  • NULL: Unused entry
  • DYNAMIC: dynamic linking tables, our code doesn't use external libraries so there is no such entry.
  • INTERP: information about ELF interpreter. For our code the default onewill be used.
  • NOTE: Note section

As you can see .text section is part of the second segment with READ and EXECUTION permissions and .rodata is part of the third segment with only READ permission.

In our case there is no difference betweend loading Sections one by one or using Program Header because every entry of Program Hader contains only one section, for bigger ELF files with multiple sections one Program Header usually contains multiple sections.

This is a list of Program Headers for simple Hello World written in C:

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
                 0x0000000000000630 0x0000000000000630  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x0000000000000161 0x0000000000000161  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x00000000000000b4 0x00000000000000b4  R      0x1000
  LOAD           0x0000000000002de8 0x0000000000003de8 0x0000000000003de8
                 0x0000000000000248 0x0000000000000250  RW     0x1000
  DYNAMIC        0x0000000000002df8 0x0000000000003df8 0x0000000000003df8
                 0x00000000000001e0 0x00000000000001e0  RW     0x8
  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000040 0x0000000000000040  R      0x8
  NOTE           0x0000000000000378 0x0000000000000378 0x0000000000000378
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000040 0x0000000000000040  R      0x8
  GNU_EH_FRAME   0x0000000000002014 0x0000000000002014 0x0000000000002014
                 0x0000000000000024 0x0000000000000024  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002de8 0x0000000000003de8 0x0000000000003de8
                 0x0000000000000218 0x0000000000000218  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 .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .got.plt .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

As you can see one segment expands over multiple sections.

Loading

So how can we load section or segment defined in Program Header into memory and execute the code? Using mmap function wich stands for memory map, it's one of the system calls, also glibc provides a wrapper for C language. It's defined as follows:

       void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

First we need to open our ELF file with open function and obtain a File Descriptor, then using mmap we can map part of it (or whole file) into adress space of our process. First argument of mmap function is the virtual address, we can use it to load our section or segment properly so if for example section header of .text requires it to be loaded at 0x41000 we can use this argument to request it. Second argument is a number of bytes to map, it also can be fetched either from Section Header or Program Header. prot argument defines memory protection for mapped memory space, available values are:

  • PROT_READ
  • PROT_WRITE
  • PROT_EXEC
  • PROT_NONE

You can combine them together using "|" operator, for example:

PROT_READ | PROT_EXEC

means that this mapped memory can be read or executed, if you try to write into this segment you will get an error.

So prot can be easily optained from Section Header or Program header. For Section Header it's encoded in sh_flags field, for Program Header it's p_flags, of course those values may not directly match those required by mmap so you need to convert them first.

Argument flags is very important because it should be set to

MAP_PRIVATE | MAP_FIXED
  • MAP_PRIVATE: every write to the mapped memory will not affect the ELF file
  • MAP_FIXED: address provided in addr argument should be interpreted exactly. So mapping to address 0x40000 should be ALWAYS mapped to 0x40000, otherwise some mmap implementations may ommit addr parameter and use any address.

fd argument is a File Descriptor of the opened ELF file, offset is an offset in bytes in this file.

NOTE: The implementation may require that addr is a multiple of a Page Size, it may happen that some section has virtual address defined at for example 0x41002, you should then align it to page size by subtracting the remiainder from the address AND file offset AND add the remainder to section length. So for example if virtual address of some section is 0x41002, file offset is 0x1002, segment size is 100 and the Page Size is 4096 (0x1000) then addr argument should be 0x41000, offset should be 0x1000 and length should be 102.

Stack

The stack should be allocated to let the code use some local variables, our code does't use that but it's worth to mention. Stack could be allocated with malloc (memory allocate) function. REMEMBER THAT THE STACK GROWS DOWNWARDS!!, so if you receive some address from malloc you should provide (address + stack_size) to your process.

Execution

Ok our sections are loaded. What next? Now you can just jump to address that is stored in entry field of ELF Header, or use clone function.

       int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
                 /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

First argument is a function pointer, so you can cast address fetched from entry field. Second is a stack pointer, remember to provide (stack_beginning + stack_size), for flags i use:

CLONE_VM | SIGCHLD

to share address space between calling process and newly created process and to send signal to parent process during termination. clone returns child's process identifier so you can just call waitpid.

What next?

I introduced the process of loading simple ELF file with code that uses only syscalls. In next episodes i will try to explain how to load shared libraries.