Then come the most important symbols:
Num:    Value          Size Type    Bind   Vis      Ndx Name
  4: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    1 hello_world
  5: 000000000000000d     0 NOTYPE  LOCAL  DEFAULT  ABS hello_world_len
  6: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    2 _start
hello_world string is in the .data section (index 1). It's value is 0: it points to the first byte of that section.
_start is marked with GLOBAL visibility since we wrote:
global _start
in NASM. This is necessary since it must be seen as the entry point. Unlike in C, by default NASM labels are local.
hello_world_len points to the special st_shndx == SHN_ABS == 0xF1FF.
0xF1FF is chosen so as to not conflict with other sections.
st_value == 0xD == 13 which is the value we have stored there on the assembly: the length of the string Hello World!.
This means that relocation will not affect this value: it is a constant.
This is small optimization that our assembler does for us and which has ELF support.
If we had used the address of hello_world_len anywhere, the assembler would not have been able to mark it as SHN_ABS, and the linker would have extra relocation work on it later.
By default, NASM places a .symtab on the executable as well.
This is only used for debugging. Without the symbols, we are completely blind, and must reverse engineer everything.
You can strip it with objcopy, and the executable will still run. Such executables are called "stripped executables".
Holds strings for the symbol table.
This section has sh_type == SHT_STRTAB.
It is pointed to by sh_link == 5 of the .symtab section.
readelf -x .strtab hello_world.o
outputs:
Hex dump of section '.strtab':
  0x00000000 0068656c 6c6f5f77 6f726c64 2e61736d .hello_world.asm
  0x00000010 0068656c 6c6f5f77 6f726c64 0068656c .hello_world.hel
  0x00000020 6c6f5f77 6f726c64 5f6c656e 005f7374 lo_world_len._st
  0x00000030 61727400                            art.
This implies that it is an ELF level limitation that global variables cannot contain NUL characters.
Section type: sh_type == SHT_RELA.
Common name: "relocation section".
.rela.text holds relocation data which says how the address should be modified when the final executable is linked. This points to bytes of the text area that must be modified when linking happens to point to the correct memory locations.
Basically, it translates the object text containing the placeholder 0x0 address:
   a:       48 be 00 00 00 00 00    movabs $0x0,%rsi
  11:       00 00 00
to the actual executable code containing the final 0x6000d8:
4000ba: 48 be d8 00 60 00 00    movabs $0x6000d8,%rsi
4000c1: 00 00 00
It was pointed to by sh_info = 6 of the .symtab section.
readelf -r hello_world.o outputs:
Relocation section '.rela.text' at offset 0x3b0 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000c  000200000001 R_X86_64_64       0000000000000000 .data + 0
The section does not exist in the executable.
The actual bytes are:
00000370  0c 00 00 00 00 00 00 00  01 00 00 00 02 00 00 00  |................|
00000380  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
The struct represented is:
typedef struct {
    Elf64_Addr  r_offset;
    Elf64_Xword r_info;
    Elf64_Sxword    r_addend;
} Elf64_Rela;
So:
  • 370 0: r_offset = 0xC: address into the .text whose address this relocation will modify
  • 370 8: r_info = 0x200000001. Contains 2 fields:
    • ELF64_R_TYPE = 0x1: meaning depends on the exact architecture.
    • ELF64_R_SYM = 0x2: index of the section to which the address points, so .data which is at index 2.
    The AMD64 ABI says that type 1 is called R_X86_64_64 and that it represents the operation S + A where:
    • S: the value of the symbol on the object file, here 0 because we point to the 00 00 00 00 00 00 00 00 of movabs $0x0,%rsi
    • A: the addend, present in field r_added
    This address is added to the section on which the relocation operates.
    This relocation operation acts on a total 8 bytes.
  • 380 0: r_addend = 0
