wiki:howto/Linker_scripts_MAP_files

Understanding Linker Command Files (Linker Scripts) / MAP files

For most people, linking a program (i.e. the step that takes the compiled object files and creates an executable program) is a little bit like magic. There are a lot of automated tools that handle everything and rarely are there ever any issues. However, in complex embedded systems, it can become necessary to dig in and understand how to control how your program is linked.

For example, in the 802.11 Reference design, the upper level MAC code required CPU High to have 256 KB of LMB memory. However, XPS could not build a design that contained a single 256 KB LMB memory. The memory size was too large to meet timing given all the other logic and constraints within the system. Therefore, we had to split the 256 KB memory into two 128 KB physical memories located contiguously within the processor memory map and then use the Linker Command File (Linker Script) to control which sections of the program to load into each memory. This required editing and understanding the Linker Command File (Linker Script) and using the MAP files generated as part of the linker output.

This tutorial should give a basic understanding of Linker Command Files (Linker Scripts) and MAP files. You can find additional information on the Linker Command Files (Linker Scripts) and MAP files by searching on the Internet.

NOTE: This tutorial is based on 802.11 Reference Design v1.3.0 but the concepts should apply generically.

Linker Command Files (Linker Scripts)

Opening the Linker Command File (Linker Script):

Open the Linker Command File (Linker Script) for a project by expanding the project in the "Project Explorer" window and finding the lscript.ld file inside the src folder. By default, double-clicking the lscript.ld file will open the Linker Command File in the SDK "Linker Script Editor". Unfortunately, this editor does not give the granularity of control that is needed for advanced manipulation of Linker Command File (Linker Script). Instead, right-click on the lscript.ld file and select "Open with" --> "Text Editor". This will open the file in a generic text editor within the SDK.

If you do not have the SDK open, you can find the Linker Command File (Linker Script) for the 802.11 Reference design v1.3.0 here.

NOTE: The Linker Command File (Linker Script) used in the 802.11 Reference Design was originally generated by the SDK Linker Script Editor and then manually edited. This is why the Linker Command File (Linker Script) is included in SVN and should not be re-generated. The SDK Linker Script Editor is very useful for simple designs. However, for more complicated designs, manual editing is required.

The best practice we have found is to let the SDK Linker Script Editor generate the initial Linker Command File (Linker Script) and then manually edit where required.

Understanding the Linker Command File (Linker Script):

The Linker Command File (Linker Script) is organized into sections:

