[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

A. Bareboard and Custom Kernel Topics

This chapter describes how to use the GNAT Pro cross tools to build programs that do not require support from any kernel running on the target hardware. It uses the PowerPC with ELF object module format as the target platform to illustrate these issues.

A.1 Introduction  
A.2 Examples  
A.3 Development Support  

[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

A.1 Introduction

Throughout this chapter we will use one approach to linking. There are many ways that gnatlink (or powerpc-elf-gnatlink as it would be invoked on this platform), or gnatmake can be used; but we will adopt a lower level approach that is a bit clearer and more flexible.

The program gnatlink normally performs 2 steps:

  1. it calls gcc to compile the file generated by gnatbind;
  2. it calls gcc to link all the relocatable object files into an executable.

We will break out those steps. We will use powerpc-elf-gcc to compile the binder-generated file, and powerpc-elf-ld to do the linking.

Other configurations of GNAT, native or targeting an embedded OS, use tools for manipulating binary files that are provided with the target system (whether native or cross). In the platform illustrated in this chapter, because there is no target OS and no development tools that would be provided with an OS, GNAT Pro provides the tools that are necessary or useful. GNAT Pro Cross comes with with the GNU binutils package properly configured and built. The linker, powerpc-elf-ld, is one of those tools. .

gcc (or powerpc-elf-gcc) is really a driver program that runs other programs for compiling, assembling, linking and performing other needed steps. When used for linking, gcc knows which object files and libraries are needed for the target environment, and it adds these to the linker command line.

[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

A.2 Examples

Given the many variations on the host and target environment that you may be using, it is not possible to formulate a universally applicable sequence of commands for building, loading, and running a program. Instead, this section presents a series of scenarios reflecting various levels of sophistication in the target tools, with enough detail to allow you to prepare the necessary setup. For illustration purposes the examples will assume a PowerPC / ELF target, and occasionally may reflect a Unix host.

There are two main issues that you need to consider:

One of the simpler configurations is as follows:

In other words, the monitor on the target board is relatively "smart". To run a program that you've built with GNAT Pro Cross (an executable ELF file) you just need to load the program and "go" -- the monitor program keeps track of the start address recorded in the ELF file.

The following simple example will illustrate the issues with building, loading and running a program; it merely sets a word in RAM to a particular value. With the ROM monitor program you can see the program's effect by looking at the memory location before and after execution.

with System.Storage_Elements; use System.Storage_Elements;

procedure nu is
   J : Integer;
   for J'Address use To_Address (16#200_000#);
   J := 5;
end nu;

Based on your experience with the GNAT tools for a native environment, you might try to build this program with the command:

  powerpc-elf-gnatmake nu

While there is a small chance that you could get the resulting executable to run, this is unlikely. In the relatively easy-to-use setup that we've described so far, the load command will likely read the entry point from the ELF file `nu'. Since there is no symbol named _start, the linker has chosen the first address in the .text section as the entry point. However, that is not where the program should start execution. Moreover, the linker has chosen some builtin default addresses for placing the various sections of the program. It is highly unlikely that they will be appropriate for your target hardware.

Assuming that 16#100_000# is a good starting location in RAM for your program, the following steps will build a more useful executable.

  powerpc-elf-gnatmake -g -b -c nu
  powerpc-elf-gcc -c b~nu.adb
  powerpc-elf-ld -e main -Ttext 0x100000 b~nu.o nu.o -o nu

Note that the `-b -c' options to gnatmake combine compiling and binding, and omit the linking step.

As with the native tools, the binder creates a file named `b~XXX.adb' where XXX is the name of the main subprogram. The second command above compiles the binder output file. The ld command works as follows:

In this case, it is also possible to combine these steps into a single command:

  powerpc-elf-gnatmake -g nu -largs -Wl,-e,main,-Ttext,0x100000

When the resulting nu is loaded, the image will start at 16#100_000#, and the entry point will be wherever the procedure main is placed.

Although this is more likely to run than the earlier example, there still may be some problems. To get more detail you can use the `-save-temps' option for gcc (or `-cargs -save-temps' to gnatmake), which will save the intermediate assembly language file using the `.s' file extension. The symbol name for the outer level procedure has _ada_ prepended and is thus _ada_nu. In `b~nu.adb' you will find the main procedure (notice the export in `b~nu.ads'). It sets default values for command line variables (which do not apply in this environment), calls adainit (which would run the elaboration code if there was any), calls Break_Start (which is there to provide a convenient place to set a breakpoint when debugging), and, finally, calls your nu code.

If your target environment is not so fortunately set up, you will not be able to use the ELF file nu produced above. Such a simple scheme also breaks down in numerous ways as soon as you write any code with any more complexity.

To cope with such issues, you will need to create and use a startup routine in assembly language. Define the symbol _start at the desired code entry point; the linker will find that symbol, and you no longer need the `-e' option to ld. Several additional items will also be useful to include in the startup code: zeroing .bss, and setting the stack pointer and perhaps other registers.

Uninitialized global data are reserved in the .bss section in the executable file. During linking, all uninitialized variables are assigned an address in .bss, and all references to these variables are relocated properly, but no actual data are contained in the ELF executable (it is allocatable but not loadable). It is expected that the locations within .bss are set to zero before execution begins.

We will no longer assume that some smart loader is taking care of so much; the startup code is responsible for zeroing the .bss data, before it branches to your application's Ada entry. Since nu contained no declarations of uninitialized variables (the address clause causes J to be treated differently), this was not an issue for that simple program. Also, it is conceivable that the ROM monitor will set the stack to a usable value before branching to the entry point. However, it is more likely that you will want to set the stack pointer early in your startup code, and there may be other registers that must be set. For example, if the relevant Application Binary Interface (ABI) specifies an addressing mode that is relative to a designated register, the linker will output a symbol containing the value that the register must be set to. For example, on MIPS, _gp must be loaded into register 28. On PowerPC you could set r13 to point to the small data area (although this is not necessary unless you take advantage of this feature, and is not relevant here).

Here is the "bare bones" startup assembly code, `start.s':

        top_of_stack = 0x200000

        .section ".text"
        .global  _start
        lis 1, __bss_start@ha
        la  1, __bss_start@l (1)
        lis 2, __end@ha
        la  2, __end@l (2)
        li  3, 0
        cmp  0,0,1,2
        bge  bssdone
        stw  3, 0(1)
        addi 1, 1, 4
        b    bssloop
        addis 1, 0, top_of_stack@h
        ori   1, 1, top_of_stack@l

        bl    main
        b     forever

And here is the link command:

powerpc-elf-ld -Ttext 0x100000 -o nu start.o nu.o b~nu.o

A single command that would build the application is:

powerpc-elf-gnatmake -g nu -largs -Wl,-Ttext,0x100000,start.o

It is very likely that your setup may work with S-record files. If so, the following objcopy command converts the executable ELF file `nu' to an S-record file `nu.sre' (details are in the binutils documentation):

powerpc-elf-objcopy -S -O srec nu nu.sre

A script written in ld's script language controls how ld composes the pieces of a relocatable (`.o') file when creating an executable. Up to now, the default builtin script has been assumed. You can see this default script by running powerpc-elf-ld -verbose, which writes the contents to your terminal. Note that this script is large and complicated, containing data specific to different debugging formats, programming languages (C++ in particular), and dynamic libraries (not applicable). However, you will also find the definitions of __bss_start and __end, which reflect the. bounds of the .bss section of the executable. Complete information about the linker and its scripting language may be found in the Using ld document.

An extremely simple linker script might be a better starting point for building what you need. Here is one that will work in our still relatively amenable working environment:

  OUTPUT_FORMAT("elf32-powerpc", "elf32-powerpc",
  PROVIDE (top_of_stack = 0x200000);
    . = 0x100000;
    .text :

    .data :
      *(.data .sdata .sdata2)

    __bss_start = .;
    .bss :
     *(.bss .sbss)
    __end = .;

Briefly, this script defines three output segments:

"." is referred to as the location counter. It is set to 0x100000 before the first segment, and all of the code follows after that (since "." is never explicitly set to a new value). The location counter is used to set the values of the __bss_start and __end symbols. The script defines the symbol top_of_stack, so you may remove it from the startup code if you prefer to use the linker script to set its value. PROVIDE defines the value if it isn't defined elsewhere.

Unless you explicitly discard input sections, they will be copied to the output executable with the same name. E.g., if you compiled your code with debug information, linking with the script above will copy it into the executable -- you will then be able to debug.

A note on debugging: the version of gdb provided with GNAT Pro Cross is "Ada aware". But you need to determine how gdb running on your host will interact with code on your target. Most likely, it will use gdb's serial protocol to interact with some agent that can control execution of your code. One example is the use of a hardware probe that controls the processor through a JTAG (BDM, COPS) port. GNAT Pro Cross supports the Abatron BDI 2000, which is a hardware device that connects to the target through JTAG, implements the gdb serial protocol in firmware, and communicates using that protocol over its ethernet port. For example, a BDI could be known on your local network by the hostname bdi, and when running powerpc-elf-gdb you would use the gdb command target remote bdi:2001. This tells gdb that it will control the execution of your program by using the serial protocol running on port 2001 of the host bdi. Another possibility is that there is a kernel running on your target hardware that controls program execution (your application) and which implements the gdb serial protocol over some connection -- often a serial port.

We will highlight one other feature of ld scripting which is often used, particularly in later stages of development. Early on, you might have a way of loading your program into RAM and running it from there. Eventually, you might want to burn some parts into ROM and (particularly) if your code runs from hardware bootup you will have an issue with the .data segment. At bootup, the initial data in .data is in ROM at one range of addresses, but it has to be in RAM during execution, at a different range of addresses. (You might well want .text moved to RAM also for performance reasons, but we'll illustrate with .data.) ld assumes that every section has two addresses: the VMA, or "virtual memory address", and the LMA, or "load module address". The VMA is the run-time address of the beginning of the section in RAM; the LMA is the address in ROM. In the ld script example, there was nothing specified between the name of the output section .data and the following : character. The syntax allows you to specify the starting address (VMA) there. It defaults to the location counter if not specified. By default, LMA is set to the VMA. An AT clause is used to set the LMA separately. For example:

  data_image_begin = 0xdc000000;
  data_begin       = 0xE0000000;

  .data data_begin : AT (data_image_begin) {
    *(.data .sdata .sdata2)
  data_end = .;

In this description of .data, the section is loaded at data_image_begin, but is expected to be at data_begin at run time. I.e., all references to objects in .data are resolved to addresses in the section starting at the VMA, data_begin. Code can be added to `start.s' to use the three symbols defined to copy the .data segment from ROM to RAM before the application starts up.

Some examples are supplied in the `examples' directory of the GNAT Pro installation containing multi-language applications with project files. A `Makefile' is provided to build the source code for the desired target and run time. For example:

make TARGET=powerpc-elf RUNTIME=zfp

[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

A.3 Development Support

Sometimes, starting a development on a bare board platform is difficult because of the lack of functionality for debugging and early verification. For example, text output routines are not expected to be used in the final version of the software, and hence they are not available in certifiable run-time libraries, but they are useful to display execution logs.

The cross-compilation toolchains come with a support library (zfp_support) including text output routines, image attributes, memory operations, and some others, intended to be used during the development and debugging phases.

To use this library, you just need to add with "zfp_support"; to the top of your project file. If you want to write a simple Hello world example for PowerPC that can be run on QEMU, you can write the program simply as:

with Ada.Text_IO; use Ada.Text_IO;

procedure Hello is
   Put_Line ("Hello");
end Hello;

A very simple project file would be the following:

with "zfp_support.gpr";

project hello is
   for Main use ("hello.adb");
end hello;

And then, building and executing the example can be achieved with the following sequence of commands:

$ powerpc-elf-gnatmake -P hello -XVARIANT=powerpc-elf-qemu
$ powerpc-elf-qemu hello

[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

This document was generated by GNAT Mailserver on June, 30 2014 using texi2html