ELF (Executable and Linking Format) is a file format that defines how the internal information of object files is composed and organized. The kernel uses this information to load executable files, determining where to fetch code, where to obtain initialization data, and where to load shared libraries, among other details.

There are three types of ELF files:
1. Object files
$ gcc -c test.c
The resulting test.o is an object file, which can be linked to generate an executable file.
A static library is also considered an object file, created by packaging object files into a .a file using the ar command.
For example: ar crv libtest.a test.o
2. Executable files
$ gcc -o test test.c
The resulting test file is an executable binary file.
3. Shared libraries
$ gcc test.c -fPIC -shared -o libtest.so
The resulting file libtest.so is a shared library.
The readelf command can be used to distinguish between the three types of ELF files, as the header information for each type is different.
$ readelf -h test.o
Object file
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: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 456 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 13 Section header string table index: 10
$ readelf -h test
Executable file
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: 0x400420 Start of program headers: 64 (bytes into file) Start of section headers: 2696 (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
$ readelf -h libtest.so
Shared library
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: DYN (Shared object file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x570 Start of program headers: 64 (bytes into file) Start of section headers: 2768 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 6 Size of section headers: 64 (bytes) Number of section headers: 29 Section header string table index: 26
Below is the content of the test.c file:
#include <stdio.h>
int global_data = 4;int global_data_2;int main(int argc, char **argv){ int local_data = 3;
printf("Hello World\n"); printf("global_data = %d\n", global_data); printf("global_data_2 = %d\n", global_data_2); printf("local_data = %d\n", local_data);
return (0);}
$ gcc -o test test.c
Generate the executable file test, and then analyze it using readelf.
$ readelf -h test
Below is the output:
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: 0x400420 Start of program headers: 64 (bytes into file) Start of section headers: 2696 (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
What information can we derive from the above?
1. Based on Class, Type, and Machine, we can determine that this file is a 64-bit executable file generated for an X86-64 machine.
2. The Entry point address indicates that when this program starts, it runs from the virtual address 0x400420. This address is not the address of the main function, but rather the address of the _start function, which is created by the linker for program initialization. You can see the _start function using this command: objdump -d -j .text test
3. The Number of program headers indicates that this program has 8 segments.
4. The Number of section headers indicates that this program has 30 sections.
The information stored in sections is used for linking and mainly includes: program code, program data (variables), relocation information, etc. For example: the Code section stores the code, while the data section stores initialized or uninitialized data, and so on.
The Linux kernel cannot recognize executable files in terms of sections. The kernel uses Virtual Memory Areas (VMAs) that include contiguous pages to identify processes. Each VMA may map one or more sections. Each VMA represents a segment of an ELF file.
So, how does the kernel know which section belongs to which VMA (segment)? The mapping relationship is stored in the Program Header Table (PHT).
Next, let’s check the contents of the sections:
$ readelf -S test
There are 30 section headers, starting at offset 0xa88:
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 0000000000000078 0000000000000018 A 6 1 8 [ 6] .dynstr STRTAB 00000000004002f8 000002f8 0000000000000044 0000000000000000 A 0 0 1 [ 7] .gnu.version VERSYM 000000000040033c 0000033c 000000000000000a 0000000000000002 A 5 0 2 [ 8] .gnu.version_r VERNEED 0000000000400348 00000348 0000000000000020 0000000000000000 A 6 1 8 [ 9] .rela.dyn RELA 0000000000400368 00000368 0000000000000018 0000000000000018 A 5 0 8 [10] .rela.plt RELA 0000000000400380 00000380 0000000000000048 0000000000000018 A 5 12 8 [11] .init PROGBITS 00000000004003c8 000003c8 0000000000000018 0000000000000000 AX 0 0 4 [12] .plt PROGBITS 00000000004003e0 000003e0 0000000000000040 0000000000000010 AX 0 0 4 [13] .text PROGBITS 0000000000400420 00000420 0000000000000238 0000000000000000 AX 0 0 16 [14] .fini PROGBITS 0000000000400658 00000658 000000000000000e 0000000000000000 AX 0 0 4 [15] .rodata PROGBITS 0000000000400668 00000668 0000000000000053 0000000000000000 A 0 0 8 [16] .eh_frame_hdr PROGBITS 00000000004006bc 000006bc 0000000000000024 0000000000000000 A 0 0 4 [17] .eh_frame PROGBITS 00000000004006e0 000000000000007c 0000000000000000 A 0 0 8 [18] .ctors PROGBITS 0000000000600760 00000760 0000000000000010 0000000000000000 WA 0 0 8 [19] .dtors PROGBITS 0000000000600770 00000770 0000000000000010 0000000000000000 WA 0 0 8 [20] .jcr PROGBITS 0000000000600780 00000780 0000000000000008 0000000000000000 WA 0 0 8 [21] .dynamic DYNAMIC 0000000000600788 00000788 0000000000000190 0000000000000010 WA 6 0 8 [22] .got PROGBITS 0000000000600918 00000918 0000000000000008 0000000000000008 WA 0 0 8 [23] .got.plt PROGBITS 0000000000600920 00000920 0000000000000030 0000000000000008 WA 0 0 8 [24] .data PROGBITS 0000000000600950 00000950 0000000000000008 0000000000000000 WA 0 0 4 [25] .bss NOBITS 0000000000600958 00000958 0000000000000018 0000000000000000 WA 0 0 8 [26] .comment PROGBITS 0000000000000000 00000958 000000000000002c 0000000000000001 MS 0 0 1 [27] .shstrtab STRTAB 0000000000000000 00000984 00000000000000fe 0000000000000000 0 0 1 [28] .symtab SYMTAB 0000000000000000 00001208 0000000000000648 0000000000000018 29 46 8 [29] .strtab STRTAB 0000000000000000 00001850 000000000000021e 0000000000000000 0 0 1Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
The .text section stores the program’s code (binary instructions), and the flag for this section is X, indicating it is executable.
Next, let’s use objdump to disassemble and view the contents of the .text section:
$ objdump -d -j .text test
The -d option tells objdump to disassemble the machine code, and the -j option specifies that we are only interested in the .text section.
test: file format elf64-x86-64
Disassembly of section .text:
0000000000400420 <_start>: 400420: 31 ed xor %ebp,%ebp 400422: 49 89 d1 mov %rdx,%r9 400425: 5e pop %rsi 400426: 48 89 e2 mov %rsp,%rdx 400429: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 40042d: 50 push %rax 40042e: 54 push %rsp 40042f: 49 c7 c0 80 05 40 00 mov $0x400580,%r8 400436: 48 c7 c1 90 05 40 00 mov $0x400590,%rcx 40043d: 48 c7 c7 04 05 40 00 mov $0x400504,%rdi 400444: e8 c7 ff ff ff callq 400410 <__libc_start_main@plt> 400449: f4 hlt 40044a: 90 nop 40044b: 90 nop
000000000040044c <call_gmon_start>: 40044c: 48 83 ec 08 sub $0x8,%rsp 400450: 48 8b 05 c1 04 20 00 mov 0x2004c1(%rip),%rax # 600918 <_DYNAMIC+0x190> 400457: 48 85 c0 test %rax,%rax 40045a: 74 02 je 40045e <call_gmon_start+0x12> 40045c: ff d0 callq *%rax 40045e: 48 83 c4 08 add $0x8,%rsp 400462: c3 retq 400463: 90 nop 400464: 90 nop 400465: 90 nop 400466: 90 nop 400467: 90 nop 400468: 90 nop 400469: 90 nop 40046a: 90 nop 40046b: 90 nop 40046c: 90 nop 40046d: 90 nop 40046e: 90 nop 40046f: 90 nop
0000000000400470 <__do_global_dtors_aux>: 400470: 55 push %rbp 400471: 48 89 e5 mov %rsp,%rbp 400474: 53 push %rbx 400475: 48 83 ec 08 sub $0x8,%rsp 400479: 80 3d d8 04 20 00 00 cmpb $0x0,0x2004d8(%rip) # 600958 <__bss_start> 400480: 75 4b jne 4004cd <__do_global_dtors_aux+0x5d> 400482: bb 78 07 60 00 mov $0x600778,%ebx 400487: 48 8b 05 d2 04 20 00 mov 0x2004d2(%rip),%rax # 600960 <dtor_idx.6349> 40048e: 48 81 eb 70 07 60 00 sub $0x600770,%rbx 400495: 48 c1 fb 03 sar $0x3,%rbx 400499: 48 83 eb 01 sub $0x1,%rbx 40049d: 48 39 d8 cmp %rbx,%rax 4004a0: 73 24 jae 4004c6 <__do_global_dtors_aux+0x56> 4004a2: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) 4004a8: 48 83 c0 01 add $0x1,%rax 4004ac: 48 89 05 ad 04 20 00 mov %rax,0x2004ad(%rip) # 600960 <dtor_idx.6349> 4004b3: ff 14 c5 70 07 60 00 callq *0x600770(,%rax,8) 4004ba: 48 8b 05 9f 04 20 00 mov 0x20049f(%rip),%rax # 600960 <dtor_idx.6349> 4004c1: 48 39 d8 cmp %rbx,%rax 4004c4: 72 e2 jb 4004a8 <__do_global_dtors_aux+0x38> 4004c6: c6 05 8b 04 20 00 01 movb $0x1,0x20048b(%rip) # 600958 <__bss_start> 4004cd: 48 83 c4 08 add $0x8,%rsp 4004d1: 5b pop %rbx 4004d2: c9 leaveq 4004d3: c3 retq 4004d4: 66 66 66 2e 0f 1f 84 data32 data32 nopw %cs:0x0(%rax,%rax,1) 4004db: 00 00 00 00 00
00000000004004e0 <frame_dummy>: 4004e0: 48 83 3d 98 02 20 00 cmpq $0x0,0x200298(%rip) # 600780 <__JCR_END__> 4004e7: 00 4004e8: 55 push %rbp 4004e9: 48 89 e5 mov %rsp,%rbp 4004ec: 74 12 je 400500 <frame_dummy+0x20> 4004ee: b8 00 00 00 00 mov $0x0,%eax 4004f3: 48 85 c0 test %rax,%rax 4004f6: 74 08 je 400500 <frame_dummy+0x20> 4004f8: bf 80 07 60 00 mov $0x600780,%edi 4004fd: c9 leaveq 4004fe: ff e0 jmpq *%rax 400500: c9 leaveq 400501: c3 retq 400502: 90 nop 400503: 90 nop
0000000000400504 <main>: 400504: 55 push %rbp 400505: 48 89 e5 mov %rsp,%rbp 400508: 48 83 ec 20 sub $0x20,%rsp 40050c: 89 7d ec mov %edi,-0x14(%rbp) 40050f: 48 89 75 e0 mov %rsi,-0x20(%rbp) 400513: c7 45 fc 03 00 00 00 movl $0x3,-0x4(%rbp) 40051a: bf 78 06 40 00 mov $0x400678,%edi 40051f: e8 dc fe ff ff callq 400400 <puts@plt> 400524: 8b 15 2a 04 20 00 mov 0x20042a(%rip),%edx # 600954 <global_data> 40052a: b8 84 06 40 00 mov $0x400684,%eax 40052f: 89 d6 mov %edx,%esi 400531: 48 89 c7 mov %rax,%rdi 400534: b8 00 00 00 00 mov $0x0,%eax 400539: e8 b2 fe ff ff callq 4003f0 <printf@plt> 40053e: 8b 15 24 04 20 00 mov 0x200424(%rip),%edx # 600968 <global_data_2> 400544: b8 96 06 40 00 mov $0x400696,%eax 400549: 89 d6 mov %edx,%esi 40054b: 48 89 c7 mov %rax,%rdi 40054e: b8 00 00 00 00 mov $0x0,%eax 400553: e8 98 fe ff ff callq 4003f0 <printf@plt> 400558: b8 aa 06 40 00 mov $0x4006aa,%eax 40055d: 8b 55 fc mov -0x4(%rbp),%edx 400560: 89 d6 mov %edx,%esi 400562: 48 89 c7 mov %rax,%rdi 400565: b8 00 00 00 00 mov $0x0,%eax 40056a: e8 81 fe ff ff callq 4003f0 <printf@plt> 40056f: b8 00 00 00 00 mov $0x0,%eax 400574: c9 leaveq 400575: c3 retq 400576: 90 nop 400577: 90 nop 400578: 90 nop 400579: 90 nop 40057a: 90 nop 40057b: 90 nop 40057c: 90 nop 40057d: 90 nop 40057e: 90 nop 40057f: 90 nop
0000000000400580 <__libc_csu_fini>: 400580: f3 c3 repz retq 400582: 66 66 66 66 66 2e 0f data32 data32 data32 data32 nopw %cs:0x0(%rax,%rax,1) 400589: 1f 84 00 00 00 00 00
0000000000400590 <__libc_csu_init>: 400590: 48 89 6c 24 d8 mov %rbp,-0x28(%rsp) 400595: 4c 89 64 24 e0 mov %r12,-0x20(%rsp) 40059a: 48 8d 2d bb 01 20 00 lea 0x2001bb(%rip),%rbp # 60075c <__init_array_end> 4005a1: 4c 8d 25 b4 01 20 00 lea 0x2001b4(%rip),%r12 # 60075c <__init_array_end> 4005a8: 4c 89 6c 24 e8 mov %r13,-0x18(%rsp) 4005ad: 4c 89 74 24 f0 mov %r14,-0x10(%rsp) 4005b2: 4c 89 7c 24 f8 mov %r15,-0x8(%rsp) 4005b7: 48 89 5c 24 d0 mov %rbx,-0x30(%rsp) 4005bc: 48 83 ec 38 sub $0x38,%rsp 4005c0: 4c 29 e5 sub %r12,%rbp 4005c3: 41 89 fd mov %edi,%r13d 4005c6: 49 89 f6 mov %rsi,%r14 4005c9: 48 c1 fd 03 sar $0x3,%rbp 4005cd: 49 89 d7 mov %rdx,%r15 4005d0: e8 f3 fd ff ff callq 4003c8 <_init> 4005d5: 48 85 ed test %rbp,%rbp 4005d8: 74 1c je 4005f6 <__libc_csu_init+0x66> 4005da: 31 db xor %ebx,%ebx 4005dc: 0f 1f 40 00 nopl 0x0(%rax) 4005e0: 4c 89 fa mov %r15,%rdx 4005e3: 4c 89 f6 mov %r14,%rsi 4005e6: 44 89 ef mov %r13d,%edi 4005e9: 41 ff 14 dc callq *(%r12,%rbx,8) 4005ed: 48 83 c3 01 add $0x1,%rbx 4005f1: 48 39 eb cmp %rbp,%rbx 4005f4: 72 ea jb 4005e0 <__libc_csu_init+0x50> 4005f6: 48 8b 5c 24 08 mov 0x8(%rsp),%rbx 4005fb: 48 8b 6c 24 10 mov 0x10(%rsp),%rbp 400600: 4c 8b 64 24 18 mov 0x18(%rsp),%r12 400605: 4c 8b 6c 24 20 mov 0x20(%rsp),%r13 40060a: 4c 8b 74 24 28 mov 0x28(%rsp),%r14 40060f: 4c 8b 7c 24 30 mov 0x30(%rsp),%r15 400614: 48 83 c4 38 add $0x38,%rsp 400618: c3 retq 400619: 90 nop 40061a: 90 nop 40061b: 90 nop 40061c: 90 nop 40061d: 90 nop 40061e: 90 nop 40061f: 90 nop
Next, let’s use objdump to disassemble and view the contents of the .data section:
$ objdump -d -j .data test
The .data section stores initialized global variables.