For this section, we look at the Linker Command File (Linker Script) for the AP project (i.e. wlan_mac_high_ap).

  1. The first section defines the sizes for the Stack and the Heap used by the program:
    _STACK_SIZE = DEFINED(_STACK_SIZE) ? _STACK_SIZE : 0x1000;
    _HEAP_SIZE = DEFINED(_HEAP_SIZE) ? _HEAP_SIZE : 0x4000;
    
    By default, for the AP project, the 802.11 Reference design allocates a 4 KB stack and a 16 KB heap so that there are no issues with function variables placed on the stack or dynamically allocated memory from the heap. These are larger than what the reference design actually uses so it is easy to extend the design without running into stack or heap issues, which can be difficult to debug.
  2. The next section defines the memories in the system that can be used to place code and data. These sections correspond to the physical memories that were defined in the XPS project:
    MEMORY
    {
       mb_high_ilmb_bram_cntlr_0_mb_high_dlmb_bram_cntlr_0 : ORIGIN = 0x00000050, LENGTH = 0x0001FFB0
       mb_high_ilmb_bram_cntlr_1_mb_high_dlmb_bram_cntlr_1 : ORIGIN = 0x00020000, LENGTH = 0x00020000
       mb_high_init_bram_ctrl_S_AXI_BASEADDR : ORIGIN = 0x50000000, LENGTH = 0x00001000
       pkt_buff_tx_bram_ctrl_S_AXI_BASEADDR : ORIGIN = 0xBF570000, LENGTH = 0x00010000
       pkt_buff_rx_bram_ctrl_S_AXI_BASEADDR : ORIGIN = 0xBF560000, LENGTH = 0x00010000
       mac_log_bram_ctrl_S_AXI_BASEADDR : ORIGIN = 0x90000000, LENGTH = 0x00002000
       mb_high_aux_bram_ctrl_S_AXI_BASEADDR : ORIGIN = 0xBF540000, LENGTH = 0x00010000
       ddr3_sodimm_S_AXI_BASEADDR : ORIGIN = 0xC0000000, LENGTH = 0x40000000
    }
    
    From this, you can see the two 128 KB LMB memories defined for CPU High as well as other memories such as the DDR.
  3. The next section specifies the default program entry point:
    ENTRY(_start)
    
    This should be left alone since it is part of the build configuration of the SDK.
  4. The last section defines all the Linker "Sections". A global variable or function can be placed in a give "section" by using the pragma: __attribute__ ((section (".my_data"))), where "my_data" is the name of the section to place teh code or data. By default, the SDK build setup defines a number of default sections that are used to place code and data. Looking at the ".text" section:
    .text : {
       *(.text)
       *(.text.*)
       *wlan_mac_common*(.text)
       *(.gnu.linkonce.t.*)
    } > mb_high_ilmb_bram_cntlr_0_mb_high_dlmb_bram_cntlr_0
    
    we can see a few thing:
    1. The section is placed in the memory: mb_high_ilmb_bram_cntlr_0_mb_high_dlmb_bram_cntlr_0 that was defined in the MEMORY section above.
    2. The section uses wildcards and pattern matching to assign code to that section. A couple notes on wildcards and pattern matching:
      1. Wildcards are "greedy" (i.e. they will match all code / data that has not previously been assigned). Therefore, entries like *(.text) or *(.text.*) will allocate any unallocated code and should be used after all other code has been allocated to other sections.
      2. Section order matters in the Linker Command File (Linker Script). For example, in the example Linker Command File, the .text_bram_1 section comes before the .text section so that some of the code can be allocated to mb_high_ilmb_bram_cntlr_1_mb_high_dlmb_bram_cntlr_1 vs mb_high_ilmb_bram_cntlr_0_mb_high_dlmb_bram_cntlr_0
    Next, if we look at a section like ".heap":
    .heap (NOLOAD) : {
       . = ALIGN(8);
       _heap = .;
       _heap_start = .;
       . += _HEAP_SIZE;
       _heap_end = .;
    } > mb_high_ilmb_bram_cntlr_1_mb_high_dlmb_bram_cntlr_1
    
    we can see a few things:
    1. The heap is not initialized due to the NOLOAD directive (i.e. it is initialized to the default BRAM state).
    2. The section can be aligned within the memory map using the ALIGN directive.
    3. Symbols can be declared that can be then used in the C code.
    4. _HEAP_SIZE bytes, declared above, are allocated by incrementing the current point in memory (i.e. the . += _HEAP_SIZE; line).
    5. The section is allocated in the mb_high_ilmb_bram_cntlr_1_mb_high_dlmb_bram_cntlr_1 memory.

By understanding the Linker Command File (Linker Script), it is possible to control how code and data is allocated and placed within memory. This can come in handy when there are hardware limitations to the amount of BRAM that can be allocated to a given physical memory.

Limitations on DDR:

Only on-chip BRAM can be initialized by the bitstream that is created from the linked program. Therefore, if the program needs to allocate memory in DDR using the linker, there are a some restrictions that must be placed on that memory:

  1. If the memory needs to be initialized, then it must be done so by the program itself.
  2. The initial value of the memory must be assumed to be random unless initialized by the program.

This is different from BRAM in that the bitstream will initialize BRAM to the value dictated by the program (and zero otherwise). Hence, no code can be placed in DDR, only data that can be initialized by the program. To declare a data section in DDR, it must be specified as "NOLOAD" so that the program that builds the bitstream does not try to do anything with it. For example, a data section for CPU High in the DDR would look like:

.cpu_high_ddr_linker_data (NOLOAD) : {
   __cpu_high_ddr_linker_data_start = .;
   *(.my_data)
   __cpu_high_ddr_linker_data_end = .;
} > ddr3_sodimm_S_AXI_BASEADDR

