6. Placing code and data

When compiling for modern computers, code and data placement often requires minimal attention, as tools handle it effectively for same-machine targets.

Cross-compilation is more complex, requiring knowledge of the target system. Embedded systems are even more challenging, with widely varying memory systems necessitating precise control over code and data placement.

Effective embedded system tools provide control over code and data placement. This is achieved using building blocks that offer placement flexibility and control. The compiler assists by providing abstractions through type qualifiers, initializers, and runtime model settings, which simplify configuring the memory system.

6.1. Sections

A section is a unit of code or data. The compiler will place code and data into different sections which are saved as separate sections in the object file.

The compiler places data objects and functions into suitable sections following specific rules. For data objects, it considers if the object is const, its address space, and explicit initializers.

Table 6.1 Sections

Section name

Type

Memory kind

Description

code

text

ROM

Executable code

compactcode

text

ROM

Executable code

farcode

text

ROM

Executable code

ztiny

bss

RAM

Zero initialized (bss) direct page data

tiny

data

RAM

Initialized direct page data

zdata

bss

RAM

Zero initialized (bss) data, default memory, Small data model only

data

data

RAM

Initialized near data, default memory, Small data model only

cdata

rodata

ROM

Constant near data, default memory, Small data model only

znear

bss

RAM

Zero initialized (bss) near data, not available in Small data model

near

data

RAM

Initialized near data, not available in Small data model

cnear

rodata

ROM

Constant near data, not available in Small data model

zfar

bss

RAM

Zero initialized (bss) far data

far

data

RAM

Initialized far data

cfar

rodata

ROM

Constant far data

zhuge

bss

RAM

Zero initialized (bss) huge data

huge

data

RAM

Initialized huge data

chuge

rodata

ROM

Constant huge data

switch

rodata

ROM

Switch tables

itiny

rodata

ROM

Tiny data initializers

idata

rodata

ROM

Data initializers, default memory, Small data model only

inear

rodata

ROM

Near data initializers, not available in Small data model

ifar

rodata

ROM

Far data initializers

ihuge

rodata

ROM

huge data initializers

data_init_table

rodata

ROM

Data initializer table

registers

noinit

RAM

Pseudo registers in direct page

reset

text

ROM

Reset vector, when used

heap

noinit

RAM

Heap memory, for malloc()

stack

noinit

RAM

CPU stack, anywhere in bank 0

The sections itiny, inear, ifar, ihuge and data_init_table in the table above are linker generated.

Note

It is assumed that there is no ROM in direct page. Constants placed in tiny address space are handled in the same way as non-constant data objects in the tiny address space. The only difference is that they have the const attribute in the C type system.

Note

The table assigns sections to ROM and RAM for ROM-based applications that start on power-up. For hosted systems loading from storage to RAM, this distinction is not applicable. Always consider ROM-marked sections as read-only.

Section types are derived from the UNIX world; see Section kinds in the assembler reference for details.

Note

When generating a ROM-executable application, the linker separates an initialized data section into two. Initializers go into a new i-prefixed section (placed in ROM); the data section is placed in RAM. The C runtime startup module copies initializers from ROM to RAM before giving control to the main() function.

Linking process

The linker reads object files with sections and a placement rules file. This file describes available memory areas and their permitted sections, optionally with additional placement constraints.

Memories containing actual value bits (program code or initialized data) are emitted in the executable.

An executable can serve as input for ROM or FLASH programmers, be loaded by the target machine, or be used by a debugger that connects to or simulates the target. Various executable formats are available to suit different execution environments.

Note

Although code and data are entirely separated into different sections, placing these sections together in the same memory may be desirable. This can be achieved at the link stage.

Placing sections in memory

The linker rules file uses file extension .scm, which is actually a source file in the Scheme language. However, you do not need to learn Scheme in order to use it, as it just a simple description of available memories and their associated properties. Sections are then bound to the memories. You can also define the size of the stack and heap in this file.

A simple example of such linker rules file is:

(define memories
   '((memory banks (address (#x10000 . #x1fffff)) (type ANY))
     (memory LoRAM (address (#x2100 . #x7fff)) (type ANY))
     )))

The Scheme interpreter evaluates the rules file, which defines a single global memories object. This object lists memory and block objects. Sections can be bound to memories either by specifying the memory type or by listing the sections within each memory.

The example simplifies memory system description. The linker deduces where compiler and linker-generated sections are placed; this requires specifying the memory type. To prevent automatic section placement in a memory, omit its type specification.

Note

You can specify a memory type and manually place custom sections. This is useful for sections requiring specific placement.

Note

Since the linker rules file is a Scheme program, it can theoretically construct the memories object via functions or macros. After evaluation, the Scheme interpreter is expected to leave a memories object in its global environment. However, a simple file like the example is usually sufficient.

6.2. Data initialization

A startup routine is performed before the main() function is called. This startup routine takes care of preparing the execution environment for the C program by initializing the stack pointer, initialize global variables and prepare system resources such as file streams and the heap, if used by the application.

Static data

Static data that needs to be initialized comes in two forms, zero initialized and those that have non-zero initializers. Zero initialization is done by filling such memory ranges with zero. Explicit initialization is done by copying an initializer section in ROM to its corresponding RAM area.

This process is table driven using a section named data_init_table which should be placed in the same memory as the cdata section, which holds constants.

Note

In a hosted environment, the linker provides the --hosted command-line option. This assumes the program image loads directly into RAM from external media. Variable initialization is done in-place via the load action; therefore, separate initializers are not required.

The --target option also has the effect of enabling running in a hosted environment.

6.3. Stack

The stack stores function return addresses, saved registers, and local variables. It tracks the current execution state, enabling reentrant and recursive functions.

Stack size considerations

The stack size is defined in the linker rules file. The stack size should be set low as possible, but not any lower. If given too much space you are wasting RAM space not being used for anything. However, if set too small it will cause corruption of other data.

Exact stack use can be hard to predict and it is better to err on the side of some safety. Also during development of your application you may want to use a bit oversized stack if possible, to allow you to focus on other issues. In the end, you are most likely constrained by some fixed amount of RAM and need to balance how much you can use for the stack with the optional heap and statically allocated data.