52938.fb2
Chapter 1 states that one characteristic of embedded systems is the cross-platform development methodology. The primary components in the development environment are the host system, the target embedded system, and potentially many connectivity solutions available between the host and the target embedded system, as shown in Figure 2.1.
Figure 2.1: Typical cross-platform development environment.
The essential development tools offered by the host system are the cross compiler, linker, and source-level debugger. The target embedded system might offer a dynamic loader, a link loader, a monitor, and a debug agent. A set of connections might be available between the host and the target system. These connections are used for downloading program images from the host system to the target system. These connections can also be used for transmitting debugger information between the host debugger and the target debug agent.
Programs including the system software, the real-time operating system (RTOS), the kernel, and the application code must be developed first, compiled into object code, and linked together into an executable image. Programmers writing applications that execute in the same environment as used for development, called native development, do not need to be concerned with how an executable image is loaded into memory and how execution control is transferred to the application. Embedded developers doing cross-platform development, however, are required to understand the target system fully, how to store the program image on the target embedded system, how and where to load the program image during runtime, and how to develop and debug the system iteratively. Each of these aspects can impact how the code is developed, compiled, and most importantly linked.
The areas of focus in this chapter are
· the ELF object file format,
· the linker and linker command file, and
· mapping the executable image onto the target embedded system.
This chapter does not provide full coverage on each tool, such as the compiler and the linker, nor does this chapter fully describe a specific object file format. Instead, this chapter focuses on providing in-depth coverage on the aspects of each tool and the object file format that are most relevant to embedded system development. The goal is to offer the embedded developer practical insights on how the components relate to one another. Knowing the big picture allows an embedded developer to put it all together and ask the specific questions if and when necessary.
Figure 2.2 illustrates how different tools take various input files and generate appropriate output files to ultimately be used in building an executable image.
Figure 2.2: Creating an image file for the target system.
The developer writes the program in the C/C++ source files and header files. Some parts of the program can be written in assembly language and are produced in the corresponding assembly source files. The developer creates a makefile for the make utility to facilitate an environment that can easily track the file modifications and invoke the compiler and the assembler to rebuild the source files when necessary. From these source files, the compiler and the assembler produce object files that contain both machine binary code and program data. The archive utility concatenates a collection of object files to form a library. The linker takes these object files as input and produces either an executable image or an object file that can be used for additional linking with other object files. The linker command file instructs the linker on how to combine the object files and where to place the binary code and data in the target embedded system.
The main function of the linker is to combine multiple object files into a larger relocatable object file, a shared object file, or a final executable image. In a typical program, a section of code in one source file can reference variables defined in another source file. A function in one source file can call a function in another source file. The global variables and non-static functions are commonly referred to as global symbols. In source files, these symbols have various names, for example, a global variable called foo_bar or a global function called func_a. In the final executable binary image, a symbol refers to an address location in memory. The content of this memory location is either data for variables or executable code for functions.
The compiler creates a symbol table containing the symbol name to address mappings as part of the object file it produces. When creating relocatable output, the compiler generates the address that, for each symbol, is relative to the file being compiled. Consequently, these addresses are generated with respect to offset 0. The symbol table contains the global symbols defined in the file being compiled, as well as the external symbols referenced in the file that the linker needs to resolve. The linking process performed by the linker involves symbol resolution and symbol relocation.
Symbol resolution is the process in which the linker goes through each object file and determines, for the object file, in which (other) object file or files the external symbols are defined. Sometimes the linker must process the list of object files multiple times while trying to resolve all of the external symbols. When external symbols are defined in a static library, the linker copies the object files from the library and writes them into the final image.
Symbol relocation is the process in which the linker maps a symbol reference to its definition. The linker modifies the machine code of the linked object files so that code references to the symbols reflect the actual addresses assigned to these symbols. For many symbols, the relative offsets change after multiple object files are merged. Symbol relocation requires code modification because the linker adjusts the machine code referencing these symbols to reflect their finalized addresses. The relocation table tells the linker where in the program code to apply the relocation action. Each entry in the relocation table contains a reference to the symbol table. Using this reference, the linker can retrieve the actual address of the symbol and apply it to the program location as specified by the relocation entry. It is possible for the relocation table to contain both the address of the symbol and the information on the relocation entry. In this case, there is no reference between the relocation table and the symbol table.
Figure 2.3 illustrates these two concepts in a simplified view and serves as an example for the following discussions.
Figure 2.3: Relationship between the symbol table and the relocation table.
For an executable image, all external symbols must be resolved so that each symbol has an absolute memory address because an executable image is ready for execution. The exception to this rule is that those symbols defined in shared libraries may still contain relative addresses, which are resolved at runtime (dynamic linking).
A relocatable object file may contain unresolved external symbols. Similar to a library, a linker-reproduced relocatable object file is a concatenation of multiple object files with one main difference-the file is partially resolved and is used for further linking with other object files to create an executable image or a shared object file. A shared object file has dual purposes. It can be used to link with other shared object files or relocatable object modules, or it can be used as an executable image with dynamic linking.
Typically an object file contains
· general information about the object file, such as file size, binary code and data size, and source file name from which it was created,
· machine-architecture-specific binary instructions and data
· symbol table and the symbol relocation table, and
· debug information, which the debugger uses.
The manner in which this information is organized in the object file is the object file format. The idea behind a standard object file format is to allow development tools which might be produced by different vendors-such as a compiler, assembler, linker, and debugger that conform to the well-defined standard-to interoperate with each other.
This interoperability means a developer can choose a compiler from vendor A to produce object code used to form a final executable image by a linker from vendor B. This concept gives the end developer great flexibility in choice for development tools because the developer can select a tool based on its functional strength rather than its vendor.
Two common object file formats are the common object file format (COFF) and the executable and linking format (ELF). These file formats are incompatible with each other; therefore, be sure to select the tools, including the debugger, that recognize the format chosen for development.
We focus our discussion on ELF because it supersedes COFF. Understanding the object file format allows the embedded developer to map an executable image into the target embedded system for static storage, as well as for runtime loading and execution. To do so, we need to discuss the specifics of ELF, as well as how it relates to the linker.
Using the ELF object file format, the compiler organizes the compiled program into various system-defined, as well as user-defined, content groupings called sections. The program's binary instructions, binary data, symbol table, relocation table, and debug information are organized and contained in various sections. Each section has a type. Content is placed into a section if the section type matches the type of the content being stored.
A section also contains important information such as the load address and the run address. The concept of load address versus run address is important because the run address and the load address can be different in embedded systems. This knowledge can also be helpful in understanding embedded system loader and link loader concepts introduced in Chapter 3.
Chapter 1 discusses the idea that embedded systems typically have some form of ROM for non-volatile storage and that the software for an embedded system can be stored in ROM. Modifiable data must reside in RAM. Programs that require fast execution speed also execute out of RAM. Commonly therefore, a small program in ROM, called a loader, copies the initialized variables into RAM, transfers the program code into RAM, and begins program execution out of RAM. This physical ROM storage address is referred to as the section's load address. The section's run address refers to the location where the section is at the time of execution. For example, if a section is copied into RAM for execution, the section's run address refers to an address in RAM, which is the destination address of the loader copy operation. The linker uses the program's run address for symbol resolutions.
The ELF file format has two different interpretations, as shown in Figure 2.4. The linker interprets the file as a linkable module described by the section header table, while the loader interprets the file as an executable module described by the program header table.
Figure 2.4: Executable and linking format.
Listing 2.1 shows both the section header and the program header, as represented in C programming structures. We describe the relevant fields during the course of this discussion.
Listing 2.1: Section header and program header.
typedef struct {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
A section header table is an array of section header structures describing the sections of an object file. A program header table is an array of program header structures describing a loadable segment of an image that allows the loader to prepare the image for execution. Program headers are applied only to executable images and shared object files.
One of the fields in the section header structure is sh_type, which specifies the type of a section. Table 2.1 lists some section types.
Table 2.1: Section types.
NULL | Inactive header without a section. |
PROGBITS | Code or initialized data. |
SYMTAB | Symbol table for static linking. |
STRTAB | String table. |
RELA/REL | Relocation entries. |
HASH | Run-time symbol hash table. |
DYNAMIC | Information used for dynamic linking. |
NOBITS | Uninitialized data. |
DYNSYM | Symbol table for dynamic linking. |
The sh_flags field in the section header specifies the attribute of a section. Table 2.2 lists some of these attributes.
Table 2.2: Section attributes.
WRITE | Section contains writeable data. |
ALLOC | Section contains allocated data. |
EXECINSTR | Section contains executable instructions. |
Some common system-created default sections with predefined names for the PROGBITS are.text,.sdata,.data,.sbss, and.bss. Program code and constant data are contained in the.text section. This section is read-only because code and constant data are not expected to change during the lifetime of the program execution. The.sbss and.bss sections contain uninitialized data. The.sbss section stores small data, which is the data such as variables with sizes that fit into a specific size. This size limit is architecture-dependent. The result is that the compiler and the assembler can generate smaller and more efficient code to access these data items. The.sdata and.data sections contain initialized data items. The small data concept described for.sbss applies to.sdata. A.text section with executable code has the EXECINSTR attribute. The.sdata and.data sections have the WRITE attribute. The.sbss and.bss sections have both the WRITE and the ALLOC attributes.
Other common system-defined sections are.symtab containing the symbol table,.strtab containing the string table for the program symbols,.shstrtab containing the string table for the section names, and.relaname containing the relocation information for the section named name. We have discussed the role of the symbol table (SYMTAB) previously. In Figure 2.3, the symbol name is shown as part of the symbol table. In practice, each entry in the symbol table contains a reference to the string table (STRTAB) where the character representation of the name is stored.
The developer can define custom sections by invoking the linker command.section. For example, where the source files states
.section my_section
the linker creates a new section called my_section. The reasons for creating custom named sections are explained shortly.
The sh_addr is the address where the program section should reside in the target memory. The p_paddr is the address where the program segment should reside in the target memory. The sh_addr and the p_paddr fields refer to the load addresses. The loader uses the load address field from the section header as the starting address for the image transfer from non-volatile memory to RAM.
For many embedded applications, the run address is the same as the load address. These embedded applications are directly downloaded into the target system memory for immediate execution without the need for any code or data transfer from one memory type or location to another. This practice is common during the development phase. We revisit this topic in Chapter 3, which covers the topic of image transfer from the host system to the target system.
After multiple source files (C/C++ and assembly files) have been compiled and assembled into ELF object files, the linker must combine these object files and merge the sections from the different object files into program segments. This process creates a single executable image for the target embedded system. The embedded developer uses linker commands (called linker directives) to control how the linker combines the sections and allocates the segments into the target system. The linker directives are kept in the linker command file. The ultimate goal of creating a linker command file is for the embedded developer to map the executable image into the target system accurately and efficiently.
The format of the linker command file, as well as the linker directives, vary from linker to linker. It is best to consult the programmer’s reference manual from the vendor for specific linker commands, syntaxes, and extensions. Some common directives, however, are found among the majority of the available linkers used for building embedded applications. Two of the more common directives supported by most linkers are MEMORY and SECTION.
The MEMORY directive can be used to describe the target system’s memory map. The memory map lists the different types of memory (such as RAM, ROM, and flash) that are present on the target system, along with the ranges of addresses that can be accessed for storing and running an executable image. An embedded developer needs to be familiar with the addressable physical memory on a target system before creating a linker command file. One of the best ways to do this process, other than having direct access to the hardware engineering team that built the target system, is to look at the target system’s schematics, as shown in Figure 2.5, and the hardware documentation. Typically, the hardware documentation describes the target system’s memory map.
Figure 2.5: Simplified schematic and memory map for a target system.
The linker combines input sections having the same name into a single output section with that name by default. The developer-created, custom-named sections appear in the object file as independent sections. Sometimes developers might want to change this default linker behavior of only coalescing sections with the same name. The embedded developer might also need to instruct the linker on where to map the sections, in other words, what addresses should the linker use when performing symbol resolutions. The embedded developer can use the SECTION directive to achieve these goals.
The MEMORY directive defines the types of physical memory present on the target system and the address range occupied by each physical memory block, as specified in the following generalized syntax
MEMORY {
area-name: org = start-address, len = number-of-bytes
…
}
In the example shown in Figure 2.5, three physical blocks of memory are present:
· a ROM chip mapped to address space location 0, with 32 bytes,
· some flash memory mapped to address space location 0x40, with 4,096 bytes, and
· a block of RAM that starts at origin 0x10000, with 65,536 bytes.
Translating this memory map into the MEMORY directive is shown in Listing 2.2. The named areas are ROM, FLASH, and RAM.
Listing 2.2: Memory map.
MEMORY {
ROM: origin = 0x0000h, length = 0x0020h
FLASH: origin = 0x0040h, length = 0x1000h
RAM: origin = 0x1000h, length = 0x10000h
}
The SECTION directive tells the linker which input sections are to be combined into which output section, which output sections are to be grouped together and allocated in contiguous memory, and where to place each section, as well as other information. A general notation of the SECTION command is shown in Listing 2.3.
Listing 2.3: SECTION command.
SECTION {
output-section-name: {contents} › area-name
…
GROUP {
[ALIGN(expression)]
section-definition
…
} › area-name
}
The example shown in Figure 2.6 contains three default sections (.text,.data, and.bss), as well as two developer-specified sections (loader and my_section), contained in two object files generated by a compiler or assembler (file1.o and file2.o). Translating this example into the MEMORY directive is shown in Listing 2.4.
Figure 2.6: Combining input sections into an executable image.
Listing 2.4: Example code.
SECTION {
.text:
{
my_section
*(.text)
}
loader: › FLASH
GROUP ALIGN (4):
{
.text,
.data: {}
.bss: {}
} ›RAM
}
The SECTION command in the linker command file instructs the linker to combine the input section named my_section and the default.text sections from all object files into the final output.text section. The loader section is placed into flash memory. The sections.text,.data, and.bss are grouped together and allocated in contiguous physical RAM memory aligned on the 4-byte boundary, as shown in Figure 2.7.
Figure 2.7: Mapping an executable image into the target system.
Tips on section allocation include the following:
· allocate sections according to size to fully use available memory, and
· examine the nature of the underlying physical memory, the attributes, and the purpose of a section to determine which physical memory is best suited for allocation.
Various reasons exist why an embedded developer might want to define custom sections, as well as to map these sections into different target memory areas as shown in the last example. The following sections list some of these reasons.
Chapter 1 discusses the storage options and upgradability of software on embedded systems. Software can be easily upgraded when stored in non-volatile memory devices, such as flash devices. It is possible to upgrade the software dynamically while the system is still running. Upgrading the software can involve downloading the new program image over either a serial line or a network and then re-programming the flash memory. The loader in the example could be such an application. The initial version of the loader might be capable of transferring an image from ROM to RAM. A newer version of the loader might be capable of transferring an image from the host over the serial connection to RAM. Therefore, the loader code and data section would be created in a custom loader section. The entire section then would be programmed into the flash memory for easy upgradeability in the future.
The target system usually has different types of physical memory, but each is limited in size. At times, it is impossible to fit all of the code and data into one type of memory, for example, the SDRAM. Because SDRAM has faster access time than DRAM, it is always desirable to map code and data into it. The available physical SDRAM might not be large enough to fit everything, but plenty of DRAM is available in the system. Therefore, the strategy is to divide the program into multiple sections and have some sections allocated into the SDARM, while the rest is mapped into the DRAM. For example, an often-used function along with a frequently searched lookup table might be mapped to the SDRAM. The remaining code and data is allocated into the DRAM.
Programs usually have various types of constants, such as integer constants and string constants. Sometimes these constants are kept in ROM to avoid accidental modification. In this case, these constants are part of a special data section, which is allocated into ROM.
Consider an example system containing 256 bytes of ROM, 16KB of flash memory, and two blocks of RAM. RAMB0 is 128KB of SDRAM, and RAMB1 is 2MB of DRAM. An embedded application with a number of sections, as listed in Table 2.3, needs to be mapped into this target system.
Table 2.3: Example embedded application with sections.
Sections | Size | Attribute[1] | Description |
---|---|---|---|
_loader | 10KB | RD | Contains the loader code |
_wflash | 2KB | RD | Contains the flash memory programmer |
.rodata | 128 bytes | RD | Contains non-volatile default initialization parameters and data, such as copyright information |
.sbss | 10KB | R/W | Contains uninitialized data less than 64KB (e.g., global variables) |
.sdata | 2KB | R/W | Contains initialized data less than 64KB |
.bss | 128KB | R/W | Contains uninitialized data larger than 64KB |
.data | 512KB | R/W | Contains initialized data larger than 64KB |
_monitor | 54KB | RD | Contains the monitor code |
.text | 512KB | RD | Contains other program code |
One possible allocation is shown in Listing 2.5; it considers why an embedded engineer might want greater section allocation control.
Listing 2.5: Possible section allocation.
MEMORY {
ROM: origin = 0x00000h, length = 0x000100h
FLASH: origin = 0x00110h, length = 0x004000h
RAMB0: origin = 0x05000h, length = 0x020000h
RAMB1: origin = 0x25000h, length = 0x200000h
}
SECTION {
.rodata: › ROM
_loader: › FLASH
_wflash: › FLASH
_monitor: › RAMB0
.sbss (ALIGN 4): › RAMB0
.sdata (ALIGN 4): › RAMB0
.text: › RAMB1
.bss (ALIGN 4): › RAMB1
.data (ALIGN 4): › RAMB1
}
This program allocation is shown in Figure 2.8. The section allocation strategies applied include the following:
· The .rodata section contains system initialization parameters. Most likely these default values never change; therefore, allocate this section to ROM.
· The loader program is usually part of the system program that executes at startup. The _loader and the _wflash sections are allocated into flash memory because the loader code can be updated with new versions that understand more object formats. You need the flash memory programmer for this purpose, which can also be updated. Therefore, section _wflash is allocated into the flash memory as well.
· The embedded programmer interacts with the monitor program to probe system execution states and help debug application code; therefore, it should be responsive to user commands. SDRAM is faster than DRAM, with shorter access time. Therefore, section _monitor is allocated into RAMB0.
· RAMB0 still has space left to accommodate both sections.sbss and.sdata. The allocation strategy for these two sections is to use the leftover fast memory fully.
· The remaining sections (.text, .bss, and .data) are allocated into RAMB1, which is the only memory that can accommodate all of these large sections.
Figure 2.8: Mapping an executable image into the target system.
Some points to remember include the following:
· The linker performs symbol resolution and symbol relocation.
· An embedded programmer must understand the exact memory layout of the target system towards which development is aimed.
· An executable target image is comprised of multiple program sections.
· The programmer can describe the physical memory, such as its size and its mapping address, to the linker using the linker command file. The programmer can also instruct the linker on combining input sections into output sections and placing the output program sections using the linker command file.
· Each program section can reside in different types of physical memory, based on how the section is used. Program code (or.text section) can stay in ROM, flash, and RAM during execution. Program data (or.data section) must stay in RAM during execution.
RD = read only; R/W = readable and writeable