This chapter contains the following sections:
Structure of a Linker Script File
Syntax of the Linker Script Language
Identifiers
Expressions
Built-in Functions
LSL Definitions in the Linker Script File
Memory and Bus Descriptions
Architecture Definition
Derivative Definition
Processor Definition and Board Specification
Section Placement Definition
Semantics of the Architecture Definition
Defining an Architecture
Defining Internal Busses
Defining Address Spaces
Mappings
Semantics of the Derivative Definition
Defining a Derivative
Instantiating Core Architectures
Defining Internal Memory and Busses
Semantics of the Board Specification
Defining a Processor
Instantiating Installed Processors
Defining External Memory and Busses
Semantics of the Section Layout Definition
Defining a Section Layout
Creating and Locating Groups of Sections
Creating or Modifying Special Sections
Conditional Group Statements
To make full use of the linker, you can write a script with information about the architecture of the target processor and locating information. The language for the script is called the Linker Script Language (LSL). This chapter first describes the structure of an LSL file. The next section contains a summary of the LSL syntax. Finally, in the remaining sections, the semantics of the Linker Script Language is explained.
The TASKING linker is a target independent linker/locator that can simultaneously link and locate all programs for all cores available on a target board. The target board may be of arbitrary complexity. A simple target board may contain one standard processor with some external memory that executes one task. A complex target board may contain multiple standard processors and DSPs combined with configurable IP-cores loaded in an FPGA. Each core may execute a different program, and external memory may be shared by multiple cores.
LSL serves two purposes. First it enables you to specify the characteristics (that are of interest to the linker) of your specific target board and of the cores installed on the board. Second it enables you to specify how sections should be located in memory.
A script file generally consists of the following parts:
In essence an architecture definition describes how the linker should convert virtual addresses into physical addresses for a given type of core. If the core supports multiple address spaces, then for each space the linker must know how to perform this conversion. In this context a physical address is an offset on a given internal or external bus. Additionally the architecture definition contains information about items such as the (hardware) stack and the interrupt vector table.
Typically an architecture definition is written by Altium and should not be changed by you unless you also modify a core's hardware architecture. If the LSL file describes a multi-core system an architecture definition must be available for each different type of core.
See section 7.5
, Semantics of the Architecture Definition for detailed descriptions of LSL in the architecture definition.
The derivative definition describes the configuration of the internal (on-chip) bus and memory system. Basically it tells the linker how to convert offsets on the busses specified in the architecture definition into offsets in internal memory. A derivative definition must be present in an LSL file. Microcontrollers and DSPs often have internal memory and I/O sub-systems apart from one or more cores. The design of such a chip is called a derivative. Altium provides LSL descriptions of supported derivatives, along with "SFR files", which provide easy access to registers in I/O sub-systems from C and assembly programs. When you build an ASIC or use a derivative that is not (yet) supported by the TASKING tools, you may have to write a derivative definition.
See section 7.6
, Semantics of the Derivative Definition for a detailed description of LSL in the derivative definition.
The processor definition describes an instance of a derivative. Typically the processor definition instantiates one derivative only (single-core processor). A processor that contains multiple cores having the same (homogeneous) or different (heterogeneous) architecture can also be described by instantiating multiple derivatives of the same or different types in separate processor definitions.
See section 7.7
, Semantics of the Board Specification for a detailed description of LSL in the processor definition.
Memory and bus definition are used within the context of a derivative definition to specify internal memory and on-chip busses. In the context of a board specification the memory and bus definitions are used to define external (off-chip) memory and busses. Given the above definitions the linker can convert a logical address into an offset into an on-chip or off-chip memory device.
See section 7.7.3
, Defining External Memory and Busses, for more information on how to specify the external physical memory layout. Internal
memory for a processor should be defined in the derivative definition for that
processor.
The processor definition and memory and bus definitions together form a board specification. LSL provides language constructs to easily describe single-core and heterogeneous or homogeneous multi-core systems. The board specification describes all characteristics of your target board's system busses, memory devices, I/O sub-systems, and cores that are of interest to the linker. Based on the information provided in the board specification the linker can for each core:
The optional section layout definition enables you to exactly control where input sections are located. Features are provided such as: the ability to place sections at a given load-address or run-time address, to place sections in a given order, and to overlay code and/or data sections.
Which object files (sections) constitute the task that will run on a given core is specified on the command line when you invoke the linker. The linker will link and locate all sections of all tasks simultaneously. From the section layout definition the linker can deduce where a given section may be located in memory, form the board specification the linker can deduce which physical memory is (still) available while locating the section.
See section 7.8
, Semantics of the Section Layout Definition,, for more information on how to locate a section at a specific place in memory.
The skeleton of a linker script file now looks as follows:
architecture architecture_name { architecture definition }
derivative derivative_name { derivative definition }
processor processor_name { processor definition }
memory definitions and/or bus definitions
section_layout space_name { section placement statements }
The following lexicon is used to describe the syntax of the Linker Script Language:
A ::= B = A is defined as B A ::= B C = A is defined as B and C; B is followed by C A ::= B | C = A is defined as B or C <B>0|1 = zero or one occurrence of B <B>>=0 = zero of more occurrences of B <B>>=1 = one of more occurrences of B
IDENTIFIER = a character sequence starting with 'a'-'z', 'A'-'Z', _ , . or @ STRING = sequence of characters not starting with \n, \r or \t DQSTRING = " STRING " (double quoted string) OCT_NUM = octal number, starting with a zero (06, 045) DEC_NUM = decimal number, not starting with a zero (14, 1024) HEX_NUM = hexadecimal number, starting with '0x' (0x0023, 0xFF00)
OCT_NUM, DEC_NUM and HEX_NUM can be followed by a k (kilo), M (mega), or G (giga).
Characters in bold are characters that occur literally. Words in italics are higher order terms that are defined in the same or in one of the other sections.
arch_name ::= IDENTIFIER bus_name ::= IDENTIFIER core_name ::= IDENTIFIER derivative_name ::= IDENTIFIER file_name ::= DQSTRING group_name ::= IDENTIFIER mem_name ::= IDENTIFIER proc_name ::= IDENTIFIER section_name ::= DQSTRING space_name ::= IDENTIFIER stack_name ::= section_name symbol_name ::= DQSTRING
The expressions and operators in this section work the same as in ANSI C.
number ::= OCT_NUM | DEC_NUM | HEX_NUM
assignment ::= symbol_name assign_op expr ;
assign_op ::= = | :=
expr ::= number | symbol_name | unary_op expr | expr binary_op expr | expr ? expr : expr | ( expr ) | function_call
unary_op ::= ! // logical NOT | ~ // bitwise complement | - // negative value
binary_op ::= ^ // exclusive OR | * // multiplication | / // division | % // modulus | + // addition | - // subtraction | >> // right shift | << // left shift | == // equal to | != // not equal to | > // greater than | < // less than | >= // greater than or equal to | <= // less than or equal to | & // bitwise AND | | // bitwise OR | && // logical AND | || // logical OR
function_call ::= absolute ( expr ) | addressof ( addr_id ) | max ( expr , expr ) | min ( expr , expr ) | sizeof ( size_id )
addr_id ::= sect : section_name | mem : mem_name | group : group_name
size_id ::= sect : section_name | group : group_name
You can use the following built-in functions in expressions. All functions return a numerical value. This value is a 64-bit signed integer.
int absolute( expr )
Converts the value of expr to a positive integer.
absolute( "labelA"-"labelB" )
int addressof( addr_id )
Returns the address of addr_id, which is a named section or memory. To get the offset of the section with the name asect:
addressof( sect: "asect")
This function only works in assignments.
int max( expr, expr )
Returns the value of the expression that has the largest value. To get the highest value of two symbols:
max( "sym1" , "sym2")
int min( expr, expr )
Returns the value of the expression hat has the smallest value. To get the lowest value of two symbols:
min( "sym1" , "sym2")
int sizeof( size_id )
Returns the size of a section or group in an object file. To get the size of the section "asection":
sizeof( sect: "asection" )
This function only works in assignments.
description ::= <definition>>=1
definition ::= architecture_definition | derivative_definition | board_spec | section_definition
mem_def ::= memory mem_name { <mem_descr ;>>=0 }
mem_descr ::= type = mem_type | mau = expr | size = expr | speed = number | mapping
mem_type ::= rom // attrs = rx | ram // attrs = rw | nvram // attrs = rwx
bus_def ::= bus bus_name { <bus_descr ;>>=0 }
bus_descr ::= mau = expr | width = expr // bus width, nr | // of data bits | mapping // legal destination // 'bus' only
mapping ::= map ( map_descr <, map_descr>>=0 )
map_descr ::= dest = destination | dest_dbits = range | dest_offset = expr | size = expr | src_dbits = range | src_offset = expr
destination ::= space : space_name | bus : <proc_name | core_name :>0|1 bus_name
- space => space
- space => bus
- bus => bus
- memory => bus
range ::= number .. number
architecture_definition ::= architecture arch_name <extends arch_name>0|1 { arch_spec>=0 }
arch_spec ::= bus_def | space_def | endianness_def
space_def ::= space space_name { <space_descr;>>=0 }
space_descr ::= space_property ; | section_definition //no space ref
space_property ::= id = number // as used in object | mau = expr | align = expr | page_size = expr | stack_def | heap_def | copy_table_def | start_address | mapping
stack_def ::= stack stack_name ( stack_heap_descr <, stack_heap_descr >>=0 )
heap_def ::= heap heap_name ( stack_heap_descr <, stack_heap_descr >>=0 )
copy_table_def ::= copytable ( copy_table_descr <, copy_table_descr>>=0 )
stack_heap_descr ::= min_size = expr | grows = direction | align = expr
direction ::= low_to_high | high_to_low
copy_table_descr ::= align = expr | copy_unit = expr | dest = space_name
start_addr ::= start_address ( start_addr_descr <, start_addr_descr>>=0 )
start_addr_descr ::= run_addr = expr | section = section_name
endianness_def ::= endianness { <endianness_type;>>=1 }
endianness_type ::= big | little
derivative_definition ::= derivative derivative_name <extends derivative_name>0|1 { <derivative_spec>>=0 }
derivative_spec ::= core_def | bus_def | mem_def
core_def ::= core core_name { <core_descr ;>>=0 }
core_descr ::= architecture = arch_name | endianness = ( endianness_type <, endianness_type>>=0 )
board_spec ::= proc_def | bus_def | mem_def
proc_def ::= processor proc_name { proc_descr ; }
proc_descr ::= derivative = derivative_name
section_definition ::= section_layout <space_ref>0|1 <( space_props )>0|1 { <section_statement>>=0 }
space_ref ::= <proc_name>0|1 : <core_name>0|1 : space_name
space_props ::= space_property <, space_property>>=0
space_property ::= locate_direction
locate_direction ::= direction = direction
direction ::= low_to_high | high_to_low
section_statement ::= simple_section_statement ; | aggregate_section_statement
simple_section_statement ::= assignment | if_statement | select_section_statement | special_section_statement
aggregate_section_statement ::= { <section_statement>>=0 } | group_descr
select_section_statement ::= select <section_name>0|1 <section_selections>0|1
section_selections ::= ( section_selection <, section_selection>>=0 )
section_selection ::= attributes = < <+|-> attribute>>0
if_statement ::= if ( expr ) section_statement <else section_statement>0|1
special_section_statement ::= heap stack_name <size_spec>0|1 | stack stack_name <size_spec>0|1 | copytable | reserved <section_name>0|1 <size_spec>0|1
size_spec ::= ( size = expr )
group_descr ::= group <group_name>0|1 <( group_specs )>0|1 section_statement group_specs ::= group_spec <, group_spec >>=0
group_spec ::= group_alignment | attributes | group_load_address | group_page | group_run_address | group_type | allow_cross_references
group_alignment ::= align = expr
attributes ::= attributes = <attribute>>=1
group_load_address ::= load_addr load_or_run_addr_assignment
group_page ::= page <= expr>0|1
group_run_address ::= run_addr load_or_run_addr_assignment
group_type ::= clustered | contiguous | ordered | overlay
attribute ::= r // read-only sections | w // read/write sections | x // executable code sections | i // initialized sections | s // scratch sections | b // blanked (cleared) sections
load_or_run_addr_assignment ::= <= load_or_run_addr>0|1
load_or_run_addr ::= expr | memory_reference < | memory_reference >>=0
memory_reference ::= mem : <proc_name :>0|1 <core_name :>0|1 mem_name
Only constant expressions are allowed, including sizes, but not addresses, of sections in object files.
All expressions are evaluated with 64-bit precision integer arithmetic. The result of an expression can be absolute or relocatable. A symbol you assign is created as an absolute symbol.
architecture extends bus mau width map space id mau align page_size stack min_size grows low_to_high high_to_low align heap min_size grows low_to_high high_to_low align copytable align copy_unit dest start_address run_addr section map
map dest bus space dest_dbits dest_offset size src_dbits src_offset
With the keyword architecture you define an architecture and assign a unique name to it. The name is used to refer to it at other places in the LSL file:
architecture name { definitions }
If you are defining multiple core architectures that show great resemblance, you can define the common features in a parent core architecture and extend this with a child core architecture that contains specific features. The child inherits all features of the parent. With the keyword extends you create a child core architecture:
architecture name_child_arch extends name_parent_arch { definitions }
With the bus keyword you define a bus (the combination of data and corresponding address bus). The bus name is used to identify a bus and does not conflict with other identifiers. Bus descriptions in an architecture definition or derivative definition define internal busses. Some internal busses are used to communicate with the components outside the core or processor. Such busses on a processor have physical pins reserved for the number of bits specified with the width statements.
bus bus_name { mau = 8; width = 8; map ( map_description ); }
With the space keyword you define a logical address space. The space name is used to identify the address space and does not conflict with other identifiers.
See section 7.8
, Semantics of the Section Layout Definition for information on creating and placing stack sections.
space space_name { id = Y1; mau = 8; align = 8; page_size = 1; stack name (min_size = 1k, grows = low_to_high); start_address ( run_addr = 0x0000, section = "start_section_name" ) map ( map_description ); }
You can use a mapping when you define a space, bus or memory. With the map field you specify how addresses from the source (space, bus or memory) are translated to addresses of a destination (space, bus). The following mappings are possible:
With a mapping you specify a range of source addresses you want to map (specified by a source offset and a size), the destination to which you want to map them (a bus or another address space), and the offset address in the destination.
If you are mapping a bus to another bus, the number of data lines of each bus may differ. In this case you have to specify a range of source data lines you want to map (src_dbits = begin..end) and the range of destination data lines you want to map them to (dest_dbits = first..last).
All spaces that are not mapped to another space must map to a bus in the architecture:
space large { id = Y1; mau = 4; map (src_offset = 0, dest_offset = 0, dest = bus:bus_name, size = 16M ); }
If you map an address space to another address space (nesting), you can do this by mapping the subspace to the containing larger space. In this example a small space of 64k is mapped on a large space of 16M.
space small { id = Y2; mau = 4; map (src_offset = 0, dest_offset = 0, dest = space : large, size = 64k); }
The next example maps an external bus called e_bus to an internal bus called i_bus. This internal bus resides on a core called mycore. The source bus has 16 data lines whereas the destination bus has only 8 data lines. Therefore, the keywords src_dbits and dest_dbits specify which source data lines are mapped on which destination data lines.
architecture mycore { bus i_bus { mau = 4; } space i_space { map (dest=bus:i_bus, size=256); } }
bus e_bus { mau = 16; width = 16; map (dest = bus : mycore : i_bus, src_dbits = 0 .. 7, dest_dbits = 0 .. 7 ) }
It is not possible to map an internal bus to an external
bus.
derivative extends core architecture bus mau width map memory type rom ram nvram mau size speed map
map dest bus space dest_dbits dest_offset size src_dbits src_offset
With the keyword derivative you define a derivative and assign a unique name to it. The name is used to refer to it at other places in the LSL file:
derivative name { definitions }
If you are defining multiple derivatives that show great resemblance, you can define the common features in a parent derivative and extend this with a child derivative that contains specific features. The child inherits all features of the parent (cores and memories). With the keyword extends you create a child derivative:
derivative name_child_deriv extends name_parent_deriv { definitions }
With the keyword core you instantiate a core architecture in a derivative.
core mycore_1 { architecture = mycorearch; }
core mycore_2 { architecture = mycorearch; }
With the memory keyword you define physical memory that is present on the target board. The memory name is used to identify the memory and does not conflict with other identifiers. It is common to define internal memory (on-chip) in the derivative definition. External memory (off-chip memory) is usually defined in the board specification (See section 7.7.3, Defining External Memory and Busses).
- rom: read only memory
- ram: random access memory
- nvram: non volatile ram
memory mem_name { type = rom; mau = 8; size = 64k; speed = 2; map ( map_description ); }
With the bus keyword you define a bus in a derivative definition. Busses are described in section 7.5.2 , Defining Internal Busses.
processor derivative bus mau width map memory type rom ram nvram mau size speed map
map dest bus space dest_dbits dest_offset size src_dbits src_offset
If you have a target board with multiple processors that have the same derivative, you need to instantiate each individual processor in a processor definition. This information tells the linker which processor has which derivative and enables the linker to distinguish between the present processors.
If you use processors that all have a unique derivative,
you may omit the processor definitions. In this case the linker assumes that
for each derivative definition in the LSL file there is one processor. The
linker uses the derivative name also for the processor.
With the keyword processor you define a processor. You can freely choose the processor name. The name is used to refer to it at other places in the LSL file:
processor proc_name { processor definition }
With the keyword derivative you tell the linker that the given processor has a certain derivative. The derivative name refers to an existing derivative definition in the same LSL file.
For examples, if you have two processors on your target board (called myproc_1 and myproc_2) that have the same derivative (called myderiv), you must instantiate both processors as follows:
processor myproc_1 { derivative = myderiv; }
processor myproc_2 { derivative = myderiv; }
It is common to define external memory (off-chip) and external busses at the global scope (outside any enclosing definition). Internal memory (on-chip memory) is usually defined in the scope of a derivative definition.
With the keyword memory you define physical memory that is present on the target board. The memory name is used to identify the memory and does not conflict with other identifiers. If you define memory parts in the LSL file, only the memory defined in these parts is used for placing sections.
If no external memory is defined in the LSL file and if the linker option to allocate memory on demand is set then the linker will assume that all virtual addresses are mapped on physical memory. You can override this behavior by specifying one or more memory definitions.
memory mem_name { type = rom; mau = 8; size = 64k; speed = 2; map ( map_description ); }
For a description of the keywords,
see section 7.6.3,
Defining Internal Memory and Busses.
With the keyword bus you define a bus (the combination of data and corresponding address bus). The bus name is used to identify a bus and does not conflict with other identifiers. Bus descriptions at the global scope (outside any definition) define external busses. These are busses that are present on the target board.
bus bus_name { mau = 8; width = 8; map ( map_description ); }
For a description of the keywords,
see section 7.5.2,
Defining Internal Busses.
You can connect off-chip memory to any derivative: you need to map the off-chip memory to a bus and map that bus on the internal bus of the derivative you want to connect it to.
section_layout direction low_to_high high_to_low group align attributes + - r w x b i s ordered clustered contiguous overlay allow_cross_references load_addr mem run_addr mem page select heap size stack size reserved size copytable
if else
With the keyword section_layout you define a section layout for exactly one address space. In the section layout you can specify how input sections are placed in the address space, relative to each other, and what the absolute run and load addresses of each section will be.
You can define one or more section definitions. Each section definition arranges the sections in one address space. You can precede the address space name with a processor name and/or core name, separated by colons. You can omit the processor name and/or the core name if only one processor is defined and/or only one core is present in the processor. A reference to a space in the only core of the only processor in the system would look like "::my_space". A reference to a space of the only core on a specific processor in the system could be "my_chip::my_space". The next example shows a section definition for sections in the my_space address space of the processor called my_chip:
section_layout my_chip::my_space ( space_props ) { section statements }
With the optional keyword direction you specify whether the linker starts locating sections from low_to_high (default) or from high_to_low. In the second case the linker starts locating sections at the highest addresses in the address space but preserves the order of sections when necessary (one processor and core in this example).
section_layout ::my_space ( direction = high_to_low ) { section statements }
If you do not explicitly tell the linker how to locate
a section, the linker decides on the basis of the section attributes in the object
file and the information in the architecture definition and memory parts
where to locate the section.
Sections are located per group. A group can contain one or more (sets of) input sections as well as other groups. Per group you can assign a mutual order to the sets of sections and locate them into a specific memory part.
group ( group_specifications ) { section_statements }
With the section_statements you generally select sets of sections to form the group. This is described in subsection Selecting sections for a group.
Instead of selecting sections, you can also modify special sections like stack and heap or create a reserved section. This is described in section 7.8.3, Creating or Modifying Special Sections.
With the group_specifications you actually locate the sections in the group. This is described in subsection Locating a group.
With the select keyword you can select one or more sections for the group. You can select a section by name or by attributes. If you select a section by name, you can use a wildcard pattern:
group ( ... ) { select ".mysection"; select "*"; }
The first select statement selects the section with the name ".mysection". The second select statement selects all sections that were not selected yet.
A section is selected by the first select statement that matches, in the union of all section layouts for the address space. Global section layouts are processed in the order in which they appear in the LSL file. Internal core architecture section layouts always take precendence over global section layouts.
- r readable sections
- w readable/writable sections
- x executable sections
- i initialized sections
- b sections that should be cleared at program startup (BSS)
- s scratch sections (not cleared and not initialized)
To select all read-only sections:
group ( ... ) { select (attributes = +r); }
Keep in mind that all section selections are restricted
to the address space of the section layout in which this group definition
occurs.
group group_name ( group_specifications ) { section_statements }
With the group_specifications you actually define how the locator must locate the group. You can roughly define three things: 1) assign properties to the group like alignment and read/write attributes, 2) define the mutual order in the address space for sections in the group and 3) assign a load-time address or run-time address to the group.
The linker creates labels that allow you to refer to the begin and end address of a group from within the application software. Labels _lc_gb_group_name and _lc_ge_group_name mark the begin and end of the group respectively, where the begin is the lowest address used within this group and the end is the highest address used. Notice that a group not necessarily occupies all memory between begin and end address. The given label refers to where the section is located at run-time (versus load-time).
1. Assign properties to the group like alignment and read/write attributes. These properties are assigned to all sections in the group (and subgroups) and override the attributes of the input sections.
2. Define the mutual order of the sections in the group. By default, a group is unrestricted which means that the linker has total freedom to place the sections of the group in the address space.
group ovl (overlay) { group a { select "my_ovl_p1"; select "my_ovl_p2"; } group b { select "my_ovl_q1"; } }
It may be possible that one of the sections in the overlay group already has been defined in another group where it received
a load-time address. In this case the linker does not overrule this load-time address and excludes the section from the overlay group.
3.
Assign a load-time address or run-time
address to the group.
By default, the position and properties of a group in an LSL file
specify the run-time addresses of its sections. The linker uses the addresses
defined by the run-time addresses when it patches symbol references in the
object code. It may be possible that you want to assign a different load-time
and run-time address to a group. The load-time address specifies where
the group's elements are loaded in at download time. The run-time address specifies where sections are located at run-time, that is when the
program is executing. The program is responsible for copying overlay sections
at appropriate moment from its load-time location to its run-time location.
group (run_addr = 0xa00f0000)
group (run_addr = mem:my_dram)
group (run_addr = mem:A | mem:B)
group (run_addr)
group (load_addr = 0x00000000)
group (load_addr = mem:my_ram, ...)
group (load_addr)
The load-time and run-time addresses of a group cannot be set at the same time. If the load-time property is set for a group, the group (only) restricts the positioning at load-time of the group's sections. It is not allowed to set the address of a group that has a not-unrestricted parent group.
The properties of the load-time and run-time start address are:
For overlays, the linker reserves memory at the run-time start address as large as the largest element in the overlay group.
group (page, ... ) group (page = 3, ...)
Instead of selecting sections, you can also create a reserved section or modify special sections like a stack or a heap, a reserved section. Because you cannot define these sections in the input files, you must use the linker to create them.
group ( ... ) { stack "mystack" ( size = 2k ); }
group ( ... ) { heap "myheap" ( size = 2k ); }
group ( ... ) { reserved "myreserved" ( size = 2k ); }
Within a group, you can conditionally select sections or create special sections.
group ( ... ) { if ( size_of ( sect:.mysection ) < 2k ) select ".mysection"; else select ".othersection"; }