Appendix A. Understanding ELF Files

The process of building Cell applications involves a bewildering number of file types: PPU executables, SPU executables, shared libraries, static libraries, object files, and even a special PPU object file format called CESOF (CBE Embedded SPE Object Format). This can be confusing until you see that these files all have the same underlying format: the Executable and Linking Format, or ELF. When you understand the ELF structure, you’ll stop thinking of all the build files as separate file types, and think of them as different variations of a single format.

In addition to explaining ELF structure, this appendix discusses how to read and manipulate ELF files in code. This can be very useful when you create SPU applications and need to keep track of the bytes and memory addresses in the executable. Further, when you have a solid grasp of how ELF files function, you’ll better understand how Cell code is linked together and how applications execute on the device.

The ultimate goal of this appendix is to explain the CESOF structure and the process of accessing ELF files in code. But first, it’s important to start with basic ELF files: object files, executables, and libraries.

ELF Object Files

As its name implies, an ELF file serves two main purposes: to provide information that the linker can integrate into an executable, and to provide information that the operating system can use to create and start a new process. An object file (*.o) is an ELF file that performs the first function, and this section examines how object files are structured.

The code in Listing A.1 is simple and commonplace, but its object file will serve as the subject of the presentation that follows.

Example A.1. HelloWorld.c

#include <stdio.h>