The symbols are declared so that it is easy to check programatically that the linked section does not exceed any predefined allocation of DDR, in the case that DDR is being allocated statically within the program.

MAP files

A MAP file is an output of the Linker that gives information about the symbols, addresses, and allocated memory in the generated ELF file. It is extremely useful when trying to understand and debug linker issues related to code size. For example, if you get an error from the linker similar to:

c:/xilinx/14.4/ise_ds/edk/gnu/microblaze/nt64/bin/../lib/gcc/microblaze-xilinx-elf/4.6.2/../../../../microblaze-xilinx-elf/bin/ld.exe: 
wlan_mac_high_ap.elf section `.abcd' will not fit in region `mb_high_ilmb_bram_cntlr_1_mb_high_dlmb_bram_cntlr_1'
c:/xilinx/14.4/ise_ds/edk/gnu/microblaze/nt64/bin/../lib/gcc/microblaze-xilinx-elf/4.6.2/../../../../microblaze-xilinx-elf/bin/ld.exe: 
region `mb_high_ilmb_bram_cntlr_1_mb_high_dlmb_bram_cntlr_1' overflowed by x bytes

then using a MAP file to understand the memory usage of the program is helpful.

Generating a MAP file:

By default, MAP files are not generated by an SDK project. To enable MAP file generation, linker flags must be added to the project:

  1. Right click on the project to modify in the "Project Expolorer" window and select "Properties". This will open a new dialog box with all of the properties for the project.
  2. Under "C/C++ Build" select "Settings"
  3. Under "Tool Settings" --> "MicroBlaze gcc linker" select "Miscellaneous"
  4. In the "Linker Flags" dialog add: -Wl,-Map=executable.map, where "exectuable.map" is the name of the MAP file to use.
  5. Click "OK" and the SDK will re-link your project and create the "exectuable.map" file.

For all WARP reference designs, we add these linker flags to each project.

Opening a MAP file:

Open the MAP file for a project by expanding the project in the "Project Explorer" window and finding the executable.map file inside the Debug folder. By default, double-clicking the executable.map file will result in an error. Instead, right-click on the executable.map file and select "Open with" --> "Text Editor". This will open the file in a generic text editor within the SDK.

Understanding a MAP file:

The MAP file is organized into sections:

For this section, we look at the MAP file for the AP project (i.e. wlan_mac_high_ap).

  1. The first section details all of the members included from the various archive files in the system:
    Archive member included because of file (symbol)
    
    ../../wlan_bsp_cpu_high/mb_high/lib\libxil.a(xil_printf.o)
                                  ./wlan_mac_high_framework/wlan_exp_common.o (xil_printf)
    ...
    
    This information is not especially useful, but lets you see all the system functions.
  2. The next section shows the names and sizes of global symbols (ie global variables) that have been allocated in the program:
    Allocating common symbols
    Common symbol       size              file
    
    UartLite            0x4c              ./wlan_mac_high_framework/wlan_mac_high.o
    cdma_inst           0x164             ./wlan_mac_high_framework/wlan_mac_high.o
    async_pkt_enable    0x4               ./wlan_mac_high_framework/wlan_exp_node.o
    mac_param_chan      0x4               ./src/wlan_mac_ap.o
    statistics_table    0xc               ./src/wlan_mac_ap.o
    my_bss_info         0x4               ./src/wlan_mac_ap.o
    ipc_msg_from_low    0x8               ./wlan_mac_high_framework/wlan_mac_high.o
    ipc_msg_from_low_payload
                        0x190             ./wlan_mac_high_framework/wlan_mac_high.o
    ...
    
    This is a good place to check that all global variables have expected sizes. A common mistake can be to unknowingly allocate a large global variable that consumes a lot of memory space.
  3. The next section show the memory configuration. This should be the same as in the Linker Command File:
    Memory Configuration
    
    Name             Origin             Length             Attributes
    mb_high_ilmb_bram_cntlr_0_mb_high_dlmb_bram_cntlr_0 0x00000050         0x0001ffb0
    mb_high_ilmb_bram_cntlr_1_mb_high_dlmb_bram_cntlr_1 0x00020000         0x00020000
    mb_high_init_bram_ctrl_S_AXI_BASEADDR 0x50000000         0x00001000
    ...
    
  4. Finally, the last section details the memory map split out by section defined in the Linker Command file:
    Linker script and memory map
    
    ...
    
    .text_bram_1    0x00020000    0x103a8
     *wlan_mac_high_framework*(.text)
     .text          0x00020000      0x310 ./wlan_mac_high_framework/wlan_exp_common.o
                    0x00020000                wlan_exp_print_header
                    0x00020114                wlan_exp_print_mac_address
                    0x00020190                wlan_exp_set_print_level
                    0x000201e0                wlan_exp_get_mac_addr
                    0x00020214                wlan_exp_put_mac_addr
                    0x0002025c                wlan_exp_configure
                    0x000202b8                get_station_status
    
    ...
    
    .text           0x00000050    0x183b8
     *(.text)
     .text          0x00000050       0x34 c:/xilinx/14.4/ise_ds/edk/gnu/microblaze/nt64/bin/../lib/gcc/microblaze-xilinx-elf/4.6.2/../../../../microblaze-xilinx-elf/lib/bs/m/le/crt0.o
                    0x00000050                _start1
                    0x00000080                _exit
     .text          0x00000084        0x0 c:/xilinx/14.4/ise_ds/edk/gnu/microblaze/nt64/bin/../lib/gcc/microblaze-xilinx-elf/4.6.2/bs/m/le/crti.o
     .text          0x00000084      0x118 c:/xilinx/14.4/ise_ds/edk/gnu/microblaze/nt64/bin/../lib/gcc/microblaze-xilinx-elf/4.6.2/bs/m/le/crtbegin.o
     .text          0x0000019c       0xb0 c:/xilinx/14.4/ise_ds/edk/gnu/microblaze/nt64/bin/../lib/gcc/microblaze-xilinx-elf/4.6.2/../../../../microblaze-xilinx-elf/lib/bs/m/le/crtinit.o
                    0x0000019c                _crtinit
     .text          0x0000024c      0x604 ./wlan_mac_common/wlan_mac_ipc_util.o
                    0x0000024c                nullCallback
                    0x00000254                MailboxIntrHandler
                    0x000002d8                wlan_lib_mailbox_setup_interrupt
    
    ...
    
    .heap           0x00037ca0     0x4000
                    0x00037ca0                . = ALIGN (0x8)
                    0x00037ca0                _heap = .
                    0x00037ca0                _heap_start = .
                    0x0003bca0                . = (. + _HEAP_SIZE)
     *fill*         0x00037ca0     0x4000 00
                    0x0003bca0                _heap_end = .
    
    .stack          0x0003bca0     0x1000
                    0x0003bca0                _stack_end = .
                    0x0003cca0                . = (. + _STACK_SIZE)
     *fill*         0x0003bca0     0x1000 00
                    0x0003cca0                . = ALIGN (0x8)
                    0x0003cca0                _stack = .
                    0x0003cca0                __stack = _stack
                    0x0003cca0                _end = .
    
    ...
    
    As you can see, this gives a wealth of information about where everything is mapped in the program. Each top level section, such as .text or .heap has both the starting address in the memory map as well as the size (in bytes) listed. Then each section is broken down into the individual object files and both the starting address and size is listed. Finally, each object file is broken down into the individual functions within the object file and the starting address for each function is listed. This allows you to understand which object files might contain large functions which are not necessary for your program execution. It can also give context when looking at pointer addresses within the program.

MAP files are a great source of information when debugging your program. While they are extremely dense and can be a little intimidating, understanding and using MAP files can give you a lot of information that is otherwise difficult to get in standard program debug.

If you have any questions, please use the forums.

Last modified 3 years ago Last modified on Oct 20, 2015, 1:17:48 PM

Attachments (4)

Download all attachments as: .zip