In my previous post, I wrote a simple “Hello World!” in ARM assembly on the Raspberry Pi. This post will explore what happens when the assembler runs. We will have a look at the resulting binary object file that it creates, decode its raw bytes and dissect the ELF format by hand.
Hereafter we are just copying the hexdump of the object file that was created in the previous post.
After some googling, I found out that the object file is actually described through the ELF (Executable and Linking Format) specification, which in turn is part of the more broad Application Binary Interface (ABI) specification. The latter defines operating system and application interfaces that are necessary to construct an execution environment for applications. The most recent (well, referencing today 29/05/2025) ABI standard can be found here. More specifically, this is the System V ABI which is composed from 2 parts, the generic specification (the gABI) and the architecture specific extensions. Chapters 4 and 5 of the gABI have recently been updated and the updates can be found here. It is chapter 4 “Object files” that is of interest to get the parsing going!
The object file is composed of different structures, we’ll typically find the ELF header, section header table and sections. The ELF header provides some metadata on how to interprete the object file, the section header table is an array of section header entries that describe the sections and the sections themselves are the bits and bytes that contain the actual data. But we’ll need to start parsing the ELF header to get a better idea about the structure of the object file.
As can be found in Chapter 4, the ELF header has the following structure:
Each of the members will be discussed hereafter, but you can always reference the specification.
e_type
We see that the lengths in the header below are referenced as Elf32_Half, Elf32_Word, Elf32_Addr, Elf32_Off, it is interesting to know that these represent 2, 4, 4 and 4 bytes respectively. It makes sense to understand that Addr means that the value will signify an address and Off an offset.
e_ident | 16 bytes | 0x7f 0x45 0x4c 0x46 0x01 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 | see below |
e_type | Elf32_Half | 0x00 0x01 | ET_REL = relocatable file |
e_machine | Elf32_Half | 0x00 0x28 | EM_ARM = Advanced RISC Machines ARM |
e_version | Elf32_Word | 0x00 0x00 0x00 0x01 | EV_CURRENT = Current Version |
e_entry | Elf32_Addr | 0x00 0x00 0x00 0x00 | Since this member holds zero, the file has no associated entry point |
e_phoff | Elf32_Off | 0x00 0x00 0x00 0x00 | Since this member holds zero, the file has no program header table |
e_shoff | Elf32_Off | 0x00 0x00 0x01 0x74 | Since this member is non-zero, it holds the section header table’s file offset in bytes |
e_flags | Elf32_Word | 0x05 0x00 0x00 0x00 | Flag names take the form of EF_machine_flag |
e_ehsize | Elf32_Half | 0x00 0x34 | This one indicates that the ELF header’s size is 52 bytes long |
e_phentsize | Elf32_Half | 0x00 0x00 | One entry in the file’s program header table measures 0 bytes |
e_phnum | Elf32_Half | 0x00 0x00 | There are 0 entries in the program header table. Since e_phnum is 0, there is no program header table. |
e_shentsize | Elf32_Half | 0x00 0x28 | A section header’s size is 40 bytes. A section header is one entry in the section header table and all entries are the same size. |
e_shnum | Elf32_Half | 0x00 0x09 | You will find 9 entries in the section header table. So the section header table entry is 360 bytes (e_shnum times e_shentsize). In case there is no section header table, this value should have been 0. |
e_shstrndx | Elf32_Half | 0x00 0x08 | This holds the section header table index of the entry associated with the section name string table. |
e_ident
Parsing this file, by hand for this first exercise, according to the updated chapter 4 of the general ABI specification, we notice the first 4 bytes indicating 0x7F, 0x45, 0x4C, 0x46. These are a so called “magic” number as they are just defined as such. The first byte will always be 0x7F, where the following 3 bytes represent the ASCII code for ELF. The fifth byte EU_CLASS is set to 0x01 representing the ELFCLASS32, which means we are dealing with 32-bit objects and supports machines with 32-bit architectures. The next byte encodes the EI_DATA, since the data is set to 0x01, it represents ELFDATA2LSB, meaning that the byte address 0 is on the left. So imagine we would have to store the hexstring 0x01020304, this would mean that the bytes will be stored as followed:
address | 3 | 2 | 1 | 0 |
stored hexstring | 0x01 | 0x02 | 0x03 | 0x04 |
Next to EI_DATA, we have EI_VERSION encoded in the 7th byte, which is currently EV_CURRENT, encoded as 0x01. Following EI_VERSION, the EI_OSABI identifies the OS- of ABI-specific ELF extension used by this file. The 0x00 just declares that there are no extensions to be taken into account (or unspecified). EI_ABIVERSION also declares that this is unspecified. The rest of the bytes are then just padding bytes for this header.
Exploring the ELF Sections
The next important concept in the object file are sections since they hold most of the object file’s information. Every section is described through information in the section header table. The latter being an array of ELf32_Shdr structures (or ELf64_Shdr structures depending on the architecture). The ELF Header’s e_shoff, e-shnum and e_shentsize provides you some information how to find and interprete the object file’s section header table as they represent the byte offset of the section header table from the beginning of the file, the number of entries and the size of an entry respectively.
So referring to our parsed ELF header above, we already found that:
e_shoff | 0x00 0x00 0x01 0x74 | The section header table’s byte offset is 372 from the beginning of the file |
e_shnum | 0x00 0x09 | We will find 9 section header table entries |
e_shentsize | 0x00 0x28 | An entry of the section header table has 40 bytes |
So the section header table starts at byte 372 (0x174) and ends at byte 731 (= 372 + (9 * 40 -1)) (0x2DB). Which means that from our hexdump above, the section header table is the following (and I’ve replaced the byte values by XX for the bytes that are not part of the section header table):
Each section header entry follows this structure:
So, we have the following section header table entries:
Well, according to the spec, it seems that index 0, called SHN_UNDEF, is actually one of the special section indexes. It marks an undefined, missing, irrelevant or otherwise meaningless section reference. Given its filled with zeroes, I would currently go for an irrelevant section…
This is the first entry with some actual values. So trying to parse this according to the spec seems to result in the folowing:
sh_name | Elf32_Word | 0x00 0x00 0x00 0x1f | The name is defined by the string at index 31 of the string table: .text |
sh_type | Elf32_Word | 0x00 0x00 0x00 0x01 | The type of this section is SHT_PROGBITS. A section holding information defined by the program. Format and meaning are determined solely by the program. |
sh_flags | Elf32_Word | 0x00 0x00 0x00 0x06 | This means that flags SHF_ALLOC (0x02) and SHF_EXECINSTR (0x04) are set. SHF_ALLOC indicates that section will occupy memory during process execution. The other flag indicates that the section contains executable machine instructions. |
sh_addr | Elf32_Addr | 0x00 0x00 0x00 0x00 | A 0 seems to indicate that the section will not appear in the memory image of a process |
sh_offset | Elf32_Off | 0x00 0x00 0x00 0x34 | This is the byte offset from the beginning of the file to the first byte in the section |
sh_size | Elf32_Word | 0x00 0x00 0x00 0x30 | Section size in bytes |
sh_link | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains a section header table index link, interpretation depends on the section type. It does not seem relevant though for an sh_type of SHT_PROGBITS |
sh_info | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains extra information whose interpretation depends on the section type. It does not seem relevant though for an sh_type of SHT_PROGBITS |
sh_addralign | Elf32_Word | 0x00 0x00 0x00 0x04 | Some sections have address alignment constraints. In this case, I’m not yet 100% certain what this value of 4 actually represents |
sh_entsize | Elf32_Word | 0x00 0x00 0x00 0x00 | The value of 0 indicates that the section does not hold a table of fixed-size entries |
Section [0x34 - 0x63]
Given the above section header table entry, the section is actually consisting out of the following bytes:
Since .text indicates that the section holds the executable instructions of the program, I would expect that the section represents the binary instructions of the assembly code. But, I won’t deep dive into it at this point in time.
sh_name | Elf32_Word | 0x00 0x00 0x00 0x1b | The name is defined by the string at index 27 of the string table: .rel.text |
sh_type | Elf32_Word | 0x00 0x00 0x00 0x09 | SHT_REL This section holds relocation entries without explicit addends. But, I’m not quite sure what this means, yet… |
sh_flags | Elf32_Word | 0x00 0x00 0x00 0x40 | SHF_INFO_LINK indicates that the sh_info field of this section header holds a section header table index |
sh_addr | Elf32_Addr | 0x00 0x00 0x00 0x00 | A 0 seems to indicate that the section will not appear in the memory image of a process |
sh_offset | Elf32_Off | 0x00 0x00 0x01 0x2C | This is the byte offset from the beginning of the file to the first byte in the section |
sh_size | Elf32_Word | 0x00 0x00 0x00 0x08 | Section size in bytes |
sh_link | Elf32_Word | 0x00 0x00 0x00 0x06 | This should represent the section header index of the associated symbol table. As defined by figure 4-12 in the spec |
sh_info | Elf32_Word | 0x00 0x00 0x00 0x01 | The section header index of the section to which the relocation applies. As defined by figure 4-12 in the spec |
sh_addralign | Elf32_Word | 0x00 0x00 0x00 0x04 | Some sections have address alignment constraints. In this case, I’m not yet 100% certain what this value of 4 actually represents |
sh_entsize | Elf32_Word | 0x00 0x00 0x00 0x08 | This section will hold a table of fixed-size entries, each entry will be 8 bytes |
Section [0x12C - 0x133]
.rel.text indicates that this section is holding relocation information as defined here. Not further looking into this for the time being.
sh_name | Elf32_Word | 0x00 0x00 0x00 0x25 | The name is defined by the string at index 37 of the string table: .data |
sh_type | Elf32_Word | 0x00 0x00 0x00 0x01 | SHT_PROGBITS A section holding information defined by the program. Format and meaning are determined solely by the program |
sh_flags | Elf32_Word | 0x00 0x00 0x00 0x03 | This means that flags SHF_WRITE (0x01) and SHF_ALLOC (0x02) are set. Meaning that the section contains data that should be writable during process execution and that the section occupies memory during process execution |
sh_addr | Elf32_Addr | 0x00 0x00 0x00 0x00 | A 0 seems to indicate that the section will not appear in the memory image of a process |
sh_offset | Elf32_Off | 0x00 0x00 0x00 0x64 | This is the byte offset from the beginning of the file to the first byte in the section |
sh_size | Elf32_Word | 0x00 0x00 0x00 0x00 | Section size in bytes |
sh_link | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains a section header table index link, interpretation depends on the section type. It does not seem relevant though for an sh_type of SHT_PROGBITS |
sh_info | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains a section header table index link, interpretation depends on the section type. It does not seem relevant though for an sh_type of SHT_PROGBITS |
sh_addralign | Elf32_Word | 0x00 0x00 0x00 0x01 | A value of 1 indicates that there are no alignment constraints |
sh_entsize | Elf32_Word | 0x00 0x00 0x00 0x00 | The value of 0 indicates that the section does not hold a table of fixed-size entries |
We do have to seem a section offset, however the size of the section appears to be 0. The .data section should also contain initialized data that contributes to the program’s memory image. Still puzzled somewhat why this section can have a size of 0 bytes in this case…
sh_name | Elf32_Word | 0x00 0x00 0x00 0x2b | The name is defined by the string at index 43 of the string table: .bss |
sh_type | Elf32_Word | 0x00 0x00 0x00 0x08 | SHT_NOBITS A section that occupies no space in the file. |
sh_flags | Elf32_Word | 0x00 0x00 0x00 0x03 | This means that flags SHF_WRITE (0x01) and SHF_ALLOC (0x02) are set. Meaning that the section contains data that should be writable during process execution and that the section occupies memory during process execution |
sh_addr | Elf32_Addr | 0x00 0x00 0x00 0x00 | A 0 seems to indicate that the section will not appear in the memory image of a process |
sh_offset | Elf32_Off | 0x00 0x00 0x00 0x64 | Since this is a section of type SHT_NOBITS, this field contains the conceptual file offset |
sh_size | Elf32_Word | 0x00 0x00 0x00 0x00 | Section size in bytes |
sh_link | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains a section header table index link, interpretation depends on the section type. It does not seem relevant though for an sh_type of SHT_NOBITS |
sh_info | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains a section header table index link, interpretation depends on the section type. It does not seem relevant though for an sh_type of SHT_NOBITS |
sh_addralign | Elf32_Word | 0x00 0x00 0x00 0x01 | A value of 1 indicates that there are no alignment constraints |
sh_entsize | Elf32_Word | 0x00 0x00 0x00 0x00 | The value of 0 indicates that the section does not hold a table of fixed-size entries |
.bss sections hold uninitialized data that contribute to the program’s memory image. Data are initialized with zeroes when the program begins to run.
sh_name | Elf32_Word | 0x00 0x00 0x00 0x30 | The name is defined by the string at index 48 of the string table: .ARM.attributes |
sh_type | Elf32_Word | 0x70 0x00 0x00 0x03 | This value is in between SHT_LOPROC (0x70000000) and SHT_HIPROC (0x7FFFFFFF), which are values reserved for processor-specific semantics |
sh_flags | Elf32_Word | 0x00 0x00 0x00 0x00 | No flags |
sh_addr | Elf32_Addr | 0x00 0x00 0x00 0x00 | A 0 seems to indicate that the section will not appear in the memory image of a process |
sh_offset | Elf32_Off | 0x00 0x00 0x00 0x64 | This is the byte offset from the beginning of the file to the first byte in the section |
sh_size | Elf32_Word | 0x00 0x00 0x00 0x12 | Section size in bytes |
sh_link | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains a section header table index link, interpretation depends on the section type. It does not seem relevant for this header type |
sh_info | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains a section header table index link, interpretation depends on the section type. It does not seem relevant for this header type |
sh_addralign | Elf32_Word | 0x00 0x00 0x00 0x01 | A value of 1 indicates that there are no alignment constraints |
sh_entsize | Elf32_Word | 0x00 0x00 0x00 0x00 | The value of 0 indicates that the section does not hold a table of fixed-size entries |
Section [0x64 - 0x75]
Not sure what to make from this section, yet…
sh_name | Elf32_Word | 0x00 0x00 0x00 0x01 | The name is defined by the string at index 1 of the string table: .symtab |
sh_type | Elf32_Word | 0x00 0x00 0x00 0x02 | SHT_SYMTAB Section that holds a symbol table |
sh_flags | Elf32_Word | 0x00 0x00 0x00 0x00 | No flags |
sh_addr | Elf32_Addr | 0x00 0x00 0x00 0x00 | A 0 seems to indicate that the section will not appear in the memory image of a process |
sh_offset | Elf32_Off | 0x00 0x00 0x00 0x78 | This is the byte offset from the beginning of the file to the first byte in the section |
sh_size | Elf32_Word | 0x00 0x00 0x00 0xa0 | Section size in bytes |
sh_link | Elf32_Word | 0x00 0x00 0x00 0x07 | The section header index of the associated string table |
sh_info | Elf32_Word | 0x00 0x00 0x00 0x09 | One greater than the symbol table index of the last local symbol (binding STB_LOCAL) |
sh_addralign | Elf32_Word | 0x00 0x00 0x00 0x04 | Some sections have address alignment constraints. In this case, I’m not yet 100% certain what this value of 4 actually represents |
sh_entsize | Elf32_Word | 0x00 0x00 0x00 0x10 | This section will hold a table of fixed-size entries, each entry will be 16 bytes |
Section [0x78 - 0x117]
This section holds a symbol table and its definition can be found here.
sh_name | Elf32_Word | 0x00 0x00 0x00 0x09 | The name is defined by the string at index 9 of the string table: .strtab |
sh_type | Elf32_Word | 0x00 0x00 0x00 0x03 | SHT_STRTAB This section holds a stringtable |
sh_flags | Elf32_Word | 0x00 0x00 0x00 0x00 | No flags |
sh_addr | Elf32_Addr | 0x00 0x00 0x00 0x00 | A 0 seems to indicate that the section will not appear in the memory image of a process |
sh_offset | Elf32_Off | 0x00 0x00 0x01 0x18 | This is the byte offset from the beginning of the file to the first byte in the section |
sh_size | Elf32_Word | 0x00 0x00 0x00 0x12 | Section size in bytes |
sh_link | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains a section header table index link, interpretation depends on the section type. It does not seem relevant for this header type |
sh_info | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains a section header table index link, interpretation depends on the section type. It does not seem relevant for this header type |
sh_addralign | Elf32_Word | 0x00 0x00 0x00 0x01 | A value of 1 indicates that there are no alignment constraints |
sh_entsize | Elf32_Word | 0x00 0x00 0x00 0x00 | The value of 0 indicates that the section does not hold a table of fixed-size entries |
Section [0x118 - 0x129] - string table
The .strtab section holds strings, typically, the names that are associated with the symbol table entries.
So in order to parse this, we slightly transform the above table into the following
0 | 00 | 24 | 61 | 00 | 6D | 73 | 67 | 00 | 24 | 64 |
10 | 00 | 5f | 73 | 74 | 61 | 72 | 74 | 00 |
Now, applying converting the hex values to their ASCII representation, we get:
0 | \0 | $ | a | \0 | m | s | g | \0 | $ | d |
10 | \0 | _ | s | t | a | r | t | \0 |
Resulting in the following string table
0 | none |
1 | $a |
4 | msg |
8 | $d |
27 | .rel.text |
11 | _start |
We specifically notice how this section header table entry is indexed by e_shstrndx. By definition, this entry is associated with the section named “string table”.
Lets have a look at what can be found inside this entry:
sh_name | Elf32_Word | 0x00 0x00 0x00 0x11 | The name is defined by the string at index 17 of the string table: .shstrtab |
sh_type | Elf32_Word | 0x00 0x00 0x00 0x03 | SHT_STRTAB So the section will be holding a string table |
sh_flags | Elf32_Word | 0x00 0x00 0x00 0x00 | No flags |
sh_addr | Elf32_Addr | 0x00 0x00 0x00 0x00 | A 0 seems to indicate that the section will not appear in the memory image of a process |
sh_offset | Elf32_Off | 0x00 0x00 0x01 0x34 | This is the byte offset from the beginning of the file to the first byte in the section |
sh_size | Elf32_Word | 0x00 0x00 0x00 0x40 | Section size in bytes |
sh_link | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains a section header table index link, interpretation depends on the section type. It does not seem relevant though for an sh_type of SHT_STRTAB |
sh_info | Elf32_Word | 0x00 0x00 0x00 0x00 | Contains extra information whose interpretation depends on the section type. It does not seem relevant though for an sh_type of SHT_STRTAB |
sh_addralign | Elf32_Word | 0x00 0x00 0x00 0x01 | A value of 1 indicates that there are no alignment constraints |
sh_entsize | Elf32_Word | 0x00 0x00 0x00 0x00 | The value of 0 indicates that the section does not hold a table of fixed-size entries |
However, in order to have a better undertanding of how the indexing into this table will work, I’ll slightly rewrite this as follows. It can be noticed how the indexes is determined by
0 | 00 | 2E | 73 | 79 | 6D | 74 | 61 | 62 | 00 | 2E |
10 | 73 | 74 | 72 | 74 | 61 | 62 | 00 | 2E | 73 | 68 |
20 | 73 | 74 | 72 | 74 | 61 | 62 | 00 | 2E | 72 | 65 |
30 | 6C | 2E | 74 | 65 | 78 | 74 | 00 | 2E | 64 | 61 |
40 | 74 | 61 | 00 | 2E | 62 | 73 | 73 | 00 | 2E | 41 |
50 | 52 | 4D | 2E | 61 | 74 | 74 | 72 | 69 | 62 | 75 |
60 | 74 | 65 | 73 | 00 |
And when converting the hex values into there ASCII representation, we have:
0 | \0 | . | s | y | m | t | a | b | \0 | . |
10 | s | t | r | t | a | b | \0 | . | s | h |
20 | s | t | r | t | a | b | \0 | . | r | e |
30 | l | . | t | e | x | t | \0 | . | d | a |
40 | t | a | \0 | . | b | s | s | \0 | . | A |
50 | R | M | . | a | t | t | r | i | b | u |
60 | t | e | s | \0 |
And in order to have an easy and quick look-up table:
0 | none |
1 | .symtab |
9 | .strtab |
17 | .shstrtab |
27 | .rel.text |
31 | .text |
37 | .data |
43 | .bss |
48 | .ARM.attributes |
Please note how you can just index whereever you want in the string table to define a string. Here, at index 31, the string .text is actually a substring of .rel.text.
Verifying with Readelf!
Luckly there is no need to parse everything by hand, the readelf command parses the ELF file for you and provides some readable output.
Returns the following
Which provides a nice summary of everything that was decoded by hand above!
Inspecting this object file has taken me deep into the ELF format. Even this only scratches the surface, it already helped me to have a better understanding of what the assembler is doing. I would certainly still like to have a better understanding of how the assembler instructions are converted in a bitwise representation and the resulting .text section. I’m curious how the GNU linker and GNU debugger will modify these binary files!
This was Steven documenting his embedded journeys! If you’ve got questions, want to share your own assembly experiences, or spot something I got wrong — drop a comment or reach out. I’d love to hear from you!
Coming up: The GNU linker, and the debugger! Or might as well get a bit more hands-on with a distance sensor :)