int main(int argc, char **argv) {
   printf("Hello World!
");
   return 0;
}

The makefile in the HelloWorld project directory uses this code to create an object file (HelloWorld.o) and an executable (HelloWorld). Both are ELF files and both consist of two parts: a body of data and headers that specify how the data is organized. An object file contains two types of header data:

  1. ELF header: Provides information about the entire ELF file—its file type, processor target, and the location and size of the section and program headers.

  2. Section headers: Contains information about the ELF’s sections. Each section contains a specific type of data used by the linker to build an executable.

The ELF Header

To see the information contained in the object file’s ELF header, enter the following command at the command line:

readelf -h HelloWorld.o

The -h flag tells readelf to display only the ELF header. Some of its important fields are listed here:

Magic:   7f 45 4c 46 02 02 01 00 00 00 00 00 00 00 00 00
Class:   ELF64
Data:    2's complement, big endian
...
Type:    REL (Relocatable File)
Machine: PowerPC64
...
Start of program headers:   0 (bytes into the file)
Start of section headers: 304 (bytes into the file)
...
Size of program headers:    0 (bytes)
Number of program headers:  0
Size of section headers:   64 (bytes)
Number of section headers: 14

The first 3 bytes of the Magic number identify the file as an ELF file (0x45, 0x4c, 0x46 form “ELF” in ASCII). The Class field states that the file was created for 64-bit systems. The ELF type, REL, stands for relocatable, which means this file contains data that can be linked into an executable. The Machine field specifies the target as PowerPC64, a category that includes the PPU.

Below the Machine field, the ELF header states the location and content of the program headers and section headers. In this example, there are no program headers and 14 section headers. The section headers can be found 304 bytes into the ELF file.

The Section Headers

The ELF header states the size of the section headers and where they are located in memory. To see the section header data, enter the following command:

readelf -S HelloWorld.o

The output presents information about the ELF file’s sections in row-column format. This may look confusing because each section header occupies two rows. To clarify, Figure A.1 shows an example header for an Official Procedure Descriptor (.opd) section. This PowerPC-specific section stores the addresses of functions contained in the object file.

Example section header in an ELF object file

Figure A.1. Example section header in an ELF object file

The section header fields are as follows:

  • Section Name—Identifies purpose of data stored in the section.

  • Section Type—Describes how the section should be processed.

    NULL Null section (first section in the array)

    PROGBITS Contains information required by the program

    SYMTAB Symbol Table

    STRTAB String Table

    REL/RELA Store relocation information

    HASH Contains symbols accessible within a hash table

    DYNAMIC Holds information needed for dynamic linking

    NOTE Function determined by operating system

    NOBITS Like PROGBITS, but takes up no memory

    SHLIB Reserved

    DYNSYM Symbols required for dynamic linking

  • Address—Location where section should be loaded into memory. Because object file sections aren’t loaded into memory, all the values are set to zero.

  • Offset—Offset of section within the ELF file.

  • Size—Amount of memory used to store the section.

  • EntSize—Amount of memory used to store the entries in the section.

  • Flags—Describe section accessibility and usage.

    W

    Writable

    A

    Allocable (placed in memory during load)

    X

    Executable

    M

    Mergeable

    S

    Strings

    I

    Info

    L

    Link order

    G

    Group

    x

    Unknown

    O

    Operating system required

    o

    OS specific

    p

    Processor specific

  • Link—Number of the section containing the symbol table.

  • Info—Section-dependent information.

  • Alignment—Must be placed on a boundary with the specified number of bytes.

With this information, you can better understand Figure A.1. The .opd section takes up 0x18 bytes, is aligned on an 8-byte boundary, and its offset is 0xB0 bytes. This section is writable (W flag) and its data is incorporated into a program (PROGBITS). It occupies memory during the load process (A flag), but there’s no address yet for where it’s going to be placed. It contains no entries (it’s not a table) and doesn’t point to the symbol table.

Figure A.2 shows the memory structure of the HelloWorld.o object file, including the ELF header, the section headers, and the sections themselves.

Structure of the HelloWorld.o object file

Figure A.2. Structure of the HelloWorld.o object file

Sections listed with the A flag, called allocable sections, are loaded into memory when the executable starts. These include the machine code in the .text section and the data in such sections as .toc, .bss, .data, and .rodata. Sections without the A flag, called nonallocable sections, provide information to the linker and loader to create the executable. When the strip() function reduces the size of an executable, it removes the ELF file’s nonallocable sections.

Table A.1 lists many of the different types of sections found in ELF files and the information they provide. Some appear frequently in object files, whereas others are found only in executables.

Table A.1. Common Sections in ELF files

Section Name

Full Name

Content

.text

Text

The program’s executable machine code

.bss

Block Started at Symbol

Uninitialized data (zeros)

.data

Data

Initialized, writeable data (variables)

.rodata

Read-Only Data

Read-only data (constants)

.toc

Table of Contents

Reference address for all data in object file

.opd

Official Procedure Descriptor

Addresses/ToC entry of functions

.got

Global Offset Table

Absolute addresses to symbols

.plt

Procedure Linkage Table

Absolute addresses to functions

.jcr

Java Class Registration

Compiler-specific instructions for Java classes

.init

Initial Instructions

Routine called before main()

.fini

Final Instructions

Routine called after main()

.rela.sec

Relocation Data for sec

Relocation data for named section

.comment

Comment

Version information for the ELF file

.shstrtab

Section Header String Table

Names of each section in the object file

.eh_frame

Exception Handling Frame

Contains information about processing exceptions

.symtab

Symbol Table

All symbols used in the object file

.dynsym

Dynamic Symbol Table

Symbols used in the dynamic linking process

.strtab

String Table

Contains strings used in the object file

.dynstr

Dynamic String Table

Strings used in the dynamic linking process

Note

The sections in an object file vary widely according to the compiler, the processor target, and the operating system. However, the basic sections (.text, .data, .bss, .symtab, and such) remain consistent across ELF files.

The first of the listed sections, .text, is the most important. It contains the machine code that executes when the program runs. When the executable needs data, it accesses the .toc section. This contains global and local variables, and assembly instructions access them with offsets to the .toc location.

Symbol and String Tables

The section containing the symbol table, .symtab, lists all the symbols used within the object file, including function names and the name of the source file. To see the symbol table of HelloWorld.o, enter the following command:

readelf -s HelloWorld.o

This prints output similar to the following:

Symbol table '.symtab' contains 11 entries:

 Num:    Value          Size Type    Bind   Vis      Ndx Name
  0: 0000000000000000     0 NOTYPE   LOCAL  DEFAULT  UND
  1: 0000000000000000     0 FILE     LOCAL  DEFAULT  ABS HelloWorld.c
  2: 0000000000000000     0 SECTION  LOCAL  DEFAULT    1
  3: 0000000000000000     0 SECTION  LOCAL  DEFAULT    3
  4: 0000000000000000     0 SECTION  LOCAL  DEFAULT    4
  5: 0000000000000000     0 SECTION  LOCAL  DEFAULT    5
  6: 0000000000000000     0 SECTION  LOCAL  DEFAULT    7
  7: 0000000000000000     0 SECTION  LOCAL  DEFAULT    8
  8: 0000000000000000     0 SECTION  LOCAL  DEFAULT   10
  9: 0000000000000000    88 FUNC     GLOBAL DEFAULT    8 main
 10: 0000000000000000     0 NOTYPE   GLOBAL DEFAULT  UND puts

TheValue column identifies the symbols’ addresses. Because HelloWorld.o is a relocatable file, its symbols don’t have addresses yet and all the values equal zero. The Type field specifies whether a symbol refers to a function, file, data object, or special type. If a symbol’s type equals SECTION, the symbol represents one of the sections listed in the section header.

The Bind and Vis columns state the symbol’s visibility with respect to other files, executables, and libraries. A symbol’s Bind value is LOCAL, GLOBAL, or WEAK, depending on its usage and scope within the code. A WEAK symbol is assumed to be global, but is set to zero if its name conflicts with a GLOBAL symbol. If the Vis value equals DEFAULT, the symbol’s external visibility is determined by its Bind value.

If a symbol has type SECTION, the Ndx value identifies its index within the list of section headers. If a symbol doesn’t represent a section, its Ndx field can take one of three values:

  1. ABS: The symbol won’t change due to relocation.

  2. COMMON: The symbol will be allocated.

  3. UNDEF: The symbol is undefined.

The last symbol, puts, refers the printf function declared in stdio.h. This symbol is undefined (UNDEF) in HelloWorld.o, so the linker will look through its search path to find a matching symbol in an available library.

An object file lists its strings in a string table whose section name is .strtab. There is no readelf flag that displays the string table, but —hex-dump=N displays the content of Section N in hexadecimal and ASCII. On my system, the string table in HelloWorld.o is Section 13, so its content can be printed with the following command:

readelf —hex-dump=13 HelloWorld.o

The output is as follows:

Hex dump of section '.strtab':
0x0000 0048656c 6c6f576f 726c642e 63006d61 .HelloWorld.c.ma
0x0010 696e0070 75747300                   in.puts.

As shown, the string table contains the names of the functions (main, puts) and the name of the file, HelloWorld.c. When the linker needs to find these strings, it accesses the string table with the appropriate index.

Relocation Tables

The compiler and assembler don’t know how much memory is available on your system or how the linker will allocate it. Therefore, instead of assigning addresses to symbols, the machine code creates offsets from a hypothetical memory location.

Three of the sections in the object file (rela.toc, rela.text, rela.opd) contain information needed to assign effective addresses to three other sections (.toc, .text, .opd). To see the contents of these sections in the HelloWorld object file, enter the following command:

readelf -r HelloWorld.o

This displays the structure of all three of the relocation sections. The structure of the rela.txt section is shown below (abridged to fit on the page):

'.rela.text' at offset 0x5d0 contains 2 entries:

Offset   Info           Type         Sym. Value  Name+Addend
0022  00050000003f  R_PPC64_TOC16_DS 00000000000  .toc + 0
0024  000a0000000a  R_PPC64_REL24    00000000000  puts + 0

These table entries tell the linker that the .text section requires specific addresses for .toc (the program data) and puts (the output function). The Offset value shows where the specific address needs to be placed in the .text section. Info contains the index into the symbol table of the data that needs to be relocated. For example, puts is the tenth entry in the symbol table, and the first four digits in the corresponding Info field are 0x000a.

The addend in the last column specifies an added value that should be used when calculating an effective address for the symbol. All relocation sections that start with .rela have addends for their entries. Sections starting with .rel do not.

ELF Executable Files

After the linker has done its work, the result is an ELF executable. There are two significant differences between this and an object file. First, all the executable’s symbols and allocable sections have effective addresses assigned to them. Second, the sections are rearranged so that the allocable and nonallocable sections are kept separate. This way, the loader knows exactly which portions of the file should be mapped into the process address space and which should be left unmapped.

These rearranged sections are packaged into segments described by program headers. The executable still contains the ELF header and section headers, but only the program headers and their segments will be accessed during the loading process.

This can be confusing, so let’s review the difference between sections and segments:

  • Sections are described by section headers. They are distinguished by the type of data they hold.

  • Segments are described by program headers. They are distinguished by how they’re processed when loaded into memory.

  • There are no segments in object files, but segments in executables contain sections.

The entire structure of the HelloWorld executable can be viewed with the following command:

readelf -e HelloWorld

The ELF header has changed significantly. The file type is no longer REL (relocatable) but EXEC (executable). There is a valid memory location for the entry point address. Because the executable accesses a shared library (libc.so), there are many more section headers than there were in the object file. Further, each of the allocable sections now has a memory address representing where it will be mapped to process memory.

The file also contains an entirely different set of headers: program headers. On my PS3, the HelloWorld executable contains eight program headers and they’re listed in a row-column format similar to the section headers. But the header fields present different types of information. This is shown in Figure A.3, which displays the header corresponding to the DYNAMIC segment.

Example program header in an ELF object file

Figure A.3. Example program header in an ELF object file

The fields in the program header are as follows:

  • Segment Type—Identifies how the segment is processed when loaded into memory

  • File Offset—The segment’s location within the ELF file

  • Virtual Address—The segment’s location within the Effective Address (EA) space

  • Physical Address—The segment’s physical address (not used)

  • Segment Size in File—Number of bytes taken up by the segment in the ELF file

  • Segment Size in Memory—Number of bytes in the segment’s memory image

  • Segment Flags—Accessibility

    R:Readable

    W:Writeable

    E/X:Executable

  • Segment Alignment—Boundary to which the segment must be loaded in memory

This DYNAMIC segment has an offset of 0x798 bytes and occupies 0x170 bytes. The loader will map the segment to address 0x10010798 in the process space, where it will take up 0x170 bytes. The loaded image will be readable and writable and will aligned in memory on an 8-byte boundary.

Figure A.4 shows the segments in the executable and the sections they contain. Memory addresses are omitted because many of the segments overlap.

Structure of the HelloWorld executable

Figure A.4. Structure of the HelloWorld executable

Most of the executables sections are placed in one of the two LOAD segments: Executable and read-only sections are placed in the first LOAD segment. Writeable sections are contained in the second LOAD segment.

Different compilers produce executables with different types of segments, but Table A.2 lists many of the common segments found in ELF executables.

Table A.2. Common Segments in ELF files

Section Name

Content

PHDR

Program headers

LOAD

The segment will be loaded into memory

DYNAMIC

Information needed to perform dynamic linking

NOTE

OS-defined support information

INTERP

Names the interpreter to be used for the executable

NULL

Not used

GNU_EH_FRAME

Exception-handling information

GNU_STACK

Control information for the loaded stack

Just as the .text section contains the machine code in the object file, the LOAD segments contain the executable’s loadable data. It’s important to understand that, unlike sections in an object file, these segments overlap many of the other segments. For example, the DYNAMIC segment is found between addresses 0x798 and 0x908 in the ELF file, but is also part of the writeable LOAD segment, which runs from 0x750 to 0xA30 in this example.

Looking at the new section headers, you’ll see that there is no longer a .toc section. Instead, the .got section (Global Offset Table) stores addresses of the program’s data. There is still a .opd section to store function descriptors, but now there is also a .plt section (Procedure Linkage Table) to store descriptors for functions obtained through shared libraries.

Dynamic Linking

The INTERP segment in the executable names an interpreter to process the executable. This segment consists of the .interp section, which is Section 1 in HelloWorld. You can examine the section’s content with the following command:

readelf —hex-dump=1 HelloWorld

The printed result is /lib64/ld64.so.1. This shared-object file is GNU’s 64-bit dynamic linker, and integrates code from dynamic libraries into the process image at runtime.

To find the information it needs, the dynamic linker searches through the .dynamic section in the DYNAMIC segment of the executable. It also searches for the dynamic string table, stored in the .dynstr section. On my system, the ASCII content of this section is given as follows:

.libc.so.6.puts.__libc_start_main.GLIBC_2.3.

This tells the dynamic linker which function is needed (puts) and which function needs it (main). Using this and the other information in the .dynamic section, the linker can make sure Hello World! is displayed for all to see.

This discussion has briefly touched on dynamic libraries, but now it’s important to take a closer look. The next section shows how static and dynamic libraries are structured, and how to create them using GNU tools.

ELF Libraries

Linux code libraries can be divided into two categories: static libraries and shared (or dynamic) libraries. Static libraries are simple to create and use, but shared libraries provide greater flexibility and code reusability. This section describes both types, placing emphasis on shared libraries.

The code for this section is contained in two folders, lib_static and lib_shared. Both contain the same source files, hello.c and goodbye.c, but the first project creates libsimple.a and the second creates libsimple.so. Listings A.2 and A.3 show the content of the two source files.

Example A.2. Hello.c

#include <stdio.h>

void hello() {
   printf("Hello!
");
}

This file consists of a single function, hello, that prints Hello! to standard output. The goodbye function in Listing A.3 also displays a string: Goodbye!

Example A.3. Goodbye.c

#include <stdio.h>

void goodbye() {
   printf("Goodbye!
");
}

The code in the third file, libcall.c, invokes both functions, hello and goodbye. This code is presented in Listing A.4.

Example A.4. Libcall.c

extern void hello();
extern void goodbye();

int main(int argc, char **argv) {
   hello();
   goodbye();
   return 0;
}

The two libraries aren’t useful in a practical sense, but examining them will provide a clear idea of how library files are structured. Understanding ELF libraries is important—not only for general coding, but also to see how the Cell toolchain combines SPU code inside a PPU-based ELF file.

Static Libraries

A static library (*.a) isn’t a new kind of ELF file; it’s just an archived collection of object files (*.o) whose structure was investigated earlier. To see this, change to the lib_static folder and run make. The two object files are created with the following commands:

ppu-gcc -c -o hello.o hello.c
ppu-gcc -c -o goodbye.o goodbye.c

Then the static library (libsimple.a) is built with the following command:

ppu-ar rcs libsimple.a hello.o goodbye.o

If you analyze libsimple.a with readelf, you’ll see that it doesn’t have an ELF structure of its own; readelf simply lists the contents of the two archived object files, hello.o and goodbye.o. Finally, the makefile creates an executable from libcall.c and libsimple.a with the following command:

ppu-gcc -o libcall libcall.c -L. -lsimple

After linking, the object code from the static library is completely incorporated into the executable. This self-containedness makes it simple to copy and distribute the application, but there’s no easy way to update the executable when libsimple.a is modified. For this reason, most *nix libraries are kept separate from the application and are integrated only during execution. These are shared libraries.

Shared Libraries

A shared library, also called a dynamic library or a shared object file, contains object code that is linked into the executable during runtime. This modularity keeps executables small and allows developers to update libraries without altering applications that require them. However, when an application depends on multiple shared libraries, version compatibility becomes a crucial issue.

Section A.2, “ELF Executable Files,” described how an executable ELF file specifies a dynamic linker and the libraries it needs. When the application executes, the linker searches through the list of directories in /etc/ld.so.cache. The cache entries are defined in /etc/ld.so.conf.

Note

It’s a good idea to add /usr/local/lib to your library search path. To do this, add this path name to ld.so.conf and then update the cache file with the ldconfig command.

The makefile in the lib_shared project creates object files for a shared library with the following command:

ppu-gcc -c -fpic hello.c goodbye.c

The -fpic option tells the compiler to create position-independent object code. This is different from the relocatable code we looked at earlier. The addresses in relocatable code are resolved after the linking process while addresses in position-independent code are resolved during program execution.

The makefile creates libsimple.so from the position-independent object files with the following command:

ppu-gcc -shared -Wl,-soname,libsimple.so.1 
   -o libsimple.so.1.0.1 hello.o goodbye.o

This is markedly different from any other usage of ppu-gcc in this book. The compiler/linker options are given by the following:

  • shared: Tells the linker to create a shared library.

  • Wl: Passes list of comma-separated options to the linker.

  • soname: Tells the linker that the following option is the library’s soname—the name that identifies the library’s major: version. No matter how much the minor versions change, any application compatible with the soname will be able to call the library functions.

To create a link between libsimple.so.1.0.1 and a name the linker will be able to recognize (*.so), the makefile executes two link commands:

ln -sf libsimple.so.1.0.1 libsimple.so.1
ln -sf libsimple.so.1 libsimple.so

Now libsimple.so is available for dynamic linking. You can view the ELF structure of this library with the following command:

readelf -e libsimple.so

You can see that the library has its own file type (DYN). Like an executable, it consists of segments that are loaded into memory at runtime. But it also has sections that executables don’t have, such as the Symbol Table (.symtab) and String Table (.strtab) sections. These sections contain references to the hello and goodbye functions provided by the library.

The last command of the makefile creates an executable that dynamically loads libsimple.so:

ppu-gcc -o libcall libcall.c -L. -R. -lsimple

The -R option identifies the executable’s rpath. This location is coded into the executable so that the dynamic linker knows where to find its library dependencies.

If libsimple.a and libsimple.so are placed in the same directory, the shared library will take precedence in the build. But you can ensure that the static library is linked by adding the -static option to the build command.

SPU-ELF and CESOF Files

All the ELF files so far have targeted the PPU. But if you create object files and executables with spu-gcc, they will target the SPU. To distinguish between the two types of ELF files, files of the first type will be called PPU-ELF files, and files of the second type will be called SPU-ELF files.

To create an application that incorporates SPU and PPU code, information from an SPU-ELF file must be embedded into a PPU object file called a CESOF (CBE Embedded SPE Object Format). When you see CESOF files are created and structured, you’ll have a better idea as to how the PPU and SPU communicate.

SPU-ELF Files and the TOE Section

PPU-ELF files and SPU-ELF files have similar structures, but files of the latter type can contain an additional section called .toe. This stands for Table of EARs (Effective Address References). The purpose of this section is to store symbols mapped to effective addresses. By initializing the data in .toe, the PPU can pass address information to SPU applications.

At the time of this writing, developers must create .toe sections in assembly. Listing A.5 shows an example of how this is done.

Example A.5. Allocating a TOE Section: toe.s

.section .toe, "a", @nobits
.align 4

.global _EAR_prime
_EAR_prime:
.octa 0x0

This code creates an allocable section, .toe, containing a global symbol called _EAR_prime. The _EAR_ prefix is required to prevent mangling. This symbol refers to a value of 0x0 in toe.s, but the PPU code assigns the _EAR_prime symbol to a memory address. The SPU code reads this memory address as an unsigned long long called _EAR_prime.

The toe.s source file can be found in the AppendixA/prime project directory. The makefile in this directory incorporates the code into the SPU executable spu_prime with the following command:

spu-gcc -o spu_prime spu_prime.c toe.s

When this command is executed, spu_prime can access _EAR_prime as if it was a regular external symbol. This is shown in Listing A.6. In this application, the SPU retrieves an array of ints from the _EAR_prime address and displays both the address and the elements in the array.

Example A.6. Reading Data from the TOE Section: spu_prime.c

#include <stdio.h>
#include <spu_mfcio.h>

#define CESOF_TAG 27

/* The .toe symbol defined in the assembly code */
extern unsigned long long _EAR_prime;

/* The array in the SPU's Local Store */
int local_prime[16] __attribute((aligned(128)));

int main(long long spuid, char** argp, char** envp) {

   /* Display the value of _EAR_prime */
   printf("SPU Array Location: %#llx
", _EAR_prime);

   /* Transfer the array at _EAR_prime to memory */
   mfc_get(local_prime, _EAR_prime, sizeof(local_prime),
      CESOF_TAG, 0, 0);
   mfc_write_tag_mask(1<<CESOF_TAG);
   mfc_read_tag_status_all();

   /* Display the array's contents */
   int i;
   for (i=0; i<16; i++)
      printf("%d ",local_prime[i]);
   printf("
");

   return 0;
}

To examine the sections and segments in the spu_prime executable, enter the following command:

spu-readelf -e spu_prime

The ELF header identifies this file as an SPU executable. The section header table contains an entry for the .toe section. This section doesn’t hold any meaningful data yet, but the PPU code will use this section to transfer a memory address to the SPU.

The CESOF Format

To be accessed by the PPU, SPU executable code must be embedded into a PPU object file. The resulting object file is called a CESOF (CBE Embedded SPE Object Format) file. The ppu-embedspu command performs the embedding. The following makefile command converts the spu_prime executable into a CESOF called spu_prime-embed.o:

ppu-embedspu -m64 spu_prime_handle spu_prime spu_prime-embed.o

The ppu-embedspu arguments in this command are as follows:

  • -m64/-m32: Specifies whether the target is a 64-bit or 32-bit PowerPC

  • spu_prime_handle: Name of the handle for the SPU program

  • spu_prime: Name of the input SPU executable

  • spu_prime-embed.o: Name of the output PPU object file

The spu_prime-embed.o object file is a CESOF file. If you examine its structure you’ll see that it’s a relocatable file targeting the PPU. You’ll also see a new section, .rodata.elf, that takes up a great deal of the ELF file. As shown in Figure A.5, this section contains the allocable sections of the SPU executable, including .text, .data, and .toe.

CESOF sections

Figure A.5. CESOF sections

The format of the CESOF is similar to that of regular PPU relocatable object files but there are a few important differences. The .data section contains a program handle, spu_prime_handle, created by ppu-embedspu. This structure holds three pieces of information: a pointer to the SPU executable, a pointer to the TOE information, and the structure’s size.

The SPU’s .toe section is mirrored in the CESOF’s .data.spetoe section. This is where the PPU code will access the TOE data when the executable runs. Like any relocatable object file, the CESOF can be converted into a static or shared library using the methods described in Section A.3, “ELF Libraries.”

Creating the Complete PPU Executable

The PPU code in Listing A.7 accesses the spu_prime_handle created earlier and loads it into a context representing the SPU. Then it runs and deallocates the context.

Example A.7. Initializing the TOE Section with a Prime Array: ppu_prime.c

#include <stdio.h>
#include <stdlib.h>
#include <libspe2.h>

/* Initializes the memory location contained in .toe */

int prime[16] __attribute((aligned(128)))
   = {2, 3, 5, 7, 11, 13, 17, 19,
      23, 29, 31, 37, 41, 43, 47, 53};

/* Accesses the handle in the CESOF file */
extern spe_program_handle_t spu_prime_handle;

int main(int argc, char **argv) {
   int ret;
   spe_context_ptr_t ctx;
   unsigned int entry_point;
   spe_stop_info_t stop_info;

   /* Display the EA of the array */
   printf("PPU array location: %#llx
",
      (unsigned long long)prime);

   /* Create the SPE Context */
   ctx = spe_context_create(0, NULL);
   if (!ctx) {
      perror("spe_context_create");
      exit(1);
   }

   /* Load the program into the context */
   ret = spe_program_load(ctx, &spu_prime_handle);
   if (ret) {
      perror("spe_program_load");
      exit(1);
   }

   /* Run the program */
   entry_point = SPE_DEFAULT_ENTRY;
   ret = spe_context_run(ctx, &entry_point, 0,
      NULL, NULL, &stop_info);
   if (ret < 0) {
      perror("spe_context_run");
      exit(1);
   }

   /* Deallocate the context */
   ret = spe_context_destroy(ctx);
   if (ret) {
      perror("spe_context_destroy");
      exit(1);
   }

   return 0;
}

 

The makefile in the prime project compiles the full application with the following command:

ppu-gcc -o ppu_prime ppu_prime.c spu_prime-embed.o -lspe2

The result of the execution should look something like this:

PPU array location: 10019480
SPU Array Location: 10019480
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53

As shown, the .toe section in the CESOF enables the SPU to access its _EAR_prime array at the same address as the PPU’s prime array.

Accessing ELF Files in Code

The Cell build process incorporates sections of SPU-ELF files into PPU-ELF object files. You can perform similar operations on ELF sections and segments by calling functions in the ELF library, libelf. The usage of these functions is straightforward: create or read a file as an Elf object and then examine or modify its contents.

As an example, the code in Listing A.8 reads in an ELF executable (ppu_prime), accesses its ELF header, and displays the file type.

Example A.8. Displaying the Type of an ELF file: elf_simple.c

#include <err.h>
#include <gelf.h>
#include <fcntl.h>
#include <sysexits.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv) {
   int fd;
   Elf *efile;
   GElf_Ehdr ehdr;
   const char filename[] = "ppu_prime";

   /* Open the ELF file */
   if ((fd = open(filename, O_RDWR, 0)) < 0)
      err(EX_NOINPUT, "open \%s" failed", filename);

   /* Verify the ELF version */
   if (elf_version(EV_CURRENT) == EV_NONE)
      errx(EX_SOFTWARE, "Initialization failed: %s",
         elf_errmsg(-1));

   /* Create the Elf structure */
   if ((efile = elf_begin(fd, ELF_C_READ, NULL)) == NULL)
      errx(EX_SOFTWARE, "elf_begin() failed: %s.",
         elf_errmsg(-1));

   /* Get the ELF Header from the structure */
   if (gelf_getehdr(efile, &ehdr) == NULL)
      errx(EX_SOFTWARE, "getehdr() failed: %s.",
         elf_errmsg(-1));

   /* Display the file type */
   printf("%s: File type is ", filename);
   switch(ehdr.e_type) {
      case ET_NONE:
         printf("Unknown
");
      break;
      case ET_REL:
         printf("Relocatable
");
      break;
      case ET_EXEC:
         printf("Executable
");
      break;
      case ET_DYN:
         printf("Dynamic Library
");
      break;
      case ET_CORE:
         printf("Core file
");
      break;
      default:
         printf("Other
");
  }
  elf_end(efile);
  close(fd);
  return 0;
}

The central data structure is the Elf, which is created from the input file. With this structure, the Generic ELF (gelf) function, gelf_getehdr(), can acquire a GElf_Ehdr structure that contains the information within the file’s ELF header. This struct changes depending on whether the target system is 32 bit or 64 bit. On the 64-bit PPU, the GElf_Ehdr corresponds to the Elf64_Ehdr in the elf.h system header. Table A.3 presents the fields of this important data structure.

Table A.3. Elements of the Elf64_Ehdr Struct: ELF Header

Element Name

Element Type

Content

e_ident[16]

unsigned char

Magic numbers, ELF version, OS

e_type

Elf64_Half

File type (executable, relocatable, and so on)

e_machine

Elf64_Half

Target processor

e_version

Elf64_Word

File version (current/not current)

e_entry

Elf64_Addr

Virtual loading address

e_phoff

Elf64_Off

Offset of the program header table

e_shoff

Elf64_Off

Offset of the section header table

e_flags

Elf64_Word

Processor-specific information

e_ehsize

Elf64_Half

Size of the ELF header

e_phentsize

Elf64_Half

Size of a program header

e_phnum

Elf64_Half

Number of program headers

e_shentsize

Elf64_Half

Size of a section header

e_shnum

Elf64_Half

Number of section headers

e_shtrndx

Elf64_Half

Index of the section header string table

The e_shtrndx field is particularly useful because it allows you to access the file’s section header string table. This table matches section names to table indices, and thereby enables you to determine where section headers are located in the file. By looking at the section header, you can determine the offset of the actual section.

For example, suppose you want to access the .toc section in a relocatable ELF file. First, access the ELF header and find the offset of the section header table. Then, search through the table to find the index of the section header string table, e_shtrndx, and the size of each section header, e_shentsize. With this information, you can locate the actual section header string table, and this will tell you the number corresponding to the .toc section header. By accessing the .toc section header, you can determine the offset of the actual .toc section.

To obtain information inside a section header, you need to access the Elf64_Shdr structure. This contains all the data stored in the ELF section header, and Table A.4 lists its available fields.

Table A.4. Elements of the Elf64_Shdr Struct: Section Header

Element Name

Element Type

Content

sh_name

Elf64_Word

Name of the section

sh_type

Elf64_Word

Section type (PROGBITS, NOBITS, and so on)

sh_flags

Elf64_Xword

Info: allocable, executable, writeable, and so forth

sh_addr

Elf64_Addr

Memory location of the loaded section

sh_offset

Elf64_Off

Memory offset of the section in the ELF file

sh_size

Elf64_Xword

Amount of memory allocated for the section

sh_link

Elf64_Word

Type-dependent information

sh_info

Elf64_Word

Extra Information

sh_addralign

Elf64_Xword

Byte alignment within the file

sh_entsize

Elf64_Xword

Size of each table entry

These elements are useful for analyzing sections of ELF files, but can’t help you when you need to analyze segments—the building blocks of executable and dynamic library files. To access an ELF file’s segments, you need to access a Elf64_Phdr structure. This represents a program header and its fields are listed in Table A.5.

Table A.5. Elements of the Elf64_Shdr Struct: Section Header

Element Name

Element Type

Content

p_type

Elf64_Word

Type of segment (LOAD, DYNAMIC, and so on)

p_flags

Elf64_Word

Segment load info (executable, writeable, and so forth)

p_offset

Elf64_Off

Offset of the segment within the ELF file

p_vaddr

Elf64_Addr

Virtual address of the loaded segment

p_paddr

Elf64_Addr

Physical address of the loaded segment

p_filesz

Elf64_Xword

Size of the segment in the ELF file

p_memsz

Elf64_Xword

Size of the segment in memory

p_align

Elf64_Xword

Byte alignment of the segment in memory

By accessing the ELF’s program headers, you can examine how an application will be loaded into memory before it’s actually loaded. You can also change image addresses to reposition the application in memory.

Conclusion

The versatility of the ELF format has made it the standard for Linux object files, executables, and libraries. An object file is divided into sections that contain specific types of information. An executable organizes these sections into segments depending on how they should be loaded into memory during execution. A static library is just an archive of object files, but a dynamic library is a distinct type of ELF file and resembles both object files and executables.

The Cell SDK relies on ELF files to incorporate SPU objects within PPU executables. The process is straightforward. After the SPU code is compiled, ppu-embedspu embeds the object code into a CESOF (CBE Embedded SPE Object Format) file. This is a PPU object file that contains SPU code. It can be linked into PPU executables and libraries like a regular PPU object file.

After you’ve created an ELF file, you can use the ELF library to analyze and modify its content. This library relies on a central data structure, Elf, and uses a series of functions to access the file’s ELF header (GElf_Ehdr), section headers (GElf_Shdr), and program headers (GElf_Phdr). Each header field is represented by a separate variable, such as e_shtrndx, which stores the index of the section header string table.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.46.130