So in our example we conclude that the new address will be: S + A = .data + 0, and thus the first thing in the data section.
Besides sh_type == SHT_RELA, there also exists SHT_REL, which would have section name .text.rel (not present in this object file).
Those represent the same struct, but without the addend, e.g.:
typedef struct {
    Elf64_Addr  r_offset;
    Elf64_Xword r_info;
} Elf64_Rela;
The ELF standard says that in many cases the both can be used, and it is just a matter of convenience.
This program did not have certain dynamic linking related sections because we linked it minimally with ld.
However, if you compile a C hello world with GCC 8.2:
gcc -o main.out main.c
some other interesting sections would appear.
Determines if an executable is a position independent executable (PIE).
Seems to be informational only, since not used by Linux kernel 5.0 or glibc 2.29.
Only appears in the executable.
Contains information of how the executable should be put into the process virtual memory.
The executable is generated from object files by the linker. The main jobs that the linker does are:
  • determine which sections of the object files will go into which segments of the executable.
    In Binutils, this comes down to parsing a linker script, and dealing with a bunch of defaults.
    You can get the linker script used with ld --verbose, and set a custom one with ld -T.
  • do relocation according to the .rela.text section. This depends on how the multiple sections are put into memory.
readelf -l hello_world.out gives:
Elf file type is EXEC (Executable file)
Entry point 0x4000b0
There are 2 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000000d7 0x00000000000000d7  R E    200000
  LOAD           0x00000000000000d8 0x00000000006000d8 0x00000000006000d8
                 0x000000000000000d 0x000000000000000d  RW     200000

 Section to Segment mapping:
  Segment Sections...
   00     .text
   01     .data
On the ELF header, e_phoff, e_phnum and e_phentsize told us that there are 2 program headers, which start at 0x40 and are 0x38 bytes long each, so they are:
00000040  01 00 00 00 05 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  00 00 40 00 00 00 00 00  00 00 40 00 00 00 00 00  |..@.......@.....|
00000060  d7 00 00 00 00 00 00 00  d7 00 00 00 00 00 00 00  |................|
00000070  00 00 20 00 00 00 00 00                           |.. .....        |
and:
00000070                           01 00 00 00 06 00 00 00  |        ........|
00000080  d8 00 00 00 00 00 00 00  d8 00 60 00 00 00 00 00  |..........`.....|
00000090  d8 00 60 00 00 00 00 00  0d 00 00 00 00 00 00 00  |..`.............|
000000a0  0d 00 00 00 00 00 00 00  00 00 20 00 00 00 00 00  |.......... .....|
Structure represented www.sco.com/developers/gabi/2003-12-17/ch5.pheader.html:
typedef struct {
    Elf64_Word  p_type;
    Elf64_Word  p_flags;
    Elf64_Off   p_offset;
    Elf64_Addr  p_vaddr;
    Elf64_Addr  p_paddr;
    Elf64_Xword p_filesz;
    Elf64_Xword p_memsz;
    Elf64_Xword p_align;
} Elf64_Phdr;
Breakdown of the first one:
  • 40 0: p_type = 01 00 00 00 = PT_LOAD: this is a regular segment that will get loaded in memory.
  • 40 4: p_flags = 05 00 00 00 = execute and read permissions. No write: we cannot modify the text segment. A classic way to do this in C is with string literals: stackoverflow.com/a/30662565/895245 This allows kernels to do certain optimizations, like sharing the segment amongst processes.
  • 40 8: p_offset = 8x 00 TODO: what is this? Standard says:
    This member gives the offset from the beginning of the file at which the first byte of the segment resides.
    But it looks like offsets from the beginning of segments, not file?
  • 50 0: p_vaddr = 00 00 40 00 00 00 00 00: initial virtual memory address to load this segment to
  • 50 8: p_paddr = 00 00 40 00 00 00 00 00: unspecified effect. Intended for systems in which physical addressing matters. TODO example?
  • 60 0: p_filesz = d7 00 00 00 00 00 00 00: size that the segment occupies in memory. If smaller than p_memsz, the OS fills it with zeroes to fit when loading the program. This is how BSS data is implemented to save space on executable files. i368 ABI says on PT_LOAD:
    The bytes from the file are mapped to the beginning of the memory segment. If the segment’s memory size (p_memsz) is larger than the file size (p_filesz), the ‘‘extra’’ bytes are defined to hold the value 0 and to follow the segment’s initialized area. The file size may not be larger than the memory size.
  • 60 8: p_memsz = d7 00 00 00 00 00 00 00: size that the segment occupies in memory
  • 70 0: p_align = 00 00 20 00 00 00 00 00: 0 or 1 mean no alignment required. TODO why is this required? Why not just use p_addr directly, and get that right? Docs also say:
    p_vaddr should equal p_offset, modulo p_align
