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.
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.
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:
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.
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.
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 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.
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.
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.
| Writable |
| Allocable (placed in memory during load) |
| Executable |
| Mergeable |
| Strings |
| Info |
| Link order |
| Group |
| Unknown |
| Operating system required |
| OS specific |
| 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.
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 | The program’s executable machine code |
| Block Started at Symbol | Uninitialized data (zeros) |
| Data | Initialized, writeable data (variables) |
| Read-Only Data | Read-only data (constants) |
| Table of Contents | Reference address for all data in object file |
| Official Procedure Descriptor | Addresses/ToC entry of functions |
| Global Offset Table | Absolute addresses to symbols |
| Procedure Linkage Table | Absolute addresses to functions |
| Java Class Registration | Compiler-specific instructions for Java classes |
| Initial Instructions | Routine called before |
| Final Instructions | Routine called after |
| Relocation Data for sec | Relocation data for named section |
| Comment | Version information for the ELF file |
| Section Header String Table | Names of each section in the object file |
| Exception Handling Frame | Contains information about processing exceptions |
| Symbol Table | All symbols used in the object file |
| Dynamic Symbol Table | Symbols used in the dynamic linking process |
| String Table | Contains strings used in the object file |
| Dynamic String Table | Strings used in the dynamic linking process |
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.
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:
ABS
: The symbol won’t change due to relocation.
COMMON
: The symbol will be allocated.
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.
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.
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.
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.
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 |
---|---|
| Program headers |
| The segment will be loaded into memory |
| Information needed to perform dynamic linking |
| OS-defined support information |
| Names the interpreter to be used for the executable |
| Not used |
| Exception-handling information |
| 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.
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.
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.
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!
The code in the third file, libcall.c, invokes both functions, hello
and goodbye
. This code is presented in Listing A.4.
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.
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.
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.
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.
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.
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.
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 int
s 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.
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
.
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.”
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.
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 Type | Content | |
---|---|---|
|
| Magic numbers, ELF version, OS |
|
| File type (executable, relocatable, and so on) |
|
| Target processor |
|
| File version (current/not current) |
|
| Virtual loading address |
|
| Offset of the program header table |
|
| Offset of the section header table |
|
| Processor-specific information |
|
| Size of the ELF header |
|
| Size of a program header |
|
| Number of program headers |
|
| Size of a section header |
|
| Number of section headers |
|
| 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 Type | Content | |
---|---|---|
|
| Name of the section |
|
| Section type (PROGBITS, NOBITS, and so on) |
|
| Info: allocable, executable, writeable, and so forth |
|
| Memory location of the loaded section |
|
| Memory offset of the section in the ELF file |
|
| Amount of memory allocated for the section |
|
| Type-dependent information |
|
| Extra Information |
|
| Byte alignment within the file |
|
| 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 |
---|---|---|
|
| Type of segment (LOAD, DYNAMIC, and so on) |
|
| Segment load info (executable, writeable, and so forth) |
|
| Offset of the segment within the ELF file |
|
| Virtual address of the loaded segment |
|
| Physical address of the loaded segment |
|
| Size of the segment in the ELF file |
|
| Size of the segment in memory |
|
| 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.
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.
3.15.182.159