The second segment (.data) is analogous. TODO: why use offset 0x0000d8 and address 0x00000000006000d8? Why not just use 0 and 0x00000000006000d8?
Then the:
 Section to Segment mapping:
section of the readelf tells us that:
  • 0 is the .text segment. Aha, so this is why it is executable, and not writable
  • 1 is the .data segment.
Whenever Ciro Santilli learns about molecular biology, he can't help but to feel that it feels like programming, and notably systems programming and computer hardware design.
In some sense, the comparison is obvious: DNA is clearly a programmable medium like any assembly language, but still, systems programming did give Ciro some further feelings.
Ciro likes to think that maybe that is why a hardcore systems programmer like Bert Hubert got into molecular biology.
Some other people who mention similar things:
QEMU by Ciro Santilli 40 Updated 2025-07-16
The leading open source cross architecture and KVM emulator of the 2010's.
Great way to understand how operating systems work, which Ciro Santilli used extensively in his Linux Kernel Module Cheat.
User mode emulation by Ciro Santilli 40 Updated 2025-07-16
User mode emulation refers to the ability of certain emulators to emulate userland code running on top of a specific operating system, usually Linux.
For example, QEMU allows you to run a variety of userland ELF programs directly on it, without an underlying Linux kernel running.
User mode emulation is achieved by implementing system calls and special filesystems such as /dev manually on the emulator one by one.
The general tradeoff is that simulation is less acurate as it may lack certain highly advanced kernel functionality you haven't implemented yet. But it is much easier to run executables with it, and you don't have to wait for boot to finish before running, you just run executables directly from the command line.

Pinned article: Introduction to the OurBigBook Project

Welcome to the OurBigBook Project! Our goal is to create the perfect publishing platform for STEM subjects, and get university-level students to write the best free STEM tutorials ever.
Everyone is welcome to create an account and play with the site: ourbigbook.com/go/register. We belive that students themselves can write amazing tutorials, but teachers are welcome too. You can write about anything you want, it doesn't have to be STEM or even educational. Silly test content is very welcome and you won't be penalized in any way. Just keep it legal!
We have two killer features:
  1. topics: topics group articles by different users with the same title, e.g. here is the topic for the "Fundamental Theorem of Calculus" ourbigbook.com/go/topic/fundamental-theorem-of-calculus
    Articles of different users are sorted by upvote within each article page. This feature is a bit like:
    • a Wikipedia where each user can have their own version of each article
    • a Q&A website like Stack Overflow, where multiple people can give their views on a given topic, and the best ones are sorted by upvote. Except you don't need to wait for someone to ask first, and any topic goes, no matter how narrow or broad
    This feature makes it possible for readers to find better explanations of any topic created by other writers. And it allows writers to create an explanation in a place that readers might actually find it.
    Figure 1.
    Screenshot of the "Derivative" topic page
    . View it live at: ourbigbook.com/go/topic/derivative
  2. local editing: you can store all your personal knowledge base content locally in a plaintext markup format that can be edited locally and published either:
    This way you can be sure that even if OurBigBook.com were to go down one day (which we have no plans to do as it is quite cheap to host!), your content will still be perfectly readable as a static site.
    Figure 2.
    You can publish local OurBigBook lightweight markup files to either https://OurBigBook.com or as a static website
    .
    Figure 3.
    Visual Studio Code extension installation
    .
    Figure 4.
    Visual Studio Code extension tree navigation
    .
    Figure 5.
    Web editor
    . You can also edit articles on the Web editor without installing anything locally.
    Video 3.
    Edit locally and publish demo
    . Source. This shows editing OurBigBook Markup and publishing it using the Visual Studio Code extension.
    Video 4.
    OurBigBook Visual Studio Code extension editing and navigation demo
    . Source.
  3. https://raw.githubusercontent.com/ourbigbook/ourbigbook-media/master/feature/x/hilbert-space-arrow.png
  4. Infinitely deep tables of contents:
    Figure 6.
    Dynamic article tree with infinitely deep table of contents
    .
    Descendant pages can also show up as toplevel e.g.: ourbigbook.com/cirosantilli/chordate-subclade
All our software is open source and hosted at: github.com/ourbigbook/ourbigbook
Further documentation can be found at: docs.ourbigbook.com
Feel free to reach our to us for any help or suggestions: docs.ourbigbook.com/#contact